code2flow-toon 0.2.4__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 (43) hide show
  1. code2flow/__init__.py +47 -0
  2. code2flow/__main__.py +6 -0
  3. code2flow/analysis/__init__.py +17 -0
  4. code2flow/analysis/call_graph.py +210 -0
  5. code2flow/analysis/cfg.py +293 -0
  6. code2flow/analysis/coupling.py +77 -0
  7. code2flow/analysis/data_analysis.py +249 -0
  8. code2flow/analysis/dfg.py +224 -0
  9. code2flow/analysis/smells.py +192 -0
  10. code2flow/cli.py +464 -0
  11. code2flow/core/__init__.py +36 -0
  12. code2flow/core/analyzer.py +765 -0
  13. code2flow/core/config.py +177 -0
  14. code2flow/core/models.py +194 -0
  15. code2flow/core/streaming_analyzer.py +666 -0
  16. code2flow/exporters/__init__.py +17 -0
  17. code2flow/exporters/base.py +13 -0
  18. code2flow/exporters/json_exporter.py +17 -0
  19. code2flow/exporters/llm_exporter.py +199 -0
  20. code2flow/exporters/mermaid_exporter.py +67 -0
  21. code2flow/exporters/toon.py +401 -0
  22. code2flow/exporters/yaml_exporter.py +108 -0
  23. code2flow/llm_flow_generator.py +451 -0
  24. code2flow/llm_task_generator.py +263 -0
  25. code2flow/mermaid_generator.py +481 -0
  26. code2flow/nlp/__init__.py +23 -0
  27. code2flow/nlp/config.py +174 -0
  28. code2flow/nlp/entity_resolution.py +326 -0
  29. code2flow/nlp/intent_matching.py +297 -0
  30. code2flow/nlp/normalization.py +122 -0
  31. code2flow/nlp/pipeline.py +388 -0
  32. code2flow/patterns/__init__.py +0 -0
  33. code2flow/patterns/detector.py +168 -0
  34. code2flow/refactor/__init__.py +0 -0
  35. code2flow/refactor/prompt_engine.py +150 -0
  36. code2flow/visualizers/__init__.py +0 -0
  37. code2flow/visualizers/graph.py +196 -0
  38. code2flow_toon-0.2.4.dist-info/METADATA +599 -0
  39. code2flow_toon-0.2.4.dist-info/RECORD +43 -0
  40. code2flow_toon-0.2.4.dist-info/WHEEL +5 -0
  41. code2flow_toon-0.2.4.dist-info/entry_points.txt +2 -0
  42. code2flow_toon-0.2.4.dist-info/licenses/LICENSE +201 -0
  43. code2flow_toon-0.2.4.dist-info/top_level.txt +1 -0
code2flow/__init__.py ADDED
@@ -0,0 +1,47 @@
1
+ """
2
+ code2flow - Optimized Python Code Flow Analysis Tool
3
+
4
+ A high-performance tool for analyzing Python code control flow, data flow,
5
+ and call graphs with caching and parallel processing.
6
+
7
+ Includes NLP Processing Pipeline for query normalization, intent matching,
8
+ and entity resolution with multilingual support.
9
+ """
10
+
11
+ __version__ = "0.2.4"
12
+ __author__ = "STTS Project"
13
+
14
+ # Core analysis components
15
+ from .core.analyzer import ProjectAnalyzer
16
+ from .core.config import Config, FAST_CONFIG
17
+ from .core.models import AnalysisResult, FunctionInfo, ClassInfo, Pattern
18
+
19
+ # NLP Processing Pipeline
20
+ from .nlp import (
21
+ NLPPipeline,
22
+ QueryNormalizer,
23
+ IntentMatcher,
24
+ EntityResolver,
25
+ NLPConfig,
26
+ FAST_NLP_CONFIG,
27
+ PRECISE_NLP_CONFIG,
28
+ )
29
+
30
+ __all__ = [
31
+ # Core
32
+ "ProjectAnalyzer",
33
+ "Config",
34
+ "FAST_CONFIG",
35
+ "AnalysisResult",
36
+ "FunctionInfo",
37
+ "ClassInfo",
38
+ "Pattern",
39
+ # NLP Pipeline
40
+ "NLPPipeline",
41
+ "QueryNormalizer",
42
+ "IntentMatcher",
43
+ "EntityResolver",
44
+ "NLPConfig",
45
+ "FAST_NLP_CONFIG",
46
+ "PRECISE_NLP_CONFIG",
47
+ ]
code2flow/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Entry point for running code2flow as a module."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == '__main__':
6
+ main()
@@ -0,0 +1,17 @@
1
+ """Analysis package for code2flow."""
2
+
3
+ from .cfg import CFGExtractor
4
+ from .dfg import DFGExtractor
5
+ from .call_graph import CallGraphExtractor
6
+ from .coupling import CouplingAnalyzer
7
+ from .smells import SmellDetector
8
+ from .data_analysis import DataAnalyzer
9
+
10
+ __all__ = [
11
+ 'CFGExtractor',
12
+ 'DFGExtractor',
13
+ 'CallGraphExtractor',
14
+ 'CouplingAnalyzer',
15
+ 'SmellDetector',
16
+ 'DataAnalyzer'
17
+ ]
@@ -0,0 +1,210 @@
1
+ """Call graph extractor using AST."""
2
+
3
+ import ast
4
+ from typing import Optional, Set, List, Dict
5
+ import astroid
6
+
7
+ from ..core.config import Config
8
+ from ..core.models import AnalysisResult, FlowEdge
9
+
10
+
11
+ class CallGraphExtractor(ast.NodeVisitor):
12
+ """Extract call graph from AST."""
13
+
14
+ def __init__(self, config: Config):
15
+ self.config = config
16
+ self.result = AnalysisResult()
17
+ self.module_name = ""
18
+ self.file_path = ""
19
+
20
+ # Context
21
+ self.function_stack = []
22
+ self.class_stack = []
23
+ self.imports = {}
24
+ self.astroid_tree = None
25
+
26
+ def extract(self, tree: ast.AST, module_name: str, file_path: str) -> AnalysisResult:
27
+ """Extract call graph from AST."""
28
+ self.result = AnalysisResult()
29
+ self.module_name = module_name
30
+ self.file_path = file_path
31
+ self.function_stack = []
32
+ self.class_stack = []
33
+ self.imports = {}
34
+
35
+ # Try to get astroid tree for better resolution
36
+ try:
37
+ self.astroid_tree = astroid.MANAGER.ast_from_file(file_path)
38
+ except Exception:
39
+ self.astroid_tree = None
40
+
41
+ self.visit(tree)
42
+ self._calculate_metrics()
43
+ return self.result
44
+
45
+ def _calculate_metrics(self):
46
+ """Calculate fan-in and fan-out metrics."""
47
+ # First, populate called_by for all functions
48
+ for caller_name, caller_info in self.result.functions.items():
49
+ for callee_name in caller_info.calls:
50
+ if callee_name in self.result.functions:
51
+ self.result.functions[callee_name].called_by.append(caller_name)
52
+
53
+ # Then calculate metrics
54
+ for func_name, func_info in self.result.functions.items():
55
+ fan_out = len(set(func_info.calls))
56
+ fan_in = len(set(func_info.called_by))
57
+
58
+ self.result.metrics[func_name] = {
59
+ "fan_in": fan_in,
60
+ "fan_out": fan_out,
61
+ "complexity": getattr(func_info, 'complexity', 1) # Placeholder for now
62
+ }
63
+
64
+ def visit_Import(self, node: ast.Import):
65
+ """Track imports."""
66
+ for alias in node.names:
67
+ name = alias.asname if alias.asname else alias.name
68
+ self.imports[name] = alias.name
69
+ self.result.imports[name] = alias.name
70
+
71
+ def visit_ImportFrom(self, node: ast.ImportFrom):
72
+ """Track from imports."""
73
+ module = node.module or ""
74
+ for alias in node.names:
75
+ name = alias.asname if alias.asname else alias.name
76
+ full_name = f"{module}.{alias.name}" if module else alias.name
77
+ self.imports[name] = full_name
78
+ self.result.imports[name] = full_name
79
+
80
+ def visit_ClassDef(self, node: ast.ClassDef):
81
+ """Visit class definition."""
82
+ self.class_stack.append(node.name)
83
+
84
+ # Store class info
85
+ self.result.classes[node.name] = {
86
+ 'file': self.file_path,
87
+ 'line': node.lineno,
88
+ 'methods': [m.name for m in node.body if isinstance(m, ast.FunctionDef)],
89
+ 'bases': [self._expr_to_str(b) for b in node.bases]
90
+ }
91
+
92
+ for stmt in node.body:
93
+ self.visit(stmt)
94
+
95
+ self.class_stack.pop()
96
+
97
+ def visit_FunctionDef(self, node: ast.FunctionDef):
98
+ """Visit function definition and track calls within it."""
99
+ func_name = self._qualified_name(node.name)
100
+ self.function_stack.append(func_name)
101
+
102
+ # Visit body to find calls
103
+ for stmt in node.body:
104
+ self.visit(stmt)
105
+
106
+ self.function_stack.pop()
107
+
108
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
109
+ """Visit async function."""
110
+ self.visit_FunctionDef(node)
111
+
112
+ def visit_Call(self, node: ast.Call):
113
+ """Track function calls."""
114
+ if not self.function_stack:
115
+ self.generic_visit(node)
116
+ return
117
+
118
+ caller = self.function_stack[-1]
119
+ callee = self._resolve_call(node.func)
120
+
121
+ # If ast-based resolution failed or returned None.sth, try astroid
122
+ if (not callee or 'None.' in callee) and self.astroid_tree:
123
+ astroid_callee = self._resolve_with_astroid(node)
124
+ if astroid_callee:
125
+ callee = astroid_callee
126
+
127
+ if callee and caller in self.result.functions:
128
+ self.result.functions[caller].calls.append(callee)
129
+
130
+ # Create call edge
131
+ edge = FlowEdge(
132
+ source=-1, # Will be resolved
133
+ target=-1,
134
+ edge_type="call",
135
+ metadata={'caller': caller, 'callee': callee}
136
+ )
137
+ self.result.call_edges.append(edge)
138
+
139
+ self.generic_visit(node)
140
+
141
+ def _qualified_name(self, name: str) -> str:
142
+ """Get fully qualified name."""
143
+ parts = [self.module_name]
144
+ if self.class_stack:
145
+ parts.append(self.class_stack[-1])
146
+ parts.append(name)
147
+ return '.'.join(parts)
148
+
149
+ def _resolve_call(self, node: ast.AST) -> Optional[str]:
150
+ """Resolve a call to its full name."""
151
+ if isinstance(node, ast.Name):
152
+ # Simple function call
153
+ if node.id in self.imports:
154
+ return self.imports[node.id]
155
+ return f"{self.module_name}.{node.id}"
156
+
157
+ elif isinstance(node, ast.Attribute):
158
+ # Method or module.function call
159
+ parts = []
160
+ current = node
161
+
162
+ while isinstance(current, ast.Attribute):
163
+ parts.append(current.attr)
164
+ current = current.value
165
+
166
+ if isinstance(current, ast.Name):
167
+ parts.append(current.id)
168
+ parts.reverse()
169
+
170
+ # Check if root is an import
171
+ root = parts[0]
172
+ if root in self.imports:
173
+ return f"{self.imports[root]}.{'.'.join(parts[1:])}"
174
+
175
+ # Check for self/cls
176
+ if root in ('self', 'cls') and self.class_stack:
177
+ return f"{self.module_name}.{self.class_stack[-1]}.{'.'.join(parts[1:])}"
178
+
179
+ return f"{self.module_name}.{'.'.join(parts)}"
180
+
181
+ return None
182
+
183
+ def _resolve_with_astroid(self, node: ast.Call) -> Optional[str]:
184
+ """Use astroid to infer the call target."""
185
+ if not self.astroid_tree:
186
+ return None
187
+
188
+ try:
189
+ # Find the corresponding astroid node by line/col
190
+ # This is a bit slow but robust
191
+ for astroid_node in self.astroid_tree.nodes_of_class(astroid.Call):
192
+ if astroid_node.lineno == node.lineno and astroid_node.col_offset == node.col_offset:
193
+ # Infer the targets
194
+ inferred = astroid_node.func.infer()
195
+ for target in inferred:
196
+ if hasattr(target, 'qname'):
197
+ return target.qname()
198
+ break
199
+ except Exception:
200
+ pass
201
+ return None
202
+
203
+ def _expr_to_str(self, node: ast.AST) -> str:
204
+ """Convert AST expression to string."""
205
+ if node is None:
206
+ return ""
207
+ try:
208
+ return ast.unparse(node) if hasattr(ast, 'unparse') else str(node)
209
+ except:
210
+ return str(node)
@@ -0,0 +1,293 @@
1
+ """Control Flow Graph (CFG) extractor using AST."""
2
+
3
+ import ast
4
+ from collections import defaultdict
5
+ from typing import Optional
6
+
7
+ from ..core.config import Config
8
+ from ..core.models import AnalysisResult, FlowNode, FlowEdge, FunctionInfo
9
+
10
+
11
+ class CFGExtractor(ast.NodeVisitor):
12
+ """Extract Control Flow Graph from AST."""
13
+
14
+ def __init__(self, config: Config):
15
+ self.config = config
16
+ self.result = AnalysisResult()
17
+ self.module_name = ""
18
+ self.file_path = ""
19
+ self.node_counter = 0
20
+
21
+ # Context tracking
22
+ self.function_stack = []
23
+ self.class_stack = []
24
+ self.current_node = None
25
+ self.entry_nodes = {} # Function -> entry node ID
26
+
27
+ def extract(self, tree: ast.AST, module_name: str, file_path: str) -> AnalysisResult:
28
+ """Extract CFG from AST."""
29
+ self.result = AnalysisResult()
30
+ self.module_name = module_name
31
+ self.file_path = file_path
32
+ self.node_counter = 0
33
+
34
+ self.visit(tree)
35
+ return self.result
36
+
37
+ def new_node(self, node_type: str, label: str, **kwargs) -> int:
38
+ """Create new flow node."""
39
+ node_id = self.node_counter
40
+ self.node_counter += 1
41
+
42
+ node = FlowNode(
43
+ id=node_id,
44
+ type=node_type,
45
+ label=label,
46
+ function=self.function_stack[-1] if self.function_stack else None,
47
+ file=self.file_path,
48
+ line=kwargs.get('line'),
49
+ column=kwargs.get('column'),
50
+ conditions=kwargs.get('conditions', []),
51
+ data_flow=kwargs.get('data_flow', [])
52
+ )
53
+
54
+ self.result.nodes[node_id] = node
55
+ return node_id
56
+
57
+ def connect(self, source: Optional[int], target: Optional[int],
58
+ edge_type: str = "control", condition: Optional[str] = None):
59
+ """Create edge between nodes."""
60
+ if source is not None and target is not None:
61
+ edge = FlowEdge(
62
+ source=source,
63
+ target=target,
64
+ edge_type=edge_type,
65
+ condition=condition
66
+ )
67
+ self.result.cfg_edges.append(edge)
68
+
69
+ def visit_FunctionDef(self, node: ast.FunctionDef):
70
+ """Visit function definition."""
71
+ func_name = self._qualified_name(node.name)
72
+ self.function_stack.append(func_name)
73
+
74
+ # Create entry node
75
+ entry = self.new_node("FUNC", f"FUNC:{func_name}", line=node.lineno)
76
+ self.entry_nodes[func_name] = entry
77
+
78
+ # Track previous node
79
+ prev_node = self.current_node
80
+ self.current_node = entry
81
+
82
+ # Store function info
83
+ func_info = FunctionInfo(
84
+ name=node.name,
85
+ qualified_name=func_name,
86
+ file=self.file_path,
87
+ line_start=node.lineno,
88
+ line_end=node.end_lineno or node.lineno,
89
+ args=[arg.arg for arg in node.args.args]
90
+ )
91
+ self.result.functions[func_name] = func_info
92
+
93
+ # Visit body
94
+ for stmt in node.body:
95
+ self.visit(stmt)
96
+
97
+ # Create exit node
98
+ exit_node = self.new_node("RETURN", f"RETURN:{func_name}",
99
+ line=node.end_lineno or node.lineno)
100
+ self.connect(self.current_node, exit_node)
101
+
102
+ # Restore context
103
+ self.function_stack.pop()
104
+ self.current_node = prev_node
105
+
106
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
107
+ """Visit async function definition."""
108
+ self.visit_FunctionDef(node) # Treat same as sync for CFG
109
+
110
+ def visit_If(self, node: ast.If):
111
+ """Visit if statement."""
112
+ # Create condition node
113
+ condition = self._extract_condition(node.test)
114
+ cond_node = self.new_node("IF", condition, line=node.lineno)
115
+ self.connect(self.current_node, cond_node, condition=condition)
116
+
117
+ # Save current for branches
118
+ branch_entry = cond_node
119
+
120
+ # Visit then branch
121
+ then_last = []
122
+ for stmt in node.body:
123
+ prev = self.current_node
124
+ self.current_node = branch_entry
125
+ self.visit(stmt)
126
+ then_last.append(self.current_node)
127
+ branch_entry = self.current_node
128
+
129
+ # Visit else branch
130
+ else_last = []
131
+ if node.orelse:
132
+ branch_entry = cond_node
133
+ for stmt in node.orelse:
134
+ prev = self.current_node
135
+ self.current_node = branch_entry
136
+ self.visit(stmt)
137
+ else_last.append(self.current_node)
138
+ branch_entry = self.current_node
139
+
140
+ # Merge point
141
+ merge_node = self.new_node("MERGE", "merge", line=node.end_lineno)
142
+ for last in then_last + else_last:
143
+ self.connect(last, merge_node)
144
+
145
+ self.current_node = merge_node
146
+
147
+ def visit_For(self, node: ast.For):
148
+ """Visit for loop."""
149
+ # Create loop header
150
+ iter_str = self._expr_to_str(node.iter)
151
+ target_str = self._expr_to_str(node.target)
152
+ loop_header = self.new_node("FOR", f"for {target_str} in {iter_str}",
153
+ line=node.lineno)
154
+ self.connect(self.current_node, loop_header)
155
+
156
+ # Loop body
157
+ body_entry = loop_header
158
+ body_last = []
159
+ for stmt in node.body:
160
+ self.current_node = body_entry
161
+ self.visit(stmt)
162
+ body_last.append(self.current_node)
163
+ body_entry = self.current_node
164
+
165
+ # Back edge to header
166
+ for last in body_last:
167
+ self.connect(last, loop_header, edge_type="loop")
168
+
169
+ # Exit (after loop)
170
+ exit_node = self.new_node("EXIT_LOOP", "exit_loop", line=node.end_lineno)
171
+ self.connect(loop_header, exit_node) # False branch
172
+ self.current_node = exit_node
173
+
174
+ def visit_While(self, node: ast.While):
175
+ """Visit while loop."""
176
+ # Loop header with condition
177
+ condition = self._extract_condition(node.test)
178
+ loop_header = self.new_node("WHILE", f"while {condition}", line=node.lineno)
179
+ self.connect(self.current_node, loop_header)
180
+
181
+ # Loop body
182
+ body_entry = loop_header
183
+ body_last = []
184
+ for stmt in node.body:
185
+ self.current_node = body_entry
186
+ self.visit(stmt)
187
+ body_last.append(self.current_node)
188
+ body_entry = self.current_node
189
+
190
+ # Back edge
191
+ for last in body_last:
192
+ self.connect(last, loop_header, edge_type="loop")
193
+
194
+ # Exit
195
+ exit_node = self.new_node("EXIT_LOOP", "exit_loop", line=node.end_lineno)
196
+ self.connect(loop_header, exit_node, condition="False")
197
+ self.current_node = exit_node
198
+
199
+ def visit_Try(self, node: ast.Try):
200
+ """Visit try statement."""
201
+ try_entry = self.new_node("TRY", "try", line=node.lineno)
202
+ self.connect(self.current_node, try_entry)
203
+
204
+ # Try body
205
+ self.current_node = try_entry
206
+ for stmt in node.body:
207
+ self.visit(stmt)
208
+ try_last = self.current_node
209
+
210
+ # Except handlers
211
+ for handler in node.handlers:
212
+ handler_node = self.new_node("EXCEPT", self._format_except(handler),
213
+ line=handler.lineno)
214
+ self.connect(try_entry, handler_node, edge_type="exception")
215
+
216
+ self.current_node = handler_node
217
+ for stmt in handler.body:
218
+ self.visit(stmt)
219
+
220
+ # Merge
221
+ merge = self.new_node("MERGE", "merge", line=node.end_lineno)
222
+ self.connect(try_last, merge)
223
+ self.current_node = merge
224
+
225
+ def visit_Assign(self, node: ast.Assign):
226
+ """Visit assignment."""
227
+ targets = [self._expr_to_str(t) for t in node.targets]
228
+ value = self._expr_to_str(node.value)
229
+ label = f"{' = '.join(targets)} = {value[:50]}"
230
+
231
+ assign_node = self.new_node("ASSIGN", label, line=node.lineno)
232
+ self.connect(self.current_node, assign_node)
233
+ self.current_node = assign_node
234
+
235
+ def visit_Return(self, node: ast.Return):
236
+ """Visit return statement."""
237
+ value = self._expr_to_str(node.value) if node.value else "None"
238
+ return_node = self.new_node("RETURN", f"return {value[:50]}", line=node.lineno)
239
+ self.connect(self.current_node, return_node)
240
+ self.current_node = return_node
241
+
242
+ def visit_Expr(self, node: ast.Expr):
243
+ """Visit expression statement."""
244
+ if isinstance(node.value, ast.Call):
245
+ # Function call
246
+ call_name = self._expr_to_str(node.value.func)
247
+ args = [self._expr_to_str(a) for a in node.value.args]
248
+ label = f"CALL {call_name}({', '.join(args)})"[:80]
249
+
250
+ call_node = self.new_node("CALL", label, line=node.lineno)
251
+ self.connect(self.current_node, call_node)
252
+ self.current_node = call_node
253
+
254
+ # Track call in function info
255
+ if self.function_stack:
256
+ func_name = self.function_stack[-1]
257
+ if func_name in self.result.functions:
258
+ self.result.functions[func_name].calls.add(call_name)
259
+ else:
260
+ self.generic_visit(node)
261
+
262
+ def _qualified_name(self, name: str) -> str:
263
+ """Get fully qualified name."""
264
+ parts = [self.module_name]
265
+ if self.class_stack:
266
+ parts.append(self.class_stack[-1])
267
+ parts.append(name)
268
+ return '.'.join(parts)
269
+
270
+ def _extract_condition(self, node: ast.AST) -> str:
271
+ """Extract condition as string."""
272
+ try:
273
+ return ast.unparse(node) if hasattr(ast, 'unparse') else str(node)[:50]
274
+ except:
275
+ return str(node)[:50]
276
+
277
+ def _expr_to_str(self, node: ast.AST) -> str:
278
+ """Convert AST expression to string."""
279
+ if node is None:
280
+ return "None"
281
+ try:
282
+ return ast.unparse(node) if hasattr(ast, 'unparse') else str(node)
283
+ except:
284
+ return str(node)
285
+
286
+ def _format_except(self, handler: ast.ExceptHandler) -> str:
287
+ """Format except handler."""
288
+ if handler.type:
289
+ type_str = self._expr_to_str(handler.type)
290
+ if handler.name:
291
+ return f"except {type_str} as {handler.name}"
292
+ return f"except {type_str}"
293
+ return "except"
@@ -0,0 +1,77 @@
1
+ """Analysis of coupling between modules."""
2
+ from typing import Dict, List, Set, Any
3
+ from ..core.models import AnalysisResult
4
+
5
+ class CouplingAnalyzer:
6
+ """Analyze coupling between modules."""
7
+
8
+ def __init__(self, result: AnalysisResult):
9
+ self.result = result
10
+
11
+ def analyze(self) -> Dict[str, Any]:
12
+ """Perform coupling analysis."""
13
+ coupling_data = {
14
+ "module_interactions": self._analyze_module_interactions(),
15
+ "data_leakage": self._detect_data_leakage(),
16
+ "shared_state": self._detect_shared_state()
17
+ }
18
+ self.result.coupling = coupling_data
19
+ return coupling_data
20
+
21
+ def _analyze_module_interactions(self) -> Dict[str, Set[str]]:
22
+ """Track which modules call which other modules."""
23
+ interactions = {}
24
+ for func_name, func_info in self.result.functions.items():
25
+ caller_mod = func_info.module or func_name.split('.')[0]
26
+ if caller_mod not in interactions:
27
+ interactions[caller_mod] = set()
28
+
29
+ for callee in func_info.calls:
30
+ callee_mod = callee.split('.')[0]
31
+ if callee_mod != caller_mod:
32
+ interactions[caller_mod].add(callee_mod)
33
+
34
+ # Convert sets to lists for JSON serialization
35
+ return {k: list(v) for k, v in interactions.items()}
36
+
37
+ def _detect_data_leakage(self) -> List[Dict[str, Any]]:
38
+ """Detect when a module mutates data defined in another module."""
39
+ leakages = []
40
+ # Heuristic: if a function in module A mutates an object passed from module B
41
+ # This is simplified: we look for mutations where the scope module
42
+ # is different from the variable's likely origin.
43
+ for mutation in self.result.mutations:
44
+ scope_parts = mutation.scope.split('.')
45
+ mut_mod = scope_parts[0]
46
+
47
+ # If the variable name looks like it belongs to another module (e.g. 'other_mod.data')
48
+ if '.' in mutation.variable:
49
+ origin_mod = mutation.variable.split('.')[0]
50
+ if origin_mod != mut_mod and origin_mod in self.result.modules:
51
+ leakages.append({
52
+ "variable": mutation.variable,
53
+ "mutator_module": mut_mod,
54
+ "origin_module": origin_mod,
55
+ "line": mutation.line,
56
+ "file": mutation.file
57
+ })
58
+ return leakages
59
+
60
+ def _detect_shared_state(self) -> List[Dict[str, Any]]:
61
+ """Detect modules that access/mutate the same global/shared variables."""
62
+ shared = []
63
+ variable_accessors = {} # var -> set(modules)
64
+
65
+ for mutation in self.result.mutations:
66
+ mut_mod = mutation.scope.split('.')[0]
67
+ if mutation.variable not in variable_accessors:
68
+ variable_accessors[mutation.variable] = set()
69
+ variable_accessors[mutation.variable].add(mut_mod)
70
+
71
+ for var, mods in variable_accessors.items():
72
+ if len(mods) > 1:
73
+ shared.append({
74
+ "variable": var,
75
+ "modules": list(mods)
76
+ })
77
+ return shared