diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index 983afe064e..01d6d3f0a2 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -1,12 +1,12 @@ { "name": "gitnexus", - "version": "1.4.7", + "version": "1.4.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gitnexus", - "version": "1.4.7", + "version": "1.4.8", "hasInstallScript": true, "license": "PolyForm-Noncommercial-1.0.0", "dependencies": { @@ -56,9 +56,10 @@ "vitest": "^4.0.18" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "optionalDependencies": { + "tree-sitter-dart": "^1.0.0", "tree-sitter-kotlin": "^0.3.8", "tree-sitter-swift": "0.7.1" } @@ -3130,7 +3131,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -3948,6 +3948,13 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/nan": { + "version": "2.26.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.26.2.tgz", + "integrity": "sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==", + "license": "MIT", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -4185,7 +4192,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4965,7 +4971,6 @@ "integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" @@ -5069,6 +5074,17 @@ "node": "^18 || ^20 || >= 21" } }, + "node_modules/tree-sitter-dart": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tree-sitter-dart/-/tree-sitter-dart-1.0.0.tgz", + "integrity": "sha512-Ve5YMPJjjGW9LEsO+MngAOibQsw5obFp+bUT41pvwdcXWRwJImOWs3eaPi6AubEiBmc09qvhdvxeIXvxlhMnug==", + "hasInstallScript": true, + "license": "ISC", + "optional": true, + "dependencies": { + "nan": "^2.15.0" + } + }, "node_modules/tree-sitter-go": { "version": "0.21.2", "resolved": "https://registry.npmjs.org/tree-sitter-go/-/tree-sitter-go-0.21.2.tgz", @@ -5368,7 +5384,6 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -5489,7 +5504,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -5565,7 +5579,6 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", @@ -5849,7 +5862,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/gitnexus/package.json b/gitnexus/package.json index b422666ab0..2e7b1e686c 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -46,7 +46,7 @@ "test:watch": "vitest", "test:coverage": "vitest run --coverage", "prepare": "npm run build", - "postinstall": "node scripts/patch-tree-sitter-swift.cjs", + "postinstall": "node scripts/patch-tree-sitter-swift.cjs && node scripts/patch-tree-sitter-dart.cjs", "prepack": "npm run build && chmod +x dist/cli/index.js" }, "dependencies": { @@ -81,6 +81,7 @@ "uuid": "^13.0.0" }, "optionalDependencies": { + "tree-sitter-dart": "^1.0.0", "tree-sitter-kotlin": "^0.3.8", "tree-sitter-swift": "0.7.1" }, diff --git a/gitnexus/scripts/patch-tree-sitter-dart.cjs b/gitnexus/scripts/patch-tree-sitter-dart.cjs new file mode 100644 index 0000000000..f97c965928 --- /dev/null +++ b/gitnexus/scripts/patch-tree-sitter-dart.cjs @@ -0,0 +1,119 @@ +#!/usr/bin/env node +/** + * WORKAROUND: tree-sitter-dart@1.0.0 NAN → NAPI binding conversion + * + * Background: + * tree-sitter-dart@1.0.0 uses NAN (Native Abstractions for Node.js) for its + * native binding. NAN modules fail with "Module did not self-register" when + * loaded in Node.js worker_threads alongside NAPI-based tree-sitter modules. + * All other tree-sitter grammars in this project use NAPI, which is thread-safe. + * + * How this workaround works: + * 1. Replaces the NAN-based binding.cc with a NAPI-based one + * 2. Updates binding.gyp to use node-addon-api instead of nan + * 3. Installs node-addon-api as a local dependency + * 4. Rebuilds the native binding + * + * TODO: Remove this script when tree-sitter-dart publishes a version with NAPI + * bindings (or when an alternative package is available). + */ +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +const dartDir = path.join(__dirname, '..', 'node_modules', 'tree-sitter-dart'); +const bindingCcPath = path.join(dartDir, 'bindings', 'node', 'binding.cc'); +const bindingGypPath = path.join(dartDir, 'binding.gyp'); + +const NAPI_BINDING_CC = `#include + +typedef struct TSLanguage TSLanguage; + +extern "C" TSLanguage *tree_sitter_dart(); + +// "tree-sitter", "language" hashed with BLAKE2 +const napi_type_tag LANGUAGE_TYPE_TAG = { + 0x8AF2E5212AD58ABF, 0xD5006CAD83ABBA16 +}; + +Napi::Object Init(Napi::Env env, Napi::Object exports) { + exports["name"] = Napi::String::New(env, "dart"); + auto language = Napi::External::New(env, tree_sitter_dart()); + language.TypeTag(&LANGUAGE_TYPE_TAG); + exports["language"] = language; + return exports; +} + +NODE_API_MODULE(tree_sitter_dart_binding, Init) +`; + +const NAPI_BINDING_GYP = `{ + "targets": [ + { + "target_name": "tree_sitter_dart_binding", + "dependencies": [ + "; /** Pre-computed merged patterns (universal + language-specific) to avoid per-call array allocation. */ @@ -378,6 +393,8 @@ export function isTestFile(filePath: string): boolean { p.includes('.integrationtests/') || p.includes('.unittests/') || p.includes('/testproject/') || + // Dart/Flutter test patterns + p.endsWith('_test.dart') || // PHP/Laravel test patterns p.endsWith('test.php') || p.endsWith('spec.php') || diff --git a/gitnexus/src/core/ingestion/export-detection.ts b/gitnexus/src/core/ingestion/export-detection.ts index fba047bd24..4fb0d8a1fa 100644 --- a/gitnexus/src/core/ingestion/export-detection.ts +++ b/gitnexus/src/core/ingestion/export-detection.ts @@ -217,3 +217,5 @@ export const swiftExportChecker: ExportChecker = (node, _name) => { /** Ruby: all top-level definitions are public (no export syntax). */ export const rubyExportChecker: ExportChecker = (_node, _name) => true; +/** Dart: names starting with _ are library-private, everything else is public. */ +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..d91d4f3a8c 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 entry point + if (p.endsWith('/main.dart') || p.endsWith('/app.dart')) { + return { framework: 'flutter', entryPointMultiplier: 3.0, reason: 'flutter-main' }; + } + + // Flutter pages/screens (high confidence entry points) + if ((p.includes('/pages/') || p.includes('/screens/') || p.includes('/views/')) && p.endsWith('.dart')) { + return { framework: 'flutter', entryPointMultiplier: 2.5, reason: 'flutter-page' }; + } + + // Flutter presentation layer (controllers, providers, notifiers) + if (p.includes('/presentation/') && p.endsWith('.dart')) { + return { framework: 'flutter', entryPointMultiplier: 2.0, reason: 'flutter-presentation' }; + } + + // Flutter routes + if (p.includes('/routes/') && p.endsWith('.dart')) { + return { framework: 'flutter', entryPointMultiplier: 2.5, reason: 'flutter-routes' }; + } + + // Flutter widgets (moderate - reusable components) + if (p.includes('/widgets/') && p.endsWith('.dart')) { + return { framework: 'flutter', entryPointMultiplier: 1.5, reason: 'flutter-widget' }; + } + + // Flutter domain/services (moderate) + if ((p.includes('/domain/') || p.includes('/services/')) && p.endsWith('.dart')) { + return { framework: 'flutter', entryPointMultiplier: 1.8, reason: 'flutter-domain' }; + } + // ========== GENERIC PATTERNS ========== // Any language: index files in API folders @@ -478,6 +510,10 @@ 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': ['@override', 'Widget build', 'StatelessWidget', 'StatefulWidget', 'ConsumerWidget'], + 'riverpod': ['@riverpod', 'ref.watch', 'ref.read', 'AsyncNotifier', 'Notifier'], }; interface AstFrameworkPatternConfig { @@ -545,6 +581,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-provider', 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..c5cd4927cd --- /dev/null +++ b/gitnexus/src/core/ingestion/import-resolvers/dart.ts @@ -0,0 +1,46 @@ +/** + * Dart import resolution. + * Handles: dart: SDK (skip), package: (pubspec), relative (.dart) imports. + */ + +import type { ImportResult, ResolveCtx } from './types.js'; + +/** Dart: package: imports via pubspec.yaml, dart: SDK skipped, relative imports. */ +export function resolveDartImport( + rawImportPath: string, + filePath: string, + ctx: ResolveCtx, +): ImportResult { + // Skip SDK imports (dart:core, dart:async, etc.) + if (rawImportPath.startsWith('dart:')) return null; + + // package: imports — resolve via pubspec.yaml package name + if (rawImportPath.startsWith('package:')) { + const dartPubspec = ctx.configs.dartPubspec; + if (dartPubspec) { + const prefix = `package:${dartPubspec.packageName}/`; + if (rawImportPath.startsWith(prefix)) { + const relativePath = 'lib/' + rawImportPath.slice(prefix.length); + if (ctx.allFilePaths.has(relativePath)) { + return { kind: 'files', files: [relativePath] }; + } + } + } + // External package — not in repo + return null; + } + + // Relative imports (./foo.dart, ../bar.dart, bare relative) + const currentDir = filePath.split('/').slice(0, -1); + const parts = rawImportPath.replace(/^\.\//, '').split('/'); + const resolvedParts = [...currentDir]; + for (const part of parts) { + if (part === '..') resolvedParts.pop(); + else if (part !== '.') resolvedParts.push(part); + } + const resolved = resolvedParts.join('/'); + if (ctx.allFilePaths.has(resolved)) { + return { kind: 'files', files: [resolved] }; + } + return null; +} diff --git a/gitnexus/src/core/ingestion/import-resolvers/types.ts b/gitnexus/src/core/ingestion/import-resolvers/types.ts index 02e5fe1fce..7283850dcc 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/types.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/types.ts @@ -19,6 +19,11 @@ export type ImportResult = | { kind: 'package'; files: string[]; dirSuffix: string } | null; +/** Dart pubspec.yaml config for package: import resolution */ +export interface DartPubspecConfig { + packageName: string; +} + /** Bundled language-specific configs loaded once per ingestion run. */ export interface ImportConfigs { tsconfigPaths: TsconfigPaths | null; @@ -26,6 +31,7 @@ export interface ImportConfigs { composerConfig: ComposerConfig | null; swiftPackageConfig: SwiftPackageConfig | null; csharpConfigs: CSharpProjectConfig[]; + dartPubspec: DartPubspecConfig | null; } /** Pre-built lookup structures for import resolution. Build once, reuse across chunks. */ diff --git a/gitnexus/src/core/ingestion/language-config.ts b/gitnexus/src/core/ingestion/language-config.ts index 3e8653245a..457758dcc5 100644 --- a/gitnexus/src/core/ingestion/language-config.ts +++ b/gitnexus/src/core/ingestion/language-config.ts @@ -218,6 +218,24 @@ export async function loadSwiftPackageConfig(repoRoot: string): Promise { + try { + const pubspecPath = path.join(repoRoot, 'pubspec.yaml'); + const content = await fs.readFile(pubspecPath, 'utf-8'); + const match = content.match(/^name:\s*(\S+)/m); + if (match) { + if (isDev) { + console.log(`📦 Loaded Dart package name: ${match[1]}`); + } + return { packageName: match[1] }; + } + } catch { + // No pubspec.yaml + } + return null; +} + // ============================================================================ // BUNDLED CONFIG LOADER // ============================================================================ @@ -230,5 +248,6 @@ export async function loadImportConfigs(repoRoot: string): Promise; /** Get provider by language enum (always succeeds for SupportedLanguages). */ diff --git a/gitnexus/src/core/ingestion/pipeline.ts b/gitnexus/src/core/ingestion/pipeline.ts index f206e2620e..4e05277cd7 100644 --- a/gitnexus/src/core/ingestion/pipeline.ts +++ b/gitnexus/src/core/ingestion/pipeline.ts @@ -144,7 +144,7 @@ function needsSynthesis(lang: SupportedLanguages): boolean { } /** Synthesize namedImportMap entries for languages with whole-module imports. - * These languages (Go, Ruby, C/C++, Swift, Python) import all exported symbols from a + * These languages (Go, Ruby, C/C++, Swift, Dart, Python) import all exported symbols from a * file, not specific named symbols. After parsing, we know which symbols each file * exports (via graph isExported), so we can expand ImportMap edges into per-symbol * bindings that Phase 14 can use for cross-file type propagation. */ @@ -215,7 +215,7 @@ function synthesizeWildcardImportBindings( } }; - // Process files from ctx.importMap (Ruby, C/C++, Swift file-based imports) + // Process files from ctx.importMap (Ruby, C/C++, Swift, Dart file-based imports) for (const [filePath, importedFiles] of ctx.importMap) { const lang = getLanguageFromFilename(filePath); if (!lang || !isWildcardImportLanguage(lang)) continue; @@ -487,7 +487,7 @@ async function runScanAndStructure( * Reads source in byte-budget chunks (~20MB each). For each chunk: * 1. Parse via worker pool (or sequential fallback) * 2. Resolve imports from extracted data - * 3. Synthesize wildcard import bindings (Go/Ruby/C++/Swift/Python) + * 3. Synthesize wildcard import bindings (Go/Ruby/C++/Swift/Dart/Python) * 4. Resolve calls, heritage, routes concurrently (Promise.all) * 5. Collect TypeEnv bindings for cross-file propagation * @@ -675,7 +675,7 @@ async function runChunkedParseAndResolve( stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount }, }); }, repoPath, importCtx); - // ── Wildcard-import synthesis (Ruby / C/C++ / Swift / Go) + Python module aliases ─ + // ── Wildcard-import synthesis (Ruby / C/C++ / Swift / Go / Dart) + Python module aliases ─ // Synthesize namedImportMap entries for wildcard-import languages and build // moduleAliasMap for Python namespace imports. Must run after imports are resolved // (importMap is populated) but BEFORE call resolution. @@ -843,7 +843,7 @@ async function runChunkedParseAndResolve( // and that Phase 14 type propagation has complete namedImportMap data. const synthesized = synthesizeWildcardImportBindings(graph, ctx); if (isDev && synthesized > 0) { - console.log(`🔗 Synthesized ${synthesized} additional wildcard import bindings (Go/Ruby/C++/Swift/Python)`); + console.log(`🔗 Synthesized ${synthesized} additional wildcard import bindings (Go/Ruby/C++/Swift/Dart/Python)`); } // Free import resolution context — suffix index + resolve cache no longer needed diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index 6bbecbd9c8..b99d11da23 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -1001,4 +1001,98 @@ export const SWIFT_QUERIES = ` `; - \ No newline at end of file +// Dart queries - works with tree-sitter-dart (UserNobody14) +export const DART_QUERIES = ` +; ── Classes ────────────────────────────────────────────────────────────────── +(class_definition + name: (identifier) @name) @definition.class + +; ── Mixins ─────────────────────────────────────────────────────────────────── +(mixin_declaration + (identifier) @name) @definition.class + +; ── Enums ──────────────────────────────────────────────────────────────────── +(enum_declaration + name: (identifier) @name) @definition.enum + +; ── Extensions ─────────────────────────────────────────────────────────────── +(extension_declaration + name: (identifier) @name) @definition.class + +; ── Type aliases ───────────────────────────────────────────────────────────── +(type_alias + (type_identifier) @name) @definition.type + +; ── Top-level functions ────────────────────────────────────────────────────── +(program + (function_signature + name: (identifier) @name)) @definition.function + +; ── Methods (in classes) ───────────────────────────────────────────────────── +(class_body + (method_signature + (function_signature + name: (identifier) @name))) @definition.method + +; ── Methods (in extensions) ────────────────────────────────────────────────── +(extension_body + (method_signature + (function_signature + name: (identifier) @name))) @definition.method + +; ── Imports ────────────────────────────────────────────────────────────────── +(import_or_export + (library_import + (import_specification + (configurable_uri + (uri + (string_literal) @import.source))))) @import + +; ── Exports (re-exports create dependency edges too) ───────────────────────── +(import_or_export + (library_export + (configurable_uri + (uri + (string_literal) @import.source)))) @import + +; ── Function calls: direct calls like foo(args) ────────────────────────────── +(expression_statement + (identifier) @call.name + (selector + (argument_part))) @call + +; ── Method calls: obj.method(args), ClassName.staticMethod(args) ───────────── +(unconditional_assignable_selector + (identifier) @call.name) + +; ── Calls in return statements: return foo(args) ───────────────────────────── +(return_statement + (identifier) @call.name + (selector + (argument_part))) @call + +; ── Calls in variable assignments: var x = foo(args) ───────────────────────── +(initialized_variable_definition + value: (identifier) @call.name + value: (selector + (argument_part))) @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.implements))) @heritage.mixin +`; 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..18104dd50e --- /dev/null +++ b/gitnexus/src/core/ingestion/type-extractors/dart.ts @@ -0,0 +1,450 @@ +import type { SyntaxNode } from '../utils/ast-helpers.js'; +import type { + LanguageTypeConfig, + ParameterExtractor, + TypeBindingExtractor, + InitializerExtractor, + ClassNameLookup, + ConstructorBindingScanner, + PendingAssignmentExtractor, + PendingAssignment, + ForLoopExtractor, + LiteralTypeInferrer, + ConstructorTypeDetector, +} from './types.js'; +import { extractSimpleTypeName, extractVarName, extractElementTypeFromString, resolveIterableElementType } from './shared.js'; +import { findChild } from '../utils/ast-helpers.js'; + +// ── Dart ────────────────────────────────────────────────────────────────── + +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 ───────────────────────────────────────────────────────────── + +/** + * Parsed representation of the right-hand side of a Dart assignment. + * tree-sitter-dart uses a flat sibling structure: identifier + selector + selector + * rather than nested call_expression nodes. + */ +interface DartRHS { + /** First identifier after `=` (callee or receiver) */ + callee?: string; + /** Member access identifier from selector > unconditional_assignable_selector */ + member?: string; + /** Whether there's a call (selector > argument_part) */ + hasCall: boolean; + /** Whether value is wrapped in unary_expression > await_expression */ + isAwait: boolean; +} + +/** + * Parse RHS children from an initialized_variable_definition or await_expression. + * Handles the flat sibling structure: identifier + selector(member) + selector(call). + */ +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') { + // Member access: selector > unconditional_assignable_selector (.member) + // or selector > conditional_assignable_selector (?.member) + 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; + } + } + // Cascade: builder..add(1)..remove(2) — skip cascades for type inference + // (the overall expression type is the receiver, not the cascaded method result) + } + + return { callee, member, hasCall }; +} + +/** + * Parse the RHS of an initialized_variable_definition. + * Walks children after `=`, handling await wrapping. + */ +function parseDartRHS(node: SyntaxNode): DartRHS { + // Collect children after '=' + 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 }; + + // Check for await wrapping: unary_expression > await_expression + 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 }; +} + +/** Check if an initialized_variable_definition has an explicit type annotation. */ +function hasDartTypeAnnotation(node: SyntaxNode): boolean { + return !!findChild(node, 'type_identifier'); +} + +// ── Tier 0: Explicit Type Annotations ─────────────────────────────────── + +/** + * Dart: extract type from explicitly typed declarations. + * `User user = ...` or `List users = ...` + * + * tree-sitter-dart puts type_identifier as a positional child (NOT a field), + * so childForFieldName('type') returns null. + */ +const extractDartDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map): void => { + const typeNode = findChild(node, 'type_identifier'); + if (!typeNode) return; // var/final without type — skip (Tier 1 handles these) + 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); +}; + +/** + * Dart: extract type from function/method parameters. + * `void foo(User user, String name)` — type is a positional child, not a field. + */ +const extractDartParameter: ParameterExtractor = (node: SyntaxNode, env: Map): void => { + const typeNode = findChild(node, '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 ───────────────────────── + +/** + * Dart: infer type from constructor calls on untyped declarations. + * `var user = User()` or `final user = User.named()` + * + * Dart doesn't use `new`, so constructor calls look identical to function calls. + * Must verify against classNames to distinguish User() (constructor) from getUser() (function). + */ +const extractDartInitializer: InitializerExtractor = (node: SyntaxNode, env: Map, classNames: ClassNameLookup): void => { + if (node.type !== 'initialized_variable_definition') return; + // Skip if explicit type — Tier 0 handled it + 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; + + // Direct constructor: var user = User() + if (!rhs.member && classNames.has(rhs.callee)) { + env.set(varName, rhs.callee); + return; + } + + // Named constructor: var user = User.named() + if (rhs.member && classNames.has(rhs.callee)) { + env.set(varName, rhs.callee); + } +}; + +// ── Constructor Binding Scan (SymbolTable deferred validation) ────────── + +/** + * Dart: scan for untyped `var = callee()` patterns for return-type inference. + * Returns { varName, calleeName } for later verification against SymbolTable. + */ +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; + + // Direct call: var x = User() or var x = getUser() + if (rhs.hasCall && !rhs.member) { + return { varName, calleeName: rhs.callee }; + } + + // Qualified call: var x = svc.getUser() → callee is the method name + if (rhs.hasCall && rhs.member) { + return { varName, calleeName: rhs.member }; + } + + return undefined; +}; + +// ── Virtual Dispatch (Constructor Type Detection) ─────────────────────── + +/** + * Dart: detect constructor-style calls for virtual dispatch. + * For `Animal animal = Dog()`, returns 'Dog' when Dog is in classNames. + */ +const detectDartConstructorType: ConstructorTypeDetector = (node, classNames) => { + if (node.type !== 'initialized_variable_definition') return undefined; + + const rhs = parseDartRHS(node); + if (!rhs.callee || !rhs.hasCall) return undefined; + + // Direct: Dog() + if (!rhs.member && classNames.has(rhs.callee)) return rhs.callee; + // Named: Dog.named() + if (rhs.member && classNames.has(rhs.callee)) return rhs.callee; + + return undefined; +}; + +// ── Literal Type Inference (Overload Disambiguation) ──────────────────── + +/** + * Dart: map literal AST nodes to canonical Dart type names. + */ +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 ───────────────────────────────── + +/** + * Dart: extract pending assignments for fixpoint resolution. + * Handles: copy, callResult, fieldAccess, methodCallResult, and await wrapping. + */ +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; + + // Bare identifier: var copy = user → copy + if (!rhs.hasCall && !rhs.member) { + return { kind: 'copy', lhs, rhs: rhs.callee }; + } + + // Field access: var name = user.name → fieldAccess + if (!rhs.hasCall && rhs.member) { + return { kind: 'fieldAccess', lhs, receiver: rhs.callee, field: rhs.member }; + } + + // Direct call: var user = getUser() → callResult + if (rhs.hasCall && !rhs.member) { + return { kind: 'callResult', lhs, callee: rhs.callee }; + } + + // Method call: var result = user.save() → methodCallResult + if (rhs.hasCall && rhs.member) { + return { kind: 'methodCallResult', lhs, receiver: rhs.callee, method: rhs.member }; + } + + return undefined; +}; + +// ── For-Loop Element Type Resolution ──────────────────────────────────── + +/** + * Extract element type from a Dart type annotation node for for-in loops. + * Handles List, Set, Map, Iterable, etc. + */ +function extractDartElementTypeFromTypeNode(typeNode: SyntaxNode): string | undefined { + if (typeNode.type === 'type_identifier') { + // Look for type_arguments sibling within parent + const parent = typeNode.parent; + if (parent) { + const args = findChild(parent, 'type_arguments'); + if (args && args.namedChildCount >= 1) { + // Use last type arg (Map → V, List → T) + const lastArg = args.namedChild(args.namedChildCount - 1); + if (lastArg) return extractSimpleTypeName(lastArg); + } + } + } + return undefined; +} + +/** + * Dart: extract loop variable type from for-in statements. + * `for (var u in users)` — resolve element type from iterable. + * `for (User u in users)` — extract type directly. + */ +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; + + // Get loop variable name (name field) + const nameNode = loopParts.childForFieldName('name'); + if (!nameNode) return; + const loopVarName = nameNode.text; + if (!loopVarName) return; + + // Check for explicit type annotation (positional type_identifier child) + const typeNode = findChild(loopParts, 'type_identifier'); + if (typeNode) { + const typeName = extractSimpleTypeName(typeNode); + if (typeName && !scopeEnv.has(loopVarName)) { + (scopeEnv as Map).set(loopVarName, typeName); + } + return; + } + + // Untyped (var/final): resolve element type from iterable + 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') { + // await expression: for (var u in await getUsers()) + // Unwrap to find the inner call + const awaitExpr = findChild(iterableNode, 'await_expression'); + if (awaitExpr) { + const innerIdent = findChild(awaitExpr, 'identifier'); + if (innerIdent) iterableName = innerIdent.text; + } + if (!iterableName) return; + } + + // Check if iterable is a call: identifier + selector(argument_part) siblings in for_loop_parts + // For await iterables, selectors live inside the await_expression, not as siblings + if (iterableName) { + let hasCallSelector = false; + let memberName: string | undefined; + + // Determine where to scan for selectors + 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; + // For await: skip past the identifier we already found + if (child.type === 'identifier' && child.text === iterableName) { foundIterable = true; continue; } + // For non-await: skip past the iterable node + 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) { + // Call expression iterable: for (var u in getUsers()) or for (var u in svc.getUsers()) + 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/ast-helpers.ts b/gitnexus/src/core/ingestion/utils/ast-helpers.ts index 778699de0d..a5b90a8bff 100644 --- a/gitnexus/src/core/ingestion/utils/ast-helpers.ts +++ b/gitnexus/src/core/ingestion/utils/ast-helpers.ts @@ -82,6 +82,9 @@ export const FUNCTION_NODE_TYPES = new Set([ // Ruby 'method', // def foo 'singleton_method', // def self.foo + // Dart + 'function_signature', + 'method_signature', ]); /** @@ -111,6 +114,9 @@ export const CLASS_CONTAINER_TYPES = new Set([ // Kotlin 'object_declaration', 'companion_object', + // Dart + 'mixin_declaration', + 'extension_declaration', ]); export const CONTAINER_TYPE_TO_LABEL: Record = { @@ -132,6 +138,9 @@ export const CONTAINER_TYPE_TO_LABEL: Record = { module: 'Module', object_declaration: 'Class', companion_object: 'Class', + // Dart + mixin_declaration: 'Mixin', + extension_declaration: 'Class', }; /** Check if a Kotlin function_declaration capture is inside a class_body (i.e., a method). diff --git a/gitnexus/src/core/ingestion/utils/language-detection.ts b/gitnexus/src/core/ingestion/utils/language-detection.ts index 242f4729e0..7c750addf3 100644 --- a/gitnexus/src/core/ingestion/utils/language-detection.ts +++ b/gitnexus/src/core/ingestion/utils/language-detection.ts @@ -54,5 +54,7 @@ export const getLanguageFromFilename = (filename: string): SupportedLanguages | } // Swift (extensions) if (filename.endsWith('.swift')) return SupportedLanguages.Swift; + // Dart + 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..f85dcd537a 100644 --- a/gitnexus/src/core/ingestion/utils/noise-filter.ts +++ b/gitnexus/src/core/ingestion/utils/noise-filter.ts @@ -161,6 +161,10 @@ export const BUILT_IN_NAMES = new Set([ 'any?', 'all?', 'none?', 'count', 'first', 'last', 'sort_by', 'min_by', 'max_by', 'group_by', 'partition', 'compact', 'flatten', 'uniq', + // Dart/Flutter built-ins + 'print', 'debugPrint', 'setState', 'initState', 'dispose', 'didChangeDependencies', + 'didUpdateWidget', 'deactivate', 'reassemble', + 'runApp', 'debugDumpApp', 'debugDumpRenderTree', ]); /** Check if a name is a built-in function or common noise that should be filtered out */ diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index 03ea96b4e4..f62404a9dc 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -24,6 +24,10 @@ try { Swift = _require('tree-sitter-swift'); } catch {} // tree-sitter-kotlin is an optionalDependency — may not be installed let Kotlin: any = null; try { Kotlin = _require('tree-sitter-kotlin'); } catch {} + +// tree-sitter-dart is an optionalDependency — may not be installed +let Dart: any = null; +try { Dart = _require('tree-sitter-dart'); } catch {} import { getLanguageFromFilename } from '../utils/language-detection.js'; import { isBuiltInOrNoise } from '../utils/noise-filter.js'; import { @@ -239,6 +243,7 @@ const languageMap: Record = { [SupportedLanguages.PHP]: PHP.php_only, [SupportedLanguages.Ruby]: Ruby, ...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}), + ...(Dart ? { [SupportedLanguages.Dart]: Dart } : {}), }; /** diff --git a/gitnexus/src/core/tree-sitter/parser-loader.ts b/gitnexus/src/core/tree-sitter/parser-loader.ts index f7740ab6c1..e0a5322cec 100644 --- a/gitnexus/src/core/tree-sitter/parser-loader.ts +++ b/gitnexus/src/core/tree-sitter/parser-loader.ts @@ -22,6 +22,10 @@ try { Swift = _require('tree-sitter-swift'); } catch {} let Kotlin: any = null; try { Kotlin = _require('tree-sitter-kotlin'); } catch {} +// tree-sitter-dart is an optionalDependency — may not be installed +let Dart: any = null; +try { Dart = _require('tree-sitter-dart'); } catch {} + let parser: Parser | null = null; const languageMap: Record = { @@ -39,6 +43,7 @@ const languageMap: Record = { [SupportedLanguages.PHP]: PHP.php_only, [SupportedLanguages.Ruby]: Ruby, ...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}), + ...(Dart ? { [SupportedLanguages.Dart]: Dart } : {}), }; export const isLanguageAvailable = (language: SupportedLanguages): boolean =>