fix(extraction): qualified Type::member refs skip the name gate — no-import references resolve (#812)

`KtHandlers::handle` registered from another file produced no edge: the
extraction gate required the scope to be a same-file type or an IMPORTED
name, but Java/Kotlin same-package references and Kotlin companion members
need no import at all, so the gate could never see them. (The "companion
members extract unqualified" limit recorded during Arc A was a probe
artifact: a SINGLE-LINE `class X { companion object { … } }` is an
upstream tree-sitter-kotlin misparse (ERROR node); real multi-line
companions extract transparently as qualified methods of the class.)

Qualified `Type::member` candidates now skip the name gate the same way
`this.<member>` ones do: the explicit-ref syntax is self-selecting, and
resolution stays scope-suffix-anchored + unique-or-drop, so a
`Decoy::handle` can never match a `KtHandlers::handle` ref (tested).

A/B vs main: rxjava +4 (same-package `Maybe::just` / `Single::just`
method refs), fmt +3 (gtest `&Test::DeleteSelf_` /
`&TestSuite::RunSetUpTestSuite` cross-file member pointers), okio 0-delta,
redis byte-identical — every new edge verified genuine, zero calls edges
touched, node counts identical.

Full suite 1392 passed. EXTRACTION_VERSION 22 → 23 (re-index to benefit).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-11 15:44:14 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 1f15f93feb
commit dce61a5f4a
5 changed files with 65 additions and 31 deletions
+1 -1
View File
@@ -21,4 +21,4 @@
* turns the re-index hint into noise — keep it honest (see CLAUDE.md, "Honesty
* in the product is load-bearing").
*/
export const EXTRACTION_VERSION = 22;
export const EXTRACTION_VERSION = 23;
+8 -26
View File
@@ -435,15 +435,8 @@ 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').
@@ -493,31 +486,20 @@ export class TreeSitterExtractor {
// 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).
// method refs, PHP `'Cls::m'`): ALWAYS flush — the explicit-ref
// syntax is self-selecting, the referenced type often needs NO
// import (Java/Kotlin same-package, Kotlin companions), and
// resolution is scope-suffix-anchored + unique-or-drop, so a
// same-named member on another class can't match.
// - 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.')) {
if (!c.name.startsWith('this.') && !c.name.includes('::')) {
const skipGate =
(ungated?.has(c.mode) === true && atFileScope) ||
c.skipGate === true; // PHP HOF-position string callables (see FnRefCandidate.skipGate)
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;
}
if (!skipGate && !definedHere.has(c.name) && !importedNames.has(c.name)) {
continue;
}
}
const key = `${c.fromNodeId}|${c.name}`;