fix(ingestion): migrate all languages' inheritance to scope-resolution on the worker path (#1951) (#1956)

This commit is contained in:
Gergő Magyar 2026-06-01 17:04:27 +01:00 committed by GitHub
parent fca30c7e26
commit 0fc0211d26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
100 changed files with 3788 additions and 398 deletions

View file

@ -1 +1 @@
f2b4376f30dab76f3befc9cbd3d7cc2bf1afbd7329a5e953439083e005de4a7c
9803b81f0c3738ecd276aba187436482be47b5f5f62e5e85a983524129713b7d

View file

@ -89,10 +89,14 @@ function generatePyDao(entityCount) {
lines.push(`import top.level.module${i}`);
}
lines.push('');
// Shared base + mixin so every Entity is heritage-bearing — exercises the
// @reference.inherits synth (#1951) at scale (single + multiple inheritance),
// not just the base capture loop.
lines.push('class Base:', ' pass', '', 'class Mixin:', ' pass', '');
for (let i = 0; i < entityCount; i++) {
const n = String(i).padStart(4, '0');
lines.push(
`class Entity${n}:`,
`class Entity${n}(Base, Mixin):`,
` def __init__(self, id: int, name: str):`,
` self.id = id`,
` self.name = name`,

View file

@ -1,31 +1,70 @@
{
"_comment": "Per-language baselines for bench/scope-capture/measure.mjs --check. fingerprint = order-independent sha256 over the lang-resolution/<lang>-* fixture corpus + a 20-entity synthetic source (correctness gate; re-baseline intentionally on a legitimate capture change). scaling_budget = max allowed (t800/t250)/(800/250); ~1.0 is linear, ~3.2 is quadratic. All six languages now thread the tree-sitter captured node instead of re-deriving it with findNodeAtRange(tree.rootNode,...) per match, so all are linear (go #1915, python #1918, ruby/php/rust/csharp this PR).",
"_comment": "Per-language baselines for bench/scope-capture/measure.mjs --check. fingerprint = order-independent sha256 over the lang-resolution/<lang>-* fixture corpus + a 20-entity synthetic source (correctness gate; re-baseline intentionally on a legitimate capture change). scaling_budget = max allowed (t800/t250)/(800/250); ~1.0 is linear, ~3.2 is quadratic. The synthetic source is now HERITAGE-BEARING for every language (each Entity extends/implements/embeds/uses-trait/conforms-to a shared base) so the #1951 @reference.inherits synth is gated at scale, not just the base capture loop. All languages thread the tree-sitter captured node instead of re-deriving it with findNodeAtRange(tree.rootNode,...) per match, so all are linear (go #1915, python #1918, ruby/php/rust/csharp #1951, java #1956).",
"go": {
"fingerprint": "faca3555c61ed6980d2b739bf6b1cac7f4ad4644968a27e4687532d9835cd4c7",
"scaling_budget": 1.5
"fingerprint": "976bfd17cee048db11e06a27298e48919b7d45d5277d11923faeb61b138737dd",
"scaling_budget": 1.5,
"_rebaselined": "#1956 synth-widening: + go-qualified-base fixture; synthesizeGoInheritanceReferences now emits embeds for qualified_type (pkg.Base), generic_type (Box[T]), pointer, AND interface_type embeds (matching the #1940 legacy leg), reduced to bare names at parity. go-ambiguous gains an embed inherits capture. Linear (~1.01). (Earlier #1956: heritage-bearing scale source so the synth is gated at scale.)"
},
"cobol": {
"fingerprint": "575016f329c0be29eb90db974f750d02a21b4a12515f7029bda312df713b27b0",
"scaling_budget": 1.5
"scaling_budget": 1.5,
"_note": "COBOL has no inheritance construct, so its scale source stays flat; unchanged."
},
"c": {
"fingerprint": "0de009bdbfe095f530fa87eb32bce6ab83092c904f26b3c8fe8d8ab587cf6dc9",
"scaling_budget": 1.5,
"_added": "#1956: c added to the scope-capture bench (was UNBENCHED). C has no inheritance — flat scale source. Adding it exposed + fixed a pre-existing O(n^2) findNodeAtRange root-walk in c/captures.ts (threaded c.node, byte-identical over c-* fixtures); scaling 3.475 -> 0.96."
},
"cpp": {
"fingerprint": "a571d260559fa48994d12970965b4f9df93efd087541ca31dc7818ac4cd2a2a6",
"scaling_budget": 1.5,
"_added": "#1956: cpp added to the scope-capture bench (was UNBENCHED). Heritage-bearing scale source (: public Base, public Mixin) drives emitCppInheritanceCaptures at scale. Adding it exposed + fixed a pre-existing O(n^2) findNodeAtRange root-walk in cpp/captures.ts (~12 sites, threaded c.node, byte-identical over 263 cpp-* fixtures); scaling 2.30 -> 1.12."
},
"csharp": {
"fingerprint": "bdc7803046011876b2d21ae38e9cb8c97ca1e01769f93ca8affe9317585427bf",
"_rebaselined": "#1956 synth-widening: + csharp-qualified-base fixture; the synth now walks record_declaration + struct_declaration base_lists and handles alias_qualified_name (matching the #1940 legacy leg), so record/struct heritage now emits. csharp-record-base gains a record inherits capture. (record->record SAME-namespace EXTENDS is a separate registry resolution gap, tracked as follow-up.) Linear (~1.00). (Earlier #1956: heritage-bearing scale source.)",
"fingerprint": "68ef32c126d5c6de5d8184c6ad0a6104043036daf9805947db8b21741b883f43",
"scaling_budget": 1.5
},
"rust": {
"fingerprint": "025f5b6d4cf1d8cc42033f1f6b592f8d5428e571939c7f61df4d34b4bbe14be3",
"scaling_budget": 1.5
"fingerprint": "2ffad4ba7b1d2eb1ac407cb6d75d0eb98cbc1878260dbdfe982c0fc925b2d00c",
"scaling_budget": 1.5,
"_rebaselined": "#1956 tri-review U1: rust-qualified-trait fixture (scoped + generic-of-scoped impl trait paths); bareTypeIdentifier now resolves scoped_type_identifier bases by their name: tail (additive, no existing-fixture drift); linear (~1.04)."
},
"php": {
"fingerprint": "00fe6e83cebd67c5f346fedb4234ebedf192995f9f171a424551cb792a0b91a9",
"scaling_budget": 1.5
"fingerprint": "f9c8eaf6d1084f9b95a9fb97ccce5e618a24d936c85fb8af4b96c73a560f7a7f",
"scaling_budget": 1.5,
"_rebaselined": "#1956: heritage-bearing scale source (class extends Base + use trait); both forms gated at scale; linear (~1.04)."
},
"ruby": {
"fingerprint": "0f44b0d153b4534866589db93c582928651238b319cb606f2c6396362770cc18",
"scaling_budget": 1.5
"fingerprint": "bdc7dbfbe5ce7b1e98292f88b404071b4a4b5566f6e756cd637e36a2214967e1",
"scaling_budget": 1.5,
"_rebaselined": "#1956 synth-widening: + ruby-qualified-base fixture; synth now reduces a scope_resolution superclass (class C < Mod::Super) to its trailing constant (matching the #1940 legacy leg), at parity. Linear (~1.03). (Earlier #1956: heritage-bearing scale source.)"
},
"swift": {
"fingerprint": "e6870c409c1005944c51dffd6e485005bb206f7afcbc2ef078e0c2f781c2b2ad",
"scaling_budget": 1.5
"fingerprint": "53325c6345161c5a495f997297af5a24fb718fd3e6647040160f8ab2a2c8e4c0",
"scaling_budget": 1.5,
"_rebaselined": "#1956: swift-qualified-base fixture + heritage-bearing scale source (class: Base, Serviceable — extends + protocol conformance); linear (~1.03)."
},
"java": {
"fingerprint": "b63f9be458f7ece854e7b007159d7bf65b4b66a86e83a6c0656fc93ebd5d83da",
"scaling_budget": 1.5,
"_rebaselined": "#1956 synth-widening: + java-iface-extends fixture; synthesizeJavaInheritanceReferences now ALSO walks interface_declaration extends_interfaces (interface IA extends IB, IC<T>), matching the #1940 legacy leg. (Earlier U2+review: java-qualified-base fixture covers 2- AND 3-segment qualified bases guarding the legacy end-anchor; synth tail-resolves scoped bases.) Linear (~1.03). (Earliest: java added to bench, exposed+fixed the O(n^2) findNodeAtRange root-walk; 3.09 -> ~0.99.)"
},
"typescript": {
"fingerprint": "7087f62dbab5fff0d8a9c39f7bc305842ee73a7ba20d7b44677f6511c92e5b92",
"scaling_budget": 1.5,
"_rebaselined": "#1956 tri-review U2: + typescript-qualified-base fixture AND terminalTsTypeNameNode now treats a member_expression tail (property_identifier) as a leaf name, so qualified `extends ns.Base` synthesizes an edge (was dropped). Linear (~1.03)."
},
"javascript": {
"fingerprint": "a8ddfb15620ae55e50651fc21ab14c4a1f874d9b19e208cc6cbf0a8daac8ec5b",
"scaling_budget": 1.5,
"_added": "#1951: bench coverage added (was ungated); scale source heritage-bearing (extends Base); js/kotlin O(n^2) findNodeAtRange-per-match fixed to threaded captured node, now linear.",
"_rebaselined": "#1956 synth-widening: + javascript-qualified-base fixture; synthesizeJsInheritanceReferences now handles a member_expression base (class S extends ns.Base -> Base), matching the #1940 legacy leg + the TS terminalTsTypeNameNode property_identifier case, at parity. Linear (~1.05)."
},
"kotlin": {
"fingerprint": "5121a11855cd9cc44a357ae3ff50953de80cdd743f00e8924c31503b132bcd84",
"scaling_budget": 1.5,
"_added": "#1951: bench coverage added (was ungated); scale source heritage-bearing (: Base()); js/kotlin O(n^2) findNodeAtRange-per-match fixed to threaded captured node, now linear.",
"_rebaselined": "#1956 synth-widening: + kotlin-qualified-base fixture; synthesizeKotlinInheritanceReferences now handles the explicit_delegation form (class F : Iface by d -> Iface), matching the #1940 legacy leg, at parity. Linear (~0.87)."
}
}

View file

@ -34,6 +34,12 @@ import { emitPhpScopeCaptures } from '../../src/core/ingestion/languages/php/ind
import { emitRubyScopeCaptures } from '../../src/core/ingestion/languages/ruby/index.ts';
import { emitCobolScopeCaptures } from '../../src/core/ingestion/languages/cobol/index.ts';
import { emitSwiftScopeCaptures } from '../../src/core/ingestion/languages/swift/index.ts';
import { emitTsScopeCaptures } from '../../src/core/ingestion/languages/typescript/index.ts';
import { emitJsScopeCaptures } from '../../src/core/ingestion/languages/javascript/index.ts';
import { emitKotlinScopeCaptures } from '../../src/core/ingestion/languages/kotlin/index.ts';
import { emitJavaScopeCaptures } from '../../src/core/ingestion/languages/java/index.ts';
import { emitCScopeCaptures } from '../../src/core/ingestion/languages/c/index.ts';
import { emitCppScopeCaptures } from '../../src/core/ingestion/languages/cpp/index.ts';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const FIXTURE_ROOT = path.resolve(__dirname, '..', '..', 'test', 'fixtures', 'lang-resolution');
@ -93,9 +99,12 @@ const LANGS = [
fixturePrefix: 'go',
exts: ['.go'],
file: 'bench.go',
header: 'package generated\n\n',
// Heritage-bearing: each Entity embeds Base (Go inheritance = struct
// embedding) so the @reference.inherits synth (#1951) is driven at scale.
header:
'package generated\n\ntype Base struct{}\n\nfunc (b *Base) BaseMethod() string { return "base" }\n\n',
unit: (n) =>
`type Entity${n} struct {\n\tid int64\n\tname string\n}\n\n` +
`type Entity${n} struct {\n\tBase\n\tid int64\n\tname string\n}\n\n` +
`func (e *Entity${n}) GetID() int64 { return e.id }\n` +
`func (e *Entity${n}) SetName(v string) { e.name = v }\n\n`,
},
@ -105,9 +114,12 @@ const LANGS = [
fixturePrefix: 'csharp',
exts: ['.cs'],
file: 'bench.cs',
header: 'namespace Generated;\n\n',
// Heritage-bearing: extends Base + implements IEntity (both forms) so the
// @reference.inherits synth (#1951) is driven at scale, not just the base loop.
header:
'namespace Generated;\n\npublic class Base { }\n\npublic interface IEntity {\n long GetId();\n}\n\n',
unit: (n) =>
`public class Entity${n} {\n` +
`public class Entity${n} : Base, IEntity {\n` +
` public long Id;\n public string Name;\n` +
` public long GetId() { return Id; }\n` +
` public void SetName(string v) { Name = v; }\n}\n\n`,
@ -118,12 +130,15 @@ const LANGS = [
fixturePrefix: 'rust',
exts: ['.rs'],
file: 'bench.rs',
header: '',
// Heritage-bearing: `impl Shape for Entity_n` (Rust inheritance lives on
// impl_item) so the @reference.inherits trait-impl synth (#1951) is driven
// at scale. The two methods move into the trait impl to keep unit size flat.
header: 'trait Shape {\n fn area(&self) -> i64;\n fn name(&self) -> String;\n}\n\n',
unit: (n) =>
`struct Entity${n} {\n id: i64,\n name: String,\n}\n\n` +
`impl Entity${n} {\n` +
` fn get_id(&self) -> i64 { self.id }\n` +
` fn set_name(&mut self, v: String) { self.name = v; }\n}\n\n`,
`impl Shape for Entity${n} {\n` +
` fn area(&self) -> i64 { self.id }\n` +
` fn name(&self) -> String { self.name.clone() }\n}\n\n`,
},
{
name: 'php',
@ -131,9 +146,13 @@ const LANGS = [
fixturePrefix: 'php',
exts: ['.php'],
file: 'bench.php',
header: '<?php\n\n',
// Heritage-bearing: extends Base + uses a trait (both forms) so the
// @reference.inherits synth (#1951) is driven at scale.
header:
'<?php\n\nclass Base {}\n\ntrait Auditable {\n public function audit() { return true; }\n}\n\n',
unit: (n) =>
`class Entity${n} {\n` +
`class Entity${n} extends Base {\n` +
` use Auditable;\n` +
` public $id;\n public $name;\n` +
` function getId() { return $this->id; }\n` +
` function setName($v) { $this->name = $v; }\n}\n\n`,
@ -144,9 +163,13 @@ const LANGS = [
fixturePrefix: 'ruby',
exts: ['.rb'],
file: 'bench.rb',
header: '',
// Heritage-bearing: `< Base` superclass + `include Trackable` mixin (both
// forms) so the @reference.inherits synth (#1951) is driven at scale.
header:
'class Base\n def base_id\n @id\n end\nend\n\nmodule Trackable\n def track\n @tracked = true\n end\nend\n\n',
unit: (n) =>
`class Entity${n}\n` +
`class Entity${n} < Base\n` +
` include Trackable\n` +
` def get_id\n @id\n end\n` +
` def set_name(v)\n @name = v\n end\nend\n\n`,
},
@ -162,18 +185,106 @@ const LANGS = [
' PROCEDURE DIVISION.\n',
unit: (n) => ` PARA-${String(n).padStart(5, '0')}.\n DISPLAY "P${n}".\n`,
},
{
name: 'c',
emit: emitCScopeCaptures,
fixturePrefix: 'c',
exts: ['.c', '.h'],
file: 'bench.c',
// C has no inheritance construct — flat scale source. Added (was unbenched);
// adding it exposed + fixed the same O(n²) findNodeAtRange root-walk (#1956).
header: '#include <stdint.h>\n#include <stddef.h>\n\ntypedef int64_t id_t;\n\n',
unit: (n) =>
`typedef struct Entity${n} {\n id_t id;\n const char *name;\n} Entity${n};\n\n` +
`id_t entity_${n}_get_id(Entity${n} *e) { return e->id; }\n` +
`void entity_${n}_set_name(Entity${n} *e, const char *v) { e->name = v; }\n\n`,
},
{
name: 'cpp',
emit: emitCppScopeCaptures,
fixturePrefix: 'cpp',
exts: ['.cpp', '.cc', '.cxx', '.hpp', '.h'],
file: 'bench.cpp',
// Heritage-bearing: `: public Base, public Mixin` (single + multiple
// inheritance) drives emitCppInheritanceCaptures (#1951) at scale. Added
// (was unbenched); adding it exposed + fixed the same O(n²) root-walk (#1956).
header:
'#include <string>\n\nclass Base {\n public:\n long baseId() const { return 0; }\n};\n\nclass Mixin {\n public:\n void mix() {}\n};\n\n',
unit: (n) =>
`class Entity${n} : public Base, public Mixin {\n public:\n long id;\n std::string name;\n` +
` long getId() const { return id; }\n` +
` void setName(std::string v) { name = v; }\n};\n\n`,
},
{
name: 'swift',
emit: emitSwiftScopeCaptures,
fixturePrefix: 'swift',
exts: ['.swift'],
file: 'bench.swift',
header: '',
// Heritage-bearing: inherits Base + conforms to Serviceable (both forms) so
// the @reference.inherits synth (#1951) is driven at scale.
header:
'class Base {\n func ping() -> String { return "base" }\n}\n\nprotocol Serviceable {\n func serve() -> String\n}\n\n',
unit: (n) =>
`class Entity${n} {\n` +
`class Entity${n}: Base, Serviceable {\n` +
` var id: Int64 = 0\n var name: String = ""\n` +
` func getId() -> Int64 { return self.id }\n` +
` func setName(_ v: String) { self.name = v }\n}\n\n`,
` func serve() -> String { return self.name }\n}\n\n`,
},
{
name: 'java',
emit: emitJavaScopeCaptures,
fixturePrefix: 'java',
exts: ['.java'],
file: 'bench.java',
// Java was previously unbenched. Heritage-bearing: extends Base + implements
// Marker (both forms) so the @reference.inherits synth (#1951) is driven at scale.
header: 'package generated;\n\nclass Base {}\n\ninterface Marker {}\n\n',
unit: (n) =>
`class Entity${n} extends Base implements Marker {\n` +
` long id = 0L;\n String name = "";\n` +
` public long getId() { return this.id; }\n` +
` public void setName(String v) { this.name = v; }\n}\n\n`,
},
{
name: 'typescript',
emit: emitTsScopeCaptures,
fixturePrefix: 'typescript',
exts: ['.ts', '.tsx'],
file: 'bench.ts',
// Inheritance-bearing units so the @reference.inherits synth pass (#1951)
// is exercised at scale, not just the base capture loop.
header: 'class Base {}\n\n',
unit: (n) =>
`class Entity${n} extends Base {\n` +
` id: number = 0;\n name: string = '';\n` +
` getId(): number { return this.id; }\n` +
` setName(v: string): void { this.name = v; }\n}\n\n`,
},
{
name: 'javascript',
emit: emitJsScopeCaptures,
fixturePrefix: 'javascript',
exts: ['.js', '.jsx', '.mjs', '.cjs'],
file: 'bench.js',
header: 'class Base {}\n\n',
unit: (n) =>
`class Entity${n} extends Base {\n` +
` getId() { return this.id; }\n` +
` setName(v) { this.name = v; }\n}\n\n`,
},
{
name: 'kotlin',
emit: emitKotlinScopeCaptures,
fixturePrefix: 'kotlin',
exts: ['.kt', '.kts'],
file: 'bench.kt',
header: 'open class Base\n\n',
unit: (n) =>
`class Entity${n} : Base() {\n` +
` var id: Long = 0\n var name: String = ""\n` +
` fun getId(): Long { return id }\n` +
` fun setName(v: String) { name = v }\n}\n\n`,
},
];

View file

@ -20,6 +20,7 @@ import Parser from 'tree-sitter';
import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
import { generateId } from '../../lib/utils.js';
import { getLanguageFromFilename, type NodeLabel, type SupportedLanguages } from 'gitnexus-shared';
import { isRegistryPrimary } from './registry-primary-flag.js';
import { isVerboseIngestionEnabled } from './utils/verbose.js';
import { yieldToEventLoop } from './utils/event-loop.js';
import { parseSourceSafe } from '../tree-sitter/safe-parse.js';
@ -202,6 +203,10 @@ export const processHeritage = async (
// 1. Check language support
const language = getLanguageFromFilename(file.path);
if (!language) continue;
// Registry-primary gate: the scope-based phase owns inheritance (EXTENDS/
// IMPLEMENTS) for this language, so the legacy `@heritage` pass skips it —
// mirrors `call-processor`/`import-processor` (#1951).
if (isRegistryPrimary(language)) continue;
if (!isLanguageAvailable(language)) {
if (skippedByLang) {
skippedByLang.set(language, (skippedByLang.get(language) ?? 0) + 1);

View file

@ -1,6 +1,6 @@
import type { Capture, CaptureMatch } from 'gitnexus-shared';
import {
findNodeAtRange,
nodeIfType,
nodeToCapture,
syntheticCapture,
type SyntaxNode,
@ -33,17 +33,30 @@ export function emitCScopeCaptures(
for (const m of rawMatches) {
const grouped: Record<string, Capture> = {};
// Parallel tag -> captured SyntaxNode map. The tree-sitter query already
// hands us each matched node as `c.node`, so anchors resolve via a
// type-guarded lookup (`nodeIfType`) instead of re-deriving them with
// `findNodeAtRange(tree.rootNode, ...)` per match — the
// O(matches × rootChildren) root-walk fixed for go #1848 / python #1918 /
// rust/csharp #1915 / java #1951, mirrored here for C. Every C scope-query
// anchor below captures directly ON the node the old root-walk re-derived
// (verified against C_SCOPE_QUERY in query.ts: @import.statement on
// preproc_include, @declaration.function on function_definition/declaration,
// @reference.call.free/.member on call_expression), so the type check is
// exact. C has no inheritance construct, so there is no heritage synthesis.
const nodeMap: Record<string, SyntaxNode> = {};
for (const c of m.captures) {
const tag = '@' + c.name;
if (tag.startsWith('@_')) continue;
grouped[tag] = nodeToCapture(tag, c.node);
nodeMap[tag] = c.node;
}
if (Object.keys(grouped).length === 0) continue;
// Handle #include statements
// Handle #include statements. `@import.statement` is captured directly on
// the `preproc_include` node.
if (grouped['@import.statement'] !== undefined) {
const anchor = grouped['@import.statement']!;
const includeNode = findNodeAtRange(tree.rootNode, anchor.range, 'preproc_include');
const includeNode = nodeIfType(nodeMap['@import.statement'], 'preproc_include');
if (includeNode !== null) {
const split = splitCInclude(includeNode);
if (split !== null) {
@ -71,12 +84,16 @@ export function emitCScopeCaptures(
if (concreteTypedefRanges.has(key)) continue;
}
// Enrich function declarations with arity metadata and detect static linkage
const declAnchor = grouped['@declaration.function'];
if (declAnchor !== undefined) {
const fnNode =
findNodeAtRange(tree.rootNode, declAnchor.range, 'function_definition') ??
findNodeAtRange(tree.rootNode, declAnchor.range, 'declaration');
// Enrich function declarations with arity metadata and detect static linkage.
// `@declaration.function` is captured directly on the `function_definition`
// node (definitions) or the `declaration` node (prototypes) — the captured
// node IS what the old findNodeAtRange re-derived.
if (grouped['@declaration.function'] !== undefined) {
const fnNode = nodeIfType(
nodeMap['@declaration.function'],
'function_definition',
'declaration',
);
if (fnNode !== null) {
const arity = computeCDeclarationArity(fnNode);
if (arity.parameterCount !== undefined) {
@ -111,10 +128,12 @@ export function emitCScopeCaptures(
}
}
// Enrich call references with arity
const callAnchor = grouped['@reference.call.free'] ?? grouped['@reference.call.member'];
if (callAnchor !== undefined && grouped['@reference.arity'] === undefined) {
const callNode = findNodeAtRange(tree.rootNode, callAnchor.range, 'call_expression');
// Enrich call references with arity. @reference.call.free / .member are both
// captured directly on the `call_expression` node — the captured node IS
// what the old findNodeAtRange re-derived.
const callAnchorNode = nodeMap['@reference.call.free'] ?? nodeMap['@reference.call.member'];
if (callAnchorNode !== undefined && grouped['@reference.arity'] === undefined) {
const callNode = nodeIfType(callAnchorNode, 'call_expression');
if (callNode !== null) {
grouped['@reference.arity'] = syntheticCapture(
'@reference.arity',

View file

@ -1,6 +1,6 @@
import type { Capture, CaptureMatch, ParameterTypeClass } from 'gitnexus-shared';
import {
findNodeAtRange,
nodeIfType,
nodeToCapture,
syntheticCapture,
type SyntaxNode,
@ -41,17 +41,28 @@ export function emitCppScopeCaptures(
for (const m of rawMatches) {
const grouped: Record<string, Capture> = {};
// Parallel tag -> captured SyntaxNode map. The tree-sitter query already
// hands us each matched node as `c.node`, so anchors resolve via a
// type-guarded lookup (`nodeIfType`) instead of re-deriving them with
// `findNodeAtRange(tree.rootNode, ...)` per match — the
// O(matches × rootChildren) root-walk fixed for go #1848 / python #1918 /
// rust/csharp #1915 / java, mirrored here for C++ (#1951). Each C++
// scope-query anchor used below captures directly ON the node the old
// root-walk re-derived (verified against CPP_SCOPE_QUERY in query.ts and a
// real-parse AST probe), so the type check is exact.
const nodeMap: Record<string, SyntaxNode> = {};
for (const c of m.captures) {
const tag = '@' + c.name;
if (tag.startsWith('@_')) continue;
grouped[tag] = nodeToCapture(tag, c.node);
nodeMap[tag] = c.node;
}
if (Object.keys(grouped).length === 0) continue;
// ── Handle #include statements ──────────────────────────────────
// `@import.statement` is captured directly on the `preproc_include` node.
if (grouped['@import.statement'] !== undefined) {
const anchor = grouped['@import.statement']!;
const includeNode = findNodeAtRange(tree.rootNode, anchor.range, 'preproc_include');
const includeNode = nodeIfType(nodeMap['@import.statement'], 'preproc_include');
if (includeNode !== null) {
const split = splitCppInclude(includeNode);
if (split !== null) {
@ -62,9 +73,9 @@ export function emitCppScopeCaptures(
}
// ── Handle using declarations (using namespace / using name) ────
// `@import.using-decl` is captured directly on the `using_declaration` node.
if (grouped['@import.using-decl'] !== undefined) {
const anchor = grouped['@import.using-decl']!;
const usingNode = findNodeAtRange(tree.rootNode, anchor.range, 'using_declaration');
const usingNode = nodeIfType(nodeMap['@import.using-decl'], 'using_declaration');
if (usingNode !== null) {
const split = splitCppUsingDecl(usingNode);
if (split !== null) {
@ -93,12 +104,18 @@ export function emitCppScopeCaptures(
}
// ── Enrich function/method declarations with arity metadata ─────
const declAnchor = grouped['@declaration.function'] ?? grouped['@declaration.method'];
if (declAnchor !== undefined) {
const fnNode =
findNodeAtRange(tree.rootNode, declAnchor.range, 'function_definition') ??
findNodeAtRange(tree.rootNode, declAnchor.range, 'declaration') ??
findNodeAtRange(tree.rootNode, declAnchor.range, 'field_declaration');
// `@declaration.function` / `@declaration.method` capture directly on the
// `function_definition` (definitions/templates), `declaration` (free/
// constructor prototypes), or `field_declaration` (class-body method
// prototypes) node — the node the old findNodeAtRange re-derived.
const declAnchorNode = nodeMap['@declaration.function'] ?? nodeMap['@declaration.method'];
if (declAnchorNode !== undefined) {
const fnNode = nodeIfType(
declAnchorNode,
'function_definition',
'declaration',
'field_declaration',
);
if (fnNode !== null) {
const arity = computeCppDeclarationArity(fnNode);
if (arity.parameterCount !== undefined) {
@ -182,9 +199,9 @@ export function emitCppScopeCaptures(
}
// ── Detect static variables (file-local linkage) ────────────────
const varDeclAnchor = grouped['@declaration.variable'];
if (varDeclAnchor !== undefined) {
const varNode = findNodeAtRange(tree.rootNode, varDeclAnchor.range, 'declaration');
// `@declaration.variable` is captured directly on the `declaration` node.
if (grouped['@declaration.variable'] !== undefined) {
const varNode = nodeIfType(nodeMap['@declaration.variable'], 'declaration');
if (varNode !== null) {
if (hasStaticStorageClass(varNode) || isInsideAnonymousNamespace(varNode)) {
const nameText = grouped['@declaration.name']?.text;
@ -196,22 +213,30 @@ export function emitCppScopeCaptures(
}
// ── Enrich call references with arity ───────────────────────────
// `@reference.call.free` / `.member` capture on the `call_expression` (plain
// / member / template calls) or on the `binary_expression` (the operator-call
// patterns: `a + b`, `lhs << rhs`); `@reference.call.qualified` always on the
// `call_expression`. The captured node IS the node the old findNodeAtRange
// re-derived (verified against CPP_SCOPE_QUERY + a real-parse probe).
const callAnchor =
grouped['@reference.call.free'] ??
grouped['@reference.call.member'] ??
grouped['@reference.call.qualified'];
const callAnchorNode =
nodeMap['@reference.call.free'] ??
nodeMap['@reference.call.member'] ??
nodeMap['@reference.call.qualified'];
const operatorAnchor = grouped['@reference.operator'];
if (operatorAnchor !== undefined) {
// When `@reference.operator` fires, the co-captured call anchor is the
// enclosing `binary_expression` itself, so a type guard reproduces the
// old findNodeAtRange(callAnchor.range, 'binary_expression').
const operatorNode =
callAnchor !== undefined
? findNodeAtRange(tree.rootNode, callAnchor.range, 'binary_expression')
: null;
callAnchorNode !== undefined ? nodeIfType(callAnchorNode, 'binary_expression') : null;
if (operatorNode !== null && isPrimitiveOnlyBinaryOperator(operatorNode)) continue;
}
if (callAnchor !== undefined && grouped['@reference.arity'] === undefined) {
const callNode =
findNodeAtRange(tree.rootNode, callAnchor.range, 'call_expression') ??
findNodeAtRange(tree.rootNode, callAnchor.range, 'binary_expression');
if (callAnchorNode !== undefined && grouped['@reference.arity'] === undefined) {
const callNode = nodeIfType(callAnchorNode, 'call_expression', 'binary_expression');
if (callNode?.type === 'call_expression') {
grouped['@reference.arity'] = syntheticCapture(
'@reference.arity',
@ -228,17 +253,25 @@ export function emitCppScopeCaptures(
}
if (operatorAnchor !== undefined && grouped['@reference.name'] === undefined) {
// The old code did `findNodeAtRange(tree.rootNode, operatorAnchor.range,
// operatorAnchor.text)`, searching for a node of type `+` / `<<` at the
// operator-token range. That token is an UNNAMED grammar node, and
// findNodeAtRange only descends `namedChild`ren, so the search NEVER hit
// and ALWAYS fell back to `tree.rootNode`. Use `tree.rootNode` directly to
// preserve the exact synthetic-capture range while dropping the root-walk.
grouped['@reference.name'] = syntheticCapture(
'@reference.name',
findNodeAtRange(tree.rootNode, operatorAnchor.range, operatorAnchor.text) ?? tree.rootNode,
tree.rootNode,
`operator${operatorAnchor.text}`,
);
}
// ── Enrich constructor calls (new Foo()) with arity ─────────────
// `@reference.call.constructor` is captured directly on the `new_expression`.
const ctorCallAnchor = grouped['@reference.call.constructor'];
const ctorCallAnchorNode = nodeMap['@reference.call.constructor'];
if (ctorCallAnchor !== undefined && grouped['@reference.arity'] === undefined) {
const newNode = findNodeAtRange(tree.rootNode, ctorCallAnchor.range, 'new_expression');
const newNode = nodeIfType(ctorCallAnchorNode, 'new_expression');
if (newNode !== null) {
grouped['@reference.arity'] = syntheticCapture(
'@reference.arity',
@ -249,12 +282,18 @@ export function emitCppScopeCaptures(
}
// ── Synthesize argument types for overload narrowing ────────────
// The any-call anchor is either the call/operator anchor (`call_expression`
// / `binary_expression`) or the constructor anchor (`new_expression`); the
// captured node IS what the old findNodeAtRange re-derived.
const anyCallAnchor = callAnchor ?? ctorCallAnchor;
const anyCallAnchorNode = callAnchorNode ?? ctorCallAnchorNode;
if (anyCallAnchor !== undefined && grouped['@reference.parameter-types'] === undefined) {
const cNode =
findNodeAtRange(tree.rootNode, anyCallAnchor.range, 'call_expression') ??
findNodeAtRange(tree.rootNode, anyCallAnchor.range, 'new_expression') ??
findNodeAtRange(tree.rootNode, anyCallAnchor.range, 'binary_expression');
const cNode = nodeIfType(
anyCallAnchorNode,
'call_expression',
'new_expression',
'binary_expression',
);
if (cNode !== null) {
const argTypes =
cNode.type === 'binary_expression'
@ -293,13 +332,12 @@ export function emitCppScopeCaptures(
// `@declaration.namespace` fires only for NAMED namespaces (the query
// requires a `name: (namespace_identifier)` child). Use the unconditional
// `@scope.namespace` capture so the anonymous-namespace branch also runs.
const namespaceScopeAnchor = grouped['@declaration.namespace'] ?? grouped['@scope.namespace'];
if (namespaceScopeAnchor !== undefined) {
const nsNode = findNodeAtRange(
tree.rootNode,
namespaceScopeAnchor.range,
'namespace_definition',
);
// `@declaration.namespace` and `@scope.namespace` both capture directly on
// the `namespace_definition` node.
const namespaceScopeAnchorNode =
nodeMap['@declaration.namespace'] ?? nodeMap['@scope.namespace'];
if (namespaceScopeAnchorNode !== undefined) {
const nsNode = nodeIfType(namespaceScopeAnchorNode, 'namespace_definition');
if (nsNode !== null) {
// Range coords stored in the shared Range shape use 1-based
// line numbers (see `ast-helpers.ts` rangeForNode where
@ -329,11 +367,11 @@ export function emitCppScopeCaptures(
// qualified `Ns::f(s)` and member `obj.f(s)` calls bypass the
// free-call fallback entirely (handled by receiver-bound-calls).
if (grouped['@reference.call.free'] !== undefined) {
const freeCallNode = findNodeAtRange(
tree.rootNode,
grouped['@reference.call.free']!.range,
'call_expression',
);
// `@reference.call.free` captures on a `call_expression` (plain/template
// free call) or a `binary_expression` (the `lhs << rhs` operator-call
// pattern). The old findNodeAtRange filtered to `call_expression`, so the
// `binary_expression` case yields null here — `nodeIfType` matches exactly.
const freeCallNode = nodeIfType(nodeMap['@reference.call.free'], 'call_expression');
if (freeCallNode !== null) {
const adlAnchorRange = grouped['@reference.call.free']!.range;
if (isParenthesizedFunctionCall(freeCallNode)) {
@ -358,7 +396,8 @@ export function emitCppScopeCaptures(
grouped['@type-binding.type']?.text === 'auto'
) {
const anchor = grouped['@type-binding.assignment']!;
const declNode = findNodeAtRange(tree.rootNode, anchor.range, 'declaration');
// `@type-binding.assignment` is captured directly on the `declaration` node.
const declNode = nodeIfType(nodeMap['@type-binding.assignment'], 'declaration');
if (declNode !== null) {
const declarator = declNode.childForFieldName('declarator');
if (declarator?.type === 'init_declarator') {

View file

@ -17,7 +17,12 @@
*/
import type { Capture, CaptureMatch } from 'gitnexus-shared';
import { nodeIfType, nodeToCapture, syntheticCapture } from '../../utils/ast-helpers.js';
import {
nodeIfType,
nodeToCapture,
syntheticCapture,
walkNamedTree,
} from '../../utils/ast-helpers.js';
import { splitUsingDirective } from './import-decomposer.js';
import { computeCsharpArityMetadata } from './arity-metadata.js';
import { synthesizeCsharpReceiverBinding } from './receiver-binding.js';
@ -257,15 +262,65 @@ export function emitCsharpScopeCaptures(
}
out.push(...synthesizeGenericTypeArgumentReferences(tree.rootNode));
out.push(...synthesizeCsharpInheritanceReferences(tree.rootNode));
return out;
}
/**
* Synthesize `@reference.inherits` captures from C# base lists so the
* registry-primary scope-resolution path emits EXTENDS / IMPLEMENTS edges
* (mirrors C++ `emitCppInheritanceCaptures`). Without this, C# inheritance
* edges came only from the legacy `@heritage.*` path, which is dropped for
* registry-primary languages in the worker pipeline (issue #1951).
*
* Scope covers every `base_list`-bearing declaration the legacy `@heritage`
* leg matches: `class_declaration`, `interface_declaration`,
* `record_declaration`, and `struct_declaration`. Records and structs were
* dropped before (#1951): a `record R(...) : Base(args), IFoo` or
* `struct S : IFoo, ns.IBar` produced no registry-primary inheritance edge
* even though the legacy heritage query covered them. The
* EXTENDS-vs-IMPLEMENTS split is decided downstream from the resolved target's
* symbol kind (`preEmitInheritanceEdges`), so all bases are emitted with the
* same `inherits` kind here; the base lookup name is normalized to its bare
* simple identifier (`IRepository<T>` `IRepository`, `A.B.IFace` `IFace`,
* `Base(args)` primary-ctor base `Base`, `MyAlias::Foo` `Foo`) to match
* the V1 simple-name `findClassBindingInScope` contract exactly the bare
* text `normalizeSupertypeName` (supertype-alternation.ts) reduces each shape
* to on the legacy leg.
*/
function synthesizeCsharpInheritanceReferences(root: SyntaxNode): CaptureMatch[] {
const out: CaptureMatch[] = [];
walkNamedTree(root, (node) => {
if (
node.type !== 'class_declaration' &&
node.type !== 'interface_declaration' &&
node.type !== 'record_declaration' &&
node.type !== 'struct_declaration'
) {
return;
}
const baseList = findNamedChild(node, 'base_list');
if (baseList === null) return;
for (const base of baseList.namedChildren) {
if (base === null) continue;
const nameNode = terminalTypeNameNode(base);
if (nameNode === null) continue;
if (BUILTIN_TYPE_NAMES.has(nameNode.text)) continue;
out.push({
'@reference.inherits': nodeToCapture('@reference.inherits', base),
'@reference.name': nodeToCapture('@reference.name', nameNode),
});
}
});
return out;
}
function synthesizeGenericTypeArgumentReferences(root: SyntaxNode): CaptureMatch[] {
const out: CaptureMatch[] = [];
// Treat all generic type arguments as static type references, including
// declaration signatures and call-site generic instantiations.
visit(root, (node) => {
walkNamedTree(root, (node) => {
if (node.type !== 'generic_name') return;
const args = findNamedChild(node, 'type_argument_list');
if (args === null) return;
@ -290,8 +345,28 @@ function terminalTypeNameNode(node: SyntaxNode): SyntaxNode | null {
return node;
case 'nullable_type':
return node.firstNamedChild === null ? null : terminalTypeNameNode(node.firstNamedChild);
case 'qualified_name':
return node.lastNamedChild;
case 'qualified_name': {
// `A.B.Base` -> tail identifier `Base`; `A.B.Base<T>` -> the tail is a
// `generic_name`, so recurse to drop the type arguments and reach the
// bare base identifier (#1951).
const tail = node.lastNamedChild;
return tail === null ? null : terminalTypeNameNode(tail);
}
case 'alias_qualified_name': {
// `MyAlias::Foo` / `global::IDisposable` -> the `name` field is the bare
// identifier (the `alias` is the qualifier). Mirrors
// normalizeSupertypeName's `name`-field reduction for this shape (#1951).
const name = node.childForFieldName('name');
return name === null ? null : terminalTypeNameNode(name);
}
case 'primary_constructor_base_type': {
// record base with a constructor call: `Base(args)` / `pkg.Base(id)` /
// `Box<int>(id)`. The `type` field holds the supertype (identifier /
// qualified_name / generic_name); the trailing argument_list is dropped.
// Mirrors normalizeSupertypeName's `type`-field reduction (#1951).
const type = node.childForFieldName('type');
return type === null ? null : terminalTypeNameNode(type);
}
case 'generic_name':
// generic_name has no `name` field (verified by real parse, #1920); the
// base identifier is the first named child.
@ -308,13 +383,6 @@ function findNamedChild(node: SyntaxNode, type: string): SyntaxNode | null {
return null;
}
function visit(node: SyntaxNode, cb: (node: SyntaxNode) => void): void {
cb(node);
for (const child of node.namedChildren) {
if (child !== null) visit(child, cb);
}
}
/** C# 12 primary constructor: `class X(a, b) { }` / `record X(a, b)`.
* The parameters are a bare `parameter_list` named child of the type
* declaration (no `constructor_declaration` node). Emit a synthetic

View file

@ -1,5 +1,10 @@
import type { Capture, CaptureMatch } from 'gitnexus-shared';
import { nodeToCapture, syntheticCapture, type SyntaxNode } from '../../utils/ast-helpers.js';
import {
nodeToCapture,
syntheticCapture,
walkNamedTree,
type SyntaxNode,
} from '../../utils/ast-helpers.js';
import { getGoParser, getGoScopeQuery } from './query.js';
import { recordGoCacheHit, recordGoCacheMiss } from './cache-stats.js';
import { computeGoCallArity, computeGoDeclarationArity } from './arity-metadata.js';
@ -159,9 +164,140 @@ export function emitGoScopeCaptures(
});
}
out.push(...synthesizeGoInheritanceReferences(tree.rootNode));
return out;
}
/**
* Synthesize `@reference.inherits` captures for Go struct embedding so the
* registry-primary scope-resolution path emits inheritance edges (mirrors C#
* `synthesizeCsharpInheritanceReferences` / C++ `emitCppInheritanceCaptures`).
* Without this, Go embedding edges came only from the legacy `@heritage.*`
* path, which is dropped for registry-primary languages in the worker pipeline
* (issue #1951).
*
* Scope EXACTLY matches the legacy Go heritage query + its `shouldSkipExtends`
* hook (`heritage-extractors/configs/go.ts`), whose supertype alternation is
* `[(type_identifier) (qualified_type) (generic_type)]` (see `goHeritageShapes`)
* matched against BOTH struct embedding and interface-in-interface embedding:
*
* struct: (struct_type (field_declaration_list
* (field_declaration type: <base>))) anonymous (no `name`) field
* interface: (interface_type (type_elem <base>)) single-element type_elem
*
* i.e. an embedded (anonymous) field inside a struct, or an embedded type inside
* an interface. Named struct fields (`Breed string`) are skipped because their
* `field_declaration` carries a `name` field; multi-operand interface type-sets
* (`int | float64`) are skipped because their `type_elem` has >1 named child
* both matching the legacy `shouldSkipExtends` filter.
*
* The base shapes covered (issue #1951 these were previously DROPPED by the
* registry-primary synth, so production silently omitted their edges even though
* the legacy `@heritage` leg, config-driven since #1940, captured them):
* - bare `type_identifier` (`Base`) the node itself
* - `qualified_type` (`pkg.Base`) `name:` tail
* - `generic_type` (`Box[T]`) `type:` base
* - pointer embeds (`*Base`, `*pkg.Base`, ) the `*` is an
* unnamed token, so `field_declaration.type` already points at the inner
* shape above; no `pointer_type` unwrap is needed in this grammar version.
*
* The captured `@reference.name` is reduced to its bare simple identifier so the
* V1 simple-name `findClassBindingInScope` contract keeps holding `pkg.Base`
* `Base`, `Box[T]` `Box`. {@link goEmbedBaseNameNode} returns the node whose
* `.text` EQUALS `normalizeSupertypeName(base)` for every shape (verified by
* real-parse), so this synth stays at parity with the legacy leg's reduction.
* For a bare `type_identifier` it returns the same node, keeping the simple-base
* path byte-identical.
*
* The EXTENDS-vs-IMPLEMENTS split is decided downstream from the resolved
* target's symbol kind (`preEmitInheritanceEdges`): an embedded struct resolves
* to EXTENDS, an embedded interface to IMPLEMENTS.
*/
function synthesizeGoInheritanceReferences(root: SyntaxNode): CaptureMatch[] {
const out: CaptureMatch[] = [];
walkNamedTree(root, (node) => {
if (node.type !== 'type_declaration') return;
for (const spec of node.namedChildren) {
if (spec.type !== 'type_spec') continue;
const typeNode = spec.childForFieldName('type');
if (typeNode === null) continue;
if (typeNode.type === 'struct_type') {
const fieldList = findNamedChildOfType(typeNode, 'field_declaration_list');
if (fieldList === null) continue;
for (const field of fieldList.namedChildren) {
if (field.type !== 'field_declaration') continue;
// Embedded (anonymous) field: no `name` field. Named fields are
// skipped (legacy `shouldSkipExtends`).
if (field.childForFieldName('name') !== null) continue;
// `field.type` is the embedded base — bare/qualified/generic, with any
// `*` pointer marker as an unnamed sibling token (already unwrapped).
emitGoEmbedInheritance(field.childForFieldName('type'), out);
}
} else if (typeNode.type === 'interface_type') {
for (const elem of typeNode.namedChildren) {
// Only `type_elem` (an embedded type); `method_elem` is a method, not
// an embed. Multi-operand type-sets (`int | float64`) parse as a
// `type_elem` with >1 named child — skip them (legacy
// `shouldSkipExtends`); a single-element `type_elem` is the embed.
if (elem.type !== 'type_elem' || elem.namedChildCount !== 1) continue;
emitGoEmbedInheritance(elem.namedChild(0), out);
}
}
}
});
return out;
}
/**
* Emit one `@reference.inherits` / `@reference.name` match for a Go embed base
* node, reducing the name to its bare simple identifier. No-ops when `baseNode`
* is null or not one of the embed shapes.
*/
function emitGoEmbedInheritance(baseNode: SyntaxNode | null, out: CaptureMatch[]): void {
if (baseNode === null) return;
const nameNode = goEmbedBaseNameNode(baseNode);
if (nameNode === null) return;
out.push({
'@reference.inherits': nodeToCapture('@reference.inherits', baseNode),
'@reference.name': nodeToCapture('@reference.name', nameNode),
});
}
/**
* Reduce a Go embed base node to its trailing bare `type_identifier`, matching
* the node shapes the legacy `@heritage` query accepts (`goHeritageShapes`) and
* the reduction `normalizeSupertypeName` performs (verified by real-parse to
* yield an identical `.text` for each shape):
* - `type_identifier` the node itself (`Base`)
* - `qualified_type` name: (type_identifier) the trailing `name:` id
* (`pkg.Base` `Base`)
* - `generic_type` type: <any of the above> recurse into `type:`
* (`Box[T]` `Box`)
* Any other node type returns null (no edge), keeping this emitter at parity
* with the legacy leg.
*/
function goEmbedBaseNameNode(node: SyntaxNode): SyntaxNode | null {
if (node.type === 'type_identifier') return node;
if (node.type === 'qualified_type') {
const name = node.childForFieldName('name');
return name !== null ? goEmbedBaseNameNode(name) : null;
}
if (node.type === 'generic_type') {
const inner = node.childForFieldName('type');
return inner !== null ? goEmbedBaseNameNode(inner) : null;
}
return null;
}
/** First named child of `node` matching `type`, else null. */
function findNamedChildOfType(node: SyntaxNode, type: string): SyntaxNode | null {
for (const child of node.namedChildren) {
if (child.type === type) return child;
}
return null;
}
/**
* Resolve the node passed to `splitGoImportStatement` for an @import.statement
* match. The capture is on the `import_spec`; the original preferred an

View file

@ -15,7 +15,7 @@
*/
import type { Capture, CaptureMatch } from 'gitnexus-shared';
import { findNodeAtRange, nodeToCapture, syntheticCapture } from '../../utils/ast-helpers.js';
import { nodeIfType, nodeToCapture, syntheticCapture } from '../../utils/ast-helpers.js';
import { splitImportDeclaration } from './import-decomposer.js';
import { computeJavaArityMetadata } from './arity-metadata.js';
import { synthesizeJavaReceiverBinding } from './receiver-binding.js';
@ -65,16 +65,26 @@ export function emitJavaScopeCaptures(
for (const m of rawMatches) {
const grouped: Record<string, Capture> = {};
// Parallel tag -> captured SyntaxNode map. The tree-sitter query already
// hands us each matched node as `c.node`, so anchors resolve via a
// type-guarded lookup (`nodeIfType`) instead of re-deriving them with
// `findNodeAtRange(tree.rootNode, ...)` per match — the
// O(matches × rootChildren) root-walk fixed for go #1848 / python #1918 /
// rust/csharp #1915, mirrored here for java (#1951). Every Java scope-query
// anchor below captures directly ON the node the old root-walk re-derived
// (verified against JAVA_SCOPE_QUERY in query.ts), so the type check is exact.
const nodeMap: Record<string, SyntaxNode> = {};
for (const c of m.captures) {
const tag = '@' + c.name;
grouped[tag] = nodeToCapture(tag, c.node);
nodeMap[tag] = c.node;
}
if (Object.keys(grouped).length === 0) continue;
// Decompose each `import_declaration`.
// Decompose each `import_declaration`. `@import.statement` is captured
// directly on the `import_declaration` node.
if (grouped['@import.statement'] !== undefined) {
const stmtCapture = grouped['@import.statement'];
const stmtNode = findNodeAtRange(tree.rootNode, stmtCapture.range, 'import_declaration');
const stmtNode = nodeIfType(nodeMap['@import.statement'], 'import_declaration');
if (stmtNode !== null) {
const decomposed = splitImportDeclaration(stmtNode);
if (decomposed !== null) {
@ -101,9 +111,9 @@ export function emitJavaScopeCaptures(
}
// Filter read.member when it's a child of method_invocation or assignment.
// `@reference.read.member` is captured directly on the `field_access` node.
if (grouped['@reference.read.member'] !== undefined) {
const anchor = grouped['@reference.read.member'];
const memberNode = findNodeAtRange(tree.rootNode, anchor.range, 'field_access');
const memberNode = nodeIfType(nodeMap['@reference.read.member'], 'field_access');
if (memberNode === null || !shouldEmitReadMember(memberNode)) {
continue;
}
@ -113,8 +123,8 @@ export function emitJavaScopeCaptures(
// instance method-like.
if (grouped['@scope.function'] !== undefined) {
out.push(grouped);
const anchor = grouped['@scope.function']!;
const fnNode = findFunctionNode(tree.rootNode, anchor.range);
// `@scope.function` is captured directly on the method/constructor node.
const fnNode = findFunctionNode(nodeMap['@scope.function']);
if (fnNode !== null) {
for (const synth of synthesizeJavaReceiverBinding(fnNode)) {
out.push(synth);
@ -126,8 +136,9 @@ export function emitJavaScopeCaptures(
// Synthesize arity metadata on function-like declarations.
const declTag = FUNCTION_DECL_TAGS.find((t) => grouped[t] !== undefined);
if (declTag !== undefined) {
const anchor = grouped[declTag]!;
const fnNode = findFunctionNode(tree.rootNode, anchor.range);
// FUNCTION_DECL_TAGS (@declaration.method/.constructor) are captured
// directly on the method/constructor node.
const fnNode = findFunctionNode(nodeMap[declTag]);
if (fnNode !== null) {
const arity = computeJavaArityMetadata(fnNode);
if (arity.parameterCount !== undefined) {
@ -159,10 +170,14 @@ export function emitJavaScopeCaptures(
['@reference.call.free', '@reference.call.member', '@reference.call.constructor'] as const
).find((t) => grouped[t] !== undefined);
if (callTag !== undefined && grouped['@reference.arity'] === undefined) {
const anchor = grouped[callTag]!;
const callNode =
findNodeAtRange(tree.rootNode, anchor.range, 'method_invocation') ??
findNodeAtRange(tree.rootNode, anchor.range, 'object_creation_expression');
// @reference.call.free/.member are captured on the `method_invocation`;
// @reference.call.constructor on the `object_creation_expression`. The
// captured node IS the call node the old findNodeAtRange re-derived.
const callNode = nodeIfType(
nodeMap[callTag],
'method_invocation',
'object_creation_expression',
);
if (callNode !== null) {
const argList = callNode.childForFieldName('arguments');
// Exclude interleaved comments — tree-sitter-java emits `block_comment` /
@ -201,7 +216,116 @@ export function emitJavaScopeCaptures(
out.push(grouped);
}
return resolveVarTypeBindings(out);
return [...resolveVarTypeBindings(out), ...synthesizeJavaInheritanceReferences(tree.rootNode)];
}
/**
* Synthesize `@reference.inherits` captures from Java class heritage so the
* registry-primary scope-resolution path emits EXTENDS / IMPLEMENTS edges
* (mirrors C++ `emitCppInheritanceCaptures`). Without this, Java inheritance
* edges came only from the legacy `@heritage.*` path, which is dropped for
* registry-primary languages in the worker pipeline (issue #1951).
*
* Scope covers `class_declaration` (`superclass` extends + `interfaces`
* implements clauses) AND `interface_declaration` (`extends_interfaces`
* interface-to-interface EXTENDS), matching the legacy Java heritage query
* (tree-sitter-queries.ts), which has a dedicated `interface_declaration
* (extends_interfaces (type_list ))` arm. Without the interface arm the
* registry-primary synth silently dropped every `interface IA extends IB`
* edge while the legacy leg emitted it the exact =0/=N parity break #1951
* targets. Enum/record heritage stays unemitted (no legacy arm). Generic
* bases (`extends Box<T>`, `implements IFoo<T>`) ARE emitted here: the legacy
* `@heritage` query was widened to capture the inner `type_identifier` of a
* `generic_type` (tree-sitter-queries.ts), so both paths now agree on SIMPLE
* (unqualified) generic bases the more-correct behavior, consistent with
* C#/Rust (#1951). Qualified bases (`a.b.Base`, `a.b.Box<T>`, `a.b.IFoo<T>`) are
* ALSO now at parity (#1956 tri-review U2): the synth resolves them by their
* `scoped_type_identifier` tail, and the legacy `@heritage` query was widened
* with matching `scoped_type_identifier` arms (plain + generic-wrapped). The
* EXTENDS-vs-IMPLEMENTS split is decided downstream from the resolved target's
* symbol kind (`preEmitInheritanceEdges`): a superclass resolves to a class
* (EXTENDS), an implemented interface resolves to an interface (IMPLEMENTS).
* An `interface IA extends IB` base resolves to an Interface too, so it is
* emitted as IMPLEMENTS matching the legacy `interface_declaration` arm,
* which tags the bases `@heritage.impl` (`kind: 'implements'`) and likewise
* resolves them as interfaces. The synth therefore does not need to know the
* declaration's own kind; it only emits inherits sites and lets the resolved
* target decide the edge type.
* Base names are normalized to their bare simple identifier (`Box<T>` `Box`,
* `java.io.Serializable` `Serializable`) to match the V1 simple-name
* `findClassBindingInScope` contract.
*/
function synthesizeJavaInheritanceReferences(root: SyntaxNode): CaptureMatch[] {
const out: CaptureMatch[] = [];
const stack: SyntaxNode[] = [root];
while (stack.length > 0) {
const node = stack.pop()!;
if (node.type === 'class_declaration') {
const superclass = node.childForFieldName('superclass');
if (superclass !== null) {
for (const base of superclass.namedChildren) emitJavaInheritanceBase(out, base);
}
const interfaces = node.childForFieldName('interfaces');
if (interfaces !== null) {
for (const typeList of interfaces.namedChildren) {
if (typeList === null || typeList.type !== 'type_list') continue;
for (const base of typeList.namedChildren) emitJavaInheritanceBase(out, base);
}
}
} else if (node.type === 'interface_declaration') {
// `interface IA extends IB, IC<T>` — the `extends_interfaces` clause is
// NOT exposed via a tree-sitter field (unlike a class's `superclass` /
// `interfaces`), so scan named children for it. It wraps a `type_list`
// whose bases reuse `javaBaseLookupNameNode` (handles type_identifier /
// generic_type / scoped_type_identifier). These resolve to Interface
// targets, so `preEmitInheritanceEdges` emits them as IMPLEMENTS, at
// parity with the legacy `interface_declaration` @heritage.impl arm.
for (let i = 0; i < node.namedChildCount; i++) {
const extendsInterfaces = node.namedChild(i);
if (extendsInterfaces === null || extendsInterfaces.type !== 'extends_interfaces') continue;
for (const typeList of extendsInterfaces.namedChildren) {
if (typeList === null || typeList.type !== 'type_list') continue;
for (const base of typeList.namedChildren) emitJavaInheritanceBase(out, base);
}
}
}
// Named children only: every type/heritage node we care about is named,
// so skipping unnamed punctuation tokens keeps the walk single-pass and
// lighter on large files.
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child !== null) stack.push(child);
}
}
return out;
}
function emitJavaInheritanceBase(out: CaptureMatch[], base: SyntaxNode | null): void {
if (base === null) return;
const nameNode = javaBaseLookupNameNode(base);
if (nameNode === null) return;
out.push({
'@reference.inherits': nodeToCapture('@reference.inherits', base),
'@reference.name': nodeToCapture('@reference.name', nameNode),
});
}
/** Resolve a Java base-type node to its bare simple-name identifier node. */
function javaBaseLookupNameNode(node: SyntaxNode): SyntaxNode | null {
switch (node.type) {
case 'type_identifier':
return node;
case 'scoped_type_identifier':
// `java.io.Serializable` → trailing `type_identifier` (`Serializable`).
return node.lastNamedChild;
case 'generic_type': {
// `Box<String>` → recurse into the base type (`Box`).
const first = node.firstNamedChild;
return first === null ? null : javaBaseLookupNameNode(first);
}
default:
return null;
}
}
function resolveVarTypeBindings(matches: CaptureMatch[]): CaptureMatch[] {
@ -350,11 +474,16 @@ function inferArgType(argNode: SyntaxNode): string {
}
}
/** Find the first Java function-like node at the given range. */
function findFunctionNode(rootNode: SyntaxNode, range: Capture['range']): SyntaxNode | null {
for (const nodeType of FUNCTION_NODE_TYPES) {
const n = findNodeAtRange(rootNode, range, nodeType);
if (n !== null) return n as SyntaxNode;
}
return null;
/**
* Resolve a Java function-like node from a query-captured node.
*
* The `@scope.function` / `@declaration.method` / `@declaration.constructor`
* anchors all capture directly on the `method_declaration` /
* `constructor_declaration` node (per JAVA_SCOPE_QUERY), so this is a type
* guard against `FUNCTION_NODE_TYPES` the threaded-node equivalent of the
* old `findNodeAtRange(tree.rootNode, range, type)` root-walk, minus the
* O(matches × rootChildren) traversal.
*/
function findFunctionNode(node: SyntaxNode | undefined): SyntaxNode | null {
return nodeIfType(node, ...FUNCTION_NODE_TYPES);
}

View file

@ -75,6 +75,45 @@ function pickFirstDefined(grouped: CaptureMatch, tags: readonly string[]): Captu
return undefined;
}
function pickFirstNode(
groupedNodes: Record<string, SyntaxNode | undefined>,
tags: readonly string[],
): SyntaxNode | undefined {
for (const tag of tags) {
const node = groupedNodes[tag];
if (node !== undefined) return node;
}
return undefined;
}
/** Walks the parent chain from `node` (inclusive), returning the first node
* whose type matches, or null. Faster than `findNodeAtRange` when the caller
* already holds the anchor node avoids re-scanning the tree from the root. */
function findSelfOrAncestorOfType(node: SyntaxNode | undefined, type: string): SyntaxNode | null {
if (node === undefined) return null;
let current: SyntaxNode | null = node;
while (current !== null) {
if (current.type === type) return current;
current = current.parent;
}
return null;
}
/** Walks the parent chain from `node` (inclusive), returning the first node
* whose type is in the set, or null. Plural form of {@link findSelfOrAncestorOfType}. */
function findSelfOrAncestorOfTypes(
node: SyntaxNode | undefined,
types: readonly string[],
): SyntaxNode | null {
if (node === undefined) return null;
let current: SyntaxNode | null = node;
while (current !== null) {
if (types.includes(current.type)) return current;
current = current.parent;
}
return null;
}
/** Filter `@reference.read.member` in non-read contexts (same logic as TS). */
function shouldEmitReadMember(memberNode: SyntaxNode): boolean {
const parent = memberNode.parent;
@ -95,8 +134,17 @@ function shouldEmitReadMember(memberNode: SyntaxNode): boolean {
}
}
/** Find the first JS function-like node at the given range. */
function findFunctionNode(rootNode: SyntaxNode, range: Capture['range']): SyntaxNode | null {
/** Find the first JS function-like node at the given range.
* Prefers the threaded anchor node (walk up its parent chain) so the common
* case avoids a root re-scan; falls back to a range scan from root only when
* the anchor isn't a function-like (or isn't supplied). */
function findFunctionNode(
rootNode: SyntaxNode,
range: Capture['range'],
anchorNode?: SyntaxNode,
): SyntaxNode | null {
const fromAnchor = findSelfOrAncestorOfTypes(anchorNode, FUNCTION_NODE_TYPES);
if (fromAnchor !== null) return fromAnchor;
for (const nodeType of FUNCTION_NODE_TYPES) {
const n = findNodeAtRange(rootNode, range, nodeType);
if (n !== null) return n;
@ -585,6 +633,100 @@ function synthesizeConstructorFieldBindings(root: SyntaxNode, out: CaptureMatch[
}
}
// ─── Inheritance references (EXTENDS) ────────────────────────────────────
/**
* Synthesize `@reference.inherits` captures from JavaScript class heritage so
* the registry-primary scope-resolution path emits EXTENDS edges (mirrors C#
* `synthesizeCsharpInheritanceReferences` / C++ `emitCppInheritanceCaptures`).
* Without this, JS inheritance edges came only from the legacy `@heritage.*`
* path, which the worker pipeline drops for registry-primary languages,
* yielding 0 inheritance edges in worker mode (issue #1951).
*
* Scope is intentionally limited to a `class_declaration`'s `class_heritage`
* base, matching the legacy JavaScript `@heritage` query's class scope and its
* supertype shape descriptor (`javascriptHeritageShapes`:
* `['identifier', 'member_expression']`). JavaScript classes have a single
* `extends` base and no `implements`, so every emission is an EXTENDS (decided
* downstream from the resolved target's symbol kind in
* `preEmitInheritanceEdges`).
*
* Bases handled (at parity with the legacy `@heritage` leg, #1951):
* - `(identifier)` base (`extends Base`) bare simple name.
* - `(member_expression)` base (`extends ns.Base`, `extends a.b.Base`)
* qualified; reduced to its trailing `property_identifier` (`Base`) so the
* V1 `findClassBindingInScope` simple-name contract holds. This mirrors the
* TypeScript `terminalTsTypeNameNode` member_expression arm.
*
* Deliberately NOT emitted (preserving parity with the legacy query, incl. the
* #1943 HOC behavior):
* - `class` EXPRESSION nodes (legacy captures `class_declaration` only).
* - `call_expression` / HOC bases (`extends withFoo(Bar)`) not a legacy
* heritage shape; left to the normal call-resolution path.
*
* The `@reference.name` bare-name text emitted for each base equals
* `normalizeSupertypeName(base)` (the legacy leg's reduction): `Base` `Base`,
* `ns.Base` `Base`, `a.b.Base` `Base` keeping the two legs at parity.
*/
function synthesizeJsInheritanceReferences(root: SyntaxNode, out: CaptureMatch[]): void {
const stack: SyntaxNode[] = [root];
for (;;) {
const node = stack.pop();
if (node === undefined) break;
for (const child of node.namedChildren) {
if (child !== null) stack.push(child);
}
if (node.type !== 'class_declaration') continue;
// Find the `class_heritage` child (holds the single `extends` base).
let heritage: SyntaxNode | null = null;
for (const child of node.namedChildren) {
if (child !== null && child.type === 'class_heritage') {
heritage = child;
break;
}
}
if (heritage === null) continue;
// Emit for `(identifier)` and `(member_expression)` bases — matching the
// legacy heritage shape descriptor (`call_expression` HOC bases excluded).
for (const base of heritage.namedChildren) {
if (base === null) continue;
const nameNode = terminalJsHeritageNameNode(base);
if (nameNode === null) continue;
out.push({
'@reference.inherits': nodeToCapture('@reference.inherits', base),
'@reference.name': nodeToCapture('@reference.name', nameNode),
});
}
}
}
/** Resolve a JavaScript heritage base node to its bare simple-identifier node.
* `Base` (identifier) `Base`, `ns.Base` / `a.b.Base` (member_expression)
* the trailing `property_identifier` `Base`. Mirrors the TypeScript
* `terminalTsTypeNameNode` member_expression arm. Returns null for any other
* shape (e.g. `call_expression` HOC bases), which is then skipped keeping
* parity with the legacy `javascriptHeritageShapes` descriptor and
* `normalizeSupertypeName`'s reduction of each shape. */
function terminalJsHeritageNameNode(node: SyntaxNode): SyntaxNode | null {
switch (node.type) {
case 'identifier':
// `extends ns.Base` parses as a member_expression whose tail is a
// `property_identifier` (not an identifier) — treat it as a leaf name.
case 'property_identifier':
return node;
case 'member_expression': {
// Qualified `ns.Base` / `a.b.Base` → tail identifier `Base`.
const tail = node.lastNamedChild;
return tail === null ? null : terminalJsHeritageNameNode(tail);
}
default:
return null;
}
}
// ─── Main emitter ──────────────────────────────────────────────────────────
export function emitJsScopeCaptures(
@ -607,9 +749,17 @@ export function emitJsScopeCaptures(
for (const m of rawMatches) {
const grouped: Record<string, Capture> = {};
// Parallel tag -> captured SyntaxNode map. The query hands us each matched
// node as c.node, so anchors resolve by walking up from the captured node
// (findSelfOrAncestorOfType[s]) instead of re-deriving them with
// findNodeAtRange(tree.rootNode, ...) per match — the O(matches x N)
// root-walk fixed for go #1915 / python #1918 / csharp, mirrored here
// (mirrors typescript/captures.ts groupedNodes).
const groupedNodes: Record<string, SyntaxNode> = {};
for (const c of m.captures) {
const tag = '@' + c.name;
grouped[tag] = nodeToCapture(tag, c.node);
groupedNodes[tag] = c.node;
}
if (Object.keys(grouped).length === 0) continue;
@ -617,6 +767,10 @@ export function emitJsScopeCaptures(
if (grouped['@import.statement'] !== undefined) {
const stmtCapture = grouped['@import.statement'];
const stmtNode =
findSelfOrAncestorOfTypes(groupedNodes['@import.statement'], [
'import_statement',
'export_statement',
]) ??
findNodeAtRange(tree.rootNode, stmtCapture.range, 'import_statement') ??
findNodeAtRange(tree.rootNode, stmtCapture.range, 'export_statement');
if (stmtNode !== null) {
@ -629,7 +783,9 @@ export function emitJsScopeCaptures(
// Decompose dynamic import() calls.
if (grouped['@import.dynamic'] !== undefined) {
const dynCapture = grouped['@import.dynamic'];
const callNode = findNodeAtRange(tree.rootNode, dynCapture.range, 'call_expression');
const callNode =
findSelfOrAncestorOfType(groupedNodes['@import.dynamic'], 'call_expression') ??
findNodeAtRange(tree.rootNode, dynCapture.range, 'call_expression');
if (callNode !== null) {
const decomposed = splitImportStatement(callNode);
for (const d of decomposed) out.push(d);
@ -640,7 +796,9 @@ export function emitJsScopeCaptures(
// Filter @reference.read.member false-positives.
if (grouped['@reference.read.member'] !== undefined) {
const anchor = grouped['@reference.read.member'];
const memberNode = findNodeAtRange(tree.rootNode, anchor.range, 'member_expression');
const memberNode =
findSelfOrAncestorOfType(groupedNodes['@reference.read.member'], 'member_expression') ??
findNodeAtRange(tree.rootNode, anchor.range, 'member_expression');
if (memberNode === null || !shouldEmitReadMember(memberNode)) {
continue;
}
@ -655,7 +813,11 @@ export function emitJsScopeCaptures(
// scope instead of a phantom Function.
const fnDeclAnchor = grouped['@declaration.function'];
if (fnDeclAnchor !== undefined) {
const arrowNode = findFunctionNode(tree.rootNode, fnDeclAnchor.range);
const arrowNode = findFunctionNode(
tree.rootNode,
fnDeclAnchor.range,
groupedNodes['@declaration.function'],
);
if (arrowNode !== null && isArrayMethodCallbackArrow(arrowNode)) {
continue;
}
@ -665,7 +827,11 @@ export function emitJsScopeCaptures(
}
if (fnDeclAnchor !== undefined) {
const fnNode = findFunctionNode(tree.rootNode, fnDeclAnchor.range);
const fnNode = findFunctionNode(
tree.rootNode,
fnDeclAnchor.range,
groupedNodes['@declaration.function'],
);
if (fnNode !== null && isDefaultExportHocFunctionNode(fnNode)) {
grouped['@declaration.name'] = syntheticCapture(
'@declaration.name',
@ -677,8 +843,9 @@ export function emitJsScopeCaptures(
// Synthesize arity metadata on function-like declarations.
const declAnchor = pickFirstDefined(grouped, FUNCTION_DECL_TAGS);
const declAnchorNode = pickFirstNode(groupedNodes, FUNCTION_DECL_TAGS);
if (declAnchor !== undefined) {
const fnNode = findFunctionNode(tree.rootNode, declAnchor.range);
const fnNode = findFunctionNode(tree.rootNode, declAnchor.range, declAnchorNode);
if (fnNode !== null) {
const arity = computeTsArityMetadata(fnNode);
if (arity.parameterCount !== undefined) {
@ -705,10 +872,26 @@ export function emitJsScopeCaptures(
}
}
// Synthesize @reference.arity on callsites.
// Synthesize @reference.arity on callsites. Skip JSX element anchors: a JSX
// component used as a call argument (e.g. `render(<Foo .../>)`) is itself a
// @reference.call.* anchor, and the ascent below would climb into the
// enclosing call_expression and mis-attribute that call's arity to the
// component. A JSX component reference has no call arity here — this restores
// the pre-#1951 range-based behavior (no call_expression at the JSX range).
// The guard lives at this call site, not inside findSelfOrAncestorOfTypes,
// which is also used by the import-statement and function-scope ascents.
const callAnchor = pickFirstDefined(grouped, CALL_TAGS);
if (callAnchor !== undefined && grouped['@reference.arity'] === undefined) {
const callAnchorNode = pickFirstNode(groupedNodes, CALL_TAGS);
const anchorIsJsxElement =
callAnchorNode?.type === 'jsx_self_closing_element' ||
callAnchorNode?.type === 'jsx_opening_element';
if (
callAnchor !== undefined &&
grouped['@reference.arity'] === undefined &&
!anchorIsJsxElement
) {
const callNode =
findSelfOrAncestorOfTypes(callAnchorNode, ['call_expression', 'new_expression']) ??
findNodeAtRange(tree.rootNode, callAnchor.range, 'call_expression') ??
findNodeAtRange(tree.rootNode, callAnchor.range, 'new_expression');
if (callNode !== null) {
@ -737,7 +920,11 @@ export function emitJsScopeCaptures(
// Synthesize `this` receiver type-bindings on class member functions.
const scopeFnAnchor = grouped['@scope.function'];
if (scopeFnAnchor !== undefined) {
const fnNode = findFunctionNode(tree.rootNode, scopeFnAnchor.range);
const fnNode = findFunctionNode(
tree.rootNode,
scopeFnAnchor.range,
groupedNodes['@scope.function'],
);
if (fnNode !== null) {
const synth = synthesizeTsReceiverBinding(fnNode);
if (synth !== null) out.push(synth);
@ -752,6 +939,7 @@ export function emitJsScopeCaptures(
synthesizeDestructuringBindings(tree.rootNode, out);
synthesizeForOfMapTupleBindings(tree.rootNode, out);
synthesizeInstanceofNarrowings(tree.rootNode, out);
synthesizeJsInheritanceReferences(tree.rootNode, out);
return out;
}

View file

@ -1,6 +1,6 @@
import { makeScopeId, type Capture, type CaptureMatch, type Range } from 'gitnexus-shared';
import { makeScopeId, type Capture, type CaptureMatch } from 'gitnexus-shared';
import {
findNodeAtRange,
nodeIfType,
nodeToCapture,
syntheticCapture,
type SyntaxNode,
@ -38,12 +38,20 @@ export function emitKotlinScopeCaptures(
out.push(...synthesizeKotlinLoopBindings(tree.rootNode, returnTypes));
out.push(...synthesizeKotlinSmartCastBindings(tree.rootNode));
out.push(...synthesizeKotlinLambdaBindings(tree.rootNode, returnTypes));
out.push(...synthesizeKotlinInheritanceReferences(tree.rootNode));
for (const match of getKotlinScopeQuery().matches(tree.rootNode)) {
const grouped: Record<string, Capture> = {};
// Parallel tag -> captured SyntaxNode map. The query hands us each matched
// node as capture.node, so anchors resolve via a type-guarded lookup
// (nodeIfType) instead of re-deriving them with
// findNodeAtRange(tree.rootNode, ...) per match — the O(matches x N)
// root-walk fixed for go #1915 / python #1918 / csharp, mirrored here.
const groupedNodes: Record<string, SyntaxNode> = {};
for (const capture of match.captures) {
const tag = '@' + capture.name;
grouped[tag] = nodeToCapture(tag, capture.node);
groupedNodes[tag] = capture.node;
}
if (Object.keys(grouped).length === 0) continue;
@ -69,11 +77,7 @@ export function emitKotlinScopeCaptures(
}
if (grouped['@import.statement'] !== undefined) {
const importNode = findNodeAtRange(
tree.rootNode,
grouped['@import.statement']!.range,
'import_header',
);
const importNode = nodeIfType(groupedNodes['@import.statement'], 'import_header');
if (importNode !== null) {
const decomposed = splitKotlinImportHeader(importNode);
if (decomposed !== null) {
@ -91,8 +95,7 @@ export function emitKotlinScopeCaptures(
}
if (grouped['@reference.read.member'] !== undefined) {
const anchor = grouped['@reference.read.member']!;
const navNode = findNodeAtRange(tree.rootNode, anchor.range, 'navigation_expression');
const navNode = nodeIfType(groupedNodes['@reference.read.member'], 'navigation_expression');
if (navNode === null || !shouldEmitReadMember(navNode)) continue;
}
@ -114,19 +117,15 @@ export function emitKotlinScopeCaptures(
grouped['@type-binding.name'] !== undefined &&
grouped['@type-binding.type'] !== undefined
) {
const annotation = grouped['@type-binding.annotation']!;
if (propertyDeclHasConstructorValue(tree.rootNode, annotation.range)) {
const propNode = nodeIfType(groupedNodes['@type-binding.annotation'], 'property_declaration');
if (propNode !== null && propertyDeclHasConstructorValue(propNode)) {
continue;
}
}
if (grouped['@scope.function'] !== undefined) {
out.push(grouped);
const fnNode = findNodeAtRange(
tree.rootNode,
grouped['@scope.function']!.range,
'function_declaration',
);
const fnNode = nodeIfType(groupedNodes['@scope.function'], 'function_declaration');
if (fnNode !== null) {
out.push(...synthesizeKotlinReceiverBinding(fnNode));
}
@ -135,11 +134,7 @@ export function emitKotlinScopeCaptures(
const declTag = FUNCTION_DECL_TAGS.find((tag) => grouped[tag] !== undefined);
if (declTag !== undefined) {
const fnNode = findNodeAtRange(
tree.rootNode,
grouped[declTag]!.range,
'function_declaration',
);
const fnNode = nodeIfType(groupedNodes[declTag], 'function_declaration');
if (fnNode !== null) {
const arity = computeKotlinArityMetadata(fnNode);
if (arity.parameterCount !== undefined) {
@ -170,7 +165,7 @@ export function emitKotlinScopeCaptures(
['@reference.call.free', '@reference.call.member', '@reference.call.constructor'] as const
).find((tag) => grouped[tag] !== undefined);
if (callTag !== undefined && grouped['@reference.arity'] === undefined) {
const callNode = findNodeAtRange(tree.rootNode, grouped[callTag]!.range, 'call_expression');
const callNode = nodeIfType(groupedNodes[callTag], 'call_expression');
if (callNode !== null) {
const args = callArguments(callNode);
grouped['@reference.arity'] = syntheticCapture(
@ -188,13 +183,90 @@ export function emitKotlinScopeCaptures(
out.push(grouped);
const extensionFallback = extensionFreeCallFallback(grouped, tree.rootNode);
const extensionFallback = extensionFreeCallFallback(grouped, groupedNodes);
if (extensionFallback !== null) out.push(extensionFallback);
}
return out;
}
/**
* Synthesize `@reference.inherits` captures from Kotlin `class_declaration`
* delegation specifiers so the registry-primary scope-resolution path emits
* EXTENDS / IMPLEMENTS edges (mirrors C# `synthesizeCsharpInheritanceReferences`
* and C++ `emitCppInheritanceCaptures`). Without this, Kotlin inheritance edges
* came only from the legacy `@heritage.*` path, which the worker pipeline drops
* for registry-primary languages 0 inheritance edges in worker mode (#1951).
*
* Scope mirrors the legacy KOTLIN_QUERIES `@heritage.extends` patterns exactly
* (the config-driven `kotlinHeritageShapes`: `user_type`,
* `constructor_invocation`, `explicit_delegation`). Each `delegation_specifier`
* child of a `class_declaration`, in one of three forms
* - bare interface/superclass: `class Foo : Bar`
* `(delegation_specifier (user_type (type_identifier)))`
* - constructor-call superclass: `class Foo : Bar()`
* `(delegation_specifier (constructor_invocation (user_type (type_identifier))))`
* - interface delegation: `class Foo : Bar by delegate`
* `(delegation_specifier (explicit_delegation (user_type (type_identifier)) …))`
* the delegated interface is the LEADING `user_type`; the trailing
* delegate expression (`by delegate`) is NOT a supertype (#1951). This is
* the dropped shape the registry-primary synth previously skipped, leaving
* `class F : Iface by d` with no IMPLEMENTS edge in worker mode.
*
* Kotlin uses `:` for BOTH superclass and interfaces the EXTENDS-vs-IMPLEMENTS
* split is decided downstream from the resolved target's symbol kind
* (`preEmitInheritanceEdges`), so every base is emitted with the same `inherits`
* kind here. The bare lookup name is normalized to the simple identifier
* (`Base()` `Base`, `Base<T>` `Base`, `pkg.Base` `Base`,
* `Iface by d` `Iface`) so V1's simple-name `findClassBindingInScope`
* resolves it. The extracted bare name agrees with the legacy leg's
* `normalizeSupertypeName` for every shape (verified by real-parse).
*/
function synthesizeKotlinInheritanceReferences(rootNode: SyntaxNode): CaptureMatch[] {
const out: CaptureMatch[] = [];
for (const classNode of descendantsOfType(rootNode, 'class_declaration')) {
for (const child of classNode.namedChildren) {
if (child.type !== 'delegation_specifier') continue;
// Three wrappers, all resolving to a leading `user_type` →
// `type_identifier`:
// - `(delegation_specifier (constructor_invocation (user_type …)))` for `Base()`
// - `(delegation_specifier (explicit_delegation (user_type …) <delegate>))`
// for `Iface by d` — the supertype is the FIRST `user_type`; the
// delegate expression that trails `by` is ignored.
// - `(delegation_specifier (user_type …))` for a bare interface/superclass.
const ctor = child.namedChildren.find((n) => n.type === 'constructor_invocation');
const delegation = child.namedChildren.find((n) => n.type === 'explicit_delegation');
const userType =
ctor?.namedChildren.find((n) => n.type === 'user_type') ??
delegation?.namedChildren.find((n) => n.type === 'user_type') ??
child.namedChildren.find((n) => n.type === 'user_type');
if (userType === undefined) continue;
const nameNode = kotlinUserTypeNameNode(userType);
if (nameNode === null) continue;
out.push({
'@reference.inherits': nodeToCapture('@reference.inherits', child),
'@reference.name': nodeToCapture('@reference.name', nameNode),
});
}
}
return out;
}
/**
* The bare simple-name `type_identifier` of a `user_type`. Strips generic
* type arguments (`Base<T>` `Base`) and qualifier tails (`pkg.Base` `Base`)
* by taking the LAST direct `type_identifier` child, matching the legacy
* `(user_type (type_identifier) @heritage.extends)` capture and V1's
* simple-name `findClassBindingInScope` contract.
*/
function kotlinUserTypeNameNode(userType: SyntaxNode): SyntaxNode | null {
let nameNode: SyntaxNode | null = null;
for (const child of userType.namedChildren) {
if (child.type === 'type_identifier') nameNode = child;
}
return nameNode;
}
function synthesizeKotlinLoopBindings(
rootNode: SyntaxNode,
returnTypes: ReadonlyMap<string, string>,
@ -1045,13 +1117,11 @@ function shouldEmitReadMember(navNode: SyntaxNode): boolean {
return true;
}
/** True when the property_declaration anchored at `range` has a
* `call_expression` value sibling (i.e. `val x: T = Foo()`). Used to
* suppress the explicit-annotation type-binding capture so the
* constructor-inferred binding wins (#1762). */
function propertyDeclHasConstructorValue(rootNode: SyntaxNode, range: Range): boolean {
const propNode = findNodeAtRange(rootNode, range, 'property_declaration');
if (propNode === null) return false;
/** True when the given `property_declaration` has a `call_expression`
* value sibling (i.e. `val x: T = Foo()`). Used to suppress the
* explicit-annotation type-binding capture so the constructor-inferred
* binding wins (#1762). */
function propertyDeclHasConstructorValue(propNode: SyntaxNode): boolean {
const variable = propNode.namedChildren.find((c) => c.type === 'variable_declaration');
if (variable === undefined) return false;
const value = propNode.namedChildren.find(
@ -1097,17 +1167,20 @@ function inferArgType(argNode: SyntaxNode): string {
function extensionFreeCallFallback(
grouped: Record<string, Capture>,
rootNode: SyntaxNode,
groupedNodes: Record<string, SyntaxNode>,
): CaptureMatch | null {
const member = grouped['@reference.call.member'];
const receiver = grouped['@reference.receiver'];
const name = grouped['@reference.name'];
if (member === undefined || receiver === undefined || name === undefined) return null;
const callNode = findNodeAtRange(rootNode, member.range, 'call_expression');
// The `@reference.call.member` anchor IS the `call_expression`, and the
// `@reference.receiver` anchor IS the receiver node — both threaded from the
// query match (no per-match root walk).
const callNode = nodeIfType(groupedNodes['@reference.call.member'], 'call_expression');
if (callNode === null) return null;
const receiverNode = findNodeAtRange(rootNode, receiver.range);
if (receiverNode === null || !isLiteralReceiver(receiverNode)) return null;
const receiverNode = groupedNodes['@reference.receiver'];
if (receiverNode === undefined || !isLiteralReceiver(receiverNode)) return null;
const out: Record<string, Capture> = {
'@reference.call.free': syntheticCapture('@reference.call.free', callNode, callNode.text),

View file

@ -32,7 +32,12 @@
*/
import type { Capture, CaptureMatch } from 'gitnexus-shared';
import { nodeIfType, nodeToCapture, syntheticCapture } from '../../utils/ast-helpers.js';
import {
nodeIfType,
nodeToCapture,
syntheticCapture,
walkNamedTree,
} from '../../utils/ast-helpers.js';
import { splitNamespaceUseDeclaration } from './import-decomposer.js';
import { computePhpArityMetadata } from './arity-metadata.js';
import { synthesizePhpReceiverBinding } from './receiver-binding.js';
@ -291,9 +296,133 @@ export function emitPhpScopeCaptures(
out.push(grouped);
}
out.push(...synthesizePhpInheritanceReferences(tree.rootNode));
return out;
}
// ─── PHP inheritance synthesis ───────────────────────────────────────────────
/**
* Synthesize `@reference.inherits` captures from PHP class/trait heritage so
* the registry-primary scope-resolution path emits EXTENDS / IMPLEMENTS edges
* (mirrors C# `synthesizeCsharpInheritanceReferences` / C++
* `emitCppInheritanceCaptures`). Without this, PHP inheritance edges came only
* from the legacy `@heritage.*` path, which the worker pipeline drops for
* registry-primary languages (issue #1951).
*
* Scope matches the legacy PHP heritage query (tree-sitter-queries.ts
* PHP_QUERIES @heritage.extends / @heritage.implements / @heritage.trait):
*
* 1. `class_declaration` > `base_clause` > [(name) (qualified_name)] extends
* 2. `class_declaration` > `class_interface_clause` > [(name) (qualified_name)] implements
* 3. `class_declaration` body `use_declaration` > [(name) (qualified_name)] trait use
* 4. `trait_declaration` body `use_declaration` > [(name) (qualified_name)] trait use
*
* The EXTENDS-vs-IMPLEMENTS split is decided downstream from the resolved
* target's symbol kind (`preEmitInheritanceEdges`: `Interface` IMPLEMENTS,
* else EXTENDS), so all bases emit the same `inherits` kind here. The base
* lookup name is normalized to its bare simple identifier (`Foo\Bar\Base`
* `Base`) to match the V1 simple-name `findClassBindingInScope` contract.
*
* NOTE (#1951 trait-use parity): legacy emits trait-use as an IMPLEMENTS edge
* (`heritage.trait` `trait-impl` IMPLEMENTS in heritage-processor.ts), and
* the central pass matches it `preEmitInheritanceEdges` (run.ts) maps a
* resolved `Interface` OR `Trait` target to IMPLEMENTS (`type === 'Interface'
* || type === 'Trait' ? 'IMPLEMENTS' : 'EXTENDS'`), so `use Trait` resolves to
* IMPLEMENTS on both the legacy and registry-primary paths.
*/
function synthesizePhpInheritanceReferences(root: SyntaxNode): CaptureMatch[] {
const out: CaptureMatch[] = [];
walkNamedTree(root, (node) => {
if (node.type === 'class_declaration') {
// extends: single base_clause child carrying one base name.
const baseClause = findNamedChild(node, 'base_clause');
if (baseClause !== null) emitPhpBaseNames(baseClause, out);
// implements: class_interface_clause may list several interfaces.
const ifaceClause = findNamedChild(node, 'class_interface_clause');
if (ifaceClause !== null) emitPhpBaseNames(ifaceClause, out);
// trait use: `use TraitName;` inside the class body.
emitPhpTraitUses(node, out);
} else if (node.type === 'trait_declaration') {
// trait-uses-trait: `use OtherTrait;` inside a trait body.
emitPhpTraitUses(node, out);
}
});
return out;
}
/**
* Emit `@reference.inherits` for every `use_declaration` (trait use) in the
* declaration body of `node` (a class_declaration or trait_declaration).
* Class-body `use_declaration` is the trait-use node (distinct from the
* top-level `namespace_use_declaration` import node).
*/
function emitPhpTraitUses(node: SyntaxNode, out: CaptureMatch[]): void {
const body = node.childForFieldName('body');
if (body === null || body.type !== 'declaration_list') return;
for (let i = 0; i < body.namedChildCount; i++) {
const child = body.namedChild(i);
if (child !== null && child.type === 'use_declaration') {
emitPhpBaseNames(child, out);
}
}
}
/**
* Walk the named children of a heritage clause (`base_clause`,
* `class_interface_clause`, or `use_declaration`) and emit one
* `@reference.inherits` match per `name` / `qualified_name` base. The lookup
* name is the bare tail identifier so `findClassBindingInScope` resolves it.
*/
function emitPhpBaseNames(clause: SyntaxNode, out: CaptureMatch[]): void {
for (let i = 0; i < clause.namedChildCount; i++) {
const base = clause.namedChild(i);
if (base === null) continue;
if (base.type !== 'name' && base.type !== 'qualified_name') continue;
const bareName = phpBareBaseName(base);
if (bareName === '') continue;
out.push({
'@reference.inherits': nodeToCapture('@reference.inherits', base),
'@reference.name': syntheticCapture('@reference.name', base, bareName),
});
}
}
/**
* Normalize a PHP base node to its bare simple identifier:
* `Base` (name) `Base`
* `Foo\Bar\Base` (qualified_name) `Base` (last `name` child)
* `\Foo\Base` (qualified_name) `Base`
* Mirrors C#'s `terminalTypeNameNode`: strip the qualifier tail so the V1
* simple-name scope-chain lookup resolves the target def.
*/
function phpBareBaseName(base: SyntaxNode): string {
if (base.type === 'name') return base.text;
if (base.type === 'qualified_name') {
// qualified_name holds one or more `name` children (plus `\` separators);
// the bare class is the last `name` child.
for (let i = base.namedChildCount - 1; i >= 0; i--) {
const child = base.namedChild(i);
if (child !== null && child.type === 'name') return child.text;
}
// Fallback: split the raw text on the namespace separator.
const segs = base.text.split('\\').filter((s) => s.length > 0);
return segs.length > 0 ? segs[segs.length - 1]! : '';
}
return '';
}
/** Find the first named child of `node` with the given type. */
function findNamedChild(node: SyntaxNode, type: string): SyntaxNode | null {
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child !== null && child.type === type) return child;
}
return null;
}
/** Pre-order walk over named children, invoking `cb` on each node. */
// ─── PHP receiver normalization ──────────────────────────────────────────────
/**

View file

@ -17,7 +17,12 @@
*/
import type { Capture, CaptureMatch } from 'gitnexus-shared';
import { nodeToCapture, syntheticCapture, type SyntaxNode } from '../../utils/ast-helpers.js';
import {
nodeToCapture,
syntheticCapture,
walkNamedTree,
type SyntaxNode,
} from '../../utils/ast-helpers.js';
import { splitImportStatement } from './import-decomposer.js';
import { getPythonParser, getPythonScopeQuery } from './query.js';
import { synthesizeReceiverTypeBinding } from './receiver-binding.js';
@ -162,9 +167,104 @@ export function emitPythonScopeCaptures(
out.push(grouped);
}
out.push(...synthesizePythonInheritanceReferences(tree.rootNode));
return out;
}
/**
* Synthesize `@reference.inherits` captures from Python class superclass
* lists so the registry-primary scope-resolution path emits EXTENDS edges
* (mirrors C#'s `synthesizeCsharpInheritanceReferences` / C++'s
* `emitCppInheritanceCaptures` / TypeScript's `synthesizeTsInheritanceReferences`).
* Without this, Python inheritance edges came only from the legacy
* `@heritage.*` path, which is dropped for registry-primary languages in the
* worker pipeline (issue #1951).
*
* Scope matches the legacy Python heritage leg (config-driven since #1940):
* every direct base in the `superclasses` `argument_list`, resolved to its bare
* simple name. Three base shapes that the previous synth DROPPED and so
* silently omitted in production while the legacy `@heritage` leg captured them
* are now handled (#1951):
*
* - `class C(pkg.Base)` `attribute` (trailing `.attribute` id `Base`)
* - `class C(pkg.sub.Base)` nested `attribute` (recurse `Base`)
* - `class C(Generic[T])` `subscript` (`.value` id `Generic`)
*
* The bare-name text MUST agree with `normalizeSupertypeName` (the legacy leg's
* reduction in heritage-extractors/supertype-alternation.ts) so both legs emit
* the same edge under the CI scope-parity gate: `pkg.Base` `Base`,
* `Generic[T]` `Generic`, `pkg.Container[str]` `Container`. Verified by a
* real tree-sitter-python parse. The simple `identifier` base keeps its exact
* prior capture (the base node itself).
*
* Tuple/multi bases (`class C(A, pkg.B, Gen[T])`) already iterate here each
* `argument_list` named child is one base. Python has no interfaces, so every
* base resolves to a Class and the central `preEmitInheritanceEdges` pass emits
* EXTENDS; the EXTENDS-vs-IMPLEMENTS split is decided downstream from the
* resolved target's symbol kind, so all bases are emitted with the same
* `inherits` kind here.
*/
function synthesizePythonInheritanceReferences(root: SyntaxNode): CaptureMatch[] {
const out: CaptureMatch[] = [];
walkNamedTree(root, (node) => {
if (node.type !== 'class_definition') return;
const superclasses = node.childForFieldName('superclasses');
if (superclasses === null || superclasses.type !== 'argument_list') return;
for (let i = 0; i < superclasses.namedChildCount; i++) {
const base = superclasses.namedChild(i);
if (base === null) continue;
const nameNode = pythonBaseLookupNameNode(base);
if (nameNode === null) continue;
out.push({
'@reference.inherits': nodeToCapture('@reference.inherits', base),
'@reference.name': nodeToCapture('@reference.name', nameNode),
});
}
});
return out;
}
/**
* Reduce a Python superclass base node to the bare simple-identifier node whose
* `.text` is the lookup name `findClassBindingInScope` resolves. Mirrors the
* TypeScript `terminalTsTypeNameNode` / C++ `extractBaseLookupName` reference
* patterns, and its returned node's `.text` is contractually equal to
* `normalizeSupertypeName(base)` for every shape (real-parse verified):
*
* - `identifier` (`Base`) the node itself
* - `attribute` (`pkg.Base`,
* `pkg.sub.Base`) trailing `attribute:` identifier `Base`
* - `subscript` (`Generic[T]`,
* `pkg.Container[T]`) `value:` (recurse, strips `[...]` and
* any qualifier) `Generic` / `Container`
*
* Returns null for any other shape (no leaf identifier reachable), so it never
* emits a spurious edge.
*/
function pythonBaseLookupNameNode(base: SyntaxNode): SyntaxNode | null {
switch (base.type) {
case 'identifier':
return base;
case 'attribute': {
// `pkg.Base` / `pkg.sub.Base`: the `attribute:` field is the trailing
// simple-identifier segment (`Base`); recurse so chained dotted paths
// still resolve to the final identifier.
const attr = base.childForFieldName('attribute');
return attr === null ? null : pythonBaseLookupNameNode(attr);
}
case 'subscript': {
// `Generic[T]` / `pkg.Container[str]`: the `value:` field is the
// subscripted base (identifier or attribute); recurse to strip the
// `[...]` slice and any qualifier, reaching the bare base name.
const value = base.childForFieldName('value');
return value === null ? null : pythonBaseLookupNameNode(value);
}
default:
return null;
}
}
function scopeExtractionError(stage: string, filePath: string, err: unknown): Error {
const reason = err instanceof Error ? err.message : String(err);
return new Error(

View file

@ -1,8 +1,10 @@
import type { Capture, CaptureMatch } from 'gitnexus-shared';
import {
findChild,
nodeIfType,
nodeToCapture,
syntheticCapture,
walkNamedTree,
type SyntaxNode,
} from '../../utils/ast-helpers.js';
import { getRubyParser, getRubyScopeQuery } from './query.js';
@ -430,9 +432,99 @@ export function emitRubyScopeCaptures(
}
}
// Fifth pass: superclass inheritance (`class Foo < Bar`).
// Emit `@reference.inherits` captures so the registry-primary scope-
// resolution path produces EXTENDS edges (issue #1951). This mirrors the
// C#/C++ inheritance synthesis: Ruby's superclass edges previously came
// only from the legacy `@heritage.extends` query, which the worker
// pipeline drops for registry-primary languages → 0 inheritance edges in
// worker mode. Mixins (include/extend/prepend) are NOT touched here — they
// flow through `emitHeritageEdges` (the `__heritage__:` import path above),
// an independent lane that stays intact when legacy @heritage is gated off.
out.push(...synthesizeRubySuperclassReferences(tree.rootNode));
return out;
}
/**
* Synthesize `@reference.inherits` captures from Ruby `class Foo < Bar`
* superclass declarations so the shared `preEmitInheritanceEdges` pass can
* resolve the base to a Class def and emit an EXTENDS edge.
*
* Scope is `class` nodes whose `superclass` field holds either a bare
* `constant` base (`class D < Super`) or a qualified/scoped
* `scope_resolution` base (`class C < Outer::Super`, `class E < A::B::C`)
* exactly the two shapes the config-driven legacy `@heritage.extends`
* alternation now captures (heritage-extractors/configs/ruby.ts
* `rubyHeritageShapes: ['constant', 'scope_resolution']`):
*
* (class
* name: (constant) @heritage.class
* superclass: (superclass
* [(constant) (scope_resolution)] @heritage.extends)) @heritage
*
* Previously this pass emitted only for a direct `(constant)` child, so the
* production registry-primary path silently dropped `Outer::Super`
* superclasses while the legacy @heritage leg captured them the exact
* EXTENDS/IMPLEMENTS-drop bug of #1951.
*
* THE PARITY CONTRACT: the `@reference.name` bare text must equal the legacy
* leg's `normalizeSupertypeName(baseNode)` reduction. For a `scope_resolution`
* (`Outer::Super`, `A::B::C`) the normalizer recurses into the `name:` field
* and returns the trailing `constant` (`Super` / `C`); this synth mirrors that
* by reading the same `name:` tail. A bare `constant` is unchanged
* (byte-identical to the prior emission). `module` nodes are excluded (no
* superclass field). Mixins (include/extend/prepend) are untouched they flow
* through the `__heritage__:` import lane above.
*
* Edge type (EXTENDS vs IMPLEMENTS) is decided downstream from the resolved
* target's symbol kind this pass only emits `@reference.inherits`.
*/
function synthesizeRubySuperclassReferences(root: SyntaxNode): CaptureMatch[] {
const out: CaptureMatch[] = [];
walkNamedTree(root, (node) => {
if (node.type !== 'class') return;
const superclass = node.childForFieldName('superclass');
if (superclass === null) return;
const baseNode = extractRubySuperclassBaseNode(superclass);
if (baseNode === null) return;
out.push({
'@reference.inherits': nodeToCapture('@reference.inherits', baseNode),
'@reference.name': nodeToCapture('@reference.name', baseNode),
});
});
return out;
}
/**
* Reduce a Ruby `superclass` node to the bare `constant` the resolver should
* look up, at parity with the legacy heritage leg's
* `normalizeSupertypeName(baseNode)`:
*
* - direct `(constant)` child (`class D < Super`) that constant
* (unchanged from the original emission kept byte-identical)
* - `(scope_resolution)` child (`class C < Outer::Super`,
* `class E < A::B::C`) the trailing
* `name:` constant (`Super` / `C`)
*
* A `scope_resolution` nests qualifier-first, name-last
* (`scope: (...) name: (constant)`), so the `name:` field is always the
* trailing simple identifier the same tail `normalizeSupertypeName` reaches
* by recursing through its `name` field. Any other shape returns null (no
* edge), keeping this emitter at parity with the legacy alternation
* (`['constant', 'scope_resolution']`).
*/
function extractRubySuperclassBaseNode(superclass: SyntaxNode): SyntaxNode | null {
const directConstant = findChild(superclass, 'constant');
if (directConstant !== null) return directConstant;
const scoped = findChild(superclass, 'scope_resolution');
if (scoped !== null) {
const tail = scoped.childForFieldName('name');
if (tail !== null && tail.type === 'constant') return tail;
}
return null;
}
function decomposeRubyImport(callNode: SyntaxNode, anchor: Capture): CaptureMatch | null {
const methodNode = callNode.childForFieldName('method');
if (methodNode === null) return null;

View file

@ -3,6 +3,7 @@ import {
nodeIfType,
nodeToCapture,
syntheticCapture,
walkNamedTree,
type SyntaxNode,
} from '../../utils/ast-helpers.js';
import { getRustParser, getRustScopeQuery } from './query.js';
@ -161,9 +162,86 @@ export function emitRustScopeCaptures(
out.push(grouped);
}
out.push(...synthesizeRustInheritanceReferences(tree.rootNode));
return out;
}
/**
* Synthesize `@reference.inherits` captures from Rust trait `impl` blocks so
* the registry-primary scope-resolution path can emit the IMPLEMENTS edge for
* `impl Trait for Struct` (mirrors the legacy `@heritage.trait`/`@heritage.class`
* path, which the worker pipeline drops for registry-primary languages #1951).
*
* Rust inheritance is structurally unlike a base list on a type declaration:
* the relationship lives on `impl_item { trait: T, type: S }`, meaning
* `S IMPLEMENTS T`. The shared `preEmitInheritanceEdges` derives an edge's
* SOURCE from the enclosing *class* def of the `@reference.inherits` site, but
* an `impl_item` scope owns no class-like def (the struct `S` is declared
* elsewhere as a `struct_item`), so `findEnclosingClassDef` returns undefined
* and that pass emits nothing for these sites (it still marks them handled,
* suppressing the generic reference bridge). The real IMPLEMENTS edge is
* therefore emitted by `rustScopeResolver.emitHeritageEdges`, which reads these
* sites back from `parsedFiles[*].referenceSites`.
*
* To carry both ends of the relationship through a single reference site we
* encode: `@reference.name` = the trait `T` (becomes `site.name`, the IMPLEMENTS
* target) and `@reference.receiver` = the struct `S` (becomes
* `site.explicitReceiver.name`, the IMPLEMENTS source).
*
* Parity is intentionally pinned to the legacy heritage query's `impl_item`
* patterns: both `trait:` and `type:` normalize to the base's trailing bare
* `type_identifier` directly, via a `scoped_type_identifier`'s `name:` tail
* (`crate::traits::Drawable` `Drawable`; KTD-1 tail resolution), or through a
* `generic_type`'s `type:` field (which may itself be either). Inherent impls
* (`impl S {}`, no `trait:` field) still emit nothing.
*/
function synthesizeRustInheritanceReferences(root: SyntaxNode): CaptureMatch[] {
const out: CaptureMatch[] = [];
walkNamedTree(root, (node) => {
if (node.type !== 'impl_item') return;
const traitField = node.childForFieldName('trait');
const typeField = node.childForFieldName('type');
if (traitField === null || typeField === null) return;
const traitName = bareTypeIdentifier(traitField);
const structName = bareTypeIdentifier(typeField);
if (traitName === null || structName === null) return;
out.push({
'@reference.inherits': nodeToCapture('@reference.inherits', traitName),
'@reference.name': nodeToCapture('@reference.name', traitName),
'@reference.receiver': syntheticCapture('@reference.receiver', structName, structName.text),
});
});
return out;
}
/**
* Normalize a `trait:` / `type:` impl_item field to the base's trailing bare
* `type_identifier`, matching exactly the node shapes the legacy `@heritage`
* query accepts (kept at parity see the `impl_item` heritage arm in
* tree-sitter-queries.ts):
* - `type_identifier` the node itself
* - `scoped_type_identifier name: (type_identifier)` the trailing `name:` id
* (`crate::traits::Drawable` `Drawable`; KTD-1 tail resolution the
* simple name then resolves scope-aware via `emitRustTraitImplEdges`)
* - `generic_type type: <any of the above>` recurse into `type:`
* (covers `Box<T>` and `m::Wrapped<T>`)
* Any other node type returns null (no edge), keeping this emitter at parity
* with the legacy query.
*/
function bareTypeIdentifier(node: SyntaxNode): SyntaxNode | null {
if (node.type === 'type_identifier') return node;
if (node.type === 'scoped_type_identifier') {
const tail = node.childForFieldName('name');
return tail !== null && tail.type === 'type_identifier' ? tail : null;
}
if (node.type === 'generic_type') {
const inner = node.childForFieldName('type');
return inner !== null ? bareTypeIdentifier(inner) : null;
}
return null;
}
function findEnclosingImpl(node: SyntaxNode): SyntaxNode | null {
let current: SyntaxNode | null = node.parent;
while (current !== null) {

View file

@ -129,6 +129,15 @@ export function getImplTraitName(implNode: SyntaxNode): string | null {
return null;
}
// NOTE: this strips reference/pointer sigils and generic arguments but NOT a
// path qualifier, so `crate::traits::Drawable` stays qualified here — whereas
// the inheritance synth (rust/captures.ts `bareTypeIdentifier`) resolves scoped
// bases by their trailing simple name (`Drawable`). The two intentionally
// diverge for scoped paths. This is inert today (`getImplTraitName` has no
// ingestion consumer and Rust's `isSuperReceiver` is false, so nothing keys an
// edge on this name); the synth is the single source of truth for the
// inheritance edge. A future change that wires `getImplTraitName` into
// resolution must reconcile this with the synth's tail-only normalization.
function normalizeRustTypeName(text: string): string {
let t = text.trim();
while (t.startsWith('&')) t = t.replace(/^&\s*(mut\s+)?/, '');

View file

@ -6,8 +6,96 @@ import { rustProvider } from '../rust.js';
import { rustArityCompatibility, rustMergeBindings, resolveRustImportTarget } from './index.js';
import { populateRustOwners } from './method-owners.js';
import { populateRustRangeBindings } from './range-binding.js';
import { isClassLike } from '../../scope-resolution/scope/walkers.js';
import {
isClassLike,
findClassBindingInScope,
resolveAmbiguousInheritanceBaseViaImports,
} from '../../scope-resolution/scope/walkers.js';
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
import { resolveDefGraphId } from '../../scope-resolution/graph-bridge/ids.js';
import type { GraphNodeLookup } from '../../scope-resolution/graph-bridge/node-lookup.js';
import type { KnowledgeGraph } from '../../../graph/types.js';
import { generateId } from '../../../../lib/utils.js';
/**
* Emit Rust `S IMPLEMENTS T` edges from `impl T for S` trait implementations.
*
* Rust inheritance is not a base list on the type declaration it lives on
* `impl_item { trait: T, type: S }`. The shared `preEmitInheritanceEdges` pass
* derives an `@reference.inherits` site's edge SOURCE from the enclosing class
* def, but an `impl_item` scope owns no class-like def, so that pass cannot
* produce these edges (it only marks the sites handled). The `@reference.inherits`
* sites synthesized in `captures.ts` carry the trait `T` as `site.name` (target)
* and the struct `S` as `site.explicitReceiver.name` (source); this hook reads
* them back and emits the IMPLEMENTS edge with source `S`, target `T`, and the
* legacy `'trait-impl'` reason matching the legacy `@heritage` DAG (#1951).
*
* Resolution is scope-aware and import-aware, mirroring the shared
* `preEmitInheritanceEdges` pass: both `S` and `T` resolve from the `impl`
* block's own scope via `findClassBindingInScope` (scope-chain + single-match
* fallbacks), then `resolveAmbiguousInheritanceBaseViaImports` for a name that
* several modules declare (disambiguated by the referencing file's `use`
* imports). A trait `T` is commonly declared in a different file (e.g. the
* `rust-traits` fixture imports `Drawable`/`Clickable` from a sibling module);
* the scope chain reaches it through those `use` bindings. When a name does
* not resolve to exactly one class-like def unresolved (e.g. a std trait
* like `Default`) OR ambiguous across modules (two same-named `struct`s /
* traits) NO edge is emitted, restoring the legacy file-scoped path's
* "a wrong edge is worse than no edge" invariant. (The prior global
* simple-name index used last-write-wins and could source an `impl` edge from
* the wrong same-named def across modules.) Idempotent: pre-seeds the dedup
* set from existing IMPLEMENTS edges so a worker-mode legacy emission (or a
* re-resolution) is not duplicated.
*/
function emitRustTraitImplEdges(
graph: KnowledgeGraph,
parsedFiles: readonly ParsedFile[],
nodeLookup: GraphNodeLookup,
scopes: ScopeResolutionIndexes | undefined,
): void {
if (scopes === undefined) return;
const emitted = new Set<string>();
for (const rel of graph.iterRelationshipsByType('IMPLEMENTS')) {
emitted.add(`${rel.sourceId}->${rel.targetId}`);
}
for (const parsed of parsedFiles) {
for (const site of parsed.referenceSites) {
if (site.kind !== 'inherits') continue;
const structName = site.explicitReceiver?.name;
const traitName = site.name;
if (structName === undefined || structName === '' || traitName === '') continue;
// Scope-aware (+ import-aware) resolution from the impl block's scope.
// Refuse when either end is unresolved or ambiguous.
const structDef =
findClassBindingInScope(site.inScope, structName, scopes) ??
resolveAmbiguousInheritanceBaseViaImports(site.inScope, structName, scopes);
const traitDef =
findClassBindingInScope(site.inScope, traitName, scopes) ??
resolveAmbiguousInheritanceBaseViaImports(site.inScope, traitName, scopes);
if (structDef === undefined || traitDef === undefined) continue;
const structGraphId = resolveDefGraphId(structDef.filePath, structDef, nodeLookup);
const traitGraphId = resolveDefGraphId(traitDef.filePath, traitDef, nodeLookup);
if (structGraphId === undefined || traitGraphId === undefined) continue;
const edgeKey = `${structGraphId}->${traitGraphId}`;
if (emitted.has(edgeKey)) continue;
emitted.add(edgeKey);
graph.addRelationship({
id: generateId('IMPLEMENTS', `${edgeKey}:trait-impl`),
sourceId: structGraphId,
targetId: traitGraphId,
type: 'IMPLEMENTS',
confidence: 0.85,
reason: 'trait-impl',
});
}
}
}
function buildRustMro(
graph: Parameters<ScopeResolver['buildMro']>[0],
@ -66,6 +154,9 @@ export const rustScopeResolver: ScopeResolver = {
buildMro: (graph, parsedFiles, nodeLookup) => buildRustMro(graph, parsedFiles, nodeLookup),
emitHeritageEdges: (graph, parsedFiles, nodeLookup, scopes) =>
emitRustTraitImplEdges(graph, parsedFiles, nodeLookup, scopes),
populateOwners: (parsed: ParsedFile) => populateRustOwners(parsed),
isSuperReceiver: () => false,

View file

@ -0,0 +1,28 @@
import type { SyntaxNode } from '../../utils/ast-helpers.js';
/**
* Trailing simple-name `type_identifier` of a Swift `user_type` base. A
* qualified `Outer.Inner` parses flat as
* `(user_type (type_identifier "Outer") (type_identifier "Inner"))`, and the
* actual base type is the TRAILING segment `Inner` (mirrors Java
* `scoped_type_identifier` `lastNamedChild` and TS `nested_type_identifier`
* tail). Generic arguments live in a sibling `type_arguments` node never a
* `type_identifier` so they are skipped: `Box<Int>` `Box`,
* `Outer.Inner<T>` `Inner`. Returns null when the `user_type` has no
* `type_identifier` child.
*
* Shared by `swiftBaseTypeIdentifier` (captures.ts returns the node for an
* `@reference.inherits` site) and `firstInheritedType` (receiver-binding.ts
* reads `.text` for `super` receiver binding). It lives in this leaf module
* rather than being exported from captures.ts because captures.ts already
* imports receiver-binding.ts, so a captures.ts export would create a
* bidirectional import cycle (#1956 tri-review U7).
*/
export function swiftQualifiedBaseTail(userType: SyntaxNode): SyntaxNode | null {
let last: SyntaxNode | null = null;
for (let i = 0; i < userType.namedChildCount; i++) {
const child = userType.namedChild(i);
if (child !== null && child.type === 'type_identifier') last = child;
}
return last;
}

View file

@ -37,9 +37,11 @@ import {
nodeIfType,
nodeToCapture,
syntheticCapture,
walkNamedTree,
type SyntaxNode,
} from '../../utils/ast-helpers.js';
import { splitSwiftImport } from './import-decomposer.js';
import { swiftQualifiedBaseTail } from './base-type.js';
import { computeSwiftArityMetadata } from './arity-metadata.js';
import { synthesizeSwiftReceiverBinding } from './receiver-binding.js';
import { synthesizeSwiftSignatureBindings } from './signature-bindings.js';
@ -282,9 +284,70 @@ export function emitSwiftScopeCaptures(
out.push(grouped);
}
// ── Emit inheritance references for scope-resolution EXTENDS / IMPLEMENTS ──
// Walk every class/struct/enum/actor/extension and protocol declaration's
// inheritance specifiers and synthesize `@reference.inherits` captures so
// the registry-primary path emits EXTENDS / IMPLEMENTS (mirrors C++ /
// C# / Java). Without this, Swift inheritance edges came only from the
// legacy `@heritage.*` path, which the worker pipeline drops for
// registry-primary languages (issue #1951).
out.push(...synthesizeSwiftInheritanceReferences(tree.rootNode));
return out;
}
/**
* Synthesize `@reference.inherits` captures from Swift inheritance
* specifiers so the registry-primary scope-resolution path emits
* EXTENDS / IMPLEMENTS edges (mirrors `synthesizeCsharpInheritanceReferences`
* / `emitCppInheritanceCaptures`). Without this, Swift inheritance edges came
* only from the legacy `@heritage.*` path, dropped for registry-primary
* languages in the worker pipeline (issue #1951).
*
* Scope matches the legacy SWIFT_QUERIES `@heritage` blocks exactly: a
* `class_declaration` (class / struct / enum / actor / extension all share
* this node) or a `protocol_declaration`, each with an
* `(inheritance_specifier inherits_from: (user_type (type_identifier)))`.
* The EXTENDS-vs-IMPLEMENTS split is decided downstream from the resolved
* target's symbol kind (`preEmitInheritanceEdges` Interface = IMPLEMENTS,
* else EXTENDS), so every base is emitted with the same `inherits` kind here.
* The base lookup name is normalized to its bare simple identifier
* (`SomeProtocol<T>` `SomeProtocol`, `Outer.Inner` `Inner`) to match the
* V1 simple-name `findClassBindingInScope` contract.
*/
function synthesizeSwiftInheritanceReferences(root: SyntaxNode): CaptureMatch[] {
const out: CaptureMatch[] = [];
walkNamedTree(root, (node) => {
if (node.type !== 'class_declaration' && node.type !== 'protocol_declaration') return;
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child === null || child.type !== 'inheritance_specifier') continue;
const inheritsFrom = child.childForFieldName('inherits_from') ?? child.firstNamedChild;
if (inheritsFrom === null) continue;
const nameNode = swiftBaseTypeIdentifier(inheritsFrom);
if (nameNode === null) continue;
out.push({
'@reference.inherits': nodeToCapture('@reference.inherits', child),
'@reference.name': nodeToCapture('@reference.name', nameNode),
});
}
});
return out;
}
/** Normalize an `inherits_from` node to its bare simple identifier node.
* Only a `user_type`-shaped base contributes an edge; its trailing
* `type_identifier` (the actual base see `swiftQualifiedBaseTail`) is
* returned. Returns null for any other base shape (e.g. a tuple /
* function-type conformance), so no edge is synthesized matching the legacy
* query's `user_type` gate. */
function swiftBaseTypeIdentifier(inheritsFrom: SyntaxNode): SyntaxNode | null {
if (inheritsFrom.type === 'type_identifier') return inheritsFrom;
if (inheritsFrom.type !== 'user_type') return null;
return swiftQualifiedBaseTail(inheritsFrom);
}
/** Pre-order walk over named children (mirrors C#'s `visit`). */
/** Synthesize a `@type-binding.constructor` for EACH clause of an
* if-let / guard-let optional binding:
* `if let u = getUser()` one binding `u: getUser`

View file

@ -26,6 +26,7 @@
import type { Capture, CaptureMatch } from 'gitnexus-shared';
import { nodeToCapture, syntheticCapture, type SyntaxNode } from '../../utils/ast-helpers.js';
import { swiftQualifiedBaseTail } from './base-type.js';
import { swiftMethodConfig } from '../../method-extractors/configs/swift.js';
const TYPE_DECL_NODE_TYPES = new Set(['class_declaration', 'protocol_declaration']);
@ -83,7 +84,10 @@ function isClassKeyword(typeNode: SyntaxNode): boolean {
/** First inherited type (superclass or first protocol) as raw text, or
* null. For a class the first `inheritance_specifier` is conventionally
* the superclass `super.x()` only compiles when that is true. */
* the superclass `super.x()` only compiles when that is true. For a
* `user_type` base the name is its trailing `type_identifier` segment (see
* `swiftQualifiedBaseTail`), falling back to the raw node text when there is
* no `type_identifier` child. */
function firstInheritedType(typeNode: SyntaxNode): string | null {
for (let i = 0; i < typeNode.namedChildCount; i++) {
const child = typeNode.namedChild(i);
@ -91,7 +95,7 @@ function firstInheritedType(typeNode: SyntaxNode): string | null {
const inheritsFrom = child.childForFieldName('inherits_from') ?? child.firstNamedChild;
if (inheritsFrom === null) return null;
if (inheritsFrom.type === 'user_type') {
return inheritsFrom.firstNamedChild?.text ?? inheritsFrom.text;
return swiftQualifiedBaseTail(inheritsFrom)?.text ?? inheritsFrom.text;
}
return inheritsFrom.text;
}

View file

@ -336,21 +336,29 @@ export function emitTsScopeCaptures(
// calls use `new_expression`; regular calls use `call_expression`.
//
// JSX call anchors (`jsx_self_closing_element` / `jsx_opening_element`
// captured by the TSX-only suffix in `query.ts`) intentionally do
// NOT carry arity metadata. The lookup below would resolve `callNode`
// to `null` for a JSX anchor (the anchor is neither a call_expression
// nor a new_expression), so the synthesis branch silently no-ops and
// the JSX call enters the registry with name-only resolution. This
// is acceptable for React: components are virtually never
// overloaded in the current GitNexus graph model, so name-only
// dispatch matches the single component definition. If a future
// codebase introduces overloaded React components AND needs JSX
// calls to disambiguate by props-arity, a JSX-aware arity
// synthesizer would need to count `jsx_attribute` children of the
// opening tag instead of `arguments`.
// captured by the TSX-only suffix in `query.ts`) intentionally do NOT carry
// arity metadata. A JSX component used as a call argument (e.g.
// `render(<Foo .../>)`) is itself a @reference.call.* anchor; without a guard
// the ascent below would climb from it into the enclosing call_expression and
// mis-attribute that call's arity to the component. The early guard skips
// arity synthesis for JSX anchors — restoring the pre-#1951 range-based
// behavior (the old findNodeAtRange found no call_expression at the JSX
// element's range). The guard lives here, not inside findSelfOrAncestorOfTypes
// (shared with the import-statement and function-scope ascents). This is
// acceptable for React: components are virtually never overloaded in the
// current GitNexus graph model, so name-only dispatch matches the single
// component definition. A future props-arity-aware synthesizer would count
// `jsx_attribute` children of the opening tag instead of `arguments`.
const callAnchor = pickFirstCapture(grouped, CALL_TAGS);
const callAnchorNode = pickFirstNode(groupedNodes, CALL_TAGS);
if (callAnchor !== undefined && grouped['@reference.arity'] === undefined) {
const anchorIsJsxElement =
callAnchorNode?.type === 'jsx_self_closing_element' ||
callAnchorNode?.type === 'jsx_opening_element';
if (
callAnchor !== undefined &&
grouped['@reference.arity'] === undefined &&
!anchorIsJsxElement
) {
const callNode =
findSelfOrAncestorOfTypes(callAnchorNode, ['call_expression', 'new_expression']) ??
findNodeAtRange(tree.rootNode, callAnchor.range, 'call_expression') ??
@ -411,10 +419,124 @@ export function emitTsScopeCaptures(
synthesizeDestructuringBindings(tree.rootNode, out);
synthesizeForOfMapTupleBindings(tree.rootNode, out);
synthesizeInstanceofNarrowings(tree.rootNode, out);
synthesizeTsInheritanceReferences(tree.rootNode, out);
return out;
}
/**
* Synthesize `@reference.inherits` captures from TypeScript class heritage so
* the registry-primary scope-resolution path emits EXTENDS / IMPLEMENTS edges
* (mirrors C# `synthesizeCsharpInheritanceReferences` / JS
* `synthesizeJsInheritanceReferences`). Without this, TS inheritance edges came
* only from the legacy `@heritage.*` path, which the worker pipeline drops for
* registry-primary languages yielding 0 inheritance edges in worker mode
* (issue #1951).
*
* Scope is intentionally limited to a `class_declaration`'s `class_heritage`
* `extends_clause` value + `implements_clause` types, matching the legacy
* TypeScript `@heritage` query's class scope (TYPESCRIPT_QUERIES). Generic
* bases agree across both paths: `extends Base<T>` is captured by the legacy
* `extends_clause value: (identifier)` already (the `type_arguments` are a
* sibling field), and `implements IFoo<T>` is captured by a legacy clause
* widened to read the `generic_type`'s `name:` identifier so the registry
* path keeps parity on SIMPLE (unqualified) generic bases too (#1951).
* Qualified bases (`ns.Base`, `ns.Base<T>`, `ns.IFoo<T>`) are ALSO now at parity
* (#1956 tri-review U2): the synth resolves them by their member_expression /
* nested_type_identifier tail, and the legacy `@heritage` query was widened with
* matching arms (member_expression for extends, nested_type_identifier plain +
* generic-wrapped for implements).
*
* `interface_declaration` / `abstract_class_declaration` heritage is NOT emitted
* the legacy query captures neither, so the registry path keeps parity with
* the legacy DAG under the CI scope-parity gate (REGISTRY_PRIMARY_TYPESCRIPT=0
* vs =1). The EXTENDS-vs-IMPLEMENTS split is decided downstream from the
* resolved target's symbol kind in `preEmitInheritanceEdges` (class-extends
* EXTENDS, implements-interface / interface-target IMPLEMENTS), so all bases
* are emitted with the same `inherits` kind here. The base lookup name is
* normalized to its bare simple identifier (`BaseModel<string>` `BaseModel`,
* `models.Base` `Base`) so `findClassBindingInScope` resolves it.
*/
function synthesizeTsInheritanceReferences(root: SyntaxNode, out: CaptureMatch[]): void {
const stack: SyntaxNode[] = [root];
for (;;) {
const node = stack.pop();
if (node === undefined) break;
for (const child of node.namedChildren) {
if (child !== null) stack.push(child);
}
if (node.type !== 'class_declaration') continue;
// Find the `class_heritage` child (holds extends / implements clauses).
let heritage: SyntaxNode | null = null;
for (const child of node.namedChildren) {
if (child !== null && child.type === 'class_heritage') {
heritage = child;
break;
}
}
if (heritage === null) continue;
for (const clause of heritage.namedChildren) {
if (clause === null) continue;
if (clause.type === 'extends_clause') {
// `extends Foo` / `extends Foo<T>` — the base is the `value:` field
// (an identifier; generics live in a sibling `type_arguments`).
const value = clause.childForFieldName('value') ?? clause.firstNamedChild;
emitTsInheritanceBase(value, out);
} else if (clause.type === 'implements_clause') {
// `implements IFoo, IBar<T>` — each base type is a direct named child.
for (const base of clause.namedChildren) {
emitTsInheritanceBase(base, out);
}
}
}
}
}
/** Emit one `@reference.inherits` match for a TS heritage base, normalizing
* the lookup name to its bare simple identifier. No-ops on null / non-type
* nodes or when the bare name can't be derived. */
function emitTsInheritanceBase(base: SyntaxNode | null, out: CaptureMatch[]): void {
if (base === null) return;
const nameNode = terminalTsTypeNameNode(base);
if (nameNode === null) return;
out.push({
'@reference.inherits': nodeToCapture('@reference.inherits', base),
'@reference.name': nodeToCapture('@reference.name', nameNode),
});
}
/** Resolve a TypeScript heritage base node to its bare simple-identifier node.
* `Foo` `Foo`, `Foo<T>` (generic_type) `Foo`, `models.Base`
* (nested_type_identifier / member_expression) `Base`. Mirrors C#'s
* `terminalTypeNameNode`; returns null when no leaf identifier is reachable. */
function terminalTsTypeNameNode(node: SyntaxNode): SyntaxNode | null {
switch (node.type) {
case 'identifier':
case 'type_identifier':
// `extends ns.Base` parses as a member_expression whose tail is a
// `property_identifier` (not a type_identifier) — treat it as a leaf name.
case 'property_identifier':
return node;
case 'generic_type': {
// generic_type has a `name:` field (type_identifier / nested_type_identifier);
// recurse to strip the type_arguments and reach the bare base identifier.
const name = node.childForFieldName('name') ?? node.firstNamedChild;
return name === null ? null : terminalTsTypeNameNode(name);
}
case 'nested_type_identifier':
case 'member_expression': {
// Qualified `A.B.Base` → tail identifier `Base`.
const tail = node.lastNamedChild;
return tail === null ? null : terminalTsTypeNameNode(tail);
}
default:
return null;
}
}
/**
* Walk the AST and synthesize type-binding captures for object
* destructuring of the form `const { field } = rhs` or

View file

@ -459,9 +459,22 @@ export interface ScopeResolver {
* `include`/`extend`/`prepend`) use this hook to emit IMPLEMENTS edges
* from parsed import or reference data.
*
* Receives the graph (writable), parsedFiles, and nodeLookup same
* surface as `buildMro`. Must be idempotent (the orchestrator may call
* it more than once during re-resolution).
* Receives the graph (writable), parsedFiles, nodeLookup, and the finalized
* `ScopeResolutionIndexes` the same scope/import/def model
* `preEmitInheritanceEdges` resolves against, and already a first-class part
* of this contract (the structure/binding hooks below take it too), so the
* trailing `scopes` parameter is not a new type dependency here. It is
* appended and optional so implementations that don't need scope-aware
* resolution keep their narrower signature.
*
* `scopes` has exactly ONE consumer: the Rust resolver see
* `emitRustTraitImplEdges` in languages/rust/scope-resolver.ts which
* resolves `impl T for S` trait/struct names through the scope chain +
* import-aware disambiguation (refusing ambiguous matches) instead of a
* global last-write-wins simple-name index (#1951). Other implementations
* (e.g. Ruby `include`/`extend`/`prepend`) ignore it and keep the 3-arg
* shape. Must be idempotent (the orchestrator may call it more than once
* during re-resolution).
*
* Default: undefined (no extra heritage edges needed).
*/
@ -469,6 +482,7 @@ export interface ScopeResolver {
graph: KnowledgeGraph,
parsedFiles: readonly ParsedFile[],
nodeLookup: GraphNodeLookup,
scopes?: ScopeResolutionIndexes,
) => void;
/**

View file

@ -70,6 +70,9 @@ export function tryEmitEdge(
confidence = 0.85,
collapseByCallerTarget = false,
): boolean {
// Inheritance edges are emitted directly by `preEmitInheritanceEdges` (which
// owns the enclosing-class caller and the EXTENDS-vs-IMPLEMENTS type), so this
// generic bridge derives caller + edge type purely from the site.
const callerGraphId = resolveCallerGraphId(site.inScope, scopes, nodeLookup);
const targetGraphId = resolveDefGraphId(targetDef.filePath, targetDef, nodeLookup);
const edgeType = mapReferenceKindToEdgeType(site.kind as Reference['kind']);

View file

@ -35,19 +35,59 @@ import { resolveReferenceSites, type ResolveStats } from '../../resolve-referenc
import { buildGraphNodeLookup } from '../graph-bridge/node-lookup.js';
import { resolveDefGraphId } from '../graph-bridge/ids.js';
import { buildPopulatedMethodDispatch } from '../graph-bridge/method-dispatch.js';
import { tryEmitEdge } from '../graph-bridge/edges.js';
import { propagateImportedReturnTypes } from '../passes/imported-return-types.js';
import { emitReceiverBoundCalls } from '../passes/receiver-bound-calls.js';
import { emitFreeCallFallback } from '../passes/free-call-fallback.js';
import { emitReferencesViaLookup } from '../graph-bridge/references-to-edges.js';
import { emitImportEdges } from '../graph-bridge/imports-to-edges.js';
import type { ScopeResolver } from '../contract/scope-resolver.js';
import { findClassBindingInScope, findEnclosingClassDef } from '../scope/walkers.js';
import {
findClassBindingInScope,
findEnclosingClassDef,
resolveAmbiguousInheritanceBaseViaImports,
} from '../scope/walkers.js';
import { buildWorkspaceResolutionIndex } from '../workspace-index.js';
import type { ResolutionOutcome, ResolutionOutcomeRecorder } from '../resolution-outcome.js';
import { logger } from '../../../logger.js';
/**
* Emit one class-owned inheritance edge directly (the inheritance pre-pass is
* the authoritative emitter see `preEmitInheritanceEdges`). Encapsulates the
* dual dedup contract so the two sets' joint semantics live in one place:
* - `existing` coarse per-`(caller, target, type)` gate, seeded from the
* graph (so this pass is a no-op when the legacy path already emitted it).
* - `seen` per-site key shared with the generic edge bridge so the two
* passes never double-emit the same resolution.
* The `dedupKey` and `rel:` id shape match `tryEmitEdge` exactly, so graph
* output stays byte-identical. The caller is the enclosing class (NOT the
* method/constructor `resolveCallerGraphId` would prefer that broke MRO for
* C# 12 primary constructors, #1951); the edge type is pre-discriminated.
*/
function emitInheritanceEdgeDirect(
graph: KnowledgeGraph,
seen: Set<string>,
existing: Set<string>,
callerGraphId: string,
targetGraphId: string,
edgeType: 'EXTENDS' | 'IMPLEMENTS',
site: { readonly atRange: { startLine: number; startCol: number } },
): void {
const edgeKey = `${edgeType}:${callerGraphId}->${targetGraphId}`;
const dedupKey = `${edgeKey}:${site.atRange.startLine}:${site.atRange.startCol}`;
if (existing.has(edgeKey) || seen.has(dedupKey)) return;
seen.add(dedupKey);
existing.add(edgeKey);
graph.addRelationship({
id: `rel:${dedupKey}`,
sourceId: callerGraphId,
targetId: targetGraphId,
type: edgeType,
confidence: 0.85,
reason: 'scope-resolution: inherits',
});
}
/**
* Resolve inheritance reference sites early and pre-emit their EXTENDS edges
* before MRO construction. This lets template-base captures contribute to the
@ -63,9 +103,16 @@ function preEmitInheritanceEdges(
): Set<string> {
const handledSites = new Set<string>();
const seen = new Set<string>();
// Seed the dedup set with both inheritance edge types already in the graph
// (e.g. emitted by the legacy heritage path in sequential mode). Keying by
// edge type lets us add IMPLEMENTS without colliding with EXTENDS and keeps
// this pass a no-op when the legacy path already produced the same edge.
const existing = new Set<string>();
for (const rel of graph.iterRelationshipsByType('EXTENDS')) {
existing.add(`${rel.sourceId}->${rel.targetId}`);
existing.add(`EXTENDS:${rel.sourceId}->${rel.targetId}`);
}
for (const rel of graph.iterRelationshipsByType('IMPLEMENTS')) {
existing.add(`IMPLEMENTS:${rel.sourceId}->${rel.targetId}`);
}
for (const site of scopes.referenceSites) {
@ -81,12 +128,20 @@ function preEmitInheritanceEdges(
// edge. The shared bridge resolves the source via
// `resolveCallerGraphId`, which can degrade class-heritage sites into
// method-owned EXTENDS edges once methods exist on the class. This
// pre-pass is the authoritative inheritance emitter, so broad
// pre-pass is the authoritative inheritance emitter and pins the source
// to the enclosing class (via the `callerGraphId` override below), so
// suppression keeps `buildMro` and the final graph class-owned.
handledSites.add(siteKey);
}
const targetDef = findClassBindingInScope(site.inScope, site.name, scopes);
const targetDef =
findClassBindingInScope(site.inScope, site.name, scopes) ??
// Import-aware disambiguation fallback (#1951). Only engages when the
// scope-chain + single-match lookups above returned undefined because
// the simple name is ambiguous (multiple same-named class-like defs).
// Picks the candidate whose defining file is imported/included by the
// referencing file. Never changes behavior for single-match cases.
resolveAmbiguousInheritanceBaseViaImports(site.inScope, site.name, scopes);
if (targetDef === undefined) continue;
const callerClass = findEnclosingClassDef(site.inScope, scopes);
@ -94,23 +149,18 @@ function preEmitInheritanceEdges(
const callerGraphId = resolveDefGraphId(callerClass.filePath, callerClass, nodeLookup);
const targetGraphId = resolveDefGraphId(targetDef.filePath, targetDef, nodeLookup);
if (callerGraphId === undefined || targetGraphId === undefined) continue;
const edgeKey = `${callerGraphId}->${targetGraphId}`;
if (existing.has(edgeKey)) continue;
if (
tryEmitEdge(
graph,
scopes,
nodeLookup,
site,
targetDef,
'scope-resolution: inherits',
seen,
0.85,
)
) {
existing.add(edgeKey);
}
// Discriminate EXTENDS vs IMPLEMENTS by the resolved target's symbol kind:
// conforming to an interface OR mixing in a trait/protocol is IMPLEMENTS,
// deriving from a class-like is EXTENDS. This matches the legacy heritage
// emitters (`resolveExtendsType` maps Interface→IMPLEMENTS; the trait-impl
// branch of `resolveAndAddHeritageEdge` maps trait use → IMPLEMENTS), so the
// registry-primary path matches the legacy DAG. The discriminator is purely
// symbol-kind-driven (no language is named here, per AGENTS.md): a base that
// resolves to neither an Interface nor a Trait symbol always takes the
// EXTENDS branch, so such languages are unchanged.
const edgeType: 'EXTENDS' | 'IMPLEMENTS' =
targetDef.type === 'Interface' || targetDef.type === 'Trait' ? 'IMPLEMENTS' : 'EXTENDS';
emitInheritanceEdgeDirect(graph, seen, existing, callerGraphId, targetGraphId, edgeType, site);
}
return handledSites;
@ -313,7 +363,7 @@ export function runScopeResolution(
// the heritage declarations are syntactic method calls, not grammar-level
// heritage clauses. Must run BEFORE `buildMro` so MRO construction sees
// the freshly-emitted IMPLEMENTS edges.
provider.emitHeritageEdges?.(graph, parsedFiles, nodeLookup);
provider.emitHeritageEdges?.(graph, parsedFiles, nodeLookup, finalized);
// Implicit IMPORTS-edge hook — for languages whose files have compiler-
// implicit cross-file visibility (no syntactic import statement). The
// finalized-ImportEdge pipeline (`emitImportEdges`) cannot produce these

View file

@ -300,6 +300,94 @@ export function findClassBindingInScope(
return undefined;
}
/**
* Import/include-aware disambiguation for an *ambiguous* class-like base
* name. Engages ONLY as a fallback after `findClassBindingInScope` has
* already returned `undefined` i.e. the scope-chain walk and the
* single-match `qualifiedNames` fast paths could not pick a winner because
* several same-named class-like defs exist (e.g. two `class Handler`s in
* different headers/namespaces).
*
* Disambiguates by the referencing file's import graph: the enclosing
* module scope's finalized `ImportEdge[]` (C++ `#include`, C# `using`, etc.)
* each carry the exporting file in `targetFile`. A candidate whose defining
* file is brought in by one of those edges is preferred. Resolution is
* tiered, strictest first, and only commits when EXACTLY ONE candidate
* survives a tier so a still-ambiguous name keeps the historical
* "return undefined" refusal:
*
* 1. Exact file match candidate.filePath === an import's `targetFile`
* (covers C++ `#include "handler_a.h"` that header's class).
* 2. Same-directory match candidate.filePath sits in the same directory
* as some import target file (covers C# `using MyApp.Models;`, where the
* namespace import resolves to ONE representative file in the namespace's
* directory, not necessarily the file declaring the referenced type).
*
* Language-neutral: keyed only on the finalized import edges and the
* candidate defs' `filePath`. Returns `undefined` (preserving refusal) when
* the name is single-match-resolvable already (never reached caller gates
* on `findClassBindingInScope` miss), when no import disambiguates, or when
* a tier leaves more than one survivor.
*/
export function resolveAmbiguousInheritanceBaseViaImports(
startScope: ScopeId,
baseName: string,
scopes: ScopeResolutionIndexes,
): SymbolDefinition | undefined {
// Gather the class-like candidates that share this simple name. Defs are
// indexed by their `qualifiedName` in `qualifiedNames`; for languages whose
// class qualifiedName IS the simple name (C++, C#, etc.) this is the full
// candidate set. A single candidate is not "ambiguous" — leave it to the
// existing single-match fast path (this fallback shouldn't have been called).
const candidateIds = scopes.qualifiedNames.get(baseName);
if (candidateIds.length < 2) return undefined;
const candidates: SymbolDefinition[] = [];
for (const id of candidateIds) {
const def = scopes.defs.get(id);
if (def !== undefined && isClassLike(def.type)) candidates.push(def);
}
if (candidates.length < 2) return undefined;
// Collect the exporting files imported by the referencing file's enclosing
// module scope (the chain may carry function-local imports too, but the
// module scope is where `#include` / `using` land).
const moduleScopeId = moduleScopeIdOf(startScope, scopes);
if (moduleScopeId === null) return undefined;
const importEdges = scopes.imports.get(moduleScopeId);
if (importEdges === undefined || importEdges.length === 0) return undefined;
const importedFiles = new Set<string>();
const importedDirs = new Set<string>();
for (const edge of importEdges) {
if (edge.targetFile === null) continue;
importedFiles.add(edge.targetFile);
importedDirs.add(dirnameOf(edge.targetFile));
}
if (importedFiles.size === 0) return undefined;
// Tier 1 — exact file match (C++ `#include "handler_a.h"`).
const exact = candidates.filter((c) => importedFiles.has(c.filePath));
if (exact.length === 1) return exact[0];
if (exact.length > 1) return undefined; // still ambiguous → refuse
// Tier 2 — same-directory match (C# namespace `using`, where the namespace
// import resolves to one representative file in the namespace's directory).
const sameDir = candidates.filter((c) => importedDirs.has(dirnameOf(c.filePath)));
if (sameDir.length === 1) return sameDir[0];
return undefined;
}
/**
* Directory portion of a forward-slash workspace-relative path. Returns `''`
* for a bare filename (no directory). Workspace paths are always normalized to
* `/` separators upstream, so a simple `lastIndexOf('/')` is sufficient and
* keeps this dependency-free.
*/
function dirnameOf(filePath: string): string {
const idx = filePath.lastIndexOf('/');
return idx === -1 ? '' : filePath.slice(0, idx);
}
/**
* Predicate for value-receiver bridge: the labels for which
* `reconcileOwnership` registers methods/fields under the def's

View file

@ -216,6 +216,28 @@ export const CONTAINER_TYPE_TO_LABEL: Record<string, string> = {
companion_object: 'Class',
};
/**
* Pre-order walk over a node and all its named descendants, invoking `cb` on
* each. Replaces the per-language `visit`/`visitGo`/`visitRust`/`visitSwift`
* clones that every language's capture-synthesis walker re-implemented (#1956
* tri-review U6).
*
* Iterates by index with a null guard: `node.namedChild(i)` is typed
* `SyntaxNode | null`, and most callers already guarded it. The Go and C#
* callers previously iterated `node.namedChildren`; the Go one had no null
* guard, so this standardizes them onto the guarded indexed form a deliberate,
* strictly-safer behavior addition (the traversal *sequence* is identical, so
* capture output stays byte-identical on well-formed trees; the guard only
* matters for a null named child, which the fixture corpus never produces).
*/
export function walkNamedTree(node: SyntaxNode, cb: (node: SyntaxNode) => void): void {
cb(node);
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child !== null) walkNamedTree(child, cb);
}
}
/** Return the first matching ancestor unless a boundary ancestor is reached first. */
export function findAncestorBeforeBoundary(
node: SyntaxNode,

View file

@ -28,8 +28,8 @@
"digest": "ceae1be5aae89de4eec68c5a64195728a7ae434a9b2cab2eaab617b912c3433d"
},
"csharp-ambiguous/Services/UserHandler.cs": {
"captureGroups": 9,
"digest": "f4e4371e9d23c48e9c1a89cc9475bddadffdd3d0bab129690b4b142a4dea94cd"
"captureGroups": 11,
"digest": "77fbdaffa5e1f6ab94b14d339af380c83466183162a57ea1b34f5d9264c2ed4a"
},
"csharp-assignment-chain/Models/Repo.cs": {
"captureGroups": 7,
@ -100,8 +100,8 @@
"digest": "817c2f4caf130d8af6ca4bbbc44d25c3186b59e1bddf6cb06b93cd99b98abd0c"
},
"csharp-child-extends-parent/src/Child.cs": {
"captureGroups": 4,
"digest": "18a42204289b0c126ff5eb78497c631c51361b030626639b60e8005d0e4e69c2"
"captureGroups": 5,
"digest": "06860696436195a2e4bdac8954307bd97d28e8fb9ef27e1adbb31aa0f4939012"
},
"csharp-child-extends-parent/src/Parent.cs": {
"captureGroups": 7,
@ -176,24 +176,24 @@
"digest": "03009e02efb0bb1fb33dae6cd15b99c46f2e11a30da79fb8696ec3066c203db0"
},
"csharp-generic-parent-resolution/src/Models/User.cs": {
"captureGroups": 9,
"digest": "62f80db9064341b9ff5f5761113672ec4ceb369af962a94e3b8b4b34c4e26e9c"
"captureGroups": 10,
"digest": "fddcbe19d2a797949e35f8a9f04546c9f483f92c6055685651b96e4dd6e87577"
},
"csharp-generic-type-refs/Program.cs": {
"captureGroups": 21,
"digest": "833526f5f33d9b648fa22f696a3c2fff9ebfd9e733f567fa13c9607d40faba06"
"captureGroups": 22,
"digest": "d4181ce42bba3ee454f4a3b9870ea88944a0df9cd5c9147d42d930dac63b8b33"
},
"csharp-grandparent-resolution/Models/A.cs": {
"captureGroups": 9,
"digest": "2687b8e8f0b869d1ea8bdb9cf04dba3845f25ec1699b518114c57c4851b3528d"
},
"csharp-grandparent-resolution/Models/B.cs": {
"captureGroups": 4,
"digest": "0031e28abc076739af04291521c7c5272201c68c60ce5cef68f259a2c10429d4"
"captureGroups": 5,
"digest": "7b5140fffae831750f64f832cb19449d88cf421089ddbeb8790d58386a821b8b"
},
"csharp-grandparent-resolution/Models/C.cs": {
"captureGroups": 4,
"digest": "8f100ee67304e05401c5021167d36fa7a1fbce9188fd8f1d2fdf9ac0e7a6eb7d"
"captureGroups": 5,
"digest": "e18b37eacce974faa20912b3ecf0588e5c3130b14624e672973bfff718aa0db9"
},
"csharp-grandparent-resolution/Models/Greeting.cs": {
"captureGroups": 7,
@ -212,8 +212,8 @@
"digest": "3f64ed5bfd0eb0bc62d73b908eebac7025f9e32639aeebc8db9160f3523b521a"
},
"csharp-interface-default-method/User.cs": {
"captureGroups": 10,
"digest": "747fe9fed63700c0ce233b76d2544ed4cae16d68beb4ea29095d24cf68ff8bbe"
"captureGroups": 11,
"digest": "40a8824497cc733891ef51ea960b3e44bb4ed23db9934884f78bb36d4dde78be"
},
"csharp-interface-default-method/Validator.cs": {
"captureGroups": 7,
@ -228,12 +228,12 @@
"digest": "14743965ad586b13254a9618686d601b0dd36f3be97531c9fb0605139e83809d"
},
"csharp-interface-dispatch/SqlRepository.cs": {
"captureGroups": 11,
"digest": "57cf94afb053e43647c295d3bd5ba9864a56cc75895d5e37194d2df18f1e744a"
"captureGroups": 12,
"digest": "65bd6fd311969af60313ea33345188894fa236ca26020b3e681fc8c3144898fb"
},
"csharp-interface-heritage/src/IAuditableService.cs": {
"captureGroups": 5,
"digest": "eca6d49bcd2d8316ced662b168c4cd379f6f0ce08e591d6fd1ff513e28372a56"
"captureGroups": 7,
"digest": "bc413b880556ec2798ec501b020d993e7a53d1d9be1e0e0bee4d885990330fad"
},
"csharp-interface-heritage/src/IBarService.cs": {
"captureGroups": 6,
@ -244,12 +244,12 @@
"digest": "cfbc24950b74dd10ddb59c25f8aa4d4b83c5e72502c4227f3f6e311e9e06401f"
},
"csharp-interface-heritage/src/IFooService.cs": {
"captureGroups": 6,
"digest": "19bef355b4992f96f06331ed11d8652b86d25ac1fb5e9cbc2d5d5c0ef85b123d"
"captureGroups": 7,
"digest": "0d00dcf860e772b8ec539064ed97941aadb64c99fc91783d3ff657c93d00b67e"
},
"csharp-interface-heritage/src/MyService.cs": {
"captureGroups": 18,
"digest": "c565e684ad7e04915bce9fc3124e51db6b41669a20f29b4bc117317506bd7985"
"captureGroups": 19,
"digest": "b39d4f1b8860dc16be70431b86e5bfaf0a6fb317a1e586d7e08bbdfd6682aee2"
},
"csharp-interface-receiver-static/src/ILogger.cs": {
"captureGroups": 6,
@ -304,8 +304,8 @@
"digest": "09e0bed66a03cddb567b863b8d916b5b2f4210547caf2f8682c266d36102125e"
},
"csharp-method-enrichment/Animal.cs": {
"captureGroups": 16,
"digest": "a2200a76108b6ef05340718bf7485d762f8004db76191992552cba370eb3999c"
"captureGroups": 17,
"digest": "b21133dbc6ecaaf9ba13de8d1b70c11c6a89d02f89696a137c8d79ae9e5e287a"
},
"csharp-method-enrichment/App.cs": {
"captureGroups": 14,
@ -388,8 +388,8 @@
"digest": "5211301516540daf9c54fb3c8ca3b43bddcce4f81a14034056f2bc897ae925a4"
},
"csharp-overload-dispatch/SqlRepository.cs": {
"captureGroups": 16,
"digest": "7052be187bc4039e4e94c29446c4dae0aed9b288d95c067905a15a33f3d1625d"
"captureGroups": 17,
"digest": "8b67299c96e0331a44bfd56edf13bc71f957e39bfa95888108d3180f454050a3"
},
"csharp-overload-interface/App/Caller.cs": {
"captureGroups": 13,
@ -400,12 +400,12 @@
"digest": "1f78db08c2a7a8d3402cf3b0f092ec6aa5b6aa9fedc790d088eaffce8e4bd713"
},
"csharp-overload-interface/Greeting/EnGreeter.cs": {
"captureGroups": 8,
"digest": "f719d5044c243225989e8cbf675d01adeb5cc2023338f320d422ccdab94964ac"
"captureGroups": 9,
"digest": "fa1429dc0e98f268ad3360d9ed55534dd39aa49b933771dd628a7b517f36fef6"
},
"csharp-overload-interface/Greeting/FrGreeter.cs": {
"captureGroups": 8,
"digest": "4e18d58d5a54a87262e42cd84043dc31cbf6d31609eb5a8b3d61c0e692f3ea08"
"captureGroups": 9,
"digest": "3a3ec96334e17fa0c48e41d7766238eca3ae4d55d206b21b0f89c1d67d9c4f93"
},
"csharp-overload-interface/Greeting/IGreeter.cs": {
"captureGroups": 6,
@ -424,17 +424,37 @@
"digest": "93f2d7c639083aa077ba3c9d2dea678d0d6a2aa914e8078307c98f4c68530226"
},
"csharp-parent-resolution/src/Models/User.cs": {
"captureGroups": 8,
"digest": "ad73f273c29be9536b40c663f6d1864f78b2bca667483d73cc3de4c4900c79ed"
"captureGroups": 10,
"digest": "848b4ec5fc727efb001134164da1158db495776bf006300fd58391040f6f906f"
},
"csharp-pattern-matching/Models/Animal.cs": {
"captureGroups": 17,
"digest": "fda2322e993f129e139d11bffe48e2742459792e90b99b8eb0c9a10da15b5139"
"captureGroups": 19,
"digest": "40f80b8588591728ed99f3a3566dcefc14df511f8350cdc6843401e0f943c8ee"
},
"csharp-pattern-matching/Services/AnimalService.cs": {
"captureGroups": 11,
"digest": "5418fbf243683914fea8eeed02f2ef856b02ba6c7e361e001da4662eb1332359"
},
"csharp-primary-ctor-heritage/src/BaseEntity.cs": {
"captureGroups": 5,
"digest": "b1ed570e315646104e03429cee0ea982f8563bce337a31293b3718f63679c2b2"
},
"csharp-primary-ctor-heritage/src/IFoo.cs": {
"captureGroups": 6,
"digest": "b19bf122c62247868382dd45b258757b653e526fd3fda8cabe080f7f83510891"
},
"csharp-primary-ctor-heritage/src/Repo.cs": {
"captureGroups": 6,
"digest": "3b339a7ef549da1554aa28d83a2d0402d79c1a7c483538e0266f283ac6175307"
},
"csharp-primary-ctor-heritage/src/Service.cs": {
"captureGroups": 5,
"digest": "9514716e0d445012f9ff62e9b53d8f48e633d1f74b05c9cf90d85f8393c2015f"
},
"csharp-primary-ctor-heritage/src/User.cs": {
"captureGroups": 11,
"digest": "36d4762bfb9ff4083315384560ae6716d12edd0f2d13550506d55721573fd214"
},
"csharp-primary-ctors/App.cs": {
"captureGroups": 14,
"digest": "4c2e190ed675ac78244e0a4230d79e9e4c74f2ef794dd9b8126f6b57a4242f4c"
@ -456,13 +476,21 @@
"digest": "633570540177423d352b44b0f5230d7f440f101789d1b92192c0fc360e6c26d3"
},
"csharp-proj/Models/User.cs": {
"captureGroups": 18,
"digest": "a9ef60d8f0ad5109369df107c4ce20ac472e5a0d018bc7df2350c2ae66607372"
"captureGroups": 20,
"digest": "1bc7f070357cf54c14c4c1c7967ceb4caeb2a4a1aba0fa32e1e6c6c672a97ea4"
},
"csharp-proj/Services/UserService.cs": {
"captureGroups": 19,
"digest": "42b79c274955abcd548ca842077e05b0b6b46e324dd29f7a297882ac790ce11f"
},
"csharp-qualified-base/src/Domain.cs": {
"captureGroups": 15,
"digest": "76cdfe6e0bb9ace81fda8e4c3e1b5bea536b3d906170ee92c68a8b4d5b918fbc"
},
"csharp-qualified-base/src/Shapes.cs": {
"captureGroups": 38,
"digest": "d0023367c412f37f030860022c859a58a249aff2d17ec92b109f47a7efe77cf3"
},
"csharp-qualified-types/Data/User.cs": {
"captureGroups": 7,
"digest": "16b056de69cc44d953e4cc702f22986b61d935193c4d6bd9cd9a1adcbe2b3e6c"
@ -488,8 +516,8 @@
"digest": "e7da2e190dad718eeaa22dad011740fd2086eb2f86331d887e47cd9c5092003c"
},
"csharp-record-base/src/Models/UserRecord.cs": {
"captureGroups": 9,
"digest": "4b7092ef2ded4e37d2fe1591610259e18b9bd3d92ebe32c90eef6683d9b9a0f6"
"captureGroups": 10,
"digest": "e6276cd40438125f468adb2f193786a6e888032a3980d0d7ac586d355c40534c"
},
"csharp-recursive-pattern/Models/Repo.cs": {
"captureGroups": 8,
@ -520,8 +548,8 @@
"digest": "3b66f87310d7dae98a79a6983996abca827e8b78d37e5a7234e26e9520634c5b"
},
"csharp-same-arity-cross-file/DbLookup.cs": {
"captureGroups": 11,
"digest": "77916f4d60865d277c624d1a7ecde55fdb10e6354e5dc1d516c80677628dee3a"
"captureGroups": 12,
"digest": "870869bc7dec677c4f037cd1ef6da71ff6a180b40b76a313a4e6f8e6a2030ffa"
},
"csharp-same-arity-cross-file/Formatter.cs": {
"captureGroups": 11,
@ -576,8 +604,8 @@
"digest": "03009e02efb0bb1fb33dae6cd15b99c46f2e11a30da79fb8696ec3066c203db0"
},
"csharp-super-resolution/src/Models/User.cs": {
"captureGroups": 9,
"digest": "231c0dabbb2e5d105ee19ae3d043f576315f4ec8764baaa310dda95770175827"
"captureGroups": 10,
"digest": "7884f0582c5b433845bfc9a8e331e8939638ee4e2308e0d8d8767f8332209a4b"
},
"csharp-switch-pattern/Models/Repo.cs": {
"captureGroups": 7,

View file

@ -16,8 +16,8 @@
"digest": "08a61721581c4f17741ef0c4ee1c8945235ee7f2aca7d88d2fe122071cd62e6f"
},
"go-ambiguous/internal/services/user.go": {
"captureGroups": 8,
"digest": "802b81a07c64c01f2381cf33d41cdb1a58f535c0007b5ae151c38cda87f28321"
"captureGroups": 9,
"digest": "642da0df644ebd5a789e2de2d473ced2c9a77e5f6f1b4a1b1d53cf04845b820c"
},
"go-assignment-chain/cmd/main.go": {
"captureGroups": 50,
@ -64,8 +64,8 @@
"digest": "5c31199e148340ee32dd48e32a00003e094be963b1287b9b40c6b4effca12175"
},
"go-child-extends-parent/models/child.go": {
"captureGroups": 3,
"digest": "6fd9fe7b82066f82a93bf5e5024ddf89382091ec04e55648845c8295f13bd412"
"captureGroups": 4,
"digest": "61dafb01f7616ab6d965bcf17197de315f57a606c7b39cb25a0f83b84508d86d"
},
"go-child-extends-parent/models/parent.go": {
"captureGroups": 8,
@ -232,8 +232,8 @@
"digest": "2afaeb50d544a55fe437ef20e2c0de92152d2ba62f2693c329255787bb3d0a02"
},
"go-parent-resolution/models/user.go": {
"captureGroups": 8,
"digest": "c76ba16343dd94024fdaac10fa7640536966e530d675c0084434f42edb5b5f15"
"captureGroups": 9,
"digest": "bc39ef8b54dcfcf975155ff69721bf2075ca170fb81bb081a1052dda72f64ecd"
},
"go-pkg/cmd/main.go": {
"captureGroups": 14,
@ -244,8 +244,8 @@
"digest": "2204643b50f486423ee7a5877b2bab7d6334cbe62b14b4435fe4ba8a6465ce92"
},
"go-pkg/internal/models/admin.go": {
"captureGroups": 13,
"digest": "1a5ec9fd5e752adcfec91cd03b3c2a67c124852228a527002237ec51cd4b39b7"
"captureGroups": 14,
"digest": "85b2e848f29885082671f00d015bcce3b14ebd372a88acbc225e6ade1ee5449c"
},
"go-pkg/internal/models/repository.go": {
"captureGroups": 3,
@ -267,6 +267,18 @@
"captureGroups": 10,
"digest": "5c31199e148340ee32dd48e32a00003e094be963b1287b9b40c6b4effca12175"
},
"go-qualified-base/base/base.go": {
"captureGroups": 16,
"digest": "4c50e40140094556e8bfc540f0ea6627c55823a1e4fcc91ca5d74912df67d499"
},
"go-qualified-base/consumers/local.go": {
"captureGroups": 19,
"digest": "9097247f77ab93b742c715ff9d656038fc8ea0558e3fe7e4df64e6b1c6fcb64c"
},
"go-qualified-base/consumers/qualified.go": {
"captureGroups": 14,
"digest": "01fe99cadcdf3ce6f00cafce8db0474ad152983f766c8dee9b9da42c9256ae2f"
},
"go-receiver-method-free-call/example.go": {
"captureGroups": 8,
"digest": "2a3c26672d3b997bdc39644361c550f8cf0749489f0945d210e2fb7f3bca9383"

View file

@ -0,0 +1,7 @@
namespace App
{
public class BaseEntity
{
public int Id { get; set; }
}
}

View file

@ -0,0 +1,7 @@
namespace App
{
public interface IFoo
{
void Foo();
}
}

View file

@ -0,0 +1,7 @@
namespace App
{
public class Repo<T>
{
public T Value { get; set; }
}
}

View file

@ -0,0 +1,7 @@
namespace App
{
// Fully-qualified GENERIC base — exercises qualified+generic name normalization.
public class Service : App.Repo<int>
{
}
}

View file

@ -0,0 +1,8 @@
namespace App
{
// C# 12 primary constructor + base list (the #1951 worker-mode regression).
public class User(int id) : BaseEntity, IFoo
{
public void Foo() { }
}
}

View file

@ -0,0 +1,20 @@
namespace App.Domain
{
// Sibling-namespace base types. `Base` resolves to EXTENDS (Class kind);
// `IFoo` / `IBar` resolve to IMPLEMENTS (Interface kind). Single definition
// per name keeps the registry-primary base lookup unambiguous.
public class Base
{
public virtual void Run() { }
}
public interface IFoo
{
void Foo();
}
public interface IBar
{
void Bar();
}
}

View file

@ -0,0 +1,47 @@
using App.Domain;
using DomainAlias = App.Domain;
namespace App
{
// Each declaration below exercises a base-list shape the registry-primary
// inheritance synth DROPPED before #1951. The legacy @heritage leg already
// covered them (tree-sitter-queries.ts record/struct base_list arms), so
// both resolver legs must now agree.
// record_declaration base_list, plain identifier bases (record traversal
// was skipped — synth only walked class/interface declarations).
public record R(int x) : Base, IFoo
{
public void Foo() { }
}
// record_declaration with a primary_constructor_base_type (`Base(id)`): the
// base-name extractor had no case for primary_constructor_base_type and
// returned null, dropping the EXTENDS edge. Its `type` field is the
// supertype; the trailing argument_list is normalized away → `Base`.
public record P(int id) : Base(id), IBar
{
public void Bar() { }
}
// struct_declaration base_list with a qualified_name base (`App.Domain.IBar`
// → `IBar`). Struct traversal was skipped before #1951.
public struct S : IFoo, App.Domain.IBar
{
public void Foo() { }
public void Bar() { }
}
// qualified_name base on a class — already handled; pinned as a regression
// guard so the simple/qualified path stays byte-identical.
public class A : App.Domain.Base
{
}
// alias_qualified_name base (`DomainAlias::Base` → `Base`): the extractor
// had no case for alias_qualified_name and returned null. Its `name` field
// is the bare identifier; `normalizeSupertypeName` reduces it the same way.
public class B : DomainAlias::Base
{
}
}

View file

@ -0,0 +1,26 @@
package base
// Base types embedded cross-package by the consumers package. Struct bases
// produce EXTENDS; the interface base produces IMPLEMENTS (the split is decided
// downstream from the resolved target's symbol kind).
type Base struct {
ID int
}
func (b *Base) Describe() string {
return "base"
}
// Box is a generic struct embedded as `base.Box[int]` (generic_type wrapping a
// qualified_type) — the previously DROPPED qualified-generic embed shape.
type Box[T any] struct {
value T
}
// Reader is embedded into a consumer interface as `base.Reader` (qualified
// interface embed) — previously DROPPED because the synth never walked
// interface_type bodies.
type Reader interface {
Read() (int, error)
}

View file

@ -0,0 +1,30 @@
package consumers
// Same-package bases for the bare-name embed forms. These exercise the
// byte-identical simple-base path (bare type_identifier) alongside the newly
// handled bare interface embed.
type Local struct {
Tag string
}
func (l *Local) Tag2() string {
return l.Tag
}
type LocalIface interface {
Local2() string
}
// Bare struct embed `Local` (type_identifier) — the long-supported simple-base
// path, unchanged by this fix. → EXTENDS T → Local.
type T struct {
Local
}
// Bare interface embed `LocalIface` inside an interface body
// (interface_type → type_elem → type_identifier) — previously DROPPED because
// the synth never walked interface bodies. → IMPLEMENTS RLocal → LocalIface.
type RLocal interface {
LocalIface
}

View file

@ -0,0 +1,30 @@
package consumers
import "example.com/app/base"
// Qualified struct embed `pkg.Base` (qualified_type) — previously DROPPED by the
// registry-primary synth (it rejected anything but a bare type_identifier).
// Resolves to the struct base.Base → EXTENDS S → Base.
type S struct {
base.Base
}
// Pointer-qualified struct embed `*pkg.Base`. The `*` is an unnamed token, so
// field_declaration.type is already the qualified_type — same shape as S.
// → EXTENDS P → Base.
type P struct {
*base.Base
}
// Qualified-generic struct embed `pkg.Box[T]` (generic_type wrapping a
// qualified_type). Reduces to the bare base name `Box`. → EXTENDS G → Box.
type G struct {
base.Box[int]
}
// Qualified interface embed `pkg.Reader` inside an interface body
// (interface_type → type_elem). The synth now walks interface bodies. The
// target base.Reader is an interface → IMPLEMENTS R → Reader.
type R interface {
base.Reader
}

View file

@ -0,0 +1,3 @@
module example.com/app
go 1.21

View file

@ -0,0 +1,5 @@
package app;
public class Box<T> {
public T get() { return null; }
}

View file

@ -0,0 +1,5 @@
package app;
public interface IFoo<T> {
void foo(T t);
}

View file

@ -0,0 +1,5 @@
package app;
public class Service extends Box<String> implements IFoo<String> {
public void foo(String t) {}
}

View file

@ -0,0 +1,13 @@
package app;
// Interface-to-interface EXTENDS (#1951). `interface IA extends IB, IC<String>`
// lives under `interface_declaration > extends_interfaces > type_list`, which
// the registry-primary synth previously NEVER walked (it visited
// class_declaration only) so production silently dropped these edges while the
// legacy @heritage `interface_declaration` arm emitted them. Both bases resolve
// to Interface symbols, so the edges are emitted as IMPLEMENTS at both legs.
// IC<String> exercises the generic-base reduction (IC<String> -> IC), matching
// normalizeSupertypeName.
public interface IA extends IB, IC<String> {
void a();
}

View file

@ -0,0 +1,5 @@
package app;
public interface IB {
void b();
}

View file

@ -0,0 +1,5 @@
package app;
public interface IC<T> {
void c(T t);
}

View file

@ -0,0 +1,10 @@
package app;
// 2-SEGMENT qualified non-generic bases (Outer.Inner shape): `extends base.Base`
// and `implements base.IBar`. Both segments parse as direct type_identifier
// children of the scoped_type_identifier (no nested prefix), so the legacy
// @heritage query MUST end-anchor to the trailing segment or it double-matches
// and emits a spurious prefix edge. Regression guard for the U2 anchor fix.
public class Plain extends base.Base implements base.IBar {
public void bar() {}
}

View file

@ -0,0 +1,8 @@
package app;
// Qualified-generic bases: `extends app.base.Box<String>` (generic_type wrapping
// a scoped_type_identifier) and `implements app.base.IFoo<String>` (in a
// type_list). Both resolve by their trailing simple name (Box / IFoo).
public class Service extends app.base.Box<String> implements app.base.IFoo<String> {
public void foo(String t) {}
}

View file

@ -0,0 +1,9 @@
package app;
// 2-SEGMENT qualified-GENERIC bases: `extends base.Box<String>` and `implements
// base.IFoo<String>`. Exercises the generic_type-wrapped scoped arms at two
// segments (the shape that double-matched before the end-anchor fix). Pairs with
// Plain (2-segment plain) and Service (3-segment generic) for full arm coverage.
public class Two extends base.Box<String> implements base.IFoo<String> {
public void foo(String t) {}
}

View file

@ -0,0 +1,5 @@
package app.base;
public class Base {
public void base() {}
}

View file

@ -0,0 +1,5 @@
package app.base;
public class Box<T> {
public T get() { return null; }
}

View file

@ -0,0 +1,5 @@
package app.base;
public interface IBar {
void bar();
}

View file

@ -0,0 +1,5 @@
package app.base;
public interface IFoo<T> {
void foo(T t);
}

View file

@ -0,0 +1,21 @@
import * as ns from './base.js';
import { Base } from './base.js';
// Qualified base: `extends ns.Base` parses as a class_heritage holding a
// member_expression (object: identifier `ns`, property: property_identifier
// `Base`). The registry-primary synth resolves it by its trailing
// property_identifier (`Base`), matching the legacy @heritage leg's
// normalizeSupertypeName reduction (member_expression -> `Base`).
export class Service extends ns.Base {
base() {
return 'service';
}
}
// Bare control: `extends Base` (direct identifier) — its handling is unchanged
// (byte-identical to the pre-fix simple-base path).
export class Plain extends Base {
base() {
return 'plain';
}
}

View file

@ -0,0 +1,5 @@
export class Base {
base() {
return 'base';
}
}

View file

@ -0,0 +1,5 @@
package models
open class Base {
fun base() {}
}

View file

@ -0,0 +1,12 @@
package models
// Interface-delegation base: `: Iface by d` parses as
// `(delegation_specifier (explicit_delegation (user_type (type_identifier)) <delegate>))`.
// The supertype is the LEADING `user_type` (Iface); the trailing delegate
// expression (`by d`) is NOT a supertype. The registry-primary synth previously
// DROPPED this shape, so production emitted no IMPLEMENTS edge here (#1951).
// Resolves by its simple name `Iface`, matching the legacy @heritage leg's
// normalizeSupertypeName(explicit_delegation) reduction.
class F(d: Iface) : Iface by d {
fun extra() {}
}

View file

@ -0,0 +1,9 @@
package models
// Bare control: constructor-call superclass `: Base()` parses as
// `(delegation_specifier (constructor_invocation (user_type (type_identifier))))`.
// This shape was already handled; it stays byte-identical and is the regression
// guard that the simple-base path is unchanged by the explicit_delegation widening.
class G : Base() {
fun other() {}
}

View file

@ -0,0 +1,5 @@
package models
interface Iface {
fun handle(): String
}

View file

@ -0,0 +1,3 @@
class Base:
def base(self) -> None:
pass

View file

@ -0,0 +1,8 @@
class Model:
def save(self) -> None:
pass
class Container:
def get(self):
return None

View file

@ -0,0 +1,29 @@
import base_mod
import a.b
from base_mod import Container
# Qualified attribute base: `class Service(base_mod.Model)` parses the base as
# an `attribute` node (object `base_mod`, attribute `Model`). The synth resolves
# it by its trailing `.attribute` identifier -> `Model` (#1951).
class Service(base_mod.Model):
pass
# Nested attribute base: `class Nested(a.b.Base)` parses as a nested `attribute`
# (object `a.b`, attribute `Base`). Recurse to the final identifier -> `Base`.
class Nested(a.b.Base):
pass
# Generic subscript base: `class Gen(Container[str])` parses the base as a
# `subscript` node (`value:` `Container`, slice `str`). The synth strips the
# `[...]` via the `value:` field -> `Container`.
class Gen(Container[str]):
pass
# Bare control: `class Plain(Container)` keeps the existing simple-identifier
# capture byte-identical.
class Plain(Container):
pass

View file

@ -0,0 +1,24 @@
require_relative 'outer'
# SCOPED superclass `class C < Outer::Super`: the superclass field holds a
# `scope_resolution` (Outer::Super), not a direct `constant`. The registry-
# primary synth previously dropped this (findChild(superclass,'constant') was
# null) so production silently omitted the EXTENDS edge while the legacy
# @heritage leg captured it (#1951). It must resolve to `Super` by the trailing
# `name:` constant, at parity with normalizeSupertypeName. `include Mixin` flows
# through the independent mixin lane (IMPLEMENTS, unchanged).
class C < Outer::Super
include Mixin
def run
base
end
end
# BARE superclass control `class D < Base` (direct `constant`): the original
# path, kept byte-identical. EXTENDS D -> Base.
class D < Base
def run
base
end
end

View file

@ -0,0 +1,24 @@
# Module-nested superclass + a top-level bare base + a mixin module.
# - `Super` is defined inside `Outer`, so a scoped superclass
# `< Outer::Super` must resolve to it by its trailing bare name (Super).
# - `Base` is a top-level class used as the bare-superclass control.
# - `Mixin` is included by C to exercise the (unchanged) mixin lane.
module Outer
class Super
def base
"super"
end
end
end
class Base
def base
"base"
end
end
module Mixin
def mixed
"mixed"
end
end

View file

@ -0,0 +1,9 @@
use crate::traits::Drawable;
pub struct User {
pub id: u32,
}
impl Drawable for User {
fn draw(&self) {}
}

View file

@ -0,0 +1,9 @@
use crate::traits::Drawable;
pub struct User {
pub name: String,
}
impl Drawable for User {
fn draw(&self) {}
}

View file

@ -0,0 +1,5 @@
mod traits;
mod a;
mod b;
fn main() {}

View file

@ -0,0 +1,3 @@
pub trait Drawable {
fn draw(&self);
}

View file

@ -0,0 +1,4 @@
mod traits;
mod widget;
fn main() {}

View file

@ -0,0 +1,7 @@
pub trait Drawable {
fn draw(&self);
}
pub trait Wrapped<T> {
fn wrap(&self) -> T;
}

View file

@ -0,0 +1,27 @@
pub struct Widget {
label: String,
}
pub struct Gadget {
id: u32,
}
// Qualified trait path with NO `use` — the base is a `scoped_type_identifier`
// that resolves by its trailing name `Drawable` (KTD-1). The trait is unique
// and lives in a sibling module, so it resolves via the single-match fast path.
// This doubles as the lone-cross-module-match characterization: tail-only
// resolution is no worse than the bare-name path here, and distinguishing
// same-named traits across modules is deferred (qualifier-preserving resolution).
impl crate::traits::Drawable for Widget {
fn draw(&self) {
println!("{}", self.label);
}
}
// Qualified-generic trait path — `crate::traits::Wrapped<u32>` normalizes to the
// trailing `Wrapped` through the generic_type -> scoped_type_identifier tail.
impl crate::traits::Wrapped<u32> for Gadget {
fn wrap(&self) -> u32 {
self.id
}
}

View file

@ -0,0 +1,2 @@
class Derived: Outer.Inner {
}

View file

@ -0,0 +1,7 @@
class Outer {
class Inner {
func ping() -> String {
return "inner"
}
}
}

View file

@ -0,0 +1,5 @@
export class Box<T> {
get(): T {
return undefined as unknown as T;
}
}

View file

@ -0,0 +1,3 @@
export interface IFoo<T> {
foo(t: T): void;
}

View file

@ -0,0 +1,6 @@
import { Box } from './Box';
import { IFoo } from './IFoo';
export class Service extends Box<string> implements IFoo<string> {
foo(t: string): void {}
}

View file

@ -0,0 +1,15 @@
import * as ns from './base';
// Qualified-generic bases: `extends ns.Box<string>` (extends_clause value is a
// member_expression, type_arguments a sibling) and `implements ns.IFoo<string>`
// (implements_clause -> generic_type wrapping a nested_type_identifier). Both
// resolve by their trailing simple name (Box / IFoo).
export class Service extends ns.Box<string> implements ns.IFoo<string> {
foo(t: string): void {}
}
// Qualified non-generic bases: `extends ns.Base` (member_expression) and
// `implements ns.IBar` (nested_type_identifier).
export class Plain extends ns.Base implements ns.IBar {
bar(): void {}
}

View file

@ -0,0 +1,17 @@
export class Base {
base(): void {}
}
export class Box<T> {
get(): T {
return undefined as unknown as T;
}
}
export interface IFoo<T> {
foo(t: T): void;
}
export interface IBar {
bar(): void;
}

View file

@ -4,8 +4,8 @@
"digest": "3459af9360d51aaaba72963fc49bb79edc188e97f9831421d6504c7919b61dc5"
},
"php-abstract-dispatch/src/Repositories/SqlRepository.php": {
"captureGroups": 13,
"digest": "01093dcbb7c4482e93572c59d091c6ece2332d46badd7d7a5b44fd2f7cad5507"
"captureGroups": 14,
"digest": "5905bb450b29d4e186d74b50f70f07a54c8e0d4b8c3748c998c1579e72283215"
},
"php-abstract-dispatch/src/app.php": {
"captureGroups": 10,
@ -40,8 +40,8 @@
"digest": "f6ad05a50da70b32744792da353d520d63722bca7c0802e603af5eb13a223c5c"
},
"php-ambiguous/app/Services/UserHandler.php": {
"captureGroups": 10,
"digest": "90054792db28ad77054994b483305ebf82d8a8959a0350a721d4a76438aea0a0"
"captureGroups": 12,
"digest": "1c47e7020939a6cd6bb18f3732b51a16f4c75ded0fe9883a87d6706a2c624d1f"
},
"php-app/app/Contracts/Loggable.php": {
"captureGroups": 7,
@ -56,16 +56,16 @@
"digest": "aadf56b87d67191b13f4e3f9fae7e8097f4c8a81d13135645ceb8c5be1063a9b"
},
"php-app/app/Models/BaseModel.php": {
"captureGroups": 16,
"digest": "3a768a8443ad599d8501e9aa1e56535cb32ba8b13779b837a68781df3442edff"
"captureGroups": 18,
"digest": "be7ca5be7e28417afdef2e45c8cfed9e04594b341b0d676e7a00fdd84c94e42a"
},
"php-app/app/Models/User.php": {
"captureGroups": 25,
"digest": "641b8f77fc29b102d2c4d4f04cfea8f5a64f769c0d53da9282c0d9505e1e7dd9"
"captureGroups": 27,
"digest": "b6b08be1af66cbd757cfd715389725952c0e2a8e5e910ed715594c0b07570a37"
},
"php-app/app/Services/UserService.php": {
"captureGroups": 37,
"digest": "4c20f536c2df20a9fc1cac33aa48a06535898dcf4f46c62067c8ffd9deab5dbd"
"captureGroups": 38,
"digest": "80b8557e183052f25222cfda8879e08b1613752001f58670ce7b3dbf4b8bee28"
},
"php-app/app/Traits/HasTimestamps.php": {
"captureGroups": 11,
@ -108,8 +108,8 @@
"digest": "0c8a7c7edaa20009b71f22fef98230119a4738d2a21c8b88d64047c3c932fd36"
},
"php-child-extends-parent/src/Child.php": {
"captureGroups": 4,
"digest": "4943bc0abc98546bb82d98e17260d686805a6aa644aabf2b86ad75dc8334b73b"
"captureGroups": 5,
"digest": "07f4216630a17fddb627eff217aa81f089b5e1fc6a9f82d727a6ae91693f8e38"
},
"php-child-extends-parent/src/Parent.php": {
"captureGroups": 7,
@ -240,12 +240,12 @@
"digest": "47af1c30a957f15a2a5ebbddd80c0214a4f68c15f08699fc81cdbfaefa4a70b5"
},
"php-grandparent-resolution/app/Models/B.php": {
"captureGroups": 4,
"digest": "d6326a8eb65bfa8da20d9fb0e5ce07c61eeabd5d53cc94faaebbc3dda0274359"
"captureGroups": 5,
"digest": "eea22a8993a483f264d7bed26999be9d4d0cb9ad48a30ef85bc09b5049b274c8"
},
"php-grandparent-resolution/app/Models/C.php": {
"captureGroups": 4,
"digest": "92fb1dabd5e6dc6eb4115d26303c96ba3d5e34ef9d0ccd965f531b5cf34b7390"
"captureGroups": 5,
"digest": "497307bd7e5c75da828297baabfc1dc2a36865ba358583a4bc39b605ddaacc16"
},
"php-grandparent-resolution/app/Models/Greeting.php": {
"captureGroups": 7,
@ -292,16 +292,16 @@
"digest": "f32de8a30558f2678e73c68d7db9c6eab827fd8d5f7655b906a76066c8421bef"
},
"php-method-enrichment/src/Models/Dog.php": {
"captureGroups": 8,
"digest": "ffe9909633e018ae077b685a83d18dde30d30b818068b7ef6674cbd184ebdf33"
"captureGroups": 9,
"digest": "04aabc879f65c14e581ab782acfedf74335ba2905402a5fbfcf15bf5308f57d5"
},
"php-method-enrichment/src/app.php": {
"captureGroups": 11,
"digest": "52791f6945c8c4ae083b816bd3af239bce44f0e97cbf80cdc119f4f366015138"
},
"php-mro-arity-mismatch/app/Models/ChildModel.php": {
"captureGroups": 15,
"digest": "e007097393563883393a2469befbbe76568fa25dce87d15cc808a877edc0177c"
"captureGroups": 16,
"digest": "eabbdb94047d99d03b7e5cafcde4b0f89666695ca4c3b97ea7e68148999c1c00"
},
"php-mro-arity-mismatch/app/Models/Orphan.php": {
"captureGroups": 9,
@ -364,8 +364,8 @@
"digest": "e047de645ae2b8029a4bb31a61bbc556f58da066d346a2e57101d22cb8dcbb6a"
},
"php-parent-resolution/app/Models/User.php": {
"captureGroups": 8,
"digest": "b34ddf79f02bc1f8a233c91e0d7ad7471cc50912897d619b03c9c60e111de2e6"
"captureGroups": 10,
"digest": "9e8891046e8311bcc07fd1f726e8f28dac37c2c2a6e28ed08b780ff475dbe60a"
},
"php-parent-vs-trait/app/Auditable.php": {
"captureGroups": 7,
@ -376,8 +376,8 @@
"digest": "d7f05cb3f8cf09740fd7063932cc4fccb5b6fff553093b64a341e2503ec42f2d"
},
"php-parent-vs-trait/app/Child.php": {
"captureGroups": 14,
"digest": "8b0149c1d5d54d5d2cb743bbf43d424bc19cf758f4dde04105084c0903117825"
"captureGroups": 16,
"digest": "e3e9273efaf6c06ed241dc88d0754b167a64197731a4ccc18be43672fab07547"
},
"php-phpdoc-attribute-return-type/Models.php": {
"captureGroups": 11,
@ -452,8 +452,8 @@
"digest": "70b05bbb0ad6e8c1ba62d3355dbc1ffa6ac01c9e85ac22e43a07e387eddf401e"
},
"php-super-resolution/app/Models/User.php": {
"captureGroups": 9,
"digest": "e62c49645b1ef609bb915c355b06379d996c030c3eaacbe40ca49d0d0b323e64"
"captureGroups": 10,
"digest": "56d657425e80ae4e58ffe139f90afab5a8527e191bc4b76662f5ce4cbbca4cbb"
},
"php-this-receiver-disambiguation/AdminService.php": {
"captureGroups": 15,
@ -468,16 +468,16 @@
"digest": "147924d3638edc63eef1a009943fae66a87ac85d02ae533428646c644e81c2bb"
},
"php-transitive-traits/app/Models/Consumer.php": {
"captureGroups": 17,
"digest": "a447ed654cc0e074c2f0d93b0d371953e823c9a77da559e841741f5b73b97058"
"captureGroups": 18,
"digest": "c4524f4fa18f6e7f0fd76a11920a6a65ab547ab4cf0fa5799b42b7678e14db08"
},
"php-transitive-traits/app/Traits/TraitA.php": {
"captureGroups": 7,
"digest": "53ca5a3b0407c54a2d6ce80a83eee592c32443795a01186bc9c4cac680e050b7"
"captureGroups": 8,
"digest": "485735b607fbe569527952fe697b33723626de3e974807e0bd9d84f63b789f65"
},
"php-transitive-traits/app/Traits/TraitB.php": {
"captureGroups": 7,
"digest": "abafc915f22428f94e82ca1450902b6ef0599557e05aea345f388392b8097bf2"
"captureGroups": 8,
"digest": "ad387364baf5195af56688cf53956d40b4da193bad28a37b148963fbea68cbea"
},
"php-transitive-traits/app/Traits/TraitC.php": {
"captureGroups": 7,

View file

@ -4,12 +4,12 @@
"digest": "8662c17b0f21fcfa650065abd62f0c9b7e1c65bf8a1f7dce6f2a16bba9df759f"
},
"python-abstract-dispatch/base.py": {
"captureGroups": 15,
"digest": "9c2891a6143c10cc3d81603c8623e54b6914cd8aaa668e84b59c1602492c0e4b"
"captureGroups": 16,
"digest": "2d25dcc17cb5b31cea26c15776d3a8cae290790d92a7adc6d6155be534fdd75b"
},
"python-abstract-dispatch/impl.py": {
"captureGroups": 14,
"digest": "2a7ec28b431cb4010829bca1f0cadf3b11ef7a67abe16f322fc4329a5b619223"
"captureGroups": 15,
"digest": "6c27015f13d32024ce06c1515ab29ca0436df7a4864bbb62188d5f79d685cc31"
},
"python-alias-imports/app.py": {
"captureGroups": 13,
@ -40,8 +40,8 @@
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"python-ambiguous/services/user_handler.py": {
"captureGroups": 7,
"digest": "2a441d1b24e272fdf5c214401d59211b6dff18b56bdaa06148f633ee3a5105e2"
"captureGroups": 8,
"digest": "5a4bb82d0e6a6a53fe012f197e42c572ac1169ff38e84db9c6281399f6739021"
},
"python-ancestor-import/a/b/c/deep.py": {
"captureGroups": 5,
@ -128,8 +128,8 @@
"digest": "0f60d5cd521b0073524b0993e82d5291f86badd5cbefb986cefdf7b0bed64157"
},
"python-child-extends-parent/child.py": {
"captureGroups": 4,
"digest": "c85d867b0b206cd3e13e8dd94c83b49f13e0f2ddde7acd61785b9755f41d3d14"
"captureGroups": 5,
"digest": "d118691eb76c9432841743efee8556f1e7a1d136e9b403a12fd512f91d73ca61"
},
"python-child-extends-parent/parent.py": {
"captureGroups": 7,
@ -208,16 +208,16 @@
"digest": "392b15be747e2b5cbd3ac5a9e61a7677ffa6ba52e49d3631681e43c557373b5f"
},
"python-django-app-imports/accounts/apps.py": {
"captureGroups": 5,
"digest": "6fc1529373f9e3183ccd1c9a7013fa2762ef30c36fea7a7afb4ba418aafab9b0"
"captureGroups": 6,
"digest": "784cba903ad9534337ed820b085c8ecc352964e797bde1d4dda9e700960366a0"
},
"python-django-app-imports/accounts/migrations/__init__.py": {
"captureGroups": 0,
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"python-django-app-imports/accounts/models.py": {
"captureGroups": 7,
"digest": "35025fe0635eb78a60be6ec7a808aa1db586f49a878f16263307b3a04452b4d5"
"captureGroups": 8,
"digest": "b240f4ea2135824ee47fd5f2d9a4788ff0374cafb5070b295fc710a523f19873"
},
"python-django-app-imports/accounts/tests.py": {
"captureGroups": 2,
@ -236,16 +236,16 @@
"digest": "392b15be747e2b5cbd3ac5a9e61a7677ffa6ba52e49d3631681e43c557373b5f"
},
"python-django-app-imports/billing/apps.py": {
"captureGroups": 5,
"digest": "b93de8fe8c56a8ef878b665667fc5fcfa73b526d095228ed4c972aeff9c1ad44"
"captureGroups": 6,
"digest": "0ff487476397cc85c2ce5ec0afe59eb82d83f97bcb3d4518b5040f52790e1833"
},
"python-django-app-imports/billing/migrations/__init__.py": {
"captureGroups": 0,
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"python-django-app-imports/billing/models.py": {
"captureGroups": 11,
"digest": "9e7f426103b6c5f53630bddf7c50253bee22c0c018ef85437fdefe0117998053"
"captureGroups": 12,
"digest": "eedb2e1992f85a6743555947118784db9c78f3c113d2d598e53dd54cb80a0629"
},
"python-django-app-imports/billing/tests.py": {
"captureGroups": 2,
@ -348,12 +348,12 @@
"digest": "5879a42d6655248623c9aee193fedcae51daa1887d8e597fdd944ad93af053a4"
},
"python-grandparent-resolution/models/b.py": {
"captureGroups": 4,
"digest": "55d7ad9e04c57cffa63b0bbcd71bfe18277b6369e7f4facb0faf802eea94db91"
"captureGroups": 5,
"digest": "be5c28ebfb06cd90c7cd453d612f0cacdffdac3e57d7ce794c0a353a9b057597"
},
"python-grandparent-resolution/models/c.py": {
"captureGroups": 4,
"digest": "1fe0d62c3f332a954e7a7b18dd3c1a6d8d051ecf007d36cb6ecb32d3f7592c38"
"captureGroups": 5,
"digest": "2ad9124422ca018e59855c55a054d5c37d15b448a90027842fc56dd6f61e4593"
},
"python-grandparent-resolution/models/greeting.py": {
"captureGroups": 7,
@ -472,8 +472,8 @@
"digest": "88dfd417951b8083184f83da8c1e0c2b19700cfbf1f1ff203a904ebc00f50b30"
},
"python-method-enrichment/models.py": {
"captureGroups": 23,
"digest": "c177fc004563a84c1ccb5ed9cac366f184a7b5d0badcbfe7b3c204caf23bd533"
"captureGroups": 25,
"digest": "14c45aebe0fc6ad1a9328bda3da8cdf7eaa6b9d84641cd2d63c1a105aa18cd42"
},
"python-module-export-vs-method-collision/app.py": {
"captureGroups": 14,
@ -500,16 +500,16 @@
"digest": "98bcec072e85a50303be141212b835322f5f9e53f7fe5d77b23d8ee524623e84"
},
"python-multi-level-mro/child.py": {
"captureGroups": 4,
"digest": "c85d867b0b206cd3e13e8dd94c83b49f13e0f2ddde7acd61785b9755f41d3d14"
"captureGroups": 5,
"digest": "d118691eb76c9432841743efee8556f1e7a1d136e9b403a12fd512f91d73ca61"
},
"python-multi-level-mro/grandparent.py": {
"captureGroups": 7,
"digest": "f9f81d3a37c55b3e23bec3774405920afa29c5793c46860a98a06c5d0c7f0980"
},
"python-multi-level-mro/parent.py": {
"captureGroups": 4,
"digest": "05380e5d4d88a93546a3185c0f6598259cc5a1c271ec08a130cf72cf9107023d"
"captureGroups": 5,
"digest": "b68bfb8fdedb8f725c609264e604ccb674a5a775c0d87c008a9990234c70bde3"
},
"python-multi-segment-ancestor-import/backend/auth_utils.py": {
"captureGroups": 6,
@ -592,16 +592,16 @@
"digest": "f0384bd6ecb7d1a9ad2306358917b7295c71ea8f1bcea818fd39c56d2c28c7e9"
},
"python-parent-resolution/models/user.py": {
"captureGroups": 8,
"digest": "0dbcf175be44961f7a7c02e21167f10063772297633173236ce49f970bb395a4"
"captureGroups": 9,
"digest": "b06a66a108097eec9427a028dec38bbab284918ac39344e86e58bba89533c530"
},
"python-pkg/models/base.py": {
"captureGroups": 9,
"digest": "4984ee01b7a9fefe622195f0e4925823c0e62a1714ca2dda5cd8250e5e45fa7c"
},
"python-pkg/models/user.py": {
"captureGroups": 7,
"digest": "8c340fe8e46b5adf03c82e736822c501983c087b85dc72bdf6d12b75faa843ab"
"captureGroups": 8,
"digest": "9ee707b36f42a635fdb867ce20359b5f51ac4e13550cde801359dc8314e01a77"
},
"python-pkg/services/auth.py": {
"captureGroups": 9,
@ -623,6 +623,22 @@
"captureGroups": 11,
"digest": "abf5fcdf7cc473efa5a149319b0fc3d14b3ae91a2e9a14d621243c5ed7fafafa"
},
"python-qualified-base/a/__init__.py": {
"captureGroups": 0,
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"python-qualified-base/a/b.py": {
"captureGroups": 7,
"digest": "725adf1074b272c8d2ce664549106b07286a92cfb08ab4d605068102317f8baa"
},
"python-qualified-base/base_mod.py": {
"captureGroups": 12,
"digest": "7fc34dae23f54cdee030a2d4a1d80c0bf226ea3b37335ecf48551c4707d32e20"
},
"python-qualified-base/service.py": {
"captureGroups": 16,
"digest": "adecbd613fe97656cd5797c4dbef9f3c32bcd1b9faa5e765227af5d2001da427"
},
"python-qualified-constructor/main.py": {
"captureGroups": 9,
"digest": "9e4d51c06d75721e3c07898124ef85139d52d9f20d6df6bae557aff98a3ed207"
@ -712,8 +728,8 @@
"digest": "1d6eb1cdc661f2463d8e1a499eaa5bfcd9367324f6090c44fe4d59ed02151215"
},
"python-super-resolution/models/user.py": {
"captureGroups": 10,
"digest": "2ab193907b46f0fd20f18e0b3b019986ae34f6252bff38145ebb40922bf7eb00"
"captureGroups": 11,
"digest": "8a66f8962fcf960b66c1106d1d67da21f7f6bac323b961e5fc0623b3c93dbc38"
},
"python-variadic-resolution/app.py": {
"captureGroups": 5,

View file

@ -1,7 +1,7 @@
{
"ruby-ambiguous/lib/user_handler.rb": {
"captureGroups": 9,
"digest": "282d98829c29708081e96207dc0a6d82142400444a1345e162287e6b2c23d019"
"captureGroups": 10,
"digest": "5c5588f1417a793f5b5d80ac09d6aa7fbff94dacb743b1da5395f72a91607ebf"
},
"ruby-ambiguous/models/handler.rb": {
"captureGroups": 6,
@ -32,8 +32,8 @@
"digest": "49bd37b2138dc17820351c126a7a75622cb564e8dd15a9b5f79fcc2d0844474d"
},
"ruby-app/lib/user.rb": {
"captureGroups": 23,
"digest": "38b79d3ef8e804c56e3819804129a6fc9c6bbb8d93d2a96413909f4884e99d94"
"captureGroups": 24,
"digest": "6567c827276575ef3a087757b5725e381fbedb421ffecfe6c5904d7d62f6bcd4"
},
"ruby-call-result-binding/app.rb": {
"captureGroups": 17,
@ -72,8 +72,8 @@
"digest": "8a75c4468b2b1f033620e7d233071340c129a06db4b8ca85a54293efdc5b989f"
},
"ruby-child-extends-parent/lib/child.rb": {
"captureGroups": 5,
"digest": "6b0b88e4101367fb23107124b5ccea5c54632cc39ec272c5816fc6119b680910"
"captureGroups": 6,
"digest": "7742919ebf9abc5c07d42b7889af78f4cb1c5b54a432b1cd42da68b69c02c052"
},
"ruby-child-extends-parent/lib/parent.rb": {
"captureGroups": 6,
@ -156,12 +156,12 @@
"digest": "83bfdb09dd8a1a0bda06c84092041e257502ca37ae393c57ec2ff43b67d699ae"
},
"ruby-grandparent-resolution/lib/models/b.rb": {
"captureGroups": 5,
"digest": "fc9e9541d4c3e85be94cc8be6559d6ee25b9d7178aafa7b50848ab00409446dc"
"captureGroups": 6,
"digest": "e904d2c4238558168579e0c3afc2ca8bf3eed71e94861942858089d30d5dc7bf"
},
"ruby-grandparent-resolution/lib/models/c.rb": {
"captureGroups": 5,
"digest": "d507a997c8d022705c15d55c3bbc6a6134353c1114a2de4629f4b4b4f9080ca0"
"captureGroups": 6,
"digest": "fd1bf5ad5b3a1eb4fbad3473e7eac7a156b01e6d77f6662d768bca1a2b97fb18"
},
"ruby-grandparent-resolution/lib/models/greeting.rb": {
"captureGroups": 6,
@ -188,8 +188,8 @@
"digest": "54fc82a9a0a67ccff1d5ca3055c93044e79e6cd2972d943f9c2e90f1f2198716"
},
"ruby-method-enrichment/lib/animal.rb": {
"captureGroups": 27,
"digest": "5feb199009fcc4923665fcd4a8f3173388be58ca6ae53f19de6795cd2521d397"
"captureGroups": 28,
"digest": "f694a63268d14ab07db08332809823351abc680a7dc53f4797651f0e7b167384"
},
"ruby-method-enrichment/lib/app.rb": {
"captureGroups": 14,
@ -220,8 +220,16 @@
"digest": "4b415b1bcb31b0290a01279f86305459f53e43efb388e96d6e7b4bf804bd12d8"
},
"ruby-parent-resolution/lib/models/user.rb": {
"captureGroups": 8,
"digest": "6bb1c81445d4a3f41b6a23e6a20ee4602a261ee5405c3f463dd81c6741bc8e00"
"captureGroups": 9,
"digest": "0e229692965ca5fd6dc5493aa69f056585912f2e4f4acf5af7915a4aee26cb09"
},
"ruby-qualified-base/lib/derived.rb": {
"captureGroups": 18,
"digest": "8825a54a774c8c77f96315413f632fda626f35d705d8fe697cd362f7acf77a8a"
},
"ruby-qualified-base/lib/outer.rb": {
"captureGroups": 18,
"digest": "f81f06be06d013a08a5c9b730a79494251f102f48ca4c25bf0bbd4a2cdcd889e"
},
"ruby-qualified-types/lib/admin/user.rb": {
"captureGroups": 8,
@ -280,8 +288,8 @@
"digest": "9effd68932555f44968ec89ea1fb0bff4719185c777e856ae167a2289fa72239"
},
"ruby-super-resolution/lib/models/user.rb": {
"captureGroups": 8,
"digest": "5122d102ff7e2e0b9f1396ee5b6ddab33a866aa3c29af10301dbf673ffaa19af"
"captureGroups": 9,
"digest": "73c8b1725670e841d01fefa807b6148017e181e6bb34d8f5110d970f4292eaff"
},
"ruby-write-access/models.rb": {
"captureGroups": 13,

View file

@ -1,7 +1,7 @@
{
"rust-abstract-dispatch/src/lib.rs": {
"captureGroups": 26,
"digest": "8723f4d9b1c4824be8ec22c28b203113b781fc05cffcd80a6b15efe60a0f927d"
"captureGroups": 27,
"digest": "e7a8f5bca32037a547093095eae32d68560b9ef2bbb29c51bef9f2e7b3e57614"
},
"rust-abstract-dispatch/src/main.rs": {
"captureGroups": 19,
@ -100,8 +100,8 @@
"digest": "60fc4ac44f58ae67d462e243655b0571392a18de85b9e200e9391b50c941a6c9"
},
"rust-child-extends-parent/src/child.rs": {
"captureGroups": 12,
"digest": "b357cb50ed1d09f5d218261ed640f73c393f05be36c959e001987d8423ab4194"
"captureGroups": 13,
"digest": "0d60bdf7a88460ee3eebdd48debee9ebb163cfe3f7a22f53323dcef535b6c6b7"
},
"rust-child-extends-parent/src/main.rs": {
"captureGroups": 17,
@ -123,6 +123,22 @@
"captureGroups": 15,
"digest": "043cd8b9341ab299750f8d07f1d1ca34b714c4ecf69acba80ec827e93e3852c0"
},
"rust-cross-module-collision/src/a.rs": {
"captureGroups": 11,
"digest": "27e5b3770416d99fa69fcd492adf565c296780181e11fd5ac6bd26f3c57a9ab0"
},
"rust-cross-module-collision/src/b.rs": {
"captureGroups": 11,
"digest": "45fbd21cf6ed58ca9501d26be034a4969388a730dfc6d36e3982fbc3aa56dcd2"
},
"rust-cross-module-collision/src/main.rs": {
"captureGroups": 7,
"digest": "e0120e3f215282e68d83b4f8f5d8918945e0b3e7ce4e0128c6afd2aa43caa1c0"
},
"rust-cross-module-collision/src/traits.rs": {
"captureGroups": 3,
"digest": "88eef9d92ea6e370bd8ef7fbf64c42ec622ec933fb53c56b32bc67db87fa8e03"
},
"rust-deep-field-chain/models.rs": {
"captureGroups": 24,
"digest": "fe28a5861492fc4b20d911e44abc246ec33e4f3305fe4bd7e95c952972f17564"
@ -136,12 +152,12 @@
"digest": "6516c6b21d2cca74b18ee443ca0049228b4832efc551ca23e5cf3098c95d34f4"
},
"rust-default-constructor/src/repo.rs": {
"captureGroups": 21,
"digest": "968aaf37e492e999c3699fb6eb8f3ee3d021bc112fa205198469e48fd65aeef8"
"captureGroups": 22,
"digest": "8abe9352adc39d7b197e7e042fbb17f5bdad89946bae7b5e8fa11c897102a7f5"
},
"rust-default-constructor/src/user.rs": {
"captureGroups": 21,
"digest": "fe375f3a12ea8c05743816c7d218d50fc1898968603a00906194ef1833e247cd"
"captureGroups": 22,
"digest": "c53db401a81fde2ffd5665393acb9cd605a62ec51c015c3aafb3f41c0897471f"
},
"rust-err-unwrap/src/error.rs": {
"captureGroups": 9,
@ -280,8 +296,8 @@
"digest": "cd836a2a9c15ab240961d2e15f192f7e33d65eb5ebf2e1a8af2f620a47fe66ae"
},
"rust-method-enrichment/src/lib.rs": {
"captureGroups": 37,
"digest": "a257cb6d2bf8f4ecc00d3c2880c5103d3d17ce9444f154be2fa283f5ce0a3e47"
"captureGroups": 38,
"digest": "014c09ab82a5a348c2a6225e07da0773981dd6f282492b4517e33071873dcda6"
},
"rust-method-enrichment/src/main.rs": {
"captureGroups": 18,
@ -320,8 +336,20 @@
"digest": "f35d44f44d81e3a0be40f68ba9dbd4bde6f01659fa15b6db34a458ad460f904e"
},
"rust-parent-resolution/src/user.rs": {
"captureGroups": 12,
"digest": "7c87a84a30e4de06e3bdc6f3adc8310aef09300bb035c77c0a3e6cf08c14c6ac"
"captureGroups": 13,
"digest": "00d29171a1c471087eb3f06cca66c275611e626e6e8f82b8f8fa9b2f6dfb6125"
},
"rust-qualified-trait/src/main.rs": {
"captureGroups": 6,
"digest": "bc8946d31db81b85d780633608fdaa7565258cd788285fa00cd6dcb0de3dd16c"
},
"rust-qualified-trait/src/traits.rs": {
"captureGroups": 5,
"digest": "15be069f28f1400e4beb0b0860acb59979f78549960486f36a92f56578f05a06"
},
"rust-qualified-trait/src/widget.rs": {
"captureGroups": 22,
"digest": "3e1d4c6167338e410d9d93bf5f80f289ffb2f05611813e59c7a53402bf6a101d"
},
"rust-receiver-resolution/src/main.rs": {
"captureGroups": 21,
@ -424,8 +452,8 @@
"digest": "60fc4ac44f58ae67d462e243655b0571392a18de85b9e200e9391b50c941a6c9"
},
"rust-traits/src/impls/button.rs": {
"captureGroups": 30,
"digest": "2b4523d8013330f59ce4bd44c7e8c4d8e185e691c5bef08fa8af20a86eed25de"
"captureGroups": 32,
"digest": "ba93629d0e5a008a5ea84b6a93ea93b1ae4901cc24d8c4c7ee685f51e2712f12"
},
"rust-traits/src/main.rs": {
"captureGroups": 11,

View file

@ -4,8 +4,8 @@
"digest": "56b82214cca3ed89312d84d6b04a48ee13cbe748ea6978e9bffa20ce46ac0930"
},
"swift-abstract-dispatch/Sources/Repository.swift": {
"captureGroups": 23,
"digest": "c97501a445d79f137705c040e57437f6488d4c87ee80f003ec547eb45e8fc35b"
"captureGroups": 24,
"digest": "3a3fa6017b6937de81e5f70bcab727910edb00053c7e5bd30da67ed1fb84c71f"
},
"swift-await-try/App.swift": {
"captureGroups": 13,
@ -28,8 +28,8 @@
"digest": "19b16f002f2e10b661ada767769723ed92c6101fbf8dd895a9077c563c06946d"
},
"swift-child-extends-parent/Sources/Child.swift": {
"captureGroups": 3,
"digest": "4af236b587337ef59887abed10892d2f179b1ad502664086d85139ee840b51fd"
"captureGroups": 4,
"digest": "6a2c35357f3efaef456f9acabaf5a068b6ac9906f83bee3afaaa005271afcbb8"
},
"swift-child-extends-parent/Sources/Parent.swift": {
"captureGroups": 7,
@ -128,8 +128,8 @@
"digest": "e830e3dca7d6c6260181b78d4af5bc61e031bdce3a451588989f17223a5eac1c"
},
"swift-method-enrichment/Sources/Animal.swift": {
"captureGroups": 22,
"digest": "4b7aa00484047437a9e6af3faf12cd6a4e9575bf4dd7d6454e29f05459cf8795"
"captureGroups": 23,
"digest": "999c14cd327ad7e1cc90bae25e9c7e79a8ade5dd274d834d87a61846c4c91259"
},
"swift-method-enrichment/Sources/App.swift": {
"captureGroups": 10,
@ -184,8 +184,8 @@
"digest": "b5ed80965325806c715d1c3a65163b014e0bbba5ca7e2df9d7c81b084fada15e"
},
"swift-overload-dispatch/SqlRepository.swift": {
"captureGroups": 22,
"digest": "0f90013bd74e3c8ee8d040f433144c2e470bd7af391cda22171b36df90d8db86"
"captureGroups": 23,
"digest": "e5abcf7249afe3fb58fad54ec17da29195a95a05e6f1275375b9dc395030d9dc"
},
"swift-parent-resolution/Sources/Models/BaseModel.swift": {
"captureGroups": 7,
@ -196,8 +196,16 @@
"digest": "684be5a2e9d7c03c4a9ce209fe5719a776ad4fe0ed12a7a79ba64eed6f34a40e"
},
"swift-parent-resolution/Sources/Models/User.swift": {
"captureGroups": 8,
"digest": "035816ff924424620539717e70c5a9c3f771ed4b5e7e167d94ccda9bd8abad7b"
"captureGroups": 10,
"digest": "bd01b5adcd523ceee95772cc74ded0dbc1d70449fc9e9d17e975299dcbac783f"
},
"swift-qualified-base/Sources/Derived.swift": {
"captureGroups": 4,
"digest": "39e6ba35775ce624d1fb982204d2e2f0fcbbaf3046e317242602d9f245eb5a9d"
},
"swift-qualified-base/Sources/Outer.swift": {
"captureGroups": 9,
"digest": "8674f64c110ed91d7e63add63a90612c56d7b827e1b0ade54841677e82c56d80"
},
"swift-return-type-inference/App.swift": {
"captureGroups": 21,

View file

@ -0,0 +1,297 @@
/**
* Worker-path inheritance edges for the registry-primary languages (issue #1951).
*
* Diagrams showed classes and interfaces with no EXTENDS / IMPLEMENTS edges
* between them. Root cause: registry-primary languages have their legacy
* `@heritage.*` edges dropped by the worker pipeline's `shouldAccumulate` gate
* (parse-impl.ts) while the scope-resolution path that DOES run in worker
* mode emitted nothing for them (unlike C++, they synthesized no
* `@reference.inherits` captures). Small fixtures stayed under the worker
* threshold and ran sequentially (legacy heritage intact), so the bug hid.
*
* The migration routed every language's inheritance through scope-resolution.
* These tests force the worker pool on small fixtures (production threshold is
* 15 files / 512 KB) and assert the edges are present. They FAIL before the
* fix (0 EXTENDS / 0 IMPLEMENTS in worker mode) and pass once each language
* emits inheritance through scope-resolution. The `usedWorkerPool === true`
* guard is mandatory: without the compiled worker (built by
* `pretest:integration`) the pipeline silently falls back to sequential, which
* would hide the regression.
*
* The C#/Java blocks below are the original (#1951) coverage; the
* table-driven block at the end extends worker-forced coverage to the other
* migrated languages (go, python, php, rust, kotlin, ruby, typescript,
* javascript, swift) so a worker-only capture regression in ANY of them fails
* here rather than slipping every sequential gate.
*
* Run under the default (registry-primary) flags the bug only exists on the
* registry-primary path, so we must NOT force REGISTRY_PRIMARY_*=0 here.
*/
import { describe, it, expect, beforeAll } from 'vitest';
import path from 'node:path';
import {
runPipelineFromRepo,
getRelationships,
edgeSet,
type PipelineResult,
} from './resolvers/helpers.js';
import { isLanguageAvailable } from '../../src/core/tree-sitter/parser-loader.js';
import { SupportedLanguages } from '../../src/config/supported-languages.js';
const FIXTURES = path.resolve(__dirname, '..', 'fixtures', 'lang-resolution');
const swiftAvailable = isLanguageAvailable(SupportedLanguages.Swift);
const runWorker = (fixture: string): Promise<PipelineResult> =>
runPipelineFromRepo(path.join(FIXTURES, fixture), () => {}, {
skipGraphPhases: true,
// Force the worker-pool gate low so a 4-5 file fixture engages the pool.
workerThresholdsForTest: { minFiles: 1, minBytes: 1 },
workerPoolSize: 2,
});
// Sequential counterpart (no worker pool): the legacy heritage path runs and
// scope-resolution dedups against it. Used to pin worker/sequential parity.
const runSequential = (fixture: string): Promise<PipelineResult> =>
runPipelineFromRepo(path.join(FIXTURES, fixture), () => {}, {
skipGraphPhases: true,
skipWorkers: true,
});
describe('C# inheritance edges on the worker path (#1951)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runWorker('csharp-proj');
}, 120_000);
it('genuinely used the worker pool (guards against silent sequential fallback)', () => {
expect(result.usedWorkerPool).toBe(true);
});
it('emits class-extends-class EXTENDS: User → BaseEntity (class-owned, via scope-resolution)', () => {
const extends_ = getRelationships(result, 'EXTENDS');
expect(edgeSet(extends_)).toEqual(['User → BaseEntity']);
// The edge must originate from scope-resolution (the worker-safe channel),
// and be owned by the Class node — not a method/constructor.
expect(extends_[0]?.sourceLabel).toBe('Class');
expect(extends_[0]?.rel.reason).toBe('scope-resolution: inherits');
});
it('emits class-implements-interface IMPLEMENTS: User → IRepository (class-owned, via scope-resolution)', () => {
const implements_ = getRelationships(result, 'IMPLEMENTS');
expect(edgeSet(implements_)).toEqual(['User → IRepository']);
expect(implements_[0]?.sourceLabel).toBe('Class');
expect(implements_[0]?.rel.reason).toBe('scope-resolution: inherits');
});
});
describe('C# interface heritage on the worker path (#1951)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runWorker('csharp-interface-heritage');
}, 120_000);
it('genuinely used the worker pool', () => {
expect(result.usedWorkerPool).toBe(true);
});
it('models interface-extends-interface and multi-interface heritage as IMPLEMENTS', () => {
// C# semantics (matching the legacy DAG): conforming to an interface is
// IMPLEMENTS regardless of whether the child is a class or interface.
const implements_ = getRelationships(result, 'IMPLEMENTS');
expect(edgeSet(implements_)).toEqual([
'IAuditableService → IBarService',
'IAuditableService → IFooService',
'IFooService → IBaseInterface',
'MyService → IAuditableService',
]);
});
it('emits no EXTENDS edges for pure interface heritage', () => {
expect(getRelationships(result, 'EXTENDS').length).toBe(0);
});
});
describe('Java inheritance edges on the worker path (#1951)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runWorker('java-heritage');
}, 120_000);
it('genuinely used the worker pool', () => {
expect(result.usedWorkerPool).toBe(true);
});
it('emits class-extends-class EXTENDS: User → BaseModel (and none to interfaces)', () => {
const extends_ = getRelationships(result, 'EXTENDS');
expect(edgeSet(extends_)).toEqual(['User → BaseModel']);
});
it('emits multi-interface IMPLEMENTS: User → Serializable, User → Validatable', () => {
const implements_ = getRelationships(result, 'IMPLEMENTS');
expect(edgeSet(implements_)).toEqual(['User → Serializable', 'User → Validatable']);
});
});
describe('C# primary-constructor + qualified-generic base on the worker path (#1951 regression)', () => {
// A C# 12 primary constructor is synthesized into the class scope, so the
// shared `resolveCallerGraphId` would degrade the inheritance edge source to
// the constructor (breaking MRO). The edge must stay owned by the Class.
// Also covers fully-qualified generic base-name normalization (App.Repo<int>).
let result: PipelineResult;
beforeAll(async () => {
result = await runWorker('csharp-primary-ctor-heritage');
}, 120_000);
it('genuinely used the worker pool', () => {
expect(result.usedWorkerPool).toBe(true);
});
it('emits EXTENDS owned by the Class, not the primary constructor', () => {
const extends_ = getRelationships(result, 'EXTENDS');
// User(int id) : BaseEntity and Service : App.Repo<int>
expect(edgeSet(extends_)).toEqual(['Service → Repo', 'User → BaseEntity']);
// The regression: every inheritance edge source is the Class node. Before
// the fix, User's source degraded to Constructor:User.
expect(extends_.every((e) => e.sourceLabel === 'Class')).toBe(true);
});
it('emits IMPLEMENTS owned by the Class for a primary-constructor class', () => {
const implements_ = getRelationships(result, 'IMPLEMENTS');
expect(edgeSet(implements_)).toEqual(['User → IFoo']);
expect(implements_.every((e) => e.sourceLabel === 'Class')).toBe(true);
});
});
describe('Worker/sequential inheritance-edge parity (#1951)', () => {
// The dedup-key change (type-prefixed, graph-seeded) must keep the worker
// path (scope-resolution emits) and the sequential path (legacy heritage
// emits, scope-resolution dedups) producing the SAME single edges — no
// double-emission, no dropped IMPLEMENTS.
let worker: PipelineResult;
let sequential: PipelineResult;
beforeAll(async () => {
worker = await runWorker('csharp-proj');
sequential = await runSequential('csharp-proj');
}, 120_000);
it('worker mode used the pool; sequential did not', () => {
expect(worker.usedWorkerPool).toBe(true);
expect(sequential.usedWorkerPool).toBe(false);
});
it('produces identical EXTENDS and IMPLEMENTS edge sets in both modes', () => {
expect(edgeSet(getRelationships(worker, 'EXTENDS'))).toEqual(
edgeSet(getRelationships(sequential, 'EXTENDS')),
);
expect(edgeSet(getRelationships(worker, 'IMPLEMENTS'))).toEqual(
edgeSet(getRelationships(sequential, 'IMPLEMENTS')),
);
});
it('emits exactly one EXTENDS and one IMPLEMENTS in each mode (no double-emission)', () => {
expect(getRelationships(worker, 'EXTENDS').length).toBe(1);
expect(getRelationships(worker, 'IMPLEMENTS').length).toBe(1);
expect(getRelationships(sequential, 'EXTENDS').length).toBe(1);
expect(getRelationships(sequential, 'IMPLEMENTS').length).toBe(1);
});
});
// ---------------------------------------------------------------------------
// Worker-forced coverage for the remaining migrated languages (#1951 review).
// Each language's inheritance now flows ONLY through its scope-resolution synth
// in worker mode (legacy heritage gated off). Edge sets are sorted (edgeSet
// sorts), so the expectations below are in sorted order.
// ---------------------------------------------------------------------------
interface WorkerHeritageCase {
readonly lang: string;
readonly fixture: string;
readonly extends: readonly string[];
readonly implements: readonly string[];
/** Optional gate for grammars that may not be installed (Swift). */
readonly available?: boolean;
}
const WORKER_HERITAGE_CASES: readonly WorkerHeritageCase[] = [
// Go struct embedding → EXTENDS.
{ lang: 'Go', fixture: 'go-child-extends-parent', extends: ['Child → Parent'], implements: [] },
// Python single inheritance → EXTENDS.
{
lang: 'Python',
fixture: 'python-child-extends-parent',
extends: ['Child → Parent'],
implements: [],
},
// PHP class extends + trait use → EXTENDS (Base) + IMPLEMENTS (trait Auditable).
{
lang: 'PHP',
fixture: 'php-parent-vs-trait',
extends: ['Child → Base'],
implements: ['Child → Auditable'],
},
// Rust `impl T for S` → IMPLEMENTS (resolved scope-aware after #1951 review).
{
lang: 'Rust',
fixture: 'rust-traits',
extends: [],
implements: ['Button → Clickable', 'Button → Drawable'],
},
// Kotlin class + interfaces → EXTENDS (BaseModel) + IMPLEMENTS (2 interfaces).
{
lang: 'Kotlin',
fixture: 'kotlin-heritage',
extends: ['User → BaseModel'],
implements: ['User → Serializable', 'User → Validatable'],
},
// Ruby `class Child < Parent` → EXTENDS.
{
lang: 'Ruby',
fixture: 'ruby-child-extends-parent',
extends: ['Child → Parent'],
implements: [],
},
// TypeScript generic base + generic interface → EXTENDS (Box) + IMPLEMENTS (IFoo).
{
lang: 'TypeScript',
fixture: 'typescript-generic-base',
extends: ['Service → Box'],
implements: ['Service → IFoo'],
},
// JavaScript `class Child extends Parent` → EXTENDS.
{
lang: 'JavaScript',
fixture: 'javascript-child-extends-parent',
extends: ['Child → Parent'],
implements: [],
},
// Swift class inheritance → EXTENDS (grammar is an optional dependency).
{
lang: 'Swift',
fixture: 'swift-child-extends-parent',
extends: ['Child → Parent'],
implements: [],
available: swiftAvailable,
},
];
for (const c of WORKER_HERITAGE_CASES) {
describe.skipIf(c.available === false)(
`${c.lang} inheritance edges on the worker path (#1951)`,
() => {
let result: PipelineResult;
beforeAll(async () => {
result = await runWorker(c.fixture);
}, 120_000);
it('genuinely used the worker pool (guards against silent sequential fallback)', () => {
expect(result.usedWorkerPool).toBe(true);
});
it('emits the expected EXTENDS / IMPLEMENTS edge set via scope-resolution', () => {
expect(edgeSet(getRelationships(result, 'EXTENDS'))).toEqual([...c.extends]);
expect(edgeSet(getRelationships(result, 'IMPLEMENTS'))).toEqual([...c.implements]);
});
},
);
}

View file

@ -116,7 +116,7 @@ describe('C# ambiguous symbol resolution', () => {
expect(ifaces.filter((n) => n === 'IProcessor').length).toBe(2);
});
it('heritage targets are synthetic (correct refusal for ambiguous namespace import)', () => {
it('resolves both ambiguous bases to the imported Models namespace via import-aware disambiguation', () => {
const extends_ = getRelationships(result, 'EXTENDS');
const implements_ = getRelationships(result, 'IMPLEMENTS');
@ -125,13 +125,18 @@ describe('C# ambiguous symbol resolution', () => {
expect(implements_.length).toBe(1);
expect(implements_[0].source).toBe('UserHandler');
// The key invariant: no edge points to Other/
if (extends_[0].targetFilePath) {
expect(extends_[0].targetFilePath).not.toContain('Other/');
}
if (implements_[0].targetFilePath) {
expect(implements_[0].targetFilePath).not.toContain('Other/');
}
// `using MyApp.Models;` emits the file-level import edge, so import-aware
// resolution (#1951) disambiguates both same-named bases to the Models/
// definitions (NOT Other/) — pinned exactly (the prior `if (targetFilePath)`
// guard was vacuous). This asserts the correct registry-primary model; the
// legacy DAG does not emit the C# namespace using-import edge and so refuses
// to disambiguate, which is why this test is listed in
// LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES (helpers.ts) — a scope-resolver-
// only correctness win, not branched with conditional logic here.
expect(extends_[0].target).toBe('Handler');
expect(extends_[0].targetFilePath).toBe('Models/Handler.cs');
expect(implements_[0].target).toBe('IProcessor');
expect(implements_[0].targetFilePath).toBe('Models/IProcessor.cs');
});
});
@ -2267,13 +2272,18 @@ describe('C# record base resolution (record inheritance + base.Save)', () => {
expect(all).toContain('UserRecord');
});
it('does not emit a spurious self-EXTENDS (record heritage not emitted by C# heritage queries)', () => {
// NOTE: C# tree-sitter heritage queries cover class/interface
// declarations but not `record_declaration`, so records don't
// emit an EXTENDS edge today. The record-base linkage is still
// visible via `base.Save()` resolution (next test). This
// assertion pins the negative invariant so a future heritage
// extension for records can flip both tests at once.
it('emits no spurious self-EXTENDS for a record (record→record same-namespace EXTENDS is a known registry gap)', () => {
// Since #1956 the registry-primary synth walks `record_declaration` base_lists
// (matching the legacy @heritage leg), so record→class and record→interface
// bases now resolve to EXTENDS/IMPLEMENTS edges — see the qualified/record/
// struct block below (record R : Base, record P : Base(id), …). The
// record→RECORD case in the SAME namespace (`record UserRecord : BaseEntity`,
// both in `Models`) is a separate, pre-existing registry resolution gap: the
// synth emits the @reference.inherits capture, but the same-namespace
// record-target binding is not resolved on the registry leg, so no
// UserRecord→BaseEntity EXTENDS edge appears there (the legacy leg does emit
// it). It is NOT asserted here — doing so would diverge between legs — and is
// tracked as a follow-up. The self-edge invariant must hold on both legs.
const extends_ = getRelationships(result, 'EXTENDS');
const selfExtend = extends_.find((e) => e.source === 'UserRecord' && e.target === 'UserRecord');
expect(selfExtend).toBeUndefined();
@ -2288,13 +2298,12 @@ describe('C# record base resolution (record inheritance + base.Save)', () => {
c.targetFilePath === 'src/Models/BaseEntity.cs',
);
expect(baseSave).toBeDefined();
// NOTE: no `rel.reason` assertion here. Records don't emit EXTENDS
// edges today (see the negative-invariant test above), so the
// super-branch MRO lookup returns no ancestor and the edge is
// produced by the downstream reference-index fallback instead of
// the canonical super path. The `csharp-super-resolution` and
// NOTE: no `rel.reason` assertion here. The base.Save() linkage is
// exercised independently of which path produces it (super-branch MRO
// now that records emit EXTENDS since #1951, or the downstream
// reference-index fallback). The `csharp-super-resolution` and
// `csharp-generic-parent` suites pin the super-branch reason on
// paths that do go through MRO.
// paths that go through MRO.
const selfSave = calls.find(
(c) =>
c.source === 'Save' &&
@ -2305,6 +2314,55 @@ describe('C# record base resolution (record inheritance + base.Save)', () => {
});
});
// ---------------------------------------------------------------------------
// C# qualified / record / struct / alias-qualified base heritage (#1951)
//
// The registry-primary synth previously walked only class/interface base
// lists, so `record R(...) : Base, IFoo`, `record P(...) : Base(id), IBar`
// (primary_constructor_base_type), `struct S : IFoo, ns.IBar`, and the
// `alias_qualified_name` base `B : DomainAlias::Base` produced NO inheritance
// edges in worker mode — even though the legacy @heritage leg covered them.
// This block runs on BOTH legs (createResolverParityIt) and asserts the now-
// emitted edge sets, exactly mirroring the bare names normalizeSupertypeName
// reduces each shape to (Base / IFoo / IBar).
// ---------------------------------------------------------------------------
describe('C# qualified/record/struct/alias base heritage (#1951)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'csharp-qualified-base'), () => {});
}, 60000);
it('emits EXTENDS for every class-like base, tail/type/alias-resolved', () => {
// R, P (record bases incl. primary_constructor_base_type `Base(id)`), and
// A (qualified_name) / B (alias_qualified_name) all derive from the Class
// `Base`, so each takes the EXTENDS branch (target kind = Class).
const extends_ = getRelationships(result, 'EXTENDS');
expect(edgeSet(extends_)).toEqual(['A → Base', 'B → Base', 'P → Base', 'R → Base']);
});
it('emits IMPLEMENTS for interface bases on records and structs', () => {
// R → IFoo (record identifier base), P → IBar (record identifier base
// alongside the primary_constructor_base_type), S → IFoo + S → IBar
// (struct base_list: identifier + qualified_name). Interface targets take
// the IMPLEMENTS branch.
const implements_ = getRelationships(result, 'IMPLEMENTS');
expect(edgeSet(implements_)).toEqual(['P → IBar', 'R → IFoo', 'S → IBar', 'S → IFoo']);
});
it('all heritage edges point to real graph nodes', () => {
for (const edge of [
...getRelationships(result, 'EXTENDS'),
...getRelationships(result, 'IMPLEMENTS'),
]) {
const target = result.graph.getNode(edge.rel.targetId);
expect(target).toBeDefined();
expect(target!.properties.name).toBe(edge.target);
}
});
});
// ---------------------------------------------------------------------------
// Finding 4: struct overload dispatch exercises the extracted
// narrowOverloadCandidates utility via implicit-this free calls.

View file

@ -92,6 +92,39 @@ describe('Go package import & call resolution', () => {
});
});
// ---------------------------------------------------------------------------
// Qualified / generic / pointer / interface embeds (#1951)
//
// The registry-primary inheritance synth (languages/go/captures.ts) used to
// emit edges ONLY for a bare `type_identifier` struct embed, silently DROPPING
// the qualified (`pkg.Base`), pointer (`*pkg.Base`), qualified-generic
// (`pkg.Box[T]`) struct embeds and ALL interface embeds — even though the
// legacy `@heritage` leg (config-driven since #1940) captured them. This
// fixture widens the synth to parity: every base reduces to its bare simple
// name, struct bases resolve to EXTENDS and interface bases to IMPLEMENTS. The
// bare-name struct embed (T → Local) is the byte-identical simple-base path
// (unchanged), kept here as a regression guard. Runs under BOTH legs
// (createResolverParityIt), so a regression on either leg fails.
// ---------------------------------------------------------------------------
describe('Go qualified-base embed resolution (#1951)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'go-qualified-base'), () => {});
}, 60000);
it('emits EXTENDS for qualified / pointer / generic / bare struct embeds (tail-resolved)', () => {
const extends_ = getRelationships(result, 'EXTENDS');
expect(edgeSet(extends_)).toEqual(['G → Box', 'P → Base', 'S → Base', 'T → Local']);
});
it('emits IMPLEMENTS for qualified and bare interface embeds (tail-resolved)', () => {
const implements_ = getRelationships(result, 'IMPLEMENTS');
expect(edgeSet(implements_)).toEqual(['R → Reader', 'RLocal → LocalIface']);
});
});
// ---------------------------------------------------------------------------
// Ambiguous: Handler struct in two packages, package import disambiguates
// ---------------------------------------------------------------------------

View file

@ -26,6 +26,16 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, Readonly
// Generic type-argument USES edges are emitted by the registry-primary
// resolver only; the legacy DAG path does not synthesize these references.
'emits USES edges for generic type arguments',
// Ambiguous same-named base (Handler/IProcessor declared in both Models/
// and Other/) is disambiguated to the Models/ definitions by the
// registry-primary import-aware resolver: `using MyApp.Models;` emits the
// file-level import edge that `resolveAmbiguousInheritanceBaseViaImports`
// keys on. The legacy DAG does not emit the C# namespace using-import edge
// (same root cause as the using-import-edge expected-failure above), so it
// cannot disambiguate and `resolveHeritageId` refuses to a synthetic
// Class:/Interface: target. Scope-resolver-only correctness win; backporting
// is out of scope per the migration policy.
'resolves both ambiguous bases to the imported Models namespace via import-aware disambiguation',
]),
go: new Set([
// The legacy DAG path does not resolve method calls when the method is

View file

@ -83,6 +83,108 @@ describe('Java heritage resolution', () => {
});
});
// ---------------------------------------------------------------------------
// Generic-base heritage (#1951): extends Box<T> + implements IFoo<T>. The
// legacy @heritage query was type_identifier-only and matched 0 generic bases,
// while the registry-primary synth emitted 1 — a latent =0/=1 parity break.
// Widening the legacy query closes it. This block runs under BOTH legs via
// createResolverParityIt, so it fails on the legacy leg if widening regresses.
// ---------------------------------------------------------------------------
describe('Java generic-base heritage resolution (#1951)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'java-generic-base'), () => {});
}, 60000);
it('emits EXTENDS Service → Box for a generic superclass (extends Box<String>)', () => {
const extends_ = getRelationships(result, 'EXTENDS');
expect(edgeSet(extends_)).toEqual(['Service → Box']);
});
it('emits IMPLEMENTS Service → IFoo for a generic interface (implements IFoo<String>)', () => {
const implements_ = getRelationships(result, 'IMPLEMENTS');
expect(edgeSet(implements_)).toEqual(['Service → IFoo']);
});
});
// ---------------------------------------------------------------------------
// Qualified (namespaced) bases (#1956 tri-review U2). Three shapes:
// - Service: 3-segment generic (extends app.base.Box<T>, implements app.base.IFoo<T>)
// - Plain: 2-segment plain (extends base.Base, implements base.IBar)
// - Two: 2-segment generic (extends base.Box<T>, implements base.IFoo<T>)
// The registry-primary synth resolves each by its scoped-name tail; the legacy
// @heritage query was widened with end-anchored scoped_type_identifier arms to
// match. The 2-segment cases are the regression guard: an un-anchored arm
// double-matches a 2-segment base (both segments are direct type_identifier
// children) and emits a spurious prefix edge, breaking the =1/=1 parity this
// runs under BOTH legs (createResolverParityIt) to assert.
// ---------------------------------------------------------------------------
describe('Java qualified-base heritage resolution (#1956 U2)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'java-qualified-base'), () => {});
}, 60000);
it('emits exactly one EXTENDS per class, tail-resolved (no spurious prefix edge)', () => {
const extends_ = getRelationships(result, 'EXTENDS');
expect(edgeSet(extends_)).toEqual(['Plain → Base', 'Service → Box', 'Two → Box']);
});
it('emits exactly one IMPLEMENTS per class, tail-resolved (no spurious prefix edge)', () => {
const implements_ = getRelationships(result, 'IMPLEMENTS');
expect(edgeSet(implements_)).toEqual(['Plain → IBar', 'Service → IFoo', 'Two → IFoo']);
});
});
// ---------------------------------------------------------------------------
// Interface-to-interface EXTENDS (#1951): `interface IA extends IB, IC<String>`.
// The registry-primary synth walked class_declaration ONLY, so it NEVER emitted
// interface-to-interface heritage — production silently dropped these edges
// while the legacy @heritage `interface_declaration (extends_interfaces …)` arm
// emitted them: a latent =N/=0 parity break. Widening the synth's traversal to
// also walk interface_declaration > extends_interfaces > type_list closes it.
// Both bases resolve to Interface symbols, so preEmitInheritanceEdges emits them
// as IMPLEMENTS (matching the legacy arm's @heritage.impl / kind:'implements').
// IC<String> exercises the generic-base reduction (IC<String> -> IC). Runs under
// BOTH legs via createResolverParityIt, so it fails on the legacy leg if the
// synth and legacy query disagree.
// ---------------------------------------------------------------------------
describe('Java interface-extends-interface heritage resolution (#1951)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'java-iface-extends'), () => {});
}, 60000);
it('detects 3 interfaces and no classes', () => {
expect(getNodesByLabel(result, 'Interface')).toEqual(['IA', 'IB', 'IC']);
expect(getNodesByLabel(result, 'Class')).toEqual([]);
});
it('emits IMPLEMENTS IA → IB and IA → IC for interface-to-interface extends', () => {
const implements_ = getRelationships(result, 'IMPLEMENTS');
expect(edgeSet(implements_)).toEqual(['IA → IB', 'IA → IC']);
});
it('emits no EXTENDS edges (interface bases resolve to Interface → IMPLEMENTS)', () => {
const extends_ = getRelationships(result, 'EXTENDS');
expect(extends_).toEqual([]);
});
it('all interface-heritage edges point to real Interface graph nodes', () => {
for (const edge of getRelationships(result, 'IMPLEMENTS')) {
const target = result.graph.getNode(edge.rel.targetId);
expect(target).toBeDefined();
expect(target!.properties.name).toBe(edge.target);
}
});
});
// ---------------------------------------------------------------------------
// Ambiguous: Handler + Processor in two packages, imports disambiguate
// ---------------------------------------------------------------------------

View file

@ -22,6 +22,31 @@ import {
// requires this for the issue #1358 singleton describes below.
const it = createResolverParityIt('javascript');
// ---------------------------------------------------------------------------
// Qualified (namespaced) base (#1951): `extends ns.Base` parses as a
// class_heritage holding a member_expression (object: `ns`, property: `Base`).
// The registry-primary synth (synthesizeJsInheritanceReferences) previously
// dropped member_expression bases, emitting only for a direct identifier base,
// so production silently omitted this EXTENDS edge. It is now resolved by the
// base's trailing property_identifier (`Base`), matching the legacy @heritage
// leg's normalizeSupertypeName reduction. `Plain extends Base` is the bare
// control (its simple-base handling is unchanged). Runs under BOTH legs via
// createResolverParityIt.
// ---------------------------------------------------------------------------
describe('JavaScript qualified-base heritage resolution (#1951)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'javascript-qualified-base'), () => {});
}, 60000);
it('emits EXTENDS for the qualified base (ns.Base) and the bare control (Base)', () => {
const extends_ = getRelationships(result, 'EXTENDS');
expect(edgeSet(extends_)).toEqual(['Plain → Base', 'Service → Base']);
});
});
// ---------------------------------------------------------------------------
// skipGraphPhases: verify pipeline works correctly when graph phases are skipped
// ---------------------------------------------------------------------------

View file

@ -113,6 +113,47 @@ describe('Kotlin heritage resolution', () => {
});
});
// ---------------------------------------------------------------------------
// Interface-delegation heritage (#1951): `class F : Iface by d`. The base is an
// `explicit_delegation` (`(user_type) by <delegate>`); the registry-primary
// synth previously DROPPED this shape (only `user_type` / `constructor_invocation`
// were handled), so production emitted NO IMPLEMENTS edge for the delegated
// interface in worker mode — while the legacy @heritage leg (config-driven
// `kotlinHeritageShapes` + normalizeSupertypeName) captured it. Widening the
// synth to descend into `explicit_delegation`'s leading `user_type` closes the
// parity break. G : Base() is the bare control proving the simple-base path is
// unchanged. This block runs under BOTH legs via createResolverParityIt.
// ---------------------------------------------------------------------------
describe('Kotlin interface-delegation heritage resolution (#1951)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'kotlin-qualified-base'), () => {});
}, 60000);
it('emits IMPLEMENTS F → Iface for an interface-delegation base (: Iface by d)', () => {
const implements_ = getRelationships(result, 'IMPLEMENTS');
expect(edgeSet(implements_)).toEqual(['F → Iface']);
});
it('emits EXTENDS G → Base for the bare constructor-call control (: Base())', () => {
const extends_ = getRelationships(result, 'EXTENDS');
expect(edgeSet(extends_)).toEqual(['G → Base']);
});
it('all heritage edges point to real graph nodes', () => {
for (const edge of [
...getRelationships(result, 'EXTENDS'),
...getRelationships(result, 'IMPLEMENTS'),
]) {
const target = result.graph.getNode(edge.rel.targetId);
expect(target).toBeDefined();
expect(target!.properties.name).toBe(edge.target);
}
});
});
// ---------------------------------------------------------------------------
// Ambiguous: Handler + Runnable in two packages, explicit imports disambiguate
// ---------------------------------------------------------------------------

View file

@ -89,6 +89,51 @@ describe('Python relative import & heritage resolution', () => {
});
});
// ---------------------------------------------------------------------------
// Qualified / generic bases (#1951). The registry-primary synth previously
// DROPPED these shapes — only bare `identifier` bases emitted, so production
// silently omitted their inheritance edges while the legacy @heritage leg
// captured them. service.py exercises the three now-handled shapes plus a bare
// control, each base defined in a sibling module:
// - Service: `base_mod.Model` (attribute base, trailing id -> Model)
// - Nested: `a.b.Base` (nested attribute base, recurse -> Base)
// - Gen: `Container[str]` (subscript base, value: field -> Container)
// - Plain: `Container` (bare control, byte-identical capture)
// Runs under BOTH legs (createResolverParityIt) so the synth's bare-name text
// is asserted equal to the legacy normalizeSupertypeName reduction.
// ---------------------------------------------------------------------------
describe('Python qualified-base heritage resolution (#1951)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'python-qualified-base'), () => {});
}, 60000);
it('emits EXTENDS edges for attribute / nested-attribute / subscript / bare bases', () => {
const extends_ = getRelationships(result, 'EXTENDS');
expect(edgeSet(extends_)).toEqual([
'Gen → Container',
'Nested → Base',
'Plain → Container',
'Service → Model',
]);
});
it('emits no IMPLEMENTS edges (Python has no interfaces)', () => {
const implements_ = getRelationships(result, 'IMPLEMENTS');
expect(implements_.length).toBe(0);
});
it('all heritage edges point to real graph nodes', () => {
for (const edge of getRelationships(result, 'EXTENDS')) {
const target = result.graph.getNode(edge.rel.targetId);
expect(target).toBeDefined();
expect(target!.properties.name).toBe(edge.target);
}
});
});
// ---------------------------------------------------------------------------
// Ambiguous: Handler in two packages, relative import disambiguates
// ---------------------------------------------------------------------------

View file

@ -279,6 +279,37 @@ describe('Ruby qualified class names', () => {
});
});
// ---------------------------------------------------------------------------
// Qualified-base heritage: `class C < Outer::Super` (scope_resolution super-
// class) must emit EXTENDS at parity with the legacy @heritage leg (#1951).
// The bare control `class D < Base` keeps the original path byte-identical, and
// `include Mixin` flows through the unchanged mixin → IMPLEMENTS lane.
// ---------------------------------------------------------------------------
describe('Ruby qualified-base heritage resolution (#1951)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'ruby-qualified-base'), () => {});
}, 60000);
pit('emits EXTENDS for scoped (C < Outer::Super) and bare (D < Base) bases', () => {
const extends_ = getRelationships(result, 'EXTENDS');
const edges = edgeSet(extends_);
// Scoped superclass resolves by its trailing bare name (Outer::Super → Super).
expect(edges).toContain('C → Super');
// Bare control resolves unchanged.
expect(edges).toContain('D → Base');
});
pit('emits IMPLEMENTS for the include Mixin (unchanged mixin lane): C → Mixin', () => {
const implements_ = getRelationships(result, 'IMPLEMENTS');
const edge = implements_.find((e) => e.source === 'C' && e.target === 'Mixin');
expect(edge).toBeDefined();
expect(edge!.rel.reason).toBe('include');
});
});
// ---------------------------------------------------------------------------
// Ambiguous: Handler in two dirs, require_relative disambiguates
// ---------------------------------------------------------------------------

View file

@ -75,6 +75,98 @@ describe('Rust trait implementation resolution', () => {
});
});
// ---------------------------------------------------------------------------
// Cross-module collision (#1951 review): two `struct User` in separate modules,
// each `impl Drawable`. The legacy global last-write-wins simple-name index
// collapsed both impl sites onto ONE `User`, sourcing one (or both) edges from
// the wrong module's struct. Scope-aware resolution sources each edge from the
// `User` defined in that impl's own module, so BOTH edges are present and
// correctly sourced.
// ---------------------------------------------------------------------------
describe('Rust cross-module trait-impl collision resolution (#1951)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'rust-cross-module-collision'),
() => {},
);
}, 60000);
it('detects 2 User structs in separate modules and 1 Drawable trait', () => {
const structs: string[] = [];
result.graph.forEachNode((n) => {
if (n.label === 'Struct') structs.push(`${n.properties.name}@${n.properties.filePath}`);
});
const users = structs.filter((s) => s.startsWith('User@')).sort();
expect(users).toEqual(['User@src/a.rs', 'User@src/b.rs']);
expect(getNodesByLabel(result, 'Trait')).toEqual(['Drawable']);
});
it('emits one IMPLEMENTS edge per module, each sourced from its OWN User', () => {
const implements_ = getRelationships(result, 'IMPLEMENTS');
expect(implements_.length).toBe(2);
expect(edgeSet(implements_)).toEqual(['User → Drawable', 'User → Drawable']);
// The fix: each edge sources from the User in its own module — not a single
// last-write-wins struct. Before the fix, both edges collapsed onto one file.
const sourceFiles = implements_.map((e) => e.sourceFilePath).sort();
expect(sourceFiles).toEqual(['src/a.rs', 'src/b.rs']);
for (const edge of implements_) {
expect(edge.rel.reason).toBe('trait-impl');
expect(edge.targetFilePath).toBe('src/traits.rs');
}
});
});
// ---------------------------------------------------------------------------
// Qualified/scoped trait paths (#1956 tri-review U1): `impl crate::traits::Foo
// for S` and `impl crate::traits::Wrapped<T> for S`. The base is a
// `scoped_type_identifier` (or a generic_type wrapping one). Both the synth
// (registry leg, rust/captures.ts `bareTypeIdentifier`) and the legacy
// `@heritage` query now resolve it by its trailing bare name (KTD-1). The traits
// are unique, so both legs resolve identically — parity-tested. (Ambiguous
// scoped bases reuse the same refuse-on-ambiguity path as bare names, already
// covered by rust-cross-module-collision / rust-ambiguous; that path diverges
// across legs by design and is intentionally not added to this parity fixture.)
// ---------------------------------------------------------------------------
describe('Rust qualified/scoped trait-impl resolution (#1956 U1)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-qualified-trait'), () => {});
}, 60000);
it('detects the structs and traits', () => {
expect(getNodesByLabel(result, 'Struct')).toEqual(['Gadget', 'Widget']);
expect(getNodesByLabel(result, 'Trait')).toEqual(['Drawable', 'Wrapped']);
});
it('emits IMPLEMENTS edges for qualified and qualified-generic trait paths', () => {
const implements_ = getRelationships(result, 'IMPLEMENTS');
// `impl crate::traits::Drawable for Widget` (scoped) and
// `impl crate::traits::Wrapped<u32> for Gadget` (generic-of-scoped) both
// resolve by their trailing bare name.
expect(edgeSet(implements_)).toEqual(['Gadget → Wrapped', 'Widget → Drawable']);
for (const edge of implements_) {
expect(edge.rel.reason).toBe('trait-impl');
}
});
it('sources each edge from its struct file and targets the trait module', () => {
const implements_ = getRelationships(result, 'IMPLEMENTS');
for (const edge of implements_) {
expect(edge.sourceFilePath).toBe('src/widget.rs');
expect(edge.targetFilePath).toBe('src/traits.rs');
}
});
it('does not emit EXTENDS edges (Rust trait impls are IMPLEMENTS)', () => {
expect(getRelationships(result, 'EXTENDS').length).toBe(0);
});
});
// ---------------------------------------------------------------------------
// Ambiguous: Handler struct in two modules, crate:: import disambiguates
// ---------------------------------------------------------------------------

View file

@ -31,6 +31,60 @@ function writeFixtureRepo(root: string, files: Record<string, string>): void {
}
}
// ---------------------------------------------------------------------------
// Generic-base heritage (#1951): extends Box<T> already worked (value: identifier
// captures Base; type_args are a sibling), but `implements IFoo<T>` matched 0 in
// the legacy @heritage query while the registry synth emitted 1 — a latent =0/=1
// parity break. Widening the legacy implements clause closes it. Runs under BOTH
// legs via createResolverParityIt, so it fails on the legacy leg if it regresses.
// ---------------------------------------------------------------------------
describe('TypeScript generic-base heritage resolution (#1951)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'typescript-generic-base'), () => {});
}, 60000);
it('emits EXTENDS Service → Box for a generic superclass (extends Box<string>)', () => {
const extends_ = getRelationships(result, 'EXTENDS');
expect(edgeSet(extends_)).toEqual(['Service → Box']);
});
it('emits IMPLEMENTS Service → IFoo for a generic interface (implements IFoo<string>)', () => {
const implements_ = getRelationships(result, 'IMPLEMENTS');
expect(edgeSet(implements_)).toEqual(['Service → IFoo']);
});
});
// ---------------------------------------------------------------------------
// Qualified (namespaced) bases (#1956 tri-review U2): `extends ns.Box<string>`
// + `implements ns.IFoo<string>` (qualified-generic, on Service) and `extends
// ns.Base` + `implements ns.IBar` (qualified non-generic, on Plain). extends
// uses a member_expression value; implements uses a nested_type_identifier
// (plain) or a generic_type wrapping one. The registry-primary synth resolves
// these by their tail; the legacy @heritage query was widened to match. Runs
// under BOTH legs via createResolverParityIt.
// ---------------------------------------------------------------------------
describe('TypeScript qualified-base heritage resolution (#1956 U2)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'typescript-qualified-base'), () => {});
}, 60000);
it('emits EXTENDS for qualified and qualified-generic superclasses', () => {
const extends_ = getRelationships(result, 'EXTENDS');
expect(edgeSet(extends_)).toEqual(['Plain → Base', 'Service → Box']);
});
it('emits IMPLEMENTS for qualified and qualified-generic interfaces', () => {
const implements_ = getRelationships(result, 'IMPLEMENTS');
expect(edgeSet(implements_)).toEqual(['Plain → IBar', 'Service → IFoo']);
});
});
// ---------------------------------------------------------------------------
// Heritage: class extends + implements interface
// ---------------------------------------------------------------------------

View file

@ -146,3 +146,43 @@ describe('emitJsScopeCaptures — #1876 array-method-callback narrowing', () =>
).toBe(true);
});
});
// ---------------------------------------------------------------------------
// JSX-element-as-call-argument arity (#1956 tri-review U3): a JSX component used
// as a call argument, e.g. `render(<Foo a={1} b={2} />)`, must NOT inherit the
// enclosing call's arity. The JSX element is itself a `@reference.call.*` anchor;
// the call-arity walk-up would ascend from it into the enclosing call_expression
// and mis-attribute that call's arity. An early guard skips arity synthesis when
// the call anchor is a JSX element (restoring the pre-#1951 range-based behavior).
// ---------------------------------------------------------------------------
/** Arity text for the call-site match whose callee `@reference.name` is `name`;
* `'NONE'` when no such call-site match exists, `undefined` when it exists with
* no `@reference.arity`. */
function callArity(src: string, name: string, file = 'test.jsx'): string | undefined | 'NONE' {
const matches = emitJsScopeCaptures(src, file).filter(
(m) =>
Object.keys(m).some((k) => k.startsWith('@reference.call')) &&
m['@reference.name']?.text === name,
);
if (matches.length === 0) return 'NONE';
return matches[0]['@reference.arity']?.text;
}
describe('emitJsScopeCaptures — JSX-as-call-arg arity (#1956 U3)', () => {
it('does not attribute the enclosing call arity to a JSX component reference', () => {
const src = 'render(<Foo a={1} b={2} />);';
// The Foo JSX component ref must carry NO arity (was wrongly 1 before the fix).
expect(callArity(src, 'Foo')).toBeUndefined();
// The enclosing render() call keeps its real arity (1 argument: the element).
expect(callArity(src, 'render')).toBe('1');
});
it('keeps arity on a plain (non-JSX) call (regression guard)', () => {
expect(callArity('foo(1, 2);', 'foo', 'test.js')).toBe('2');
});
it('emits no arity for a standalone JSX element not used as a call argument', () => {
expect(callArity('const x = <Foo />;', 'Foo')).toBeUndefined();
});
});

View file

@ -0,0 +1,142 @@
/**
* Unit coverage for `resolveAmbiguousInheritanceBaseViaImports` (#1956 tri-review
* U8). The import-aware disambiguation fallback for an ambiguous (2 same-named
* class-like) inheritance base. It only commits when EXACTLY ONE candidate
* survives a tier, otherwise preserves the historical "return undefined" refusal:
*
* - guard: fewer than 2 class-like candidates undefined (not this fallback's job)
* - guard: no import edges on the module scope undefined (refuse)
* - Tier 1: exactly one candidate file is imported exactly resolve
* - Tier 1: more than one imported exactly undefined (refuse)
* - Tier 2: exactly one candidate shares a dir with an import target resolve
* - Tier 2: more than one shares a dir undefined (refuse)
*
* Drives the function directly through a minimal cast `ScopeResolutionIndexes`
* (only the accessors it reads qualifiedNames/defs/scopeTree/imports), so the
* branch behavior is pinned independent of the full finalize pipeline.
*/
import { describe, it, expect } from 'vitest';
import { resolveAmbiguousInheritanceBaseViaImports } from '../../../src/core/ingestion/scope-resolution/scope/walkers.js';
import type { ImportEdge, Scope, ScopeId, SymbolDefinition } from 'gitnexus-shared';
import type { ScopeResolutionIndexes } from '../../../src/core/ingestion/model/scope-resolution-indexes.js';
const MODULE = 'scope:module' as ScopeId;
const BASE = 'Handler';
interface Candidate {
nodeId: string;
filePath: string;
type?: string; // class-like; defaults to 'Class'
}
/** Build a minimal indexes object whose module scope imports `importTargetFiles`
* and whose `qualifiedNames` maps BASE the given class-like candidates. */
function buildIndexes(
candidates: Candidate[],
importTargetFiles: string[],
): ScopeResolutionIndexes {
const defsMap = new Map<string, SymbolDefinition>();
const ids: string[] = [];
for (const c of candidates) {
defsMap.set(c.nodeId, {
nodeId: c.nodeId,
filePath: c.filePath,
type: c.type ?? 'Class',
} as SymbolDefinition);
ids.push(c.nodeId);
}
const moduleScope = {
id: MODULE,
kind: 'Module',
parent: null,
filePath: 'ref.ts',
} as unknown as Scope;
const importEdges = importTargetFiles.map((f) => ({ targetFile: f }) as unknown as ImportEdge);
return {
qualifiedNames: { get: (n: string) => (n === BASE ? ids : []) },
defs: { get: (id: string) => defsMap.get(id) },
scopeTree: { getScope: (id: ScopeId) => (id === MODULE ? moduleScope : undefined) },
imports: new Map<ScopeId, readonly ImportEdge[]>([[MODULE, importEdges]]),
} as unknown as ScopeResolutionIndexes;
}
function resolve(candidates: Candidate[], importTargetFiles: string[]): string | undefined {
const def = resolveAmbiguousInheritanceBaseViaImports(
MODULE,
BASE,
buildIndexes(candidates, importTargetFiles),
);
return def?.nodeId;
}
describe('resolveAmbiguousInheritanceBaseViaImports (#1956 U8)', () => {
it('refuses (undefined) when there is only a single candidate (not ambiguous)', () => {
expect(
resolve([{ nodeId: 'd:models', filePath: 'Models/Handler.ts' }], ['Models/Handler.ts']),
).toBeUndefined();
});
it('refuses (undefined) when the module scope has no import edges', () => {
expect(
resolve(
[
{ nodeId: 'd:models', filePath: 'Models/Handler.ts' },
{ nodeId: 'd:other', filePath: 'Other/Handler.ts' },
],
[],
),
).toBeUndefined();
});
it('Tier 1: resolves to the single candidate whose file is imported exactly', () => {
expect(
resolve(
[
{ nodeId: 'd:models', filePath: 'Models/Handler.ts' },
{ nodeId: 'd:other', filePath: 'Other/Handler.ts' },
],
['Models/Handler.ts'],
),
).toBe('d:models');
});
it('Tier 1: refuses when more than one candidate file is imported exactly', () => {
expect(
resolve(
[
{ nodeId: 'd:models', filePath: 'Models/Handler.ts' },
{ nodeId: 'd:other', filePath: 'Other/Handler.ts' },
],
['Models/Handler.ts', 'Other/Handler.ts'],
),
).toBeUndefined();
});
it('Tier 2: resolves to the single candidate sharing a directory with an import target', () => {
// No exact file match (import target is a different file in Models/), so it
// falls to the same-directory tier — only Models/Handler.ts shares a dir.
expect(
resolve(
[
{ nodeId: 'd:models', filePath: 'Models/Handler.ts' },
{ nodeId: 'd:other', filePath: 'Other/Handler.ts' },
],
['Models/IProcessor.ts'],
),
).toBe('d:models');
});
it('Tier 2: refuses when more than one candidate shares a directory with an import target', () => {
// Two same-named candidates in the same directory; the import target is a
// third file in that directory (no exact match) — still ambiguous, refuse.
expect(
resolve(
[
{ nodeId: 'd:a', filePath: 'Models/HandlerA.ts' },
{ nodeId: 'd:b', filePath: 'Models/HandlerB.ts' },
],
['Models/Registry.ts'],
),
).toBeUndefined();
});
});

View file

@ -0,0 +1,41 @@
/**
* Focused capture-synthesis test for the Swift qualified-base fix (#1951 review).
*
* `class Derived: Outer.Inner` inherits from the NESTED base `Inner`, not the
* qualifier `Outer`. `swiftBaseTypeIdentifier` previously returned the FIRST
* `type_identifier` of the flat `user_type` (`Outer`); it now returns the LAST
* (`Inner`). This asserts the synthesized `@reference.inherits` site carries the
* trailing segment, directly at the changed path independent of downstream
* resolution (a bare nested-type name does not resolve to an edge in the current
* model, so the integration resolver test cannot observe it).
*/
import { describe, it, expect } from 'vitest';
import { emitSwiftScopeCaptures } from '../../../../src/core/ingestion/languages/swift/index.js';
import { isLanguageAvailable } from '../../../../src/core/tree-sitter/parser-loader.js';
import { SupportedLanguages } from '../../../../src/config/supported-languages.js';
const swiftAvailable = isLanguageAvailable(SupportedLanguages.Swift);
function inheritedBaseNames(src: string): string[] {
return emitSwiftScopeCaptures(src, 'Probe.swift')
.filter((m) => m['@reference.inherits'] !== undefined)
.map((m) => m['@reference.name']?.text ?? '');
}
describe.skipIf(!swiftAvailable)('Swift qualified-base capture synthesis (#1951)', () => {
it('extracts the trailing segment Inner from a qualified base Outer.Inner', () => {
expect(inheritedBaseNames('class Derived: Outer.Inner {}\n')).toEqual(['Inner']);
});
it('extracts the trailing segment from a qualified generic base Outer.Inner<T>', () => {
expect(inheritedBaseNames('class Derived: Outer.Inner<String> {}\n')).toEqual(['Inner']);
});
it('leaves a non-qualified base unchanged (no regression)', () => {
expect(inheritedBaseNames('class Child: Parent {}\n')).toEqual(['Parent']);
});
it('leaves a non-qualified generic base unchanged (Box<Int> -> Box)', () => {
expect(inheritedBaseNames('class Boxed: Box<Int> {}\n')).toEqual(['Box']);
});
});

View file

@ -89,6 +89,28 @@ describe('captures.ts ancestor-walk rewrite (U8 / B5)', () => {
expect('@declaration.parameter-count' in jsxCalls[0]).toBe(false);
});
it('JSX as a call argument does not inherit the enclosing call arity (#1956 U3)', () => {
// `render(<Foo a={1} b={2} />)`: the JSX element is itself a
// @reference.call.free anchor nested INSIDE the render() call_expression.
// The arity walk-up (findSelfOrAncestorOfTypes) would climb from the JSX
// element into render() and stamp arity 1 onto the Foo component ref. The
// early JSX-anchor guard prevents that; the enclosing render() call still
// gets its real arity (1 argument: the element).
const matches = emitTsScopeCaptures(
'function App() { return render(<Foo a={1} b={2} />); }',
'test.tsx',
);
const fooJsx = matches.find(
(m) => '@reference.call.free' in m && m['@reference.name']?.text === 'Foo',
);
const renderCall = matches.find(
(m) => '@reference.call.free' in m && m['@reference.name']?.text === 'render',
);
expect(fooJsx).toBeDefined();
expect('@reference.arity' in fooJsx!).toBe(false);
expect(renderCall?.['@reference.arity']?.text).toBe('1');
});
it('constructor call `new Foo(1, 2)` emits exactly one @reference.call.constructor capture', () => {
// new_expression anchor → self in ancestor walk.
const count = countMatches(

View file

@ -89,7 +89,13 @@ describe('sequential native parser availability', () => {
}
});
it('skips Swift files in processCalls when the native parser is unavailable', async () => {
it('skips Swift files in processCalls (registry-primary: scope-resolution owns call resolution)', async () => {
// Swift is registry-primary, so processCalls skips it via the
// isRegistryPrimary gate (call-processor.ts) BEFORE the parser-availability
// check — the registry-primary scope-resolution path owns its call edges
// (#1951). The unavailable-parser mock is therefore moot: the file is skipped
// (no loadLanguage) regardless. The legacy availability-skip path itself is
// exercised by the Dart verbose test below (Dart is not registry-primary).
vi.mocked(parserLoader.isLanguageAvailable).mockReturnValue(false);
await expect(
@ -107,19 +113,18 @@ describe('sequential native parser availability', () => {
it('warns when processCalls skips files in verbose mode', async () => {
cap = _captureLogger();
const previous = process.env.GITNEXUS_VERBOSE;
// Swift is now registry-primary (MIGRATED_LANGUAGES), and
// call-processor gates registry-primary languages before the skip
// counter — so force the legacy path off here to exercise the
// skip/warn branch. (We do NOT edit the processor.)
const previousFlag = process.env.REGISTRY_PRIMARY_SWIFT;
process.env.GITNEXUS_VERBOSE = '1';
process.env.REGISTRY_PRIMARY_SWIFT = '0';
try {
vi.mocked(parserLoader.isLanguageAvailable).mockReturnValue(false);
// Use Dart, a non-registry-primary language. call-processor gates
// registry-primary languages (Swift, etc.) via the isRegistryPrimary
// gate before the parser-availability skip counter, so a Dart file
// exercises the skip/warn branch without forcing any language out of
// registry-primary mode (Swift must stay scope-based).
await processCalls(
createKnowledgeGraph(),
[{ path: 'App.swift', content: 'func demo() {}' }],
[{ path: 'App.dart', content: 'void demo() {}' }],
createASTCache(),
createResolutionContext(),
);
@ -130,7 +135,7 @@ describe('sequential native parser availability', () => {
.some(
(r) =>
r.msg ===
'[ingestion] Skipped 1 swift file(s) in call processing — swift parser not available.',
'[ingestion] Skipped 1 dart file(s) in call processing — dart parser not available.',
),
).toBe(true);
} finally {
@ -139,15 +144,16 @@ describe('sequential native parser availability', () => {
} else {
process.env.GITNEXUS_VERBOSE = previous;
}
if (previousFlag === undefined) {
delete process.env.REGISTRY_PRIMARY_SWIFT;
} else {
process.env.REGISTRY_PRIMARY_SWIFT = previousFlag;
}
}
});
it('skips Swift files in processHeritage when the native parser is unavailable', async () => {
it('skips Swift files in processHeritage (registry-primary: scope-resolution owns heritage)', async () => {
// Swift is registry-primary, so processHeritage skips it via the
// isRegistryPrimary gate (heritage-processor.ts) BEFORE the parser-availability
// check — scope-resolution (#1951) owns its EXTENDS/IMPLEMENTS edges. The
// unavailable-parser mock is therefore moot: the file is skipped (no
// loadLanguage) regardless. The legacy availability-skip path itself is
// exercised by the Dart verbose test below (Dart is not registry-primary).
vi.mocked(parserLoader.isLanguageAvailable).mockReturnValue(false);
await expect(
@ -169,9 +175,15 @@ describe('sequential native parser availability', () => {
try {
vi.mocked(parserLoader.isLanguageAvailable).mockReturnValue(false);
// Use Dart, a non-registry-primary language. processHeritage skips
// registry-primary languages (Swift, etc.) via the isRegistryPrimary gate
// — scope-based resolution owns their inheritance (#1951) — BEFORE the
// legacy parser-availability skip this test exercises. Dart still flows
// through the legacy heritage path, so the skip/warn branch fires without
// forcing any language out of registry-primary mode.
await processHeritage(
createKnowledgeGraph(),
[{ path: 'App.swift', content: 'class AppViewController: UIViewController {}' }],
[{ path: 'App.dart', content: 'class Widget extends StatelessWidget {}' }],
createASTCache(),
createResolutionContext(),
);
@ -182,7 +194,7 @@ describe('sequential native parser availability', () => {
.some(
(r) =>
r.msg ===
'[ingestion] Skipped 1 swift file(s) in heritage processing — swift parser not available.',
'[ingestion] Skipped 1 dart file(s) in heritage processing — dart parser not available.',
),
).toBe(true);
} finally {