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:
commit
2966f1c9d4
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -82,7 +82,7 @@ jobs:
|
||||
ini-values: zend.assertions=1, assert.exception=1
|
||||
tools: composer:v2
|
||||
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
|
||||
|
||||
|
1
.github/workflows/windows-ci.yml
vendored
1
.github/workflows/windows-ci.yml
vendored
@ -52,6 +52,7 @@ jobs:
|
||||
ini-values: zend.assertions=1, assert.exception=1
|
||||
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
|
||||
|
||||
|
@ -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`
|
||||
|
39
config.xsd
39
config.xsd
@ -21,6 +21,8 @@
|
||||
<xs:element name="ignoreExceptions" type="ExceptionsType" 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="enableExtensions" type="ExtensionsType" minOccurs="0" maxOccurs="1" />
|
||||
<xs:element name="disableExtensions" type="ExtensionsType" minOccurs="0" maxOccurs="1" />
|
||||
</xs:choice>
|
||||
|
||||
<xs:attribute name="autoloader" type="xs:string" />
|
||||
@ -47,18 +49,6 @@
|
||||
<xs:attribute name="ignoreInternalFunctionNullReturn" type="xs:boolean" default="true" />
|
||||
<xs:attribute name="includePhpVersionsInErrorBaseline" type="xs:boolean" default="false" />
|
||||
<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="rememberPropertyAssignmentsAfterCall" type="xs:boolean" default="true" />
|
||||
<xs:attribute name="resolveFromConfigFile" type="xs:boolean" default="true" />
|
||||
@ -682,4 +672,29 @@
|
||||
<xs:enumeration value="always"/>
|
||||
</xs:restriction>
|
||||
</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>
|
||||
|
@ -43,9 +43,6 @@ return [
|
||||
'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']],
|
||||
|
@ -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`.
|
||||
|
||||
#### 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
|
||||
```xml
|
||||
<psalm
|
||||
@ -336,7 +326,7 @@ When `false`, Psalm will not consider issue at lower level than `errorLevel` as
|
||||
#### allowNamedArgumentCalls
|
||||
|
||||
```xml
|
||||
<psalm
|
||||
<psalm
|
||||
allowNamedArgumentCalls="[bool]"
|
||||
>
|
||||
```
|
||||
@ -432,6 +422,23 @@ Optional. Same format as `<projectFiles>`. 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
|
||||
<enableExtensions>
|
||||
<extension name="decimal"/>
|
||||
<extension name="pdo"/>
|
||||
</enableExtensions>
|
||||
```
|
||||
|
||||
#### <disableExtensions>
|
||||
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>
|
||||
```
|
||||
|
||||
#### <plugins>
|
||||
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
|
||||
<globals>
|
||||
<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>
|
||||
```
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<files psalm-version="dev-master@c90cffd382bb7a5d390387d4ad2a02a4935fba5c">
|
||||
<files psalm-version="dev-master@093edcbe1d5c90ad574efcddd62fe741819e7cf2">
|
||||
<file src="examples/TemplateChecker.php">
|
||||
<PossiblyUndefinedIntArrayOffset occurrences="2">
|
||||
<code>$comment_block->tags['variablesfrom'][0]</code>
|
||||
@ -24,9 +24,6 @@
|
||||
<code>getAdditionalFileTypeAnalyzers</code>
|
||||
<code>getAdditionalFileTypeScanners</code>
|
||||
</DeprecatedMethod>
|
||||
<DeprecatedProperty occurrences="1">
|
||||
<code>$this->load_xdebug_stub</code>
|
||||
</DeprecatedProperty>
|
||||
</file>
|
||||
<file src="src/Psalm/Config/FileFilter.php">
|
||||
<PossiblyUndefinedIntArrayOffset occurrences="1">
|
||||
@ -203,12 +200,6 @@
|
||||
<code>$stmt->expr->getArgs()[0]</code>
|
||||
</PossiblyUndefinedIntArrayOffset>
|
||||
</file>
|
||||
<file src="src/Psalm/Internal/Cli/Psalm.php">
|
||||
<DeprecatedProperty occurrences="2">
|
||||
<code>$config->load_xdebug_stub</code>
|
||||
<code>$config->load_xdebug_stub</code>
|
||||
</DeprecatedProperty>
|
||||
</file>
|
||||
<file src="src/Psalm/Internal/Codebase/InternalCallMapHandler.php">
|
||||
<PossiblyUndefinedIntArrayOffset occurrences="2">
|
||||
<code>$callables[0]</code>
|
||||
@ -286,8 +277,7 @@
|
||||
<DeprecatedProperty occurrences="1">
|
||||
<code>$storage->template_extended_count</code>
|
||||
</DeprecatedProperty>
|
||||
<PossiblyUndefinedIntArrayOffset occurrences="4">
|
||||
<code>$imported_type_data[3]</code>
|
||||
<PossiblyUndefinedIntArrayOffset occurrences="3">
|
||||
<code>$l[4]</code>
|
||||
<code>$r[4]</code>
|
||||
<code>$var_line_parts[0]</code>
|
||||
|
@ -46,6 +46,7 @@ use UnexpectedValueException;
|
||||
use XdgBaseDir\Xdg;
|
||||
use stdClass;
|
||||
|
||||
use function array_key_exists;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_pad;
|
||||
@ -58,7 +59,6 @@ use function class_exists;
|
||||
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;
|
||||
@ -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
|
||||
*
|
||||
@ -574,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;
|
||||
@ -931,7 +950,6 @@ class Config
|
||||
'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',
|
||||
@ -966,6 +984,36 @@ class Config
|
||||
$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'])) {
|
||||
$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 . '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',
|
||||
];
|
||||
@ -1984,39 +2031,11 @@ class Config
|
||||
$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) {
|
||||
|
@ -253,7 +253,7 @@ final class Psalm
|
||||
|
||||
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 @@ 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');
|
||||
|
||||
@ -907,10 +907,6 @@ final class Psalm
|
||||
|
||||
// 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
|
||||
|
@ -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\TObject;
|
||||
use Psalm\Type\Atomic\TScalar;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function class_exists;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ -29,11 +27,12 @@ class PdoStatementReturnTypeProvider implements MethodReturnTypeProviderInterfac
|
||||
|
||||
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 @@ class PdoStatementReturnTypeProvider implements MethodReturnTypeProviderInterfac
|
||||
$fetch_mode = $first_arg_type->getSingleIntLiteral()->value;
|
||||
|
||||
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([
|
||||
new TArray([
|
||||
Type::getString(),
|
||||
@ -53,7 +52,7 @@ class PdoStatementReturnTypeProvider implements MethodReturnTypeProviderInterfac
|
||||
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([
|
||||
new TArray([
|
||||
Type::getArrayKey(),
|
||||
@ -65,16 +64,16 @@ class PdoStatementReturnTypeProvider implements MethodReturnTypeProviderInterfac
|
||||
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 @@ class PdoStatementReturnTypeProvider implements MethodReturnTypeProviderInterfac
|
||||
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([
|
||||
new TArray([
|
||||
Type::getString(),
|
||||
@ -94,7 +93,7 @@ class PdoStatementReturnTypeProvider implements MethodReturnTypeProviderInterfac
|
||||
new TFalse(),
|
||||
]);
|
||||
|
||||
case PDO::FETCH_NUM: // list<scalar|null>|false
|
||||
case 3: // PDO::FETCH_NUM - list<scalar|null>|false
|
||||
return new Union([
|
||||
new TList(
|
||||
new Union([
|
||||
@ -105,7 +104,7 @@ class PdoStatementReturnTypeProvider implements MethodReturnTypeProviderInterfac
|
||||
new TFalse(),
|
||||
]);
|
||||
|
||||
case PDO::FETCH_OBJ: // stdClass|false
|
||||
case 5: // PDO::FETCH_OBJ - stdClass|false
|
||||
return new Union([
|
||||
new TNamedObject('stdClass'),
|
||||
new TFalse(),
|
||||
|
@ -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 @@ class PdoStatementSetFetchMode implements MethodParamsProviderInterface
|
||||
$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 @@ class PdoStatementSetFetchMode implements MethodParamsProviderInterface
|
||||
);
|
||||
break;
|
||||
|
||||
case PDO::FETCH_CLASS:
|
||||
case 8: // PDO::FETCH_CLASS
|
||||
$params[] = new FunctionLikeParameter(
|
||||
'classname',
|
||||
false,
|
||||
@ -93,7 +92,7 @@ class PdoStatementSetFetchMode implements MethodParamsProviderInterface
|
||||
);
|
||||
break;
|
||||
|
||||
case PDO::FETCH_INTO:
|
||||
case 9: // PDO::FETCH_INTO
|
||||
$params[] = new FunctionLikeParameter(
|
||||
'object',
|
||||
false,
|
||||
|
11
stubs/extensions/gmp.phpstub
Normal file
11
stubs/extensions/gmp.phpstub
Normal 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 {}
|
||||
}
|
158
stubs/extensions/pdo.phpstub
Normal file
158
stubs/extensions/pdo.phpstub
Normal 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;
|
||||
}
|
@ -282,3 +282,30 @@ 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
|
||||
) {}
|
||||
}
|
||||
|
||||
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
|
||||
) {}
|
||||
}
|
@ -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()) {}
|
||||
}
|
@ -8,8 +8,6 @@ use Psalm\Exception\CodeException;
|
||||
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',
|
||||
'<?php
|
||||
@ -88,10 +82,6 @@ class BinaryOperationTest extends TestCase
|
||||
|
||||
public function testDecimalOperations(): void
|
||||
{
|
||||
if (!class_exists('Decimal\\Decimal')) {
|
||||
$this->markTestSkipped('Cannot run test, base class "Decimal\\Decimal" does not exist!');
|
||||
}
|
||||
|
||||
$this->addFile(
|
||||
'somefile.php',
|
||||
'<?php
|
||||
|
@ -5,8 +5,6 @@ namespace Psalm\Tests;
|
||||
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
|
||||
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
|
||||
|
||||
use function class_exists;
|
||||
|
||||
class ClassTest extends TestCase
|
||||
{
|
||||
use InvalidCodeAnalysisTestTrait;
|
||||
@ -14,10 +12,6 @@ class ClassTest extends TestCase
|
||||
|
||||
public function testExtendsMysqli(): void
|
||||
{
|
||||
if (class_exists('mysqli') === false) {
|
||||
$this->markTestSkipped('Cannot run test, base class "mysqli" does not exist!');
|
||||
}
|
||||
|
||||
$this->addFile(
|
||||
'somefile.php',
|
||||
'<?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;
|
||||
@ -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 $contents
|
||||
|
@ -6,8 +6,6 @@ use Psalm\Context;
|
||||
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
|
||||
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
|
||||
|
||||
use function class_exists;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
|
||||
class MethodCallTest extends TestCase
|
||||
@ -17,10 +15,6 @@ class MethodCallTest extends TestCase
|
||||
|
||||
public function testExtendDocblockParamType(): void
|
||||
{
|
||||
if (class_exists('SoapClient') === false) {
|
||||
$this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!');
|
||||
}
|
||||
|
||||
$this->addFile(
|
||||
'somefile.php',
|
||||
'<?php
|
||||
|
@ -7,8 +7,6 @@ use Psalm\Exception\CodeException;
|
||||
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
|
||||
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
|
||||
|
||||
use function class_exists;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
|
||||
class MethodSignatureTest extends TestCase
|
||||
@ -18,10 +16,6 @@ class MethodSignatureTest extends TestCase
|
||||
|
||||
public function testExtendSoapClientWithDocblockTypes(): void
|
||||
{
|
||||
if (class_exists('SoapClient') === false) {
|
||||
$this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!');
|
||||
}
|
||||
|
||||
$this->addFile(
|
||||
'somefile.php',
|
||||
'<?php
|
||||
@ -52,10 +46,6 @@ class MethodSignatureTest extends TestCase
|
||||
|
||||
public function testExtendSoapClientWithNoDocblockTypes(): void
|
||||
{
|
||||
if (class_exists('SoapClient') === false) {
|
||||
$this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!');
|
||||
}
|
||||
|
||||
$this->addFile(
|
||||
'somefile.php',
|
||||
'<?php
|
||||
@ -78,10 +68,6 @@ class MethodSignatureTest extends TestCase
|
||||
|
||||
public function testExtendSoapClientWithParamType(): void
|
||||
{
|
||||
if (class_exists('SoapClient') === false) {
|
||||
$this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!');
|
||||
}
|
||||
|
||||
$this->addFile(
|
||||
'somefile.php',
|
||||
'<?php
|
||||
@ -249,9 +235,6 @@ class MethodSignatureTest extends TestCase
|
||||
{
|
||||
$this->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 @@ class MethodSignatureTest extends TestCase
|
||||
$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
|
||||
|
@ -20,6 +20,10 @@ class TestConfig extends Config
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
foreach ($this->php_extensions as $ext => $_enabled) {
|
||||
$this->php_extensions[$ext] = true;
|
||||
}
|
||||
|
||||
$this->throw_exception = true;
|
||||
$this->use_docblock_types = true;
|
||||
$this->level = 1;
|
||||
|
Loading…
Reference in New Issue
Block a user