Skip to content

Commit 21cff5d

Browse files
committed
fix(analyzer): template parameters with compatible constraints should be allowed
closes #739 Signed-off-by: azjezz <[email protected]>
1 parent b7a4a31 commit 21cff5d

File tree

4 files changed

+137
-20
lines changed

4 files changed

+137
-20
lines changed

crates/analyzer/src/invocation/resolver.rs

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,27 +44,52 @@ pub fn resolve_invocation_type<'ctx, 'ast, 'arena>(
4444
}
4545
};
4646

47-
if let Some(template_types) = invocation.target.get_template_types()
48-
&& !template_types.is_empty()
49-
{
50-
for (template_name, _) in template_types {
51-
let has_bound_for_this_parent = template_result
52-
.lower_bounds
53-
.get(template_name)
54-
.and_then(|bounds| bounds.get(&generic_parent))
55-
.is_some_and(|bounds| !bounds.is_empty());
47+
let method_templates = invocation.target.get_template_types().unwrap_or(&[]);
48+
49+
let all_template_names: Vec<_> = method_templates
50+
.iter()
51+
.map(|(name, _)| *name)
52+
.chain(template_result.template_types.keys().copied())
53+
.collect();
54+
55+
for template_name in all_template_names {
56+
let has_bound_for_method = template_result
57+
.lower_bounds
58+
.get(&template_name)
59+
.and_then(|bounds| bounds.get(&generic_parent))
60+
.is_some_and(|bounds| !bounds.is_empty());
61+
62+
let method_parents: Vec<_> = method_templates
63+
.iter()
64+
.filter(|(name, _)| name == &template_name)
65+
.flat_map(|(_, constraints)| constraints.iter().map(|(parent, _)| parent))
66+
.collect();
67+
68+
let result_parents: Vec<_> = template_result
69+
.template_types
70+
.get(&template_name)
71+
.map(|v| v.iter().map(|(parent, _)| parent).collect())
72+
.unwrap_or_default();
73+
74+
let has_bound_for_template_parent =
75+
method_parents.iter().chain(result_parents.iter()).any(|constraint_parent| {
76+
template_result
77+
.lower_bounds
78+
.get(&template_name)
79+
.and_then(|bounds| bounds.get(*constraint_parent))
80+
.is_some_and(|bounds| !bounds.is_empty())
81+
});
5682

57-
if !has_bound_for_this_parent {
58-
let mut owned_template_result = template_result.into_owned();
83+
if !has_bound_for_method && !has_bound_for_template_parent {
84+
let mut owned_template_result = template_result.into_owned();
5985

60-
owned_template_result
61-
.lower_bounds
62-
.entry(*template_name)
63-
.or_default()
64-
.insert(generic_parent, vec![TemplateBound::new(get_never(), 1, None, None)]);
86+
owned_template_result
87+
.lower_bounds
88+
.entry(template_name)
89+
.or_default()
90+
.insert(generic_parent, vec![TemplateBound::new(get_never(), 1, None, None)]);
6591

66-
template_result = Cow::Owned(owned_template_result);
67-
}
92+
template_result = Cow::Owned(owned_template_result);
6893
}
6994
}
7095
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* @template-covariant T
7+
*/
8+
interface TypeInterface
9+
{
10+
/**
11+
* @return T
12+
*
13+
* @phpstan-assert T $value
14+
*/
15+
public function assert(mixed $value): mixed;
16+
17+
/**
18+
* @phpstan-assert-if-true T $value
19+
*/
20+
public function isValid(mixed $value): bool;
21+
}
22+
23+
/**
24+
* @template TLeft
25+
* @template TRight
26+
*
27+
* @implements TypeInterface<TLeft|TRight>
28+
*/
29+
final class UnionType implements TypeInterface
30+
{
31+
/**
32+
* @param TypeInterface<TLeft> $left
33+
* @param TypeInterface<TRight> $right
34+
*/
35+
public function __construct(
36+
private TypeInterface $left,
37+
private TypeInterface $right,
38+
) {}
39+
40+
#[Override]
41+
public function assert(mixed $value): mixed
42+
{
43+
if ($this->left->isValid($value)) {
44+
return $value;
45+
}
46+
47+
if ($this->right->isValid($value)) {
48+
return $value;
49+
}
50+
51+
die('invalid');
52+
}
53+
54+
#[Override]
55+
public function isValid(mixed $value): bool
56+
{
57+
return $this->left->isValid($value) || $this->right->isValid($value);
58+
}
59+
}

crates/analyzer/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ test_case!(issue_725);
442442
test_case!(issue_728_symfony_reference);
443443
test_case!(issue_736);
444444
test_case!(issue_737);
445+
test_case!(issue_739);
445446

446447
#[test]
447448
fn test_all_test_cases_are_ran() {

crates/codex/src/ttype/comparator/atomic_comparator.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,24 @@ pub fn is_contained_by(
110110
}
111111
}
112112

113+
if inside_assertion
114+
&& let TAtomic::GenericParameter(TGenericParameter {
115+
parameter_name: container_param_name,
116+
defining_entity: container_entity,
117+
..
118+
}) = container_type_part
119+
&& let TAtomic::GenericParameter(TGenericParameter {
120+
parameter_name: input_param_name,
121+
defining_entity: input_entity,
122+
..
123+
}) = input_type_part
124+
{
125+
// Different template parameters are not contained by each other during assertion reconciliation
126+
if input_param_name != container_param_name || input_entity != container_entity {
127+
return false;
128+
}
129+
}
130+
113131
if container_type_part.is_vanilla_mixed() || container_type_part.is_templated_as_vanilla_mixed() {
114132
return true;
115133
}
@@ -372,9 +390,23 @@ pub fn is_contained_by(
372390
return false;
373391
}
374392

375-
if let TAtomic::GenericParameter(TGenericParameter { constraint: container_constraint, .. }) = container_type_part
376-
&& let TAtomic::GenericParameter(TGenericParameter { constraint: input_constraint, .. }) = input_type_part
393+
if let TAtomic::GenericParameter(TGenericParameter {
394+
parameter_name: container_param_name,
395+
defining_entity: container_entity,
396+
constraint: container_constraint,
397+
..
398+
}) = container_type_part
399+
&& let TAtomic::GenericParameter(TGenericParameter {
400+
parameter_name: input_param_name,
401+
defining_entity: input_entity,
402+
constraint: input_constraint,
403+
..
404+
}) = input_type_part
377405
{
406+
if inside_assertion && (input_param_name != container_param_name || input_entity != container_entity) {
407+
return false;
408+
}
409+
378410
return union_comparator::is_contained_by(
379411
codebase,
380412
input_constraint,

0 commit comments

Comments
 (0)