Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
parameters:
level: 8
level: 10
paths:
- src
parallel:
Expand Down
28 changes: 24 additions & 4 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -258,12 +258,12 @@ private function bootstrapMetricsCollectors(): void
private function configureEventBus(): void
{
$progressbar = new ProgressBarHandler(
$this->get(OutputInterface::class)
$this->resolveOutputInterface()
);

$verbose = new VerboseHandler(
$this->get(InputInterface::class),
$this->get(OutputInterface::class)
$this->resolveInputInterface(),
$this->resolveOutputInterface()
);

$handlersLocator = $this->setUpEventHandlersLocator($progressbar, $verbose);
Expand Down Expand Up @@ -502,8 +502,28 @@ private function setUpEventHandlersLocator(
$verbose
],
ParserFailed::class => [
new ParserErrorHandler($this->get(OutputInterface::class))
new ParserErrorHandler($this->resolveOutputInterface())
],
]);
}

private function resolveInputInterface(): InputInterface
{
$input = $this->get(InputInterface::class);
if (!$input instanceof InputInterface) {
throw new CognitiveAnalysisException('Console input is not configured.');
}

return $input;
}

private function resolveOutputInterface(): OutputInterface
{
$output = $this->get(OutputInterface::class);
if (!$output instanceof OutputInterface) {
throw new CognitiveAnalysisException('Console output is not configured.');
}

return $output;
}
}
31 changes: 24 additions & 7 deletions src/Business/Churn/ChurnMetrics.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,35 @@ public function __construct(
*/
public static function fromArray(string $className, array $data): self
{
$file = $data['file'] ?? '';
$riskLevel = $data['riskLevel'] ?? null;

return new self(
className: $className,
file: $data['file'] ?? '',
score: (float)($data['score'] ?? 0),
timesChanged: (int)($data['timesChanged'] ?? 0),
churn: (float)($data['churn'] ?? 0),
coverage: isset($data['coverage']) ? (float)$data['coverage'] : null,
riskChurn: isset($data['riskChurn']) ? (float)$data['riskChurn'] : null,
riskLevel: $data['riskLevel'] ?? null
file: is_string($file) ? $file : '',
score: self::resolveFloatValue($data['score'] ?? null, 0.0),
timesChanged: self::resolveIntValue($data['timesChanged'] ?? null, 0),
churn: self::resolveFloatValue($data['churn'] ?? null, 0.0),
coverage: isset($data['coverage']) ? self::resolveFloatValue($data['coverage'], 0.0) : null,
riskChurn: isset($data['riskChurn']) ? self::resolveFloatValue($data['riskChurn'], 0.0) : null,
riskLevel: is_string($riskLevel) ? $riskLevel : null
);
}

private static function resolveIntValue(mixed $value, int $default): int
{
return is_int($value) ? $value : $default;
}

private static function resolveFloatValue(mixed $value, float $default): float
{
if (is_int($value) || is_float($value)) {
return (float) $value;
}

return $default;
}

/**
* Convert to array format (for backward compatibility).
*
Expand Down
2 changes: 1 addition & 1 deletion src/Business/Churn/ChurnMetricsCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ public function toArray(): array
{
$result = [];
foreach ($this->metrics as $className => $metric) {
$result[$className] = $metric->toArray();
$result[(string) $className] = $metric->toArray();
}
return $result;
}
Expand Down
23 changes: 19 additions & 4 deletions src/Business/Churn/Report/ChurnReportFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,14 @@ public function create(string $type): ReportGeneratorInterface
return $builtIn;
}

// Check custom exporters
if (isset($customReporters[$type])) {
return $this->createCustomExporter($customReporters[$type]);
$exporterConfig = $customReporters[$type];
if (!is_array($exporterConfig)) {
throw new InvalidArgumentException("Invalid custom exporter configuration for type: {$type}");
}

/** @var array<string, mixed> $exporterConfig */
return $this->createCustomExporter($exporterConfig);
}

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

$this->registry->loadExporter($config['class'], $config['file'] ?? null);
$class = $config['class'] ?? null;
if (!is_string($class)) {
throw new InvalidArgumentException('Custom exporter must define a "class" string.');
}

$file = $config['file'] ?? null;
if ($file !== null && !is_string($file)) {
throw new InvalidArgumentException('Custom exporter "file" must be a string or null.');
}

$this->registry->loadExporter($class, $file);
$exporter = $this->registry->instantiate(
$config['class'],
$class,
$cognitiveConfig
);
$this->registry->validateInterface($exporter, ReportGeneratorInterface::class);
Expand Down
33 changes: 24 additions & 9 deletions src/Business/Churn/Report/SvgTreemapReport.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,12 @@ private function renderSvgRects(array $rects, float $minScore, float $maxScore):
{
$svgRects = [];
foreach ($rects as $rect) {
$normalizedScore = $this->treemapMath->normalizeScore(score: $rect['score'], minScore: $minScore, maxScore: $maxScore);
$score = $this->resolveFloatValue($rect['score'] ?? null, 0.0);
$normalizedScore = $this->treemapMath->normalizeScore(
score: $score,
minScore: $minScore,
maxScore: $maxScore
);
$svgRects[] = $this->renderSvgRect(rect: $rect, normalizedScore: $normalizedScore);
}

Expand All @@ -98,17 +103,18 @@ private function renderSvgRects(array $rects, float $minScore, float $maxScore):
*/
private function renderSvgRect(array $rect, float $normalizedScore): string
{
$x = $rect['x'] + self::PADDING;
$y = $rect['y'] + self::PADDING;
$width = max(0, $rect['width'] - self::PADDING * 2);
$height = max(0, $rect['height'] - self::PADDING * 2);
$x = $this->resolveFloatValue($rect['x'] ?? null, 0.0) + self::PADDING;
$y = $this->resolveFloatValue($rect['y'] ?? null, 0.0) + self::PADDING;
$width = max(0, $this->resolveFloatValue($rect['width'] ?? null, 0.0) - self::PADDING * 2);
$height = max(0, $this->resolveFloatValue($rect['height'] ?? null, 0.0) - self::PADDING * 2);
$color = $this->treemapMath->scoreToColor(score: $normalizedScore);
$class = htmlspecialchars($rect['class']);
$churn = $rect['churn'];
$score = $rect['score'];
$className = is_string($rect['class'] ?? null) ? $rect['class'] : '';
$class = htmlspecialchars($className);
$churn = $this->resolveFloatValue($rect['churn'] ?? null, 0.0);
$score = $this->resolveFloatValue($rect['score'] ?? null, 0.0);
$textX = $x + 4;
$textY = $y + 18;
$label = htmlspecialchars(mb_strimwidth($rect['class'], 0, 40, '…'));
$label = htmlspecialchars(mb_strimwidth($className, 0, 40, '…'));

return sprintf(
'<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>',
Expand Down Expand Up @@ -153,4 +159,13 @@ private function wrapSvg(string $rectsSvg): string
</svg>
SVG;
}

private function resolveFloatValue(mixed $value, float $default): float
{
if (is_int($value) || is_float($value)) {
return (float) $value;
}

return $default;
}
}
13 changes: 11 additions & 2 deletions src/Business/Churn/Report/TreemapMath.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ public function prepareItems(array $classes): array
{
$items = [];
foreach ($classes as $class => $data) {
$churn = (float)($data['churn'] ?? 0);
$score = (float)($data['score'] ?? 0);
$churn = $this->resolveFloatValue($data['churn'] ?? null, 0.0);
$score = $this->resolveFloatValue($data['score'] ?? null, 0.0);
if ($churn <= 0) {
continue;
}
Expand Down Expand Up @@ -261,4 +261,13 @@ private function findSplitIndex(array $items, float $sum): int

return 1;
}

private function resolveFloatValue(mixed $value, float $default): float
{
if (is_int($value) || is_float($value)) {
return (float) $value;
}

return $default;
}
}
4 changes: 4 additions & 0 deletions src/Business/CodeCoverage/CloverReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,10 @@ private function extractMethodCoverageFromLines(mixed $allLines, int $methodLine
$coveredStatements = 0;
$inMethod = false;

if (!is_iterable($allLines)) {
return ['statements' => 0, 'covered' => 0];
}

foreach ($allLines as $line) {
if (!$line instanceof DOMElement) {
continue;
Expand Down
25 changes: 22 additions & 3 deletions src/Business/Cognitive/Baseline/Baseline.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,17 @@ public function calculateDeltas(
$warnings = [];

foreach ($baseline as $class => $data) {
foreach ($data['methods'] as $methodName => $methodData) {
$methods = $data['methods'] ?? null;
if (!is_array($methods)) {
continue;
}

foreach ($methods as $methodName => $methodData) {
if (!is_string($methodName) || !is_array($methodData)) {
continue;
}

/** @var array<string, mixed> $methodData */
$metrics = $metricsCollection->getClassWithMethod($class, $methodName);
if (!$metrics) {
continue;
Expand Down Expand Up @@ -57,6 +67,11 @@ public function loadBaseline(string $baselineFile): array
}

$data = json_decode($baseline, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($data)) {
throw new CognitiveAnalysisException('Baseline file must contain a JSON object.');
}

/** @var array<string, mixed> $data */

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

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

return [
'metrics' => $result['metrics'],
'metrics' => $metrics,
'baselineFile' => $result['baselineFile'],
'warnings' => []
];
Expand Down Expand Up @@ -182,10 +199,12 @@ public function isValidBaselineFile(string $filePath): bool
}

$data = json_decode($content, true);
if ($data === null) {
if (!is_array($data)) {
return false;
}

/** @var array<string, mixed> $data */

// Use schema validator for comprehensive validation
$validator = new BaselineSchemaValidator();
return $validator->isValidBaseline($data);
Expand Down
19 changes: 15 additions & 4 deletions src/Business/Cognitive/Baseline/BaselineFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\Baseline;

use InvalidArgumentException;
use JsonSerializable;
use Phauthentic\CognitiveCodeAnalysis\Config\CognitiveConfig;
use Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetricsCollection;
Expand Down Expand Up @@ -51,19 +52,29 @@ public static function fromJson(array $data): array
{
// Check if this is the new format (has version field)
if (isset($data['version']) && $data['version'] === self::VERSION) {
$createdAt = $data['createdAt'] ?? null;
$configHash = $data['configHash'] ?? null;
$metrics = $data['metrics'] ?? null;

if (!is_string($createdAt) || !is_string($configHash) || !is_array($metrics)) {
throw new InvalidArgumentException('Invalid baseline file metadata.');
}

/** @var array<string, array<string, mixed>> $metrics */
$baselineFile = new self(
$data['createdAt'],
$data['configHash'],
$data['metrics']
$createdAt,
$configHash,
$metrics
);

return [
'baselineFile' => $baselineFile,
'metrics' => $data['metrics']
'metrics' => $metrics
];
}

// Old format - return null for baselineFile, data as metrics
/** @var array<string, mixed> $data */
return [
'baselineFile' => null,
'metrics' => $data
Expand Down
8 changes: 6 additions & 2 deletions src/Business/Cognitive/Baseline/BaselineSchemaValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ private function validateNewFormat(array $data): array

// Validate version
if ($data['version'] !== '2.0') {
$errors[] = "Invalid version: {$data['version']}. Expected: 2.0";
$version = is_string($data['version']) ? $data['version'] : gettype($data['version']);
$errors[] = "Invalid version: {$version}. Expected: 2.0";
}

// Validate createdAt format
Expand All @@ -70,10 +71,13 @@ private function validateNewFormat(array $data): array
$errors[] = "Invalid configHash. Must be a non-empty string";
}

$errors = array_merge($errors, $this->validateMetrics($data['metrics']));
// Validate metrics structure
if (!is_array($data['metrics'])) {
$errors[] = "Invalid metrics. Must be an object";
} else {
/** @var array<string, mixed> $metrics */
$metrics = $data['metrics'];
$errors = array_merge($errors, $this->validateMetrics($metrics));
}

// Check for additional properties
Expand Down
Loading
Loading