44
55use PhpParser \Node ;
66use PhpParser \Node \Expr ;
7+ use PHPStan \Analyser \MutatingScope ;
78use PHPStan \Analyser \NameScope ;
89use PHPStan \Analyser \Scope ;
910use PHPStan \DependencyInjection \AutowiredParameter ;
@@ -46,17 +47,22 @@ public function __construct(
4647 private bool $ checkTypeAgainstPhpDocType ,
4748 #[AutowiredParameter(ref: '%reportAnyTypeWideningInVarTag% ' )]
4849 private bool $ strictWideningCheck ,
50+ #[AutowiredParameter(ref: '%reportRedundantVarTag% ' )]
51+ private bool $ reportRedundantVarTag ,
4952 private InitializerExprTypeResolver $ initializerExprTypeResolver ,
5053 )
5154 {
5255 }
5356
5457 /**
58+ * $canBeRedundant is false where an equal type does not make the tag
59+ * redundant, e.g. the default of a static variable.
60+ *
5561 * @param VarTag[] $varTags
5662 * @param string[] $assignedVariables
5763 * @return list<IdentifierRuleError>
5864 */
59- public function checkVarType (Scope $ scope , Node \Expr $ var , Node \Expr $ expr , array $ varTags , array $ assignedVariables ): array
65+ public function checkVarType (Scope $ scope , Node \Expr $ var , Node \Expr $ expr , array $ varTags , array $ assignedVariables, bool $ canBeRedundant ): array
6066 {
6167 $ errors = [];
6268
@@ -69,7 +75,7 @@ public function checkVarType(Scope $scope, Node\Expr $var, Node\Expr $expr, arra
6975 return [];
7076 }
7177
72- return $ this ->checkExprType ($ scope , $ expr , $ varTagType );
78+ return $ this ->checkExprType ($ scope , $ expr , $ varTagType, $ canBeRedundant );
7379 } elseif ($ var instanceof Expr \List_ || $ var instanceof Expr \Array_) {
7480 foreach ($ var ->items as $ i => $ arrayItem ) {
7581 if ($ arrayItem === null ) {
@@ -87,7 +93,7 @@ public function checkVarType(Scope $scope, Node\Expr $var, Node\Expr $expr, arra
8793 $ itemErrors = $ this ->checkVarType ($ scope , $ arrayItem ->value , new NativeTypeExpr (
8894 $ scope ->getType ($ expr )->getOffsetValueType ($ scope ->getType ($ dimExpr )),
8995 $ scope ->getNativeType ($ expr )->getOffsetValueType ($ scope ->getNativeType ($ dimExpr )),
90- ), $ varTags , $ assignedVariables );
96+ ), $ varTags , $ assignedVariables, $ canBeRedundant );
9197 foreach ($ itemErrors as $ error ) {
9298 $ errors [] = $ error ;
9399 }
@@ -100,7 +106,7 @@ public function checkVarType(Scope $scope, Node\Expr $var, Node\Expr $expr, arra
100106 /**
101107 * @return list<IdentifierRuleError>
102108 */
103- public function checkExprType (Scope $ scope , Node \Expr $ expr , Type $ varTagType ): array
109+ public function checkExprType (Scope $ scope , Node \Expr $ expr , Type $ varTagType, bool $ canBeRedundant ): array
104110 {
105111 $ errors = [];
106112 // a constant expression (a static variable's `= 1` default, a literal) is
@@ -143,9 +149,63 @@ public function checkExprType(Scope $scope, Node\Expr $expr, Type $varTagType):
143149 }
144150 }
145151
152+ if (
153+ $ canBeRedundant
154+ && $ this ->reportRedundantVarTag
155+ && count ($ errors ) === 0
156+ // a trait is analysed once per class using it, and the tag may be
157+ // redundant in only some of them
158+ && !$ scope ->isInTrait ()
159+ && !$ this ->hasResolvedTemplateArguments ($ scope )
160+ ) {
161+ $ exprType = $ constantType ?? $ scope ->getScopeType ($ expr );
162+ if (
163+ $ exprType ->equals ($ varTagType )
164+ // equals() ignores whether mixed is explicit, but a tag spelling out
165+ // mixed makes the implicit mixed of the expression explicit
166+ && !($ this ->containsMixed ($ exprType , false ) && $ this ->containsMixed ($ varTagType , true ))
167+ ) {
168+ $ errors [] = RuleErrorBuilder ::message (sprintf (
169+ 'PHPDoc tag @var with type %s is redundant, the expression already has this type. ' ,
170+ $ varTagType ->describe (VerbosityLevel ::getRecommendedLevelByType ($ varTagType )),
171+ ))->identifier ('varTag.redundant ' )->build ();
172+ }
173+ }
174+
146175 return $ errors ;
147176 }
148177
178+ /**
179+ * Whether the statement is walked with template arguments resolved from
180+ * their usages. A @var tag is one of those usages, so the expression may
181+ * have the tag's type only because of the tag.
182+ */
183+ private function hasResolvedTemplateArguments (Scope $ scope ): bool
184+ {
185+ if (!$ scope instanceof MutatingScope) {
186+ return false ;
187+ }
188+
189+ $ frame = $ scope ->getCurrentTemplateArgumentFrame ();
190+
191+ return $ frame !== null && !$ frame ->isObserving ();
192+ }
193+
194+ private function containsMixed (Type $ type , bool $ explicit ): bool
195+ {
196+ $ containsMixed = false ;
197+ TypeTraverser ::map ($ type , static function (Type $ type , callable $ traverse ) use ($ explicit , &$ containsMixed ): Type {
198+ if ($ type instanceof MixedType && $ type ->isExplicitMixed () === $ explicit ) {
199+ $ containsMixed = true ;
200+ return $ type ;
201+ }
202+
203+ return $ traverse ($ type );
204+ });
205+
206+ return $ containsMixed ;
207+ }
208+
149209 private function containsPhpStanType (Type $ type ): bool
150210 {
151211 $ classReflections = TypeUtils ::toBenevolentUnion ($ type )->getObjectClassReflections ();
0 commit comments