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
+82
View File
@@ -1437,6 +1437,88 @@ protocol UploadConvertible: URLRequestConvertible {
// UploadConvertible extends URLRequestConvertible
expect(extendsRefs.find((r) => r.referenceName === 'URLRequestConvertible')).toBeDefined();
});
it('indexes Swift properties so they are findable: computed → property, stored → field, static → constant/variable (#1020)', () => {
const code = `
struct ReproConfig {
let reproStoredValue: Int
var reproComputedFlag: Bool {
reproStoredValue > 0
}
static let sharedLimit = 10
static var sharedCount = 0
func reproControlMethod() -> Bool {
reproComputedFlag
}
}
final class ReproService {
private let reproClassStored: String = "x"
var reproClassComputed: Int { reproClassStored.count }
}
`;
const result = extractFromSource('Repro.swift', code);
const byName = (name: string) => result.nodes.find((n) => n.name === name);
// Computed properties are the regression this fix targets: before #1020 they
// were dropped entirely, so search/explore returned nothing for them.
expect(byName('reproComputedFlag')?.kind).toBe('property');
expect(byName('reproClassComputed')?.kind).toBe('property');
// Stored instance properties stay `field` (fixed earlier in #708 — guard it).
expect(byName('reproStoredValue')?.kind).toBe('field');
expect(byName('reproClassStored')?.kind).toBe('field');
// `static let`/`static var` members remain shared constant/variable nodes.
expect(byName('sharedLimit')?.kind).toBe('constant');
expect(byName('sharedCount')?.kind).toBe('variable');
// The control method is unaffected.
expect(byName('reproControlMethod')?.kind).toBe('method');
});
it("attributes a computed property's getter calls to the property, not the type (SwiftUI body flow) (#1020)", () => {
const code = `
struct GreetingView {
let name: String
var body: some View {
let prefix = "Hi"
return VStack {
Text(greeting(prefix))
}
}
func greeting(_ p: String) -> String { p }
}
`;
const result = extractFromSource('View.swift', code);
const body = result.nodes.find((n) => n.kind === 'property' && n.name === 'body');
expect(body).toBeDefined();
// The getter's call to greeting() must originate from `body` (so a SwiftUI
// view's render flow is reachable through the property), not flatten onto the
// enclosing struct.
const callsFromBody = result.unresolvedReferences.filter(
(r) => r.fromNodeId === body!.id && r.referenceKind === 'calls'
);
expect(callsFromBody.some((r) => r.referenceName === 'greeting')).toBe(true);
// The getter is walked as a body, so a local declared inside it is NOT
// node-ified (locals are the data-flow frontier we leave uncovered). Before
// this fix the generic walker treated such a local as a struct `field`.
expect(result.nodes.find((n) => n.name === 'prefix')).toBeUndefined();
});
it('indexes a Swift protocol property requirement as a findable property (#1020)', () => {
const code = `
protocol Themable {
var accentColor: Color { get }
var title: String { get set }
}
`;
const result = extractFromSource('Themable.swift', code);
expect(result.nodes.find((n) => n.name === 'accentColor')?.kind).toBe('property');
expect(result.nodes.find((n) => n.name === 'title')?.kind).toBe('property');
});
});
describe('Kotlin Extraction', () => {