interlinked-mapper 0.1.0__tar.gz
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.
- interlinked_mapper-0.1.0/PKG-INFO +26 -0
- interlinked_mapper-0.1.0/interlinked/__init__.py +3 -0
- interlinked_mapper-0.1.0/interlinked/analyzer/__init__.py +7 -0
- interlinked_mapper-0.1.0/interlinked/analyzer/dead_code.py +137 -0
- interlinked_mapper-0.1.0/interlinked/analyzer/graph.py +822 -0
- interlinked_mapper-0.1.0/interlinked/analyzer/parser.py +1141 -0
- interlinked_mapper-0.1.0/interlinked/analyzer/similarity.py +486 -0
- interlinked_mapper-0.1.0/interlinked/cli.py +136 -0
- interlinked_mapper-0.1.0/interlinked/commander/__init__.py +6 -0
- interlinked_mapper-0.1.0/interlinked/commander/llm.py +304 -0
- interlinked_mapper-0.1.0/interlinked/commander/query.py +966 -0
- interlinked_mapper-0.1.0/interlinked/commander/repl.py +50 -0
- interlinked_mapper-0.1.0/interlinked/mcp_server.py +324 -0
- interlinked_mapper-0.1.0/interlinked/models.py +107 -0
- interlinked_mapper-0.1.0/interlinked/visualizer/__init__.py +1 -0
- interlinked_mapper-0.1.0/interlinked/visualizer/layouts.py +181 -0
- interlinked_mapper-0.1.0/interlinked/visualizer/server.py +428 -0
- interlinked_mapper-0.1.0/interlinked_mapper.egg-info/PKG-INFO +26 -0
- interlinked_mapper-0.1.0/interlinked_mapper.egg-info/SOURCES.txt +23 -0
- interlinked_mapper-0.1.0/interlinked_mapper.egg-info/dependency_links.txt +1 -0
- interlinked_mapper-0.1.0/interlinked_mapper.egg-info/entry_points.txt +2 -0
- interlinked_mapper-0.1.0/interlinked_mapper.egg-info/requires.txt +6 -0
- interlinked_mapper-0.1.0/interlinked_mapper.egg-info/top_level.txt +1 -0
- interlinked_mapper-0.1.0/pyproject.toml +43 -0
- interlinked_mapper-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: interlinked-mapper
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python program topology explorer — visualize the shape of your codebase
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/austerecryptid/interlinked
|
|
7
|
+
Project-URL: Repository, https://github.com/austerecryptid/interlinked
|
|
8
|
+
Keywords: ast,code-analysis,topology,graph,visualization,mcp,networkx
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
12
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Framework :: FastAPI
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: networkx>=3.1
|
|
22
|
+
Requires-Dist: fastapi>=0.110
|
|
23
|
+
Requires-Dist: uvicorn[standard]>=0.29
|
|
24
|
+
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Requires-Dist: mcp>=1.0
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Analyzer — static analysis and graph extraction from Python source."""
|
|
2
|
+
|
|
3
|
+
from interlinked.analyzer.parser import parse_project
|
|
4
|
+
from interlinked.analyzer.graph import CodeGraph
|
|
5
|
+
from interlinked.analyzer.dead_code import detect_dead_code
|
|
6
|
+
|
|
7
|
+
__all__ = ["parse_project", "CodeGraph", "detect_dead_code"]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Dead code detection via NetworkX graph queries.
|
|
2
|
+
|
|
3
|
+
All detection is done through native NetworkX degree/reachability queries
|
|
4
|
+
on the typed MultiDiGraph. No manual graph traversal.
|
|
5
|
+
|
|
6
|
+
Detects:
|
|
7
|
+
- Uncalled functions/methods (zero in-degree on 'calls' + 'reads' keys)
|
|
8
|
+
- Dead parameters (zero in-degree on 'reads' key)
|
|
9
|
+
- Dead variables (has 'writes' in-edges but zero 'reads' in-edges)
|
|
10
|
+
- Dead returns (RETURNS edges whose targets have zero 'reads' in-edges)
|
|
11
|
+
- Dead imports (target not a project node and not a prefix of one)
|
|
12
|
+
- Transitive dead (all ancestors in calls graph are already dead)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import networkx as nx
|
|
18
|
+
|
|
19
|
+
from interlinked.analyzer.graph import CodeGraph
|
|
20
|
+
from interlinked.models import EdgeType, SymbolType
|
|
21
|
+
|
|
22
|
+
_EXEMPT: frozenset[str] = frozenset({
|
|
23
|
+
"__init__", "__new__", "__del__", "__repr__", "__str__",
|
|
24
|
+
"__enter__", "__exit__", "__aenter__", "__aexit__",
|
|
25
|
+
"__iter__", "__next__", "__len__", "__getitem__", "__setitem__",
|
|
26
|
+
"__contains__", "__hash__", "__eq__", "__ne__", "__lt__", "__gt__",
|
|
27
|
+
"__le__", "__ge__", "__bool__", "__call__", "__get__", "__set__",
|
|
28
|
+
"__post_init__", "main", "setup", "teardown",
|
|
29
|
+
"setUp", "tearDown", "setUpClass", "tearDownClass",
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def detect_dead_code(graph: CodeGraph) -> list[str]:
|
|
34
|
+
"""Mark dead nodes/edges using NetworkX queries. Returns dead node IDs."""
|
|
35
|
+
G = graph._g
|
|
36
|
+
dead: set[str] = set()
|
|
37
|
+
node_ids = {n.id for n in graph.all_nodes(include_proposed=False)}
|
|
38
|
+
|
|
39
|
+
# ── Uncalled functions ────────────────────────────────────────
|
|
40
|
+
for n in graph.all_nodes(include_proposed=False):
|
|
41
|
+
if n.symbol_type not in (SymbolType.FUNCTION, SymbolType.METHOD):
|
|
42
|
+
continue
|
|
43
|
+
if n.name in _EXEMPT or n.name.startswith("test_"):
|
|
44
|
+
continue
|
|
45
|
+
if n.id not in G:
|
|
46
|
+
continue
|
|
47
|
+
# No incoming calls AND no incoming reads (callback references)
|
|
48
|
+
has_caller = any(
|
|
49
|
+
d.get("edge_type") in ("calls", "reads")
|
|
50
|
+
for _, _, d in G.in_edges(n.id, data=True)
|
|
51
|
+
)
|
|
52
|
+
if not has_caller:
|
|
53
|
+
n.is_dead = True
|
|
54
|
+
dead.add(n.id)
|
|
55
|
+
|
|
56
|
+
# ── Dead parameters (never read) ─────────────────────────────
|
|
57
|
+
for n in graph.all_nodes(include_proposed=False):
|
|
58
|
+
if n.symbol_type != SymbolType.PARAMETER or n.name in ("self", "cls"):
|
|
59
|
+
continue
|
|
60
|
+
# Skip params of already-dead functions
|
|
61
|
+
parent = n.id.rsplit(".", 1)[0] if "." in n.id else ""
|
|
62
|
+
if parent in dead:
|
|
63
|
+
continue
|
|
64
|
+
if n.id not in G:
|
|
65
|
+
continue
|
|
66
|
+
has_reader = any(
|
|
67
|
+
d.get("edge_type") == "reads"
|
|
68
|
+
for _, _, d in G.in_edges(n.id, data=True)
|
|
69
|
+
)
|
|
70
|
+
if not has_reader:
|
|
71
|
+
n.is_dead = True
|
|
72
|
+
dead.add(n.id)
|
|
73
|
+
|
|
74
|
+
# ── Dead variables (written but never read) ──────────────────
|
|
75
|
+
for n in graph.all_nodes(include_proposed=False):
|
|
76
|
+
if n.symbol_type != SymbolType.VARIABLE:
|
|
77
|
+
continue
|
|
78
|
+
if n.id not in G:
|
|
79
|
+
continue
|
|
80
|
+
has_writer = any(d.get("edge_type") == "writes" for _, _, d in G.in_edges(n.id, data=True))
|
|
81
|
+
has_reader = any(d.get("edge_type") == "reads" for _, _, d in G.in_edges(n.id, data=True))
|
|
82
|
+
if has_writer and not has_reader:
|
|
83
|
+
n.is_dead = True
|
|
84
|
+
dead.add(n.id)
|
|
85
|
+
|
|
86
|
+
# ── Dead returns (return value never read) ───────────────────
|
|
87
|
+
for n in graph.all_nodes(include_proposed=False):
|
|
88
|
+
if n.symbol_type not in (SymbolType.FUNCTION, SymbolType.METHOD):
|
|
89
|
+
continue
|
|
90
|
+
if n.id in dead or n.id not in G:
|
|
91
|
+
continue
|
|
92
|
+
ret_targets = [
|
|
93
|
+
v for _, v, d in G.out_edges(n.id, data=True)
|
|
94
|
+
if d.get("edge_type") == "returns"
|
|
95
|
+
]
|
|
96
|
+
if not ret_targets:
|
|
97
|
+
continue
|
|
98
|
+
any_read = any(
|
|
99
|
+
any(d.get("edge_type") == "reads" for _, _, d in G.in_edges(rt, data=True))
|
|
100
|
+
for rt in ret_targets
|
|
101
|
+
)
|
|
102
|
+
if not any_read:
|
|
103
|
+
# Mark the return edges as dead, not the function
|
|
104
|
+
for e in graph.edges_from(n.id, EdgeType.RETURNS):
|
|
105
|
+
e.is_dead = True
|
|
106
|
+
|
|
107
|
+
# ── Dead imports ─────────────────────────────────────────────
|
|
108
|
+
for e in graph.all_edges(include_proposed=False):
|
|
109
|
+
if e.edge_type != EdgeType.IMPORTS:
|
|
110
|
+
continue
|
|
111
|
+
if e.target not in node_ids:
|
|
112
|
+
is_prefix = any(nid.startswith(e.target) for nid in node_ids)
|
|
113
|
+
if is_prefix:
|
|
114
|
+
e.is_dead = True
|
|
115
|
+
|
|
116
|
+
# ── Transitive dead ──────────────────────────────────────────
|
|
117
|
+
# If ALL ancestors of a function in the calls graph are dead, it's dead too
|
|
118
|
+
calls_graph = G.edge_subgraph(
|
|
119
|
+
[(u, v, k) for u, v, k in G.edges(keys=True) if k == "calls"]
|
|
120
|
+
)
|
|
121
|
+
changed = True
|
|
122
|
+
while changed:
|
|
123
|
+
changed = False
|
|
124
|
+
for n in graph.all_nodes(include_proposed=False):
|
|
125
|
+
if n.is_dead or n.id not in calls_graph:
|
|
126
|
+
continue
|
|
127
|
+
if n.symbol_type not in (SymbolType.FUNCTION, SymbolType.METHOD):
|
|
128
|
+
continue
|
|
129
|
+
if n.name in _EXEMPT or n.name.startswith("test_"):
|
|
130
|
+
continue
|
|
131
|
+
callers = set(calls_graph.predecessors(n.id))
|
|
132
|
+
if callers and callers.issubset(dead):
|
|
133
|
+
n.is_dead = True
|
|
134
|
+
dead.add(n.id)
|
|
135
|
+
changed = True
|
|
136
|
+
|
|
137
|
+
return list(dead)
|