build: upgrade PHPStan to level 9 and fix strict type errors

This commit is contained in:
Enes Yakıştır
2026-06-06 09:36:46 +03:00
parent 1ea2579d20
commit 75171c6f96
14 changed files with 124 additions and 77 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
parameters: parameters:
level: 6 level: 9
paths: paths:
- src - src
+11 -3
View File
@@ -30,8 +30,16 @@ class FixCommand extends Command
{ {
$io = new SymfonyStyle($input, $output); $io = new SymfonyStyle($input, $output);
$target = $input->getArgument('target'); $target = $input->getArgument('target');
$projectPath = $input->getOption('project-path') ?? getcwd(); if (!is_string($target)) {
$dryRun = $input->getOption('dry-run'); $io->error("Target must be a string URL or file path.");
return Command::FAILURE;
}
$projectPathOption = $input->getOption('project-path');
$projectPath = is_string($projectPathOption) ? $projectPathOption : getcwd();
if ($projectPath === false) {
$projectPath = '.';
}
$dryRun = (bool)$input->getOption('dry-run');
$config = new Config(); $config = new Config();
$apiKey = $config->getGeminiApiKey(); $apiKey = $config->getGeminiApiKey();
@@ -45,7 +53,7 @@ class FixCommand extends Command
// 1. Fetch HTML // 1. Fetch HTML
$html = @file_get_contents($target); $html = @file_get_contents($target);
if (!$html) { if ($html === false || $html === '') {
$io->error("Could not read target: $target"); $io->error("Could not read target: $target");
return Command::FAILURE; return Command::FAILURE;
} }
+12 -4
View File
@@ -33,14 +33,22 @@ class ScanCommand extends Command
{ {
$io = new SymfonyStyle($input, $output); $io = new SymfonyStyle($input, $output);
$target = $input->getArgument('target'); $target = $input->getArgument('target');
$enableAi = $input->getOption('ai'); if (!is_string($target)) {
$projectPath = $input->getOption('project-path') ?? getcwd(); $io->error("Target must be a string URL or file path.");
return Command::FAILURE;
}
$enableAi = (bool)$input->getOption('ai');
$projectPathOption = $input->getOption('project-path');
$projectPath = is_string($projectPathOption) ? $projectPathOption : getcwd();
if ($projectPath === false) {
$projectPath = '.';
}
$io->title("Accessibility Console - Scanning: $target"); $io->title("Accessibility Console - Scanning: $target");
// 1. Fetch HTML // 1. Fetch HTML
$html = @file_get_contents($target); $html = @file_get_contents($target);
if (!$html) { if ($html === false || $html === '') {
$io->error("Could not read target: $target"); $io->error("Could not read target: $target");
return Command::FAILURE; return Command::FAILURE;
} }
@@ -97,7 +105,7 @@ class ScanCommand extends Command
// Export Report // Export Report
$reportPath = $input->getOption('report'); $reportPath = $input->getOption('report');
if ($reportPath) { if (is_string($reportPath) && $reportPath !== '') {
$extension = pathinfo($reportPath, PATHINFO_EXTENSION); $extension = pathinfo($reportPath, PATHINFO_EXTENSION);
if ($extension === 'json') { if ($extension === 'json') {
$data = array_map(fn($v) => $v->toArray(), $violations); $data = array_map(fn($v) => $v->toArray(), $violations);
+10 -1
View File
@@ -21,12 +21,21 @@ abstract class AbstractRule
protected function createViolation(string $message, \DOMElement $element): Violation protected function createViolation(string $message, \DOMElement $element): Violation
{ {
$doc = $element->ownerDocument;
$html = '';
if ($doc !== null) {
$htmlVal = $doc->saveHTML($element);
if (is_string($htmlVal)) {
$html = $htmlVal;
}
}
return new Violation( return new Violation(
ruleId: (new \ReflectionClass($this))->getShortName(), ruleId: (new \ReflectionClass($this))->getShortName(),
message: $message, message: $message,
severity: $this->getSeverity(), severity: $this->getSeverity(),
standard: $this->getStandard(), standard: $this->getStandard(),
htmlSnippet: $element->ownerDocument->saveHTML($element) htmlSnippet: $html
); );
} }
} }
+14 -18
View File
@@ -19,25 +19,21 @@ class RuleEngine
{ {
$allViolations = []; $allViolations = [];
foreach ($this->rules as $rule) { foreach ($this->rules as $rule) {
if (method_exists($rule, 'check')) { if ($rule instanceof AbstractRule) {
// Determine if it's a document-level rule or element-level rule // Document-level rule (Core\AbstractRule style)
$reflection = new \ReflectionMethod($rule, 'check'); $violations = $rule->check($doc);
$params = $reflection->getParameters(); $allViolations = array_merge($allViolations, $violations);
} elseif ($rule instanceof \YakNet\AccessibilityConsole\Rules\RuleInterface) {
$type = $params[0]->getType(); // Element-level rule (Rules\RuleInterface style)
if (isset($params[0]) && $type instanceof \ReflectionNamedType && $type->getName() === 'DOMDocument') { $xpath = new \DOMXPath($doc);
// Document-level rule (Core\AbstractRule style) $elements = $xpath->query('//*');
$violations = $rule->check($doc); if ($elements !== false) {
$allViolations = array_merge($allViolations, $violations);
} else {
// Element-level rule (Rules\RuleInterface style)
// We need to iterate over all elements
$xpath = new \DOMXPath($doc);
$elements = $xpath->query('//*');
foreach ($elements as $element) { foreach ($elements as $element) {
$violation = $rule->check($element); if ($element instanceof \DOMElement) {
if ($violation) { $violation = $rule->check($element);
$allViolations[] = $violation; if ($violation) {
$allViolations[] = $violation;
}
} }
} }
} }
+4 -4
View File
@@ -4,11 +4,11 @@ namespace YakNet\AccessibilityConsole\Core;
class Scanner class Scanner
{ {
public function __construct(private ?RuleEngine $engine = null) private RuleEngine $engine;
public function __construct(?RuleEngine $engine = null)
{ {
if ($this->engine === null) { $this->engine = $engine ?? new RuleEngine();
$this->engine = new RuleEngine();
}
} }
public function addRule(mixed $rule): void public function addRule(mixed $rule): void
+10 -1
View File
@@ -16,12 +16,21 @@ abstract class AbstractRule implements RuleInterface
protected function createViolation(DOMElement $element, string $message, ?string $suggestion = null): Violation protected function createViolation(DOMElement $element, string $message, ?string $suggestion = null): Violation
{ {
$doc = $element->ownerDocument;
$html = '';
if ($doc !== null) {
$htmlVal = $doc->saveHTML($element);
if (is_string($htmlVal)) {
$html = $htmlVal;
}
}
return new Violation( return new Violation(
ruleId: $this->getId(), ruleId: $this->getId(),
message: $message, message: $message,
severity: $this->getSeverity(), severity: $this->getSeverity(),
standard: $this->getStandard(), standard: $this->getStandard(),
htmlSnippet: $element->ownerDocument->saveHTML($element), htmlSnippet: $html,
fixSuggestion: $suggestion fixSuggestion: $suggestion
); );
} }
+35 -31
View File
@@ -28,43 +28,47 @@ class AriaRole extends AbstractRule
// 1. Check WAI-ARIA Role Validity // 1. Check WAI-ARIA Role Validity
$roleElements = $xpath->query('//*[@role]'); $roleElements = $xpath->query('//*[@role]');
foreach ($roleElements as $el) { if ($roleElements !== false) {
if (!$el instanceof \DOMElement) { foreach ($roleElements as $el) {
continue; if (!$el instanceof \DOMElement) {
} continue;
$role = trim($el->getAttribute('role')); }
if ($role === '') { $role = trim($el->getAttribute('role'));
continue; if ($role === '') {
} continue;
$roles = array_filter(explode(' ', $role)); }
foreach ($roles as $r) { $roles = array_filter(explode(' ', $role));
if (!in_array(strtolower($r), self::$validRoles, true)) { foreach ($roles as $r) {
$violations[] = $this->createViolation( if (!in_array(strtolower($r), self::$validRoles, true)) {
"Invalid ARIA role \"{$r}\" declared on element. Must use standard WAI-ARIA roles.", $violations[] = $this->createViolation(
$el "Invalid ARIA role \"{$r}\" declared on element. Must use standard WAI-ARIA roles.",
); $el
);
}
} }
} }
} }
// 2. Check aria-controls Targets Existence // 2. Check aria-controls Targets Existence
$controlsElements = $xpath->query('//*[@aria-controls]'); $controlsElements = $xpath->query('//*[@aria-controls]');
foreach ($controlsElements as $el) { if ($controlsElements !== false) {
if (!$el instanceof \DOMElement) { foreach ($controlsElements as $el) {
continue; if (!$el instanceof \DOMElement) {
} continue;
$targetsStr = trim($el->getAttribute('aria-controls')); }
if ($targetsStr === '') { $targetsStr = trim($el->getAttribute('aria-controls'));
continue; if ($targetsStr === '') {
} continue;
$targets = array_filter(explode(' ', $targetsStr)); }
foreach ($targets as $targetId) { $targets = array_filter(explode(' ', $targetsStr));
$targetQuery = $xpath->query('//*[@id="' . $targetId . '"]'); foreach ($targets as $targetId) {
if ($targetQuery->length === 0) { $targetQuery = $xpath->query('//*[@id="' . $targetId . '"]');
$violations[] = $this->createViolation( if ($targetQuery !== false && $targetQuery->length === 0) {
"Attribute aria-controls=\"{$targetId}\" targets an element that does not exist in the document.", $violations[] = $this->createViolation(
$el "Attribute aria-controls=\"{$targetId}\" targets an element that does not exist in the document.",
); $el
);
}
} }
} }
} }
+6 -6
View File
@@ -99,14 +99,14 @@ class ColorContrast extends AbstractRule
{ {
$hex = ltrim($hex, '#'); $hex = ltrim($hex, '#');
if (strlen($hex) === 3) { if (strlen($hex) === 3) {
$r = hexdec(substr($hex, 0, 1) . substr($hex, 0, 1)); $r = (int)hexdec(substr($hex, 0, 1) . substr($hex, 0, 1));
$g = hexdec(substr($hex, 1, 1) . substr($hex, 1, 1)); $g = (int)hexdec(substr($hex, 1, 1) . substr($hex, 1, 1));
$b = hexdec(substr($hex, 2, 1) . substr($hex, 2, 1)); $b = (int)hexdec(substr($hex, 2, 1) . substr($hex, 2, 1));
return [$r, $g, $b]; return [$r, $g, $b];
} elseif (strlen($hex) === 6) { } elseif (strlen($hex) === 6) {
$r = hexdec(substr($hex, 0, 2)); $r = (int)hexdec(substr($hex, 0, 2));
$g = hexdec(substr($hex, 2, 2)); $g = (int)hexdec(substr($hex, 2, 2));
$b = hexdec(substr($hex, 4, 2)); $b = (int)hexdec(substr($hex, 4, 2));
return [$r, $g, $b]; return [$r, $g, $b];
} }
return null; return null;
+3
View File
@@ -13,6 +13,9 @@ class DuplicateId extends AbstractRule
$violations = []; $violations = [];
$xpath = new \DOMXPath($doc); $xpath = new \DOMXPath($doc);
$elements = $xpath->query('//*[@id]'); $elements = $xpath->query('//*[@id]');
if ($elements === false) {
return [];
}
$ids = []; $ids = [];
foreach ($elements as $el) { foreach ($elements as $el) {
+7 -4
View File
@@ -29,10 +29,13 @@ class FormLabel extends AbstractRule
if ($element->hasAttribute('id')) { if ($element->hasAttribute('id')) {
$id = $element->getAttribute('id'); $id = $element->getAttribute('id');
$xpath = new \DOMXPath($element->ownerDocument); $doc = $element->ownerDocument;
$labels = $xpath->query("//label[@for='$id']"); if ($doc !== null) {
if ($labels->length > 0) { $xpath = new \DOMXPath($doc);
return null; $labels = $xpath->query("//label[@for='$id']");
if ($labels !== false && $labels->length > 0) {
return null;
}
} }
} }
+3
View File
@@ -13,6 +13,9 @@ class HeadingOrderRule extends AbstractRule
$violations = []; $violations = [];
$xpath = new \DOMXPath($doc); $xpath = new \DOMXPath($doc);
$headings = $xpath->query('//h1|//h2|//h3|//h4|//h5|//h6'); $headings = $xpath->query('//h1|//h2|//h3|//h4|//h5|//h6');
if ($headings === false) {
return [];
}
$lastLevel = 0; $lastLevel = 0;
foreach ($headings as $h) { foreach ($headings as $h) {
+3 -1
View File
@@ -9,7 +9,8 @@ class StandardRuleSet
*/ */
public static function all(): array public static function all(): array
{ {
return [ /** @var RuleInterface[] $rules */
$rules = [
new HtmlHasLang(), new HtmlHasLang(),
new ImgAltText(), new ImgAltText(),
new EmptyLink(), new EmptyLink(),
@@ -24,5 +25,6 @@ class StandardRuleSet
new AriaRole(), new AriaRole(),
new IframeTitle(), new IframeTitle(),
]; ];
return $rules;
} }
} }
+5 -3
View File
@@ -24,7 +24,8 @@ class SourceLocator
return null; return null;
} }
$cleanSnippet = trim(preg_replace('/\s+/', ' ', $snippet)); $replaced = preg_replace('/\s+/', ' ', $snippet);
$cleanSnippet = is_string($replaced) ? trim($replaced) : '';
if ($cleanSnippet === '') { if ($cleanSnippet === '') {
return null; return null;
} }
@@ -81,7 +82,7 @@ class SourceLocator
} }
/** /**
* @return array{type: string, score?: float, location?: array{file: string, line: int}} * @return array{type: 'exact', location: array{file: string, line: int}}|array{type: 'fuzzy', score: float, location: array{file: string, line: int}}|array{type: 'none'}
*/ */
private function searchInFile(string $filepath, string $searchSnippet, string $fullSnippet): array private function searchInFile(string $filepath, string $searchSnippet, string $fullSnippet): array
{ {
@@ -98,7 +99,8 @@ class SourceLocator
foreach ($lines as $index => $line) { foreach ($lines as $index => $line) {
$lineNumber = $index + 1; $lineNumber = $index + 1;
$cleanLine = trim(preg_replace('/\s+/', ' ', $line)); $replacedLine = preg_replace('/\s+/', ' ', $line);
$cleanLine = is_string($replacedLine) ? trim($replacedLine) : '';
if (str_contains($cleanLine, $searchSnippet)) { if (str_contains($cleanLine, $searchSnippet)) {
return [ return [