* 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\ConfigurationDefinitionFixerInterface; use PhpCsFixer\FixerConfiguration\AllowedValueSubset; use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverRootless; use PhpCsFixer\FixerConfiguration\FixerOptionBuilder; use PhpCsFixer\FixerConfiguration\InvalidOptionsForEnvException; use PhpCsFixer\FixerDefinition\CodeSample; use PhpCsFixer\FixerDefinition\FixerDefinition; use PhpCsFixer\FixerDefinition\VersionSpecification; use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample; use PhpCsFixer\Tokenizer\Token; use PhpCsFixer\Tokenizer\Tokens; use PhpCsFixer\Tokenizer\TokensAnalyzer; use Symfony\Component\OptionsResolver\Options; /** * Fixer for rules defined in PSR2 ¶4.3, ¶4.5. * * @author Dariusz Rumiński * @author SpacePossum */ final class VisibilityRequiredFixer extends AbstractFixer implements ConfigurationDefinitionFixerInterface { /** * {@inheritdoc} */ public function getDefinition() { return new FixerDefinition( 'Visibility MUST be declared on all properties and methods; `abstract` and `final` MUST be declared before the visibility; `static` MUST be declared after the visibility.', [ new CodeSample( ' ['const']] ), ] ); } /** * {@inheritdoc} */ public function isCandidate(Tokens $tokens) { return $tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds()); } /** * {@inheritdoc} */ protected function applyFix(\SplFileInfo $file, Tokens $tokens) { $tokensAnalyzer = new TokensAnalyzer($tokens); $elements = $tokensAnalyzer->getClassyElements(); foreach (array_reverse($elements, true) as $index => $element) { if (!\in_array($element['type'], $this->configuration['elements'], true)) { continue; } if ('method' === $element['type']) { $this->fixMethodVisibility($tokens, $index); } elseif ('property' === $element['type']) { $this->fixPropertyVisibility($tokens, $index); } elseif ('const' === $element['type']) { $this->fixConstVisibility($tokens, $index); } } } /** * {@inheritdoc} */ protected function createConfigurationDefinition() { return new FixerConfigurationResolverRootless('elements', [ (new FixerOptionBuilder('elements', 'The structural elements to fix (PHP >= 7.1 required for `const`).')) ->setAllowedTypes(['array']) ->setAllowedValues([new AllowedValueSubset(['property', 'method', 'const'])]) ->setNormalizer(static function (Options $options, $value) { if (\PHP_VERSION_ID < 70100 && \in_array('const', $value, true)) { throw new InvalidOptionsForEnvException('"const" option can only be enabled with PHP 7.1+.'); } return $value; }) ->setDefault(['property', 'method']) ->getOption(), ], $this->getName()); } /** * @param Tokens $tokens * @param int $index */ private function fixMethodVisibility(Tokens $tokens, $index) { $this->overrideAttribs($tokens, $index, $this->grabAttribsBeforeMethodToken($tokens, $index)); // force whitespace between function keyword and function name to be single space char if ($tokens[$index + 1]->isWhitespace()) { $tokens[$index + 1] = new Token([T_WHITESPACE, ' ']); } } /** * @param Tokens $tokens * @param int $index */ private function fixPropertyVisibility(Tokens $tokens, $index) { $prevIndex = $tokens->getPrevTokenOfKind($index, [';', ',', '{']); if (null === $prevIndex || !$tokens[$prevIndex]->equals(',')) { $this->overrideAttribs($tokens, $index, $this->grabAttribsBeforePropertyToken($tokens, $index)); } } /** * @param Tokens $tokens * @param int $index */ private function fixConstVisibility(Tokens $tokens, $index) { $prev = $tokens->getPrevMeaningfulToken($index); if ($tokens[$prev]->isGivenKind([T_PRIVATE, T_PROTECTED, T_PUBLIC])) { return; } $tokens->insertAt($index, new Token([T_WHITESPACE, ' '])); $tokens->insertAt($index, new Token([T_PUBLIC, 'public'])); } /** * Grab attributes before method token at given index. * * It's a shorthand for grabAttribsBeforeToken method. * * @param Tokens $tokens Tokens collection * @param int $index token index * * @return array map of grabbed attributes, key is attribute name and value is array of index and clone of Token */ private function grabAttribsBeforeMethodToken(Tokens $tokens, $index) { static $tokenAttribsMap = [ T_PRIVATE => 'visibility', T_PROTECTED => 'visibility', T_PUBLIC => 'visibility', T_ABSTRACT => 'abstract', T_FINAL => 'final', T_STATIC => 'static', ]; return $this->grabAttribsBeforeToken( $tokens, $index, $tokenAttribsMap, [ 'abstract' => null, 'final' => null, 'visibility' => ['index' => null, 'token' => new Token([T_PUBLIC, 'public'])], 'static' => null, ] ); } /** * Apply token attributes. * * Token at given index is prepended by attributes. * * @param Tokens $tokens Tokens collection * @param int $memberIndex token index * @param array $attribs map of grabbed attributes, key is attribute name and value is array of index and clone of Token */ private function overrideAttribs(Tokens $tokens, $memberIndex, array $attribs) { $toOverride = []; $firstAttribIndex = $memberIndex; foreach ($attribs as $attrib) { if (null === $attrib) { continue; } if (null !== $attrib['index']) { $firstAttribIndex = min($firstAttribIndex, $attrib['index']); } if (!$attrib['token']->isGivenKind(T_VAR) && '' !== $attrib['token']->getContent()) { $toOverride[] = $attrib['token']; $toOverride[] = new Token([T_WHITESPACE, ' ']); } } if (!empty($toOverride)) { $tokens->overrideRange($firstAttribIndex, $memberIndex - 1, $toOverride); } } /** * Grab attributes before property token at given index. * * It's a shorthand for grabAttribsBeforeToken method. * * @param Tokens $tokens Tokens collection * @param int $index token index * * @return array map of grabbed attributes, key is attribute name and value is array of index and clone of Token */ private function grabAttribsBeforePropertyToken(Tokens $tokens, $index) { static $tokenAttribsMap = [ T_VAR => 'var', T_PRIVATE => 'visibility', T_PROTECTED => 'visibility', T_PUBLIC => 'visibility', T_STATIC => 'static', ]; return $this->grabAttribsBeforeToken( $tokens, $index, $tokenAttribsMap, [ 'visibility' => ['index' => null, 'token' => new Token([T_PUBLIC, 'public'])], 'static' => null, ] ); } /** * Grab info about attributes before token at given index. * * @param Tokens $tokens Tokens collection * @param int $index token index * @param array $tokenAttribsMap token to attribute name map * @param array $attribs array of token attributes * * @return array map of grabbed attributes, key is attribute name and value is array of index and clone of Token */ private function grabAttribsBeforeToken(Tokens $tokens, $index, array $tokenAttribsMap, array $attribs) { while (true) { $token = $tokens[--$index]; if (!$token->isArray()) { if ($token->equalsAny(['{', '}', '(', ')'])) { break; } continue; } // if token is attribute, set token attribute name if (isset($tokenAttribsMap[$token->getId()])) { $attribs[$tokenAttribsMap[$token->getId()]] = [ 'token' => clone $token, 'index' => $index, ]; continue; } if ($token->isGivenKind([T_WHITESPACE, T_COMMENT, T_DOC_COMMENT])) { continue; } break; } return $attribs; } }