Skip to content

Commit a5710b2

Browse files
committed
SlevomatCodingStandard.Classes.RequireSelfReference: New sniff
1 parent d5a5ebb commit a5710b2

File tree

6 files changed

+237
-0
lines changed

6 files changed

+237
-0
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SlevomatCodingStandard\Sniffs\Classes;
4+
5+
use PHP_CodeSniffer\Files\File;
6+
use PHP_CodeSniffer\Sniffs\Sniff;
7+
use SlevomatCodingStandard\Helpers\ClassHelper;
8+
use SlevomatCodingStandard\Helpers\FixerHelper;
9+
use SlevomatCodingStandard\Helpers\NamespaceHelper;
10+
use SlevomatCodingStandard\Helpers\ReferencedNameHelper;
11+
use SlevomatCodingStandard\Helpers\TokenHelper;
12+
use function array_merge;
13+
use function preg_quote;
14+
use function preg_replace;
15+
use const T_ANON_CLASS;
16+
use const T_ATTRIBUTE;
17+
use const T_OPEN_TAG;
18+
19+
class RequireSelfReferenceSniff implements Sniff
20+
{
21+
22+
public const CODE_REQUIRED_SELF_REFERENCE = 'RequiredSelfReference';
23+
24+
/**
25+
* @return array<int, (int|string)>
26+
*/
27+
public function register(): array
28+
{
29+
return [
30+
T_OPEN_TAG,
31+
];
32+
}
33+
34+
/**
35+
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
36+
* @param int $openTagPointer
37+
*/
38+
public function process(File $phpcsFile, $openTagPointer): void
39+
{
40+
if (TokenHelper::findPrevious($phpcsFile, T_OPEN_TAG, $openTagPointer - 1) !== null) {
41+
return;
42+
}
43+
44+
$tokens = $phpcsFile->getTokens();
45+
46+
$referencedNames = array_merge(
47+
ReferencedNameHelper::getAllReferencedNames($phpcsFile, $openTagPointer),
48+
ReferencedNameHelper::getAllReferencedNamesInAttributes($phpcsFile, $openTagPointer)
49+
);
50+
51+
foreach ($referencedNames as $referencedName) {
52+
if (!$referencedName->isClass()) {
53+
continue;
54+
}
55+
56+
$classPointer = ClassHelper::getClassPointer($phpcsFile, $referencedName->getStartPointer());
57+
if ($classPointer === null) {
58+
continue;
59+
}
60+
61+
if ($tokens[$classPointer]['code'] === T_ANON_CLASS) {
62+
continue;
63+
}
64+
65+
$className = ClassHelper::getFullyQualifiedName($phpcsFile, $classPointer);
66+
67+
$resolvedName = NamespaceHelper::resolveClassName(
68+
$phpcsFile,
69+
$referencedName->getNameAsReferencedInFile(),
70+
$referencedName->getStartPointer()
71+
);
72+
73+
if ($className !== $resolvedName) {
74+
continue;
75+
}
76+
77+
$fix = $phpcsFile->addFixableError(
78+
'"self" for local reference is required.',
79+
$referencedName->getStartPointer(),
80+
self::CODE_REQUIRED_SELF_REFERENCE
81+
);
82+
if (!$fix) {
83+
continue;
84+
}
85+
86+
$inAttribute = $tokens[$referencedName->getStartPointer()]['code'] === T_ATTRIBUTE;
87+
88+
$phpcsFile->fixer->beginChangeset();
89+
90+
if ($inAttribute) {
91+
$attributeContent = TokenHelper::getContent(
92+
$phpcsFile,
93+
$referencedName->getStartPointer(),
94+
$referencedName->getEndPointer()
95+
);
96+
$fixedAttributeContent = preg_replace(
97+
'~(?<=\W)' . preg_quote($referencedName->getNameAsReferencedInFile(), '~') . '(?=\W)~',
98+
'self',
99+
$attributeContent
100+
);
101+
$phpcsFile->fixer->replaceToken($referencedName->getStartPointer(), $fixedAttributeContent);
102+
103+
} else {
104+
$phpcsFile->fixer->replaceToken($referencedName->getStartPointer(), 'self');
105+
}
106+
107+
FixerHelper::removeBetweenIncluding($phpcsFile, $referencedName->getStartPointer() + 1, $referencedName->getEndPointer());
108+
109+
$phpcsFile->fixer->endChangeset();
110+
}
111+
}
112+
113+
}

doc/classes.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ Sniff provides the following settings:
207207

208208
* `excludedMethodPatterns`: allows to configure which methods are excluded from sniff detection. This is an array of regular expressions (PCRE) with delimiters. You should not use this with `includedMethodPatterns`, as it will not work properly.
209209

210+
#### SlevomatCodingStandard.Classes.RequireSelfReference 🔧
211+
212+
Requires `self` for local reference.
213+
210214
#### SlevomatCodingStandard.Classes.RequireSingleLineMethodSignature 🔧
211215

212216
Enforces method signature to be on a single line.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SlevomatCodingStandard\Sniffs\Classes;
4+
5+
use SlevomatCodingStandard\Sniffs\TestCase;
6+
7+
class RequireSelfReferenceSniffTest extends TestCase
8+
{
9+
10+
public function testNoErrors(): void
11+
{
12+
$report = self::checkFile(__DIR__ . '/data/requireSelfReferenceNoErrors.php');
13+
14+
self::assertNoSniffErrorInFile($report);
15+
}
16+
17+
public function testErrors(): void
18+
{
19+
$report = self::checkFile(__DIR__ . '/data/requireSelfReferenceErrors.php');
20+
21+
self::assertSame(10, $report->getErrorCount());
22+
23+
foreach ([14, 16, 19, 20, 23, 24, 26, 28] as $line) {
24+
self::assertSniffError($report, $line, RequireSelfReferenceSniff::CODE_REQUIRED_SELF_REFERENCE);
25+
}
26+
27+
self::assertAllFixedInFile($report);
28+
}
29+
30+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php // lint >= 8.0
2+
3+
namespace SomeNamespace;
4+
5+
class Anything
6+
{
7+
8+
}
9+
10+
class Whatever
11+
{
12+
13+
const FIRST_CONSTANT = 'first';
14+
const SECOND_CONSTANT = self::FIRST_CONSTANT;
15+
16+
private $property = self::SECOND_CONSTANT;
17+
18+
private $arrayProperty = [
19+
self::FIRST_CONSTANT,
20+
self::SECOND_CONSTANT,
21+
];
22+
23+
#[Attribute(self::FIRST_CONSTANT)]
24+
public function doSomething(self $parameter): self
25+
{
26+
$arrowFunction = fn ($a = self::SECOND_CONSTANT): ?self => null;
27+
28+
return function (): self|Anything {
29+
};
30+
}
31+
32+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php // lint >= 8.0
2+
3+
namespace SomeNamespace;
4+
5+
class Anything
6+
{
7+
8+
}
9+
10+
class Whatever
11+
{
12+
13+
const FIRST_CONSTANT = 'first';
14+
const SECOND_CONSTANT = \SomeNamespace\Whatever::FIRST_CONSTANT;
15+
16+
private $property = Whatever::SECOND_CONSTANT;
17+
18+
private $arrayProperty = [
19+
Whatever::FIRST_CONSTANT,
20+
\SomeNamespace\Whatever::SECOND_CONSTANT,
21+
];
22+
23+
#[Attribute(Whatever::FIRST_CONSTANT)]
24+
public function doSomething(\SomeNamespace\Whatever $parameter): Whatever
25+
{
26+
$arrowFunction = fn ($a = Whatever::SECOND_CONSTANT): ?Whatever => null;
27+
28+
return function (): Whatever|Anything {
29+
};
30+
}
31+
32+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
?><?php
4+
5+
class Whatever
6+
{
7+
8+
const SOME_CONSTANT = PHP_VERSION;
9+
10+
public function doSomething(): self
11+
{
12+
return new class extends Anything {
13+
14+
public function doSomethingElse(): Whatever
15+
{
16+
17+
}
18+
19+
};
20+
}
21+
22+
}
23+
24+
$function = function (): Whatever {
25+
26+
};

0 commit comments

Comments
 (0)