Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
iamacarpet committed Oct 11, 2022
0 parents commit 6de99d8
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/.php-cs-fixer.cache
/composer.lock
/vendor
34 changes: 34 additions & 0 deletions .php-cs-fixer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

require_once __DIR__.'/vendor/autoload.php';

$finder = PhpCsFixer\Finder::create()
->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;
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -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": "[email protected]"
}
],
"require": {
"php": "^8.1",
"friendsofphp/php-cs-fixer": "~3"
},
"autoload": {
"psr-4": {
"A1comms\\PhpCsFixer\\": "src/"
}
}
}
246 changes: 246 additions & 0 deletions src/Fixer/Operator/NotEmptyTernaryToNullCoalescingFixer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
<?php

declare(strict_types=1);

/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <[email protected]>
* Dariusz Rumiński <[email protected]>
* Samuel Melrose <[email protected]>
*
* 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 <[email protected]>
* @author Samuel Melrose <[email protected]>
*/
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(
"<?php\n\$sample = !empty(\$a) ? \$a : \$b;\n"
),
]
);
}

/**
* {@inheritdoc}
*
* Must run before AssignNullCoalescingToCoalesceEqualFixer.
*/
public function getPriority(): int
{
return 0;
}

/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens): bool
{
return $tokens->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;
}
}

0 comments on commit 6de99d8

Please sign in to comment.