From cebb7c013d40da39eef15e866071e8eb104d3a22 Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Wed, 8 Dec 2021 17:47:32 -0600 Subject: [PATCH] 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 c80ca64de65..a0a00007d11 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -57,7 +57,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; @@ -566,6 +565,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; @@ -955,6 +982,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']; } @@ -1947,7 +2000,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', ]; @@ -1962,39 +2014,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;