Skip to content

Commit 201d577

Browse files
committed
Squiz/StaticThisUsage: examine the body of static closures
As noted in the previous commit, closures can be declared as `static`, in which case they do not have access to `$this`. That case was so far not considered by this sniff. This commit changes the sniff to: 1. Also examine `T_CLOSURE` tokens. 2. Examine these when nested inside OO methods, as well as when found completely outside of OO structures. Note: in a future iteration it should be considered to get rid of the implementation via the `AbstractScopeSniff` as that implementation actually makes the sniff _more complicated_ instead of _less complicated_. Includes unit tests.
1 parent 447b0b5 commit 201d577

File tree

3 files changed

+80
-22
lines changed

3 files changed

+80
-22
lines changed

src/Standards/Squiz/Sniffs/Scope/StaticThisUsageSniff.php

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class StaticThisUsageSniff extends AbstractScopeSniff
2323
*/
2424
public function __construct()
2525
{
26-
parent::__construct([T_CLASS, T_TRAIT, T_ENUM, T_ANON_CLASS], [T_FUNCTION]);
26+
parent::__construct([T_CLASS, T_TRAIT, T_ENUM, T_ANON_CLASS], [T_FUNCTION, T_CLOSURE], true);
2727
}
2828

2929

@@ -42,22 +42,28 @@ public function processTokenWithinScope(File $phpcsFile, int $stackPtr, int $cur
4242
$tokens = $phpcsFile->getTokens();
4343

4444
// Determine if this is a function which needs to be examined.
45-
$conditions = $tokens[$stackPtr]['conditions'];
46-
end($conditions);
47-
$deepestScope = key($conditions);
48-
if ($deepestScope !== $currScope) {
49-
return;
50-
}
45+
if ($tokens[$stackPtr]['code'] === T_FUNCTION) {
46+
$conditions = $tokens[$stackPtr]['conditions'];
47+
end($conditions);
48+
$deepestScope = key($conditions);
49+
if ($deepestScope !== $currScope) {
50+
return;
51+
}
5152

52-
// Ignore abstract functions.
53-
if (isset($tokens[$stackPtr]['scope_closer']) === false) {
54-
return;
55-
}
53+
// Ignore abstract functions.
54+
if (isset($tokens[$stackPtr]['scope_closer']) === false) {
55+
return;
56+
}
5657

57-
$next = $phpcsFile->findNext(Tokens::EMPTY_TOKENS, ($stackPtr + 1), null, true);
58-
if ($next === false || $tokens[$next]['code'] !== T_STRING) {
59-
// Not a function declaration, or incomplete.
60-
return;
58+
$next = $phpcsFile->findNext(Tokens::EMPTY_TOKENS, ($stackPtr + 1), null, true);
59+
if ($next === false || $tokens[$next]['code'] !== T_STRING) {
60+
// Not a function declaration, or incomplete.
61+
return;
62+
}
63+
64+
$type = 'methods';
65+
} else {
66+
$type = 'closures';
6167
}
6268

6369
$methodProps = $phpcsFile->getMethodProperties($stackPtr);
@@ -68,7 +74,7 @@ public function processTokenWithinScope(File $phpcsFile, int $stackPtr, int $cur
6874
$next = $stackPtr;
6975
$end = $tokens[$stackPtr]['scope_closer'];
7076

71-
$this->checkThisUsage($phpcsFile, $next, $end);
77+
$this->checkThisUsage($phpcsFile, $next, $end, $type);
7278
}
7379

7480

@@ -78,10 +84,11 @@ public function processTokenWithinScope(File $phpcsFile, int $stackPtr, int $cur
7884
* @param \PHP_CodeSniffer\Files\File $phpcsFile The current file being scanned.
7985
* @param int $next The position of the next token to check.
8086
* @param int $end The position of the last token to check.
87+
* @param string $type Either 'method' or 'closure'.
8188
*
8289
* @return void
8390
*/
84-
private function checkThisUsage(File $phpcsFile, int $next, int $end)
91+
private function checkThisUsage(File $phpcsFile, int $next, int $end, string $type)
8592
{
8693
$tokens = $phpcsFile->getTokens();
8794

@@ -95,7 +102,7 @@ private function checkThisUsage(File $phpcsFile, int $next, int $end)
95102
|| $tokens[$next]['code'] === T_CLOSURE)
96103
&& isset($tokens[$next]['scope_opener']) === true
97104
) {
98-
$this->checkThisUsage($phpcsFile, $next, $tokens[$next]['scope_opener']);
105+
$this->checkThisUsage($phpcsFile, $next, $tokens[$next]['scope_opener'], $type);
99106
$next = $tokens[$next]['scope_closer'];
100107
continue;
101108
}
@@ -104,8 +111,10 @@ private function checkThisUsage(File $phpcsFile, int $next, int $end)
104111
continue;
105112
}
106113

107-
$error = 'Usage of "$this" in static methods will cause runtime errors';
108-
$phpcsFile->addError($error, $next, 'Found');
114+
$error = 'Usage of "$this" in static %s will cause runtime errors';
115+
$data = [$type];
116+
117+
$phpcsFile->addError($error, $next, 'Found', $data);
109118
} while ($next !== false);
110119
}
111120

@@ -122,5 +131,18 @@ private function checkThisUsage(File $phpcsFile, int $next, int $end)
122131
*/
123132
protected function processTokenOutsideScope(File $phpcsFile, int $stackPtr)
124133
{
134+
$tokens = $phpcsFile->getTokens();
135+
136+
if ($tokens[$stackPtr]['code'] !== T_CLOSURE) {
137+
// We're only interested in closures when looking outside of OO.
138+
return;
139+
}
140+
141+
$methodProps = $phpcsFile->getMethodProperties($stackPtr);
142+
if ($methodProps['is_static'] === false) {
143+
return;
144+
}
145+
146+
$this->checkThisUsage($phpcsFile, $stackPtr, $tokens[$stackPtr]['scope_closer'], 'closures');
125147
}
126148
}

src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.inc

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class MyClass
3737
}
3838

3939
public static function func1() {
40-
return function() {
40+
return static function() {
4141
echo $this->name;
4242
};
4343
}
@@ -137,10 +137,42 @@ class NestedScopesShouldBeSkipped
137137

138138
\Closure::bind(
139139
function ($this) { // Cannot use $this as a param, so this should still be flagged.
140-
$this->x = 1; // This is fine. $this related to the Closure class, not to the NestedScopesShouldBeSkipped class.
140+
$this->x = 1; // This is fine. $this related to the Target class, not to the NestedScopesShouldBeSkipped class.
141141
},
142142
new Target(),
143143
Target::class,
144144
)();
145+
146+
$callable = static function ($a, $b) {
147+
return $a * $b * $this->multiplier; // This should be flagged. The closure is declared as static.
148+
};
149+
}
150+
151+
public function dontExamineMe() {
152+
$this->shouldNotBeFlagged();
153+
154+
\Closure::bind(
155+
function () {
156+
$this->x = 1; // This is fine. $this related to the Target class, not to the NestedScopesShouldBeSkipped class.
157+
},
158+
new Target(),
159+
Target::class,
160+
)();
161+
162+
$callable = static function ($a, $b) {
163+
return $a * $b * $this->multiplier; // This should be flagged. The closure is declared as static.
164+
};
145165
}
146166
}
167+
168+
function ignoreMe() {
169+
$this->notAllowedButNotTheSameIssueAsThisSniffIsLookingFor();
170+
}
171+
172+
$callable = function ($a, $b) {
173+
return $a * $b * $this->multiplier; // This should NOT be flagged. The closure might be bound to an object.
174+
};
175+
176+
$callable = static function ($a, $b) {
177+
return $a * $b * $this->multiplier; // This should be flagged. The closure is declared as static.
178+
};

src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public function getErrorList()
3737
9 => 1,
3838
14 => 1,
3939
20 => 1,
40+
41 => 1,
4041
61 => 1,
4142
69 => 1,
4243
76 => 1,
@@ -47,6 +48,9 @@ public function getErrorList()
4748
132 => 1,
4849
135 => 1,
4950
139 => 1,
51+
147 => 1,
52+
163 => 1,
53+
177 => 1,
5054
];
5155
}
5256

0 commit comments

Comments
 (0)