Passed
Push — main ( fefea7...05b26b )
by mikhail
08:12
created

Analyzer::countTotalLines()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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