Skip to content

Commit f1b5a72

Browse files
Add Java language support
- Create java.py parser with tree-sitter queries for methods, classes, imports, and calls - Add Java file extension mapping and parser initialization to graph_builder.py - Support Java constructs: classes, interfaces, enums, annotations, methods, constructors - Include pre-scan functionality for import resolution - Successfully tested with cgc index, visualize, and delete commands
1 parent 539e39f commit f1b5a72

2 files changed

Lines changed: 312 additions & 1 deletion

File tree

src/codegraphcontext/tools/graph_builder.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ def __init__(self, language_name: str):
5656
elif self.language_name == 'c':
5757
from .languages.c import CTreeSitterParser
5858
self.language_specific_parser = CTreeSitterParser(self)
59+
elif self.language_name == 'java':
60+
from .languages.java import JavaTreeSitterParser
61+
self.language_specific_parser = JavaTreeSitterParser(self)
5962

6063

6164
def parse(self, file_path: Path, is_dependency: bool = False) -> Dict:
@@ -87,7 +90,8 @@ def __init__(self, db_manager: DatabaseManager, job_manager: JobManager, loop: a
8790
'.h': TreeSitterParser('cpp'),
8891
'.hpp': TreeSitterParser('cpp'),
8992
'.rs': TreeSitterParser('rust'),
90-
'.c': TreeSitterParser('c'), # Added C parser
93+
'.c': TreeSitterParser('c'),
94+
'.java': TreeSitterParser('java'),
9195

9296
}
9397
self.create_schema()
@@ -142,6 +146,9 @@ def _pre_scan_for_imports(self, files: list[Path]) -> dict:
142146
elif '.go' in files_by_lang:
143147
from .languages import go as go_lang_module
144148
imports_map.update(go_lang_module.pre_scan_go(files_by_lang['.go'], self.parsers['.go']))
149+
elif '.java' in files_by_lang:
150+
from .languages import java as java_lang_module
151+
imports_map.update(java_lang_module.pre_scan_java(files_by_lang['.java'], self.parsers['.java']))
145152

146153
return imports_map
147154

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
from pathlib import Path
2+
from typing import Any, Dict, Optional, Tuple
3+
import logging
4+
import re
5+
6+
logger = logging.getLogger(__name__)
7+
8+
JAVA_QUERIES = {
9+
"functions": """
10+
(method_declaration
11+
name: (identifier) @name
12+
parameters: (formal_parameters) @params
13+
) @function_node
14+
15+
(constructor_declaration
16+
name: (identifier) @name
17+
parameters: (formal_parameters) @params
18+
) @function_node
19+
""",
20+
"classes": """
21+
[
22+
(class_declaration name: (identifier) @name)
23+
(interface_declaration name: (identifier) @name)
24+
(enum_declaration name: (identifier) @name)
25+
(annotation_type_declaration name: (identifier) @name)
26+
] @class
27+
""",
28+
"imports": """
29+
(import_declaration) @import
30+
""",
31+
"calls": """
32+
(method_invocation
33+
name: (identifier) @name
34+
)
35+
36+
(object_creation_expression
37+
type: (type_identifier) @name
38+
)
39+
""",
40+
}
41+
42+
class JavaTreeSitterParser:
43+
def __init__(self, generic_parser_wrapper: Any):
44+
self.generic_parser_wrapper = generic_parser_wrapper
45+
self.language_name = "java"
46+
self.language = generic_parser_wrapper.language
47+
self.parser = generic_parser_wrapper.parser
48+
49+
self.queries = {
50+
name: self.language.query(query_str)
51+
for name, query_str in JAVA_QUERIES.items()
52+
}
53+
54+
def parse(self, file_path: Path, is_dependency: bool = False) -> Dict[str, Any]:
55+
try:
56+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
57+
source_code = f.read()
58+
59+
if not source_code.strip():
60+
logger.warning(f"Empty or whitespace-only file: {file_path}")
61+
return {
62+
"file_path": str(file_path),
63+
"functions": [],
64+
"classes": [],
65+
"variables": [],
66+
"imports": [],
67+
"function_calls": [],
68+
"is_dependency": is_dependency,
69+
"lang": self.language_name,
70+
}
71+
72+
tree = self.parser.parse(bytes(source_code, "utf8"))
73+
74+
parsed_functions = []
75+
parsed_classes = []
76+
parsed_imports = []
77+
parsed_calls = []
78+
79+
for capture_name, query in self.queries.items():
80+
captures = query.captures(tree.root_node)
81+
82+
if capture_name == "functions":
83+
parsed_functions = self._parse_functions(captures, source_code, file_path)
84+
elif capture_name == "classes":
85+
parsed_classes = self._parse_classes(captures, source_code, file_path)
86+
elif capture_name == "imports":
87+
parsed_imports = self._parse_imports(captures, source_code)
88+
elif capture_name == "calls":
89+
parsed_calls = self._parse_calls(captures, source_code)
90+
91+
return {
92+
"file_path": str(file_path),
93+
"functions": parsed_functions,
94+
"classes": parsed_classes,
95+
"variables": [],
96+
"imports": parsed_imports,
97+
"function_calls": parsed_calls,
98+
"is_dependency": is_dependency,
99+
"lang": self.language_name,
100+
}
101+
102+
except Exception as e:
103+
logger.error(f"Error parsing Java file {file_path}: {e}")
104+
return {
105+
"file_path": str(file_path),
106+
"functions": [],
107+
"classes": [],
108+
"variables": [],
109+
"imports": [],
110+
"function_calls": [],
111+
"is_dependency": is_dependency,
112+
"lang": self.language_name,
113+
}
114+
115+
def _parse_functions(self, captures: list, source_code: str, file_path: Path) -> list[Dict[str, Any]]:
116+
functions = []
117+
source_lines = source_code.splitlines()
118+
119+
for node, capture_name in captures:
120+
if capture_name == "function_node":
121+
try:
122+
start_line = node.start_point[0] + 1
123+
end_line = node.end_point[0] + 1
124+
125+
name_captures = [
126+
(n, cn) for n, cn in captures
127+
if cn == "name" and n.parent == node
128+
]
129+
130+
if name_captures:
131+
name_node = name_captures[0][0]
132+
func_name = source_code[name_node.start_byte:name_node.end_byte]
133+
134+
params_captures = [
135+
(n, cn) for n, cn in captures
136+
if cn == "params" and n.parent == node
137+
]
138+
139+
parameters = []
140+
if params_captures:
141+
params_node = params_captures[0][0]
142+
params_text = source_code[params_node.start_byte:params_node.end_byte]
143+
parameters = self._extract_parameter_names(params_text)
144+
145+
source_text = source_code[node.start_byte:node.end_byte]
146+
147+
functions.append({
148+
"name": func_name,
149+
"parameters": parameters,
150+
"line_number": start_line,
151+
"end_line": end_line,
152+
"source": source_text,
153+
"file_path": str(file_path),
154+
"lang": self.language_name,
155+
})
156+
157+
except Exception as e:
158+
logger.error(f"Error parsing function in {file_path}: {e}")
159+
continue
160+
161+
return functions
162+
163+
def _parse_classes(self, captures: list, source_code: str, file_path: Path) -> list[Dict[str, Any]]:
164+
classes = []
165+
166+
for node, capture_name in captures:
167+
if capture_name == "class":
168+
try:
169+
start_line = node.start_point[0] + 1
170+
end_line = node.end_point[0] + 1
171+
172+
name_captures = [
173+
(n, cn) for n, cn in captures
174+
if cn == "name" and n.parent == node
175+
]
176+
177+
if name_captures:
178+
name_node = name_captures[0][0]
179+
class_name = source_code[name_node.start_byte:name_node.end_byte]
180+
181+
source_text = source_code[node.start_byte:node.end_byte]
182+
183+
classes.append({
184+
"name": class_name,
185+
"line_number": start_line,
186+
"end_line": end_line,
187+
"source": source_text,
188+
"file_path": str(file_path),
189+
"lang": self.language_name,
190+
})
191+
192+
except Exception as e:
193+
logger.error(f"Error parsing class in {file_path}: {e}")
194+
continue
195+
196+
return classes
197+
198+
def _parse_imports(self, captures: list, source_code: str) -> list[dict]:
199+
imports = []
200+
201+
for node, capture_name in captures:
202+
if capture_name == "import":
203+
try:
204+
import_text = source_code[node.start_byte:node.end_byte]
205+
import_match = re.search(r'import\s+(?:static\s+)?([^;]+)', import_text)
206+
if import_match:
207+
import_path = import_match.group(1).strip()
208+
209+
import_data = {
210+
"name": import_path,
211+
"full_import_name": import_path,
212+
"line_number": node.start_point[0] + 1,
213+
"alias": None,
214+
"context": (None, None),
215+
"lang": self.language_name,
216+
"is_dependency": False,
217+
}
218+
imports.append(import_data)
219+
except Exception as e:
220+
logger.error(f"Error parsing import: {e}")
221+
continue
222+
223+
return imports
224+
225+
def _parse_calls(self, captures: list, source_code: str) -> list[dict]:
226+
calls = []
227+
seen_calls = set()
228+
229+
for node, capture_name in captures:
230+
if capture_name == "name":
231+
try:
232+
call_name = source_code[node.start_byte:node.end_byte]
233+
line_number = node.start_point[0] + 1
234+
235+
# Avoid duplicates
236+
call_key = f"{call_name}_{line_number}"
237+
if call_key in seen_calls:
238+
continue
239+
seen_calls.add(call_key)
240+
241+
call_data = {
242+
"name": call_name,
243+
"full_name": call_name,
244+
"line_number": line_number,
245+
"args": [],
246+
"inferred_obj_type": None,
247+
"context": (None, None),
248+
"class_context": (None, None),
249+
"lang": self.language_name,
250+
"is_dependency": False,
251+
}
252+
calls.append(call_data)
253+
except Exception as e:
254+
logger.error(f"Error parsing call: {e}")
255+
continue
256+
257+
return calls
258+
259+
def _extract_parameter_names(self, params_text: str) -> list[str]:
260+
params = []
261+
if not params_text or params_text.strip() == "()":
262+
return params
263+
264+
params_content = params_text.strip("()")
265+
if not params_content:
266+
return params
267+
268+
for param in params_content.split(","):
269+
param = param.strip()
270+
if param:
271+
parts = param.split()
272+
if len(parts) >= 2:
273+
param_name = parts[-1]
274+
params.append(param_name)
275+
276+
return params
277+
278+
279+
def pre_scan_java(files: list[Path], parser_wrapper) -> dict:
280+
name_to_files = {}
281+
282+
for file_path in files:
283+
try:
284+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
285+
content = f.read()
286+
287+
class_matches = re.finditer(r'\b(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:abstract\s+)?(?:final\s+)?class\s+(\w+)', content)
288+
for match in class_matches:
289+
class_name = match.group(1)
290+
if class_name not in name_to_files:
291+
name_to_files[class_name] = []
292+
name_to_files[class_name].append(str(file_path))
293+
294+
interface_matches = re.finditer(r'\b(?:public\s+|private\s+|protected\s+)?interface\s+(\w+)', content)
295+
for match in interface_matches:
296+
interface_name = match.group(1)
297+
if interface_name not in name_to_files:
298+
name_to_files[interface_name] = []
299+
name_to_files[interface_name].append(str(file_path))
300+
301+
except Exception as e:
302+
logger.error(f"Error pre-scanning Java file {file_path}: {e}")
303+
304+
return name_to_files

0 commit comments

Comments
 (0)