diff --git a/gitnexus-web/src/config/supported-languages.ts b/gitnexus-web/src/config/supported-languages.ts index 4c51b56abc..b1c61e7453 100644 --- a/gitnexus-web/src/config/supported-languages.ts +++ b/gitnexus-web/src/config/supported-languages.ts @@ -12,4 +12,5 @@ export enum SupportedLanguages { Ruby = 'ruby', Kotlin = 'kotlin', Swift = 'swift', -} \ No newline at end of file + Dart = 'dart', +} diff --git a/gitnexus-web/src/core/ingestion/call-routing.ts b/gitnexus-web/src/core/ingestion/call-routing.ts index 5a112aca15..cf5a3217b1 100644 --- a/gitnexus-web/src/core/ingestion/call-routing.ts +++ b/gitnexus-web/src/core/ingestion/call-routing.ts @@ -41,6 +41,7 @@ export const callRouters = { [SupportedLanguages.C]: noRouting, [SupportedLanguages.Ruby]: routeRubyCall, [SupportedLanguages.Kotlin]: noRouting, + [SupportedLanguages.Dart]: noRouting, } satisfies Record; // ── Result types ──────────────────────────────────────────────────────────── diff --git a/gitnexus-web/src/core/ingestion/framework-detection.ts b/gitnexus-web/src/core/ingestion/framework-detection.ts index b190e4baf5..0cc76d5d92 100644 --- a/gitnexus-web/src/core/ingestion/framework-detection.ts +++ b/gitnexus-web/src/core/ingestion/framework-detection.ts @@ -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'], diff --git a/gitnexus-web/src/core/ingestion/parsing-processor.ts b/gitnexus-web/src/core/ingestion/parsing-processor.ts index a063db6ccd..9421990a1f 100644 --- a/gitnexus-web/src/core/ingestion/parsing-processor.ts +++ b/gitnexus-web/src/core/ingestion/parsing-processor.ts @@ -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; } diff --git a/gitnexus-web/src/core/ingestion/tree-sitter-queries.ts b/gitnexus-web/src/core/ingestion/tree-sitter-queries.ts index 65b67bcafd..8a5a7eeb23 100644 --- a/gitnexus-web/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus-web/src/core/ingestion/tree-sitter-queries.ts @@ -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.TypeScript]: TYPESCRIPT_QUERIES, [SupportedLanguages.JavaScript]: JAVASCRIPT_QUERIES, @@ -497,5 +593,6 @@ export const LANGUAGE_QUERIES: Record = { [SupportedLanguages.Ruby]: RUBY_QUERIES, [SupportedLanguages.Kotlin]: '', // Kotlin WASM parser not yet available for web [SupportedLanguages.Swift]: SWIFT_QUERIES, + [SupportedLanguages.Dart]: DART_QUERIES, }; \ No newline at end of file diff --git a/gitnexus-web/src/core/ingestion/utils.ts b/gitnexus-web/src/core/ingestion/utils.ts index 22bb6d72e1..0a6a5213f5 100644 --- a/gitnexus-web/src/core/ingestion/utils.ts +++ b/gitnexus-web/src/core/ingestion/utils.ts @@ -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; }; diff --git a/gitnexus-web/src/core/tree-sitter/parser-loader.ts b/gitnexus-web/src/core/tree-sitter/parser-loader.ts index 44bc7c37d6..51142a2c0d 100644 --- a/gitnexus-web/src/core/tree-sitter/parser-loader.ts +++ b/gitnexus-web/src/core/tree-sitter/parser-loader.ts @@ -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]; diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index f7239b237c..135312bdce 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -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", diff --git a/gitnexus/package.json b/gitnexus/package.json index b58767be40..dfb3d2711f 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -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" }, diff --git a/gitnexus/src/config/supported-languages.ts b/gitnexus/src/config/supported-languages.ts index 4ddd085fa6..a35c3d2b11 100644 --- a/gitnexus/src/config/supported-languages.ts +++ b/gitnexus/src/config/supported-languages.ts @@ -41,4 +41,5 @@ export enum SupportedLanguages { PHP = 'php', Kotlin = 'kotlin', Swift = 'swift', -} \ No newline at end of file + Dart = 'dart', +} diff --git a/gitnexus/src/core/ingestion/entry-point-scoring.ts b/gitnexus/src/core/ingestion/entry-point-scoring.ts index 4c6f58cbe3..f6128f9fd8 100644 --- a/gitnexus/src/core/ingestion/entry-point-scoring.ts +++ b/gitnexus/src/core/ingestion/entry-point-scoring.ts @@ -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; /** Pre-computed merged patterns (universal + language-specific) to avoid per-call array allocation. */ diff --git a/gitnexus/src/core/ingestion/export-detection.ts b/gitnexus/src/core/ingestion/export-detection.ts index fba047bd24..cea8908e08 100644 --- a/gitnexus/src/core/ingestion/export-detection.ts +++ b/gitnexus/src/core/ingestion/export-detection.ts @@ -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('_'); + diff --git a/gitnexus/src/core/ingestion/framework-detection.ts b/gitnexus/src/core/ingestion/framework-detection.ts index 0eb8fca942..2c0dd7950f 100644 --- a/gitnexus/src/core/ingestion/framework-detection.ts +++ b/gitnexus/src/core/ingestion/framework-detection.ts @@ -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; /** Pre-lowercased patterns for O(1) pattern matching at runtime */ diff --git a/gitnexus/src/core/ingestion/import-resolvers/dart.ts b/gitnexus/src/core/ingestion/import-resolvers/dart.ts new file mode 100644 index 0000000000..0a9511c80e --- /dev/null +++ b/gitnexus/src/core/ingestion/import-resolvers/dart.ts @@ -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/ + 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); +} diff --git a/gitnexus/src/core/ingestion/languages/dart.ts b/gitnexus/src/core/ingestion/languages/dart.ts new file mode 100644 index 0000000000..8b4dd3ee5a --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/dart.ts @@ -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', +}); diff --git a/gitnexus/src/core/ingestion/languages/index.ts b/gitnexus/src/core/ingestion/languages/index.ts index 8e456cde6b..d9984f990d 100644 --- a/gitnexus/src/core/ingestion/languages/index.ts +++ b/gitnexus/src/core/ingestion/languages/index.ts @@ -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; /** Get provider by language enum (always succeeds for SupportedLanguages). */ diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index 6bbecbd9c8..9b7b456935 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -1001,4 +1001,137 @@ export const SWIFT_QUERIES = ` `; - \ No newline at end of file +// 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.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, +}; diff --git a/gitnexus/src/core/ingestion/type-extractors/dart.ts b/gitnexus/src/core/ingestion/type-extractors/dart.ts new file mode 100644 index 0000000000..394730ad45 --- /dev/null +++ b/gitnexus/src/core/ingestion/type-extractors/dart.ts @@ -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 = new Set([ + 'initialized_variable_definition', + 'initialized_identifier', +]); + +const DART_FOR_LOOP_NODE_TYPES: ReadonlySet = new Set([ + 'for_statement', +]); + +// ── Helpers ───────────────────────────────────────────────────────────── + +interface DartRHS { + callee?: string; + member?: string; + hasCall: boolean; + isAwait: boolean; +} + +function parseDartRHSChildren(children: Iterable): Omit { + 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): 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): 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, 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).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).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, +}; diff --git a/gitnexus/src/core/ingestion/utils/language-detection.ts b/gitnexus/src/core/ingestion/utils/language-detection.ts index 242f4729e0..a0c08bfb79 100644 --- a/gitnexus/src/core/ingestion/utils/language-detection.ts +++ b/gitnexus/src/core/ingestion/utils/language-detection.ts @@ -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; }; diff --git a/gitnexus/src/core/ingestion/utils/noise-filter.ts b/gitnexus/src/core/ingestion/utils/noise-filter.ts index ab2b408ed1..b1516b4d96 100644 --- a/gitnexus/src/core/ingestion/utils/noise-filter.ts +++ b/gitnexus/src/core/ingestion/utils/noise-filter.ts @@ -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', diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index 03ea96b4e4..4f7a78edfa 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -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 = { ...(Kotlin ? { [SupportedLanguages.Kotlin]: Kotlin } : {}), [SupportedLanguages.PHP]: PHP.php_only, [SupportedLanguages.Ruby]: Ruby, + ...(Dart ? { [SupportedLanguages.Dart]: Dart } : {}), ...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}), }; diff --git a/gitnexus/src/core/tree-sitter/parser-loader.ts b/gitnexus/src/core/tree-sitter/parser-loader.ts index f7740ab6c1..dc41583283 100644 --- a/gitnexus/src/core/tree-sitter/parser-loader.ts +++ b/gitnexus/src/core/tree-sitter/parser-loader.ts @@ -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 = { ...(Kotlin ? { [SupportedLanguages.Kotlin]: Kotlin } : {}), [SupportedLanguages.PHP]: PHP.php_only, [SupportedLanguages.Ruby]: Ruby, + ...(Dart ? { [SupportedLanguages.Dart]: Dart } : {}), ...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}), }; diff --git a/gitnexus/test/fixtures/sample-code/dart-advanced.dart b/gitnexus/test/fixtures/sample-code/dart-advanced.dart new file mode 100644 index 0000000000..1d24363a03 --- /dev/null +++ b/gitnexus/test/fixtures/sample-code/dart-advanced.dart @@ -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 _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 names = []; +} + +// ── Type resolution: constructor inference ────────────────────────────── +void constructorInference() { + var dog = Dog('Rex', 'GSD'); + final repo = Repository(); + var named = Dog.unknown(); +} + +// ── Type resolution: for-loop ────────────────────────────────────────── +void loopTypes(List 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 createState() => _CounterPageState(); +} + +class _CounterPageState extends State { + 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 { + @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 { + void initState() {} + void dispose() {} +} +class Widget {} +class BuildContext {} +class Text extends Widget { + Text(String text); +} +class Bloc { + 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 {} +class UserEvent {} +class UserState {} + +// ── Helper functions for call extraction tests ───────────────────────── +List fetchUsers() => []; +String processData() => ''; +String formatOutput() => ''; +int computeScore() => 0; +User loadUser() => User('test'); diff --git a/gitnexus/test/fixtures/sample-code/simple.dart b/gitnexus/test/fixtures/sample-code/simple.dart new file mode 100644 index 0000000000..efdfa5a300 --- /dev/null +++ b/gitnexus/test/fixtures/sample-code/simple.dart @@ -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; +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 { + final List _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 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')); +} diff --git a/gitnexus/test/integration/tree-sitter-languages.test.ts b/gitnexus/test/integration/tree-sitter-languages.test.ts index 1d857a744e..2227cea47c 100644 --- a/gitnexus/test/integration/tree-sitter-languages.test.ts +++ b/gitnexus/test/integration/tree-sitter-languages.test.ts @@ -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 = {}; + 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) { diff --git a/gitnexus/test/unit/dart-import-resolver.test.ts b/gitnexus/test/unit/dart-import-resolver.test.ts new file mode 100644 index 0000000000..e62b7b1caf --- /dev/null +++ b/gitnexus/test/unit/dart-import-resolver.test.ts @@ -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(); + }); + }); +}); diff --git a/gitnexus/test/unit/dart-type-extractor.test.ts b/gitnexus/test/unit/dart-type-extractor.test.ts new file mode 100644 index 0000000000..48cc6bbc0f --- /dev/null +++ b/gitnexus/test/unit/dart-type-extractor.test.ts @@ -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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + const classNames = new Set(); + 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(); + 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 users) { for (User u in users) {} }', 'for_statement'); + expect(nodes.length).toBeGreaterThan(0); + const scopeEnv = new Map(); + 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(); + const ctx = { + scopeEnv, + declarationTypeNodes: new Map(), + scope: 'test@0', + returnTypeLookup: { + lookupReturnType: (name: string) => name === 'getUsers' ? 'User' : undefined, + lookupRawReturnType: (name: string) => name === 'getUsers' ? 'List' : 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(); + 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(); + 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(); + const nodes = parseAndFindNodes(parser, 'void f() { List 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(); + 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(); + 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'); + } + }); + }); +});