mcp-vector-search 0.12.6__py3-none-any.whl → 1.1.22__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.
- mcp_vector_search/__init__.py +3 -3
- mcp_vector_search/analysis/__init__.py +111 -0
- mcp_vector_search/analysis/baseline/__init__.py +68 -0
- mcp_vector_search/analysis/baseline/comparator.py +462 -0
- mcp_vector_search/analysis/baseline/manager.py +621 -0
- mcp_vector_search/analysis/collectors/__init__.py +74 -0
- mcp_vector_search/analysis/collectors/base.py +164 -0
- mcp_vector_search/analysis/collectors/cohesion.py +463 -0
- mcp_vector_search/analysis/collectors/complexity.py +743 -0
- mcp_vector_search/analysis/collectors/coupling.py +1162 -0
- mcp_vector_search/analysis/collectors/halstead.py +514 -0
- mcp_vector_search/analysis/collectors/smells.py +325 -0
- mcp_vector_search/analysis/debt.py +516 -0
- mcp_vector_search/analysis/interpretation.py +685 -0
- mcp_vector_search/analysis/metrics.py +414 -0
- mcp_vector_search/analysis/reporters/__init__.py +7 -0
- mcp_vector_search/analysis/reporters/console.py +646 -0
- mcp_vector_search/analysis/reporters/markdown.py +480 -0
- mcp_vector_search/analysis/reporters/sarif.py +377 -0
- mcp_vector_search/analysis/storage/__init__.py +93 -0
- mcp_vector_search/analysis/storage/metrics_store.py +762 -0
- mcp_vector_search/analysis/storage/schema.py +245 -0
- mcp_vector_search/analysis/storage/trend_tracker.py +560 -0
- mcp_vector_search/analysis/trends.py +308 -0
- mcp_vector_search/analysis/visualizer/__init__.py +90 -0
- mcp_vector_search/analysis/visualizer/d3_data.py +534 -0
- mcp_vector_search/analysis/visualizer/exporter.py +484 -0
- mcp_vector_search/analysis/visualizer/html_report.py +2895 -0
- mcp_vector_search/analysis/visualizer/schemas.py +525 -0
- mcp_vector_search/cli/commands/analyze.py +1062 -0
- mcp_vector_search/cli/commands/chat.py +1455 -0
- mcp_vector_search/cli/commands/index.py +621 -5
- mcp_vector_search/cli/commands/index_background.py +467 -0
- mcp_vector_search/cli/commands/init.py +13 -0
- mcp_vector_search/cli/commands/install.py +597 -335
- mcp_vector_search/cli/commands/install_old.py +8 -4
- mcp_vector_search/cli/commands/mcp.py +78 -6
- mcp_vector_search/cli/commands/reset.py +68 -26
- mcp_vector_search/cli/commands/search.py +224 -8
- mcp_vector_search/cli/commands/setup.py +1184 -0
- mcp_vector_search/cli/commands/status.py +339 -5
- mcp_vector_search/cli/commands/uninstall.py +276 -357
- mcp_vector_search/cli/commands/visualize/__init__.py +39 -0
- mcp_vector_search/cli/commands/visualize/cli.py +292 -0
- mcp_vector_search/cli/commands/visualize/exporters/__init__.py +12 -0
- mcp_vector_search/cli/commands/visualize/exporters/html_exporter.py +33 -0
- mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +33 -0
- mcp_vector_search/cli/commands/visualize/graph_builder.py +647 -0
- mcp_vector_search/cli/commands/visualize/layout_engine.py +469 -0
- mcp_vector_search/cli/commands/visualize/server.py +600 -0
- mcp_vector_search/cli/commands/visualize/state_manager.py +428 -0
- mcp_vector_search/cli/commands/visualize/templates/__init__.py +16 -0
- mcp_vector_search/cli/commands/visualize/templates/base.py +234 -0
- mcp_vector_search/cli/commands/visualize/templates/scripts.py +4542 -0
- mcp_vector_search/cli/commands/visualize/templates/styles.py +2522 -0
- mcp_vector_search/cli/didyoumean.py +27 -2
- mcp_vector_search/cli/main.py +127 -160
- mcp_vector_search/cli/output.py +158 -13
- mcp_vector_search/config/__init__.py +4 -0
- mcp_vector_search/config/default_thresholds.yaml +52 -0
- mcp_vector_search/config/settings.py +12 -0
- mcp_vector_search/config/thresholds.py +273 -0
- mcp_vector_search/core/__init__.py +16 -0
- mcp_vector_search/core/auto_indexer.py +3 -3
- mcp_vector_search/core/boilerplate.py +186 -0
- mcp_vector_search/core/config_utils.py +394 -0
- mcp_vector_search/core/database.py +406 -94
- mcp_vector_search/core/embeddings.py +24 -0
- mcp_vector_search/core/exceptions.py +11 -0
- mcp_vector_search/core/git.py +380 -0
- mcp_vector_search/core/git_hooks.py +4 -4
- mcp_vector_search/core/indexer.py +632 -54
- mcp_vector_search/core/llm_client.py +756 -0
- mcp_vector_search/core/models.py +91 -1
- mcp_vector_search/core/project.py +17 -0
- mcp_vector_search/core/relationships.py +473 -0
- mcp_vector_search/core/scheduler.py +11 -11
- mcp_vector_search/core/search.py +179 -29
- mcp_vector_search/mcp/server.py +819 -9
- mcp_vector_search/parsers/python.py +285 -5
- mcp_vector_search/utils/__init__.py +2 -0
- mcp_vector_search/utils/gitignore.py +0 -3
- mcp_vector_search/utils/gitignore_updater.py +212 -0
- mcp_vector_search/utils/monorepo.py +66 -4
- mcp_vector_search/utils/timing.py +10 -6
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/METADATA +184 -53
- mcp_vector_search-1.1.22.dist-info/RECORD +120 -0
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/WHEEL +1 -1
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/entry_points.txt +1 -0
- mcp_vector_search/cli/commands/visualize.py +0 -1467
- mcp_vector_search-0.12.6.dist-info/RECORD +0 -68
- {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
"""Complexity metric collectors for structural code analysis.
|
|
2
|
+
|
|
3
|
+
This module provides collectors for various complexity metrics:
|
|
4
|
+
- CognitiveComplexityCollector: Measures how hard code is to understand
|
|
5
|
+
- CyclomaticComplexityCollector: Counts independent execution paths
|
|
6
|
+
- NestingDepthCollector: Tracks maximum nesting level
|
|
7
|
+
- ParameterCountCollector: Counts function parameters
|
|
8
|
+
- MethodCountCollector: Counts methods in classes
|
|
9
|
+
|
|
10
|
+
All collectors support multiple languages via tree-sitter node type mappings.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
from .base import CollectorContext, MetricCollector
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from tree_sitter import Node
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Multi-language node type mappings
|
|
24
|
+
# Maps logical categories to language-specific tree-sitter node types
|
|
25
|
+
LANGUAGE_NODE_TYPES = {
|
|
26
|
+
"python": {
|
|
27
|
+
"function_def": ["function_definition"],
|
|
28
|
+
"class_def": ["class_definition"],
|
|
29
|
+
"if_statement": ["if_statement"],
|
|
30
|
+
"elif_clause": ["elif_clause"],
|
|
31
|
+
"else_clause": ["else_clause"],
|
|
32
|
+
"for_loop": ["for_statement"],
|
|
33
|
+
"while_loop": ["while_statement"],
|
|
34
|
+
"try_statement": ["try_statement"],
|
|
35
|
+
"except_clause": ["except_clause"],
|
|
36
|
+
"with_statement": ["with_statement"],
|
|
37
|
+
"match_statement": ["match_statement"],
|
|
38
|
+
"case_clause": ["case_clause"],
|
|
39
|
+
"ternary": ["conditional_expression"],
|
|
40
|
+
"boolean_and": ["and"],
|
|
41
|
+
"boolean_or": ["or"],
|
|
42
|
+
"parameters": ["parameters"],
|
|
43
|
+
"break": ["break_statement"],
|
|
44
|
+
"continue": ["continue_statement"],
|
|
45
|
+
"return": ["return_statement"],
|
|
46
|
+
},
|
|
47
|
+
"javascript": {
|
|
48
|
+
"function_def": [
|
|
49
|
+
"function_declaration",
|
|
50
|
+
"function",
|
|
51
|
+
"arrow_function",
|
|
52
|
+
"method_definition",
|
|
53
|
+
],
|
|
54
|
+
"class_def": ["class_declaration", "class"],
|
|
55
|
+
"if_statement": ["if_statement"],
|
|
56
|
+
"elif_clause": [], # JS uses else if, not elif
|
|
57
|
+
"else_clause": ["else_clause"],
|
|
58
|
+
"for_loop": ["for_statement", "for_in_statement"],
|
|
59
|
+
"while_loop": ["while_statement"],
|
|
60
|
+
"try_statement": ["try_statement"],
|
|
61
|
+
"except_clause": ["catch_clause"],
|
|
62
|
+
"with_statement": [],
|
|
63
|
+
"match_statement": ["switch_statement"],
|
|
64
|
+
"case_clause": ["switch_case"],
|
|
65
|
+
"ternary": ["ternary_expression"],
|
|
66
|
+
"boolean_and": ["&&"],
|
|
67
|
+
"boolean_or": ["||"],
|
|
68
|
+
"parameters": ["formal_parameters"],
|
|
69
|
+
"break": ["break_statement"],
|
|
70
|
+
"continue": ["continue_statement"],
|
|
71
|
+
"return": ["return_statement"],
|
|
72
|
+
},
|
|
73
|
+
"typescript": {
|
|
74
|
+
"function_def": [
|
|
75
|
+
"function_declaration",
|
|
76
|
+
"function",
|
|
77
|
+
"arrow_function",
|
|
78
|
+
"method_definition",
|
|
79
|
+
"method_signature",
|
|
80
|
+
],
|
|
81
|
+
"class_def": ["class_declaration", "class"],
|
|
82
|
+
"if_statement": ["if_statement"],
|
|
83
|
+
"elif_clause": [],
|
|
84
|
+
"else_clause": ["else_clause"],
|
|
85
|
+
"for_loop": ["for_statement", "for_in_statement"],
|
|
86
|
+
"while_loop": ["while_statement"],
|
|
87
|
+
"try_statement": ["try_statement"],
|
|
88
|
+
"except_clause": ["catch_clause"],
|
|
89
|
+
"with_statement": [],
|
|
90
|
+
"match_statement": ["switch_statement"],
|
|
91
|
+
"case_clause": ["switch_case"],
|
|
92
|
+
"ternary": ["ternary_expression"],
|
|
93
|
+
"boolean_and": ["&&"],
|
|
94
|
+
"boolean_or": ["||"],
|
|
95
|
+
"parameters": ["formal_parameters"],
|
|
96
|
+
"break": ["break_statement"],
|
|
97
|
+
"continue": ["continue_statement"],
|
|
98
|
+
"return": ["return_statement"],
|
|
99
|
+
},
|
|
100
|
+
"java": {
|
|
101
|
+
"function_def": ["method_declaration", "constructor_declaration"],
|
|
102
|
+
"class_def": ["class_declaration", "interface_declaration"],
|
|
103
|
+
"if_statement": ["if_statement"],
|
|
104
|
+
"elif_clause": [], # Java uses else if
|
|
105
|
+
"else_clause": ["else"],
|
|
106
|
+
"for_loop": ["for_statement", "enhanced_for_statement"],
|
|
107
|
+
"while_loop": ["while_statement"],
|
|
108
|
+
"try_statement": ["try_statement"],
|
|
109
|
+
"except_clause": ["catch_clause"],
|
|
110
|
+
"with_statement": ["try_with_resources_statement"],
|
|
111
|
+
"match_statement": ["switch_expression", "switch_statement"],
|
|
112
|
+
"case_clause": ["switch_label"],
|
|
113
|
+
"ternary": ["ternary_expression"],
|
|
114
|
+
"boolean_and": ["&&"],
|
|
115
|
+
"boolean_or": ["||"],
|
|
116
|
+
"parameters": ["formal_parameters"],
|
|
117
|
+
"break": ["break_statement"],
|
|
118
|
+
"continue": ["continue_statement"],
|
|
119
|
+
"return": ["return_statement"],
|
|
120
|
+
},
|
|
121
|
+
"rust": {
|
|
122
|
+
"function_def": ["function_item"],
|
|
123
|
+
"class_def": ["struct_item", "impl_item", "trait_item"],
|
|
124
|
+
"if_statement": ["if_expression"],
|
|
125
|
+
"elif_clause": [], # Rust uses else if
|
|
126
|
+
"else_clause": ["else_clause"],
|
|
127
|
+
"for_loop": ["for_expression"],
|
|
128
|
+
"while_loop": ["while_expression"],
|
|
129
|
+
"try_statement": [], # Rust uses Result/Option, not try
|
|
130
|
+
"except_clause": [],
|
|
131
|
+
"with_statement": [],
|
|
132
|
+
"match_statement": ["match_expression"],
|
|
133
|
+
"case_clause": ["match_arm"],
|
|
134
|
+
"ternary": [], # Rust uses if-else expressions
|
|
135
|
+
"boolean_and": ["&&"],
|
|
136
|
+
"boolean_or": ["||"],
|
|
137
|
+
"parameters": ["parameters"],
|
|
138
|
+
"break": ["break_expression"],
|
|
139
|
+
"continue": ["continue_expression"],
|
|
140
|
+
"return": ["return_expression"],
|
|
141
|
+
},
|
|
142
|
+
"php": {
|
|
143
|
+
"function_def": ["function_definition", "method_declaration"],
|
|
144
|
+
"class_def": [
|
|
145
|
+
"class_declaration",
|
|
146
|
+
"interface_declaration",
|
|
147
|
+
"trait_declaration",
|
|
148
|
+
],
|
|
149
|
+
"if_statement": ["if_statement"],
|
|
150
|
+
"elif_clause": ["else_if_clause"],
|
|
151
|
+
"else_clause": ["else_clause"],
|
|
152
|
+
"for_loop": ["for_statement", "foreach_statement"],
|
|
153
|
+
"while_loop": ["while_statement"],
|
|
154
|
+
"try_statement": ["try_statement"],
|
|
155
|
+
"except_clause": ["catch_clause"],
|
|
156
|
+
"with_statement": [],
|
|
157
|
+
"match_statement": ["switch_statement", "match_expression"],
|
|
158
|
+
"case_clause": ["case_statement", "match_arm"],
|
|
159
|
+
"ternary": ["conditional_expression"],
|
|
160
|
+
"boolean_and": ["and", "&&"],
|
|
161
|
+
"boolean_or": ["or", "||"],
|
|
162
|
+
"parameters": ["formal_parameters"],
|
|
163
|
+
"break": ["break_statement"],
|
|
164
|
+
"continue": ["continue_statement"],
|
|
165
|
+
"return": ["return_statement"],
|
|
166
|
+
},
|
|
167
|
+
"ruby": {
|
|
168
|
+
"function_def": ["method", "singleton_method"],
|
|
169
|
+
"class_def": ["class", "module"],
|
|
170
|
+
"if_statement": ["if", "unless"],
|
|
171
|
+
"elif_clause": ["elsif"],
|
|
172
|
+
"else_clause": ["else"],
|
|
173
|
+
"for_loop": ["for"],
|
|
174
|
+
"while_loop": ["while", "until"],
|
|
175
|
+
"try_statement": ["begin"],
|
|
176
|
+
"except_clause": ["rescue"],
|
|
177
|
+
"with_statement": [],
|
|
178
|
+
"match_statement": ["case"],
|
|
179
|
+
"case_clause": ["when"],
|
|
180
|
+
"ternary": ["conditional"],
|
|
181
|
+
"boolean_and": ["and", "&&"],
|
|
182
|
+
"boolean_or": ["or", "||"],
|
|
183
|
+
"parameters": ["method_parameters"],
|
|
184
|
+
"break": ["break"],
|
|
185
|
+
"continue": ["next"],
|
|
186
|
+
"return": ["return"],
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def get_node_types(language: str, category: str) -> list[str]:
|
|
192
|
+
"""Get tree-sitter node types for a given language and category.
|
|
193
|
+
|
|
194
|
+
Provides language-agnostic access to node types by mapping logical
|
|
195
|
+
categories (e.g., "if_statement", "function_def") to language-specific
|
|
196
|
+
tree-sitter node type names.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
language: Programming language identifier (e.g., "python", "javascript")
|
|
200
|
+
category: Logical category of node (e.g., "if_statement", "for_loop")
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
List of node type names for this language/category combination.
|
|
204
|
+
Returns empty list if language/category not found.
|
|
205
|
+
|
|
206
|
+
Examples:
|
|
207
|
+
>>> get_node_types("python", "function_def")
|
|
208
|
+
["function_definition"]
|
|
209
|
+
|
|
210
|
+
>>> get_node_types("javascript", "function_def")
|
|
211
|
+
["function_declaration", "function", "arrow_function", "method_definition"]
|
|
212
|
+
|
|
213
|
+
>>> get_node_types("unknown_lang", "if_statement")
|
|
214
|
+
[] # Falls back to Python-like behavior
|
|
215
|
+
"""
|
|
216
|
+
# Default to Python-like node types for unknown languages
|
|
217
|
+
lang_mapping = LANGUAGE_NODE_TYPES.get(language, LANGUAGE_NODE_TYPES["python"])
|
|
218
|
+
return lang_mapping.get(category, [])
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class CognitiveComplexityCollector(MetricCollector):
|
|
222
|
+
"""Tracks cognitive complexity - how hard code is to understand.
|
|
223
|
+
|
|
224
|
+
Cognitive complexity measures the difficulty of understanding code flow
|
|
225
|
+
by penalizing nested control structures and complex boolean logic.
|
|
226
|
+
|
|
227
|
+
Scoring Rules:
|
|
228
|
+
- +1 for each: if, elif, else, for, while, catch/except, ternary
|
|
229
|
+
- +1 for each nesting level (nested if inside if gets +2 total)
|
|
230
|
+
- +1 for each: break, continue, goto
|
|
231
|
+
- +1 for boolean operators: and, or, &&, ||
|
|
232
|
+
- +1 for recursion (function calling itself)
|
|
233
|
+
|
|
234
|
+
The final score indicates code readability:
|
|
235
|
+
- 0-5: Excellent (Grade A)
|
|
236
|
+
- 6-10: Good (Grade B)
|
|
237
|
+
- 11-20: Acceptable (Grade C)
|
|
238
|
+
- 21-30: Needs improvement (Grade D)
|
|
239
|
+
- 31+: Refactor recommended (Grade F)
|
|
240
|
+
|
|
241
|
+
Example:
|
|
242
|
+
Simple function (complexity = 0):
|
|
243
|
+
def add(a, b):
|
|
244
|
+
return a + b
|
|
245
|
+
|
|
246
|
+
Nested conditionals (complexity = 4):
|
|
247
|
+
def process(x): # +0 (function entry)
|
|
248
|
+
if x > 0: # +1 (if) +0 (nesting level 0)
|
|
249
|
+
if x > 10: # +1 (if) +1 (nesting level 1) = +2
|
|
250
|
+
return x
|
|
251
|
+
return 0 # Total: 4
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
def __init__(self) -> None:
|
|
255
|
+
"""Initialize cognitive complexity collector."""
|
|
256
|
+
self._complexity = 0
|
|
257
|
+
self._nesting_level = 0
|
|
258
|
+
self._current_function_name: str | None = None
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def name(self) -> str:
|
|
262
|
+
"""Return collector identifier.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Collector name "cognitive_complexity"
|
|
266
|
+
"""
|
|
267
|
+
return "cognitive_complexity"
|
|
268
|
+
|
|
269
|
+
def collect_node(self, node: Node, context: CollectorContext, depth: int) -> None:
|
|
270
|
+
"""Process node and update cognitive complexity.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
node: Current tree-sitter AST node
|
|
274
|
+
context: Shared context with language and scope info
|
|
275
|
+
depth: Current depth in AST (unused, we track logical nesting)
|
|
276
|
+
"""
|
|
277
|
+
language = context.language
|
|
278
|
+
node_type = node.type
|
|
279
|
+
|
|
280
|
+
# Control flow statements (+1 + nesting level)
|
|
281
|
+
control_flow_categories = [
|
|
282
|
+
"if_statement",
|
|
283
|
+
"elif_clause",
|
|
284
|
+
"else_clause",
|
|
285
|
+
"for_loop",
|
|
286
|
+
"while_loop",
|
|
287
|
+
"except_clause",
|
|
288
|
+
"ternary",
|
|
289
|
+
]
|
|
290
|
+
|
|
291
|
+
for category in control_flow_categories:
|
|
292
|
+
if node_type in get_node_types(language, category):
|
|
293
|
+
# +1 for statement itself, +nesting_level for being nested
|
|
294
|
+
self._complexity += 1 + self._nesting_level
|
|
295
|
+
break
|
|
296
|
+
|
|
297
|
+
# Match/switch statements (+1 + nesting level)
|
|
298
|
+
if node_type in get_node_types(language, "match_statement"):
|
|
299
|
+
self._complexity += 1 + self._nesting_level
|
|
300
|
+
|
|
301
|
+
# Case clauses (+1, no nesting penalty)
|
|
302
|
+
if node_type in get_node_types(language, "case_clause"):
|
|
303
|
+
self._complexity += 1
|
|
304
|
+
|
|
305
|
+
# Jump statements (+1)
|
|
306
|
+
jump_categories = ["break", "continue"]
|
|
307
|
+
for category in jump_categories:
|
|
308
|
+
if node_type in get_node_types(language, category):
|
|
309
|
+
self._complexity += 1
|
|
310
|
+
break
|
|
311
|
+
|
|
312
|
+
# Boolean operators (+1 per operator)
|
|
313
|
+
if node_type in get_node_types(
|
|
314
|
+
language, "boolean_and"
|
|
315
|
+
) or node_type in get_node_types(language, "boolean_or"):
|
|
316
|
+
self._complexity += 1
|
|
317
|
+
|
|
318
|
+
# Track nesting level changes
|
|
319
|
+
nesting_categories = [
|
|
320
|
+
"if_statement",
|
|
321
|
+
"for_loop",
|
|
322
|
+
"while_loop",
|
|
323
|
+
"try_statement",
|
|
324
|
+
"match_statement",
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
for category in nesting_categories:
|
|
328
|
+
if node_type in get_node_types(language, category):
|
|
329
|
+
self._nesting_level += 1
|
|
330
|
+
break
|
|
331
|
+
|
|
332
|
+
# Track function name for recursion detection
|
|
333
|
+
if node_type in get_node_types(language, "function_def"):
|
|
334
|
+
# Extract function name if available
|
|
335
|
+
if hasattr(node, "child_by_field_name"):
|
|
336
|
+
name_node = node.child_by_field_name("name")
|
|
337
|
+
if name_node:
|
|
338
|
+
self._current_function_name = name_node.text.decode("utf-8")
|
|
339
|
+
|
|
340
|
+
def finalize_function(
|
|
341
|
+
self, node: Node, context: CollectorContext
|
|
342
|
+
) -> dict[str, Any]:
|
|
343
|
+
"""Return final cognitive complexity for completed function.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
node: Function definition node
|
|
347
|
+
context: Shared context
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Dictionary with cognitive_complexity metric
|
|
351
|
+
"""
|
|
352
|
+
# TODO: Detect recursion by analyzing function calls
|
|
353
|
+
# This would require looking at all identifier nodes and checking
|
|
354
|
+
# if any match self._current_function_name
|
|
355
|
+
return {"cognitive_complexity": self._complexity}
|
|
356
|
+
|
|
357
|
+
def reset(self) -> None:
|
|
358
|
+
"""Reset collector state for next function."""
|
|
359
|
+
self._complexity = 0
|
|
360
|
+
self._nesting_level = 0
|
|
361
|
+
self._current_function_name = None
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class CyclomaticComplexityCollector(MetricCollector):
|
|
365
|
+
"""Tracks cyclomatic complexity - number of independent execution paths.
|
|
366
|
+
|
|
367
|
+
Cyclomatic complexity measures the number of linearly independent paths
|
|
368
|
+
through code. Higher values indicate more test cases needed for coverage.
|
|
369
|
+
|
|
370
|
+
Scoring Rules:
|
|
371
|
+
- Start with complexity = 1 (single straight-through path)
|
|
372
|
+
- +1 for each: if, elif, for, while, case/match, catch/except
|
|
373
|
+
- +1 for each boolean operator: and, or, &&, ||
|
|
374
|
+
- +1 for each ternary expression
|
|
375
|
+
|
|
376
|
+
Interpretation:
|
|
377
|
+
- 1-4: Simple, low risk
|
|
378
|
+
- 5-7: Moderate complexity
|
|
379
|
+
- 8-10: Complex, higher risk
|
|
380
|
+
- 11+: Very complex, difficult to test
|
|
381
|
+
|
|
382
|
+
Example:
|
|
383
|
+
def check_value(x):
|
|
384
|
+
# complexity = 1 (baseline)
|
|
385
|
+
if x > 0: # +1 = 2
|
|
386
|
+
return "positive"
|
|
387
|
+
elif x < 0: # +1 = 3
|
|
388
|
+
return "negative"
|
|
389
|
+
else:
|
|
390
|
+
return "zero"
|
|
391
|
+
# Total: 3 (three independent paths)
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
def __init__(self) -> None:
|
|
395
|
+
"""Initialize cyclomatic complexity collector."""
|
|
396
|
+
self._complexity = 1 # Start at 1 (baseline path)
|
|
397
|
+
|
|
398
|
+
@property
|
|
399
|
+
def name(self) -> str:
|
|
400
|
+
"""Return collector identifier.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Collector name "cyclomatic_complexity"
|
|
404
|
+
"""
|
|
405
|
+
return "cyclomatic_complexity"
|
|
406
|
+
|
|
407
|
+
def collect_node(self, node: Node, context: CollectorContext, depth: int) -> None:
|
|
408
|
+
"""Process node and update cyclomatic complexity.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
node: Current tree-sitter AST node
|
|
412
|
+
context: Shared context with language info
|
|
413
|
+
depth: Current depth in AST (unused)
|
|
414
|
+
"""
|
|
415
|
+
language = context.language
|
|
416
|
+
node_type = node.type
|
|
417
|
+
|
|
418
|
+
# Decision points (+1 each)
|
|
419
|
+
decision_categories = [
|
|
420
|
+
"if_statement",
|
|
421
|
+
"elif_clause",
|
|
422
|
+
"for_loop",
|
|
423
|
+
"while_loop",
|
|
424
|
+
"except_clause",
|
|
425
|
+
"case_clause",
|
|
426
|
+
"ternary",
|
|
427
|
+
]
|
|
428
|
+
|
|
429
|
+
for category in decision_categories:
|
|
430
|
+
if node_type in get_node_types(language, category):
|
|
431
|
+
self._complexity += 1
|
|
432
|
+
break
|
|
433
|
+
|
|
434
|
+
# Boolean operators (+1 each)
|
|
435
|
+
if node_type in get_node_types(
|
|
436
|
+
language, "boolean_and"
|
|
437
|
+
) or node_type in get_node_types(language, "boolean_or"):
|
|
438
|
+
self._complexity += 1
|
|
439
|
+
|
|
440
|
+
def finalize_function(
|
|
441
|
+
self, node: Node, context: CollectorContext
|
|
442
|
+
) -> dict[str, Any]:
|
|
443
|
+
"""Return final cyclomatic complexity for completed function.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
node: Function definition node
|
|
447
|
+
context: Shared context
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
Dictionary with cyclomatic_complexity metric
|
|
451
|
+
"""
|
|
452
|
+
return {"cyclomatic_complexity": self._complexity}
|
|
453
|
+
|
|
454
|
+
def reset(self) -> None:
|
|
455
|
+
"""Reset collector state for next function."""
|
|
456
|
+
self._complexity = 1 # Reset to baseline
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class NestingDepthCollector(MetricCollector):
|
|
460
|
+
"""Tracks maximum nesting depth of control structures.
|
|
461
|
+
|
|
462
|
+
Nesting depth measures how deeply control structures are nested.
|
|
463
|
+
Deep nesting (>3 levels) indicates code that is hard to read and maintain.
|
|
464
|
+
|
|
465
|
+
Tracked Structures:
|
|
466
|
+
- Functions/methods
|
|
467
|
+
- If/elif/else blocks
|
|
468
|
+
- For/while loops
|
|
469
|
+
- Try/catch/except blocks
|
|
470
|
+
- With/using statements
|
|
471
|
+
- Match/switch statements
|
|
472
|
+
|
|
473
|
+
Interpretation:
|
|
474
|
+
- 0-1: Flat, easy to read
|
|
475
|
+
- 2-3: Acceptable nesting
|
|
476
|
+
- 4-5: High nesting, consider refactoring
|
|
477
|
+
- 6+: Excessive nesting, refactor recommended
|
|
478
|
+
|
|
479
|
+
Example:
|
|
480
|
+
def process(): # depth 0 (not counted in nesting)
|
|
481
|
+
if condition: # depth 1
|
|
482
|
+
for item in items: # depth 2
|
|
483
|
+
while busy: # depth 3
|
|
484
|
+
if ready: # depth 4 (max_nesting = 4)
|
|
485
|
+
process()
|
|
486
|
+
"""
|
|
487
|
+
|
|
488
|
+
def __init__(self) -> None:
|
|
489
|
+
"""Initialize nesting depth collector."""
|
|
490
|
+
self._max_depth = 0
|
|
491
|
+
self._current_depth = 0
|
|
492
|
+
|
|
493
|
+
@property
|
|
494
|
+
def name(self) -> str:
|
|
495
|
+
"""Return collector identifier.
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
Collector name "nesting_depth"
|
|
499
|
+
"""
|
|
500
|
+
return "nesting_depth"
|
|
501
|
+
|
|
502
|
+
def collect_node(self, node: Node, context: CollectorContext, depth: int) -> None:
|
|
503
|
+
"""Process node and track nesting depth changes.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
node: Current tree-sitter AST node
|
|
507
|
+
context: Shared context with language info
|
|
508
|
+
depth: Current depth in AST
|
|
509
|
+
"""
|
|
510
|
+
# Use context nesting stack for accurate tracking
|
|
511
|
+
# The traversal engine should manage this stack
|
|
512
|
+
if context.nesting_stack:
|
|
513
|
+
stack_depth = len(context.nesting_stack)
|
|
514
|
+
self._max_depth = max(self._max_depth, stack_depth)
|
|
515
|
+
|
|
516
|
+
def finalize_function(
|
|
517
|
+
self, node: Node, context: CollectorContext
|
|
518
|
+
) -> dict[str, Any]:
|
|
519
|
+
"""Return maximum nesting depth for completed function.
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
node: Function definition node
|
|
523
|
+
context: Shared context
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
Dictionary with max_nesting_depth metric
|
|
527
|
+
"""
|
|
528
|
+
return {"max_nesting_depth": self._max_depth}
|
|
529
|
+
|
|
530
|
+
def reset(self) -> None:
|
|
531
|
+
"""Reset collector state for next function."""
|
|
532
|
+
self._max_depth = 0
|
|
533
|
+
self._current_depth = 0
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
class ParameterCountCollector(MetricCollector):
|
|
537
|
+
"""Counts function parameters.
|
|
538
|
+
|
|
539
|
+
Parameter count indicates function complexity and potential coupling.
|
|
540
|
+
Functions with many parameters are harder to understand and test.
|
|
541
|
+
|
|
542
|
+
Interpretation:
|
|
543
|
+
- 0-2: Ideal, easy to understand
|
|
544
|
+
- 3-4: Acceptable
|
|
545
|
+
- 5-6: Consider refactoring
|
|
546
|
+
- 7+: Too many parameters, refactor recommended
|
|
547
|
+
|
|
548
|
+
Recommendations for high parameter counts:
|
|
549
|
+
- Introduce parameter objects
|
|
550
|
+
- Use builder pattern
|
|
551
|
+
- Split into smaller functions
|
|
552
|
+
|
|
553
|
+
Example:
|
|
554
|
+
def simple(x, y): # parameter_count = 2
|
|
555
|
+
return x + y
|
|
556
|
+
|
|
557
|
+
def complex(a, b, c, d, e, f, g): # parameter_count = 7 (too many!)
|
|
558
|
+
pass
|
|
559
|
+
"""
|
|
560
|
+
|
|
561
|
+
def __init__(self) -> None:
|
|
562
|
+
"""Initialize parameter count collector."""
|
|
563
|
+
self._parameter_count = 0
|
|
564
|
+
|
|
565
|
+
@property
|
|
566
|
+
def name(self) -> str:
|
|
567
|
+
"""Return collector identifier.
|
|
568
|
+
|
|
569
|
+
Returns:
|
|
570
|
+
Collector name "parameter_count"
|
|
571
|
+
"""
|
|
572
|
+
return "parameter_count"
|
|
573
|
+
|
|
574
|
+
def collect_node(self, node: Node, context: CollectorContext, depth: int) -> None:
|
|
575
|
+
"""Process node and count parameters.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
node: Current tree-sitter AST node
|
|
579
|
+
context: Shared context with language info
|
|
580
|
+
depth: Current depth in AST (unused)
|
|
581
|
+
"""
|
|
582
|
+
language = context.language
|
|
583
|
+
node_type = node.type
|
|
584
|
+
|
|
585
|
+
# Look for parameter list nodes
|
|
586
|
+
if node_type in get_node_types(language, "parameters"):
|
|
587
|
+
# Count child nodes that are parameters
|
|
588
|
+
# Different languages have different parameter node structures
|
|
589
|
+
# Python: named nodes are parameters
|
|
590
|
+
# JavaScript: formal_parameter nodes
|
|
591
|
+
# Java: formal_parameter nodes
|
|
592
|
+
self._parameter_count = self._count_parameters(node, language)
|
|
593
|
+
|
|
594
|
+
def _count_parameters(self, params_node: Node, language: str) -> int:
|
|
595
|
+
"""Count parameters in a parameter list node.
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
params_node: Parameter list node
|
|
599
|
+
language: Programming language
|
|
600
|
+
|
|
601
|
+
Returns:
|
|
602
|
+
Number of parameters
|
|
603
|
+
"""
|
|
604
|
+
count = 0
|
|
605
|
+
|
|
606
|
+
# Iterate through child nodes
|
|
607
|
+
for child in params_node.children:
|
|
608
|
+
# Skip punctuation and keywords (commas, parentheses, etc.)
|
|
609
|
+
if child.type in (",", "(", ")", "self", "cls"):
|
|
610
|
+
continue
|
|
611
|
+
|
|
612
|
+
# Language-specific parameter node types
|
|
613
|
+
param_types = {
|
|
614
|
+
"python": ["identifier", "typed_parameter", "default_parameter"],
|
|
615
|
+
"javascript": ["formal_parameter", "identifier"],
|
|
616
|
+
"typescript": ["required_parameter", "optional_parameter"],
|
|
617
|
+
"java": ["formal_parameter"],
|
|
618
|
+
"rust": ["parameter"],
|
|
619
|
+
"php": ["simple_parameter", "variadic_parameter"],
|
|
620
|
+
"ruby": ["identifier", "optional_parameter"],
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
lang_params = param_types.get(language, ["identifier"])
|
|
624
|
+
|
|
625
|
+
# Check if this child is a parameter node
|
|
626
|
+
if child.type in lang_params or child.is_named:
|
|
627
|
+
count += 1
|
|
628
|
+
|
|
629
|
+
return count
|
|
630
|
+
|
|
631
|
+
def finalize_function(
|
|
632
|
+
self, node: Node, context: CollectorContext
|
|
633
|
+
) -> dict[str, Any]:
|
|
634
|
+
"""Return parameter count for completed function.
|
|
635
|
+
|
|
636
|
+
Args:
|
|
637
|
+
node: Function definition node
|
|
638
|
+
context: Shared context
|
|
639
|
+
|
|
640
|
+
Returns:
|
|
641
|
+
Dictionary with parameter_count metric
|
|
642
|
+
"""
|
|
643
|
+
return {"parameter_count": self._parameter_count}
|
|
644
|
+
|
|
645
|
+
def reset(self) -> None:
|
|
646
|
+
"""Reset collector state for next function."""
|
|
647
|
+
self._parameter_count = 0
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
class MethodCountCollector(MetricCollector):
|
|
651
|
+
"""Counts methods in classes.
|
|
652
|
+
|
|
653
|
+
Method count indicates class complexity and potential violation of
|
|
654
|
+
Single Responsibility Principle. Classes with many methods may need
|
|
655
|
+
to be split into smaller, focused classes.
|
|
656
|
+
|
|
657
|
+
Interpretation:
|
|
658
|
+
- 0-5: Focused class, good
|
|
659
|
+
- 6-10: Moderate complexity
|
|
660
|
+
- 11-15: High complexity, consider refactoring
|
|
661
|
+
- 16+: Too many responsibilities, split class
|
|
662
|
+
|
|
663
|
+
Only counts methods inside classes, not top-level functions.
|
|
664
|
+
|
|
665
|
+
Example:
|
|
666
|
+
class Simple: # method_count = 2
|
|
667
|
+
def __init__(self): pass
|
|
668
|
+
def process(self): pass
|
|
669
|
+
|
|
670
|
+
class Complex: # method_count = 12 (too many!)
|
|
671
|
+
def method1(self): pass
|
|
672
|
+
def method2(self): pass
|
|
673
|
+
# ... 10 more methods
|
|
674
|
+
"""
|
|
675
|
+
|
|
676
|
+
def __init__(self) -> None:
|
|
677
|
+
"""Initialize method count collector."""
|
|
678
|
+
self._method_count = 0
|
|
679
|
+
self._inside_class = False
|
|
680
|
+
|
|
681
|
+
@property
|
|
682
|
+
def name(self) -> str:
|
|
683
|
+
"""Return collector identifier.
|
|
684
|
+
|
|
685
|
+
Returns:
|
|
686
|
+
Collector name "method_count"
|
|
687
|
+
"""
|
|
688
|
+
return "method_count"
|
|
689
|
+
|
|
690
|
+
def collect_node(self, node: Node, context: CollectorContext, depth: int) -> None:
|
|
691
|
+
"""Process node and count methods in classes.
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
node: Current tree-sitter AST node
|
|
695
|
+
context: Shared context with language and class info
|
|
696
|
+
depth: Current depth in AST (unused)
|
|
697
|
+
"""
|
|
698
|
+
language = context.language
|
|
699
|
+
node_type = node.type
|
|
700
|
+
|
|
701
|
+
# Track when we enter/exit a class
|
|
702
|
+
if node_type in get_node_types(language, "class_def"):
|
|
703
|
+
self._inside_class = True
|
|
704
|
+
|
|
705
|
+
# Count function definitions inside classes
|
|
706
|
+
# Use context.current_class as the primary indicator
|
|
707
|
+
if context.current_class and node_type in get_node_types(
|
|
708
|
+
language, "function_def"
|
|
709
|
+
):
|
|
710
|
+
self._method_count += 1
|
|
711
|
+
elif self._inside_class and node_type in get_node_types(
|
|
712
|
+
language, "function_def"
|
|
713
|
+
):
|
|
714
|
+
# Fallback to _inside_class flag if context.current_class not set
|
|
715
|
+
self._method_count += 1
|
|
716
|
+
|
|
717
|
+
def finalize_function(
|
|
718
|
+
self, node: Node, context: CollectorContext
|
|
719
|
+
) -> dict[str, Any]:
|
|
720
|
+
"""Return method count for completed class.
|
|
721
|
+
|
|
722
|
+
Note: This is called when exiting a function, but for classes
|
|
723
|
+
we need to track this differently. For now, return the count
|
|
724
|
+
accumulated so far.
|
|
725
|
+
|
|
726
|
+
Args:
|
|
727
|
+
node: Function/class definition node
|
|
728
|
+
context: Shared context
|
|
729
|
+
|
|
730
|
+
Returns:
|
|
731
|
+
Dictionary with method_count metric (0 for functions, count for classes)
|
|
732
|
+
"""
|
|
733
|
+
# Only return method count if we're in a class
|
|
734
|
+
if context.current_class or self._inside_class:
|
|
735
|
+
return {"method_count": self._method_count}
|
|
736
|
+
|
|
737
|
+
# For regular functions, method_count is 0
|
|
738
|
+
return {"method_count": 0}
|
|
739
|
+
|
|
740
|
+
def reset(self) -> None:
|
|
741
|
+
"""Reset collector state for next class/function."""
|
|
742
|
+
self._method_count = 0
|
|
743
|
+
self._inside_class = False
|