codecompass-mcp 2.3.0__tar.gz → 2.5.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 (41) hide show
  1. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/PKG-INFO +1 -1
  2. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/codecompass_mcp.egg-info/PKG-INFO +1 -1
  3. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/codecompass_mcp.egg-info/SOURCES.txt +3 -0
  4. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/graph/code_graph_client.py +2 -0
  5. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/graph/code_query_cli.py +5 -0
  6. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/ingestion/code_normalizer.py +2 -1
  7. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/ingestion/code_parser.py +356 -39
  8. codecompass_mcp-2.5.0/ingestion/description_enricher.py +189 -0
  9. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/main.py +81 -9
  10. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/models/code_types.py +2 -1
  11. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/pyproject.toml +1 -1
  12. codecompass_mcp-2.5.0/tests/test_description_enricher.py +95 -0
  13. codecompass_mcp-2.5.0/tests/test_php_parser.py +217 -0
  14. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/LICENSE +0 -0
  15. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/README.md +0 -0
  16. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/codecompass_mcp.egg-info/dependency_links.txt +0 -0
  17. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/codecompass_mcp.egg-info/entry_points.txt +0 -0
  18. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/codecompass_mcp.egg-info/requires.txt +0 -0
  19. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/codecompass_mcp.egg-info/top_level.txt +0 -0
  20. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/config.py +0 -0
  21. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/graph/__init__.py +0 -0
  22. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/graph/cli.py +0 -0
  23. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/graph/setup.py +0 -0
  24. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/ingestion/__init__.py +0 -0
  25. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/ingestion/chunker.py +0 -0
  26. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/ingestion/entity_resolver.py +0 -0
  27. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/ingestion/file_watcher.py +0 -0
  28. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/ingestion/graph_writer.py +0 -0
  29. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/ingestion/hierarchy_builder.py +0 -0
  30. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/ingestion/reader_agent.py +0 -0
  31. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/ingestion/source_context.py +0 -0
  32. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/models/__init__.py +0 -0
  33. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/models/types.py +0 -0
  34. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/setup.cfg +0 -0
  35. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/tests/test_batch_impact.py +0 -0
  36. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/tests/test_blast_radius.py +0 -0
  37. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/tests/test_code_parser.py +0 -0
  38. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/tests/test_ingestion.py +0 -0
  39. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/tests/test_lit_css_parser.py +0 -0
  40. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.0}/utils/__init__.py +0 -0
  41. {codecompass_mcp-2.3.0 → codecompass_mcp-2.5.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.5.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.5.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
@@ -18,6 +18,7 @@ ingestion/__init__.py
18
18
  ingestion/chunker.py
19
19
  ingestion/code_normalizer.py
20
20
  ingestion/code_parser.py
21
+ ingestion/description_enricher.py
21
22
  ingestion/entity_resolver.py
22
23
  ingestion/file_watcher.py
23
24
  ingestion/graph_writer.py
@@ -30,7 +31,9 @@ models/types.py
30
31
  tests/test_batch_impact.py
31
32
  tests/test_blast_radius.py
32
33
  tests/test_code_parser.py
34
+ tests/test_description_enricher.py
33
35
  tests/test_ingestion.py
34
36
  tests/test_lit_css_parser.py
37
+ tests/test_php_parser.py
35
38
  utils/__init__.py
36
39
  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,