1010use PhpParser \Node ;
1111use PhpParser \Node \Arg ;
1212use PhpParser \Node \AttributeGroup ;
13+ use PhpParser \Node \ComplexType ;
1314use PhpParser \Node \Expr ;
1415use PhpParser \Node \Expr \Array_ ;
1516use PhpParser \Node \Expr \ArrayDimFetch ;
3536use PhpParser \Node \Expr \StaticPropertyFetch ;
3637use PhpParser \Node \Expr \Ternary ;
3738use PhpParser \Node \Expr \Variable ;
39+ use PhpParser \Node \Identifier ;
3840use PhpParser \Node \Name ;
3941use PhpParser \Node \Stmt \Break_ ;
4042use PhpParser \Node \Stmt \Class_ ;
98100use PHPStan \Node \InClosureNode ;
99101use PHPStan \Node \InForeachNode ;
100102use PHPStan \Node \InFunctionNode ;
103+ use PHPStan \Node \InPropertyHookNode ;
101104use PHPStan \Node \InstantiationCallableNode ;
102105use PHPStan \Node \InTraitNode ;
103106use 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
0 commit comments