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.
Files changed (92) hide show
  1. mcp_vector_search/__init__.py +3 -3
  2. mcp_vector_search/analysis/__init__.py +111 -0
  3. mcp_vector_search/analysis/baseline/__init__.py +68 -0
  4. mcp_vector_search/analysis/baseline/comparator.py +462 -0
  5. mcp_vector_search/analysis/baseline/manager.py +621 -0
  6. mcp_vector_search/analysis/collectors/__init__.py +74 -0
  7. mcp_vector_search/analysis/collectors/base.py +164 -0
  8. mcp_vector_search/analysis/collectors/cohesion.py +463 -0
  9. mcp_vector_search/analysis/collectors/complexity.py +743 -0
  10. mcp_vector_search/analysis/collectors/coupling.py +1162 -0
  11. mcp_vector_search/analysis/collectors/halstead.py +514 -0
  12. mcp_vector_search/analysis/collectors/smells.py +325 -0
  13. mcp_vector_search/analysis/debt.py +516 -0
  14. mcp_vector_search/analysis/interpretation.py +685 -0
  15. mcp_vector_search/analysis/metrics.py +414 -0
  16. mcp_vector_search/analysis/reporters/__init__.py +7 -0
  17. mcp_vector_search/analysis/reporters/console.py +646 -0
  18. mcp_vector_search/analysis/reporters/markdown.py +480 -0
  19. mcp_vector_search/analysis/reporters/sarif.py +377 -0
  20. mcp_vector_search/analysis/storage/__init__.py +93 -0
  21. mcp_vector_search/analysis/storage/metrics_store.py +762 -0
  22. mcp_vector_search/analysis/storage/schema.py +245 -0
  23. mcp_vector_search/analysis/storage/trend_tracker.py +560 -0
  24. mcp_vector_search/analysis/trends.py +308 -0
  25. mcp_vector_search/analysis/visualizer/__init__.py +90 -0
  26. mcp_vector_search/analysis/visualizer/d3_data.py +534 -0
  27. mcp_vector_search/analysis/visualizer/exporter.py +484 -0
  28. mcp_vector_search/analysis/visualizer/html_report.py +2895 -0
  29. mcp_vector_search/analysis/visualizer/schemas.py +525 -0
  30. mcp_vector_search/cli/commands/analyze.py +1062 -0
  31. mcp_vector_search/cli/commands/chat.py +1455 -0
  32. mcp_vector_search/cli/commands/index.py +621 -5
  33. mcp_vector_search/cli/commands/index_background.py +467 -0
  34. mcp_vector_search/cli/commands/init.py +13 -0
  35. mcp_vector_search/cli/commands/install.py +597 -335
  36. mcp_vector_search/cli/commands/install_old.py +8 -4
  37. mcp_vector_search/cli/commands/mcp.py +78 -6
  38. mcp_vector_search/cli/commands/reset.py +68 -26
  39. mcp_vector_search/cli/commands/search.py +224 -8
  40. mcp_vector_search/cli/commands/setup.py +1184 -0
  41. mcp_vector_search/cli/commands/status.py +339 -5
  42. mcp_vector_search/cli/commands/uninstall.py +276 -357
  43. mcp_vector_search/cli/commands/visualize/__init__.py +39 -0
  44. mcp_vector_search/cli/commands/visualize/cli.py +292 -0
  45. mcp_vector_search/cli/commands/visualize/exporters/__init__.py +12 -0
  46. mcp_vector_search/cli/commands/visualize/exporters/html_exporter.py +33 -0
  47. mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +33 -0
  48. mcp_vector_search/cli/commands/visualize/graph_builder.py +647 -0
  49. mcp_vector_search/cli/commands/visualize/layout_engine.py +469 -0
  50. mcp_vector_search/cli/commands/visualize/server.py +600 -0
  51. mcp_vector_search/cli/commands/visualize/state_manager.py +428 -0
  52. mcp_vector_search/cli/commands/visualize/templates/__init__.py +16 -0
  53. mcp_vector_search/cli/commands/visualize/templates/base.py +234 -0
  54. mcp_vector_search/cli/commands/visualize/templates/scripts.py +4542 -0
  55. mcp_vector_search/cli/commands/visualize/templates/styles.py +2522 -0
  56. mcp_vector_search/cli/didyoumean.py +27 -2
  57. mcp_vector_search/cli/main.py +127 -160
  58. mcp_vector_search/cli/output.py +158 -13
  59. mcp_vector_search/config/__init__.py +4 -0
  60. mcp_vector_search/config/default_thresholds.yaml +52 -0
  61. mcp_vector_search/config/settings.py +12 -0
  62. mcp_vector_search/config/thresholds.py +273 -0
  63. mcp_vector_search/core/__init__.py +16 -0
  64. mcp_vector_search/core/auto_indexer.py +3 -3
  65. mcp_vector_search/core/boilerplate.py +186 -0
  66. mcp_vector_search/core/config_utils.py +394 -0
  67. mcp_vector_search/core/database.py +406 -94
  68. mcp_vector_search/core/embeddings.py +24 -0
  69. mcp_vector_search/core/exceptions.py +11 -0
  70. mcp_vector_search/core/git.py +380 -0
  71. mcp_vector_search/core/git_hooks.py +4 -4
  72. mcp_vector_search/core/indexer.py +632 -54
  73. mcp_vector_search/core/llm_client.py +756 -0
  74. mcp_vector_search/core/models.py +91 -1
  75. mcp_vector_search/core/project.py +17 -0
  76. mcp_vector_search/core/relationships.py +473 -0
  77. mcp_vector_search/core/scheduler.py +11 -11
  78. mcp_vector_search/core/search.py +179 -29
  79. mcp_vector_search/mcp/server.py +819 -9
  80. mcp_vector_search/parsers/python.py +285 -5
  81. mcp_vector_search/utils/__init__.py +2 -0
  82. mcp_vector_search/utils/gitignore.py +0 -3
  83. mcp_vector_search/utils/gitignore_updater.py +212 -0
  84. mcp_vector_search/utils/monorepo.py +66 -4
  85. mcp_vector_search/utils/timing.py +10 -6
  86. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/METADATA +184 -53
  87. mcp_vector_search-1.1.22.dist-info/RECORD +120 -0
  88. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/WHEEL +1 -1
  89. {mcp_vector_search-0.12.6.dist-info → mcp_vector_search-1.1.22.dist-info}/entry_points.txt +1 -0
  90. mcp_vector_search/cli/commands/visualize.py +0 -1467
  91. mcp_vector_search-0.12.6.dist-info/RECORD +0 -68
  92. {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