Passed
Push — main ( 35d689...3d4288 )
by mikhail
03:20
created

Analyzer::analyzeFile()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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