feat(resolution): inherited this.X, Java/Kotlin cross-file method refs, Swift type scoping (#810)
Three callback-registration shapes deferred from #756/#808, one arc: 1. INHERITED this.X (TS/JS + every this.-routed language): a `this.<member>` registration whose member isn't on the enclosing class defers to a second pass (resolveDeferredThisMemberRefs — in-memory like deferredChainRefs, runs after implements/extends edges persist, same lifecycle as the #750 conformance pass) and resolves up the supertype chain, depth-capped BFS, validated targets only. `bus.on("submit", this.handleSubmit)` in a subclass links to FormBase::handleSubmit; same-named methods on unrelated classes never match. this.-prefixed candidates skip the extraction name gate (an inherited member can't be in definedHere). 2. JAVA/KOTLIN qualified method refs: `Handlers::onMessage` / `OtherClass::handle` emit QUALIFIED names resolved by the scoped suffix-matcher — cross-file capable, gated on the scope name being a same-file type or an imported name (dotted JVM imports now contribute their last segment). `this::m` and `super::m` route through the class-scoped resolver (super rides the supertype pass). References through a VARIABLE (`subscriber::onNext`) deliberately produce nothing — receiver type is unknowable; RxJava's baseline bare capture was resolving these to same-named same-file methods (a test method "registering" an anonymous class's onNext) — the rework drops 18 such wrong edges and keeps the 7 genuine Type::method refs RxJava's main tree actually has. 3. SWIFT enclosing-type scoping (implicit self): bare callback names match methods only of the from-symbol's own type (extension/nested scopes reconciled by suffix), and top-level code never matches methods. Alamofire: −44 wrong edges (parameters like `request`/`data`/`retrier` resolving to same-named methods on unrelated protocols), all verified; the same-class param collision (`task`) remains and is documented. New ResolutionContext.getNodeById lets matchers derive the from-symbol's class scope. Controls: redis/fmt fnref edges byte-identical; excalidraw stable; typeorm +4 genuine inherited-getter dependencies; zero calls edges changed on any of 7 A/B repos; nodes identical everywhere. Kotlin companion-object members extract unqualified (pre-existing) so `Type::companionFn` stays silent rather than guessing — documented. Full suite 1389 passed. EXTRACTION_VERSION 20 → 21 (re-index to benefit). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
38eb4e688c
commit
38095aa95b
@@ -435,19 +435,34 @@ export class TreeSitterExtractor {
|
||||
if (isGeneratedFile(this.filePath)) return;
|
||||
|
||||
const definedHere = new Set<string>();
|
||||
const definedTypes = new Set<string>();
|
||||
for (const n of this.nodes) {
|
||||
if (n.kind === 'function' || n.kind === 'method') definedHere.add(n.name);
|
||||
if (
|
||||
n.kind === 'class' || n.kind === 'struct' || n.kind === 'interface' ||
|
||||
n.kind === 'enum' || n.kind === 'trait' || n.kind === 'protocol'
|
||||
) {
|
||||
definedTypes.add(n.name);
|
||||
}
|
||||
}
|
||||
|
||||
// Import-binding names only (all binding emitters push kind 'imports').
|
||||
// Deliberately NOT 'references': those carry type-annotation and
|
||||
// interface-member names, which let local variables that share a type
|
||||
// member's name slip through the gate (excalidraw A/B finding).
|
||||
// member's name slip through the gate (excalidraw A/B finding). A dotted
|
||||
// import (JVM `import com.example.OtherClass`) also contributes its LAST
|
||||
// segment — the simple name Java/Kotlin code uses in `OtherClass::method`
|
||||
// references.
|
||||
const SIMPLE_NAME = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
||||
const DOTTED_NAME = /^[A-Za-z_$][A-Za-z0-9_$.]*\.([A-Za-z_$][A-Za-z0-9_$]*)$/;
|
||||
const importedNames = new Set<string>();
|
||||
for (const r of this.unresolvedReferences) {
|
||||
if (r.referenceKind === 'imports' && SIMPLE_NAME.test(r.referenceName)) {
|
||||
if (r.referenceKind !== 'imports') continue;
|
||||
if (SIMPLE_NAME.test(r.referenceName)) {
|
||||
importedNames.add(r.referenceName);
|
||||
} else {
|
||||
const dotted = r.referenceName.match(DOTTED_NAME);
|
||||
if (dotted) importedNames.add(dotted[1]!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,21 +483,37 @@ export class TreeSitterExtractor {
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
// C-family file-scope initializers skip the gate (constant-expression
|
||||
// context — a bare identifier there is a function address, never a
|
||||
// variable; see FnRefSpec.ungatedModes). Local initializers and
|
||||
// everything else require a same-file/import match.
|
||||
const skipGate = ungated?.has(c.mode) === true && atFileScope;
|
||||
// Qualified C++ member-pointers (`Widget::on_click`) and TS/JS
|
||||
// `this.<member>` candidates gate on the member name; everything else
|
||||
// on the full name.
|
||||
const gateName = c.name.startsWith('this.')
|
||||
? c.name.slice(5)
|
||||
: c.name.includes('::')
|
||||
? c.name.slice(c.name.lastIndexOf('::') + 2)
|
||||
: c.name;
|
||||
if (!skipGate && !definedHere.has(gateName) && !importedNames.has(gateName)) {
|
||||
continue;
|
||||
// Gate policy by candidate shape:
|
||||
// - `this.<member>`: ALWAYS flush — the member may be inherited from a
|
||||
// class in another file (definedHere can't see it), volume is
|
||||
// naturally bounded by real `this.X` expressions, and resolution is
|
||||
// strictly class-scoped (own members or the validated supertype
|
||||
// pass), so nothing fuzzy can leak.
|
||||
// - `Scope::member` (C++ member-pointers, Java/Kotlin type-qualified
|
||||
// method refs): the SCOPE name must be a type defined here or an
|
||||
// imported name (covers `OtherClass::method` cross-file), or the
|
||||
// member matches the plain gate (back-compat for C++ same-file).
|
||||
// - C-family file-scope initializers skip the gate entirely
|
||||
// (constant-expression context — see FnRefSpec.ungatedModes).
|
||||
// - everything else: name ∈ same-file functions/methods ∪ imports.
|
||||
if (!c.name.startsWith('this.')) {
|
||||
const skipGate = ungated?.has(c.mode) === true && atFileScope;
|
||||
if (!skipGate) {
|
||||
if (c.name.includes('::')) {
|
||||
const scopeName = c.name.slice(0, c.name.indexOf('::'));
|
||||
const memberName = c.name.slice(c.name.lastIndexOf('::') + 2);
|
||||
if (
|
||||
!definedTypes.has(scopeName) &&
|
||||
!importedNames.has(scopeName) &&
|
||||
!definedHere.has(memberName) &&
|
||||
!importedNames.has(memberName)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
} else if (!definedHere.has(c.name) && !importedNames.has(c.name)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
const key = `${c.fromNodeId}|${c.name}`;
|
||||
if (seen.has(key)) continue;
|
||||
|
||||
Reference in New Issue
Block a user