Skip to content

Commit 387d448

Browse files
authored
Merge pull request #592 from Automattic/fix/590-prevent-undefined-offset-notice
Functions/DynamicCalls: various bug fixes and improvements
2 parents 4b3d7f9 + 1d1a977 commit 387d448

File tree

3 files changed

+92
-128
lines changed

3 files changed

+92
-128
lines changed

WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php

Lines changed: 64 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -7,47 +7,47 @@
77

88
namespace WordPressVIPMinimum\Sniffs\Functions;
99

10+
use PHP_CodeSniffer\Util\Tokens;
1011
use WordPressVIPMinimum\Sniffs\Sniff;
1112

1213
/**
13-
* This sniff enforces that certain functions are not
14-
* dynamically called.
14+
* This sniff enforces that certain functions are not dynamically called.
1515
*
1616
* An example:
17-
*
18-
* <code>
17+
* ```php
1918
* $func = 'func_num_args';
2019
* $func();
21-
* </code>
20+
* ```
2221
*
23-
* See here: http://php.net/manual/en/migration71.incompatible.php
22+
* Note that this sniff does not catch all possible forms of dynamic calling, only some.
2423
*
25-
* Note that this sniff does not catch all possible forms of dynamic
26-
* calling, only some.
24+
* @link http://php.net/manual/en/migration71.incompatible.php
2725
*/
2826
class DynamicCallsSniff extends Sniff {
27+
2928
/**
3029
* Functions that should not be called dynamically.
3130
*
3231
* @var array
3332
*/
34-
private $blacklisted_functions = [
35-
'assert',
36-
'compact',
37-
'extract',
38-
'func_get_args',
39-
'func_get_arg',
40-
'func_num_args',
41-
'get_defined_vars',
42-
'mb_parse_str',
43-
'parse_str',
33+
private $function_names = [
34+
'assert' => true,
35+
'compact' => true,
36+
'extract' => true,
37+
'func_get_args' => true,
38+
'func_get_arg' => true,
39+
'func_num_args' => true,
40+
'get_defined_vars' => true,
41+
'mb_parse_str' => true,
42+
'parse_str' => true,
4443
];
4544

4645
/**
47-
* Array of functions encountered, along with their values.
48-
* Populated on run-time.
46+
* Array of variable assignments encountered, along with their values.
4947
*
50-
* @var array
48+
* Populated at run-time.
49+
*
50+
* @var array The key is the name of the variable, the value, its assigned value.
5151
*/
5252
private $variables_arr = [];
5353

@@ -61,8 +61,6 @@ class DynamicCallsSniff extends Sniff {
6161
/**
6262
* Returns the token types that this sniff is interested in.
6363
*
64-
* We want everything variable- and function-related.
65-
*
6664
* @return array(int)
6765
*/
6866
public function register() {
@@ -87,163 +85,104 @@ public function process_token( $stackPtr ) {
8785
}
8886

8987
/**
90-
* Finds any variable-definitions in the file being processed,
91-
* and stores them internally in a private array. The data stored
92-
* is the name of the variable and its assigned value.
88+
* Finds any variable-definitions in the file being processed and stores them
89+
* internally in a private array.
9390
*
9491
* @return void
9592
*/
9693
private function collect_variables() {
97-
/*
98-
* Make sure we are working with a variable,
99-
* get its value if so.
100-
*/
101-
102-
if (
103-
$this->tokens[ $this->stackPtr ]['type'] !==
104-
'T_VARIABLE'
105-
) {
106-
return;
107-
}
10894

10995
$current_var_name = $this->tokens[ $this->stackPtr ]['content'];
11096

11197
/*
112-
* Find assignments ( $foo = "bar"; )
113-
* -- do this by finding all non-whitespaces, and
114-
* check if the first one is T_EQUAL.
98+
* Find assignments ( $foo = "bar"; ) by finding all non-whitespaces,
99+
* and checking if the first one is T_EQUAL.
115100
*/
116-
117101
$t_item_key = $this->phpcsFile->findNext(
118-
[ T_WHITESPACE ],
102+
Tokens::$emptyTokens,
119103
$this->stackPtr + 1,
120104
null,
121105
true,
122106
null,
123107
true
124108
);
125109

126-
if ( $t_item_key === false ) {
110+
if ( $t_item_key === false || $this->tokens[ $t_item_key ]['code'] !== T_EQUAL ) {
127111
return;
128112
}
129113

130-
if ( $this->tokens[ $t_item_key ]['type'] !== 'T_EQUAL' ) {
131-
return;
114+
/*
115+
* Find assignments which only assign a plain text string.
116+
*/
117+
$end_of_statement = $this->phpcsFile->findNext( [ T_SEMICOLON, T_CLOSE_TAG ], ( $t_item_key + 1 ) );
118+
$value_ptr = null;
119+
120+
for ( $i = $t_item_key + 1; $i < $end_of_statement; $i++ ) {
121+
if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) === true ) {
122+
continue;
123+
}
124+
125+
if ( $this->tokens[ $i ]['code'] !== T_CONSTANT_ENCAPSED_STRING ) {
126+
// Not a plain text string value. Value cannot be determined reliably.
127+
return;
128+
}
129+
130+
$value_ptr = $i;
132131
}
133132

134-
if ( $this->tokens[ $t_item_key ]['length'] !== 1 ) {
133+
if ( isset( $value_ptr ) === false ) {
134+
// Parse error. Bow out.
135135
return;
136136
}
137137

138138
/*
139-
* Find encapsulated string ( "" )
139+
* If we reached the end of the loop and the $value_ptr was set, we know for sure
140+
* this was a plain text string variable assignment.
140141
*/
141-
$t_item_key = $this->phpcsFile->findNext(
142-
[ T_CONSTANT_ENCAPSED_STRING ],
143-
$t_item_key + 1,
144-
null,
145-
false,
146-
null,
147-
true
148-
);
142+
$current_var_value = $this->strip_quotes( $this->tokens[ $value_ptr ]['content'] );
149143

150-
if ( $t_item_key === false ) {
144+
if ( isset( $this->function_names[ $current_var_value ] ) === false ) {
145+
// Text string is not one of the ones we're looking for.
151146
return;
152147
}
153148

154149
/*
155-
* We have found variable-assignment,
156-
* register its name and value in the
157-
* internal array for later usage.
150+
* Register the variable name and value in the internal array for later usage.
158151
*/
159-
160-
$current_var_value =
161-
$this->tokens[ $t_item_key ]['content'];
162-
163-
$this->variables_arr[ $current_var_name ] =
164-
str_replace( "'", '', $current_var_value );
152+
$this->variables_arr[ $current_var_name ] = $current_var_value;
165153
}
166154

167155
/**
168156
* Find any dynamic calls being made using variables.
169-
* Report on this when found, using name of the function
170-
* in the message.
157+
*
158+
* Report on this when found, using the name of the function in the message.
171159
*
172160
* @return void
173161
*/
174162
private function find_dynamic_calls() {
175-
/*
176-
* No variables detected; no basis for doing
177-
* anything
178-
*/
179-
163+
// No variables detected; no basis for doing anything.
180164
if ( empty( $this->variables_arr ) ) {
181165
return;
182166
}
183167

184168
/*
185-
* Make sure we do have a variable to work with.
186-
*/
187-
188-
if (
189-
$this->tokens[ $this->stackPtr ]['type'] !==
190-
'T_VARIABLE'
191-
) {
192-
return;
193-
}
194-
195-
/*
196-
* If variable is not found in our registry of
197-
* variables, do nothing, as we cannot be
198-
* sure that the function being called is one of the
199-
* blacklisted ones.
169+
* If variable is not found in our registry of variables, do nothing, as we cannot be
170+
* sure that the function being called is one of the blacklisted ones.
200171
*/
201-
202-
if ( ! isset(
203-
$this->variables_arr[ $this->tokens[ $this->stackPtr ]['content'] ]
204-
) ) {
172+
if ( ! isset( $this->variables_arr[ $this->tokens[ $this->stackPtr ]['content'] ] ) ) {
205173
return;
206174
}
207175

208176
/*
209-
* Check if we have an '(' next, or separated by whitespaces
210-
* from our current position.
177+
* Check if we have an '(' next.
211178
*/
212-
213-
$i = 0;
214-
215-
do {
216-
$i++;
217-
} while (
218-
$this->tokens[ $this->stackPtr + $i ]['type'] ===
219-
'T_WHITESPACE'
220-
);
221-
222-
if (
223-
$this->tokens[ $this->stackPtr + $i ]['type'] !==
224-
'T_OPEN_PARENTHESIS'
225-
) {
226-
return;
227-
}
228-
229-
$t_item_key = $this->stackPtr + $i;
230-
231-
/*
232-
* We have a variable match, but make sure it contains name
233-
* of a function which is on our blacklist.
234-
*/
235-
236-
if ( ! in_array(
237-
$this->variables_arr[ $this->tokens[ $this->stackPtr ]['content'] ],
238-
$this->blacklisted_functions,
239-
true
240-
) ) {
179+
$next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $this->stackPtr + 1 ), null, true );
180+
if ( $next === false || $this->tokens[ $next ]['code'] !== T_OPEN_PARENTHESIS ) {
241181
return;
242182
}
243183

244-
// We do, so report.
245-
$message = 'Dynamic calling is not recommended in the case of %s.';
184+
$message = 'Dynamic calling is not recommended in the case of %s().';
246185
$data = [ $this->variables_arr[ $this->tokens[ $this->stackPtr ]['content'] ] ];
247-
$this->phpcsFile->addError( $message, $t_item_key, 'DynamicCalls', $data );
186+
$this->phpcsFile->addError( $message, $this->stackPtr, 'DynamicCalls', $data );
248187
}
249188
}

WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.inc

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,33 @@ function my_test() {
66

77

88
$my_notokay_func = 'extract';
9-
$my_notokay_func();
9+
$my_notokay_func(); // Bad.
1010

1111
$my_okay_func = 'my_test';
12-
$my_okay_func();
12+
$my_okay_func(); // OK.
1313

14+
$test_with_comment /*comment*/ = 'func_get_args';
15+
$test_with_comment /*comment*/ (); // Bad.
1416

17+
$test_getting_the_actual_value_1 = function_call( 'extract' );
18+
$test_getting_the_actual_value_1(); // OK. Unclear what the actual variable value will be.
1519

20+
$test_getting_the_actual_value_2 = $array['compact'];
21+
$test_getting_the_actual_value_2(); // OK. Unclear what the actual variable value will be.
22+
23+
$test_getting_the_actual_value_3 = 10 ?>
24+
<div>html</div>
25+
<?php
26+
echo 'extract';
27+
$test_getting_the_actual_value_3(); // OK. Broken function call, but not calling extract().
28+
29+
$test_getting_the_actual_value_4 = 'get_defined_vars' . $source;
30+
$test_getting_the_actual_value_4(); // OK. Unclear what the actual variable value will be.
31+
32+
$ensure_no_notices_are_thrown_on_parse_error = /*comment*/ ;
33+
34+
$test_double_quoted_string = "assert";
35+
$test_double_quoted_string(); // Bad.
36+
37+
// Intentional parse error. This has to be the last test in the file.
38+
$my_notokay_func

WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ class DynamicCallsUnitTest extends AbstractSniffUnitTest {
2525
*/
2626
public function getErrorList() {
2727
return [
28-
9 => 1,
28+
9 => 1,
29+
15 => 1,
30+
35 => 1,
2931
];
3032
}
3133

0 commit comments

Comments
 (0)