Passed
Push — main ( 2d42b4...56ead9 )
by mikhail
05:21 queued 01:44
created

Analyzer::analyze()   A

Complexity

Conditions 6
Paths 12

Size

Total Lines 33
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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