Browse Source

Add ArrayIndentationFixer

Julien Falque 7 years ago
parent
commit
5134589586

+ 1 - 0
.php_cs.dist

@@ -18,6 +18,7 @@ $config = PhpCsFixer\Config::create()
         '@Symfony' => true,
         '@Symfony:risky' => true,
         'align_multiline_comment' => true,
+        'array_indentation' => true,
         'array_syntax' => ['syntax' => 'short'],
         'blank_line_before_statement' => true,
         'combine_consecutive_issets' => true,

+ 4 - 0
README.rst

@@ -248,6 +248,10 @@ Choose from the list of available rules:
     whose lines all start with an asterisk (``phpdocs_like``) or any
     multi-line comment (``all_multiline``); defaults to ``'phpdocs_only'``
 
+* **array_indentation**
+
+  Each element of an array must be indented exactly once.
+
 * **array_syntax**
 
   PHP arrays should be declared using the configured syntax.

+ 1 - 1
src/Fixer/PhpUnit/PhpUnitTestAnnotationFixer.php

@@ -59,7 +59,7 @@ class Test extends \\PhpUnit\\FrameWork\\TestCase
 class Test extends \\PhpUnit\\FrameWork\\TestCase
 {
 public function testItDoesSomething() {}}'.$this->whitespacesConfig->getLineEnding(), ['style' => 'annotation']),
-                ],
+            ],
             null,
             'This fixer may change the name of your tests, and could cause incompatibility with'.
             ' abstract classes or interfaces.'

+ 375 - 0
src/Fixer/Whitespace/ArrayIndentationFixer.php

@@ -0,0 +1,375 @@
+<?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\Whitespace;
+
+use PhpCsFixer\AbstractFixer;
+use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
+use PhpCsFixer\FixerDefinition\CodeSample;
+use PhpCsFixer\FixerDefinition\FixerDefinition;
+use PhpCsFixer\Preg;
+use PhpCsFixer\Tokenizer\CT;
+use PhpCsFixer\Tokenizer\Token;
+use PhpCsFixer\Tokenizer\Tokens;
+
+final class ArrayIndentationFixer extends AbstractFixer implements WhitespacesAwareFixerInterface
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefinition()
+    {
+        return new FixerDefinition(
+            'Each element of an array must be indented exactly once.',
+            [
+                new CodeSample("<?php\n\$foo = [\n   'bar' => [\n    'baz' => true,\n  ],\n];\n"),
+            ]
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isCandidate(Tokens $tokens)
+    {
+        return $tokens->isAnyTokenKindsFound([T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN]);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getPriority()
+    {
+        // should run after BracesFixer
+        return -30;
+    }
+
+    protected function applyFix(\SplFileInfo $file, Tokens $tokens)
+    {
+        foreach ($this->findArrays($tokens) as $array) {
+            $indentLevel = 1;
+            $scopes = [[
+                'opening_braces' => $array['start_braces']['opening'],
+                'unindented' => false,
+            ]];
+            $currentScope = 0;
+
+            $arrayIndent = $this->getLineIndentation($tokens, $array['start']);
+            $previousLineInitialIndent = $arrayIndent;
+            $previousLineNewIndent = $arrayIndent;
+
+            foreach ($array['braces'] as $index => $braces) {
+                $currentIndentLevel = $indentLevel;
+                if (
+                    $braces['starts_with_closing']
+                    && !$scopes[$currentScope]['unindented']
+                    && !$this->isClosingLineWithMeaningfulContent($tokens, $index)
+                ) {
+                    --$currentIndentLevel;
+                }
+
+                $token = $tokens[$index];
+                if ($this->newlineIsInArrayScope($tokens, $index, $array)) {
+                    $content = Preg::replace(
+                        '/(\R)[\t ]*$/',
+                        '$1'.$arrayIndent.str_repeat($this->whitespacesConfig->getIndent(), $currentIndentLevel),
+                        $token->getContent()
+                    );
+
+                    $previousLineInitialIndent = $this->extractIndent($token->getContent());
+                    $previousLineNewIndent = $this->extractIndent($content);
+                } else {
+                    $content = Preg::replace(
+                        '/(\R)'.preg_quote($previousLineInitialIndent, '/').'([\t ]*)$/',
+                        '$1'.$previousLineNewIndent.'$2',
+                        $token->getContent()
+                    );
+                }
+
+                $closingBraces = $braces['closing'];
+                while ($closingBraces-- > 0) {
+                    if (!$scopes[$currentScope]['unindented']) {
+                        --$indentLevel;
+                        $scopes[$currentScope]['unindented'] = true;
+                    }
+
+                    if (0 === --$scopes[$currentScope]['opening_braces']) {
+                        array_pop($scopes);
+                        --$currentScope;
+                    }
+                }
+
+                if ($braces['opening'] > 0) {
+                    $scopes[] = [
+                        'opening_braces' => $braces['opening'],
+                        'unindented' => false,
+                    ];
+                    ++$indentLevel;
+                    ++$currentScope;
+                }
+
+                $tokens[$index] = new Token([T_WHITESPACE, $content]);
+            }
+        }
+    }
+
+    private function findArrays(Tokens $tokens)
+    {
+        $arrays = [];
+
+        foreach ($this->findArrayTokenRanges($tokens, 0, count($tokens) - 1) as $arrayTokenRanges) {
+            $array = [
+                'start' => $arrayTokenRanges[0][0],
+                'end' => $arrayTokenRanges[count($arrayTokenRanges) - 1][1],
+                'token_ranges' => $arrayTokenRanges,
+            ];
+
+            $array['start_braces'] = $this->getLineSignificantBraces($tokens, $array['start'] - 1, $array);
+            $array['braces'] = $this->computeArrayLineSignificantBraces($tokens, $array);
+
+            $arrays[] = $array;
+        }
+
+        return $arrays;
+    }
+
+    private function findArrayTokenRanges(Tokens $tokens, $from, $to)
+    {
+        $arrayTokenRanges = [];
+        $currentArray = null;
+
+        for ($index = $from; $index <= $to; ++$index) {
+            $token = $tokens[$index];
+
+            if (null !== $currentArray && $currentArray['end'] === $index) {
+                $rangeIndexes = [$currentArray['start']];
+                foreach ($currentArray['ignored_tokens_ranges'] as list($start, $end)) {
+                    $rangeIndexes[] = $start - 1;
+                    $rangeIndexes[] = $end + 1;
+                }
+                $rangeIndexes[] = $currentArray['end'];
+
+                $arrayTokenRanges[] = array_chunk($rangeIndexes, 2);
+
+                foreach ($currentArray['ignored_tokens_ranges'] as list($start, $end)) {
+                    foreach ($this->findArrayTokenRanges($tokens, $start, $end) as $nestedArray) {
+                        $arrayTokenRanges[] = $nestedArray;
+                    }
+                }
+
+                $currentArray = null;
+
+                continue;
+            }
+
+            if (null === $currentArray && $token->isGivenKind([T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN])) {
+                if ($token->isGivenKind(T_ARRAY)) {
+                    $index = $tokens->getNextTokenOfKind($index, ['(']);
+                }
+
+                $currentArray = [
+                    'start' => $index,
+                    'end' => $tokens->findBlockEnd(
+                        $tokens[$index]->equals('(') ? Tokens::BLOCK_TYPE_PARENTHESIS_BRACE : Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE,
+                        $index
+                    ),
+                    'ignored_tokens_ranges' => [],
+                ];
+
+                continue;
+            }
+
+            if (
+                null !== $currentArray && (
+                    ($token->equals('(') && !$tokens[$tokens->getPrevMeaningfulToken($index)]->isGivenKind(T_ARRAY))
+                    || $token->equals('{')
+                )
+            ) {
+                $endIndex = $tokens->findBlockEnd(
+                    $token->equals('{') ? Tokens::BLOCK_TYPE_CURLY_BRACE : Tokens::BLOCK_TYPE_PARENTHESIS_BRACE,
+                    $index
+                );
+
+                $currentArray['ignored_tokens_ranges'][] = [$index, $endIndex];
+
+                $index = $endIndex;
+
+                continue;
+            }
+        }
+
+        return $arrayTokenRanges;
+    }
+
+    private function computeArrayLineSignificantBraces(Tokens $tokens, array $array)
+    {
+        $braces = [];
+
+        for ($index = $array['start']; $index <= $array['end']; ++$index) {
+            if (!$this->isNewLineToken($tokens, $index)) {
+                continue;
+            }
+
+            $braces[$index] = $this->getLineSignificantBraces($tokens, $index, $array);
+        }
+
+        return $braces;
+    }
+
+    private function getLineSignificantBraces(Tokens $tokens, $index, array $array)
+    {
+        $deltas = [];
+
+        for (++$index; $index <= $array['end']; ++$index) {
+            if ($this->isNewLineToken($tokens, $index)) {
+                break;
+            }
+
+            if (!$this->indexIsInArrayTokenRanges($index, $array)) {
+                continue;
+            }
+
+            $token = $tokens[$index];
+            if ($token->equals('(') && !$tokens[$tokens->getPrevMeaningfulToken($index)]->isGivenKind(T_ARRAY)) {
+                continue;
+            }
+
+            if ($token->equals(')')) {
+                $openBraceIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
+                if (!$tokens[$tokens->getPrevMeaningfulToken($openBraceIndex)]->isGivenKind(T_ARRAY)) {
+                    continue;
+                }
+            }
+
+            if ($token->equalsAny(['(', [CT::T_ARRAY_SQUARE_BRACE_OPEN]])) {
+                $deltas[] = 1;
+
+                continue;
+            }
+
+            if ($token->equalsAny([')', [CT::T_ARRAY_SQUARE_BRACE_CLOSE]])) {
+                $deltas[] = -1;
+            }
+        }
+
+        $braces = [
+            'opening' => 0,
+            'closing' => 0,
+            'starts_with_closing' => -1 === reset($deltas),
+        ];
+
+        foreach ($deltas as $delta) {
+            if (1 === $delta) {
+                ++$braces['opening'];
+            } elseif ($braces['opening'] > 0) {
+                --$braces['opening'];
+            } else {
+                ++$braces['closing'];
+            }
+        }
+
+        return $braces;
+    }
+
+    private function isClosingLineWithMeaningfulContent(Tokens $tokens, $newLineIndex)
+    {
+        $nextMeaningfulIndex = $tokens->getNextMeaningfulToken($newLineIndex);
+
+        return !$tokens[$nextMeaningfulIndex]->equalsAny([')', [CT::T_ARRAY_SQUARE_BRACE_CLOSE]]);
+    }
+
+    private function getLineIndentation(Tokens $tokens, $index)
+    {
+        $newlineTokenIndex = $this->getPreviousNewlineTokenIndex($tokens, $index);
+
+        if (null === $newlineTokenIndex) {
+            return '';
+        }
+
+        return $this->extractIndent($this->computeNewLineContent($tokens, $newlineTokenIndex));
+    }
+
+    private function extractIndent($content)
+    {
+        if (Preg::match('/\R([\t ]*)$/', $content, $matches)) {
+            return $matches[1];
+        }
+
+        return '';
+    }
+
+    private function getPreviousNewlineTokenIndex(Tokens $tokens, $index)
+    {
+        while ($index > 0) {
+            $index = $tokens->getPrevTokenOfKind($index, [[T_WHITESPACE], [T_INLINE_HTML]]);
+
+            if (null === $index) {
+                break;
+            }
+
+            if ($this->isNewLineToken($tokens, $index)) {
+                return $index;
+            }
+        }
+
+        return null;
+    }
+
+    private function newlineIsInArrayScope(Tokens $tokens, $index, array $array)
+    {
+        if ($tokens[$tokens->getPrevMeaningfulToken($index)]->equalsAny(['.', '?', ':'])) {
+            return false;
+        }
+
+        $nextToken = $tokens[$tokens->getNextMeaningfulToken($index)];
+        if ($nextToken->isGivenKind(T_OBJECT_OPERATOR) || $nextToken->equalsAny(['.', '?', ':'])) {
+            return false;
+        }
+
+        return $this->indexIsInArrayTokenRanges($index, $array);
+    }
+
+    private function indexIsInArrayTokenRanges($index, array $array)
+    {
+        foreach ($array['token_ranges'] as list($start, $end)) {
+            if ($index < $start) {
+                return false;
+            }
+
+            if ($index <= $end) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private function isNewLineToken(Tokens $tokens, $index)
+    {
+        if (!$tokens[$index]->equalsAny([[T_WHITESPACE], [T_INLINE_HTML]])) {
+            return false;
+        }
+
+        return (bool) Preg::match('/\R/', $this->computeNewLineContent($tokens, $index));
+    }
+
+    private function computeNewLineContent(Tokens $tokens, $index)
+    {
+        $content = $tokens[$index]->getContent();
+
+        if (0 !== $index && $tokens[$index - 1]->equalsAny([[T_OPEN_TAG], [T_CLOSE_TAG]])) {
+            $content = Preg::replace('/\S/', '', $tokens[$index - 1]->getContent()).$content;
+        }
+
+        return $content;
+    }
+}

+ 1 - 0
tests/AutoReview/FixerFactoryTest.php

@@ -62,6 +62,7 @@ final class FixerFactoryTest extends TestCase
             [$fixers['array_syntax'], $fixers['ternary_operator_spaces']],
             [$fixers['backtick_to_shell_exec'], $fixers['escape_implicit_backslashes']],
             [$fixers['blank_line_after_opening_tag'], $fixers['no_blank_lines_before_namespace']],
+            [$fixers['braces'], $fixers['array_indentation']],
             [$fixers['class_attributes_separation'], $fixers['braces']],
             [$fixers['class_attributes_separation'], $fixers['indentation_type']],
             [$fixers['class_keyword_remove'], $fixers['no_unused_imports']],

+ 10 - 10
tests/Console/ConfigurationResolverTest.php

@@ -969,16 +969,16 @@ final class ConfigurationResolverTest extends TestCase
         );
 
         $resolver = $this->createConfigurationResolver([
-                'path-mode' => 'intersection',
-                'allow-risky' => 'yes',
-                'config' => null,
-                'dry-run' => true,
-                'rules' => 'php_unit_construct',
-                'using-cache' => false,
-                'diff' => true,
-                'diff-format' => 'udiff',
-                'format' => 'json',
-                'stop-on-violation' => true,
+            'path-mode' => 'intersection',
+            'allow-risky' => 'yes',
+            'config' => null,
+            'dry-run' => true,
+            'rules' => 'php_unit_construct',
+            'using-cache' => false,
+            'diff' => true,
+            'diff-format' => 'udiff',
+            'format' => 'json',
+            'stop-on-violation' => true,
         ]);
 
         $this->assertTrue($resolver->shouldStopOnViolation());

+ 6 - 6
tests/Fixer/Alias/BacktickToShellExecFixerTest.php

@@ -46,28 +46,28 @@ final class BacktickToShellExecFixerTest extends AbstractFixerTestCase
                 '<?php `$var1 ls ${var2} -lah {$var3} file1.txt {$var4[0]} file2.txt {$var5->call()}`;',
             ],
             'with single quote' => [
-<<<'EOT'
+                <<<'EOT'
 <?php
 `echo a\'b`;
 `echo 'ab'`;
 EOT
-,
+                ,
             ],
             'with double quote' => [
-<<<'EOT'
+                <<<'EOT'
 <?php
 `echo a\"b`;
 `echo 'a"b'`;
 EOT
-,
+                ,
             ],
             'with backtick' => [
-<<<'EOT'
+                <<<'EOT'
 <?php
 `echo 'a\`b'`;
 `echo a\\\`b`;
 EOT
-,
+                ,
             ],
         ];
     }

+ 4 - 4
tests/Fixer/Basic/BracesFixerTest.php

@@ -5221,7 +5221,7 @@ NOWDOC;
 }
 
 EOT
-,
+                ,
                 <<<'EOT'
 <?php
 if (true) {
@@ -5232,7 +5232,7 @@ NOWDOC;
 }
 
 EOT
-,
+                ,
             ],
             [
                 <<<'EOT'
@@ -5245,7 +5245,7 @@ HEREDOC;
 }
 
 EOT
-,
+                ,
                 <<<'EOT'
 <?php
 if (true) {
@@ -5256,7 +5256,7 @@ HEREDOC;
 }
 
 EOT
-,
+                ,
             ],
         ];
     }

+ 4 - 4
tests/Fixer/ClassNotation/ClassAttributesSeparationFixerTest.php

@@ -169,7 +169,7 @@ class SomeClass2
     }
 }
             ',
-            ];
+        ];
         $cases[] = [
             '<?php
 class SomeClass3
@@ -734,7 +734,7 @@ private $a;
 
 function getF(){echo 4;}
 }',
-    '<?php
+            '<?php
 trait ezcReflectionReturnInfo {
     public $x = 1;
     protected function getA(){echo 1;}function getB(){echo 2;}
@@ -760,7 +760,7 @@ trait SomeReturnInfo {
 
     abstract public function getWorld();
 }',
-    '<?php
+            '<?php
 trait SomeReturnInfo {
     function getReturnType()
     {
@@ -849,7 +849,7 @@ function afterUseTrait(){}
 
 function afterUseTrait2(){}
 }',
-'<?php
+            '<?php
 trait ezcReflectionReturnInfo {
     function getReturnDescription() {}
 }

+ 10 - 10
tests/Fixer/Comment/MultilineCommentOpeningClosingFixerTest.php

@@ -55,43 +55,43 @@ final class MultilineCommentOpeningClosingFixerTest extends AbstractFixerTestCas
                 '<?php /* Closing Multiline comment ***/',
             ],
             [
-<<<'EOT'
+                <<<'EOT'
 <?php
 
 /*
  * WUT
  */
 EOT
-,
-<<<'EOT'
+                ,
+                <<<'EOT'
 <?php
 
 /********
  * WUT
  ********/
 EOT
-,
+                ,
             ],
             [
-<<<'EOT'
+                <<<'EOT'
 <?php
 
 /*\
  * False DocBlock
  */
 EOT
-,
-<<<'EOT'
+                ,
+                <<<'EOT'
 <?php
 
 /**\
  * False DocBlock
  */
 EOT
-,
+                ,
             ],
             [
-<<<'EOT'
+                <<<'EOT'
 <?php
 # Hash
 #*** Hash asterisk
@@ -105,7 +105,7 @@ Weird multiline comment
 */
 
 EOT
-,
+                ,
             ],
         ];
     }

Some files were not shown because too many files changed in this diff