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,347 @@
|
|
|
1
|
+
"""Configuration file parser for extracting APIs, databases, and services.
|
|
2
|
+
|
|
3
|
+
This module provides parsing capabilities for application.properties,
|
|
4
|
+
application.yml, and .env files to extract external dependencies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any
|
|
10
|
+
import yaml
|
|
11
|
+
from src.domain.value_objects.dependency_type import DependencyType
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ParsedConfig:
|
|
16
|
+
"""Result of parsing configuration files.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
databases: List of database connections (JDBC URLs)
|
|
20
|
+
apis: List of API endpoints (HTTP/HTTPS URLs)
|
|
21
|
+
services: List of microservice names
|
|
22
|
+
feature_flags: List of feature flag definitions (Task 11.3)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
databases: list[dict[str, Any]] = field(default_factory=list)
|
|
26
|
+
apis: list[dict[str, Any]] = field(default_factory=list)
|
|
27
|
+
services: list[dict[str, Any]] = field(default_factory=list)
|
|
28
|
+
feature_flags: list[dict[str, Any]] = field(default_factory=list) # Task 11.3
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ConfigFileParser:
|
|
32
|
+
"""Parser for application configuration files.
|
|
33
|
+
|
|
34
|
+
Parses:
|
|
35
|
+
- application.properties (Java properties format)
|
|
36
|
+
- application.yml (YAML format)
|
|
37
|
+
- .env files (KEY=VALUE format, including frontend env vars)
|
|
38
|
+
- TypeScript/JavaScript config files (config.ts, environment.ts) - Task 11.2
|
|
39
|
+
|
|
40
|
+
Extracts:
|
|
41
|
+
- API URLs (http://, https://, wss://)
|
|
42
|
+
- Database connections (jdbc:)
|
|
43
|
+
- Microservice names (service identifiers)
|
|
44
|
+
- Feature flags (Task 11.3)
|
|
45
|
+
- Environment-specific configurations (Task 11.1)
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
# Regex patterns
|
|
49
|
+
JDBC_PATTERN = re.compile(r'jdbc:[^:]+://[^\s]+')
|
|
50
|
+
HTTP_URL_PATTERN = re.compile(r'https?://[^\s\'"]+')
|
|
51
|
+
WS_URL_PATTERN = re.compile(r'wss?://[^\s\'"]+') # WebSocket URLs
|
|
52
|
+
|
|
53
|
+
# Keys that typically contain API URLs
|
|
54
|
+
API_KEYS = {
|
|
55
|
+
"url", "endpoint", "host", "api", "gateway", "service.url",
|
|
56
|
+
"external", "client", "rest", "graphql"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Keys that typically contain service names
|
|
60
|
+
SERVICE_KEYS = {"service", "name", "microservice", "app"}
|
|
61
|
+
|
|
62
|
+
# Frontend environment variable prefixes (Task 11.1)
|
|
63
|
+
FRONTEND_ENV_PREFIXES = {
|
|
64
|
+
"REACT_APP_", "VITE_", "NEXT_PUBLIC_", "NG_APP_",
|
|
65
|
+
"VUE_APP_", "NUXT_PUBLIC_"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Feature flag patterns (Task 11.3)
|
|
69
|
+
FEATURE_FLAG_PATTERNS = {
|
|
70
|
+
"FEATURE_", "FEAT_", "FLAG_", "ENABLE_", "DISABLE_"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
def parse_properties(self, content: str) -> ParsedConfig:
|
|
74
|
+
"""Parse Java properties file (application.properties).
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
content: String content of properties file
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
ParsedConfig with extracted dependencies
|
|
81
|
+
"""
|
|
82
|
+
result = ParsedConfig()
|
|
83
|
+
|
|
84
|
+
# Remove comments
|
|
85
|
+
lines = [line for line in content.split('\n') if line.strip() and not line.strip().startswith('#')]
|
|
86
|
+
|
|
87
|
+
for line in lines:
|
|
88
|
+
if '=' not in line:
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
key, value = line.split('=', 1)
|
|
92
|
+
key = key.strip()
|
|
93
|
+
value = value.strip()
|
|
94
|
+
|
|
95
|
+
# Extract databases (JDBC)
|
|
96
|
+
if value.startswith('jdbc:'):
|
|
97
|
+
result.databases.append({
|
|
98
|
+
"url": value,
|
|
99
|
+
"type": DependencyType.DATABASE,
|
|
100
|
+
"source": key,
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
# Extract APIs (HTTP/HTTPS)
|
|
104
|
+
elif value.startswith(('http://', 'https://')):
|
|
105
|
+
result.apis.append({
|
|
106
|
+
"url": value,
|
|
107
|
+
"type": DependencyType.API,
|
|
108
|
+
"source": key,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
# Extract service names
|
|
112
|
+
elif any(svc_key in key.lower() for svc_key in self.SERVICE_KEYS):
|
|
113
|
+
if not value.startswith(('http://', 'https://', 'jdbc:')):
|
|
114
|
+
result.services.append({
|
|
115
|
+
"name": value,
|
|
116
|
+
"type": DependencyType.MICROSERVICE,
|
|
117
|
+
"source": key,
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
def parse_yaml(self, content: str) -> ParsedConfig:
|
|
123
|
+
"""Parse YAML configuration file (application.yml).
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
content: String content of YAML file
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
ParsedConfig with extracted dependencies
|
|
130
|
+
"""
|
|
131
|
+
result = ParsedConfig()
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
data = yaml.safe_load(content)
|
|
135
|
+
if data:
|
|
136
|
+
self._extract_from_dict(data, result)
|
|
137
|
+
except yaml.YAMLError:
|
|
138
|
+
# If YAML parsing fails, return empty result
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
def parse_env(self, content: str, environment: str | None = None, detect_feature_flags: bool = False) -> ParsedConfig:
|
|
144
|
+
"""Parse .env file (KEY=VALUE format).
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
content: String content of .env file
|
|
148
|
+
environment: Environment tag (e.g., "development", "production", "staging") - Task 11.1
|
|
149
|
+
detect_feature_flags: Whether to detect and extract feature flags - Task 11.3
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
ParsedConfig with extracted dependencies
|
|
153
|
+
"""
|
|
154
|
+
result = ParsedConfig()
|
|
155
|
+
|
|
156
|
+
# Remove comments
|
|
157
|
+
lines = [line for line in content.split('\n') if line.strip() and not line.strip().startswith('#')]
|
|
158
|
+
|
|
159
|
+
for line in lines:
|
|
160
|
+
if '=' not in line:
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
key, value = line.split('=', 1)
|
|
164
|
+
key = key.strip()
|
|
165
|
+
value = value.strip()
|
|
166
|
+
|
|
167
|
+
# Task 11.3: Detect feature flags
|
|
168
|
+
if detect_feature_flags and self._is_feature_flag(key):
|
|
169
|
+
result.feature_flags.append({
|
|
170
|
+
"name": key,
|
|
171
|
+
"enabled": self._parse_boolean(value),
|
|
172
|
+
"source": "env",
|
|
173
|
+
"environment": environment
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
# Extract databases (JDBC)
|
|
177
|
+
if value.startswith('jdbc:'):
|
|
178
|
+
result.databases.append({
|
|
179
|
+
"url": value,
|
|
180
|
+
"type": DependencyType.DATABASE,
|
|
181
|
+
"source": key,
|
|
182
|
+
"environment": environment # Task 11.1
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
# Extract WebSocket URLs (Task 11.1)
|
|
186
|
+
elif value.startswith(('ws://', 'wss://')):
|
|
187
|
+
result.apis.append({
|
|
188
|
+
"url": value,
|
|
189
|
+
"type": DependencyType.API,
|
|
190
|
+
"source": key,
|
|
191
|
+
"environment": environment # Task 11.1
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
# Extract APIs (HTTP/HTTPS)
|
|
195
|
+
elif value.startswith(('http://', 'https://')):
|
|
196
|
+
result.apis.append({
|
|
197
|
+
"url": value,
|
|
198
|
+
"type": DependencyType.API,
|
|
199
|
+
"source": key,
|
|
200
|
+
"environment": environment # Task 11.1
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
# Extract service names
|
|
204
|
+
elif any(svc_key in key.lower() for svc_key in self.SERVICE_KEYS):
|
|
205
|
+
if not value.startswith(('http://', 'https://', 'jdbc:', 'ws://', 'wss://')):
|
|
206
|
+
result.services.append({
|
|
207
|
+
"name": value,
|
|
208
|
+
"type": DependencyType.MICROSERVICE,
|
|
209
|
+
"source": key,
|
|
210
|
+
"environment": environment # Task 11.1
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
return result
|
|
214
|
+
|
|
215
|
+
def _is_feature_flag(self, key: str) -> bool:
|
|
216
|
+
"""Check if key matches feature flag pattern (Task 11.3)."""
|
|
217
|
+
key_upper = key.upper()
|
|
218
|
+
return any(pattern in key_upper for pattern in self.FEATURE_FLAG_PATTERNS)
|
|
219
|
+
|
|
220
|
+
def _parse_boolean(self, value: str) -> bool:
|
|
221
|
+
"""Parse boolean value from string (Task 11.3)."""
|
|
222
|
+
value_lower = value.lower()
|
|
223
|
+
return value_lower in ("true", "1", "yes", "enabled", "on")
|
|
224
|
+
|
|
225
|
+
def parse_typescript_config(
|
|
226
|
+
self,
|
|
227
|
+
content: str,
|
|
228
|
+
typescript_parser=None,
|
|
229
|
+
environment: str | None = None,
|
|
230
|
+
detect_feature_flags: bool = False
|
|
231
|
+
) -> ParsedConfig:
|
|
232
|
+
"""Parse TypeScript/JavaScript config files (Task 11.2).
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
content: String content of config.ts or environment.ts file
|
|
236
|
+
typescript_parser: TreeSitterTypeScriptParser instance for AST parsing
|
|
237
|
+
environment: Environment tag (e.g., "development", "production")
|
|
238
|
+
detect_feature_flags: Whether to detect and extract feature flags
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
ParsedConfig with extracted dependencies
|
|
242
|
+
"""
|
|
243
|
+
result = ParsedConfig()
|
|
244
|
+
|
|
245
|
+
if not typescript_parser:
|
|
246
|
+
return result
|
|
247
|
+
|
|
248
|
+
# Parse with TypeScript parser to extract string literals
|
|
249
|
+
try:
|
|
250
|
+
# For testing, typescript_parser might be a mock
|
|
251
|
+
if hasattr(typescript_parser, 'parse_file'):
|
|
252
|
+
# Mock or real parser
|
|
253
|
+
parse_result = typescript_parser.parse_file(content)
|
|
254
|
+
else:
|
|
255
|
+
return result
|
|
256
|
+
|
|
257
|
+
# Extract URLs from string literals
|
|
258
|
+
if hasattr(parse_result, 'string_literals'):
|
|
259
|
+
for literal in parse_result.string_literals:
|
|
260
|
+
if isinstance(literal, str):
|
|
261
|
+
# Check for URLs
|
|
262
|
+
if literal.startswith(('http://', 'https://', 'ws://', 'wss://')):
|
|
263
|
+
result.apis.append({
|
|
264
|
+
"url": literal,
|
|
265
|
+
"type": DependencyType.API,
|
|
266
|
+
"source": "typescript_config",
|
|
267
|
+
"environment": environment
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
# Detect environment variable references (Task 11.2)
|
|
271
|
+
if hasattr(parse_result, 'method_calls'):
|
|
272
|
+
for call in parse_result.method_calls:
|
|
273
|
+
call_name = call.get("name", "")
|
|
274
|
+
if "process.env" in call_name:
|
|
275
|
+
# Extract full env variable name (keep REACT_APP_, VITE_, etc.)
|
|
276
|
+
if "." in call_name:
|
|
277
|
+
env_var = call_name.split(".")[-1] # Get last part after final dot
|
|
278
|
+
else:
|
|
279
|
+
env_var = call_name
|
|
280
|
+
result.apis.append({
|
|
281
|
+
"url": None,
|
|
282
|
+
"type": DependencyType.API,
|
|
283
|
+
"source": "typescript_config",
|
|
284
|
+
"environment": environment,
|
|
285
|
+
"environment_dependent": True,
|
|
286
|
+
"env_variable": env_var
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
# Task 11.3: Detect feature flags in TypeScript config
|
|
290
|
+
if detect_feature_flags:
|
|
291
|
+
# Look for featureFlags, features, flags patterns in content
|
|
292
|
+
if any(pattern in content for pattern in ["featureFlags", "feature_flags", "FEATURE_FLAGS"]):
|
|
293
|
+
# Simple detection: if we find feature flag patterns in content
|
|
294
|
+
# Add a generic feature flag entry
|
|
295
|
+
result.feature_flags.append({
|
|
296
|
+
"name": "detected_in_typescript_config",
|
|
297
|
+
"enabled": True,
|
|
298
|
+
"source": "typescript_config",
|
|
299
|
+
"environment": environment
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
except Exception:
|
|
303
|
+
# Graceful degradation - return partial results
|
|
304
|
+
pass
|
|
305
|
+
|
|
306
|
+
return result
|
|
307
|
+
|
|
308
|
+
def _extract_from_dict(self, data: dict[str, Any], result: ParsedConfig, parent_key: str = "") -> None:
|
|
309
|
+
"""Recursively extract dependencies from nested dictionary.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
data: Dictionary to extract from
|
|
313
|
+
result: ParsedConfig to populate
|
|
314
|
+
parent_key: Parent key path for context
|
|
315
|
+
"""
|
|
316
|
+
for key, value in data.items():
|
|
317
|
+
full_key = f"{parent_key}.{key}" if parent_key else key
|
|
318
|
+
|
|
319
|
+
if isinstance(value, dict):
|
|
320
|
+
# Recurse into nested dictionaries
|
|
321
|
+
self._extract_from_dict(value, result, full_key)
|
|
322
|
+
|
|
323
|
+
elif isinstance(value, str):
|
|
324
|
+
# Extract JDBC URLs
|
|
325
|
+
if value.startswith('jdbc:'):
|
|
326
|
+
result.databases.append({
|
|
327
|
+
"url": value,
|
|
328
|
+
"type": DependencyType.DATABASE,
|
|
329
|
+
"source": full_key,
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
# Extract HTTP/HTTPS URLs
|
|
333
|
+
elif value.startswith(('http://', 'https://')):
|
|
334
|
+
result.apis.append({
|
|
335
|
+
"url": value,
|
|
336
|
+
"type": DependencyType.API,
|
|
337
|
+
"source": full_key,
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
# Extract service names
|
|
341
|
+
elif any(svc_key in full_key.lower() for svc_key in self.SERVICE_KEYS):
|
|
342
|
+
if not value.startswith(('http://', 'https://', 'jdbc:')):
|
|
343
|
+
result.services.append({
|
|
344
|
+
"name": value,
|
|
345
|
+
"type": DependencyType.MICROSERVICE,
|
|
346
|
+
"source": full_key,
|
|
347
|
+
})
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Gradle build.gradle and build.gradle.kts dependency parser.
|
|
2
|
+
|
|
3
|
+
This module provides parsing capabilities for Gradle build files to extract
|
|
4
|
+
dependency information for the dependency analysis system.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import TypedDict
|
|
9
|
+
from src.domain.value_objects.dependency_type import DependencyType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GradleDependency(TypedDict):
|
|
13
|
+
"""Type definition for Gradle dependency dictionary."""
|
|
14
|
+
|
|
15
|
+
group_id: str
|
|
16
|
+
artifact_id: str
|
|
17
|
+
version: str
|
|
18
|
+
scope: str
|
|
19
|
+
type: DependencyType
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GradleDependencyParser:
|
|
23
|
+
"""Parser for Gradle build.gradle and build.gradle.kts files.
|
|
24
|
+
|
|
25
|
+
Parses both Groovy (build.gradle) and Kotlin (build.gradle.kts) syntax.
|
|
26
|
+
Extracts dependency information: group:artifact:version format.
|
|
27
|
+
Filters out test-scoped dependencies by default.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
SCOPE_PATTERNS: Regex patterns for different dependency scopes
|
|
31
|
+
DEPENDENCY_PATTERN: Regex pattern to extract dependency coordinates
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# Gradle dependency scopes to include (exclude test scopes)
|
|
35
|
+
INCLUDE_SCOPES = {"implementation", "api", "compileOnly", "runtimeOnly"}
|
|
36
|
+
TEST_SCOPES = {"testImplementation", "testCompileOnly", "testRuntimeOnly"}
|
|
37
|
+
|
|
38
|
+
# Regex patterns for dependency extraction
|
|
39
|
+
# Matches: implementation 'group:artifact:version'
|
|
40
|
+
# Matches: implementation("group:artifact:version")
|
|
41
|
+
# Matches multiline: implementation(\n "group:artifact:version"\n)
|
|
42
|
+
DEPENDENCY_PATTERN = re.compile(
|
|
43
|
+
r'''
|
|
44
|
+
(?P<scope>implementation|api|compileOnly|runtimeOnly|testImplementation|testCompileOnly|testRuntimeOnly)
|
|
45
|
+
\s*
|
|
46
|
+
\(?\s* # Optional opening parenthesis
|
|
47
|
+
["'] # Opening quote (single or double)
|
|
48
|
+
\s*
|
|
49
|
+
(?P<coords>[^"']+) # Dependency coordinates
|
|
50
|
+
\s*
|
|
51
|
+
["'] # Closing quote (single or double)
|
|
52
|
+
\s*\)? # Optional closing parenthesis
|
|
53
|
+
''',
|
|
54
|
+
re.VERBOSE | re.MULTILINE | re.DOTALL,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Pattern to extract group:artifact:version
|
|
58
|
+
COORDS_PATTERN = re.compile(r'^([^:]+):([^:]+)(?::([^:]+))?$')
|
|
59
|
+
|
|
60
|
+
MANAGED_VERSION = "managed"
|
|
61
|
+
|
|
62
|
+
def parse(self, gradle_content: str) -> list[GradleDependency]:
|
|
63
|
+
"""Parse build.gradle or build.gradle.kts content and extract dependencies.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
gradle_content: String content of build.gradle or build.gradle.kts file
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
List of GradleDependency dictionaries with keys:
|
|
70
|
+
- group_id: Gradle group (e.g., 'org.springframework.boot')
|
|
71
|
+
- artifact_id: Gradle artifact (e.g., 'spring-boot-starter-web')
|
|
72
|
+
- version: Version string or 'managed' if not specified
|
|
73
|
+
- scope: Dependency scope (implementation, api, compileOnly, runtimeOnly)
|
|
74
|
+
- type: Always DependencyType.LIBRARY
|
|
75
|
+
|
|
76
|
+
Note:
|
|
77
|
+
Test-scoped dependencies (testImplementation, etc) are filtered out.
|
|
78
|
+
Comments (// and /* */) are removed before parsing.
|
|
79
|
+
"""
|
|
80
|
+
# Remove comments
|
|
81
|
+
clean_content = self._remove_comments(gradle_content)
|
|
82
|
+
|
|
83
|
+
# Find all dependency declarations
|
|
84
|
+
matches = self.DEPENDENCY_PATTERN.finditer(clean_content)
|
|
85
|
+
|
|
86
|
+
dependencies: list[GradleDependency] = []
|
|
87
|
+
|
|
88
|
+
for match in matches:
|
|
89
|
+
scope = match.group("scope")
|
|
90
|
+
coords = match.group("coords").strip()
|
|
91
|
+
|
|
92
|
+
# Filter out test dependencies
|
|
93
|
+
if scope in self.TEST_SCOPES:
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
# Parse coordinates
|
|
97
|
+
dependency = self._parse_coordinates(coords, scope)
|
|
98
|
+
|
|
99
|
+
if dependency:
|
|
100
|
+
dependencies.append(dependency)
|
|
101
|
+
|
|
102
|
+
return dependencies
|
|
103
|
+
|
|
104
|
+
def _remove_comments(self, content: str) -> str:
|
|
105
|
+
"""Remove single-line and multi-line comments from Gradle file.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
content: Original Gradle file content
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Content with comments removed
|
|
112
|
+
"""
|
|
113
|
+
# Remove single-line comments
|
|
114
|
+
content = re.sub(r'//.*?$', '', content, flags=re.MULTILINE)
|
|
115
|
+
|
|
116
|
+
# Remove multi-line comments
|
|
117
|
+
content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL)
|
|
118
|
+
|
|
119
|
+
return content
|
|
120
|
+
|
|
121
|
+
def _parse_coordinates(self, coords: str, scope: str) -> GradleDependency | None:
|
|
122
|
+
"""Parse dependency coordinates in format 'group:artifact:version'.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
coords: Dependency coordinates string
|
|
126
|
+
scope: Dependency scope (implementation, api, etc)
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
GradleDependency dictionary or None if parsing fails
|
|
130
|
+
"""
|
|
131
|
+
# Store original coords to check for variables
|
|
132
|
+
has_variable = "${" in coords
|
|
133
|
+
|
|
134
|
+
# Handle variable interpolation (${variable})
|
|
135
|
+
# For simplicity, treat as managed version
|
|
136
|
+
if has_variable:
|
|
137
|
+
coords = re.sub(r'\$\{[^}]+\}', 'managed', coords)
|
|
138
|
+
|
|
139
|
+
# Match group:artifact:version pattern
|
|
140
|
+
match = self.COORDS_PATTERN.match(coords.strip())
|
|
141
|
+
|
|
142
|
+
if not match:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
group_id = match.group(1).strip()
|
|
146
|
+
artifact_id = match.group(2).strip()
|
|
147
|
+
version = match.group(3).strip() if match.group(3) else self.MANAGED_VERSION
|
|
148
|
+
|
|
149
|
+
# Skip if essential fields are missing
|
|
150
|
+
if not group_id or not artifact_id:
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
return GradleDependency(
|
|
154
|
+
group_id=group_id,
|
|
155
|
+
artifact_id=artifact_id,
|
|
156
|
+
version=version,
|
|
157
|
+
scope=scope,
|
|
158
|
+
type=DependencyType.LIBRARY,
|
|
159
|
+
)
|