claude-mpm 4.21.0__py3-none-any.whl → 4.21.3__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.

Potentially problematic release.


This version of claude-mpm might be problematic. Click here for more details.

@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Analysis Module
4
+ ===============
5
+
6
+ Coordinates file analysis across different language analyzers.
7
+
8
+ WHY: Centralizes file analysis logic and result processing,
9
+ separating it from directory traversal and caching concerns.
10
+ """
11
+
12
+ import time
13
+ from pathlib import Path
14
+ from typing import Dict, List, Tuple
15
+
16
+ from ...core.logging_config import get_logger
17
+ from .cache import CacheManager
18
+ from .events import EventManager
19
+ from .models import CodeNode
20
+ from .multilang_analyzer import MultiLanguageAnalyzer
21
+ from .python_analyzer import PythonAnalyzer
22
+
23
+
24
+ class FileAnalyzer:
25
+ """Coordinates file analysis using appropriate language analyzers."""
26
+
27
+ def __init__(
28
+ self,
29
+ python_analyzer: PythonAnalyzer,
30
+ multilang_analyzer: MultiLanguageAnalyzer,
31
+ cache_manager: CacheManager,
32
+ event_manager: EventManager,
33
+ ):
34
+ """Initialize file analyzer.
35
+
36
+ Args:
37
+ python_analyzer: Python analyzer instance
38
+ multilang_analyzer: Multi-language analyzer instance
39
+ cache_manager: Cache manager instance
40
+ event_manager: Event manager instance
41
+ """
42
+ self.logger = get_logger(__name__)
43
+ self.python_analyzer = python_analyzer
44
+ self.multilang_analyzer = multilang_analyzer
45
+ self.cache_manager = cache_manager
46
+ self.event_manager = event_manager
47
+
48
+ def analyze_file(self, file_path: str) -> Dict[str, any]:
49
+ """Analyze a specific file and return its AST structure.
50
+
51
+ Args:
52
+ file_path: Path to file to analyze
53
+
54
+ Returns:
55
+ Dictionary with file analysis results
56
+ """
57
+ path = Path(file_path)
58
+ if not path.exists() or not path.is_file():
59
+ return {"error": f"Invalid file: {file_path}"}
60
+
61
+ language = self._get_language(path)
62
+ self.event_manager.emit_analysis_start(path, language)
63
+
64
+ # Check cache
65
+ cache_key = self.cache_manager.get_cache_key(path)
66
+
67
+ if cached_nodes := self.cache_manager.get(cache_key):
68
+ self.event_manager.emit_cache_hit(path)
69
+ filtered_nodes = self._filter_nodes(cached_nodes)
70
+ else:
71
+ nodes, filtered_nodes, duration = self._analyze_and_cache_file(
72
+ path, language, cache_key
73
+ )
74
+ self.event_manager.emit_analysis_complete(path, filtered_nodes, duration)
75
+
76
+ # Prepare final data structures
77
+ final_nodes = filtered_nodes if filtered_nodes else []
78
+ elements = self._convert_nodes_to_elements(final_nodes)
79
+
80
+ return self._build_result(file_path, language, final_nodes, elements)
81
+
82
+ def _analyze_and_cache_file(
83
+ self, path: Path, language: str, cache_key: str
84
+ ) -> Tuple[List[CodeNode], List[dict], float]:
85
+ """Analyze file content and cache results.
86
+
87
+ Args:
88
+ path: File path
89
+ language: Programming language
90
+ cache_key: Cache key for storing results
91
+
92
+ Returns:
93
+ Tuple of (all_nodes, filtered_nodes, duration)
94
+ """
95
+ self.event_manager.emit_cache_miss(path)
96
+ self.event_manager.emit_parsing_start(path)
97
+
98
+ # Select analyzer based on language
99
+ analyzer = self._select_analyzer(language)
100
+
101
+ # Perform analysis
102
+ start_time = time.time()
103
+ nodes = analyzer.analyze_file(path) if analyzer else []
104
+ duration = time.time() - start_time
105
+
106
+ # Cache results
107
+ self.cache_manager.set(cache_key, nodes)
108
+
109
+ # Filter and process nodes
110
+ filtered_nodes = self._filter_and_emit_nodes(nodes, path)
111
+
112
+ return nodes, filtered_nodes, duration
113
+
114
+ def _select_analyzer(self, language: str):
115
+ """Select appropriate analyzer for language.
116
+
117
+ Args:
118
+ language: Programming language
119
+
120
+ Returns:
121
+ Analyzer instance or None
122
+ """
123
+ if language == "python":
124
+ return self.python_analyzer
125
+ if language in {"javascript", "typescript"}:
126
+ return self.multilang_analyzer
127
+ return self.multilang_analyzer
128
+
129
+ def _filter_nodes(self, nodes: List[CodeNode]) -> List[dict]:
130
+ """Filter nodes without emitting events.
131
+
132
+ Args:
133
+ nodes: List of CodeNode objects
134
+
135
+ Returns:
136
+ List of filtered node dictionaries
137
+ """
138
+ return [self._node_to_dict(n) for n in nodes if not self._is_internal_node(n)]
139
+
140
+ def _filter_and_emit_nodes(self, nodes: List[CodeNode], path: Path) -> List[dict]:
141
+ """Filter nodes and emit events for each.
142
+
143
+ Args:
144
+ nodes: List of CodeNode objects
145
+ path: File path being analyzed
146
+
147
+ Returns:
148
+ List of filtered node dictionaries
149
+ """
150
+ filtered_nodes = []
151
+ for node in nodes:
152
+ if not self._is_internal_node(node):
153
+ self.event_manager.emit_node_found(node, path)
154
+ filtered_nodes.append(self._node_to_dict(node))
155
+ return filtered_nodes
156
+
157
+ def _node_to_dict(self, node: CodeNode) -> dict:
158
+ """Convert CodeNode to dictionary.
159
+
160
+ Args:
161
+ node: CodeNode object
162
+
163
+ Returns:
164
+ Dictionary representation
165
+ """
166
+ return {
167
+ "name": node.name,
168
+ "type": node.node_type,
169
+ "line_start": node.line_start,
170
+ "line_end": node.line_end,
171
+ "complexity": node.complexity,
172
+ "has_docstring": node.has_docstring,
173
+ "signature": node.signature,
174
+ }
175
+
176
+ def _convert_nodes_to_elements(self, final_nodes: List[dict]) -> List[dict]:
177
+ """Convert nodes to elements format for dashboard.
178
+
179
+ Args:
180
+ final_nodes: List of node dictionaries
181
+
182
+ Returns:
183
+ List of element dictionaries
184
+ """
185
+ elements = []
186
+ for node in final_nodes:
187
+ element = {
188
+ "name": node["name"],
189
+ "type": node["type"],
190
+ "line": node["line_start"],
191
+ "complexity": node["complexity"],
192
+ "signature": node.get("signature", ""),
193
+ "has_docstring": node.get("has_docstring", False),
194
+ }
195
+ if node["type"] == "class":
196
+ element["methods"] = []
197
+ elements.append(element)
198
+ return elements
199
+
200
+ def _build_result(
201
+ self,
202
+ file_path: str,
203
+ language: str,
204
+ final_nodes: List[dict],
205
+ elements: List[dict],
206
+ ) -> dict:
207
+ """Build final result dictionary.
208
+
209
+ Args:
210
+ file_path: File path
211
+ language: Programming language
212
+ final_nodes: List of node dictionaries
213
+ elements: List of element dictionaries
214
+
215
+ Returns:
216
+ Complete result dictionary
217
+ """
218
+ return {
219
+ "path": file_path,
220
+ "language": language,
221
+ "nodes": final_nodes,
222
+ "elements": elements,
223
+ "complexity": sum(e["complexity"] for e in elements),
224
+ "lines": len(elements),
225
+ "stats": {
226
+ "classes": len([e for e in elements if e["type"] == "class"]),
227
+ "functions": len([e for e in elements if e["type"] == "function"]),
228
+ "methods": len([e for e in elements if e["type"] == "method"]),
229
+ "variables": len([e for e in elements if e["type"] == "variable"]),
230
+ "imports": len([e for e in elements if e["type"] == "import"]),
231
+ "total": len(elements),
232
+ },
233
+ }
234
+
235
+ def _is_internal_node(self, node: CodeNode) -> bool:
236
+ """Check if node is an internal function that should be filtered.
237
+
238
+ Args:
239
+ node: CodeNode to check
240
+
241
+ Returns:
242
+ True if node should be filtered out
243
+ """
244
+ # Don't filter classes - always show them
245
+ if node.node_type == "class":
246
+ return False
247
+
248
+ # Don't filter variables or imports - they're useful for tree view
249
+ if node.node_type in ["variable", "import"]:
250
+ return False
251
+
252
+ name_lower = node.name.lower()
253
+
254
+ # Filter only very specific internal patterns
255
+ # Be more conservative - only filter obvious internal handlers
256
+ if name_lower.startswith(("handle_", "on_")):
257
+ return True
258
+
259
+ # Filter Python magic methods except important ones
260
+ if name_lower.startswith("__") and name_lower.endswith("__"):
261
+ # Keep important magic methods
262
+ important_magic = [
263
+ "__init__",
264
+ "__call__",
265
+ "__enter__",
266
+ "__exit__",
267
+ "__str__",
268
+ "__repr__",
269
+ ]
270
+ return node.name not in important_magic
271
+
272
+ # Filter very generic getters/setters only if they're trivial
273
+ if (name_lower.startswith(("get_", "set_"))) and len(node.name) <= 8:
274
+ return True
275
+
276
+ # Don't filter single underscore functions - they're often important
277
+ # (like _setup_logging, _validate_input, etc.)
278
+ return False
279
+
280
+ def _get_language(self, file_path: Path) -> str:
281
+ """Determine language from file extension.
282
+
283
+ Args:
284
+ file_path: Path to file
285
+
286
+ Returns:
287
+ Language string
288
+ """
289
+ ext = file_path.suffix.lower()
290
+ language_map = {
291
+ ".py": "python",
292
+ ".js": "javascript",
293
+ ".jsx": "javascript",
294
+ ".ts": "typescript",
295
+ ".tsx": "typescript",
296
+ ".mjs": "javascript",
297
+ ".cjs": "javascript",
298
+ }
299
+ return language_map.get(ext, "unknown")
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Cache Module
4
+ ============
5
+
6
+ Handles caching of analyzed code tree results.
7
+
8
+ WHY: Caching prevents re-parsing files that haven't changed,
9
+ significantly improving performance for large codebases.
10
+ """
11
+
12
+ import hashlib
13
+ import json
14
+ from pathlib import Path
15
+ from typing import Dict, List
16
+
17
+ from ...core.logging_config import get_logger
18
+ from .models import CodeNode
19
+
20
+
21
+ class CacheManager:
22
+ """Manages caching of code analysis results."""
23
+
24
+ def __init__(self, cache_dir: Path):
25
+ """Initialize cache manager.
26
+
27
+ Args:
28
+ cache_dir: Directory to store cache files
29
+ """
30
+ self.logger = get_logger(__name__)
31
+ self.cache_dir = cache_dir
32
+ self.cache: Dict[str, List[CodeNode]] = {}
33
+
34
+ def get_file_hash(self, file_path: Path) -> str:
35
+ """Get hash of file contents for caching.
36
+
37
+ Args:
38
+ file_path: Path to file
39
+
40
+ Returns:
41
+ MD5 hash of file contents
42
+ """
43
+ hasher = hashlib.md5()
44
+ with file_path.open("rb") as f:
45
+ hasher.update(f.read())
46
+ return hasher.hexdigest()
47
+
48
+ def get_cache_key(self, file_path: Path) -> str:
49
+ """Generate cache key for a file.
50
+
51
+ Args:
52
+ file_path: Path to file
53
+
54
+ Returns:
55
+ Cache key string
56
+ """
57
+ file_hash = self.get_file_hash(file_path)
58
+ return f"{file_path}:{file_hash}"
59
+
60
+ def get(self, cache_key: str) -> List[CodeNode]:
61
+ """Get cached nodes for a file.
62
+
63
+ Args:
64
+ cache_key: Cache key
65
+
66
+ Returns:
67
+ List of cached nodes or None if not cached
68
+ """
69
+ return self.cache.get(cache_key)
70
+
71
+ def set(self, cache_key: str, nodes: List[CodeNode]) -> None:
72
+ """Cache nodes for a file.
73
+
74
+ Args:
75
+ cache_key: Cache key
76
+ nodes: List of nodes to cache
77
+ """
78
+ self.cache[cache_key] = nodes
79
+
80
+ def load(self) -> None:
81
+ """Load cache from disk."""
82
+ cache_file = self.cache_dir / "code_tree_cache.json"
83
+ if cache_file.exists():
84
+ try:
85
+ with cache_file.open() as f:
86
+ cache_data = json.load(f)
87
+ # Reconstruct CodeNode objects
88
+ for key, nodes_data in cache_data.items():
89
+ self.cache[key] = [
90
+ CodeNode(**node_data) for node_data in nodes_data
91
+ ]
92
+ self.logger.info(f"Loaded cache with {len(self.cache)} entries")
93
+ except Exception as e:
94
+ self.logger.warning(f"Failed to load cache: {e}")
95
+
96
+ def save(self) -> None:
97
+ """Save cache to disk."""
98
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
99
+ cache_file = self.cache_dir / "code_tree_cache.json"
100
+
101
+ try:
102
+ # Convert CodeNode objects to dictionaries
103
+ cache_data = {}
104
+ for key, nodes in self.cache.items():
105
+ cache_data[key] = [
106
+ {
107
+ "file_path": n.file_path,
108
+ "node_type": n.node_type,
109
+ "name": n.name,
110
+ "line_start": n.line_start,
111
+ "line_end": n.line_end,
112
+ "complexity": n.complexity,
113
+ "has_docstring": n.has_docstring,
114
+ "decorators": n.decorators,
115
+ "parent": n.parent,
116
+ "language": n.language,
117
+ "signature": n.signature,
118
+ }
119
+ for n in nodes
120
+ ]
121
+
122
+ with cache_file.open("w") as f:
123
+ json.dump(cache_data, f, indent=2)
124
+
125
+ self.logger.info(f"Saved cache with {len(self.cache)} entries")
126
+ except Exception as e:
127
+ self.logger.warning(f"Failed to save cache: {e}")
128
+
129
+ def clear(self) -> None:
130
+ """Clear all cached data."""
131
+ self.cache.clear()