Skip to content

Commit 71f24e0

Browse files
committed
Remove T_NAME_QUALIFIED support from UseStatementSniff
Partially qualified names (e.g., Foo\Bar without leading \) cannot be safely auto-fixed because we cannot determine the intended full namespace path. Only fully qualified names (starting with \) are now auto-fixed. The sniff still supports: - PHP 8+ attributes (#[\Foo\Bar]) - PHP 8.1+ enum implements clause - All existing fully qualified name contexts
2 parents 65102a8 + f3d1f33 commit 71f24e0

8 files changed

Lines changed: 38 additions & 197 deletions

File tree

PhpCollective/Sniffs/Namespaces/UseStatementSniff.php

Lines changed: 34 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -161,13 +161,9 @@ protected function checkUseForAttribute(File $phpcsFile, int $stackPtr): void
161161
return;
162162
}
163163

164-
// PHP 8+: Check if it's T_NAME_FULLY_QUALIFIED or T_NAME_QUALIFIED token
165-
if (
166-
defined('T_NAME_FULLY_QUALIFIED') && (
167-
$this->isGivenKind(T_NAME_FULLY_QUALIFIED, $tokens[$nextIndex]) ||
168-
$this->isGivenKind(T_NAME_QUALIFIED, $tokens[$nextIndex])
169-
)
170-
) {
164+
// PHP 8+: Check if it's a single T_NAME_FULLY_QUALIFIED token
165+
// Note: T_NAME_QUALIFIED (partial names) are not auto-fixed as we can't determine the full namespace
166+
if (defined('T_NAME_FULLY_QUALIFIED') && $this->isGivenKind(T_NAME_FULLY_QUALIFIED, $tokens[$nextIndex])) {
171167
$extractedUseStatement = ltrim($tokens[$nextIndex]['content'], '\\');
172168
if (!str_contains($extractedUseStatement, '\\')) {
173169
return;
@@ -228,6 +224,12 @@ protected function fixStatement(File $phpcsFile, array $statement, int $stackPtr
228224
return;
229225
}
230226

227+
// Skip partial FQCNs (not starting with \) as we can't determine the full namespace
228+
$partial = !str_starts_with($statement['content'], '\\');
229+
if ($partial) {
230+
return;
231+
}
232+
231233
$extractedUseStatement = ltrim($statement['content'], '\\');
232234
$className = substr($statement['content'], strrpos($statement['content'], '\\') + 1);
233235

@@ -271,13 +273,9 @@ protected function checkUseForNew(File $phpcsFile, int $stackPtr): void
271273
return;
272274
}
273275

274-
// PHP 8+: Check if it's a single T_NAME_FULLY_QUALIFIED or T_NAME_QUALIFIED token
275-
if (
276-
defined('T_NAME_FULLY_QUALIFIED') && (
277-
$this->isGivenKind(T_NAME_FULLY_QUALIFIED, $tokens[$nextIndex]) ||
278-
$this->isGivenKind(T_NAME_QUALIFIED, $tokens[$nextIndex])
279-
)
280-
) {
276+
// PHP 8+: Check if it's a single T_NAME_FULLY_QUALIFIED token
277+
// Note: T_NAME_QUALIFIED (partial names) are not auto-fixed as we can't determine the full namespace
278+
if (defined('T_NAME_FULLY_QUALIFIED') && $this->isGivenKind(T_NAME_FULLY_QUALIFIED, $tokens[$nextIndex])) {
281279
$extractedUseStatement = ltrim($tokens[$nextIndex]['content'], '\\');
282280
if (!str_contains($extractedUseStatement, '\\')) {
283281
return;
@@ -389,13 +387,9 @@ protected function checkUseForStatic(File $phpcsFile, int $stackPtr): void
389387
return;
390388
}
391389

392-
// PHP 8+: Check if it's a single T_NAME_FULLY_QUALIFIED or T_NAME_QUALIFIED token
393-
if (
394-
defined('T_NAME_FULLY_QUALIFIED') && (
395-
$this->isGivenKind(T_NAME_FULLY_QUALIFIED, $tokens[$prevIndex]) ||
396-
$this->isGivenKind(T_NAME_QUALIFIED, $tokens[$prevIndex])
397-
)
398-
) {
390+
// PHP 8+: Check if it's a single T_NAME_FULLY_QUALIFIED token
391+
// Note: T_NAME_QUALIFIED (partial names) are not auto-fixed as we can't determine the full namespace
392+
if (defined('T_NAME_FULLY_QUALIFIED') && $this->isGivenKind(T_NAME_FULLY_QUALIFIED, $tokens[$prevIndex])) {
399393
$extractedUseStatement = ltrim($tokens[$prevIndex]['content'], '\\');
400394
if (!str_contains($extractedUseStatement, '\\')) {
401395
return;
@@ -499,13 +493,9 @@ protected function checkUseForInstanceOf(File $phpcsFile, int $stackPtr): void
499493
return;
500494
}
501495

502-
// PHP 8+: Check if it's a single T_NAME_FULLY_QUALIFIED or T_NAME_QUALIFIED token
503-
if (
504-
defined('T_NAME_FULLY_QUALIFIED') && (
505-
$this->isGivenKind(T_NAME_FULLY_QUALIFIED, $tokens[$classNameIndex]) ||
506-
$this->isGivenKind(T_NAME_QUALIFIED, $tokens[$classNameIndex])
507-
)
508-
) {
496+
// PHP 8+: Check if it's a single T_NAME_FULLY_QUALIFIED token
497+
// Note: T_NAME_QUALIFIED (partial names) are not auto-fixed as we can't determine the full namespace
498+
if (defined('T_NAME_FULLY_QUALIFIED') && $this->isGivenKind(T_NAME_FULLY_QUALIFIED, $tokens[$classNameIndex])) {
509499
$extractedUseStatement = ltrim($tokens[$classNameIndex]['content'], '\\');
510500
if (!str_contains($extractedUseStatement, '\\')) {
511501
return;
@@ -613,13 +603,9 @@ public function checkUseForCatchOrCallable(File $phpcsFile, int $stackPtr): void
613603
return;
614604
}
615605

616-
// PHP 8+: Check if it's a single T_NAME_FULLY_QUALIFIED or T_NAME_QUALIFIED token
617-
if (
618-
defined('T_NAME_FULLY_QUALIFIED') && (
619-
$this->isGivenKind(T_NAME_FULLY_QUALIFIED, $tokens[$classNameIndex]) ||
620-
$this->isGivenKind(T_NAME_QUALIFIED, $tokens[$classNameIndex])
621-
)
622-
) {
606+
// PHP 8+: Check if it's a single T_NAME_FULLY_QUALIFIED token
607+
// Note: T_NAME_QUALIFIED (partial names) are not auto-fixed as we can't determine the full namespace
608+
if (defined('T_NAME_FULLY_QUALIFIED') && $this->isGivenKind(T_NAME_FULLY_QUALIFIED, $tokens[$classNameIndex])) {
623609
$extractedUseStatement = ltrim($tokens[$classNameIndex]['content'], '\\');
624610
if (!str_contains($extractedUseStatement, '\\')) {
625611
return;
@@ -735,13 +721,9 @@ protected function checkUseForSignature(File $phpcsFile, int $stackPtr): void
735721
$startIndex = $i;
736722
}
737723

738-
// PHP 8+: Check if it's a single T_NAME_FULLY_QUALIFIED or T_NAME_QUALIFIED token
739-
if (
740-
defined('T_NAME_FULLY_QUALIFIED') && (
741-
$this->isGivenKind(T_NAME_FULLY_QUALIFIED, $tokens[$i]) ||
742-
$this->isGivenKind(T_NAME_QUALIFIED, $tokens[$i])
743-
)
744-
) {
724+
// PHP 8+: Check if it's a single T_NAME_FULLY_QUALIFIED token
725+
// Note: T_NAME_QUALIFIED (partial names) are not auto-fixed as we can't determine the full namespace
726+
if (defined('T_NAME_FULLY_QUALIFIED') && $this->isGivenKind(T_NAME_FULLY_QUALIFIED, $tokens[$i])) {
745727
$extractedUseStatement = ltrim($tokens[$i]['content'], '\\');
746728
if (!str_contains($extractedUseStatement, '\\')) {
747729
continue;
@@ -857,13 +839,9 @@ protected function checkUseForReturnTypeHint(File $phpcsFile, int $stackPtr): vo
857839
return;
858840
}
859841

860-
// PHP 8+: Check if it's a single T_NAME_FULLY_QUALIFIED or T_NAME_QUALIFIED token
861-
if (
862-
defined('T_NAME_FULLY_QUALIFIED') && (
863-
$this->isGivenKind(T_NAME_FULLY_QUALIFIED, $tokens[$startIndex]) ||
864-
$this->isGivenKind(T_NAME_QUALIFIED, $tokens[$startIndex])
865-
)
866-
) {
842+
// PHP 8+: Check if it's a single T_NAME_FULLY_QUALIFIED token
843+
// Note: T_NAME_QUALIFIED (partial names) are not auto-fixed as we can't determine the full namespace
844+
if (defined('T_NAME_FULLY_QUALIFIED') && $this->isGivenKind(T_NAME_FULLY_QUALIFIED, $tokens[$startIndex])) {
867845
$extractedUseStatement = ltrim($tokens[$startIndex]['content'], '\\');
868846
if (!str_contains($extractedUseStatement, '\\')) {
869847
return;
@@ -966,12 +944,10 @@ protected function checkPropertyForInstanceOf(File $phpcsFile, int $stackPtr): v
966944
return;
967945
}
968946

969-
// Handle T_NAME_FULLY_QUALIFIED or T_NAME_QUALIFIED token (PHP CodeSniffer v4)
947+
// Handle T_NAME_FULLY_QUALIFIED token (PHP CodeSniffer v4)
948+
// Note: T_NAME_QUALIFIED (partial names) are not auto-fixed as we can't determine the full namespace
970949
$className = '';
971-
if (
972-
$tokens[$startIndex]['code'] === T_NAME_FULLY_QUALIFIED ||
973-
(defined('T_NAME_QUALIFIED') && $tokens[$startIndex]['code'] === T_NAME_QUALIFIED)
974-
) {
950+
if ($tokens[$startIndex]['code'] === T_NAME_FULLY_QUALIFIED) {
975951
$extractedUseStatement = ltrim($tokens[$startIndex]['content'], '\\');
976952
if (!str_contains($extractedUseStatement, '\\')) {
977953
return; // Not a namespaced class
@@ -1031,7 +1007,7 @@ protected function checkPropertyForInstanceOf(File $phpcsFile, int $stackPtr): v
10311007
$phpcsFile->fixer->replaceToken($lastIndex, $addedUseStatement['alias']);
10321008
}
10331009
} else {
1034-
// PHP CodeSniffer v4: replace single T_NAME_FULLY_QUALIFIED or T_NAME_QUALIFIED token
1010+
// PHP CodeSniffer v4: replace single T_NAME_FULLY_QUALIFIED token
10351011
if ($addedUseStatement['alias'] !== null) {
10361012
$phpcsFile->fixer->replaceToken($startIndex, $addedUseStatement['alias']);
10371013
} else {
@@ -1365,13 +1341,9 @@ protected function parse(File $phpcsFile, int $startIndex, int $endIndex): array
13651341
break;
13661342
}
13671343

1368-
// PHP 8+: Check for T_NAME_FULLY_QUALIFIED or T_NAME_QUALIFIED token
1369-
if (
1370-
defined('T_NAME_FULLY_QUALIFIED') && (
1371-
$tokens[$i]['code'] === T_NAME_FULLY_QUALIFIED ||
1372-
$tokens[$i]['code'] === T_NAME_QUALIFIED
1373-
)
1374-
) {
1344+
// PHP 8+: Check for T_NAME_FULLY_QUALIFIED token
1345+
// Note: T_NAME_QUALIFIED (partial names) are not auto-fixed as we can't determine the full namespace
1346+
if (defined('T_NAME_FULLY_QUALIFIED') && $tokens[$i]['code'] === T_NAME_FULLY_QUALIFIED) {
13751347
$implements[] = [
13761348
'start' => $i,
13771349
'end' => $i,

tests/PhpCollective/Sniffs/Namespaces/UseStatementSniffTest.php

Lines changed: 4 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,16 @@
1515
*
1616
* This sniff has been updated with the following fixes ported from PSR2R NoInlineFullyQualifiedClassName:
1717
* - PHP 8+ T_NAME_FULLY_QUALIFIED token support in all check methods
18-
* - PHP 8+ T_NAME_QUALIFIED token support for partially qualified names (e.g., Foo\Bar::method())
1918
* - PHP 8+ T_NAME_FULLY_QUALIFIED token support in parse() method for extends/implements
19+
* - PHP 8+ attribute and enum support
2020
* - shortName fallback when alias is null (prevents undefined replacements)
2121
* - str_contains() instead of deprecated strpos()
2222
*
2323
* The fixes ensure the sniff works correctly on PHP 8+ where inline FQCNs like \Foo\Bar\Class
2424
* are tokenized as a single T_NAME_FULLY_QUALIFIED token instead of multiple T_NS_SEPARATOR + T_STRING tokens.
25-
* Partially qualified names like Foo\Bar\Class are tokenized as T_NAME_QUALIFIED.
25+
*
26+
* Note: Partially qualified names (T_NAME_QUALIFIED, e.g., Foo\Bar without leading \) are NOT auto-fixed
27+
* because we cannot determine the intended full namespace path.
2628
*/
2729
class UseStatementSniffTest extends TestCase
2830
{
@@ -134,28 +136,6 @@ public function testStaticFixer(): void
134136
$this->prefix = null;
135137
}
136138

137-
/**
138-
* Tests static call with PHP 8+ T_NAME_QUALIFIED (partially qualified name).
139-
*
140-
* @return void
141-
*/
142-
public function testStaticQualifiedSniffer(): void
143-
{
144-
$this->prefix = 'static-qualified-';
145-
$this->assertSnifferFindsErrors(new UseStatementSniff(), 1);
146-
$this->prefix = null;
147-
}
148-
149-
/**
150-
* @return void
151-
*/
152-
public function testStaticQualifiedFixer(): void
153-
{
154-
$this->prefix = 'static-qualified-';
155-
$this->assertSnifferCanFixErrors(new UseStatementSniff());
156-
$this->prefix = null;
157-
}
158-
159139
/**
160140
* Tests instanceof with PHP 8+ T_NAME_FULLY_QUALIFIED (checkUseForInstanceOf() fix).
161141
*
@@ -266,28 +246,6 @@ public function testAttributeFixer(): void
266246
$this->prefix = null;
267247
}
268248

269-
/**
270-
* Tests PHP 8+ attribute with T_NAME_QUALIFIED (partially qualified name).
271-
*
272-
* @return void
273-
*/
274-
public function testAttributeQualifiedSniffer(): void
275-
{
276-
$this->prefix = 'attribute-qualified-';
277-
$this->assertSnifferFindsErrors(new UseStatementSniff(), 1);
278-
$this->prefix = null;
279-
}
280-
281-
/**
282-
* @return void
283-
*/
284-
public function testAttributeQualifiedFixer(): void
285-
{
286-
$this->prefix = 'attribute-qualified-';
287-
$this->assertSnifferCanFixErrors(new UseStatementSniff());
288-
$this->prefix = null;
289-
}
290-
291249
/**
292250
* Tests PHP 8.1+ enum implements with T_NAME_FULLY_QUALIFIED.
293251
*
@@ -309,26 +267,4 @@ public function testEnumFixer(): void
309267
$this->assertSnifferCanFixErrors(new UseStatementSniff());
310268
$this->prefix = null;
311269
}
312-
313-
/**
314-
* Tests PHP 8.1+ enum implements with T_NAME_QUALIFIED (partially qualified name).
315-
*
316-
* @return void
317-
*/
318-
public function testEnumQualifiedSniffer(): void
319-
{
320-
$this->prefix = 'enum-qualified-';
321-
$this->assertSnifferFindsErrors(new UseStatementSniff(), 1);
322-
$this->prefix = null;
323-
}
324-
325-
/**
326-
* @return void
327-
*/
328-
public function testEnumQualifiedFixer(): void
329-
{
330-
$this->prefix = 'enum-qualified-';
331-
$this->assertSnifferCanFixErrors(new UseStatementSniff());
332-
$this->prefix = null;
333-
}
334270
}

tests/_data/UseStatement/attribute-qualified-after.php

Lines changed: 0 additions & 10 deletions
This file was deleted.

tests/_data/UseStatement/attribute-qualified-before.php

Lines changed: 0 additions & 9 deletions
This file was deleted.

tests/_data/UseStatement/enum-qualified-after.php

Lines changed: 0 additions & 10 deletions
This file was deleted.

tests/_data/UseStatement/enum-qualified-before.php

Lines changed: 0 additions & 9 deletions
This file was deleted.

tests/_data/UseStatement/static-qualified-after.php

Lines changed: 0 additions & 15 deletions
This file was deleted.

tests/_data/UseStatement/static-qualified-before.php

Lines changed: 0 additions & 14 deletions
This file was deleted.

0 commit comments

Comments
 (0)