fix: close remaining Dart language support gaps (#524)
* fix: close remaining Dart language support gaps Four issues that were not addressed in PR #204: 1. extractFunctionName: add function_signature/method_signature handlers and add both to FUNCTION_NODE_TYPES. Without this, findEnclosingFunctionId cannot resolve Dart function scopes — all calls inside Dart functions have no sourceId, breaking CALLS edge attribution. 2. formal_parameter_list: add to paramListTypes in extractMethodSignature. Dart's tree-sitter grammar uses this node type (not formal_parameters), so parameter counting returns 0 for all Dart functions. 3. Write-access queries: add @assignment patterns for obj.field = value and this.field = value. Without these, no ACCESSES write edges are emitted for Dart code. 4. initialized_identifier guard in extractDartDeclaration: comma-separated declarations (String a, b, c) produce initialized_identifier nodes which are in DART_DECLARATION_NODE_TYPES but were unhandled — the type lives on the parent node. Also adds Dart column to the feature matrix in type-resolution-system.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(dart): field-type resolution, call attribution, import resolution, and integration tests Fixes five Dart language support gaps with integration tests and architectural alignment: **Tree-sitter queries** — Add field declaration patterns for typed and nullable class fields (`String name = ''`, `String? name`). Without these, Dart class fields were invisible to the pipeline (zero Property nodes, zero HAS_PROPERTY edges). **Import resolution** — Dart relative imports (`import 'models.dart'`) don't use a leading `./`. The standard resolver only recognises paths starting with `.` as relative; bare paths fell through to a Java-style dot-to-slash conversion that mangled `models.dart` into `models/dart`. Fix: prepend `./` before calling resolveStandard. **Call attribution** — Dart's tree-sitter grammar places `function_body` as a sibling of `function_signature`, not as a child wrapping both. The `findEnclosingFunction` parent-walk never found the function because the call lives inside `function_body` which is a sibling of the signature. Fix: add `enclosingFunctionFinder` hook to LanguageProvider interface (following the same strategy pattern as `labelOverride`), with the Dart-specific logic in `languages/dart.ts`. Both `parse-worker.ts` and `call-processor.ts` consume the hook generically — no Dart-specific code in the generic processors. **Receiver chain extraction** — Add `unconditional_assignable_selector` to `MEMBER_ACCESS_NODE_TYPES` so `inferCallForm` returns `'member'` for Dart method calls. Add Dart-specific receiver extraction blocks in `extractReceiverName`, `extractReceiverNode`, and a `selector` handler in `extractMixedChain` for Dart's flat sibling-selector model (vs the nested member-expression model used by all other languages). **Integration tests** — New `dart.test.ts` with field-type resolution and call-result-binding describe blocks. Fixtures: `dart-field-types/` (models.dart + app.dart) and `dart-call-result-binding/` (models.dart + app.dart). 9 passing tests, 1 skipped (ACCESSES edges for field reads depend on type-env parameter binding propagation — tracked for follow-up). --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
Gergo Magyar
parent
546128cdcb
commit
3c896cdbcd
@@ -209,6 +209,26 @@ const findEnclosingFunction = (
|
||||
return generateId(finalLabel, `${filePath}:${funcName}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Language-specific enclosing function resolution (e.g., Dart where
|
||||
// function_body is a sibling of function_signature, not a child).
|
||||
if (provider.enclosingFunctionFinder) {
|
||||
const customResult = provider.enclosingFunctionFinder(current);
|
||||
if (customResult) {
|
||||
// Try SymbolTable first (same pattern as the FUNCTION_NODE_TYPES branch above).
|
||||
const resolved = ctx.resolve(customResult.funcName, filePath);
|
||||
if (resolved?.tier === 'same-file' && resolved.candidates.length > 0) {
|
||||
return resolved.candidates[0].nodeId;
|
||||
}
|
||||
let finalLabel = customResult.label;
|
||||
if (provider.labelOverride) {
|
||||
const override = provider.labelOverride(current.previousSibling!, finalLabel);
|
||||
if (override !== null) finalLabel = override;
|
||||
}
|
||||
return generateId(finalLabel, `${filePath}:${customResult.funcName}`);
|
||||
}
|
||||
}
|
||||
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,12 @@ export function resolveDartImport(
|
||||
return null;
|
||||
}
|
||||
|
||||
// Relative imports — use standard resolution
|
||||
return resolveStandard(stripped, filePath, ctx, SupportedLanguages.Dart);
|
||||
// Relative imports — use standard resolution.
|
||||
// Dart relative imports don't require a leading "./" (e.g. `import 'models.dart'`).
|
||||
// The standard resolver only recognises paths starting with "." as relative, so
|
||||
// prepend "./" when the path doesn't already start with "." to ensure correct
|
||||
// same-directory resolution (without this, "models.dart" would be mangled by the
|
||||
// generic dot-to-slash conversion intended for Java-style package imports).
|
||||
const relPath = stripped.startsWith('.') ? stripped : './' + stripped;
|
||||
return resolveStandard(relPath, filePath, ctx, SupportedLanguages.Dart);
|
||||
}
|
||||
|
||||
@@ -79,6 +79,17 @@ interface LanguageProviderConfig {
|
||||
projectConfig: unknown,
|
||||
) => void;
|
||||
|
||||
// ── Enclosing function resolution ───────────────────────────────
|
||||
/** Resolve the enclosing function name + label from an AST ancestor node
|
||||
* that is NOT a standard FUNCTION_NODE_TYPE. For languages where the
|
||||
* function body is a sibling of the signature (e.g. Dart: function_body ↔
|
||||
* function_signature are siblings under program/class_body), the default
|
||||
* parent walk cannot find the enclosing function. This hook lets the
|
||||
* language provider inspect each ancestor and return the resolved result.
|
||||
* Return null to continue the default walk.
|
||||
* Default: undefined (standard parent walk only). */
|
||||
readonly enclosingFunctionFinder?: (ancestorNode: SyntaxNode) => { funcName: string; label: NodeLabel } | null;
|
||||
|
||||
// ── Labels ────────────────────────────────────────────────────────
|
||||
/** Override the default node label for definition.function captures.
|
||||
* Return null to skip (C/C++ duplicate), a different label to reclassify
|
||||
|
||||
@@ -5,8 +5,14 @@
|
||||
* - importSemantics: 'wildcard' (Dart imports bring everything public into scope)
|
||||
* - exportChecker: public if no leading underscore
|
||||
* - Dart SDK imports (dart:*) and external packages are skipped
|
||||
* - enclosingFunctionFinder: Dart's tree-sitter grammar places function_body
|
||||
* as a sibling of function_signature/method_signature (not as a child).
|
||||
* The hook resolves the enclosing function by inspecting the previous sibling.
|
||||
*/
|
||||
|
||||
import type { SyntaxNode } from '../utils/ast-helpers.js';
|
||||
import type { NodeLabel } from '../../graph/types.js';
|
||||
import { FUNCTION_NODE_TYPES, extractFunctionName } from '../utils/ast-helpers.js';
|
||||
import { SupportedLanguages } from '../../../config/supported-languages.js';
|
||||
import { defineLanguage } from '../language-provider.js';
|
||||
import { typeConfig as dartConfig } from '../type-extractors/dart.js';
|
||||
@@ -14,6 +20,23 @@ import { dartExportChecker } from '../export-detection.js';
|
||||
import { resolveDartImport } from '../import-resolvers/dart.js';
|
||||
import { DART_QUERIES } from '../tree-sitter-queries.js';
|
||||
|
||||
/**
|
||||
* Resolve the enclosing function from a `function_body` node by looking at its
|
||||
* previous sibling. In Dart's tree-sitter grammar, function_signature and
|
||||
* function_body are siblings under program or class_body, unlike most languages
|
||||
* where the function declaration wraps both.
|
||||
*
|
||||
* Delegates name extraction to the shared `extractFunctionName` which already
|
||||
* handles Dart's function_signature and method_signature node types.
|
||||
*/
|
||||
const dartEnclosingFunctionFinder = (node: SyntaxNode): { funcName: string; label: NodeLabel } | null => {
|
||||
if (node.type !== 'function_body') return null;
|
||||
const prev = node.previousSibling;
|
||||
if (!prev || !FUNCTION_NODE_TYPES.has(prev.type)) return null;
|
||||
const { funcName, label } = extractFunctionName(prev);
|
||||
return funcName ? { funcName, label } : null;
|
||||
};
|
||||
|
||||
const BUILT_INS: ReadonlySet<string> = new Set([
|
||||
'setState', 'mounted', 'debugPrint',
|
||||
'runApp', 'showDialog', 'showModalBottomSheet',
|
||||
@@ -31,5 +54,6 @@ export const dartProvider = defineLanguage({
|
||||
exportChecker: dartExportChecker,
|
||||
importResolver: resolveDartImport,
|
||||
importSemantics: 'wildcard',
|
||||
enclosingFunctionFinder: dartEnclosingFunctionFinder,
|
||||
builtInNames: BUILT_INS,
|
||||
});
|
||||
|
||||
@@ -1054,6 +1054,20 @@ export const DART_QUERIES = `
|
||||
(factory_constructor_signature
|
||||
(identifier) @name . (formal_parameter_list))) @definition.constructor
|
||||
|
||||
; ── Field declarations (String name = '', Address address = Address()) ──────
|
||||
(declaration
|
||||
(type_identifier)
|
||||
(initialized_identifier_list
|
||||
(initialized_identifier
|
||||
(identifier) @name))) @definition.property
|
||||
|
||||
; ── Nullable field declarations (String? name) ──────────────────────────────
|
||||
(declaration
|
||||
(nullable_type)
|
||||
(initialized_identifier_list
|
||||
(initialized_identifier
|
||||
(identifier) @name))) @definition.property
|
||||
|
||||
; ── Getters ──────────────────────────────────────────────────────────────────
|
||||
(method_signature
|
||||
(getter_signature
|
||||
@@ -1097,6 +1111,22 @@ export const DART_QUERIES = `
|
||||
(library_export
|
||||
(configurable_uri) @import.source)) @import
|
||||
|
||||
; ── Write access: obj.field = value ──────────────────────────────────────────
|
||||
(assignment_expression
|
||||
left: (assignable_expression
|
||||
(identifier) @assignment.receiver
|
||||
(unconditional_assignable_selector
|
||||
(identifier) @assignment.property))
|
||||
right: (_)) @assignment
|
||||
|
||||
; ── Write access: this.field = value ─────────────────────────────────────────
|
||||
(assignment_expression
|
||||
left: (assignable_expression
|
||||
(this) @assignment.receiver
|
||||
(unconditional_assignable_selector
|
||||
(identifier) @assignment.property))
|
||||
right: (_)) @assignment
|
||||
|
||||
; ── Heritage: extends ────────────────────────────────────────────────────────
|
||||
(class_definition
|
||||
name: (identifier) @heritage.class
|
||||
|
||||
@@ -111,6 +111,25 @@ function hasDartTypeAnnotation(node: SyntaxNode): boolean {
|
||||
// ── Tier 0: Explicit Type Annotations ───────────────────────────────────
|
||||
|
||||
const extractDartDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
|
||||
// initialized_identifier: comma-separated variable (String a, b, c) — type is on parent
|
||||
if (node.type === 'initialized_identifier') {
|
||||
const parent = node.parent;
|
||||
if (!parent) return;
|
||||
let typeNode = findChild(parent, 'type_identifier');
|
||||
if (!typeNode) {
|
||||
const nullable = findChild(parent, 'nullable_type');
|
||||
if (nullable) typeNode = findChild(nullable, 'type_identifier');
|
||||
}
|
||||
if (!typeNode) return;
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
if (!typeName || typeName === 'dynamic') return;
|
||||
const nameNode = findChild(node, 'identifier');
|
||||
if (!nameNode) return;
|
||||
const varName = extractVarName(nameNode);
|
||||
if (varName) env.set(varName, typeName);
|
||||
return;
|
||||
}
|
||||
|
||||
let typeNode = findChild(node, 'type_identifier');
|
||||
if (!typeNode) {
|
||||
const nullable = findChild(node, 'nullable_type');
|
||||
|
||||
@@ -82,6 +82,9 @@ export const FUNCTION_NODE_TYPES = new Set([
|
||||
// Ruby
|
||||
'method', // def foo
|
||||
'singleton_method', // def self.foo
|
||||
// Dart
|
||||
'function_signature',
|
||||
'method_signature',
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -430,6 +433,34 @@ export const extractFunctionName = (node: SyntaxNode): { funcName: string | null
|
||||
}
|
||||
funcName = nameNode?.text;
|
||||
label = 'Method';
|
||||
} else if (node.type === 'function_signature') {
|
||||
// Dart: top-level function signatures
|
||||
let nameNode = node.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (c?.type === 'identifier') { nameNode = c; break; }
|
||||
}
|
||||
}
|
||||
funcName = nameNode?.text ?? null;
|
||||
} else if (node.type === 'method_signature') {
|
||||
// Dart: method_signature wraps function_signature
|
||||
let funcSig: SyntaxNode | null = null;
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (c?.type === 'function_signature') { funcSig = c; break; }
|
||||
}
|
||||
if (funcSig) {
|
||||
let nameNode = funcSig.childForFieldName?.('name');
|
||||
if (!nameNode) {
|
||||
for (let i = 0; i < funcSig.childCount; i++) {
|
||||
const c = funcSig.child(i);
|
||||
if (c?.type === 'identifier') { nameNode = c; break; }
|
||||
}
|
||||
}
|
||||
funcName = nameNode?.text ?? null;
|
||||
}
|
||||
label = 'Method';
|
||||
}
|
||||
|
||||
return { funcName, label };
|
||||
@@ -471,6 +502,7 @@ export const extractMethodSignature = (node: SyntaxNode | null | undefined): Met
|
||||
const paramListTypes = new Set([
|
||||
'formal_parameters', 'parameters', 'parameter_list',
|
||||
'function_parameters', 'method_parameters', 'function_value_parameters',
|
||||
'formal_parameter_list', // Dart
|
||||
]);
|
||||
|
||||
// Node types that indicate variadic/rest parameters
|
||||
|
||||
@@ -64,6 +64,7 @@ const MEMBER_ACCESS_NODE_TYPES = new Set([
|
||||
'selector_expression', // Go: obj.Method()
|
||||
'navigation_suffix', // Kotlin/Swift: obj.method() — nameNode sits inside navigation_suffix
|
||||
'member_binding_expression', // C#: user?.Method() — null-conditional access
|
||||
'unconditional_assignable_selector', // Dart: obj.method() — nameNode inside selector > unconditional_assignable_selector
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -208,6 +209,16 @@ export const extractReceiverName = (
|
||||
}
|
||||
}
|
||||
|
||||
// Dart: unconditional_assignable_selector is inside a `selector`, which is a sibling
|
||||
// of the receiver in the expression_statement. For `user.save()`, the previous named
|
||||
// sibling of the `selector` is `identifier [user]`.
|
||||
if (!receiver && parent.type === 'unconditional_assignable_selector') {
|
||||
const selectorNode = parent.parent; // selector [.save]
|
||||
if (selectorNode) {
|
||||
receiver = selectorNode.previousNamedSibling;
|
||||
}
|
||||
}
|
||||
|
||||
// C# null-conditional: user?.Save() → conditional_access_expression wraps member_binding_expression
|
||||
if (!receiver && parent.type === 'member_binding_expression') {
|
||||
const condAccess = parent.parent;
|
||||
@@ -291,6 +302,14 @@ export const extractReceiverNode = (
|
||||
}
|
||||
}
|
||||
|
||||
// Dart: unconditional_assignable_selector — receiver is previous sibling of the selector
|
||||
if (!receiver && parent.type === 'unconditional_assignable_selector') {
|
||||
const selectorNode = parent.parent;
|
||||
if (selectorNode) {
|
||||
receiver = selectorNode.previousNamedSibling;
|
||||
}
|
||||
}
|
||||
|
||||
if (!receiver && parent.type === 'member_binding_expression') {
|
||||
const condAccess = parent.parent;
|
||||
if (condAccess?.type === 'conditional_access_expression') {
|
||||
@@ -527,6 +546,27 @@ export function extractMixedChain(
|
||||
} else {
|
||||
return { chain, baseReceiverName: innerObject.text || undefined };
|
||||
}
|
||||
} else if (current.type === 'selector') {
|
||||
// ── Dart: flat selector siblings (user.address.save() uses selector nodes) ──
|
||||
// Extract field name from unconditional_assignable_selector child
|
||||
const uas = current.namedChildren?.find(
|
||||
(c: SyntaxNode) => c.type === 'unconditional_assignable_selector',
|
||||
);
|
||||
const propertyName = uas?.namedChildren?.find(
|
||||
(c: SyntaxNode) => c.type === 'identifier',
|
||||
)?.text;
|
||||
if (!propertyName) break;
|
||||
chain.unshift({ kind: 'field', name: propertyName });
|
||||
|
||||
// Walk to previous sibling for the next step in the chain
|
||||
const prev = current.previousNamedSibling;
|
||||
if (!prev) break;
|
||||
if (prev.type === 'selector') {
|
||||
current = prev;
|
||||
} else {
|
||||
// Base receiver (identifier or other terminal)
|
||||
return { chain, baseReceiverName: prev.text || undefined };
|
||||
}
|
||||
} else {
|
||||
// Simple identifier — this is the base receiver
|
||||
return chain.length > 0
|
||||
|
||||
@@ -318,6 +318,23 @@ const findEnclosingFunctionId = (node: any, filePath: string, provider: Language
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Language-specific enclosing function resolution (e.g., Dart where
|
||||
// function_body is a sibling of function_signature, not a child).
|
||||
if (provider.enclosingFunctionFinder) {
|
||||
const customResult = provider.enclosingFunctionFinder(current);
|
||||
if (customResult) {
|
||||
let finalLabel: NodeLabel = customResult.label;
|
||||
if (provider.labelOverride) {
|
||||
const override = provider.labelOverride(current.previousSibling, finalLabel);
|
||||
if (override !== null) finalLabel = override;
|
||||
}
|
||||
const result = generateId(finalLabel, `${filePath}:${customResult.funcName}`);
|
||||
functionIdCache.set(node, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
current = current.parent;
|
||||
}
|
||||
functionIdCache.set(node, null);
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import 'models.dart';
|
||||
|
||||
void processUser() {
|
||||
var user = getUser('alice');
|
||||
user.save();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
class User {
|
||||
String name = '';
|
||||
|
||||
bool save() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
User getUser(String name) {
|
||||
return User();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import 'models.dart';
|
||||
|
||||
void processUser(User user) {
|
||||
user.address.save();
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
class Address {
|
||||
String city = '';
|
||||
|
||||
void save() {
|
||||
// persist address
|
||||
}
|
||||
}
|
||||
|
||||
class User {
|
||||
String name = '';
|
||||
Address address = Address();
|
||||
|
||||
String greet() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Remaining known Dart gaps (field-chain ACCESSES) are documented as
|
||||
* it.todo() tests to be filled when the pipeline is extended.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import path from 'path';
|
||||
import {
|
||||
FIXTURES, getRelationships, getNodesByLabel, edgeSet,
|
||||
runPipelineFromRepo, type PipelineResult,
|
||||
} from './helpers.js';
|
||||
import { isLanguageAvailable } from '../../../src/core/tree-sitter/parser-loader.js';
|
||||
import { SupportedLanguages } from '../../../src/config/supported-languages.js';
|
||||
|
||||
const dartAvailable = isLanguageAvailable(SupportedLanguages.Dart);
|
||||
|
||||
// ── 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);
|
||||
});
|
||||
|
||||
// Dart field-chain ACCESSES edges require the call-processor's chain-resolution
|
||||
// tier (Step 1c) to fire. This needs the type-env's scoped parameter binding
|
||||
// (processUser's `user: User`) to propagate to processCallsFromExtracted so
|
||||
// walkMixedChain can resolve User → address → Address and emit ACCESSES.
|
||||
// The chain extraction (extractMixedChain) and member detection
|
||||
// (MEMBER_ACCESS_NODE_TYPES) are wired, but the base receiver type lookup
|
||||
// from the type-env currently returns undefined for Dart function parameters
|
||||
// in the call-processor context. Tracked for follow-up.
|
||||
it.skip('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');
|
||||
}
|
||||
});
|
||||
});
|
||||
+21
-21
@@ -375,27 +375,27 @@ So return-type-aware receiver inference already exists in a constrained downstre
|
||||
|
||||
## Language Feature Matrix
|
||||
|
||||
| Feature | TS | JS | Java | Kotlin | C# | Go | Rust | Python | PHP | Ruby | Swift | C++ | C |
|
||||
|---------|:--:|:--:|:----:|:------:|:--:|:--:|:----:|:------:|:---:|:----:|:-----:|:---:|:-:|
|
||||
| Declarations | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Parameters | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Initializer / constructor inference | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Constructor binding scan | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| For-loop element types | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes††† | Yes | Yes |
|
||||
| Pattern binding | Yes | Yes | Yes | Yes | No | Yes | Yes | No | No | No | Partial‡‡‡ | No | No |
|
||||
| Assignment chains | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes |
|
||||
| Field/property type resolution | Yes | No† | Yes | Yes | Yes | Yes | Yes | Yes* | Yes | YARD | No | Yes | No‡ |
|
||||
| Comment-based types | JSDoc | JSDoc | No | No | No | No | No | No | PHPDoc | YARD | No | No | No |
|
||||
| Return type extraction | JSDoc | JSDoc | No | No | No | No | No | No | PHPDoc | YARD | No | No | No |
|
||||
| Call-result variable binding | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes¶ | Yes††† | Yes | No |
|
||||
| Field access binding | Yes | No† | Yes | Yes | Yes | Yes | Yes | No‖ | Yes | N/A | Yes††† | Yes | No |
|
||||
| Method-call-result binding | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes¶ | Yes††† | Yes | No |
|
||||
| Write access (ACCESSES write) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes§ | Yes | Yes | Yes | No |
|
||||
| Parameter types extracted | Yes** | No | Yes | Yes | Yes | Yes | Yes | Partial†† | No | No | No | Yes | No |
|
||||
| Method overload disambiguation | Yes** | No | Yes | Yes | Yes | No | No | No | No | No | No | Yes | No |
|
||||
| Constructor-visible virtual dispatch | Yes | No | Yes | Yes‡‡ | Yes | No | No | No | No | No | No | Yes§§ | No |
|
||||
| Optional parameter arity resolution | Yes | No | No | Yes | Yes | No | No | Yes | Yes | Yes | No | Yes | No |
|
||||
| Cross-file binding propagation | Yes | Yes | Yes‖‖ | Yes | Yes¶¶ | Yes*** | Yes | Yes | Partial | Yes*** | Yes*** | Yes*** | Yes*** |
|
||||
| Feature | TS | JS | Java | Kotlin | C# | Go | Rust | Python | PHP | Ruby | Swift | C++ | C | Dart |
|
||||
|---------|:--:|:--:|:----:|:------:|:--:|:--:|:----:|:------:|:---:|:----:|:-----:|:---:|:-:|:----:|
|
||||
| Declarations | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Parameters | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Initializer / constructor inference | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Constructor binding scan | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| For-loop element types | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes††† | Yes | Yes | Yes |
|
||||
| Pattern binding | Yes | Yes | Yes | Yes | No | Yes | Yes | No | No | No | Partial‡‡‡ | No | No | No |
|
||||
| Assignment chains | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes | Yes | Yes | Yes |
|
||||
| Field/property type resolution | Yes | No† | Yes | Yes | Yes | Yes | Yes | Yes* | Yes | YARD | No | Yes | No‡ | No |
|
||||
| Comment-based types | JSDoc | JSDoc | No | No | No | No | No | No | PHPDoc | YARD | No | No | No | No |
|
||||
| Return type extraction | JSDoc | JSDoc | No | No | No | No | No | No | PHPDoc | YARD | No | No | No | No |
|
||||
| Call-result variable binding | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes¶ | Yes††† | Yes | No | Yes |
|
||||
| Field access binding | Yes | No† | Yes | Yes | Yes | Yes | Yes | No‖ | Yes | N/A | Yes††† | Yes | No | Yes |
|
||||
| Method-call-result binding | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes¶ | Yes††† | Yes | No | Yes |
|
||||
| Write access (ACCESSES write) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes§ | Yes | Yes | Yes | No | Yes |
|
||||
| Parameter types extracted | Yes** | No | Yes | Yes | Yes | Yes | Yes | Partial†† | No | No | No | Yes | No | No |
|
||||
| Method overload disambiguation | Yes** | No | Yes | Yes | Yes | No | No | No | No | No | No | Yes | No | No |
|
||||
| Constructor-visible virtual dispatch | Yes | No | Yes | Yes‡‡ | Yes | No | No | No | No | No | No | Yes§§ | No | Yes |
|
||||
| Optional parameter arity resolution | Yes | No | No | Yes | Yes | No | No | Yes | Yes | Yes | No | Yes | No | No |
|
||||
| Cross-file binding propagation | Yes | Yes | Yes‖‖ | Yes | Yes¶¶ | Yes*** | Yes | Yes | Partial | Yes*** | Yes*** | Yes*** | Yes*** | Yes*** |
|
||||
|
||||
\* Python class-level annotated attributes (`address: Address`) now resolve `declaredType` correctly. The `self.x` instance attribute pattern is not yet supported.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user