Merge remote-tracking branch 'origin/main' into feat/phase8-9-implementation

This commit is contained in:
Gergo Magyar 2026-03-26 08:58:59 +00:00
commit da162332aa
36 changed files with 2075 additions and 14 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

@ -100,6 +100,7 @@ export type RelationshipType =
| 'HANDLES_TOOL' // Function/File → Tool (handler implements this tool)
| 'ENTRY_POINT_OF' // Route/Tool → Process (this endpoint starts this execution flow)
| 'WRAPS' // Function → Function (middleware wrapper chain) — Reserved: future middleware graph traversal (not yet emitted)
| 'QUERIES' // File/Function → CodeElement (ORM query to model/table)
export interface GraphNode {
id: string,

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

@ -81,6 +81,17 @@ export function detectFrameworkFromPath(filePath: string): FrameworkHint | null
return { framework: 'expo-router', entryPointMultiplier: 2.5, reason: 'expo-screen' };
}
// Prisma schema (ORM data model definition)
if (p.includes('/prisma/') && p.endsWith('schema.prisma')) {
return { framework: 'prisma', entryPointMultiplier: 1.5, reason: 'prisma-schema' };
}
// Supabase client files
if ((p.includes('/lib/supabase') || p.includes('/utils/supabase') || p.includes('/supabase/')) &&
(p.endsWith('.ts') || p.endsWith('.js'))) {
return { framework: 'supabase', entryPointMultiplier: 1.5, reason: 'supabase-client' };
}
// Express / Node.js routes
if (p.includes('/routes/') && (p.endsWith('.ts') || p.endsWith('.js'))) {
return { framework: 'express', entryPointMultiplier: 2.5, reason: 'routes-folder' };
@ -406,6 +417,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
@ -456,6 +499,10 @@ export const FRAMEWORK_AST_PATTERNS = {
'fiber': ['fiber.Ctx', 'fiber.New', 'fiber.App'],
'go-grpc': ['grpc.Server', 'RegisterServer', 'pb.Unimplemented'],
// ORM patterns
'prisma': ['prisma.', 'PrismaClient', '@prisma/client'],
'supabase': ['supabase.from', 'createClient', '@supabase/supabase-js'],
// PHP/Laravel
'laravel': ['Route::get', 'Route::post', 'Route::put', 'Route::delete',
'Route::resource', 'Route::apiResource', '#[Route('],
@ -478,6 +525,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 +597,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

@ -11,7 +11,7 @@ import { getDefinitionNodeFromCaptures, findEnclosingClassId, extractMethodSigna
import { extractPropertyDeclaredType } from './type-extractors/shared.js';
import { detectFrameworkFromAST } from './framework-detection.js';
import { WorkerPool } from './workers/worker-pool.js';
import type { ParseWorkerResult, ParseWorkerInput, ExtractedImport, ExtractedCall, ExtractedAssignment, ExtractedHeritage, ExtractedRoute, ExtractedFetchCall, ExtractedDecoratorRoute, ExtractedToolDef, FileConstructorBindings, FileTypeEnvBindings } from './workers/parse-worker.js';
import type { ParseWorkerResult, ParseWorkerInput, ExtractedImport, ExtractedCall, ExtractedAssignment, ExtractedHeritage, ExtractedRoute, ExtractedFetchCall, ExtractedDecoratorRoute, ExtractedToolDef, FileConstructorBindings, FileTypeEnvBindings, ExtractedORMQuery } from './workers/parse-worker.js';
import { getTreeSitterBufferSize, TREE_SITTER_MAX_BUFFER } from './constants.js';
export type FileProgressCallback = (current: number, total: number, filePath: string) => void;
@ -25,6 +25,7 @@ export interface WorkerExtractedData {
fetchCalls: ExtractedFetchCall[];
decoratorRoutes: ExtractedDecoratorRoute[];
toolDefs: ExtractedToolDef[];
ormQueries: ExtractedORMQuery[];
constructorBindings: FileConstructorBindings[];
typeEnvBindings: FileTypeEnvBindings[];
}
@ -48,7 +49,7 @@ const processParsingWithWorkers = async (
if (lang) parseableFiles.push({ path: file.path, content: file.content });
}
if (parseableFiles.length === 0) return { imports: [], calls: [], assignments: [], heritage: [], routes: [], fetchCalls: [], decoratorRoutes: [], toolDefs: [], constructorBindings: [], typeEnvBindings: [] };
if (parseableFiles.length === 0) return { imports: [], calls: [], assignments: [], heritage: [], routes: [], fetchCalls: [], decoratorRoutes: [], toolDefs: [], ormQueries: [], constructorBindings: [], typeEnvBindings: [] };
const total = files.length;
@ -69,6 +70,7 @@ const processParsingWithWorkers = async (
const allFetchCalls: ExtractedFetchCall[] = [];
const allDecoratorRoutes: ExtractedDecoratorRoute[] = [];
const allToolDefs: ExtractedToolDef[] = [];
const allORMQueries: ExtractedORMQuery[] = [];
const allConstructorBindings: FileConstructorBindings[] = [];
const allTypeEnvBindings: FileTypeEnvBindings[] = [];
for (const result of chunkResults) {
@ -103,6 +105,7 @@ const processParsingWithWorkers = async (
allFetchCalls.push(...result.fetchCalls);
allDecoratorRoutes.push(...result.decoratorRoutes);
allToolDefs.push(...result.toolDefs);
if (result.ormQueries) allORMQueries.push(...result.ormQueries);
allConstructorBindings.push(...result.constructorBindings);
allTypeEnvBindings.push(...result.typeEnvBindings);
}
@ -123,7 +126,7 @@ const processParsingWithWorkers = async (
// Final progress
onFileProgress?.(total, total, 'done');
return { imports: allImports, calls: allCalls, assignments: allAssignments, heritage: allHeritage, routes: allRoutes, fetchCalls: allFetchCalls, decoratorRoutes: allDecoratorRoutes, toolDefs: allToolDefs, constructorBindings: allConstructorBindings, typeEnvBindings: allTypeEnvBindings };
return { imports: allImports, calls: allCalls, assignments: allAssignments, heritage: allHeritage, routes: allRoutes, fetchCalls: allFetchCalls, decoratorRoutes: allDecoratorRoutes, toolDefs: allToolDefs, ormQueries: allORMQueries, constructorBindings: allConstructorBindings, typeEnvBindings: allTypeEnvBindings };
};
// ============================================================================

View file

@ -15,7 +15,7 @@ import { phpFileToRouteURL } from './route-extractors/php.js';
import { extractResponseShapes, extractPHPResponseShapes } from './route-extractors/response-shapes.js';
import { extractMiddlewareChain, extractNextjsMiddlewareConfig, compileMatcher, compiledMatcherMatchesRoute } from './route-extractors/middleware.js';
import { generateId } from '../../lib/utils.js';
import type { ExtractedFetchCall, ExtractedRoute, ExtractedDecoratorRoute, ExtractedToolDef } from './workers/parse-worker.js';
import type { ExtractedFetchCall, ExtractedRoute, ExtractedDecoratorRoute, ExtractedToolDef, ExtractedORMQuery } from './workers/parse-worker.js';
import { processHeritage, processHeritageFromExtracted } from './heritage-processor.js';
import { computeMRO } from './mro-processor.js';
import { processCommunities } from './community-processor.js';
@ -514,6 +514,7 @@ async function runChunkedParseAndResolve(
allExtractedRoutes: ExtractedRoute[];
allDecoratorRoutes: ExtractedDecoratorRoute[];
allToolDefs: ExtractedToolDef[];
allORMQueries: ExtractedORMQuery[];
}> {
const symbolTable = ctx.symbols;
@ -634,6 +635,7 @@ async function runChunkedParseAndResolve(
const allDecoratorRoutes: ExtractedDecoratorRoute[] = [];
// Accumulate MCP/RPC tool definitions (@mcp.tool(), @app.tool(), etc.)
const allToolDefs: ExtractedToolDef[] = [];
const allORMQueries: ExtractedORMQuery[] = [];
try {
for (let chunkIdx = 0; chunkIdx < numChunks; chunkIdx++) {
@ -762,6 +764,9 @@ async function runChunkedParseAndResolve(
if (chunkWorkerData.toolDefs?.length) {
allToolDefs.push(...chunkWorkerData.toolDefs);
}
if (chunkWorkerData.ormQueries?.length) {
allORMQueries.push(...chunkWorkerData.ormQueries);
}
} else {
await processImports(graph, chunkFiles, astCache, ctx, undefined, repoPath, allPaths);
sequentialChunkPaths.push(chunkPaths);
@ -797,6 +802,10 @@ async function runChunkedParseAndResolve(
if (chunkFetchCalls.length > 0) {
allFetchCalls.push(...chunkFetchCalls);
}
// Extract ORM queries (sequential path)
for (const f of chunkFiles) {
extractORMQueriesInline(f.path, f.content, allORMQueries);
}
astCache.clear();
}
@ -853,7 +862,7 @@ async function runChunkedParseAndResolve(
importCtx.index = EMPTY_INDEX; // Release suffix index memory (~30MB for large repos)
importCtx.normalizedFileList = [];
return { exportedTypeMap, allFetchCalls, allExtractedRoutes, allDecoratorRoutes, allToolDefs };
return { exportedTypeMap, allFetchCalls, allExtractedRoutes, allDecoratorRoutes, allToolDefs, allORMQueries };
}
/**
@ -1075,7 +1084,7 @@ export const runPipelineFromRepo = async (
const { scannedFiles, allPaths, totalFiles } = await runScanAndStructure(repoPath, graph, onProgress);
// Phase 3+4: Chunked parse + resolve (imports, calls, heritage, routes)
const { exportedTypeMap, allFetchCalls, allExtractedRoutes, allDecoratorRoutes, allToolDefs } = await runChunkedParseAndResolve(
const { exportedTypeMap, allFetchCalls, allExtractedRoutes, allDecoratorRoutes, allToolDefs, allORMQueries } = await runChunkedParseAndResolve(
graph, ctx, scannedFiles, allPaths, totalFiles, repoPath, pipelineStart, onProgress,
);
@ -1340,6 +1349,11 @@ export const runPipelineFromRepo = async (
}
}
// ── Phase 3.7: ORM Dataflow Detection (Prisma + Supabase) ──────────
if (allORMQueries.length > 0) {
processORMQueries(graph, allORMQueries, isDev);
}
// ── Phase 14: Cross-file binding propagation (topological level sort) ──
await runCrossFileBindingPropagation(
graph, ctx, exportedTypeMap, allPaths, totalFiles, repoPath, pipelineStart, onProgress,
@ -1374,3 +1388,92 @@ export const runPipelineFromRepo = async (
throw error;
}
};
// Inline ORM regex extraction (avoids importing parse-worker which has worker-only code)
const PRISMA_QUERY_RE = /\bprisma\.(\w+)\.(findMany|findFirst|findUnique|findUniqueOrThrow|findFirstOrThrow|create|createMany|update|updateMany|delete|deleteMany|upsert|count|aggregate|groupBy)\s*\(/g;
const SUPABASE_QUERY_RE = /\bsupabase\.from\s*\(\s*['"](\w+)['"]\s*\)\s*\.(select|insert|update|delete|upsert)\s*\(/g;
function extractORMQueriesInline(filePath: string, content: string, out: ExtractedORMQuery[]): void {
const hasPrisma = content.includes('prisma.');
const hasSupabase = content.includes('supabase.from');
if (!hasPrisma && !hasSupabase) return;
if (hasPrisma) {
PRISMA_QUERY_RE.lastIndex = 0;
let m;
while ((m = PRISMA_QUERY_RE.exec(content)) !== null) {
const model = m[1];
if (model.startsWith('$')) continue;
out.push({ filePath, orm: 'prisma', model, method: m[2], lineNumber: content.substring(0, m.index).split('\n').length - 1 });
}
}
if (hasSupabase) {
SUPABASE_QUERY_RE.lastIndex = 0;
let m;
while ((m = SUPABASE_QUERY_RE.exec(content)) !== null) {
out.push({ filePath, orm: 'supabase', model: m[1], method: m[2], lineNumber: content.substring(0, m.index).split('\n').length - 1 });
}
}
}
// ============================================================================
// ORM Query Processing — creates QUERIES edges from callers to model nodes
// ============================================================================
function processORMQueries(
graph: ReturnType<typeof createKnowledgeGraph>,
queries: ExtractedORMQuery[],
isDev: boolean,
): void {
const modelNodes = new Map<string, string>();
const seenEdges = new Set<string>();
let edgesCreated = 0;
for (const q of queries) {
const modelKey = `${q.orm}:${q.model}`;
let modelNodeId = modelNodes.get(modelKey);
if (!modelNodeId) {
const candidateIds = [
generateId('Class', `${q.model}`),
generateId('Interface', `${q.model}`),
generateId('CodeElement', `${q.model}`),
];
const existing = candidateIds.find(id => graph.getNode(id));
if (existing) {
modelNodeId = existing;
} else {
modelNodeId = generateId('CodeElement', `${q.orm}:${q.model}`);
graph.addNode({
id: modelNodeId,
label: 'CodeElement',
properties: {
name: q.model,
filePath: '',
description: `${q.orm} model/table: ${q.model}`,
},
});
}
modelNodes.set(modelKey, modelNodeId);
}
const fileId = generateId('File', q.filePath);
const edgeKey = `${fileId}->${modelNodeId}:${q.method}`;
if (seenEdges.has(edgeKey)) continue;
seenEdges.add(edgeKey);
graph.addRelationship({
id: generateId('QUERIES', edgeKey),
sourceId: fileId,
targetId: modelNodeId,
type: 'QUERIES',
confidence: 0.9,
reason: `${q.orm}-${q.method}`,
});
edgesCreated++;
}
if (isDev) {
console.log(`ORM dataflow: ${edgesCreated} QUERIES edges, ${modelNodes.size} models (${queries.length} total calls)`);
}
}

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 {}
@ -181,6 +185,14 @@ export interface ExtractedToolDef {
lineNumber: number;
}
export interface ExtractedORMQuery {
filePath: string;
orm: 'prisma' | 'supabase';
model: string;
method: string;
lineNumber: number;
}
/** Constructor bindings keyed by filePath for cross-file type resolution */
export interface FileConstructorBindings {
filePath: string;
@ -206,6 +218,7 @@ export interface ParseWorkerResult {
fetchCalls: ExtractedFetchCall[];
decoratorRoutes: ExtractedDecoratorRoute[];
toolDefs: ExtractedToolDef[];
ormQueries: ExtractedORMQuery[];
constructorBindings: FileConstructorBindings[];
/** File-scope type bindings from TypeEnv fixpoint for exported symbol collection. */
typeEnvBindings: FileTypeEnvBindings[];
@ -238,6 +251,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 } : {}),
};
@ -353,6 +367,7 @@ const processBatch = (files: ParseWorkerInput[], onProgress?: (filesProcessed: n
fetchCalls: [],
decoratorRoutes: [],
toolDefs: [],
ormQueries: [],
constructorBindings: [],
typeEnvBindings: [],
skippedLanguages: {},
@ -825,6 +840,53 @@ function extractLaravelRoutes(tree: any, filePath: string): ExtractedRoute[] {
return routes;
}
// ============================================================================
// ORM Query Detection (Prisma + Supabase)
// ============================================================================
const PRISMA_QUERY_RE = /\bprisma\.(\w+)\.(findMany|findFirst|findUnique|findUniqueOrThrow|findFirstOrThrow|create|createMany|update|updateMany|delete|deleteMany|upsert|count|aggregate|groupBy)\s*\(/g;
const SUPABASE_QUERY_RE = /\bsupabase\.from\s*\(\s*['"](\w+)['"]\s*\)\s*\.(select|insert|update|delete|upsert)\s*\(/g;
/**
* Extract ORM query calls from file content via regex.
* Appends results to the provided array (avoids allocation when no matches).
*/
export function extractORMQueries(filePath: string, content: string, out: ExtractedORMQuery[]): void {
const hasPrisma = content.includes('prisma.');
const hasSupabase = content.includes('supabase.from');
if (!hasPrisma && !hasSupabase) return;
if (hasPrisma) {
PRISMA_QUERY_RE.lastIndex = 0;
let m;
while ((m = PRISMA_QUERY_RE.exec(content)) !== null) {
const model = m[1];
if (model.startsWith('$')) continue;
out.push({
filePath,
orm: 'prisma',
model,
method: m[2],
lineNumber: content.substring(0, m.index).split('\n').length - 1,
});
}
}
if (hasSupabase) {
SUPABASE_QUERY_RE.lastIndex = 0;
let m;
while ((m = SUPABASE_QUERY_RE.exec(content)) !== null) {
out.push({
filePath,
orm: 'supabase',
model: m[1],
method: m[2],
lineNumber: content.substring(0, m.index).split('\n').length - 1,
});
}
}
}
const processFileGroup = (
files: ParseWorkerInput[],
language: SupportedLanguages,
@ -1346,6 +1408,9 @@ const processFileGroup = (
const extractedRoutes = extractLaravelRoutes(tree, file.path);
result.routes.push(...extractedRoutes);
}
// Extract ORM queries (Prisma, Supabase)
extractORMQueries(file.path, file.content, result.ormQueries);
}
};
@ -1356,7 +1421,7 @@ const processFileGroup = (
/** Accumulated result across sub-batches */
let accumulated: ParseWorkerResult = {
nodes: [], relationships: [], symbols: [],
imports: [], calls: [], assignments: [], heritage: [], routes: [], fetchCalls: [], decoratorRoutes: [], toolDefs: [], constructorBindings: [], typeEnvBindings: [], skippedLanguages: {}, fileCount: 0,
imports: [], calls: [], assignments: [], heritage: [], routes: [], fetchCalls: [], decoratorRoutes: [], toolDefs: [], ormQueries: [], constructorBindings: [], typeEnvBindings: [], skippedLanguages: {}, fileCount: 0,
};
let cumulativeProcessed = 0;
@ -1372,6 +1437,7 @@ const mergeResult = (target: ParseWorkerResult, src: ParseWorkerResult) => {
target.fetchCalls.push(...src.fetchCalls);
target.decoratorRoutes.push(...src.decoratorRoutes);
target.toolDefs.push(...src.toolDefs);
target.ormQueries.push(...src.ormQueries);
target.constructorBindings.push(...src.constructorBindings);
target.typeEnvBindings.push(...src.typeEnvBindings);
for (const [lang, count] of Object.entries(src.skippedLanguages)) {
@ -1398,7 +1464,7 @@ parentPort!.on('message', (msg: any) => {
if (msg && msg.type === 'flush') {
parentPort!.postMessage({ type: 'result', data: accumulated });
// Reset for potential reuse
accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], assignments: [], heritage: [], routes: [], fetchCalls: [], decoratorRoutes: [], toolDefs: [], constructorBindings: [], typeEnvBindings: [], skippedLanguages: {}, fileCount: 0 };
accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], assignments: [], heritage: [], routes: [], fetchCalls: [], decoratorRoutes: [], toolDefs: [], ormQueries: [], constructorBindings: [], typeEnvBindings: [], skippedLanguages: {}, fileCount: 0 };
cumulativeProcessed = 0;
return;
}

View file

@ -29,7 +29,7 @@ export const REL_TABLE_NAME = 'CodeRelation';
// Valid relation types
// Note: WRAPS is reserved for future middleware graph traversal (not yet emitted)
export const REL_TYPES = ['CONTAINS', 'DEFINES', 'IMPORTS', 'CALLS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'ACCESSES', 'OVERRIDES', 'MEMBER_OF', 'STEP_IN_PROCESS', 'HANDLES_ROUTE', 'FETCHES', 'HANDLES_TOOL', 'ENTRY_POINT_OF', 'WRAPS'] as const;
export const REL_TYPES = ['CONTAINS', 'DEFINES', 'IMPORTS', 'CALLS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'ACCESSES', 'OVERRIDES', 'MEMBER_OF', 'STEP_IN_PROCESS', 'HANDLES_ROUTE', 'FETCHES', 'HANDLES_TOOL', 'ENTRY_POINT_OF', 'WRAPS', 'QUERIES'] as const;
export type RelType = typeof REL_TYPES[number];
// ============================================================================
@ -284,6 +284,7 @@ CREATE REL TABLE ${REL_TABLE_NAME} (
FROM Function TO \`Typedef\`,
FROM Function TO \`Union\`,
FROM Function TO \`Property\`,
FROM Function TO CodeElement,
FROM Class TO Method,
FROM Class TO Function,
FROM Class TO Class,
@ -317,6 +318,7 @@ CREATE REL TABLE ${REL_TABLE_NAME} (
FROM Method TO Interface,
FROM Method TO \`Constructor\`,
FROM Method TO \`Property\`,
FROM Method TO CodeElement,
FROM \`Template\` TO \`Template\`,
FROM \`Template\` TO Function,
FROM \`Template\` TO Method,

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,3 @@
import { createClient } from '@supabase/supabase-js';
export const supabase = createClient('https://example.supabase.co', 'anon-key');

View file

@ -0,0 +1,3 @@
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();

View file

@ -0,0 +1,25 @@
import { prisma } from './db';
export async function getUsers() {
return prisma.user.findMany({ where: { active: true } });
}
export async function createPost(title: string, userId: number) {
return prisma.post.create({ data: { title, authorId: userId } });
}
export async function getUserById(id: number) {
return prisma.user.findUnique({ where: { id } });
}
export async function updatePost(id: number, title: string) {
return prisma.post.update({ where: { id }, data: { title } });
}
export async function deletePost(id: number) {
return prisma.post.delete({ where: { id } });
}
export async function countUsers() {
return prisma.user.count();
}

View file

@ -0,0 +1,17 @@
import { supabase } from './client';
export async function getBookings() {
return supabase.from('bookings').select('*');
}
export async function createInterpreter(name: string) {
return supabase.from('interpreters').insert({ name, active: true });
}
export async function updateBooking(id: string, status: string) {
return supabase.from('bookings').update({ status }).eq('id', id);
}
export async function deleteSession(id: string) {
return supabase.from('sessions').delete().eq('id', id);
}

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

@ -0,0 +1,80 @@
/**
* Integration Tests: ORM Dataflow Detection (Prisma + Supabase)
*/
import { describe, it, expect, beforeAll } from 'vitest';
import path from 'path';
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
import type { PipelineResult } from '../../src/types/pipeline.js';
const ORM_REPO = path.resolve(__dirname, '..', 'fixtures', 'orm-repo');
describe('ORM dataflow detection', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(ORM_REPO, () => {});
}, 60000);
it('creates QUERIES edges for Prisma calls', () => {
const queryEdges: { source: string; target: string; reason: string }[] = [];
for (const rel of result.graph.iterRelationships()) {
if (rel.type === 'QUERIES') {
const source = result.graph.getNode(rel.sourceId);
const target = result.graph.getNode(rel.targetId);
if (source && target) {
queryEdges.push({
source: source.properties.filePath || source.properties.name,
target: target.properties.name,
reason: rel.reason ?? '',
});
}
}
}
const prismaEdges = queryEdges.filter(e => e.source.includes('prisma-service'));
const prismaModels = [...new Set(prismaEdges.map(e => e.target))];
expect(prismaModels).toContain('user');
expect(prismaModels).toContain('post');
const reasons = prismaEdges.map(e => e.reason);
expect(reasons.some(r => r.includes('prisma-findMany'))).toBe(true);
expect(reasons.some(r => r.includes('prisma-create'))).toBe(true);
});
it('creates QUERIES edges for Supabase calls', () => {
const queryEdges: { source: string; target: string; reason: string }[] = [];
for (const rel of result.graph.iterRelationships()) {
if (rel.type === 'QUERIES') {
const source = result.graph.getNode(rel.sourceId);
const target = result.graph.getNode(rel.targetId);
if (source && target) {
queryEdges.push({
source: source.properties.filePath || source.properties.name,
target: target.properties.name,
reason: rel.reason ?? '',
});
}
}
}
const supabaseEdges = queryEdges.filter(e => e.source.includes('supabase-service'));
const supabaseModels = [...new Set(supabaseEdges.map(e => e.target))];
expect(supabaseModels).toContain('bookings');
expect(supabaseModels).toContain('interpreters');
expect(supabaseModels).toContain('sessions');
const reasons = supabaseEdges.map(e => e.reason);
expect(reasons.some(r => r.includes('supabase-select'))).toBe(true);
expect(reasons.some(r => r.includes('supabase-insert'))).toBe(true);
});
it('creates CodeElement nodes for ORM models', () => {
const codeElements: string[] = [];
result.graph.forEachNode(n => {
if (n.label === 'CodeElement' && n.properties.description?.includes('model/table')) {
codeElements.push(n.properties.name);
}
});
expect(codeElements).toContain('user');
expect(codeElements).toContain('post');
expect(codeElements).toContain('bookings');
expect(codeElements).toContain('interpreters');
expect(codeElements).toContain('sessions');
});
});

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');
}
});
});
});