codecompass-mcp 2.2.0__tar.gz → 2.4.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.2.0 → codecompass_mcp-2.4.0}/PKG-INFO +3 -3
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/README.md +2 -2
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/codecompass_mcp.egg-info/PKG-INFO +3 -3
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/codecompass_mcp.egg-info/SOURCES.txt +1 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/graph/code_graph_client.py +2 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/graph/code_query_cli.py +5 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/ingestion/chunker.py +0 -9
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/ingestion/code_normalizer.py +2 -1
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/ingestion/code_parser.py +409 -10
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/ingestion/entity_resolver.py +13 -15
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/ingestion/reader_agent.py +4 -3
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/main.py +20 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/models/code_types.py +2 -1
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/pyproject.toml +1 -1
- codecompass_mcp-2.4.0/tests/test_php_parser.py +217 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/LICENSE +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/codecompass_mcp.egg-info/dependency_links.txt +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/codecompass_mcp.egg-info/entry_points.txt +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/codecompass_mcp.egg-info/requires.txt +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/codecompass_mcp.egg-info/top_level.txt +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/config.py +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/graph/__init__.py +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/graph/cli.py +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/graph/setup.py +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/ingestion/__init__.py +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/ingestion/file_watcher.py +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/ingestion/graph_writer.py +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/ingestion/hierarchy_builder.py +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/ingestion/source_context.py +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/models/__init__.py +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/models/types.py +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/setup.cfg +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/tests/test_batch_impact.py +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/tests/test_blast_radius.py +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/tests/test_code_parser.py +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/tests/test_ingestion.py +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/tests/test_lit_css_parser.py +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/utils/__init__.py +0 -0
- {codecompass_mcp-2.2.0 → codecompass_mcp-2.4.0}/utils/formatting.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecompass-mcp
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.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
|
|
@@ -129,10 +129,10 @@ All commands default to the current directory.
|
|
|
129
129
|
### Install
|
|
130
130
|
|
|
131
131
|
```bash
|
|
132
|
-
|
|
133
|
-
pip install -e .
|
|
132
|
+
pip install codecompass-mcp
|
|
134
133
|
```
|
|
135
134
|
|
|
135
|
+
|
|
136
136
|
### Index a project
|
|
137
137
|
|
|
138
138
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecompass-mcp
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.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
|
|
@@ -129,10 +129,10 @@ All commands default to the current directory.
|
|
|
129
129
|
### Install
|
|
130
130
|
|
|
131
131
|
```bash
|
|
132
|
-
|
|
133
|
-
pip install -e .
|
|
132
|
+
pip install codecompass-mcp
|
|
134
133
|
```
|
|
135
134
|
|
|
135
|
+
|
|
136
136
|
### Index a project
|
|
137
137
|
|
|
138
138
|
```bash
|
|
@@ -17,11 +17,13 @@ from models.code_types import CodeTriple, FileNode, FolderNode
|
|
|
17
17
|
_EXT_TO_LANGUAGE = {
|
|
18
18
|
".py": "python",
|
|
19
19
|
".js": "javascript",
|
|
20
|
+
".jsx": "javascript",
|
|
20
21
|
".ts": "typescript",
|
|
21
22
|
".tsx": "typescript",
|
|
22
23
|
".html": "html",
|
|
23
24
|
".css": "css",
|
|
24
25
|
".scss": "scss",
|
|
26
|
+
".php": "php",
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
_ALLOWED_REL_TYPES = frozenset({
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import re
|
|
2
|
-
import PyPDF2
|
|
3
2
|
|
|
4
3
|
# Rough approximation: 1 token ≈ 4 characters (standard for English prose/code).
|
|
5
4
|
# Used when a tokeniser library is unavailable.
|
|
@@ -10,14 +9,6 @@ def _estimate_tokens(text: str) -> int:
|
|
|
10
9
|
return max(1, len(text) // _CHARS_PER_TOKEN)
|
|
11
10
|
|
|
12
11
|
|
|
13
|
-
def chunk_pdf(filepath: str, tokens_per_chunk: int = 500, overlap_tokens: int = 50) -> list[str]:
|
|
14
|
-
"""Split PDF into overlapping token-sized chunks."""
|
|
15
|
-
with open(filepath, "rb") as f:
|
|
16
|
-
reader = PyPDF2.PdfReader(f)
|
|
17
|
-
full_text = " ".join(page.extract_text() or "" for page in reader.pages)
|
|
18
|
-
return chunk_text(full_text, tokens_per_chunk=tokens_per_chunk, overlap_tokens=overlap_tokens)
|
|
19
|
-
|
|
20
|
-
|
|
21
12
|
def chunk_text(text: str, tokens_per_chunk: int = 500, overlap_tokens: int = 50) -> list[str]:
|
|
22
13
|
"""
|
|
23
14
|
Split text into chunks of approximately `tokens_per_chunk` tokens with
|
|
@@ -44,7 +44,8 @@ Your job:
|
|
|
44
44
|
POSTS_TO, INCLUDES, USED_BY, OVERRIDES, RAISES, RETURNS_TYPE.
|
|
45
45
|
3. Correct entity types if obviously wrong.
|
|
46
46
|
Allowed types: function, class, module, css_selector, html_element,
|
|
47
|
-
scss_mixin, scss_variable, endpoint, css_class, file
|
|
47
|
+
scss_mixin, scss_variable, endpoint, css_class, file, interface, trait,
|
|
48
|
+
enum, property, constant.
|
|
48
49
|
|
|
49
50
|
Return the same JSON array with corrections applied.
|
|
50
51
|
Do NOT add, remove, or reorder triples.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Parses source files locally (no API calls) into typed CodeTriples.
|
|
4
4
|
Uses direct AST node walking — compatible with any tree-sitter version.
|
|
5
5
|
|
|
6
|
-
Supports: .py, .js, .ts, .tsx, .html, .css, .scss
|
|
6
|
+
Supports: .py, .js, .jsx, .ts, .tsx, .html, .css, .scss, .php
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
@@ -21,7 +21,7 @@ from models.code_types import CodeTriple
|
|
|
21
21
|
# Constants
|
|
22
22
|
# ---------------------------------------------------------------------------
|
|
23
23
|
|
|
24
|
-
SUPPORTED_EXTENSIONS = {".py", ".js", ".ts", ".tsx", ".html", ".css", ".scss"}
|
|
24
|
+
SUPPORTED_EXTENSIONS = {".py", ".js", ".jsx", ".ts", ".tsx", ".html", ".css", ".scss", ".php"}
|
|
25
25
|
|
|
26
26
|
# Relation types
|
|
27
27
|
DEFINED_IN = "DEFINED_IN"
|
|
@@ -47,6 +47,23 @@ TYPE_SCSS_VARIABLE = "scss_variable"
|
|
|
47
47
|
TYPE_ENDPOINT = "endpoint"
|
|
48
48
|
TYPE_CSS_CLASS = "css_class"
|
|
49
49
|
TYPE_FILE = "file"
|
|
50
|
+
TYPE_INTERFACE = "interface"
|
|
51
|
+
TYPE_TRAIT = "trait"
|
|
52
|
+
TYPE_ENUM = "enum"
|
|
53
|
+
TYPE_PROPERTY = "property"
|
|
54
|
+
TYPE_CONSTANT = "constant"
|
|
55
|
+
|
|
56
|
+
# PHP pseudo-class references that never resolve to a real, distinct entity.
|
|
57
|
+
_PHP_SELF_REFERENTIAL = {"self", "static", "parent"}
|
|
58
|
+
|
|
59
|
+
# Container node types that own PHP class-body members (properties, consts,
|
|
60
|
+
# trait uses), mapped to the entity type of the container itself.
|
|
61
|
+
_PHP_CONTAINER_TYPES = {
|
|
62
|
+
"class_declaration": TYPE_CLASS,
|
|
63
|
+
"trait_declaration": TYPE_TRAIT,
|
|
64
|
+
"interface_declaration": TYPE_INTERFACE,
|
|
65
|
+
"enum_declaration": TYPE_ENUM,
|
|
66
|
+
}
|
|
50
67
|
|
|
51
68
|
# Regex patterns for CSS/SCSS source scanning
|
|
52
69
|
_CSS_VAR_RE = re.compile(r'var\(\s*(--[\w-]+)')
|
|
@@ -101,6 +118,11 @@ def _load_html_parser() -> tuple[Parser, Language]:
|
|
|
101
118
|
return _make_parser(tshtml.language)
|
|
102
119
|
|
|
103
120
|
|
|
121
|
+
def _load_php_parser() -> tuple[Parser, Language]:
|
|
122
|
+
import tree_sitter_php as tsphp
|
|
123
|
+
return _make_parser(tsphp.language_php)
|
|
124
|
+
|
|
125
|
+
|
|
104
126
|
def _load_css_parser() -> tuple[Parser, Language]:
|
|
105
127
|
import tree_sitter_css as tscss
|
|
106
128
|
return _make_parser(tscss.language)
|
|
@@ -109,11 +131,13 @@ def _load_css_parser() -> tuple[Parser, Language]:
|
|
|
109
131
|
_PARSER_LOADERS: dict[str, Callable[[], tuple[Parser, Language]]] = {
|
|
110
132
|
".py": _load_python_parser,
|
|
111
133
|
".js": _load_javascript_parser,
|
|
134
|
+
".jsx": _load_javascript_parser,
|
|
112
135
|
".ts": _load_typescript_parser,
|
|
113
136
|
".tsx": _load_tsx_parser,
|
|
114
137
|
".html": _load_html_parser,
|
|
115
138
|
".css": _load_css_parser,
|
|
116
139
|
".scss": _load_css_parser,
|
|
140
|
+
".php": _load_php_parser,
|
|
117
141
|
}
|
|
118
142
|
|
|
119
143
|
|
|
@@ -147,6 +171,15 @@ def _child_of_type(node: Node, type_name: str) -> Node | None:
|
|
|
147
171
|
return None
|
|
148
172
|
|
|
149
173
|
|
|
174
|
+
def _get_node_name(node: Node, types: list[str]) -> str | None:
|
|
175
|
+
"""Try multiple type names to find an identifier/name node."""
|
|
176
|
+
for t in types:
|
|
177
|
+
found = _child_of_type(node, t)
|
|
178
|
+
if found:
|
|
179
|
+
return _text(found)
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
|
|
150
183
|
def _children_of_type(node: Node, type_name: str) -> list[Node]:
|
|
151
184
|
return [c for c in node.children if c.type == type_name]
|
|
152
185
|
|
|
@@ -174,23 +207,23 @@ def _extract_python(root: Node, source: bytes, file_path: str) -> list[CodeTripl
|
|
|
174
207
|
for node in _walk(root):
|
|
175
208
|
match node.type:
|
|
176
209
|
case "function_definition":
|
|
177
|
-
|
|
178
|
-
if
|
|
210
|
+
name = _get_node_name(node, ["identifier"])
|
|
211
|
+
if name:
|
|
179
212
|
triples.append(CodeTriple(
|
|
180
|
-
from_entity=
|
|
213
|
+
from_entity=name,
|
|
181
214
|
from_type=TYPE_FUNCTION,
|
|
182
215
|
relation_type=DEFINED_IN,
|
|
183
216
|
to_entity=module_name,
|
|
184
217
|
to_type=TYPE_MODULE,
|
|
185
218
|
source_file=file_path,
|
|
186
|
-
line_number=_line(
|
|
219
|
+
line_number=_line(node),
|
|
187
220
|
))
|
|
188
221
|
|
|
189
222
|
case "class_definition":
|
|
190
|
-
|
|
191
|
-
if not
|
|
223
|
+
name = _get_node_name(node, ["identifier"])
|
|
224
|
+
if not name:
|
|
192
225
|
continue
|
|
193
|
-
class_name =
|
|
226
|
+
class_name = name
|
|
194
227
|
|
|
195
228
|
# Extract base classes from argument_list
|
|
196
229
|
arg_list = _child_of_type(node, "argument_list")
|
|
@@ -203,7 +236,7 @@ def _extract_python(root: Node, source: bytes, file_path: str) -> list[CodeTripl
|
|
|
203
236
|
to_entity=_text(base),
|
|
204
237
|
to_type=TYPE_CLASS,
|
|
205
238
|
source_file=file_path,
|
|
206
|
-
line_number=_line(
|
|
239
|
+
line_number=_line(base),
|
|
207
240
|
))
|
|
208
241
|
|
|
209
242
|
case "import_statement":
|
|
@@ -281,6 +314,370 @@ def _extract_python_callee(call_node: Node) -> str | None:
|
|
|
281
314
|
return None
|
|
282
315
|
|
|
283
316
|
|
|
317
|
+
# ---------------------------------------------------------------------------
|
|
318
|
+
# PHP extraction
|
|
319
|
+
# ---------------------------------------------------------------------------
|
|
320
|
+
#
|
|
321
|
+
# Node-type names below were verified against tree_sitter_php 0.24.1 by
|
|
322
|
+
# parsing representative snippets and inspecting the resulting AST directly
|
|
323
|
+
# (the grammar ships no node-types.json in the pip package). Names differ
|
|
324
|
+
# substantially from other PHP tree-sitter grammar versions found online —
|
|
325
|
+
# e.g. classes are `class_declaration` (not `class_definition`), function
|
|
326
|
+
# calls are `function_call_expression` (not `function_call`), and name
|
|
327
|
+
# fields are typed `name` (not `identifier`).
|
|
328
|
+
|
|
329
|
+
def _php_name(node: Node) -> str | None:
|
|
330
|
+
"""Direct `name` child — used for the declared name of most PHP nodes."""
|
|
331
|
+
found = _child_of_type(node, "name")
|
|
332
|
+
return _text(found) if found else None
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _php_basename(node: Node) -> str:
|
|
336
|
+
"""Strip a namespace prefix: `App\\Models\\Foo` -> `Foo`. Also handles a
|
|
337
|
+
bare `name` node (no backslash) and a leading-backslash FQN (`\\Foo`)."""
|
|
338
|
+
return _text(node).rsplit("\\", 1)[-1]
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _php_dotted(node: Node) -> str:
|
|
342
|
+
"""Namespace-qualified name -> dotted form, matching the module-name
|
|
343
|
+
convention used elsewhere (`App\\Models\\Foo` -> `App.Models.Foo`)."""
|
|
344
|
+
return _text(node).lstrip("\\").replace("\\", ".")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _php_var_name(node: Node) -> str:
|
|
348
|
+
"""`variable_name` node text (`$name`) -> bare name (`name`)."""
|
|
349
|
+
return _text(node).lstrip("$")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _php_extends_targets(clause: Node) -> list[Node]:
|
|
353
|
+
"""`name`/`qualified_name` children of a base_clause or interface clause.
|
|
354
|
+
|
|
355
|
+
Handles both single-target class `extends` and multi-target interface
|
|
356
|
+
`extends`/`implements` (comma-separated siblings, not a nested list).
|
|
357
|
+
"""
|
|
358
|
+
return [c for c in clause.children if c.type in ("name", "qualified_name")]
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _enclosing_php_container(node: Node) -> tuple[str, str] | None:
|
|
362
|
+
"""Walk up to the nearest enclosing class/trait/interface/enum.
|
|
363
|
+
|
|
364
|
+
Used to attach properties, constants, and trait-uses to their owner.
|
|
365
|
+
"""
|
|
366
|
+
current = node.parent
|
|
367
|
+
while current:
|
|
368
|
+
entity_type = _PHP_CONTAINER_TYPES.get(current.type)
|
|
369
|
+
if entity_type:
|
|
370
|
+
name = _php_name(current)
|
|
371
|
+
if name:
|
|
372
|
+
return name, entity_type
|
|
373
|
+
current = current.parent
|
|
374
|
+
return None
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _enclosing_php_scope(node: Node) -> tuple[str, str] | None:
|
|
378
|
+
"""Walk up from node to find the nearest enclosing function/method/closure.
|
|
379
|
+
|
|
380
|
+
Closures (anonymous_function, arrow_function) only count as a scope if
|
|
381
|
+
assigned to a variable (`$fn = function() {...}`), whose name is then
|
|
382
|
+
used as the caller entity; unnamed inline closures are transparent and
|
|
383
|
+
resolution continues further up the tree.
|
|
384
|
+
"""
|
|
385
|
+
current = node.parent
|
|
386
|
+
while current:
|
|
387
|
+
if current.type in ("function_definition", "method_declaration"):
|
|
388
|
+
name = _php_name(current)
|
|
389
|
+
if name:
|
|
390
|
+
return name, TYPE_FUNCTION
|
|
391
|
+
elif current.type in ("anonymous_function", "arrow_function"):
|
|
392
|
+
parent = current.parent
|
|
393
|
+
if parent and parent.type == "assignment_expression" and parent.children:
|
|
394
|
+
lhs = parent.children[0]
|
|
395
|
+
if lhs.type == "variable_name":
|
|
396
|
+
return _php_var_name(lhs), TYPE_FUNCTION
|
|
397
|
+
current = current.parent
|
|
398
|
+
return None
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _extract_php_namespace_use(node: Node) -> list[tuple[str, Node]]:
|
|
402
|
+
"""Resolve every imported target of a `use ...;` statement to a dotted
|
|
403
|
+
name plus the node to attribute the line number to.
|
|
404
|
+
|
|
405
|
+
Handles plain (`use App\\Foo;`), aliased (`use App\\Foo as F;`), grouped
|
|
406
|
+
(`use App\\{Bar, Baz as Z};`), and `use function`/`use const` forms.
|
|
407
|
+
"""
|
|
408
|
+
results: list[tuple[str, Node]] = []
|
|
409
|
+
group = _child_of_type(node, "namespace_use_group")
|
|
410
|
+
if group is not None:
|
|
411
|
+
prefix_node = _child_of_type(node, "namespace_name")
|
|
412
|
+
prefix = _text(prefix_node) if prefix_node else ""
|
|
413
|
+
for clause in _children_of_type(group, "namespace_use_clause"):
|
|
414
|
+
target = _child_of_type(clause, "qualified_name") or _child_of_type(clause, "name")
|
|
415
|
+
if target:
|
|
416
|
+
full = f"{prefix}\\{_text(target)}" if prefix else _text(target)
|
|
417
|
+
results.append((full.lstrip("\\").replace("\\", "."), target))
|
|
418
|
+
else:
|
|
419
|
+
for clause in _children_of_type(node, "namespace_use_clause"):
|
|
420
|
+
target = _child_of_type(clause, "qualified_name") or _child_of_type(clause, "name")
|
|
421
|
+
if target:
|
|
422
|
+
results.append((_php_dotted(target), target))
|
|
423
|
+
return results
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _php_include_target(node: Node) -> Node | None:
|
|
427
|
+
"""The literal string argument of a require/include expression, if any.
|
|
428
|
+
|
|
429
|
+
Dynamic paths (`require __DIR__ . '/x.php';`) have no plain `string`
|
|
430
|
+
child and are skipped — no static target to record.
|
|
431
|
+
"""
|
|
432
|
+
return _child_of_type(node, "string")
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _php_string_text(string_node: Node) -> str:
|
|
436
|
+
frag = _child_of_type(string_node, "string_content")
|
|
437
|
+
return _text(frag) if frag else _text(string_node).strip("'\"")
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _extract_php(root: Node, source: bytes, file_path: str) -> list[CodeTriple]:
|
|
441
|
+
module_name = _module_name_from_path(file_path)
|
|
442
|
+
triples: list[CodeTriple] = []
|
|
443
|
+
|
|
444
|
+
def defined_in(name: str, entity_type: str, line: int) -> None:
|
|
445
|
+
triples.append(CodeTriple(
|
|
446
|
+
from_entity=name,
|
|
447
|
+
from_type=entity_type,
|
|
448
|
+
relation_type=DEFINED_IN,
|
|
449
|
+
to_entity=module_name,
|
|
450
|
+
to_type=TYPE_MODULE,
|
|
451
|
+
source_file=file_path,
|
|
452
|
+
line_number=line,
|
|
453
|
+
))
|
|
454
|
+
|
|
455
|
+
def inherits(from_name: str, from_type: str, to_node: Node, to_type: str) -> None:
|
|
456
|
+
basename = _php_basename(to_node)
|
|
457
|
+
if basename and basename.lower() not in _PHP_SELF_REFERENTIAL:
|
|
458
|
+
triples.append(CodeTriple(
|
|
459
|
+
from_entity=from_name,
|
|
460
|
+
from_type=from_type,
|
|
461
|
+
relation_type=INHERITS,
|
|
462
|
+
to_entity=basename,
|
|
463
|
+
to_type=to_type,
|
|
464
|
+
source_file=file_path,
|
|
465
|
+
line_number=_line(to_node),
|
|
466
|
+
))
|
|
467
|
+
|
|
468
|
+
def calls(callee: str, callee_type: str, line: int, scope_node: Node) -> None:
|
|
469
|
+
if not callee or not _is_meaningful_callee(callee):
|
|
470
|
+
return
|
|
471
|
+
scope = _enclosing_php_scope(scope_node)
|
|
472
|
+
caller, caller_type = scope if scope else (module_name, TYPE_MODULE)
|
|
473
|
+
triples.append(CodeTriple(
|
|
474
|
+
from_entity=caller,
|
|
475
|
+
from_type=caller_type,
|
|
476
|
+
relation_type=CALLS,
|
|
477
|
+
to_entity=callee,
|
|
478
|
+
to_type=callee_type,
|
|
479
|
+
source_file=file_path,
|
|
480
|
+
line_number=line,
|
|
481
|
+
))
|
|
482
|
+
|
|
483
|
+
for node in _walk(root):
|
|
484
|
+
match node.type:
|
|
485
|
+
# -- Functions -----------------------------------------------
|
|
486
|
+
case "function_definition":
|
|
487
|
+
name = _php_name(node)
|
|
488
|
+
if name:
|
|
489
|
+
defined_in(name, TYPE_FUNCTION, _line(node))
|
|
490
|
+
|
|
491
|
+
case "method_declaration":
|
|
492
|
+
name = _php_name(node)
|
|
493
|
+
if name:
|
|
494
|
+
defined_in(name, TYPE_FUNCTION, _line(node))
|
|
495
|
+
|
|
496
|
+
case "anonymous_function" | "arrow_function":
|
|
497
|
+
parent = node.parent
|
|
498
|
+
if parent and parent.type == "assignment_expression" and parent.children:
|
|
499
|
+
lhs = parent.children[0]
|
|
500
|
+
if lhs.type == "variable_name":
|
|
501
|
+
defined_in(_php_var_name(lhs), TYPE_FUNCTION, _line(node))
|
|
502
|
+
|
|
503
|
+
# -- Classes / interfaces / traits / enums --------------------
|
|
504
|
+
case "class_declaration":
|
|
505
|
+
name = _php_name(node)
|
|
506
|
+
if not name:
|
|
507
|
+
continue
|
|
508
|
+
defined_in(name, TYPE_CLASS, _line(node))
|
|
509
|
+
base_clause = _child_of_type(node, "base_clause")
|
|
510
|
+
if base_clause:
|
|
511
|
+
for target in _php_extends_targets(base_clause):
|
|
512
|
+
inherits(name, TYPE_CLASS, target, TYPE_CLASS)
|
|
513
|
+
iface_clause = _child_of_type(node, "class_interface_clause")
|
|
514
|
+
if iface_clause:
|
|
515
|
+
for target in _php_extends_targets(iface_clause):
|
|
516
|
+
inherits(name, TYPE_CLASS, target, TYPE_INTERFACE)
|
|
517
|
+
|
|
518
|
+
case "interface_declaration":
|
|
519
|
+
name = _php_name(node)
|
|
520
|
+
if not name:
|
|
521
|
+
continue
|
|
522
|
+
defined_in(name, TYPE_INTERFACE, _line(node))
|
|
523
|
+
base_clause = _child_of_type(node, "base_clause")
|
|
524
|
+
if base_clause:
|
|
525
|
+
for target in _php_extends_targets(base_clause):
|
|
526
|
+
inherits(name, TYPE_INTERFACE, target, TYPE_INTERFACE)
|
|
527
|
+
|
|
528
|
+
case "trait_declaration":
|
|
529
|
+
name = _php_name(node)
|
|
530
|
+
if name:
|
|
531
|
+
defined_in(name, TYPE_TRAIT, _line(node))
|
|
532
|
+
|
|
533
|
+
case "enum_declaration":
|
|
534
|
+
name = _php_name(node)
|
|
535
|
+
if not name:
|
|
536
|
+
continue
|
|
537
|
+
defined_in(name, TYPE_ENUM, _line(node))
|
|
538
|
+
iface_clause = _child_of_type(node, "class_interface_clause")
|
|
539
|
+
if iface_clause:
|
|
540
|
+
for target in _php_extends_targets(iface_clause):
|
|
541
|
+
inherits(name, TYPE_ENUM, target, TYPE_INTERFACE)
|
|
542
|
+
|
|
543
|
+
case "enum_case":
|
|
544
|
+
container = _enclosing_php_container(node)
|
|
545
|
+
name = _php_name(node)
|
|
546
|
+
if name and container:
|
|
547
|
+
container_name, container_type = container
|
|
548
|
+
triples.append(CodeTriple(
|
|
549
|
+
from_entity=name,
|
|
550
|
+
from_type=TYPE_CONSTANT,
|
|
551
|
+
relation_type=DEFINED_IN,
|
|
552
|
+
to_entity=container_name,
|
|
553
|
+
to_type=container_type,
|
|
554
|
+
source_file=file_path,
|
|
555
|
+
line_number=_line(node),
|
|
556
|
+
))
|
|
557
|
+
|
|
558
|
+
# -- Trait usage inside a class/trait body --------------------
|
|
559
|
+
case "use_declaration":
|
|
560
|
+
container = _enclosing_php_container(node)
|
|
561
|
+
if container:
|
|
562
|
+
container_name, container_type = container
|
|
563
|
+
for target in _children_of_type(node, "name"):
|
|
564
|
+
triples.append(CodeTriple(
|
|
565
|
+
from_entity=container_name,
|
|
566
|
+
from_type=container_type,
|
|
567
|
+
relation_type=INHERITS,
|
|
568
|
+
to_entity=_text(target),
|
|
569
|
+
to_type=TYPE_TRAIT,
|
|
570
|
+
source_file=file_path,
|
|
571
|
+
line_number=_line(target),
|
|
572
|
+
))
|
|
573
|
+
|
|
574
|
+
# -- Namespace imports -----------------------------------------
|
|
575
|
+
case "namespace_use_declaration":
|
|
576
|
+
for dotted, target_node in _extract_php_namespace_use(node):
|
|
577
|
+
triples.append(CodeTriple(
|
|
578
|
+
from_entity=module_name,
|
|
579
|
+
from_type=TYPE_MODULE,
|
|
580
|
+
relation_type=IMPORTS,
|
|
581
|
+
to_entity=dotted,
|
|
582
|
+
to_type=TYPE_MODULE,
|
|
583
|
+
source_file=file_path,
|
|
584
|
+
line_number=_line(target_node),
|
|
585
|
+
))
|
|
586
|
+
|
|
587
|
+
# -- require/include -------------------------------------------
|
|
588
|
+
case "require_once_expression" | "require_expression" | "include_expression" | "include_once_expression":
|
|
589
|
+
str_node = _php_include_target(node)
|
|
590
|
+
if str_node:
|
|
591
|
+
triples.append(CodeTriple(
|
|
592
|
+
from_entity=module_name,
|
|
593
|
+
from_type=TYPE_MODULE,
|
|
594
|
+
relation_type=INCLUDES,
|
|
595
|
+
to_entity=_php_string_text(str_node),
|
|
596
|
+
to_type=TYPE_MODULE,
|
|
597
|
+
source_file=file_path,
|
|
598
|
+
line_number=_line(str_node),
|
|
599
|
+
))
|
|
600
|
+
|
|
601
|
+
# -- Properties / constructor-promoted properties ---------------
|
|
602
|
+
case "property_declaration":
|
|
603
|
+
container = _enclosing_php_container(node)
|
|
604
|
+
if container:
|
|
605
|
+
container_name, container_type = container
|
|
606
|
+
for element in _children_of_type(node, "property_element"):
|
|
607
|
+
var_node = _child_of_type(element, "variable_name")
|
|
608
|
+
if var_node:
|
|
609
|
+
triples.append(CodeTriple(
|
|
610
|
+
from_entity=_php_var_name(var_node),
|
|
611
|
+
from_type=TYPE_PROPERTY,
|
|
612
|
+
relation_type=DEFINED_IN,
|
|
613
|
+
to_entity=container_name,
|
|
614
|
+
to_type=container_type,
|
|
615
|
+
source_file=file_path,
|
|
616
|
+
line_number=_line(var_node),
|
|
617
|
+
))
|
|
618
|
+
|
|
619
|
+
case "property_promotion_parameter":
|
|
620
|
+
container = _enclosing_php_container(node)
|
|
621
|
+
var_node = _child_of_type(node, "variable_name")
|
|
622
|
+
if container and var_node:
|
|
623
|
+
container_name, container_type = container
|
|
624
|
+
triples.append(CodeTriple(
|
|
625
|
+
from_entity=_php_var_name(var_node),
|
|
626
|
+
from_type=TYPE_PROPERTY,
|
|
627
|
+
relation_type=DEFINED_IN,
|
|
628
|
+
to_entity=container_name,
|
|
629
|
+
to_type=container_type,
|
|
630
|
+
source_file=file_path,
|
|
631
|
+
line_number=_line(var_node),
|
|
632
|
+
))
|
|
633
|
+
|
|
634
|
+
# -- Constants (class/interface/trait/enum or top-level) -------
|
|
635
|
+
case "const_declaration":
|
|
636
|
+
container = _enclosing_php_container(node)
|
|
637
|
+
for element in _children_of_type(node, "const_element"):
|
|
638
|
+
name_node = _child_of_type(element, "name")
|
|
639
|
+
if not name_node:
|
|
640
|
+
continue
|
|
641
|
+
to_entity, to_type = container if container else (module_name, TYPE_MODULE)
|
|
642
|
+
triples.append(CodeTriple(
|
|
643
|
+
from_entity=_text(name_node),
|
|
644
|
+
from_type=TYPE_CONSTANT,
|
|
645
|
+
relation_type=DEFINED_IN,
|
|
646
|
+
to_entity=to_entity,
|
|
647
|
+
to_type=to_type,
|
|
648
|
+
source_file=file_path,
|
|
649
|
+
line_number=_line(name_node),
|
|
650
|
+
))
|
|
651
|
+
|
|
652
|
+
# -- Calls -------------------------------------------------------
|
|
653
|
+
case "function_call_expression":
|
|
654
|
+
callee_node = _child_of_type(node, "name") or _child_of_type(node, "qualified_name")
|
|
655
|
+
if callee_node:
|
|
656
|
+
calls(_php_basename(callee_node), TYPE_FUNCTION, _line(node), node)
|
|
657
|
+
|
|
658
|
+
case "member_call_expression" | "nullsafe_member_call_expression":
|
|
659
|
+
callee_node = _child_of_type(node, "name")
|
|
660
|
+
if callee_node:
|
|
661
|
+
calls(_text(callee_node), TYPE_FUNCTION, _line(node), node)
|
|
662
|
+
|
|
663
|
+
case "scoped_call_expression":
|
|
664
|
+
names = _children_of_type(node, "name")
|
|
665
|
+
if names:
|
|
666
|
+
calls(_text(names[-1]), TYPE_FUNCTION, _line(node), node)
|
|
667
|
+
|
|
668
|
+
case "object_creation_expression":
|
|
669
|
+
class_node = None
|
|
670
|
+
for child in node.children:
|
|
671
|
+
if child.type in ("name", "qualified_name"):
|
|
672
|
+
class_node = child
|
|
673
|
+
break
|
|
674
|
+
if class_node:
|
|
675
|
+
basename = _php_basename(class_node)
|
|
676
|
+
if basename.lower() not in _PHP_SELF_REFERENTIAL:
|
|
677
|
+
calls(basename, TYPE_CLASS, _line(node), node)
|
|
678
|
+
|
|
679
|
+
return triples
|
|
680
|
+
|
|
284
681
|
# ---------------------------------------------------------------------------
|
|
285
682
|
# JavaScript / TypeScript extraction
|
|
286
683
|
# ---------------------------------------------------------------------------
|
|
@@ -642,11 +1039,13 @@ def _extract_lit_css_tokens(source_text: str, file_path: str) -> list[CodeTriple
|
|
|
642
1039
|
_EXTRACTORS: dict[str, Callable] = {
|
|
643
1040
|
".py": _extract_python,
|
|
644
1041
|
".js": _extract_javascript,
|
|
1042
|
+
".jsx": _extract_javascript,
|
|
645
1043
|
".ts": _extract_javascript,
|
|
646
1044
|
".tsx": _extract_javascript,
|
|
647
1045
|
".html": _extract_html,
|
|
648
1046
|
".css": _extract_css,
|
|
649
1047
|
".scss": _extract_css,
|
|
1048
|
+
".php": _extract_php,
|
|
650
1049
|
}
|
|
651
1050
|
|
|
652
1051
|
|
|
@@ -23,7 +23,7 @@ Return ONLY valid JSON, no other text:
|
|
|
23
23
|
If no clear duplicates exist, return {"groups": []}"""
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def resolve_entities(client
|
|
26
|
+
def resolve_entities(client) -> int:
|
|
27
27
|
"""Identify and merge duplicate entity nodes in the local graph."""
|
|
28
28
|
all_nodes = [
|
|
29
29
|
{"id": n, "name": attr.get("name", n), "type": attr.get("entity_type", "")}
|
|
@@ -79,20 +79,18 @@ def resolve_entities(client, dry_run: bool = False) -> int:
|
|
|
79
79
|
dup_node = name_to_node.get(dup_name)
|
|
80
80
|
if not dup_node or dup_node["id"] == canonical_node["id"]:
|
|
81
81
|
continue
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
client.graph.remove_node(dup_id)
|
|
95
|
-
merged_count += 1
|
|
82
|
+
print(f" '{dup_name}' → '{canonical_name}'")
|
|
83
|
+
# Remap all edges from duplicate to canonical
|
|
84
|
+
dup_id = dup_node["id"]
|
|
85
|
+
canon_id = canonical_node["id"]
|
|
86
|
+
for pred in list(client.graph.predecessors(dup_id)):
|
|
87
|
+
for key, data in list(client.graph.get_edge_data(pred, dup_id).items()):
|
|
88
|
+
client.graph.add_edge(pred, canon_id, **data)
|
|
89
|
+
for succ in list(client.graph.successors(dup_id)):
|
|
90
|
+
for key, data in list(client.graph.get_edge_data(dup_id, succ).items()):
|
|
91
|
+
client.graph.add_edge(canon_id, succ, **data)
|
|
92
|
+
client.graph.remove_node(dup_id)
|
|
93
|
+
merged_count += 1
|
|
96
94
|
|
|
97
95
|
if merged_count:
|
|
98
96
|
client.save()
|
|
@@ -58,9 +58,10 @@ def _extract_triples_sync(chunk: str) -> list[Triple]:
|
|
|
58
58
|
)
|
|
59
59
|
|
|
60
60
|
try:
|
|
61
|
-
raw = response.content[0].text
|
|
62
|
-
raw
|
|
63
|
-
|
|
61
|
+
raw = response.content[0].text.strip()
|
|
62
|
+
if raw.startswith("```"):
|
|
63
|
+
raw = re.sub(r"^```[a-z]*\n?", "", raw)
|
|
64
|
+
raw = re.sub(r"```$", "", raw)
|
|
64
65
|
data = json.loads(raw.strip())
|
|
65
66
|
except (json.JSONDecodeError, IndexError) as e:
|
|
66
67
|
print(f"[reader_agent] JSON parse failed: {e} | raw: {raw[:200]!r}")
|
|
@@ -23,6 +23,10 @@ console = Console()
|
|
|
23
23
|
|
|
24
24
|
_CODECOMPASS_START = "<!-- codecompass-code-graph-start -->"
|
|
25
25
|
_CODECOMPASS_END = "<!-- codecompass-code-graph-end -->"
|
|
26
|
+
_CODECOMPASS_READ_INSTRUCTION = (
|
|
27
|
+
"Read `.codecompass` before making any changes or before reading any file. "
|
|
28
|
+
"If you think codecompass will help in any way, use it."
|
|
29
|
+
)
|
|
26
30
|
|
|
27
31
|
|
|
28
32
|
def init_project(repo_path: str) -> None:
|
|
@@ -69,6 +73,20 @@ def init_project(repo_path: str) -> None:
|
|
|
69
73
|
with open(file_path, "w") as f:
|
|
70
74
|
f.write(content)
|
|
71
75
|
|
|
76
|
+
# ponytail: minimal way to create claude.md in repo root
|
|
77
|
+
claude_md_path = os.path.join(repo_path, "claude.md")
|
|
78
|
+
if not os.path.exists(claude_md_path):
|
|
79
|
+
with open(claude_md_path, "w") as f:
|
|
80
|
+
f.write(f"AGENTS.md\n\n{_CODECOMPASS_READ_INSTRUCTION}\n")
|
|
81
|
+
else:
|
|
82
|
+
with open(claude_md_path) as f:
|
|
83
|
+
claude_md_content = f.read()
|
|
84
|
+
if _CODECOMPASS_READ_INSTRUCTION not in claude_md_content:
|
|
85
|
+
with open(claude_md_path, "a") as f:
|
|
86
|
+
if claude_md_content and not claude_md_content.endswith("\n"):
|
|
87
|
+
f.write("\n")
|
|
88
|
+
f.write(f"\n{_CODECOMPASS_READ_INSTRUCTION}\n")
|
|
89
|
+
|
|
72
90
|
_ensure_gitignore(repo_path)
|
|
73
91
|
console.print(f"[bold green]Initialized CodeCompass in:[/] {compass_dir}")
|
|
74
92
|
_register_project_agents_md(repo_path)
|
|
@@ -173,6 +191,8 @@ def _register_project_agents_md(repo_path: str) -> None:
|
|
|
173
191
|
block = f"""{_CODECOMPASS_START}
|
|
174
192
|
## Code graph
|
|
175
193
|
|
|
194
|
+
**{_CODECOMPASS_READ_INSTRUCTION}**
|
|
195
|
+
|
|
176
196
|
This project has a CodeCompass code knowledge graph at `.codecompass/graph.json`.
|
|
177
197
|
Every node carries `kind` (e.g. `function:python`, `class:typescript`) and a
|
|
178
198
|
human-readable `description`. Use it as your primary navigation tool.
|
|
@@ -8,7 +8,8 @@ class CodeTriple:
|
|
|
8
8
|
"""A single typed relationship extracted from source code."""
|
|
9
9
|
|
|
10
10
|
from_entity: str
|
|
11
|
-
from_type: str # function | class | module | css_selector | html_element | scss_mixin | scss_variable
|
|
11
|
+
from_type: str # function | class | module | css_selector | html_element | scss_mixin | scss_variable |
|
|
12
|
+
# interface | trait | enum | property | constant
|
|
12
13
|
relation_type: str # CALLS | IMPORTS | INHERITS | DEFINED_IN | STYLES | HAS_CLASS | POSTS_TO | INCLUDES | USED_BY
|
|
13
14
|
to_entity: str
|
|
14
15
|
to_type: str
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "codecompass-mcp"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.4.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"}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Tests for PHP ingestion entity coverage."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
9
|
+
|
|
10
|
+
from ingestion.code_parser import parse_file
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _parse(content: str) -> list:
|
|
14
|
+
with tempfile.NamedTemporaryFile(suffix=".php", mode="w", delete=False) as f:
|
|
15
|
+
f.write(content)
|
|
16
|
+
path = f.name
|
|
17
|
+
try:
|
|
18
|
+
return parse_file(path, os.path.dirname(path))
|
|
19
|
+
finally:
|
|
20
|
+
os.unlink(path)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _rels(triples, relation_type=None, from_type=None, to_type=None):
|
|
24
|
+
out = triples
|
|
25
|
+
if relation_type is not None:
|
|
26
|
+
out = [t for t in out if t.relation_type == relation_type]
|
|
27
|
+
if from_type is not None:
|
|
28
|
+
out = [t for t in out if t.from_type == from_type]
|
|
29
|
+
if to_type is not None:
|
|
30
|
+
out = [t for t in out if t.to_type == to_type]
|
|
31
|
+
return out
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# The parser must actually run — regression guard for the broken loader
|
|
36
|
+
# (tsphp.language vs tsphp.language_php) that silently no-op'd every .php file.
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
def test_php_file_parses_without_error():
|
|
40
|
+
triples = _parse("<?php\nfunction foo() { return 1; }\n")
|
|
41
|
+
assert triples, "PHP parsing should produce triples, not silently fail"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Functions, classes, inheritance
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
def test_top_level_function_indexed():
|
|
49
|
+
triples = _parse("<?php\nfunction topLevel($x) { return $x; }\n")
|
|
50
|
+
defs = _rels(triples, relation_type="DEFINED_IN", from_type="function")
|
|
51
|
+
assert any(t.from_entity == "topLevel" for t in defs)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_class_extends_indexed():
|
|
55
|
+
triples = _parse("<?php\nclass Foo extends Bar {}\n")
|
|
56
|
+
inh = _rels(triples, relation_type="INHERITS", to_type="class")
|
|
57
|
+
assert any(t.from_entity == "Foo" and t.to_entity == "Bar" for t in inh)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_class_implements_indexed():
|
|
61
|
+
triples = _parse("<?php\nclass Foo implements A, B {}\n")
|
|
62
|
+
inh = _rels(triples, relation_type="INHERITS", to_type="interface")
|
|
63
|
+
targets = {t.to_entity for t in inh}
|
|
64
|
+
assert {"A", "B"} <= targets
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Methods, properties, constructor promotion, constants
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
def test_class_method_indexed():
|
|
72
|
+
src = "<?php\nclass Foo {\n public function bar() { return 1; }\n}\n"
|
|
73
|
+
triples = _parse(src)
|
|
74
|
+
defs = _rels(triples, relation_type="DEFINED_IN", from_type="function")
|
|
75
|
+
assert any(t.from_entity == "bar" for t in defs)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_property_declaration_indexed():
|
|
79
|
+
src = "<?php\nclass Foo {\n protected static ?string $name = \"x\";\n}\n"
|
|
80
|
+
triples = _parse(src)
|
|
81
|
+
props = _rels(triples, relation_type="DEFINED_IN", from_type="property")
|
|
82
|
+
assert any(t.from_entity == "name" and t.to_entity == "Foo" for t in props)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_constructor_promoted_property_indexed():
|
|
86
|
+
src = "<?php\nclass Foo {\n public function __construct(private readonly int $id) {}\n}\n"
|
|
87
|
+
triples = _parse(src)
|
|
88
|
+
props = _rels(triples, relation_type="DEFINED_IN", from_type="property")
|
|
89
|
+
assert any(t.from_entity == "id" and t.to_entity == "Foo" for t in props)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_class_constant_indexed():
|
|
93
|
+
src = "<?php\nclass Foo {\n public const MAX = 10;\n}\n"
|
|
94
|
+
triples = _parse(src)
|
|
95
|
+
consts = _rels(triples, relation_type="DEFINED_IN", from_type="constant")
|
|
96
|
+
assert any(t.from_entity == "MAX" and t.to_entity == "Foo" for t in consts)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_top_level_constant_indexed():
|
|
100
|
+
triples = _parse("<?php\nconst MAX = 10;\n")
|
|
101
|
+
consts = _rels(triples, relation_type="DEFINED_IN", from_type="constant")
|
|
102
|
+
assert any(t.from_entity == "MAX" and t.to_type == "module" for t in consts)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# Interfaces, traits, enums
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
def test_interface_declaration_indexed():
|
|
110
|
+
triples = _parse("<?php\ninterface Payable {\n public function pay(): void;\n}\n")
|
|
111
|
+
defs = _rels(triples, relation_type="DEFINED_IN", from_type="interface")
|
|
112
|
+
assert any(t.from_entity == "Payable" for t in defs)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_interface_extends_multiple_indexed():
|
|
116
|
+
triples = _parse("<?php\ninterface Multi extends X, Y {}\n")
|
|
117
|
+
inh = _rels(triples, relation_type="INHERITS", from_type="interface", to_type="interface")
|
|
118
|
+
targets = {t.to_entity for t in inh}
|
|
119
|
+
assert {"X", "Y"} <= targets
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_trait_declaration_and_use_indexed():
|
|
123
|
+
src = "<?php\ntrait Loggable {\n public function log() {}\n}\nclass Foo {\n use Loggable;\n}\n"
|
|
124
|
+
triples = _parse(src)
|
|
125
|
+
trait_defs = _rels(triples, relation_type="DEFINED_IN", from_type="trait")
|
|
126
|
+
assert any(t.from_entity == "Loggable" for t in trait_defs)
|
|
127
|
+
uses = _rels(triples, relation_type="INHERITS", to_type="trait")
|
|
128
|
+
assert any(t.from_entity == "Foo" and t.to_entity == "Loggable" for t in uses)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_enum_and_case_indexed():
|
|
132
|
+
src = "<?php\nenum Status: string {\n case Active = 'active';\n}\n"
|
|
133
|
+
triples = _parse(src)
|
|
134
|
+
enum_defs = _rels(triples, relation_type="DEFINED_IN", from_type="enum")
|
|
135
|
+
assert any(t.from_entity == "Status" for t in enum_defs)
|
|
136
|
+
cases = _rels(triples, relation_type="DEFINED_IN", from_type="constant", to_type="enum")
|
|
137
|
+
assert any(t.from_entity == "Active" and t.to_entity == "Status" for t in cases)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# Namespaces / imports / includes
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
def test_namespace_use_indexed():
|
|
145
|
+
triples = _parse("<?php\nuse App\\Contracts\\Payable;\n")
|
|
146
|
+
imports = _rels(triples, relation_type="IMPORTS")
|
|
147
|
+
assert any(t.to_entity == "App.Contracts.Payable" for t in imports)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_namespace_use_group_indexed():
|
|
151
|
+
triples = _parse("<?php\nuse App\\{Bar, Baz};\n")
|
|
152
|
+
imports = _rels(triples, relation_type="IMPORTS")
|
|
153
|
+
targets = {t.to_entity for t in imports}
|
|
154
|
+
assert {"App.Bar", "App.Baz"} <= targets
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_require_once_indexed():
|
|
158
|
+
triples = _parse("<?php\nrequire_once 'bootstrap.php';\n")
|
|
159
|
+
includes = _rels(triples, relation_type="INCLUDES")
|
|
160
|
+
assert any(t.to_entity == "bootstrap.php" for t in includes)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
# Calls: plain, method, static, new, closures
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
def test_plain_function_call_indexed():
|
|
168
|
+
triples = _parse("<?php\nfunction outer() { inner(); }\nfunction inner() {}\n")
|
|
169
|
+
calls = _rels(triples, relation_type="CALLS")
|
|
170
|
+
assert any(t.from_entity == "outer" and t.to_entity == "inner" for t in calls)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_method_call_indexed():
|
|
174
|
+
src = "<?php\nfunction run() {\n $obj = new Foo();\n $obj->save();\n}\n"
|
|
175
|
+
triples = _parse(src)
|
|
176
|
+
calls = _rels(triples, relation_type="CALLS")
|
|
177
|
+
assert any(t.to_entity == "save" and t.to_type == "function" for t in calls)
|
|
178
|
+
assert any(t.to_entity == "Foo" and t.to_type == "class" for t in calls)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_static_call_indexed():
|
|
182
|
+
src = "<?php\nfunction run() {\n BaseModel::make();\n}\n"
|
|
183
|
+
triples = _parse(src)
|
|
184
|
+
calls = _rels(triples, relation_type="CALLS")
|
|
185
|
+
assert any(t.to_entity == "make" for t in calls)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_self_new_not_recorded_as_class_entity():
|
|
189
|
+
src = "<?php\nclass Foo {\n public static function make() {\n return new static();\n }\n}\n"
|
|
190
|
+
triples = _parse(src)
|
|
191
|
+
calls = _rels(triples, relation_type="CALLS", to_type="class")
|
|
192
|
+
assert not any(t.to_entity.lower() in ("self", "static", "parent") for t in calls)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_closure_assigned_to_variable_indexed():
|
|
196
|
+
src = "<?php\nfunction run() {\n $fn = function($x) { return $x; };\n}\n"
|
|
197
|
+
triples = _parse(src)
|
|
198
|
+
defs = _rels(triples, relation_type="DEFINED_IN", from_type="function")
|
|
199
|
+
assert any(t.from_entity == "fn" for t in defs)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_arrow_function_assigned_to_variable_indexed():
|
|
203
|
+
src = "<?php\nfunction run() {\n $arrow = fn($x) => $x + 1;\n}\n"
|
|
204
|
+
triples = _parse(src)
|
|
205
|
+
defs = _rels(triples, relation_type="DEFINED_IN", from_type="function")
|
|
206
|
+
assert any(t.from_entity == "arrow" for t in defs)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
# No regression on other extractors
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
def test_php_does_not_emit_html_relations():
|
|
214
|
+
triples = _parse("<?php\nclass Foo {}\n")
|
|
215
|
+
rel_types = {t.relation_type for t in triples}
|
|
216
|
+
assert "HAS_CLASS" not in rel_types
|
|
217
|
+
assert "STYLES" not in rel_types
|
|
File without changes
|
{codecompass_mcp-2.2.0 → codecompass_mcp-2.4.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
|