Skip to content

Commit dca5a77

Browse files
committed
Fix up NoInlineFullyQualifiedClassNameSniff
1 parent 5767c24 commit dca5a77

File tree

4 files changed

+307
-4
lines changed

4 files changed

+307
-4
lines changed

PSR2R/Sniffs/Namespaces/NoInlineFullyQualifiedClassNameSniff.php

Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class NoInlineFullyQualifiedClassNameSniff extends AbstractSniff {
5151
* @inheritDoc
5252
*/
5353
public function register(): array {
54-
return [T_NEW, T_FUNCTION, T_DOUBLE_COLON, T_CLASS];
54+
return [T_NEW, T_FUNCTION, T_DOUBLE_COLON, T_CLASS, T_INSTANCEOF, T_CATCH];
5555
}
5656

5757
/**
@@ -494,6 +494,40 @@ protected function checkUseForNew(File $phpcsFile, int $stackPtr): void {
494494
$tokens = $phpcsFile->getTokens();
495495

496496
$nextIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true);
497+
498+
// PHP 8+: Check if it's a single T_NAME_FULLY_QUALIFIED token
499+
if (defined('T_NAME_FULLY_QUALIFIED') && $this->isGivenKind(T_NAME_FULLY_QUALIFIED, $tokens[$nextIndex])) {
500+
$extractedUseStatement = ltrim($tokens[$nextIndex]['content'], '\\');
501+
if (!str_contains($extractedUseStatement, '\\')) {
502+
return;
503+
}
504+
505+
$className = $this->extractClassNameFromUseStatementAsString($extractedUseStatement);
506+
$error = 'Use statement ' . $extractedUseStatement . ' for ' . $className . ' should be in use block.';
507+
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'New');
508+
if (!$fix) {
509+
return;
510+
}
511+
512+
$phpcsFile->fixer->beginChangeset();
513+
$addedUseStatement = $this->addUseStatement($className, $extractedUseStatement);
514+
515+
if ($addedUseStatement['alias'] !== null) {
516+
$phpcsFile->fixer->replaceToken($nextIndex, $addedUseStatement['alias']);
517+
} else {
518+
$phpcsFile->fixer->replaceToken($nextIndex, $addedUseStatement['shortName']);
519+
}
520+
521+
if ($nextIndex === $stackPtr + 1) {
522+
$phpcsFile->fixer->replaceToken($stackPtr, $tokens[$stackPtr]['content'] . ' ');
523+
}
524+
525+
$phpcsFile->fixer->endChangeset();
526+
527+
return;
528+
}
529+
530+
// PHP < 8: Multi-token format (T_NS_SEPARATOR + T_STRING)
497531
$lastIndex = null;
498532
$i = $nextIndex;
499533
$extractedUseStatement = '';
@@ -570,6 +604,35 @@ protected function checkUseForStatic(File $phpcsFile, int $stackPtr): void {
570604

571605
$prevIndex = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true);
572606

607+
// PHP 8+: Check if it's a single T_NAME_FULLY_QUALIFIED token
608+
if (defined('T_NAME_FULLY_QUALIFIED') && $this->isGivenKind(T_NAME_FULLY_QUALIFIED, $tokens[$prevIndex])) {
609+
$extractedUseStatement = ltrim($tokens[$prevIndex]['content'], '\\');
610+
if (!str_contains($extractedUseStatement, '\\')) {
611+
return;
612+
}
613+
614+
$className = $this->extractClassNameFromUseStatementAsString($extractedUseStatement);
615+
$error = 'Use statement ' . $extractedUseStatement . ' for ' . $className . ' should be in use block.';
616+
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'Static');
617+
if (!$fix) {
618+
return;
619+
}
620+
621+
$phpcsFile->fixer->beginChangeset();
622+
$addedUseStatement = $this->addUseStatement($className, $extractedUseStatement);
623+
624+
if ($addedUseStatement['alias'] !== null) {
625+
$phpcsFile->fixer->replaceToken($prevIndex, $addedUseStatement['alias']);
626+
} else {
627+
$phpcsFile->fixer->replaceToken($prevIndex, $addedUseStatement['shortName']);
628+
}
629+
630+
$phpcsFile->fixer->endChangeset();
631+
632+
return;
633+
}
634+
635+
// PHP < 8: Multi-token format (T_NS_SEPARATOR + T_STRING)
573636
$lastIndex = null;
574637
$i = $prevIndex;
575638
$extractedUseStatement = '';
@@ -632,6 +695,35 @@ protected function checkUseForInstanceOf(File $phpcsFile, int $stackPtr): void {
632695

633696
$classNameIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true);
634697

698+
// PHP 8+: Check if it's a single T_NAME_FULLY_QUALIFIED token
699+
if (defined('T_NAME_FULLY_QUALIFIED') && $this->isGivenKind(T_NAME_FULLY_QUALIFIED, $tokens[$classNameIndex])) {
700+
$extractedUseStatement = ltrim($tokens[$classNameIndex]['content'], '\\');
701+
if (!str_contains($extractedUseStatement, '\\')) {
702+
return;
703+
}
704+
705+
$className = $this->extractClassNameFromUseStatementAsString($extractedUseStatement);
706+
$error = 'Use statement ' . $extractedUseStatement . ' for class ' . $className . ' should be in use block.';
707+
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'InstanceOf');
708+
if (!$fix) {
709+
return;
710+
}
711+
712+
$phpcsFile->fixer->beginChangeset();
713+
$addedUseStatement = $this->addUseStatement($className, $extractedUseStatement);
714+
715+
if ($addedUseStatement['alias'] !== null) {
716+
$phpcsFile->fixer->replaceToken($classNameIndex, $addedUseStatement['alias']);
717+
} else {
718+
$phpcsFile->fixer->replaceToken($classNameIndex, $addedUseStatement['shortName']);
719+
}
720+
721+
$phpcsFile->fixer->endChangeset();
722+
723+
return;
724+
}
725+
726+
// PHP < 8: Multi-token format (T_NS_SEPARATOR + T_STRING)
635727
$lastIndex = null;
636728
$i = $classNameIndex;
637729
$extractedUseStatement = '';
@@ -699,6 +791,35 @@ public function checkUseForCatchOrCallable(File $phpcsFile, int $stackPtr): void
699791
$closeParenthesisIndex = $tokens[$openParenthesisIndex]['parenthesis_closer'];
700792
$classNameIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $openParenthesisIndex + 1, null, true);
701793

794+
// PHP 8+: Check if it's a single T_NAME_FULLY_QUALIFIED token
795+
if (defined('T_NAME_FULLY_QUALIFIED') && $this->isGivenKind(T_NAME_FULLY_QUALIFIED, $tokens[$classNameIndex])) {
796+
$extractedUseStatement = ltrim($tokens[$classNameIndex]['content'], '\\');
797+
if (!str_contains($extractedUseStatement, '\\')) {
798+
return;
799+
}
800+
801+
$className = $this->extractClassNameFromUseStatementAsString($extractedUseStatement);
802+
$error = 'Use statement ' . $extractedUseStatement . ' for class ' . $className . ' should be in use block.';
803+
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'Catch');
804+
if (!$fix) {
805+
return;
806+
}
807+
808+
$phpcsFile->fixer->beginChangeset();
809+
$addedUseStatement = $this->addUseStatement($className, $extractedUseStatement);
810+
811+
if ($addedUseStatement['alias'] !== null) {
812+
$phpcsFile->fixer->replaceToken($classNameIndex, $addedUseStatement['alias']);
813+
} else {
814+
$phpcsFile->fixer->replaceToken($classNameIndex, $addedUseStatement['shortName']);
815+
}
816+
817+
$phpcsFile->fixer->endChangeset();
818+
819+
return;
820+
}
821+
822+
// PHP < 8: Multi-token format (T_NS_SEPARATOR + T_STRING)
702823
$lastIndex = null;
703824
$i = $classNameIndex;
704825
$extractedUseStatement = '';
@@ -785,6 +906,35 @@ protected function checkUseForReturnTypeHint(File $phpcsFile, int $stackPtr): vo
785906
$startIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $startIndex + 1, $startIndex + 3, true);
786907
}
787908

909+
// PHP 8+: Check if it's a single T_NAME_FULLY_QUALIFIED token
910+
if (defined('T_NAME_FULLY_QUALIFIED') && $this->isGivenKind(T_NAME_FULLY_QUALIFIED, $tokens[$startIndex])) {
911+
$extractedUseStatement = ltrim($tokens[$startIndex]['content'], '\\');
912+
if (!str_contains($extractedUseStatement, '\\')) {
913+
return;
914+
}
915+
916+
$className = $this->extractClassNameFromUseStatementAsString($extractedUseStatement);
917+
$error = 'Use statement ' . $extractedUseStatement . ' for class ' . $className . ' should be in use block.';
918+
$fix = $phpcsFile->addFixableError($error, $colonIndex, 'ReturnSignature');
919+
if (!$fix) {
920+
return;
921+
}
922+
923+
$phpcsFile->fixer->beginChangeset();
924+
$addedUseStatement = $this->addUseStatement($className, $extractedUseStatement);
925+
926+
if ($addedUseStatement['alias'] !== null) {
927+
$phpcsFile->fixer->replaceToken($startIndex, $addedUseStatement['alias']);
928+
} else {
929+
$phpcsFile->fixer->replaceToken($startIndex, $addedUseStatement['shortName']);
930+
}
931+
932+
$phpcsFile->fixer->endChangeset();
933+
934+
return;
935+
}
936+
937+
// PHP < 8: Multi-token format (T_NS_SEPARATOR + T_STRING)
788938
$lastIndex = null;
789939
$j = $startIndex;
790940
$extractedUseStatement = '';
@@ -854,6 +1004,35 @@ protected function checkUseForSignature(File $phpcsFile, int $stackPtr): void {
8541004
$closeParenthesisIndex = $tokens[$openParenthesisIndex]['parenthesis_closer'];
8551005

8561006
for ($i = $openParenthesisIndex + 1; $i < $closeParenthesisIndex; $i++) {
1007+
// PHP 8+: Check if it's a single T_NAME_FULLY_QUALIFIED token
1008+
if (defined('T_NAME_FULLY_QUALIFIED') && $this->isGivenKind(T_NAME_FULLY_QUALIFIED, $tokens[$i])) {
1009+
$extractedUseStatement = ltrim($tokens[$i]['content'], '\\');
1010+
if (!str_contains($extractedUseStatement, '\\')) {
1011+
continue;
1012+
}
1013+
1014+
$className = $this->extractClassNameFromUseStatementAsString($extractedUseStatement);
1015+
$error = 'Use statement ' . $extractedUseStatement . ' for ' . $className . ' should be in use block.';
1016+
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'Signature');
1017+
if (!$fix) {
1018+
return;
1019+
}
1020+
1021+
$phpcsFile->fixer->beginChangeset();
1022+
$addedUseStatement = $this->addUseStatement($className, $extractedUseStatement);
1023+
1024+
if ($addedUseStatement['alias'] !== null) {
1025+
$phpcsFile->fixer->replaceToken($i, $addedUseStatement['alias']);
1026+
} else {
1027+
$phpcsFile->fixer->replaceToken($i, $addedUseStatement['shortName']);
1028+
}
1029+
1030+
$phpcsFile->fixer->endChangeset();
1031+
1032+
continue;
1033+
}
1034+
1035+
// PHP < 8: Multi-token format (T_NS_SEPARATOR + T_STRING)
8571036
$lastIndex = null;
8581037
$j = $i;
8591038
$extractedUseStatement = '';

tests/PSR2R/Sniffs/Namespaces/NoInlineFullyQualifiedClassNameSniffTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class NoInlineFullyQualifiedClassNameSniffTest extends TestCase
1717
*/
1818
public function testNoInlineFullyQualifiedClassNameSniffer(): void
1919
{
20-
$this->assertSnifferFindsErrors(new NoInlineFullyQualifiedClassNameSniff(), 11);
20+
$this->assertSnifferFindsErrors(new NoInlineFullyQualifiedClassNameSniff(), 36);
2121
}
2222

2323
/**

tests/_data/NoInlineFullyQualifiedClassName/after.php

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,73 @@
44

55
use Some\Base\Exception;
66
use Psr\Log\LoggerInterface;
7+
use Some\Namespace\ClassName;
8+
use Some\Namespace\CustomException;
79
use Another\SerializableInterface;
810
use Some\CountableInterface;
911

10-
// Anonymous class implementing interface with FQCN
12+
// Test 1: new statement with FQCN
13+
class NewStatement {
14+
public function test(): void {
15+
$obj = new ClassName();
16+
}
17+
}
18+
19+
// Test 2: static call with FQCN
20+
class StaticCall {
21+
public function test(): void {
22+
ClassName::staticMethod();
23+
}
24+
}
25+
26+
// Test 3: instanceof with FQCN
27+
class InstanceOfCheck {
28+
public function test(): void {
29+
$obj = new \stdClass();
30+
if ($obj instanceof ClassName) {
31+
// do something
32+
}
33+
}
34+
}
35+
36+
// Test 4: catch with FQCN
37+
class CatchBlock {
38+
public function test(): void {
39+
try {
40+
// something
41+
} catch (CustomException $e) {
42+
// handle
43+
}
44+
}
45+
}
46+
47+
// Test 5: parameter type hint with FQCN
48+
class ParameterTypeHint {
49+
public function test(ClassName $param): void {
50+
}
51+
}
52+
53+
// Test 6: return type hint with FQCN
54+
class ReturnTypeHint {
55+
public function test(): ClassName {
56+
return new ClassName();
57+
}
58+
}
59+
60+
// Test 7: nullable parameter type with FQCN
61+
class NullableParameter {
62+
public function test(?ClassName $param): void {
63+
}
64+
}
65+
66+
// Test 8: nullable return type with FQCN
67+
class NullableReturn {
68+
public function test(): ?ClassName {
69+
return null;
70+
}
71+
}
72+
73+
// Test 9: Anonymous class implementing interface with FQCN
1174
class AnonymousClassImplements {
1275
protected function test(): void {
1376
$logger = new class implements LoggerInterface {

tests/_data/NoInlineFullyQualifiedClassName/before.php

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,68 @@
22

33
namespace Test;
44

5-
// Anonymous class implementing interface with FQCN
5+
// Test 1: new statement with FQCN
6+
class NewStatement {
7+
public function test(): void {
8+
$obj = new \Some\Namespace\ClassName();
9+
}
10+
}
11+
12+
// Test 2: static call with FQCN
13+
class StaticCall {
14+
public function test(): void {
15+
\Some\Namespace\ClassName::staticMethod();
16+
}
17+
}
18+
19+
// Test 3: instanceof with FQCN
20+
class InstanceOfCheck {
21+
public function test(): void {
22+
$obj = new \stdClass();
23+
if ($obj instanceof \Some\Namespace\ClassName) {
24+
// do something
25+
}
26+
}
27+
}
28+
29+
// Test 4: catch with FQCN
30+
class CatchBlock {
31+
public function test(): void {
32+
try {
33+
// something
34+
} catch (\Some\Namespace\CustomException $e) {
35+
// handle
36+
}
37+
}
38+
}
39+
40+
// Test 5: parameter type hint with FQCN
41+
class ParameterTypeHint {
42+
public function test(\Some\Namespace\ClassName $param): void {
43+
}
44+
}
45+
46+
// Test 6: return type hint with FQCN
47+
class ReturnTypeHint {
48+
public function test(): \Some\Namespace\ClassName {
49+
return new \Some\Namespace\ClassName();
50+
}
51+
}
52+
53+
// Test 7: nullable parameter type with FQCN
54+
class NullableParameter {
55+
public function test(?\Some\Namespace\ClassName $param): void {
56+
}
57+
}
58+
59+
// Test 8: nullable return type with FQCN
60+
class NullableReturn {
61+
public function test(): ?\Some\Namespace\ClassName {
62+
return null;
63+
}
64+
}
65+
66+
// Test 9: Anonymous class implementing interface with FQCN
667
class AnonymousClassImplements {
768
protected function test(): void {
869
$logger = new class implements \Psr\Log\LoggerInterface {

0 commit comments

Comments
 (0)