GitNexus/gitnexus/test/integration/resolvers/dart.test.ts
Gergő Magyar df5ce1f49b
fix(ingestion): close remaining open language parsing-layer coverage gaps (#1919) (#2072)
* fix(c): skip computed #include MACRO instead of emitting a garbage import source (F5)

* fix(cpp): emit a Variable per name for structured-binding declarations (F9)

* fix(dart): extract static const/final class fields (F26)

* fix(dart): capture old-style function typedefs (F28)

* fix(dart): read real top-level variable shape instead of a dead type field (F29)

* fix(kotlin): capture callable references (F47)

* fix(kotlin): anchor infix-call capture to the operator only (F49)

* fix(kotlin): extract secondary constructors as members (F48)

* fix(kotlin): capture destructuring declarations (F51)

* fix(kotlin): index companion-object properties as fields (F52)

* test(kotlin): assert callable-reference coverage runs on the worker path (F47)

* fix(swift): extract protocol property requirements (F75)

* fix(swift): recognize enum_class_body as a method body node (F79)

* test(ingestion): rebaseline swift captures-golden + scope-capture fingerprints (#1919)

* fix(kotlin): attribute secondary-constructor body calls to the Constructor node (#1919 review CF1)

A Kotlin secondary constructor's body executes statements like a method body,
but the registry-primary scope-resolution path had no Function scope or
Constructor def for it. A call inside the body resolved its caller anchor up to
the enclosing Class scope, mis-attributing the CALLS edge to the class rather
than the Constructor.

Add `(secondary_constructor) @scope.function` to the Kotlin scope query so the
body becomes its own scope, and synthesize a `@declaration.constructor` (named
`constructor`, qualified `<Class>.constructor`, with parameter metadata) so the
scope owns a Constructor def that bridges to the structure-phase Constructor node.

Also add an arity-disambiguating lookup key for overloadable callables: two
same-name secondary constructors of different arity (e.g. a zero-arg vs a 2-arg)
share the qualified key whose first-write-wins assignment is source-order-
dependent — so a zero-arg overload could resolve to a sibling. The structure
node id encodes `#<arity>`; mirror that in the bridge keyspace and match by the
def's parameterCount. Same-arity overloads collapse onto one arity key exactly
as before, so no regression there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(kotlin): do not own function-local property bindings under the enclosing class (#1919 review CF3)

Kotlin emits destructuring / loop bindings (`val (a,b) = pair`,
`for ((k,v) in m)`) as `@definition.property` to dodge the block-scope
local-symbol pruner. When such a binding sits inside a method body of a class,
the structure-phase owner walk found the enclosing class and emitted a spurious
HAS_PROPERTY edge (e.g. `C -> k`), treating a function-local as a class member.

Guard the Property owner resolution: if a function-like ancestor is reached
before any class container, the property is function-local and gets no owner
edge (it falls back to a File DEFINES edge). Language-agnostic — genuine class
fields sit directly in the class body with no intervening function, so they
keep their HAS_PROPERTY owner edge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(kotlin): guard non-companion property isStatic=false (#1919 review CF4)

Add a field-extraction case for a plain non-companion class
`class C { val x: Int = 1 }` asserting the property `x` has isStatic=false,
guarding the `isInsideKotlinCompanion` walk against false-positives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(kotlin): dedup type_identifier lookup in extractOwnerName (#1919 review CF5)

The `node.namedChildren.find(c => c.type === 'type_identifier')?.text` lookup was
duplicated across the companion and non-companion branches of the Kotlin
field-extractor's extractOwnerName. Hoist it into a single local, preserving the
existing behavior (anonymous companion falls back to "Companion"; other nodes
prefer the `name` field, else the type_identifier text, else undefined).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(dart): capture generic old-style function typedefs (#1919 review CF2)

* test(dart): guard multi-name field count and top-level-var labels (#1919 review CF4)

* docs(swift): correct isStatic comment re multi-modifier hasKeyword (#1919 review CF5)

* test(ingestion): rebaseline dart+kotlin scope-capture fingerprints after review remediation (#1919)

* fix(ingestion): correct CF3 owner-strip boundary set for accessor/init bodies and Dart signatures (#1919 review)

The CF3 property-ownership guard used FUNCTION_NODE_TYPES, which (a) includes
Dart bare signatures (function_signature/method_signature) — over-stripping
every Dart class getter/setter's HAS_PROPERTY owner — and (b) omits Kotlin
anonymous_initializer/getter/setter and Swift computed accessors — under-
stripping destructuring/locals inside init{} and accessor bodies, emitting
spurious Class->local HAS_PROPERTY edges. Introduces a guard-specific
LOCAL_SCOPE_BODY_NODE_TYPES set (signatures excluded, accessor/init bodies
included). Adds Dart accessor-ownership + Kotlin init/accessor destructuring
regression fixtures. Both confirmed on the worker pipeline; no cross-language
regression (1597 cross-language tests green).

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 08:09:43 +01:00

873 lines
35 KiB
TypeScript

/**
* Dart: field-type resolution and call-result binding.
* Verifies that class fields are captured as Property nodes with HAS_PROPERTY
* edges, and that calls (including chained and call-result-bound) are resolved.
*
* All Dart pipeline features are covered: Property nodes, HAS_PROPERTY edges,
* CALLS chain resolution, IMPORTS, call attribution, and ACCESSES field reads.
*/
import { describe, it, expect, beforeAll } from 'vitest';
import path from 'path';
import {
FIXTURES,
getRelationships,
getNodesByLabel,
getNodesByLabelFull,
edgeSet,
runPipelineFromRepo,
type PipelineResult,
} from './helpers.js';
import {
isLanguageAvailable,
loadParser,
loadLanguage,
} from '../../../src/core/tree-sitter/parser-loader.js';
import { SupportedLanguages } from '../../../src/config/supported-languages.js';
// isLanguageAvailable only checks whether the module loaded — it does NOT verify
// that the native binary works at runtime (tree-sitter-dart can fail on setLanguage).
// Probe the parser to get a reliable skip guard.
let dartAvailable = isLanguageAvailable(SupportedLanguages.Dart);
if (dartAvailable) {
try {
await loadParser();
await loadLanguage(SupportedLanguages.Dart);
} catch {
dartAvailable = false;
}
}
// ── Phase 8: Field-type resolution ──────────────────────────────────────
describe.skipIf(!dartAvailable)('Dart field-type resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'dart-field-types'), () => {});
}, 60000);
it('detects classes and their properties', () => {
expect(getNodesByLabel(result, 'Class')).toEqual(expect.arrayContaining(['Address', 'User']));
const properties = getNodesByLabel(result, 'Property');
expect(properties).toContain('address');
expect(properties).toContain('city');
expect(properties).toContain('name');
});
it('emits HAS_PROPERTY edges from class to field', () => {
const propEdges = getRelationships(result, 'HAS_PROPERTY');
expect(edgeSet(propEdges)).toEqual(
expect.arrayContaining(['User → address', 'User → name', 'Address → city']),
);
});
it('resolves save() call from field-chain user.address.save()', () => {
const calls = getRelationships(result, 'CALLS');
// Dart attributes calls to the enclosing Function
const saveCalls = calls.filter(
(c) => c.target === 'save' && c.sourceFilePath.includes('app.dart'),
);
expect(saveCalls.length).toBe(1);
expect(saveCalls[0]!.targetFilePath).toContain('models.dart');
});
it('attributes save() call source to processUser, not File', () => {
const calls = getRelationships(result, 'CALLS');
const saveCalls = calls.filter(
(c) => c.target === 'save' && c.sourceFilePath.includes('app.dart'),
);
expect(saveCalls.length).toBe(1);
expect(saveCalls[0]!.source).toBe('processUser');
expect(saveCalls[0]!.sourceLabel).toBe('Function');
});
it('creates IMPORTS edge between app.dart and models.dart', () => {
const imports = getRelationships(result, 'IMPORTS');
const appImports = imports.filter(
(e) => e.sourceFilePath.includes('app.dart') && e.targetFilePath.includes('models.dart'),
);
expect(appImports.length).toBe(1);
});
it('emits ACCESSES edges for field reads in chains', () => {
const accesses = getRelationships(result, 'ACCESSES');
const addressReads = accesses.filter((e) => e.target === 'address' && e.rel.reason === 'read');
expect(addressReads.length).toBe(1);
expect(addressReads[0]!.source).toBe('processUser');
expect(addressReads[0]!.targetLabel).toBe('Property');
});
});
// ── Phase 9: Call-result binding ────────────────────────────────────────
describe.skipIf(!dartAvailable)('Dart call-result binding', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'dart-call-result-binding'), () => {});
}, 60000);
it('detects classes, methods, and functions', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
expect(getNodesByLabel(result, 'Function')).toEqual(
expect.arrayContaining(['getUser', 'processUser']),
);
});
it('resolves save() call via call-result binding', () => {
const calls = getRelationships(result, 'CALLS');
// Dart attributes calls to the enclosing Function
const saveCalls = calls.filter(
(c) => c.target === 'save' && c.sourceFilePath.includes('app.dart'),
);
expect(saveCalls.length).toBe(1);
expect(saveCalls[0]!.targetFilePath).toContain('models.dart');
});
it('resolves getUser() call', () => {
const calls = getRelationships(result, 'CALLS');
const getUserCalls = calls.filter(
(c) => c.target === 'getUser' && c.sourceFilePath.includes('app.dart'),
);
expect(getUserCalls.length).toBe(1);
});
it('attributes calls to processUser, not File', () => {
const calls = getRelationships(result, 'CALLS');
const appCalls = calls.filter((c) => c.sourceFilePath.includes('app.dart'));
for (const call of appCalls) {
expect(call.source).toBe('processUser');
expect(call.sourceLabel).toBe('Function');
}
});
});
// ---------------------------------------------------------------------------
// Phase 16: Method enrichment (isAbstract, isStatic, annotations)
// animal.dart: abstract Animal with abstract speak(), static classify(), breathe()
// Dog extends Animal, @override speak()
// app.dart: dog.speak(), Animal.classify("dog")
// ---------------------------------------------------------------------------
describe.skipIf(!dartAvailable)('Dart method enrichment', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'dart-method-enrichment'), () => {});
}, 60000);
it('detects Animal and Dog classes', () => {
const classes = getNodesByLabel(result, 'Class');
expect(classes).toContain('Animal');
expect(classes).toContain('Dog');
});
it('emits HAS_METHOD edges for Animal methods', () => {
const hasMethod = getRelationships(result, 'HAS_METHOD');
const animalMethods = hasMethod
.filter((e) => e.source === 'Animal')
.map((e) => e.target)
.sort();
expect(animalMethods).toContain('speak');
expect(animalMethods).toContain('classify');
expect(animalMethods).toContain('breathe');
});
it('emits HAS_METHOD edge for Dog.speak', () => {
const hasMethod = getRelationships(result, 'HAS_METHOD');
const dogSpeak = hasMethod.find((e) => e.source === 'Dog' && e.target === 'speak');
expect(dogSpeak).toBeDefined();
});
it('emits EXTENDS edge Dog -> Animal', () => {
const extends_ = getRelationships(result, 'EXTENDS');
const dogExtends = extends_.find((e) => e.source === 'Dog' && e.target === 'Animal');
expect(dogExtends).toBeDefined();
});
it('marks abstract speak as isAbstract', () => {
const methods = getNodesByLabelFull(result, 'Method');
const speak = methods.find(
(n) => n.name === 'speak' && n.properties.filePath === 'animal.dart',
);
expect(speak).toBeDefined();
expect(speak!.properties.isAbstract).toBe(true);
});
it('marks breathe as NOT isAbstract', () => {
const methods = getNodesByLabelFull(result, 'Method');
const breathe = methods.find((n) => n.name === 'breathe');
expect(breathe).toBeDefined();
expect(breathe!.properties.isAbstract).toBe(false);
});
it('marks classify as isStatic', () => {
const methods = getNodesByLabelFull(result, 'Method');
const classify = methods.find((n) => n.name === 'classify');
expect(classify).toBeDefined();
expect(classify!.properties.isStatic).toBe(true);
});
it('marks breathe as NOT isStatic', () => {
const methods = getNodesByLabelFull(result, 'Method');
const breathe = methods.find((n) => n.name === 'breathe');
expect(breathe).toBeDefined();
expect(breathe!.properties.isStatic).toBe(false);
});
it('abstract Animal.speak has isAbstract=true and concrete breathe does not', () => {
const methods = getNodesByLabelFull(result, 'Method');
const speak = methods.find((n) => n.name === 'speak');
expect(speak).toBeDefined();
expect(speak!.properties.isAbstract).toBe(true);
const breathe = methods.find((n) => n.name === 'breathe');
expect(breathe).toBeDefined();
expect(breathe!.properties.isAbstract).toBe(false);
});
it('populates parameterTypes for classify', () => {
const methods = getNodesByLabelFull(result, 'Method');
const classify = methods.find((n) => n.name === 'classify');
expect(classify).toBeDefined();
const params = classify!.properties.parameterTypes;
expect(params).toContain('String');
});
it('resolves dog.speak() CALLS edge', () => {
const calls = getRelationships(result, 'CALLS');
const speakCall = calls.find(
(c) => c.target === 'speak' && c.sourceFilePath.includes('app.dart'),
);
expect(speakCall).toBeDefined();
});
it('resolves Animal.classify("dog") static CALLS edge', () => {
const calls = getRelationships(result, 'CALLS');
const classifyCall = calls.find(
(c) => c.target === 'classify' && c.sourceFilePath.includes('app.dart'),
);
expect(classifyCall).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Basic arity-based call resolution (Dart catch-up)
// one.dart: writeAudit(String message), zero.dart: writeAuditSimple()
// app.dart: writeAudit("hello"), writeAuditSimple()
// ---------------------------------------------------------------------------
describe.skipIf(!dartAvailable)('Dart calls', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'dart-calls'), () => {});
}, 60000);
it('detects top-level functions', () => {
const functions = getNodesByLabel(result, 'Function');
expect(functions).toContain('writeAudit');
expect(functions).toContain('writeAuditSimple');
expect(functions).toContain('run');
});
it('resolves writeAudit("hello") CALLS edge', () => {
const calls = getRelationships(result, 'CALLS');
const auditCall = calls.find(
(c) => c.target === 'writeAudit' && c.sourceFilePath.includes('app.dart'),
);
expect(auditCall).toBeDefined();
expect(auditCall!.targetFilePath).toContain('one.dart');
});
it('resolves writeAuditSimple() CALLS edge', () => {
const calls = getRelationships(result, 'CALLS');
const simpleCall = calls.find(
(c) => c.target === 'writeAuditSimple' && c.sourceFilePath.includes('app.dart'),
);
expect(simpleCall).toBeDefined();
expect(simpleCall!.targetFilePath).toContain('zero.dart');
});
it('attributes calls to run, not File', () => {
const calls = getRelationships(result, 'CALLS');
const appCalls = calls.filter((c) => c.sourceFilePath.includes('app.dart'));
for (const call of appCalls) {
expect(call.source).toBe('run');
expect(call.sourceLabel).toBe('Function');
}
});
it('creates IMPORTS edges from app.dart', () => {
const imports = getRelationships(result, 'IMPORTS');
const appImports = imports.filter((e) => e.sourceFilePath.includes('app.dart'));
const targetFiles = appImports.map((e) => e.targetFilePath).sort();
expect(targetFiles).toEqual(expect.arrayContaining(['one.dart', 'zero.dart']));
});
});
// ---------------------------------------------------------------------------
// Member calls: receiver-type resolution via constructor inference
// user.dart: User { save() }, app.dart: var user = User(); user.save()
// ---------------------------------------------------------------------------
describe.skipIf(!dartAvailable)('Dart member calls', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'dart-member-calls'), () => {});
}, 60000);
it('detects User class and save method', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
expect(getNodesByLabel(result, 'Method')).toContain('save');
expect(getNodesByLabel(result, 'Function')).toContain('processUser');
});
it('emits HAS_METHOD edge User -> save', () => {
const hasMethod = getRelationships(result, 'HAS_METHOD');
const userSave = hasMethod.find((e) => e.source === 'User' && e.target === 'save');
expect(userSave).toBeDefined();
});
it('resolves user.save() CALLS edge via constructor inference', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(
(c) => c.target === 'save' && c.sourceFilePath.includes('app.dart'),
);
expect(saveCall).toBeDefined();
expect(saveCall!.targetFilePath).toContain('user.dart');
});
it('attributes save() call to processUser, not File', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(
(c) => c.target === 'save' && c.sourceFilePath.includes('app.dart'),
);
expect(saveCall).toBeDefined();
expect(saveCall!.source).toBe('processUser');
expect(saveCall!.sourceLabel).toBe('Function');
});
it('creates IMPORTS edge from app.dart to user.dart', () => {
const imports = getRelationships(result, 'IMPORTS');
const appImport = imports.find(
(e) => e.sourceFilePath.includes('app.dart') && e.targetFilePath.includes('user.dart'),
);
expect(appImport).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Dart async / async* / sync* method detection
// Verifies isDartAsync correctly identifies all three async-like forms
// ---------------------------------------------------------------------------
describe.skipIf(!dartAvailable)('Dart async method detection', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'dart-async-methods'), () => {});
}, 60000);
it('detects DataService class', () => {
expect(getNodesByLabel(result, 'Class')).toContain('DataService');
});
it('emits HAS_METHOD edges for all DataService methods', () => {
const hasMethod = getRelationships(result, 'HAS_METHOD');
const methods = hasMethod
.filter((e) => e.source === 'DataService')
.map((e) => e.target)
.sort();
expect(methods).toContain('fetchUser');
expect(methods).toContain('countUp');
expect(methods).toContain('generateNames');
expect(methods).toContain('formatName');
});
it('marks async method fetchUser as isAsync=true', () => {
const methods = getNodesByLabelFull(result, 'Method');
const fetchUser = methods.find((n) => n.name === 'fetchUser');
expect(fetchUser).toBeDefined();
expect(fetchUser!.properties.isAsync).toBe(true);
});
it('marks async* generator countUp as isAsync=true', () => {
const methods = getNodesByLabelFull(result, 'Method');
const countUp = methods.find((n) => n.name === 'countUp');
expect(countUp).toBeDefined();
expect(countUp!.properties.isAsync).toBe(true);
});
it('marks sync* generator generateNames as isAsync=true', () => {
const methods = getNodesByLabelFull(result, 'Method');
const generateNames = methods.find((n) => n.name === 'generateNames');
expect(generateNames).toBeDefined();
expect(generateNames!.properties.isAsync).toBe(true);
});
it('marks regular sync method formatName as isAsync=false', () => {
const methods = getNodesByLabelFull(result, 'Method');
const formatName = methods.find((n) => n.name === 'formatName');
expect(formatName).toBeDefined();
// buildMethodProps only sets isAsync when truthy; for sync methods the
// property is absent (undefined), which is equivalent to false.
expect(formatName!.properties.isAsync ?? false).toBe(false);
});
it('populates parameterTypes for fetchUser(int id)', () => {
const methods = getNodesByLabelFull(result, 'Method');
const fetchUser = methods.find((n) => n.name === 'fetchUser');
expect(fetchUser).toBeDefined();
expect(fetchUser!.properties.parameterTypes).toContain('int');
});
it('populates returnType for formatName', () => {
const methods = getNodesByLabelFull(result, 'Method');
const formatName = methods.find((n) => n.name === 'formatName');
expect(formatName).toBeDefined();
expect(formatName!.properties.returnType).toBe('String');
});
});
// ---------------------------------------------------------------------------
// Interface dispatch: METHOD_IMPLEMENTS edges from concrete → abstract methods
// abstract Repository with find/save, SqlRepository implements them
// ---------------------------------------------------------------------------
describe.skipIf(!dartAvailable)('Dart interface dispatch (METHOD_IMPLEMENTS)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'dart-interface-dispatch'), () => {});
}, 60000);
it('detects Repository class and SqlRepository class', () => {
const classes = getNodesByLabel(result, 'Class');
expect(classes).toContain('Repository');
expect(classes).toContain('SqlRepository');
});
it('emits IMPLEMENTS edge SqlRepository → Repository', () => {
const impl = getRelationships(result, 'IMPLEMENTS');
const edge = impl.find((e) => e.source === 'SqlRepository' && e.target === 'Repository');
expect(edge).toBeDefined();
});
it('emits METHOD_IMPLEMENTS edges for find and save', () => {
const mi = getRelationships(result, 'METHOD_IMPLEMENTS');
const findEdge = mi.find(
(e) =>
e.source === 'find' &&
e.target === 'find' &&
e.sourceFilePath.includes('sql_repository') &&
e.targetFilePath.includes('repository'),
);
const saveEdge = mi.find(
(e) =>
e.source === 'save' &&
e.target === 'save' &&
e.sourceFilePath.includes('sql_repository') &&
e.targetFilePath.includes('repository'),
);
expect(findEdge).toBeDefined();
expect(saveEdge).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// SM-9/SM-10: inherited method resolution — Dart first-wins inheritance walk
// ---------------------------------------------------------------------------
describe.skipIf(!dartAvailable)(
'Dart Child extends Parent — inherited method resolution (SM-9)',
() => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'dart-child-extends-parent'),
() => {},
);
}, 60000);
it('detects Parent and Child classes', () => {
const classes = getNodesByLabel(result, 'Class');
expect(classes).toContain('Parent');
expect(classes).toContain('Child');
});
it('emits EXTENDS edge: Child → Parent', () => {
const extends_ = getRelationships(result, 'EXTENDS');
expect(edgeSet(extends_)).toContain('Child → Parent');
});
it('resolves c.parentMethod() to Parent.parentMethod via first-wins MRO walk', () => {
const calls = getRelationships(result, 'CALLS');
const parentMethodCall = calls.find(
(c) => c.target === 'parentMethod' && c.targetFilePath.includes('parent.dart'),
);
expect(parentMethodCall).toBeDefined();
expect(parentMethodCall!.source).toBe('run');
});
},
);
// ---------------------------------------------------------------------------
// await call patterns: await fetchUser(), await processData()
// ---------------------------------------------------------------------------
describe.skipIf(!dartAvailable)('Dart await call resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'dart-await-calls'), () => {});
}, 60000);
it('detects fetchUser and processData as functions', () => {
const fns = getNodesByLabel(result, 'Function');
expect(fns).toContain('fetchUser');
expect(fns).toContain('processData');
});
it('resolves run → fetchUser via await direct call', () => {
const calls = getRelationships(result, 'CALLS');
const edge = calls.find((c) => c.source === 'run' && c.target === 'fetchUser');
expect(edge).toBeDefined();
});
it('resolves run → processData via await direct call', () => {
const calls = getRelationships(result, 'CALLS');
const edge = calls.find((c) => c.source === 'run' && c.target === 'processData');
expect(edge).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Widget-tree call patterns: named argument and list literal
// ---------------------------------------------------------------------------
describe.skipIf(!dartAvailable)('Dart widget-tree call resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'dart-widget-tree-calls'), () => {});
}, 60000);
it('detects buildHeader, buildBody, buildFooter as functions', () => {
const fns = getNodesByLabel(result, 'Function');
expect(fns).toContain('buildHeader');
expect(fns).toContain('buildBody');
expect(fns).toContain('buildFooter');
});
it('resolves buildPage → buildHeader via named argument call', () => {
const calls = getRelationships(result, 'CALLS');
const edge = calls.find((c) => c.source === 'buildPage' && c.target === 'buildHeader');
expect(edge).toBeDefined();
});
it('resolves buildPage → buildBody via list literal call', () => {
const calls = getRelationships(result, 'CALLS');
const edge = calls.find((c) => c.source === 'buildPage' && c.target === 'buildBody');
expect(edge).toBeDefined();
});
it('resolves buildPage → buildFooter via list literal call', () => {
const calls = getRelationships(result, 'CALLS');
const edge = calls.find((c) => c.source === 'buildPage' && c.target === 'buildFooter');
expect(edge).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Implicit-constructor construction edge (PR #1970 review regression)
// build() { return Widget(); } where Widget has only an implicit constructor.
// Both the legacy DAG and the registry-primary path must emit a CALLS edge
// from the enclosing function to the constructed Class.
// ---------------------------------------------------------------------------
describe.skipIf(!dartAvailable)('Dart implicit-constructor construction', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'dart-construct-cascade'), () => {});
}, 60000);
it('detects the Widget class and the build function', () => {
expect(getNodesByLabel(result, 'Class')).toContain('Widget');
expect(getNodesByLabel(result, 'Function')).toContain('build');
});
it('emits a CALLS edge build → Widget for the implicit-constructor call', () => {
const calls = getRelationships(result, 'CALLS');
const ctorCall = calls.find((c) => c.source === 'build' && c.target === 'Widget');
expect(ctorCall).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// F24 (issue #1926): member calls (obj.method()) in return / list-literal /
// named-argument / arrow-body contexts. The legacy DAG (removed in #942) only
// captured member calls under expression_statement /
// initialized_variable_definition; scope-resolution now owns and resolves these
// broader contexts.
// ---------------------------------------------------------------------------
describe.skipIf(!dartAvailable)('Dart member-call contexts (F24)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'dart-member-call-contexts'), () => {});
}, 60000);
it('resolves a member call in a return statement (svc.compute())', () => {
const calls = getRelationships(result, 'CALLS');
const edge = calls.find((c) => c.source === 'inReturn' && c.target === 'compute');
expect(edge).toBeDefined();
expect(edge!.targetFilePath).toContain('models.dart');
});
it('resolves member calls inside a list literal', () => {
const calls = getRelationships(result, 'CALLS');
expect(calls.find((c) => c.source === 'inList' && c.target === 'first')).toBeDefined();
expect(calls.find((c) => c.source === 'inList' && c.target === 'second')).toBeDefined();
});
it('resolves a member call in a named argument', () => {
const calls = getRelationships(result, 'CALLS');
const edge = calls.find((c) => c.source === 'inNamedArg' && c.target === 'load');
expect(edge).toBeDefined();
});
it('resolves a member call in an arrow body', () => {
const calls = getRelationships(result, 'CALLS');
const edge = calls.find((c) => c.source === 'inArrow' && c.target === 'run');
expect(edge).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// F25 (issue #1926): calls inside a constructor body are mis-attributed by the
// legacy enclosing-function finder (it only unwraps function_signature, not the
// constructor_signature whose body is a sibling of the wrapping method_signature).
// The registry-primary scope path synthesizes a Function scope for the
// constructor body, and the Constructor def is a valid caller anchor, so the
// call attributes to the constructor. Registry-primary-only win.
// (Getter/setter and operator bodies are out of scope: Property is not a caller
// anchor, and the structure phase emits no Method node for operators — see the
// helpers.ts expected-failures comment.)
// ---------------------------------------------------------------------------
describe.skipIf(!dartAvailable)('Dart constructor body call attribution (F25)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'dart-constructor-body'), () => {});
}, 60000);
it('attributes a call inside a constructor body to the constructor', () => {
const calls = getRelationships(result, 'CALLS');
const edge = calls.find((c) => c.target === 'setup' && c.sourceLabel === 'Constructor');
expect(edge).toBeDefined();
expect(edge!.source).toBe('Vector');
});
});
// ---------------------------------------------------------------------------
// Named constructor with a body (PR #1970 tri-review P0 regression).
// `class A { A.named() { ... } }` parses as a constructor_signature with
// multiple name: fields, double-matching the scope query. Without dedup, two
// identical-range Function scopes throw ScopeTreeInvariantError and the WHOLE
// file is dropped from registry-primary resolution. Test 1 (parity) guards
// against the file-drop in both modes; test 2 is the F25 constructor-attribution
// win (registry-only, like the dart-constructor-body case).
// ---------------------------------------------------------------------------
describe.skipIf(!dartAvailable)('Dart named-constructor body (no file drop)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'dart-named-constructor-body'),
() => {},
);
}, 60000);
it('still resolves other calls in a class that has a named constructor with a body', () => {
const calls = getRelationships(result, 'CALLS');
const greetCall = calls.find((c) => c.source === 'greet' && c.target === 'setup');
expect(greetCall).toBeDefined();
});
it('attributes a call inside a named-constructor body to the constructor', () => {
const calls = getRelationships(result, 'CALLS');
const ctorCall = calls.find((c) => c.target === 'setup' && c.sourceLabel === 'Constructor');
expect(ctorCall).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// F26 (issue #1919): static const / static final class fields.
// `static const`/`static final` fields parse with a static_final_declaration_list
// (not initialized_identifier_list), so the legacy field rules missed them and
// no Property node was created end-to-end. They must surface as Property nodes
// marked static + readonly, one per name in a multi-name declaration.
// ---------------------------------------------------------------------------
describe.skipIf(!dartAvailable)('Dart static const/final fields (F26)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'dart-static-fields'), () => {});
}, 60000);
it('captures static const and static final fields as Properties', () => {
const properties = getNodesByLabel(result, 'Property');
expect(properties).toContain('maxRetries'); // static const
expect(properties).toContain('host'); // static final, name 1 of 2
expect(properties).toContain('scheme'); // static final, name 2 of 2
expect(properties).toContain('port'); // instance field (regression)
expect(properties).toContain('_secret'); // private static const
});
it('emits HAS_PROPERTY edges for static fields', () => {
const propEdges = getRelationships(result, 'HAS_PROPERTY');
expect(edgeSet(propEdges)).toEqual(
expect.arrayContaining([
'Config → maxRetries',
'Config → host',
'Config → scheme',
'Config → port',
'Config → _secret',
]),
);
});
it('marks static const/final fields as static + readonly', () => {
const props = getNodesByLabelFull(result, 'Property');
for (const name of ['maxRetries', 'host', 'scheme', '_secret']) {
const p = props.find((n) => n.name === name);
expect(p, name).toBeDefined();
expect(p!.properties.isStatic, name).toBe(true);
expect(p!.properties.isReadonly, name).toBe(true);
}
});
it('keeps the instance field non-static, non-readonly (regression)', () => {
const props = getNodesByLabelFull(result, 'Property');
const port = props.find((n) => n.name === 'port');
expect(port).toBeDefined();
expect(port!.properties.isStatic).toBe(false);
expect(port!.properties.isReadonly).toBe(false);
});
});
// ---------------------------------------------------------------------------
// F29 (issue #1919): top-level Dart variables. Top-level vars are loose
// siblings under `program` (no `declaration` wrapper), so the structure query
// never captured them and no Variable node existed end-to-end. They must now
// surface as Variable nodes with the real type/const metadata read from the
// captured container's leading siblings (not a phantom `type` field).
// ---------------------------------------------------------------------------
describe.skipIf(!dartAvailable)('Dart top-level variables (F29)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'dart-toplevel-vars'), () => {});
}, 60000);
it('captures top-level variables (typed final, inferred var, multi-name const)', () => {
const vars = getNodesByLabel(result, 'Variable');
expect(vars).toContain('count'); // final int count = 3; (covers F29)
expect(vars).toContain('name'); // var name = 'x';
expect(vars).toContain('a'); // const a = 1, b = 2;
expect(vars).toContain('b');
});
it('reads the real type for a typed final and leaves an inferred var untyped', () => {
const vars = getNodesByLabelFull(result, 'Variable');
const count = vars.find((n) => n.name === 'count');
expect(count).toBeDefined();
expect(count!.properties.declaredType).toBe('int');
expect(count!.properties.isConst).toBe(true);
const name = vars.find((n) => n.name === 'name');
expect(name).toBeDefined();
// inferred `var` → no declaredType from a phantom field; mutable.
expect(name!.properties.declaredType).toBeUndefined();
expect(name!.properties.isMutable).toBe(true);
});
it('keeps the class instance field as a Property, not a Variable (regression)', () => {
const vars = getNodesByLabel(result, 'Variable');
expect(vars).not.toContain('z');
const props = getNodesByLabel(result, 'Property');
expect(props).toContain('z');
});
it('does NOT emit top-level vars as Property nodes (#1919 review CF4)', () => {
// Guards the `(program …)` vs `(declaration …)` anchor split: top-level
// siblings under `program` must surface as Variable, never Property. If the
// top-level anchor regressed to the class-field `(declaration …)` rule, these
// names would mis-classify as class Properties.
const props = getNodesByLabel(result, 'Property');
for (const name of ['count', 'a', 'b', 'name']) {
expect(props).not.toContain(name);
}
});
});
// ---------------------------------------------------------------------------
// Heritage cross-file simple-name collision (PR #1970 tri-review P2).
// console_logger.dart and file_logger.dart each declare `class Logger`; each
// file's service `implements Logger`. emitDartHeritageEdges resolves the base
// with same-file affinity, so each IMPLEMENTS edge must target its OWN file's
// Logger (not a globally last-written one). Parity — both modes resolve same-file.
// ---------------------------------------------------------------------------
describe.skipIf(!dartAvailable)('Dart heritage cross-file name collision', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'dart-heritage-name-collision'),
() => {},
);
}, 60000);
it('resolves implements to the same-file class on a name collision', () => {
const impl = getRelationships(result, 'IMPLEMENTS');
const consoleEdge = impl.find((e) => e.source === 'ConsoleService' && e.target === 'Logger');
expect(consoleEdge).toBeDefined();
expect(consoleEdge!.targetFilePath).toContain('console_logger.dart');
const fileEdge = impl.find((e) => e.source === 'FileService' && e.target === 'Logger');
expect(fileEdge).toBeDefined();
expect(fileEdge!.targetFilePath).toContain('file_logger.dart');
});
});
// ---------------------------------------------------------------------------
// CF3 (#1919 review): Dart class getters/setters keep their class owner edge.
// ---------------------------------------------------------------------------
// A Dart accessor's name lives under `method_signature`; the CF3 owner-strip
// guard must NOT treat that signature as an executable body and strip the
// HAS_PROPERTY owner (the over-strip regression this guards against).
describe('CF3 — Dart class accessors keep their class owner', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'dart-accessor-owner'), () => {});
}, 60000);
it('owns the getter/setter property `answer` and the stored field under Box', () => {
const owned = getRelationships(result, 'HAS_PROPERTY')
.filter((e) => e.source === 'Box')
.map((e) => e.target)
.sort();
expect(owned).toContain('answer');
expect(owned).toContain('normalField');
});
});