Skip to content

Commit a0ff275

Browse files
added support for dart CodeGraphContext#671
1 parent be3ddf1 commit a0ff275

8 files changed

Lines changed: 571 additions & 1 deletion

File tree

src/codegraphcontext/tools/graph_builder.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ def __init__(self, language_name: str):
7474
elif self.language_name == 'haskell':
7575
from .languages.haskell import HaskellTreeSitterParser
7676
self.language_specific_parser = HaskellTreeSitterParser(self)
77+
elif self.language_name == 'dart':
78+
from .languages.dart import DartTreeSitterParser
79+
self.language_specific_parser = DartTreeSitterParser(self)
7780

7881

7982

@@ -120,6 +123,7 @@ def __init__(self, db_manager: DatabaseManager, job_manager: JobManager, loop: a
120123
'.sc': TreeSitterParser('scala'),
121124
'.swift': TreeSitterParser('swift'),
122125
'.hs': TreeSitterParser('haskell'),
126+
'.dart': TreeSitterParser('dart'),
123127
}
124128
self.create_schema()
125129

@@ -249,6 +253,9 @@ def _pre_scan_for_imports(self, files: list[Path]) -> dict:
249253
if '.swift' in files_by_lang:
250254
from .languages import swift as swift_lang_module
251255
imports_map.update(swift_lang_module.pre_scan_swift(files_by_lang['.swift'], self.parsers['.swift']))
256+
if '.dart' in files_by_lang:
257+
from .languages import dart as dart_lang_module
258+
imports_map.update(dart_lang_module.pre_scan_dart(files_by_lang['.dart'], self.parsers['.dart']))
252259

253260
return imports_map
254261

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
1+
from pathlib import Path
2+
from typing import Any, Dict, Optional, Tuple, List
3+
import logging
4+
from codegraphcontext.utils.debug_log import debug_log, info_logger, error_logger, warning_logger
5+
from codegraphcontext.utils.tree_sitter_manager import execute_query
6+
7+
DART_QUERIES = {
8+
"functions": """
9+
(function_signature
10+
name: (identifier) @name
11+
(formal_parameter_list) @params
12+
) @function_node
13+
(constructor_signature
14+
name: (identifier) @name
15+
(formal_parameter_list) @params
16+
) @function_node
17+
""",
18+
"classes": """
19+
[
20+
(class_definition name: (identifier) @name)
21+
(mixin_declaration name: (identifier) @name)
22+
(extension_declaration name: (identifier) @name)
23+
(enum_declaration name: (identifier) @name)
24+
] @class
25+
""",
26+
"imports": """
27+
(library_import) @import
28+
(library_export) @import
29+
""",
30+
"calls": """
31+
(expression_statement
32+
(identifier) @name
33+
) @call
34+
(selector
35+
(argument_part (arguments))
36+
) @call
37+
""",
38+
"variables": """
39+
(local_variable_declaration
40+
(initialized_variable_definition
41+
name: (identifier) @name
42+
)
43+
) @variable
44+
(declaration
45+
(initialized_identifier_list
46+
(initialized_identifier
47+
(identifier) @name
48+
)
49+
)
50+
) @variable
51+
""",
52+
}
53+
54+
class DartTreeSitterParser:
55+
"""A Dart-specific parser using tree-sitter, encapsulating language-specific logic."""
56+
57+
def __init__(self, generic_parser_wrapper):
58+
self.generic_parser_wrapper = generic_parser_wrapper
59+
self.language_name = "dart"
60+
self.language = generic_parser_wrapper.language
61+
self.parser = generic_parser_wrapper.parser
62+
self.index_source = False
63+
64+
def _get_node_text(self, node) -> str:
65+
if not node: return ""
66+
return node.text.decode('utf-8')
67+
68+
def _get_parent_context(self, node, types=('function_signature', 'class_definition', 'mixin_declaration', 'extension_declaration')):
69+
curr = node.parent
70+
while curr:
71+
if curr.type in types:
72+
name_node = curr.child_by_field_name('name')
73+
return self._get_node_text(name_node) if name_node else None, curr.type, curr.start_point[0] + 1
74+
curr = curr.parent
75+
return None, None, None
76+
77+
def _calculate_complexity(self, node):
78+
complexity_nodes = {
79+
"if_statement", "for_statement", "while_statement", "do_statement",
80+
"switch_statement", "switch_case", "if_element", "for_element",
81+
"conditional_expression", "binary_expression", "catch_clause"
82+
}
83+
count = 1
84+
85+
def traverse(n):
86+
nonlocal count
87+
if n.type in complexity_nodes:
88+
if n.type == "binary_expression":
89+
op = n.child_by_field_name("operator")
90+
if op and self._get_node_text(op) in ("&&", "||"):
91+
count += 1
92+
else:
93+
count += 1
94+
for child in n.children:
95+
traverse(child)
96+
97+
traverse(node)
98+
return count
99+
100+
def parse(self, path: Path, is_dependency: bool = False, index_source: bool = False) -> Dict:
101+
"""Parses a Dart file and returns its structure in a standardized dictionary format."""
102+
self.index_source = index_source
103+
try:
104+
with open(path, "r", encoding="utf-8", errors='ignore') as f:
105+
source_code = f.read()
106+
107+
tree = self.parser.parse(bytes(source_code, "utf8"))
108+
root_node = tree.root_node
109+
110+
functions = self._find_functions(root_node)
111+
classes = self._find_classes(root_node)
112+
imports = self._find_imports(root_node, source_code)
113+
function_calls = self._find_calls(root_node)
114+
variables = self._find_variables(root_node)
115+
116+
return {
117+
"path": str(path),
118+
"functions": functions,
119+
"classes": classes,
120+
"variables": variables,
121+
"imports": imports,
122+
"function_calls": function_calls,
123+
"is_dependency": is_dependency,
124+
"lang": self.language_name,
125+
}
126+
except Exception as e:
127+
error_logger(f"Failed to parse Dart file {path}: {e}")
128+
return {"path": str(path), "error": str(e)}
129+
130+
def _find_functions(self, root_node):
131+
functions = []
132+
seen_nodes = set()
133+
query_str = DART_QUERIES['functions']
134+
135+
for node, capture_name in execute_query(self.language, query_str, root_node):
136+
if capture_name == "function_node":
137+
node_id = (node.start_byte, node.end_byte)
138+
if node_id in seen_nodes: continue
139+
seen_nodes.add(node_id)
140+
141+
name_node = node.child_by_field_name('name')
142+
if not name_node: continue
143+
144+
name = self._get_node_text(name_node)
145+
params_node = node.child_by_field_name('parameters') or node.child_by_field_name('formal_parameter_list')
146+
147+
args = []
148+
if params_node:
149+
for child in params_node.children:
150+
if child.type == 'formal_parameter':
151+
# Extract parameter name
152+
# can be 'int x', 'var x', 'final x', 'x', 'this.x'
153+
p_name = self._extract_param_name(child)
154+
if p_name: args.append(p_name)
155+
156+
# Find body to get complexity and end_line
157+
# In Dart, body is often a sibling of signature
158+
body_node = None
159+
parent = node.parent
160+
if parent:
161+
# Look for function_body among siblings after the signature
162+
found_sig = False
163+
for child in parent.children:
164+
if child == node:
165+
found_sig = True
166+
continue
167+
if found_sig:
168+
if child.type == 'function_body':
169+
body_node = child
170+
break
171+
elif child.type in ('function_signature', 'method_signature', 'declaration', 'class_definition'):
172+
# Hit another signature or declaration, stop looking
173+
break
174+
175+
context, context_type, context_line = self._get_parent_context(node)
176+
# Check if it's in a class
177+
class_context = None
178+
curr = node.parent
179+
while curr:
180+
if curr.type == 'class_definition':
181+
cn = curr.child_by_field_name('name')
182+
class_context = self._get_node_text(cn) if cn else None
183+
break
184+
curr = curr.parent
185+
186+
func_data = {
187+
"name": name,
188+
"line_number": node.start_point[0] + 1,
189+
"end_line": (body_node or node).end_point[0] + 1,
190+
"args": args,
191+
"cyclomatic_complexity": self._calculate_complexity(body_node) if body_node else 1,
192+
"context": context,
193+
"context_type": context_type,
194+
"class_context": class_context,
195+
"lang": self.language_name,
196+
"is_dependency": False,
197+
}
198+
if self.index_source:
199+
func_data["source"] = self._get_node_text(node) + (self._get_node_text(body_node) if body_node else "")
200+
201+
functions.append(func_data)
202+
return functions
203+
204+
def _extract_param_name(self, param_node) -> Optional[str]:
205+
# formal_parameter -> normal_parameter -> ... -> identifier
206+
# or formal_parameter -> constructor_param -> this.identifier
207+
def find_id(n):
208+
if n.type == 'identifier':
209+
return self._get_node_text(n)
210+
for child in n.children:
211+
res = find_id(child)
212+
if res: return res
213+
return None
214+
return find_id(param_node)
215+
216+
def _find_classes(self, root_node):
217+
classes = []
218+
query_str = DART_QUERIES['classes']
219+
for node, capture_name in execute_query(self.language, query_str, root_node):
220+
if capture_name == "class":
221+
name_node = node.child_by_field_name('name')
222+
if not name_node: continue
223+
224+
name = self._get_node_text(name_node)
225+
226+
# Bases (implements, extends, with)
227+
bases = []
228+
# This is simplified, can be improved by navigating children
229+
for child in node.children:
230+
if child.type in ('superclass', 'interfaces', 'mixins'):
231+
for sub in child.children:
232+
if sub.type in ('type_identifier', 'type_not_void'):
233+
bases.append(self._get_node_text(sub))
234+
235+
class_data = {
236+
"name": name,
237+
"line_number": node.start_point[0] + 1,
238+
"end_line": node.end_point[0] + 1,
239+
"bases": bases,
240+
"lang": self.language_name,
241+
"is_dependency": False,
242+
}
243+
if self.index_source:
244+
class_data["source"] = self._get_node_text(node)
245+
246+
classes.append(class_data)
247+
return classes
248+
249+
def _find_imports(self, root_node, source_code):
250+
imports = []
251+
query_str = DART_QUERIES['imports']
252+
for node, capture_name in execute_query(self.language, query_str, root_node):
253+
if capture_name == "import":
254+
# Find URI
255+
uri_node = None
256+
def find_uri(n):
257+
nonlocal uri_node
258+
if n.type == 'uri':
259+
uri_node = n
260+
return
261+
for child in n.children:
262+
find_uri(child)
263+
if uri_node: return
264+
265+
find_uri(node)
266+
if uri_node:
267+
uri_text = self._get_node_text(uri_node).strip("'\"")
268+
269+
# Handle 'as' alias
270+
alias = None
271+
for child in node.children:
272+
if child.type == 'import_specification':
273+
for sub in child.children:
274+
if sub.type == 'prefix':
275+
alias_node = sub.child_by_field_name('identifier')
276+
if alias_node:
277+
alias = self._get_node_text(alias_node)
278+
279+
imports.append({
280+
"name": uri_text,
281+
"full_import_name": uri_text,
282+
"line_number": node.start_point[0] + 1,
283+
"alias": alias,
284+
"lang": self.language_name,
285+
"is_dependency": False,
286+
})
287+
return imports
288+
289+
def _find_calls(self, root_node):
290+
calls = []
291+
seen_calls = set()
292+
query_str = DART_QUERIES['calls']
293+
294+
for node, capture_name in execute_query(self.language, query_str, root_node):
295+
if capture_name in ("name", "call"):
296+
# Ensure we are at the right node level
297+
target_node = node
298+
if capture_name == "call":
299+
# For call capture, find the name identifier
300+
# selector -> argument_part -> arguments
301+
# we want the name before this selector
302+
pass
303+
304+
# Deduplicate by start byte
305+
node_id = target_node.start_byte
306+
if node_id in seen_calls: continue
307+
seen_calls.add(node_id)
308+
309+
name = self._get_node_text(target_node)
310+
311+
# Find arguments
312+
args = []
313+
# Logic to find arguments node from selector or expression_statement
314+
315+
context, context_type, context_line = self._get_parent_context(target_node)
316+
317+
calls.append({
318+
"name": name,
319+
"full_name": name, # Simplified for now
320+
"line_number": target_node.start_point[0] + 1,
321+
"args": args,
322+
"context": (context, context_type, context_line),
323+
"lang": self.language_name,
324+
"is_dependency": False,
325+
})
326+
return calls
327+
328+
def _find_variables(self, root_node):
329+
variables = []
330+
query_str = DART_QUERIES['variables']
331+
for node, capture_name in execute_query(self.language, query_str, root_node):
332+
if capture_name == "name":
333+
name = self._get_node_text(node)
334+
context, _, _ = self._get_parent_context(node)
335+
336+
variables.append({
337+
"name": name,
338+
"line_number": node.start_point[0] + 1,
339+
"context": context,
340+
"lang": self.language_name,
341+
"is_dependency": False,
342+
})
343+
return variables
344+
345+
def pre_scan_dart(files: List[Path], parser_wrapper) -> Dict[str, List[str]]:
346+
"""Scans Dart files to create a map of class/function names to their file paths."""
347+
name_to_files = {}
348+
query_str = """
349+
[
350+
(class_definition name: (identifier) @name)
351+
(mixin_declaration name: (identifier) @name)
352+
(extension_declaration name: (identifier) @name)
353+
(function_signature name: (identifier) @name)
354+
]
355+
"""
356+
for path in files:
357+
try:
358+
with open(path, "r", encoding="utf-8", errors='ignore') as f:
359+
content = f.read()
360+
tree = parser_wrapper.parser.parse(bytes(content, "utf8"))
361+
for node, _ in execute_query(parser_wrapper.language, query_str, tree.root_node):
362+
name = node.text.decode('utf-8')
363+
if name not in name_to_files:
364+
name_to_files[name] = []
365+
name_to_files[name].append(str(path.resolve()))
366+
except Exception as e:
367+
warning_logger(f"Error pre-scanning Dart file {path}: {e}")
368+
return name_to_files

0 commit comments

Comments
 (0)