Passed
Push — main ( 5c2914...7703d5 )
by mikhail
03:07
created

Analyzer::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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