Skip to content

Commit ab84eb2

Browse files
Updating to PHPStan Level 10 (#73)
* Update PHPStan level and refactor application and report handling - Increased PHPStan analysis level from 8 to 9 for stricter type checking and improved code quality. - Refactored `Application` class to use dedicated methods for resolving input and output interfaces, enhancing clarity and error handling. - Improved `ChurnMetrics` and related classes by adding type checks and default value handling for various metrics, ensuring robustness. - Enhanced report generation by implementing consistent value resolution methods across multiple classes, improving data integrity and reducing redundancy. - Updated command classes to format statistic values more reliably, ensuring consistent output across different data types. * Update PHPStan level and enhance cognitive metrics handling - Increased PHPStan analysis level from 9 to 10 for stricter type checking and improved code quality. - Refactored `ChurnMetricsCollection` to ensure class names are cast to strings when converting metrics to arrays. - Updated `CognitiveMetricsCollection` to return values as an indexed array for consistency. - Improved `CognitiveMetricsCollector` by normalizing ignored items and adding a new method for better handling of ignored items. - Enhanced `Parser` class with better type handling for metrics and added methods to merge cyclomatic and Halstead metrics. - Introduced `CustomExporterConfigValidator` to streamline validation of custom exporter configurations. - Refactored `CompositeChurnSpecification` and `CustomExporter` to utilize the new validator for improved error handling. - Added `ConfigException` for better error management in configuration handling. * Refactor cognitive metrics handling for improved type validation - Updated `CognitiveMetrics` to return early if `cyclomatic_complexity` is not an array, enhancing robustness. - Modified `ScoreCalculator` to skip non-numeric weights, ensuring only valid scores are accumulated. - Cleaned up unnecessary whitespace in `ConfigFactory` for better code clarity. * Fix empty return value in CacheConfig class - Added an empty string to the return array in the `CacheConfig` class to ensure consistent return structure. - This change improves clarity and maintains the expected format for configuration data. * Remove empty string from return array in CacheConfig class to ensure consistent return structure.
1 parent 36a2acd commit ab84eb2

49 files changed

Lines changed: 1071 additions & 348 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

phpstan.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
parameters:
2-
level: 8
2+
level: 10
33
paths:
44
- src
55
parallel:

src/Application.php

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -258,12 +258,12 @@ private function bootstrapMetricsCollectors(): void
258258
private function configureEventBus(): void
259259
{
260260
$progressbar = new ProgressBarHandler(
261-
$this->get(OutputInterface::class)
261+
$this->resolveOutputInterface()
262262
);
263263

264264
$verbose = new VerboseHandler(
265-
$this->get(InputInterface::class),
266-
$this->get(OutputInterface::class)
265+
$this->resolveInputInterface(),
266+
$this->resolveOutputInterface()
267267
);
268268

269269
$handlersLocator = $this->setUpEventHandlersLocator($progressbar, $verbose);
@@ -502,8 +502,28 @@ private function setUpEventHandlersLocator(
502502
$verbose
503503
],
504504
ParserFailed::class => [
505-
new ParserErrorHandler($this->get(OutputInterface::class))
505+
new ParserErrorHandler($this->resolveOutputInterface())
506506
],
507507
]);
508508
}
509+
510+
private function resolveInputInterface(): InputInterface
511+
{
512+
$input = $this->get(InputInterface::class);
513+
if (!$input instanceof InputInterface) {
514+
throw new CognitiveAnalysisException('Console input is not configured.');
515+
}
516+
517+
return $input;
518+
}
519+
520+
private function resolveOutputInterface(): OutputInterface
521+
{
522+
$output = $this->get(OutputInterface::class);
523+
if (!$output instanceof OutputInterface) {
524+
throw new CognitiveAnalysisException('Console output is not configured.');
525+
}
526+
527+
return $output;
528+
}
509529
}

src/Business/Churn/ChurnMetrics.php

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,18 +49,35 @@ public function __construct(
4949
*/
5050
public static function fromArray(string $className, array $data): self
5151
{
52+
$file = $data['file'] ?? '';
53+
$riskLevel = $data['riskLevel'] ?? null;
54+
5255
return new self(
5356
className: $className,
54-
file: $data['file'] ?? '',
55-
score: (float)($data['score'] ?? 0),
56-
timesChanged: (int)($data['timesChanged'] ?? 0),
57-
churn: (float)($data['churn'] ?? 0),
58-
coverage: isset($data['coverage']) ? (float)$data['coverage'] : null,
59-
riskChurn: isset($data['riskChurn']) ? (float)$data['riskChurn'] : null,
60-
riskLevel: $data['riskLevel'] ?? null
57+
file: is_string($file) ? $file : '',
58+
score: self::resolveFloatValue($data['score'] ?? null, 0.0),
59+
timesChanged: self::resolveIntValue($data['timesChanged'] ?? null, 0),
60+
churn: self::resolveFloatValue($data['churn'] ?? null, 0.0),
61+
coverage: isset($data['coverage']) ? self::resolveFloatValue($data['coverage'], 0.0) : null,
62+
riskChurn: isset($data['riskChurn']) ? self::resolveFloatValue($data['riskChurn'], 0.0) : null,
63+
riskLevel: is_string($riskLevel) ? $riskLevel : null
6164
);
6265
}
6366

67+
private static function resolveIntValue(mixed $value, int $default): int
68+
{
69+
return is_int($value) ? $value : $default;
70+
}
71+
72+
private static function resolveFloatValue(mixed $value, float $default): float
73+
{
74+
if (is_int($value) || is_float($value)) {
75+
return (float) $value;
76+
}
77+
78+
return $default;
79+
}
80+
6481
/**
6582
* Convert to array format (for backward compatibility).
6683
*

src/Business/Churn/ChurnMetricsCollection.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ public function toArray(): array
173173
{
174174
$result = [];
175175
foreach ($this->metrics as $className => $metric) {
176-
$result[$className] = $metric->toArray();
176+
$result[(string) $className] = $metric->toArray();
177177
}
178178
return $result;
179179
}

src/Business/Churn/Report/ChurnReportFactory.php

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,14 @@ public function create(string $type): ReportGeneratorInterface
4747
return $builtIn;
4848
}
4949

50-
// Check custom exporters
5150
if (isset($customReporters[$type])) {
52-
return $this->createCustomExporter($customReporters[$type]);
51+
$exporterConfig = $customReporters[$type];
52+
if (!is_array($exporterConfig)) {
53+
throw new InvalidArgumentException("Invalid custom exporter configuration for type: {$type}");
54+
}
55+
56+
/** @var array<string, mixed> $exporterConfig */
57+
return $this->createCustomExporter($exporterConfig);
5358
}
5459

5560
throw new InvalidArgumentException("Unsupported exporter type: {$type}");
@@ -65,9 +70,19 @@ private function createCustomExporter(array $config): ReportGeneratorInterface
6570
{
6671
$cognitiveConfig = $this->configService->getConfig();
6772

68-
$this->registry->loadExporter($config['class'], $config['file'] ?? null);
73+
$class = $config['class'] ?? null;
74+
if (!is_string($class)) {
75+
throw new InvalidArgumentException('Custom exporter must define a "class" string.');
76+
}
77+
78+
$file = $config['file'] ?? null;
79+
if ($file !== null && !is_string($file)) {
80+
throw new InvalidArgumentException('Custom exporter "file" must be a string or null.');
81+
}
82+
83+
$this->registry->loadExporter($class, $file);
6984
$exporter = $this->registry->instantiate(
70-
$config['class'],
85+
$class,
7186
$cognitiveConfig
7287
);
7388
$this->registry->validateInterface($exporter, ReportGeneratorInterface::class);

src/Business/Churn/Report/SvgTreemapReport.php

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,12 @@ private function renderSvgRects(array $rects, float $minScore, float $maxScore):
8282
{
8383
$svgRects = [];
8484
foreach ($rects as $rect) {
85-
$normalizedScore = $this->treemapMath->normalizeScore(score: $rect['score'], minScore: $minScore, maxScore: $maxScore);
85+
$score = $this->resolveFloatValue($rect['score'] ?? null, 0.0);
86+
$normalizedScore = $this->treemapMath->normalizeScore(
87+
score: $score,
88+
minScore: $minScore,
89+
maxScore: $maxScore
90+
);
8691
$svgRects[] = $this->renderSvgRect(rect: $rect, normalizedScore: $normalizedScore);
8792
}
8893

@@ -98,17 +103,18 @@ private function renderSvgRects(array $rects, float $minScore, float $maxScore):
98103
*/
99104
private function renderSvgRect(array $rect, float $normalizedScore): string
100105
{
101-
$x = $rect['x'] + self::PADDING;
102-
$y = $rect['y'] + self::PADDING;
103-
$width = max(0, $rect['width'] - self::PADDING * 2);
104-
$height = max(0, $rect['height'] - self::PADDING * 2);
106+
$x = $this->resolveFloatValue($rect['x'] ?? null, 0.0) + self::PADDING;
107+
$y = $this->resolveFloatValue($rect['y'] ?? null, 0.0) + self::PADDING;
108+
$width = max(0, $this->resolveFloatValue($rect['width'] ?? null, 0.0) - self::PADDING * 2);
109+
$height = max(0, $this->resolveFloatValue($rect['height'] ?? null, 0.0) - self::PADDING * 2);
105110
$color = $this->treemapMath->scoreToColor(score: $normalizedScore);
106-
$class = htmlspecialchars($rect['class']);
107-
$churn = $rect['churn'];
108-
$score = $rect['score'];
111+
$className = is_string($rect['class'] ?? null) ? $rect['class'] : '';
112+
$class = htmlspecialchars($className);
113+
$churn = $this->resolveFloatValue($rect['churn'] ?? null, 0.0);
114+
$score = $this->resolveFloatValue($rect['score'] ?? null, 0.0);
109115
$textX = $x + 4;
110116
$textY = $y + 18;
111-
$label = htmlspecialchars(mb_strimwidth($rect['class'], 0, 40, ''));
117+
$label = htmlspecialchars(mb_strimwidth($className, 0, 40, ''));
112118

113119
return sprintf(
114120
'<g><rect x="%.2f" y="%.2f" width="%.2f" height="%.2f" fill="%s" stroke="#222" stroke-width="1"/><title>%s&#10;Churn: %s&#10;Score: %s</title><text x="%.2f" y="%.2f" font-size="13" fill="#000">%s</text></g>',
@@ -153,4 +159,13 @@ private function wrapSvg(string $rectsSvg): string
153159
</svg>
154160
SVG;
155161
}
162+
163+
private function resolveFloatValue(mixed $value, float $default): float
164+
{
165+
if (is_int($value) || is_float($value)) {
166+
return (float) $value;
167+
}
168+
169+
return $default;
170+
}
156171
}

src/Business/Churn/Report/TreemapMath.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ public function prepareItems(array $classes): array
2929
{
3030
$items = [];
3131
foreach ($classes as $class => $data) {
32-
$churn = (float)($data['churn'] ?? 0);
33-
$score = (float)($data['score'] ?? 0);
32+
$churn = $this->resolveFloatValue($data['churn'] ?? null, 0.0);
33+
$score = $this->resolveFloatValue($data['score'] ?? null, 0.0);
3434
if ($churn <= 0) {
3535
continue;
3636
}
@@ -261,4 +261,13 @@ private function findSplitIndex(array $items, float $sum): int
261261

262262
return 1;
263263
}
264+
265+
private function resolveFloatValue(mixed $value, float $default): float
266+
{
267+
if (is_int($value) || is_float($value)) {
268+
return (float) $value;
269+
}
270+
271+
return $default;
272+
}
264273
}

src/Business/CodeCoverage/CloverReader.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,10 @@ private function extractMethodCoverageFromLines(mixed $allLines, int $methodLine
220220
$coveredStatements = 0;
221221
$inMethod = false;
222222

223+
if (!is_iterable($allLines)) {
224+
return ['statements' => 0, 'covered' => 0];
225+
}
226+
223227
foreach ($allLines as $line) {
224228
if (!$line instanceof DOMElement) {
225229
continue;

src/Business/Cognitive/Baseline/Baseline.php

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,17 @@ public function calculateDeltas(
2323
$warnings = [];
2424

2525
foreach ($baseline as $class => $data) {
26-
foreach ($data['methods'] as $methodName => $methodData) {
26+
$methods = $data['methods'] ?? null;
27+
if (!is_array($methods)) {
28+
continue;
29+
}
30+
31+
foreach ($methods as $methodName => $methodData) {
32+
if (!is_string($methodName) || !is_array($methodData)) {
33+
continue;
34+
}
35+
36+
/** @var array<string, mixed> $methodData */
2737
$metrics = $metricsCollection->getClassWithMethod($class, $methodName);
2838
if (!$metrics) {
2939
continue;
@@ -57,6 +67,11 @@ public function loadBaseline(string $baselineFile): array
5767
}
5868

5969
$data = json_decode($baseline, true, 512, JSON_THROW_ON_ERROR);
70+
if (!is_array($data)) {
71+
throw new CognitiveAnalysisException('Baseline file must contain a JSON object.');
72+
}
73+
74+
/** @var array<string, mixed> $data */
6075

6176
// Validate against JSON schema
6277
$validator = new BaselineSchemaValidator();
@@ -68,9 +83,11 @@ public function loadBaseline(string $baselineFile): array
6883
}
6984

7085
$result = BaselineFile::fromJson($data);
86+
/** @var array<string, array<string, mixed>> $metrics */
87+
$metrics = $result['metrics'];
7188

7289
return [
73-
'metrics' => $result['metrics'],
90+
'metrics' => $metrics,
7491
'baselineFile' => $result['baselineFile'],
7592
'warnings' => []
7693
];
@@ -182,10 +199,12 @@ public function isValidBaselineFile(string $filePath): bool
182199
}
183200

184201
$data = json_decode($content, true);
185-
if ($data === null) {
202+
if (!is_array($data)) {
186203
return false;
187204
}
188205

206+
/** @var array<string, mixed> $data */
207+
189208
// Use schema validator for comprehensive validation
190209
$validator = new BaselineSchemaValidator();
191210
return $validator->isValidBaseline($data);

src/Business/Cognitive/Baseline/BaselineFile.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Baseline;
66

7+
use InvalidArgumentException;
78
use JsonSerializable;
89
use Phauthentic\CognitiveCodeAnalysis\Config\CognitiveConfig;
910
use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsCollection;
@@ -51,19 +52,29 @@ public static function fromJson(array $data): array
5152
{
5253
// Check if this is the new format (has version field)
5354
if (isset($data['version']) && $data['version'] === self::VERSION) {
55+
$createdAt = $data['createdAt'] ?? null;
56+
$configHash = $data['configHash'] ?? null;
57+
$metrics = $data['metrics'] ?? null;
58+
59+
if (!is_string($createdAt) || !is_string($configHash) || !is_array($metrics)) {
60+
throw new InvalidArgumentException('Invalid baseline file metadata.');
61+
}
62+
63+
/** @var array<string, array<string, mixed>> $metrics */
5464
$baselineFile = new self(
55-
$data['createdAt'],
56-
$data['configHash'],
57-
$data['metrics']
65+
$createdAt,
66+
$configHash,
67+
$metrics
5868
);
5969

6070
return [
6171
'baselineFile' => $baselineFile,
62-
'metrics' => $data['metrics']
72+
'metrics' => $metrics
6373
];
6474
}
6575

6676
// Old format - return null for baselineFile, data as metrics
77+
/** @var array<string, mixed> $data */
6778
return [
6879
'baselineFile' => null,
6980
'metrics' => $data

0 commit comments

Comments
 (0)