ProtectedToPrivateFixer.php 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of PHP CS Fixer.
  5. *
  6. * (c) Fabien Potencier <fabien@symfony.com>
  7. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  8. *
  9. * This source file is subject to the MIT license that is bundled
  10. * with this source code in the file LICENSE.
  11. */
  12. namespace PhpCsFixer\Fixer\ClassNotation;
  13. use PhpCsFixer\AbstractFixer;
  14. use PhpCsFixer\FixerDefinition\CodeSample;
  15. use PhpCsFixer\FixerDefinition\FixerDefinition;
  16. use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
  17. use PhpCsFixer\Tokenizer\CT;
  18. use PhpCsFixer\Tokenizer\Token;
  19. use PhpCsFixer\Tokenizer\Tokens;
  20. use PhpCsFixer\Tokenizer\TokensAnalyzer;
  21. /**
  22. * @author Filippo Tessarotto <zoeslam@gmail.com>
  23. */
  24. final class ProtectedToPrivateFixer extends AbstractFixer
  25. {
  26. private TokensAnalyzer $tokensAnalyzer;
  27. public function getDefinition(): FixerDefinitionInterface
  28. {
  29. return new FixerDefinition(
  30. 'Converts `protected` variables and methods to `private` where possible.',
  31. [
  32. new CodeSample(
  33. '<?php
  34. final class Sample
  35. {
  36. protected $a;
  37. protected function test()
  38. {
  39. }
  40. }
  41. '
  42. ),
  43. ]
  44. );
  45. }
  46. /**
  47. * {@inheritdoc}
  48. *
  49. * Must run before OrderedClassElementsFixer.
  50. * Must run after FinalClassFixer, FinalInternalClassFixer.
  51. */
  52. public function getPriority(): int
  53. {
  54. return 66;
  55. }
  56. public function isCandidate(Tokens $tokens): bool
  57. {
  58. if (\defined('T_ENUM') && $tokens->isAllTokenKindsFound([T_ENUM, T_PROTECTED])) { // @TODO: drop condition when PHP 8.1+ is required
  59. return true;
  60. }
  61. return $tokens->isAllTokenKindsFound([T_CLASS, T_FINAL, T_PROTECTED]);
  62. }
  63. protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
  64. {
  65. $this->tokensAnalyzer = new TokensAnalyzer($tokens);
  66. $modifierKinds = [T_PUBLIC, T_PROTECTED, T_PRIVATE, T_FINAL, T_ABSTRACT, T_NS_SEPARATOR, T_STRING, CT::T_NULLABLE_TYPE, CT::T_ARRAY_TYPEHINT, T_STATIC, CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION];
  67. if (\defined('T_READONLY')) { // @TODO: drop condition when PHP 8.1+ is required
  68. $modifierKinds[] = T_READONLY;
  69. }
  70. $classesCandidate = [];
  71. $classElementTypes = ['method' => true, 'property' => true, 'const' => true];
  72. foreach ($this->tokensAnalyzer->getClassyElements() as $index => $element) {
  73. $classIndex = $element['classIndex'];
  74. if (!isset($classesCandidate[$classIndex])) {
  75. $classesCandidate[$classIndex] = $this->isClassCandidate($tokens, $classIndex);
  76. }
  77. if (false === $classesCandidate[$classIndex]) {
  78. continue;
  79. }
  80. if (!isset($classElementTypes[$element['type']])) {
  81. continue;
  82. }
  83. $previous = $index;
  84. $isProtected = false;
  85. $isFinal = false;
  86. do {
  87. $previous = $tokens->getPrevMeaningfulToken($previous);
  88. if ($tokens[$previous]->isGivenKind(T_PROTECTED)) {
  89. $isProtected = $previous;
  90. } elseif ($tokens[$previous]->isGivenKind(T_FINAL)) {
  91. $isFinal = $previous;
  92. }
  93. } while ($tokens[$previous]->isGivenKind($modifierKinds));
  94. if (false === $isProtected) {
  95. continue;
  96. }
  97. if ($isFinal && 'const' === $element['type']) {
  98. continue; // Final constants cannot be private
  99. }
  100. $element['protected_index'] = $isProtected;
  101. $tokens[$element['protected_index']] = new Token([T_PRIVATE, 'private']);
  102. }
  103. }
  104. /**
  105. * Consider symbol as candidate for fixing if it's:
  106. * - an Enum (PHP8.1+)
  107. * - a class, which:
  108. * - is not anonymous
  109. * - is final
  110. * - does not use traits
  111. * - does not extend other class.
  112. */
  113. private function isClassCandidate(Tokens $tokens, int $classIndex): bool
  114. {
  115. if (\defined('T_ENUM') && $tokens[$classIndex]->isGivenKind(T_ENUM)) { // @TODO: drop condition when PHP 8.1+ is required
  116. return true;
  117. }
  118. if (!$tokens[$classIndex]->isGivenKind(T_CLASS) || $this->tokensAnalyzer->isAnonymousClass($classIndex)) {
  119. return false;
  120. }
  121. $modifiers = $this->tokensAnalyzer->getClassyModifiers($classIndex);
  122. if (!isset($modifiers['final'])) {
  123. return false;
  124. }
  125. $classNameIndex = $tokens->getNextMeaningfulToken($classIndex); // move to class name as anonymous class is never "final"
  126. $classExtendsIndex = $tokens->getNextMeaningfulToken($classNameIndex); // move to possible "extends"
  127. if ($tokens[$classExtendsIndex]->isGivenKind(T_EXTENDS)) {
  128. return false;
  129. }
  130. if (!$tokens->isTokenKindFound(CT::T_USE_TRAIT)) {
  131. return true; // cheap test
  132. }
  133. $classOpenIndex = $tokens->getNextTokenOfKind($classNameIndex, ['{']);
  134. $classCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $classOpenIndex);
  135. $useIndex = $tokens->getNextTokenOfKind($classOpenIndex, [[CT::T_USE_TRAIT]]);
  136. return null === $useIndex || $useIndex > $classCloseIndex;
  137. }
  138. }