interlinked-mapper 0.3.9__tar.gz → 0.3.11__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.3.9 → interlinked_mapper-0.3.11}/PKG-INFO +1 -1
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/analyzer/graph.py +34 -20
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/analyzer/parser.py +12 -1
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/analyzer/similarity.py +2 -2
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/mcp_server.py +2 -11
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/PKG-INFO +1 -1
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/SOURCES.txt +2 -1
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/pyproject.toml +1 -1
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/tests/test_accuracy.py +85 -0
- interlinked_mapper-0.3.11/tests/test_query_completeness.py +506 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/__init__.py +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/analyzer/__init__.py +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/analyzer/dead_code.py +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/analyzer/embeddings.py +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/cli.py +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/commander/__init__.py +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/commander/llm.py +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/commander/query.py +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/commander/repl.py +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/models.py +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/__init__.py +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/dist/assets/index-CyhrxsQU.css +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/dist/assets/index-Dh01aXoE.js +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/dist/index.html +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/index.html +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/index.html.d3-legacy +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/package-lock.json +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/package.json +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/App.tsx +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/graph/GraphCanvas.tsx +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/graph/nodePrograms.ts +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/index.css +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/main.tsx +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/state/graphStore.ts +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/state/sseClient.ts +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/theme.ts +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/types.ts +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/vite-env.d.ts +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/tsconfig.json +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/vite.config.ts +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/layouts.py +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/server.py +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/dependency_links.txt +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/entry_points.txt +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/requires.txt +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/top_level.txt +0 -0
- {interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/setup.cfg +0 -0
|
@@ -15,6 +15,7 @@ from interlinked.models import (
|
|
|
15
15
|
# Method names so common on builtins (dict, list, set, str, etc.) that resolving
|
|
16
16
|
# them to project symbols by bare-name matching is almost always a false positive.
|
|
17
17
|
# e.g. `op_dict.items()` should NOT resolve to `ActionCost.items`.
|
|
18
|
+
# Checked against the LAST component of dotted targets (e.g. "effect.get" → "get").
|
|
18
19
|
_BUILTIN_METHOD_NAMES: frozenset[str] = frozenset({
|
|
19
20
|
# dict
|
|
20
21
|
"items", "keys", "values", "get", "pop", "update", "setdefault",
|
|
@@ -28,8 +29,9 @@ _BUILTIN_METHOD_NAMES: frozenset[str] = frozenset({
|
|
|
28
29
|
# str
|
|
29
30
|
"strip", "split", "join", "replace", "startswith", "endswith",
|
|
30
31
|
"lower", "upper", "format", "encode", "decode",
|
|
31
|
-
# general
|
|
32
|
+
# general / logging
|
|
32
33
|
"close", "read", "write", "flush", "seek", "tell",
|
|
34
|
+
"warning", "error", "info", "debug", "exception", "critical",
|
|
33
35
|
})
|
|
34
36
|
|
|
35
37
|
|
|
@@ -236,36 +238,48 @@ class CodeGraph:
|
|
|
236
238
|
source = next(iter(candidates))
|
|
237
239
|
|
|
238
240
|
if target not in node_ids:
|
|
239
|
-
#
|
|
240
|
-
#
|
|
241
|
-
|
|
242
|
-
if
|
|
241
|
+
# Check the last dotted component against builtin method names.
|
|
242
|
+
# Catches both bare "items" and dotted "effect.get", "args.items".
|
|
243
|
+
leaf = target.rsplit(".", 1)[-1]
|
|
244
|
+
if leaf in _BUILTIN_METHOD_NAMES:
|
|
243
245
|
return edge
|
|
244
246
|
|
|
247
|
+
# Dotted targets like "effect.get" or "logger.warning" where the
|
|
248
|
+
# root is NOT a known node are local-variable method calls —
|
|
249
|
+
# resolving them through the name index produces false positives.
|
|
250
|
+
if "." in target:
|
|
251
|
+
root = target.split(".", 1)[0]
|
|
252
|
+
# If the root isn't itself a project node, it's a local var
|
|
253
|
+
if root not in node_ids and root not in name_index:
|
|
254
|
+
return edge
|
|
255
|
+
|
|
245
256
|
candidates = name_index.get(target, set())
|
|
246
257
|
if len(candidates) == 1:
|
|
247
258
|
target = next(iter(candidates))
|
|
248
259
|
elif len(candidates) > 1:
|
|
249
|
-
#
|
|
250
|
-
# NEVER resolves to the same class — you need `self.process()`
|
|
251
|
-
# for that. Exclude the source itself to prevent self-call
|
|
252
|
-
# artifacts, and prefer module-level over class-level.
|
|
260
|
+
# Exclude self-calls (bare `process()` != `self.process()`)
|
|
253
261
|
filtered = candidates - {source}
|
|
254
262
|
if not filtered:
|
|
255
263
|
filtered = candidates
|
|
256
264
|
|
|
257
|
-
#
|
|
265
|
+
# Prefer candidates sharing the longest common prefix with
|
|
266
|
+
# the source. This ensures a call from engine.rules.resolver
|
|
267
|
+
# to _resolve_entity_ref picks resolver's own definition
|
|
268
|
+
# over engine.rules.field_paths._resolve_entity_ref.
|
|
258
269
|
src_parts = source.split(".")
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
270
|
+
|
|
271
|
+
def _common_prefix_len(c: str) -> int:
|
|
272
|
+
c_parts = c.split(".")
|
|
273
|
+
n = 0
|
|
274
|
+
for a, b in zip(src_parts, c_parts):
|
|
275
|
+
if a == b:
|
|
276
|
+
n += 1
|
|
277
|
+
else:
|
|
278
|
+
break
|
|
279
|
+
return n
|
|
280
|
+
|
|
281
|
+
best = max(filtered, key=lambda c: (_common_prefix_len(c), -c.count(".")))
|
|
282
|
+
target = best
|
|
269
283
|
|
|
270
284
|
if source == edge.source and target == edge.target:
|
|
271
285
|
return edge
|
|
@@ -34,6 +34,7 @@ import warnings
|
|
|
34
34
|
from pathlib import Path
|
|
35
35
|
from typing import Any
|
|
36
36
|
|
|
37
|
+
from interlinked.analyzer.graph import _BUILTIN_METHOD_NAMES
|
|
37
38
|
from interlinked.models import NodeData, EdgeData, SymbolType, EdgeType
|
|
38
39
|
|
|
39
40
|
# Python builtins we should never create nodes/edges for
|
|
@@ -323,7 +324,17 @@ def parse_project(root: str | Path) -> tuple[list[NodeData], list[EdgeData]]:
|
|
|
323
324
|
metadata=e.metadata,
|
|
324
325
|
))
|
|
325
326
|
else:
|
|
326
|
-
# Keep the raw call target — external library calls visible
|
|
327
|
+
# Keep the raw call target — external library calls visible
|
|
328
|
+
# to auditors. BUT filter out localvar.builtin_method()
|
|
329
|
+
# patterns: if the inferencer couldn't resolve the target
|
|
330
|
+
# and the leaf is a common builtin method name, it's almost
|
|
331
|
+
# certainly a dict/list/str/logging method on a local var
|
|
332
|
+
# (e.g. effect.get(), args.items(), logger.warning()).
|
|
333
|
+
# Real project method calls resolve through the type system.
|
|
334
|
+
if "." in raw_target:
|
|
335
|
+
leaf = raw_target.rsplit(".", 1)[-1]
|
|
336
|
+
if leaf in _BUILTIN_METHOD_NAMES:
|
|
337
|
+
continue
|
|
327
338
|
resolved_edges.append(e)
|
|
328
339
|
continue
|
|
329
340
|
|
|
@@ -254,8 +254,8 @@ def get_rich_context(graph: CodeGraph, node: NodeData) -> dict:
|
|
|
254
254
|
# Connections
|
|
255
255
|
callers = graph.callers_of(node.id)
|
|
256
256
|
callees = graph.callees_of(node.id)
|
|
257
|
-
context["callers"] = [{"id": n.id, "name": n.name} for n in callers
|
|
258
|
-
context["callees"] = [{"id": n.id, "name": n.name} for n in callees
|
|
257
|
+
context["callers"] = [{"id": n.id, "name": n.name} for n in callers]
|
|
258
|
+
context["callees"] = [{"id": n.id, "name": n.name} for n in callees]
|
|
259
259
|
|
|
260
260
|
# Fingerprint — slim version (drop ast_tree, minhash, ast_node_counts)
|
|
261
261
|
fp = node.metadata.get("fingerprint")
|
|
@@ -563,10 +563,7 @@ def _dispatch_via_server(name: str, args: dict[str, Any], server_url: str) -> st
|
|
|
563
563
|
return json.dumps(result, indent=2)
|
|
564
564
|
return str(result)
|
|
565
565
|
if "results" in data:
|
|
566
|
-
|
|
567
|
-
if len(results) > 20:
|
|
568
|
-
return f"Found {len(results)} results. Showing first 20:\n" + json.dumps(results[:20], indent=2)
|
|
569
|
-
return json.dumps(results, indent=2)
|
|
566
|
+
return json.dumps(data["results"], indent=2)
|
|
570
567
|
if "error" in data:
|
|
571
568
|
return f"Error: {data['error']}"
|
|
572
569
|
return json.dumps(data, indent=2)
|
|
@@ -654,10 +651,7 @@ async def _async_dispatch_via_server(name: str, args: dict[str, Any], server_url
|
|
|
654
651
|
return json.dumps(result, indent=2)
|
|
655
652
|
return str(result)
|
|
656
653
|
if "results" in data:
|
|
657
|
-
|
|
658
|
-
if len(results) > 20:
|
|
659
|
-
return f"Found {len(results)} results. Showing first 20:\n" + json.dumps(results[:20], indent=2)
|
|
660
|
-
return json.dumps(results, indent=2)
|
|
654
|
+
return json.dumps(data["results"], indent=2)
|
|
661
655
|
if "error" in data:
|
|
662
656
|
return f"Error: {data['error']}"
|
|
663
657
|
return json.dumps(data, indent=2)
|
|
@@ -692,9 +686,6 @@ def _dispatch_tool(
|
|
|
692
686
|
|
|
693
687
|
elif name == "interlinked_query":
|
|
694
688
|
results = engine.query(args["expression"])
|
|
695
|
-
if len(results) > 20:
|
|
696
|
-
summary = f"Found {len(results)} results. Showing first 20:\n"
|
|
697
|
-
return summary + json.dumps(results[:20], indent=2)
|
|
698
689
|
return json.dumps(results, indent=2)
|
|
699
690
|
|
|
700
691
|
elif name == "interlinked_trace_variable":
|
{interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/SOURCES.txt
RENAMED
|
@@ -41,4 +41,5 @@ interlinked_mapper.egg-info/dependency_links.txt
|
|
|
41
41
|
interlinked_mapper.egg-info/entry_points.txt
|
|
42
42
|
interlinked_mapper.egg-info/requires.txt
|
|
43
43
|
interlinked_mapper.egg-info/top_level.txt
|
|
44
|
-
tests/test_accuracy.py
|
|
44
|
+
tests/test_accuracy.py
|
|
45
|
+
tests/test_query_completeness.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "interlinked-mapper"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.11"
|
|
8
8
|
description = "A Python program topology explorer — visualize the shape of your codebase"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -917,6 +917,91 @@ class TestParserSkipDirs:
|
|
|
917
917
|
# ══════════════════════════════════════════════════════════════════════
|
|
918
918
|
|
|
919
919
|
|
|
920
|
+
class TestFalsePositiveEdges:
|
|
921
|
+
"""Regression tests for false positive edge resolution.
|
|
922
|
+
|
|
923
|
+
These patterns caused incorrect edges in production:
|
|
924
|
+
- effect.get(), args.items() — dotted builtin methods not caught
|
|
925
|
+
- logger.warning() — logging method resolving to project symbols
|
|
926
|
+
- _resolve_entity_ref — same-name function in sibling module
|
|
927
|
+
"""
|
|
928
|
+
|
|
929
|
+
@pytest.fixture(autouse=True)
|
|
930
|
+
def setup(self):
|
|
931
|
+
self.graph, self.engine, _, _ = _build(FIXTURES)
|
|
932
|
+
|
|
933
|
+
def _edge_targets_from(self, source_partial: str, edge_type: EdgeType | None = None) -> set[str]:
|
|
934
|
+
"""All edge targets from a source matching partial name."""
|
|
935
|
+
src = _find(self.graph, source_partial)
|
|
936
|
+
return {
|
|
937
|
+
e.target for e in self.graph.all_edges()
|
|
938
|
+
if e.source == src
|
|
939
|
+
and (edge_type is None or e.edge_type == edge_type)
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
def test_dict_get_not_resolved(self):
|
|
943
|
+
"""effect.get('value') must NOT create a call to any project symbol."""
|
|
944
|
+
targets = self._edge_targets_from("dict_method_calls", EdgeType.CALLS)
|
|
945
|
+
# "effect.get" should not resolve to anything with ".get" in it
|
|
946
|
+
assert not any(t.endswith(".get") for t in targets), \
|
|
947
|
+
f"dict.get() false positive: {[t for t in targets if '.get' in t]}"
|
|
948
|
+
|
|
949
|
+
def test_dict_items_not_resolved(self):
|
|
950
|
+
"""args.items() must NOT create a call to any project symbol."""
|
|
951
|
+
targets = self._edge_targets_from("dict_method_calls", EdgeType.CALLS)
|
|
952
|
+
assert not any(t.endswith(".items") for t in targets), \
|
|
953
|
+
f"dict.items() false positive: {[t for t in targets if '.items' in t]}"
|
|
954
|
+
|
|
955
|
+
def test_list_extend_not_resolved(self):
|
|
956
|
+
"""ops.extend() must NOT resolve to a project symbol."""
|
|
957
|
+
targets = self._edge_targets_from("dict_method_calls", EdgeType.CALLS)
|
|
958
|
+
assert not any(t.endswith(".extend") for t in targets), \
|
|
959
|
+
f"list.extend() false positive: {[t for t in targets if '.extend' in t]}"
|
|
960
|
+
|
|
961
|
+
def test_list_append_not_resolved(self):
|
|
962
|
+
"""collected.append() must NOT resolve to a project symbol."""
|
|
963
|
+
targets = self._edge_targets_from("dict_method_calls", EdgeType.CALLS)
|
|
964
|
+
assert not any(t.endswith(".append") and "." in t.rsplit(".append", 1)[0] for t in targets), \
|
|
965
|
+
f"list.append() false positive: {[t for t in targets if '.append' in t]}"
|
|
966
|
+
|
|
967
|
+
def test_str_startswith_not_resolved(self):
|
|
968
|
+
"""ref.startswith() must NOT resolve to a project symbol."""
|
|
969
|
+
targets = self._edge_targets_from("dict_method_calls", EdgeType.CALLS)
|
|
970
|
+
assert not any(t.endswith(".startswith") for t in targets), \
|
|
971
|
+
f"str.startswith() false positive: {[t for t in targets if '.startswith' in t]}"
|
|
972
|
+
|
|
973
|
+
def test_str_split_not_resolved(self):
|
|
974
|
+
"""ref.split() must NOT resolve to a project symbol."""
|
|
975
|
+
targets = self._edge_targets_from("dict_method_calls", EdgeType.CALLS)
|
|
976
|
+
assert not any(t.endswith(".split") for t in targets), \
|
|
977
|
+
f"str.split() false positive: {[t for t in targets if '.split' in t]}"
|
|
978
|
+
|
|
979
|
+
def test_logger_warning_not_resolved(self):
|
|
980
|
+
"""logger.warning() must NOT create edges to project symbols."""
|
|
981
|
+
targets = self._edge_targets_from("logging_calls", EdgeType.CALLS)
|
|
982
|
+
# Should not resolve to any project node
|
|
983
|
+
node_ids = _node_ids(self.graph)
|
|
984
|
+
resolved_to_project = targets & node_ids
|
|
985
|
+
bad = {t for t in resolved_to_project if "warning" in t or "error" in t or "info" in t or "debug" in t}
|
|
986
|
+
assert not bad, \
|
|
987
|
+
f"logging calls resolved to project symbols: {bad}"
|
|
988
|
+
|
|
989
|
+
def test_same_name_helper_resolves_locally(self):
|
|
990
|
+
"""calls_shared_helper() should call THIS module's _shared_helper."""
|
|
991
|
+
caller = _find(self.graph, "calls_shared_helper")
|
|
992
|
+
targets = {
|
|
993
|
+
e.target for e in self.graph.all_edges()
|
|
994
|
+
if e.source == caller and e.edge_type == EdgeType.CALLS
|
|
995
|
+
}
|
|
996
|
+
# Should resolve to false_positive_edges._shared_helper, not another module's
|
|
997
|
+
local_match = [t for t in targets if "false_positive_edges._shared_helper" in t]
|
|
998
|
+
foreign_match = [t for t in targets if "_shared_helper" in t and "false_positive_edges" not in t]
|
|
999
|
+
assert local_match, \
|
|
1000
|
+
f"calls_shared_helper should call local _shared_helper, got: {targets}"
|
|
1001
|
+
assert not foreign_match, \
|
|
1002
|
+
f"calls_shared_helper resolved to foreign _shared_helper: {foreign_match}"
|
|
1003
|
+
|
|
1004
|
+
|
|
920
1005
|
class TestMCPFidelity:
|
|
921
1006
|
"""Verify MCP dispatch produces the same results as direct engine calls."""
|
|
922
1007
|
|
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
"""Tests that every query type returns FULL untruncated results.
|
|
2
|
+
|
|
3
|
+
No result set should ever be silently capped or truncated.
|
|
4
|
+
Every query path in QueryEngine.query() and every MCP dispatch path
|
|
5
|
+
must return all matching results.
|
|
6
|
+
|
|
7
|
+
Run: pytest tests/test_query_completeness.py -v
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
from interlinked.analyzer.parser import parse_project
|
|
18
|
+
from interlinked.analyzer.graph import CodeGraph
|
|
19
|
+
from interlinked.analyzer.dead_code import detect_dead_code
|
|
20
|
+
from interlinked.commander.query import QueryEngine, _slim_node_dict
|
|
21
|
+
from interlinked.models import SymbolType, EdgeType, NodeData
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
FIXTURES = Path(__file__).parent / "fixtures"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _build() -> tuple[CodeGraph, QueryEngine]:
|
|
28
|
+
nodes, edges = parse_project(str(FIXTURES))
|
|
29
|
+
graph = CodeGraph()
|
|
30
|
+
graph.build_from(nodes, edges)
|
|
31
|
+
detect_dead_code(graph)
|
|
32
|
+
engine = QueryEngine(graph)
|
|
33
|
+
return graph, engine
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ══════════════════════════════════════════════════════════════════════
|
|
37
|
+
# 1. QUERY COMPLETENESS — every query type returns all results
|
|
38
|
+
# ══════════════════════════════════════════════════════════════════════
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TestQueryCompleteness:
|
|
42
|
+
"""Every query type must return the full result set, never truncated."""
|
|
43
|
+
|
|
44
|
+
@pytest.fixture(autouse=True)
|
|
45
|
+
def setup(self):
|
|
46
|
+
self.graph, self.engine = _build()
|
|
47
|
+
|
|
48
|
+
def _count_by_type(self, sym_type: SymbolType) -> int:
|
|
49
|
+
return len(self.graph.nodes_by_type(sym_type))
|
|
50
|
+
|
|
51
|
+
def _count_dead(self, type_filter: set[SymbolType] | None = None) -> int:
|
|
52
|
+
return len([
|
|
53
|
+
n for n in self.graph.all_nodes()
|
|
54
|
+
if n.is_dead
|
|
55
|
+
and (type_filter is None or n.symbol_type in type_filter)
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
# ── "functions" / "methods" ──────────────────────────────────
|
|
59
|
+
|
|
60
|
+
def test_functions_query_returns_all(self):
|
|
61
|
+
results = self.engine.query("functions")
|
|
62
|
+
expected = (
|
|
63
|
+
self._count_by_type(SymbolType.FUNCTION)
|
|
64
|
+
+ self._count_by_type(SymbolType.METHOD)
|
|
65
|
+
)
|
|
66
|
+
assert len(results) == expected, \
|
|
67
|
+
f"'functions' query returned {len(results)}, expected {expected}"
|
|
68
|
+
|
|
69
|
+
def test_methods_query_returns_all(self):
|
|
70
|
+
results = self.engine.query("methods")
|
|
71
|
+
expected = (
|
|
72
|
+
self._count_by_type(SymbolType.FUNCTION)
|
|
73
|
+
+ self._count_by_type(SymbolType.METHOD)
|
|
74
|
+
)
|
|
75
|
+
assert len(results) == expected, \
|
|
76
|
+
f"'methods' query returned {len(results)}, expected {expected}"
|
|
77
|
+
|
|
78
|
+
# ── "classes" ────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
def test_classes_query_returns_all(self):
|
|
81
|
+
results = self.engine.query("classes")
|
|
82
|
+
expected = self._count_by_type(SymbolType.CLASS)
|
|
83
|
+
assert len(results) == expected, \
|
|
84
|
+
f"'classes' query returned {len(results)}, expected {expected}"
|
|
85
|
+
|
|
86
|
+
# ── "modules" ────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
def test_modules_query_returns_all(self):
|
|
89
|
+
results = self.engine.query("modules")
|
|
90
|
+
expected = self._count_by_type(SymbolType.MODULE)
|
|
91
|
+
assert len(results) == expected, \
|
|
92
|
+
f"'modules' query returned {len(results)}, expected {expected}"
|
|
93
|
+
|
|
94
|
+
# ── "variables" ──────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
def test_variables_query_returns_all(self):
|
|
97
|
+
results = self.engine.query("variables")
|
|
98
|
+
expected = len([
|
|
99
|
+
n for n in self.graph.all_nodes()
|
|
100
|
+
if n.symbol_type == SymbolType.VARIABLE
|
|
101
|
+
])
|
|
102
|
+
assert len(results) == expected, \
|
|
103
|
+
f"'variables' query returned {len(results)}, expected {expected}"
|
|
104
|
+
|
|
105
|
+
# ── "parameters" ─────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
def test_parameters_query_returns_all(self):
|
|
108
|
+
results = self.engine.query("parameters")
|
|
109
|
+
expected = len([
|
|
110
|
+
n for n in self.graph.all_nodes()
|
|
111
|
+
if n.symbol_type == SymbolType.PARAMETER
|
|
112
|
+
])
|
|
113
|
+
assert len(results) == expected, \
|
|
114
|
+
f"'parameters' query returned {len(results)}, expected {expected}"
|
|
115
|
+
|
|
116
|
+
# ── "dead functions" ─────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
def test_dead_functions_returns_all(self):
|
|
119
|
+
results = self.engine.query("dead functions")
|
|
120
|
+
expected = self._count_dead({SymbolType.FUNCTION, SymbolType.METHOD})
|
|
121
|
+
assert len(results) == expected, \
|
|
122
|
+
f"'dead functions' returned {len(results)}, expected {expected}"
|
|
123
|
+
|
|
124
|
+
def test_dead_returns_all(self):
|
|
125
|
+
results = self.engine.query("dead")
|
|
126
|
+
expected = self._count_dead()
|
|
127
|
+
assert len(results) == expected, \
|
|
128
|
+
f"'dead' returned {len(results)}, expected {expected}"
|
|
129
|
+
|
|
130
|
+
# ── "callers of X" ───────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
def test_callers_of_returns_all(self):
|
|
133
|
+
# Pick a node that has callers
|
|
134
|
+
for node in self.graph.all_nodes():
|
|
135
|
+
callers = self.graph.callers_of(node.id)
|
|
136
|
+
if len(callers) >= 2:
|
|
137
|
+
results = self.engine.query(f"callers of {node.qualified_name}")
|
|
138
|
+
assert len(results) == len(callers), \
|
|
139
|
+
f"'callers of {node.name}' returned {len(results)}, expected {len(callers)}"
|
|
140
|
+
return
|
|
141
|
+
pytest.skip("No node with >=2 callers found in fixtures")
|
|
142
|
+
|
|
143
|
+
# ── "callees of X" ───────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
def test_callees_of_returns_all(self):
|
|
146
|
+
for node in self.graph.all_nodes():
|
|
147
|
+
callees = self.graph.callees_of(node.id)
|
|
148
|
+
if len(callees) >= 2:
|
|
149
|
+
results = self.engine.query(f"callees of {node.qualified_name}")
|
|
150
|
+
assert len(results) == len(callees), \
|
|
151
|
+
f"'callees of {node.name}' returned {len(results)}, expected {len(callees)}"
|
|
152
|
+
return
|
|
153
|
+
pytest.skip("No node with >=2 callees found in fixtures")
|
|
154
|
+
|
|
155
|
+
# ── "parameters of X" ────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
def test_parameters_of_returns_all(self):
|
|
158
|
+
for node in self.graph.all_nodes():
|
|
159
|
+
if node.symbol_type not in (SymbolType.FUNCTION, SymbolType.METHOD):
|
|
160
|
+
continue
|
|
161
|
+
G = self.graph._g
|
|
162
|
+
if node.id not in G:
|
|
163
|
+
continue
|
|
164
|
+
param_ids = [
|
|
165
|
+
v for _, v, d in G.out_edges(node.id, data=True)
|
|
166
|
+
if d.get("edge_type") == "contains"
|
|
167
|
+
and self.graph.get_node(v)
|
|
168
|
+
and self.graph.get_node(v).symbol_type == SymbolType.PARAMETER
|
|
169
|
+
]
|
|
170
|
+
if len(param_ids) >= 2:
|
|
171
|
+
results = self.engine.query(f"parameters of {node.qualified_name}")
|
|
172
|
+
assert len(results) == len(param_ids), \
|
|
173
|
+
f"'parameters of {node.name}' returned {len(results)}, expected {len(param_ids)}"
|
|
174
|
+
return
|
|
175
|
+
pytest.skip("No function with >=2 params found in fixtures")
|
|
176
|
+
|
|
177
|
+
# ── "returns of X" ───────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
def test_returns_of_returns_all(self):
|
|
180
|
+
for node in self.graph.all_nodes():
|
|
181
|
+
edges = self.graph.edges_from(node.id, EdgeType.RETURNS)
|
|
182
|
+
ret_nodes = [self.graph.get_node(e.target) for e in edges if self.graph.get_node(e.target)]
|
|
183
|
+
if len(ret_nodes) >= 1:
|
|
184
|
+
results = self.engine.query(f"returns of {node.qualified_name}")
|
|
185
|
+
assert len(results) == len(ret_nodes), \
|
|
186
|
+
f"'returns of {node.name}' returned {len(results)}, expected {len(ret_nodes)}"
|
|
187
|
+
return
|
|
188
|
+
pytest.skip("No function with return edges found in fixtures")
|
|
189
|
+
|
|
190
|
+
# ── "imports of X" ───────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
def test_imports_of_returns_all(self):
|
|
193
|
+
for node in self.graph.all_nodes():
|
|
194
|
+
if node.symbol_type != SymbolType.MODULE:
|
|
195
|
+
continue
|
|
196
|
+
edges = self.graph.edges_from(node.id, EdgeType.IMPORTS)
|
|
197
|
+
if len(edges) >= 1:
|
|
198
|
+
results = self.engine.query(f"imports of {node.qualified_name}")
|
|
199
|
+
assert len(results) == len(edges), \
|
|
200
|
+
f"'imports of {node.name}' returned {len(results)}, expected {len(edges)}"
|
|
201
|
+
return
|
|
202
|
+
pytest.skip("No module with imports found in fixtures")
|
|
203
|
+
|
|
204
|
+
# ── "external calls" ─────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
def test_external_calls_returns_all(self):
|
|
207
|
+
node_ids = {n.id for n in self.graph.all_nodes()}
|
|
208
|
+
expected = [
|
|
209
|
+
e for e in self.graph.all_edges()
|
|
210
|
+
if e.edge_type == EdgeType.CALLS and e.target not in node_ids
|
|
211
|
+
]
|
|
212
|
+
results = self.engine.query("external calls")
|
|
213
|
+
assert len(results) == len(expected), \
|
|
214
|
+
f"'external calls' returned {len(results)}, expected {len(expected)}"
|
|
215
|
+
|
|
216
|
+
# ── scoped queries ───────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
def test_scoped_functions_returns_all(self):
|
|
219
|
+
# Find a module with multiple functions
|
|
220
|
+
modules = self.graph.nodes_by_type(SymbolType.MODULE)
|
|
221
|
+
for mod in modules:
|
|
222
|
+
scope = mod.qualified_name
|
|
223
|
+
expected = [
|
|
224
|
+
n for n in self.graph.all_nodes()
|
|
225
|
+
if n.symbol_type in (SymbolType.FUNCTION, SymbolType.METHOD)
|
|
226
|
+
and n.qualified_name.startswith(scope)
|
|
227
|
+
]
|
|
228
|
+
if len(expected) >= 3:
|
|
229
|
+
results = self.engine.query(f"functions in {scope}")
|
|
230
|
+
assert len(results) == len(expected), \
|
|
231
|
+
f"'functions in {scope}' returned {len(results)}, expected {len(expected)}"
|
|
232
|
+
return
|
|
233
|
+
pytest.skip("No module with >=3 functions found")
|
|
234
|
+
|
|
235
|
+
def test_scoped_classes_returns_all(self):
|
|
236
|
+
modules = self.graph.nodes_by_type(SymbolType.MODULE)
|
|
237
|
+
for mod in modules:
|
|
238
|
+
scope = mod.qualified_name
|
|
239
|
+
expected = [
|
|
240
|
+
n for n in self.graph.nodes_by_type(SymbolType.CLASS)
|
|
241
|
+
if n.qualified_name.startswith(scope)
|
|
242
|
+
]
|
|
243
|
+
if len(expected) >= 2:
|
|
244
|
+
results = self.engine.query(f"classes in {scope}")
|
|
245
|
+
assert len(results) == len(expected), \
|
|
246
|
+
f"'classes in {scope}' returned {len(results)}, expected {len(expected)}"
|
|
247
|
+
return
|
|
248
|
+
pytest.skip("No module with >=2 classes found")
|
|
249
|
+
|
|
250
|
+
# ── fuzzy name search ────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
def test_fuzzy_search_returns_all(self):
|
|
253
|
+
# Search for a common substring
|
|
254
|
+
term = "process"
|
|
255
|
+
expected = [
|
|
256
|
+
n for n in self.graph.all_nodes()
|
|
257
|
+
if term in n.qualified_name.lower() or term in n.name.lower()
|
|
258
|
+
]
|
|
259
|
+
results = self.engine.query(term)
|
|
260
|
+
assert len(results) == len(expected), \
|
|
261
|
+
f"fuzzy search '{term}' returned {len(results)}, expected {len(expected)}"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# ══════════════════════════════════════════════════════════════════════
|
|
265
|
+
# 2. MCP DISPATCH — no truncation in the MCP layer
|
|
266
|
+
# ══════════════════════════════════════════════════════════════════════
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class TestMCPNoTruncation:
|
|
270
|
+
"""MCP _dispatch_tool must never truncate query results."""
|
|
271
|
+
|
|
272
|
+
@pytest.fixture(autouse=True)
|
|
273
|
+
def setup(self):
|
|
274
|
+
self.graph, self.engine = _build()
|
|
275
|
+
|
|
276
|
+
def _mcp_query(self, expression: str) -> list:
|
|
277
|
+
from interlinked.mcp_server import _dispatch_tool
|
|
278
|
+
self.engine.reset_filter()
|
|
279
|
+
raw = _dispatch_tool(
|
|
280
|
+
"interlinked_query",
|
|
281
|
+
{"expression": expression},
|
|
282
|
+
self.engine, self.graph, ""
|
|
283
|
+
)
|
|
284
|
+
return json.loads(raw)
|
|
285
|
+
|
|
286
|
+
def test_mcp_functions_not_truncated(self):
|
|
287
|
+
self.engine.reset_filter()
|
|
288
|
+
engine_results = self.engine.query("functions")
|
|
289
|
+
mcp_results = self._mcp_query("functions")
|
|
290
|
+
assert len(mcp_results) == len(engine_results), \
|
|
291
|
+
f"MCP truncated: {len(mcp_results)} vs engine {len(engine_results)}"
|
|
292
|
+
|
|
293
|
+
def test_mcp_classes_not_truncated(self):
|
|
294
|
+
self.engine.reset_filter()
|
|
295
|
+
engine_results = self.engine.query("classes")
|
|
296
|
+
mcp_results = self._mcp_query("classes")
|
|
297
|
+
assert len(mcp_results) == len(engine_results)
|
|
298
|
+
|
|
299
|
+
def test_mcp_modules_not_truncated(self):
|
|
300
|
+
self.engine.reset_filter()
|
|
301
|
+
engine_results = self.engine.query("modules")
|
|
302
|
+
mcp_results = self._mcp_query("modules")
|
|
303
|
+
assert len(mcp_results) == len(engine_results)
|
|
304
|
+
|
|
305
|
+
def test_mcp_dead_not_truncated(self):
|
|
306
|
+
self.engine.reset_filter()
|
|
307
|
+
engine_results = self.engine.query("dead")
|
|
308
|
+
mcp_results = self._mcp_query("dead")
|
|
309
|
+
assert len(mcp_results) == len(engine_results)
|
|
310
|
+
|
|
311
|
+
def test_mcp_variables_not_truncated(self):
|
|
312
|
+
self.engine.reset_filter()
|
|
313
|
+
engine_results = self.engine.query("variables")
|
|
314
|
+
mcp_results = self._mcp_query("variables")
|
|
315
|
+
assert len(mcp_results) == len(engine_results)
|
|
316
|
+
|
|
317
|
+
def test_mcp_parameters_not_truncated(self):
|
|
318
|
+
self.engine.reset_filter()
|
|
319
|
+
engine_results = self.engine.query("parameters")
|
|
320
|
+
mcp_results = self._mcp_query("parameters")
|
|
321
|
+
assert len(mcp_results) == len(engine_results)
|
|
322
|
+
|
|
323
|
+
def test_mcp_external_calls_not_truncated(self):
|
|
324
|
+
self.engine.reset_filter()
|
|
325
|
+
engine_results = self.engine.query("external calls")
|
|
326
|
+
mcp_results = self._mcp_query("external calls")
|
|
327
|
+
assert len(mcp_results) == len(engine_results)
|
|
328
|
+
|
|
329
|
+
def test_mcp_callers_not_truncated(self):
|
|
330
|
+
# Find a node with callers
|
|
331
|
+
for node in self.graph.all_nodes():
|
|
332
|
+
callers = self.graph.callers_of(node.id)
|
|
333
|
+
if len(callers) >= 2:
|
|
334
|
+
self.engine.reset_filter()
|
|
335
|
+
engine_results = self.engine.query(f"callers of {node.qualified_name}")
|
|
336
|
+
mcp_results = self._mcp_query(f"callers of {node.qualified_name}")
|
|
337
|
+
assert len(mcp_results) == len(engine_results), \
|
|
338
|
+
f"MCP truncated callers: {len(mcp_results)} vs {len(engine_results)}"
|
|
339
|
+
return
|
|
340
|
+
pytest.skip("No node with >=2 callers")
|
|
341
|
+
|
|
342
|
+
def test_mcp_callees_not_truncated(self):
|
|
343
|
+
for node in self.graph.all_nodes():
|
|
344
|
+
callees = self.graph.callees_of(node.id)
|
|
345
|
+
if len(callees) >= 2:
|
|
346
|
+
self.engine.reset_filter()
|
|
347
|
+
engine_results = self.engine.query(f"callees of {node.qualified_name}")
|
|
348
|
+
mcp_results = self._mcp_query(f"callees of {node.qualified_name}")
|
|
349
|
+
assert len(mcp_results) == len(engine_results), \
|
|
350
|
+
f"MCP truncated callees: {len(mcp_results)} vs {len(engine_results)}"
|
|
351
|
+
return
|
|
352
|
+
pytest.skip("No node with >=2 callees")
|
|
353
|
+
|
|
354
|
+
def test_mcp_fuzzy_search_not_truncated(self):
|
|
355
|
+
self.engine.reset_filter()
|
|
356
|
+
engine_results = self.engine.query("process")
|
|
357
|
+
mcp_results = self._mcp_query("process")
|
|
358
|
+
assert len(mcp_results) == len(engine_results)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# ══════════════════════════════════════════════════════════════════════
|
|
362
|
+
# 3. SLIM NODE — fingerprint stripping doesn't lose essential fields
|
|
363
|
+
# ══════════════════════════════════════════════════════════════════════
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class TestSlimNode:
|
|
367
|
+
"""_slim_node_dict strips heavy fields but preserves everything else."""
|
|
368
|
+
|
|
369
|
+
def test_no_fingerprint_passthrough(self):
|
|
370
|
+
d = {"id": "a.b", "metadata": {}}
|
|
371
|
+
assert _slim_node_dict(d) == d
|
|
372
|
+
|
|
373
|
+
def test_strips_heavy_keys(self):
|
|
374
|
+
d = {
|
|
375
|
+
"id": "a.b",
|
|
376
|
+
"metadata": {
|
|
377
|
+
"fingerprint": {
|
|
378
|
+
"arg_count": 3,
|
|
379
|
+
"has_loops": True,
|
|
380
|
+
"ast_tree": ("FunctionDef", (("Return", ()),)),
|
|
381
|
+
"minhash": tuple(range(100)),
|
|
382
|
+
"ast_node_counts": {"FunctionDef": 1, "Return": 1},
|
|
383
|
+
"source_snippet": "def foo(): return 1",
|
|
384
|
+
"callees": ["bar"],
|
|
385
|
+
"callers": ["baz"],
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
}
|
|
389
|
+
result = _slim_node_dict(d)
|
|
390
|
+
fp = result["metadata"]["fingerprint"]
|
|
391
|
+
assert "ast_tree" not in fp
|
|
392
|
+
assert "minhash" not in fp
|
|
393
|
+
assert "ast_node_counts" not in fp
|
|
394
|
+
assert "source_snippet" not in fp
|
|
395
|
+
# Preserved fields
|
|
396
|
+
assert fp["arg_count"] == 3
|
|
397
|
+
assert fp["has_loops"] is True
|
|
398
|
+
assert fp["callees"] == ["bar"]
|
|
399
|
+
assert fp["callers"] == ["baz"]
|
|
400
|
+
|
|
401
|
+
def test_does_not_mutate_original(self):
|
|
402
|
+
d = {
|
|
403
|
+
"id": "a.b",
|
|
404
|
+
"metadata": {
|
|
405
|
+
"fingerprint": {
|
|
406
|
+
"arg_count": 3,
|
|
407
|
+
"minhash": (1, 2, 3),
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
}
|
|
411
|
+
_slim_node_dict(d)
|
|
412
|
+
assert "minhash" in d["metadata"]["fingerprint"], \
|
|
413
|
+
"_slim_node_dict must not mutate the original dict"
|
|
414
|
+
|
|
415
|
+
def test_snapshot_nodes_are_slim(self):
|
|
416
|
+
"""engine.snapshot() nodes must not contain heavy fingerprint fields."""
|
|
417
|
+
graph, engine = _build()
|
|
418
|
+
# Run similarity to populate fingerprints
|
|
419
|
+
try:
|
|
420
|
+
from interlinked.analyzer.similarity import analyze_similarity
|
|
421
|
+
analyze_similarity(graph)
|
|
422
|
+
except Exception:
|
|
423
|
+
pytest.skip("similarity module not available")
|
|
424
|
+
|
|
425
|
+
snap = engine.snapshot()
|
|
426
|
+
heavy_keys = {"ast_tree", "minhash", "ast_node_counts", "source_snippet"}
|
|
427
|
+
for node in snap["nodes"]:
|
|
428
|
+
fp = node.get("metadata", {}).get("fingerprint")
|
|
429
|
+
if fp:
|
|
430
|
+
found = heavy_keys & set(fp.keys())
|
|
431
|
+
assert not found, \
|
|
432
|
+
f"Node {node['id']} snapshot contains heavy keys: {found}"
|
|
433
|
+
|
|
434
|
+
def test_query_results_are_slim(self):
|
|
435
|
+
"""engine.query() results must not contain heavy fingerprint fields."""
|
|
436
|
+
graph, engine = _build()
|
|
437
|
+
try:
|
|
438
|
+
from interlinked.analyzer.similarity import analyze_similarity
|
|
439
|
+
analyze_similarity(graph)
|
|
440
|
+
except Exception:
|
|
441
|
+
pytest.skip("similarity module not available")
|
|
442
|
+
|
|
443
|
+
results = engine.query("functions")
|
|
444
|
+
heavy_keys = {"ast_tree", "minhash", "ast_node_counts", "source_snippet"}
|
|
445
|
+
for node in results:
|
|
446
|
+
fp = node.get("metadata", {}).get("fingerprint")
|
|
447
|
+
if fp:
|
|
448
|
+
found = heavy_keys & set(fp.keys())
|
|
449
|
+
assert not found, \
|
|
450
|
+
f"Query result {node['id']} contains heavy keys: {found}"
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# ══════════════════════════════════════════════════════════════════════
|
|
454
|
+
# 4. GET_CONTEXT — callers/callees not truncated
|
|
455
|
+
# ══════════════════════════════════════════════════════════════════════
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
class TestGetContextCompleteness:
|
|
459
|
+
"""get_context must return full callers/callees lists."""
|
|
460
|
+
|
|
461
|
+
@pytest.fixture(autouse=True)
|
|
462
|
+
def setup(self):
|
|
463
|
+
self.graph, self.engine = _build()
|
|
464
|
+
|
|
465
|
+
def test_callers_not_capped(self):
|
|
466
|
+
for node in self.graph.all_nodes():
|
|
467
|
+
callers = self.graph.callers_of(node.id)
|
|
468
|
+
if len(callers) >= 2:
|
|
469
|
+
ctx = self.engine.get_context(node.qualified_name)
|
|
470
|
+
ctx_data = json.loads(ctx) if isinstance(ctx, str) else ctx
|
|
471
|
+
assert len(ctx_data["callers"]) == len(callers), \
|
|
472
|
+
f"get_context callers capped: {len(ctx_data['callers'])} vs {len(callers)}"
|
|
473
|
+
return
|
|
474
|
+
pytest.skip("No node with >=2 callers")
|
|
475
|
+
|
|
476
|
+
def test_callees_not_capped(self):
|
|
477
|
+
for node in self.graph.all_nodes():
|
|
478
|
+
callees = self.graph.callees_of(node.id)
|
|
479
|
+
if len(callees) >= 2:
|
|
480
|
+
ctx = self.engine.get_context(node.qualified_name)
|
|
481
|
+
ctx_data = json.loads(ctx) if isinstance(ctx, str) else ctx
|
|
482
|
+
assert len(ctx_data["callees"]) == len(callees), \
|
|
483
|
+
f"get_context callees capped: {len(ctx_data['callees'])} vs {len(callees)}"
|
|
484
|
+
return
|
|
485
|
+
pytest.skip("No node with >=2 callees")
|
|
486
|
+
|
|
487
|
+
def test_fingerprint_slim_in_context(self):
|
|
488
|
+
"""get_context fingerprint must not include heavy fields."""
|
|
489
|
+
try:
|
|
490
|
+
from interlinked.analyzer.similarity import analyze_similarity
|
|
491
|
+
analyze_similarity(self.graph)
|
|
492
|
+
except Exception:
|
|
493
|
+
pytest.skip("similarity module not available")
|
|
494
|
+
|
|
495
|
+
for node in self.graph.all_nodes():
|
|
496
|
+
if node.metadata.get("fingerprint"):
|
|
497
|
+
ctx = self.engine.get_context(node.qualified_name)
|
|
498
|
+
ctx_data = json.loads(ctx) if isinstance(ctx, str) else ctx
|
|
499
|
+
fp = ctx_data.get("fingerprint")
|
|
500
|
+
if fp:
|
|
501
|
+
heavy = {"ast_tree", "minhash", "ast_node_counts", "source_snippet"}
|
|
502
|
+
found = heavy & set(fp.keys())
|
|
503
|
+
assert not found, \
|
|
504
|
+
f"get_context fingerprint has heavy keys: {found}"
|
|
505
|
+
return
|
|
506
|
+
pytest.skip("No node with fingerprint")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/index.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/package.json
RENAMED
|
File without changes
|
{interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/App.tsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/index.css
RENAMED
|
File without changes
|
{interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/main.tsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/theme.ts
RENAMED
|
File without changes
|
{interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/src/types.ts
RENAMED
|
File without changes
|
|
File without changes
|
{interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked/visualizer/frontend/tsconfig.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/requires.txt
RENAMED
|
File without changes
|
{interlinked_mapper-0.3.9 → interlinked_mapper-0.3.11}/interlinked_mapper.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|