1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Merge pull request #7107 from AndrolGenhald/feature/5482-load-extensions-based-on-composer-config

Enable extensions based on composer.json instead of those loaded at runtime (fixes #5482).
This commit is contained in:
orklah 2022-01-28 18:45:52 +01:00 committed by GitHub
commit 2966f1c9d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 390 additions and 169 deletions

View File

@ -82,7 +82,7 @@ jobs:
ini-values: zend.assertions=1, assert.exception=1 ini-values: zend.assertions=1, assert.exception=1
tools: composer:v2 tools: composer:v2
coverage: none coverage: none
extensions: decimal extensions: none, curl, dom, filter, json, libxml, mbstring, openssl, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@ -52,6 +52,7 @@ jobs:
ini-values: zend.assertions=1, assert.exception=1 ini-values: zend.assertions=1, assert.exception=1
tools: composer:v2 tools: composer:v2
coverage: none coverage: none
extensions: none, curl, dom, filter, json, libxml, mbstring, openssl, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@ -165,6 +165,7 @@
- [BC] Property `Psalm\Config::$allow_phpstorm_generics` was removed - [BC] Property `Psalm\Config::$allow_phpstorm_generics` was removed
- [BC] Property `Psalm\Config::$exit_functions` was removed - [BC] Property `Psalm\Config::$exit_functions` was removed
- [BC] Property `Psalm\Config::$forbid_echo` 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] Method `Psalm\Type::getEmpty()` was removed
- [BC] Legacy hook interfaces have been removed: - [BC] Legacy hook interfaces have been removed:
- `Psalm\Plugin\Hook\MethodReturnTypeProviderInterface` - `Psalm\Plugin\Hook\MethodReturnTypeProviderInterface`

View File

@ -21,6 +21,8 @@
<xs:element name="ignoreExceptions" type="ExceptionsType" minOccurs="0" maxOccurs="1" /> <xs:element name="ignoreExceptions" type="ExceptionsType" minOccurs="0" maxOccurs="1" />
<xs:element name="globals" type="GlobalsType" minOccurs="0" maxOccurs="1" /> <xs:element name="globals" type="GlobalsType" minOccurs="0" maxOccurs="1" />
<xs:element name="universalObjectCrates" type="UniversalObjectCratesType" minOccurs="0" maxOccurs="1" /> <xs:element name="universalObjectCrates" type="UniversalObjectCratesType" minOccurs="0" maxOccurs="1" />
<xs:element name="enableExtensions" type="ExtensionsType" minOccurs="0" maxOccurs="1" />
<xs:element name="disableExtensions" type="ExtensionsType" minOccurs="0" maxOccurs="1" />
</xs:choice> </xs:choice>
<xs:attribute name="autoloader" type="xs:string" /> <xs:attribute name="autoloader" type="xs:string" />
@ -47,18 +49,6 @@
<xs:attribute name="ignoreInternalFunctionNullReturn" type="xs:boolean" default="true" /> <xs:attribute name="ignoreInternalFunctionNullReturn" type="xs:boolean" default="true" />
<xs:attribute name="includePhpVersionsInErrorBaseline" type="xs:boolean" default="false" /> <xs:attribute name="includePhpVersionsInErrorBaseline" type="xs:boolean" default="false" />
<xs:attribute name="inferPropertyTypesFromConstructor" type="xs:boolean" default="true" /> <xs:attribute name="inferPropertyTypesFromConstructor" type="xs:boolean" default="true" />
<xs:attribute name="loadXdebugStub" type="xs:boolean">
<xs:annotation>
<xs:documentation xml:lang="en">
Default is runtime-specific: if not present, Psalm will only load the Xdebug stub if psalm has unloaded the extension.
</xs:documentation>
<!-- note: for PHPStorm to mark the attribute as deprecated the doc entry has to be *single line* and start with the word `deprecated` -->
<xs:documentation xml:lang="en">
Deprecated. In Psalm 5 extensions will be loaded based on composer.json and overridden with enableExtensions/disableExtensions.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="memoizeMethodCallResults" type="xs:boolean" default="false" /> <xs:attribute name="memoizeMethodCallResults" type="xs:boolean" default="false" />
<xs:attribute name="rememberPropertyAssignmentsAfterCall" type="xs:boolean" default="true" /> <xs:attribute name="rememberPropertyAssignmentsAfterCall" type="xs:boolean" default="true" />
<xs:attribute name="resolveFromConfigFile" type="xs:boolean" default="true" /> <xs:attribute name="resolveFromConfigFile" type="xs:boolean" default="true" />
@ -682,4 +672,29 @@
<xs:enumeration value="always"/> <xs:enumeration value="always"/>
</xs:restriction> </xs:restriction>
</xs:simpleType> </xs:simpleType>
<xs:complexType name="ExtensionsType">
<xs:sequence>
<xs:element name="extension" maxOccurs="unbounded" type="ExtensionAttributeType" />
</xs:sequence>
</xs:complexType>
<xs:simpleType name="ExtensionType">
<xs:restriction base="xs:string">
<xs:enumeration value="decimal"/>
<xs:enumeration value="dom"/>
<xs:enumeration value="ds"/>
<xs:enumeration value="geos"/>
<xs:enumeration value="gmp"/>
<xs:enumeration value="mongodb"/>
<xs:enumeration value="mysqli"/>
<xs:enumeration value="pdo"/>
<xs:enumeration value="soap"/>
<xs:enumeration value="xdebug"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="ExtensionAttributeType">
<xs:attribute name="name" type="ExtensionType" use="required" />
</xs:complexType>
</xs:schema> </xs:schema>

View File

@ -43,9 +43,6 @@ return [
'mysqli_stmt::prepare' => [['sql']], 'mysqli_stmt::prepare' => [['sql']],
'passthru' => [['shell']], 'passthru' => [['shell']],
'pcntl_exec' => [['shell']], 'pcntl_exec' => [['shell']],
'PDO::prepare' => [['sql']],
'PDO::query' => [['sql']],
'PDO::exec' => [['sql']],
'pg_exec' => [[], ['sql']], 'pg_exec' => [[], ['sql']],
'pg_prepare' => [[], [], ['sql']], 'pg_prepare' => [[], [], ['sql']],
'pg_put_line' => [[], ['sql']], 'pg_put_line' => [[], ['sql']],

View File

@ -246,16 +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`. 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
<psalm
loadXdebugStub="[bool]"
>
```
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 #### ensureArrayStringOffsetsExist
```xml ```xml
<psalm <psalm
@ -336,7 +326,7 @@ When `false`, Psalm will not consider issue at lower level than `errorLevel` as
#### allowNamedArgumentCalls #### allowNamedArgumentCalls
```xml ```xml
<psalm <psalm
allowNamedArgumentCalls="[bool]" allowNamedArgumentCalls="[bool]"
> >
``` ```
@ -432,6 +422,23 @@ Optional. Same format as `<projectFiles>`. Directories Psalm should load but not
#### &lt;fileExtensions&gt; #### &lt;fileExtensions&gt;
Optional. A list of extensions to search over. See [Checking non-PHP files](checking_non_php_files.md) to understand how to extend this. Optional. A list of extensions to search over. See [Checking non-PHP files](checking_non_php_files.md) to understand how to extend this.
#### &lt;enableExtensions&gt;
Optional. A list of extensions to enable. By default, only extensions required by your composer.json will be enabled.
```xml
<enableExtensions>
<extension name="decimal"/>
<extension name="pdo"/>
</enableExtensions>
```
#### &lt;disableExtensions&gt;
Optional. A list of extensions to disable. By default, only extensions required by your composer.json will be enabled.
```xml
<disableExtensions>
<extension name="gmp"/>
</disableExtensions>
```
#### &lt;plugins&gt; #### &lt;plugins&gt;
Optional. A list of `<plugin filename="path_to_plugin.php" />` entries. See the [Plugins](plugins/using_plugins.md) section for more information. Optional. A list of `<plugin filename="path_to_plugin.php" />` entries. See the [Plugins](plugins/using_plugins.md) section for more information.
@ -483,7 +490,7 @@ The following configuration declares custom types for super-globals (`$GLOBALS`
```xml ```xml
<globals> <globals>
<var name="$GLOBALS" type="array{DB: MyVendor\DatabaseConnection, VIEW: MyVendor\TemplateView}" /> <var name="$GLOBALS" type="array{DB: MyVendor\DatabaseConnection, VIEW: MyVendor\TemplateView}" />
<var name="$_GET" type="array{data: array<string, string>}" /> <var name="$_GET" type="array{data: array<string, string>}" />
</globals> </globals>
``` ```

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="dev-master@c90cffd382bb7a5d390387d4ad2a02a4935fba5c"> <files psalm-version="dev-master@093edcbe1d5c90ad574efcddd62fe741819e7cf2">
<file src="examples/TemplateChecker.php"> <file src="examples/TemplateChecker.php">
<PossiblyUndefinedIntArrayOffset occurrences="2"> <PossiblyUndefinedIntArrayOffset occurrences="2">
<code>$comment_block-&gt;tags['variablesfrom'][0]</code> <code>$comment_block-&gt;tags['variablesfrom'][0]</code>
@ -24,9 +24,6 @@
<code>getAdditionalFileTypeAnalyzers</code> <code>getAdditionalFileTypeAnalyzers</code>
<code>getAdditionalFileTypeScanners</code> <code>getAdditionalFileTypeScanners</code>
</DeprecatedMethod> </DeprecatedMethod>
<DeprecatedProperty occurrences="1">
<code>$this-&gt;load_xdebug_stub</code>
</DeprecatedProperty>
</file> </file>
<file src="src/Psalm/Config/FileFilter.php"> <file src="src/Psalm/Config/FileFilter.php">
<PossiblyUndefinedIntArrayOffset occurrences="1"> <PossiblyUndefinedIntArrayOffset occurrences="1">
@ -203,12 +200,6 @@
<code>$stmt-&gt;expr-&gt;getArgs()[0]</code> <code>$stmt-&gt;expr-&gt;getArgs()[0]</code>
</PossiblyUndefinedIntArrayOffset> </PossiblyUndefinedIntArrayOffset>
</file> </file>
<file src="src/Psalm/Internal/Cli/Psalm.php">
<DeprecatedProperty occurrences="2">
<code>$config-&gt;load_xdebug_stub</code>
<code>$config-&gt;load_xdebug_stub</code>
</DeprecatedProperty>
</file>
<file src="src/Psalm/Internal/Codebase/InternalCallMapHandler.php"> <file src="src/Psalm/Internal/Codebase/InternalCallMapHandler.php">
<PossiblyUndefinedIntArrayOffset occurrences="2"> <PossiblyUndefinedIntArrayOffset occurrences="2">
<code>$callables[0]</code> <code>$callables[0]</code>
@ -286,8 +277,7 @@
<DeprecatedProperty occurrences="1"> <DeprecatedProperty occurrences="1">
<code>$storage-&gt;template_extended_count</code> <code>$storage-&gt;template_extended_count</code>
</DeprecatedProperty> </DeprecatedProperty>
<PossiblyUndefinedIntArrayOffset occurrences="4"> <PossiblyUndefinedIntArrayOffset occurrences="3">
<code>$imported_type_data[3]</code>
<code>$l[4]</code> <code>$l[4]</code>
<code>$r[4]</code> <code>$r[4]</code>
<code>$var_line_parts[0]</code> <code>$var_line_parts[0]</code>

View File

@ -46,6 +46,7 @@ use UnexpectedValueException;
use XdgBaseDir\Xdg; use XdgBaseDir\Xdg;
use stdClass; use stdClass;
use function array_key_exists;
use function array_map; use function array_map;
use function array_merge; use function array_merge;
use function array_pad; use function array_pad;
@ -58,7 +59,6 @@ use function class_exists;
use function count; use function count;
use function dirname; use function dirname;
use function explode; use function explode;
use function extension_loaded;
use function file_exists; use function file_exists;
use function file_get_contents; use function file_get_contents;
use function filetype; use function filetype;
@ -196,15 +196,6 @@ class Config
*/ */
public $throw_exception = false; 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 * The directory to store PHP Parser (and other) caches
* *
@ -574,6 +565,34 @@ class Config
/** @var ?int */ /** @var ?int */
public $threads; 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() protected function __construct()
{ {
self::$instance = $this; self::$instance = $this;
@ -931,7 +950,6 @@ class Config
'ignoreInternalFunctionFalseReturn' => 'ignore_internal_falsable_issues', 'ignoreInternalFunctionFalseReturn' => 'ignore_internal_falsable_issues',
'ignoreInternalFunctionNullReturn' => 'ignore_internal_nullable_issues', 'ignoreInternalFunctionNullReturn' => 'ignore_internal_nullable_issues',
'includePhpVersionsInErrorBaseline' => 'include_php_versions_in_error_baseline', 'includePhpVersionsInErrorBaseline' => 'include_php_versions_in_error_baseline',
'loadXdebugStub' => 'load_xdebug_stub',
'ensureArrayStringOffsetsExist' => 'ensure_array_string_offsets_exist', 'ensureArrayStringOffsetsExist' => 'ensure_array_string_offsets_exist',
'ensureArrayIntOffsetsExist' => 'ensure_array_int_offsets_exist', 'ensureArrayIntOffsetsExist' => 'ensure_array_int_offsets_exist',
'reportMixedIssues' => 'show_mixed_issues', 'reportMixedIssues' => 'show_mixed_issues',
@ -966,6 +984,36 @@ class Config
$base_dir = $current_dir; $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 ($config->php_extensions as $ext => $_) {
$config->php_extensions[$ext] = isset($composer_json["require"]["ext-$ext"]);
}
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(array_key_exists($extensionName, $config->php_extensions));
$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(array_key_exists($extensionName, $config->php_extensions));
$config->php_extensions[$extensionName] = false;
}
}
if (isset($config_xml['phpVersion'])) { if (isset($config_xml['phpVersion'])) {
$config->configured_php_version = (string) $config_xml['phpVersion']; $config->configured_php_version = (string) $config_xml['phpVersion'];
} }
@ -1969,7 +2017,6 @@ class Config
$dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'CoreGenericClasses.phpstub', $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 . 'CoreGenericIterators.phpstub',
$dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'CoreImmutableClasses.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 . 'Reflection.phpstub',
$dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'SPL.phpstub', $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'SPL.phpstub',
]; ];
@ -1984,39 +2031,11 @@ class Config
$this->internal_stubs[] = $stringable_path; $this->internal_stubs[] = $stringable_path;
} }
if (extension_loaded('PDO')) { foreach ($this->php_extensions as $ext => $enabled) {
$ext_pdo_path = $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'pdo.phpstub'; if ($enabled) {
$this->internal_stubs[] = $ext_pdo_path; $this->internal_stubs[] = $dir_lvl_2 . DIRECTORY_SEPARATOR . "stubs"
} . DIRECTORY_SEPARATOR . "extensions" . DIRECTORY_SEPARATOR . "$ext.phpstub";
}
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->internal_stubs as $stub_path) { foreach ($this->internal_stubs as $stub_path) {

View File

@ -253,7 +253,7 @@ final class Psalm
self::emitMacPcreWarning($options, $threads); self::emitMacPcreWarning($options, $threads);
self::restart($options, $config, $threads); self::restart($options, $threads);
if (isset($options['debug-emitted-issues'])) { if (isset($options['debug-emitted-issues'])) {
$config->debug_emitted_issues = true; $config->debug_emitted_issues = true;
@ -882,7 +882,7 @@ final class Psalm
} }
} }
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'); $ini_handler = new PsalmRestarter('PSALM');
@ -907,10 +907,6 @@ final class Psalm
// If Xdebug is enabled, restart without it // If Xdebug is enabled, restart without it
$ini_handler->check(); $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 private static function detectThreads(array $options, Config $config, bool $in_ci): int

View File

@ -2,7 +2,7 @@
namespace Psalm\Internal\Provider\ReturnTypeProvider; namespace Psalm\Internal\Provider\ReturnTypeProvider;
use PDO; use Psalm\Config;
use Psalm\Plugin\EventHandler\Event\MethodReturnTypeProviderEvent; use Psalm\Plugin\EventHandler\Event\MethodReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\MethodReturnTypeProviderInterface; use Psalm\Plugin\EventHandler\MethodReturnTypeProviderInterface;
use Psalm\Type; use Psalm\Type;
@ -15,8 +15,6 @@ use Psalm\Type\Atomic\TObject;
use Psalm\Type\Atomic\TScalar; use Psalm\Type\Atomic\TScalar;
use Psalm\Type\Union; use Psalm\Type\Union;
use function class_exists;
/** /**
* @internal * @internal
*/ */
@ -29,11 +27,12 @@ class PdoStatementReturnTypeProvider implements MethodReturnTypeProviderInterfac
public static function getMethodReturnType(MethodReturnTypeProviderEvent $event): ?Union public static function getMethodReturnType(MethodReturnTypeProviderEvent $event): ?Union
{ {
$config = Config::getInstance();
$source = $event->getSource(); $source = $event->getSource();
$call_args = $event->getCallArgs(); $call_args = $event->getCallArgs();
$method_name_lowercase = $event->getMethodNameLowercase(); $method_name_lowercase = $event->getMethodNameLowercase();
if ($method_name_lowercase === 'fetch' if ($method_name_lowercase === 'fetch'
&& class_exists('PDO') && $config->php_extensions["pdo"]
&& isset($call_args[0]) && isset($call_args[0])
&& ($first_arg_type = $source->getNodeTypeProvider()->getType($call_args[0]->value)) && ($first_arg_type = $source->getNodeTypeProvider()->getType($call_args[0]->value))
&& $first_arg_type->isSingleIntLiteral() && $first_arg_type->isSingleIntLiteral()
@ -41,7 +40,7 @@ class PdoStatementReturnTypeProvider implements MethodReturnTypeProviderInterfac
$fetch_mode = $first_arg_type->getSingleIntLiteral()->value; $fetch_mode = $first_arg_type->getSingleIntLiteral()->value;
switch ($fetch_mode) { switch ($fetch_mode) {
case PDO::FETCH_ASSOC: // array<string,scalar|null>|false case 2: // PDO::FETCH_ASSOC - array<string,scalar|null>|false
return new Union([ return new Union([
new TArray([ new TArray([
Type::getString(), Type::getString(),
@ -53,7 +52,7 @@ class PdoStatementReturnTypeProvider implements MethodReturnTypeProviderInterfac
new TFalse(), new TFalse(),
]); ]);
case PDO::FETCH_BOTH: // array<array-key,scalar|null>|false case 4: // PDO::FETCH_BOTH - array<array-key,scalar|null>|false
return new Union([ return new Union([
new TArray([ new TArray([
Type::getArrayKey(), Type::getArrayKey(),
@ -65,16 +64,16 @@ class PdoStatementReturnTypeProvider implements MethodReturnTypeProviderInterfac
new TFalse(), new TFalse(),
]); ]);
case PDO::FETCH_BOUND: // bool case 6: // PDO::FETCH_BOUND - bool
return Type::getBool(); return Type::getBool();
case PDO::FETCH_CLASS: // object|false case 8: // PDO::FETCH_CLASS - object|false
return new Union([ return new Union([
new TObject(), new TObject(),
new TFalse(), 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 // This actually returns a PDORow object, but that class is
// undocumented, and its attributes are all dynamic anyway // undocumented, and its attributes are all dynamic anyway
return new Union([ return new Union([
@ -82,7 +81,7 @@ class PdoStatementReturnTypeProvider implements MethodReturnTypeProviderInterfac
new TFalse(), new TFalse(),
]); ]);
case PDO::FETCH_NAMED: // array<string, scalar|list<scalar>>|false case 11: // PDO::FETCH_NAMED - array<string, scalar|list<scalar>>|false
return new Union([ return new Union([
new TArray([ new TArray([
Type::getString(), Type::getString(),
@ -94,7 +93,7 @@ class PdoStatementReturnTypeProvider implements MethodReturnTypeProviderInterfac
new TFalse(), new TFalse(),
]); ]);
case PDO::FETCH_NUM: // list<scalar|null>|false case 3: // PDO::FETCH_NUM - list<scalar|null>|false
return new Union([ return new Union([
new TList( new TList(
new Union([ new Union([
@ -105,7 +104,7 @@ class PdoStatementReturnTypeProvider implements MethodReturnTypeProviderInterfac
new TFalse(), new TFalse(),
]); ]);
case PDO::FETCH_OBJ: // stdClass|false case 5: // PDO::FETCH_OBJ - stdClass|false
return new Union([ return new Union([
new TNamedObject('stdClass'), new TNamedObject('stdClass'),
new TFalse(), new TFalse(),

View File

@ -2,7 +2,6 @@
namespace Psalm\Internal\Provider\ReturnTypeProvider; namespace Psalm\Internal\Provider\ReturnTypeProvider;
use PDO;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Plugin\EventHandler\Event\MethodParamsProviderEvent; use Psalm\Plugin\EventHandler\Event\MethodParamsProviderEvent;
@ -62,7 +61,7 @@ class PdoStatementSetFetchMode implements MethodParamsProviderInterface
$value = $first_call_arg_type->getSingleIntLiteral()->value; $value = $first_call_arg_type->getSingleIntLiteral()->value;
switch ($value) { switch ($value) {
case PDO::FETCH_COLUMN: case 7: // PDO::FETCH_COLUMN
$params[] = new FunctionLikeParameter( $params[] = new FunctionLikeParameter(
'colno', 'colno',
false, false,
@ -73,7 +72,7 @@ class PdoStatementSetFetchMode implements MethodParamsProviderInterface
); );
break; break;
case PDO::FETCH_CLASS: case 8: // PDO::FETCH_CLASS
$params[] = new FunctionLikeParameter( $params[] = new FunctionLikeParameter(
'classname', 'classname',
false, false,
@ -93,7 +92,7 @@ class PdoStatementSetFetchMode implements MethodParamsProviderInterface
); );
break; break;
case PDO::FETCH_INTO: case 9: // PDO::FETCH_INTO
$params[] = new FunctionLikeParameter( $params[] = new FunctionLikeParameter(
'object', 'object',
false, false,

View File

@ -0,0 +1,11 @@
<?php
class GMP implements Serializable {
private function __construct() {}
public function __toString(): string {}
public function serialize(): string {}
public function unserialize(string $data): void {}
}

View File

@ -0,0 +1,158 @@
<?php
class PDO
{
public const PARAM_NULL = 0;
public const PARAM_INT = 1;
public const PARAM_STR = 2;
public const PARAM_LOB = 3;
public const PARAM_STMT = 4;
public const PARAM_BOOL = 5;
// public const PARAM_STR_NATL = 1073741824; since 7.2
// public const PARAM_STR_CHAR = 536870912; since 7.2
public const PARAM_INPUT_OUTPUT = 2147483648;
// public const FETCH_DEFAULT = 0; since 8.0.7
public const FETCH_LAZY = 1;
public const FETCH_ASSOC = 2;
public const FETCH_NAMED = 11;
public const FETCH_NUM = 3;
public const FETCH_BOTH = 4;
public const FETCH_OBJ = 5;
public const FETCH_BOUND = 6;
public const FETCH_COLUMN = 7;
public const FETCH_CLASS = 8;
public const FETCH_INTO = 9;
public const FETCH_FUNC = 10;
public const FETCH_GROUP = 65536;
public const FETCH_UNIQUE = 196608;
public const FETCH_KEY_PAIR = 12;
public const FETCH_CLASSTYPE = 262144;
public const FETCH_SERIALIZE = 524288; // Deprecated 8.1
public const FETCH_PROPS_LATE = 1048576;
public const ATTR_AUTOCOMMIT = 0;
public const ATTR_PREFETCH = 1;
public const ATTR_TIMEOUT = 2;
public const ATTR_ERRMODE = 3;
public const ATTR_SERVER_VERSION = 4;
public const ATTR_CLIENT_VERSION = 5;
public const ATTR_SERVER_INFO = 6;
public const ATTR_CONNECTION_STATUS = 7;
public const ATTR_CASE = 8;
public const ATTR_CURSOR_NAME = 9;
public const ATTR_CURSOR = 10;
public const ATTR_DRIVER_NAME = 16;
public const ATTR_ORACLE_NULLS = 11;
public const ATTR_PERSISTENT = 12;
public const ATTR_STATEMENT_CLASS = 13;
public const ATTR_FETCH_CATALOG_NAMES = 15;
public const ATTR_FETCH_TABLE_NAMES = 14;
public const ATTR_STRINGIFY_FETCHES = 17;
public const ATTR_MAX_COLUMN_LEN = 18;
public const ATTR_DEFAULT_FETCH_MODE = 19;
public const ATTR_EMULATE_PREPARES = 20;
// public const ATTR_DEFAULT_STR_PARAM = 21; since 7.2
public const ERRMODE_SILENT = 0;
public const ERRMODE_WARNING = 1;
public const ERRMODE_EXCEPTION = 2;
public const CASE_NATURAL = 0;
public const CASE_LOWER = 2;
public const CASE_UPPER = 1;
public const NULL_NATURAL = 0;
public const NULL_EMPTY_STRING = 1;
public const NULL_TO_STRING = 2;
public const FETCH_ORI_NEXT = 0;
public const FETCH_ORI_PRIOR = 1;
public const FETCH_ORI_FIRST = 2;
public const FETCH_ORI_LAST = 3;
public const FETCH_ORI_ABS = 4;
public const FETCH_ORI_REL = 5;
public const CURSOR_FWDONLY = 0;
public const CURSOR_SCROLL = 1;
public const ERR_NONE = 00000;
public const PARAM_EVT_ALLOC = 0;
public const PARAM_EVT_FREE = 1;
public const PARAM_EVT_EXEC_PRE = 2;
public const PARAM_EVT_EXEC_POST = 3;
public const PARAM_EVT_FETCH_PRE = 4;
public const PARAM_EVT_FETCH_POST = 5;
public const PARAM_EVT_NORMALIZE = 6;
// public const SQLITE_DETERMINISTIC = ???; since 7.1.4 with pdo_sqlite
public function __construct(
string $dsn,
?string $username = null,
?string $password = null,
?array $options = null
) {}
public function beginTransaction(): bool {}
public function commit(): bool {}
public function errorCode(): ?string {}
public function errorInfo(): array {}
/**
* @psalm-taint-sink sql $statement
*
* @return int|false
*/
public function exec(string $statement) {}
/** @return bool|int|string|array|null */
public function getAttribute(int $attribute) {}
public static function getAvailableDrivers(): array {}
public function inTransaction(): bool {}
/** @return string|false */
public function lastInsertId(?string $name = null) {}
/**
* @psalm-taint-sink sql $query
*
* @return PDOStatement|false
*/
public function prepare(string $query, array $options = []) {}
/**
* @psalm-taint-sink sql $query
*
* @return PDOStatement|false
*/
public function query(string $query, ?int $fetchMode = null) {}
/**
* @return string|false
*/
public function quote(string $string, int $type = PDO::PARAM_STR) {}
public function rollBack(): bool {}
public function setAttribute(int $attribute, mixed $value): bool {}
}
/**
* @template TValue
*
* @template-implements Traversable<int, TValue>
*/
class PDOStatement implements Traversable
{
/**
* @psalm-taint-sink callable $class
*
* @template T of object
* @param class-string<T> $class
* @param array $ctorArgs
* @return false|T
*/
public function fetchObject($class = \stdclass::class, array $ctorArgs = array()) {}
}
class PDOException extends RuntimeException {
protected string $code;
public ?array $errorInfo = null;
}

View File

@ -282,3 +282,30 @@ class SoapClient {
public function __setSoapHeaders ($soapheaders = null) {} 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
) {}
}
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
) {}
}

View File

@ -1,19 +0,0 @@
<?php
/**
* @template TValue
*
* @template-implements Traversable<int, TValue>
*/
class PDOStatement implements Traversable
{
/**
* @psalm-taint-sink callable $class
*
* @template T of object
* @param class-string<T> $class
* @param array $ctorArgs
* @return false|T
*/
public function fetchObject($class = \stdclass::class, array $ctorArgs = array()) {}
}

View File

@ -8,8 +8,6 @@ use Psalm\Exception\CodeException;
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait; use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
use function class_exists;
use const DIRECTORY_SEPARATOR; use const DIRECTORY_SEPARATOR;
class BinaryOperationTest extends TestCase class BinaryOperationTest extends TestCase
@ -19,10 +17,6 @@ class BinaryOperationTest extends TestCase
public function testGMPOperations(): void public function testGMPOperations(): void
{ {
if (class_exists('GMP') === false) {
$this->markTestSkipped('Cannot run test, base class "GMP" does not exist!');
}
$this->addFile( $this->addFile(
'somefile.php', 'somefile.php',
'<?php '<?php
@ -88,10 +82,6 @@ class BinaryOperationTest extends TestCase
public function testDecimalOperations(): void public function testDecimalOperations(): void
{ {
if (!class_exists('Decimal\\Decimal')) {
$this->markTestSkipped('Cannot run test, base class "Decimal\\Decimal" does not exist!');
}
$this->addFile( $this->addFile(
'somefile.php', 'somefile.php',
'<?php '<?php

View File

@ -5,8 +5,6 @@ namespace Psalm\Tests;
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait; use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
use function class_exists;
class ClassTest extends TestCase class ClassTest extends TestCase
{ {
use InvalidCodeAnalysisTestTrait; use InvalidCodeAnalysisTestTrait;
@ -14,10 +12,6 @@ class ClassTest extends TestCase
public function testExtendsMysqli(): void public function testExtendsMysqli(): void
{ {
if (class_exists('mysqli') === false) {
$this->markTestSkipped('Cannot run test, base class "mysqli" does not exist!');
}
$this->addFile( $this->addFile(
'somefile.php', 'somefile.php',
'<?php '<?php

View File

@ -3,6 +3,7 @@
namespace Psalm\Tests\Config; namespace Psalm\Tests\Config;
use Psalm\Config; use Psalm\Config;
use Psalm\Exception\ConfigException;
use Psalm\Internal\PluginManager\ConfigFile; use Psalm\Internal\PluginManager\ConfigFile;
use Psalm\Internal\RuntimeCaches; use Psalm\Internal\RuntimeCaches;
use Psalm\Tests\TestCase; use Psalm\Tests\TestCase;
@ -206,6 +207,64 @@ class ConfigFileTest extends TestCase
); );
} }
public function testEnableExtensions(): void
{
file_put_contents($this->file_path, trim('
<?xml version="1.0"?>
<psalm>
<enableExtensions>
<extension name="mysqli"/>
<extension name="pdo"/>
</enableExtensions>
</psalm>
'));
$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('
<?xml version="1.0"?>
<psalm>
<enableExtensions>
<extension name="mysqli"/>
<extension name="pdo"/>
</enableExtensions>
<disableExtensions>
<extension name="mysqli"/>
<extension name="pdo"/>
</disableExtensions>
</psalm>
'));
$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('
<?xml version="1.0"?>
<psalm>
<enableExtensions>
<extension name="NotARealExtension"/>
</enableExtensions>
</psalm>
'));
(new ConfigFile((string)getcwd(), $this->file_path))->getConfig();
}
/** /**
* @param string $expected_template * @param string $expected_template
* @param string $contents * @param string $contents

View File

@ -6,8 +6,6 @@ use Psalm\Context;
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait; use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
use function class_exists;
use const DIRECTORY_SEPARATOR; use const DIRECTORY_SEPARATOR;
class MethodCallTest extends TestCase class MethodCallTest extends TestCase
@ -17,10 +15,6 @@ class MethodCallTest extends TestCase
public function testExtendDocblockParamType(): void public function testExtendDocblockParamType(): void
{ {
if (class_exists('SoapClient') === false) {
$this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!');
}
$this->addFile( $this->addFile(
'somefile.php', 'somefile.php',
'<?php '<?php

View File

@ -7,8 +7,6 @@ use Psalm\Exception\CodeException;
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait; use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
use function class_exists;
use const DIRECTORY_SEPARATOR; use const DIRECTORY_SEPARATOR;
class MethodSignatureTest extends TestCase class MethodSignatureTest extends TestCase
@ -18,10 +16,6 @@ class MethodSignatureTest extends TestCase
public function testExtendSoapClientWithDocblockTypes(): void public function testExtendSoapClientWithDocblockTypes(): void
{ {
if (class_exists('SoapClient') === false) {
$this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!');
}
$this->addFile( $this->addFile(
'somefile.php', 'somefile.php',
'<?php '<?php
@ -52,10 +46,6 @@ class MethodSignatureTest extends TestCase
public function testExtendSoapClientWithNoDocblockTypes(): void public function testExtendSoapClientWithNoDocblockTypes(): void
{ {
if (class_exists('SoapClient') === false) {
$this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!');
}
$this->addFile( $this->addFile(
'somefile.php', 'somefile.php',
'<?php '<?php
@ -78,10 +68,6 @@ class MethodSignatureTest extends TestCase
public function testExtendSoapClientWithParamType(): void public function testExtendSoapClientWithParamType(): void
{ {
if (class_exists('SoapClient') === false) {
$this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!');
}
$this->addFile( $this->addFile(
'somefile.php', 'somefile.php',
'<?php '<?php
@ -249,9 +235,6 @@ class MethodSignatureTest extends TestCase
{ {
$this->expectExceptionMessage('ImplementedParamTypeMismatch'); $this->expectExceptionMessage('ImplementedParamTypeMismatch');
$this->expectException(CodeException::class); $this->expectException(CodeException::class);
if (class_exists('SoapClient') === false) {
$this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!');
}
$this->addFile( $this->addFile(
'somefile.php', 'somefile.php',
@ -286,10 +269,6 @@ class MethodSignatureTest extends TestCase
$this->expectException(CodeException::class); $this->expectException(CodeException::class);
$this->expectExceptionMessage('MethodSignatureMismatch'); $this->expectExceptionMessage('MethodSignatureMismatch');
if (class_exists('SoapClient') === false) {
$this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!');
}
$this->addFile( $this->addFile(
'somefile.php', 'somefile.php',
'<?php '<?php

View File

@ -20,6 +20,10 @@ class TestConfig extends Config
{ {
parent::__construct(); parent::__construct();
foreach ($this->php_extensions as $ext => $_enabled) {
$this->php_extensions[$ext] = true;
}
$this->throw_exception = true; $this->throw_exception = true;
$this->use_docblock_types = true; $this->use_docblock_types = true;
$this->level = 1; $this->level = 1;