33namespace PHPStan \Rules \Methods ;
44
55use PhpParser \Node ;
6- use PhpParser \ NodeFinder ;
6+ use PHPStan \ Analyser \ RuleErrorTransformer ;
77use PHPStan \Analyser \Scope ;
88use PHPStan \Internal \SprintfHelper ;
9- use PHPStan \Node \VariableWritesNode ;
10- use PHPStan \Parser \ Parser ;
11- use PHPStan \Reflection \ ClassReflection ;
9+ use PHPStan \Node \ClassMethodsNode ;
10+ use PHPStan \Node \ Method \ MethodVariableWrites ;
11+ use PHPStan \Rules \ IdentifierRuleError ;
1212use PHPStan \Rules \Rule ;
13+ use PHPStan \Rules \RuleErrors \TransformedRuleError ;
1314use PHPStan \Rules \UnusedParametersCheck ;
1415use function count ;
1516use function sprintf ;
16- use function str_ends_with ;
1717use function str_starts_with ;
18- use function strlen ;
1918use function strtolower ;
2019
2120/**
2221 * Reports unused parameters of private methods: a private method has no
2322 * callers outside the class, so its signature is not a contract - unlike a
2423 * public or protected one, whose parameters may be dictated by an interface,
25- * a parent or an override. A private method referenced as a callable
26- * (first-class callable syntax, a callable array or a callable string) escapes
27- * the class, so it is skipped as well.
24+ * a parent or an override. A private method the class hands out as a callable
25+ * (first-class callable syntax or a callable array) escapes the class, so it
26+ * is skipped as well.
2827 *
29- * @implements Rule<VariableWritesNode>
28+ * Errors are reported with the scope inside the method, as if the rule
29+ * listened to the method's VariableWritesNode, so a method declared in a
30+ * trait is reported in the trait's file.
31+ *
32+ * @implements Rule<ClassMethodsNode>
3033 */
3134final class UnusedMethodParametersRule implements Rule
3235{
3336
3437 public function __construct (
3538 private UnusedParametersCheck $ check ,
36- private Parser $ parser ,
39+ private RuleErrorTransformer $ ruleErrorTransformer ,
3740 )
3841 {
3942 }
4043
4144 public function getNodeType (): string
4245 {
43- return VariableWritesNode ::class;
46+ return ClassMethodsNode ::class;
4447 }
4548
4649 public function processNode (Node $ node , Scope $ scope ): array
4750 {
48- $ originalNode = $ node ->getFunctionLike ();
51+ $ referencedAsCallable = null ;
52+ $ errors = [];
53+ foreach ($ node ->getMethodVariableWrites () as $ methodVariableWrites ) {
54+ $ methodErrors = $ this ->getMethodErrors ($ methodVariableWrites );
55+ if (count ($ methodErrors ) === 0 ) {
56+ continue ;
57+ }
58+
59+ $ referencedAsCallable ??= $ this ->getMethodsReferencedAsCallable ($ node );
60+ if ($ referencedAsCallable === true ) {
61+ return [];
62+ }
63+
64+ $ methodNode = $ methodVariableWrites ->getNode ()->getFunctionLike ();
65+ if (!$ methodNode instanceof Node \Stmt \ClassMethod) {
66+ continue ;
67+ }
68+ if (isset ($ referencedAsCallable [$ methodNode ->name ->toLowerString ()])) {
69+ continue ;
70+ }
71+
72+ foreach ($ methodErrors as $ methodError ) {
73+ $ errors [] = new TransformedRuleError ($ this ->ruleErrorTransformer ->transform (
74+ $ methodError ,
75+ $ methodVariableWrites ->getScope (),
76+ [],
77+ $ methodVariableWrites ->getNode (),
78+ ));
79+ }
80+ }
81+
82+ return $ errors ;
83+ }
84+
85+ /**
86+ * @return list<IdentifierRuleError>
87+ */
88+ private function getMethodErrors (MethodVariableWrites $ methodVariableWrites ): array
89+ {
90+ $ variableWritesNode = $ methodVariableWrites ->getNode ();
91+ $ scope = $ methodVariableWrites ->getScope ();
92+ $ originalNode = $ variableWritesNode ->getFunctionLike ();
4993 if (!$ originalNode instanceof Node \Stmt \ClassMethod) {
5094 return [];
5195 }
@@ -74,8 +118,8 @@ public function processNode(Node $node, Scope $scope): array
74118 SprintfHelper ::escapeFormatString ($ originalNode ->name ->toString ()),
75119 );
76120
77- $ errors = $ this ->check ->getUnusedParameterErrors (
78- $ node ,
121+ return $ this ->check ->getUnusedParameterErrors (
122+ $ variableWritesNode ,
79123 $ method ,
80124 $ originalNode ->params ,
81125 sprintf ('Method %s has an unused parameter $%%s. ' , $ methodDescription ),
@@ -84,65 +128,55 @@ public function processNode(Node $node, Scope $scope): array
84128 sprintf ('Method %s has a parameter $%%s that only flows into values that are never used. ' , $ methodDescription ),
85129 'method.unusedParameterFlow ' ,
86130 );
87- if (count ($ errors ) === 0 ) {
88- return [];
89- }
90-
91- $ methodName = $ originalNode ->name ->toString ();
92- if ($ this ->isReferencedAsCallable ($ scope ->getClassReflection (), $ methodName )) {
93- return [];
94- }
95- $ traitReflection = $ scope ->getTraitReflection ();
96- if ($ traitReflection !== null && $ this ->isReferencedAsCallable ($ traitReflection , $ methodName )) {
97- return [];
98- }
99-
100- return $ errors ;
101131 }
102132
103- private function isReferencedAsCallable (ClassReflection $ classReflection , string $ methodName ): bool
133+ /**
134+ * Lowercased names of the methods the class references as callables.
135+ * True when a first-class callable's method name cannot be resolved, so
136+ * any method may be referenced.
137+ *
138+ * @return array<string, true>|true
139+ */
140+ private function getMethodsReferencedAsCallable (ClassMethodsNode $ node ): array |bool
104141 {
105- $ fileName = $ classReflection ->getFileName ();
106- if ($ fileName === null ) {
107- return false ;
108- }
109-
110- $ startLine = $ classReflection ->getNativeReflection ()->getStartLine ();
111- $ nodeFinder = new NodeFinder ();
112- $ classNodes = $ nodeFinder ->find (
113- $ this ->parser ->parseFile ($ fileName ),
114- static fn (Node $ node ): bool => $ node instanceof Node \Stmt \ClassLike && $ node ->getStartLine () === $ startLine ,
115- );
116-
117- $ lowerMethodName = strtolower ($ methodName );
118- foreach ($ classNodes as $ classNode ) {
119- $ reference = $ nodeFinder ->findFirst ($ classNode , static function (Node $ node ) use ($ lowerMethodName ): bool {
120- if ($ node instanceof Node \Expr \MethodCall || $ node instanceof Node \Expr \StaticCall) {
121- return $ node ->isFirstClassCallable ()
122- && $ node ->name instanceof Node \Identifier
123- && $ node ->name ->toLowerString () === $ lowerMethodName ;
142+ $ methodNames = [];
143+ foreach ($ node ->getMethodCalls () as $ methodCall ) {
144+ $ callNode = $ methodCall ->getNode ();
145+ $ callScope = $ methodCall ->getScope ();
146+ if ($ callNode instanceof Node \Expr \Array_) {
147+ $ arrayType = $ callScope ->getType ($ callNode );
148+ if ($ arrayType ->isCallable ()->no ()) {
149+ continue ;
124150 }
125-
126- if ($ node instanceof Node \Expr \Array_) {
127- return count ($ node ->items ) === 2
128- && $ node ->items [1 ]->value instanceof Node \Scalar \String_
129- && strtolower ($ node ->items [1 ]->value ->value ) === $ lowerMethodName ;
151+ foreach ($ arrayType ->getConstantArrays () as $ constantArray ) {
152+ foreach ($ constantArray ->findTypeAndMethodNames () as $ typeAndMethod ) {
153+ if ($ typeAndMethod ->isUnknown ()) {
154+ continue ;
155+ }
156+ $ methodNames [strtolower ($ typeAndMethod ->getMethod ())] = true ;
157+ }
130158 }
159+ continue ;
160+ }
131161
132- if ($ node instanceof Node \Scalar \String_) {
133- $ value = strtolower ($ node ->value );
134- return strlen ($ value ) > strlen ($ lowerMethodName ) + 2
135- && str_ends_with ($ value , ':: ' . $ lowerMethodName );
136- }
162+ if (!$ callNode ->isFirstClassCallable ()) {
163+ continue ;
164+ }
165+ if ($ callNode ->name instanceof Node \Identifier) {
166+ $ methodNames [$ callNode ->name ->toLowerString ()] = true ;
167+ continue ;
168+ }
137169
138- return false ;
139- });
140- if ($ reference !== null ) {
170+ $ constantStrings = $ callScope ->getType ($ callNode ->name )->getConstantStrings ();
171+ if (count ($ constantStrings ) === 0 ) {
141172 return true ;
142173 }
174+ foreach ($ constantStrings as $ constantString ) {
175+ $ methodNames [strtolower ($ constantString ->getValue ())] = true ;
176+ }
143177 }
144178
145- return false ;
179+ return $ methodNames ;
146180 }
147181
148182}
0 commit comments