Skip to content

Commit e172e3e

Browse files
committed
Merge branch '3.x' into 4.x
2 parents efd671a + bc5755a commit e172e3e

11 files changed

+400
-88
lines changed

src/Tokenizers/PHP.php

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1510,15 +1510,11 @@ protected function tokenize(string $code)
15101510
&& strpos($token[1], '#[') === 0
15111511
) {
15121512
$subTokens = $this->parsePhpAttribute($tokens, $stackPtr);
1513-
if ($subTokens !== null) {
1514-
array_splice($tokens, $stackPtr, 1, $subTokens);
1515-
$numTokens = count($tokens);
1513+
array_splice($tokens, $stackPtr, 1, $subTokens);
1514+
$numTokens = count($tokens);
15161515

1517-
$tokenIsArray = true;
1518-
$token = $tokens[$stackPtr];
1519-
} else {
1520-
$token[0] = T_ATTRIBUTE;
1521-
}
1516+
$tokenIsArray = true;
1517+
$token = $tokens[$stackPtr];
15221518
}
15231519

15241520
if ($tokenIsArray === true
@@ -3984,11 +3980,10 @@ private function findCloser(array &$tokens, int $start, $openerTokens, string $c
39843980
* @param array $tokens The original array of tokens (as returned by token_get_all).
39853981
* @param int $stackPtr The current position in token array.
39863982
*
3987-
* @return array|null The array of parsed attribute tokens
3983+
* @return array The array of parsed attribute tokens
39883984
*/
39893985
private function parsePhpAttribute(array &$tokens, int $stackPtr)
39903986
{
3991-
39923987
$token = $tokens[$stackPtr];
39933988

39943989
$commentBody = substr($token[1], 2);
@@ -4000,18 +3995,24 @@ private function parsePhpAttribute(array &$tokens, int $stackPtr)
40003995
&& strpos($subToken[1], '#[') === 0
40013996
) {
40023997
$reparsed = $this->parsePhpAttribute($subTokens, $i);
4003-
if ($reparsed !== null) {
4004-
array_splice($subTokens, $i, 1, $reparsed);
4005-
} else {
4006-
$subToken[0] = T_ATTRIBUTE;
4007-
}
3998+
array_splice($subTokens, $i, 1, $reparsed);
40083999
}
40094000
}
40104001

40114002
array_splice($subTokens, 0, 1, [[T_ATTRIBUTE, '#[']]);
40124003

40134004
// Go looking for the close bracket.
40144005
$bracketCloser = $this->findCloser($subTokens, 1, '[', ']');
4006+
4007+
/*
4008+
* No closer bracket found, this might be a multi-line attribute,
4009+
* but it could also be an unfinished attribute (parse error).
4010+
*
4011+
* If it is a multi-line attribute, we need to grab a larger part of the code.
4012+
* If it is a parse error, we need to stick with only handling the line
4013+
* containing the attribute opener.
4014+
*/
4015+
40154016
if (PHP_VERSION_ID < 80000 && $bracketCloser === null) {
40164017
foreach (array_slice($tokens, ($stackPtr + 1)) as $token) {
40174018
if (is_array($token) === true) {
@@ -4021,20 +4022,17 @@ private function parsePhpAttribute(array &$tokens, int $stackPtr)
40214022
}
40224023
}
40234024

4024-
$subTokens = @token_get_all('<?php ' . $commentBody);
4025-
array_splice($subTokens, 0, 1, [[T_ATTRIBUTE, '#[']]);
4025+
$newSubTokens = @token_get_all('<?php ' . $commentBody);
4026+
array_splice($newSubTokens, 0, 1, [[T_ATTRIBUTE, '#[']]);
40264027

4027-
$bracketCloser = $this->findCloser($subTokens, 1, '[', ']');
4028+
$bracketCloser = $this->findCloser($newSubTokens, 1, '[', ']');
40284029
if ($bracketCloser !== null) {
4029-
array_splice($tokens, ($stackPtr + 1), count($tokens), array_slice($subTokens, ($bracketCloser + 1)));
4030-
$subTokens = array_slice($subTokens, 0, ($bracketCloser + 1));
4030+
// We found the closer, overwrite the original $subTokens array.
4031+
array_splice($tokens, ($stackPtr + 1), count($tokens), array_slice($newSubTokens, ($bracketCloser + 1)));
4032+
$subTokens = array_slice($newSubTokens, 0, ($bracketCloser + 1));
40314033
}
40324034
}
40334035

4034-
if ($bracketCloser === null) {
4035-
return null;
4036-
}
4037-
40384036
return $subTokens;
40394037
}
40404038

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
// Intentional parse error.
4+
// This must be the only test in the file.
5+
6+
/* testInvalidAttribute */
7+
#[ThisIsNotAnAttribute
8+
function invalid_attribute_test() {}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
/**
3+
* Tests the support of PHP 8 attributes
4+
*
5+
* @author Alessandro Chitolina <alekitto@gmail.com>
6+
* @author Juliette Reinders Folmer <phpcs_nospam@adviesenzo.nl>
7+
* @copyright 2019-2023 Squiz Pty Ltd (ABN 77 084 670 600)
8+
* @copyright 2023 PHPCSStandards and contributors
9+
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/HEAD/licence.txt BSD Licence
10+
*/
11+
12+
namespace PHP_CodeSniffer\Tests\Core\Tokenizers\PHP;
13+
14+
use PHP_CodeSniffer\Tests\Core\Tokenizers\AbstractTokenizerTestCase;
15+
16+
final class AttributesParseError1Test extends AbstractTokenizerTestCase
17+
{
18+
19+
20+
/**
21+
* Test that invalid attribute (or comment starting with #[ and without ]) are parsed correctly
22+
* and that tokens "within" the attribute are not removed.
23+
*
24+
* @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
25+
* @covers PHP_CodeSniffer\Tokenizers\PHP::findCloser
26+
* @covers PHP_CodeSniffer\Tokenizers\PHP::parsePhpAttribute
27+
*
28+
* @return void
29+
*/
30+
public function testInvalidAttribute()
31+
{
32+
$tokens = $this->phpcsFile->getTokens();
33+
34+
$attribute = $this->getTargetToken('/* testInvalidAttribute */', T_ATTRIBUTE);
35+
36+
$this->assertArrayHasKey('attribute_closer', $tokens[$attribute]);
37+
$this->assertNull($tokens[$attribute]['attribute_closer']);
38+
39+
$expectedTokenCodes = [
40+
'T_ATTRIBUTE',
41+
'T_STRING',
42+
'T_WHITESPACE',
43+
'T_FUNCTION',
44+
];
45+
$length = count($expectedTokenCodes);
46+
47+
$map = array_map(
48+
function ($token) {
49+
if ($token['code'] === T_ATTRIBUTE) {
50+
$this->assertArrayHasKey('attribute_closer', $token);
51+
$this->assertNull($token['attribute_closer']);
52+
} else {
53+
$this->assertArrayNotHasKey('attribute_closer', $token);
54+
}
55+
56+
return $token['type'];
57+
},
58+
array_slice($tokens, $attribute, $length)
59+
);
60+
61+
$this->assertSame($expectedTokenCodes, $map);
62+
}
63+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
// Intentional parse error.
4+
// This must be the only test in the file.
5+
6+
/* testLiveCoding */
7+
#[AttributeName(10)
8+
function hasUnfinishedAttribute() {}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
/**
3+
* Tests the support of PHP 8 attributes
4+
*
5+
* @copyright 2025 PHPCSStandards and contributors
6+
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/HEAD/licence.txt BSD Licence
7+
*/
8+
9+
namespace PHP_CodeSniffer\Tests\Core\Tokenizers\PHP;
10+
11+
use PHP_CodeSniffer\Tests\Core\Tokenizers\AbstractTokenizerTestCase;
12+
13+
final class AttributesParseError2Test extends AbstractTokenizerTestCase
14+
{
15+
16+
17+
/**
18+
* Test that invalid attribute (or comment starting with #[ and without ]) are parsed correctly
19+
* and that tokens "within" the attribute are not removed.
20+
*
21+
* @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
22+
* @covers PHP_CodeSniffer\Tokenizers\PHP::findCloser
23+
* @covers PHP_CodeSniffer\Tokenizers\PHP::parsePhpAttribute
24+
*
25+
* @return void
26+
*/
27+
public function testInvalidAttribute()
28+
{
29+
$tokens = $this->phpcsFile->getTokens();
30+
31+
$attribute = $this->getTargetToken('/* testLiveCoding */', T_ATTRIBUTE);
32+
33+
$this->assertArrayHasKey('attribute_closer', $tokens[$attribute]);
34+
$this->assertNull($tokens[$attribute]['attribute_closer']);
35+
36+
$expectedTokenCodes = [
37+
'T_ATTRIBUTE',
38+
'T_STRING',
39+
'T_OPEN_PARENTHESIS',
40+
'T_LNUMBER',
41+
'T_CLOSE_PARENTHESIS',
42+
'T_WHITESPACE',
43+
'T_FUNCTION',
44+
];
45+
46+
$length = count($expectedTokenCodes);
47+
48+
$map = array_map(
49+
function ($token) {
50+
if ($token['code'] === T_ATTRIBUTE) {
51+
$this->assertArrayHasKey('attribute_closer', $token);
52+
$this->assertNull($token['attribute_closer']);
53+
} else {
54+
$this->assertArrayNotHasKey('attribute_closer', $token);
55+
}
56+
57+
return $token['type'];
58+
},
59+
array_slice($tokens, $attribute, $length)
60+
);
61+
62+
$this->assertSame($expectedTokenCodes, $map);
63+
}
64+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
// Intentional parse error.
4+
// This must be the only test in the file.
5+
6+
class LiveCoding {
7+
/* testLiveCoding */
8+
#[AttributeName(10), SecondAttribute(
9+
public final function hasUnfinishedAttribute() {}
10+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
/**
3+
* Tests the support of PHP 8 attributes
4+
*
5+
* @copyright 2025 PHPCSStandards and contributors
6+
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/HEAD/licence.txt BSD Licence
7+
*/
8+
9+
namespace PHP_CodeSniffer\Tests\Core\Tokenizers\PHP;
10+
11+
use PHP_CodeSniffer\Tests\Core\Tokenizers\AbstractTokenizerTestCase;
12+
13+
final class AttributesParseError3Test extends AbstractTokenizerTestCase
14+
{
15+
16+
17+
/**
18+
* Test that invalid attribute (or comment starting with #[ and without ]) are parsed correctly
19+
* and that tokens "within" the attribute are not removed.
20+
*
21+
* @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
22+
* @covers PHP_CodeSniffer\Tokenizers\PHP::findCloser
23+
* @covers PHP_CodeSniffer\Tokenizers\PHP::parsePhpAttribute
24+
*
25+
* @return void
26+
*/
27+
public function testInvalidAttribute()
28+
{
29+
$tokens = $this->phpcsFile->getTokens();
30+
31+
$attribute = $this->getTargetToken('/* testLiveCoding */', T_ATTRIBUTE);
32+
33+
$this->assertArrayHasKey('attribute_closer', $tokens[$attribute]);
34+
$this->assertNull($tokens[$attribute]['attribute_closer']);
35+
36+
$expectedTokenCodes = [
37+
'T_ATTRIBUTE',
38+
'T_STRING',
39+
'T_OPEN_PARENTHESIS',
40+
'T_LNUMBER',
41+
'T_CLOSE_PARENTHESIS',
42+
'T_COMMA',
43+
'T_WHITESPACE',
44+
'T_STRING',
45+
'T_OPEN_PARENTHESIS',
46+
'T_WHITESPACE',
47+
'T_WHITESPACE',
48+
'T_PUBLIC',
49+
'T_WHITESPACE',
50+
'T_FINAL',
51+
'T_WHITESPACE',
52+
'T_FUNCTION',
53+
];
54+
55+
$length = count($expectedTokenCodes);
56+
57+
$map = array_map(
58+
function ($token) {
59+
if ($token['code'] === T_ATTRIBUTE) {
60+
$this->assertArrayHasKey('attribute_closer', $token);
61+
$this->assertNull($token['attribute_closer']);
62+
} else {
63+
$this->assertArrayNotHasKey('attribute_closer', $token);
64+
}
65+
66+
return $token['type'];
67+
},
68+
array_slice($tokens, $attribute, $length)
69+
);
70+
71+
$this->assertSame($expectedTokenCodes, $map);
72+
}
73+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
// Intentional parse error.
4+
// This must be the only test in the file.
5+
6+
/* testLiveCoding */
7+
#[ClosedAttribute] #[UnfinishedAttribute
8+
function hasUnfinishedAttribute() {}

0 commit comments

Comments
 (0)