Skip to content

Commit c54af67

Browse files
committed
Abstracts\AbstractArrayDeclarationSniff: new getActualArrayKey() method
This is a helper method which can optionally be used from the `processKey()` method to retrieve the actual array key value based on the array key rules used in PHP. * Array keys can only be integers or strings. * Purely numeric (integer) string keys will automatically be cast to integer. Dynamic array keys using variables, constants or function calls to set the key are ignored. The method will return the integer or string array key or void for when the array key could not be determined. Includes dedicated unit tests.
1 parent 8dcca5e commit c54af67

File tree

4 files changed

+430
-0
lines changed

4 files changed

+430
-0
lines changed

PHPCSUtils/AbstractSniffs/AbstractArrayDeclarationSniff.php

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@
1313
use PHP_CodeSniffer\Exceptions\RuntimeException;
1414
use PHP_CodeSniffer\Files\File;
1515
use PHP_CodeSniffer\Sniffs\Sniff;
16+
use PHP_CodeSniffer\Util\Tokens;
17+
use PHPCSUtils\BackCompat\BCTokens;
1618
use PHPCSUtils\Utils\Arrays;
19+
use PHPCSUtils\Utils\Numbers;
1720
use PHPCSUtils\Utils\PassedParameters;
21+
use PHPCSUtils\Utils\TextStrings;
1822

1923
/**
2024
* Abstract sniff to easily examine all parts of an array declaration.
@@ -93,6 +97,48 @@ abstract class AbstractArrayDeclarationSniff implements Sniff
9397
*/
9498
protected $singleLine;
9599

100+
/**
101+
* List of tokens which can safely be used with an eval() expression.
102+
*
103+
* @since 1.0.0
104+
*
105+
* @var array
106+
*/
107+
private $acceptedTokens = [
108+
\T_NULL => \T_NULL,
109+
\T_TRUE => \T_TRUE,
110+
\T_FALSE => \T_FALSE,
111+
\T_LNUMBER => \T_LNUMBER,
112+
\T_DNUMBER => \T_DNUMBER,
113+
\T_CONSTANT_ENCAPSED_STRING => \T_CONSTANT_ENCAPSED_STRING,
114+
\T_STRING_CONCAT => \T_STRING_CONCAT,
115+
\T_INLINE_THEN => \T_INLINE_THEN,
116+
\T_INLINE_ELSE => \T_INLINE_ELSE,
117+
\T_BOOLEAN_NOT => \T_BOOLEAN_NOT,
118+
];
119+
120+
/**
121+
* Set up this class.
122+
*
123+
* @since 1.0.0
124+
*
125+
* @codeCoverageIgnore
126+
*
127+
* @return void
128+
*/
129+
final public function __construct()
130+
{
131+
// Enhance the list of accepted tokens.
132+
$this->acceptedTokens += BCTokens::assignmentTokens();
133+
$this->acceptedTokens += BCTokens::comparisonTokens();
134+
$this->acceptedTokens += BCTokens::arithmeticTokens();
135+
$this->acceptedTokens += BCTokens::operators();
136+
$this->acceptedTokens += BCTokens::booleanOperators();
137+
$this->acceptedTokens += BCTokens::castTokens();
138+
$this->acceptedTokens += BCTokens::bracketTokens();
139+
$this->acceptedTokens += BCTokens::heredocTokens();
140+
}
141+
96142
/**
97143
* Returns an array of tokens this test wants to listen for.
98144
*
@@ -243,6 +289,8 @@ public function processOpenClose(File $phpcsFile, $openPtr, $closePtr)
243289
*
244290
* @codeCoverageIgnore
245291
*
292+
* @see \PHPCSUtils\AbstractSniffs\AbstractArrayDeclarationSniff::getActualArrayKey() Optional helper function.
293+
*
246294
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
247295
* token was found.
248296
* @param int $startPtr The stack pointer to the first token in the "key" part of
@@ -349,4 +397,121 @@ public function processValue(File $phpcsFile, $startPtr, $endPtr, $itemNr)
349397
public function processComma(File $phpcsFile, $commaPtr, $itemNr)
350398
{
351399
}
400+
401+
/**
402+
* Determine what the actual array key would be.
403+
*
404+
* Optional helper function for processsing array keys in the processKey() function.
405+
*
406+
* @since 1.0.0
407+
*
408+
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
409+
* token was found.
410+
* @param int $startPtr The stack pointer to the first token in the "key" part of
411+
* an array item.
412+
* @param int $endPtr The stack pointer to the last token in the "key" part of
413+
* an array item.
414+
*
415+
* @return string|int|void The string or integer array key or void if the array key could not
416+
* reliably be determined.
417+
*/
418+
public function getActualArrayKey(File $phpcsFile, $startPtr, $endPtr)
419+
{
420+
/*
421+
* Determine the value of the key.
422+
*/
423+
$firstNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $startPtr, null, true);
424+
$lastNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, $endPtr, null, true);
425+
426+
$content = '';
427+
428+
for ($i = $firstNonEmpty; $i <= $lastNonEmpty; $i++) {
429+
if (isset(Tokens::$commentTokens[$this->tokens[$i]['code']]) === true) {
430+
continue;
431+
}
432+
433+
if ($this->tokens[$i]['code'] === \T_WHITESPACE) {
434+
$content .= ' ';
435+
continue;
436+
}
437+
438+
if (isset($this->acceptedTokens[$this->tokens[$i]['code']]) === false) {
439+
// This is not a key we can evaluate. Might be a variable or constant.
440+
return;
441+
}
442+
443+
// Take PHP 7.4 numeric literal separators into account.
444+
if ($this->tokens[$i]['code'] === \T_LNUMBER || $this->tokens[$i]['code'] === \T_DNUMBER) {
445+
try {
446+
$number = Numbers::getCompleteNumber($phpcsFile, $i);
447+
$content .= $number['content'];
448+
$i = $number['last_token'];
449+
} catch (RuntimeException $e) {
450+
// This must be PHP 3.5.3 with the broken backfill. Let's presume it's a ordinary number.
451+
// If it's not, the sniff will bow out on the following T_STRING anyway if the
452+
// backfill was broken.
453+
$content .= \str_replace('_', '', $this->tokens[$i]['content']);
454+
}
455+
continue;
456+
}
457+
458+
// Account for heredoc with vars.
459+
if ($this->tokens[$i]['code'] === \T_START_HEREDOC) {
460+
$text = TextStrings::getCompleteTextString($phpcsFile, $i);
461+
462+
// Check if there's a variable in the heredoc.
463+
if (\preg_match('`(?<![\\\\])\$`', $text) === 1) {
464+
return;
465+
}
466+
467+
for ($j = $i; $j <= $this->tokens[$i]['scope_closer']; $j++) {
468+
$content .= $this->tokens[$j]['content'];
469+
}
470+
471+
$i = $this->tokens[$i]['scope_closer'];
472+
continue;
473+
}
474+
475+
$content .= $this->tokens[$i]['content'];
476+
}
477+
478+
// The PHP_EOL is to prevent getting parse errors when the key is a heredoc/nowdoc.
479+
$key = eval('return ' . $content . ';' . \PHP_EOL);
480+
481+
/*
482+
* Ok, so now we know the base value of the key, let's determine whether it is
483+
* an acceptable index key for an array and if not, what it would turn into.
484+
*/
485+
486+
$integerKey = false;
487+
488+
switch (\gettype($key)) {
489+
case 'NULL':
490+
// An array key of `null` will become an empty string.
491+
return '';
492+
493+
case 'boolean':
494+
return ($key === true) ? 1 : 0;
495+
496+
case 'integer':
497+
return $key;
498+
499+
case 'double':
500+
return (int) $key; // Will automatically cut off the decimal part.
501+
502+
case 'string':
503+
if (Numbers::isDecimalInt($key) === true) {
504+
return (int) $key;
505+
}
506+
507+
return $key;
508+
509+
default:
510+
/*
511+
* Shouldn't be possible. Either way, if it's not one of the above types,
512+
* this is not a key we can handle.
513+
*/
514+
return;
515+
}
516+
}
352517
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
/**
3+
* PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers.
4+
*
5+
* @package PHPCSUtils
6+
* @copyright 2019 PHPCSUtils Contributors
7+
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
8+
* @link https://github.com/PHPCSStandards/PHPCSUtils
9+
*/
10+
11+
namespace PHPCSUtils\Tests\AbstractSniffs\AbstractArrayDeclaration;
12+
13+
use PHP_CodeSniffer\Files\File;
14+
use PHPCSUtils\AbstractSniffs\AbstractArrayDeclarationSniff;
15+
16+
/**
17+
* Test double for the AbstractArrayDeclarationSniff to allow for testing the getActualArrayKey() method.
18+
*
19+
* @since 1.0.0
20+
*/
21+
class ArrayDeclarationSniffTestDouble extends AbstractArrayDeclarationSniff
22+
{
23+
24+
/**
25+
* The token stack for the current file being examined.
26+
*
27+
* @var array
28+
*/
29+
public $tokens;
30+
31+
/**
32+
* Process every part of the array declaration.
33+
*
34+
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
35+
* token was found.
36+
*
37+
* @return void
38+
*/
39+
public function processArray(File $phpcsFile)
40+
{
41+
}
42+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
/* testAllVoid */
4+
$excluded = [
5+
$var => 'excluded',
6+
MY_CONSTANT => 'excluded',
7+
PHP_INT_MAX => 'excluded',
8+
str_replace('.', '', '1.1') => 'excluded',
9+
self::CONSTANT => 'excluded',
10+
$obj->get_key() => 'excluded',
11+
$obj->prop => 'excluded',
12+
"my $var text" => 'excluded',
13+
<<<EOD
14+
my $var text
15+
EOD
16+
=> 'excluded',
17+
$var['key']{1} => 'excluded',
18+
];
19+
20+
/* testAllEmptyString */
21+
$emptyStringKey = array(
22+
'' => 'empty',
23+
null => 'null',
24+
(string) false => 'false',
25+
);
26+
27+
/* testAllZero */
28+
$everythingZero = [
29+
'0',
30+
0 => 'a',
31+
0.0 => 'b',
32+
'0' => 'c',
33+
0b0 => 'd',
34+
0x0 => 'e',
35+
00 => 'f',
36+
false => 'g',
37+
0.4 => 'h',
38+
-0.8 => 'i',
39+
0e0 => 'j',
40+
0_0 => 'k',
41+
-1 + 1 => 'l',
42+
3 * 0 => 'm',
43+
00.00 => 'n',
44+
(int) 'nothing' => 'o',
45+
15 > 200 => 'p',
46+
"0" => 'q',
47+
0. => 'r',
48+
.0 => 's',
49+
(true) ? 0 : 1 => 't',
50+
! true => 'u',
51+
];
52+
53+
/* testAllOne */
54+
$everythingOne = [
55+
'0',
56+
'1',
57+
1 => 'a',
58+
1.1 => 'b',
59+
'1' => 'c',
60+
0b1 => 'd',
61+
0x1 => 'e',
62+
01 => 'f',
63+
true => 'g',
64+
1.2 /*comment*/ => 'h',
65+
1e0 => 'i',
66+
0_1 => 'j',
67+
-1 + 2 => 'k',
68+
3 * 0.5 => 'l',
69+
01.00 => 'm',
70+
(int) '1 penny' => 'n',
71+
15 < 200 => 'o',
72+
"1" => 'p',
73+
1. => 'q',
74+
001. => 'r',
75+
(true) ? 1 : 0 => 's',
76+
! false => 't',
77+
(string) true => 'u',
78+
];
79+
80+
/* testAllEleven */
81+
$everythingEleven = [
82+
11 => 'a',
83+
11.0 => 'b',
84+
'11' => 'c',
85+
0b1011 => 'd',
86+
0Xb => 'e',
87+
013 => 'f',
88+
11.8 => 'g',
89+
1.1e1 => 'h',
90+
1_1 => 'i',
91+
0_13 => 'j',
92+
-1 + 12 => 'k',
93+
22 / /*comment*/ 2 => 'l',
94+
0011.0011 => 'm',
95+
(int) '11 lane' => 'n',
96+
"11" => 'o',
97+
11. => 'p',
98+
35 % 12 => 'q',
99+
];
100+
101+
/* testAllStringAbc */
102+
$textualStringKeyVariations = [
103+
'abc' => 1,
104+
'ab' . 'c' => 4,
105+
<<<EOT
106+
abc
107+
EOT
108+
=> 5,
109+
<<< 'NOW'
110+
abc
111+
NOW
112+
=> 6,
113+
"abc" => 7,
114+
];

0 commit comments

Comments
 (0)