1616use PHPStan \Rules \Functions \CallToFunctionStatementWithoutSideEffectsRule ;
1717use PHPStan \Rules \IdentifierRuleError ;
1818use PHPStan \Rules \RuleErrorBuilder ;
19+ use PHPStan \TrinaryLogic ;
1920use PHPStan \Type \Type ;
2021use function array_filter ;
2122use function array_key_exists ;
@@ -51,18 +52,111 @@ public function check(
5152 bool $ isConstructor ,
5253 ): array
5354 {
54- $ errors = [] ;
55+ $ errors = $ this -> checkSignature ( $ scope , $ functionDescription , $ identifier , $ functionReflection , $ parameters , $ returnType , $ isConstructor ) ;
5556 $ isPure = $ functionReflection ->isPure ();
57+ $ pureUnlessCallableParamNames = $ this ->collectFlaggedParameterNames ($ parameters , $ functionReflection ->getPureUnlessCallableIsImpureParameters ());
58+ $ pureUnlessParameterPassedParamNames = $ this ->collectFlaggedParameterNames ($ parameters , $ functionReflection ->getPureUnlessParameterPassedParameters ());
59+
60+ if ($ isPure ->yes ()) {
61+ $ errors = array_merge ($ errors , $ this ->reportImpurePoints ($ impurePoints , $ pureUnlessCallableParamNames , $ functionDescription ));
62+ } elseif ($ pureUnlessCallableParamNames !== [] || $ pureUnlessParameterPassedParamNames !== []) {
63+ // A function declared @pure-unless-callable-is-impure is pure except
64+ // for the flagged callables, so its body is checked for purity while
65+ // the flagged callables' own invocations are exempt. The same holds for
66+ // @pure-unless-parameter-passed: writing through the flagged by-ref
67+ // parameter is not an impure point, so the rest of the body still has
68+ // to be pure for the conditional verdict to hold.
69+ $ errors = array_merge ($ errors , $ this ->reportImpurePoints ($ impurePoints , $ pureUnlessCallableParamNames , $ functionDescription ));
70+ } elseif ($ isPure ->no ()) {
71+ if (
72+ count ($ throwPoints ) === 0
73+ && count ($ impurePoints ) === 0
74+ && count ($ functionReflection ->getAsserts ()->getAll ()) === 0
75+ && (
76+ !$ functionReflection instanceof ExtendedMethodReflection
77+ || $ functionReflection ->isFinal ()->yes ()
78+ || $ functionReflection ->getDeclaringClass ()->isFinal ()
79+ )
80+ ) {
81+ $ errors [] = RuleErrorBuilder ::message (sprintf (
82+ '%s is marked as impure but does not have any side effects. ' ,
83+ $ functionDescription ,
84+ ))->identifier (sprintf ('impure%s.pure ' , $ identifier ))->build ();
85+ }
86+ } elseif ($ returnType ->isVoid ()->yes ()) {
87+ if (
88+ count ($ throwPoints ) === 0
89+ && count ($ impurePoints ) === 0
90+ && !$ isConstructor
91+ && (!$ functionReflection instanceof ExtendedMethodReflection || $ functionReflection ->isPrivate ())
92+ && count ($ functionReflection ->getAsserts ()->getAll ()) === 0
93+ ) {
94+ $ hasByRef = false ;
95+ foreach ($ parameters as $ parameter ) {
96+ if (!$ parameter ->passedByReference ()->createsNewVariable ()) {
97+ continue ;
98+ }
99+
100+ $ hasByRef = true ;
101+ break ;
102+ }
103+
104+ $ statements = array_filter ($ statements , static function (Stmt $ stmt ): bool {
105+ if ($ stmt instanceof Stmt \Nop) {
106+ return false ;
107+ }
108+
109+ if (!$ stmt instanceof Stmt \Expression) {
110+ return true ;
111+ }
112+ if (!$ stmt ->expr instanceof FuncCall) {
113+ return true ;
114+ }
115+ if (!$ stmt ->expr ->name instanceof Name) {
116+ return true ;
117+ }
118+
119+ return !in_array ($ stmt ->expr ->name ->toString (), CallToFunctionStatementWithoutSideEffectsRule::PHPSTAN_TESTING_FUNCTIONS , true );
120+ });
121+
122+ if (!$ hasByRef && count ($ statements ) > 0 ) {
123+ $ errors [] = RuleErrorBuilder ::message (sprintf (
124+ '%s returns void but does not have any side effects. ' ,
125+ $ functionDescription ,
126+ ))->identifier ('void.pure ' )->build ();
127+ }
128+ }
129+ }
130+
131+ return $ errors ;
132+ }
133+
134+ /**
135+ * The checks of a purity declaration that need only the signature, so they
136+ * also apply to methods without a body.
137+ *
138+ * @param 'Function'|'Method'|'PropertyHook' $identifier
139+ * @param ExtendedParameterReflection[] $parameters
140+ * @return list<IdentifierRuleError>
141+ */
142+ public function checkSignature (
143+ Scope $ scope ,
144+ string $ functionDescription ,
145+ string $ identifier ,
146+ FunctionReflection |ExtendedMethodReflection $ functionReflection ,
147+ array $ parameters ,
148+ Type $ returnType ,
149+ bool $ isConstructor ,
150+ ): array
151+ {
152+ $ errors = [];
56153
57154 $ pureUnlessCallableParameters = $ functionReflection ->getPureUnlessCallableIsImpureParameters ();
58- $ pureUnlessCallableParamNames = [];
59155 foreach ($ parameters as $ parameter ) {
60156 if (!array_key_exists ($ parameter ->getName (), $ pureUnlessCallableParameters )) {
61157 continue ;
62158 }
63159
64- $ pureUnlessCallableParamNames [$ parameter ->getName ()] = true ;
65-
66160 $ acceptors = $ parameter ->getType ()->getCallableParametersAcceptors ($ scope );
67161 if (count ($ acceptors ) === 0 ) {
68162 continue ;
@@ -92,14 +186,11 @@ public function check(
92186 }
93187
94188 $ pureUnlessParameterPassedParameters = $ functionReflection ->getPureUnlessParameterPassedParameters ();
95- $ pureUnlessParameterPassedParamNames = [];
96189 foreach ($ parameters as $ parameter ) {
97190 if (!array_key_exists ($ parameter ->getName (), $ pureUnlessParameterPassedParameters )) {
98191 continue ;
99192 }
100193
101- $ pureUnlessParameterPassedParamNames [$ parameter ->getName ()] = true ;
102-
103194 if (!$ parameter ->isOptional ()) {
104195 $ errors [] = RuleErrorBuilder ::message (sprintf (
105196 '%s is marked @pure-unless-parameter-passed for parameter $%s, but $%s is not optional, so %s is never pure. ' ,
@@ -127,7 +218,7 @@ public function check(
127218 ))->identifier (sprintf ('pure%s.parameterPassedNotByRef ' , $ identifier ))->build ();
128219 }
129220
130- if ($ isPure ->yes ()) {
221+ if ($ functionReflection -> isPure () ->yes ()) {
131222 foreach ($ parameters as $ parameter ) {
132223 if (!$ parameter ->passedByReference ()->createsNewVariable ()) {
133224 continue ;
@@ -152,78 +243,28 @@ public function check(
152243 $ functionDescription ,
153244 ))->identifier (sprintf ('pure%s.void ' , $ identifier ))->build ();
154245 }
246+ }
155247
156- $ errors = array_merge ($ errors , $ this ->reportImpurePoints ($ impurePoints , $ pureUnlessCallableParamNames , $ functionDescription ));
157- } elseif ($ pureUnlessCallableParamNames !== [] || $ pureUnlessParameterPassedParamNames !== []) {
158- // A function declared @pure-unless-callable-is-impure is pure except
159- // for the flagged callables, so its body is checked for purity while
160- // the flagged callables' own invocations are exempt. The same holds for
161- // @pure-unless-parameter-passed: writing through the flagged by-ref
162- // parameter is not an impure point, so the rest of the body still has
163- // to be pure for the conditional verdict to hold.
164- $ errors = array_merge ($ errors , $ this ->reportImpurePoints ($ impurePoints , $ pureUnlessCallableParamNames , $ functionDescription ));
165- } elseif ($ isPure ->no ()) {
166- if (
167- count ($ throwPoints ) === 0
168- && count ($ impurePoints ) === 0
169- && count ($ functionReflection ->getAsserts ()->getAll ()) === 0
170- && (
171- !$ functionReflection instanceof ExtendedMethodReflection
172- || $ functionReflection ->isFinal ()->yes ()
173- || $ functionReflection ->getDeclaringClass ()->isFinal ()
174- )
175- ) {
176- $ errors [] = RuleErrorBuilder ::message (sprintf (
177- '%s is marked as impure but does not have any side effects. ' ,
178- $ functionDescription ,
179- ))->identifier (sprintf ('impure%s.pure ' , $ identifier ))->build ();
180- }
181- } elseif ($ returnType ->isVoid ()->yes ()) {
182- if (
183- count ($ throwPoints ) === 0
184- && count ($ impurePoints ) === 0
185- && !$ isConstructor
186- && (!$ functionReflection instanceof ExtendedMethodReflection || $ functionReflection ->isPrivate ())
187- && count ($ functionReflection ->getAsserts ()->getAll ()) === 0
188- ) {
189- $ hasByRef = false ;
190- foreach ($ parameters as $ parameter ) {
191- if (!$ parameter ->passedByReference ()->createsNewVariable ()) {
192- continue ;
193- }
194-
195- $ hasByRef = true ;
196- break ;
197- }
198-
199- $ statements = array_filter ($ statements , static function (Stmt $ stmt ): bool {
200- if ($ stmt instanceof Stmt \Nop) {
201- return false ;
202- }
203-
204- if (!$ stmt instanceof Stmt \Expression) {
205- return true ;
206- }
207- if (!$ stmt ->expr instanceof FuncCall) {
208- return true ;
209- }
210- if (!$ stmt ->expr ->name instanceof Name) {
211- return true ;
212- }
213-
214- return !in_array ($ stmt ->expr ->name ->toString (), CallToFunctionStatementWithoutSideEffectsRule::PHPSTAN_TESTING_FUNCTIONS , true );
215- });
248+ return $ errors ;
249+ }
216250
217- if (!$ hasByRef && count ($ statements ) > 0 ) {
218- $ errors [] = RuleErrorBuilder ::message (sprintf (
219- '%s returns void but does not have any side effects. ' ,
220- $ functionDescription ,
221- ))->identifier ('void.pure ' )->build ();
222- }
251+ /**
252+ * @param ExtendedParameterReflection[] $parameters
253+ * @param array<string, TrinaryLogic> $flaggedParameters
254+ * @return array<string, true>
255+ */
256+ private function collectFlaggedParameterNames (array $ parameters , array $ flaggedParameters ): array
257+ {
258+ $ names = [];
259+ foreach ($ parameters as $ parameter ) {
260+ if (!array_key_exists ($ parameter ->getName (), $ flaggedParameters )) {
261+ continue ;
223262 }
263+
264+ $ names [$ parameter ->getName ()] = true ;
224265 }
225266
226- return $ errors ;
267+ return $ names ;
227268 }
228269
229270 /**
0 commit comments