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
@@ -553,35 +553,66 @@ function normalizeSpecial(
|
||||
source: string
|
||||
): Array<{ name: string; node: SyntaxNode }> {
|
||||
switch (type) {
|
||||
// Java `Main::targetCb` / `this::run0` — last identifier child is the method.
|
||||
// Java method references. Receiver decides the resolution route (#808):
|
||||
// `this::run0` / `super::close` → `this.<m>` (class-scoped resolver;
|
||||
// super rides the inherited-member supertype pass)
|
||||
// `Type::method` (capitalized) → qualified `Type::method` (suffix-
|
||||
// matched against that type's members, cross-file capable)
|
||||
// `variable::method` → nothing (receiver type unknown statically —
|
||||
// the deferred obj.method class)
|
||||
case 'method_reference': {
|
||||
let last: SyntaxNode | null = null;
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child && child.type === 'identifier') last = child;
|
||||
}
|
||||
return last ? [{ name: getNodeText(last, source), node: last }] : [];
|
||||
}
|
||||
|
||||
// Kotlin `::targetCb` — the simple_identifier child.
|
||||
case 'callable_reference': {
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child && child.type === 'simple_identifier') {
|
||||
return [{ name: getNodeText(child, source), node: child }];
|
||||
}
|
||||
if (!last) return [];
|
||||
const m = getNodeText(last, source);
|
||||
const text = getNodeText(node, source);
|
||||
if (text.startsWith('this::') || text.startsWith('super::')) {
|
||||
return [{ name: `this.${m}`, node: last }];
|
||||
}
|
||||
const recv = text.match(/^([A-Z][A-Za-z0-9_]*)\s*::/);
|
||||
if (recv) {
|
||||
// `Type::method` — but `Type::new` (constructor ref) has no method
|
||||
// node to land on; let the stoplist drop it via the bare name.
|
||||
return m === 'new' ? [] : [{ name: `${recv[1]}::${m}`, node: last }];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Kotlin `::targetCb` (one part) / `OtherClass::handle` (two parts —
|
||||
// receiver is a type_identifier; lowercase receivers are variables, the
|
||||
// deferred obj.method class).
|
||||
case 'callable_reference': {
|
||||
let receiver: SyntaxNode | null = null;
|
||||
let member: SyntaxNode | null = null;
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (!child) continue;
|
||||
if (child.type === 'type_identifier') receiver = child;
|
||||
if (child.type === 'simple_identifier') member = child;
|
||||
}
|
||||
if (!member) return [];
|
||||
const m = getNodeText(member, source);
|
||||
if (!receiver) return [{ name: m, node: member }]; // ::topLevelFn
|
||||
const recvText = getNodeText(receiver, source);
|
||||
return /^[A-Z]/.test(recvText)
|
||||
? [{ name: `${recvText}::${m}`, node: member }]
|
||||
: []; // variable::method — unknown receiver type
|
||||
}
|
||||
|
||||
// Kotlin `this::fire` parses as navigation_expression with a `::fire`
|
||||
// navigation_suffix. Ordinary `a.b` navigation MUST yield nothing.
|
||||
// navigation_suffix — route through the class-scoped `this.` resolver.
|
||||
// Ordinary `a.b` navigation (and any non-`this` receiver) MUST yield
|
||||
// nothing.
|
||||
case 'navigation_expression': {
|
||||
if (!getNodeText(node, source).startsWith('this::')) return [];
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child && child.type === 'navigation_suffix' && getNodeText(child, source).startsWith('::')) {
|
||||
const id = child.namedChild(child.namedChildCount - 1);
|
||||
if (id) return [{ name: getNodeText(id, source), node: id }];
|
||||
if (id) return [{ name: `this.${getNodeText(id, source)}`, node: id }];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
|
||||
Reference in New Issue
Block a user