Passed
Push — main ( e2cb9c...09e2a2 )
by mikhail
03:47
created

Analyzer::checkThresholdExceeded()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 7
c 1
b 1
f 0
dl 0
loc 11
ccs 0
cts 8
cp 0
rs 9.6111
cc 5
nc 12
nop 0
crap 30
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SavinMikhail\CommentsDensity\Analyzer;
6
7
use Generator;
8
use SavinMikhail\CommentsDensity\Baseline\Storage\BaselineStorageInterface;
9
use SavinMikhail\CommentsDensity\Comments\CommentFactory;
10
use SavinMikhail\CommentsDensity\Comments\CommentTypeInterface;
11
use SavinMikhail\CommentsDensity\DTO\Input\ConfigDTO;
0 ignored issues
show
Bug introduced by
The type SavinMikhail\CommentsDensity\DTO\Input\ConfigDTO was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
12
use SavinMikhail\CommentsDensity\DTO\Output\CommentDTO;
0 ignored issues
show
Bug introduced by
The type SavinMikhail\CommentsDensity\DTO\Output\CommentDTO was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
13
use SavinMikhail\CommentsDensity\DTO\Output\CommentStatisticsDTO;
0 ignored issues
show
Bug introduced by
The type SavinMikhail\CommentsDen...ut\CommentStatisticsDTO was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
use SavinMikhail\CommentsDensity\DTO\Output\OutputDTO;
0 ignored issues
show
Bug introduced by
The type SavinMikhail\CommentsDensity\DTO\Output\OutputDTO was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
use SavinMikhail\CommentsDensity\Metrics\MetricsFacade;
0 ignored issues
show
Bug introduced by
The type SavinMikhail\CommentsDensity\Metrics\MetricsFacade was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
16
use SavinMikhail\CommentsDensity\MissingDocblock\MissingDocBlockAnalyzer;
17
use SplFileInfo;
18
use Symfony\Component\Console\Output\OutputInterface;
19
20
use function array_merge;
21
use function array_push;
22
use function file;
23
use function file_get_contents;
24
use function in_array;
25
use function is_array;
26
use function str_contains;
27
use function substr_count;
28
use function token_get_all;
29
30
use const PHP_EOL;
31
use const T_COMMENT;
32
use const T_DOC_COMMENT;
33
34
final class Analyzer
35
{
36
    private bool $exceedThreshold = false;
37
38 6
    public function __construct(
39
        private readonly ConfigDTO $configDTO,
40
        private readonly CommentFactory $commentFactory,
41
        private readonly MissingDocBlockAnalyzer $missingDocBlock,
42
        private readonly MetricsFacade $metrics,
43
        private readonly OutputInterface $output,
44
        private readonly MissingDocBlockAnalyzer $docBlockAnalyzer,
45
        private readonly BaselineStorageInterface $baselineStorage,
46
    ) {
47 6
    }
48
49
    public function analyze(Generator $files): OutputDTO
50
    {
51
        $this->metrics->startPerformanceMonitoring();
52
        $comments = [];
53
        $totalLinesOfCode = 0;
54
        $filesAnalyzed = 0;
55
56
        foreach ($files as $file) {
57
            if ($this->shouldSkipFile($file)) {
58
                continue;
59
            }
60
61
            $commentsAndLines = $this->analyzeFile($file->getRealPath());
62
            $totalLinesOfCode += $commentsAndLines['linesOfCode'];
63
            array_push($comments, ...$commentsAndLines['comments']);
64
65
            $filesAnalyzed++;
66
        }
67
68
        if ($this->configDTO->useBaseline) {
69
            $comments = $this->baselineStorage->filterComments($comments);
70
        }
71
72
        $commentStatistics = $this->countCommentOccurrences($comments);
73
74
        return $this->createOutputDTO($comments, $commentStatistics, $totalLinesOfCode, $filesAnalyzed);
75
    }
76
77 1
    private function shouldSkipFile(SplFileInfo $file): bool
78
    {
79 1
        return
80 1
            $this->isInWhitelist($file->getRealPath()) ||
81 1
            $file->getSize() === 0 ||
82 1
            !$this->isPhpFile($file) ||
83 1
            !$file->isReadable();
84
    }
85
86 1
    private function analyzeFile(string $filename): array
87
    {
88 1
        $this->output->writeln("<info>Analyzing $filename</info>");
89
90 1
        $code = file_get_contents($filename);
91 1
        $tokens = token_get_all($code);
92
93 1
        $comments = $this->getCommentsFromFile($tokens, $filename);
94 1
        if ($this->shouldAnalyzeMissingDocBlocks()) {
95 1
            $missingDocBlocks = $this->docBlockAnalyzer->getMissingDocblocks($code, $filename);
96 1
            $comments = array_merge($missingDocBlocks, $comments);
97
        }
98
99 1
        $linesOfCode = $this->countTotalLines($filename);
100
101 1
        return [
102 1
            'comments' => $comments,
103 1
            'linesOfCode' => $linesOfCode,
104 1
        ];
105
    }
106
107 1
    private function shouldAnalyzeMissingDocBlocks(): bool
108
    {
109 1
        return
110 1
            empty($this->configDTO->only)
111 1
            || in_array($this->missingDocBlock->getName(), $this->configDTO->only, true);
112
    }
113
114 2
    private function getCommentsFromFile(array $tokens, string $filename): array
115
    {
116 2
        $comments = [];
117 2
        foreach ($tokens as $token) {
118 2
            if (!is_array($token) || !in_array($token[0], [T_COMMENT, T_DOC_COMMENT])) {
119 1
                continue;
120
            }
121 2
            $commentType = $this->commentFactory->classifyComment($token[1]);
122 2
            if ($commentType) {
123 1
                $comments[] = [
124 1
                    'content' => $token[1],
125 1
                    'type' => $commentType,
126 1
                    'line' => $token[2],
127 1
                    'file' => $filename
128 1
                ];
129
            }
130
        }
131 2
        return $comments;
132
    }
133
134 2
    private function countTotalLines(string $filename): int
135
    {
136 2
        $fileContent = file($filename);
137 2
        return count($fileContent);
138
    }
139
140 2
    private function isPhpFile(SplFileInfo $file): bool
141
    {
142 2
        return $file->isFile() && $file->getExtension() === 'php';
143
    }
144
145 1
    private function countCommentOccurrences(array $comments): array
146
    {
147 1
        $lineCounts = [];
148 1
        foreach ($comments as $comment) {
149 1
            $typeName = (string)$comment['type'];
150 1
            if (!isset($lineCounts[$typeName])) {
151 1
                $lineCounts[$typeName] = [
152 1
                    'lines' => 0,
153 1
                    'count' => 0,
154 1
                ];
155
            }
156 1
            $lineCounts[$typeName]['lines'] += substr_count($comment['content'], PHP_EOL) + 1;
157 1
            $lineCounts[$typeName]['count']++;
158
        }
159 1
        return $lineCounts;
160
    }
161
162
    private function checkThresholdExceeded(): void
163
    {
164
        if ($this->metrics->hasExceededThreshold()) {
165
            $this->exceedThreshold = true;
166
        }
167
        if ($this->missingDocBlock->hasExceededThreshold()) {
168
            $this->exceedThreshold = true;
169
        }
170
        foreach ($this->commentFactory->getCommentTypes() as $commentType) {
171
            if ($commentType->hasExceededThreshold()) {
172
                $this->exceedThreshold = true;
173
            }
174
        }
175
    }
176
177
    private function createOutputDTO(
178
        array $comments,
179
        array $commentStatistics,
180
        int $totalLinesOfCode,
181
        int $filesAnalyzed
182
    ): OutputDTO {
183
        $preparedStatistics = $this->prepareCommentStatistics($commentStatistics);
184
        $preparedComments = $this->prepareComments($comments);
185
        $comToLoc = $this->metrics->prepareComToLoc($commentStatistics, $totalLinesOfCode);
186
        $cds = $this->metrics->prepareCDS($this->metrics->calculateCDS($commentStatistics));
187
        $this->checkThresholdExceeded();
188
        $this->metrics->stopPerformanceMonitoring();
189
        $performanceMetrics = $this->metrics->getPerformanceMetrics();
190
191
        return new OutputDTO(
192
            $filesAnalyzed,
193
            $preparedStatistics,
194
            $preparedComments,
195
            $performanceMetrics,
196
            $comToLoc,
197
            $cds,
198
            $this->exceedThreshold
199
        );
200
    }
201
202
    private function prepareCommentStatistics(array $commentStatistics): array
203
    {
204
        $preparedStatistics = [];
205
        foreach ($commentStatistics as $type => $stat) {
206
            $preparedStatistics[] = $this->prepareCommentStatistic($type, $stat);
207
        }
208
        return $preparedStatistics;
209
    }
210
211
    private function prepareCommentStatistic(string $type, array $stat): CommentStatisticsDTO
212
    {
213
        if ($type === $this->missingDocBlock->getName()) {
214
            return new CommentStatisticsDTO(
215
                $this->missingDocBlock->getColor(),
216
                $this->missingDocBlock->getName(),
217
                $stat['lines'],
218
                $this->missingDocBlock->getStatColor($stat['count'], $this->configDTO->thresholds),
219
                $stat['count']
220
            );
221
        }
222
223
        $commentType = $this->commentFactory->getCommentType($type);
224
        if ($commentType) {
225
            return new CommentStatisticsDTO(
226
                $commentType->getColor(),
227
                $commentType->getName(),
228
                $stat['lines'],
229
                $commentType->getStatColor($stat['count'], $this->configDTO->thresholds),
230
                $stat['count']
231
            );
232
        }
233
234
        return new CommentStatisticsDTO('', $type, $stat['lines'], '', $stat['count']);
235
    }
236
237
    private function prepareComments(array $comments): array
238
    {
239
        $preparedComments = [];
240
        foreach ($comments as $comment) {
241
            /** @var CommentTypeInterface|string $commentType */
242
            $commentType = $comment['type'];
243
            if ($commentType === $this->missingDocBlock->getName()) {
244
                $preparedComments[] = new CommentDTO(
245
                    $this->missingDocBlock->getName(),
246
                    $this->missingDocBlock->getColor(),
247
                    $comment['file'],
248
                    $comment['line'],
249
                    $comment['content']
250
                );
251
            } elseif ($commentType->getWeight() <= 0) {
252
                $preparedComments[] = new CommentDTO(
253
                    $commentType->getName(),
254
                    $commentType->getColor(),
255
                    $comment['file'],
256
                    $comment['line'],
257
                    $comment['content']
258
                );
259
            }
260
        }
261
        return $preparedComments;
262
    }
263
264 1
    private function isInWhitelist(string $filePath): bool
265
    {
266 1
        foreach ($this->configDTO->exclude as $whitelistedDir) {
267 1
            if (str_contains($filePath, $whitelistedDir)) {
268 1
                return true;
269
            }
270
        }
271 1
        return false;
272
    }
273
}
274