/** * 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'; import { pubspecWalkAnchored } from '../../../src/core/ingestion/languages/dart/package-config.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; } } describe.skipIf(!dartAvailable || !pubspecWalkAnchored())( 'Dart pubspec package identity (#2963)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'dart-package-imports'), () => {}); }, 60000); it('emits declared imports and their package identity dependencies', () => { const imports = getRelationships(result, 'IMPORTS') .filter((edge) => edge.sourceFilePath === 'lib/main.dart') .map((edge) => edge.targetFilePath) .sort(); expect(imports).toEqual([ 'lib/models.dart', 'lib/relative.dart', 'packages/data/lib/models.dart', 'packages/data/pubspec.yaml', 'pubspec.yaml', ]); }); it.each([ ['loadOwn', 'lib/models.dart'], ['loadData', 'packages/data/lib/models.dart'], ['loadRelative', 'lib/relative.dart'], ])('resolves %s in the correct library', (name, file) => { const calls = getRelationships(result, 'CALLS').filter( (edge) => edge.sourceFilePath === 'lib/main.dart' && edge.target === name, ); expect(calls).toHaveLength(1); expect(calls[0]?.targetFilePath).toBe(file); }); }, ); // ── 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'); }); });