From 776850a6facb53fd9a8426f21c0d391f4dcafc09 Mon Sep 17 00:00:00 2001 From: SpacePossum Date: Wed, 2 Feb 2022 14:02:07 +0100 Subject: [PATCH] ClassReferenceNameCasingFixer - introduction --- doc/list.rst | 7 + doc/ruleSets/Symfony.rst | 1 + .../casing/class_reference_name_casing.rst | 30 +++ doc/rules/index.rst | 3 + src/DocBlock/TypeExpression.php | 2 +- .../Casing/ClassReferenceNameCasingFixer.php | 147 +++++++++++++ .../NativeConstantInvocationFixer.php | 2 +- .../NativeFunctionInvocationFixer.php | 2 +- .../Import/GlobalNamespaceImportFixer.php | 2 +- src/RuleSet/Sets/SymfonySet.php | 1 + .../Analyzer/Analysis/NamespaceAnalysis.php | 5 + src/Tokenizer/Analyzer/FunctionsAnalyzer.php | 2 +- .../ClassReferenceNameCasingFixerTest.php | 202 ++++++++++++++++++ .../Analysis/NamespaceAnalysisTest.php | 7 + 14 files changed, 408 insertions(+), 5 deletions(-) create mode 100644 doc/rules/casing/class_reference_name_casing.rst create mode 100644 src/Fixer/Casing/ClassReferenceNameCasingFixer.php create mode 100644 tests/Fixer/Casing/ClassReferenceNameCasingFixerTest.php diff --git a/doc/list.rst b/doc/list.rst index 4cea70576ae..86bad617b3d 100644 --- a/doc/list.rst +++ b/doc/list.rst @@ -208,6 +208,13 @@ List of Available Rules *warning deprecated* `Source PhpCsFixer\\Fixer\\LanguageConstruct\\ClassKeywordRemoveFixer <./../src/Fixer/LanguageConstruct/ClassKeywordRemoveFixer.php>`_ +- `class_reference_name_casing <./rules/casing/class_reference_name_casing.rst>`_ + + When referencing a class it must be written using the correct casing. + + Part of rule sets `@PhpCsFixer <./ruleSets/PhpCsFixer.rst>`_ `@Symfony <./ruleSets/Symfony.rst>`_ + + `Source PhpCsFixer\\Fixer\\Casing\\ClassReferenceNameCasingFixer <./../src/Fixer/Casing/ClassReferenceNameCasingFixer.php>`_ - `clean_namespace <./rules/namespace_notation/clean_namespace.rst>`_ Namespace must not contain spacing, comments or PHPDoc. diff --git a/doc/ruleSets/Symfony.rst b/doc/ruleSets/Symfony.rst index 5a24c927193..95972acde2d 100644 --- a/doc/ruleSets/Symfony.rst +++ b/doc/ruleSets/Symfony.rst @@ -24,6 +24,7 @@ Rules - `class_definition <./../rules/class_notation/class_definition.rst>`_ config: ``['single_line' => true]`` +- `class_reference_name_casing <./../rules/casing/class_reference_name_casing.rst>`_ - `clean_namespace <./../rules/namespace_notation/clean_namespace.rst>`_ - `concat_space <./../rules/operator/concat_space.rst>`_ - `echo_tag_syntax <./../rules/php_tag/echo_tag_syntax.rst>`_ diff --git a/doc/rules/casing/class_reference_name_casing.rst b/doc/rules/casing/class_reference_name_casing.rst new file mode 100644 index 00000000000..751dd19afd7 --- /dev/null +++ b/doc/rules/casing/class_reference_name_casing.rst @@ -0,0 +1,30 @@ +==================================== +Rule ``class_reference_name_casing`` +==================================== + +When referencing a class it must be written using the correct casing. + +Examples +-------- + +Example #1 +~~~~~~~~~~ + +.. code-block:: diff + + --- Original + +++ New + `_ rule set will enable the ``class_reference_name_casing`` rule. + +@Symfony + Using the `@Symfony <./../../ruleSets/Symfony.rst>`_ rule set will enable the ``class_reference_name_casing`` rule. diff --git a/doc/rules/index.rst b/doc/rules/index.rst index 4edb51e0935..dc5d99839e5 100644 --- a/doc/rules/index.rst +++ b/doc/rules/index.rst @@ -86,6 +86,9 @@ Basic Casing ------ +- `class_reference_name_casing <./casing/class_reference_name_casing.rst>`_ + + When referencing a class it must be written using the correct casing. - `constant_case <./casing/constant_case.rst>`_ The PHP constants ``true``, ``false``, and ``null`` MUST be written using the correct casing. diff --git a/src/DocBlock/TypeExpression.php b/src/DocBlock/TypeExpression.php index b3707cd0958..d3b21d1074b 100644 --- a/src/DocBlock/TypeExpression.php +++ b/src/DocBlock/TypeExpression.php @@ -449,7 +449,7 @@ private function normalize(string $type): string } } - if (null === $this->namespace || '' === $this->namespace->getShortName()) { + if (null === $this->namespace || $this->namespace->isGlobalNamespace()) { return $type; } diff --git a/src/Fixer/Casing/ClassReferenceNameCasingFixer.php b/src/Fixer/Casing/ClassReferenceNameCasingFixer.php new file mode 100644 index 00000000000..f17f2ba8c8c --- /dev/null +++ b/src/Fixer/Casing/ClassReferenceNameCasingFixer.php @@ -0,0 +1,147 @@ + + * Dariusz Rumiński + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace PhpCsFixer\Fixer\Casing; + +use PhpCsFixer\AbstractFixer; +use PhpCsFixer\FixerDefinition\CodeSample; +use PhpCsFixer\FixerDefinition\FixerDefinition; +use PhpCsFixer\FixerDefinition\FixerDefinitionInterface; +use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis; +use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer; +use PhpCsFixer\Tokenizer\CT; +use PhpCsFixer\Tokenizer\Token; +use PhpCsFixer\Tokenizer\Tokens; + +final class ClassReferenceNameCasingFixer extends AbstractFixer +{ + /** + * {@inheritdoc} + */ + public function getDefinition(): FixerDefinitionInterface + { + return new FixerDefinition( + 'When referencing a class it must be written using the correct casing.', + [ + new CodeSample("isTokenKindFound(T_STRING); + } + + /** + * {@inheritdoc} + */ + protected function applyFix(\SplFileInfo $file, Tokens $tokens): void + { + $namespacesAnalyzer = new NamespacesAnalyzer(); + $classNames = $this->getClassNames(); + + foreach ($namespacesAnalyzer->getDeclarations($tokens) as $namespace) { + foreach ($this->getClassReference($tokens, $namespace) as $reference) { + $currentContent = $tokens[$reference]->getContent(); + $lowerCurrentContent = strtolower($currentContent); + + if (isset($classNames[$lowerCurrentContent]) && $currentContent !== $classNames[$lowerCurrentContent]) { + $tokens[$reference] = new Token([T_STRING, $classNames[$lowerCurrentContent]]); + } + } + } + } + + private function getClassReference(Tokens $tokens, NamespaceAnalysis $namespace): \Generator + { + static $notBeforeKinds; + + if (null === $notBeforeKinds) { + $notBeforeKinds = [ + CT::T_USE_TRAIT, + T_AS, + T_CASE, // PHP 8.1 trait enum-case + T_CLASS, + T_CONST, + T_DOUBLE_COLON, + T_FUNCTION, + T_INTERFACE, + T_OBJECT_OPERATOR, + T_TRAIT, + ]; + + if (\defined('T_ENUM')) { // @TODO: drop condition when PHP 8.1+ is required + $notBeforeKinds[] = T_ENUM; + } + } + + $namespaceIsGlobal = $namespace->isGlobalNamespace(); + + for ($index = $namespace->getScopeStartIndex(); $index < $namespace->getScopeEndIndex(); ++$index) { + if (!$tokens[$index]->isGivenKind(T_STRING)) { + continue; + } + + $nextIndex = $tokens->getNextMeaningfulToken($index); + + if ($tokens[$nextIndex]->isGivenKind(T_NS_SEPARATOR)) { + continue; + } + + $prevIndex = $tokens->getPrevMeaningfulToken($index); + $isNamespaceSeparator = $tokens[$prevIndex]->isGivenKind(T_NS_SEPARATOR); + + if (!$isNamespaceSeparator && !$namespaceIsGlobal) { + continue; + } + + if ($isNamespaceSeparator) { + $prevIndex = $tokens->getPrevMeaningfulToken($prevIndex); + + if ($tokens[$prevIndex]->isGivenKind(T_STRING)) { + continue; + } + } elseif ($tokens[$prevIndex]->isGivenKind($notBeforeKinds)) { + continue; + } + + if (!$tokens[$prevIndex]->isGivenKind([T_NEW]) && $tokens[$nextIndex]->equals('(')) { + continue; + } + + yield $index; + } + } + + private function getClassNames(): array + { + static $classes = null; + + if (null === $classes) { + $classes = []; + + foreach (get_declared_classes() as $class) { + if ((new \ReflectionClass($class))->isInternal()) { + $classes[strtolower($class)] = $class; + } + } + } + + return $classes; + } +} diff --git a/src/Fixer/ConstantNotation/NativeConstantInvocationFixer.php b/src/Fixer/ConstantNotation/NativeConstantInvocationFixer.php index 732ca9d53c5..959ef0f1b36 100644 --- a/src/Fixer/ConstantNotation/NativeConstantInvocationFixer.php +++ b/src/Fixer/ConstantNotation/NativeConstantInvocationFixer.php @@ -188,7 +188,7 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void // 'scope' is 'namespaced' here /** @var NamespaceAnalysis $namespace */ foreach (array_reverse($namespaces) as $namespace) { - if ('' === $namespace->getFullName()) { + if ($namespace->isGlobalNamespace()) { continue; } diff --git a/src/Fixer/FunctionNotation/NativeFunctionInvocationFixer.php b/src/Fixer/FunctionNotation/NativeFunctionInvocationFixer.php index 0a778974357..95e3e0c60e1 100644 --- a/src/Fixer/FunctionNotation/NativeFunctionInvocationFixer.php +++ b/src/Fixer/FunctionNotation/NativeFunctionInvocationFixer.php @@ -206,7 +206,7 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void // 'scope' is 'namespaced' here /** @var NamespaceAnalysis $namespace */ foreach (array_reverse($namespaces) as $namespace) { - $this->fixFunctionCalls($tokens, $this->functionFilter, $namespace->getScopeStartIndex(), $namespace->getScopeEndIndex(), '' === $namespace->getFullName()); + $this->fixFunctionCalls($tokens, $this->functionFilter, $namespace->getScopeStartIndex(), $namespace->getScopeEndIndex(), $namespace->isGlobalNamespace()); } } diff --git a/src/Fixer/Import/GlobalNamespaceImportFixer.php b/src/Fixer/Import/GlobalNamespaceImportFixer.php index dbc0ef7f644..183e26cb3bc 100644 --- a/src/Fixer/Import/GlobalNamespaceImportFixer.php +++ b/src/Fixer/Import/GlobalNamespaceImportFixer.php @@ -120,7 +120,7 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void { $namespaceAnalyses = (new NamespacesAnalyzer())->getDeclarations($tokens); - if (1 !== \count($namespaceAnalyses) || '' === $namespaceAnalyses[0]->getFullName()) { + if (1 !== \count($namespaceAnalyses) || $namespaceAnalyses[0]->isGlobalNamespace()) { return; } diff --git a/src/RuleSet/Sets/SymfonySet.php b/src/RuleSet/Sets/SymfonySet.php index 68abb199466..051ad957689 100644 --- a/src/RuleSet/Sets/SymfonySet.php +++ b/src/RuleSet/Sets/SymfonySet.php @@ -46,6 +46,7 @@ public function getRules(): array 'class_definition' => [ 'single_line' => true, ], + 'class_reference_name_casing' => true, 'clean_namespace' => true, 'concat_space' => true, 'echo_tag_syntax' => true, diff --git a/src/Tokenizer/Analyzer/Analysis/NamespaceAnalysis.php b/src/Tokenizer/Analyzer/Analysis/NamespaceAnalysis.php index 2a9aa0a4c37..e6f18215de9 100644 --- a/src/Tokenizer/Analyzer/Analysis/NamespaceAnalysis.php +++ b/src/Tokenizer/Analyzer/Analysis/NamespaceAnalysis.php @@ -100,4 +100,9 @@ public function getScopeEndIndex(): int { return $this->scopeEndIndex; } + + public function isGlobalNamespace(): bool + { + return '' === $this->getFullName(); + } } diff --git a/src/Tokenizer/Analyzer/FunctionsAnalyzer.php b/src/Tokenizer/Analyzer/FunctionsAnalyzer.php index 5edfab27cd4..af1b77069f9 100644 --- a/src/Tokenizer/Analyzer/FunctionsAnalyzer.php +++ b/src/Tokenizer/Analyzer/FunctionsAnalyzer.php @@ -90,7 +90,7 @@ public function isGlobalFunctionCall(Tokens $tokens, int $index): bool $scopeEndIndex = $declaration->getScopeEndIndex(); if ($index >= $scopeStartIndex && $index <= $scopeEndIndex) { - $inGlobalNamespace = '' === $declaration->getFullName(); + $inGlobalNamespace = $declaration->isGlobalNamespace(); break; } diff --git a/tests/Fixer/Casing/ClassReferenceNameCasingFixerTest.php b/tests/Fixer/Casing/ClassReferenceNameCasingFixerTest.php new file mode 100644 index 00000000000..2b310f28403 --- /dev/null +++ b/tests/Fixer/Casing/ClassReferenceNameCasingFixerTest.php @@ -0,0 +1,202 @@ + + * Dariusz Rumiński + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace PhpCsFixer\Tests\Fixer\Casing; + +use PhpCsFixer\Tests\Test\AbstractFixerTestCase; + +/** + * @internal + * + * @covers \PhpCsFixer\Fixer\Casing\ClassReferenceNameCasingFixer + */ +final class ClassReferenceNameCasingFixerTest extends AbstractFixerTestCase +{ + /** + * @dataProvider provideFixCases + */ + public function testFix(string $expected, string $input = null): void + { + $this->doTest($expected, $input); + } + + public function provideFixCases(): \Generator + { + yield [ + 'exception;', + ]; + + yield [ + 'doTest($expected, $input); + } + + public function provideFix81Cases(): \Generator + { + yield [ + 'getShortName()); + static::assertFalse($analysis->isGlobalNamespace()); } public function testStartIndex(): void @@ -61,4 +62,10 @@ public function testScopeEndIndex(): void $analysis = new NamespaceAnalysis('Full\NamespaceName', 'NamespaceName', 1, 2, 1, 10); static::assertSame(10, $analysis->getScopeEndIndex()); } + + public function testGlobal(): void + { + $analysis = new NamespaceAnalysis('', '', 1, 2, 1, 10); + static::assertTrue($analysis->isGlobalNamespace()); + } }