Passed
Push — main ( 09e2a2...6a58fa )
by mikhail
13:23
created

Analyzer::shouldAnalyzeMissingDocBlocks()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
c 1
b 0
f 0
dl 0
loc 5
ccs 4
cts 4
cp 1
rs 10
cc 2
nc 2
nop 0
crap 2
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 checkThresholdsExceeded(): bool
163
    {
164
        if ($this->metrics->hasExceededThreshold()) {
165
            return true;
166
        }
167
        if ($this->missingDocBlock->hasExceededThreshold()) {
168
            return true;
169
        }
170
        foreach ($this->commentFactory->getCommentTypes() as $commentType) {
171
            if ($commentType->hasExceededThreshold()) {
172
                return true;
173
            }
174
        }
175
        return false;
176
    }
177
178
    private function createOutputDTO(
179
        array $comments,
180
        array $commentStatistics,
181
        int $totalLinesOfCode,
182
        int $filesAnalyzed
183
    ): OutputDTO {
184
        $preparedStatistics = $this->prepareCommentStatistics($commentStatistics);
185
        $preparedComments = $this->prepareComments($comments);
186
        $comToLoc = $this->metrics->prepareComToLoc($commentStatistics, $totalLinesOfCode);
187
        $cds = $this->metrics->prepareCDS($this->metrics->calculateCDS($commentStatistics));
188
        $this->exceedThreshold = $this->checkThresholdsExceeded();
189
        $this->metrics->stopPerformanceMonitoring();
190
        $performanceMetrics = $this->metrics->getPerformanceMetrics();
191
192
        return new OutputDTO(
193
            $filesAnalyzed,
194
            $preparedStatistics,
195
            $preparedComments,
196
            $performanceMetrics,
197
            $comToLoc,
198
            $cds,
199
            $this->exceedThreshold
200
        );
201
    }
202
203
    private function prepareCommentStatistics(array $commentStatistics): array
204
    {
205
        $preparedStatistics = [];
206
        foreach ($commentStatistics as $type => $stat) {
207
            $preparedStatistics[] = $this->prepareCommentStatistic($type, $stat);
208
        }
209
        return $preparedStatistics;
210
    }
211
212
    private function prepareCommentStatistic(string $type, array $stat): CommentStatisticsDTO
213
    {
214
        if ($type === $this->missingDocBlock->getName()) {
215
            return new CommentStatisticsDTO(
216
                $this->missingDocBlock->getColor(),
217
                $this->missingDocBlock->getName(),
218
                $stat['lines'],
219
                $this->missingDocBlock->getStatColor($stat['count'], $this->configDTO->thresholds),
220
                $stat['count']
221
            );
222
        }
223
224
        $commentType = $this->commentFactory->getCommentType($type);
225
        if ($commentType) {
226
            return new CommentStatisticsDTO(
227
                $commentType->getColor(),
228
                $commentType->getName(),
229
                $stat['lines'],
230
                $commentType->getStatColor($stat['count'], $this->configDTO->thresholds),
231
                $stat['count']
232
            );
233
        }
234
235
        return new CommentStatisticsDTO('', $type, $stat['lines'], '', $stat['count']);
236
    }
237
238
    private function prepareComments(array $comments): array
239
    {
240
        $preparedComments = [];
241
        foreach ($comments as $comment) {
242
            /** @var CommentTypeInterface|string $commentType */
243
            $commentType = $comment['type'];
244
            if ($commentType === $this->missingDocBlock->getName()) {
245
                $preparedComments[] = new CommentDTO(
246
                    $this->missingDocBlock->getName(),
247
                    $this->missingDocBlock->getColor(),
248
                    $comment['file'],
249
                    $comment['line'],
250
                    $comment['content']
251
                );
252
            } elseif ($commentType->getWeight() <= 0) {
253
                $preparedComments[] = new CommentDTO(
254
                    $commentType->getName(),
255
                    $commentType->getColor(),
256
                    $comment['file'],
257
                    $comment['line'],
258
                    $comment['content']
259
                );
260
            }
261
        }
262
        return $preparedComments;
263
    }
264
265 1
    private function isInWhitelist(string $filePath): bool
266
    {
267 1
        foreach ($this->configDTO->exclude as $whitelistedDir) {
268 1
            if (str_contains($filePath, $whitelistedDir)) {
269 1
                return true;
270
            }
271
        }
272 1
        return false;
273
    }
274
}
275