Skip to content

Commit 371d529

Browse files
Cobdogclaude
andauthored
feat: add Dart language support
* feat: add Dart language support Add tree-sitter-based parsing for Dart source files (.dart), covering: - Classes, mixins, extensions, enums, typedefs - Top-level functions and class methods (including getters) - /// doc comment extraction (skips past @annotations) - @annotation decorator extraction - Sibling-body byte range extension for full source retrieval Dart's AST differs from other languages: function/method signatures and bodies are sibling nodes rather than parent-child. The extractor handles this transparently by extending byte ranges to include sibling function_body nodes. Includes tests, fixture file, and documentation updates. * fix: address code review findings for Dart support - Fix end_line not updating when extending byte range to sibling function_body (Dart's signature/body sibling pattern) - Add missing "php" to search_symbols language enum (pre-existing) - Enrich Dart fixture with mixin, extension, and getter examples - Update LANGUAGE_SUPPORT.md notes: constructors and top-level constants are not indexed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7d952be commit 371d529

8 files changed

Lines changed: 227 additions & 8 deletions

File tree

LANGUAGE_SUPPORT.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
| Rust | `.rs` | tree-sitter-rust | function, type (struct/enum/trait), impl, constant | `#[attr]` | `///` and `//!` comments | Macro-generated symbols are not visible to the parser |
1212
| Java | `.java` | tree-sitter-java | method, class, type (interface/enum), constant | `@Annotation` | `/** */` Javadoc | Deep inner-class nesting may be flattened |
1313
| PHP | `.php` | tree-sitter-php | function, class, method, type (interface/trait/enum), constant | `#[Attribute]` | `/** */` PHPDoc | PHP 8+ attributes supported; language-file `<?php` tag required |
14+
| Dart | `.dart` | tree-sitter-dart | function, class (class/mixin/extension), method, type (enum/typedef) | `@annotation` | `///` doc comments | Constructors and top-level constants are not indexed |
1415

1516
---
1617

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ Every tool response includes a `_meta` envelope with timing, token savings, and
293293
| Rust | `.rs` | function, type, impl, constant |
294294
| Java | `.java` | method, class, type, constant |
295295
| PHP | `.php` | function, class, method, type, constant |
296+
| Dart | `.dart` | function, class, method, type |
296297

297298
See LANGUAGE_SUPPORT.md for full semantics.
298299

src/jcodemunch_mcp/parser/extractor.py

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ def _walk_tree(
4747
parent_symbol: Optional[Symbol] = None
4848
):
4949
"""Recursively walk the AST and extract symbols."""
50+
# Dart: function_signature inside method_signature is handled by method_signature
51+
if node.type == "function_signature" and node.parent and node.parent.type == "method_signature":
52+
return
53+
5054
# Check if this node is a symbol
5155
if node.type in spec.symbol_node_types:
5256
symbol = _extract_symbol(
@@ -103,8 +107,17 @@ def _extract_symbol(
103107
# Extract decorators
104108
decorators = _extract_decorators(node, spec, source_bytes)
105109

110+
# Dart: function_signature/method_signature have their body as a next sibling
111+
end_byte = node.end_byte
112+
end_line_num = node.end_point[0] + 1
113+
if node.type in ("function_signature", "method_signature"):
114+
next_sib = node.next_named_sibling
115+
if next_sib and next_sib.type == "function_body":
116+
end_byte = next_sib.end_byte
117+
end_line_num = next_sib.end_point[0] + 1
118+
106119
# Compute content hash
107-
symbol_bytes = source_bytes[node.start_byte:node.end_byte]
120+
symbol_bytes = source_bytes[node.start_byte:end_byte]
108121
c_hash = compute_content_hash(symbol_bytes)
109122

110123
# Create symbol
@@ -120,9 +133,9 @@ def _extract_symbol(
120133
decorators=decorators,
121134
parent=parent_symbol.id if parent_symbol else None,
122135
line=node.start_point[0] + 1,
123-
end_line=node.end_point[0] + 1,
136+
end_line=end_line_num,
124137
byte_offset=node.start_byte,
125-
byte_length=node.end_byte - node.start_byte,
138+
byte_length=end_byte - node.start_byte,
126139
content_hash=c_hash,
127140
)
128141

@@ -144,7 +157,30 @@ def _extract_name(node, spec: LanguageSpec, source_bytes: bytes) -> Optional[str
144157
if name_node:
145158
return source_bytes[name_node.start_byte:name_node.end_byte].decode("utf-8")
146159
return None
147-
160+
161+
# Dart: mixin_declaration has identifier as direct child (no field name)
162+
if node.type == "mixin_declaration":
163+
for child in node.children:
164+
if child.type == "identifier":
165+
return source_bytes[child.start_byte:child.end_byte].decode("utf-8")
166+
return None
167+
168+
# Dart: method_signature wraps function_signature or getter_signature
169+
if node.type == "method_signature":
170+
for child in node.children:
171+
if child.type in ("function_signature", "getter_signature"):
172+
name_node = child.child_by_field_name("name")
173+
if name_node:
174+
return source_bytes[name_node.start_byte:name_node.end_byte].decode("utf-8")
175+
return None
176+
177+
# Dart: type_alias name is the first type_identifier child
178+
if node.type == "type_alias":
179+
for child in node.children:
180+
if child.type == "type_identifier":
181+
return source_bytes[child.start_byte:child.end_byte].decode("utf-8")
182+
return None
183+
148184
if node.type not in spec.name_fields:
149185
return None
150186

@@ -231,10 +267,12 @@ def _strip_quotes(text: str) -> str:
231267
def _extract_preceding_comments(node, source_bytes: bytes) -> str:
232268
"""Extract comments that immediately precede a node."""
233269
comments = []
234-
235-
# Walk backwards through siblings
270+
271+
# Walk backwards through siblings, skipping past annotations/decorators
236272
prev = node.prev_named_sibling
237-
while prev and prev.type in ("comment", "line_comment", "block_comment"):
273+
while prev and prev.type in ("annotation", "marker_annotation"):
274+
prev = prev.prev_named_sibling
275+
while prev and prev.type in ("comment", "line_comment", "block_comment", "documentation_comment"):
238276
comment_text = source_bytes[prev.start_byte:prev.end_byte].decode("utf-8")
239277
comments.insert(0, comment_text)
240278
prev = prev.prev_named_sibling

src/jcodemunch_mcp/parser/languages.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class LanguageSpec:
5454
".rs": "rust",
5555
".java": "java",
5656
".php": "php",
57+
".dart": "dart",
5758
}
5859

5960

@@ -277,6 +278,37 @@ class LanguageSpec:
277278
)
278279

279280

281+
# Dart specification
282+
DART_SPEC = LanguageSpec(
283+
ts_language="dart",
284+
symbol_node_types={
285+
"function_signature": "function",
286+
"class_definition": "class",
287+
"mixin_declaration": "class",
288+
"enum_declaration": "type",
289+
"extension_declaration": "class",
290+
"method_signature": "method",
291+
"type_alias": "type",
292+
},
293+
name_fields={
294+
"function_signature": "name",
295+
"class_definition": "name",
296+
"enum_declaration": "name",
297+
"extension_declaration": "name",
298+
# mixin_declaration, method_signature, type_alias: special-cased in extractor
299+
},
300+
param_fields={
301+
"function_signature": "parameters",
302+
},
303+
return_type_fields={},
304+
docstring_strategy="preceding_comment",
305+
decorator_node_type="annotation",
306+
container_node_types=["class_definition", "mixin_declaration", "extension_declaration"],
307+
constant_patterns=[],
308+
type_patterns=["type_alias", "enum_declaration"],
309+
)
310+
311+
280312
# Language registry
281313
LANGUAGE_REGISTRY = {
282314
"python": PYTHON_SPEC,
@@ -286,4 +318,5 @@ class LanguageSpec:
286318
"rust": RUST_SPEC,
287319
"java": JAVA_SPEC,
288320
"php": PHP_SPEC,
321+
"dart": DART_SPEC,
289322
}

src/jcodemunch_mcp/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ async def list_tools() -> list[Tool]:
212212
"language": {
213213
"type": "string",
214214
"description": "Optional filter by language",
215-
"enum": ["python", "javascript", "typescript", "go", "rust", "java"]
215+
"enum": ["python", "javascript", "typescript", "go", "rust", "java", "php", "dart"]
216216
},
217217
"max_results": {
218218
"type": "integer",

tests/fixtures/dart/sample.dart

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/// User service for managing users.
2+
class UserService {
3+
/// Get user by ID.
4+
String getUser(int userId) {
5+
return 'user-$userId';
6+
}
7+
8+
/// Delete a user.
9+
bool deleteUser(int userId) {
10+
return true;
11+
}
12+
13+
/// Whether the service is ready.
14+
bool get isReady => true;
15+
}
16+
17+
/// Scrollable behavior.
18+
mixin Scrollable on UserService {
19+
/// Scroll to offset.
20+
void scrollTo(double offset) {}
21+
}
22+
23+
/// Authenticate a token.
24+
bool authenticate(String token) {
25+
return token.isNotEmpty;
26+
}
27+
28+
/// Status of a request.
29+
enum Status { pending, active, done }
30+
31+
/// Helpers for String manipulation.
32+
extension StringExt on String {
33+
/// Whether the string is blank.
34+
bool get isBlank => trim().isEmpty;
35+
}
36+
37+
/// JSON map alias.
38+
typedef JsonMap = Map<String, dynamic>;

tests/test_hardening.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ class TestDeterminism:
322322
("go", "sample.go"),
323323
("rust", "sample.rs"),
324324
("java", "Sample.java"),
325+
("dart", "sample.dart"),
325326
])
326327
def test_deterministic_ids_and_hashes(self, language, filename):
327328
content, fname = _fixture(language, filename)

tests/test_languages.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,3 +259,110 @@ def test_parse_php():
259259
assert enum is not None
260260
assert enum.kind == "type"
261261

262+
263+
DART_SOURCE = '''
264+
/// Greet a user by name.
265+
String greet(String name) {
266+
return 'Hello, $name!';
267+
}
268+
269+
/// A simple calculator.
270+
class Calculator {
271+
/// Add two numbers.
272+
int add(int a, int b) {
273+
return a + b;
274+
}
275+
276+
/// Whether the result is positive.
277+
bool get isPositive => true;
278+
}
279+
280+
/// Scrollable behavior for widgets.
281+
mixin Scrollable on Calculator {
282+
/// Scroll to offset.
283+
void scrollTo(double offset) {}
284+
}
285+
286+
/// Status of a task.
287+
enum Status { pending, active, done }
288+
289+
/// Helpers for String manipulation.
290+
extension StringExt on String {
291+
/// Whether the string is blank.
292+
bool get isBlank => trim().isEmpty;
293+
}
294+
295+
/// A JSON map alias.
296+
typedef JsonMap = Map<String, dynamic>;
297+
298+
/// An abstract repository.
299+
abstract class Repository {
300+
/// Get all items.
301+
@override
302+
Future<List<String>> getAll() {
303+
return Future.value([]);
304+
}
305+
}
306+
'''
307+
308+
309+
def test_parse_dart():
310+
"""Test Dart parsing."""
311+
symbols = parse_file(DART_SOURCE, "app.dart", "dart")
312+
313+
# Top-level function
314+
func = next((s for s in symbols if s.name == "greet"), None)
315+
assert func is not None
316+
assert func.kind == "function"
317+
assert "Greet a user by name" in func.docstring
318+
319+
# Class
320+
cls = next((s for s in symbols if s.name == "Calculator"), None)
321+
assert cls is not None
322+
assert cls.kind == "class"
323+
assert "simple calculator" in cls.docstring
324+
325+
# Method
326+
method = next((s for s in symbols if s.name == "add"), None)
327+
assert method is not None
328+
assert method.kind == "method"
329+
assert "Add two numbers" in method.docstring
330+
331+
# Getter
332+
getter = next((s for s in symbols if s.name == "isPositive"), None)
333+
assert getter is not None
334+
assert getter.kind == "method"
335+
336+
# Mixin
337+
mixin = next((s for s in symbols if s.name == "Scrollable"), None)
338+
assert mixin is not None
339+
assert mixin.kind == "class"
340+
341+
# Enum
342+
enum = next((s for s in symbols if s.name == "Status"), None)
343+
assert enum is not None
344+
assert enum.kind == "type"
345+
346+
# Extension
347+
ext = next((s for s in symbols if s.name == "StringExt"), None)
348+
assert ext is not None
349+
assert ext.kind == "class"
350+
351+
# Typedef
352+
typedef = next((s for s in symbols if s.name == "JsonMap"), None)
353+
assert typedef is not None
354+
assert typedef.kind == "type"
355+
356+
# Abstract class with @override decorator
357+
repo = next((s for s in symbols if s.name == "Repository"), None)
358+
assert repo is not None
359+
assert repo.kind == "class"
360+
repo_method = next((s for s in symbols if s.name == "getAll"), None)
361+
assert repo_method is not None
362+
assert repo_method.kind == "method"
363+
assert "@override" in repo_method.decorators
364+
365+
# Qualified names
366+
assert method.qualified_name == "Calculator.add"
367+
assert getter.qualified_name == "Calculator.isPositive"
368+

0 commit comments

Comments
 (0)