graph-dependency-analyzer 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- graph_dependency_analyzer-0.2.0.dist-info/METADATA +158 -0
- graph_dependency_analyzer-0.2.0.dist-info/RECORD +86 -0
- graph_dependency_analyzer-0.2.0.dist-info/WHEEL +5 -0
- graph_dependency_analyzer-0.2.0.dist-info/entry_points.txt +2 -0
- graph_dependency_analyzer-0.2.0.dist-info/top_level.txt +1 -0
- src/__init__.py +0 -0
- src/application/__init__.py +0 -0
- src/application/dtos/__init__.py +0 -0
- src/application/dtos/affected_component.py +59 -0
- src/application/dtos/batch_processing_result.py +17 -0
- src/application/dtos/impact_analysis_response.py +56 -0
- src/application/dtos/impact_query_request.py +38 -0
- src/application/dtos/index_batch_request.py +53 -0
- src/application/dtos/index_batch_response.py +72 -0
- src/application/dtos/index_result.py +20 -0
- src/application/dtos/repository_index_result.py +79 -0
- src/application/exceptions.py +41 -0
- src/application/services/__init__.py +0 -0
- src/application/services/file_scanner_service.py +258 -0
- src/application/services/progress_tracker.py +132 -0
- src/application/use_cases/__init__.py +0 -0
- src/application/use_cases/clear_all_data_use_case.py +180 -0
- src/application/use_cases/index_batch_use_case.py +153 -0
- src/application/use_cases/index_repository_use_case.py +683 -0
- src/application/use_cases/indexing_orchestrator.py +181 -0
- src/application/use_cases/list_repositories_use_case.py +84 -0
- src/cli.py +512 -0
- src/config/__init__.py +5 -0
- src/config/container.py +211 -0
- src/config/logging_config.py +123 -0
- src/config/settings.py +165 -0
- src/domain/__init__.py +0 -0
- src/domain/entities/__init__.py +20 -0
- src/domain/entities/ast_node.py +32 -0
- src/domain/entities/code_chunk.py +45 -0
- src/domain/entities/code_file.py +44 -0
- src/domain/entities/dependency_graph.py +110 -0
- src/domain/entities/repository.py +46 -0
- src/domain/exceptions.py +36 -0
- src/domain/repositories/__init__.py +22 -0
- src/domain/repositories/i_code_parser.py +50 -0
- src/domain/repositories/i_dependency_extractor.py +35 -0
- src/domain/repositories/i_embedding_provider.py +31 -0
- src/domain/repositories/i_graph_repository.py +78 -0
- src/domain/repositories/i_vector_repository.py +55 -0
- src/domain/services/__init__.py +8 -0
- src/domain/services/dependency_extractor.py +962 -0
- src/domain/services/semantic_chunk_builder.py +719 -0
- src/domain/value_objects/__init__.py +12 -0
- src/domain/value_objects/chunk_type.py +24 -0
- src/domain/value_objects/dependency_type.py +22 -0
- src/domain/value_objects/node_type.py +22 -0
- src/domain/value_objects/qualified_name.py +78 -0
- src/infrastructure/__init__.py +0 -0
- src/infrastructure/exceptions.py +41 -0
- src/infrastructure/external_apis/__init__.py +0 -0
- src/infrastructure/external_apis/ollama_embedding_provider.py +134 -0
- src/infrastructure/external_apis/openai_embedding_provider.py +231 -0
- src/infrastructure/parsing/__init__.py +20 -0
- src/infrastructure/parsing/config_file_parser.py +347 -0
- src/infrastructure/parsing/gradle_dependency_parser.py +159 -0
- src/infrastructure/parsing/groovy_parser.py +198 -0
- src/infrastructure/parsing/maven_dependency_parser.py +147 -0
- src/infrastructure/parsing/microfrontend_config_parser.py +242 -0
- src/infrastructure/parsing/npm_package_dependency_parser.py +255 -0
- src/infrastructure/parsing/python_parser.py +211 -0
- src/infrastructure/parsing/sql_schema_parser.py +658 -0
- src/infrastructure/parsing/tree_sitter_java_parser.py +434 -0
- src/infrastructure/parsing/tree_sitter_typescript_parser.py +600 -0
- src/infrastructure/persistence/__init__.py +0 -0
- src/infrastructure/persistence/graph_export_repository.py +443 -0
- src/infrastructure/persistence/neo4j_dependency_repository.py +657 -0
- src/infrastructure/persistence/qdrant_vector_repository.py +357 -0
- src/infrastructure/persistence/repository_relationship_repository.py +1312 -0
- src/presentation/__init__.py +0 -0
- src/presentation/api/__init__.py +0 -0
- src/presentation/api/rest/__init__.py +0 -0
- src/presentation/api/rest/exception_handlers.py +174 -0
- src/presentation/api/rest/main.py +80 -0
- src/presentation/api/rest/routers/__init__.py +5 -0
- src/presentation/api/rest/routers/admin.py +103 -0
- src/presentation/api/rest/routers/analysis.py +980 -0
- src/presentation/api/rest/routers/export.py +442 -0
- src/presentation/api/rest/routers/health.py +117 -0
- src/presentation/api/rest/routers/indexing.py +148 -0
- src/presentation/api/rest/routers/relationships.py +1089 -0
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
"""SemanticChunkBuilder domain service - creates semantic code chunks."""
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from src.domain.entities.ast_node import ASTNode
|
|
6
|
+
from src.domain.entities.code_chunk import CodeChunk
|
|
7
|
+
from src.domain.value_objects.chunk_type import ChunkType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SemanticChunkBuilder:
|
|
11
|
+
"""
|
|
12
|
+
Domain service for creating semantic code chunks from AST.
|
|
13
|
+
|
|
14
|
+
Creates method-based chunks with context enrichment (javadoc, imports, class signature).
|
|
15
|
+
Groups small methods (<20 lines) together (up to 3 methods).
|
|
16
|
+
Keeps large methods (>500 lines) as single chunks.
|
|
17
|
+
|
|
18
|
+
Extensions for frontend (Tasks 5.1-5.2):
|
|
19
|
+
- TypeScript/JavaScript functions with JSDoc
|
|
20
|
+
- React/Angular/Vue components
|
|
21
|
+
- Hooks detection (use* functions)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
JAVADOC_PATTERN = re.compile(r'/\*\*.*?\*/', re.DOTALL)
|
|
25
|
+
JSDOC_PATTERN = re.compile(r'/\*\*.*?\*/', re.DOTALL) # Same as JavaDoc
|
|
26
|
+
SMALL_METHOD_THRESHOLD = 20 # lines
|
|
27
|
+
LARGE_METHOD_THRESHOLD = 500 # lines
|
|
28
|
+
MAX_METHODS_PER_CHUNK = 3
|
|
29
|
+
|
|
30
|
+
def build_chunks(
|
|
31
|
+
self,
|
|
32
|
+
ast_nodes: list[ASTNode],
|
|
33
|
+
source_code: dict[str, str]
|
|
34
|
+
) -> list[CodeChunk]:
|
|
35
|
+
"""
|
|
36
|
+
Build semantic chunks from AST nodes and source code.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
ast_nodes: Parsed AST nodes
|
|
40
|
+
source_code: Source code by file path
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
List of CodeChunk with metadata
|
|
44
|
+
"""
|
|
45
|
+
chunks = []
|
|
46
|
+
|
|
47
|
+
# Separate nodes by type
|
|
48
|
+
method_nodes = [n for n in ast_nodes if n.node_type in ("METHOD", "CONSTRUCTOR")]
|
|
49
|
+
class_nodes = [n for n in ast_nodes if n.node_type == "CLASS"]
|
|
50
|
+
import_nodes = [n for n in ast_nodes if n.node_type == "IMPORT"]
|
|
51
|
+
|
|
52
|
+
# Build chunks from methods
|
|
53
|
+
chunks.extend(self._build_method_chunks(method_nodes, source_code))
|
|
54
|
+
|
|
55
|
+
# Build context chunks if no methods
|
|
56
|
+
if not method_nodes and (class_nodes or import_nodes):
|
|
57
|
+
chunks.extend(self._build_context_chunks(class_nodes, import_nodes, source_code))
|
|
58
|
+
|
|
59
|
+
return chunks
|
|
60
|
+
|
|
61
|
+
def _build_method_chunks(
|
|
62
|
+
self,
|
|
63
|
+
method_nodes: list[ASTNode],
|
|
64
|
+
source_code: dict[str, str]
|
|
65
|
+
) -> list[CodeChunk]:
|
|
66
|
+
"""Build chunks from method nodes."""
|
|
67
|
+
chunks = []
|
|
68
|
+
|
|
69
|
+
# Group small methods together
|
|
70
|
+
small_methods = []
|
|
71
|
+
large_methods = []
|
|
72
|
+
|
|
73
|
+
for node in method_nodes:
|
|
74
|
+
method_size = node.line_end - node.line_start + 1
|
|
75
|
+
|
|
76
|
+
if method_size < self.SMALL_METHOD_THRESHOLD:
|
|
77
|
+
small_methods.append(node)
|
|
78
|
+
else:
|
|
79
|
+
large_methods.append(node)
|
|
80
|
+
|
|
81
|
+
# Process small methods (group up to 3)
|
|
82
|
+
chunks.extend(self._group_small_methods(small_methods, source_code))
|
|
83
|
+
|
|
84
|
+
# Process large methods (one per chunk)
|
|
85
|
+
for node in large_methods:
|
|
86
|
+
chunk = self._create_chunk_from_method(node, source_code)
|
|
87
|
+
if chunk:
|
|
88
|
+
chunks.append(chunk)
|
|
89
|
+
|
|
90
|
+
return chunks
|
|
91
|
+
|
|
92
|
+
def _group_small_methods(
|
|
93
|
+
self,
|
|
94
|
+
small_methods: list[ASTNode],
|
|
95
|
+
source_code: dict[str, str]
|
|
96
|
+
) -> list[CodeChunk]:
|
|
97
|
+
"""Group small methods together (up to 3 per chunk)."""
|
|
98
|
+
chunks = []
|
|
99
|
+
i = 0
|
|
100
|
+
|
|
101
|
+
while i < len(small_methods):
|
|
102
|
+
# Take up to 3 methods
|
|
103
|
+
group = small_methods[i:i + self.MAX_METHODS_PER_CHUNK]
|
|
104
|
+
|
|
105
|
+
if len(group) == 1:
|
|
106
|
+
# Single method
|
|
107
|
+
chunk = self._create_chunk_from_method(group[0], source_code)
|
|
108
|
+
if chunk:
|
|
109
|
+
chunks.append(chunk)
|
|
110
|
+
else:
|
|
111
|
+
# Multiple methods grouped
|
|
112
|
+
chunk = self._create_grouped_chunk(group, source_code)
|
|
113
|
+
if chunk:
|
|
114
|
+
chunks.append(chunk)
|
|
115
|
+
|
|
116
|
+
i += self.MAX_METHODS_PER_CHUNK
|
|
117
|
+
|
|
118
|
+
return chunks
|
|
119
|
+
|
|
120
|
+
def _create_chunk_from_method(
|
|
121
|
+
self,
|
|
122
|
+
node: ASTNode,
|
|
123
|
+
source_code: dict[str, str]
|
|
124
|
+
) -> CodeChunk | None:
|
|
125
|
+
"""Create a chunk from a single method."""
|
|
126
|
+
# Find source file
|
|
127
|
+
source_file = self._find_source_file(node, source_code)
|
|
128
|
+
if not source_file:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
content = source_code[source_file]
|
|
132
|
+
lines = content.split('\n')
|
|
133
|
+
|
|
134
|
+
# Extract method content (with bounds checking)
|
|
135
|
+
# Line numbers in AST are 1-indexed, but list is 0-indexed
|
|
136
|
+
start_idx = max(0, node.line_start - 1)
|
|
137
|
+
end_idx = min(len(lines), node.line_end)
|
|
138
|
+
|
|
139
|
+
method_content = '\n'.join(lines[start_idx:end_idx])
|
|
140
|
+
|
|
141
|
+
# If method content doesn't contain method name, try expanding range
|
|
142
|
+
if node.name not in method_content and start_idx > 0:
|
|
143
|
+
start_idx = max(0, start_idx - 5)
|
|
144
|
+
method_content = '\n'.join(lines[start_idx:end_idx])
|
|
145
|
+
|
|
146
|
+
# Try to include javadoc (look backwards from method start)
|
|
147
|
+
javadoc_start = max(0, start_idx - 10)
|
|
148
|
+
preceding_lines = '\n'.join(lines[javadoc_start:start_idx])
|
|
149
|
+
javadoc_match = self.JAVADOC_PATTERN.search(preceding_lines)
|
|
150
|
+
if javadoc_match:
|
|
151
|
+
method_content = javadoc_match.group() + '\n' + method_content
|
|
152
|
+
|
|
153
|
+
# Determine chunk type
|
|
154
|
+
chunk_type = ChunkType.CONSTRUCTOR.value if node.node_type == "CONSTRUCTOR" else ChunkType.METHOD.value
|
|
155
|
+
|
|
156
|
+
# Build metadata
|
|
157
|
+
metadata = {
|
|
158
|
+
"name": node.name,
|
|
159
|
+
"qualified_name": node.qualified_name,
|
|
160
|
+
"parameters": node.metadata.get("parameters", []),
|
|
161
|
+
"return_type": node.metadata.get("return_type"),
|
|
162
|
+
"parent_class": node.metadata.get("parent_class"),
|
|
163
|
+
"line_start": node.line_start,
|
|
164
|
+
"line_end": node.line_end
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return CodeChunk(
|
|
168
|
+
content=method_content,
|
|
169
|
+
chunk_type=chunk_type,
|
|
170
|
+
metadata=metadata
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def _create_grouped_chunk(
|
|
174
|
+
self,
|
|
175
|
+
nodes: list[ASTNode],
|
|
176
|
+
source_code: dict[str, str]
|
|
177
|
+
) -> CodeChunk | None:
|
|
178
|
+
"""Create a chunk from multiple grouped methods."""
|
|
179
|
+
if not nodes:
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
# Find source file from first node
|
|
183
|
+
source_file = self._find_source_file(nodes[0], source_code)
|
|
184
|
+
if not source_file:
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
content = source_code[source_file]
|
|
188
|
+
lines = content.split('\n')
|
|
189
|
+
|
|
190
|
+
# Extract all methods
|
|
191
|
+
all_content = []
|
|
192
|
+
for node in nodes:
|
|
193
|
+
start_idx = max(0, node.line_start - 1)
|
|
194
|
+
end_idx = min(len(lines), node.line_end)
|
|
195
|
+
method_content = '\n'.join(lines[start_idx:end_idx])
|
|
196
|
+
all_content.append(method_content)
|
|
197
|
+
|
|
198
|
+
grouped_content = '\n\n'.join(all_content)
|
|
199
|
+
|
|
200
|
+
# Use first node's metadata as base
|
|
201
|
+
first_node = nodes[0]
|
|
202
|
+
metadata = {
|
|
203
|
+
"name": f"grouped_{len(nodes)}_methods",
|
|
204
|
+
"qualified_name": first_node.metadata.get("parent_class", "unknown"),
|
|
205
|
+
"parent_class": first_node.metadata.get("parent_class"),
|
|
206
|
+
"line_start": nodes[0].line_start,
|
|
207
|
+
"line_end": nodes[-1].line_end,
|
|
208
|
+
"method_count": len(nodes)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return CodeChunk(
|
|
212
|
+
content=grouped_content,
|
|
213
|
+
chunk_type=ChunkType.METHOD.value,
|
|
214
|
+
metadata=metadata
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def _build_context_chunks(
|
|
218
|
+
self,
|
|
219
|
+
class_nodes: list[ASTNode],
|
|
220
|
+
import_nodes: list[ASTNode],
|
|
221
|
+
source_code: dict[str, str]
|
|
222
|
+
) -> list[CodeChunk]:
|
|
223
|
+
"""Build context chunks for files with only imports and class declaration."""
|
|
224
|
+
chunks = []
|
|
225
|
+
|
|
226
|
+
# If we have class nodes but no methods, create CLASS chunk
|
|
227
|
+
for class_node in class_nodes:
|
|
228
|
+
source_file = self._find_source_file(class_node, source_code)
|
|
229
|
+
if not source_file:
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
content = source_code[source_file]
|
|
233
|
+
lines = content.split('\n')
|
|
234
|
+
|
|
235
|
+
# Extract class declaration and imports
|
|
236
|
+
start_idx = max(0, class_node.line_start - 1)
|
|
237
|
+
end_idx = min(len(lines), class_node.line_end)
|
|
238
|
+
|
|
239
|
+
class_content = '\n'.join(lines[start_idx:end_idx])
|
|
240
|
+
|
|
241
|
+
# Include imports
|
|
242
|
+
import_content = ""
|
|
243
|
+
for imp_node in import_nodes:
|
|
244
|
+
imp_start = max(0, imp_node.line_start - 1)
|
|
245
|
+
imp_end = min(len(lines), imp_node.line_end)
|
|
246
|
+
import_content += '\n'.join(lines[imp_start:imp_end]) + '\n'
|
|
247
|
+
|
|
248
|
+
full_content = import_content + '\n' + class_content
|
|
249
|
+
|
|
250
|
+
metadata = {
|
|
251
|
+
"name": class_node.name,
|
|
252
|
+
"qualified_name": class_node.qualified_name,
|
|
253
|
+
"line_start": class_node.line_start,
|
|
254
|
+
"line_end": class_node.line_end
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
chunk = CodeChunk(
|
|
258
|
+
content=full_content,
|
|
259
|
+
chunk_type=ChunkType.CLASS.value,
|
|
260
|
+
metadata=metadata
|
|
261
|
+
)
|
|
262
|
+
chunks.append(chunk)
|
|
263
|
+
|
|
264
|
+
# If only imports, create IMPORT_BLOCK chunk
|
|
265
|
+
if not class_nodes and import_nodes:
|
|
266
|
+
source_file = self._find_source_file(import_nodes[0], source_code)
|
|
267
|
+
if source_file:
|
|
268
|
+
content = source_code[source_file]
|
|
269
|
+
lines = content.split('\n')
|
|
270
|
+
|
|
271
|
+
import_lines = []
|
|
272
|
+
for imp_node in import_nodes:
|
|
273
|
+
start_idx = max(0, imp_node.line_start - 1)
|
|
274
|
+
end_idx = min(len(lines), imp_node.line_end)
|
|
275
|
+
import_lines.extend(lines[start_idx:end_idx])
|
|
276
|
+
|
|
277
|
+
chunk = CodeChunk(
|
|
278
|
+
content='\n'.join(import_lines),
|
|
279
|
+
chunk_type=ChunkType.IMPORT_BLOCK.value,
|
|
280
|
+
metadata={"import_count": len(import_nodes)}
|
|
281
|
+
)
|
|
282
|
+
chunks.append(chunk)
|
|
283
|
+
|
|
284
|
+
return chunks
|
|
285
|
+
|
|
286
|
+
def _find_source_file(self, node: ASTNode, source_code: dict[str, str]) -> str | None:
|
|
287
|
+
"""Find the source file for a given node."""
|
|
288
|
+
# Try to match by qualified name
|
|
289
|
+
qualified_parts = node.qualified_name.split('.')
|
|
290
|
+
|
|
291
|
+
for filename in source_code.keys():
|
|
292
|
+
# Simple heuristic: match class name in filename
|
|
293
|
+
if any(part in filename for part in qualified_parts):
|
|
294
|
+
return filename
|
|
295
|
+
|
|
296
|
+
# Fallback: return first available file
|
|
297
|
+
return next(iter(source_code.keys()), None)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def build_typescript_chunks(
|
|
301
|
+
self,
|
|
302
|
+
parse_results: list,
|
|
303
|
+
source_code: dict[str, str]
|
|
304
|
+
) -> list[CodeChunk]:
|
|
305
|
+
"""
|
|
306
|
+
Build semantic chunks from TypeScript/JavaScript parse results (Tasks 5.1-5.2).
|
|
307
|
+
|
|
308
|
+
Uses Tree-sitter parsed data to extract REAL code content.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
parse_results: List of ParseResult from TreeSitterTypeScriptParser
|
|
312
|
+
source_code: Source code by file path {file_path: code_string}
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
List of CodeChunk with actual TypeScript/JavaScript code
|
|
316
|
+
"""
|
|
317
|
+
chunks = []
|
|
318
|
+
|
|
319
|
+
for parse_result in parse_results:
|
|
320
|
+
file_path = str(parse_result.file_path)
|
|
321
|
+
code = source_code.get(file_path, "")
|
|
322
|
+
|
|
323
|
+
if not code:
|
|
324
|
+
# Try reading from file if not in dict
|
|
325
|
+
try:
|
|
326
|
+
from pathlib import Path
|
|
327
|
+
code = Path(file_path).read_text(encoding='utf-8')
|
|
328
|
+
except Exception:
|
|
329
|
+
continue
|
|
330
|
+
|
|
331
|
+
code_lines = code.split('\n')
|
|
332
|
+
|
|
333
|
+
# Build chunks from methods/components (extracted by tree-sitter)
|
|
334
|
+
for method in parse_result.methods:
|
|
335
|
+
chunk = self._create_typescript_chunk(method, code_lines, file_path)
|
|
336
|
+
if chunk:
|
|
337
|
+
chunks.append(chunk)
|
|
338
|
+
|
|
339
|
+
return chunks
|
|
340
|
+
|
|
341
|
+
def _create_typescript_chunk(
|
|
342
|
+
self,
|
|
343
|
+
method: dict,
|
|
344
|
+
code_lines: list[str],
|
|
345
|
+
file_path: str
|
|
346
|
+
) -> CodeChunk | None:
|
|
347
|
+
"""
|
|
348
|
+
Create a single chunk from a TypeScript method/component.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
method: Method metadata from tree-sitter (name, line, type, framework, etc)
|
|
352
|
+
code_lines: Source code split by lines
|
|
353
|
+
file_path: Path to source file
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
CodeChunk with real code content
|
|
357
|
+
"""
|
|
358
|
+
name = method.get('name', 'anonymous')
|
|
359
|
+
start_line = method.get('line', 1) - 1 # Convert to 0-indexed
|
|
360
|
+
|
|
361
|
+
# Estimate end line (in production, tree-sitter should provide this)
|
|
362
|
+
# For now, extract ~30 lines or until next function/class
|
|
363
|
+
end_line = self._find_function_end(code_lines, start_line)
|
|
364
|
+
|
|
365
|
+
# Extract JSDoc comment if present (look backwards up to 10 lines)
|
|
366
|
+
comment_start = max(0, start_line - 10)
|
|
367
|
+
has_jsdoc = False
|
|
368
|
+
jsdoc_lines = []
|
|
369
|
+
|
|
370
|
+
for i in range(start_line - 1, comment_start - 1, -1):
|
|
371
|
+
if i < 0 or i >= len(code_lines):
|
|
372
|
+
break
|
|
373
|
+
line = code_lines[i].strip()
|
|
374
|
+
if '*/' in line:
|
|
375
|
+
has_jsdoc = True
|
|
376
|
+
jsdoc_lines.insert(0, code_lines[i])
|
|
377
|
+
elif has_jsdoc and ('/**' in line or '*' in line):
|
|
378
|
+
jsdoc_lines.insert(0, code_lines[i])
|
|
379
|
+
if '/**' in line:
|
|
380
|
+
break
|
|
381
|
+
elif has_jsdoc:
|
|
382
|
+
break
|
|
383
|
+
|
|
384
|
+
# Extract actual code content using tree-sitter line info
|
|
385
|
+
if has_jsdoc:
|
|
386
|
+
content_lines = jsdoc_lines + code_lines[start_line:end_line]
|
|
387
|
+
else:
|
|
388
|
+
content_lines = code_lines[start_line:end_line]
|
|
389
|
+
|
|
390
|
+
content = '\n'.join(content_lines)
|
|
391
|
+
|
|
392
|
+
if not content.strip():
|
|
393
|
+
return None
|
|
394
|
+
|
|
395
|
+
# Detect chunk type based on tree-sitter metadata
|
|
396
|
+
chunk_type = self._detect_typescript_chunk_type(method)
|
|
397
|
+
|
|
398
|
+
# Build rich metadata
|
|
399
|
+
metadata = {
|
|
400
|
+
'language': 'typescript',
|
|
401
|
+
'file_path': file_path,
|
|
402
|
+
'function_name': name,
|
|
403
|
+
'start_line': start_line + 1,
|
|
404
|
+
'end_line': end_line,
|
|
405
|
+
'has_jsdoc': has_jsdoc
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
# Add framework-specific metadata
|
|
409
|
+
if method.get('framework'):
|
|
410
|
+
metadata['framework'] = method['framework']
|
|
411
|
+
|
|
412
|
+
if method.get('type'):
|
|
413
|
+
metadata['component_type'] = method['type']
|
|
414
|
+
|
|
415
|
+
# React hooks detection (Task 5.1)
|
|
416
|
+
if name.startswith('use') and len(name) > 3 and name[3].isupper():
|
|
417
|
+
metadata['is_hook'] = True
|
|
418
|
+
|
|
419
|
+
# JSX children for components (Task 5.2)
|
|
420
|
+
if 'jsx_children' in method:
|
|
421
|
+
metadata['jsx_children'] = method['jsx_children']
|
|
422
|
+
|
|
423
|
+
chunk = CodeChunk(
|
|
424
|
+
content=content,
|
|
425
|
+
chunk_type=chunk_type,
|
|
426
|
+
metadata=metadata
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
return chunk
|
|
430
|
+
|
|
431
|
+
def _find_function_end(self, code_lines: list[str], start_line: int) -> int:
|
|
432
|
+
"""
|
|
433
|
+
Find the end line of a function (heuristic-based).
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
code_lines: Source code lines
|
|
437
|
+
start_line: Starting line (0-indexed)
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
End line number (0-indexed)
|
|
441
|
+
"""
|
|
442
|
+
brace_count = 0
|
|
443
|
+
in_function = False
|
|
444
|
+
|
|
445
|
+
for i in range(start_line, min(len(code_lines), start_line + 100)):
|
|
446
|
+
line = code_lines[i]
|
|
447
|
+
|
|
448
|
+
# Count braces to find function end
|
|
449
|
+
if '{' in line:
|
|
450
|
+
brace_count += line.count('{')
|
|
451
|
+
in_function = True
|
|
452
|
+
if '}' in line:
|
|
453
|
+
brace_count -= line.count('}')
|
|
454
|
+
|
|
455
|
+
# Arrow functions: const X = () => ...
|
|
456
|
+
if '=>' in line and not in_function:
|
|
457
|
+
in_function = True
|
|
458
|
+
if '{' not in line:
|
|
459
|
+
# Single-line arrow function
|
|
460
|
+
return i + 1
|
|
461
|
+
|
|
462
|
+
# Function ended when braces balanced
|
|
463
|
+
if in_function and brace_count == 0:
|
|
464
|
+
return i + 1
|
|
465
|
+
|
|
466
|
+
# Stop at next function/class declaration
|
|
467
|
+
if i > start_line and any(keyword in line for keyword in ['function ', 'class ', 'const ', 'export ']):
|
|
468
|
+
stripped = line.strip()
|
|
469
|
+
if stripped.startswith(('function', 'class', 'export function', 'export class', 'const')):
|
|
470
|
+
return i
|
|
471
|
+
|
|
472
|
+
# Default: take 30 lines
|
|
473
|
+
return min(len(code_lines), start_line + 30)
|
|
474
|
+
|
|
475
|
+
def _detect_typescript_chunk_type(self, method: dict) -> str:
|
|
476
|
+
"""
|
|
477
|
+
Detect chunk type from TypeScript method metadata (Tasks 5.1-5.2).
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
method: Method dictionary from TreeSitterTypeScriptParser
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
ChunkType enum value
|
|
484
|
+
"""
|
|
485
|
+
method_type = method.get('type', '')
|
|
486
|
+
name = method.get('name', '')
|
|
487
|
+
|
|
488
|
+
# React/Angular/Vue components (Task 5.2)
|
|
489
|
+
if method_type in ('function_component', 'class_component', 'angular_component'):
|
|
490
|
+
return ChunkType.CLASS.value # Use CLASS for components
|
|
491
|
+
|
|
492
|
+
# React hooks (Task 5.1)
|
|
493
|
+
if name.startswith('use') and len(name) > 3 and name[3].isupper():
|
|
494
|
+
return ChunkType.METHOD.value # Use METHOD for hooks
|
|
495
|
+
|
|
496
|
+
# Regular functions
|
|
497
|
+
return ChunkType.METHOD.value
|
|
498
|
+
|
|
499
|
+
def _build_function_chunks(
|
|
500
|
+
self,
|
|
501
|
+
function_nodes: list[ASTNode],
|
|
502
|
+
source_code: dict[str, str]
|
|
503
|
+
) -> list[CodeChunk]:
|
|
504
|
+
"""
|
|
505
|
+
Build chunks from TypeScript/JavaScript functions (Task 15.1).
|
|
506
|
+
|
|
507
|
+
Applies same grouping strategy as Java methods:
|
|
508
|
+
- Small functions (<20 lines): group up to 3 per chunk
|
|
509
|
+
- Large functions (≥20 lines): one per chunk
|
|
510
|
+
- React hooks (name starts with 'use'): tagged as HOOK
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
function_nodes: List of FUNCTION nodes from AST
|
|
514
|
+
source_code: Source code by file path
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
List of CodeChunk instances
|
|
518
|
+
"""
|
|
519
|
+
chunks = []
|
|
520
|
+
|
|
521
|
+
# Group by size
|
|
522
|
+
small_functions = []
|
|
523
|
+
large_functions = []
|
|
524
|
+
|
|
525
|
+
for node in function_nodes:
|
|
526
|
+
function_size = node.line_end - node.line_start + 1
|
|
527
|
+
|
|
528
|
+
if function_size < self.SMALL_METHOD_THRESHOLD:
|
|
529
|
+
small_functions.append(node)
|
|
530
|
+
else:
|
|
531
|
+
large_functions.append(node)
|
|
532
|
+
|
|
533
|
+
# Process small functions (group up to 3)
|
|
534
|
+
i = 0
|
|
535
|
+
while i < len(small_functions):
|
|
536
|
+
group = small_functions[i:i + self.MAX_METHODS_PER_CHUNK]
|
|
537
|
+
|
|
538
|
+
if len(group) == 1:
|
|
539
|
+
chunk = self._create_function_chunk(group[0], source_code)
|
|
540
|
+
if chunk:
|
|
541
|
+
chunks.append(chunk)
|
|
542
|
+
else:
|
|
543
|
+
chunk = self._create_grouped_function_chunk(group, source_code)
|
|
544
|
+
if chunk:
|
|
545
|
+
chunks.append(chunk)
|
|
546
|
+
|
|
547
|
+
i += self.MAX_METHODS_PER_CHUNK
|
|
548
|
+
|
|
549
|
+
# Process large functions (one per chunk)
|
|
550
|
+
for node in large_functions:
|
|
551
|
+
chunk = self._create_function_chunk(node, source_code)
|
|
552
|
+
if chunk:
|
|
553
|
+
chunks.append(chunk)
|
|
554
|
+
|
|
555
|
+
return chunks
|
|
556
|
+
|
|
557
|
+
def _create_function_chunk(
|
|
558
|
+
self,
|
|
559
|
+
node: ASTNode,
|
|
560
|
+
source_code: dict[str, str]
|
|
561
|
+
) -> CodeChunk | None:
|
|
562
|
+
"""Create chunk from a single TypeScript/JavaScript function."""
|
|
563
|
+
file_path = node.metadata.get("file_path")
|
|
564
|
+
if not file_path or file_path not in source_code:
|
|
565
|
+
return None
|
|
566
|
+
|
|
567
|
+
content = source_code[file_path]
|
|
568
|
+
lines = content.split('\n')
|
|
569
|
+
|
|
570
|
+
# Extract function content
|
|
571
|
+
start_idx = max(0, node.line_start - 1)
|
|
572
|
+
end_idx = min(len(lines), node.line_end)
|
|
573
|
+
function_content = '\n'.join(lines[start_idx:end_idx])
|
|
574
|
+
|
|
575
|
+
# Try to include JSDoc
|
|
576
|
+
jsdoc_start = max(0, start_idx - 10)
|
|
577
|
+
preceding_lines = '\n'.join(lines[jsdoc_start:start_idx])
|
|
578
|
+
jsdoc_match = self.JSDOC_PATTERN.search(preceding_lines)
|
|
579
|
+
if jsdoc_match:
|
|
580
|
+
function_content = jsdoc_match.group() + '\n' + function_content
|
|
581
|
+
|
|
582
|
+
# Detect React hooks
|
|
583
|
+
is_hook = node.name.startswith('use') and len(node.name) > 3 and node.name[3].isupper()
|
|
584
|
+
chunk_type = ChunkType.HOOK.value if is_hook else ChunkType.FUNCTION.value
|
|
585
|
+
|
|
586
|
+
metadata = {
|
|
587
|
+
"name": node.name,
|
|
588
|
+
"qualified_name": node.qualified_name,
|
|
589
|
+
"line_start": node.line_start,
|
|
590
|
+
"line_end": node.line_end,
|
|
591
|
+
"language": node.metadata.get("language", "typescript"),
|
|
592
|
+
"is_hook": is_hook
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return CodeChunk(
|
|
596
|
+
content=function_content,
|
|
597
|
+
chunk_type=chunk_type,
|
|
598
|
+
metadata=metadata
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
def _create_grouped_function_chunk(
|
|
602
|
+
self,
|
|
603
|
+
nodes: list[ASTNode],
|
|
604
|
+
source_code: dict[str, str]
|
|
605
|
+
) -> CodeChunk | None:
|
|
606
|
+
"""Create chunk from multiple small functions grouped together."""
|
|
607
|
+
if not nodes:
|
|
608
|
+
return None
|
|
609
|
+
|
|
610
|
+
file_path = nodes[0].metadata.get("file_path")
|
|
611
|
+
if not file_path or file_path not in source_code:
|
|
612
|
+
return None
|
|
613
|
+
|
|
614
|
+
content = source_code[file_path]
|
|
615
|
+
lines = content.split('\n')
|
|
616
|
+
|
|
617
|
+
# Extract all functions
|
|
618
|
+
grouped_content = []
|
|
619
|
+
for node in nodes:
|
|
620
|
+
start_idx = max(0, node.line_start - 1)
|
|
621
|
+
end_idx = min(len(lines), node.line_end)
|
|
622
|
+
function_content = '\n'.join(lines[start_idx:end_idx])
|
|
623
|
+
grouped_content.append(function_content)
|
|
624
|
+
|
|
625
|
+
metadata = {
|
|
626
|
+
"name": f"grouped_{len(nodes)}_functions",
|
|
627
|
+
"qualified_name": nodes[0].qualified_name.rsplit('.', 1)[0],
|
|
628
|
+
"line_start": nodes[0].line_start,
|
|
629
|
+
"line_end": nodes[-1].line_end,
|
|
630
|
+
"function_count": len(nodes),
|
|
631
|
+
"language": nodes[0].metadata.get("language", "typescript")
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return CodeChunk(
|
|
635
|
+
content='\n\n'.join(grouped_content),
|
|
636
|
+
chunk_type=ChunkType.FUNCTION.value,
|
|
637
|
+
metadata=metadata
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
def _build_component_chunks(
|
|
641
|
+
self,
|
|
642
|
+
component_nodes: list[ASTNode],
|
|
643
|
+
source_code: dict[str, str]
|
|
644
|
+
) -> list[CodeChunk]:
|
|
645
|
+
"""
|
|
646
|
+
Build chunks from React/Angular/Vue components (Task 15.2).
|
|
647
|
+
|
|
648
|
+
Includes:
|
|
649
|
+
- Props interface for React
|
|
650
|
+
- @Component decorator for Angular
|
|
651
|
+
- Script + template for Vue
|
|
652
|
+
- JSDoc comments
|
|
653
|
+
|
|
654
|
+
Args:
|
|
655
|
+
component_nodes: List of COMPONENT nodes from AST
|
|
656
|
+
source_code: Source code by file path
|
|
657
|
+
|
|
658
|
+
Returns:
|
|
659
|
+
List of CodeChunk instances
|
|
660
|
+
"""
|
|
661
|
+
chunks = []
|
|
662
|
+
|
|
663
|
+
for node in component_nodes:
|
|
664
|
+
chunk = self._create_component_chunk(node, source_code)
|
|
665
|
+
if chunk:
|
|
666
|
+
chunks.append(chunk)
|
|
667
|
+
|
|
668
|
+
return chunks
|
|
669
|
+
|
|
670
|
+
def _create_component_chunk(
|
|
671
|
+
self,
|
|
672
|
+
node: ASTNode,
|
|
673
|
+
source_code: dict[str, str]
|
|
674
|
+
) -> CodeChunk | None:
|
|
675
|
+
"""Create chunk from a single component."""
|
|
676
|
+
file_path = node.metadata.get("file_path")
|
|
677
|
+
if not file_path or file_path not in source_code:
|
|
678
|
+
return None
|
|
679
|
+
|
|
680
|
+
content = source_code[file_path]
|
|
681
|
+
lines = content.split('\n')
|
|
682
|
+
|
|
683
|
+
# Extract component content
|
|
684
|
+
start_idx = max(0, node.line_start - 1)
|
|
685
|
+
end_idx = min(len(lines), node.line_end)
|
|
686
|
+
component_content = '\n'.join(lines[start_idx:end_idx])
|
|
687
|
+
|
|
688
|
+
# Try to include JSDoc
|
|
689
|
+
jsdoc_start = max(0, start_idx - 10)
|
|
690
|
+
preceding_lines = '\n'.join(lines[jsdoc_start:start_idx])
|
|
691
|
+
jsdoc_match = self.JSDOC_PATTERN.search(preceding_lines)
|
|
692
|
+
if jsdoc_match:
|
|
693
|
+
component_content = jsdoc_match.group() + '\n' + component_content
|
|
694
|
+
|
|
695
|
+
framework = node.metadata.get("framework", "unknown")
|
|
696
|
+
|
|
697
|
+
metadata = {
|
|
698
|
+
"name": node.name,
|
|
699
|
+
"qualified_name": node.qualified_name,
|
|
700
|
+
"line_start": node.line_start,
|
|
701
|
+
"line_end": node.line_end,
|
|
702
|
+
"framework": framework,
|
|
703
|
+
"language": node.metadata.get("language", "typescript")
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
# Add framework-specific metadata
|
|
707
|
+
if "props_interface" in node.metadata:
|
|
708
|
+
metadata["props_interface"] = node.metadata["props_interface"]
|
|
709
|
+
if "selector" in node.metadata:
|
|
710
|
+
metadata["selector"] = node.metadata["selector"]
|
|
711
|
+
if "has_script_setup" in node.metadata:
|
|
712
|
+
metadata["has_script_setup"] = node.metadata["has_script_setup"]
|
|
713
|
+
|
|
714
|
+
return CodeChunk(
|
|
715
|
+
content=component_content,
|
|
716
|
+
chunk_type=ChunkType.COMPONENT.value,
|
|
717
|
+
metadata=metadata
|
|
718
|
+
)
|
|
719
|
+
|