mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
fix(ingestion): migrate all languages' inheritance to scope-resolution on the worker path (#1951) (#1956)
This commit is contained in:
parent
fca30c7e26
commit
0fc0211d26
100 changed files with 3788 additions and 398 deletions
|
|
@ -1 +1 @@
|
|||
f2b4376f30dab76f3befc9cbd3d7cc2bf1afbd7329a5e953439083e005de4a7c
|
||||
9803b81f0c3738ecd276aba187436482be47b5f5f62e5e85a983524129713b7d
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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)."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 ──────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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+)?/, '');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
28
gitnexus/src/core/ingestion/languages/swift/base-type.ts
Normal file
28
gitnexus/src/core/ingestion/languages/swift/base-type.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
7
gitnexus/test/fixtures/lang-resolution/csharp-primary-ctor-heritage/src/BaseEntity.cs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/csharp-primary-ctor-heritage/src/BaseEntity.cs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace App
|
||||
{
|
||||
public class BaseEntity
|
||||
{
|
||||
public int Id { get; set; }
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/csharp-primary-ctor-heritage/src/IFoo.cs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/csharp-primary-ctor-heritage/src/IFoo.cs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace App
|
||||
{
|
||||
public interface IFoo
|
||||
{
|
||||
void Foo();
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/csharp-primary-ctor-heritage/src/Repo.cs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/csharp-primary-ctor-heritage/src/Repo.cs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace App
|
||||
{
|
||||
public class Repo<T>
|
||||
{
|
||||
public T Value { get; set; }
|
||||
}
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/csharp-primary-ctor-heritage/src/Service.cs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/csharp-primary-ctor-heritage/src/Service.cs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace App
|
||||
{
|
||||
// Fully-qualified GENERIC base — exercises qualified+generic name normalization.
|
||||
public class Service : App.Repo<int>
|
||||
{
|
||||
}
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/csharp-primary-ctor-heritage/src/User.cs
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/csharp-primary-ctor-heritage/src/User.cs
vendored
Normal 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() { }
|
||||
}
|
||||
}
|
||||
20
gitnexus/test/fixtures/lang-resolution/csharp-qualified-base/src/Domain.cs
vendored
Normal file
20
gitnexus/test/fixtures/lang-resolution/csharp-qualified-base/src/Domain.cs
vendored
Normal 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();
|
||||
}
|
||||
}
|
||||
47
gitnexus/test/fixtures/lang-resolution/csharp-qualified-base/src/Shapes.cs
vendored
Normal file
47
gitnexus/test/fixtures/lang-resolution/csharp-qualified-base/src/Shapes.cs
vendored
Normal 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
|
||||
{
|
||||
}
|
||||
}
|
||||
26
gitnexus/test/fixtures/lang-resolution/go-qualified-base/base/base.go
vendored
Normal file
26
gitnexus/test/fixtures/lang-resolution/go-qualified-base/base/base.go
vendored
Normal 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)
|
||||
}
|
||||
30
gitnexus/test/fixtures/lang-resolution/go-qualified-base/consumers/local.go
vendored
Normal file
30
gitnexus/test/fixtures/lang-resolution/go-qualified-base/consumers/local.go
vendored
Normal 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
|
||||
}
|
||||
30
gitnexus/test/fixtures/lang-resolution/go-qualified-base/consumers/qualified.go
vendored
Normal file
30
gitnexus/test/fixtures/lang-resolution/go-qualified-base/consumers/qualified.go
vendored
Normal 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
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/go-qualified-base/go.mod
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/go-qualified-base/go.mod
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module example.com/app
|
||||
|
||||
go 1.21
|
||||
5
gitnexus/test/fixtures/lang-resolution/java-generic-base/src/app/Box.java
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/java-generic-base/src/app/Box.java
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package app;
|
||||
|
||||
public class Box<T> {
|
||||
public T get() { return null; }
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/java-generic-base/src/app/IFoo.java
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/java-generic-base/src/app/IFoo.java
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package app;
|
||||
|
||||
public interface IFoo<T> {
|
||||
void foo(T t);
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/java-generic-base/src/app/Service.java
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/java-generic-base/src/app/Service.java
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package app;
|
||||
|
||||
public class Service extends Box<String> implements IFoo<String> {
|
||||
public void foo(String t) {}
|
||||
}
|
||||
13
gitnexus/test/fixtures/lang-resolution/java-iface-extends/src/app/IA.java
vendored
Normal file
13
gitnexus/test/fixtures/lang-resolution/java-iface-extends/src/app/IA.java
vendored
Normal 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();
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/java-iface-extends/src/app/IB.java
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/java-iface-extends/src/app/IB.java
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package app;
|
||||
|
||||
public interface IB {
|
||||
void b();
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/java-iface-extends/src/app/IC.java
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/java-iface-extends/src/app/IC.java
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package app;
|
||||
|
||||
public interface IC<T> {
|
||||
void c(T t);
|
||||
}
|
||||
10
gitnexus/test/fixtures/lang-resolution/java-qualified-base/src/app/Plain.java
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/java-qualified-base/src/app/Plain.java
vendored
Normal 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() {}
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/java-qualified-base/src/app/Service.java
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/java-qualified-base/src/app/Service.java
vendored
Normal 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) {}
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/java-qualified-base/src/app/Two.java
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/java-qualified-base/src/app/Two.java
vendored
Normal 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) {}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/java-qualified-base/src/app/base/Base.java
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/java-qualified-base/src/app/base/Base.java
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package app.base;
|
||||
|
||||
public class Base {
|
||||
public void base() {}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/java-qualified-base/src/app/base/Box.java
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/java-qualified-base/src/app/base/Box.java
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package app.base;
|
||||
|
||||
public class Box<T> {
|
||||
public T get() { return null; }
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/java-qualified-base/src/app/base/IBar.java
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/java-qualified-base/src/app/base/IBar.java
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package app.base;
|
||||
|
||||
public interface IBar {
|
||||
void bar();
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/java-qualified-base/src/app/base/IFoo.java
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/java-qualified-base/src/app/base/IFoo.java
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package app.base;
|
||||
|
||||
public interface IFoo<T> {
|
||||
void foo(T t);
|
||||
}
|
||||
21
gitnexus/test/fixtures/lang-resolution/javascript-qualified-base/src/Service.js
vendored
Normal file
21
gitnexus/test/fixtures/lang-resolution/javascript-qualified-base/src/Service.js
vendored
Normal 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';
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/javascript-qualified-base/src/base.js
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/javascript-qualified-base/src/base.js
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export class Base {
|
||||
base() {
|
||||
return 'base';
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/kotlin-qualified-base/src/Base.kt
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/kotlin-qualified-base/src/Base.kt
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package models
|
||||
|
||||
open class Base {
|
||||
fun base() {}
|
||||
}
|
||||
12
gitnexus/test/fixtures/lang-resolution/kotlin-qualified-base/src/F.kt
vendored
Normal file
12
gitnexus/test/fixtures/lang-resolution/kotlin-qualified-base/src/F.kt
vendored
Normal 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() {}
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/kotlin-qualified-base/src/G.kt
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/kotlin-qualified-base/src/G.kt
vendored
Normal 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() {}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/kotlin-qualified-base/src/Iface.kt
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/kotlin-qualified-base/src/Iface.kt
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package models
|
||||
|
||||
interface Iface {
|
||||
fun handle(): String
|
||||
}
|
||||
0
gitnexus/test/fixtures/lang-resolution/python-qualified-base/a/__init__.py
vendored
Normal file
0
gitnexus/test/fixtures/lang-resolution/python-qualified-base/a/__init__.py
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/python-qualified-base/a/b.py
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/python-qualified-base/a/b.py
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
class Base:
|
||||
def base(self) -> None:
|
||||
pass
|
||||
8
gitnexus/test/fixtures/lang-resolution/python-qualified-base/base_mod.py
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/python-qualified-base/base_mod.py
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
class Model:
|
||||
def save(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class Container:
|
||||
def get(self):
|
||||
return None
|
||||
29
gitnexus/test/fixtures/lang-resolution/python-qualified-base/service.py
vendored
Normal file
29
gitnexus/test/fixtures/lang-resolution/python-qualified-base/service.py
vendored
Normal 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
|
||||
24
gitnexus/test/fixtures/lang-resolution/ruby-qualified-base/lib/derived.rb
vendored
Normal file
24
gitnexus/test/fixtures/lang-resolution/ruby-qualified-base/lib/derived.rb
vendored
Normal 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
|
||||
24
gitnexus/test/fixtures/lang-resolution/ruby-qualified-base/lib/outer.rb
vendored
Normal file
24
gitnexus/test/fixtures/lang-resolution/ruby-qualified-base/lib/outer.rb
vendored
Normal 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
|
||||
9
gitnexus/test/fixtures/lang-resolution/rust-cross-module-collision/src/a.rs
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/rust-cross-module-collision/src/a.rs
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
use crate::traits::Drawable;
|
||||
|
||||
pub struct User {
|
||||
pub id: u32,
|
||||
}
|
||||
|
||||
impl Drawable for User {
|
||||
fn draw(&self) {}
|
||||
}
|
||||
9
gitnexus/test/fixtures/lang-resolution/rust-cross-module-collision/src/b.rs
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/rust-cross-module-collision/src/b.rs
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
use crate::traits::Drawable;
|
||||
|
||||
pub struct User {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Drawable for User {
|
||||
fn draw(&self) {}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/rust-cross-module-collision/src/main.rs
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/rust-cross-module-collision/src/main.rs
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
mod traits;
|
||||
mod a;
|
||||
mod b;
|
||||
|
||||
fn main() {}
|
||||
3
gitnexus/test/fixtures/lang-resolution/rust-cross-module-collision/src/traits.rs
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/rust-cross-module-collision/src/traits.rs
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub trait Drawable {
|
||||
fn draw(&self);
|
||||
}
|
||||
4
gitnexus/test/fixtures/lang-resolution/rust-qualified-trait/src/main.rs
vendored
Normal file
4
gitnexus/test/fixtures/lang-resolution/rust-qualified-trait/src/main.rs
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
mod traits;
|
||||
mod widget;
|
||||
|
||||
fn main() {}
|
||||
7
gitnexus/test/fixtures/lang-resolution/rust-qualified-trait/src/traits.rs
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/rust-qualified-trait/src/traits.rs
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
pub trait Drawable {
|
||||
fn draw(&self);
|
||||
}
|
||||
|
||||
pub trait Wrapped<T> {
|
||||
fn wrap(&self) -> T;
|
||||
}
|
||||
27
gitnexus/test/fixtures/lang-resolution/rust-qualified-trait/src/widget.rs
vendored
Normal file
27
gitnexus/test/fixtures/lang-resolution/rust-qualified-trait/src/widget.rs
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
2
gitnexus/test/fixtures/lang-resolution/swift-qualified-base/Sources/Derived.swift
vendored
Normal file
2
gitnexus/test/fixtures/lang-resolution/swift-qualified-base/Sources/Derived.swift
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
class Derived: Outer.Inner {
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/swift-qualified-base/Sources/Outer.swift
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/swift-qualified-base/Sources/Outer.swift
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
class Outer {
|
||||
class Inner {
|
||||
func ping() -> String {
|
||||
return "inner"
|
||||
}
|
||||
}
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/typescript-generic-base/src/Box.ts
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/typescript-generic-base/src/Box.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export class Box<T> {
|
||||
get(): T {
|
||||
return undefined as unknown as T;
|
||||
}
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/typescript-generic-base/src/IFoo.ts
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/typescript-generic-base/src/IFoo.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export interface IFoo<T> {
|
||||
foo(t: T): void;
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/typescript-generic-base/src/Service.ts
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/typescript-generic-base/src/Service.ts
vendored
Normal 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 {}
|
||||
}
|
||||
15
gitnexus/test/fixtures/lang-resolution/typescript-qualified-base/src/Service.ts
vendored
Normal file
15
gitnexus/test/fixtures/lang-resolution/typescript-qualified-base/src/Service.ts
vendored
Normal 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 {}
|
||||
}
|
||||
17
gitnexus/test/fixtures/lang-resolution/typescript-qualified-base/src/base.ts
vendored
Normal file
17
gitnexus/test/fixtures/lang-resolution/typescript-qualified-base/src/base.ts
vendored
Normal 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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
297
gitnexus/test/integration/heritage-worker-path.test.ts
Normal file
297
gitnexus/test/integration/heritage-worker-path.test.ts
Normal 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]);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue