mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-12 23:02:45 +00:00
* feat(scope-resolution): migrate Dart to registry-primary call resolution (#939) Add a Dart scope-resolution module (languages/dart/) mirroring the Swift template and flip Dart to registry-primary. Resolution edges (CALLS/IMPORTS/ACCESSES/EXTENDS/IMPLEMENTS/METHOD_IMPLEMENTS) now route through the shared registry pipeline with byte-for-byte parity against the legacy DAG: test/integration/resolvers/dart.test.ts passes 53/53 under both REGISTRY_PRIMARY_DART=0 and =1 (scripts/run-parity.ts --language dart: 2/2). Dart-specific handling: - Function scopes are synthesized to span signature..body (tree-sitter function_signature/function_body are siblings, not parent/child). - extends rides @reference.inherits (EXTENDS via the generic pre-pass); implements/with are carried as __heritage__ side-effect imports and emitted as IMPLEMENTS, since Dart `implements <class>` must be IMPLEMENTS regardless of the target's symbol kind. - imports are wildcard (whole-library) with expandsWildcardTo so imported return types propagate cross-file (var u = getUser(); u.save()). - getInnerSignature now self-returns a bare signature node so top-level function params/return/name extract (legacy-safe: legacy only ever passes method_signature/declaration wrappers). Also: add Dart scope-capture bench coverage (linear ~0.99 scaling); update two tests that used Dart as a non-migrated control (Vue / forced legacy). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(scope-resolution): close Dart registry-primary parity gaps from review Adversarial review of #1970 surfaced real divergences from the legacy DAG on constructs the 10 fixtures don't exercise. All fixed; parity gate still 2/2 (now 55/55 each mode): - Implicit-constructor construction (`Foo()` with no explicit ctor): the legacy DAG emits `caller -> Foo` (Class) but registry emitted nothing (callee tagged @reference.call.free never reaches constructorCallTargetsClass). Re-tag UpperCamelCase free-callees to @reference.call.constructor (Dart types are UpperCamelCase) so they link to the Class. Locked in with a regression fixture + test that passes in BOTH modes. - Cascade calls (`list..add(1)..sort()`) were dropped — cascade_section has no `selector` wrapper, so the reference walk never saw them while legacy emitted them as free calls. Add a cascade_section handler. - BUILT_INS (setState/then/push/pop/listen/...) were not suppressed on the registry path, so a user symbol shadowing one produced a spurious CALLS edge the legacy DAG suppresses. Skip built-in-named call refs at capture time (extract the set to a leaf module shared with the provider). - Enhanced-enum methods mis-parented to Module (no enum scope). Add `(enum_declaration) @scope.class` so enum members are owned by the enum. Re-baseline the Dart scope-capture fingerprint (linear ~0.95). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(scope-resolution): apply issue #1926 F24/F25 findings to the Dart scope path Issue #1926 catalogs Dart parsing-layer coverage gaps. Apply the two that the registry-primary scope-resolution path owns (call edges + call attribution), registered as legacy-expected-failures since they are scope-resolver-only wins. - F24: the scope path's unified tree-walk already captures member calls (obj.method()) in return / list-literal / named-argument / arrow-body contexts — the legacy DAG only captures them under expression_statement / initialized_variable_definition. Lock it with the dart-member-call-contexts fixture + tests. - F25 (constructor portion): a constructor's body is a sibling of the WRAPPING method_signature (class_body > method_signature > constructor_signature, then function_body), so findFunctionBody now walks up to the method_signature wrapper. Constructor bodies get a Function scope and their body-calls attribute to the Constructor (a valid caller anchor) instead of the class. Add the dart-constructor-body fixture + test. Switch dart.test.ts to createResolverParityIt('dart') and add the dart entry to LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES (5 wins). Both modes pass: run-parity --language dart → 2/2 (registry 60/60; legacy 55 pass + 5 skipped). Not applicable to the scope path (structure-phase / shared-pipeline, tracked by #1926's legacy fix): F25 getter/setter (Property is not a caller anchor) and operator (no Method node emitted by the structure phase) bodies; F26 (static field Property nodes); F27 (no generic_type reference in the scope module); F28/F29 (typedef/variable node extraction). Re-baseline the Dart scope-capture fingerprint (linear ~1.0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(scope-resolution): fix Dart named-constructor file-drop + container-name mis-binding (tri-review) Multi-engine tri-review (GitNexus + CE personas + Codex gpt-5.5) of #1970 found a P0 the parity gate missed plus a P2 wrong-edge: - P0 (file drop): a named constructor with a body (`class A { A.named() {…} }`, idiomatic Dart) parses as ONE constructor_signature carrying multiple `name:` fields, so the scope query matched it more than once and synthesized two identical-range @scope.function captures → ScopeTreeInvariantError(duplicate- scope-id) → extractParsedFile swallowed it → the WHOLE file was dropped from registry-primary resolution (CALLS=0 vs legacy CALLS=2). Introduced by the #1926 F25 findFunctionBody change that started giving constructors body scopes. Fix: dedup function-like declarations by their statement node so each is emitted once. Add dart-named-constructor-body fixture + a parity guard test (both modes) that fails if the file is dropped, plus the named-ctor F25 attribution win (registry-only). - P2 (wrong edge): normalizeDartType's Future<X>/List<X> unwrap is unreachable (generic args are stripped upstream to a bare `Future`/`List`), so a return/ field type binding to the bare container name let a same-named user class (`class Stream {…}`) capture the receiver — a wrong CALLS edge legacy didn't emit. Suppress type bindings that normalize to a bare container name (leaving the call unresolved, matching legacy) instead of binding to the container. Both modes still pass: run-parity --language dart → 2/2 (registry 62/62; legacy 56 + 6 skipped). Re-baseline the Dart scope-capture fingerprint. Also: refresh the captures.ts module doc (constructors get scopes; cascade calls). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(scope-resolution): address Dart tri-review follow-ups (heritage collision + polish) - P2 heritage cross-file name collision: emitDartHeritageEdges resolved both child and base by a global last-write-wins simple-name map, so two files each declaring `class Logger` (one `implements Logger`) produced a wrong-file IMPLEMENTS edge. Resolve with same-file affinity (prefer a same-file class, then a workspace-unique match, else refuse to guess) — the #1951 file-affinity pattern. Add dart-heritage-name-collision fixture + a parity test (both modes resolve same-file). Also reason-qualify the dedup key so `implements X` + `with X` keep distinct edges. - Polish: buildDartMro uses Sets instead of Array.includes-in-loop; merge-bindings uses named tier constants matching swift; drop the dead no-op stripQuotes in import-target (targetRaw already arrives quote-stripped). Both modes pass: run-parity --language dart → 2/2 (registry 63/63; legacy 57 + 6 skipped). Re-baseline the Dart scope-capture fingerprint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
269 lines
9.4 KiB
TypeScript
269 lines
9.4 KiB
TypeScript
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
|
|
vi.mock('../../src/core/tree-sitter/parser-loader.js', () => ({
|
|
loadParser: vi.fn(async () => ({
|
|
parse: vi.fn(),
|
|
getLanguage: vi.fn(),
|
|
})),
|
|
loadLanguage: vi.fn(async () => undefined),
|
|
isLanguageAvailable: vi.fn(() => true),
|
|
}));
|
|
|
|
import { createKnowledgeGraph } from '../../src/core/graph/graph.js';
|
|
import { createASTCache } from '../../src/core/ingestion/ast-cache.js';
|
|
import { processParsing } from '../../src/core/ingestion/parsing-processor.js';
|
|
import { processImports } from '../../src/core/ingestion/import-processor.js';
|
|
import { processCalls } from '../../src/core/ingestion/call-processor.js';
|
|
import { processHeritage } from '../../src/core/ingestion/heritage-processor.js';
|
|
import { createSymbolTable } from '../../src/core/ingestion/model/symbol-table.js';
|
|
import { createResolutionContext } from '../../src/core/ingestion/model/resolution-context.js';
|
|
import * as parserLoader from '../../src/core/tree-sitter/parser-loader.js';
|
|
|
|
import { _captureLogger } from '../../src/core/logger.js';
|
|
import type { LoggerCapture } from '../../src/core/logger.js';
|
|
describe('sequential native parser availability', () => {
|
|
// Hoisted so a stray live capture from a failed warn test can always be
|
|
// torn down in afterEach — otherwise a single assertion failure cascades
|
|
// into `_captureLogger: a previous capture is still active` (logger.ts).
|
|
let cap: LoggerCapture | undefined;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cap?.restore();
|
|
cap = undefined;
|
|
});
|
|
|
|
it('skips Swift files in processImports when the native parser is unavailable', async () => {
|
|
vi.mocked(parserLoader.isLanguageAvailable).mockReturnValue(false);
|
|
|
|
await expect(
|
|
processImports(
|
|
createKnowledgeGraph(),
|
|
[{ path: 'App.swift', content: 'import Foundation' }],
|
|
createASTCache(),
|
|
createResolutionContext(),
|
|
undefined,
|
|
'/tmp/repo',
|
|
['App.swift'],
|
|
),
|
|
).resolves.toBeUndefined();
|
|
|
|
expect(parserLoader.loadLanguage).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('warns when processImports skips files in verbose mode', async () => {
|
|
cap = _captureLogger();
|
|
const previous = process.env.GITNEXUS_VERBOSE;
|
|
process.env.GITNEXUS_VERBOSE = '1';
|
|
try {
|
|
vi.mocked(parserLoader.isLanguageAvailable).mockReturnValue(false);
|
|
|
|
await processImports(
|
|
createKnowledgeGraph(),
|
|
[{ path: 'App.swift', content: 'import Foundation' }],
|
|
createASTCache(),
|
|
createResolutionContext(),
|
|
undefined,
|
|
'/tmp/repo',
|
|
['App.swift'],
|
|
);
|
|
|
|
expect(
|
|
cap
|
|
.records()
|
|
.some(
|
|
(r) =>
|
|
r.msg ===
|
|
'[ingestion] Skipped 1 swift file(s) in import processing — swift parser not available.',
|
|
),
|
|
).toBe(true);
|
|
} finally {
|
|
if (previous === undefined) {
|
|
delete process.env.GITNEXUS_VERBOSE;
|
|
} else {
|
|
process.env.GITNEXUS_VERBOSE = previous;
|
|
}
|
|
}
|
|
});
|
|
|
|
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(
|
|
processCalls(
|
|
createKnowledgeGraph(),
|
|
[{ path: 'App.swift', content: 'func demo() {}' }],
|
|
createASTCache(),
|
|
createResolutionContext(),
|
|
),
|
|
).resolves.toEqual([]);
|
|
|
|
expect(parserLoader.loadLanguage).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('warns when processCalls skips files in verbose mode', async () => {
|
|
cap = _captureLogger();
|
|
const previous = process.env.GITNEXUS_VERBOSE;
|
|
const previousDart = process.env.REGISTRY_PRIMARY_DART;
|
|
process.env.GITNEXUS_VERBOSE = '1';
|
|
// call-processor gates registry-primary languages (Swift, Dart, etc.) via
|
|
// the isRegistryPrimary gate BEFORE the parser-availability skip counter.
|
|
// Dart is now in MIGRATED_LANGUAGES, so force it onto the legacy call path
|
|
// (REGISTRY_PRIMARY_DART=0) to exercise the skip/warn branch this test
|
|
// covers — without disturbing any other language's mode.
|
|
process.env.REGISTRY_PRIMARY_DART = '0';
|
|
try {
|
|
vi.mocked(parserLoader.isLanguageAvailable).mockReturnValue(false);
|
|
|
|
await processCalls(
|
|
createKnowledgeGraph(),
|
|
[{ path: 'App.dart', content: 'void demo() {}' }],
|
|
createASTCache(),
|
|
createResolutionContext(),
|
|
);
|
|
|
|
expect(
|
|
cap
|
|
.records()
|
|
.some(
|
|
(r) =>
|
|
r.msg ===
|
|
'[ingestion] Skipped 1 dart file(s) in call processing — dart parser not available.',
|
|
),
|
|
).toBe(true);
|
|
} finally {
|
|
if (previous === undefined) {
|
|
delete process.env.GITNEXUS_VERBOSE;
|
|
} else {
|
|
process.env.GITNEXUS_VERBOSE = previous;
|
|
}
|
|
if (previousDart === undefined) {
|
|
delete process.env.REGISTRY_PRIMARY_DART;
|
|
} else {
|
|
process.env.REGISTRY_PRIMARY_DART = previousDart;
|
|
}
|
|
}
|
|
});
|
|
|
|
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(
|
|
processHeritage(
|
|
createKnowledgeGraph(),
|
|
[{ path: 'App.swift', content: 'class AppViewController: UIViewController {}' }],
|
|
createASTCache(),
|
|
createResolutionContext(),
|
|
),
|
|
).resolves.toBeUndefined();
|
|
|
|
expect(parserLoader.loadLanguage).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('warns when processHeritage skips files in verbose mode', async () => {
|
|
cap = _captureLogger();
|
|
const previous = process.env.GITNEXUS_VERBOSE;
|
|
const previousDart = process.env.REGISTRY_PRIMARY_DART;
|
|
process.env.GITNEXUS_VERBOSE = '1';
|
|
// processHeritage skips registry-primary languages (Swift, Dart, etc.) via
|
|
// the isRegistryPrimary gate — scope-based resolution owns their
|
|
// inheritance (#1951) — BEFORE the legacy parser-availability skip this
|
|
// test exercises. Dart is now in MIGRATED_LANGUAGES, so force it onto the
|
|
// legacy heritage path (REGISTRY_PRIMARY_DART=0) to fire the skip/warn
|
|
// branch, without disturbing any other language's mode.
|
|
process.env.REGISTRY_PRIMARY_DART = '0';
|
|
try {
|
|
vi.mocked(parserLoader.isLanguageAvailable).mockReturnValue(false);
|
|
|
|
await processHeritage(
|
|
createKnowledgeGraph(),
|
|
[{ path: 'App.dart', content: 'class Widget extends StatelessWidget {}' }],
|
|
createASTCache(),
|
|
createResolutionContext(),
|
|
);
|
|
|
|
expect(
|
|
cap
|
|
.records()
|
|
.some(
|
|
(r) =>
|
|
r.msg ===
|
|
'[ingestion] Skipped 1 dart file(s) in heritage processing — dart parser not available.',
|
|
),
|
|
).toBe(true);
|
|
} finally {
|
|
if (previous === undefined) {
|
|
delete process.env.GITNEXUS_VERBOSE;
|
|
} else {
|
|
process.env.GITNEXUS_VERBOSE = previous;
|
|
}
|
|
if (previousDart === undefined) {
|
|
delete process.env.REGISTRY_PRIMARY_DART;
|
|
} else {
|
|
process.env.REGISTRY_PRIMARY_DART = previousDart;
|
|
}
|
|
}
|
|
});
|
|
|
|
it('skips Swift files in processParsing when the native parser is unavailable', async () => {
|
|
vi.mocked(parserLoader.isLanguageAvailable).mockReturnValue(false);
|
|
|
|
await expect(
|
|
processParsing(
|
|
createKnowledgeGraph(),
|
|
[{ path: 'App.swift', content: 'class AppViewController: UIViewController {}' }],
|
|
createSymbolTable(),
|
|
createASTCache(),
|
|
),
|
|
).resolves.toBeNull();
|
|
|
|
expect(parserLoader.loadLanguage).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('warns when processParsing skips files in verbose mode', async () => {
|
|
cap = _captureLogger();
|
|
const previous = process.env.GITNEXUS_VERBOSE;
|
|
process.env.GITNEXUS_VERBOSE = '1';
|
|
try {
|
|
vi.mocked(parserLoader.isLanguageAvailable).mockReturnValue(false);
|
|
|
|
await processParsing(
|
|
createKnowledgeGraph(),
|
|
[{ path: 'App.swift', content: 'class AppViewController: UIViewController {}' }],
|
|
createSymbolTable(),
|
|
createASTCache(),
|
|
);
|
|
|
|
expect(
|
|
cap
|
|
.records()
|
|
.some(
|
|
(r) =>
|
|
r.msg ===
|
|
'[ingestion] Skipped 1 swift file(s) in parsing processing — swift parser not available.',
|
|
),
|
|
).toBe(true);
|
|
} finally {
|
|
if (previous === undefined) {
|
|
delete process.env.GITNEXUS_VERBOSE;
|
|
} else {
|
|
process.env.GITNEXUS_VERBOSE = previous;
|
|
}
|
|
}
|
|
});
|
|
});
|