Test Failed
Push — main ( 62eb89...28fe2f )
by mikhail
03:32
created

MissingDocBlockAnalyzer::isFollowingToken()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

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