feat: add Dart language support (#204)

This commit is contained in:
Wojciech Guziak 2026-03-26 09:42:49 +01:00 committed by GitHub
parent 1a0784befe
commit 8864a0290c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 1753 additions and 5 deletions

View file

@ -12,4 +12,5 @@ export enum SupportedLanguages {
Ruby = 'ruby',
Kotlin = 'kotlin',
Swift = 'swift',
}
Dart = 'dart',
}

View file

@ -41,6 +41,7 @@ export const callRouters = {
[SupportedLanguages.C]: noRouting,
[SupportedLanguages.Ruby]: routeRubyCall,
[SupportedLanguages.Kotlin]: noRouting,
[SupportedLanguages.Dart]: noRouting,
} satisfies Record<SupportedLanguages, CallRouter>;
// ── Result types ────────────────────────────────────────────────────────────

View file

@ -315,6 +315,33 @@ export function detectFrameworkFromPath(filePath: string): FrameworkHint | null
return { framework: 'ios-router', entryPointMultiplier: 2.0, reason: 'ios-router' };
}
// ========== DART / FLUTTER ==========
// Flutter main entry point
if (p.endsWith('/main.dart')) {
return { framework: 'flutter', entryPointMultiplier: 3.0, reason: 'flutter-main' };
}
// Flutter screens/pages (high priority - route entry points)
if ((p.includes('/lib/screens/') || p.includes('/lib/pages/')) && p.endsWith('.dart')) {
return { framework: 'flutter', entryPointMultiplier: 2.5, reason: 'flutter-screen' };
}
// Flutter BLoC / controllers (state management entry points)
if ((p.includes('/lib/bloc/') || p.includes('/lib/controllers/') || p.includes('/lib/cubit/')) && p.endsWith('.dart')) {
return { framework: 'flutter', entryPointMultiplier: 2.0, reason: 'flutter-state-management' };
}
// Flutter services
if (p.includes('/lib/services/') && p.endsWith('.dart')) {
return { framework: 'flutter', entryPointMultiplier: 1.8, reason: 'flutter-service' };
}
// Flutter widgets (reusable components)
if (p.includes('/lib/widgets/') && p.endsWith('.dart')) {
return { framework: 'flutter', entryPointMultiplier: 1.5, reason: 'flutter-widget' };
}
// ========== GENERIC PATTERNS ==========
// Any language: index files in API folders
@ -366,6 +393,10 @@ export const FRAMEWORK_AST_PATTERNS = {
'axum': ['Router::new'],
'rocket': ['#[get', '#[post'],
// Dart/Flutter
'flutter': ['StatelessWidget', 'StatefulWidget', 'BuildContext', 'Widget build',
'ChangeNotifier', 'GetxController', 'Cubit<', 'Bloc<'],
// Swift/iOS
'uikit': ['viewDidLoad', 'viewWillAppear', 'viewDidAppear', 'UIViewController'],
'swiftui': ['@main', 'WindowGroup', 'ContentView', '@StateObject', '@ObservedObject'],

View file

@ -109,6 +109,10 @@ const isNodeExported = (node: any, name: string, language: string): boolean => {
case 'ruby':
return true;
// Dart: Public if no leading underscore (same convention as Python)
case 'dart':
return !name.startsWith('_');
default:
return false;
}

View file

@ -483,6 +483,102 @@ export const SWIFT_QUERIES = `
(inheritance_specifier inherits_from: (user_type (type_identifier) @heritage.extends))) @heritage
`;
// Dart queries - works with tree-sitter-dart
export const DART_QUERIES = `
; Classes
(class_definition
name: (identifier) @name) @definition.class
; Mixins
(mixin_declaration
(identifier) @name) @definition.trait
; Extensions
(extension_declaration
name: (identifier) @name) @definition.class
; Enums
(enum_declaration
name: (identifier) @name) @definition.enum
; Type aliases
; Anchor "=" after the name to avoid capturing the RHS type
(type_alias
(type_identifier) @name
"=") @definition.type
; Top-level functions (parent is program, not method_signature)
(program
(function_signature
name: (identifier) @name) @definition.function)
; Abstract method declarations (function_signature inside class body declaration)
(declaration
(function_signature
name: (identifier) @name)) @definition.method
; Methods (inside class/mixin/extension bodies)
(method_signature
(function_signature
name: (identifier) @name)) @definition.method
; Constructors
(constructor_signature
name: (identifier) @name) @definition.constructor
; Factory constructors
(method_signature
(factory_constructor_signature
(identifier) @name)) @definition.constructor
; Getters
(method_signature
(getter_signature
name: (identifier) @name)) @definition.property
; Setters
(method_signature
(setter_signature
name: (identifier) @name)) @definition.property
; Imports
(import_or_export
(library_import
(import_specification
(configurable_uri) @import.source))) @import
; Calls: direct function/constructor calls (identifier immediately before argument_part)
(expression_statement
(identifier) @call.name
.
(selector (argument_part))) @call
; Calls: method calls (obj.method())
(expression_statement
(selector
(unconditional_assignable_selector
(identifier) @call.name))) @call
; Heritage: extends
(class_definition
name: (identifier) @heritage.class
superclass: (superclass
(type_identifier) @heritage.extends)) @heritage
; Heritage: implements
(class_definition
name: (identifier) @heritage.class
interfaces: (interfaces
(type_identifier) @heritage.implements)) @heritage.impl
; Heritage: with (mixins)
(class_definition
name: (identifier) @heritage.class
superclass: (superclass
(mixins
(type_identifier) @heritage.trait))) @heritage
`;
export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
[SupportedLanguages.TypeScript]: TYPESCRIPT_QUERIES,
[SupportedLanguages.JavaScript]: JAVASCRIPT_QUERIES,
@ -497,5 +593,6 @@ export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
[SupportedLanguages.Ruby]: RUBY_QUERIES,
[SupportedLanguages.Kotlin]: '', // Kotlin WASM parser not yet available for web
[SupportedLanguages.Swift]: SWIFT_QUERIES,
[SupportedLanguages.Dart]: DART_QUERIES,
};

View file

@ -45,6 +45,7 @@ export const getLanguageFromFilename = (filename: string): SupportedLanguages |
}
// Swift
if (filename.endsWith('.swift')) return SupportedLanguages.Swift;
if (filename.endsWith('.dart')) return SupportedLanguages.Dart;
return null;
};

View file

@ -43,6 +43,7 @@ const getWasmPath = (language: SupportedLanguages, filePath?: string): string =>
[SupportedLanguages.Ruby]: '/wasm/ruby/tree-sitter-ruby.wasm',
[SupportedLanguages.Kotlin]: '', // Kotlin WASM parser not yet available for web
[SupportedLanguages.Swift]: '/wasm/swift/tree-sitter-swift.wasm',
[SupportedLanguages.Dart]: '/wasm/dart/tree-sitter-dart.wasm',
};
return languageFileMap[language];

View file

@ -58,6 +58,7 @@
"node": ">=20.0.0"
},
"optionalDependencies": {
"tree-sitter-dart": "github:UserNobody14/tree-sitter-dart#0fc19c3a57b1109802af41d2b8f60d8835c5da3a",
"tree-sitter-kotlin": "^0.3.8",
"tree-sitter-swift": "0.7.1"
}
@ -5116,6 +5117,33 @@
}
}
},
"node_modules/tree-sitter-dart": {
"version": "1.0.0",
"resolved": "git+ssh://git@github.com/UserNobody14/tree-sitter-dart.git#0fc19c3a57b1109802af41d2b8f60d8835c5da3a",
"integrity": "sha512-tdKxw1KmBWOT1DrqjKpn2Qj8nCpfza72fiDwKpUncG98YOrQ1A8kfLioYFEsQlXmDhF0yLsQ52LqFoOjOLq9JQ==",
"hasInstallScript": true,
"license": "ISC",
"optional": true,
"dependencies": {
"node-addon-api": "^7.1.0",
"node-gyp-build": "^4.8.0"
},
"peerDependencies": {
"tree-sitter": "^0.25.0"
},
"peerDependenciesMeta": {
"tree_sitter": {
"optional": true
}
}
},
"node_modules/tree-sitter-dart/node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT",
"optional": true
},
"node_modules/tree-sitter-go": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/tree-sitter-go/-/tree-sitter-go-0.25.0.tgz",

View file

@ -80,6 +80,7 @@
"uuid": "^13.0.0"
},
"optionalDependencies": {
"tree-sitter-dart": "github:UserNobody14/tree-sitter-dart#0fc19c3a57b1109802af41d2b8f60d8835c5da3a",
"tree-sitter-kotlin": "^0.3.8",
"tree-sitter-swift": "0.7.1"
},

View file

@ -41,4 +41,5 @@ export enum SupportedLanguages {
PHP = 'php',
Kotlin = 'kotlin',
Swift = 'swift',
}
Dart = 'dart',
}

View file

@ -212,6 +212,20 @@ export const ENTRY_POINT_PATTERNS = {
/^perform$/, // Background jobs (Sidekiq, ActiveJob)
/^execute$/, // Command pattern
],
// Dart / Flutter
[SupportedLanguages.Dart]: [
/^main$/, // App entry
/^build$/, // Widget.build — fundamental Flutter render entry point
/^createState$/, // StatefulWidget.createState
/^initState$/, // State lifecycle initialization
/^dispose$/, // State lifecycle teardown
/^didChangeDependencies$/, // State lifecycle — InheritedWidget changes
/^didUpdateWidget$/, // State lifecycle — widget rebuild with new config
/^runApp$/, // App entry point
/^onEvent$/, // BLoC event handler
/^mapEventToState$/, // Legacy BLoC pattern
],
} satisfies Record<SupportedLanguages, RegExp[]>;
/** Pre-computed merged patterns (universal + language-specific) to avoid per-call array allocation. */

View file

@ -217,3 +217,6 @@ export const swiftExportChecker: ExportChecker = (node, _name) => {
/** Ruby: all top-level definitions are public (no export syntax). */
export const rubyExportChecker: ExportChecker = (_node, _name) => true;
/** Dart: public if no leading underscore (convention, same as Python). */
export const dartExportChecker: ExportChecker = (_node, name) => !name.startsWith('_');

View file

@ -406,6 +406,38 @@ export function detectFrameworkFromPath(filePath: string): FrameworkHint | null
return { framework: 'ios-router', entryPointMultiplier: 2.0, reason: 'ios-router' };
}
// ========== DART / FLUTTER ==========
// Flutter main/app entry points
if (p.endsWith('/main.dart') || p.endsWith('/app.dart')) {
return { framework: 'flutter', entryPointMultiplier: 3.0, reason: 'flutter-main' };
}
// Flutter screens/pages/views (high priority - route entry points)
if ((p.includes('/screens/') || p.includes('/pages/') || p.includes('/views/')) && p.endsWith('.dart')) {
return { framework: 'flutter', entryPointMultiplier: 2.5, reason: 'flutter-screen' };
}
// Flutter routes
if (p.includes('/routes/') && p.endsWith('.dart')) {
return { framework: 'flutter', entryPointMultiplier: 2.5, reason: 'flutter-routes' };
}
// Flutter BLoC / controllers / presentation (state management entry points)
if ((p.includes('/bloc/') || p.includes('/controllers/') || p.includes('/cubit/') || p.includes('/presentation/')) && p.endsWith('.dart')) {
return { framework: 'flutter', entryPointMultiplier: 2.0, reason: 'flutter-state-management' };
}
// Flutter services / domain
if ((p.includes('/services/') || p.includes('/domain/')) && p.endsWith('.dart')) {
return { framework: 'flutter', entryPointMultiplier: 1.8, reason: 'flutter-service' };
}
// Flutter widgets (reusable components)
if (p.includes('/widgets/') && p.endsWith('.dart')) {
return { framework: 'flutter', entryPointMultiplier: 1.5, reason: 'flutter-widget' };
}
// ========== GENERIC PATTERNS ==========
// Any language: index files in API folders
@ -478,6 +510,11 @@ export const FRAMEWORK_AST_PATTERNS = {
'rails': ['ApplicationController', 'ApplicationRecord', 'ActiveRecord::Base',
'before_action', 'after_action', 'has_many', 'belongs_to', 'has_one', 'validates'],
'sinatra': ['Sinatra::Base', 'Sinatra::Application'],
// Dart/Flutter
'flutter': ['StatelessWidget', 'StatefulWidget', 'BuildContext', 'Widget build',
'ChangeNotifier', 'GetxController', 'Cubit<', 'Bloc<', 'ConsumerWidget'],
'riverpod': ['@riverpod', 'ref.watch', 'ref.read', 'AsyncNotifier', 'Notifier'],
};
interface AstFrameworkPatternConfig {
@ -545,6 +582,10 @@ export const AST_FRAMEWORK_PATTERNS_BY_LANGUAGE = {
{ framework: 'rails', entryPointMultiplier: 3.0, reason: 'rails-pattern', patterns: FRAMEWORK_AST_PATTERNS.rails },
{ framework: 'sinatra', entryPointMultiplier: 2.8, reason: 'sinatra-pattern', patterns: FRAMEWORK_AST_PATTERNS.sinatra },
],
[SupportedLanguages.Dart]: [
{ framework: 'flutter', entryPointMultiplier: 2.5, reason: 'flutter-widget', patterns: FRAMEWORK_AST_PATTERNS.flutter },
{ framework: 'riverpod', entryPointMultiplier: 2.8, reason: 'riverpod-pattern', patterns: FRAMEWORK_AST_PATTERNS.riverpod },
],
} satisfies Record<SupportedLanguages, AstFrameworkPatternConfig[]>;
/** Pre-lowercased patterns for O(1) pattern matching at runtime */

View file

@ -0,0 +1,44 @@
/**
* Dart import resolution.
* Handles package: imports (local packages) and relative imports.
* SDK imports (dart:*) and external packages are skipped.
*/
import type { ImportResult, ResolveCtx } from './types.js';
import { resolveStandard } from './standard.js';
import { SupportedLanguages } from '../../../config/supported-languages.js';
export function resolveDartImport(
rawImportPath: string,
filePath: string,
ctx: ResolveCtx,
): ImportResult {
// Strip surrounding quotes from configurable_uri capture
const stripped = rawImportPath.replace(/^['"]|['"]$/g, '');
// Skip dart: SDK imports (dart:async, dart:io, etc.)
if (stripped.startsWith('dart:')) return null;
// Local package: imports → resolve to lib/<path>
if (stripped.startsWith('package:')) {
const slashIdx = stripped.indexOf('/');
if (slashIdx === -1) return null;
const relPath = stripped.slice(slashIdx + 1);
const candidates = [`lib/${relPath}`, relPath];
const files: string[] = [];
for (const candidate of candidates) {
for (const fp of ctx.allFileList) {
if (fp.endsWith('/' + candidate) || fp === candidate) {
files.push(fp);
break;
}
}
if (files.length > 0) break;
}
if (files.length > 0) return { kind: 'files', files };
return null;
}
// Relative imports — use standard resolution
return resolveStandard(stripped, filePath, ctx, SupportedLanguages.Dart);
}

View file

@ -0,0 +1,25 @@
/**
* Dart Language Provider
*
* Dart traits:
* - importSemantics: 'wildcard' (Dart imports bring everything public into scope)
* - exportChecker: public if no leading underscore
* - Dart SDK imports (dart:*) and external packages are skipped
*/
import { SupportedLanguages } from '../../../config/supported-languages.js';
import { defineLanguage } from '../language-provider.js';
import { typeConfig as dartConfig } from '../type-extractors/dart.js';
import { dartExportChecker } from '../export-detection.js';
import { resolveDartImport } from '../import-resolvers/dart.js';
import { DART_QUERIES } from '../tree-sitter-queries.js';
export const dartProvider = defineLanguage({
id: SupportedLanguages.Dart,
extensions: ['.dart'],
treeSitterQueries: DART_QUERIES,
typeConfig: dartConfig,
exportChecker: dartExportChecker,
importResolver: resolveDartImport,
importSemantics: 'wildcard',
});

View file

@ -22,6 +22,7 @@ import { cProvider, cppProvider } from './c-cpp.js';
import { phpProvider } from './php.js';
import { rubyProvider } from './ruby.js';
import { swiftProvider } from './swift.js';
import { dartProvider } from './dart.js';
export const providers = {
[SupportedLanguages.JavaScript]: javascriptProvider,
@ -37,6 +38,7 @@ export const providers = {
[SupportedLanguages.PHP]: phpProvider,
[SupportedLanguages.Ruby]: rubyProvider,
[SupportedLanguages.Swift]: swiftProvider,
[SupportedLanguages.Dart]: dartProvider,
} satisfies Record<SupportedLanguages, LanguageProvider>;
/** Get provider by language enum (always succeeds for SupportedLanguages). */

View file

@ -1001,4 +1001,137 @@ export const SWIFT_QUERIES = `
`;
// Dart queries - works with tree-sitter-dart (UserNobody14/tree-sitter-dart, ABI 14)
// Note: Dart grammar has function_signature/method_signature as wrappers;
// top-level functions are (program > function_signature),
// methods inside classes are (method_signature > function_signature).
// We match top-level functions via (program (function_signature ...)) to avoid
// double-counting methods that also contain function_signature.
export const DART_QUERIES = `
; Classes
(class_definition
name: (identifier) @name) @definition.class
; Mixins
(mixin_declaration
(identifier) @name) @definition.trait
; Extensions
(extension_declaration
name: (identifier) @name) @definition.class
; Enums
(enum_declaration
name: (identifier) @name) @definition.enum
; Type aliases
; Anchor "=" after the name to avoid capturing the RHS type
(type_alias
(type_identifier) @name
"=") @definition.type
; Top-level functions (parent is program, not method_signature)
(program
(function_signature
name: (identifier) @name) @definition.function)
; Abstract method declarations (function_signature inside class body declaration)
(declaration
(function_signature
name: (identifier) @name)) @definition.method
; Methods (inside class/mixin/extension bodies)
(method_signature
(function_signature
name: (identifier) @name)) @definition.method
; Constructors
(constructor_signature
name: (identifier) @name) @definition.constructor
; Factory constructors (anchor before param list to capture variant name, not class)
(method_signature
(factory_constructor_signature
(identifier) @name . (formal_parameter_list))) @definition.constructor
; Getters
(method_signature
(getter_signature
name: (identifier) @name)) @definition.property
; Setters
(method_signature
(setter_signature
name: (identifier) @name)) @definition.property
; Imports
(import_or_export
(library_import
(import_specification
(configurable_uri) @import.source))) @import
; Calls: direct function/constructor calls (identifier immediately before argument_part)
(expression_statement
(identifier) @call.name
.
(selector (argument_part))) @call
; Calls: method calls (obj.method())
(expression_statement
(selector
(unconditional_assignable_selector
(identifier) @call.name))) @call
; Calls: in return statements (return User())
(return_statement
(identifier) @call.name
(selector (argument_part))) @call
; Calls: in variable assignments (var x = getUser())
(initialized_variable_definition
value: (identifier) @call.name
(selector (argument_part))) @call
; Re-exports (export 'foo.dart')
(import_or_export
(library_export
(configurable_uri) @import.source)) @import
; Heritage: extends
(class_definition
name: (identifier) @heritage.class
superclass: (superclass
(type_identifier) @heritage.extends)) @heritage
; Heritage: implements
(class_definition
name: (identifier) @heritage.class
interfaces: (interfaces
(type_identifier) @heritage.implements)) @heritage.impl
; Heritage: with (mixins)
(class_definition
name: (identifier) @heritage.class
superclass: (superclass
(mixins
(type_identifier) @heritage.trait))) @heritage
`;
import { SupportedLanguages } from '../../config/supported-languages.js';
export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
[SupportedLanguages.TypeScript]: TYPESCRIPT_QUERIES,
[SupportedLanguages.JavaScript]: JAVASCRIPT_QUERIES,
[SupportedLanguages.Python]: PYTHON_QUERIES,
[SupportedLanguages.Java]: JAVA_QUERIES,
[SupportedLanguages.C]: C_QUERIES,
[SupportedLanguages.Go]: GO_QUERIES,
[SupportedLanguages.CPlusPlus]: CPP_QUERIES,
[SupportedLanguages.CSharp]: CSHARP_QUERIES,
[SupportedLanguages.Rust]: RUST_QUERIES,
[SupportedLanguages.PHP]: PHP_QUERIES,
[SupportedLanguages.Kotlin]: KOTLIN_QUERIES,
[SupportedLanguages.Ruby]: RUBY_QUERIES,
[SupportedLanguages.Swift]: SWIFT_QUERIES,
[SupportedLanguages.Dart]: DART_QUERIES,
};

View file

@ -0,0 +1,365 @@
/**
* Dart type extractor full implementation following type-resolution-system.md.
*
* Tier 0: Explicit type annotations (User user = ...)
* Tier 0b: For-loop element types (for (var u in users))
* Tier 1: Constructor/initializer inference (var user = User())
* Tier 2: Assignment chain propagation (copy, fieldAccess, callResult, methodCallResult)
*
* Handles tree-sitter-dart's flat sibling AST structure:
* identifier + selector + selector (not nested call_expression).
*
* Credit: Type resolution approach adapted from @xFlaviews' PR #83.
*/
import type { SyntaxNode } from '../utils/ast-helpers.js';
import type {
LanguageTypeConfig,
ParameterExtractor,
TypeBindingExtractor,
InitializerExtractor,
ClassNameLookup,
ConstructorBindingScanner,
PendingAssignmentExtractor,
ForLoopExtractor,
LiteralTypeInferrer,
ConstructorTypeDetector,
} from './types.js';
import { extractSimpleTypeName, extractVarName, extractElementTypeFromString, resolveIterableElementType } from './shared.js';
import { findChild } from '../utils/ast-helpers.js';
// ── Node types ──────────────────────────────────────────────────────────
const DART_DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
'initialized_variable_definition',
'initialized_identifier',
]);
const DART_FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set([
'for_statement',
]);
// ── Helpers ─────────────────────────────────────────────────────────────
interface DartRHS {
callee?: string;
member?: string;
hasCall: boolean;
isAwait: boolean;
}
function parseDartRHSChildren(children: Iterable<SyntaxNode>): Omit<DartRHS, 'isAwait'> {
let callee: string | undefined;
let member: string | undefined;
let hasCall = false;
for (const child of children) {
if (child.type === 'identifier' && !callee) {
callee = child.text;
continue;
}
if (child.type === 'selector') {
const uas = findChild(child, 'unconditional_assignable_selector')
?? findChild(child, 'conditional_assignable_selector');
if (uas) {
const id = findChild(uas, 'identifier');
if (id && !member) member = id.text;
continue;
}
if (findChild(child, 'argument_part')) {
hasCall = true;
continue;
}
}
}
return { callee, member, hasCall };
}
function parseDartRHS(node: SyntaxNode): DartRHS {
const rhsChildren: SyntaxNode[] = [];
let foundEquals = false;
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (!child) continue;
if (!child.isNamed && child.text === '=') { foundEquals = true; continue; }
if (foundEquals) rhsChildren.push(child);
}
if (rhsChildren.length === 0) return { hasCall: false, isAwait: false };
const first = rhsChildren[0];
if (first.type === 'unary_expression') {
const awaitExpr = findChild(first, 'await_expression');
if (awaitExpr) {
const innerChildren: SyntaxNode[] = [];
for (let i = 0; i < awaitExpr.namedChildCount; i++) {
const c = awaitExpr.namedChild(i);
if (c && c.type !== 'await') innerChildren.push(c);
}
return { ...parseDartRHSChildren(innerChildren), isAwait: true };
}
}
return { ...parseDartRHSChildren(rhsChildren), isAwait: false };
}
function hasDartTypeAnnotation(node: SyntaxNode): boolean {
return !!(findChild(node, 'type_identifier') || findChild(node, 'nullable_type'));
}
// ── Tier 0: Explicit Type Annotations ───────────────────────────────────
const extractDartDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
let typeNode = findChild(node, 'type_identifier');
if (!typeNode) {
const nullable = findChild(node, 'nullable_type');
if (nullable) typeNode = findChild(nullable, 'type_identifier');
}
if (!typeNode) return;
const typeName = extractSimpleTypeName(typeNode);
if (!typeName || typeName === 'dynamic') return;
const nameNode = node.childForFieldName('name');
if (!nameNode) return;
const varName = extractVarName(nameNode);
if (varName) env.set(varName, typeName);
};
const extractDartParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
let typeNode = findChild(node, 'type_identifier');
if (!typeNode) {
const nullable = findChild(node, 'nullable_type');
if (nullable) typeNode = findChild(nullable, 'type_identifier');
}
if (!typeNode) return;
const typeName = extractSimpleTypeName(typeNode);
if (!typeName || typeName === 'dynamic') return;
const nameNode = node.childForFieldName('name');
if (!nameNode) return;
const varName = extractVarName(nameNode);
if (varName) env.set(varName, typeName);
};
// ── Tier 1: Constructor / Initializer Inference ─────────────────────────
const extractDartInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<string, string>, classNames: ClassNameLookup): void => {
if (node.type !== 'initialized_variable_definition') return;
if (hasDartTypeAnnotation(node)) return;
const nameNode = node.childForFieldName('name');
if (!nameNode) return;
const varName = extractVarName(nameNode);
if (!varName || env.has(varName)) return;
const rhs = parseDartRHS(node);
if (!rhs.callee || !rhs.hasCall) return;
if (!rhs.member && classNames.has(rhs.callee)) {
env.set(varName, rhs.callee);
return;
}
if (rhs.member && classNames.has(rhs.callee)) {
env.set(varName, rhs.callee);
}
};
// ── Constructor Binding Scan ────────────────────────────────────────────
const scanDartConstructorBinding: ConstructorBindingScanner = (node) => {
if (node.type !== 'initialized_variable_definition') return undefined;
if (hasDartTypeAnnotation(node)) return undefined;
const nameNode = node.childForFieldName('name');
if (!nameNode) return undefined;
const varName = nameNode.text;
if (!varName) return undefined;
const rhs = parseDartRHS(node);
if (!rhs.callee) return undefined;
if (rhs.hasCall && !rhs.member) return { varName, calleeName: rhs.callee };
if (rhs.hasCall && rhs.member) return { varName, calleeName: rhs.member };
return undefined;
};
// ── Virtual Dispatch ────────────────────────────────────────────────────
const detectDartConstructorType: ConstructorTypeDetector = (node, classNames) => {
if (node.type !== 'initialized_variable_definition') return undefined;
const rhs = parseDartRHS(node);
if (!rhs.callee || !rhs.hasCall) return undefined;
if (!rhs.member && classNames.has(rhs.callee)) return rhs.callee;
if (rhs.member && classNames.has(rhs.callee)) return rhs.callee;
return undefined;
};
// ── Literal Type Inference ──────────────────────────────────────────────
const inferDartLiteralType: LiteralTypeInferrer = (node) => {
switch (node.type) {
case 'decimal_integer_literal':
case 'hex_integer_literal':
return 'int';
case 'decimal_floating_point_literal':
return 'double';
case 'string_literal':
return 'String';
case 'true':
case 'false':
return 'bool';
case 'null_literal':
return 'null';
default:
return undefined;
}
};
// ── Tier 2: Assignment Chain Propagation ─────────────────────────────────
const extractDartPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) => {
if (node.type !== 'initialized_variable_definition') return undefined;
if (hasDartTypeAnnotation(node)) return undefined;
const nameNode = node.childForFieldName('name');
if (!nameNode) return undefined;
const lhs = nameNode.text;
if (!lhs || scopeEnv.has(lhs)) return undefined;
const rhs = parseDartRHS(node);
if (!rhs.callee) return undefined;
if (!rhs.hasCall && !rhs.member) return { kind: 'copy', lhs, rhs: rhs.callee };
if (!rhs.hasCall && rhs.member) return { kind: 'fieldAccess', lhs, receiver: rhs.callee, field: rhs.member };
if (rhs.hasCall && !rhs.member) return { kind: 'callResult', lhs, callee: rhs.callee };
if (rhs.hasCall && rhs.member) return { kind: 'methodCallResult', lhs, receiver: rhs.callee, method: rhs.member };
return undefined;
};
// ── For-Loop Element Type Resolution ────────────────────────────────────
function extractDartElementTypeFromTypeNode(typeNode: SyntaxNode): string | undefined {
if (typeNode.type === 'type_identifier') {
const parent = typeNode.parent;
if (parent) {
const args = findChild(parent, 'type_arguments');
if (args && args.namedChildCount >= 1) {
const lastArg = args.namedChild(args.namedChildCount - 1);
if (lastArg) return extractSimpleTypeName(lastArg);
}
}
}
return undefined;
}
const extractDartForLoopBinding: ForLoopExtractor = (node, ctx): void => {
if (node.type !== 'for_statement') return;
const { scopeEnv, declarationTypeNodes, scope, returnTypeLookup } = ctx;
const loopParts = findChild(node, 'for_loop_parts');
if (!loopParts) return;
const nameNode = loopParts.childForFieldName('name');
if (!nameNode) return;
const loopVarName = nameNode.text;
if (!loopVarName) return;
const typeNode = findChild(loopParts, 'type_identifier');
if (typeNode) {
const typeName = extractSimpleTypeName(typeNode);
if (typeName && !scopeEnv.has(loopVarName)) {
(scopeEnv as Map<string, string>).set(loopVarName, typeName);
}
return;
}
const iterableNode = loopParts.childForFieldName('value');
if (!iterableNode) return;
let iterableName: string | undefined;
let callExprElementType: string | undefined;
if (iterableNode.type === 'identifier') {
iterableName = iterableNode.text;
} else if (iterableNode.type === 'unary_expression') {
const awaitExpr = findChild(iterableNode, 'await_expression');
if (awaitExpr) {
const innerIdent = findChild(awaitExpr, 'identifier');
if (innerIdent) iterableName = innerIdent.text;
}
if (!iterableName) return;
}
if (iterableName) {
let hasCallSelector = false;
let memberName: string | undefined;
const selectorParent = iterableNode.type === 'unary_expression'
? findChild(iterableNode, 'await_expression')
: loopParts;
if (!selectorParent) return;
let foundIterable = false;
for (let i = 0; i < selectorParent.childCount; i++) {
const child = selectorParent.child(i);
if (!child) continue;
if (child.type === 'identifier' && child.text === iterableName) { foundIterable = true; continue; }
if (child === iterableNode) { foundIterable = true; continue; }
if (!foundIterable) continue;
if (child.type === 'selector') {
const uas = findChild(child, 'unconditional_assignable_selector')
?? findChild(child, 'conditional_assignable_selector');
if (uas) {
const id = findChild(uas, 'identifier');
if (id) memberName = id.text;
continue;
}
if (findChild(child, 'argument_part')) {
hasCallSelector = true;
}
}
}
if (hasCallSelector) {
const callee = memberName ?? iterableName;
const rawReturn = returnTypeLookup.lookupRawReturnType(callee);
if (rawReturn) callExprElementType = extractElementTypeFromString(rawReturn);
}
}
if (!iterableName && !callExprElementType) return;
let elementType: string | undefined;
if (callExprElementType) {
elementType = callExprElementType;
} else if (iterableName) {
elementType = resolveIterableElementType(
iterableName, node, scopeEnv, declarationTypeNodes, scope,
extractDartElementTypeFromTypeNode,
);
}
if (elementType && !scopeEnv.has(loopVarName)) {
(scopeEnv as Map<string, string>).set(loopVarName, elementType);
}
};
// ── Export ───────────────────────────────────────────────────────────────
export const typeConfig: LanguageTypeConfig = {
declarationNodeTypes: DART_DECLARATION_NODE_TYPES,
forLoopNodeTypes: DART_FOR_LOOP_NODE_TYPES,
extractDeclaration: extractDartDeclaration,
extractParameter: extractDartParameter,
extractInitializer: extractDartInitializer,
scanConstructorBinding: scanDartConstructorBinding,
extractForLoopBinding: extractDartForLoopBinding,
extractPendingAssignment: extractDartPendingAssignment,
inferLiteralType: inferDartLiteralType,
detectConstructorType: detectDartConstructorType,
};

View file

@ -54,5 +54,6 @@ export const getLanguageFromFilename = (filename: string): SupportedLanguages |
}
// Swift (extensions)
if (filename.endsWith('.swift')) return SupportedLanguages.Swift;
if (filename.endsWith('.dart')) return SupportedLanguages.Dart;
return null;
};

View file

@ -2,7 +2,7 @@
* Built-in name filtering identifies standard library functions and common noise
* that should not be tracked as call targets in the knowledge graph.
*
* Covers: JS/TS, Python, Kotlin, C/C++, C#, PHP, Swift, Rust, Ruby standard libraries.
* Covers: JS/TS, Python, Kotlin, C/C++, C#, PHP, Swift, Rust, Ruby, Dart/Flutter standard libraries.
*/
export const BUILT_IN_NAMES = new Set([
@ -155,6 +155,14 @@ export const BUILT_IN_NAMES = new Set([
'lambda', 'proc', 'block_given?',
'nil?', 'is_a?', 'kind_of?', 'instance_of?', 'respond_to?',
'freeze', 'frozen?', 'dup', 'tap', 'yield_self',
// Dart / Flutter
'setState', 'mounted', 'debugPrint',
'runApp', 'showDialog', 'showModalBottomSheet',
'Navigator', 'push', 'pushNamed', 'pushReplacement', 'pop', 'maybePop',
'ScaffoldMessenger', 'showSnackBar',
'deactivate', 'reassemble', 'debugDumpApp', 'debugDumpRenderTree',
// Dart async
'then', 'catchError', 'whenComplete', 'listen',
// Ruby enumerables
'each', 'select', 'reject', 'detect', 'collect',
'inject', 'flat_map', 'each_with_object', 'each_with_index',

View file

@ -21,6 +21,10 @@ const _require = createRequire(import.meta.url);
let Swift: any = null;
try { Swift = _require('tree-sitter-swift'); } catch {}
// tree-sitter-dart is an optionalDependency — may not be installed
let Dart: any = null;
try { Dart = _require('tree-sitter-dart'); } catch {}
// tree-sitter-kotlin is an optionalDependency — may not be installed
let Kotlin: any = null;
try { Kotlin = _require('tree-sitter-kotlin'); } catch {}
@ -238,6 +242,7 @@ const languageMap: Record<string, any> = {
...(Kotlin ? { [SupportedLanguages.Kotlin]: Kotlin } : {}),
[SupportedLanguages.PHP]: PHP.php_only,
[SupportedLanguages.Ruby]: Ruby,
...(Dart ? { [SupportedLanguages.Dart]: Dart } : {}),
...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}),
};

View file

@ -13,10 +13,12 @@ import Ruby from 'tree-sitter-ruby';
import { createRequire } from 'node:module';
import { SupportedLanguages } from '../../config/supported-languages.js';
// tree-sitter-swift is an optionalDependency — may not be installed
// tree-sitter-swift and tree-sitter-dart are optionalDependencies — may not be installed
const _require = createRequire(import.meta.url);
let Swift: any = null;
try { Swift = _require('tree-sitter-swift'); } catch {}
let Dart: any = null;
try { Dart = _require('tree-sitter-dart'); } catch {}
// tree-sitter-kotlin is an optionalDependency — may not be installed
let Kotlin: any = null;
@ -38,6 +40,7 @@ const languageMap: Record<string, any> = {
...(Kotlin ? { [SupportedLanguages.Kotlin]: Kotlin } : {}),
[SupportedLanguages.PHP]: PHP.php_only,
[SupportedLanguages.Ruby]: Ruby,
...(Dart ? { [SupportedLanguages.Dart]: Dart } : {}),
...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}),
};

View file

@ -0,0 +1,139 @@
// Imports and re-exports
import 'dart:convert';
import 'package:http/http.dart' as http;
export 'src/models.dart';
// Private symbols (should be filtered by exportChecker)
void _internalSetup() {
print('internal');
}
class _PrivateCache {
final Map<String, dynamic> _data = {};
}
// Call sites: expression statement
void expressionCalls() {
fetchUsers();
processData();
}
// Call sites: return statement
String returnCall() {
return formatOutput();
}
// Call sites: variable assignment
void assignmentCalls() {
var result = computeScore();
final user = loadUser();
}
// Type resolution: explicit annotations
void typedDeclarations() {
User admin = User('admin');
User? maybeUser = null;
List<String> names = [];
}
// Type resolution: constructor inference
void constructorInference() {
var dog = Dog('Rex', 'GSD');
final repo = Repository<User>();
var named = Dog.unknown();
}
// Type resolution: for-loop
void loopTypes(List<User> users) {
for (var user in users) {
print(user);
}
for (User u in users) {
print(u);
}
}
// Flutter widget pattern
class UserWidget extends StatelessWidget {
final String username;
const UserWidget({required this.username});
@override
Widget build(BuildContext context) {
return Text(username);
}
}
class CounterPage extends StatefulWidget {
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _count = 0;
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text('$_count');
}
}
// Framework detection patterns
class UserBloc extends Bloc<UserEvent, UserState> {
@override
void onEvent(UserEvent event) {}
}
class SettingsNotifier extends ChangeNotifier {
void toggle() {
notifyListeners();
}
}
// Stub types for compilation (not real Flutter)
class StatelessWidget {}
class StatefulWidget {}
class State<T> {
void initState() {}
void dispose() {}
}
class Widget {}
class BuildContext {}
class Text extends Widget {
Text(String text);
}
class Bloc<E, S> {
void onEvent(E event) {}
}
class ChangeNotifier {
void notifyListeners() {}
}
class User {
final String name;
User(this.name);
}
class Dog {
Dog(String name, String breed);
factory Dog.unknown() => Dog('?', '?');
}
class Repository<T> {}
class UserEvent {}
class UserState {}
// Helper functions for call extraction tests
List<User> fetchUsers() => [];
String processData() => '';
String formatOutput() => '';
int computeScore() => 0;
User loadUser() => User('test');

View file

@ -0,0 +1,121 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
// Top-level function
String greet(String name) {
return 'Hello, $name!';
}
// Top-level typedef
typedef JsonMap = Map<String, dynamic>;
typedef Callback = void Function(int value);
// Enum
enum Status {
active,
inactive,
pending;
String get label => name.toUpperCase();
}
// Class with extends and implements
class Animal {
final String name;
Animal(this.name);
String speak() => 'I am $name';
}
abstract class Describable {
String describe();
}
class Dog extends Animal implements Describable {
final String breed;
Dog(super.name, this.breed);
// Factory constructor
factory Dog.unknown() {
return Dog('Unknown', 'Mixed');
}
@override
String speak() => 'Woof! I am $name';
@override
String describe() => 'Dog($name, $breed)';
// Getter
String get info => '$name - $breed';
// Setter
set nickname(String value) {
print('Nickname set to $value');
}
}
// Mixin
mixin Swimming {
void swim() {
print('Swimming!');
}
}
mixin Flying {
void fly() {
print('Flying!');
}
}
// Class with mixin
class Duck extends Animal with Swimming, Flying {
Duck(super.name);
}
// Extension
extension StringExtension on String {
String capitalize() {
if (isEmpty) return this;
return '${this[0].toUpperCase()}${substring(1)}';
}
}
// Generic class
class Repository<T> {
final List<T> _items = [];
void add(T item) {
_items.add(item);
}
T? findFirst(bool Function(T) predicate) {
for (final item in _items) {
if (predicate(item)) return item;
}
return null;
}
}
// Private function (starts with _)
void _privateHelper() {
print('I am private');
}
// Async function
Future<String> fetchData(String url) async {
final response = await http.get(Uri.parse(url));
return response.body;
}
// Top-level const
const String appName = 'MyApp';
void main() {
final dog = Dog('Rex', 'German Shepherd');
dog.speak();
print(greet('World'));
}

View file

@ -422,6 +422,211 @@ describe('Tree-sitter multi-language parsing', () => {
});
});
describe('Dart', () => {
const dartQueries = () => getProvider(SupportedLanguages.Dart).treeSitterQueries;
function loadDartOrSkip() {
return loadLanguage(SupportedLanguages.Dart).catch(() => null);
}
// ── Definition extraction ──────────────────────────────────────────
it('parses classes, functions, mixins, enums, and type aliases', async () => {
if (!(await loadDartOrSkip())) return;
const { matches } = parseAndQuery(parser, readFixture('simple.dart'), dartQueries());
const defs = extractDefinitions(matches);
const defTypes = defs.map(d => d.type);
const defNames = defs.map(d => d.name);
expect(defTypes).toContain('definition.class');
expect(defTypes).toContain('definition.function');
expect(defTypes).toContain('definition.method');
expect(defTypes).toContain('definition.enum');
expect(defTypes).toContain('definition.trait'); // mixin
expect(defTypes).toContain('definition.type'); // typedef
expect(defTypes).toContain('definition.constructor');
expect(defTypes).toContain('definition.property'); // getter/setter
expect(defNames).toContain('Animal');
expect(defNames).toContain('Dog');
expect(defNames).toContain('greet');
expect(defNames).toContain('Swimming');
expect(defNames).toContain('Status');
expect(defNames).toContain('main');
expect(defNames).toContain('StringExtension');
});
it('captures factory constructor variant name (not class name)', async () => {
if (!(await loadDartOrSkip())) return;
const { matches } = parseAndQuery(parser, readFixture('simple.dart'), dartQueries());
const defs = extractDefinitions(matches);
const constructors = defs.filter(d => d.type === 'definition.constructor');
expect(constructors.length).toBeGreaterThan(0);
// factory Dog.unknown() should capture 'unknown', not 'Dog'
const constructorNames = constructors.map(c => c.name);
expect(constructorNames).toContain('unknown');
expect(constructorNames).not.toContain('Dog');
});
it('captures getter and setter as definition.property', async () => {
if (!(await loadDartOrSkip())) return;
const { matches } = parseAndQuery(parser, readFixture('simple.dart'), dartQueries());
const defs = extractDefinitions(matches);
const props = defs.filter(d => d.type === 'definition.property');
expect(props.map(p => p.name)).toContain('info'); // getter
expect(props.map(p => p.name)).toContain('nickname'); // setter
});
it('captures typedef names without duplicates', async () => {
if (!(await loadDartOrSkip())) return;
const { matches } = parseAndQuery(parser, readFixture('simple.dart'), dartQueries());
const defs = extractDefinitions(matches);
const typedefs = defs.filter(d => d.type === 'definition.type');
const typedefNames = typedefs.map(d => d.name);
expect(typedefNames).toContain('JsonMap');
expect(typedefNames).toContain('Callback');
// Should not capture RHS types as names
expect(typedefNames).not.toContain('Map');
expect(typedefNames).not.toContain('Function');
});
// ── Export detection (private underscore convention) ────────────────
it('filters private symbols via underscore convention', async () => {
if (!(await loadDartOrSkip())) return;
const { matches } = parseAndQuery(parser, readFixture('simple.dart'), dartQueries());
const defs = extractDefinitions(matches);
const { dartExportChecker } = await import('../../src/core/ingestion/export-detection.js');
const publicNames = defs.filter(d => dartExportChecker(null as any, d.name)).map(d => d.name);
const privateNames = defs.filter(d => !dartExportChecker(null as any, d.name)).map(d => d.name);
expect(publicNames).toContain('Animal');
expect(publicNames).toContain('greet');
expect(privateNames).toContain('_privateHelper');
});
// ── Import extraction ──────────────────────────────────────────────
it('extracts imports', async () => {
if (!(await loadDartOrSkip())) return;
const { matches } = parseAndQuery(parser, readFixture('simple.dart'), dartQueries());
const imports: string[] = [];
for (const match of matches) {
for (const capture of match.captures) {
if (capture.name === 'import.source') imports.push(capture.node.text);
}
}
expect(imports.length).toBe(3);
});
it('extracts re-exports', async () => {
if (!(await loadDartOrSkip())) return;
const { matches } = parseAndQuery(parser, readFixture('dart-advanced.dart'), dartQueries());
const imports: string[] = [];
for (const match of matches) {
for (const capture of match.captures) {
if (capture.name === 'import.source') imports.push(capture.node.text);
}
}
// 2 imports + 1 re-export = 3 import.source captures
expect(imports.length).toBe(3);
});
// ── Heritage extraction ────────────────────────────────────────────
it('extracts heritage (extends, implements, with)', async () => {
if (!(await loadDartOrSkip())) return;
const { matches } = parseAndQuery(parser, readFixture('simple.dart'), dartQueries());
const heritage: { class: string; parent: string }[] = [];
for (const match of matches) {
const captures: Record<string, string> = {};
for (const c of match.captures) captures[c.name] = c.node.text;
if (captures['heritage.extends']) {
heritage.push({ class: captures['heritage.class'], parent: captures['heritage.extends'] });
}
if (captures['heritage.implements']) {
heritage.push({ class: captures['heritage.class'], parent: captures['heritage.implements'] });
}
if (captures['heritage.trait']) {
heritage.push({ class: captures['heritage.class'], parent: captures['heritage.trait'] });
}
}
const pairs = heritage.map(h => `${h.class}->${h.parent}`);
expect(pairs).toContain('Dog->Animal');
expect(pairs).toContain('Dog->Describable');
expect(pairs).toContain('Duck->Animal');
expect(pairs).toContain('Duck->Swimming');
expect(pairs).toContain('Duck->Flying');
});
// ── Call extraction ────────────────────────────────────────────────
it('extracts calls in expression statements', async () => {
if (!(await loadDartOrSkip())) return;
const { matches } = parseAndQuery(parser, readFixture('dart-advanced.dart'), dartQueries());
const callNames: string[] = [];
for (const match of matches) {
for (const capture of match.captures) {
if (capture.name === 'call.name') callNames.push(capture.node.text);
}
}
expect(callNames).toContain('fetchUsers');
expect(callNames).toContain('processData');
});
it('extracts calls in return statements', async () => {
if (!(await loadDartOrSkip())) return;
const { matches } = parseAndQuery(parser, readFixture('dart-advanced.dart'), dartQueries());
const callNames: string[] = [];
for (const match of matches) {
for (const capture of match.captures) {
if (capture.name === 'call.name') callNames.push(capture.node.text);
}
}
expect(callNames).toContain('formatOutput');
});
it('extracts calls in variable assignments', async () => {
if (!(await loadDartOrSkip())) return;
const { matches } = parseAndQuery(parser, readFixture('dart-advanced.dart'), dartQueries());
const callNames: string[] = [];
for (const match of matches) {
for (const capture of match.captures) {
if (capture.name === 'call.name') callNames.push(capture.node.text);
}
}
expect(callNames).toContain('computeScore');
expect(callNames).toContain('loadUser');
});
// ── Framework detection (path-based) ───────────────────────────────
it('detects Flutter framework from file paths', async () => {
const { detectFrameworkFromPath } = await import('../../src/core/ingestion/framework-detection.js');
expect(detectFrameworkFromPath('lib/main.dart')?.framework).toBe('flutter');
expect(detectFrameworkFromPath('lib/app.dart')?.framework).toBe('flutter');
expect(detectFrameworkFromPath('lib/screens/home.dart')?.framework).toBe('flutter');
expect(detectFrameworkFromPath('lib/pages/login.dart')?.framework).toBe('flutter');
expect(detectFrameworkFromPath('lib/widgets/button.dart')?.framework).toBe('flutter');
expect(detectFrameworkFromPath('lib/bloc/user_bloc.dart')?.framework).toBe('flutter');
expect(detectFrameworkFromPath('lib/services/api.dart')?.framework).toBe('flutter');
expect(detectFrameworkFromPath('lib/routes/app_router.dart')?.framework).toBe('flutter');
// main.dart gets highest boost
expect(detectFrameworkFromPath('lib/main.dart')?.entryPointMultiplier).toBe(3.0);
// widgets get lowest
expect(detectFrameworkFromPath('lib/widgets/button.dart')?.entryPointMultiplier).toBe(1.5);
});
});
describe('cross-language assertions', () => {
it('all supported languages produce at least one definition from fixtures', async () => {
const langFixtures: [SupportedLanguages, string, string?][] = [
@ -435,6 +640,7 @@ describe('Tree-sitter multi-language parsing', () => {
[SupportedLanguages.CSharp, 'simple.cs'],
[SupportedLanguages.Rust, 'simple.rs'],
[SupportedLanguages.PHP, 'simple.php'],
// Dart and Swift are excluded — they are optionalDependencies that may not be installed
];
for (const [lang, fixture, filePath] of langFixtures) {

View file

@ -0,0 +1,79 @@
import { describe, it, expect } from 'vitest';
import { resolveDartImport } from '../../src/core/ingestion/import-resolvers/dart.js';
import type { ResolveCtx } from '../../src/core/ingestion/import-resolvers/types.js';
function makeCtx(files: string[]): ResolveCtx {
const allFileList = files;
const normalizedFileList = files.map(f => f.toLowerCase());
return {
allFilePaths: new Set(files),
allFileList,
normalizedFileList,
index: undefined as any,
resolveCache: new Map(),
configs: {
tsconfigPaths: null,
goModule: null,
composerConfig: null,
swiftPackageConfig: null,
csharpConfigs: [],
},
};
}
describe('Dart import resolver', () => {
describe('dart: SDK imports', () => {
it('skips dart:async', () => {
const result = resolveDartImport("'dart:async'", 'lib/main.dart', makeCtx([]));
expect(result).toBeNull();
});
it('skips dart:io', () => {
const result = resolveDartImport("'dart:io'", 'lib/main.dart', makeCtx([]));
expect(result).toBeNull();
});
});
describe('package: imports', () => {
it('resolves local package import to lib/', () => {
const ctx = makeCtx(['lib/models/user.dart', 'lib/main.dart']);
const result = resolveDartImport("'package:my_app/models/user.dart'", 'lib/main.dart', ctx);
expect(result).toEqual({ kind: 'files', files: ['lib/models/user.dart'] });
});
it('returns null for external package imports', () => {
const ctx = makeCtx(['lib/main.dart']);
const result = resolveDartImport("'package:http/http.dart'", 'lib/main.dart', ctx);
expect(result).toBeNull();
});
it('returns null for malformed package import (no slash)', () => {
const result = resolveDartImport("'package:http'", 'lib/main.dart', makeCtx([]));
expect(result).toBeNull();
});
});
describe('relative imports', () => {
it('resolves relative import via standard resolver', () => {
const ctx = makeCtx(['lib/models/user.dart', 'lib/main.dart']);
const result = resolveDartImport("'models/user.dart'", 'lib/main.dart', ctx);
// resolveStandard handles relative path resolution
// The exact result depends on the standard resolver — we just check it doesn't crash
expect(result === null || result?.kind === 'files').toBe(true);
});
});
describe('quote stripping', () => {
it('strips single quotes', () => {
const ctx = makeCtx([]);
const result = resolveDartImport("'dart:core'", 'lib/main.dart', ctx);
expect(result).toBeNull(); // dart: is skipped, proving quotes were stripped
});
it('strips double quotes', () => {
const ctx = makeCtx([]);
const result = resolveDartImport('"dart:core"', 'lib/main.dart', ctx);
expect(result).toBeNull();
});
});
});

View file

@ -0,0 +1,393 @@
import { describe, it, expect, beforeAll } from 'vitest';
import Parser from 'tree-sitter';
import { SupportedLanguages } from '../../src/config/supported-languages.js';
import { loadParser, loadLanguage } from '../../src/core/tree-sitter/parser-loader.js';
import { typeConfig } from '../../src/core/ingestion/type-extractors/dart.js';
import { findChild } from '../../src/core/ingestion/utils/ast-helpers.js';
function loadDartOrSkip() {
return loadLanguage(SupportedLanguages.Dart).catch(() => null);
}
function parseAndFindNodes(parser: Parser, code: string, nodeType: string) {
const tree = parser.parse(code);
const results: any[] = [];
function walk(node: any) {
if (node.type === nodeType) results.push(node);
for (let i = 0; i < node.namedChildCount; i++) walk(node.namedChild(i));
}
walk(tree.rootNode);
return results;
}
describe('Dart type extractor', () => {
let parser: Parser;
beforeAll(async () => {
parser = await loadParser();
if (!(await loadDartOrSkip())) return;
});
describe('Tier 0: explicit type annotations', () => {
it('extracts type from typed variable declaration', async () => {
if (!(await loadDartOrSkip())) return;
const env = new Map<string, string>();
const nodes = parseAndFindNodes(parser, 'void f() { User admin = User("x"); }', 'initialized_variable_definition');
expect(nodes.length).toBeGreaterThan(0);
typeConfig.extractDeclaration(nodes[0], env);
expect(env.get('admin')).toBe('User');
});
it('extracts type from nullable declaration', async () => {
if (!(await loadDartOrSkip())) return;
const env = new Map<string, string>();
const nodes = parseAndFindNodes(parser, 'void f() { User? maybeUser = null; }', 'initialized_variable_definition');
expect(nodes.length).toBeGreaterThan(0);
typeConfig.extractDeclaration(nodes[0], env);
expect(env.get('maybeUser')).toBe('User');
});
it('skips dynamic type', async () => {
if (!(await loadDartOrSkip())) return;
const env = new Map<string, string>();
const nodes = parseAndFindNodes(parser, 'void f() { dynamic x = 1; }', 'initialized_variable_definition');
if (nodes.length > 0) {
typeConfig.extractDeclaration(nodes[0], env);
expect(env.has('x')).toBe(false);
}
});
});
describe('Tier 0: parameter extraction', () => {
it('extracts typed parameter', async () => {
if (!(await loadDartOrSkip())) return;
const env = new Map<string, string>();
const tree = parser.parse('void greet(String name, int age) {}');
// Find formal_parameter nodes
const params: any[] = [];
function walk(node: any) {
if (node.type === 'formal_parameter') params.push(node);
for (let i = 0; i < node.namedChildCount; i++) walk(node.namedChild(i));
}
walk(tree.rootNode);
for (const p of params) {
typeConfig.extractParameter(p, env);
}
expect(env.get('name')).toBe('String');
});
});
describe('Tier 1: constructor inference', () => {
it('infers type from direct constructor call', async () => {
if (!(await loadDartOrSkip())) return;
const env = new Map<string, string>();
const classNames = new Set(['User']);
const nodes = parseAndFindNodes(parser, 'void f() { var user = User("x"); }', 'initialized_variable_definition');
expect(nodes.length).toBeGreaterThan(0);
typeConfig.extractInitializer!(nodes[0], env, classNames);
expect(env.get('user')).toBe('User');
});
it('infers type from named constructor', async () => {
if (!(await loadDartOrSkip())) return;
const env = new Map<string, string>();
const classNames = new Set(['Dog']);
const nodes = parseAndFindNodes(parser, 'void f() { var d = Dog.unknown(); }', 'initialized_variable_definition');
expect(nodes.length).toBeGreaterThan(0);
typeConfig.extractInitializer!(nodes[0], env, classNames);
expect(env.get('d')).toBe('Dog');
});
it('does not infer when callee is not a known class', async () => {
if (!(await loadDartOrSkip())) return;
const env = new Map<string, string>();
const classNames = new Set<string>();
const nodes = parseAndFindNodes(parser, 'void f() { var x = getUser(); }', 'initialized_variable_definition');
expect(nodes.length).toBeGreaterThan(0);
typeConfig.extractInitializer!(nodes[0], env, classNames);
expect(env.has('x')).toBe(false);
});
it('skips if explicit type present', async () => {
if (!(await loadDartOrSkip())) return;
const env = new Map<string, string>();
const classNames = new Set(['User']);
const nodes = parseAndFindNodes(parser, 'void f() { User user = User("x"); }', 'initialized_variable_definition');
expect(nodes.length).toBeGreaterThan(0);
typeConfig.extractInitializer!(nodes[0], env, classNames);
// Should not set — Tier 0 handles this
expect(env.has('user')).toBe(false);
});
});
describe('constructor binding scanner', () => {
it('scans direct constructor call', async () => {
if (!(await loadDartOrSkip())) return;
const nodes = parseAndFindNodes(parser, 'void f() { var user = User(); }', 'initialized_variable_definition');
expect(nodes.length).toBeGreaterThan(0);
const result = typeConfig.scanConstructorBinding!(nodes[0]);
expect(result).toEqual({ varName: 'user', calleeName: 'User' });
});
it('scans qualified call (method on receiver)', async () => {
if (!(await loadDartOrSkip())) return;
const nodes = parseAndFindNodes(parser, 'void f() { var u = svc.getUser(); }', 'initialized_variable_definition');
expect(nodes.length).toBeGreaterThan(0);
const result = typeConfig.scanConstructorBinding!(nodes[0]);
expect(result?.varName).toBe('u');
expect(result?.calleeName).toBe('getUser');
});
it('returns undefined for non-call assignment', async () => {
if (!(await loadDartOrSkip())) return;
const nodes = parseAndFindNodes(parser, 'void f() { var x = y; }', 'initialized_variable_definition');
expect(nodes.length).toBeGreaterThan(0);
const result = typeConfig.scanConstructorBinding!(nodes[0]);
expect(result).toBeUndefined();
});
});
describe('virtual dispatch detection', () => {
it('detects constructor type for virtual dispatch', async () => {
if (!(await loadDartOrSkip())) return;
const classNames = new Set(['Dog']);
const nodes = parseAndFindNodes(parser, 'void f() { var d = Dog(); }', 'initialized_variable_definition');
expect(nodes.length).toBeGreaterThan(0);
const result = typeConfig.detectConstructorType!(nodes[0], classNames);
expect(result).toBe('Dog');
});
});
describe('literal type inference', () => {
it('infers int literal', async () => {
if (!(await loadDartOrSkip())) return;
const tree = parser.parse('void f() { var x = 42; }');
const nodes: any[] = [];
function walk(n: any) { if (n.type.includes('integer_literal')) nodes.push(n); for (let i = 0; i < n.namedChildCount; i++) walk(n.namedChild(i)); }
walk(tree.rootNode);
if (nodes.length > 0) {
expect(typeConfig.inferLiteralType!(nodes[0])).toBe('int');
}
});
it('infers string literal', async () => {
if (!(await loadDartOrSkip())) return;
const tree = parser.parse('void f() { var x = "hello"; }');
const nodes: any[] = [];
function walk(n: any) { if (n.type === 'string_literal') nodes.push(n); for (let i = 0; i < n.namedChildCount; i++) walk(n.namedChild(i)); }
walk(tree.rootNode);
if (nodes.length > 0) {
expect(typeConfig.inferLiteralType!(nodes[0])).toBe('String');
}
});
it('infers bool literal', async () => {
if (!(await loadDartOrSkip())) return;
const tree = parser.parse('void f() { var x = true; }');
const nodes: any[] = [];
function walk(n: any) { if (n.type === 'true' || n.type === 'false') nodes.push(n); for (let i = 0; i < n.childCount; i++) { const c = n.child(i); if (c) walk(c); } }
walk(tree.rootNode);
if (nodes.length > 0) {
expect(typeConfig.inferLiteralType!(nodes[0])).toBe('bool');
}
});
});
describe('Tier 2: pending assignment extraction', () => {
it('extracts copy assignment', async () => {
if (!(await loadDartOrSkip())) return;
const nodes = parseAndFindNodes(parser, 'void f() { var copy = original; }', 'initialized_variable_definition');
expect(nodes.length).toBeGreaterThan(0);
const result = typeConfig.extractPendingAssignment!(nodes[0], new Map());
expect(result).toEqual({ kind: 'copy', lhs: 'copy', rhs: 'original' });
});
it('extracts field access', async () => {
if (!(await loadDartOrSkip())) return;
const nodes = parseAndFindNodes(parser, 'void f() { var n = user.name; }', 'initialized_variable_definition');
expect(nodes.length).toBeGreaterThan(0);
const result = typeConfig.extractPendingAssignment!(nodes[0], new Map());
expect(result).toEqual({ kind: 'fieldAccess', lhs: 'n', receiver: 'user', field: 'name' });
});
it('extracts call result', async () => {
if (!(await loadDartOrSkip())) return;
const nodes = parseAndFindNodes(parser, 'void f() { var u = getUser(); }', 'initialized_variable_definition');
expect(nodes.length).toBeGreaterThan(0);
const result = typeConfig.extractPendingAssignment!(nodes[0], new Map());
expect(result).toEqual({ kind: 'callResult', lhs: 'u', callee: 'getUser' });
});
it('extracts method call result', async () => {
if (!(await loadDartOrSkip())) return;
const nodes = parseAndFindNodes(parser, 'void f() { var r = svc.fetch(); }', 'initialized_variable_definition');
expect(nodes.length).toBeGreaterThan(0);
const result = typeConfig.extractPendingAssignment!(nodes[0], new Map());
expect(result).toEqual({ kind: 'methodCallResult', lhs: 'r', receiver: 'svc', method: 'fetch' });
});
it('skips when lhs already in scope', async () => {
if (!(await loadDartOrSkip())) return;
const scope = new Map([['x', 'String']]);
const nodes = parseAndFindNodes(parser, 'void f() { var x = y; }', 'initialized_variable_definition');
expect(nodes.length).toBeGreaterThan(0);
const result = typeConfig.extractPendingAssignment!(nodes[0], scope);
expect(result).toBeUndefined();
});
it('extracts call result from await expression', async () => {
if (!(await loadDartOrSkip())) return;
const nodes = parseAndFindNodes(parser, 'void f() async { var user = await getUser(); }', 'initialized_variable_definition');
expect(nodes.length).toBeGreaterThan(0);
const result = typeConfig.extractPendingAssignment!(nodes[0], new Map());
expect(result).toEqual({ kind: 'callResult', lhs: 'user', callee: 'getUser' });
});
it('extracts method call result from await expression', async () => {
if (!(await loadDartOrSkip())) return;
const nodes = parseAndFindNodes(parser, 'void f() async { var user = await svc.fetch(); }', 'initialized_variable_definition');
expect(nodes.length).toBeGreaterThan(0);
const result = typeConfig.extractPendingAssignment!(nodes[0], new Map());
expect(result).toEqual({ kind: 'methodCallResult', lhs: 'user', receiver: 'svc', method: 'fetch' });
});
});
describe('for-loop element type resolution', () => {
it('extracts type from explicit for-loop annotation', async () => {
if (!(await loadDartOrSkip())) return;
const nodes = parseAndFindNodes(parser, 'void f(List<User> users) { for (User u in users) {} }', 'for_statement');
expect(nodes.length).toBeGreaterThan(0);
const scopeEnv = new Map<string, string>();
const ctx = {
scopeEnv,
declarationTypeNodes: new Map(),
scope: 'test@0',
returnTypeLookup: {
lookupReturnType: () => undefined,
lookupRawReturnType: () => undefined,
},
};
typeConfig.extractForLoopBinding!(nodes[0], ctx);
expect(scopeEnv.get('u')).toBe('User');
});
it('infers element type from call iterable return type', async () => {
if (!(await loadDartOrSkip())) return;
const nodes = parseAndFindNodes(parser, 'void f() { for (var u in getUsers()) {} }', 'for_statement');
expect(nodes.length).toBeGreaterThan(0);
const scopeEnv = new Map<string, string>();
const ctx = {
scopeEnv,
declarationTypeNodes: new Map(),
scope: 'test@0',
returnTypeLookup: {
lookupReturnType: (name: string) => name === 'getUsers' ? 'User' : undefined,
lookupRawReturnType: (name: string) => name === 'getUsers' ? 'List<User>' : undefined,
},
};
typeConfig.extractForLoopBinding!(nodes[0], ctx);
expect(scopeEnv.get('u')).toBe('User');
});
it('skips non-for_statement nodes', async () => {
if (!(await loadDartOrSkip())) return;
const nodes = parseAndFindNodes(parser, 'void f() { var x = 1; }', 'initialized_variable_definition');
expect(nodes.length).toBeGreaterThan(0);
const scopeEnv = new Map<string, string>();
const ctx = {
scopeEnv,
declarationTypeNodes: new Map(),
scope: 'test@0',
returnTypeLookup: { lookupReturnType: () => undefined, lookupRawReturnType: () => undefined },
};
typeConfig.extractForLoopBinding!(nodes[0], ctx);
expect(scopeEnv.size).toBe(0);
});
});
describe('virtual dispatch — named constructor', () => {
it('detects constructor type for named constructor', async () => {
if (!(await loadDartOrSkip())) return;
const classNames = new Set(['Dog']);
const nodes = parseAndFindNodes(parser, 'void f() { var d = Dog.unknown(); }', 'initialized_variable_definition');
expect(nodes.length).toBeGreaterThan(0);
const result = typeConfig.detectConstructorType!(nodes[0], classNames);
expect(result).toBe('Dog');
});
it('returns undefined when callee is not a known class', async () => {
if (!(await loadDartOrSkip())) return;
const classNames = new Set<string>();
const nodes = parseAndFindNodes(parser, 'void f() { var x = getUser(); }', 'initialized_variable_definition');
expect(nodes.length).toBeGreaterThan(0);
const result = typeConfig.detectConstructorType!(nodes[0], classNames);
expect(result).toBeUndefined();
});
});
describe('literal type inference — full coverage', () => {
it('infers double literal', async () => {
if (!(await loadDartOrSkip())) return;
const tree = parser.parse('void f() { var x = 3.14; }');
const nodes: any[] = [];
function walk(n: any) { if (n.type.includes('floating_point')) nodes.push(n); for (let i = 0; i < n.namedChildCount; i++) walk(n.namedChild(i)); }
walk(tree.rootNode);
if (nodes.length > 0) expect(typeConfig.inferLiteralType!(nodes[0])).toBe('double');
});
it('infers false literal', async () => {
if (!(await loadDartOrSkip())) return;
const tree = parser.parse('void f() { var x = false; }');
const nodes: any[] = [];
function walk(n: any) { if (n.type === 'false') nodes.push(n); for (let i = 0; i < n.childCount; i++) { const c = n.child(i); if (c) walk(c); } }
walk(tree.rootNode);
if (nodes.length > 0) expect(typeConfig.inferLiteralType!(nodes[0])).toBe('bool');
});
it('infers null literal', async () => {
if (!(await loadDartOrSkip())) return;
const tree = parser.parse('void f() { var x = null; }');
const nodes: any[] = [];
function walk(n: any) { if (n.type === 'null_literal') nodes.push(n); for (let i = 0; i < n.namedChildCount; i++) walk(n.namedChild(i)); }
walk(tree.rootNode);
if (nodes.length > 0) expect(typeConfig.inferLiteralType!(nodes[0])).toBe('null');
});
it('returns undefined for unknown node type', async () => {
if (!(await loadDartOrSkip())) return;
expect(typeConfig.inferLiteralType!({ type: 'identifier' } as any)).toBeUndefined();
});
});
describe('generic and const declarations', () => {
it('extracts outer type from generic declaration', async () => {
if (!(await loadDartOrSkip())) return;
const env = new Map<string, string>();
const nodes = parseAndFindNodes(parser, 'void f() { List<String> names = []; }', 'initialized_variable_definition');
expect(nodes.length).toBeGreaterThan(0);
typeConfig.extractDeclaration(nodes[0], env);
expect(env.get('names')).toBe('List');
});
it('infers type from generic constructor call', async () => {
if (!(await loadDartOrSkip())) return;
const env = new Map<string, string>();
const classNames = new Set(['Repository']);
const nodes = parseAndFindNodes(parser, 'void f() { var repo = Repository(); }', 'initialized_variable_definition');
expect(nodes.length).toBeGreaterThan(0);
typeConfig.extractInitializer!(nodes[0], env, classNames);
expect(env.get('repo')).toBe('Repository');
});
it('infers type from const constructor call', async () => {
if (!(await loadDartOrSkip())) return;
const env = new Map<string, string>();
const classNames = new Set(['Config']);
const nodes = parseAndFindNodes(parser, 'void f() { const config = Config(); }', 'initialized_variable_definition');
if (nodes.length > 0) {
typeConfig.extractInitializer!(nodes[0], env, classNames);
expect(env.get('config')).toBe('Config');
}
});
});
});