Skip to content

Commit ecdcbed

Browse files
authoredApr 27, 2023
Merge pull request #212 from magento-l3/ACP2E-1868
ACP2E-1868: Move sniff PHPCompatibility.TextStrings.RemovedDollarBraceStringEmbeds from PHPCompatibility to Magento Coding Standards
2 parents 7465837 + 32d4ded commit ecdcbed

5 files changed

+521
-0
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<?php
2+
/**
3+
* PHPCompatibility, an external standard for PHP_CodeSniffer.
4+
*
5+
* @package PHPCompatibility
6+
* @copyright 2012-2022 PHPCompatibility Contributors
7+
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
8+
* @link https://github.com/PHPCompatibility/PHPCompatibility
9+
*/
10+
11+
namespace Magento2\Sniffs\PHPCompatibility;
12+
13+
use PHP_CodeSniffer\Files\File;
14+
use PHPCompatibility\Sniff;
15+
use PHPCSUtils\Utils\GetTokensAsString;
16+
use PHPCSUtils\Utils\TextStrings;
17+
18+
/**
19+
* Detect use of select forms of variable embedding in heredocs and double strings as deprecated per PHP 8.2.
20+
*
21+
* > PHP allows embedding variables in strings with double-quotes (") and heredoc in various ways.
22+
* > 1. Directly embedding variables (`$foo`)
23+
* > 2. Braces outside the variable (`{$foo}`)
24+
* > 3. Braces after the dollar sign (`${foo}`)
25+
* > 4. Variable variables (`${expr}`, equivalent to `(string) ${expr}`)
26+
* >
27+
* > [...] to deprecate options 3 and 4 in PHP 8.2 and remove them in PHP 9.0.
28+
*
29+
* PHP version 8.2
30+
* PHP version 9.0
31+
*
32+
* @link https://wiki.php.net/rfc/deprecate_dollar_brace_string_interpolation
33+
*
34+
* @since 10.0.0
35+
*/
36+
class RemovedDollarBraceStringEmbedsSniff extends Sniff
37+
{
38+
39+
/**
40+
* Returns an array of tokens this test wants to listen for.
41+
*
42+
* @since 10.0.0
43+
*
44+
* @return array
45+
*/
46+
public function register()
47+
{
48+
return [
49+
\T_DOUBLE_QUOTED_STRING,
50+
\T_START_HEREDOC,
51+
\T_DOLLAR_OPEN_CURLY_BRACES,
52+
];
53+
}
54+
55+
/**
56+
* Processes this test, when one of its tokens is encountered.
57+
*
58+
* @since 10.0.0
59+
*
60+
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
61+
* @param int $stackPtr The position of the current token in the
62+
* stack passed in $tokens.
63+
*
64+
* @return void|int Void or a stack pointer to skip forward.
65+
*/
66+
public function process(File $phpcsFile, $stackPtr)
67+
{
68+
if ($this->supportsAbove('8.2') === false) {
69+
return;
70+
}
71+
72+
$tokens = $phpcsFile->getTokens();
73+
74+
/*
75+
* Defensive coding, this code is not expected to ever actually be hit since PHPCS#3604
76+
* (included in 3.7.0), but _will_ be hit if a file containing a PHP 7.3 indented heredoc/nowdocs
77+
* is scanned with PHPCS on PHP < 7.3. People shouldn't do that, but hey, we can't stop them.
78+
*/
79+
if ($tokens[$stackPtr]['code'] === \T_DOLLAR_OPEN_CURLY_BRACES) {
80+
// @codeCoverageIgnoreStart
81+
if ($tokens[($stackPtr - 1)]['code'] === \T_DOUBLE_QUOTED_STRING) {
82+
--$stackPtr;
83+
} else {
84+
// Throw an error anyway, though it won't be very informative.
85+
$message = 'Using ${} in strings is deprecated since PHP 8.2, use {$var} or {${expr}} instead.';
86+
$code = 'DeprecatedDollarBraceEmbed';
87+
$phpcsFile->addWarning($message, $stackPtr, $code);
88+
return;
89+
}
90+
// @codeCoverageIgnoreEnd
91+
}
92+
93+
$endOfString = TextStrings::getEndOfCompleteTextString($phpcsFile, $stackPtr);
94+
$startOfString = $stackPtr;
95+
if ($tokens[$stackPtr]['code'] === \T_START_HEREDOC) {
96+
$startOfString = ($stackPtr + 1);
97+
}
98+
99+
$contents = GetTokensAsString::normal($phpcsFile, $startOfString, $endOfString);
100+
if (\strpos($contents, '${') === false) {
101+
// No interpolation found or only interpolations which are still supported.
102+
return ($endOfString + 1);
103+
}
104+
105+
$embeds = TextStrings::getEmbeds($contents);
106+
foreach ($embeds as $offset => $embed) {
107+
if (\strpos($embed, '${') !== 0) {
108+
continue;
109+
}
110+
111+
// Figure out the stack pointer to throw the warning on.
112+
$errorPtr = $startOfString;
113+
$length = 0;
114+
while (($length + $tokens[$errorPtr]['length']) < $offset) {
115+
$length += $tokens[$errorPtr]['length'];
116+
++$errorPtr;
117+
}
118+
119+
// Type 4.
120+
$message = 'Using %s (variable variables) in strings is deprecated since PHP 8.2, use {${expr}} instead.';
121+
$code = 'DeprecatedExpressionSyntax';
122+
if (\preg_match('`^\$\{(?P<varname>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]+)(?:\[([\'"])?[^\$\{\}\]]+(?:\2)?\])?\}$`', $embed) === 1) {
123+
// Type 3.
124+
$message = 'Using ${var} in strings is deprecated since PHP 8.2, use {$var} instead. Found: %s';
125+
$code = 'DeprecatedVariableSyntax';
126+
}
127+
128+
$phpcsFile->addWarning($message, $errorPtr, $code, [$embed]);
129+
130+
}
131+
132+
return ($endOfString + 1);
133+
}
134+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
/*
4+
* Embedded variables which are supported cross-version.
5+
*/
6+
7+
// Type 1: directly embedding variables.
8+
echo "$foo";
9+
echo "$$foo";
10+
echo "$foo[bar]";
11+
echo "$foo->bar";
12+
$text = "some text $var some text";
13+
14+
$heredoc = <<<EOD
15+
some text $var some text
16+
EOD;
17+
18+
// Type 2: Braces around the variable/expression.
19+
echo "{$foo}";
20+
echo "{$$foo}";
21+
echo "{$foo['bar']}";
22+
echo "{$foo->bar}";
23+
echo "{$foo->bar()}";
24+
echo "{$foo['bar']->baz()()}";
25+
echo "{${$bar}}";
26+
echo "{$foo()}";
27+
echo "{${$object->getMethod()}}"
28+
$text = "some text {$var} some text";
29+
30+
$heredoc = <<<"EOD"
31+
some text {$var} some text
32+
EOD;
33+
34+
/*
35+
* Not our target.
36+
*/
37+
38+
// Ordinary variable variables outside strings.
39+
$foo = ${'bar'};
40+
41+
// Heredoc without embeds.
42+
echo <<<EOD
43+
Some text
44+
EOD;
45+
46+
// Not actually interpolated - $ is escaped. The second $foo is to force T_DOUBLE_QUOTED_STRING tokenization.
47+
echo "\${foo} and $foo";
48+
echo "\${foo[\"bar\"]} and $foo";
49+
echo "$\{foo} and $foo";
50+
51+
52+
/*
53+
* PHP 8.2: deprecated forms of embedding variables.
54+
*/
55+
56+
// Type 3: Braces after the dollar sign.
57+
echo "${foo}";
58+
echo "${foo['bar']}";
59+
$text = "some text ${foo} some ${text}";
60+
61+
$heredoc = <<<EOD
62+
some text ${foo} some text
63+
EOD;
64+
65+
echo "\\${foo}"; // Not actually escaped, the backslash escapes the backslash, not the dollar sign.
66+
67+
// Type 4: Variable variables.
68+
echo "${$bar}";
69+
echo "${(foo)}";
70+
echo "${foo->bar}";
71+
echo "${$object->getMethod()}"
72+
$text = "some text ${(foo)} some text";
73+
echo "${substr('laruence', 0, 2)}";
74+
75+
echo "${foo["${bar}"]}";
76+
echo "${foo["${bar['baz']}"]}";
77+
echo "${foo->{$baz}}";
78+
echo "${foo->{${'a'}}}";
79+
echo "${foo->{"${'a'}"}}";
80+
81+
// Verify correct handling of stack pointers in multi-token code.
82+
$text = "Line without embed
83+
some text ${foo["${bar}"]} some text
84+
some text ${foo["${bar['baz']}"]} some text
85+
some text ${foo->{${'a'}}} some text
86+
";
87+
88+
$heredoc = <<<"EOD"
89+
some text ${(foo)} some text
90+
EOD;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
/**
4+
* The tests involving PHP 7.3+ indented heredocs are in a separate test case file
5+
* as any code after an indented heredoc will be tokenizer garbage on PHP < 7.3.
6+
*/
7+
8+
// No embeds.
9+
$php73IndentedHeredoc = <<<"EOD"
10+
some text some text
11+
EOD;
12+
13+
/*
14+
* Embedded variables which are supported cross-version.
15+
*/
16+
17+
// Type 1: directly embedding variables.
18+
$php73IndentedHeredoc = <<<"EOD"
19+
some text $foo[bar] some text
20+
EOD;
21+
22+
// Type 2: Braces around the variable/expression.
23+
$php73IndentedHeredoc = <<<EOD
24+
some text {${$bar}} some text
25+
EOD;
26+
27+
/*
28+
* PHP 8.2: deprecated forms of embedding variables.
29+
*/
30+
31+
// Type 3: Braces after the dollar sign.
32+
$php73IndentedHeredoc = <<<"EOD"
33+
some text ${foo['bar']} some text
34+
EOD;
35+
36+
// Type 4: Variable variables.
37+
$php73IndentedHeredoc = <<<EOD
38+
Line without embed
39+
some text ${$object->getMethod()} some text
40+
some text ${foo["${bar['baz']}"]} some text
41+
some text ${foo->{${'a'}}} some text
42+
EOD;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
<?php
2+
/**
3+
* PHPCompatibility, an external standard for PHP_CodeSniffer.
4+
*
5+
* @package PHPCompatibility
6+
* @copyright 2012-2022 PHPCompatibility Contributors
7+
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
8+
* @link https://github.com/PHPCompatibility/PHPCompatibility
9+
*/
10+
11+
namespace Magento2\Tests\PHPCompatibility;
12+
13+
/**
14+
* Test the RemovedDollarBraceStringEmbeds sniff.
15+
*
16+
* @group removedDollarBraceStringEmbeds
17+
* @group textstrings
18+
*
19+
* @covers \Magento2\Sniffs\PHPCompatibility\RemovedDollarBraceStringEmbedsSniff
20+
*
21+
* @since 10.0.0
22+
*/
23+
class RemovedDollarBraceStringEmbedsUnitTest extends BaseSniffTest
24+
{
25+
26+
/**
27+
* The name of the primary test case file.
28+
*
29+
* @var string
30+
*/
31+
const TEST_FILE = 'RemovedDollarBraceStringEmbedsUnitTest.1.inc';
32+
33+
/**
34+
* The name of a secondary test case file containing PHP 7.3+ indented heredocs.
35+
*
36+
* @var string
37+
*/
38+
const TEST_FILE_PHP73HEREDOCS = 'RemovedDollarBraceStringEmbedsUnitTest.2.inc';
39+
40+
/**
41+
* Test that variable embeds of "type 3" - Braces after the dollar sign (�${foo}�) -
42+
* are correctly detected.
43+
*
44+
* @dataProvider dataRemovedDollarBraceStringEmbedsType3
45+
*
46+
* @param int $line The line number.
47+
* @param string $found The embedded variable found.
48+
*
49+
* @return void
50+
*/
51+
public function testRemovedDollarBraceStringEmbedsType3($line, $found)
52+
{
53+
$file = $this->sniffFile(__DIR__ . '/' . self::TEST_FILE, '8.2');
54+
$this->assertWarning($file, $line, 'Using ${var} in strings is deprecated since PHP 8.2, use {$var} instead. Found: ' . $found);
55+
}
56+
57+
/**
58+
* Data provider.
59+
*
60+
* @see testRemovedDollarBraceStringEmbedsType3()
61+
*
62+
* @return array
63+
*/
64+
public function dataRemovedDollarBraceStringEmbedsType3()
65+
{
66+
return [
67+
[57, '${foo}'],
68+
[58, '${foo[\'bar\']}'],
69+
[59, '${foo}'],
70+
[59, '${text}'],
71+
[62, '${foo}'],
72+
[65, '${foo}'],
73+
];
74+
}
75+
76+
77+
/**
78+
* Test that variable embeds of "type 4" - Variable variables (�${expr}�, equivalent to
79+
* (string) ${expr}) - are correctly detected.
80+
*
81+
* @dataProvider dataRemovedDollarBraceStringEmbedsType4
82+
*
83+
* @param int $line The line number.
84+
* @param string $found The embedded expression found.
85+
*
86+
* @return void
87+
*/
88+
public function testRemovedDollarBraceStringEmbedsType4($line, $found)
89+
{
90+
$file = $this->sniffFile(__DIR__ . '/' . self::TEST_FILE, '8.2');
91+
$this->assertWarning($file, $line, "Using {$found} (variable variables) in strings is deprecated since PHP 8.2, use {\${expr}} instead.");
92+
}
93+
94+
/**
95+
* Data provider.
96+
*
97+
* @see testRemovedDollarBraceStringEmbedsType4()
98+
*
99+
* @return array
100+
*/
101+
public function dataRemovedDollarBraceStringEmbedsType4()
102+
{
103+
return [
104+
[68, '${$bar}'],
105+
[69, '${(foo)}'],
106+
[70, '${foo->bar}'],
107+
[71, '${$object->getMethod()}'],
108+
[72, '${(foo)}'],
109+
[73, '${substr(\'laruence\', 0, 2)}'],
110+
[75, '${foo["${bar}"]}'],
111+
[76, '${foo["${bar[\'baz\']}"]}'],
112+
[77, '${foo->{$baz}}'],
113+
[78, '${foo->{${\'a\'}}}'],
114+
[79, '${foo->{"${\'a\'}"}}'],
115+
[83, '${foo["${bar}"]}'],
116+
[84, '${foo["${bar[\'baz\']}"]}'],
117+
[85, '${foo->{${\'a\'}}}'],
118+
[89, '${(foo)}'],
119+
];
120+
}
121+
122+
123+
/**
124+
* Test that variable embeds of "type 3" - Braces after the dollar sign (�${foo}�) -
125+
* are correctly detected in PHP 7.3+ indented heredocs.
126+
*
127+
* @dataProvider dataRemovedDollarBraceStringEmbedsType3InIndentedHeredoc
128+
*
129+
* @param int $line The line number.
130+
* @param string $found The embedded variable found.
131+
*
132+
* @return void
133+
*/
134+
public function testRemovedDollarBraceStringEmbedsType3InIndentedHeredoc($line, $found)
135+
{
136+
if (\PHP_VERSION_ID < 70300) {
137+
$this->markTestSkipped('Test code involving PHP 7.3 heredocs will not tokenize correctly on PHP < 7.3');
138+
}
139+
140+
$file = $this->sniffFile(__DIR__ . '/' . self::TEST_FILE_PHP73HEREDOCS, '8.2');
141+
$this->assertWarning($file, $line, 'Using ${var} in strings is deprecated since PHP 8.2, use {$var} instead. Found: ' . $found);
142+
}
143+
144+
/**
145+
* Data provider.
146+
*
147+
* @see testRemovedDollarBraceStringEmbedsType3InIndentedHeredoc()
148+
*
149+
* @return array
150+
*/
151+
public function dataRemovedDollarBraceStringEmbedsType3InIndentedHeredoc()
152+
{
153+
return [
154+
[33, '${foo[\'bar\']}'],
155+
];
156+
}
157+
158+
159+
/**
160+
* Test that variable embeds of "type 4" - Variable variables (�${expr}�, equivalent to
161+
* (string) ${expr}) - are correctly detected in PHP 7.3+ indented heredocs.
162+
*
163+
* @dataProvider dataRemovedDollarBraceStringEmbedsType4InIndentedHeredoc
164+
*
165+
* @param int $line The line number.
166+
* @param string $found The embedded expression found.
167+
*
168+
* @return void
169+
*/
170+
public function testRemovedDollarBraceStringEmbedsType4InIndentedHeredoc($line, $found)
171+
{
172+
if (\PHP_VERSION_ID < 70300) {
173+
$this->markTestSkipped('Test code involving PHP 7.3 heredocs will not tokenize correctly on PHP < 7.3');
174+
}
175+
176+
$file = $this->sniffFile(__DIR__ . '/' . self::TEST_FILE_PHP73HEREDOCS, '8.2');
177+
$this->assertWarning($file, $line, "Using {$found} (variable variables) in strings is deprecated since PHP 8.2, use {\${expr}} instead.");
178+
}
179+
180+
/**
181+
* Data provider.
182+
*
183+
* @see testRemovedDollarBraceStringEmbedsType4InIndentedHeredoc()
184+
*
185+
* @return array
186+
*/
187+
public function dataRemovedDollarBraceStringEmbedsType4InIndentedHeredoc()
188+
{
189+
return [
190+
[39, '${$object->getMethod()}'],
191+
[40, '${foo["${bar[\'baz\']}"]}'],
192+
[41, '${foo->{${\'a\'}}}'],
193+
];
194+
}
195+
196+
197+
/**
198+
* Verify the sniff does not throw false positives for valid code.
199+
*
200+
* @dataProvider dataTestFiles
201+
*
202+
* @param string $testFile File name for the test case file to use.
203+
* @param int $lines Number of lines at the top of the file for which we don't expect errors.
204+
*
205+
* @return void
206+
*/
207+
public function testNoFalsePositives($testFile, $lines)
208+
{
209+
if ($testFile === self::TEST_FILE_PHP73HEREDOCS && \PHP_VERSION_ID < 70300) {
210+
$this->markTestSkipped('Test code involving PHP 7.3 heredocs will not tokenize correctly on PHP < 7.3');
211+
}
212+
213+
$file = $this->sniffFile(__DIR__ . '/' . $testFile, '8.2');
214+
215+
// No errors expected on the first # lines.
216+
for ($line = 1; $line <= $lines; $line++) {
217+
$this->assertNoViolation($file, $line);
218+
}
219+
}
220+
221+
222+
/**
223+
* Verify no notices are thrown at all.
224+
*
225+
* @dataProvider dataTestFiles
226+
*
227+
* @param string $testFile File name for the test case file to use.
228+
*
229+
* @return void
230+
*/
231+
public function testNoViolationsInFileOnValidVersion($testFile)
232+
{
233+
if ($testFile === self::TEST_FILE_PHP73HEREDOCS && \PHP_VERSION_ID < 70300) {
234+
$this->markTestSkipped('Test code involving PHP 7.3 heredocs will not tokenize correctly on PHP < 7.3');
235+
}
236+
237+
$file = $this->sniffFile(__DIR__ . '/' . $testFile, '8.1');
238+
$this->assertNoViolation($file);
239+
}
240+
241+
242+
/**
243+
* Data provider.
244+
*
245+
* @return array
246+
*/
247+
public function dataTestFiles()
248+
{
249+
return [
250+
[self::TEST_FILE, 51],
251+
[self::TEST_FILE_PHP73HEREDOCS, 26],
252+
];
253+
}
254+
}

‎Magento2/ruleset.xml

+1
Original file line numberDiff line numberDiff line change
@@ -783,4 +783,5 @@
783783
<rule ref="Magento2.PHPCompatibility.RemovedAssertStringAssertion" />
784784
<rule ref="Magento2.PHPCompatibility.RemovedGetDefinedFunctionsExcludeDisabledFalse" />
785785
<rule ref="Magento2.PHPCompatibility.RemovedSplAutoloadRegisterThrowFalse" />
786+
<rule ref="Magento2.PHPCompatibility.RemovedDollarBraceStringEmbeds" />
786787
</ruleset>

0 commit comments

Comments
 (0)
Please sign in to comment.