Skip to content

Commit f3d2f71

Browse files
committed
Walk attribute groups of closures and arrow functions
Amp-Thread-ID: https://ampcode.com/threads/T-01a179f2-995f-7745-8d8c-4c174ba5c0ce
1 parent f75b4f6 commit f3d2f71

5 files changed

Lines changed: 284 additions & 142 deletions

File tree

‎src/Analyser/ClosureProcessor.php‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,34 @@ private function getParametersProcessor(): ParametersProcessor
7474
return $this->container->getByType(ParametersProcessor::class);
7575
}
7676

77+
/**
78+
* Looked up instead of injected for the same reason as getParametersProcessor().
79+
*/
80+
private function getAttributesHandler(): AttributesHandler
81+
{
82+
return $this->container->getByType(AttributesHandler::class);
83+
}
84+
85+
/**
86+
* Returns what the attributes and the parameters of the closure or arrow function depend on.
87+
*
88+
* @param callable(Node $node, Scope $scope): void $nodeCallback
89+
*/
90+
private function processAttributeGroupsAndParams(
91+
NodeScopeResolver $nodeScopeResolver,
92+
Node\Stmt $stmt,
93+
Expr\Closure|Expr\ArrowFunction $expr,
94+
MutatingScope $scope,
95+
ExpressionResultStorage $storage,
96+
callable $nodeCallback,
97+
): ?Dependencies
98+
{
99+
return Dependencies::merge(
100+
$this->getAttributesHandler()->processAttributeGroups($nodeScopeResolver, $stmt, $expr->attrGroups, $scope, $storage, $nodeCallback),
101+
$this->getParametersProcessor()->processParams($nodeScopeResolver, $stmt, $expr->params, $scope, $storage, $nodeCallback),
102+
);
103+
}
104+
77105
/**
78106
* @param callable(Node $node, Scope $scope): void $nodeCallback
79107
*/
@@ -107,7 +135,7 @@ private function processClosureNodeInternal(
107135
?Type $nativePassedToType = null,
108136
): ProcessClosureResult
109137
{
110-
$parametersDependencies = $this->getParametersProcessor()->processParams($nodeScopeResolver, $stmt, $expr->params, $scope, $storage, $nodeCallback);
138+
$parametersDependencies = $this->processAttributeGroupsAndParams($nodeScopeResolver, $stmt, $expr, $scope, $storage, $nodeCallback);
111139

112140
$byRefUses = [];
113141

@@ -756,7 +784,7 @@ public function processArrowFunctionNode(
756784
): ProcessArrowFunctionResult
757785
{
758786
$context ??= ExpressionContext::createTopLevel();
759-
$parametersDependencies = $this->getParametersProcessor()->processParams($nodeScopeResolver, $stmt, $expr->params, $scope, $storage, $nodeCallback);
787+
$parametersDependencies = $this->processAttributeGroupsAndParams($nodeScopeResolver, $stmt, $expr, $scope, $storage, $nodeCallback);
760788
if ($this->closureSignatureInference->isObserving($scope)) {
761789
foreach (ClosureSignatureInference::getArrowFunctionOuterVariables($expr) as $name) {
762790
if (!$scope->hasVariableType($name)->yes()) {

‎tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,4 +604,36 @@ public function testConditionallyExecutedClassConstantOnExpression(): void
604604
$this->analyse([__DIR__ . '/data/class-constant-on-expr-php-versions.php'], $errors);
605605
}
606606

607+
#[RequiresPhp('>= 8.0.0')]
608+
public function testClosureAttributeArguments(): void
609+
{
610+
$tip = 'Learn more at https://phpstan.org/user-guide/discovering-symbols';
611+
$this->analyse([__DIR__ . '/data/closure-attribute-class-constant.php'], [
612+
[
613+
'Access to constant FOO on an unknown class ClosureAttributeClassConstant\\Missing.',
614+
26,
615+
$tip,
616+
],
617+
[
618+
'Access to constant FOO on an unknown class ClosureAttributeClassConstant\\Missing.',
619+
27,
620+
$tip,
621+
],
622+
[
623+
'Access to constant FOO on an unknown class ClosureAttributeClassConstant\\Missing.',
624+
28,
625+
$tip,
626+
],
627+
[
628+
'Access to undefined constant ClosureAttributeClassConstant\\Foo::BAR.',
629+
29,
630+
],
631+
[
632+
'Access to constant FOO on an unknown class ClosureAttributeClassConstant\\Missing.',
633+
29,
634+
$tip,
635+
],
636+
]);
637+
}
638+
607639
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php // lint >= 8.0
2+
3+
namespace ClosureAttributeClassConstant;
4+
5+
#[\Attribute]
6+
class Attr
7+
{
8+
9+
public function __construct(public string $value)
10+
{
11+
}
12+
13+
}
14+
15+
#[\Attribute]
16+
class AttrWithoutConstructor
17+
{
18+
19+
}
20+
21+
class Foo
22+
{
23+
24+
public function doFoo(): void
25+
{
26+
$f = #[Attr(Missing::FOO)] function () {};
27+
$g = #[Attr(Missing::FOO)] fn () => 1;
28+
$h = #[AttrWithoutConstructor(Missing::FOO)] static function () {};
29+
$i = #[Attr(self::BAR)] fn () => #[Attr(Missing::FOO)] fn () => 1;
30+
}
31+
32+
}

‎turbo-ext/src/ClosureProcessor.cpp‎

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,20 @@ zv::Val getParametersProcessor(zval *container)
7474
return service;
7575
}
7676

77+
/* $this->container->getByType(AttributesHandler::class) */
78+
zv::Val getAttributesHandler(zval *container)
79+
{
80+
if (UNEXPECTED(Z_TYPE_P(container) != IS_OBJECT)) {
81+
zend_throw_error(NULL, "Call to a member function getByType() on %s", zend_zval_value_name(container));
82+
return zv::Val();
83+
}
84+
zval className;
85+
ZVAL_STRINGL(&className, "PHPStan\\Analyser\\AttributesHandler", sizeof("PHPStan\\Analyser\\AttributesHandler") - 1);
86+
zv::Val service = pt_call_method_cached(pt_cp_get_by_type_site, Z_OBJ_P(container), PT_LC("getbytype"), 1, &className);
87+
zval_ptr_dtor(&className);
88+
return service;
89+
}
90+
7791
/* $invalidateExprNode->getExpr(): the final node's promoted $expr, the method
7892
* of anything else; UNDEF = pending exception */
7993
zv::Val invalidateExprNodeExpr(zval *node)
@@ -636,7 +650,7 @@ class ClosureProcessor
636650
if (UNEXPECTED(contextHold.isUndef())) return zv::Val();
637651
context = contextHold.raw();
638652
}
639-
zv::Val parametersDependencies = processParams(nodeScopeResolver, stmt, expr, scopeArg, storage, nodeCallback);
653+
zv::Val parametersDependencies = processAttributeGroupsAndParams(nodeScopeResolver, stmt, expr, scopeArg, storage, nodeCallback);
640654
if (UNEXPECTED(parametersDependencies.isUndef())) return zv::Val();
641655
zv::Val scopeHold = zv::Val::copyOf(zv::Ref(scopeArg));
642656
bool observing;
@@ -847,6 +861,28 @@ class ClosureProcessor
847861
Z_PROP_FLAG_P(OBJ_PROP_NUM(self, index)) = 0;
848862
}
849863

864+
/* Mirrors processAttributeGroupsAndParams(): Dependencies::merge(
865+
* $this->getAttributesHandler()->processAttributeGroups(..., $expr->attrGroups, ...),
866+
* $this->getParametersProcessor()->processParams(..., $expr->params, ...));
867+
* UNDEF = pending exception */
868+
zv::Val processAttributeGroupsAndParams(zval *nodeScopeResolver, zval *stmt, zval *expr, zval *scope, zval *storage, zval *nodeCallback) const
869+
{
870+
zv::Val attributesHandler = getAttributesHandler(slot(slots::container));
871+
if (UNEXPECTED(attributesHandler.isUndef())) return zv::Val();
872+
if (UNEXPECTED(!attributesHandler.ref().isObject())) {
873+
zend_throw_error(NULL, "Call to a member function processAttributeGroups() on %s", zend_zval_value_name(attributesHandler.raw()));
874+
return zv::Val();
875+
}
876+
zval *attrGroups = ptclosure::prop(ptclosure::attrGroupsSite, expr, PT_LC("attrGroups"));
877+
if (UNEXPECTED(attrGroups == NULL)) return zv::Val();
878+
zv::Val attrGroupsHold = zv::Val::copyOf(zv::Ref(attrGroups));
879+
zv::Val attributesDependencies = pt_attributes_handler_process_attribute_groups(attributesHandler.raw(), nodeScopeResolver, stmt, attrGroupsHold.raw(), scope, storage, nodeCallback);
880+
if (UNEXPECTED(attributesDependencies.isUndef())) return zv::Val();
881+
zv::Val parametersDependencies = processParams(nodeScopeResolver, stmt, expr, scope, storage, nodeCallback);
882+
if (UNEXPECTED(parametersDependencies.isUndef())) return zv::Val();
883+
return pt_dependencies_merge({attributesDependencies.raw(), parametersDependencies.raw()});
884+
}
885+
850886
/* $this->getParametersProcessor()->processParams($nodeScopeResolver, $stmt,
851887
* $expr->params, $scope, $storage, $nodeCallback): what the parameters
852888
* depend on; UNDEF = pending exception */
@@ -940,7 +976,7 @@ class ClosureProcessor
940976
* gatherer; false = pending exception */
941977
[[nodiscard]] zend_never_inline bool enterClosure(ClosureWalk &w, zval *passedToType, zval *nativePassedToType) const
942978
{
943-
w.parametersDependencies = processParams(w.nodeScopeResolver, w.stmt, w.expr, w.scope.raw(), w.storage, w.nodeCallback);
979+
w.parametersDependencies = processAttributeGroupsAndParams(w.nodeScopeResolver, w.stmt, w.expr, w.scope.raw(), w.storage, w.nodeCallback);
944980
if (UNEXPECTED(w.parametersDependencies.isUndef())) return false;
945981

946982
zval *closureCallArgs = ptclosure::attribute(w.expr, pt_cp_closure_call_args);

0 commit comments

Comments
 (0)