codecompass-mcp 2.3.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.
Files changed (39) hide show
  1. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/PKG-INFO +1 -1
  2. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/codecompass_mcp.egg-info/PKG-INFO +1 -1
  3. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/codecompass_mcp.egg-info/SOURCES.txt +1 -0
  4. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/graph/code_graph_client.py +2 -0
  5. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/graph/code_query_cli.py +5 -0
  6. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/ingestion/code_normalizer.py +2 -1
  7. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/ingestion/code_parser.py +356 -39
  8. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/models/code_types.py +2 -1
  9. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/pyproject.toml +1 -1
  10. codecompass_mcp-2.4.0/tests/test_php_parser.py +217 -0
  11. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/LICENSE +0 -0
  12. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/README.md +0 -0
  13. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/codecompass_mcp.egg-info/dependency_links.txt +0 -0
  14. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/codecompass_mcp.egg-info/entry_points.txt +0 -0
  15. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/codecompass_mcp.egg-info/requires.txt +0 -0
  16. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/codecompass_mcp.egg-info/top_level.txt +0 -0
  17. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/config.py +0 -0
  18. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/graph/__init__.py +0 -0
  19. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/graph/cli.py +0 -0
  20. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/graph/setup.py +0 -0
  21. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/ingestion/__init__.py +0 -0
  22. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/ingestion/chunker.py +0 -0
  23. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/ingestion/entity_resolver.py +0 -0
  24. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/ingestion/file_watcher.py +0 -0
  25. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/ingestion/graph_writer.py +0 -0
  26. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/ingestion/hierarchy_builder.py +0 -0
  27. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/ingestion/reader_agent.py +0 -0
  28. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/ingestion/source_context.py +0 -0
  29. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/main.py +0 -0
  30. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/models/__init__.py +0 -0
  31. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/models/types.py +0 -0
  32. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/setup.cfg +0 -0
  33. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/tests/test_batch_impact.py +0 -0
  34. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/tests/test_blast_radius.py +0 -0
  35. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/tests/test_code_parser.py +0 -0
  36. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/tests/test_ingestion.py +0 -0
  37. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/tests/test_lit_css_parser.py +0 -0
  38. {codecompass_mcp-2.3.0 → codecompass_mcp-2.4.0}/utils/__init__.py +0 -0
  39. {codecompass_mcp-2.3.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.0
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecompass-mcp
3
- Version: 2.3.0
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
@@ -32,5 +32,6 @@ tests/test_blast_radius.py
32
32
  tests/test_code_parser.py
33
33
  tests/test_ingestion.py
34
34
  tests/test_lit_css_parser.py
35
+ tests/test_php_parser.py
35
36
  utils/__init__.py
36
37
  utils/formatting.py
@@ -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({
@@ -667,6 +667,11 @@ _KIND_FILL = {
667
667
  "module": "#E8F5E9",
668
668
  "endpoint": "#FCE4EC",
669
669
  "variable": "#F3E5F5",
670
+ "interface": "#FFF8E1",
671
+ "trait": "#E0F7FA",
672
+ "enum": "#EDE7F6",
673
+ "property": "#FBE9E7",
674
+ "constant": "#F1F8E9",
670
675
  }
671
676
 
672
677
 
@@ -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", ".php"}
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-]+)')
@@ -103,7 +120,7 @@ def _load_html_parser() -> tuple[Parser, Language]:
103
120
 
104
121
  def _load_php_parser() -> tuple[Parser, Language]:
105
122
  import tree_sitter_php as tsphp
106
- return _make_parser(tsphp.language)
123
+ return _make_parser(tsphp.language_php)
107
124
 
108
125
 
109
126
  def _load_css_parser() -> tuple[Parser, Language]:
@@ -114,6 +131,7 @@ def _load_css_parser() -> tuple[Parser, Language]:
114
131
  _PARSER_LOADERS: dict[str, Callable[[], tuple[Parser, Language]]] = {
115
132
  ".py": _load_python_parser,
116
133
  ".js": _load_javascript_parser,
134
+ ".jsx": _load_javascript_parser,
117
135
  ".ts": _load_typescript_parser,
118
136
  ".tsx": _load_tsx_parser,
119
137
  ".html": _load_html_parser,
@@ -218,7 +236,7 @@ def _extract_python(root: Node, source: bytes, file_path: str) -> list[CodeTripl
218
236
  to_entity=_text(base),
219
237
  to_type=TYPE_CLASS,
220
238
  source_file=file_path,
221
- line_number=_line(name_node),
239
+ line_number=_line(base),
222
240
  ))
223
241
 
224
242
  case "import_statement":
@@ -299,68 +317,366 @@ def _extract_python_callee(call_node: Node) -> str | None:
299
317
  # ---------------------------------------------------------------------------
300
318
  # PHP extraction
301
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
+
302
439
 
303
440
  def _extract_php(root: Node, source: bytes, file_path: str) -> list[CodeTriple]:
304
441
  module_name = _module_name_from_path(file_path)
305
442
  triples: list[CodeTriple] = []
306
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
+
307
483
  for node in _walk(root):
308
484
  match node.type:
485
+ # -- Functions -----------------------------------------------
309
486
  case "function_definition":
310
- name = _get_node_name(node, ["function_name", "identifier"])
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)
311
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
312
548
  triples.append(CodeTriple(
313
549
  from_entity=name,
314
- from_type=TYPE_FUNCTION,
550
+ from_type=TYPE_CONSTANT,
315
551
  relation_type=DEFINED_IN,
316
- to_entity=module_name,
317
- to_type=TYPE_MODULE,
552
+ to_entity=container_name,
553
+ to_type=container_type,
318
554
  source_file=file_path,
319
555
  line_number=_line(node),
320
556
  ))
321
557
 
322
- case "class_definition":
323
- name = _get_node_name(node, ["class_name", "identifier"])
324
- if not name:
325
- continue
326
- class_name = name
327
-
328
- # Extract base classes (extends)
329
- extends_node = _child_of_type(node, "extends_clause")
330
- if extends_node:
331
- base_node = _child_of_type(extends_node, "type_name") or _child_of_type(extends_node, "identifier")
332
- if base_node:
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"):
333
564
  triples.append(CodeTriple(
334
- from_entity=class_name,
335
- from_type=TYPE_CLASS,
565
+ from_entity=container_name,
566
+ from_type=container_type,
336
567
  relation_type=INHERITS,
337
- to_entity=_text(base_node),
338
- to_type=TYPE_CLASS,
568
+ to_entity=_text(target),
569
+ to_type=TYPE_TRAIT,
339
570
  source_file=file_path,
340
- line_number=_line(base_node),
571
+ line_number=_line(target),
341
572
  ))
342
573
 
343
- case "function_call":
344
- callee = _extract_php_callee(node)
345
- if callee and _is_meaningful_callee(callee):
346
- scope = _enclosing_scope(node) # Reuse python-style scope walker for now
347
- caller = scope or module_name
348
- caller_type = TYPE_FUNCTION if scope else TYPE_MODULE
574
+ # -- Namespace imports -----------------------------------------
575
+ case "namespace_use_declaration":
576
+ for dotted, target_node in _extract_php_namespace_use(node):
349
577
  triples.append(CodeTriple(
350
- from_entity=caller,
351
- from_type=caller_type,
352
- relation_type=CALLS,
353
- to_entity=callee,
354
- to_type=TYPE_FUNCTION,
578
+ from_entity=module_name,
579
+ from_type=TYPE_MODULE,
580
+ relation_type=IMPORTS,
581
+ to_entity=dotted,
582
+ to_type=TYPE_MODULE,
355
583
  source_file=file_path,
356
- line_number=_line(node),
584
+ line_number=_line(target_node),
357
585
  ))
358
586
 
359
- return triples
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)
360
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)
361
662
 
362
- def _extract_php_callee(call_node: Node) -> str | None:
363
- return _get_node_name(call_node, ["function_name", "identifier"])
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
364
680
 
365
681
  # ---------------------------------------------------------------------------
366
682
  # JavaScript / TypeScript extraction
@@ -723,6 +1039,7 @@ def _extract_lit_css_tokens(source_text: str, file_path: str) -> list[CodeTriple
723
1039
  _EXTRACTORS: dict[str, Callable] = {
724
1040
  ".py": _extract_python,
725
1041
  ".js": _extract_javascript,
1042
+ ".jsx": _extract_javascript,
726
1043
  ".ts": _extract_javascript,
727
1044
  ".tsx": _extract_javascript,
728
1045
  ".html": _extract_html,
@@ -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.0"
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
File without changes