fix(code-knowledge): do not resolve a Swift bare call a type member shadows - #962
Open
Dev-next-gen wants to merge 1 commit into
Open
Dev-next-gen wants to merge 1 commit into
Dev-next-gen wants to merge 1 commit into
Conversation
…hadows An unqualified call inside a Swift type runs that type's member when one is named after the callee, and the member can come from a superclass, an `extension` in a third file or a protocol default implementation. The module-scope fallback knows only the module's top-level declarations, so it saw a same-named top-level `func` as the single candidate and claimed it, emitting a REFERENCES edge between two files that never call each other. Answering this per type would need the module's inheritance and conformance graph, which this layer does not build. Asking it of the module instead -- does any type declare this name as a member? -- needs only names already extracted, and fails the way the rest of the layer fails: a call that really was the module-level one loses its edge when an unrelated type shares the name, and a missing edge still surfaces as a gap. The walker already had the declaration node in hand, so members are collected there and carried into the module index alongside the module-visible declarations. The receiver fallback below has the same shape and is left alone: a receiver is a type name, and a member named like a type is a different case that would need its own evidence.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
An unqualified call inside a Swift type runs that type's member when one is named after the callee. The member does not have to be declared in the same file: a superclass, an
extensionin a third file, or a protocol default implementation anywhere in the module puts it in scope. The module-scope fallback incall-resolver.tsknows only the module's top-level declarations, so it saw a same-named top-levelfuncas the single candidate and claimed it — emitting aREFERENCESedge between two files that never reference each other.On these three files of one target:
extractStructuralGraphAsFactsemitsSources/App/Sub.swift -REFERENCES-> Sources/App/Global.swift, confidenceINFERRED.work()is the memberSubinherits fromBase;Global.swifthas nothing to do with that call, and the edge lands in the generated graph as a fact. Replacing the inheritance withextension Sub { func work() ... }in a third file gives the same wrong edge.Answering "is this callee a member of the enclosing type?" per type would need the module's inheritance and conformance graph, which this layer does not build. Asking it of the module instead — does any type in the module declare this name as a member? — needs only names that are already extracted, and errs the way the rest of this layer errs: a call that really was the module-level one loses its edge when some unrelated type happens to share the name. A missing edge still surfaces as a gap; an invented one is read as a fact. That is the direction #909 suggested, and it is what this does.
The walker already has the declaration node in hand when it decides
isSwiftModuleVisible, so members are collected at the same point and carried into the module index beside the module-visible declarations.swiftModuleDeclaresMemberthen guards the bare-callee fallback alongside the enclosing-scope bindings that were already there — the two are the same rule applied at two scopes.The receiver fallback further down has the same shape, and I left it alone: a receiver matches a class declaration, so the case would be a member named like a type, which is different enough to need its own evidence rather than being folded in here.
Type of Change
Test Plan
Three tests in
src/__tests__/ast-swift-module-scope.test.ts, all going throughextractStructuralGraphAsFactsand the real WASM grammar rather than the resolver in isolation:does not resolve a call to a member the enclosing type inheritsdoes not resolve a call to a member an extension in another file addsstill resolves a call no type in the module declares as a member— the control: with no member namedwork, the module-level function is the only candidate and the edge still stands.On
mainthe first two fail withexpected 'Sources/App/Global.swift' to be undefined, which is the wrong edge itself; the control passes. After the change all three pass.npx tsc --noEmitpassesnpm run lintpasses (oxlint --type-aware --deny-warningsoversrc/wiki-engine/code-knowledge/ast)npx vitest runpasses — I ran the four test files this touches rather than the whole suite:ast-swift-module-scope.test.ts,ast-extract-swift.test.ts,ast-extract.test.ts,swift-extractor.test.ts, 73 tests, all green.Related Issues
Closes #909
Notes for Reviewers
swiftModuleDeclaresMemberdoc comment says where that cost falls.isSwiftTypeMemberkeys on the parent body node —class_bodyfor classes, structs, actors and extensions,enum_class_bodyfor enums,protocol_bodyfor protocol requirements. I checked those three against the grammar by dumping the ancestor chain of every declaration in a fixture covering class, subclass, extension, protocol, enum, struct, nested type and a function local to another function; the local function sits understatementsand is deliberately not counted, since nothing outside that body can be referring to it.private funcis not reachable from a sibling file, but it is still what a bare call inside its own type means, which is the question this set answers.Found by a defect-hunting pipeline I build and run (Dev-next-gen), using Claude Code with Anthropic's Claude Opus 5.