Completed
Pull Request — master (#54)
by
unknown
01:45
created

IndentationLevelSniff::findNestedTokens()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 8
nc 2
nop 2
1
<?php
2
3
namespace Codor\Sniffs\Files;
4
5
use PHP_CodeSniffer_Sniff;
6
use 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()
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);
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]);
0 ignored issues
show
Documentation Bug introduced by
It seems like max($this->maxIndentFoun...elativeScopeLevels[$i]) can also be of type double. However, the property $maxIndentFound is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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, $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, $type)
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, $type)
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()
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