7
7
use PHPStan \Analyser \Scope ;
8
8
use PHPStan \Reflection \ReflectionProvider ;
9
9
use PHPStan \Rules \Rule ;
10
+ use PHPStan \Rules \RuleError ;
10
11
use PHPStan \Rules \RuleErrorBuilder ;
11
12
use PHPStan \Type \ClosureType ;
12
13
use PHPStan \Type \Constant \ConstantArrayType ;
15
16
use PHPStan \Type \Generic \GenericClassStringType ;
16
17
use PHPStan \Type \IntersectionType ;
17
18
use PHPStan \Type \ObjectType ;
19
+ use PHPStan \Type \ThisType ;
18
20
use PHPStan \Type \Type ;
19
21
use PHPStan \Type \UnionType ;
20
22
use PHPStan \Type \VerbosityLevel ;
@@ -44,8 +46,9 @@ public function processNode(Node $node, Scope $scope): array
44
46
if (!$ key instanceof Node \Scalar \String_) {
45
47
return [];
46
48
}
47
- // @see https://www.drupal.org/node/2966725
48
49
50
+ // @todo this should be 3 rules.
51
+ // @see https://www.drupal.org/node/2966725
49
52
$ keysToCheck = ['#pre_render ' , '#post_render ' , '#access_callback ' , '#lazy_builder ' ];
50
53
$ keySearch = array_search ($ key ->value , $ keysToCheck , true );
51
54
if ($ keySearch === false ) {
@@ -54,121 +57,132 @@ public function processNode(Node $node, Scope $scope): array
54
57
$ keyChecked = $ keysToCheck [$ keySearch ];
55
58
56
59
$ value = $ node ->value ;
57
- if (!$ value instanceof Node \Expr \Array_) {
58
- return [
59
- RuleErrorBuilder::message (sprintf ('The "%s" render array value expects an array of callbacks. ' , $ keyChecked ))
60
- ->line ($ node ->getLine ())->build ()
61
- ];
62
- }
63
- if (count ($ value ->items ) === 0 ) {
64
- return [];
60
+
61
+ $ errors = [];
62
+
63
+ if ($ keyChecked === '#lazy_builder ' ) {
64
+ if (!$ value instanceof Node \Expr \Array_) {
65
+ return [
66
+ RuleErrorBuilder::message (sprintf ('The "%s" expects a callable array with arguments. ' , $ keyChecked ))
67
+ ->line ($ node ->getLine ())->build ()
68
+ ];
69
+ }
70
+ if (count ($ value ->items ) === 0 ) {
71
+ return [];
72
+ }
73
+ if ($ value ->items [0 ] === null ) {
74
+ return [];
75
+ }
76
+ $ errors [] = $ this ->doProcessNode ($ value ->items [0 ]->value , $ scope , $ keyChecked , 0 );
77
+ } elseif ($ keyChecked === '#access_callback ' ) {
78
+ $ errors [] = $ this ->doProcessNode ($ value , $ scope , $ keyChecked , 0 );
79
+ } else {
80
+ if (!$ value instanceof Node \Expr \Array_) {
81
+ return [
82
+ RuleErrorBuilder::message (sprintf ('The "%s" render array value expects an array of callbacks. ' , $ keyChecked ))
83
+ ->line ($ node ->getLine ())->build ()
84
+ ];
85
+ }
86
+ if (count ($ value ->items ) === 0 ) {
87
+ return [];
88
+ }
89
+ foreach ($ value ->items as $ pos => $ item ) {
90
+ if (!$ item instanceof Node \Expr \ArrayItem) {
91
+ continue ;
92
+ }
93
+ $ errors [] = $ this ->doProcessNode ($ item ->value , $ scope , $ keyChecked , $ pos );
94
+ }
65
95
}
96
+ return array_filter ($ errors );
97
+ }
66
98
99
+ private function doProcessNode (Node \Expr $ node , Scope $ scope , string $ keyChecked , int $ pos ): ?RuleError
100
+ {
67
101
$ trustedCallbackType = new UnionType ([
68
102
new ObjectType ('Drupal\Core\Security\TrustedCallbackInterface ' ),
69
103
new ObjectType ('Drupal\Core\Render\Element\RenderCallbackInterface ' ),
70
104
]);
71
- $ errors = [];
72
- foreach ($ value ->items as $ pos => $ item ) {
73
- if (!$ item instanceof Node \Expr \ArrayItem) {
74
- continue ;
75
- }
76
- // '#lazy_builder' has two items, callback and args. Others are direct callbacks.
77
- // Lazy builder in Renderer: $elements['#lazy_builder'][0], $elements['#lazy_builder'][1]
78
- if ($ keyChecked === '#lazy_builder ' ) {
79
- if (!$ item ->value instanceof Node \Expr \Array_) {
80
- $ errors [] = RuleErrorBuilder::message (
81
- sprintf ("%s callback %s at key '%s' is not callable. " , $ keyChecked , $ scope ->getType ($ item ->value )->describe (VerbosityLevel::value ()), $ pos )
82
- )->line ($ item ->value ->getLine ())->build ();
83
- continue ;
84
- }
85
105
86
- if (count ($ item ->value ->items ) !== 2 || $ item ->value ->items [0 ] === null ) {
87
- $ errors [] = RuleErrorBuilder::message (
88
- sprintf ("%s callback %s at key '%s' is not valid. First value must be a callback and second value its arguments. " , $ keyChecked , $ scope ->getType ($ item ->value )->describe (VerbosityLevel::value ()), $ pos )
89
- )->line ($ item ->value ->getLine ())->build ();
90
- continue ;
91
- }
92
- // Replace $item with our nested callback.
93
- $ item = $ item ->value ->items [0 ];
106
+ $ errorLine = $ node ->getLine ();
107
+ $ type = $ this ->getType ($ node , $ scope );
108
+
109
+ if ($ type instanceof ConstantStringType) {
110
+ if (!$ type ->isCallable ()->yes ()) {
111
+ return RuleErrorBuilder::message (
112
+ sprintf ("%s callback %s at key '%s' is not callable. " , $ keyChecked , $ type ->describe (VerbosityLevel::value ()), $ pos )
113
+ )->line ($ errorLine )->build ();
114
+ }
115
+ // We can determine if the callback is callable through the type system. However, we cannot determine
116
+ // if it is just a function or a static class call (MyClass::staticFunc).
117
+ if ($ this ->reflectionProvider ->hasFunction (new \PhpParser \Node \Name ($ type ->getValue ()), null )) {
118
+ return RuleErrorBuilder::message (
119
+ sprintf ("%s callback %s at key '%s' is not trusted. " , $ keyChecked , $ type ->describe (VerbosityLevel::value ()), $ pos )
120
+ )->line ($ errorLine )
121
+ ->tip ('Change record: https://www.drupal.org/node/2966725. ' )
122
+ ->build ();
123
+ }
124
+ } elseif ($ type instanceof ConstantArrayType) {
125
+ if (!$ type ->isCallable ()->yes ()) {
126
+ return RuleErrorBuilder::message (
127
+ sprintf ("%s callback %s at key '%s' is not callable. " , $ keyChecked , $ type ->describe (VerbosityLevel::value ()), $ pos )
128
+ )->line ($ errorLine )->build ();
129
+ }
130
+ $ typeAndMethodName = $ type ->findTypeAndMethodName ();
131
+ if ($ typeAndMethodName === null ) {
132
+ throw new \PHPStan \ShouldNotHappenException ();
94
133
}
95
- $ errorLine = $ item ->value ->getLine ();
96
- $ type = $ this ->getType ($ item ->value , $ scope );
97
134
98
- if ($ type instanceof ConstantStringType) {
99
- if (!$ type ->isCallable ()->yes ()) {
100
- $ errors [] = RuleErrorBuilder::message (
101
- sprintf ("%s callback %s at key '%s' is not callable. " , $ keyChecked , $ type ->describe (VerbosityLevel::value ()), $ pos )
102
- )->line ($ errorLine )->build ();
103
- continue ;
104
- }
105
- // We can determine if the callback is callable through the type system. However, we cannot determine
106
- // if it is just a function or a static class call (MyClass::staticFunc).
107
- if ($ this ->reflectionProvider ->hasFunction (new \PhpParser \Node \Name ($ type ->getValue ()), null )) {
108
- $ errors [] = RuleErrorBuilder::message (
109
- sprintf ("%s callback %s at key '%s' is not trusted. " , $ keyChecked , $ type ->describe (VerbosityLevel::value ()), $ pos )
110
- )->line ($ errorLine )
111
- ->tip ('Change record: https://www.drupal.org/node/2966725. ' )
112
- ->build ();
113
- }
114
- } elseif ($ type instanceof ConstantArrayType) {
115
- if (!$ type ->isCallable ()->yes ()) {
116
- $ errors [] = RuleErrorBuilder::message (
117
- sprintf ("%s callback %s at key '%s' is not callable. " , $ keyChecked , $ type ->describe (VerbosityLevel::value ()), $ pos )
118
- )->line ($ errorLine )->build ();
119
- continue ;
120
- }
121
- $ typeAndMethodName = $ type ->findTypeAndMethodName ();
122
- if ($ typeAndMethodName === null ) {
135
+ if (!$ trustedCallbackType ->isSuperTypeOf ($ typeAndMethodName ->getType ())->yes ()) {
136
+ return RuleErrorBuilder::message (
137
+ sprintf ("%s callback class '%s' at key '%s' does not implement Drupal\Core\Security\TrustedCallbackInterface. " , $ keyChecked , $ typeAndMethodName ->getType ()->describe (VerbosityLevel::value ()), $ pos )
138
+ )->line ($ errorLine )->tip ('Change record: https://www.drupal.org/node/2966725. ' )->build ();
139
+ }
140
+ } elseif ($ type instanceof ClosureType) {
141
+ if ($ scope ->isInClass ()) {
142
+ $ classReflection = $ scope ->getClassReflection ();
143
+ if ($ classReflection === null ) {
123
144
throw new \PHPStan \ShouldNotHappenException ();
124
145
}
125
-
126
- if (!$ trustedCallbackType ->isSuperTypeOf ($ typeAndMethodName ->getType ())->yes ()) {
127
- $ errors [] = RuleErrorBuilder::message (
128
- sprintf ("%s callback class '%s' at key '%s' does not implement Drupal\Core\Security\TrustedCallbackInterface. " , $ keyChecked , $ typeAndMethodName ->getType ()->describe (VerbosityLevel::value ()), $ pos )
129
- )->line ($ errorLine )->tip ('Change record: https://www.drupal.org/node/2966725. ' )->build ();
130
- }
131
- } elseif ($ type instanceof ClosureType) {
132
- if ($ scope ->isInClass ()) {
133
- $ classReflection = $ scope ->getClassReflection ();
134
- if ($ classReflection === null ) {
135
- throw new \PHPStan \ShouldNotHappenException ();
136
- }
137
- $ classType = new ObjectType ($ classReflection ->getName ());
138
- $ formType = new ObjectType ('\Drupal\Core\Form\FormInterface ' );
139
- if ($ formType ->isSuperTypeOf ($ classType )->yes ()) {
140
- $ errors [] = RuleErrorBuilder::message (
141
- sprintf ("%s may not contain a closure at key '%s' as forms may be serialized and serialization of closures is not allowed. " , $ keyChecked , $ pos )
142
- )->line ($ errorLine )->build ();
143
- }
144
- }
145
- } elseif ($ type instanceof IntersectionType) {
146
- // Try to provide a tip for this weird occurrence.
147
- $ tip = '' ;
148
- if ($ item ->value instanceof Node \Expr \BinaryOp \Concat) {
149
- $ leftStringType = $ scope ->getType ($ item ->value ->left )->toString ();
150
- $ rightStringType = $ scope ->getType ($ item ->value ->right )->toString ();
151
- if ($ leftStringType instanceof GenericClassStringType && $ rightStringType instanceof ConstantStringType) {
152
- $ methodName = str_replace (': ' , '' , $ rightStringType ->getValue ());
153
- $ tip = "Refactor concatenation of `static::class` with method name to an array callback: [static::class, ' $ methodName'] " ;
154
- }
146
+ $ classType = new ObjectType ($ classReflection ->getName ());
147
+ $ formType = new ObjectType ('\Drupal\Core\Form\FormInterface ' );
148
+ if ($ formType ->isSuperTypeOf ($ classType )->yes ()) {
149
+ return RuleErrorBuilder::message (
150
+ sprintf ("%s may not contain a closure at key '%s' as forms may be serialized and serialization of closures is not allowed. " , $ keyChecked , $ pos )
151
+ )->line ($ errorLine )->build ();
155
152
}
156
-
157
- if ($ tip === '' ) {
158
- $ tip = 'If this error is unexpected, open an issue with the error and sample code https://github.com/mglaman/phpstan-drupal/issues/new ' ;
153
+ }
154
+ } elseif ($ type instanceof ThisType) {
155
+ if (!$ type ->isCallable ()->yes ()) {
156
+ return RuleErrorBuilder::message (
157
+ sprintf ("%s callback %s at key '%s' is not callable. " , $ keyChecked , $ type ->describe (VerbosityLevel::value ()), $ pos )
158
+ )->line ($ errorLine )->build ();
159
+ }
160
+ } elseif ($ type instanceof IntersectionType) {
161
+ // Try to provide a tip for this weird occurrence.
162
+ $ tip = '' ;
163
+ if ($ node instanceof Node \Expr \BinaryOp \Concat) {
164
+ $ leftStringType = $ scope ->getType ($ node ->left )->toString ();
165
+ $ rightStringType = $ scope ->getType ($ node ->right )->toString ();
166
+ if ($ leftStringType instanceof GenericClassStringType && $ rightStringType instanceof ConstantStringType) {
167
+ $ methodName = str_replace (': ' , '' , $ rightStringType ->getValue ());
168
+ $ tip = "Refactor concatenation of `static::class` with method name to an array callback: [static::class, ' $ methodName'] " ;
159
169
}
170
+ }
160
171
161
- $ errors [] = RuleErrorBuilder::message (
162
- sprintf ("%s value '%s' at key '%s' is invalid. " , $ keyChecked , $ type ->describe (VerbosityLevel::value ()), $ pos )
163
- )->line ($ errorLine )->tip ($ tip )->build ();
164
- } else {
165
- $ errors [] = RuleErrorBuilder::message (
166
- sprintf ("%s value '%s' at key '%s' is invalid. " , $ keyChecked , $ type ->describe (VerbosityLevel::value ()), $ pos )
167
- )->line ($ errorLine )->build ();
172
+ if ($ tip === '' ) {
173
+ $ tip = 'If this error is unexpected, open an issue with the error and sample code https://github.com/mglaman/phpstan-drupal/issues/new ' ;
168
174
}
175
+
176
+ return RuleErrorBuilder::message (
177
+ sprintf ("%s value '%s' at key '%s' is invalid. " , $ keyChecked , $ type ->describe (VerbosityLevel::value ()), $ pos )
178
+ )->line ($ errorLine )->tip ($ tip )->build ();
179
+ } else {
180
+ return RuleErrorBuilder::message (
181
+ sprintf ("%s value '%s' at key '%s' is invalid. " , $ keyChecked , $ type ->describe (VerbosityLevel::value ()), $ pos )
182
+ )->line ($ errorLine )->build ();
169
183
}
170
184
171
- return $ errors ;
185
+ return null ;
172
186
}
173
187
174
188
private function getType (Node \Expr $ node , Scope $ scope ): Type
0 commit comments