Passed
Push — main ( ec2ade...f9de31 )
by mikhail
03:35
created

Analyzer::shouldSkipFile()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 5
c 1
b 0
f 0
dl 0
loc 7
ccs 0
cts 6
cp 0
rs 10
cc 4
nc 4
nop 1
crap 20
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
    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
    }
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
    private function shouldSkipFile(SplFileInfo $file): bool
78
    {
79
        return
80
            $this->isInWhitelist($file->getRealPath()) ||
81
            $file->getSize() === 0 ||
82
            !$this->isPhpFile($file) ||
83
            !$file->isReadable();
84
    }
85
86
    private function analyzeFile(string $filename): array
87
    {
88
        $this->output->writeln("<info>Analyzing $filename</info>");
89
90
        $code = file_get_contents($filename);
91
        $tokens = token_get_all($code);
92
93
        $comments = $this->getCommentsFromFile($tokens, $filename);
94
        if ($this->shouldAnalyzeMissingDocBlocks()) {
95
            $missingDocBlocks = $this->docBlockAnalyzer->getMissingDocblocks($code, $filename);
96
            $comments = array_merge($missingDocBlocks, $comments);
97
        }
98
99
        $linesOfCode = $this->countTotalLines($filename);
100
101
        return [
102
            'comments' => $comments,
103
            'linesOfCode' => $linesOfCode,
104
        ];
105
    }
106
107
    private function shouldAnalyzeMissingDocBlocks(): bool
108
    {
109
        return
110
            empty($this->configDTO->only)
111
            || in_array($this->missingDocBlock->getName(), $this->configDTO->only, true);
112
    }
113
114
    private function getCommentsFromFile(array $tokens, string $filename): array
115
    {
116
        $comments = [];
117
        foreach ($tokens as $token) {
118
            if (!is_array($token) || !in_array($token[0], [T_COMMENT, T_DOC_COMMENT])) {
119
                continue;
120
            }
121
            $commentType = $this->commentFactory->classifyComment($token[1]);
122
            if ($commentType) {
123
                $comments[] = [
124
                    'content' => $token[1],
125
                    'type' => $commentType,
126
                    'line' => $token[2],
127
                    'file' => $filename
128
                ];
129
            }
130
        }
131
        return $comments;
132
    }
133
134
    private function countTotalLines(string $filename): int
135
    {
136
        $fileContent = file($filename);
137
        return count($fileContent);
138
    }
139
140
    private function isPhpFile(SplFileInfo $file): bool
141
    {
142
        return $file->isFile() && $file->getExtension() === 'php';
143
    }
144
145
    private function countCommentOccurrences(array $comments): array
146
    {
147
        $lineCounts = [];
148
        foreach ($comments as $comment) {
149
            $typeName = (string)$comment['type'];
150
            if (!isset($lineCounts[$typeName])) {
151
                $lineCounts[$typeName] = [
152
                    'lines' => 0,
153
                    'count' => 0,
154
                ];
155
            }
156
            $lineCounts[$typeName]['lines'] += substr_count($comment['content'], PHP_EOL) + 1;
157
            $lineCounts[$typeName]['count']++;
158
        }
159
        return $lineCounts;
160
    }
161
162
    private function createOutputDTO(
163
        array $comments,
164
        array $commentStatistics,
165
        int $totalLinesOfCode,
166
        int $filesAnalyzed
167
    ): OutputDTO {
168
        $preparedStatistics = $this->prepareCommentStatistics($commentStatistics);
169
        $preparedComments = $this->prepareComments($comments);
170
        $comToLoc = $this->metrics->prepareComToLoc($commentStatistics, $totalLinesOfCode);
171
        $cds = $this->metrics->prepareCDS($this->metrics->calculateCDS($commentStatistics));
172
        if ($this->metrics->hasExceededThreshold()) {
173
            $this->exceedThreshold = true;
174
        }
175
        $this->metrics->stopPerformanceMonitoring();
176
        $performanceMetrics = $this->metrics->getPerformanceMetrics();
177
178
        return new OutputDTO(
179
            $filesAnalyzed,
180
            $preparedStatistics,
181
            $preparedComments,
182
            $performanceMetrics,
183
            $comToLoc,
184
            $cds,
185
            $this->exceedThreshold
186
        );
187
    }
188
189
    private function prepareCommentStatistics(array $commentStatistics): array
190
    {
191
        $preparedStatistics = [];
192
        foreach ($commentStatistics as $type => $stat) {
193
            $preparedStatistics[] = $this->prepareCommentStatistic($type, $stat);
194
        }
195
        return $preparedStatistics;
196
    }
197
198
    private function prepareCommentStatistic(string $type, array $stat): CommentStatisticsDTO
199
    {
200
        if ($type === $this->missingDocBlock->getName()) {
201
            return new CommentStatisticsDTO(
202
                $this->missingDocBlock->getColor(),
203
                $this->missingDocBlock->getName(),
204
                $stat['lines'],
205
                $this->missingDocBlock->getStatColor($stat['count'], $this->configDTO->thresholds),
206
                $stat['count']
207
            );
208
        }
209
210
        $commentType = $this->commentFactory->getCommentType($type);
211
        if ($commentType) {
212
            return new CommentStatisticsDTO(
213
                $commentType->getColor(),
214
                $commentType->getName(),
215
                $stat['lines'],
216
                $commentType->getStatColor($stat['count'], $this->configDTO->thresholds),
217
                $stat['count']
218
            );
219
        }
220
221
        return new CommentStatisticsDTO('', $type, $stat['lines'], '', $stat['count']);
222
    }
223
224
    private function prepareComments(array $comments): array
225
    {
226
        $preparedComments = [];
227
        foreach ($comments as $comment) {
228
            /** @var CommentTypeInterface|string $commentType */
229
            $commentType = $comment['type'];
230
            if ($commentType === $this->missingDocBlock->getName()) {
231
                $preparedComments[] = new CommentDTO(
232
                    $this->missingDocBlock->getName(),
233
                    $this->missingDocBlock->getColor(),
234
                    $comment['file'],
235
                    $comment['line'],
236
                    $comment['content']
237
                );
238
            } elseif ($commentType->getWeight() <= 0) {
239
                $preparedComments[] = new CommentDTO(
240
                    $commentType->getName(),
241
                    $commentType->getColor(),
242
                    $comment['file'],
243
                    $comment['line'],
244
                    $comment['content']
245
                );
246
            }
247
        }
248
        return $preparedComments;
249
    }
250
251
    private function isInWhitelist(string $filePath): bool
252
    {
253
        foreach ($this->configDTO->exclude as $whitelistedDir) {
254
            if (str_contains($filePath, $whitelistedDir)) {
255
                return true;
256
            }
257
        }
258
        return false;
259
    }
260
}
261