codecompass-mcp 2.8.0__tar.gz → 3.0.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.
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/PKG-INFO +1 -1
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/codecompass_mcp.egg-info/PKG-INFO +1 -1
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/graph/code_graph_client.py +38 -18
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/ingestion/code_parser.py +137 -6
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/ingestion/hierarchy_builder.py +1 -1
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/models/code_types.py +6 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/pyproject.toml +1 -1
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/tests/test_accuracy_fixes.py +24 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/LICENSE +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/README.md +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/codecompass_mcp.egg-info/SOURCES.txt +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/codecompass_mcp.egg-info/dependency_links.txt +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/codecompass_mcp.egg-info/entry_points.txt +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/codecompass_mcp.egg-info/requires.txt +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/codecompass_mcp.egg-info/top_level.txt +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/config.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/graph/__init__.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/graph/cli.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/graph/code_query_cli.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/graph/setup.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/ingestion/__init__.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/ingestion/chunker.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/ingestion/code_normalizer.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/ingestion/description_enricher.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/ingestion/entity_resolver.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/ingestion/file_watcher.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/ingestion/graph_writer.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/ingestion/reader_agent.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/ingestion/source_context.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/main.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/mcp_server.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/models/__init__.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/models/types.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/setup.cfg +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/tests/test_batch_impact.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/tests/test_blast_radius.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/tests/test_code_parser.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/tests/test_description_enricher.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/tests/test_ingestion.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/tests/test_lit_css_parser.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/tests/test_mcp_server.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/tests/test_php_parser.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/utils/__init__.py +0 -0
- {codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/utils/formatting.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecompass-mcp
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Structural code context for AI coding agents — a local code knowledge graph for blast radius, impact, deps, dead code, and flow tracing
|
|
5
5
|
Author: CodeCompass contributors
|
|
6
6
|
License: MIT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecompass-mcp
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Structural code context for AI coding agents — a local code knowledge graph for blast radius, impact, deps, dead code, and flow tracing
|
|
5
5
|
Author: CodeCompass contributors
|
|
6
6
|
License: MIT
|
|
@@ -37,12 +37,15 @@ def _now() -> str:
|
|
|
37
37
|
return datetime.now(timezone.utc).isoformat()
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def _entity_id(project: str, name: str, file: str | None) -> str:
|
|
41
|
-
"""Node id for an entity — file-qualified so same-named entities
|
|
42
|
-
|
|
43
|
-
(function) don't collide.
|
|
40
|
+
def _entity_id(project: str, name: str, file: str | None, owner: str | None = None) -> str:
|
|
41
|
+
"""Node id for an entity — file- and class-qualified so same-named entities
|
|
42
|
+
stay distinct across files AND across classes in the same file. Case is
|
|
43
|
+
preserved so `Session` (class) and `session` (function) don't collide.
|
|
44
|
+
External/module targets with no file are name-only.
|
|
45
|
+
"""
|
|
44
46
|
if file:
|
|
45
|
-
|
|
47
|
+
local = f"{owner}.{name}" if owner else name
|
|
48
|
+
return f"{project}:{file}:{local}"
|
|
46
49
|
return f"{project}:{name}"
|
|
47
50
|
|
|
48
51
|
|
|
@@ -225,6 +228,8 @@ class LocalGraphClient:
|
|
|
225
228
|
# later non-exported definition clear a flag an earlier one set.
|
|
226
229
|
if getattr(triple, "is_exported", False):
|
|
227
230
|
self.graph.nodes[from_id]["is_exported"] = True
|
|
231
|
+
if getattr(triple, "owner_class", None):
|
|
232
|
+
self.graph.nodes[from_id]["owner"] = triple.owner_class
|
|
228
233
|
|
|
229
234
|
# Upsert to entity — only set language/kind/description if not already known
|
|
230
235
|
self.graph.add_node(to_id)
|
|
@@ -260,13 +265,18 @@ class LocalGraphClient:
|
|
|
260
265
|
name = triple.to_entity
|
|
261
266
|
if triple.relation_type == "DEFINED_IN":
|
|
262
267
|
return f"{project}:{name}" # module container — name-only
|
|
263
|
-
cands = defs_by_name.get(name)
|
|
268
|
+
cands = defs_by_name.get(name) # list of (node_id, file, owner)
|
|
264
269
|
if cands:
|
|
265
270
|
rt = getattr(triple, "call_receiver_type", None)
|
|
266
271
|
if rt:
|
|
272
|
+
# strongest: a definition owned by exactly the receiver's class
|
|
273
|
+
for nid, f, owner in cands:
|
|
274
|
+
if owner == rt:
|
|
275
|
+
return nid
|
|
276
|
+
# next: any definition in the receiver class's file
|
|
267
277
|
cf = class_file.get(rt)
|
|
268
278
|
if cf:
|
|
269
|
-
for nid, f in cands:
|
|
279
|
+
for nid, f, owner in cands:
|
|
270
280
|
if f == cf:
|
|
271
281
|
return nid
|
|
272
282
|
if len(cands) == 1:
|
|
@@ -281,19 +291,21 @@ class LocalGraphClient:
|
|
|
281
291
|
project: str
|
|
282
292
|
) -> int:
|
|
283
293
|
# Pass 1: index every definition so calls can resolve to a specific one.
|
|
284
|
-
defs_by_name: dict[str, list] = {}
|
|
294
|
+
defs_by_name: dict[str, list] = {} # name -> [(node_id, file, owner)]
|
|
285
295
|
class_file: dict[str, str] = {}
|
|
286
296
|
for t in triples:
|
|
287
297
|
if t.relation_type == "DEFINED_IN":
|
|
288
|
-
|
|
289
|
-
|
|
298
|
+
owner = getattr(t, "owner_class", None)
|
|
299
|
+
nid = _entity_id(project, t.from_entity, t.source_file, owner)
|
|
300
|
+
defs_by_name.setdefault(t.from_entity, []).append((nid, t.source_file, owner))
|
|
290
301
|
if t.from_type == "class":
|
|
291
302
|
class_file.setdefault(t.from_entity, t.source_file)
|
|
292
303
|
|
|
293
|
-
# Pass 2: write nodes/edges with resolved, file-qualified ids.
|
|
304
|
+
# Pass 2: write nodes/edges with resolved, file+class-qualified ids.
|
|
294
305
|
for triple in triples:
|
|
295
306
|
file_id = file_id_map.get(triple.source_file, "")
|
|
296
|
-
from_id = _entity_id(project, triple.from_entity, triple.source_file
|
|
307
|
+
from_id = _entity_id(project, triple.from_entity, triple.source_file,
|
|
308
|
+
getattr(triple, "owner_class", None))
|
|
297
309
|
to_id = self._resolve_to_id(triple, project, defs_by_name, class_file)
|
|
298
310
|
self.write_code_triple(triple, file_id, project, from_id=from_id, to_id=to_id)
|
|
299
311
|
|
|
@@ -361,6 +373,12 @@ class LocalGraphClient:
|
|
|
361
373
|
matches.sort(key=lambda n: self.graph.nodes[n].get("file") is None)
|
|
362
374
|
|
|
363
375
|
if qualifier and matches:
|
|
376
|
+
# strongest: nodes whose owner class IS the qualifier (Command.invoke)
|
|
377
|
+
owned = [n for n in matches
|
|
378
|
+
if (self.graph.nodes[n].get("owner") or "").lower() == qualifier.lower()]
|
|
379
|
+
if owned:
|
|
380
|
+
return owned
|
|
381
|
+
# next: nodes in the file where class <qualifier> is defined
|
|
364
382
|
cls_files = {
|
|
365
383
|
a.get("file") for n, a in self.graph.nodes(data=True)
|
|
366
384
|
if a.get("type") == "Entity" and a.get("entity_type") == "class"
|
|
@@ -450,12 +468,14 @@ class LocalGraphClient:
|
|
|
450
468
|
visited.add(pred)
|
|
451
469
|
queue.append((pred, depth + 1))
|
|
452
470
|
|
|
453
|
-
# Qualified query (Type.method):
|
|
454
|
-
#
|
|
455
|
-
# (e.g. `adapter = self.get_adapter(url); adapter.send()`). They MAY
|
|
456
|
-
# this method, so we
|
|
457
|
-
#
|
|
458
|
-
|
|
471
|
+
# Qualified query (Type.method): if we found NO precise caller, fall back
|
|
472
|
+
# to the name-only bucket — calls whose receiver couldn't be statically
|
|
473
|
+
# typed (e.g. `adapter = self.get_adapter(url); adapter.send()`). They MAY
|
|
474
|
+
# hit this method, so we surface them flagged resolved=False rather than
|
|
475
|
+
# return nothing. We skip this when there's already a precise answer,
|
|
476
|
+
# because the bucket for a common name (`invoke`) is mostly calls to OTHER
|
|
477
|
+
# same-named methods and would flood a query that already resolved.
|
|
478
|
+
if want_receiver is not None and not results:
|
|
459
479
|
method = entity_name.rsplit(".", 1)[1]
|
|
460
480
|
bucket = f"{project}:{method}"
|
|
461
481
|
if bucket in self.graph and bucket not in target_set:
|
|
@@ -221,6 +221,7 @@ def _extract_python(root: Node, source: bytes, file_path: str) -> list[CodeTripl
|
|
|
221
221
|
source_file=file_path,
|
|
222
222
|
line_number=_line(node),
|
|
223
223
|
is_exported=name in exported,
|
|
224
|
+
owner_class=_enclosing_python_class(node),
|
|
224
225
|
))
|
|
225
226
|
|
|
226
227
|
case "class_definition":
|
|
@@ -308,6 +309,7 @@ def _extract_python(root: Node, source: bytes, file_path: str) -> list[CodeTripl
|
|
|
308
309
|
line_number=_line(node),
|
|
309
310
|
call_receiver=receiver,
|
|
310
311
|
call_receiver_type=receiver_type,
|
|
312
|
+
owner_class=_enclosing_python_class(node) if scope else None,
|
|
311
313
|
))
|
|
312
314
|
|
|
313
315
|
return triples
|
|
@@ -630,11 +632,127 @@ def _php_string_text(string_node: Node) -> str:
|
|
|
630
632
|
return _text(frag) if frag else _text(string_node).strip("'\"")
|
|
631
633
|
|
|
632
634
|
|
|
635
|
+
def _php_type_basename(node: Node | None) -> str | None:
|
|
636
|
+
"""Class name from a PHP type node (`named_type`/`type`/`qualified_name`),
|
|
637
|
+
stripping any namespace. Skips scalar/pseudo types (int, string, void, …)."""
|
|
638
|
+
if node is None:
|
|
639
|
+
return None
|
|
640
|
+
for desc in _walk(node):
|
|
641
|
+
if desc.type in ("name", "qualified_name"):
|
|
642
|
+
base = _php_basename(desc)
|
|
643
|
+
if base and base[:1].isupper(): # PSR class-name convention
|
|
644
|
+
return base
|
|
645
|
+
return None
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _php_new_type(node: Node | None) -> str | None:
|
|
649
|
+
"""Class name of `new X(...)`, else None."""
|
|
650
|
+
if node is None or node.type != "object_creation_expression":
|
|
651
|
+
return None
|
|
652
|
+
for child in node.children:
|
|
653
|
+
if child.type in ("name", "qualified_name"):
|
|
654
|
+
base = _php_basename(child)
|
|
655
|
+
if base and base.lower() not in _PHP_SELF_REFERENTIAL:
|
|
656
|
+
return base
|
|
657
|
+
return None
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _php_is_public(method_node: Node) -> bool:
|
|
661
|
+
"""PHP method visibility — public (or no modifier, which defaults public) is
|
|
662
|
+
API; private/protected is internal. Reads the `visibility_modifier` child."""
|
|
663
|
+
for child in method_node.children:
|
|
664
|
+
if child.type == "visibility_modifier":
|
|
665
|
+
return _text(child).lower() not in ("private", "protected")
|
|
666
|
+
return True # PHP methods default to public
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _php_return_types(root: Node) -> dict[str, str]:
|
|
670
|
+
"""Map function/method name -> declared return type (`function f(): Foo`)."""
|
|
671
|
+
rets: dict[str, str] = {}
|
|
672
|
+
conflict: set[str] = set()
|
|
673
|
+
for node in _walk(root):
|
|
674
|
+
if node.type in ("function_definition", "method_declaration"):
|
|
675
|
+
name = _php_name(node)
|
|
676
|
+
rt = node.child_by_field_name("return_type")
|
|
677
|
+
t = _php_type_basename(rt) if rt is not None else None
|
|
678
|
+
if name and t:
|
|
679
|
+
if name in rets and rets[name] != t:
|
|
680
|
+
conflict.add(name)
|
|
681
|
+
else:
|
|
682
|
+
rets[name] = t
|
|
683
|
+
for name in conflict:
|
|
684
|
+
rets.pop(name, None)
|
|
685
|
+
return rets
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def _php_type_env(root: Node) -> dict[str, str]:
|
|
689
|
+
"""Best-effort PHP variable → class map (no `$` in keys).
|
|
690
|
+
|
|
691
|
+
Binds from `$x = new Foo()`, typed params (`function f(Foo $x)`), and
|
|
692
|
+
return-type inference (`$x = make()` where `make(): Foo`). Conflicts dropped.
|
|
693
|
+
"""
|
|
694
|
+
ret_types = _php_return_types(root)
|
|
695
|
+
types: dict[str, str] = {}
|
|
696
|
+
ambiguous: set[str] = set()
|
|
697
|
+
|
|
698
|
+
def bind(name: str | None, t: str | None) -> None:
|
|
699
|
+
if not name or not t:
|
|
700
|
+
return
|
|
701
|
+
if name in types and types[name] != t:
|
|
702
|
+
ambiguous.add(name)
|
|
703
|
+
else:
|
|
704
|
+
types[name] = t
|
|
705
|
+
|
|
706
|
+
for node in _walk(root):
|
|
707
|
+
if node.type == "assignment_expression" and node.children:
|
|
708
|
+
lhs = node.children[0]
|
|
709
|
+
rhs = node.children[-1]
|
|
710
|
+
if lhs.type == "variable_name":
|
|
711
|
+
name = _php_var_name(lhs)
|
|
712
|
+
bind(name, _php_new_type(rhs))
|
|
713
|
+
if rhs.type in ("function_call_expression", "member_call_expression",
|
|
714
|
+
"nullsafe_member_call_expression", "scoped_call_expression"):
|
|
715
|
+
callee = _child_of_type(rhs, "name")
|
|
716
|
+
if callee is not None:
|
|
717
|
+
bind(name, ret_types.get(_text(callee)))
|
|
718
|
+
elif node.type == "simple_parameter":
|
|
719
|
+
var = _child_of_type(node, "variable_name")
|
|
720
|
+
ty = node.child_by_field_name("type")
|
|
721
|
+
if var is not None and ty is not None:
|
|
722
|
+
bind(_php_var_name(var), _php_type_basename(ty))
|
|
723
|
+
|
|
724
|
+
for name in ambiguous:
|
|
725
|
+
types.pop(name, None)
|
|
726
|
+
return types
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _php_call_receiver(node: Node, env: dict[str, str]) -> tuple[str | None, str | None]:
|
|
730
|
+
"""Receiver text + inferred type for a member call `$obj->method()`."""
|
|
731
|
+
obj = node.child_by_field_name("object")
|
|
732
|
+
if obj is None:
|
|
733
|
+
obj = node.children[0] if node.children else None
|
|
734
|
+
if obj is None:
|
|
735
|
+
return None, None
|
|
736
|
+
# inline `(new Foo())->m()`
|
|
737
|
+
nt = _php_new_type(obj)
|
|
738
|
+
if nt:
|
|
739
|
+
return "new", nt
|
|
740
|
+
if obj.type == "variable_name":
|
|
741
|
+
var = _php_var_name(obj)
|
|
742
|
+
if var == "this":
|
|
743
|
+
container = _enclosing_php_container(node)
|
|
744
|
+
return "this", (container[0] if container else None)
|
|
745
|
+
return var, env.get(var)
|
|
746
|
+
return _text(obj), None
|
|
747
|
+
|
|
748
|
+
|
|
633
749
|
def _extract_php(root: Node, source: bytes, file_path: str) -> list[CodeTriple]:
|
|
634
750
|
module_name = _module_name_from_path(file_path)
|
|
635
751
|
triples: list[CodeTriple] = []
|
|
752
|
+
type_env = _php_type_env(root)
|
|
636
753
|
|
|
637
|
-
def defined_in(name: str, entity_type: str, line: int
|
|
754
|
+
def defined_in(name: str, entity_type: str, line: int, is_exported: bool = True,
|
|
755
|
+
owner: str | None = None) -> None:
|
|
638
756
|
triples.append(CodeTriple(
|
|
639
757
|
from_entity=name,
|
|
640
758
|
from_type=entity_type,
|
|
@@ -643,6 +761,8 @@ def _extract_php(root: Node, source: bytes, file_path: str) -> list[CodeTriple]:
|
|
|
643
761
|
to_type=TYPE_MODULE,
|
|
644
762
|
source_file=file_path,
|
|
645
763
|
line_number=line,
|
|
764
|
+
is_exported=is_exported,
|
|
765
|
+
owner_class=owner,
|
|
646
766
|
))
|
|
647
767
|
|
|
648
768
|
def inherits(from_name: str, from_type: str, to_node: Node, to_type: str) -> None:
|
|
@@ -658,11 +778,13 @@ def _extract_php(root: Node, source: bytes, file_path: str) -> list[CodeTriple]:
|
|
|
658
778
|
line_number=_line(to_node),
|
|
659
779
|
))
|
|
660
780
|
|
|
661
|
-
def calls(callee: str, callee_type: str, line: int, scope_node: Node
|
|
781
|
+
def calls(callee: str, callee_type: str, line: int, scope_node: Node,
|
|
782
|
+
receiver: str | None = None, receiver_type: str | None = None) -> None:
|
|
662
783
|
if not callee or not _is_meaningful_callee(callee):
|
|
663
784
|
return
|
|
664
785
|
scope = _enclosing_php_scope(scope_node)
|
|
665
786
|
caller, caller_type = scope if scope else (module_name, TYPE_MODULE)
|
|
787
|
+
container = _enclosing_php_container(scope_node)
|
|
666
788
|
triples.append(CodeTriple(
|
|
667
789
|
from_entity=caller,
|
|
668
790
|
from_type=caller_type,
|
|
@@ -671,6 +793,9 @@ def _extract_php(root: Node, source: bytes, file_path: str) -> list[CodeTriple]:
|
|
|
671
793
|
to_type=callee_type,
|
|
672
794
|
source_file=file_path,
|
|
673
795
|
line_number=line,
|
|
796
|
+
call_receiver=receiver,
|
|
797
|
+
call_receiver_type=receiver_type,
|
|
798
|
+
owner_class=container[0] if (scope and container) else None,
|
|
674
799
|
))
|
|
675
800
|
|
|
676
801
|
for node in _walk(root):
|
|
@@ -684,7 +809,9 @@ def _extract_php(root: Node, source: bytes, file_path: str) -> list[CodeTriple]:
|
|
|
684
809
|
case "method_declaration":
|
|
685
810
|
name = _php_name(node)
|
|
686
811
|
if name:
|
|
687
|
-
|
|
812
|
+
container = _enclosing_php_container(node)
|
|
813
|
+
defined_in(name, TYPE_FUNCTION, _line(node), is_exported=_php_is_public(node),
|
|
814
|
+
owner=container[0] if container else None)
|
|
688
815
|
|
|
689
816
|
case "anonymous_function" | "arrow_function":
|
|
690
817
|
parent = node.parent
|
|
@@ -851,7 +978,9 @@ def _extract_php(root: Node, source: bytes, file_path: str) -> list[CodeTriple]:
|
|
|
851
978
|
case "member_call_expression" | "nullsafe_member_call_expression":
|
|
852
979
|
callee_node = _child_of_type(node, "name")
|
|
853
980
|
if callee_node:
|
|
854
|
-
|
|
981
|
+
recv, recv_type = _php_call_receiver(node, type_env)
|
|
982
|
+
calls(_text(callee_node), TYPE_FUNCTION, _line(node), node,
|
|
983
|
+
receiver=recv, receiver_type=recv_type)
|
|
855
984
|
|
|
856
985
|
case "scoped_call_expression":
|
|
857
986
|
names = _children_of_type(node, "name")
|
|
@@ -886,7 +1015,7 @@ def _extract_javascript(root: Node, source: bytes, file_path: str) -> list[CodeT
|
|
|
886
1015
|
# Best-effort variable → type map for receiver type inference on calls.
|
|
887
1016
|
type_env = _js_type_env(root)
|
|
888
1017
|
|
|
889
|
-
def defined(name: str, line: int) -> None:
|
|
1018
|
+
def defined(name: str, line: int, owner: str | None = None) -> None:
|
|
890
1019
|
triples.append(CodeTriple(
|
|
891
1020
|
from_entity=name,
|
|
892
1021
|
from_type=TYPE_FUNCTION,
|
|
@@ -896,6 +1025,7 @@ def _extract_javascript(root: Node, source: bytes, file_path: str) -> list[CodeT
|
|
|
896
1025
|
source_file=file_path,
|
|
897
1026
|
line_number=line,
|
|
898
1027
|
is_exported=name in exported,
|
|
1028
|
+
owner_class=owner,
|
|
899
1029
|
))
|
|
900
1030
|
|
|
901
1031
|
for node in _walk(root):
|
|
@@ -908,7 +1038,7 @@ def _extract_javascript(root: Node, source: bytes, file_path: str) -> list[CodeT
|
|
|
908
1038
|
case "method_definition":
|
|
909
1039
|
name_node = _child_of_type(node, "property_identifier")
|
|
910
1040
|
if name_node:
|
|
911
|
-
defined(_text(name_node), _line(name_node))
|
|
1041
|
+
defined(_text(name_node), _line(name_node), owner=_enclosing_class_name(node))
|
|
912
1042
|
|
|
913
1043
|
case "variable_declarator":
|
|
914
1044
|
# Capture: const foo = () => ...
|
|
@@ -970,6 +1100,7 @@ def _extract_javascript(root: Node, source: bytes, file_path: str) -> list[CodeT
|
|
|
970
1100
|
line_number=_line(node),
|
|
971
1101
|
call_receiver=receiver,
|
|
972
1102
|
call_receiver_type=receiver_type,
|
|
1103
|
+
owner_class=_enclosing_class_name(node) if scope else None,
|
|
973
1104
|
))
|
|
974
1105
|
|
|
975
1106
|
return triples
|
|
@@ -20,7 +20,7 @@ _SKIP_DIRS = {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
# Supported source file extensions (mirrors code_parser.SUPPORTED_EXTENSIONS)
|
|
23
|
-
_SOURCE_EXTENSIONS = {".py", ".js", ".ts", ".tsx", ".html", ".css", ".scss"}
|
|
23
|
+
_SOURCE_EXTENSIONS = {".py", ".js", ".jsx", ".ts", ".tsx", ".html", ".css", ".scss", ".php"}
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def build_hierarchy(project_root: str, project_name: str, client) -> dict[str, str]:
|
|
@@ -33,6 +33,12 @@ class CodeTriple:
|
|
|
33
33
|
# Lets dead-code analysis avoid flagging intentionally-exported symbols.
|
|
34
34
|
is_exported: bool = False
|
|
35
35
|
|
|
36
|
+
# Class that `from_entity` belongs to — the method's class for a definition,
|
|
37
|
+
# or the caller's class for a call. None for module-level entities. Lets node
|
|
38
|
+
# ids be class-qualified so same-named methods of different classes in the
|
|
39
|
+
# SAME file stay distinct (Command.invoke vs Context.invoke in core.py).
|
|
40
|
+
owner_class: str | None = None
|
|
41
|
+
|
|
36
42
|
|
|
37
43
|
@dataclass
|
|
38
44
|
class FileNode:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "codecompass-mcp"
|
|
3
|
-
version = "
|
|
3
|
+
version = "3.0.0"
|
|
4
4
|
description = "Structural code context for AI coding agents — a local code knowledge graph for blast radius, impact, deps, dead code, and flow tracing"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = {text = "MIT"}
|
|
@@ -339,6 +339,30 @@ def test_python_public_method_marked_exported():
|
|
|
339
339
|
assert by_name.get("_private") is False, "underscore method is not exported"
|
|
340
340
|
|
|
341
341
|
|
|
342
|
+
def test_php_receiver_type_and_exports():
|
|
343
|
+
src = (
|
|
344
|
+
"<?php\n"
|
|
345
|
+
"class Session {\n"
|
|
346
|
+
" public function send($r) { return 1; }\n"
|
|
347
|
+
" private function _helper() { return 2; }\n"
|
|
348
|
+
" public function getAdapter($u): HTTPAdapter { return new HTTPAdapter(); }\n"
|
|
349
|
+
" public function request($m) {\n"
|
|
350
|
+
" $adapter = $this->getAdapter($m);\n"
|
|
351
|
+
" $this->send($m);\n"
|
|
352
|
+
" return $adapter->send($m);\n"
|
|
353
|
+
" }\n"
|
|
354
|
+
"}\n"
|
|
355
|
+
)
|
|
356
|
+
triples = _parse(src, suffix=".php")
|
|
357
|
+
sends = [t for t in triples if t.relation_type == "CALLS" and t.to_entity == "send"]
|
|
358
|
+
types = {t.call_receiver_type for t in sends}
|
|
359
|
+
assert "Session" in types, "$this->send() typed by enclosing class"
|
|
360
|
+
assert "HTTPAdapter" in types, "$adapter->send() typed via getAdapter's return type"
|
|
361
|
+
by_name = {t.from_entity: t.is_exported for t in triples if t.relation_type == "DEFINED_IN"}
|
|
362
|
+
assert by_name.get("_helper") is False, "private method is not exported"
|
|
363
|
+
assert by_name.get("request") is True, "public method is exported"
|
|
364
|
+
|
|
365
|
+
|
|
342
366
|
def test_graph_grep_regex():
|
|
343
367
|
triples = [
|
|
344
368
|
CodeTriple("HTTPAdapter", "class", "DEFINED_IN", "m", "module", "adapters.py", 1),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codecompass_mcp-2.8.0 → codecompass_mcp-3.0.0}/codecompass_mcp.egg-info/dependency_links.txt
RENAMED
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|