mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
feat: add Dart language support (#204)
This commit is contained in:
parent
1a0784befe
commit
8864a0290c
27 changed files with 1753 additions and 5 deletions
|
|
@ -12,4 +12,5 @@ export enum SupportedLanguages {
|
|||
Ruby = 'ruby',
|
||||
Kotlin = 'kotlin',
|
||||
Swift = 'swift',
|
||||
}
|
||||
Dart = 'dart',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export const callRouters = {
|
|||
[SupportedLanguages.C]: noRouting,
|
||||
[SupportedLanguages.Ruby]: routeRubyCall,
|
||||
[SupportedLanguages.Kotlin]: noRouting,
|
||||
[SupportedLanguages.Dart]: noRouting,
|
||||
} satisfies Record<SupportedLanguages, CallRouter>;
|
||||
|
||||
// ── Result types ────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -483,6 +483,102 @@ export const SWIFT_QUERIES = `
|
|||
(inheritance_specifier inherits_from: (user_type (type_identifier) @heritage.extends))) @heritage
|
||||
`;
|
||||
|
||||
// Dart queries - works with tree-sitter-dart
|
||||
export const DART_QUERIES = `
|
||||
; ── Classes ──────────────────────────────────────────────────────────────────
|
||||
(class_definition
|
||||
name: (identifier) @name) @definition.class
|
||||
|
||||
; ── Mixins ───────────────────────────────────────────────────────────────────
|
||||
(mixin_declaration
|
||||
(identifier) @name) @definition.trait
|
||||
|
||||
; ── Extensions ───────────────────────────────────────────────────────────────
|
||||
(extension_declaration
|
||||
name: (identifier) @name) @definition.class
|
||||
|
||||
; ── Enums ────────────────────────────────────────────────────────────────────
|
||||
(enum_declaration
|
||||
name: (identifier) @name) @definition.enum
|
||||
|
||||
; ── Type aliases ─────────────────────────────────────────────────────────────
|
||||
; Anchor "=" after the name to avoid capturing the RHS type
|
||||
(type_alias
|
||||
(type_identifier) @name
|
||||
"=") @definition.type
|
||||
|
||||
; ── Top-level functions (parent is program, not method_signature) ────────────
|
||||
(program
|
||||
(function_signature
|
||||
name: (identifier) @name) @definition.function)
|
||||
|
||||
; ── Abstract method declarations (function_signature inside class body declaration) ──
|
||||
(declaration
|
||||
(function_signature
|
||||
name: (identifier) @name)) @definition.method
|
||||
|
||||
; ── Methods (inside class/mixin/extension bodies) ────────────────────────────
|
||||
(method_signature
|
||||
(function_signature
|
||||
name: (identifier) @name)) @definition.method
|
||||
|
||||
; ── Constructors ─────────────────────────────────────────────────────────────
|
||||
(constructor_signature
|
||||
name: (identifier) @name) @definition.constructor
|
||||
|
||||
; ── Factory constructors ─────────────────────────────────────────────────────
|
||||
(method_signature
|
||||
(factory_constructor_signature
|
||||
(identifier) @name)) @definition.constructor
|
||||
|
||||
; ── Getters ──────────────────────────────────────────────────────────────────
|
||||
(method_signature
|
||||
(getter_signature
|
||||
name: (identifier) @name)) @definition.property
|
||||
|
||||
; ── Setters ──────────────────────────────────────────────────────────────────
|
||||
(method_signature
|
||||
(setter_signature
|
||||
name: (identifier) @name)) @definition.property
|
||||
|
||||
; ── Imports ──────────────────────────────────────────────────────────────────
|
||||
(import_or_export
|
||||
(library_import
|
||||
(import_specification
|
||||
(configurable_uri) @import.source))) @import
|
||||
|
||||
; ── Calls: direct function/constructor calls (identifier immediately before argument_part) ──
|
||||
(expression_statement
|
||||
(identifier) @call.name
|
||||
.
|
||||
(selector (argument_part))) @call
|
||||
|
||||
; ── Calls: method calls (obj.method()) ───────────────────────────────────────
|
||||
(expression_statement
|
||||
(selector
|
||||
(unconditional_assignable_selector
|
||||
(identifier) @call.name))) @call
|
||||
|
||||
; ── Heritage: extends ────────────────────────────────────────────────────────
|
||||
(class_definition
|
||||
name: (identifier) @heritage.class
|
||||
superclass: (superclass
|
||||
(type_identifier) @heritage.extends)) @heritage
|
||||
|
||||
; ── Heritage: implements ─────────────────────────────────────────────────────
|
||||
(class_definition
|
||||
name: (identifier) @heritage.class
|
||||
interfaces: (interfaces
|
||||
(type_identifier) @heritage.implements)) @heritage.impl
|
||||
|
||||
; ── Heritage: with (mixins) ──────────────────────────────────────────────────
|
||||
(class_definition
|
||||
name: (identifier) @heritage.class
|
||||
superclass: (superclass
|
||||
(mixins
|
||||
(type_identifier) @heritage.trait))) @heritage
|
||||
`;
|
||||
|
||||
export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
|
||||
[SupportedLanguages.TypeScript]: TYPESCRIPT_QUERIES,
|
||||
[SupportedLanguages.JavaScript]: JAVASCRIPT_QUERIES,
|
||||
|
|
@ -497,5 +593,6 @@ export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
|
|||
[SupportedLanguages.Ruby]: RUBY_QUERIES,
|
||||
[SupportedLanguages.Kotlin]: '', // Kotlin WASM parser not yet available for web
|
||||
[SupportedLanguages.Swift]: SWIFT_QUERIES,
|
||||
[SupportedLanguages.Dart]: DART_QUERIES,
|
||||
};
|
||||
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
28
gitnexus/package-lock.json
generated
28
gitnexus/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -41,4 +41,5 @@ export enum SupportedLanguages {
|
|||
PHP = 'php',
|
||||
Kotlin = 'kotlin',
|
||||
Swift = 'swift',
|
||||
}
|
||||
Dart = 'dart',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -212,6 +212,20 @@ export const ENTRY_POINT_PATTERNS = {
|
|||
/^perform$/, // Background jobs (Sidekiq, ActiveJob)
|
||||
/^execute$/, // Command pattern
|
||||
],
|
||||
|
||||
// Dart / Flutter
|
||||
[SupportedLanguages.Dart]: [
|
||||
/^main$/, // App entry
|
||||
/^build$/, // Widget.build — fundamental Flutter render entry point
|
||||
/^createState$/, // StatefulWidget.createState
|
||||
/^initState$/, // State lifecycle initialization
|
||||
/^dispose$/, // State lifecycle teardown
|
||||
/^didChangeDependencies$/, // State lifecycle — InheritedWidget changes
|
||||
/^didUpdateWidget$/, // State lifecycle — widget rebuild with new config
|
||||
/^runApp$/, // App entry point
|
||||
/^onEvent$/, // BLoC event handler
|
||||
/^mapEventToState$/, // Legacy BLoC pattern
|
||||
],
|
||||
} satisfies Record<SupportedLanguages, RegExp[]>;
|
||||
|
||||
/** Pre-computed merged patterns (universal + language-specific) to avoid per-call array allocation. */
|
||||
|
|
|
|||
|
|
@ -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('_');
|
||||
|
||||
|
|
|
|||
|
|
@ -406,6 +406,38 @@ export function detectFrameworkFromPath(filePath: string): FrameworkHint | null
|
|||
return { framework: 'ios-router', entryPointMultiplier: 2.0, reason: 'ios-router' };
|
||||
}
|
||||
|
||||
// ========== DART / FLUTTER ==========
|
||||
|
||||
// Flutter main/app entry points
|
||||
if (p.endsWith('/main.dart') || p.endsWith('/app.dart')) {
|
||||
return { framework: 'flutter', entryPointMultiplier: 3.0, reason: 'flutter-main' };
|
||||
}
|
||||
|
||||
// Flutter screens/pages/views (high priority - route entry points)
|
||||
if ((p.includes('/screens/') || p.includes('/pages/') || p.includes('/views/')) && p.endsWith('.dart')) {
|
||||
return { framework: 'flutter', entryPointMultiplier: 2.5, reason: 'flutter-screen' };
|
||||
}
|
||||
|
||||
// Flutter routes
|
||||
if (p.includes('/routes/') && p.endsWith('.dart')) {
|
||||
return { framework: 'flutter', entryPointMultiplier: 2.5, reason: 'flutter-routes' };
|
||||
}
|
||||
|
||||
// Flutter BLoC / controllers / presentation (state management entry points)
|
||||
if ((p.includes('/bloc/') || p.includes('/controllers/') || p.includes('/cubit/') || p.includes('/presentation/')) && p.endsWith('.dart')) {
|
||||
return { framework: 'flutter', entryPointMultiplier: 2.0, reason: 'flutter-state-management' };
|
||||
}
|
||||
|
||||
// Flutter services / domain
|
||||
if ((p.includes('/services/') || p.includes('/domain/')) && p.endsWith('.dart')) {
|
||||
return { framework: 'flutter', entryPointMultiplier: 1.8, reason: 'flutter-service' };
|
||||
}
|
||||
|
||||
// Flutter widgets (reusable components)
|
||||
if (p.includes('/widgets/') && p.endsWith('.dart')) {
|
||||
return { framework: 'flutter', entryPointMultiplier: 1.5, reason: 'flutter-widget' };
|
||||
}
|
||||
|
||||
// ========== GENERIC PATTERNS ==========
|
||||
|
||||
// Any language: index files in API folders
|
||||
|
|
@ -478,6 +510,11 @@ export const FRAMEWORK_AST_PATTERNS = {
|
|||
'rails': ['ApplicationController', 'ApplicationRecord', 'ActiveRecord::Base',
|
||||
'before_action', 'after_action', 'has_many', 'belongs_to', 'has_one', 'validates'],
|
||||
'sinatra': ['Sinatra::Base', 'Sinatra::Application'],
|
||||
|
||||
// Dart/Flutter
|
||||
'flutter': ['StatelessWidget', 'StatefulWidget', 'BuildContext', 'Widget build',
|
||||
'ChangeNotifier', 'GetxController', 'Cubit<', 'Bloc<', 'ConsumerWidget'],
|
||||
'riverpod': ['@riverpod', 'ref.watch', 'ref.read', 'AsyncNotifier', 'Notifier'],
|
||||
};
|
||||
|
||||
interface AstFrameworkPatternConfig {
|
||||
|
|
@ -545,6 +582,10 @@ export const AST_FRAMEWORK_PATTERNS_BY_LANGUAGE = {
|
|||
{ framework: 'rails', entryPointMultiplier: 3.0, reason: 'rails-pattern', patterns: FRAMEWORK_AST_PATTERNS.rails },
|
||||
{ framework: 'sinatra', entryPointMultiplier: 2.8, reason: 'sinatra-pattern', patterns: FRAMEWORK_AST_PATTERNS.sinatra },
|
||||
],
|
||||
[SupportedLanguages.Dart]: [
|
||||
{ framework: 'flutter', entryPointMultiplier: 2.5, reason: 'flutter-widget', patterns: FRAMEWORK_AST_PATTERNS.flutter },
|
||||
{ framework: 'riverpod', entryPointMultiplier: 2.8, reason: 'riverpod-pattern', patterns: FRAMEWORK_AST_PATTERNS.riverpod },
|
||||
],
|
||||
} satisfies Record<SupportedLanguages, AstFrameworkPatternConfig[]>;
|
||||
|
||||
/** Pre-lowercased patterns for O(1) pattern matching at runtime */
|
||||
|
|
|
|||
44
gitnexus/src/core/ingestion/import-resolvers/dart.ts
Normal file
44
gitnexus/src/core/ingestion/import-resolvers/dart.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* Dart import resolution.
|
||||
* Handles package: imports (local packages) and relative imports.
|
||||
* SDK imports (dart:*) and external packages are skipped.
|
||||
*/
|
||||
|
||||
import type { ImportResult, ResolveCtx } from './types.js';
|
||||
import { resolveStandard } from './standard.js';
|
||||
import { SupportedLanguages } from '../../../config/supported-languages.js';
|
||||
|
||||
export function resolveDartImport(
|
||||
rawImportPath: string,
|
||||
filePath: string,
|
||||
ctx: ResolveCtx,
|
||||
): ImportResult {
|
||||
// Strip surrounding quotes from configurable_uri capture
|
||||
const stripped = rawImportPath.replace(/^['"]|['"]$/g, '');
|
||||
|
||||
// Skip dart: SDK imports (dart:async, dart:io, etc.)
|
||||
if (stripped.startsWith('dart:')) return null;
|
||||
|
||||
// Local package: imports → resolve to lib/<path>
|
||||
if (stripped.startsWith('package:')) {
|
||||
const slashIdx = stripped.indexOf('/');
|
||||
if (slashIdx === -1) return null;
|
||||
const relPath = stripped.slice(slashIdx + 1);
|
||||
const candidates = [`lib/${relPath}`, relPath];
|
||||
const files: string[] = [];
|
||||
for (const candidate of candidates) {
|
||||
for (const fp of ctx.allFileList) {
|
||||
if (fp.endsWith('/' + candidate) || fp === candidate) {
|
||||
files.push(fp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (files.length > 0) break;
|
||||
}
|
||||
if (files.length > 0) return { kind: 'files', files };
|
||||
return null;
|
||||
}
|
||||
|
||||
// Relative imports — use standard resolution
|
||||
return resolveStandard(stripped, filePath, ctx, SupportedLanguages.Dart);
|
||||
}
|
||||
25
gitnexus/src/core/ingestion/languages/dart.ts
Normal file
25
gitnexus/src/core/ingestion/languages/dart.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* Dart Language Provider
|
||||
*
|
||||
* Dart traits:
|
||||
* - importSemantics: 'wildcard' (Dart imports bring everything public into scope)
|
||||
* - exportChecker: public if no leading underscore
|
||||
* - Dart SDK imports (dart:*) and external packages are skipped
|
||||
*/
|
||||
|
||||
import { SupportedLanguages } from '../../../config/supported-languages.js';
|
||||
import { defineLanguage } from '../language-provider.js';
|
||||
import { typeConfig as dartConfig } from '../type-extractors/dart.js';
|
||||
import { dartExportChecker } from '../export-detection.js';
|
||||
import { resolveDartImport } from '../import-resolvers/dart.js';
|
||||
import { DART_QUERIES } from '../tree-sitter-queries.js';
|
||||
|
||||
export const dartProvider = defineLanguage({
|
||||
id: SupportedLanguages.Dart,
|
||||
extensions: ['.dart'],
|
||||
treeSitterQueries: DART_QUERIES,
|
||||
typeConfig: dartConfig,
|
||||
exportChecker: dartExportChecker,
|
||||
importResolver: resolveDartImport,
|
||||
importSemantics: 'wildcard',
|
||||
});
|
||||
|
|
@ -22,6 +22,7 @@ import { cProvider, cppProvider } from './c-cpp.js';
|
|||
import { phpProvider } from './php.js';
|
||||
import { rubyProvider } from './ruby.js';
|
||||
import { swiftProvider } from './swift.js';
|
||||
import { dartProvider } from './dart.js';
|
||||
|
||||
export const providers = {
|
||||
[SupportedLanguages.JavaScript]: javascriptProvider,
|
||||
|
|
@ -37,6 +38,7 @@ export const providers = {
|
|||
[SupportedLanguages.PHP]: phpProvider,
|
||||
[SupportedLanguages.Ruby]: rubyProvider,
|
||||
[SupportedLanguages.Swift]: swiftProvider,
|
||||
[SupportedLanguages.Dart]: dartProvider,
|
||||
} satisfies Record<SupportedLanguages, LanguageProvider>;
|
||||
|
||||
/** Get provider by language enum (always succeeds for SupportedLanguages). */
|
||||
|
|
|
|||
|
|
@ -1001,4 +1001,137 @@ export const SWIFT_QUERIES = `
|
|||
|
||||
`;
|
||||
|
||||
|
||||
// Dart queries - works with tree-sitter-dart (UserNobody14/tree-sitter-dart, ABI 14)
|
||||
// Note: Dart grammar has function_signature/method_signature as wrappers;
|
||||
// top-level functions are (program > function_signature),
|
||||
// methods inside classes are (method_signature > function_signature).
|
||||
// We match top-level functions via (program (function_signature ...)) to avoid
|
||||
// double-counting methods that also contain function_signature.
|
||||
export const DART_QUERIES = `
|
||||
; ── Classes ──────────────────────────────────────────────────────────────────
|
||||
(class_definition
|
||||
name: (identifier) @name) @definition.class
|
||||
|
||||
; ── Mixins ───────────────────────────────────────────────────────────────────
|
||||
(mixin_declaration
|
||||
(identifier) @name) @definition.trait
|
||||
|
||||
; ── Extensions ───────────────────────────────────────────────────────────────
|
||||
(extension_declaration
|
||||
name: (identifier) @name) @definition.class
|
||||
|
||||
; ── Enums ────────────────────────────────────────────────────────────────────
|
||||
(enum_declaration
|
||||
name: (identifier) @name) @definition.enum
|
||||
|
||||
; ── Type aliases ─────────────────────────────────────────────────────────────
|
||||
; Anchor "=" after the name to avoid capturing the RHS type
|
||||
(type_alias
|
||||
(type_identifier) @name
|
||||
"=") @definition.type
|
||||
|
||||
; ── Top-level functions (parent is program, not method_signature) ────────────
|
||||
(program
|
||||
(function_signature
|
||||
name: (identifier) @name) @definition.function)
|
||||
|
||||
; ── Abstract method declarations (function_signature inside class body declaration) ──
|
||||
(declaration
|
||||
(function_signature
|
||||
name: (identifier) @name)) @definition.method
|
||||
|
||||
; ── Methods (inside class/mixin/extension bodies) ────────────────────────────
|
||||
(method_signature
|
||||
(function_signature
|
||||
name: (identifier) @name)) @definition.method
|
||||
|
||||
; ── Constructors ─────────────────────────────────────────────────────────────
|
||||
(constructor_signature
|
||||
name: (identifier) @name) @definition.constructor
|
||||
|
||||
; ── Factory constructors (anchor before param list to capture variant name, not class) ──
|
||||
(method_signature
|
||||
(factory_constructor_signature
|
||||
(identifier) @name . (formal_parameter_list))) @definition.constructor
|
||||
|
||||
; ── Getters ──────────────────────────────────────────────────────────────────
|
||||
(method_signature
|
||||
(getter_signature
|
||||
name: (identifier) @name)) @definition.property
|
||||
|
||||
; ── Setters ──────────────────────────────────────────────────────────────────
|
||||
(method_signature
|
||||
(setter_signature
|
||||
name: (identifier) @name)) @definition.property
|
||||
|
||||
; ── Imports ──────────────────────────────────────────────────────────────────
|
||||
(import_or_export
|
||||
(library_import
|
||||
(import_specification
|
||||
(configurable_uri) @import.source))) @import
|
||||
|
||||
; ── Calls: direct function/constructor calls (identifier immediately before argument_part) ──
|
||||
(expression_statement
|
||||
(identifier) @call.name
|
||||
.
|
||||
(selector (argument_part))) @call
|
||||
|
||||
; ── Calls: method calls (obj.method()) ───────────────────────────────────────
|
||||
(expression_statement
|
||||
(selector
|
||||
(unconditional_assignable_selector
|
||||
(identifier) @call.name))) @call
|
||||
|
||||
; ── Calls: in return statements (return User()) ─────────────────────────────
|
||||
(return_statement
|
||||
(identifier) @call.name
|
||||
(selector (argument_part))) @call
|
||||
|
||||
; ── Calls: in variable assignments (var x = getUser()) ──────────────────────
|
||||
(initialized_variable_definition
|
||||
value: (identifier) @call.name
|
||||
(selector (argument_part))) @call
|
||||
|
||||
; ── Re-exports (export 'foo.dart') ───────────────────────────────────────────
|
||||
(import_or_export
|
||||
(library_export
|
||||
(configurable_uri) @import.source)) @import
|
||||
|
||||
; ── Heritage: extends ────────────────────────────────────────────────────────
|
||||
(class_definition
|
||||
name: (identifier) @heritage.class
|
||||
superclass: (superclass
|
||||
(type_identifier) @heritage.extends)) @heritage
|
||||
|
||||
; ── Heritage: implements ─────────────────────────────────────────────────────
|
||||
(class_definition
|
||||
name: (identifier) @heritage.class
|
||||
interfaces: (interfaces
|
||||
(type_identifier) @heritage.implements)) @heritage.impl
|
||||
|
||||
; ── Heritage: with (mixins) ──────────────────────────────────────────────────
|
||||
(class_definition
|
||||
name: (identifier) @heritage.class
|
||||
superclass: (superclass
|
||||
(mixins
|
||||
(type_identifier) @heritage.trait))) @heritage
|
||||
`;
|
||||
|
||||
import { SupportedLanguages } from '../../config/supported-languages.js';
|
||||
|
||||
export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
|
||||
[SupportedLanguages.TypeScript]: TYPESCRIPT_QUERIES,
|
||||
[SupportedLanguages.JavaScript]: JAVASCRIPT_QUERIES,
|
||||
[SupportedLanguages.Python]: PYTHON_QUERIES,
|
||||
[SupportedLanguages.Java]: JAVA_QUERIES,
|
||||
[SupportedLanguages.C]: C_QUERIES,
|
||||
[SupportedLanguages.Go]: GO_QUERIES,
|
||||
[SupportedLanguages.CPlusPlus]: CPP_QUERIES,
|
||||
[SupportedLanguages.CSharp]: CSHARP_QUERIES,
|
||||
[SupportedLanguages.Rust]: RUST_QUERIES,
|
||||
[SupportedLanguages.PHP]: PHP_QUERIES,
|
||||
[SupportedLanguages.Kotlin]: KOTLIN_QUERIES,
|
||||
[SupportedLanguages.Ruby]: RUBY_QUERIES,
|
||||
[SupportedLanguages.Swift]: SWIFT_QUERIES,
|
||||
[SupportedLanguages.Dart]: DART_QUERIES,
|
||||
};
|
||||
|
|
|
|||
365
gitnexus/src/core/ingestion/type-extractors/dart.ts
Normal file
365
gitnexus/src/core/ingestion/type-extractors/dart.ts
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
/**
|
||||
* Dart type extractor — full implementation following type-resolution-system.md.
|
||||
*
|
||||
* Tier 0: Explicit type annotations (User user = ...)
|
||||
* Tier 0b: For-loop element types (for (var u in users))
|
||||
* Tier 1: Constructor/initializer inference (var user = User())
|
||||
* Tier 2: Assignment chain propagation (copy, fieldAccess, callResult, methodCallResult)
|
||||
*
|
||||
* Handles tree-sitter-dart's flat sibling AST structure:
|
||||
* identifier + selector + selector (not nested call_expression).
|
||||
*
|
||||
* Credit: Type resolution approach adapted from @xFlaviews' PR #83.
|
||||
*/
|
||||
|
||||
import type { SyntaxNode } from '../utils/ast-helpers.js';
|
||||
import type {
|
||||
LanguageTypeConfig,
|
||||
ParameterExtractor,
|
||||
TypeBindingExtractor,
|
||||
InitializerExtractor,
|
||||
ClassNameLookup,
|
||||
ConstructorBindingScanner,
|
||||
PendingAssignmentExtractor,
|
||||
ForLoopExtractor,
|
||||
LiteralTypeInferrer,
|
||||
ConstructorTypeDetector,
|
||||
} from './types.js';
|
||||
import { extractSimpleTypeName, extractVarName, extractElementTypeFromString, resolveIterableElementType } from './shared.js';
|
||||
import { findChild } from '../utils/ast-helpers.js';
|
||||
|
||||
// ── Node types ──────────────────────────────────────────────────────────
|
||||
|
||||
const DART_DECLARATION_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'initialized_variable_definition',
|
||||
'initialized_identifier',
|
||||
]);
|
||||
|
||||
const DART_FOR_LOOP_NODE_TYPES: ReadonlySet<string> = new Set([
|
||||
'for_statement',
|
||||
]);
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
interface DartRHS {
|
||||
callee?: string;
|
||||
member?: string;
|
||||
hasCall: boolean;
|
||||
isAwait: boolean;
|
||||
}
|
||||
|
||||
function parseDartRHSChildren(children: Iterable<SyntaxNode>): Omit<DartRHS, 'isAwait'> {
|
||||
let callee: string | undefined;
|
||||
let member: string | undefined;
|
||||
let hasCall = false;
|
||||
|
||||
for (const child of children) {
|
||||
if (child.type === 'identifier' && !callee) {
|
||||
callee = child.text;
|
||||
continue;
|
||||
}
|
||||
if (child.type === 'selector') {
|
||||
const uas = findChild(child, 'unconditional_assignable_selector')
|
||||
?? findChild(child, 'conditional_assignable_selector');
|
||||
if (uas) {
|
||||
const id = findChild(uas, 'identifier');
|
||||
if (id && !member) member = id.text;
|
||||
continue;
|
||||
}
|
||||
if (findChild(child, 'argument_part')) {
|
||||
hasCall = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { callee, member, hasCall };
|
||||
}
|
||||
|
||||
function parseDartRHS(node: SyntaxNode): DartRHS {
|
||||
const rhsChildren: SyntaxNode[] = [];
|
||||
let foundEquals = false;
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (!child) continue;
|
||||
if (!child.isNamed && child.text === '=') { foundEquals = true; continue; }
|
||||
if (foundEquals) rhsChildren.push(child);
|
||||
}
|
||||
|
||||
if (rhsChildren.length === 0) return { hasCall: false, isAwait: false };
|
||||
|
||||
const first = rhsChildren[0];
|
||||
if (first.type === 'unary_expression') {
|
||||
const awaitExpr = findChild(first, 'await_expression');
|
||||
if (awaitExpr) {
|
||||
const innerChildren: SyntaxNode[] = [];
|
||||
for (let i = 0; i < awaitExpr.namedChildCount; i++) {
|
||||
const c = awaitExpr.namedChild(i);
|
||||
if (c && c.type !== 'await') innerChildren.push(c);
|
||||
}
|
||||
return { ...parseDartRHSChildren(innerChildren), isAwait: true };
|
||||
}
|
||||
}
|
||||
|
||||
return { ...parseDartRHSChildren(rhsChildren), isAwait: false };
|
||||
}
|
||||
|
||||
function hasDartTypeAnnotation(node: SyntaxNode): boolean {
|
||||
return !!(findChild(node, 'type_identifier') || findChild(node, 'nullable_type'));
|
||||
}
|
||||
|
||||
// ── Tier 0: Explicit Type Annotations ───────────────────────────────────
|
||||
|
||||
const extractDartDeclaration: TypeBindingExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
|
||||
let typeNode = findChild(node, 'type_identifier');
|
||||
if (!typeNode) {
|
||||
const nullable = findChild(node, 'nullable_type');
|
||||
if (nullable) typeNode = findChild(nullable, 'type_identifier');
|
||||
}
|
||||
if (!typeNode) return;
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
if (!typeName || typeName === 'dynamic') return;
|
||||
const nameNode = node.childForFieldName('name');
|
||||
if (!nameNode) return;
|
||||
const varName = extractVarName(nameNode);
|
||||
if (varName) env.set(varName, typeName);
|
||||
};
|
||||
|
||||
const extractDartParameter: ParameterExtractor = (node: SyntaxNode, env: Map<string, string>): void => {
|
||||
let typeNode = findChild(node, 'type_identifier');
|
||||
if (!typeNode) {
|
||||
const nullable = findChild(node, 'nullable_type');
|
||||
if (nullable) typeNode = findChild(nullable, 'type_identifier');
|
||||
}
|
||||
if (!typeNode) return;
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
if (!typeName || typeName === 'dynamic') return;
|
||||
const nameNode = node.childForFieldName('name');
|
||||
if (!nameNode) return;
|
||||
const varName = extractVarName(nameNode);
|
||||
if (varName) env.set(varName, typeName);
|
||||
};
|
||||
|
||||
// ── Tier 1: Constructor / Initializer Inference ─────────────────────────
|
||||
|
||||
const extractDartInitializer: InitializerExtractor = (node: SyntaxNode, env: Map<string, string>, classNames: ClassNameLookup): void => {
|
||||
if (node.type !== 'initialized_variable_definition') return;
|
||||
if (hasDartTypeAnnotation(node)) return;
|
||||
|
||||
const nameNode = node.childForFieldName('name');
|
||||
if (!nameNode) return;
|
||||
const varName = extractVarName(nameNode);
|
||||
if (!varName || env.has(varName)) return;
|
||||
|
||||
const rhs = parseDartRHS(node);
|
||||
if (!rhs.callee || !rhs.hasCall) return;
|
||||
|
||||
if (!rhs.member && classNames.has(rhs.callee)) {
|
||||
env.set(varName, rhs.callee);
|
||||
return;
|
||||
}
|
||||
|
||||
if (rhs.member && classNames.has(rhs.callee)) {
|
||||
env.set(varName, rhs.callee);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Constructor Binding Scan ────────────────────────────────────────────
|
||||
|
||||
const scanDartConstructorBinding: ConstructorBindingScanner = (node) => {
|
||||
if (node.type !== 'initialized_variable_definition') return undefined;
|
||||
if (hasDartTypeAnnotation(node)) return undefined;
|
||||
|
||||
const nameNode = node.childForFieldName('name');
|
||||
if (!nameNode) return undefined;
|
||||
const varName = nameNode.text;
|
||||
if (!varName) return undefined;
|
||||
|
||||
const rhs = parseDartRHS(node);
|
||||
if (!rhs.callee) return undefined;
|
||||
|
||||
if (rhs.hasCall && !rhs.member) return { varName, calleeName: rhs.callee };
|
||||
if (rhs.hasCall && rhs.member) return { varName, calleeName: rhs.member };
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// ── Virtual Dispatch ────────────────────────────────────────────────────
|
||||
|
||||
const detectDartConstructorType: ConstructorTypeDetector = (node, classNames) => {
|
||||
if (node.type !== 'initialized_variable_definition') return undefined;
|
||||
|
||||
const rhs = parseDartRHS(node);
|
||||
if (!rhs.callee || !rhs.hasCall) return undefined;
|
||||
|
||||
if (!rhs.member && classNames.has(rhs.callee)) return rhs.callee;
|
||||
if (rhs.member && classNames.has(rhs.callee)) return rhs.callee;
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// ── Literal Type Inference ──────────────────────────────────────────────
|
||||
|
||||
const inferDartLiteralType: LiteralTypeInferrer = (node) => {
|
||||
switch (node.type) {
|
||||
case 'decimal_integer_literal':
|
||||
case 'hex_integer_literal':
|
||||
return 'int';
|
||||
case 'decimal_floating_point_literal':
|
||||
return 'double';
|
||||
case 'string_literal':
|
||||
return 'String';
|
||||
case 'true':
|
||||
case 'false':
|
||||
return 'bool';
|
||||
case 'null_literal':
|
||||
return 'null';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// ── Tier 2: Assignment Chain Propagation ─────────────────────────────────
|
||||
|
||||
const extractDartPendingAssignment: PendingAssignmentExtractor = (node, scopeEnv) => {
|
||||
if (node.type !== 'initialized_variable_definition') return undefined;
|
||||
if (hasDartTypeAnnotation(node)) return undefined;
|
||||
|
||||
const nameNode = node.childForFieldName('name');
|
||||
if (!nameNode) return undefined;
|
||||
const lhs = nameNode.text;
|
||||
if (!lhs || scopeEnv.has(lhs)) return undefined;
|
||||
|
||||
const rhs = parseDartRHS(node);
|
||||
if (!rhs.callee) return undefined;
|
||||
|
||||
if (!rhs.hasCall && !rhs.member) return { kind: 'copy', lhs, rhs: rhs.callee };
|
||||
if (!rhs.hasCall && rhs.member) return { kind: 'fieldAccess', lhs, receiver: rhs.callee, field: rhs.member };
|
||||
if (rhs.hasCall && !rhs.member) return { kind: 'callResult', lhs, callee: rhs.callee };
|
||||
if (rhs.hasCall && rhs.member) return { kind: 'methodCallResult', lhs, receiver: rhs.callee, method: rhs.member };
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// ── For-Loop Element Type Resolution ────────────────────────────────────
|
||||
|
||||
function extractDartElementTypeFromTypeNode(typeNode: SyntaxNode): string | undefined {
|
||||
if (typeNode.type === 'type_identifier') {
|
||||
const parent = typeNode.parent;
|
||||
if (parent) {
|
||||
const args = findChild(parent, 'type_arguments');
|
||||
if (args && args.namedChildCount >= 1) {
|
||||
const lastArg = args.namedChild(args.namedChildCount - 1);
|
||||
if (lastArg) return extractSimpleTypeName(lastArg);
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const extractDartForLoopBinding: ForLoopExtractor = (node, ctx): void => {
|
||||
if (node.type !== 'for_statement') return;
|
||||
const { scopeEnv, declarationTypeNodes, scope, returnTypeLookup } = ctx;
|
||||
|
||||
const loopParts = findChild(node, 'for_loop_parts');
|
||||
if (!loopParts) return;
|
||||
|
||||
const nameNode = loopParts.childForFieldName('name');
|
||||
if (!nameNode) return;
|
||||
const loopVarName = nameNode.text;
|
||||
if (!loopVarName) return;
|
||||
|
||||
const typeNode = findChild(loopParts, 'type_identifier');
|
||||
if (typeNode) {
|
||||
const typeName = extractSimpleTypeName(typeNode);
|
||||
if (typeName && !scopeEnv.has(loopVarName)) {
|
||||
(scopeEnv as Map<string, string>).set(loopVarName, typeName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const iterableNode = loopParts.childForFieldName('value');
|
||||
if (!iterableNode) return;
|
||||
|
||||
let iterableName: string | undefined;
|
||||
let callExprElementType: string | undefined;
|
||||
|
||||
if (iterableNode.type === 'identifier') {
|
||||
iterableName = iterableNode.text;
|
||||
} else if (iterableNode.type === 'unary_expression') {
|
||||
const awaitExpr = findChild(iterableNode, 'await_expression');
|
||||
if (awaitExpr) {
|
||||
const innerIdent = findChild(awaitExpr, 'identifier');
|
||||
if (innerIdent) iterableName = innerIdent.text;
|
||||
}
|
||||
if (!iterableName) return;
|
||||
}
|
||||
|
||||
if (iterableName) {
|
||||
let hasCallSelector = false;
|
||||
let memberName: string | undefined;
|
||||
|
||||
const selectorParent = iterableNode.type === 'unary_expression'
|
||||
? findChild(iterableNode, 'await_expression')
|
||||
: loopParts;
|
||||
if (!selectorParent) return;
|
||||
|
||||
let foundIterable = false;
|
||||
for (let i = 0; i < selectorParent.childCount; i++) {
|
||||
const child = selectorParent.child(i);
|
||||
if (!child) continue;
|
||||
if (child.type === 'identifier' && child.text === iterableName) { foundIterable = true; continue; }
|
||||
if (child === iterableNode) { foundIterable = true; continue; }
|
||||
if (!foundIterable) continue;
|
||||
if (child.type === 'selector') {
|
||||
const uas = findChild(child, 'unconditional_assignable_selector')
|
||||
?? findChild(child, 'conditional_assignable_selector');
|
||||
if (uas) {
|
||||
const id = findChild(uas, 'identifier');
|
||||
if (id) memberName = id.text;
|
||||
continue;
|
||||
}
|
||||
if (findChild(child, 'argument_part')) {
|
||||
hasCallSelector = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCallSelector) {
|
||||
const callee = memberName ?? iterableName;
|
||||
const rawReturn = returnTypeLookup.lookupRawReturnType(callee);
|
||||
if (rawReturn) callExprElementType = extractElementTypeFromString(rawReturn);
|
||||
}
|
||||
}
|
||||
|
||||
if (!iterableName && !callExprElementType) return;
|
||||
|
||||
let elementType: string | undefined;
|
||||
if (callExprElementType) {
|
||||
elementType = callExprElementType;
|
||||
} else if (iterableName) {
|
||||
elementType = resolveIterableElementType(
|
||||
iterableName, node, scopeEnv, declarationTypeNodes, scope,
|
||||
extractDartElementTypeFromTypeNode,
|
||||
);
|
||||
}
|
||||
|
||||
if (elementType && !scopeEnv.has(loopVarName)) {
|
||||
(scopeEnv as Map<string, string>).set(loopVarName, elementType);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Export ───────────────────────────────────────────────────────────────
|
||||
|
||||
export const typeConfig: LanguageTypeConfig = {
|
||||
declarationNodeTypes: DART_DECLARATION_NODE_TYPES,
|
||||
forLoopNodeTypes: DART_FOR_LOOP_NODE_TYPES,
|
||||
extractDeclaration: extractDartDeclaration,
|
||||
extractParameter: extractDartParameter,
|
||||
extractInitializer: extractDartInitializer,
|
||||
scanConstructorBinding: scanDartConstructorBinding,
|
||||
extractForLoopBinding: extractDartForLoopBinding,
|
||||
extractPendingAssignment: extractDartPendingAssignment,
|
||||
inferLiteralType: inferDartLiteralType,
|
||||
detectConstructorType: detectDartConstructorType,
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ const _require = createRequire(import.meta.url);
|
|||
let Swift: any = null;
|
||||
try { Swift = _require('tree-sitter-swift'); } catch {}
|
||||
|
||||
// tree-sitter-dart is an optionalDependency — may not be installed
|
||||
let Dart: any = null;
|
||||
try { Dart = _require('tree-sitter-dart'); } catch {}
|
||||
|
||||
// tree-sitter-kotlin is an optionalDependency — may not be installed
|
||||
let Kotlin: any = null;
|
||||
try { Kotlin = _require('tree-sitter-kotlin'); } catch {}
|
||||
|
|
@ -238,6 +242,7 @@ const languageMap: Record<string, any> = {
|
|||
...(Kotlin ? { [SupportedLanguages.Kotlin]: Kotlin } : {}),
|
||||
[SupportedLanguages.PHP]: PHP.php_only,
|
||||
[SupportedLanguages.Ruby]: Ruby,
|
||||
...(Dart ? { [SupportedLanguages.Dart]: Dart } : {}),
|
||||
...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -13,10 +13,12 @@ import Ruby from 'tree-sitter-ruby';
|
|||
import { createRequire } from 'node:module';
|
||||
import { SupportedLanguages } from '../../config/supported-languages.js';
|
||||
|
||||
// tree-sitter-swift is an optionalDependency — may not be installed
|
||||
// tree-sitter-swift and tree-sitter-dart are optionalDependencies — may not be installed
|
||||
const _require = createRequire(import.meta.url);
|
||||
let Swift: any = null;
|
||||
try { Swift = _require('tree-sitter-swift'); } catch {}
|
||||
let Dart: any = null;
|
||||
try { Dart = _require('tree-sitter-dart'); } catch {}
|
||||
|
||||
// tree-sitter-kotlin is an optionalDependency — may not be installed
|
||||
let Kotlin: any = null;
|
||||
|
|
@ -38,6 +40,7 @@ const languageMap: Record<string, any> = {
|
|||
...(Kotlin ? { [SupportedLanguages.Kotlin]: Kotlin } : {}),
|
||||
[SupportedLanguages.PHP]: PHP.php_only,
|
||||
[SupportedLanguages.Ruby]: Ruby,
|
||||
...(Dart ? { [SupportedLanguages.Dart]: Dart } : {}),
|
||||
...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}),
|
||||
};
|
||||
|
||||
|
|
|
|||
139
gitnexus/test/fixtures/sample-code/dart-advanced.dart
vendored
Normal file
139
gitnexus/test/fixtures/sample-code/dart-advanced.dart
vendored
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
// ── Imports and re-exports ──────────────────────────────────────────────
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
export 'src/models.dart';
|
||||
|
||||
// ── Private symbols (should be filtered by exportChecker) ──────────────
|
||||
void _internalSetup() {
|
||||
print('internal');
|
||||
}
|
||||
|
||||
class _PrivateCache {
|
||||
final Map<String, dynamic> _data = {};
|
||||
}
|
||||
|
||||
// ── Call sites: expression statement ────────────────────────────────────
|
||||
void expressionCalls() {
|
||||
fetchUsers();
|
||||
processData();
|
||||
}
|
||||
|
||||
// ── Call sites: return statement ────────────────────────────────────────
|
||||
String returnCall() {
|
||||
return formatOutput();
|
||||
}
|
||||
|
||||
// ── Call sites: variable assignment ─────────────────────────────────────
|
||||
void assignmentCalls() {
|
||||
var result = computeScore();
|
||||
final user = loadUser();
|
||||
}
|
||||
|
||||
// ── Type resolution: explicit annotations ──────────────────────────────
|
||||
void typedDeclarations() {
|
||||
User admin = User('admin');
|
||||
User? maybeUser = null;
|
||||
List<String> names = [];
|
||||
}
|
||||
|
||||
// ── Type resolution: constructor inference ──────────────────────────────
|
||||
void constructorInference() {
|
||||
var dog = Dog('Rex', 'GSD');
|
||||
final repo = Repository<User>();
|
||||
var named = Dog.unknown();
|
||||
}
|
||||
|
||||
// ── Type resolution: for-loop ──────────────────────────────────────────
|
||||
void loopTypes(List<User> users) {
|
||||
for (var user in users) {
|
||||
print(user);
|
||||
}
|
||||
for (User u in users) {
|
||||
print(u);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Flutter widget pattern ─────────────────────────────────────────────
|
||||
class UserWidget extends StatelessWidget {
|
||||
final String username;
|
||||
|
||||
const UserWidget({required this.username});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(username);
|
||||
}
|
||||
}
|
||||
|
||||
class CounterPage extends StatefulWidget {
|
||||
@override
|
||||
State<CounterPage> createState() => _CounterPageState();
|
||||
}
|
||||
|
||||
class _CounterPageState extends State<CounterPage> {
|
||||
int _count = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text('$_count');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Framework detection patterns ───────────────────────────────────────
|
||||
class UserBloc extends Bloc<UserEvent, UserState> {
|
||||
@override
|
||||
void onEvent(UserEvent event) {}
|
||||
}
|
||||
|
||||
class SettingsNotifier extends ChangeNotifier {
|
||||
void toggle() {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Stub types for compilation (not real Flutter) ──────────────────────
|
||||
class StatelessWidget {}
|
||||
class StatefulWidget {}
|
||||
class State<T> {
|
||||
void initState() {}
|
||||
void dispose() {}
|
||||
}
|
||||
class Widget {}
|
||||
class BuildContext {}
|
||||
class Text extends Widget {
|
||||
Text(String text);
|
||||
}
|
||||
class Bloc<E, S> {
|
||||
void onEvent(E event) {}
|
||||
}
|
||||
class ChangeNotifier {
|
||||
void notifyListeners() {}
|
||||
}
|
||||
class User {
|
||||
final String name;
|
||||
User(this.name);
|
||||
}
|
||||
class Dog {
|
||||
Dog(String name, String breed);
|
||||
factory Dog.unknown() => Dog('?', '?');
|
||||
}
|
||||
class Repository<T> {}
|
||||
class UserEvent {}
|
||||
class UserState {}
|
||||
|
||||
// ── Helper functions for call extraction tests ─────────────────────────
|
||||
List<User> fetchUsers() => [];
|
||||
String processData() => '';
|
||||
String formatOutput() => '';
|
||||
int computeScore() => 0;
|
||||
User loadUser() => User('test');
|
||||
121
gitnexus/test/fixtures/sample-code/simple.dart
vendored
Normal file
121
gitnexus/test/fixtures/sample-code/simple.dart
vendored
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
// Top-level function
|
||||
String greet(String name) {
|
||||
return 'Hello, $name!';
|
||||
}
|
||||
|
||||
// Top-level typedef
|
||||
typedef JsonMap = Map<String, dynamic>;
|
||||
typedef Callback = void Function(int value);
|
||||
|
||||
// Enum
|
||||
enum Status {
|
||||
active,
|
||||
inactive,
|
||||
pending;
|
||||
|
||||
String get label => name.toUpperCase();
|
||||
}
|
||||
|
||||
// Class with extends and implements
|
||||
class Animal {
|
||||
final String name;
|
||||
|
||||
Animal(this.name);
|
||||
|
||||
String speak() => 'I am $name';
|
||||
}
|
||||
|
||||
abstract class Describable {
|
||||
String describe();
|
||||
}
|
||||
|
||||
class Dog extends Animal implements Describable {
|
||||
final String breed;
|
||||
|
||||
Dog(super.name, this.breed);
|
||||
|
||||
// Factory constructor
|
||||
factory Dog.unknown() {
|
||||
return Dog('Unknown', 'Mixed');
|
||||
}
|
||||
|
||||
@override
|
||||
String speak() => 'Woof! I am $name';
|
||||
|
||||
@override
|
||||
String describe() => 'Dog($name, $breed)';
|
||||
|
||||
// Getter
|
||||
String get info => '$name - $breed';
|
||||
|
||||
// Setter
|
||||
set nickname(String value) {
|
||||
print('Nickname set to $value');
|
||||
}
|
||||
}
|
||||
|
||||
// Mixin
|
||||
mixin Swimming {
|
||||
void swim() {
|
||||
print('Swimming!');
|
||||
}
|
||||
}
|
||||
|
||||
mixin Flying {
|
||||
void fly() {
|
||||
print('Flying!');
|
||||
}
|
||||
}
|
||||
|
||||
// Class with mixin
|
||||
class Duck extends Animal with Swimming, Flying {
|
||||
Duck(super.name);
|
||||
}
|
||||
|
||||
// Extension
|
||||
extension StringExtension on String {
|
||||
String capitalize() {
|
||||
if (isEmpty) return this;
|
||||
return '${this[0].toUpperCase()}${substring(1)}';
|
||||
}
|
||||
}
|
||||
|
||||
// Generic class
|
||||
class Repository<T> {
|
||||
final List<T> _items = [];
|
||||
|
||||
void add(T item) {
|
||||
_items.add(item);
|
||||
}
|
||||
|
||||
T? findFirst(bool Function(T) predicate) {
|
||||
for (final item in _items) {
|
||||
if (predicate(item)) return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Private function (starts with _)
|
||||
void _privateHelper() {
|
||||
print('I am private');
|
||||
}
|
||||
|
||||
// Async function
|
||||
Future<String> fetchData(String url) async {
|
||||
final response = await http.get(Uri.parse(url));
|
||||
return response.body;
|
||||
}
|
||||
|
||||
// Top-level const
|
||||
const String appName = 'MyApp';
|
||||
|
||||
void main() {
|
||||
final dog = Dog('Rex', 'German Shepherd');
|
||||
dog.speak();
|
||||
print(greet('World'));
|
||||
}
|
||||
|
|
@ -422,6 +422,211 @@ describe('Tree-sitter multi-language parsing', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Dart', () => {
|
||||
const dartQueries = () => getProvider(SupportedLanguages.Dart).treeSitterQueries;
|
||||
|
||||
function loadDartOrSkip() {
|
||||
return loadLanguage(SupportedLanguages.Dart).catch(() => null);
|
||||
}
|
||||
|
||||
// ── Definition extraction ──────────────────────────────────────────
|
||||
|
||||
it('parses classes, functions, mixins, enums, and type aliases', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const { matches } = parseAndQuery(parser, readFixture('simple.dart'), dartQueries());
|
||||
const defs = extractDefinitions(matches);
|
||||
const defTypes = defs.map(d => d.type);
|
||||
const defNames = defs.map(d => d.name);
|
||||
|
||||
expect(defTypes).toContain('definition.class');
|
||||
expect(defTypes).toContain('definition.function');
|
||||
expect(defTypes).toContain('definition.method');
|
||||
expect(defTypes).toContain('definition.enum');
|
||||
expect(defTypes).toContain('definition.trait'); // mixin
|
||||
expect(defTypes).toContain('definition.type'); // typedef
|
||||
expect(defTypes).toContain('definition.constructor');
|
||||
expect(defTypes).toContain('definition.property'); // getter/setter
|
||||
|
||||
expect(defNames).toContain('Animal');
|
||||
expect(defNames).toContain('Dog');
|
||||
expect(defNames).toContain('greet');
|
||||
expect(defNames).toContain('Swimming');
|
||||
expect(defNames).toContain('Status');
|
||||
expect(defNames).toContain('main');
|
||||
expect(defNames).toContain('StringExtension');
|
||||
});
|
||||
|
||||
it('captures factory constructor variant name (not class name)', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const { matches } = parseAndQuery(parser, readFixture('simple.dart'), dartQueries());
|
||||
const defs = extractDefinitions(matches);
|
||||
const constructors = defs.filter(d => d.type === 'definition.constructor');
|
||||
expect(constructors.length).toBeGreaterThan(0);
|
||||
// factory Dog.unknown() should capture 'unknown', not 'Dog'
|
||||
const constructorNames = constructors.map(c => c.name);
|
||||
expect(constructorNames).toContain('unknown');
|
||||
expect(constructorNames).not.toContain('Dog');
|
||||
});
|
||||
|
||||
it('captures getter and setter as definition.property', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const { matches } = parseAndQuery(parser, readFixture('simple.dart'), dartQueries());
|
||||
const defs = extractDefinitions(matches);
|
||||
const props = defs.filter(d => d.type === 'definition.property');
|
||||
expect(props.map(p => p.name)).toContain('info'); // getter
|
||||
expect(props.map(p => p.name)).toContain('nickname'); // setter
|
||||
});
|
||||
|
||||
it('captures typedef names without duplicates', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const { matches } = parseAndQuery(parser, readFixture('simple.dart'), dartQueries());
|
||||
const defs = extractDefinitions(matches);
|
||||
const typedefs = defs.filter(d => d.type === 'definition.type');
|
||||
const typedefNames = typedefs.map(d => d.name);
|
||||
expect(typedefNames).toContain('JsonMap');
|
||||
expect(typedefNames).toContain('Callback');
|
||||
// Should not capture RHS types as names
|
||||
expect(typedefNames).not.toContain('Map');
|
||||
expect(typedefNames).not.toContain('Function');
|
||||
});
|
||||
|
||||
// ── Export detection (private underscore convention) ────────────────
|
||||
|
||||
it('filters private symbols via underscore convention', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const { matches } = parseAndQuery(parser, readFixture('simple.dart'), dartQueries());
|
||||
const defs = extractDefinitions(matches);
|
||||
const { dartExportChecker } = await import('../../src/core/ingestion/export-detection.js');
|
||||
const publicNames = defs.filter(d => dartExportChecker(null as any, d.name)).map(d => d.name);
|
||||
const privateNames = defs.filter(d => !dartExportChecker(null as any, d.name)).map(d => d.name);
|
||||
|
||||
expect(publicNames).toContain('Animal');
|
||||
expect(publicNames).toContain('greet');
|
||||
expect(privateNames).toContain('_privateHelper');
|
||||
});
|
||||
|
||||
// ── Import extraction ──────────────────────────────────────────────
|
||||
|
||||
it('extracts imports', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const { matches } = parseAndQuery(parser, readFixture('simple.dart'), dartQueries());
|
||||
|
||||
const imports: string[] = [];
|
||||
for (const match of matches) {
|
||||
for (const capture of match.captures) {
|
||||
if (capture.name === 'import.source') imports.push(capture.node.text);
|
||||
}
|
||||
}
|
||||
expect(imports.length).toBe(3);
|
||||
});
|
||||
|
||||
it('extracts re-exports', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const { matches } = parseAndQuery(parser, readFixture('dart-advanced.dart'), dartQueries());
|
||||
|
||||
const imports: string[] = [];
|
||||
for (const match of matches) {
|
||||
for (const capture of match.captures) {
|
||||
if (capture.name === 'import.source') imports.push(capture.node.text);
|
||||
}
|
||||
}
|
||||
// 2 imports + 1 re-export = 3 import.source captures
|
||||
expect(imports.length).toBe(3);
|
||||
});
|
||||
|
||||
// ── Heritage extraction ────────────────────────────────────────────
|
||||
|
||||
it('extracts heritage (extends, implements, with)', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const { matches } = parseAndQuery(parser, readFixture('simple.dart'), dartQueries());
|
||||
|
||||
const heritage: { class: string; parent: string }[] = [];
|
||||
for (const match of matches) {
|
||||
const captures: Record<string, string> = {};
|
||||
for (const c of match.captures) captures[c.name] = c.node.text;
|
||||
if (captures['heritage.extends']) {
|
||||
heritage.push({ class: captures['heritage.class'], parent: captures['heritage.extends'] });
|
||||
}
|
||||
if (captures['heritage.implements']) {
|
||||
heritage.push({ class: captures['heritage.class'], parent: captures['heritage.implements'] });
|
||||
}
|
||||
if (captures['heritage.trait']) {
|
||||
heritage.push({ class: captures['heritage.class'], parent: captures['heritage.trait'] });
|
||||
}
|
||||
}
|
||||
|
||||
const pairs = heritage.map(h => `${h.class}->${h.parent}`);
|
||||
expect(pairs).toContain('Dog->Animal');
|
||||
expect(pairs).toContain('Dog->Describable');
|
||||
expect(pairs).toContain('Duck->Animal');
|
||||
expect(pairs).toContain('Duck->Swimming');
|
||||
expect(pairs).toContain('Duck->Flying');
|
||||
});
|
||||
|
||||
// ── Call extraction ────────────────────────────────────────────────
|
||||
|
||||
it('extracts calls in expression statements', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const { matches } = parseAndQuery(parser, readFixture('dart-advanced.dart'), dartQueries());
|
||||
|
||||
const callNames: string[] = [];
|
||||
for (const match of matches) {
|
||||
for (const capture of match.captures) {
|
||||
if (capture.name === 'call.name') callNames.push(capture.node.text);
|
||||
}
|
||||
}
|
||||
expect(callNames).toContain('fetchUsers');
|
||||
expect(callNames).toContain('processData');
|
||||
});
|
||||
|
||||
it('extracts calls in return statements', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const { matches } = parseAndQuery(parser, readFixture('dart-advanced.dart'), dartQueries());
|
||||
|
||||
const callNames: string[] = [];
|
||||
for (const match of matches) {
|
||||
for (const capture of match.captures) {
|
||||
if (capture.name === 'call.name') callNames.push(capture.node.text);
|
||||
}
|
||||
}
|
||||
expect(callNames).toContain('formatOutput');
|
||||
});
|
||||
|
||||
it('extracts calls in variable assignments', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const { matches } = parseAndQuery(parser, readFixture('dart-advanced.dart'), dartQueries());
|
||||
|
||||
const callNames: string[] = [];
|
||||
for (const match of matches) {
|
||||
for (const capture of match.captures) {
|
||||
if (capture.name === 'call.name') callNames.push(capture.node.text);
|
||||
}
|
||||
}
|
||||
expect(callNames).toContain('computeScore');
|
||||
expect(callNames).toContain('loadUser');
|
||||
});
|
||||
|
||||
// ── Framework detection (path-based) ───────────────────────────────
|
||||
|
||||
it('detects Flutter framework from file paths', async () => {
|
||||
const { detectFrameworkFromPath } = await import('../../src/core/ingestion/framework-detection.js');
|
||||
|
||||
expect(detectFrameworkFromPath('lib/main.dart')?.framework).toBe('flutter');
|
||||
expect(detectFrameworkFromPath('lib/app.dart')?.framework).toBe('flutter');
|
||||
expect(detectFrameworkFromPath('lib/screens/home.dart')?.framework).toBe('flutter');
|
||||
expect(detectFrameworkFromPath('lib/pages/login.dart')?.framework).toBe('flutter');
|
||||
expect(detectFrameworkFromPath('lib/widgets/button.dart')?.framework).toBe('flutter');
|
||||
expect(detectFrameworkFromPath('lib/bloc/user_bloc.dart')?.framework).toBe('flutter');
|
||||
expect(detectFrameworkFromPath('lib/services/api.dart')?.framework).toBe('flutter');
|
||||
expect(detectFrameworkFromPath('lib/routes/app_router.dart')?.framework).toBe('flutter');
|
||||
|
||||
// main.dart gets highest boost
|
||||
expect(detectFrameworkFromPath('lib/main.dart')?.entryPointMultiplier).toBe(3.0);
|
||||
// widgets get lowest
|
||||
expect(detectFrameworkFromPath('lib/widgets/button.dart')?.entryPointMultiplier).toBe(1.5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cross-language assertions', () => {
|
||||
it('all supported languages produce at least one definition from fixtures', async () => {
|
||||
const langFixtures: [SupportedLanguages, string, string?][] = [
|
||||
|
|
@ -435,6 +640,7 @@ describe('Tree-sitter multi-language parsing', () => {
|
|||
[SupportedLanguages.CSharp, 'simple.cs'],
|
||||
[SupportedLanguages.Rust, 'simple.rs'],
|
||||
[SupportedLanguages.PHP, 'simple.php'],
|
||||
// Dart and Swift are excluded — they are optionalDependencies that may not be installed
|
||||
];
|
||||
|
||||
for (const [lang, fixture, filePath] of langFixtures) {
|
||||
|
|
|
|||
79
gitnexus/test/unit/dart-import-resolver.test.ts
Normal file
79
gitnexus/test/unit/dart-import-resolver.test.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { resolveDartImport } from '../../src/core/ingestion/import-resolvers/dart.js';
|
||||
import type { ResolveCtx } from '../../src/core/ingestion/import-resolvers/types.js';
|
||||
|
||||
function makeCtx(files: string[]): ResolveCtx {
|
||||
const allFileList = files;
|
||||
const normalizedFileList = files.map(f => f.toLowerCase());
|
||||
return {
|
||||
allFilePaths: new Set(files),
|
||||
allFileList,
|
||||
normalizedFileList,
|
||||
index: undefined as any,
|
||||
resolveCache: new Map(),
|
||||
configs: {
|
||||
tsconfigPaths: null,
|
||||
goModule: null,
|
||||
composerConfig: null,
|
||||
swiftPackageConfig: null,
|
||||
csharpConfigs: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('Dart import resolver', () => {
|
||||
describe('dart: SDK imports', () => {
|
||||
it('skips dart:async', () => {
|
||||
const result = resolveDartImport("'dart:async'", 'lib/main.dart', makeCtx([]));
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('skips dart:io', () => {
|
||||
const result = resolveDartImport("'dart:io'", 'lib/main.dart', makeCtx([]));
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('package: imports', () => {
|
||||
it('resolves local package import to lib/', () => {
|
||||
const ctx = makeCtx(['lib/models/user.dart', 'lib/main.dart']);
|
||||
const result = resolveDartImport("'package:my_app/models/user.dart'", 'lib/main.dart', ctx);
|
||||
expect(result).toEqual({ kind: 'files', files: ['lib/models/user.dart'] });
|
||||
});
|
||||
|
||||
it('returns null for external package imports', () => {
|
||||
const ctx = makeCtx(['lib/main.dart']);
|
||||
const result = resolveDartImport("'package:http/http.dart'", 'lib/main.dart', ctx);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for malformed package import (no slash)', () => {
|
||||
const result = resolveDartImport("'package:http'", 'lib/main.dart', makeCtx([]));
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('relative imports', () => {
|
||||
it('resolves relative import via standard resolver', () => {
|
||||
const ctx = makeCtx(['lib/models/user.dart', 'lib/main.dart']);
|
||||
const result = resolveDartImport("'models/user.dart'", 'lib/main.dart', ctx);
|
||||
// resolveStandard handles relative path resolution
|
||||
// The exact result depends on the standard resolver — we just check it doesn't crash
|
||||
expect(result === null || result?.kind === 'files').toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('quote stripping', () => {
|
||||
it('strips single quotes', () => {
|
||||
const ctx = makeCtx([]);
|
||||
const result = resolveDartImport("'dart:core'", 'lib/main.dart', ctx);
|
||||
expect(result).toBeNull(); // dart: is skipped, proving quotes were stripped
|
||||
});
|
||||
|
||||
it('strips double quotes', () => {
|
||||
const ctx = makeCtx([]);
|
||||
const result = resolveDartImport('"dart:core"', 'lib/main.dart', ctx);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
393
gitnexus/test/unit/dart-type-extractor.test.ts
Normal file
393
gitnexus/test/unit/dart-type-extractor.test.ts
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import Parser from 'tree-sitter';
|
||||
import { SupportedLanguages } from '../../src/config/supported-languages.js';
|
||||
import { loadParser, loadLanguage } from '../../src/core/tree-sitter/parser-loader.js';
|
||||
import { typeConfig } from '../../src/core/ingestion/type-extractors/dart.js';
|
||||
import { findChild } from '../../src/core/ingestion/utils/ast-helpers.js';
|
||||
|
||||
function loadDartOrSkip() {
|
||||
return loadLanguage(SupportedLanguages.Dart).catch(() => null);
|
||||
}
|
||||
|
||||
function parseAndFindNodes(parser: Parser, code: string, nodeType: string) {
|
||||
const tree = parser.parse(code);
|
||||
const results: any[] = [];
|
||||
function walk(node: any) {
|
||||
if (node.type === nodeType) results.push(node);
|
||||
for (let i = 0; i < node.namedChildCount; i++) walk(node.namedChild(i));
|
||||
}
|
||||
walk(tree.rootNode);
|
||||
return results;
|
||||
}
|
||||
|
||||
describe('Dart type extractor', () => {
|
||||
let parser: Parser;
|
||||
|
||||
beforeAll(async () => {
|
||||
parser = await loadParser();
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
});
|
||||
|
||||
describe('Tier 0: explicit type annotations', () => {
|
||||
it('extracts type from typed variable declaration', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const env = new Map<string, string>();
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { User admin = User("x"); }', 'initialized_variable_definition');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
typeConfig.extractDeclaration(nodes[0], env);
|
||||
expect(env.get('admin')).toBe('User');
|
||||
});
|
||||
|
||||
it('extracts type from nullable declaration', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const env = new Map<string, string>();
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { User? maybeUser = null; }', 'initialized_variable_definition');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
typeConfig.extractDeclaration(nodes[0], env);
|
||||
expect(env.get('maybeUser')).toBe('User');
|
||||
});
|
||||
|
||||
it('skips dynamic type', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const env = new Map<string, string>();
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { dynamic x = 1; }', 'initialized_variable_definition');
|
||||
if (nodes.length > 0) {
|
||||
typeConfig.extractDeclaration(nodes[0], env);
|
||||
expect(env.has('x')).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tier 0: parameter extraction', () => {
|
||||
it('extracts typed parameter', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const env = new Map<string, string>();
|
||||
const tree = parser.parse('void greet(String name, int age) {}');
|
||||
// Find formal_parameter nodes
|
||||
const params: any[] = [];
|
||||
function walk(node: any) {
|
||||
if (node.type === 'formal_parameter') params.push(node);
|
||||
for (let i = 0; i < node.namedChildCount; i++) walk(node.namedChild(i));
|
||||
}
|
||||
walk(tree.rootNode);
|
||||
for (const p of params) {
|
||||
typeConfig.extractParameter(p, env);
|
||||
}
|
||||
expect(env.get('name')).toBe('String');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tier 1: constructor inference', () => {
|
||||
it('infers type from direct constructor call', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const env = new Map<string, string>();
|
||||
const classNames = new Set(['User']);
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { var user = User("x"); }', 'initialized_variable_definition');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
typeConfig.extractInitializer!(nodes[0], env, classNames);
|
||||
expect(env.get('user')).toBe('User');
|
||||
});
|
||||
|
||||
it('infers type from named constructor', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const env = new Map<string, string>();
|
||||
const classNames = new Set(['Dog']);
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { var d = Dog.unknown(); }', 'initialized_variable_definition');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
typeConfig.extractInitializer!(nodes[0], env, classNames);
|
||||
expect(env.get('d')).toBe('Dog');
|
||||
});
|
||||
|
||||
it('does not infer when callee is not a known class', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const env = new Map<string, string>();
|
||||
const classNames = new Set<string>();
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { var x = getUser(); }', 'initialized_variable_definition');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
typeConfig.extractInitializer!(nodes[0], env, classNames);
|
||||
expect(env.has('x')).toBe(false);
|
||||
});
|
||||
|
||||
it('skips if explicit type present', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const env = new Map<string, string>();
|
||||
const classNames = new Set(['User']);
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { User user = User("x"); }', 'initialized_variable_definition');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
typeConfig.extractInitializer!(nodes[0], env, classNames);
|
||||
// Should not set — Tier 0 handles this
|
||||
expect(env.has('user')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('constructor binding scanner', () => {
|
||||
it('scans direct constructor call', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { var user = User(); }', 'initialized_variable_definition');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
const result = typeConfig.scanConstructorBinding!(nodes[0]);
|
||||
expect(result).toEqual({ varName: 'user', calleeName: 'User' });
|
||||
});
|
||||
|
||||
it('scans qualified call (method on receiver)', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { var u = svc.getUser(); }', 'initialized_variable_definition');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
const result = typeConfig.scanConstructorBinding!(nodes[0]);
|
||||
expect(result?.varName).toBe('u');
|
||||
expect(result?.calleeName).toBe('getUser');
|
||||
});
|
||||
|
||||
it('returns undefined for non-call assignment', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { var x = y; }', 'initialized_variable_definition');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
const result = typeConfig.scanConstructorBinding!(nodes[0]);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('virtual dispatch detection', () => {
|
||||
it('detects constructor type for virtual dispatch', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const classNames = new Set(['Dog']);
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { var d = Dog(); }', 'initialized_variable_definition');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
const result = typeConfig.detectConstructorType!(nodes[0], classNames);
|
||||
expect(result).toBe('Dog');
|
||||
});
|
||||
});
|
||||
|
||||
describe('literal type inference', () => {
|
||||
it('infers int literal', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const tree = parser.parse('void f() { var x = 42; }');
|
||||
const nodes: any[] = [];
|
||||
function walk(n: any) { if (n.type.includes('integer_literal')) nodes.push(n); for (let i = 0; i < n.namedChildCount; i++) walk(n.namedChild(i)); }
|
||||
walk(tree.rootNode);
|
||||
if (nodes.length > 0) {
|
||||
expect(typeConfig.inferLiteralType!(nodes[0])).toBe('int');
|
||||
}
|
||||
});
|
||||
|
||||
it('infers string literal', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const tree = parser.parse('void f() { var x = "hello"; }');
|
||||
const nodes: any[] = [];
|
||||
function walk(n: any) { if (n.type === 'string_literal') nodes.push(n); for (let i = 0; i < n.namedChildCount; i++) walk(n.namedChild(i)); }
|
||||
walk(tree.rootNode);
|
||||
if (nodes.length > 0) {
|
||||
expect(typeConfig.inferLiteralType!(nodes[0])).toBe('String');
|
||||
}
|
||||
});
|
||||
|
||||
it('infers bool literal', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const tree = parser.parse('void f() { var x = true; }');
|
||||
const nodes: any[] = [];
|
||||
function walk(n: any) { if (n.type === 'true' || n.type === 'false') nodes.push(n); for (let i = 0; i < n.childCount; i++) { const c = n.child(i); if (c) walk(c); } }
|
||||
walk(tree.rootNode);
|
||||
if (nodes.length > 0) {
|
||||
expect(typeConfig.inferLiteralType!(nodes[0])).toBe('bool');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tier 2: pending assignment extraction', () => {
|
||||
it('extracts copy assignment', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { var copy = original; }', 'initialized_variable_definition');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
const result = typeConfig.extractPendingAssignment!(nodes[0], new Map());
|
||||
expect(result).toEqual({ kind: 'copy', lhs: 'copy', rhs: 'original' });
|
||||
});
|
||||
|
||||
it('extracts field access', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { var n = user.name; }', 'initialized_variable_definition');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
const result = typeConfig.extractPendingAssignment!(nodes[0], new Map());
|
||||
expect(result).toEqual({ kind: 'fieldAccess', lhs: 'n', receiver: 'user', field: 'name' });
|
||||
});
|
||||
|
||||
it('extracts call result', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { var u = getUser(); }', 'initialized_variable_definition');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
const result = typeConfig.extractPendingAssignment!(nodes[0], new Map());
|
||||
expect(result).toEqual({ kind: 'callResult', lhs: 'u', callee: 'getUser' });
|
||||
});
|
||||
|
||||
it('extracts method call result', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { var r = svc.fetch(); }', 'initialized_variable_definition');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
const result = typeConfig.extractPendingAssignment!(nodes[0], new Map());
|
||||
expect(result).toEqual({ kind: 'methodCallResult', lhs: 'r', receiver: 'svc', method: 'fetch' });
|
||||
});
|
||||
|
||||
it('skips when lhs already in scope', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const scope = new Map([['x', 'String']]);
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { var x = y; }', 'initialized_variable_definition');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
const result = typeConfig.extractPendingAssignment!(nodes[0], scope);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('extracts call result from await expression', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const nodes = parseAndFindNodes(parser, 'void f() async { var user = await getUser(); }', 'initialized_variable_definition');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
const result = typeConfig.extractPendingAssignment!(nodes[0], new Map());
|
||||
expect(result).toEqual({ kind: 'callResult', lhs: 'user', callee: 'getUser' });
|
||||
});
|
||||
|
||||
it('extracts method call result from await expression', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const nodes = parseAndFindNodes(parser, 'void f() async { var user = await svc.fetch(); }', 'initialized_variable_definition');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
const result = typeConfig.extractPendingAssignment!(nodes[0], new Map());
|
||||
expect(result).toEqual({ kind: 'methodCallResult', lhs: 'user', receiver: 'svc', method: 'fetch' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('for-loop element type resolution', () => {
|
||||
it('extracts type from explicit for-loop annotation', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const nodes = parseAndFindNodes(parser, 'void f(List<User> users) { for (User u in users) {} }', 'for_statement');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
const scopeEnv = new Map<string, string>();
|
||||
const ctx = {
|
||||
scopeEnv,
|
||||
declarationTypeNodes: new Map(),
|
||||
scope: 'test@0',
|
||||
returnTypeLookup: {
|
||||
lookupReturnType: () => undefined,
|
||||
lookupRawReturnType: () => undefined,
|
||||
},
|
||||
};
|
||||
typeConfig.extractForLoopBinding!(nodes[0], ctx);
|
||||
expect(scopeEnv.get('u')).toBe('User');
|
||||
});
|
||||
|
||||
it('infers element type from call iterable return type', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { for (var u in getUsers()) {} }', 'for_statement');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
const scopeEnv = new Map<string, string>();
|
||||
const ctx = {
|
||||
scopeEnv,
|
||||
declarationTypeNodes: new Map(),
|
||||
scope: 'test@0',
|
||||
returnTypeLookup: {
|
||||
lookupReturnType: (name: string) => name === 'getUsers' ? 'User' : undefined,
|
||||
lookupRawReturnType: (name: string) => name === 'getUsers' ? 'List<User>' : undefined,
|
||||
},
|
||||
};
|
||||
typeConfig.extractForLoopBinding!(nodes[0], ctx);
|
||||
expect(scopeEnv.get('u')).toBe('User');
|
||||
});
|
||||
|
||||
it('skips non-for_statement nodes', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { var x = 1; }', 'initialized_variable_definition');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
const scopeEnv = new Map<string, string>();
|
||||
const ctx = {
|
||||
scopeEnv,
|
||||
declarationTypeNodes: new Map(),
|
||||
scope: 'test@0',
|
||||
returnTypeLookup: { lookupReturnType: () => undefined, lookupRawReturnType: () => undefined },
|
||||
};
|
||||
typeConfig.extractForLoopBinding!(nodes[0], ctx);
|
||||
expect(scopeEnv.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('virtual dispatch — named constructor', () => {
|
||||
it('detects constructor type for named constructor', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const classNames = new Set(['Dog']);
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { var d = Dog.unknown(); }', 'initialized_variable_definition');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
const result = typeConfig.detectConstructorType!(nodes[0], classNames);
|
||||
expect(result).toBe('Dog');
|
||||
});
|
||||
|
||||
it('returns undefined when callee is not a known class', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const classNames = new Set<string>();
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { var x = getUser(); }', 'initialized_variable_definition');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
const result = typeConfig.detectConstructorType!(nodes[0], classNames);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('literal type inference — full coverage', () => {
|
||||
it('infers double literal', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const tree = parser.parse('void f() { var x = 3.14; }');
|
||||
const nodes: any[] = [];
|
||||
function walk(n: any) { if (n.type.includes('floating_point')) nodes.push(n); for (let i = 0; i < n.namedChildCount; i++) walk(n.namedChild(i)); }
|
||||
walk(tree.rootNode);
|
||||
if (nodes.length > 0) expect(typeConfig.inferLiteralType!(nodes[0])).toBe('double');
|
||||
});
|
||||
|
||||
it('infers false literal', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const tree = parser.parse('void f() { var x = false; }');
|
||||
const nodes: any[] = [];
|
||||
function walk(n: any) { if (n.type === 'false') nodes.push(n); for (let i = 0; i < n.childCount; i++) { const c = n.child(i); if (c) walk(c); } }
|
||||
walk(tree.rootNode);
|
||||
if (nodes.length > 0) expect(typeConfig.inferLiteralType!(nodes[0])).toBe('bool');
|
||||
});
|
||||
|
||||
it('infers null literal', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const tree = parser.parse('void f() { var x = null; }');
|
||||
const nodes: any[] = [];
|
||||
function walk(n: any) { if (n.type === 'null_literal') nodes.push(n); for (let i = 0; i < n.namedChildCount; i++) walk(n.namedChild(i)); }
|
||||
walk(tree.rootNode);
|
||||
if (nodes.length > 0) expect(typeConfig.inferLiteralType!(nodes[0])).toBe('null');
|
||||
});
|
||||
|
||||
it('returns undefined for unknown node type', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
expect(typeConfig.inferLiteralType!({ type: 'identifier' } as any)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('generic and const declarations', () => {
|
||||
it('extracts outer type from generic declaration', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const env = new Map<string, string>();
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { List<String> names = []; }', 'initialized_variable_definition');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
typeConfig.extractDeclaration(nodes[0], env);
|
||||
expect(env.get('names')).toBe('List');
|
||||
});
|
||||
|
||||
it('infers type from generic constructor call', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const env = new Map<string, string>();
|
||||
const classNames = new Set(['Repository']);
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { var repo = Repository(); }', 'initialized_variable_definition');
|
||||
expect(nodes.length).toBeGreaterThan(0);
|
||||
typeConfig.extractInitializer!(nodes[0], env, classNames);
|
||||
expect(env.get('repo')).toBe('Repository');
|
||||
});
|
||||
|
||||
it('infers type from const constructor call', async () => {
|
||||
if (!(await loadDartOrSkip())) return;
|
||||
const env = new Map<string, string>();
|
||||
const classNames = new Set(['Config']);
|
||||
const nodes = parseAndFindNodes(parser, 'void f() { const config = Config(); }', 'initialized_variable_definition');
|
||||
if (nodes.length > 0) {
|
||||
typeConfig.extractInitializer!(nodes[0], env, classNames);
|
||||
expect(env.get('config')).toBe('Config');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue