Skip to content

Commit 26a545f

Browse files
committed
fix bug in attributes tokenization on PHP < 8.0 (#3294)
1 parent ffced0d commit 26a545f

File tree

3 files changed

+180
-7
lines changed

3 files changed

+180
-7
lines changed

src/Tokenizers/PHP.php

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3213,14 +3213,27 @@ private function parsePhpAttribute(array &$tokens, $stackPtr)
32133213

32143214
// Go looking for the close bracket.
32153215
$bracketCloser = $this->findCloser($subTokens, 1, '[', ']');
3216-
if ($bracketCloser === null) {
3217-
$bracketCloser = $this->findCloser($tokens, $stackPtr, '[', ']');
3218-
if ($bracketCloser === null) {
3219-
return null;
3216+
if (PHP_VERSION_ID < 80000 && $bracketCloser === null) {
3217+
foreach (array_slice($tokens, ($stackPtr + 1)) as $token) {
3218+
if (is_array($token) === true) {
3219+
$commentBody .= $token[1];
3220+
} else {
3221+
$commentBody .= $token;
3222+
}
3223+
}
3224+
3225+
$subTokens = @token_get_all('<?php '.$commentBody);
3226+
array_splice($subTokens, 0, 1, [[T_ATTRIBUTE, '#[']]);
3227+
3228+
$bracketCloser = $this->findCloser($subTokens, 1, '[', ']');
3229+
if ($bracketCloser !== null) {
3230+
array_splice($tokens, ($stackPtr + 1), count($tokens), array_slice($subTokens, ($bracketCloser + 1)));
3231+
$subTokens = array_slice($subTokens, 0, ($bracketCloser + 1));
32203232
}
3233+
}
32213234

3222-
$subTokens = array_merge($subTokens, array_slice($tokens, ($stackPtr + 1), ($bracketCloser - $stackPtr)));
3223-
array_splice($tokens, ($stackPtr + 1), ($bracketCloser - $stackPtr));
3235+
if ($bracketCloser === null) {
3236+
return null;
32243237
}
32253238

32263239
return $subTokens;

tests/Core/Tokenizer/AttributesTest.inc

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,16 @@ function multiline_attributes_on_parameter_test(#[
7575
)
7676
] int $param) {}
7777

78+
/* testAttributeContainingTextLookingLikeCloseTag */
79+
#[DeprecationReason('reason: <https://some-website/reason?>')]
80+
function attribute_containing_text_looking_like_close_tag() {}
81+
82+
/* testAttributeContainingMultilineTextLookingLikeCloseTag */
83+
#[DeprecationReason(
84+
'reason: <https://some-website/reason?>'
85+
)]
86+
function attribute_containing_mulitline_text_looking_like_close_tag() {}
87+
7888
/* testInvalidAttribute */
7989
#[ThisIsNotAnAttribute
8090
function invalid_attribute_test() {}
81-

tests/Core/Tokenizer/AttributesTest.php

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,157 @@ public function dataAttributeOnParameters()
395395
}//end dataAttributeOnParameters()
396396

397397

398+
/**
399+
* Test that an attribute containing text which looks like a PHP close tag is tokenized correctly.
400+
*
401+
* @param string $testMarker The comment which prefaces the target token in the test file.
402+
* @param int $length The number of tokens between opener and closer.
403+
* @param array $expectedTokensAttribute The codes of tokens inside the attributes.
404+
* @param array $expectedTokensAfter The codes of tokens after the attributes.
405+
*
406+
* @covers PHP_CodeSniffer\Tokenizers\PHP::parsePhpAttribute
407+
*
408+
* @dataProvider dataAttributeOnTextLookingLikeCloseTag
409+
*
410+
* @return void
411+
*/
412+
public function testAttributeContainingTextLookingLikeCloseTag($testMarker, $length, array $expectedTokensAttribute, array $expectedTokensAfter)
413+
{
414+
$tokens = self::$phpcsFile->getTokens();
415+
416+
$attribute = $this->getTargetToken($testMarker, T_ATTRIBUTE);
417+
418+
$this->assertSame('T_ATTRIBUTE', $tokens[$attribute]['type']);
419+
$this->assertArrayHasKey('attribute_closer', $tokens[$attribute]);
420+
421+
$closer = $tokens[$attribute]['attribute_closer'];
422+
$this->assertSame(($attribute + $length), $closer);
423+
$this->assertSame(T_ATTRIBUTE_END, $tokens[$closer]['code']);
424+
$this->assertSame('T_ATTRIBUTE_END', $tokens[$closer]['type']);
425+
426+
$this->assertSame($tokens[$attribute]['attribute_opener'], $tokens[$closer]['attribute_opener']);
427+
$this->assertSame($tokens[$attribute]['attribute_closer'], $tokens[$closer]['attribute_closer']);
428+
429+
$i = ($attribute + 1);
430+
foreach ($expectedTokensAttribute as $item) {
431+
list($expectedType, $expectedContents) = $item;
432+
$this->assertSame($expectedType, $tokens[$i]['type']);
433+
$this->assertSame($expectedContents, $tokens[$i]['content']);
434+
$this->assertArrayHasKey('attribute_opener', $tokens[$i]);
435+
$this->assertArrayHasKey('attribute_closer', $tokens[$i]);
436+
++$i;
437+
}
438+
439+
$i = ($closer + 1);
440+
foreach ($expectedTokensAfter as $expectedCode) {
441+
$this->assertSame($expectedCode, $tokens[$i]['code']);
442+
++$i;
443+
}
444+
445+
}//end testAttributeContainingTextLookingLikeCloseTag()
446+
447+
448+
/**
449+
* Data provider.
450+
*
451+
* @see dataAttributeOnTextLookingLikeCloseTag()
452+
*
453+
* @return array
454+
*/
455+
public function dataAttributeOnTextLookingLikeCloseTag()
456+
{
457+
return [
458+
[
459+
'/* testAttributeContainingTextLookingLikeCloseTag */',
460+
5,
461+
[
462+
[
463+
'T_STRING',
464+
'DeprecationReason',
465+
],
466+
[
467+
'T_OPEN_PARENTHESIS',
468+
'(',
469+
],
470+
[
471+
'T_CONSTANT_ENCAPSED_STRING',
472+
"'reason: <https://some-website/reason?>'",
473+
],
474+
[
475+
'T_CLOSE_PARENTHESIS',
476+
')',
477+
],
478+
[
479+
'T_ATTRIBUTE_END',
480+
']',
481+
],
482+
],
483+
[
484+
T_WHITESPACE,
485+
T_FUNCTION,
486+
T_WHITESPACE,
487+
T_STRING,
488+
T_OPEN_PARENTHESIS,
489+
T_CLOSE_PARENTHESIS,
490+
T_WHITESPACE,
491+
T_OPEN_CURLY_BRACKET,
492+
T_CLOSE_CURLY_BRACKET,
493+
],
494+
],
495+
[
496+
'/* testAttributeContainingMultilineTextLookingLikeCloseTag */',
497+
8,
498+
[
499+
[
500+
'T_STRING',
501+
'DeprecationReason',
502+
],
503+
[
504+
'T_OPEN_PARENTHESIS',
505+
'(',
506+
],
507+
[
508+
'T_WHITESPACE',
509+
"\n",
510+
],
511+
[
512+
'T_WHITESPACE',
513+
" ",
514+
],
515+
[
516+
'T_CONSTANT_ENCAPSED_STRING',
517+
"'reason: <https://some-website/reason?>'",
518+
],
519+
[
520+
'T_WHITESPACE',
521+
"\n",
522+
],
523+
[
524+
'T_CLOSE_PARENTHESIS',
525+
')',
526+
],
527+
[
528+
'T_ATTRIBUTE_END',
529+
']',
530+
],
531+
],
532+
[
533+
T_WHITESPACE,
534+
T_FUNCTION,
535+
T_WHITESPACE,
536+
T_STRING,
537+
T_OPEN_PARENTHESIS,
538+
T_CLOSE_PARENTHESIS,
539+
T_WHITESPACE,
540+
T_OPEN_CURLY_BRACKET,
541+
T_CLOSE_CURLY_BRACKET,
542+
],
543+
],
544+
];
545+
546+
}//end dataAttributeOnTextLookingLikeCloseTag()
547+
548+
398549
/**
399550
* Test that invalid attribute (or comment starting with #[ and without ]) are parsed correctly.
400551
*

0 commit comments

Comments
 (0)