Skip to content

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
Tencent:mainfrom
Dev-next-gen:fix/swift-member-shadows-module-fallback
Open

Dev-next-gen wants to merge 1 commit into
Tencent:mainfrom
Dev-next-gen:fix/swift-member-shadows-module-fallback

Conversation

@Dev-next-gen

Copy link
Copy Markdown
Contributor

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 extension in a third file, or a protocol default implementation anywhere in the module puts it in scope. The module-scope fallback in call-resolver.ts 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 reference each other.

On these three files of one target:

// Sources/App/Base.swift
class Base { func work() -> Int { return 1 } }

// Sources/App/Sub.swift
class Sub: Base { func run() -> Int { return work() } }

// Sources/App/Global.swift
func work() -> Int { return 2 }

extractStructuralGraphAsFacts emits Sources/App/Sub.swift -REFERENCES-> Sources/App/Global.swift, confidence INFERRED. work() is the member Sub inherits from Base; Global.swift has nothing to do with that call, and the edge lands in the generated graph as a fact. Replacing the inheritance with extension 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. swiftModuleDeclaresMember then 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

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature causing existing behavior to change)
  • Documentation only
  • Refactor / internal cleanup

Test Plan

Three tests in src/__tests__/ast-swift-module-scope.test.ts, all going through extractStructuralGraphAsFacts and the real WASM grammar rather than the resolver in isolation:

  • does not resolve a call to a member the enclosing type inherits
  • does not resolve a call to a member an extension in another file adds
  • still resolves a call no type in the module declares as a member — the control: with no member named work, the module-level function is the only candidate and the edge still stands.

On main the first two fail with expected 'Sources/App/Global.swift' to be undefined, which is the wrong edge itself; the control passes. After the change all three pass.

  • npx tsc --noEmit passes
  • npm run lint passes (oxlint --type-aware --deny-warnings over src/wiki-engine/code-knowledge/ast)
  • npx vitest run passes — 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.
  • Added/updated tests for the change

Related Issues

Closes #909

Notes for Reviewers

  • The member-name set is module-wide on purpose, not per type. It is the cheap approximation Swift: an unqualified call to an inherited or extension member resolves to an unrelated top-level function #909 proposed; the cost is recall, and the swiftModuleDeclaresMember doc comment says where that cost falls.
  • isSwiftTypeMember keys on the parent body node — class_body for classes, structs, actors and extensions, enum_class_body for enums, protocol_body for 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 under statements and is deliberately not counted, since nothing outside that body can be referring to it.
  • Private members are collected too. A private func is 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.

…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Swift: an unqualified call to an inherited or extension member resolves to an unrelated top-level function

1 participant