Procházet zdrojové kódy

feat(impact): Swift property wrappers & attributes — link @Argument/@Published/etc.

Swift is otherwise healthy (init calls -> instantiates, protocol conformances ->
implements). The systematic gap was property wrappers / attributes:

- A Swift `@X` parses as `property_declaration -> modifiers -> attribute ->
  user_type`. `extractDecoratorsFor` didn't recognize the `attribute` node type
  (only decorator/annotation/marker_annotation) nor extract the name from its
  `user_type` child, so every Swift attribute usage was dropped.
- Swift stored properties inside a type aren't extracted as their own nodes, so
  the decorator helper never ran on them. Added a dispatcher branch that runs
  extractDecoratorsFor + type-annotation extraction on Swift property declarations
  inside a type.

Now `@Argument`/`@Option`/`@Published`/`@State`/custom `@propertyWrapper` usages
(and `@objc`/`@MainActor` on types/methods) link to the wrapper/attribute type.
Alamofire 93%->95.3%, swift-argument-parser 84.6%->96.2%. Reuses the same
extractDecoratorsFor path as the Java annotation fix, unifying
annotation/attribute/property-wrapper handling across Java/C#/Kotlin/Swift. New
test + CHANGELOG; full suite green (1151).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Colby McHenry před 2 týdny
rodič
revize
d111f26
3 změnil soubory, kde provedl 48 přidání a 4 odebrání
  1. 1 0
      CHANGELOG.md
  2. 19 0
      __tests__/extraction.test.ts
  3. 28 4
      src/extraction/tree-sitter.ts

+ 1 - 0
CHANGELOG.md

@@ -19,6 +19,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 - Blast radius, callers, and `codegraph affected` now recognize far more of the dependencies that were already in your code. A symbol now counts as a dependency whether it's called, used only in a type annotation inside a function body (`const items: Foo[] = []`), imported and placed in a registry array or passed as an argument, used as a JSX component, simply re-exported from a barrel (`export { X } from './x'`), or pulled in as a namespace (`import * as ns from '@/x'`) — including through tsconfig path aliases like `@/`. Previously only called, instantiated, or signature-typed symbols created a cross-file link, so a file that used a dependency in any other way could look like it depended on nothing — and the file that defined a widely-used symbol could look like nothing depended on it. The graph still indexes exactly the same symbols; it just connects the ones that were already there. (TypeScript/JavaScript)
 - The same completeness fix now applies to **Python**: a name brought in with `from module import X` is recorded as a dependency on that module even when `X` is only stored in a list/dict, passed as an argument, used as a decorator, or re-exported through an `__init__.py`. Previously Python linked only imports that were called or instantiated, so a module consumed purely by value — or only re-exported — looked like nothing depended on it.
 - Rust impact and `codegraph affected` now connect far more of the module graph. Struct literals (`Widget { n: 1 }`) are recorded as instantiations; a `use` / `pub use` brings its item into the dependency graph — so a `pub use` re-export hub (a `mod.rs` re-exporting its submodules) depends on the modules it re-exports — resolved by Rust module path (`crate::`/`self::`/`super::`), so a re-export of a common name like `read` links to the right module instead of a same-named symbol elsewhere; and trait dispatch reaches implementations — a struct whose methods cover a trait's is treated as implementing it, and a call through `&dyn Trait` resolves to the concrete method. Previously a Rust type linked only when called or used in a type position, so structs built by literal, modules surfaced only through `pub use`, and trait-only implementations looked like they had no dependents. (#584 for Rust traits)
+- 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.
 - 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.
 - C# `record` types are now indexed. `record`, `record class`, and `record struct` declarations (everywhere in modern C# — DTOs, value objects, CQRS messages, MediatR notifications) were previously skipped entirely, so every reference, generic type argument (`IEnumerable<MyRecord>`), and `new MyRecord(...)` pointed at nothing and the file defining a record looked like it had no callers or dependents. (#237)
 - Go interfaces now connect to their implementations. Go has no `implements` keyword — a type satisfies an interface just by having the right methods — so CodeGraph now infers that link: a struct whose methods cover an interface's method set is treated as implementing it, and a call through the interface (`API.Marshal(...)`) reaches every concrete implementation. This means a type used only via an interface (the common plugin/strategy pattern — e.g. JSON-codec or renderer implementations selected at runtime) is no longer reported as having no callers or no dependents, and impact now flows from an interface method to the implementations behind it. (#584)

+ 19 - 0
__tests__/extraction.test.ts

@@ -4880,3 +4880,22 @@ describe('Java annotations (blast-radius recall)', () => {
     } finally { cleanupTempDir(dir); }
   });
 });
+
+describe('Swift property wrappers / attributes (blast-radius recall)', () => {
+  it('links a @propertyWrapper usage to the wrapper type', async () => {
+    const dir = createTempDir();
+    try {
+      fs.mkdirSync(path.join(dir, 'Sources', 'M'), { recursive: true });
+      fs.writeFileSync(path.join(dir, 'Sources', 'M', 'Wrap.swift'), `@propertyWrapper\npublic struct Argument<T> { public var wrappedValue: T }\n`);
+      // `@Argument` is a Swift attribute on a stored property — it lives in the
+      // property's `modifiers` and Swift doesn't extract instance properties as
+      // their own nodes, so without the fix the wrapper type has no users.
+      fs.writeFileSync(path.join(dir, 'Sources', 'M', 'Cmd.swift'), `public struct MyCommand {\n  @Argument var name: String\n  @Argument var count: Int\n}\n`);
+      const cg = CodeGraph.initSync(dir, { config: { include: ['Sources/**/*.swift'], exclude: [] } });
+      await cg.indexAll();
+      cg.resolveReferences();
+      expect(cg.getFileDependents('Sources/M/Wrap.swift')).toContain('Sources/M/Cmd.swift');
+      cg.destroy();
+    } finally { cleanupTempDir(dir); }
+  });
+});

+ 28 - 4
src/extraction/tree-sitter.ts

@@ -363,6 +363,23 @@ export class TreeSitterExtractor {
       this.extractVariable(node);
       skipChildren = true; // extractVariable handles children
     }
+    // Swift stored properties inside a type. Swift instance properties aren't
+    // extracted as their own nodes, but a property's PROPERTY WRAPPER
+    // (`@Argument`/`@Published`/`@State`/custom) and declared type ARE
+    // dependencies — attribute them to the enclosing type so the wrapper/type
+    // files get dependents. Don't skipChildren: an initializer's calls still
+    // matter. (Other languages extract properties via property/field types.)
+    else if (
+      this.language === 'swift' &&
+      nodeType === 'property_declaration' &&
+      this.isInsideClassLikeNode()
+    ) {
+      const ownerId = this.nodeStack[this.nodeStack.length - 1];
+      if (ownerId) {
+        this.extractDecoratorsFor(node, ownerId);
+        this.extractVariableTypeAnnotation(node, ownerId);
+      }
+    }
     // `export_statement` itself is not extracted — the walker descends
     // into children, where the inner declaration (lexical_declaration,
     // function_declaration, class_declaration, etc.) is dispatched to
@@ -2316,12 +2333,14 @@ export class TreeSitterExtractor {
     const consider = (n: SyntaxNode | null): void => {
       if (!n) return;
       // `marker_annotation` is Java's grammar for arg-less annotations
-      // (`@Override`, `@Deprecated`); without including it, every
-      // such Java annotation would be silently skipped.
+      // (`@Override`, `@Deprecated`); `attribute` is Swift's grammar for
+      // attributes and PROPERTY WRAPPERS (`@objc`, `@Argument`, `@Published`,
+      // `@State`). Without these, those usages would be silently skipped.
       if (
         n.type !== 'decorator' &&
         n.type !== 'annotation' &&
-        n.type !== 'marker_annotation'
+        n.type !== 'marker_annotation' &&
+        n.type !== 'attribute'
       ) {
         return;
       }
@@ -2340,7 +2359,9 @@ export class TreeSitterExtractor {
           child.type === 'identifier' ||
           child.type === 'member_expression' ||
           child.type === 'scoped_identifier' ||
-          child.type === 'navigation_expression'
+          child.type === 'navigation_expression' ||
+          child.type === 'user_type' ||      // swift attribute → user_type (`@Argument`)
+          child.type === 'type_identifier'
         ) {
           target = child;
           break;
@@ -2348,8 +2369,11 @@ export class TreeSitterExtractor {
       }
       if (!target) return;
       let name = getNodeText(target, this.source);
+      const lt = name.indexOf('<'); // strip generic args: `@Argument<T>` → `Argument`
+      if (lt > 0) name = name.slice(0, lt);
       const lastDot = Math.max(name.lastIndexOf('.'), name.lastIndexOf('::'));
       if (lastDot >= 0) name = name.slice(lastDot + 1).replace(/^[:.]/, '');
+      name = name.trim();
       if (!name) return;
       this.unresolvedReferences.push({
         fromNodeId: decoratedId,