Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ClassReferenceNameCasingFixer - introduction #6262

Merged
merged 1 commit into from Feb 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions doc/list.rst
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions doc/ruleSets/Symfony.rst
Expand Up @@ -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>`_
Expand Down
30 changes: 30 additions & 0 deletions 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
<?php
-throw new \exception();
+throw new \Exception();

Rule sets
---------

The rule is part of the following rule sets:

@PhpCsFixer
Using the `@PhpCsFixer <./../../ruleSets/PhpCsFixer.rst>`_ 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.
3 changes: 3 additions & 0 deletions doc/rules/index.rst
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/DocBlock/TypeExpression.php
Expand Up @@ -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;
}

Expand Down
147 changes: 147 additions & 0 deletions src/Fixer/Casing/ClassReferenceNameCasingFixer.php
@@ -0,0 +1,147 @@
<?php

declare(strict_types=1);

/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* 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("<?php\nthrow new \\exception();\n"),
]
);
}

/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens): bool
{
return $tokens->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;
}
}
Expand Up @@ -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;
}

Expand Down
Expand Up @@ -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());
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Fixer/Import/GlobalNamespaceImportFixer.php
Expand Up @@ -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;
}

Expand Down
1 change: 1 addition & 0 deletions src/RuleSet/Sets/SymfonySet.php
Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions src/Tokenizer/Analyzer/Analysis/NamespaceAnalysis.php
Expand Up @@ -100,4 +100,9 @@ public function getScopeEndIndex(): int
{
return $this->scopeEndIndex;
}

public function isGlobalNamespace(): bool
{
return '' === $this->getFullName();
}
}
2 changes: 1 addition & 1 deletion src/Tokenizer/Analyzer/FunctionsAnalyzer.php
Expand Up @@ -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;
}
Expand Down