From 6de99d8043b1a6f464eaac51d0a21e1e4cfc3394 Mon Sep 17 00:00:00 2001 From: Samuel Melrose Date: Tue, 11 Oct 2022 12:11:50 +0100 Subject: [PATCH] Initial Commit --- .gitignore | 3 + .php-cs-fixer.php | 34 +++ Makefile | 12 + composer.json | 20 ++ .../NotEmptyTernaryToNullCoalescingFixer.php | 246 ++++++++++++++++++ 5 files changed, 315 insertions(+) create mode 100644 .gitignore create mode 100644 .php-cs-fixer.php create mode 100644 Makefile create mode 100755 composer.json create mode 100644 src/Fixer/Operator/NotEmptyTernaryToNullCoalescingFixer.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4c8743 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.php-cs-fixer.cache +/composer.lock +/vendor \ No newline at end of file diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..2571fd3 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,34 @@ +ignoreDotFiles(false) + ->ignoreVCSIgnored(true) + ->in(__DIR__) +; + +$config = new PhpCsFixer\Config(); +$config + ->registerCustomFixers([ + new \A1comms\PhpCsFixer\Fixer\Operator\NotEmptyTernaryToNullCoalescingFixer(), + ]) + ->setRiskyAllowed(true) + ->setRules([ + '@PHP81Migration' => true, + '@PHP80Migration:risky' => true, + 'heredoc_indentation' => false, + '@PhpCsFixer' => true, + '@PhpCsFixer:risky' => true, + 'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false], + 'binary_operator_spaces' => [ + 'default' => 'align', + ], + 'A1comms/not_empty_ternary_to_null_coalescing' => true, + ]) + ->setFinder($finder) +; + +return $config; diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..754c263 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +THIS := $(realpath $(lastword $(MAKEFILE_LIST))) +HERE := $(shell dirname $(THIS)) + +.PHONY: all fix audit + +all: audit + +fix: + php -n -dmemory_limit=12G -dzend_extension=opcache.so -dopcache.enable_cli=On -dopcache.jit_buffer_size=128M $(HERE)/vendor/bin/php-cs-fixer fix -vvv --config=$(HERE)/.php-cs-fixer.php + +audit: + php -n -dmemory_limit=12G -dzend_extension=opcache.so -dopcache.enable_cli=On -dopcache.jit_buffer_size=128M $(HERE)/vendor/bin/php-cs-fixer fix -vvv --config=$(HERE)/.php-cs-fixer.php --dry-run diff --git a/composer.json b/composer.json new file mode 100755 index 0000000..473eede --- /dev/null +++ b/composer.json @@ -0,0 +1,20 @@ +{ + "name": "a1comms/php-cs-fixer-rules", + "description": "Code formatting rules for php-cs-fixer", + "license": "MIT", + "authors": [ + { + "name": "Samuel Melrose", + "email": "sam.melrose@a1comms.com" + } + ], + "require": { + "php": "^8.1", + "friendsofphp/php-cs-fixer": "~3" + }, + "autoload": { + "psr-4": { + "A1comms\\PhpCsFixer\\": "src/" + } + } +} diff --git a/src/Fixer/Operator/NotEmptyTernaryToNullCoalescingFixer.php b/src/Fixer/Operator/NotEmptyTernaryToNullCoalescingFixer.php new file mode 100644 index 0000000..8865668 --- /dev/null +++ b/src/Fixer/Operator/NotEmptyTernaryToNullCoalescingFixer.php @@ -0,0 +1,246 @@ + + * Dariusz RumiƄski + * Samuel Melrose + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace A1comms\PhpCsFixer\Fixer\Operator; + +use PhpCsFixer\AbstractFixer; +use PhpCsFixer\FixerDefinition\CodeSample; +use PhpCsFixer\FixerDefinition\FixerDefinition; +use PhpCsFixer\FixerDefinition\FixerDefinitionInterface; +use PhpCsFixer\Tokenizer\Token; +use PhpCsFixer\Tokenizer\Tokens; + +/** + * @author Filippo Tessarotto + * @author Samuel Melrose + */ +final class NotEmptyTernaryToNullCoalescingFixer extends AbstractFixer +{ + /** + * Returns the name of the fixer. + * + * The name must be all lowercase and without any spaces. + * + * @return string The name of the fixer + */ + public function getName(): string + { + return sprintf('A1comms/%s', parent::getName()); + } + + /** + * {@inheritdoc} + */ + public function getDefinition(): FixerDefinitionInterface + { + return new FixerDefinition( + 'Use `null` coalescing operator `??` where possible. Requires PHP >= 7.0.', + [ + new CodeSample( + "isTokenKindFound(T_EMPTY); + } + + /** + * {@inheritdoc} + */ + protected function applyFix(\SplFileInfo $file, Tokens $tokens): void + { + $emptyIndices = array_keys($tokens->findGivenKind(T_EMPTY)); + + while ($emptyIndex = array_pop($emptyIndices)) { + $this->fixEmpty($tokens, $emptyIndex); + } + } + + /** + * @param int $index of `T_EMPTY` token + */ + private function fixEmpty(Tokens $tokens, int $index): void + { + $preTokenIndex = $tokens->getPrevMeaningfulToken($index); + if (!$tokens[$preTokenIndex]->equals('!')) { + return; // we are not in a !empty statement + } + + $prevTokenIndex = $tokens->getPrevMeaningfulToken($preTokenIndex); + + if ($this->isHigherPrecedenceAssociativityOperator($tokens[$prevTokenIndex])) { + return; + } + + $startBraceIndex = $tokens->getNextTokenOfKind($index, ['(']); + $endBraceIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $startBraceIndex); + + $ternaryQuestionMarkIndex = $tokens->getNextMeaningfulToken($endBraceIndex); + + if (!$tokens[$ternaryQuestionMarkIndex]->equals('?')) { + return; // we are not in a ternary operator + } + + // search what is inside the !empty() + $emptyTokens = $this->getMeaningfulSequence($tokens, $startBraceIndex, $endBraceIndex); + + if ($this->hasChangingContent($emptyTokens)) { + return; // some weird stuff inside the empty + } + + // search what is inside the middle argument of ternary operator + $ternaryColonIndex = $tokens->getNextTokenOfKind($ternaryQuestionMarkIndex, [':']); + $ternaryFirstOperandTokens = $this->getMeaningfulSequence($tokens, $ternaryQuestionMarkIndex, $ternaryColonIndex); + + if ($emptyTokens->generateCode() !== $ternaryFirstOperandTokens->generateCode()) { + return; // regardless of non-meaningful tokens, the operands are different + } + + $ternaryFirstOperandIndex = $tokens->getNextMeaningfulToken($ternaryQuestionMarkIndex); + + // preserve comments and spaces + $comments = []; + $commentStarted = false; + + for ($loopIndex = $index; $loopIndex < $ternaryFirstOperandIndex; ++$loopIndex) { + if ($tokens[$loopIndex]->isComment()) { + $comments[] = $tokens[$loopIndex]; + $commentStarted = true; + } elseif ($commentStarted) { + if ($tokens[$loopIndex]->isWhitespace()) { + $comments[] = $tokens[$loopIndex]; + } + + $commentStarted = false; + } + } + + $tokens[$ternaryColonIndex] = new Token([T_COALESCE, '??']); + $tokens->overrideRange($preTokenIndex, $ternaryFirstOperandIndex - 1, $comments); + } + + /** + * Get the sequence of meaningful tokens and returns a new Tokens instance. + * + * @param int $start start index + * @param int $end end index + */ + private function getMeaningfulSequence(Tokens $tokens, int $start, int $end): Tokens + { + $sequence = []; + $index = $start; + + while ($index < $end) { + $index = $tokens->getNextMeaningfulToken($index); + + if ($index >= $end || $index === null) { + break; + } + + $sequence[] = $tokens[$index]; + } + + return Tokens::fromArray($sequence); + } + + /** + * Check if the requested token is an operator computed + * before the ternary operator along with the `empty()`. + */ + private function isHigherPrecedenceAssociativityOperator(Token $token): bool + { + static $operatorsPerId = [ + T_ARRAY_CAST => true, + T_BOOLEAN_AND => true, + T_BOOLEAN_OR => true, + T_BOOL_CAST => true, + T_COALESCE => true, + T_DEC => true, + T_DOUBLE_CAST => true, + T_INC => true, + T_INT_CAST => true, + T_IS_EQUAL => true, + T_IS_GREATER_OR_EQUAL => true, + T_IS_IDENTICAL => true, + T_IS_NOT_EQUAL => true, + T_IS_NOT_IDENTICAL => true, + T_IS_SMALLER_OR_EQUAL => true, + T_OBJECT_CAST => true, + T_POW => true, + T_SL => true, + T_SPACESHIP => true, + T_SR => true, + T_STRING_CAST => true, + T_UNSET_CAST => true, + ]; + + static $operatorsPerContent = [ + '!', + '%', + '&', + '*', + '+', + '-', + '/', + ':', + '^', + '|', + '~', + '.', + ]; + + return isset($operatorsPerId[$token->getId()]) || $token->equalsAny($operatorsPerContent); + } + + /** + * Check if the `empty()` content may change if called multiple times. + * + * @param Tokens $tokens The original token list + */ + private function hasChangingContent(Tokens $tokens): bool + { + static $operatorsPerId = [ + T_DEC, + T_INC, + T_YIELD, + T_YIELD_FROM, + ]; + + foreach ($tokens as $token) { + if ($token->isGivenKind($operatorsPerId) || $token->equals('(')) { + return true; + } + } + + return false; + } +}