diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index d9fadf20fae..a65deb91270 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -72,6 +72,7 @@ use function implode; use function in_array; use function is_a; +use function is_array; use function is_dir; use function is_file; use function is_string; @@ -595,6 +596,13 @@ class Config "xdebug" => false, ]; + /** + * A list of php extensions required by the project that aren't fully supported by Psalm. + * + * @var array + */ + public $php_extensions_not_supported = []; + /** * @var array */ @@ -995,12 +1003,24 @@ private static function fromXmlAndPaths( $composer_json = null; if (file_exists($composer_json_path)) { - if (!$composer_json = json_decode(file_get_contents($composer_json_path), true)) { + $composer_json = json_decode(file_get_contents($composer_json_path), true); + if (!is_array($composer_json)) { throw new UnexpectedValueException('Invalid composer.json at ' . $composer_json_path); } } - foreach ($config->php_extensions as $ext => $_) { - $config->php_extensions[$ext] = isset($composer_json["require"]["ext-$ext"]); + $required_extensions = []; + foreach (($composer_json["require"] ?? []) as $required => $_) { + if (strpos($required, "ext-") === 0) { + $required_extensions[strtolower(substr($required, 4))] = true; + } + } + foreach ($required_extensions as $required_ext => $_) { + if (isset($config->php_extensions[$required_ext])) { + /** @psalm-suppress PropertyTypeCoercion isset doesn't narrow $required_ext like it should */ + $config->php_extensions[$required_ext] = true; + } else { + $config->php_extensions_not_supported[$required_ext] = true; + } } if (isset($config_xml->enableExtensions) && isset($config_xml->enableExtensions->extension)) { diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index 8904b472c30..740735dc44a 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -59,6 +59,7 @@ use function array_combine; use function array_diff; use function array_fill_keys; +use function array_filter; use function array_keys; use function array_map; use function array_merge; @@ -590,7 +591,9 @@ private function generatePHPVersionMessage(): string break; } - return "Target PHP version: $version $source\n"; + return "Target PHP version: $version $source Extensions enabled: " + . implode(", ", array_keys(array_filter($codebase->config->php_extensions))) . " (unsupported extensions: " + . implode(", ", array_keys($codebase->config->php_extensions_not_supported)) . ")\n"; } public function check(string $base_dir, bool $is_diff = false): void diff --git a/tests/EndToEnd/PsalmEndToEndTest.php b/tests/EndToEnd/PsalmEndToEndTest.php index 59498a274df..e8e9ddb1ee5 100644 --- a/tests/EndToEnd/PsalmEndToEndTest.php +++ b/tests/EndToEnd/PsalmEndToEndTest.php @@ -20,6 +20,7 @@ use function readdir; use function rmdir; use function str_replace; +use function substr_count; use function sys_get_temp_dir; use function tempnam; use function unlink; @@ -168,7 +169,7 @@ public function testPsalmDiff(): void $this->assertStringContainsString('InvalidReturnType', $result['STDOUT']); $this->assertStringContainsString('InvalidReturnStatement', $result['STDOUT']); $this->assertStringContainsString('3 errors', $result['STDOUT']); - $this->assertStringNotContainsString('E', $result['STDERR']); + $this->assertEquals(1, substr_count($result['STDERR'], 'E')); // Should only have 'E' from 'Extensions' in version message $this->assertSame(2, $result['CODE']); diff --git a/tests/ProjectCheckerTest.php b/tests/ProjectCheckerTest.php index 622e6948e04..36b0c6c0ae7 100644 --- a/tests/ProjectCheckerTest.php +++ b/tests/ProjectCheckerTest.php @@ -40,6 +40,13 @@ class ProjectCheckerTest extends TestCase /** @var ProjectAnalyzer */ protected $project_analyzer; + private const EXPECTED_OUTPUT = "Target PHP version: 8.1 (set by tests) Extensions enabled: dom (unsupported " + . "extensions: simplexml, ctype, json, libxml, mbstring, tokenizer)\n" + . "Scanning files...\n" + . "Analyzing files...\n" + . "\n" + ; + public static function setUpBeforeClass(): void { self::$config = new TestConfig(); @@ -97,13 +104,7 @@ public function testCheck(): void $this->project_analyzer->check('tests/fixtures/DummyProject'); $output = ob_get_clean(); - $this->assertSame( - 'Target PHP version: 8.1 (set by tests)' . "\n" - . 'Scanning files...' . "\n" - . 'Analyzing files...' . "\n" - . "\n", - $output - ); + $this->assertSame(self::EXPECTED_OUTPUT, $output); $this->assertSame(0, IssueBuffer::getErrorCount()); @@ -289,13 +290,7 @@ public function testCheckDir(): void $this->project_analyzer->checkDir('tests/fixtures/DummyProject'); $output = ob_get_clean(); - $this->assertSame( - 'Target PHP version: 8.1 (set by tests)' . "\n" - . 'Scanning files...' . "\n" - . 'Analyzing files...' . "\n" - . "\n", - $output - ); + $this->assertSame(self::EXPECTED_OUTPUT, $output); $this->assertSame(0, IssueBuffer::getErrorCount()); @@ -334,13 +329,7 @@ public function testCheckPaths(): void ]); $output = ob_get_clean(); - $this->assertSame( - 'Target PHP version: 8.1 (set by tests)' . "\n" - . 'Scanning files...' . "\n" - . 'Analyzing files...' . "\n" - . "\n", - $output - ); + $this->assertSame(self::EXPECTED_OUTPUT, $output); $this->assertSame(0, IssueBuffer::getErrorCount()); @@ -379,13 +368,7 @@ public function testCheckFile(): void ]); $output = ob_get_clean(); - $this->assertSame( - 'Target PHP version: 8.1 (set by tests)' . "\n" - . 'Scanning files...' . "\n" - . 'Analyzing files...' . "\n" - . "\n", - $output - ); + $this->assertSame(self::EXPECTED_OUTPUT, $output); $this->assertSame(0, IssueBuffer::getErrorCount());