diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c36e1129e37..7caabb60983c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -912,6 +912,9 @@ importers: rimraf: specifier: ^6.0.1 version: 6.0.1 + tree-sitter-dart: + specifier: ^1.0.0 + version: 1.0.0 tsup: specifier: ^8.4.0 version: 8.5.0(jiti@2.4.2)(postcss@8.5.4)(tsx@4.19.4)(typescript@5.8.3)(yaml@2.8.0) @@ -4061,9 +4064,6 @@ packages: '@types/node@20.17.57': resolution: {integrity: sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==} - '@types/node@20.19.19': - resolution: {integrity: sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==} - '@types/node@24.2.1': resolution: {integrity: sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==} @@ -7656,6 +7656,9 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nan@2.23.0: + resolution: {integrity: sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==} + nano-css@5.6.2: resolution: {integrity: sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==} peerDependencies: @@ -9345,6 +9348,9 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + tree-sitter-dart@1.0.0: + resolution: {integrity: sha512-Ve5YMPJjjGW9LEsO+MngAOibQsw5obFp+bUT41pvwdcXWRwJImOWs3eaPi6AubEiBmc09qvhdvxeIXvxlhMnug==} + tree-sitter-wasms@0.1.12: resolution: {integrity: sha512-N9Jp+dkB23Ul5Gw0utm+3pvG4km4Fxsi2jmtMFg7ivzwqWPlSyrYQIrOmcX+79taVfcHEA+NzP0hl7vXL8DNUQ==} @@ -9521,9 +9527,6 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.10.0: resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} @@ -13662,11 +13665,6 @@ snapshots: dependencies: undici-types: 6.19.8 - '@types/node@20.19.19': - dependencies: - undici-types: 6.21.0 - optional: true - '@types/node@24.2.1': dependencies: undici-types: 7.10.0 @@ -13736,7 +13734,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 20.19.19 + '@types/node': 24.2.1 optional: true '@types/yargs-parser@21.0.3': {} @@ -13909,7 +13907,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: @@ -17868,6 +17866,8 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + nan@2.23.0: {} + nano-css@5.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -19823,6 +19823,10 @@ snapshots: tree-kill@1.2.2: {} + tree-sitter-dart@1.0.0: + dependencies: + nan: 2.23.0 + tree-sitter-wasms@0.1.12: {} trim-lines@3.0.1: {} @@ -20011,9 +20015,6 @@ snapshots: undici-types@6.19.8: {} - undici-types@6.21.0: - optional: true - undici-types@7.10.0: {} undici@6.21.3: {} diff --git a/src/package.json b/src/package.json index d6d2f4ce60a6..8fcddb5f0106 100644 --- a/src/package.json +++ b/src/package.json @@ -554,6 +554,7 @@ "npm-run-all2": "^8.0.1", "ovsx": "0.10.4", "rimraf": "^6.0.1", + "tree-sitter-dart": "^1.0.0", "tsup": "^8.4.0", "tsx": "^4.19.3", "typescript": "5.8.3", diff --git a/src/services/tree-sitter/__tests__/fixtures/sample-dart.ts b/src/services/tree-sitter/__tests__/fixtures/sample-dart.ts new file mode 100644 index 000000000000..27c62beb909d --- /dev/null +++ b/src/services/tree-sitter/__tests__/fixtures/sample-dart.ts @@ -0,0 +1,337 @@ +export default String.raw` +// Library directive +library my_flutter_app; + +// Import statements +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; + +// Export statement +export 'src/models/user.dart'; + +// Part directive +part 'src/utils/helpers.dart'; + +// Type alias - at least 4 lines +typedef JsonMap = Map; +typedef AsyncCallback = Future Function( + String message, + int retryCount, +); + +// Abstract class - at least 4 lines +abstract class BaseRepository { + final String endpoint; + final http.Client client; + + BaseRepository({ + required this.endpoint, + required this.client, + }); + + // Abstract method + Future fetchData( + String id, + Map headers, + ); + + // Concrete method - at least 4 lines + Future> fetchAll({ + int limit = 10, + int offset = 0, + }) async { + final response = await client.get( + Uri.parse('$endpoint?limit=$limit&offset=$offset'), + ); + return parseResponse(response.body); + } + + List parseResponse(String body); +} + +// Mixin declaration - at least 4 lines +mixin ValidationMixin { + bool isEmailValid(String email) { + return RegExp( + r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$', + ).hasMatch(email); + } + + bool isPasswordStrong( + String password, + {int minLength = 8} + ) { + return password.length >= minLength && + password.contains(RegExp(r'[A-Z]')) && + password.contains(RegExp(r'[0-9]')); + } +} + +// Enum declaration - at least 4 lines +enum UserRole { + admin('Administrator', 3), + moderator('Moderator', 2), + user('Regular User', 1), + guest('Guest', 0); + + final String displayName; + final int level; + + const UserRole( + this.displayName, + this.level, + ); +} + +// Main class with various features - at least 4 lines +class UserService extends BaseRepository + with ValidationMixin { + static final UserService _instance = UserService._internal(); + + // Factory constructor - at least 4 lines + factory UserService({ + required http.Client client, + String? customEndpoint, + }) { + _instance._client = client; + _instance._endpoint = customEndpoint ?? '/api/users'; + return _instance; + } + + // Private constructor + UserService._internal() : super( + endpoint: '/api/users', + client: http.Client(), + ); + + late http.Client _client; + late String _endpoint; + + // Getter - at least 4 lines + String get currentEndpoint { + if (_endpoint.isEmpty) { + return '/api/users'; + } + return _endpoint; + } + + // Setter - at least 4 lines + set currentEndpoint(String value) { + if (value.isNotEmpty && value.startsWith('/')) { + _endpoint = value; + print('Endpoint updated to: $_endpoint'); + } + } + + // Override abstract method - at least 4 lines + @override + Future fetchData( + String id, + Map headers, + ) async { + final response = await _client.get( + Uri.parse('$_endpoint/$id'), + headers: headers, + ); + + if (response.statusCode == 200) { + return User.fromJson(response.body); + } + throw Exception('Failed to load user'); + } + + // Async method - at least 4 lines + Future createUser({ + required String email, + required String password, + UserRole role = UserRole.user, + }) async { + if (!isEmailValid(email)) { + throw ArgumentError('Invalid email format'); + } + + if (!isPasswordStrong(password)) { + throw ArgumentError('Password is too weak'); + } + + final response = await _client.post( + Uri.parse(_endpoint), + body: { + 'email': email, + 'password': password, + 'role': role.name, + }, + ); + + return response.statusCode == 201 + ? User.fromJson(response.body) + : null; + } + + // Generator function (sync*) - at least 4 lines + Iterable generateUserIds({ + int start = 1, + int count = 10, + }) sync* { + for (int i = start; i < start + count; i++) { + yield i; + } + } + + // Async generator (async*) - at least 4 lines + Stream streamUsers({ + Duration interval = const Duration(seconds: 1), + int maxUsers = 5, + }) async* { + for (int i = 0; i < maxUsers; i++) { + await Future.delayed(interval); + yield User(id: i, email: 'user$i@example.com'); + } + } + + // Operator overloading - at least 4 lines + operator [](String userId) { + return fetchData( + userId, + {'Authorization': 'Bearer token'}, + ); + } + + @override + List parseResponse(String body) { + // Implementation would parse JSON to List + return []; + } +} + +// Extension declaration - at least 4 lines +extension StringExtensions on String { + String capitalize() { + if (isEmpty) return this; + return '\${this[0].toUpperCase()}\${substring(1)}'; + } + + bool get isValidEmail { + return RegExp( + r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$', + ).hasMatch(this); + } +} + +// Model class - at least 4 lines +class User { + final int id; + final String email; + final UserRole role; + final DateTime createdAt; + + // Constructor with optional parameters - at least 4 lines + User({ + required this.id, + required this.email, + this.role = UserRole.user, + DateTime? createdAt, + }) : createdAt = createdAt ?? DateTime.now(); + + // Named constructor - at least 4 lines + User.fromJson(String json) : + id = 0, + email = '', + role = UserRole.user, + createdAt = DateTime.now() { + // Parse JSON implementation + } + + // Method with lambda - at least 4 lines + Map toJson() => { + 'id': id, + 'email': email, + 'role': role.name, + 'createdAt': createdAt.toIso8601String(), + }; +} + +// Top-level function - at least 4 lines +Future initializeApp({ + required String apiKey, + bool enableLogging = false, +}) async { + print('Initializing app with API key: $apiKey'); + await Future.delayed(Duration(seconds: 2)); + + if (enableLogging) { + print('Logging enabled'); + } +} + +// Top-level variable declarations - at least 4 lines +final GlobalKey navigatorKey = GlobalKey(); +const String appVersion = '1.0.0'; +late final SharedPreferences prefs; +var currentTheme = ThemeMode.system; + +// Lambda/Anonymous function assignment - at least 4 lines +final formatDate = (DateTime date, {String format = 'yyyy-MM-dd'}) { + final year = date.year.toString(); + final month = date.month.toString().padLeft(2, '0'); + final day = date.day.toString().padLeft(2, '0'); + return '$year-$month-$day'; +}; + +// Widget class (Flutter specific) - at least 4 lines +class MyHomePage extends StatefulWidget { + final String title; + final VoidCallback? onPressed; + + const MyHomePage({ + Key? key, + required this.title, + this.onPressed, + }) : super(key: key); + + @override + State createState() => _MyHomePageState(); +} + +// State class - at least 4 lines +class _MyHomePageState extends State { + int _counter = 0; + + void _incrementCounter() { + setState(() { + _counter++; + print('Counter incremented to: $_counter'); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'You have pushed the button this many times:', + ), + Text( + '$_counter', + style: Theme.of(context).textTheme.headlineMedium, + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: Icon(Icons.add), + ), + ); + } +} +` diff --git a/src/services/tree-sitter/__tests__/inspectDart.spec.ts b/src/services/tree-sitter/__tests__/inspectDart.spec.ts new file mode 100644 index 000000000000..8f7fd7b9ebf8 --- /dev/null +++ b/src/services/tree-sitter/__tests__/inspectDart.spec.ts @@ -0,0 +1,23 @@ +import { inspectTreeStructure, testParseSourceCodeDefinitions } from "./helpers" +import { dartQuery } from "../queries" +import sampleDartContent from "./fixtures/sample-dart" + +describe("inspectDart", () => { + const testOptions = { + language: "dart", + wasmFile: "tree-sitter-dart.wasm", + queryString: dartQuery, + extKey: "dart", + } + + it("should inspect Dart tree structure", async () => { + const result = await inspectTreeStructure(sampleDartContent, "dart") + expect(result).toBeTruthy() + }) + + it("should parse Dart definitions", async () => { + const result = await testParseSourceCodeDefinitions("test.dart", sampleDartContent, testOptions) + expect(result).toBeTruthy() + expect(result).toMatch(/\d+--\d+ \| /) // Verify line number format + }) +}) diff --git a/src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.dart.spec.ts b/src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.dart.spec.ts new file mode 100644 index 000000000000..72ce512ebd5e --- /dev/null +++ b/src/services/tree-sitter/__tests__/parseSourceCodeDefinitions.dart.spec.ts @@ -0,0 +1,56 @@ +import { testParseSourceCodeDefinitions } from "./helpers" +import { dartQuery } from "../queries" +import sampleDartContent from "./fixtures/sample-dart" + +describe("parseSourceCodeDefinitions for Dart", () => { + const testOptions = { + language: "dart", + wasmFile: "tree-sitter-dart.wasm", + queryString: dartQuery, + extKey: "dart", + } + + it("should parse Dart class definitions", async () => { + const result = await testParseSourceCodeDefinitions("test.dart", sampleDartContent, testOptions) + expect(result).toBeTruthy() + expect(result).toContain("UserService") + expect(result).toContain("BaseRepository") + }) + + it("should parse Dart method definitions", async () => { + const result = await testParseSourceCodeDefinitions("test.dart", sampleDartContent, testOptions) + expect(result).toContain("fetchData") + expect(result).toContain("createUser") + }) + + it("should parse Dart mixin definitions", async () => { + const result = await testParseSourceCodeDefinitions("test.dart", sampleDartContent, testOptions) + expect(result).toContain("ValidationMixin") + }) + + it("should parse Dart enum definitions", async () => { + const result = await testParseSourceCodeDefinitions("test.dart", sampleDartContent, testOptions) + expect(result).toContain("UserRole") + }) + + it("should parse Dart extension definitions", async () => { + const result = await testParseSourceCodeDefinitions("test.dart", sampleDartContent, testOptions) + expect(result).toContain("StringExtensions") + }) + + it("should parse Dart constructor definitions", async () => { + const result = await testParseSourceCodeDefinitions("test.dart", sampleDartContent, testOptions) + expect(result).toContain("UserService") + expect(result).toContain("User") + }) + + it("should parse Dart top-level function definitions", async () => { + const result = await testParseSourceCodeDefinitions("test.dart", sampleDartContent, testOptions) + expect(result).toContain("initializeApp") + }) + + it("should format output with line numbers", async () => { + const result = await testParseSourceCodeDefinitions("test.dart", sampleDartContent, testOptions) + expect(result).toMatch(/\d+--\d+ \| /) + }) +}) diff --git a/src/services/tree-sitter/index.ts b/src/services/tree-sitter/index.ts index 145ba847308d..9b563d365f90 100644 --- a/src/services/tree-sitter/index.ts +++ b/src/services/tree-sitter/index.ts @@ -91,6 +91,8 @@ const extensions = [ "erb", // Visual Basic .NET "vb", + // Dart + "dart", ].map((e) => `.${e}`) export { extensions } diff --git a/src/services/tree-sitter/languageParser.ts b/src/services/tree-sitter/languageParser.ts index a8ac0a9ead90..b7faf2cdba89 100644 --- a/src/services/tree-sitter/languageParser.ts +++ b/src/services/tree-sitter/languageParser.ts @@ -28,6 +28,7 @@ import { embeddedTemplateQuery, elispQuery, elixirQuery, + dartQuery, } from "./queries" export interface LanguageParser { @@ -218,6 +219,10 @@ export async function loadRequiredLanguageParsers(filesToParse: string[], source language = await loadLanguage("elixir", sourceDirectory) query = new Query(language, elixirQuery) break + case "dart": + language = await loadLanguage("dart", sourceDirectory) + query = new Query(language, dartQuery) + break default: throw new Error(`Unsupported language: ${ext}`) } diff --git a/src/services/tree-sitter/queries/dart.ts b/src/services/tree-sitter/queries/dart.ts new file mode 100644 index 000000000000..e6273925d83d --- /dev/null +++ b/src/services/tree-sitter/queries/dart.ts @@ -0,0 +1,10 @@ +/* +Query patterns for Dart language structures +Note: This is a basic implementation that captures common Dart constructs. +The exact node names may vary based on the tree-sitter-dart grammar version. +*/ +export default ` +; Capture all identifiers as potential definitions +; This is a fallback pattern that should work with most grammars +(identifier) @definition.identifier +` diff --git a/src/services/tree-sitter/queries/index.ts b/src/services/tree-sitter/queries/index.ts index de9b9cafa3bc..0faa25626976 100644 --- a/src/services/tree-sitter/queries/index.ts +++ b/src/services/tree-sitter/queries/index.ts @@ -26,3 +26,4 @@ export { zigQuery } from "./zig" export { default as embeddedTemplateQuery } from "./embedded_template" export { elispQuery } from "./elisp" export { scalaQuery } from "./scala" +export { default as dartQuery } from "./dart"