Passed
Push — main ( b8dd52...35d689 )
by mikhail
03:26
created

CommentDensity::analyzeFile()   A

Complexity

Conditions 3
Paths 2

Size

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