mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-08 03:08:13 +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>
178 lines
8.2 KiB
TypeScript
178 lines
8.2 KiB
TypeScript
/**
|
|
* Unit tests for `registry-primary-flag` (RFC #909 Ring 2 PKG #924).
|
|
*
|
|
* Flag is `REGISTRY_PRIMARY_<UPPER(lang)>`. Each test manipulates
|
|
* `process.env` directly and restores it in `afterEach` — there is no
|
|
* per-process cache to invalidate, so isolation is lexical.
|
|
*/
|
|
|
|
import { describe, it, expect, afterEach, beforeEach } from 'vitest';
|
|
import { SupportedLanguages } from 'gitnexus-shared';
|
|
import {
|
|
envVarNameFor,
|
|
isRegistryPrimary,
|
|
primaryLanguages,
|
|
MIGRATED_LANGUAGES,
|
|
} from '../../src/core/ingestion/registry-primary-flag.js';
|
|
|
|
// ─── Test isolation ─────────────────────────────────────────────────────────
|
|
//
|
|
// Scrub every `REGISTRY_PRIMARY_*` env var before + after each test so
|
|
// parallel vitest runs on the same process don't bleed state.
|
|
|
|
function clearAllRegistryPrimaryVars(): void {
|
|
for (const key of Object.keys(process.env)) {
|
|
if (key.startsWith('REGISTRY_PRIMARY_')) delete process.env[key];
|
|
}
|
|
}
|
|
|
|
beforeEach(clearAllRegistryPrimaryVars);
|
|
afterEach(clearAllRegistryPrimaryVars);
|
|
|
|
// ─── envVarNameFor ─────────────────────────────────────────────────────────
|
|
|
|
describe('envVarNameFor', () => {
|
|
it('produces upper-cased env-var names from the enum value', () => {
|
|
expect(envVarNameFor(SupportedLanguages.Python)).toBe('REGISTRY_PRIMARY_PYTHON');
|
|
expect(envVarNameFor(SupportedLanguages.TypeScript)).toBe('REGISTRY_PRIMARY_TYPESCRIPT');
|
|
expect(envVarNameFor(SupportedLanguages.JavaScript)).toBe('REGISTRY_PRIMARY_JAVASCRIPT');
|
|
});
|
|
|
|
it('uses the enum VALUE, not the key, for languages whose key differs from the value', () => {
|
|
// Key 'CPlusPlus' → value 'cpp' → env var 'REGISTRY_PRIMARY_CPP'.
|
|
// Users see the language by its canonical name, not its TS symbol.
|
|
expect(envVarNameFor(SupportedLanguages.CPlusPlus)).toBe('REGISTRY_PRIMARY_CPP');
|
|
expect(envVarNameFor(SupportedLanguages.CSharp)).toBe('REGISTRY_PRIMARY_CSHARP');
|
|
});
|
|
|
|
it('covers every member of SupportedLanguages', () => {
|
|
// Build env-var names for every language and assert no duplicates —
|
|
// catches a future enum-value collision or accidental renaming.
|
|
const names = new Set<string>();
|
|
for (const lang of Object.values(SupportedLanguages)) {
|
|
names.add(envVarNameFor(lang));
|
|
}
|
|
expect(names.size).toBe(Object.values(SupportedLanguages).length);
|
|
});
|
|
});
|
|
|
|
// ─── isRegistryPrimary ─────────────────────────────────────────────────────
|
|
|
|
describe('isRegistryPrimary', () => {
|
|
it('returns MIGRATED_LANGUAGES membership by default (no env var set)', () => {
|
|
// Ring 3: languages in MIGRATED_LANGUAGES are registry-primary by
|
|
// default — operators don't need to set an env var for the rolled-out
|
|
// migration to take effect. Unmigrated languages default to false.
|
|
for (const lang of Object.values(SupportedLanguages)) {
|
|
expect(isRegistryPrimary(lang)).toBe(MIGRATED_LANGUAGES.has(lang));
|
|
}
|
|
});
|
|
|
|
it("returns true when the env var is 'true' (lowercase)", () => {
|
|
process.env['REGISTRY_PRIMARY_PYTHON'] = 'true';
|
|
expect(isRegistryPrimary(SupportedLanguages.Python)).toBe(true);
|
|
});
|
|
|
|
it("returns true when the env var is '1'", () => {
|
|
process.env['REGISTRY_PRIMARY_PYTHON'] = '1';
|
|
expect(isRegistryPrimary(SupportedLanguages.Python)).toBe(true);
|
|
});
|
|
|
|
it("returns true when the env var is 'yes'", () => {
|
|
process.env['REGISTRY_PRIMARY_PYTHON'] = 'yes';
|
|
expect(isRegistryPrimary(SupportedLanguages.Python)).toBe(true);
|
|
});
|
|
|
|
it('accepts mixed-case and whitespace-padded truthy values', () => {
|
|
process.env['REGISTRY_PRIMARY_PYTHON'] = ' TRUE ';
|
|
expect(isRegistryPrimary(SupportedLanguages.Python)).toBe(true);
|
|
process.env['REGISTRY_PRIMARY_PYTHON'] = 'Yes';
|
|
expect(isRegistryPrimary(SupportedLanguages.Python)).toBe(true);
|
|
});
|
|
|
|
it("returns false for falsy-looking values ('false', '0', empty, 'off')", () => {
|
|
for (const value of ['false', '0', '', 'off', 'no', 'disabled']) {
|
|
process.env['REGISTRY_PRIMARY_PYTHON'] = value;
|
|
expect(isRegistryPrimary(SupportedLanguages.Python)).toBe(false);
|
|
}
|
|
});
|
|
|
|
it('returns false for unrecognized tokens (fail-safe on typos)', () => {
|
|
// User meant to type 'true' but fat-fingered — conservative: treat as off.
|
|
for (const value of ['ture', 'tru', 'yeah', 'enable', 'y']) {
|
|
process.env['REGISTRY_PRIMARY_PYTHON'] = value;
|
|
expect(isRegistryPrimary(SupportedLanguages.Python)).toBe(false);
|
|
}
|
|
});
|
|
|
|
it('isolates flags per-language (one on does not affect others)', () => {
|
|
process.env['REGISTRY_PRIMARY_PYTHON'] = 'true';
|
|
expect(isRegistryPrimary(SupportedLanguages.Python)).toBe(true);
|
|
// Vue is not in MIGRATED_LANGUAGES — default false stays
|
|
// false regardless of Python's flag.
|
|
expect(isRegistryPrimary(SupportedLanguages.Vue)).toBe(false);
|
|
});
|
|
|
|
it('respects a mid-process env-var mutation (no stale cache)', () => {
|
|
// Use Vue — not in MIGRATED_LANGUAGES — so the unset default is
|
|
// deterministically `false`, independent of which languages have
|
|
// been flipped to registry-primary.
|
|
expect(isRegistryPrimary(SupportedLanguages.Vue)).toBe(false);
|
|
process.env['REGISTRY_PRIMARY_VUE'] = 'true';
|
|
expect(isRegistryPrimary(SupportedLanguages.Vue)).toBe(true);
|
|
delete process.env['REGISTRY_PRIMARY_VUE'];
|
|
expect(isRegistryPrimary(SupportedLanguages.Vue)).toBe(false);
|
|
});
|
|
|
|
it('handles the CPlusPlus → REGISTRY_PRIMARY_CPP mapping correctly', () => {
|
|
process.env['REGISTRY_PRIMARY_CPP'] = 'true';
|
|
expect(isRegistryPrimary(SupportedLanguages.CPlusPlus)).toBe(true);
|
|
// Negative: the TS-key-style name is NOT read. CPlusPlus is now in
|
|
// MIGRATED_LANGUAGES, so we must explicitly opt it out via the
|
|
// canonical env var to verify the wrong-name var has no effect.
|
|
process.env['REGISTRY_PRIMARY_CPP'] = 'false';
|
|
process.env['REGISTRY_PRIMARY_CPLUSPLUS'] = 'true';
|
|
expect(isRegistryPrimary(SupportedLanguages.CPlusPlus)).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ─── primaryLanguages ──────────────────────────────────────────────────────
|
|
|
|
describe('primaryLanguages', () => {
|
|
it('returns MIGRATED_LANGUAGES when no flags are set', () => {
|
|
// Default-on for migrated languages (Ring 3); unmigrated stay off.
|
|
const enabled = primaryLanguages();
|
|
expect(enabled.size).toBe(MIGRATED_LANGUAGES.size);
|
|
for (const lang of MIGRATED_LANGUAGES) {
|
|
expect(enabled.has(lang)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('returns exactly the flipped languages (env opts in unmigrated, opts out migrated)', () => {
|
|
// Migrated languages are default-on; each must be opted out here when
|
|
// testing explicit env overrides. Ruby (unmigrated) opts in.
|
|
// Opt out every member of MIGRATED_LANGUAGES dynamically so this test
|
|
// does not have to be updated each time a new language ships its
|
|
// Ring 3 migration (C++ and PHP joined the set in their respective
|
|
// Ring 3 migrations; future Ring 3 additions land here without test churn).
|
|
for (const lang of MIGRATED_LANGUAGES) {
|
|
process.env[envVarNameFor(lang)] = 'false';
|
|
}
|
|
process.env['REGISTRY_PRIMARY_RUBY'] = '1';
|
|
const enabled = primaryLanguages();
|
|
expect(enabled.has(SupportedLanguages.Python)).toBe(false);
|
|
expect(enabled.has(SupportedLanguages.CSharp)).toBe(false);
|
|
expect(enabled.has(SupportedLanguages.Go)).toBe(false);
|
|
expect(enabled.has(SupportedLanguages.CPlusPlus)).toBe(false);
|
|
expect(enabled.has(SupportedLanguages.PHP)).toBe(false);
|
|
expect(enabled.has(SupportedLanguages.Ruby)).toBe(true);
|
|
// Only Ruby is on: migrated defaults overridden off, Ruby explicitly on.
|
|
expect(enabled.size).toBe(1);
|
|
});
|
|
|
|
it('returns a plain Set (not a frozen proxy) — consistent shape', () => {
|
|
process.env['REGISTRY_PRIMARY_PYTHON'] = 'true';
|
|
const enabled = primaryLanguages();
|
|
expect(enabled).toBeInstanceOf(Set);
|
|
});
|
|
});
|