From 2fe4fc397cd7752b20062b3eb3b105f3488fcbe3 Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Wed, 8 Dec 2021 17:47:32 -0600 Subject: [PATCH 01/10] Enable extensions based on composer.json instead of those loaded at runtime (fixes #5482). --- .github/workflows/ci.yml | 2 +- dictionaries/InternalTaintSinkMap.php | 3 - src/Psalm/Config.php | 94 +++++++---- .../PdoStatementReturnTypeProvider.php | 23 ++- .../PdoStatementSetFetchMode.php | 7 +- stubs/{ => extensions}/decimal.phpstub | 0 stubs/{DOM.phpstub => extensions/dom.phpstub} | 0 .../{ext-ds.phpstub => extensions/ds.phpstub} | 0 .../geos.phpstub} | 0 stubs/extensions/gmp.phpstub | 11 ++ stubs/{ => extensions}/mongodb.phpstub | 0 stubs/{ => extensions}/mysqli.phpstub | 0 stubs/extensions/pdo.phpstub | 153 ++++++++++++++++++ stubs/{ => extensions}/soap.phpstub | 0 .../xdebug.phpstub} | 0 stubs/pdo.phpstub | 19 --- tests/BinaryOperationTest.php | 10 -- tests/ClassTest.php | 6 - tests/MethodCallTest.php | 6 - tests/MethodSignatureTest.php | 21 --- tests/TestConfig.php | 4 + 21 files changed, 242 insertions(+), 117 deletions(-) rename stubs/{ => extensions}/decimal.phpstub (100%) rename stubs/{DOM.phpstub => extensions/dom.phpstub} (100%) rename stubs/{ext-ds.phpstub => extensions/ds.phpstub} (100%) rename stubs/{ext-geos.phpstub => extensions/geos.phpstub} (100%) create mode 100644 stubs/extensions/gmp.phpstub rename stubs/{ => extensions}/mongodb.phpstub (100%) rename stubs/{ => extensions}/mysqli.phpstub (100%) create mode 100644 stubs/extensions/pdo.phpstub rename stubs/{ => extensions}/soap.phpstub (100%) rename stubs/{Xdebug.phpstub => extensions/xdebug.phpstub} (100%) delete mode 100644 stubs/pdo.phpstub diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9b7d2edcfc..07270f593a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,7 +81,7 @@ jobs: php-version: '8.0' tools: composer:v2 coverage: none - extensions: decimal + extensions: :pdo - uses: actions/checkout@v2 diff --git a/dictionaries/InternalTaintSinkMap.php b/dictionaries/InternalTaintSinkMap.php index 3838720a2fb..ac244bead48 100644 --- a/dictionaries/InternalTaintSinkMap.php +++ b/dictionaries/InternalTaintSinkMap.php @@ -43,9 +43,6 @@ 'mysqli_stmt::prepare' => [['sql']], 'passthru' => [['shell']], 'pcntl_exec' => [['shell']], -'PDO::prepare' => [['sql']], -'PDO::query' => [['sql']], -'PDO::exec' => [['sql']], 'pg_exec' => [[], ['sql']], 'pg_prepare' => [[], [], ['sql']], 'pg_put_line' => [[], ['sql']], diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index d99f30d3db3..831863db2f0 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -58,7 +58,6 @@ use function count; use function dirname; use function explode; -use function extension_loaded; use function file_exists; use function file_get_contents; use function filetype; @@ -574,6 +573,34 @@ class Config /** @var ?int */ public $threads; + /** + * @psalm-readonly-allow-private-mutation + * @var array{ + * decimal: bool, + * dom: bool, + * ds: bool, + * geos: bool, + * gmp: bool, + * mongodb: bool, + * mysqli: bool, + * pdo: bool, + * soap: bool, + * xdebug: bool, + * } + */ + public $php_extensions = [ + "decimal" => false, + "dom" => false, + "ds" => false, + "geos" => false, + "gmp" => false, + "mongodb" => false, + "mysqli" => false, + "pdo" => false, + "soap" => false, + "xdebug" => false, + ]; + protected function __construct() { self::$instance = $this; @@ -964,6 +991,32 @@ private static function fromXmlAndPaths( $base_dir = $current_dir; } + $composer_json_path = Composer::getJsonFilePath($config->base_dir); + + $composer_json = null; + if (file_exists($composer_json_path)) { + if (!$composer_json = json_decode(file_get_contents($composer_json_path), true)) { + throw new UnexpectedValueException('Invalid composer.json at ' . $composer_json_path); + } + } + foreach ([ + "decimal", + "dom", + "ds", + "geos", + "mongodb", + "mysqli", + "pdo", + "soap", + "xdebug", + ] as $ext) { + $config->php_extensions[$ext] = isset($composer_json["require"]["ext-$ext"]); + } + + if ($config->load_xdebug_stub !== null) { + $config->php_extensions[$ext] = $config->load_xdebug_stub; + } + if (isset($config_xml['phpVersion'])) { $config->configured_php_version = (string) $config_xml['phpVersion']; } @@ -1967,7 +2020,6 @@ public function visitStubFiles(Codebase $codebase, ?Progress $progress = null): $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'CoreGenericClasses.phpstub', $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'CoreGenericIterators.phpstub', $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'CoreImmutableClasses.phpstub', - $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'DOM.phpstub', $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'Reflection.phpstub', $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'SPL.phpstub', ]; @@ -1982,39 +2034,11 @@ public function visitStubFiles(Codebase $codebase, ?Progress $progress = null): $this->internal_stubs[] = $stringable_path; } - if (extension_loaded('PDO')) { - $ext_pdo_path = $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'pdo.phpstub'; - $this->internal_stubs[] = $ext_pdo_path; - } - - if (extension_loaded('soap')) { - $ext_soap_path = $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'soap.phpstub'; - $this->internal_stubs[] = $ext_soap_path; - } - - if (extension_loaded('ds')) { - $ext_ds_path = $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'ext-ds.phpstub'; - $this->internal_stubs[] = $ext_ds_path; - } - - if (extension_loaded('mongodb')) { - $ext_mongodb_path = $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'mongodb.phpstub'; - $this->internal_stubs[] = $ext_mongodb_path; - } - - if ($this->load_xdebug_stub) { - $xdebug_stub_path = $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'Xdebug.phpstub'; - $this->internal_stubs[] = $xdebug_stub_path; - } - - if (extension_loaded('mysqli')) { - $ext_mysqli_path = $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'mysqli.phpstub'; - $this->internal_stubs[] = $ext_mysqli_path; - } - - if (extension_loaded('decimal')) { - $ext_decimal_path = $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'decimal.phpstub'; - $this->internal_stubs[] = $ext_decimal_path; + foreach ($this->php_extensions as $ext => $enabled) { + if ($enabled) { + $this->internal_stubs[] = $dir_lvl_2 . DIRECTORY_SEPARATOR . "stubs" + . DIRECTORY_SEPARATOR . "extensions" . DIRECTORY_SEPARATOR . "$ext.phpstub"; + } } foreach ($this->internal_stubs as $stub_path) { diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php index 1d2521ea003..6ef85193afc 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php @@ -2,7 +2,7 @@ namespace Psalm\Internal\Provider\ReturnTypeProvider; -use PDO; +use Psalm\Config; use Psalm\Plugin\EventHandler\Event\MethodReturnTypeProviderEvent; use Psalm\Plugin\EventHandler\MethodReturnTypeProviderInterface; use Psalm\Type; @@ -15,8 +15,6 @@ use Psalm\Type\Atomic\TScalar; use Psalm\Type\Union; -use function class_exists; - /** * @internal */ @@ -29,11 +27,12 @@ public static function getClassLikeNames(): array public static function getMethodReturnType(MethodReturnTypeProviderEvent $event): ?Union { + $config = Config::getInstance(); $source = $event->getSource(); $call_args = $event->getCallArgs(); $method_name_lowercase = $event->getMethodNameLowercase(); if ($method_name_lowercase === 'fetch' - && class_exists('PDO') + && $config->php_extensions["pdo"] && isset($call_args[0]) && ($first_arg_type = $source->getNodeTypeProvider()->getType($call_args[0]->value)) && $first_arg_type->isSingleIntLiteral() @@ -41,7 +40,7 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) $fetch_mode = $first_arg_type->getSingleIntLiteral()->value; switch ($fetch_mode) { - case PDO::FETCH_ASSOC: // array|false + case 2: // PDO::FETCH_ASSOC - array|false return new Union([ new TArray([ Type::getString(), @@ -53,7 +52,7 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) new TFalse(), ]); - case PDO::FETCH_BOTH: // array|false + case 4: // PDO::FETCH_BOTH - array|false return new Union([ new TArray([ Type::getArrayKey(), @@ -65,16 +64,16 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) new TFalse(), ]); - case PDO::FETCH_BOUND: // bool + case 6: // PDO::FETCH_BOUND - bool return Type::getBool(); - case PDO::FETCH_CLASS: // object|false + case 8: // PDO::FETCH_CLASS - object|false return new Union([ new TObject(), new TFalse(), ]); - case PDO::FETCH_LAZY: // object|false + case 1: // PDO::FETCH_LAZY - object|false // This actually returns a PDORow object, but that class is // undocumented, and its attributes are all dynamic anyway return new Union([ @@ -82,7 +81,7 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) new TFalse(), ]); - case PDO::FETCH_NAMED: // array>|false + case 11: // PDO::FETCH_NAMED - array>|false return new Union([ new TArray([ Type::getString(), @@ -94,7 +93,7 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) new TFalse(), ]); - case PDO::FETCH_NUM: // list|false + case 3: // PDO::FETCH_NUM - list|false return new Union([ new TList( new Union([ @@ -105,7 +104,7 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) new TFalse(), ]); - case PDO::FETCH_OBJ: // stdClass|false + case 5: // PDO::FETCH_OBJ - stdClass|false return new Union([ new TNamedObject('stdClass'), new TFalse(), diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementSetFetchMode.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementSetFetchMode.php index cd88b142770..c8eba436197 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementSetFetchMode.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementSetFetchMode.php @@ -2,7 +2,6 @@ namespace Psalm\Internal\Provider\ReturnTypeProvider; -use PDO; use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Plugin\EventHandler\Event\MethodParamsProviderEvent; @@ -62,7 +61,7 @@ public static function getMethodParams(MethodParamsProviderEvent $event): ?array $value = $first_call_arg_type->getSingleIntLiteral()->value; switch ($value) { - case PDO::FETCH_COLUMN: + case 7: // PDO::FETCH_COLUMN $params[] = new FunctionLikeParameter( 'colno', false, @@ -73,7 +72,7 @@ public static function getMethodParams(MethodParamsProviderEvent $event): ?array ); break; - case PDO::FETCH_CLASS: + case 8: // PDO::FETCH_CLASS $params[] = new FunctionLikeParameter( 'classname', false, @@ -93,7 +92,7 @@ public static function getMethodParams(MethodParamsProviderEvent $event): ?array ); break; - case PDO::FETCH_INTO: + case 9: // PDO::FETCH_INTO $params[] = new FunctionLikeParameter( 'object', false, diff --git a/stubs/decimal.phpstub b/stubs/extensions/decimal.phpstub similarity index 100% rename from stubs/decimal.phpstub rename to stubs/extensions/decimal.phpstub diff --git a/stubs/DOM.phpstub b/stubs/extensions/dom.phpstub similarity index 100% rename from stubs/DOM.phpstub rename to stubs/extensions/dom.phpstub diff --git a/stubs/ext-ds.phpstub b/stubs/extensions/ds.phpstub similarity index 100% rename from stubs/ext-ds.phpstub rename to stubs/extensions/ds.phpstub diff --git a/stubs/ext-geos.phpstub b/stubs/extensions/geos.phpstub similarity index 100% rename from stubs/ext-geos.phpstub rename to stubs/extensions/geos.phpstub diff --git a/stubs/extensions/gmp.phpstub b/stubs/extensions/gmp.phpstub new file mode 100644 index 00000000000..6a621bc0d66 --- /dev/null +++ b/stubs/extensions/gmp.phpstub @@ -0,0 +1,11 @@ + + */ +class PDOStatement implements Traversable +{ + /** + * @psalm-taint-sink callable $class + * + * @template T of object + * @param class-string $class + * @param array $ctorArgs + * @return false|T + */ + public function fetchObject($class = \stdclass::class, array $ctorArgs = array()) {} +} diff --git a/stubs/soap.phpstub b/stubs/extensions/soap.phpstub similarity index 100% rename from stubs/soap.phpstub rename to stubs/extensions/soap.phpstub diff --git a/stubs/Xdebug.phpstub b/stubs/extensions/xdebug.phpstub similarity index 100% rename from stubs/Xdebug.phpstub rename to stubs/extensions/xdebug.phpstub diff --git a/stubs/pdo.phpstub b/stubs/pdo.phpstub deleted file mode 100644 index 0abf5053353..00000000000 --- a/stubs/pdo.phpstub +++ /dev/null @@ -1,19 +0,0 @@ - - */ -class PDOStatement implements Traversable -{ - /** - * @psalm-taint-sink callable $class - * - * @template T of object - * @param class-string $class - * @param array $ctorArgs - * @return false|T - */ - public function fetchObject($class = \stdclass::class, array $ctorArgs = array()) {} -} diff --git a/tests/BinaryOperationTest.php b/tests/BinaryOperationTest.php index 2f2e37f197d..9b268caf1e6 100644 --- a/tests/BinaryOperationTest.php +++ b/tests/BinaryOperationTest.php @@ -8,8 +8,6 @@ use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; -use function class_exists; - use const DIRECTORY_SEPARATOR; class BinaryOperationTest extends TestCase @@ -19,10 +17,6 @@ class BinaryOperationTest extends TestCase public function testGMPOperations(): void { - if (class_exists('GMP') === false) { - $this->markTestSkipped('Cannot run test, base class "GMP" does not exist!'); - } - $this->addFile( 'somefile.php', 'markTestSkipped('Cannot run test, base class "Decimal\\Decimal" does not exist!'); - } - $this->addFile( 'somefile.php', 'markTestSkipped('Cannot run test, base class "mysqli" does not exist!'); - } - $this->addFile( 'somefile.php', 'markTestSkipped('Cannot run test, base class "SoapClient" does not exist!'); - } - $this->addFile( 'somefile.php', 'markTestSkipped('Cannot run test, base class "SoapClient" does not exist!'); - } - $this->addFile( 'somefile.php', 'markTestSkipped('Cannot run test, base class "SoapClient" does not exist!'); - } - $this->addFile( 'somefile.php', 'markTestSkipped('Cannot run test, base class "SoapClient" does not exist!'); - } - $this->addFile( 'somefile.php', 'expectExceptionMessage('ImplementedParamTypeMismatch'); $this->expectException(CodeException::class); - if (class_exists('SoapClient') === false) { - $this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!'); - } $this->addFile( 'somefile.php', @@ -286,10 +269,6 @@ public function testExtendDocblockParamTypeWithWrongParam(): void $this->expectException(CodeException::class); $this->expectExceptionMessage('MethodSignatureMismatch'); - if (class_exists('SoapClient') === false) { - $this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!'); - } - $this->addFile( 'somefile.php', 'php_extensions as $ext => $_enabled) { + $this->php_extensions[$ext] = true; + } + $this->throw_exception = true; $this->use_docblock_types = true; $this->level = 1; From 63cd3f3b9548ab4264edb0d47fff95bdc857a2c3 Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Thu, 9 Dec 2021 14:33:24 -0600 Subject: [PATCH 02/10] Add SoapFault stub. --- stubs/extensions/soap.phpstub | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/stubs/extensions/soap.phpstub b/stubs/extensions/soap.phpstub index 0eb107823ef..f1c59ca052e 100644 --- a/stubs/extensions/soap.phpstub +++ b/stubs/extensions/soap.phpstub @@ -282,3 +282,17 @@ class SoapClient { public function __setSoapHeaders ($soapheaders = null) {} } + +class SoapFault extends Exception { + /** + * @param array|string|null $code + */ + public function __construct( + $code, + string $string, + ?string $actor = null, + mixed $details = null, + ?string $name = null, + mixed $headerFault = null + ) {} +} From 4672e5a3242a6b9591647ba824b6cea9b0f80135 Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Thu, 9 Dec 2021 14:33:31 -0600 Subject: [PATCH 03/10] Disable all PHP extensions for CI except those required by psalm. --- .github/workflows/ci.yml | 2 +- .github/workflows/windows-ci.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 07270f593a1..8a59f4b4416 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,7 +81,7 @@ jobs: php-version: '8.0' tools: composer:v2 coverage: none - extensions: :pdo + extensions: none, curl, dom, filter, json, libxml, mbstring, openssl, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter - uses: actions/checkout@v2 diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index dc5fd77c560..828b74ccb89 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -51,6 +51,7 @@ jobs: php-version: '8.0' tools: composer:v2 coverage: none + extensions: none, curl, dom, filter, json, libxml, mbstring, openssl, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter - uses: actions/checkout@v2 From 02cd7bbbbafe1b5d56b92729ddb398141f6a5bde Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Thu, 9 Dec 2021 15:22:05 -0600 Subject: [PATCH 04/10] Add SoapHeader stub. --- stubs/extensions/soap.phpstub | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/stubs/extensions/soap.phpstub b/stubs/extensions/soap.phpstub index f1c59ca052e..8a3fafa4dcd 100644 --- a/stubs/extensions/soap.phpstub +++ b/stubs/extensions/soap.phpstub @@ -296,3 +296,16 @@ class SoapFault extends Exception { mixed $headerFault = null ) {} } + +class SoapHeader { + public function __construct( + string $namespace, + string $name, + // Actually doesn't have a default, not specifying results in no SoapHeader::$data property. Specifying null + // results in a SoapHeader::$data property with null as the value. This probably makes no difference. + mixed $data = null, + bool $mustUnderstand = false, + // Same as $data, no default. The documentation specifies this as a `string` but it accepts null. + ?string $actor = null + ) {} +} From 8132b578812c94034fcc960c13e30e3bade33aea Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Thu, 9 Dec 2021 16:47:01 -0600 Subject: [PATCH 05/10] Add XML config options to enable/disable PHP extensions. --- config.xsd | 27 +++++++++++++++ src/Psalm/Config.php | 26 +++++++++++++++ tests/Config/ConfigFileTest.php | 59 +++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) diff --git a/config.xsd b/config.xsd index 1a29b16b27c..a909e644933 100644 --- a/config.xsd +++ b/config.xsd @@ -21,6 +21,8 @@ + + @@ -682,4 +684,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 831863db2f0..e8ae7b0c20c 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -1017,6 +1017,32 @@ private static function fromXmlAndPaths( $config->php_extensions[$ext] = $config->load_xdebug_stub; } + if (isset($config_xml->enableExtensions) && isset($config_xml->enableExtensions->extension)) { + foreach ($config_xml->enableExtensions->extension as $extension) { + assert(isset($extension["name"])); + $extensionName = (string) $extension["name"]; + assert(in_array( + $extensionName, + ["decimal", "dom", "ds", "geos", "mongodb", "mysqli", "pdo", "soap", "xdebug"], + true + )); + $config->php_extensions[$extensionName] = true; + } + } + + if (isset($config_xml->disableExtensions) && isset($config_xml->disableExtensions->extension)) { + foreach ($config_xml->disableExtensions->extension as $extension) { + assert(isset($extension["name"])); + $extensionName = (string) $extension["name"]; + assert(in_array( + $extensionName, + ["decimal", "dom", "ds", "geos", "mongodb", "mysqli", "pdo", "soap", "xdebug"], + true + )); + $config->php_extensions[$extensionName] = false; + } + } + if (isset($config_xml['phpVersion'])) { $config->configured_php_version = (string) $config_xml['phpVersion']; } diff --git a/tests/Config/ConfigFileTest.php b/tests/Config/ConfigFileTest.php index a6b5f4cd503..36bf38eb781 100644 --- a/tests/Config/ConfigFileTest.php +++ b/tests/Config/ConfigFileTest.php @@ -16,6 +16,7 @@ use function unlink; use const PHP_EOL; +use Psalm\Exception\ConfigException; /** @group PluginManager */ class ConfigFileTest extends TestCase @@ -206,6 +207,64 @@ public function removeKillsSpecifiedPluginWithOneRemaining(): void ); } + public function testEnableExtensions(): void + { + file_put_contents($this->file_path, trim(' + + + + + + + + ')); + + $config_file = new ConfigFile((string)getcwd(), $this->file_path); + $config = $config_file->getConfig(); + + $this->assertTrue($config->php_extensions["mysqli"]); + $this->assertTrue($config->php_extensions["pdo"]); + } + + public function testDisableExtensions(): void + { + file_put_contents($this->file_path, trim(' + + + + + + + + + + + + ')); + + $config_file = new ConfigFile((string)getcwd(), $this->file_path); + $config = $config_file->getConfig(); + + $this->assertFalse($config->php_extensions["mysqli"]); + $this->assertFalse($config->php_extensions["pdo"]); + } + + public function testInvalidExtension(): void + { + $this->expectException(ConfigException::class); + + file_put_contents($this->file_path, trim(' + + + + + + + ')); + + (new ConfigFile((string)getcwd(), $this->file_path))->getConfig(); + } + /** * @param string $expected_template * @param string $contents From 5cd3b68ab358dda3bc3060b2240f1a8bf2acee02 Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Thu, 9 Dec 2021 17:01:40 -0600 Subject: [PATCH 06/10] Add documentation for enableExtensions and disableExtensions. --- docs/running_psalm/configuration.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/running_psalm/configuration.md b/docs/running_psalm/configuration.md index 26861e4ada4..dae932211a6 100644 --- a/docs/running_psalm/configuration.md +++ b/docs/running_psalm/configuration.md @@ -252,6 +252,7 @@ When `true`, Psalm will report all `@psalm-suppress` annotations that aren't use loadXdebugStub="[bool]" > ``` +Deprecated, use <enableExtensions> instead. If not present, Psalm will only load the Xdebug stub if Psalm has unloaded the extension. When `true`, Psalm will load the Xdebug extension stub (as the extension is unloaded when Psalm runs). Setting to `false` prevents the stub from loading. @@ -336,7 +337,7 @@ When `false`, Psalm will not consider issue at lower level than `errorLevel` as #### allowNamedArgumentCalls ```xml - ``` @@ -432,6 +433,23 @@ Optional. Same format as ``. Directories Psalm should load but not #### <fileExtensions> Optional. A list of extensions to search over. See [Checking non-PHP files](checking_non_php_files.md) to understand how to extend this. +#### <enableExtensions> +Optional. A list of extensions to enable. By default, only extensions required by your composer.json will be enabled. +```xml + + + + +``` + +#### <disableExtensions> +Optional. A list of extensions to disable. By default, only extensions required by your composer.json will be enabled. +```xml + + + +``` + #### <plugins> Optional. A list of `` entries. See the [Plugins](plugins/using_plugins.md) section for more information. @@ -483,7 +501,7 @@ The following configuration declares custom types for super-globals (`$GLOBALS` ```xml - + ``` From c2b44ff07310f4972a776e17f49e34c0d9b2ca02 Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Thu, 9 Dec 2021 17:13:52 -0600 Subject: [PATCH 07/10] Fix copy/paste fail and other psalm/cs issues, reduce duplication of literals. --- src/Psalm/Config.php | 27 +++++---------------------- tests/Config/ConfigFileTest.php | 2 +- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index e8ae7b0c20c..06c89692b43 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -46,6 +46,7 @@ use XdgBaseDir\Xdg; use stdClass; +use function array_key_exists; use function array_map; use function array_merge; use function array_pad; @@ -999,33 +1000,19 @@ private static function fromXmlAndPaths( throw new UnexpectedValueException('Invalid composer.json at ' . $composer_json_path); } } - foreach ([ - "decimal", - "dom", - "ds", - "geos", - "mongodb", - "mysqli", - "pdo", - "soap", - "xdebug", - ] as $ext) { + foreach ($config->php_extensions as $ext => $_) { $config->php_extensions[$ext] = isset($composer_json["require"]["ext-$ext"]); } if ($config->load_xdebug_stub !== null) { - $config->php_extensions[$ext] = $config->load_xdebug_stub; + $config->php_extensions["xdebug"] = $config->load_xdebug_stub; } if (isset($config_xml->enableExtensions) && isset($config_xml->enableExtensions->extension)) { foreach ($config_xml->enableExtensions->extension as $extension) { assert(isset($extension["name"])); $extensionName = (string) $extension["name"]; - assert(in_array( - $extensionName, - ["decimal", "dom", "ds", "geos", "mongodb", "mysqli", "pdo", "soap", "xdebug"], - true - )); + assert(array_key_exists($extensionName, $config->php_extensions)); $config->php_extensions[$extensionName] = true; } } @@ -1034,11 +1021,7 @@ private static function fromXmlAndPaths( foreach ($config_xml->disableExtensions->extension as $extension) { assert(isset($extension["name"])); $extensionName = (string) $extension["name"]; - assert(in_array( - $extensionName, - ["decimal", "dom", "ds", "geos", "mongodb", "mysqli", "pdo", "soap", "xdebug"], - true - )); + assert(array_key_exists($extensionName, $config->php_extensions)); $config->php_extensions[$extensionName] = false; } } diff --git a/tests/Config/ConfigFileTest.php b/tests/Config/ConfigFileTest.php index 36bf38eb781..bf19f93699f 100644 --- a/tests/Config/ConfigFileTest.php +++ b/tests/Config/ConfigFileTest.php @@ -3,6 +3,7 @@ namespace Psalm\Tests\Config; use Psalm\Config; +use Psalm\Exception\ConfigException; use Psalm\Internal\PluginManager\ConfigFile; use Psalm\Internal\RuntimeCaches; use Psalm\Tests\TestCase; @@ -16,7 +17,6 @@ use function unlink; use const PHP_EOL; -use Psalm\Exception\ConfigException; /** @group PluginManager */ class ConfigFileTest extends TestCase From 5c01913456ebdbd54cd4f5e6a4851d90bcbd3f24 Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Sat, 22 Jan 2022 14:40:16 -0600 Subject: [PATCH 08/10] Remove deprecated loadXdebugStub attribute. --- config.xsd | 12 ------------ docs/running_psalm/configuration.md | 11 ----------- psalm-baseline.xml | 14 ++------------ src/Psalm/Config.php | 14 -------------- src/Psalm/Internal/Cli/Psalm.php | 8 ++------ 5 files changed, 4 insertions(+), 55 deletions(-) diff --git a/config.xsd b/config.xsd index a909e644933..a4f4b5042f5 100644 --- a/config.xsd +++ b/config.xsd @@ -49,18 +49,6 @@ - - - - Default is runtime-specific: if not present, Psalm will only load the Xdebug stub if psalm has unloaded the extension. - - - - - Deprecated. In Psalm 5 extensions will be loaded based on composer.json and overridden with enableExtensions/disableExtensions. - - - diff --git a/docs/running_psalm/configuration.md b/docs/running_psalm/configuration.md index dae932211a6..d50b3633e32 100644 --- a/docs/running_psalm/configuration.md +++ b/docs/running_psalm/configuration.md @@ -246,17 +246,6 @@ When `true`, Psalm will attempt to find all unused code (including unused variab ``` When `true`, Psalm will report all `@psalm-suppress` annotations that aren't used, the equivalent of running with `--find-unused-psalm-suppress`. Defaults to `false`. -#### loadXdebugStub -```xml - -``` -Deprecated, use <enableExtensions> instead. -If not present, Psalm will only load the Xdebug stub if Psalm has unloaded the extension. -When `true`, Psalm will load the Xdebug extension stub (as the extension is unloaded when Psalm runs). -Setting to `false` prevents the stub from loading. - #### ensureArrayStringOffsetsExist ```xml - + $comment_block->tags['variablesfrom'][0] @@ -24,9 +24,6 @@ getAdditionalFileTypeAnalyzers getAdditionalFileTypeScanners - - $this->load_xdebug_stub - @@ -203,12 +200,6 @@ $stmt->expr->getArgs()[0] - - - $config->load_xdebug_stub - $config->load_xdebug_stub - - $callables[0] @@ -286,8 +277,7 @@ $storage->template_extended_count - - $imported_type_data[3] + $l[4] $r[4] $var_line_parts[0] diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 06c89692b43..44ac5f26387 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -196,15 +196,6 @@ class Config */ public $throw_exception = false; - /** - * Whether or not to load Xdebug stub - * - * @deprecated going to be removed in Psalm 5 - * - * @var bool|null - */ - public $load_xdebug_stub; - /** * The directory to store PHP Parser (and other) caches * @@ -957,7 +948,6 @@ private static function fromXmlAndPaths( 'ignoreInternalFunctionFalseReturn' => 'ignore_internal_falsable_issues', 'ignoreInternalFunctionNullReturn' => 'ignore_internal_nullable_issues', 'includePhpVersionsInErrorBaseline' => 'include_php_versions_in_error_baseline', - 'loadXdebugStub' => 'load_xdebug_stub', 'ensureArrayStringOffsetsExist' => 'ensure_array_string_offsets_exist', 'ensureArrayIntOffsetsExist' => 'ensure_array_int_offsets_exist', 'reportMixedIssues' => 'show_mixed_issues', @@ -1004,10 +994,6 @@ private static function fromXmlAndPaths( $config->php_extensions[$ext] = isset($composer_json["require"]["ext-$ext"]); } - if ($config->load_xdebug_stub !== null) { - $config->php_extensions["xdebug"] = $config->load_xdebug_stub; - } - if (isset($config_xml->enableExtensions) && isset($config_xml->enableExtensions->extension)) { foreach ($config_xml->enableExtensions->extension as $extension) { assert(isset($extension["name"])); diff --git a/src/Psalm/Internal/Cli/Psalm.php b/src/Psalm/Internal/Cli/Psalm.php index 70893dddc4a..2063355c711 100644 --- a/src/Psalm/Internal/Cli/Psalm.php +++ b/src/Psalm/Internal/Cli/Psalm.php @@ -253,7 +253,7 @@ public static function run(array $argv): void self::emitMacPcreWarning($options, $threads); - self::restart($options, $config, $threads); + self::restart($options, $threads); if (isset($options['debug-emitted-issues'])) { $config->debug_emitted_issues = true; @@ -882,7 +882,7 @@ private static function emitMacPcreWarning(array $options, int $threads): void } } - private static function restart(array $options, Config $config, int $threads): void + private static function restart(array $options, int $threads): void { $ini_handler = new PsalmRestarter('PSALM'); @@ -907,10 +907,6 @@ private static function restart(array $options, Config $config, int $threads): v // If Xdebug is enabled, restart without it $ini_handler->check(); - - if ($config->load_xdebug_stub === null && PsalmRestarter::getSkippedVersion() !== '') { - $config->load_xdebug_stub = true; - } } private static function detectThreads(array $options, Config $config, bool $in_ci): int From 653d17ff81f650e5010259a2a72db511db6fcdb7 Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Thu, 27 Jan 2022 17:05:48 -0600 Subject: [PATCH 09/10] Add stub for PDOException. --- stubs/extensions/pdo.phpstub | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/stubs/extensions/pdo.phpstub b/stubs/extensions/pdo.phpstub index 5ed1439ff74..039b565e2d7 100644 --- a/stubs/extensions/pdo.phpstub +++ b/stubs/extensions/pdo.phpstub @@ -151,3 +151,8 @@ class PDOStatement implements Traversable */ public function fetchObject($class = \stdclass::class, array $ctorArgs = array()) {} } + + class PDOException extends RuntimeException { + protected string $code; + public ?array $errorInfo = null; +} From d705d5e83acf2ba89d85cbafcb345ccf578ef688 Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Thu, 27 Jan 2022 17:52:50 -0600 Subject: [PATCH 10/10] Add note about $load_xdebug_stub removal to UPGRADING.md. --- UPGRADING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/UPGRADING.md b/UPGRADING.md index 8dafd844e3b..d25d8c8936d 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -165,6 +165,7 @@ - [BC] Property `Psalm\Config::$allow_phpstorm_generics` was removed - [BC] Property `Psalm\Config::$exit_functions` was removed - [BC] Property `Psalm\Config::$forbid_echo` was removed + - [BC] Property `Psalm\Config::$load_xdebug_stub` was removed - [BC] Method `Psalm\Type::getEmpty()` was removed - [BC] Legacy hook interfaces have been removed: - `Psalm\Plugin\Hook\MethodReturnTypeProviderInterface`