fix(php): emit type references for promoted constructor properties

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.
This commit is contained in:
Synvoya
2026-07-01 16:36:49 +01:00
committed by safishamsi
parent cd3a376030
commit 51f805e953
3 changed files with 31 additions and 1 deletions
+14 -1
View File
@@ -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 = []
+7
View File
@@ -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);
+10
View File
@@ -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():