Skip to content

Commit 7cea81a

Browse files
committed
Process property hooks
* They get a new virtual node InPropertyHookNode * Existing virtual nodes like InFunctionNode or InClassMethodNode are NOT invoked for them * But rules for FunctionLike node are invoked for property hooks * `Scope::getFunction()` returns PhpMethodFromParserNodeReflection inside property hooks
1 parent 8a1da56 commit 7cea81a

File tree

8 files changed

+547
-24
lines changed

8 files changed

+547
-24
lines changed

src/Analyser/MutatingScope.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Generator;
88
use PhpParser\Node;
99
use PhpParser\Node\Arg;
10+
use PhpParser\Node\ComplexType;
1011
use PhpParser\Node\Expr;
1112
use PhpParser\Node\Expr\Array_;
1213
use PhpParser\Node\Expr\BinaryOp;
@@ -21,6 +22,7 @@
2122
use PhpParser\Node\Expr\New_;
2223
use PhpParser\Node\Expr\PropertyFetch;
2324
use PhpParser\Node\Expr\Variable;
25+
use PhpParser\Node\Identifier;
2426
use PhpParser\Node\InterpolatedStringPart;
2527
use PhpParser\Node\Name;
2628
use PhpParser\Node\Name\FullyQualified;
@@ -2966,6 +2968,7 @@ public function enterClassMethod(
29662968
new PhpMethodFromParserNodeReflection(
29672969
$this->getClassReflection(),
29682970
$classMethod,
2971+
null,
29692972
$this->getFile(),
29702973
$templateTypeMap,
29712974
$this->getRealParameterTypes($classMethod),
@@ -2991,6 +2994,88 @@ public function enterClassMethod(
29912994
);
29922995
}
29932996

2997+
/**
2998+
* @param Type[] $phpDocParameterTypes
2999+
*/
3000+
public function enterPropertyHook(
3001+
Node\PropertyHook $hook,
3002+
string $propertyName,
3003+
Identifier|Name|ComplexType|null $nativePropertyTypeNode,
3004+
?Type $phpDocPropertyType,
3005+
array $phpDocParameterTypes,
3006+
?Type $throwType,
3007+
?string $phpDocComment,
3008+
): self
3009+
{
3010+
if (!$this->isInClass()) {
3011+
throw new ShouldNotHappenException();
3012+
}
3013+
3014+
$phpDocParameterTypes = array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocParameterTypes);
3015+
3016+
$hookName = $hook->name->toLowerString();
3017+
if ($hookName === 'set') {
3018+
if ($hook->params === []) {
3019+
$hook = clone $hook;
3020+
$hook->params = [
3021+
new Node\Param(new Variable('value'), null, $nativePropertyTypeNode),
3022+
];
3023+
}
3024+
3025+
$firstParam = $hook->params[0] ?? null;
3026+
if (
3027+
$firstParam !== null
3028+
&& $phpDocPropertyType !== null
3029+
&& $firstParam->var instanceof Variable
3030+
&& is_string($firstParam->var->name)
3031+
) {
3032+
$valueParamPhpDocType = $phpDocParameterTypes[$firstParam->var->name] ?? null;
3033+
if ($valueParamPhpDocType === null) {
3034+
$phpDocParameterTypes[$firstParam->var->name] = $this->transformStaticType(TemplateTypeHelper::toArgument($phpDocPropertyType));
3035+
}
3036+
}
3037+
3038+
$realReturnType = new VoidType();
3039+
$phpDocReturnType = null;
3040+
} elseif ($hookName === 'get') {
3041+
$realReturnType = $this->getFunctionType($nativePropertyTypeNode, false, false);
3042+
$phpDocReturnType = $phpDocPropertyType !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($phpDocPropertyType)) : null;
3043+
} else {
3044+
throw new ShouldNotHappenException();
3045+
}
3046+
3047+
$realParameterTypes = $this->getRealParameterTypes($hook);
3048+
3049+
return $this->enterFunctionLike(
3050+
new PhpMethodFromParserNodeReflection(
3051+
$this->getClassReflection(),
3052+
$hook,
3053+
$propertyName,
3054+
$this->getFile(),
3055+
TemplateTypeMap::createEmpty(),
3056+
$realParameterTypes,
3057+
$phpDocParameterTypes,
3058+
[],
3059+
$realReturnType,
3060+
$phpDocReturnType,
3061+
$throwType,
3062+
null,
3063+
false,
3064+
false,
3065+
false,
3066+
false,
3067+
true,
3068+
Assertions::createEmpty(),
3069+
null,
3070+
$phpDocComment,
3071+
[],
3072+
[],
3073+
[],
3074+
),
3075+
true,
3076+
);
3077+
}
3078+
29943079
private function transformStaticType(Type $type): Type
29953080
{
29963081
return TypeTraverser::map($type, function (Type $type, callable $traverse): Type {

src/Analyser/NodeScopeResolver.php

Lines changed: 103 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PhpParser\Node;
1111
use PhpParser\Node\Arg;
1212
use PhpParser\Node\AttributeGroup;
13+
use PhpParser\Node\ComplexType;
1314
use PhpParser\Node\Expr;
1415
use PhpParser\Node\Expr\Array_;
1516
use PhpParser\Node\Expr\ArrayDimFetch;
@@ -35,6 +36,7 @@
3536
use PhpParser\Node\Expr\StaticPropertyFetch;
3637
use PhpParser\Node\Expr\Ternary;
3738
use PhpParser\Node\Expr\Variable;
39+
use PhpParser\Node\Identifier;
3840
use PhpParser\Node\Name;
3941
use PhpParser\Node\Stmt\Break_;
4042
use PhpParser\Node\Stmt\Class_;
@@ -98,6 +100,7 @@
98100
use PHPStan\Node\InClosureNode;
99101
use PHPStan\Node\InForeachNode;
100102
use PHPStan\Node\InFunctionNode;
103+
use PHPStan\Node\InPropertyHookNode;
101104
use PHPStan\Node\InstantiationCallableNode;
102105
use PHPStan\Node\InTraitNode;
103106
use PHPStan\Node\InvalidateExprNode;
@@ -642,6 +645,8 @@ private function processStmtNode(
642645
throw new ShouldNotHappenException();
643646
}
644647

648+
$classReflection = $scope->getClassReflection();
649+
645650
$isFromTrait = $stmt->getAttribute('originalTraitMethodName') === '__construct';
646651
if ($isFromTrait || $stmt->name->toLowerString() === '__construct') {
647652
foreach ($stmt->params as $param) {
@@ -659,7 +664,7 @@ private function processStmtNode(
659664
$nodeCallback(new ClassPropertyNode(
660665
$param->var->name,
661666
$param->flags,
662-
$param->type !== null ? ParserNodeTypeToPHPStanType::resolve($param->type, $scope->getClassReflection()) : null,
667+
$param->type !== null ? ParserNodeTypeToPHPStanType::resolve($param->type, $classReflection) : null,
663668
null,
664669
$phpDoc,
665670
$phpDocParameterTypes[$param->var->name] ?? null,
@@ -668,10 +673,19 @@ private function processStmtNode(
668673
$param,
669674
false,
670675
$scope->isInTrait(),
671-
$scope->getClassReflection()->isReadOnly(),
676+
$classReflection->isReadOnly(),
672677
false,
673-
$scope->getClassReflection(),
678+
$classReflection,
674679
), $methodScope);
680+
$this->processPropertyHooks(
681+
$stmt,
682+
$param->type,
683+
$phpDocParameterTypes[$param->var->name] ?? null,
684+
$param->var->name,
685+
$param->hooks,
686+
$scope,
687+
$nodeCallback,
688+
);
675689
$methodScope = $methodScope->assignExpression(new PropertyInitializationExpr($param->var->name), new MixedType(), new MixedType());
676690
}
677691
}
@@ -681,7 +695,7 @@ private function processStmtNode(
681695
if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) {
682696
throw new ShouldNotHappenException();
683697
}
684-
$nodeCallback(new InClassMethodNode($scope->getClassReflection(), $methodReflection, $stmt), $methodScope);
698+
$nodeCallback(new InClassMethodNode($classReflection, $methodReflection, $stmt), $methodScope);
685699
}
686700

687701
if ($stmt->stmts !== null) {
@@ -730,8 +744,6 @@ private function processStmtNode(
730744
$gatheredReturnStatements[] = new ReturnStatement($scope, $node);
731745
}, StatementContext::createTopLevel());
732746

733-
$classReflection = $scope->getClassReflection();
734-
735747
$methodReflection = $methodScope->getFunction();
736748
if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) {
737749
throw new ShouldNotHappenException();
@@ -893,29 +905,38 @@ private function processStmtNode(
893905
$impurePoints = [];
894906
$this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback);
895907

908+
$nativePropertyType = $stmt->type !== null ? ParserNodeTypeToPHPStanType::resolve($stmt->type, $scope->getClassReflection()) : null;
909+
910+
[,,,,,,,,,,,,$isReadOnly, $docComment, ,,,$varTags, $isAllowedPrivateMutation] = $this->getPhpDocs($scope, $stmt);
911+
$phpDocType = null;
912+
if (isset($varTags[0]) && count($varTags) === 1) {
913+
$phpDocType = $varTags[0]->getType();
914+
}
915+
896916
foreach ($stmt->props as $prop) {
897917
$nodeCallback($prop, $scope);
898918
if ($prop->default !== null) {
899919
$this->processExprNode($stmt, $prop->default, $scope, $nodeCallback, ExpressionContext::createDeep());
900920
}
901-
[,,,,,,,,,,,,$isReadOnly, $docComment, ,,,$varTags, $isAllowedPrivateMutation] = $this->getPhpDocs($scope, $stmt);
921+
902922
if (!$scope->isInClass()) {
903923
throw new ShouldNotHappenException();
904924
}
905925
$propertyName = $prop->name->toString();
906-
$phpDocType = null;
907-
if (isset($varTags[0]) && count($varTags) === 1) {
908-
$phpDocType = $varTags[0]->getType();
909-
} elseif (isset($varTags[$propertyName])) {
910-
$phpDocType = $varTags[$propertyName]->getType();
926+
927+
if ($phpDocType === null) {
928+
if (isset($varTags[$propertyName])) {
929+
$phpDocType = $varTags[$propertyName]->getType();
930+
}
911931
}
932+
912933
$propStmt = clone $stmt;
913934
$propStmt->setAttributes($prop->getAttributes());
914935
$nodeCallback(
915936
new ClassPropertyNode(
916937
$propertyName,
917938
$stmt->flags,
918-
$stmt->type !== null ? ParserNodeTypeToPHPStanType::resolve($stmt->type, $scope->getClassReflection()) : null,
939+
$nativePropertyType,
919940
$prop->default,
920941
$docComment,
921942
$phpDocType,
@@ -932,6 +953,21 @@ private function processStmtNode(
932953
);
933954
}
934955

956+
if (count($stmt->hooks) > 0) {
957+
if (!isset($propertyName)) {
958+
throw new ShouldNotHappenException('Property name should be known when analysing hooks.');
959+
}
960+
$this->processPropertyHooks(
961+
$stmt,
962+
$stmt->type,
963+
$phpDocType,
964+
$propertyName,
965+
$stmt->hooks,
966+
$scope,
967+
$nodeCallback,
968+
);
969+
}
970+
935971
if ($stmt->type !== null) {
936972
$nodeCallback($stmt->type, $scope);
937973
}
@@ -4614,6 +4650,60 @@ private function processAttributeGroups(
46144650
}
46154651
}
46164652

4653+
/**
4654+
* @param Node\PropertyHook[] $hooks
4655+
* @param callable(Node $node, Scope $scope): void $nodeCallback
4656+
*/
4657+
private function processPropertyHooks(
4658+
Node\Stmt $stmt,
4659+
Identifier|Name|ComplexType|null $nativeTypeNode,
4660+
?Type $phpDocType,
4661+
string $propertyName,
4662+
array $hooks,
4663+
MutatingScope $scope,
4664+
callable $nodeCallback,
4665+
): void
4666+
{
4667+
if (!$scope->isInClass()) {
4668+
throw new ShouldNotHappenException();
4669+
}
4670+
4671+
$classReflection = $scope->getClassReflection();
4672+
4673+
foreach ($hooks as $hook) {
4674+
$nodeCallback($hook, $scope);
4675+
$this->processAttributeGroups($stmt, $hook->attrGroups, $scope, $nodeCallback);
4676+
4677+
[, $phpDocParameterTypes,,,, $phpDocThrowType,,,,,,,, $phpDocComment] = $this->getPhpDocs($scope, $hook);
4678+
4679+
foreach ($hook->params as $param) {
4680+
$this->processParamNode($stmt, $param, $scope, $nodeCallback);
4681+
}
4682+
4683+
$hookScope = $scope->enterPropertyHook(
4684+
$hook,
4685+
$propertyName,
4686+
$nativeTypeNode,
4687+
$phpDocType,
4688+
$phpDocParameterTypes,
4689+
$phpDocThrowType,
4690+
$phpDocComment,
4691+
);
4692+
$hookReflection = $hookScope->getFunction();
4693+
if (!$hookReflection instanceof PhpMethodFromParserNodeReflection) {
4694+
throw new ShouldNotHappenException();
4695+
}
4696+
$nodeCallback(new InPropertyHookNode($classReflection, $hookReflection, $hook), $hookScope);
4697+
4698+
if ($hook->body instanceof Expr) {
4699+
$this->processExprNode($stmt, $hook->body, $hookScope, $nodeCallback, ExpressionContext::createTopLevel());
4700+
} elseif (is_array($hook->body)) {
4701+
$this->processStmtNodes($stmt, $hook->body, $hookScope, $nodeCallback, StatementContext::createTopLevel());
4702+
}
4703+
4704+
}
4705+
}
4706+
46174707
/**
46184708
* @param MethodReflection|FunctionReflection|null $calleeReflection
46194709
* @param callable(Node $node, Scope $scope): void $nodeCallback

src/Node/InPropertyHookNode.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Node;
4+
5+
use PhpParser\Node;
6+
use PhpParser\NodeAbstract;
7+
use PHPStan\Reflection\ClassReflection;
8+
use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
9+
10+
/**
11+
* @api
12+
*/
13+
final class InPropertyHookNode extends NodeAbstract implements VirtualNode
14+
{
15+
16+
public function __construct(
17+
private ClassReflection $classReflection,
18+
private PhpMethodFromParserNodeReflection $hookReflection,
19+
private Node\PropertyHook $originalNode,
20+
)
21+
{
22+
parent::__construct($originalNode->getAttributes());
23+
}
24+
25+
public function getClassReflection(): ClassReflection
26+
{
27+
return $this->classReflection;
28+
}
29+
30+
public function getMethodReflection(): PhpMethodFromParserNodeReflection
31+
{
32+
return $this->hookReflection;
33+
}
34+
35+
public function getOriginalNode(): Node\PropertyHook
36+
{
37+
return $this->originalNode;
38+
}
39+
40+
public function getType(): string
41+
{
42+
return 'PHPStan_Node_InPropertyHookNode';
43+
}
44+
45+
/**
46+
* @return string[]
47+
*/
48+
public function getSubNodeNames(): array
49+
{
50+
return [];
51+
}
52+
53+
}

0 commit comments

Comments
 (0)