Просмотр исходного кода

feat(impact): Swift property-wrapper attribute arg type refs (Fluent @Siblings)

A type referenced only through a property-wrapper ARGUMENT — `@Siblings(through:
AcronymCategoryPivot.self, …)`, the Fluent many-to-many pivot/join model — was
orphaned: the wrapper type (`Siblings`) and the property's declared type were
captured, but the attribute's argument expressions were never walked, so a model
reached solely through a relationship looked like nothing depended on it. Route
the property's attribute args through extractStaticMemberRef (which already
handles Swift `Type.self` navigation, self-filtering to capitalized receivers and
skipping the `\.$keypath` args).

vapor-til 94.7% -> 100% (the AcronymCategoryPivot model, referenced only via
@Siblings, now resolves). Additive A/B (same faircov): vapor-til +1, Alamofire
neutral (44/50); cross-family false edges 0. Regression test fails without the fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Colby McHenry 2 недель назад
Родитель
Сommit
bb7659e
3 измененных файлов с 56 добавлено и 0 удалено
  1. 1 0
      CHANGELOG.md
  2. 36 0
      __tests__/extraction.test.ts
  3. 19 0
      src/extraction/tree-sitter.ts

+ 1 - 0
CHANGELOG.md

@@ -29,6 +29,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 - Rust cross-module function calls now resolve to the right file. A call to a sibling submodule's function — `users::router()`, the common router-assembly / handler-registration pattern where `mod users;` makes `users` a child of the current module — is now resolved relative to the current module, not only the crate root. Deeper module-path calls (`database::profiles::find()` — the `db.run(|c| …)` data-access shape) now resolve too; these were being discarded before resolution even ran, because the path's leaf function name was never checked. Previously such a call linked to nothing, so a module reached only as `module::path::function()` looked like it had no dependents; a web app wired this way (Axum, Rocket, and similar) now surfaces its handler and data-access modules' real callers. (Rust)
 - Rocket route handlers now connect to where they're mounted. A handler registered in a `routes![a::b::handler, …]` or `catchers![…]` macro used to be invisible — the macro body is a raw token tree, so the handler looked like it had no caller (Rocket mounts it at runtime) and its file showed no dependents. The handler paths are now read out of the macro and linked to the `mount`/`register` call, so editing a Rocket handler surfaces its route registration and a routes module is no longer reported as unused. (Rust, Rocket)
 - Swift property wrappers and attributes are now connected. A `@Argument` / `@Published` / `@State` / custom `@propertyWrapper` on a property — and attributes on types, methods, and functions (`@objc`, `@MainActor`, …) — now record a dependency on the wrapper/attribute type. Previously these were dropped entirely (Swift attributes parse differently from other languages, and stored properties weren't being inspected), so the wrapper type looked unused and the file using it depended on nothing — a big gap for SwiftUI and argument-parser-style code.
+- Swift Fluent relationship models are no longer orphaned. A type referenced only through a property-wrapper *argument* — `@Siblings(through: AcronymCategoryPivot.self, …)`, the many-to-many pivot/join model — now records a dependency on that type. Previously only the wrapper itself (`Siblings`) and the property's declared type were captured, so a pivot model reached solely through the relationship looked like nothing depended on it and editing it surfaced no impact. (Swift, Vapor/Fluent)
 - Java annotations are now connected. Annotation definitions (`@interface Foo`) are indexed as types, and every `@Foo` usage on a class, method, or field is recorded as a dependency on it. Previously neither side was captured — annotation usages were dropped (they live inside the declaration's modifiers) and `@interface` types weren't indexed at all — so annotation-driven code (Spring `@GetMapping`, JPA `@Entity`, Gson `@SerializedName`, …) showed the annotation as having no users and the annotated class as not depending on it.
 - Kotlin Multiplatform `expect`/`actual` declarations are now connected. A platform implementation — `actual fun`, `actual class`, or an `actual typealias` in a `jvm` / `native` / `js` / `wasm` source set — is linked to the common `expect` declaration it fulfills (including the common case of an `expect class` fulfilled by an `actual typealias`). Previously a caller in common code resolved to the `expect` declaration, so every platform `actual` looked like it had no dependents and editing one showed an empty blast radius; now changing a platform implementation surfaces the common API and everything that uses it. (Kotlin)
 - Scala impact and `codegraph affected` now connect the type graph that typeclass-style code is built on. A parameterized supertype (`trait Monoid[A] extends Semigroup[A] with Serializable`) now links to each parent; a type used in a `val`/`def` signature, as a type argument, or as a context bound (`def f[A: Monoid]`) — including the trailing implicit parameter list (`(implicit M: Monoid[A])`) where typeclass instances are passed — now records a dependency; and `new T[...] { … }` counts as an instantiation. Previously Scala linked only plain calls and bare, non-generic supertypes, so a trait extended with type parameters, used as a type, or required as an implicit looked like nothing depended on it — which on a typeclass-heavy codebase (cats, algebra) was most of the graph. (Scala)

+ 36 - 0
__tests__/extraction.test.ts

@@ -4317,6 +4317,42 @@ describe('Rust module-path call resolution', () => {
   });
 });
 
+describe('Swift property-wrapper attribute type references', () => {
+  let tempDir: string;
+  let cg: CodeGraph;
+
+  beforeEach(() => {
+    tempDir = createTempDir();
+  });
+
+  afterEach(() => {
+    if (cg) cg.close();
+    if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
+  });
+
+  it('a Fluent `@Siblings(through: Pivot.self)` links the model to the pivot type', async () => {
+    // A many-to-many pivot/join model is referenced ONLY through the relationship
+    // property wrapper's metatype argument (`Pivot.self`), never by a controller
+    // query. The wrapper type was captured but the argument expression wasn't
+    // walked, so the pivot model looked like nothing depended on it.
+    fs.writeFileSync(path.join(tempDir, 'Pivot.swift'),
+      `import Fluent\nfinal class AcronymCategoryPivot: Model {\n  static let schema = "acronym-category"\n}\n`);
+    fs.writeFileSync(path.join(tempDir, 'Acronym.swift'),
+      `import Fluent\nfinal class Acronym: Model {\n` +
+      `  @Siblings(through: AcronymCategoryPivot.self, from: \\.$acronym, to: \\.$category)\n` +
+      `  var categories: [Category]\n}\n`);
+
+    cg = CodeGraph.initSync(tempDir);
+    await cg.indexAll();
+    cg.resolveReferences();
+
+    const pivot = cg.getNodesByKind('class').find((n) => n.name === 'AcronymCategoryPivot');
+    expect(pivot, 'pivot model class').toBeDefined();
+    const deps = [...cg.getImpactRadius(pivot!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
+    expect(deps.some((p) => p.endsWith('Acronym.swift')), '@Siblings metatype arg links Acronym to the pivot').toBe(true);
+  });
+});
+
 describe('Objective-C messages, class receivers, and #import', () => {
   let tempDir: string;
   let cg: CodeGraph;

+ 19 - 0
src/extraction/tree-sitter.ts

@@ -453,6 +453,25 @@ export class TreeSitterExtractor {
       if (ownerId) {
         this.extractDecoratorsFor(node, ownerId);
         this.extractVariableTypeAnnotation(node, ownerId);
+        // Fluent / SwiftUI property-wrapper attributes often reference a model or
+        // type by metatype in their ARGUMENTS — `@Siblings(through: Pivot.self,
+        // …)`, `@Group(…)`. extractDecoratorsFor captures the wrapper type
+        // (`Siblings`); this pulls the TYPE out of the argument expressions
+        // (`Pivot.self` → a dependency on Pivot), so a model reached ONLY through
+        // a relationship (a many-to-many pivot/join model) isn't left orphaned.
+        // extractStaticMemberRef self-filters to `Type.member` navigation, so the
+        // `\.$keypath` arguments and the wrapper `user_type` are skipped.
+        const modifiers = node.namedChildren.find((c: SyntaxNode) => c.type === 'modifiers');
+        if (modifiers) {
+          const walkAttrArgs = (n: SyntaxNode): void => {
+            this.extractStaticMemberRef(n);
+            for (let i = 0; i < n.namedChildCount; i++) {
+              const c = n.namedChild(i);
+              if (c) walkAttrArgs(c);
+            }
+          };
+          walkAttrArgs(modifiers);
+        }
       }
     }
     // `export_statement` itself is not extracted — the walker descends