Passed
Push — main ( 41d632...dfc5c9 )
by mikhail
03:00
created

MissingDocBlockAnalyzer::isAnonymousClass()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 1
eloc 1
c 3
b 1
f 0
nc 1
nop 2
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SavinMikhail\CommentsDensity;
6
7
use function dd;
8
use function in_array;
9
use function is_array;
10
11
use const T_ARRAY;
12
use const T_CLASS;
13
use const T_CONST;
14
use const T_DOC_COMMENT;
15
use const T_FINAL;
16
use const T_FUNCTION;
17
use const T_INTERFACE;
18
use const T_PAAMAYIM_NEKUDOTAYIM;
19
use const T_PRIVATE;
20
use const T_PROTECTED;
21
use const T_PUBLIC;
22
use const T_READONLY;
23
use const T_STATIC;
24
use const T_STRING;
25
use const T_TRAIT;
26
use const T_ENUM;
27
use const T_VARIABLE;
28
use const T_WHITESPACE;
29
30
final class MissingDocBlockAnalyzer
31
{
32
    private bool $exceedThreshold = false;
33
34
    /**
35
     * Analyzes the tokens of a file for docblocks.
36
     *
37
     * @param array $tokens The tokens to analyze.
38
     * @return array The analysis results.
39
     */
40 19
    private function analyzeTokens(array $tokens, string $filename): array
41
    {
42 19
        $lastDocBlock = null;
43 19
        $missingDocBlocks = [];
44 19
        $tokenCount = count($tokens);
45
46 19
        for ($i = 0; $i < $tokenCount; $i++) {
47 19
            $token = $tokens[$i];
48
49 19
            if (! is_array($token)) {
50 19
                continue;
51
            }
52
53 19
            if ($token[0] === T_DOC_COMMENT) {
54 8
                $lastDocBlock = $token[1];
55 19
            } elseif ($this->isDocBlockRequired($token, $tokens, $i)) {
56 13
                if (empty($lastDocBlock)) {
57 12
                    $missingDocBlocks[] = $this->createMissingDocBlockStat($token, $filename);
58
                }
59 13
                $lastDocBlock = null;
60
            }
61
        }
62
63 19
        return $missingDocBlocks;
64
    }
65
66 15
    private function isFollowingToken(array $tokens, int $index, string $expectedToken): bool
67
    {
68 15
        for ($j = $index + 1, $count = count($tokens); $j < $count; $j++) {
69 15
            $nextToken = $tokens[$j];
70 15
            if ($nextToken[0] === T_WHITESPACE) {
71 13
                continue;
72
            }
73 15
            return $nextToken === $expectedToken;
74
        }
75
        return false;
76
    }
77
78 12
    private function isAnonymousClass(array $tokens, int $index): bool
79
    {
80 12
        return $this->isFollowingToken($tokens, $index, '{');
81
    }
82
83 6
    private function isAnonymousFunction(array $tokens, int $index): bool
84
    {
85 6
        return $this->isFollowingToken($tokens, $index, '(');
86
    }
87
88 19
    private function isDocBlockRequired(array $token, array $tokens, int $index): bool
89
    {
90 19
        if (! in_array($token[0], [T_CLASS, T_TRAIT, T_INTERFACE, T_ENUM, T_FUNCTION, T_CONST, T_VARIABLE], true)) {
91 19
            return false;
92
        }
93 19
        if ($token[0] === T_CLASS) {
94 12
            if ($this->isAnonymousClass($tokens, $index) || $this->isClosure($tokens, $index)) {
95 3
                return false;
96
            }
97
        }
98
99 19
        if ($token[0] === T_FUNCTION) {
100 6
            if ($this->isAnonymousFunction($tokens, $index) || ! $this->isFunctionDeclaration($tokens, $index)) {
101 2
                return false;
102
            }
103
        }
104
105 18
        if ($token[0] === T_CONST || $token[0] === T_VARIABLE) {
106 12
            if (! $this->isPropertyOrConstant($tokens, $index)) {
107 7
                return false;
108
            }
109
        }
110
111 13
        return true;
112
    }
113
114 12
    private function createMissingDocBlockStat(array $token, string $filename): array
115
    {
116 12
        return [
117 12
            'type' => 'missingDocblock',
118 12
            'content' => '',
119 12
            'file' => $filename,
120 12
            'line' => $token[2]
121 12
        ];
122
    }
123
124 12
    private function isPropertyOrConstant(array $tokens, int $index): bool
125
    {
126 12
        return (
127 12
            $tokens[$index - 1][0] === T_WHITESPACE
128 12
            && (
129 12
                $tokens[$index - 2][0] === T_STRING
130 12
                || $tokens[$index - 2][0] === T_ARRAY
131 12
                || $tokens[$index - 2][0] === T_STATIC
132 12
                || $tokens[$index - 2][0] === T_READONLY
133 12
                || $tokens[$index - 2][0] === T_PUBLIC
134 12
                || $tokens[$index - 2][0] === T_PROTECTED
135 12
                || $tokens[$index - 2][0] === T_PRIVATE
136 12
                || $tokens[$index - 2][0] === T_FINAL
137 12
            )
138 12
            && (
139 12
                $tokens[$index - 3][0] === T_WHITESPACE
140 12
            )
141 12
            && ($tokens[$index - 4] !== '(' && $tokens[$index - 4] !== ',')
142 12
        );
143
    }
144
145 11
    protected function isClosure(array $tokens, int $index): bool
146
    {
147 11
        $tokenCount = count($tokens);
148 11
        for ($j = $index + 1; $j < $tokenCount; $j++) {
149 11
            $nextToken = $tokens[$j];
150 11
            if ($nextToken[0] === T_WHITESPACE) {
151 9
                continue;
152
            }
153
            if (
154 11
                $tokens[$index - 2][0] === T_STRING
155 11
                && $tokens[$index - 1][0] === T_PAAMAYIM_NEKUDOTAYIM
156 11
                && $tokens[$index][0] === T_CLASS
157
            ) {
158 2
                return true;
159
            }
160
        }
161 9
        return false;
162
    }
163
164 5
    private function isFunctionDeclaration(array $tokens, int $index): bool
165
    {
166 5
        $tokenCount = count($tokens);
167 5
        for ($j = $index + 1; $j < $tokenCount; $j++) {
168 5
            $nextToken = $tokens[$j];
169 5
            if (is_array($nextToken) && $nextToken[0] === T_STRING) {
170 5
                continue;
171
            }
172 5
            if ($nextToken === '(') {
173 4
                return true;
174
            }
175 5
            if ($nextToken === ';') {
176 1
                return false;
177
            }
178
        }
179
        return false;
180
    }
181
182 19
    public function getMissingDocblocks(array $tokens, string $filename): array
183
    {
184 19
        return $this->analyzeTokens($tokens, $filename);
185
    }
186
187
    public function getColor(): string
188
    {
189
        return 'red';
190
    }
191
192
    public function getStatColor(float $count, array $thresholds): string
193
    {
194
        if (! isset($thresholds['missingDocBlock'])) {
195
            return 'white';
196
        }
197
        if ($count <= $thresholds['missingDocBlock']) {
198
            return 'green';
199
        }
200
        $this->exceedThreshold = true;
201
        return 'red';
202
    }
203
204
    public function hasExceededThreshold(): bool
205
    {
206
        return $this->exceedThreshold;
207
    }
208
209
    public function getName(): string
210
    {
211
        return 'missingDocblock';
212
    }
213
}
214