fix(extraction): index Swift computed properties so they're findable (#1020) (#1024)

Swift in-class properties are extracted by a dedicated branch in
TreeSitterExtractor.visitNode, not the generic nameField/variableTypes path
swift.ts declares. That branch had a `!isComputed` gate that dropped computed
properties entirely, so `codegraph query`/`codegraph_explore` returned "No
results found" for them — including a SwiftUI view's `var body: some View`,
the most important symbol in any SwiftUI app, and the heavily-read
`var isCloudProxy: Bool` from the report.

Stored properties were already fixed in #708 (v1.0.0); the reporter tested
v0.9.9 and confirmed "still present on main" by inspecting swift.ts only,
missing the dedicated branch — so only the computed-property half was real.

- Computed properties now index as `property` nodes; the getter is walked via
  visitFunctionBody so its calls attribute to the property (a SwiftUI `body`'s
  subview tree becomes the property's callees — the render flow is traceable
  through it), not flattened onto the enclosing type.
- Protocol property requirements (`var x: T { get }`) — a third never-indexed
  category — index as `property` too.
- Routing the getter through visitFunctionBody also stops getter-local
  `let`/`var` declarations from being wrongly node-ified as struct fields
  (the generic child-walk used to do this): Alamofire property 0→348, field
  618→588, idempotent.

Stored/static behavior is unchanged.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-27 14:21:00 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 30dc303f4c
commit b3f59c717a
3 changed files with 132 additions and 18 deletions
+49 -18
View File
@@ -986,32 +986,46 @@ export class TreeSitterExtractor {
this.scanFnRefSubtree(node, 0);
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.)
// Swift properties inside a type. A stored instance property becomes a `field`
// node; a `static let`/`static var` member becomes `constant`/`variable`
// (Swift's `static`-namespacing idiom — value-reference edges can then target
// it); a COMPUTED property (getter block, no stored value) becomes a `property`
// node whose getter is walked below so its calls attribute to it. A property's
// PROPERTY WRAPPER (`@Argument`/`@Published`/`@State`/custom) and declared type
// are dependencies attributed to the enclosing type. (Other languages extract
// properties via property/field types.)
else if (
this.language === 'swift' &&
nodeType === 'property_declaration' &&
(nodeType === 'property_declaration' || nodeType === 'protocol_property_declaration') &&
this.isInsideClassLikeNode()
) {
const ownerId = this.nodeStack[this.nodeStack.length - 1];
// A `static let`/`static var` member is a SHARED constant of the type
// (Swift's `static`-namespacing idiom, esp. in `enum`/`struct`) — extract
// it as `constant`/`variable` so value-reference edges can target it. An
// instance stored property stays a `field` (per-instance; Swift instance
// properties otherwise aren't own nodes — that's unchanged). A *computed*
// property (getter, no stored value) is never a constant — skip the node.
const { nameNode, isLet, isComputed } = swiftPropertyInfo(node, this.source);
if (nameNode && !isComputed) {
const isStatic = this.extractor.isStatic?.(node) ?? false;
this.createNode(isStatic ? (isLet ? 'constant' : 'variable') : 'field',
getNodeText(nameNode, this.source), node, {
let computedPropId: string | undefined;
if (nameNode) {
if (isComputed) {
// Computed property — accessed like a property but its getter holds real
// logic. Index as `property` so search/explore find it (#1020: computed
// props such as a heavily-read `var isCloudProxy: Bool` returned "No
// results found"); pushed below so the getter's calls attribute to it
// rather than flattening onto the owning type (SwiftUI `var body: some
// View { … }` — the whole subview tree — is the canonical case).
const prop = this.createNode('property', getNodeText(nameNode, this.source), node, {
visibility: this.extractor.getVisibility?.(node),
isStatic,
isStatic: this.extractor.isStatic?.(node) ?? false,
});
computedPropId = prop?.id;
} else {
// A `static let`/`static var` member is a SHARED constant of the type
// (esp. in `enum`/`struct`); an instance stored property stays a `field`
// (per-instance — Swift instance properties otherwise aren't own nodes).
const isStatic = this.extractor.isStatic?.(node) ?? false;
this.createNode(isStatic ? (isLet ? 'constant' : 'variable') : 'field',
getNodeText(nameNode, this.source), node, {
visibility: this.extractor.getVisibility?.(node),
isStatic,
});
}
}
if (ownerId) {
this.extractDecoratorsFor(node, ownerId);
@@ -1036,6 +1050,23 @@ export class TreeSitterExtractor {
walkAttrArgs(modifiers);
}
}
// A computed property's getter holds real logic — walk it with the property
// node pushed so its calls/instantiations attribute to the property (a
// SwiftUI `body`'s subview tree becomes the property's callees). skipChildren
// then stops the generic walker from re-walking the getter (and the
// modifiers/type annotation already handled above).
if (computedPropId) {
const getter = node.namedChildren.find(
(c: SyntaxNode) =>
c.type === 'computed_property' || c.type === 'protocol_property_requirements',
);
if (getter) {
this.nodeStack.push(computedPropId);
this.visitFunctionBody(getter, '');
this.nodeStack.pop();
}
skipChildren = true;
}
}
// `export_statement` itself is not extracted — the walker descends
// into children, where the inner declaration (lexical_declaration,