Skip to content

Commit b742ecb

Browse files
committed
ReferenceUsedNamesOnlySniff: functions and constants support is hopefully complete
1 parent e7ae6cb commit b742ecb

21 files changed

+411
-24
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,9 +335,13 @@ new Foo\Bar();
335335

336336
* `namespacesRequiredToUse`: if not set, all namespaces are required to be used. When set, only mentioned namespaces are required to be used. Useful in tandem with UseOnlyWhitelistedNamespaces sniff.
337337
* `allowFullyQualifiedNameForCollidingClasses`: allow fully qualified name for a class with a colliding use statement.
338+
* `allowFullyQualifiedNameForCollidingFunctions`: allow fully qualified name for a function with a colliding use statement.
339+
* `allowFullyQualifiedNameForCollidingConstants`: allow fully qualified name for a constant with a colliding use statement.
338340
* `allowFullyQualifiedGlobalClasses`: allows using fully qualified classes from global space (i.e. `\DateTimeImmutable`).
339341
* `allowFullyQualifiedGlobalFunctions`: allows using fully qualified functions from global space (i.e. `\phpversion()`).
340342
* `allowFullyQualifiedGlobalConstants`: allows using fully qualified constants from global space (i.e. `\PHP_VERSION`).
343+
* `allowFallbackGlobalFunctions`: allows using global functions via fallback name without `use` (i.e. `phpversion()`).
344+
* `allowFallbackGlobalConstants`: allows using global constants via fallback name without `use` (i.e. `PHP_VERSION`).
341345

342346
#### SlevomatCodingStandard.Namespaces.UseOnlyWhitelistedNamespaces
343347

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SlevomatCodingStandard\Helpers;
4+
5+
class ConstantHelper
6+
{
7+
8+
public static function getName(\PHP_CodeSniffer\Files\File $codeSnifferFile, int $constantPointer): string
9+
{
10+
$tokens = $codeSnifferFile->getTokens();
11+
return $tokens[TokenHelper::findNext($codeSnifferFile, T_STRING, $constantPointer + 1)]['content'];
12+
}
13+
14+
public static function getFullyQualifiedName(\PHP_CodeSniffer\Files\File $codeSnifferFile, int $constantPointer): string
15+
{
16+
$name = self::getName($codeSnifferFile, $constantPointer);
17+
$namespace = NamespaceHelper::findCurrentNamespaceName($codeSnifferFile, $constantPointer);
18+
19+
return $namespace !== null ? sprintf('%s%s%s%s', NamespaceHelper::NAMESPACE_SEPARATOR, $namespace, NamespaceHelper::NAMESPACE_SEPARATOR, $name) : $name;
20+
}
21+
22+
/**
23+
* @param \PHP_CodeSniffer\Files\File $codeSnifferFile
24+
* @return string[]
25+
*/
26+
public static function getAllNames(\PHP_CodeSniffer\Files\File $codeSnifferFile): array
27+
{
28+
$previousConstantPointer = 0;
29+
30+
return array_map(
31+
function (int $constantPointer) use ($codeSnifferFile): string {
32+
return self::getName($codeSnifferFile, $constantPointer);
33+
},
34+
array_filter(
35+
iterator_to_array(self::getAllConstantPointers($codeSnifferFile, $previousConstantPointer)),
36+
function (int $constantPointer) use ($codeSnifferFile): bool {
37+
foreach (array_reverse($codeSnifferFile->getTokens()[$constantPointer]['conditions']) as $conditionTokenCode) {
38+
if (in_array($conditionTokenCode, [T_CLASS, T_INTERFACE, T_TRAIT, T_ANON_CLASS], true)) {
39+
return false;
40+
}
41+
}
42+
43+
return true;
44+
}
45+
)
46+
);
47+
}
48+
49+
private static function getAllConstantPointers(\PHP_CodeSniffer\Files\File $codeSnifferFile, int &$previousConstantPointer): \Generator
50+
{
51+
do {
52+
$nextConstantPointer = TokenHelper::findNext($codeSnifferFile, T_CONST, $previousConstantPointer + 1);
53+
if ($nextConstantPointer !== null) {
54+
$previousConstantPointer = $nextConstantPointer;
55+
yield $nextConstantPointer;
56+
}
57+
} while ($nextConstantPointer !== null);
58+
}
59+
60+
}

SlevomatCodingStandard/Helpers/FunctionHelper.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,4 +218,36 @@ public static function findReturnAnnotation(\PHP_CodeSniffer\Files\File $codeSni
218218
return $returnAnnotations[0];
219219
}
220220

221+
/**
222+
* @param \PHP_CodeSniffer\Files\File $codeSnifferFile
223+
* @return string[]
224+
*/
225+
public static function getAllFunctionNames(\PHP_CodeSniffer\Files\File $codeSnifferFile): array
226+
{
227+
$previousFunctionPointer = 0;
228+
229+
return array_map(
230+
function (int $functionPointer) use ($codeSnifferFile): string {
231+
return self::getName($codeSnifferFile, $functionPointer);
232+
},
233+
array_filter(
234+
iterator_to_array(self::getAllFunctionOrMethodPointers($codeSnifferFile, $previousFunctionPointer)),
235+
function (int $functionOrMethodPointer) use ($codeSnifferFile): bool {
236+
return !self::isMethod($codeSnifferFile, $functionOrMethodPointer);
237+
}
238+
)
239+
);
240+
}
241+
242+
private static function getAllFunctionOrMethodPointers(\PHP_CodeSniffer\Files\File $codeSnifferFile, int &$previousFunctionPointer): \Generator
243+
{
244+
do {
245+
$nextFunctionPointer = TokenHelper::findNext($codeSnifferFile, T_FUNCTION, $previousFunctionPointer + 1);
246+
if ($nextFunctionPointer !== null) {
247+
$previousFunctionPointer = $nextFunctionPointer;
248+
yield $nextFunctionPointer;
249+
}
250+
} while ($nextFunctionPointer !== null);
251+
}
252+
221253
}

SlevomatCodingStandard/Helpers/ReferencedName.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ public function getEndPointer(): int
4949
return $this->endPointer;
5050
}
5151

52+
public function isClass(): bool
53+
{
54+
return $this->type === self::TYPE_DEFAULT;
55+
}
56+
5257
public function isConstant(): bool
5358
{
5459
return $this->type === self::TYPE_CONSTANT;

SlevomatCodingStandard/Sniffs/Namespaces/ReferenceUsedNamesOnlySniff.php

Lines changed: 106 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace SlevomatCodingStandard\Sniffs\Namespaces;
44

55
use SlevomatCodingStandard\Helpers\ClassHelper;
6+
use SlevomatCodingStandard\Helpers\ConstantHelper;
7+
use SlevomatCodingStandard\Helpers\FunctionHelper;
68
use SlevomatCodingStandard\Helpers\NamespaceHelper;
79
use SlevomatCodingStandard\Helpers\ReferencedName;
810
use SlevomatCodingStandard\Helpers\ReferencedNameHelper;
@@ -19,6 +21,8 @@ class ReferenceUsedNamesOnlySniff implements \PHP_CodeSniffer\Sniffs\Sniff
1921

2022
public const CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME_WITHOUT_NAMESPACE = 'ReferenceViaFullyQualifiedNameWithoutNamespace';
2123

24+
public const CODE_REFERENCE_VIA_FALLBACK_GLOBAL_NAME = 'ReferenceViaFallbackGlobalName';
25+
2226
public const CODE_PARTIAL_USE = 'PartialUse';
2327

2428
/** @var string[] */
@@ -36,9 +40,15 @@ class ReferenceUsedNamesOnlySniff implements \PHP_CodeSniffer\Sniffs\Sniff
3640
/** @var bool */
3741
public $allowFullyQualifiedGlobalFunctions = false;
3842

43+
/** @var bool */
44+
public $allowFallbackGlobalFunctions = true;
45+
3946
/** @var bool */
4047
public $allowFullyQualifiedGlobalConstants = false;
4148

49+
/** @var bool */
50+
public $allowFallbackGlobalConstants = true;
51+
4252
/** @var string[] */
4353
public $specialExceptionNames = [];
4454

@@ -67,6 +77,12 @@ class ReferenceUsedNamesOnlySniff implements \PHP_CodeSniffer\Sniffs\Sniff
6777
/** @var bool */
6878
public $allowFullyQualifiedNameForCollidingClasses = false;
6979

80+
/** @var bool */
81+
public $allowFullyQualifiedNameForCollidingFunctions = false;
82+
83+
/** @var bool */
84+
public $allowFullyQualifiedNameForCollidingConstants = false;
85+
7086
/**
7187
* @return mixed[]
7288
*/
@@ -140,17 +156,51 @@ public function process(\PHP_CodeSniffer\Files\File $phpcsFile, $openTagPointer)
140156
$tokens = $phpcsFile->getTokens();
141157

142158
$referencedNames = ReferencedNameHelper::getAllReferencedNames($phpcsFile, $openTagPointer);
159+
$useStatements = UseStatementHelper::getUseStatements($phpcsFile, $openTagPointer);
160+
143161
$definedClassesIndex = array_flip(array_map(function (string $className): string {
144162
return strtolower($className);
145163
}, ClassHelper::getAllNames($phpcsFile)));
164+
$definedFunctionsIndex = array_flip(array_map(function (string $functionName): string {
165+
return strtolower($functionName);
166+
}, FunctionHelper::getAllFunctionNames($phpcsFile)));
167+
$definedConstantsIndex = array_flip(ConstantHelper::getAllNames($phpcsFile));
146168

147169
if ($this->allowFullyQualifiedNameForCollidingClasses) {
148-
$referencesIndex = array_flip(
170+
$classReferencesIndex = array_flip(
171+
array_map(
172+
function (ReferencedName $referencedName): string {
173+
return strtolower($referencedName->getNameAsReferencedInFile());
174+
},
175+
array_filter($referencedNames, function (ReferencedName $referencedName): bool {
176+
return $referencedName->isClass();
177+
})
178+
)
179+
);
180+
}
181+
182+
if ($this->allowFullyQualifiedNameForCollidingFunctions) {
183+
$functionReferencesIndex = array_flip(
149184
array_map(
150185
function (ReferencedName $referencedName): string {
151186
return strtolower($referencedName->getNameAsReferencedInFile());
152187
},
153-
$referencedNames
188+
array_filter($referencedNames, function (ReferencedName $referencedName): bool {
189+
return $referencedName->isFunction();
190+
})
191+
)
192+
);
193+
}
194+
195+
if ($this->allowFullyQualifiedNameForCollidingConstants) {
196+
$constantReferencesIndex = array_flip(
197+
array_map(
198+
function (ReferencedName $referencedName): string {
199+
return $referencedName->getNameAsReferencedInFile();
200+
},
201+
array_filter($referencedNames, function (ReferencedName $referencedName): bool {
202+
return $referencedName->isConstant();
203+
})
154204
)
155205
);
156206
}
@@ -159,16 +209,33 @@ function (ReferencedName $referencedName): string {
159209
$name = $referencedName->getNameAsReferencedInFile();
160210
$nameStartPointer = $referencedName->getStartPointer();
161211
$canonicalName = NamespaceHelper::normalizeToCanonicalName($name);
162-
163-
if ($this->allowFullyQualifiedNameForCollidingClasses) {
164-
$unqualifiedClassName = strtolower(NamespaceHelper::getUnqualifiedNameFromFullyQualifiedName($name));
165-
if (isset($referencesIndex[$unqualifiedClassName]) || array_key_exists($unqualifiedClassName, $definedClassesIndex ?? [])) {
212+
$unqualifiedName = NamespaceHelper::getUnqualifiedNameFromFullyQualifiedName($name);
213+
214+
$isFullyQualified = NamespaceHelper::isFullyQualifiedName($name);
215+
$isGlobalFallback = !$isFullyQualified
216+
&& !NamespaceHelper::hasNamespace($name)
217+
&& !array_key_exists(UseStatement::getUniqueId($referencedName->getType(), $name), $useStatements);
218+
$isGlobalFunctionFallback = $referencedName->isFunction() && $isGlobalFallback;
219+
$isGlobalConstantFallback = $referencedName->isConstant() && $isGlobalFallback;
220+
221+
if ($referencedName->isClass() && $this->allowFullyQualifiedNameForCollidingClasses) {
222+
$lowerCasedUnqualifiedClassName = strtolower($unqualifiedName);
223+
if (isset($classReferencesIndex[$lowerCasedUnqualifiedClassName]) || array_key_exists($lowerCasedUnqualifiedClassName, $definedClassesIndex)) {
224+
continue;
225+
}
226+
} elseif ($referencedName->isFunction() && $this->allowFullyQualifiedNameForCollidingFunctions) {
227+
$lowerCasedUnqualifiedFunctionName = strtolower($unqualifiedName);
228+
if (isset($functionReferencesIndex[$lowerCasedUnqualifiedFunctionName]) || array_key_exists($lowerCasedUnqualifiedFunctionName, $definedFunctionsIndex)) {
229+
continue;
230+
}
231+
} elseif ($referencedName->isConstant() && $this->allowFullyQualifiedNameForCollidingConstants) {
232+
if (isset($constantReferencesIndex[$unqualifiedName]) || array_key_exists($unqualifiedName, $definedConstantsIndex)) {
166233
continue;
167234
}
168235
}
169236

170-
if (NamespaceHelper::isFullyQualifiedName($name)) {
171-
if (!$this->isClassRequiredToBeUsed($name)) {
237+
if ($isFullyQualified || $isGlobalFunctionFallback || $isGlobalConstantFallback) {
238+
if ($isFullyQualified && !$this->isRequiredToBeUsed($name)) {
172239
continue;
173240
}
174241

@@ -185,12 +252,15 @@ function (ReferencedName $referencedName): string {
185252
$previousKeywordPointer = TokenHelper::findPreviousExcluding($phpcsFile, array_merge(TokenHelper::$nameTokenCodes, [T_WHITESPACE, T_COMMA]), $nameStartPointer - 1);
186253
if (!in_array($tokens[$previousKeywordPointer]['code'], $this->getFullyQualifiedKeywords(), true)) {
187254
if (
188-
!NamespaceHelper::hasNamespace($name)
255+
$isFullyQualified
256+
&& !NamespaceHelper::hasNamespace($name)
189257
&& NamespaceHelper::findCurrentNamespaceName($phpcsFile, $nameStartPointer) === null
190258
) {
259+
$label = sprintf($referencedName->isConstant() ? 'Constant %s' : ($referencedName->isFunction() ? 'Function %s()' : 'Class %s'), $name);
260+
191261
$fix = $phpcsFile->addFixableError(sprintf(
192-
'Type %s should not be referenced via a fully qualified name, but via an unqualified name without the leading \\, because the file does not have a namespace and the type cannot be put in a use statement.',
193-
$name
262+
'%s should not be referenced via a fully qualified name, but via an unqualified name without the leading \\, because the file does not have a namespace and the type cannot be put in a use statement.',
263+
$label
194264
), $nameStartPointer, self::CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME_WITHOUT_NAMESPACE);
195265
if ($fix) {
196266
$phpcsFile->fixer->beginChangeset();
@@ -201,9 +271,9 @@ function (ReferencedName $referencedName): string {
201271
$shouldBeUsed = NamespaceHelper::hasNamespace($name);
202272
if (!$shouldBeUsed) {
203273
if ($referencedName->isFunction()) {
204-
$shouldBeUsed = !$this->allowFullyQualifiedGlobalFunctions;
274+
$shouldBeUsed = $isFullyQualified ? !$this->allowFullyQualifiedGlobalFunctions : !$this->allowFallbackGlobalFunctions;
205275
} elseif ($referencedName->isConstant()) {
206-
$shouldBeUsed = !$this->allowFullyQualifiedGlobalConstants;
276+
$shouldBeUsed = $isFullyQualified ? !$this->allowFullyQualifiedGlobalConstants : !$this->allowFallbackGlobalConstants;
207277
} else {
208278
$shouldBeUsed = !$this->allowFullyQualifiedGlobalClasses;
209279
}
@@ -213,27 +283,41 @@ function (ReferencedName $referencedName): string {
213283
continue;
214284
}
215285

216-
$useStatements = UseStatementHelper::getUseStatements($phpcsFile, $openTagPointer);
217286
$nameToReference = NamespaceHelper::getUnqualifiedNameFromFullyQualifiedName($name);
218-
$canonicalNameToReference = strtolower($nameToReference);
287+
$canonicalNameToReference = $referencedName->isConstant() ? $nameToReference : strtolower($nameToReference);
219288

220289
$canBeFixed = true;
221290
foreach ($useStatements as $useStatement) {
291+
if ($useStatement->getType() !== $referencedName->getType()) {
292+
continue;
293+
}
294+
295+
if ($useStatement->getFullyQualifiedTypeName() === $canonicalName) {
296+
continue;
297+
}
298+
222299
if (
223-
$useStatement->getType() === $referencedName->getType()
224-
&& $useStatement->getFullyQualifiedTypeName() !== $canonicalName
225-
&& ($useStatement->getCanonicalNameAsReferencedInFile() === $canonicalNameToReference || array_key_exists($canonicalNameToReference, $definedClassesIndex))
300+
$useStatement->getCanonicalNameAsReferencedInFile() === $canonicalNameToReference
301+
|| ($referencedName->isClass() && array_key_exists($canonicalNameToReference, $definedClassesIndex))
302+
|| ($referencedName->isFunction() && array_key_exists($canonicalNameToReference, $definedFunctionsIndex))
303+
|| ($referencedName->isConstant() && array_key_exists($canonicalNameToReference, $definedConstantsIndex))
226304
) {
227305
$canBeFixed = false;
228306
break;
229307
}
230308
}
231309

232-
$errorMessage = sprintf('Type %s should not be referenced via a fully qualified name, but via a use statement.', $name);
310+
$label = sprintf($referencedName->isConstant() ? 'Constant %s' : ($referencedName->isFunction() ? 'Function %s()' : 'Class %s'), $name);
311+
$errorCode = $isGlobalConstantFallback || $isGlobalFunctionFallback
312+
? self::CODE_REFERENCE_VIA_FALLBACK_GLOBAL_NAME
313+
: self::CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME;
314+
$errorMessage = $isGlobalConstantFallback || $isGlobalFunctionFallback
315+
? sprintf('%s should not be referenced via a fallback global name, but via a use statement.', $label)
316+
: sprintf('%s should not be referenced via a fully qualified name, but via a use statement.', $label);
233317
if ($canBeFixed) {
234-
$fix = $phpcsFile->addFixableError($errorMessage, $nameStartPointer, self::CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME);
318+
$fix = $phpcsFile->addFixableError($errorMessage, $nameStartPointer, $errorCode);
235319
} else {
236-
$phpcsFile->addError($errorMessage, $nameStartPointer, self::CODE_REFERENCE_VIA_FULLY_QUALIFIED_NAME);
320+
$phpcsFile->addError($errorMessage, $nameStartPointer, $errorCode);
237321
$fix = false;
238322
}
239323

@@ -286,7 +370,7 @@ function (ReferencedName $referencedName): string {
286370
}
287371
}
288372

289-
private function isClassRequiredToBeUsed(string $name): bool
373+
private function isRequiredToBeUsed(string $name): bool
290374
{
291375
if (count($this->namespacesRequiredToUse) === 0) {
292376
return true;

build/phpcs.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
<property name="namespacesRequiredToUse" type="array" value="SlevomatCodingStandard"/>
5050
<property name="fullyQualifiedKeywords" type="array" value="T_EXTENDS,T_IMPLEMENTS"/>
5151
<property name="allowFullyQualifiedExceptions" value="true"/>
52+
<property name="allowFullyQualifiedGlobalFunctions" value="true"/>
53+
<property name="allowFullyQualifiedGlobalConstants" value="true"/>
5254
<property name="allowPartialUses" value="false"/>
5355
</properties>
5456
</rule>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SlevomatCodingStandard\Helpers;
4+
5+
class ConstantHelperTest extends \SlevomatCodingStandard\Helpers\TestCase
6+
{
7+
8+
public function testNameWithNamespace(): void
9+
{
10+
$codeSnifferFile = $this->getCodeSnifferFile(__DIR__ . '/data/constantWithNamespace.php');
11+
12+
$constantPointer = $this->findConstantPointerByName($codeSnifferFile, 'FOO');
13+
$this->assertSame('\FooNamespace\FOO', ConstantHelper::getFullyQualifiedName($codeSnifferFile, $constantPointer));
14+
$this->assertSame('FOO', ConstantHelper::getName($codeSnifferFile, $constantPointer));
15+
}
16+
17+
public function testNameWithoutNamespace(): void
18+
{
19+
$codeSnifferFile = $this->getCodeSnifferFile(__DIR__ . '/data/constantWithoutNamespace.php');
20+
21+
$constantPointer = $this->findConstantPointerByName($codeSnifferFile, 'FOO');
22+
$this->assertSame('FOO', ConstantHelper::getFullyQualifiedName($codeSnifferFile, $constantPointer));
23+
$this->assertSame('FOO', ConstantHelper::getName($codeSnifferFile, $constantPointer));
24+
}
25+
26+
public function testGetAllNames(): void
27+
{
28+
$codeSnifferFile = $this->getCodeSnifferFile(__DIR__ . '/data/constantNames.php');
29+
$this->assertSame(['FOO', 'BOO'], ConstantHelper::getAllNames($codeSnifferFile));
30+
}
31+
32+
}

0 commit comments

Comments
 (0)