Skip to content

Commit 7a1c8b9

Browse files
authored
Merge pull request #937 from PHPCSStandards/feature/tokenizer-php-open-tag-end-of-file
PHP 7.4 | Tokenizer/PHP: handle PHP tag at end of file consistently
2 parents 5349ce9 + 43299a8 commit 7a1c8b9

File tree

8 files changed

+222
-1
lines changed

8 files changed

+222
-1
lines changed

.github/workflows/test.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ jobs:
9292
- php: '7.0'
9393
os: 'ubuntu-latest'
9494
custom_ini: true
95+
- php: '8.0'
96+
os: 'ubuntu-latest'
97+
custom_ini: true
98+
- php: '8.2'
99+
os: 'ubuntu-latest'
100+
custom_ini: true
95101

96102
# yamllint disable-line rule:line-length
97103
name: "PHP: ${{ matrix.php }} ${{ matrix.custom_ini && ' with custom ini settings' || '' }}${{ matrix.libxml_minor && format( ' with libxml {0}', matrix.libxml_minor ) || '' }} (${{ matrix.os == 'ubuntu-latest' && 'Linux' || 'Win' }})"
@@ -177,7 +183,7 @@ jobs:
177183
# Also turn on error_reporting to ensure all notices are shown.
178184
if [[ ${{ matrix.custom_ini }} == true && "${{ matrix.php }}" == '5.5' ]]; then
179185
echo 'PHP_INI=error_reporting=-1, display_errors=On, date.timezone=Australia/Sydney, short_open_tag=On, asp_tags=On' >> "$GITHUB_OUTPUT"
180-
elif [[ ${{ matrix.custom_ini }} == true && "${{ matrix.php }}" == '7.0' ]]; then
186+
elif [[ ${{ matrix.custom_ini }} == true && "${{ matrix.php }}" != '5.5' ]]; then
181187
echo 'PHP_INI=error_reporting=-1, display_errors=On, date.timezone=Australia/Sydney, short_open_tag=On' >> "$GITHUB_OUTPUT"
182188
else
183189
echo 'PHP_INI=error_reporting=-1, display_errors=On' >> "$GITHUB_OUTPUT"
@@ -233,6 +239,7 @@ jobs:
233239
PHP_CODESNIFFER_CBF: '1'
234240

235241
- name: 'PHPCS: check code style without cache, no parallel'
242+
if: ${{ matrix.custom_ini == false }}
236243
id: phpcs
237244
run: >
238245
php "bin/phpcs" --no-cache --parallel=1

src/Tokenizers/PHP.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,46 @@ protected function tokenize($string)
803803
}
804804
}//end if
805805

806+
/*
807+
Prior to PHP 7.4, PHP didn't support stand-alone PHP open tags at the end of a file
808+
(without a new line), so we need to make sure that the tokenization in PHPCS is consistent
809+
cross-version PHP by retokenizing to T_OPEN_TAG.
810+
*/
811+
812+
if (PHP_VERSION_ID < 70400
813+
&& $tokenIsArray === true
814+
// PHP < 7.4 with short open tags off.
815+
&& (($stackPtr === ($numTokens - 1)
816+
&& $token[0] === T_INLINE_HTML
817+
&& stripos($token[1], '<?php') === 0)
818+
// PHP < 7.4 with short open tags on.
819+
|| ($stackPtr === ($numTokens - 2)
820+
&& $token[0] === T_OPEN_TAG
821+
&& $token[1] === '<?'
822+
&& is_array($tokens[($stackPtr + 1)]) === true
823+
&& $tokens[($stackPtr + 1)][0] === T_STRING
824+
&& strtolower($tokens[($stackPtr + 1)][1]) === 'php'))
825+
) {
826+
if ($token[0] === T_INLINE_HTML) {
827+
$finalTokens[$newStackPtr] = [
828+
'code' => T_OPEN_TAG,
829+
'type' => 'T_OPEN_TAG',
830+
'content' => $token[1],
831+
];
832+
} else {
833+
$finalTokens[$newStackPtr] = [
834+
'code' => T_OPEN_TAG,
835+
'type' => 'T_OPEN_TAG',
836+
'content' => $token[1].$tokens[($stackPtr + 1)][1],
837+
];
838+
839+
$stackPtr++;
840+
}
841+
842+
$newStackPtr++;
843+
continue;
844+
}//end if
845+
806846
/*
807847
Parse doc blocks into something that can be easily iterated over.
808848
*/
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?php
2+
// This test case must be the last (only) test in the file without a new line after it!
3+
/* testLongOpenTagEndOfFileSpaceNoNewLine */ ?>
4+
<?php
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
/**
3+
* Tests the tokenization of PHP open tags.
4+
*
5+
* Prior to PHP 7.4, PHP didn't support stand-alone PHP open tags at the end of a file (without a new line),
6+
* so we need to make sure that the tokenization in PHPCS is consistent and correct.
7+
*
8+
* @copyright 2025 PHPCSStandards and contributors
9+
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
10+
*/
11+
12+
namespace PHP_CodeSniffer\Tests\Core\Tokenizers\PHP;
13+
14+
use PHP_CodeSniffer\Tests\Core\Tokenizers\AbstractTokenizerTestCase;
15+
use PHP_CodeSniffer\Util\Tokens;
16+
17+
/**
18+
* Tests the tokenization of PHP open tags.
19+
*
20+
* @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
21+
*/
22+
final class PHPOpenTagEOF1Test extends AbstractTokenizerTestCase
23+
{
24+
25+
26+
/**
27+
* Test that the tokenization of a long PHP open tag at the very end of a file is correct and consistent.
28+
*
29+
* @return void
30+
*/
31+
public function testLongOpenTagAtEndOfFile()
32+
{
33+
$tokens = $this->phpcsFile->getTokens();
34+
$stackPtr = $this->getTargetToken('/* testLongOpenTagEndOfFileSpaceNoNewLine */', [T_OPEN_TAG, T_STRING, T_INLINE_HTML]);
35+
36+
$this->assertSame(
37+
T_OPEN_TAG,
38+
$tokens[$stackPtr]['code'],
39+
'Token tokenized as '.Tokens::tokenName($tokens[$stackPtr]['code']).', not T_OPEN_TAG (code)'
40+
);
41+
$this->assertSame(
42+
'T_OPEN_TAG',
43+
$tokens[$stackPtr]['type'],
44+
'Token tokenized as '.$tokens[$stackPtr]['type'].', not T_OPEN_TAG (type)'
45+
);
46+
$this->assertSame('<?php ', $tokens[$stackPtr]['content']);
47+
48+
// Now make sure that this is the very last token in the file and there are no tokens after it.
49+
$this->assertArrayNotHasKey(($stackPtr + 1), $tokens);
50+
51+
}//end testLongOpenTagAtEndOfFile()
52+
53+
54+
}//end class
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?php
2+
// This test case must be the last (only) test in the file without a new line after it!
3+
/* testLongOpenTagEndOfFileNoSpaceNoNewLine */ ?>
4+
<?php
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
/**
3+
* Tests the tokenization of PHP open tags.
4+
*
5+
* Prior to PHP 7.4, PHP didn't support stand-alone PHP open tags at the end of a file (without a new line),
6+
* so we need to make sure that the tokenization in PHPCS is consistent and correct.
7+
*
8+
* @copyright 2025 PHPCSStandards and contributors
9+
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
10+
*/
11+
12+
namespace PHP_CodeSniffer\Tests\Core\Tokenizers\PHP;
13+
14+
use PHP_CodeSniffer\Tests\Core\Tokenizers\AbstractTokenizerTestCase;
15+
use PHP_CodeSniffer\Util\Tokens;
16+
17+
/**
18+
* Tests the tokenization of PHP open tags.
19+
*
20+
* @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
21+
*/
22+
final class PHPOpenTagEOF2Test extends AbstractTokenizerTestCase
23+
{
24+
25+
26+
/**
27+
* Test that the tokenization of a long PHP open tag at the very end of a file is correct and consistent.
28+
*
29+
* @return void
30+
*/
31+
public function testLongOpenTagAtEndOfFile()
32+
{
33+
$tokens = $this->phpcsFile->getTokens();
34+
$stackPtr = $this->getTargetToken('/* testLongOpenTagEndOfFileNoSpaceNoNewLine */', [T_OPEN_TAG, T_STRING, T_INLINE_HTML]);
35+
36+
$this->assertSame(
37+
T_OPEN_TAG,
38+
$tokens[$stackPtr]['code'],
39+
'Token tokenized as '.Tokens::tokenName($tokens[$stackPtr]['code']).', not T_OPEN_TAG (code)'
40+
);
41+
$this->assertSame(
42+
'T_OPEN_TAG',
43+
$tokens[$stackPtr]['type'],
44+
'Token tokenized as '.$tokens[$stackPtr]['type'].', not T_OPEN_TAG (type)'
45+
);
46+
$this->assertSame('<?php', $tokens[$stackPtr]['content']);
47+
48+
// Now make sure that this is the very last token in the file and there are no tokens after it.
49+
$this->assertArrayNotHasKey(($stackPtr + 1), $tokens);
50+
51+
}//end testLongOpenTagAtEndOfFile()
52+
53+
54+
}//end class
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?php
2+
// This test case must be the last (only) test in the file without a new line after it!
3+
/* testLongOpenTagEndOfFileNoSpaceNoNewLineUppercase */ ?>
4+
<?PHP
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
/**
3+
* Tests the tokenization of PHP open tags.
4+
*
5+
* Prior to PHP 7.4, PHP didn't support stand-alone PHP open tags at the end of a file (without a new line),
6+
* so we need to make sure that the tokenization in PHPCS is consistent and correct.
7+
*
8+
* @copyright 2025 PHPCSStandards and contributors
9+
* @license https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
10+
*/
11+
12+
namespace PHP_CodeSniffer\Tests\Core\Tokenizers\PHP;
13+
14+
use PHP_CodeSniffer\Tests\Core\Tokenizers\AbstractTokenizerTestCase;
15+
use PHP_CodeSniffer\Util\Tokens;
16+
17+
/**
18+
* Tests the tokenization of PHP open tags.
19+
*
20+
* @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
21+
*/
22+
final class PHPOpenTagEOF3Test extends AbstractTokenizerTestCase
23+
{
24+
25+
26+
/**
27+
* Test that the tokenization of a long PHP open tag at the very end of a file is correct and consistent.
28+
*
29+
* @return void
30+
*/
31+
public function testLongOpenTagAtEndOfFile()
32+
{
33+
$tokens = $this->phpcsFile->getTokens();
34+
$stackPtr = $this->getTargetToken('/* testLongOpenTagEndOfFileNoSpaceNoNewLineUppercase */', [T_OPEN_TAG, T_STRING, T_INLINE_HTML]);
35+
36+
$this->assertSame(
37+
T_OPEN_TAG,
38+
$tokens[$stackPtr]['code'],
39+
'Token tokenized as '.Tokens::tokenName($tokens[$stackPtr]['code']).', not T_OPEN_TAG (code)'
40+
);
41+
$this->assertSame(
42+
'T_OPEN_TAG',
43+
$tokens[$stackPtr]['type'],
44+
'Token tokenized as '.$tokens[$stackPtr]['type'].', not T_OPEN_TAG (type)'
45+
);
46+
$this->assertSame('<?PHP', $tokens[$stackPtr]['content']);
47+
48+
// Now make sure that this is the very last token in the file and there are no tokens after it.
49+
$this->assertArrayNotHasKey(($stackPtr + 1), $tokens);
50+
51+
}//end testLongOpenTagAtEndOfFile()
52+
53+
54+
}//end class

0 commit comments

Comments
 (0)