Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 47 additions & 22 deletions src/Standards/Squiz/Sniffs/Scope/StaticThisUsageSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class StaticThisUsageSniff extends AbstractScopeSniff
*/
public function __construct()
{
parent::__construct([T_CLASS, T_TRAIT, T_ENUM, T_ANON_CLASS], [T_FUNCTION]);
parent::__construct([T_CLASS, T_TRAIT, T_ENUM, T_ANON_CLASS], [T_FUNCTION, T_CLOSURE], true);
}


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

// Determine if this is a function which needs to be examined.
$conditions = $tokens[$stackPtr]['conditions'];
end($conditions);
$deepestScope = key($conditions);
if ($deepestScope !== $currScope) {
return;
}
if ($tokens[$stackPtr]['code'] === T_FUNCTION) {
$conditions = $tokens[$stackPtr]['conditions'];
end($conditions);
$deepestScope = key($conditions);
if ($deepestScope !== $currScope) {
return;
}

// Ignore abstract functions.
if (isset($tokens[$stackPtr]['scope_closer']) === false) {
return;
}
// Ignore abstract functions.
if (isset($tokens[$stackPtr]['scope_closer']) === false) {
return;
}

$next = $phpcsFile->findNext(Tokens::EMPTY_TOKENS, ($stackPtr + 1), null, true);
if ($next === false || $tokens[$next]['code'] !== T_STRING) {
// Not a function declaration, or incomplete.
return;
$next = $phpcsFile->findNext(Tokens::EMPTY_TOKENS, ($stackPtr + 1), null, true);
if ($next === false || $tokens[$next]['code'] !== T_STRING) {
// Not a function declaration, or incomplete.
return;
}

$type = 'method';
} else {
$type = 'closure';
}

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

$this->checkThisUsage($phpcsFile, $next, $end);
$this->checkThisUsage($phpcsFile, $next, $end, $type);
}


Expand All @@ -78,21 +84,25 @@ public function processTokenWithinScope(File $phpcsFile, int $stackPtr, int $cur
* @param \PHP_CodeSniffer\Files\File $phpcsFile The current file being scanned.
* @param int $next The position of the next token to check.
* @param int $end The position of the last token to check.
* @param string $type Type of context being checked. Either 'method' or 'closure'.
*
* @return void
*/
private function checkThisUsage(File $phpcsFile, int $next, int $end)
private function checkThisUsage(File $phpcsFile, int $next, int $end, string $type)
{
$tokens = $phpcsFile->getTokens();

do {
$next = $phpcsFile->findNext([T_VARIABLE, T_ANON_CLASS], ($next + 1), $end);
$next = $phpcsFile->findNext([T_VARIABLE, T_CLOSURE, T_ANON_CLASS], ($next + 1), $end);
if ($next === false) {
continue;
}

if ($tokens[$next]['code'] === T_ANON_CLASS) {
$this->checkThisUsage($phpcsFile, $next, $tokens[$next]['scope_opener']);
if (($tokens[$next]['code'] === T_ANON_CLASS
|| $tokens[$next]['code'] === T_CLOSURE)
&& isset($tokens[$next]['scope_opener']) === true
) {
$this->checkThisUsage($phpcsFile, $next, $tokens[$next]['scope_opener'], $type);
$next = $tokens[$next]['scope_closer'];
continue;
}
Expand All @@ -101,8 +111,10 @@ private function checkThisUsage(File $phpcsFile, int $next, int $end)
continue;
}

$error = 'Usage of "$this" in static methods will cause runtime errors';
$phpcsFile->addError($error, $next, 'Found');
$error = 'Usage of "$this" in a static %s will cause runtime errors';
$data = [$type];

$phpcsFile->addError($error, $next, 'Found', $data);
} while ($next !== false);
}

Expand All @@ -119,5 +131,18 @@ private function checkThisUsage(File $phpcsFile, int $next, int $end)
*/
protected function processTokenOutsideScope(File $phpcsFile, int $stackPtr)
{
$tokens = $phpcsFile->getTokens();

if ($tokens[$stackPtr]['code'] !== T_CLOSURE) {
// We're only interested in closures when looking outside of OO.
return;
}

$methodProps = $phpcsFile->getMethodProperties($stackPtr);
if ($methodProps['is_static'] === false) {
return;
}

$this->checkThisUsage($phpcsFile, $stackPtr, $tokens[$stackPtr]['scope_closer'], 'closure');
}
}
57 changes: 54 additions & 3 deletions src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.inc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php
class MyClass
{
abstract class MyClass {
abstract public static function ignoreMe($a, $b);
public static function func1()
{
$value = 'hello';
Expand Down Expand Up @@ -37,7 +37,7 @@ class MyClass
}

public static function func1() {
return function() {
return static function() {
echo $this->name;
};
}
Expand Down Expand Up @@ -125,3 +125,54 @@ enum MyEnum {
$this->doSomething();
}
}

class NestedScopesShouldBeSkipped
{
public static function examineMe() {
$this->shouldBeFlagged();

function nestedFunction() {
$this->nestedFunctionsDoNotHaveAccessToThis();
}

\Closure::bind(
function ($this) { // Cannot use $this as a param, so this should still be flagged.
$this->x = 1; // This is fine. $this related to the Target class, not to the NestedScopesShouldBeSkipped class.
},
new Target(),
Target::class,
)();

$callable = static function ($a, $b) {
return $a * $b * $this->multiplier; // This should be flagged. The closure is declared as static.
};
}

public function dontExamineMe() {
$this->shouldNotBeFlagged();

\Closure::bind(
function () {
$this->x = 1; // This is fine. $this related to the Target class, not to the NestedScopesShouldBeSkipped class.
},
new Target(),
Target::class,
)();

$callable = static function ($a, $b) {
return $a * $b * $this->multiplier; // This should be flagged. The closure is declared as static.
};
}
}

function ignoreMe() {
$this->notAllowedButNotTheSameIssueAsThisSniffIsLookingFor();
}

$callable = function ($a, $b) {
return $a * $b * $this->multiplier; // This should NOT be flagged. The closure might be bound to an object.
};

$callable = static function ($a, $b) {
return $a * $b * $this->multiplier; // This should be flagged. The closure is declared as static.
};
6 changes: 6 additions & 0 deletions src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ public function getErrorList()
84 => 1,
99 => 1,
125 => 1,
132 => 1,
135 => 1,
139 => 1,
147 => 1,
163 => 1,
177 => 1,
];
}

Expand Down