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,436 @@
|
|
|
1
|
+
"""Complexity Scanner - Multi-language complexity analysis.
|
|
2
|
+
|
|
3
|
+
Supports Python (AST-based) and TypeScript/JavaScript/Java (AST or regex-based).
|
|
4
|
+
Uses ConfigLoader to load language-specific thresholds.
|
|
5
|
+
Calculates LOC, cyclomatic/cognitive complexity, function/class counts.
|
|
6
|
+
Implements dual-threshold system: function-level + file-level.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
import fnmatch
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import structlog
|
|
15
|
+
|
|
16
|
+
from ref_agents.core.config_loader import ConfigLoader
|
|
17
|
+
from ref_agents.core.language_detector import LanguageDetector
|
|
18
|
+
|
|
19
|
+
logger = structlog.get_logger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ComplexityVisitor(ast.NodeVisitor):
|
|
23
|
+
"""AST visitor for Python complexity analysis."""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
"""Initialize visitor."""
|
|
27
|
+
self.complexity = 0
|
|
28
|
+
self.functions = 0
|
|
29
|
+
self.classes = 0
|
|
30
|
+
|
|
31
|
+
def visit_FunctionDef(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
|
|
32
|
+
"""Count function and base complexity."""
|
|
33
|
+
self.functions += 1
|
|
34
|
+
self.complexity += 1 # Base complexity
|
|
35
|
+
self.generic_visit(node)
|
|
36
|
+
|
|
37
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
38
|
+
"""Count async function."""
|
|
39
|
+
self.visit_FunctionDef(node)
|
|
40
|
+
|
|
41
|
+
def visit_ClassDef(self, node) -> None:
|
|
42
|
+
"""Count class."""
|
|
43
|
+
self.classes += 1
|
|
44
|
+
self.generic_visit(node)
|
|
45
|
+
|
|
46
|
+
def visit_If(self, node) -> None:
|
|
47
|
+
"""Count if statement."""
|
|
48
|
+
self.complexity += 1
|
|
49
|
+
self.generic_visit(node)
|
|
50
|
+
|
|
51
|
+
def visit_For(self, node) -> None:
|
|
52
|
+
"""Count for loop."""
|
|
53
|
+
self.complexity += 1
|
|
54
|
+
self.generic_visit(node)
|
|
55
|
+
|
|
56
|
+
def visit_AsyncFor(self, node) -> None:
|
|
57
|
+
"""Count async for loop."""
|
|
58
|
+
self.complexity += 1
|
|
59
|
+
self.generic_visit(node)
|
|
60
|
+
|
|
61
|
+
def visit_While(self, node) -> None:
|
|
62
|
+
"""Count while loop."""
|
|
63
|
+
self.complexity += 1
|
|
64
|
+
self.generic_visit(node)
|
|
65
|
+
|
|
66
|
+
def visit_Try(self, node) -> None:
|
|
67
|
+
"""Count try block."""
|
|
68
|
+
self.complexity += 1
|
|
69
|
+
self.generic_visit(node)
|
|
70
|
+
|
|
71
|
+
def visit_ExceptHandler(self, node) -> None:
|
|
72
|
+
"""Count exception handler."""
|
|
73
|
+
self.complexity += 1
|
|
74
|
+
self.generic_visit(node)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _analyze_python_ast(content: str) -> dict:
|
|
78
|
+
"""Analyze Python file using AST.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
content: File content.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Dict with complexity metrics.
|
|
85
|
+
"""
|
|
86
|
+
tree = ast.parse(content)
|
|
87
|
+
visitor = ComplexityVisitor()
|
|
88
|
+
visitor.visit(tree)
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
"complexity": visitor.complexity,
|
|
92
|
+
"functions": visitor.functions,
|
|
93
|
+
"classes": visitor.classes,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _analyze_regex_based(content: str, language: str) -> dict:
|
|
98
|
+
"""Analyze file using regex patterns (TypeScript/JavaScript/Java).
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
content: File content.
|
|
102
|
+
language: Language name.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Dict with complexity metrics.
|
|
106
|
+
"""
|
|
107
|
+
complexity = 0
|
|
108
|
+
functions = 0
|
|
109
|
+
classes = 0
|
|
110
|
+
|
|
111
|
+
# Count functions/methods
|
|
112
|
+
if language in ("typescript", "javascript"):
|
|
113
|
+
# Function patterns: function name(), arrow functions, async functions
|
|
114
|
+
function_patterns = [
|
|
115
|
+
r"\bfunction\s+\w+\s*\(",
|
|
116
|
+
r"\bconst\s+\w+\s*=\s*(async\s+)?\([^)]*\)\s*=>",
|
|
117
|
+
r"\bconst\s+\w+\s*=\s*(async\s+)?function",
|
|
118
|
+
r"\bexport\s+(async\s+)?function",
|
|
119
|
+
r"\bexport\s+const\s+\w+\s*=\s*(async\s+)?\([^)]*\)\s*=>",
|
|
120
|
+
]
|
|
121
|
+
for pattern in function_patterns:
|
|
122
|
+
functions += len(re.findall(pattern, content, re.MULTILINE))
|
|
123
|
+
|
|
124
|
+
# Class patterns
|
|
125
|
+
class_pattern = r"\b(class|interface|type)\s+\w+"
|
|
126
|
+
classes = len(re.findall(class_pattern, content, re.MULTILINE))
|
|
127
|
+
|
|
128
|
+
elif language == "java":
|
|
129
|
+
# Method patterns: public/private/protected returnType methodName(
|
|
130
|
+
method_pattern = r"\b(public|private|protected|static)\s+.*?\s+\w+\s*\("
|
|
131
|
+
functions = len(re.findall(method_pattern, content, re.MULTILINE))
|
|
132
|
+
|
|
133
|
+
# Class patterns
|
|
134
|
+
class_pattern = r"\b(public\s+)?(abstract\s+)?(final\s+)?class\s+\w+"
|
|
135
|
+
classes = len(re.findall(class_pattern, content, re.MULTILINE))
|
|
136
|
+
|
|
137
|
+
elif language == "csharp":
|
|
138
|
+
# Method patterns: access modifier + return type + name(
|
|
139
|
+
method_pattern = (
|
|
140
|
+
r"\b(public|private|protected|internal|static|override|virtual|abstract)"
|
|
141
|
+
r"\s+.*?\s+\w+\s*\("
|
|
142
|
+
)
|
|
143
|
+
functions = len(re.findall(method_pattern, content, re.MULTILINE))
|
|
144
|
+
|
|
145
|
+
# Class, interface, struct, record declarations
|
|
146
|
+
class_pattern = (
|
|
147
|
+
r"\b(public|internal|private|protected|abstract|sealed|partial|static)?"
|
|
148
|
+
r"\s*(partial\s+)?(class|interface|struct|record)\s+\w+"
|
|
149
|
+
)
|
|
150
|
+
classes = len(re.findall(class_pattern, content, re.MULTILINE))
|
|
151
|
+
|
|
152
|
+
# Count complexity contributors (if, for, while, switch, catch, ternary)
|
|
153
|
+
complexity_patterns = [
|
|
154
|
+
r"\bif\s*\(",
|
|
155
|
+
r"\bfor\s*\(",
|
|
156
|
+
r"\bwhile\s*\(",
|
|
157
|
+
r"\bswitch\s*\(",
|
|
158
|
+
r"\bcatch\s*\(",
|
|
159
|
+
r"\?\s*.*\s*:",
|
|
160
|
+
]
|
|
161
|
+
for pattern in complexity_patterns:
|
|
162
|
+
complexity += len(re.findall(pattern, content, re.MULTILINE))
|
|
163
|
+
|
|
164
|
+
# Base complexity = number of functions
|
|
165
|
+
complexity += functions
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
"complexity": complexity,
|
|
169
|
+
"functions": functions,
|
|
170
|
+
"classes": classes,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def analyze_file(file_path: Path, config_loader: ConfigLoader | None = None) -> dict:
|
|
175
|
+
"""Analyze file for complexity metrics.
|
|
176
|
+
|
|
177
|
+
Supports Python (AST), TypeScript, JavaScript, Java (regex).
|
|
178
|
+
Uses ConfigLoader to get language-specific thresholds.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
file_path: Path to file.
|
|
182
|
+
config_loader: Optional ConfigLoader instance. Creates default if None.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Dict with loc, complexity, functions, classes, thresholds, error.
|
|
186
|
+
"""
|
|
187
|
+
if config_loader is None:
|
|
188
|
+
config_loader = ConfigLoader()
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
if config_loader is None:
|
|
192
|
+
config_loader = ConfigLoader()
|
|
193
|
+
|
|
194
|
+
# Detect language first
|
|
195
|
+
detector = LanguageDetector()
|
|
196
|
+
lang_info = detector.detect(file_path)
|
|
197
|
+
|
|
198
|
+
if lang_info.language == "unknown":
|
|
199
|
+
# Can't check exclusions without language, proceed with basic analysis
|
|
200
|
+
content = file_path.read_text(encoding="utf-8")
|
|
201
|
+
loc = len(content.splitlines())
|
|
202
|
+
return {
|
|
203
|
+
"loc": loc,
|
|
204
|
+
"complexity": 0,
|
|
205
|
+
"functions": 0,
|
|
206
|
+
"classes": 0,
|
|
207
|
+
"error": f"Unknown language for file: {file_path}",
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# Load config to check exclusions (before reading file content)
|
|
211
|
+
try:
|
|
212
|
+
config = config_loader.load(lang_info.language, lang_info.framework)
|
|
213
|
+
except FileNotFoundError:
|
|
214
|
+
logger.warning(
|
|
215
|
+
"config_not_found",
|
|
216
|
+
language=lang_info.language,
|
|
217
|
+
file=str(file_path),
|
|
218
|
+
)
|
|
219
|
+
from ref_agents.core.config_models import LanguageConfig, Thresholds
|
|
220
|
+
|
|
221
|
+
config = LanguageConfig(
|
|
222
|
+
language=lang_info.language, thresholds=Thresholds()
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Check skip_analysis exclusions (before reading file)
|
|
226
|
+
if config.exclusions and "skip_analysis" in config.exclusions:
|
|
227
|
+
file_str = str(file_path)
|
|
228
|
+
file_name = file_path.name
|
|
229
|
+
for pattern in config.exclusions["skip_analysis"]:
|
|
230
|
+
# Check both full path and filename
|
|
231
|
+
if fnmatch.fnmatch(file_str, pattern) or fnmatch.fnmatch(
|
|
232
|
+
file_name, pattern
|
|
233
|
+
):
|
|
234
|
+
logger.debug(
|
|
235
|
+
"file_excluded_from_analysis",
|
|
236
|
+
file=str(file_path),
|
|
237
|
+
pattern=pattern,
|
|
238
|
+
)
|
|
239
|
+
return {
|
|
240
|
+
"loc": 0,
|
|
241
|
+
"complexity": 0,
|
|
242
|
+
"functions": 0,
|
|
243
|
+
"classes": 0,
|
|
244
|
+
"error": None,
|
|
245
|
+
"excluded": True,
|
|
246
|
+
"exclusion_reason": f"Matches skip pattern: {pattern}",
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
# Read file content after exclusion check
|
|
250
|
+
content = file_path.read_text(encoding="utf-8")
|
|
251
|
+
loc = len(content.splitlines())
|
|
252
|
+
|
|
253
|
+
# Get thresholds
|
|
254
|
+
thresholds = config.thresholds
|
|
255
|
+
|
|
256
|
+
# Analyze based on language and parser type
|
|
257
|
+
if lang_info.language == "python":
|
|
258
|
+
metrics = _analyze_python_ast(content)
|
|
259
|
+
per_function = None # Python doesn't track per-function yet
|
|
260
|
+
elif lang_info.language in ("typescript", "javascript"):
|
|
261
|
+
# Use AST parser if configured, else regex fallback
|
|
262
|
+
if config.parser == "ast":
|
|
263
|
+
from ref_agents.tools.complexity_ast import analyze_typescript_ast
|
|
264
|
+
|
|
265
|
+
ast_result = analyze_typescript_ast(file_path, content)
|
|
266
|
+
if ast_result.error:
|
|
267
|
+
logger.warning(
|
|
268
|
+
"ast_fallback_to_regex",
|
|
269
|
+
error=ast_result.error,
|
|
270
|
+
file=str(file_path),
|
|
271
|
+
)
|
|
272
|
+
metrics = _analyze_regex_based(content, lang_info.language)
|
|
273
|
+
per_function = None
|
|
274
|
+
else:
|
|
275
|
+
# Convert AST result to metrics format
|
|
276
|
+
metrics = {
|
|
277
|
+
"complexity": ast_result.file_complexity,
|
|
278
|
+
"functions": len(ast_result.functions),
|
|
279
|
+
"classes": ast_result.classes,
|
|
280
|
+
}
|
|
281
|
+
per_function = [
|
|
282
|
+
{"name": f.name, "complexity": f.complexity, "line": f.line}
|
|
283
|
+
for f in ast_result.functions
|
|
284
|
+
]
|
|
285
|
+
else:
|
|
286
|
+
metrics = _analyze_regex_based(content, lang_info.language)
|
|
287
|
+
per_function = None
|
|
288
|
+
elif lang_info.language == "java":
|
|
289
|
+
metrics = _analyze_regex_based(content, lang_info.language)
|
|
290
|
+
per_function = None
|
|
291
|
+
elif lang_info.language == "csharp":
|
|
292
|
+
metrics = _analyze_regex_based(content, lang_info.language)
|
|
293
|
+
per_function = None
|
|
294
|
+
else:
|
|
295
|
+
return {
|
|
296
|
+
"loc": loc,
|
|
297
|
+
"complexity": 0,
|
|
298
|
+
"functions": 0,
|
|
299
|
+
"classes": 0,
|
|
300
|
+
"error": f"Unsupported language: {lang_info.language}",
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
# Check exclusions for file-level complexity
|
|
304
|
+
skip_file_complexity = False
|
|
305
|
+
if config.exclusions and "file_complexity" in config.exclusions:
|
|
306
|
+
for pattern in config.exclusions["file_complexity"]:
|
|
307
|
+
if fnmatch.fnmatch(file_path.name, pattern):
|
|
308
|
+
skip_file_complexity = True
|
|
309
|
+
logger.debug(
|
|
310
|
+
"file_excluded_from_complexity",
|
|
311
|
+
file=str(file_path),
|
|
312
|
+
pattern=pattern,
|
|
313
|
+
)
|
|
314
|
+
break
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
"loc": loc,
|
|
318
|
+
"complexity": metrics["complexity"],
|
|
319
|
+
"functions": metrics["functions"],
|
|
320
|
+
"classes": metrics["classes"],
|
|
321
|
+
"per_function": per_function, # List of {name, complexity, line} if available
|
|
322
|
+
"thresholds": {
|
|
323
|
+
"complexity": thresholds.complexity, # Legacy
|
|
324
|
+
"function_complexity_warn": thresholds.function_complexity_warn,
|
|
325
|
+
"function_complexity_block": thresholds.function_complexity_block,
|
|
326
|
+
"file_complexity_warn": thresholds.file_complexity_warn,
|
|
327
|
+
"file_complexity_block": thresholds.file_complexity_block,
|
|
328
|
+
"file_length": thresholds.file_length,
|
|
329
|
+
},
|
|
330
|
+
"skip_file_complexity": skip_file_complexity,
|
|
331
|
+
"error": None,
|
|
332
|
+
}
|
|
333
|
+
except SyntaxError as e:
|
|
334
|
+
return {"error": f"Syntax error: {e}"}
|
|
335
|
+
except Exception as e:
|
|
336
|
+
logger.error("analyze_file_failed", error=str(e))
|
|
337
|
+
raise
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def scan_complexity_tool(files: str) -> str:
|
|
341
|
+
"""Scan files for complexity metrics (LOC, Cognitive/Cyclomatic).
|
|
342
|
+
|
|
343
|
+
Uses dual-threshold system:
|
|
344
|
+
- Function-level: Warn 15, Block 25 (cognitive complexity)
|
|
345
|
+
- File-level: Warn 80, Block 150 (aggregate)
|
|
346
|
+
|
|
347
|
+
Automatically excludes:
|
|
348
|
+
- Autogenerated files (*.generated.ts, *.auto.ts)
|
|
349
|
+
- Hidden directories (.*/)
|
|
350
|
+
- Coverage/build directories (coverage/, dist/, build/)
|
|
351
|
+
- Vendor directories (node_modules/, vendor/, third-party/)
|
|
352
|
+
|
|
353
|
+
Usage: scan_complexity("file1.py,file2.ts")
|
|
354
|
+
"""
|
|
355
|
+
file_list = [f.strip() for f in files.split(",") if f.strip()]
|
|
356
|
+
report = ["🔍 **Code Complexity Scan**\n"]
|
|
357
|
+
|
|
358
|
+
for fname in file_list:
|
|
359
|
+
p = Path(fname)
|
|
360
|
+
if not p.exists():
|
|
361
|
+
report.append(f"❌ `{fname}`: File not found")
|
|
362
|
+
continue
|
|
363
|
+
|
|
364
|
+
metrics = analyze_file(p)
|
|
365
|
+
|
|
366
|
+
# Skip excluded files silently (autogenerated, vendor, etc.)
|
|
367
|
+
if metrics.get("excluded"):
|
|
368
|
+
continue
|
|
369
|
+
|
|
370
|
+
if metrics.get("error"):
|
|
371
|
+
report.append(f"❌ `{fname}`: Parse Error: {metrics['error']}")
|
|
372
|
+
continue
|
|
373
|
+
|
|
374
|
+
thresholds = metrics.get("thresholds", {})
|
|
375
|
+
skip_file = metrics.get("skip_file_complexity", False)
|
|
376
|
+
|
|
377
|
+
# File-level checks
|
|
378
|
+
loc_status = (
|
|
379
|
+
"🔴" if metrics["loc"] > thresholds.get("file_length", 500) else "🟢"
|
|
380
|
+
)
|
|
381
|
+
file_comp = metrics["complexity"]
|
|
382
|
+
|
|
383
|
+
report.append(f"### `{p.name}`")
|
|
384
|
+
report.append(
|
|
385
|
+
f"- LOC: {metrics['loc']} {loc_status} (Limit: {thresholds.get('file_length', 500)})"
|
|
386
|
+
)
|
|
387
|
+
report.append(f"- Functions: {metrics['functions']}")
|
|
388
|
+
report.append(f"- Classes: {metrics['classes']}")
|
|
389
|
+
|
|
390
|
+
# File-level complexity (if not excluded)
|
|
391
|
+
if not skip_file:
|
|
392
|
+
file_warn = thresholds.get("file_complexity_warn", 80)
|
|
393
|
+
file_block = thresholds.get("file_complexity_block", 150)
|
|
394
|
+
|
|
395
|
+
if file_comp >= file_block:
|
|
396
|
+
file_status = "🔴 BLOCKER"
|
|
397
|
+
report.append(
|
|
398
|
+
f"- File Complexity: {file_comp} {file_status} (Block: {file_block}, Warn: {file_warn})"
|
|
399
|
+
)
|
|
400
|
+
elif file_comp >= file_warn:
|
|
401
|
+
file_status = "🟡 WARNING"
|
|
402
|
+
report.append(
|
|
403
|
+
f"- File Complexity: {file_comp} {file_status} (Block: {file_block}, Warn: {file_warn})"
|
|
404
|
+
)
|
|
405
|
+
else:
|
|
406
|
+
file_status = "🟢 OK"
|
|
407
|
+
report.append(
|
|
408
|
+
f"- File Complexity: {file_comp} {file_status} (Block: {file_block}, Warn: {file_warn})"
|
|
409
|
+
)
|
|
410
|
+
else:
|
|
411
|
+
report.append(f"- File Complexity: {file_comp} ⚪ EXCLUDED")
|
|
412
|
+
|
|
413
|
+
# Per-function complexity (if available)
|
|
414
|
+
per_function = metrics.get("per_function")
|
|
415
|
+
if per_function:
|
|
416
|
+
func_warn = thresholds.get("function_complexity_warn", 15)
|
|
417
|
+
func_block = thresholds.get("function_complexity_block", 25)
|
|
418
|
+
|
|
419
|
+
violations = [f for f in per_function if f["complexity"] >= func_warn]
|
|
420
|
+
|
|
421
|
+
if violations:
|
|
422
|
+
report.append("")
|
|
423
|
+
report.append("**Function-Level Violations:**")
|
|
424
|
+
for func in violations:
|
|
425
|
+
if func["complexity"] >= func_block:
|
|
426
|
+
status = "🔴 BLOCKER"
|
|
427
|
+
else:
|
|
428
|
+
status = "🟡 WARNING"
|
|
429
|
+
report.append(
|
|
430
|
+
f"- `{func['name']}` (line {func['line']}): "
|
|
431
|
+
f"{func['complexity']} {status} (Block: {func_block}, Warn: {func_warn})"
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
report.append("")
|
|
435
|
+
|
|
436
|
+
return "\n".join(report)
|