skill-seekers 2.7.3__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.
- skill_seekers/__init__.py +22 -0
- skill_seekers/cli/__init__.py +39 -0
- skill_seekers/cli/adaptors/__init__.py +120 -0
- skill_seekers/cli/adaptors/base.py +221 -0
- skill_seekers/cli/adaptors/claude.py +485 -0
- skill_seekers/cli/adaptors/gemini.py +453 -0
- skill_seekers/cli/adaptors/markdown.py +269 -0
- skill_seekers/cli/adaptors/openai.py +503 -0
- skill_seekers/cli/ai_enhancer.py +310 -0
- skill_seekers/cli/api_reference_builder.py +373 -0
- skill_seekers/cli/architectural_pattern_detector.py +525 -0
- skill_seekers/cli/code_analyzer.py +1462 -0
- skill_seekers/cli/codebase_scraper.py +1225 -0
- skill_seekers/cli/config_command.py +563 -0
- skill_seekers/cli/config_enhancer.py +431 -0
- skill_seekers/cli/config_extractor.py +871 -0
- skill_seekers/cli/config_manager.py +452 -0
- skill_seekers/cli/config_validator.py +394 -0
- skill_seekers/cli/conflict_detector.py +528 -0
- skill_seekers/cli/constants.py +72 -0
- skill_seekers/cli/dependency_analyzer.py +757 -0
- skill_seekers/cli/doc_scraper.py +2332 -0
- skill_seekers/cli/enhance_skill.py +488 -0
- skill_seekers/cli/enhance_skill_local.py +1096 -0
- skill_seekers/cli/enhance_status.py +194 -0
- skill_seekers/cli/estimate_pages.py +433 -0
- skill_seekers/cli/generate_router.py +1209 -0
- skill_seekers/cli/github_fetcher.py +534 -0
- skill_seekers/cli/github_scraper.py +1466 -0
- skill_seekers/cli/guide_enhancer.py +723 -0
- skill_seekers/cli/how_to_guide_builder.py +1267 -0
- skill_seekers/cli/install_agent.py +461 -0
- skill_seekers/cli/install_skill.py +178 -0
- skill_seekers/cli/language_detector.py +614 -0
- skill_seekers/cli/llms_txt_detector.py +60 -0
- skill_seekers/cli/llms_txt_downloader.py +104 -0
- skill_seekers/cli/llms_txt_parser.py +150 -0
- skill_seekers/cli/main.py +558 -0
- skill_seekers/cli/markdown_cleaner.py +132 -0
- skill_seekers/cli/merge_sources.py +806 -0
- skill_seekers/cli/package_multi.py +77 -0
- skill_seekers/cli/package_skill.py +241 -0
- skill_seekers/cli/pattern_recognizer.py +1825 -0
- skill_seekers/cli/pdf_extractor_poc.py +1166 -0
- skill_seekers/cli/pdf_scraper.py +617 -0
- skill_seekers/cli/quality_checker.py +519 -0
- skill_seekers/cli/rate_limit_handler.py +438 -0
- skill_seekers/cli/resume_command.py +160 -0
- skill_seekers/cli/run_tests.py +230 -0
- skill_seekers/cli/setup_wizard.py +93 -0
- skill_seekers/cli/split_config.py +390 -0
- skill_seekers/cli/swift_patterns.py +560 -0
- skill_seekers/cli/test_example_extractor.py +1081 -0
- skill_seekers/cli/test_unified_simple.py +179 -0
- skill_seekers/cli/unified_codebase_analyzer.py +572 -0
- skill_seekers/cli/unified_scraper.py +932 -0
- skill_seekers/cli/unified_skill_builder.py +1605 -0
- skill_seekers/cli/upload_skill.py +162 -0
- skill_seekers/cli/utils.py +432 -0
- skill_seekers/mcp/__init__.py +33 -0
- skill_seekers/mcp/agent_detector.py +316 -0
- skill_seekers/mcp/git_repo.py +273 -0
- skill_seekers/mcp/server.py +231 -0
- skill_seekers/mcp/server_fastmcp.py +1249 -0
- skill_seekers/mcp/server_legacy.py +2302 -0
- skill_seekers/mcp/source_manager.py +285 -0
- skill_seekers/mcp/tools/__init__.py +115 -0
- skill_seekers/mcp/tools/config_tools.py +251 -0
- skill_seekers/mcp/tools/packaging_tools.py +826 -0
- skill_seekers/mcp/tools/scraping_tools.py +842 -0
- skill_seekers/mcp/tools/source_tools.py +828 -0
- skill_seekers/mcp/tools/splitting_tools.py +212 -0
- skill_seekers/py.typed +0 -0
- skill_seekers-2.7.3.dist-info/METADATA +2027 -0
- skill_seekers-2.7.3.dist-info/RECORD +79 -0
- skill_seekers-2.7.3.dist-info/WHEEL +5 -0
- skill_seekers-2.7.3.dist-info/entry_points.txt +19 -0
- skill_seekers-2.7.3.dist-info/licenses/LICENSE +21 -0
- skill_seekers-2.7.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Dependency Graph Analyzer (C2.6)
|
|
4
|
+
|
|
5
|
+
Analyzes import/require/include/use statements to build dependency graphs.
|
|
6
|
+
Supports 9 programming languages with language-specific extraction.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Multi-language import extraction (Python AST, others regex-based)
|
|
10
|
+
- Dependency graph construction with NetworkX
|
|
11
|
+
- Circular dependency detection
|
|
12
|
+
- Graph export (JSON, DOT/GraphViz, Mermaid)
|
|
13
|
+
- Strongly connected component analysis
|
|
14
|
+
|
|
15
|
+
Supported Languages:
|
|
16
|
+
- Python: import, from...import, relative imports (AST-based)
|
|
17
|
+
- JavaScript/TypeScript: ES6 import, CommonJS require (regex-based)
|
|
18
|
+
- C/C++: #include directives (regex-based)
|
|
19
|
+
- C#: using statements (regex, based on MS C# spec)
|
|
20
|
+
- Go: import statements (regex, based on Go language spec)
|
|
21
|
+
- Rust: use statements (regex, based on Rust reference)
|
|
22
|
+
- Java: import statements (regex, based on Oracle Java spec)
|
|
23
|
+
- Ruby: require/require_relative/load (regex, based on Ruby docs)
|
|
24
|
+
- PHP: require/include/use (regex, based on PHP reference)
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
from dependency_analyzer import DependencyAnalyzer
|
|
28
|
+
|
|
29
|
+
analyzer = DependencyAnalyzer()
|
|
30
|
+
analyzer.analyze_file('src/main.py', content, 'Python')
|
|
31
|
+
analyzer.analyze_file('src/utils.go', go_content, 'Go')
|
|
32
|
+
graph = analyzer.build_graph()
|
|
33
|
+
cycles = analyzer.detect_cycles()
|
|
34
|
+
|
|
35
|
+
Credits:
|
|
36
|
+
- Regex patterns inspired by official language specifications
|
|
37
|
+
- NetworkX for graph algorithms: https://networkx.org/
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
import ast
|
|
41
|
+
import logging
|
|
42
|
+
import re
|
|
43
|
+
from dataclasses import dataclass, field
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
from typing import Any
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
import networkx as nx
|
|
49
|
+
|
|
50
|
+
NETWORKX_AVAILABLE = True
|
|
51
|
+
except ImportError:
|
|
52
|
+
NETWORKX_AVAILABLE = False
|
|
53
|
+
|
|
54
|
+
logger = logging.getLogger(__name__)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class DependencyInfo:
|
|
59
|
+
"""Information about a single dependency relationship."""
|
|
60
|
+
|
|
61
|
+
source_file: str
|
|
62
|
+
imported_module: str
|
|
63
|
+
import_type: str # 'import', 'from', 'require', 'include'
|
|
64
|
+
is_relative: bool = False
|
|
65
|
+
line_number: int = 0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class FileNode:
|
|
70
|
+
"""Represents a file node in the dependency graph."""
|
|
71
|
+
|
|
72
|
+
file_path: str
|
|
73
|
+
language: str
|
|
74
|
+
dependencies: list[str] = field(default_factory=list)
|
|
75
|
+
imported_by: list[str] = field(default_factory=list)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class DependencyAnalyzer:
|
|
79
|
+
"""
|
|
80
|
+
Multi-language dependency analyzer using NetworkX.
|
|
81
|
+
|
|
82
|
+
Analyzes import/require/include statements and builds dependency graphs
|
|
83
|
+
with circular dependency detection.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self):
|
|
87
|
+
"""Initialize dependency analyzer."""
|
|
88
|
+
if not NETWORKX_AVAILABLE:
|
|
89
|
+
raise ImportError(
|
|
90
|
+
"NetworkX is required for dependency analysis. Install with: pip install networkx"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
self.graph = nx.DiGraph() # Directed graph for dependencies
|
|
94
|
+
self.file_dependencies: dict[str, list[DependencyInfo]] = {}
|
|
95
|
+
self.file_nodes: dict[str, FileNode] = {}
|
|
96
|
+
|
|
97
|
+
def analyze_file(self, file_path: str, content: str, language: str) -> list[DependencyInfo]:
|
|
98
|
+
"""
|
|
99
|
+
Extract dependencies from a source file.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
file_path: Path to source file
|
|
103
|
+
content: File content
|
|
104
|
+
language: Programming language (Python, JavaScript, TypeScript, C, C++, C#, Go, Rust, Java, Ruby, PHP)
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
List of DependencyInfo objects
|
|
108
|
+
"""
|
|
109
|
+
if language == "Python":
|
|
110
|
+
deps = self._extract_python_imports(content, file_path)
|
|
111
|
+
elif language in ("JavaScript", "TypeScript"):
|
|
112
|
+
deps = self._extract_js_imports(content, file_path)
|
|
113
|
+
elif language in ("C++", "C"):
|
|
114
|
+
deps = self._extract_cpp_includes(content, file_path)
|
|
115
|
+
elif language == "C#":
|
|
116
|
+
deps = self._extract_csharp_imports(content, file_path)
|
|
117
|
+
elif language == "Go":
|
|
118
|
+
deps = self._extract_go_imports(content, file_path)
|
|
119
|
+
elif language == "Rust":
|
|
120
|
+
deps = self._extract_rust_imports(content, file_path)
|
|
121
|
+
elif language == "Java":
|
|
122
|
+
deps = self._extract_java_imports(content, file_path)
|
|
123
|
+
elif language == "Ruby":
|
|
124
|
+
deps = self._extract_ruby_imports(content, file_path)
|
|
125
|
+
elif language == "PHP":
|
|
126
|
+
deps = self._extract_php_imports(content, file_path)
|
|
127
|
+
else:
|
|
128
|
+
logger.warning(f"Unsupported language: {language}")
|
|
129
|
+
deps = []
|
|
130
|
+
|
|
131
|
+
self.file_dependencies[file_path] = deps
|
|
132
|
+
|
|
133
|
+
# Create file node
|
|
134
|
+
imported_modules = [dep.imported_module for dep in deps]
|
|
135
|
+
self.file_nodes[file_path] = FileNode(
|
|
136
|
+
file_path=file_path, language=language, dependencies=imported_modules
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return deps
|
|
140
|
+
|
|
141
|
+
def _extract_python_imports(self, content: str, file_path: str) -> list[DependencyInfo]:
|
|
142
|
+
"""
|
|
143
|
+
Extract Python import statements using AST.
|
|
144
|
+
|
|
145
|
+
Handles:
|
|
146
|
+
- import module
|
|
147
|
+
- import module as alias
|
|
148
|
+
- from module import name
|
|
149
|
+
- from . import relative
|
|
150
|
+
"""
|
|
151
|
+
deps = []
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
tree = ast.parse(content)
|
|
155
|
+
except SyntaxError:
|
|
156
|
+
logger.warning(f"Syntax error in {file_path}, skipping import extraction")
|
|
157
|
+
return deps
|
|
158
|
+
|
|
159
|
+
for node in ast.walk(tree):
|
|
160
|
+
if isinstance(node, ast.Import):
|
|
161
|
+
for alias in node.names:
|
|
162
|
+
deps.append(
|
|
163
|
+
DependencyInfo(
|
|
164
|
+
source_file=file_path,
|
|
165
|
+
imported_module=alias.name,
|
|
166
|
+
import_type="import",
|
|
167
|
+
is_relative=False,
|
|
168
|
+
line_number=node.lineno,
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
elif isinstance(node, ast.ImportFrom):
|
|
173
|
+
module = node.module or ""
|
|
174
|
+
is_relative = node.level > 0
|
|
175
|
+
|
|
176
|
+
# Handle relative imports
|
|
177
|
+
if is_relative:
|
|
178
|
+
module = "." * node.level + module
|
|
179
|
+
|
|
180
|
+
deps.append(
|
|
181
|
+
DependencyInfo(
|
|
182
|
+
source_file=file_path,
|
|
183
|
+
imported_module=module,
|
|
184
|
+
import_type="from",
|
|
185
|
+
is_relative=is_relative,
|
|
186
|
+
line_number=node.lineno,
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return deps
|
|
191
|
+
|
|
192
|
+
def _extract_js_imports(self, content: str, file_path: str) -> list[DependencyInfo]:
|
|
193
|
+
"""
|
|
194
|
+
Extract JavaScript/TypeScript import statements.
|
|
195
|
+
|
|
196
|
+
Handles:
|
|
197
|
+
- import x from 'module'
|
|
198
|
+
- import { x } from 'module'
|
|
199
|
+
- import * as x from 'module'
|
|
200
|
+
- const x = require('module')
|
|
201
|
+
- require('module')
|
|
202
|
+
"""
|
|
203
|
+
deps = []
|
|
204
|
+
|
|
205
|
+
# ES6 imports: import ... from 'module'
|
|
206
|
+
import_pattern = r"import\s+(?:[\w\s{},*]+\s+from\s+)?['\"]([^'\"]+)['\"]"
|
|
207
|
+
for match in re.finditer(import_pattern, content):
|
|
208
|
+
module = match.group(1)
|
|
209
|
+
line_num = content[: match.start()].count("\n") + 1
|
|
210
|
+
is_relative = module.startswith(".") or module.startswith("/")
|
|
211
|
+
|
|
212
|
+
deps.append(
|
|
213
|
+
DependencyInfo(
|
|
214
|
+
source_file=file_path,
|
|
215
|
+
imported_module=module,
|
|
216
|
+
import_type="import",
|
|
217
|
+
is_relative=is_relative,
|
|
218
|
+
line_number=line_num,
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# CommonJS requires: require('module')
|
|
223
|
+
require_pattern = r"require\s*\(['\"]([^'\"]+)['\"]\)"
|
|
224
|
+
for match in re.finditer(require_pattern, content):
|
|
225
|
+
module = match.group(1)
|
|
226
|
+
line_num = content[: match.start()].count("\n") + 1
|
|
227
|
+
is_relative = module.startswith(".") or module.startswith("/")
|
|
228
|
+
|
|
229
|
+
deps.append(
|
|
230
|
+
DependencyInfo(
|
|
231
|
+
source_file=file_path,
|
|
232
|
+
imported_module=module,
|
|
233
|
+
import_type="require",
|
|
234
|
+
is_relative=is_relative,
|
|
235
|
+
line_number=line_num,
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
return deps
|
|
240
|
+
|
|
241
|
+
def _extract_cpp_includes(self, content: str, file_path: str) -> list[DependencyInfo]:
|
|
242
|
+
"""
|
|
243
|
+
Extract C++ #include directives.
|
|
244
|
+
|
|
245
|
+
Handles:
|
|
246
|
+
- #include "local/header.h"
|
|
247
|
+
- #include <system/header.h>
|
|
248
|
+
"""
|
|
249
|
+
deps = []
|
|
250
|
+
|
|
251
|
+
# Match #include statements
|
|
252
|
+
include_pattern = r'#include\s+[<"]([^>"]+)[>"]'
|
|
253
|
+
for match in re.finditer(include_pattern, content):
|
|
254
|
+
header = match.group(1)
|
|
255
|
+
line_num = content[: match.start()].count("\n") + 1
|
|
256
|
+
|
|
257
|
+
# Headers with "" are usually local, <> are system headers
|
|
258
|
+
is_relative = '"' in match.group(0)
|
|
259
|
+
|
|
260
|
+
deps.append(
|
|
261
|
+
DependencyInfo(
|
|
262
|
+
source_file=file_path,
|
|
263
|
+
imported_module=header,
|
|
264
|
+
import_type="include",
|
|
265
|
+
is_relative=is_relative,
|
|
266
|
+
line_number=line_num,
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
return deps
|
|
271
|
+
|
|
272
|
+
def _extract_csharp_imports(self, content: str, file_path: str) -> list[DependencyInfo]:
|
|
273
|
+
"""
|
|
274
|
+
Extract C# using statements.
|
|
275
|
+
|
|
276
|
+
Handles:
|
|
277
|
+
- using System;
|
|
278
|
+
- using MyNamespace;
|
|
279
|
+
- using static MyClass;
|
|
280
|
+
- using alias = Namespace;
|
|
281
|
+
|
|
282
|
+
Regex patterns based on C# language specification:
|
|
283
|
+
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-directive
|
|
284
|
+
"""
|
|
285
|
+
deps = []
|
|
286
|
+
|
|
287
|
+
# Match using statements: using [static] Namespace[.Type];
|
|
288
|
+
using_pattern = r"using\s+(?:static\s+)?(?:(\w+)\s*=\s*)?([A-Za-z_][\w.]*)\s*;"
|
|
289
|
+
for match in re.finditer(using_pattern, content):
|
|
290
|
+
alias = match.group(1) # Optional alias
|
|
291
|
+
namespace = match.group(2)
|
|
292
|
+
line_num = content[: match.start()].count("\n") + 1
|
|
293
|
+
|
|
294
|
+
# Skip 'using' statements for IDisposable (using var x = ...)
|
|
295
|
+
if "=" in match.group(0) and not alias:
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
deps.append(
|
|
299
|
+
DependencyInfo(
|
|
300
|
+
source_file=file_path,
|
|
301
|
+
imported_module=namespace,
|
|
302
|
+
import_type="using",
|
|
303
|
+
is_relative=False, # C# uses absolute namespaces
|
|
304
|
+
line_number=line_num,
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return deps
|
|
309
|
+
|
|
310
|
+
def _extract_go_imports(self, content: str, file_path: str) -> list[DependencyInfo]:
|
|
311
|
+
"""
|
|
312
|
+
Extract Go import statements.
|
|
313
|
+
|
|
314
|
+
Handles:
|
|
315
|
+
- import "package"
|
|
316
|
+
- import alias "package"
|
|
317
|
+
- import ( "pkg1" "pkg2" )
|
|
318
|
+
|
|
319
|
+
Regex patterns based on Go language specification:
|
|
320
|
+
https://go.dev/ref/spec#Import_declarations
|
|
321
|
+
"""
|
|
322
|
+
deps = []
|
|
323
|
+
|
|
324
|
+
# Single import: import [alias] "package"
|
|
325
|
+
single_import_pattern = r'import\s+(?:(\w+)\s+)?"([^"]+)"'
|
|
326
|
+
for match in re.finditer(single_import_pattern, content):
|
|
327
|
+
match.group(1) # Optional alias
|
|
328
|
+
package = match.group(2)
|
|
329
|
+
line_num = content[: match.start()].count("\n") + 1
|
|
330
|
+
|
|
331
|
+
# Check if relative (starts with ./ or ../)
|
|
332
|
+
is_relative = package.startswith("./")
|
|
333
|
+
|
|
334
|
+
deps.append(
|
|
335
|
+
DependencyInfo(
|
|
336
|
+
source_file=file_path,
|
|
337
|
+
imported_module=package,
|
|
338
|
+
import_type="import",
|
|
339
|
+
is_relative=is_relative,
|
|
340
|
+
line_number=line_num,
|
|
341
|
+
)
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# Multi-import block: import ( ... )
|
|
345
|
+
multi_import_pattern = r"import\s*\((.*?)\)"
|
|
346
|
+
for match in re.finditer(multi_import_pattern, content, re.DOTALL):
|
|
347
|
+
block = match.group(1)
|
|
348
|
+
block_start = match.start()
|
|
349
|
+
|
|
350
|
+
# Extract individual imports from block
|
|
351
|
+
import_line_pattern = r'(?:(\w+)\s+)?"([^"]+)"'
|
|
352
|
+
for line_match in re.finditer(import_line_pattern, block):
|
|
353
|
+
_alias = line_match.group(1)
|
|
354
|
+
package = line_match.group(2)
|
|
355
|
+
line_num = content[: block_start + line_match.start()].count("\n") + 1
|
|
356
|
+
|
|
357
|
+
is_relative = package.startswith("./")
|
|
358
|
+
|
|
359
|
+
deps.append(
|
|
360
|
+
DependencyInfo(
|
|
361
|
+
source_file=file_path,
|
|
362
|
+
imported_module=package,
|
|
363
|
+
import_type="import",
|
|
364
|
+
is_relative=is_relative,
|
|
365
|
+
line_number=line_num,
|
|
366
|
+
)
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
return deps
|
|
370
|
+
|
|
371
|
+
def _extract_rust_imports(self, content: str, file_path: str) -> list[DependencyInfo]:
|
|
372
|
+
"""
|
|
373
|
+
Extract Rust use statements.
|
|
374
|
+
|
|
375
|
+
Handles:
|
|
376
|
+
- use std::collections::HashMap;
|
|
377
|
+
- use crate::module;
|
|
378
|
+
- use super::sibling;
|
|
379
|
+
- use self::child;
|
|
380
|
+
|
|
381
|
+
Regex patterns based on Rust reference:
|
|
382
|
+
https://doc.rust-lang.org/reference/items/use-declarations.html
|
|
383
|
+
"""
|
|
384
|
+
deps = []
|
|
385
|
+
|
|
386
|
+
# Match use statements: use path::to::item; (including curly braces with spaces)
|
|
387
|
+
# This pattern matches: use word::word; or use word::{item, item};
|
|
388
|
+
use_pattern = r"use\s+([\w:{}]+(?:\s*,\s*[\w:{}]+)*|[\w:]+::\{[^}]+\})\s*;"
|
|
389
|
+
for match in re.finditer(use_pattern, content):
|
|
390
|
+
module_path = match.group(1)
|
|
391
|
+
line_num = content[: match.start()].count("\n") + 1
|
|
392
|
+
|
|
393
|
+
# Determine if relative
|
|
394
|
+
is_relative = module_path.startswith(("self::", "super::"))
|
|
395
|
+
|
|
396
|
+
# Handle curly brace imports (use std::{io, fs})
|
|
397
|
+
if "{" in module_path:
|
|
398
|
+
# Extract base path
|
|
399
|
+
base_path = module_path.split("{")[0].rstrip(":")
|
|
400
|
+
# Extract items inside braces
|
|
401
|
+
items_match = re.search(r"\{([^}]+)\}", module_path)
|
|
402
|
+
if items_match:
|
|
403
|
+
items = [item.strip() for item in items_match.group(1).split(",")]
|
|
404
|
+
for item in items:
|
|
405
|
+
full_path = f"{base_path}::{item}" if base_path else item
|
|
406
|
+
deps.append(
|
|
407
|
+
DependencyInfo(
|
|
408
|
+
source_file=file_path,
|
|
409
|
+
imported_module=full_path,
|
|
410
|
+
import_type="use",
|
|
411
|
+
is_relative=is_relative,
|
|
412
|
+
line_number=line_num,
|
|
413
|
+
)
|
|
414
|
+
)
|
|
415
|
+
else:
|
|
416
|
+
deps.append(
|
|
417
|
+
DependencyInfo(
|
|
418
|
+
source_file=file_path,
|
|
419
|
+
imported_module=module_path,
|
|
420
|
+
import_type="use",
|
|
421
|
+
is_relative=is_relative,
|
|
422
|
+
line_number=line_num,
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
return deps
|
|
427
|
+
|
|
428
|
+
def _extract_java_imports(self, content: str, file_path: str) -> list[DependencyInfo]:
|
|
429
|
+
"""
|
|
430
|
+
Extract Java import statements.
|
|
431
|
+
|
|
432
|
+
Handles:
|
|
433
|
+
- import java.util.List;
|
|
434
|
+
- import java.util.*;
|
|
435
|
+
- import static java.lang.Math.PI;
|
|
436
|
+
|
|
437
|
+
Regex patterns based on Java language specification:
|
|
438
|
+
https://docs.oracle.com/javase/specs/jls/se17/html/jls-7.html#jls-7.5
|
|
439
|
+
"""
|
|
440
|
+
deps = []
|
|
441
|
+
|
|
442
|
+
# Match import statements: import [static] package.Class;
|
|
443
|
+
import_pattern = r"import\s+(?:static\s+)?([A-Za-z_][\w.]*(?:\.\*)?)\s*;"
|
|
444
|
+
for match in re.finditer(import_pattern, content):
|
|
445
|
+
import_path = match.group(1)
|
|
446
|
+
line_num = content[: match.start()].count("\n") + 1
|
|
447
|
+
|
|
448
|
+
deps.append(
|
|
449
|
+
DependencyInfo(
|
|
450
|
+
source_file=file_path,
|
|
451
|
+
imported_module=import_path,
|
|
452
|
+
import_type="import",
|
|
453
|
+
is_relative=False, # Java uses absolute package names
|
|
454
|
+
line_number=line_num,
|
|
455
|
+
)
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
return deps
|
|
459
|
+
|
|
460
|
+
def _extract_ruby_imports(self, content: str, file_path: str) -> list[DependencyInfo]:
|
|
461
|
+
"""
|
|
462
|
+
Extract Ruby require/require_relative/load statements.
|
|
463
|
+
|
|
464
|
+
Handles:
|
|
465
|
+
- require 'gem_name'
|
|
466
|
+
- require_relative 'file'
|
|
467
|
+
- load 'script.rb'
|
|
468
|
+
|
|
469
|
+
Regex patterns based on Ruby documentation:
|
|
470
|
+
https://ruby-doc.org/core/Kernel.html#method-i-require
|
|
471
|
+
"""
|
|
472
|
+
deps = []
|
|
473
|
+
|
|
474
|
+
# Match require: require 'module' or require "module"
|
|
475
|
+
require_pattern = r"require\s+['\"]([^'\"]+)['\"]"
|
|
476
|
+
for match in re.finditer(require_pattern, content):
|
|
477
|
+
module = match.group(1)
|
|
478
|
+
line_num = content[: match.start()].count("\n") + 1
|
|
479
|
+
|
|
480
|
+
deps.append(
|
|
481
|
+
DependencyInfo(
|
|
482
|
+
source_file=file_path,
|
|
483
|
+
imported_module=module,
|
|
484
|
+
import_type="require",
|
|
485
|
+
is_relative=False, # require looks in load path
|
|
486
|
+
line_number=line_num,
|
|
487
|
+
)
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
# Match require_relative: require_relative 'file'
|
|
491
|
+
require_relative_pattern = r"require_relative\s+['\"]([^'\"]+)['\"]"
|
|
492
|
+
for match in re.finditer(require_relative_pattern, content):
|
|
493
|
+
module = match.group(1)
|
|
494
|
+
line_num = content[: match.start()].count("\n") + 1
|
|
495
|
+
|
|
496
|
+
deps.append(
|
|
497
|
+
DependencyInfo(
|
|
498
|
+
source_file=file_path,
|
|
499
|
+
imported_module=module,
|
|
500
|
+
import_type="require_relative",
|
|
501
|
+
is_relative=True,
|
|
502
|
+
line_number=line_num,
|
|
503
|
+
)
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
# Match load: load 'script.rb'
|
|
507
|
+
load_pattern = r"load\s+['\"]([^'\"]+)['\"]"
|
|
508
|
+
for match in re.finditer(load_pattern, content):
|
|
509
|
+
module = match.group(1)
|
|
510
|
+
line_num = content[: match.start()].count("\n") + 1
|
|
511
|
+
|
|
512
|
+
deps.append(
|
|
513
|
+
DependencyInfo(
|
|
514
|
+
source_file=file_path,
|
|
515
|
+
imported_module=module,
|
|
516
|
+
import_type="load",
|
|
517
|
+
is_relative=True, # load is usually relative
|
|
518
|
+
line_number=line_num,
|
|
519
|
+
)
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
return deps
|
|
523
|
+
|
|
524
|
+
def _extract_php_imports(self, content: str, file_path: str) -> list[DependencyInfo]:
|
|
525
|
+
"""
|
|
526
|
+
Extract PHP require/include/use statements.
|
|
527
|
+
|
|
528
|
+
Handles:
|
|
529
|
+
- require 'file.php';
|
|
530
|
+
- require_once 'file.php';
|
|
531
|
+
- include 'file.php';
|
|
532
|
+
- include_once 'file.php';
|
|
533
|
+
- use Namespace\\Class;
|
|
534
|
+
|
|
535
|
+
Regex patterns based on PHP language reference:
|
|
536
|
+
https://www.php.net/manual/en/function.require.php
|
|
537
|
+
"""
|
|
538
|
+
deps = []
|
|
539
|
+
|
|
540
|
+
# Match require/include: require[_once] 'file' or require[_once] "file"
|
|
541
|
+
require_pattern = r"(?:require|include)(?:_once)?\s+['\"]([^'\"]+)['\"]"
|
|
542
|
+
for match in re.finditer(require_pattern, content):
|
|
543
|
+
module = match.group(1)
|
|
544
|
+
line_num = content[: match.start()].count("\n") + 1
|
|
545
|
+
|
|
546
|
+
# Determine import type
|
|
547
|
+
import_type = "require" if "require" in match.group(0) else "include"
|
|
548
|
+
|
|
549
|
+
# PHP file paths are relative by default
|
|
550
|
+
is_relative = not module.startswith(("/", "http://", "https://"))
|
|
551
|
+
|
|
552
|
+
deps.append(
|
|
553
|
+
DependencyInfo(
|
|
554
|
+
source_file=file_path,
|
|
555
|
+
imported_module=module,
|
|
556
|
+
import_type=import_type,
|
|
557
|
+
is_relative=is_relative,
|
|
558
|
+
line_number=line_num,
|
|
559
|
+
)
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
# Match namespace use: use Namespace\Class;
|
|
563
|
+
use_pattern = r"use\s+([A-Za-z_][\w\\]*)\s*(?:as\s+\w+)?\s*;"
|
|
564
|
+
for match in re.finditer(use_pattern, content):
|
|
565
|
+
namespace = match.group(1)
|
|
566
|
+
line_num = content[: match.start()].count("\n") + 1
|
|
567
|
+
|
|
568
|
+
deps.append(
|
|
569
|
+
DependencyInfo(
|
|
570
|
+
source_file=file_path,
|
|
571
|
+
imported_module=namespace,
|
|
572
|
+
import_type="use",
|
|
573
|
+
is_relative=False, # Namespaces are absolute
|
|
574
|
+
line_number=line_num,
|
|
575
|
+
)
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
return deps
|
|
579
|
+
|
|
580
|
+
def build_graph(self) -> nx.DiGraph:
|
|
581
|
+
"""
|
|
582
|
+
Build dependency graph from analyzed files.
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
NetworkX DiGraph with file dependencies
|
|
586
|
+
"""
|
|
587
|
+
self.graph.clear()
|
|
588
|
+
|
|
589
|
+
# Add all file nodes
|
|
590
|
+
for file_path, node in self.file_nodes.items():
|
|
591
|
+
self.graph.add_node(file_path, language=node.language)
|
|
592
|
+
|
|
593
|
+
# Add dependency edges
|
|
594
|
+
for file_path, deps in self.file_dependencies.items():
|
|
595
|
+
for dep in deps:
|
|
596
|
+
# Try to resolve the imported module to an actual file
|
|
597
|
+
target = self._resolve_import(file_path, dep.imported_module, dep.is_relative)
|
|
598
|
+
|
|
599
|
+
if target and target in self.file_nodes:
|
|
600
|
+
# Add edge from source to dependency
|
|
601
|
+
self.graph.add_edge(
|
|
602
|
+
file_path, target, import_type=dep.import_type, line_number=dep.line_number
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
# Update imported_by lists
|
|
606
|
+
if target in self.file_nodes:
|
|
607
|
+
self.file_nodes[target].imported_by.append(file_path)
|
|
608
|
+
|
|
609
|
+
return self.graph
|
|
610
|
+
|
|
611
|
+
def _resolve_import(
|
|
612
|
+
self, _source_file: str, imported_module: str, _is_relative: bool
|
|
613
|
+
) -> str | None:
|
|
614
|
+
"""
|
|
615
|
+
Resolve import statement to actual file path.
|
|
616
|
+
|
|
617
|
+
This is a simplified resolution - a full implementation would need
|
|
618
|
+
to handle module resolution rules for each language.
|
|
619
|
+
"""
|
|
620
|
+
# For now, just return the imported module if it exists in our file_nodes
|
|
621
|
+
# In a real implementation, this would resolve relative paths, handle
|
|
622
|
+
# module resolution (node_modules, Python packages, etc.)
|
|
623
|
+
|
|
624
|
+
if imported_module in self.file_nodes:
|
|
625
|
+
return imported_module
|
|
626
|
+
|
|
627
|
+
# Try common variations
|
|
628
|
+
variations = [
|
|
629
|
+
imported_module,
|
|
630
|
+
f"{imported_module}.py",
|
|
631
|
+
f"{imported_module}.js",
|
|
632
|
+
f"{imported_module}.ts",
|
|
633
|
+
f"{imported_module}.h",
|
|
634
|
+
f"{imported_module}.cpp",
|
|
635
|
+
]
|
|
636
|
+
|
|
637
|
+
for var in variations:
|
|
638
|
+
if var in self.file_nodes:
|
|
639
|
+
return var
|
|
640
|
+
|
|
641
|
+
return None
|
|
642
|
+
|
|
643
|
+
def detect_cycles(self) -> list[list[str]]:
|
|
644
|
+
"""
|
|
645
|
+
Detect circular dependencies in the graph.
|
|
646
|
+
|
|
647
|
+
Returns:
|
|
648
|
+
List of cycles, where each cycle is a list of file paths
|
|
649
|
+
"""
|
|
650
|
+
try:
|
|
651
|
+
cycles = list(nx.simple_cycles(self.graph))
|
|
652
|
+
if cycles:
|
|
653
|
+
logger.warning(f"Found {len(cycles)} circular dependencies")
|
|
654
|
+
for cycle in cycles:
|
|
655
|
+
logger.warning(f" Cycle: {' -> '.join(cycle)} -> {cycle[0]}")
|
|
656
|
+
return cycles
|
|
657
|
+
except Exception as e:
|
|
658
|
+
logger.error(f"Error detecting cycles: {e}")
|
|
659
|
+
return []
|
|
660
|
+
|
|
661
|
+
def get_strongly_connected_components(self) -> list[set[str]]:
|
|
662
|
+
"""
|
|
663
|
+
Get strongly connected components (groups of mutually dependent files).
|
|
664
|
+
|
|
665
|
+
Returns:
|
|
666
|
+
List of sets, each containing file paths in a component
|
|
667
|
+
"""
|
|
668
|
+
return list(nx.strongly_connected_components(self.graph))
|
|
669
|
+
|
|
670
|
+
def export_dot(self, output_path: str):
|
|
671
|
+
"""
|
|
672
|
+
Export graph as GraphViz DOT format.
|
|
673
|
+
|
|
674
|
+
Args:
|
|
675
|
+
output_path: Path to save .dot file
|
|
676
|
+
"""
|
|
677
|
+
try:
|
|
678
|
+
from networkx.drawing.nx_pydot import write_dot
|
|
679
|
+
|
|
680
|
+
write_dot(self.graph, output_path)
|
|
681
|
+
logger.info(f"Exported graph to DOT format: {output_path}")
|
|
682
|
+
except ImportError:
|
|
683
|
+
logger.warning("pydot not installed - cannot export to DOT format")
|
|
684
|
+
logger.warning("Install with: pip install pydot")
|
|
685
|
+
|
|
686
|
+
def export_json(self) -> dict[str, Any]:
|
|
687
|
+
"""
|
|
688
|
+
Export graph as JSON structure.
|
|
689
|
+
|
|
690
|
+
Returns:
|
|
691
|
+
Dictionary with nodes and edges
|
|
692
|
+
"""
|
|
693
|
+
return {
|
|
694
|
+
"nodes": [
|
|
695
|
+
{"file": node, "language": data.get("language", "Unknown")}
|
|
696
|
+
for node, data in self.graph.nodes(data=True)
|
|
697
|
+
],
|
|
698
|
+
"edges": [
|
|
699
|
+
{
|
|
700
|
+
"source": source,
|
|
701
|
+
"target": target,
|
|
702
|
+
"import_type": data.get("import_type", "unknown"),
|
|
703
|
+
"line_number": data.get("line_number", 0),
|
|
704
|
+
}
|
|
705
|
+
for source, target, data in self.graph.edges(data=True)
|
|
706
|
+
],
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
def export_mermaid(self) -> str:
|
|
710
|
+
"""
|
|
711
|
+
Export graph as Mermaid diagram format.
|
|
712
|
+
|
|
713
|
+
Returns:
|
|
714
|
+
Mermaid diagram as string
|
|
715
|
+
"""
|
|
716
|
+
lines = ["graph TD"]
|
|
717
|
+
|
|
718
|
+
# Create node labels (shorten file paths for readability)
|
|
719
|
+
node_ids = {}
|
|
720
|
+
for i, node in enumerate(self.graph.nodes()):
|
|
721
|
+
node_id = f"N{i}"
|
|
722
|
+
node_ids[node] = node_id
|
|
723
|
+
label = Path(node).name # Just filename
|
|
724
|
+
lines.append(f" {node_id}[{label}]")
|
|
725
|
+
|
|
726
|
+
# Add edges
|
|
727
|
+
for source, target in self.graph.edges():
|
|
728
|
+
source_id = node_ids[source]
|
|
729
|
+
target_id = node_ids[target]
|
|
730
|
+
lines.append(f" {source_id} --> {target_id}")
|
|
731
|
+
|
|
732
|
+
return "\n".join(lines)
|
|
733
|
+
|
|
734
|
+
def get_statistics(self) -> dict[str, Any]:
|
|
735
|
+
"""
|
|
736
|
+
Get graph statistics.
|
|
737
|
+
|
|
738
|
+
Returns:
|
|
739
|
+
Dictionary with various statistics
|
|
740
|
+
"""
|
|
741
|
+
return {
|
|
742
|
+
"total_files": self.graph.number_of_nodes(),
|
|
743
|
+
"total_dependencies": self.graph.number_of_edges(),
|
|
744
|
+
"circular_dependencies": len(self.detect_cycles()),
|
|
745
|
+
"strongly_connected_components": len(self.get_strongly_connected_components()),
|
|
746
|
+
"avg_dependencies_per_file": (
|
|
747
|
+
self.graph.number_of_edges() / self.graph.number_of_nodes()
|
|
748
|
+
if self.graph.number_of_nodes() > 0
|
|
749
|
+
else 0
|
|
750
|
+
),
|
|
751
|
+
"files_with_no_dependencies": len(
|
|
752
|
+
[node for node in self.graph.nodes() if self.graph.out_degree(node) == 0]
|
|
753
|
+
),
|
|
754
|
+
"files_not_imported": len(
|
|
755
|
+
[node for node in self.graph.nodes() if self.graph.in_degree(node) == 0]
|
|
756
|
+
),
|
|
757
|
+
}
|