* 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\ClassNotation; use PhpCsFixer\AbstractFixer; use PhpCsFixer\Fixer\ConfigurableFixerInterface; use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver; use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface; use PhpCsFixer\FixerConfiguration\FixerOptionBuilder; use PhpCsFixer\FixerDefinition\CodeSample; use PhpCsFixer\FixerDefinition\FixerDefinition; use PhpCsFixer\FixerDefinition\FixerDefinitionInterface; use PhpCsFixer\Tokenizer\Tokens; use PhpCsFixer\Tokenizer\TokensAnalyzer; /** * @author Filippo Tessarotto */ final class NoUnneededFinalMethodFixer extends AbstractFixer implements ConfigurableFixerInterface { /** * {@inheritdoc} */ public function getDefinition(): FixerDefinitionInterface { return new FixerDefinition( 'A `final` class must not have `final` methods and `private` methods must not be `final`.', [ new CodeSample( ' false] ), ], null, 'Risky when child class overrides a `private` method.' ); } /** * {@inheritdoc} */ public function isCandidate(Tokens $tokens): bool { return $tokens->isAllTokenKindsFound([T_CLASS, T_FINAL]); } public function isRisky(): bool { return true; } /** * {@inheritdoc} */ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void { foreach ($this->getClassMethods($tokens) as $element) { $index = $element['method_final_index']; if ($element['class_is_final']) { $this->clearFinal($tokens, $index); continue; } if (!$element['method_is_private'] || false === $this->configuration['private_methods'] || $element['method_is_constructor']) { continue; } $this->clearFinal($tokens, $index); } } /** * {@inheritdoc} */ protected function createConfigurationDefinition(): FixerConfigurationResolverInterface { return new FixerConfigurationResolver([ (new FixerOptionBuilder('private_methods', 'Private methods of non-`final` classes must not be declared `final`.')) ->setAllowedTypes(['bool']) ->setDefault(true) ->getOption(), ]); } private function getClassMethods(Tokens $tokens): \Generator { $tokensAnalyzer = new TokensAnalyzer($tokens); $modifierKinds = [T_PUBLIC, T_PROTECTED, T_PRIVATE, T_FINAL, T_ABSTRACT, T_STATIC]; $classesAreFinal = []; $elements = $tokensAnalyzer->getClassyElements(); for (end($elements);; prev($elements)) { $index = key($elements); if (null === $index) { break; } $element = current($elements); if ('method' !== $element['type']) { continue; // not a method } $classIndex = $element['classIndex']; if (!\array_key_exists($classIndex, $classesAreFinal)) { $prevToken = $tokens[$tokens->getPrevMeaningfulToken($classIndex)]; $classesAreFinal[$classIndex] = $prevToken->isGivenKind(T_FINAL); } $element['class_is_final'] = $classesAreFinal[$classIndex]; $element['method_is_constructor'] = '__construct' === strtolower($tokens[$tokens->getNextMeaningfulToken($index)]->getContent()); $element['method_final_index'] = null; $element['method_is_private'] = false; $previous = $index; do { $previous = $tokens->getPrevMeaningfulToken($previous); if ($tokens[$previous]->isGivenKind(T_PRIVATE)) { $element['method_is_private'] = true; } elseif ($tokens[$previous]->isGivenKind(T_FINAL)) { $element['method_final_index'] = $previous; } } while ($tokens[$previous]->isGivenKind($modifierKinds)); yield $element; } } private function clearFinal(Tokens $tokens, ?int $index): void { if (null === $index) { return; } $tokens->clearAt($index); ++$index; if ($tokens[$index]->isWhitespace()) { $tokens->clearAt($index); } } }