Skip to content

Commit

Permalink
Add OperatorLinebreakFixer
Browse files Browse the repository at this point in the history
  • Loading branch information
kubawerlos committed Oct 8, 2018
1 parent 818e26c commit 2658c28
Show file tree
Hide file tree
Showing 3 changed files with 448 additions and 0 deletions.
12 changes: 12 additions & 0 deletions README.rst
Expand Up @@ -1186,6 +1186,18 @@ Choose from the list of available rules:
There should not be space before or after object ``T_OBJECT_OPERATOR``
``->``.

* **operator_linebreak**

Binary operators must always be at the beginning or at the end of the
line.

Configuration options:

- ``only_booleans`` (``bool``): whether to limit operators to only boolean ones;
defaults to ``false``
- ``position`` (``'beginning'``, ``'end'``): whether to place operators at the
beginning or at the end of the line; defaults to ``'beginning'``

* **ordered_class_elements**

Orders the elements of classes/interfaces/traits.
Expand Down
229 changes: 229 additions & 0 deletions src/Fixer/Operator/OperatorLinebreakFixer.php
@@ -0,0 +1,229 @@
<?php

/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace PhpCsFixer\Fixer\Operator;

use PhpCsFixer\AbstractFixer;
use PhpCsFixer\Fixer\ConfigurationDefinitionFixerInterface;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;

/**
* @author Kuba Werłos <werlos@gmail.com>
*/
final class OperatorLinebreakFixer extends AbstractFixer implements ConfigurationDefinitionFixerInterface
{
/**
* @internal
*/
const BOOLEAN_OPERATORS = [
'&&',
'||',
'and',
'or',
'xor',
];

/**
* @internal
*/
const NON_BOOLEAN_OPERATORS = [
'=',
'*',
'/',
'%',
'<',
'>',
'|',
'^',
'+',
'-',
'&',
'&=',
'.=',
'/=',
'=>',
'==',
'>=',
'===',
'!=',
'<>',
'!==',
'<=',
'-=',
'%=',
'*=',
'|=',
'+=',
'<<',
'<<=',
'>>',
'>>=',
'^=',
'**',
'**=',
'<=>',
'??',
];

/** @var string[] */
private $operators;

/** @var string */
private $position = 'beginning';

public function __construct()
{
$this->operators = \array_merge(self::BOOLEAN_OPERATORS, self::NON_BOOLEAN_OPERATORS);

parent::__construct();
}

/**
* @return FixerDefinition
*/
public function getDefinition()
{
return new FixerDefinition(
'Binary operators must always be at the beginning or at the end of the line.',
[new CodeSample('<?php
function foo() {
return $bar ||
$baz;
}
')]
);
}

/**
* @return FixerConfigurationResolver
*/
public function getConfigurationDefinition()
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('only_booleans', 'whether to limit operators to only boolean ones'))
->setDefault(false)
->setAllowedTypes(['bool'])
->getOption(),
(new FixerOptionBuilder('position', 'whether to place operators at the beginning or at the end of the line'))
->setDefault($this->position)
->setAllowedValues(['beginning', 'end'])
->getOption(),
]);
}

/**
* {@inheritdoc}
*/
public function configure(array $configuration = null)
{
parent::configure($configuration);

if (isset($configuration['only_booleans']) && $configuration['only_booleans']) {
$this->operators = self::BOOLEAN_OPERATORS;
}
$this->position = isset($configuration['position']) ? $configuration['position'] : $this->position;
}

/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens)
{
return true;
}

/**
* {@inheritdoc}
*/
public function isRisky()
{
return false;
}

/**
* {@inheritdoc}
*/
public function getPriority()
{
return 0;
}

/**
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens)
{
if ('beginning' === $this->position) {
$this->fixMoveToTheBeginning($tokens);
} else {
$this->fixMoveToTheEnd($tokens);
}
}

private function fixMoveToTheBeginning(Tokens $tokens)
{
for ($index = 0; $index < $tokens->count(); ++$index) {
$tokenContent = \strtolower($tokens[$index]->getContent());

if (!\in_array($tokenContent, $this->operators, true)) {
continue;
}

$nextIndex = $tokens->getNextMeaningfulToken($index);
for ($i = $nextIndex - 1; $i > $index; --$i) {
if ($tokens[$i]->isWhitespace() && 1 === Preg::match('/\R/u', $tokens[$i]->getContent())) {
$operator = clone $tokens[$index];
$tokens->clearAt($index);
if ($tokens[$index - 1]->isWhitespace()) {
$tokens->clearTokenAndMergeSurroundingWhitespace($index - 1);
}
$tokens->insertAt($nextIndex, [$operator, new Token([T_WHITESPACE, ' '])]);

break;
}
}
$index = $nextIndex;
}
}

private function fixMoveToTheEnd(Tokens $tokens)
{
for ($index = $tokens->count() - 1; $index > 0; --$index) {
$tokenContent = \strtolower($tokens[$index]->getContent());

if (!\in_array($tokenContent, $this->operators, true)) {
continue;
}

$prevIndex = $tokens->getPrevMeaningfulToken($index);
for ($i = $prevIndex + 1; $i < $index; ++$i) {
if ($tokens[$i]->isWhitespace() && 1 === Preg::match('/\R/u', $tokens[$i]->getContent())) {
$operator = clone $tokens[$index];
$tokens->clearAt($index);
if ($tokens[$index + 1]->isWhitespace()) {
$tokens->clearTokenAndMergeSurroundingWhitespace($index + 1);
}
$tokens->insertAt($prevIndex + 1, [new Token([T_WHITESPACE, ' ']), $operator]);

break;
}
}
$index = $prevIndex;
}
}
}

0 comments on commit 2658c28

Please sign in to comment.