diff --git a/phpstan.neon b/phpstan.neon index 8fc3894..0ec59d3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,6 +2,6 @@ includes: - phar://phpstan.phar/conf/bleedingEdge.neon parameters: - level: 8 + level: 9 paths: - src diff --git a/src/Block/Inspector.php b/src/Block/Inspector.php index eaea02a..aa4d20a 100644 --- a/src/Block/Inspector.php +++ b/src/Block/Inspector.php @@ -89,7 +89,8 @@ public function getJsUrl(): string */ public function getTheme(): string { - return (string) $this->scopeConfig->getValue('mageforge/inspector/theme') ?: 'dark'; + $value = $this->scopeConfig->getValue('mageforge/inspector/theme'); + return is_string($value) && $value !== '' ? $value : 'dark'; } /** diff --git a/src/Console/Command/AbstractCommand.php b/src/Console/Command/AbstractCommand.php index 561402d..e7bb887 100644 --- a/src/Console/Command/AbstractCommand.php +++ b/src/Console/Command/AbstractCommand.php @@ -6,8 +6,8 @@ use Laravel\Prompts\SelectPrompt; use Magento\Framework\Console\Cli; -use OpenForgeProject\MageForge\Service\ThemeSuggester; use OpenForgeProject\MageForge\Model\ThemeList; +use OpenForgeProject\MageForge\Service\ThemeSuggester; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -183,7 +183,7 @@ protected function handleInvalidThemeWithSuggestions( return null; } - return $selection; + return is_string($selection) ? $selection : null; } catch (\Exception $e) { $this->resetPromptEnvironment(); $this->io->error('Selection failed: ' . $e->getMessage()); @@ -487,10 +487,8 @@ private function removeSecureEnvironmentValue(string $name): void * @param ThemeList $themeList * @return array */ - protected function resolveVendorThemes( - array $themeCodes, - ThemeList $themeList - ): array { + protected function resolveVendorThemes(array $themeCodes, ThemeList $themeList): array + { $resolved = []; $availableThemes = null; @@ -502,10 +500,7 @@ protected function resolveVendorThemes( if ($isExplicitWildcard || $isVendorOnly) { // Lazy-load themes only when needed if ($availableThemes === null) { - $availableThemes = array_map( - fn($theme) => $theme->getCode(), - $themeList->getAllThemes() - ); + $availableThemes = array_map(fn($theme) => $theme->getCode(), $themeList->getAllThemes()); } if ($isExplicitWildcard) { @@ -514,10 +509,10 @@ protected function resolveVendorThemes( $prefix = $code . '/'; // e.g. "Vendor" -> "Vendor/" } - $matched = array_filter( - $availableThemes, - fn(string $availableCode) => \str_starts_with($availableCode, $prefix) - ); + $matched = array_filter($availableThemes, fn(string $availableCode) => \str_starts_with( + $availableCode, + $prefix, + )); if (empty($matched)) { $this->io->warning(sprintf("No themes found for vendor/prefix '%s'", $prefix)); @@ -532,7 +527,7 @@ protected function resolveVendorThemes( "Resolved vendor '%s' to %d theme(s): %s", $code, count($matched), - implode(', ', $matched) + implode(', ', $matched), )); foreach ($matched as $match) { diff --git a/src/Console/Command/Dev/InspectorCommand.php b/src/Console/Command/Dev/InspectorCommand.php index 7719e75..5ba4523 100644 --- a/src/Console/Command/Dev/InspectorCommand.php +++ b/src/Console/Command/Dev/InspectorCommand.php @@ -82,7 +82,8 @@ protected function configure(): void */ protected function executeCommand(InputInterface $input, OutputInterface $output): int { - $action = strtolower((string) $input->getArgument(self::ARGUMENT_ACTION)); + $arg = $input->getArgument(self::ARGUMENT_ACTION); + $action = strtolower(is_string($arg) ? $arg : ''); // Validate action if (!in_array($action, ['enable', 'disable', 'status'], true)) { diff --git a/src/Console/Command/Hyva/CompatibilityCheckCommand.php b/src/Console/Command/Hyva/CompatibilityCheckCommand.php index 4265c98..4cd297e 100644 --- a/src/Console/Command/Hyva/CompatibilityCheckCommand.php +++ b/src/Console/Command/Hyva/CompatibilityCheckCommand.php @@ -19,6 +19,10 @@ * * Scans modules for RequireJS, Knockout.js, jQuery, and UI Components usage * that would be incompatible with Hyvä themes. + * + * @phpstan-import-type CheckResults from \OpenForgeProject\MageForge\Service\Hyva\CompatibilityChecker + * @phpstan-import-type CheckSummary from \OpenForgeProject\MageForge\Service\Hyva\CompatibilityChecker + * @phpstan-import-type ModuleEntry from \OpenForgeProject\MageForge\Service\Hyva\CompatibilityChecker */ class CompatibilityCheckCommand extends AbstractCommand { @@ -146,7 +150,7 @@ private function runInteractiveMode(InputInterface $input, OutputInterface $outp // Detailed view confirmation $detailedPrompt = new ConfirmPrompt(label: 'Show detailed file-level issues?', default: false); - $detailed = $detailedPrompt->prompt(); + $detailed = (bool) $detailedPrompt->prompt(); // Map selected options to flags $showAll = $displayMode === self::DISPLAY_MODE_SHOW_ALL; @@ -193,10 +197,10 @@ private function runInteractiveMode(InputInterface $input, OutputInterface $outp */ private function runDirectMode(InputInterface $input, OutputInterface $output): int { - $showAll = $input->getOption(self::OPTION_SHOW_ALL); - $thirdPartyOnly = $input->getOption(self::OPTION_THIRD_PARTY_ONLY); - $includeVendor = $input->getOption(self::OPTION_INCLUDE_VENDOR); - $detailed = $input->getOption(self::OPTION_DETAILED); + $showAll = (bool) $input->getOption(self::OPTION_SHOW_ALL); + $thirdPartyOnly = (bool) $input->getOption(self::OPTION_THIRD_PARTY_ONLY); + $includeVendor = (bool) $input->getOption(self::OPTION_INCLUDE_VENDOR); + $detailed = (bool) $input->getOption(self::OPTION_DETAILED); $this->io->title('Hyvä Theme Compatibility Check'); @@ -271,7 +275,7 @@ private function runScan( * Display compatibility check results * * @param array $results - * @phpstan-param array $results + * @phpstan-param CheckResults $results * @param bool $showAll */ private function displayResults(array $results, bool $showAll): void @@ -292,7 +296,7 @@ private function displayResults(array $results, bool $showAll): void * Display detailed file-level issues * * @param array $results - * @phpstan-param array $results + * @phpstan-param CheckResults $results */ private function displayDetailedIssues(array $results): void { @@ -333,7 +337,7 @@ private function displayDetailedIssues(array $results): void * Display summary statistics * * @param array $results - * @phpstan-param array $results + * @phpstan-param array{summary: CheckSummary} $results */ private function displaySummary(array $results): void { diff --git a/src/Console/Command/System/CheckCommand.php b/src/Console/Command/System/CheckCommand.php index d8ec8f7..774ec50 100644 --- a/src/Console/Command/System/CheckCommand.php +++ b/src/Console/Command/System/CheckCommand.php @@ -162,8 +162,13 @@ private function getLatestLtsNodeVersion(): string return 'Unknown'; } + /** @var array> $nodes */ foreach ($nodes as $node) { - if (isset($node['lts']) && $node['lts'] !== false) { + if (isset($node['lts']) + && $node['lts'] !== false + && isset($node['version']) + && is_string($node['version']) + ) { return trim($node['version'], 'v'); } } @@ -407,12 +412,15 @@ private function getSearchEngineFromMagentoConfig(): ?string private function checkSearchEngineViaDeploymentConfig($objectManager): ?string { try { + /** @var \Magento\Framework\App\DeploymentConfig $deploymentConfig */ $deploymentConfig = $objectManager->get(\Magento\Framework\App\DeploymentConfig::class); $engineConfig = $deploymentConfig->get('system/search/engine'); - if (!empty($engineConfig)) { - $host = $deploymentConfig->get('system/search/engine_host') ?: 'localhost'; - $port = $deploymentConfig->get('system/search/engine_port') ?: '9200'; + if (!empty($engineConfig) && is_string($engineConfig)) { + $hostRaw = $deploymentConfig->get('system/search/engine_host'); + $portRaw = $deploymentConfig->get('system/search/engine_port'); + $host = is_string($hostRaw) ? $hostRaw : 'localhost'; + $port = is_string($portRaw) ? $portRaw : '9200'; $url = "http://{$host}:{$port}"; if ($this->testElasticsearchConnection($url)) { @@ -439,12 +447,11 @@ private function checkSearchEngineViaDeploymentConfig($objectManager): ?string private function checkSearchEngineViaEngineResolver($objectManager): ?string { try { + /** @var \Magento\Framework\Search\EngineResolverInterface $engineResolver */ $engineResolver = $objectManager->get(\Magento\Framework\Search\EngineResolverInterface::class); - if ($engineResolver) { - $currentEngine = $engineResolver->getCurrentSearchEngine(); - if (!empty($currentEngine)) { - return ucfirst($currentEngine) . ' (Magento config)'; - } + $currentEngine = $engineResolver->getCurrentSearchEngine(); + if (!empty($currentEngine)) { + return ucfirst($currentEngine) . ' (Magento config)'; } } catch (\Exception $e) { if ($this->io->isVerbose()) { @@ -541,12 +548,16 @@ private function getSearchEngineHosts(): array */ private function formatSearchEngineVersion(array $info): string { - if (isset($info['version']['distribution']) && $info['version']['distribution'] === 'opensearch') { - return 'OpenSearch ' . $info['version']['number']; + $version = $info['version'] ?? null; + if (!is_array($version)) { + return 'Search Engine Available'; + } + if (isset($version['distribution']) && $version['distribution'] === 'opensearch') { + return 'OpenSearch ' . (is_string($version['number']) ? $version['number'] : ''); } - if (isset($info['version']['number'])) { - return 'Elasticsearch ' . $info['version']['number']; + if (isset($version['number'])) { + return 'Elasticsearch ' . (is_string($version['number']) ? $version['number'] : ''); } return 'Search Engine Available'; @@ -597,6 +608,7 @@ private function tryMagentoHttpClient(string $url): ?array if ($status === 200 && !empty($response)) { $data = json_decode($response, true); if (is_array($data)) { + /** @var array $data */ return $data; } } @@ -661,10 +673,24 @@ private function getDiskSpace(): string $totalSpace = disk_total_space('.'); $freeSpace = disk_free_space('.'); - $totalGB = round((($totalSpace / 1024) / 1024) / 1024, 2); - $freeGB = round((($freeSpace / 1024) / 1024) / 1024, 2); + if ($totalSpace === false || $freeSpace === false) { + return 'Unknown'; + } + + if ($totalSpace <= 0) { + $totalGB = 0.0; + $usedGB = 0.0; + $usedPercent = 0.0; + + return "$usedGB GB / $totalGB GB ($usedPercent%)"; + } + + $totalGB = round($totalSpace / 1024 / 1024 / 1024, 2); + $freeGB = round($freeSpace / 1024 / 1024 / 1024, 2); $usedGB = round($totalGB - $freeGB, 2); - $usedPercent = round(($usedGB / $totalGB) * 100, 2); + $usedPercent = $totalGB > 0.0 + ? round(($usedGB / $totalGB) * 100, 2) + : 0.0; return "$usedGB GB / $totalGB GB ($usedPercent%)"; } @@ -734,9 +760,10 @@ private function getMagentoEnvironmentValue(string $name): ?string private function getValueFromDeploymentConfig($objectManager, string $name): ?string { try { + /** @var \Magento\Framework\App\DeploymentConfig $deploymentConfig */ $deploymentConfig = $objectManager->get(\Magento\Framework\App\DeploymentConfig::class); $envValue = $deploymentConfig->get('system/default/environment/' . $name); - if ($envValue !== null) { + if ($envValue !== null && is_scalar($envValue)) { return (string) $envValue; } } catch (\Exception $e) { @@ -760,7 +787,7 @@ private function getValueFromEnvironmentService($objectManager, string $name): ? try { $environmentService = $objectManager->get(\Magento\Framework\App\EnvironmentInterface::class); $method = 'get' . str_replace(' ', '', ucwords(str_replace('_', ' ', strtolower($name)))); - if (method_exists($environmentService, $method)) { + if (is_object($environmentService) && method_exists($environmentService, $method)) { $value = $environmentService->$method(); if ($value !== null) { return (string) $value; diff --git a/src/Console/Command/System/CheckCommand.php.new b/src/Console/Command/System/CheckCommand.php.new deleted file mode 100644 index e69de29..0000000 diff --git a/src/Console/Command/System/VersionCommand.php b/src/Console/Command/System/VersionCommand.php index 9e58891..02f515c 100644 --- a/src/Console/Command/System/VersionCommand.php +++ b/src/Console/Command/System/VersionCommand.php @@ -73,7 +73,10 @@ private function getModuleVersion(): string try { $composerJson = $this->fileDriver->fileGetContents(__DIR__ . '/../../../../composer.json'); $composerData = json_decode($composerJson, true); - return $composerData['version'] ?? self::UNKNOWN_VERSION; + if (is_array($composerData) && isset($composerData['version']) && is_string($composerData['version'])) { + return $composerData['version']; + } + return self::UNKNOWN_VERSION; } catch (\Exception $e) { return self::UNKNOWN_VERSION; } @@ -96,7 +99,9 @@ private function getLatestVersion(): string if ($response->getStatusCode() === 200) { $data = json_decode($response->getBody()->getContents(), true); - return $data['tag_name'] ?? self::UNKNOWN_VERSION; + if (is_array($data) && isset($data['tag_name']) && is_string($data['tag_name'])) { + return $data['tag_name']; + } } } catch (\Exception $e) { if ($this->io->isVerbose()) { diff --git a/src/Console/Command/Theme/BuildCommand.php b/src/Console/Command/Theme/BuildCommand.php index 8d89536..c1558a9 100644 --- a/src/Console/Command/Theme/BuildCommand.php +++ b/src/Console/Command/Theme/BuildCommand.php @@ -65,6 +65,7 @@ protected function configure(): void */ protected function executeCommand(InputInterface $input, OutputInterface $output): int { + /** @var array $themeCodes */ $themeCodes = $input->getArgument('themeCodes'); // Allow wildcards using the AbstractCommand helper @@ -97,7 +98,7 @@ protected function executeCommand(InputInterface $input, OutputInterface $output label: 'Select themes to build', options: fn(string $value) => empty($value) ? $options - : array_values(array_filter($options, fn($option) => stripos((string)$option, $value) !== false)), + : array_values(array_filter($options, fn($option) => stripos((string) $option, $value) !== false)), placeholder: 'Type to search theme...', hint: 'Type to search, arrow keys to navigate, Space to toggle, Enter to confirm', required: false, @@ -106,6 +107,7 @@ protected function executeCommand(InputInterface $input, OutputInterface $output try { $themeCodes = $themeCodesPrompt->prompt(); \Laravel\Prompts\Prompt::terminal()->restoreTty(); + /** @var array $themeCodes */ // Reset environment $this->resetPromptEnvironment(); @@ -355,14 +357,14 @@ private function displayBuildSummary(SymfonyStyle $io, array $successList, float $io->success(sprintf( '🚀 Successfully built %d theme(s). Build process completed in %.2f seconds.', $successCount, - $duration + $duration, )); $io->writeln('Summary:'); $io->newLine(); } else { $io->warning(sprintf( 'Build process completed in %.2f seconds, but no themes were built successfully.', - $duration + $duration, )); return; } diff --git a/src/Console/Command/Theme/CleanCommand.php b/src/Console/Command/Theme/CleanCommand.php index 053ac26..569c6a2 100644 --- a/src/Console/Command/Theme/CleanCommand.php +++ b/src/Console/Command/Theme/CleanCommand.php @@ -70,7 +70,7 @@ protected function configure(): void */ protected function executeCommand(InputInterface $input, OutputInterface $output): int { - $dryRun = $input->getOption('dry-run'); + $dryRun = (bool) $input->getOption('dry-run'); if ($dryRun) { $this->io->note('DRY RUN MODE: No files will be deleted'); @@ -98,6 +98,7 @@ protected function executeCommand(InputInterface $input, OutputInterface $output */ private function resolveThemeCodes(InputInterface $input, OutputInterface $output): ?array { + /** @var array $themeCodes */ $themeCodes = $input->getArgument('themeCodes'); $cleanAll = $input->getOption('all'); @@ -163,7 +164,7 @@ private function selectThemesInteractively(OutputInterface $output): ?array /** * Display available themes for non-interactive environments * - * @param array $themes + * @param \Magento\Theme\Model\Theme[] $themes * @return void */ private function displayAvailableThemes(array $themes): void @@ -188,9 +189,9 @@ private function displayAvailableThemes(array $themes): void /** * Prompt user to select themes * - * @param array $options - * @param array $themes - * @return array|null + * @param string[] $options + * @param \Magento\Theme\Model\Theme[] $themes + * @return string[]|null */ private function promptForThemes(array $options, array $themes): ?array { @@ -200,7 +201,7 @@ private function promptForThemes(array $options, array $themes): ?array label: 'Select themes to clean', options: fn(string $value) => empty($value) ? $options - : array_values(array_filter($options, fn($option) => stripos((string)$option, $value) !== false)), + : array_values(array_filter($options, fn($option) => stripos((string) $option, $value) !== false)), placeholder: 'Type to search theme...', hint: 'Type to search, arrow keys to navigate, Space to toggle, Enter to confirm', required: false, @@ -216,6 +217,7 @@ private function promptForThemes(array $options, array $themes): ?array return null; } + /** @var array $themeCodes */ return $themeCodes; } catch (\Exception $e) { $this->resetPromptEnvironment(); @@ -231,7 +233,7 @@ private function promptForThemes(array $options, array $themes): ?array * @param array $themeCodes * @param bool $dryRun * @param OutputInterface $output - * @return array [totalCleaned, failedThemes] + * @return array{int, array} [totalCleaned, failedThemes] */ private function processThemes(array $themeCodes, bool $dryRun, OutputInterface $output): array { diff --git a/src/Console/Command/Theme/TokensCommand.php b/src/Console/Command/Theme/TokensCommand.php index f97684a..11a3b46 100644 --- a/src/Console/Command/Theme/TokensCommand.php +++ b/src/Console/Command/Theme/TokensCommand.php @@ -68,7 +68,8 @@ protected function configure(): void */ protected function executeCommand(InputInterface $input, OutputInterface $output): int { - $themeCode = $this->selectTheme($input->getArgument('themeCode')); + $arg = $input->getArgument('themeCode'); + $themeCode = $this->selectTheme(is_string($arg) ? $arg : null); if ($themeCode === null) { return Cli::RETURN_FAILURE; } @@ -112,7 +113,7 @@ private function selectTheme(?string $themeCode): ?string label: 'Select theme to generate tokens for', options: fn(string $value) => empty($value) ? $options - : array_values(array_filter($options, fn($option) => stripos((string)$option, $value) !== false)), + : array_values(array_filter($options, fn($option) => stripos((string) $option, $value) !== false)), placeholder: 'Type to search theme...', scroll: 10, hint: 'Type to search, arrow keys to navigate, Enter to confirm', @@ -121,7 +122,7 @@ private function selectTheme(?string $themeCode): ?string try { $themeCode = $themeCodePrompt->prompt(); \Laravel\Prompts\Prompt::terminal()->restoreTty(); - return $themeCode; + return is_string($themeCode) ? $themeCode : null; } catch (\Exception $e) { $this->io->error('Interactive mode failed: ' . $e->getMessage()); return null; diff --git a/src/Console/Command/Theme/WatchCommand.php b/src/Console/Command/Theme/WatchCommand.php index eea1da9..ce47cf7 100644 --- a/src/Console/Command/Theme/WatchCommand.php +++ b/src/Console/Command/Theme/WatchCommand.php @@ -77,7 +77,7 @@ protected function executeCommand(InputInterface $input, OutputInterface $output label: 'Select theme to watch', options: fn(string $value) => empty($value) ? $options - : array_values(array_filter($options, fn($option) => stripos((string)$option, $value) !== false)), + : array_values(array_filter($options, fn($option) => stripos((string) $option, $value) !== false)), placeholder: 'Type to search theme...', scroll: 10, hint: 'Type to search, arrow keys to navigate, Enter to confirm', @@ -87,6 +87,11 @@ protected function executeCommand(InputInterface $input, OutputInterface $output \Laravel\Prompts\Prompt::terminal()->restoreTty(); } + if (!is_string($themeCode)) { + $this->io->error('No valid theme code provided.'); + return self::FAILURE; + } + $themePath = $this->themePath->getPath($themeCode); if ($themePath === null) { // Try to suggest similar themes diff --git a/src/Model/ThemeList.php b/src/Model/ThemeList.php index c3219d7..939bbcb 100644 --- a/src/Model/ThemeList.php +++ b/src/Model/ThemeList.php @@ -5,6 +5,7 @@ namespace OpenForgeProject\MageForge\Model; use Magento\Framework\View\Design\Theme\ThemeList as MagentoThemeList; +use Magento\Theme\Model\Theme; class ThemeList { @@ -21,10 +22,12 @@ public function __construct( /** * Get all themes * - * @return array + * @return array */ public function getAllThemes(): array { - return $this->magentoThemeList->getItems(); + /** @var array $items */ + $items = $this->magentoThemeList->getItems(); + return $items; } } diff --git a/src/Service/Hyva/CompatibilityChecker.php b/src/Service/Hyva/CompatibilityChecker.php index de01676..d513639 100644 --- a/src/Service/Hyva/CompatibilityChecker.php +++ b/src/Service/Hyva/CompatibilityChecker.php @@ -14,6 +14,29 @@ * * This service scans modules, detects incompatibilities with Hyvä theme framework, * and provides formatted results with summary statistics. + * + * @phpstan-import-type ScanIssue from IncompatibilityDetector + * @phpstan-import-type ScanResult from ModuleScanner + * @phpstan-import-type ModuleInfo from ModuleScanner + * @phpstan-type ModuleEntry array{ + * compatible: bool, + * hasWarnings: bool, + * scanResult: ScanResult, + * moduleInfo: ModuleInfo + * } + * @phpstan-type CheckSummary array{ + * total: int, + * compatible: int, + * incompatible: int, + * hyvaAware: int, + * criticalIssues: int, + * warningIssues: int + * } + * @phpstan-type CheckResults array{ + * modules: array, + * summary: CheckSummary, + * hasIncompatibilities: bool + * } */ class CompatibilityChecker { @@ -37,7 +60,7 @@ public function __construct( * @param bool $excludeVendor Whether to exclude modules from the vendor/ directory * @return array Results with structure: ['modules' => [], 'summary' => [], * 'hasIncompatibilities' => bool] - * @phpstan-return array{modules: array, summary: array, hasIncompatibilities: bool} + * @phpstan-return CheckResults */ public function check( SymfonyStyle $io, @@ -86,7 +109,6 @@ public function check( $hasWarnings = $scanResult['totalIssues'] > $scanResult['criticalIssues']; $results['modules'][$moduleName] = [ - 'path' => $modulePath, 'compatible' => $isCompatible, 'hasWarnings' => $hasWarnings, 'scanResult' => $scanResult, @@ -140,7 +162,7 @@ private function isMagentoModule(string $moduleName): bool * Format results for display * * @param array $results - * @phpstan-param array $results + * @phpstan-param CheckResults $results * @param bool $showAll * @return array> */ @@ -168,7 +190,7 @@ public function formatResultsForDisplay(array $results, bool $showAll = false): * Get status display string with colors * * @param array $moduleData - * @phpstan-param array $moduleData + * @phpstan-param ModuleEntry $moduleData * @return string */ private function getStatusDisplay(array $moduleData): string @@ -192,7 +214,7 @@ private function getStatusDisplay(array $moduleData): string * Get issues display string * * @param array $moduleData - * @phpstan-param array $moduleData + * @phpstan-param ModuleEntry $moduleData * @return string */ private function getIssuesDisplay(array $moduleData): string @@ -223,21 +245,13 @@ private function getIssuesDisplay(array $moduleData): string * * @param string $moduleName * @param array $moduleData - * @phpstan-param array $moduleData - * @return array> + * @phpstan-param ModuleEntry $moduleData + * @return array}> */ public function getDetailedIssues(string $moduleName, array $moduleData): array { - // Safely access nested array structure - $scanResult = $moduleData['scanResult'] ?? []; - if (!is_array($scanResult)) { - return []; - } - - $files = $scanResult['files'] ?? []; - if (!is_array($files)) { - return []; - } + $scanResult = $moduleData['scanResult']; + $files = $scanResult['files']; $details = []; diff --git a/src/Service/Hyva/IncompatibilityDetector.php b/src/Service/Hyva/IncompatibilityDetector.php index 5edce53..c3be313 100644 --- a/src/Service/Hyva/IncompatibilityDetector.php +++ b/src/Service/Hyva/IncompatibilityDetector.php @@ -11,6 +11,8 @@ * * Uses pattern matching to identify RequireJS, Knockout.js, jQuery, and UI Components * usage that would be problematic in a Hyvä environment. + * + * @phpstan-type ScanIssue array{description: string, severity: string, line: int, pattern: string} */ class IncompatibilityDetector { @@ -107,6 +109,7 @@ public function __construct( * * @param string $filePath * @return array> Array of issues with keys: pattern, description, severity, line + * @phpstan-return array */ public function detectInFile(string $filePath): array { @@ -153,8 +156,9 @@ private function mapExtensionToType(string $extension): string * @param array $lines * @param array $patterns * @return array> + * @phpstan-return array * @phpstan-param array $lines - * @phpstan-param array> $patterns + * @phpstan-param array $patterns */ private function scanContentForPatterns(array $lines, array $patterns): array { diff --git a/src/Service/Hyva/ModuleScanner.php b/src/Service/Hyva/ModuleScanner.php index 2fb17bc..8b280da 100644 --- a/src/Service/Hyva/ModuleScanner.php +++ b/src/Service/Hyva/ModuleScanner.php @@ -11,6 +11,10 @@ * * Recursively scans JavaScript, XML, and PHTML files within module directories * to identify patterns that may be incompatible with Hyvä themes. + * + * @phpstan-import-type ScanIssue from IncompatibilityDetector + * @phpstan-type ScanResult array{files: array>, totalIssues: int, criticalIssues: int} + * @phpstan-type ModuleInfo array{name: string, version: string, isHyvaAware: bool} */ class ModuleScanner { @@ -32,6 +36,7 @@ public function __construct( * * @param string $modulePath * @return array Array with structure: ['files' => [], 'totalIssues' => int, 'criticalIssues' => int] + * @phpstan-return ScanResult */ public function scanModule(string $modulePath): array { @@ -120,14 +125,20 @@ private function isHyvaCompatibilityPackage(array $composerData): bool { // Check if this IS a Hyvä compatibility package $packageName = $composerData['name'] ?? ''; - if (str_starts_with($packageName, 'hyva-themes/') && str_contains($packageName, '-compat')) { + if (is_string($packageName) + && str_starts_with($packageName, 'hyva-themes/') + && str_contains($packageName, '-compat') + ) { return true; } // Check dependencies for Hyvä packages $requires = $composerData['require'] ?? []; + if (!is_array($requires)) { + return false; + } foreach ($requires as $package => $version) { - if (str_starts_with($package, 'hyva-themes/')) { + if (is_string($package) && str_starts_with($package, 'hyva-themes/')) { return true; } } @@ -157,6 +168,7 @@ public function hasHyvaCompatibilityPackage(string $modulePath): bool return false; } + /** @var array $composerData */ return $this->isHyvaCompatibilityPackage($composerData); } catch (\Exception $e) { return false; @@ -168,6 +180,7 @@ public function hasHyvaCompatibilityPackage(string $modulePath): bool * * @param string $modulePath * @return array + * @phpstan-return ModuleInfo */ public function getModuleInfo(string $modulePath): array { @@ -185,9 +198,10 @@ public function getModuleInfo(string $modulePath): array return ['name' => 'Unknown', 'version' => 'Unknown', 'isHyvaAware' => false]; } + /** @var array $composerData */ return [ - 'name' => $composerData['name'] ?? 'Unknown', - 'version' => $composerData['version'] ?? 'Unknown', + 'name' => is_string($composerData['name'] ?? null) ? $composerData['name'] : 'Unknown', + 'version' => is_string($composerData['version'] ?? null) ? $composerData['version'] : 'Unknown', 'isHyvaAware' => $this->isHyvaCompatibilityPackage($composerData), ]; } catch (\Exception $e) { diff --git a/src/Service/ThemeBuilder/HyvaThemes/Builder.php b/src/Service/ThemeBuilder/HyvaThemes/Builder.php index 4cb8ab0..c9043e4 100644 --- a/src/Service/ThemeBuilder/HyvaThemes/Builder.php +++ b/src/Service/ThemeBuilder/HyvaThemes/Builder.php @@ -68,7 +68,11 @@ public function detect(string $themePath): bool if ($this->fileDriver->isExists($themePath . '/composer.json')) { $composerContent = $this->fileDriver->fileGetContents($themePath . '/composer.json'); $composerJson = json_decode($composerContent, true); - if (isset($composerJson['name']) && str_contains($composerJson['name'], 'hyva')) { + if (is_array($composerJson) + && isset($composerJson['name']) + && is_string($composerJson['name']) + && str_contains($composerJson['name'], 'hyva') + ) { return true; } } diff --git a/src/Service/ThemeBuilder/TailwindCSS/Builder.php b/src/Service/ThemeBuilder/TailwindCSS/Builder.php index ed8789d..838006f 100644 --- a/src/Service/ThemeBuilder/TailwindCSS/Builder.php +++ b/src/Service/ThemeBuilder/TailwindCSS/Builder.php @@ -68,7 +68,11 @@ public function detect(string $themePath): bool if ($this->fileDriver->isExists($themePath . '/composer.json')) { $composerContent = $this->fileDriver->fileGetContents($themePath . '/composer.json'); $composerJson = json_decode($composerContent, true); - if (!isset($composerJson['name']) && str_contains($composerJson['name'], 'hyva')) { + if (\is_array($composerJson) + && isset($composerJson['name']) + && \is_string($composerJson['name']) + && !str_contains($composerJson['name'], 'hyva') + ) { return true; } }