Skip to content

Commit d29643b

Browse files
authored
Merge pull request #1377 from PHPCSStandards/feature/1357/squiz-staticthisusage-fix-false-positive-nested-closure
Squiz/StaticThisUsage: improve handling of anonymous functions
2 parents ce8f782 + 2da3413 commit d29643b

3 files changed

Lines changed: 107 additions & 25 deletions

File tree

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

Lines changed: 47 additions & 22 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 = 'method';
65+
} else {
66+
$type = 'closure';
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,21 +84,25 @@ 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 Type of context being checked. 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

8895
do {
89-
$next = $phpcsFile->findNext([T_VARIABLE, T_ANON_CLASS], ($next + 1), $end);
96+
$next = $phpcsFile->findNext([T_VARIABLE, T_CLOSURE, T_ANON_CLASS], ($next + 1), $end);
9097
if ($next === false) {
9198
continue;
9299
}
93100

94-
if ($tokens[$next]['code'] === T_ANON_CLASS) {
95-
$this->checkThisUsage($phpcsFile, $next, $tokens[$next]['scope_opener']);
101+
if (($tokens[$next]['code'] === T_ANON_CLASS
102+
|| $tokens[$next]['code'] === T_CLOSURE)
103+
&& isset($tokens[$next]['scope_opener']) === true
104+
) {
105+
$this->checkThisUsage($phpcsFile, $next, $tokens[$next]['scope_opener'], $type);
96106
$next = $tokens[$next]['scope_closer'];
97107
continue;
98108
}
@@ -101,8 +111,10 @@ private function checkThisUsage(File $phpcsFile, int $next, int $end)
101111
continue;
102112
}
103113

104-
$error = 'Usage of "$this" in static methods will cause runtime errors';
105-
$phpcsFile->addError($error, $next, 'Found');
114+
$error = 'Usage of "$this" in a static %s will cause runtime errors';
115+
$data = [$type];
116+
117+
$phpcsFile->addError($error, $next, 'Found', $data);
106118
} while ($next !== false);
107119
}
108120

@@ -119,5 +131,18 @@ private function checkThisUsage(File $phpcsFile, int $next, int $end)
119131
*/
120132
protected function processTokenOutsideScope(File $phpcsFile, int $stackPtr)
121133
{
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'], 'closure');
122147
}
123148
}

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

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php
2-
class MyClass
3-
{
2+
abstract class MyClass {
3+
abstract public static function ignoreMe($a, $b);
44
public static function func1()
55
{
66
$value = 'hello';
@@ -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
}
@@ -125,3 +125,54 @@ enum MyEnum {
125125
$this->doSomething();
126126
}
127127
}
128+
129+
class NestedScopesShouldBeSkipped
130+
{
131+
public static function examineMe() {
132+
$this->shouldBeFlagged();
133+
134+
function nestedFunction() {
135+
$this->nestedFunctionsDoNotHaveAccessToThis();
136+
}
137+
138+
\Closure::bind(
139+
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 Target class, not to the NestedScopesShouldBeSkipped class.
141+
},
142+
new Target(),
143+
Target::class,
144+
)();
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+
};
165+
}
166+
}
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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ public function getErrorList()
4545
84 => 1,
4646
99 => 1,
4747
125 => 1,
48+
132 => 1,
49+
135 => 1,
50+
139 => 1,
51+
147 => 1,
52+
163 => 1,
53+
177 => 1,
4854
];
4955
}
5056

0 commit comments

Comments
 (0)