From 6ba8918a77c639730b3b21d1c4678f935c7b85c6 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 29 May 2026 09:24:23 +0200 Subject: [PATCH 01/10] Reproduce crash on invalid composer.json --- e2e/bug-14724/.gitignore | 1 + e2e/bug-14724/README | 1 + e2e/bug-14724/app/classes/path-1/A.php | 3 +++ e2e/bug-14724/app/classes/path-2/B.php | 5 +++++ e2e/bug-14724/composer.json | 10 ++++++++++ e2e/bug-14724/composer.lock | 18 ++++++++++++++++++ 6 files changed, 38 insertions(+) create mode 100644 e2e/bug-14724/.gitignore create mode 100644 e2e/bug-14724/README create mode 100644 e2e/bug-14724/app/classes/path-1/A.php create mode 100644 e2e/bug-14724/app/classes/path-2/B.php create mode 100644 e2e/bug-14724/composer.json create mode 100644 e2e/bug-14724/composer.lock diff --git a/e2e/bug-14724/.gitignore b/e2e/bug-14724/.gitignore new file mode 100644 index 00000000000..61ead86667c --- /dev/null +++ b/e2e/bug-14724/.gitignore @@ -0,0 +1 @@ +/vendor diff --git a/e2e/bug-14724/README b/e2e/bug-14724/README new file mode 100644 index 00000000000..68670114dc5 --- /dev/null +++ b/e2e/bug-14724/README @@ -0,0 +1 @@ +this test case intentionally ships with a invalid composer.json (according to schema), as this is what made PHPStan crash at startup. diff --git a/e2e/bug-14724/app/classes/path-1/A.php b/e2e/bug-14724/app/classes/path-1/A.php new file mode 100644 index 00000000000..7643c3311a9 --- /dev/null +++ b/e2e/bug-14724/app/classes/path-1/A.php @@ -0,0 +1,3 @@ + Date: Fri, 29 May 2026 09:26:21 +0200 Subject: [PATCH 02/10] Update e2e-tests.yml --- .github/workflows/e2e-tests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index be9b72ff74d..89f764c0da6 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -128,6 +128,10 @@ jobs: cd e2e/bug-14514 composer install ../../bin/phpstan analyze bug-14515.php + - script: | + cd e2e/bug-14724 + composer install + ../../bin/phpstan analyze app/ - script: | cd e2e/bug10449 ../../bin/phpstan analyze From 92775770e47db743a4d6fafe4587597ea94943c6 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 29 May 2026 09:30:15 +0200 Subject: [PATCH 03/10] fix --- .../ComposerJsonAndInstalledJsonSourceLocatorMaker.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php index 83e7d869928..e63b800ca6a 100644 --- a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php +++ b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php @@ -195,7 +195,14 @@ private function packageToPsr0AutoloadNamespaces(array $package, string $autoloa */ private function packageToClassMapPaths(array $package, string $autoloadSection = 'autoload'): array { - return $package[$autoloadSection]['classmap'] ?? []; + $classmap = $package[$autoloadSection]['classmap'] ?? []; + foreach($classmap as $k => $value) { + // skip invalid classmap + if (!is_string($value)) { + return []; + } + } + return $classmap; } /** From 1bd0b6f623dc298a12fac7de1880f66f0803cf41 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 29 May 2026 09:35:07 +0200 Subject: [PATCH 04/10] fix files --- e2e/bug-14724/app/file.php | 3 +++ e2e/bug-14724/composer.json | 5 +++++ ...omposerJsonAndInstalledJsonSourceLocatorMaker.php | 12 +++--------- 3 files changed, 11 insertions(+), 9 deletions(-) create mode 100644 e2e/bug-14724/app/file.php diff --git a/e2e/bug-14724/app/file.php b/e2e/bug-14724/app/file.php new file mode 100644 index 00000000000..6bb18cfc7cb --- /dev/null +++ b/e2e/bug-14724/app/file.php @@ -0,0 +1,3 @@ + $value) { - // skip invalid classmap - if (!is_string($value)) { - return []; - } - } - return $classmap; + return array_map(static fn ($classmapPath): string => (string) $classmapPath, $package[$autoloadSection]['classmap'] ?? []); } /** @@ -212,7 +206,7 @@ private function packageToClassMapPaths(array $package, string $autoloadSection */ private function packageToFilePaths(array $package, string $autoloadSection = 'autoload'): array { - return $package[$autoloadSection]['files'] ?? []; + return array_map(static fn ($filePath): string => (string) $filePath, $package[$autoloadSection]['files'] ?? []); } /** From f2f6ba7a776e138a9cede396d4277aeb8f8bf29c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 29 May 2026 09:36:37 +0200 Subject: [PATCH 05/10] cs --- .../ComposerJsonAndInstalledJsonSourceLocatorMaker.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php index 3ad8b56f889..09ca5df93f4 100644 --- a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php +++ b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php @@ -23,7 +23,6 @@ use function glob; use function is_dir; use function is_file; -use function is_string; use function str_contains; use const GLOB_ONLYDIR; From 1d271f55aba570874c2d16ddf470d4f881d78edb Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 29 May 2026 09:54:53 +0200 Subject: [PATCH 06/10] fix more cases --- ...JsonAndInstalledJsonSourceLocatorMaker.php | 54 +++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php index 09ca5df93f4..c9c6514492e 100644 --- a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php +++ b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php @@ -21,8 +21,10 @@ use function count; use function dirname; use function glob; +use function is_array; use function is_dir; use function is_file; +use function is_string; use function str_contains; use const GLOB_ONLYDIR; @@ -175,7 +177,23 @@ public function create(string $projectInstallationPath): ?SourceLocator */ private function packageToPsr4AutoloadNamespaces(array $package, string $autoloadSection = 'autoload'): array { - return array_map(static fn ($namespacePaths): array => (array) $namespacePaths, $package[$autoloadSection]['psr-4'] ?? []); + $psr4 = $package[$autoloadSection]['psr-4'] ?? []; + foreach ($psr4 as $key => $namespacePaths) { + if (!is_string($key)) { + return []; // skip on invalid schema + } + + if (is_array($namespacePaths)) { + foreach ($namespacePaths as $namespacePath) { + if (!is_string($namespacePath)) { + return []; // skip on invalid schema + } + } + } elseif (!is_string($namespacePaths)) { + return []; // skip on invalid schema + } + } + return $psr4; } /** @@ -185,7 +203,23 @@ private function packageToPsr4AutoloadNamespaces(array $package, string $autoloa */ private function packageToPsr0AutoloadNamespaces(array $package, string $autoloadSection = 'autoload'): array { - return array_map(static fn ($namespacePaths): array => (array) $namespacePaths, $package[$autoloadSection]['psr-0'] ?? []); + $psr0 = $package[$autoloadSection]['psr-0'] ?? []; + foreach ($psr0 as $key => $namespacePaths) { + if (!is_string($key)) { + return []; // skip on invalid schema + } + + if (is_array($namespacePaths)) { + foreach ($namespacePaths as $namespacePath) { + if (!is_string($namespacePath)) { + return []; // skip on invalid schema + } + } + } elseif (!is_string($namespacePaths)) { + return []; // skip on invalid schema + } + } + return $psr0; } /** @@ -195,7 +229,13 @@ private function packageToPsr0AutoloadNamespaces(array $package, string $autoloa */ private function packageToClassMapPaths(array $package, string $autoloadSection = 'autoload'): array { - return array_map(static fn ($classmapPath): string => (string) $classmapPath, $package[$autoloadSection]['classmap'] ?? []); + $classMap = $package[$autoloadSection]['classmap'] ?? []; + foreach ($classMap as $classmapPath) { + if (!is_string($classmapPath)) { + return []; // skip on invalid schema + } + } + return $classMap; } /** @@ -205,7 +245,13 @@ private function packageToClassMapPaths(array $package, string $autoloadSection */ private function packageToFilePaths(array $package, string $autoloadSection = 'autoload'): array { - return array_map(static fn ($filePath): string => (string) $filePath, $package[$autoloadSection]['files'] ?? []); + $filePaths = $package[$autoloadSection]['files'] ?? []; + foreach ($filePaths as $filePath) { + if (!is_string($filePath)) { + return []; // skip on invalid schema + } + } + return $filePaths; } /** From cca089a190a2d2d0039a7e54e18516b7e8106329 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 29 May 2026 10:01:08 +0200 Subject: [PATCH 07/10] Update ComposerJsonAndInstalledJsonSourceLocatorMaker.php --- ...omposerJsonAndInstalledJsonSourceLocatorMaker.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php index c9c6514492e..c0728c51c7e 100644 --- a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php +++ b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php @@ -178,6 +178,9 @@ public function create(string $projectInstallationPath): ?SourceLocator private function packageToPsr4AutoloadNamespaces(array $package, string $autoloadSection = 'autoload'): array { $psr4 = $package[$autoloadSection]['psr-4'] ?? []; + if (!is_array($psr4)) { + return []; // skip on invalid schema + } foreach ($psr4 as $key => $namespacePaths) { if (!is_string($key)) { return []; // skip on invalid schema @@ -204,6 +207,9 @@ private function packageToPsr4AutoloadNamespaces(array $package, string $autoloa private function packageToPsr0AutoloadNamespaces(array $package, string $autoloadSection = 'autoload'): array { $psr0 = $package[$autoloadSection]['psr-0'] ?? []; + if (!is_array($psr0)) { + return []; // skip on invalid schema + } foreach ($psr0 as $key => $namespacePaths) { if (!is_string($key)) { return []; // skip on invalid schema @@ -230,6 +236,9 @@ private function packageToPsr0AutoloadNamespaces(array $package, string $autoloa private function packageToClassMapPaths(array $package, string $autoloadSection = 'autoload'): array { $classMap = $package[$autoloadSection]['classmap'] ?? []; + if (!is_array($classMap)) { + return []; // skip on invalid schema + } foreach ($classMap as $classmapPath) { if (!is_string($classmapPath)) { return []; // skip on invalid schema @@ -246,6 +255,9 @@ private function packageToClassMapPaths(array $package, string $autoloadSection private function packageToFilePaths(array $package, string $autoloadSection = 'autoload'): array { $filePaths = $package[$autoloadSection]['files'] ?? []; + if (!is_array($filePaths)) { + return []; // skip on invalid schema + } foreach ($filePaths as $filePath) { if (!is_string($filePath)) { return []; // skip on invalid schema From fe254e360b4d89afe184a53765c9e97d918ed033 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 29 May 2026 10:15:20 +0200 Subject: [PATCH 08/10] Update ComposerJsonAndInstalledJsonSourceLocatorMaker.php --- .../ComposerJsonAndInstalledJsonSourceLocatorMaker.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php index c0728c51c7e..742cef43255 100644 --- a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php +++ b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php @@ -195,6 +195,8 @@ private function packageToPsr4AutoloadNamespaces(array $package, string $autoloa } elseif (!is_string($namespacePaths)) { return []; // skip on invalid schema } + + $psr4[$key] = (array) $namespacePaths; } return $psr4; } @@ -224,6 +226,8 @@ private function packageToPsr0AutoloadNamespaces(array $package, string $autoloa } elseif (!is_string($namespacePaths)) { return []; // skip on invalid schema } + + $psr0[$key] = (array) $namespacePaths; } return $psr0; } From 1e5a2a1aa0dbe8c81017b749da6376a3d2c91524 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 29 May 2026 10:24:16 +0200 Subject: [PATCH 09/10] simplify --- ...JsonAndInstalledJsonSourceLocatorMaker.php | 70 +++++++------------ 1 file changed, 26 insertions(+), 44 deletions(-) diff --git a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php index 742cef43255..27dec9d6a82 100644 --- a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php +++ b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php @@ -182,21 +182,12 @@ private function packageToPsr4AutoloadNamespaces(array $package, string $autoloa return []; // skip on invalid schema } foreach ($psr4 as $key => $namespacePaths) { - if (!is_string($key)) { + $stringArray = $this->toStringArray($namespacePaths); + if (!is_string($key) || $stringArray === null) { return []; // skip on invalid schema } - if (is_array($namespacePaths)) { - foreach ($namespacePaths as $namespacePath) { - if (!is_string($namespacePath)) { - return []; // skip on invalid schema - } - } - } elseif (!is_string($namespacePaths)) { - return []; // skip on invalid schema - } - - $psr4[$key] = (array) $namespacePaths; + $psr4[$key] = $stringArray; } return $psr4; } @@ -213,21 +204,12 @@ private function packageToPsr0AutoloadNamespaces(array $package, string $autoloa return []; // skip on invalid schema } foreach ($psr0 as $key => $namespacePaths) { - if (!is_string($key)) { + $stringArray = $this->toStringArray($namespacePaths); + if (!is_string($key) || $stringArray === null) { return []; // skip on invalid schema } - if (is_array($namespacePaths)) { - foreach ($namespacePaths as $namespacePath) { - if (!is_string($namespacePath)) { - return []; // skip on invalid schema - } - } - } elseif (!is_string($namespacePaths)) { - return []; // skip on invalid schema - } - - $psr0[$key] = (array) $namespacePaths; + $psr0[$key] = $stringArray; } return $psr0; } @@ -239,16 +221,7 @@ private function packageToPsr0AutoloadNamespaces(array $package, string $autoloa */ private function packageToClassMapPaths(array $package, string $autoloadSection = 'autoload'): array { - $classMap = $package[$autoloadSection]['classmap'] ?? []; - if (!is_array($classMap)) { - return []; // skip on invalid schema - } - foreach ($classMap as $classmapPath) { - if (!is_string($classmapPath)) { - return []; // skip on invalid schema - } - } - return $classMap; + return $this->toStringArray($package[$autoloadSection]['classmap'] ?? []) ?? []; } /** @@ -258,16 +231,7 @@ private function packageToClassMapPaths(array $package, string $autoloadSection */ private function packageToFilePaths(array $package, string $autoloadSection = 'autoload'): array { - $filePaths = $package[$autoloadSection]['files'] ?? []; - if (!is_array($filePaths)) { - return []; // skip on invalid schema - } - foreach ($filePaths as $filePath) { - if (!is_string($filePath)) { - return []; // skip on invalid schema - } - } - return $filePaths; + return $this->toStringArray($package[$autoloadSection]['files'] ?? []) ?? []; } /** @@ -319,4 +283,22 @@ private function prefixPaths(array $paths, string $prefix): array return array_map(static fn (string $path): string => $prefix . $path, $paths); } + /** + * @param array|string $stringOrArray + * @return array|null + */ + private function toStringArray(array|string $stringOrArray): ?array + { + if (is_string($stringOrArray)) { + return (array) $stringOrArray; + } + + foreach ($stringOrArray as $stringOrArrayItem) { + if (!is_string($stringOrArrayItem)) { + return null; + } + } + return $stringOrArray; + } + } From 7ccafa649d2d41701b663ff8f2ba75edec802548 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 29 May 2026 10:25:17 +0200 Subject: [PATCH 10/10] docfix --- .../ComposerJsonAndInstalledJsonSourceLocatorMaker.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php index 27dec9d6a82..58637b2b11f 100644 --- a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php +++ b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php @@ -179,12 +179,12 @@ private function packageToPsr4AutoloadNamespaces(array $package, string $autoloa { $psr4 = $package[$autoloadSection]['psr-4'] ?? []; if (!is_array($psr4)) { - return []; // skip on invalid schema + return []; // skip on invalid data } foreach ($psr4 as $key => $namespacePaths) { $stringArray = $this->toStringArray($namespacePaths); if (!is_string($key) || $stringArray === null) { - return []; // skip on invalid schema + return []; // skip on invalid data } $psr4[$key] = $stringArray; @@ -201,12 +201,12 @@ private function packageToPsr0AutoloadNamespaces(array $package, string $autoloa { $psr0 = $package[$autoloadSection]['psr-0'] ?? []; if (!is_array($psr0)) { - return []; // skip on invalid schema + return []; // skip on invalid data } foreach ($psr0 as $key => $namespacePaths) { $stringArray = $this->toStringArray($namespacePaths); if (!is_string($key) || $stringArray === null) { - return []; // skip on invalid schema + return []; // skip on invalid data } $psr0[$key] = $stringArray;