ref-agents 1.0.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.
- ref_agents/__init__.py +9 -0
- ref_agents/api_keys.json.example +8 -0
- ref_agents/auth.py +129 -0
- ref_agents/codemap/..md +62 -0
- ref_agents/codemap/CODE_MAP.md +37 -0
- ref_agents/codemap/core.md +43 -0
- ref_agents/codemap/models.md +43 -0
- ref_agents/codemap/prompts.md +40 -0
- ref_agents/codemap/security.md +45 -0
- ref_agents/codemap/tools.md +94 -0
- ref_agents/codemap/tools_browser.md +44 -0
- ref_agents/codemap/utils.md +42 -0
- ref_agents/codemap/workflow.md +42 -0
- ref_agents/config/ai_patterns.yaml +101 -0
- ref_agents/config/frameworks/angular.yaml +104 -0
- ref_agents/config/frameworks/aspnet.yaml +84 -0
- ref_agents/config/frameworks/ef_core.yaml +81 -0
- ref_agents/config/frameworks/react.yaml +111 -0
- ref_agents/config/frameworks/spring_boot.yaml +117 -0
- ref_agents/config/languages/csharp.yaml +153 -0
- ref_agents/config/languages/java.yaml +188 -0
- ref_agents/config/languages/javascript.yaml +172 -0
- ref_agents/config/languages/python.yaml +153 -0
- ref_agents/config/languages/typescript.yaml +193 -0
- ref_agents/constants.py +553 -0
- ref_agents/core/__init__.py +15 -0
- ref_agents/core/config_loader.py +160 -0
- ref_agents/core/config_models.py +167 -0
- ref_agents/core/config_parsing.py +84 -0
- ref_agents/core/language_detector.py +388 -0
- ref_agents/core/validation_models.py +66 -0
- ref_agents/core/validation_primitives.py +176 -0
- ref_agents/errors.py +34 -0
- ref_agents/license_client.py +247 -0
- ref_agents/models/__init__.py +22 -0
- ref_agents/models/gherkin.py +45 -0
- ref_agents/models/hierarchy.py +80 -0
- ref_agents/models/invest.py +59 -0
- ref_agents/models/version.py +49 -0
- ref_agents/prompts/__init__.py +9 -0
- ref_agents/prompts/start_agent.py +772 -0
- ref_agents/rules/architecture/backend_patterns.md +43 -0
- ref_agents/rules/architecture/diagramming.md +100 -0
- ref_agents/rules/architecture/frontend_patterns.md +40 -0
- ref_agents/rules/architecture/impact_analysis.md +129 -0
- ref_agents/rules/architecture/migration_strategy.md +208 -0
- ref_agents/rules/architecture/regression_protocol.md +77 -0
- ref_agents/rules/architecture/system_design.md +97 -0
- ref_agents/rules/common/codemap_standard.md +97 -0
- ref_agents/rules/common/core_protocol.md +59 -0
- ref_agents/rules/common/prompt_engineering.md +294 -0
- ref_agents/rules/development/debugging.md +32 -0
- ref_agents/rules/development/implementation.md +205 -0
- ref_agents/rules/operations/completion.md +119 -0
- ref_agents/rules/operations/cutover_protocol.md +218 -0
- ref_agents/rules/operations/discovery.md +179 -0
- ref_agents/rules/operations/fix_workflow.md +87 -0
- ref_agents/rules/operations/forensics.md +278 -0
- ref_agents/rules/operations/platform.md +263 -0
- ref_agents/rules/operations/synchronous_flow.md +25 -0
- ref_agents/rules/product/ac_validation.md +25 -0
- ref_agents/rules/product/brainstorming.md +27 -0
- ref_agents/rules/product/ref_flow.md +101 -0
- ref_agents/rules/product/requirements_std.md +114 -0
- ref_agents/rules/product/spec_writing.md +235 -0
- ref_agents/rules/product/strategy.md +96 -0
- ref_agents/rules/quality/documentation_standards.md +46 -0
- ref_agents/rules/quality/parity_testing.md +234 -0
- ref_agents/rules/quality/project_documentation.md +56 -0
- ref_agents/rules/quality/qa_lead.md +111 -0
- ref_agents/rules/quality/test_design.md +146 -0
- ref_agents/rules/quality/testing_standards.md +293 -0
- ref_agents/rules/review/pr_review.md +116 -0
- ref_agents/rules/security/security_audit.md +83 -0
- ref_agents/security/__init__.py +22 -0
- ref_agents/security/dependency_audit.py +188 -0
- ref_agents/security/file_audit.py +208 -0
- ref_agents/security/network_scan.py +179 -0
- ref_agents/security/report_generator.py +313 -0
- ref_agents/security/secret_scan.py +252 -0
- ref_agents/security/url_scan.py +240 -0
- ref_agents/security_scan.py +236 -0
- ref_agents/server.py +1586 -0
- ref_agents/session.py +100 -0
- ref_agents/tool_names.py +55 -0
- ref_agents/tools/__init__.py +8 -0
- ref_agents/tools/agents_generator.py +315 -0
- ref_agents/tools/ai_pattern_detector.py +815 -0
- ref_agents/tools/brownfield_populator.py +529 -0
- ref_agents/tools/browser/__init__.py +50 -0
- ref_agents/tools/browser/evidence_verifier.py +302 -0
- ref_agents/tools/browser/execution_logger.py +249 -0
- ref_agents/tools/browser/playwright_mcp_client.py +259 -0
- ref_agents/tools/browser/screenshot_utils.py +184 -0
- ref_agents/tools/browser/test_executor.py +537 -0
- ref_agents/tools/code_quality_scanner.py +629 -0
- ref_agents/tools/codemap/..md +93 -0
- ref_agents/tools/codemap/CODE_MAP.md +30 -0
- ref_agents/tools/codemap/browser.md +44 -0
- ref_agents/tools/codemap.py +403 -0
- ref_agents/tools/codemap_freshness.py +234 -0
- ref_agents/tools/comment_smell_scanner.py +346 -0
- ref_agents/tools/complexity.py +436 -0
- ref_agents/tools/complexity_ast.py +333 -0
- ref_agents/tools/compliance.py +246 -0
- ref_agents/tools/compliance_remediation.py +846 -0
- ref_agents/tools/context_graph.py +839 -0
- ref_agents/tools/context_manager.py +550 -0
- ref_agents/tools/context_tools.py +121 -0
- ref_agents/tools/cross_repo_linker.py +393 -0
- ref_agents/tools/dead_code_scanner.py +637 -0
- ref_agents/tools/debt_scanner.py +1092 -0
- ref_agents/tools/dependency_graph.py +272 -0
- ref_agents/tools/discovery_audit.py +372 -0
- ref_agents/tools/docs_scanner.py +600 -0
- ref_agents/tools/evaluate_gate.py +119 -0
- ref_agents/tools/external_detector.py +524 -0
- ref_agents/tools/features_generator.py +282 -0
- ref_agents/tools/flow_gap_detector.py +373 -0
- ref_agents/tools/flow_mapper.py +327 -0
- ref_agents/tools/full_suite_runner.py +740 -0
- ref_agents/tools/gherkin_parser.py +227 -0
- ref_agents/tools/guard_tools.py +139 -0
- ref_agents/tools/handoff_tools.py +282 -0
- ref_agents/tools/health_scanner.py +1211 -0
- ref_agents/tools/hierarchy_manager.py +289 -0
- ref_agents/tools/invest_scorer.py +249 -0
- ref_agents/tools/jira_confluence_export.py +306 -0
- ref_agents/tools/json_output.py +76 -0
- ref_agents/tools/migration_mapper.py +946 -0
- ref_agents/tools/migration_readiness_scanner.py +209 -0
- ref_agents/tools/pattern_learner.py +522 -0
- ref_agents/tools/report_utils.py +155 -0
- ref_agents/tools/requirements_serializer.py +225 -0
- ref_agents/tools/security_audit_tool.py +106 -0
- ref_agents/tools/sequencing_engine.py +288 -0
- ref_agents/tools/summary_generator.py +275 -0
- ref_agents/tools/symbol_resolver.py +306 -0
- ref_agents/tools/symbol_smoke_runner.py +336 -0
- ref_agents/tools/test_plan_validator.py +189 -0
- ref_agents/tools/test_smell_walker.py +902 -0
- ref_agents/tools/tier1_fixer.py +502 -0
- ref_agents/tools/validators/__init__.py +419 -0
- ref_agents/tools/validators/architect.py +268 -0
- ref_agents/tools/validators/cutover_engineer.py +167 -0
- ref_agents/tools/validators/developer.py +180 -0
- ref_agents/tools/validators/discovery.py +150 -0
- ref_agents/tools/validators/forensic_engineer.py +191 -0
- ref_agents/tools/validators/impact_architect.py +181 -0
- ref_agents/tools/validators/migration_planner.py +166 -0
- ref_agents/tools/validators/parity_tester.py +155 -0
- ref_agents/tools/validators/platform_engineer.py +134 -0
- ref_agents/tools/validators/pr_reviewer.py +129 -0
- ref_agents/tools/validators/product_manager.py +291 -0
- ref_agents/tools/validators/qa_lead.py +172 -0
- ref_agents/tools/validators/scrum_master.py +212 -0
- ref_agents/tools/validators/security_owner.py +162 -0
- ref_agents/tools/validators/specifier.py +134 -0
- ref_agents/tools/validators/strategist.py +149 -0
- ref_agents/tools/validators/tester.py +121 -0
- ref_agents/tools/version_manager.py +202 -0
- ref_agents/tools/workflow_tools.py +1549 -0
- ref_agents/utils/__init__.py +21 -0
- ref_agents/utils/git_utils.py +351 -0
- ref_agents/utils/handoff_logger.py +368 -0
- ref_agents/utils/ignore_matcher.py +270 -0
- ref_agents/workflow/__init__.py +19 -0
- ref_agents/workflow/capabilities.py +328 -0
- ref_agents/workflow/state_machine.py +708 -0
- ref_agents/workflow/transitions.py +658 -0
- ref_agents-1.0.0.dist-info/METADATA +365 -0
- ref_agents-1.0.0.dist-info/RECORD +175 -0
- ref_agents-1.0.0.dist-info/WHEEL +4 -0
- ref_agents-1.0.0.dist-info/entry_points.txt +2 -0
- ref_agents-1.0.0.dist-info/licenses/LICENSE +115 -0
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
"""Dead Code Scanner for REF Agents.
|
|
2
|
+
|
|
3
|
+
Detects unused code:
|
|
4
|
+
- Unused imports
|
|
5
|
+
- Unused local functions (not exported)
|
|
6
|
+
- Orphan files (never imported)
|
|
7
|
+
|
|
8
|
+
Supports Python (AST), TypeScript, JavaScript, Java (regex).
|
|
9
|
+
|
|
10
|
+
Respects .gitignore and .dockerignore to avoid false positives.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import ast
|
|
14
|
+
import re
|
|
15
|
+
from typing import Any
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import structlog
|
|
20
|
+
|
|
21
|
+
from ref_agents.constants import Severity
|
|
22
|
+
from ref_agents.core.language_detector import LanguageDetector, LANGUAGE_EXTENSIONS
|
|
23
|
+
from ref_agents.tools.debt_scanner import DebtItem, ScanResult
|
|
24
|
+
from ref_agents.utils.ignore_matcher import IgnorePatternMatcher
|
|
25
|
+
|
|
26
|
+
logger = structlog.get_logger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class ImportInfo:
|
|
31
|
+
"""Information about an import statement.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
module: Module being imported.
|
|
35
|
+
names: Names imported from module.
|
|
36
|
+
file_path: File containing import.
|
|
37
|
+
line_number: Line number of import.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
module: str
|
|
41
|
+
names: list[str]
|
|
42
|
+
file_path: Path
|
|
43
|
+
line_number: int
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class FunctionInfo:
|
|
48
|
+
"""Information about a function definition.
|
|
49
|
+
|
|
50
|
+
Attributes:
|
|
51
|
+
name: Function name.
|
|
52
|
+
file_path: File containing function.
|
|
53
|
+
line_number: Line number of definition.
|
|
54
|
+
is_private: True if name starts with _.
|
|
55
|
+
is_exported: True if referenced in __all__.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
name: str
|
|
59
|
+
file_path: Path
|
|
60
|
+
line_number: int
|
|
61
|
+
is_private: bool = False
|
|
62
|
+
is_exported: bool = False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class DeadCodeResult:
|
|
67
|
+
"""Result of dead code scan.
|
|
68
|
+
|
|
69
|
+
Attributes:
|
|
70
|
+
unused_imports: List of unused imports.
|
|
71
|
+
orphan_files: Files not imported anywhere.
|
|
72
|
+
unused_functions: Functions never called.
|
|
73
|
+
files_scanned: Number of files analyzed.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
unused_imports: list[DebtItem] = field(default_factory=list)
|
|
77
|
+
orphan_files: list[DebtItem] = field(default_factory=list)
|
|
78
|
+
unused_functions: list[DebtItem] = field(default_factory=list)
|
|
79
|
+
files_scanned: int = 0
|
|
80
|
+
|
|
81
|
+
def to_scan_result(self) -> ScanResult:
|
|
82
|
+
"""Convert to ScanResult for integration with debt scanner."""
|
|
83
|
+
result = ScanResult(files_scanned=self.files_scanned)
|
|
84
|
+
result.items.extend(self.unused_imports)
|
|
85
|
+
result.items.extend(self.orphan_files)
|
|
86
|
+
result.items.extend(self.unused_functions)
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def total_items(self) -> int:
|
|
91
|
+
"""Total dead code items found."""
|
|
92
|
+
return (
|
|
93
|
+
len(self.unused_imports)
|
|
94
|
+
+ len(self.orphan_files)
|
|
95
|
+
+ len(self.unused_functions)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class UnusedImportDetector(ast.NodeVisitor):
|
|
100
|
+
"""AST visitor to detect unused imports in a Python file.
|
|
101
|
+
|
|
102
|
+
Attributes:
|
|
103
|
+
imports: Dictionary mapping imported names to line numbers.
|
|
104
|
+
used_names: Set of names used in the file.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(self) -> None:
|
|
108
|
+
"""Initialize detector."""
|
|
109
|
+
self.imports: dict[str, int] = {}
|
|
110
|
+
self.used_names: set[str] = set()
|
|
111
|
+
self._in_import = False
|
|
112
|
+
|
|
113
|
+
def visit_Import(self, node: ast.Import) -> None:
|
|
114
|
+
"""Record import statements."""
|
|
115
|
+
self._in_import = True
|
|
116
|
+
for alias in node.names:
|
|
117
|
+
name = alias.asname if alias.asname else alias.name.split(".")[0]
|
|
118
|
+
self.imports[name] = node.lineno
|
|
119
|
+
self._in_import = False
|
|
120
|
+
self.generic_visit(node)
|
|
121
|
+
|
|
122
|
+
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
|
123
|
+
"""Record from...import statements."""
|
|
124
|
+
self._in_import = True
|
|
125
|
+
for alias in node.names:
|
|
126
|
+
if alias.name == "*":
|
|
127
|
+
# Can't track star imports
|
|
128
|
+
continue
|
|
129
|
+
name = alias.asname if alias.asname else alias.name
|
|
130
|
+
self.imports[name] = node.lineno
|
|
131
|
+
self._in_import = False
|
|
132
|
+
self.generic_visit(node)
|
|
133
|
+
|
|
134
|
+
def visit_Name(self, node: ast.Name) -> None:
|
|
135
|
+
"""Record name usage."""
|
|
136
|
+
if not self._in_import:
|
|
137
|
+
self.used_names.add(node.id)
|
|
138
|
+
self.generic_visit(node)
|
|
139
|
+
|
|
140
|
+
def visit_Attribute(self, node: ast.Attribute) -> None:
|
|
141
|
+
"""Record attribute access (module.attr)."""
|
|
142
|
+
# Get the root name
|
|
143
|
+
current = node
|
|
144
|
+
while isinstance(current, ast.Attribute):
|
|
145
|
+
current = current.value
|
|
146
|
+
if isinstance(current, ast.Name) and not self._in_import:
|
|
147
|
+
self.used_names.add(current.id)
|
|
148
|
+
self.generic_visit(node)
|
|
149
|
+
|
|
150
|
+
def get_unused_imports(self) -> dict[str, int]:
|
|
151
|
+
"""Get imports that are never used.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Dictionary mapping unused import names to line numbers.
|
|
155
|
+
"""
|
|
156
|
+
unused = {}
|
|
157
|
+
for name, lineno in self.imports.items():
|
|
158
|
+
if name not in self.used_names:
|
|
159
|
+
# Skip common false positives
|
|
160
|
+
if name in ("__future__", "annotations", "TYPE_CHECKING"):
|
|
161
|
+
continue
|
|
162
|
+
unused[name] = lineno
|
|
163
|
+
return unused
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class DeadCodeScanner:
|
|
167
|
+
"""Scanner for detecting dead code across a project.
|
|
168
|
+
|
|
169
|
+
Attributes:
|
|
170
|
+
project_root: Root directory of project.
|
|
171
|
+
ignore_matcher: Matcher for .gitignore/.dockerignore patterns.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
def __init__(self, project_root: Path) -> None:
|
|
175
|
+
"""Initialize scanner.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
project_root: Project root directory.
|
|
179
|
+
"""
|
|
180
|
+
self.project_root = project_root.resolve()
|
|
181
|
+
self.ignore_matcher = IgnorePatternMatcher.from_directory(self.project_root)
|
|
182
|
+
self._debt_counter = 0
|
|
183
|
+
|
|
184
|
+
# Excluded directories
|
|
185
|
+
self._exclude_dirs = {
|
|
186
|
+
"__pycache__",
|
|
187
|
+
".git",
|
|
188
|
+
".venv",
|
|
189
|
+
".ref",
|
|
190
|
+
"venv",
|
|
191
|
+
"node_modules",
|
|
192
|
+
".tox",
|
|
193
|
+
"dist",
|
|
194
|
+
"build",
|
|
195
|
+
"target",
|
|
196
|
+
".pytest_cache",
|
|
197
|
+
".mypy_cache",
|
|
198
|
+
".ruff_cache",
|
|
199
|
+
"egg-info",
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
def _next_debt_id(self) -> str:
|
|
203
|
+
"""Generate next debt ID."""
|
|
204
|
+
self._debt_counter += 1
|
|
205
|
+
return f"DEAD-{self._debt_counter:03d}"
|
|
206
|
+
|
|
207
|
+
def _should_skip_file(self, file_path: Path) -> bool:
|
|
208
|
+
"""Check if file should be skipped.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
file_path: Path to check.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
True if file should be skipped.
|
|
215
|
+
"""
|
|
216
|
+
# Check excluded directories
|
|
217
|
+
if any(excluded in file_path.parts for excluded in self._exclude_dirs):
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
# Check ignore patterns
|
|
221
|
+
if self.ignore_matcher.is_ignored(file_path):
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
def _scan_imports_regex(self, content: str, language: str) -> list[tuple[str, int]]:
|
|
227
|
+
"""Extract imports using regex for TS/JS/Java.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
content: File content.
|
|
231
|
+
language: Language name.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
List of (import_name, line_number) tuples.
|
|
235
|
+
"""
|
|
236
|
+
imports: list[tuple[str, int]] = []
|
|
237
|
+
lines = content.splitlines()
|
|
238
|
+
|
|
239
|
+
if language in ("typescript", "javascript"):
|
|
240
|
+
# import ... from '...'
|
|
241
|
+
pattern1 = re.compile(
|
|
242
|
+
r"import\s+(?:\{[^}]+\}|\*\s+as\s+\w+|\w+).*?\s+from\s+['\"]([^'\"]+)['\"]"
|
|
243
|
+
)
|
|
244
|
+
# import '...'
|
|
245
|
+
pattern2 = re.compile(r"import\s+['\"]([^'\"]+)['\"]")
|
|
246
|
+
# require('...')
|
|
247
|
+
pattern3 = re.compile(r"require\(['\"]([^'\"]+)['\"]\)")
|
|
248
|
+
|
|
249
|
+
for i, line in enumerate(lines, 1):
|
|
250
|
+
for pattern in [pattern1, pattern2, pattern3]:
|
|
251
|
+
matches = pattern.findall(line)
|
|
252
|
+
imports.extend((match, i) for match in matches)
|
|
253
|
+
|
|
254
|
+
elif language == "java":
|
|
255
|
+
# import package.Class;
|
|
256
|
+
pattern = re.compile(r"import\s+(?:static\s+)?([\w.]+);")
|
|
257
|
+
for i, line in enumerate(lines, 1):
|
|
258
|
+
matches = pattern.findall(line)
|
|
259
|
+
imports.extend((match, i) for match in matches)
|
|
260
|
+
|
|
261
|
+
elif language == "csharp":
|
|
262
|
+
# using Namespace; / using static Namespace; / using Alias = Namespace;
|
|
263
|
+
pattern = re.compile(r"^using\s+(?:static\s+)?(?:\w+\s*=\s*)?([\w.]+)\s*;")
|
|
264
|
+
for i, line in enumerate(lines, 1):
|
|
265
|
+
match = pattern.match(line.strip())
|
|
266
|
+
if match:
|
|
267
|
+
imports.append((match.group(1), i))
|
|
268
|
+
|
|
269
|
+
return imports
|
|
270
|
+
|
|
271
|
+
def scan_unused_imports(self, file_path: Path) -> list[DebtItem]:
|
|
272
|
+
"""Scan single file for unused imports.
|
|
273
|
+
|
|
274
|
+
Supports Python (AST), TypeScript, JavaScript, Java (regex).
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
file_path: Path to file.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
List of debt items for unused imports.
|
|
281
|
+
"""
|
|
282
|
+
if self._should_skip_file(file_path):
|
|
283
|
+
return []
|
|
284
|
+
|
|
285
|
+
# Detect language
|
|
286
|
+
detector = LanguageDetector()
|
|
287
|
+
lang_info = detector.detect(file_path)
|
|
288
|
+
|
|
289
|
+
if lang_info.language == "unknown":
|
|
290
|
+
return []
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
content = file_path.read_text(encoding="utf-8")
|
|
294
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
295
|
+
logger.debug("file_read_error", file=str(file_path), error=str(e))
|
|
296
|
+
return []
|
|
297
|
+
|
|
298
|
+
items: list[DebtItem] = []
|
|
299
|
+
|
|
300
|
+
if lang_info.language == "python":
|
|
301
|
+
try:
|
|
302
|
+
tree = ast.parse(content)
|
|
303
|
+
except SyntaxError as e:
|
|
304
|
+
logger.debug("file_parse_error", file=str(file_path), error=str(e))
|
|
305
|
+
return []
|
|
306
|
+
|
|
307
|
+
detector_ast = UnusedImportDetector()
|
|
308
|
+
detector_ast.visit(tree)
|
|
309
|
+
unused = detector_ast.get_unused_imports()
|
|
310
|
+
|
|
311
|
+
for name, lineno in unused.items():
|
|
312
|
+
items.append(
|
|
313
|
+
DebtItem(
|
|
314
|
+
debt_id=self._next_debt_id(),
|
|
315
|
+
location=f"{file_path}:{lineno}",
|
|
316
|
+
debt_type="unused-import",
|
|
317
|
+
severity=Severity.LOW,
|
|
318
|
+
description=f"Unused import: `{name}`",
|
|
319
|
+
line_number=lineno,
|
|
320
|
+
language="python",
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
else:
|
|
324
|
+
# For TS/JS/Java, we can detect imports but unused detection is limited
|
|
325
|
+
# without full AST parsing. For now, we'll skip unused import detection
|
|
326
|
+
# for these languages (can be enhanced later with proper parsers).
|
|
327
|
+
logger.debug(
|
|
328
|
+
"unused_import_scan_skipped",
|
|
329
|
+
language=lang_info.language,
|
|
330
|
+
file=str(file_path),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
return items
|
|
334
|
+
|
|
335
|
+
def _collect_all_files(self, dir_path: Path) -> set[Path]:
|
|
336
|
+
"""Collect all supported language files."""
|
|
337
|
+
all_files: set[Path] = set()
|
|
338
|
+
for extensions in LANGUAGE_EXTENSIONS.values():
|
|
339
|
+
for ext in extensions:
|
|
340
|
+
for file_path in dir_path.rglob(f"*{ext}"):
|
|
341
|
+
if self._should_skip_file(file_path):
|
|
342
|
+
continue
|
|
343
|
+
all_files.add(file_path)
|
|
344
|
+
return all_files
|
|
345
|
+
|
|
346
|
+
def _collect_imported_modules(
|
|
347
|
+
self, files: set[Path], detector: LanguageDetector
|
|
348
|
+
) -> set[str]:
|
|
349
|
+
"""Collect all imports across files."""
|
|
350
|
+
imported_modules: set[str] = set()
|
|
351
|
+
for file_path in files:
|
|
352
|
+
lang_info = detector.detect(file_path)
|
|
353
|
+
if lang_info.language == "unknown":
|
|
354
|
+
continue
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
content = file_path.read_text(encoding="utf-8")
|
|
358
|
+
except (OSError, UnicodeDecodeError):
|
|
359
|
+
continue
|
|
360
|
+
|
|
361
|
+
if lang_info.language == "python":
|
|
362
|
+
try:
|
|
363
|
+
tree = ast.parse(content)
|
|
364
|
+
except SyntaxError:
|
|
365
|
+
continue
|
|
366
|
+
|
|
367
|
+
for node in ast.walk(tree):
|
|
368
|
+
if isinstance(node, ast.Import):
|
|
369
|
+
for alias in node.names:
|
|
370
|
+
self._add_module_and_parents(imported_modules, alias.name)
|
|
371
|
+
elif isinstance(node, ast.ImportFrom) and node.module:
|
|
372
|
+
self._add_module_and_parents(imported_modules, node.module)
|
|
373
|
+
else:
|
|
374
|
+
imports = self._scan_imports_regex(content, lang_info.language)
|
|
375
|
+
for import_name, _ in imports:
|
|
376
|
+
self._add_module_and_parents(imported_modules, import_name)
|
|
377
|
+
return imported_modules
|
|
378
|
+
|
|
379
|
+
def _add_module_and_parents(self, modules: set[str], name: str) -> None:
|
|
380
|
+
"""Add module and its parent packages to set."""
|
|
381
|
+
modules.add(name)
|
|
382
|
+
parts = name.split(".")
|
|
383
|
+
for i in range(len(parts)):
|
|
384
|
+
modules.add(".".join(parts[: i + 1]))
|
|
385
|
+
|
|
386
|
+
def _is_entry_point_or_test(self, file_path: Path, lang_info: Any) -> bool:
|
|
387
|
+
"""Check if file is entry point or test."""
|
|
388
|
+
skip_names = {
|
|
389
|
+
"python": (
|
|
390
|
+
"__init__.py",
|
|
391
|
+
"__main__.py",
|
|
392
|
+
"setup.py",
|
|
393
|
+
"conftest.py",
|
|
394
|
+
"manage.py",
|
|
395
|
+
"main.py",
|
|
396
|
+
"app.py",
|
|
397
|
+
"wsgi.py",
|
|
398
|
+
"asgi.py",
|
|
399
|
+
"cli.py",
|
|
400
|
+
"server.py",
|
|
401
|
+
),
|
|
402
|
+
"typescript": ("index.ts", "index.tsx", "main.ts", "app.tsx"),
|
|
403
|
+
"javascript": ("index.js", "index.jsx", "main.js", "app.jsx"),
|
|
404
|
+
"java": ("Main.java", "Application.java"),
|
|
405
|
+
"csharp": (
|
|
406
|
+
"Program.cs",
|
|
407
|
+
"Startup.cs",
|
|
408
|
+
"GlobalUsings.cs",
|
|
409
|
+
"AssemblyInfo.cs",
|
|
410
|
+
),
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if file_path.name in skip_names.get(lang_info.language, ()):
|
|
414
|
+
return True
|
|
415
|
+
|
|
416
|
+
test_patterns = {
|
|
417
|
+
"python": (r"^test_.*\.py$", r".*_test\.py$"),
|
|
418
|
+
"typescript": (r"^.*\.test\.tsx?$", r"^.*\.spec\.tsx?$"),
|
|
419
|
+
"javascript": (r"^.*\.test\.jsx?$", r"^.*\.spec\.jsx?$"),
|
|
420
|
+
"java": (r"^.*Test\.java$", r"^.*Tests\.java$"),
|
|
421
|
+
"csharp": (r"^.*Tests?\.cs$", r"^.*Spec\.cs$"),
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
for pattern in test_patterns.get(lang_info.language, []):
|
|
425
|
+
if re.match(pattern, file_path.name):
|
|
426
|
+
return True
|
|
427
|
+
return False
|
|
428
|
+
|
|
429
|
+
def _get_module_candidates(
|
|
430
|
+
self, file_path: Path, dir_path: Path, language: str
|
|
431
|
+
) -> list[str]:
|
|
432
|
+
"""Get possible module names for a file."""
|
|
433
|
+
try:
|
|
434
|
+
rel_path = file_path.relative_to(dir_path)
|
|
435
|
+
except ValueError:
|
|
436
|
+
return []
|
|
437
|
+
|
|
438
|
+
candidates = []
|
|
439
|
+
if language == "python":
|
|
440
|
+
module_parts = list(rel_path.parts[:-1]) + [rel_path.stem]
|
|
441
|
+
candidates.append(".".join(module_parts))
|
|
442
|
+
if module_parts and module_parts[0] == "src":
|
|
443
|
+
candidates.append(".".join(module_parts[1:]))
|
|
444
|
+
candidates.append(rel_path.stem)
|
|
445
|
+
elif language in ("typescript", "javascript"):
|
|
446
|
+
module_parts = list(rel_path.parts[:-1]) + [rel_path.stem]
|
|
447
|
+
candidates.append("/".join(module_parts))
|
|
448
|
+
candidates.append(".".join(module_parts))
|
|
449
|
+
if module_parts and module_parts[0] == "src":
|
|
450
|
+
candidates.append("/".join(module_parts[1:]))
|
|
451
|
+
candidates.append(".".join(module_parts[1:]))
|
|
452
|
+
candidates.append(rel_path.stem)
|
|
453
|
+
elif language == "java":
|
|
454
|
+
module_parts = list(rel_path.parts[:-1])
|
|
455
|
+
if module_parts:
|
|
456
|
+
candidates.append(".".join(module_parts))
|
|
457
|
+
candidates.append(rel_path.stem)
|
|
458
|
+
|
|
459
|
+
elif language == "csharp":
|
|
460
|
+
# C# files are identified by namespace segments + class name
|
|
461
|
+
module_parts = list(rel_path.parts[:-1]) + [rel_path.stem]
|
|
462
|
+
candidates.append(".".join(module_parts))
|
|
463
|
+
# Strip leading src/ convention
|
|
464
|
+
if module_parts and module_parts[0] == "src":
|
|
465
|
+
candidates.append(".".join(module_parts[1:]))
|
|
466
|
+
candidates.append(rel_path.stem)
|
|
467
|
+
|
|
468
|
+
return candidates
|
|
469
|
+
|
|
470
|
+
def scan_orphan_files(self, directory: Path) -> list[DebtItem]:
|
|
471
|
+
"""Find files that are never imported.
|
|
472
|
+
|
|
473
|
+
Supports Python (AST), TypeScript, JavaScript, Java (regex).
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
directory: Directory to scan.
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
List of debt items for orphan files.
|
|
480
|
+
"""
|
|
481
|
+
dir_path = directory.resolve()
|
|
482
|
+
detector = LanguageDetector()
|
|
483
|
+
|
|
484
|
+
all_files = self._collect_all_files(dir_path)
|
|
485
|
+
imported_modules = self._collect_imported_modules(all_files, detector)
|
|
486
|
+
|
|
487
|
+
items = []
|
|
488
|
+
for file_path in all_files:
|
|
489
|
+
lang_info = detector.detect(file_path)
|
|
490
|
+
if lang_info.language == "unknown":
|
|
491
|
+
continue
|
|
492
|
+
|
|
493
|
+
if self._is_entry_point_or_test(file_path, lang_info):
|
|
494
|
+
continue
|
|
495
|
+
|
|
496
|
+
possible_modules = self._get_module_candidates(
|
|
497
|
+
file_path, dir_path, lang_info.language
|
|
498
|
+
)
|
|
499
|
+
is_imported = any(mod in imported_modules for mod in possible_modules)
|
|
500
|
+
|
|
501
|
+
if not is_imported:
|
|
502
|
+
# Use relative path for display
|
|
503
|
+
try:
|
|
504
|
+
display_path = str(file_path.relative_to(dir_path))
|
|
505
|
+
except ValueError:
|
|
506
|
+
display_path = str(file_path)
|
|
507
|
+
|
|
508
|
+
items.append(
|
|
509
|
+
DebtItem(
|
|
510
|
+
debt_id=self._next_debt_id(),
|
|
511
|
+
location=str(file_path),
|
|
512
|
+
debt_type="orphan-file",
|
|
513
|
+
severity=Severity.MEDIUM,
|
|
514
|
+
description=f"File never imported: `{display_path}`",
|
|
515
|
+
line_number=None,
|
|
516
|
+
language=lang_info.language,
|
|
517
|
+
)
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
return items
|
|
521
|
+
|
|
522
|
+
def scan_directory(self, directory: Path | str) -> DeadCodeResult:
|
|
523
|
+
"""Scan directory for all dead code patterns.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
directory: Directory to scan.
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
DeadCodeResult with all findings.
|
|
530
|
+
"""
|
|
531
|
+
dir_path = Path(directory).resolve()
|
|
532
|
+
result = DeadCodeResult()
|
|
533
|
+
|
|
534
|
+
if not dir_path.exists():
|
|
535
|
+
logger.error("directory_not_found", directory=str(directory))
|
|
536
|
+
return result
|
|
537
|
+
|
|
538
|
+
# Scan for unused imports in all supported language files
|
|
539
|
+
for extensions in LANGUAGE_EXTENSIONS.values():
|
|
540
|
+
for ext in extensions:
|
|
541
|
+
for file_path in dir_path.rglob(f"*{ext}"):
|
|
542
|
+
if self._should_skip_file(file_path):
|
|
543
|
+
continue
|
|
544
|
+
|
|
545
|
+
result.files_scanned += 1
|
|
546
|
+
unused_imports = self.scan_unused_imports(file_path)
|
|
547
|
+
result.unused_imports.extend(unused_imports)
|
|
548
|
+
|
|
549
|
+
# Scan for orphan files
|
|
550
|
+
orphan_files = self.scan_orphan_files(dir_path)
|
|
551
|
+
result.orphan_files.extend(orphan_files)
|
|
552
|
+
|
|
553
|
+
logger.info(
|
|
554
|
+
"dead_code_scan_complete",
|
|
555
|
+
files_scanned=result.files_scanned,
|
|
556
|
+
unused_imports=len(result.unused_imports),
|
|
557
|
+
orphan_files=len(result.orphan_files),
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
return result
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def scan_dead_code(directory: str, summary: bool = False) -> str:
|
|
564
|
+
"""Scan directory for dead code.
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
directory: Path to scan.
|
|
568
|
+
summary: If True, return quick summary. If False, return full report.
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
Markdown formatted report or summary.
|
|
572
|
+
"""
|
|
573
|
+
dir_path = Path(directory).resolve()
|
|
574
|
+
scanner = DeadCodeScanner(dir_path)
|
|
575
|
+
result = scanner.scan_directory(dir_path)
|
|
576
|
+
|
|
577
|
+
if result.total_items == 0:
|
|
578
|
+
return f"✅ No dead code found in {result.files_scanned} files."
|
|
579
|
+
|
|
580
|
+
# Summary mode
|
|
581
|
+
if summary:
|
|
582
|
+
return f"""📋 **Dead Code Summary** ({result.files_scanned} files scanned)
|
|
583
|
+
|
|
584
|
+
| Type | Count |
|
|
585
|
+
|------|-------|
|
|
586
|
+
| Unused Imports | {len(result.unused_imports)} |
|
|
587
|
+
| Orphan Files | {len(result.orphan_files)} |
|
|
588
|
+
|
|
589
|
+
**Total:** {result.total_items} items
|
|
590
|
+
|
|
591
|
+
Run `scan_dead_code(directory, summary=False)` for full report."""
|
|
592
|
+
|
|
593
|
+
# Full report mode
|
|
594
|
+
output = f"""# Dead Code Scan Report
|
|
595
|
+
|
|
596
|
+
*Scanned: {result.files_scanned} files*
|
|
597
|
+
*Found: {result.total_items} items*
|
|
598
|
+
|
|
599
|
+
## Summary
|
|
600
|
+
|
|
601
|
+
| Type | Count | Severity |
|
|
602
|
+
|------|-------|----------|
|
|
603
|
+
| Unused Imports | {len(result.unused_imports)} | {Severity.LOW} |
|
|
604
|
+
| Orphan Files | {len(result.orphan_files)} | {Severity.MEDIUM} |
|
|
605
|
+
|
|
606
|
+
"""
|
|
607
|
+
|
|
608
|
+
if result.unused_imports:
|
|
609
|
+
output += "## Unused Imports\n\n"
|
|
610
|
+
output += "| ID | Location | Import |\n"
|
|
611
|
+
output += "|----|----------|--------|\n"
|
|
612
|
+
for item in result.unused_imports:
|
|
613
|
+
# Extract import name from description
|
|
614
|
+
import_name = item.description.replace("Unused import: ", "")
|
|
615
|
+
output += f"| {item.debt_id} | `{item.location}` | {import_name} |\n"
|
|
616
|
+
output += "\n"
|
|
617
|
+
|
|
618
|
+
if result.orphan_files:
|
|
619
|
+
output += "## Orphan Files (Never Imported)\n\n"
|
|
620
|
+
output += "| ID | File | Note |\n"
|
|
621
|
+
output += "|----|------|------|\n"
|
|
622
|
+
for item in result.orphan_files:
|
|
623
|
+
output += f"| {item.debt_id} | `{item.location}` | Verify if needed |\n"
|
|
624
|
+
output += "\n"
|
|
625
|
+
|
|
626
|
+
output += """## Recommendations
|
|
627
|
+
|
|
628
|
+
1. **Unused Imports**: Remove to reduce file size and improve clarity
|
|
629
|
+
2. **Orphan Files**: Verify if file is:
|
|
630
|
+
- Entry point (add to exclusions)
|
|
631
|
+
- Dynamically imported (add comment)
|
|
632
|
+
- Actually unused (safe to delete)
|
|
633
|
+
|
|
634
|
+
*Note: Files in .gitignore/.dockerignore are automatically excluded.*
|
|
635
|
+
"""
|
|
636
|
+
|
|
637
|
+
return output
|