Skip to content

Commit 7c73f92

Browse files
authored
Improve static declaration checks (sirbrillig#267)
* Add test for `new` keyword in static initializer * Add tests for static and nullable class properties * Adjust processVariableAsStaticDeclaration to be more liberal * Add test for static methods with a parameter * Just examine the token at the start of the statement * Remove T_MATCH_ARROW which did not exist in php 5 * Improve static variable tests by covering static methods with vars * Refactor exclusion of static functions for phpcs 3.5.6 * Make FunctionWithReferenceFixture test code more clear
1 parent 5f03314 commit 7c73f92

File tree

5 files changed

+176
-93
lines changed

5 files changed

+176
-93
lines changed

Tests/VariableAnalysisSniff/VariableAnalysisTest.php

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -300,20 +300,25 @@ public function testFunctionWithReferenceWarnings()
300300
$phpcsFile->process();
301301
$lines = $this->getWarningLineNumbersFromFile($phpcsFile);
302302
$expectedWarnings = [
303-
8,
304-
20,
305-
32,
306-
33,
307-
34,
308-
36,
309-
37,
310-
39,
311-
40,
303+
10,
304+
11,
305+
12,
306+
13,
307+
14,
308+
16,
309+
29,
310+
41,
311+
42,
312+
43,
312313
46,
313-
59,
314-
60,
315-
64,
314+
52,
315+
56,
316+
57,
317+
63,
318+
76,
319+
77,
316320
81,
321+
98,
317322
];
318323
$this->assertSame($expectedWarnings, $lines);
319324
}
@@ -330,18 +335,23 @@ public function testFunctionWithReferenceWarningsAllowsCustomFunctions()
330335
$phpcsFile->process();
331336
$lines = $this->getWarningLineNumbersFromFile($phpcsFile);
332337
$expectedWarnings = [
333-
8,
334-
20,
335-
32,
336-
33,
337-
34,
338-
36,
339-
37,
340-
39,
341-
40,
338+
10,
339+
11,
340+
12,
341+
13,
342+
14,
343+
16,
344+
29,
345+
41,
346+
42,
347+
43,
342348
46,
343-
64,
349+
52,
350+
56,
351+
57,
352+
63,
344353
81,
354+
98,
345355
];
346356
$this->assertSame($expectedWarnings, $lines);
347357
}
@@ -358,19 +368,24 @@ public function testFunctionWithReferenceWarningsAllowsWordPressFunctionsIfSet()
358368
$phpcsFile->process();
359369
$lines = $this->getWarningLineNumbersFromFile($phpcsFile);
360370
$expectedWarnings = [
361-
8,
362-
20,
363-
32,
364-
33,
365-
34,
366-
36,
367-
37,
368-
39,
369-
40,
371+
10,
372+
11,
373+
12,
374+
13,
375+
14,
376+
16,
377+
29,
378+
41,
379+
42,
380+
43,
370381
46,
371-
59,
372-
60,
373-
81,
382+
52,
383+
56,
384+
57,
385+
63,
386+
76,
387+
77,
388+
98,
374389
];
375390
$this->assertSame($expectedWarnings, $lines);
376391
}

Tests/VariableAnalysisSniff/fixtures/ClassWithMembersFixture.php

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ function method_with_member_var() {
4545

4646
class ClassWithMembers {
4747
public $member_var;
48-
private $private_member_var;
49-
protected $protected_member_var;
48+
private ?string $private_member_var;
49+
protected string $protected_member_var;
5050
static $static_member_var;
5151

5252
function method_with_member_var() {
@@ -123,3 +123,40 @@ public function __construct(
123123
) {
124124
}
125125
}
126+
127+
class ClassWithStaticProperties {
128+
static $static_simple;
129+
public static $static_with_visibility;
130+
public static $static_with_visibility_unused;
131+
public static int $static_with_visibility_and_type;
132+
public static ?int $static_with_visibility_and_nullable_type;
133+
134+
public function use_vars() {
135+
echo self::$static_simple;
136+
echo self::$static_with_visibility;
137+
echo self::$static_with_visibility_and_type;
138+
echo self::$static_with_visibility_and_nullable_type;
139+
}
140+
141+
public static function getIntOrNull($value) {
142+
return is_int($value) ? $value : null;
143+
}
144+
145+
static function getIntOrNull2($value) {
146+
return is_int($value) ? $value : null;
147+
}
148+
}
149+
150+
abstract class AbstractClassWithStaticProperties {
151+
static $static_simple;
152+
public static $static_with_visibility;
153+
public static $static_with_visibility_unused;
154+
public static int $static_with_visibility_and_type;
155+
public static ?int $static_with_visibility_and_nullable_type;
156+
157+
public function use_vars();
158+
159+
public static function getIntOrNull($value);
160+
161+
static function getIntOrNull2($value);
162+
}

Tests/VariableAnalysisSniff/fixtures/FunctionWithReferenceFixture.php

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,16 @@ function /*comment*/ &function_with_return_by_reference_and_param($param) {
55
}
66

77
function function_with_static_var() {
8-
static $static1, $static_num = 12, $static_neg_num = -1.5, $static_string = 'abc', $static_string2 = "def", $static_define = MYDEFINE, $static_constant = MyClass::CONSTANT, $static2;
8+
static $static1,
9+
$static_num = 12,
10+
$static_neg_num = -1.5, // Unused variable $static_neg_num
11+
$static_string = 'abc', // Unused variable $static_string
12+
$static_string2 = "def", // Unused variable $static_string2
13+
$static_define = MYDEFINE, // Unused variable $static_define
14+
$static_constant = MyClass::CONSTANT, // Unused variable $static_constant
15+
$static2,
16+
$static_new_unused = new Foobar(), // Unused variable $static_new_unused
17+
$static_new = new Foobar();
918
static $static_heredoc = <<<END_OF_HEREDOC
1019
this is an ugly but valid way to continue after a heredoc
1120
END_OF_HEREDOC
@@ -17,33 +26,41 @@ function function_with_static_var() {
1726
echo $static1;
1827
echo $static_num;
1928
echo $static2;
20-
echo $var;
29+
echo $var; // Undefined variable $var
2130
echo $static_heredoc;
2231
echo $static3;
2332
echo $static_nowdoc;
24-
echo $static4;
33+
echo $static4 . $static_new;
2534
}
2635

2736
function function_with_pass_by_reference_param(&$param) {
2837
echo $param;
2938
}
3039

3140
function function_with_pass_by_reference_calls() {
32-
echo $matches;
33-
echo $needle;
34-
echo $haystack;
41+
echo $matches; // Undefined variable $matches
42+
echo $needle; // Undefined variable $needle
43+
echo $haystack; // Undefined variable $haystack
3544
preg_match('/(abc)/', 'defabcghi', /* comment */ $matches);
36-
preg_match($needle, 'defabcghi', $matches);
37-
preg_match('/(abc)/', $haystack, $matches);
45+
preg_match(
46+
$needle, // Undefined variable $needle
47+
'defabcghi',
48+
$matches
49+
);
50+
preg_match(
51+
'/(abc)/',
52+
$haystack, // Undefined variable $haystack
53+
$matches
54+
);
3855
echo $matches;
39-
echo $needle;
40-
echo $haystack;
56+
echo $needle; // Undefined variable $needle
57+
echo $haystack; // Undefined variable $haystack
4158
$stmt = 'whatever';
4259
$var1 = 'one';
4360
$var2 = 'two';
4461
echo $var1;
4562
echo $var2;
46-
echo $var3;
63+
echo $var3; // Undefined variable $var3
4764
maxdb_stmt_bind_result /*comment*/ ($stmt, $var1, $var2, $var3);
4865
echo $var1;
4966
echo $var2;
@@ -56,12 +73,12 @@ function function_with_pass_by_ref_assign_only_arg(& /*comment*/ $return_value
5673

5774
function function_with_ignored_reference_call() {
5875
$foo = 'bar';
59-
my_reference_function($foo, $baz, $bip);
60-
another_reference_function($foo, $foo2, $foo3);
76+
my_reference_function($foo, $baz, $bip); // Undefined variable $bar, Undefined variable $bip
77+
another_reference_function($foo, $foo2, $foo3); // Undefined variable $foo2, Undefined variable $foo3
6178
}
6279

6380
function function_with_wordpress_reference_calls() {
64-
wp_parse_str('foo=bar', $vars);
81+
wp_parse_str('foo=bar', $vars); // Undefined variable $vars
6582
}
6683

6784
function function_with_array_walk($userNameParts) {
@@ -78,7 +95,7 @@ function function_with_foreach_with_reference($derivatives, $base_plugin_definit
7895
foreach ($derivatives as &$entry) {
7996
$entry .= $base_plugin_definition;
8097
}
81-
foreach ($derivatives as &$unused) { // unused variable
98+
foreach ($derivatives as &$unused) { // Unused variable $unused
8299
$base_plugin_definition .= '1';
83100
}
84101
return $derivatives;

VariableAnalysis/Lib/Helpers.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,13 @@ public static function isTokenInsideFunctionCallArgument(File $phpcsFile, $stack
159159
}
160160

161161
/**
162-
* Find the index of the function keyword for a token in a function definition's arguments
162+
* Find the index of the function keyword for a token in a function
163+
* definition's parameters.
163164
*
164165
* Does not work for tokens inside the "use".
165166
*
166-
* Will also work for the parenthesis that make up the function definition's arguments list.
167+
* Will also work for the parenthesis that make up the function definition's
168+
* parameters list.
167169
*
168170
* For arguments inside a function call, rather than a definition, use
169171
* `getFunctionIndexForFunctionCallArgument`.
@@ -256,6 +258,10 @@ public static function getUseIndexForUseImport(File $phpcsFile, $stackPtr)
256258
}
257259

258260
/**
261+
* Return the index of a function's name token from inside the function.
262+
*
263+
* $stackPtr must be inside the function body or parameters for this to work.
264+
*
259265
* @param File $phpcsFile
260266
* @param int $stackPtr
261267
*

VariableAnalysis/Sniffs/CodeAnalysis/VariableAnalysisSniff.php

Lines changed: 48 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1275,6 +1275,33 @@ protected function processVariableAsGlobalDeclaration(File $phpcsFile, $stackPtr
12751275
}
12761276

12771277
/**
1278+
* Process a variable as a static declaration within a function.
1279+
*
1280+
* This will not operate on variables that are written a class definition
1281+
* like `static $foo;` or `public static ?int $foo = 'bar';` because class
1282+
* properties (static or instance) are currently not tracked by this sniff.
1283+
* This is because a class property might be unused inside the class, but
1284+
* used outside the class (we cannot easily know if it is unused); this is
1285+
* also because it's common and legal to define class properties when they
1286+
* are assigned and that assignment can happen outside a class (we cannot
1287+
* easily know if the use of a property is undefined). These sorts of checks
1288+
* are better performed by static analysis tools that can see a whole project
1289+
* rather than a linter which can only easily see a file or some lines.
1290+
*
1291+
* If found, such a variable will be marked as declared (and possibly
1292+
* assigned, if it includes an initial value) within the scope of the
1293+
* function body.
1294+
*
1295+
* This will not operate on variables that use late static binding
1296+
* (`static::$foobar`) or the parameters of static methods even though they
1297+
* include the word `static` in the same statement.
1298+
*
1299+
* This only finds the defintions of static variables. Their use is handled
1300+
* by `processVariableAsStaticMember()`.
1301+
*
1302+
* Can be called for any token and will return false if the variable is not
1303+
* of this type.
1304+
*
12781305
* @param File $phpcsFile
12791306
* @param int $stackPtr
12801307
* @param string $varName
@@ -1286,55 +1313,36 @@ protected function processVariableAsStaticDeclaration(File $phpcsFile, $stackPtr
12861313
{
12871314
$tokens = $phpcsFile->getTokens();
12881315

1289-
// Are we a static declaration?
1290-
// Static declarations are a bit more complicated than globals, since they
1291-
// can contain assignments. The assignment is compile-time however so can
1292-
// only be constant values, which makes life manageable.
1293-
//
1294-
// Just to complicate matters further, late static binding constants
1295-
// take the form static::CONSTANT and are invalid within static variable
1296-
// assignments, but we don't want to accidentally match their use of the
1297-
// static keyword.
1298-
//
1299-
// Valid values are:
1300-
// number T_MINUS T_LNUMBER T_DNUMBER
1301-
// string T_CONSTANT_ENCAPSED_STRING
1302-
// heredoc T_START_HEREDOC T_HEREDOC T_END_HEREDOC
1303-
// nowdoc T_START_NOWDOC T_NOWDOC T_END_NOWDOC
1304-
// define T_STRING
1305-
// class constant T_STRING T_DOUBLE_COLON T_STRING
1306-
// Search backwards for first token that isn't whitespace, comma, variable,
1307-
// equals, or on the list of assignable constant values above.
1308-
$find = [
1309-
T_WHITESPACE => T_WHITESPACE,
1310-
T_VARIABLE => T_VARIABLE,
1311-
T_COMMA => T_COMMA,
1312-
T_EQUAL => T_EQUAL,
1313-
T_MINUS => T_MINUS,
1314-
T_LNUMBER => T_LNUMBER,
1315-
T_DNUMBER => T_DNUMBER,
1316-
T_CONSTANT_ENCAPSED_STRING => T_CONSTANT_ENCAPSED_STRING,
1317-
T_STRING => T_STRING,
1318-
T_DOUBLE_COLON => T_DOUBLE_COLON,
1319-
];
1320-
$find += Tokens::$heredocTokens;
1316+
// Search backwards for a `static` keyword that occurs before the start of the statement.
1317+
$startOfStatement = $phpcsFile->findPrevious([T_SEMICOLON, T_OPEN_CURLY_BRACKET], $stackPtr - 1, null, false, null, true);
1318+
$staticPtr = $phpcsFile->findPrevious([T_STATIC], $stackPtr - 1, null, false, null, true);
1319+
if (! is_int($startOfStatement)) {
1320+
$line = $tokens[$stackPtr]['line'];
1321+
throw new \Exception("Could not find start of statement on line {$line}");
1322+
}
1323+
if (! is_int($staticPtr)) {
1324+
return false;
1325+
}
1326+
// PHPCS is bad at finding the start of statements so we have to do it ourselves.
1327+
if ($staticPtr < $startOfStatement) {
1328+
return false;
1329+
}
13211330

1322-
$staticPtr = $phpcsFile->findPrevious($find, $stackPtr - 1, null, true, null, true);
1323-
if (($staticPtr === false) || ($tokens[$staticPtr]['code'] !== T_STATIC)) {
1331+
// Is the token inside function parameters? If so, this is not a static
1332+
// declaration because we must be inside a function body.
1333+
if (Helpers::isTokenFunctionParameter($phpcsFile, $stackPtr)) {
13241334
return false;
13251335
}
13261336

1327-
// Is it a late static binding static::?
1328-
// If so, this isn't the static keyword we're looking for, but since
1329-
// static:: isn't allowed in a compile-time constant, we also know
1330-
// we can't be part of a static declaration anyway, so there's no
1331-
// need to look any further.
1337+
// Is the keyword a late static binding? If so, this isn't the static
1338+
// keyword we're looking for, but since static:: isn't allowed in a
1339+
// compile-time constant, we also know we can't be part of a static
1340+
// declaration anyway, so there's no need to look any further.
13321341
$lateStaticBindingPtr = $phpcsFile->findNext(T_WHITESPACE, $staticPtr + 1, null, true, null, true);
13331342
if (($lateStaticBindingPtr !== false) && ($tokens[$lateStaticBindingPtr]['code'] === T_DOUBLE_COLON)) {
13341343
return false;
13351344
}
13361345

1337-
// It's a static declaration.
13381346
$this->markVariableDeclaration($varName, ScopeType::STATICSCOPE, null, $stackPtr, $currScope);
13391347
if (Helpers::getNextAssignPointer($phpcsFile, $stackPtr) !== null) {
13401348
$this->markVariableAssignment($varName, $stackPtr, $currScope);

0 commit comments

Comments
 (0)