From 51f805e9537d01d2bc31b83d3c3fe3f76640b35f Mon Sep 17 00:00:00 2001 From: Synvoya <16019863+Synvoya@users.noreply.github.com> Date: Thu, 2 Jul 2026 00:22:51 +1000 Subject: [PATCH] fix(php): emit type references for promoted constructor properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PHP 8 constructor property promotion (`__construct(private Repo $repo)`) parses the promoted parameter as `property_promotion_parameter`, not `simple_parameter`. The PHP parameter loop filtered on `simple_parameter` only, so promoted params were skipped entirely: their type emitted no `parameter_type` edge on the constructor, and — because a promoted param is also a real class field — no `field` edge on the class either. A non-promoted param in the same signature still emitted `parameter_type`, so the type reference was silently dropped for exactly the promoted case. The promoted param's type sits in the same direct named-child shape the loop already reads for `simple_parameter`, so widening the filter to accept `property_promotion_parameter` makes the existing type extraction emit the `parameter_type` edge. Additionally, for a promoted param, emit a `field`-context references edge on the class (mirroring the `property_declaration` handler), guarded so it only fires when a parent class is in scope and the target is not the class node itself. Normal `simple_parameter` behaviour is unchanged. Adds a promoted-property constructor to tests/fixtures/sample.php and test_php_constructor_property_promotion_contexts asserting the promoted type appears as both `field` and `parameter_type`, and that a non-promoted param does not leak a field edge. --- graphify/extract.py | 15 ++++++++++++++- tests/fixtures/sample.php | 7 +++++++ tests/test_languages.py | 10 ++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/graphify/extract.py b/graphify/extract.py index 6887d08..e6c611d 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -4069,8 +4069,14 @@ def _extract_generic( break if params_container is not None: for p in params_container.children: - if p.type != "simple_parameter": + # PHP 8 constructor property promotion (`__construct(private + # Repo $repo)`) parses the promoted param as + # property_promotion_parameter, not simple_parameter. Its + # type sits in the same direct named child shape, so accept + # both here; a promoted param is additionally a class field. + if p.type not in ("simple_parameter", "property_promotion_parameter"): continue + is_promoted = p.type == "property_promotion_parameter" type_node = None for sub in p.children: if sub.type in ("named_type", "primitive_type", "nullable_type", @@ -4084,6 +4090,13 @@ def _extract_generic( target_nid = ensure_named_node(ref_name, line) if target_nid != func_nid: add_edge(func_nid, target_nid, "references", line, context=ctx) + # A promoted param declares a real class field; mirror + # the property_declaration field-context edge so the + # type is discoverable as a class field too. + if is_promoted and parent_class_nid and target_nid != parent_class_nid: + fctx = "generic_arg" if role == "generic_arg" else "field" + add_edge(parent_class_nid, target_nid, "references", + line, context=fctx) return_node = _php_method_return_type_node(node) if return_node is not None: refs = [] diff --git a/tests/fixtures/sample.php b/tests/fixtures/sample.php index 1397f56..5ff337a 100644 --- a/tests/fixtures/sample.php +++ b/tests/fixtures/sample.php @@ -66,6 +66,13 @@ class DataProcessor extends BaseProcessor implements Loggable } } +class Service +{ + public function __construct(private Result $result, string $label) + { + } +} + function parseResponse(string $raw): array { return json_decode($raw, true); diff --git a/tests/test_languages.py b/tests/test_languages.py index 8227131..95dfd9a 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -777,6 +777,16 @@ def test_php_property_parameter_and_return_contexts(): assert ("run", "Result") in _edge_labels(r, "references", "return_type") +def test_php_constructor_property_promotion_contexts(): + # PHP 8 constructor property promotion: a promoted param is both a + # constructor parameter (parameter_type) and a class field (field). + r = extract_php(FIXTURES / "sample.php") + assert ("Service", "Result") in _edge_labels(r, "references", "field") + assert ("__construct", "Result") in _edge_labels(r, "references", "parameter_type") + # A non-promoted param must not leak a field edge onto the class. + assert ("Service", "string") not in _edge_labels(r, "references", "field") + + # ── Swift ──────────────────────────────────────────────────────────────────── def test_swift_no_error():