mcp-vector-search 1.0.3__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 +48 -1
- 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 +35 -0
- mcp_vector_search/analysis/collectors/cohesion.py +463 -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 +74 -1
- mcp_vector_search/analysis/reporters/__init__.py +3 -1
- mcp_vector_search/analysis/reporters/console.py +424 -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 +665 -11
- mcp_vector_search/cli/commands/chat.py +193 -0
- mcp_vector_search/cli/commands/index.py +600 -2
- mcp_vector_search/cli/commands/index_background.py +467 -0
- mcp_vector_search/cli/commands/search.py +194 -1
- mcp_vector_search/cli/commands/setup.py +64 -13
- mcp_vector_search/cli/commands/status.py +302 -3
- mcp_vector_search/cli/commands/visualize/cli.py +26 -10
- mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +8 -4
- mcp_vector_search/cli/commands/visualize/graph_builder.py +167 -234
- mcp_vector_search/cli/commands/visualize/server.py +304 -15
- mcp_vector_search/cli/commands/visualize/templates/base.py +60 -6
- mcp_vector_search/cli/commands/visualize/templates/scripts.py +2100 -65
- mcp_vector_search/cli/commands/visualize/templates/styles.py +1297 -88
- mcp_vector_search/cli/didyoumean.py +5 -0
- mcp_vector_search/cli/main.py +16 -5
- mcp_vector_search/cli/output.py +134 -5
- mcp_vector_search/config/thresholds.py +89 -1
- mcp_vector_search/core/__init__.py +16 -0
- mcp_vector_search/core/database.py +39 -2
- mcp_vector_search/core/embeddings.py +24 -0
- mcp_vector_search/core/git.py +380 -0
- mcp_vector_search/core/indexer.py +445 -84
- mcp_vector_search/core/llm_client.py +9 -4
- mcp_vector_search/core/models.py +88 -1
- mcp_vector_search/core/relationships.py +473 -0
- mcp_vector_search/core/search.py +1 -1
- mcp_vector_search/mcp/server.py +795 -4
- mcp_vector_search/parsers/python.py +285 -5
- mcp_vector_search/utils/gitignore.py +0 -3
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/METADATA +3 -2
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/RECORD +62 -39
- mcp_vector_search/cli/commands/visualize.py.original +0 -2536
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/WHEEL +0 -0
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/entry_points.txt +0 -0
- {mcp_vector_search-1.0.3.dist-info → mcp_vector_search-1.1.22.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Code smell detection based on structural metrics.
|
|
2
|
+
|
|
3
|
+
This module provides smell detection functionality that identifies common
|
|
4
|
+
code smells based on complexity metrics and structural analysis.
|
|
5
|
+
|
|
6
|
+
Supported Smells:
|
|
7
|
+
- Long Method: Functions/methods with too many lines or complexity
|
|
8
|
+
- Deep Nesting: Excessive nesting depth
|
|
9
|
+
- Long Parameter List: Too many function parameters
|
|
10
|
+
- God Class: Classes with too many methods and lines
|
|
11
|
+
- Complex Method: High cyclomatic complexity
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from ...config.thresholds import ThresholdConfig
|
|
22
|
+
from ..metrics import ChunkMetrics, FileMetrics
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SmellSeverity(Enum):
|
|
26
|
+
"""Severity level for code smells.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
INFO: Informational smell, no immediate action needed
|
|
30
|
+
WARNING: Warning smell, should be addressed during refactoring
|
|
31
|
+
ERROR: Error-level smell, requires immediate attention
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
INFO = "info"
|
|
35
|
+
WARNING = "warning"
|
|
36
|
+
ERROR = "error"
|
|
37
|
+
|
|
38
|
+
def __str__(self) -> str:
|
|
39
|
+
"""Return string representation of severity."""
|
|
40
|
+
return self.value
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class CodeSmell:
|
|
45
|
+
"""Represents a detected code smell.
|
|
46
|
+
|
|
47
|
+
Attributes:
|
|
48
|
+
name: Human-readable smell name (e.g., "Long Method")
|
|
49
|
+
description: Detailed description of the smell
|
|
50
|
+
severity: Severity level (INFO, WARNING, ERROR)
|
|
51
|
+
location: File location in format "file:line" or "file:line-range"
|
|
52
|
+
metric_value: Actual metric value that triggered the smell
|
|
53
|
+
threshold: Threshold value that was exceeded
|
|
54
|
+
suggestion: Optional suggestion for fixing the smell
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
name: str
|
|
58
|
+
description: str
|
|
59
|
+
severity: SmellSeverity
|
|
60
|
+
location: str
|
|
61
|
+
metric_value: float
|
|
62
|
+
threshold: float
|
|
63
|
+
suggestion: str = ""
|
|
64
|
+
|
|
65
|
+
def __str__(self) -> str:
|
|
66
|
+
"""Return string representation of code smell."""
|
|
67
|
+
return (
|
|
68
|
+
f"[{self.severity.value.upper()}] {self.name} at {self.location}: "
|
|
69
|
+
f"{self.description} (value: {self.metric_value}, threshold: {self.threshold})"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SmellDetector:
|
|
74
|
+
"""Detects code smells based on structural metrics.
|
|
75
|
+
|
|
76
|
+
This detector analyzes ChunkMetrics and FileMetrics to identify common
|
|
77
|
+
code smells that indicate maintainability issues. Detection rules are
|
|
78
|
+
configurable via ThresholdConfig.
|
|
79
|
+
|
|
80
|
+
Detection Rules:
|
|
81
|
+
- Long Method: lines > 50 OR cognitive_complexity > 15
|
|
82
|
+
- Deep Nesting: max_nesting_depth > 4
|
|
83
|
+
- Long Parameter List: parameter_count > 5
|
|
84
|
+
- God Class: method_count > 20 AND lines > 500
|
|
85
|
+
- Complex Method: cyclomatic_complexity > 10
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
detector = SmellDetector()
|
|
89
|
+
smells = detector.detect(chunk_metrics)
|
|
90
|
+
|
|
91
|
+
for smell in smells:
|
|
92
|
+
print(f"{smell.severity}: {smell.name} at {smell.location}")
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(self, thresholds: ThresholdConfig | None = None) -> None:
|
|
96
|
+
"""Initialize smell detector.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
thresholds: Optional custom threshold configuration.
|
|
100
|
+
If None, uses default thresholds from ThresholdConfig.
|
|
101
|
+
"""
|
|
102
|
+
if thresholds is None:
|
|
103
|
+
# Import here to avoid circular dependency
|
|
104
|
+
from ...config.thresholds import ThresholdConfig
|
|
105
|
+
|
|
106
|
+
thresholds = ThresholdConfig()
|
|
107
|
+
|
|
108
|
+
self.thresholds = thresholds
|
|
109
|
+
|
|
110
|
+
def detect(
|
|
111
|
+
self, metrics: ChunkMetrics, file_path: str = "", start_line: int = 0
|
|
112
|
+
) -> list[CodeSmell]:
|
|
113
|
+
"""Detect code smells in a single chunk (function/method/class).
|
|
114
|
+
|
|
115
|
+
Analyzes the provided metrics and returns a list of detected smells.
|
|
116
|
+
Each smell includes location information and severity level.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
metrics: Chunk metrics to analyze
|
|
120
|
+
file_path: Path to the file containing this chunk
|
|
121
|
+
start_line: Starting line number of the chunk
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
List of detected CodeSmell objects (empty if no smells found)
|
|
125
|
+
"""
|
|
126
|
+
smells: list[CodeSmell] = []
|
|
127
|
+
|
|
128
|
+
# Location string for reporting
|
|
129
|
+
location = f"{file_path}:{start_line}" if file_path else f"line {start_line}"
|
|
130
|
+
|
|
131
|
+
# 1. Long Method Detection
|
|
132
|
+
# Rule: lines > 50 OR cognitive_complexity > 15
|
|
133
|
+
long_method_lines_threshold = self.thresholds.smells.long_method_lines
|
|
134
|
+
high_complexity_threshold = self.thresholds.smells.high_complexity
|
|
135
|
+
|
|
136
|
+
is_long_by_lines = metrics.lines_of_code > long_method_lines_threshold
|
|
137
|
+
is_long_by_complexity = metrics.cognitive_complexity > high_complexity_threshold
|
|
138
|
+
|
|
139
|
+
if is_long_by_lines or is_long_by_complexity:
|
|
140
|
+
description_parts = []
|
|
141
|
+
if is_long_by_lines:
|
|
142
|
+
description_parts.append(
|
|
143
|
+
f"{metrics.lines_of_code} lines (threshold: {long_method_lines_threshold})"
|
|
144
|
+
)
|
|
145
|
+
if is_long_by_complexity:
|
|
146
|
+
description_parts.append(
|
|
147
|
+
f"cognitive complexity {metrics.cognitive_complexity} (threshold: {high_complexity_threshold})"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
smells.append(
|
|
151
|
+
CodeSmell(
|
|
152
|
+
name="Long Method",
|
|
153
|
+
description=f"Method/function is too long: {', '.join(description_parts)}",
|
|
154
|
+
severity=SmellSeverity.WARNING,
|
|
155
|
+
location=location,
|
|
156
|
+
metric_value=float(
|
|
157
|
+
max(metrics.lines_of_code, metrics.cognitive_complexity)
|
|
158
|
+
),
|
|
159
|
+
threshold=float(
|
|
160
|
+
max(long_method_lines_threshold, high_complexity_threshold)
|
|
161
|
+
),
|
|
162
|
+
suggestion="Consider breaking this method into smaller, focused functions",
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# 2. Deep Nesting Detection
|
|
167
|
+
# Rule: max_nesting_depth > 4
|
|
168
|
+
nesting_threshold = self.thresholds.smells.deep_nesting_depth
|
|
169
|
+
|
|
170
|
+
if metrics.max_nesting_depth > nesting_threshold:
|
|
171
|
+
smells.append(
|
|
172
|
+
CodeSmell(
|
|
173
|
+
name="Deep Nesting",
|
|
174
|
+
description=f"Excessive nesting depth: {metrics.max_nesting_depth} levels (threshold: {nesting_threshold})",
|
|
175
|
+
severity=SmellSeverity.WARNING,
|
|
176
|
+
location=location,
|
|
177
|
+
metric_value=float(metrics.max_nesting_depth),
|
|
178
|
+
threshold=float(nesting_threshold),
|
|
179
|
+
suggestion="Consider extracting nested logic into separate functions or using early returns",
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# 3. Long Parameter List Detection
|
|
184
|
+
# Rule: parameter_count > 5
|
|
185
|
+
param_threshold = self.thresholds.smells.too_many_parameters
|
|
186
|
+
|
|
187
|
+
if metrics.parameter_count > param_threshold:
|
|
188
|
+
smells.append(
|
|
189
|
+
CodeSmell(
|
|
190
|
+
name="Long Parameter List",
|
|
191
|
+
description=f"Too many parameters: {metrics.parameter_count} parameters (threshold: {param_threshold})",
|
|
192
|
+
severity=SmellSeverity.WARNING,
|
|
193
|
+
location=location,
|
|
194
|
+
metric_value=float(metrics.parameter_count),
|
|
195
|
+
threshold=float(param_threshold),
|
|
196
|
+
suggestion="Consider introducing a parameter object or using builder pattern",
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# 4. Complex Method Detection
|
|
201
|
+
# Rule: cyclomatic_complexity > 10
|
|
202
|
+
cyclomatic_threshold = self.thresholds.complexity.cyclomatic_moderate
|
|
203
|
+
|
|
204
|
+
if metrics.cyclomatic_complexity > cyclomatic_threshold:
|
|
205
|
+
smells.append(
|
|
206
|
+
CodeSmell(
|
|
207
|
+
name="Complex Method",
|
|
208
|
+
description=f"High cyclomatic complexity: {metrics.cyclomatic_complexity} (threshold: {cyclomatic_threshold})",
|
|
209
|
+
severity=SmellSeverity.WARNING,
|
|
210
|
+
location=location,
|
|
211
|
+
metric_value=float(metrics.cyclomatic_complexity),
|
|
212
|
+
threshold=float(cyclomatic_threshold),
|
|
213
|
+
suggestion="Simplify control flow by extracting complex conditions into separate functions",
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return smells
|
|
218
|
+
|
|
219
|
+
def detect_god_class(
|
|
220
|
+
self, file_metrics: FileMetrics, file_path: str = ""
|
|
221
|
+
) -> list[CodeSmell]:
|
|
222
|
+
"""Detect God Class smell at file level.
|
|
223
|
+
|
|
224
|
+
A God Class has too many responsibilities, indicated by high method
|
|
225
|
+
count and large file size.
|
|
226
|
+
|
|
227
|
+
Rule: method_count > 20 AND lines > 500
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
file_metrics: File-level metrics to analyze
|
|
231
|
+
file_path: Path to the file being analyzed
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
List containing CodeSmell if God Class detected (empty otherwise)
|
|
235
|
+
"""
|
|
236
|
+
smells: list[CodeSmell] = []
|
|
237
|
+
|
|
238
|
+
# God Class Detection
|
|
239
|
+
# Rule: method_count > 20 AND total_lines > 500
|
|
240
|
+
method_threshold = self.thresholds.smells.god_class_methods
|
|
241
|
+
lines_threshold = self.thresholds.smells.god_class_lines
|
|
242
|
+
|
|
243
|
+
is_god_class = (
|
|
244
|
+
file_metrics.method_count > method_threshold
|
|
245
|
+
and file_metrics.total_lines > lines_threshold
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if is_god_class:
|
|
249
|
+
location = file_path if file_path else "file"
|
|
250
|
+
smells.append(
|
|
251
|
+
CodeSmell(
|
|
252
|
+
name="God Class",
|
|
253
|
+
description=(
|
|
254
|
+
f"Class has too many responsibilities: "
|
|
255
|
+
f"{file_metrics.method_count} methods (threshold: {method_threshold}), "
|
|
256
|
+
f"{file_metrics.total_lines} lines (threshold: {lines_threshold})"
|
|
257
|
+
),
|
|
258
|
+
severity=SmellSeverity.ERROR,
|
|
259
|
+
location=location,
|
|
260
|
+
metric_value=float(file_metrics.method_count),
|
|
261
|
+
threshold=float(method_threshold),
|
|
262
|
+
suggestion="Split this class into smaller, focused classes following Single Responsibility Principle",
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
return smells
|
|
267
|
+
|
|
268
|
+
def detect_all(
|
|
269
|
+
self, file_metrics: FileMetrics, file_path: str = ""
|
|
270
|
+
) -> list[CodeSmell]:
|
|
271
|
+
"""Detect all code smells across an entire file.
|
|
272
|
+
|
|
273
|
+
Analyzes both chunk-level smells (Long Method, Deep Nesting, etc.)
|
|
274
|
+
and file-level smells (God Class).
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
file_metrics: File metrics containing all chunks
|
|
278
|
+
file_path: Path to the file being analyzed
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
List of all detected CodeSmell objects
|
|
282
|
+
"""
|
|
283
|
+
all_smells: list[CodeSmell] = []
|
|
284
|
+
|
|
285
|
+
# Detect chunk-level smells
|
|
286
|
+
for i, chunk in enumerate(file_metrics.chunks):
|
|
287
|
+
# Estimate start line for each chunk
|
|
288
|
+
# This is a rough approximation - ideally we'd have actual line numbers
|
|
289
|
+
estimated_start_line = (
|
|
290
|
+
1 + i * 20
|
|
291
|
+
) # Rough estimate assuming 20 lines per chunk
|
|
292
|
+
|
|
293
|
+
chunk_smells = self.detect(chunk, file_path, estimated_start_line)
|
|
294
|
+
all_smells.extend(chunk_smells)
|
|
295
|
+
|
|
296
|
+
# Detect file-level smells (God Class)
|
|
297
|
+
file_smells = self.detect_god_class(file_metrics, file_path)
|
|
298
|
+
all_smells.extend(file_smells)
|
|
299
|
+
|
|
300
|
+
return all_smells
|
|
301
|
+
|
|
302
|
+
def get_smell_summary(self, smells: list[CodeSmell]) -> dict[str, int]:
|
|
303
|
+
"""Generate summary statistics for detected smells.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
smells: List of detected code smells
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Dictionary with counts by severity and smell type
|
|
310
|
+
"""
|
|
311
|
+
summary: dict[str, int] = {
|
|
312
|
+
"total": len(smells),
|
|
313
|
+
"error": sum(1 for s in smells if s.severity == SmellSeverity.ERROR),
|
|
314
|
+
"warning": sum(1 for s in smells if s.severity == SmellSeverity.WARNING),
|
|
315
|
+
"info": sum(1 for s in smells if s.severity == SmellSeverity.INFO),
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
# Count by smell type
|
|
319
|
+
smell_types = {}
|
|
320
|
+
for smell in smells:
|
|
321
|
+
smell_types[smell.name] = smell_types.get(smell.name, 0) + 1
|
|
322
|
+
|
|
323
|
+
summary["by_type"] = smell_types
|
|
324
|
+
|
|
325
|
+
return summary
|