Skip to content

Commit

Permalink
feature #6262 ClassReferenceNameCasingFixer - introduction (SpacePossum)
Browse files Browse the repository at this point in the history
This PR was merged into the master branch.

Discussion
----------

ClassReferenceNameCasingFixer - introduction

Commits
-------

776850a ClassReferenceNameCasingFixer - introduction
  • Loading branch information
SpacePossum committed Feb 5, 2022
2 parents 248a56b + 776850a commit 86d0a4c
Show file tree
Hide file tree
Showing 14 changed files with 408 additions and 5 deletions.
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

0 comments on commit 86d0a4c

Please sign in to comment.