IndentationLevelSniff   A
last analyzed

Complexity

Total Complexity 14

Size/Duplication

Total Lines 148
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 14
eloc 39
c 4
b 0
f 0
dl 0
loc 148
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A removeTokenScopes() 0 3 1
A register() 0 3 1
A inspectScope() 0 12 2
A process() 0 18 3
A getErrorMessage() 0 9 1
A findNestedTokens() 0 13 2
A setRelativeScopeLevels() 0 14 4
1
<?php declare(strict_types = 1);
2
3
namespace Codor\Sniffs\Files;
4
5
use PHP_CodeSniffer\Sniffs\Sniff as PHP_CodeSniffer_Sniff;
6
use PHP_CodeSniffer\Files\File as PHP_CodeSniffer_File;
7
8
class IndentationLevelSniff implements PHP_CodeSniffer_Sniff
9
{
10
    /**
11
     * Indentations cannot be >= to this number.
12
     * @var integer
13
     */
14
    public $indentationLimit = 2;
15
16
    /**
17
     * The highest indentation level found.
18
     * @var integer
19
     */
20
    protected $maxIndentFound = 0;
21
22
    /**
23
     * This array contains the relative scope level per token.
24
     * It is increased inside try-catch blocks.
25
     * @var array
26
     */
27
    protected $relativeScopeLevels = [];
28
29
    /**
30
     * Returns the token types that this sniff is interested in.
31
     * @return array
32
     */
33
    public function register(): array
34
    {
35
        return [T_FUNCTION, T_CLOSURE, T_SWITCH];
36
    }
37
38
    /**
39
     * Processes the tokens that this sniff is interested in.
40
     *
41
     * @param PHP_CodeSniffer_File $phpcsFile The file where the token was found.
42
     * @param integer              $stackPtr  The position in the stack where
43
     *                                    the token was found.
44
     * @return void
45
     */
46
    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
47
    {
48
        $tokens = $phpcsFile->getTokens();
49
        $token = $tokens[$stackPtr];
50
        $this->maxIndentFound = 0;
51
52
        // Ignore functions with no body
53
        if (isset($token['scope_opener']) === false) {
54
            return;
55
        }
56
57
        $this->inspectScope($token, $tokens);
58
59
        if ($this->maxIndentFound <= $this->indentationLimit) {
60
            return;
61
        }
62
63
        $phpcsFile->addError($this->getErrorMessage(), $stackPtr, __CLASS__);
64
    }
65
66
    /**
67
     * Inspect the tokens in the scope of the provided $token.
68
     * @param  array $token  Token Data.
69
     * @param  array $tokens Tokens.
70
     * @return void
71
     */
72
    protected function inspectScope(array $token, array $tokens)
73
    {
74
        $start = $token['scope_opener'];
75
        $length = $token['scope_closer'] - $start + 1;
76
77
        $scope = array_slice($tokens, $start, $length, true);
78
        $scope = $this->removeTokenScopes($scope, 'T_SWITCH');
79
80
        $this->setRelativeScopeLevels($scope, $scope[$start]['level']);
81
82
        foreach ($scope as $i => $token) {
83
            $this->maxIndentFound = max($this->maxIndentFound, $token['level'] - $this->relativeScopeLevels[$i]);
84
        }
85
    }
86
87
    /**
88
     * Set the relative scope level per token.
89
     * In a try-catch block the relative scope is one higher.
90
     * @param  array   $scope The tokens in a scope.
91
     * @param  integer $level The base level of the relative scopes.
92
     * @return void
93
     */
94
    protected function setRelativeScopeLevels(array $scope, int $level)
95
    {
96
        // first set the base level for all tokens
97
        foreach (array_keys($scope) as $i) {
98
            $this->relativeScopeLevels[$i] = $level;
99
        }
100
101
        // then increase the base level by one for all the tokens in a try-catch block
102
        foreach (array_keys($this->findNestedTokens($scope, 'T_TRY')) as $i) {
103
            $this->relativeScopeLevels[$i] += 1;
104
        }
105
106
        foreach (array_keys($this->findNestedTokens($scope, 'T_CATCH')) as $i) {
107
            $this->relativeScopeLevels[$i] += 1;
108
        }
109
    }
110
111
    /**
112
     * Remove the bodies of given token type from the scope.
113
     * @param  array  $scope The tokens in a scope.
114
     * @param  string $type  The type of token to remove from the scope.
115
     * @return array  $scope The tokens scope without the removed tokens.
116
     */
117
    protected function removeTokenScopes(array $scope, string $type): array
118
    {
119
        return array_diff_key($scope, $this->findNestedTokens($scope, $type));
120
    }
121
122
    /**
123
     * Find the tokens nested in the scope of given token type.
124
     * @param  array  $scope The tokens in a scope.
125
     * @param  string $type  The type of token to find in the scope.
126
     * @return array  $scope The nested tokens.
127
     */
128
    protected function findNestedTokens(array $scope, string $type): array
129
    {
130
        $typeTokens = array_filter($scope, function ($token) use ($type) {
131
            return $token['type'] == $type;
132
        });
133
134
        $nestedTokens = [];
135
        foreach ($typeTokens as $token) {
136
            $range = array_flip(range($token['scope_opener'], $token['scope_closer']));
137
            $nestedTokens += array_intersect_key($scope, $range);
138
        }
139
140
        return $nestedTokens;
141
    }
142
143
    /**
144
     * Produce the error message.
145
     * @return string
146
     */
147
    protected function getErrorMessage(): string
148
    {
149
        // Hack to fix the output numbers for the
150
        // indentation levels found and the
151
        // indentation limit.
152
        $indentationFound = $this->maxIndentFound - 1;
153
        $indentationLimit = $this->indentationLimit - 1;
154
        return "{$indentationFound} indentation levels found. " .
155
        "Maximum of {$indentationLimit} indentation levels allowed.";
156
    }
157
}
158