From 4c656f0a2b2e577a4910047198de39199b0497bb Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sat, 21 Oct 2023 20:50:08 +0200 Subject: [PATCH 001/357] Revert --- .../Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php index 05bfb3fc387..b90ffbc387d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php @@ -180,7 +180,7 @@ public static function analyze( if ($literal_concat) { // Bypass opcache bug: https://github.com/php/php-src/issues/10635 - (static function (int $_): void { + (function (int $_): void { })($combinations); if (count($result_type_parts) === 0) { throw new AssertionError("The number of parts cannot be 0!"); From e8b7b30043c55e878653fb7fdded656fa5d2fa7c Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 22 Oct 2023 20:11:28 +0200 Subject: [PATCH 002/357] Fixes --- UPGRADING.md | 8 ++++ src/Psalm/Config.php | 43 +++++++++---------- src/Psalm/Plugin/ArgTypeInferer.php | 15 +++---- src/Psalm/Report/CodeClimateReport.php | 6 +-- src/Psalm/Type/Atomic/TTypeAlias.php | 57 ++------------------------ tests/EnumTest.php | 2 +- tests/PsalmPluginTest.php | 4 +- tests/ReportOutputTest.php | 20 ++++----- tests/TestConfig.php | 3 +- 9 files changed, 58 insertions(+), 100 deletions(-) diff --git a/UPGRADING.md b/UPGRADING.md index 55cb4f65119..2bf88c584a4 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -44,6 +44,14 @@ - [BC] `Psalm\CodeLocation\Raw`, `Psalm\CodeLocation\ParseErrorLocation`, `Psalm\CodeLocation\DocblockTypeLocation`, `Psalm\Report\CountReport`, `Psalm\Type\Atomic\TNonEmptyArray` are now all final. +- [BC] `Psalm\Config` is now final. + +- [BC] The return type of `Psalm\Plugin\ArgTypeInferer::infer` changed from `Union|false` to `Union|null` + +- [BC] The `extra_types` property and `setIntersectionTypes` method of `Psalm\Type\Atomic\TTypeAlias` were removed. + +- [BC] Methods `convertSeverity` and `calculateFingerprint` of `Psalm\Report\CodeClimateReport` were removed. + # Upgrading from Psalm 4 to Psalm 5 ## Changed diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index d86d787c73b..8759c0ddbcd 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -69,7 +69,6 @@ use function flock; use function fopen; use function function_exists; -use function get_class; use function get_defined_constants; use function get_defined_functions; use function getcwd; @@ -98,7 +97,9 @@ use function scandir; use function sha1; use function simplexml_import_dom; +use function str_contains; use function str_replace; +use function str_starts_with; use function strlen; use function strpos; use function strrpos; @@ -127,13 +128,13 @@ * @psalm-suppress PropertyNotSetInConstructor * @psalm-consistent-constructor */ -class Config +final class Config { private const DEFAULT_FILE_NAME = 'psalm.xml'; - public const CONFIG_NAMESPACE = 'https://getpsalm.org/schema/config'; - public const REPORT_INFO = 'info'; - public const REPORT_ERROR = 'error'; - public const REPORT_SUPPRESS = 'suppress'; + final public const CONFIG_NAMESPACE = 'https://getpsalm.org/schema/config'; + final public const REPORT_INFO = 'info'; + final public const REPORT_ERROR = 'error'; + final public const REPORT_SUPPRESS = 'suppress'; /** * @var array @@ -172,7 +173,7 @@ class Config * * @var array */ - protected array $universal_object_crates; + private array $universal_object_crates; /** * @var static|null @@ -222,7 +223,7 @@ class Config protected ?ProjectFileFilter $project_files = null; - protected ?ProjectFileFilter $extra_files = null; + private ?ProjectFileFilter $extra_files = null; /** * The base directory of this config file @@ -426,7 +427,7 @@ class Config private ?IncludeCollector $include_collector = null; - protected ?TaintAnalysisFileFilter $taint_analysis_ignored_files = null; + private ?TaintAnalysisFileFilter $taint_analysis_ignored_files = null; /** * @var bool whether to emit a backtrace of emitted issues to stderr @@ -874,7 +875,6 @@ private static function processConfigDeprecations( /** * @param non-empty-string $file_contents * @psalm-suppress MixedAssignment - * @psalm-suppress MixedArgument * @psalm-suppress MixedPropertyFetch * @throws ConfigException */ @@ -963,15 +963,15 @@ private static function fromXmlAndPaths( if (file_exists($composer_json_path)) { $composer_json_contents = file_get_contents($composer_json_path); assert($composer_json_contents !== false); - $composer_json = json_decode($composer_json_contents, true); + $composer_json = json_decode($composer_json_contents, true, 512, JSON_THROW_ON_ERROR); if (!is_array($composer_json)) { throw new UnexpectedValueException('Invalid composer.json at ' . $composer_json_path); } } $required_extensions = []; foreach (($composer_json["require"] ?? []) as $required => $_) { - if (strpos($required, "ext-") === 0) { - $required_extensions[strtolower(substr($required, 4))] = true; + if (str_starts_with((string) $required, "ext-")) { + $required_extensions[strtolower(substr((string) $required, 4))] = true; } } foreach ($required_extensions as $required_ext => $_) { @@ -1649,7 +1649,7 @@ public function reportIssueInFile(string $issue_type, string $file_path): bool try { $file_storage = $codebase->file_storage_provider->get($file_path); $dependent_files += $file_storage->required_by_file_paths; - } catch (InvalidArgumentException $e) { + } catch (InvalidArgumentException) { // do nothing } } @@ -1700,7 +1700,7 @@ public function trackTaintsInPath(string $file_path): bool public function getReportingLevelForIssue(CodeIssue $e): string { - $fqcn_parts = explode('\\', get_class($e)); + $fqcn_parts = explode('\\', $e::class); $issue_type = array_pop($fqcn_parts); $reporting_level = null; @@ -1765,17 +1765,17 @@ public static function getParentIssueType(string $issue_type): ?string return null; } - if (strpos($issue_type, 'Possibly') === 0) { + if (str_starts_with($issue_type, 'Possibly')) { $stripped_issue_type = (string) preg_replace('/^Possibly(False|Null)?/', '', $issue_type, 1); - if (strpos($stripped_issue_type, 'Invalid') === false && strpos($stripped_issue_type, 'Un') !== 0) { + if (!str_contains($stripped_issue_type, 'Invalid') && !str_starts_with($stripped_issue_type, 'Un')) { $stripped_issue_type = 'Invalid' . $stripped_issue_type; } return $stripped_issue_type; } - if (strpos($issue_type, 'Tainted') === 0) { + if (str_starts_with($issue_type, 'Tainted')) { return 'TaintedInput'; } @@ -2298,7 +2298,7 @@ public function visitComposerAutoloadFiles(ProjectAnalyzer $project_analyzer, ?P $codebase->classlikes->forgetMissingClassLikes(); $this->include_collector->runAndCollect( - [$this, 'requireAutoloader'], + $this->requireAutoloader(...), ); } @@ -2324,7 +2324,8 @@ public function visitComposerAutoloadFiles(ProjectAnalyzer $project_analyzer, ?P } } - public function getComposerFilePathForClassLike(string $fq_classlike_name): string|false + /** @return string|false */ + public function getComposerFilePathForClassLike(string $fq_classlike_name): string|bool { if (!$this->composer_class_loader) { return false; @@ -2502,7 +2503,7 @@ public function getPHPVersionFromComposerJson(): ?string $composer_json_contents = file_get_contents($composer_json_path); assert($composer_json_contents !== false); $composer_json = json_decode($composer_json_contents, true, 512, JSON_THROW_ON_ERROR); - } catch (JsonException $e) { + } catch (JsonException) { $composer_json = null; } diff --git a/src/Psalm/Plugin/ArgTypeInferer.php b/src/Psalm/Plugin/ArgTypeInferer.php index 0347eead24a..109769c8024 100644 --- a/src/Psalm/Plugin/ArgTypeInferer.php +++ b/src/Psalm/Plugin/ArgTypeInferer.php @@ -13,19 +13,16 @@ final class ArgTypeInferer { - private Context $context; - private StatementsAnalyzer $statements_analyzer; - /** * @internal */ - public function __construct(Context $context, StatementsAnalyzer $statements_analyzer) - { - $this->context = $context; - $this->statements_analyzer = $statements_analyzer; + public function __construct( + private readonly Context $context, + private readonly StatementsAnalyzer $statements_analyzer, + ) { } - public function infer(PhpParser\Node\Arg $arg): false|Union + public function infer(PhpParser\Node\Arg $arg): null|Union { $already_inferred_type = $this->statements_analyzer->node_data->getType($arg->value); @@ -34,7 +31,7 @@ public function infer(PhpParser\Node\Arg $arg): false|Union } if (ExpressionAnalyzer::analyze($this->statements_analyzer, $arg->value, $this->context) === false) { - return false; + return null; } return $this->statements_analyzer->node_data->getType($arg->value) ?? Type::getMixed(); diff --git a/src/Psalm/Report/CodeClimateReport.php b/src/Psalm/Report/CodeClimateReport.php index fb0cbf28689..61027d6371b 100644 --- a/src/Psalm/Report/CodeClimateReport.php +++ b/src/Psalm/Report/CodeClimateReport.php @@ -28,7 +28,7 @@ public function create(): string $options = $this->pretty ? Json::PRETTY : Json::DEFAULT; $issues_data = array_map( - [$this, 'mapToNewStructure'], + $this->mapToNewStructure(...), $this->issues_data, ); @@ -39,7 +39,7 @@ public function create(): string * convert our own severity to CodeClimate format * Values can be : info, minor, major, critical, or blocker */ - protected function convertSeverity(string $input): string + private function convertSeverity(string $input): string { if (Config::REPORT_INFO === $input) { return 'info'; @@ -58,7 +58,7 @@ protected function convertSeverity(string $input): string /** * calculate a unique fingerprint for a given issue */ - protected function calculateFingerprint(IssueData $issue): string + private function calculateFingerprint(IssueData $issue): string { return md5($issue->type.$issue->message.$issue->file_name.$issue->from.$issue->to); } diff --git a/src/Psalm/Type/Atomic/TTypeAlias.php b/src/Psalm/Type/Atomic/TTypeAlias.php index 52a2fe9037a..ebda8e4118b 100644 --- a/src/Psalm/Type/Atomic/TTypeAlias.php +++ b/src/Psalm/Type/Atomic/TTypeAlias.php @@ -6,55 +6,17 @@ use Psalm\Type\Atomic; -use function array_map; -use function implode; - /** * @psalm-immutable */ final class TTypeAlias extends Atomic { - /** - * @var array|null - * @deprecated type aliases are resolved within {@see TypeParser::resolveTypeAliases()} and therefore the - * referencing type(s) are part of other intersection types. The intersection types are not set anymore - * and with v6 this property along with its related methods will get removed. - */ - public ?array $extra_types = null; - - public string $declaring_fq_classlike_name; - - public string $alias_name; - - /** - * @param array|null $extra_types - */ - public function __construct(string $declaring_fq_classlike_name, string $alias_name, ?array $extra_types = null) - { - $this->declaring_fq_classlike_name = $declaring_fq_classlike_name; - $this->alias_name = $alias_name; - /** @psalm-suppress DeprecatedProperty For backwards compatibility, we have to keep this here. */ - $this->extra_types = $extra_types; + public function __construct( + public string $declaring_fq_classlike_name, + public string $alias_name, + ) { parent::__construct(true); } - /** - * @param array|null $extra_types - * @deprecated type aliases are resolved within {@see TypeParser::resolveTypeAliases()} and therefore the - * referencing type(s) are part of other intersection types. This method will get removed with v6. - * @psalm-suppress PossiblyUnusedMethod For backwards compatibility, we have to keep this here. - */ - public function setIntersectionTypes(?array $extra_types): self - { - /** @psalm-suppress DeprecatedProperty For backwards compatibility, we have to keep this here. */ - if ($extra_types === $this->extra_types) { - return $this; - } - return new self( - $this->declaring_fq_classlike_name, - $this->alias_name, - $extra_types, - ); - } public function getKey(bool $include_extra = true): string { @@ -63,17 +25,6 @@ public function getKey(bool $include_extra = true): string public function getId(bool $exact = true, bool $nested = false): string { - /** @psalm-suppress DeprecatedProperty For backwards compatibility, we have to keep this here. */ - if ($this->extra_types) { - return $this->getKey() . '&' . implode( - '&', - array_map( - static fn(Atomic $type): string => $type->getId($exact, true), - $this->extra_types, - ), - ); - } - return $this->getKey(); } diff --git a/tests/EnumTest.php b/tests/EnumTest.php index 4015c3f7735..ebc67a05e1e 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -632,7 +632,7 @@ function noop(string $s): string $foo = FooEnum::Foo->value; noop($foo); noop(FooEnum::Foo->value); - PHP, + PHP, 'assertions' => [], 'ignored_issues' => [], 'php_version' => '8.1', diff --git a/tests/PsalmPluginTest.php b/tests/PsalmPluginTest.php index 64311a30122..5b97dc23136 100644 --- a/tests/PsalmPluginTest.php +++ b/tests/PsalmPluginTest.php @@ -25,9 +25,9 @@ class PsalmPluginTest extends TestCase { use MockeryPHPUnitIntegration; - private PluginList&MockInterface $plugin_list; + private MockInterface $plugin_list; - private PluginListFactory&MockInterface $plugin_list_factory; + private MockInterface $plugin_list_factory; private Application $app; diff --git a/tests/ReportOutputTest.php b/tests/ReportOutputTest.php index bee44b19d37..c17370a8106 100644 --- a/tests/ReportOutputTest.php +++ b/tests/ReportOutputTest.php @@ -712,6 +712,7 @@ public function testJsonReport(): void $issue_data = [ [ + 'link' => 'https://psalm.dev/024', 'severity' => 'error', 'line_from' => 3, 'line_to' => 3, @@ -727,13 +728,13 @@ public function testJsonReport(): void 'snippet_to' => 83, 'column_from' => 10, 'column_to' => 26, - 'error_level' => -1, 'shortcode' => 24, - 'link' => 'https://psalm.dev/024', + 'error_level' => -1, 'taint_trace' => null, 'other_references' => null, ], [ + 'link' => 'https://psalm.dev/138', 'severity' => 'error', 'line_from' => 3, 'line_to' => 3, @@ -749,13 +750,13 @@ public function testJsonReport(): void 'snippet_to' => 83, 'column_from' => 10, 'column_to' => 26, - 'error_level' => 1, 'shortcode' => 138, - 'link' => 'https://psalm.dev/138', + 'error_level' => 1, 'taint_trace' => null, 'other_references' => null, ], [ + 'link' => 'https://psalm.dev/047', 'severity' => 'error', 'line_from' => 2, 'line_to' => 2, @@ -771,13 +772,13 @@ public function testJsonReport(): void 'snippet_to' => 56, 'column_from' => 42, 'column_to' => 49, - 'error_level' => 1, 'shortcode' => 47, - 'link' => 'https://psalm.dev/047', + 'error_level' => 1, 'taint_trace' => null, 'other_references' => null, ], [ + 'link' => 'https://psalm.dev/020', 'severity' => 'error', 'line_from' => 8, 'line_to' => 8, @@ -793,13 +794,13 @@ public function testJsonReport(): void 'snippet_to' => 172, 'column_from' => 6, 'column_to' => 15, - 'error_level' => -1, 'shortcode' => 20, - 'link' => 'https://psalm.dev/020', + 'error_level' => -1, 'taint_trace' => null, 'other_references' => null, ], [ + 'link' => 'https://psalm.dev/126', 'severity' => 'info', 'line_from' => 17, 'line_to' => 17, @@ -815,9 +816,8 @@ public function testJsonReport(): void 'snippet_to' => 277, 'column_from' => 6, 'column_to' => 8, - 'error_level' => 3, 'shortcode' => 126, - 'link' => 'https://psalm.dev/126', + 'error_level' => 3, 'taint_trace' => null, 'other_references' => null, ], diff --git a/tests/TestConfig.php b/tests/TestConfig.php index 572deabbce6..87433cc67c6 100644 --- a/tests/TestConfig.php +++ b/tests/TestConfig.php @@ -61,7 +61,8 @@ protected function getContents(): string '; } - public function getComposerFilePathForClassLike(string $fq_classlike_name): string|false + /** @return false */ + public function getComposerFilePathForClassLike(string $fq_classlike_name): bool { return false; } From 292ed063233e29d59d0bde61bb6daedee94d55bb Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 22 Oct 2023 20:13:31 +0200 Subject: [PATCH 003/357] Fix --- composer.json | 1 + phpunit.xml.dist | 2 +- src/Psalm/Internal/Type/TypeExpander.php | 22 ---------------------- tests/autoload.php | 9 +++++++++ 4 files changed, 11 insertions(+), 23 deletions(-) create mode 100644 tests/autoload.php diff --git a/composer.json b/composer.json index 0a56c517c06..0a36e057c1f 100644 --- a/composer.json +++ b/composer.json @@ -50,6 +50,7 @@ "amphp/phpunit-util": "^3", "bamarni/composer-bin-plugin": "^1.4", "brianium/paratest": "^6.9", + "dg/bypass-finals": "^1.5", "mockery/mockery": "^1.5", "nunomaduro/mock-final-classes": "^1.1", "php-parallel-lint/php-parallel-lint": "^1.2", diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 748be83439b..4f2c25cff79 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,7 @@ extra_types ?? [] as $alias) { - $more_recursively_fleshed_out_types = self::expandAtomic( - $codebase, - $alias, - $self_class, - $static_class_type, - $parent_class, - $evaluate_class_constants, - $evaluate_conditional_types, - $final, - $expand_generic, - $expand_templates, - $throw_on_unresolvable_constant, - ); - - $recursively_fleshed_out_types = [ - ...$more_recursively_fleshed_out_types, - ...$recursively_fleshed_out_types, - ]; - } - return $recursively_fleshed_out_types; } diff --git a/tests/autoload.php b/tests/autoload.php new file mode 100644 index 00000000000..449bca68efa --- /dev/null +++ b/tests/autoload.php @@ -0,0 +1,9 @@ + Date: Sun, 22 Oct 2023 20:17:39 +0200 Subject: [PATCH 004/357] cs-fix --- psalm-baseline.xml | 15 ++++++++++++++- src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php | 2 +- src/Psalm/Internal/Type/TypeParser.php | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index d04ba234885..abd3dbf4b71 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + tags['variablesfrom'][0]]]> @@ -16,6 +16,9 @@ $deprecated_element_xml + + $this + @@ -622,6 +625,16 @@ hasLowercaseString + + + Config + + + public function __construct() + public function getComposerFilePathForClassLike(string $fq_classlike_name): bool + public function getProjectDirectories(): array + + diff --git a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php index a6bff08e5b7..2e89585d8f6 100644 --- a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php @@ -206,7 +206,7 @@ public static function checkFullyQualifiedClassLikeName( ?string $calling_method_id, array $suppressed_issues, ?ClassLikeNameOptions $options = null, - bool $check_classes = true + bool $check_classes = true, ): ?bool { if ($options === null) { $options = new ClassLikeNameOptions(); diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 5063728eaa8..5b3bb664fe7 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -962,7 +962,7 @@ private static function getTypeFromGenericTree( $get_int_range_bound = static function ( ParseTree $parse_tree, Union $generic_param, - string $bound_name + string $bound_name, ): ?int { if (!$parse_tree instanceof Value || count($generic_param->getAtomicTypes()) > 1 From e72fb5a2b31e606abd525f867696c5ba5bf7451b Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 22 Oct 2023 20:22:01 +0200 Subject: [PATCH 005/357] Fix --- tests/ReportOutputTest.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/ReportOutputTest.php b/tests/ReportOutputTest.php index c17370a8106..bee44b19d37 100644 --- a/tests/ReportOutputTest.php +++ b/tests/ReportOutputTest.php @@ -712,7 +712,6 @@ public function testJsonReport(): void $issue_data = [ [ - 'link' => 'https://psalm.dev/024', 'severity' => 'error', 'line_from' => 3, 'line_to' => 3, @@ -728,13 +727,13 @@ public function testJsonReport(): void 'snippet_to' => 83, 'column_from' => 10, 'column_to' => 26, - 'shortcode' => 24, 'error_level' => -1, + 'shortcode' => 24, + 'link' => 'https://psalm.dev/024', 'taint_trace' => null, 'other_references' => null, ], [ - 'link' => 'https://psalm.dev/138', 'severity' => 'error', 'line_from' => 3, 'line_to' => 3, @@ -750,13 +749,13 @@ public function testJsonReport(): void 'snippet_to' => 83, 'column_from' => 10, 'column_to' => 26, - 'shortcode' => 138, 'error_level' => 1, + 'shortcode' => 138, + 'link' => 'https://psalm.dev/138', 'taint_trace' => null, 'other_references' => null, ], [ - 'link' => 'https://psalm.dev/047', 'severity' => 'error', 'line_from' => 2, 'line_to' => 2, @@ -772,13 +771,13 @@ public function testJsonReport(): void 'snippet_to' => 56, 'column_from' => 42, 'column_to' => 49, - 'shortcode' => 47, 'error_level' => 1, + 'shortcode' => 47, + 'link' => 'https://psalm.dev/047', 'taint_trace' => null, 'other_references' => null, ], [ - 'link' => 'https://psalm.dev/020', 'severity' => 'error', 'line_from' => 8, 'line_to' => 8, @@ -794,13 +793,13 @@ public function testJsonReport(): void 'snippet_to' => 172, 'column_from' => 6, 'column_to' => 15, - 'shortcode' => 20, 'error_level' => -1, + 'shortcode' => 20, + 'link' => 'https://psalm.dev/020', 'taint_trace' => null, 'other_references' => null, ], [ - 'link' => 'https://psalm.dev/126', 'severity' => 'info', 'line_from' => 17, 'line_to' => 17, @@ -816,8 +815,9 @@ public function testJsonReport(): void 'snippet_to' => 277, 'column_from' => 6, 'column_to' => 8, - 'shortcode' => 126, 'error_level' => 3, + 'shortcode' => 126, + 'link' => 'https://psalm.dev/126', 'taint_trace' => null, 'other_references' => null, ], From fa97e6ddf53683182aea20971f6e745838289f5b Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 22 Oct 2023 21:26:19 +0200 Subject: [PATCH 006/357] Fix --- .../Internal/FileManipulation/FunctionDocblockManipulator.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php index 091e13a3e14..3004060d044 100644 --- a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php +++ b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php @@ -45,7 +45,6 @@ final class FunctionDocblockManipulator private static array $manipulators = []; private readonly int $docblock_start; - private Closure|Function_|ClassMethod|ArrowFunction $stmt; private readonly int $docblock_end; From dcec7ac82cee0498bb9523119eb89981c9144960 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 07:36:44 +0000 Subject: [PATCH 007/357] Bump fkirc/skip-duplicate-actions from 5.3.0 to 5.3.1 Bumps [fkirc/skip-duplicate-actions](https://github.com/fkirc/skip-duplicate-actions) from 5.3.0 to 5.3.1. - [Release notes](https://github.com/fkirc/skip-duplicate-actions/releases) - [Commits](https://github.com/fkirc/skip-duplicate-actions/compare/v5.3.0...v5.3.1) --- updated-dependencies: - dependency-name: fkirc/skip-duplicate-actions dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/build-phar.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-phar.yml b/.github/workflows/build-phar.yml index 4f658298a50..68bc63ce139 100644 --- a/.github/workflows/build-phar.yml +++ b/.github/workflows/build-phar.yml @@ -20,7 +20,7 @@ jobs: should_skip: ${{ steps.skip_check.outputs.should_skip }} steps: - id: skip_check - uses: fkirc/skip-duplicate-actions@v5.3.0 + uses: fkirc/skip-duplicate-actions@v5.3.1 with: concurrent_skipping: always cancel_others: true From a375f441f2872f03d0a1bd4be49f8bcc3b9e60b2 Mon Sep 17 00:00:00 2001 From: Daniel Linjama Date: Mon, 23 Oct 2023 10:21:04 +0300 Subject: [PATCH 008/357] fix @param-out with named arguments --- .../Expression/Call/ArgumentsAnalyzer.php | 22 ++++++++++++++++++- tests/ReferenceConstraintTest.php | 18 +++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 653ffedc9ac..d166ed3774c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -51,6 +51,7 @@ use UnexpectedValueException; use function array_map; +use function array_reduce; use function array_reverse; use function array_slice; use function array_values; @@ -1031,7 +1032,26 @@ private static function handlePossiblyMatchingByRefParam( $check_null_ref = true; if ($last_param) { - if ($argument_offset < count($function_params)) { + if ($arg->name !== null) { + $function_param = array_reduce( + $function_params, + static function ( + ?FunctionLikeParameter $function_param, + FunctionLikeParameter $param + ) use ( + $arg + ) { + if ($param->name === $arg->name->name) { + return $param; + } + return $function_param; + }, + null, + ); + if ($function_param === null) { + return false; + } + } elseif ($argument_offset < count($function_params)) { $function_param = $function_params[$argument_offset]; } else { $function_param = $last_param; diff --git a/tests/ReferenceConstraintTest.php b/tests/ReferenceConstraintTest.php index 1c6dd4c6aeb..bf1f5d2e43f 100644 --- a/tests/ReferenceConstraintTest.php +++ b/tests/ReferenceConstraintTest.php @@ -193,6 +193,24 @@ function takesNullableObj(?A &$a): bool { return true; } if ($a) {}', ], + 'PHP80-paramOutChangeTypeWithNamedArgument' => [ + 'code' => ' [ + '$a' => 'int', + ], + ], ]; } From 6f32c723f52547dbfb42ffd9d89e704189c080d8 Mon Sep 17 00:00:00 2001 From: Simon Berger Date: Tue, 24 Oct 2023 15:03:39 +0200 Subject: [PATCH 009/357] Narrow down `get_browser` returning array or object based on `$return_array` param --- stubs/CoreGenericFunctions.phpstub | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/stubs/CoreGenericFunctions.phpstub b/stubs/CoreGenericFunctions.phpstub index 793150fbdfa..e793b13d22b 100644 --- a/stubs/CoreGenericFunctions.phpstub +++ b/stubs/CoreGenericFunctions.phpstub @@ -1797,3 +1797,9 @@ if (defined('GLOB_BRACE')) { * @psalm-taint-sink shell $command */ function exec(string $command, &$output = null, int &$result_code = null): string|false {} + +/** +* @return ($return_array is true ? array|false : object|false) +* @psalm-ignore-falsable-return +*/ +function get_browser(?string $user_agent = null, bool $return_array = false): object|array|false {} From 6044cc702c4d8d7ab5e6e904c89c8ad5b4665391 Mon Sep 17 00:00:00 2001 From: RobChett Date: Sun, 14 May 2023 12:27:47 +0100 Subject: [PATCH 010/357] Combining a array value empty list with a non-empty list was returning a non-empty-list --- src/Psalm/Internal/Type/TypeCombiner.php | 2 +- tests/ArrayAssignmentTest.php | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 7bad564408c..67850a2846b 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -1516,7 +1516,7 @@ private static function getArrayTypeFromGenericParams( $generic_type_params[1], $objectlike_generic_type, $codebase, - $overwrite_empty_array, + false, $allow_mixed_union, ); } diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 91a319f2fb3..d7651949d58 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -2048,6 +2048,15 @@ function getQueryParams(): array return $queryParams; }', ], + 'AssignListToNonEmptyList' => [ + 'code' => '> $l*/ + $l = []; + $l[] = [];', + 'assertions' => [ + '$l===' => 'non-empty-array>', + ], + ], ]; } From ec23f998ea38413cdea8e6c14644179b26d407b2 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 26 Oct 2023 17:00:29 +0200 Subject: [PATCH 011/357] Finalize all internal classes --- examples/TemplateChecker.php | 2 +- examples/TemplateScanner.php | 2 +- examples/plugins/ClassUnqualifier.php | 2 +- examples/plugins/FunctionCasingChecker.php | 4 ++-- examples/plugins/InternalChecker.php | 2 +- examples/plugins/PreventFloatAssignmentChecker.php | 4 ++-- examples/plugins/SafeArrayKeyChecker.php | 2 +- examples/plugins/StringChecker.php | 2 +- examples/plugins/composer-based/echo-checker/EchoChecker.php | 2 +- .../plugins/composer-based/echo-checker/PluginEntryPoint.php | 2 +- src/Psalm/Internal/Algebra.php | 2 +- src/Psalm/Internal/Algebra/FormulaGenerator.php | 2 +- src/Psalm/Internal/Analyzer/AlgebraAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/AttributesAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/ClassLikeNameOptions.php | 2 +- src/Psalm/Internal/Analyzer/ClosureAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/CommentAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/DataFlowNodeData.php | 2 +- src/Psalm/Internal/Analyzer/FunctionAnalyzer.php | 2 +- .../Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php | 2 +- .../Internal/Analyzer/FunctionLike/ReturnTypeCollector.php | 2 +- src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/IssueData.php | 2 +- src/Psalm/Internal/Analyzer/MethodAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/MethodComparator.php | 2 +- src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/ProjectAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/ScopeAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/Statements/Block/DoAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/Statements/Block/ForAnalyzer.php | 2 +- .../Internal/Analyzer/Statements/Block/ForeachAnalyzer.php | 2 +- .../Analyzer/Statements/Block/IfConditionalAnalyzer.php | 2 +- .../Analyzer/Statements/Block/IfElse/ElseAnalyzer.php | 2 +- .../Analyzer/Statements/Block/IfElse/ElseIfAnalyzer.php | 2 +- .../Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php | 2 +- .../Internal/Analyzer/Statements/Block/IfElseAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php | 2 +- .../Internal/Analyzer/Statements/Block/SwitchAnalyzer.php | 2 +- .../Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php | 2 +- .../Internal/Analyzer/Statements/Block/WhileAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/Statements/BreakAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/Statements/ContinueAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php | 2 +- .../Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php | 2 +- .../Analyzer/Statements/Expression/ArrayCreationInfo.php | 2 +- .../Analyzer/Statements/Expression/AssertionFinder.php | 2 +- .../Expression/Assignment/ArrayAssignmentAnalyzer.php | 2 +- .../Statements/Expression/Assignment/AssignedProperty.php | 2 +- .../Assignment/InstancePropertyAssignmentAnalyzer.php | 2 +- .../Assignment/StaticPropertyAssignmentAnalyzer.php | 2 +- .../Analyzer/Statements/Expression/AssignmentAnalyzer.php | 2 +- .../Analyzer/Statements/Expression/BinaryOp/AndAnalyzer.php | 2 +- .../Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php | 2 +- .../Statements/Expression/BinaryOp/CoalesceAnalyzer.php | 2 +- .../Statements/Expression/BinaryOp/ConcatAnalyzer.php | 2 +- .../Expression/BinaryOp/NonComparisonOpAnalyzer.php | 2 +- .../Analyzer/Statements/Expression/BinaryOp/OrAnalyzer.php | 2 +- .../Analyzer/Statements/Expression/BinaryOpAnalyzer.php | 2 +- .../Analyzer/Statements/Expression/BitwiseNotAnalyzer.php | 2 +- .../Analyzer/Statements/Expression/BooleanNotAnalyzer.php | 2 +- .../Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php | 2 +- .../Statements/Expression/Call/ArgumentMapPopulator.php | 2 +- .../Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php | 2 +- .../Expression/Call/ArrayFunctionArgumentsAnalyzer.php | 2 +- .../Expression/Call/ClassTemplateParamCollector.php | 2 +- .../Statements/Expression/Call/FunctionCallAnalyzer.php | 2 +- .../Analyzer/Statements/Expression/Call/FunctionCallInfo.php | 2 +- .../Expression/Call/FunctionCallReturnTypeFetcher.php | 2 +- .../Statements/Expression/Call/Method/AtomicCallContext.php | 2 +- .../Expression/Call/Method/AtomicMethodCallAnalysisResult.php | 2 +- .../Expression/Call/Method/AtomicMethodCallAnalyzer.php | 2 +- .../Call/Method/ExistingAtomicMethodCallAnalyzer.php | 2 +- .../Expression/Call/Method/MethodCallProhibitionAnalyzer.php | 2 +- .../Expression/Call/Method/MethodCallPurityAnalyzer.php | 2 +- .../Expression/Call/Method/MethodCallReturnTypeFetcher.php | 2 +- .../Expression/Call/Method/MethodVisibilityAnalyzer.php | 2 +- .../Expression/Call/Method/MissingMethodCallHandler.php | 2 +- .../Statements/Expression/Call/MethodCallAnalyzer.php | 2 +- .../Statements/Expression/Call/NamedFunctionCallHandler.php | 2 +- .../Analyzer/Statements/Expression/Call/NewAnalyzer.php | 2 +- .../Statements/Expression/Call/StaticCallAnalyzer.php | 2 +- .../Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php | 2 +- .../Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php | 2 +- .../Internal/Analyzer/Statements/Expression/CastAnalyzer.php | 2 +- .../Analyzer/Statements/Expression/ClassConstAnalyzer.php | 2 +- .../Internal/Analyzer/Statements/Expression/CloneAnalyzer.php | 2 +- .../Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php | 2 +- .../Statements/Expression/EncapsulatedStringAnalyzer.php | 2 +- .../Internal/Analyzer/Statements/Expression/EvalAnalyzer.php | 2 +- .../Internal/Analyzer/Statements/Expression/ExitAnalyzer.php | 2 +- .../Analyzer/Statements/Expression/ExpressionIdentifier.php | 2 +- .../Statements/Expression/Fetch/ArrayFetchAnalyzer.php | 2 +- .../Expression/Fetch/AtomicPropertyFetchAnalyzer.php | 2 +- .../Statements/Expression/Fetch/ConstFetchAnalyzer.php | 2 +- .../Expression/Fetch/InstancePropertyFetchAnalyzer.php | 2 +- .../Expression/Fetch/StaticPropertyFetchAnalyzer.php | 2 +- .../Statements/Expression/Fetch/VariableFetchAnalyzer.php | 2 +- .../Statements/Expression/IncDecExpressionAnalyzer.php | 2 +- .../Analyzer/Statements/Expression/IncludeAnalyzer.php | 2 +- .../Analyzer/Statements/Expression/InstanceofAnalyzer.php | 2 +- .../Internal/Analyzer/Statements/Expression/IssetAnalyzer.php | 2 +- .../Analyzer/Statements/Expression/MagicConstAnalyzer.php | 2 +- .../Internal/Analyzer/Statements/Expression/MatchAnalyzer.php | 2 +- .../Analyzer/Statements/Expression/NullsafeAnalyzer.php | 2 +- .../Internal/Analyzer/Statements/Expression/PrintAnalyzer.php | 2 +- .../Analyzer/Statements/Expression/SimpleTypeInferer.php | 2 +- .../Analyzer/Statements/Expression/TernaryAnalyzer.php | 2 +- .../Analyzer/Statements/Expression/UnaryPlusMinusAnalyzer.php | 2 +- .../Internal/Analyzer/Statements/Expression/YieldAnalyzer.php | 2 +- .../Analyzer/Statements/Expression/YieldFromAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/Statements/ThrowAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php | 2 +- .../Internal/Analyzer/Statements/UnusedAssignmentRemover.php | 2 +- src/Psalm/Internal/Analyzer/StatementsAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/TraitAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/TypeAnalyzer.php | 2 +- src/Psalm/Internal/Cache.php | 2 +- src/Psalm/Internal/Clause.php | 2 +- src/Psalm/Internal/Codebase/Analyzer.php | 2 +- src/Psalm/Internal/Codebase/ClassLikes.php | 2 +- src/Psalm/Internal/Codebase/ConstantTypeResolver.php | 2 +- src/Psalm/Internal/Codebase/Functions.php | 2 +- src/Psalm/Internal/Codebase/InternalCallMapHandler.php | 2 +- src/Psalm/Internal/Codebase/Methods.php | 2 +- src/Psalm/Internal/Codebase/Populator.php | 2 +- src/Psalm/Internal/Codebase/Properties.php | 2 +- src/Psalm/Internal/Codebase/PropertyMap.php | 2 +- src/Psalm/Internal/Codebase/ReferenceMapGenerator.php | 2 +- src/Psalm/Internal/Codebase/Reflection.php | 2 +- src/Psalm/Internal/Codebase/Scanner.php | 2 +- src/Psalm/Internal/Codebase/TaintFlowGraph.php | 2 +- src/Psalm/Internal/Codebase/VariableUseGraph.php | 2 +- src/Psalm/Internal/DataFlow/Path.php | 2 +- src/Psalm/Internal/DataFlow/TaintSink.php | 2 +- src/Psalm/Internal/DataFlow/TaintSource.php | 2 +- src/Psalm/Internal/Diff/ClassStatementsDiffer.php | 2 +- src/Psalm/Internal/Diff/DiffElem.php | 2 +- src/Psalm/Internal/Diff/FileDiffer.php | 2 +- src/Psalm/Internal/Diff/FileStatementsDiffer.php | 2 +- src/Psalm/Internal/Diff/NamespaceStatementsDiffer.php | 2 +- src/Psalm/Internal/EventDispatcher.php | 2 +- .../Internal/ExecutionEnvironment/BuildInfoCollector.php | 2 +- src/Psalm/Internal/ExecutionEnvironment/GitInfoCollector.php | 2 +- .../Internal/FileManipulation/ClassDocblockManipulator.php | 2 +- src/Psalm/Internal/FileManipulation/CodeMigration.php | 2 +- .../Internal/FileManipulation/FileManipulationBuffer.php | 2 +- .../Internal/FileManipulation/FunctionDocblockManipulator.php | 2 +- .../Internal/FileManipulation/PropertyDocblockManipulator.php | 2 +- src/Psalm/Internal/Fork/ForkProcessDoneMessage.php | 2 +- src/Psalm/Internal/Fork/ForkProcessErrorMessage.php | 2 +- src/Psalm/Internal/Fork/ForkTaskDoneMessage.php | 2 +- src/Psalm/Internal/Fork/Pool.php | 2 +- src/Psalm/Internal/Fork/PsalmRestarter.php | 2 +- src/Psalm/Internal/Json/Json.php | 2 +- src/Psalm/Internal/LanguageServer/Client/TextDocument.php | 2 +- src/Psalm/Internal/LanguageServer/Client/Workspace.php | 2 +- src/Psalm/Internal/LanguageServer/ClientConfiguration.php | 2 +- src/Psalm/Internal/LanguageServer/ClientHandler.php | 2 +- src/Psalm/Internal/LanguageServer/IdGenerator.php | 2 +- src/Psalm/Internal/LanguageServer/LanguageClient.php | 2 +- src/Psalm/Internal/LanguageServer/LanguageServer.php | 2 +- src/Psalm/Internal/LanguageServer/Message.php | 2 +- src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php | 2 +- src/Psalm/Internal/LanguageServer/Progress.php | 2 +- src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php | 2 +- src/Psalm/Internal/LanguageServer/ProtocolStreamWriter.php | 2 +- .../LanguageServer/Provider/ClassLikeStorageCacheProvider.php | 2 +- .../LanguageServer/Provider/FileReferenceCacheProvider.php | 2 +- .../LanguageServer/Provider/FileStorageCacheProvider.php | 2 +- .../Internal/LanguageServer/Provider/ParserCacheProvider.php | 2 +- .../Internal/LanguageServer/Provider/ProjectCacheProvider.php | 2 +- src/Psalm/Internal/LanguageServer/Reference.php | 2 +- src/Psalm/Internal/LanguageServer/Server/TextDocument.php | 2 +- src/Psalm/Internal/LanguageServer/Server/Workspace.php | 2 +- src/Psalm/Internal/MethodIdentifier.php | 2 +- src/Psalm/Internal/PhpTraverser/CustomTraverser.php | 2 +- src/Psalm/Internal/PhpVisitor/AssignmentMapVisitor.php | 2 +- src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php | 2 +- src/Psalm/Internal/PhpVisitor/CloningVisitor.php | 2 +- src/Psalm/Internal/PhpVisitor/ConditionCloningVisitor.php | 2 +- src/Psalm/Internal/PhpVisitor/NodeCleanerVisitor.php | 2 +- src/Psalm/Internal/PhpVisitor/NodeCounterVisitor.php | 2 +- src/Psalm/Internal/PhpVisitor/OffsetShifterVisitor.php | 2 +- src/Psalm/Internal/PhpVisitor/ParamReplacementVisitor.php | 2 +- src/Psalm/Internal/PhpVisitor/PartialParserVisitor.php | 2 +- src/Psalm/Internal/PhpVisitor/Reflector/AttributeResolver.php | 2 +- .../Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php | 2 +- .../Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php | 2 +- .../Internal/PhpVisitor/Reflector/ExpressionResolver.php | 2 +- src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php | 2 +- .../PhpVisitor/Reflector/FunctionLikeDocblockParser.php | 2 +- .../PhpVisitor/Reflector/FunctionLikeDocblockScanner.php | 2 +- .../Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php | 2 +- src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php | 2 +- src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php | 2 +- src/Psalm/Internal/PhpVisitor/ShortClosureVisitor.php | 2 +- src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php | 2 +- src/Psalm/Internal/PhpVisitor/TraitFinder.php | 2 +- src/Psalm/Internal/PhpVisitor/TypeMappingVisitor.php | 2 +- src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php | 2 +- src/Psalm/Internal/PluginManager/Command/DisableCommand.php | 2 +- src/Psalm/Internal/PluginManager/Command/EnableCommand.php | 2 +- src/Psalm/Internal/PluginManager/Command/ShowCommand.php | 2 +- src/Psalm/Internal/PluginManager/ComposerLock.php | 2 +- src/Psalm/Internal/PluginManager/ConfigFile.php | 2 +- src/Psalm/Internal/PluginManager/PluginList.php | 2 +- src/Psalm/Internal/PluginManager/PluginListFactory.php | 2 +- .../Internal/Provider/AddRemoveTaints/HtmlFunctionTainter.php | 2 +- src/Psalm/Internal/Provider/ClassLikeStorageProvider.php | 2 +- src/Psalm/Internal/Provider/FakeFileProvider.php | 2 +- src/Psalm/Internal/Provider/FileReferenceProvider.php | 2 +- src/Psalm/Internal/Provider/FileStorageProvider.php | 2 +- src/Psalm/Internal/Provider/FunctionExistenceProvider.php | 2 +- src/Psalm/Internal/Provider/FunctionParamsProvider.php | 2 +- src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php | 2 +- src/Psalm/Internal/Provider/MethodExistenceProvider.php | 2 +- src/Psalm/Internal/Provider/MethodParamsProvider.php | 2 +- src/Psalm/Internal/Provider/MethodReturnTypeProvider.php | 2 +- src/Psalm/Internal/Provider/MethodVisibilityProvider.php | 2 +- src/Psalm/Internal/Provider/NodeDataProvider.php | 2 +- src/Psalm/Internal/Provider/PropertyExistenceProvider.php | 2 +- src/Psalm/Internal/Provider/PropertyTypeProvider.php | 2 +- .../PropertyTypeProvider/DomDocumentPropertyTypeProvider.php | 2 +- src/Psalm/Internal/Provider/PropertyVisibilityProvider.php | 2 +- src/Psalm/Internal/Provider/Providers.php | 2 +- .../ReturnTypeProvider/ArrayChunkReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/ArrayColumnReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/ArrayCombineReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/ArrayFillKeysReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/ArrayFillReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/ArrayFilterReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/ArrayMapReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/ArrayMergeReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/ArrayPadReturnTypeProvider.php | 2 +- .../ArrayPointerAdjustmentReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/ArrayPopReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/ArrayRandReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/ArrayReduceReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/ArrayReverseReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/ArraySliceReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/ArraySpliceReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/BasenameReturnTypeProvider.php | 2 +- .../ClosureFromCallableReturnTypeProvider.php | 2 +- .../Provider/ReturnTypeProvider/DateReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/DateTimeModifyReturnTypeProvider.php | 2 +- .../Provider/ReturnTypeProvider/DirnameReturnTypeProvider.php | 2 +- .../Provider/ReturnTypeProvider/DomNodeAppendChild.php | 2 +- .../ReturnTypeProvider/FilterVarReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/FirstArgStringReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/GetClassMethodsReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php | 2 +- .../Provider/ReturnTypeProvider/HexdecReturnTypeProvider.php | 2 +- .../ImagickPixelColorReturnTypeProvider.php | 2 +- .../Provider/ReturnTypeProvider/InArrayReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/IteratorToArrayReturnTypeProvider.php | 2 +- .../MbInternalEncodingReturnTypeProvider.php | 2 +- .../Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php | 2 +- .../Provider/ReturnTypeProvider/MktimeReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/ParseUrlReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/PdoStatementReturnTypeProvider.php | 2 +- .../Provider/ReturnTypeProvider/PdoStatementSetFetchMode.php | 2 +- .../Provider/ReturnTypeProvider/PowReturnTypeProvider.php | 2 +- .../Provider/ReturnTypeProvider/RandReturnTypeProvider.php | 2 +- .../Provider/ReturnTypeProvider/RoundReturnTypeProvider.php | 2 +- .../Provider/ReturnTypeProvider/SprintfReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/StrReplaceReturnTypeProvider.php | 2 +- .../Provider/ReturnTypeProvider/StrTrReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/TriggerErrorReturnTypeProvider.php | 2 +- .../ReturnTypeProvider/VersionCompareReturnTypeProvider.php | 2 +- src/Psalm/Internal/Provider/StatementsProvider.php | 2 +- src/Psalm/Internal/ReferenceConstraint.php | 2 +- src/Psalm/Internal/Scanner/ClassLikeDocblockComment.php | 2 +- src/Psalm/Internal/Scanner/DocblockParser.php | 2 +- src/Psalm/Internal/Scanner/FunctionDocblockComment.php | 2 +- src/Psalm/Internal/Scanner/ParsedDocblock.php | 2 +- src/Psalm/Internal/Scanner/PhpStormMetaScanner.php | 2 +- .../Internal/Scanner/UnresolvedConstant/ArrayOffsetFetch.php | 2 +- src/Psalm/Internal/Scanner/UnresolvedConstant/ArraySpread.php | 2 +- src/Psalm/Internal/Scanner/UnresolvedConstant/ArrayValue.php | 2 +- .../Internal/Scanner/UnresolvedConstant/ClassConstant.php | 2 +- src/Psalm/Internal/Scanner/UnresolvedConstant/Constant.php | 2 +- .../Internal/Scanner/UnresolvedConstant/EnumNameFetch.php | 2 +- .../Internal/Scanner/UnresolvedConstant/EnumValueFetch.php | 2 +- .../Internal/Scanner/UnresolvedConstant/KeyValuePair.php | 2 +- src/Psalm/Internal/Scanner/UnresolvedConstant/ScalarValue.php | 2 +- .../Scanner/UnresolvedConstant/UnresolvedAdditionOp.php | 2 +- .../Scanner/UnresolvedConstant/UnresolvedBitwiseAnd.php | 2 +- .../Scanner/UnresolvedConstant/UnresolvedBitwiseOr.php | 2 +- .../Scanner/UnresolvedConstant/UnresolvedBitwiseXor.php | 2 +- .../Scanner/UnresolvedConstant/UnresolvedConcatOp.php | 2 +- .../Scanner/UnresolvedConstant/UnresolvedDivisionOp.php | 2 +- .../Scanner/UnresolvedConstant/UnresolvedMultiplicationOp.php | 2 +- .../Scanner/UnresolvedConstant/UnresolvedSubtractionOp.php | 2 +- .../Internal/Scanner/UnresolvedConstant/UnresolvedTernary.php | 2 +- src/Psalm/Internal/Scanner/VarDocblockComment.php | 2 +- src/Psalm/Internal/Scope/CaseScope.php | 2 +- src/Psalm/Internal/Scope/FinallyScope.php | 2 +- src/Psalm/Internal/Scope/IfConditionalScope.php | 2 +- src/Psalm/Internal/Scope/IfScope.php | 2 +- src/Psalm/Internal/Scope/LoopScope.php | 2 +- src/Psalm/Internal/Scope/SwitchScope.php | 2 +- src/Psalm/Internal/Stubs/Generator/ClassLikeStubGenerator.php | 2 +- src/Psalm/Internal/Stubs/Generator/StubsGenerator.php | 2 +- src/Psalm/Internal/Type/ArrayType.php | 2 +- src/Psalm/Internal/Type/AssertionReconciler.php | 2 +- src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php | 2 +- src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php | 2 +- src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php | 2 +- .../Internal/Type/Comparator/ClassLikeStringComparator.php | 2 +- src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php | 2 +- src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php | 2 +- src/Psalm/Internal/Type/Comparator/KeyedArrayComparator.php | 2 +- src/Psalm/Internal/Type/Comparator/ObjectComparator.php | 2 +- src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php | 2 +- src/Psalm/Internal/Type/Comparator/TypeComparisonResult.php | 2 +- src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php | 2 +- src/Psalm/Internal/Type/NegatedAssertionReconciler.php | 2 +- src/Psalm/Internal/Type/ParseTree/CallableParamTree.php | 2 +- src/Psalm/Internal/Type/ParseTree/CallableTree.php | 2 +- .../Internal/Type/ParseTree/CallableWithReturnTypeTree.php | 2 +- src/Psalm/Internal/Type/ParseTree/ConditionalTree.php | 2 +- src/Psalm/Internal/Type/ParseTree/EncapsulationTree.php | 2 +- src/Psalm/Internal/Type/ParseTree/FieldEllipsis.php | 2 +- src/Psalm/Internal/Type/ParseTree/GenericTree.php | 2 +- src/Psalm/Internal/Type/ParseTree/IndexedAccessTree.php | 2 +- src/Psalm/Internal/Type/ParseTree/IntersectionTree.php | 2 +- src/Psalm/Internal/Type/ParseTree/KeyedArrayPropertyTree.php | 2 +- src/Psalm/Internal/Type/ParseTree/KeyedArrayTree.php | 2 +- src/Psalm/Internal/Type/ParseTree/MethodParamTree.php | 2 +- src/Psalm/Internal/Type/ParseTree/MethodTree.php | 2 +- .../Internal/Type/ParseTree/MethodWithReturnTypeTree.php | 2 +- src/Psalm/Internal/Type/ParseTree/NullableTree.php | 2 +- src/Psalm/Internal/Type/ParseTree/Root.php | 2 +- src/Psalm/Internal/Type/ParseTree/TemplateAsTree.php | 2 +- src/Psalm/Internal/Type/ParseTree/TemplateIsTree.php | 2 +- src/Psalm/Internal/Type/ParseTree/UnionTree.php | 2 +- src/Psalm/Internal/Type/ParseTree/Value.php | 2 +- src/Psalm/Internal/Type/ParseTreeCreator.php | 2 +- src/Psalm/Internal/Type/SimpleAssertionReconciler.php | 2 +- src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php | 2 +- src/Psalm/Internal/Type/TemplateBound.php | 2 +- src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php | 2 +- src/Psalm/Internal/Type/TemplateResult.php | 2 +- src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php | 2 +- src/Psalm/Internal/Type/TypeAlias/ClassTypeAlias.php | 2 +- src/Psalm/Internal/Type/TypeAlias/InlineTypeAlias.php | 2 +- src/Psalm/Internal/Type/TypeAlias/LinkableTypeAlias.php | 2 +- src/Psalm/Internal/Type/TypeCombination.php | 2 +- src/Psalm/Internal/Type/TypeCombiner.php | 2 +- src/Psalm/Internal/Type/TypeExpander.php | 2 +- src/Psalm/Internal/Type/TypeParser.php | 2 +- src/Psalm/Internal/Type/TypeTokenizer.php | 2 +- .../Internal/TypeVisitor/CanContainObjectTypeVisitor.php | 2 +- src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php | 2 +- src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php | 2 +- src/Psalm/Internal/TypeVisitor/ContainsLiteralVisitor.php | 2 +- src/Psalm/Internal/TypeVisitor/ContainsStaticVisitor.php | 2 +- src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php | 2 +- src/Psalm/Internal/TypeVisitor/TemplateTypeCollector.php | 2 +- src/Psalm/Internal/TypeVisitor/TypeChecker.php | 2 +- src/Psalm/Internal/TypeVisitor/TypeLocalizer.php | 2 +- src/Psalm/Internal/TypeVisitor/TypeScanner.php | 2 +- 368 files changed, 370 insertions(+), 370 deletions(-) diff --git a/examples/TemplateChecker.php b/examples/TemplateChecker.php index 55e65d2300f..ddaaab2baab 100644 --- a/examples/TemplateChecker.php +++ b/examples/TemplateChecker.php @@ -30,7 +30,7 @@ use function strtolower; use function trim; -class TemplateAnalyzer extends Psalm\Internal\Analyzer\FileAnalyzer +final class TemplateAnalyzer extends Psalm\Internal\Analyzer\FileAnalyzer { const VIEW_CLASS = 'Your\\View\\Class'; diff --git a/examples/TemplateScanner.php b/examples/TemplateScanner.php index d177f80e6b8..70c37470ca0 100644 --- a/examples/TemplateScanner.php +++ b/examples/TemplateScanner.php @@ -14,7 +14,7 @@ use function preg_match; use function trim; -class TemplateScanner extends Psalm\Internal\Scanner\FileScanner +final class TemplateScanner extends Psalm\Internal\Scanner\FileScanner { const VIEW_CLASS = 'Your\\View\\Class'; diff --git a/examples/plugins/ClassUnqualifier.php b/examples/plugins/ClassUnqualifier.php index 57a9f86da08..2c84f051ef1 100644 --- a/examples/plugins/ClassUnqualifier.php +++ b/examples/plugins/ClassUnqualifier.php @@ -12,7 +12,7 @@ use function strpos; use function strtolower; -class ClassUnqualifier implements AfterClassLikeExistenceCheckInterface +final class ClassUnqualifier implements AfterClassLikeExistenceCheckInterface { public static function afterClassLikeExistenceCheck( AfterClassLikeExistenceCheckEvent $event diff --git a/examples/plugins/FunctionCasingChecker.php b/examples/plugins/FunctionCasingChecker.php index 50cfc7358ff..f4147ab3f3f 100644 --- a/examples/plugins/FunctionCasingChecker.php +++ b/examples/plugins/FunctionCasingChecker.php @@ -21,7 +21,7 @@ /** * Checks that functions and methods are correctly-cased */ -class FunctionCasingChecker implements AfterFunctionCallAnalysisInterface, AfterMethodCallAnalysisInterface +final class FunctionCasingChecker implements AfterFunctionCallAnalysisInterface, AfterMethodCallAnalysisInterface { public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $event): void { @@ -99,6 +99,6 @@ public static function afterFunctionCallAnalysis(AfterFunctionCallAnalysisEvent } } -class IncorrectFunctionCasing extends PluginIssue +final class IncorrectFunctionCasing extends PluginIssue { } diff --git a/examples/plugins/InternalChecker.php b/examples/plugins/InternalChecker.php index 1ae5d8afee9..802edccf87d 100644 --- a/examples/plugins/InternalChecker.php +++ b/examples/plugins/InternalChecker.php @@ -12,7 +12,7 @@ use function strpos; -class InternalChecker implements AfterClassLikeAnalysisInterface +final class InternalChecker implements AfterClassLikeAnalysisInterface { /** @return null|false */ public static function afterStatementAnalysis(AfterClassLikeAnalysisEvent $event): ?bool diff --git a/examples/plugins/PreventFloatAssignmentChecker.php b/examples/plugins/PreventFloatAssignmentChecker.php index 18b5630efbc..5bc7b0765bb 100644 --- a/examples/plugins/PreventFloatAssignmentChecker.php +++ b/examples/plugins/PreventFloatAssignmentChecker.php @@ -12,7 +12,7 @@ /** * Prevents any assignment to a float value */ -class PreventFloatAssignmentChecker implements AfterExpressionAnalysisInterface +final class PreventFloatAssignmentChecker implements AfterExpressionAnalysisInterface { /** * Called after an expression has been checked @@ -40,6 +40,6 @@ public static function afterExpressionAnalysis(AfterExpressionAnalysisEvent $eve } } -class NoFloatAssignment extends PluginIssue +final class NoFloatAssignment extends PluginIssue { } diff --git a/examples/plugins/SafeArrayKeyChecker.php b/examples/plugins/SafeArrayKeyChecker.php index 5ea69772535..0360ed79155 100644 --- a/examples/plugins/SafeArrayKeyChecker.php +++ b/examples/plugins/SafeArrayKeyChecker.php @@ -7,7 +7,7 @@ use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; use Psalm\Plugin\EventHandler\RemoveTaintsInterface; -class SafeArrayKeyChecker implements RemoveTaintsInterface +final class SafeArrayKeyChecker implements RemoveTaintsInterface { /** * Called to see what taints should be removed diff --git a/examples/plugins/StringChecker.php b/examples/plugins/StringChecker.php index 621997dd965..8366596ce33 100644 --- a/examples/plugins/StringChecker.php +++ b/examples/plugins/StringChecker.php @@ -16,7 +16,7 @@ use function strpos; use function strtolower; -class StringChecker implements AfterExpressionAnalysisInterface +final class StringChecker implements AfterExpressionAnalysisInterface { /** * Called after an expression has been checked diff --git a/examples/plugins/composer-based/echo-checker/EchoChecker.php b/examples/plugins/composer-based/echo-checker/EchoChecker.php index 668206c26e3..642e0a53e9c 100644 --- a/examples/plugins/composer-based/echo-checker/EchoChecker.php +++ b/examples/plugins/composer-based/echo-checker/EchoChecker.php @@ -11,7 +11,7 @@ use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TString; -class EchoChecker implements AfterStatementAnalysisInterface +final class EchoChecker implements AfterStatementAnalysisInterface { /** * Called after a statement has been checked diff --git a/examples/plugins/composer-based/echo-checker/PluginEntryPoint.php b/examples/plugins/composer-based/echo-checker/PluginEntryPoint.php index 4d6102281e4..e83927e0d04 100644 --- a/examples/plugins/composer-based/echo-checker/PluginEntryPoint.php +++ b/examples/plugins/composer-based/echo-checker/PluginEntryPoint.php @@ -6,7 +6,7 @@ use Psalm\Plugin\RegistrationInterface; use SimpleXMLElement; -class PluginEntryPoint implements PluginEntryPointInterface +final class PluginEntryPoint implements PluginEntryPointInterface { public function __invoke(RegistrationInterface $registration, ?SimpleXMLElement $config = null): void { diff --git a/src/Psalm/Internal/Algebra.php b/src/Psalm/Internal/Algebra.php index d2b16a26c0f..43345dfd004 100644 --- a/src/Psalm/Internal/Algebra.php +++ b/src/Psalm/Internal/Algebra.php @@ -24,7 +24,7 @@ /** * @internal */ -class Algebra +final class Algebra { /** * @param array>> $all_types diff --git a/src/Psalm/Internal/Algebra/FormulaGenerator.php b/src/Psalm/Internal/Algebra/FormulaGenerator.php index c5d75f8829c..b6810291e5d 100644 --- a/src/Psalm/Internal/Algebra/FormulaGenerator.php +++ b/src/Psalm/Internal/Algebra/FormulaGenerator.php @@ -21,7 +21,7 @@ /** * @internal */ -class FormulaGenerator +final class FormulaGenerator { /** * @return list diff --git a/src/Psalm/Internal/Analyzer/AlgebraAnalyzer.php b/src/Psalm/Internal/Analyzer/AlgebraAnalyzer.php index 6fc52514aa3..5f721f5313e 100644 --- a/src/Psalm/Internal/Analyzer/AlgebraAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/AlgebraAnalyzer.php @@ -20,7 +20,7 @@ /** * @internal */ -class AlgebraAnalyzer +final class AlgebraAnalyzer { /** * This looks to see if there are any clauses in one formula that contradict diff --git a/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php b/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php index f5010ecd242..7b1eb545d72 100644 --- a/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php @@ -33,7 +33,7 @@ /** * @internal */ -class AttributesAnalyzer +final class AttributesAnalyzer { private const TARGET_DESCRIPTIONS = [ 1 => 'class', diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index bcd63764a0f..a37b4b7ad04 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -106,7 +106,7 @@ /** * @internal */ -class ClassAnalyzer extends ClassLikeAnalyzer +final class ClassAnalyzer extends ClassLikeAnalyzer { /** * @var array diff --git a/src/Psalm/Internal/Analyzer/ClassLikeNameOptions.php b/src/Psalm/Internal/Analyzer/ClassLikeNameOptions.php index 60f61a9e87d..9ae064cab5d 100644 --- a/src/Psalm/Internal/Analyzer/ClassLikeNameOptions.php +++ b/src/Psalm/Internal/Analyzer/ClassLikeNameOptions.php @@ -5,7 +5,7 @@ /** * @internal */ -class ClassLikeNameOptions +final class ClassLikeNameOptions { public bool $inferred; diff --git a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php index c5daf4f5ea6..9ed72b26914 100644 --- a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php @@ -27,7 +27,7 @@ * @internal * @extends FunctionLikeAnalyzer */ -class ClosureAnalyzer extends FunctionLikeAnalyzer +final class ClosureAnalyzer extends FunctionLikeAnalyzer { /** * @param PhpParser\Node\Expr\Closure|PhpParser\Node\Expr\ArrowFunction $function diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php index 65915d1f34c..ec06f1878fe 100644 --- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php @@ -41,7 +41,7 @@ /** * @internal */ -class CommentAnalyzer +final class CommentAnalyzer { public const TYPE_REGEX = '(\??\\\?[\(\)A-Za-z0-9_&\<\.=,\>\[\]\-\{\}:|?\\\\]*|\$[a-zA-Z_0-9_]+)'; diff --git a/src/Psalm/Internal/Analyzer/DataFlowNodeData.php b/src/Psalm/Internal/Analyzer/DataFlowNodeData.php index bc4f2dbf533..df96e6fe9a2 100644 --- a/src/Psalm/Internal/Analyzer/DataFlowNodeData.php +++ b/src/Psalm/Internal/Analyzer/DataFlowNodeData.php @@ -8,7 +8,7 @@ * @psalm-immutable * @internal */ -class DataFlowNodeData +final class DataFlowNodeData { use ImmutableNonCloneableTrait; diff --git a/src/Psalm/Internal/Analyzer/FunctionAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionAnalyzer.php index f22e09cf29f..8aec6258a3c 100644 --- a/src/Psalm/Internal/Analyzer/FunctionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionAnalyzer.php @@ -14,7 +14,7 @@ * @internal * @extends FunctionLikeAnalyzer */ -class FunctionAnalyzer extends FunctionLikeAnalyzer +final class FunctionAnalyzer extends FunctionLikeAnalyzer { public function __construct(PhpParser\Node\Stmt\Function_ $function, SourceAnalyzer $source) { diff --git a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php index 15ec85e3938..e13308e5c0f 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php @@ -63,7 +63,7 @@ /** * @internal */ -class ReturnTypeAnalyzer +final class ReturnTypeAnalyzer { /** * @param Closure|Function_|ClassMethod|ArrowFunction $function diff --git a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php index 13280aa1746..2d99b3435af 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php +++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php @@ -24,7 +24,7 @@ * * @internal */ -class ReturnTypeCollector +final class ReturnTypeCollector { /** * Gets the return types from a list of statements diff --git a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php index c1f8ed6b4e5..a1a10ebe46b 100644 --- a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php @@ -25,7 +25,7 @@ /** * @internal */ -class InterfaceAnalyzer extends ClassLikeAnalyzer +final class InterfaceAnalyzer extends ClassLikeAnalyzer { public function __construct( PhpParser\Node\Stmt\Interface_ $interface, diff --git a/src/Psalm/Internal/Analyzer/IssueData.php b/src/Psalm/Internal/Analyzer/IssueData.php index 72e72b21b7c..3bfb9a414cc 100644 --- a/src/Psalm/Internal/Analyzer/IssueData.php +++ b/src/Psalm/Internal/Analyzer/IssueData.php @@ -9,7 +9,7 @@ /** * @internal */ -class IssueData +final class IssueData { public const SEVERITY_INFO = 'info'; public const SEVERITY_ERROR = 'error'; diff --git a/src/Psalm/Internal/Analyzer/MethodAnalyzer.php b/src/Psalm/Internal/Analyzer/MethodAnalyzer.php index 89af7aaec71..bc42192496a 100644 --- a/src/Psalm/Internal/Analyzer/MethodAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/MethodAnalyzer.php @@ -27,7 +27,7 @@ * @internal * @extends FunctionLikeAnalyzer */ -class MethodAnalyzer extends FunctionLikeAnalyzer +final class MethodAnalyzer extends FunctionLikeAnalyzer { // https://github.com/php/php-src/blob/a83923044c48982c80804ae1b45e761c271966d3/Zend/zend_enum.c#L77-L95 private const FORBIDDEN_ENUM_METHODS = [ diff --git a/src/Psalm/Internal/Analyzer/MethodComparator.php b/src/Psalm/Internal/Analyzer/MethodComparator.php index d595df15803..3bfd4de65e9 100644 --- a/src/Psalm/Internal/Analyzer/MethodComparator.php +++ b/src/Psalm/Internal/Analyzer/MethodComparator.php @@ -44,7 +44,7 @@ /** * @internal */ -class MethodComparator +final class MethodComparator { /** * @param string[] $suppressed_issues diff --git a/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php b/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php index a01318a3e81..d5f2ac9e4a7 100644 --- a/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php @@ -23,7 +23,7 @@ /** * @internal */ -class NamespaceAnalyzer extends SourceAnalyzer +final class NamespaceAnalyzer extends SourceAnalyzer { use CanAlias; diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index 6be055ea43b..1a152c1c931 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -97,7 +97,7 @@ /** * @internal */ -class ProjectAnalyzer +final class ProjectAnalyzer { /** * Cached config diff --git a/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php b/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php index 4b8946623b5..808010d09bc 100644 --- a/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php @@ -17,7 +17,7 @@ /** * @internal */ -class ScopeAnalyzer +final class ScopeAnalyzer { public const ACTION_END = 'END'; public const ACTION_BREAK = 'BREAK'; diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/DoAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/DoAnalyzer.php index 228f0b9e05e..234f8bab45e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/DoAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/DoAnalyzer.php @@ -28,7 +28,7 @@ /** * @internal */ -class DoAnalyzer +final class DoAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/ForAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/ForAnalyzer.php index 4c0dd4ee16f..dbad2cd129a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/ForAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/ForAnalyzer.php @@ -19,7 +19,7 @@ /** * @internal */ -class ForAnalyzer +final class ForAnalyzer { /** * @return false|null diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php index 128eaada6ef..8f0ec8a713f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php @@ -74,7 +74,7 @@ /** * @internal */ -class ForeachAnalyzer +final class ForeachAnalyzer { /** * @return false|null diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfConditionalAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfConditionalAnalyzer.php index e1c5e5e4024..a306cca4ca0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/IfConditionalAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfConditionalAnalyzer.php @@ -29,7 +29,7 @@ /** * @internal */ -class IfConditionalAnalyzer +final class IfConditionalAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/ElseAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/ElseAnalyzer.php index 31fca6dc0f7..f1d9e5a9fd0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/ElseAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/ElseAnalyzer.php @@ -26,7 +26,7 @@ /** * @internal */ -class ElseAnalyzer +final class ElseAnalyzer { /** * @return false|null diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/ElseIfAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/ElseIfAnalyzer.php index f55d1d0a63e..00c7895c2be 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/ElseIfAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/ElseIfAnalyzer.php @@ -40,7 +40,7 @@ /** * @internal */ -class ElseIfAnalyzer +final class ElseIfAnalyzer { /** * @return false|null diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php index dd2e0989402..ad95cce30d5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php @@ -46,7 +46,7 @@ /** * @internal */ -class IfAnalyzer +final class IfAnalyzer { /** * @param array $pre_assignment_else_redefined_vars diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfElseAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfElseAnalyzer.php index a5a006a0b31..b4648cd59f2 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/IfElseAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfElseAnalyzer.php @@ -41,7 +41,7 @@ /** * @internal */ -class IfElseAnalyzer +final class IfElseAnalyzer { /** * System of type substitution and deletion diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php index f1f5ad61fca..f925f961336 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php @@ -28,7 +28,7 @@ /** * @internal */ -class LoopAnalyzer +final class LoopAnalyzer { /** * Checks an array of statements in a loop diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/SwitchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/SwitchAnalyzer.php index a0d9d48200c..73240624c8e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/SwitchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/SwitchAnalyzer.php @@ -21,7 +21,7 @@ /** * @internal */ -class SwitchAnalyzer +final class SwitchAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php index e116f1173f7..54b7e7f3f01 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php @@ -53,7 +53,7 @@ /** * @internal */ -class SwitchCaseAnalyzer +final class SwitchCaseAnalyzer { /** * @return null|false diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php index 18536b49394..c366f7c612c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php @@ -32,7 +32,7 @@ /** * @internal */ -class TryAnalyzer +final class TryAnalyzer { /** * @return false|null diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/WhileAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/WhileAnalyzer.php index bb486123431..6eea413c4ce 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/WhileAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/WhileAnalyzer.php @@ -16,7 +16,7 @@ /** * @internal */ -class WhileAnalyzer +final class WhileAnalyzer { /** * @return false|null diff --git a/src/Psalm/Internal/Analyzer/Statements/BreakAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/BreakAnalyzer.php index cbf9be6c0ea..e1b7ff72024 100644 --- a/src/Psalm/Internal/Analyzer/Statements/BreakAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/BreakAnalyzer.php @@ -13,7 +13,7 @@ /** * @internal */ -class BreakAnalyzer +final class BreakAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/ContinueAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ContinueAnalyzer.php index 9e04d165440..def2aa47287 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ContinueAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ContinueAnalyzer.php @@ -16,7 +16,7 @@ /** * @internal */ -class ContinueAnalyzer +final class ContinueAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php index 300b8fe02aa..b21ae4647c5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php @@ -21,7 +21,7 @@ /** * @internal */ -class EchoAnalyzer +final class EchoAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php index fa1fb7f1248..3c1034df2db 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php @@ -53,7 +53,7 @@ /** * @internal */ -class ArrayAnalyzer +final class ArrayAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayCreationInfo.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayCreationInfo.php index 43161d22f3b..35b2161683e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayCreationInfo.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayCreationInfo.php @@ -9,7 +9,7 @@ /** * @internal */ -class ArrayCreationInfo +final class ArrayCreationInfo { /** * @var list diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index fc57f2177c7..de4d2022aaa 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -120,7 +120,7 @@ * For example if $a is an int, if($a > 0) will be turned into an assertion to make psalm understand that in the * if block, $a is a positive-int */ -class AssertionFinder +final class AssertionFinder { public const ASSIGNMENT_TO_RIGHT = 1; public const ASSIGNMENT_TO_LEFT = -1; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php index c071a741181..f00d81a48dd 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php @@ -55,7 +55,7 @@ /** * @internal */ -class ArrayAssignmentAnalyzer +final class ArrayAssignmentAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/AssignedProperty.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/AssignedProperty.php index a49249992b5..828c9181b7f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/AssignedProperty.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/AssignedProperty.php @@ -7,7 +7,7 @@ /** * @internal */ -class AssignedProperty +final class AssignedProperty { public Union $property_type; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php index 127bab853f6..bedec945c25 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php @@ -87,7 +87,7 @@ /** * @internal */ -class InstancePropertyAssignmentAnalyzer +final class InstancePropertyAssignmentAnalyzer { /** * @param PropertyFetch|PropertyProperty $stmt diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/StaticPropertyAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/StaticPropertyAssignmentAnalyzer.php index e307f514644..cff735cd273 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/StaticPropertyAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/StaticPropertyAssignmentAnalyzer.php @@ -33,7 +33,7 @@ /** * @internal */ -class StaticPropertyAssignmentAnalyzer +final class StaticPropertyAssignmentAnalyzer { /** * @return false|null diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index c59a554d9ab..debb27e87b7 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -97,7 +97,7 @@ /** * @internal */ -class AssignmentAnalyzer +final class AssignmentAnalyzer { /** * @param PhpParser\Node\Expr|null $assign_value This has to be null to support list destructuring diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/AndAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/AndAnalyzer.php index 1f5f90309e6..d721f481587 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/AndAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/AndAnalyzer.php @@ -28,7 +28,7 @@ /** * @internal */ -class AndAnalyzer +final class AndAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php index 4e2b30f63b4..f4ec365b722 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php @@ -58,7 +58,7 @@ /** * @internal */ -class ArithmeticOpAnalyzer +final class ArithmeticOpAnalyzer { public static function analyze( ?StatementsSource $statements_source, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php index 8a0eed58346..0a52166699a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php @@ -18,7 +18,7 @@ /** * @internal */ -class CoalesceAnalyzer +final class CoalesceAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php index b90ffbc387d..fd7ab531f3e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php @@ -50,7 +50,7 @@ /** * @internal */ -class ConcatAnalyzer +final class ConcatAnalyzer { private const MAX_LITERALS = 64; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/NonComparisonOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/NonComparisonOpAnalyzer.php index 7f3e7f5391f..63bff0ed888 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/NonComparisonOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/NonComparisonOpAnalyzer.php @@ -14,7 +14,7 @@ /** * @internal */ -class NonComparisonOpAnalyzer +final class NonComparisonOpAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/OrAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/OrAnalyzer.php index d315e36f900..2a2e0cf8217 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/OrAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/OrAnalyzer.php @@ -36,7 +36,7 @@ /** * @internal */ -class OrAnalyzer +final class OrAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php index bc799edd290..309d1b8d537 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php @@ -39,7 +39,7 @@ /** * @internal */ -class BinaryOpAnalyzer +final class BinaryOpAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php index 3efa5c6ce69..e9b16763e37 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php @@ -24,7 +24,7 @@ /** * @internal */ -class BitwiseNotAnalyzer +final class BitwiseNotAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php index fa053702791..3c75dd9efca 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php @@ -15,7 +15,7 @@ /** * @internal */ -class BooleanNotAnalyzer +final class BooleanNotAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index 2d8380ceb69..cd3e590b8b0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -80,7 +80,7 @@ /** * @internal */ -class ArgumentAnalyzer +final class ArgumentAnalyzer { /** * @param array> $class_generic_params diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentMapPopulator.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentMapPopulator.php index fbbc5e24ea6..ac8016e2a20 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentMapPopulator.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentMapPopulator.php @@ -21,7 +21,7 @@ /** * @internal */ -class ArgumentMapPopulator +final class ArgumentMapPopulator { /** * @param MethodCall|StaticCall|FuncCall|New_ $stmt diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index d166ed3774c..9ff268abeb6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -67,7 +67,7 @@ /** * @internal */ -class ArgumentsAnalyzer +final class ArgumentsAnalyzer { /** * @param list $args diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php index c1aa8540e1a..55364514eb9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php @@ -57,7 +57,7 @@ /** * @internal */ -class ArrayFunctionArgumentsAnalyzer +final class ArrayFunctionArgumentsAnalyzer { /** * @param array $args diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php index c3bbd2995b7..367f2d26d27 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php @@ -21,7 +21,7 @@ /** * @internal */ -class ClassTemplateParamCollector +final class ClassTemplateParamCollector { /** * @param lowercase-string $method_name diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index 21bdf3f9ea7..3c44bad7067 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -78,7 +78,7 @@ /** * @internal */ -class FunctionCallAnalyzer extends CallAnalyzer +final class FunctionCallAnalyzer extends CallAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallInfo.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallInfo.php index c2e38864d5b..9bcec9ed740 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallInfo.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallInfo.php @@ -10,7 +10,7 @@ /** * @internal */ -class FunctionCallInfo +final class FunctionCallInfo { public ?string $function_id = null; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index 34dae01f345..e15fd22a71d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -55,7 +55,7 @@ /** * @internal */ -class FunctionCallReturnTypeFetcher +final class FunctionCallReturnTypeFetcher { /** * @param non-empty-string $function_id diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicCallContext.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicCallContext.php index 7e033b36f1d..04da740e01c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicCallContext.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicCallContext.php @@ -8,7 +8,7 @@ /** * @internal */ -class AtomicCallContext +final class AtomicCallContext { public MethodIdentifier $method_id; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalysisResult.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalysisResult.php index 94703207fc6..a4e6d2153fb 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalysisResult.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalysisResult.php @@ -8,7 +8,7 @@ /** * @internal */ -class AtomicMethodCallAnalysisResult +final class AtomicMethodCallAnalysisResult { public ?Union $return_type = null; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php index 5997ac82efa..62903d0af76 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php @@ -62,7 +62,7 @@ * * @internal */ -class AtomicMethodCallAnalyzer extends CallAnalyzer +final class AtomicMethodCallAnalyzer extends CallAnalyzer { /** * @param TNamedObject|TTemplateParam|null $static_type diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php index 05210818b64..bf8d1af4239 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php @@ -55,7 +55,7 @@ /** * @internal */ -class ExistingAtomicMethodCallAnalyzer extends CallAnalyzer +final class ExistingAtomicMethodCallAnalyzer extends CallAnalyzer { /** * @param TNamedObject|TTemplateParam|null $static_type diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallProhibitionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallProhibitionAnalyzer.php index d3d42a95ef3..782062ea292 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallProhibitionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallProhibitionAnalyzer.php @@ -15,7 +15,7 @@ /** * @internal */ -class MethodCallProhibitionAnalyzer +final class MethodCallProhibitionAnalyzer { /** * @param string[] $suppressed_issues diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallPurityAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallPurityAnalyzer.php index 760adf93590..a23332ee3ca 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallPurityAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallPurityAnalyzer.php @@ -22,7 +22,7 @@ /** * @internal */ -class MethodCallPurityAnalyzer +final class MethodCallPurityAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php index a3bd4b3b493..84021ca3651 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php @@ -41,7 +41,7 @@ /** * @internal */ -class MethodCallReturnTypeFetcher +final class MethodCallReturnTypeFetcher { /** * @param TNamedObject|TTemplateParam|null $static_type diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodVisibilityAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodVisibilityAnalyzer.php index 2cc632a35e3..0a81ee8ecac 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodVisibilityAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodVisibilityAnalyzer.php @@ -20,7 +20,7 @@ /** * @internal */ -class MethodVisibilityAnalyzer +final class MethodVisibilityAnalyzer { /** * @param string[] $suppressed_issues diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php index 1f287b98d57..22f483c1bcd 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php @@ -32,7 +32,7 @@ /** * @internal */ -class MissingMethodCallHandler +final class MissingMethodCallHandler { public static function handleMagicMethod( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php index 72c343d1073..ab4f1f0428b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php @@ -41,7 +41,7 @@ /** * @internal */ -class MethodCallAnalyzer extends CallAnalyzer +final class MethodCallAnalyzer extends CallAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php index 874f0618172..961662f7044 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php @@ -56,7 +56,7 @@ /** * @internal */ -class NamedFunctionCallHandler +final class NamedFunctionCallHandler { /** * @param lowercase-string $function_id diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php index 45476ca22b6..1a9daef9fb5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php @@ -67,7 +67,7 @@ /** * @internal */ -class NewAnalyzer extends CallAnalyzer +final class NewAnalyzer extends CallAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php index 8302dcfca44..a357afac605 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php @@ -36,7 +36,7 @@ /** * @internal */ -class StaticCallAnalyzer extends CallAnalyzer +final class StaticCallAnalyzer extends CallAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php index b0839345418..e42024d08f6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -66,7 +66,7 @@ /** * @internal */ -class AtomicStaticCallAnalyzer +final class AtomicStaticCallAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php index a8c916534ea..23c0078facd 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -50,7 +50,7 @@ /** * @internal */ -class ExistingAtomicStaticCallAnalyzer +final class ExistingAtomicStaticCallAnalyzer { /** * @param list $args diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 99f045d71c6..bbcb0e3105a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -58,7 +58,7 @@ /** * @internal */ -class CastAnalyzer +final class CastAnalyzer { /** @var string[] */ private const PSEUDO_CASTABLE_CLASSES = [ diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php index 9335f658324..d9ec74f47bf 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php @@ -56,7 +56,7 @@ /** * @internal */ -class ClassConstAnalyzer +final class ClassConstAnalyzer { /** * @psalm-suppress ComplexMethod to be refactored. We should probably regroup the two big if about $stmt->class and diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CloneAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CloneAnalyzer.php index 7f1927951d2..c179d12c142 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CloneAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CloneAnalyzer.php @@ -27,7 +27,7 @@ /** * @internal */ -class CloneAnalyzer +final class CloneAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php index 9728d331397..3b9014f85b4 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php @@ -14,7 +14,7 @@ /** * @internal */ -class EmptyAnalyzer +final class EmptyAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php index 9792a91c87e..563d58b1a58 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php @@ -26,7 +26,7 @@ /** * @internal */ -class EncapsulatedStringAnalyzer +final class EncapsulatedStringAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EvalAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EvalAnalyzer.php index 34160e58b49..3f6b2a8d19b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/EvalAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EvalAnalyzer.php @@ -19,7 +19,7 @@ /** * @internal */ -class EvalAnalyzer +final class EvalAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ExitAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ExitAnalyzer.php index 36d6be84b95..fcafecdf3b1 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ExitAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ExitAnalyzer.php @@ -24,7 +24,7 @@ /** * @internal */ -class ExitAnalyzer +final class ExitAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ExpressionIdentifier.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ExpressionIdentifier.php index 53794faa358..00a249ec83b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ExpressionIdentifier.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ExpressionIdentifier.php @@ -17,7 +17,7 @@ /** * @internal */ -class ExpressionIdentifier +final class ExpressionIdentifier { public static function getVarId( PhpParser\Node\Expr $stmt, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php index 9830012c3a6..4a3a1f5c903 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php @@ -100,7 +100,7 @@ /** * @internal */ -class ArrayFetchAnalyzer +final class ArrayFetchAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index ff14ed24273..0fdc76b40ce 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -76,7 +76,7 @@ /** * @internal */ -class AtomicPropertyFetchAnalyzer +final class AtomicPropertyFetchAnalyzer { /** * @param array $invalid_fetch_types $invalid_fetch_types diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ConstFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ConstFetchAnalyzer.php index 8b30a60fc91..db382c4f914 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ConstFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ConstFetchAnalyzer.php @@ -28,7 +28,7 @@ /** * @internal */ -class ConstFetchAnalyzer +final class ConstFetchAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php index 528acd0609d..acf004490c1 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php @@ -33,7 +33,7 @@ /** * @internal */ -class InstancePropertyFetchAnalyzer +final class InstancePropertyFetchAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php index 35bc7061427..94771ed2e17 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php @@ -37,7 +37,7 @@ /** * @internal */ -class StaticPropertyFetchAnalyzer +final class StaticPropertyFetchAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index 80ad5e0e97f..324dd7b30b0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -41,7 +41,7 @@ /** * @internal */ -class VariableFetchAnalyzer +final class VariableFetchAnalyzer { public const SUPER_GLOBALS = [ '$GLOBALS', diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/IncDecExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/IncDecExpressionAnalyzer.php index 1526bb6a59f..83079c98879 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/IncDecExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/IncDecExpressionAnalyzer.php @@ -20,7 +20,7 @@ /** * @internal */ -class IncDecExpressionAnalyzer +final class IncDecExpressionAnalyzer { /** * @param PostInc|PostDec|PreInc|PreDec $stmt diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php index e293afabc58..24db2b6d92e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php @@ -47,7 +47,7 @@ /** * @internal */ -class IncludeAnalyzer +final class IncludeAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/InstanceofAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/InstanceofAnalyzer.php index 83b04cd9478..6e036490587 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/InstanceofAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/InstanceofAnalyzer.php @@ -17,7 +17,7 @@ /** * @internal */ -class InstanceofAnalyzer +final class InstanceofAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/IssetAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/IssetAnalyzer.php index a9db2fead9e..6303733642b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/IssetAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/IssetAnalyzer.php @@ -14,7 +14,7 @@ /** * @internal */ -class IssetAnalyzer +final class IssetAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/MagicConstAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/MagicConstAnalyzer.php index 129266f95d2..379e3e75bf5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/MagicConstAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/MagicConstAnalyzer.php @@ -21,7 +21,7 @@ /** * @internal */ -class MagicConstAnalyzer +final class MagicConstAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/MatchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/MatchAnalyzer.php index a76a199089a..ffaa0f5b387 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/MatchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/MatchAnalyzer.php @@ -44,7 +44,7 @@ /** * @internal */ -class MatchAnalyzer +final class MatchAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/NullsafeAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/NullsafeAnalyzer.php index 739bb5b7ba2..129c5326d16 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/NullsafeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/NullsafeAnalyzer.php @@ -18,7 +18,7 @@ /** * @internal */ -class NullsafeAnalyzer +final class NullsafeAnalyzer { /** * @param PhpParser\Node\Expr\NullsafePropertyFetch|PhpParser\Node\Expr\NullsafeMethodCall $stmt diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php index 64cebbc80d4..3d1bd13fad1 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php @@ -21,7 +21,7 @@ /** * @internal */ -class PrintAnalyzer +final class PrintAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php index 6d94391ffd2..d35231e4102 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php @@ -45,7 +45,7 @@ * * @internal */ -class SimpleTypeInferer +final class SimpleTypeInferer { /** * @param ?array $existing_class_constants diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/TernaryAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/TernaryAnalyzer.php index 7d990eaf1dd..7524c523baa 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/TernaryAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/TernaryAnalyzer.php @@ -38,7 +38,7 @@ /** * @internal */ -class TernaryAnalyzer +final class TernaryAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/UnaryPlusMinusAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/UnaryPlusMinusAnalyzer.php index eb4f256f444..eec166064d2 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/UnaryPlusMinusAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/UnaryPlusMinusAnalyzer.php @@ -25,7 +25,7 @@ /** * @internal */ -class UnaryPlusMinusAnalyzer +final class UnaryPlusMinusAnalyzer { /** * @param PhpParser\Node\Expr\UnaryMinus|PhpParser\Node\Expr\UnaryPlus $stmt diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php index d5e6174b230..a34deaedfbb 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php @@ -28,7 +28,7 @@ /** * @internal */ -class YieldAnalyzer +final class YieldAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/YieldFromAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/YieldFromAnalyzer.php index 03f63c637f3..d77e1936c99 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/YieldFromAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/YieldFromAnalyzer.php @@ -17,7 +17,7 @@ /** * @internal */ -class YieldFromAnalyzer +final class YieldFromAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php index 7cdb1b54a05..277787b3ef7 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php @@ -65,7 +65,7 @@ /** * @internal */ -class ExpressionAnalyzer +final class ExpressionAnalyzer { /** * @param bool $assigned_to_reference This is set to true when the expression being analyzed diff --git a/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php index 4eb29ca7b62..d7ec4dcf4b0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php @@ -19,7 +19,7 @@ /** * @internal */ -class GlobalAnalyzer +final class GlobalAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php index 2ea1e981544..4f0867f169d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php @@ -52,7 +52,7 @@ /** * @internal */ -class ReturnAnalyzer +final class ReturnAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php index eecb5b84284..9ade50098f6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php @@ -19,7 +19,7 @@ /** * @internal */ -class StaticAnalyzer +final class StaticAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/ThrowAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ThrowAnalyzer.php index c0d1b0b2a45..6ae148f8b9e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ThrowAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ThrowAnalyzer.php @@ -16,7 +16,7 @@ /** * @internal */ -class ThrowAnalyzer +final class ThrowAnalyzer { /** * @param PhpParser\Node\Stmt\Throw_|PhpParser\Node\Expr\Throw_ $stmt diff --git a/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php index b1c12d12cd4..e93b58b6329 100644 --- a/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php @@ -24,7 +24,7 @@ /** * @internal */ -class UnsetAnalyzer +final class UnsetAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/UnusedAssignmentRemover.php b/src/Psalm/Internal/Analyzer/Statements/UnusedAssignmentRemover.php index 188ad00bb3c..d3aaa7050a7 100644 --- a/src/Psalm/Internal/Analyzer/Statements/UnusedAssignmentRemover.php +++ b/src/Psalm/Internal/Analyzer/Statements/UnusedAssignmentRemover.php @@ -22,7 +22,7 @@ /** * @internal */ -class UnusedAssignmentRemover +final class UnusedAssignmentRemover { /** * @var array diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index fc75e1631eb..cb3c9b49d94 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -93,7 +93,7 @@ /** * @internal */ -class StatementsAnalyzer extends SourceAnalyzer +final class StatementsAnalyzer extends SourceAnalyzer { protected SourceAnalyzer $source; diff --git a/src/Psalm/Internal/Analyzer/TraitAnalyzer.php b/src/Psalm/Internal/Analyzer/TraitAnalyzer.php index c6192a666a5..dcb456fd75f 100644 --- a/src/Psalm/Internal/Analyzer/TraitAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/TraitAnalyzer.php @@ -12,7 +12,7 @@ /** * @internal */ -class TraitAnalyzer extends ClassLikeAnalyzer +final class TraitAnalyzer extends ClassLikeAnalyzer { private Aliases $aliases; diff --git a/src/Psalm/Internal/Analyzer/TypeAnalyzer.php b/src/Psalm/Internal/Analyzer/TypeAnalyzer.php index 0c871fe2200..1e13aa83a51 100644 --- a/src/Psalm/Internal/Analyzer/TypeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/TypeAnalyzer.php @@ -11,7 +11,7 @@ /** * @internal */ -class TypeAnalyzer +final class TypeAnalyzer { /** * Takes two arrays of types and merges them diff --git a/src/Psalm/Internal/Cache.php b/src/Psalm/Internal/Cache.php index 88e2f5704d7..50c838f3b5a 100644 --- a/src/Psalm/Internal/Cache.php +++ b/src/Psalm/Internal/Cache.php @@ -23,7 +23,7 @@ /** * @internal */ -class Cache +final class Cache { private Config $config; diff --git a/src/Psalm/Internal/Clause.php b/src/Psalm/Internal/Clause.php index 31f32bea065..b8a44c939ee 100644 --- a/src/Psalm/Internal/Clause.php +++ b/src/Psalm/Internal/Clause.php @@ -26,7 +26,7 @@ * @internal * @psalm-immutable */ -class Clause +final class Clause { use ImmutableNonCloneableTrait; diff --git a/src/Psalm/Internal/Codebase/Analyzer.php b/src/Psalm/Internal/Codebase/Analyzer.php index ba0209ea6f2..72825b05baf 100644 --- a/src/Psalm/Internal/Codebase/Analyzer.php +++ b/src/Psalm/Internal/Codebase/Analyzer.php @@ -95,7 +95,7 @@ * * Called in the analysis phase of Psalm's execution */ -class Analyzer +final class Analyzer { private Config $config; diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php index a7045a43af5..b57bbe076da 100644 --- a/src/Psalm/Internal/Codebase/ClassLikes.php +++ b/src/Psalm/Internal/Codebase/ClassLikes.php @@ -71,7 +71,7 @@ * * Handles information about classes, interfaces and traits */ -class ClassLikes +final class ClassLikes { private ClassLikeStorageProvider $classlike_storage_provider; diff --git a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php index 9bf8cabab2c..866b5180e14 100644 --- a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php +++ b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php @@ -53,7 +53,7 @@ /** * @internal */ -class ConstantTypeResolver +final class ConstantTypeResolver { public static function resolve( ClassLikes $classlikes, diff --git a/src/Psalm/Internal/Codebase/Functions.php b/src/Psalm/Internal/Codebase/Functions.php index 5e24f071313..acba38f28bb 100644 --- a/src/Psalm/Internal/Codebase/Functions.php +++ b/src/Psalm/Internal/Codebase/Functions.php @@ -35,7 +35,7 @@ /** * @internal */ -class Functions +final class Functions { private FileStorageProvider $file_storage_provider; diff --git a/src/Psalm/Internal/Codebase/InternalCallMapHandler.php b/src/Psalm/Internal/Codebase/InternalCallMapHandler.php index 71ce092eae9..e5c96b624ed 100644 --- a/src/Psalm/Internal/Codebase/InternalCallMapHandler.php +++ b/src/Psalm/Internal/Codebase/InternalCallMapHandler.php @@ -33,7 +33,7 @@ * * Gets values from the call map array, which stores data about native functions and methods */ -class InternalCallMapHandler +final class InternalCallMapHandler { private const PHP_MAJOR_VERSION = 8; private const PHP_MINOR_VERSION = 3; diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php index 2d276bf2f24..9648729c473 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -53,7 +53,7 @@ * * Handles information about class methods */ -class Methods +final class Methods { private ClassLikeStorageProvider $classlike_storage_provider; diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php index 69aa208044e..6c6dc25b187 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -42,7 +42,7 @@ * * Populates file and class information so that analysis can work properly */ -class Populator +final class Populator { private ClassLikeStorageProvider $classlike_storage_provider; diff --git a/src/Psalm/Internal/Codebase/Properties.php b/src/Psalm/Internal/Codebase/Properties.php index ac7c8add999..9dad3906fc2 100644 --- a/src/Psalm/Internal/Codebase/Properties.php +++ b/src/Psalm/Internal/Codebase/Properties.php @@ -23,7 +23,7 @@ * * Handles information about class properties */ -class Properties +final class Properties { private ClassLikeStorageProvider $classlike_storage_provider; diff --git a/src/Psalm/Internal/Codebase/PropertyMap.php b/src/Psalm/Internal/Codebase/PropertyMap.php index d66c58c4579..36e9da02d6b 100644 --- a/src/Psalm/Internal/Codebase/PropertyMap.php +++ b/src/Psalm/Internal/Codebase/PropertyMap.php @@ -8,7 +8,7 @@ /** * @internal */ -class PropertyMap +final class PropertyMap { /** * @var array>|null diff --git a/src/Psalm/Internal/Codebase/ReferenceMapGenerator.php b/src/Psalm/Internal/Codebase/ReferenceMapGenerator.php index 1421846bc1d..73d3918824d 100644 --- a/src/Psalm/Internal/Codebase/ReferenceMapGenerator.php +++ b/src/Psalm/Internal/Codebase/ReferenceMapGenerator.php @@ -7,7 +7,7 @@ /** * @internal */ -class ReferenceMapGenerator +final class ReferenceMapGenerator { /** * @return array diff --git a/src/Psalm/Internal/Codebase/Reflection.php b/src/Psalm/Internal/Codebase/Reflection.php index f62cfdc7e28..6ab8e4af59d 100644 --- a/src/Psalm/Internal/Codebase/Reflection.php +++ b/src/Psalm/Internal/Codebase/Reflection.php @@ -37,7 +37,7 @@ * * Handles information gleaned from class and function reflection */ -class Reflection +final class Reflection { private ClassLikeStorageProvider $storage_provider; diff --git a/src/Psalm/Internal/Codebase/Scanner.php b/src/Psalm/Internal/Codebase/Scanner.php index ab2c586ef72..c30d6c5a022 100644 --- a/src/Psalm/Internal/Codebase/Scanner.php +++ b/src/Psalm/Internal/Codebase/Scanner.php @@ -83,7 +83,7 @@ * * Contains methods that aid in the scanning of Psalm's codebase */ -class Scanner +final class Scanner { private Codebase $codebase; diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index b18b82eb161..bb7cd993879 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -46,7 +46,7 @@ /** * @internal */ -class TaintFlowGraph extends DataFlowGraph +final class TaintFlowGraph extends DataFlowGraph { /** @var array */ private array $sources = []; diff --git a/src/Psalm/Internal/Codebase/VariableUseGraph.php b/src/Psalm/Internal/Codebase/VariableUseGraph.php index 22069bca81f..d90b2878c00 100644 --- a/src/Psalm/Internal/Codebase/VariableUseGraph.php +++ b/src/Psalm/Internal/Codebase/VariableUseGraph.php @@ -13,7 +13,7 @@ /** * @internal */ -class VariableUseGraph extends DataFlowGraph +final class VariableUseGraph extends DataFlowGraph { /** @var array> */ protected array $backward_edges = []; diff --git a/src/Psalm/Internal/DataFlow/Path.php b/src/Psalm/Internal/DataFlow/Path.php index a6de16c95ee..c6c0e279761 100644 --- a/src/Psalm/Internal/DataFlow/Path.php +++ b/src/Psalm/Internal/DataFlow/Path.php @@ -8,7 +8,7 @@ * @psalm-immutable * @internal */ -class Path +final class Path { use ImmutableNonCloneableTrait; diff --git a/src/Psalm/Internal/DataFlow/TaintSink.php b/src/Psalm/Internal/DataFlow/TaintSink.php index 2d89b628d3c..997b63b97c8 100644 --- a/src/Psalm/Internal/DataFlow/TaintSink.php +++ b/src/Psalm/Internal/DataFlow/TaintSink.php @@ -5,6 +5,6 @@ /** * @internal */ -class TaintSink extends DataFlowNode +final class TaintSink extends DataFlowNode { } diff --git a/src/Psalm/Internal/DataFlow/TaintSource.php b/src/Psalm/Internal/DataFlow/TaintSource.php index 747155f7bcb..1777afe99f7 100644 --- a/src/Psalm/Internal/DataFlow/TaintSource.php +++ b/src/Psalm/Internal/DataFlow/TaintSource.php @@ -5,6 +5,6 @@ /** * @internal */ -class TaintSource extends DataFlowNode +final class TaintSource extends DataFlowNode { } diff --git a/src/Psalm/Internal/Diff/ClassStatementsDiffer.php b/src/Psalm/Internal/Diff/ClassStatementsDiffer.php index 65cccb3e60c..235cff8f400 100644 --- a/src/Psalm/Internal/Diff/ClassStatementsDiffer.php +++ b/src/Psalm/Internal/Diff/ClassStatementsDiffer.php @@ -16,7 +16,7 @@ /** * @internal */ -class ClassStatementsDiffer extends AstDiffer +final class ClassStatementsDiffer extends AstDiffer { /** * Calculate diff (edit script) from $a to $b. diff --git a/src/Psalm/Internal/Diff/DiffElem.php b/src/Psalm/Internal/Diff/DiffElem.php index b76caab99f9..e64a56cce0d 100644 --- a/src/Psalm/Internal/Diff/DiffElem.php +++ b/src/Psalm/Internal/Diff/DiffElem.php @@ -10,7 +10,7 @@ * @internal * @psalm-immutable */ -class DiffElem +final class DiffElem { use ImmutableNonCloneableTrait; diff --git a/src/Psalm/Internal/Diff/FileDiffer.php b/src/Psalm/Internal/Diff/FileDiffer.php index 668c6456bc5..801dae66173 100644 --- a/src/Psalm/Internal/Diff/FileDiffer.php +++ b/src/Psalm/Internal/Diff/FileDiffer.php @@ -23,7 +23,7 @@ * * @internal */ -class FileDiffer +final class FileDiffer { /** * @param list $a diff --git a/src/Psalm/Internal/Diff/FileStatementsDiffer.php b/src/Psalm/Internal/Diff/FileStatementsDiffer.php index 1fb58c710c5..f8197fc12c3 100644 --- a/src/Psalm/Internal/Diff/FileStatementsDiffer.php +++ b/src/Psalm/Internal/Diff/FileStatementsDiffer.php @@ -11,7 +11,7 @@ /** * @internal */ -class FileStatementsDiffer extends AstDiffer +final class FileStatementsDiffer extends AstDiffer { /** * Calculate diff (edit script) from $a to $b. diff --git a/src/Psalm/Internal/Diff/NamespaceStatementsDiffer.php b/src/Psalm/Internal/Diff/NamespaceStatementsDiffer.php index cfeab6123e2..b8ea37c5e16 100644 --- a/src/Psalm/Internal/Diff/NamespaceStatementsDiffer.php +++ b/src/Psalm/Internal/Diff/NamespaceStatementsDiffer.php @@ -11,7 +11,7 @@ /** * @internal */ -class NamespaceStatementsDiffer extends AstDiffer +final class NamespaceStatementsDiffer extends AstDiffer { /** * Calculate diff (edit script) from $a to $b. diff --git a/src/Psalm/Internal/EventDispatcher.php b/src/Psalm/Internal/EventDispatcher.php index fb574949ea1..989e8ec1f3b 100644 --- a/src/Psalm/Internal/EventDispatcher.php +++ b/src/Psalm/Internal/EventDispatcher.php @@ -48,7 +48,7 @@ /** * @internal */ -class EventDispatcher +final class EventDispatcher { /** * Static methods to be called after method checks have completed diff --git a/src/Psalm/Internal/ExecutionEnvironment/BuildInfoCollector.php b/src/Psalm/Internal/ExecutionEnvironment/BuildInfoCollector.php index 30a96bae428..e2c4ae99cf6 100644 --- a/src/Psalm/Internal/ExecutionEnvironment/BuildInfoCollector.php +++ b/src/Psalm/Internal/ExecutionEnvironment/BuildInfoCollector.php @@ -20,7 +20,7 @@ * @author Kitamura Satoshi * @internal */ -class BuildInfoCollector +final class BuildInfoCollector { /** * Environment variables. diff --git a/src/Psalm/Internal/ExecutionEnvironment/GitInfoCollector.php b/src/Psalm/Internal/ExecutionEnvironment/GitInfoCollector.php index d9ec447db52..2330062120e 100644 --- a/src/Psalm/Internal/ExecutionEnvironment/GitInfoCollector.php +++ b/src/Psalm/Internal/ExecutionEnvironment/GitInfoCollector.php @@ -21,7 +21,7 @@ * @author Kitamura Satoshi * @internal */ -class GitInfoCollector +final class GitInfoCollector { /** * Git command. diff --git a/src/Psalm/Internal/FileManipulation/ClassDocblockManipulator.php b/src/Psalm/Internal/FileManipulation/ClassDocblockManipulator.php index 53b6509344b..fd1445402cf 100644 --- a/src/Psalm/Internal/FileManipulation/ClassDocblockManipulator.php +++ b/src/Psalm/Internal/FileManipulation/ClassDocblockManipulator.php @@ -17,7 +17,7 @@ /** * @internal */ -class ClassDocblockManipulator +final class ClassDocblockManipulator { /** * @var array> diff --git a/src/Psalm/Internal/FileManipulation/CodeMigration.php b/src/Psalm/Internal/FileManipulation/CodeMigration.php index 641e6aaf5d1..e118e195e72 100644 --- a/src/Psalm/Internal/FileManipulation/CodeMigration.php +++ b/src/Psalm/Internal/FileManipulation/CodeMigration.php @@ -8,7 +8,7 @@ * @psalm-immutable * @internal */ -class CodeMigration +final class CodeMigration { use ImmutableNonCloneableTrait; diff --git a/src/Psalm/Internal/FileManipulation/FileManipulationBuffer.php b/src/Psalm/Internal/FileManipulation/FileManipulationBuffer.php index 7bd147c06ec..7c5870b65c1 100644 --- a/src/Psalm/Internal/FileManipulation/FileManipulationBuffer.php +++ b/src/Psalm/Internal/FileManipulation/FileManipulationBuffer.php @@ -19,7 +19,7 @@ /** * @internal */ -class FileManipulationBuffer +final class FileManipulationBuffer { /** @var array */ private static array $file_manipulations = []; diff --git a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php index f10a42379e2..85ad399f583 100644 --- a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php +++ b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php @@ -34,7 +34,7 @@ /** * @internal */ -class FunctionDocblockManipulator +final class FunctionDocblockManipulator { /** * Manipulators ordered by line number diff --git a/src/Psalm/Internal/FileManipulation/PropertyDocblockManipulator.php b/src/Psalm/Internal/FileManipulation/PropertyDocblockManipulator.php index 3894e4edf81..8e25959cff6 100644 --- a/src/Psalm/Internal/FileManipulation/PropertyDocblockManipulator.php +++ b/src/Psalm/Internal/FileManipulation/PropertyDocblockManipulator.php @@ -22,7 +22,7 @@ /** * @internal */ -class PropertyDocblockManipulator +final class PropertyDocblockManipulator { /** * @var array> diff --git a/src/Psalm/Internal/Fork/ForkProcessDoneMessage.php b/src/Psalm/Internal/Fork/ForkProcessDoneMessage.php index c845be80315..8129dd0ac69 100644 --- a/src/Psalm/Internal/Fork/ForkProcessDoneMessage.php +++ b/src/Psalm/Internal/Fork/ForkProcessDoneMessage.php @@ -8,7 +8,7 @@ * @psalm-immutable * @internal */ -class ForkProcessDoneMessage implements ForkMessage +final class ForkProcessDoneMessage implements ForkMessage { use ImmutableNonCloneableTrait; /** @var mixed */ diff --git a/src/Psalm/Internal/Fork/ForkProcessErrorMessage.php b/src/Psalm/Internal/Fork/ForkProcessErrorMessage.php index 6bf9acb2c12..43f4b4ceaa6 100644 --- a/src/Psalm/Internal/Fork/ForkProcessErrorMessage.php +++ b/src/Psalm/Internal/Fork/ForkProcessErrorMessage.php @@ -8,7 +8,7 @@ * @psalm-immutable * @internal */ -class ForkProcessErrorMessage implements ForkMessage +final class ForkProcessErrorMessage implements ForkMessage { use ImmutableNonCloneableTrait; diff --git a/src/Psalm/Internal/Fork/ForkTaskDoneMessage.php b/src/Psalm/Internal/Fork/ForkTaskDoneMessage.php index 11fff6edbd3..591b472db4b 100644 --- a/src/Psalm/Internal/Fork/ForkTaskDoneMessage.php +++ b/src/Psalm/Internal/Fork/ForkTaskDoneMessage.php @@ -8,7 +8,7 @@ * @psalm-immutable * @internal */ -class ForkTaskDoneMessage implements ForkMessage +final class ForkTaskDoneMessage implements ForkMessage { use ImmutableNonCloneableTrait; diff --git a/src/Psalm/Internal/Fork/Pool.php b/src/Psalm/Internal/Fork/Pool.php index 09e525dde6d..9311fc4d6fc 100644 --- a/src/Psalm/Internal/Fork/Pool.php +++ b/src/Psalm/Internal/Fork/Pool.php @@ -70,7 +70,7 @@ * * @internal */ -class Pool +final class Pool { private const EXIT_SUCCESS = 0; private const EXIT_FAILURE = 1; diff --git a/src/Psalm/Internal/Fork/PsalmRestarter.php b/src/Psalm/Internal/Fork/PsalmRestarter.php index 27840944143..53de9ec014c 100644 --- a/src/Psalm/Internal/Fork/PsalmRestarter.php +++ b/src/Psalm/Internal/Fork/PsalmRestarter.php @@ -22,7 +22,7 @@ /** * @internal */ -class PsalmRestarter extends XdebugHandler +final class PsalmRestarter extends XdebugHandler { private const REQUIRED_OPCACHE_SETTINGS = [ 'enable_cli' => true, diff --git a/src/Psalm/Internal/Json/Json.php b/src/Psalm/Internal/Json/Json.php index 10c823d2773..9cd7ebb743a 100644 --- a/src/Psalm/Internal/Json/Json.php +++ b/src/Psalm/Internal/Json/Json.php @@ -16,7 +16,7 @@ * * @internal */ -class Json +final class Json { public const PRETTY = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; diff --git a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php index c7f4e378656..1e65565df3c 100644 --- a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php @@ -13,7 +13,7 @@ * * @internal */ -class TextDocument +final class TextDocument { private ClientHandler $handler; diff --git a/src/Psalm/Internal/LanguageServer/Client/Workspace.php b/src/Psalm/Internal/LanguageServer/Client/Workspace.php index f9d9cf39e90..0df62350373 100644 --- a/src/Psalm/Internal/LanguageServer/Client/Workspace.php +++ b/src/Psalm/Internal/LanguageServer/Client/Workspace.php @@ -14,7 +14,7 @@ * * @internal */ -class Workspace +final class Workspace { private ClientHandler $handler; diff --git a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php index 115a38f6567..788d699fc16 100644 --- a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php +++ b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php @@ -9,7 +9,7 @@ /** * @internal */ -class ClientConfiguration +final class ClientConfiguration { /** diff --git a/src/Psalm/Internal/LanguageServer/ClientHandler.php b/src/Psalm/Internal/LanguageServer/ClientHandler.php index 06e48e3d143..5c2588516cf 100644 --- a/src/Psalm/Internal/LanguageServer/ClientHandler.php +++ b/src/Psalm/Internal/LanguageServer/ClientHandler.php @@ -17,7 +17,7 @@ /** * @internal */ -class ClientHandler +final class ClientHandler { public ProtocolReader $protocolReader; diff --git a/src/Psalm/Internal/LanguageServer/IdGenerator.php b/src/Psalm/Internal/LanguageServer/IdGenerator.php index 330015db5e7..29540b62625 100644 --- a/src/Psalm/Internal/LanguageServer/IdGenerator.php +++ b/src/Psalm/Internal/LanguageServer/IdGenerator.php @@ -9,7 +9,7 @@ * * @internal */ -class IdGenerator +final class IdGenerator { public int $counter = 1; diff --git a/src/Psalm/Internal/LanguageServer/LanguageClient.php b/src/Psalm/Internal/LanguageServer/LanguageClient.php index 9b092510e8d..4575aa3d575 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageClient.php +++ b/src/Psalm/Internal/LanguageServer/LanguageClient.php @@ -20,7 +20,7 @@ /** * @internal */ -class LanguageClient +final class LanguageClient { /** * Handles textDocument/* methods diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index fde6b541395..4b10f55ef27 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -99,7 +99,7 @@ * @psalm-api * @internal */ -class LanguageServer extends Dispatcher +final class LanguageServer extends Dispatcher { /** * Handles textDocument/* method calls diff --git a/src/Psalm/Internal/LanguageServer/Message.php b/src/Psalm/Internal/LanguageServer/Message.php index 56b46c23350..400cf9d0dbb 100644 --- a/src/Psalm/Internal/LanguageServer/Message.php +++ b/src/Psalm/Internal/LanguageServer/Message.php @@ -13,7 +13,7 @@ /** * @internal */ -class Message +final class Message { public ?MessageBody $body = null; diff --git a/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php b/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php index 3290ea5cd4c..0f954b257a3 100644 --- a/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php +++ b/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php @@ -15,7 +15,7 @@ * @psalm-api * @internal */ -class PHPMarkdownContent extends MarkupContent implements JsonSerializable +final class PHPMarkdownContent extends MarkupContent implements JsonSerializable { public string $code; diff --git a/src/Psalm/Internal/LanguageServer/Progress.php b/src/Psalm/Internal/LanguageServer/Progress.php index 8512b8055f8..50f74472c00 100644 --- a/src/Psalm/Internal/LanguageServer/Progress.php +++ b/src/Psalm/Internal/LanguageServer/Progress.php @@ -9,7 +9,7 @@ /** * @internal */ -class Progress extends Base +final class Progress extends Base { private ?LanguageServer $server = null; diff --git a/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php b/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php index 35031d8541d..026a6a7d56e 100644 --- a/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php +++ b/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php @@ -21,7 +21,7 @@ * * @internal */ -class ProtocolStreamReader implements ProtocolReader +final class ProtocolStreamReader implements ProtocolReader { use EmitterTrait; diff --git a/src/Psalm/Internal/LanguageServer/ProtocolStreamWriter.php b/src/Psalm/Internal/LanguageServer/ProtocolStreamWriter.php index 10d86e14cd9..fe9cdd86a29 100644 --- a/src/Psalm/Internal/LanguageServer/ProtocolStreamWriter.php +++ b/src/Psalm/Internal/LanguageServer/ProtocolStreamWriter.php @@ -10,7 +10,7 @@ /** * @internal */ -class ProtocolStreamWriter implements ProtocolWriter +final class ProtocolStreamWriter implements ProtocolWriter { private ResourceOutputStream $output; diff --git a/src/Psalm/Internal/LanguageServer/Provider/ClassLikeStorageCacheProvider.php b/src/Psalm/Internal/LanguageServer/Provider/ClassLikeStorageCacheProvider.php index f1925fe7904..1dbc6387d45 100644 --- a/src/Psalm/Internal/LanguageServer/Provider/ClassLikeStorageCacheProvider.php +++ b/src/Psalm/Internal/LanguageServer/Provider/ClassLikeStorageCacheProvider.php @@ -11,7 +11,7 @@ /** * @internal */ -class ClassLikeStorageCacheProvider extends InternalClassLikeStorageCacheProvider +final class ClassLikeStorageCacheProvider extends InternalClassLikeStorageCacheProvider { /** @var array */ private array $cache = []; diff --git a/src/Psalm/Internal/LanguageServer/Provider/FileReferenceCacheProvider.php b/src/Psalm/Internal/LanguageServer/Provider/FileReferenceCacheProvider.php index ca12912ec0a..b4476c2e118 100644 --- a/src/Psalm/Internal/LanguageServer/Provider/FileReferenceCacheProvider.php +++ b/src/Psalm/Internal/LanguageServer/Provider/FileReferenceCacheProvider.php @@ -11,7 +11,7 @@ * * @internal */ -class FileReferenceCacheProvider extends InternalFileReferenceCacheProvider +final class FileReferenceCacheProvider extends InternalFileReferenceCacheProvider { private ?array $cached_file_references = null; diff --git a/src/Psalm/Internal/LanguageServer/Provider/FileStorageCacheProvider.php b/src/Psalm/Internal/LanguageServer/Provider/FileStorageCacheProvider.php index 8841cf407a8..54186f6466f 100644 --- a/src/Psalm/Internal/LanguageServer/Provider/FileStorageCacheProvider.php +++ b/src/Psalm/Internal/LanguageServer/Provider/FileStorageCacheProvider.php @@ -10,7 +10,7 @@ /** * @internal */ -class FileStorageCacheProvider extends InternalFileStorageCacheProvider +final class FileStorageCacheProvider extends InternalFileStorageCacheProvider { /** @var array */ private array $cache = []; diff --git a/src/Psalm/Internal/LanguageServer/Provider/ParserCacheProvider.php b/src/Psalm/Internal/LanguageServer/Provider/ParserCacheProvider.php index 0b30a2dda57..498126d69c1 100644 --- a/src/Psalm/Internal/LanguageServer/Provider/ParserCacheProvider.php +++ b/src/Psalm/Internal/LanguageServer/Provider/ParserCacheProvider.php @@ -10,7 +10,7 @@ /** * @internal */ -class ParserCacheProvider extends InternalParserCacheProvider +final class ParserCacheProvider extends InternalParserCacheProvider { /** * @var array diff --git a/src/Psalm/Internal/LanguageServer/Provider/ProjectCacheProvider.php b/src/Psalm/Internal/LanguageServer/Provider/ProjectCacheProvider.php index ed210fa0ae2..80f758c83ca 100644 --- a/src/Psalm/Internal/LanguageServer/Provider/ProjectCacheProvider.php +++ b/src/Psalm/Internal/LanguageServer/Provider/ProjectCacheProvider.php @@ -7,7 +7,7 @@ /** * @internal */ -class ProjectCacheProvider extends PsalmProjectCacheProvider +final class ProjectCacheProvider extends PsalmProjectCacheProvider { private int $last_run = 0; diff --git a/src/Psalm/Internal/LanguageServer/Reference.php b/src/Psalm/Internal/LanguageServer/Reference.php index 32694c7c8df..345b08705e3 100644 --- a/src/Psalm/Internal/LanguageServer/Reference.php +++ b/src/Psalm/Internal/LanguageServer/Reference.php @@ -7,7 +7,7 @@ /** * @internal */ -class Reference +final class Reference { public string $file_path; public string $symbol; diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index cb359d5b9fe..a4af46cacec 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -38,7 +38,7 @@ * * @internal */ -class TextDocument +final class TextDocument { protected LanguageServer $server; diff --git a/src/Psalm/Internal/LanguageServer/Server/Workspace.php b/src/Psalm/Internal/LanguageServer/Server/Workspace.php index 6d2e1622575..75c810cf800 100644 --- a/src/Psalm/Internal/LanguageServer/Server/Workspace.php +++ b/src/Psalm/Internal/LanguageServer/Server/Workspace.php @@ -25,7 +25,7 @@ * * @internal */ -class Workspace +final class Workspace { protected LanguageServer $server; diff --git a/src/Psalm/Internal/MethodIdentifier.php b/src/Psalm/Internal/MethodIdentifier.php index ad519202430..8d2d18d6d23 100644 --- a/src/Psalm/Internal/MethodIdentifier.php +++ b/src/Psalm/Internal/MethodIdentifier.php @@ -15,7 +15,7 @@ * @psalm-immutable * @internal */ -class MethodIdentifier +final class MethodIdentifier { use ImmutableNonCloneableTrait; diff --git a/src/Psalm/Internal/PhpTraverser/CustomTraverser.php b/src/Psalm/Internal/PhpTraverser/CustomTraverser.php index 0f6279770d6..f1e2673572d 100644 --- a/src/Psalm/Internal/PhpTraverser/CustomTraverser.php +++ b/src/Psalm/Internal/PhpTraverser/CustomTraverser.php @@ -16,7 +16,7 @@ /** * @internal */ -class CustomTraverser extends NodeTraverser +final class CustomTraverser extends NodeTraverser { public function __construct() { diff --git a/src/Psalm/Internal/PhpVisitor/AssignmentMapVisitor.php b/src/Psalm/Internal/PhpVisitor/AssignmentMapVisitor.php index 703db43da3c..28b3770c167 100644 --- a/src/Psalm/Internal/PhpVisitor/AssignmentMapVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/AssignmentMapVisitor.php @@ -13,7 +13,7 @@ * With this map we can calculate how many times the loop analysis must * be run before all variables have the correct types */ -class AssignmentMapVisitor extends PhpParser\NodeVisitorAbstract +final class AssignmentMapVisitor extends PhpParser\NodeVisitorAbstract { /** * @var array> diff --git a/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php b/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php index c3289c5e3c4..4fe4afe5269 100644 --- a/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php @@ -7,7 +7,7 @@ /** * @internal */ -class CheckTrivialExprVisitor extends PhpParser\NodeVisitorAbstract +final class CheckTrivialExprVisitor extends PhpParser\NodeVisitorAbstract { /** * @var array diff --git a/src/Psalm/Internal/PhpVisitor/CloningVisitor.php b/src/Psalm/Internal/PhpVisitor/CloningVisitor.php index f6a089b1f87..1005caee248 100644 --- a/src/Psalm/Internal/PhpVisitor/CloningVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/CloningVisitor.php @@ -14,7 +14,7 @@ * * @internal */ -class CloningVisitor extends NodeVisitorAbstract +final class CloningVisitor extends NodeVisitorAbstract { public function enterNode(Node $node): Node { diff --git a/src/Psalm/Internal/PhpVisitor/ConditionCloningVisitor.php b/src/Psalm/Internal/PhpVisitor/ConditionCloningVisitor.php index b1e4018a1b0..c0c4d3e3f5a 100644 --- a/src/Psalm/Internal/PhpVisitor/ConditionCloningVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ConditionCloningVisitor.php @@ -12,7 +12,7 @@ /** * @internal */ -class ConditionCloningVisitor extends NodeVisitorAbstract +final class ConditionCloningVisitor extends NodeVisitorAbstract { private NodeDataProvider $type_provider; diff --git a/src/Psalm/Internal/PhpVisitor/NodeCleanerVisitor.php b/src/Psalm/Internal/PhpVisitor/NodeCleanerVisitor.php index 2342123c24c..d7a9ab78dbc 100644 --- a/src/Psalm/Internal/PhpVisitor/NodeCleanerVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/NodeCleanerVisitor.php @@ -8,7 +8,7 @@ /** * @internal */ -class NodeCleanerVisitor extends PhpParser\NodeVisitorAbstract +final class NodeCleanerVisitor extends PhpParser\NodeVisitorAbstract { private NodeDataProvider $type_provider; diff --git a/src/Psalm/Internal/PhpVisitor/NodeCounterVisitor.php b/src/Psalm/Internal/PhpVisitor/NodeCounterVisitor.php index fadcf34e4a0..c889bd2fe42 100644 --- a/src/Psalm/Internal/PhpVisitor/NodeCounterVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/NodeCounterVisitor.php @@ -7,7 +7,7 @@ /** * @internal */ -class NodeCounterVisitor extends PhpParser\NodeVisitorAbstract +final class NodeCounterVisitor extends PhpParser\NodeVisitorAbstract { public int $count = 0; diff --git a/src/Psalm/Internal/PhpVisitor/OffsetShifterVisitor.php b/src/Psalm/Internal/PhpVisitor/OffsetShifterVisitor.php index 2f6e1567d1d..9937ec8bb7e 100644 --- a/src/Psalm/Internal/PhpVisitor/OffsetShifterVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/OffsetShifterVisitor.php @@ -9,7 +9,7 @@ * * @internal */ -class OffsetShifterVisitor extends PhpParser\NodeVisitorAbstract +final class OffsetShifterVisitor extends PhpParser\NodeVisitorAbstract { private int $file_offset; diff --git a/src/Psalm/Internal/PhpVisitor/ParamReplacementVisitor.php b/src/Psalm/Internal/PhpVisitor/ParamReplacementVisitor.php index 11f1604b1a4..ce3574db13c 100644 --- a/src/Psalm/Internal/PhpVisitor/ParamReplacementVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ParamReplacementVisitor.php @@ -14,7 +14,7 @@ /** * @internal */ -class ParamReplacementVisitor extends PhpParser\NodeVisitorAbstract +final class ParamReplacementVisitor extends PhpParser\NodeVisitorAbstract { private string $old_name; diff --git a/src/Psalm/Internal/PhpVisitor/PartialParserVisitor.php b/src/Psalm/Internal/PhpVisitor/PartialParserVisitor.php index d1c09f32478..59343c14c1b 100644 --- a/src/Psalm/Internal/PhpVisitor/PartialParserVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/PartialParserVisitor.php @@ -28,7 +28,7 @@ * * @internal */ -class PartialParserVisitor extends PhpParser\NodeVisitorAbstract +final class PartialParserVisitor extends PhpParser\NodeVisitorAbstract { /** @var array */ private array $offset_map; diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/AttributeResolver.php b/src/Psalm/Internal/PhpVisitor/Reflector/AttributeResolver.php index 339d86229d0..63ec7c07293 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/AttributeResolver.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/AttributeResolver.php @@ -20,7 +20,7 @@ /** * @internal */ -class AttributeResolver +final class AttributeResolver { public static function resolve( Codebase $codebase, diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php index 2f1c5467b93..1d48be61220 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php @@ -48,7 +48,7 @@ /** * @internal */ -class ClassLikeDocblockParser +final class ClassLikeDocblockParser { /** * @throws DocblockParseException if there was a problem parsing the docblock diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index fae0c8daf26..c4a6e3b491a 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -92,7 +92,7 @@ /** * @internal */ -class ClassLikeNodeScanner +final class ClassLikeNodeScanner { private FileScanner $file_scanner; diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionResolver.php b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionResolver.php index ac05615b4fe..cf5dd8a0519 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionResolver.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionResolver.php @@ -45,7 +45,7 @@ /** * @internal */ -class ExpressionResolver +final class ExpressionResolver { public static function getUnresolvedClassConstExpr( PhpParser\Node\Expr $stmt, diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php index 696b7497d70..6afd142bfa5 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php @@ -36,7 +36,7 @@ /** * @internal */ -class ExpressionScanner +final class ExpressionScanner { public static function scan( Codebase $codebase, diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php index 7ade521fd10..e2a2cc5a14b 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php @@ -39,7 +39,7 @@ /** * @internal */ -class FunctionLikeDocblockParser +final class FunctionLikeDocblockParser { /** * @throws DocblockParseException if there was a problem parsing the docblock diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php index 9f9a477cb4d..828d894763e 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php @@ -69,7 +69,7 @@ /** * @internal */ -class FunctionLikeDocblockScanner +final class FunctionLikeDocblockScanner { /** * @param array> $existing_function_template_types diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index e0ba4e3478b..f04a90072d0 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -66,7 +66,7 @@ /** * @internal */ -class FunctionLikeNodeScanner +final class FunctionLikeNodeScanner { private FileScanner $file_scanner; diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php index 3e5fcef5d45..41c91d9ad39 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php @@ -26,7 +26,7 @@ /** * @internal */ -class TypeHintResolver +final class TypeHintResolver { /** * @param Identifier|IntersectionType|Name|NullableType|UnionType $hint diff --git a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php index 734c14d8b11..85d538c3ea8 100644 --- a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php @@ -51,7 +51,7 @@ /** * @internal */ -class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements FileSource +final class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements FileSource { private Aliases $aliases; diff --git a/src/Psalm/Internal/PhpVisitor/ShortClosureVisitor.php b/src/Psalm/Internal/PhpVisitor/ShortClosureVisitor.php index fe0bd1f4a92..aa14b34613a 100644 --- a/src/Psalm/Internal/PhpVisitor/ShortClosureVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ShortClosureVisitor.php @@ -9,7 +9,7 @@ /** * @internal */ -class ShortClosureVisitor extends PhpParser\NodeVisitorAbstract +final class ShortClosureVisitor extends PhpParser\NodeVisitorAbstract { /** * @var array diff --git a/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php b/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php index d33c19d64e8..1eca6d3fd10 100644 --- a/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php +++ b/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php @@ -17,7 +17,7 @@ /** * @internal */ -class SimpleNameResolver extends NodeVisitorAbstract +final class SimpleNameResolver extends NodeVisitorAbstract { private NameContext $nameContext; diff --git a/src/Psalm/Internal/PhpVisitor/TraitFinder.php b/src/Psalm/Internal/PhpVisitor/TraitFinder.php index 9d5717dad10..eefc452b274 100644 --- a/src/Psalm/Internal/PhpVisitor/TraitFinder.php +++ b/src/Psalm/Internal/PhpVisitor/TraitFinder.php @@ -17,7 +17,7 @@ * * @internal */ -class TraitFinder extends PhpParser\NodeVisitorAbstract +final class TraitFinder extends PhpParser\NodeVisitorAbstract { /** @var list */ private array $matching_trait_nodes = []; diff --git a/src/Psalm/Internal/PhpVisitor/TypeMappingVisitor.php b/src/Psalm/Internal/PhpVisitor/TypeMappingVisitor.php index 354a639e4cc..a42a9881dbf 100644 --- a/src/Psalm/Internal/PhpVisitor/TypeMappingVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/TypeMappingVisitor.php @@ -11,7 +11,7 @@ /** * @internal */ -class TypeMappingVisitor extends NodeVisitorAbstract +final class TypeMappingVisitor extends NodeVisitorAbstract { private NodeDataProvider $fake_type_provider; private NodeDataProvider $real_type_provider; diff --git a/src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php b/src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php index 4d9f075a5e7..881b28b3644 100644 --- a/src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php +++ b/src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php @@ -16,7 +16,7 @@ /** * @internal */ -class YieldTypeCollector extends NodeVisitorAbstract +final class YieldTypeCollector extends NodeVisitorAbstract { /** @var list */ private array $yield_types = []; diff --git a/src/Psalm/Internal/PluginManager/Command/DisableCommand.php b/src/Psalm/Internal/PluginManager/Command/DisableCommand.php index c6b5dd1d89e..af7b4bb90d9 100644 --- a/src/Psalm/Internal/PluginManager/Command/DisableCommand.php +++ b/src/Psalm/Internal/PluginManager/Command/DisableCommand.php @@ -21,7 +21,7 @@ /** * @internal */ -class DisableCommand extends Command +final class DisableCommand extends Command { private PluginListFactory $plugin_list_factory; diff --git a/src/Psalm/Internal/PluginManager/Command/EnableCommand.php b/src/Psalm/Internal/PluginManager/Command/EnableCommand.php index c520dac31b5..6278b7018f2 100644 --- a/src/Psalm/Internal/PluginManager/Command/EnableCommand.php +++ b/src/Psalm/Internal/PluginManager/Command/EnableCommand.php @@ -21,7 +21,7 @@ /** * @internal */ -class EnableCommand extends Command +final class EnableCommand extends Command { private PluginListFactory $plugin_list_factory; diff --git a/src/Psalm/Internal/PluginManager/Command/ShowCommand.php b/src/Psalm/Internal/PluginManager/Command/ShowCommand.php index 40ac6d0638b..a8e78a732c4 100644 --- a/src/Psalm/Internal/PluginManager/Command/ShowCommand.php +++ b/src/Psalm/Internal/PluginManager/Command/ShowCommand.php @@ -22,7 +22,7 @@ /** * @internal */ -class ShowCommand extends Command +final class ShowCommand extends Command { private PluginListFactory $plugin_list_factory; diff --git a/src/Psalm/Internal/PluginManager/ComposerLock.php b/src/Psalm/Internal/PluginManager/ComposerLock.php index e9c36807c87..43bfdcfceaa 100644 --- a/src/Psalm/Internal/PluginManager/ComposerLock.php +++ b/src/Psalm/Internal/PluginManager/ComposerLock.php @@ -15,7 +15,7 @@ /** * @internal */ -class ComposerLock +final class ComposerLock { /** @var string[] */ private array $file_names; diff --git a/src/Psalm/Internal/PluginManager/ConfigFile.php b/src/Psalm/Internal/PluginManager/ConfigFile.php index 84ffed92ff6..4fdc21a8823 100644 --- a/src/Psalm/Internal/PluginManager/ConfigFile.php +++ b/src/Psalm/Internal/PluginManager/ConfigFile.php @@ -17,7 +17,7 @@ /** * @internal */ -class ConfigFile +final class ConfigFile { private string $path; diff --git a/src/Psalm/Internal/PluginManager/PluginList.php b/src/Psalm/Internal/PluginManager/PluginList.php index 789dcc0ad0a..0eb325bd28c 100644 --- a/src/Psalm/Internal/PluginManager/PluginList.php +++ b/src/Psalm/Internal/PluginManager/PluginList.php @@ -14,7 +14,7 @@ /** * @internal */ -class PluginList +final class PluginList { private ?ConfigFile $config_file = null; diff --git a/src/Psalm/Internal/PluginManager/PluginListFactory.php b/src/Psalm/Internal/PluginManager/PluginListFactory.php index e6104032776..950b6dd24a6 100644 --- a/src/Psalm/Internal/PluginManager/PluginListFactory.php +++ b/src/Psalm/Internal/PluginManager/PluginListFactory.php @@ -16,7 +16,7 @@ /** * @internal */ -class PluginListFactory +final class PluginListFactory { private string $project_root; diff --git a/src/Psalm/Internal/Provider/AddRemoveTaints/HtmlFunctionTainter.php b/src/Psalm/Internal/Provider/AddRemoveTaints/HtmlFunctionTainter.php index 1f862278d48..d49ebcee48b 100644 --- a/src/Psalm/Internal/Provider/AddRemoveTaints/HtmlFunctionTainter.php +++ b/src/Psalm/Internal/Provider/AddRemoveTaints/HtmlFunctionTainter.php @@ -16,7 +16,7 @@ /** * @internal */ -class HtmlFunctionTainter implements AddTaintsInterface, RemoveTaintsInterface +final class HtmlFunctionTainter implements AddTaintsInterface, RemoveTaintsInterface { /** * Called to see what taints should be added diff --git a/src/Psalm/Internal/Provider/ClassLikeStorageProvider.php b/src/Psalm/Internal/Provider/ClassLikeStorageProvider.php index 9b8c82617ea..353bdb3f408 100644 --- a/src/Psalm/Internal/Provider/ClassLikeStorageProvider.php +++ b/src/Psalm/Internal/Provider/ClassLikeStorageProvider.php @@ -12,7 +12,7 @@ /** * @internal */ -class ClassLikeStorageProvider +final class ClassLikeStorageProvider { /** * Storing this statically is much faster (at least in PHP 7.2.1) diff --git a/src/Psalm/Internal/Provider/FakeFileProvider.php b/src/Psalm/Internal/Provider/FakeFileProvider.php index 756e0d72806..734cd64dab8 100644 --- a/src/Psalm/Internal/Provider/FakeFileProvider.php +++ b/src/Psalm/Internal/Provider/FakeFileProvider.php @@ -8,7 +8,7 @@ /** * @internal */ -class FakeFileProvider extends FileProvider +final class FakeFileProvider extends FileProvider { /** * @var array diff --git a/src/Psalm/Internal/Provider/FileReferenceProvider.php b/src/Psalm/Internal/Provider/FileReferenceProvider.php index 15950320835..b89a2604128 100644 --- a/src/Psalm/Internal/Provider/FileReferenceProvider.php +++ b/src/Psalm/Internal/Provider/FileReferenceProvider.php @@ -22,7 +22,7 @@ * @psalm-import-type FileMapType from Analyzer * @internal */ -class FileReferenceProvider +final class FileReferenceProvider { private bool $loaded_from_cache = false; diff --git a/src/Psalm/Internal/Provider/FileStorageProvider.php b/src/Psalm/Internal/Provider/FileStorageProvider.php index 946d3c46062..91bbbbe8c52 100644 --- a/src/Psalm/Internal/Provider/FileStorageProvider.php +++ b/src/Psalm/Internal/Provider/FileStorageProvider.php @@ -11,7 +11,7 @@ /** * @internal */ -class FileStorageProvider +final class FileStorageProvider { /** * A list of data useful to analyse files diff --git a/src/Psalm/Internal/Provider/FunctionExistenceProvider.php b/src/Psalm/Internal/Provider/FunctionExistenceProvider.php index a3c2f7e17e5..3f5c96f5f5c 100644 --- a/src/Psalm/Internal/Provider/FunctionExistenceProvider.php +++ b/src/Psalm/Internal/Provider/FunctionExistenceProvider.php @@ -13,7 +13,7 @@ /** * @internal */ -class FunctionExistenceProvider +final class FunctionExistenceProvider { /** * @var array< diff --git a/src/Psalm/Internal/Provider/FunctionParamsProvider.php b/src/Psalm/Internal/Provider/FunctionParamsProvider.php index cd83b294ac6..b34d2b0a10d 100644 --- a/src/Psalm/Internal/Provider/FunctionParamsProvider.php +++ b/src/Psalm/Internal/Provider/FunctionParamsProvider.php @@ -16,7 +16,7 @@ /** * @internal */ -class FunctionParamsProvider +final class FunctionParamsProvider { /** * @var array< diff --git a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php index ab43dcf5c17..04a5a74d572 100644 --- a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php @@ -55,7 +55,7 @@ /** * @internal */ -class FunctionReturnTypeProvider +final class FunctionReturnTypeProvider { /** * @var array< diff --git a/src/Psalm/Internal/Provider/MethodExistenceProvider.php b/src/Psalm/Internal/Provider/MethodExistenceProvider.php index 6d3b9612980..7f85cf9e6ed 100644 --- a/src/Psalm/Internal/Provider/MethodExistenceProvider.php +++ b/src/Psalm/Internal/Provider/MethodExistenceProvider.php @@ -13,7 +13,7 @@ /** * @internal */ -class MethodExistenceProvider +final class MethodExistenceProvider { /** * @var array< diff --git a/src/Psalm/Internal/Provider/MethodParamsProvider.php b/src/Psalm/Internal/Provider/MethodParamsProvider.php index a8faa312ccd..7074627cd05 100644 --- a/src/Psalm/Internal/Provider/MethodParamsProvider.php +++ b/src/Psalm/Internal/Provider/MethodParamsProvider.php @@ -19,7 +19,7 @@ /** * @internal */ -class MethodParamsProvider +final class MethodParamsProvider { /** * @var array< diff --git a/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php b/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php index bba8d867243..a892575be77 100644 --- a/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php @@ -22,7 +22,7 @@ /** * @internal */ -class MethodReturnTypeProvider +final class MethodReturnTypeProvider { /** * @var array< diff --git a/src/Psalm/Internal/Provider/MethodVisibilityProvider.php b/src/Psalm/Internal/Provider/MethodVisibilityProvider.php index 276de010434..b4309d232df 100644 --- a/src/Psalm/Internal/Provider/MethodVisibilityProvider.php +++ b/src/Psalm/Internal/Provider/MethodVisibilityProvider.php @@ -15,7 +15,7 @@ /** * @internal */ -class MethodVisibilityProvider +final class MethodVisibilityProvider { /** * @var array< diff --git a/src/Psalm/Internal/Provider/NodeDataProvider.php b/src/Psalm/Internal/Provider/NodeDataProvider.php index 8b4a2274274..bf384767b32 100644 --- a/src/Psalm/Internal/Provider/NodeDataProvider.php +++ b/src/Psalm/Internal/Provider/NodeDataProvider.php @@ -20,7 +20,7 @@ /** * @internal */ -class NodeDataProvider implements NodeTypeProvider +final class NodeDataProvider implements NodeTypeProvider { /** @var SplObjectStorage */ private SplObjectStorage $node_types; diff --git a/src/Psalm/Internal/Provider/PropertyExistenceProvider.php b/src/Psalm/Internal/Provider/PropertyExistenceProvider.php index 9593ec3d183..101bf1c2fe7 100644 --- a/src/Psalm/Internal/Provider/PropertyExistenceProvider.php +++ b/src/Psalm/Internal/Provider/PropertyExistenceProvider.php @@ -15,7 +15,7 @@ /** * @internal */ -class PropertyExistenceProvider +final class PropertyExistenceProvider { /** * @var array< diff --git a/src/Psalm/Internal/Provider/PropertyTypeProvider.php b/src/Psalm/Internal/Provider/PropertyTypeProvider.php index 9fc62b024ac..0089621023e 100644 --- a/src/Psalm/Internal/Provider/PropertyTypeProvider.php +++ b/src/Psalm/Internal/Provider/PropertyTypeProvider.php @@ -16,7 +16,7 @@ /** * @internal */ -class PropertyTypeProvider +final class PropertyTypeProvider { /** * @var array< diff --git a/src/Psalm/Internal/Provider/PropertyTypeProvider/DomDocumentPropertyTypeProvider.php b/src/Psalm/Internal/Provider/PropertyTypeProvider/DomDocumentPropertyTypeProvider.php index d529abe1850..a76061780b8 100644 --- a/src/Psalm/Internal/Provider/PropertyTypeProvider/DomDocumentPropertyTypeProvider.php +++ b/src/Psalm/Internal/Provider/PropertyTypeProvider/DomDocumentPropertyTypeProvider.php @@ -15,7 +15,7 @@ /** * @internal */ -class DomDocumentPropertyTypeProvider implements PropertyTypeProviderInterface +final class DomDocumentPropertyTypeProvider implements PropertyTypeProviderInterface { private static ?Union $cache = null; public static function getPropertyType(PropertyTypeProviderEvent $event): ?Union diff --git a/src/Psalm/Internal/Provider/PropertyVisibilityProvider.php b/src/Psalm/Internal/Provider/PropertyVisibilityProvider.php index 099bf800733..ea0f450a6a7 100644 --- a/src/Psalm/Internal/Provider/PropertyVisibilityProvider.php +++ b/src/Psalm/Internal/Provider/PropertyVisibilityProvider.php @@ -14,7 +14,7 @@ /** * @internal */ -class PropertyVisibilityProvider +final class PropertyVisibilityProvider { /** * @var array< diff --git a/src/Psalm/Internal/Provider/Providers.php b/src/Psalm/Internal/Provider/Providers.php index b3a5a2a8abb..9e74bc67ef6 100644 --- a/src/Psalm/Internal/Provider/Providers.php +++ b/src/Psalm/Internal/Provider/Providers.php @@ -16,7 +16,7 @@ /** * @internal */ -class Providers +final class Providers { public FileProvider $file_provider; diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayChunkReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayChunkReturnTypeProvider.php index 5e949bec3e1..4e236619a13 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayChunkReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayChunkReturnTypeProvider.php @@ -16,7 +16,7 @@ /** * @internal */ -class ArrayChunkReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class ArrayChunkReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php index dbb3f3dfb36..22377c272e0 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php @@ -21,7 +21,7 @@ /** * @internal */ -class ArrayColumnReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class ArrayColumnReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayCombineReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayCombineReturnTypeProvider.php index 88125e398e3..57de9047e55 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayCombineReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayCombineReturnTypeProvider.php @@ -19,7 +19,7 @@ /** * @internal */ -class ArrayCombineReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class ArrayCombineReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillKeysReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillKeysReturnTypeProvider.php index c40175a13ea..8b891887b6d 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillKeysReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillKeysReturnTypeProvider.php @@ -15,7 +15,7 @@ /** * @internal */ -class ArrayFillKeysReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class ArrayFillKeysReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillReturnTypeProvider.php index 3d0a08f2faf..7653fd64880 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillReturnTypeProvider.php @@ -14,7 +14,7 @@ /** * @internal */ -class ArrayFillReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class ArrayFillReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php index 4f2a81e1f47..3e44102f912 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php @@ -36,7 +36,7 @@ /** * @internal */ -class ArrayFilterReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class ArrayFilterReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php index 910c5d5c9e6..3d30faa936d 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php @@ -48,7 +48,7 @@ /** * @internal */ -class ArrayMapReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class ArrayMapReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php index 394988baee1..b93ff08826e 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php @@ -26,7 +26,7 @@ /** * @internal */ -class ArrayMergeReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class ArrayMergeReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPadReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPadReturnTypeProvider.php index f818945d779..ff616f908a9 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPadReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPadReturnTypeProvider.php @@ -17,7 +17,7 @@ /** * @internal */ -class ArrayPadReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class ArrayPadReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php index a9421acfa4c..df1c3a0518a 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php @@ -23,7 +23,7 @@ /** * @internal */ -class ArrayPointerAdjustmentReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class ArrayPointerAdjustmentReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * These functions are already handled by the CoreGenericFunctions stub diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php index 1d6806ffe7d..a167a8ea2b3 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php @@ -15,7 +15,7 @@ /** * @internal */ -class ArrayPopReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class ArrayPopReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayRandReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayRandReturnTypeProvider.php index f4a6689051a..cb8e402dac5 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayRandReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayRandReturnTypeProvider.php @@ -13,7 +13,7 @@ /** * @internal */ -class ArrayRandReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class ArrayRandReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php index 9b4ee1dd53d..7153792184d 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php @@ -31,7 +31,7 @@ /** * @internal */ -class ArrayReduceReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class ArrayReduceReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReverseReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReverseReturnTypeProvider.php index 4a77a7322cb..8d20cd11b57 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReverseReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReverseReturnTypeProvider.php @@ -15,7 +15,7 @@ /** * @internal */ -class ArrayReverseReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class ArrayReverseReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArraySliceReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArraySliceReturnTypeProvider.php index 64d988b8498..122488bc257 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArraySliceReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArraySliceReturnTypeProvider.php @@ -19,7 +19,7 @@ /** * @internal */ -class ArraySliceReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class ArraySliceReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArraySpliceReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArraySpliceReturnTypeProvider.php index 90abc661e12..93bcd4c271e 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArraySpliceReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArraySpliceReturnTypeProvider.php @@ -13,7 +13,7 @@ /** * @internal */ -class ArraySpliceReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class ArraySpliceReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/BasenameReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/BasenameReturnTypeProvider.php index 72d54927170..8e11092326e 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/BasenameReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/BasenameReturnTypeProvider.php @@ -16,7 +16,7 @@ /** * @internal */ -class BasenameReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class BasenameReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ClosureFromCallableReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ClosureFromCallableReturnTypeProvider.php index 9d33e12ef57..69c9f7ec020 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ClosureFromCallableReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ClosureFromCallableReturnTypeProvider.php @@ -14,7 +14,7 @@ /** * @internal */ -class ClosureFromCallableReturnTypeProvider implements MethodReturnTypeProviderInterface +final class ClosureFromCallableReturnTypeProvider implements MethodReturnTypeProviderInterface { public static function getClassLikeNames(): array { diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/DateReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/DateReturnTypeProvider.php index 13bc381defd..956bffb61e2 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/DateReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/DateReturnTypeProvider.php @@ -19,7 +19,7 @@ /** * @internal */ -class DateReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class DateReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/DateTimeModifyReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/DateTimeModifyReturnTypeProvider.php index b693d788ff1..2d2453fefdb 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/DateTimeModifyReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/DateTimeModifyReturnTypeProvider.php @@ -13,7 +13,7 @@ /** * @internal */ -class DateTimeModifyReturnTypeProvider implements MethodReturnTypeProviderInterface +final class DateTimeModifyReturnTypeProvider implements MethodReturnTypeProviderInterface { public static function getClassLikeNames(): array { diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/DirnameReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/DirnameReturnTypeProvider.php index 6d96bf81e35..f6c63c47ac4 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/DirnameReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/DirnameReturnTypeProvider.php @@ -19,7 +19,7 @@ /** * @internal */ -class DirnameReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class DirnameReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/DomNodeAppendChild.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/DomNodeAppendChild.php index e714f74c7c5..ee4de71ecf5 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/DomNodeAppendChild.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/DomNodeAppendChild.php @@ -11,7 +11,7 @@ /** * @internal */ -class DomNodeAppendChild implements MethodReturnTypeProviderInterface +final class DomNodeAppendChild implements MethodReturnTypeProviderInterface { public static function getClassLikeNames(): array { diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php index 19c839c44b3..bbf3642fa63 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php @@ -31,7 +31,7 @@ /** * @internal */ -class FilterVarReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class FilterVarReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FirstArgStringReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FirstArgStringReturnTypeProvider.php index 25332b58215..e9df5f741e5 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FirstArgStringReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FirstArgStringReturnTypeProvider.php @@ -13,7 +13,7 @@ /** * @internal */ -class FirstArgStringReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class FirstArgStringReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/GetClassMethodsReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/GetClassMethodsReturnTypeProvider.php index a5f07266a36..c2da32919ae 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/GetClassMethodsReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/GetClassMethodsReturnTypeProvider.php @@ -11,7 +11,7 @@ /** * @internal */ -class GetClassMethodsReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class GetClassMethodsReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php index 04b8b59cd51..55e4f38bd42 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php @@ -31,7 +31,7 @@ /** * @internal */ -class GetObjectVarsReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class GetObjectVarsReturnTypeProvider implements FunctionReturnTypeProviderInterface { public static function getFunctionIds(): array { diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/HexdecReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/HexdecReturnTypeProvider.php index 0cbd821ab00..f99d9bed7ef 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/HexdecReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/HexdecReturnTypeProvider.php @@ -11,7 +11,7 @@ /** * @internal */ -class HexdecReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class HexdecReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ImagickPixelColorReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ImagickPixelColorReturnTypeProvider.php index b756e98a5dc..1b121df74bc 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ImagickPixelColorReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ImagickPixelColorReturnTypeProvider.php @@ -16,7 +16,7 @@ /** * @internal */ -class ImagickPixelColorReturnTypeProvider implements MethodReturnTypeProviderInterface +final class ImagickPixelColorReturnTypeProvider implements MethodReturnTypeProviderInterface { public static function getClassLikeNames(): array { diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/InArrayReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/InArrayReturnTypeProvider.php index 4e675915e8e..c80b10f50dc 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/InArrayReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/InArrayReturnTypeProvider.php @@ -13,7 +13,7 @@ /** * @internal */ -class InArrayReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class InArrayReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/IteratorToArrayReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/IteratorToArrayReturnTypeProvider.php index 94167577fb5..db47500e12f 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/IteratorToArrayReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/IteratorToArrayReturnTypeProvider.php @@ -22,7 +22,7 @@ /** * @internal */ -class IteratorToArrayReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class IteratorToArrayReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/MbInternalEncodingReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/MbInternalEncodingReturnTypeProvider.php index 053efea8e1c..3c918678deb 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/MbInternalEncodingReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/MbInternalEncodingReturnTypeProvider.php @@ -21,7 +21,7 @@ /** * @internal */ -class MbInternalEncodingReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class MbInternalEncodingReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php index 2f4b7fffe87..db7000bc721 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php @@ -27,7 +27,7 @@ /** * @internal */ -class MinMaxReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class MinMaxReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/MktimeReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/MktimeReturnTypeProvider.php index 8eb1b7d2c02..a65b838b1a5 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/MktimeReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/MktimeReturnTypeProvider.php @@ -13,7 +13,7 @@ /** * @internal */ -class MktimeReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class MktimeReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ParseUrlReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ParseUrlReturnTypeProvider.php index bda2ab4f89e..5486741df63 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ParseUrlReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ParseUrlReturnTypeProvider.php @@ -29,7 +29,7 @@ /** * @internal */ -class ParseUrlReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class ParseUrlReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php index 0ed2c493f62..8c18ecfd9dc 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php @@ -17,7 +17,7 @@ /** * @internal */ -class PdoStatementReturnTypeProvider implements MethodReturnTypeProviderInterface +final class PdoStatementReturnTypeProvider implements MethodReturnTypeProviderInterface { public static function getClassLikeNames(): array { diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementSetFetchMode.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementSetFetchMode.php index cfb060f8e0c..91192788b3d 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementSetFetchMode.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementSetFetchMode.php @@ -12,7 +12,7 @@ /** * @internal */ -class PdoStatementSetFetchMode implements MethodParamsProviderInterface +final class PdoStatementSetFetchMode implements MethodParamsProviderInterface { public static function getClassLikeNames(): array { diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PowReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PowReturnTypeProvider.php index bffd07fcd3d..dcf7fb2db89 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PowReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PowReturnTypeProvider.php @@ -16,7 +16,7 @@ /** * @internal */ -class PowReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class PowReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/RandReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/RandReturnTypeProvider.php index ea6184e70fd..98ef8759223 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/RandReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/RandReturnTypeProvider.php @@ -16,7 +16,7 @@ /** * @internal */ -class RandReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class RandReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/RoundReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/RoundReturnTypeProvider.php index eb8ee561f54..32779b41f7e 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/RoundReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/RoundReturnTypeProvider.php @@ -18,7 +18,7 @@ /** * @internal */ -class RoundReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class RoundReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/SprintfReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/SprintfReturnTypeProvider.php index cca21b07ecf..3e7bbbc1283 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/SprintfReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/SprintfReturnTypeProvider.php @@ -31,7 +31,7 @@ /** * @internal */ -class SprintfReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class SprintfReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/StrReplaceReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/StrReplaceReturnTypeProvider.php index c7d64270691..78ebc6a32fb 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/StrReplaceReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/StrReplaceReturnTypeProvider.php @@ -14,7 +14,7 @@ /** * @internal */ -class StrReplaceReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class StrReplaceReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/StrTrReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/StrTrReturnTypeProvider.php index 721afd0713c..9994e696cba 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/StrTrReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/StrTrReturnTypeProvider.php @@ -15,7 +15,7 @@ /** * @internal */ -class StrTrReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class StrTrReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/TriggerErrorReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/TriggerErrorReturnTypeProvider.php index dfb8a173545..c0a812a40a7 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/TriggerErrorReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/TriggerErrorReturnTypeProvider.php @@ -25,7 +25,7 @@ /** * @internal */ -class TriggerErrorReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class TriggerErrorReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/VersionCompareReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/VersionCompareReturnTypeProvider.php index 6340feddbaf..3b9d313db45 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/VersionCompareReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/VersionCompareReturnTypeProvider.php @@ -17,7 +17,7 @@ /** * @internal */ -class VersionCompareReturnTypeProvider implements FunctionReturnTypeProviderInterface +final class VersionCompareReturnTypeProvider implements FunctionReturnTypeProviderInterface { /** * @return array diff --git a/src/Psalm/Internal/Provider/StatementsProvider.php b/src/Psalm/Internal/Provider/StatementsProvider.php index 21bccd1c35c..bd8e34a4b8c 100644 --- a/src/Psalm/Internal/Provider/StatementsProvider.php +++ b/src/Psalm/Internal/Provider/StatementsProvider.php @@ -39,7 +39,7 @@ /** * @internal */ -class StatementsProvider +final class StatementsProvider { private FileProvider $file_provider; diff --git a/src/Psalm/Internal/ReferenceConstraint.php b/src/Psalm/Internal/ReferenceConstraint.php index 64db0e24344..74bfb4eef88 100644 --- a/src/Psalm/Internal/ReferenceConstraint.php +++ b/src/Psalm/Internal/ReferenceConstraint.php @@ -10,7 +10,7 @@ /** * @internal */ -class ReferenceConstraint +final class ReferenceConstraint { public ?Union $type = null; diff --git a/src/Psalm/Internal/Scanner/ClassLikeDocblockComment.php b/src/Psalm/Internal/Scanner/ClassLikeDocblockComment.php index 0d68ece0f25..77a14ec379d 100644 --- a/src/Psalm/Internal/Scanner/ClassLikeDocblockComment.php +++ b/src/Psalm/Internal/Scanner/ClassLikeDocblockComment.php @@ -7,7 +7,7 @@ /** * @internal */ -class ClassLikeDocblockComment +final class ClassLikeDocblockComment { /** * Whether or not the class is deprecated diff --git a/src/Psalm/Internal/Scanner/DocblockParser.php b/src/Psalm/Internal/Scanner/DocblockParser.php index 0c052346bef..52459acbed8 100644 --- a/src/Psalm/Internal/Scanner/DocblockParser.php +++ b/src/Psalm/Internal/Scanner/DocblockParser.php @@ -31,7 +31,7 @@ * * @internal */ -class DocblockParser +final class DocblockParser { /** * $offsetStart is the absolute position of the docblock in the file. It'll be used to add to the position of some diff --git a/src/Psalm/Internal/Scanner/FunctionDocblockComment.php b/src/Psalm/Internal/Scanner/FunctionDocblockComment.php index 400dbcc6904..959cfb3231f 100644 --- a/src/Psalm/Internal/Scanner/FunctionDocblockComment.php +++ b/src/Psalm/Internal/Scanner/FunctionDocblockComment.php @@ -5,7 +5,7 @@ /** * @internal */ -class FunctionDocblockComment +final class FunctionDocblockComment { public ?string $return_type = null; diff --git a/src/Psalm/Internal/Scanner/ParsedDocblock.php b/src/Psalm/Internal/Scanner/ParsedDocblock.php index 1e5b0f89692..bdd18f429d0 100644 --- a/src/Psalm/Internal/Scanner/ParsedDocblock.php +++ b/src/Psalm/Internal/Scanner/ParsedDocblock.php @@ -8,7 +8,7 @@ /** * @internal */ -class ParsedDocblock +final class ParsedDocblock { public string $description; diff --git a/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php b/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php index b812456d7af..6058c39ccbd 100644 --- a/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php +++ b/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php @@ -23,7 +23,7 @@ /** * @internal */ -class PhpStormMetaScanner +final class PhpStormMetaScanner { /** * @param list $args diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/ArrayOffsetFetch.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/ArrayOffsetFetch.php index dcff9097691..7efbfb48c77 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/ArrayOffsetFetch.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/ArrayOffsetFetch.php @@ -8,7 +8,7 @@ * @psalm-immutable * @internal */ -class ArrayOffsetFetch extends UnresolvedConstantComponent +final class ArrayOffsetFetch extends UnresolvedConstantComponent { public UnresolvedConstantComponent $array; diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/ArraySpread.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/ArraySpread.php index 7078bdcc671..ef8c443d7ba 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/ArraySpread.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/ArraySpread.php @@ -8,7 +8,7 @@ * @psalm-immutable * @internal */ -class ArraySpread extends UnresolvedConstantComponent +final class ArraySpread extends UnresolvedConstantComponent { public UnresolvedConstantComponent $array; diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/ArrayValue.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/ArrayValue.php index eb56c44533b..6fc943daa79 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/ArrayValue.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/ArrayValue.php @@ -8,7 +8,7 @@ * @psalm-immutable * @internal */ -class ArrayValue extends UnresolvedConstantComponent +final class ArrayValue extends UnresolvedConstantComponent { /** @var array */ public array $entries; diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/ClassConstant.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/ClassConstant.php index e75bd93b318..101c18a48ea 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/ClassConstant.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/ClassConstant.php @@ -8,7 +8,7 @@ * @psalm-immutable * @internal */ -class ClassConstant extends UnresolvedConstantComponent +final class ClassConstant extends UnresolvedConstantComponent { public string $fqcln; diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/Constant.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/Constant.php index f7363e5dafc..45c36ee7bf7 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/Constant.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/Constant.php @@ -8,7 +8,7 @@ * @psalm-immutable * @internal */ -class Constant extends UnresolvedConstantComponent +final class Constant extends UnresolvedConstantComponent { public string $name; diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/EnumNameFetch.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/EnumNameFetch.php index 29639fb3ceb..5b56cc696ed 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/EnumNameFetch.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/EnumNameFetch.php @@ -6,6 +6,6 @@ * @psalm-immutable * @internal */ -class EnumNameFetch extends EnumPropertyFetch +final class EnumNameFetch extends EnumPropertyFetch { } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/EnumValueFetch.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/EnumValueFetch.php index 1ee9c003dd8..4387213787b 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/EnumValueFetch.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/EnumValueFetch.php @@ -6,6 +6,6 @@ * @psalm-immutable * @internal */ -class EnumValueFetch extends EnumPropertyFetch +final class EnumValueFetch extends EnumPropertyFetch { } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/KeyValuePair.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/KeyValuePair.php index 607bc7a58f8..adb980ed4db 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/KeyValuePair.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/KeyValuePair.php @@ -8,7 +8,7 @@ * @psalm-immutable * @internal */ -class KeyValuePair extends UnresolvedConstantComponent +final class KeyValuePair extends UnresolvedConstantComponent { public ?UnresolvedConstantComponent $key = null; diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/ScalarValue.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/ScalarValue.php index 688387ef6c1..6e1f292cec9 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/ScalarValue.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/ScalarValue.php @@ -8,7 +8,7 @@ * @psalm-immutable * @internal */ -class ScalarValue extends UnresolvedConstantComponent +final class ScalarValue extends UnresolvedConstantComponent { /** @var string|int|float|bool|null */ public $value; diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedAdditionOp.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedAdditionOp.php index 64c33e5874f..21637ee839f 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedAdditionOp.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedAdditionOp.php @@ -6,6 +6,6 @@ * @psalm-immutable * @internal */ -class UnresolvedAdditionOp extends UnresolvedBinaryOp +final class UnresolvedAdditionOp extends UnresolvedBinaryOp { } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedBitwiseAnd.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedBitwiseAnd.php index bbd471f08e7..1b8e4ae018b 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedBitwiseAnd.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedBitwiseAnd.php @@ -6,6 +6,6 @@ * @psalm-immutable * @internal */ -class UnresolvedBitwiseAnd extends UnresolvedBinaryOp +final class UnresolvedBitwiseAnd extends UnresolvedBinaryOp { } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedBitwiseOr.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedBitwiseOr.php index 6995432d1fe..9b2956d12c0 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedBitwiseOr.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedBitwiseOr.php @@ -6,6 +6,6 @@ * @psalm-immutable * @internal */ -class UnresolvedBitwiseOr extends UnresolvedBinaryOp +final class UnresolvedBitwiseOr extends UnresolvedBinaryOp { } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedBitwiseXor.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedBitwiseXor.php index d2070eb941e..62102caf69e 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedBitwiseXor.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedBitwiseXor.php @@ -6,6 +6,6 @@ * @psalm-immutable * @internal */ -class UnresolvedBitwiseXor extends UnresolvedBinaryOp +final class UnresolvedBitwiseXor extends UnresolvedBinaryOp { } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedConcatOp.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedConcatOp.php index 2d7910ead63..bd8b9671411 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedConcatOp.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedConcatOp.php @@ -8,7 +8,7 @@ * @psalm-immutable * @internal */ -class UnresolvedConcatOp extends UnresolvedBinaryOp +final class UnresolvedConcatOp extends UnresolvedBinaryOp { use ImmutableNonCloneableTrait; } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedDivisionOp.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedDivisionOp.php index 00ee1dd6be8..27c83dad87c 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedDivisionOp.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedDivisionOp.php @@ -8,7 +8,7 @@ * @psalm-immutable * @internal */ -class UnresolvedDivisionOp extends UnresolvedBinaryOp +final class UnresolvedDivisionOp extends UnresolvedBinaryOp { use ImmutableNonCloneableTrait; } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedMultiplicationOp.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedMultiplicationOp.php index f167d9d8fc7..fdfde38ef66 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedMultiplicationOp.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedMultiplicationOp.php @@ -8,7 +8,7 @@ * @psalm-immutable * @internal */ -class UnresolvedMultiplicationOp extends UnresolvedBinaryOp +final class UnresolvedMultiplicationOp extends UnresolvedBinaryOp { use ImmutableNonCloneableTrait; } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedSubtractionOp.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedSubtractionOp.php index 2b111f3aeda..26096d7b81a 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedSubtractionOp.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedSubtractionOp.php @@ -8,7 +8,7 @@ * @psalm-immutable * @internal */ -class UnresolvedSubtractionOp extends UnresolvedBinaryOp +final class UnresolvedSubtractionOp extends UnresolvedBinaryOp { use ImmutableNonCloneableTrait; } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedTernary.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedTernary.php index 294b129e01c..8a2acf839ae 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedTernary.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedTernary.php @@ -9,7 +9,7 @@ * @psalm-immutable * @internal */ -class UnresolvedTernary extends UnresolvedConstantComponent +final class UnresolvedTernary extends UnresolvedConstantComponent { use ImmutableNonCloneableTrait; diff --git a/src/Psalm/Internal/Scanner/VarDocblockComment.php b/src/Psalm/Internal/Scanner/VarDocblockComment.php index 4a1651c9044..15150bd0ced 100644 --- a/src/Psalm/Internal/Scanner/VarDocblockComment.php +++ b/src/Psalm/Internal/Scanner/VarDocblockComment.php @@ -7,7 +7,7 @@ /** * @internal */ -class VarDocblockComment +final class VarDocblockComment { public ?Union $type = null; diff --git a/src/Psalm/Internal/Scope/CaseScope.php b/src/Psalm/Internal/Scope/CaseScope.php index a96832f4eff..7bc396f1ca7 100644 --- a/src/Psalm/Internal/Scope/CaseScope.php +++ b/src/Psalm/Internal/Scope/CaseScope.php @@ -8,7 +8,7 @@ /** * @internal */ -class CaseScope +final class CaseScope { public Context $parent_context; diff --git a/src/Psalm/Internal/Scope/FinallyScope.php b/src/Psalm/Internal/Scope/FinallyScope.php index 4fcda32379f..816ed49904e 100644 --- a/src/Psalm/Internal/Scope/FinallyScope.php +++ b/src/Psalm/Internal/Scope/FinallyScope.php @@ -7,7 +7,7 @@ /** * @internal */ -class FinallyScope +final class FinallyScope { /** * @var array diff --git a/src/Psalm/Internal/Scope/IfConditionalScope.php b/src/Psalm/Internal/Scope/IfConditionalScope.php index 5d7579c594e..acd923a58f7 100644 --- a/src/Psalm/Internal/Scope/IfConditionalScope.php +++ b/src/Psalm/Internal/Scope/IfConditionalScope.php @@ -8,7 +8,7 @@ /** * @internal */ -class IfConditionalScope +final class IfConditionalScope { public Context $if_context; diff --git a/src/Psalm/Internal/Scope/IfScope.php b/src/Psalm/Internal/Scope/IfScope.php index 27a5181d970..4d75ee24462 100644 --- a/src/Psalm/Internal/Scope/IfScope.php +++ b/src/Psalm/Internal/Scope/IfScope.php @@ -10,7 +10,7 @@ /** * @internal */ -class IfScope +final class IfScope { /** * @var array|null diff --git a/src/Psalm/Internal/Scope/LoopScope.php b/src/Psalm/Internal/Scope/LoopScope.php index 1eb1fa0026e..76b059329f1 100644 --- a/src/Psalm/Internal/Scope/LoopScope.php +++ b/src/Psalm/Internal/Scope/LoopScope.php @@ -8,7 +8,7 @@ /** * @internal */ -class LoopScope +final class LoopScope { public int $iteration_count = 0; diff --git a/src/Psalm/Internal/Scope/SwitchScope.php b/src/Psalm/Internal/Scope/SwitchScope.php index 64937c78ae3..c5f5938ebf1 100644 --- a/src/Psalm/Internal/Scope/SwitchScope.php +++ b/src/Psalm/Internal/Scope/SwitchScope.php @@ -9,7 +9,7 @@ /** * @internal */ -class SwitchScope +final class SwitchScope { /** * @var array|null diff --git a/src/Psalm/Internal/Stubs/Generator/ClassLikeStubGenerator.php b/src/Psalm/Internal/Stubs/Generator/ClassLikeStubGenerator.php index 4ea25eabbe6..58888f21df9 100644 --- a/src/Psalm/Internal/Stubs/Generator/ClassLikeStubGenerator.php +++ b/src/Psalm/Internal/Stubs/Generator/ClassLikeStubGenerator.php @@ -28,7 +28,7 @@ /** * @internal */ -class ClassLikeStubGenerator +final class ClassLikeStubGenerator { /** * @return PhpParser\Node\Stmt\Class_|PhpParser\Node\Stmt\Interface_|PhpParser\Node\Stmt\Trait_ diff --git a/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php b/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php index df5c8d40e7e..95909902309 100644 --- a/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php +++ b/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php @@ -51,7 +51,7 @@ /** * @internal */ -class StubsGenerator +final class StubsGenerator { public static function getAll( Codebase $codebase, diff --git a/src/Psalm/Internal/Type/ArrayType.php b/src/Psalm/Internal/Type/ArrayType.php index c9e1d17acf7..f115fcc9879 100644 --- a/src/Psalm/Internal/Type/ArrayType.php +++ b/src/Psalm/Internal/Type/ArrayType.php @@ -15,7 +15,7 @@ /** * @internal */ -class ArrayType +final class ArrayType { public Union $key; diff --git a/src/Psalm/Internal/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index 85410c4c09c..d82a5c07384 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -68,7 +68,7 @@ /** * @internal */ -class AssertionReconciler extends Reconciler +final class AssertionReconciler extends Reconciler { /** * Reconciles types diff --git a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php index 6977e465876..c440526fea5 100644 --- a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php @@ -16,7 +16,7 @@ /** * @internal */ -class ArrayTypeComparator +final class ArrayTypeComparator { /** * @param TArray|TKeyedArray|TClassStringMap $input_type_part diff --git a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php index 26516c0a9e3..0b3478ea978 100644 --- a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php @@ -48,7 +48,7 @@ /** * @internal */ -class AtomicTypeComparator +final class AtomicTypeComparator { /** * Does the input param atomic type match the given param atomic type diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index 2999480dd36..855445fadd0 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -37,7 +37,7 @@ /** * @internal */ -class CallableTypeComparator +final class CallableTypeComparator { /** * @param TCallable|TClosure $input_type_part diff --git a/src/Psalm/Internal/Type/Comparator/ClassLikeStringComparator.php b/src/Psalm/Internal/Type/Comparator/ClassLikeStringComparator.php index a3830ea83d4..0fe514cc300 100644 --- a/src/Psalm/Internal/Type/Comparator/ClassLikeStringComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ClassLikeStringComparator.php @@ -14,7 +14,7 @@ /** * @internal */ -class ClassLikeStringComparator +final class ClassLikeStringComparator { /** * @param TClassString|TLiteralClassString $input_type_part diff --git a/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php b/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php index 964ffdf0828..fe91a3bc003 100644 --- a/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php @@ -12,7 +12,7 @@ /** * @internal */ -class GenericTypeComparator +final class GenericTypeComparator { /** * @param TGenericObject|TIterable $container_type_part diff --git a/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php b/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php index e462143b563..3263efdb78b 100644 --- a/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php @@ -16,7 +16,7 @@ /** * @internal */ -class IntegerRangeComparator +final class IntegerRangeComparator { /** * This method is used to check if an integer range can be contained in another diff --git a/src/Psalm/Internal/Type/Comparator/KeyedArrayComparator.php b/src/Psalm/Internal/Type/Comparator/KeyedArrayComparator.php index c9d7aad5842..1fad958847d 100644 --- a/src/Psalm/Internal/Type/Comparator/KeyedArrayComparator.php +++ b/src/Psalm/Internal/Type/Comparator/KeyedArrayComparator.php @@ -19,7 +19,7 @@ /** * @internal */ -class KeyedArrayComparator +final class KeyedArrayComparator { /** * @param TKeyedArray|TObjectWithProperties $input_type_part diff --git a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php index 1d264658dd8..88b239efb06 100644 --- a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php @@ -23,7 +23,7 @@ /** * @internal */ -class ObjectComparator +final class ObjectComparator { /** * @param TNamedObject|TTemplateParam|TIterable $input_type_part diff --git a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php index b8941667156..c688407cad6 100644 --- a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php @@ -45,7 +45,7 @@ /** * @internal */ -class ScalarTypeComparator +final class ScalarTypeComparator { public static function isContainedBy( Codebase $codebase, diff --git a/src/Psalm/Internal/Type/Comparator/TypeComparisonResult.php b/src/Psalm/Internal/Type/Comparator/TypeComparisonResult.php index 284d946b591..3933881fd76 100644 --- a/src/Psalm/Internal/Type/Comparator/TypeComparisonResult.php +++ b/src/Psalm/Internal/Type/Comparator/TypeComparisonResult.php @@ -8,7 +8,7 @@ /** * @internal */ -class TypeComparisonResult +final class TypeComparisonResult { /** * This is used to trigger `InvalidScalarArgument` in situations where we know PHP diff --git a/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php b/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php index d3c1b457322..7a22dc87325 100644 --- a/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php @@ -29,7 +29,7 @@ /** * @internal */ -class UnionTypeComparator +final class UnionTypeComparator { /** * Does the input param type match the given param type diff --git a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php index ab7da873005..af2b84b2764 100644 --- a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php @@ -41,7 +41,7 @@ /** * @internal */ -class NegatedAssertionReconciler extends Reconciler +final class NegatedAssertionReconciler extends Reconciler { /** * @param string[] $suppressed_issues diff --git a/src/Psalm/Internal/Type/ParseTree/CallableParamTree.php b/src/Psalm/Internal/Type/ParseTree/CallableParamTree.php index 4154483e76f..53bc98bbbc8 100644 --- a/src/Psalm/Internal/Type/ParseTree/CallableParamTree.php +++ b/src/Psalm/Internal/Type/ParseTree/CallableParamTree.php @@ -7,7 +7,7 @@ /** * @internal */ -class CallableParamTree extends ParseTree +final class CallableParamTree extends ParseTree { public bool $variadic = false; diff --git a/src/Psalm/Internal/Type/ParseTree/CallableTree.php b/src/Psalm/Internal/Type/ParseTree/CallableTree.php index 5f3c64014cc..f249d06e88e 100644 --- a/src/Psalm/Internal/Type/ParseTree/CallableTree.php +++ b/src/Psalm/Internal/Type/ParseTree/CallableTree.php @@ -7,7 +7,7 @@ /** * @internal */ -class CallableTree extends ParseTree +final class CallableTree extends ParseTree { public string $value; diff --git a/src/Psalm/Internal/Type/ParseTree/CallableWithReturnTypeTree.php b/src/Psalm/Internal/Type/ParseTree/CallableWithReturnTypeTree.php index 3069d73681c..a1dc3798253 100644 --- a/src/Psalm/Internal/Type/ParseTree/CallableWithReturnTypeTree.php +++ b/src/Psalm/Internal/Type/ParseTree/CallableWithReturnTypeTree.php @@ -7,6 +7,6 @@ /** * @internal */ -class CallableWithReturnTypeTree extends ParseTree +final class CallableWithReturnTypeTree extends ParseTree { } diff --git a/src/Psalm/Internal/Type/ParseTree/ConditionalTree.php b/src/Psalm/Internal/Type/ParseTree/ConditionalTree.php index 786d1ca07b9..cada2a61c45 100644 --- a/src/Psalm/Internal/Type/ParseTree/ConditionalTree.php +++ b/src/Psalm/Internal/Type/ParseTree/ConditionalTree.php @@ -7,7 +7,7 @@ /** * @internal */ -class ConditionalTree extends ParseTree +final class ConditionalTree extends ParseTree { public TemplateIsTree $condition; diff --git a/src/Psalm/Internal/Type/ParseTree/EncapsulationTree.php b/src/Psalm/Internal/Type/ParseTree/EncapsulationTree.php index 63fc3e0fafb..45027ce5ec3 100644 --- a/src/Psalm/Internal/Type/ParseTree/EncapsulationTree.php +++ b/src/Psalm/Internal/Type/ParseTree/EncapsulationTree.php @@ -7,7 +7,7 @@ /** * @internal */ -class EncapsulationTree extends ParseTree +final class EncapsulationTree extends ParseTree { public bool $terminated = false; } diff --git a/src/Psalm/Internal/Type/ParseTree/FieldEllipsis.php b/src/Psalm/Internal/Type/ParseTree/FieldEllipsis.php index 8ec2156c599..58f6fcf79bb 100644 --- a/src/Psalm/Internal/Type/ParseTree/FieldEllipsis.php +++ b/src/Psalm/Internal/Type/ParseTree/FieldEllipsis.php @@ -7,6 +7,6 @@ /** * @internal */ -class FieldEllipsis extends ParseTree +final class FieldEllipsis extends ParseTree { } diff --git a/src/Psalm/Internal/Type/ParseTree/GenericTree.php b/src/Psalm/Internal/Type/ParseTree/GenericTree.php index cefe8e4bb66..9b84271d06d 100644 --- a/src/Psalm/Internal/Type/ParseTree/GenericTree.php +++ b/src/Psalm/Internal/Type/ParseTree/GenericTree.php @@ -7,7 +7,7 @@ /** * @internal */ -class GenericTree extends ParseTree +final class GenericTree extends ParseTree { public string $value; diff --git a/src/Psalm/Internal/Type/ParseTree/IndexedAccessTree.php b/src/Psalm/Internal/Type/ParseTree/IndexedAccessTree.php index c50f401d343..fdaa1e98cfc 100644 --- a/src/Psalm/Internal/Type/ParseTree/IndexedAccessTree.php +++ b/src/Psalm/Internal/Type/ParseTree/IndexedAccessTree.php @@ -7,7 +7,7 @@ /** * @internal */ -class IndexedAccessTree extends ParseTree +final class IndexedAccessTree extends ParseTree { public string $value; diff --git a/src/Psalm/Internal/Type/ParseTree/IntersectionTree.php b/src/Psalm/Internal/Type/ParseTree/IntersectionTree.php index 7025160753e..f7bea8dbfb5 100644 --- a/src/Psalm/Internal/Type/ParseTree/IntersectionTree.php +++ b/src/Psalm/Internal/Type/ParseTree/IntersectionTree.php @@ -7,6 +7,6 @@ /** * @internal */ -class IntersectionTree extends ParseTree +final class IntersectionTree extends ParseTree { } diff --git a/src/Psalm/Internal/Type/ParseTree/KeyedArrayPropertyTree.php b/src/Psalm/Internal/Type/ParseTree/KeyedArrayPropertyTree.php index ba17ccfc1dd..443275f6100 100644 --- a/src/Psalm/Internal/Type/ParseTree/KeyedArrayPropertyTree.php +++ b/src/Psalm/Internal/Type/ParseTree/KeyedArrayPropertyTree.php @@ -7,7 +7,7 @@ /** * @internal */ -class KeyedArrayPropertyTree extends ParseTree +final class KeyedArrayPropertyTree extends ParseTree { public string $value; diff --git a/src/Psalm/Internal/Type/ParseTree/KeyedArrayTree.php b/src/Psalm/Internal/Type/ParseTree/KeyedArrayTree.php index 90c1249bd1b..f3ec3e4fe89 100644 --- a/src/Psalm/Internal/Type/ParseTree/KeyedArrayTree.php +++ b/src/Psalm/Internal/Type/ParseTree/KeyedArrayTree.php @@ -7,7 +7,7 @@ /** * @internal */ -class KeyedArrayTree extends ParseTree +final class KeyedArrayTree extends ParseTree { public string $value; diff --git a/src/Psalm/Internal/Type/ParseTree/MethodParamTree.php b/src/Psalm/Internal/Type/ParseTree/MethodParamTree.php index 82f8bd80a01..39237a1b9d2 100644 --- a/src/Psalm/Internal/Type/ParseTree/MethodParamTree.php +++ b/src/Psalm/Internal/Type/ParseTree/MethodParamTree.php @@ -7,7 +7,7 @@ /** * @internal */ -class MethodParamTree extends ParseTree +final class MethodParamTree extends ParseTree { public bool $variadic; diff --git a/src/Psalm/Internal/Type/ParseTree/MethodTree.php b/src/Psalm/Internal/Type/ParseTree/MethodTree.php index f77494b405c..12f800c565e 100644 --- a/src/Psalm/Internal/Type/ParseTree/MethodTree.php +++ b/src/Psalm/Internal/Type/ParseTree/MethodTree.php @@ -7,7 +7,7 @@ /** * @internal */ -class MethodTree extends ParseTree +final class MethodTree extends ParseTree { public string $value; diff --git a/src/Psalm/Internal/Type/ParseTree/MethodWithReturnTypeTree.php b/src/Psalm/Internal/Type/ParseTree/MethodWithReturnTypeTree.php index 47701b3ce7c..ed81373f73b 100644 --- a/src/Psalm/Internal/Type/ParseTree/MethodWithReturnTypeTree.php +++ b/src/Psalm/Internal/Type/ParseTree/MethodWithReturnTypeTree.php @@ -7,6 +7,6 @@ /** * @internal */ -class MethodWithReturnTypeTree extends ParseTree +final class MethodWithReturnTypeTree extends ParseTree { } diff --git a/src/Psalm/Internal/Type/ParseTree/NullableTree.php b/src/Psalm/Internal/Type/ParseTree/NullableTree.php index 2471184da64..8a18e12b0ac 100644 --- a/src/Psalm/Internal/Type/ParseTree/NullableTree.php +++ b/src/Psalm/Internal/Type/ParseTree/NullableTree.php @@ -7,6 +7,6 @@ /** * @internal */ -class NullableTree extends ParseTree +final class NullableTree extends ParseTree { } diff --git a/src/Psalm/Internal/Type/ParseTree/Root.php b/src/Psalm/Internal/Type/ParseTree/Root.php index b08f963480f..35aefa0b108 100644 --- a/src/Psalm/Internal/Type/ParseTree/Root.php +++ b/src/Psalm/Internal/Type/ParseTree/Root.php @@ -7,6 +7,6 @@ /** * @internal */ -class Root extends ParseTree +final class Root extends ParseTree { } diff --git a/src/Psalm/Internal/Type/ParseTree/TemplateAsTree.php b/src/Psalm/Internal/Type/ParseTree/TemplateAsTree.php index bb5de0c72b6..f350184f2b9 100644 --- a/src/Psalm/Internal/Type/ParseTree/TemplateAsTree.php +++ b/src/Psalm/Internal/Type/ParseTree/TemplateAsTree.php @@ -7,7 +7,7 @@ /** * @internal */ -class TemplateAsTree extends ParseTree +final class TemplateAsTree extends ParseTree { public string $param_name; diff --git a/src/Psalm/Internal/Type/ParseTree/TemplateIsTree.php b/src/Psalm/Internal/Type/ParseTree/TemplateIsTree.php index 4ad27411daf..2cc6c06e3ac 100644 --- a/src/Psalm/Internal/Type/ParseTree/TemplateIsTree.php +++ b/src/Psalm/Internal/Type/ParseTree/TemplateIsTree.php @@ -7,7 +7,7 @@ /** * @internal */ -class TemplateIsTree extends ParseTree +final class TemplateIsTree extends ParseTree { public string $param_name; diff --git a/src/Psalm/Internal/Type/ParseTree/UnionTree.php b/src/Psalm/Internal/Type/ParseTree/UnionTree.php index 1f8de487211..93f6195fcb5 100644 --- a/src/Psalm/Internal/Type/ParseTree/UnionTree.php +++ b/src/Psalm/Internal/Type/ParseTree/UnionTree.php @@ -7,6 +7,6 @@ /** * @internal */ -class UnionTree extends ParseTree +final class UnionTree extends ParseTree { } diff --git a/src/Psalm/Internal/Type/ParseTree/Value.php b/src/Psalm/Internal/Type/ParseTree/Value.php index e61d14efbe6..1f9a506f42f 100644 --- a/src/Psalm/Internal/Type/ParseTree/Value.php +++ b/src/Psalm/Internal/Type/ParseTree/Value.php @@ -7,7 +7,7 @@ /** * @internal */ -class Value extends ParseTree +final class Value extends ParseTree { public string $value; diff --git a/src/Psalm/Internal/Type/ParseTreeCreator.php b/src/Psalm/Internal/Type/ParseTreeCreator.php index 0ea26080433..b9b7368d34e 100644 --- a/src/Psalm/Internal/Type/ParseTreeCreator.php +++ b/src/Psalm/Internal/Type/ParseTreeCreator.php @@ -34,7 +34,7 @@ /** * @internal */ -class ParseTreeCreator +final class ParseTreeCreator { private ParseTree $parse_tree; diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index aa606ec8f89..f63100c49aa 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -97,7 +97,7 @@ * * @internal */ -class SimpleAssertionReconciler extends Reconciler +final class SimpleAssertionReconciler extends Reconciler { /** * @param string[] $suppressed_issues diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index 160bdc7482f..06d654ec1c2 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -68,7 +68,7 @@ /** * @internal */ -class SimpleNegatedAssertionReconciler extends Reconciler +final class SimpleNegatedAssertionReconciler extends Reconciler { /** * @param string[] $suppressed_issues diff --git a/src/Psalm/Internal/Type/TemplateBound.php b/src/Psalm/Internal/Type/TemplateBound.php index 01aada21077..71e219afa43 100644 --- a/src/Psalm/Internal/Type/TemplateBound.php +++ b/src/Psalm/Internal/Type/TemplateBound.php @@ -7,7 +7,7 @@ /** * @internal */ -class TemplateBound +final class TemplateBound { public Union $type; diff --git a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php index 259aedd9521..ab367ca66c8 100644 --- a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -38,7 +38,7 @@ /** * @internal */ -class TemplateInferredTypeReplacer +final class TemplateInferredTypeReplacer { /** * This replaces template types in unions with the inferred types they should be diff --git a/src/Psalm/Internal/Type/TemplateResult.php b/src/Psalm/Internal/Type/TemplateResult.php index 72a7a05cd5e..a759e4f5507 100644 --- a/src/Psalm/Internal/Type/TemplateResult.php +++ b/src/Psalm/Internal/Type/TemplateResult.php @@ -24,7 +24,7 @@ * * @internal */ -class TemplateResult +final class TemplateResult { /** * @var array> diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index bbab4e24b7b..7ce88ebb54c 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -53,7 +53,7 @@ /** * @internal */ -class TemplateStandinTypeReplacer +final class TemplateStandinTypeReplacer { /** * This method fills in the values in $template_result based on how the various atomic types diff --git a/src/Psalm/Internal/Type/TypeAlias/ClassTypeAlias.php b/src/Psalm/Internal/Type/TypeAlias/ClassTypeAlias.php index 49267d9924e..d3076affc00 100644 --- a/src/Psalm/Internal/Type/TypeAlias/ClassTypeAlias.php +++ b/src/Psalm/Internal/Type/TypeAlias/ClassTypeAlias.php @@ -8,7 +8,7 @@ /** * @internal */ -class ClassTypeAlias implements TypeAlias +final class ClassTypeAlias implements TypeAlias { /** * @var list diff --git a/src/Psalm/Internal/Type/TypeAlias/InlineTypeAlias.php b/src/Psalm/Internal/Type/TypeAlias/InlineTypeAlias.php index ab49068d4b1..03d09f216fb 100644 --- a/src/Psalm/Internal/Type/TypeAlias/InlineTypeAlias.php +++ b/src/Psalm/Internal/Type/TypeAlias/InlineTypeAlias.php @@ -9,7 +9,7 @@ * @psalm-immutable * @internal */ -class InlineTypeAlias implements TypeAlias +final class InlineTypeAlias implements TypeAlias { use ImmutableNonCloneableTrait; diff --git a/src/Psalm/Internal/Type/TypeAlias/LinkableTypeAlias.php b/src/Psalm/Internal/Type/TypeAlias/LinkableTypeAlias.php index 96aa6678936..3eb75ce824c 100644 --- a/src/Psalm/Internal/Type/TypeAlias/LinkableTypeAlias.php +++ b/src/Psalm/Internal/Type/TypeAlias/LinkableTypeAlias.php @@ -9,7 +9,7 @@ * @psalm-immutable * @internal */ -class LinkableTypeAlias implements TypeAlias +final class LinkableTypeAlias implements TypeAlias { use ImmutableNonCloneableTrait; diff --git a/src/Psalm/Internal/Type/TypeCombination.php b/src/Psalm/Internal/Type/TypeCombination.php index 863ae77f932..0706315d1ac 100644 --- a/src/Psalm/Internal/Type/TypeCombination.php +++ b/src/Psalm/Internal/Type/TypeCombination.php @@ -22,7 +22,7 @@ /** * @internal */ -class TypeCombination +final class TypeCombination { /** @var array */ public array $value_types = []; diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index ee1dd03cd27..3f841da4fa3 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -74,7 +74,7 @@ /** * @internal */ -class TypeCombiner +final class TypeCombiner { /** * Combines types together diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index e329f92b632..795b5ad9a8a 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -51,7 +51,7 @@ /** * @internal */ -class TypeExpander +final class TypeExpander { /** * @psalm-suppress InaccessibleProperty We just created the type diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 0287ab4d6bd..770e7efc958 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -101,7 +101,7 @@ * @psalm-suppress InaccessibleProperty Allowed during construction * @internal */ -class TypeParser +final class TypeParser { /** * Parses a string type representation diff --git a/src/Psalm/Internal/Type/TypeTokenizer.php b/src/Psalm/Internal/Type/TypeTokenizer.php index 66e463e4ccd..ce0660972dd 100644 --- a/src/Psalm/Internal/Type/TypeTokenizer.php +++ b/src/Psalm/Internal/Type/TypeTokenizer.php @@ -22,7 +22,7 @@ /** * @internal */ -class TypeTokenizer +final class TypeTokenizer { /** * @var array diff --git a/src/Psalm/Internal/TypeVisitor/CanContainObjectTypeVisitor.php b/src/Psalm/Internal/TypeVisitor/CanContainObjectTypeVisitor.php index b107ba2eaa0..0dee9e263a9 100644 --- a/src/Psalm/Internal/TypeVisitor/CanContainObjectTypeVisitor.php +++ b/src/Psalm/Internal/TypeVisitor/CanContainObjectTypeVisitor.php @@ -10,7 +10,7 @@ use Psalm\Type\Union; /** @internal */ -class CanContainObjectTypeVisitor extends TypeVisitor +final class CanContainObjectTypeVisitor extends TypeVisitor { private bool $contains_object_type = false; diff --git a/src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php b/src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php index 576f647cd3e..ce54f8959b0 100644 --- a/src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php +++ b/src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php @@ -14,7 +14,7 @@ /** * @internal */ -class ClasslikeReplacer extends MutableTypeVisitor +final class ClasslikeReplacer extends MutableTypeVisitor { private string $old; private string $new; diff --git a/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php b/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php index faedea36d6d..7a93f182713 100644 --- a/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php +++ b/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php @@ -13,7 +13,7 @@ /** * @internal */ -class ContainsClassLikeVisitor extends TypeVisitor +final class ContainsClassLikeVisitor extends TypeVisitor { /** * @var lowercase-string diff --git a/src/Psalm/Internal/TypeVisitor/ContainsLiteralVisitor.php b/src/Psalm/Internal/TypeVisitor/ContainsLiteralVisitor.php index e2358063480..dfb3367740b 100644 --- a/src/Psalm/Internal/TypeVisitor/ContainsLiteralVisitor.php +++ b/src/Psalm/Internal/TypeVisitor/ContainsLiteralVisitor.php @@ -14,7 +14,7 @@ /** * @internal */ -class ContainsLiteralVisitor extends TypeVisitor +final class ContainsLiteralVisitor extends TypeVisitor { private bool $contains_literal = false; diff --git a/src/Psalm/Internal/TypeVisitor/ContainsStaticVisitor.php b/src/Psalm/Internal/TypeVisitor/ContainsStaticVisitor.php index c8ef3fdd8a4..1465381a83c 100644 --- a/src/Psalm/Internal/TypeVisitor/ContainsStaticVisitor.php +++ b/src/Psalm/Internal/TypeVisitor/ContainsStaticVisitor.php @@ -9,7 +9,7 @@ /** * @internal */ -class ContainsStaticVisitor extends TypeVisitor +final class ContainsStaticVisitor extends TypeVisitor { private bool $contains_static = false; diff --git a/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php b/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php index 5e624820a9d..189166483c7 100644 --- a/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php +++ b/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php @@ -12,7 +12,7 @@ /** * @internal */ -class FromDocblockSetter extends MutableTypeVisitor +final class FromDocblockSetter extends MutableTypeVisitor { private bool $from_docblock; public function __construct(bool $from_docblock) diff --git a/src/Psalm/Internal/TypeVisitor/TemplateTypeCollector.php b/src/Psalm/Internal/TypeVisitor/TemplateTypeCollector.php index a6b2275a466..36db3898416 100644 --- a/src/Psalm/Internal/TypeVisitor/TemplateTypeCollector.php +++ b/src/Psalm/Internal/TypeVisitor/TemplateTypeCollector.php @@ -13,7 +13,7 @@ /** * @internal */ -class TemplateTypeCollector extends TypeVisitor +final class TemplateTypeCollector extends TypeVisitor { /** * @var list diff --git a/src/Psalm/Internal/TypeVisitor/TypeChecker.php b/src/Psalm/Internal/TypeVisitor/TypeChecker.php index 2818b414c16..e55ef5de0cd 100644 --- a/src/Psalm/Internal/TypeVisitor/TypeChecker.php +++ b/src/Psalm/Internal/TypeVisitor/TypeChecker.php @@ -45,7 +45,7 @@ /** * @internal */ -class TypeChecker extends TypeVisitor +final class TypeChecker extends TypeVisitor { private StatementsSource $source; diff --git a/src/Psalm/Internal/TypeVisitor/TypeLocalizer.php b/src/Psalm/Internal/TypeVisitor/TypeLocalizer.php index 81ba6a0eb42..4a2866e93da 100644 --- a/src/Psalm/Internal/TypeVisitor/TypeLocalizer.php +++ b/src/Psalm/Internal/TypeVisitor/TypeLocalizer.php @@ -17,7 +17,7 @@ /** * @internal */ -class TypeLocalizer extends MutableTypeVisitor +final class TypeLocalizer extends MutableTypeVisitor { /** * @var array> diff --git a/src/Psalm/Internal/TypeVisitor/TypeScanner.php b/src/Psalm/Internal/TypeVisitor/TypeScanner.php index d840e7327c2..524044f6358 100644 --- a/src/Psalm/Internal/TypeVisitor/TypeScanner.php +++ b/src/Psalm/Internal/TypeVisitor/TypeScanner.php @@ -15,7 +15,7 @@ /** * @internal */ -class TypeScanner extends TypeVisitor +final class TypeScanner extends TypeVisitor { private Scanner $scanner; From 07b45b8116ec07fab08518a432dfdd82bdcef46c Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 26 Oct 2023 17:18:33 +0200 Subject: [PATCH 012/357] Get rid of legacy 7.3 logic --- .../Internal/Analyzer/ProjectAnalyzer.php | 31 +++---------------- .../Analyzer/Statements/Block/TryAnalyzer.php | 6 +--- src/Psalm/Internal/Cli/Psalm.php | 24 -------------- src/Psalm/Internal/Fork/Pool.php | 19 ------------ 4 files changed, 6 insertions(+), 74 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index 6be055ea43b..e1e47e7451a 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -72,7 +72,6 @@ use function fwrite; use function implode; use function in_array; -use function ini_get; use function is_dir; use function is_file; use function microtime; @@ -86,11 +85,8 @@ use function strtolower; use function substr; use function usort; -use function version_compare; use const PHP_EOL; -use const PHP_OS; -use const PHP_VERSION; use const PSALM_VERSION; use const STDERR; @@ -391,21 +387,13 @@ public function serverMode(LanguageServer $server): void $this->file_reference_provider->loadReferenceCache(); $this->codebase->enterServerMode(); - if (ini_get('pcre.jit') === '1' - && PHP_OS === 'Darwin' - && version_compare(PHP_VERSION, '7.3.0') >= 0 - && version_compare(PHP_VERSION, '7.4.0') < 0 - ) { - // do nothing - } else { - $cpu_count = self::getCpuCount(); + $cpu_count = self::getCpuCount(); - // let's not go crazy - $usable_cpus = $cpu_count - 2; + // let's not go crazy + $usable_cpus = $cpu_count - 2; - if ($usable_cpus > 1) { - $this->threads = $usable_cpus; - } + if ($usable_cpus > 1) { + $this->threads = $usable_cpus; } $server->logInfo("Initializing: Initialize Plugins..."); @@ -1356,15 +1344,6 @@ public static function getCpuCount(): int return 1; } - // PHP 7.3 with JIT on OSX is screwed for multi-threads - if (ini_get('pcre.jit') === '1' - && PHP_OS === 'Darwin' - && version_compare(PHP_VERSION, '7.3.0') >= 0 - && version_compare(PHP_VERSION, '7.4.0') < 0 - ) { - return 1; - } - if (!extension_loaded('pcntl')) { // Psalm requires pcntl for multi-threads support return 1; diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php index 18536b49394..3f6ade09a3b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php @@ -25,9 +25,6 @@ use function in_array; use function is_string; use function strtolower; -use function version_compare; - -use const PHP_VERSION; /** * @internal @@ -267,8 +264,7 @@ public static function analyze( $fq_catch_class, false, false, - version_compare(PHP_VERSION, '7.0.0dev', '>=') - && strtolower($fq_catch_class) !== 'throwable' + strtolower($fq_catch_class) !== 'throwable' && $codebase->interfaceExists($fq_catch_class) && !$codebase->interfaceExtends($fq_catch_class, 'Throwable') ? ['Throwable' => new TNamedObject('Throwable')] diff --git a/src/Psalm/Internal/Cli/Psalm.php b/src/Psalm/Internal/Cli/Psalm.php index d4cd2f8f8fb..dda0e27530b 100644 --- a/src/Psalm/Internal/Cli/Psalm.php +++ b/src/Psalm/Internal/Cli/Psalm.php @@ -13,7 +13,6 @@ use Psalm\Internal\Codebase\ReferenceMapGenerator; use Psalm\Internal\Composer; use Psalm\Internal\ErrorHandler; -use Psalm\Internal\Fork\Pool; use Psalm\Internal\Fork\PsalmRestarter; use Psalm\Internal\IncludeCollector; use Psalm\Internal\Provider\ClassLikeStorageCacheProvider; @@ -73,15 +72,12 @@ use function strlen; use function strpos; use function substr; -use function version_compare; use const DIRECTORY_SEPARATOR; use const JSON_THROW_ON_ERROR; use const LC_CTYPE; use const PHP_EOL; -use const PHP_OS; use const PHP_URL_SCHEME; -use const PHP_VERSION; use const STDERR; // phpcs:disable PSR1.Files.SideEffects @@ -269,8 +265,6 @@ public static function run(array $argv): void $progress = self::initProgress($options, $config); - self::emitMacPcreWarning($options, $threads); - self::restart($options, $threads, $progress); if (isset($options['debug-emitted-issues'])) { @@ -865,24 +859,6 @@ private static function getCurrentDir(array $options): string return $current_dir; } - private static function emitMacPcreWarning(array $options, int $threads): void - { - if (!isset($options['threads']) - && !isset($options['debug']) - && $threads === 1 - && ini_get('pcre.jit') === '1' - && PHP_OS === 'Darwin' - && version_compare(PHP_VERSION, '7.3.0') >= 0 - && version_compare(PHP_VERSION, '7.4.0') < 0 - ) { - echo( - 'If you want to run Psalm as a language server, or run Psalm with' . PHP_EOL - . 'multiple processes (--threads=4), beware:' . PHP_EOL - . Pool::MAC_PCRE_MESSAGE . PHP_EOL . PHP_EOL - ); - } - } - private static function restart(array $options, int $threads, Progress $progress): void { $ini_handler = new PsalmRestarter('PSALM'); diff --git a/src/Psalm/Internal/Fork/Pool.php b/src/Psalm/Internal/Fork/Pool.php index 09e525dde6d..2c50675e27f 100644 --- a/src/Psalm/Internal/Fork/Pool.php +++ b/src/Psalm/Internal/Fork/Pool.php @@ -48,11 +48,8 @@ use function substr; use function unserialize; use function usleep; -use function version_compare; use const PHP_EOL; -use const PHP_OS; -use const PHP_VERSION; use const SIGALRM; use const SIGTERM; use const STREAM_IPPROTO_IP; @@ -88,12 +85,6 @@ class Pool /** @var ?Closure(mixed): void */ private ?Closure $task_done_closure = null; - public const MAC_PCRE_MESSAGE = 'Mac users: pcre.jit is set to 1 in your PHP config.' . PHP_EOL - . 'The pcre jit is known to cause segfaults in PHP 7.3 on Macs, and Psalm' . PHP_EOL - . 'will not execute in threaded mode to avoid indecipherable errors.' . PHP_EOL - . 'Consider adding pcre.jit=0 to your PHP config, or upgrade to PHP 7.4.' . PHP_EOL - . 'Relevant info: https://bugs.php.net/bug.php?id=77260'; - /** * @param array> $process_task_data_iterator * An array of task data items to be divided up among the @@ -141,16 +132,6 @@ public function __construct( exit(1); } - if (ini_get('pcre.jit') === '1' - && PHP_OS === 'Darwin' - && version_compare(PHP_VERSION, '7.3.0') >= 0 - && version_compare(PHP_VERSION, '7.4.0') < 0 - ) { - die( - self::MAC_PCRE_MESSAGE . PHP_EOL - ); - } - // We'll keep track of if this is the parent process // so that we can tell who will be doing the waiting $is_parent = false; From d6faff2844dbcd40df108052fbdd929bdeafc7a9 Mon Sep 17 00:00:00 2001 From: Ivan Sidorov Date: Thu, 26 Oct 2023 21:19:39 +0000 Subject: [PATCH 013/357] Fix auto comletion by partial property or method --- src/Psalm/Codebase.php | 47 ++++- .../LanguageServer/Server/TextDocument.php | 3 + tests/LanguageServer/CompletionTest.php | 198 +++++++++++++++++- 3 files changed, 246 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index e1c2f017090..bf555d4d07e 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -1742,6 +1742,9 @@ public function getCompletionDataAtPosition(string $file_path, Position $positio $offset = $position->toOffset($file_contents); + $literal_part = $this->getBeginedLiteralPart($file_path, $position); + $begin_literal_offset = $offset - strlen($literal_part); + [$reference_map, $type_map] = $this->analyzer->getMapsForFile($file_path); if (!$reference_map && !$type_map) { @@ -1776,7 +1779,7 @@ public function getCompletionDataAtPosition(string $file_path, Position $positio } } - if ($offset - $end_pos === 2 || $offset - $end_pos === 3) { + if ($begin_literal_offset - $end_pos === 2) { $candidate_gap = substr($file_contents, $end_pos, 2); if ($candidate_gap === '->' || $candidate_gap === '::') { @@ -1801,6 +1804,11 @@ public function getCompletionDataAtPosition(string $file_path, Position $positio return [$possible_reference, '::', $offset]; } + if ($offset <= $end_pos && substr($file_contents, $begin_literal_offset - 2, 2) === '::') { + $class_name = explode('::', $possible_reference)[0]; + return [$class_name, '::', $offset]; + } + // Only continue for references that are partial / don't exist. if ($possible_reference[0] !== '*') { continue; @@ -1816,6 +1824,23 @@ public function getCompletionDataAtPosition(string $file_path, Position $positio return null; } + public function getBeginedLiteralPart(string $file_path, Position $position): string + { + $is_open = $this->file_provider->isOpen($file_path); + + if (!$is_open) { + throw new UnanalyzedFileException($file_path . ' is not open'); + } + + $file_contents = $this->getFileContents($file_path); + + $offset = $position->toOffset($file_contents); + + preg_match('/\$?\w+$/', substr($file_contents, 0, $offset), $matches); + + return $matches[0] ?? ''; + } + public function getTypeContextAtPosition(string $file_path, Position $position): ?Union { $file_contents = $this->getFileContents($file_path); @@ -1957,6 +1982,26 @@ public function getCompletionItemsForClassishThing( return $completion_items; } + /** + * @param list $items + * @return list + */ + public function filterCompletionItemsByBeginLiteralPart(array $items, string $literal_part): array + { + if (!$literal_part) { + return $items; + } + + $res = []; + foreach ($items as $item) { + if ($item->insertText && strpos($item->insertText, $literal_part) === 0) { + $res[] = $item; + } + } + + return $res; + } + /** * @return list */ diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index cb359d5b9fe..c8b0d36e7a0 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -297,6 +297,7 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit try { $completion_data = $this->codebase->getCompletionDataAtPosition($file_path, $position); + $literal_part = $this->codebase->getBeginedLiteralPart($file_path, $position); if ($completion_data) { [$recent_type, $gap, $offset] = $completion_data; @@ -305,6 +306,8 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit ->textDocument->completion->completionItem->snippetSupport ?? false; $completion_items = $this->codebase->getCompletionItemsForClassishThing($recent_type, $gap, $snippetSupport); + $completion_items = + $this->codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); } elseif ($gap === '[') { $completion_items = $this->codebase->getCompletionItemsForArrayKeys($recent_type); } else { diff --git a/tests/LanguageServer/CompletionTest.php b/tests/LanguageServer/CompletionTest.php index 942556f56cd..f5a778dd7d5 100644 --- a/tests/LanguageServer/CompletionTest.php +++ b/tests/LanguageServer/CompletionTest.php @@ -15,6 +15,7 @@ use Psalm\Tests\TestConfig; use Psalm\Type; +use function array_map; use function count; class CompletionTest extends TestCase @@ -370,7 +371,7 @@ public function foo() : void { $codebase->scanFiles(); $this->analyzeFile('somefile.php', new Context()); - $this->assertNull($codebase->getCompletionDataAtPosition('somefile.php', new Position(16, 41))); + $this->assertSame(['B\C', '->', 456], $codebase->getCompletionDataAtPosition('somefile.php', new Position(16, 41))); } public function testCompletionOnTemplatedThisProperty(): void @@ -725,6 +726,201 @@ public function baz() {} $this->assertSame('baz()', $completion_items[1]->insertText); } + public function testObjectPropertyOnAppendToEnd(): void + { + $codebase = $this->codebase; + $config = $codebase->config; + $config->throw_exception = false; + + $this->addFile( + 'somefile.php', + 'aPr + } + }', + ); + + $codebase->file_provider->openFile('somefile.php'); + $codebase->scanFiles(); + + $this->analyzeFile('somefile.php', new Context()); + + $position = new Position(8, 34); + $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position); + $literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position); + + $this->assertSame(['B\A&static', '->', 223], $completion_data); + + $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); + $completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); + $completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items); + + $this->assertSame(['aProp'], $completion_item_texts); + } + + public function testObjectPropertyOnReplaceEndPart(): void + { + $codebase = $this->codebase; + $config = $codebase->config; + $config->throw_exception = false; + + $this->addFile( + 'somefile.php', + 'aProp2; + } + }', + ); + + $codebase->file_provider->openFile('somefile.php'); + $codebase->scanFiles(); + + $this->analyzeFile('somefile.php', new Context()); + + $position = new Position(8, 34); + $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position); + $literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position); + + $this->assertSame(['B\A&static', '->', 225], $completion_data); + + $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); + $completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); + $completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items); + + $this->assertSame(['aProp1', 'aProp2'], $completion_item_texts); + } + + public function testSelfPropertyOnAppendToEnd(): void + { + $codebase = $this->codebase; + $config = $codebase->config; + $config->throw_exception = false; + + $this->addFile( + 'somefile.php', + 'file_provider->openFile('somefile.php'); + $codebase->scanFiles(); + + $this->analyzeFile('somefile.php', new Context()); + + $position = new Position(8, 34); + $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position); + $literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position); + + $this->assertSame(['B\A', '::', 237], $completion_data); + + $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); + $completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); + $completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items); + + $this->assertSame(['$aProp'], $completion_item_texts); + } + + public function testStaticPropertyOnAppendToEnd(): void + { + $codebase = $this->codebase; + $config = $codebase->config; + $config->throw_exception = false; + + $this->addFile( + 'somefile.php', + 'file_provider->openFile('somefile.php'); + $codebase->scanFiles(); + + $this->analyzeFile('somefile.php', new Context()); + + $position = new Position(8, 36); + $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position); + $literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position); + + $this->assertSame(['B\A', '::', 239], $completion_data); + + $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); + $completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); + $completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items); + + $this->assertSame(['$aProp'], $completion_item_texts); + } + + public function testStaticPropertyOnReplaceEndPart(): void + { + $codebase = $this->codebase; + $config = $codebase->config; + $config->throw_exception = false; + + $this->addFile( + 'somefile.php', + 'file_provider->openFile('somefile.php'); + $codebase->scanFiles(); + + $this->analyzeFile('somefile.php', new Context()); + + $position = new Position(8, 34); + $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position); + $literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position); + + $this->assertSame(['B\A', '::', 239], $completion_data); + + $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); + $completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); + $completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items); + + $this->assertSame(['$aProp1', '$aProp2'], $completion_item_texts); + } + public function testCompletionOnNewExceptionWithoutNamespace(): void { $codebase = $this->codebase; From 2f039f9072e5a69d4021055d74c8b47a32b9c575 Mon Sep 17 00:00:00 2001 From: Ivan Sidorov Date: Fri, 27 Oct 2023 09:07:33 +0000 Subject: [PATCH 014/357] Fix auto-completion with declared static method by DocBlock --- src/Psalm/Codebase.php | 10 +++++--- tests/LanguageServer/CompletionTest.php | 32 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index bf555d4d07e..7ad8572dc44 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -69,6 +69,7 @@ use UnexpectedValueException; use function array_combine; +use function array_merge; use function array_pop; use function array_reverse; use function array_values; @@ -1883,9 +1884,12 @@ public function getCompletionItemsForClassishThing( try { $class_storage = $this->classlike_storage_provider->get($atomic_type->value); - foreach ($class_storage->appearing_method_ids as $declaring_method_id) { - $method_storage = $this->methods->getStorage($declaring_method_id); - + $methods = array_merge( + $class_storage->methods, + $class_storage->pseudo_methods, + $class_storage->pseudo_static_methods, + ); + foreach ($methods as $method_storage) { if ($method_storage->is_static || $gap === '->') { $completion_item = new CompletionItem( $method_storage->cased_name, diff --git a/tests/LanguageServer/CompletionTest.php b/tests/LanguageServer/CompletionTest.php index f5a778dd7d5..268f399821e 100644 --- a/tests/LanguageServer/CompletionTest.php +++ b/tests/LanguageServer/CompletionTest.php @@ -1456,6 +1456,38 @@ static function add() : void { $this->assertCount(2, $completion_items); } + public function testCompletionStaticMethodOnDocBlock(): void + { + $codebase = $this->codebase; + $config = $codebase->config; + $config->throw_exception = false; + + $this->addFile( + 'somefile.php', + 'file_provider->openFile('somefile.php'); + $codebase->scanFiles(); + $this->analyzeFile('somefile.php', new Context()); + + $position = new Position(7, 23); + $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position); + + $this->assertSame(['Bar\Alpha', '::', 177], $completion_data); + + $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); + $this->assertCount(1, $completion_items); + $this->assertSame('foo()', $completion_items[0]->insertText); + } + public function testCompletionOnClassInstanceReferenceWithAssignmentAfter(): void { From d07b57576d954a9dbb39f8dc511d4994444721df Mon Sep 17 00:00:00 2001 From: Brian Dunne Date: Mon, 30 Oct 2023 21:46:40 -0500 Subject: [PATCH 015/357] Stub constants for ZipArchive from ext-zip This stubs out the class constants for ZipArchive, which I believe are the only constants introduced by the `zip` extension. This should allow Psalm to run over code utilizing any of these constants even if the analyzing system doesn't have ext-zip installed/enabled (e.g. a GitHub Actions container). --- stubs/extensions/zip.phpstub | 109 +++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 stubs/extensions/zip.phpstub diff --git a/stubs/extensions/zip.phpstub b/stubs/extensions/zip.phpstub new file mode 100644 index 00000000000..8c217d314c4 --- /dev/null +++ b/stubs/extensions/zip.phpstub @@ -0,0 +1,109 @@ + Date: Mon, 30 Oct 2023 22:36:06 -0500 Subject: [PATCH 016/357] Add constants from SOAP extension to stub The SOAP extension stub was missing some constants we used (really just SOAP_1_1 and SOAP_1_2), so I thought I'd add the rest of the constants declared by the extension to the stub. Values are all pulled straight from the PHP docs. --- stubs/extensions/soap.phpstub | 92 +++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/stubs/extensions/soap.phpstub b/stubs/extensions/soap.phpstub index 8a3fafa4dcd..11c289ecb43 100644 --- a/stubs/extensions/soap.phpstub +++ b/stubs/extensions/soap.phpstub @@ -1,5 +1,97 @@ Date: Tue, 31 Oct 2023 10:29:33 -0400 Subject: [PATCH 017/357] allow symfony 7 --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 5d1884fb6df..7e41dd27eeb 100644 --- a/composer.json +++ b/composer.json @@ -36,8 +36,8 @@ "nikic/php-parser": "^4.16", "sebastian/diff": "^4.0 || ^5.0", "spatie/array-to-xml": "^2.17.0 || ^3.0", - "symfony/console": "^4.1.6 || ^5.0 || ^6.0", - "symfony/filesystem": "^5.4 || ^6.0" + "symfony/console": "^4.1.6 || ^5.0 || ^6.0 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.0 || ^7.0" }, "conflict": { "nikic/php-parser": "4.17.0" @@ -59,7 +59,7 @@ "psalm/plugin-phpunit": "^0.18", "slevomat/coding-standard": "^8.4", "squizlabs/php_codesniffer": "^3.6", - "symfony/process": "^4.4 || ^5.0 || ^6.0" + "symfony/process": "^4.4 || ^5.0 || ^6.0 || ^7.0" }, "suggest": { "ext-igbinary": "^2.0.5 is required, used to serialize caching data", From cfe57e9dbe958e853560fc0bf16949876ada80cf Mon Sep 17 00:00:00 2001 From: Brian Dunne Date: Wed, 1 Nov 2023 15:01:48 -0500 Subject: [PATCH 018/357] Fix redundant PHP tag in SOAP stub --- stubs/extensions/soap.phpstub | 2 -- 1 file changed, 2 deletions(-) diff --git a/stubs/extensions/soap.phpstub b/stubs/extensions/soap.phpstub index 11c289ecb43..dac3ece837c 100644 --- a/stubs/extensions/soap.phpstub +++ b/stubs/extensions/soap.phpstub @@ -1,7 +1,5 @@ Date: Thu, 2 Nov 2023 11:59:42 +0000 Subject: [PATCH 019/357] Allow enum cases to be global constants --- .../Expression/SimpleTypeInferer.php | 16 +++-- tests/EnumTest.php | 66 +++++++++++++++++++ 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php index 101196a2ff0..baead15e9da 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php @@ -12,6 +12,7 @@ use Psalm\FileSource; use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\BinaryOp\ArithmeticOpAnalyzer; +use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ConstFetchAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Provider\NodeDataProvider; use Psalm\Internal\Type\TypeCombiner; @@ -261,23 +262,28 @@ public static function infer( } if ($stmt instanceof PhpParser\Node\Expr\ConstFetch) { - $name = strtolower($stmt->name->getFirst()); - if ($name === 'false') { + $name = $stmt->name->getFirst(); + $name_lowercase = strtolower($name); + if ($name_lowercase === 'false') { return Type::getFalse(); } - if ($name === 'true') { + if ($name_lowercase === 'true') { return Type::getTrue(); } - if ($name === 'null') { + if ($name_lowercase === 'null') { return Type::getNull(); } - if ($stmt->name->getFirst() === '__NAMESPACE__') { + if ($name === '__NAMESPACE__') { return Type::getString($aliases->namespace); } + if ($type = ConstFetchAnalyzer::getGlobalConstType($codebase, $name, $name)) { + return $type; + } + return null; } diff --git a/tests/EnumTest.php b/tests/EnumTest.php index ebc67a05e1e..6991b89ae37 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -657,6 +657,28 @@ enum BarEnum: int { 'ignored_issues' => [], 'php_version' => '8.1', ], + 'stringBackedEnumCaseValueFromStringGlobalConstant' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'intBackedEnumCaseValueFromIntGlobalConstant' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } @@ -1107,6 +1129,50 @@ enum Bar: int 'ignored_issues' => [], 'php_version' => '8.1', ], + 'invalidStringBackedEnumCaseValueFromStringGlobalConstant' => [ + 'code' => ' 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'invalidIntBackedEnumCaseValueFromIntGlobalConstant' => [ + 'code' => ' 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'invalidStringBackedEnumCaseValueFromIntGlobalConstant' => [ + 'code' => ' 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'invalidIntBackedEnumCaseValueFromStringGlobalConstant' => [ + 'code' => ' 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } } From c93fe1471d329b09caba7947a701398be0ac9b1d Mon Sep 17 00:00:00 2001 From: robchett Date: Fri, 3 Nov 2023 07:52:42 +0000 Subject: [PATCH 020/357] Hotfix shepard build - see #10342 --- src/Psalm/Internal/PluginManager/ComposerLock.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/PluginManager/ComposerLock.php b/src/Psalm/Internal/PluginManager/ComposerLock.php index 03c9733d95c..428b914aab8 100644 --- a/src/Psalm/Internal/PluginManager/ComposerLock.php +++ b/src/Psalm/Internal/PluginManager/ComposerLock.php @@ -18,7 +18,7 @@ /** * @internal */ -final class ComposerLock +class ComposerLock { /** @var string[] */ private array $file_names; From f2343ed2e1016810a12b0f71a373bb121c8c6fd7 Mon Sep 17 00:00:00 2001 From: robchett Date: Fri, 3 Nov 2023 10:15:48 +0000 Subject: [PATCH 021/357] Add progress for scanning stage --- src/Psalm/Internal/Codebase/Scanner.php | 6 ++++- src/Psalm/Progress/DefaultProgress.php | 2 +- src/Psalm/Progress/LongProgress.php | 30 +++++++++++++++++++++++-- src/Psalm/Progress/Progress.php | 4 ++++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/Codebase/Scanner.php b/src/Psalm/Internal/Codebase/Scanner.php index aaa5e5c6aea..262f0ad4ed8 100644 --- a/src/Psalm/Internal/Codebase/Scanner.php +++ b/src/Psalm/Internal/Codebase/Scanner.php @@ -317,6 +317,7 @@ private function scanFilePaths(int $pool_size): bool $pool_size = 1; } + $this->progress->expand(count($files_to_scan)); if ($pool_size > 1) { $process_file_paths = []; @@ -355,7 +356,6 @@ function (): void { */ function () { $this->progress->debug('Collecting data from forked scanner process' . PHP_EOL); - $project_analyzer = ProjectAnalyzer::getInstance(); $codebase = $project_analyzer->getCodebase(); $statements_provider = $codebase->statements_provider; @@ -377,6 +377,9 @@ function () { 'taint_data' => $codebase->taint_flow_graph, ]; }, + function (): void { + $this->progress->taskDone(0); + }, ); // Wait for all tasks to complete and collect the results. @@ -427,6 +430,7 @@ function () { $i = 0; foreach ($files_to_scan as $file_path => $_) { + $this->progress->taskDone(0); $this->scanAPath($i, $file_path); ++$i; } diff --git a/src/Psalm/Progress/DefaultProgress.php b/src/Psalm/Progress/DefaultProgress.php index 57024cbf72f..64788f224cd 100644 --- a/src/Psalm/Progress/DefaultProgress.php +++ b/src/Psalm/Progress/DefaultProgress.php @@ -22,7 +22,7 @@ class DefaultProgress extends LongProgress public function taskDone(int $level): void { - if ($this->number_of_tasks > self::TOO_MANY_FILES) { + if ($this->fixed_size && $this->number_of_tasks > self::TOO_MANY_FILES) { ++$this->progress; // Source for rate limiting: diff --git a/src/Psalm/Progress/LongProgress.php b/src/Psalm/Progress/LongProgress.php index 8cf23fa1fc6..5a58886c4b0 100644 --- a/src/Psalm/Progress/LongProgress.php +++ b/src/Psalm/Progress/LongProgress.php @@ -25,6 +25,8 @@ class LongProgress extends Progress protected bool $print_infos = false; + protected bool $fixed_size = true; + public function __construct(bool $print_errors = true, bool $print_infos = true) { $this->print_errors = $print_errors; @@ -33,16 +35,19 @@ public function __construct(bool $print_errors = true, bool $print_infos = true) public function startScanningFiles(): void { + $this->fixed_size = false; $this->write('Scanning files...' . "\n"); } public function startAnalyzingFiles(): void { - $this->write('Analyzing files...' . "\n\n"); + $this->fixed_size = true; + $this->write("\n" . 'Analyzing files...' . "\n\n"); } public function startAlteringFiles(): void { + $this->fixed_size = true; $this->write('Altering files...' . "\n"); } @@ -57,8 +62,30 @@ public function start(int $number_of_tasks): void $this->progress = 0; } + public function expand(int $number_of_tasks): void + { + $this->number_of_tasks += $number_of_tasks; + } + public function taskDone(int $level): void { + if ($this->number_of_tasks === null) { + throw new LogicException('Progress::start() should be called before Progress::taskDone()'); + } + + ++$this->progress; + + if (!$this->fixed_size) { + if ($this->progress == 1 || $this->progress == $this->number_of_tasks || $this->progress % 10 == 0) { + $this->write(sprintf( + "\r%s / %s?", + $this->progress, + $this->number_of_tasks, + )); + } + return; + } + if ($level === 0 || ($level === 1 && !$this->print_infos) || !$this->print_errors) { $this->write(self::doesTerminalSupportUtf8() ? '░' : '_'); } elseif ($level === 1) { @@ -67,7 +94,6 @@ public function taskDone(int $level): void $this->write('E'); } - ++$this->progress; if (($this->progress % self::NUMBER_OF_COLUMNS) !== 0) { return; diff --git a/src/Psalm/Progress/Progress.php b/src/Psalm/Progress/Progress.php index f6313214775..248878ff0a1 100644 --- a/src/Psalm/Progress/Progress.php +++ b/src/Psalm/Progress/Progress.php @@ -46,6 +46,10 @@ public function start(int $number_of_tasks): void { } + public function expand(int $number_of_tasks): void + { + } + public function taskDone(int $level): void { } From 54999abc54f717ff78967ea0aff66a66f861bf5b Mon Sep 17 00:00:00 2001 From: robchett Date: Fri, 3 Nov 2023 08:45:19 +0000 Subject: [PATCH 022/357] Allow (no-)seal-(properties|methods) without the psalm- prefix --- docs/annotating_code/supported_annotations.md | 8 ++--- .../Reflector/ClassLikeDocblockParser.php | 24 +++++++------- tests/MagicMethodAnnotationTest.php | 15 +++++++++ tests/MagicPropertyTest.php | 32 +++++++++++++++++++ 4 files changed, 64 insertions(+), 15 deletions(-) diff --git a/docs/annotating_code/supported_annotations.md b/docs/annotating_code/supported_annotations.md index 346b525f878..ba14e7d67c9 100644 --- a/docs/annotating_code/supported_annotations.md +++ b/docs/annotating_code/supported_annotations.md @@ -202,7 +202,7 @@ takesFoo(getFoo()); This provides the same, but for `false`. Psalm uses this internally for functions like `preg_replace`, which can return false if the given input has encoding errors, but where 99.9% of the time the function operates as expected. -### `@psalm-seal-properties`, `@psalm-no-seal-properties` +### `@psalm-seal-properties`, `@psalm-no-seal-properties`, `@seal-properties`, `@no-seal-properties` If you have a magic property getter/setter, you can use `@psalm-seal-properties` to instruct Psalm to disallow getting and setting any properties not contained in a list of `@property` (or `@property-read`/`@property-write`) annotations. This is automatically enabled with the configuration option `sealAllProperties` and can be disabled for a class with `@psalm-no-seal-properties` @@ -211,7 +211,7 @@ This is automatically enabled with the configuration option `sealAllProperties` bar = 5; // this call fails ``` -### `@psalm-seal-methods`, `@psalm-no-seal-methods` +### `@psalm-seal-methods`, `@psalm-no-seal-methods`, `@seal-methods`, `@no-seal-methods` If you have a magic method caller, you can use `@psalm-seal-methods` to instruct Psalm to disallow calling any methods not contained in a list of `@method` annotations. This is automatically enabled with the configuration option `sealAllMethods` and can be disabled for a class with `@psalm-no-seal-methods` @@ -236,7 +236,7 @@ This is automatically enabled with the configuration option `sealAllMethods` and tags['psalm-seal-properties'])) { - $info->sealed_properties = true; - } - if (isset($parsed_docblock->tags['psalm-no-seal-properties'])) { - $info->sealed_properties = false; - } + foreach (['', 'psalm-'] as $prefix) { + if (isset($parsed_docblock->tags[$prefix . 'seal-properties'])) { + $info->sealed_properties = true; + } + if (isset($parsed_docblock->tags[$prefix . 'no-seal-properties'])) { + $info->sealed_properties = false; + } - if (isset($parsed_docblock->tags['psalm-seal-methods'])) { - $info->sealed_methods = true; - } - if (isset($parsed_docblock->tags['psalm-no-seal-methods'])) { - $info->sealed_methods = false; + if (isset($parsed_docblock->tags[$prefix . 'seal-methods'])) { + $info->sealed_methods = true; + } + if (isset($parsed_docblock->tags[$prefix . 'no-seal-methods'])) { + $info->sealed_methods = false; + } } if (isset($parsed_docblock->tags['psalm-inheritors'])) { diff --git a/tests/MagicMethodAnnotationTest.php b/tests/MagicMethodAnnotationTest.php index ad157dad39b..88f5a2a478a 100644 --- a/tests/MagicMethodAnnotationTest.php +++ b/tests/MagicMethodAnnotationTest.php @@ -1118,6 +1118,21 @@ class B extends A {} $b->foo();', 'error_message' => 'UndefinedMagicMethod', ], + 'inheritSealedMethodsWithoutPrefix' => [ + 'code' => 'foo();', + 'error_message' => 'UndefinedMagicMethod', + ], 'lonelyMethod' => [ 'code' => ' 'InvalidDocblock', ], + 'sealedWithNoProperties' => [ + 'code' => 'errors;', + 'error_message' => 'UndefinedMagicPropertyFetch', + ], + 'sealedWithNoPropertiesNoPrefix' => [ + 'code' => 'errors;', + 'error_message' => 'UndefinedMagicPropertyFetch', + ], ]; } From 3448c47931ffc46033ca72c9955c5a26b747d139 Mon Sep 17 00:00:00 2001 From: robchett Date: Thu, 2 Nov 2023 18:15:21 +0000 Subject: [PATCH 023/357] Warn when an issue handler suppression is unused --- config.xsd | 2 + docs/running_psalm/configuration.md | 5 +++ docs/running_psalm/issues.md | 1 + .../issues/UnusedIssueHandlerSuppression.md | 17 +++++++++ src/Psalm/Config.php | 36 ++++++++++++++++++ src/Psalm/Config/ErrorLevelFileFilter.php | 2 + src/Psalm/Config/IssueHandler.php | 15 +++++++- src/Psalm/Internal/Codebase/Analyzer.php | 6 +++ .../Issue/UnusedIssueHandlerSuppression.php | 11 ++++++ src/Psalm/IssueBuffer.php | 38 +++++++++++++++++++ tests/DocumentationTest.php | 2 + 11 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 docs/running_psalm/issues/UnusedIssueHandlerSuppression.md create mode 100644 src/Psalm/Issue/UnusedIssueHandlerSuppression.php diff --git a/config.xsd b/config.xsd index 72745ea604f..88d7b527fe4 100644 --- a/config.xsd +++ b/config.xsd @@ -47,6 +47,7 @@ + @@ -494,6 +495,7 @@ + diff --git a/docs/running_psalm/configuration.md b/docs/running_psalm/configuration.md index 05f65236f0c..ac222f95148 100644 --- a/docs/running_psalm/configuration.md +++ b/docs/running_psalm/configuration.md @@ -513,6 +513,11 @@ class PremiumCar extends StandardCar { Emits [UnusedBaselineEntry](issues/UnusedBaselineEntry.md) when a baseline entry is not being used to suppress an issue. +#### findUnusedIssueHandlerSuppression + +Emits [UnusedIssueHandlerSuppression](issues/UnusedIssueHandlerSuppression.md) when a suppressed issue handler +is not being used to suppress an issue. + ## Project settings #### <projectFiles> diff --git a/docs/running_psalm/issues.md b/docs/running_psalm/issues.md index f2655635cf1..b9e3d8fe25f 100644 --- a/docs/running_psalm/issues.md +++ b/docs/running_psalm/issues.md @@ -298,6 +298,7 @@ - [UnusedDocblockParam](issues/UnusedDocblockParam.md) - [UnusedForeachValue](issues/UnusedForeachValue.md) - [UnusedFunctionCall](issues/UnusedFunctionCall.md) + - [UnusedIssueHandlerSuppression](issues/UnusedIssueHandlerSuppression.md) - [UnusedMethod](issues/UnusedMethod.md) - [UnusedMethodCall](issues/UnusedMethodCall.md) - [UnusedParam](issues/UnusedParam.md) diff --git a/docs/running_psalm/issues/UnusedIssueHandlerSuppression.md b/docs/running_psalm/issues/UnusedIssueHandlerSuppression.md new file mode 100644 index 00000000000..dc796e35265 --- /dev/null +++ b/docs/running_psalm/issues/UnusedIssueHandlerSuppression.md @@ -0,0 +1,17 @@ +# UnusedIssueHandlerSuppression + +Emitted when an issue type suppression in the configuration file is not being used to suppress an issue. + +Enabled by [findUnusedIssueHandlerSuppression](../configuration.md#findunusedissuehandlersuppression) + +```php + + + + +``` diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 8759c0ddbcd..a91336d2cc5 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -230,6 +230,8 @@ final class Config */ public string $base_dir; + public ?string $source_filename = null; + /** * The PHP version to assume as declared in the config file */ @@ -369,6 +371,8 @@ final class Config public bool $find_unused_baseline_entry = true; + public bool $find_unused_issue_handler_suppression = true; + public bool $run_taint_analysis = false; public bool $use_phpstorm_meta_path = true; @@ -935,6 +939,7 @@ private static function fromXmlAndPaths( 'allowNamedArgumentCalls' => 'allow_named_arg_calls', 'findUnusedPsalmSuppress' => 'find_unused_psalm_suppress', 'findUnusedBaselineEntry' => 'find_unused_baseline_entry', + 'findUnusedIssueHandlerSuppression' => 'find_unused_issue_handler_suppression', 'reportInfo' => 'report_info', 'restrictReturnTypes' => 'restrict_return_types', 'limitMethodComplexity' => 'limit_method_complexity', @@ -950,6 +955,7 @@ private static function fromXmlAndPaths( } } + $config->source_filename = $config_path; if ($config->resolve_from_config_file) { $config->base_dir = $base_dir; } else { @@ -1311,6 +1317,12 @@ public function setComposerClassLoader(?ClassLoader $loader = null): void $this->composer_class_loader = $loader; } + /** @return array */ + public function getIssueHandlers(): array + { + return $this->issue_handlers; + } + public function setAdvancedErrorLevel(string $issue_key, array $config, ?string $default_error_level = null): void { $this->issue_handlers[$issue_key] = new IssueHandler(); @@ -1858,6 +1870,30 @@ public static function getParentIssueType(string $issue_type): ?string return null; } + /** @return array{type: string, index: int, count: int}[] */ + public function getIssueHandlerSuppressions(): array + { + $suppressions = []; + foreach ($this->issue_handlers as $key => $handler) { + foreach ($handler->getFilters() as $index => $filter) { + $suppressions[] = [ + 'type' => $key, + 'index' => $index, + 'count' => $filter->suppressions, + ]; + } + } + return $suppressions; + } + + /** @param array{type: string, index: int, count: int}[] $filters */ + public function combineIssueHandlerSuppressions(array $filters): void + { + foreach ($filters as $filter) { + $this->issue_handlers[$filter['type']]->getFilters()[$filter['index']]->suppressions += $filter['count']; + } + } + public function getReportingLevelForFile(string $issue_type, string $file_path): string { if (isset($this->issue_handlers[$issue_type])) { diff --git a/src/Psalm/Config/ErrorLevelFileFilter.php b/src/Psalm/Config/ErrorLevelFileFilter.php index 1778ccfae35..de3ed732c19 100644 --- a/src/Psalm/Config/ErrorLevelFileFilter.php +++ b/src/Psalm/Config/ErrorLevelFileFilter.php @@ -15,6 +15,8 @@ final class ErrorLevelFileFilter extends FileFilter { private string $error_level = ''; + public int $suppressions = 0; + public static function loadFromArray( array $config, string $base_dir, diff --git a/src/Psalm/Config/IssueHandler.php b/src/Psalm/Config/IssueHandler.php index a5af5aefe4b..aba87f0232b 100644 --- a/src/Psalm/Config/IssueHandler.php +++ b/src/Psalm/Config/IssueHandler.php @@ -25,7 +25,7 @@ final class IssueHandler private string $error_level = Config::REPORT_ERROR; /** - * @var array + * @var list */ private array $custom_levels = []; @@ -50,6 +50,12 @@ public static function loadFromXMLElement(SimpleXMLElement $e, string $base_dir) return $handler; } + /** @return list */ + public function getFilters(): array + { + return $this->custom_levels; + } + public function setCustomLevels(array $customLevels, string $base_dir): void { /** @var array $customLevel */ @@ -71,6 +77,7 @@ public function getReportingLevelForFile(string $file_path): string { foreach ($this->custom_levels as $custom_level) { if ($custom_level->allows($file_path)) { + $custom_level->suppressions++; return $custom_level->getErrorLevel(); } } @@ -82,6 +89,7 @@ public function getReportingLevelForClass(string $fq_classlike_name): ?string { foreach ($this->custom_levels as $custom_level) { if ($custom_level->allowsClass($fq_classlike_name)) { + $custom_level->suppressions++; return $custom_level->getErrorLevel(); } } @@ -93,6 +101,7 @@ public function getReportingLevelForMethod(string $method_id): ?string { foreach ($this->custom_levels as $custom_level) { if ($custom_level->allowsMethod(strtolower($method_id))) { + $custom_level->suppressions++; return $custom_level->getErrorLevel(); } } @@ -115,6 +124,7 @@ public function getReportingLevelForArgument(string $function_id): ?string { foreach ($this->custom_levels as $custom_level) { if ($custom_level->allowsMethod(strtolower($function_id))) { + $custom_level->suppressions++; return $custom_level->getErrorLevel(); } } @@ -126,6 +136,7 @@ public function getReportingLevelForProperty(string $property_id): ?string { foreach ($this->custom_levels as $custom_level) { if ($custom_level->allowsProperty($property_id)) { + $custom_level->suppressions++; return $custom_level->getErrorLevel(); } } @@ -137,6 +148,7 @@ public function getReportingLevelForClassConstant(string $constant_id): ?string { foreach ($this->custom_levels as $custom_level) { if ($custom_level->allowsClassConstant($constant_id)) { + $custom_level->suppressions++; return $custom_level->getErrorLevel(); } } @@ -148,6 +160,7 @@ public function getReportingLevelForVariable(string $var_name): ?string { foreach ($this->custom_levels as $custom_level) { if ($custom_level->allowsVariable($var_name)) { + $custom_level->suppressions++; return $custom_level->getErrorLevel(); } } diff --git a/src/Psalm/Internal/Codebase/Analyzer.php b/src/Psalm/Internal/Codebase/Analyzer.php index e7eb49e1c15..461aae3e153 100644 --- a/src/Psalm/Internal/Codebase/Analyzer.php +++ b/src/Psalm/Internal/Codebase/Analyzer.php @@ -89,6 +89,7 @@ * used_suppressions: array>, * function_docblock_manipulators: array>, * mutable_classes: array, + * issue_handlers: array{type: string, index: int, count: int}[], * } */ @@ -418,6 +419,10 @@ static function (): void { IssueBuffer::addUsedSuppressions($pool_data['used_suppressions']); } + if ($codebase->config->find_unused_issue_handler_suppression) { + $codebase->config->combineIssueHandlerSuppressions($pool_data['issue_handlers']); + } + if ($codebase->taint_flow_graph && $pool_data['taint_data']) { $codebase->taint_flow_graph->addGraph($pool_data['taint_data']); } @@ -1639,6 +1644,7 @@ private function getWorkerData(): array 'used_suppressions' => $codebase->track_unused_suppressions ? IssueBuffer::getUsedSuppressions() : [], 'function_docblock_manipulators' => FunctionDocblockManipulator::getManipulators(), 'mutable_classes' => $codebase->analyzer->mutable_classes, + 'issue_handlers' => $this->config->getIssueHandlerSuppressions() ]; // @codingStandardsIgnoreEnd } diff --git a/src/Psalm/Issue/UnusedIssueHandlerSuppression.php b/src/Psalm/Issue/UnusedIssueHandlerSuppression.php new file mode 100644 index 00000000000..43699843d26 --- /dev/null +++ b/src/Psalm/Issue/UnusedIssueHandlerSuppression.php @@ -0,0 +1,11 @@ +config->find_unused_issue_handler_suppression) { + foreach ($codebase->config->getIssueHandlers() as $type => $handler) { + foreach ($handler->getFilters() as $filter) { + if ($filter->suppressions > 0 && $filter->getErrorLevel() == Config::REPORT_SUPPRESS) { + continue; + } + $issues_data['config'][] = new IssueData( + IssueData::SEVERITY_ERROR, + 0, + 0, + UnusedIssueHandlerSuppression::getIssueType(), + sprintf( + 'Suppressed issue type "%s" for %s was not thrown.', + $type, + str_replace( + $codebase->config->base_dir, + '', + implode(', ', [...$filter->getFiles(), ...$filter->getDirectories()]), + ), + ), + $codebase->config->source_filename ?? '', + '', + '', + '', + 0, + 0, + 0, + 0, + 0, + 0, + UnusedIssueHandlerSuppression::SHORTCODE, + UnusedIssueHandlerSuppression::ERROR_LEVEL, + ); + } + } + } + echo self::getOutput( $issues_data, $project_analyzer->stdout_report_options, diff --git a/tests/DocumentationTest.php b/tests/DocumentationTest.php index d86293a8241..a8d135e4a88 100644 --- a/tests/DocumentationTest.php +++ b/tests/DocumentationTest.php @@ -18,6 +18,7 @@ use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; use Psalm\Issue\UnusedBaselineEntry; +use Psalm\Issue\UnusedIssueHandlerSuppression; use Psalm\Tests\Internal\Provider\FakeParserCacheProvider; use UnexpectedValueException; @@ -270,6 +271,7 @@ public function providerInvalidCodeParse(): array case 'TraitMethodSignatureMismatch': case 'UncaughtThrowInGlobalScope': case UnusedBaselineEntry::getIssueType(): + case UnusedIssueHandlerSuppression::getIssueType(): continue 2; /** @todo reinstate this test when the issue is restored */ From ccabf2144f3f30a7263c28773739477a41d29b81 Mon Sep 17 00:00:00 2001 From: robchett Date: Thu, 2 Nov 2023 18:16:25 +0000 Subject: [PATCH 024/357] Remove unused suppressions --- psalm.xml.dist | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/psalm.xml.dist b/psalm.xml.dist index 1452823757e..5e8e8ac33d0 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -13,6 +13,7 @@ errorBaseline="psalm-baseline.xml" findUnusedPsalmSuppress="true" findUnusedBaselineEntry="true" + findUnusedIssueHandlerSuppression="true" > @@ -63,24 +64,6 @@ - - - - - - - - - - - - - - - - - - @@ -104,12 +87,6 @@ - - - - - - From 16c06b9dd4aaf736b4e30fea9c50a38992428eae Mon Sep 17 00:00:00 2001 From: robchett Date: Thu, 2 Nov 2023 12:53:54 +0000 Subject: [PATCH 025/357] Fix stub for RecursiveArrayIterator::getChildren --- stubs/CoreGenericIterators.phpstub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stubs/CoreGenericIterators.phpstub b/stubs/CoreGenericIterators.phpstub index 43a7bb1f85c..1a7daaf53cf 100644 --- a/stubs/CoreGenericIterators.phpstub +++ b/stubs/CoreGenericIterators.phpstub @@ -774,7 +774,7 @@ class RecursiveArrayIterator extends ArrayIterator implements RecursiveIterator const CHILD_ARRAYS_ONLY = 4 ; /** - * @return RecursiveArrayIterator + * @return ?RecursiveArrayIterator */ public function getChildren() {} From d05bd5430d34e6490892088e004803b558596168 Mon Sep 17 00:00:00 2001 From: robchett Date: Thu, 26 Oct 2023 12:29:12 +0100 Subject: [PATCH 026/357] Use CommentAnalyzer::sanitizeDocblockType consistently --- .../Internal/Analyzer/CommentAnalyzer.php | 3 ++- .../Reflector/ClassLikeDocblockParser.php | 22 +++++++--------- .../Reflector/ClassLikeNodeScanner.php | 8 ++---- .../Reflector/FunctionLikeDocblockParser.php | 20 +++++--------- tests/Template/TraitTemplateTest.php | 26 +++++++++++++++++++ 5 files changed, 45 insertions(+), 34 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php index efbcee99bad..ae03ba45311 100644 --- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php @@ -262,8 +262,9 @@ public static function sanitizeDocblockType(string $docblock_type): string { $docblock_type = (string) preg_replace('@^[ \t]*\*@m', '', $docblock_type); $docblock_type = (string) preg_replace('/,\n\s+}/', '}', $docblock_type); + $docblock_type = (string) preg_replace('/[ \t]+/', ' ', $docblock_type); - return str_replace("\n", '', $docblock_type); + return trim(str_replace("\n", '', $docblock_type)); } /** diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php index 6e9f8715852..638bb973531 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php @@ -68,9 +68,9 @@ public static function parse( $templates = []; if (isset($parsed_docblock->combined_tags['template'])) { foreach ($parsed_docblock->combined_tags['template'] as $offset => $template_line) { - $template_type = preg_split('/[\s]+/', (string) preg_replace('@^[ \t]*\*@m', '', $template_line)); + $template_type = preg_split('/[\s]+/', CommentAnalyzer::sanitizeDocblockType($template_line)); if ($template_type === false) { - throw new IncorrectDocblockException('Invalid @ŧemplate tag: '.preg_last_error_msg()); + throw new IncorrectDocblockException('Invalid @template tag: '.preg_last_error_msg()); } $template_name = array_shift($template_type); @@ -111,7 +111,7 @@ public static function parse( if (isset($parsed_docblock->combined_tags['template-covariant'])) { foreach ($parsed_docblock->combined_tags['template-covariant'] as $offset => $template_line) { - $template_type = preg_split('/[\s]+/', (string) preg_replace('@^[ \t]*\*@m', '', $template_line)); + $template_type = preg_split('/[\s]+/', CommentAnalyzer::sanitizeDocblockType($template_line)); if ($template_type === false) { throw new IncorrectDocblockException('Invalid @template-covariant tag: '.preg_last_error_msg()); } @@ -171,20 +171,16 @@ public static function parse( if (isset($parsed_docblock->tags['psalm-require-extends']) && count($extension_requirements = $parsed_docblock->tags['psalm-require-extends']) > 0) { - $info->extension_requirement = trim((string) preg_replace( - '@^[ \t]*\*@m', - '', + $info->extension_requirement = CommentAnalyzer::sanitizeDocblockType( $extension_requirements[array_key_first($extension_requirements)], - )); + ); } if (isset($parsed_docblock->tags['psalm-require-implements'])) { foreach ($parsed_docblock->tags['psalm-require-implements'] as $implementation_requirement) { - $info->implementation_requirements[] = trim((string) preg_replace( - '@^[ \t]*\*@m', - '', + $info->implementation_requirements[] = CommentAnalyzer::sanitizeDocblockType( $implementation_requirement, - )); + ); } } @@ -199,7 +195,7 @@ public static function parse( if (isset($parsed_docblock->tags['psalm-yield'])) { $yield = (string) reset($parsed_docblock->tags['psalm-yield']); - $info->yield = trim((string) preg_replace('@^[ \t]*\*@m', '', $yield)); + $info->yield = CommentAnalyzer::sanitizeDocblockType($yield); } if (isset($parsed_docblock->tags['deprecated'])) { @@ -552,7 +548,7 @@ protected static function addMagicPropertyToInfo( $end = $offset + strlen($line_parts[0]); - $line_parts[0] = str_replace("\n", '', (string) preg_replace('@^[ \t]*\*@m', '', $line_parts[0])); + $line_parts[0] = CommentAnalyzer::sanitizeDocblockType($line_parts[0]); if ($line_parts[0] === '' || ($line_parts[0][0] === '$' diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 49958adcd0b..ecc0b53fb39 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -80,7 +80,6 @@ use function get_class; use function implode; use function preg_match; -use function preg_replace; use function preg_split; use function str_replace; use function strtolower; @@ -940,7 +939,7 @@ public function handleTraitUse(PhpParser\Node\Stmt\TraitUse $node): void $this->useTemplatedType( $storage, $node, - trim((string) preg_replace('@^[ \t]*\*@m', '', $template_line)), + CommentAnalyzer::sanitizeDocblockType($template_line), ); } } @@ -1912,10 +1911,7 @@ private static function getTypeAliasesFromCommentLines( continue; } - $var_line = (string) preg_replace('/[ \t]+/', ' ', (string) preg_replace('@^[ \t]*\*@m', '', $var_line)); - $var_line = (string) preg_replace('/,\n\s+\}/', '}', $var_line); - $var_line = str_replace("\n", '', $var_line); - + $var_line = CommentAnalyzer::sanitizeDocblockType($var_line); $var_line_parts = preg_split('/( |=)/', $var_line, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); if (!$var_line_parts) { diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php index 26ce42a0ac0..940a70e9b85 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php @@ -151,11 +151,7 @@ public static function parse( $line_parts[1] = substr($line_parts[1], 1); } - $line_parts[0] = str_replace( - "\n", - '', - (string) preg_replace('@^[ \t]*\*@m', '', $line_parts[0]), - ); + $line_parts[0] = CommentAnalyzer::sanitizeDocblockType($line_parts[0]); if ($line_parts[0] === '' || ($line_parts[0][0] === '$' @@ -194,14 +190,10 @@ public static function parse( $line_parts = CommentAnalyzer::splitDocLine($param); if (count($line_parts) > 0) { - $line_parts[0] = str_replace( - "\n", - '', - (string) preg_replace('@^[ \t]*\*@m', '', $line_parts[0]), - ); + $line_parts[0] = CommentAnalyzer::sanitizeDocblockType($line_parts[0]); $info->self_out = [ - 'type' => str_replace("\n", '', $line_parts[0]), + 'type' => $line_parts[0], 'line_number' => $comment->getStartLine() + substr_count( $comment_text, "\n", @@ -225,10 +217,10 @@ public static function parse( foreach ($parsed_docblock->tags['psalm-if-this-is'] as $offset => $param) { $line_parts = CommentAnalyzer::splitDocLine($param); - $line_parts[0] = str_replace("\n", '', (string) preg_replace('@^[ \t]*\*@m', '', $line_parts[0])); + $line_parts[0] = CommentAnalyzer::sanitizeDocblockType($line_parts[0]); $info->if_this_is = [ - 'type' => str_replace("\n", '', $line_parts[0]), + 'type' => $line_parts[0], 'line_number' => $comment->getStartLine() + substr_count( $comment->getText(), "\n", @@ -454,7 +446,7 @@ public static function parse( $templates = []; if (isset($parsed_docblock->combined_tags['template'])) { foreach ($parsed_docblock->combined_tags['template'] as $offset => $template_line) { - $template_type = preg_split('/[\s]+/', (string) preg_replace('@^[ \t]*\*@m', '', $template_line)); + $template_type = preg_split('/[\s]+/', CommentAnalyzer::sanitizeDocblockType($template_line)); if ($template_type === false) { throw new AssertionError(preg_last_error_msg()); } diff --git a/tests/Template/TraitTemplateTest.php b/tests/Template/TraitTemplateTest.php index 86cf5d8f022..7074c24ea10 100644 --- a/tests/Template/TraitTemplateTest.php +++ b/tests/Template/TraitTemplateTest.php @@ -168,6 +168,32 @@ class B { use T; }', ], + 'multilineTemplateUse' => [ + 'code' => ' + */ + use MyTrait; + } + + class Bar { + /** + * @template-use MyTrait + */ + use MyTrait; + }', + ], 'allowTraitExtendAndImplementWithExplicitParamType' => [ 'code' => ' Date: Thu, 26 Oct 2023 12:39:02 +0100 Subject: [PATCH 027/357] Fix for spaces after , in multiline docblock types --- src/Psalm/Internal/Analyzer/CommentAnalyzer.php | 2 +- tests/TypeAnnotationTest.php | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php index ae03ba45311..7428f91f3ae 100644 --- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php @@ -261,7 +261,7 @@ private static function decorateVarDocblockComment( public static function sanitizeDocblockType(string $docblock_type): string { $docblock_type = (string) preg_replace('@^[ \t]*\*@m', '', $docblock_type); - $docblock_type = (string) preg_replace('/,\n\s+}/', '}', $docblock_type); + $docblock_type = (string) preg_replace('/,[\n\s]+}/', '}', $docblock_type); $docblock_type = (string) preg_replace('/[ \t]+/', ' ', $docblock_type); return trim(str_replace("\n", '', $docblock_type)); diff --git a/tests/TypeAnnotationTest.php b/tests/TypeAnnotationTest.php index 79ade7c41f6..07058f21998 100644 --- a/tests/TypeAnnotationTest.php +++ b/tests/TypeAnnotationTest.php @@ -835,6 +835,20 @@ class Foo { '$output===' => 'array{phone: string}', ], ], + 'multilineTypeWithExtraSpace' => [ + 'code' => ' [ 'code' => ' Date: Thu, 26 Oct 2023 13:24:00 +0100 Subject: [PATCH 028/357] Fix parsing of class-string-map --- src/Psalm/Internal/Type/ParseTreeCreator.php | 3 ++- src/Psalm/Internal/Type/TypeTokenizer.php | 8 ++++---- tests/TypeParseTest.php | 8 ++++++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Psalm/Internal/Type/ParseTreeCreator.php b/src/Psalm/Internal/Type/ParseTreeCreator.php index fcd0c77aeb0..eb57c1ad41c 100644 --- a/src/Psalm/Internal/Type/ParseTreeCreator.php +++ b/src/Psalm/Internal/Type/ParseTreeCreator.php @@ -143,6 +143,7 @@ public function create(): ParseTree case 'is': case 'as': + case 'of': $this->handleIsOrAs($type_token); break; @@ -771,7 +772,7 @@ private function handleIsOrAs(array $type_token): void array_pop($current_parent->children); } - if ($type_token[0] === 'as') { + if ($type_token[0] === 'as' || $type_token[0] == 'of') { $next_token = $this->t + 1 < $this->type_token_count ? $this->type_tokens[$this->t + 1] : null; if (!$this->current_leaf instanceof Value diff --git a/src/Psalm/Internal/Type/TypeTokenizer.php b/src/Psalm/Internal/Type/TypeTokenizer.php index a772bc7503e..9b4c6fdefda 100644 --- a/src/Psalm/Internal/Type/TypeTokenizer.php +++ b/src/Psalm/Internal/Type/TypeTokenizer.php @@ -9,9 +9,11 @@ use Psalm\Internal\Type\TypeAlias\InlineTypeAlias; use Psalm\Type; +use function array_slice; use function array_splice; use function array_unshift; use function count; +use function implode; use function in_array; use function is_numeric; use function preg_match; @@ -146,11 +148,9 @@ public static function tokenize(string $string_type, bool $ignore_space = true): $type_tokens[++$rtc] = [' ', $i - 1]; $type_tokens[++$rtc] = ['', $i]; } elseif ($was_space - && ($char === 'a' || $char === 'i') - && ($chars[$i + 1] ?? null) === 's' - && ($chars[$i + 2] ?? null) === ' ' + && in_array(implode('', array_slice($chars, $i, 3)), ['as ', 'is ', 'of ']) ) { - $type_tokens[++$rtc] = [$char . 's', $i - 1]; + $type_tokens[++$rtc] = [$char . $chars[$i+1], $i - 1]; $type_tokens[++$rtc] = ['', ++$i]; $was_char = false; continue; diff --git a/tests/TypeParseTest.php b/tests/TypeParseTest.php index 2ebae82ce4b..53536356689 100644 --- a/tests/TypeParseTest.php +++ b/tests/TypeParseTest.php @@ -935,6 +935,14 @@ public function testClassStringMap(): void ); } + public function testClassStringMapOf(): void + { + $this->assertSame( + 'class-string-map', + Type::parseString('class-string-map')->getId(false), + ); + } + public function testVeryLargeType(): void { $very_large_type = 'array{a: Closure():(array|null), b?: Closure():array, c?: Closure():array, d?: Closure():array, e?: Closure():(array{f: null|string, g: null|string, h: null|string, i: string, j: mixed, k: mixed, l: mixed, m: mixed, n: bool, o?: array{0: string}}|null), p?: Closure():(array{f: null|string, g: null|string, h: null|string, i: string, j: mixed, k: mixed, l: mixed, m: mixed, n: bool, o?: array{0: string}}|null), q: string, r?: Closure():(array|null), s: array}|null'; From e76db142f8c160aa31549f97bee8df8d00fee5e6 Mon Sep 17 00:00:00 2001 From: robchett Date: Thu, 26 Oct 2023 13:40:50 +0100 Subject: [PATCH 029/357] Suppress '@template T as' test failures --- tests/AnnotationTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/AnnotationTest.php b/tests/AnnotationTest.php index 765dcaca8bb..defd951aa28 100644 --- a/tests/AnnotationTest.php +++ b/tests/AnnotationTest.php @@ -1679,7 +1679,7 @@ class A { }', 'error_message' => 'InvalidDocblock', ], - 'noCrashOnInvalidClassTemplateAsType' => [ + 'SKIPPED-noCrashOnInvalidClassTemplateAsType' => [ 'code' => ' 'InvalidDocblock', ], - 'noCrashOnInvalidFunctionTemplateAsType' => [ + 'SKIPPED-noCrashOnInvalidFunctionTemplateAsType' => [ 'code' => ' Date: Thu, 26 Oct 2023 14:57:48 +0100 Subject: [PATCH 030/357] Skip inline docblocks like {@see ...} --- src/Psalm/Internal/Type/ParseTreeCreator.php | 24 +++++++++++++++++--- tests/MethodSignatureTest.php | 9 ++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Type/ParseTreeCreator.php b/src/Psalm/Internal/Type/ParseTreeCreator.php index eb57c1ad41c..737b5ac3f1b 100644 --- a/src/Psalm/Internal/Type/ParseTreeCreator.php +++ b/src/Psalm/Internal/Type/ParseTreeCreator.php @@ -31,6 +31,7 @@ use function in_array; use function preg_match; use function strlen; +use function strpos; use function strtolower; /** @@ -825,13 +826,30 @@ private function handleValue(array $type_token): void break; case '{': + ++$this->t; + + $nexter_token = $this->t + 1 < $this->type_token_count ? $this->type_tokens[$this->t + 1] : null; + + if ($nexter_token && strpos($nexter_token[0], '@') !== false) { + $this->t = $this->type_token_count; + if ($type_token[0] === '$this') { + $type_token[0] = 'static'; + } + + $new_leaf = new Value( + $type_token[0], + $type_token[1], + $type_token[1] + strlen($type_token[0]), + $type_token[2] ?? null, + $new_parent, + ); + break; + } + $new_leaf = new KeyedArrayTree( $type_token[0], $new_parent, ); - ++$this->t; - - $nexter_token = $this->t + 1 < $this->type_token_count ? $this->type_tokens[$this->t + 1] : null; if ($nexter_token !== null && $nexter_token[0] === '}') { $new_leaf->terminated = true; diff --git a/tests/MethodSignatureTest.php b/tests/MethodSignatureTest.php index f20e73a03e5..e60ddb4c627 100644 --- a/tests/MethodSignatureTest.php +++ b/tests/MethodSignatureTest.php @@ -400,6 +400,15 @@ public static function foo() { '$b' => 'B', ], ], + 'returnIgnoresInlineComments' => [ + 'code' => ' [ 'code' => ' Date: Thu, 26 Oct 2023 14:58:38 +0100 Subject: [PATCH 031/357] Fix some stub docblocks that were thowing parse errors --- stubs/CoreGenericIterators.phpstub | 4 ++-- stubs/Reflection.phpstub | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/stubs/CoreGenericIterators.phpstub b/stubs/CoreGenericIterators.phpstub index 43a7bb1f85c..8d38f399c62 100644 --- a/stubs/CoreGenericIterators.phpstub +++ b/stubs/CoreGenericIterators.phpstub @@ -477,7 +477,7 @@ class EmptyIterator implements Iterator { } /** - * @template-extends SeekableIterator + * @template-extends DirectoryIterator */ class FilesystemIterator extends DirectoryIterator { @@ -523,7 +523,7 @@ class FilesystemIterator extends DirectoryIterator /** - * @template-extends SeekableIterator + * @template-extends FilesystemIterator */ class GlobIterator extends FilesystemIterator implements Countable { /** diff --git a/stubs/Reflection.phpstub b/stubs/Reflection.phpstub index 3e86431e581..4007be3f007 100644 --- a/stubs/Reflection.phpstub +++ b/stubs/Reflection.phpstub @@ -496,7 +496,7 @@ class ReflectionProperty implements Reflector public function isDefault(): bool {} /** - * @return int-mask-of + * @return int-mask-of * @psalm-pure */ public function getModifiers(): int {} From 3cf93345a9fb50f075c40954fe045fb9c852398b Mon Sep 17 00:00:00 2001 From: robchett Date: Thu, 2 Nov 2023 13:13:11 +0000 Subject: [PATCH 032/357] Sanitize docblocks for psalm-check-type --- src/Psalm/Internal/Analyzer/StatementsAnalyzer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index 3306c6627ba..6187083430d 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -684,7 +684,7 @@ private static function analyzeStatement( $check_type_string, $statements_analyzer->getAliases(), ); - $check_type = Type::parseString($fq_check_type_string); + $check_type = Type::parseString(CommentAnalyzer::sanitizeDocblockType($fq_check_type_string)); /** @psalm-suppress InaccessibleProperty We just created this type */ $check_type->possibly_undefined = $possibly_undefined; From ec5eae33476e7bd4d985e1c57d6403811064469d Mon Sep 17 00:00:00 2001 From: robchett Date: Sun, 8 Oct 2023 16:42:44 +0100 Subject: [PATCH 033/357] Maintain loop start value after an increment --- .../BinaryOp/ArithmeticOpAnalyzer.php | 3 ++- tests/BinaryOperationTest.php | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php index 90e8f78b881..478c34c2896 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php @@ -822,8 +822,9 @@ private static function analyzeOperands( } } } else { + $start = $left_type_part instanceof TLiteralInt ? $left_type_part->value : 1; $result_type = Type::combineUnionTypes( - $always_positive ? Type::getIntRange(1, null) : Type::getInt(true), + $always_positive ? Type::getIntRange($start, null) : Type::getInt(true), $result_type, ); } diff --git a/tests/BinaryOperationTest.php b/tests/BinaryOperationTest.php index 094f1bbe09c..b1013c65435 100644 --- a/tests/BinaryOperationTest.php +++ b/tests/BinaryOperationTest.php @@ -955,6 +955,23 @@ function scope(array $a): int|float { '$b' => 'float|int', ], ], + 'incrementInLoop' => [ + 'code' => ' [ + '$i' => 'int<0, 10>', + '$j' => 'int<100, 110>', + ], + ], 'coalesceFilterOutNullEvenWithTernary' => [ 'code' => ' Date: Sun, 8 Oct 2023 16:56:03 +0100 Subject: [PATCH 034/357] Correct decrement min/max ranges --- .../BinaryOp/ArithmeticOpAnalyzer.php | 11 +++++++++-- tests/BinaryOperationTest.php | 17 +++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php index 478c34c2896..4a14d1f25d9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php @@ -24,6 +24,8 @@ use Psalm\Issue\PossiblyNullOperand; use Psalm\Issue\StringIncrement; use Psalm\IssueBuffer; +use Psalm\Node\Expr\BinaryOp\VirtualMinus; +use Psalm\Node\Expr\BinaryOp\VirtualPlus; use Psalm\StatementsSource; use Psalm\Type; use Psalm\Type\Atomic; @@ -821,10 +823,15 @@ private static function analyzeOperands( $result_type = Type::getInt(); } } - } else { + } elseif ($parent instanceof VirtualPlus) { + $start = $left_type_part instanceof TLiteralInt ? $left_type_part->value : 1; + $result_type = Type::combineUnionTypes(Type::getIntRange($start, null), $result_type); + } elseif ($parent instanceof VirtualMinus) { $start = $left_type_part instanceof TLiteralInt ? $left_type_part->value : 1; + $result_type = Type::combineUnionTypes(Type::getIntRange(null, $start), $result_type); + } else { $result_type = Type::combineUnionTypes( - $always_positive ? Type::getIntRange($start, null) : Type::getInt(true), + $always_positive ? Type::getIntRange(1, null) : Type::getInt(true), $result_type, ); } diff --git a/tests/BinaryOperationTest.php b/tests/BinaryOperationTest.php index b1013c65435..c2463e43d83 100644 --- a/tests/BinaryOperationTest.php +++ b/tests/BinaryOperationTest.php @@ -972,6 +972,23 @@ function scope(array $a): int|float { '$j' => 'int<100, 110>', ], ], + 'decrementInLoop' => [ + 'code' => ' 0; $i--) { + if (rand(0,1)) { + break; + } + } + for ($j = 110; $j > 100; $j--) { + if (rand(0,1)) { + break; + } + }', + 'assertions' => [ + '$i' => 'int<0, 10>', + '$j' => 'int<100, 110>', + ], + ], 'coalesceFilterOutNullEvenWithTernary' => [ 'code' => ' Date: Sun, 8 Oct 2023 20:29:28 +0100 Subject: [PATCH 035/357] Better reconciling of ++/-- operators in ints --- .../BinaryOp/ArithmeticOpAnalyzer.php | 28 +++++++-- .../RedundantConditionTest.php | 58 +++++++++++++++++++ 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php index 4a14d1f25d9..5d4e3e290aa 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php @@ -823,12 +823,28 @@ private static function analyzeOperands( $result_type = Type::getInt(); } } - } elseif ($parent instanceof VirtualPlus) { - $start = $left_type_part instanceof TLiteralInt ? $left_type_part->value : 1; - $result_type = Type::combineUnionTypes(Type::getIntRange($start, null), $result_type); - } elseif ($parent instanceof VirtualMinus) { - $start = $left_type_part instanceof TLiteralInt ? $left_type_part->value : 1; - $result_type = Type::combineUnionTypes(Type::getIntRange(null, $start), $result_type); + } elseif ($parent instanceof VirtualPlus || $parent instanceof VirtualMinus) { + $sum = $parent instanceof VirtualPlus ? 1 : -1; + if ($context && $context->inside_loop && $left_type_part instanceof TLiteralInt) { + if ($parent instanceof VirtualPlus) { + $new_type = new TIntRange($left_type_part->value + $sum, null); + } else { + $new_type = new TIntRange(null, $left_type_part->value + $sum); + } + } elseif ($left_type_part instanceof TLiteralInt) { + $new_type = new TLiteralInt($left_type_part->value + $sum); + } elseif ($left_type_part instanceof TIntRange) { + $start = $left_type_part->min_bound === null ? null : $left_type_part->min_bound + $sum; + $end = $left_type_part->max_bound === null ? null : $left_type_part->max_bound + $sum; + $new_type = new TIntRange($start, $end); + } else { + $new_type = new TInt(); + } + + $result_type = Type::combineUnionTypes( + new Union([$new_type], ['from_calculation' => true]), + $result_type, + ); } else { $result_type = Type::combineUnionTypes( $always_positive ? Type::getIntRange(1, null) : Type::getInt(true), diff --git a/tests/TypeReconciliation/RedundantConditionTest.php b/tests/TypeReconciliation/RedundantConditionTest.php index b6e01460cc9..a7217834014 100644 --- a/tests/TypeReconciliation/RedundantConditionTest.php +++ b/tests/TypeReconciliation/RedundantConditionTest.php @@ -441,6 +441,64 @@ function foo(int $x) : void { } }', ], + 'allowIntValueCheckAfterComparisonDueToUnderflow' => [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ 'code' => ' Date: Thu, 26 Oct 2023 11:25:12 +0100 Subject: [PATCH 036/357] Rework test as it was a false negative --- tests/Loop/ForTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Loop/ForTest.php b/tests/Loop/ForTest.php index f8fd5f2b22b..037893b1ef4 100644 --- a/tests/Loop/ForTest.php +++ b/tests/Loop/ForTest.php @@ -143,7 +143,7 @@ function test(Node $head) { * @param list $arr */ function cartesianProduct(array $arr) : void { - for ($i = 20; $arr[$i] === 5 && $i > 0; $i--) {} + for ($i = 20; $i > 0 && $arr[$i] === 5 ; $i--) {} }', ], 'noCrashOnLongThing' => [ From e015b7f7de315fddf5332405726d69ec4a4a72a1 Mon Sep 17 00:00:00 2001 From: Oliver Hader Date: Mon, 6 Nov 2023 21:38:49 +0100 Subject: [PATCH 037/357] Add documentation for @psalm-flow --- docs/security_analysis/annotations.md | 4 ++ docs/security_analysis/taint_flow.md | 66 +++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 docs/security_analysis/taint_flow.md diff --git a/docs/security_analysis/annotations.md b/docs/security_analysis/annotations.md index dd552743187..d3650922c9e 100644 --- a/docs/security_analysis/annotations.md +++ b/docs/security_analysis/annotations.md @@ -19,3 +19,7 @@ See [Unescaping statements](avoiding_false_negatives.md#unescaping-statements). ## `@psalm-taint-specialize` See [Specializing taints in functions](avoiding_false_positives.md#specializing-taints-in-functions) and [Specializing taints in classes](avoiding_false_positives.md#specializing-taints-in-classes). + +## `@psalm-flow [proxy ] ( , [ , ] ) [ -> return ]` + +See [Taint Flow](taint_flow.md#optimized-taint-flow) diff --git a/docs/security_analysis/taint_flow.md b/docs/security_analysis/taint_flow.md new file mode 100644 index 00000000000..2c77c07950b --- /dev/null +++ b/docs/security_analysis/taint_flow.md @@ -0,0 +1,66 @@ +# Taint Flow + +## Optimized Taint Flow + +When dealing with frameworks, keeping track of the data flow might involve different layers +and even other 3rd party components. Using the `@psalm-flow` annotation allows PsalmPHP to +take a shortcut and to make a tainted data flow more explicit. + +### Proxy hint + +```php + return + */ +function inputOutputHandler(string $value, string ...$items): string +{ + // lots of complicated magic +} + +echo inputOutputHandler('first', 'second', $_GET['malicious'] ?? ''); +``` + +The example above states, that the function parameters `$value` and `$items` are reflected +again in the return value. Thus, in case any of the input parameters to the function +`inputOutputHandler` is tainted, then the resulting return value is as well. In this +example `TaintedHtml` would be detected due to using `echo`. + +### Combined proxy & return value hint + +```php + return + */ +function handleInput(string $value, string ...$items): string +{ + // lots of complicated magic +} + +echo handleInput($_GET['malicious'] ?? ''); +``` + +The example above combines both previous examples and shows, that the `@psalm-flow` annotation +can be used multiple times. Here, it would lead to detecting both `TaintedHtml` and `TaintedShell`. From 3578b42e5c25702a2b34c565b457ab30d3da4027 Mon Sep 17 00:00:00 2001 From: robchett Date: Tue, 7 Nov 2023 10:46:53 +0000 Subject: [PATCH 038/357] Add stubs for rdKafka Fixes #3406 --- dictionaries/CallMap.php | 91 -- dictionaries/CallMap_historical.php | 91 -- src/Psalm/Config.php | 1 + stubs/extensions/rdkafka.phpstub | 1195 +++++++++++++++++++++++++++ 4 files changed, 1196 insertions(+), 182 deletions(-) create mode 100644 stubs/extensions/rdkafka.phpstub diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 782ed5d8fdf..ccefe6a0b6f 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -9696,97 +9696,6 @@ 'RarException::setUsingExceptions' => ['RarEntry', 'using_exceptions'=>'bool'], 'rawurldecode' => ['string', 'string'=>'string'], 'rawurlencode' => ['string', 'string'=>'string'], -'rd_kafka_err2str' => ['string', 'err'=>'int'], -'rd_kafka_errno' => ['int'], -'rd_kafka_errno2err' => ['int', 'errnox'=>'int'], -'rd_kafka_offset_tail' => ['int', 'cnt'=>'int'], -'RdKafka::addBrokers' => ['int', 'broker_list'=>'string'], -'RdKafka::flush' => ['int', 'timeout_ms'=>'int'], -'RdKafka::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\Topic', 'timeout_ms'=>'int'], -'RdKafka::getOutQLen' => ['int'], -'RdKafka::newQueue' => ['RdKafka\Queue'], -'RdKafka::newTopic' => ['RdKafka\Topic', 'topic_name'=>'string', 'topic_conf='=>'?RdKafka\TopicConf'], -'RdKafka::poll' => ['void', 'timeout_ms'=>'int'], -'RdKafka::setLogLevel' => ['void', 'level'=>'int'], -'RdKafka\Conf::dump' => ['array'], -'RdKafka\Conf::set' => ['void', 'name'=>'string', 'value'=>'string'], -'RdKafka\Conf::setDefaultTopicConf' => ['void', 'topic_conf'=>'RdKafka\TopicConf'], -'RdKafka\Conf::setDrMsgCb' => ['void', 'callback'=>'callable'], -'RdKafka\Conf::setErrorCb' => ['void', 'callback'=>'callable'], -'RdKafka\Conf::setRebalanceCb' => ['void', 'callback'=>'callable'], -'RdKafka\Conf::setStatsCb' => ['void', 'callback'=>'callable'], -'RdKafka\Consumer::__construct' => ['void', 'conf='=>'?RdKafka\Conf'], -'RdKafka\Consumer::addBrokers' => ['int', 'broker_list'=>'string'], -'RdKafka\Consumer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\Topic', 'timeout_ms'=>'int'], -'RdKafka\Consumer::getOutQLen' => ['int'], -'RdKafka\Consumer::newQueue' => ['RdKafka\Queue'], -'RdKafka\Consumer::newTopic' => ['RdKafka\ConsumerTopic', 'topic_name'=>'string', 'topic_conf='=>'?RdKafka\TopicConf'], -'RdKafka\Consumer::poll' => ['void', 'timeout_ms'=>'int'], -'RdKafka\Consumer::setLogLevel' => ['void', 'level'=>'int'], -'RdKafka\ConsumerTopic::__construct' => ['void'], -'RdKafka\ConsumerTopic::consume' => ['RdKafka\Message', 'partition'=>'int', 'timeout_ms'=>'int'], -'RdKafka\ConsumerTopic::consumeQueueStart' => ['void', 'partition'=>'int', 'offset'=>'int', 'queue'=>'RdKafka\Queue'], -'RdKafka\ConsumerTopic::consumeStart' => ['void', 'partition'=>'int', 'offset'=>'int'], -'RdKafka\ConsumerTopic::consumeStop' => ['void', 'partition'=>'int'], -'RdKafka\ConsumerTopic::getName' => ['string'], -'RdKafka\ConsumerTopic::offsetStore' => ['void', 'partition'=>'int', 'offset'=>'int'], -'RdKafka\KafkaConsumer::__construct' => ['void', 'conf'=>'RdKafka\Conf'], -'RdKafka\KafkaConsumer::assign' => ['void', 'topic_partitions='=>'RdKafka\TopicPartition[]|null'], -'RdKafka\KafkaConsumer::commit' => ['void', 'message_or_offsets='=>'RdKafka\Message|RdKafka\TopicPartition[]|null'], -'RdKafka\KafkaConsumer::commitAsync' => ['void', 'message_or_offsets='=>'RdKafka\Message|RdKafka\TopicPartition[]|null'], -'RdKafka\KafkaConsumer::consume' => ['RdKafka\Message', 'timeout_ms'=>'int'], -'RdKafka\KafkaConsumer::getAssignment' => ['RdKafka\TopicPartition[]'], -'RdKafka\KafkaConsumer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\KafkaConsumerTopic', 'timeout_ms'=>'int'], -'RdKafka\KafkaConsumer::getSubscription' => ['array'], -'RdKafka\KafkaConsumer::subscribe' => ['void', 'topics'=>'array'], -'RdKafka\KafkaConsumer::unsubscribe' => ['void'], -'RdKafka\KafkaConsumerTopic::getName' => ['string'], -'RdKafka\KafkaConsumerTopic::offsetStore' => ['void', 'partition'=>'int', 'offset'=>'int'], -'RdKafka\Message::errstr' => ['string'], -'RdKafka\Metadata::getBrokers' => ['RdKafka\Metadata\Collection'], -'RdKafka\Metadata::getOrigBrokerId' => ['int'], -'RdKafka\Metadata::getOrigBrokerName' => ['string'], -'RdKafka\Metadata::getTopics' => ['RdKafka\Metadata\Collection|RdKafka\Metadata\Topic[]'], -'RdKafka\Metadata\Collection::__construct' => ['void'], -'RdKafka\Metadata\Collection::count' => ['int'], -'RdKafka\Metadata\Collection::current' => ['mixed'], -'RdKafka\Metadata\Collection::key' => ['mixed'], -'RdKafka\Metadata\Collection::next' => ['void'], -'RdKafka\Metadata\Collection::rewind' => ['void'], -'RdKafka\Metadata\Collection::valid' => ['bool'], -'RdKafka\Metadata\Partition::getErr' => ['mixed'], -'RdKafka\Metadata\Partition::getId' => ['int'], -'RdKafka\Metadata\Partition::getIsrs' => ['mixed'], -'RdKafka\Metadata\Partition::getLeader' => ['mixed'], -'RdKafka\Metadata\Partition::getReplicas' => ['mixed'], -'RdKafka\Metadata\Topic::getErr' => ['mixed'], -'RdKafka\Metadata\Topic::getPartitions' => ['RdKafka\Metadata\Partition[]'], -'RdKafka\Metadata\Topic::getTopic' => ['string'], -'RdKafka\Producer::__construct' => ['void', 'conf='=>'?RdKafka\Conf'], -'RdKafka\Producer::addBrokers' => ['int', 'broker_list'=>'string'], -'RdKafka\Producer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\Topic', 'timeout_ms'=>'int'], -'RdKafka\Producer::getOutQLen' => ['int'], -'RdKafka\Producer::newQueue' => ['RdKafka\Queue'], -'RdKafka\Producer::newTopic' => ['RdKafka\ProducerTopic', 'topic_name'=>'string', 'topic_conf='=>'?RdKafka\TopicConf'], -'RdKafka\Producer::poll' => ['void', 'timeout_ms'=>'int'], -'RdKafka\Producer::setLogLevel' => ['void', 'level'=>'int'], -'RdKafka\ProducerTopic::__construct' => ['void'], -'RdKafka\ProducerTopic::getName' => ['string'], -'RdKafka\ProducerTopic::produce' => ['void', 'partition'=>'int', 'msgflags'=>'int', 'payload'=>'string', 'key='=>'?string'], -'RdKafka\ProducerTopic::producev' => ['void', 'partition'=>'int', 'msgflags'=>'int', 'payload'=>'string', 'key='=>'?string', 'headers='=>'?array', 'timestamp_ms='=>'?int', 'opaque='=>'?string'], -'RdKafka\Queue::__construct' => ['void'], -'RdKafka\Queue::consume' => ['?RdKafka\Message', 'timeout_ms'=>'string'], -'RdKafka\Topic::getName' => ['string'], -'RdKafka\TopicConf::dump' => ['array'], -'RdKafka\TopicConf::set' => ['void', 'name'=>'string', 'value'=>'string'], -'RdKafka\TopicConf::setPartitioner' => ['void', 'partitioner'=>'int'], -'RdKafka\TopicPartition::__construct' => ['void', 'topic'=>'string', 'partition'=>'int', 'offset='=>'int'], -'RdKafka\TopicPartition::getOffset' => ['int'], -'RdKafka\TopicPartition::getPartition' => ['int'], -'RdKafka\TopicPartition::getTopic' => ['string'], -'RdKafka\TopicPartition::setOffset' => ['void', 'offset'=>'string'], -'RdKafka\TopicPartition::setPartition' => ['void', 'partition'=>'string'], -'RdKafka\TopicPartition::setTopic' => ['void', 'topic_name'=>'string'], 'readdir' => ['string|false', 'dir_handle='=>'resource'], 'readfile' => ['int|false', 'filename'=>'string', 'use_include_path='=>'bool', 'context='=>'resource'], 'readgzfile' => ['int|false', 'filename'=>'string', 'use_include_path='=>'int'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 5ff4033aff0..73ce32e3d7e 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -5070,93 +5070,6 @@ 'RarException::getTraceAsString' => ['string'], 'RarException::isUsingExceptions' => ['bool'], 'RarException::setUsingExceptions' => ['RarEntry', 'using_exceptions'=>'bool'], - 'RdKafka::addBrokers' => ['int', 'broker_list'=>'string'], - 'RdKafka::flush' => ['int', 'timeout_ms'=>'int'], - 'RdKafka::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\Topic', 'timeout_ms'=>'int'], - 'RdKafka::getOutQLen' => ['int'], - 'RdKafka::newQueue' => ['RdKafka\Queue'], - 'RdKafka::newTopic' => ['RdKafka\Topic', 'topic_name'=>'string', 'topic_conf='=>'?RdKafka\TopicConf'], - 'RdKafka::poll' => ['void', 'timeout_ms'=>'int'], - 'RdKafka::setLogLevel' => ['void', 'level'=>'int'], - 'RdKafka\Conf::dump' => ['array'], - 'RdKafka\Conf::set' => ['void', 'name'=>'string', 'value'=>'string'], - 'RdKafka\Conf::setDefaultTopicConf' => ['void', 'topic_conf'=>'RdKafka\TopicConf'], - 'RdKafka\Conf::setDrMsgCb' => ['void', 'callback'=>'callable'], - 'RdKafka\Conf::setErrorCb' => ['void', 'callback'=>'callable'], - 'RdKafka\Conf::setRebalanceCb' => ['void', 'callback'=>'callable'], - 'RdKafka\Conf::setStatsCb' => ['void', 'callback'=>'callable'], - 'RdKafka\Consumer::__construct' => ['void', 'conf='=>'?RdKafka\Conf'], - 'RdKafka\Consumer::addBrokers' => ['int', 'broker_list'=>'string'], - 'RdKafka\Consumer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\Topic', 'timeout_ms'=>'int'], - 'RdKafka\Consumer::getOutQLen' => ['int'], - 'RdKafka\Consumer::newQueue' => ['RdKafka\Queue'], - 'RdKafka\Consumer::newTopic' => ['RdKafka\ConsumerTopic', 'topic_name'=>'string', 'topic_conf='=>'?RdKafka\TopicConf'], - 'RdKafka\Consumer::poll' => ['void', 'timeout_ms'=>'int'], - 'RdKafka\Consumer::setLogLevel' => ['void', 'level'=>'int'], - 'RdKafka\ConsumerTopic::__construct' => ['void'], - 'RdKafka\ConsumerTopic::consume' => ['RdKafka\Message', 'partition'=>'int', 'timeout_ms'=>'int'], - 'RdKafka\ConsumerTopic::consumeQueueStart' => ['void', 'partition'=>'int', 'offset'=>'int', 'queue'=>'RdKafka\Queue'], - 'RdKafka\ConsumerTopic::consumeStart' => ['void', 'partition'=>'int', 'offset'=>'int'], - 'RdKafka\ConsumerTopic::consumeStop' => ['void', 'partition'=>'int'], - 'RdKafka\ConsumerTopic::getName' => ['string'], - 'RdKafka\ConsumerTopic::offsetStore' => ['void', 'partition'=>'int', 'offset'=>'int'], - 'RdKafka\KafkaConsumer::__construct' => ['void', 'conf'=>'RdKafka\Conf'], - 'RdKafka\KafkaConsumer::assign' => ['void', 'topic_partitions='=>'RdKafka\TopicPartition[]|null'], - 'RdKafka\KafkaConsumer::commit' => ['void', 'message_or_offsets='=>'RdKafka\Message|RdKafka\TopicPartition[]|null'], - 'RdKafka\KafkaConsumer::commitAsync' => ['void', 'message_or_offsets='=>'RdKafka\Message|RdKafka\TopicPartition[]|null'], - 'RdKafka\KafkaConsumer::consume' => ['RdKafka\Message', 'timeout_ms'=>'int'], - 'RdKafka\KafkaConsumer::getAssignment' => ['RdKafka\TopicPartition[]'], - 'RdKafka\KafkaConsumer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\KafkaConsumerTopic', 'timeout_ms'=>'int'], - 'RdKafka\KafkaConsumer::getSubscription' => ['array'], - 'RdKafka\KafkaConsumer::subscribe' => ['void', 'topics'=>'array'], - 'RdKafka\KafkaConsumer::unsubscribe' => ['void'], - 'RdKafka\KafkaConsumerTopic::getName' => ['string'], - 'RdKafka\KafkaConsumerTopic::offsetStore' => ['void', 'partition'=>'int', 'offset'=>'int'], - 'RdKafka\Message::errstr' => ['string'], - 'RdKafka\Metadata::getBrokers' => ['RdKafka\Metadata\Collection'], - 'RdKafka\Metadata::getOrigBrokerId' => ['int'], - 'RdKafka\Metadata::getOrigBrokerName' => ['string'], - 'RdKafka\Metadata::getTopics' => ['RdKafka\Metadata\Collection|RdKafka\Metadata\Topic[]'], - 'RdKafka\Metadata\Collection::__construct' => ['void'], - 'RdKafka\Metadata\Collection::count' => ['int'], - 'RdKafka\Metadata\Collection::current' => ['mixed'], - 'RdKafka\Metadata\Collection::key' => ['mixed'], - 'RdKafka\Metadata\Collection::next' => ['void'], - 'RdKafka\Metadata\Collection::rewind' => ['void'], - 'RdKafka\Metadata\Collection::valid' => ['bool'], - 'RdKafka\Metadata\Partition::getErr' => ['mixed'], - 'RdKafka\Metadata\Partition::getId' => ['int'], - 'RdKafka\Metadata\Partition::getIsrs' => ['mixed'], - 'RdKafka\Metadata\Partition::getLeader' => ['mixed'], - 'RdKafka\Metadata\Partition::getReplicas' => ['mixed'], - 'RdKafka\Metadata\Topic::getErr' => ['mixed'], - 'RdKafka\Metadata\Topic::getPartitions' => ['RdKafka\Metadata\Partition[]'], - 'RdKafka\Metadata\Topic::getTopic' => ['string'], - 'RdKafka\Producer::__construct' => ['void', 'conf='=>'?RdKafka\Conf'], - 'RdKafka\Producer::addBrokers' => ['int', 'broker_list'=>'string'], - 'RdKafka\Producer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\Topic', 'timeout_ms'=>'int'], - 'RdKafka\Producer::getOutQLen' => ['int'], - 'RdKafka\Producer::newQueue' => ['RdKafka\Queue'], - 'RdKafka\Producer::newTopic' => ['RdKafka\ProducerTopic', 'topic_name'=>'string', 'topic_conf='=>'?RdKafka\TopicConf'], - 'RdKafka\Producer::poll' => ['void', 'timeout_ms'=>'int'], - 'RdKafka\Producer::setLogLevel' => ['void', 'level'=>'int'], - 'RdKafka\ProducerTopic::__construct' => ['void'], - 'RdKafka\ProducerTopic::getName' => ['string'], - 'RdKafka\ProducerTopic::produce' => ['void', 'partition'=>'int', 'msgflags'=>'int', 'payload'=>'string', 'key='=>'?string'], - 'RdKafka\ProducerTopic::producev' => ['void', 'partition'=>'int', 'msgflags'=>'int', 'payload'=>'string', 'key='=>'?string', 'headers='=>'?array', 'timestamp_ms='=>'?int', 'opaque='=>'?string'], - 'RdKafka\Queue::__construct' => ['void'], - 'RdKafka\Queue::consume' => ['?RdKafka\Message', 'timeout_ms'=>'string'], - 'RdKafka\Topic::getName' => ['string'], - 'RdKafka\TopicConf::dump' => ['array'], - 'RdKafka\TopicConf::set' => ['void', 'name'=>'string', 'value'=>'string'], - 'RdKafka\TopicConf::setPartitioner' => ['void', 'partitioner'=>'int'], - 'RdKafka\TopicPartition::__construct' => ['void', 'topic'=>'string', 'partition'=>'int', 'offset='=>'int'], - 'RdKafka\TopicPartition::getOffset' => ['int'], - 'RdKafka\TopicPartition::getPartition' => ['int'], - 'RdKafka\TopicPartition::getTopic' => ['string'], - 'RdKafka\TopicPartition::setOffset' => ['void', 'offset'=>'string'], - 'RdKafka\TopicPartition::setPartition' => ['void', 'partition'=>'string'], - 'RdKafka\TopicPartition::setTopic' => ['void', 'topic_name'=>'string'], 'RecursiveArrayIterator::__construct' => ['void', 'array='=>'array|object', 'flags='=>'int'], 'RecursiveArrayIterator::append' => ['void', 'value'=>'mixed'], 'RecursiveArrayIterator::asort' => ['true', 'flags='=>'int'], @@ -13778,10 +13691,6 @@ 'rar_wrapper_cache_stats' => ['string'], 'rawurldecode' => ['string', 'string'=>'string'], 'rawurlencode' => ['string', 'string'=>'string'], - 'rd_kafka_err2str' => ['string', 'err'=>'int'], - 'rd_kafka_errno' => ['int'], - 'rd_kafka_errno2err' => ['int', 'errnox'=>'int'], - 'rd_kafka_offset_tail' => ['int', 'cnt'=>'int'], 'read_exif_data' => ['array', 'filename'=>'string', 'sections_needed='=>'string', 'sub_arrays='=>'bool', 'read_thumbnail='=>'bool'], 'readdir' => ['string|false', 'dir_handle='=>'resource'], 'readfile' => ['int|false', 'filename'=>'string', 'use_include_path='=>'bool', 'context='=>'resource'], diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 0eaa9f8378d..c2a54477344 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -631,6 +631,7 @@ class Config "mysqli" => null, "pdo" => null, "random" => null, + "rdkafka" => null, "redis" => null, "simplexml" => null, "soap" => null, diff --git a/stubs/extensions/rdkafka.phpstub b/stubs/extensions/rdkafka.phpstub new file mode 100644 index 00000000000..af8c3a76e9e --- /dev/null +++ b/stubs/extensions/rdkafka.phpstub @@ -0,0 +1,1195 @@ + + */ + public function dump() + { + } + + /** + * @param string $name + * @param string $value + * + * @return void + */ + public function set($name, $value) + { + } + + /** + * @param TopicConf $topic_conf + * + * @return void + * @deprecated Set default topic settings normally like global configuration settings. + * + */ + public function setDefaultTopicConf(TopicConf $topic_conf) + { + } + + /** + * @param callable $callback + * + * @return void + */ + public function setDrMsgCb(callable $callback) + { + } + + /** + * @param callable $callback + * + * @return void + */ + public function setErrorCb(callable $callback) + { + } + + /** + * @param callable $callback + * + * @return void + */ + public function setRebalanceCb(callable $callback) + { + } + + /** + * @param callable $callback + * + * @return void + */ + public function setStatsCb(callable $callback) + { + } + + /** + * @param callable $callback + * + * @return void + */ + public function setConsumeCb(callable $callback) + { + } + + /** + * @param callable $callback + * + * @return void + */ + public function setOffsetCommitCb(callable $callback) + { + } + + /** + * @param callable $callback + * + * @return void + */ + public function setLogCb(callable $callback) + { + } + } + + class Consumer extends \RdKafka + { + /** + * @param Conf $conf + */ + public function __construct(Conf $conf = null) + { + } + + /** + * @param string $topic_name + * @param TopicConf $topic_conf + * + * @return ConsumerTopic + */ + public function newTopic($topic_name, TopicConf $topic_conf = null) + { + } + + /** + * @return Queue + */ + public function newQueue() + { + } + } + + class ConsumerTopic extends Topic + { + private function __construct() + { + } + + /** + * @param int $partition + * @param int $timeout_ms + * + * @return Message|null + */ + public function consume($partition, $timeout_ms) + { + } + + /** + * @param int $partition + * @param int $offset + * @param Queue $queue + * + * @return void + */ + public function consumeQueueStart($partition, $offset, Queue $queue) + { + } + + /** + * @param int $partition + * @param int $offset + * + * @return void + */ + public function consumeStart($partition, $offset) + { + } + + /** + * @param int $partition + * + * @return void + */ + public function consumeStop($partition) + { + } + + /** + * @param int $partition + * @param int $offset + * + * @return void + */ + public function offsetStore($partition, $offset) + { + } + + /** + * @param int $partition + * @param int $timeout_ms + * @param int $batch_size + * + * @return array + * @throws \InvalidArgumentException + * @throws Exception + */ + public function consumeBatch($partition, $timeout_ms, $batch_size) + { + } + + /** + * @param int $partition + * @param int $timeout_ms + * @param callable $callback + * + * @return void + */ + public function consumeCallback($partition, $timeout_ms, callable $callback) + { + } + } + + class Exception extends \Exception + { + } + + class KafkaConsumer + { + /** + * @param Conf $conf + */ + public function __construct(Conf $conf) + { + } + + /** + * @param TopicPartition[] $topic_partitions + * + * @return void + * @throws Exception + */ + public function assign($topic_partitions = null) + { + } + + /** + * @param null|Message|TopicPartition[] $message_or_offsets + * + * @return void + * @throws Exception + */ + public function commit($message_or_offsets = null) + { + } + + /** + * @param string $message_or_offsets + * + * @return void + * @throws Exception + */ + public function commitAsync($message_or_offsets = null) + { + } + + /** + * @param int $timeout_ms + * + * @return Message + * @throws \InvalidArgumentException + * @throws Exception + */ + public function consume($timeout_ms) + { + } + + /** + * @return TopicPartition[] + * @throws Exception + */ + public function getAssignment() + { + } + + /** + * @param bool $all_topics + * @param KafkaConsumerTopic|null $only_topic + * @param int $timeout_ms + * + * @return Metadata + * @throws Exception + */ + public function getMetadata($all_topics, $only_topic, $timeout_ms) + { + } + + /** + * @return array + */ + public function getSubscription() + { + } + + /** + * @param string $topic_name + * @param TopicConf $topic_conf + * + * @return KafkaConsumerTopic + */ + public function newTopic($topic_name, TopicConf $topic_conf = null) + { + } + + /** + * @param array $topics + * + * @return void + * @throws Exception + */ + public function subscribe($topics) + { + } + + /** + * @return void + * @throws Exception + */ + public function unsubscribe() + { + } + + /** + * @param TopicPartition[] $topicPartitions + * @param int $timeout_ms Timeout in milliseconds + * + * @return TopicPartition[] + * @throws Exception + */ + public function getCommittedOffsets($topicPartitions, $timeout_ms) + { + } + + /** + * @param array $topicPartitions + * @param int $timeout_ms + * @return array + */ + public function offsetsForTimes($topicPartitions, $timeout_ms) + { + } + + /** + * @param string $topic + * @param int $partition + * @param int $low + * @param int $high + * @param int $timeout_ms + */ + public function queryWatermarkOffsets($topic, $partition, &$low, &$high, $timeout_ms) + { + } + + /** + * @param array $topics + * + * @return array + * @throws Exception + */ + public function getOffsetPositions($topics) + { + } + + /** + * @return void + */ + public function close() + { + } + + /** + * @param TopicPartition[] $topic_partitions + * @return TopicPartition[] + */ + public function pausePartitions($topic_partitions) + { + } + + /** + * @param TopicPartition[] $topic_partitions + * @return TopicPartition[] + */ + public function resumePartitions($topic_partitions) + { + } + } + + class KafkaConsumerTopic extends Topic + { + /** + * @param int $partition + * @param int $offset + * + * @return void + */ + public function offsetStore($partition, $offset) + { + } + } + + class KafkaErrorException extends \Exception + { + /** + * @param string $message + * @param int $code + * @param string $errorString + * @param boolean $isFatal + * @param boolean $isRetriable + * @param boolean $transactionRequiresAbort + */ + public function __construct($message, $code, $errorString, $isFatal, $isRetriable, $transactionRequiresAbort) + { + parent::__construct($message, $code); + } + + /** + * @returns string + */ + public function getErrorString() + { + } + + /** + * @returns boolean + */ + public function isFatal() + { + } + + /** + * @returns boolean + */ + public function isRetriable() + { + } + + /** + * @returns boolean + */ + public function transactionRequiresAbort() + { + } + } + + class Message + { + /** + * @var int + */ + public $err; + + /** + * @var string + */ + public $topic_name; + + /** + * @var int + */ + public $partition; + + /** + * @var string|null + */ + public $payload; + + /** + * @var int|null + */ + public $len; + + /** + * @var string|null + */ + public $key; + + /** + * @var int + */ + public $offset; + + /** + * @var int + */ + public $timestamp; + + /** + * @var array|null + */ + public $headers; + + /** + * @var string|null + */ + public $opaque; + + /** + * @return string + */ + public function errstr() + { + } + } + + class Metadata + { + /** + * @return \RdKafka\Metadata\Collection|\RdKafka\Metadata\Broker[] + */ + public function getBrokers() + { + } + + /** + * @return \RdKafka\Metadata\Collection|\RdKafka\Metadata\Topic[] + */ + public function getTopics() + { + } + + /** + * @return int + */ + public function getOrigBrokerId() + { + } + + /** + * @return string + */ + public function getOrigBrokerName() + { + } + } + + class Producer extends \RdKafka + { + /** + * @param Conf $conf + */ + public function __construct(Conf $conf = null) + { + } + + /** + * @param string $topic_name + * @param TopicConf $topic_conf + * + * @return ProducerTopic + */ + public function newTopic($topic_name, TopicConf $topic_conf = null) + { + } + + /** + * @param int $timeoutMs + * + * @return void + * @throws KafkaErrorException + */ + public function initTransactions(int $timeoutMs) + { + } + + /** + * @return void + * @throws KafkaErrorException + */ + public function beginTransaction() + { + } + + /** + * @param int $timeoutMs + * + * @return void + * @throws KafkaErrorException + */ + public function commitTransaction(int $timeoutMs) + { + } + + /** + * @param int $timeoutMs + * + * @return void + * @throws KafkaErrorException + */ + public function abortTransaction(int $timeoutMs) + { + } + } + + class ProducerTopic extends Topic + { + private function __construct() + { + } + + /** + * @param int $partition + * @param int $msgflags + * @param string $payload + * @param string $key + * + * @return void + * @throws Exception + */ + public function produce($partition, $msgflags, $payload, $key = null) + { + } + + /** + * @param int $partition + * @param int $msgflags + * @param string $payload + * @param string|null $key + * @param array|null $headers + * @param int $timestamp_ms + * + * @throws Exception + */ + public function producev($partition, $msgflags, $payload, $key = null, $headers = null, $timestamp_ms = null) + { + } + } + + class Queue + { + private function __construct() + { + } + + /** + * @param string $timeout_ms + * + * @return Message|null + */ + public function consume($timeout_ms) + { + } + } + + abstract class Topic + { + /** + * @return string + */ + public function getName() + { + } + } + + /** + * Configuration reference: https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md + */ + class TopicConf + { + /** + * @return array + */ + public function dump() + { + } + + /** + * @param string $name + * @param string $value + * + * @return void + */ + public function set($name, $value) + { + } + + /** + * @param int $partitioner + * + * @return void + */ + public function setPartitioner($partitioner) + { + } + } + + class TopicPartition + { + /** + * @param string $topic + * @param int $partition + * @param int $offset + */ + public function __construct($topic, $partition, $offset = null) + { + } + + /** + * @return int + */ + public function getOffset() + { + } + + /** + * @return int + */ + public function getPartition() + { + } + + /** + * @return string + */ + public function getTopic() + { + } + + /** + * @param int $offset + * + * @return void + */ + public function setOffset($offset) + { + } + + /** + * @param int $partition + * + * @return void + */ + public function setPartition($partition) + { + } + + /** + * @param string $topic_name + * + * @return void + */ + public function setTopic($topic_name) + { + } + } +} + +namespace RdKafka\Metadata { + + class Broker + { + /** + * @return int + */ + public function getId() + { + } + + /** + * @return string + */ + public function getHost() + { + } + + /** + * @return int + */ + public function getPort() + { + } + } + + class Collection implements \Iterator, \Countable + { + /** + * @return mixed + */ + public function current() + { + } + + /** + * @return void + */ + public function next() + { + } + + /** + * @return mixed + */ + public function key() + { + } + + /** + * @return boolean + */ + public function valid() + { + } + + /** + * @return void + */ + public function rewind() + { + } + + /** + * @return int + */ + public function count() + { + } + } + + class Partition + { + /** + * @return int + */ + public function getId() + { + } + + /** + * @return mixed + */ + public function getErr() + { + } + + /** + * @return mixed + */ + public function getLeader() + { + } + + /** + * @return mixed + */ + public function getReplicas() + { + } + + /** + * @return mixed + */ + public function getIsrs() + { + } + } + + class Topic + { + /** + * @return string + */ + public function getTopic() + { + } + + /** + * @return Partition[] + */ + public function getPartitions() + { + } + + /** + * @return mixed + */ + public function getErr() + { + } + } +} \ No newline at end of file From 86f503ab8273d834cce64ac311f0912df0fc6e0e Mon Sep 17 00:00:00 2001 From: robchett Date: Wed, 8 Nov 2023 10:40:53 +0000 Subject: [PATCH 039/357] Docblock psudo methods can be inherited via @mixin Fixes #3556 --- .../Call/Method/MissingMethodCallHandler.php | 6 +++++ tests/MixinAnnotationTest.php | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php index 6241127b579..14026d3d1ab 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php @@ -435,6 +435,12 @@ private static function findPseudoMethodAndClassStorages( } $ancestors = $static_class_storage->class_implements; + foreach ($static_class_storage->namedMixins as $namedObject) { + $type = $namedObject->value; + if ($type) { + $ancestors[$type] = true; + } + } foreach ($ancestors as $fq_class_name => $_) { $class_storage = $codebase->classlikes->getStorageFor($fq_class_name); diff --git a/tests/MixinAnnotationTest.php b/tests/MixinAnnotationTest.php index f4f0372cd13..807fc4a57a5 100644 --- a/tests/MixinAnnotationTest.php +++ b/tests/MixinAnnotationTest.php @@ -596,6 +596,28 @@ class FooModel extends Model {} '$g' => 'list', ], ], + 'mixinInheritMagicMethods' => [ + 'code' => 'active();', + 'assertions' => [ + '$c' => 'B', + ], + ], ]; } From ba17015dfb017434138e381a332dabb265b8ccc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Wed, 8 Nov 2023 11:22:47 +0100 Subject: [PATCH 040/357] Allow int keys when calling Redis methods In 5bfc0f960be71093c4b4dc754aa94c6142b44bd9, risky casting was invoked as a way to show there is an issue here. However, it is not always possible to use a string. For instance, there is no such thing as this in PHP: ["1" => "whatever"]. If you try to create such an array, you will obtain [1 => "whatever"] instead. Ironically, this was copied in jetbrains/phpstorm, which is used in PHPStan, which exhibited that false positive. See https://github.com/JetBrains/phpstorm-stubs/pull/1454 --- stubs/extensions/redis.phpstub | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stubs/extensions/redis.phpstub b/stubs/extensions/redis.phpstub index de8474d4742..d14673868cf 100644 --- a/stubs/extensions/redis.phpstub +++ b/stubs/extensions/redis.phpstub @@ -276,12 +276,12 @@ class Redis { public function move(string $key, int $index): bool {} /** - * @param array + * @param array */ public function mset($key_values): Redis|bool {} /** - * @param array + * @param array */ public function msetnx($key_values): Redis|bool {} From f4aef37ae561aff7c32eff1235600f6fbd031f1f Mon Sep 17 00:00:00 2001 From: robchett Date: Thu, 9 Nov 2023 09:24:46 +0000 Subject: [PATCH 041/357] A segment of progress was being output early as the startScanningFiles() method was not called before actually starting to scan files --- src/Psalm/Internal/Analyzer/ProjectAnalyzer.php | 6 +++--- src/Psalm/Progress/LongProgress.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index 2999499f382..a5eb0f00c9c 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -1050,6 +1050,9 @@ public function checkFile(string $file_path): void */ public function checkPaths(array $paths_to_check): void { + $this->progress->write($this->generatePHPVersionMessage()); + $this->progress->startScanningFiles(); + $this->config->visitPreloadedStubFiles($this->codebase, $this->progress); $this->visitAutoloadFiles(); @@ -1069,9 +1072,6 @@ public function checkPaths(array $paths_to_check): void $this->file_reference_provider->loadReferenceCache(); - $this->progress->write($this->generatePHPVersionMessage()); - $this->progress->startScanningFiles(); - $this->config->initializePlugins($this); diff --git a/src/Psalm/Progress/LongProgress.php b/src/Psalm/Progress/LongProgress.php index 5a58886c4b0..4f7044d7275 100644 --- a/src/Psalm/Progress/LongProgress.php +++ b/src/Psalm/Progress/LongProgress.php @@ -25,7 +25,7 @@ class LongProgress extends Progress protected bool $print_infos = false; - protected bool $fixed_size = true; + protected bool $fixed_size = false; public function __construct(bool $print_errors = true, bool $print_infos = true) { From 61f02d888990c698a3e83f5086def351be8288ca Mon Sep 17 00:00:00 2001 From: robchett Date: Thu, 9 Nov 2023 11:30:36 +0000 Subject: [PATCH 042/357] Detect magic method signature mismatch on interfaces Fixes #5786 --- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 40 +------------ .../Internal/Analyzer/InterfaceAnalyzer.php | 4 ++ .../Internal/Analyzer/MethodComparator.php | 47 ++++++++++++++++ tests/MagicMethodAnnotationTest.php | 56 +++++++++++++++++++ 4 files changed, 108 insertions(+), 39 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index 3842dce767d..57ba7d23cd5 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -258,8 +258,6 @@ public function analyze( IssueBuffer::maybeAdd($docblock_issue); } - $classlike_storage_provider = $codebase->classlike_storage_provider; - $parent_fq_class_name = $this->parent_fq_class_name; if ($class instanceof PhpParser\Node\Stmt\Class_ && $class->extends && $parent_fq_class_name) { @@ -626,43 +624,7 @@ public function analyze( } $pseudo_methods = $storage->pseudo_methods + $storage->pseudo_static_methods; - - foreach ($pseudo_methods as $pseudo_method_name => $pseudo_method_storage) { - $pseudo_method_id = new MethodIdentifier( - $this->fq_class_name, - $pseudo_method_name, - ); - - $overridden_method_ids = $codebase->methods->getOverriddenMethodIds($pseudo_method_id); - - if ($overridden_method_ids - && $pseudo_method_name !== '__construct' - && $pseudo_method_storage->location - ) { - foreach ($overridden_method_ids as $overridden_method_id) { - $parent_method_storage = $codebase->methods->getStorage($overridden_method_id); - - $overridden_fq_class_name = $overridden_method_id->fq_class_name; - - $parent_storage = $classlike_storage_provider->get($overridden_fq_class_name); - - MethodComparator::compare( - $codebase, - null, - $storage, - $parent_storage, - $pseudo_method_storage, - $parent_method_storage, - $this->fq_class_name, - $pseudo_method_storage->visibility ?: 0, - $storage->location ?: $pseudo_method_storage->location, - $storage->suppressed_issues, - true, - false, - ); - } - } - } + MethodComparator::comparePseudoMethods($pseudo_methods, $this->fq_class_name, $codebase, $storage); $event = new AfterClassLikeAnalysisEvent( $class, diff --git a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php index fc798e4433e..d7092e97b11 100644 --- a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php @@ -217,6 +217,10 @@ public function analyze(): void } } + $pseudo_methods = $class_storage->pseudo_methods + $class_storage->pseudo_static_methods; + + MethodComparator::comparePseudoMethods($pseudo_methods, $this->fq_class_name, $codebase, $class_storage); + $statements_analyzer = new StatementsAnalyzer($this, new NodeDataProvider()); $statements_analyzer->analyze($member_stmts, $interface_context, null, true); diff --git a/src/Psalm/Internal/Analyzer/MethodComparator.php b/src/Psalm/Internal/Analyzer/MethodComparator.php index a750940a27a..50f34758aaa 100644 --- a/src/Psalm/Internal/Analyzer/MethodComparator.php +++ b/src/Psalm/Internal/Analyzer/MethodComparator.php @@ -238,6 +238,53 @@ public static function compare( return null; } + /** + * @param array $pseudo_methods + */ + public static function comparePseudoMethods( + array $pseudo_methods, + string $fq_class_name, + Codebase $codebase, + ClassLikeStorage $class_storage, + ): void { + foreach ($pseudo_methods as $pseudo_method_name => $pseudo_method_storage) { + $pseudo_method_id = new MethodIdentifier( + $fq_class_name, + $pseudo_method_name, + ); + + $overridden_method_ids = $codebase->methods->getOverriddenMethodIds($pseudo_method_id); + + if ($overridden_method_ids + && $pseudo_method_name !== '__construct' + && $pseudo_method_storage->location + ) { + foreach ($overridden_method_ids as $overridden_method_id) { + $parent_method_storage = $codebase->methods->getStorage($overridden_method_id); + + $overridden_fq_class_name = $overridden_method_id->fq_class_name; + + $parent_storage = $codebase->classlike_storage_provider->get($overridden_fq_class_name); + + self::compare( + $codebase, + null, + $class_storage, + $parent_storage, + $pseudo_method_storage, + $parent_method_storage, + $fq_class_name, + $pseudo_method_storage->visibility ?: 0, + $class_storage->location ?: $pseudo_method_storage->location, + $class_storage->suppressed_issues, + true, + false, + ); + } + } + } + } + /** * @param string[] $suppressed_issues */ diff --git a/tests/MagicMethodAnnotationTest.php b/tests/MagicMethodAnnotationTest.php index 88f5a2a478a..ea2f6e8f729 100644 --- a/tests/MagicMethodAnnotationTest.php +++ b/tests/MagicMethodAnnotationTest.php @@ -1164,6 +1164,62 @@ public function baz(): Foo }', 'error_message' => 'UndefinedVariable', ], + 'MagicMethodReturnTypesCheckedForClasses' => [ + 'code' => ' 'ImplementedReturnTypeMismatch', + ], + 'MagicMethodParamTypesCheckedForClasses' => [ + 'code' => ' 'ImplementedParamTypeMismatch', + ], + 'MagicMethodReturnTypesCheckedForInterfaces' => [ + 'code' => ' 'ImplementedReturnTypeMismatch', + ], + 'MagicMethodParamTypesCheckedForInterfaces' => [ + 'code' => ' 'ImplementedParamTypeMismatch', + ], ]; } From 44f9440664554224e2c82f0e38e2eff71f7e38c7 Mon Sep 17 00:00:00 2001 From: robchett Date: Thu, 9 Nov 2023 11:32:58 +0000 Subject: [PATCH 043/357] Only inherit docblock param type if they type was not expanded fixes this issue: https://psalm.dev/r/edaea88e00 --- .../Statements/Expression/CallAnalyzer.php | 1 - src/Psalm/Internal/Codebase/Methods.php | 7 +++++ tests/DocblockInheritanceTest.php | 26 +++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index 666ccbc7d8a..4ac64a5ef65 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -311,7 +311,6 @@ public static function checkMethodArgs( $declaring_method_id = $class_storage->declaring_method_ids[$method_name]; $declaring_fq_class_name = $declaring_method_id->fq_class_name; - $declaring_method_name = $declaring_method_id->method_name; if ($declaring_fq_class_name !== $fq_class_name) { $declaring_class_storage = $codebase->classlike_storage_provider->get($declaring_fq_class_name); diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php index bdcf71befc6..68d0d2ca917 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -459,6 +459,13 @@ public function getMethodParams( foreach ($params as $i => $param) { if (isset($overridden_storage->params[$i]->type) && $overridden_storage->params[$i]->has_docblock_type + && ( + ! $param->type + || $param->type->equals( + $overridden_storage->params[$i]->signature_type + ?? $overridden_storage->params[$i]->type, + ) + ) ) { $params[$i] = clone $param; /** @var Union $params[$i]->type */ diff --git a/tests/DocblockInheritanceTest.php b/tests/DocblockInheritanceTest.php index 792c7972b86..84c50b6366b 100644 --- a/tests/DocblockInheritanceTest.php +++ b/tests/DocblockInheritanceTest.php @@ -149,6 +149,32 @@ function takesF(F $f) : B { return $f->map(); }', ], + 'inheritCorrectParamOnTypeChange' => [ + 'code' => '|int $className */ + public function a(array|int $className): int + { + return 0; + } + } + + class B extends A + { + public function a(array|int|bool $className): int + { + return 0; + } + } + + print_r((new A)->a(1)); + print_r((new B)->a(true)); + ', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.0', + ], ]; } From 68d6d9b70bbb8f9af1274e38afbaa22b4a9fe6f6 Mon Sep 17 00:00:00 2001 From: robchett Date: Thu, 9 Nov 2023 12:19:37 +0000 Subject: [PATCH 044/357] Trigger ImplementedParamTypeMismatch if concrete implementation of magic method does not match the magic method signature Fixes #3871 --- .../Internal/Analyzer/MethodComparator.php | 1 + .../Statements/Expression/CallAnalyzer.php | 6 +----- src/Psalm/Internal/Codebase/Methods.php | 8 ++++++-- src/Psalm/Internal/Codebase/Populator.php | 4 +++- .../Reflector/ClassLikeNodeScanner.php | 13 +++++++++---- .../Reflector/FunctionLikeNodeScanner.php | 1 + tests/MagicMethodAnnotationTest.php | 19 ++++++++++++++++++- 7 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/MethodComparator.php b/src/Psalm/Internal/Analyzer/MethodComparator.php index 50f34758aaa..534773a5cea 100644 --- a/src/Psalm/Internal/Analyzer/MethodComparator.php +++ b/src/Psalm/Internal/Analyzer/MethodComparator.php @@ -542,6 +542,7 @@ private static function compareMethodParams( if ($guide_classlike_storage->user_defined && $implementer_param->signature_type + && $guide_param->signature_type ) { self::compareMethodSignatureParams( $codebase, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index 4ac64a5ef65..a1bdb63638f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -318,11 +318,7 @@ public static function checkMethodArgs( $declaring_class_storage = $class_storage; } - if (!isset($declaring_class_storage->methods[$declaring_method_name])) { - throw new UnexpectedValueException('Storage should not be empty here'); - } - - $method_storage = $declaring_class_storage->methods[$declaring_method_name]; + $method_storage = $codebase->methods->getStorage($declaring_method_id); if ($declaring_class_storage->user_defined && !$method_storage->has_docblock_param_types diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php index 68d0d2ca917..34bc56181a1 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -1148,14 +1148,18 @@ public function getStorage(MethodIdentifier $method_id): MethodStorage } $method_name = $method_id->method_name; + $method_storage = $class_storage->methods[$method_name] + ?? $class_storage->pseudo_methods[$method_name] + ?? $class_storage->pseudo_static_methods[$method_name] + ?? null; - if (!isset($class_storage->methods[$method_name])) { + if (! $method_storage) { throw new UnexpectedValueException( '$storage should not be null for ' . $method_id, ); } - return $class_storage->methods[$method_name]; + return $method_storage; } /** @psalm-mutation-free */ diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php index d5ded4434a0..93754c6717a 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -367,7 +367,9 @@ private function populateOverriddenMethods( $declaring_method_name = $declaring_method_id->method_name; $declaring_class_storage = $declaring_class_storages[$declaring_class]; - $declaring_method_storage = $declaring_class_storage->methods[$declaring_method_name]; + $declaring_method_storage = $declaring_class_storage->methods[$declaring_method_name] + ?? $declaring_class_storage->pseudo_methods[$declaring_method_name] + ?? $declaring_class_storage->pseudo_static_methods[$declaring_method_name]; if (($declaring_method_storage->has_docblock_param_types || $declaring_method_storage->has_docblock_return_type) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index ecc0b53fb39..d570b029749 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -623,11 +623,16 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool $storage->pseudo_static_methods[$lc_method_name] = $pseudo_method_storage; } else { $storage->pseudo_methods[$lc_method_name] = $pseudo_method_storage; - $storage->declaring_pseudo_method_ids[$lc_method_name] = new MethodIdentifier( - $fq_classlike_name, - $lc_method_name, - ); } + $method_identifier = new MethodIdentifier( + $fq_classlike_name, + $lc_method_name, + ); + $storage->inheritable_method_ids[$lc_method_name] = $method_identifier; + if (!isset($storage->overridden_method_ids[$lc_method_name])) { + $storage->overridden_method_ids[$lc_method_name] = []; + } + $storage->declaring_pseudo_method_ids[$lc_method_name] = $method_identifier; } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index b2bc3a4f6af..850051356a2 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -929,6 +929,7 @@ private function createStorageForFunctionLike( $storage->is_static = $stmt->isStatic(); $storage->final = $this->classlike_storage && $this->classlike_storage->final; $storage->final_from_docblock = $this->classlike_storage && $this->classlike_storage->final_from_docblock; + $storage->visibility = ClassLikeAnalyzer::VISIBILITY_PUBLIC; } elseif ($stmt instanceof PhpParser\Node\Stmt\Function_) { $cased_function_id = ($this->aliases->namespace ? $this->aliases->namespace . '\\' : '') . $stmt->name->name; diff --git a/tests/MagicMethodAnnotationTest.php b/tests/MagicMethodAnnotationTest.php index ea2f6e8f729..2feeff3d4c4 100644 --- a/tests/MagicMethodAnnotationTest.php +++ b/tests/MagicMethodAnnotationTest.php @@ -824,7 +824,7 @@ function consumeInt(int $i): void {} 'callUsingParent' => [ 'code' => ' 'ImplementedParamTypeMismatch', ], + 'MagicMethodMadeConcreteChecksParams' => [ + 'code' => ' 'ImplementedParamTypeMismatch', + ], ]; } From 975d59032bbb4862857e68cd7db1930ee9902634 Mon Sep 17 00:00:00 2001 From: robchett Date: Thu, 9 Nov 2023 13:56:22 +0000 Subject: [PATCH 045/357] Don't inherit psuedo methods from parent if a concrete implementation exists Fixes #4546 --- src/Psalm/Internal/Codebase/Populator.php | 12 ++++++++++-- tests/MagicMethodAnnotationTest.php | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php index 93754c6717a..37cd50086b1 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -564,8 +564,16 @@ private function populateDataFromParentClass( $parent_storage->dependent_classlikes[strtolower($storage->name)] = true; - $storage->pseudo_methods += $parent_storage->pseudo_methods; - $storage->declaring_pseudo_method_ids += $parent_storage->declaring_pseudo_method_ids; + foreach ($parent_storage->pseudo_methods as $method_name => $pseudo_method) { + if (!isset($storage->methods[$method_name])) { + $storage->pseudo_methods[$method_name] = $pseudo_method; + } + } + foreach ($parent_storage->declaring_pseudo_method_ids as $method_name => $pseudo_method_id) { + if (!isset($storage->methods[$method_name])) { + $storage->declaring_pseudo_method_ids[$method_name] = $pseudo_method_id; + }; + } } private function populateInterfaceData( diff --git a/tests/MagicMethodAnnotationTest.php b/tests/MagicMethodAnnotationTest.php index 2feeff3d4c4..e6737ab0d78 100644 --- a/tests/MagicMethodAnnotationTest.php +++ b/tests/MagicMethodAnnotationTest.php @@ -949,6 +949,21 @@ class C {} //C::array(); PHP, ], + 'DoubleInheritedDontComplain' => [ + 'code' => ' [], + 'ignored_issues' => ['ParamNameMismatch'], + ], ]; } From ac465067e31a31366fa4cf6f77f0ba47a5ca7327 Mon Sep 17 00:00:00 2001 From: robchett Date: Thu, 9 Nov 2023 15:26:38 +0000 Subject: [PATCH 046/357] Warn if @method annotation contradicts concrete function Fixes #5990 --- .../Internal/Analyzer/MethodComparator.php | 28 +++++++++++++++++++ tests/MethodSignatureTest.php | 22 +++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/Psalm/Internal/Analyzer/MethodComparator.php b/src/Psalm/Internal/Analyzer/MethodComparator.php index 534773a5cea..b720f1769b2 100644 --- a/src/Psalm/Internal/Analyzer/MethodComparator.php +++ b/src/Psalm/Internal/Analyzer/MethodComparator.php @@ -23,6 +23,8 @@ use Psalm\Issue\LessSpecificImplementedReturnType; use Psalm\Issue\MethodSignatureMismatch; use Psalm\Issue\MethodSignatureMustProvideReturnType; +use Psalm\Issue\MismatchingDocblockParamType; +use Psalm\Issue\MismatchingDocblockReturnType; use Psalm\Issue\MissingImmutableAnnotation; use Psalm\Issue\MoreSpecificImplementedParamType; use Psalm\Issue\OverriddenMethodAccess; @@ -254,6 +256,9 @@ public static function comparePseudoMethods( ); $overridden_method_ids = $codebase->methods->getOverriddenMethodIds($pseudo_method_id); + if (isset($class_storage->methods[$pseudo_method_id->method_name])) { + $overridden_method_ids[$class_storage->name] = $pseudo_method_id; + } if ($overridden_method_ids && $pseudo_method_name !== '__construct' @@ -871,6 +876,18 @@ private static function compareMethodDocblockParams( ), $suppressed_issues + $implementer_classlike_storage->suppressed_issues, ); + } elseif ($guide_class_name == $implementer_called_class_name) { + IssueBuffer::maybeAdd( + new MismatchingDocblockParamType( + 'Argument ' . ($i + 1) . ' of ' . $cased_implementer_method_id + . ' has wrong type \'' . + $implementer_method_storage_param_type->getId() . '\' in @method annotation, expecting \'' . + $guide_method_storage_param_type->getId() . '\'', + $implementer_method_storage->params[$i]->location + ?: $code_location, + ), + $suppressed_issues + $implementer_classlike_storage->suppressed_issues, + ); } else { IssueBuffer::maybeAdd( new ImplementedParamTypeMismatch( @@ -1092,6 +1109,17 @@ private static function compareMethodDocblockReturnTypes( ), $suppressed_issues + $implementer_classlike_storage->suppressed_issues, ); + } elseif ($guide_class_name == $implementer_called_class_name) { + IssueBuffer::maybeAdd( + new MismatchingDocblockReturnType( + 'The inherited return type \'' . $guide_method_storage_return_type->getId() + . '\' for ' . $cased_guide_method_id . ' is different to the corresponding ' + . '@method annotation \'' . $implementer_method_storage_return_type->getId() . '\'', + $implementer_method_storage->return_type_location + ?: $code_location, + ), + $suppressed_issues + $implementer_classlike_storage->suppressed_issues, + ); } else { IssueBuffer::maybeAdd( new ImplementedReturnTypeMismatch( diff --git a/tests/MethodSignatureTest.php b/tests/MethodSignatureTest.php index e60ddb4c627..84625e98a9b 100644 --- a/tests/MethodSignatureTest.php +++ b/tests/MethodSignatureTest.php @@ -1639,6 +1639,28 @@ public function foo(): int { ', 'error_message' => 'MethodSignatureMismatch', ], + 'methodAnnotationReturnMismatch' => [ + 'code' => ' 'MismatchingDocblockReturnType', + ], + 'methodAnnotationParamMismatch' => [ + 'code' => ' 'MismatchingDocblockParamType', + ], ]; } } From 80edd4185889c5ab342538c94a370bc690822129 Mon Sep 17 00:00:00 2001 From: robchett Date: Thu, 9 Nov 2023 15:57:42 +0000 Subject: [PATCH 047/357] Fix failing tests with invalid code --- tests/ArrayAssignmentTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index d7651949d58..8392192fb4d 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -1045,13 +1045,13 @@ function foo(array $arr) : void { * @template-implements ArrayAccess */ class C implements ArrayAccess { - public function offsetExists(int $offset) : bool { return true; } + public function offsetExists($offset) : bool { return true; } public function offsetGet($offset) : string { return "";} - public function offsetSet(?int $offset, string $value) : void {} + public function offsetSet($offset, string $value) : void {} - public function offsetUnset(int $offset) : void { } + public function offsetUnset($offset) : void { } } $c = new C(); @@ -1964,13 +1964,13 @@ function foobar(): ?array * @template-implements ArrayAccess */ class C implements ArrayAccess { - public function offsetExists(int $offset) : bool { return true; } + public function offsetExists($offset) : bool { return true; } public function offsetGet($offset) : string { return "";} - public function offsetSet(int $offset, string $value) : void {} + public function offsetSet($offset, $value) : void {} - public function offsetUnset(int $offset) : void { } + public function offsetUnset($offset) : void { } } $c = new C(); From 84ed631a9f10fca6317daabbadc3dd05bd748ec0 Mon Sep 17 00:00:00 2001 From: robchett Date: Thu, 9 Nov 2023 16:18:36 +0000 Subject: [PATCH 048/357] Correct test min php version --- tests/MethodSignatureTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/MethodSignatureTest.php b/tests/MethodSignatureTest.php index 84625e98a9b..18609a2ccb9 100644 --- a/tests/MethodSignatureTest.php +++ b/tests/MethodSignatureTest.php @@ -926,6 +926,9 @@ final class B implements I { public function a(mixed $a): void {} }', + 'assertions' => [], + 'ignored_errors' => [], + 'php_version' => '8.0', ], 'doesNotRequireInterfaceDestructorsToHaveReturnType' => [ 'code' => ' Date: Sat, 13 May 2023 15:20:24 +0100 Subject: [PATCH 049/357] Remove MixedInferredReturnType as the related issue is more accuratly reported by MixedReturnStatement --- UPGRADING.md | 3 + config.xsd | 1 - docs/running_psalm/error_levels.md | 1 - docs/running_psalm/issues.md | 1 - .../issues/MixedInferredReturnType.md | 11 --- src/Psalm/Config.php | 1 - .../FunctionLike/ReturnTypeAnalyzer.php | 12 --- src/Psalm/Issue/MixedInferredReturnType.php | 13 --- tests/ArrayAssignmentTest.php | 3 - tests/Cache/CacheTest.php | 1 - tests/CallableTest.php | 2 +- tests/ClassTest.php | 2 - tests/DocumentationTest.php | 4 - tests/FunctionCallTest.php | 2 +- tests/JsonOutputTest.php | 4 +- tests/MagicPropertyTest.php | 2 +- tests/MethodCallTest.php | 4 +- tests/ReferenceConstraintTest.php | 1 - tests/ReportOutputTest.php | 87 +++---------------- tests/ReturnTypeTest.php | 18 +--- .../FunctionClassStringTemplateTest.php | 3 - tests/Template/FunctionTemplateTest.php | 2 - .../TypeReconciliation/ArrayKeyExistsTest.php | 6 +- .../AssignmentInConditionalTest.php | 1 - tests/TypeReconciliation/ConditionalTest.php | 3 - tests/TypeReconciliation/TypeAlgebraTest.php | 1 - tests/UnusedVariableTest.php | 2 - 27 files changed, 25 insertions(+), 166 deletions(-) delete mode 100644 docs/running_psalm/issues/MixedInferredReturnType.md delete mode 100644 src/Psalm/Issue/MixedInferredReturnType.php diff --git a/UPGRADING.md b/UPGRADING.md index 2bf88c584a4..9b3665d4dcf 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -12,8 +12,11 @@ - [BC] The only optional boolean parameter of `TKeyedArray::getGenericArrayType` was removed, and was replaced with a string parameter with a different meaning. - [BC] The `TDependentListKey` type was removed and replaced with an optional property of the `TIntRange` type. +- - [BC] `TCallableArray` and `TCallableList` removed and replaced with `TCallableKeyedArray`. +- [BC] Class `Psalm\Issue\MixedInferredReturnType` was removed + - [BC] Value of constant `Psalm\Type\TaintKindGroup::ALL_INPUT` changed to reflect new `TaintKind::INPUT_SLEEP` and `TaintKind::INPUT_XPATH` have been added. Accordingly, default values for `$taint` parameters of `Psalm\Codebase::addTaintSource()` and `Psalm\Codebase::addTaintSink()` have been changed as well. - [BC] Property `Config::$shepherd_host` was replaced with `Config::$shepherd_endpoint` diff --git a/config.xsd b/config.xsd index 88d7b527fe4..6a6d182dca3 100644 --- a/config.xsd +++ b/config.xsd @@ -334,7 +334,6 @@ - diff --git a/docs/running_psalm/error_levels.md b/docs/running_psalm/error_levels.md index 9bb001277c3..2d9c35ced37 100644 --- a/docs/running_psalm/error_levels.md +++ b/docs/running_psalm/error_levels.md @@ -262,7 +262,6 @@ Level 5 and above allows a more non-verifiable code, and higher levels are even - [MixedAssignment](issues/MixedAssignment.md) - [MixedClone](issues/MixedClone.md) - [MixedFunctionCall](issues/MixedFunctionCall.md) - - [MixedInferredReturnType](issues/MixedInferredReturnType.md) - [MixedMethodCall](issues/MixedMethodCall.md) - [MixedOperand](issues/MixedOperand.md) - [MixedPropertyAssignment](issues/MixedPropertyAssignment.md) diff --git a/docs/running_psalm/issues.md b/docs/running_psalm/issues.md index b9e3d8fe25f..95f3839593b 100644 --- a/docs/running_psalm/issues.md +++ b/docs/running_psalm/issues.md @@ -134,7 +134,6 @@ - [MixedAssignment](issues/MixedAssignment.md) - [MixedClone](issues/MixedClone.md) - [MixedFunctionCall](issues/MixedFunctionCall.md) - - [MixedInferredReturnType](issues/MixedInferredReturnType.md) - [MixedMethodCall](issues/MixedMethodCall.md) - [MixedOperand](issues/MixedOperand.md) - [MixedPropertyAssignment](issues/MixedPropertyAssignment.md) diff --git a/docs/running_psalm/issues/MixedInferredReturnType.md b/docs/running_psalm/issues/MixedInferredReturnType.md deleted file mode 100644 index 0ca57b91255..00000000000 --- a/docs/running_psalm/issues/MixedInferredReturnType.md +++ /dev/null @@ -1,11 +0,0 @@ -# MixedInferredReturnType - -Emitted when Psalm cannot determine a function's return type - -```php -hasMixed()) { - if (IssueBuffer::accepts( - new MixedInferredReturnType( - 'Could not verify return type \'' . $declared_return_type . '\' for ' . - $cased_method_id, - $return_type_location, - ), - $suppressed_issues, - )) { - return false; - } - return null; } diff --git a/src/Psalm/Issue/MixedInferredReturnType.php b/src/Psalm/Issue/MixedInferredReturnType.php deleted file mode 100644 index b3943899f12..00000000000 --- a/src/Psalm/Issue/MixedInferredReturnType.php +++ /dev/null @@ -1,13 +0,0 @@ - [ 'code' => ' [ "UndefinedThisPropertyFetch: Instance property A::\$foo is not defined", "MixedReturnStatement: Could not infer a return type", - "MixedInferredReturnType: Could not verify return type 'string' for A::bar", ], ], ], diff --git a/tests/CallableTest.php b/tests/CallableTest.php index 4dc185ca6d2..5291961c975 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -1917,7 +1917,7 @@ public function bar($argOne, $argTwo) } }', 'error_message' => 'InvalidFunctionCall', - 'ignored_issues' => ['UndefinedClass', 'MixedInferredReturnType'], + 'ignored_issues' => ['UndefinedClass'], ], 'undefinedCallableMethodFullString' => [ 'code' => ' [], 'ignored_issues' => [ 'UndefinedClass', - 'MixedInferredReturnType', 'InvalidArgument', ], ], @@ -356,7 +355,6 @@ function foo() : D { 'assertions' => [], 'ignored_issues' => [ 'UndefinedClass', - 'MixedInferredReturnType', 'InvalidArgument', ], ], diff --git a/tests/DocumentationTest.php b/tests/DocumentationTest.php index a8d135e4a88..cee36ca3a30 100644 --- a/tests/DocumentationTest.php +++ b/tests/DocumentationTest.php @@ -290,10 +290,6 @@ public function providerInvalidCodeParse(): array $ignored_issues = ['InvalidReturnStatement']; break; - case 'MixedInferredReturnType': - $ignored_issues = ['MixedReturnStatement']; - break; - case 'MixedStringOffsetAssignment': $ignored_issues = ['MixedAssignment']; break; diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index d012e72fa9e..a9599e5e9e9 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -917,7 +917,7 @@ function portismaybeint(string $s) : ? int { '$porta' => 'false|int|null', '$porte' => 'false|int|null', ], - 'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'], + 'ignored_issues' => ['MixedReturnStatement'], ], 'parseUrlComponent' => [ 'code' => ' 5, + 'error_count' => 4, 'message' => 'Cannot find referenced variable $b', 'line' => 3, 'error' => '$b', @@ -100,7 +100,7 @@ function fooFoo(int $a): int { function fooFoo(Badger\Bodger $a): Badger\Bodger { return $a; }', - 'error_count' => 3, + 'error_count' => 2, 'message' => 'Class, interface or enum named Badger\\Bodger does not exist', 'line' => 2, 'error' => 'Badger\\Bodger', diff --git a/tests/MagicPropertyTest.php b/tests/MagicPropertyTest.php index bd0f03fe6ca..abb03aed1a0 100644 --- a/tests/MagicPropertyTest.php +++ b/tests/MagicPropertyTest.php @@ -398,7 +398,7 @@ public function __get(string $name) : string { } }', 'assertions' => [], - 'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'], + 'ignored_issues' => ['MixedReturnStatement'], ], 'overrideInheritedProperty' => [ 'code' => ' [], - 'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'], + 'ignored_issues' => ['MixedReturnStatement'], 'php_version' => '8.0', ], 'nullsafeShortCircuit' => [ @@ -1342,7 +1342,7 @@ public function returns_nullable_class() { } }', 'error_message' => 'LessSpecificReturnStatement', - 'ignored_issues' => ['MixedInferredReturnType', 'MixedReturnStatement', 'MixedMethodCall'], + 'ignored_issues' => ['MixedReturnStatement', 'MixedMethodCall'], ], 'undefinedVariableStaticCall' => [ 'code' => ' null, 'other_references' => null, ], - [ - 'severity' => 'error', - 'line_from' => 2, - 'line_to' => 2, - 'type' => 'MixedInferredReturnType', - 'message' => 'Could not verify return type \'null|string\' for psalmCanVerify', - 'file_name' => 'somefile.php', - 'file_path' => 'somefile.php', - 'snippet' => 'function psalmCanVerify(int $your_code): ?string {', - 'selected_text' => '?string', - 'from' => 47, - 'to' => 54, - 'snippet_from' => 6, - 'snippet_to' => 56, - 'column_from' => 42, - 'column_to' => 49, - 'error_level' => 1, - 'shortcode' => 47, - 'link' => 'https://psalm.dev/047', - 'taint_trace' => null, - 'other_references' => null, - ], [ 'severity' => 'error', 'line_from' => 8, @@ -854,7 +832,7 @@ public function testFilteredJsonReportIsStillArray(): void ]; $report_options = ProjectAnalyzer::getFileReportOptions([__DIR__ . '/test-report.json'])[0]; - $fixable_issue_counts = ['MixedInferredReturnType' => 1]; + $fixable_issue_counts = []; $report = new JsonReport( $issues_data, @@ -902,22 +880,6 @@ public function testSonarqubeReport(): void 'type' => 'CODE_SMELL', 'severity' => 'CRITICAL', ], - [ - 'engineId' => 'Psalm', - 'ruleId' => 'MixedInferredReturnType', - 'primaryLocation' => [ - 'message' => 'Could not verify return type \'null|string\' for psalmCanVerify', - 'filePath' => 'somefile.php', - 'textRange' => [ - 'startLine' => 2, - 'endLine' => 2, - 'startColumn' => 41, - 'endColumn' => 48, - ], - ], - 'type' => 'CODE_SMELL', - 'severity' => 'CRITICAL', - ], [ 'engineId' => 'Psalm', 'ruleId' => 'UndefinedConstant', @@ -972,7 +934,6 @@ public function testEmacsReport(): void <<<'EOF' somefile.php:3:10:error - UndefinedVariable: Cannot find referenced variable $as_you_____type (see https://psalm.dev/024) somefile.php:3:10:error - MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138) - somefile.php:2:42:error - MixedInferredReturnType: Could not verify return type 'null|string' for psalmCanVerify (see https://psalm.dev/047) somefile.php:8:6:error - UndefinedConstant: Const CHANGE_ME is not defined (see https://psalm.dev/020) somefile.php:17:6:warning - PossiblyUndefinedGlobalVariable: Possibly undefined global variable $a, first seen on line 11 (see https://psalm.dev/126) @@ -991,7 +952,6 @@ public function testPylintReport(): void <<<'EOF' somefile.php:3: [E0001] UndefinedVariable: Cannot find referenced variable $as_you_____type (column 10) somefile.php:3: [E0001] MixedReturnStatement: Could not infer a return type (column 10) - somefile.php:2: [E0001] MixedInferredReturnType: Could not verify return type 'null|string' for psalmCanVerify (column 42) somefile.php:8: [E0001] UndefinedConstant: Const CHANGE_ME is not defined (column 6) somefile.php:17: [W0001] PossiblyUndefinedGlobalVariable: Possibly undefined global variable $a, first seen on line 11 (column 6) @@ -1015,9 +975,6 @@ public function testConsoleReport(): void ERROR: MixedReturnStatement - somefile.php:3:10 - Could not infer a return type (see https://psalm.dev/138) return $as_you_____type; - ERROR: MixedInferredReturnType - somefile.php:2:42 - Could not verify return type 'null|string' for psalmCanVerify (see https://psalm.dev/047) - function psalmCanVerify(int $your_code): ?string { - ERROR: UndefinedConstant - somefile.php:8:6 - Const CHANGE_ME is not defined (see https://psalm.dev/020) echo CHANGE_ME; @@ -1046,9 +1003,6 @@ public function testConsoleReportNoInfo(): void ERROR: MixedReturnStatement - somefile.php:3:10 - Could not infer a return type (see https://psalm.dev/138) return $as_you_____type; - ERROR: MixedInferredReturnType - somefile.php:2:42 - Could not verify return type 'null|string' for psalmCanVerify (see https://psalm.dev/047) - function psalmCanVerify(int $your_code): ?string { - ERROR: UndefinedConstant - somefile.php:8:6 - Const CHANGE_ME is not defined (see https://psalm.dev/020) echo CHANGE_ME; @@ -1074,9 +1028,6 @@ public function testConsoleReportNoSnippet(): void ERROR: MixedReturnStatement - somefile.php:3:10 - Could not infer a return type (see https://psalm.dev/138) - ERROR: MixedInferredReturnType - somefile.php:2:42 - Could not verify return type 'null|string' for psalmCanVerify (see https://psalm.dev/047) - - ERROR: UndefinedConstant - somefile.php:8:6 - Const CHANGE_ME is not defined (see https://psalm.dev/020) @@ -1135,15 +1086,14 @@ public function testCompactReport(): void <<<'EOF' FILE: somefile.php - +----------+------+---------------------------------+---------------------------------------------------------------+ - | SEVERITY | LINE | ISSUE | DESCRIPTION | - +----------+------+---------------------------------+---------------------------------------------------------------+ - | ERROR | 3 | UndefinedVariable | Cannot find referenced variable $as_you_____type | - | ERROR | 3 | MixedReturnStatement | Could not infer a return type | - | ERROR | 2 | MixedInferredReturnType | Could not verify return type 'null|string' for psalmCanVerify | - | ERROR | 8 | UndefinedConstant | Const CHANGE_ME is not defined | - | INFO | 17 | PossiblyUndefinedGlobalVariable | Possibly undefined global variable $a, first seen on line 11 | - +----------+------+---------------------------------+---------------------------------------------------------------+ + +----------+------+---------------------------------+--------------------------------------------------------------+ + | SEVERITY | LINE | ISSUE | DESCRIPTION | + +----------+------+---------------------------------+--------------------------------------------------------------+ + | ERROR | 3 | UndefinedVariable | Cannot find referenced variable $as_you_____type | + | ERROR | 3 | MixedReturnStatement | Could not infer a return type | + | ERROR | 8 | UndefinedConstant | Const CHANGE_ME is not defined | + | INFO | 17 | PossiblyUndefinedGlobalVariable | Possibly undefined global variable $a, first seen on line 11 | + +----------+------+---------------------------------+--------------------------------------------------------------+ EOF, $this->toUnixLineEndings(IssueBuffer::getOutput(IssueBuffer::getIssuesData(), $compact_report_options)), @@ -1166,9 +1116,6 @@ public function testCheckstyleReport(): void - - - @@ -1199,8 +1146,8 @@ public function testJunitReport(): void $this->assertSame( <<<'EOF' - - + + message: Cannot find referenced variable $as_you_____type type: UndefinedVariable @@ -1219,16 +1166,6 @@ public function testJunitReport(): void line: 3 column_from: 10 column_to: 26 - - - - message: Could not verify return type 'null|string' for psalmCanVerify - type: MixedInferredReturnType - snippet: function psalmCanVerify(int $your_code): ?string { - selected_text: ?string - line: 2 - column_from: 42 - column_to: 49 @@ -1283,7 +1220,6 @@ public function testGithubActionsOutput(): void $expected_output = <<<'EOF' ::error file=somefile.php,line=3,col=10,title=UndefinedVariable::somefile.php:3:10: UndefinedVariable: Cannot find referenced variable $as_you_____type (see https://psalm.dev/024) ::error file=somefile.php,line=3,col=10,title=MixedReturnStatement::somefile.php:3:10: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138) - ::error file=somefile.php,line=2,col=42,title=MixedInferredReturnType::somefile.php:2:42: MixedInferredReturnType: Could not verify return type 'null|string' for psalmCanVerify (see https://psalm.dev/047) ::error file=somefile.php,line=8,col=6,title=UndefinedConstant::somefile.php:8:6: UndefinedConstant: Const CHANGE_ME is not defined (see https://psalm.dev/020) ::warning file=somefile.php,line=17,col=6,title=PossiblyUndefinedGlobalVariable::somefile.php:17:6: PossiblyUndefinedGlobalVariable: Possibly undefined global variable $a, first seen on line 11 (see https://psalm.dev/126) @@ -1301,7 +1237,6 @@ public function testCountOutput(): void $report_options = new ReportOptions(); $report_options->format = Report::TYPE_COUNT; $expected_output = <<<'EOF' - MixedInferredReturnType: 1 MixedReturnStatement: 1 PossiblyUndefinedGlobalVariable: 1 UndefinedConstant: 1 diff --git a/tests/ReturnTypeTest.php b/tests/ReturnTypeTest.php index 41d93d12453..0cfc5f52c89 100644 --- a/tests/ReturnTypeTest.php +++ b/tests/ReturnTypeTest.php @@ -1380,14 +1380,6 @@ function fooFoo() { }', 'error_message' => 'MissingReturnType', ], - 'mixedInferredReturnType' => [ - 'code' => ' 'MixedInferredReturnType', - ], 'mixedInferredReturnStatement' => [ 'code' => ' 'MixedReturnStatement', ], - 'invalidReturnTypeClass' => [ - 'code' => ' 'UndefinedClass', - 'ignored_issues' => ['MixedInferredReturnType'], - ], 'invalidClassOnCall' => [ 'code' => 'bar();', 'error_message' => 'UndefinedClass', - 'ignored_issues' => ['MixedInferredReturnType', 'MixedReturnStatement'], + 'ignored_issues' => ['MixedReturnStatement'], ], 'returnArrayOfNullableInvalid' => [ 'code' => ' $className * @psalm-return RequestedType&MockObject - * @psalm-suppress MixedInferredReturnType * @psalm-suppress MixedReturnStatement */ function mockHelper(string $className) @@ -444,7 +443,6 @@ public function checkExpectations() : void * @psalm-template RequestedType * @psalm-param class-string $className * @psalm-return RequestedType&MockObject - * @psalm-suppress MixedInferredReturnType * @psalm-suppress MixedReturnStatement */ function mockHelper(string $className) @@ -482,7 +480,6 @@ public function checkExpectations() : void * @psalm-template RequestedType * @psalm-param class-string $className * @psalm-return MockObject&RequestedType - * @psalm-suppress MixedInferredReturnType * @psalm-suppress MixedReturnStatement */ function mockHelper(string $className) diff --git a/tests/Template/FunctionTemplateTest.php b/tests/Template/FunctionTemplateTest.php index 64210ed4bbc..efb5123e0c3 100644 --- a/tests/Template/FunctionTemplateTest.php +++ b/tests/Template/FunctionTemplateTest.php @@ -1336,7 +1336,6 @@ function foo(Closure $fn, $arg): void { * @param E $e * @param mixed $d * @return ?E - * @psalm-suppress MixedInferredReturnType */ function reduce_values($e, $d) { if (rand(0, 1)) { @@ -1359,7 +1358,6 @@ function reduce_values($e, $d) { * @param E $e * @param mixed $d * @return ?E - * @psalm-suppress MixedInferredReturnType */ function reduce_values($e, $d) { diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php index 4c96c6783f9..6ed17a19da3 100644 --- a/tests/TypeReconciliation/ArrayKeyExistsTest.php +++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php @@ -79,7 +79,7 @@ public function bar(string $key): bool { } }', 'assertions' => [], - 'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'], + 'ignored_issues' => ['MixedReturnStatement'], ], 'assertSelfClassConstantOffsetsInFunction' => [ 'code' => ' [], - 'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'], + 'ignored_issues' => ['MixedReturnStatement'], ], 'assertNamedClassConstantOffsetsInFunction' => [ 'code' => ' [], - 'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'], + 'ignored_issues' => ['MixedReturnStatement'], ], 'possiblyUndefinedArrayAccessWithArrayKeyExists' => [ 'code' => ' [ 'code' => ' ' [], - 'ignored_issues' => ['MixedInferredReturnType'], ], 'grandParentInstanceofConfusion' => [ 'code' => ' [ 'code' => ' ' Date: Thu, 9 Nov 2023 16:44:53 +0000 Subject: [PATCH 050/357] Allow type aliases for static variables Fixes #3837 --- src/Psalm/Internal/Analyzer/CommentAnalyzer.php | 5 +++++ tests/AnnotationTest.php | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php index 7428f91f3ae..ea50398ba48 100644 --- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php @@ -432,6 +432,10 @@ public static function getVarComments( $var_comments = []; try { + $file_path = $statements_analyzer->getRootFilePath(); + $file_storage_provider = $codebase->file_storage_provider; + $file_storage = $file_storage_provider->get($file_path); + $var_comments = $codebase->config->disable_var_parsing ? [] : self::arrayToDocblocks( @@ -440,6 +444,7 @@ public static function getVarComments( $statements_analyzer->getSource(), $statements_analyzer->getSource()->getAliases(), $statements_analyzer->getSource()->getTemplateTypeMap(), + $file_storage->type_aliases, ); } catch (IncorrectDocblockException $e) { IssueBuffer::maybeAdd( diff --git a/tests/AnnotationTest.php b/tests/AnnotationTest.php index defd951aa28..cdd002de207 100644 --- a/tests/AnnotationTest.php +++ b/tests/AnnotationTest.php @@ -1013,6 +1013,21 @@ function getPerson_error(): array { return json_decode($json, true); }', ], + 'psalmTypeAnnotationForStaticVar' => [ + 'code' => ' [ 'code' => ' Date: Sat, 21 Oct 2023 13:52:22 +0200 Subject: [PATCH 051/357] Suppressing NoValue should not treat subsequent code as unevaluated Fix https://github.com/vimeo/psalm/issues/10302 --- .../Expression/AssignmentAnalyzer.php | 8 ++--- .../Expression/Call/ArgumentAnalyzer.php | 7 ++-- tests/UnusedCodeTest.php | 36 +++++++++++++++++-- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index debb27e87b7..72dbba84522 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -528,18 +528,18 @@ public static function analyze( } if ($context->vars_in_scope[$var_id]->isNever()) { - if (IssueBuffer::accepts( + if (!IssueBuffer::accepts( new NoValue( 'All possible types for this assignment were invalidated - This may be dead code', new CodeLocation($statements_analyzer->getSource(), $assign_var), ), $statements_analyzer->getSuppressedIssues(), )) { - return false; + // if the error is suppressed, do not treat it as never anymore + $context->vars_in_scope[$var_id] = Type::getMixed(); + $context->has_returned = false; } - $context->vars_in_scope[$var_id] = Type::getNever(); - $context->inside_assignment = $was_in_assignment; return $context->vars_in_scope[$var_id]; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index cd3e590b8b0..4f184b6ce49 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -806,13 +806,16 @@ public static function verifyType( } if ($input_type->isNever()) { - IssueBuffer::maybeAdd( + if (!IssueBuffer::accepts( new NoValue( 'All possible types for this argument were invalidated - This may be dead code', $arg_location, ), $statements_analyzer->getSuppressedIssues(), - ); + )) { + // if the error is suppressed, do not treat it as exited anymore + $context->has_returned = false; + } return null; } diff --git a/tests/UnusedCodeTest.php b/tests/UnusedCodeTest.php index ef570f8a6f0..b6857577c19 100644 --- a/tests/UnusedCodeTest.php +++ b/tests/UnusedCodeTest.php @@ -476,13 +476,13 @@ public static function get(): a { return new a; } } - + final class b { public function test(): a { return new a; } } - + function process(b $handler): a { if (\extension_loaded("fdsfdsfd")) { return $handler->test(); @@ -1323,6 +1323,38 @@ public function b(): void {} new A; PHP, ], + 'callNeverReturnsSuppressed' => [ + 'code' => ' [ + 'code' => ' Date: Mon, 13 Nov 2023 19:08:04 +0100 Subject: [PATCH 052/357] fix failing test caused by psalm-suppress as this should be invalidCodeParse test --- tests/IntRangeTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/IntRangeTest.php b/tests/IntRangeTest.php index c7c09ae2d92..9e7f3388413 100644 --- a/tests/IntRangeTest.php +++ b/tests/IntRangeTest.php @@ -273,22 +273,22 @@ function getInt(): int{return 0;} '$h===' => 'int<-4, 4>', '$i===' => 'int', '$j===' => 'int', - '$k===' => 'never', + '$k===' => 'mixed', '$l===' => 'int', '$m===' => 'int<0, max>', '$n===' => 'int', - '$o===' => 'never', + '$o===' => 'mixed', '$p===' => 'int', - '$q===' => 'never', + '$q===' => 'mixed', '$r===' => 'int<0, 2>', '$s===' => 'int<-2, 0>', - '$t===' => 'never', + '$t===' => 'mixed', '$u===' => 'int<-2, 0>', '$v===' => 'int<2, 0>', - '$w===' => 'never', + '$w===' => 'mixed', '$x===' => 'int<0, 2>', '$y===' => 'int<-2, 0>', - '$z===' => 'never', + '$z===' => 'mixed', '$aa===' => 'int<-2, 2>', '$ab===' => 'int<-2, 2>', ], From 88e3cda3b92a7daf87bc9bd3cca35421dd42b35e Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Mon, 13 Nov 2023 20:24:58 +0100 Subject: [PATCH 053/357] remove unused function, fix unusedVariable error after suppress --- .../Statements/Expression/AssignmentAnalyzer.php | 12 +++++++----- tests/UnusedCodeTest.php | 5 +---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index 72dbba84522..904d801fbf7 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -536,13 +536,15 @@ public static function analyze( $statements_analyzer->getSuppressedIssues(), )) { // if the error is suppressed, do not treat it as never anymore - $context->vars_in_scope[$var_id] = Type::getMixed(); + $new_mutable = $context->vars_in_scope[$var_id]->getBuilder()->addType(new TMixed); + $new_mutable->removeType('never'); + $context->vars_in_scope[$var_id] = $new_mutable->freeze(); $context->has_returned = false; - } - - $context->inside_assignment = $was_in_assignment; + } else { + $context->inside_assignment = $was_in_assignment; - return $context->vars_in_scope[$var_id]; + return $context->vars_in_scope[$var_id]; + } } if ($statements_analyzer->data_flow_graph) { diff --git a/tests/UnusedCodeTest.php b/tests/UnusedCodeTest.php index b6857577c19..947ff34de36 100644 --- a/tests/UnusedCodeTest.php +++ b/tests/UnusedCodeTest.php @@ -1332,10 +1332,6 @@ public function b(): void {} */ function foo() : void {} - function bar(mixed $s) : string { - return is_string($s) ? "hello" : "world"; - } - /** @psalm-suppress NoValue */ $a = foo(); print_r($a);', @@ -1349,6 +1345,7 @@ function bar(mixed $s) : string { */ function foo() : void {} + /** @psalm-suppress UnusedParam */ function bar(mixed $s) : void {} /** @psalm-suppress NoValue */ From b0adeb463da616006dfd843a817456a4a4fbb576 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Mon, 13 Nov 2023 20:37:48 +0100 Subject: [PATCH 054/357] fix test --- tests/UnusedCodeTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/UnusedCodeTest.php b/tests/UnusedCodeTest.php index 947ff34de36..32b85090d1b 100644 --- a/tests/UnusedCodeTest.php +++ b/tests/UnusedCodeTest.php @@ -1346,7 +1346,7 @@ function foo() : void {} function foo() : void {} /** @psalm-suppress UnusedParam */ - function bar(mixed $s) : void {} + function bar(string $s) : void {} /** @psalm-suppress NoValue */ bar(foo()); From a040ad122860ca3be46394433939e1d73ad75e64 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 16 Nov 2023 09:31:30 +0100 Subject: [PATCH 055/357] update decimal stubs from documented URL --- stubs/extensions/decimal.phpstub | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/stubs/extensions/decimal.phpstub b/stubs/extensions/decimal.phpstub index 90b329dab7f..57ffc0d282a 100644 --- a/stubs/extensions/decimal.phpstub +++ b/stubs/extensions/decimal.phpstub @@ -52,8 +52,9 @@ final class Decimal implements \JsonSerializable * * Initializes a new instance using a given value and minimum precision. * - * @param Decimal|string|int $value - * @param int $precision + * @phpstan-param Decimal|numeric-string|int $value + * @param Decimal|string|int $value + * @param int $precision * * @throws \BadMethodCallException if already constructed. * @throws \TypeError if the value is not a decimal, string, or integer. @@ -119,7 +120,8 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @param Decimal|string|int $value + * @phpstan-param Decimal|numeric-string|int $value + * @param Decimal|string|int $value * * @return Decimal the result of adding this decimal to the given value. * @@ -135,7 +137,8 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @param Decimal|string|int $value + * @phpstan-param Decimal|numeric-string|int $value + * @param Decimal|string|int $value * * @return Decimal the result of subtracting a given value from this decimal. * @@ -151,7 +154,8 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @param Decimal|string|int $value + * @phpstan-param Decimal|numeric-string|int $value + * @param Decimal|string|int $value * * @return Decimal the result of multiplying this decimal by the given value. * @@ -167,7 +171,8 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @param Decimal|string|int $value + * @phpstan-param Decimal|numeric-string|int $value + * @param Decimal|string|int $value * * @return Decimal the result of dividing this decimal by the given value. * @@ -187,7 +192,8 @@ final class Decimal implements \JsonSerializable * * @see Decimal::rem for the decimal remainder. * - * @param Decimal|string|int $value + * @phpstan-param Decimal|numeric-string|int $value + * @param Decimal|string|int $value * * @return Decimal the remainder after dividing the integer value of this * decimal by the integer value of the given value @@ -204,7 +210,8 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @param Decimal|string|int $value + * @phpstan-param Decimal|numeric-string|int $value + * @param Decimal|string|int $value * * @return Decimal the remainder after dividing this decimal by a given value. * @@ -222,7 +229,8 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @param Decimal|string|int $exponent The power to raise this decimal to. + * @phpstan-param Decimal|numeric-string|int $exponent The power to raise this decimal to. + * @param Decimal|string|int $exponent The power to raise this decimal to. * * @return Decimal the result of raising this decimal to a given power. * @@ -488,5 +496,5 @@ final class Decimal implements \JsonSerializable * * @return string */ - public function jsonSerialize() {} + public function jsonSerialize(): string {} } From 650b8fcd1b3fe6f76153c02bf58dbdabc90270e1 Mon Sep 17 00:00:00 2001 From: Ivan Sidorov Date: Fri, 17 Nov 2023 11:29:42 +0000 Subject: [PATCH 056/357] Tests of current behaviors in the auto-completion feature --- ...GetCompletionItemsForClassishThingTest.php | 539 ++++++++++++++++++ 1 file changed, 539 insertions(+) create mode 100644 tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php diff --git a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php new file mode 100644 index 00000000000..6c367b18998 --- /dev/null +++ b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php @@ -0,0 +1,539 @@ +file_provider = new FakeFileProvider(); + + $config = new TestConfig(); + + $providers = new Providers( + $this->file_provider, + new ParserInstanceCacheProvider(), + null, + null, + new FakeFileReferenceCacheProvider(), + new ProjectCacheProvider(), + ); + + $this->codebase = new Codebase($config, $providers); + + $this->project_analyzer = new ProjectAnalyzer( + $config, + $providers, + null, + [], + 1, + null, + $this->codebase, + ); + + $this->project_analyzer->setPhpVersion('7.3', 'tests'); + $this->project_analyzer->getCodebase()->store_node_types = true; + + $this->codebase->config->throw_exception = false; + } + + /** + * @return list + */ + protected function getCompletionLabels(string $content, string $class_name, string $gap): array + { + $this->addFile('somefile.php', $content); + + $this->analyzeFile('somefile.php', new Context()); + + $items = $this->codebase->getCompletionItemsForClassishThing($class_name, $gap, true); + + return array_map(fn($item) => $item->label, $items); + } + + /** + * @return iterable + */ + public function providerGaps(): iterable + { + return [ + 'object-gap' => ['->'], + 'static-gap' => ['::'], + ]; + } + + /** + * @dataProvider providerGaps + */ + public function testSimpleOnceClass(string $gap): void + { + $content = <<<'EOF' + getCompletionLabels($content, 'B\A', $gap); + + $expected_labels = [ + '->' => [ + 'magicObjProp1', + 'magicObjProp2', + + 'magicObjMethod', + 'magicStaticMethod', + + '$publicObjProp', + '$protectedObjProp', + '$privateObjProp', + + '$publicStaticProp', + '$protectedStaticProp', + '$privateStaticProp', + + 'publicObjMethod', + 'protectedObjMethod', + 'privateObjMethod', + + 'publicStaticMethod', + 'protectedStaticMethod', + 'privateStaticMethod', + ], + '::' => [ + 'magicObjProp1', + 'magicObjProp2', + 'magicStaticMethod', + + '$publicStaticProp', + '$protectedStaticProp', + '$privateStaticProp', + + 'publicStaticMethod', + 'protectedStaticMethod', + 'privateStaticMethod', + ], + ]; + + $this->assertEqualsCanonicalizing($expected_labels[$gap], $actual_labels); + } + + /** + * @dataProvider providerGaps + */ + public function testAbstractClass(string $gap): void + { + $content = <<<'EOF' + getCompletionLabels($content, 'B\A', $gap); + + $expected_labels = [ + '->' => [ + 'magicObjProp1', + 'magicObjProp2', + + 'magicObjMethod', + 'magicStaticMethod', + + '$publicObjProp', + '$protectedObjProp', + '$privateObjProp', + + '$publicStaticProp', + '$protectedStaticProp', + '$privateStaticProp', + + 'abstractPublicMethod', + 'abstractProtectedMethod', + + 'publicObjMethod', + 'protectedObjMethod', + 'privateObjMethod', + + 'publicStaticMethod', + 'protectedStaticMethod', + 'privateStaticMethod', + ], + '::' => [ + 'magicObjProp1', + 'magicObjProp2', + 'magicStaticMethod', + + '$publicStaticProp', + '$protectedStaticProp', + '$privateStaticProp', + + 'publicStaticMethod', + 'protectedStaticMethod', + 'privateStaticMethod', + ], + ]; + + $this->assertEqualsCanonicalizing($expected_labels[$gap], $actual_labels); + } + + /** + * @dataProvider providerGaps + */ + public function testUseTrait(string $gap): void + { + $content = <<<'EOF' + getCompletionLabels($content, 'B\A', $gap); + + $expected_labels = [ + '->' => [ + 'magicObjProp1', + 'magicObjProp2', + + 'magicObjMethod', + + '$publicObjProp', + '$protectedObjProp', + '$privateObjProp', + + '$publicStaticProp', + '$protectedStaticProp', + '$privateStaticProp', + ], + '::' => [ + 'magicObjProp1', + 'magicObjProp2', + + '$publicStaticProp', + '$protectedStaticProp', + '$privateStaticProp', + ], + ]; + + $this->assertEqualsCanonicalizing($expected_labels[$gap], $actual_labels); + } + + /** + * @dataProvider providerGaps + */ + public function testUseTraitWithAbstractClass(string $gap): void + { + $content = <<<'EOF' + getCompletionLabels($content, 'B\A', $gap); + + $expected_labels = [ + '->' => [ + 'magicObjProp1', + 'magicObjProp2', + + 'magicObjMethod', + + '$publicObjProp', + '$protectedObjProp', + '$privateObjProp', + + '$publicStaticProp', + '$protectedStaticProp', + '$privateStaticProp', + ], + '::' => [ + 'magicObjProp1', + 'magicObjProp2', + + '$publicStaticProp', + '$protectedStaticProp', + '$privateStaticProp', + ], + ]; + + $this->assertEqualsCanonicalizing($expected_labels[$gap], $actual_labels); + } + + /** + * @dataProvider providerGaps + */ + public function testClassWithExtends(string $gap): void + { + $content = <<<'EOF' + getCompletionLabels($content, 'B\A', $gap); + + $expected_labels = [ + '->' => [ + 'magicObjProp1', + 'magicObjProp2', + + 'magicObjMethod', + + '$publicObjProp', + '$protectedObjProp', + + '$publicStaticProp', + '$protectedStaticProp', + ], + '::' => [ + 'magicObjProp1', + 'magicObjProp2', + + '$publicStaticProp', + '$protectedStaticProp', + ], + ]; + + $this->assertEqualsCanonicalizing($expected_labels[$gap], $actual_labels); + } + + /** + * @dataProvider providerGaps + */ + public function testAstractClassWithInterface(string $gap): void + { + $content = <<<'EOF' + getCompletionLabels($content, 'B\A', $gap); + + $expected_labels = [ + '->' => [ + 'publicObjMethod', + 'protectedObjMethod', + ], + '::' => [], + ]; + + $this->assertEqualsCanonicalizing($expected_labels[$gap], $actual_labels); + } + + /** + * @dataProvider providerGaps + */ + public function testClassWithAnnotationMixin(string $gap): void + { + $content = <<<'EOF' + getCompletionLabels($content, 'B\A', $gap); + + $expected_labels = [ + '->' => [], + '::' => [], + ]; + + $this->assertEqualsCanonicalizing($expected_labels[$gap], $actual_labels); + } +} From 6f19440b0201be580955cf84d365e86e2d818ef9 Mon Sep 17 00:00:00 2001 From: Ivan Sidorov Date: Fri, 17 Nov 2023 11:41:53 +0000 Subject: [PATCH 057/357] Fix text-labels with type of property The labels for magic and declared properties must be equal. --- src/Psalm/Codebase.php | 2 +- ...GetCompletionItemsForClassishThingTest.php | 84 +++++++++---------- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 7ad8572dc44..b46ccd62218 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -1954,7 +1954,7 @@ public function getCompletionItemsForClassishThing( if ($property_storage->is_static || $gap === '->') { $completion_items[] = new CompletionItem( - '$' . $property_name, + $property_name, CompletionItemKind::PROPERTY, $property_storage->getInfo(), $property_storage->description, diff --git a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php index 6c367b18998..c21bd14a48d 100644 --- a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php +++ b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php @@ -128,13 +128,13 @@ private static function privateStaticMethod() {} 'magicObjMethod', 'magicStaticMethod', - '$publicObjProp', - '$protectedObjProp', - '$privateObjProp', + 'publicObjProp', + 'protectedObjProp', + 'privateObjProp', - '$publicStaticProp', - '$protectedStaticProp', - '$privateStaticProp', + 'publicStaticProp', + 'protectedStaticProp', + 'privateStaticProp', 'publicObjMethod', 'protectedObjMethod', @@ -149,9 +149,9 @@ private static function privateStaticMethod() {} 'magicObjProp2', 'magicStaticMethod', - '$publicStaticProp', - '$protectedStaticProp', - '$privateStaticProp', + 'publicStaticProp', + 'protectedStaticProp', + 'privateStaticProp', 'publicStaticMethod', 'protectedStaticMethod', @@ -209,13 +209,13 @@ private static function privateStaticMethod() {} 'magicObjMethod', 'magicStaticMethod', - '$publicObjProp', - '$protectedObjProp', - '$privateObjProp', + 'publicObjProp', + 'protectedObjProp', + 'privateObjProp', - '$publicStaticProp', - '$protectedStaticProp', - '$privateStaticProp', + 'publicStaticProp', + 'protectedStaticProp', + 'privateStaticProp', 'abstractPublicMethod', 'abstractProtectedMethod', @@ -233,9 +233,9 @@ private static function privateStaticMethod() {} 'magicObjProp2', 'magicStaticMethod', - '$publicStaticProp', - '$protectedStaticProp', - '$privateStaticProp', + 'publicStaticProp', + 'protectedStaticProp', + 'privateStaticProp', 'publicStaticMethod', 'protectedStaticMethod', @@ -296,21 +296,21 @@ class A { 'magicObjMethod', - '$publicObjProp', - '$protectedObjProp', - '$privateObjProp', + 'publicObjProp', + 'protectedObjProp', + 'privateObjProp', - '$publicStaticProp', - '$protectedStaticProp', - '$privateStaticProp', + 'publicStaticProp', + 'protectedStaticProp', + 'privateStaticProp', ], '::' => [ 'magicObjProp1', 'magicObjProp2', - '$publicStaticProp', - '$protectedStaticProp', - '$privateStaticProp', + 'publicStaticProp', + 'protectedStaticProp', + 'privateStaticProp', ], ]; @@ -367,21 +367,21 @@ abstract class A { 'magicObjMethod', - '$publicObjProp', - '$protectedObjProp', - '$privateObjProp', + 'publicObjProp', + 'protectedObjProp', + 'privateObjProp', - '$publicStaticProp', - '$protectedStaticProp', - '$privateStaticProp', + 'publicStaticProp', + 'protectedStaticProp', + 'privateStaticProp', ], '::' => [ 'magicObjProp1', 'magicObjProp2', - '$publicStaticProp', - '$protectedStaticProp', - '$privateStaticProp', + 'publicStaticProp', + 'protectedStaticProp', + 'privateStaticProp', ], ]; @@ -435,18 +435,18 @@ class A extends C { 'magicObjMethod', - '$publicObjProp', - '$protectedObjProp', + 'publicObjProp', + 'protectedObjProp', - '$publicStaticProp', - '$protectedStaticProp', + 'publicStaticProp', + 'protectedStaticProp', ], '::' => [ 'magicObjProp1', 'magicObjProp2', - '$publicStaticProp', - '$protectedStaticProp', + 'publicStaticProp', + 'protectedStaticProp', ], ]; From 1cfc343c43ee4c444d15669d7bc16a49bdf1a06b Mon Sep 17 00:00:00 2001 From: Ivan Sidorov Date: Wed, 15 Nov 2023 19:22:12 +0000 Subject: [PATCH 058/357] Ignore declared static properties with usage object gap Usage static properties in PHP is allow only by static gap. --- src/Psalm/Codebase.php | 2 +- ...GetCompletionItemsForClassishThingTest.php | 19 ------------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index b46ccd62218..13304c92ba5 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -1952,7 +1952,7 @@ public function getCompletionItemsForClassishThing( $declaring_class . '::$' . $property_name, ); - if ($property_storage->is_static || $gap === '->') { + if ($property_storage->is_static === ($gap === '::')) { $completion_items[] = new CompletionItem( $property_name, CompletionItemKind::PROPERTY, diff --git a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php index c21bd14a48d..51695c25c37 100644 --- a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php +++ b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php @@ -132,10 +132,6 @@ private static function privateStaticMethod() {} 'protectedObjProp', 'privateObjProp', - 'publicStaticProp', - 'protectedStaticProp', - 'privateStaticProp', - 'publicObjMethod', 'protectedObjMethod', 'privateObjMethod', @@ -212,10 +208,6 @@ private static function privateStaticMethod() {} 'publicObjProp', 'protectedObjProp', 'privateObjProp', - - 'publicStaticProp', - 'protectedStaticProp', - 'privateStaticProp', 'abstractPublicMethod', 'abstractProtectedMethod', @@ -299,10 +291,6 @@ class A { 'publicObjProp', 'protectedObjProp', 'privateObjProp', - - 'publicStaticProp', - 'protectedStaticProp', - 'privateStaticProp', ], '::' => [ 'magicObjProp1', @@ -370,10 +358,6 @@ abstract class A { 'publicObjProp', 'protectedObjProp', 'privateObjProp', - - 'publicStaticProp', - 'protectedStaticProp', - 'privateStaticProp', ], '::' => [ 'magicObjProp1', @@ -437,9 +421,6 @@ class A extends C { 'publicObjProp', 'protectedObjProp', - - 'publicStaticProp', - 'protectedStaticProp', ], '::' => [ 'magicObjProp1', From 715f6f51e1560edd082248513e1d1dabac52236b Mon Sep 17 00:00:00 2001 From: Ivan Sidorov Date: Tue, 14 Nov 2023 10:47:25 +0000 Subject: [PATCH 059/357] Ignore annotated magic properties with usage static gap In PHP is not exists methods for implementation magic static properties. So, these magic static properties must not be presented. --- src/Psalm/Codebase.php | 50 +++++++++---------- ...GetCompletionItemsForClassishThingTest.php | 13 ----- 2 files changed, 25 insertions(+), 38 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 13304c92ba5..4865129efc7 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -1918,35 +1918,35 @@ public function getCompletionItemsForClassishThing( } } - $pseudo_property_types = []; - foreach ($class_storage->pseudo_property_get_types as $property_name => $type) { - $pseudo_property_types[$property_name] = new CompletionItem( - str_replace('$', '', $property_name), - CompletionItemKind::PROPERTY, - $type->__toString(), - null, - '1', //sort text - str_replace('$', '', $property_name), - ($gap === '::' ? '$' : '') . + if ($gap === '->') { + $pseudo_property_types = []; + foreach ($class_storage->pseudo_property_get_types as $property_name => $type) { + $pseudo_property_types[$property_name] = new CompletionItem( str_replace('$', '', $property_name), - ); - } - - foreach ($class_storage->pseudo_property_set_types as $property_name => $type) { - $pseudo_property_types[$property_name] = new CompletionItem( - str_replace('$', '', $property_name), - CompletionItemKind::PROPERTY, - $type->__toString(), - null, - '1', - str_replace('$', '', $property_name), - ($gap === '::' ? '$' : '') . + CompletionItemKind::PROPERTY, + $type->__toString(), + null, + '1', //sort text str_replace('$', '', $property_name), - ); + str_replace('$', '', $property_name), + ); + } + + foreach ($class_storage->pseudo_property_set_types as $property_name => $type) { + $pseudo_property_types[$property_name] = new CompletionItem( + str_replace('$', '', $property_name), + CompletionItemKind::PROPERTY, + $type->__toString(), + null, + '1', + str_replace('$', '', $property_name), + str_replace('$', '', $property_name), + ); + } + + $completion_items = [...$completion_items, ...array_values($pseudo_property_types)]; } - $completion_items = [...$completion_items, ...array_values($pseudo_property_types)]; - foreach ($class_storage->declaring_property_ids as $property_name => $declaring_class) { $property_storage = $this->properties->getStorage( $declaring_class . '::$' . $property_name, diff --git a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php index 51695c25c37..3a8f56c7431 100644 --- a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php +++ b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php @@ -141,8 +141,6 @@ private static function privateStaticMethod() {} 'privateStaticMethod', ], '::' => [ - 'magicObjProp1', - 'magicObjProp2', 'magicStaticMethod', 'publicStaticProp', @@ -221,8 +219,6 @@ private static function privateStaticMethod() {} 'privateStaticMethod', ], '::' => [ - 'magicObjProp1', - 'magicObjProp2', 'magicStaticMethod', 'publicStaticProp', @@ -293,9 +289,6 @@ class A { 'privateObjProp', ], '::' => [ - 'magicObjProp1', - 'magicObjProp2', - 'publicStaticProp', 'protectedStaticProp', 'privateStaticProp', @@ -360,9 +353,6 @@ abstract class A { 'privateObjProp', ], '::' => [ - 'magicObjProp1', - 'magicObjProp2', - 'publicStaticProp', 'protectedStaticProp', 'privateStaticProp', @@ -423,9 +413,6 @@ class A extends C { 'protectedObjProp', ], '::' => [ - 'magicObjProp1', - 'magicObjProp2', - 'publicStaticProp', 'protectedStaticProp', ], From d533bb8c2e91654b6de0e4ee2bad0401ef0facd3 Mon Sep 17 00:00:00 2001 From: Ivan Sidorov Date: Wed, 15 Nov 2023 11:09:54 +0000 Subject: [PATCH 060/357] Ignore annotated magic static methods with usage object gap Magic methods annotated by methods `__call` and `__callStatic`. Magic static methods implemented in `__callStatic` are not required implementation in `__call'. --- src/Psalm/Codebase.php | 16 +++++++++------- ...hodGetCompletionItemsForClassishThingTest.php | 2 -- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 4865129efc7..90af927a377 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -69,7 +69,6 @@ use UnexpectedValueException; use function array_combine; -use function array_merge; use function array_pop; use function array_reverse; use function array_values; @@ -1884,12 +1883,15 @@ public function getCompletionItemsForClassishThing( try { $class_storage = $this->classlike_storage_provider->get($atomic_type->value); - $methods = array_merge( - $class_storage->methods, - $class_storage->pseudo_methods, - $class_storage->pseudo_static_methods, - ); - foreach ($methods as $method_storage) { + $method_storages = [...$class_storage->methods]; + if ($gap === '->') { + $method_storages += $class_storage->pseudo_methods; + } + if ($gap === '::') { + $method_storages += $class_storage->pseudo_static_methods; + } + + foreach ($method_storages as $method_storage) { if ($method_storage->is_static || $gap === '->') { $completion_item = new CompletionItem( $method_storage->cased_name, diff --git a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php index 3a8f56c7431..3dac881b88c 100644 --- a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php +++ b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php @@ -126,7 +126,6 @@ private static function privateStaticMethod() {} 'magicObjProp2', 'magicObjMethod', - 'magicStaticMethod', 'publicObjProp', 'protectedObjProp', @@ -201,7 +200,6 @@ private static function privateStaticMethod() {} 'magicObjProp2', 'magicObjMethod', - 'magicStaticMethod', 'publicObjProp', 'protectedObjProp', From 75394c5037cad5816bc6d220d6c78e1c40735b81 Mon Sep 17 00:00:00 2001 From: Ivan Sidorov Date: Tue, 14 Nov 2023 12:26:47 +0000 Subject: [PATCH 061/357] Find declared methods on parent classes and traits Partial revert commit 2f039f9072e5a69d4021055d74c8b47a32b9c575 --- src/Psalm/Codebase.php | 5 ++- ...GetCompletionItemsForClassishThingTest.php | 39 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 90af927a377..7aa70426465 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -1883,7 +1883,10 @@ public function getCompletionItemsForClassishThing( try { $class_storage = $this->classlike_storage_provider->get($atomic_type->value); - $method_storages = [...$class_storage->methods]; + $method_storages = []; + foreach ($class_storage->declaring_method_ids as $declaring_method_id) { + $method_storages[] = $this->methods->getStorage($declaring_method_id); + } if ($gap === '->') { $method_storages += $class_storage->pseudo_methods; } diff --git a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php index 3dac881b88c..962854d15d0 100644 --- a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php +++ b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php @@ -285,11 +285,26 @@ class A { 'publicObjProp', 'protectedObjProp', 'privateObjProp', + + 'abstractPublicMethod', + 'abstractProtectedMethod', + + 'publicObjMethod', + 'protectedObjMethod', + 'privateObjMethod', + + 'publicStaticMethod', + 'protectedStaticMethod', + 'privateStaticMethod', ], '::' => [ 'publicStaticProp', 'protectedStaticProp', 'privateStaticProp', + + 'publicStaticMethod', + 'protectedStaticMethod', + 'privateStaticMethod', ], ]; @@ -349,11 +364,26 @@ abstract class A { 'publicObjProp', 'protectedObjProp', 'privateObjProp', + + 'abstractPublicMethod', + 'abstractProtectedMethod', + + 'publicObjMethod', + 'protectedObjMethod', + 'privateObjMethod', + + 'publicStaticMethod', + 'protectedStaticMethod', + 'privateStaticMethod', ], '::' => [ 'publicStaticProp', 'protectedStaticProp', 'privateStaticProp', + + 'publicStaticMethod', + 'protectedStaticMethod', + 'privateStaticMethod', ], ]; @@ -409,10 +439,19 @@ class A extends C { 'publicObjProp', 'protectedObjProp', + + 'publicObjMethod', + 'protectedObjMethod', + + 'publicStaticMethod', + 'protectedStaticMethod', ], '::' => [ 'publicStaticProp', 'protectedStaticProp', + + 'publicStaticMethod', + 'protectedStaticMethod', ], ]; From ceba3d889d3424a71046797f6e256e3756149101 Mon Sep 17 00:00:00 2001 From: Ivan Sidorov Date: Tue, 14 Nov 2023 11:32:20 +0000 Subject: [PATCH 062/357] Find annotated magic static methods on parent class --- src/Psalm/Internal/Codebase/Populator.php | 2 ++ .../Codebase/MethodGetCompletionItemsForClassishThingTest.php | 1 + 2 files changed, 3 insertions(+) diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php index 6c6dc25b187..958c9bfdf80 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -560,6 +560,8 @@ private function populateDataFromParentClass( $parent_storage->dependent_classlikes[strtolower($storage->name)] = true; + $storage->pseudo_static_methods += $parent_storage->pseudo_static_methods; + $storage->pseudo_methods += $parent_storage->pseudo_methods; $storage->declaring_pseudo_method_ids += $parent_storage->declaring_pseudo_method_ids; } diff --git a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php index 962854d15d0..3b4f651ab61 100644 --- a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php +++ b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php @@ -447,6 +447,7 @@ class A extends C { 'protectedStaticMethod', ], '::' => [ + 'magicStaticMethod', 'publicStaticProp', 'protectedStaticProp', From 0c4b8a3c4990911339bd5f092e226d52da0e0f61 Mon Sep 17 00:00:00 2001 From: Ivan Sidorov Date: Tue, 14 Nov 2023 12:35:32 +0000 Subject: [PATCH 063/357] Find annotated magic static methods on traits --- src/Psalm/Internal/Codebase/Populator.php | 2 ++ .../Codebase/MethodGetCompletionItemsForClassishThingTest.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php index 958c9bfdf80..864a27e7c91 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -450,6 +450,8 @@ private function populateDataFromTrait( $storage->pseudo_property_get_types += $trait_storage->pseudo_property_get_types; $storage->pseudo_property_set_types += $trait_storage->pseudo_property_set_types; + $storage->pseudo_static_methods += $trait_storage->pseudo_static_methods; + $storage->pseudo_methods += $trait_storage->pseudo_methods; $storage->declaring_pseudo_method_ids += $trait_storage->declaring_pseudo_method_ids; } diff --git a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php index 3b4f651ab61..aa14cc89c90 100644 --- a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php +++ b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php @@ -298,6 +298,7 @@ class A { 'privateStaticMethod', ], '::' => [ + 'magicStaticMethod', 'publicStaticProp', 'protectedStaticProp', 'privateStaticProp', @@ -377,6 +378,7 @@ abstract class A { 'privateStaticMethod', ], '::' => [ + 'magicStaticMethod', 'publicStaticProp', 'protectedStaticProp', 'privateStaticProp', From 8bb68e9b3f4226bd53e52240281a7d7af34420c3 Mon Sep 17 00:00:00 2001 From: Ivan Sidorov Date: Thu, 16 Nov 2023 20:54:44 +0000 Subject: [PATCH 064/357] Find properties and methods annotated by mixin --- src/Psalm/Codebase.php | 32 ++++++++++++++++++- ...GetCompletionItemsForClassishThingTest.php | 12 ++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 7aa70426465..f8b6103306e 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -18,6 +18,7 @@ use Psalm\CodeLocation\Raw; use Psalm\Exception\UnanalyzedFileException; use Psalm\Exception\UnpopulatedClasslikeException; +use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Internal\Analyzer\FunctionLikeAnalyzer; use Psalm\Internal\Analyzer\NamespaceAnalyzer; use Psalm\Internal\Analyzer\ProjectAnalyzer; @@ -1867,13 +1868,24 @@ public function getTypeContextAtPosition(string $file_path, Position $position): } /** + * @param list $allow_visibilities * @return list */ public function getCompletionItemsForClassishThing( string $type_string, string $gap, - bool $snippets_supported = false + bool $snippets_supported = false, + array $allow_visibilities = null ): array { + if ($allow_visibilities === null) { + $allow_visibilities = [ + ClassLikeAnalyzer::VISIBILITY_PUBLIC, + ClassLikeAnalyzer::VISIBILITY_PROTECTED, + ClassLikeAnalyzer::VISIBILITY_PRIVATE, + ]; + } + $allow_visibilities[] = null; + $completion_items = []; $type = Type::parseString($type_string); @@ -1895,6 +1907,9 @@ public function getCompletionItemsForClassishThing( } foreach ($method_storages as $method_storage) { + if (!in_array($method_storage->visibility, $allow_visibilities)) { + continue; + } if ($method_storage->is_static || $gap === '->') { $completion_item = new CompletionItem( $method_storage->cased_name, @@ -1957,6 +1972,9 @@ public function getCompletionItemsForClassishThing( $declaring_class . '::$' . $property_name, ); + if (!in_array($property_storage->visibility, $allow_visibilities)) { + continue; + } if ($property_storage->is_static === ($gap === '::')) { $completion_items[] = new CompletionItem( $property_name, @@ -1981,6 +1999,18 @@ public function getCompletionItemsForClassishThing( $const_name, ); } + + if ($gap === '->') { + foreach ($class_storage->namedMixins as $mixin) { + $mixin_completion_items = $this->getCompletionItemsForClassishThing( + $mixin->value, + $gap, + $snippets_supported, + [ClassLikeAnalyzer::VISIBILITY_PUBLIC], + ); + $completion_items = [...$completion_items, ...$mixin_completion_items]; + } + } } catch (Exception $e) { error_log($e->getMessage()); continue; diff --git a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php index aa14cc89c90..51a00f58b16 100644 --- a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php +++ b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php @@ -538,7 +538,17 @@ class A { $actual_labels = $this->getCompletionLabels($content, 'B\A', $gap); $expected_labels = [ - '->' => [], + '->' => [ + 'magicObjProp1', + 'magicObjProp2', + 'magicObjMethod', + + 'publicObjProp', + + 'publicObjMethod', + + 'publicStaticMethod', + ], '::' => [], ]; From e677523843e8da6820e462cde603afd716833286 Mon Sep 17 00:00:00 2001 From: Ivan Sidorov Date: Thu, 16 Nov 2023 21:05:15 +0000 Subject: [PATCH 065/357] Fixing a collisions on annotations with mixins --- src/Psalm/Codebase.php | 8 ++++++- ...GetCompletionItemsForClassishThingTest.php | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index f8b6103306e..7b838caf24c 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -1869,13 +1869,15 @@ public function getTypeContextAtPosition(string $file_path, Position $position): /** * @param list $allow_visibilities + * @param list $ignore_fq_class_names * @return list */ public function getCompletionItemsForClassishThing( string $type_string, string $gap, bool $snippets_supported = false, - array $allow_visibilities = null + array $allow_visibilities = null, + array $ignore_fq_class_names = [] ): array { if ($allow_visibilities === null) { $allow_visibilities = [ @@ -2002,11 +2004,15 @@ public function getCompletionItemsForClassishThing( if ($gap === '->') { foreach ($class_storage->namedMixins as $mixin) { + if (in_array($mixin->value, $ignore_fq_class_names)) { + continue; + } $mixin_completion_items = $this->getCompletionItemsForClassishThing( $mixin->value, $gap, $snippets_supported, [ClassLikeAnalyzer::VISIBILITY_PUBLIC], + [$type_string, ...$ignore_fq_class_names], ); $completion_items = [...$completion_items, ...$mixin_completion_items]; } diff --git a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php index 51a00f58b16..9519553237b 100644 --- a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php +++ b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php @@ -554,4 +554,28 @@ class A { $this->assertEqualsCanonicalizing($expected_labels[$gap], $actual_labels); } + + public function testResolveCollisionWithMixin(): void + { + $content = <<<'EOF' + getCompletionLabels($content, 'B\A', '->'); + + $expected_labels = [ + 'myObjProp', + ]; + + $this->assertEqualsCanonicalizing($expected_labels, $actual_labels); + } } From 88c98ef333df381da936b7168ddb4a4833184541 Mon Sep 17 00:00:00 2001 From: Ivan Sidorov Date: Fri, 17 Nov 2023 14:38:45 +0000 Subject: [PATCH 066/357] Fixing a crash if a method or property does not exist --- src/Psalm/Codebase.php | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 7b838caf24c..414703a4435 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -1899,7 +1899,11 @@ public function getCompletionItemsForClassishThing( $method_storages = []; foreach ($class_storage->declaring_method_ids as $declaring_method_id) { - $method_storages[] = $this->methods->getStorage($declaring_method_id); + try { + $method_storages[] = $this->methods->getStorage($declaring_method_id); + } catch (UnexpectedValueException $e) { + error_log($e->getMessage()); + } } if ($gap === '->') { $method_storages += $class_storage->pseudo_methods; @@ -1970,9 +1974,14 @@ public function getCompletionItemsForClassishThing( } foreach ($class_storage->declaring_property_ids as $property_name => $declaring_class) { - $property_storage = $this->properties->getStorage( - $declaring_class . '::$' . $property_name, - ); + try { + $property_storage = $this->properties->getStorage( + $declaring_class . '::$' . $property_name, + ); + } catch (UnexpectedValueException $e) { + error_log($e->getMessage()); + continue; + } if (!in_array($property_storage->visibility, $allow_visibilities)) { continue; From e9b0103c7fc0468493548e2960520497c10c2304 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sat, 18 Nov 2023 08:03:30 +0100 Subject: [PATCH 067/357] fix prefix --- stubs/extensions/decimal.phpstub | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/stubs/extensions/decimal.phpstub b/stubs/extensions/decimal.phpstub index 57ffc0d282a..5ead065a7af 100644 --- a/stubs/extensions/decimal.phpstub +++ b/stubs/extensions/decimal.phpstub @@ -52,7 +52,7 @@ final class Decimal implements \JsonSerializable * * Initializes a new instance using a given value and minimum precision. * - * @phpstan-param Decimal|numeric-string|int $value + * @psalm-param Decimal|numeric-string|int $value * @param Decimal|string|int $value * @param int $precision * @@ -120,7 +120,7 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @phpstan-param Decimal|numeric-string|int $value + * @psalm-param Decimal|numeric-string|int $value * @param Decimal|string|int $value * * @return Decimal the result of adding this decimal to the given value. @@ -137,7 +137,7 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @phpstan-param Decimal|numeric-string|int $value + * @psalm-param Decimal|numeric-string|int $value * @param Decimal|string|int $value * * @return Decimal the result of subtracting a given value from this decimal. @@ -154,7 +154,7 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @phpstan-param Decimal|numeric-string|int $value + * @psalm-param Decimal|numeric-string|int $value * @param Decimal|string|int $value * * @return Decimal the result of multiplying this decimal by the given value. @@ -171,7 +171,7 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @phpstan-param Decimal|numeric-string|int $value + * @psalm-param Decimal|numeric-string|int $value * @param Decimal|string|int $value * * @return Decimal the result of dividing this decimal by the given value. @@ -192,7 +192,7 @@ final class Decimal implements \JsonSerializable * * @see Decimal::rem for the decimal remainder. * - * @phpstan-param Decimal|numeric-string|int $value + * @psalm-param Decimal|numeric-string|int $value * @param Decimal|string|int $value * * @return Decimal the remainder after dividing the integer value of this @@ -210,7 +210,7 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @phpstan-param Decimal|numeric-string|int $value + * @psalm-param Decimal|numeric-string|int $value * @param Decimal|string|int $value * * @return Decimal the remainder after dividing this decimal by a given value. @@ -229,7 +229,7 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @phpstan-param Decimal|numeric-string|int $exponent The power to raise this decimal to. + * @psalm-param Decimal|numeric-string|int $exponent The power to raise this decimal to. * @param Decimal|string|int $exponent The power to raise this decimal to. * * @return Decimal the result of raising this decimal to a given power. From fdbd9b133c3300a590a50ccf7b8d24038c05390a Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sat, 18 Nov 2023 08:10:44 +0100 Subject: [PATCH 068/357] fix prefixed param and document it --- stubs/extensions/decimal.phpstub | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/stubs/extensions/decimal.phpstub b/stubs/extensions/decimal.phpstub index 5ead065a7af..7d699e8fc6b 100644 --- a/stubs/extensions/decimal.phpstub +++ b/stubs/extensions/decimal.phpstub @@ -3,6 +3,9 @@ namespace Decimal; /** * Copied from https://github.com/php-decimal/stubs/blob/master/Decimal.php + * with prefixed param replaced regular param using regex: + * @(?:psalm|phpstan)-param (.+?) (\$\w+)[^@]+?@param .+?\2 + * @param $1 $2 * * The MIT License (MIT) * Copyright (c) 2018 Rudi Theunissen @@ -52,8 +55,7 @@ final class Decimal implements \JsonSerializable * * Initializes a new instance using a given value and minimum precision. * - * @psalm-param Decimal|numeric-string|int $value - * @param Decimal|string|int $value + * @param Decimal|numeric-string|int $value * @param int $precision * * @throws \BadMethodCallException if already constructed. @@ -120,8 +122,7 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @psalm-param Decimal|numeric-string|int $value - * @param Decimal|string|int $value + * @param Decimal|numeric-string|int $value * * @return Decimal the result of adding this decimal to the given value. * @@ -137,8 +138,7 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @psalm-param Decimal|numeric-string|int $value - * @param Decimal|string|int $value + * @param Decimal|numeric-string|int $value * * @return Decimal the result of subtracting a given value from this decimal. * @@ -154,8 +154,7 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @psalm-param Decimal|numeric-string|int $value - * @param Decimal|string|int $value + * @param Decimal|numeric-string|int $value * * @return Decimal the result of multiplying this decimal by the given value. * @@ -171,8 +170,7 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @psalm-param Decimal|numeric-string|int $value - * @param Decimal|string|int $value + * @param Decimal|numeric-string|int $value * * @return Decimal the result of dividing this decimal by the given value. * @@ -192,8 +190,7 @@ final class Decimal implements \JsonSerializable * * @see Decimal::rem for the decimal remainder. * - * @psalm-param Decimal|numeric-string|int $value - * @param Decimal|string|int $value + * @param Decimal|numeric-string|int $value * * @return Decimal the remainder after dividing the integer value of this * decimal by the integer value of the given value @@ -210,8 +207,7 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @psalm-param Decimal|numeric-string|int $value - * @param Decimal|string|int $value + * @param Decimal|numeric-string|int $value * * @return Decimal the remainder after dividing this decimal by a given value. * @@ -229,8 +225,7 @@ final class Decimal implements \JsonSerializable * The precision of the result will be the max of this decimal's precision * and the given value's precision, where scalar values assume the default. * - * @psalm-param Decimal|numeric-string|int $exponent The power to raise this decimal to. - * @param Decimal|string|int $exponent The power to raise this decimal to. + * @param Decimal|numeric-string|int $exponent The power to raise this decimal to. * * @return Decimal the result of raising this decimal to a given power. * From f3e9263ea4bf625f10c32203f6e6dbe92d2042be Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Mon, 13 Nov 2023 20:54:41 +0100 Subject: [PATCH 069/357] Fix array_filter callback type Fix https://github.com/vimeo/psalm/issues/9068 --- dictionaries/CallMap.php | 2 +- dictionaries/CallMap_80_delta.php | 4 ++-- dictionaries/CallMap_historical.php | 2 +- tests/ArrayFunctionCallTest.php | 12 ++++++++++++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index ccefe6a0b6f..7b45dcc9103 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -371,7 +371,7 @@ 'array_diff_ukey\'1' => ['array', 'array'=>'array', 'rest'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'], 'array_fill' => ['array', 'start_index'=>'int', 'count'=>'int', 'value'=>'mixed'], 'array_fill_keys' => ['array', 'keys'=>'array', 'value'=>'mixed'], -'array_filter' => ['array', 'array'=>'array', 'callback='=>'callable(mixed,mixed=):scalar|null', 'mode='=>'int'], +'array_filter' => ['array', 'array'=>'array', 'callback='=>'callable(mixed,array-key=):mixed|null', 'mode='=>'int'], 'array_flip' => ['array', 'array'=>'array'], 'array_intersect' => ['array', 'array'=>'array', '...arrays='=>'array'], 'array_intersect_assoc' => ['array', 'array'=>'array', '...arrays='=>'array'], diff --git a/dictionaries/CallMap_80_delta.php b/dictionaries/CallMap_80_delta.php index 29227b632bc..adaca6c7cb7 100644 --- a/dictionaries/CallMap_80_delta.php +++ b/dictionaries/CallMap_80_delta.php @@ -553,8 +553,8 @@ 'new' => ['array', 'array'=>'array', '...arrays='=>'array'], ], 'array_filter' => [ - 'old' => ['array', 'array'=>'array', 'callback='=>'callable(mixed,mixed=):scalar', 'mode='=>'int'], - 'new' => ['array', 'array'=>'array', 'callback='=>'callable(mixed,mixed=):scalar|null', 'mode='=>'int'], + 'old' => ['array', 'array'=>'array', 'callback='=>'callable(mixed,array-key=):mixed', 'mode='=>'int'], + 'new' => ['array', 'array'=>'array', 'callback='=>'callable(mixed,array-key=):mixed|null', 'mode='=>'int'], ], 'array_key_exists' => [ 'old' => ['bool', 'key'=>'string|int', 'array'=>'array|object'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 73ce32e3d7e..649f393bea7 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -9288,7 +9288,7 @@ 'array_diff_ukey\'1' => ['array', 'array'=>'array', 'rest'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'], 'array_fill' => ['array', 'start_index'=>'int', 'count'=>'int', 'value'=>'mixed'], 'array_fill_keys' => ['array', 'keys'=>'array', 'value'=>'mixed'], - 'array_filter' => ['array', 'array'=>'array', 'callback='=>'callable(mixed,mixed=):scalar', 'mode='=>'int'], + 'array_filter' => ['array', 'array'=>'array', 'callback='=>'callable(mixed,array-key=):mixed', 'mode='=>'int'], 'array_flip' => ['array', 'array'=>'array'], 'array_intersect' => ['array', 'array'=>'array', '...arrays'=>'array'], 'array_intersect_assoc' => ['array', 'array'=>'array', '...arrays'=>'array'], diff --git a/tests/ArrayFunctionCallTest.php b/tests/ArrayFunctionCallTest.php index fc13113e9de..fb7db0522f8 100644 --- a/tests/ArrayFunctionCallTest.php +++ b/tests/ArrayFunctionCallTest.php @@ -29,6 +29,18 @@ function(?int $i): bool { '$e' => 'array|null>', ], ], + 'arrayFilterObject' => [ + 'code' => ' [ + '$e' => 'array, object>', + ], + ], 'positiveIntArrayFilter' => [ 'code' => ' Date: Mon, 13 Nov 2023 21:10:40 +0100 Subject: [PATCH 070/357] remove now invalid test --- tests/ArrayFunctionCallTest.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/ArrayFunctionCallTest.php b/tests/ArrayFunctionCallTest.php index fb7db0522f8..730aad221d4 100644 --- a/tests/ArrayFunctionCallTest.php +++ b/tests/ArrayFunctionCallTest.php @@ -2637,17 +2637,6 @@ function takes_non_empty_array(array $input): void {} public function providerInvalidCodeParse(): iterable { return [ - 'arrayFilterWithoutTypes' => [ - 'code' => ' 5, "b" => 12, "c" => null], - function(?int $i) { - return $GLOBALS["a"]; - } - );', - 'error_message' => 'MixedArgumentTypeCoercion', - 'ignored_issues' => ['MissingClosureParamType', 'MissingClosureReturnType'], - ], 'arrayFilterUseMethodOnInferrableInt' => [ 'code' => 'foo(); });', From 506fb9c77316feb11585b270fda2189678ea4400 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Mon, 13 Nov 2023 21:12:02 +0100 Subject: [PATCH 071/357] Fix https://github.com/vimeo/psalm/issues/8919 --- .../ArrayFilterReturnTypeProvider.php | 28 ++++++++++++++++--- tests/ArrayFunctionCallTest.php | 13 +++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php index 3e44102f912..d596567c0b0 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php @@ -164,14 +164,34 @@ static function ($keyed_type) use ($statements_source, $context) { if (!isset($call_args[2])) { $function_call_arg = $call_args[1]; + $callable_extended_var_id = ExpressionIdentifier::getExtendedVarId( + $function_call_arg->value, + null, + $statements_source, + ); + + $mapping_function_ids = array(); + if ($callable_extended_var_id) { + $possibly_function_ids = $context->vars_in_scope[$callable_extended_var_id] ?? null; + // @todo for array callables + if ($possibly_function_ids && $possibly_function_ids->allStringLiterals()) { + foreach ($possibly_function_ids->getLiteralStrings() as $atomic) { + $mapping_function_ids[] = $atomic->value; + } + } + } + if ($function_call_arg->value instanceof PhpParser\Node\Scalar\String_ || $function_call_arg->value instanceof PhpParser\Node\Expr\Array_ || $function_call_arg->value instanceof PhpParser\Node\Expr\BinaryOp\Concat + || $mapping_function_ids !== array() ) { - $mapping_function_ids = CallAnalyzer::getFunctionIdsFromCallableArg( - $statements_source, - $function_call_arg->value, - ); + if ($mapping_function_ids === array()) { + $mapping_function_ids = CallAnalyzer::getFunctionIdsFromCallableArg( + $statements_source, + $function_call_arg->value, + ); + } if ($array_arg && $mapping_function_ids) { $assertions = []; diff --git a/tests/ArrayFunctionCallTest.php b/tests/ArrayFunctionCallTest.php index 730aad221d4..004019a83c7 100644 --- a/tests/ArrayFunctionCallTest.php +++ b/tests/ArrayFunctionCallTest.php @@ -41,6 +41,19 @@ function($i) { '$e' => 'array, object>', ], ], + 'arrayFilterStringCallable' => [ + 'code' => ' $bar + */ + $keys = array_keys( $bar ); + $strings = array_filter( $keys, $arg );', + 'assertions' => [ + '$strings' => 'array, string>', + ], + ], 'positiveIntArrayFilter' => [ 'code' => ' Date: Mon, 13 Nov 2023 21:38:02 +0100 Subject: [PATCH 072/357] Fix https://github.com/vimeo/psalm/issues/10371 --- .../ArrayFilterReturnTypeProvider.php | 27 ++++++++++--------- tests/ArrayFunctionCallTest.php | 8 ++++++ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php index d596567c0b0..ada2e2825c6 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php @@ -58,19 +58,22 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev return Type::getMixed(); } + $fallback = new TArray([Type::getArrayKey(), Type::getMixed()]); $array_arg = $call_args[0]->value ?? null; - - $first_arg_array = $array_arg - && ($first_arg_type = $statements_source->node_data->getType($array_arg)) - && $first_arg_type->hasType('array') - && ($array_atomic_type = $first_arg_type->getArray()) - && ($array_atomic_type instanceof TArray - || $array_atomic_type instanceof TKeyedArray) - ? $array_atomic_type - : null; - - if (!$first_arg_array) { - return Type::getArray(); + if (!$array_arg) { + $first_arg_array = $fallback; + } else { + $first_arg_type = $statements_source->node_data->getType($array_arg); + if (!$first_arg_type || $first_arg_type->isMixed()) { + $first_arg_array = $fallback; + } else { + $first_arg_array = $first_arg_type->hasType('array') + && ($array_atomic_type = $first_arg_type->getArray()) + && ($array_atomic_type instanceof TArray + || $array_atomic_type instanceof TKeyedArray) + ? $array_atomic_type + : $fallback; + } } if ($first_arg_array instanceof TArray) { diff --git a/tests/ArrayFunctionCallTest.php b/tests/ArrayFunctionCallTest.php index 004019a83c7..3b1f48e3802 100644 --- a/tests/ArrayFunctionCallTest.php +++ b/tests/ArrayFunctionCallTest.php @@ -54,6 +54,14 @@ function($i) { '$strings' => 'array, string>', ], ], + 'arrayFilterMixed' => [ + 'code' => ' [ + '$x' => 'array', + ], + ], 'positiveIntArrayFilter' => [ 'code' => ' Date: Wed, 15 Nov 2023 09:33:53 +0100 Subject: [PATCH 073/357] Fix param providers for native methods not working Fix array_filter params callable incorrect required params depending on the 3rd argument value (at this commit psalm doesn't report an error, due to https://github.com/vimeo/psalm/issues/8438, which gets fixed by this PR later on though) Fix https://github.com/vimeo/psalm/issues/3047 and further improve types for array_multisort and add errors for invalid params Fix SimpleTypeInferer failing on bitwise operations with constants --- .../Expression/Call/FunctionCallAnalyzer.php | 4 +- .../Expression/SimpleTypeInferer.php | 23 ++ .../Provider/FunctionParamsProvider.php | 5 + .../ArrayFilterParamsProvider.php | 263 +++++++++++++++ .../ArrayMultisortParamsProvider.php | 309 ++++++++++++++++++ tests/ArrayFunctionCallTest.php | 167 +++++++++- 6 files changed, 766 insertions(+), 5 deletions(-) create mode 100644 src/Psalm/Internal/Provider/ParamsProvider/ArrayFilterParamsProvider.php create mode 100644 src/Psalm/Internal/Provider/ParamsProvider/ArrayMultisortParamsProvider.php diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index 3c44bad7067..c5b60af8c80 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -205,7 +205,9 @@ public static function analyze( $statements_analyzer->node_data, ); - $function_call_info->function_params = $function_callable->params; + if (!$codebase->functions->params_provider->has($function_call_info->function_id)) { + $function_call_info->function_params = $function_callable->params; + } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php index d35231e4102..b8f67edb619 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php @@ -10,6 +10,7 @@ use Psalm\FileSource; use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\BinaryOp\ArithmeticOpAnalyzer; +use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ConstFetchAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Provider\NodeDataProvider; use Psalm\Internal\Type\TypeCombiner; @@ -143,6 +144,17 @@ public static function infer( $fq_classlike_name, ); + if (!$stmt_left_type + && $file_source instanceof StatementsAnalyzer + && $stmt->left instanceof PhpParser\Node\Expr\ConstFetch) { + $stmt_left_type = ConstFetchAnalyzer::getConstType( + $file_source, + $stmt->left->name->toString(), + true, + null, + ); + } + $stmt_right_type = self::infer( $codebase, $nodes, @@ -153,6 +165,17 @@ public static function infer( $fq_classlike_name, ); + if (!$stmt_right_type + && $file_source instanceof StatementsAnalyzer + && $stmt->right instanceof PhpParser\Node\Expr\ConstFetch) { + $stmt_right_type = ConstFetchAnalyzer::getConstType( + $file_source, + $stmt->right->name->toString(), + true, + null, + ); + } + if (!$stmt_left_type || !$stmt_right_type) { return null; } diff --git a/src/Psalm/Internal/Provider/FunctionParamsProvider.php b/src/Psalm/Internal/Provider/FunctionParamsProvider.php index b34d2b0a10d..b1ca3aaafd9 100644 --- a/src/Psalm/Internal/Provider/FunctionParamsProvider.php +++ b/src/Psalm/Internal/Provider/FunctionParamsProvider.php @@ -6,6 +6,8 @@ use PhpParser\Node\Arg; use Psalm\CodeLocation; use Psalm\Context; +use Psalm\Internal\Provider\ParamsProvider\ArrayFilterParamsProvider; +use Psalm\Internal\Provider\ParamsProvider\ArrayMultisortParamsProvider; use Psalm\Plugin\EventHandler\Event\FunctionParamsProviderEvent; use Psalm\Plugin\EventHandler\FunctionParamsProviderInterface; use Psalm\StatementsSource; @@ -29,6 +31,9 @@ final class FunctionParamsProvider public function __construct() { self::$handlers = []; + + $this->registerClass(ArrayFilterParamsProvider::class); + $this->registerClass(ArrayMultisortParamsProvider::class); } /** diff --git a/src/Psalm/Internal/Provider/ParamsProvider/ArrayFilterParamsProvider.php b/src/Psalm/Internal/Provider/ParamsProvider/ArrayFilterParamsProvider.php new file mode 100644 index 00000000000..44a4908d41f --- /dev/null +++ b/src/Psalm/Internal/Provider/ParamsProvider/ArrayFilterParamsProvider.php @@ -0,0 +1,263 @@ + + */ + public static function getFunctionIds(): array + { + return [ + 'array_filter', + ]; + } + + /** + * @return ?list + */ + public static function getFunctionParams(FunctionParamsProviderEvent $event): ?array + { + $call_args = $event->getCallArgs(); + if (!isset($call_args[0]) || !isset($call_args[1])) { + return null; + } + + $statements_source = $event->getStatementsSource(); + if (!($statements_source instanceof StatementsAnalyzer)) { + // this is practically impossible + // but the type in the caller is parent type StatementsSource + // even though all callers provide StatementsAnalyzer + return null; + } + + $code_location = $event->getCodeLocation(); + if ($call_args[1]->value instanceof ConstFetch + && strtolower($call_args[1]->value->name->toString()) === 'null' + && isset($call_args[2]) + ) { + if ($code_location) { + // using e.g. ARRAY_FILTER_USE_KEY as 3rd arg won't have any effect if the 2nd arg is null + // as it will still filter on the values + IssueBuffer::maybeAdd( + new InvalidArgument( + 'The 3rd argument of array_filter is not used, when the 2nd argument is null', + $code_location, + 'array_filter', + ), + $statements_source->getSuppressedIssues(), + ); + } + + return null; + } + + // currently only supports literal types and variables (but not function calls) + // due to https://github.com/vimeo/psalm/issues/8905 + $first_arg_type = SimpleTypeInferer::infer( + $statements_source->getCodebase(), + $statements_source->node_data, + $call_args[0]->value, + $statements_source->getAliases(), + $statements_source, + ); + + if (!$first_arg_type) { + $extended_var_id = ExpressionIdentifier::getExtendedVarId( + $call_args[0]->value, + null, + $statements_source, + ); + + $first_arg_type = $event->getContext()->vars_in_scope[$extended_var_id] ?? null; + } + + $fallback = new TArray([Type::getArrayKey(), Type::getMixed()]); + if (!$first_arg_type || $first_arg_type->isMixed()) { + $first_arg_array = $fallback; + } else { + $first_arg_array = $first_arg_type->hasType('array') + && ($array_atomic_type = $first_arg_type->getArray()) + && ($array_atomic_type instanceof TArray + || $array_atomic_type instanceof TKeyedArray) + ? $array_atomic_type + : $fallback; + } + + if ($first_arg_array instanceof TArray) { + $inner_type = $first_arg_array->type_params[1]; + $key_type = $first_arg_array->type_params[0]; + } else { + $inner_type = $first_arg_array->getGenericValueType(); + $key_type = $first_arg_array->getGenericKeyType(); + } + + $has_both = false; + if (isset($call_args[2])) { + $mode_type = SimpleTypeInferer::infer( + $statements_source->getCodebase(), + $statements_source->node_data, + $call_args[2]->value, + $statements_source->getAliases(), + $statements_source, + ); + + if (!$mode_type && $call_args[2]->value instanceof ConstFetch) { + $mode_type = ConstFetchAnalyzer::getConstType( + $statements_source, + $call_args[2]->value->name->toString(), + true, + $event->getContext(), + ); + } elseif (!$mode_type) { + $extended_var_id = ExpressionIdentifier::getExtendedVarId( + $call_args[2]->value, + null, + $statements_source, + ); + + $mode_type = $event->getContext()->vars_in_scope[$extended_var_id] ?? null; + } + + if (!$mode_type || !$mode_type->allIntLiterals()) { + // if we have multiple possible types, keep the default args + return null; + } + + if ($mode_type->isSingleIntLiteral()) { + $mode = $mode_type->getSingleIntLiteral()->value; + } else { + $mode = 0; + foreach ($mode_type->getLiteralInts() as $atomic) { + if ($atomic->value === ARRAY_FILTER_USE_BOTH) { + // we have one which uses both keys and values and one that uses only keys/values + $has_both = true; + continue; + } + + if ($atomic->value === ARRAY_FILTER_USE_KEY) { + // if one of them is ARRAY_FILTER_USE_KEY, all the other types will behave like mode 0 + $inner_type = Type::combineUnionTypes( + $inner_type, + $key_type, + $statements_source->getCodebase(), + ); + + continue; + } + + // to report an error later on + if ($mode === 0 && $atomic->value !== 0) { + $mode = $atomic->value; + } + } + } + + if ($mode > ARRAY_FILTER_USE_KEY || $mode < 0) { + if ($code_location) { + IssueBuffer::maybeAdd( + new PossiblyInvalidArgument( + 'The provided 3rd argument of array_filter contains a value of ' . $mode + . ', which will behave like 0 and filter on values only', + $code_location, + 'array_filter', + ), + $statements_source->getSuppressedIssues(), + ); + } + + $mode = 0; + } + } else { + $mode = 0; + } + + $callback_arg_value = new FunctionLikeParameter( + 'value', + false, + $inner_type, + null, + null, + null, + false, + ); + + $callback_arg_key = new FunctionLikeParameter( + 'key', + false, + $key_type, + null, + null, + null, + false, + ); + + if ($mode === ARRAY_FILTER_USE_BOTH) { + $callback_arg = [ + $callback_arg_value, + $callback_arg_key, + ]; + } elseif ($mode === ARRAY_FILTER_USE_KEY) { + $callback_arg = [ + $callback_arg_key, + ]; + } elseif ($has_both) { + // if we have both + other flags, the 2nd arg is optional + $callback_arg_key->is_optional = true; + $callback_arg = [ + $callback_arg_value, + $callback_arg_key, + ]; + } else { + $callback_arg = [ + $callback_arg_value, + ]; + } + + $callable = new TCallable( + 'callable', + $callback_arg, + Type::getMixed(), + ); + + return [ + new FunctionLikeParameter( + 'array', + false, + Type::getArray(), + Type::getArray(), + null, + null, + false, + ), + new FunctionLikeParameter('callback', false, new Union([$callable])), + new FunctionLikeParameter('mode', false, Type::getInt(), Type::getInt()), + ]; + } +} diff --git a/src/Psalm/Internal/Provider/ParamsProvider/ArrayMultisortParamsProvider.php b/src/Psalm/Internal/Provider/ParamsProvider/ArrayMultisortParamsProvider.php new file mode 100644 index 00000000000..b510f950da4 --- /dev/null +++ b/src/Psalm/Internal/Provider/ParamsProvider/ArrayMultisortParamsProvider.php @@ -0,0 +1,309 @@ + + */ + public static function getFunctionIds(): array + { + return [ + 'array_multisort', + ]; + } + + /** + * @return ?list + */ + public static function getFunctionParams(FunctionParamsProviderEvent $event): ?array + { + $call_args = $event->getCallArgs(); + if (!isset($call_args[0])) { + return null; + } + + $statements_source = $event->getStatementsSource(); + if (!($statements_source instanceof StatementsAnalyzer)) { + // this is practically impossible + // but the type in the caller is parent type StatementsSource + // even though all callers provide StatementsAnalyzer + return null; + } + + $code_location = $event->getCodeLocation(); + $params = []; + $previous_param = false; + $last_array_index = 0; + $last_by_ref_index = -1; + $first_non_ref_index_after_by_ref = -1; + foreach ($call_args as $key => $call_arg) { + $param_type = SimpleTypeInferer::infer( + $statements_source->getCodebase(), + $statements_source->node_data, + $call_arg->value, + $statements_source->getAliases(), + $statements_source, + ); + + if (!$param_type && $call_arg->value instanceof ConstFetch) { + $param_type = ConstFetchAnalyzer::getConstType( + $statements_source, + $call_arg->value->name->toString(), + true, + $event->getContext(), + ); + } + + // @todo currently assumes any function calls are for array types not for sort order/flags + // actually need to check the return type + // which isn't possible atm due to https://github.com/vimeo/psalm/issues/8905 + if (!$param_type && ($call_arg->value instanceof FuncCall || $call_arg->value instanceof MethodCall)) { + if ($first_non_ref_index_after_by_ref < $last_by_ref_index) { + $first_non_ref_index_after_by_ref = $key; + } + + $last_array_index = $key; + $previous_param = 'array'; + $params[] = new FunctionLikeParameter( + 'array' . ($last_array_index + 1), + // function calls will not be used by reference + false, + Type::getArray(), + $key === 0 ? Type::getArray() : null, + ); + + continue; + } + + $extended_var_id = null; + if (!$param_type) { + $extended_var_id = ExpressionIdentifier::getExtendedVarId( + $call_arg->value, + null, + $statements_source, + ); + + $param_type = $event->getContext()->vars_in_scope[$extended_var_id] ?? null; + } + + if (!$param_type) { + return null; + } + + if ($key === 0 && !$param_type->isArray()) { + return null; + } + + if ($param_type->isArray() && $extended_var_id) { + $last_by_ref_index = $key; + $last_array_index = $key; + $previous_param = 'array'; + $params[] = new FunctionLikeParameter( + 'array' . ($last_array_index + 1), + true, + $param_type, + $key === 0 ? Type::getArray() : null, + ); + + continue; + } + + if ($param_type->allIntLiterals()) { + $sort_order = [ + SORT_ASC, + SORT_DESC, + ]; + + $sort_flags = [ + SORT_REGULAR, + SORT_NUMERIC, + SORT_STRING, + SORT_LOCALE_STRING, + SORT_NATURAL, + SORT_STRING|SORT_FLAG_CASE, + SORT_NATURAL|SORT_FLAG_CASE, + ]; + + $sort_param = false; + foreach ($param_type->getLiteralInts() as $atomic) { + if (in_array($atomic->value, $sort_order, true)) { + if ($sort_param === 'sort_order_flags') { + continue; + } + + if ($sort_param === 'sort_order') { + continue; + } + + if ($sort_param === 'sort_flags') { + $sort_param = 'sort_order_flags'; + continue; + } + + $sort_param = 'sort_order'; + + continue; + } + + if (in_array($atomic->value, $sort_flags, true)) { + if ($sort_param === 'sort_order_flags') { + continue; + } + + if ($sort_param === 'sort_flags') { + continue; + } + + if ($sort_param === 'sort_order') { + $sort_param = 'sort_order_flags'; + continue; + } + + $sort_param = 'sort_flags'; + + continue; + } + + if ($code_location) { + IssueBuffer::maybeAdd( + new InvalidArgument( + 'Argument ' . ( $key + 1 ) + . ' of array_multisort sort order/flag contains an invalid value of ' . $atomic->value, + $code_location, + 'array_multisort', + ), + $statements_source->getSuppressedIssues(), + ); + } + } + + if ($sort_param === false) { + return null; + } + + if (($sort_param === 'sort_order' || $sort_param === 'sort_order_flags') + && $previous_param !== 'array') { + if ($code_location) { + IssueBuffer::maybeAdd( + new InvalidArgument( + 'Argument ' . ( $key + 1 ) + . ' of array_multisort contains sort order flags' + . ' and can only be used after an array parameter', + $code_location, + 'array_multisort', + ), + $statements_source->getSuppressedIssues(), + ); + } + + return null; + } + + if ($sort_param === 'sort_flags' && $previous_param !== 'array' && $previous_param !== 'sort_order') { + if ($code_location) { + IssueBuffer::maybeAdd( + new InvalidArgument( + 'Argument ' . ( $key + 1 ) + . ' of array_multisort are sort flags' + . ' and cannot be used after a parameter with sort flags', + $code_location, + 'array_multisort', + ), + $statements_source->getSuppressedIssues(), + ); + } + + return null; + } + + if ($sort_param === 'sort_order_flags') { + $previous_param = 'sort_order'; + } else { + $previous_param = $sort_param; + } + + $params[] = new FunctionLikeParameter( + 'array' . ($last_array_index + 1) . '_' . $previous_param, + false, + Type::getInt(), + ); + + continue; + } + + if (!$param_type->isArray()) { + // too complex for now + return null; + } + + if ($first_non_ref_index_after_by_ref < $last_by_ref_index) { + $first_non_ref_index_after_by_ref = $key; + } + + $last_array_index = $key; + $previous_param = 'array'; + $params[] = new FunctionLikeParameter( + 'array' . ($last_array_index + 1), + false, + Type::getArray(), + ); + } + + if ($code_location) { + if ($last_by_ref_index === - 1) { + IssueBuffer::maybeAdd( + new InvalidArgument( + 'At least 1 array argument of array_multisort must be a variable,' + . ' since the sorting happens by reference and otherwise this function call does nothing', + $code_location, + 'array_multisort', + ), + $statements_source->getSuppressedIssues(), + ); + } elseif ($first_non_ref_index_after_by_ref > $last_by_ref_index) { + IssueBuffer::maybeAdd( + new InvalidArgument( + 'All arguments of array_multisort after argument ' . $first_non_ref_index_after_by_ref + . ', which are after the last by reference passed array argument and its flags,' + . ' are redundant and can be removed, since the sorting happens by reference', + $code_location, + 'array_multisort', + ), + $statements_source->getSuppressedIssues(), + ); + } + } + + return $params; + } +} diff --git a/tests/ArrayFunctionCallTest.php b/tests/ArrayFunctionCallTest.php index 3b1f48e3802..8604de266da 100644 --- a/tests/ArrayFunctionCallTest.php +++ b/tests/ArrayFunctionCallTest.php @@ -48,8 +48,8 @@ function($i) { /** * @var array $bar */ - $keys = array_keys( $bar ); - $strings = array_filter( $keys, $arg );', + $keys = array_keys($bar); + $strings = array_filter($keys, $arg);', 'assertions' => [ '$strings' => 'array, string>', ], @@ -174,6 +174,26 @@ public static function isStatus(string $role): bool $statusList = [Baz::STATUS_FOO, Baz::STATUS_QUX]; $statusList = array_filter($statusList, [Baz::class, "isStatus"]);', ], + 'arrayFilterUseKeyCallback' => [ + 'code' => ' $arg + */ + $a = array_filter($arg, "strlen", ARRAY_FILTER_USE_KEY);', + 'assertions' => [ + '$a' => 'array', + ], + ], + 'arrayFilterUseBothCallback' => [ + 'code' => ' $arg + */ + $a = array_filter($arg, function (int $v, int $k) { return ($v > $k);}, ARRAY_FILTER_USE_BOTH);', + 'assertions' => [ + '$a' => 'array, int>', + ], + ], 'arrayKeysNonEmpty' => [ 'code' => ' 1, "b" => 2]);', @@ -2514,9 +2534,30 @@ function bar(array $list) : void { * @param array $foos */ function foo(array $foos): void { - array_multisort($formLayoutFields, SORT_ASC, array_column($foos, "y")); + array_multisort(array_column($foos, "y"), SORT_ASC, $foos); }', ], + 'arrayMultisortSortRestByRef' => [ + 'code' => ' $test */ + array_multisort( + array_column($test, "s"), + SORT_DESC, + SORT_NATURAL|SORT_FLAG_CASE, + $test + );', + 'assertions' => [ + '$test' => 'non-empty-array', + ], + ], + 'arrayMultisortSort' => [ + 'code' => ' $test */ + array_multisort($test);', + 'assertions' => [ + '$test' => 'non-empty-array', + ], + ], 'arrayMapGenericObject' => [ 'code' => 'foo(); });', 'error_message' => 'InvalidMethodCall', ], + 'arrayFilterThirdArgWillNotBeUsedWhenSecondNull' => [ + 'code' => ' 'InvalidArgument', + 'ignored_issues' => [], + 'php_version' => '8.0', + ], + 'arrayFilterThirdArgInvalidBehavesLike0' => [ + 'code' => ' 'PossiblyInvalidArgument', + ], + 'arrayFilterCallbackValidationThirdArg0' => [ + 'code' => ' $arg + */ + array_filter($arg, "abs", 0);', + 'error_message' => 'InvalidArgument', + ], + 'arrayFilterKeyCallbackLiteral' => [ + 'code' => ' 5, "b" => 12, "c" => null], "abs", ARRAY_FILTER_USE_KEY);', + 'error_message' => 'InvalidArgument', + ], + 'arrayFilterBothCallback' => [ + 'code' => ' $arg + */ + array_filter($arg, "strlen", ARRAY_FILTER_USE_BOTH);', + 'error_message' => 'InvalidArgument', + ], + 'arrayFilterKeyCallback' => [ + 'code' => ' $arg + */ + array_filter($arg, "strlen", ARRAY_FILTER_USE_KEY);', + 'error_message' => 'InvalidScalarArgument', + ], 'arrayMapUseMethodOnInferrableInt' => [ 'code' => 'foo(); }, [1, 2, 3, 4]);', @@ -2701,7 +2783,7 @@ function foo(int $i, string $s) : bool { } array_filter([1, 2, 3], "foo");', - 'error_message' => 'TooFewArguments', + 'error_message' => 'InvalidArgument', ], 'arrayMapBadArgs' => [ 'code' => ' 'InvalidArgument', ], + 'arrayMultisortInvalidFlag' => [ + 'code' => '> $test */ + array_multisort( + $test, + SORT_FLAG_CASE, + );', + 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:21 - Argument 2 of array_multisort sort order/flag contains an invalid value of 8', + ], + 'arrayMultisortInvalidSortFlags' => [ + 'code' => '> $test */ + array_multisort( + array_column($test, "s"), + SORT_DESC, + SORT_ASC, + $test + );', + 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:21 - Argument 3 of array_multisort contains sort order flags and can only be used after an array parameter', + ], + 'arrayMultisortInvalidSortAfterFlags' => [ + 'code' => '> $test */ + array_multisort( + array_column($test, "s"), + SORT_NATURAL, + SORT_DESC, + $test + );', + 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:21 - Argument 3 of array_multisort contains sort order flags and can only be used after an array parameter', + ], + 'arrayMultisortInvalidFlagsAfterFlags' => [ + 'code' => '> $test */ + array_multisort( + array_column($test, "s"), + $test, + SORT_NATURAL|SORT_FLAG_CASE, + SORT_LOCALE_STRING, + );', + 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:21 - Argument 4 of array_multisort are sort flags and cannot be used after a parameter with sort flags', + ], + 'arrayMultisortNoByRef' => [ + 'code' => ' $test */ + array_multisort( + array_column($test, "s"), + SORT_DESC, + array_column($test, "id") + );', + 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:21 - At least 1 array argument of array_multisort must be a variable, since the sorting happens by reference and otherwise this function call does nothing', + ], + 'arrayMultisortNotByRefAfterLastByRef' => [ + 'code' => ' $test */ + array_multisort( + array_column($test, "s"), + SORT_DESC, + $test, + SORT_ASC, + array_column($test, "id"), + );', + 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:21 - All arguments of array_multisort after argument 4, which are after the last by reference passed array argument and its flags, are redundant and can be removed, since the sorting happens by reference', + ], + 'arrayMultisortNotByRefAfterLastByRefWithFlag' => [ + 'code' => ' $test */ + array_multisort( + array_column($test, "s"), + SORT_DESC, + $test, + SORT_ASC, + array_column($test, "id"), + SORT_NATURAL + );', + 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:21 - All arguments of array_multisort after argument 4, which are after the last by reference passed array argument and its flags, are redundant and can be removed, since the sorting happens by reference', + ], ]; } } From 0252bbbd7cb93e36be1d087854d31eea2da0f1c2 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 15 Nov 2023 09:45:56 +0100 Subject: [PATCH 074/357] Fixed https://psalm.dev/r/7f112fd745 - MethodComparator only reported an error for this if the parent class was user defined (= not in stubs), which is wrong, since this will cause a fatal error when running the code --- .../Internal/Analyzer/MethodComparator.php | 96 ++++++++++++++----- .../Event/FunctionParamsProviderEvent.php | 4 +- tests/ArrayAccessTest.php | 14 +-- tests/ArrayAssignmentTest.php | 18 ++-- tests/MethodSignatureTest.php | 8 +- tests/StubTest.php | 2 +- tests/TestCase.php | 13 +++ 7 files changed, 111 insertions(+), 44 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/MethodComparator.php b/src/Psalm/Internal/Analyzer/MethodComparator.php index 3bfd4de65e9..c18fe47f883 100644 --- a/src/Psalm/Internal/Analyzer/MethodComparator.php +++ b/src/Psalm/Internal/Analyzer/MethodComparator.php @@ -37,6 +37,7 @@ use Psalm\Type\Union; use function array_filter; +use function count; use function in_array; use function strpos; use function strtolower; @@ -130,7 +131,7 @@ public static function compare( ) { IssueBuffer::maybeAdd( new MethodSignatureMustProvideReturnType( - 'Method ' . $cased_implementer_method_id . ' must have a return type signature!', + 'Method ' . $cased_implementer_method_id . ' must have a return type signature', $implementer_method_storage->location ?: $code_location, ), $suppressed_issues + $implementer_classlike_storage->suppressed_issues, @@ -199,10 +200,9 @@ public static function compare( ); } - if ($guide_classlike_storage->user_defined - && ($guide_classlike_storage->is_interface - || $guide_classlike_storage->preserve_constructor_signature - || $implementer_method_storage->cased_name !== '__construct') + if (($guide_classlike_storage->is_interface + || $guide_classlike_storage->preserve_constructor_signature + || $implementer_method_storage->cased_name !== '__construct') && $implementer_method_storage->required_param_count > $guide_method_storage->required_param_count ) { if ($implementer_method_storage->cased_name !== '__construct') { @@ -361,10 +361,20 @@ private static function compareMethodParams( CodeLocation $code_location, array $suppressed_issues ): void { + // ignore errors from stubbed/out of project files + $config = Config::getInstance(); + if (!$implementer_classlike_storage->user_defined + && (!$implementer_param->location + || !$config->isInProjectDirs( + $implementer_param->location->file_path, + ) + )) { + return; + } + if ($prevent_method_signature_mismatch) { if (!$guide_classlike_storage->user_defined - && $guide_param->type - ) { + && $guide_param->type) { $implementer_param_type = $implementer_param->signature_type; $guide_param_signature_type = $guide_param->type; @@ -386,8 +396,6 @@ private static function compareMethodParams( && !$guide_param->type->from_docblock && ($implementer_param_type || $guide_param_signature_type) ) { - $config = Config::getInstance(); - if ($implementer_param_type && (!$guide_param_signature_type || strtolower($implementer_param_type->getId()) @@ -438,11 +446,8 @@ private static function compareMethodParams( } } - $config = Config::getInstance(); - if ($guide_param->name !== $implementer_param->name && $guide_method_storage->allow_named_arg_calls - && $guide_classlike_storage->user_defined && $implementer_classlike_storage->user_defined && $implementer_param->location && $guide_method_storage->cased_name @@ -451,7 +456,10 @@ private static function compareMethodParams( $implementer_param->location->file_path, ) ) { - if ($config->allow_named_arg_calls + if (!$guide_classlike_storage->user_defined && $i === 0 && count($guide_method_storage->params) < 2) { + // if it's third party defined and a single arg, renaming is unnecessary + // if we still want to psalter it, move this if and change the else below to elseif + } elseif ($config->allow_named_arg_calls || ($guide_classlike_storage->location && !$config->isInProjectDirs($guide_classlike_storage->location->file_path) ) @@ -491,9 +499,7 @@ private static function compareMethodParams( } } - if ($guide_classlike_storage->user_defined - && $implementer_param->signature_type - ) { + if ($implementer_param->signature_type) { self::compareMethodSignatureParams( $codebase, $i, @@ -532,9 +538,7 @@ private static function compareMethodParams( ); } - if ($guide_classlike_storage->user_defined && $implementer_param->by_ref !== $guide_param->by_ref) { - $config = Config::getInstance(); - + if ($implementer_param->by_ref !== $guide_param->by_ref) { IssueBuffer::maybeAdd( new MethodSignatureMismatch( 'Argument ' . ($i + 1) . ' of ' . $cased_implementer_method_id . ' is' . @@ -585,6 +589,50 @@ private static function compareMethodSignatureParams( ) : null; + // CallMapHandler needed due to https://github.com/vimeo/psalm/issues/10378 + if (!$guide_param->signature_type + && $guide_param->type + && InternalCallMapHandler::inCallMap($cased_guide_method_id)) { + $guide_method_storage_param_type = TypeExpander::expandUnion( + $codebase, + $guide_param->type, + $guide_classlike_storage->is_trait && $guide_method_storage->abstract + ? $implementer_classlike_storage->name + : $guide_classlike_storage->name, + $guide_classlike_storage->is_trait && $guide_method_storage->abstract + ? $implementer_classlike_storage->name + : $guide_classlike_storage->name, + $guide_classlike_storage->is_trait && $guide_method_storage->abstract + ? $implementer_classlike_storage->parent_class + : $guide_classlike_storage->parent_class, + ); + + $builder = $guide_method_storage_param_type->getBuilder(); + foreach ($builder->getAtomicTypes() as $k => $t) { + if ($t instanceof TTemplateParam) { + $builder->removeType($k); + + foreach ($t->as->getAtomicTypes() as $as_t) { + $builder->addType($as_t); + } + } + } + + if ($builder->hasMixed()) { + foreach ($builder->getAtomicTypes() as $k => $_) { + if ($k !== 'mixed') { + $builder->removeType($k); + } + } + } + $guide_method_storage_param_type = $builder->freeze(); + unset($builder); + + if (!$guide_method_storage_param_type->hasMixed() || $codebase->analysis_php_version_id >= 8_00_00) { + $guide_param_signature_type = $guide_method_storage_param_type; + } + } + $implementer_param_signature_type = TypeExpander::expandUnion( $codebase, $implementer_param_signature_type, @@ -897,11 +945,11 @@ private static function compareMethodSignatureReturnTypes( if (!$is_contained_by) { if ($codebase->analysis_php_version_id >= 8_00_00 - || $guide_classlike_storage->is_trait === $implementer_classlike_storage->is_trait - || !in_array($guide_classlike_storage->name, $implementer_classlike_storage->used_traits) - || $implementer_method_storage->defining_fqcln !== $implementer_classlike_storage->name - || (!$implementer_method_storage->abstract - && !$guide_method_storage->abstract) + || $guide_classlike_storage->is_trait === $implementer_classlike_storage->is_trait + || !in_array($guide_classlike_storage->name, $implementer_classlike_storage->used_traits) + || $implementer_method_storage->defining_fqcln !== $implementer_classlike_storage->name + || (!$implementer_method_storage->abstract + && !$guide_method_storage->abstract) ) { IssueBuffer::maybeAdd( new MethodSignatureMismatch( diff --git a/src/Psalm/Plugin/EventHandler/Event/FunctionParamsProviderEvent.php b/src/Psalm/Plugin/EventHandler/Event/FunctionParamsProviderEvent.php index 23f3e05ff15..6dd717c6413 100644 --- a/src/Psalm/Plugin/EventHandler/Event/FunctionParamsProviderEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/FunctionParamsProviderEvent.php @@ -12,7 +12,7 @@ final class FunctionParamsProviderEvent private StatementsSource $statements_source; private string $function_id; /** - * @var PhpParser\Node\Arg[] + * @var list */ private array $call_args; private ?Context $context; @@ -47,7 +47,7 @@ public function getFunctionId(): string } /** - * @return PhpParser\Node\Arg[] + * @return list */ public function getCallArgs(): array { diff --git a/tests/ArrayAccessTest.php b/tests/ArrayAccessTest.php index 2302b2661b6..8bc2a488fc1 100644 --- a/tests/ArrayAccessTest.php +++ b/tests/ArrayAccessTest.php @@ -907,20 +907,20 @@ public function offsetGet($name) } /** - * @param ?string $name + * @param ?string $offset * @param scalar|array $value * @psalm-suppress MixedArgumentTypeCoercion */ - public function offsetSet($name, $value) : void + public function offsetSet($offset, $value) : void { if (is_array($value)) { $value = new static($value); } - if (null === $name) { + if (null === $offset) { $this->data[] = $value; } else { - $this->data[$name] = $value; + $this->data[$offset] = $value; } } @@ -1053,12 +1053,12 @@ public function offsetGet($name) } /** - * @param string $name + * @param string $offset * @param mixed $value */ - public function offsetSet($name, $value) : void + public function offsetSet($offset, $value) : void { - $this->data[$name] = $value; + $this->data[$offset] = $value; } public function __isset(string $name) : bool diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 11f30867619..38f98d70d60 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -1043,17 +1043,20 @@ function foo(array $arr) : void { * @template-implements ArrayAccess */ class C implements ArrayAccess { - public function offsetExists(int $offset) : bool { return true; } + public function offsetExists(mixed $offset) : bool { return true; } public function offsetGet($offset) : string { return "";} - public function offsetSet(?int $offset, string $value) : void {} + public function offsetSet(mixed $offset, mixed $value) : void {} - public function offsetUnset(int $offset) : void { } + public function offsetUnset(mixed $offset) : void { } } $c = new C(); $c[] = "hello";', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.0', ], 'checkEmptinessAfterConditionalArrayAdjustment' => [ 'code' => ' */ class C implements ArrayAccess { - public function offsetExists(int $offset) : bool { return true; } + public function offsetExists(mixed $offset) : bool { return true; } public function offsetGet($offset) : string { return "";} - public function offsetSet(int $offset, string $value) : void {} + public function offsetSet(mixed $offset, mixed $value) : void {} - public function offsetUnset(int $offset) : void { } + public function offsetUnset(mixed $offset) : void { } } $c = new C(); $c[] = "hello";', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.0', ], 'conditionalRestrictedDocblockKeyAssignment' => [ 'code' => 'expectException(CodeException::class); - $this->expectExceptionMessage('ImplementedParamTypeMismatch'); + $this->expectExceptionMessage('MethodSignatureMismatch'); $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__), diff --git a/tests/TestCase.php b/tests/TestCase.php index d0d285eb9e3..2da89b3558c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -32,10 +32,23 @@ class TestCase extends BaseTestCase { protected static string $src_dir_path; + /** + * caused by phpunit using setUp() instead of __construct + * could perhaps use psalm-plugin-phpunit once https://github.com/psalm/psalm-plugin-phpunit/issues/129 + * to remove this suppression + * + * @psalm-suppress PropertyNotSetInConstructor + */ protected ProjectAnalyzer $project_analyzer; + /** + * @psalm-suppress PropertyNotSetInConstructor + */ protected FakeFileProvider $file_provider; + /** + * @psalm-suppress PropertyNotSetInConstructor + */ protected Config $testConfig; public static function setUpBeforeClass(): void From 5eacd301e992b34329ec3c28588970741d3bcb43 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 15 Nov 2023 09:52:54 +0100 Subject: [PATCH 075/357] Fixed ReturnTypeWillChange false positive https://psalm.dev/r/91c6992bf1 with missing return type signature --- .../Internal/Analyzer/MethodComparator.php | 15 ++++++++--- tests/AttributeTest.php | 26 +++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/MethodComparator.php b/src/Psalm/Internal/Analyzer/MethodComparator.php index c18fe47f883..61726c5adbe 100644 --- a/src/Psalm/Internal/Analyzer/MethodComparator.php +++ b/src/Psalm/Internal/Analyzer/MethodComparator.php @@ -7,6 +7,7 @@ use Psalm\CodeLocation; use Psalm\Codebase; use Psalm\Config; +use Psalm\Internal\Codebase\InternalCallMapHandler; use Psalm\Internal\FileManipulation\FileManipulationBuffer; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\PhpVisitor\ParamReplacementVisitor; @@ -117,13 +118,13 @@ public static function compare( ); } + // CallMapHandler needed due to https://github.com/vimeo/psalm/issues/10378 if (!$guide_classlike_storage->user_defined && $implementer_classlike_storage->user_defined && $codebase->analysis_php_version_id >= 8_01_00 - && ($guide_method_storage->return_type + && (($guide_method_storage->return_type && InternalCallMapHandler::inCallMap($cased_guide_method_id)) || $guide_method_storage->signature_return_type - ) - && !$implementer_method_storage->signature_return_type + ) && !$implementer_method_storage->signature_return_type && !array_filter( $implementer_method_storage->attributes, static fn(AttributeStorage $s): bool => $s->fq_class_name === 'ReturnTypeWillChange', @@ -944,7 +945,13 @@ private static function compareMethodSignatureReturnTypes( : UnionTypeComparator::isContainedByInPhp($implementer_signature_return_type, $guide_signature_return_type); if (!$is_contained_by) { - if ($codebase->analysis_php_version_id >= 8_00_00 + if ($implementer_signature_return_type === null + && array_filter( + $implementer_method_storage->attributes, + static fn(AttributeStorage $s): bool => $s->fq_class_name === 'ReturnTypeWillChange', + )) { + // no error if return type will change and no signature set at all + } elseif ($codebase->analysis_php_version_id >= 8_00_00 || $guide_classlike_storage->is_trait === $implementer_classlike_storage->is_trait || !in_array($guide_classlike_storage->name, $implementer_classlike_storage->used_traits) || $implementer_method_storage->defining_fqcln !== $implementer_classlike_storage->name diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index 18d82b04ec4..f1051773882 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -252,6 +252,32 @@ public function getIterator() 'ignored_issues' => [], 'php_version' => '8.1', ], + 'returnTypeWillChangeNoSignatureType' => [ + 'code' => ' $arg + * @return string + */ + public function run($arg) : string { + return implode("s", $arg); + } + } + + class Bar extends Foo { + /** + * @param array $arg + * @return string + */ + #[ReturnTypeWillChange] + public function run($arg) { + return implode(" ", $arg); + } + }', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], 'allowDynamicProperties' => [ 'code' => ' Date: Wed, 15 Nov 2023 09:59:29 +0100 Subject: [PATCH 076/357] Fix that files passed via CLI but are not in projectFiles reported only a limited, random selection of errors instead of all - treat all files passed as CLI args as if they were in projectFiles --- src/Psalm/Config.php | 61 +++++++++++++++++++++++++++++++++ src/Psalm/Internal/CliUtils.php | 9 ++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index c2a54477344..d8ba8e8f416 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -21,6 +21,7 @@ use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Internal\Analyzer\FileAnalyzer; use Psalm\Internal\Analyzer\ProjectAnalyzer; +use Psalm\Internal\CliUtils; use Psalm\Internal\Composer; use Psalm\Internal\EventDispatcher; use Psalm\Internal\IncludeCollector; @@ -1298,6 +1299,66 @@ private static function fromXmlAndPaths( $config->project_files = ProjectFileFilter::loadFromXMLElement($config_xml->projectFiles, $base_dir, true); } + // any paths passed via CLI should be added to the projectFiles + // as they're getting analyzed like if they are part of the project + // ProjectAnalyzer::getInstance()->check_paths_files is not populated at this point in time + $paths_to_check = CliUtils::getPathsToCheck(null); + if ($paths_to_check !== null) { + $paths_to_add_to_project_files = array(); + foreach ($paths_to_check as $path) { + // if we have an .xml arg here, the files passed are invalid + // valid cases (in which we don't want to add CLI passed files to projectFiles though) + // are e.g. if running phpunit tests for psalm itself + if (substr($path, -4) === '.xml') { + $paths_to_add_to_project_files = array(); + break; + } + + // we need an absolute path for checks + if ($path[0] !== '/' && DIRECTORY_SEPARATOR === '/') { + $prospective_path = $base_dir . DIRECTORY_SEPARATOR . $path; + } else { + $prospective_path = $path; + } + + // will report an error when config is loaded anyway + if (!file_exists($prospective_path)) { + continue; + } + + if ($config->isInProjectDirs($prospective_path)) { + continue; + } + + $paths_to_add_to_project_files[] = $prospective_path; + } + + if ($paths_to_add_to_project_files !== array() && !isset($config_xml->projectFiles)) { + if ($config_xml === null) { + $config_xml = new SimpleXMLElement(''); + } + $config_xml->addChild('projectFiles'); + } + + if ($paths_to_add_to_project_files !== array() && isset($config_xml->projectFiles)) { + foreach ($paths_to_add_to_project_files as $path) { + if (is_dir($path)) { + $child = $config_xml->projectFiles->addChild('directory'); + } else { + $child = $config_xml->projectFiles->addChild('file'); + } + + $child->addAttribute('name', $path); + } + + $config->project_files = ProjectFileFilter::loadFromXMLElement( + $config_xml->projectFiles, + $base_dir, + true, + ); + } + } + if (isset($config_xml->extraFiles)) { $config->extra_files = ProjectFileFilter::loadFromXMLElement($config_xml->extraFiles, $base_dir, true); } diff --git a/src/Psalm/Internal/CliUtils.php b/src/Psalm/Internal/CliUtils.php index 03328e0d999..cdd4e281311 100644 --- a/src/Psalm/Internal/CliUtils.php +++ b/src/Psalm/Internal/CliUtils.php @@ -282,7 +282,14 @@ public static function getPathsToCheck($f_paths): ?array } if (strpos($input_path, '--') === 0 && strlen($input_path) > 2) { - if (substr($input_path, 2) === 'config') { + // ignore --config psalm.xml + // ignore common phpunit args that accept a class instead of a path, as this can cause issues on Windows + $ignored_arguments = array( + 'config', + 'printer', + ); + + if (in_array(substr($input_path, 2), $ignored_arguments, true)) { ++$i; } continue; From cea98fffa0f7d5c27ccdefaf0f622829488c7c69 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 15 Nov 2023 10:11:46 +0100 Subject: [PATCH 077/357] Fix for classes what https://github.com/vimeo/psalm/pull/8503 fixed for functions (as some issues didn't report for other reasons, which were solved since then) --- .../PhpVisitor/Reflector/ClassLikeNodeScanner.php | 9 +++++++++ src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index c4a6e3b491a..87da361b2e1 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -176,6 +176,15 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool if ($this->codebase->classlike_storage_provider->has($fq_classlike_name_lc)) { $duplicate_storage = $this->codebase->classlike_storage_provider->get($fq_classlike_name_lc); + // don't override data from files that are getting analyzed with data from stubs + // if the stubs contain the same class + if (!$duplicate_storage->stubbed + && $this->codebase->register_stub_files + && $duplicate_storage->stmt_location + && $this->config->isInProjectDirs($duplicate_storage->stmt_location->file_path)) { + return false; + } + if (!$this->codebase->register_stub_files) { if (!$duplicate_storage->stmt_location || $duplicate_storage->stmt_location->file_path !== $this->file_path diff --git a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php index 85d538c3ea8..c8a05ea6d65 100644 --- a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php @@ -152,13 +152,13 @@ public function enterNode(PhpParser\Node $node): ?int $this->namespace_name, ); - $this->classlike_node_scanners[] = $classlike_node_scanner; - if ($classlike_node_scanner->start($node) === false) { $this->bad_classes[spl_object_id($node)] = true; - return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN; + return PhpParser\NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } + $this->classlike_node_scanners[] = $classlike_node_scanner; + $this->type_aliases = array_merge($this->type_aliases, $classlike_node_scanner->type_aliases); } elseif ($node instanceof PhpParser\Node\Stmt\TryCatch) { foreach ($node->catches as $catch) { From 0ca2d4f597ac4d74d2cf11b31836efbc3731a22e Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 15 Nov 2023 10:32:35 +0100 Subject: [PATCH 078/357] Fix https://github.com/vimeo/psalm/issues/6085 --- .../Comparator/CallableTypeComparator.php | 2 ++ tests/CallableTest.php | 29 ++++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index 855445fadd0..4c7c3b8c7df 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -30,6 +30,7 @@ use UnexpectedValueException; use function array_slice; +use function count; use function end; use function strtolower; use function substr; @@ -477,6 +478,7 @@ public static function getCallableMethodIdFromTKeyedArray( ) { if (!isset($input_type_part->properties[0]) || !isset($input_type_part->properties[1]) + || count($input_type_part->properties) > 2 ) { return 'not-callable'; } diff --git a/tests/CallableTest.php b/tests/CallableTest.php index 605213b4a51..943b2990f78 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -1827,16 +1827,16 @@ function withVariadic(int $a, int $b, int ...$rest): int { return 0; } - + /** @param Closure(int, int): int $f */ function int_int(Closure $f): void {} - + /** @param Closure(int, int, int): int $f */ function int_int_int(Closure $f): void {} - + /** @param Closure(int, int, int, int): int $f */ function int_int_int_int(Closure $f): void {} - + int_int(withVariadic(...)); int_int_int(withVariadic(...)); int_int_int_int(withVariadic(...));', @@ -2109,6 +2109,19 @@ public function __construct(string $name, callable $callable) {} new Func("f", ["Foo", "bar"]);', 'error_message' => 'InvalidArgument', ], + 'invalidArrayCallable' => [ + 'code' => ' 'InvalidArgument', + ], 'preventStringDocblockType' => [ 'code' => ' Date: Wed, 15 Nov 2023 12:44:16 +0100 Subject: [PATCH 079/357] Fix optional args not enforced in callable (fix for non-closure/arrow functions of https://github.com/vimeo/psalm/issues/8438) Fix array callables not treated as callable https://psalm.dev/r/23f3787207 (this is needed to fix the optional args enforcement for array callables too) --- .../Expression/Call/ArgumentAnalyzer.php | 51 +++++-- .../Type/Comparator/UnionTypeComparator.php | 11 +- .../Type/SimpleAssertionReconciler.php | 8 ++ .../Type/SimpleNegatedAssertionReconciler.php | 19 ++- tests/ArgTest.php | 5 +- tests/CallableTest.php | 133 ++++++++++++++++++ tests/Template/ClassTemplateTest.php | 2 +- 7 files changed, 211 insertions(+), 18 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index 4f184b6ce49..bbcc2135598 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -17,7 +17,6 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Analyzer\TraitAnalyzer; use Psalm\Internal\Codebase\ConstantTypeResolver; -use Psalm\Internal\Codebase\InternalCallMapHandler; use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\Codebase\VariableUseGraph; use Psalm\Internal\DataFlow\DataFlowNode; @@ -840,21 +839,55 @@ public static function verifyType( // $statements_analyzer, which is necessary to understand string function names $input_type = $input_type->getBuilder(); foreach ($input_type->getAtomicTypes() as $key => $atomic_type) { - if (!$atomic_type instanceof TLiteralString - || InternalCallMapHandler::inCallMap($atomic_type->value) - ) { - continue; - } + $container_callable_type = $param_type->getSingleAtomic(); + $container_callable_type = $container_callable_type instanceof TCallable + ? $container_callable_type + : null; $candidate_callable = CallableTypeComparator::getCallableFromAtomic( $codebase, $atomic_type, - null, + $container_callable_type, $statements_analyzer, true, ); - if ($candidate_callable) { + if ($candidate_callable && $candidate_callable !== $atomic_type) { + // if we had an array callable, mark it as used now, since it's not possible later + $potential_method_id = null; + if ($atomic_type instanceof TList) { + $atomic_type = $atomic_type->getKeyedArray(); + } + + if ($atomic_type instanceof TKeyedArray) { + $potential_method_id = CallableTypeComparator::getCallableMethodIdFromTKeyedArray( + $atomic_type, + $codebase, + $context->calling_method_id, + $statements_analyzer->getFilePath(), + ); + } elseif ($atomic_type instanceof TLiteralString + && strpos($atomic_type->value, '::') + ) { + $parts = explode('::', $atomic_type->value); + $potential_method_id = new MethodIdentifier( + $parts[0], + strtolower($parts[1]), + ); + } + + if ($potential_method_id && $potential_method_id !== 'not-callable') { + $codebase->methods->methodExists( + $potential_method_id, + $context->calling_method_id, + $arg_location, + $statements_analyzer, + $statements_analyzer->getFilePath(), + true, + $context->insideUse(), + ); + } + $input_type->removeType($key); $input_type->addType($candidate_callable); } @@ -937,7 +970,7 @@ public static function verifyType( $codebase->methods->methodExists( $potential_method_id, $context->calling_method_id, - null, + $arg_location, $statements_analyzer, $statements_analyzer->getFilePath(), true, diff --git a/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php b/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php index 7a22dc87325..f83427ea101 100644 --- a/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php @@ -145,7 +145,7 @@ public static function isContainedBy( $container_all_param_count = count($container_type_part->params); $container_required_param_count = 0; foreach ($container_type_part->params as $index => $container_param) { - if ($container_param->is_optional === false) { + if (!$container_param->is_optional) { $container_required_param_count = $index + 1; } @@ -161,7 +161,8 @@ public static function isContainedBy( } else { $input_all_param_count = count($input_type_part->params); foreach ($input_type_part->params as $index => $input_param) { - if ($input_param->is_optional === false) { + // can be false or not set at all + if (!$input_param->is_optional) { $input_required_param_count = $index + 1; } @@ -172,8 +173,10 @@ public static function isContainedBy( } // too few or too many non-optional params provided in callback - if ($container_required_param_count > $input_all_param_count - || $container_all_param_count < $input_required_param_count + if ($container_all_param_count > $input_all_param_count + || $container_required_param_count > $input_all_param_count + || $input_required_param_count > $container_all_param_count + || $input_required_param_count > $container_required_param_count ) { continue; } diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index f63100c49aa..3c0762da1c0 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -7,6 +7,7 @@ use Psalm\Codebase; use Psalm\Internal\Codebase\ClassConstantByWildcardResolver; use Psalm\Internal\Codebase\InternalCallMapHandler; +use Psalm\Internal\Type\Comparator\CallableTypeComparator; use Psalm\Storage\Assertion; use Psalm\Storage\Assertion\Any; use Psalm\Storage\Assertion\ArrayKeyExists; @@ -2695,6 +2696,13 @@ private static function reconcileCallable( $redundant = false; $callable_types[] = $type; + } elseif ($candidate_callable = CallableTypeComparator::getCallableFromAtomic( + $codebase, + $type, + )) { + $redundant = false; + + $callable_types[] = $candidate_callable; } else { $redundant = false; } diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index 06d654ec1c2..406c77f8624 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -5,6 +5,7 @@ use Psalm\CodeLocation; use Psalm\Codebase; use Psalm\Internal\Codebase\InternalCallMapHandler; +use Psalm\Internal\Type\Comparator\CallableTypeComparator; use Psalm\Issue\DocblockTypeContradiction; use Psalm\Issue\RedundantPropertyInitializationCheck; use Psalm\Issue\TypeDoesNotContainType; @@ -416,6 +417,8 @@ public static function reconcile( if ($assertion_type instanceof TCallable) { return self::reconcileCallable( $existing_var_type, + $codebase, + $assertion_type, ); } @@ -423,7 +426,9 @@ public static function reconcile( } private static function reconcileCallable( - Union $existing_var_type + Union $existing_var_type, + Codebase $codebase, + TCallable $assertion_type ): Union { $existing_var_type = $existing_var_type->getBuilder(); foreach ($existing_var_type->getAtomicTypes() as $atomic_key => $type) { @@ -431,10 +436,22 @@ private static function reconcileCallable( && InternalCallMapHandler::inCallMap($type->value) ) { $existing_var_type->removeType($atomic_key); + continue; } if ($type->isCallableType()) { $existing_var_type->removeType($atomic_key); + continue; + } + + $candidate_callable = CallableTypeComparator::getCallableFromAtomic( + $codebase, + $type, + $assertion_type, + ); + + if ($candidate_callable) { + $existing_var_type->removeType($atomic_key); } } diff --git a/tests/ArgTest.php b/tests/ArgTest.php index 9cb3753b653..5fefdbef086 100644 --- a/tests/ArgTest.php +++ b/tests/ArgTest.php @@ -348,11 +348,10 @@ function var_caller($callback) {} /** * @param string $a - * @param int $b - * @param int $c + * @param int ...$b * @return void */ - function foo($a, $b, $c) {} + function foo($a, ...$b) {} var_caller("foo");', ], diff --git a/tests/CallableTest.php b/tests/CallableTest.php index 943b2990f78..200177e1d7b 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -1805,6 +1805,30 @@ function foo($arg): void {} foo(["a", "b"]);', ], + 'callableOptionalOrAdditionalOptional' => [ + 'code' => ' [], + 'ignored_issues' => ['InvalidReturnType'], + ], 'abstractInvokeInTrait' => [ 'code' => ' 'InvalidArgument', ], + 'callableMissingOptional' => [ + 'code' => ' 5 ? true : false; + } + + foo("bar");', + 'error_message' => 'PossiblyInvalidArgument', + ], + 'callableMissingOptionalThisArray' => [ + 'code' => ' 'PossiblyInvalidArgument', + ], + 'callableMissingOptionalVariableInstanceArray' => [ + 'code' => ' 'PossiblyInvalidArgument', + ], + 'callableMissingOptionalMultipleParams' => [ + 'code' => ' 'PossiblyInvalidArgument', + 'ignored_issues' => ['InvalidReturnType'], + ], + 'callableMissingRequiredMultipleParams' => [ + 'code' => ' 'PossiblyInvalidArgument', + 'ignored_issues' => ['InvalidReturnType'], + ], + 'callableAdditionalRequiredParam' => [ + 'code' => ' 'InvalidArgument', + 'ignored_issues' => ['InvalidReturnType'], + ], + 'callableMultipleParamsWithOptional' => [ + 'code' => ' 'PossiblyInvalidArgument', + 'ignored_issues' => ['InvalidReturnType'], + ], 'preventStringDocblockType' => [ 'code' => ' */ public function map(callable $callback) { From be6028d7d463d6052ed6af96f7b81444a9bf1b19 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 15 Nov 2023 17:12:39 +0100 Subject: [PATCH 080/357] Fix potential cache race conditions/cache not deleted with non-lowercase file paths and add missing docs --- .../Provider/ClassLikeStorageCacheProvider.php | 8 +++++++- .../Internal/Provider/ClassLikeStorageCacheProvider.php | 9 +++++++++ src/Psalm/Internal/Provider/FileStorageCacheProvider.php | 2 +- .../Provider/ClassLikeStorageInstanceCacheProvider.php | 8 +++++++- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/LanguageServer/Provider/ClassLikeStorageCacheProvider.php b/src/Psalm/Internal/LanguageServer/Provider/ClassLikeStorageCacheProvider.php index 1dbc6387d45..9f4ac2fdc81 100644 --- a/src/Psalm/Internal/LanguageServer/Provider/ClassLikeStorageCacheProvider.php +++ b/src/Psalm/Internal/LanguageServer/Provider/ClassLikeStorageCacheProvider.php @@ -13,7 +13,7 @@ */ final class ClassLikeStorageCacheProvider extends InternalClassLikeStorageCacheProvider { - /** @var array */ + /** @var array */ private array $cache = []; public function __construct() @@ -26,6 +26,9 @@ public function writeToCache(ClassLikeStorage $storage, ?string $file_path, ?str $this->cache[$fq_classlike_name_lc] = $storage; } + /** + * @param lowercase-string $fq_classlike_name_lc + */ public function getLatestFromCache( string $fq_classlike_name_lc, ?string $file_path, @@ -40,6 +43,9 @@ public function getLatestFromCache( return $cached_value; } + /** + * @param lowercase-string $fq_classlike_name_lc + */ private function loadFromCache(string $fq_classlike_name_lc): ?ClassLikeStorage { return $this->cache[$fq_classlike_name_lc] ?? null; diff --git a/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php b/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php index 82eb578fecb..2a3a8a0d3e1 100644 --- a/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php +++ b/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php @@ -77,6 +77,9 @@ public function writeToCache(ClassLikeStorage $storage, string $file_path, strin $this->cache->saveItem($cache_location, $storage); } + /** + * @param lowercase-string $fq_classlike_name_lc + */ public function getLatestFromCache( string $fq_classlike_name_lc, ?string $file_path, @@ -108,6 +111,9 @@ private function getCacheHash(?string $_unused_file_path, ?string $file_contents return PHP_VERSION_ID >= 8_01_00 ? hash('xxh128', $data) : hash('md4', $data); } + /** + * @param lowercase-string $fq_classlike_name_lc + */ private function loadFromCache(string $fq_classlike_name_lc, ?string $file_path): ?ClassLikeStorage { $storage = $this->cache->getItem($this->getCacheLocationForClass($fq_classlike_name_lc, $file_path)); @@ -118,6 +124,9 @@ private function loadFromCache(string $fq_classlike_name_lc, ?string $file_path) return null; } + /** + * @param lowercase-string $fq_classlike_name_lc + */ private function getCacheLocationForClass( string $fq_classlike_name_lc, ?string $file_path, diff --git a/src/Psalm/Internal/Provider/FileStorageCacheProvider.php b/src/Psalm/Internal/Provider/FileStorageCacheProvider.php index 9fcb7d9d32b..874feacb339 100644 --- a/src/Psalm/Internal/Provider/FileStorageCacheProvider.php +++ b/src/Psalm/Internal/Provider/FileStorageCacheProvider.php @@ -103,7 +103,7 @@ public function getLatestFromCache(string $file_path, string $file_contents): ?F public function removeCacheForFile(string $file_path): void { - $this->cache->deleteItem($this->getCacheLocationForPath($file_path)); + $this->cache->deleteItem($this->getCacheLocationForPath(strtolower($file_path))); } private function getCacheHash(string $_unused_file_path, string $file_contents): string diff --git a/tests/Internal/Provider/ClassLikeStorageInstanceCacheProvider.php b/tests/Internal/Provider/ClassLikeStorageInstanceCacheProvider.php index 5af1cb945b1..1b81870c9eb 100644 --- a/tests/Internal/Provider/ClassLikeStorageInstanceCacheProvider.php +++ b/tests/Internal/Provider/ClassLikeStorageInstanceCacheProvider.php @@ -10,7 +10,7 @@ class ClassLikeStorageInstanceCacheProvider extends ClassLikeStorageCacheProvider { - /** @var array */ + /** @var array */ private array $cache = []; public function __construct() @@ -23,6 +23,9 @@ public function writeToCache(ClassLikeStorage $storage, ?string $file_path, ?str $this->cache[$fq_classlike_name_lc] = $storage; } + /** + * @param lowercase-string $fq_classlike_name_lc + */ public function getLatestFromCache(string $fq_classlike_name_lc, ?string $file_path, ?string $file_contents): ClassLikeStorage { $cached_value = $this->loadFromCache($fq_classlike_name_lc); @@ -34,6 +37,9 @@ public function getLatestFromCache(string $fq_classlike_name_lc, ?string $file_p return $cached_value; } + /** + * @param lowercase-string $fq_classlike_name_lc + */ private function loadFromCache(string $fq_classlike_name_lc): ?ClassLikeStorage { return $this->cache[$fq_classlike_name_lc] ?? null; From 3299689d99483d181bdf9b3bec5c8448895f4b16 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 15 Nov 2023 21:03:14 +0100 Subject: [PATCH 081/357] revert 804087b of https://github.com/vimeo/psalm/issues/10026 --- tests/Traits/ValidCodeAnalysisTestTrait.php | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/tests/Traits/ValidCodeAnalysisTestTrait.php b/tests/Traits/ValidCodeAnalysisTestTrait.php index b37acced357..7a76481d275 100644 --- a/tests/Traits/ValidCodeAnalysisTestTrait.php +++ b/tests/Traits/ValidCodeAnalysisTestTrait.php @@ -14,7 +14,6 @@ use const PHP_OS; use const PHP_VERSION; -use const PHP_VERSION_ID; trait ValidCodeAnalysisTestTrait { @@ -77,20 +76,6 @@ public function testValidCode( $codebase->enterServerMode(); $codebase->config->visitPreloadedStubFiles($codebase); - // avoid MethodSignatureMismatch for __unserialize/() when extending DateTime - if (PHP_VERSION_ID >= 8_02_00) { - $this->addStubFile( - 'stubOne.phpstub', - 'addFile($file_path, $code); From fd2f87657466fa3e0ec6221ef148ad90605bf43f Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 15 Nov 2023 21:04:58 +0100 Subject: [PATCH 082/357] Remove tests for partially stubbed classes introduced in https://github.com/vimeo/psalm/issues/849, as this didn't work correctly and lead to errors not being reported at all in many cases (since the error was reported for the stubs file and therefore suppressed) With the previous commit "Fix for classes what https://github.com/vimeo/psalm/pull/8503 fixed for functions", the stubs of classes where the actual file is in the analyzed files are ignored completely. --- tests/StubTest.php | 96 ++-------------------------------------------- 1 file changed, 4 insertions(+), 92 deletions(-) diff --git a/tests/StubTest.php b/tests/StubTest.php index 8d92421a1a2..6c835a65d8d 100644 --- a/tests/StubTest.php +++ b/tests/StubTest.php @@ -317,10 +317,10 @@ public function testPhpStormMetaParsingFile(): void 'creAte2("object"); $y2 = (new \Ns\MyClass)->creaTe2("exception"); - + $const1 = (new \Ns\MyClass)->creAte3(\Ns\MyClass::OBJECT); $const2 = (new \Ns\MyClass)->creaTe3("exception"); @@ -1194,94 +1194,6 @@ class Bar extends PartiallyStubbedClass {} $this->analyzeFile($file_path, new Context()); } - public function testStubFileWithPartialClassDefinitionWithCoercion(): void - { - $this->expectExceptionMessage('TypeCoercion'); - $this->expectException(CodeException::class); - $this->project_analyzer = $this->getProjectAnalyzerWithConfig( - TestConfig::loadFromXML( - dirname(__DIR__), - ' - - - - - - - - - ', - ), - ); - - $file_path = getcwd() . '/src/somefile.php'; - - $this->addFile( - $file_path, - 'foo("dasda");', - ); - - $this->analyzeFile($file_path, new Context()); - } - - public function testStubFileWithPartialClassDefinitionGeneralReturnType(): void - { - $this->expectExceptionMessage('InvalidReturnStatement'); - $this->expectException(CodeException::class); - $this->project_analyzer = $this->getProjectAnalyzerWithConfig( - TestConfig::loadFromXML( - dirname(__DIR__), - ' - - - - - - - - - ', - ), - ); - - $file_path = getcwd() . '/src/somefile.php'; - - $this->addFile( - $file_path, - 'analyzeFile($file_path, new Context()); - } - public function testStubFileWithTemplatedClassDefinitionAndMagicMethodOverride(): void { $this->project_analyzer = $this->getProjectAnalyzerWithConfig( From 00bed512afd895300bf5096fbd7d8c0d2621f8be Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 16 Nov 2023 15:57:45 +0100 Subject: [PATCH 083/357] suppress false positive --- .../Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index bbcc2135598..706534fbbe5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -959,6 +959,7 @@ public static function verifyType( && strpos($input_type_part->value, '::') ) { $parts = explode('::', $input_type_part->value); + /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ $potential_method_ids[] = new MethodIdentifier( $parts[0], strtolower($parts[1]), From 2c6aa77baf715c0f6dfc962b40dc2016da5126d7 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sat, 18 Nov 2023 13:31:37 +0100 Subject: [PATCH 084/357] fix CI "split" unevenly splits the number of tests --- bin/tests-github-actions.sh | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/bin/tests-github-actions.sh b/bin/tests-github-actions.sh index f180d9ca6d5..4296f70591a 100755 --- a/bin/tests-github-actions.sh +++ b/bin/tests-github-actions.sh @@ -24,7 +24,23 @@ exit "$exit_code"' mkdir -p build/parallel/ build/phpunit/logs/ find tests -name '*Test.php' | shuf --random-source=<(get_seeded_random) > build/tests_all - split --number="l/$chunk_number/$chunk_count" build/tests_all > build/tests_split + # split incorrectly splits the lines by byte size, which means that the number of tests per file are as evenly distributed as possible + #split --number="l/$chunk_number/$chunk_count" build/tests_all > build/tests_split + local -r lines=$(wc -l build/tests_split + parallel --group -j"$parallel_processes" --rpl {_}\ s/\\//_/g --joblog build/parallel/jobs.log "$phpunit_cmd" < build/tests_split } From 466bda088b5ae4aabe9bbea8885395bc6870d878 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tinjo=20Sch=C3=B6ni?= <32767367+tscni@users.noreply.github.com> Date: Tue, 21 Nov 2023 01:22:02 +0100 Subject: [PATCH 085/357] Fix Phar build failure The issue was likely caused by Composer 2.6.4 making the autoloader generation (more) reproducible (composer/composer#11663) We can either try to change the generated autoloader with an autoloader suffix, or just change the Psalm root directory for the smoke test. The latter approach seems easier. :P --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0c4d5026289..f33bb0fde8c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -50,7 +50,8 @@ jobs: command: bin/build-phar.sh - run: name: Smoke test Phar file - command: build/psalm.phar --version + # Change the root away from the project root to avoid conflicts with the Composer autoloader + command: build/psalm.phar --version --root build - store_artifacts: path: build/psalm.phar - run: From d94f7bd5536d8d3ce93f6bdf65e487caeca0d1bf Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sat, 18 Nov 2023 15:21:01 +0100 Subject: [PATCH 086/357] loose comparisons of int with string shouldn't change change the int to empty-mixed --- src/Psalm/Internal/Type/SimpleAssertionReconciler.php | 4 ++++ tests/TypeReconciliation/ConditionalTest.php | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index f63100c49aa..24041dfaceb 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -1040,6 +1040,10 @@ private static function reconcileString( $string_types[] = $type; } + $redundant = false; + } elseif ($type instanceof TInt && $assertion instanceof IsLooselyEqual) { + // don't change the type of an int for non-strict comparisons + $string_types[] = $type; $redundant = false; } else { $redundant = false; diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index 6b246d2762d..926e2866b1a 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -1350,7 +1350,6 @@ function takes_string(string $string) : void {} function takes_int(int $int) : void {} if ($int == $string) { - /** @psalm-suppress MixedArgument */ takes_int($int); }', ], From 0d7c5a2d8ec12e66cfc3a8f6f1b27d25365d3305 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sat, 18 Nov 2023 11:03:17 +0100 Subject: [PATCH 087/357] Fix docblock mixed escape hatch revert https://github.com/vimeo/psalm/pull/7663 including previous from_docblock Mixed assignments, as the tests required 2 suppressions and created an escape hatch via mixed on higher psalm error levels, where mixed isn't reported, thus hiding potentially fatal bugs. It's still possible to run the validation of docblock docs though: a @var declaration that contains both possible types, to ensure later code won't escape any checks (and no @psalm-suppress needed at all) This is also a required preparation to fix some isset issues of https://github.com/vimeo/psalm/issues/9759 --- .../Type/NegatedAssertionReconciler.php | 4 +- .../Type/SimpleAssertionReconciler.php | 69 +++++-------------- .../Type/SimpleNegatedAssertionReconciler.php | 56 ++++----------- tests/TypeReconciliation/ConditionalTest.php | 2 +- tests/UnusedVariableTest.php | 13 +++- 5 files changed, 43 insertions(+), 101 deletions(-) diff --git a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php index af2b84b2764..88be51f72e1 100644 --- a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php @@ -304,9 +304,7 @@ public static function reconcile( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } return $existing_var_type; diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 24041dfaceb..568d059f7f6 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -40,7 +40,6 @@ use Psalm\Type\Atomic\TCallableString; use Psalm\Type\Atomic\TClassConstant; use Psalm\Type\Atomic\TClassString; -use Psalm\Type\Atomic\TEmptyMixed; use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TFloat; use Psalm\Type\Atomic\TGenericObject; @@ -976,9 +975,7 @@ private static function reconcileHasMethod( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? new Union([new TEmptyMixed()]) - : Type::getNever(); + return Type::getNever(); } /** @@ -1071,9 +1068,7 @@ private static function reconcileString( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? new Union([new TEmptyMixed()]) - : Type::getNever(); + return Type::getNever(); } /** @@ -1165,9 +1160,7 @@ private static function reconcileInt( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? new Union([new TEmptyMixed()]) - : Type::getNever(); + return Type::getNever(); } /** @@ -1244,9 +1237,7 @@ private static function reconcileBool( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1329,9 +1320,7 @@ private static function reconcileFalse( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1414,9 +1403,7 @@ private static function reconcileTrue( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1489,9 +1476,7 @@ private static function reconcileScalar( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1582,9 +1567,7 @@ private static function reconcileNumeric( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1702,9 +1685,7 @@ private static function reconcileObject( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1760,9 +1741,7 @@ private static function reconcileResource( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1832,9 +1811,7 @@ private static function reconcileCountable( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1895,9 +1872,7 @@ private static function reconcileIterable( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1939,9 +1914,7 @@ private static function reconcileInArray( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } return $intersection; @@ -2260,9 +2233,7 @@ private static function reconcileTraversable( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -2375,9 +2346,7 @@ private static function reconcileArray( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -2485,9 +2454,7 @@ private static function reconcileList( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -2725,9 +2692,7 @@ private static function reconcileCallable( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index 06d654ec1c2..4fcdea90979 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -141,9 +141,7 @@ public static function reconcile( } } - return $existing_var_type->from_docblock - ? Type::getNull() - : Type::getNever(); + return Type::getNever(); } return Type::getNull(); @@ -507,9 +505,7 @@ private static function reconcileBool( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -703,9 +699,7 @@ private static function reconcileNull( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -785,9 +779,7 @@ private static function reconcileFalse( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -867,9 +859,7 @@ private static function reconcileTrue( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -924,9 +914,7 @@ private static function reconcileFalsyOrEmpty( $failed_reconciliation = 2; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } if ($redundant) { @@ -1139,9 +1127,7 @@ private static function reconcileScalar( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1240,9 +1226,7 @@ private static function reconcileObject( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1336,9 +1320,7 @@ private static function reconcileNumeric( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1438,9 +1420,7 @@ private static function reconcileInt( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1535,9 +1515,7 @@ private static function reconcileFloat( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1641,9 +1619,7 @@ private static function reconcileString( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1746,9 +1722,7 @@ private static function reconcileArray( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** @@ -1818,9 +1792,7 @@ private static function reconcileResource( $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return $existing_var_type->from_docblock - ? Type::getMixed() - : Type::getNever(); + return Type::getNever(); } /** diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index 926e2866b1a..ca69a70a358 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -80,7 +80,7 @@ function foo($length) { } }', 'assertions' => [], - 'ignored_issues' => ['DocblockTypeContradiction'], + 'ignored_issues' => ['DocblockTypeContradiction', 'TypeDoesNotContainType'], ], 'notInstanceof' => [ 'code' => ' Date: Wed, 22 Nov 2023 11:10:23 +0100 Subject: [PATCH 088/357] Doc typo --- docs/running_psalm/issues/TaintedEval.md | 2 +- docs/running_psalm/issues/TaintedHtml.md | 2 +- docs/running_psalm/issues/TaintedInclude.md | 2 +- docs/running_psalm/issues/TaintedShell.md | 2 +- docs/running_psalm/issues/TaintedSql.md | 2 +- docs/running_psalm/issues/TaintedTextWithQuotes.md | 2 +- docs/running_psalm/issues/TaintedXpath.md | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/running_psalm/issues/TaintedEval.md b/docs/running_psalm/issues/TaintedEval.md index c1bce38b23f..afdf53ceeb0 100644 --- a/docs/running_psalm/issues/TaintedEval.md +++ b/docs/running_psalm/issues/TaintedEval.md @@ -1,6 +1,6 @@ # TaintedEval -Emitted when user-controlled input can be passed into to an `eval` call. +Emitted when user-controlled input can be passed into an `eval` call. Passing untrusted user input to `eval` calls is dangerous, as it allows arbitrary data to be executed on your server. diff --git a/docs/running_psalm/issues/TaintedHtml.md b/docs/running_psalm/issues/TaintedHtml.md index ff8add010c3..012a3121d80 100644 --- a/docs/running_psalm/issues/TaintedHtml.md +++ b/docs/running_psalm/issues/TaintedHtml.md @@ -1,6 +1,6 @@ # TaintedHtml -Emitted when user-controlled input that can contain HTML can be passed into to an `echo` statement. +Emitted when user-controlled input that can contain HTML can be passed into an `echo` statement. ## Risk diff --git a/docs/running_psalm/issues/TaintedInclude.md b/docs/running_psalm/issues/TaintedInclude.md index 929adb8a296..8f088dfcadf 100644 --- a/docs/running_psalm/issues/TaintedInclude.md +++ b/docs/running_psalm/issues/TaintedInclude.md @@ -1,6 +1,6 @@ # TaintedInclude -Emitted when user-controlled input can be passed into to an `include` or `require` expression. +Emitted when user-controlled input can be passed into an `include` or `require` expression. Passing untrusted user input to `include` calls is dangerous, as it can allow an attacker to execute arbitrary scripts on your server. diff --git a/docs/running_psalm/issues/TaintedShell.md b/docs/running_psalm/issues/TaintedShell.md index a91e1f1b188..5f0e2f718bc 100644 --- a/docs/running_psalm/issues/TaintedShell.md +++ b/docs/running_psalm/issues/TaintedShell.md @@ -1,6 +1,6 @@ # TaintedShell -Emitted when user-controlled input can be passed into to an `exec` call or similar. +Emitted when user-controlled input can be passed into an `exec` call or similar. ```php Date: Wed, 22 Nov 2023 15:27:19 +0100 Subject: [PATCH 089/357] Fix circleCI builds --- .circleci/config.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f33bb0fde8c..690e7f45a2e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,6 +11,9 @@ executors: php-81: docker: - image: thecodingmachine/php:8.1-v4-cli + php-82: + docker: + - image: thecodingmachine/php:8.2-v4-cli jobs: "Code Style Analysis": executor: php-74 @@ -67,7 +70,7 @@ jobs: # The resource_class feature allows configuring CPU and RAM resources for each job. Different resource classes are available for different executors. https://circleci.com/docs/2.0/configuration-reference/#resourceclass resource_class: large test-with-real-projects: - executor: php-81 + executor: php-82 steps: - checkout # used here just for the side effect of loading the github public ssh key so we can clone other stuff - attach_workspace: From 29ac774b0183dc698b276b2e923f20272aa7aea9 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 22 Nov 2023 15:35:40 +0100 Subject: [PATCH 090/357] Fix --- bin/test-with-real-projects.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/test-with-real-projects.sh b/bin/test-with-real-projects.sh index 9fd6483b0e6..fdcddae7d67 100755 --- a/bin/test-with-real-projects.sh +++ b/bin/test-with-real-projects.sh @@ -38,6 +38,7 @@ psl) cd endtoend-test-psl git checkout 2.3.x composer install + sed 's/ErrorOutputBehavior::Packed, ErrorOutputBehavior::Discard/ErrorOutputBehavior::Discard/g' -i src/Psl/Shell/execute.php "$PSALM" --monochrome -c config/psalm.xml "$PSALM" --monochrome -c config/psalm.xml tests/static-analysis ;; From ed1ea524c99e42a3e7b151540941275fc2b716b5 Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Wed, 22 Nov 2023 17:19:22 +0100 Subject: [PATCH 091/357] Fix #10398: add missing methods of ReflectionProperty class in CallMap and CallMap deltas. --- dictionaries/CallMap.php | 5 +++++ dictionaries/CallMap_74_delta.php | 1 + dictionaries/CallMap_80_delta.php | 7 +++++++ dictionaries/CallMap_81_delta.php | 1 + 4 files changed, 14 insertions(+) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index ccefe6a0b6f..bb5252454a7 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -10636,16 +10636,21 @@ 'ReflectionProperty::__toString' => ['string'], 'ReflectionProperty::getAttributes' => ['list', 'name='=>'?string', 'flags='=>'int'], 'ReflectionProperty::getDeclaringClass' => ['ReflectionClass'], +'ReflectionProperty::getDefaultValue' => ['mixed'], 'ReflectionProperty::getDocComment' => ['string|false'], 'ReflectionProperty::getModifiers' => ['int'], 'ReflectionProperty::getName' => ['string'], 'ReflectionProperty::getType' => ['?ReflectionType'], 'ReflectionProperty::getValue' => ['mixed', 'object='=>'null|object'], +'ReflectionProperty::hasDefaultValue' => ['bool'], 'ReflectionProperty::hasType' => ['bool'], 'ReflectionProperty::isDefault' => ['bool'], +'ReflectionProperty::isInitialized' => ['bool', 'object='=>'null|object'], 'ReflectionProperty::isPrivate' => ['bool'], +'ReflectionProperty::isPromoted' => ['bool'], 'ReflectionProperty::isProtected' => ['bool'], 'ReflectionProperty::isPublic' => ['bool'], +'ReflectionProperty::isReadonly' => ['bool'], 'ReflectionProperty::isStatic' => ['bool'], 'ReflectionProperty::setAccessible' => ['void', 'accessible'=>'bool'], 'ReflectionProperty::setValue' => ['void', 'object'=>'null|object', 'value'=>''], diff --git a/dictionaries/CallMap_74_delta.php b/dictionaries/CallMap_74_delta.php index 87872004a22..9fdb508aebb 100644 --- a/dictionaries/CallMap_74_delta.php +++ b/dictionaries/CallMap_74_delta.php @@ -17,6 +17,7 @@ return [ 'added' => [ 'ReflectionProperty::getType' => ['?ReflectionType'], + 'ReflectionProperty::isInitialized' => ['bool', 'object'=>'object'], 'mb_str_split' => ['list|false', 'string'=>'string', 'length='=>'positive-int', 'encoding='=>'string'], 'openssl_x509_verify' => ['int', 'certificate'=>'string|resource', 'public_key'=>'string|array|resource'], ], diff --git a/dictionaries/CallMap_80_delta.php b/dictionaries/CallMap_80_delta.php index 29227b632bc..62e6cd7e054 100644 --- a/dictionaries/CallMap_80_delta.php +++ b/dictionaries/CallMap_80_delta.php @@ -27,6 +27,9 @@ 'ReflectionFunctionAbstract::getAttributes' => ['list', 'name='=>'?string', 'flags='=>'int'], 'ReflectionParameter::getAttributes' => ['list', 'name='=>'?string', 'flags='=>'int'], 'ReflectionProperty::getAttributes' => ['list', 'name='=>'?string', 'flags='=>'int'], + 'ReflectionProperty::getDefaultValue' => ['mixed'], + 'ReflectionProperty::hasDefaultValue' => ['bool'], + 'ReflectionProperty::isPromoted' => ['bool'], 'ReflectionUnionType::getTypes' => ['list'], 'SplFixedArray::getIterator' => ['Iterator'], 'WeakMap::count' => ['int'], @@ -424,6 +427,10 @@ 'old' => ['mixed', 'object='=>'object'], 'new' => ['mixed', 'object='=>'null|object'], ], + 'ReflectionProperty::isInitialized' => [ + 'old' => ['bool', 'object'=>'object'], + 'new' => ['bool', 'object='=>'null|object'], + ], 'SplFileInfo::getFileInfo' => [ 'old' => ['SplFileInfo', 'class='=>'class-string'], 'new' => ['SplFileInfo', 'class='=>'?class-string'], diff --git a/dictionaries/CallMap_81_delta.php b/dictionaries/CallMap_81_delta.php index 18cc1ae4792..bfb2da40bb6 100644 --- a/dictionaries/CallMap_81_delta.php +++ b/dictionaries/CallMap_81_delta.php @@ -52,6 +52,7 @@ 'ReflectionFunctionAbstract::hasTentativeReturnType' => ['bool'], 'ReflectionFunctionAbstract::isStatic' => ['bool'], 'ReflectionObject::isEnum' => ['bool'], + 'ReflectionProperty::isReadonly' => ['bool'], 'sodium_crypto_stream_xchacha20' => ['non-empty-string', 'length'=>'positive-int', 'nonce'=>'non-empty-string', 'key'=>'non-empty-string'], 'sodium_crypto_stream_xchacha20_keygen' => ['non-empty-string'], 'sodium_crypto_stream_xchacha20_xor' => ['string', 'message'=>'string', 'nonce'=>'non-empty-string', 'key'=>'non-empty-string'], From acf92537c19adc0b61d516e9050cca30565f2eb3 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Thu, 23 Nov 2023 10:41:54 +0100 Subject: [PATCH 092/357] Update MongoDB stubs for 1.17 --- dictionaries/CallMap.php | 60 +++++++++++++++++------------ dictionaries/CallMap_historical.php | 60 +++++++++++++++++------------ 2 files changed, 70 insertions(+), 50 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index a532c46e817..4e7166ec105 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -7060,19 +7060,19 @@ 'MongoDB\BSON\Binary::getType' => ['int'], 'MongoDB\BSON\Binary::__toString' => ['string'], 'MongoDB\BSON\Binary::serialize' => ['string'], -'MongoDB\BSON\Binary::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Binary::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Binary::jsonSerialize' => ['mixed'], 'MongoDB\BSON\BinaryInterface::getData' => ['string'], 'MongoDB\BSON\BinaryInterface::getType' => ['int'], 'MongoDB\BSON\BinaryInterface::__toString' => ['string'], 'MongoDB\BSON\DBPointer::__toString' => ['string'], 'MongoDB\BSON\DBPointer::serialize' => ['string'], -'MongoDB\BSON\DBPointer::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\DBPointer::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\DBPointer::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Decimal128::__construct' => ['void', 'value' => 'string'], 'MongoDB\BSON\Decimal128::__toString' => ['string'], 'MongoDB\BSON\Decimal128::serialize' => ['string'], -'MongoDB\BSON\Decimal128::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Decimal128::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Decimal128::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Decimal128Interface::__toString' => ['string'], 'MongoDB\BSON\Document::fromBSON' => ['MongoDB\BSON\Document', 'bson' => 'string'], @@ -7084,13 +7084,17 @@ 'MongoDB\BSON\Document::toPHP' => ['object|array', 'typeMap=' => '?array'], 'MongoDB\BSON\Document::toCanonicalExtendedJSON' => ['string'], 'MongoDB\BSON\Document::toRelaxedExtendedJSON' => ['string'], +'MongoDB\BSON\Document::offsetExists' => ['bool', 'offset' => 'mixed'], +'MongoDB\BSON\Document::offsetGet' => ['mixed', 'offset' => 'mixed'], +'MongoDB\BSON\Document::offsetSet' => ['void', 'offset' => 'mixed', 'value' => 'mixed'], +'MongoDB\BSON\Document::offsetUnset' => ['void', 'offset' => 'mixed'], 'MongoDB\BSON\Document::__toString' => ['string'], 'MongoDB\BSON\Document::serialize' => ['string'], -'MongoDB\BSON\Document::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Document::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Int64::__construct' => ['void', 'value' => 'string|int'], 'MongoDB\BSON\Int64::__toString' => ['string'], 'MongoDB\BSON\Int64::serialize' => ['string'], -'MongoDB\BSON\Int64::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Int64::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Int64::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Iterator::current' => ['mixed'], 'MongoDB\BSON\Iterator::key' => ['string|int'], @@ -7102,22 +7106,22 @@ 'MongoDB\BSON\Javascript::getScope' => ['?object'], 'MongoDB\BSON\Javascript::__toString' => ['string'], 'MongoDB\BSON\Javascript::serialize' => ['string'], -'MongoDB\BSON\Javascript::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Javascript::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Javascript::jsonSerialize' => ['mixed'], 'MongoDB\BSON\JavascriptInterface::getCode' => ['string'], 'MongoDB\BSON\JavascriptInterface::getScope' => ['?object'], 'MongoDB\BSON\JavascriptInterface::__toString' => ['string'], 'MongoDB\BSON\MaxKey::serialize' => ['string'], -'MongoDB\BSON\MaxKey::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\MaxKey::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\MaxKey::jsonSerialize' => ['mixed'], 'MongoDB\BSON\MinKey::serialize' => ['string'], -'MongoDB\BSON\MinKey::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\MinKey::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\MinKey::jsonSerialize' => ['mixed'], 'MongoDB\BSON\ObjectId::__construct' => ['void', 'id=' => '?string'], 'MongoDB\BSON\ObjectId::getTimestamp' => ['int'], 'MongoDB\BSON\ObjectId::__toString' => ['string'], 'MongoDB\BSON\ObjectId::serialize' => ['string'], -'MongoDB\BSON\ObjectId::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\ObjectId::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\ObjectId::jsonSerialize' => ['mixed'], 'MongoDB\BSON\ObjectIdInterface::getTimestamp' => ['int'], 'MongoDB\BSON\ObjectIdInterface::__toString' => ['string'], @@ -7126,30 +7130,35 @@ 'MongoDB\BSON\PackedArray::getIterator' => ['MongoDB\BSON\Iterator'], 'MongoDB\BSON\PackedArray::has' => ['bool', 'index' => 'int'], 'MongoDB\BSON\PackedArray::toPHP' => ['object|array', 'typeMap=' => '?array'], +'MongoDB\BSON\PackedArray::offsetExists' => ['bool', 'offset' => 'mixed'], +'MongoDB\BSON\PackedArray::offsetGet' => ['mixed', 'offset' => 'mixed'], +'MongoDB\BSON\PackedArray::offsetSet' => ['void', 'offset' => 'mixed', 'value' => 'mixed'], +'MongoDB\BSON\PackedArray::offsetUnset' => ['void', 'offset' => 'mixed'], 'MongoDB\BSON\PackedArray::__toString' => ['string'], 'MongoDB\BSON\PackedArray::serialize' => ['string'], -'MongoDB\BSON\PackedArray::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\PackedArray::unserialize' => ['void', 'data' => 'string'], +'MongoDB\BSON\Persistable::bsonSerialize' => ['stdClass|MongoDB\BSON\Document|array'], 'MongoDB\BSON\Regex::__construct' => ['void', 'pattern' => 'string', 'flags=' => 'string'], 'MongoDB\BSON\Regex::getPattern' => ['string'], 'MongoDB\BSON\Regex::getFlags' => ['string'], 'MongoDB\BSON\Regex::__toString' => ['string'], 'MongoDB\BSON\Regex::serialize' => ['string'], -'MongoDB\BSON\Regex::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Regex::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Regex::jsonSerialize' => ['mixed'], 'MongoDB\BSON\RegexInterface::getPattern' => ['string'], 'MongoDB\BSON\RegexInterface::getFlags' => ['string'], 'MongoDB\BSON\RegexInterface::__toString' => ['string'], -'MongoDB\BSON\Serializable::bsonSerialize' => ['object|array'], +'MongoDB\BSON\Serializable::bsonSerialize' => ['stdClass|MongoDB\BSON\Document|MongoDB\BSON\PackedArray|array'], 'MongoDB\BSON\Symbol::__toString' => ['string'], 'MongoDB\BSON\Symbol::serialize' => ['string'], -'MongoDB\BSON\Symbol::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Symbol::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Symbol::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Timestamp::__construct' => ['void', 'increment' => 'string|int', 'timestamp' => 'string|int'], 'MongoDB\BSON\Timestamp::getTimestamp' => ['int'], 'MongoDB\BSON\Timestamp::getIncrement' => ['int'], 'MongoDB\BSON\Timestamp::__toString' => ['string'], 'MongoDB\BSON\Timestamp::serialize' => ['string'], -'MongoDB\BSON\Timestamp::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Timestamp::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Timestamp::jsonSerialize' => ['mixed'], 'MongoDB\BSON\TimestampInterface::getTimestamp' => ['int'], 'MongoDB\BSON\TimestampInterface::getIncrement' => ['int'], @@ -7158,13 +7167,13 @@ 'MongoDB\BSON\UTCDateTime::toDateTime' => ['DateTime'], 'MongoDB\BSON\UTCDateTime::__toString' => ['string'], 'MongoDB\BSON\UTCDateTime::serialize' => ['string'], -'MongoDB\BSON\UTCDateTime::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\UTCDateTime::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\UTCDateTime::jsonSerialize' => ['mixed'], 'MongoDB\BSON\UTCDateTimeInterface::toDateTime' => ['DateTime'], 'MongoDB\BSON\UTCDateTimeInterface::__toString' => ['string'], 'MongoDB\BSON\Undefined::__toString' => ['string'], 'MongoDB\BSON\Undefined::serialize' => ['string'], -'MongoDB\BSON\Undefined::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\BSON\Undefined::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Undefined::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Unserializable::bsonUnserialize' => ['void', 'data' => 'array'], 'MongoDB\Driver\BulkWrite::__construct' => ['void', 'options=' => '?array'], @@ -7197,7 +7206,7 @@ 'MongoDB\Driver\Cursor::valid' => ['bool'], 'MongoDB\Driver\CursorId::__toString' => ['string'], 'MongoDB\Driver\CursorId::serialize' => ['string'], -'MongoDB\Driver\CursorId::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\Driver\CursorId::unserialize' => ['void', 'data' => 'string'], 'MongoDB\Driver\CursorInterface::getId' => ['MongoDB\Driver\CursorId'], 'MongoDB\Driver\CursorInterface::getServer' => ['MongoDB\Driver\Server'], 'MongoDB\Driver\CursorInterface::isDead' => ['bool'], @@ -7266,6 +7275,7 @@ 'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServer' => ['MongoDB\Driver\Server'], 'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], 'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServerConnectionId' => ['?int'], +'MongoDB\Driver\Monitoring\LogSubscriber::log' => ['void', 'level' => 'int', 'domain' => 'string', 'message' => 'string'], 'MongoDB\Driver\Monitoring\SDAMSubscriber::serverChanged' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerChangedEvent'], 'MongoDB\Driver\Monitoring\SDAMSubscriber::serverClosed' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerClosedEvent'], 'MongoDB\Driver\Monitoring\SDAMSubscriber::serverOpening' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerOpeningEvent'], @@ -7308,18 +7318,18 @@ 'MongoDB\Driver\ReadConcern::__construct' => ['void', 'level=' => '?string'], 'MongoDB\Driver\ReadConcern::getLevel' => ['?string'], 'MongoDB\Driver\ReadConcern::isDefault' => ['bool'], -'MongoDB\Driver\ReadConcern::bsonSerialize' => ['object|array'], +'MongoDB\Driver\ReadConcern::bsonSerialize' => ['stdClass'], 'MongoDB\Driver\ReadConcern::serialize' => ['string'], -'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'data' => 'string'], 'MongoDB\Driver\ReadPreference::__construct' => ['void', 'mode' => 'string|int', 'tagSets=' => '?array', 'options=' => '?array'], 'MongoDB\Driver\ReadPreference::getHedge' => ['?object'], 'MongoDB\Driver\ReadPreference::getMaxStalenessSeconds' => ['int'], 'MongoDB\Driver\ReadPreference::getMode' => ['int'], 'MongoDB\Driver\ReadPreference::getModeString' => ['string'], 'MongoDB\Driver\ReadPreference::getTagSets' => ['array'], -'MongoDB\Driver\ReadPreference::bsonSerialize' => ['object|array'], +'MongoDB\Driver\ReadPreference::bsonSerialize' => ['stdClass'], 'MongoDB\Driver\ReadPreference::serialize' => ['string'], -'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'data' => 'string'], 'MongoDB\Driver\Server::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace' => 'string', 'bulkWrite' => 'MongoDB\Driver\BulkWrite', 'options=' => 'MongoDB\Driver\WriteConcern|array|null'], 'MongoDB\Driver\Server::executeCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], 'MongoDB\Driver\Server::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace' => 'string', 'query' => 'MongoDB\Driver\Query', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], @@ -7339,9 +7349,9 @@ 'MongoDB\Driver\Server::isPrimary' => ['bool'], 'MongoDB\Driver\Server::isSecondary' => ['bool'], 'MongoDB\Driver\ServerApi::__construct' => ['void', 'version' => 'string', 'strict=' => '?bool', 'deprecationErrors=' => '?bool'], -'MongoDB\Driver\ServerApi::bsonSerialize' => ['object|array'], +'MongoDB\Driver\ServerApi::bsonSerialize' => ['stdClass'], 'MongoDB\Driver\ServerApi::serialize' => ['string'], -'MongoDB\Driver\ServerApi::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\Driver\ServerApi::unserialize' => ['void', 'data' => 'string'], 'MongoDB\Driver\ServerDescription::getHelloResponse' => ['array'], 'MongoDB\Driver\ServerDescription::getHost' => ['string'], 'MongoDB\Driver\ServerDescription::getLastUpdateTime' => ['int'], @@ -7371,9 +7381,9 @@ 'MongoDB\Driver\WriteConcern::getW' => ['string|int|null'], 'MongoDB\Driver\WriteConcern::getWtimeout' => ['int'], 'MongoDB\Driver\WriteConcern::isDefault' => ['bool'], -'MongoDB\Driver\WriteConcern::bsonSerialize' => ['object|array'], +'MongoDB\Driver\WriteConcern::bsonSerialize' => ['stdClass'], 'MongoDB\Driver\WriteConcern::serialize' => ['string'], -'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'serialized' => 'string'], +'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'data' => 'string'], 'MongoDB\Driver\WriteConcernError::getCode' => ['int'], 'MongoDB\Driver\WriteConcernError::getInfo' => ['?object'], 'MongoDB\Driver\WriteConcernError::getMessage' => ['string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 649f393bea7..afb21ba72df 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -3744,19 +3744,19 @@ 'MongoDB\BSON\Binary::getType' => ['int'], 'MongoDB\BSON\Binary::__toString' => ['string'], 'MongoDB\BSON\Binary::serialize' => ['string'], - 'MongoDB\BSON\Binary::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Binary::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Binary::jsonSerialize' => ['mixed'], 'MongoDB\BSON\BinaryInterface::getData' => ['string'], 'MongoDB\BSON\BinaryInterface::getType' => ['int'], 'MongoDB\BSON\BinaryInterface::__toString' => ['string'], 'MongoDB\BSON\DBPointer::__toString' => ['string'], 'MongoDB\BSON\DBPointer::serialize' => ['string'], - 'MongoDB\BSON\DBPointer::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\DBPointer::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\DBPointer::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Decimal128::__construct' => ['void', 'value' => 'string'], 'MongoDB\BSON\Decimal128::__toString' => ['string'], 'MongoDB\BSON\Decimal128::serialize' => ['string'], - 'MongoDB\BSON\Decimal128::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Decimal128::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Decimal128::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Decimal128Interface::__toString' => ['string'], 'MongoDB\BSON\Document::fromBSON' => ['MongoDB\BSON\Document', 'bson' => 'string'], @@ -3768,13 +3768,17 @@ 'MongoDB\BSON\Document::toPHP' => ['object|array', 'typeMap=' => '?array'], 'MongoDB\BSON\Document::toCanonicalExtendedJSON' => ['string'], 'MongoDB\BSON\Document::toRelaxedExtendedJSON' => ['string'], + 'MongoDB\BSON\Document::offsetExists' => ['bool', 'offset' => 'mixed'], + 'MongoDB\BSON\Document::offsetGet' => ['mixed', 'offset' => 'mixed'], + 'MongoDB\BSON\Document::offsetSet' => ['void', 'offset' => 'mixed', 'value' => 'mixed'], + 'MongoDB\BSON\Document::offsetUnset' => ['void', 'offset' => 'mixed'], 'MongoDB\BSON\Document::__toString' => ['string'], 'MongoDB\BSON\Document::serialize' => ['string'], - 'MongoDB\BSON\Document::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Document::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Int64::__construct' => ['void', 'value' => 'string|int'], 'MongoDB\BSON\Int64::__toString' => ['string'], 'MongoDB\BSON\Int64::serialize' => ['string'], - 'MongoDB\BSON\Int64::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Int64::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Int64::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Iterator::current' => ['mixed'], 'MongoDB\BSON\Iterator::key' => ['string|int'], @@ -3786,22 +3790,22 @@ 'MongoDB\BSON\Javascript::getScope' => ['?object'], 'MongoDB\BSON\Javascript::__toString' => ['string'], 'MongoDB\BSON\Javascript::serialize' => ['string'], - 'MongoDB\BSON\Javascript::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Javascript::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Javascript::jsonSerialize' => ['mixed'], 'MongoDB\BSON\JavascriptInterface::getCode' => ['string'], 'MongoDB\BSON\JavascriptInterface::getScope' => ['?object'], 'MongoDB\BSON\JavascriptInterface::__toString' => ['string'], 'MongoDB\BSON\MaxKey::serialize' => ['string'], - 'MongoDB\BSON\MaxKey::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\MaxKey::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\MaxKey::jsonSerialize' => ['mixed'], 'MongoDB\BSON\MinKey::serialize' => ['string'], - 'MongoDB\BSON\MinKey::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\MinKey::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\MinKey::jsonSerialize' => ['mixed'], 'MongoDB\BSON\ObjectId::__construct' => ['void', 'id=' => '?string'], 'MongoDB\BSON\ObjectId::getTimestamp' => ['int'], 'MongoDB\BSON\ObjectId::__toString' => ['string'], 'MongoDB\BSON\ObjectId::serialize' => ['string'], - 'MongoDB\BSON\ObjectId::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\ObjectId::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\ObjectId::jsonSerialize' => ['mixed'], 'MongoDB\BSON\ObjectIdInterface::getTimestamp' => ['int'], 'MongoDB\BSON\ObjectIdInterface::__toString' => ['string'], @@ -3810,30 +3814,35 @@ 'MongoDB\BSON\PackedArray::getIterator' => ['MongoDB\BSON\Iterator'], 'MongoDB\BSON\PackedArray::has' => ['bool', 'index' => 'int'], 'MongoDB\BSON\PackedArray::toPHP' => ['object|array', 'typeMap=' => '?array'], + 'MongoDB\BSON\PackedArray::offsetExists' => ['bool', 'offset' => 'mixed'], + 'MongoDB\BSON\PackedArray::offsetGet' => ['mixed', 'offset' => 'mixed'], + 'MongoDB\BSON\PackedArray::offsetSet' => ['void', 'offset' => 'mixed', 'value' => 'mixed'], + 'MongoDB\BSON\PackedArray::offsetUnset' => ['void', 'offset' => 'mixed'], 'MongoDB\BSON\PackedArray::__toString' => ['string'], 'MongoDB\BSON\PackedArray::serialize' => ['string'], - 'MongoDB\BSON\PackedArray::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\PackedArray::unserialize' => ['void', 'data' => 'string'], + 'MongoDB\BSON\Persistable::bsonSerialize' => ['stdClass|MongoDB\BSON\Document|array'], 'MongoDB\BSON\Regex::__construct' => ['void', 'pattern' => 'string', 'flags=' => 'string'], 'MongoDB\BSON\Regex::getPattern' => ['string'], 'MongoDB\BSON\Regex::getFlags' => ['string'], 'MongoDB\BSON\Regex::__toString' => ['string'], 'MongoDB\BSON\Regex::serialize' => ['string'], - 'MongoDB\BSON\Regex::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Regex::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Regex::jsonSerialize' => ['mixed'], 'MongoDB\BSON\RegexInterface::getPattern' => ['string'], 'MongoDB\BSON\RegexInterface::getFlags' => ['string'], 'MongoDB\BSON\RegexInterface::__toString' => ['string'], - 'MongoDB\BSON\Serializable::bsonSerialize' => ['object|array'], + 'MongoDB\BSON\Serializable::bsonSerialize' => ['stdClass|MongoDB\BSON\Document|MongoDB\BSON\PackedArray|array'], 'MongoDB\BSON\Symbol::__toString' => ['string'], 'MongoDB\BSON\Symbol::serialize' => ['string'], - 'MongoDB\BSON\Symbol::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Symbol::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Symbol::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Timestamp::__construct' => ['void', 'increment' => 'string|int', 'timestamp' => 'string|int'], 'MongoDB\BSON\Timestamp::getTimestamp' => ['int'], 'MongoDB\BSON\Timestamp::getIncrement' => ['int'], 'MongoDB\BSON\Timestamp::__toString' => ['string'], 'MongoDB\BSON\Timestamp::serialize' => ['string'], - 'MongoDB\BSON\Timestamp::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Timestamp::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Timestamp::jsonSerialize' => ['mixed'], 'MongoDB\BSON\TimestampInterface::getTimestamp' => ['int'], 'MongoDB\BSON\TimestampInterface::getIncrement' => ['int'], @@ -3842,13 +3851,13 @@ 'MongoDB\BSON\UTCDateTime::toDateTime' => ['DateTime'], 'MongoDB\BSON\UTCDateTime::__toString' => ['string'], 'MongoDB\BSON\UTCDateTime::serialize' => ['string'], - 'MongoDB\BSON\UTCDateTime::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\UTCDateTime::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\UTCDateTime::jsonSerialize' => ['mixed'], 'MongoDB\BSON\UTCDateTimeInterface::toDateTime' => ['DateTime'], 'MongoDB\BSON\UTCDateTimeInterface::__toString' => ['string'], 'MongoDB\BSON\Undefined::__toString' => ['string'], 'MongoDB\BSON\Undefined::serialize' => ['string'], - 'MongoDB\BSON\Undefined::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\BSON\Undefined::unserialize' => ['void', 'data' => 'string'], 'MongoDB\BSON\Undefined::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Unserializable::bsonUnserialize' => ['void', 'data' => 'array'], 'MongoDB\Driver\BulkWrite::__construct' => ['void', 'options=' => '?array'], @@ -3881,7 +3890,7 @@ 'MongoDB\Driver\Cursor::valid' => ['bool'], 'MongoDB\Driver\CursorId::__toString' => ['string'], 'MongoDB\Driver\CursorId::serialize' => ['string'], - 'MongoDB\Driver\CursorId::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\Driver\CursorId::unserialize' => ['void', 'data' => 'string'], 'MongoDB\Driver\CursorInterface::getId' => ['MongoDB\Driver\CursorId'], 'MongoDB\Driver\CursorInterface::getServer' => ['MongoDB\Driver\Server'], 'MongoDB\Driver\CursorInterface::isDead' => ['bool'], @@ -3950,6 +3959,7 @@ 'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServer' => ['MongoDB\Driver\Server'], 'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], 'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServerConnectionId' => ['?int'], + 'MongoDB\Driver\Monitoring\LogSubscriber::log' => ['void', 'level' => 'int', 'domain' => 'string', 'message' => 'string'], 'MongoDB\Driver\Monitoring\SDAMSubscriber::serverChanged' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerChangedEvent'], 'MongoDB\Driver\Monitoring\SDAMSubscriber::serverClosed' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerClosedEvent'], 'MongoDB\Driver\Monitoring\SDAMSubscriber::serverOpening' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerOpeningEvent'], @@ -3992,18 +4002,18 @@ 'MongoDB\Driver\ReadConcern::__construct' => ['void', 'level=' => '?string'], 'MongoDB\Driver\ReadConcern::getLevel' => ['?string'], 'MongoDB\Driver\ReadConcern::isDefault' => ['bool'], - 'MongoDB\Driver\ReadConcern::bsonSerialize' => ['object|array'], + 'MongoDB\Driver\ReadConcern::bsonSerialize' => ['stdClass'], 'MongoDB\Driver\ReadConcern::serialize' => ['string'], - 'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'data' => 'string'], 'MongoDB\Driver\ReadPreference::__construct' => ['void', 'mode' => 'string|int', 'tagSets=' => '?array', 'options=' => '?array'], 'MongoDB\Driver\ReadPreference::getHedge' => ['?object'], 'MongoDB\Driver\ReadPreference::getMaxStalenessSeconds' => ['int'], 'MongoDB\Driver\ReadPreference::getMode' => ['int'], 'MongoDB\Driver\ReadPreference::getModeString' => ['string'], 'MongoDB\Driver\ReadPreference::getTagSets' => ['array'], - 'MongoDB\Driver\ReadPreference::bsonSerialize' => ['object|array'], + 'MongoDB\Driver\ReadPreference::bsonSerialize' => ['stdClass'], 'MongoDB\Driver\ReadPreference::serialize' => ['string'], - 'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'data' => 'string'], 'MongoDB\Driver\Server::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace' => 'string', 'bulkWrite' => 'MongoDB\Driver\BulkWrite', 'options=' => 'MongoDB\Driver\WriteConcern|array|null'], 'MongoDB\Driver\Server::executeCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], 'MongoDB\Driver\Server::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace' => 'string', 'query' => 'MongoDB\Driver\Query', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], @@ -4023,9 +4033,9 @@ 'MongoDB\Driver\Server::isPrimary' => ['bool'], 'MongoDB\Driver\Server::isSecondary' => ['bool'], 'MongoDB\Driver\ServerApi::__construct' => ['void', 'version' => 'string', 'strict=' => '?bool', 'deprecationErrors=' => '?bool'], - 'MongoDB\Driver\ServerApi::bsonSerialize' => ['object|array'], + 'MongoDB\Driver\ServerApi::bsonSerialize' => ['stdClass'], 'MongoDB\Driver\ServerApi::serialize' => ['string'], - 'MongoDB\Driver\ServerApi::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\Driver\ServerApi::unserialize' => ['void', 'data' => 'string'], 'MongoDB\Driver\ServerDescription::getHelloResponse' => ['array'], 'MongoDB\Driver\ServerDescription::getHost' => ['string'], 'MongoDB\Driver\ServerDescription::getLastUpdateTime' => ['int'], @@ -4055,9 +4065,9 @@ 'MongoDB\Driver\WriteConcern::getW' => ['string|int|null'], 'MongoDB\Driver\WriteConcern::getWtimeout' => ['int'], 'MongoDB\Driver\WriteConcern::isDefault' => ['bool'], - 'MongoDB\Driver\WriteConcern::bsonSerialize' => ['object|array'], + 'MongoDB\Driver\WriteConcern::bsonSerialize' => ['stdClass'], 'MongoDB\Driver\WriteConcern::serialize' => ['string'], - 'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'serialized' => 'string'], + 'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'data' => 'string'], 'MongoDB\Driver\WriteConcernError::getCode' => ['int'], 'MongoDB\Driver\WriteConcernError::getInfo' => ['?object'], 'MongoDB\Driver\WriteConcernError::getMessage' => ['string'], From 461ff956c5d3023c25a39c3179bb7a0144b46336 Mon Sep 17 00:00:00 2001 From: Andrew Nagy <564256+tm1000@users.noreply.github.com> Date: Sat, 25 Nov 2023 15:20:44 -0800 Subject: [PATCH 093/357] Fixes #10397 Explicitly state that we do not support document highlighting. --- src/Psalm/Internal/LanguageServer/LanguageServer.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 4b10f55ef27..3a0ff9a1e02 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -507,6 +507,11 @@ function () use ($workDoneToken) { * Support "Hover" */ $serverCapabilities->hoverProvider = true; + /** + * The server does not support documentHighlight-ing + * Ref: https://github.com/vimeo/psalm/issues/10397 + */ + $serverCapabilities->documentHighlightProvider = false; /** * The server provides completion support. From 35f194e9e8ab74000a7fcb066589d21e35dee393 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 26 Nov 2023 13:12:11 +0100 Subject: [PATCH 094/357] Fix backtick analysis --- .../Statements/ExpressionAnalyzer.php | 97 ++++--------------- tests/ExpressionTest.php | 8 ++ tests/TaintTest.php | 16 +++ 3 files changed, 41 insertions(+), 80 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php index 277787b3ef7..063277aaff6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php @@ -13,7 +13,6 @@ use Psalm\Internal\Analyzer\Statements\Expression\BinaryOpAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\BitwiseNotAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\BooleanNotAnalyzer; -use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\Call\FunctionCallAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\Call\NewAnalyzer; @@ -43,20 +42,18 @@ use Psalm\Internal\Analyzer\Statements\Expression\YieldAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\YieldFromAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; -use Psalm\Internal\Codebase\TaintFlowGraph; -use Psalm\Internal\DataFlow\DataFlowNode; -use Psalm\Internal\DataFlow\TaintSink; use Psalm\Internal\FileManipulation\FileManipulationBuffer; use Psalm\Internal\Type\TemplateResult; -use Psalm\Issue\ForbiddenCode; use Psalm\Issue\UnrecognizedExpression; use Psalm\Issue\UnsupportedReferenceUsage; use Psalm\IssueBuffer; +use Psalm\Node\Expr\VirtualFuncCall; +use Psalm\Node\Scalar\VirtualEncapsed; +use Psalm\Node\VirtualArg; +use Psalm\Node\VirtualName; use Psalm\Plugin\EventHandler\Event\AfterExpressionAnalysisEvent; use Psalm\Plugin\EventHandler\Event\BeforeExpressionAnalysisEvent; -use Psalm\Storage\FunctionLikeParameter; use Psalm\Type; -use Psalm\Type\TaintKind; use function get_class; use function in_array; @@ -378,80 +375,20 @@ private static function handleExpression( } if ($stmt instanceof PhpParser\Node\Expr\ShellExec) { - if ($statements_analyzer->data_flow_graph) { - $call_location = new CodeLocation($statements_analyzer->getSource(), $stmt); - - if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { - $sink = TaintSink::getForMethodArgument( - 'shell_exec', - 'shell_exec', - 0, - null, - $call_location, - ); - - $sink->taints = [TaintKind::INPUT_SHELL]; - - $statements_analyzer->data_flow_graph->addSink($sink); - } - - foreach ($stmt->parts as $part) { - if ($part instanceof PhpParser\Node\Expr\Variable) { - if (self::analyze($statements_analyzer, $part, $context) === false) { - break; - } - - $expr_type = $statements_analyzer->node_data->getType($part); - if ($expr_type === null) { - break; - } - - $shell_exec_param = new FunctionLikeParameter( - 'var', - false, - ); - - if (ArgumentAnalyzer::verifyType( - $statements_analyzer, - $expr_type, - Type::getString(), - null, - 'shell_exec', - null, - 0, - $call_location, - $stmt, - $context, - $shell_exec_param, - false, - null, - true, - true, - new CodeLocation($statements_analyzer, $stmt), - ) === false) { - return false; - } - - foreach ($expr_type->parent_nodes as $parent_node) { - $statements_analyzer->data_flow_graph->addPath( - $parent_node, - new DataFlowNode('variable-use', 'variable use', null), - 'variable-use', - ); - } - } - } - } - - IssueBuffer::maybeAdd( - new ForbiddenCode( - 'Use of shell_exec', - new CodeLocation($statements_analyzer->getSource(), $stmt), - ), - $statements_analyzer->getSuppressedIssues(), + $concat = new VirtualEncapsed($stmt->parts, $stmt->getAttributes()); + $virtual_call = new VirtualFuncCall(new VirtualName(['shell_exec']), [ + new VirtualArg($concat), + ], $stmt->getAttributes()); + return self::handleExpression( + $statements_analyzer, + $virtual_call, + $context, + $array_assignment, + $global_context, + $from_stmt, + $template_result, + $assigned_to_reference, ); - - return true; } if ($stmt instanceof PhpParser\Node\Expr\Print_) { diff --git a/tests/ExpressionTest.php b/tests/ExpressionTest.php index 57186daf99d..97c57c9db96 100644 --- a/tests/ExpressionTest.php +++ b/tests/ExpressionTest.php @@ -51,6 +51,14 @@ public function providerValidCodeParse(): iterable '$a===' => 'array{9223372036854775806: 0, 9223372036854775807: 1}', ], ]; + yield 'shellExecConcatInt' => [ + 'code' => <<<'PHP' + 'TaintedShell', ], + 'shellExecBacktickConcat' => [ + 'code' => ' 'TaintedShell', + ], + 'shellExecBacktickConcatInt' => [ + 'code' => ' 'TaintedShell', + ], /* // TODO: Stubs do not support this type of inference even with $this->message = $message. // Most uses of getMessage() would be with caught exceptions, so this is not representative of real code. From e386b072f393b92835f1180f0244b3ba8008585d Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 26 Nov 2023 13:33:25 +0100 Subject: [PATCH 095/357] Fix --- src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php index 063277aaff6..fad82a2e9f1 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php @@ -375,6 +375,7 @@ private static function handleExpression( } if ($stmt instanceof PhpParser\Node\Expr\ShellExec) { + /** @psalm-suppress ArgumentTypeCoercion $stmt->parts is untyped, but it's a list of expressions */ $concat = new VirtualEncapsed($stmt->parts, $stmt->getAttributes()); $virtual_call = new VirtualFuncCall(new VirtualName(['shell_exec']), [ new VirtualArg($concat), From 1e2e2248d908d412bdf2762d565b0c9caf937534 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 26 Nov 2023 19:34:49 +0100 Subject: [PATCH 096/357] Fix wrong type --- dictionaries/PropertyMap.php | 2 +- src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/dictionaries/PropertyMap.php b/dictionaries/PropertyMap.php index 7f07ea657de..c5755235e91 100644 --- a/dictionaries/PropertyMap.php +++ b/dictionaries/PropertyMap.php @@ -384,7 +384,7 @@ 'items' => 'array', ], 'phpparser\\node\\expr\\shellexec' => [ - 'parts' => 'list', + 'parts' => 'list', ], 'phpparser\\node\\matcharm' => [ 'conds' => 'null|non-empty-list', diff --git a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php index fad82a2e9f1..063277aaff6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php @@ -375,7 +375,6 @@ private static function handleExpression( } if ($stmt instanceof PhpParser\Node\Expr\ShellExec) { - /** @psalm-suppress ArgumentTypeCoercion $stmt->parts is untyped, but it's a list of expressions */ $concat = new VirtualEncapsed($stmt->parts, $stmt->getAttributes()); $virtual_call = new VirtualFuncCall(new VirtualName(['shell_exec']), [ new VirtualArg($concat), From 73a340fde83e8a22125e2a9575db8d537faf48dd Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 26 Nov 2023 19:46:45 +0100 Subject: [PATCH 097/357] Cleanup --- tests/TaintTest.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/TaintTest.php b/tests/TaintTest.php index c03bf2c6a65..7a947ca8127 100644 --- a/tests/TaintTest.php +++ b/tests/TaintTest.php @@ -2314,14 +2314,6 @@ function foo(array $arr) : void { ', 'error_message' => 'TaintedShell', ], - 'shellExecBacktickConcatInt' => [ - 'code' => ' 'TaintedShell', - ], /* // TODO: Stubs do not support this type of inference even with $this->message = $message. // Most uses of getMessage() would be with caught exceptions, so this is not representative of real code. From fa908e4ee18bc76d138eff4c93e745c3034b2e42 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 27 Nov 2023 11:55:38 +0100 Subject: [PATCH 098/357] Fixes --- src/Psalm/Codebase.php | 2 +- .../Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php | 3 --- .../Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php | 4 ++-- .../Provider/ParamsProvider/ArrayFilterParamsProvider.php | 2 ++ .../Provider/ParamsProvider/ArrayMultisortParamsProvider.php | 2 ++ src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 4a87186bea7..7e157e327b4 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -1565,7 +1565,7 @@ public function getCompletionItemsForClassishThing( string $gap, bool $snippets_supported = false, array $allow_visibilities = null, - array $ignore_fq_class_names = [] + array $ignore_fq_class_names = [], ): array { if ($allow_visibilities === null) { $allow_visibilities = [ diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index 24864e9dd9b..7e1c1a9758d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -852,9 +852,6 @@ public static function verifyType( if ($candidate_callable && $candidate_callable !== $atomic_type) { // if we had an array callable, mark it as used now, since it's not possible later $potential_method_id = null; - if ($atomic_type instanceof TList) { - $atomic_type = $atomic_type->getKeyedArray(); - } if ($atomic_type instanceof TKeyedArray) { $potential_method_id = CallableTypeComparator::getCallableMethodIdFromTKeyedArray( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 9e63d95161c..87106622daa 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -1036,9 +1036,9 @@ private static function handlePossiblyMatchingByRefParam( $function_params, static function ( ?FunctionLikeParameter $function_param, - FunctionLikeParameter $param + FunctionLikeParameter $param, ) use ( - $arg + $arg, ) { if ($param->name === $arg->name->name) { return $param; diff --git a/src/Psalm/Internal/Provider/ParamsProvider/ArrayFilterParamsProvider.php b/src/Psalm/Internal/Provider/ParamsProvider/ArrayFilterParamsProvider.php index 44a4908d41f..e89abe8978f 100644 --- a/src/Psalm/Internal/Provider/ParamsProvider/ArrayFilterParamsProvider.php +++ b/src/Psalm/Internal/Provider/ParamsProvider/ArrayFilterParamsProvider.php @@ -1,5 +1,7 @@ getBuilder(); foreach ($existing_var_type->getAtomicTypes() as $atomic_key => $type) { From 851121b48c7c1c0f63adf9509b1f8b999c48912d Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 27 Nov 2023 12:03:38 +0100 Subject: [PATCH 099/357] Fix --- tests/MethodSignatureTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/MethodSignatureTest.php b/tests/MethodSignatureTest.php index 7f1448cd6ce..2446c5c65c0 100644 --- a/tests/MethodSignatureTest.php +++ b/tests/MethodSignatureTest.php @@ -927,7 +927,7 @@ final class B implements I public function a(mixed $a): void {} }', 'assertions' => [], - 'ignored_errors' => [], + 'ignored_issues' => [], 'php_version' => '8.0', ], 'doesNotRequireInterfaceDestructorsToHaveReturnType' => [ From af9649b42eb677445159fa067595eaed437a4929 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 27 Nov 2023 12:13:37 +0100 Subject: [PATCH 100/357] Improve scanning progress output --- src/Psalm/Progress/DebugProgress.php | 4 ++-- src/Psalm/Progress/LongProgress.php | 6 +++--- tests/MethodSignatureTest.php | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Psalm/Progress/DebugProgress.php b/src/Psalm/Progress/DebugProgress.php index 3a8fa711787..2bfe8327466 100644 --- a/src/Psalm/Progress/DebugProgress.php +++ b/src/Psalm/Progress/DebugProgress.php @@ -22,12 +22,12 @@ public function debug(string $message): void public function startScanningFiles(): void { - $this->write('Scanning files...' . "\n"); + $this->write("\n" . 'Scanning files...' . "\n\n"); } public function startAnalyzingFiles(): void { - $this->write('Analyzing files...' . "\n"); + $this->write("\n" . 'Analyzing files...' . "\n"); } public function startAlteringFiles(): void diff --git a/src/Psalm/Progress/LongProgress.php b/src/Psalm/Progress/LongProgress.php index 4f7044d7275..8ec228b79da 100644 --- a/src/Psalm/Progress/LongProgress.php +++ b/src/Psalm/Progress/LongProgress.php @@ -36,13 +36,13 @@ public function __construct(bool $print_errors = true, bool $print_infos = true) public function startScanningFiles(): void { $this->fixed_size = false; - $this->write('Scanning files...' . "\n"); + $this->write("\n" . 'Scanning files...' . "\n\n"); } public function startAnalyzingFiles(): void { $this->fixed_size = true; - $this->write("\n" . 'Analyzing files...' . "\n\n"); + $this->write("\n\n" . 'Analyzing files...' . "\n\n"); } public function startAlteringFiles(): void @@ -78,7 +78,7 @@ public function taskDone(int $level): void if (!$this->fixed_size) { if ($this->progress == 1 || $this->progress == $this->number_of_tasks || $this->progress % 10 == 0) { $this->write(sprintf( - "\r%s / %s?", + "\r%s / %s...", $this->progress, $this->number_of_tasks, )); diff --git a/tests/MethodSignatureTest.php b/tests/MethodSignatureTest.php index 18609a2ccb9..5e76b8c0b85 100644 --- a/tests/MethodSignatureTest.php +++ b/tests/MethodSignatureTest.php @@ -927,7 +927,7 @@ final class B implements I public function a(mixed $a): void {} }', 'assertions' => [], - 'ignored_errors' => [], + 'ignored_issues' => [], 'php_version' => '8.0', ], 'doesNotRequireInterfaceDestructorsToHaveReturnType' => [ From bd33348206f5cc09515ae2f9dc8e86c3c35b4cd9 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 27 Nov 2023 13:23:52 +0100 Subject: [PATCH 101/357] Minor fixes --- src/Psalm/Codebase.php | 6 ------ .../MethodGetCompletionItemsForClassishThingTest.php | 1 + tests/StubTest.php | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 7e157e327b4..a84014d8ab9 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -1593,12 +1593,6 @@ public function getCompletionItemsForClassishThing( error_log($e->getMessage()); } } - if ($gap === '->') { - $method_storages += $class_storage->pseudo_methods; - } - if ($gap === '::') { - $method_storages += $class_storage->pseudo_static_methods; - } foreach ($method_storages as $method_storage) { if (!in_array($method_storage->visibility, $allow_visibilities)) { diff --git a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php index 9519553237b..836e7ae0f68 100644 --- a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php +++ b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php @@ -437,6 +437,7 @@ class A extends C { 'magicObjProp1', 'magicObjProp2', + 'magicStaticMethod', 'magicObjMethod', 'publicObjProp', diff --git a/tests/StubTest.php b/tests/StubTest.php index 87f3bfa42b6..5b9aa0eb095 100644 --- a/tests/StubTest.php +++ b/tests/StubTest.php @@ -222,7 +222,7 @@ public function testStubFileConstant(): void public function testStubFileParentClass(): void { $this->expectException(CodeException::class); - $this->expectExceptionMessage('MethodSignatureMismatch'); + $this->expectExceptionMessage('ImplementedParamTypeMismatch'); $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__), From a84be6465ddb8d3ee098f29bdc8978dc4cd6645d Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 27 Nov 2023 13:27:23 +0100 Subject: [PATCH 102/357] Test --- .../Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php | 3 +++ .../MethodGetCompletionItemsForClassishThingTest.php | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index bc0f61bb169..64eb5550bdb 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -641,6 +641,9 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool if (!isset($storage->overridden_method_ids[$lc_method_name])) { $storage->overridden_method_ids[$lc_method_name] = []; } + if (!isset($storage->declaring_method_ids[$lc_method_name])) { + $storage->declaring_method_ids[$lc_method_name] = $method_identifier; + } $storage->declaring_pseudo_method_ids[$lc_method_name] = $method_identifier; } diff --git a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php index 836e7ae0f68..ea48ce571cb 100644 --- a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php +++ b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php @@ -126,6 +126,7 @@ private static function privateStaticMethod() {} 'magicObjProp2', 'magicObjMethod', + 'magicStaticMethod', 'publicObjProp', 'protectedObjProp', @@ -201,6 +202,8 @@ private static function privateStaticMethod() {} 'magicObjMethod', + 'magicStaticMethod', + 'publicObjProp', 'protectedObjProp', 'privateObjProp', @@ -281,6 +284,7 @@ class A { 'magicObjProp2', 'magicObjMethod', + 'magicStaticMethod', 'publicObjProp', 'protectedObjProp', @@ -361,6 +365,7 @@ abstract class A { 'magicObjProp2', 'magicObjMethod', + 'magicStaticMethod', 'publicObjProp', 'protectedObjProp', @@ -543,6 +548,7 @@ class A { 'magicObjProp1', 'magicObjProp2', 'magicObjMethod', + 'magicStaticMethod', 'publicObjProp', From 62ed2b0cec1510fde20b33ff3907602d492b2125 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 27 Nov 2023 13:36:04 +0100 Subject: [PATCH 103/357] Hotfix --- src/Psalm/Codebase.php | 11 +++++++++++ .../PhpVisitor/Reflector/ClassLikeNodeScanner.php | 3 --- .../MethodGetCompletionItemsForClassishThingTest.php | 6 +----- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index a84014d8ab9..d7e566754a2 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -1593,11 +1593,22 @@ public function getCompletionItemsForClassishThing( error_log($e->getMessage()); } } + if ($gap === '->') { + $method_storages += $class_storage->pseudo_methods; + } + if ($gap === '::') { + $method_storages += $class_storage->pseudo_static_methods; + } + $had = []; foreach ($method_storages as $method_storage) { if (!in_array($method_storage->visibility, $allow_visibilities)) { continue; } + if (array_key_exists($method_storage->cased_name, $had)) { + continue; + } + $had[$method_storage->cased_name] = true; if ($method_storage->is_static || $gap === '->') { $completion_item = new CompletionItem( $method_storage->cased_name, diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 64eb5550bdb..bc0f61bb169 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -641,9 +641,6 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool if (!isset($storage->overridden_method_ids[$lc_method_name])) { $storage->overridden_method_ids[$lc_method_name] = []; } - if (!isset($storage->declaring_method_ids[$lc_method_name])) { - $storage->declaring_method_ids[$lc_method_name] = $method_identifier; - } $storage->declaring_pseudo_method_ids[$lc_method_name] = $method_identifier; } diff --git a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php index ea48ce571cb..704074560dc 100644 --- a/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php +++ b/tests/Internal/Codebase/MethodGetCompletionItemsForClassishThingTest.php @@ -126,7 +126,6 @@ private static function privateStaticMethod() {} 'magicObjProp2', 'magicObjMethod', - 'magicStaticMethod', 'publicObjProp', 'protectedObjProp', @@ -202,8 +201,6 @@ private static function privateStaticMethod() {} 'magicObjMethod', - 'magicStaticMethod', - 'publicObjProp', 'protectedObjProp', 'privateObjProp', @@ -442,8 +439,8 @@ class A extends C { 'magicObjProp1', 'magicObjProp2', - 'magicStaticMethod', 'magicObjMethod', + 'magicStaticMethod', 'publicObjProp', 'protectedObjProp', @@ -548,7 +545,6 @@ class A { 'magicObjProp1', 'magicObjProp2', 'magicObjMethod', - 'magicStaticMethod', 'publicObjProp', From d59a5493dbf059ca49bfa6c9eb920599b4ad2bbd Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 27 Nov 2023 13:36:11 +0100 Subject: [PATCH 104/357] cs-fix --- src/Psalm/Codebase.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index d7e566754a2..dde2fda896f 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -72,6 +72,7 @@ use UnexpectedValueException; use function array_combine; +use function array_key_exists; use function array_pop; use function array_reverse; use function array_values; From cd110d549de64a4b6d65b6009f08ce93f6f3acac Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 27 Nov 2023 13:41:27 +0100 Subject: [PATCH 105/357] Cleanup --- psalm-baseline.xml | 27 ++++++++++++++++++++++++++- src/Psalm/Codebase.php | 8 +++++--- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index abd3dbf4b71..4cdca652b5f 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + tags['variablesfrom'][0]]]> @@ -16,6 +16,9 @@ $deprecated_element_xml + + addAttribute + $this @@ -625,6 +628,28 @@ hasLowercaseString + + + Mockery::mock(ConfigFile::class) + + + addPlugin + expects + expects + removePlugin + + + config_file]]> + config_file]]> + config_file]]> + config_file]]> + config_file]]> + config_file]]> + config_file]]> + config_file]]> + config_file]]> + + Config diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index dde2fda896f..e3f48f025ed 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -1606,10 +1606,12 @@ public function getCompletionItemsForClassishThing( if (!in_array($method_storage->visibility, $allow_visibilities)) { continue; } - if (array_key_exists($method_storage->cased_name, $had)) { - continue; + if ($method_storage->cased_name !== null) { + if (array_key_exists($method_storage->cased_name, $had)) { + continue; + } + $had[$method_storage->cased_name] = true; } - $had[$method_storage->cased_name] = true; if ($method_storage->is_static || $gap === '->') { $completion_item = new CompletionItem( $method_storage->cased_name, From 48f243077461671e6392c0a0279b5ab49d78c475 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 27 Nov 2023 13:45:58 +0100 Subject: [PATCH 106/357] Update baseline --- psalm-baseline.xml | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 4cdca652b5f..9728c570c20 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -628,28 +628,6 @@ hasLowercaseString - - - Mockery::mock(ConfigFile::class) - - - addPlugin - expects - expects - removePlugin - - - config_file]]> - config_file]]> - config_file]]> - config_file]]> - config_file]]> - config_file]]> - config_file]]> - config_file]]> - config_file]]> - - Config From 2eed29209caebbb9df9e9af1d6f69c3ab33185d1 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 27 Nov 2023 14:49:04 +0100 Subject: [PATCH 107/357] Fix --- src/Psalm/Internal/Codebase/Populator.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php index 185e1563c55..65078689a12 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -1034,7 +1034,11 @@ protected function inheritMethodsFromParent( $implementing_method_id->fq_class_name, ); - if (!$implementing_class_storage->methods[$implementing_method_id->method_name]->abstract + $method = $implementing_class_storage->methods[$implementing_method_id->method_name] + ?? $implementing_class_storage->pseudo_methods[$implementing_method_id->method_name] + ?? $implementing_class_storage->pseudo_static_methods[$implementing_method_id->method_name]; + + if (!$method->abstract || !empty($storage->methods[$implementing_method_id->method_name]->abstract) ) { continue; From 8d6174cb99def7f0a234c3d83c9978b1ae2ada84 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 27 Nov 2023 15:00:31 +0100 Subject: [PATCH 108/357] Fix --- bin/test-with-real-projects.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/test-with-real-projects.sh b/bin/test-with-real-projects.sh index fdcddae7d67..8bd78455de2 100755 --- a/bin/test-with-real-projects.sh +++ b/bin/test-with-real-projects.sh @@ -38,6 +38,8 @@ psl) cd endtoend-test-psl git checkout 2.3.x composer install + # Avoid conflicts with old psalm when running phar tests + rm -rf vendor/vimeo/psalm sed 's/ErrorOutputBehavior::Packed, ErrorOutputBehavior::Discard/ErrorOutputBehavior::Discard/g' -i src/Psl/Shell/execute.php "$PSALM" --monochrome -c config/psalm.xml "$PSALM" --monochrome -c config/psalm.xml tests/static-analysis From 80580283948a2f963e10c8849877aebe19fc71dc Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 27 Nov 2023 15:03:46 +0100 Subject: [PATCH 109/357] Fix --- bin/test-with-real-projects.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/test-with-real-projects.sh b/bin/test-with-real-projects.sh index 8bd78455de2..923312bbea6 100755 --- a/bin/test-with-real-projects.sh +++ b/bin/test-with-real-projects.sh @@ -41,8 +41,8 @@ psl) # Avoid conflicts with old psalm when running phar tests rm -rf vendor/vimeo/psalm sed 's/ErrorOutputBehavior::Packed, ErrorOutputBehavior::Discard/ErrorOutputBehavior::Discard/g' -i src/Psl/Shell/execute.php - "$PSALM" --monochrome -c config/psalm.xml - "$PSALM" --monochrome -c config/psalm.xml tests/static-analysis + "$PSALM_PHAR" --monochrome -c config/psalm.xml + "$PSALM_PHAR" --monochrome -c config/psalm.xml tests/static-analysis ;; laravel) From 626ec3592e14d1b39c31b6f9c110bc4410ca81e4 Mon Sep 17 00:00:00 2001 From: robchett Date: Wed, 8 Nov 2023 11:41:42 +0000 Subject: [PATCH 110/357] Inherit conditional returns Fixes #3593 --- .../Method/MethodCallReturnTypeFetcher.php | 6 +- .../ExistingAtomicStaticCallAnalyzer.php | 6 +- .../Reflector/FunctionLikeDocblockScanner.php | 7 +-- tests/Template/ConditionalReturnTypeTest.php | 58 +++++++++++++++++++ 4 files changed, 67 insertions(+), 10 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php index a04104107b2..3d3b39e5b37 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php @@ -577,7 +577,7 @@ public static function replaceTemplateTypes( ) { if ($template_type->param_name === 'TFunctionArgCount') { $template_result->lower_bounds[$template_type->param_name] = [ - 'fn-' . strtolower((string) $method_id) => [ + 'fn-' . $method_id->method_name => [ new TemplateBound( Type::getInt(false, $arg_count), ), @@ -585,7 +585,7 @@ public static function replaceTemplateTypes( ]; } elseif ($template_type->param_name === 'TPhpMajorVersion') { $template_result->lower_bounds[$template_type->param_name] = [ - 'fn-' . strtolower((string) $method_id) => [ + 'fn-' . $method_id->method_name => [ new TemplateBound( Type::getInt(false, $codebase->getMajorAnalysisPhpVersion()), ), @@ -593,7 +593,7 @@ public static function replaceTemplateTypes( ]; } elseif ($template_type->param_name === 'TPhpVersionId') { $template_result->lower_bounds[$template_type->param_name] = [ - 'fn-' . strtolower((string) $method_id) => [ + 'fn-' . $method_id->method_name => [ new TemplateBound( Type::getInt( false, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php index c87fa0f8462..af7a26fafae 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -629,7 +629,7 @@ private static function resolveTemplateResultLowerBound( ): array { if ($template_type->param_name === 'TFunctionArgCount') { return [ - 'fn-' . strtolower((string)$method_id) => [ + 'fn-' . $method_id->method_name => [ new TemplateBound( Type::getInt(false, count($stmt->getArgs())), ), @@ -639,7 +639,7 @@ private static function resolveTemplateResultLowerBound( if ($template_type->param_name === 'TPhpMajorVersion') { return [ - 'fn-' . strtolower((string)$method_id) => [ + 'fn-' . $method_id->method_name => [ new TemplateBound( Type::getInt(false, $codebase->getMajorAnalysisPhpVersion()), ), @@ -649,7 +649,7 @@ private static function resolveTemplateResultLowerBound( if ($template_type->param_name === 'TPhpVersionId') { return [ - 'fn-' . strtolower((string) $method_id) => [ + 'fn-' . $method_id->method_name => [ new TemplateBound( Type::getInt( false, diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php index 0d9bc183cba..b7808ee3879 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php @@ -512,9 +512,8 @@ private static function getConditionalSanitizedTypeTokens( if ($token_body === 'func_num_args()') { $template_name = 'TFunctionArgCount'; - $storage->template_types[$template_name] = [ - $template_function_id => Type::getInt(), + 'fn-' . strtolower($storage->cased_name ?? '') => Type::getInt(), ]; $function_template_types[$template_name] @@ -527,7 +526,7 @@ private static function getConditionalSanitizedTypeTokens( $template_name = 'TPhpMajorVersion'; $storage->template_types[$template_name] = [ - $template_function_id => Type::getInt(), + 'fn-' . strtolower($storage->cased_name ?? '') => Type::getInt(), ]; $function_template_types[$template_name] @@ -540,7 +539,7 @@ private static function getConditionalSanitizedTypeTokens( $template_name = 'TPhpVersionId'; $storage->template_types[$template_name] = [ - $template_function_id => Type::getInt(), + 'fn-' . strtolower($storage->cased_name ?? '') => Type::getInt(), ]; $function_template_types[$template_name] diff --git a/tests/Template/ConditionalReturnTypeTest.php b/tests/Template/ConditionalReturnTypeTest.php index 5bf824e9b23..7698f3ce966 100644 --- a/tests/Template/ConditionalReturnTypeTest.php +++ b/tests/Template/ConditionalReturnTypeTest.php @@ -463,6 +463,31 @@ function zeroArgsFalseOneArgString(string $s = "") { '$c' => 'string', ], ], + 'InheritFuncNumArgs' => [ + 'code' => ' [ 'code' => ' [], 'php_version' => '7.2', ], + 'ineritedreturnTypeBasedOnPhpVersionId' => [ + 'code' => ' ? string : int) + */ + function getSomething() + { + return mt_rand(1, 10) > 5 ? "a value" : 42; + } + + /** + * @psalm-return (PHP_VERSION_ID is int<70100, max> ? string : int) + */ + function getSomethingElse() + { + return mt_rand(1, 10) > 5 ? "a value" : 42; + } + } + + class B extends A {} + + $class = new B(); + $something = $class->getSomething(); + $somethingElse = $class->getSomethingElse(); + ', + 'assertions' => [ + '$something' => 'int', + '$somethingElse' => 'string', + ], + 'ignored_issues' => [], + 'php_version' => '7.2', + ], 'ineritedConditionalTemplatedReturnType' => [ 'code' => ' Date: Wed, 22 Nov 2023 11:10:50 +0100 Subject: [PATCH 111/357] TaintedExtract --- UPGRADING.md | 2 +- config.xsd | 1 + docs/running_psalm/error_levels.md | 1 + docs/running_psalm/issues.md | 1 + docs/running_psalm/issues/TaintedExtract.md | 10 ++++++++++ .../Statements/Expression/Call/ArgumentsAnalyzer.php | 2 +- src/Psalm/Internal/Codebase/TaintFlowGraph.php | 10 ++++++++++ src/Psalm/Issue/TaintedExtract.php | 10 ++++++++++ src/Psalm/Type/TaintKind.php | 1 + src/Psalm/Type/TaintKindGroup.php | 1 + stubs/CoreGenericFunctions.phpstub | 5 +++++ tests/TaintTest.php | 11 +++++++++++ 12 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 docs/running_psalm/issues/TaintedExtract.md create mode 100644 src/Psalm/Issue/TaintedExtract.php diff --git a/UPGRADING.md b/UPGRADING.md index 9b3665d4dcf..767e293871e 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -17,7 +17,7 @@ - [BC] Class `Psalm\Issue\MixedInferredReturnType` was removed -- [BC] Value of constant `Psalm\Type\TaintKindGroup::ALL_INPUT` changed to reflect new `TaintKind::INPUT_SLEEP` and `TaintKind::INPUT_XPATH` have been added. Accordingly, default values for `$taint` parameters of `Psalm\Codebase::addTaintSource()` and `Psalm\Codebase::addTaintSink()` have been changed as well. +- [BC] Value of constant `Psalm\Type\TaintKindGroup::ALL_INPUT` changed to reflect new `TaintKind::INPUT_EXTRACT`, `TaintKind::INPUT_SLEEP` and `TaintKind::INPUT_XPATH` have been added. Accordingly, default values for `$taint` parameters of `Psalm\Codebase::addTaintSource()` and `Psalm\Codebase::addTaintSink()` have been changed as well. - [BC] Property `Config::$shepherd_host` was replaced with `Config::$shepherd_endpoint` diff --git a/config.xsd b/config.xsd index 6a6d182dca3..c97e3198ad9 100644 --- a/config.xsd +++ b/config.xsd @@ -433,6 +433,7 @@ + diff --git a/docs/running_psalm/error_levels.md b/docs/running_psalm/error_levels.md index 2d9c35ced37..a7b61ee78a1 100644 --- a/docs/running_psalm/error_levels.md +++ b/docs/running_psalm/error_levels.md @@ -286,6 +286,7 @@ Level 5 and above allows a more non-verifiable code, and higher levels are even - [TaintedCookie](issues/TaintedCookie.md) - [TaintedCustom](issues/TaintedCustom.md) - [TaintedEval](issues/TaintedEval.md) + - [TaintedExtract](issues/TaintedExtract.md) - [TaintedFile](issues/TaintedFile.md) - [TaintedHeader](issues/TaintedHeader.md) - [TaintedHtml](issues/TaintedHtml.md) diff --git a/docs/running_psalm/issues.md b/docs/running_psalm/issues.md index 95f3839593b..364541ee439 100644 --- a/docs/running_psalm/issues.md +++ b/docs/running_psalm/issues.md @@ -234,6 +234,7 @@ - [TaintedCookie](issues/TaintedCookie.md) - [TaintedCustom](issues/TaintedCustom.md) - [TaintedEval](issues/TaintedEval.md) + - [TaintedExtract](issues/TaintedExtract.md) - [TaintedFile](issues/TaintedFile.md) - [TaintedHeader](issues/TaintedHeader.md) - [TaintedHtml](issues/TaintedHtml.md) diff --git a/docs/running_psalm/issues/TaintedExtract.md b/docs/running_psalm/issues/TaintedExtract.md new file mode 100644 index 00000000000..7b0fa27d85a --- /dev/null +++ b/docs/running_psalm/issues/TaintedExtract.md @@ -0,0 +1,10 @@ +# TaintedExtract + +Emitted when user-controlled array can be passed into an `extract` call. + +```php +vars_in_scope[$var_id])) diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index 5c5f72173eb..1cab3ea6dd8 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -14,6 +14,7 @@ use Psalm\Issue\TaintedCookie; use Psalm\Issue\TaintedCustom; use Psalm\Issue\TaintedEval; +use Psalm\Issue\TaintedExtract; use Psalm\Issue\TaintedFile; use Psalm\Issue\TaintedHeader; use Psalm\Issue\TaintedHtml; @@ -471,6 +472,15 @@ private function getChildNodes( ); break; + case TaintKind::INPUT_EXTRACT: + $issue = new TaintedExtract( + 'Detected tainted extract', + $issue_location, + $issue_trace, + $path, + ); + break; + default: $issue = new TaintedCustom( 'Detected tainted ' . $matching_taint, diff --git a/src/Psalm/Issue/TaintedExtract.php b/src/Psalm/Issue/TaintedExtract.php new file mode 100644 index 00000000000..60eef6b9271 --- /dev/null +++ b/src/Psalm/Issue/TaintedExtract.php @@ -0,0 +1,10 @@ + 'TaintedSleep', ], + 'taintedExtract' => [ + 'code' => ' 'TaintedExtract', + ], + 'extractPost' => [ + 'code' => ' 'TaintedExtract', + ], ]; } From c75e6da8665b83ae30fe5f0174368b26f7581e92 Mon Sep 17 00:00:00 2001 From: cgocast Date: Tue, 28 Nov 2023 10:24:01 +0100 Subject: [PATCH 112/357] Fix coding style --- .../Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index acdd6c272fd..d265e22d0bb 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -1270,7 +1270,7 @@ private static function handleByRefFunctionArg( $builtin_array_functions = [ 'ksort', 'asort', 'krsort', 'arsort', 'natcasesort', 'natsort', - 'reset', 'end', 'next', 'prev', 'array_pop', 'array_shift', 'extract' + 'reset', 'end', 'next', 'prev', 'array_pop', 'array_shift', 'extract', ]; if (($var_id && isset($context->vars_in_scope[$var_id])) From d91aab15be353b1edabec92e3447c70b78fedf85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tinjo=20Sch=C3=B6ni?= <32767367+tscni@users.noreply.github.com> Date: Wed, 29 Nov 2023 02:06:20 +0100 Subject: [PATCH 113/357] Restore support for null coalesce on match expressions https://github.com/vimeo/psalm/pull/10068 added isset restrictions that didn't consider null coalesces on match expressions. This restores that support by converting the match expression to a virtual variable for the isset analysis, similar to other incompatible expressions. --- .../Expression/BinaryOp/CoalesceAnalyzer.php | 1 + tests/MatchTest.php | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php index 0a52166699a..72194f55224 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php @@ -39,6 +39,7 @@ public static function analyze( || $root_expr instanceof PhpParser\Node\Expr\MethodCall || $root_expr instanceof PhpParser\Node\Expr\StaticCall || $root_expr instanceof PhpParser\Node\Expr\Cast + || $root_expr instanceof PhpParser\Node\Expr\Match_ || $root_expr instanceof PhpParser\Node\Expr\NullsafePropertyFetch || $root_expr instanceof PhpParser\Node\Expr\NullsafeMethodCall || $root_expr instanceof PhpParser\Node\Expr\Ternary diff --git a/tests/MatchTest.php b/tests/MatchTest.php index e281a200e97..64f2c4f5e0d 100644 --- a/tests/MatchTest.php +++ b/tests/MatchTest.php @@ -167,6 +167,21 @@ function process(Obj1|Obj2 $obj): int|string 'ignored_issues' => [], 'php_version' => '8.0', ], + 'nullCoalesce' => [ + 'code' => <<<'PHP' + null, + true => 1, + } ?? 2; + PHP, + 'assertions' => [ + '$match' => 'int', + ], + 'ignored_issues' => [], + 'php_version' => '8.0', + ], ]; } From 4f25ccee400c3e2241d393eb60332e5edabcc67c Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 29 Nov 2023 08:41:54 +0100 Subject: [PATCH 114/357] update define types to be correct --- dictionaries/CallMap.php | 2 +- dictionaries/CallMap_73_delta.php | 4 +++ dictionaries/CallMap_historical.php | 2 +- .../Call/NamedFunctionCallHandler.php | 33 ++++++++++++++++++- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 4e7166ec105..5f3dba9c880 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1552,7 +1552,7 @@ 'decbin' => ['string', 'num'=>'int'], 'dechex' => ['string', 'num'=>'int'], 'decoct' => ['string', 'num'=>'int'], -'define' => ['bool', 'constant_name'=>'string', 'value'=>'mixed', 'case_insensitive='=>'bool'], +'define' => ['bool', 'constant_name'=>'string', 'value'=>'array|scalar|null', 'case_insensitive='=>'false'], 'define_syslog_variables' => ['void'], 'defined' => ['bool', 'constant_name'=>'string'], 'deflate_add' => ['string|false', 'context'=>'DeflateContext', 'data'=>'string', 'flush_mode='=>'int'], diff --git a/dictionaries/CallMap_73_delta.php b/dictionaries/CallMap_73_delta.php index d79026c3805..6352fa06292 100644 --- a/dictionaries/CallMap_73_delta.php +++ b/dictionaries/CallMap_73_delta.php @@ -64,6 +64,10 @@ 'old' => ['int', 'scale'=>'int'], 'new' => ['int', 'scale='=>'int'], ], + 'define' => [ + 'old' => ['bool', 'constant_name'=>'string', 'value'=>'array|scalar|null', 'case_insensitive='=>'bool'], + 'new' => ['bool', 'constant_name'=>'string', 'value'=>'array|scalar|null', 'case_insensitive='=>'false'], + ], 'ldap_compare' => [ 'old' => ['bool|int', 'ldap'=>'resource', 'dn'=>'string', 'attribute'=>'string', 'value'=>'string'], 'new' => ['bool|int', 'ldap'=>'resource', 'dn'=>'string', 'attribute'=>'string', 'value'=>'string', 'controls='=>'array'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index afb21ba72df..864b668bd0b 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -9937,7 +9937,7 @@ 'decbin' => ['string', 'num'=>'int'], 'dechex' => ['string', 'num'=>'int'], 'decoct' => ['string', 'num'=>'int'], - 'define' => ['bool', 'constant_name'=>'string', 'value'=>'mixed', 'case_insensitive='=>'bool'], + 'define' => ['bool', 'constant_name'=>'string', 'value'=>'array|scalar|null', 'case_insensitive='=>'bool'], 'define_syslog_variables' => ['void'], 'defined' => ['bool', 'constant_name'=>'string'], 'deflate_add' => ['string|false', 'context'=>'resource', 'data'=>'string', 'flush_mode='=>'int'], diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php index 961662f7044..514df1a55a5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php @@ -225,7 +225,38 @@ public static function handle( } if ($function_id === 'defined') { - $context->check_consts = false; + if ($first_arg && !$context->inside_negation) { + $fq_const_name = ConstFetchAnalyzer::getConstName( + $first_arg->value, + $statements_analyzer->node_data, + $codebase, + $statements_analyzer->getAliases(), + ); + + if ($fq_const_name !== null) { + $const_type = ConstFetchAnalyzer::getConstType( + $statements_analyzer, + $fq_const_name, + true, + $context, + ); + + if (!$const_type) { + ConstFetchAnalyzer::setConstType( + $statements_analyzer, + $fq_const_name, + Type::getMixed(), + $context, + ); + + $context->check_consts = false; + } + } else { + $context->check_consts = false; + } + } else { + $context->check_consts = false; + } return; } From 2c5645c4669d6a6fd65a2d0e477f920eec74ed8b Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 29 Nov 2023 08:56:31 +0100 Subject: [PATCH 115/357] use branch name to randomize tests to ensure test dependencies will error --- .github/workflows/ci.yml | 1 + bin/tests-github-actions.sh | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8baa8961369..e01a894b61b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,6 +136,7 @@ jobs: CHUNK_COUNT: "${{ matrix.count }}" CHUNK_NUMBER: "${{ matrix.chunk }}" PARALLEL_PROCESSES: 5 + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} steps: - name: Set up PHP diff --git a/bin/tests-github-actions.sh b/bin/tests-github-actions.sh index 4296f70591a..2c0ef4b787b 100755 --- a/bin/tests-github-actions.sh +++ b/bin/tests-github-actions.sh @@ -3,13 +3,15 @@ set -eu function get_seeded_random() { - openssl enc -aes-256-ctr -pass pass:"vimeo/psalm" -nosalt /dev/null + local -r branch_name="$1" + openssl enc -aes-256-ctr -pass pass:"$branch_name" -nosalt /dev/null } function run { local -r chunk_count="$1" local -r chunk_number="$2" local -r parallel_processes="$3" + local -r branch_name="$4" local -r phpunit_cmd=' echo "::group::{}"; @@ -23,7 +25,7 @@ exit "$exit_code"' mkdir -p build/parallel/ build/phpunit/logs/ - find tests -name '*Test.php' | shuf --random-source=<(get_seeded_random) > build/tests_all + find tests -name '*Test.php' | shuf --random-source=<(get_seeded_random "$branch_name") > build/tests_all # split incorrectly splits the lines by byte size, which means that the number of tests per file are as evenly distributed as possible #split --number="l/$chunk_number/$chunk_count" build/tests_all > build/tests_split local -r lines=$(wc -l Date: Wed, 29 Nov 2023 12:24:48 +0100 Subject: [PATCH 116/357] Tweak config --- src/Psalm/Internal/Fork/PsalmRestarter.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/Fork/PsalmRestarter.php b/src/Psalm/Internal/Fork/PsalmRestarter.php index 9332cdaab75..e25f5627e78 100644 --- a/src/Psalm/Internal/Fork/PsalmRestarter.php +++ b/src/Psalm/Internal/Fork/PsalmRestarter.php @@ -31,12 +31,12 @@ final class PsalmRestarter extends XdebugHandler 'jit' => 1205, 'validate_timestamps' => 0, 'file_update_protection' => 0, - 'jit_buffer_size' => 512 * 1024 * 1024, + 'jit_buffer_size' => 128 * 1024 * 1024, 'max_accelerated_files' => 1_000_000, 'interned_strings_buffer' => 64, - 'jit_max_root_traces' => 30_000_000, - 'jit_max_side_traces' => 30_000_000, - 'jit_max_exit_counters' => 30_000_000, + 'jit_max_root_traces' => 1_000_000, + 'jit_max_side_traces' => 1_000_000, + 'jit_max_exit_counters' => 1_000_000, 'jit_hot_loop' => 1, 'jit_hot_func' => 1, 'jit_hot_return' => 1, From e0778fcc11614baa89cb5a912e7224efc7932e8c Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 29 Nov 2023 13:32:45 +0100 Subject: [PATCH 117/357] Fix parsing of array keys with @ --- src/Psalm/Internal/Type/ParseTreeCreator.php | 6 +++++- tests/TypeAnnotationTest.php | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Type/ParseTreeCreator.php b/src/Psalm/Internal/Type/ParseTreeCreator.php index 737b5ac3f1b..587d2fcd842 100644 --- a/src/Psalm/Internal/Type/ParseTreeCreator.php +++ b/src/Psalm/Internal/Type/ParseTreeCreator.php @@ -830,7 +830,11 @@ private function handleValue(array $type_token): void $nexter_token = $this->t + 1 < $this->type_token_count ? $this->type_tokens[$this->t + 1] : null; - if ($nexter_token && strpos($nexter_token[0], '@') !== false) { + if ($nexter_token + && strpos($nexter_token[0], '@') !== false + && $type_token[0] !== 'list' + && $type_token[0] !== 'array' + ) { $this->t = $this->type_token_count; if ($type_token[0] === '$this') { $type_token[0] = 'static'; diff --git a/tests/TypeAnnotationTest.php b/tests/TypeAnnotationTest.php index 07058f21998..bdec0ba36bf 100644 --- a/tests/TypeAnnotationTest.php +++ b/tests/TypeAnnotationTest.php @@ -15,6 +15,14 @@ class TypeAnnotationTest extends TestCase public function providerValidCodeParse(): iterable { return [ + 'atInArrayKey' => [ + 'code' => ' $v + */ + function a(array $v): void {}', + ], 'typeAliasBeforeClass' => [ 'code' => ' Date: Wed, 29 Nov 2023 23:08:44 +0100 Subject: [PATCH 118/357] Fix static magic method pureness not being inherited from traits https://github.com/vimeo/psalm/pull/10385 "broke" this by propagating pseudo static methods from traits to using classes. `AtomicStaticCallAnalyzer` was then not capable of dealing with this, because now these static pseudo methods actually exist. As long as the methods from traits aren't actually transferred to the using class, it seems right that the logic in `AtomicStaticCallAnalyzer` uses `::getDeclaringMethodId()` instead of `::getAppearingMethodId()` for this purpose. --- .../StaticMethod/AtomicStaticCallAnalyzer.php | 8 +- tests/PureAnnotationTest.php | 75 +++++++++++++++++++ 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php index e42024d08f6..ad9fd724f4e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -566,12 +566,12 @@ private static function handleNamedCall( true, $context->insideUse(), )) { - $callstatic_appearing_id = $codebase->methods->getAppearingMethodId($callstatic_id); - assert($callstatic_appearing_id !== null); + $callstatic_declaring_id = $codebase->methods->getDeclaringMethodId($callstatic_id); + assert($callstatic_declaring_id !== null); $callstatic_pure = false; $callstatic_mutation_free = false; - if ($codebase->methods->hasStorage($callstatic_appearing_id)) { - $callstatic_storage = $codebase->methods->getStorage($callstatic_appearing_id); + if ($codebase->methods->hasStorage($callstatic_declaring_id)) { + $callstatic_storage = $codebase->methods->getStorage($callstatic_declaring_id); $callstatic_pure = $callstatic_storage->pure; $callstatic_mutation_free = $callstatic_storage->mutation_free; } diff --git a/tests/PureAnnotationTest.php b/tests/PureAnnotationTest.php index f27632baed6..57c27e7b113 100644 --- a/tests/PureAnnotationTest.php +++ b/tests/PureAnnotationTest.php @@ -446,6 +446,81 @@ function gimmeFoo(): MyEnum return MyEnum::FOO(); }', ], + 'pureThroughCallStaticInTrait' => [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ 'code' => ' Date: Fri, 1 Dec 2023 12:03:24 +0100 Subject: [PATCH 119/357] Use correct file path while adding unused suppressions for virtual __constructs --- src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 4e93704e2c3..7b7a9972c98 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -186,7 +186,7 @@ public function analyze( || !in_array("UnusedPsalmSuppress", $storage->suppressed_issues) ) { foreach ($storage->suppressed_issues as $offset => $issue_name) { - IssueBuffer::addUnusedSuppression($this->getFilePath(), $offset, $issue_name); + IssueBuffer::addUnusedSuppression($storage->location->file_path, $offset, $issue_name); } } } From 8111319fc3eea807789f0c6f1d8211a823ef3fc9 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 1 Dec 2023 12:25:04 +0100 Subject: [PATCH 120/357] Fix --- src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 7b7a9972c98..920ff547287 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -186,7 +186,13 @@ public function analyze( || !in_array("UnusedPsalmSuppress", $storage->suppressed_issues) ) { foreach ($storage->suppressed_issues as $offset => $issue_name) { - IssueBuffer::addUnusedSuppression($storage->location->file_path, $offset, $issue_name); + IssueBuffer::addUnusedSuppression( + $storage->location !== null + ? $storage->location->file_path + : $this->getFilePath(), + $offset, + $issue_name + ); } } } From 461cd184e5fc571bf82777a9efacfd1308b110d2 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 1 Dec 2023 12:25:30 +0100 Subject: [PATCH 121/357] cs-fix --- src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 920ff547287..3df1a6a1b0b 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -191,7 +191,7 @@ public function analyze( ? $storage->location->file_path : $this->getFilePath(), $offset, - $issue_name + $issue_name, ); } } From 7e948419cddac6b146ceaaeeac9d42f61b3a7627 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 1 Dec 2023 13:37:28 +0100 Subject: [PATCH 122/357] Fix array_key_exists negation --- src/Psalm/Type/Reconciler.php | 5 ++++- tests/TypeReconciliation/ArrayKeyExistsTest.php | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 1ae92b40571..7863d05c265 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -20,6 +20,7 @@ use Psalm\Issue\TypeDoesNotContainType; use Psalm\IssueBuffer; use Psalm\Storage\Assertion; +use Psalm\Storage\Assertion\ArrayKeyDoesNotExist; use Psalm\Storage\Assertion\ArrayKeyExists; use Psalm\Storage\Assertion\Empty_; use Psalm\Storage\Assertion\Falsy; @@ -197,7 +198,9 @@ public static function reconcileKeyedTypes( $is_equality = $is_equality && $new_type_part_part instanceof IsIdentical; - $has_inverted_isset = $has_inverted_isset || $new_type_part_part instanceof IsNotIsset; + $has_inverted_isset = $has_inverted_isset + || $new_type_part_part instanceof IsNotIsset + || $new_type_part_part instanceof ArrayKeyDoesNotExist; $has_count_check = $has_count_check || $new_type_part_part instanceof NonEmptyCountable; diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php index 3ee780251c1..c8b8dafbaf8 100644 --- a/tests/TypeReconciliation/ArrayKeyExistsTest.php +++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php @@ -46,6 +46,15 @@ function three(array $a): void { echo $a["a"]; echo $a["b"]; }', + ], + 'arrayKeyExistsNegation' => [ + 'code' => ' */ + } + ', ], 'arrayKeyExistsTwice' => [ 'code' => ' Date: Fri, 1 Dec 2023 15:03:17 +0100 Subject: [PATCH 123/357] Fix --- .../Statements/Block/IfElse/IfAnalyzer.php | 14 -------------- tests/TypeReconciliation/ArrayKeyExistsTest.php | 13 +++++++++++++ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php index ad95cce30d5..c74da5d3efe 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php @@ -272,20 +272,6 @@ public static function analyze( array_keys($if_scope->negated_types), ); - $extra_vars_to_update = []; - - // if there's an object-like array in there, we also need to update the root array variable - foreach ($vars_to_update as $var_id) { - $bracked_pos = strpos($var_id, '['); - if ($bracked_pos !== false) { - $extra_vars_to_update[] = substr($var_id, 0, $bracked_pos); - } - } - - if ($extra_vars_to_update) { - $vars_to_update = array_unique(array_merge($extra_vars_to_update, $vars_to_update)); - } - $outer_context->update( $old_if_context, $if_context, diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php index c8b8dafbaf8..19174d347cd 100644 --- a/tests/TypeReconciliation/ArrayKeyExistsTest.php +++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php @@ -56,6 +56,19 @@ function getMethodName(array $data = []): void { } ', ], + 'arrayKeyExistsNoSideEffects' => [ + 'code' => ' */ + } + ' + ] 'arrayKeyExistsTwice' => [ 'code' => ' Date: Fri, 1 Dec 2023 15:03:24 +0100 Subject: [PATCH 124/357] Fix --- tests/TypeReconciliation/ArrayKeyExistsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php index 19174d347cd..1390caeee13 100644 --- a/tests/TypeReconciliation/ArrayKeyExistsTest.php +++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php @@ -68,7 +68,7 @@ function getMethodName(array $ddata = []): void { /** @psalm-check-type-exact $ddata = array */ } ' - ] + ], 'arrayKeyExistsTwice' => [ 'code' => ' Date: Fri, 1 Dec 2023 15:07:55 +0100 Subject: [PATCH 125/357] cs-fix --- .../Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php | 3 --- tests/TypeReconciliation/ArrayKeyExistsTest.php | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php index c74da5d3efe..2a2be74606f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php @@ -34,14 +34,11 @@ use function array_keys; use function array_merge; use function array_reduce; -use function array_unique; use function count; use function in_array; use function preg_match; use function preg_quote; use function spl_object_id; -use function strpos; -use function substr; /** * @internal diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php index 1390caeee13..e44bfa55464 100644 --- a/tests/TypeReconciliation/ArrayKeyExistsTest.php +++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php @@ -67,7 +67,7 @@ function getMethodName(array $ddata = []): void { } /** @psalm-check-type-exact $ddata = array */ } - ' + ', ], 'arrayKeyExistsTwice' => [ 'code' => ' Date: Fri, 1 Dec 2023 16:03:08 +0100 Subject: [PATCH 126/357] Create keyed arrays when assigning literal union keys --- .../Assignment/ArrayAssignmentAnalyzer.php | 38 ++++++++----------- tests/ArrayAssignmentTest.php | 21 ++++++++++ 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php index f00d81a48dd..576fe45a011 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php @@ -349,29 +349,23 @@ private static function updateTypeWithKeyValues( } if (!$has_matching_objectlike_property && !$has_matching_string) { - if (count($key_values) === 1) { - $key_value = $key_values[0]; - - $object_like = new TKeyedArray( - [$key_value->value => $current_type], - $key_value instanceof TLiteralClassString - ? [$key_value->value => true] - : null, - ); - - $array_assignment_type = new Union([ - $object_like, - ]); - } else { - $array_assignment_literals = $key_values; - - $array_assignment_type = new Union([ - new TNonEmptyArray([ - new Union($array_assignment_literals), - $current_type, - ]), - ]); + $properties = []; + $classStrings = []; + $current_type = $current_type->setPossiblyUndefined(count($key_values) > 1); + foreach ($key_values as $key_value) { + $properties[$key_value->value] = $current_type; + if ($key_value instanceof TLiteralClassString) { + $classStrings[$key_value->value] = true; + } } + $object_like = new TKeyedArray( + $properties, + $classStrings ?: null, + ); + + $array_assignment_type = new Union([ + $object_like, + ]); return Type::combineUnionTypes( $child_stmt_type, diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 38f98d70d60..4377c1e72cf 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -34,6 +34,27 @@ public function testConditionalAssignment(): void public function providerValidCodeParse(): iterable { return [ + 'assignUnionOfLiterals' => [ + 'code' => ' [ + '$result===' => 'array{a: true, b: true}', + '$resultOpt===' => 'array{a?: true, b?: true}', + ], + ], 'genericArrayCreationWithSingleIntValue' => [ 'code' => ' Date: Fri, 1 Dec 2023 16:11:05 +0100 Subject: [PATCH 127/357] Fix --- tests/ArrayAssignmentTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 4377c1e72cf..364b6def325 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -213,7 +213,7 @@ class B {} 'assertions' => [ '$foo' => 'array{0: string, 1: string, 2: string}', '$bar' => 'list{int, int, int}', - '$bat' => 'non-empty-array', + '$bat' => 'array{a: int, b: int, c: int}', ], ], 'implicitStringArrayCreation' => [ @@ -1000,6 +1000,7 @@ function updateArray(array $arr) : array { $a = []; foreach (["one", "two", "three"] as $key) { + $a[$key] ??= 0; $a[$key] += rand(0, 10); } From 1a4656564a5b3c41d5444473d31725936db86b6a Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 1 Dec 2023 16:31:01 +0100 Subject: [PATCH 128/357] Cleanup --- tests/TypeReconciliation/TypeAlgebraTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/TypeReconciliation/TypeAlgebraTest.php b/tests/TypeReconciliation/TypeAlgebraTest.php index aea50a778e6..cfc8374a94d 100644 --- a/tests/TypeReconciliation/TypeAlgebraTest.php +++ b/tests/TypeReconciliation/TypeAlgebraTest.php @@ -287,12 +287,12 @@ function foo(array $arr): void { $arr = []; foreach ([0, 1, 2, 3] as $i) { - $a = rand(0, 1) ? 5 : "010"; + $a = (int) (rand(0, 1) ? 5 : "010"); - if (!isset($arr[(int) $a])) { - $arr[(int) $a] = 5; + if (!isset($arr[$a])) { + $arr[$a] = 5; } else { - $arr[(int) $a] += 4; + $arr[$a] += 4; } }', ], From ead29084646d0c1bbb842e981b32ca400bf1f78b Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 1 Dec 2023 17:05:23 +0100 Subject: [PATCH 129/357] Fixup tests --- tests/Loop/ForeachTest.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Loop/ForeachTest.php b/tests/Loop/ForeachTest.php index fbbb6e445f9..d7b56e5fa48 100644 --- a/tests/Loop/ForeachTest.php +++ b/tests/Loop/ForeachTest.php @@ -1027,9 +1027,7 @@ function foo() : void { $arr = []; foreach ([1, 2, 3] as $i) { - if (!isset($arr[$i]["a"])) { - $arr[$i]["a"] = 0; - } + $arr[$i]["a"] ??= 0; $arr[$i]["a"] += 5; } From 147129345ea848d0f2559206a106ab13e2c364c2 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 1 Dec 2023 17:23:35 +0100 Subject: [PATCH 130/357] Add failing test --- tests/TypeReconciliation/IssetTest.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/TypeReconciliation/IssetTest.php b/tests/TypeReconciliation/IssetTest.php index c15612692e5..19e7dbfcd0e 100644 --- a/tests/TypeReconciliation/IssetTest.php +++ b/tests/TypeReconciliation/IssetTest.php @@ -41,6 +41,21 @@ public function providerValidCodeParse(): iterable 'assertions' => [], 'ignored_issues' => ['MixedArrayAccess'], ], + 'issetWithArrayAssignment' => [ + 'code'=> ' [ 'code' => ' Date: Fri, 1 Dec 2023 17:46:24 +0100 Subject: [PATCH 131/357] Fixup --- src/Psalm/Type/Reconciler.php | 133 ++++++++++++++----------- tests/Loop/ForeachTest.php | 4 +- tests/TypeReconciliation/IssetTest.php | 31 +++++- 3 files changed, 108 insertions(+), 60 deletions(-) diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 7863d05c265..61e731862ff 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -46,6 +46,8 @@ use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TList; +use Psalm\Type\Atomic\TLiteralInt; +use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNever; @@ -1108,88 +1110,103 @@ private static function adjustTKeyedArrayType( throw new UnexpectedValueException('Not expecting null array key'); } + $array_key_offsets = []; if ($array_key[0] === '$') { - return; + if (!isset($existing_types[$array_key])) { + return; + } + $t = $existing_types[$array_key]; + foreach ($t->getAtomicTypes() as $lit) { + if ($lit instanceof TLiteralInt || $lit instanceof TLiteralString) { + $array_key_offsets []= $lit->value; + continue; + } + return; + } + } else { + $array_key_offsets []= $array_key[0] === '\'' || $array_key[0] === '"' ? substr($array_key, 1, -1) : $array_key; } - $array_key_offset = $array_key[0] === '\'' || $array_key[0] === '"' ? substr($array_key, 1, -1) : $array_key; - $base_key = implode($key_parts); - if (isset($existing_types[$base_key]) && $array_key_offset !== false) { - foreach ($existing_types[$base_key]->getAtomicTypes() as $base_atomic_type) { - if ($base_atomic_type instanceof TList) { - $base_atomic_type = $base_atomic_type->getKeyedArray(); - } - if ($base_atomic_type instanceof TKeyedArray + $result_type = $result_type->setPossiblyUndefined(count($array_key_offsets) > 1); + + foreach ($array_key_offsets as $array_key_offset) { + if (isset($existing_types[$base_key]) && $array_key_offset !== false) { + foreach ($existing_types[$base_key]->getAtomicTypes() as $base_atomic_type) { + if ($base_atomic_type instanceof TList) { + $base_atomic_type = $base_atomic_type->getKeyedArray(); + } + if ($base_atomic_type instanceof TKeyedArray || ($base_atomic_type instanceof TArray && !$base_atomic_type->isEmptyArray()) || $base_atomic_type instanceof TClassStringMap - ) { - $new_base_type = $existing_types[$base_key]; + ) { + $new_base_type = $existing_types[$base_key]; - if ($base_atomic_type instanceof TArray) { - $fallback_key_type = $base_atomic_type->type_params[0]; - $fallback_value_type = $base_atomic_type->type_params[1]; + if ($base_atomic_type instanceof TArray) { + $fallback_key_type = $base_atomic_type->type_params[0]; + $fallback_value_type = $base_atomic_type->type_params[1]; - $base_atomic_type = new TKeyedArray( - [ + $base_atomic_type = new TKeyedArray( + [ $array_key_offset => $result_type, - ], - null, - $fallback_key_type->isNever() ? null : [$fallback_key_type, $fallback_value_type], - ); - } elseif ($base_atomic_type instanceof TClassStringMap) { - // do nothing - } else { - $properties = $base_atomic_type->properties; - $properties[$array_key_offset] = $result_type; - if ($base_atomic_type->is_list + ], + null, + $fallback_key_type->isNever() ? null : [$fallback_key_type, $fallback_value_type], + ); + } elseif ($base_atomic_type instanceof TClassStringMap) { + // do nothing + } else { + $properties = $base_atomic_type->properties; + $properties[$array_key_offset] = $result_type; + if ($base_atomic_type->is_list && (!is_numeric($array_key_offset) || ($array_key_offset && !isset($properties[$array_key_offset-1]) ) ) - ) { - if ($base_atomic_type->fallback_params && is_numeric($array_key_offset)) { - $fallback = $base_atomic_type->fallback_params[1]->setPossiblyUndefined( - $result_type->isNever(), - ); - for ($x = 0; $x < $array_key_offset; $x++) { - $properties[$x] ??= $fallback; + ) { + if ($base_atomic_type->fallback_params && is_numeric($array_key_offset)) { + $fallback = $base_atomic_type->fallback_params[1]->setPossiblyUndefined( + $result_type->isNever(), + ); + for ($x = 0; $x < $array_key_offset; $x++) { + $properties[$x] ??= $fallback; + } + ksort($properties); + $base_atomic_type = $base_atomic_type->setProperties($properties); + } else { + // This should actually be a paradox + $base_atomic_type = new TKeyedArray( + $properties, + null, + $base_atomic_type->fallback_params, + false, + $base_atomic_type->from_docblock, + ); } - ksort($properties); - $base_atomic_type = $base_atomic_type->setProperties($properties); } else { - // This should actually be a paradox - $base_atomic_type = new TKeyedArray( - $properties, - null, - $base_atomic_type->fallback_params, - false, - $base_atomic_type->from_docblock, - ); + $base_atomic_type = $base_atomic_type->setProperties($properties); } - } else { - $base_atomic_type = $base_atomic_type->setProperties($properties); } - } - $new_base_type = $new_base_type->getBuilder()->addType($base_atomic_type)->freeze(); + $new_base_type = $new_base_type->getBuilder()->addType($base_atomic_type)->freeze(); - $changed_var_ids[$base_key . '[' . $array_key . ']'] = true; + $changed_var_ids[$base_key . '[' . $array_key . ']'] = true; - if ($key_parts[count($key_parts) - 1] === ']') { - self::adjustTKeyedArrayType( - $key_parts, - $existing_types, - $changed_var_ids, - $new_base_type, - ); - } + if ($key_parts[count($key_parts) - 1] === ']') { + self::adjustTKeyedArrayType( + $key_parts, + $existing_types, + $changed_var_ids, + $new_base_type, + ); + } - $existing_types[$base_key] = $new_base_type; - break; + $existing_types[$base_key] = $new_base_type; + break; + } } } } diff --git a/tests/Loop/ForeachTest.php b/tests/Loop/ForeachTest.php index d7b56e5fa48..fbbb6e445f9 100644 --- a/tests/Loop/ForeachTest.php +++ b/tests/Loop/ForeachTest.php @@ -1027,7 +1027,9 @@ function foo() : void { $arr = []; foreach ([1, 2, 3] as $i) { - $arr[$i]["a"] ??= 0; + if (!isset($arr[$i]["a"])) { + $arr[$i]["a"] = 0; + } $arr[$i]["a"] += 5; } diff --git a/tests/TypeReconciliation/IssetTest.php b/tests/TypeReconciliation/IssetTest.php index 19e7dbfcd0e..f9d24846e10 100644 --- a/tests/TypeReconciliation/IssetTest.php +++ b/tests/TypeReconciliation/IssetTest.php @@ -54,7 +54,36 @@ function t2(array $arr, int $i): array { $arr[$i] = 1; } return $arr; - }' + }', + ], + 'issetWithArrayAssignment2' => [ + 'code'=> ' [ + 'code'=> ' [ 'code' => ' Date: Fri, 1 Dec 2023 17:48:47 +0100 Subject: [PATCH 132/357] Fixup --- src/Psalm/Type/Reconciler.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 61e731862ff..f5e288639cf 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -1124,7 +1124,10 @@ private static function adjustTKeyedArrayType( return; } } else { - $array_key_offsets []= $array_key[0] === '\'' || $array_key[0] === '"' ? substr($array_key, 1, -1) : $array_key; + $array_key_offsets []= $array_key[0] === '\'' || $array_key[0] === '"' + ? substr($array_key, 1, -1) + : $array_key + ; } $base_key = implode($key_parts); From 0aeb87c21cd730a5bf7796724653671a5df89b6f Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 1 Dec 2023 17:57:50 +0100 Subject: [PATCH 133/357] Simplify --- tests/Loop/ForeachTest.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Loop/ForeachTest.php b/tests/Loop/ForeachTest.php index fbbb6e445f9..d7b56e5fa48 100644 --- a/tests/Loop/ForeachTest.php +++ b/tests/Loop/ForeachTest.php @@ -1027,9 +1027,7 @@ function foo() : void { $arr = []; foreach ([1, 2, 3] as $i) { - if (!isset($arr[$i]["a"])) { - $arr[$i]["a"] = 0; - } + $arr[$i]["a"] ??= 0; $arr[$i]["a"] += 5; } From 59fd539ab98b3401c6f25b57aaa8f00d163129e5 Mon Sep 17 00:00:00 2001 From: rarila Date: Fri, 1 Dec 2023 18:01:57 +0100 Subject: [PATCH 134/357] Fix POSIX only detection of absolute paths --- src/Psalm/Config/FileFilter.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Config/FileFilter.php b/src/Psalm/Config/FileFilter.php index 4ef5f993c4c..dd8fb31186a 100644 --- a/src/Psalm/Config/FileFilter.php +++ b/src/Psalm/Config/FileFilter.php @@ -7,6 +7,7 @@ use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use SimpleXMLElement; +use Symfony\Component\Filesystem\Path; use function array_filter; use function array_map; @@ -127,7 +128,7 @@ public static function loadFromArray( $resolve_symlinks = (bool) ($directory['resolveSymlinks'] ?? false); $declare_strict_types = (bool) ($directory['useStrictTypes'] ?? false); - if ($directory_path[0] === '/' && DIRECTORY_SEPARATOR === '/') { + if (Path::isAbsolute($directory_path)) { /** @var non-empty-string */ $prospective_directory_path = $directory_path; } else { From c6bf949c712b1565170d7ae1203d776ae18bfb6d Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sat, 2 Dec 2023 09:04:37 +0100 Subject: [PATCH 135/357] Fix CLI -r error Fix https://github.com/vimeo/psalm/issues/10418 --- src/Psalm/Internal/CliUtils.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/CliUtils.php b/src/Psalm/Internal/CliUtils.php index cdd4e281311..e90f73e4e5d 100644 --- a/src/Psalm/Internal/CliUtils.php +++ b/src/Psalm/Internal/CliUtils.php @@ -231,7 +231,7 @@ public static function getArguments(): array } if ($input_path[0] === '-' && strlen($input_path) === 2) { - if ($input_path[1] === 'c' || $input_path[1] === 'f') { + if ($input_path[1] === 'c' || $input_path[1] === 'f' || $input_path[1] === 'r') { ++$i; } continue; @@ -271,7 +271,7 @@ public static function getPathsToCheck($f_paths): ?array $input_path = $input_paths[$i]; if ($input_path[0] === '-' && strlen($input_path) === 2) { - if ($input_path[1] === 'c' || $input_path[1] === 'f') { + if ($input_path[1] === 'c' || $input_path[1] === 'f' || $input_path[1] === 'r') { ++$i; } continue; @@ -287,6 +287,7 @@ public static function getPathsToCheck($f_paths): ?array $ignored_arguments = array( 'config', 'printer', + 'root', ); if (in_array(substr($input_path, 2), $ignored_arguments, true)) { From 6eba2f564c499262bad320d6f697a3bdfc09e0fd Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sat, 2 Dec 2023 12:02:55 +0100 Subject: [PATCH 136/357] Fix return type of DOMXPath::query This can also return namespace nodes, which are not a child class of DOMNode. --- stubs/extensions/dom.phpstub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stubs/extensions/dom.phpstub b/stubs/extensions/dom.phpstub index f52153787d6..2520a479902 100644 --- a/stubs/extensions/dom.phpstub +++ b/stubs/extensions/dom.phpstub @@ -975,7 +975,7 @@ class DOMXPath public function evaluate(string $expression, ?DOMNode $contextNode = null, bool $registerNodeNS = true): mixed {} /** - * @return DOMNodeList|false + * @return DOMNodeList|false */ public function query(string $expression, ?DOMNode $contextNode = null, bool $registerNodeNS = true): mixed {} From b03b846682e2ee6b90c2c2742faa7b93821a7c81 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 30 Nov 2023 13:48:32 +0100 Subject: [PATCH 137/357] Emit UnusedPsalmSuppress issues for suppressed issues already removed by plugins --- psalm-baseline.xml | 28 ++++++++++++++++++++- src/Psalm/IssueBuffer.php | 13 ++++++---- tests/CodebaseTest.php | 51 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 49462a40934..12ccf4812d9 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + tags['variablesfrom'][0]]]> @@ -350,6 +350,32 @@ $cs[0] + + + $config_file_path !== null + + + getArgument('pluginName')]]> + getOption('config')]]> + + + + + $config_file_path !== null + + + getArgument('pluginName')]]> + getOption('config')]]> + + + + + $config_file_path !== null + + + getOption('config')]]> + + $callable_method_name diff --git a/src/Psalm/IssueBuffer.php b/src/Psalm/IssueBuffer.php index 9ca29ef701b..33fc16bc800 100644 --- a/src/Psalm/IssueBuffer.php +++ b/src/Psalm/IssueBuffer.php @@ -133,6 +133,14 @@ final class IssueBuffer */ public static function accepts(CodeIssue $e, array $suppressed_issues = [], bool $is_fixable = false): bool { + $config = Config::getInstance(); + $project_analyzer = ProjectAnalyzer::getInstance(); + $codebase = $project_analyzer->getCodebase(); + $event = new BeforeAddIssueEvent($e, $is_fixable, $codebase); + if ($config->eventDispatcher->dispatchBeforeAddIssue($event) === false) { + return false; + } + if (self::isSuppressed($e, $suppressed_issues)) { return false; } @@ -258,11 +266,6 @@ public static function add(CodeIssue $e, bool $is_fixable = false): bool $project_analyzer = ProjectAnalyzer::getInstance(); $codebase = $project_analyzer->getCodebase(); - $event = new BeforeAddIssueEvent($e, $is_fixable, $codebase); - if ($config->eventDispatcher->dispatchBeforeAddIssue($event) === false) { - return false; - } - $fqcn_parts = explode('\\', get_class($e)); $issue_type = array_pop($fqcn_parts); diff --git a/tests/CodebaseTest.php b/tests/CodebaseTest.php index f5291649826..254922c2060 100644 --- a/tests/CodebaseTest.php +++ b/tests/CodebaseTest.php @@ -6,6 +6,7 @@ use PhpParser\Node\Stmt\Class_; use Psalm\Codebase; use Psalm\Context; +use Psalm\Exception\CodeException; use Psalm\Exception\UnpopulatedClasslikeException; use Psalm\Issue\InvalidReturnStatement; use Psalm\Issue\InvalidReturnType; @@ -21,6 +22,9 @@ use function array_map; use function array_values; use function get_class; +use function getcwd; + +use const DIRECTORY_SEPARATOR; class CodebaseTest extends TestCase { @@ -246,4 +250,51 @@ public static function beforeAddIssue(BeforeAddIssueEvent $event): ?bool $this->analyzeFile('somefile.php', new Context); self::assertSame(0, IssueBuffer::getErrorCount()); } + /** + * @test + */ + public function addingCodeIssueIsMarkedAsRedundant(): void + { + $this->expectException(CodeException::class); + $this->expectExceptionMessage('UnusedPsalmSuppress'); + + $this->addFile( + (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', + 'getIssue(); + if ($issue->code_location->file_path !== (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php') { + return null; + } + if ($issue instanceof InvalidReturnStatement && $event->isFixable() === false) { + return false; + } elseif ($issue instanceof InvalidReturnType && $event->isFixable() === true) { + return false; + } + return null; + } + }; + + (new PluginRegistrationSocket($this->codebase->config, $this->codebase)) + ->registerHooksFromClass(get_class($eventHandler)); + + $this->analyzeFile( + (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', + new Context, + ); + } } From ee5e4b800f01e183c7366badde4ef39be7663623 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 3 Dec 2023 12:36:14 +0100 Subject: [PATCH 138/357] Update --- psalm.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psalm.xml.dist b/psalm.xml.dist index 1452823757e..816cdc02e87 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -12,7 +12,7 @@ limitMethodComplexity="true" errorBaseline="psalm-baseline.xml" findUnusedPsalmSuppress="true" - findUnusedBaselineEntry="true" + findUnusedBaselineEntry="false" > From de53638295356ba5b7a20f2d003be1e927f1e804 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 3 Dec 2023 13:06:36 +0100 Subject: [PATCH 139/357] Fixes --- psalm-baseline.xml | 37 +++++++++++++++++++++-- psalm.xml.dist | 2 +- src/Psalm/Internal/Cli/LanguageServer.php | 8 ++--- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 49462a40934..71cc2ef6324 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + tags['variablesfrom'][0]]]> @@ -216,13 +216,15 @@ + + $token_list[$iter] + $token_list[$iter] $token_list[$iter] $token_list[$iter] $token_list[$iter] $token_list[0] - $token_list[1] @@ -230,6 +232,11 @@ expr->getArgs()[0]]]> + + + + + $identifier_name @@ -350,6 +357,32 @@ $cs[0] + + + $config_file_path !== null + + + getArgument('pluginName')]]> + getOption('config')]]> + + + + + $config_file_path !== null + + + getArgument('pluginName')]]> + getOption('config')]]> + + + + + $config_file_path !== null + + + getOption('config')]]> + + $callable_method_name diff --git a/psalm.xml.dist b/psalm.xml.dist index 1452823757e..816cdc02e87 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -12,7 +12,7 @@ limitMethodComplexity="true" errorBaseline="psalm-baseline.xml" findUnusedPsalmSuppress="true" - findUnusedBaselineEntry="true" + findUnusedBaselineEntry="false" > diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index 07d5e6f93e8..1dc16fbe5bf 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -315,11 +315,9 @@ static function (string $arg) use ($valid_long_options): void { $path_to_config = CliUtils::getPathToConfig($options); - if (isset($options['tcp'])) { - if (!is_string($options['tcp'])) { - fwrite(STDERR, 'tcp url should be a string' . PHP_EOL); - exit(1); - } + if (isset($options['tcp']) && !is_string($options['tcp'])) { + fwrite(STDERR, 'tcp url should be a string' . PHP_EOL); + exit(1); } $config = CliUtils::initializeConfig( From 18a6c0b6e9aade82a2f3cc36e3a644ba70eaf539 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 3 Dec 2023 15:28:51 +0100 Subject: [PATCH 140/357] Implement by-ref closure use analysis --- .../Internal/Analyzer/ClosureAnalyzer.php | 41 ++------- .../Analyzer/FunctionLikeAnalyzer.php | 36 +++++++- .../Expression/Call/FunctionCallAnalyzer.php | 7 -- .../Internal/Diff/ClassStatementsDiffer.php | 2 - .../LanguageServer/LanguageServer.php | 7 -- tests/CallableTest.php | 12 +-- tests/ClosureTest.php | 84 ++++++++++++------- tests/ReferenceConstraintTest.php | 13 --- tests/UnusedVariableTest.php | 7 -- 9 files changed, 95 insertions(+), 114 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php index 9ed72b26914..8a69cf2fd10 100644 --- a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php @@ -13,7 +13,6 @@ use Psalm\Issue\UndefinedVariable; use Psalm\IssueBuffer; use Psalm\Type; -use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Union; @@ -133,12 +132,6 @@ public static function analyzeExpression( $use_var_id = '$' . $use->var->name; - // insert the ref into the current context if passed by ref, as whatever we're passing - // the closure to could execute it straight away. - if ($use->byRef && !$context->hasVariable($use_var_id)) { - $context->vars_in_scope[$use_var_id] = new Union([new TMixed()], ['by_ref' => true]); - } - if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph && $context->hasVariable($use_var_id) ) { @@ -154,7 +147,7 @@ public static function analyzeExpression( } $use_context->vars_in_scope[$use_var_id] = - $context->hasVariable($use_var_id) && !$use->byRef + $context->hasVariable($use_var_id) ? $context->vars_in_scope[$use_var_id] : Type::getMixed(); @@ -205,7 +198,12 @@ public static function analyzeExpression( $use_context->calling_method_id = $context->calling_method_id; $use_context->phantom_classes = $context->phantom_classes; - $closure_analyzer->analyze($use_context, $statements_analyzer->node_data, $context, false); + $byref_vars = []; + $closure_analyzer->analyze($use_context, $statements_analyzer->node_data, $context, false, $byref_vars); + + foreach ($byref_vars as $key => $value) { + $context->vars_in_scope[$key] = $value; + } if ($closure_analyzer->inferred_impure && $statements_analyzer->getSource() instanceof FunctionLikeAnalyzer @@ -229,7 +227,7 @@ public static function analyzeExpression( /** * @return false|null */ - public static function analyzeClosureUses( + private static function analyzeClosureUses( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\Closure $stmt, Context $context @@ -268,21 +266,6 @@ public static function analyzeClosureUses( continue; } - if ($use->byRef) { - $context->vars_in_scope[$use_var_id] = Type::getMixed(); - $context->vars_possibly_in_scope[$use_var_id] = true; - - if (!$statements_analyzer->hasVariable($use_var_id)) { - $statements_analyzer->registerVariable( - $use_var_id, - new CodeLocation($statements_analyzer, $use->var), - null, - ); - } - - return null; - } - if (!isset($context->vars_possibly_in_scope[$use_var_id])) { if ($context->check_variables) { if (IssueBuffer::accepts( @@ -329,14 +312,6 @@ public static function analyzeClosureUses( continue; } - } elseif ($use->byRef) { - $new_type = new Union([new TMixed()], [ - 'parent_nodes' => $context->vars_in_scope[$use_var_id]->parent_nodes, - ]); - - $context->remove($use_var_id); - - $context->vars_in_scope[$use_var_id] = $new_type; } } diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 3df1a6a1b0b..bf4378d9158 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -45,6 +45,8 @@ use Psalm\Issue\UnusedDocblockParam; use Psalm\Issue\UnusedParam; use Psalm\IssueBuffer; +use Psalm\Node\Expr\VirtualVariable; +use Psalm\Node\Stmt\VirtualWhile; use Psalm\Plugin\EventHandler\Event\AfterFunctionLikeAnalysisEvent; use Psalm\Storage\ClassLikeStorage; use Psalm\Storage\FunctionLikeParameter; @@ -149,14 +151,18 @@ public function __construct($function, SourceAnalyzer $source, FunctionLikeStora /** * @param bool $add_mutations whether or not to add mutations to this method + * @param array $byref_vars + * @param-out array $byref_vars * @return false|null * @psalm-suppress PossiblyUnusedReturnValue unused but seems important + * @psalm-suppress ComplexMethod Unavoidably complex */ public function analyze( Context $context, NodeDataProvider $type_provider, ?Context $global_context = null, - bool $add_mutations = false + bool $add_mutations = false, + array &$byref_vars = [] ): ?bool { $storage = $this->storage; @@ -235,9 +241,8 @@ public function analyze( $statements_analyzer = new StatementsAnalyzer($this, $type_provider); + $byref_uses = []; if ($this instanceof ClosureAnalyzer && $this->function instanceof Closure) { - $byref_uses = []; - foreach ($this->function->uses as $use) { if (!is_string($use->var->name)) { continue; @@ -352,6 +357,31 @@ public function analyze( (bool) $template_types, ); + if ($byref_uses) { + $ref_context = clone $context; + $var = '$__tmp_byref_closure_if__' . (int) $this->function->getAttribute('startFilePos'); + + $ref_context->vars_in_scope[$var] = Type::getBool(); + + $var = new VirtualVariable( + substr($var, 1), + ); + $virtual_while = new VirtualWhile( + $var, + $function_stmts, + ); + + $statements_analyzer->analyze( + [$virtual_while], + $ref_context, + ); + + foreach ($byref_uses as $var_id => $_) { + $byref_vars[$var_id] = $ref_context->vars_in_scope[$var_id]; + $context->vars_in_scope[$var_id] = $ref_context->vars_in_scope[$var_id]; + } + } + if ($storage->pure) { $context->pure = true; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index c5b60af8c80..98e192c72f7 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -404,13 +404,6 @@ public static function analyze( } } - if ($function_call_info->byref_uses) { - foreach ($function_call_info->byref_uses as $byref_use_var => $_) { - $context->vars_in_scope['$' . $byref_use_var] = Type::getMixed(); - $context->vars_possibly_in_scope['$' . $byref_use_var] = true; - } - } - if ($function_name instanceof PhpParser\Node\Name && $function_call_info->function_id) { NamedFunctionCallHandler::handle( $statements_analyzer, diff --git a/src/Psalm/Internal/Diff/ClassStatementsDiffer.php b/src/Psalm/Internal/Diff/ClassStatementsDiffer.php index 235cff8f400..c6d54c869c8 100644 --- a/src/Psalm/Internal/Diff/ClassStatementsDiffer.php +++ b/src/Psalm/Internal/Diff/ClassStatementsDiffer.php @@ -89,7 +89,6 @@ static function ( $start_diff = $b_start - $a_start; $line_diff = $b->getLine() - $a->getLine(); - /** @psalm-suppress MixedArrayAssignment */ $diff_map[] = [$a_start, $a_end, $start_diff, $line_diff]; return true; @@ -183,7 +182,6 @@ static function ( } if (!$signature_change && !$body_change) { - /** @psalm-suppress MixedArrayAssignment */ $diff_map[] = [$a_start, $a_end, $b_start - $a_start, $b->getLine() - $a->getLine()]; } diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 3a0ff9a1e02..7062885e790 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -777,11 +777,8 @@ function (IssueData $issue_data): Diagnostic { //Process Baseline $file = $issue_data->file_name; $type = $issue_data->type; - /** @psalm-suppress MixedArrayAccess */ if (isset($issue_baseline[$file][$type]) && $issue_baseline[$file][$type]['o'] > 0) { - /** @psalm-suppress MixedArrayAccess, MixedArgument */ if ($issue_baseline[$file][$type]['o'] === count($issue_baseline[$file][$type]['s'])) { - /** @psalm-suppress MixedArrayAccess, MixedAssignment */ $position = array_search( str_replace("\r\n", "\n", trim($issue_data->selected_text)), $issue_baseline[$file][$type]['s'], @@ -790,16 +787,12 @@ function (IssueData $issue_data): Diagnostic { if ($position !== false) { $issue_data->severity = IssueData::SEVERITY_INFO; - /** @psalm-suppress MixedArgument */ array_splice($issue_baseline[$file][$type]['s'], $position, 1); - /** @psalm-suppress MixedArrayAssignment, MixedOperand, MixedAssignment */ $issue_baseline[$file][$type]['o']--; } } else { - /** @psalm-suppress MixedArrayAssignment */ $issue_baseline[$file][$type]['s'] = []; $issue_data->severity = IssueData::SEVERITY_INFO; - /** @psalm-suppress MixedArrayAssignment, MixedOperand, MixedAssignment */ $issue_baseline[$file][$type]['o']--; } } diff --git a/tests/CallableTest.php b/tests/CallableTest.php index 200177e1d7b..23054bf9a6e 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -22,9 +22,9 @@ function run_function(\Closure $fnc) { /** * @return void - * @psalm-suppress MixedArgument */ function f() { + $data = 0; run_function( /** * @return void @@ -1786,16 +1786,6 @@ function takesCallable(callable $c) : void {} takesCallable(function() { return; });', ], - 'byRefUsesAlwaysMixed' => [ - 'code' => ' [ 'code' => ' [ 'code' => ' [ + '$testBefore===' => '123', + '$testInsideBefore===' => "'test'|123|null", + '$testInsideAfter===' => "'test'|null", + '$test===' => "'test'|123", + + '$doNotContaminate===' => '123', + ], + ], + 'byRefUseSelf' => [ + 'code' => ' [ + 'code' => ' [ 'code' => ' 'ArgumentTypeCoercion - src' . DIRECTORY_SEPARATOR . 'somefile.php:13:28 - Argument 1 of takesB expects B, but parent type A provided', ], - 'closureByRefUseToMixed' => [ - 'code' => ' 'MixedReturnStatement', - ], 'noCrashWhenComparingIllegitimateCallable' => [ 'code' => 'getString());', ], - 'makeByRefUseMixed' => [ - 'code' => ' [], - 'ignored_issues' => ['MixedArgument'], - ], 'assignByRefToMixed' => [ 'code' => ' [ @@ -2234,10 +2232,6 @@ function string_to_float(string $a): float { ], 'allowUseByRef' => [ 'code' => ' Date: Sun, 3 Dec 2023 15:30:57 +0100 Subject: [PATCH 141/357] Fixup --- psalm-baseline.xml | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 49462a40934..84bbae63a53 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + tags['variablesfrom'][0]]]> @@ -350,6 +350,32 @@ $cs[0] + + + $config_file_path !== null + + + getArgument('pluginName')]]> + getOption('config')]]> + + + + + $config_file_path !== null + + + getArgument('pluginName')]]> + getOption('config')]]> + + + + + $config_file_path !== null + + + getOption('config')]]> + + $callable_method_name From a2d89d09902c8b422ea4bbf444f5a66384d3e322 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 3 Dec 2023 15:32:20 +0100 Subject: [PATCH 142/357] Fixup --- psalm.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psalm.xml.dist b/psalm.xml.dist index 1452823757e..816cdc02e87 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -12,7 +12,7 @@ limitMethodComplexity="true" errorBaseline="psalm-baseline.xml" findUnusedPsalmSuppress="true" - findUnusedBaselineEntry="true" + findUnusedBaselineEntry="false" > From 4ed0fe934f3902ab8d7a3e26bcdd41d9ea3cf6eb Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 6 Dec 2023 14:12:19 +0100 Subject: [PATCH 143/357] Fix shaped array class string key combination --- .../Assignment/ArrayAssignmentAnalyzer.php | 4 +++- src/Psalm/Internal/Type/TypeCombination.php | 3 +++ src/Psalm/Internal/Type/TypeCombiner.php | 12 +++++++++++- src/Psalm/Type/Reconciler.php | 4 +++- tests/ArrayAssignmentTest.php | 18 ++++++++++++++++++ 5 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php index 576fe45a011..01150233a66 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php @@ -351,7 +351,9 @@ private static function updateTypeWithKeyValues( if (!$has_matching_objectlike_property && !$has_matching_string) { $properties = []; $classStrings = []; - $current_type = $current_type->setPossiblyUndefined(count($key_values) > 1); + $current_type = $current_type->setPossiblyUndefined( + $current_type->possibly_undefined || count($key_values) > 1, + ); foreach ($key_values as $key_value) { $properties[$key_value->value] = $current_type; if ($key_value instanceof TLiteralClassString) { diff --git a/src/Psalm/Internal/Type/TypeCombination.php b/src/Psalm/Internal/Type/TypeCombination.php index 0706315d1ac..0e4cc225383 100644 --- a/src/Psalm/Internal/Type/TypeCombination.php +++ b/src/Psalm/Internal/Type/TypeCombination.php @@ -55,6 +55,9 @@ final class TypeCombination /** @var array */ public array $objectlike_entries = []; + /** @var array */ + public array $objectlike_class_string_keys = []; + public bool $objectlike_sealed = true; public ?Union $objectlike_key_type = null; diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 3f841da4fa3..b48387acb8c 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -667,6 +667,7 @@ private static function scrapeTypeProperties( $has_defined_keys = false; + $class_strings = $type->class_strings ?? []; foreach ($type->properties as $candidate_property_name => $candidate_property_type) { $value_type = $combination->objectlike_entries[$candidate_property_name] ?? null; @@ -705,6 +706,15 @@ private static function scrapeTypeProperties( ); } + if (isset($combination->objectlike_class_string_keys[$candidate_property_name])) { + $combination->objectlike_class_string_keys[$candidate_property_name] = + $combination->objectlike_class_string_keys[$candidate_property_name] + && ($class_strings[$candidate_property_name] ?? false); + } else { + $combination->objectlike_class_string_keys[$candidate_property_name] = + ($class_strings[$candidate_property_name] ?? false); + } + unset($missing_entries[$candidate_property_name]); } @@ -1421,7 +1431,7 @@ private static function handleKeyedArrayEntries( } else { $objectlike = new TKeyedArray( $combination->objectlike_entries, - null, + $combination->objectlike_class_string_keys, $sealed || $fallback_key_type === null || $fallback_value_type === null ? null : [$fallback_key_type, $fallback_value_type], diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index f5e288639cf..baae339644d 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -1132,7 +1132,9 @@ private static function adjustTKeyedArrayType( $base_key = implode($key_parts); - $result_type = $result_type->setPossiblyUndefined(count($array_key_offsets) > 1); + $result_type = $result_type->setPossiblyUndefined( + $result_type->possibly_undefined || count($array_key_offsets) > 1, + ); foreach ($array_key_offsets as $array_key_offset) { if (isset($existing_types[$base_key]) && $array_key_offset !== false) { diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 364b6def325..790f901b398 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -55,6 +55,24 @@ public function providerValidCodeParse(): iterable '$resultOpt===' => 'array{a?: true, b?: true}', ], ], + 'assignUnionOfLiteralsClassKeys' => [ + 'code' => ' $v) { + $vv = new $k; + }', + 'assertions' => [ + '$result===' => 'array{a::class: true, b::class: true}', + ], + ], 'genericArrayCreationWithSingleIntValue' => [ 'code' => ' Date: Wed, 6 Dec 2023 14:23:45 +0100 Subject: [PATCH 144/357] Fix --- src/Psalm/Internal/Type/TypeCombiner.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index b48387acb8c..2854b1cb25f 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -1431,7 +1431,7 @@ private static function handleKeyedArrayEntries( } else { $objectlike = new TKeyedArray( $combination->objectlike_entries, - $combination->objectlike_class_string_keys, + array_filter($combination->objectlike_class_string_keys), $sealed || $fallback_key_type === null || $fallback_value_type === null ? null : [$fallback_key_type, $fallback_value_type], From d3b7f3f0b4c27a3436dc3c5d25df56c1c28e5cc4 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 6 Dec 2023 14:47:24 +0100 Subject: [PATCH 145/357] Fix --- src/Psalm/Internal/Type/TypeCombination.php | 2 +- src/Psalm/Internal/Type/TypeCombiner.php | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Type/TypeCombination.php b/src/Psalm/Internal/Type/TypeCombination.php index 0e4cc225383..94e64793e8b 100644 --- a/src/Psalm/Internal/Type/TypeCombination.php +++ b/src/Psalm/Internal/Type/TypeCombination.php @@ -55,7 +55,7 @@ final class TypeCombination /** @var array */ public array $objectlike_entries = []; - /** @var array */ + /** @var array */ public array $objectlike_class_string_keys = []; public bool $objectlike_sealed = true; diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 2854b1cb25f..773e3f71a47 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -706,6 +706,12 @@ private static function scrapeTypeProperties( ); } + unset($missing_entries[$candidate_property_name]); + + if (is_int($candidate_property_name)) { + continue; + } + if (isset($combination->objectlike_class_string_keys[$candidate_property_name])) { $combination->objectlike_class_string_keys[$candidate_property_name] = $combination->objectlike_class_string_keys[$candidate_property_name] @@ -714,8 +720,6 @@ private static function scrapeTypeProperties( $combination->objectlike_class_string_keys[$candidate_property_name] = ($class_strings[$candidate_property_name] ?? false); } - - unset($missing_entries[$candidate_property_name]); } if ($type->fallback_params) { From 76458e0b50d8871366b0f72c257562dafc592859 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 6 Dec 2023 14:52:54 +0100 Subject: [PATCH 146/357] Add test --- tests/ArrayAssignmentTest.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 790f901b398..713756ad2a9 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -73,6 +73,20 @@ class b {} '$result===' => 'array{a::class: true, b::class: true}', ], ], + 'assignUnionOfLiteralsClassKeys2' => [ + 'code' => ' true]; + + foreach ([a::class, b::class] as $k) { + $result[$k] = true; + }', + 'assertions' => [ + '$result===' => 'array{a::class: true, b::class: true, c: true}', + ], + ], 'genericArrayCreationWithSingleIntValue' => [ 'code' => ' Date: Wed, 6 Dec 2023 11:20:18 -0600 Subject: [PATCH 147/357] Replace remaining POSIX only absolute path detection These were missed in #10441. Fixes "Could not resolve config path" error on Windows (#10418). --- src/Psalm/Config.php | 2 +- src/Psalm/Config/FileFilter.php | 2 +- .../Statements/Expression/IncludeAnalyzer.php | 17 +++-------------- .../PhpVisitor/Reflector/ExpressionScanner.php | 10 ++-------- 4 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index d8ba8e8f416..0cd65f165f4 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -1315,7 +1315,7 @@ private static function fromXmlAndPaths( } // we need an absolute path for checks - if ($path[0] !== '/' && DIRECTORY_SEPARATOR === '/') { + if (Path::isRelative($path)) { $prospective_path = $base_dir . DIRECTORY_SEPARATOR . $path; } else { $prospective_path = $path; diff --git a/src/Psalm/Config/FileFilter.php b/src/Psalm/Config/FileFilter.php index dd8fb31186a..8626deca4ee 100644 --- a/src/Psalm/Config/FileFilter.php +++ b/src/Psalm/Config/FileFilter.php @@ -247,7 +247,7 @@ public static function loadFromArray( foreach ($config['file'] as $file) { $file_path = (string) ($file['name'] ?? ''); - if ($file_path[0] === '/' && DIRECTORY_SEPARATOR === '/') { + if (Path::isAbsolute($file_path)) { /** @var non-empty-string */ $prospective_file_path = $file_path; } else { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php index 24db2b6d92e..6c4a36ab2bf 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php @@ -20,6 +20,7 @@ use Psalm\IssueBuffer; use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; use Psalm\Type\TaintKind; +use Symfony\Component\Filesystem\Path; use function constant; use function defined; @@ -93,13 +94,7 @@ public static function analyze( $include_path = self::resolveIncludePath($path_to_file, dirname($statements_analyzer->getFilePath())); $path_to_file = $include_path ?: $path_to_file; - if (DIRECTORY_SEPARATOR === '/') { - $is_path_relative = $path_to_file[0] !== DIRECTORY_SEPARATOR; - } else { - $is_path_relative = !preg_match('~^[A-Z]:\\\\~i', $path_to_file); - } - - if ($is_path_relative) { + if (Path::isRelative($path_to_file)) { $path_to_file = $config->base_dir . DIRECTORY_SEPARATOR . $path_to_file; } } else { @@ -285,13 +280,7 @@ public static function getPathTo( string $file_name, Config $config ): ?string { - if (DIRECTORY_SEPARATOR === '/') { - $is_path_relative = $file_name[0] !== DIRECTORY_SEPARATOR; - } else { - $is_path_relative = !preg_match('~^[A-Z]:\\\\~i', $file_name); - } - - if ($is_path_relative) { + if (Path::isRelative($file_name)) { $file_name = $config->base_dir . DIRECTORY_SEPARATOR . $file_name; } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php index 6afd142bfa5..6e7887ff158 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php @@ -20,13 +20,13 @@ use Psalm\Storage\FileStorage; use Psalm\Storage\FunctionLikeStorage; use Psalm\Type; +use Symfony\Component\Filesystem\Path; use function assert; use function defined; use function dirname; use function explode; use function in_array; -use function preg_match; use function strpos; use function strtolower; use function substr; @@ -316,13 +316,7 @@ public static function visitInclude( $include_path = IncludeAnalyzer::resolveIncludePath($path_to_file, dirname($file_storage->file_path)); $path_to_file = $include_path ?: $path_to_file; - if (DIRECTORY_SEPARATOR === '/') { - $is_path_relative = $path_to_file[0] !== DIRECTORY_SEPARATOR; - } else { - $is_path_relative = !preg_match('~^[A-Z]:\\\\~i', $path_to_file); - } - - if ($is_path_relative) { + if (Path::isRelative($path_to_file)) { $path_to_file = $config->base_dir . DIRECTORY_SEPARATOR . $path_to_file; } } else { From dbded437ada472efbae3c6b6c018f2647ec6b086 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 7 Dec 2023 11:29:33 +0100 Subject: [PATCH 148/357] Small assertion fix --- src/Psalm/Type/Reconciler.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index f5e288639cf..287e9f78816 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -171,6 +171,7 @@ public static function reconcileKeyedTypes( $has_negation = false; $has_isset = false; $has_inverted_isset = false; + $has_inverted_key_exists = false; $has_truthy_or_falsy_or_empty = false; $has_empty = false; $has_count_check = false; @@ -201,7 +202,9 @@ public static function reconcileKeyedTypes( && $new_type_part_part instanceof IsIdentical; $has_inverted_isset = $has_inverted_isset - || $new_type_part_part instanceof IsNotIsset + || $new_type_part_part instanceof IsNotIsset; + + $has_inverted_key_exists = $has_inverted_key_exists || $new_type_part_part instanceof ArrayKeyDoesNotExist; $has_count_check = $has_count_check @@ -221,6 +224,7 @@ public static function reconcileKeyedTypes( $code_location, $has_isset, $has_inverted_isset, + $has_inverted_key_exists, $has_empty, $inside_loop, $has_object_array_access, @@ -334,7 +338,7 @@ public static function reconcileKeyedTypes( if ($type_changed || $failed_reconciliation) { $changed_var_ids[$key] = true; - if (substr($key, -1) === ']' && !$has_inverted_isset && !$has_empty && !$is_equality) { + if (substr($key, -1) === ']' && !$has_inverted_isset && !$has_inverted_key_exists && !$has_empty && !$is_equality) { self::adjustTKeyedArrayType( $key_parts, $existing_types, @@ -648,6 +652,7 @@ private static function getValueForKey( ?CodeLocation $code_location, bool $has_isset, bool $has_inverted_isset, + bool $has_inverted_key_exists, bool $has_empty, bool $inside_loop, bool &$has_object_array_access @@ -723,11 +728,11 @@ private static function getValueForKey( $new_base_type_candidate = $existing_key_type_part->type_params[1]; - if ($new_base_type_candidate->isMixed() && !$has_isset && !$has_inverted_isset) { + if ($new_base_type_candidate->isMixed() && !$has_isset && !$has_inverted_isset && !$has_inverted_key_exists) { return $new_base_type_candidate; } - if (($has_isset || $has_inverted_isset) && isset($new_assertions[$new_base_key])) { + if (($has_isset || $has_inverted_isset || $has_inverted_key_exists) && isset($new_assertions[$new_base_key])) { if ($has_inverted_isset && $new_base_key === $key) { $new_base_type_candidate = $new_base_type_candidate->getBuilder(); $new_base_type_candidate->addType(new TNull); @@ -756,7 +761,7 @@ private static function getValueForKey( } elseif ($existing_key_type_part instanceof TString) { $new_base_type_candidate = Type::getString(); } elseif ($existing_key_type_part instanceof TNamedObject - && ($has_isset || $has_inverted_isset) + && ($has_isset || $has_inverted_isset || $has_inverted_key_exists) ) { $has_object_array_access = true; From bfd167515b47323cfe823adea6a696fe15c7372e Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 7 Dec 2023 12:25:03 +0100 Subject: [PATCH 149/357] the new version has no changes --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 7e41dd27eeb..c4dda05c87e 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "dnoegel/php-xdg-base-dir": "^0.1.1", "felixfbecker/advanced-json-rpc": "^3.1", "felixfbecker/language-server-protocol": "^1.5.2", - "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1", + "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", "nikic/php-parser": "^4.16", "sebastian/diff": "^4.0 || ^5.0", From 6650bd8a572e886e677f7c323a4c3031a3ad37c9 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 7 Dec 2023 12:31:21 +0100 Subject: [PATCH 150/357] cs-fix --- src/Psalm/Type/Reconciler.php | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 287e9f78816..751d76f63cc 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -338,7 +338,12 @@ public static function reconcileKeyedTypes( if ($type_changed || $failed_reconciliation) { $changed_var_ids[$key] = true; - if (substr($key, -1) === ']' && !$has_inverted_isset && !$has_inverted_key_exists && !$has_empty && !$is_equality) { + if (substr($key, -1) === ']' + && !$has_inverted_isset + && !$has_inverted_key_exists + && !$has_empty + && !$is_equality + ) { self::adjustTKeyedArrayType( $key_parts, $existing_types, @@ -728,11 +733,17 @@ private static function getValueForKey( $new_base_type_candidate = $existing_key_type_part->type_params[1]; - if ($new_base_type_candidate->isMixed() && !$has_isset && !$has_inverted_isset && !$has_inverted_key_exists) { + if ($new_base_type_candidate->isMixed() + && !$has_isset + && !$has_inverted_isset + && !$has_inverted_key_exists + ) { return $new_base_type_candidate; } - if (($has_isset || $has_inverted_isset || $has_inverted_key_exists) && isset($new_assertions[$new_base_key])) { + if (($has_isset || $has_inverted_isset || $has_inverted_key_exists) + && isset($new_assertions[$new_base_key]) + ) { if ($has_inverted_isset && $new_base_key === $key) { $new_base_type_candidate = $new_base_type_candidate->getBuilder(); $new_base_type_candidate->addType(new TNull); From d5bac4d51d9dae580dc0846a219f84b42b1052f1 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 7 Dec 2023 12:46:34 +0100 Subject: [PATCH 151/357] Emit AfterCodebasePopulatedEvent even on partial scans --- src/Psalm/Internal/Analyzer/ProjectAnalyzer.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index 3dd7a646038..ed22c279d97 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -1065,6 +1065,10 @@ public function checkPaths(array $paths_to_check): void $this->config->visitStubFiles($this->codebase, $this->progress); + $event = new AfterCodebasePopulatedEvent($this->codebase); + + $this->config->eventDispatcher->dispatchAfterCodebasePopulated($event); + $this->progress->startAnalyzingFiles(); $this->codebase->analyzer->analyzeFiles( From 0d3485b588a572d4f5f13324ee96a623c0b03779 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 7 Dec 2023 13:04:59 +0100 Subject: [PATCH 152/357] Commit just first part of fix for now --- tests/ArrayAssignmentTest.php | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 713756ad2a9..790f901b398 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -73,20 +73,6 @@ class b {} '$result===' => 'array{a::class: true, b::class: true}', ], ], - 'assignUnionOfLiteralsClassKeys2' => [ - 'code' => ' true]; - - foreach ([a::class, b::class] as $k) { - $result[$k] = true; - }', - 'assertions' => [ - '$result===' => 'array{a::class: true, b::class: true, c: true}', - ], - ], 'genericArrayCreationWithSingleIntValue' => [ 'code' => ' Date: Thu, 7 Dec 2023 17:20:37 +0100 Subject: [PATCH 153/357] fix composer scripts running with inconsistent php versions --- composer.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 7e41dd27eeb..d1678884c54 100644 --- a/composer.json +++ b/composer.json @@ -110,16 +110,16 @@ "psalter" ], "scripts": { - "cs": "phpcs -ps", - "cs-fix": "phpcbf -ps", - "lint": "parallel-lint ./src ./tests", + "cs": "@php phpcs -ps", + "cs-fix": "@php phpcbf -ps", + "lint": "@php parallel-lint ./src ./tests", "phpunit": [ "Composer\\Config::disableProcessTimeout", - "paratest --runner=WrapperRunner" + "@php paratest --runner=WrapperRunner" ], "phpunit-std": [ "Composer\\Config::disableProcessTimeout", - "phpunit" + "@php phpunit" ], "verify-callmap": "@php phpunit tests/Internal/Codebase/InternalCallMapHandlerTest.php", "psalm": "@php ./psalm", From 576ecd66e6c909bb63a8e106e8fe573cf0d1c358 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 7 Dec 2023 17:29:22 +0100 Subject: [PATCH 154/357] Fix #10460 --- .../Codebase/ConstantTypeResolver.php | 4 ++-- tests/ArrayAssignmentTest.php | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php index 866b5180e14..4bc718e3e49 100644 --- a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php +++ b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php @@ -216,8 +216,8 @@ public static function resolve( return new TArray([Type::getArrayKey(), Type::getMixed()]); } - foreach ($spread_array->properties as $spread_array_type) { - $properties[$auto_key++] = $spread_array_type; + foreach ($spread_array->properties as $k => $spread_array_type) { + $properties[is_string($k) ? $k : $auto_key++] = $spread_array_type; } continue; } diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 364b6def325..d016298c44a 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -1280,6 +1280,29 @@ function foo(array $arr) : string { 'ignored_issues' => [], 'php_version' => '8.1', ], + 'constantArraySpreadWithString' => [ + 'code' => ' "a", + "b" => "b", + ]; + } + + class ChildClass extends BaseClass { + public const A = [ + ...parent::KEYS, + "c" => "c", + ]; + } + + $a = ChildClass::A;', + 'assertions' => [ + '$a===' => "array{a: 'a', b: 'b', c: 'c'}", + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], 'listPropertyAssignmentAfterIsset' => [ 'code' => ' Date: Thu, 7 Dec 2023 16:12:03 +0100 Subject: [PATCH 155/357] dont combine empty string with numeric-string Fix https://github.com/vimeo/psalm/issues/6646 --- src/Psalm/Internal/Type/TypeCombiner.php | 3 +++ tests/TypeCombinationTest.php | 32 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 773e3f71a47..e21d41b0559 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -1080,6 +1080,9 @@ private static function scrapeStringProperties( if ($has_only_numeric_strings) { $combination->value_types['string'] = $type; + } elseif (count($combination->strings) === 1 && !$has_only_non_empty_strings) { + $combination->value_types['string'] = $type; + return; } elseif ($has_only_non_empty_strings) { $combination->value_types['string'] = new TNonEmptyString(); } else { diff --git a/tests/TypeCombinationTest.php b/tests/TypeCombinationTest.php index c3371ef2a6b..b891e84dcc2 100644 --- a/tests/TypeCombinationTest.php +++ b/tests/TypeCombinationTest.php @@ -88,6 +88,38 @@ function expectsTraversableOrArray($_a): void } ', ], + 'emptyStringNumericStringDontCombine' => [ + 'code' => ' [ + 'code' => ' Date: Thu, 7 Dec 2023 15:22:56 -0700 Subject: [PATCH 156/357] Fixed docblock spacing in supported_annotations.md `@psalm-internal` example --- docs/annotating_code/supported_annotations.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/annotating_code/supported_annotations.md b/docs/annotating_code/supported_annotations.md index ba14e7d67c9..070f3ddd682 100644 --- a/docs/annotating_code/supported_annotations.md +++ b/docs/annotating_code/supported_annotations.md @@ -263,9 +263,9 @@ is not within the given namespace. Date: Fri, 8 Dec 2023 12:31:42 +0100 Subject: [PATCH 157/357] Fix iteration over weakmaps --- stubs/CoreGenericClasses.phpstub | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/stubs/CoreGenericClasses.phpstub b/stubs/CoreGenericClasses.phpstub index 2b7b76da7e4..c93c1d8f01e 100644 --- a/stubs/CoreGenericClasses.phpstub +++ b/stubs/CoreGenericClasses.phpstub @@ -492,6 +492,16 @@ final class WeakMap implements ArrayAccess, Countable, IteratorAggregate, Traver * @return void */ public function offsetUnset($offset) {} + + /** + * Create a new iterator from an ArrayObject instance + * @link http://php.net/manual/en/arrayobject.getiterator.php + * + * @return \Traversable An iterator from an ArrayObject. + * + * @since 5.0.0 + */ + public function getIterator() { } } class mysqli From 25be3c1d88d400cd9eca3dd20eac8ab49e7e40a6 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 8 Dec 2023 12:48:46 +0100 Subject: [PATCH 158/357] Fix --- stubs/CoreGenericClasses.phpstub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stubs/CoreGenericClasses.phpstub b/stubs/CoreGenericClasses.phpstub index c93c1d8f01e..24ba0e50539 100644 --- a/stubs/CoreGenericClasses.phpstub +++ b/stubs/CoreGenericClasses.phpstub @@ -497,7 +497,7 @@ final class WeakMap implements ArrayAccess, Countable, IteratorAggregate, Traver * Create a new iterator from an ArrayObject instance * @link http://php.net/manual/en/arrayobject.getiterator.php * - * @return \Traversable An iterator from an ArrayObject. + * @return \Traversable An iterator from an ArrayObject. * * @since 5.0.0 */ From 20ae081ee1665dd69d2686a843ae5a137f0bfa18 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Tue, 12 Dec 2023 07:51:21 +0100 Subject: [PATCH 159/357] fix psalm v4 hardcoded in tests --- tests/Config/ConfigTest.php | 5 +++-- tests/Config/PluginTest.php | 5 +++-- tests/ProjectCheckerTest.php | 5 +++-- tests/ReportOutputTest.php | 3 ++- tests/StubTest.php | 5 +++-- tests/TestCase.php | 5 +++-- 6 files changed, 17 insertions(+), 11 deletions(-) diff --git a/tests/Config/ConfigTest.php b/tests/Config/ConfigTest.php index 88273122b36..e8fe9a94c40 100644 --- a/tests/Config/ConfigTest.php +++ b/tests/Config/ConfigTest.php @@ -17,6 +17,7 @@ use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; use Psalm\Internal\Scanner\FileScanner; +use Psalm\Internal\VersionUtils; use Psalm\Issue\TooManyArguments; use Psalm\Issue\UndefinedFunction; use Psalm\Tests\Config\Plugin\FileTypeSelfRegisteringPlugin; @@ -58,11 +59,11 @@ public static function setUpBeforeClass(): void self::$config = new TestConfig(); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', VersionUtils::getPsalmVersion()); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', VersionUtils::getPhpParserVersion()); } } diff --git a/tests/Config/PluginTest.php b/tests/Config/PluginTest.php index 130fd2d3304..bfd98cffc0b 100644 --- a/tests/Config/PluginTest.php +++ b/tests/Config/PluginTest.php @@ -12,6 +12,7 @@ use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; +use Psalm\Internal\VersionUtils; use Psalm\IssueBuffer; use Psalm\Plugin\EventHandler\AfterCodebasePopulatedInterface; use Psalm\Plugin\EventHandler\AfterEveryFunctionCallAnalysisInterface; @@ -46,11 +47,11 @@ public static function setUpBeforeClass(): void self::$config = new TestConfig(); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', VersionUtils::getPsalmVersion()); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', VersionUtils::getPhpParserVersion()); } } diff --git a/tests/ProjectCheckerTest.php b/tests/ProjectCheckerTest.php index cce7da9d697..3e7bf7af31c 100644 --- a/tests/ProjectCheckerTest.php +++ b/tests/ProjectCheckerTest.php @@ -8,6 +8,7 @@ use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; +use Psalm\Internal\VersionUtils; use Psalm\IssueBuffer; use Psalm\Plugin\EventHandler\AfterCodebasePopulatedInterface; use Psalm\Plugin\EventHandler\Event\AfterCodebasePopulatedEvent; @@ -43,11 +44,11 @@ public static function setUpBeforeClass(): void self::$config = new TestConfig(); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', VersionUtils::getPsalmVersion()); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', VersionUtils::getPhpParserVersion()); } } diff --git a/tests/ReportOutputTest.php b/tests/ReportOutputTest.php index 8864247abe3..67aeedaca4b 100644 --- a/tests/ReportOutputTest.php +++ b/tests/ReportOutputTest.php @@ -10,6 +10,7 @@ use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; +use Psalm\Internal\VersionUtils; use Psalm\IssueBuffer; use Psalm\Report; use Psalm\Report\JsonReport; @@ -109,7 +110,7 @@ public function testSarifReport(): void 'driver' => [ 'name' => 'Psalm', 'informationUri' => 'https://psalm.dev', - 'version' => '4.0.0', + 'version' => VersionUtils::getPsalmVersion(), 'rules' => [ [ 'id' => '246', diff --git a/tests/StubTest.php b/tests/StubTest.php index 6c835a65d8d..f12fb943ed8 100644 --- a/tests/StubTest.php +++ b/tests/StubTest.php @@ -13,6 +13,7 @@ use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; +use Psalm\Internal\VersionUtils; use Psalm\Tests\Internal\Provider\FakeParserCacheProvider; use function define; @@ -37,11 +38,11 @@ public static function setUpBeforeClass(): void self::$config = new TestConfig(); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', VersionUtils::getPsalmVersion()); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', VersionUtils::getPhpParserVersion()); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 2da89b3558c..39f02d46703 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -12,6 +12,7 @@ use Psalm\Internal\RuntimeCaches; use Psalm\Internal\Type\TypeParser; use Psalm\Internal\Type\TypeTokenizer; +use Psalm\Internal\VersionUtils; use Psalm\IssueBuffer; use Psalm\Tests\Internal\Provider\FakeParserCacheProvider; use Psalm\Type\Union; @@ -56,11 +57,11 @@ public static function setUpBeforeClass(): void ini_set('memory_limit', '-1'); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', VersionUtils::getPsalmVersion()); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', VersionUtils::getPhpParserVersion()); } parent::setUpBeforeClass(); From 0fd789cdcc2afd0784d8dd87eccd08482e2cbac9 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:44:17 +0100 Subject: [PATCH 160/357] Fix type not equal when parent parent nodes are only populated if taint/unused variable analysis is enabled --- src/Psalm/Type/Atomic/TArray.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Type/Atomic/TArray.php b/src/Psalm/Type/Atomic/TArray.php index 06477607592..54dfef34117 100644 --- a/src/Psalm/Type/Atomic/TArray.php +++ b/src/Psalm/Type/Atomic/TArray.php @@ -84,7 +84,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool } foreach ($this->type_params as $i => $type_param) { - if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality)) { + if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality, false)) { return false; } } From 679a492609100e586fce6e356d1b378f98d9da06 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:54:35 +0100 Subject: [PATCH 161/357] other atomics --- src/Psalm/Type/Atomic/TClassStringMap.php | 2 +- src/Psalm/Type/Atomic/TGenericObject.php | 2 +- src/Psalm/Type/Atomic/TIterable.php | 2 +- src/Psalm/Type/Atomic/TKeyedArray.php | 6 +++--- src/Psalm/Type/Atomic/TList.php | 2 +- src/Psalm/Type/Atomic/TObjectWithProperties.php | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Psalm/Type/Atomic/TClassStringMap.php b/src/Psalm/Type/Atomic/TClassStringMap.php index d15d297f10e..56b43ccf597 100644 --- a/src/Psalm/Type/Atomic/TClassStringMap.php +++ b/src/Psalm/Type/Atomic/TClassStringMap.php @@ -216,7 +216,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return false; } - if (!$this->value_param->equals($other_type->value_param, $ensure_source_equality)) { + if (!$this->value_param->equals($other_type->value_param, $ensure_source_equality, false)) { return false; } diff --git a/src/Psalm/Type/Atomic/TGenericObject.php b/src/Psalm/Type/Atomic/TGenericObject.php index c362085f565..415458de135 100644 --- a/src/Psalm/Type/Atomic/TGenericObject.php +++ b/src/Psalm/Type/Atomic/TGenericObject.php @@ -111,7 +111,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool } foreach ($this->type_params as $i => $type_param) { - if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality)) { + if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality, false)) { return false; } } diff --git a/src/Psalm/Type/Atomic/TIterable.php b/src/Psalm/Type/Atomic/TIterable.php index 6b6c9ea32ab..1f67bfb5602 100644 --- a/src/Psalm/Type/Atomic/TIterable.php +++ b/src/Psalm/Type/Atomic/TIterable.php @@ -115,7 +115,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool } foreach ($this->type_params as $i => $type_param) { - if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality)) { + if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality, false)) { return false; } } diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index dfddbe81334..3bd7e2a65e4 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -658,11 +658,11 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool } if ($this->fallback_params !== null && $other_type->fallback_params !== null) { - if (!$this->fallback_params[0]->equals($other_type->fallback_params[0])) { + if (!$this->fallback_params[0]->equals($other_type->fallback_params[0], false, false)) { return false; } - if (!$this->fallback_params[1]->equals($other_type->fallback_params[1])) { + if (!$this->fallback_params[1]->equals($other_type->fallback_params[1], false, false)) { return false; } } @@ -672,7 +672,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return false; } - if (!$property_type->equals($other_type->properties[$property_name], $ensure_source_equality)) { + if (!$property_type->equals($other_type->properties[$property_name], $ensure_source_equality, false)) { return false; } } diff --git a/src/Psalm/Type/Atomic/TList.php b/src/Psalm/Type/Atomic/TList.php index 13c44e5b453..7d102709919 100644 --- a/src/Psalm/Type/Atomic/TList.php +++ b/src/Psalm/Type/Atomic/TList.php @@ -205,7 +205,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return false; } - if (!$this->type_param->equals($other_type->type_param, $ensure_source_equality)) { + if (!$this->type_param->equals($other_type->type_param, $ensure_source_equality, false)) { return false; } diff --git a/src/Psalm/Type/Atomic/TObjectWithProperties.php b/src/Psalm/Type/Atomic/TObjectWithProperties.php index cae5be7e7f7..b681459bfaa 100644 --- a/src/Psalm/Type/Atomic/TObjectWithProperties.php +++ b/src/Psalm/Type/Atomic/TObjectWithProperties.php @@ -207,7 +207,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return false; } - if (!$property_type->equals($other_type->properties[$property_name], $ensure_source_equality)) { + if (!$property_type->equals($other_type->properties[$property_name], $ensure_source_equality, false)) { return false; } } From 3c045b30a7a2aad2f2e9395ba179a0d136351391 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 7 Dec 2023 12:05:15 +0100 Subject: [PATCH 162/357] fix false positive ArgumentTypeCoercion for callback param when unsealed and all optional --- .../Type/Comparator/ArrayTypeComparator.php | 13 +++++++++++++ tests/CallableTest.php | 16 ++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php index c440526fea5..122bc65d70e 100644 --- a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php @@ -49,6 +49,19 @@ public static function isContainedBy( return true; } + if ($container_type_part instanceof TKeyedArray + && $input_type_part instanceof TArray + && !$container_type_part->is_list + && !$container_type_part->isNonEmpty() + && !$container_type_part->isSealed() + && $input_type_part->equals( + $container_type_part->getGenericArrayType($container_type_part->isNonEmpty()), + false, + ) + ) { + return true; + } + if ($container_type_part instanceof TKeyedArray && $input_type_part instanceof TArray ) { diff --git a/tests/CallableTest.php b/tests/CallableTest.php index 23054bf9a6e..fc8c36d212f 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -1892,6 +1892,22 @@ function addHandler(string $_message, callable $_handler): void {} return [1, 2, 3]; });', ], + 'unsealedAllOptionalCbParam' => [ + 'code' => ') $arg + * @return void + */ + function foo($arg) {} + + /** + * @param array{a?: string}&array $cb_arg + * @return void + */ + function bar($cb_arg) {} + + foo("bar");', + ], ]; } From 761f390d9bb4803d8cf35fb2934db389838b66d4 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 12 Dec 2023 18:51:31 +0100 Subject: [PATCH 163/357] Use same parameter names in stubs --- stubs/CoreGenericClasses.phpstub | 18 +++++------ stubs/CoreGenericIterators.phpstub | 18 +++++------ stubs/SPL.phpstub | 52 +++++++++++++++--------------- 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/stubs/CoreGenericClasses.phpstub b/stubs/CoreGenericClasses.phpstub index 2b7b76da7e4..62e0ff33750 100644 --- a/stubs/CoreGenericClasses.phpstub +++ b/stubs/CoreGenericClasses.phpstub @@ -158,46 +158,46 @@ class ArrayObject implements IteratorAggregate, ArrayAccess, Serializable, Count * Returns whether the requested index exists * @link http://php.net/manual/en/arrayobject.offsetexists.php * - * @param TKey $index The index being checked. + * @param TKey $offset The index being checked. * @return bool true if the requested index exists, otherwise false * * @since 5.0.0 */ - public function offsetExists($index) { } + public function offsetExists($offset) { } /** * Returns the value at the specified index * @link http://php.net/manual/en/arrayobject.offsetget.php * - * @param TKey $index The index with the value. + * @param TKey $offset The index with the value. * @return TValue The value at the specified index or false. * * @since 5.0.0 */ - public function offsetGet($index) { } + public function offsetGet($offset) { } /** * Sets the value at the specified index to newval * @link http://php.net/manual/en/arrayobject.offsetset.php * - * @param TKey $index The index being set. - * @param TValue $newval The new value for the index. + * @param TKey $offset The index being set. + * @param TValue $value The new value for the index. * @return void * * @since 5.0.0 */ - public function offsetSet($index, $newval) { } + public function offsetSet($offset, $value) { } /** * Unsets the value at the specified index * @link http://php.net/manual/en/arrayobject.offsetunset.php * - * @param TKey $index The index being unset. + * @param TKey $offset The index being unset. * @return void * * @since 5.0.0 */ - public function offsetUnset($index) { } + public function offsetUnset($offset) { } /** * Appends the value diff --git a/stubs/CoreGenericIterators.phpstub b/stubs/CoreGenericIterators.phpstub index 43a7bb1f85c..48abad51dea 100644 --- a/stubs/CoreGenericIterators.phpstub +++ b/stubs/CoreGenericIterators.phpstub @@ -185,30 +185,30 @@ class ArrayIterator implements SeekableIterator, ArrayAccess, Serializable, Coun public function __construct($array = array(), $flags = 0) { } /** - * @param TKey $index The offset being checked. + * @param TKey $offset The offset being checked. * @return bool true if the offset exists, otherwise false */ - public function offsetExists($index) { } + public function offsetExists($offset) { } /** - * @param TKey $index The offset to get the value from. + * @param TKey $offset The offset to get the value from. * @return TValue|null The value at offset index, null when accessing invalid indexes * @psalm-ignore-nullable-return */ - public function offsetGet($index) { } + public function offsetGet($offset) { } /** - * @param TKey $index The index to set for. - * @param TValue $newval The new value to store at the index. + * @param TKey $offset The index to set for. + * @param TValue $value The new value to store at the index. * @return void */ - public function offsetSet($index, $newval) { } + public function offsetSet($offset, $value) { } /** - * @param TKey $index The offset to unset. + * @param TKey $offset The offset to unset. * @return void */ - public function offsetUnset($index) { } + public function offsetUnset($offset) { } /** * @param TValue $value The value to append. diff --git a/stubs/SPL.phpstub b/stubs/SPL.phpstub index 288ceba770a..7a623c933a0 100644 --- a/stubs/SPL.phpstub +++ b/stubs/SPL.phpstub @@ -15,14 +15,14 @@ class SplDoublyLinkedList implements Iterator, Countable, ArrayAccess, Serializa /** * Add/insert a new value at the specified index * - * @param int $index The index where the new value is to be inserted. - * @param TValue $newval The new value for the index. + * @param int $offset The index where the new value is to be inserted. + * @param TValue $value The new value for the index. * @return void * * @link https://php.net/spldoublylinkedlist.add * @since 5.5.0 */ - public function add($index, $newval) {} + public function add($offset, $value) {} /** * Pops a node from the end of the doubly linked list @@ -107,49 +107,49 @@ class SplDoublyLinkedList implements Iterator, Countable, ArrayAccess, Serializa public function isEmpty() {} /** - * Returns whether the requested $index exists + * Returns whether the requested $offset exists * @link https://php.net/manual/en/spldoublylinkedlist.offsetexists.php * - * @param int $index The index being checked. + * @param int $offset The index being checked. * @return bool true if the requested index exists, otherwise false * * @since 5.3.0 */ - public function offsetExists($index) {} + public function offsetExists($offset) {} /** - * Returns the value at the specified $index + * Returns the value at the specified $offset * @link https://php.net/manual/en/spldoublylinkedlist.offsetget.php * - * @param int $index The index with the value. + * @param int $offset The index with the value. * @return TValue The value at the specified index. * * @since 5.3.0 */ - public function offsetGet($index) {} + public function offsetGet($offset) {} /** - * Sets the value at the specified $index to $newval + * Sets the value at the specified $offset to $value * @link https://php.net/manual/en/spldoublylinkedlist.offsetset.php * - * @param int $index The index being set. - * @param TValue $newval The new value for the index. + * @param int $offset The index being set. + * @param TValue $value The new value for the index. * @return void * * @since 5.3.0 */ - public function offsetSet($index, $newval) {} + public function offsetSet($offset, $value) {} /** - * Unsets the value at the specified $index + * Unsets the value at the specified $offset * @link https://php.net/manual/en/spldoublylinkedlist.offsetunset.php * - * @param int $index The index being unset. + * @param int $offset The index being unset. * @return void * * @since 5.3.0 */ - public function offsetUnset($index) {} + public function offsetUnset($offset) {} /** * Return current array entry @@ -297,46 +297,46 @@ class SplFixedArray implements Iterator, ArrayAccess, Countable { * Returns whether the specified index exists * @link https://php.net/manual/en/splfixedarray.offsetexists.php * - * @param int $index The index being checked. + * @param int $offset The index being checked. * @return bool true if the requested index exists, and false otherwise. * * @since 5.3.0 */ - public function offsetExists(int $index): bool {} + public function offsetExists(int $offset): bool {} /** * Sets a new value at a specified index * @link https://php.net/manual/en/splfixedarray.offsetset.php * - * @param int $index The index being sent. - * @param TValue $newval The new value for the index + * @param int $offset The index being sent. + * @param TValue $value The new value for the index * @return void * * @since 5.3.0 */ - public function offsetSet(int $index, $newval): void {} + public function offsetSet(int $offset, $value): void {} /** - * Unsets the value at the specified $index + * Unsets the value at the specified $offset * @link https://php.net/manual/en/splfixedarray.offsetunset.php * - * @param int $index The index being unset + * @param int $offset The index being unset * @return void * * @since 5.3.0 */ - public function offsetUnset(int $index): void {} + public function offsetUnset(int $offset): void {} /** * Returns the value at the specified index * @link https://php.net/manual/en/splfixedarray.offsetget.php * - * @param int $index The index with the value + * @param int $offset The index with the value * @return TValue The value at the specified index * * @since 5.3.0 */ - public function offsetGet(int $index) {} + public function offsetGet(int $offset) {} } From 82ff58228092cb15f6bd17adfba5aa607179a4a9 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Tue, 12 Dec 2023 23:29:54 +0100 Subject: [PATCH 164/357] add error for invalid array key type in docblock --- src/Psalm/Internal/Type/TypeParser.php | 58 ++++++++++++++++++++++++++ tests/AnnotationTest.php | 10 ++++- tests/ArrayAssignmentTest.php | 9 ++-- tests/KeyOfArrayTest.php | 18 ++++---- 4 files changed, 81 insertions(+), 14 deletions(-) diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 770e7efc958..b498c9944e5 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -50,11 +50,13 @@ use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; +use Psalm\Type\Atomic\TNever; use Psalm\Type\Atomic\TNonEmptyArray; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TObject; use Psalm\Type\Atomic\TObjectWithProperties; use Psalm\Type\Atomic\TPropertiesOf; +use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateIndexedAccess; use Psalm\Type\Atomic\TTemplateKeyOf; use Psalm\Type\Atomic\TTemplateParam; @@ -643,6 +645,34 @@ private static function getTypeFromGenericTree( throw new TypeParseTreeException('Too many template parameters for array'); } + foreach ($generic_params[0]->getAtomicTypes() as $key => $atomic_type) { + if ($atomic_type instanceof TInt + || $atomic_type instanceof TString + || $atomic_type instanceof TArrayKey + || $atomic_type instanceof TClassConstant // @todo resolve and check types + || $atomic_type instanceof TMixed + || $atomic_type instanceof TNever + || $atomic_type instanceof TTemplateParam + || $atomic_type instanceof TValueOf + ) { + continue; + } + + if ($codebase->register_stub_files || $codebase->register_autoload_files) { + $builder = $generic_params[0]->getBuilder(); + $builder->removeType($key); + + if (count($generic_params[0]->getAtomicTypes()) <= 1) { + $builder = $builder->addType(new TArrayKey($from_docblock)); + } + + $generic_params[0] = $builder->freeze(); + continue; + } + + throw new TypeParseTreeException('Invalid array key type ' . $atomic_type->getKey()); + } + return new TArray($generic_params, $from_docblock); } @@ -671,6 +701,34 @@ private static function getTypeFromGenericTree( throw new TypeParseTreeException('Too many template parameters for non-empty-array'); } + foreach ($generic_params[0]->getAtomicTypes() as $key => $atomic_type) { + if ($atomic_type instanceof TInt + || $atomic_type instanceof TString + || $atomic_type instanceof TArrayKey + || $atomic_type instanceof TClassConstant // @todo resolve and check types + || $atomic_type instanceof TMixed + || $atomic_type instanceof TNever + || $atomic_type instanceof TTemplateParam + || $atomic_type instanceof TValueOf + ) { + continue; + } + + if ($codebase->register_stub_files || $codebase->register_autoload_files) { + $builder = $generic_params[0]->getBuilder(); + $builder->removeType($key); + + if (count($generic_params[0]->getAtomicTypes()) <= 1) { + $builder = $builder->addType(new TArrayKey($from_docblock)); + } + + $generic_params[0] = $builder->freeze(); + continue; + } + + throw new TypeParseTreeException('Invalid array key type ' . $atomic_type->getKey()); + } + return new TNonEmptyArray($generic_params, null, null, 'non-empty-array', $from_docblock); } diff --git a/tests/AnnotationTest.php b/tests/AnnotationTest.php index b06bd217e7f..e0855c3bdc8 100644 --- a/tests/AnnotationTest.php +++ b/tests/AnnotationTest.php @@ -1366,7 +1366,15 @@ public function barBar() { }', 'error_message' => 'MissingDocblockType', ], - + 'invalidArrayKeyType' => [ + 'code' => ' $arg + * @return void + */ + function foo($arg) {}', + 'error_message' => 'InvalidDocblock', + ], 'invalidClassMethodReturnBrackets' => [ 'code' => ' [ 'code' => ' 'InvalidArrayOffset', + 'error_message' => 'MixedArrayAccess', + 'ignored_issues' => ['InvalidDocblock'], ], 'unpackTypedIterableWithStringKeysIntoArray' => [ 'code' => ' [ 'code' => '|array> + * @return key-of|array> */ - function getKey(bool $asFloat) { - if ($asFloat) { - return 42.0; + function getKey(bool $asString) { + if ($asString) { + return "42"; } return 42; } @@ -194,14 +194,14 @@ public function getKey() { ', 'error_message' => 'InvalidReturnStatement', ], - 'noStringAllowedInKeyOfIntFloatArray' => [ + 'noStringAllowedInKeyOfIntFloatStringArray' => [ 'code' => '|array> + * @return key-of|array<"42.0", string>> */ - function getKey(bool $asFloat) { - if ($asFloat) { - return 42.0; + function getKey(bool $asInt) { + if ($asInt) { + return 42; } return "42"; } From 9be7fceb594eb1fdfe9886c39c54db48a815afca Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 13 Dec 2023 00:17:43 +0100 Subject: [PATCH 165/357] Fix literal string keys int not handled as int as PHP does Fix https://github.com/vimeo/psalm/issues/8680 See also https://github.com/vimeo/psalm/issues/9295 --- src/Psalm/Internal/Type/TypeParser.php | 29 ++++++++++++++ tests/ArrayAssignmentTest.php | 23 +++++++++++ tests/ArrayKeysTest.php | 55 ++++++++++++++++++++++++++ 3 files changed, 107 insertions(+) diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index b498c9944e5..58718eae2dd 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -48,6 +48,7 @@ use Psalm\Type\Atomic\TLiteralClassString; use Psalm\Type\Atomic\TLiteralFloat; use Psalm\Type\Atomic\TLiteralInt; +use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNever; @@ -85,6 +86,7 @@ use function defined; use function end; use function explode; +use function filter_var; use function get_class; use function in_array; use function is_int; @@ -98,6 +100,9 @@ use function strtolower; use function strtr; use function substr; +use function trim; + +use const FILTER_VALIDATE_INT; /** * @psalm-suppress InaccessibleProperty Allowed during construction @@ -646,6 +651,18 @@ private static function getTypeFromGenericTree( } foreach ($generic_params[0]->getAtomicTypes() as $key => $atomic_type) { + // PHP 8 values with whitespace after number are counted as numeric + // and filter_var treats them as such too + if ($atomic_type instanceof TLiteralString + && trim($atomic_type->value) === $atomic_type->value + && ($string_to_int = filter_var($atomic_type->value, FILTER_VALIDATE_INT)) !== false + ) { + $builder = $generic_params[0]->getBuilder(); + $builder->removeType($key); + $generic_params[0] = $builder->addType(new TLiteralInt($string_to_int, $from_docblock))->freeze(); + continue; + } + if ($atomic_type instanceof TInt || $atomic_type instanceof TString || $atomic_type instanceof TArrayKey @@ -702,6 +719,18 @@ private static function getTypeFromGenericTree( } foreach ($generic_params[0]->getAtomicTypes() as $key => $atomic_type) { + // PHP 8 values with whitespace after number are counted as numeric + // and filter_var treats them as such too + if ($atomic_type instanceof TLiteralString + && trim($atomic_type->value) === $atomic_type->value + && ($string_to_int = filter_var($atomic_type->value, FILTER_VALIDATE_INT)) !== false + ) { + $builder = $generic_params[0]->getBuilder(); + $builder->removeType($key); + $generic_params[0] = $builder->addType(new TLiteralInt($string_to_int, $from_docblock))->freeze(); + continue; + } + if ($atomic_type instanceof TInt || $atomic_type instanceof TString || $atomic_type instanceof TArrayKey diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 769ebadc290..fa65da8223c 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -2115,6 +2115,29 @@ function getQueryParams(): array return $queryParams; }', ], + 'stringIntKeys' => [ + 'code' => ' $arg + * @return bool + */ + function foo($arg) { + foreach ($arg as $k => $v) { + if ( $k === 15 ) { + return true; + } + + if ( $k === 17 ) { + return false; + } + } + + return true; + } + + $x = ["15" => "a", 17 => "b"]; + foo($x);', + ], ]; } diff --git a/tests/ArrayKeysTest.php b/tests/ArrayKeysTest.php index 070ea52873b..e3f9cc897a4 100644 --- a/tests/ArrayKeysTest.php +++ b/tests/ArrayKeysTest.php @@ -97,6 +97,33 @@ function getKey() { } ', ], + 'literalStringAsIntArrayKey' => [ + 'code' => ' [ + "from" => "79268724911", + "to" => "74950235931", + ], + "b" => [ + "from" => "79313044964", + "to" => "78124169167", + ], + ]; + + private const SIP_FORMAT = "sip:%s@voip.test.com:9090"; + + /** @return array */ + public function test(): array { + $redirects = []; + foreach (self::REDIRECTS as $redirect) { + $redirects[$redirect["from"]] = sprintf(self::SIP_FORMAT, $redirect["to"]); + } + + return $redirects; + } + }', + ], ]; } @@ -126,6 +153,34 @@ function getKeys() { ', 'error_message' => 'InvalidReturnStatement', ], + 'literalStringAsIntArrayKey' => [ + 'code' => ' [ + "from" => "79268724911", + "to" => "74950235931", + ], + "b" => [ + "from" => "79313044964", + "to" => "78124169167", + ], + ]; + + private const SIP_FORMAT = "sip:%s@voip.test.com:9090"; + + /** @return array */ + public function test(): array { + $redirects = []; + foreach (self::REDIRECTS as $redirect) { + $redirects[$redirect["from"]] = sprintf(self::SIP_FORMAT, $redirect["to"]); + } + + return $redirects; + } + }', + 'error_message' => 'InvalidReturnStatement', + ], ]; } } From e981778383aa1048c1f0a641517cfda746d4d696 Mon Sep 17 00:00:00 2001 From: Floris Luiten Date: Wed, 13 Dec 2023 13:27:35 +0100 Subject: [PATCH 166/357] Fix "Cannot locate enable" with Symfony project Fixes the "Cannot locate enable" error when using psalm-plugin enable with a Symfony project --- src/Psalm/Config.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 0cd65f165f4..82c696c0bcd 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -1302,7 +1302,17 @@ private static function fromXmlAndPaths( // any paths passed via CLI should be added to the projectFiles // as they're getting analyzed like if they are part of the project // ProjectAnalyzer::getInstance()->check_paths_files is not populated at this point in time - $paths_to_check = CliUtils::getPathsToCheck(null); + + $paths_to_check = null; + + global $argv; + + // Hack for Symfonys own argv resolution. + // @see https://github.com/vimeo/psalm/issues/10465 + if (!isset($argv[0]) || basename($argv[0]) !== 'psalm-plugin') { + $paths_to_check = CliUtils::getPathsToCheck(null); + } + if ($paths_to_check !== null) { $paths_to_add_to_project_files = array(); foreach ($paths_to_check as $path) { From 108f6267124377263ac1c99e5f22e105367b0842 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 13 Dec 2023 13:59:26 +0100 Subject: [PATCH 167/357] fix literal int/string comparisons only using one literal Fix https://github.com/vimeo/psalm/issues/9552 --- .../Statements/Expression/AssertionFinder.php | 9 ++++++-- tests/TypeReconciliation/ConditionalTest.php | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index de4d2022aaa..740994506ed 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -555,8 +555,13 @@ private static function scrapeEqualityAssertions( $var_assertion_different = $var_type->getId() !== $intersection_type->getId(); + $all_assertions = []; + foreach ($intersection_type->getAtomicTypes() as $atomic_type) { + $all_assertions[] = new IsIdentical($atomic_type); + } + if ($var_name_left && $var_assertion_different) { - $if_types[$var_name_left] = [[new IsIdentical($intersection_type->getSingleAtomic())]]; + $if_types[$var_name_left] = [$all_assertions]; } $var_name_right = ExpressionIdentifier::getExtendedVarId( @@ -568,7 +573,7 @@ private static function scrapeEqualityAssertions( $other_assertion_different = $other_type->getId() !== $intersection_type->getId(); if ($var_name_right && $other_assertion_different) { - $if_types[$var_name_right] = [[new IsIdentical($intersection_type->getSingleAtomic())]]; + $if_types[$var_name_right] = [$all_assertions]; } return $if_types ? [$if_types] : []; diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index ca69a70a358..819d871eebb 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -580,6 +580,29 @@ function foo($a, string $b) : void { } }', ], + 'reconcileMultipleLiteralStrings' => [ + 'code' => ' [ 'code' => ' Date: Wed, 13 Dec 2023 14:10:22 +0100 Subject: [PATCH 168/357] Fix https://psalm.dev/r/aada187f50 where 2 union types are not intersected and the condition contains both types --- .../Statements/Expression/AssertionFinder.php | 2 +- tests/TypeReconciliation/ConditionalTest.php | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index 740994506ed..4b81cb4adb9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -544,7 +544,7 @@ private static function scrapeEqualityAssertions( // both side of the Identical can be asserted to the intersection of both $intersection_type = Type::intersectUnionTypes($var_type, $other_type, $codebase); - if ($intersection_type !== null && $intersection_type->isSingle()) { + if ($intersection_type !== null) { $if_types = []; $var_name_left = ExpressionIdentifier::getExtendedVarId( diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index 819d871eebb..c8f3f9ab7bb 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -603,6 +603,21 @@ function foo($param, $param2) { } }', ], + 'reconcileMultipleUnionIntersection' => [ + 'code' => ' [ 'code' => ' Date: Wed, 13 Dec 2023 14:43:55 +0100 Subject: [PATCH 169/357] fix bug equality assertion with int and float setting wrong type - required so previous commit works --- .../Statements/Expression/AssertionFinder.php | 2 +- src/Psalm/Type.php | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index 4b81cb4adb9..8c6aaa03ce3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -542,7 +542,7 @@ private static function scrapeEqualityAssertions( ); } else { // both side of the Identical can be asserted to the intersection of both - $intersection_type = Type::intersectUnionTypes($var_type, $other_type, $codebase); + $intersection_type = Type::intersectUnionTypes($var_type, $other_type, $codebase, false, false); if ($intersection_type !== null) { $if_types = []; diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 6903c94094a..1215799f785 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -712,7 +712,9 @@ public static function combineUnionTypes( public static function intersectUnionTypes( ?Union $type_1, ?Union $type_2, - Codebase $codebase + Codebase $codebase, + bool $allow_interface_equality = false, + bool $allow_float_int_equality = true ): ?Union { if ($type_2 === null && $type_1 === null) { throw new UnexpectedValueException('At least one type must be provided to combine'); @@ -766,6 +768,8 @@ public static function intersectUnionTypes( $type_2_atomic, $codebase, $intersection_performed, + $allow_interface_equality, + $allow_float_int_equality, ); if (null !== $intersection_atomic) { @@ -838,7 +842,9 @@ private static function intersectAtomicTypes( Atomic $type_1_atomic, Atomic $type_2_atomic, Codebase $codebase, - bool &$intersection_performed + bool &$intersection_performed, + bool $allow_interface_equality = false, + bool $allow_float_int_equality = true ): ?Atomic { $intersection_atomic = null; $wider_type = null; @@ -884,6 +890,8 @@ private static function intersectAtomicTypes( $codebase, $type_2_atomic, $type_1_atomic, + $allow_interface_equality, + $allow_float_int_equality, )) { $intersection_atomic = $type_2_atomic; $wider_type = $type_1_atomic; @@ -892,6 +900,8 @@ private static function intersectAtomicTypes( $codebase, $type_1_atomic, $type_2_atomic, + $allow_interface_equality, + $allow_float_int_equality, )) { $intersection_atomic = $type_1_atomic; $wider_type = $type_2_atomic; From af3978281edc7e4feec1205b72fb31cbf725d27c Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 13 Dec 2023 15:05:48 +0100 Subject: [PATCH 170/357] remove previously broken test https://github.com/vimeo/psalm/issues/10487 --- tests/TypeReconciliation/ValueTest.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/TypeReconciliation/ValueTest.php b/tests/TypeReconciliation/ValueTest.php index 10df002174c..527b4f6f76f 100644 --- a/tests/TypeReconciliation/ValueTest.php +++ b/tests/TypeReconciliation/ValueTest.php @@ -272,17 +272,6 @@ function foo($f) : void { if ($s === "a") {} }', ], - 'moreValueReconciliation' => [ - 'code' => ' [ 'code' => ' Date: Wed, 13 Dec 2023 15:10:15 +0100 Subject: [PATCH 171/357] add missing phpdoc in new tests --- tests/TypeReconciliation/ConditionalTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index c8f3f9ab7bb..99650002db3 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -608,6 +608,7 @@ function foo($param, $param2) { /** * @param int|string $param * @param float|string $param2 + * @return void */ function foo($param, $param2) { if ($param === $param2) { @@ -616,7 +617,7 @@ function foo($param, $param2) { } } - function takesString(string $arg) {}', + function takesString(string $arg): void {}', ], 'reconcileNullableStringWithWeakEquality' => [ 'code' => ' Date: Wed, 13 Dec 2023 15:30:43 +0100 Subject: [PATCH 172/357] Fix https://github.com/vimeo/psalm/issues/9267 --- .../Statements/Expression/Call/ArgumentAnalyzer.php | 2 +- tests/FunctionCallTest.php | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index 706534fbbe5..a1df71add81 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -902,7 +902,7 @@ public static function verifyType( $input_type, $param_type, true, - true, + !isset($param_type->getAtomicTypes()['true']), $union_comparison_results, ); diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index f712af9dacd..2b73d655018 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -2358,6 +2358,17 @@ function fooFoo(int $a): void {} fooFoo("string");', 'error_message' => 'InvalidArgument', ], + 'invalidArgumentFalseTrueExpected' => [ + 'code' => ' 'InvalidArgument', + ], 'builtinFunctioninvalidArgumentWithWeakTypes' => [ 'code' => ' Date: Thu, 14 Dec 2023 09:44:28 +0300 Subject: [PATCH 173/357] Fix Uncaught RuntimeException: PHP Error: Uninitialized string offset 0 when $pattern is empty --- .../Call/FunctionCallReturnTypeFetcher.php | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index e15fd22a71d..8695053c6f8 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -636,17 +636,19 @@ private static function taintReturnType( $first_arg_value = $first_stmt_type->getSingleStringLiteral()->value; $pattern = substr($first_arg_value, 1, -1); + if (strlen(trim($pattern)) > 0) { + $pattern = trim($pattern); + if ($pattern[0] === '[' + && $pattern[1] === '^' + && substr($pattern, -1) === ']' + ) { + $pattern = substr($pattern, 2, -1); - if ($pattern[0] === '[' - && $pattern[1] === '^' - && substr($pattern, -1) === ']' - ) { - $pattern = substr($pattern, 2, -1); - - if (self::simpleExclusion($pattern, $first_arg_value[0])) { - $removed_taints[] = 'html'; - $removed_taints[] = 'has_quotes'; - $removed_taints[] = 'sql'; + if (self::simpleExclusion($pattern, $first_arg_value[0])) { + $removed_taints[] = 'html'; + $removed_taints[] = 'has_quotes'; + $removed_taints[] = 'sql'; + } } } } From c8748dc5c976cc52940b93b6c3689869c1363978 Mon Sep 17 00:00:00 2001 From: mu3ic Date: Thu, 14 Dec 2023 09:54:32 +0300 Subject: [PATCH 174/357] Add trim() in global use --- .../Statements/Expression/Call/FunctionCallReturnTypeFetcher.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index 8695053c6f8..55557576e6b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -51,6 +51,7 @@ use function strpos; use function strtolower; use function substr; +use function trim; /** * @internal From d6cf9faebbfc0c21710b99eae01e9d4188aae76e Mon Sep 17 00:00:00 2001 From: Antonio del Olmo Date: Fri, 15 Dec 2023 11:14:53 +0100 Subject: [PATCH 175/357] Add support for Override attribute --- stubs/CoreGenericAttributes.phpstub | 6 ++++++ tests/AttributeTest.php | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/stubs/CoreGenericAttributes.phpstub b/stubs/CoreGenericAttributes.phpstub index 7aa6400df06..92abe9542f8 100644 --- a/stubs/CoreGenericAttributes.phpstub +++ b/stubs/CoreGenericAttributes.phpstub @@ -6,6 +6,12 @@ final class AllowDynamicProperties public function __construct() {} } +#[Attribute(Attribute::TARGET_METHOD)] +final class Override +{ + public function __construct() {} +} + #[Attribute(Attribute::TARGET_PARAMETER)] final class SensitiveParameter { diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index f1051773882..ddb5b1f5fd9 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -293,6 +293,22 @@ class Foo 'ignored_issues' => [], 'php_version' => '8.2', ], + 'override' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.3', + ], 'sensitiveParameter' => [ 'code' => ' Date: Sun, 17 Dec 2023 16:06:03 +0100 Subject: [PATCH 176/357] strtok always returns a non-empty-string when it does not return false --- dictionaries/CallMap.php | 4 ++-- dictionaries/CallMap_historical.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 5f3dba9c880..853b78ed8a0 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -12923,8 +12923,8 @@ 'strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'strspn' => ['int', 'string'=>'string', 'characters'=>'string', 'offset='=>'int', 'length='=>'?int'], 'strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], -'strtok' => ['string|false', 'string'=>'string', 'token'=>'string'], -'strtok\'1' => ['string|false', 'string'=>'string'], +'strtok' => ['non-empty-string|false', 'string'=>'string', 'token'=>'string'], +'strtok\'1' => ['non-empty-string|false', 'string'=>'string'], 'strtolower' => ['lowercase-string', 'string'=>'string'], 'strtotime' => ['int|false', 'datetime'=>'string', 'baseTimestamp='=>'?int'], 'strtoupper' => ['string', 'string'=>'string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 864b668bd0b..60e798e09cb 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -14333,8 +14333,8 @@ 'strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], 'strspn' => ['int', 'string'=>'string', 'characters'=>'string', 'offset='=>'int', 'length='=>'int'], 'strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string|int', 'before_needle='=>'bool'], - 'strtok' => ['string|false', 'string'=>'string', 'token'=>'string'], - 'strtok\'1' => ['string|false', 'string'=>'string'], + 'strtok' => ['non-empty-string|false', 'string'=>'string', 'token'=>'string'], + 'strtok\'1' => ['non-empty-string|false', 'string'=>'string'], 'strtolower' => ['lowercase-string', 'string'=>'string'], 'strtotime' => ['int|false', 'datetime'=>'string', 'baseTimestamp='=>'int'], 'strtoupper' => ['string', 'string'=>'string'], From dee555daaf1f474fb3485ee63714b876d530e334 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Mon, 18 Sep 2023 14:48:31 +0200 Subject: [PATCH 177/357] filter_input & filter_var return type more specific --- config.xsd | 1 + dictionaries/CallMap.php | 8 +- dictionaries/CallMap_historical.php | 8 +- docs/running_psalm/issues.md | 1 + docs/running_psalm/issues/RedundantFlag.md | 8 + .../Provider/FunctionReturnTypeProvider.php | 2 + .../FilterInputReturnTypeProvider.php | 251 +++ .../ReturnTypeProvider/FilterUtils.php | 1738 +++++++++++++++++ .../FilterVarReturnTypeProvider.php | 226 +-- src/Psalm/Issue/RedundantFlag.php | 9 + tests/FunctionCallTest.php | 35 + tests/TypeReconciliation/ConditionalTest.php | 5 +- 12 files changed, 2157 insertions(+), 135 deletions(-) create mode 100644 docs/running_psalm/issues/RedundantFlag.md create mode 100644 src/Psalm/Internal/Provider/ReturnTypeProvider/FilterInputReturnTypeProvider.php create mode 100644 src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php create mode 100644 src/Psalm/Issue/RedundantFlag.php diff --git a/config.xsd b/config.xsd index 4cf075b6ece..5c176821e24 100644 --- a/config.xsd +++ b/config.xsd @@ -418,6 +418,7 @@ + diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 853b78ed8a0..19d755085ca 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -2847,11 +2847,11 @@ 'FilesystemIterator::setInfoClass' => ['void', 'class='=>'class-string'], 'FilesystemIterator::valid' => ['bool'], 'filetype' => ['string|false', 'filename'=>'string'], -'filter_has_var' => ['bool', 'input_type'=>'int', 'var_name'=>'string'], +'filter_has_var' => ['bool', 'input_type'=>'0|1|2|4|5', 'var_name'=>'string'], 'filter_id' => ['int|false', 'name'=>'string'], -'filter_input' => ['mixed|false', 'type'=>'int', 'var_name'=>'string', 'filter='=>'int', 'options='=>'array|int'], -'filter_input_array' => ['array|false|null', 'type'=>'int', 'options='=>'int|array', 'add_empty='=>'bool'], -'filter_list' => ['array'], +'filter_input' => ['mixed|false|null', 'type'=>'0|1|2|4|5', 'var_name'=>'string', 'filter='=>'int', 'options='=>'array|int'], +'filter_input_array' => ['array|false|null', 'type'=>'0|1|2|4|5', 'options='=>'int|array', 'add_empty='=>'bool'], +'filter_list' => ['non-empty-list'], 'filter_var' => ['mixed|false', 'value'=>'mixed', 'filter='=>'int', 'options='=>'array|int'], 'filter_var_array' => ['array|false|null', 'array'=>'array', 'options='=>'array|int', 'add_empty='=>'bool'], 'FilterIterator::__construct' => ['void', 'iterator'=>'Iterator'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 60e798e09cb..6ca97944a9c 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -10434,11 +10434,11 @@ 'filepro_rowcount' => ['int'], 'filesize' => ['int|false', 'filename'=>'string'], 'filetype' => ['string|false', 'filename'=>'string'], - 'filter_has_var' => ['bool', 'input_type'=>'int', 'var_name'=>'string'], + 'filter_has_var' => ['bool', 'input_type'=>'0|1|2|4|5', 'var_name'=>'string'], 'filter_id' => ['int|false', 'name'=>'string'], - 'filter_input' => ['mixed|false', 'type'=>'int', 'var_name'=>'string', 'filter='=>'int', 'options='=>'array|int'], - 'filter_input_array' => ['array|false|null', 'type'=>'int', 'options='=>'int|array', 'add_empty='=>'bool'], - 'filter_list' => ['array'], + 'filter_input' => ['mixed|false|null', 'type'=>'0|1|2|4|5', 'var_name'=>'string', 'filter='=>'int', 'options='=>'array|int'], + 'filter_input_array' => ['array|false|null', 'type'=>'0|1|2|4|5', 'options='=>'int|array', 'add_empty='=>'bool'], + 'filter_list' => ['non-empty-list'], 'filter_var' => ['mixed|false', 'value'=>'mixed', 'filter='=>'int', 'options='=>'array|int'], 'filter_var_array' => ['array|false|null', 'array'=>'array', 'options='=>'array|int', 'add_empty='=>'bool'], 'finfo::__construct' => ['void', 'flags='=>'int', 'magic_database='=>'string'], diff --git a/docs/running_psalm/issues.md b/docs/running_psalm/issues.md index d9b3b4f168a..ac8135c7142 100644 --- a/docs/running_psalm/issues.md +++ b/docs/running_psalm/issues.md @@ -220,6 +220,7 @@ - [RedundantCastGivenDocblockType](issues/RedundantCastGivenDocblockType.md) - [RedundantCondition](issues/RedundantCondition.md) - [RedundantConditionGivenDocblockType](issues/RedundantConditionGivenDocblockType.md) + - [RedundantFlag](issues/RedundantFlag.md) - [RedundantFunctionCall](issues/RedundantFunctionCall.md) - [RedundantFunctionCallGivenDocblockType](issues/RedundantFunctionCallGivenDocblockType.md) - [RedundantIdentityWithTrue](issues/RedundantIdentityWithTrue.md) diff --git a/docs/running_psalm/issues/RedundantFlag.md b/docs/running_psalm/issues/RedundantFlag.md new file mode 100644 index 00000000000..2ec918aefc5 --- /dev/null +++ b/docs/running_psalm/issues/RedundantFlag.md @@ -0,0 +1,8 @@ +# RedundantFlag + +Emitted when a flag is redundant. e.g. FILTER_NULL_ON_FAILURE won't do anything when the default option is specified + +```php + array('default' => 'world.com'), 'flags' => FILTER_NULL_ON_FAILURE)); +``` diff --git a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php index 04a5a74d572..546b7d38a02 100644 --- a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php @@ -25,6 +25,7 @@ use Psalm\Internal\Provider\ReturnTypeProvider\BasenameReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\DateReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\DirnameReturnTypeProvider; +use Psalm\Internal\Provider\ReturnTypeProvider\FilterInputReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\FilterVarReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\FirstArgStringReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\GetClassMethodsReturnTypeProvider; @@ -85,6 +86,7 @@ public function __construct() $this->registerClass(ArrayReverseReturnTypeProvider::class); $this->registerClass(ArrayFillReturnTypeProvider::class); $this->registerClass(ArrayFillKeysReturnTypeProvider::class); + $this->registerClass(FilterInputReturnTypeProvider::class); $this->registerClass(FilterVarReturnTypeProvider::class); $this->registerClass(IteratorToArrayReturnTypeProvider::class); $this->registerClass(ParseUrlReturnTypeProvider::class); diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterInputReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterInputReturnTypeProvider.php new file mode 100644 index 00000000000..09b32b2aa65 --- /dev/null +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterInputReturnTypeProvider.php @@ -0,0 +1,251 @@ + + */ + public static function getFunctionIds(): array + { + return ['filter_input']; + } + + public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Union + { + $statements_analyzer = $event->getStatementsSource(); + if (! $statements_analyzer instanceof StatementsAnalyzer) { + throw new UnexpectedValueException('Expected StatementsAnalyzer not StatementsSource'); + } + + $call_args = $event->getCallArgs(); + $function_id = $event->getFunctionId(); + $code_location = $event->getCodeLocation(); + $codebase = $statements_analyzer->getCodebase(); + + if (! isset($call_args[0]) || ! isset($call_args[1])) { + return FilterUtils::missingFirstArg($codebase); + } + + $first_arg_type = $statements_analyzer->node_data->getType($call_args[0]->value); + if ($first_arg_type && ! $first_arg_type->isInt()) { + if ($codebase->analysis_php_version_id >= 8_00_00) { + // throws + return Type::getNever(); + } + + // default option won't be used in this case + return Type::getNull(); + } + + $filter_int_used = FILTER_DEFAULT; + if (isset($call_args[2])) { + $filter_int_used = FilterUtils::getFilterArgValueOrError( + $call_args[2], + $statements_analyzer, + $codebase, + ); + + if (!is_int($filter_int_used)) { + return $filter_int_used; + } + } + + $options = null; + $flags_int_used = FILTER_FLAG_NONE; + if (isset($call_args[3])) { + $helper = FilterUtils::getOptionsArgValueOrError( + $call_args[3], + $statements_analyzer, + $codebase, + $code_location, + $function_id, + $filter_int_used, + ); + + if (!is_array($helper)) { + return $helper; + } + + $flags_int_used = $helper['flags_int_used']; + $options = $helper['options']; + } + + // if we reach this point with callback, the callback is missing + if ($filter_int_used === FILTER_CALLBACK) { + return FilterUtils::missingFilterCallbackCallable( + $function_id, + $code_location, + $statements_analyzer, + $codebase, + ); + } + + [$default, $min_range, $max_range, $has_range, $regexp] = FilterUtils::getOptions( + $filter_int_used, + $flags_int_used, + $options, + $statements_analyzer, + $code_location, + $codebase, + $function_id, + ); + + // only return now, as we still want to report errors above + if (!$first_arg_type) { + return null; + } + + if (! $first_arg_type->isSingleIntLiteral()) { + // eventually complex cases can be handled too, however practically this is irrelevant + return null; + } + + if (!$default) { + [$fails_type, $not_set_type, $fails_or_not_set_type] = FilterUtils::getFailsNotSetType($flags_int_used); + } else { + $fails_type = $default; + $not_set_type = $default; + $fails_or_not_set_type = $default; + } + + if ($filter_int_used === FILTER_VALIDATE_REGEXP && $regexp === null) { + if ($codebase->analysis_php_version_id >= 8_00_00) { + // throws + return Type::getNever(); + } + + // any "array" flags are ignored by this filter! + return $fails_or_not_set_type; + } + + $possible_types = array( + '$_GET' => INPUT_GET, + '$_POST' => INPUT_POST, + '$_COOKIE' => INPUT_COOKIE, + '$_SERVER' => INPUT_SERVER, + '$_ENV' => INPUT_ENV, + ); + + $first_arg_type_type = $first_arg_type->getSingleIntLiteral(); + $global_name = array_search($first_arg_type_type->value, $possible_types); + if (!$global_name) { + // invalid + if ($codebase->analysis_php_version_id >= 8_00_00) { + // throws + return Type::getNever(); + } + + // the "not set type" is never in an array, even if FILTER_FORCE_ARRAY is set! + return $not_set_type; + } + + $second_arg_type = $statements_analyzer->node_data->getType($call_args[1]->value); + if (!$second_arg_type) { + return null; + } + + if (! $second_arg_type->hasString()) { + // for filter_input there can only be string array keys + return $not_set_type; + } + + if (! $second_arg_type->isString()) { + // already reports an error by default + return null; + } + + // in all these cases it can fail or be not set, depending on whether the variable is set or not + $redundant_error_return_type = FilterUtils::checkRedundantFlags( + $filter_int_used, + $flags_int_used, + $fails_or_not_set_type, + $statements_analyzer, + $code_location, + $codebase, + ); + if ($redundant_error_return_type !== null) { + return $redundant_error_return_type; + } + + if (FilterUtils::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY) + && in_array($first_arg_type_type->value, array(INPUT_COOKIE, INPUT_SERVER, INPUT_ENV), true)) { + // these globals can never be an array + return $fails_or_not_set_type; + } + + // @todo eventually this needs to be changed when we fully support filter_has_var + $global_type = VariableFetchAnalyzer::getGlobalType($global_name, $codebase->analysis_php_version_id); + + $input_type = null; + if ($global_type->isArray() && $global_type->getArray() instanceof TKeyedArray) { + $array_instance = $global_type->getArray(); + if ($second_arg_type->isSingleStringLiteral()) { + $key = $second_arg_type->getSingleStringLiteral()->value; + + if (isset($array_instance->properties[ $key ])) { + $input_type = $array_instance->properties[ $key ]; + } + } + + if ($input_type === null) { + $input_type = $array_instance->getGenericValueType(); + $input_type = $input_type->setPossiblyUndefined(true); + } + } elseif ($global_type->isArray() + && ($array_atomic = $global_type->getArray()) + && $array_atomic instanceof TArray) { + [$_, $input_type] = $array_atomic->type_params; + $input_type = $input_type->setPossiblyUndefined(true); + } else { + // this is impossible + throw new UnexpectedValueException('This should not happen'); + } + + return FilterUtils::getReturnType( + $filter_int_used, + $flags_int_used, + $input_type, + $fails_type, + $not_set_type, + $statements_analyzer, + $code_location, + $codebase, + $function_id, + $has_range, + $min_range, + $max_range, + $regexp, + ); + } +} diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php new file mode 100644 index 00000000000..e0de55bba04 --- /dev/null +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php @@ -0,0 +1,1738 @@ +analysis_php_version_id >= 8_00_00) { + // throws + return Type::getNever(); + } + + return Type::getNull(); + } + + /** @return int|Union|null */ + public static function getFilterArgValueOrError( + Arg $filter_arg, + StatementsAnalyzer $statements_analyzer, + Codebase $codebase + ) { + $filter_arg_type = $statements_analyzer->node_data->getType($filter_arg->value); + if (!$filter_arg_type) { + return null; + } + + if (! $filter_arg_type->isInt()) { + // invalid + if ($codebase->analysis_php_version_id >= 8_00_00) { + // throws + return Type::getNever(); + } + + // will return null independent of FILTER_NULL_ON_FAILURE or default option + return Type::getNull(); + } + + if (! $filter_arg_type->isSingleIntLiteral()) { + // too complex for now + return null; + } + + $all_filters = self::getFilters($codebase); + $filter_int_used = $filter_arg_type->getSingleIntLiteral()->value; + if (! isset($all_filters[ $filter_int_used ])) { + // inconsistently, this will always return false, even when FILTER_NULL_ON_FAILURE + // or a default option is set + // and will also not use any default set + return Type::getFalse(); + } + + return $filter_int_used; + } + + /** @return array{flags_int_used: int, options: TKeyedArray|null}|Union|null */ + public static function getOptionsArgValueOrError( + Arg $options_arg, + StatementsAnalyzer $statements_analyzer, + Codebase $codebase, + CodeLocation $code_location, + string $function_id, + int $filter_int_used + ) { + $options_arg_type = $statements_analyzer->node_data->getType($options_arg->value); + if (!$options_arg_type) { + return null; + } + + if ($options_arg_type->isArray()) { + $return_null = false; + $defaults = array( + 'flags_int_used' => FILTER_FLAG_NONE, + 'options' => null, + ); + + $atomic_type = $options_arg_type->getArray(); + if ($atomic_type instanceof TKeyedArray) { + $redundant_keys = array_diff(array_keys($atomic_type->properties), array('flags', 'options')); + if ($redundant_keys !== array()) { + // reported as it's usually an oversight/misunderstanding of how the function works + // it's silently ignored by the function though + IssueBuffer::maybeAdd( + new RedundantFlag( + 'The options array contains unused keys ' + . implode(', ', $redundant_keys), + $code_location, + ), + $statements_analyzer->getSuppressedIssues(), + ); + } + + if (isset($atomic_type->properties['options'])) { + if ($filter_int_used === FILTER_CALLBACK) { + $only_callables = true; + foreach ($atomic_type->properties['options']->getAtomicTypes() as $option_atomic) { + if ($option_atomic->isCallableType()) { + continue; + } + + if (CallableTypeComparator::getCallableFromAtomic( + $codebase, + $option_atomic, + null, + $statements_analyzer, + )) { + continue; + } + + $only_callables = false; + } + if ($atomic_type->properties['options']->possibly_undefined) { + $only_callables = false; + } + + if (!$only_callables) { + return self::missingFilterCallbackCallable( + $function_id, + $code_location, + $statements_analyzer, + $codebase, + ); + } + + // eventually can improve it to return the type from the callback + // there are no flags or other options/flags, so it can be handled here directly + // @todo + $return_type = Type::getMixed(); + return self::addReturnTaint( + $statements_analyzer, + $code_location, + $return_type, + $function_id, + ); + } + + if (! $atomic_type->properties['options']->isArray()) { + // silently ignored by the function, but this usually indicates a bug + IssueBuffer::maybeAdd( + new InvalidArgument( + 'The "options" key in ' . $function_id + . ' must be a an array', + $code_location, + $function_id, + ), + $statements_analyzer->getSuppressedIssues(), + ); + } elseif (($options_array = $atomic_type->properties['options']->getArray()) + && $options_array instanceof TKeyedArray) { + $defaults['options'] = $options_array; + } else { + // cannot infer a 100% correct specific return type + $return_null = true; + } + } + + if (isset($atomic_type->properties['flags'])) { + if ($atomic_type->properties['flags']->isSingleIntLiteral()) { + $defaults['flags_int_used'] = $atomic_type->properties['flags']->getSingleIntLiteral()->value; + } elseif ($atomic_type->properties['flags']->isInt()) { + // cannot infer a 100% correct specific return type + $return_null = true; + } else { + // silently ignored by the function, but this usually indicates a bug + IssueBuffer::maybeAdd( + new InvalidArgument( + 'The "flags" key in ' . + $function_id . ' must be a valid flag', + $code_location, + $function_id, + ), + $statements_analyzer->getSuppressedIssues(), + ); + + $defaults['flags_int_used'] = FILTER_FLAG_NONE; + } + } + + return $return_null ? null : $defaults; + } + + // cannot infer a 100% correct specific return type + return null; + } + + if ($filter_int_used === FILTER_CALLBACK) { + return self::missingFilterCallbackCallable( + $function_id, + $code_location, + $statements_analyzer, + $codebase, + ); + } + + if ($options_arg_type->isSingleIntLiteral()) { + return array( + 'flags_int_used' => $options_arg_type->getSingleIntLiteral()->value, + 'options' => null, + ); + } + + if ($options_arg_type->isInt()) { + // in most cases we cannot infer a 100% correct specific return type though + // unless all int are literal + // @todo could handle all literal int cases + return null; + } + + foreach ($options_arg_type->getAtomicTypes() as $atomic_type) { + if ($atomic_type instanceof TArray) { + continue; + } + + if ($atomic_type instanceof TInt) { + continue; + } + + if ($atomic_type instanceof TFloat) { + // ignored + continue; + } + + if ($atomic_type instanceof TBool) { + // ignored + continue; + } + + if ($codebase->analysis_php_version_id >= 8_00_00) { + // throws for the invalid type + // for the other types it will still work correctly + // however "never" is a bottom type + // and will be lost, therefore it's better to return it here + // to identify hard to find bugs in the code + return Type::getNever(); + } + // before PHP 8, it's ignored but gives a PHP notice + } + + // array|int type which is too complex for now + // or any other invalid type + return null; + } + + public static function missingFilterCallbackCallable( + string $function_id, + CodeLocation $code_location, + StatementsAnalyzer $statements_analyzer, + Codebase $codebase + ): Union { + IssueBuffer::maybeAdd( + new InvalidArgument( + 'The "options" key in ' . $function_id + . ' must be a callable for FILTER_CALLBACK', + $code_location, + $function_id, + ), + $statements_analyzer->getSuppressedIssues(), + ); + + if ($codebase->analysis_php_version_id >= 8_00_00) { + // throws + return Type::getNever(); + } + + // flags are ignored here + return Type::getNull(); + } + + /** @return array{Union, Union, Union} */ + public static function getFailsNotSetType(int $flags_int_used): array + { + $fails_type = Type::getFalse(); + $not_set_type = Type::getNull(); + if (self::hasFlag($flags_int_used, FILTER_NULL_ON_FAILURE)) { + $fails_type = Type::getNull(); + $not_set_type = Type::getFalse(); + } + + $fails_or_not_set_type = new Union([new TNull(), new TFalse()]); + return array( + $fails_type, + $not_set_type, + $fails_or_not_set_type, + ); + } + + public static function hasFlag(int $flags, int $flag): bool + { + if ($flags === 0) { + return false; + } + + if (($flags & $flag) === $flag) { + return true; + } + + return false; + } + + public static function checkRedundantFlags( + int $filter_int_used, + int $flags_int_used, + Union $fails_type, + StatementsAnalyzer $statements_analyzer, + CodeLocation $code_location, + Codebase $codebase + ): ?Union { + $all_filters = self::getFilters($codebase); + $flags_int_used_rest = $flags_int_used; + foreach ($all_filters[ $filter_int_used ]['flags'] as $flag) { + if ($flags_int_used_rest === 0) { + break; + } + + if (self::hasFlag($flags_int_used_rest, $flag)) { + $flags_int_used_rest = $flags_int_used_rest ^ $flag; + } + } + + if ($flags_int_used_rest !== 0) { + // invalid flags used + // while they are silently ignored + // usually it means there's a mistake and the filter doesn't actually do what one expects + // as otherwise the flag wouldn't have been provided + IssueBuffer::maybeAdd( + new RedundantFlag( + 'Not all flags used are supported by the filter used', + $code_location, + ), + $statements_analyzer->getSuppressedIssues(), + ); + } + + if (self::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY) + && self::hasFlag($flags_int_used, FILTER_FORCE_ARRAY)) { + IssueBuffer::maybeAdd( + new RedundantFlag( + 'Flag FILTER_FORCE_ARRAY is ignored when using FILTER_REQUIRE_ARRAY', + $code_location, + ), + $statements_analyzer->getSuppressedIssues(), + ); + } + + if ($filter_int_used === FILTER_VALIDATE_REGEXP + && ( + self::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY) + || self::hasFlag($flags_int_used, FILTER_FORCE_ARRAY) + || self::hasFlag($flags_int_used, FILTER_REQUIRE_SCALAR)) + ) { + IssueBuffer::maybeAdd( + new RedundantFlag( + 'FILTER_VALIDATE_REGEXP will ignore ' . + 'FILTER_REQUIRE_ARRAY/FILTER_FORCE_ARRAY/FILTER_REQUIRE_SCALAR ' . + 'as it only works on scalar types', + $code_location, + ), + $statements_analyzer->getSuppressedIssues(), + ); + } + + if (self::hasFlag($flags_int_used, FILTER_FLAG_STRIP_LOW) + && self::hasFlag($flags_int_used, FILTER_FLAG_ENCODE_LOW)) { + IssueBuffer::maybeAdd( + new RedundantFlag( + 'Using flag FILTER_FLAG_ENCODE_LOW is redundant when using FILTER_FLAG_STRIP_LOW', + $code_location, + ), + $statements_analyzer->getSuppressedIssues(), + ); + } + + if (self::hasFlag($flags_int_used, FILTER_FLAG_STRIP_HIGH) + && self::hasFlag($flags_int_used, FILTER_FLAG_ENCODE_HIGH)) { + IssueBuffer::maybeAdd( + new RedundantFlag( + 'Using flag FILTER_FLAG_ENCODE_HIGH is redundant when using FILTER_FLAG_STRIP_HIGH', + $code_location, + ), + $statements_analyzer->getSuppressedIssues(), + ); + } + + if (self::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY) + && self::hasFlag($flags_int_used, FILTER_REQUIRE_SCALAR)) { + IssueBuffer::maybeAdd( + new RedundantFlag( + 'You cannot use FILTER_REQUIRE_ARRAY together with FILTER_REQUIRE_SCALAR flag', + $code_location, + ), + $statements_analyzer->getSuppressedIssues(), + ); + + // FILTER_REQUIRE_ARRAY will make PHP ignore FILTER_FORCE_ARRAY + return $fails_type; + } + + return null; + } + + /** @return array{Union|null, float|int|null, float|int|null, bool, non-falsy-string|true|null} */ + public static function getOptions( + int $filter_int_used, + int $flags_int_used, + ?TKeyedArray $options, + StatementsAnalyzer $statements_analyzer, + CodeLocation $code_location, + Codebase $codebase, + string $function_id + ): array { + $default = null; + $min_range = null; + $max_range = null; + $has_range = false; + $regexp = null; + + if (!$options) { + return [$default, $min_range, $max_range, $has_range, $regexp]; + } + + $all_filters = self::getFilters($codebase); + foreach ($options->properties as $option => $option_value) { + if (! isset($all_filters[ $filter_int_used ]['options'][ $option ])) { + IssueBuffer::maybeAdd( + new RedundantFlag( + 'The option ' . $option . ' is not valid for the filter used', + $code_location, + ), + $statements_analyzer->getSuppressedIssues(), + ); + + continue; + } + + if (! UnionTypeComparator::isContainedBy( + $codebase, + $option_value, + $all_filters[ $filter_int_used ]['options'][ $option ], + )) { + // silently ignored by the function, but it's a bug in the code + // since the filtering/option will not do what you expect + IssueBuffer::maybeAdd( + new InvalidArgument( + 'The option "' . $option . '" of ' . $function_id . ' expects ' + . $all_filters[ $filter_int_used ]['options'][ $option ]->getId() + . ', but ' . $option_value->getId() . ' provided', + $code_location, + $function_id, + ), + $statements_analyzer->getSuppressedIssues(), + ); + + continue; + } + + if ($option === 'default') { + $default = $option_value; + + if (self::hasFlag($flags_int_used, FILTER_NULL_ON_FAILURE)) { + IssueBuffer::maybeAdd( + new RedundantFlag( + 'Redundant flag FILTER_NULL_ON_FAILURE when using the "default" option', + $code_location, + ), + $statements_analyzer->getSuppressedIssues(), + ); + } + + continue; + } + + // currently only int ranges are supported + // must be numeric, otherwise we would have continued above already + if ($option === 'min_range' && $option_value->isSingleLiteral()) { + if ($filter_int_used === FILTER_VALIDATE_INT) { + $min_range = (int) $option_value->getSingleLiteral()->value; + } elseif ($filter_int_used === FILTER_VALIDATE_FLOAT) { + $min_range = (float) $option_value->getSingleLiteral()->value; + } + } + + if ($option === 'max_range' && $option_value->isSingleLiteral()) { + if ($filter_int_used === FILTER_VALIDATE_INT) { + $max_range = (int) $option_value->getSingleLiteral()->value; + } elseif ($filter_int_used === FILTER_VALIDATE_FLOAT) { + $max_range = (float) $option_value->getSingleLiteral()->value; + } + } + + if (($filter_int_used === FILTER_VALIDATE_INT || $filter_int_used === FILTER_VALIDATE_FLOAT) + && ($option === 'min_range' || $option === 'max_range') + ) { + $has_range = true; + } + + if ($filter_int_used === FILTER_VALIDATE_REGEXP + && $option === 'regexp' + ) { + if ($option_value->isSingleStringLiteral()) { + /** + * if it's another type, we would have reported an error above already + * @var non-falsy-string $regexp + */ + $regexp = $option_value->getSingleStringLiteral()->value; + } elseif ($option_value->isString()) { + $regexp = true; + } + } + } + + return [$default, $min_range, $max_range, $has_range, $regexp]; + } + + /** + * @param float|int|null $min_range + * @param float|int|null $max_range + */ + protected static function isRangeValid( + $min_range, + $max_range, + StatementsAnalyzer $statements_analyzer, + CodeLocation $code_location, + string $function_id + ): bool { + if ($min_range !== null && $max_range !== null && $min_range > $max_range) { + IssueBuffer::maybeAdd( + new InvalidArgument( + 'min_range cannot be larger than max_range', + $code_location, + $function_id, + ), + $statements_analyzer->getSuppressedIssues(), + ); + + return false; + } + + return true; + } + + /** + * can't split this because the switch is complex since there are too many possibilities + * + * @psalm-suppress ComplexMethod + * @param Union|null $not_set_type null if undefined filtered variable will return $fails_type + * @param float|int|null $min_range + * @param float|int|null $max_range + * @param non-falsy-string|true|null $regexp + */ + public static function getReturnType( + int $filter_int_used, + int $flags_int_used, + Union $input_type, + Union $fails_type, + ?Union $not_set_type, + StatementsAnalyzer $statements_analyzer, + CodeLocation $code_location, + Codebase $codebase, + string $function_id, + bool $has_range, + $min_range, + $max_range, + $regexp, + bool $in_array_recursion = false + ): Union { + // if we are inside a recursion of e.g. array + // it will never fail or change the type, so we can immediately return + if ($in_array_recursion && $input_type->isNever()) { + return $input_type; + } + + $from_array = []; + // will only handle arrays correctly if either flag is set, otherwise always error + // regexp doesn't work on arrays + if ((self::hasFlag($flags_int_used, FILTER_FORCE_ARRAY) || self::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY)) + && $filter_int_used !== FILTER_VALIDATE_REGEXP + && !self::hasFlag($flags_int_used, FILTER_REQUIRE_SCALAR) + ) { + foreach ($input_type->getAtomicTypes() as $key => $atomic_type) { + if ($atomic_type instanceof TList) { + $atomic_type = $atomic_type->getKeyedArray(); + } + + if ($atomic_type instanceof TKeyedArray) { + $input_type = $input_type->getBuilder(); + $input_type->removeType($key); + $input_type = $input_type->freeze(); + + $new = []; + foreach ($atomic_type->properties as $k => $property) { + if ($property->isNever()) { + $new[$k] = $property; + continue; + } + + $new[$k] = self::getReturnType( + $filter_int_used, + $flags_int_used, + $property, + $fails_type, + // irrelevant in nested elements + null, + $statements_analyzer, + $code_location, + $codebase, + $function_id, + $has_range, + $min_range, + $max_range, + $regexp, + true, + ); + } + + // false positive error in psalm when we loop over a non-empty array + if ($new === array()) { + throw new UnexpectedValueException('This is impossible'); + } + + $fallback_params = null; + if ($atomic_type->fallback_params) { + [$keys_union, $values_union] = $atomic_type->fallback_params; + $values_union = self::getReturnType( + $filter_int_used, + $flags_int_used, + $values_union, + $fails_type, + // irrelevant in nested elements + null, + $statements_analyzer, + $code_location, + $codebase, + $function_id, + $has_range, + $min_range, + $max_range, + $regexp, + true, + ); + $fallback_params = [$keys_union, $values_union]; + } + + $from_array[] = new TKeyedArray( + $new, + $atomic_type->class_strings, + $fallback_params, + $atomic_type->is_list, + ); + + continue; + } + + if ($atomic_type instanceof TArray) { + $input_type = $input_type->getBuilder(); + $input_type->removeType($key); + $input_type = $input_type->freeze(); + + [$keys_union, $values_union] = $atomic_type->type_params; + $values_union = self::getReturnType( + $filter_int_used, + $flags_int_used, + $values_union, + $fails_type, + // irrelevant in nested elements + null, + $statements_analyzer, + $code_location, + $codebase, + $function_id, + $has_range, + $min_range, + $max_range, + $regexp, + true, + ); + + if ($atomic_type instanceof TNonEmptyArray) { + $from_array[] = new TNonEmptyArray([$keys_union, $values_union]); + } else { + $from_array[] = new TArray([$keys_union, $values_union]); + } + + continue; + } + + // can be an array too + if ($atomic_type instanceof TMixed) { + $from_array[] = new TArray( + [ + new Union([new TArrayKey]), + new Union([new TMixed]), + ], + ); + } + } + } + + $can_fail = false; + $filter_types = array(); + switch ($filter_int_used) { + case FILTER_VALIDATE_FLOAT: + if (!self::isRangeValid( + $min_range, + $max_range, + $statements_analyzer, + $code_location, + $function_id, + )) { + $can_fail = true; + break; + } + + foreach ($input_type->getAtomicTypes() as $atomic_type) { + if ($atomic_type instanceof TLiteralFloat) { + if ($min_range !== null && $min_range > $atomic_type->value) { + $can_fail = true; + continue; + } + + if ($max_range !== null && $max_range < $atomic_type->value) { + $can_fail = true; + continue; + } + + if ($min_range !== null || $max_range !== null || $has_range === false) { + $filter_types[] = $atomic_type; + continue; + } + + // we don't know what the min/max of the range are + // and it might be out of the range too + // float ranges aren't supported yet + $filter_types[] = new TFloat(); + } elseif ($atomic_type instanceof TFloat) { + if ($has_range === false) { + $filter_types[] = $atomic_type; + continue; + } + + // float ranges aren't supported yet + $filter_types[] = new TFloat(); + } + + if ($atomic_type instanceof TLiteralInt) { + if ($min_range !== null && $min_range > $atomic_type->value) { + $can_fail = true; + continue; + } + + if ($max_range !== null && $max_range < $atomic_type->value) { + $can_fail = true; + continue; + } + + if ($min_range !== null || $max_range !== null || $has_range === false) { + $filter_types[] = new TLiteralFloat((float) $atomic_type->value); + continue; + } + + // we don't know what the min/max of the range are + // and it might be out of the range too + $filter_types[] = new TFloat(); + } elseif ($atomic_type instanceof TInt) { + $filter_types[] = new TFloat(); + + if ($has_range === false) { + continue; + } + } + + if ($atomic_type instanceof TLiteralString) { + if (($string_to_float = filter_var($atomic_type->value, FILTER_VALIDATE_FLOAT)) === false) { + $can_fail = true; + continue; + } + + if ($min_range !== null && $min_range > $string_to_float) { + $can_fail = true; + continue; + } + + if ($max_range !== null && $max_range < $string_to_float) { + $can_fail = true; + continue; + } + + if ($min_range !== null || $max_range !== null || $has_range === false) { + $filter_types[] = new TLiteralFloat($string_to_float); + continue; + } + + // we don't know what the min/max of the range are + // and it might be out of the range too + $filter_types[] = new TFloat(); + } elseif ($atomic_type instanceof TString) { + $filter_types[] = new TFloat(); + } + + if ($atomic_type instanceof TBool) { + if ($min_range !== null && $min_range > 1) { + $can_fail = true; + continue; + } + + if ($max_range !== null && $max_range < 1) { + $can_fail = true; + continue; + } + + if ($atomic_type instanceof TFalse) { + $can_fail = true; + continue; + } + + if ($min_range !== null || $max_range !== null || $has_range === false) { + $filter_types[] = new TLiteralFloat(1.0); + + if ($atomic_type instanceof TTrue) { + continue; + } + } + + // we don't know what the min/max of the range are + // and it might be out of the range too + $filter_types[] = new TFloat(); + } + + if ($atomic_type instanceof TMixed) { + $filter_types[] = new TFloat(); + } + + $can_fail = true; + } + break; + case FILTER_VALIDATE_BOOLEAN: + foreach ($input_type->getAtomicTypes() as $atomic_type) { + if ($atomic_type instanceof TBool) { + $filter_types[] = $atomic_type; + continue; + } + + if (($atomic_type instanceof TLiteralInt && $atomic_type->value === 1) + || ($atomic_type instanceof TLiteralFloat && $atomic_type->value === 1.0) + || ($atomic_type instanceof TLiteralString + && in_array(strtolower($atomic_type->value), ['1', 'true', 'on', 'yes'], true)) + ) { + $filter_types[] = new TTrue(); + continue; + } + + if (self::hasFlag($flags_int_used, FILTER_NULL_ON_FAILURE) + && ( + ($atomic_type instanceof TLiteralInt && $atomic_type->value === 0) + || ($atomic_type instanceof TLiteralFloat && $atomic_type->value === 0.0) + || ($atomic_type instanceof TLiteralString + && in_array(strtolower($atomic_type->value), ['0', 'false', 'off', 'no', ''], true) + ) + ) + ) { + $filter_types[] = new TFalse(); + continue; + } + + if ($atomic_type instanceof TLiteralInt + || $atomic_type instanceof TLiteralFloat + || $atomic_type instanceof TLiteralString + ) { + // all other literals will fail + $can_fail = true; + continue; + } + + if ($atomic_type instanceof TMixed + || $atomic_type instanceof TString + || $atomic_type instanceof TInt + || $atomic_type instanceof TFloat) { + $filter_types[] = new TBool(); + } + + $can_fail = true; + } + break; + case FILTER_VALIDATE_INT: + if (!self::isRangeValid( + $min_range, + $max_range, + $statements_analyzer, + $code_location, + $function_id, + )) { + $can_fail = true; + break; + } + + $min_range = $min_range !== null ? (int) $min_range : null; + $max_range = $max_range !== null ? (int) $max_range : null; + + if ($min_range !== null || $max_range !== null) { + $int_type = new TIntRange($min_range, $max_range); + } else { + $int_type = new TInt(); + } + foreach ($input_type->getAtomicTypes() as $atomic_type) { + if ($atomic_type instanceof TLiteralInt) { + if ($min_range !== null && $min_range > $atomic_type->value) { + $can_fail = true; + continue; + } + + if ($max_range !== null && $max_range < $atomic_type->value) { + $can_fail = true; + continue; + } + + if ($min_range !== null || $max_range !== null || $has_range === false) { + $filter_types[] = $atomic_type; + continue; + } + + // we don't know what the min/max of the range are + // and it might be out of the range too + $filter_types[] = new TInt(); + } elseif ($atomic_type instanceof TInt) { + if ($has_range === false) { + $filter_types[] = $atomic_type; + continue; + } + + $filter_types[] = $int_type; + } + + if ($atomic_type instanceof TLiteralFloat) { + if ((float) (int) $atomic_type->value !== $atomic_type->value) { + $can_fail = true; + continue; + } + + if ($min_range !== null && $min_range > $atomic_type->value) { + $can_fail = true; + continue; + } + + if ($max_range !== null && $max_range < $atomic_type->value) { + $can_fail = true; + continue; + } + + if ($min_range !== null || $max_range !== null || $has_range === false) { + $filter_types[] = new TLiteralInt((int) $atomic_type->value); + continue; + } + + // we don't know what the min/max of the range are + // and it might be out of the range too + $filter_types[] = $int_type; + } elseif ($atomic_type instanceof TFloat) { + $filter_types[] = $int_type; + } + + if ($atomic_type instanceof TLiteralString) { + if (($string_to_int = filter_var($atomic_type->value, FILTER_VALIDATE_INT)) === false) { + $can_fail = true; + continue; + } + + if ($min_range !== null && $min_range > $string_to_int) { + $can_fail = true; + continue; + } + + if ($max_range !== null && $max_range < $string_to_int) { + $can_fail = true; + continue; + } + + if ($min_range !== null || $max_range !== null || $has_range === false) { + $filter_types[] = new TLiteralInt($string_to_int); + continue; + } + + // we don't know what the min/max of the range are + // and it might be out of the range too + $filter_types[] = $int_type; + } elseif ($atomic_type instanceof TString) { + $filter_types[] = $int_type; + } + + if ($atomic_type instanceof TBool) { + if ($min_range !== null && $min_range > 1) { + $can_fail = true; + continue; + } + + if ($max_range !== null && $max_range < 1) { + $can_fail = true; + continue; + } + + if ($atomic_type instanceof TFalse) { + $can_fail = true; + continue; + } + + if ($min_range !== null || $max_range !== null || $has_range === false) { + $filter_types[] = new TLiteralInt(1); + + if ($atomic_type instanceof TTrue) { + continue; + } + } + + // we don't know what the min/max of the range are + // and it might be out of the range too + $filter_types[] = $int_type; + } + + if ($atomic_type instanceof TMixed) { + $filter_types[] = $int_type; + } + + $can_fail = true; + } + break; + case FILTER_VALIDATE_IP: + case FILTER_VALIDATE_MAC: + case FILTER_VALIDATE_URL: + case FILTER_VALIDATE_EMAIL: + foreach ($input_type->getAtomicTypes() as $atomic_type) { + if ($atomic_type instanceof TNumericString) { + $can_fail = true; + continue; + } + + if ($atomic_type instanceof TNonFalsyString) { + $filter_types[] = $atomic_type; + } elseif ($atomic_type instanceof TString) { + $filter_types[] = new TNonFalsyString(); + } + + if ($atomic_type instanceof TMixed) { + $filter_types[] = new TNonFalsyString(); + } + + $can_fail = true; + } + break; + case FILTER_VALIDATE_REGEXP: + // the regexp key is mandatory for this filter + // it will only fail if the value exists, therefore it's after the checks above + // this must be (and is) handled BEFORE calling this function though + // since PHP 8+ throws instead of returning the fails case + if ($regexp === null) { + $can_fail = true; + break; + } + + // invalid regex + if ($regexp !== true && @preg_match($regexp, 'placeholder') === false) { + $can_fail = true; + break; + } + + foreach ($input_type->getAtomicTypes() as $atomic_type) { + if ($atomic_type instanceof TString + || $atomic_type instanceof TInt + || $atomic_type instanceof TFloat + || $atomic_type instanceof TNumeric + || $atomic_type instanceof TMixed) { + $filter_types[] = new TString(); + } + + $can_fail = true; + } + + break; + case FILTER_VALIDATE_DOMAIN: + if (self::hasFlag($flags_int_used, FILTER_FLAG_HOSTNAME)) { + $string_type = new TNonEmptyString(); + } else { + $string_type = new TString(); + } + + foreach ($input_type->getAtomicTypes() as $atomic_type) { + if ($atomic_type instanceof TNonEmptyString) { + $filter_types[] = $atomic_type; + } elseif ($atomic_type instanceof TString) { + if (self::hasFlag($flags_int_used, FILTER_FLAG_HOSTNAME)) { + $filter_types[] = $string_type; + } else { + $filter_types[] = $atomic_type; + } + } + + if ($atomic_type instanceof TMixed + || $atomic_type instanceof TInt + || $atomic_type instanceof TFloat) { + $filter_types[] = $string_type; + } + + $can_fail = true; + } + break; + case FILTER_SANITIZE_EMAIL: + case FILTER_SANITIZE_URL: + foreach ($input_type->getAtomicTypes() as $atomic_type) { + if ($atomic_type instanceof TNumericString) { + $filter_types[] = $atomic_type; + continue; + } + + if ($atomic_type instanceof TString) { + $filter_types[] = new TString(); + continue; + } + + if ($atomic_type instanceof TFloat + || $atomic_type instanceof TInt + || $atomic_type instanceof TNumeric) { + $filter_types[] = new TNumericString(); + continue; + } + + if ($atomic_type instanceof TTrue) { + $filter_types[] = Type::getAtomicStringFromLiteral('1'); + continue; + } + + if ($atomic_type instanceof TFalse) { + $filter_types[] = Type::getAtomicStringFromLiteral(''); + continue; + } + + if ($atomic_type instanceof TBool) { + $filter_types[] = Type::getAtomicStringFromLiteral('1'); + $filter_types[] = Type::getAtomicStringFromLiteral(''); + continue; + } + + if ($atomic_type instanceof TMixed) { + $filter_types[] = new TString(); + } + + $can_fail = true; + } + break; + case FILTER_SANITIZE_ENCODED: + case FILTER_SANITIZE_ADD_SLASHES: + case 521: // 8.0.0 FILTER_SANITIZE_MAGIC_QUOTES has been removed. + case FILTER_SANITIZE_SPECIAL_CHARS: + case FILTER_SANITIZE_FULL_SPECIAL_CHARS: + case FILTER_DEFAULT: + foreach ($input_type->getAtomicTypes() as $atomic_type) { + if ($filter_int_used === FILTER_DEFAULT + && $flags_int_used === 0 + && $atomic_type instanceof TString + ) { + $filter_types[] = $atomic_type; + continue; + } + + if ($atomic_type instanceof TNumericString) { + $filter_types[] = $atomic_type; + continue; + } + + if (in_array( + $filter_int_used, + [FILTER_SANITIZE_ENCODED, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_DEFAULT], + true, + ) + && $atomic_type instanceof TNonEmptyString + && (self::hasFlag($flags_int_used, FILTER_FLAG_STRIP_LOW) + || self::hasFlag($flags_int_used, FILTER_FLAG_STRIP_HIGH) + || self::hasFlag($flags_int_used, FILTER_FLAG_STRIP_BACKTICK) + ) + ) { + $filter_types[] = new TString(); + continue; + } + + if ($atomic_type instanceof TNonFalsyString) { + $filter_types[] = new TNonFalsyString(); + continue; + } + + if ($atomic_type instanceof TNonEmptyString) { + $filter_types[] = new TNonEmptyString(); + continue; + } + + if ($atomic_type instanceof TString) { + $filter_types[] = new TString(); + continue; + } + + if ($atomic_type instanceof TFloat + || $atomic_type instanceof TInt + || $atomic_type instanceof TNumeric) { + $filter_types[] = new TNumericString(); + continue; + } + + if ($atomic_type instanceof TTrue) { + $filter_types[] = Type::getAtomicStringFromLiteral('1'); + continue; + } + + if ($atomic_type instanceof TFalse) { + $filter_types[] = Type::getAtomicStringFromLiteral(''); + continue; + } + + if ($atomic_type instanceof TBool) { + $filter_types[] = Type::getAtomicStringFromLiteral('1'); + $filter_types[] = Type::getAtomicStringFromLiteral(''); + continue; + } + + if ($atomic_type instanceof TMixed) { + $filter_types[] = new TString(); + } + + $can_fail = true; + } + break; + case 513: // 8.1.0 FILTER_SANITIZE_STRING and FILTER_SANITIZE_STRIPPED (alias) have been deprecated. + foreach ($input_type->getAtomicTypes() as $atomic_type) { + if ($atomic_type instanceof TBool + || $atomic_type instanceof TString + || $atomic_type instanceof TInt + || $atomic_type instanceof TFloat + || $atomic_type instanceof TNumeric) { + // only basic checking since it's deprecated anyway and not worth the time + $filter_types[] = new TString(); + continue; + } + + if ($atomic_type instanceof TMixed) { + $filter_types[] = new TString(); + } + + $can_fail = true; + } + break; + case FILTER_SANITIZE_NUMBER_INT: + case FILTER_SANITIZE_NUMBER_FLOAT: + foreach ($input_type->getAtomicTypes() as $atomic_type) { + if ($atomic_type instanceof TLiteralString + || $atomic_type instanceof TLiteralInt + || $atomic_type instanceof TLiteralFloat + ) { + /** @var string|false $literal */ + $literal = filter_var($atomic_type->value, $filter_int_used); + if ($literal === false) { + $can_fail = true; + } else { + $filter_types[] = Type::getAtomicStringFromLiteral($literal); + } + + continue; + } + + if ($atomic_type instanceof TFloat + || $atomic_type instanceof TNumericString + || $atomic_type instanceof TInt + || $atomic_type instanceof TNumeric) { + $filter_types[] = new TNumericString(); + continue; + } + + if ($atomic_type instanceof TString) { + $filter_types[] = new TNumericString(); + // for numeric-string it won't collapse since https://github.com/vimeo/psalm/pull/10459 + // therefore we can add both + $filter_types[] = Type::getAtomicStringFromLiteral(''); + continue; + } + + if ($atomic_type instanceof TTrue) { + $filter_types[] = Type::getAtomicStringFromLiteral('1'); + continue; + } + + if ($atomic_type instanceof TFalse) { + $filter_types[] = Type::getAtomicStringFromLiteral(''); + continue; + } + + if ($atomic_type instanceof TBool) { + $filter_types[] = Type::getAtomicStringFromLiteral('1'); + $filter_types[] = Type::getAtomicStringFromLiteral(''); + continue; + } + + if ($atomic_type instanceof TMixed) { + $filter_types[] = new TNumericString(); + $filter_types[] = Type::getAtomicStringFromLiteral(''); + } + + $can_fail = true; + } + break; + } + + if ($input_type->hasMixed()) { + // can always fail if we have mixed + // only for redundancy in case there's a mistake in the switch above + $can_fail = true; + } + + // if an array is required, ignore all types we created from non-array on first level + if (!$in_array_recursion && self::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY)) { + $filter_types = array(); + } + + $return_type = $fails_type; + if ($filter_types !== array() + && ($can_fail === true || + (!$in_array_recursion && !$not_set_type && $input_type->possibly_undefined) + )) { + $return_type = Type::combineUnionTypes( + $return_type, + TypeCombiner::combine($filter_types, $codebase), + $codebase, + ); + } elseif ($filter_types !== array()) { + $return_type = TypeCombiner::combine($filter_types, $codebase); + } + + if (!$in_array_recursion + && !self::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY) + && self::hasFlag($flags_int_used, FILTER_FORCE_ARRAY)) { + $return_type = new Union([new TKeyedArray( + [$return_type], + null, + null, + true, + )]); + } + + if ($from_array !== array()) { + $from_array_union = TypeCombiner::combine($from_array, $codebase); + + $return_type = Type::combineUnionTypes( + $return_type, + $from_array_union, + $codebase, + ); + } + + if ($in_array_recursion && $input_type->possibly_undefined) { + $return_type = $return_type->setPossiblyUndefined(true); + } elseif (!$in_array_recursion && $not_set_type && $input_type->possibly_undefined) { + // in case of PHP CLI it will always fail for all filter_input even when they're set + // to fix this we would have to add support for environments in Context + // e.g. if php_sapi_name() === 'cli' + $return_type = Type::combineUnionTypes( + $return_type, + // the not set type is not coerced into an array when FILTER_FORCE_ARRAY is used + $not_set_type, + $codebase, + ); + } + + if (!$in_array_recursion) { + $return_type = self::addReturnTaint( + $statements_analyzer, + $code_location, + $return_type, + $function_id, + ); + } + + return $return_type; + } + + private static function addReturnTaint( + StatementsAnalyzer $statements_analyzer, + CodeLocation $code_location, + Union $return_type, + string $function_id + ): Union { + if ($statements_analyzer->data_flow_graph + && !in_array('TaintedInput', $statements_analyzer->getSuppressedIssues()) + ) { + $function_return_sink = DataFlowNode::getForMethodReturn( + $function_id, + $function_id, + null, + $code_location, + ); + + $statements_analyzer->data_flow_graph->addNode($function_return_sink); + + $function_param_sink = DataFlowNode::getForMethodArgument( + $function_id, + $function_id, + 0, + null, + $code_location, + ); + + $statements_analyzer->data_flow_graph->addNode($function_param_sink); + + $statements_analyzer->data_flow_graph->addPath( + $function_param_sink, + $function_return_sink, + 'arg', + ); + + $return_type = $return_type->setParentNodes([$function_return_sink->id => $function_return_sink]); + } + + return $return_type; + } + + /** @return array, options: array}> */ + public static function getFilters(Codebase $codebase): array + { + $general_filter_flags = array( + FILTER_REQUIRE_SCALAR, + FILTER_REQUIRE_ARRAY, + FILTER_FORCE_ARRAY, + FILTER_FLAG_NONE, // does nothing, default + ); + + // https://www.php.net/manual/en/filter.filters.sanitize.php + $sanitize_filters = array( + FILTER_SANITIZE_EMAIL => array( + 'flags' => array(), + 'options' => array(), + ), + FILTER_SANITIZE_ENCODED => array( + 'flags' => array( + FILTER_FLAG_STRIP_LOW, + FILTER_FLAG_STRIP_HIGH, + FILTER_FLAG_STRIP_BACKTICK, + FILTER_FLAG_ENCODE_LOW, + FILTER_FLAG_ENCODE_HIGH, + ), + 'options' => array(), + ), + FILTER_SANITIZE_NUMBER_FLOAT => array( + 'flags' => array( + FILTER_FLAG_ALLOW_FRACTION, + FILTER_FLAG_ALLOW_THOUSAND, + FILTER_FLAG_ALLOW_SCIENTIFIC, + ), + 'options' => array(), + ), + FILTER_SANITIZE_NUMBER_INT => array( + 'flags' => array(), + 'options' => array(), + ), + FILTER_SANITIZE_SPECIAL_CHARS => array( + 'flags' => array( + FILTER_FLAG_STRIP_LOW, + FILTER_FLAG_STRIP_HIGH, + FILTER_FLAG_STRIP_BACKTICK, + FILTER_FLAG_ENCODE_HIGH, + ), + 'options' => array(), + ), + FILTER_SANITIZE_FULL_SPECIAL_CHARS => array( + 'flags' => array( + FILTER_FLAG_NO_ENCODE_QUOTES, + ), + 'options' => array(), + ), + FILTER_SANITIZE_URL => array( + 'flags' => array(), + 'options' => array(), + ), + FILTER_UNSAFE_RAW => array( + 'flags' => array( + FILTER_FLAG_STRIP_LOW, + FILTER_FLAG_STRIP_HIGH, + FILTER_FLAG_STRIP_BACKTICK, + FILTER_FLAG_ENCODE_LOW, + FILTER_FLAG_ENCODE_HIGH, + FILTER_FLAG_ENCODE_AMP, + ), + 'options' => array(), + ), + + ); + + if ($codebase->analysis_php_version_id <= 7_03_00) { + // FILTER_SANITIZE_MAGIC_QUOTES + $sanitize_filters[521] = array( + 'flags' => array(), + 'options' => array(), + ); + } + + if ($codebase->analysis_php_version_id <= 8_01_00) { + // FILTER_SANITIZE_STRING + $sanitize_filters[513] = array( + 'flags' => array( + FILTER_FLAG_NO_ENCODE_QUOTES, + FILTER_FLAG_STRIP_LOW, + FILTER_FLAG_STRIP_HIGH, + FILTER_FLAG_STRIP_BACKTICK, + FILTER_FLAG_ENCODE_LOW, + FILTER_FLAG_ENCODE_HIGH, + FILTER_FLAG_ENCODE_AMP, + ), + 'options' => array(), + ); + } + + if ($codebase->analysis_php_version_id >= 7_03_00) { + // was added as a replacement for FILTER_SANITIZE_MAGIC_QUOTES + $sanitize_filters[FILTER_SANITIZE_ADD_SLASHES] = array( + 'flags' => array(), + 'options' => array(), + ); + } + + foreach ($sanitize_filters as $filter_int => $filter_data) { + $sanitize_filters[$filter_int]['flags'] = array_merge($filter_data['flags'], $general_filter_flags); + } + + // https://www.php.net/manual/en/filter.filters.validate.php + // validation filters all match bitmask 0x100 + // all support FILTER_NULL_ON_FAILURE flag https://www.php.net/manual/en/filter.filters.flags.php + $general_filter_flags_validate = array_merge($general_filter_flags, array(FILTER_NULL_ON_FAILURE)); + + $validate_filters = array( + FILTER_VALIDATE_BOOLEAN => array( + 'flags' => array(), + 'options' => array(), + ), + FILTER_VALIDATE_EMAIL => array( + 'flags' => array( + FILTER_FLAG_EMAIL_UNICODE, + ), + 'options' => array(), + ), + FILTER_VALIDATE_FLOAT => array( + 'flags' => array( + FILTER_FLAG_ALLOW_THOUSAND, + ), + 'options' => array( + 'decimal' => new Union([ + Type::getAtomicStringFromLiteral('.'), + Type::getAtomicStringFromLiteral(','), + ]), + ), + ), + FILTER_VALIDATE_INT => array( + 'flags' => array( + FILTER_FLAG_ALLOW_OCTAL, + FILTER_FLAG_ALLOW_HEX, + ), + 'options' => array( + 'min_range' => Type::getNumeric(), + 'max_range' => Type::getNumeric(), + ), + ), + FILTER_VALIDATE_IP => array( + 'flags' => array( + FILTER_FLAG_IPV4, + FILTER_FLAG_IPV6, + FILTER_FLAG_NO_PRIV_RANGE, + FILTER_FLAG_NO_RES_RANGE, + + ), + 'options' => array(), + ), + FILTER_VALIDATE_MAC => array( + 'flags' => array(), + 'options' => array(), + ), + FILTER_VALIDATE_REGEXP => array( + 'flags' => array(), + 'options' => array( + 'regexp' => Type::getNonFalsyString(), + ), + ), + FILTER_VALIDATE_URL => array( + 'flags' => array( + FILTER_FLAG_PATH_REQUIRED, + FILTER_FLAG_QUERY_REQUIRED, + ), + 'options' => array(), + ), + + ); + + if ($codebase->analysis_php_version_id >= 7_04_00) { + $validate_filters[FILTER_VALIDATE_FLOAT]['options']['min_range'] = Type::getNumeric(); + $validate_filters[FILTER_VALIDATE_FLOAT]['options']['max_range'] = Type::getNumeric(); + } + + if ($codebase->analysis_php_version_id < 8_00_00) { + // phpcs:ignore SlevomatCodingStandard.Numbers.RequireNumericLiteralSeparator.RequiredNumericLiteralSeparator + $validate_filters[FILTER_VALIDATE_URL]['flags'][] = 65536; // FILTER_FLAG_SCHEME_REQUIRED + // phpcs:ignore SlevomatCodingStandard.Numbers.RequireNumericLiteralSeparator.RequiredNumericLiteralSeparator + $validate_filters[FILTER_VALIDATE_URL]['flags'][] = 131072; // FILTER_FLAG_HOST_REQUIRED + } + + if ($codebase->analysis_php_version_id >= 8_02_00) { + // phpcs:ignore SlevomatCodingStandard.Numbers.RequireNumericLiteralSeparator.RequiredNumericLiteralSeparator + $validate_filters[FILTER_VALIDATE_IP]['flags'][] = 268435456; // FILTER_FLAG_GLOBAL_RANGE + } + + if ($codebase->analysis_php_version_id >= 7_00_00) { + $validate_filters[FILTER_VALIDATE_DOMAIN] = array( + 'flags' => array( + FILTER_FLAG_HOSTNAME, + ), + 'options' => array(), + ); + } + + foreach ($validate_filters as $filter_int => $filter_data) { + $validate_filters[$filter_int]['flags'] = array_merge( + $filter_data['flags'], + $general_filter_flags_validate, + ); + + $default_options = array( + 'default' => Type::getMixed(), + ); + $validate_filters[$filter_int]['options'] = array_merge($filter_data['options'], $default_options); + } + + // https://www.php.net/manual/en/filter.filters.misc.php + $other_filters = array( + FILTER_CALLBACK => array( + // the docs say that all flags are ignored + // however this seems to be incorrect https://github.com/php/doc-en/issues/2708 + // however they can only be used in the options array, not as a param directly + 'flags' => $general_filter_flags_validate, + // the options array is required for this filter + // and must be a valid callback instead of an array like in other cases + 'options' => array(), + ), + ); + + return $sanitize_filters + $validate_filters + $other_filters; + } +} diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php index bbf3642fa63..c6ea6a15fdf 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php @@ -3,30 +3,19 @@ namespace Psalm\Internal\Provider\ReturnTypeProvider; use Psalm\Internal\Analyzer\StatementsAnalyzer; -use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent; use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface; use Psalm\Type; -use Psalm\Type\Atomic\TFalse; -use Psalm\Type\Atomic\TKeyedArray; -use Psalm\Type\Atomic\TLiteralInt; -use Psalm\Type\Atomic\TNull; use Psalm\Type\Union; use UnexpectedValueException; -use function in_array; - -use const FILTER_NULL_ON_FAILURE; -use const FILTER_SANITIZE_URL; -use const FILTER_VALIDATE_BOOLEAN; -use const FILTER_VALIDATE_DOMAIN; -use const FILTER_VALIDATE_EMAIL; -use const FILTER_VALIDATE_FLOAT; -use const FILTER_VALIDATE_INT; -use const FILTER_VALIDATE_IP; -use const FILTER_VALIDATE_MAC; +use function is_array; +use function is_int; + +use const FILTER_CALLBACK; +use const FILTER_DEFAULT; +use const FILTER_FLAG_NONE; use const FILTER_VALIDATE_REGEXP; -use const FILTER_VALIDATE_URL; /** * @internal @@ -41,135 +30,124 @@ public static function getFunctionIds(): array return ['filter_var']; } - public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): Union + public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Union { - $statements_source = $event->getStatementsSource(); + $statements_analyzer = $event->getStatementsSource(); + if (!$statements_analyzer instanceof StatementsAnalyzer) { + throw new UnexpectedValueException(); + } + $call_args = $event->getCallArgs(); $function_id = $event->getFunctionId(); $code_location = $event->getCodeLocation(); - if (!$statements_source instanceof StatementsAnalyzer) { - throw new UnexpectedValueException(); + $codebase = $statements_analyzer->getCodebase(); + + if (! isset($call_args[0])) { + return FilterUtils::missingFirstArg($codebase); } - $filter_type = null; - - if (isset($call_args[1]) - && ($second_arg_type = $statements_source->node_data->getType($call_args[1]->value)) - && $second_arg_type->isSingleIntLiteral() - ) { - $filter_type_type = $second_arg_type->getSingleIntLiteral(); - - switch ($filter_type_type->value) { - case FILTER_VALIDATE_INT: - $filter_type = Type::getInt(); - break; - - case FILTER_VALIDATE_FLOAT: - $filter_type = Type::getFloat(); - break; - - case FILTER_VALIDATE_BOOLEAN: - $filter_type = Type::getBool(); - - break; - - case FILTER_VALIDATE_IP: - case FILTER_VALIDATE_MAC: - case FILTER_VALIDATE_REGEXP: - case FILTER_VALIDATE_URL: - case FILTER_VALIDATE_EMAIL: - case FILTER_VALIDATE_DOMAIN: - case FILTER_SANITIZE_URL: - $filter_type = Type::getString(); - break; - } + $filter_int_used = FILTER_DEFAULT; + if (isset($call_args[1])) { + $filter_int_used = FilterUtils::getFilterArgValueOrError( + $call_args[1], + $statements_analyzer, + $codebase, + ); - $has_object_like = false; - $filter_null = false; - - if (isset($call_args[2]) - && ($third_arg_type = $statements_source->node_data->getType($call_args[2]->value)) - && $filter_type - ) { - foreach ($third_arg_type->getAtomicTypes() as $atomic_type) { - if ($atomic_type instanceof TKeyedArray) { - $has_object_like = true; - - if (isset($atomic_type->properties['options']) - && $atomic_type->properties['options']->hasArray() - && ($options_array = $atomic_type->properties['options']->getArray()) - && $options_array instanceof TKeyedArray - && isset($options_array->properties['default']) - ) { - $filter_type = Type::combineUnionTypes( - $filter_type, - $options_array->properties['default'], - ); - } else { - $filter_type = $filter_type->getBuilder()->addType(new TFalse)->freeze(); - } - - if (isset($atomic_type->properties['flags']) - && $atomic_type->properties['flags']->isSingleIntLiteral() - ) { - $filter_flag_type = - $atomic_type->properties['flags']->getSingleIntLiteral(); - - if ($filter_type->hasBool() - && $filter_flag_type->value === FILTER_NULL_ON_FAILURE - ) { - $filter_type = $filter_type->getBuilder()->addType(new TNull)->freeze(); - } - } - } elseif ($atomic_type instanceof TLiteralInt) { - if ($atomic_type->value === FILTER_NULL_ON_FAILURE) { - $filter_null = true; - $filter_type = $filter_type->getBuilder()->addType(new TNull)->freeze(); - } - } - } + if (!is_int($filter_int_used)) { + return $filter_int_used; } + } + + $options = null; + $flags_int_used = FILTER_FLAG_NONE; + if (isset($call_args[2])) { + $helper = FilterUtils::getOptionsArgValueOrError( + $call_args[2], + $statements_analyzer, + $codebase, + $code_location, + $function_id, + $filter_int_used, + ); - if (!$has_object_like && !$filter_null && $filter_type) { - $filter_type = $filter_type->getBuilder()->addType(new TFalse)->freeze(); + if (!is_array($helper)) { + return $helper; } - } - if (!$filter_type) { - $filter_type = Type::getMixed(); + $flags_int_used = $helper['flags_int_used']; + $options = $helper['options']; } - if ($statements_source->data_flow_graph - && !in_array('TaintedInput', $statements_source->getSuppressedIssues()) - ) { - $function_return_sink = DataFlowNode::getForMethodReturn( - $function_id, + // if we reach this point with callback, the callback is missing + if ($filter_int_used === FILTER_CALLBACK) { + return FilterUtils::missingFilterCallbackCallable( $function_id, - null, $code_location, + $statements_analyzer, + $codebase, ); + } - $statements_source->data_flow_graph->addNode($function_return_sink); + [$default, $min_range, $max_range, $has_range, $regexp] = FilterUtils::getOptions( + $filter_int_used, + $flags_int_used, + $options, + $statements_analyzer, + $code_location, + $codebase, + $function_id, + ); + + if (!$default) { + [$fails_type] = FilterUtils::getFailsNotSetType($flags_int_used); + } else { + $fails_type = $default; + } - $function_param_sink = DataFlowNode::getForMethodArgument( - $function_id, - $function_id, - 0, - null, - $code_location, - ); + if ($filter_int_used === FILTER_VALIDATE_REGEXP && $regexp === null) { + if ($codebase->analysis_php_version_id >= 8_00_00) { + // throws + return Type::getNever(); + } + + // any "array" flags are ignored by this filter! + return $fails_type; + } - $statements_source->data_flow_graph->addNode($function_param_sink); + $input_type = $statements_analyzer->node_data->getType($call_args[0]->value); - $statements_source->data_flow_graph->addPath( - $function_param_sink, - $function_return_sink, - 'arg', - ); + // only return now, as we still want to report errors above + if (!$input_type) { + return null; + } - return $filter_type->setParentNodes([$function_return_sink->id => $function_return_sink]); + $redundant_error_return_type = FilterUtils::checkRedundantFlags( + $filter_int_used, + $flags_int_used, + $fails_type, + $statements_analyzer, + $code_location, + $codebase, + ); + if ($redundant_error_return_type !== null) { + return $redundant_error_return_type; } - return $filter_type; + return FilterUtils::getReturnType( + $filter_int_used, + $flags_int_used, + $input_type, + $fails_type, + null, + $statements_analyzer, + $code_location, + $codebase, + $function_id, + $has_range, + $min_range, + $max_range, + $regexp, + ); } } diff --git a/src/Psalm/Issue/RedundantFlag.php b/src/Psalm/Issue/RedundantFlag.php new file mode 100644 index 00000000000..d3f0429636d --- /dev/null +++ b/src/Psalm/Issue/RedundantFlag.php @@ -0,0 +1,9 @@ + 'string', ], ], + 'filterInput' => [ + 'code' => ' ["default" => null]]); + } + function filterIntWithDefault(string $s) : int { + return filter_var($s, FILTER_VALIDATE_INT, ["options" => ["default" => 5]]); + } + function filterBool(string $s) : bool { + return filter_var($s, FILTER_VALIDATE_BOOLEAN); + } + function filterNullableBool(string $s) : ?bool { + return filter_var($s, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + } + function filterNullableBoolWithFlagsArray(string $s) : ?bool { + return filter_var($s, FILTER_VALIDATE_BOOLEAN, ["flags" => FILTER_NULL_ON_FAILURE]); + } + function filterFloat(string $s) : float { + $filtered = filter_var($s, FILTER_VALIDATE_FLOAT); + if ($filtered === false) { + return 0.0; + } + return $filtered; + } + function filterFloatWithDefault(string $s) : float { + return filter_var($s, FILTER_VALIDATE_FLOAT, ["options" => ["default" => 5.0]]); + }', + ], 'filterVar' => [ 'code' => ' [ 'code' => '|false|string */ - function foo() : array { + function foo() { return filter_input(INPUT_POST, "some_var") ?? []; }', ], From f8a53ebc5db2ae12a51f94a4590b67b0c4071592 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Mon, 18 Dec 2023 12:48:13 +0100 Subject: [PATCH 178/357] Fix callable without args not handled correctly --- .../Expression/Call/HighOrderFunctionArgHandler.php | 3 ++- tests/FunctionCallTest.php | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php index 3d1a51c4e67..14a947f517a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php @@ -288,7 +288,8 @@ private static function isSupported(FunctionLikeParameter $container_param): boo } foreach ($container_param->type->getAtomicTypes() as $a) { - if (($a instanceof TClosure || $a instanceof TCallable) && !$a->params) { + // must check null explicitly, since no params (empty array) would not be handled correctly otherwise + if (($a instanceof TClosure || $a instanceof TCallable) && $a->params === null) { return false; } diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 2b73d655018..b24166f9c2f 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -2352,6 +2352,18 @@ function fooFoo(int $a): void {} fooFoo("string");', 'error_message' => 'InvalidScalarArgument', ], + 'invalidArgumentCallableWithoutArgsUnion' => [ + 'code' => ' 'InvalidArgument', + ], 'invalidArgumentWithDeclareStrictTypes' => [ 'code' => ' Date: Mon, 18 Dec 2023 12:53:45 +0100 Subject: [PATCH 179/357] fix other places that have a similar bug --- src/Psalm/Codebase.php | 8 ++++---- src/Psalm/Internal/Codebase/Functions.php | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 414703a4435..993958e607a 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -1688,7 +1688,7 @@ public function getSignatureInformation( if (InternalCallMapHandler::inCallMap($function_symbol)) { $callables = InternalCallMapHandler::getCallablesFromCallMap($function_symbol); - if (!$callables || !$callables[0]->params) { + if (!$callables || !isset($callables[0]->params)) { return null; } @@ -1838,7 +1838,7 @@ public function getBeginedLiteralPart(string $file_path, Position $position): st $offset = $position->toOffset($file_contents); preg_match('/\$?\w+$/', substr($file_contents, 0, $offset), $matches); - + return $matches[0] ?? ''; } @@ -1957,7 +1957,7 @@ public function getCompletionItemsForClassishThing( str_replace('$', '', $property_name), ); } - + foreach ($class_storage->pseudo_property_set_types as $property_name => $type) { $pseudo_property_types[$property_name] = new CompletionItem( str_replace('$', '', $property_name), @@ -1969,7 +1969,7 @@ public function getCompletionItemsForClassishThing( str_replace('$', '', $property_name), ); } - + $completion_items = [...$completion_items, ...array_values($pseudo_property_types)]; } diff --git a/src/Psalm/Internal/Codebase/Functions.php b/src/Psalm/Internal/Codebase/Functions.php index acba38f28bb..22fe2ee8edf 100644 --- a/src/Psalm/Internal/Codebase/Functions.php +++ b/src/Psalm/Internal/Codebase/Functions.php @@ -466,7 +466,7 @@ public function isCallMapFunctionPure( null, ); - if (!$function_callable->params + if (!isset($function_callable->params) || ($args !== null && count($args) === 0) || ($function_callable->return_type && $function_callable->return_type->isVoid()) ) { From 456a5cde16bba4209cb4d17e6e965642fabb542d Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 19 Dec 2023 10:57:16 +0100 Subject: [PATCH 180/357] Fixes --- src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php | 2 +- src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php | 2 +- src/Psalm/Type.php | 4 ++-- src/Psalm/Type/Reconciler.php | 6 +----- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 35ab5488bb9..6f0c64de108 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -164,7 +164,7 @@ public function analyze( NodeDataProvider $type_provider, ?Context $global_context = null, bool $add_mutations = false, - array &$byref_vars = [] + array &$byref_vars = [], ): ?bool { $storage = $this->storage; diff --git a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php index d479c926e48..c9ac356eb68 100644 --- a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php @@ -57,7 +57,7 @@ public static function isContainedBy( && !$container_type_part->isNonEmpty() && !$container_type_part->isSealed() && $input_type_part->equals( - $container_type_part->getGenericArrayType($container_type_part->isNonEmpty()), + $container_type_part->getGenericArrayType(), false, ) ) { diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 589e6bd0711..12f19b3d658 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -716,7 +716,7 @@ public static function intersectUnionTypes( ?Union $type_2, Codebase $codebase, bool $allow_interface_equality = false, - bool $allow_float_int_equality = true + bool $allow_float_int_equality = true, ): ?Union { if ($type_2 === null && $type_1 === null) { throw new UnexpectedValueException('At least one type must be provided to combine'); @@ -846,7 +846,7 @@ private static function intersectAtomicTypes( Codebase $codebase, bool &$intersection_performed, bool $allow_interface_equality = false, - bool $allow_float_int_equality = true + bool $allow_float_int_equality = true, ): ?Atomic { $intersection_atomic = null; $wider_type = null; diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 1836641a169..1822bf75422 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -47,7 +47,6 @@ use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TKeyedArray; -use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TMixed; @@ -1152,11 +1151,8 @@ private static function adjustTKeyedArrayType( ); foreach ($array_key_offsets as $array_key_offset) { - if (isset($existing_types[$base_key]) && $array_key_offset !== false) { + if (isset($existing_types[$base_key])) { foreach ($existing_types[$base_key]->getAtomicTypes() as $base_atomic_type) { - if ($base_atomic_type instanceof TList) { - $base_atomic_type = $base_atomic_type->getKeyedArray(); - } if ($base_atomic_type instanceof TKeyedArray || ($base_atomic_type instanceof TArray && !$base_atomic_type->isEmptyArray()) From 1ff8518888e8c8839f59a54f4fbb30d0cb2de716 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Tue, 19 Dec 2023 11:07:11 +0100 Subject: [PATCH 181/357] Fix https://github.com/vimeo/psalm/issues/9840 --- .../Expression/Call/ArgumentsAnalyzer.php | 73 +++++++++++++++++++ tests/ImmutableAnnotationTest.php | 63 ++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 9ff268abeb6..97026ac25e2 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -7,6 +7,7 @@ use Psalm\Codebase; use Psalm\Context; use Psalm\Internal\Analyzer\AttributesAnalyzer; +use Psalm\Internal\Analyzer\Statements\Expression\Assignment\InstancePropertyAssignmentAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\AssignmentAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier; @@ -45,6 +46,7 @@ use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TLiteralString; +use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNonEmptyArray; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Union; @@ -1253,6 +1255,37 @@ private static function evaluateArbitraryParam( return null; } + private static function handleByRefReadonlyArg( + StatementsAnalyzer $statements_analyzer, + Context $context, + PhpParser\Node\Expr\PropertyFetch $stmt, + string $fq_class_name, + string $prop_name + ): void { + $property_id = $fq_class_name . '::$' . $prop_name; + + $codebase = $statements_analyzer->getCodebase(); + $declaring_property_class = (string) $codebase->properties->getDeclaringClassForProperty( + $property_id, + false, # what does this do? @todo + $statements_analyzer, + ); + $declaring_class_storage = $codebase->classlike_storage_provider->get($declaring_property_class); + + if (isset($declaring_class_storage->properties[$prop_name])) { + $property_storage = $declaring_class_storage->properties[$prop_name]; + + InstancePropertyAssignmentAnalyzer::trackPropertyImpurity( + $statements_analyzer, + $stmt, + $property_id, + $property_storage, + $declaring_class_storage, + $context, + ); + } + } + /** * @return false|null */ @@ -1274,6 +1307,46 @@ private static function handleByRefFunctionArg( 'reset', 'end', 'next', 'prev', 'array_pop', 'array_shift', ]; + if ($arg->value instanceof PhpParser\Node\Expr\PropertyFetch + && $arg->value->name instanceof PhpParser\Node\Identifier) { + $prop_name = $arg->value->name->name; + if ($statements_analyzer->getFQCLN()) { + $fq_class_name = $statements_analyzer->getFQCLN(); + + self::handleByRefReadonlyArg( + $statements_analyzer, + $context, + $arg->value, + $fq_class_name, + $prop_name, + ); + } else { + // @todo atm only works for simple fetch, $a->foo, not $a->foo->bar + // I guess there's a function to do this, but I couldn't locate it + $var_id = ExpressionIdentifier::getVarId( + $arg->value->var, + $statements_analyzer->getFQCLN(), + $statements_analyzer, + ); + + if ($var_id && isset($context->vars_in_scope[$var_id])) { + foreach ($context->vars_in_scope[$var_id]->getAtomicTypes() as $atomic_type) { + if ($atomic_type instanceof TNamedObject) { + $fq_class_name = $atomic_type->value; + + self::handleByRefReadonlyArg( + $statements_analyzer, + $context, + $arg->value, + $fq_class_name, + $prop_name, + ); + } + } + } + } + } + if (($var_id && isset($context->vars_in_scope[$var_id])) || ($method_id && in_array( diff --git a/tests/ImmutableAnnotationTest.php b/tests/ImmutableAnnotationTest.php index 1aa481b2a03..56838b9411f 100644 --- a/tests/ImmutableAnnotationTest.php +++ b/tests/ImmutableAnnotationTest.php @@ -764,6 +764,69 @@ public function getShortMutating() : string { }', 'error_message' => 'ImpurePropertyAssignment', ], + 'readonlyByRefInClass' => [ + 'code' => 'values = $values; + } + + public function bar(): mixed + { + return reset($this->values); + } + }', + 'error_message' => 'InaccessibleProperty', + ], + 'readonlyByRef' => [ + 'code' => 'values = $values; + } + } + + $x = new Foo([]); + reset($x->values);', + 'error_message' => 'InaccessibleProperty', + ], + 'readonlyByRefCustomFunction' => [ + 'code' => 'values = $values; + } + } + + /** + * @param string $a + * @param array $b + * @return void + */ + function bar($a, &$b) {} + + $x = new Foo([]); + bar("hello", $x->values);', + 'error_message' => 'InaccessibleProperty', + ], 'preventUnset' => [ 'code' => ' Date: Tue, 19 Dec 2023 11:36:06 +0100 Subject: [PATCH 182/357] Fixes --- .../Type/Comparator/ArrayTypeComparator.php | 13 ------------- src/Psalm/Type/Atomic/TKeyedArray.php | 2 +- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php index c9ac356eb68..8a39b72c976 100644 --- a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php @@ -51,19 +51,6 @@ public static function isContainedBy( return true; } - if ($container_type_part instanceof TKeyedArray - && $input_type_part instanceof TArray - && !$container_type_part->is_list - && !$container_type_part->isNonEmpty() - && !$container_type_part->isSealed() - && $input_type_part->equals( - $container_type_part->getGenericArrayType(), - false, - ) - ) { - return true; - } - if ($container_type_part instanceof TKeyedArray && $input_type_part instanceof TArray ) { diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index ed6a9c6cfb5..f3c10394ad7 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -416,7 +416,7 @@ public function getGenericArrayType(?string $list_var_id = null): TArray $value_type = $value_type->setPossiblyUndefined(false); - if ($has_defined_keys || $this->fallback_params !== null) { + if ($has_defined_keys) { return new TNonEmptyArray([$key_type, $value_type]); } return new TArray([$key_type, $value_type]); From 9e463bbe75720ae3ed10a3733d8f8e89d4727cf2 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Tue, 19 Dec 2023 11:22:51 +0100 Subject: [PATCH 183/357] make tests work in PHP < 8.2 --- .../Expression/Call/ArgumentsAnalyzer.php | 12 ++- src/Psalm/Type/Atomic/TKeyedArray.php | 2 + tests/ImmutableAnnotationTest.php | 98 +++++++++++-------- 3 files changed, 66 insertions(+), 46 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 97026ac25e2..003355bfcd4 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Analyzer\Statements\Expression\Call; +use InvalidArgumentException; use PhpParser; use Psalm\CodeLocation; use Psalm\Codebase; @@ -1267,10 +1268,15 @@ private static function handleByRefReadonlyArg( $codebase = $statements_analyzer->getCodebase(); $declaring_property_class = (string) $codebase->properties->getDeclaringClassForProperty( $property_id, - false, # what does this do? @todo + true, $statements_analyzer, ); - $declaring_class_storage = $codebase->classlike_storage_provider->get($declaring_property_class); + + try { + $declaring_class_storage = $codebase->classlike_storage_provider->get($declaring_property_class); + } catch (InvalidArgumentException $_) { + return; + } if (isset($declaring_class_storage->properties[$prop_name])) { $property_storage = $declaring_class_storage->properties[$prop_name]; @@ -1310,7 +1316,7 @@ private static function handleByRefFunctionArg( if ($arg->value instanceof PhpParser\Node\Expr\PropertyFetch && $arg->value->name instanceof PhpParser\Node\Identifier) { $prop_name = $arg->value->name->name; - if ($statements_analyzer->getFQCLN()) { + if (!empty($statements_analyzer->getFQCLN())) { $fq_class_name = $statements_analyzer->getFQCLN(); self::handleByRefReadonlyArg( diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index 3bd7e2a65e4..c1c29b991c8 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -112,6 +112,8 @@ public function setProperties(array $properties): self if ($cloned->is_list) { $last_k = -1; $had_possibly_undefined = false; + + /** @psalm-suppress InaccessibleProperty */ ksort($cloned->properties); foreach ($cloned->properties as $k => $v) { if (is_string($k) || $last_k !== ($k-1) || ($had_possibly_undefined && !$v->possibly_undefined)) { diff --git a/tests/ImmutableAnnotationTest.php b/tests/ImmutableAnnotationTest.php index 56838b9411f..da36f660df9 100644 --- a/tests/ImmutableAnnotationTest.php +++ b/tests/ImmutableAnnotationTest.php @@ -768,63 +768,75 @@ public function getShortMutating() : string { 'code' => 'values = $values; - } - - public function bar(): mixed - { - return reset($this->values); - } - }', + final class Foo + { + /** + * @readonly + */ + public array $values; + + public function __construct(array $values) + { + $this->values = $values; + } + + /** + * @return mixed + */ + public function bar() + { + return reset($this->values); + } + }', 'error_message' => 'InaccessibleProperty', ], 'readonlyByRef' => [ 'code' => 'values = $values; - } - } + public function __construct(array $values) + { + $this->values = $values; + } + } - $x = new Foo([]); - reset($x->values);', + $x = new Foo([]); + reset($x->values);', 'error_message' => 'InaccessibleProperty', ], 'readonlyByRefCustomFunction' => [ 'code' => 'values = $values; - } - } - - /** - * @param string $a - * @param array $b - * @return void - */ - function bar($a, &$b) {} - - $x = new Foo([]); - bar("hello", $x->values);', + final class Foo + { + /** + * @readonly + */ + public array $values; + + public function __construct(array $values) + { + $this->values = $values; + } + } + + /** + * @param string $a + * @param array $b + * @return void + */ + function bar($a, &$b) {} + + $x = new Foo([]); + bar("hello", $x->values);', 'error_message' => 'InaccessibleProperty', ], 'preventUnset' => [ From c6c7649af37d425ee831e158c4a4990338766b10 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 19 Dec 2023 11:53:41 +0100 Subject: [PATCH 184/357] Fixup --- tests/ArrayAssignmentTest.php | 2 +- tests/TypeReconciliation/ConditionalTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 6e7aaf7bb20..343b86506b2 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -1869,7 +1869,7 @@ function with($key): void 'code' => ' $array - * @return non-empty-array + * @return array */ function getArray(array $array): array { if (rand(0, 1)) { diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index c9fcb15706f..460ea919a3c 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -1642,7 +1642,7 @@ function foo(string $a, string $b) : void { 'code' => ' $arr - * @return non-empty-array + * @return array */ function foo(array $arr) : array { if (isset($arr["a"])) { From 5731f927feb67e762e16bdd3c5694569d8315a95 Mon Sep 17 00:00:00 2001 From: ging-dev Date: Wed, 20 Dec 2023 17:16:21 +0700 Subject: [PATCH 185/357] fix: #10496 #10503 --- src/Psalm/Internal/Type/TypeParser.php | 80 ++++++++------------------ tests/AnnotationTest.php | 14 +++++ 2 files changed, 39 insertions(+), 55 deletions(-) diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 58718eae2dd..e908c4e3a1d 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -641,13 +641,31 @@ private static function getTypeFromGenericTree( throw new TypeParseTreeException('No generic params provided for type'); } - if ($generic_type_value === 'array' || $generic_type_value === 'associative-array') { + if ($generic_type_value === 'array' + || $generic_type_value === 'associative-array' + || $generic_type_value === 'non-empty-array' + ) { + if ($generic_type_value !== 'non-empty-array') { + $generic_type_value = 'array'; + } + if ($generic_params[0]->isMixed()) { $generic_params[0] = Type::getArrayKey($from_docblock); } if (count($generic_params) !== 2) { - throw new TypeParseTreeException('Too many template parameters for array'); + throw new TypeParseTreeException('Too many template parameters for '.$generic_type_value); + } + + if ($type_aliases !== []) { + $intersection_types = self::resolveTypeAliases( + $codebase, + $generic_params[0]->getAtomicTypes(), + ); + + if ($intersection_types !== []) { + $generic_params[0] = $generic_params[0]->setTypes($intersection_types); + } } foreach ($generic_params[0]->getAtomicTypes() as $key => $atomic_type) { @@ -671,6 +689,7 @@ private static function getTypeFromGenericTree( || $atomic_type instanceof TNever || $atomic_type instanceof TTemplateParam || $atomic_type instanceof TValueOf + || !$from_docblock ) { continue; } @@ -690,7 +709,10 @@ private static function getTypeFromGenericTree( throw new TypeParseTreeException('Invalid array key type ' . $atomic_type->getKey()); } - return new TArray($generic_params, $from_docblock); + return $generic_type_value === 'array' + ? new TArray($generic_params, $from_docblock) + : new TNonEmptyArray($generic_params, null, null, 'non-empty-array', $from_docblock) + ; } if ($generic_type_value === 'arraylike-object') { @@ -709,58 +731,6 @@ private static function getTypeFromGenericTree( ); } - if ($generic_type_value === 'non-empty-array') { - if ($generic_params[0]->isMixed()) { - $generic_params[0] = Type::getArrayKey($from_docblock); - } - - if (count($generic_params) !== 2) { - throw new TypeParseTreeException('Too many template parameters for non-empty-array'); - } - - foreach ($generic_params[0]->getAtomicTypes() as $key => $atomic_type) { - // PHP 8 values with whitespace after number are counted as numeric - // and filter_var treats them as such too - if ($atomic_type instanceof TLiteralString - && trim($atomic_type->value) === $atomic_type->value - && ($string_to_int = filter_var($atomic_type->value, FILTER_VALIDATE_INT)) !== false - ) { - $builder = $generic_params[0]->getBuilder(); - $builder->removeType($key); - $generic_params[0] = $builder->addType(new TLiteralInt($string_to_int, $from_docblock))->freeze(); - continue; - } - - if ($atomic_type instanceof TInt - || $atomic_type instanceof TString - || $atomic_type instanceof TArrayKey - || $atomic_type instanceof TClassConstant // @todo resolve and check types - || $atomic_type instanceof TMixed - || $atomic_type instanceof TNever - || $atomic_type instanceof TTemplateParam - || $atomic_type instanceof TValueOf - ) { - continue; - } - - if ($codebase->register_stub_files || $codebase->register_autoload_files) { - $builder = $generic_params[0]->getBuilder(); - $builder->removeType($key); - - if (count($generic_params[0]->getAtomicTypes()) <= 1) { - $builder = $builder->addType(new TArrayKey($from_docblock)); - } - - $generic_params[0] = $builder->freeze(); - continue; - } - - throw new TypeParseTreeException('Invalid array key type ' . $atomic_type->getKey()); - } - - return new TNonEmptyArray($generic_params, null, null, 'non-empty-array', $from_docblock); - } - if ($generic_type_value === 'iterable') { if (count($generic_params) > 2) { throw new TypeParseTreeException('Too many template parameters for iterable'); diff --git a/tests/AnnotationTest.php b/tests/AnnotationTest.php index e0855c3bdc8..64ebf674b4e 100644 --- a/tests/AnnotationTest.php +++ b/tests/AnnotationTest.php @@ -1348,6 +1348,20 @@ function f(): array } EOT, ], + 'validArrayKeyAlias' => [ + 'code' => ' + */ + class Foo {}', + 'assertions' => [], + ], ]; } From 1cb7dd9d7277cf630584c13a3a7d165c756de595 Mon Sep 17 00:00:00 2001 From: Sam L Date: Sun, 24 Dec 2023 12:02:37 -0500 Subject: [PATCH 186/357] Initial commit, issue-10490 Reproduce failure in unit test --- tests/DeprecatedAnnotationTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/DeprecatedAnnotationTest.php b/tests/DeprecatedAnnotationTest.php index adcf30e5b17..0f465bf7c72 100644 --- a/tests/DeprecatedAnnotationTest.php +++ b/tests/DeprecatedAnnotationTest.php @@ -99,6 +99,24 @@ class A { public $property; } '], + 'suppressDeprecatedClassOnTemplateType' => [ + 'code' => ' + * @psalm-suppress DeprecatedClass + */ + class TheChildClass extends TheParentClass {} + '], ]; } From 21617c702913b5556383421e72f88b9884fc6aad Mon Sep 17 00:00:00 2001 From: Sam L Date: Sun, 24 Dec 2023 17:29:05 -0500 Subject: [PATCH 187/357] Include suppressed issues from the class the template is being used by --- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index a37b4b7ad04..7d376caf544 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -350,7 +350,7 @@ public function analyze( null, true, ), - $this->getSuppressedIssues(), + $storage->suppressed_issues + $this->getSuppressedIssues(), ); } } From a60de4bac81a1d99b976bea2a962ad67fe65093f Mon Sep 17 00:00:00 2001 From: Sam L Date: Sun, 24 Dec 2023 17:50:57 -0500 Subject: [PATCH 188/357] Whitelist suppressed issue types to for template extended params --- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 2 +- src/Psalm/Storage/ClassLikeStorage.php | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index 7d376caf544..4e6ef467bcb 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -350,7 +350,7 @@ public function analyze( null, true, ), - $storage->suppressed_issues + $this->getSuppressedIssues(), + $storage->getSuppressedIssuesForTemplateExtendParams() + $this->getSuppressedIssues(), ); } } diff --git a/src/Psalm/Storage/ClassLikeStorage.php b/src/Psalm/Storage/ClassLikeStorage.php index f8564444a26..737854d5eda 100644 --- a/src/Psalm/Storage/ClassLikeStorage.php +++ b/src/Psalm/Storage/ClassLikeStorage.php @@ -10,11 +10,13 @@ use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Type\TypeAlias\ClassTypeAlias; use Psalm\Issue\CodeIssue; +use Psalm\Issue\DeprecatedClass; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Union; use function array_values; +use function in_array; final class ClassLikeStorage implements HasAttributesInterface { @@ -550,4 +552,22 @@ private function hasAttribute(string $fq_class_name): bool return false; } + + /** + * @return array + */ + public function getSuppressedIssuesForTemplateExtendParams(): array + { + $allowed_issue_types = [ + DeprecatedClass::getIssueType(), + ]; + $suppressed_issues_for_template_extend_params = []; + foreach ($this->suppressed_issues as $offset => $suppressed_issue) { + if (!in_array($suppressed_issue, $allowed_issue_types, true)) { + continue; + } + $suppressed_issues_for_template_extend_params[$offset] = $suppressed_issue; + } + return $suppressed_issues_for_template_extend_params; + } } From 891036ef3b5aba5466e7fad85ff93c3530610277 Mon Sep 17 00:00:00 2001 From: Ivan Kurnosov Date: Wed, 3 Jan 2024 10:55:04 +1300 Subject: [PATCH 189/357] Fixed SessionUpdateTimestampHandlerInterface parameter names Close https://github.com/vimeo/psalm/issues/10512 --- dictionaries/CallMap.php | 4 ++-- dictionaries/CallMap_historical.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 853b78ed8a0..a771eb5b74d 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -11141,8 +11141,8 @@ 'SessionIdInterface::create_sid' => ['string'], 'SessionUpdateTimestampHandler::updateTimestamp' => ['bool', 'id'=>'string', 'data'=>'string'], 'SessionUpdateTimestampHandler::validateId' => ['char', 'id'=>'string'], -'SessionUpdateTimestampHandlerInterface::updateTimestamp' => ['bool', 'key'=>'string', 'value'=>'string'], -'SessionUpdateTimestampHandlerInterface::validateId' => ['bool', 'key'=>'string'], +'SessionUpdateTimestampHandlerInterface::updateTimestamp' => ['bool', 'id'=>'string', 'data'=>'string'], +'SessionUpdateTimestampHandlerInterface::validateId' => ['bool', 'id'=>'string'], 'set_error_handler' => ['null|callable(int,string,string=,int=,array=):bool', 'callback'=>'null|callable(int,string,string=,int=,array=):bool', 'error_levels='=>'int'], 'set_exception_handler' => ['null|callable(Throwable):void', 'callback'=>'null|callable(Throwable):void'], 'set_file_buffer' => ['int', 'stream'=>'resource', 'size'=>'int'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 60e798e09cb..56399017b9f 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -6583,8 +6583,8 @@ 'SessionIdInterface::create_sid' => ['string'], 'SessionUpdateTimestampHandler::updateTimestamp' => ['bool', 'id'=>'string', 'data'=>'string'], 'SessionUpdateTimestampHandler::validateId' => ['char', 'id'=>'string'], - 'SessionUpdateTimestampHandlerInterface::updateTimestamp' => ['bool', 'key'=>'string', 'value'=>'string'], - 'SessionUpdateTimestampHandlerInterface::validateId' => ['bool', 'key'=>'string'], + 'SessionUpdateTimestampHandlerInterface::updateTimestamp' => ['bool', 'id'=>'string', 'data'=>'string'], + 'SessionUpdateTimestampHandlerInterface::validateId' => ['bool', 'id'=>'string'], 'SimpleXMLElement::__construct' => ['void', 'data'=>'string', 'options='=>'int', 'dataIsURL='=>'bool', 'namespaceOrPrefix='=>'string', 'isPrefix='=>'bool'], 'SimpleXMLElement::__get' => ['SimpleXMLElement', 'name'=>'string'], 'SimpleXMLElement::__toString' => ['string'], From 1a3bba3e91d4771faeb76d4412c47b8533292e54 Mon Sep 17 00:00:00 2001 From: sji Date: Tue, 9 Jan 2024 06:42:49 +0900 Subject: [PATCH 190/357] Reduce memory consumption of caching and parallel processing without igbinary --- src/Psalm/Aliases.php | 4 ++ src/Psalm/CodeLocation.php | 39 ++++++++++++++++ src/Psalm/Internal/MethodIdentifier.php | 2 + src/Psalm/Storage/Assertion.php | 1 + src/Psalm/Storage/AttributeArg.php | 1 + src/Psalm/Storage/AttributeStorage.php | 1 + src/Psalm/Storage/ClassConstantStorage.php | 1 + src/Psalm/Storage/ClassLikeStorage.php | 1 + src/Psalm/Storage/EnumCaseStorage.php | 2 + src/Psalm/Storage/FileStorage.php | 1 + src/Psalm/Storage/FunctionLikeParameter.php | 1 + src/Psalm/Storage/FunctionLikeStorage.php | 1 + src/Psalm/Storage/Possibilities.php | 2 + src/Psalm/Storage/PropertyStorage.php | 1 + ...UnserializeMemoryUsageSuppressionTrait.php | 24 ++++++++++ src/Psalm/Type/Atomic.php | 3 ++ src/Psalm/Type/Union.php | 46 +++++++++++++++++++ 17 files changed, 131 insertions(+) create mode 100644 src/Psalm/Storage/UnserializeMemoryUsageSuppressionTrait.php diff --git a/src/Psalm/Aliases.php b/src/Psalm/Aliases.php index 526021c8a57..103d3f449d0 100644 --- a/src/Psalm/Aliases.php +++ b/src/Psalm/Aliases.php @@ -2,8 +2,12 @@ namespace Psalm; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; + final class Aliases { + use UnserializeMemoryUsageSuppressionTrait; + /** * @var array */ diff --git a/src/Psalm/CodeLocation.php b/src/Psalm/CodeLocation.php index 0bdfd64a683..344a5981972 100644 --- a/src/Psalm/CodeLocation.php +++ b/src/Psalm/CodeLocation.php @@ -100,6 +100,32 @@ class CodeLocation public const CATCH_VAR = 6; public const FUNCTION_PHPDOC_METHOD = 7; + private const PROPERTY_KEYS_FOR_UNSERIALIZE = [ + 'file_path' => 'file_path', + 'file_name' => 'file_name', + 'raw_line_number' => 'raw_line_number', + "\0" . self::class . "\0" . 'end_line_number' => 'end_line_number', + 'raw_file_start' => 'raw_file_start', + 'raw_file_end' => 'raw_file_end', + "\0*\0" . 'file_start' => 'file_start', + "\0*\0" . 'file_end' => 'file_end', + "\0*\0" . 'single_line' => 'single_line', + "\0*\0" . 'preview_start' => 'preview_start', + "\0" . self::class . "\0" . 'preview_end' => 'preview_end', + "\0" . self::class . "\0" . 'selection_start' => 'selection_start', + "\0" . self::class . "\0" . 'selection_end' => 'selection_end', + "\0" . self::class . "\0" . 'column_from' => 'column_from', + "\0" . self::class . "\0" . 'column_to' => 'column_to', + "\0" . self::class . "\0" . 'snippet' => 'snippet', + "\0" . self::class . "\0" . 'text' => 'text', + 'docblock_start' => 'docblock_start', + "\0" . self::class . "\0" . 'docblock_start_line_number' => 'docblock_start_line_number', + "\0*\0" . 'docblock_line_number' => 'docblock_line_number', + "\0" . self::class . "\0" . 'regex_type' => 'regex_type', + "\0" . self::class . "\0" . 'have_recalculated' => 'have_recalculated', + 'previous_location' => 'previous_location', + ]; + public function __construct( FileSource $file_source, PhpParser\Node $stmt, @@ -136,6 +162,19 @@ public function __construct( $this->docblock_line_number = $comment_line; } + /** + * Suppresses memory usage when unserializing objects. + * + * @see \Psalm\Storage\UnserializeMemoryUsageSuppressionTrait + */ + public function __unserialize(array $properties): void + { + foreach (self::PROPERTY_KEYS_FOR_UNSERIALIZE as $key => $property_name) { + /** @psalm-suppress PossiblyUndefinedStringArrayOffset */ + $this->$property_name = $properties[$key]; + } + } + /** * @psalm-suppress PossiblyUnusedMethod Part of public API * @return static diff --git a/src/Psalm/Internal/MethodIdentifier.php b/src/Psalm/Internal/MethodIdentifier.php index 8d2d18d6d23..62742a74b9f 100644 --- a/src/Psalm/Internal/MethodIdentifier.php +++ b/src/Psalm/Internal/MethodIdentifier.php @@ -4,6 +4,7 @@ use InvalidArgumentException; use Psalm\Storage\ImmutableNonCloneableTrait; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use function explode; use function is_string; @@ -18,6 +19,7 @@ final class MethodIdentifier { use ImmutableNonCloneableTrait; + use UnserializeMemoryUsageSuppressionTrait; public string $fq_class_name; /** @var lowercase-string */ diff --git a/src/Psalm/Storage/Assertion.php b/src/Psalm/Storage/Assertion.php index 2fcc4324bbc..4053ba9c44d 100644 --- a/src/Psalm/Storage/Assertion.php +++ b/src/Psalm/Storage/Assertion.php @@ -10,6 +10,7 @@ abstract class Assertion { use ImmutableNonCloneableTrait; + use UnserializeMemoryUsageSuppressionTrait; abstract public function getNegation(): Assertion; diff --git a/src/Psalm/Storage/AttributeArg.php b/src/Psalm/Storage/AttributeArg.php index 0472ad6b94b..17b5fd43d92 100644 --- a/src/Psalm/Storage/AttributeArg.php +++ b/src/Psalm/Storage/AttributeArg.php @@ -12,6 +12,7 @@ final class AttributeArg { use ImmutableNonCloneableTrait; + use UnserializeMemoryUsageSuppressionTrait; /** * @var ?string * @psalm-suppress PossiblyUnusedProperty It's part of the public API for now diff --git a/src/Psalm/Storage/AttributeStorage.php b/src/Psalm/Storage/AttributeStorage.php index 7b9b0e7298a..72bca00bb60 100644 --- a/src/Psalm/Storage/AttributeStorage.php +++ b/src/Psalm/Storage/AttributeStorage.php @@ -10,6 +10,7 @@ final class AttributeStorage { use ImmutableNonCloneableTrait; + use UnserializeMemoryUsageSuppressionTrait; /** * @var string */ diff --git a/src/Psalm/Storage/ClassConstantStorage.php b/src/Psalm/Storage/ClassConstantStorage.php index 9c6d7cfcde5..072f80a0439 100644 --- a/src/Psalm/Storage/ClassConstantStorage.php +++ b/src/Psalm/Storage/ClassConstantStorage.php @@ -19,6 +19,7 @@ final class ClassConstantStorage /** @psalm-suppress MutableDependency Mutable by design */ use CustomMetadataTrait; use ImmutableNonCloneableTrait; + use UnserializeMemoryUsageSuppressionTrait; public ?CodeLocation $type_location; diff --git a/src/Psalm/Storage/ClassLikeStorage.php b/src/Psalm/Storage/ClassLikeStorage.php index f8564444a26..0ba397d2ada 100644 --- a/src/Psalm/Storage/ClassLikeStorage.php +++ b/src/Psalm/Storage/ClassLikeStorage.php @@ -19,6 +19,7 @@ final class ClassLikeStorage implements HasAttributesInterface { use CustomMetadataTrait; + use UnserializeMemoryUsageSuppressionTrait; /** * @var array diff --git a/src/Psalm/Storage/EnumCaseStorage.php b/src/Psalm/Storage/EnumCaseStorage.php index 24ec0791399..4c91419a69b 100644 --- a/src/Psalm/Storage/EnumCaseStorage.php +++ b/src/Psalm/Storage/EnumCaseStorage.php @@ -6,6 +6,8 @@ final class EnumCaseStorage { + use UnserializeMemoryUsageSuppressionTrait; + /** * @var int|string|null */ diff --git a/src/Psalm/Storage/FileStorage.php b/src/Psalm/Storage/FileStorage.php index fe818ff6d86..043cd196a1a 100644 --- a/src/Psalm/Storage/FileStorage.php +++ b/src/Psalm/Storage/FileStorage.php @@ -10,6 +10,7 @@ final class FileStorage { use CustomMetadataTrait; + use UnserializeMemoryUsageSuppressionTrait; /** * @var array diff --git a/src/Psalm/Storage/FunctionLikeParameter.php b/src/Psalm/Storage/FunctionLikeParameter.php index 6c4a241080d..39125c366bd 100644 --- a/src/Psalm/Storage/FunctionLikeParameter.php +++ b/src/Psalm/Storage/FunctionLikeParameter.php @@ -12,6 +12,7 @@ final class FunctionLikeParameter implements HasAttributesInterface, TypeNode { use CustomMetadataTrait; + use UnserializeMemoryUsageSuppressionTrait; /** * @var string diff --git a/src/Psalm/Storage/FunctionLikeStorage.php b/src/Psalm/Storage/FunctionLikeStorage.php index 7be91f80434..ee6128e787f 100644 --- a/src/Psalm/Storage/FunctionLikeStorage.php +++ b/src/Psalm/Storage/FunctionLikeStorage.php @@ -16,6 +16,7 @@ abstract class FunctionLikeStorage implements HasAttributesInterface { use CustomMetadataTrait; + use UnserializeMemoryUsageSuppressionTrait; /** * @var CodeLocation|null diff --git a/src/Psalm/Storage/Possibilities.php b/src/Psalm/Storage/Possibilities.php index 70eb35287b4..2c04ed415ed 100644 --- a/src/Psalm/Storage/Possibilities.php +++ b/src/Psalm/Storage/Possibilities.php @@ -12,6 +12,8 @@ final class Possibilities { + use UnserializeMemoryUsageSuppressionTrait; + /** * @var list the rule being asserted */ diff --git a/src/Psalm/Storage/PropertyStorage.php b/src/Psalm/Storage/PropertyStorage.php index 745e1e50ea3..c285c737ee8 100644 --- a/src/Psalm/Storage/PropertyStorage.php +++ b/src/Psalm/Storage/PropertyStorage.php @@ -9,6 +9,7 @@ final class PropertyStorage implements HasAttributesInterface { use CustomMetadataTrait; + use UnserializeMemoryUsageSuppressionTrait; /** * @var ?bool diff --git a/src/Psalm/Storage/UnserializeMemoryUsageSuppressionTrait.php b/src/Psalm/Storage/UnserializeMemoryUsageSuppressionTrait.php new file mode 100644 index 00000000000..a59b7ce2a02 --- /dev/null +++ b/src/Psalm/Storage/UnserializeMemoryUsageSuppressionTrait.php @@ -0,0 +1,24 @@ + $value) { + $this->$key = $value; + } + } +} diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index d54d1a6e3ce..46092ec8162 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -11,6 +11,7 @@ use Psalm\Internal\Type\TypeAlias; use Psalm\Internal\Type\TypeAlias\LinkableTypeAlias; use Psalm\Internal\TypeVisitor\ClasslikeReplacer; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TArrayKey; @@ -82,6 +83,8 @@ */ abstract class Atomic implements TypeNode { + use UnserializeMemoryUsageSuppressionTrait; + public function __construct(bool $from_docblock = false) { $this->from_docblock = $from_docblock; diff --git a/src/Psalm/Type/Union.php b/src/Psalm/Type/Union.php index 90a891917b1..c6602cd19a4 100644 --- a/src/Psalm/Type/Union.php +++ b/src/Psalm/Type/Union.php @@ -233,6 +233,52 @@ final class Union implements TypeNode */ public $different = false; + private const PROPERTY_KEYS_FOR_UNSERIALIZE = [ + "\0" . self::class . "\0" . 'types' => 'types', + 'from_docblock' => 'from_docblock', + 'from_calculation' => 'from_calculation', + 'from_property' => 'from_property', + 'from_static_property' => 'from_static_property', + 'initialized' => 'initialized', + 'initialized_class' => 'initialized_class', + 'checked' => 'checked', + 'failed_reconciliation' => 'failed_reconciliation', + 'ignore_nullable_issues' => 'ignore_nullable_issues', + 'ignore_falsable_issues' => 'ignore_falsable_issues', + 'ignore_isset' => 'ignore_isset', + 'possibly_undefined' => 'possibly_undefined', + 'possibly_undefined_from_try' => 'possibly_undefined_from_try', + 'explicit_never' => 'explicit_never', + 'had_template' => 'had_template', + 'from_template_default' => 'from_template_default', + "\0" . self::class . "\0" . 'literal_string_types' => 'literal_string_types', + "\0" . self::class . "\0" . 'typed_class_strings' => 'typed_class_strings', + "\0" . self::class . "\0" . 'literal_int_types' => 'literal_int_types', + "\0" . self::class . "\0" . 'literal_float_types' => 'literal_float_types', + 'by_ref' => 'by_ref', + 'reference_free' => 'reference_free', + 'allow_mutations' => 'allow_mutations', + 'has_mutations' => 'has_mutations', + "\0" . self::class . "\0" . 'id' => 'id', + "\0" . self::class . "\0" . 'exact_id' => 'exact_id', + 'parent_nodes' => 'parent_nodes', + 'propagate_parent_nodes' => 'propagate_parent_nodes', + 'different' => 'different', + ]; + + /** + * Suppresses memory usage when unserializing objects. + * + * @see \Psalm\Storage\UnserializeMemoryUsageSuppressionTrait + */ + public function __unserialize(array $properties): void + { + foreach (self::PROPERTY_KEYS_FOR_UNSERIALIZE as $key => $property_name) { + /** @psalm-suppress PossiblyUndefinedStringArrayOffset */ + $this->$property_name = $properties[$key]; + } + } + /** * @param TProperties $properties * @return static From c3a41d136d94a0b823e1c4c185c1bd4e0ada5650 Mon Sep 17 00:00:00 2001 From: Sam L Date: Tue, 9 Jan 2024 17:49:07 -0500 Subject: [PATCH 191/357] Bots From b2e53f6cf8d3696c051e826c8f4b898b79aebddd Mon Sep 17 00:00:00 2001 From: Karl Thaler Date: Tue, 9 Jan 2024 15:28:23 -0800 Subject: [PATCH 192/357] Implement __set method in SimpleXMLElement --- stubs/extensions/simplexml.phpstub | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stubs/extensions/simplexml.phpstub b/stubs/extensions/simplexml.phpstub index 7f0bfa2143f..4fc5e3046bc 100644 --- a/stubs/extensions/simplexml.phpstub +++ b/stubs/extensions/simplexml.phpstub @@ -66,6 +66,8 @@ class SimpleXMLElement implements Traversable, Countable public function count(): int {} public function __get(string $name): SimpleXMLElement|SimpleXMLIterator|null {} + + public function __set(string $name, $value): void {} } /** From 138b47a04f8481dbe268e5c069352c47b4643b92 Mon Sep 17 00:00:00 2001 From: Karl Thaler Date: Wed, 10 Jan 2024 13:07:32 -0800 Subject: [PATCH 193/357] Omit property assignment tests for SimpleXMLElement --- tests/PropertyTypeTest.php | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/PropertyTypeTest.php b/tests/PropertyTypeTest.php index 0fbd78699e3..69be1387253 100644 --- a/tests/PropertyTypeTest.php +++ b/tests/PropertyTypeTest.php @@ -3825,22 +3825,6 @@ class A { ', 'error_message' => 'UndefinedPropertyAssignment', ], - 'setPropertiesOfSimpleXMLElement1' => [ - 'code' => '"); - $a->b = "c"; - ', - 'error_message' => 'UndefinedPropertyAssignment', - ], - 'setPropertiesOfSimpleXMLElement2' => [ - 'code' => '"); - if (isset($a->b)) { - $a->b = "c"; - } - ', - 'error_message' => 'UndefinedPropertyAssignment', - ], ]; } } From f04be62e8b6896acdb2fd714230b6a37f31ff466 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 11 Jan 2024 18:40:45 +0100 Subject: [PATCH 194/357] can only be positive --- dictionaries/CallMap.php | 4 ++-- dictionaries/CallMap_historical.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index c4215cfc163..f3ea1336bae 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -3315,7 +3315,7 @@ 'getopt' => ['array>|false', 'short_options'=>'string', 'long_options='=>'array', '&w_rest_index='=>'int'], 'getprotobyname' => ['int|false', 'protocol'=>'string'], 'getprotobynumber' => ['string', 'protocol'=>'int'], -'getrandmax' => ['int'], +'getrandmax' => ['int<1, max>'], 'getrusage' => ['array', 'mode='=>'int'], 'getservbyname' => ['int|false', 'service'=>'string', 'protocol'=>'string'], 'getservbyport' => ['string|false', 'port'=>'int', 'protocol'=>'string'], @@ -7646,7 +7646,7 @@ 'msql_query' => ['resource', 'query'=>'string', 'link_identifier='=>'?resource'], 'msql_result' => ['string', 'result'=>'resource', 'row'=>'int', 'field='=>'mixed'], 'msql_select_db' => ['bool', 'database_name'=>'string', 'link_identifier='=>'?resource'], -'mt_getrandmax' => ['int'], +'mt_getrandmax' => ['int<1, max>'], 'mt_rand' => ['int', 'min'=>'int', 'max'=>'int'], 'mt_rand\'1' => ['int'], 'mt_srand' => ['void', 'seed='=>'?int', 'mode='=>'int'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 6e16b365027..bceffdfafcd 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -10679,7 +10679,7 @@ 'getopt' => ['array>|false', 'short_options'=>'string', 'long_options='=>'array'], 'getprotobyname' => ['int|false', 'protocol'=>'string'], 'getprotobynumber' => ['string', 'protocol'=>'int'], - 'getrandmax' => ['int'], + 'getrandmax' => ['int<1, max>'], 'getrusage' => ['array', 'mode='=>'int'], 'getservbyname' => ['int|false', 'service'=>'string', 'protocol'=>'string'], 'getservbyport' => ['string|false', 'port'=>'int', 'protocol'=>'string'], @@ -12488,7 +12488,7 @@ 'msql_query' => ['resource', 'query'=>'string', 'link_identifier='=>'?resource'], 'msql_result' => ['string', 'result'=>'resource', 'row'=>'int', 'field='=>'mixed'], 'msql_select_db' => ['bool', 'database_name'=>'string', 'link_identifier='=>'?resource'], - 'mt_getrandmax' => ['int'], + 'mt_getrandmax' => ['int<1, max>'], 'mt_rand' => ['int', 'min'=>'int', 'max'=>'int'], 'mt_rand\'1' => ['int'], 'mt_srand' => ['void', 'seed='=>'int', 'mode='=>'int'], From 7658fc4d331f89f070a3205a280af470dc4e2052 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 11 Jan 2024 18:43:15 +0100 Subject: [PATCH 195/357] unserialize change to class-string --- dictionaries/CallMap.php | 2 +- dictionaries/CallMap_historical.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index f3ea1336bae..783f520e962 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -14165,7 +14165,7 @@ 'unlink' => ['bool', 'filename'=>'string', 'context='=>'resource'], 'unpack' => ['array|false', 'format'=>'string', 'string'=>'string', 'offset='=>'int'], 'unregister_tick_function' => ['void', 'callback'=>'callable'], -'unserialize' => ['mixed', 'data'=>'string', 'options='=>'array{allowed_classes?:string[]|bool}'], +'unserialize' => ['mixed', 'data'=>'string', 'options='=>'array{allowed_classes?:class-string[]|bool}'], 'unset' => ['void', 'var='=>'mixed', '...args='=>'mixed'], 'untaint' => ['bool', '&rw_string'=>'string', '&...rw_strings='=>'string'], 'uopz_allow_exit' => ['void', 'allow'=>'bool'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index bceffdfafcd..61e3db09d55 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -15197,7 +15197,7 @@ 'unlink' => ['bool', 'filename'=>'string', 'context='=>'resource'], 'unpack' => ['array', 'format'=>'string', 'string'=>'string'], 'unregister_tick_function' => ['void', 'callback'=>'callable'], - 'unserialize' => ['mixed', 'data'=>'string', 'options='=>'array{allowed_classes?:string[]|bool}'], + 'unserialize' => ['mixed', 'data'=>'string', 'options='=>'array{allowed_classes?:class-string[]|bool}'], 'unset' => ['void', 'var='=>'mixed', '...args='=>'mixed'], 'untaint' => ['bool', '&rw_string'=>'string', '&...rw_strings='=>'string'], 'uopz_allow_exit' => ['void', 'allow'=>'bool'], From d209bab9f5d8eadd0cba49a2635e209354fd466b Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sat, 13 Jan 2024 00:57:53 +0100 Subject: [PATCH 196/357] Fix mb_get_info can return null - CI failing bc of reflection See https://github.com/php/php-src/issues/12753 --- dictionaries/CallMap.php | 2 +- dictionaries/CallMap_82_delta.php | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 783f520e962..cec0545f126 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -6625,7 +6625,7 @@ 'mb_ereg_search_setpos' => ['bool', 'offset'=>'int'], 'mb_eregi' => ['bool', 'pattern'=>'string', 'string'=>'string', '&w_matches='=>'array|null'], 'mb_eregi_replace' => ['string|false|null', 'pattern'=>'string', 'replacement'=>'string', 'string'=>'string', 'options='=>'string|null'], -'mb_get_info' => ['array|string|int|false', 'type='=>'string'], +'mb_get_info' => ['array|string|int|false|null', 'type='=>'string'], 'mb_http_input' => ['array|string|false', 'type='=>'string|null'], 'mb_http_output' => ['string|bool', 'encoding='=>'string|null'], 'mb_internal_encoding' => ['string|bool', 'encoding='=>'string|null'], diff --git a/dictionaries/CallMap_82_delta.php b/dictionaries/CallMap_82_delta.php index 47ded4fbb19..3064f54ff52 100644 --- a/dictionaries/CallMap_82_delta.php +++ b/dictionaries/CallMap_82_delta.php @@ -53,6 +53,10 @@ 'old' => ['non-empty-list', 'string'=>'string', 'length='=>'positive-int'], 'new' => ['list', 'string'=>'string', 'length='=>'positive-int'], ], + 'mb_get_info' => [ + 'old' => ['array|string|int|false', 'type='=>'string'], + 'new' => ['array|string|int|false|null', 'type='=>'string'], + ], ], 'removed' => [ From 6f184dca916ec156addc5e997f3e915dee87004d Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Fri, 12 Jan 2024 20:46:04 +0100 Subject: [PATCH 197/357] remove redundat directory separator which caused "//" in path not found errors --- src/Psalm/Config.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 82c696c0bcd..e6f05540b55 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -787,7 +787,7 @@ public static function loadFromXMLFile(string $file_path, string $current_dir): { $file_contents = file_get_contents($file_path); - $base_dir = dirname($file_path) . DIRECTORY_SEPARATOR; + $base_dir = dirname($file_path); if ($file_contents === false) { throw new InvalidArgumentException('Cannot open ' . $file_path); @@ -1304,7 +1304,7 @@ private static function fromXmlAndPaths( // ProjectAnalyzer::getInstance()->check_paths_files is not populated at this point in time $paths_to_check = null; - + global $argv; // Hack for Symfonys own argv resolution. @@ -1312,7 +1312,7 @@ private static function fromXmlAndPaths( if (!isset($argv[0]) || basename($argv[0]) !== 'psalm-plugin') { $paths_to_check = CliUtils::getPathsToCheck(null); } - + if ($paths_to_check !== null) { $paths_to_add_to_project_files = array(); foreach ($paths_to_check as $path) { @@ -1473,7 +1473,7 @@ private static function fromXmlAndPaths( $path = Path::isAbsolute($plugin_file_name) ? $plugin_file_name - : $config->base_dir . $plugin_file_name; + : $config->base_dir . DIRECTORY_SEPARATOR . $plugin_file_name; $config->addPluginPath($path); } From a2bf5cb0546661bd8b058132b738a8798fc68959 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sun, 17 Dec 2023 20:31:32 +0100 Subject: [PATCH 198/357] Fix empty literal string becomes non-empty-string when max literal string length is 0, which means literal strings are disabled --- src/Psalm/Type.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 1215799f785..d05a9b9d5c3 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -287,7 +287,7 @@ public static function getAtomicStringFromLiteral(string $value, bool $from_docb $type = $config->eventDispatcher->dispatchStringInterpreter($event); if (!$type) { - if (strlen($value) < $config->max_string_length) { + if ($value === '' || strlen($value) < $config->max_string_length) { $type = new TLiteralString($value, $from_docblock); } else { $type = new TNonEmptyString($from_docblock); From 40926182604e32a97e0fd342c2ff6704b5c7dac8 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 20 Dec 2023 09:11:59 +0100 Subject: [PATCH 199/357] should be non-falsy-string instead of non-empty-string in most cases --- src/Psalm/Type.php | 6 ++++-- tests/TypeCombinationTest.php | 7 +++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index d05a9b9d5c3..cc5de1f8b03 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -277,7 +277,7 @@ public static function getString(?string $value = null): Union return new Union([$value === null ? new TString() : self::getAtomicStringFromLiteral($value)]); } - /** @return TLiteralString|TNonEmptyString */ + /** @return TLiteralString|TNonEmptyString|TNonFalsyString */ public static function getAtomicStringFromLiteral(string $value, bool $from_docblock = false): TString { $config = Config::getInstance(); @@ -289,8 +289,10 @@ public static function getAtomicStringFromLiteral(string $value, bool $from_docb if (!$type) { if ($value === '' || strlen($value) < $config->max_string_length) { $type = new TLiteralString($value, $from_docblock); - } else { + } elseif ($value === '0') { $type = new TNonEmptyString($from_docblock); + } else { + $type = new TNonFalsyString($from_docblock); } } diff --git a/tests/TypeCombinationTest.php b/tests/TypeCombinationTest.php index b891e84dcc2..f041453d9d5 100644 --- a/tests/TypeCombinationTest.php +++ b/tests/TypeCombinationTest.php @@ -120,6 +120,13 @@ function takesLiteralString($arg) {} takesLiteralString($c); }', ], + 'tooLongLiteralShouldBeNonFalsyString' => [ + 'code' => ' [ + '$x===' => 'non-falsy-string', + ] + ], ]; } From 7f0f0bc36173909a333324cd1a4ce0f63e81ab75 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 20 Dec 2023 09:22:49 +0100 Subject: [PATCH 200/357] ensure concat https://psalm.dev/r/323e33ae8a will be a non-falsy-string --- .../Expression/BinaryOp/ConcatAnalyzer.php | 18 ++++++++++++++++-- tests/BinaryOperationTest.php | 9 +++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php index fd7ab531f3e..6a59185b55f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php @@ -258,6 +258,19 @@ public static function analyze( $has_numeric_and_non_empty = $has_numeric_type && $has_non_empty; + $non_falsy_string = $numeric_type->getBuilder()->addType(new TNonFalsyString())->freeze(); + $left_non_falsy = UnionTypeComparator::isContainedBy( + $codebase, + $left_type, + $non_falsy_string, + ); + + $right_non_falsy = UnionTypeComparator::isContainedBy( + $codebase, + $right_type, + $non_falsy_string, + ); + $all_literals = $left_type->allLiterals() && $right_type->allLiterals(); if ($has_non_empty) { @@ -265,9 +278,10 @@ public static function analyze( $result_type = new Union([new TNonEmptyNonspecificLiteralString]); } elseif ($all_lowercase) { $result_type = Type::getNonEmptyLowercaseString(); + } elseif ($all_non_empty || $has_numeric_and_non_empty || $left_non_falsy || $right_non_falsy) { + $result_type = Type::getNonFalsyString(); } else { - $result_type = $all_non_empty || $has_numeric_and_non_empty ? - Type::getNonFalsyString() : Type::getNonEmptyString(); + $result_type = Type::getNonEmptyString(); } } else { if ($all_literals) { diff --git a/tests/BinaryOperationTest.php b/tests/BinaryOperationTest.php index f0fe0c9e3ae..e2dafc18724 100644 --- a/tests/BinaryOperationTest.php +++ b/tests/BinaryOperationTest.php @@ -357,6 +357,15 @@ public function providerValidCodeParse(): iterable 'code' => ' [ + 'code' => ' [ + '$a===' => 'non-falsy-string', + ], + 'ignored_issues' => ['InvalidReturnType'], + ], 'concatenationWithNumberInWeakMode' => [ 'code' => ' Date: Sat, 13 Jan 2024 17:12:41 +0100 Subject: [PATCH 201/357] code style --- tests/TypeCombinationTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TypeCombinationTest.php b/tests/TypeCombinationTest.php index f041453d9d5..56af82a69ed 100644 --- a/tests/TypeCombinationTest.php +++ b/tests/TypeCombinationTest.php @@ -125,7 +125,7 @@ function takesLiteralString($arg) {} $x = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";', 'assertions' => [ '$x===' => 'non-falsy-string', - ] + ], ], ]; } From 02467fbb6a484c9e8b8bb08ac317c7e8a1c20947 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sun, 14 Jan 2024 10:42:05 +0100 Subject: [PATCH 202/357] add support for extract to set variables for keyed arrays and respect EXTR_SKIP --- .../Call/NamedFunctionCallHandler.php | 93 +++++++++++++++++++ tests/FunctionCallTest.php | 40 +++++++- 2 files changed, 129 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php index 514df1a55a5..5f1900c2b78 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php @@ -35,6 +35,8 @@ use Psalm\Type\Atomic\TDependentGetType; use Psalm\Type\Atomic\TFloat; use Psalm\Type\Atomic\TInt; +use Psalm\Type\Atomic\TKeyedArray; +use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TLowercaseString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; @@ -47,12 +49,18 @@ use Psalm\Type\Union; use function array_map; +use function count; use function extension_loaded; use function in_array; +use function is_numeric; use function is_string; +use function preg_match; use function strpos; use function strtolower; +use const EXTR_OVERWRITE; +use const EXTR_SKIP; + /** * @internal */ @@ -261,13 +269,98 @@ public static function handle( } if ($function_id === 'extract') { + $flag_value = false; + if (!isset($stmt->args[1])) { + $flag_value = EXTR_OVERWRITE; + } elseif (isset($stmt->args[1]->value) + && $stmt->args[1]->value instanceof PhpParser\Node\Expr + && ($flags_type = $statements_analyzer->node_data->getType($stmt->args[1]->value)) + && $flags_type->hasLiteralInt() && count($flags_type->getAtomicTypes()) === 1) { + $flag_type_value = $flags_type->getSingleIntLiteral()->value; + if ($flag_type_value === EXTR_SKIP) { + $flag_value = EXTR_SKIP; + } elseif ($flag_type_value === EXTR_OVERWRITE) { + $flag_value = EXTR_OVERWRITE; + } + // @todo add support for other flags + } + + $is_unsealed = true; + $validated_var_ids = []; + if ($flag_value !== false && isset($stmt->args[0]->value) + && $stmt->args[0]->value instanceof PhpParser\Node\Expr + && ($array_type_union = $statements_analyzer->node_data->getType($stmt->args[0]->value)) + && $array_type_union->isSingle() + ) { + foreach ($array_type_union->getAtomicTypes() as $array_type) { + if ($array_type instanceof TList) { + $array_type = $array_type->getKeyedArray(); + } + + if ($array_type instanceof TKeyedArray) { + foreach ($array_type->properties as $key => $type) { + // variables must start with letters or underscore + if ($key === '' || is_numeric($key) || preg_match('/^[A-Za-z_]/', $key) !== 1) { + continue; + } + + $var_id = '$' . $key; + $validated_var_ids[] = $var_id; + + if (isset($context->vars_in_scope[$var_id]) && $flag_value === EXTR_SKIP) { + continue; + } + + if (!isset($context->vars_in_scope[$var_id]) && $type->possibly_undefined === true) { + $context->possibly_assigned_var_ids[$var_id] = true; + } elseif (isset($context->vars_in_scope[$var_id]) + && $type->possibly_undefined === true + && $flag_value === EXTR_OVERWRITE) { + $type = Type::combineUnionTypes( + $context->vars_in_scope[$var_id], + $type, + $codebase, + false, + true, + 500, + false, + ); + } + + $context->vars_in_scope[$var_id] = $type; + $context->assigned_var_ids[$var_id] = (int) $stmt->getAttribute('startFilePos'); + } + + if (!isset($array_type->fallback_params)) { + $is_unsealed = false; + } + } + } + } + + if ($flag_value === EXTR_OVERWRITE && $is_unsealed === false) { + return; + } + + if ($flag_value === EXTR_SKIP && $is_unsealed === false) { + return; + } + $context->check_variables = false; + if ($flag_value === EXTR_SKIP) { + return; + } + foreach ($context->vars_in_scope as $var_id => $_) { if ($var_id === '$this' || strpos($var_id, '[') || strpos($var_id, '>')) { continue; } + if (in_array($var_id, $validated_var_ids, true)) { + continue; + } + $mixed_type = new Union([new TMixed()], [ 'parent_nodes' => $context->vars_in_scope[$var_id]->parent_nodes, ]); diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 93297a79ddd..e63636dff43 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -513,19 +513,41 @@ function foo($s): array { ], 'extractVarCheck' => [ 'code' => ' "bar"]; + $foo = "foo"; + $a = getUnsealedArray(); extract($a); takesString($foo);', 'assertions' => [], 'ignored_issues' => [ - 'MixedAssignment', - 'MixedArrayAccess', 'MixedArgument', ], ], + 'extractVarCheckValid' => [ + 'code' => ' 15]; + extract($a); + takesInt($foo);', + ], + 'extractSkipExtr' => [ + 'code' => ' "x", "b" => "y"], EXTR_SKIP);', + 'assertions' => [ + '$a===' => '1', + '$b===' => '\'y\'', + ], + ], 'compact' => [ 'code' => ' [], 'php_version' => '8.1', ], + 'extractVarCheckInvalid' => [ + 'code' => ' 15]; + extract($a); + takesInt($foo);', + 'error_message' => 'InvalidScalarArgument', + ], ]; } From f940c029e17d1f2165d7a03877efbc53b2f380c9 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sun, 14 Jan 2024 19:00:09 +0100 Subject: [PATCH 203/357] make basename & dirname return types more specific --- .../BasenameReturnTypeProvider.php | 43 +++++++++++++++- .../DirnameReturnTypeProvider.php | 50 ++++++++++++++----- tests/ReturnTypeProvider/BasenameTest.php | 20 ++++++++ tests/ReturnTypeProvider/DirnameTest.php | 22 +++++++- 4 files changed, 120 insertions(+), 15 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/BasenameReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/BasenameReturnTypeProvider.php index 8e11092326e..487f1dcffb4 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/BasenameReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/BasenameReturnTypeProvider.php @@ -44,7 +44,48 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev ); if ($evaled_path === null) { - return Type::getString(); + $union = $statements_source->getNodeTypeProvider()->getType($call_args[0]->value); + $generic = false; + $non_empty = false; + if ($union !== null) { + foreach ($union->getAtomicTypes() as $atomic) { + if ($atomic instanceof Type\Atomic\TNonFalsyString) { + continue; + } + + if ($atomic instanceof Type\Atomic\TLiteralString) { + if ($atomic->value === '') { + $generic = true; + break; + } + + if ($atomic->value === '0') { + $non_empty = true; + continue; + } + + continue; + } + + if ($atomic instanceof Type\Atomic\TNonEmptyString) { + $non_empty = true; + continue; + } + + $generic = true; + break; + } + } + + if ($union === null || $generic) { + return Type::getString(); + } + + if ($non_empty) { + return Type::getNonEmptyString(); + } + + return Type::getNonFalsyString(); } $basename = basename($evaled_path); diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/DirnameReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/DirnameReturnTypeProvider.php index f6c63c47ac4..ff4fbc54fed 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/DirnameReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/DirnameReturnTypeProvider.php @@ -9,7 +9,6 @@ use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface; use Psalm\Type; use Psalm\Type\Atomic\TLiteralInt; -use Psalm\Type\Atomic\TNonEmptyString; use Psalm\Type\Union; use function array_values; @@ -39,6 +38,41 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $statements_source = $event->getStatementsSource(); $node_type_provider = $statements_source->getNodeTypeProvider(); + $union = $node_type_provider->getType($call_args[0]->value); + $generic = false; + if ($union !== null) { + foreach ($union->getAtomicTypes() as $atomic) { + if ($atomic instanceof Type\Atomic\TNonFalsyString) { + continue; + } + + if ($atomic instanceof Type\Atomic\TLiteralString) { + if ($atomic->value === '') { + $generic = true; + break; + } + + // 0 will be non-falsy too (.) + continue; + } + + if ($atomic instanceof Type\Atomic\TNonEmptyString + || $atomic instanceof Type\Atomic\TEmptyNumeric) { + continue; + } + + // generic string is the only other possible case of empty string + // which would result in a generic string + $generic = true; + break; + } + } + + $fallback_type = Type::getNonFalsyString(); + if ($union === null || $generic) { + $fallback_type = Type::getString(); + } + $dir_level = 1; if (isset($call_args[1])) { $type = $node_type_provider->getType($call_args[1]->value); @@ -49,7 +83,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $atomic_type->value > 0) { $dir_level = $atomic_type->value; } else { - return Type::getString(); + return $fallback_type; } } } @@ -63,17 +97,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev ); if ($evaled_path === null) { - $type = $node_type_provider->getType($call_args[0]->value); - if ($type !== null && $type->isSingle()) { - $atomic_type = array_values($type->getAtomicTypes())[0]; - if ($atomic_type instanceof TNonEmptyString) { - return Type::getNonEmptyString(); - } - } - } - - if ($evaled_path === null) { - return Type::getString(); + return $fallback_type; } $path_to_file = dirname($evaled_path, $dir_level); diff --git a/tests/ReturnTypeProvider/BasenameTest.php b/tests/ReturnTypeProvider/BasenameTest.php index 5c624651f69..89ab1562581 100644 --- a/tests/ReturnTypeProvider/BasenameTest.php +++ b/tests/ReturnTypeProvider/BasenameTest.php @@ -32,5 +32,25 @@ public function providerValidCodeParse(): iterable '$base===' => 'string', ], ]; + + yield 'basenameOfStringPathReturnsNonEmptyString' => [ + 'code' => ' [ + '$base===' => 'non-empty-string', + ], + ]; + + yield 'basenameOfStringPathReturnsNonFalsyString' => [ + 'code' => ' [ + '$base===' => 'non-falsy-string', + ], + ]; } } diff --git a/tests/ReturnTypeProvider/DirnameTest.php b/tests/ReturnTypeProvider/DirnameTest.php index 48dea4d2cca..98d7b40f722 100644 --- a/tests/ReturnTypeProvider/DirnameTest.php +++ b/tests/ReturnTypeProvider/DirnameTest.php @@ -49,7 +49,27 @@ public function providerValidCodeParse(): iterable $dir = dirname(uniqid() . "abc", 2); ', 'assertions' => [ - '$dir===' => 'non-empty-string', + '$dir===' => 'non-falsy-string', + ], + ]; + + yield 'dirnameOfNonEmptyShouldBeNonFalsy' => [ + 'code' => ' [ + '$dir===' => 'non-falsy-string', + ], + ]; + + yield 'dirnameOfEmptyShouldBeString' => [ + 'code' => ' [ + '$dir===' => 'string', ], ]; } From d5b713e4391e6339bd0c69f664ae15c3ccd96e01 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Mon, 18 Dec 2023 14:51:53 +0100 Subject: [PATCH 204/357] Fix https://github.com/vimeo/psalm/issues/10501 - report error for non-strict comparison on truthy+falsy union --- .../Block/IfConditionalAnalyzer.php | 33 +++++++ .../Expression/BooleanNotAnalyzer.php | 35 ++++++++ tests/TypeReconciliation/ConditionalTest.php | 86 +++++++++++++++++++ 3 files changed, 154 insertions(+) diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfConditionalAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfConditionalAnalyzer.php index a306cca4ca0..8db97e794a3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/IfConditionalAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfConditionalAnalyzer.php @@ -17,6 +17,7 @@ use Psalm\Issue\RedundantConditionGivenDocblockType; use Psalm\Issue\TypeDoesNotContainType; use Psalm\IssueBuffer; +use Psalm\Type\Atomic\TBool; use Psalm\Type\Reconciler; use function array_diff_key; @@ -366,6 +367,38 @@ public static function handleParadoxicalCondition( $statements_analyzer->getSuppressedIssues(), ); } + } elseif (!($stmt instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical) + && !($stmt instanceof PhpParser\Node\Expr\BinaryOp\Identical) + && !($stmt instanceof PhpParser\Node\Expr\BooleanNot)) { + $has_both = false; + $both_types = $type->getBuilder(); + if (count($type->getAtomicTypes()) > 1) { + foreach ($both_types->getAtomicTypes() as $key => $atomic_type) { + if ($atomic_type->isTruthy() + || $atomic_type->isFalsy() + || $atomic_type instanceof TBool) { + $both_types->removeType($key); + continue; + } + + $has_both = true; + } + } + + if ($has_both) { + $both_types = $both_types->freeze(); + IssueBuffer::maybeAdd( + new TypeDoesNotContainType( + 'Operand of type ' . $type->getId() . ' contains ' . + 'type' . (count($both_types->getAtomicTypes()) > 1 ? 's' : '') . ' ' . + $both_types->getId() . ', which can be falsy and truthy. ' . + 'This can cause possibly unexpected behavior. Use strict comparison instead.', + new CodeLocation($statements_analyzer, $stmt), + $type->getId() . ' truthy-falsy', + ), + $statements_analyzer->getSuppressedIssues(), + ); + } } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php index 3c75dd9efca..bfe8d209e02 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php @@ -3,15 +3,20 @@ namespace Psalm\Internal\Analyzer\Statements\Expression; use PhpParser; +use Psalm\CodeLocation; use Psalm\Context; use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Issue\TypeDoesNotContainType; +use Psalm\IssueBuffer; use Psalm\Type; use Psalm\Type\Atomic\TBool; use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TTrue; use Psalm\Type\Union; +use function count; + /** * @internal */ @@ -40,6 +45,36 @@ public static function analyze( } elseif ($expr_type->isAlwaysFalsy()) { $stmt_type = new TTrue($expr_type->from_docblock); } else { + $has_both = false; + $both_types = $expr_type->getBuilder(); + if (count($expr_type->getAtomicTypes()) > 1) { + foreach ($both_types->getAtomicTypes() as $key => $atomic_type) { + if ($atomic_type->isTruthy() + || $atomic_type->isFalsy() + || $atomic_type instanceof TBool) { + $both_types->removeType($key); + continue; + } + + $has_both = true; + } + } + + if ($has_both) { + $both_types = $both_types->freeze(); + IssueBuffer::maybeAdd( + new TypeDoesNotContainType( + 'Operand of type ' . $expr_type->getId() . ' contains ' . + 'type' . (count($both_types->getAtomicTypes()) > 1 ? 's' : '') . ' ' . + $both_types->getId() . ', which can be falsy and truthy. ' . + 'This can cause possibly unexpected behavior. Use strict comparison instead.', + new CodeLocation($statements_analyzer, $stmt), + $expr_type->getId() . ' truthy-falsy', + ), + $statements_analyzer->getSuppressedIssues(), + ); + } + $stmt_type = new TBool(); } diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index 700ba17c621..bb16a20043e 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -38,6 +38,32 @@ function foo($a): void { if ($b === $a) { } }', ], + 'nonStrictConditionTruthyFalsyNoOverlap' => [ + 'code' => ' [ 'code' => ' 'TypeDoesNotContainType', ], + 'nonStrictConditionTruthyFalsy' => [ + 'code' => ' 'TypeDoesNotContainType', + ], + 'nonStrictConditionTruthyFalsyNegated' => [ + 'code' => ' 'TypeDoesNotContainType', + ], + 'nonStrictConditionTruthyFalsyFuncCall' => [ + 'code' => ' 'TypeDoesNotContainType', + ], + 'nonStrictConditionTruthyFalsyFuncCallNegated' => [ + 'code' => ' 'TypeDoesNotContainType', + ], 'redundantConditionForNonEmptyString' => [ 'code' => ' Date: Fri, 12 Jan 2024 22:40:47 +0100 Subject: [PATCH 205/357] add the fix for empty() too and fix empty returning bool on true/false only cases hiding errors when functions called --- .../Statements/Expression/EmptyAnalyzer.php | 82 +++++++++++++++---- tests/TypeReconciliation/EmptyTest.php | 32 ++++++++ 2 files changed, 100 insertions(+), 14 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php index 3b9014f85b4..40bf489ea5e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php @@ -8,8 +8,15 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Issue\ForbiddenCode; use Psalm\Issue\InvalidArgument; +use Psalm\Issue\TypeDoesNotContainType; use Psalm\IssueBuffer; use Psalm\Type; +use Psalm\Type\Atomic\TBool; +use Psalm\Type\Atomic\TFalse; +use Psalm\Type\Atomic\TTrue; +use Psalm\Type\Union; + +use function count; /** * @internal @@ -35,21 +42,68 @@ public static function analyze( ); } - if (($stmt_expr_type = $statements_analyzer->node_data->getType($stmt->expr)) - && $stmt_expr_type->hasBool() - && $stmt_expr_type->isSingle() - && !$stmt_expr_type->from_docblock - ) { - IssueBuffer::maybeAdd( - new InvalidArgument( - 'Calling empty on a boolean value is almost certainly unintended', - new CodeLocation($statements_analyzer->getSource(), $stmt->expr), - 'empty', - ), - $statements_analyzer->getSuppressedIssues(), - ); + $expr_type = $statements_analyzer->node_data->getType($stmt->expr); + + if ($expr_type) { + if ($expr_type->hasBool() + && $expr_type->isSingle() + && !$expr_type->from_docblock + ) { + IssueBuffer::maybeAdd( + new InvalidArgument( + 'Calling empty on a boolean value is almost certainly unintended', + new CodeLocation($statements_analyzer->getSource(), $stmt->expr), + 'empty', + ), + $statements_analyzer->getSuppressedIssues(), + ); + } + + if ($expr_type->isAlwaysTruthy() && $expr_type->possibly_undefined === false) { + $stmt_type = new TFalse($expr_type->from_docblock); + } elseif ($expr_type->isAlwaysFalsy()) { + $stmt_type = new TTrue($expr_type->from_docblock); + } else { + $has_both = false; + $both_types = $expr_type->getBuilder(); + if (count($expr_type->getAtomicTypes()) > 1) { + foreach ($both_types->getAtomicTypes() as $key => $atomic_type) { + if ($atomic_type->isTruthy() + || $atomic_type->isFalsy() + || $atomic_type instanceof TBool) { + $both_types->removeType($key); + continue; + } + + $has_both = true; + } + } + + if ($has_both) { + $both_types = $both_types->freeze(); + IssueBuffer::maybeAdd( + new TypeDoesNotContainType( + 'Operand of type ' . $expr_type->getId() . ' contains ' . + 'type' . (count($both_types->getAtomicTypes()) > 1 ? 's' : '') . ' ' . + $both_types->getId() . ', which can be falsy and truthy. ' . + 'This can cause possibly unexpected behavior. Use strict comparison instead.', + new CodeLocation($statements_analyzer, $stmt), + $expr_type->getId() . ' truthy-falsy', + ), + $statements_analyzer->getSuppressedIssues(), + ); + } + + $stmt_type = new TBool(); + } + + $stmt_type = new Union([$stmt_type], [ + 'parent_nodes' => $expr_type->parent_nodes, + ]); + } else { + $stmt_type = Type::getBool(); } - $statements_analyzer->node_data->setType($stmt, Type::getBool()); + $statements_analyzer->node_data->setType($stmt, $stmt_type); } } diff --git a/tests/TypeReconciliation/EmptyTest.php b/tests/TypeReconciliation/EmptyTest.php index c7991804454..944ae2218b6 100644 --- a/tests/TypeReconciliation/EmptyTest.php +++ b/tests/TypeReconciliation/EmptyTest.php @@ -612,6 +612,14 @@ function test(string $s): void { '$GLOBALS[\'sql_query\']===' => 'string', ], ], + 'emptyLiteralTrueFalse' => [ + 'code' => ' [ + '$x===' => 'true', + ], + ], ]; } @@ -720,6 +728,30 @@ function nonEmptyString(string $str): string { }', 'error_message' => 'LessSpecificReturnStatement', ], + 'impossibleEmptyOnFalsyFunctionCall' => [ + 'code' => ' 'DocblockTypeContradiction', + ], + 'redundantEmptyOnFalsyFunctionCall' => [ + 'code' => ' 'RedundantConditionGivenDocblockType', + ], ]; } } From fb93aede1279db1a34c6477a1d5aa56a99bcbfa4 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Fri, 12 Jan 2024 22:52:26 +0100 Subject: [PATCH 206/357] create a separate issue type --- config.xsd | 1 + docs/running_psalm/error_levels.md | 1 + docs/running_psalm/issues.md | 1 + .../issues/RiskyTruthyFalsyComparison.md | 29 +++++++++++++++++++ .../Block/IfConditionalAnalyzer.php | 5 ++-- .../Expression/BooleanNotAnalyzer.php | 6 ++-- .../Statements/Expression/EmptyAnalyzer.php | 6 ++-- .../Issue/RiskyTruthyFalsyComparison.php | 17 +++++++++++ tests/TypeReconciliation/ConditionalTest.php | 8 ++--- 9 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 docs/running_psalm/issues/RiskyTruthyFalsyComparison.md create mode 100644 src/Psalm/Issue/RiskyTruthyFalsyComparison.php diff --git a/config.xsd b/config.xsd index 5c176821e24..0f3e88916c8 100644 --- a/config.xsd +++ b/config.xsd @@ -427,6 +427,7 @@ + diff --git a/docs/running_psalm/error_levels.md b/docs/running_psalm/error_levels.md index 55a18b8fa61..df53f5227f6 100644 --- a/docs/running_psalm/error_levels.md +++ b/docs/running_psalm/error_levels.md @@ -173,6 +173,7 @@ Level 5 and above allows a more non-verifiable code, and higher levels are even - [TooManyArguments](issues/TooManyArguments.md) - [TypeDoesNotContainNull](issues/TypeDoesNotContainNull.md) - [TypeDoesNotContainType](issues/TypeDoesNotContainType.md) +- [RiskyTruthyFalsyComparison](issues/RiskyTruthyFalsyComparison.md) - [UndefinedMagicMethod](issues/UndefinedMagicMethod.md) - [UndefinedMagicPropertyAssignment](issues/UndefinedMagicPropertyAssignment.md) - [UndefinedMagicPropertyFetch](issues/UndefinedMagicPropertyFetch.md) diff --git a/docs/running_psalm/issues.md b/docs/running_psalm/issues.md index ac8135c7142..179f9bf7b53 100644 --- a/docs/running_psalm/issues.md +++ b/docs/running_psalm/issues.md @@ -229,6 +229,7 @@ - [ReferenceReusedFromConfusingScope](issues/ReferenceReusedFromConfusingScope.md) - [ReservedWord](issues/ReservedWord.md) - [RiskyCast](issues/RiskyCast.md) + - [RiskyTruthyFalsyComparison](issues/RiskyTruthyFalsyComparison.md) - [StringIncrement](issues/StringIncrement.md) - [TaintedCallable](issues/TaintedCallable.md) - [TaintedCookie](issues/TaintedCookie.md) diff --git a/docs/running_psalm/issues/RiskyTruthyFalsyComparison.md b/docs/running_psalm/issues/RiskyTruthyFalsyComparison.md new file mode 100644 index 00000000000..8d60969633e --- /dev/null +++ b/docs/running_psalm/issues/RiskyTruthyFalsyComparison.md @@ -0,0 +1,29 @@ +# RiskyTruthyFalsyComparison + +Emitted when comparing a value with multiple types that can both contain truthy and falsy values. + +```php +freeze(); IssueBuffer::maybeAdd( - new TypeDoesNotContainType( + new RiskyTruthyFalsyComparison( 'Operand of type ' . $type->getId() . ' contains ' . 'type' . (count($both_types->getAtomicTypes()) > 1 ? 's' : '') . ' ' . $both_types->getId() . ', which can be falsy and truthy. ' . 'This can cause possibly unexpected behavior. Use strict comparison instead.', new CodeLocation($statements_analyzer, $stmt), - $type->getId() . ' truthy-falsy', + $type->getId(), ), $statements_analyzer->getSuppressedIssues(), ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php index bfe8d209e02..71f6e19324f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php @@ -7,7 +7,7 @@ use Psalm\Context; use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; -use Psalm\Issue\TypeDoesNotContainType; +use Psalm\Issue\RiskyTruthyFalsyComparison; use Psalm\IssueBuffer; use Psalm\Type; use Psalm\Type\Atomic\TBool; @@ -63,13 +63,13 @@ public static function analyze( if ($has_both) { $both_types = $both_types->freeze(); IssueBuffer::maybeAdd( - new TypeDoesNotContainType( + new RiskyTruthyFalsyComparison( 'Operand of type ' . $expr_type->getId() . ' contains ' . 'type' . (count($both_types->getAtomicTypes()) > 1 ? 's' : '') . ' ' . $both_types->getId() . ', which can be falsy and truthy. ' . 'This can cause possibly unexpected behavior. Use strict comparison instead.', new CodeLocation($statements_analyzer, $stmt), - $expr_type->getId() . ' truthy-falsy', + $expr_type->getId(), ), $statements_analyzer->getSuppressedIssues(), ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php index 40bf489ea5e..02fae12fd44 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php @@ -8,7 +8,7 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Issue\ForbiddenCode; use Psalm\Issue\InvalidArgument; -use Psalm\Issue\TypeDoesNotContainType; +use Psalm\Issue\RiskyTruthyFalsyComparison; use Psalm\IssueBuffer; use Psalm\Type; use Psalm\Type\Atomic\TBool; @@ -82,13 +82,13 @@ public static function analyze( if ($has_both) { $both_types = $both_types->freeze(); IssueBuffer::maybeAdd( - new TypeDoesNotContainType( + new RiskyTruthyFalsyComparison( 'Operand of type ' . $expr_type->getId() . ' contains ' . 'type' . (count($both_types->getAtomicTypes()) > 1 ? 's' : '') . ' ' . $both_types->getId() . ', which can be falsy and truthy. ' . 'This can cause possibly unexpected behavior. Use strict comparison instead.', new CodeLocation($statements_analyzer, $stmt), - $expr_type->getId() . ' truthy-falsy', + $expr_type->getId(), ), $statements_analyzer->getSuppressedIssues(), ); diff --git a/src/Psalm/Issue/RiskyTruthyFalsyComparison.php b/src/Psalm/Issue/RiskyTruthyFalsyComparison.php new file mode 100644 index 00000000000..9150aa30b8c --- /dev/null +++ b/src/Psalm/Issue/RiskyTruthyFalsyComparison.php @@ -0,0 +1,17 @@ +dupe_key = $dupe_key; + } +} diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index bb16a20043e..2209c2f163d 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -3528,7 +3528,7 @@ function foo($arg) { if ($arg) { } }', - 'error_message' => 'TypeDoesNotContainType', + 'error_message' => 'RiskyTruthyFalsyComparison', ], 'nonStrictConditionTruthyFalsyNegated' => [ 'code' => ' 'TypeDoesNotContainType', + 'error_message' => 'RiskyTruthyFalsyComparison', ], 'nonStrictConditionTruthyFalsyFuncCall' => [ 'code' => ' 'TypeDoesNotContainType', + 'error_message' => 'RiskyTruthyFalsyComparison', ], 'nonStrictConditionTruthyFalsyFuncCallNegated' => [ 'code' => ' 'TypeDoesNotContainType', + 'error_message' => 'RiskyTruthyFalsyComparison', ], 'redundantConditionForNonEmptyString' => [ 'code' => ' Date: Sat, 13 Jan 2024 00:53:32 +0100 Subject: [PATCH 207/357] fix shepherd issue --- src/Psalm/PluginFileExtensionsSocket.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Psalm/PluginFileExtensionsSocket.php b/src/Psalm/PluginFileExtensionsSocket.php index 1e698e89cc5..0b5ac1333fa 100644 --- a/src/Psalm/PluginFileExtensionsSocket.php +++ b/src/Psalm/PluginFileExtensionsSocket.php @@ -55,8 +55,8 @@ public function addFileTypeScanner(string $fileExtension, string $className): vo 1_622_727_271, ); } - if (!empty($this->config->getFiletypeScanners()[$fileExtension]) - || !empty($this->additionalFileTypeScanners[$fileExtension]) + if (isset($this->config->getFiletypeScanners()[$fileExtension]) + || isset($this->additionalFileTypeScanners[$fileExtension]) ) { throw new LogicException( sprintf('Cannot redeclare scanner for file-type %s', $fileExtension), @@ -91,8 +91,8 @@ public function addFileTypeAnalyzer(string $fileExtension, string $className): v 1_622_727_281, ); } - if (!empty($this->config->getFiletypeAnalyzers()[$fileExtension]) - || !empty($this->additionalFileTypeAnalyzers[$fileExtension]) + if (isset($this->config->getFiletypeAnalyzers()[$fileExtension]) + || isset($this->additionalFileTypeAnalyzers[$fileExtension]) ) { throw new LogicException( sprintf('Cannot redeclare analyzer for file-type %s', $fileExtension), From 19b1a33a20609fbb8b8ababfe899de6af122fa3c Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sat, 13 Jan 2024 11:18:48 +0100 Subject: [PATCH 208/357] fix possibly undefined array key in keyed array doesnt include null when not validated Fix https://psalm.dev/r/b153d0d248 to return 'a'|null instead of 'a' - this is required as otherwise empty would report RedundantCondition errors now which would bring back https://github.com/vimeo/psalm/issues/2681 --- .../Expression/Fetch/ArrayFetchAnalyzer.php | 8 ++++++-- tests/ArrayAccessTest.php | 13 +++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php index 4a3a1f5c903..753e2891920 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php @@ -313,14 +313,18 @@ public static function analyze( && !$context->inside_unset && ($stmt_var_type && !$stmt_var_type->hasMixed()) ) { - IssueBuffer::maybeAdd( + if (IssueBuffer::accepts( new PossiblyUndefinedArrayOffset( 'Possibly undefined array key ' . $keyed_array_var_id . ' on ' . $stmt_var_type->getId(), new CodeLocation($statements_analyzer->getSource(), $stmt), ), $statements_analyzer->getSuppressedIssues(), - ); + )) { + $stmt_type = $stmt_type->getBuilder()->addType(new TNull())->freeze(); + } + } elseif ($stmt_type->possibly_undefined) { + $stmt_type = $stmt_type->getBuilder()->addType(new TNull())->freeze(); } $stmt_type = $stmt_type->setPossiblyUndefined(false); diff --git a/tests/ArrayAccessTest.php b/tests/ArrayAccessTest.php index 8bc2a488fc1..91ef428f99b 100644 --- a/tests/ArrayAccessTest.php +++ b/tests/ArrayAccessTest.php @@ -654,6 +654,19 @@ function f(array $p): void '$x3===' => "array{b: 'value'}", ], ], + 'possiblyUndefinedArrayOffsetKeyedArray' => [ + 'code' => ' [ + '$x===' => '"a"|null', + ], + 'ignored_issues' => ['PossiblyUndefinedArrayOffset'], + ], 'domNodeListAccessible' => [ 'code' => ' Date: Fri, 12 Jan 2024 23:37:45 +0100 Subject: [PATCH 209/357] fix bugs in tests --- tests/ArrayAccessTest.php | 19 +++++++++-- tests/FunctionCallTest.php | 4 +-- tests/ImmutableAnnotationTest.php | 2 +- tests/JsonOutputTest.php | 3 +- tests/Loop/DoTest.php | 2 +- tests/Loop/WhileTest.php | 4 +-- tests/MethodCallTest.php | 14 ++++---- tests/MethodSignatureTest.php | 6 ++-- tests/PropertyTypeTest.php | 8 +++-- tests/ReturnTypeTest.php | 6 ++++ tests/SwitchTypeTest.php | 2 +- tests/Template/ClassTemplateTest.php | 2 ++ .../AssignmentInConditionalTest.php | 6 ++-- tests/TypeReconciliation/ConditionalTest.php | 10 ++++++ tests/TypeReconciliation/EmptyTest.php | 8 ++++- tests/TypeReconciliation/IssetTest.php | 2 +- .../RedundantConditionTest.php | 15 ++++++--- tests/TypeReconciliation/TypeAlgebraTest.php | 32 +++++++++++++++++++ tests/UnusedVariableTest.php | 18 +++++------ 19 files changed, 121 insertions(+), 42 deletions(-) diff --git a/tests/ArrayAccessTest.php b/tests/ArrayAccessTest.php index 91ef428f99b..af33aa025f8 100644 --- a/tests/ArrayAccessTest.php +++ b/tests/ArrayAccessTest.php @@ -663,7 +663,7 @@ function f(array $p): void $x = $d[0];', 'assertions' => [ - '$x===' => '"a"|null', + '$x===' => '\'a\'', ], 'ignored_issues' => ['PossiblyUndefinedArrayOffset'], ], @@ -1349,7 +1349,7 @@ public function __toString() { echo $a[new Foo];', 'error_message' => 'InvalidArrayOffset', ], - 'possiblyUndefinedIntArrayOffet' => [ + 'possiblyUndefinedIntArrayOffset' => [ 'code' => ' 'PossiblyUndefinedArrayOffset', ], - 'possiblyUndefinedStringArrayOffet' => [ + 'possiblyUndefinedStringArrayOffset' => [ 'code' => ' 0.5, "b" => 1.5, "c" => new Exception()]);', 'error_message' => 'InvalidArgument', ], + 'possiblyUndefinedArrayOffsetKeyedArray' => [ + 'code' => ' 'PossiblyUndefinedArrayOffset', + ], ]; } } diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index e63636dff43..4d997070211 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -1937,7 +1937,7 @@ function badpattern() { 'strposAllowDictionary' => [ 'code' => ' [ @@ -2138,7 +2138,7 @@ function foo(A $a1, A $a2 = null): void 'strposFirstParamAllowClassString' => [ 'code' => ' [ diff --git a/tests/ImmutableAnnotationTest.php b/tests/ImmutableAnnotationTest.php index da36f660df9..07994b59b44 100644 --- a/tests/ImmutableAnnotationTest.php +++ b/tests/ImmutableAnnotationTest.php @@ -300,7 +300,7 @@ public function getError(): ?string { $dto = new DTO("BOOM!"); - if ($dto->getError()) { + if ($dto->getError() !== null) { takesString($dto->getError()); }', ], diff --git a/tests/JsonOutputTest.php b/tests/JsonOutputTest.php index c20d7b9d721..0fefa3f6dec 100644 --- a/tests/JsonOutputTest.php +++ b/tests/JsonOutputTest.php @@ -138,6 +138,7 @@ function fooFoo() { ], 'singleIssueForTypeDifference' => [ 'code' => ' 1, 'message' => 'Operand of type non-falsy-string is always truthy', - 'line' => 4, + 'line' => 5, 'error' => '$b', ], ]; diff --git a/tests/Loop/DoTest.php b/tests/Loop/DoTest.php index 5c125de7fac..0c703985b25 100644 --- a/tests/Loop/DoTest.php +++ b/tests/Loop/DoTest.php @@ -245,7 +245,7 @@ function bar(?string &$i) : void {} $c = null; do { - if (!$c) { + if ($c === null || $c === "" || $c === "0") { foo($c); } else { bar($c); diff --git a/tests/Loop/WhileTest.php b/tests/Loop/WhileTest.php index 73dd8085bdf..3056bdc8660 100644 --- a/tests/Loop/WhileTest.php +++ b/tests/Loop/WhileTest.php @@ -155,7 +155,7 @@ function foo(): ?A { } while ($a = foo()) { - if ($a->bar) {} + if ($a->bar !== null) {} }', ], 'whileTrueWithBreak' => [ @@ -271,7 +271,7 @@ function bar(?string &$i) : void {} $c = null; while (rand(0, 1)) { - if (!$c) { + if ($c === null || $c === "" || $c === "0") { foo($c); } else { bar($c); diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 9b8ba5b72b2..4c411170272 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -149,7 +149,7 @@ function printInt(int $int): void { $obj = new SomeClass(); - if ($obj->getInt()) { + if ($obj->getInt() !== null) { printInt($obj->getInt()); }', ); @@ -185,7 +185,7 @@ function printInt(int $int): void { $obj = new SomeClass(); - if ($obj->getInt()) { + if ($obj->getInt() !== null) { printInt($obj->getInt()); }', ); @@ -936,7 +936,7 @@ final public function getA() { $a = new A(); - if ($a->getA()) { + if ($a->getA() !== null) { echo strlen($a->getA()); }', ], @@ -1007,7 +1007,7 @@ function printInt(int $int): void { $obj = new SomeClass(); - if ($obj->getInt()) { + if ($obj->getInt() !== null) { printInt($obj->getInt()); }', ], @@ -1031,7 +1031,7 @@ function printInt(int $int): void { $obj = new SomeClass(); - if ($obj->getInt()) { + if ($obj->getInt() !== null) { printInt($obj->getInt()); }', ], @@ -1631,7 +1631,7 @@ function getA() { } function foo(A $a) : void { - if ($a->getA()) { + if ($a->getA() !== null) { echo strlen($a->getA()); } } @@ -1697,7 +1697,7 @@ function printInt(int $int): void { $obj = new SomeClass(); - if ($obj->getInt()) { + if ($obj->getInt() !== null) { printInt($obj->getInt()); }', 'error_message' => 'PossiblyNullArgument', diff --git a/tests/MethodSignatureTest.php b/tests/MethodSignatureTest.php index a740cf0f4a9..e973c0f7e77 100644 --- a/tests/MethodSignatureTest.php +++ b/tests/MethodSignatureTest.php @@ -311,7 +311,7 @@ public function foo(string $s): ?string { class B extends A { public function foo(?string $s): string { - return $s ?: "hello"; + return $s !== null ? $s : "hello"; } } @@ -327,7 +327,7 @@ public function foo(string $s): string { class B extends A { public function foo(string $s = null): string { - return $s ?: "hello"; + return $s !== null ? $s : "hello"; } } @@ -1044,7 +1044,7 @@ public function fooFoo(int $a, bool $c): void { 'code' => 'getX()) { + if (is_int($x->getX())) { XCollector::modify(); if ($x->getX() === null) {} } @@ -221,7 +221,7 @@ public function getX() : ?int { } function testX(X $x): void { - if ($x->getX()) { + if ($x->getX() !== null) { XCollector::modify(); if ($x->getX() === null) {} } @@ -255,7 +255,7 @@ public function __construct(?int $x) { } function testX(X $x): void { - if ($x->x) { + if ($x->x !== null) { XCollector::modify(); if ($x->x === null) {} } @@ -686,6 +686,8 @@ class A { } echo substr($a->aa, 1);', + 'assertions' => [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'nullableStaticPropertyWithIfCheck' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'returnTypeNotEmptyCheckInElseIf' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'returnTypeNotEmptyCheckInElse' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'returnTypeAfterIf' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'noCrashTemplateInsideGenerator' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'assignmentInIf' => [ 'code' => ' 5) { - } elseif (($a = rand(0, 1) ? new A : null) && $a->foo) {}', + } elseif (($a = rand(0, 1) ? new A : null) && is_string($a->foo)) {}', ], 'noParadoxAfterConditionalAssignment' => [ 'code' => ' 'InvalidReturnStatement', - 'ignored_issues' => [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], 'php_version' => '8.0', ], 'assignmentInBranchOfAndReferencedAfterIf' => [ diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index 2209c2f163d..5a070688870 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -594,6 +594,8 @@ function Foo($value = null) : bool { } return false; }', + 'assertions' => [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'numericStringAssertion' => [ 'code' => ' 5) {} } }', + 'assertions' => [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'arrayUnionTypeSwitching' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'propertySetOnElementInConditional' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'issetAssertionOnStaticProperty' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'nonEmptyStringAfterLiteralCheck' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'emptyExceptionReconciliationAfterIf' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'alwaysBoolResult' => [ 'code' => ' [], - 'ignored_issues' => ['MixedAssignment', 'MissingParamType', 'MixedArgument'], + 'ignored_issues' => ['MixedAssignment', 'MissingParamType', 'MixedArgument', 'RiskyTruthyFalsyComparison'], ], 'multipleEmptiesInCondition' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'reconcileEmptyTwiceWithoutReturn' => [ 'code' => ' [], - 'ignored_issues' => ['MixedAssignment', 'MixedArrayAccess'], + 'ignored_issues' => ['MixedAssignment', 'MixedArrayAccess', 'RiskyTruthyFalsyComparison'], ], 'mixedArrayIssetGetStringVar' => [ 'code' => ' [], + 'ignored_issues' => [ + 'RiskyTruthyFalsyComparison', + ], ], 'noRedundantConditionAfterAssignment' => [ 'code' => 'foo) {} + if ($i->foo !== null) {} break; default: @@ -180,7 +184,7 @@ function makeA() { } if ($a) {}', 'assertions' => [], - 'ignored_issues' => ['MixedAssignment', 'MixedArrayAccess'], + 'ignored_issues' => ['MixedAssignment', 'MixedArrayAccess', 'RiskyTruthyFalsyComparison'], ], 'noComplaintWithIsNumericThenIsEmpty' => [ 'code' => ' [], 'ignored_issues' => ['MixedAssignment', 'MixedArrayAccess'], ], @@ -539,7 +543,7 @@ function bar(string $b) : bool { exit; } - if ($i) {}', + if ($i !== array() && $i !== "" && $i !== "0") {}', ], 'emptyWithoutKnowingArrayType' => [ 'code' => ' ' 'RedundantCondition', + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'refineTypeInMethodCall' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'twoVarLogicNotNestedWithAllPathsReturning' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'twoVarLogicNotNestedWithAssignmentBeforeReturn' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'invertedTwoVarLogicNotNested' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'invertedTwoVarLogicNotNestedWithAssignmentBeforeReturn' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'twoVarLogicNotNestedWithElseifAndNoNegations' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'threeVarLogicNotNestedWithNoRedefinitionsWithClasses' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'threeVarLogicNotNestedAndOrWithNoRedefinitions' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'twoVarLogicNotNestedWithElseifCorrectlyNegatedInElseIf' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'lotsaTruthyStatements' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'cancelOutDifferentStatement' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'moreChecks' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'compareToIntInsideIfCNF' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'ternaryAssertionOnBool' => [ 'code' => ' 'NullableReturnStatement', + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'invertedTwoVarLogicNotNestedWithElseif' => [ 'code' => ' 'NullableReturnStatement', + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'threeVarLogicWithElseifAndAnd' => [ 'code' => ' 'TypeDoesNotContainType', + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'twoVarLogicNotNestedWithElseifIncorrectlyReinforcedInIf' => [ 'code' => ' 'RedundantCondition', + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'repeatedIfStatements' => [ 'code' => ' 'TypeDoesNotContainType', + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'repeatedConditionals' => [ 'code' => ' 'RedundantCondition', + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'dependentTypeInvalidated' => [ 'code' => ' [ @@ -128,10 +128,10 @@ function foo(string $a): void { 'dummyByRefVar' => [ 'code' => 'getMessage(); } - if ($s) {} + if ($s !== null) {} }', ], 'throwWithMessageCallAndAssignmentInCatchAndReference' => [ @@ -940,7 +940,7 @@ function foo() : void { if ($foo) {} } catch (Exception $e) {} - if ($foo) {}', + if ($foo !== false && $foo !== 0) {}', ], 'useTryAssignedVariableInsideFinally' => [ 'code' => ' [], - 'ignored_issues' => [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], 'php_version' => '8.0', ], 'concatWithUnknownProperty' => [ @@ -3165,7 +3165,7 @@ function bar() : void { $user = $user_id; } - if ($user) { + if ($user !== null && $user !== 0) { $a = 0; for ($i = 1; $i <= 10; $i++) { $a += $i; @@ -3185,7 +3185,7 @@ function bar() : void { $user = $user_id; } - if ($user) { + if ($user !== null && $user !== 0) { $a = 0; foreach ([1, 2, 3] as $i) { $a += $i; From dca17bcb6affce958f4650172db70379138a6d46 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sat, 13 Jan 2024 13:05:43 +0100 Subject: [PATCH 210/357] unrelated fix spelling --- tests/ArrayAccessTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ArrayAccessTest.php b/tests/ArrayAccessTest.php index af33aa025f8..88eb7d15e52 100644 --- a/tests/ArrayAccessTest.php +++ b/tests/ArrayAccessTest.php @@ -691,7 +691,7 @@ function example(array $x, $y) : void { 'assertions' => [], 'ignored_issues' => ['MixedArgument', 'MixedArrayOffset', 'MissingParamType'], ], - 'suppressPossiblyUndefinedStringArrayOffet' => [ + 'suppressPossiblyUndefinedStringArrayOffset' => [ 'code' => ' Date: Sat, 13 Jan 2024 13:03:28 +0100 Subject: [PATCH 211/357] simplify and remove redundant variable --- .../Block/IfConditionalAnalyzer.php | 34 ++++++++----------- .../Expression/BooleanNotAnalyzer.php | 34 ++++++++----------- .../Statements/Expression/EmptyAnalyzer.php | 34 ++++++++----------- 3 files changed, 45 insertions(+), 57 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfConditionalAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfConditionalAnalyzer.php index 23b88d0710e..ebbc0a66e4a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/IfConditionalAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfConditionalAnalyzer.php @@ -371,34 +371,30 @@ public static function handleParadoxicalCondition( } elseif (!($stmt instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical) && !($stmt instanceof PhpParser\Node\Expr\BinaryOp\Identical) && !($stmt instanceof PhpParser\Node\Expr\BooleanNot)) { - $has_both = false; - $both_types = $type->getBuilder(); if (count($type->getAtomicTypes()) > 1) { + $both_types = $type->getBuilder(); foreach ($both_types->getAtomicTypes() as $key => $atomic_type) { if ($atomic_type->isTruthy() || $atomic_type->isFalsy() || $atomic_type instanceof TBool) { $both_types->removeType($key); - continue; } - - $has_both = true; } - } - if ($has_both) { - $both_types = $both_types->freeze(); - IssueBuffer::maybeAdd( - new RiskyTruthyFalsyComparison( - 'Operand of type ' . $type->getId() . ' contains ' . - 'type' . (count($both_types->getAtomicTypes()) > 1 ? 's' : '') . ' ' . - $both_types->getId() . ', which can be falsy and truthy. ' . - 'This can cause possibly unexpected behavior. Use strict comparison instead.', - new CodeLocation($statements_analyzer, $stmt), - $type->getId(), - ), - $statements_analyzer->getSuppressedIssues(), - ); + if (count($both_types->getAtomicTypes()) > 0) { + $both_types = $both_types->freeze(); + IssueBuffer::maybeAdd( + new RiskyTruthyFalsyComparison( + 'Operand of type ' . $type->getId() . ' contains ' . + 'type' . (count($both_types->getAtomicTypes()) > 1 ? 's' : '') . ' ' . + $both_types->getId() . ', which can be falsy and truthy. ' . + 'This can cause possibly unexpected behavior. Use strict comparison instead.', + new CodeLocation($statements_analyzer, $stmt), + $type->getId(), + ), + $statements_analyzer->getSuppressedIssues(), + ); + } } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php index 71f6e19324f..93d17c3f7f5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php @@ -45,34 +45,30 @@ public static function analyze( } elseif ($expr_type->isAlwaysFalsy()) { $stmt_type = new TTrue($expr_type->from_docblock); } else { - $has_both = false; - $both_types = $expr_type->getBuilder(); if (count($expr_type->getAtomicTypes()) > 1) { + $both_types = $expr_type->getBuilder(); foreach ($both_types->getAtomicTypes() as $key => $atomic_type) { if ($atomic_type->isTruthy() || $atomic_type->isFalsy() || $atomic_type instanceof TBool) { $both_types->removeType($key); - continue; } - - $has_both = true; } - } - if ($has_both) { - $both_types = $both_types->freeze(); - IssueBuffer::maybeAdd( - new RiskyTruthyFalsyComparison( - 'Operand of type ' . $expr_type->getId() . ' contains ' . - 'type' . (count($both_types->getAtomicTypes()) > 1 ? 's' : '') . ' ' . - $both_types->getId() . ', which can be falsy and truthy. ' . - 'This can cause possibly unexpected behavior. Use strict comparison instead.', - new CodeLocation($statements_analyzer, $stmt), - $expr_type->getId(), - ), - $statements_analyzer->getSuppressedIssues(), - ); + if (count($both_types->getAtomicTypes()) > 0) { + $both_types = $both_types->freeze(); + IssueBuffer::maybeAdd( + new RiskyTruthyFalsyComparison( + 'Operand of type ' . $expr_type->getId() . ' contains ' . + 'type' . (count($both_types->getAtomicTypes()) > 1 ? 's' : '') . ' ' . + $both_types->getId() . ', which can be falsy and truthy. ' . + 'This can cause possibly unexpected behavior. Use strict comparison instead.', + new CodeLocation($statements_analyzer, $stmt), + $expr_type->getId(), + ), + $statements_analyzer->getSuppressedIssues(), + ); + } } $stmt_type = new TBool(); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php index 02fae12fd44..8dbcca2f9bb 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php @@ -64,34 +64,30 @@ public static function analyze( } elseif ($expr_type->isAlwaysFalsy()) { $stmt_type = new TTrue($expr_type->from_docblock); } else { - $has_both = false; - $both_types = $expr_type->getBuilder(); if (count($expr_type->getAtomicTypes()) > 1) { + $both_types = $expr_type->getBuilder(); foreach ($both_types->getAtomicTypes() as $key => $atomic_type) { if ($atomic_type->isTruthy() || $atomic_type->isFalsy() || $atomic_type instanceof TBool) { $both_types->removeType($key); - continue; } - - $has_both = true; } - } - if ($has_both) { - $both_types = $both_types->freeze(); - IssueBuffer::maybeAdd( - new RiskyTruthyFalsyComparison( - 'Operand of type ' . $expr_type->getId() . ' contains ' . - 'type' . (count($both_types->getAtomicTypes()) > 1 ? 's' : '') . ' ' . - $both_types->getId() . ', which can be falsy and truthy. ' . - 'This can cause possibly unexpected behavior. Use strict comparison instead.', - new CodeLocation($statements_analyzer, $stmt), - $expr_type->getId(), - ), - $statements_analyzer->getSuppressedIssues(), - ); + if (count($both_types->getAtomicTypes()) > 0) { + $both_types = $both_types->freeze(); + IssueBuffer::maybeAdd( + new RiskyTruthyFalsyComparison( + 'Operand of type ' . $expr_type->getId() . ' contains ' . + 'type' . (count($both_types->getAtomicTypes()) > 1 ? 's' : '') . ' ' . + $both_types->getId() . ', which can be falsy and truthy. ' . + 'This can cause possibly unexpected behavior. Use strict comparison instead.', + new CodeLocation($statements_analyzer, $stmt), + $expr_type->getId(), + ), + $statements_analyzer->getSuppressedIssues(), + ); + } } $stmt_type = new TBool(); From 5643cf53d4983b75a7772cd96b53b109c3a020a9 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sat, 13 Jan 2024 16:00:57 +0100 Subject: [PATCH 212/357] fix mixed test not actually checking mixed (since superglobals have a more specific type now) --- tests/TypeReconciliation/ConditionalTest.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index 5a070688870..4d48add4f1d 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -1168,11 +1168,19 @@ function Foo($width, $height) : void { ], 'notEmptyCheckOnMixedInTernary' => [ 'code' => ' [ 'code' => ' Date: Mon, 15 Jan 2024 10:09:49 +0100 Subject: [PATCH 213/357] change error level to 2 --- docs/running_psalm/error_levels.md | 2 +- src/Psalm/Issue/RiskyTruthyFalsyComparison.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/running_psalm/error_levels.md b/docs/running_psalm/error_levels.md index df53f5227f6..f3df22adb45 100644 --- a/docs/running_psalm/error_levels.md +++ b/docs/running_psalm/error_levels.md @@ -173,7 +173,6 @@ Level 5 and above allows a more non-verifiable code, and higher levels are even - [TooManyArguments](issues/TooManyArguments.md) - [TypeDoesNotContainNull](issues/TypeDoesNotContainNull.md) - [TypeDoesNotContainType](issues/TypeDoesNotContainType.md) -- [RiskyTruthyFalsyComparison](issues/RiskyTruthyFalsyComparison.md) - [UndefinedMagicMethod](issues/UndefinedMagicMethod.md) - [UndefinedMagicPropertyAssignment](issues/UndefinedMagicPropertyAssignment.md) - [UndefinedMagicPropertyFetch](issues/UndefinedMagicPropertyFetch.md) @@ -245,6 +244,7 @@ Level 5 and above allows a more non-verifiable code, and higher levels are even - [RedundantConditionGivenDocblockType](issues/RedundantConditionGivenDocblockType.md) - [RedundantFunctionCallGivenDocblockType](issues/RedundantFunctionCallGivenDocblockType.md) - [ReferenceConstraintViolation](issues/ReferenceConstraintViolation.md) +- [RiskyTruthyFalsyComparison](issues/RiskyTruthyFalsyComparison.md) - [UndefinedTrace](issues/UndefinedTrace.md) - [UnresolvableInclude](issues/UnresolvableInclude.md) - [UnsafeInstantiation](issues/UnsafeInstantiation.md) diff --git a/src/Psalm/Issue/RiskyTruthyFalsyComparison.php b/src/Psalm/Issue/RiskyTruthyFalsyComparison.php index 9150aa30b8c..68ab4e1322b 100644 --- a/src/Psalm/Issue/RiskyTruthyFalsyComparison.php +++ b/src/Psalm/Issue/RiskyTruthyFalsyComparison.php @@ -6,7 +6,7 @@ final class RiskyTruthyFalsyComparison extends CodeIssue { - public const ERROR_LEVEL = 4; + public const ERROR_LEVEL = 2; public const SHORTCODE = 356; public function __construct(string $message, CodeLocation $code_location, ?string $dupe_key) From 4b418918299e9846ca95c2f9451a2a8fa8063135 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Mon, 15 Jan 2024 10:33:46 +0100 Subject: [PATCH 214/357] update baseline --- psalm-baseline.xml | 1874 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 1868 insertions(+), 6 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 71cc2ef6324..85f358308e2 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,186 @@ - + + + + mapper]]> + + + + + $items + + + + + $returnType + attrGroups]]> + byRef]]> + expr]]> + params]]> + returnType]]> + static]]> + + + + + + + + $returnType + attrGroups]]> + byRef]]> + params]]> + returnType]]> + static]]> + stmts]]> + uses]]> + + + + + $items + + + + + $parts + + + + + $conds + + + + + $parts + $parts + $parts + + + + + $stmts + + + + + $stmts + + + + + $returnType + attrGroups]]> + byRef]]> + flags]]> + params]]> + returnType]]> + stmts]]> + + + + + attrGroups]]> + extends]]> + flags]]> + implements]]> + stmts]]> + + + + + $stmts + + + + + $stmts + + + + + $stmts + + + + + $stmts + + + + + cond]]> + init]]> + loop]]> + stmts]]> + + + + + byRef]]> + keyVar]]> + stmts]]> + + + + + $returnType + attrGroups]]> + byRef]]> + params]]> + returnType]]> + stmts]]> + + + + + else]]> + elseifs]]> + stmts]]> + + + + + attrGroups]]> + extends]]> + stmts]]> + + + + + $stmts + + + + + attrGroups]]> + stmts]]> + + + + + $stmts + + + + + $stmts + + + + + static::getDefaultDescription() + static::getDefaultDescription() + static::getDefaultDescription() + static::getDefaultName() + static::getDefaultName() + static::getDefaultName() + + + $name + + tags['variablesfrom'][0]]]> @@ -12,6 +193,25 @@ $matches[1] + + + cased_name]]> + + + + + !$appearing_method_id + + + + + docblock_line_number]]> + docblock_line_number]]> + docblock_start]]> + docblock_start_line_number]]> + text]]> + + $const_name @@ -19,11 +219,83 @@ $symbol_name $symbol_parts[1] + + !$function_name + namespace]]> + namespace]]> + namespace]]> + namespace_first_stmt_start]]> + uses_end]]> + $file_path + insertText]]> + symbol, '()')]]> + symbol, '()')]]> + symbol, '()')]]> + symbol, '()')]]> + symbol, '::')]]> + symbol, '::')]]> + symbol, '\\')]]> + + + + + + + + + + + + + !$composer_json + !$config_path + !$file_path + + $cwd + $dir + function_id]]> + + $issue_handler_children + $parent_issue_type + composer_class_loader->findFile($pluginClassName)]]> + autoloader]]> + localName, $offset)]]> + name, $offset - strlen($file_contents))]]> + + + + + $suggested_dir + file_path, 'stub')]]> + file_path, 'vendor')]]> + + + !$directory_path + !$file_path + !$glob_directory_path + !$glob_file_path + directory]]> + file]]> + referencedClass]]> + referencedConstant]]> + referencedFunction]]> + referencedMethod]]> + referencedProperty]]> + referencedVariable]]> + glob($parts[0], GLOB_NOSORT) + glob($parts[0], GLOB_ONLYDIR | GLOB_NOSORT) + + + + + + + @@ -32,6 +304,17 @@ $matches[3] + + + $creating_conditional_id + $creating_conditional_id + + + + + name->name ?? null !== "name"]]> + + $comments[0] @@ -39,11 +322,147 @@ props[0]]]> $uninitialized_variables[0] + + !$declaring_property_class + !$fq_class_name + self]]> + self]]> + self]]> + self]]> + template_extended_params]]> + template_types]]> + $class_template_params + initialized_class]]> + $parent_fq_class_name + getStmts()]]> + getStmts()]]> + template_extended_params]]> + template_types]]> + classlike_storage_provider->get($original_fq_classlike_name), + strtolower($stmt->name->name), + $this_object_type, + )]]> + $property_name + + !$appearing_property_class + self]]> + !$declaring_property_class + self]]> + template_types]]> + $resolved_name + template_covariants]]> + template_extended_params]]> + template_types]]> + template_types]]> + + + + + self]]> + self]]> + self]]> + + + + + !$original_type + description]]> + var_id]]> + !$var_type_tokens + $brackets + $template_type_map + $type_aliases + line_number]]> + type_end]]> + type_start]]> + + + + + $namespace_name + $namespace_name + root_file_name]]> + root_file_path]]> + + + + + $namespace + $namespace + getNamespace()]]> + + + + + getStmts()]]> + $class_template_params + self]]> + self]]> + $fq_class_name + $self_fq_class_name + + + + + calling_method_id]]> + cased_name]]> + cased_name]]> + + template_types]]> + template_types]]> + $cased_method_id + $cased_method_id + $cased_method_id + $cased_method_id + $cased_method_id + self]]> + self]]> + self]]> + self]]> + self]]> + $context_self + $hash + $namespace + $parent_fqcln + $parent_fqcln + cased_name]]> + template_types]]> + $template_types + function->getStmts()]]> + source->getTemplateTypeMap()]]> + storage->template_types]]> + + + + + + !$calling_method_id + self]]> + $appearing_method_class + $appearing_method_class + self]]> + $context_self + + + + + template_types]]> + cased_name]]> + cased_name]]> + cased_name]]> + template_extended_params]]> + template_extended_params]]> + template_extended_params]]> + defining_fqcln]]> + @@ -53,22 +472,86 @@ $php_minor_version $source_parts[1] + + self]]> + + $potential_file_path + + + + + + branch_point]]> + cond]]> + + branch_point]]> + if (AtomicTypeComparator::isContainedBy( if (AtomicTypeComparator::isContainedBy( + + var_id]]> + var_id]]> + $calling_type_params + branch_point]]> + template_types]]> + getTemplateTypeMap()]]> + line_number]]> + type_end]]> + type_start]]> + $var_id + $var_id + + + + + negatable_if_types]]> + getTemplateTypeMap()]]> + + + + + getTemplateTypeMap()]]> + getTemplateTypeMap()]]> + + + + + getTemplateTypeMap()]]> + + + + + branch_point]]> + branch_point]]> + branch_point]]> + assigned_var_ids]]> + new_vars]]> + redefined_vars]]> + getTemplateTypeMap()]]> + assigned_var_ids += $switch_scope->new_assigned_var_ids]]> + + !$switch_var_id + new_assigned_var_ids]]> + new_vars_in_scope]]> + possibly_redefined_vars]]> + possibly_redefined_vars]]> + redefined_vars]]> + $switch_var_id + @@ -76,6 +559,29 @@ leftover_statements[0]]]> traverse([$switch_condition])[0]]]> + + branch_point]]> + $nested_or_options + $switch_var_id + $switch_var_id + $switch_var_id + $type_statements + + + + + branch_point]]> + + + + + branch_point]]> + + + + + $var_id + @@ -108,23 +614,204 @@ getArgs()[0]]]> getArgs()[0]]]> + + !$var_name + !$var_type + ')]]> + + $array_root + $count_equality_position + $count_equality_position + $count_equality_position + $count_inequality_position + $count_inequality_position + $count_inequality_position + $false_position + $false_position + $first_var_name + $first_var_name + $first_var_name + $first_var_name + $first_var_name + $first_var_name + $first_var_name + $first_var_name + $first_var_name + $first_var_name + $first_var_name + $first_var_name + $first_var_name + $first_var_name_in_array_argument + $get_debug_type_position + $get_debug_type_position + $getclass_position + $getclass_position + $gettype_position + $gettype_position + $if_false_assertions + $if_true_assertions + $inferior_value_position + $other_var_name + $superior_value_position + $this_class_name + $this_class_name + $this_class_name + $true_position + $true_position + $typed_value_position + $typed_value_position + $var_id + $var_id + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name + $var_name_left + $var_name_right + $var_type + $var_type + $var_type + self::hasReconcilableNonEmptyCountEqualityCheck($conditional) + + + + + !$parent_var_id + $object_id + $parent_var_id + $parent_var_id + $root_var_id + $root_var_id + $root_var_id + $root_var_id + $root_var_id + $var_id + $var_var_id + + + + + self]]> + !$var_id + $appearing_property_class + $class_template_params + $class_template_params + calling_method_id]]> + calling_method_id]]> + self]]> + self]]> + self]]> + $declaring_property_class + getter_method]]> + $var_id + $var_id + $var_id + $var_id + $var_id + $var_id + $var_id + $var_id + $var_id + $var_property_id + $var_property_id + calling_method_id, '::__clone')]]> + calling_method_id, '::__construct')]]> + calling_method_id, '::__unserialize')]]> + calling_method_id, '::unserialize')]]> + $new_property_name + + calling_method_id]]> + $var_id + $var_id + + + var_id]]> + ')]]> + ')]]> + + $assign_value_id + calling_method_id]]> + $extended_var_id + $extended_var_id + $extended_var_id + $extended_var_id + $extended_var_id + $list_var_id + $list_var_id + $list_var_id + $prop_name + $root_var_id + line_number]]> + type_end]]> + type_start]]> + $var_id + $var_id + $var_id + $var_id + $var_id + $var_id + $var_id + $var_id + $var_id + vars_in_scope[$lhs_var_id] = &$context->vars_in_scope[$rhs_var_id]]]> + + + getTemplateTypeMap()]]> + + $invalid_left_messages[0] $invalid_right_messages[0] + + + branch_point]]> + $var_id + + verifyType @@ -134,6 +821,54 @@ $parts[1] + + !$container_class + $cased_method_id + $cased_method_id + $cased_method_id + $cased_method_id + $cased_method_id + $class_generic_params + calling_function_id]]> + calling_function_id]]> + calling_method_id]]> + $self_fq_class_name + $static_fq_class_name + $var_id + value, '::')]]> + value, '::')]]> + + + + + self]]> + $cased_method_id + $cased_method_id + $cased_method_id + $cased_method_id + $cased_method_id + $cased_method_id + calling_method_id]]> + calling_method_id]]> + calling_method_id]]> + calling_method_id]]> + calling_method_id]]> + calling_method_id]]> + calling_method_id]]> + sinks]]> + $function_params + $function_params + $function_params + template_types]]> + $method_id + $method_id + $method_id + $method_id + $var_id + $var_id + $var_id + getFQCLN())]]> + @@ -142,19 +877,117 @@ $args[1] $method_name + + !$container_class + calling_method_id]]> + $var_id + + + + + !$template_types + !$template_types + template_types]]> + $method_name + $overridden_template_types + template_extended_params]]> + template_types]]> + + + $function_name + $function_name + + + getArgs()[0]->value]]> + getArgs()[0]]]> $parts[1] + + function_id]]> + function_id]]> + function_id]]> + function_id]]> + function_id]]> + function_id]]> + getTemplateTypeMap()]]> + value, '::')]]> + $method + + self]]> + self]]> + self]]> + template_types]]> + template_types]]> + + + + + mixin_declaring_fqcln]]> + mixin_declaring_fqcln]]> + template_types]]> + template_types]]> + calling_method_id]]> + calling_method_id]]> + self]]> + $lhs_var_id + $mixin_class_template_params + + + + + $class_template_params + calling_method_id]]> + calling_method_id]]> + $lhs_var_id + template_types]]> + template_types]]> + + + + + $caller_identifier + + + + + this_property_mutations]]> + + + + + specialization_key]]> + $var_id + + + + + self]]> + self]]> + $appearing_method_name + + + + + $found_generic_params + $found_generic_params + $found_generic_params + $found_generic_params + $found_generic_params + $found_generic_params + $intersection_method_id + $intersection_method_id + @@ -165,23 +998,115 @@ non_existent_interface_method_ids[0]]]> non_existent_magic_method_ids[0]]]> + + getFQCLN()]]> + $lhs_var_id + $lhs_var_id + $lhs_var_id + + + + + getFQCLN()]]> + $path_to_file + $var_id + ')]]> + + + + + + calling_method_id]]> + self]]> + $fq_class_name + $fq_class_name + getFullyQualifiedFunctionMethodOrNamespaceName()]]> + template_extended_params]]> + template_types]]> + template_types]]> + template_types]]> + + + + + parent_class]]> + $child_fq_class_name + calling_method_id]]> + self]]> + self]]> + + + + + self]]> + !$fq_class_name + mixin_declaring_fqcln]]> + parent_class]]> + parent_class]]> + calling_method_id]]> + calling_method_id]]> + self]]> + $new_method_name + + self]]> + self]]> + self]]> + self]]> + $found_generic_params + $found_generic_params + template_extended_params]]> + items[0]]]> items[1]]]> + + !$arg_var_id + $arg_var_id + $assertion_var_id + template_extended_params]]> + self]]> + self]]> + self]]> + template_types]]> + template_types]]> + $new_const_name $new_const_name + + self]]> + calling_method_id]]> + calling_method_id]]> + self]]> + self]]> + self]]> + self]]> + + + + + !$lhs_var_name + !$object_id + !$object_id + !$this_class_name + $object_id + $property_root + $resolved_name + $resolved_name + $root_var_id + $this_class_name + @@ -189,31 +1114,143 @@ $stmt_type $stmt_type + + $dim_var_id + $dim_var_id + $extended_var_id + $extended_var_id + $keyed_array_var_id + $keyed_array_var_id + $keyed_array_var_id + $keyed_array_var_id + $stmt_type + + self]]> + self]]> + $declaring_property_class + $declaring_property_class + template_types]]> + template_types]]> + $var_id + $var_id + $var_property_id + $var_property_id + $invalid_fetch_types[0] + + !$prop_name + calling_method_id]]> + calling_method_id]]> + $declaring_property_class + $stmt_var_id + $var_id + $var_id + $new_property_name + + !$prop_name + calling_method_id]]> + calling_method_id]]> + calling_method_id]]> + self]]> + $string_type + $var_id + $var_id + + + + + $branch_point + $branch_point + + + + + $var_id + + + + + !$evaled_path + !$var_id + $include_path + $left_string + $path_to_file + $right_string + $var_id + + + + + self]]> + + + + + !$switch_var_id + $switch_var_id + + + + + $fq_classlike_name + + + + + branch_point]]> + getTemplateTypeMap()]]> + getTemplateTypeMap()]]> + type_params[2]]]> + + var_id]]> + $class_template_params + declaring_yield_fqcn]]> + self]]> + line_number]]> + type_end]]> + type_start]]> + $method_name + + calling_function_id]]> + calling_method_id]]> + var_id]]> + calling_function_id]]> + self]]> + $found_generic_params + line_number]]> + type_end]]> + type_start]]> + + + + + $root_var_id + $var_id + @@ -231,21 +1268,84 @@ expr->getArgs()[0]]]> + + $branch_point + $new_issues + getNamespace()]]> + $possible_traced_variable_names + fake_this_class]]> + vars_to_initialize]]> + + + + + UndefinedFunction + UndefinedFunction + + + + + !$root_path + + + + error_baseline]]> + !$paths_to_check + !$root_path + + + $baseline_file_path + $cache_directory + threads]]> + $find_references_to + empty($baselineFile) + + + + + + !$root_path + $paths_to_check + $identifier_name + + !$last_arg + !$last_arg + !$last_arg + !$root_path + + + + + + !$config_file + !$end_psalm_open_tag + !$path_to_check + error_baseline]]> + $f_paths + $path_to_config + $stdin = fgets(STDIN) + getPHPVersionFromComposerJson()]]> + getPhpVersionFromConfig()]]> + + $trait + + + @@ -255,11 +1355,92 @@ $source_const_name $stub + + !$calling_fq_class_name + !$insert_pos + !$insert_pos + !$insert_pos + $calling_fq_class_name + $calling_fq_class_name + $calling_fq_class_name + $calling_fq_class_name + $calling_fq_class_name + $calling_fq_class_name + $calling_fq_class_name + $calling_fq_class_name + $calling_fq_class_name + $calling_fq_class_name + $calling_method_id + $calling_method_id + $calling_method_id + $calling_method_id + $calling_method_id + $file_path + $file_path + $file_path + $file_path + $file_path + $migrated_source_fqcln + $migrated_source_fqcln + + + + + value]]> + $stub + + !$checked_file_path + !$root_file_path + $args + cased_name]]> + $namespace + + + + + !$return_type_string + + + + + !$calling_class_name + !$extends + $calling_method_id + $calling_method_id + $calling_method_id + $calling_method_id + $calling_method_id + $calling_method_id + $calling_method_id + $calling_method_id + $calling_method_id + $found_generic_params + $old_method_id + $source_file_path + $source_file_path + $source_file_path + $source_file_path + $source_file_path + $source_file_path + $source_file_path + $source_file_path + + + + + + $mapped_name + template_extended_params]]> + template_extended_params]]> + template_extended_params]]> + template_types]]> + template_extended_params]]> + @@ -270,6 +1451,38 @@ $property_name $property_name + + calling_method_id]]> + calling_method_id]]> + calling_method_id]]> + + + + + $composer_file_path + cased_name]]> + cased_name]]> + + + + + specialization_key]]> + unspecialized_id]]> + escaped_taints]]> + unescaped_taints]]> + specialization_key]]> + path_types)]]> + + + + + + + + + + $specialization_key + @@ -280,28 +1493,110 @@ stmts[0]]]> $b_stmt_comments[0] + + stmts]]> + stmts]]> + $b[$y] + + + readEnv['CI_PR_NUMBER']]]> + + $exploded[1] $url + + + $var_end + $var_start + + + + + + new_php_return_type]]> + $last_arg_position + new_php_return_type]]> + new_phpdoc_return_type]]> + return_typehint_colon_start]]> + return_typehint_end]]> + return_typehint_end]]> + return_typehint_start]]> + return_typehint_start]]> + $php_type + new_phpdoc_return_type]]> + new_psalm_return_type]]> + return_type_description]]> + return_type_description]]> + + props[0]]]> + + new_php_type]]> + new_php_type]]> + new_phpdoc_type]]> + typehint_end]]> + typehint_end]]> + typehint_start]]> + typehint_start]]> + $preceding_semicolon_pos + new_phpdoc_type]]> + new_psalm_type]]> + type_description]]> + + + + + !$sockets + + + + + tmpIni]]> + + + + + empty($message) + + + + + TCPServerAddress]]> + TCPServerAddress]]> + onchangeLineLimit]]> + empty($additional_info) + $method_id_parts[1] + + + $arg_var_id + $arg_var_id + $left_var_id + $left_var_id + $right_var_id + $right_var_id + $var_id + $var_id + + $cs[0] @@ -313,6 +1608,17 @@ $replacement_stmts[0] $replacement_stmts[0] + + !$method_contents + parser->parse( + $hacky_class_fix, + $error_handler, + )]]> + parser->parse( + $fake_class, + $error_handler, + )]]> + @@ -321,18 +1627,56 @@ children[0]]]> children[1]]]> + + !$method_entry + + $l[4] $r[4] + + !$var_line_parts + newModifier]]> + $class_name + description]]> + inheritors]]> + yield]]> + template_types]]> + template_types]]> + template_types]]> + template_types]]> + template_types]]> + template_types]]> + template_types]]> + aliases->namespace]]> + aliases->namespace]]> + line_number]]> + type_end]]> + type_start]]> + + + + + $fq_classlike_name + $string_value + $string_value + $string_value + getArgs()[0]]]> getArgs()[1]]]> + + !$skip_if_descendants + !$skip_if_descendants + $include_path + $path_to_file + @@ -346,16 +1690,75 @@ $source_param_string + + namespace]]> + template_types]]> + template_types]]> + description]]> + return_type_end]]> + return_type_line_number]]> + return_type_line_number]]> + return_type_start]]> + template_types]]> + template_types]]> + template_types]]> + template_types]]> + $template_types + $template_types + $template_types + stmts[0]]]> + + stmts]]> + aliases->namespace]]> + aliases->namespace]]> + template_types]]> + $fq_classlike_name + $function_id + $function_id + $method_name_lc + stmts]]> + stmts]]> + stmts]]> + stmts]]> + aliases->namespace]]> + aliases->namespace]]> + + + + + $type_string + + + + + aliases->uses_start]]> + aliases->uses_start]]> + skip_if_descendants]]> + skip_if_descendants]]> + skip_if_descendants]]> + skip_if_descendants]]> + skip_if_descendants]]> + skip_if_descendants]]> + skip_if_descendants]]> + code_location->file_path, 'CoreGenericClasses.phpstub')]]> + code_location->file_path, 'CoreGenericFunctions.phpstub')]]> + file_path, 'CoreGenericIterators.phpstub')]]> + $cs[0] + + $offset_map + end_change]]> + start_change]]> + @@ -383,6 +1786,87 @@ getOption('config')]]> + + + !$path + $explicit_path + psalm_header]]> + psalm_tag_end_pos]]> + + + + + enabled_plugins]]> + + + + + !$root_cache_directory + $file_contents + $file_path + + + + + !$cache_directory + !$cache_directory + !$cache_directory + $cache_directory + + + + + cache->getFileMapCache()]]> + + + + + !$root_cache_directory + + + + + $result + + + + + $called_method_name + + + + + $extended_var_id + + + + + !$cache_directory + !$root_cache_directory + !$root_cache_directory + !$root_cache_directory + + + + + !$cache_directory + !$cache_directory + composer_lock_hash]]> + $cache_directory + + + + + !$key_column_name + + + + + $callable_extended_var_id + getTemplateTypeMap()]]> + getTemplateTypeMap()]]> + + $callable_method_name @@ -398,6 +1882,73 @@ $method_name + + + $fetch_class_name + + + + + !$call_args + + + + + $existing_file_contents + $existing_file_contents + $existing_file_contents + $existing_statements + $existing_statements + $existing_statements + $existing_statements + $file_changes + $file_path + parse($file_contents, $error_handler)]]> + parse($file_contents, $error_handler)]]> + + + + + + $first_line_padding + + + + + !$resolved_name + $mapped_type = $map[$offset_arg_value] ?? null + $mapped_type = $map[$offset_arg_value] ?? null + + + + + + + cased_name]]> + template_types]]> + parent_class]]> + template_types]]> + + + + + cased_name]]> + cased_name]]> + template_types]]> + + + + + $key + $var_id + $var_id + $var_id + $var_id + $var_id + $var_id + $var_id + + isContainedBy @@ -414,6 +1965,36 @@ TCallable|TClosure|null + + !$class_name + $calling_method_id + $calling_method_id + $calling_method_id + $calling_method_id + $calling_method_id + params]]> + $file_name + $file_name + $input_variadic_param_idx + $member_id + + + + + !($container_type_params_covariant[$i] ?? false) + + + + + $intersection_container_type_lower + + + + + $key + $key + $key + @@ -422,11 +2003,83 @@ $properties[0] $properties[0] + + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $var_id + $var_id + $var_id + $var_id + + + + + + !$count + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $key + $var_id + $var_id + $var_id + $var_id + + + + + template_extended_params]]> + getClassTemplateTypes + + + + $input_template_types + template_extended_params[$container_class])]]> + template_extended_params[$base_type->as_type->value])]]> + template_extended_params[$base_type->value])]]> + + @@ -437,11 +2090,48 @@ array_type_params[1]]]> array_type_params[1]]]> + + class_string_types]]> + floats]]> + ints]]> + named_object_types]]> + strings]]> + array_counts]]> + array_min_counts]]> + array_min_counts]]> + class_string_types]]> + class_string_types]]> + floats]]> + ints]]> + ints]]> + ints]]> + named_object_types]]> + strings]]> + strings]]> + strings]]> + strings]]> + value_types['string'] instanceof TNonFalsyString + ? $type->value + : $type->value !== '']]> + $shared_classlikes + $fallback_params + + template_types]]> + $params + $parent_class + $self_class + $self_class + $self_class + $self_class + $self_class + $self_class + $static_class_type + @@ -455,6 +2145,11 @@ array_keys($template_type_map[$fq_classlike_name])[0] array_keys($template_type_map[$template_param_name])[0] + + $extra_params + value, '::')]]> + value, '::')]]> + @@ -463,6 +2158,59 @@ $type_tokens[$i - 1] $type_tokens[$i - 1] + + $parent_fqcln + $self_fqcln + + + + + + !$fq_classlike_name + template_types]]> + template_types]]> + calling_method_id]]> + + + + + $function_id + + + + + $function_id + + + + + $function_id + + + + + output_path]]> + $parent_issue_type + + + + + other_references]]> + taint_trace]]> + + + + + + other_references]]> + taint_trace]]> + + + + + + taint_trace]]> + @@ -477,6 +2225,19 @@ traverse + + + $this_var_id + + + + + !$namespace + $namespace + $namespace + + + classOrInterfaceExists @@ -495,6 +2256,9 @@ $value + + + @@ -503,6 +2267,10 @@ replace replace + + $params + $params + @@ -513,6 +2281,9 @@ type_params[1]]]> + + !($container_type_params_covariant[$offset] ?? true) + @@ -528,6 +2299,10 @@ replace + + !$namespace + $namespace + @@ -540,6 +2315,12 @@ value_param]]> + + + !$intersection + !$intersection + + replace @@ -550,6 +2331,17 @@ __construct + + + !$intersection + !$intersection + + + + + !$intersection + + TList @@ -591,6 +2383,18 @@ type_param]]> + + + !$namespace + $namespace + + + + + !$intersection + $intersection + + TList @@ -604,12 +2408,21 @@ replace replace + + !$intersection + !$intersection + replace + + + !$intersection + + replace @@ -620,6 +2433,11 @@ replace + + + extra_types]]> + + $allow_mutations @@ -630,11 +2448,24 @@ $initialized_class $reference_free + + + $const_name + + + + $array_key_offset + $failed_reconciliation + + + ')]]> + + @@ -670,10 +2501,41 @@ hasLowercaseString hasLowercaseString - - - - - + + !$php_type + exact_id]]> + id]]> + exact_id]]> + exact_id]]> + id]]> + id]]> + + + + + + + + + + + $level + $php_version + + + + + + + + + + + $param_type_1 + $param_type_2 + $param_type_3 + $param_type_4 + $return_type + From 4dd06f72964eb9098df92356895673d9f836a7d7 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 15 Jan 2024 13:07:06 +0100 Subject: [PATCH 215/357] Revert #10361 --- .../PhpVisitor/Reflector/ClassLikeNodeScanner.php | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index bc0f61bb169..21ab7445e88 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -632,16 +632,11 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool $storage->pseudo_static_methods[$lc_method_name] = $pseudo_method_storage; } else { $storage->pseudo_methods[$lc_method_name] = $pseudo_method_storage; + $storage->declaring_pseudo_method_ids[$lc_method_name] = new MethodIdentifier( + $fq_classlike_name, + $lc_method_name, + ); } - $method_identifier = new MethodIdentifier( - $fq_classlike_name, - $lc_method_name, - ); - $storage->inheritable_method_ids[$lc_method_name] = $method_identifier; - if (!isset($storage->overridden_method_ids[$lc_method_name])) { - $storage->overridden_method_ids[$lc_method_name] = []; - } - $storage->declaring_pseudo_method_ids[$lc_method_name] = $method_identifier; } From c9fe76cadc102d1e7eeaf03bbf17daf6bb1f4931 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 15 Jan 2024 13:16:41 +0100 Subject: [PATCH 216/357] Patch --- .../Expression/Call/ArgumentsAnalyzer.php | 2 +- .../FilterInputReturnTypeProvider.php | 2 + .../ReturnTypeProvider/FilterUtils.php | 39 ++++++++----------- src/Psalm/Issue/RedundantFlag.php | 2 + src/Psalm/Issue/TaintedXpath.php | 2 +- ...UnserializeMemoryUsageSuppressionTrait.php | 2 + ...GetCompletionItemsForClassishThingTest.php | 6 +-- 7 files changed, 28 insertions(+), 27 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 970f3cad650..d8174c71778 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -1260,7 +1260,7 @@ private static function handleByRefReadonlyArg( Context $context, PhpParser\Node\Expr\PropertyFetch $stmt, string $fq_class_name, - string $prop_name + string $prop_name, ): void { $property_id = $fq_class_name . '::$' . $prop_name; diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterInputReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterInputReturnTypeProvider.php index 09b32b2aa65..b3bc3ad48b4 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterInputReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterInputReturnTypeProvider.php @@ -1,5 +1,7 @@ node_data->getType($filter_arg->value); if (!$filter_arg_type) { return null; @@ -154,8 +155,8 @@ public static function getOptionsArgValueOrError( Codebase $codebase, CodeLocation $code_location, string $function_id, - int $filter_int_used - ) { + int $filter_int_used, + ): array|Union|null { $options_arg_type = $statements_analyzer->node_data->getType($options_arg->value); if (!$options_arg_type) { return null; @@ -339,7 +340,7 @@ public static function missingFilterCallbackCallable( string $function_id, CodeLocation $code_location, StatementsAnalyzer $statements_analyzer, - Codebase $codebase + Codebase $codebase, ): Union { IssueBuffer::maybeAdd( new InvalidArgument( @@ -397,7 +398,7 @@ public static function checkRedundantFlags( Union $fails_type, StatementsAnalyzer $statements_analyzer, CodeLocation $code_location, - Codebase $codebase + Codebase $codebase, ): ?Union { $all_filters = self::getFilters($codebase); $flags_int_used_rest = $flags_int_used; @@ -500,7 +501,7 @@ public static function getOptions( StatementsAnalyzer $statements_analyzer, CodeLocation $code_location, Codebase $codebase, - string $function_id + string $function_id, ): array { $default = null; $min_range = null; @@ -605,16 +606,12 @@ public static function getOptions( return [$default, $min_range, $max_range, $has_range, $regexp]; } - /** - * @param float|int|null $min_range - * @param float|int|null $max_range - */ protected static function isRangeValid( - $min_range, - $max_range, + float|int|null $min_range, + float|int|null $max_range, StatementsAnalyzer $statements_analyzer, CodeLocation $code_location, - string $function_id + string $function_id, ): bool { if ($min_range !== null && $max_range !== null && $min_range > $max_range) { IssueBuffer::maybeAdd( @@ -637,8 +634,6 @@ protected static function isRangeValid( * * @psalm-suppress ComplexMethod * @param Union|null $not_set_type null if undefined filtered variable will return $fails_type - * @param float|int|null $min_range - * @param float|int|null $max_range * @param non-falsy-string|true|null $regexp */ public static function getReturnType( @@ -652,10 +647,10 @@ public static function getReturnType( Codebase $codebase, string $function_id, bool $has_range, - $min_range, - $max_range, - $regexp, - bool $in_array_recursion = false + float|int|null $min_range, + float|int|null $max_range, + string|bool|null $regexp, + bool $in_array_recursion = false, ): Union { // if we are inside a recursion of e.g. array // it will never fail or change the type, so we can immediately return @@ -1472,7 +1467,7 @@ private static function addReturnTaint( StatementsAnalyzer $statements_analyzer, CodeLocation $code_location, Union $return_type, - string $function_id + string $function_id, ): Union { if ($statements_analyzer->data_flow_graph && !in_array('TaintedInput', $statements_analyzer->getSuppressedIssues()) diff --git a/src/Psalm/Issue/RedundantFlag.php b/src/Psalm/Issue/RedundantFlag.php index d3f0429636d..7734803ac06 100644 --- a/src/Psalm/Issue/RedundantFlag.php +++ b/src/Psalm/Issue/RedundantFlag.php @@ -1,5 +1,7 @@ Date: Mon, 15 Jan 2024 13:27:53 +0100 Subject: [PATCH 217/357] Cleanup --- psalm-baseline.xml | 32 ++----------------- .../Call/NamedFunctionCallHandler.php | 5 --- .../ReturnTypeProvider/FilterUtils.php | 5 --- tests/MagicMethodAnnotationTest.php | 2 +- 4 files changed, 4 insertions(+), 40 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 1926d014f4c..098c1fdf2e3 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + tags['variablesfrom'][0]]]> @@ -236,9 +236,9 @@ - + - + @@ -385,32 +385,6 @@ $cs[0] - - - $config_file_path !== null - - - getArgument('pluginName')]]> - getOption('config')]]> - - - - - $config_file_path !== null - - - getArgument('pluginName')]]> - getOption('config')]]> - - - - - $config_file_path !== null - - - getOption('config')]]> - - $callable_method_name diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php index fc5049f6747..36b37b59fdc 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php @@ -38,7 +38,6 @@ use Psalm\Type\Atomic\TFloat; use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TKeyedArray; -use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TLowercaseString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; @@ -295,10 +294,6 @@ public static function handle( && $array_type_union->isSingle() ) { foreach ($array_type_union->getAtomicTypes() as $array_type) { - if ($array_type instanceof TList) { - $array_type = $array_type->getKeyedArray(); - } - if ($array_type instanceof TKeyedArray) { foreach ($array_type->properties as $key => $type) { // variables must start with letters or underscore diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php index 3f715feeeed..c026134f434 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php @@ -24,7 +24,6 @@ use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TIntRange; use Psalm\Type\Atomic\TKeyedArray; -use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TLiteralFloat; use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; @@ -666,10 +665,6 @@ public static function getReturnType( && !self::hasFlag($flags_int_used, FILTER_REQUIRE_SCALAR) ) { foreach ($input_type->getAtomicTypes() as $key => $atomic_type) { - if ($atomic_type instanceof TList) { - $atomic_type = $atomic_type->getKeyedArray(); - } - if ($atomic_type instanceof TKeyedArray) { $input_type = $input_type->getBuilder(); $input_type->removeType($key); diff --git a/tests/MagicMethodAnnotationTest.php b/tests/MagicMethodAnnotationTest.php index e6737ab0d78..d246a38a7ce 100644 --- a/tests/MagicMethodAnnotationTest.php +++ b/tests/MagicMethodAnnotationTest.php @@ -1235,7 +1235,7 @@ interface B extends A {} ', 'error_message' => 'ImplementedParamTypeMismatch', ], - 'MagicMethodMadeConcreteChecksParams' => [ + 'SKIPPED-MagicMethodMadeConcreteChecksParams' => [ 'code' => ' Date: Mon, 15 Jan 2024 13:45:08 +0100 Subject: [PATCH 218/357] Update baseline --- psalm-baseline.xml | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 098c1fdf2e3..13b1f97de6a 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + tags['variablesfrom'][0]]]> @@ -635,6 +635,28 @@ hasLowercaseString + + + Mockery::mock(ConfigFile::class) + + + addPlugin + expects + expects + removePlugin + + + config_file]]> + config_file]]> + config_file]]> + config_file]]> + config_file]]> + config_file]]> + config_file]]> + config_file]]> + config_file]]> + + Config From a032c6e1cd04130be5b8c11c7ba3f0eb97a016d4 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 15 Jan 2024 13:47:06 +0100 Subject: [PATCH 219/357] Ignore test issues bypassed by bypass-finals --- psalm-baseline.xml | 23 ++++++++++++----------- tests/Config/PluginListTest.php | 6 +++--- tests/ErrorBaselineTest.php | 2 +- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 13b1f97de6a..77da0c81689 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -636,16 +636,17 @@ - - Mockery::mock(ConfigFile::class) - - - addPlugin - expects - expects - removePlugin - - + + composer_lock]]> + composer_lock]]> + composer_lock]]> + composer_lock]]> + composer_lock]]> + composer_lock]]> + composer_lock]]> + composer_lock]]> + composer_lock]]> + composer_lock]]> config_file]]> config_file]]> config_file]]> @@ -655,7 +656,7 @@ config_file]]> config_file]]> config_file]]> - + diff --git a/tests/Config/PluginListTest.php b/tests/Config/PluginListTest.php index 5ca98ab0ef8..7083f0891a2 100644 --- a/tests/Config/PluginListTest.php +++ b/tests/Config/PluginListTest.php @@ -20,11 +20,11 @@ class PluginListTest extends TestCase { use MockeryPHPUnitIntegration; - private ConfigFile&MockInterface $config_file; + private MockInterface $config_file; - private Config&MockInterface $config; + private MockInterface $config; - private ComposerLock&MockInterface $composer_lock; + private MockInterface $composer_lock; public function setUp(): void { diff --git a/tests/ErrorBaselineTest.php b/tests/ErrorBaselineTest.php index 584748d418e..3f37112e3b4 100644 --- a/tests/ErrorBaselineTest.php +++ b/tests/ErrorBaselineTest.php @@ -23,7 +23,7 @@ class ErrorBaselineTest extends TestCase { use MockeryPHPUnitIntegration; - private FileProvider&MockInterface $fileProvider; + private MockInterface $fileProvider; public function setUp(): void { From 3a263d2cd1543d1e77063dd24df964d171b2e678 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 15 Jan 2024 13:58:32 +0100 Subject: [PATCH 220/357] Bump --- bin/test-with-real-projects.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/test-with-real-projects.sh b/bin/test-with-real-projects.sh index 923312bbea6..5f7263d2a26 100755 --- a/bin/test-with-real-projects.sh +++ b/bin/test-with-real-projects.sh @@ -36,7 +36,7 @@ psl) git clone git@github.com:psalm/endtoend-test-psl.git cd endtoend-test-psl - git checkout 2.3.x + git checkout 2.3.x_master composer install # Avoid conflicts with old psalm when running phar tests rm -rf vendor/vimeo/psalm From adeca9341de6da07d3736c89db11022555e88903 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 15 Jan 2024 14:04:24 +0100 Subject: [PATCH 221/357] Bump --- bin/test-with-real-projects.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bin/test-with-real-projects.sh b/bin/test-with-real-projects.sh index 5f7263d2a26..d22dc0c2d0a 100755 --- a/bin/test-with-real-projects.sh +++ b/bin/test-with-real-projects.sh @@ -26,6 +26,9 @@ collections) git clone --depth=1 git@github.com:psalm/endtoend-test-collections.git cd endtoend-test-collections composer install + rm vendor/amphp/amp/lib/functions.php; touch vendor/amphp/amp/lib/functions.php; + rm vendor/amphp/amp/lib/Internal/functions.php; touch vendor/amphp/amp/lib/Internal/functions.php + rm vendor/amphp/byte-stream/lib/functions.php; touch vendor/amphp/byte-stream/lib/functions.php "$PSALM" --monochrome --show-info=false ;; From 645e59ee538141ff7706e685220f3a7ad8e9ef2c Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 15 Jan 2024 17:36:32 +0100 Subject: [PATCH 222/357] cs-fix --- src/Psalm/Aliases.php | 2 ++ .../Expression/Call/FunctionCallReturnTypeFetcher.php | 1 - .../Statements/Expression/Call/NamedFunctionCallHandler.php | 2 +- src/Psalm/Internal/MethodIdentifier.php | 2 +- src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php | 2 -- src/Psalm/Internal/Type/TypeParser.php | 1 - src/Psalm/IssueBuffer.php | 1 + 7 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Psalm/Aliases.php b/src/Psalm/Aliases.php index 58f7eccc63c..9471120d03e 100644 --- a/src/Psalm/Aliases.php +++ b/src/Psalm/Aliases.php @@ -10,6 +10,8 @@ final class Aliases { use UnserializeMemoryUsageSuppressionTrait; + public ?int $namespace_first_stmt_start = null; + public ?int $uses_start = null; public ?int $uses_end = null; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index 4374ca7cd78..b7de2fd223c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -48,7 +48,6 @@ use function explode; use function in_array; use function str_contains; -use function str_ends_with; use function strlen; use function strtolower; use function substr; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php index 1e608a427c4..80c780de50b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php @@ -55,8 +55,8 @@ use function in_array; use function is_numeric; use function is_string; -use function str_starts_with; use function preg_match; +use function str_starts_with; use function strpos; use function strtolower; diff --git a/src/Psalm/Internal/MethodIdentifier.php b/src/Psalm/Internal/MethodIdentifier.php index b75b4d5209e..b50a9f9f316 100644 --- a/src/Psalm/Internal/MethodIdentifier.php +++ b/src/Psalm/Internal/MethodIdentifier.php @@ -6,8 +6,8 @@ use InvalidArgumentException; use Psalm\Storage\ImmutableNonCloneableTrait; -use Stringable; use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; +use Stringable; use function explode; use function is_string; diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php index a4d6c50f3ca..9ccbd8e6c61 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php @@ -29,9 +29,7 @@ use function dirname; use function explode; use function in_array; -use function preg_match; use function str_contains; -use function strpos; use function strtolower; use function substr; diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 99865754b6e..3f344916582 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -89,7 +89,6 @@ use function end; use function explode; use function filter_var; -use function get_class; use function in_array; use function is_int; use function is_numeric; diff --git a/src/Psalm/IssueBuffer.php b/src/Psalm/IssueBuffer.php index 57a4f9c6f83..dd35480c48f 100644 --- a/src/Psalm/IssueBuffer.php +++ b/src/Psalm/IssueBuffer.php @@ -56,6 +56,7 @@ use function explode; use function file_put_contents; use function fwrite; +use function get_class; use function implode; use function in_array; use function is_dir; From a3a13241a76d19e7e7f0f51bef973f8c9266d594 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 17 Jan 2024 12:41:58 +0100 Subject: [PATCH 223/357] fix baseline somehow incorrect paths --- psalm-baseline.xml | 52 +++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 85f358308e2..ed55df4536a 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,16 +1,16 @@ - + mapper]]> - + $items - + $returnType attrGroups]]> @@ -24,7 +24,7 @@ - + $returnType attrGroups]]> @@ -36,39 +36,39 @@ uses]]> - + $items - + $parts - + $conds - + $parts $parts $parts - + $stmts - + $stmts - + $returnType attrGroups]]> @@ -79,7 +79,7 @@ stmts]]> - + attrGroups]]> extends]]> @@ -88,27 +88,27 @@ stmts]]> - + $stmts - + $stmts - + $stmts - + $stmts - + cond]]> init]]> @@ -116,14 +116,14 @@ stmts]]> - + byRef]]> keyVar]]> stmts]]> - + $returnType attrGroups]]> @@ -133,42 +133,42 @@ stmts]]> - + else]]> elseifs]]> stmts]]> - + attrGroups]]> extends]]> stmts]]> - + $stmts - + attrGroups]]> stmts]]> - + $stmts - + $stmts - + static::getDefaultDescription() static::getDefaultDescription() From 0504394e8b72733aa1e399bf0dba1547bbbccb8b Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 17 Jan 2024 12:52:36 +0100 Subject: [PATCH 224/357] Fixup --- src/Psalm/Internal/PhpTraverser/CustomTraverser.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/PhpTraverser/CustomTraverser.php b/src/Psalm/Internal/PhpTraverser/CustomTraverser.php index f1e2673572d..397601d4e59 100644 --- a/src/Psalm/Internal/PhpTraverser/CustomTraverser.php +++ b/src/Psalm/Internal/PhpTraverser/CustomTraverser.php @@ -29,7 +29,7 @@ public function __construct() * @param Node $node node to traverse * @return Node Result of traversal (may be original node or new one) */ - protected function traverseNode(Node $node): Node + protected function traverseNode(Node $node): void { foreach ($node->getSubNodeNames() as $name) { $subNode = &$node->$name; @@ -60,7 +60,7 @@ protected function traverseNode(Node $node): Node } if ($traverseChildren) { - $subNode = $this->traverseNode($subNode); + $this->traverseNode($subNode); if ($this->stopTraversal) { break; } @@ -88,8 +88,6 @@ protected function traverseNode(Node $node): Node } } } - - return $node; } /** From 4e5e30633bd4798da15d9f2bc51b59fc2fba068b Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 17 Jan 2024 13:29:08 +0100 Subject: [PATCH 225/357] Fixup --- src/Psalm/Internal/Analyzer/ClosureAnalyzer.php | 2 ++ src/Psalm/Internal/Analyzer/FunctionAnalyzer.php | 2 ++ src/Psalm/Internal/Analyzer/MethodAnalyzer.php | 2 ++ src/Psalm/Storage/Assertion/Any.php | 2 ++ src/Psalm/Storage/Assertion/ArrayKeyDoesNotExist.php | 2 ++ src/Psalm/Storage/Assertion/ArrayKeyExists.php | 2 ++ src/Psalm/Storage/Assertion/DoesNotHaveAtLeastCount.php | 2 ++ src/Psalm/Storage/Assertion/DoesNotHaveExactCount.php | 2 ++ src/Psalm/Storage/Assertion/DoesNotHaveMethod.php | 2 ++ src/Psalm/Storage/Assertion/Empty_.php | 2 ++ src/Psalm/Storage/Assertion/Falsy.php | 2 ++ src/Psalm/Storage/Assertion/HasArrayKey.php | 2 ++ src/Psalm/Storage/Assertion/HasAtLeastCount.php | 2 ++ src/Psalm/Storage/Assertion/HasExactCount.php | 2 ++ src/Psalm/Storage/Assertion/HasIntOrStringArrayAccess.php | 2 ++ src/Psalm/Storage/Assertion/HasMethod.php | 2 ++ src/Psalm/Storage/Assertion/HasStringArrayAccess.php | 2 ++ src/Psalm/Storage/Assertion/InArray.php | 2 ++ src/Psalm/Storage/Assertion/IsAClass.php | 2 ++ src/Psalm/Storage/Assertion/IsClassEqual.php | 2 ++ src/Psalm/Storage/Assertion/IsClassNotEqual.php | 2 ++ src/Psalm/Storage/Assertion/IsCountable.php | 2 ++ src/Psalm/Storage/Assertion/IsEqualIsset.php | 2 ++ src/Psalm/Storage/Assertion/IsGreaterThan.php | 2 ++ src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php | 2 ++ src/Psalm/Storage/Assertion/IsIdentical.php | 2 ++ src/Psalm/Storage/Assertion/IsIsset.php | 2 ++ src/Psalm/Storage/Assertion/IsLessThan.php | 2 ++ src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php | 2 ++ src/Psalm/Storage/Assertion/IsLooselyEqual.php | 2 ++ src/Psalm/Storage/Assertion/IsNotAClass.php | 2 ++ src/Psalm/Storage/Assertion/IsNotCountable.php | 2 ++ src/Psalm/Storage/Assertion/IsNotIdentical.php | 2 ++ src/Psalm/Storage/Assertion/IsNotIsset.php | 2 ++ src/Psalm/Storage/Assertion/IsNotLooselyEqual.php | 2 ++ src/Psalm/Storage/Assertion/IsNotType.php | 2 ++ src/Psalm/Storage/Assertion/IsType.php | 2 ++ src/Psalm/Storage/Assertion/NestedAssertions.php | 2 ++ src/Psalm/Storage/Assertion/NonEmpty.php | 2 ++ src/Psalm/Storage/Assertion/NonEmptyCountable.php | 2 ++ src/Psalm/Storage/Assertion/NotInArray.php | 2 ++ src/Psalm/Storage/Assertion/NotNestedAssertions.php | 2 ++ src/Psalm/Storage/Assertion/NotNonEmptyCountable.php | 2 ++ src/Psalm/Storage/Assertion/Truthy.php | 2 ++ src/Psalm/Storage/FunctionStorage.php | 1 + src/Psalm/Storage/MethodStorage.php | 1 + src/Psalm/Type/Atomic/Scalar.php | 2 ++ src/Psalm/Type/Atomic/TArray.php | 2 ++ src/Psalm/Type/Atomic/TCallable.php | 2 ++ src/Psalm/Type/Atomic/TClassConstant.php | 2 ++ src/Psalm/Type/Atomic/TClassStringMap.php | 2 ++ src/Psalm/Type/Atomic/TClosedResource.php | 2 ++ src/Psalm/Type/Atomic/TConditional.php | 2 ++ src/Psalm/Type/Atomic/TIterable.php | 2 ++ src/Psalm/Type/Atomic/TKeyedArray.php | 2 ++ src/Psalm/Type/Atomic/TMixed.php | 2 ++ src/Psalm/Type/Atomic/TNever.php | 2 ++ src/Psalm/Type/Atomic/TNull.php | 2 ++ src/Psalm/Type/Atomic/TObject.php | 2 ++ src/Psalm/Type/Atomic/TPropertiesOf.php | 2 ++ src/Psalm/Type/Atomic/TResource.php | 2 ++ src/Psalm/Type/Atomic/TTemplateIndexedAccess.php | 2 ++ src/Psalm/Type/Atomic/TTemplateKeyOf.php | 2 ++ src/Psalm/Type/Atomic/TTemplateParam.php | 2 ++ src/Psalm/Type/Atomic/TTemplatePropertiesOf.php | 2 ++ src/Psalm/Type/Atomic/TTemplateValueOf.php | 2 ++ src/Psalm/Type/Atomic/TTypeAlias.php | 2 ++ src/Psalm/Type/Atomic/TValueOf.php | 2 ++ src/Psalm/Type/Atomic/TVoid.php | 2 ++ 69 files changed, 136 insertions(+) diff --git a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php index afe483caf49..0149e800c92 100644 --- a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php @@ -14,6 +14,7 @@ use Psalm\Issue\PossiblyUndefinedVariable; use Psalm\Issue\UndefinedVariable; use Psalm\IssueBuffer; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Union; @@ -30,6 +31,7 @@ */ final class ClosureAnalyzer extends FunctionLikeAnalyzer { + use UnserializeMemoryUsageSuppressionTrait; /** * @param PhpParser\Node\Expr\Closure|PhpParser\Node\Expr\ArrowFunction $function */ diff --git a/src/Psalm/Internal/Analyzer/FunctionAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionAnalyzer.php index bcf1395909a..3c8a293b0be 100644 --- a/src/Psalm/Internal/Analyzer/FunctionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionAnalyzer.php @@ -7,6 +7,7 @@ use PhpParser; use Psalm\Config; use Psalm\Context; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use UnexpectedValueException; use function is_string; @@ -18,6 +19,7 @@ */ final class FunctionAnalyzer extends FunctionLikeAnalyzer { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(PhpParser\Node\Stmt\Function_ $function, SourceAnalyzer $source) { $codebase = $source->getCodebase(); diff --git a/src/Psalm/Internal/Analyzer/MethodAnalyzer.php b/src/Psalm/Internal/Analyzer/MethodAnalyzer.php index 3758cd06bef..1c68bee7a7b 100644 --- a/src/Psalm/Internal/Analyzer/MethodAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/MethodAnalyzer.php @@ -20,6 +20,7 @@ use Psalm\StatementsSource; use Psalm\Storage\ClassLikeStorage; use Psalm\Storage\MethodStorage; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use UnexpectedValueException; use function in_array; @@ -31,6 +32,7 @@ */ final class MethodAnalyzer extends FunctionLikeAnalyzer { + use UnserializeMemoryUsageSuppressionTrait; // https://github.com/php/php-src/blob/a83923044c48982c80804ae1b45e761c271966d3/Zend/zend_enum.c#L77-L95 private const FORBIDDEN_ENUM_METHODS = [ '__construct', diff --git a/src/Psalm/Storage/Assertion/Any.php b/src/Psalm/Storage/Assertion/Any.php index ad9e4e88c13..b8e3e3e584f 100644 --- a/src/Psalm/Storage/Assertion/Any.php +++ b/src/Psalm/Storage/Assertion/Any.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class Any extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return $this; diff --git a/src/Psalm/Storage/Assertion/ArrayKeyDoesNotExist.php b/src/Psalm/Storage/Assertion/ArrayKeyDoesNotExist.php index 73f0e3e9cea..53e7cdffb72 100644 --- a/src/Psalm/Storage/Assertion/ArrayKeyDoesNotExist.php +++ b/src/Psalm/Storage/Assertion/ArrayKeyDoesNotExist.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class ArrayKeyDoesNotExist extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return new ArrayKeyExists(); diff --git a/src/Psalm/Storage/Assertion/ArrayKeyExists.php b/src/Psalm/Storage/Assertion/ArrayKeyExists.php index aab0e3f01db..c21d4428e47 100644 --- a/src/Psalm/Storage/Assertion/ArrayKeyExists.php +++ b/src/Psalm/Storage/Assertion/ArrayKeyExists.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class ArrayKeyExists extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return new ArrayKeyDoesNotExist(); diff --git a/src/Psalm/Storage/Assertion/DoesNotHaveAtLeastCount.php b/src/Psalm/Storage/Assertion/DoesNotHaveAtLeastCount.php index 01e55a42192..b37c997134a 100644 --- a/src/Psalm/Storage/Assertion/DoesNotHaveAtLeastCount.php +++ b/src/Psalm/Storage/Assertion/DoesNotHaveAtLeastCount.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class DoesNotHaveAtLeastCount extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; /** @param positive-int $count */ public function __construct(public readonly int $count) { diff --git a/src/Psalm/Storage/Assertion/DoesNotHaveExactCount.php b/src/Psalm/Storage/Assertion/DoesNotHaveExactCount.php index 125b0bd8f1e..e557085cd3f 100644 --- a/src/Psalm/Storage/Assertion/DoesNotHaveExactCount.php +++ b/src/Psalm/Storage/Assertion/DoesNotHaveExactCount.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class DoesNotHaveExactCount extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; /** @param positive-int $count */ public function __construct(public readonly int $count) { diff --git a/src/Psalm/Storage/Assertion/DoesNotHaveMethod.php b/src/Psalm/Storage/Assertion/DoesNotHaveMethod.php index 72b5e0e20ba..4a6a891c46e 100644 --- a/src/Psalm/Storage/Assertion/DoesNotHaveMethod.php +++ b/src/Psalm/Storage/Assertion/DoesNotHaveMethod.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class DoesNotHaveMethod extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public readonly string $method) { } diff --git a/src/Psalm/Storage/Assertion/Empty_.php b/src/Psalm/Storage/Assertion/Empty_.php index 344b99d6b08..53bca41fada 100644 --- a/src/Psalm/Storage/Assertion/Empty_.php +++ b/src/Psalm/Storage/Assertion/Empty_.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class Empty_ extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return new NonEmpty(); diff --git a/src/Psalm/Storage/Assertion/Falsy.php b/src/Psalm/Storage/Assertion/Falsy.php index d758b8352ee..5ac93ba421e 100644 --- a/src/Psalm/Storage/Assertion/Falsy.php +++ b/src/Psalm/Storage/Assertion/Falsy.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class Falsy extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return new Truthy(); diff --git a/src/Psalm/Storage/Assertion/HasArrayKey.php b/src/Psalm/Storage/Assertion/HasArrayKey.php index 1326f09510e..8b0bb509651 100644 --- a/src/Psalm/Storage/Assertion/HasArrayKey.php +++ b/src/Psalm/Storage/Assertion/HasArrayKey.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use UnexpectedValueException; /** @@ -12,6 +13,7 @@ */ final class HasArrayKey extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public readonly string $key) { } diff --git a/src/Psalm/Storage/Assertion/HasAtLeastCount.php b/src/Psalm/Storage/Assertion/HasAtLeastCount.php index 479d5191866..98581348f93 100644 --- a/src/Psalm/Storage/Assertion/HasAtLeastCount.php +++ b/src/Psalm/Storage/Assertion/HasAtLeastCount.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class HasAtLeastCount extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; /** @param positive-int $count */ public function __construct(public readonly int $count) { diff --git a/src/Psalm/Storage/Assertion/HasExactCount.php b/src/Psalm/Storage/Assertion/HasExactCount.php index 9bb5b8edda5..8f28be407fb 100644 --- a/src/Psalm/Storage/Assertion/HasExactCount.php +++ b/src/Psalm/Storage/Assertion/HasExactCount.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class HasExactCount extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; /** @param positive-int $count */ public function __construct(public readonly int $count) { diff --git a/src/Psalm/Storage/Assertion/HasIntOrStringArrayAccess.php b/src/Psalm/Storage/Assertion/HasIntOrStringArrayAccess.php index db401486ad7..39be9c6d610 100644 --- a/src/Psalm/Storage/Assertion/HasIntOrStringArrayAccess.php +++ b/src/Psalm/Storage/Assertion/HasIntOrStringArrayAccess.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use UnexpectedValueException; /** @@ -12,6 +13,7 @@ */ final class HasIntOrStringArrayAccess extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { throw new UnexpectedValueException('This should never be called'); diff --git a/src/Psalm/Storage/Assertion/HasMethod.php b/src/Psalm/Storage/Assertion/HasMethod.php index 87e090cadcb..ed994ce149e 100644 --- a/src/Psalm/Storage/Assertion/HasMethod.php +++ b/src/Psalm/Storage/Assertion/HasMethod.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class HasMethod extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public readonly string $method) { } diff --git a/src/Psalm/Storage/Assertion/HasStringArrayAccess.php b/src/Psalm/Storage/Assertion/HasStringArrayAccess.php index 1d2b519b72a..3a8e65a2813 100644 --- a/src/Psalm/Storage/Assertion/HasStringArrayAccess.php +++ b/src/Psalm/Storage/Assertion/HasStringArrayAccess.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use UnexpectedValueException; /** @@ -12,6 +13,7 @@ */ final class HasStringArrayAccess extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { throw new UnexpectedValueException('This should never be called'); diff --git a/src/Psalm/Storage/Assertion/InArray.php b/src/Psalm/Storage/Assertion/InArray.php index 021f18f1d58..6c88a4f3d66 100644 --- a/src/Psalm/Storage/Assertion/InArray.php +++ b/src/Psalm/Storage/Assertion/InArray.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Union; /** @@ -12,6 +13,7 @@ */ final class InArray extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public readonly Union $type) { } diff --git a/src/Psalm/Storage/Assertion/IsAClass.php b/src/Psalm/Storage/Assertion/IsAClass.php index 1909c7eb000..905302771c0 100644 --- a/src/Psalm/Storage/Assertion/IsAClass.php +++ b/src/Psalm/Storage/Assertion/IsAClass.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -12,6 +13,7 @@ */ final class IsAClass extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; /** @param Atomic\TTemplateParamClass|Atomic\TNamedObject $type */ public function __construct(public readonly Atomic $type, public readonly bool $allow_string) { diff --git a/src/Psalm/Storage/Assertion/IsClassEqual.php b/src/Psalm/Storage/Assertion/IsClassEqual.php index fc117d1506a..88e8d1527c8 100644 --- a/src/Psalm/Storage/Assertion/IsClassEqual.php +++ b/src/Psalm/Storage/Assertion/IsClassEqual.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class IsClassEqual extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public readonly string $type) { } diff --git a/src/Psalm/Storage/Assertion/IsClassNotEqual.php b/src/Psalm/Storage/Assertion/IsClassNotEqual.php index e5ccaa42130..d94fb6246ef 100644 --- a/src/Psalm/Storage/Assertion/IsClassNotEqual.php +++ b/src/Psalm/Storage/Assertion/IsClassNotEqual.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class IsClassNotEqual extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public readonly string $type) { } diff --git a/src/Psalm/Storage/Assertion/IsCountable.php b/src/Psalm/Storage/Assertion/IsCountable.php index 552d7904ca4..f1f213df5b2 100644 --- a/src/Psalm/Storage/Assertion/IsCountable.php +++ b/src/Psalm/Storage/Assertion/IsCountable.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class IsCountable extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return new IsNotCountable(true); diff --git a/src/Psalm/Storage/Assertion/IsEqualIsset.php b/src/Psalm/Storage/Assertion/IsEqualIsset.php index ce2a2f27901..7cc82cacb8c 100644 --- a/src/Psalm/Storage/Assertion/IsEqualIsset.php +++ b/src/Psalm/Storage/Assertion/IsEqualIsset.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class IsEqualIsset extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return new Any(); diff --git a/src/Psalm/Storage/Assertion/IsGreaterThan.php b/src/Psalm/Storage/Assertion/IsGreaterThan.php index fa3087475e3..a4d4fb153f8 100644 --- a/src/Psalm/Storage/Assertion/IsGreaterThan.php +++ b/src/Psalm/Storage/Assertion/IsGreaterThan.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class IsGreaterThan extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public readonly int $value) { } diff --git a/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php b/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php index 3d4dde1bd92..295ad5eb444 100644 --- a/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php +++ b/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class IsGreaterThanOrEqualTo extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public readonly int $value) { } diff --git a/src/Psalm/Storage/Assertion/IsIdentical.php b/src/Psalm/Storage/Assertion/IsIdentical.php index de3c3db39f5..86f5212407a 100644 --- a/src/Psalm/Storage/Assertion/IsIdentical.php +++ b/src/Psalm/Storage/Assertion/IsIdentical.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -12,6 +13,7 @@ */ final class IsIdentical extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public readonly Atomic $type) { } diff --git a/src/Psalm/Storage/Assertion/IsIsset.php b/src/Psalm/Storage/Assertion/IsIsset.php index be7c11bedb0..01fc40467e7 100644 --- a/src/Psalm/Storage/Assertion/IsIsset.php +++ b/src/Psalm/Storage/Assertion/IsIsset.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class IsIsset extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return new IsNotIsset(); diff --git a/src/Psalm/Storage/Assertion/IsLessThan.php b/src/Psalm/Storage/Assertion/IsLessThan.php index 508b3f8031c..5587ad32ce4 100644 --- a/src/Psalm/Storage/Assertion/IsLessThan.php +++ b/src/Psalm/Storage/Assertion/IsLessThan.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class IsLessThan extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public readonly int $value) { } diff --git a/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php b/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php index d6934334e71..2ef344bf3c1 100644 --- a/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php +++ b/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class IsLessThanOrEqualTo extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public readonly int $value) { } diff --git a/src/Psalm/Storage/Assertion/IsLooselyEqual.php b/src/Psalm/Storage/Assertion/IsLooselyEqual.php index a5d5b6ac4ed..4fd2aa367fb 100644 --- a/src/Psalm/Storage/Assertion/IsLooselyEqual.php +++ b/src/Psalm/Storage/Assertion/IsLooselyEqual.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -12,6 +13,7 @@ */ final class IsLooselyEqual extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public readonly Atomic $type) { } diff --git a/src/Psalm/Storage/Assertion/IsNotAClass.php b/src/Psalm/Storage/Assertion/IsNotAClass.php index 80303df369c..d710eb7eea7 100644 --- a/src/Psalm/Storage/Assertion/IsNotAClass.php +++ b/src/Psalm/Storage/Assertion/IsNotAClass.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -12,6 +13,7 @@ */ final class IsNotAClass extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; /** @param Atomic\TTemplateParamClass|Atomic\TNamedObject $type */ public function __construct(public readonly Atomic $type, public readonly bool $allow_string) { diff --git a/src/Psalm/Storage/Assertion/IsNotCountable.php b/src/Psalm/Storage/Assertion/IsNotCountable.php index 5f11cf6df6b..bf9b4db9a04 100644 --- a/src/Psalm/Storage/Assertion/IsNotCountable.php +++ b/src/Psalm/Storage/Assertion/IsNotCountable.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class IsNotCountable extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public readonly bool $is_negatable) { } diff --git a/src/Psalm/Storage/Assertion/IsNotIdentical.php b/src/Psalm/Storage/Assertion/IsNotIdentical.php index 11c482d1c04..22b6e7c02d1 100644 --- a/src/Psalm/Storage/Assertion/IsNotIdentical.php +++ b/src/Psalm/Storage/Assertion/IsNotIdentical.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -12,6 +13,7 @@ */ final class IsNotIdentical extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public readonly Atomic $type) { } diff --git a/src/Psalm/Storage/Assertion/IsNotIsset.php b/src/Psalm/Storage/Assertion/IsNotIsset.php index d73aa64b68d..890b3fdb1e1 100644 --- a/src/Psalm/Storage/Assertion/IsNotIsset.php +++ b/src/Psalm/Storage/Assertion/IsNotIsset.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class IsNotIsset extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return new IsIsset(); diff --git a/src/Psalm/Storage/Assertion/IsNotLooselyEqual.php b/src/Psalm/Storage/Assertion/IsNotLooselyEqual.php index bb8d4ee7d0e..63a5a78f8c4 100644 --- a/src/Psalm/Storage/Assertion/IsNotLooselyEqual.php +++ b/src/Psalm/Storage/Assertion/IsNotLooselyEqual.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -12,6 +13,7 @@ */ final class IsNotLooselyEqual extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public readonly Atomic $type) { } diff --git a/src/Psalm/Storage/Assertion/IsNotType.php b/src/Psalm/Storage/Assertion/IsNotType.php index 1d51e037017..28a769ca26e 100644 --- a/src/Psalm/Storage/Assertion/IsNotType.php +++ b/src/Psalm/Storage/Assertion/IsNotType.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -12,6 +13,7 @@ */ final class IsNotType extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public readonly Atomic $type) { } diff --git a/src/Psalm/Storage/Assertion/IsType.php b/src/Psalm/Storage/Assertion/IsType.php index 70920806743..0bc6c63c9a1 100644 --- a/src/Psalm/Storage/Assertion/IsType.php +++ b/src/Psalm/Storage/Assertion/IsType.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -12,6 +13,7 @@ */ final class IsType extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public readonly Atomic $type) { } diff --git a/src/Psalm/Storage/Assertion/NestedAssertions.php b/src/Psalm/Storage/Assertion/NestedAssertions.php index 353807d2401..7d6f405caa2 100644 --- a/src/Psalm/Storage/Assertion/NestedAssertions.php +++ b/src/Psalm/Storage/Assertion/NestedAssertions.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use function json_encode; @@ -15,6 +16,7 @@ */ final class NestedAssertions extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; /** @param array>> $assertions */ public function __construct(public readonly array $assertions) { diff --git a/src/Psalm/Storage/Assertion/NonEmpty.php b/src/Psalm/Storage/Assertion/NonEmpty.php index ac208f4d2a3..159da72e798 100644 --- a/src/Psalm/Storage/Assertion/NonEmpty.php +++ b/src/Psalm/Storage/Assertion/NonEmpty.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class NonEmpty extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return new Empty_(); diff --git a/src/Psalm/Storage/Assertion/NonEmptyCountable.php b/src/Psalm/Storage/Assertion/NonEmptyCountable.php index a36bffdd2a6..8ab74c5bfce 100644 --- a/src/Psalm/Storage/Assertion/NonEmptyCountable.php +++ b/src/Psalm/Storage/Assertion/NonEmptyCountable.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class NonEmptyCountable extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public readonly bool $is_negatable) { } diff --git a/src/Psalm/Storage/Assertion/NotInArray.php b/src/Psalm/Storage/Assertion/NotInArray.php index 17c385f8825..fd47839f84e 100644 --- a/src/Psalm/Storage/Assertion/NotInArray.php +++ b/src/Psalm/Storage/Assertion/NotInArray.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Union; /** @@ -12,6 +13,7 @@ */ final class NotInArray extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function __construct( public readonly Union $type, ) { diff --git a/src/Psalm/Storage/Assertion/NotNestedAssertions.php b/src/Psalm/Storage/Assertion/NotNestedAssertions.php index 4ca457dcf02..acf8696b1ea 100644 --- a/src/Psalm/Storage/Assertion/NotNestedAssertions.php +++ b/src/Psalm/Storage/Assertion/NotNestedAssertions.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use function json_encode; @@ -15,6 +16,7 @@ */ final class NotNestedAssertions extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; /** @param array>> $assertions */ public function __construct(public readonly array $assertions) { diff --git a/src/Psalm/Storage/Assertion/NotNonEmptyCountable.php b/src/Psalm/Storage/Assertion/NotNonEmptyCountable.php index 1306167789c..b9eb20f26b5 100644 --- a/src/Psalm/Storage/Assertion/NotNonEmptyCountable.php +++ b/src/Psalm/Storage/Assertion/NotNonEmptyCountable.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class NotNonEmptyCountable extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return new NonEmptyCountable(true); diff --git a/src/Psalm/Storage/Assertion/Truthy.php b/src/Psalm/Storage/Assertion/Truthy.php index 35ec5d32e3c..c1e62224f2c 100644 --- a/src/Psalm/Storage/Assertion/Truthy.php +++ b/src/Psalm/Storage/Assertion/Truthy.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class Truthy extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return new Falsy(); diff --git a/src/Psalm/Storage/FunctionStorage.php b/src/Psalm/Storage/FunctionStorage.php index f9c9dcfeeb2..043bb1832ba 100644 --- a/src/Psalm/Storage/FunctionStorage.php +++ b/src/Psalm/Storage/FunctionStorage.php @@ -6,6 +6,7 @@ final class FunctionStorage extends FunctionLikeStorage { + use UnserializeMemoryUsageSuppressionTrait; /** @var array */ public array $byref_uses = []; } diff --git a/src/Psalm/Storage/MethodStorage.php b/src/Psalm/Storage/MethodStorage.php index c6ebc2d8aab..3dd5986db9a 100644 --- a/src/Psalm/Storage/MethodStorage.php +++ b/src/Psalm/Storage/MethodStorage.php @@ -8,6 +8,7 @@ final class MethodStorage extends FunctionLikeStorage { + use UnserializeMemoryUsageSuppressionTrait; public bool $is_static = false; public int $visibility = 0; diff --git a/src/Psalm/Type/Atomic/Scalar.php b/src/Psalm/Type/Atomic/Scalar.php index 739bb9bb514..c4597dffbe7 100644 --- a/src/Psalm/Type/Atomic/Scalar.php +++ b/src/Psalm/Type/Atomic/Scalar.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -11,6 +12,7 @@ */ abstract class Scalar extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool { return true; diff --git a/src/Psalm/Type/Atomic/TArray.php b/src/Psalm/Type/Atomic/TArray.php index 0aee4453e40..37cf0bc694f 100644 --- a/src/Psalm/Type/Atomic/TArray.php +++ b/src/Psalm/Type/Atomic/TArray.php @@ -7,6 +7,7 @@ use Psalm\Codebase; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\TemplateResult; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -19,6 +20,7 @@ */ class TArray extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; /** * @use GenericTrait */ diff --git a/src/Psalm/Type/Atomic/TCallable.php b/src/Psalm/Type/Atomic/TCallable.php index 90fb430a291..cbf8141d1db 100644 --- a/src/Psalm/Type/Atomic/TCallable.php +++ b/src/Psalm/Type/Atomic/TCallable.php @@ -8,6 +8,7 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\TemplateResult; use Psalm\Storage\FunctionLikeParameter; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -18,6 +19,7 @@ */ final class TCallable extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; use CallableTrait; public string $value; diff --git a/src/Psalm/Type/Atomic/TClassConstant.php b/src/Psalm/Type/Atomic/TClassConstant.php index 21e8c0a25a5..6acc150a2ec 100644 --- a/src/Psalm/Type/Atomic/TClassConstant.php +++ b/src/Psalm/Type/Atomic/TClassConstant.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type; use Psalm\Type\Atomic; @@ -14,6 +15,7 @@ */ final class TClassConstant extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function __construct( public string $fq_classlike_name, public string $const_name, diff --git a/src/Psalm/Type/Atomic/TClassStringMap.php b/src/Psalm/Type/Atomic/TClassStringMap.php index aae43a24b99..e64f0c60313 100644 --- a/src/Psalm/Type/Atomic/TClassStringMap.php +++ b/src/Psalm/Type/Atomic/TClassStringMap.php @@ -9,6 +9,7 @@ use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TemplateStandinTypeReplacer; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -21,6 +22,7 @@ */ final class TClassStringMap extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; /** * Constructs a new instance of a list */ diff --git a/src/Psalm/Type/Atomic/TClosedResource.php b/src/Psalm/Type/Atomic/TClosedResource.php index 3bde7d5508e..ffdf3697f27 100644 --- a/src/Psalm/Type/Atomic/TClosedResource.php +++ b/src/Psalm/Type/Atomic/TClosedResource.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -13,6 +14,7 @@ */ final class TClosedResource extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function getKey(bool $include_extra = true): string { return 'closed-resource'; diff --git a/src/Psalm/Type/Atomic/TConditional.php b/src/Psalm/Type/Atomic/TConditional.php index 4d6fc9f9afb..6509a574724 100644 --- a/src/Psalm/Type/Atomic/TConditional.php +++ b/src/Psalm/Type/Atomic/TConditional.php @@ -7,6 +7,7 @@ use Psalm\Codebase; use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\TemplateResult; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -17,6 +18,7 @@ */ final class TConditional extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function __construct( public string $param_name, public string $defining_class, diff --git a/src/Psalm/Type/Atomic/TIterable.php b/src/Psalm/Type/Atomic/TIterable.php index a9b9ae988a4..52878f27caa 100644 --- a/src/Psalm/Type/Atomic/TIterable.php +++ b/src/Psalm/Type/Atomic/TIterable.php @@ -7,6 +7,7 @@ use Psalm\Codebase; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\TemplateResult; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -22,6 +23,7 @@ */ final class TIterable extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; use HasIntersectionTrait; /** * @use GenericTrait diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index 9a7da6213f0..189784ec3f0 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -10,6 +10,7 @@ use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Internal\Type\TypeCombiner; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -32,6 +33,7 @@ */ class TKeyedArray extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; /** * If the shape has fallback params then they are here * diff --git a/src/Psalm/Type/Atomic/TMixed.php b/src/Psalm/Type/Atomic/TMixed.php index 579c8a34737..a1019583718 100644 --- a/src/Psalm/Type/Atomic/TMixed.php +++ b/src/Psalm/Type/Atomic/TMixed.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -13,6 +14,7 @@ */ class TMixed extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public bool $from_loop_isset = false, bool $from_docblock = false) { parent::__construct($from_docblock); diff --git a/src/Psalm/Type/Atomic/TNever.php b/src/Psalm/Type/Atomic/TNever.php index a9918aeb8e2..6121a752b05 100644 --- a/src/Psalm/Type/Atomic/TNever.php +++ b/src/Psalm/Type/Atomic/TNever.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -14,6 +15,7 @@ */ final class TNever extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function getKey(bool $include_extra = true): string { return 'never'; diff --git a/src/Psalm/Type/Atomic/TNull.php b/src/Psalm/Type/Atomic/TNull.php index 4f8df9abe72..e8ee44fef9a 100644 --- a/src/Psalm/Type/Atomic/TNull.php +++ b/src/Psalm/Type/Atomic/TNull.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -13,6 +14,7 @@ */ final class TNull extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function getKey(bool $include_extra = true): string { return 'null'; diff --git a/src/Psalm/Type/Atomic/TObject.php b/src/Psalm/Type/Atomic/TObject.php index 14b5036a75e..e6c4556aa85 100644 --- a/src/Psalm/Type/Atomic/TObject.php +++ b/src/Psalm/Type/Atomic/TObject.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -13,6 +14,7 @@ */ class TObject extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function getKey(bool $include_extra = true): string { return 'object'; diff --git a/src/Psalm/Type/Atomic/TPropertiesOf.php b/src/Psalm/Type/Atomic/TPropertiesOf.php index 7724143c1b9..c9df9d0bd32 100644 --- a/src/Psalm/Type/Atomic/TPropertiesOf.php +++ b/src/Psalm/Type/Atomic/TPropertiesOf.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -15,6 +16,7 @@ */ final class TPropertiesOf extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; // These should match the values of // `Psalm\Internal\Analyzer\ClassLikeAnalyzer::VISIBILITY_*`, as they are // used to compared against properties visibililty. diff --git a/src/Psalm/Type/Atomic/TResource.php b/src/Psalm/Type/Atomic/TResource.php index b01a0987bde..f67527df27a 100644 --- a/src/Psalm/Type/Atomic/TResource.php +++ b/src/Psalm/Type/Atomic/TResource.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -13,6 +14,7 @@ */ final class TResource extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function getKey(bool $include_extra = true): string { return 'resource'; diff --git a/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php b/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php index c572c185234..102b8c0e5af 100644 --- a/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php +++ b/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -11,6 +12,7 @@ */ final class TTemplateIndexedAccess extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function __construct( public string $array_param_name, public string $offset_param_name, diff --git a/src/Psalm/Type/Atomic/TTemplateKeyOf.php b/src/Psalm/Type/Atomic/TTemplateKeyOf.php index 4773dc9d4df..c6a728c88a2 100644 --- a/src/Psalm/Type/Atomic/TTemplateKeyOf.php +++ b/src/Psalm/Type/Atomic/TTemplateKeyOf.php @@ -7,6 +7,7 @@ use Psalm\Codebase; use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\TemplateResult; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -17,6 +18,7 @@ */ final class TTemplateKeyOf extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function __construct( public string $param_name, public string $defining_class, diff --git a/src/Psalm/Type/Atomic/TTemplateParam.php b/src/Psalm/Type/Atomic/TTemplateParam.php index 9f618b3090b..84169899a52 100644 --- a/src/Psalm/Type/Atomic/TTemplateParam.php +++ b/src/Psalm/Type/Atomic/TTemplateParam.php @@ -6,6 +6,7 @@ use Psalm\Codebase; use Psalm\Internal\Type\TemplateResult; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -19,6 +20,7 @@ */ final class TTemplateParam extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; use HasIntersectionTrait; /** diff --git a/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php b/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php index 4efe92a602a..48cd32f857d 100644 --- a/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php +++ b/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php @@ -7,6 +7,7 @@ use Psalm\Codebase; use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\TemplateResult; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -17,6 +18,7 @@ */ final class TTemplatePropertiesOf extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; /** * @param TPropertiesOf::VISIBILITY_*|null $visibility_filter */ diff --git a/src/Psalm/Type/Atomic/TTemplateValueOf.php b/src/Psalm/Type/Atomic/TTemplateValueOf.php index 583d079578b..23d33af5de0 100644 --- a/src/Psalm/Type/Atomic/TTemplateValueOf.php +++ b/src/Psalm/Type/Atomic/TTemplateValueOf.php @@ -7,6 +7,7 @@ use Psalm\Codebase; use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\TemplateResult; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -17,6 +18,7 @@ */ final class TTemplateValueOf extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function __construct( public string $param_name, public string $defining_class, diff --git a/src/Psalm/Type/Atomic/TTypeAlias.php b/src/Psalm/Type/Atomic/TTypeAlias.php index ebda8e4118b..9a76d80405e 100644 --- a/src/Psalm/Type/Atomic/TTypeAlias.php +++ b/src/Psalm/Type/Atomic/TTypeAlias.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -11,6 +12,7 @@ */ final class TTypeAlias extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function __construct( public string $declaring_fq_classlike_name, public string $alias_name, diff --git a/src/Psalm/Type/Atomic/TValueOf.php b/src/Psalm/Type/Atomic/TValueOf.php index 38e09570959..55a9d8d6ab0 100644 --- a/src/Psalm/Type/Atomic/TValueOf.php +++ b/src/Psalm/Type/Atomic/TValueOf.php @@ -6,6 +6,7 @@ use Psalm\Codebase; use Psalm\Storage\EnumCaseStorage; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -20,6 +21,7 @@ */ final class TValueOf extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public Union $type, bool $from_docblock = false) { parent::__construct($from_docblock); diff --git a/src/Psalm/Type/Atomic/TVoid.php b/src/Psalm/Type/Atomic/TVoid.php index cac95d62293..d1cd2faf60c 100644 --- a/src/Psalm/Type/Atomic/TVoid.php +++ b/src/Psalm/Type/Atomic/TVoid.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -13,6 +14,7 @@ */ final class TVoid extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function getKey(bool $include_extra = true): string { return 'void'; From 823c9180fd801bf8bee948afb57a8b9673f38af0 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 17 Jan 2024 13:32:15 +0100 Subject: [PATCH 226/357] Fixup --- src/Psalm/Internal/PhpTraverser/CustomTraverser.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/PhpTraverser/CustomTraverser.php b/src/Psalm/Internal/PhpTraverser/CustomTraverser.php index 397601d4e59..f1e2673572d 100644 --- a/src/Psalm/Internal/PhpTraverser/CustomTraverser.php +++ b/src/Psalm/Internal/PhpTraverser/CustomTraverser.php @@ -29,7 +29,7 @@ public function __construct() * @param Node $node node to traverse * @return Node Result of traversal (may be original node or new one) */ - protected function traverseNode(Node $node): void + protected function traverseNode(Node $node): Node { foreach ($node->getSubNodeNames() as $name) { $subNode = &$node->$name; @@ -60,7 +60,7 @@ protected function traverseNode(Node $node): void } if ($traverseChildren) { - $this->traverseNode($subNode); + $subNode = $this->traverseNode($subNode); if ($this->stopTraversal) { break; } @@ -88,6 +88,8 @@ protected function traverseNode(Node $node): void } } } + + return $node; } /** From 950293c6e74c6e9db842f537c5722755b1594313 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 17 Jan 2024 13:37:23 +0100 Subject: [PATCH 227/357] Fixup test --- tests/ReportOutputTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ReportOutputTest.php b/tests/ReportOutputTest.php index 0f0461987c1..b30aa993e9d 100644 --- a/tests/ReportOutputTest.php +++ b/tests/ReportOutputTest.php @@ -757,7 +757,7 @@ public function testJsonReport(): void 'other_references' => null, ], [ - 'link' => 'https://psalm.dev/047', + 'link' => 'https://psalm.dev/020', 'severity' => 'error', 'line_from' => 8, 'line_to' => 8, From 2a7d6d2750d5745539f739c403a2309e50c10e87 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 17 Jan 2024 13:45:40 +0100 Subject: [PATCH 228/357] cs-fix --- src/Psalm/Issue/RiskyTruthyFalsyComparison.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Psalm/Issue/RiskyTruthyFalsyComparison.php b/src/Psalm/Issue/RiskyTruthyFalsyComparison.php index 68ab4e1322b..1fe4865beb0 100644 --- a/src/Psalm/Issue/RiskyTruthyFalsyComparison.php +++ b/src/Psalm/Issue/RiskyTruthyFalsyComparison.php @@ -1,5 +1,7 @@ Date: Wed, 17 Jan 2024 13:52:58 +0100 Subject: [PATCH 229/357] Try bumping PHP on windows CI --- .github/workflows/windows-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index 3314de45861..70923db72b8 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -54,7 +54,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.1' + php-version: '8.3' ini-values: zend.assertions=1, assert.exception=1, opcache.enable_cli=1, opcache.jit=function, opcache.jit_buffer_size=512M tools: composer:v2 coverage: none From 58c5598405580aed08ec86cd82567f2e2e913f85 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 17 Jan 2024 17:01:31 +0100 Subject: [PATCH 230/357] Try disabling opcache --- .github/workflows/windows-ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index 70923db72b8..7fefd1f13f9 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -54,8 +54,9 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.3' - ini-values: zend.assertions=1, assert.exception=1, opcache.enable_cli=1, opcache.jit=function, opcache.jit_buffer_size=512M + php-version: '8.1' + #ini-values: zend.assertions=1, assert.exception=1, opcache.enable_cli=1, opcache.jit=function, opcache.jit_buffer_size=512M + ini-values: zend.assertions=1, assert.exception=1 tools: composer:v2 coverage: none extensions: none, curl, dom, filter, intl, json, libxml, mbstring, openssl, opcache, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter From 3411dd8eb5b73666a47979893facc2912357809f Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 17 Jan 2024 17:07:48 +0100 Subject: [PATCH 231/357] Skip opcache on windows --- .github/workflows/windows-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index 7fefd1f13f9..c5bd292b267 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -59,7 +59,8 @@ jobs: ini-values: zend.assertions=1, assert.exception=1 tools: composer:v2 coverage: none - extensions: none, curl, dom, filter, intl, json, libxml, mbstring, openssl, opcache, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter + #extensions: none, curl, dom, filter, intl, json, libxml, mbstring, openssl, opcache, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter + extensions: none, curl, dom, filter, intl, json, libxml, mbstring, openssl, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter env: fail-fast: true From d2c3f8900098ba300963223db4876a805ec33c17 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 17 Jan 2024 18:23:17 +0100 Subject: [PATCH 232/357] Do not use JIT on windows --- src/Psalm/Internal/Cli/Psalm.php | 3 ++- src/Psalm/Internal/Fork/PsalmRestarter.php | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Cli/Psalm.php b/src/Psalm/Internal/Cli/Psalm.php index 8ca086c47fd..a37cb8a828a 100644 --- a/src/Psalm/Internal/Cli/Psalm.php +++ b/src/Psalm/Internal/Cli/Psalm.php @@ -45,6 +45,7 @@ use function array_values; use function chdir; use function count; +use function defined; use function extension_loaded; use function file_exists; use function file_put_contents; @@ -898,7 +899,7 @@ private static function restart(array $options, int $threads, Progress $progress // If Xdebug is enabled, restart without it $ini_handler->check(); - if (!function_exists('opcache_get_status')) { + if (!function_exists('opcache_get_status') && !defined('PHP_WINDOWS_VERSION_MAJOR')) { $progress->write(PHP_EOL . 'Install the opcache extension to make use of JIT for a 20%+ performance boost!' . PHP_EOL . PHP_EOL); diff --git a/src/Psalm/Internal/Fork/PsalmRestarter.php b/src/Psalm/Internal/Fork/PsalmRestarter.php index e25f5627e78..1550c787e07 100644 --- a/src/Psalm/Internal/Fork/PsalmRestarter.php +++ b/src/Psalm/Internal/Fork/PsalmRestarter.php @@ -10,6 +10,7 @@ use function array_merge; use function array_splice; use function assert; +use function defined; use function extension_loaded; use function file_get_contents; use function file_put_contents; @@ -81,7 +82,7 @@ protected function requiresRestart($default): bool $opcache_loaded = extension_loaded('opcache') || extension_loaded('Zend OPcache'); - if ($opcache_loaded) { + if ($opcache_loaded && !defined('PHP_WINDOWS_VERSION_MAJOR')) { // restart to enable JIT if it's not configured in the optimal way foreach (self::REQUIRED_OPCACHE_SETTINGS as $ini_name => $required_value) { $value = (string) ini_get("opcache.$ini_name"); @@ -155,7 +156,7 @@ protected function restart($command): void // executed in the parent process (before restart) // if it wasn't loaded then we apparently don't have opcache installed and there's no point trying // to tweak it - if ($opcache_loaded) { + if ($opcache_loaded && !defined('PHP_WINDOWS_VERSION_MAJOR')) { $additional_options = []; foreach (self::REQUIRED_OPCACHE_SETTINGS as $key => $value) { $additional_options []= "-dopcache.{$key}={$value}"; From aa4038337fcd43fa99224486122b2ba6e21fa8cc Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 17 Jan 2024 18:48:34 +0100 Subject: [PATCH 233/357] Let disable-extension also disable zend extensions --- src/Psalm/Internal/Cli/Psalm.php | 4 ++++ src/Psalm/Internal/Fork/PsalmRestarter.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Cli/Psalm.php b/src/Psalm/Internal/Cli/Psalm.php index a37cb8a828a..2c1d5b83e22 100644 --- a/src/Psalm/Internal/Cli/Psalm.php +++ b/src/Psalm/Internal/Cli/Psalm.php @@ -896,6 +896,10 @@ private static function restart(array $options, int $threads, Progress $progress 'blackfire', ]); + if (defined('PHP_WINDOWS_VERSION_MAJOR')) { + $ini_handler->disableExtensions(['opcache', 'Zend OPcache']); + } + // If Xdebug is enabled, restart without it $ini_handler->check(); diff --git a/src/Psalm/Internal/Fork/PsalmRestarter.php b/src/Psalm/Internal/Fork/PsalmRestarter.php index 1550c787e07..19c98e15c71 100644 --- a/src/Psalm/Internal/Fork/PsalmRestarter.php +++ b/src/Psalm/Internal/Fork/PsalmRestarter.php @@ -141,7 +141,7 @@ private static function toBytes(string $value): int protected function restart($command): void { if ($this->required && $this->tmpIni) { - $regex = '/^\s*(extension\s*=.*(' . implode('|', $this->disabled_extensions) . ').*)$/mi'; + $regex = '/^\s*((?:zend_)?extension\s*=.*(' . implode('|', $this->disabled_extensions) . ').*)$/mi'; $content = file_get_contents($this->tmpIni); assert($content !== false); From 23543218dcb7090593ccb92441d53dac879c7b77 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 18 Jan 2024 12:59:56 +0100 Subject: [PATCH 234/357] Fix template, conditional array keys --- src/Psalm/Internal/Type/TypeParser.php | 9 +++- tests/ArrayKeysTest.php | 58 ++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index e908c4e3a1d..ecdd5910c86 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -672,8 +672,8 @@ private static function getTypeFromGenericTree( // PHP 8 values with whitespace after number are counted as numeric // and filter_var treats them as such too if ($atomic_type instanceof TLiteralString - && trim($atomic_type->value) === $atomic_type->value && ($string_to_int = filter_var($atomic_type->value, FILTER_VALIDATE_INT)) !== false + && trim($atomic_type->value) === $atomic_type->value ) { $builder = $generic_params[0]->getBuilder(); $builder->removeType($key); @@ -688,7 +688,14 @@ private static function getTypeFromGenericTree( || $atomic_type instanceof TMixed || $atomic_type instanceof TNever || $atomic_type instanceof TTemplateParam + || $atomic_type instanceof TTemplateIndexedAccess + || $atomic_type instanceof TTemplateValueOf + || $atomic_type instanceof TTemplateKeyOf + || $atomic_type instanceof TTemplateParamClass + || $atomic_type instanceof TTypeAlias || $atomic_type instanceof TValueOf + || $atomic_type instanceof TConditional + || $atomic_type instanceof TKeyOf || !$from_docblock ) { continue; diff --git a/tests/ArrayKeysTest.php b/tests/ArrayKeysTest.php index e3f9cc897a4..d2c363f940e 100644 --- a/tests/ArrayKeysTest.php +++ b/tests/ArrayKeysTest.php @@ -124,6 +124,64 @@ public function test(): array { } }', ], + 'variousArrayKeys' => [ + 'code' => ' + * @template TT + * @template TBool as bool + */ + class b { + /** + * @var array + */ + private array $a = []; + + /** @var array, int> */ + private array $c = []; + + /** @var array, int> */ + private array $d = []; + + /** @var array */ + private array $e = []; + + /** @var array>, int> */ + private array $f = []; + + /** @var array>, int> */ + private array $g = []; + + /** @var array */ + private array $h = []; + + /** + * @param T $arr + * @param class-string $b + * @param TBool $c + */ + public function __construct( + array $arr, + string $b, + bool $c + ) {} + + /** + * @return array<$v is true ? "a" : 123, 123> + */ + public function test(bool $v): array { + return $v ? ["a" => 123] : [123 => 123]; + } + }', + ], ]; } From 94efd67515026e2998a9b46a03dc52ebab658c27 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 18 Jan 2024 13:10:32 +0100 Subject: [PATCH 235/357] Improve tests --- tests/ArrayKeysTest.php | 45 +++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/tests/ArrayKeysTest.php b/tests/ArrayKeysTest.php index d2c363f940e..659cd5af5df 100644 --- a/tests/ArrayKeysTest.php +++ b/tests/ArrayKeysTest.php @@ -136,43 +136,35 @@ class a {} * @template TKey as array-key * @template TValue as array-key * @template T as array - * @template TT + * + * @template TOrig as a|b + * @template TT as class-string + * * @template TBool as bool */ class b { /** * @var array */ - private array $a = []; + private array $a = [123 => 123]; /** @var array, int> */ - private array $c = []; + public array $c = []; /** @var array, int> */ - private array $d = []; + public array $d = []; /** @var array */ - private array $e = []; + public array $e = []; - /** @var array>, int> */ - private array $f = []; + /** @var array>, int> */ + private array $f = [123 => 123]; - /** @var array>, int> */ - private array $g = []; + /** @var array>, int> */ + private array $g = ["test" => 123]; /** @var array */ - private array $h = []; - - /** - * @param T $arr - * @param class-string $b - * @param TBool $c - */ - public function __construct( - array $arr, - string $b, - bool $c - ) {} + private array $h = [123 => 123]; /** * @return array<$v is true ? "a" : 123, 123> @@ -180,7 +172,16 @@ public function __construct( public function test(bool $v): array { return $v ? ["a" => 123] : [123 => 123]; } - }', + } + + /** @var b<"testKey", "testValue", array<"testKey", "testValue">, b, class-string, true> */ + $b = new b; + $b->d["testKey"] = 123; + + // TODO + //$b->c["testValue"] = 123; + //$b->e["b"] = 123; + ', ], ]; } From f7edaa66acbb0f6ac2861ad95251b0019e603746 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 18 Jan 2024 20:33:54 +0100 Subject: [PATCH 236/357] Fix #10552 --- .../Expression/Call/MethodCallAnalyzer.php | 15 ++++++++++++- tests/MethodCallTest.php | 21 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php index ab4f1f0428b..ceff9788724 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php @@ -28,11 +28,13 @@ use Psalm\Issue\UndefinedMethod; use Psalm\IssueBuffer; use Psalm\Type; +use Psalm\Type\Atomic\TConditional; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TObject; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Union; +use function array_merge; use function array_reduce; use function count; use function is_string; @@ -175,6 +177,17 @@ public static function analyze( $lhs_types = $class_type->getAtomicTypes(); + foreach ($lhs_types as $k => $lhs_type_part) { + if ($lhs_type_part instanceof TConditional) { + $lhs_types = array_merge( + $lhs_types, + $lhs_type_part->if_type->getAtomicTypes(), + $lhs_type_part->else_type->getAtomicTypes(), + ); + unset($lhs_types[$k]); + } + } + $result = new AtomicMethodCallAnalysisResult(); $possible_new_class_types = []; @@ -398,7 +411,7 @@ public static function analyze( $types = $class_type->getAtomicTypes(); foreach ($types as $key => &$type) { - if (!$type instanceof TNamedObject && !$type instanceof TObject) { + if (!$type instanceof TNamedObject && !$type instanceof TObject && !$type instanceof TConditional) { unset($types[$key]); } else { $type = $type->setFromDocblock(false); diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 4c411170272..fa56564e790 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -1230,6 +1230,27 @@ public function bar(&$object): void {} $x = new Foo(); $x->bar($x);', ], + 'conditional' => [ + 'code' => 'x(); + } + }', + ], ]; } From d17757c422ab7ca9f5b245b622f44507b40b19fb Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Fri, 19 Jan 2024 10:01:32 +1300 Subject: [PATCH 237/357] Fix PropertyTypeTest case --- tests/PropertyTypeTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PropertyTypeTest.php b/tests/PropertyTypeTest.php index 2e1fb412e58..f34e1c53cf5 100644 --- a/tests/PropertyTypeTest.php +++ b/tests/PropertyTypeTest.php @@ -1166,7 +1166,7 @@ class Finally_ extends Node\Stmt * Constructs a finally node. * * @param list $stmts Statements - * @param array $attributes Additional attributes + * @param array $attributes Additional attributes */ public function __construct(array $stmts = array(), array $attributes = array()) { parent::__construct($attributes); From eb7ce32f40a86bb96d7456661b78b05140c8af4f Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Fri, 19 Jan 2024 16:14:40 +1300 Subject: [PATCH 238/357] Fix TemporaryUpdateTest cases --- tests/FileUpdates/TemporaryUpdateTest.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/FileUpdates/TemporaryUpdateTest.php b/tests/FileUpdates/TemporaryUpdateTest.php index 5acae2112f9..861c4548470 100644 --- a/tests/FileUpdates/TemporaryUpdateTest.php +++ b/tests/FileUpdates/TemporaryUpdateTest.php @@ -603,7 +603,7 @@ public function foo() : void { class A { public function foo() : void { - throw new Error("bad", 5); + throw new Error("bad", []); } }', ], @@ -613,7 +613,7 @@ public function foo() : void { class A { public function foo() : void { - throw new Error("bad", 5); + throw new Error("bad", []); } }', ], @@ -657,7 +657,7 @@ public function foo() : void { class A { public function foo() : void { - throw new E("bad", 5); + throw new E("bad", []); } }', ], @@ -667,7 +667,7 @@ public function foo() : void { class A { public function foo() : void { - throw new E("bad", 5); + throw new E("bad", []); } }', ], @@ -707,7 +707,7 @@ public function foo() : void { class A { public function foo() : void { - throw new Error("bad", 5); + throw new Error("bad", []); } }', ], @@ -717,7 +717,7 @@ public function foo() : void { class A { public function foo() : void { - throw new Error("bad", 5); + throw new Error("bad", []); } }', ], @@ -755,7 +755,7 @@ public function foo() : void { class A { public function foo() : void { - throw new E("bad", 5); + throw new E("bad", []); } }', ], @@ -765,7 +765,7 @@ public function foo() : void { class A { public function foo() : void { - throw new E("bad", 5); + throw new E("bad", []); } }', ], From d3eb02a93b4a03396da4235b7083f930f9b74eba Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Fri, 19 Jan 2024 16:20:41 +1300 Subject: [PATCH 239/357] Fix ClassTest case --- tests/ClassTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/ClassTest.php b/tests/ClassTest.php index 8c4d9ccbf15..d245fb44fc9 100644 --- a/tests/ClassTest.php +++ b/tests/ClassTest.php @@ -464,7 +464,9 @@ class T extends \PHPUnit\Framework\TestCase { ], 'classAliasNoException' => [ 'code' => ' Date: Mon, 22 Jan 2024 07:14:10 +0000 Subject: [PATCH 240/357] Bump actions/cache from 3 to 4 Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/bcc.yml | 2 +- .github/workflows/build-phar.yml | 2 +- .github/workflows/ci.yml | 6 +++--- .github/workflows/windows-ci.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/bcc.yml b/.github/workflows/bcc.yml index 3fe7bdde2f7..0d5ddb8324b 100644 --- a/.github/workflows/bcc.yml +++ b/.github/workflows/bcc.yml @@ -30,7 +30,7 @@ jobs: composer update --no-install - name: Cache composer cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ${{ steps.composer-cache.outputs.files_cache }} diff --git a/.github/workflows/build-phar.yml b/.github/workflows/build-phar.yml index 68bc63ce139..51abbb71970 100644 --- a/.github/workflows/build-phar.yml +++ b/.github/workflows/build-phar.yml @@ -59,7 +59,7 @@ jobs: composer update --no-install - name: Cache composer cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ${{ steps.composer-cache.outputs.files_cache }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e01a894b61b..3d1103373cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: COMPOSER_ROOT_VERSION: dev-master - name: Cache composer cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ${{ steps.composer-cache.outputs.files_cache }} @@ -77,7 +77,7 @@ jobs: COMPOSER_ROOT_VERSION: dev-master - name: Cache composer cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ${{ steps.composer-cache.outputs.files_cache }} @@ -165,7 +165,7 @@ jobs: COMPOSER_ROOT_VERSION: dev-master - name: Cache composer cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ${{ steps.composer-cache.outputs.files_cache }} diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index de0665c9a72..d17fd73547c 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -78,7 +78,7 @@ jobs: COMPOSER_ROOT_VERSION: dev-master - name: Cache composer cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ${{ steps.composer-cache.outputs.files_cache }} From c307bcbc7669c930c8f6dd8afd74537ba272385f Mon Sep 17 00:00:00 2001 From: klimick Date: Mon, 22 Jan 2024 07:09:57 +0300 Subject: [PATCH 241/357] Try to fix template replacement edge case --- .../Type/TemplateStandinTypeReplacer.php | 2 - tests/Template/FunctionTemplateTest.php | 62 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index 7ce88ebb54c..7384d3bce88 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -1254,7 +1254,6 @@ public static function getMappedGenericTypeParams( Atomic $container_type_part, ?array &$container_type_params_covariant = null ): array { - $_ = null; if ($input_type_part instanceof TGenericObject || $input_type_part instanceof TIterable) { $input_type_params = $input_type_part->type_params; } elseif ($codebase->classlike_storage_provider->has($input_type_part->value)) { @@ -1290,7 +1289,6 @@ public static function getMappedGenericTypeParams( $replacement_templates = []; if ($input_template_types - && (!$input_type_part instanceof TGenericObject || !$input_type_part->remapped_params) && (!$container_type_part instanceof TGenericObject || !$container_type_part->remapped_params) ) { foreach ($input_template_types as $template_name => $_) { diff --git a/tests/Template/FunctionTemplateTest.php b/tests/Template/FunctionTemplateTest.php index 800e2956cd7..01b58efb671 100644 --- a/tests/Template/FunctionTemplateTest.php +++ b/tests/Template/FunctionTemplateTest.php @@ -14,6 +14,68 @@ class FunctionTemplateTest extends TestCase public function providerValidCodeParse(): iterable { return [ + 'extractTypeParameterValue' => [ + 'code' => ' + */ + final readonly class IntType implements Type {} + + /** + * @template T + * @implements Type> + */ + final readonly class ListType implements Type + { + /** + * @param Type $type + */ + public function __construct( + public Type $type, + ) { + } + } + + /** + * @template T + * @param Type $type + * @return T + */ + function extractType(Type $type): mixed + { + throw new \RuntimeException("Should never be called at runtime"); + } + + /** + * @template T + * @param Type $t + * @return ListType + */ + function listType(Type $t): ListType + { + return new ListType($t); + } + + function intType(): IntType + { + return new IntType(); + } + + $listType = listType(intType()); + $list = extractType($listType); + ', + 'assertions' => [ + '$listType===' => 'ListType', + '$list' => 'list', + ], + 'ignored_issues' => [], + 'php_version' => '8.2', + ], 'validTemplatedType' => [ 'code' => ' Date: Wed, 24 Jan 2024 22:43:40 +0100 Subject: [PATCH 242/357] Update psalm-baseline.xml --- psalm-baseline.xml | 184 +-------------------------------------------- 1 file changed, 1 insertion(+), 183 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index ed55df4536a..88b88bd8c55 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,186 +1,10 @@ - - - - mapper]]> - - - - - $items - - + - - $returnType - attrGroups]]> - byRef]]> - expr]]> - params]]> - returnType]]> - static]]> - - - - $returnType - attrGroups]]> - byRef]]> - params]]> - returnType]]> - static]]> - stmts]]> - uses]]> - - - - - $items - - - - - $parts - - - - - $conds - - - - - $parts - $parts - $parts - - - - - $stmts - - - - - $stmts - - - - - $returnType - attrGroups]]> - byRef]]> - flags]]> - params]]> - returnType]]> - stmts]]> - - - - - attrGroups]]> - extends]]> - flags]]> - implements]]> - stmts]]> - - - - - $stmts - - - - - $stmts - - - - - $stmts - - - - - $stmts - - - - - cond]]> - init]]> - loop]]> - stmts]]> - - - - - byRef]]> - keyVar]]> - stmts]]> - - - - - $returnType - attrGroups]]> - byRef]]> - params]]> - returnType]]> - stmts]]> - - - - - else]]> - elseifs]]> - stmts]]> - - - - - attrGroups]]> - extends]]> - stmts]]> - - - - - $stmts - - - - - attrGroups]]> - stmts]]> - - - - - $stmts - - - - - $stmts - - - - - static::getDefaultDescription() - static::getDefaultDescription() - static::getDefaultDescription() - static::getDefaultName() - static::getDefaultName() - static::getDefaultName() - - - $name - - tags['variablesfrom'][0]]]> @@ -1277,12 +1101,6 @@ vars_to_initialize]]> - - - UndefinedFunction - UndefinedFunction - - !$root_path From 68a1d1e2b480cd63700337663dcbc694dd43f2ea Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Sat, 13 Jan 2024 07:01:37 +1300 Subject: [PATCH 243/357] Switch condition order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change is for forward-compatibility with nikic/php-parser 5.0, where `InterpolatedStringPart` (née `EncapsedStringPart`) is no longer an expression. Thus we can't pass it to `NodeTypeProvider::getType()` anymore. Since that call returns `null` anyway, we can swap the condition order. Everything still works and Psalm type-checking is happy. This also might be a tiny performance improvement since it lets the common, cheap instanceof check come before a method call, but I haven't actually benchmarked it. --- .../Expression/EncapsulatedStringAnalyzer.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php index 563d58b1a58..63c815ff521 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php @@ -46,9 +46,12 @@ public static function analyze( return false; } - $part_type = $statements_analyzer->node_data->getType($part); - - if ($part_type !== null) { + if ($part instanceof EncapsedStringPart) { + if ($literal_string !== null) { + $literal_string .= $part->value; + } + $non_empty = $non_empty || $part->value !== ""; + } elseif ($part_type = $statements_analyzer->node_data->getType($part)) { $casted_part_type = CastAnalyzer::castStringAttempt( $statements_analyzer, $context, @@ -110,11 +113,6 @@ public static function analyze( } } } - } elseif ($part instanceof EncapsedStringPart) { - if ($literal_string !== null) { - $literal_string .= $part->value; - } - $non_empty = $non_empty || $part->value !== ""; } else { $all_literals = false; $literal_string = null; From 10402c426b86a0b49f35ab81585c660e94b4b726 Mon Sep 17 00:00:00 2001 From: Ivan Sidorov Date: Tue, 23 Jan 2024 07:25:51 +0000 Subject: [PATCH 244/357] Partial revert "Fix auto completion by partial property or method" Filtering is not necessary. Clients using LSP should filter the results themselves. That's what it says in the documentation. This reverts commit d6faff2844dbcd40df108052fbdd929bdeafc7a9. --- src/Psalm/Codebase.php | 2 + .../LanguageServer/Server/TextDocument.php | 3 - tests/LanguageServer/CompletionTest.php | 196 ------------------ 3 files changed, 2 insertions(+), 199 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 993958e607a..db400c999c2 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -2039,6 +2039,8 @@ public function getCompletionItemsForClassishThing( /** * @param list $items * @return list + * @deprecated to be removed in Psalm 6 + * @api fix deprecation problem "PossiblyUnusedMethod: Cannot find any calls to method" */ public function filterCompletionItemsByBeginLiteralPart(array $items, string $literal_part): array { diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index 451da44e938..a4af46cacec 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -297,7 +297,6 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit try { $completion_data = $this->codebase->getCompletionDataAtPosition($file_path, $position); - $literal_part = $this->codebase->getBeginedLiteralPart($file_path, $position); if ($completion_data) { [$recent_type, $gap, $offset] = $completion_data; @@ -306,8 +305,6 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit ->textDocument->completion->completionItem->snippetSupport ?? false; $completion_items = $this->codebase->getCompletionItemsForClassishThing($recent_type, $gap, $snippetSupport); - $completion_items = - $this->codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); } elseif ($gap === '[') { $completion_items = $this->codebase->getCompletionItemsForArrayKeys($recent_type); } else { diff --git a/tests/LanguageServer/CompletionTest.php b/tests/LanguageServer/CompletionTest.php index 268f399821e..656951253d5 100644 --- a/tests/LanguageServer/CompletionTest.php +++ b/tests/LanguageServer/CompletionTest.php @@ -15,7 +15,6 @@ use Psalm\Tests\TestConfig; use Psalm\Type; -use function array_map; use function count; class CompletionTest extends TestCase @@ -726,201 +725,6 @@ public function baz() {} $this->assertSame('baz()', $completion_items[1]->insertText); } - public function testObjectPropertyOnAppendToEnd(): void - { - $codebase = $this->codebase; - $config = $codebase->config; - $config->throw_exception = false; - - $this->addFile( - 'somefile.php', - 'aPr - } - }', - ); - - $codebase->file_provider->openFile('somefile.php'); - $codebase->scanFiles(); - - $this->analyzeFile('somefile.php', new Context()); - - $position = new Position(8, 34); - $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position); - $literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position); - - $this->assertSame(['B\A&static', '->', 223], $completion_data); - - $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); - $completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); - $completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items); - - $this->assertSame(['aProp'], $completion_item_texts); - } - - public function testObjectPropertyOnReplaceEndPart(): void - { - $codebase = $this->codebase; - $config = $codebase->config; - $config->throw_exception = false; - - $this->addFile( - 'somefile.php', - 'aProp2; - } - }', - ); - - $codebase->file_provider->openFile('somefile.php'); - $codebase->scanFiles(); - - $this->analyzeFile('somefile.php', new Context()); - - $position = new Position(8, 34); - $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position); - $literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position); - - $this->assertSame(['B\A&static', '->', 225], $completion_data); - - $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); - $completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); - $completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items); - - $this->assertSame(['aProp1', 'aProp2'], $completion_item_texts); - } - - public function testSelfPropertyOnAppendToEnd(): void - { - $codebase = $this->codebase; - $config = $codebase->config; - $config->throw_exception = false; - - $this->addFile( - 'somefile.php', - 'file_provider->openFile('somefile.php'); - $codebase->scanFiles(); - - $this->analyzeFile('somefile.php', new Context()); - - $position = new Position(8, 34); - $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position); - $literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position); - - $this->assertSame(['B\A', '::', 237], $completion_data); - - $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); - $completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); - $completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items); - - $this->assertSame(['$aProp'], $completion_item_texts); - } - - public function testStaticPropertyOnAppendToEnd(): void - { - $codebase = $this->codebase; - $config = $codebase->config; - $config->throw_exception = false; - - $this->addFile( - 'somefile.php', - 'file_provider->openFile('somefile.php'); - $codebase->scanFiles(); - - $this->analyzeFile('somefile.php', new Context()); - - $position = new Position(8, 36); - $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position); - $literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position); - - $this->assertSame(['B\A', '::', 239], $completion_data); - - $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); - $completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); - $completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items); - - $this->assertSame(['$aProp'], $completion_item_texts); - } - - public function testStaticPropertyOnReplaceEndPart(): void - { - $codebase = $this->codebase; - $config = $codebase->config; - $config->throw_exception = false; - - $this->addFile( - 'somefile.php', - 'file_provider->openFile('somefile.php'); - $codebase->scanFiles(); - - $this->analyzeFile('somefile.php', new Context()); - - $position = new Position(8, 34); - $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position); - $literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position); - - $this->assertSame(['B\A', '::', 239], $completion_data); - - $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); - $completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); - $completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items); - - $this->assertSame(['$aProp1', '$aProp2'], $completion_item_texts); - } - public function testCompletionOnNewExceptionWithoutNamespace(): void { $codebase = $this->codebase; From 6e2effaf9a3e53f1eade63aa84af1bd7dc7ea157 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sat, 27 Jan 2024 13:41:43 +0100 Subject: [PATCH 245/357] `key_exists()` is an alias for `array_key_exists()` Fixes vimeo/psalm#10346 --- .../Statements/Expression/AssertionFinder.php | 4 +++- tests/TypeReconciliation/ArrayKeyExistsTest.php | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index 8c6aaa03ce3..4cf8e778ee6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -2055,7 +2055,9 @@ protected static function hasNonEmptyCountCheck(PhpParser\Node\Expr\FuncCall $st protected static function hasArrayKeyExistsCheck(PhpParser\Node\Expr\FuncCall $stmt): bool { - return $stmt->name instanceof PhpParser\Node\Name && strtolower($stmt->name->getFirst()) === 'array_key_exists'; + return $stmt->name instanceof PhpParser\Node\Name + && (strtolower($stmt->name->getFirst()) === 'array_key_exists' + || strtolower($stmt->name->getFirst()) === 'key_exists'); } /** diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php index e44bfa55464..4317379eb7b 100644 --- a/tests/TypeReconciliation/ArrayKeyExistsTest.php +++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php @@ -507,6 +507,19 @@ public function isCriticalError(int|string $key): bool { 'ignored_issues' => [], 'php_version' => '8.0', ], + 'keyExistsAsAliasForArrayKeyExists' => [ + 'code' => <<<'PHP' + $arr + */ + function foo(array $arr): void { + if (key_exists("a", $arr)) { + echo $arr["a"]; + } + } + PHP, + ], ]; } From c1e22ddcaa26a0923b268172c8f70087d9c75cc5 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sat, 27 Jan 2024 14:37:26 +0100 Subject: [PATCH 246/357] Allow properties on intersections with enum interfaces Fixes vimeo/psalm#10585 --- .../Fetch/AtomicPropertyFetchAnalyzer.php | 11 +++++++- tests/EnumTest.php | 27 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index 0fdc76b40ce..f01f379e26a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -1141,6 +1141,8 @@ private static function handleNonExistentClass( $override_property_visibility = $interface_storage->override_property_visibility; + $intersects_with_enum = false; + foreach ($intersection_types as $intersection_type) { if ($intersection_type instanceof TNamedObject && $codebase->classExists($intersection_type->value) @@ -1149,12 +1151,19 @@ private static function handleNonExistentClass( $class_exists = true; return; } + if ($intersection_type instanceof TNamedObject + && (in_array($intersection_type->value, ['UnitEnum', 'BackedEnum'], true) + || in_array('UnitEnum', $codebase->getParentInterfaces($intersection_type->value))) + ) { + $intersects_with_enum = true; + } } if (!$class_exists && //interfaces can't have properties. Except when they do... In PHP Core, they can !in_array($fq_class_name, ['UnitEnum', 'BackedEnum'], true) && - !in_array('UnitEnum', $codebase->getParentInterfaces($fq_class_name)) + !in_array('UnitEnum', $codebase->getParentInterfaces($fq_class_name)) && + !$intersects_with_enum ) { if (IssueBuffer::accepts( new NoInterfaceProperties( diff --git a/tests/EnumTest.php b/tests/EnumTest.php index 12322c66698..0cb1c2c0e4d 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -630,6 +630,33 @@ enum BarEnum: int { 'ignored_issues' => [], 'php_version' => '8.1', ], + 'allowPropertiesOnIntersectionsWithEnumInterfaces' => [ + 'code' => <<<'PHP' + name; + } + if ($i instanceof UnitEnum) { + echo $i->name; + } + if ($i instanceof UE) { + echo $i->name; + } + if ($i instanceof BE) { + echo $i->name; + } + } + PHP, + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } From 9372adb98015bd5744645a0a434b0266d8de9786 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sat, 27 Jan 2024 15:38:27 +0100 Subject: [PATCH 247/357] `readgzfile()` is impure Fixes vimeo/psalm#10528 --- dictionaries/ImpureFunctionsList.php | 1 + 1 file changed, 1 insertion(+) diff --git a/dictionaries/ImpureFunctionsList.php b/dictionaries/ImpureFunctionsList.php index d3a3f7ce0a3..25f94ef5298 100644 --- a/dictionaries/ImpureFunctionsList.php +++ b/dictionaries/ImpureFunctionsList.php @@ -85,6 +85,7 @@ 'ob_end_clean' => true, 'ob_get_clean' => true, 'readfile' => true, + 'readgzfile' => true, 'printf' => true, 'var_dump' => true, 'phpinfo' => true, From 67a91e05b2cb43885ed9582261628ec1fc99f215 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sat, 27 Jan 2024 16:38:37 +0100 Subject: [PATCH 248/357] Do not validate callable arguments in lenient contexts Fixes vimeo/psalm#10453 --- .../Statements/Expression/Call/ArgumentAnalyzer.php | 1 + tests/FunctionCallTest.php | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index a1df71add81..ec72396f28d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -1355,6 +1355,7 @@ private static function verifyExplicitParam( } else { if (!$param_type->hasString() && !$param_type->hasArray() + && $context->check_functions && CallAnalyzer::checkFunctionExists( $statements_analyzer, $function_id, diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 4d997070211..f5965f98f40 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -1647,6 +1647,14 @@ function in_array($a, $b) { } }', ], + 'callableArgumentWithFunctionExists' => [ + 'code' => <<<'PHP' + [ 'code' => ' Date: Fri, 12 Jan 2024 21:50:20 +1300 Subject: [PATCH 249/357] Use flags key instead of type for ClassMethod --- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index 4e6ef467bcb..75cf98e3f89 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -1228,7 +1228,7 @@ static function (FunctionLikeParameter $param): PhpParser\Node\Arg { $fake_stmt = new VirtualClassMethod( new VirtualIdentifier('__construct'), [ - 'type' => PhpParser\Node\Stmt\Class_::MODIFIER_PUBLIC, + 'flags' => PhpParser\Node\Stmt\Class_::MODIFIER_PUBLIC, 'params' => $fake_constructor_params, 'stmts' => $fake_constructor_stmts, ], From f1a206fbf523ad1cd50c1cc73961a26175596a9d Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Wed, 10 Jan 2024 08:41:01 +1300 Subject: [PATCH 250/357] Remove usages of deprecated getLine --- src/Psalm/CodeLocation.php | 2 +- src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Psalm/CodeLocation.php b/src/Psalm/CodeLocation.php index 344a5981972..1d80ef71912 100644 --- a/src/Psalm/CodeLocation.php +++ b/src/Psalm/CodeLocation.php @@ -157,7 +157,7 @@ public function __construct( $this->preview_start = $this->docblock_start ?: $this->file_start; /** @psalm-suppress ImpureMethodCall Actually mutation-free just not marked */ - $this->raw_line_number = $stmt->getLine(); + $this->raw_line_number = $stmt->getStartLine(); $this->docblock_line_number = $comment_line; } diff --git a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php index c8a05ea6d65..188e073153e 100644 --- a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php @@ -546,7 +546,7 @@ public function leaveNode(PhpParser\Node $node) } throw new UnexpectedValueException( - 'There should be function storages for line ' . $this->file_path . ':' . $node->getLine(), + 'There should be function storages for line ' . $this->file_path . ':' . $node->getStartLine(), ); } From 4b2cd0f23f350db439fd6c2f11ccaa189a3aad9d Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 28 Jan 2024 16:36:03 +0100 Subject: [PATCH 251/357] [LSP] Add issue type in description Fixes psalm/psalm-vscode-plugin#287 --- src/Psalm/Internal/LanguageServer/LanguageServer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 7062885e790..54d15a4aa7c 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -721,7 +721,7 @@ public function emitVersionedIssues(array $files, ?int $version = null): void $diagnostics = array_map( function (IssueData $issue_data): Diagnostic { //$check_name = $issue->check_name; - $description = $issue_data->message; + $description = '[' . $issue_data->type . '] ' . $issue_data->message; $severity = $issue_data->severity; $start_line = max($issue_data->line_from, 1); From 7701cd638560f69d4e80fc16f633177ff232cf70 Mon Sep 17 00:00:00 2001 From: Theodore Brown Date: Sun, 28 Jan 2024 16:18:15 -0600 Subject: [PATCH 252/357] Remove unnecessary null type from `initialized_methods` Context property --- src/Psalm/Context.php | 8 ++++---- .../StaticMethod/ExistingAtomicStaticCallAnalyzer.php | 4 ---- .../Analyzer/Statements/Expression/CallAnalyzer.php | 8 -------- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/Psalm/Context.php b/src/Psalm/Context.php index b09a704d2c2..02b1b74a6ba 100644 --- a/src/Psalm/Context.php +++ b/src/Psalm/Context.php @@ -85,7 +85,7 @@ final class Context /** * A set of references that might still be in scope from a scope likely to cause confusion. This applies * to references set inside a loop or if statement, since it's easy to forget about PHP's weird scope - * rules, and assinging to a reference will change the referenced variable rather than shadowing it. + * rules, and assigning to a reference will change the referenced variable rather than shadowing it. * * @var array */ @@ -112,7 +112,7 @@ final class Context public bool $inside_unset = false; /** - * Whether or not we're inside an class_exists call, where + * Whether or not we're inside a class_exists call, where * we don't care about possibly undefined classes */ public bool $inside_class_exists = false; @@ -207,9 +207,9 @@ final class Context /** * Stored to prevent re-analysing methods when checking for initialised properties * - * @var array|null + * @var array */ - public ?array $initialized_methods = null; + public array $initialized_methods = []; /** * @var array diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php index 88646faa5b6..c7dc29376ad 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -126,10 +126,6 @@ public static function analyze( } if (!isset($context->initialized_methods[(string) $appearing_method_id])) { - if ($context->initialized_methods === null) { - $context->initialized_methods = []; - } - $context->initialized_methods[(string) $appearing_method_id] = true; $file_analyzer->getMethodMutations($appearing_method_id, $context); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index 0e081a2c67c..6746e21027d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -110,10 +110,6 @@ public static function collectSpecialInformation( return; } - if ($context->initialized_methods === null) { - $context->initialized_methods = []; - } - $context->initialized_methods[(string) $method_id] = true; } @@ -193,10 +189,6 @@ public static function collectSpecialInformation( return; } - if ($context->initialized_methods === null) { - $context->initialized_methods = []; - } - $context->initialized_methods[(string) $declaring_method_id] = true; $method_storage = $codebase->methods->getStorage($declaring_method_id); From 081fe2b90307643a43e5e60d87a422f3e6c9b81c Mon Sep 17 00:00:00 2001 From: Theodore Brown Date: Sun, 28 Jan 2024 16:21:27 -0600 Subject: [PATCH 253/357] Optimize ConfigFileTest slightly --- tests/Config/ConfigFileTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Config/ConfigFileTest.php b/tests/Config/ConfigFileTest.php index 4408b729691..f6102f09ac6 100644 --- a/tests/Config/ConfigFileTest.php +++ b/tests/Config/ConfigFileTest.php @@ -285,8 +285,9 @@ protected static function compareContentWithTemplateAndTrailingLineEnding(string $passed = false; foreach ([PHP_EOL, "\n", "\r", "\r\n"] as $eol) { - if (!$passed && $contents === ($expected_template . $eol)) { + if ($contents === ($expected_template . $eol)) { $passed = true; + break; } } From 98d98be4439d9b8b20757125025954df73f3b1ef Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Wed, 10 Jan 2024 21:57:56 +1300 Subject: [PATCH 254/357] Re-work CheckTrivialExprVisitor In php-parser 5.0, `ClosureUse` is no longer considered an expression. This requires changes to Psalm's `CheckTrivialExprVisitor`, which stores an array of "non-trivial" `Expr` nodes. However the only use of this array is to count whether or not it's empty. Instead of keeping the array, we can keep a boolean, and avoid needing to change the types in this class when we upgrade to php-parser 5.0. --- .../Statements/UnusedAssignmentRemover.php | 2 +- .../PhpVisitor/CheckTrivialExprVisitor.php | 14 ++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/UnusedAssignmentRemover.php b/src/Psalm/Internal/Analyzer/Statements/UnusedAssignmentRemover.php index d3aaa7050a7..a82a3fabb13 100644 --- a/src/Psalm/Internal/Analyzer/Statements/UnusedAssignmentRemover.php +++ b/src/Psalm/Internal/Analyzer/Statements/UnusedAssignmentRemover.php @@ -65,7 +65,7 @@ public function findUnusedAssignment( $traverser->addVisitor($visitor); $traverser->traverse([$rhs_exp]); - $rhs_exp_trivial = (count($visitor->getNonTrivialExpr()) === 0); + $rhs_exp_trivial = !$visitor->hasNonTrivialExpr(); if ($rhs_exp_trivial) { $treat_as_expr = false; diff --git a/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php b/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php index 4fe4afe5269..4179ac0d0e6 100644 --- a/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php @@ -9,10 +9,7 @@ */ final class CheckTrivialExprVisitor extends PhpParser\NodeVisitorAbstract { - /** - * @var array - */ - protected array $non_trivial_expr = []; + private bool $has_non_trivial_expr = false; private function checkNonTrivialExpr(PhpParser\Node\Expr $node): bool { @@ -55,7 +52,7 @@ public function enterNode(PhpParser\Node $node): ?int if ($node instanceof PhpParser\Node\Expr) { // Check for Non-Trivial Expression first if ($this->checkNonTrivialExpr($node)) { - $this->non_trivial_expr[] = $node; + $this->has_non_trivial_expr = true; return PhpParser\NodeTraverser::STOP_TRAVERSAL; } @@ -70,11 +67,8 @@ public function enterNode(PhpParser\Node $node): ?int return null; } - /** - * @return array - */ - public function getNonTrivialExpr(): array + public function hasNonTrivialExpr(): bool { - return $this->non_trivial_expr; + return $this->has_non_trivial_expr; } } From 67ba758c091768ae93b094f10d2f26a3977d51a5 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sat, 27 Jan 2024 22:19:17 +0100 Subject: [PATCH 255/357] Fix unstable `hasFullyQualified(Interface|Enum)()` Before this change, calling: ```php $classlikes->hasFullyQualifiedInterfaceName($i); // true $classlikes->hasFullyQualifiedClassName($i); // false $classlikes->hasFullyQualifiedInterfaceName($i); // false ``` would result in the last call returning `false` --- src/Psalm/Internal/Codebase/ClassLikes.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php index b57bbe076da..552fab265d5 100644 --- a/src/Psalm/Internal/Codebase/ClassLikes.php +++ b/src/Psalm/Internal/Codebase/ClassLikes.php @@ -356,6 +356,7 @@ public function hasFullyQualifiedClassName( } } + // fixme: this looks like a crazy caching hack if (!isset($this->existing_classes_lc[$fq_class_name_lc]) || !$this->existing_classes_lc[$fq_class_name_lc] || !$this->classlike_storage_provider->has($fq_class_name_lc) @@ -396,13 +397,14 @@ public function hasFullyQualifiedInterfaceName( ): bool { $fq_class_name_lc = strtolower($this->getUnAliasedName($fq_class_name)); + // fixme: this looks like a crazy caching hack if (!isset($this->existing_interfaces_lc[$fq_class_name_lc]) || !$this->existing_interfaces_lc[$fq_class_name_lc] || !$this->classlike_storage_provider->has($fq_class_name_lc) ) { if (( - !isset($this->existing_classes_lc[$fq_class_name_lc]) - || $this->existing_classes_lc[$fq_class_name_lc] + !isset($this->existing_interfaces_lc[$fq_class_name_lc]) + || $this->existing_interfaces_lc[$fq_class_name_lc] ) && !$this->classlike_storage_provider->has($fq_class_name_lc) ) { @@ -463,13 +465,14 @@ public function hasFullyQualifiedEnumName( ): bool { $fq_class_name_lc = strtolower($this->getUnAliasedName($fq_class_name)); + // fixme: this looks like a crazy caching hack if (!isset($this->existing_enums_lc[$fq_class_name_lc]) || !$this->existing_enums_lc[$fq_class_name_lc] || !$this->classlike_storage_provider->has($fq_class_name_lc) ) { if (( - !isset($this->existing_classes_lc[$fq_class_name_lc]) - || $this->existing_classes_lc[$fq_class_name_lc] + !isset($this->existing_enums_lc[$fq_class_name_lc]) + || $this->existing_enums_lc[$fq_class_name_lc] ) && !$this->classlike_storage_provider->has($fq_class_name_lc) ) { From 58b7139470b4977936d4e26ff10d85733951ada5 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Mon, 29 Jan 2024 21:49:45 +0100 Subject: [PATCH 256/357] Reflection may reference interfaces and enums --- tests/Internal/Codebase/InternalCallMapHandlerTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index e3e99991c13..c0c1293928a 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -26,9 +26,11 @@ use function array_shift; use function class_exists; use function count; +use function enum_exists; use function explode; use function function_exists; use function in_array; +use function interface_exists; use function is_array; use function is_int; use function json_encode; @@ -631,6 +633,8 @@ private function assertTypeValidity(ReflectionType $reflected, string $specified } catch (InvalidArgumentException $e) { if (preg_match('/^Could not get class storage for (.*)$/', $e->getMessage(), $matches) && !class_exists($matches[1]) + && !interface_exists($matches[1]) + && !enum_exists($matches[1]) ) { $this->fail("Class used in CallMap does not exist: {$matches[1]}"); } From c935cc307b3d9ce1d0af7dc9507b64313de412d6 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Mon, 29 Jan 2024 21:56:36 +0100 Subject: [PATCH 257/357] Un-ignore recursiveiteratoriterator::__construct --- tests/Internal/Codebase/InternalCallMapHandlerTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index c0c1293928a..5ae7945209e 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -142,7 +142,6 @@ class InternalCallMapHandlerTest extends TestCase 'oci_result', 'ocigetbufferinglob', 'ocisetbufferinglob', - 'recursiveiteratoriterator::__construct', // Class used in CallMap does not exist: recursiveiterator 'sqlsrv_fetch_array', 'sqlsrv_fetch_object', 'sqlsrv_get_field', From e8763968a0ee1c6492971d34ac7aca98e42e8e58 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Mon, 29 Jan 2024 22:03:59 +0100 Subject: [PATCH 258/357] Un-ignore arrayobject::getiterator - appears it's been fixed too --- tests/Internal/Codebase/InternalCallMapHandlerTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index 5ae7945209e..3768c83ea37 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -174,7 +174,6 @@ class InternalCallMapHandlerTest extends TestCase private static array $ignoredReturnTypeOnlyFunctions = [ 'appenditerator::getinneriterator' => ['8.1', '8.2', '8.3'], 'appenditerator::getiteratorindex' => ['8.1', '8.2', '8.3'], - 'arrayobject::getiterator' => ['8.1', '8.2', '8.3'], 'cachingiterator::getinneriterator' => ['8.1', '8.2', '8.3'], 'callbackfilteriterator::getinneriterator' => ['8.1', '8.2', '8.3'], 'curl_multi_getcontent', From b2aebd90a769537cd89f8581dea356aa2aa0a8df Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Mon, 29 Jan 2024 22:22:34 +0100 Subject: [PATCH 259/357] Fix test by preloading interface --- tests/TypeReconciliation/ReconcilerTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/TypeReconciliation/ReconcilerTest.php b/tests/TypeReconciliation/ReconcilerTest.php index a3b722ff3fd..91f958425eb 100644 --- a/tests/TypeReconciliation/ReconcilerTest.php +++ b/tests/TypeReconciliation/ReconcilerTest.php @@ -61,6 +61,7 @@ class A {} class B {} interface SomeInterface {} '); + $this->project_analyzer->getCodebase()->queueClassLikeForScanning('Countable'); $this->project_analyzer->getCodebase()->scanFiles(); } From e63db9c12a42e5de16a0c183847f1e0fb3915c10 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Mon, 29 Jan 2024 22:29:19 +0100 Subject: [PATCH 260/357] Drop suppression as the method is now used in tests --- src/Psalm/Codebase.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index db400c999c2..a02f61403f6 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -2406,7 +2406,6 @@ public function getKeyValueParamsForTraversableObject(Atomic $type): array /** * @param array $phantom_classes - * @psalm-suppress PossiblyUnusedMethod part of the public API */ public function queueClassLikeForScanning( string $fq_classlike_name, From 98756ba992865cc9535df7aaebdb82a79baad8e4 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Tue, 30 Jan 2024 04:59:12 +0100 Subject: [PATCH 261/357] Fix language server running with `opcache.save_comments=0` Fixes vimeo/psalm#10353 --- src/Psalm/Internal/Fork/PsalmRestarter.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Psalm/Internal/Fork/PsalmRestarter.php b/src/Psalm/Internal/Fork/PsalmRestarter.php index 53de9ec014c..af4d83776b8 100644 --- a/src/Psalm/Internal/Fork/PsalmRestarter.php +++ b/src/Psalm/Internal/Fork/PsalmRestarter.php @@ -84,6 +84,11 @@ protected function requiresRestart($default): bool } } + // opcache.save_comments is required for json mapper (used in language server) to work + if ($opcache_loaded && in_array(ini_get('opcache.save_comments'), ['0', 'false', 0, false])) { + return true; + } + return $default || $this->required; } @@ -152,6 +157,10 @@ protected function restart($command): void ]; } + if ($opcache_loaded) { + $additional_options[] = '-dopcache.save_comments=1'; + } + array_splice( $command, 1, From ca9a12dddaabb9caf8dd622d558696de0dabb9e8 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Tue, 30 Jan 2024 17:24:27 +0100 Subject: [PATCH 262/357] Report `MissingConstructor` for natively typed mixed properties Fixes vimeo/psalm#10589 --- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 7 +++++-- tests/PropertyTypeTest.php | 9 +++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index 4e6ef467bcb..e4324c9a233 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -1127,8 +1127,11 @@ private function checkPropertyInitialization( $uninitialized_variables[] = '$this->' . $property_name; $uninitialized_properties[$property_class_name . '::$' . $property_name] = $property; - if ($property->type && !$property->type->isMixed()) { - $uninitialized_typed_properties[$property_class_name . '::$' . $property_name] = $property; + if ($property->type) { + // Complain about all natively typed properties and all non-mixed docblock typed properties + if (!$property->type->from_docblock || !$property->type->isMixed()) { + $uninitialized_typed_properties[$property_class_name . '::$' . $property_name] = $property; + } } } diff --git a/tests/PropertyTypeTest.php b/tests/PropertyTypeTest.php index f34e1c53cf5..bf8e00f4ea5 100644 --- a/tests/PropertyTypeTest.php +++ b/tests/PropertyTypeTest.php @@ -3827,6 +3827,15 @@ class A { ', 'error_message' => 'UndefinedPropertyAssignment', ], + 'nativeMixedPropertyWithNoConstructor' => [ + 'code' => <<< 'PHP' + 'MissingConstructor', + ], ]; } } From 6d32d2f69276b31723c98f590f68866972839187 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Tue, 30 Jan 2024 20:29:51 +0100 Subject: [PATCH 263/357] Allow importing typedefs from enums Fixes vimeo/psalm#10416 --- src/Psalm/Internal/Type/TypeExpander.php | 2 +- tests/TypeAnnotationTest.php | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index 795b5ad9a8a..61af641066f 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -283,7 +283,7 @@ public static function expandAtomic( $declaring_fq_classlike_name = $self_class; } - if (!($evaluate_class_constants && $codebase->classOrInterfaceExists($declaring_fq_classlike_name))) { + if (!($evaluate_class_constants && $codebase->classOrInterfaceOrEnumExists($declaring_fq_classlike_name))) { return [$return_type]; } diff --git a/tests/TypeAnnotationTest.php b/tests/TypeAnnotationTest.php index 29ee8f6581f..dbcbf0b987d 100644 --- a/tests/TypeAnnotationTest.php +++ b/tests/TypeAnnotationTest.php @@ -865,6 +865,25 @@ public function doesNotWork($_doesNotWork): void { } }', ], + 'importFromEnum' => [ + 'code' => <<<'PHP' + [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } From fc3e15d0794b32989fdbee2a899ac067eb708bd7 Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Wed, 8 Mar 2023 22:19:55 +1300 Subject: [PATCH 264/357] Install php-parser 5.0 --- composer.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/composer.json b/composer.json index f3d319883c9..95943a9d28c 100644 --- a/composer.json +++ b/composer.json @@ -33,15 +33,12 @@ "felixfbecker/language-server-protocol": "^1.5.2", "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", - "nikic/php-parser": "^4.16", + "nikic/php-parser": "^5.0.0", "sebastian/diff": "^4.0 || ^5.0", "spatie/array-to-xml": "^2.17.0 || ^3.0", "symfony/console": "^4.1.6 || ^5.0 || ^6.0 || ^7.0", "symfony/filesystem": "^5.4 || ^6.0 || ^7.0" }, - "conflict": { - "nikic/php-parser": "4.17.0" - }, "provide": { "psalm/psalm": "self.version" }, From 1987309dd551233656cbcaad9755116ac1523057 Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Wed, 8 Mar 2023 22:45:01 +1300 Subject: [PATCH 265/357] Type-checking actually completes --- examples/plugins/SafeArrayKeyChecker.php | 2 +- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 2 +- .../FunctionLike/ReturnTypeCollector.php | 2 +- .../Internal/Analyzer/ProjectAnalyzer.php | 3 +- src/Psalm/Internal/Analyzer/ScopeAnalyzer.php | 4 +- .../Statements/Block/SwitchCaseAnalyzer.php | 16 ++++---- .../Analyzer/Statements/BreakAnalyzer.php | 2 +- .../Analyzer/Statements/ContinueAnalyzer.php | 2 +- .../Statements/Expression/ArrayAnalyzer.php | 4 +- .../Statements/Expression/AssertionFinder.php | 40 +++++++++---------- .../Assignment/ArrayAssignmentAnalyzer.php | 8 ++-- .../InstancePropertyAssignmentAnalyzer.php | 8 ++-- .../Expression/AssignmentAnalyzer.php | 2 +- .../Expression/Call/ArgumentsAnalyzer.php | 2 +- .../Call/Method/MissingMethodCallHandler.php | 4 +- .../Call/NamedFunctionCallHandler.php | 2 +- .../StaticMethod/AtomicStaticCallAnalyzer.php | 4 +- .../Expression/EncapsulatedStringAnalyzer.php | 13 +++--- .../Expression/ExpressionIdentifier.php | 2 +- .../Expression/Fetch/ArrayFetchAnalyzer.php | 2 +- .../Expression/IncDecExpressionAnalyzer.php | 6 +-- .../Statements/Expression/IncludeAnalyzer.php | 2 +- .../Statements/Expression/MatchAnalyzer.php | 4 +- .../Expression/SimpleTypeInferer.php | 8 ++-- .../Statements/ExpressionAnalyzer.php | 12 +++--- .../PhpVisitor/CheckTrivialExprVisitor.php | 2 +- .../Reflector/ExpressionResolver.php | 8 ++-- .../Reflector/FunctionLikeNodeScanner.php | 12 +++--- .../Internal/Provider/StatementsProvider.php | 25 ++---------- src/Psalm/Internal/RuntimeCaches.php | 1 - .../Internal/Scanner/PhpStormMetaScanner.php | 12 +++--- .../Generator/ClassLikeStubGenerator.php | 28 ++++++------- .../Stubs/Generator/StubsGenerator.php | 12 +++--- src/Psalm/Node/Scalar/VirtualDNumber.php | 13 ------ src/Psalm/Node/Scalar/VirtualEncapsed.php | 13 ------ src/Psalm/Node/Scalar/VirtualFloat.php | 13 ++++++ src/Psalm/Node/Scalar/VirtualInt.php | 13 ++++++ ...Part.php => VirtualInterpolatedString.php} | 4 +- .../Scalar/VirtualInterpolatedStringPart.php | 13 ++++++ src/Psalm/Node/Scalar/VirtualLNumber.php | 13 ------ src/Psalm/Node/Stmt/VirtualDeclareDeclare.php | 4 +- .../Node/Stmt/VirtualPropertyProperty.php | 13 ------ src/Psalm/Node/Stmt/VirtualUseUse.php | 13 ------ .../Node/{Expr => }/VirtualArrayItem.php | 4 +- .../Node/{Expr => }/VirtualClosureUse.php | 4 +- src/Psalm/Node/VirtualPropertyItem.php | 13 ++++++ .../Node/{Stmt => }/VirtualStaticVar.php | 4 +- src/Psalm/Node/VirtualUseItem.php | 13 ++++++ .../Event/AddRemoveTaintsEvent.php | 5 ++- tests/ClosureTest.php | 2 +- 50 files changed, 199 insertions(+), 214 deletions(-) delete mode 100644 src/Psalm/Node/Scalar/VirtualDNumber.php delete mode 100644 src/Psalm/Node/Scalar/VirtualEncapsed.php create mode 100644 src/Psalm/Node/Scalar/VirtualFloat.php create mode 100644 src/Psalm/Node/Scalar/VirtualInt.php rename src/Psalm/Node/Scalar/{VirtualEncapsedStringPart.php => VirtualInterpolatedString.php} (52%) create mode 100644 src/Psalm/Node/Scalar/VirtualInterpolatedStringPart.php delete mode 100644 src/Psalm/Node/Scalar/VirtualLNumber.php delete mode 100644 src/Psalm/Node/Stmt/VirtualPropertyProperty.php delete mode 100644 src/Psalm/Node/Stmt/VirtualUseUse.php rename src/Psalm/Node/{Expr => }/VirtualArrayItem.php (69%) rename src/Psalm/Node/{Expr => }/VirtualClosureUse.php (68%) create mode 100644 src/Psalm/Node/VirtualPropertyItem.php rename src/Psalm/Node/{Stmt => }/VirtualStaticVar.php (69%) create mode 100644 src/Psalm/Node/VirtualUseItem.php diff --git a/examples/plugins/SafeArrayKeyChecker.php b/examples/plugins/SafeArrayKeyChecker.php index 0360ed79155..fafcc2727da 100644 --- a/examples/plugins/SafeArrayKeyChecker.php +++ b/examples/plugins/SafeArrayKeyChecker.php @@ -2,7 +2,7 @@ namespace Psalm\Example\Plugin; -use PhpParser\Node\Expr\ArrayItem; +use PhpParser\Node\ArrayItem; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; use Psalm\Plugin\EventHandler\RemoveTaintsInterface; diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index 969efe0d2e0..d2da1eb3d7e 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -1195,7 +1195,7 @@ static function (FunctionLikeParameter $param): PhpParser\Node\Arg { $fake_stmt = new VirtualClassMethod( new VirtualIdentifier('__construct'), [ - 'flags' => PhpParser\Node\Stmt\Class_::MODIFIER_PUBLIC, + 'flags' => PhpParser\Modifiers::PUBLIC, 'params' => $fake_constructor_params, 'stmts' => $fake_constructor_stmts, ], diff --git a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php index 21e9d22d1c4..6ecc2026c72 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php +++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php @@ -54,7 +54,7 @@ public static function getReturnTypes( $yield_types = array_merge($yield_types, self::getYieldTypeFromExpression($stmt->expr, $nodes)); } elseif ($stmt->expr instanceof PhpParser\Node\Scalar\String_) { $return_types[] = Type::getString(); - } elseif ($stmt->expr instanceof PhpParser\Node\Scalar\LNumber) { + } elseif ($stmt->expr instanceof PhpParser\Node\Scalar\Int_) { $return_types[] = Type::getInt(); } elseif ($stmt->expr instanceof PhpParser\Node\Expr\ConstFetch) { if ((string)$stmt->expr->name === 'true') { diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index 40c85901cba..3187bd05e29 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -1186,8 +1186,7 @@ public function setPhpVersion(string $version, string $source): void $analysis_php_version_id = $php_major_version * 10_000 + $php_minor_version * 100; if ($this->codebase->analysis_php_version_id !== $analysis_php_version_id) { - // reset lexer and parser when php version changes - StatementsProvider::clearLexer(); + // reset parser when php version changes StatementsProvider::clearParser(); } diff --git a/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php b/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php index bca1b1aa895..7fb9a18464a 100644 --- a/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php @@ -85,7 +85,7 @@ public static function getControlActions( if ($stmt instanceof PhpParser\Node\Stmt\Continue_) { $count = !$stmt->num ? 1 - : ($stmt->num instanceof PhpParser\Node\Scalar\LNumber ? $stmt->num->value : null); + : ($stmt->num instanceof PhpParser\Node\Scalar\Int_ ? $stmt->num->value : null); if ($break_types && $count !== null && count($break_types) >= $count) { /** @psalm-suppress InvalidArrayOffset Some int-range improvements are needed */ @@ -102,7 +102,7 @@ public static function getControlActions( if ($stmt instanceof PhpParser\Node\Stmt\Break_) { $count = !$stmt->num ? 1 - : ($stmt->num instanceof PhpParser\Node\Scalar\LNumber ? $stmt->num->value : null); + : ($stmt->num instanceof PhpParser\Node\Scalar\Int_ ? $stmt->num->value : null); if ($break_types && $count !== null && count($break_types) >= $count) { /** @psalm-suppress InvalidArrayOffset Some int-range improvements are needed */ diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php index a5a9f51e542..33fa2b39dff 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php @@ -26,15 +26,15 @@ use Psalm\Node\Expr\BinaryOp\VirtualEqual; use Psalm\Node\Expr\BinaryOp\VirtualIdentical; use Psalm\Node\Expr\VirtualArray; -use Psalm\Node\Expr\VirtualArrayItem; use Psalm\Node\Expr\VirtualBooleanNot; use Psalm\Node\Expr\VirtualConstFetch; use Psalm\Node\Expr\VirtualFuncCall; use Psalm\Node\Expr\VirtualVariable; use Psalm\Node\Name\VirtualFullyQualified; -use Psalm\Node\Scalar\VirtualLNumber; +use Psalm\Node\Scalar\VirtualInt; use Psalm\Node\Stmt\VirtualIf; use Psalm\Node\VirtualArg; +use Psalm\Node\VirtualArrayItem; use Psalm\Node\VirtualName; use Psalm\Type; use Psalm\Type\Atomic\TDependentGetClass; @@ -249,8 +249,8 @@ public static function analyze( $case_equality_expr = new VirtualFuncCall( new VirtualFullyQualified(['rand']), [ - new VirtualArg(new VirtualLNumber(0)), - new VirtualArg(new VirtualLNumber(1)), + new VirtualArg(new VirtualInt(0)), + new VirtualArg(new VirtualInt(1)), ], $case->getAttributes(), ); @@ -294,8 +294,8 @@ public static function analyze( $case_or_default_equality_expr = new VirtualFuncCall( new VirtualFullyQualified(['rand']), [ - new VirtualArg(new VirtualLNumber(0)), - new VirtualArg(new VirtualLNumber(1)), + new VirtualArg(new VirtualInt(0)), + new VirtualArg(new VirtualInt(1)), ], $case->getAttributes(), ); @@ -690,8 +690,8 @@ private static function simplifyCaseEqualityExpression( } /** - * @param array $in_array_values - * @return ?array + * @param array $in_array_values + * @return ?array */ private static function getOptionsFromNestedOr( PhpParser\Node\Expr $case_equality_expr, diff --git a/src/Psalm/Internal/Analyzer/Statements/BreakAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/BreakAnalyzer.php index 330918a9559..52eede08d3a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/BreakAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/BreakAnalyzer.php @@ -29,7 +29,7 @@ public static function analyze( if ($loop_scope) { if ($context->break_types && end($context->break_types) === 'switch' - && (!$stmt->num instanceof PhpParser\Node\Scalar\LNumber || $stmt->num->value < 2) + && (!$stmt->num instanceof PhpParser\Node\Scalar\Int_ || $stmt->num->value < 2) ) { $loop_scope->final_actions[] = ScopeAnalyzer::ACTION_LEAVE_SWITCH; } else { diff --git a/src/Psalm/Internal/Analyzer/Statements/ContinueAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ContinueAnalyzer.php index b5b0de71f9f..a41a1bca392 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ContinueAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ContinueAnalyzer.php @@ -25,7 +25,7 @@ public static function analyze( PhpParser\Node\Stmt\Continue_ $stmt, Context $context, ): void { - $count = $stmt->num instanceof PhpParser\Node\Scalar\LNumber? $stmt->num->value : 1; + $count = $stmt->num instanceof PhpParser\Node\Scalar\Int_? $stmt->num->value : 1; $loop_scope = $context->loop_scope; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php index 8c6dd5d7ccd..05543abbfad 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php @@ -242,7 +242,7 @@ private static function analyzeArrayItem( StatementsAnalyzer $statements_analyzer, Context $context, ArrayCreationInfo $array_creation_info, - PhpParser\Node\Expr\ArrayItem $item, + PhpParser\Node\ArrayItem $item, Codebase $codebase, ): void { if ($item->unpack) { @@ -519,7 +519,7 @@ private static function analyzeArrayItem( private static function handleUnpackedArray( StatementsAnalyzer $statements_analyzer, ArrayCreationInfo $array_creation_info, - PhpParser\Node\Expr\ArrayItem $item, + PhpParser\Node\ArrayItem $item, Union $unpacked_array_type, Codebase $codebase, ): void { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index ea8840e4689..cad1f07833b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -16,7 +16,7 @@ use PhpParser\Node\Expr\BinaryOp\SmallerOrEqual; use PhpParser\Node\Expr\UnaryMinus; use PhpParser\Node\Expr\UnaryPlus; -use PhpParser\Node\Scalar\LNumber; +use PhpParser\Node\Scalar\Int_; use Psalm\CodeLocation; use Psalm\Codebase; use Psalm\FileSource; @@ -1575,7 +1575,7 @@ private static function hasNonEmptyCountEqualityCheck( } // TODO get node type provider here somehow and check literal ints and int ranges - if ($compare_to instanceof PhpParser\Node\Scalar\LNumber + if ($compare_to instanceof PhpParser\Node\Scalar\Int_ && $compare_to->value > (-1 * $comparison_adjustment) ) { $min_count = $compare_to->value + $comparison_adjustment; @@ -1605,7 +1605,7 @@ private static function hasLessThanCountEqualityCheck( if ($left_count && $operator_less_than_or_equal - && $conditional->right instanceof PhpParser\Node\Scalar\LNumber + && $conditional->right instanceof PhpParser\Node\Scalar\Int_ ) { $max_count = $conditional->right->value - ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Smaller ? 1 : 0); @@ -1624,7 +1624,7 @@ private static function hasLessThanCountEqualityCheck( if ($right_count && $operator_greater_than_or_equal - && $conditional->left instanceof PhpParser\Node\Scalar\LNumber + && $conditional->left instanceof PhpParser\Node\Scalar\Int_ ) { $max_count = $conditional->left->value - ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater ? 1 : 0); @@ -1648,7 +1648,7 @@ private static function hasCountEqualityCheck( && in_array(strtolower($conditional->left->name->getFirst()), ['count', 'sizeof']) && $conditional->left->getArgs(); - if ($left_count && $conditional->right instanceof PhpParser\Node\Scalar\LNumber) { + if ($left_count && $conditional->right instanceof PhpParser\Node\Scalar\Int_) { $count = $conditional->right->value; return self::ASSIGNMENT_TO_RIGHT; @@ -1659,7 +1659,7 @@ private static function hasCountEqualityCheck( && in_array(strtolower($conditional->right->name->getFirst()), ['count', 'sizeof']) && $conditional->right->getArgs(); - if ($right_count && $conditional->left instanceof PhpParser\Node\Scalar\LNumber) { + if ($right_count && $conditional->left instanceof PhpParser\Node\Scalar\Int_) { $count = $conditional->left->value; return self::ASSIGNMENT_TO_LEFT; @@ -1685,13 +1685,13 @@ private static function hasSuperiorNumberCheck( ) { $right_assignment = true; $value_right = $type->getSingleIntLiteral()->value; - } elseif ($conditional->right instanceof LNumber) { + } elseif ($conditional->right instanceof Int_) { $right_assignment = true; $value_right = $conditional->right->value; - } elseif ($conditional->right instanceof UnaryMinus && $conditional->right->expr instanceof LNumber) { + } elseif ($conditional->right instanceof UnaryMinus && $conditional->right->expr instanceof Int_) { $right_assignment = true; $value_right = -$conditional->right->expr->value; - } elseif ($conditional->right instanceof UnaryPlus && $conditional->right->expr instanceof LNumber) { + } elseif ($conditional->right instanceof UnaryPlus && $conditional->right->expr instanceof Int_) { $right_assignment = true; $value_right = $conditional->right->expr->value; } @@ -1709,13 +1709,13 @@ private static function hasSuperiorNumberCheck( ) { $left_assignment = true; $value_left = $type->getSingleIntLiteral()->value; - } elseif ($conditional->left instanceof LNumber) { + } elseif ($conditional->left instanceof Int_) { $left_assignment = true; $value_left = $conditional->left->value; - } elseif ($conditional->left instanceof UnaryMinus && $conditional->left->expr instanceof LNumber) { + } elseif ($conditional->left instanceof UnaryMinus && $conditional->left->expr instanceof Int_) { $left_assignment = true; $value_left = -$conditional->left->expr->value; - } elseif ($conditional->left instanceof UnaryPlus && $conditional->left->expr instanceof LNumber) { + } elseif ($conditional->left instanceof UnaryPlus && $conditional->left->expr instanceof Int_) { $left_assignment = true; $value_left = $conditional->left->expr->value; } @@ -1745,13 +1745,13 @@ private static function hasInferiorNumberCheck( ) { $right_assignment = true; $value_right = $type->getSingleIntLiteral()->value; - } elseif ($conditional->right instanceof LNumber) { + } elseif ($conditional->right instanceof Int_) { $right_assignment = true; $value_right = $conditional->right->value; - } elseif ($conditional->right instanceof UnaryMinus && $conditional->right->expr instanceof LNumber) { + } elseif ($conditional->right instanceof UnaryMinus && $conditional->right->expr instanceof Int_) { $right_assignment = true; $value_right = -$conditional->right->expr->value; - } elseif ($conditional->right instanceof UnaryPlus && $conditional->right->expr instanceof LNumber) { + } elseif ($conditional->right instanceof UnaryPlus && $conditional->right->expr instanceof Int_) { $right_assignment = true; $value_right = $conditional->right->expr->value; } @@ -1769,13 +1769,13 @@ private static function hasInferiorNumberCheck( ) { $left_assignment = true; $value_left = $type->getSingleIntLiteral()->value; - } elseif ($conditional->left instanceof LNumber) { + } elseif ($conditional->left instanceof Int_) { $left_assignment = true; $value_left = $conditional->left->value; - } elseif ($conditional->left instanceof UnaryMinus && $conditional->left->expr instanceof LNumber) { + } elseif ($conditional->left instanceof UnaryMinus && $conditional->left->expr instanceof Int_) { $left_assignment = true; $value_left = -$conditional->left->expr->value; - } elseif ($conditional->left instanceof UnaryPlus && $conditional->left->expr instanceof LNumber) { + } elseif ($conditional->left instanceof UnaryPlus && $conditional->left->expr instanceof Int_) { $left_assignment = true; $value_left = $conditional->left->expr->value; } @@ -1799,7 +1799,7 @@ private static function hasReconcilableNonEmptyCountEqualityCheck( && $conditional->left->name instanceof PhpParser\Node\Name && in_array(strtolower($conditional->left->name->getFirst()), ['count', 'sizeof']); - $right_number = $conditional->right instanceof PhpParser\Node\Scalar\LNumber + $right_number = $conditional->right instanceof PhpParser\Node\Scalar\Int_ && $conditional->right->value === ( $conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater ? 0 : 1); @@ -3775,7 +3775,7 @@ private static function getArrayKeyExistsAssertions( if ($first_arg->value instanceof PhpParser\Node\Scalar\String_) { $first_var_name = '\'' . $first_arg->value->value . '\''; - } elseif ($first_arg->value instanceof PhpParser\Node\Scalar\LNumber) { + } elseif ($first_arg->value instanceof PhpParser\Node\Scalar\Int_) { $first_var_name = (string)$first_arg->value->value; } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php index 4f49634c593..91d28b02088 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php @@ -174,7 +174,7 @@ public static function updateArrayType( if ($value_type instanceof TLiteralString) { $key_values[] = $value_type; } - } elseif ($current_dim instanceof PhpParser\Node\Scalar\LNumber && !$root_is_string) { + } elseif ($current_dim instanceof PhpParser\Node\Scalar\Int_ && !$root_is_string) { $key_values[] = new TLiteralInt($current_dim->value); } elseif ($current_dim && ($key_type = $statements_analyzer->node_data->getType($current_dim)) @@ -1027,7 +1027,7 @@ private static function getDimKeyValues( if ($value_type instanceof TLiteralString) { $key_values[] = $value_type; } - } elseif ($dim instanceof PhpParser\Node\Scalar\LNumber) { + } elseif ($dim instanceof PhpParser\Node\Scalar\Int_) { $key_values[] = new TLiteralInt($dim->value); } else { $key_type = $statements_analyzer->node_data->getType($dim); @@ -1084,12 +1084,12 @@ private static function getArrayAssignmentOffsetType( return [$offset_type, $var_id_addition, true]; } - if ($child_stmt->dim instanceof PhpParser\Node\Scalar\LNumber + if ($child_stmt->dim instanceof PhpParser\Node\Scalar\Int_ || (($child_stmt->dim instanceof PhpParser\Node\Expr\ConstFetch || $child_stmt->dim instanceof PhpParser\Node\Expr\ClassConstFetch) && $child_stmt_dim_type->isSingleIntLiteral()) ) { - if ($child_stmt->dim instanceof PhpParser\Node\Scalar\LNumber) { + if ($child_stmt->dim instanceof PhpParser\Node\Scalar\Int_) { $offset_type = new TLiteralInt($child_stmt->dim->value); } else { $offset_type = $child_stmt_dim_type->getSingleIntLiteral(); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php index c11731e2758..e96263fd83a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php @@ -7,7 +7,7 @@ use PhpParser; use PhpParser\Node\Expr; use PhpParser\Node\Expr\PropertyFetch; -use PhpParser\Node\Stmt\PropertyProperty; +use PhpParser\Node\PropertyItem; use Psalm\CodeLocation; use Psalm\Codebase; use Psalm\Config; @@ -92,8 +92,8 @@ final class InstancePropertyAssignmentAnalyzer { /** - * @param PropertyFetch|PropertyProperty $stmt - * @param bool $direct_assignment whether the variable is assigned explicitly + * @param PropertyFetch|PropertyItem $stmt + * @param bool $direct_assignment whether the variable is assigned explicitly */ public static function analyze( StatementsAnalyzer $statements_analyzer, @@ -106,7 +106,7 @@ public static function analyze( ): void { $codebase = $statements_analyzer->getCodebase(); - if ($stmt instanceof PropertyProperty) { + if ($stmt instanceof PropertyItem) { if (!$context->self || !$stmt->default) { return; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index 2e28081e8e5..6abd4654028 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -1412,7 +1412,7 @@ private static function analyzeDestructuringAssignment( $can_be_empty = !$assign_value_atomic_type instanceof TNonEmptyArray; } elseif ($assign_value_atomic_type instanceof TKeyedArray) { if (($assign_var_item->key instanceof PhpParser\Node\Scalar\String_ - || $assign_var_item->key instanceof PhpParser\Node\Scalar\LNumber) + || $assign_var_item->key instanceof PhpParser\Node\Scalar\Int_) && isset($assign_value_atomic_type->properties[$assign_var_item->key->value]) ) { $new_assign_type = diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index a563b7495f2..ced7c65c93d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -1171,7 +1171,7 @@ private static function evaluateArbitraryParam( || $arg->value instanceof PhpParser\Node\Expr\Array_ || $arg->value instanceof PhpParser\Node\Expr\BinaryOp || $arg->value instanceof PhpParser\Node\Expr\Ternary - || $arg->value instanceof PhpParser\Node\Scalar\Encapsed + || $arg->value instanceof PhpParser\Node\Scalar\InterpolatedString || $arg->value instanceof PhpParser\Node\Expr\PostInc || $arg->value instanceof PhpParser\Node\Expr\PostDec || $arg->value instanceof PhpParser\Node\Expr\PreInc diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php index 14026d3d1ab..a5048bd2f3b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php @@ -18,9 +18,9 @@ use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TypeExpander; use Psalm\Node\Expr\VirtualArray; -use Psalm\Node\Expr\VirtualArrayItem; use Psalm\Node\Scalar\VirtualString; use Psalm\Node\VirtualArg; +use Psalm\Node\VirtualArrayItem; use Psalm\Storage\ClassLikeStorage; use Psalm\Storage\MethodStorage; use Psalm\Type; @@ -203,7 +203,7 @@ public static function handleMagicMethod( $result->existent_method_ids[$method_id->__toString()] = true; $array_values = array_map( - static fn(PhpParser\Node\Arg $arg): PhpParser\Node\Expr\ArrayItem => new VirtualArrayItem( + static fn(PhpParser\Node\Arg $arg): PhpParser\Node\ArrayItem => new VirtualArrayItem( $arg->value, null, false, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php index 80c780de50b..f29b75fbb5a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php @@ -25,9 +25,9 @@ use Psalm\Issue\RedundantFunctionCallGivenDocblockType; use Psalm\IssueBuffer; use Psalm\Node\Expr\VirtualArray; -use Psalm\Node\Expr\VirtualArrayItem; use Psalm\Node\Expr\VirtualVariable; use Psalm\Node\Scalar\VirtualString; +use Psalm\Node\VirtualArrayItem; use Psalm\Type; use Psalm\Type\Atomic\TBool; use Psalm\Type\Atomic\TClassString; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php index 54b93dccbde..85f4d465fa8 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -34,11 +34,11 @@ use Psalm\Issue\UndefinedClass; use Psalm\IssueBuffer; use Psalm\Node\Expr\VirtualArray; -use Psalm\Node\Expr\VirtualArrayItem; use Psalm\Node\Expr\VirtualMethodCall; use Psalm\Node\Expr\VirtualVariable; use Psalm\Node\Scalar\VirtualString; use Psalm\Node\VirtualArg; +use Psalm\Node\VirtualArrayItem; use Psalm\Storage\ClassLikeStorage; use Psalm\Storage\MethodStorage; use Psalm\Type; @@ -670,7 +670,7 @@ private static function handleNamedCall( } $array_values = array_map( - static fn(PhpParser\Node\Arg $arg): PhpParser\Node\Expr\ArrayItem => new VirtualArrayItem( + static fn(PhpParser\Node\Arg $arg): PhpParser\Node\ArrayItem => new VirtualArrayItem( $arg->value, null, false, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php index 5ee02bef358..88ef13bc054 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php @@ -5,7 +5,8 @@ namespace Psalm\Internal\Analyzer\Statements\Expression; use PhpParser; -use PhpParser\Node\Scalar\EncapsedStringPart; +use PhpParser\Node\Expr; +use PhpParser\Node\InterpolatedStringPart; use Psalm\CodeLocation; use Psalm\Context; use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; @@ -32,7 +33,7 @@ final class EncapsulatedStringAnalyzer { public static function analyze( StatementsAnalyzer $statements_analyzer, - PhpParser\Node\Scalar\Encapsed $stmt, + PhpParser\Node\Scalar\InterpolatedString $stmt, Context $context, ): bool { $parent_nodes = []; @@ -44,11 +45,13 @@ public static function analyze( $literal_string = ""; foreach ($stmt->parts as $part) { - if (ExpressionAnalyzer::analyze($statements_analyzer, $part, $context) === false) { - return false; + if ($part instanceof Expr) { + if (ExpressionAnalyzer::analyze($statements_analyzer, $part, $context) === false) { + return false; + } } - if ($part instanceof EncapsedStringPart) { + if ($part instanceof InterpolatedStringPart) { if ($literal_string !== null) { $literal_string .= $part->value; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ExpressionIdentifier.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ExpressionIdentifier.php index 9cf2a92f518..660ad0ff087 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ExpressionIdentifier.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ExpressionIdentifier.php @@ -116,7 +116,7 @@ public static function getExtendedVarId( if ($root_var_id) { if ($stmt->dim instanceof PhpParser\Node\Scalar\String_ - || $stmt->dim instanceof PhpParser\Node\Scalar\LNumber + || $stmt->dim instanceof PhpParser\Node\Scalar\Int_ ) { $offset = $stmt->dim instanceof PhpParser\Node\Scalar\String_ ? '\'' . $stmt->dim->value . '\'' diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php index 70bf0c77412..b6a3ec6d85e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php @@ -501,7 +501,7 @@ public static function getArrayAccessTypeGivenOffset( if ($value_type instanceof TLiteralString) { $key_values[] = $value_type; } - } elseif ($stmt->dim instanceof PhpParser\Node\Scalar\LNumber) { + } elseif ($stmt->dim instanceof PhpParser\Node\Scalar\Int_) { $key_values[] = new TLiteralInt($stmt->dim->value); } elseif ($stmt->dim && ($stmt_dim_type = $statements_analyzer->node_data->getType($stmt->dim))) { $string_literals = $stmt_dim_type->getLiteralStrings(); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/IncDecExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/IncDecExpressionAnalyzer.php index 08abb627c4c..61728389918 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/IncDecExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/IncDecExpressionAnalyzer.php @@ -16,7 +16,7 @@ use Psalm\Node\Expr\BinaryOp\VirtualMinus; use Psalm\Node\Expr\BinaryOp\VirtualPlus; use Psalm\Node\Expr\VirtualAssign; -use Psalm\Node\Scalar\VirtualLNumber; +use Psalm\Node\Scalar\VirtualInt; use Psalm\Type; /** @@ -55,7 +55,7 @@ public static function analyze( ) { $return_type = null; - $fake_right_expr = new VirtualLNumber(1, $stmt->getAttributes()); + $fake_right_expr = new VirtualInt(1, $stmt->getAttributes()); $statements_analyzer->node_data->setType($fake_right_expr, Type::getInt()); ArithmeticOpAnalyzer::analyze( @@ -100,7 +100,7 @@ public static function analyze( ); } } else { - $fake_right_expr = new VirtualLNumber(1, $stmt->getAttributes()); + $fake_right_expr = new VirtualInt(1, $stmt->getAttributes()); $operation = $stmt instanceof PostInc || $stmt instanceof PreInc ? new VirtualPlus( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php index f79479cc530..ce884452f53 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php @@ -332,7 +332,7 @@ public static function getPathTo( $dir_level = 1; if (isset($stmt->getArgs()[1])) { - if ($stmt->getArgs()[1]->value instanceof PhpParser\Node\Scalar\LNumber) { + if ($stmt->getArgs()[1]->value instanceof PhpParser\Node\Scalar\Int_) { $dir_level = $stmt->getArgs()[1]->value->value; } else { if ($statements_analyzer) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/MatchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/MatchAnalyzer.php index 38ab1befc28..98693f2ccf8 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/MatchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/MatchAnalyzer.php @@ -15,7 +15,6 @@ use Psalm\IssueBuffer; use Psalm\Node\Expr\BinaryOp\VirtualIdentical; use Psalm\Node\Expr\VirtualArray; -use Psalm\Node\Expr\VirtualArrayItem; use Psalm\Node\Expr\VirtualConstFetch; use Psalm\Node\Expr\VirtualFuncCall; use Psalm\Node\Expr\VirtualNew; @@ -24,6 +23,7 @@ use Psalm\Node\Expr\VirtualVariable; use Psalm\Node\Name\VirtualFullyQualified; use Psalm\Node\VirtualArg; +use Psalm\Node\VirtualArrayItem; use Psalm\Type; use Psalm\Type\Atomic; use Psalm\Type\Atomic\TEnumCase; @@ -333,7 +333,7 @@ private static function convertCondsToConditional( } $array_items = array_map( - static fn(PhpParser\Node\Expr $cond): PhpParser\Node\Expr\ArrayItem => + static fn(PhpParser\Node\Expr $cond): PhpParser\Node\ArrayItem => new VirtualArrayItem($cond, null, false, $cond->getAttributes()), $conds, ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php index cba0730a470..93b6d68ea48 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php @@ -400,11 +400,11 @@ public static function infer( return Type::getString($stmt->value); } - if ($stmt instanceof PhpParser\Node\Scalar\LNumber) { + if ($stmt instanceof PhpParser\Node\Scalar\Int_) { return Type::getInt(false, $stmt->value); } - if ($stmt instanceof PhpParser\Node\Scalar\DNumber) { + if ($stmt instanceof PhpParser\Node\Scalar\Float_) { return Type::getFloat($stmt->value); } @@ -623,7 +623,7 @@ private static function handleArrayItem( Codebase $codebase, NodeDataProvider $nodes, ArrayCreationInfo $array_creation_info, - PhpParser\Node\Expr\ArrayItem $item, + PhpParser\Node\ArrayItem $item, Aliases $aliases, FileSource $file_source = null, ?array $existing_class_constants = null, @@ -730,7 +730,7 @@ private static function handleArrayItem( $array_creation_info->all_list = $array_creation_info->all_list && $item_is_list_item; if ($item->key instanceof PhpParser\Node\Scalar\String_ - || $item->key instanceof PhpParser\Node\Scalar\LNumber + || $item->key instanceof PhpParser\Node\Scalar\Int_ || !$item->key ) { if ($item_key_value !== null diff --git a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php index e82c928ddbd..203a8780f22 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php @@ -50,7 +50,7 @@ use Psalm\Issue\UnsupportedReferenceUsage; use Psalm\IssueBuffer; use Psalm\Node\Expr\VirtualFuncCall; -use Psalm\Node\Scalar\VirtualEncapsed; +use Psalm\Node\Scalar\VirtualInterpolatedString; use Psalm\Node\VirtualArg; use Psalm\Node\VirtualName; use Psalm\Plugin\EventHandler\Event\AfterExpressionAnalysisEvent; @@ -191,7 +191,7 @@ private static function handleExpression( return true; } - if ($stmt instanceof PhpParser\Node\Scalar\EncapsedStringPart) { + if ($stmt instanceof PhpParser\Node\InterpolatedStringPart) { return true; } @@ -201,13 +201,13 @@ private static function handleExpression( return true; } - if ($stmt instanceof PhpParser\Node\Scalar\LNumber) { + if ($stmt instanceof PhpParser\Node\Scalar\Int_) { $statements_analyzer->node_data->setType($stmt, Type::getInt(false, $stmt->value)); return true; } - if ($stmt instanceof PhpParser\Node\Scalar\DNumber) { + if ($stmt instanceof PhpParser\Node\Scalar\Float_) { $statements_analyzer->node_data->setType($stmt, Type::getFloat($stmt->value)); return true; @@ -276,7 +276,7 @@ private static function handleExpression( return ArrayAnalyzer::analyze($statements_analyzer, $stmt, $context); } - if ($stmt instanceof PhpParser\Node\Scalar\Encapsed) { + if ($stmt instanceof PhpParser\Node\Scalar\InterpolatedString) { return EncapsulatedStringAnalyzer::analyze($statements_analyzer, $stmt, $context); } @@ -376,7 +376,7 @@ private static function handleExpression( } if ($stmt instanceof PhpParser\Node\Expr\ShellExec) { - $concat = new VirtualEncapsed($stmt->parts, $stmt->getAttributes()); + $concat = new VirtualInterpolatedString($stmt->parts, $stmt->getAttributes()); $virtual_call = new VirtualFuncCall(new VirtualName(['shell_exec']), [ new VirtualArg($concat), ], $stmt->getAttributes()); diff --git a/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php b/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php index 4bad245493c..3508d24efb7 100644 --- a/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php @@ -17,7 +17,7 @@ private function checkNonTrivialExpr(PhpParser\Node\Expr $node): bool { if ($node instanceof PhpParser\Node\Expr\ArrayDimFetch || $node instanceof PhpParser\Node\Expr\Closure - || $node instanceof PhpParser\Node\Expr\ClosureUse + || $node instanceof PhpParser\Node\ClosureUse || $node instanceof PhpParser\Node\Expr\Eval_ || $node instanceof PhpParser\Node\Expr\Exit_ || $node instanceof PhpParser\Node\Expr\Include_ diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionResolver.php b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionResolver.php index bfcfc996a89..da64d4115b1 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionResolver.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionResolver.php @@ -218,8 +218,8 @@ public static function getUnresolvedClassConstExpr( } if ($stmt instanceof PhpParser\Node\Scalar\String_ - || $stmt instanceof PhpParser\Node\Scalar\LNumber - || $stmt instanceof PhpParser\Node\Scalar\DNumber + || $stmt instanceof PhpParser\Node\Scalar\Int_ + || $stmt instanceof PhpParser\Node\Scalar\Float_ ) { return new ScalarValue($stmt->value); } @@ -373,11 +373,11 @@ public static function enterConditional( ( $expr->left instanceof PhpParser\Node\Expr\ConstFetch && $expr->left->name->getParts() === ['PHP_VERSION_ID'] - && $expr->right instanceof PhpParser\Node\Scalar\LNumber + && $expr->right instanceof PhpParser\Node\Scalar\Int_ ) || ( $expr->right instanceof PhpParser\Node\Expr\ConstFetch && $expr->right->name->getParts() === ['PHP_VERSION_ID'] - && $expr->left instanceof PhpParser\Node\Scalar\LNumber + && $expr->left instanceof PhpParser\Node\Scalar\Int_ ) ) ) { diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index 54c42971897..e03d6f9a8ad 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -6,11 +6,11 @@ use LogicException; use PhpParser; +use PhpParser\Modifiers; use PhpParser\Node\Identifier; use PhpParser\Node\IntersectionType; use PhpParser\Node\Name; use PhpParser\Node\NullableType; -use PhpParser\Node\Stmt\Class_; use PhpParser\Node\UnionType; use Psalm\Aliases; use Psalm\CodeLocation; @@ -618,7 +618,7 @@ public function start( $property_storage->location = $param_storage->location; $property_storage->stmt_location = new CodeLocation($this->file_scanner, $param); $property_storage->has_default = (bool)$param->default; - $param_type_readonly = (bool)($param->flags & PhpParser\Node\Stmt\Class_::MODIFIER_READONLY); + $param_type_readonly = (bool)($param->flags & PhpParser\Modifiers::READONLY); $property_storage->readonly = $param_type_readonly ?: $var_comment_readonly; $property_storage->allow_private_mutation = $var_comment_allow_private_mutation; $param_storage->promoted_property = true; @@ -626,18 +626,18 @@ public function start( $property_id = $fq_classlike_name . '::$' . $param_storage->name; - switch ($param->flags & Class_::VISIBILITY_MODIFIER_MASK) { - case Class_::MODIFIER_PUBLIC: + switch ($param->flags & Modifiers::VISIBILITY_MASK) { + case Modifiers::PUBLIC: $property_storage->visibility = ClassLikeAnalyzer::VISIBILITY_PUBLIC; $classlike_storage->inheritable_property_ids[$param_storage->name] = $property_id; break; - case Class_::MODIFIER_PROTECTED: + case Modifiers::PROTECTED: $property_storage->visibility = ClassLikeAnalyzer::VISIBILITY_PROTECTED; $classlike_storage->inheritable_property_ids[$param_storage->name] = $property_id; break; - case Class_::MODIFIER_PRIVATE: + case Modifiers::PRIVATE: $property_storage->visibility = ClassLikeAnalyzer::VISIBILITY_PRIVATE; break; } diff --git a/src/Psalm/Internal/Provider/StatementsProvider.php b/src/Psalm/Internal/Provider/StatementsProvider.php index b5bdbef8ac1..d929a8614e7 100644 --- a/src/Psalm/Internal/Provider/StatementsProvider.php +++ b/src/Psalm/Internal/Provider/StatementsProvider.php @@ -6,9 +6,9 @@ use PhpParser; use PhpParser\ErrorHandler\Collecting; -use PhpParser\Lexer\Emulative; use PhpParser\Node\Stmt; use PhpParser\Parser; +use PhpParser\PhpVersion; use Psalm\CodeLocation\ParseErrorLocation; use Psalm\Codebase; use Psalm\Config; @@ -74,8 +74,6 @@ final class StatementsProvider */ private array $deletion_ranges = []; - private static ?Emulative $lexer = null; - private static ?Parser $parser = null; public function __construct( @@ -375,21 +373,11 @@ public static function parseStatements( ?array $existing_statements = null, ?array $file_changes = null, ): array { - $attributes = [ - 'comments', 'startLine', 'startFilePos', 'endFilePos', - ]; - - if (!self::$lexer) { + if (!self::$parser) { $major_version = Codebase::transformPhpVersionId($analysis_php_version_id, 10_000); $minor_version = Codebase::transformPhpVersionId($analysis_php_version_id % 10_000, 100); - self::$lexer = new Emulative([ - 'usedAttributes' => $attributes, - 'phpVersion' => $major_version . '.' . $minor_version, - ]); - } - - if (!self::$parser) { - self::$parser = (new PhpParser\ParserFactory())->create(PhpParser\ParserFactory::ONLY_PHP7, self::$lexer); + $php_version = PhpVersion::fromComponents($major_version, $minor_version); + self::$parser = (new PhpParser\ParserFactory())->createForVersion($php_version); } $used_cached_statements = false; @@ -466,11 +454,6 @@ public static function parseStatements( return $stmts; } - public static function clearLexer(): void - { - self::$lexer = null; - } - public static function clearParser(): void { self::$parser = null; diff --git a/src/Psalm/Internal/RuntimeCaches.php b/src/Psalm/Internal/RuntimeCaches.php index a876f2c5d45..a0a8383d9b9 100644 --- a/src/Psalm/Internal/RuntimeCaches.php +++ b/src/Psalm/Internal/RuntimeCaches.php @@ -40,7 +40,6 @@ public static function clearAll(): void FunctionLikeAnalyzer::clearCache(); ClassLikeStorageProvider::deleteAll(); FileStorageProvider::deleteAll(); - StatementsProvider::clearLexer(); StatementsProvider::clearParser(); ParsedDocblock::resetNewlineBetweenAnnotations(); } diff --git a/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php b/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php index 2ca354dc68f..ed5f7e92800 100644 --- a/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php +++ b/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php @@ -107,7 +107,7 @@ public static function handleOverride(array $args, Codebase $codebase): void if ($args[1]->value->name->getParts() === ['type'] && $args[1]->value->getArgs() - && $args[1]->value->getArgs()[0]->value instanceof PhpParser\Node\Scalar\LNumber + && $args[1]->value->getArgs()[0]->value instanceof PhpParser\Node\Scalar\Int_ ) { $type_offset = $args[1]->value->getArgs()[0]->value->value; } @@ -116,7 +116,7 @@ public static function handleOverride(array $args, Codebase $codebase): void if ($args[1]->value->name->getParts() === ['elementType'] && $args[1]->value->getArgs() - && $args[1]->value->getArgs()[0]->value instanceof PhpParser\Node\Scalar\LNumber + && $args[1]->value->getArgs()[0]->value instanceof PhpParser\Node\Scalar\Int_ ) { $element_type_offset = $args[1]->value->getArgs()[0]->value->value; } @@ -126,7 +126,7 @@ public static function handleOverride(array $args, Codebase $codebase): void && $identifier->name instanceof PhpParser\Node\Identifier && ( $identifier->getArgs() === [] - || $identifier->getArgs()[0]->value instanceof PhpParser\Node\Scalar\LNumber + || $identifier->getArgs()[0]->value instanceof PhpParser\Node\Scalar\Int_ ) ) { $meta_fq_classlike_name = $identifier->class->toString(); @@ -136,7 +136,7 @@ public static function handleOverride(array $args, Codebase $codebase): void if ($map) { $offset = 0; if ($identifier->getArgs() - && $identifier->getArgs()[0]->value instanceof PhpParser\Node\Scalar\LNumber + && $identifier->getArgs()[0]->value instanceof PhpParser\Node\Scalar\Int_ ) { $offset = $identifier->getArgs()[0]->value->value; } @@ -278,7 +278,7 @@ static function ( && $identifier->name instanceof PhpParser\Node\Name\FullyQualified && ( $identifier->getArgs() === [] - || $identifier->getArgs()[0]->value instanceof PhpParser\Node\Scalar\LNumber + || $identifier->getArgs()[0]->value instanceof PhpParser\Node\Scalar\Int_ ) ) { $function_id = strtolower($identifier->name->toString()); @@ -286,7 +286,7 @@ static function ( if ($map) { $offset = 0; if ($identifier->getArgs() - && $identifier->getArgs()[0]->value instanceof PhpParser\Node\Scalar\LNumber + && $identifier->getArgs()[0]->value instanceof PhpParser\Node\Scalar\Int_ ) { $offset = $identifier->getArgs()[0]->value->value; } diff --git a/src/Psalm/Internal/Stubs/Generator/ClassLikeStubGenerator.php b/src/Psalm/Internal/Stubs/Generator/ClassLikeStubGenerator.php index 7a110f6a32e..4d938f6a0a9 100644 --- a/src/Psalm/Internal/Stubs/Generator/ClassLikeStubGenerator.php +++ b/src/Psalm/Internal/Stubs/Generator/ClassLikeStubGenerator.php @@ -11,9 +11,9 @@ use Psalm\Node\Stmt\VirtualClassMethod; use Psalm\Node\Stmt\VirtualInterface; use Psalm\Node\Stmt\VirtualProperty; -use Psalm\Node\Stmt\VirtualPropertyProperty; use Psalm\Node\Stmt\VirtualTrait; use Psalm\Node\VirtualConst; +use Psalm\Node\VirtualPropertyItem; use Psalm\Storage\ClassLikeStorage; use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Internal\Scanner\ParsedDocblock; @@ -142,10 +142,10 @@ private static function getConstantNodes(Codebase $codebase, ClassLikeStorage $s ) ], $constant_storage->visibility === ClassLikeAnalyzer::VISIBILITY_PUBLIC - ? PhpParser\Node\Stmt\Class_::MODIFIER_PUBLIC + ? PhpParser\Modifiers::PUBLIC : ($constant_storage->visibility === ClassLikeAnalyzer::VISIBILITY_PROTECTED - ? PhpParser\Node\Stmt\Class_::MODIFIER_PROTECTED - : PhpParser\Node\Stmt\Class_::MODIFIER_PRIVATE) + ? PhpParser\Modifiers::PROTECTED + : PhpParser\Modifiers::PRIVATE) ); } @@ -163,9 +163,9 @@ private static function getPropertyNodes(ClassLikeStorage $storage): array foreach ($storage->properties as $property_name => $property_storage) { $flag = match ($property_storage->visibility) { - ClassLikeAnalyzer::VISIBILITY_PRIVATE => PhpParser\Node\Stmt\Class_::MODIFIER_PRIVATE, - ClassLikeAnalyzer::VISIBILITY_PROTECTED => PhpParser\Node\Stmt\Class_::MODIFIER_PROTECTED, - default => PhpParser\Node\Stmt\Class_::MODIFIER_PUBLIC, + ClassLikeAnalyzer::VISIBILITY_PRIVATE => PhpParser\Modifiers::PRIVATE, + ClassLikeAnalyzer::VISIBILITY_PROTECTED => PhpParser\Modifiers::PROTECTED, + default => PhpParser\Modifiers::PUBLIC, }; $docblock = new ParsedDocblock('', []); @@ -182,9 +182,9 @@ private static function getPropertyNodes(ClassLikeStorage $storage): array } $property_nodes[] = new VirtualProperty( - $flag | ($property_storage->is_static ? PhpParser\Node\Stmt\Class_::MODIFIER_STATIC : 0), + $flag | ($property_storage->is_static ? PhpParser\Modifiers::STATIC : 0), [ - new VirtualPropertyProperty( + new VirtualPropertyItem( $property_name, $property_storage->suggested_type ? StubsGenerator::getExpressionFromType($property_storage->suggested_type) @@ -222,9 +222,9 @@ private static function getMethodNodes(ClassLikeStorage $storage): array { } $flag = match ($method_storage->visibility) { - ReflectionProperty::IS_PRIVATE => PhpParser\Node\Stmt\Class_::MODIFIER_PRIVATE, - ReflectionProperty::IS_PROTECTED => PhpParser\Node\Stmt\Class_::MODIFIER_PROTECTED, - default => PhpParser\Node\Stmt\Class_::MODIFIER_PUBLIC, + ReflectionProperty::IS_PRIVATE => PhpParser\Modifiers::PRIVATE, + ReflectionProperty::IS_PROTECTED => PhpParser\Modifiers::PROTECTED, + default => PhpParser\Modifiers::PUBLIC, }; $docblock = new ParsedDocblock('', []); @@ -276,8 +276,8 @@ private static function getMethodNodes(ClassLikeStorage $storage): array { $method_storage->cased_name, [ 'flags' => $flag - | ($method_storage->is_static ? PhpParser\Node\Stmt\Class_::MODIFIER_STATIC : 0) - | ($method_storage->abstract ? PhpParser\Node\Stmt\Class_::MODIFIER_ABSTRACT : 0), + | ($method_storage->is_static ? PhpParser\Modifiers::STATIC : 0) + | ($method_storage->abstract ? PhpParser\Modifiers::ABSTRACT : 0), 'params' => StubsGenerator::getFunctionParamNodes($method_storage), 'returnType' => $method_storage->signature_return_type ? StubsGenerator::getParserTypeFromPsalmType($method_storage->signature_return_type) diff --git a/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php b/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php index 301c2979d0b..19e212854b9 100644 --- a/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php +++ b/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php @@ -23,13 +23,13 @@ use PhpParser; use Psalm\Internal\Scanner\ParsedDocblock; use Psalm\Node\Expr\VirtualArray; -use Psalm\Node\Expr\VirtualArrayItem; +use Psalm\Node\VirtualArrayItem; use Psalm\Node\Expr\VirtualClassConstFetch; use Psalm\Node\Expr\VirtualConstFetch; use Psalm\Node\Expr\VirtualVariable; use Psalm\Node\Name\VirtualFullyQualified; -use Psalm\Node\Scalar\VirtualDNumber; -use Psalm\Node\Scalar\VirtualLNumber; +use Psalm\Node\Scalar\VirtualFloat; +use Psalm\Node\Scalar\VirtualInt; use Psalm\Node\Scalar\VirtualString; use Psalm\Node\Stmt\VirtualFunction; use Psalm\Node\Stmt\VirtualNamespace; @@ -365,11 +365,11 @@ public static function getExpressionFromType(Union $type) : PhpParser\Node\Expr } if ($atomic_type instanceof TLiteralInt) { - return new VirtualLNumber($atomic_type->value); + return new VirtualInt($atomic_type->value); } if ($atomic_type instanceof TLiteralFloat) { - return new VirtualDNumber($atomic_type->value); + return new VirtualFloat($atomic_type->value); } if ($atomic_type instanceof TFalse) { @@ -395,7 +395,7 @@ public static function getExpressionFromType(Union $type) : PhpParser\Node\Expr if ($atomic_type->is_list) { $key_type = null; } elseif (is_int($property_name)) { - $key_type = new VirtualLNumber($property_name); + $key_type = new VirtualInt($property_name); } else { $key_type = new VirtualString($property_name); } diff --git a/src/Psalm/Node/Scalar/VirtualDNumber.php b/src/Psalm/Node/Scalar/VirtualDNumber.php deleted file mode 100644 index 9a95e9b0312..00000000000 --- a/src/Psalm/Node/Scalar/VirtualDNumber.php +++ /dev/null @@ -1,13 +0,0 @@ -expr; } diff --git a/tests/ClosureTest.php b/tests/ClosureTest.php index 7e3b5b77fde..27b6d1db366 100644 --- a/tests/ClosureTest.php +++ b/tests/ClosureTest.php @@ -1346,7 +1346,7 @@ function () { );', 'error_message' => 'InvalidArgument', ], - 'undefinedVariableInEncapsedString' => [ + 'undefinedVariableInInterpolatedString' => [ 'code' => ' "$a"; ', From d51e3945d7c50b19cfe3cee84aaab5683a283c37 Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Mon, 8 Jan 2024 23:11:23 +1300 Subject: [PATCH 266/357] Address parser changes to Throw --- .../Analyzer/FunctionLike/ReturnTypeCollector.php | 9 ++------- src/Psalm/Internal/Analyzer/ScopeAnalyzer.php | 13 +++++++------ .../Statements/{ => Expression}/ThrowAnalyzer.php | 12 ++++-------- .../Analyzer/Statements/ExpressionAnalyzer.php | 3 ++- src/Psalm/Internal/Analyzer/StatementsAnalyzer.php | 3 --- src/Psalm/Node/Stmt/VirtualThrow.php | 13 ------------- 6 files changed, 15 insertions(+), 38 deletions(-) rename src/Psalm/Internal/Analyzer/Statements/{ => Expression}/ThrowAnalyzer.php (91%) delete mode 100644 src/Psalm/Node/Stmt/VirtualThrow.php diff --git a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php index 6ecc2026c72..9d6a0bf62d2 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php +++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php @@ -77,14 +77,9 @@ public static function getReturnTypes( break; } - if ($stmt instanceof PhpParser\Node\Stmt\Throw_) { - $return_types[] = Type::getNever(); - - break; - } - if ($stmt instanceof PhpParser\Node\Stmt\Expression) { - if ($stmt->expr instanceof PhpParser\Node\Expr\Exit_) { + if ($stmt->expr instanceof PhpParser\Node\Expr\Exit_ + || $stmt->expr instanceof PhpParser\Node\Expr\Throw_) { $return_types[] = Type::getNever(); break; diff --git a/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php b/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php index 7fb9a18464a..25cca24d9ec 100644 --- a/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php @@ -50,8 +50,9 @@ public static function getControlActions( foreach ($stmts as $stmt) { if ($stmt instanceof PhpParser\Node\Stmt\Return_ || - $stmt instanceof PhpParser\Node\Stmt\Throw_ || - ($stmt instanceof PhpParser\Node\Stmt\Expression && $stmt->expr instanceof PhpParser\Node\Expr\Exit_) + ($stmt instanceof PhpParser\Node\Stmt\Expression + && ($stmt->expr instanceof PhpParser\Node\Expr\Exit_ + || $stmt->expr instanceof PhpParser\Node\Expr\Throw_)) ) { if (!$return_is_exit && $stmt instanceof PhpParser\Node\Stmt\Return_) { $stmt_return_type = null; @@ -408,9 +409,9 @@ public static function onlyThrowsOrExits(NodeTypeProvider $type_provider, array for ($i = count($stmts) - 1; $i >= 0; --$i) { $stmt = $stmts[$i]; - if ($stmt instanceof PhpParser\Node\Stmt\Throw_ - || ($stmt instanceof PhpParser\Node\Stmt\Expression - && $stmt->expr instanceof PhpParser\Node\Expr\Exit_) + if ($stmt instanceof PhpParser\Node\Stmt\Expression + && ($stmt->expr instanceof PhpParser\Node\Expr\Exit_ + || $stmt->expr instanceof PhpParser\Node\Expr\Throw_) ) { return true; } @@ -438,7 +439,7 @@ public static function onlyThrows(array $stmts): bool } foreach ($stmts as $stmt) { - if ($stmt instanceof PhpParser\Node\Stmt\Throw_) { + if ($stmt instanceof PhpParser\Node\Stmt\Expression && $stmt->expr instanceof PhpParser\Node\Expr\Throw_) { return true; } } diff --git a/src/Psalm/Internal/Analyzer/Statements/ThrowAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ThrowAnalyzer.php similarity index 91% rename from src/Psalm/Internal/Analyzer/Statements/ThrowAnalyzer.php rename to src/Psalm/Internal/Analyzer/Statements/Expression/ThrowAnalyzer.php index 890c0637204..2d0c0a07653 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ThrowAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ThrowAnalyzer.php @@ -2,11 +2,12 @@ declare(strict_types=1); -namespace Psalm\Internal\Analyzer\Statements; +namespace Psalm\Internal\Analyzer\Statements\Expression; use PhpParser; use Psalm\CodeLocation; use Psalm\Context; +use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\Comparator\UnionTypeComparator; use Psalm\Issue\InvalidThrow; @@ -20,12 +21,9 @@ */ final class ThrowAnalyzer { - /** - * @param PhpParser\Node\Stmt\Throw_|PhpParser\Node\Expr\Throw_ $stmt - */ public static function analyze( StatementsAnalyzer $statements_analyzer, - PhpParser\Node $stmt, + PhpParser\Node\Expr\Throw_ $stmt, Context $context, ): bool { $context->inside_throw = true; @@ -87,9 +85,7 @@ public static function analyze( } } - if ($stmt instanceof PhpParser\Node\Expr\Throw_) { - $statements_analyzer->node_data->setType($stmt, Type::getNever()); - } + $statements_analyzer->node_data->setType($stmt, Type::getNever()); return true; } diff --git a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php index 203a8780f22..2d432b16b89 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php @@ -40,6 +40,7 @@ use Psalm\Internal\Analyzer\Statements\Expression\NullsafeAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\PrintAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\TernaryAnalyzer; +use Psalm\Internal\Analyzer\Statements\Expression\ThrowAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\UnaryPlusMinusAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\YieldAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\YieldFromAnalyzer; @@ -420,7 +421,7 @@ private static function handleExpression( return MatchAnalyzer::analyze($statements_analyzer, $stmt, $context); } - if ($stmt instanceof PhpParser\Node\Expr\Throw_ && $analysis_php_version_id >= 8_00_00) { + if ($stmt instanceof PhpParser\Node\Expr\Throw_) { return ThrowAnalyzer::analyze($statements_analyzer, $stmt, $context); } diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index 0ccadac662e..37540217361 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -35,7 +35,6 @@ use Psalm\Internal\Analyzer\Statements\GlobalAnalyzer; use Psalm\Internal\Analyzer\Statements\ReturnAnalyzer; use Psalm\Internal\Analyzer\Statements\StaticAnalyzer; -use Psalm\Internal\Analyzer\Statements\ThrowAnalyzer; use Psalm\Internal\Analyzer\Statements\UnsetAnalyzer; use Psalm\Internal\Analyzer\Statements\UnusedAssignmentRemover; use Psalm\Internal\Codebase\DataFlowGraph; @@ -536,8 +535,6 @@ private static function analyzeStatement( UnsetAnalyzer::analyze($statements_analyzer, $stmt, $context); } elseif ($stmt instanceof PhpParser\Node\Stmt\Return_) { ReturnAnalyzer::analyze($statements_analyzer, $stmt, $context); - } elseif ($stmt instanceof PhpParser\Node\Stmt\Throw_) { - ThrowAnalyzer::analyze($statements_analyzer, $stmt, $context); } elseif ($stmt instanceof PhpParser\Node\Stmt\Switch_) { SwitchAnalyzer::analyze($statements_analyzer, $stmt, $context); } elseif ($stmt instanceof PhpParser\Node\Stmt\Break_) { diff --git a/src/Psalm/Node/Stmt/VirtualThrow.php b/src/Psalm/Node/Stmt/VirtualThrow.php deleted file mode 100644 index 304a521bf25..00000000000 --- a/src/Psalm/Node/Stmt/VirtualThrow.php +++ /dev/null @@ -1,13 +0,0 @@ - Date: Tue, 9 Jan 2024 09:42:44 +1300 Subject: [PATCH 267/357] Update CustomTraverser traverseNode method --- src/Psalm/Internal/PhpTraverser/CustomTraverser.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Psalm/Internal/PhpTraverser/CustomTraverser.php b/src/Psalm/Internal/PhpTraverser/CustomTraverser.php index f1e2673572d..fbe4429db9d 100644 --- a/src/Psalm/Internal/PhpTraverser/CustomTraverser.php +++ b/src/Psalm/Internal/PhpTraverser/CustomTraverser.php @@ -27,9 +27,8 @@ public function __construct() * Recursively traverse a node. * * @param Node $node node to traverse - * @return Node Result of traversal (may be original node or new one) */ - protected function traverseNode(Node $node): Node + protected function traverseNode(Node $node): void { foreach ($node->getSubNodeNames() as $name) { $subNode = &$node->$name; @@ -60,7 +59,7 @@ protected function traverseNode(Node $node): Node } if ($traverseChildren) { - $subNode = $this->traverseNode($subNode); + $this->traverseNode($subNode); if ($this->stopTraversal) { break; } @@ -88,8 +87,6 @@ protected function traverseNode(Node $node): Node } } } - - return $node; } /** @@ -124,7 +121,7 @@ protected function traverseArray(array $nodes): array } if ($traverseChildren) { - $node = $this->traverseNode($node); + $this->traverseNode($node); if ($this->stopTraversal) { break; } From 087b1a195f1f30f62840c91a640b60dbc29c2a0d Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Tue, 9 Jan 2024 22:29:53 +1300 Subject: [PATCH 268/357] Get inline assignment doc comment from statement --- .../Statements/Expression/AssignmentAnalyzer.php | 2 +- .../Statements/Expression/Call/ArgumentsAnalyzer.php | 2 +- .../Analyzer/Statements/ExpressionAnalyzer.php | 10 +++++----- src/Psalm/Internal/Analyzer/StatementsAnalyzer.php | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index 6abd4654028..6931b9d46ff 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -900,7 +900,7 @@ public static function analyzeAssignmentRef( PhpParser\Node\Expr\AssignRef $stmt, Context $context, ): bool { - ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context, false, null, false, null, true); + ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context, false, null, null, null, true); $lhs_var_id = ExpressionIdentifier::getExtendedVarId( $stmt->var, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index ced7c65c93d..7151f232b15 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -237,7 +237,7 @@ public static function analyze( $context, false, null, - false, + null, $high_order_template_result, ) === false) { $context->inside_call = $was_inside_call; diff --git a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php index 2d432b16b89..7c944a8051c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php @@ -76,7 +76,7 @@ public static function analyze( Context $context, bool $array_assignment = false, ?Context $global_context = null, - bool $from_stmt = false, + PhpParser\Node\Stmt $from_stmt = null, ?TemplateResult $template_result = null, bool $assigned_to_reference = false, ): bool { @@ -147,7 +147,7 @@ private static function handleExpression( Context $context, bool $array_assignment, ?Context $global_context, - bool $from_stmt, + ?PhpParser\Node\Stmt $from_stmt, ?TemplateResult $template_result = null, bool $assigned_to_reference = false, ): bool { @@ -257,7 +257,7 @@ private static function handleExpression( $stmt, $context, 0, - $from_stmt, + $from_stmt !== null, ); } @@ -461,7 +461,7 @@ private static function analyzeAssignment( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr $stmt, Context $context, - bool $from_stmt, + ?PhpParser\Node\Stmt $from_stmt, ): bool { $assignment_type = AssignmentAnalyzer::analyze( $statements_analyzer, @@ -469,7 +469,7 @@ private static function analyzeAssignment( $stmt->expr, null, $context, - $stmt->getDocComment(), + $stmt->getDocComment() ?? $from_stmt?->getDocComment(), [], !$from_stmt ? $stmt : null, ); diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index 37540217361..ec9e3df80a8 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -556,7 +556,7 @@ private static function analyzeStatement( $context, false, $global_context, - true, + $stmt, ) === false) { return false; } From e394609b4c22e0f70014e2516e3504af1e5332f3 Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Tue, 9 Jan 2024 22:45:45 +1300 Subject: [PATCH 269/357] Add suppressions --- psalm.xml.dist | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/psalm.xml.dist b/psalm.xml.dist index 5e8e8ac33d0..aea7d6775e1 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -149,5 +149,13 @@ + + + + + + + + From 4b3846454d0d95127201e97011a9a883f24eac6b Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Wed, 10 Jan 2024 08:00:41 +1300 Subject: [PATCH 270/357] Replace deprecated constants --- src/Psalm/Internal/PhpTraverser/CustomTraverser.php | 2 +- .../Internal/PhpVisitor/AssignmentMapVisitor.php | 4 ++-- .../Internal/PhpVisitor/CheckTrivialExprVisitor.php | 4 ++-- .../Internal/PhpVisitor/ParamReplacementVisitor.php | 4 ++-- .../Internal/PhpVisitor/PartialParserVisitor.php | 12 ++++++------ src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php | 4 ++-- src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php | 2 +- src/Psalm/Internal/PhpVisitor/TraitFinder.php | 2 +- src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php | 3 +-- 9 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/Psalm/Internal/PhpTraverser/CustomTraverser.php b/src/Psalm/Internal/PhpTraverser/CustomTraverser.php index fbe4429db9d..2f32f5c6379 100644 --- a/src/Psalm/Internal/PhpTraverser/CustomTraverser.php +++ b/src/Psalm/Internal/PhpTraverser/CustomTraverser.php @@ -144,7 +144,7 @@ protected function traverseArray(array $nodes): array } elseif (false === $return) { throw new LogicException( 'bool(false) return from leaveNode() no longer supported. ' . - 'Return NodeTraverser::REMOVE_NODE instead', + 'Return NodeVisitor::REMOVE_NODE instead', ); } else { throw new LogicException( diff --git a/src/Psalm/Internal/PhpVisitor/AssignmentMapVisitor.php b/src/Psalm/Internal/PhpVisitor/AssignmentMapVisitor.php index de750effdf5..440e1aa2db8 100644 --- a/src/Psalm/Internal/PhpVisitor/AssignmentMapVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/AssignmentMapVisitor.php @@ -51,7 +51,7 @@ public function enterNode(PhpParser\Node $node): ?int } } - return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN; + return PhpParser\NodeVisitor::DONT_TRAVERSE_CHILDREN; } if ($node instanceof PhpParser\Node\Expr\PostInc @@ -66,7 +66,7 @@ public function enterNode(PhpParser\Node $node): ?int $this->assignment_map[$var_id][$var_id] = true; } - return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN; + return PhpParser\NodeVisitor::DONT_TRAVERSE_CHILDREN; } if ($node instanceof PhpParser\Node\Expr\FuncCall diff --git a/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php b/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php index 3508d24efb7..eda795a1afa 100644 --- a/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php @@ -55,7 +55,7 @@ public function enterNode(PhpParser\Node $node): ?int // Check for Non-Trivial Expression first if ($this->checkNonTrivialExpr($node)) { $this->has_non_trivial_expr = true; - return PhpParser\NodeTraverser::STOP_TRAVERSAL; + return self::STOP_TRAVERSAL; } if ($node instanceof PhpParser\Node\Expr\ClassConstFetch @@ -63,7 +63,7 @@ public function enterNode(PhpParser\Node $node): ?int || $node instanceof PhpParser\Node\Expr\Error || $node instanceof PhpParser\Node\Expr\PropertyFetch || $node instanceof PhpParser\Node\Expr\StaticPropertyFetch) { - return PhpParser\NodeTraverser::STOP_TRAVERSAL; + return self::STOP_TRAVERSAL; } } return null; diff --git a/src/Psalm/Internal/PhpVisitor/ParamReplacementVisitor.php b/src/Psalm/Internal/PhpVisitor/ParamReplacementVisitor.php index a10b264916f..a09b2c1eb53 100644 --- a/src/Psalm/Internal/PhpVisitor/ParamReplacementVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ParamReplacementVisitor.php @@ -43,7 +43,7 @@ public function enterNode(PhpParser\Node $node): ?int } elseif ($node->name === $this->new_name) { if ($this->new_new_name_used) { $this->replacements = []; - return PhpParser\NodeTraverser::STOP_TRAVERSAL; + return self::STOP_TRAVERSAL; } $this->replacements[] = new FileManipulation( @@ -56,7 +56,7 @@ public function enterNode(PhpParser\Node $node): ?int } elseif ($node->name === $this->new_name . '_new') { if ($this->new_name_replaced) { $this->replacements = []; - return PhpParser\NodeTraverser::STOP_TRAVERSAL; + return self::STOP_TRAVERSAL; } $this->new_new_name_used = true; diff --git a/src/Psalm/Internal/PhpVisitor/PartialParserVisitor.php b/src/Psalm/Internal/PhpVisitor/PartialParserVisitor.php index 19040e27a92..d185eadea03 100644 --- a/src/Psalm/Internal/PhpVisitor/PartialParserVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/PartialParserVisitor.php @@ -103,7 +103,7 @@ public function enterNode(PhpParser\Node $node, bool &$traverseChildren = true): if ($a_e2 > $stmt_end_pos) { $this->must_rescan = true; - return PhpParser\NodeTraverser::STOP_TRAVERSAL; + return self::STOP_TRAVERSAL; } $end_offset = $b_e2 - $a_e2; @@ -139,7 +139,7 @@ public function enterNode(PhpParser\Node $node, bool &$traverseChildren = true): if (!$method_contents) { $this->must_rescan = true; - return PhpParser\NodeTraverser::STOP_TRAVERSAL; + return self::STOP_TRAVERSAL; } $error_handler = new Collecting(); @@ -204,7 +204,7 @@ public function enterNode(PhpParser\Node $node, bool &$traverseChildren = true): ) { $this->must_rescan = true; - return PhpParser\NodeTraverser::STOP_TRAVERSAL; + return self::STOP_TRAVERSAL; } // changes "): {" to ") {" @@ -227,7 +227,7 @@ public function enterNode(PhpParser\Node $node, bool &$traverseChildren = true): ) { $this->must_rescan = true; - return PhpParser\NodeTraverser::STOP_TRAVERSAL; + return self::STOP_TRAVERSAL; } } @@ -286,7 +286,7 @@ public function enterNode(PhpParser\Node $node, bool &$traverseChildren = true): $this->must_rescan = true; - return PhpParser\NodeTraverser::STOP_TRAVERSAL; + return self::STOP_TRAVERSAL; } if ($node->stmts) { @@ -317,7 +317,7 @@ public function enterNode(PhpParser\Node $node, bool &$traverseChildren = true): $this->must_rescan = true; - return PhpParser\NodeTraverser::STOP_TRAVERSAL; + return self::STOP_TRAVERSAL; } if ($start_offset !== 0 || $end_offset !== 0 || $line_offset !== 0) { diff --git a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php index 54d9ce3d323..20d3d9d337c 100644 --- a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php @@ -143,7 +143,7 @@ public function enterNode(PhpParser\Node $node): ?int if ($classlike_node_scanner->start($node) === false) { $this->bad_classes[spl_object_id($node)] = true; - return PhpParser\NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; + return self::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } $this->classlike_node_scanners[] = $classlike_node_scanner; @@ -212,7 +212,7 @@ public function enterNode(PhpParser\Node $node): ?int } if (!$this->scan_deep) { - return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN; + return self::DONT_TRAVERSE_CHILDREN; } } elseif ($node instanceof PhpParser\Node\Stmt\Global_) { $functionlike_node_scanner = end($this->functionlike_node_scanners); diff --git a/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php b/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php index 455a22696df..193cce5a197 100644 --- a/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php +++ b/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php @@ -93,7 +93,7 @@ public function enterNode(Node $node): ?int if ($attrs['endFilePos'] < $this->start_change || $attrs['startFilePos'] > $this->end_change ) { - return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN; + return PhpParser\NodeVisitor::DONT_TRAVERSE_CHILDREN; } } diff --git a/src/Psalm/Internal/PhpVisitor/TraitFinder.php b/src/Psalm/Internal/PhpVisitor/TraitFinder.php index 9decb8d8af1..e7640cce7df 100644 --- a/src/Psalm/Internal/PhpVisitor/TraitFinder.php +++ b/src/Psalm/Internal/PhpVisitor/TraitFinder.php @@ -53,7 +53,7 @@ public function enterNode(PhpParser\Node $node, bool &$traverseChildren = true): if ($node instanceof PhpParser\Node\Stmt\ClassLike || $node instanceof PhpParser\Node\FunctionLike ) { - return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN; + return PhpParser\NodeVisitor::DONT_TRAVERSE_CHILDREN; } return null; diff --git a/src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php b/src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php index 94de49e6619..03912348153 100644 --- a/src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php +++ b/src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php @@ -8,7 +8,6 @@ use PhpParser\Node\Expr\YieldFrom; use PhpParser\Node\Expr\Yield_; use PhpParser\Node\FunctionLike; -use PhpParser\NodeTraverser; use PhpParser\NodeVisitorAbstract; use Psalm\Internal\Provider\NodeDataProvider; use Psalm\Type; @@ -63,7 +62,7 @@ public function enterNode(Node $node): ?int $this->yield_types []= Type::getMixed(); } elseif ($node instanceof FunctionLike) { - return NodeTraverser::DONT_TRAVERSE_CHILDREN; + return self::DONT_TRAVERSE_CHILDREN; } return null; From 8293da9b53b75f3042c0a967f82cd24f94360e6f Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Wed, 10 Jan 2024 21:57:56 +1300 Subject: [PATCH 271/357] Fix CheckTrivialExprVisitor for ClosureUse --- src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php b/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php index eda795a1afa..ce4da4e2191 100644 --- a/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php @@ -17,7 +17,6 @@ private function checkNonTrivialExpr(PhpParser\Node\Expr $node): bool { if ($node instanceof PhpParser\Node\Expr\ArrayDimFetch || $node instanceof PhpParser\Node\Expr\Closure - || $node instanceof PhpParser\Node\ClosureUse || $node instanceof PhpParser\Node\Expr\Eval_ || $node instanceof PhpParser\Node\Expr\Exit_ || $node instanceof PhpParser\Node\Expr\Include_ @@ -65,6 +64,9 @@ public function enterNode(PhpParser\Node $node): ?int || $node instanceof PhpParser\Node\Expr\StaticPropertyFetch) { return self::STOP_TRAVERSAL; } + } elseif ($node instanceof PhpParser\Node\ClosureUse) { + $this->has_non_trivial_expr = true; + return self::STOP_TRAVERSAL; } return null; } From 662354e0597f7a427b74a221ea953fec45dba17e Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Wed, 10 Jan 2024 22:26:33 +1300 Subject: [PATCH 272/357] Fix type for node attributes --- .../Internal/Analyzer/Statements/Expression/MatchAnalyzer.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/MatchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/MatchAnalyzer.php index 98693f2ccf8..3d2e242748a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/MatchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/MatchAnalyzer.php @@ -318,6 +318,7 @@ public static function analyze( /** * @param non-empty-list $conds + * @param array $attributes */ private static function convertCondsToConditional( array $conds, From 029ec43eb1c68d0bc79aa1a7c7efc58afcdb8319 Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Thu, 11 Jan 2024 22:10:03 +1300 Subject: [PATCH 273/357] Handle closure statements with docblocks --- .../Reflector/FunctionLikeNodeScanner.php | 3 ++- .../Internal/PhpVisitor/ReflectorVisitor.php | 20 +++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index e03d6f9a8ad..b91b521ec88 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -100,6 +100,7 @@ public function __construct( public function start( PhpParser\Node\FunctionLike $stmt, bool $fake_method = false, + PhpParser\Comment\Doc $doc_comment = null, ): FunctionStorage|MethodStorage|false { if ($stmt instanceof PhpParser\Node\Expr\Closure || $stmt instanceof PhpParser\Node\Expr\ArrowFunction @@ -411,7 +412,7 @@ public function start( $storage->returns_by_ref = true; } - $doc_comment = $stmt->getDocComment(); + $doc_comment = $stmt->getDocComment() ?? $doc_comment; if ($classlike_storage && !$classlike_storage->is_trait) { diff --git a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php index 20d3d9d337c..c0cc88ae482 100644 --- a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php @@ -34,6 +34,7 @@ use Psalm\Storage\FileStorage; use Psalm\Storage\MethodStorage; use Psalm\Type; +use SplObjectStorage; use UnexpectedValueException; use function array_pop; @@ -84,6 +85,11 @@ final class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements Fi private array $bad_classes = []; private readonly EventDispatcher $eventDispatcher; + /** + * @var SplObjectStorage + */ + private SplObjectStorage $closure_statements; + public function __construct( private readonly Codebase $codebase, private readonly FileScanner $file_scanner, @@ -93,6 +99,7 @@ public function __construct( $this->scan_deep = $file_scanner->will_analyze; $this->aliases = $this->file_storage->aliases = new Aliases(); $this->eventDispatcher = $this->codebase->config->eventDispatcher; + $this->closure_statements = new SplObjectStorage(); } public function enterNode(PhpParser\Node $node): ?int @@ -160,13 +167,22 @@ public function enterNode(PhpParser\Node $node): ?int } } } - } elseif ($node instanceof PhpParser\Node\FunctionLike) { + } elseif ($node instanceof PhpParser\Node\FunctionLike || $node instanceof PhpParser\Node\Stmt\Expression && ($node->expr instanceof PhpParser\Node\Expr\ArrowFunction || $node->expr instanceof PhpParser\Node\Expr\Closure)) { + $doc_comment = null; if ($node instanceof PhpParser\Node\Stmt\Function_ || $node instanceof PhpParser\Node\Stmt\ClassMethod ) { if ($this->skip_if_descendants) { return null; } + } elseif ($node instanceof PhpParser\Node\Stmt\Expression) { + $doc_comment = $node->getDocComment(); + /** @var PhpParser\Node\FunctionLike */ + $node = $node->expr; + $this->closure_statements->attach($node); + } elseif ($this->closure_statements->contains($node)) { + // This is a closure that was already processed at the statement level. + return null; } $classlike_storage = null; @@ -193,7 +209,7 @@ public function enterNode(PhpParser\Node $node): ?int $functionlike_types, ); - $functionlike_node_scanner->start($node); + $functionlike_node_scanner->start($node, false, $doc_comment); $this->functionlike_node_scanners[] = $functionlike_node_scanner; From 8f7158fc70839d2daf63fd0a3204d1f82e862c35 Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Fri, 12 Jan 2024 21:43:16 +1300 Subject: [PATCH 274/357] Handle closures passed as arguments with docblocks --- src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php | 14 +++++++++++++- tests/ClosureTest.php | 13 +++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php index c0cc88ae482..b22cb9fc7fc 100644 --- a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php @@ -167,7 +167,14 @@ public function enterNode(PhpParser\Node $node): ?int } } } - } elseif ($node instanceof PhpParser\Node\FunctionLike || $node instanceof PhpParser\Node\Stmt\Expression && ($node->expr instanceof PhpParser\Node\Expr\ArrowFunction || $node->expr instanceof PhpParser\Node\Expr\Closure)) { + } elseif ($node instanceof PhpParser\Node\FunctionLike + || $node instanceof PhpParser\Node\Stmt\Expression + && ($node->expr instanceof PhpParser\Node\Expr\ArrowFunction + || $node->expr instanceof PhpParser\Node\Expr\Closure) + || $node instanceof PhpParser\Node\Arg + && ($node->value instanceof PhpParser\Node\Expr\ArrowFunction + || $node->value instanceof PhpParser\Node\Expr\Closure) + ) { $doc_comment = null; if ($node instanceof PhpParser\Node\Stmt\Function_ || $node instanceof PhpParser\Node\Stmt\ClassMethod @@ -180,6 +187,11 @@ public function enterNode(PhpParser\Node $node): ?int /** @var PhpParser\Node\FunctionLike */ $node = $node->expr; $this->closure_statements->attach($node); + } elseif ($node instanceof PhpParser\Node\Arg) { + $doc_comment = $node->getDocComment(); + /** @var PhpParser\Node\FunctionLike */ + $node = $node->value; + $this->closure_statements->attach($node); } elseif ($this->closure_statements->contains($node)) { // This is a closure that was already processed at the statement level. return null; diff --git a/tests/ClosureTest.php b/tests/ClosureTest.php index 27b6d1db366..ebdf2532344 100644 --- a/tests/ClosureTest.php +++ b/tests/ClosureTest.php @@ -1008,6 +1008,19 @@ function &(): int { fn &(int &$x): int => $x; ', ], + 'arrowFunctionArg' => [ + 'code' => '}> $existingIssue */ + array_reduce( + $existingIssue, + /** + * @param array{o:int, s:array} $existingIssue + */ + static fn(int $carry, array $existingIssue): int => $carry + $existingIssue["o"], + 0, + ); + ', + ], ]; } From 3837c949ad29e3c8cae9b8626689251c1d83f0e3 Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Fri, 12 Jan 2024 21:46:26 +1300 Subject: [PATCH 275/357] Remove impossible handleExpression condition --- src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php index 7c944a8051c..44744a82840 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php @@ -192,10 +192,6 @@ private static function handleExpression( return true; } - if ($stmt instanceof PhpParser\Node\InterpolatedStringPart) { - return true; - } - if ($stmt instanceof PhpParser\Node\Scalar\MagicConst) { MagicConstAnalyzer::analyze($statements_analyzer, $stmt, $context); From 4246586188dfb80b75f9437d34e3a4978a0d1421 Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Fri, 12 Jan 2024 22:00:31 +1300 Subject: [PATCH 276/357] Fix errors within php-parser --- psalm-baseline.xml | 5 ----- psalm.xml.dist | 13 +++++++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 3ef4c082be2..0bc6e674f42 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -2351,9 +2351,4 @@ $return_type - - - - - diff --git a/psalm.xml.dist b/psalm.xml.dist index aea7d6775e1..744af215afb 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -144,6 +144,19 @@ + + + + + + + + + + + + + From beced71a7041296528145047a3e2362391eeb779 Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Fri, 12 Jan 2024 22:12:04 +1300 Subject: [PATCH 277/357] Fix errors in SimpleNameResolver --- src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php b/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php index 193cce5a197..065fbf1210d 100644 --- a/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php +++ b/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php @@ -146,7 +146,10 @@ public function enterNode(Node $node): ?int return null; } - private function addAlias(Stmt\UseUse $use, int $type, ?Name $prefix = null): void + /** + * @param Stmt\Use_::TYPE_* $type + */ + private function addAlias(Node\UseItem $use, int $type, ?Name $prefix = null): void { // Add prefix for group uses /** @var Name $name */ From 1e3f4e24b97eba5679d46e1b1e2ddb9b02281856 Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Sat, 13 Jan 2024 07:02:59 +1300 Subject: [PATCH 278/357] Remove unneeded pure suppressions --- src/Psalm/CodeLocation/ParseErrorLocation.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Psalm/CodeLocation/ParseErrorLocation.php b/src/Psalm/CodeLocation/ParseErrorLocation.php index d20d96eb8e1..911371c284b 100644 --- a/src/Psalm/CodeLocation/ParseErrorLocation.php +++ b/src/Psalm/CodeLocation/ParseErrorLocation.php @@ -19,9 +19,9 @@ public function __construct( string $file_path, string $file_name, ) { - /** @psalm-suppress PossiblyUndefinedStringArrayOffset, ImpureMethodCall */ + /** @psalm-suppress PossiblyUndefinedStringArrayOffset */ $this->file_start = (int)$error->getAttributes()['startFilePos']; - /** @psalm-suppress PossiblyUndefinedStringArrayOffset, ImpureMethodCall */ + /** @psalm-suppress PossiblyUndefinedStringArrayOffset */ $this->file_end = (int)$error->getAttributes()['endFilePos']; $this->raw_file_start = $this->file_start; $this->raw_file_end = $this->file_end; From 6ca0043f080143d348f68a8659971f9b3467250b Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Fri, 19 Jan 2024 09:42:41 +1300 Subject: [PATCH 279/357] Handle reference assignments with @var --- .../Analyzer/Statements/Expression/AssignmentAnalyzer.php | 3 ++- src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index 6931b9d46ff..414a8773b5e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -899,6 +899,7 @@ public static function analyzeAssignmentRef( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\AssignRef $stmt, Context $context, + ?PhpParser\Node\Stmt $from_stmt, ): bool { ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context, false, null, null, null, true); @@ -914,7 +915,7 @@ public static function analyzeAssignmentRef( $statements_analyzer, ); - $doc_comment = $stmt->getDocComment(); + $doc_comment = $stmt->getDocComment() ?? $from_stmt?->getDocComment(); if ($doc_comment) { try { $var_comments = CommentAnalyzer::getTypeFromComment( diff --git a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php index 44744a82840..9261457eff1 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php @@ -340,7 +340,7 @@ private static function handleExpression( } if ($stmt instanceof PhpParser\Node\Expr\AssignRef) { - if (!AssignmentAnalyzer::analyzeAssignmentRef($statements_analyzer, $stmt, $context)) { + if (!AssignmentAnalyzer::analyzeAssignmentRef($statements_analyzer, $stmt, $context, $from_stmt)) { IssueBuffer::maybeAdd( new UnsupportedReferenceUsage( "This reference cannot be analyzed by Psalm", From 9577ff58fd71b42fc8b5f1a9939362c46824c5b2 Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Wed, 31 Jan 2024 13:47:02 +1300 Subject: [PATCH 280/357] Remove unnecessary baseline suppression --- psalm-baseline.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 0bc6e674f42..67e332207d3 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,10 +1,5 @@ - - - - - tags['variablesfrom'][0]]]> From 6aa2ddfe1c9d4e29bbc3bd2d413cc5b55d17de10 Mon Sep 17 00:00:00 2001 From: fluffycondor <7ionmail@gmail.com> Date: Wed, 31 Jan 2024 15:39:54 +0600 Subject: [PATCH 281/357] Fix ownerDocument type --- dictionaries/PropertyMap.php | 3 ++- stubs/extensions/dom.phpstub | 6 ++++-- tests/CoreStubsTest.php | 14 ++++++++++++++ tests/PropertyTypeTest.php | 2 +- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/dictionaries/PropertyMap.php b/dictionaries/PropertyMap.php index c5755235e91..521a1e7e34f 100644 --- a/dictionaries/PropertyMap.php +++ b/dictionaries/PropertyMap.php @@ -113,6 +113,7 @@ 'formatOutput' => 'bool', 'implementation' => 'DOMImplementation', 'lastElementChild' => 'DOMElement|null', + 'ownerDocument' => 'null', 'preserveWhiteSpace' => 'bool', 'recover' => 'bool', 'resolveExternals' => 'bool', @@ -173,7 +174,7 @@ 'nodeName' => 'string', 'nodeType' => 'int', 'nodeValue' => 'string|null', - 'ownerDocument' => 'DOMDocument|null', + 'ownerDocument' => 'DOMDocument', 'parentNode' => 'DOMNode|null', 'prefix' => 'string', 'previousSibling' => 'DOMNode|null', diff --git a/stubs/extensions/dom.phpstub b/stubs/extensions/dom.phpstub index 2520a479902..b240a8d143d 100644 --- a/stubs/extensions/dom.phpstub +++ b/stubs/extensions/dom.phpstub @@ -154,7 +154,7 @@ class DOMNode */ public ?DOMNamedNodeMap $attributes; /** @readonly */ - public ?DOMDocument $ownerDocument; + public DOMDocument $ownerDocument; /** @readonly */ public ?string $namespaceURI; public string $prefix; @@ -242,7 +242,7 @@ class DOMNameSpaceNode /** @readonly */ public ?string $namespaceURI; /** @readonly */ - public ?DOMDocument $ownerDocument; + public DOMDocument $ownerDocument; /** @readonly */ public ?DOMNode $parentNode; } @@ -281,6 +281,8 @@ class DOMDocument extends DOMNode implements DOMParentNode public DOMImplementation $implementation; /** @readonly */ public ?DOMElement $documentElement; + /** @readonly */ + public null $ownerDocument; /** * @deprecated diff --git a/tests/CoreStubsTest.php b/tests/CoreStubsTest.php index b0189897090..142be4a3e8a 100644 --- a/tests/CoreStubsTest.php +++ b/tests/CoreStubsTest.php @@ -424,6 +424,20 @@ function takesList(array $list): void {} $globBrace = glob('abc', GLOB_BRACE); PHP, ]; + yield "ownerDocument's type is non-nullable DOMDocument and always null on DOMDocument itself" => [ + 'code' => 'ownerDocument; + $b = (new DOMNode())->ownerDocument; + $c = (new DOMElement("p"))->ownerDocument; + $d = (new DOMNameSpaceNode())->ownerDocument; + ', + 'assertions' => [ + '$a===' => 'null', + '$b===' => 'DOMDocument', + '$c===' => 'DOMDocument', + '$d===' => 'DOMDocument', + ], + ]; } public function providerInvalidCodeParse(): iterable diff --git a/tests/PropertyTypeTest.php b/tests/PropertyTypeTest.php index bf8e00f4ea5..3d066210444 100644 --- a/tests/PropertyTypeTest.php +++ b/tests/PropertyTypeTest.php @@ -718,7 +718,7 @@ class Foo { $a = new DOMElement("foo"); $owner = $a->ownerDocument;', 'assertions' => [ - '$owner' => 'DOMDocument|null', + '$owner' => 'DOMDocument', ], ], 'propertyMapHydration' => [ From 551625aa4b57fa0bbcdb115d0e6a14f63924fff0 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 31 Jan 2024 14:03:01 +0100 Subject: [PATCH 282/357] Fix https://github.com/vimeo/psalm/issues/10561 numeric input incorrect return type --- .../ReturnTypeProvider/FilterUtils.php | 5 +++-- tests/FunctionCallTest.php | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php index e0de55bba04..dfce3b37b55 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php @@ -919,7 +919,7 @@ public static function getReturnType( $filter_types[] = new TFloat(); } - if ($atomic_type instanceof TMixed) { + if ($atomic_type instanceof TMixed || $atomic_type instanceof TNumeric) { $filter_types[] = new TFloat(); } @@ -994,6 +994,7 @@ public static function getReturnType( } else { $int_type = new TInt(); } + foreach ($input_type->getAtomicTypes() as $atomic_type) { if ($atomic_type instanceof TLiteralInt) { if ($min_range !== null && $min_range > $atomic_type->value) { @@ -1108,7 +1109,7 @@ public static function getReturnType( $filter_types[] = $int_type; } - if ($atomic_type instanceof TMixed) { + if ($atomic_type instanceof TMixed || $atomic_type instanceof TNumeric) { $filter_types[] = $int_type; } diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index f5965f98f40..27b10b28228 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -1118,6 +1118,24 @@ function filterFloat(string $s) : float { } function filterFloatWithDefault(string $s) : float { return filter_var($s, FILTER_VALIDATE_FLOAT, ["options" => ["default" => 5.0]]); + } + + /** + * @param mixed $c + * @return int<1, 100>|stdClass|array + */ + function filterNumericIntWithDefault($c) { + if (is_numeric($c)) { + return filter_var($c, FILTER_VALIDATE_INT, [ + "options" => [ + "default" => new stdClass(), + "min_range" => 1, + "max_range" => 100, + ], + ]); + } + + return array(); }', ], 'callVariableVar' => [ From 7023855fb3e8652417c1ec27ead2b33b41226ce4 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 31 Jan 2024 14:19:53 +0100 Subject: [PATCH 283/357] add scalar & numeric handling for all cases where appropriate and ensure no more generic types being added for int/float (previous commit) --- .../ReturnTypeProvider/FilterUtils.php | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php index dfce3b37b55..5c32126de0e 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php @@ -33,6 +33,7 @@ use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TNumeric; use Psalm\Type\Atomic\TNumericString; +use Psalm\Type\Atomic\TScalar; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTrue; use Psalm\Type\Union; @@ -42,6 +43,7 @@ use function array_keys; use function array_merge; use function filter_var; +use function get_class; use function implode; use function in_array; use function preg_match; @@ -919,7 +921,11 @@ public static function getReturnType( $filter_types[] = new TFloat(); } - if ($atomic_type instanceof TMixed || $atomic_type instanceof TNumeric) { + // only these specific classes, not any class that extends either + // to avoid matching already better handled cases from above, e.g. float is numeric and scalar + if ($atomic_type instanceof TMixed + || get_class($atomic_type) === TNumeric::class + || get_class($atomic_type) === TScalar::class) { $filter_types[] = new TFloat(); } @@ -967,7 +973,9 @@ public static function getReturnType( if ($atomic_type instanceof TMixed || $atomic_type instanceof TString || $atomic_type instanceof TInt - || $atomic_type instanceof TFloat) { + || $atomic_type instanceof TFloat + || $atomic_type instanceof TNumeric + || $atomic_type instanceof TScalar) { $filter_types[] = new TBool(); } @@ -1109,7 +1117,9 @@ public static function getReturnType( $filter_types[] = $int_type; } - if ($atomic_type instanceof TMixed || $atomic_type instanceof TNumeric) { + if ($atomic_type instanceof TMixed + || get_class($atomic_type) === TNumeric::class + || get_class($atomic_type) === TScalar::class) { $filter_types[] = $int_type; } @@ -1130,9 +1140,7 @@ public static function getReturnType( $filter_types[] = $atomic_type; } elseif ($atomic_type instanceof TString) { $filter_types[] = new TNonFalsyString(); - } - - if ($atomic_type instanceof TMixed) { + } elseif ($atomic_type instanceof TMixed || $atomic_type instanceof TScalar) { $filter_types[] = new TNonFalsyString(); } @@ -1160,6 +1168,7 @@ public static function getReturnType( || $atomic_type instanceof TInt || $atomic_type instanceof TFloat || $atomic_type instanceof TNumeric + || $atomic_type instanceof TScalar || $atomic_type instanceof TMixed) { $filter_types[] = new TString(); } @@ -1184,11 +1193,10 @@ public static function getReturnType( } else { $filter_types[] = $atomic_type; } - } - - if ($atomic_type instanceof TMixed + } elseif ($atomic_type instanceof TMixed || $atomic_type instanceof TInt - || $atomic_type instanceof TFloat) { + || $atomic_type instanceof TFloat + || $atomic_type instanceof TScalar) { $filter_types[] = $string_type; } @@ -1231,7 +1239,7 @@ public static function getReturnType( continue; } - if ($atomic_type instanceof TMixed) { + if ($atomic_type instanceof TMixed || $atomic_type instanceof TScalar) { $filter_types[] = new TString(); } @@ -1311,7 +1319,7 @@ public static function getReturnType( continue; } - if ($atomic_type instanceof TMixed) { + if ($atomic_type instanceof TMixed || $atomic_type instanceof TScalar) { $filter_types[] = new TString(); } @@ -1330,7 +1338,7 @@ public static function getReturnType( continue; } - if ($atomic_type instanceof TMixed) { + if ($atomic_type instanceof TMixed || $atomic_type instanceof TScalar) { $filter_types[] = new TString(); } @@ -1387,7 +1395,7 @@ public static function getReturnType( continue; } - if ($atomic_type instanceof TMixed) { + if ($atomic_type instanceof TMixed || $atomic_type instanceof TScalar) { $filter_types[] = new TNumericString(); $filter_types[] = Type::getAtomicStringFromLiteral(''); } From 9ec556bb14f4c43a28d1461e4b9e7d7800e494ea Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Wed, 31 Jan 2024 21:59:40 +0100 Subject: [PATCH 284/357] Allow inline comments in typedef shapes Fixes vimeo/psalm#10492 --- .../Reflector/ClassLikeNodeScanner.php | 7 +------ tests/TypeAnnotationTest.php | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 87da361b2e1..7589c018f98 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -81,7 +81,6 @@ use function preg_match; use function preg_replace; use function preg_split; -use function str_replace; use function strtolower; use function trim; use function usort; @@ -1913,10 +1912,6 @@ private static function getTypeAliasesFromCommentLines( continue; } - $var_line = preg_replace('/[ \t]+/', ' ', preg_replace('@^[ \t]*\*@m', '', $var_line)); - $var_line = preg_replace('/,\n\s+\}/', '}', $var_line); - $var_line = str_replace("\n", '', $var_line); - $var_line_parts = preg_split('/( |=)/', $var_line, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); if (!$var_line_parts) { @@ -1949,7 +1944,7 @@ private static function getTypeAliasesFromCommentLines( array_shift($var_line_parts); } - $type_string = str_replace("\n", '', implode('', $var_line_parts)); + $type_string = implode('', $var_line_parts); try { $type_string = CommentAnalyzer::splitDocLine($type_string)[0]; } catch (DocblockParseException $e) { diff --git a/tests/TypeAnnotationTest.php b/tests/TypeAnnotationTest.php index dbcbf0b987d..ea4d4be65b7 100644 --- a/tests/TypeAnnotationTest.php +++ b/tests/TypeAnnotationTest.php @@ -884,6 +884,23 @@ public function f(array $foo): void { 'ignored_issues' => [], 'php_version' => '8.1', ], + 'inlineComments' => [ + 'code' => <<<'PHP' + Date: Wed, 31 Jan 2024 23:07:23 +0100 Subject: [PATCH 285/357] Allow typedef imports from any classlike type All we really need is for the source to be autoloadable, and it includes all classlikes (interfaces, classes, enums and traits at the time of writing). --- src/Psalm/Internal/Analyzer/FileAnalyzer.php | 2 +- src/Psalm/Internal/Type/TypeExpander.php | 4 +++- tests/TypeAnnotationTest.php | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/FileAnalyzer.php b/src/Psalm/Internal/Analyzer/FileAnalyzer.php index d9879558922..80db22ed9d1 100644 --- a/src/Psalm/Internal/Analyzer/FileAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FileAnalyzer.php @@ -237,7 +237,7 @@ public function analyze( $this->suppressed_issues, new ClassLikeNameOptions( true, - false, + true, true, true, true, diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index 61af641066f..0855a1ab732 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -283,7 +283,9 @@ public static function expandAtomic( $declaring_fq_classlike_name = $self_class; } - if (!($evaluate_class_constants && $codebase->classOrInterfaceOrEnumExists($declaring_fq_classlike_name))) { + if (!($evaluate_class_constants + && $codebase->classlikes->doesClassLikeExist(strtolower($declaring_fq_classlike_name)) + )) { return [$return_type]; } diff --git a/tests/TypeAnnotationTest.php b/tests/TypeAnnotationTest.php index ea4d4be65b7..0e101137c84 100644 --- a/tests/TypeAnnotationTest.php +++ b/tests/TypeAnnotationTest.php @@ -884,6 +884,21 @@ public function f(array $foo): void { 'ignored_issues' => [], 'php_version' => '8.1', ], + 'importFromTrait' => [ + 'code' => <<<'PHP' + [ 'code' => <<<'PHP' Date: Thu, 1 Feb 2024 01:41:24 +0100 Subject: [PATCH 286/357] Fix baseline loading for path specified on the command line Fixes vimeo/psalm#10624 --- src/Psalm/Internal/Cli/Psalm.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Cli/Psalm.php b/src/Psalm/Internal/Cli/Psalm.php index dda0e27530b..c354cf21a58 100644 --- a/src/Psalm/Internal/Cli/Psalm.php +++ b/src/Psalm/Internal/Cli/Psalm.php @@ -1067,7 +1067,8 @@ private static function initBaseline( if ($paths_to_check !== null) { $filtered_issue_baseline = []; foreach ($paths_to_check as $path_to_check) { - $path_to_check = substr($path_to_check, strlen($config->base_dir)); + // +1 to remove the initial slash from $path_to_check + $path_to_check = substr($path_to_check, strlen($config->base_dir) + 1); if (isset($issue_baseline[$path_to_check])) { $filtered_issue_baseline[$path_to_check] = $issue_baseline[$path_to_check]; } From a66aace523c9e39a6129c404789ecabe247cdad1 Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Thu, 1 Feb 2024 17:31:15 +1300 Subject: [PATCH 287/357] Analyze dynamic static property names --- .../Fetch/StaticPropertyFetchAnalyzer.php | 24 +++++++++++++++---- tests/UnusedVariableTest.php | 16 ++++++++++++- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php index 94771ed2e17..790b36b30e7 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php @@ -134,12 +134,26 @@ public static function analyze( if ($stmt->name instanceof PhpParser\Node\VarLikeIdentifier) { $prop_name = $stmt->name->name; - } elseif (($stmt_name_type = $statements_analyzer->node_data->getType($stmt->name)) - && $stmt_name_type->isSingleStringLiteral() - ) { - $prop_name = $stmt_name_type->getSingleStringLiteral()->value; } else { - $prop_name = null; + $was_inside_general_use = $context->inside_general_use; + + $context->inside_general_use = true; + + if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->name, $context) === false) { + $context->inside_general_use = $was_inside_general_use; + + return false; + } + + $context->inside_general_use = $was_inside_general_use; + + if (($stmt_name_type = $statements_analyzer->node_data->getType($stmt->name)) + && $stmt_name_type->isSingleStringLiteral() + ) { + $prop_name = $stmt_name_type->getSingleStringLiteral()->value; + } else { + $prop_name = null; + } } if (!$prop_name) { diff --git a/tests/UnusedVariableTest.php b/tests/UnusedVariableTest.php index 8518dd5107f..d58c210c052 100644 --- a/tests/UnusedVariableTest.php +++ b/tests/UnusedVariableTest.php @@ -1012,7 +1012,7 @@ function foo() : void { A::$method(); }', ], - 'usedAsStaticPropertyName' => [ + 'usedAsStaticPropertyAssign' => [ 'code' => ' [ + 'code' => ' [ 'code' => ' Date: Thu, 1 Feb 2024 17:39:38 +1300 Subject: [PATCH 288/357] Analyze dynamic class const names --- .../Expression/ClassConstAnalyzer.php | 10 +++++++++- tests/UnusedVariableTest.php | 17 +++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php index d9ec74f47bf..58b3cfb419e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php @@ -206,7 +206,15 @@ public static function analyzeFetch( } if (!$stmt->name instanceof PhpParser\Node\Identifier) { - return true; + $was_inside_general_use = $context->inside_general_use; + + $context->inside_general_use = true; + + $ret = ExpressionAnalyzer::analyze($statements_analyzer, $stmt->name, $context); + + $context->inside_general_use = $was_inside_general_use; + + return $ret; } $const_id = $fq_class_name . '::' . $stmt->name; diff --git a/tests/UnusedVariableTest.php b/tests/UnusedVariableTest.php index d58c210c052..2b88fbef301 100644 --- a/tests/UnusedVariableTest.php +++ b/tests/UnusedVariableTest.php @@ -1012,6 +1012,23 @@ function foo() : void { A::$method(); }', ], + 'usedAsClassConstFetch' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.3', + ], 'usedAsStaticPropertyAssign' => [ 'code' => ' Date: Thu, 1 Feb 2024 17:54:46 +0100 Subject: [PATCH 289/357] Stable baseline Fixes vimeo/psalm#10632 --- src/Psalm/ErrorBaseline.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Psalm/ErrorBaseline.php b/src/Psalm/ErrorBaseline.php index 9a83b0a2899..783ef4a8c84 100644 --- a/src/Psalm/ErrorBaseline.php +++ b/src/Psalm/ErrorBaseline.php @@ -16,7 +16,6 @@ use function array_reduce; use function array_values; use function get_loaded_extensions; -use function htmlspecialchars; use function implode; use function ksort; use function min; @@ -268,11 +267,7 @@ private static function writeToFile( foreach ($existingIssueType['s'] as $selection) { $codeNode = $baselineDoc->createElement('code'); $textContent = trim($selection); - if ($textContent !== htmlspecialchars($textContent)) { - $codeNode->appendChild($baselineDoc->createCDATASection($textContent)); - } else { - $codeNode->textContent = trim($textContent); - } + $codeNode->appendChild($baselineDoc->createCDATASection($textContent)); $issueNode->appendChild($codeNode); } $fileNode->appendChild($issueNode); From 421cb0f7a1f61e8c25eba04544441bb14868a94b Mon Sep 17 00:00:00 2001 From: robchett Date: Thu, 2 Nov 2023 11:59:42 +0000 Subject: [PATCH 290/357] Allow enum cases to be global constants --- .../Expression/SimpleTypeInferer.php | 15 +++-- tests/EnumTest.php | 66 +++++++++++++++++++ 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php index b8f67edb619..3d60782d1b9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php @@ -283,23 +283,28 @@ public static function infer( } if ($stmt instanceof PhpParser\Node\Expr\ConstFetch) { - $name = strtolower($stmt->name->getFirst()); - if ($name === 'false') { + $name = $stmt->name->getFirst(); + $name_lowercase = strtolower($name); + if ($name_lowercase === 'false') { return Type::getFalse(); } - if ($name === 'true') { + if ($name_lowercase === 'true') { return Type::getTrue(); } - if ($name === 'null') { + if ($name_lowercase === 'null') { return Type::getNull(); } - if ($stmt->name->getFirst() === '__NAMESPACE__') { + if ($name === '__NAMESPACE__') { return Type::getString($aliases->namespace); } + if ($type = ConstFetchAnalyzer::getGlobalConstType($codebase, $name, $name)) { + return $type; + } + return null; } diff --git a/tests/EnumTest.php b/tests/EnumTest.php index 0cb1c2c0e4d..49d1693054d 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -657,6 +657,28 @@ function f(I $i): void { 'ignored_issues' => [], 'php_version' => '8.1', ], + 'stringBackedEnumCaseValueFromStringGlobalConstant' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'intBackedEnumCaseValueFromIntGlobalConstant' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } @@ -1107,6 +1129,50 @@ enum Bar: int 'ignored_issues' => [], 'php_version' => '8.1', ], + 'invalidStringBackedEnumCaseValueFromStringGlobalConstant' => [ + 'code' => ' 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'invalidIntBackedEnumCaseValueFromIntGlobalConstant' => [ + 'code' => ' 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'invalidStringBackedEnumCaseValueFromIntGlobalConstant' => [ + 'code' => ' 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'invalidIntBackedEnumCaseValueFromStringGlobalConstant' => [ + 'code' => ' 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } } From d4a5909e1f703b846677816ce3c80ee9f59c98fb Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 1 Feb 2024 08:10:57 +0100 Subject: [PATCH 291/357] Fix additional places where base_dir was broken due to missing separator Improves upon https://github.com/vimeo/psalm/pull/10542 and https://github.com/vimeo/psalm/pull/10628 --- src/Psalm/Config.php | 12 ++++++------ tests/Cache/CacheTest.php | 34 +++++++++++++++++----------------- tests/StubTest.php | 2 +- tests/TestConfig.php | 4 +--- 4 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index e6f05540b55..bebf471599c 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -244,7 +244,7 @@ class Config protected $extra_files; /** - * The base directory of this config file + * The base directory of this config file without trailing slash * * @var string */ @@ -1445,7 +1445,7 @@ private static function fromXmlAndPaths( if (!$file_path) { throw new ConfigException( 'Cannot resolve stubfile path ' - . rtrim($config->base_dir, DIRECTORY_SEPARATOR) + . $config->base_dir . DIRECTORY_SEPARATOR . $stub_file['name'], ); @@ -1582,11 +1582,11 @@ public function safeSetCustomErrorLevel(string $issue_key, string $error_level): private function loadFileExtensions(SimpleXMLElement $extensions): void { foreach ($extensions as $extension) { - $extension_name = preg_replace('/^\.?/', '', (string)$extension['name'], 1); + $extension_name = preg_replace('/^\.?/', '', (string) $extension['name'], 1); $this->file_extensions[] = $extension_name; if (isset($extension['scanner'])) { - $path = $this->base_dir . (string)$extension['scanner']; + $path = $this->base_dir . DIRECTORY_SEPARATOR . (string) $extension['scanner']; if (!file_exists($path)) { throw new ConfigException('Error parsing config: cannot find file ' . $path); @@ -1596,7 +1596,7 @@ private function loadFileExtensions(SimpleXMLElement $extensions): void } if (isset($extension['checker'])) { - $path = $this->base_dir . (string)$extension['checker']; + $path = $this->base_dir . DIRECTORY_SEPARATOR . (string) $extension['checker']; if (!file_exists($path)) { throw new ConfigException('Error parsing config: cannot find file ' . $path); @@ -1817,7 +1817,7 @@ private function getPluginClassForPath(Codebase $codebase, string $path, string public function shortenFileName(string $to): string { if (!is_file($to)) { - return preg_replace('/^' . preg_quote($this->base_dir, '/') . '/', '', $to, 1); + return preg_replace('/^' . preg_quote($this->base_dir . DIRECTORY_SEPARATOR, '/') . '?/', '', $to, 1); } $from = $this->base_dir; diff --git a/tests/Cache/CacheTest.php b/tests/Cache/CacheTest.php index e714256837a..c696aef96dd 100644 --- a/tests/Cache/CacheTest.php +++ b/tests/Cache/CacheTest.php @@ -92,7 +92,7 @@ public function testCacheInteractions( foreach ($interactions as $interaction) { foreach ($interaction['files'] as $file_path => $file_contents) { - $file_path = $config->base_dir . str_replace('/', DIRECTORY_SEPARATOR, $file_path); + $file_path = $config->base_dir . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $file_path); if ($file_contents === null) { $file_provider->deleteFile($file_path); } else { @@ -126,7 +126,7 @@ public static function provideCacheInteractions(): iterable [ [ 'files' => [ - '/src/A.php' => <<<'PHP' + 'src/A.php' => <<<'PHP' <<<'PHP' + 'src/B.php' => <<<'PHP' [ - '/src/B.php' => null, + 'src/B.php' => null, ], 'issues' => [ - '/src/A.php' => [ + 'src/A.php' => [ 'UndefinedClass: Class, interface or enum named B does not exist', ], ], @@ -163,7 +163,7 @@ public function do(): void [ [ 'files' => [ - '/src/A.php' => <<<'PHP' + 'src/A.php' => <<<'PHP' <<<'PHP' + 'src/B.php' => <<<'PHP' [ - '/src/A.php' => [ + 'src/A.php' => [ "NullableReturnStatement: The declared return type 'int' for A::foo is not nullable, but the function returns 'int|null'", "InvalidNullableReturnType: The declared return type 'int' for A::foo is not nullable, but 'int|null' contains null", ], @@ -188,7 +188,7 @@ class B { ], [ 'files' => [ - '/src/B.php' => <<<'PHP' + 'src/B.php' => <<<'PHP' [ - '/src/A.php' => <<<'PHP' + 'src/A.php' => <<<'PHP' <<<'PHP' + 'src/B.php' => <<<'PHP' [ - '/src/A.php' => <<<'PHP' + 'src/A.php' => <<<'PHP' [ - '/src/A.php' => [ + 'src/A.php' => [ "UndefinedDocblockClass: Docblock-defined class, interface or enum named T does not exist", ], - '/src/B.php' => [ + 'src/B.php' => [ "InvalidArgument: Argument 1 of A::foo expects T, but 1 provided", ], ], @@ -266,7 +266,7 @@ public function foo($baz): void [ [ 'files' => [ - '/src/A.php' => <<<'PHP' + 'src/A.php' => <<<'PHP' [ - '/src/A.php' => <<<'PHP' + 'src/A.php' => <<<'PHP' [ - '/src/A.php' => [ + 'src/A.php' => [ "UndefinedThisPropertyFetch: Instance property A::\$foo is not defined", "MixedReturnStatement: Could not infer a return type", "MixedInferredReturnType: Could not verify return type 'string' for A::bar", diff --git a/tests/StubTest.php b/tests/StubTest.php index f12fb943ed8..1e297daf976 100644 --- a/tests/StubTest.php +++ b/tests/StubTest.php @@ -864,7 +864,7 @@ function_exists("fooBar"); public function testNoStubFunction(): void { - $this->expectExceptionMessage('UndefinedFunction - /src/somefile.php:2:22 - Function barBar does not exist'); + $this->expectExceptionMessage('UndefinedFunction'); $this->expectException(CodeException::class); $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( diff --git a/tests/TestConfig.php b/tests/TestConfig.php index dc72087410f..1b8a1ea5c94 100644 --- a/tests/TestConfig.php +++ b/tests/TestConfig.php @@ -9,8 +9,6 @@ use function getcwd; -use const DIRECTORY_SEPARATOR; - class TestConfig extends Config { private static ?ProjectFileFilter $cached_project_files = null; @@ -28,7 +26,7 @@ public function __construct() $this->level = 1; $this->cache_directory = null; - $this->base_dir = getcwd() . DIRECTORY_SEPARATOR; + $this->base_dir = getcwd(); if (!self::$cached_project_files) { self::$cached_project_files = ProjectFileFilter::loadFromXMLElement( From f185f3d98593233d9b68b05959bc10adb6c5876a Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 1 Feb 2024 08:46:18 +0100 Subject: [PATCH 292/357] additional places with inconsistent trailing slash --- src/Psalm/Internal/Cli/LanguageServer.php | 6 +++--- src/Psalm/Internal/Cli/Plugin.php | 6 ++---- src/Psalm/Internal/Cli/Psalm.php | 12 ++++++------ src/Psalm/Internal/Cli/Psalter.php | 6 +++--- src/Psalm/Internal/Cli/Refactor.php | 6 +++--- .../PluginManager/Command/DisableCommand.php | 4 +--- .../Internal/PluginManager/Command/EnableCommand.php | 4 +--- .../Internal/PluginManager/Command/ShowCommand.php | 4 +--- .../Internal/PluginManager/PluginListFactory.php | 10 ++++------ 9 files changed, 24 insertions(+), 34 deletions(-) diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index 1dc16fbe5bf..0fa174eff1f 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -258,12 +258,12 @@ static function (string $arg) use ($valid_long_options): void { $options['r'] = $options['root']; } - $current_dir = (string)getcwd() . DIRECTORY_SEPARATOR; + $current_dir = (string) getcwd(); if (isset($options['r']) && is_string($options['r'])) { $root_path = realpath($options['r']); - if (!$root_path) { + if ($root_path === false) { fwrite( STDERR, 'Could not locate root directory ' . $current_dir . DIRECTORY_SEPARATOR . $options['r'] . PHP_EOL, @@ -271,7 +271,7 @@ static function (string $arg) use ($valid_long_options): void { exit(1); } - $current_dir = $root_path . DIRECTORY_SEPARATOR; + $current_dir = $root_path; } $vendor_dir = CliUtils::getVendorDir($current_dir); diff --git a/src/Psalm/Internal/Cli/Plugin.php b/src/Psalm/Internal/Cli/Plugin.php index 2388238262d..c89cbeed54c 100644 --- a/src/Psalm/Internal/Cli/Plugin.php +++ b/src/Psalm/Internal/Cli/Plugin.php @@ -12,8 +12,6 @@ use function dirname; use function getcwd; -use const DIRECTORY_SEPARATOR; - // phpcs:disable PSR1.Files.SideEffects require_once __DIR__ . '/../CliUtils.php'; @@ -27,13 +25,13 @@ final class Plugin public static function run(): void { CliUtils::checkRuntimeRequirements(); - $current_dir = (string)getcwd() . DIRECTORY_SEPARATOR; + $current_dir = (string) getcwd(); $vendor_dir = CliUtils::getVendorDir($current_dir); CliUtils::requireAutoloaders($current_dir, false, $vendor_dir); $app = new Application('psalm-plugin', PSALM_VERSION); - $psalm_root = dirname(__DIR__, 4) . DIRECTORY_SEPARATOR; + $psalm_root = dirname(__DIR__, 4); $plugin_list_factory = new PluginListFactory($current_dir, $psalm_root); diff --git a/src/Psalm/Internal/Cli/Psalm.php b/src/Psalm/Internal/Cli/Psalm.php index c354cf21a58..c8a5a4ee20c 100644 --- a/src/Psalm/Internal/Cli/Psalm.php +++ b/src/Psalm/Internal/Cli/Psalm.php @@ -485,7 +485,7 @@ static function (string $arg): void { */ private static function generateConfig(string $current_dir, array &$args): void { - if (file_exists($current_dir . 'psalm.xml')) { + if (file_exists($current_dir . DIRECTORY_SEPARATOR . 'psalm.xml')) { die('A config file already exists in the current directory' . PHP_EOL); } @@ -535,7 +535,7 @@ private static function generateConfig(string $current_dir, array &$args): void die($e->getMessage() . PHP_EOL); } - if (!file_put_contents($current_dir . 'psalm.xml', $template_contents)) { + if (!file_put_contents($current_dir . DIRECTORY_SEPARATOR . 'psalm.xml', $template_contents)) { die('Could not write to psalm.xml' . PHP_EOL); } @@ -779,7 +779,7 @@ private static function autoGenerateConfig( die($e->getMessage() . PHP_EOL); } - if (!file_put_contents($current_dir . 'psalm.xml', $template_contents)) { + if (!file_put_contents($current_dir . DIRECTORY_SEPARATOR . 'psalm.xml', $template_contents)) { die('Could not write to psalm.xml' . PHP_EOL); } @@ -840,12 +840,12 @@ private static function getCurrentDir(array $options): string exit(1); } - $current_dir = $cwd . DIRECTORY_SEPARATOR; + $current_dir = $cwd; if (isset($options['r']) && is_string($options['r'])) { $root_path = realpath($options['r']); - if (!$root_path) { + if ($root_path === false) { fwrite( STDERR, 'Could not locate root directory ' . $current_dir . DIRECTORY_SEPARATOR . $options['r'] . PHP_EOL, @@ -853,7 +853,7 @@ private static function getCurrentDir(array $options): string exit(1); } - $current_dir = $root_path . DIRECTORY_SEPARATOR; + $current_dir = $root_path; } return $current_dir; diff --git a/src/Psalm/Internal/Cli/Psalter.php b/src/Psalm/Internal/Cli/Psalter.php index 9dd8eaf47d0..5b53ec0dc13 100644 --- a/src/Psalm/Internal/Cli/Psalter.php +++ b/src/Psalm/Internal/Cli/Psalter.php @@ -194,16 +194,16 @@ public static function run(array $argv): void exit(1); } - $current_dir = (string)getcwd() . DIRECTORY_SEPARATOR; + $current_dir = (string) getcwd(); if (isset($options['r']) && is_string($options['r'])) { $root_path = realpath($options['r']); - if (!$root_path) { + if ($root_path === false) { die('Could not locate root directory ' . $current_dir . DIRECTORY_SEPARATOR . $options['r'] . PHP_EOL); } - $current_dir = $root_path . DIRECTORY_SEPARATOR; + $current_dir = $root_path; } $vendor_dir = CliUtils::getVendorDir($current_dir); diff --git a/src/Psalm/Internal/Cli/Refactor.php b/src/Psalm/Internal/Cli/Refactor.php index 0fca3ab46f2..22761b873f1 100644 --- a/src/Psalm/Internal/Cli/Refactor.php +++ b/src/Psalm/Internal/Cli/Refactor.php @@ -165,16 +165,16 @@ static function (string $arg) use ($valid_long_options): void { $options['r'] = $options['root']; } - $current_dir = (string)getcwd() . DIRECTORY_SEPARATOR; + $current_dir = (string) getcwd(); if (isset($options['r']) && is_string($options['r'])) { $root_path = realpath($options['r']); - if (!$root_path) { + if ($root_path === false) { die('Could not locate root directory ' . $current_dir . DIRECTORY_SEPARATOR . $options['r'] . PHP_EOL); } - $current_dir = $root_path . DIRECTORY_SEPARATOR; + $current_dir = $root_path; } $vendor_dir = CliUtils::getVendorDir($current_dir); diff --git a/src/Psalm/Internal/PluginManager/Command/DisableCommand.php b/src/Psalm/Internal/PluginManager/Command/DisableCommand.php index af7b4bb90d9..7c1e6b9a27a 100644 --- a/src/Psalm/Internal/PluginManager/Command/DisableCommand.php +++ b/src/Psalm/Internal/PluginManager/Command/DisableCommand.php @@ -16,8 +16,6 @@ use function getcwd; use function is_string; -use const DIRECTORY_SEPARATOR; - /** * @internal */ @@ -50,7 +48,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $current_dir = (string) getcwd() . DIRECTORY_SEPARATOR; + $current_dir = (string) getcwd(); $config_file_path = $input->getOption('config'); if ($config_file_path !== null && !is_string($config_file_path)) { diff --git a/src/Psalm/Internal/PluginManager/Command/EnableCommand.php b/src/Psalm/Internal/PluginManager/Command/EnableCommand.php index 6278b7018f2..0a8df8d1dfe 100644 --- a/src/Psalm/Internal/PluginManager/Command/EnableCommand.php +++ b/src/Psalm/Internal/PluginManager/Command/EnableCommand.php @@ -16,8 +16,6 @@ use function getcwd; use function is_string; -use const DIRECTORY_SEPARATOR; - /** * @internal */ @@ -50,7 +48,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $current_dir = (string) getcwd() . DIRECTORY_SEPARATOR; + $current_dir = (string) getcwd(); $config_file_path = $input->getOption('config'); if ($config_file_path !== null && !is_string($config_file_path)) { diff --git a/src/Psalm/Internal/PluginManager/Command/ShowCommand.php b/src/Psalm/Internal/PluginManager/Command/ShowCommand.php index a8e78a732c4..ecc24712ce4 100644 --- a/src/Psalm/Internal/PluginManager/Command/ShowCommand.php +++ b/src/Psalm/Internal/PluginManager/Command/ShowCommand.php @@ -17,8 +17,6 @@ use function getcwd; use function is_string; -use const DIRECTORY_SEPARATOR; - /** * @internal */ @@ -44,7 +42,7 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $current_dir = (string) getcwd() . DIRECTORY_SEPARATOR; + $current_dir = (string) getcwd(); $config_file_path = $input->getOption('config'); if ($config_file_path !== null && !is_string($config_file_path)) { diff --git a/src/Psalm/Internal/PluginManager/PluginListFactory.php b/src/Psalm/Internal/PluginManager/PluginListFactory.php index 950b6dd24a6..927d0f8c32e 100644 --- a/src/Psalm/Internal/PluginManager/PluginListFactory.php +++ b/src/Psalm/Internal/PluginManager/PluginListFactory.php @@ -7,10 +7,8 @@ use function array_filter; use function json_encode; -use function rtrim; use function urlencode; -use const DIRECTORY_SEPARATOR; use const JSON_THROW_ON_ERROR; /** @@ -53,13 +51,13 @@ private function findLockFiles(): array if ($this->psalm_root === $this->project_root) { // managing plugins for psalm itself $composer_lock_filenames = [ - Composer::getLockFilePath(rtrim($this->psalm_root, DIRECTORY_SEPARATOR)), + Composer::getLockFilePath($this->psalm_root), ]; } else { $composer_lock_filenames = [ - Composer::getLockFilePath(rtrim($this->project_root, DIRECTORY_SEPARATOR)), - Composer::getLockFilePath(rtrim($this->psalm_root, DIRECTORY_SEPARATOR) . '/../../..'), - Composer::getLockFilePath(rtrim($this->psalm_root, DIRECTORY_SEPARATOR)), + Composer::getLockFilePath($this->project_root), + Composer::getLockFilePath($this->psalm_root . '/../../..'), + Composer::getLockFilePath($this->psalm_root), ]; } From 47c52ad60225877fa92f394a7f4d9213974ad12f Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 1 Feb 2024 12:28:32 +0100 Subject: [PATCH 293/357] fix /src/psalm.xml not removed between tests and remove psalm.xml at the end of tests --- tests/EndToEnd/PsalmEndToEndTest.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/EndToEnd/PsalmEndToEndTest.php b/tests/EndToEnd/PsalmEndToEndTest.php index b02660cecd2..bdbb2cb1a0e 100644 --- a/tests/EndToEnd/PsalmEndToEndTest.php +++ b/tests/EndToEnd/PsalmEndToEndTest.php @@ -24,6 +24,8 @@ use function tempnam; use function unlink; +use const DIRECTORY_SEPARATOR; + /** * Tests some of the most important use cases of the psalm and psalter commands, by launching a new * process as if invoked by a real user. @@ -47,8 +49,6 @@ public static function setUpBeforeClass(): void throw new Exception('Couldn\'t get working directory'); } - mkdir(self::$tmpDir . '/src'); - copy(__DIR__ . '/../fixtures/DummyProjectWithErrors/composer.json', self::$tmpDir . '/composer.json'); $process = new Process(['composer', 'install', '--no-plugins'], self::$tmpDir, null, null, 120); @@ -63,7 +63,8 @@ public static function tearDownAfterClass(): void public function setUp(): void { - @unlink(self::$tmpDir . '/psalm.xml'); + mkdir(self::$tmpDir . '/src'); + copy( __DIR__ . '/../fixtures/DummyProjectWithErrors/src/FileWithErrors.php', self::$tmpDir . '/src/FileWithErrors.php', @@ -73,9 +74,16 @@ public function setUp(): void public function tearDown(): void { + @unlink(self::$tmpDir . '/psalm.xml'); + if (file_exists(self::$tmpDir . '/cache')) { self::recursiveRemoveDirectory(self::$tmpDir . '/cache'); } + + if (file_exists(self::$tmpDir . '/src')) { + self::recursiveRemoveDirectory(self::$tmpDir . '/src'); + } + parent::tearDown(); } @@ -275,7 +283,7 @@ private static function recursiveRemoveDirectory(string $src): void $dir = opendir($src); while (false !== ($file = readdir($dir))) { if (($file !== '.') && ($file !== '..')) { - $full = $src . '/' . $file; + $full = $src . DIRECTORY_SEPARATOR . $file; if (is_dir($full)) { self::recursiveRemoveDirectory($full); } else { From fae1d414fdedd55defbd507a9cb2d5841ceab887 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 1 Feb 2024 18:10:40 +0100 Subject: [PATCH 294/357] fix errors output in stdout making test fails with cryptic errors --- src/Psalm/Internal/Cli/Psalm.php | 31 +++++++++++++++-------- src/Psalm/Internal/Cli/Psalter.php | 39 +++++++++++++++++++++-------- src/Psalm/Internal/Cli/Refactor.php | 24 ++++++++++++------ src/Psalm/Internal/CliUtils.php | 3 ++- 4 files changed, 68 insertions(+), 29 deletions(-) diff --git a/src/Psalm/Internal/Cli/Psalm.php b/src/Psalm/Internal/Cli/Psalm.php index c8a5a4ee20c..5aacced0f61 100644 --- a/src/Psalm/Internal/Cli/Psalm.php +++ b/src/Psalm/Internal/Cli/Psalm.php @@ -276,7 +276,8 @@ public static function run(array $argv): void if (isset($options['set-baseline'])) { if (is_array($options['set-baseline'])) { - die('Only one baseline file can be created at a time' . PHP_EOL); + fwrite(STDERR, 'Only one baseline file can be created at a time' . PHP_EOL); + exit(1); } } @@ -486,7 +487,8 @@ static function (string $arg): void { private static function generateConfig(string $current_dir, array &$args): void { if (file_exists($current_dir . DIRECTORY_SEPARATOR . 'psalm.xml')) { - die('A config file already exists in the current directory' . PHP_EOL); + fwrite(STDERR, 'A config file already exists in the current directory' . PHP_EOL); + exit(1); } $args = array_values(array_filter( @@ -507,12 +509,14 @@ private static function generateConfig(string $current_dir, array &$args): void $init_source_dir = null; if (count($args)) { if (count($args) > 2) { - die('Too many arguments provided for psalm --init' . PHP_EOL); + fwrite(STDERR, 'Too many arguments provided for psalm --init' . PHP_EOL); + exit(1); } if (isset($args[1])) { if (!preg_match('/^[1-8]$/', $args[1])) { - die('Config strictness must be a number between 1 and 8 inclusive' . PHP_EOL); + fwrite(STDERR, 'Config strictness must be a number between 1 and 8 inclusive' . PHP_EOL); + exit(1); } $init_level = (int)$args[1]; @@ -532,11 +536,13 @@ private static function generateConfig(string $current_dir, array &$args): void $vendor_dir, ); } catch (ConfigCreationException $e) { - die($e->getMessage() . PHP_EOL); + fwrite(STDERR, $e->getMessage() . PHP_EOL); + exit(1); } - if (!file_put_contents($current_dir . DIRECTORY_SEPARATOR . 'psalm.xml', $template_contents)) { - die('Could not write to psalm.xml' . PHP_EOL); + if (file_put_contents($current_dir . DIRECTORY_SEPARATOR . 'psalm.xml', $template_contents) === false) { + fwrite(STDERR, 'Could not write to psalm.xml' . PHP_EOL); + exit(1); } exit('Config file created successfully. Please re-run psalm.' . PHP_EOL); @@ -681,7 +687,8 @@ private static function updateBaseline(array $options, Config $config): array $baselineFile = $config->error_baseline; if (empty($baselineFile)) { - die('Cannot update baseline, because no baseline file is configured.' . PHP_EOL); + fwrite(STDERR, 'Cannot update baseline, because no baseline file is configured.' . PHP_EOL); + exit(1); } try { @@ -776,11 +783,13 @@ private static function autoGenerateConfig( $vendor_dir, ); } catch (ConfigCreationException $e) { - die($e->getMessage() . PHP_EOL); + fwrite(STDERR, $e->getMessage() . PHP_EOL); + exit(1); } - if (!file_put_contents($current_dir . DIRECTORY_SEPARATOR . 'psalm.xml', $template_contents)) { - die('Could not write to psalm.xml' . PHP_EOL); + if (file_put_contents($current_dir . DIRECTORY_SEPARATOR . 'psalm.xml', $template_contents) === false) { + fwrite(STDERR, 'Could not write to psalm.xml' . PHP_EOL); + exit(1); } exit('Config file created successfully. Please re-run psalm.' . PHP_EOL); diff --git a/src/Psalm/Internal/Cli/Psalter.php b/src/Psalm/Internal/Cli/Psalter.php index 5b53ec0dc13..d42a1f10843 100644 --- a/src/Psalm/Internal/Cli/Psalter.php +++ b/src/Psalm/Internal/Cli/Psalter.php @@ -112,7 +112,8 @@ public static function run(array $argv): void self::syncShortOptions($options); if (isset($options['c']) && is_array($options['c'])) { - die('Too many config files provided' . PHP_EOL); + fwrite(STDERR, 'Too many config files provided' . PHP_EOL); + exit(1); } if (array_key_exists('h', $options)) { @@ -200,7 +201,11 @@ public static function run(array $argv): void $root_path = realpath($options['r']); if ($root_path === false) { - die('Could not locate root directory ' . $current_dir . DIRECTORY_SEPARATOR . $options['r'] . PHP_EOL); + fwrite( + STDERR, + 'Could not locate root directory ' . $current_dir . DIRECTORY_SEPARATOR . $options['r'] . PHP_EOL, + ); + exit(1); } $current_dir = $root_path; @@ -304,7 +309,8 @@ public static function run(array $argv): void if (array_key_exists('issues', $options)) { if (!is_string($options['issues']) || !$options['issues']) { - die('Expecting a comma-separated list of issues' . PHP_EOL); + fwrite(STDERR, 'Expecting a comma-separated list of issues' . PHP_EOL); + exit(1); } $issues = explode(',', $options['issues']); @@ -339,7 +345,11 @@ public static function run(array $argv): void ); if ($allow_backwards_incompatible_changes === null) { - die('--allow-backwards-incompatible-changes expects a boolean value [true|false|1|0]' . PHP_EOL); + fwrite( + STDERR, + '--allow-backwards-incompatible-changes expects a boolean value [true|false|1|0]' . PHP_EOL, + ); + exit(1); } $project_analyzer->getCodebase()->allow_backwards_incompatible_changes @@ -354,7 +364,11 @@ public static function run(array $argv): void ); if ($doc_block_add_new_line_before_return === null) { - die('--add-newline-between-docblock-annotations expects a boolean value [true|false|1|0]' . PHP_EOL); + fwrite( + STDERR, + '--add-newline-between-docblock-annotations expects a boolean value [true|false|1|0]' . PHP_EOL, + ); + exit(1); } ParsedDocblock::addNewLineBetweenAnnotations($doc_block_add_new_line_before_return); @@ -505,7 +519,8 @@ private static function loadCodeowners(Providers $providers): array } elseif (file_exists('docs/CODEOWNERS')) { $codeowners_file_path = realpath('docs/CODEOWNERS'); } else { - die('Cannot use --codeowner without a CODEOWNERS file' . PHP_EOL); + fwrite(STDERR, 'Cannot use --codeowner without a CODEOWNERS file' . PHP_EOL); + exit(1); } $codeowners_file = file_get_contents($codeowners_file_path); @@ -555,7 +570,8 @@ static function (string $line): bool { } if (!$codeowner_files) { - die('Could not find any available entries in CODEOWNERS' . PHP_EOL); + fwrite(STDERR, 'Could not find any available entries in CODEOWNERS' . PHP_EOL); + exit(1); } return $codeowner_files; @@ -571,11 +587,13 @@ private static function loadCodeownersFiles(array $desired_codeowners, array $co /** @psalm-suppress MixedAssignment */ foreach ($desired_codeowners as $desired_codeowner) { if (!is_string($desired_codeowner)) { - die('Invalid --codeowner ' . (string)$desired_codeowner . PHP_EOL); + fwrite(STDERR, 'Invalid --codeowner ' . (string) $desired_codeowner . PHP_EOL); + exit(1); } if ($desired_codeowner[0] !== '@') { - die('--codeowner option must start with @' . PHP_EOL); + fwrite(STDERR, '--codeowner option must start with @' . PHP_EOL); + exit(1); } $matched_file = false; @@ -588,7 +606,8 @@ private static function loadCodeownersFiles(array $desired_codeowners, array $co } if (!$matched_file) { - die('User/group ' . $desired_codeowner . ' does not own any PHP files' . PHP_EOL); + fwrite(STDERR, 'User/group ' . $desired_codeowner . ' does not own any PHP files' . PHP_EOL); + exit(1); } } diff --git a/src/Psalm/Internal/Cli/Refactor.php b/src/Psalm/Internal/Cli/Refactor.php index 22761b873f1..b23bcb863c2 100644 --- a/src/Psalm/Internal/Cli/Refactor.php +++ b/src/Psalm/Internal/Cli/Refactor.php @@ -121,7 +121,8 @@ static function (string $arg) use ($valid_long_options): void { } if (isset($options['c']) && is_array($options['c'])) { - die('Too many config files provided' . PHP_EOL); + fwrite(STDERR, 'Too many config files provided' . PHP_EOL); + exit(1); } if (array_key_exists('h', $options)) { @@ -171,7 +172,11 @@ static function (string $arg) use ($valid_long_options): void { $root_path = realpath($options['r']); if ($root_path === false) { - die('Could not locate root directory ' . $current_dir . DIRECTORY_SEPARATOR . $options['r'] . PHP_EOL); + fwrite( + STDERR, + 'Could not locate root directory ' . $current_dir . DIRECTORY_SEPARATOR . $options['r'] . PHP_EOL, + ); + exit(1); } $current_dir = $root_path; @@ -210,7 +215,8 @@ static function (string $arg) use ($valid_long_options): void { if ($arg === '--into') { if ($operation !== 'move' || !$last_arg) { - die('--into is not expected here' . PHP_EOL); + fwrite(STDERR, '--into is not expected here' . PHP_EOL); + exit(1); } $operation = 'move_into'; @@ -224,7 +230,8 @@ static function (string $arg) use ($valid_long_options): void { if ($arg === '--to') { if ($operation !== 'rename' || !$last_arg) { - die('--to is not expected here' . PHP_EOL); + fwrite(STDERR, '--to is not expected here' . PHP_EOL); + exit(1); } $operation = 'rename_to'; @@ -239,7 +246,8 @@ static function (string $arg) use ($valid_long_options): void { if ($operation === 'move_into' || $operation === 'rename_to') { if (!$last_arg) { - die('Expecting a previous argument' . PHP_EOL); + fwrite(STDERR, 'Expecting a previous argument' . PHP_EOL); + exit(1); } if ($operation === 'move_into') { @@ -273,11 +281,13 @@ static function (string $arg) use ($valid_long_options): void { continue; } - die('Unexpected argument "' . $arg . '"' . PHP_EOL); + fwrite(STDERR, 'Unexpected argument "' . $arg . '"' . PHP_EOL); + exit(1); } if (!$to_refactor) { - die('No --move or --rename arguments supplied' . PHP_EOL); + fwrite(STDERR, 'No --move or --rename arguments supplied' . PHP_EOL); + exit(1); } $config = CliUtils::initializeConfig( diff --git a/src/Psalm/Internal/CliUtils.php b/src/Psalm/Internal/CliUtils.php index e90f73e4e5d..8f0f1fbf9cb 100644 --- a/src/Psalm/Internal/CliUtils.php +++ b/src/Psalm/Internal/CliUtils.php @@ -483,7 +483,8 @@ public static function initPhpVersion(array $options, Config $config, ProjectAna if (isset($options['php-version'])) { if (!is_string($options['php-version'])) { - die('Expecting a version number in the format x.y' . PHP_EOL); + fwrite(STDERR, 'Expecting a version number in the format x.y' . PHP_EOL); + exit(1); } $version = $options['php-version']; $source = 'cli'; From d79f77215b51df8238a06c26d08759df2a673edb Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 1 Feb 2024 08:47:24 +0100 Subject: [PATCH 295/357] improve getcwd return type --- dictionaries/CallMap.php | 2 +- dictionaries/CallMap_historical.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index cec0545f126..130468e475f 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -3296,7 +3296,7 @@ 'get_resource_type' => ['string', 'resource'=>'resource'], 'get_resources' => ['array', 'type='=>'?string'], 'getallheaders' => ['array|false'], -'getcwd' => ['string|false'], +'getcwd' => ['non-falsy-string|false'], 'getdate' => ['array{seconds: int<0, 59>, minutes: int<0, 59>, hours: int<0, 23>, mday: int<1, 31>, wday: int<0, 6>, mon: int<1, 12>, year: int, yday: int<0, 365>, weekday: "Monday"|"Tuesday"|"Wednesday"|"Thursday"|"Friday"|"Saturday"|"Sunday", month: "January"|"February"|"March"|"April"|"May"|"June"|"July"|"August"|"September"|"October"|"November"|"December", 0: int}', 'timestamp='=>'?int'], 'getenv' => ['string|false', 'name'=>'string', 'local_only='=>'bool'], 'getenv\'1' => ['array'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 61e3db09d55..889e1b4fb12 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -10661,7 +10661,7 @@ 'get_resource_type' => ['string', 'resource'=>'resource'], 'get_resources' => ['array', 'type='=>'string'], 'getallheaders' => ['array|false'], - 'getcwd' => ['string|false'], + 'getcwd' => ['non-falsy-string|false'], 'getdate' => ['array{seconds: int<0, 59>, minutes: int<0, 59>, hours: int<0, 23>, mday: int<1, 31>, wday: int<0, 6>, mon: int<1, 12>, year: int, yday: int<0, 365>, weekday: "Monday"|"Tuesday"|"Wednesday"|"Thursday"|"Friday"|"Saturday"|"Sunday", month: "January"|"February"|"March"|"April"|"May"|"June"|"July"|"August"|"September"|"October"|"November"|"December", 0: int}', 'timestamp='=>'int'], 'getenv' => ['string|false', 'name'=>'string', 'local_only='=>'bool'], 'gethostbyaddr' => ['string|false', 'ip'=>'string'], From d2e5a0722b75eba39de9e5b78032eb6e33aa6797 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 1 Feb 2024 09:08:22 +0100 Subject: [PATCH 296/357] improve realpath and readlink return type --- dictionaries/CallMap.php | 22 +++++++++++----------- dictionaries/CallMap_historical.php | 22 +++++++++++----------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 130468e475f..048c7287da7 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1590,7 +1590,7 @@ 'DirectoryIterator::getPathInfo' => ['?SplFileInfo', 'class='=>'?class-string'], 'DirectoryIterator::getPathname' => ['string'], 'DirectoryIterator::getPerms' => ['int'], -'DirectoryIterator::getRealPath' => ['string'], +'DirectoryIterator::getRealPath' => ['non-falsy-string'], 'DirectoryIterator::getSize' => ['int'], 'DirectoryIterator::getType' => ['string'], 'DirectoryIterator::isDir' => ['bool'], @@ -2827,7 +2827,7 @@ 'FilesystemIterator::getPathInfo' => ['?SplFileInfo', 'class='=>'?class-string'], 'FilesystemIterator::getPathname' => ['string'], 'FilesystemIterator::getPerms' => ['int'], -'FilesystemIterator::getRealPath' => ['string'], +'FilesystemIterator::getRealPath' => ['non-falsy-string'], 'FilesystemIterator::getSize' => ['int'], 'FilesystemIterator::getType' => ['string'], 'FilesystemIterator::isDir' => ['bool'], @@ -3343,7 +3343,7 @@ 'GlobIterator::getPathInfo' => ['?SplFileInfo', 'class='=>'?class-string'], 'GlobIterator::getPathname' => ['string'], 'GlobIterator::getPerms' => ['int'], -'GlobIterator::getRealPath' => ['string|false'], +'GlobIterator::getRealPath' => ['non-falsy-string|false'], 'GlobIterator::getSize' => ['int'], 'GlobIterator::getType' => ['string|false'], 'GlobIterator::isDir' => ['bool'], @@ -9722,8 +9722,8 @@ 'readline_read_history' => ['bool', 'filename='=>'?string'], 'readline_redisplay' => ['void'], 'readline_write_history' => ['bool', 'filename='=>'?string'], -'readlink' => ['string|false', 'path'=>'string'], -'realpath' => ['string|false', 'path'=>'string'], +'readlink' => ['non-falsy-string|false', 'path'=>'string'], +'realpath' => ['non-falsy-string|false', 'path'=>'string'], 'realpath_cache_get' => ['array'], 'realpath_cache_size' => ['int'], 'recode' => ['string', 'request'=>'string', 'string'=>'string'], @@ -9811,7 +9811,7 @@ 'RecursiveDirectoryIterator::getPathInfo' => ['?SplFileInfo', 'class='=>'?class-string'], 'RecursiveDirectoryIterator::getPathname' => ['string'], 'RecursiveDirectoryIterator::getPerms' => ['int'], -'RecursiveDirectoryIterator::getRealPath' => ['string'], +'RecursiveDirectoryIterator::getRealPath' => ['non-falsy-string'], 'RecursiveDirectoryIterator::getSize' => ['int'], 'RecursiveDirectoryIterator::getSubPath' => ['string'], 'RecursiveDirectoryIterator::getSubPathname' => ['string'], @@ -12238,7 +12238,7 @@ 'SplFileInfo::getPathInfo' => ['SplFileInfo|null', 'class='=>'?class-string'], 'SplFileInfo::getPathname' => ['string'], 'SplFileInfo::getPerms' => ['int|false'], -'SplFileInfo::getRealPath' => ['string|false'], +'SplFileInfo::getRealPath' => ['non-falsy-string|false'], 'SplFileInfo::getSize' => ['int|false'], 'SplFileInfo::getType' => ['string|false'], 'SplFileInfo::isDir' => ['bool'], @@ -12288,7 +12288,7 @@ 'SplFileObject::getPathInfo' => ['SplFileInfo|null', 'class='=>'?class-string'], 'SplFileObject::getPathname' => ['string'], 'SplFileObject::getPerms' => ['int|false'], -'SplFileObject::getRealPath' => ['false|string'], +'SplFileObject::getRealPath' => ['false|non-falsy-string'], 'SplFileObject::getSize' => ['int|false'], 'SplFileObject::getType' => ['string|false'], 'SplFileObject::hasChildren' => ['false'], @@ -12475,7 +12475,7 @@ 'SplTempFileObject::getPathInfo' => ['SplFileInfo|null', 'class='=>'?class-string'], 'SplTempFileObject::getPathname' => ['string'], 'SplTempFileObject::getPerms' => ['int|false'], -'SplTempFileObject::getRealPath' => ['false|string'], +'SplTempFileObject::getRealPath' => ['false|non-falsy-string'], 'SplTempFileObject::getSize' => ['int|false'], 'SplTempFileObject::getType' => ['string|false'], 'SplTempFileObject::hasChildren' => ['false'], @@ -12690,8 +12690,8 @@ 'ssh2_sftp_chmod' => ['bool', 'sftp'=>'resource', 'filename'=>'string', 'mode'=>'int'], 'ssh2_sftp_lstat' => ['array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', 'sftp'=>'resource', 'path'=>'string'], 'ssh2_sftp_mkdir' => ['bool', 'sftp'=>'resource', 'dirname'=>'string', 'mode='=>'int', 'recursive='=>'bool'], -'ssh2_sftp_readlink' => ['string|false', 'sftp'=>'resource', 'link'=>'string'], -'ssh2_sftp_realpath' => ['string|false', 'sftp'=>'resource', 'filename'=>'string'], +'ssh2_sftp_readlink' => ['non-falsy-string|false', 'sftp'=>'resource', 'link'=>'string'], +'ssh2_sftp_realpath' => ['non-falsy-string|false', 'sftp'=>'resource', 'filename'=>'string'], 'ssh2_sftp_rename' => ['bool', 'sftp'=>'resource', 'from'=>'string', 'to'=>'string'], 'ssh2_sftp_rmdir' => ['bool', 'sftp'=>'resource', 'dirname'=>'string'], 'ssh2_sftp_stat' => ['array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', 'sftp'=>'resource', 'path'=>'string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 889e1b4fb12..b9dc4b7b545 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -877,7 +877,7 @@ 'DirectoryIterator::getPathInfo' => ['?SplFileInfo', 'class='=>'class-string'], 'DirectoryIterator::getPathname' => ['string'], 'DirectoryIterator::getPerms' => ['int'], - 'DirectoryIterator::getRealPath' => ['string'], + 'DirectoryIterator::getRealPath' => ['non-falsy-string'], 'DirectoryIterator::getSize' => ['int'], 'DirectoryIterator::getType' => ['string'], 'DirectoryIterator::isDir' => ['bool'], @@ -1507,7 +1507,7 @@ 'FilesystemIterator::getPathInfo' => ['?SplFileInfo', 'class='=>'class-string'], 'FilesystemIterator::getPathname' => ['string'], 'FilesystemIterator::getPerms' => ['int'], - 'FilesystemIterator::getRealPath' => ['string'], + 'FilesystemIterator::getRealPath' => ['non-falsy-string'], 'FilesystemIterator::getSize' => ['int'], 'FilesystemIterator::getType' => ['string'], 'FilesystemIterator::isDir' => ['bool'], @@ -1762,7 +1762,7 @@ 'GlobIterator::getPathInfo' => ['?SplFileInfo', 'class='=>'class-string'], 'GlobIterator::getPathname' => ['string'], 'GlobIterator::getPerms' => ['int'], - 'GlobIterator::getRealPath' => ['string|false'], + 'GlobIterator::getRealPath' => ['non-falsy-string|false'], 'GlobIterator::getSize' => ['int'], 'GlobIterator::getType' => ['string|false'], 'GlobIterator::isDir' => ['bool'], @@ -5155,7 +5155,7 @@ 'RecursiveDirectoryIterator::getPathInfo' => ['?SplFileInfo', 'class='=>'class-string'], 'RecursiveDirectoryIterator::getPathname' => ['string'], 'RecursiveDirectoryIterator::getPerms' => ['int'], - 'RecursiveDirectoryIterator::getRealPath' => ['string'], + 'RecursiveDirectoryIterator::getRealPath' => ['non-falsy-string'], 'RecursiveDirectoryIterator::getSize' => ['int'], 'RecursiveDirectoryIterator::getSubPath' => ['string'], 'RecursiveDirectoryIterator::getSubPathname' => ['string'], @@ -7481,7 +7481,7 @@ 'SplFileInfo::getPathInfo' => ['SplFileInfo|null', 'class='=>'class-string'], 'SplFileInfo::getPathname' => ['string'], 'SplFileInfo::getPerms' => ['int|false'], - 'SplFileInfo::getRealPath' => ['string|false'], + 'SplFileInfo::getRealPath' => ['non-falsy-string|false'], 'SplFileInfo::getSize' => ['int|false'], 'SplFileInfo::getType' => ['string|false'], 'SplFileInfo::isDir' => ['bool'], @@ -7532,7 +7532,7 @@ 'SplFileObject::getPathInfo' => ['SplFileInfo|null', 'class='=>'class-string'], 'SplFileObject::getPathname' => ['string'], 'SplFileObject::getPerms' => ['int|false'], - 'SplFileObject::getRealPath' => ['false|string'], + 'SplFileObject::getRealPath' => ['false|non-falsy-string'], 'SplFileObject::getSize' => ['int|false'], 'SplFileObject::getType' => ['string|false'], 'SplFileObject::hasChildren' => ['false'], @@ -7723,7 +7723,7 @@ 'SplTempFileObject::getPathInfo' => ['SplFileInfo|null', 'class='=>'class-string'], 'SplTempFileObject::getPathname' => ['string'], 'SplTempFileObject::getPerms' => ['int|false'], - 'SplTempFileObject::getRealPath' => ['false|string'], + 'SplTempFileObject::getRealPath' => ['false|non-falsy-string'], 'SplTempFileObject::getSize' => ['int|false'], 'SplTempFileObject::getType' => ['string|false'], 'SplTempFileObject::hasChildren' => ['false'], @@ -13718,8 +13718,8 @@ 'readline_read_history' => ['bool', 'filename='=>'string'], 'readline_redisplay' => ['void'], 'readline_write_history' => ['bool', 'filename='=>'string'], - 'readlink' => ['string|false', 'path'=>'string'], - 'realpath' => ['string|false', 'path'=>'string'], + 'readlink' => ['non-falsy-string|false', 'path'=>'string'], + 'realpath' => ['non-falsy-string|false', 'path'=>'string'], 'realpath_cache_get' => ['array'], 'realpath_cache_size' => ['int'], 'recode' => ['string', 'request'=>'string', 'string'=>'string'], @@ -14120,8 +14120,8 @@ 'ssh2_sftp_chmod' => ['bool', 'sftp'=>'resource', 'filename'=>'string', 'mode'=>'int'], 'ssh2_sftp_lstat' => ['array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', 'sftp'=>'resource', 'path'=>'string'], 'ssh2_sftp_mkdir' => ['bool', 'sftp'=>'resource', 'dirname'=>'string', 'mode='=>'int', 'recursive='=>'bool'], - 'ssh2_sftp_readlink' => ['string|false', 'sftp'=>'resource', 'link'=>'string'], - 'ssh2_sftp_realpath' => ['string|false', 'sftp'=>'resource', 'filename'=>'string'], + 'ssh2_sftp_readlink' => ['non-falsy-string|false', 'sftp'=>'resource', 'link'=>'string'], + 'ssh2_sftp_realpath' => ['non-falsy-string|false', 'sftp'=>'resource', 'filename'=>'string'], 'ssh2_sftp_rename' => ['bool', 'sftp'=>'resource', 'from'=>'string', 'to'=>'string'], 'ssh2_sftp_rmdir' => ['bool', 'sftp'=>'resource', 'dirname'=>'string'], 'ssh2_sftp_stat' => ['array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', 'sftp'=>'resource', 'path'=>'string'], From 41ce826f6dd8595bfa7e08470ee515bdb20e3cd1 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 1 Feb 2024 17:58:04 +0100 Subject: [PATCH 297/357] improve file_put_contents return type --- dictionaries/CallMap.php | 2 +- dictionaries/CallMap_historical.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 048c7287da7..e79081aca8f 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -2792,7 +2792,7 @@ 'file' => ['list|false', 'filename'=>'string', 'flags='=>'int', 'context='=>'resource'], 'file_exists' => ['bool', 'filename'=>'string'], 'file_get_contents' => ['string|false', 'filename'=>'string', 'use_include_path='=>'bool', 'context='=>'?resource', 'offset='=>'int', 'length='=>'?int'], -'file_put_contents' => ['int|false', 'filename'=>'string', 'data'=>'string|resource|array', 'flags='=>'int', 'context='=>'resource'], +'file_put_contents' => ['int<0, max>|false', 'filename'=>'string', 'data'=>'string|resource|array', 'flags='=>'int', 'context='=>'resource'], 'fileatime' => ['int|false', 'filename'=>'string'], 'filectime' => ['int|false', 'filename'=>'string'], 'filegroup' => ['int|false', 'filename'=>'string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index b9dc4b7b545..80f6771ae84 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -10417,7 +10417,7 @@ 'file' => ['list|false', 'filename'=>'string', 'flags='=>'int', 'context='=>'resource'], 'file_exists' => ['bool', 'filename'=>'string'], 'file_get_contents' => ['string|false', 'filename'=>'string', 'use_include_path='=>'bool', 'context='=>'?resource', 'offset='=>'int', 'length='=>'int'], - 'file_put_contents' => ['int|false', 'filename'=>'string', 'data'=>'string|resource|array', 'flags='=>'int', 'context='=>'resource'], + 'file_put_contents' => ['int<0, max>|false', 'filename'=>'string', 'data'=>'string|resource|array', 'flags='=>'int', 'context='=>'resource'], 'fileatime' => ['int|false', 'filename'=>'string'], 'filectime' => ['int|false', 'filename'=>'string'], 'filegroup' => ['int|false', 'filename'=>'string'], From 1698239677b031ff30d9e6a84b80a39385e971df Mon Sep 17 00:00:00 2001 From: Ulrich Eckhardt Date: Fri, 2 Feb 2024 09:35:05 +0100 Subject: [PATCH 298/357] CallMap: Adjust return type for `inotify_add_watch()` to `int|false` See https://github.com/vimeo/psalm/issues/10636 --- dictionaries/CallMap.php | 2 +- dictionaries/CallMap_historical.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index cec0545f126..ab2c944fa0b 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -5700,7 +5700,7 @@ 'ini_restore' => ['void', 'option'=>'string'], 'ini_parse_quantity' => ['int', 'shorthand'=>'non-empty-string'], 'ini_set' => ['string|false', 'option'=>'string', 'value'=>'string|int|float|bool|null'], -'inotify_add_watch' => ['int', 'inotify_instance'=>'resource', 'pathname'=>'string', 'mask'=>'int'], +'inotify_add_watch' => ['int|false', 'inotify_instance'=>'resource', 'pathname'=>'string', 'mask'=>'int'], 'inotify_init' => ['resource|false'], 'inotify_queue_len' => ['int', 'inotify_instance'=>'resource'], 'inotify_read' => ['array|false', 'inotify_instance'=>'resource'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 61e3db09d55..713abcad748 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -11832,7 +11832,7 @@ 'ini_get_all' => ['array|false', 'extension='=>'?string', 'details='=>'bool'], 'ini_restore' => ['void', 'option'=>'string'], 'ini_set' => ['string|false', 'option'=>'string', 'value'=>'string'], - 'inotify_add_watch' => ['int', 'inotify_instance'=>'resource', 'pathname'=>'string', 'mask'=>'int'], + 'inotify_add_watch' => ['int|false', 'inotify_instance'=>'resource', 'pathname'=>'string', 'mask'=>'int'], 'inotify_init' => ['resource|false'], 'inotify_queue_len' => ['int', 'inotify_instance'=>'resource'], 'inotify_read' => ['array|false', 'inotify_instance'=>'resource'], From 9fd17cf749d148ecda5b668c76acfd307c9e1dfe Mon Sep 17 00:00:00 2001 From: Ulrich Eckhardt Date: Fri, 2 Feb 2024 10:06:03 +0100 Subject: [PATCH 299/357] CallMap: Improve returntype annotation for `inotify_read()` In case of success, it returns an array of associative arrays with defined fields: - `wd` is a watch descriptor - `mask` is a bit mask of events - `cookie` is a unique id to connect related events - `name` is the name of a file --- dictionaries/CallMap.php | 2 +- dictionaries/CallMap_historical.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index ab2c944fa0b..d1dd28456ad 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -5703,7 +5703,7 @@ 'inotify_add_watch' => ['int|false', 'inotify_instance'=>'resource', 'pathname'=>'string', 'mask'=>'int'], 'inotify_init' => ['resource|false'], 'inotify_queue_len' => ['int', 'inotify_instance'=>'resource'], -'inotify_read' => ['array|false', 'inotify_instance'=>'resource'], +'inotify_read' => ['array{wd: int, mask: int, cookie: int, name: string}[]|false', 'inotify_instance'=>'resource'], 'inotify_rm_watch' => ['bool', 'inotify_instance'=>'resource', 'watch_descriptor'=>'int'], 'intdiv' => ['int', 'num1'=>'int', 'num2'=>'int'], 'interface_exists' => ['bool', 'interface'=>'string', 'autoload='=>'bool'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 713abcad748..d3d5f3fb110 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -11835,7 +11835,7 @@ 'inotify_add_watch' => ['int|false', 'inotify_instance'=>'resource', 'pathname'=>'string', 'mask'=>'int'], 'inotify_init' => ['resource|false'], 'inotify_queue_len' => ['int', 'inotify_instance'=>'resource'], - 'inotify_read' => ['array|false', 'inotify_instance'=>'resource'], + 'inotify_read' => ['array{wd: int, mask: int, cookie: int, name: string}[]|false', 'inotify_instance'=>'resource'], 'inotify_rm_watch' => ['bool', 'inotify_instance'=>'resource', 'watch_descriptor'=>'int'], 'intdiv' => ['int', 'num1'=>'int', 'num2'=>'int'], 'interface_exists' => ['bool', 'interface'=>'string', 'autoload='=>'bool'], From 4fc35949eff045c584375f76ce22138f3fa7d906 Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Fri, 2 Feb 2024 10:22:38 +0100 Subject: [PATCH 300/357] Allow sebastian/diff v6 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3a0a3465c27..411c4565105 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", "nikic/php-parser": "^4.16", - "sebastian/diff": "^4.0 || ^5.0", + "sebastian/diff": "^4.0 || ^5.0 || ^6.0", "spatie/array-to-xml": "^2.17.0 || ^3.0", "symfony/console": "^4.1.6 || ^5.0 || ^6.0 || ^7.0", "symfony/filesystem": "^5.4 || ^6.0 || ^7.0" From 9a970cafc3cf89cfc1cdffe5fc711f48289f07b2 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sat, 3 Feb 2024 18:15:40 +0100 Subject: [PATCH 301/357] Update our actual baseline to use the new format --- psalm-baseline.xml | 1544 ++++++++++++++++++++++---------------------- 1 file changed, 772 insertions(+), 772 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 88b88bd8c55..58202505f72 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + @@ -8,13 +8,13 @@ tags['variablesfrom'][0]]]> - $matches[1] + tags['variablesfrom'][0]]]> - $matches[1] + @@ -24,7 +24,7 @@ - !$appearing_method_id + @@ -38,19 +38,19 @@ - $const_name - $const_name - $symbol_name - $symbol_parts[1] + + + + - !$function_name + namespace]]> namespace]]> namespace]]> namespace_first_stmt_start]]> uses_end]]> - $file_path + insertText]]> symbol, '()')]]> symbol, '()')]]> @@ -71,16 +71,16 @@ - !$composer_json - !$config_path - !$file_path + + + - $cwd - $dir + + function_id]]> - $issue_handler_children - $parent_issue_type + + composer_class_loader->findFile($pluginClassName)]]> autoloader]]> localName, $offset)]]> @@ -89,7 +89,7 @@ - $suggested_dir + file_path, 'stub')]]> file_path, 'vendor')]]> @@ -99,10 +99,10 @@ - !$directory_path - !$file_path - !$glob_directory_path - !$glob_file_path + + + + directory]]> file]]> referencedClass]]> @@ -111,8 +111,8 @@ referencedMethod]]> referencedProperty]]> referencedVariable]]> - glob($parts[0], GLOB_NOSORT) - glob($parts[0], GLOB_ONLYDIR | GLOB_NOSORT) + + @@ -123,15 +123,15 @@ - $matches[1] - $matches[2] - $matches[3] + + + - $creating_conditional_id - $creating_conditional_id + + @@ -141,23 +141,23 @@ - $comments[0] - $property_name + + props[0]]]> - $uninitialized_variables[0] + - !$declaring_property_class - !$fq_class_name + + self]]> self]]> self]]> self]]> template_extended_params]]> template_types]]> - $class_template_params + initialized_class]]> - $parent_fq_class_name + getStmts()]]> getStmts()]]> template_extended_params]]> @@ -173,15 +173,15 @@ - $property_name + - !$appearing_property_class + self]]> - !$declaring_property_class + self]]> template_types]]> - $resolved_name + template_covariants]]> template_extended_params]]> template_types]]> @@ -197,13 +197,13 @@ - !$original_type + description]]> var_id]]> - !$var_type_tokens - $brackets - $template_type_map - $type_aliases + + + + line_number]]> type_end]]> type_start]]> @@ -211,27 +211,27 @@ - $namespace_name - $namespace_name + + root_file_name]]> root_file_path]]> - $namespace - $namespace + + getNamespace()]]> getStmts()]]> - $class_template_params + self]]> self]]> - $fq_class_name - $self_fq_class_name + + @@ -242,24 +242,24 @@ template_types]]> template_types]]> - $cased_method_id - $cased_method_id - $cased_method_id - $cased_method_id - $cased_method_id + + + + + self]]> self]]> self]]> self]]> self]]> - $context_self - $hash - $namespace - $parent_fqcln - $parent_fqcln + + + + + cased_name]]> template_types]]> - $template_types + function->getStmts()]]> source->getTemplateTypeMap()]]> storage->template_types]]> @@ -268,12 +268,12 @@ - !$calling_method_id + self]]> - $appearing_method_class - $appearing_method_class + + self]]> - $context_self + @@ -290,16 +290,16 @@ - $destination_parts[1] - $destination_parts[1] - $destination_parts[1] - $php_minor_version - $source_parts[1] + + + + + self]]> - $potential_file_path + @@ -318,21 +318,21 @@ - if (AtomicTypeComparator::isContainedBy( - if (AtomicTypeComparator::isContainedBy( + + var_id]]> var_id]]> - $calling_type_params + branch_point]]> template_types]]> getTemplateTypeMap()]]> line_number]]> type_end]]> type_start]]> - $var_id - $var_id + + @@ -368,13 +368,13 @@ assigned_var_ids += $switch_scope->new_assigned_var_ids]]> - !$switch_var_id + new_assigned_var_ids]]> new_vars_in_scope]]> possibly_redefined_vars]]> possibly_redefined_vars]]> redefined_vars]]> - $switch_var_id + @@ -385,11 +385,11 @@ branch_point]]> - $nested_or_options - $switch_var_id - $switch_var_id - $switch_var_id - $type_statements + + + + + @@ -404,7 +404,7 @@ - $var_id + @@ -439,135 +439,135 @@ getArgs()[0]]]> - !$var_name - !$var_type + + ')]]> - $array_root - $count_equality_position - $count_equality_position - $count_equality_position - $count_inequality_position - $count_inequality_position - $count_inequality_position - $false_position - $false_position - $first_var_name - $first_var_name - $first_var_name - $first_var_name - $first_var_name - $first_var_name - $first_var_name - $first_var_name - $first_var_name - $first_var_name - $first_var_name - $first_var_name - $first_var_name - $first_var_name_in_array_argument - $get_debug_type_position - $get_debug_type_position - $getclass_position - $getclass_position - $gettype_position - $gettype_position - $if_false_assertions - $if_true_assertions - $inferior_value_position - $other_var_name - $superior_value_position - $this_class_name - $this_class_name - $this_class_name - $true_position - $true_position - $typed_value_position - $typed_value_position - $var_id - $var_id - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name_left - $var_name_right - $var_type - $var_type - $var_type - self::hasReconcilableNonEmptyCountEqualityCheck($conditional) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - !$parent_var_id - $object_id - $parent_var_id - $parent_var_id - $root_var_id - $root_var_id - $root_var_id - $root_var_id - $root_var_id - $var_id - $var_var_id + + + + + + + + + + + self]]> - !$var_id - $appearing_property_class - $class_template_params - $class_template_params + + + + calling_method_id]]> calling_method_id]]> self]]> self]]> self]]> - $declaring_property_class + getter_method]]> - $var_id - $var_id - $var_id - $var_id - $var_id - $var_id - $var_id - $var_id - $var_id - $var_property_id - $var_property_id + + + + + + + + + + + calling_method_id, '::__clone')]]> calling_method_id, '::__construct')]]> calling_method_id, '::__unserialize')]]> @@ -576,12 +576,12 @@ - $new_property_name + calling_method_id]]> - $var_id - $var_id + + @@ -590,30 +590,30 @@ ')]]> ')]]> - $assign_value_id + calling_method_id]]> - $extended_var_id - $extended_var_id - $extended_var_id - $extended_var_id - $extended_var_id - $list_var_id - $list_var_id - $list_var_id - $prop_name - $root_var_id + + + + + + + + + + line_number]]> type_end]]> type_start]]> - $var_id - $var_id - $var_id - $var_id - $var_id - $var_id - $var_id - $var_id - $var_id + + + + + + + + + vars_in_scope[$lhs_var_id] = &$context->vars_in_scope[$rhs_var_id]]]> @@ -626,39 +626,39 @@ - $invalid_left_messages[0] - $invalid_right_messages[0] + + branch_point]]> - $var_id + - verifyType + - $method_name - $parts[1] + + - !$container_class - $cased_method_id - $cased_method_id - $cased_method_id - $cased_method_id - $cased_method_id - $class_generic_params + + + + + + + calling_function_id]]> calling_function_id]]> calling_method_id]]> - $self_fq_class_name - $static_fq_class_name - $var_id + + + value, '::')]]> value, '::')]]> @@ -666,12 +666,12 @@ self]]> - $cased_method_id - $cased_method_id - $cased_method_id - $cased_method_id - $cased_method_id - $cased_method_id + + + + + + calling_method_id]]> calling_method_id]]> calling_method_id]]> @@ -680,48 +680,48 @@ calling_method_id]]> calling_method_id]]> sinks]]> - $function_params - $function_params - $function_params + + + template_types]]> - $method_id - $method_id - $method_id - $method_id - $var_id - $var_id - $var_id + + + + + + + getFQCLN())]]> - $args[0] - $args[0] - $args[1] - $method_name + + + + - !$container_class + calling_method_id]]> - $var_id + - !$template_types - !$template_types + + template_types]]> - $method_name - $overridden_template_types + + template_extended_params]]> template_types]]> - $function_name - $function_name + + getArgs()[0]->value]]> @@ -730,7 +730,7 @@ getArgs()[0]]]> - $parts[1] + function_id]]> @@ -745,7 +745,7 @@ - $method + self]]> @@ -764,23 +764,23 @@ calling_method_id]]> calling_method_id]]> self]]> - $lhs_var_id - $mixin_class_template_params + + - $class_template_params + calling_method_id]]> calling_method_id]]> - $lhs_var_id + template_types]]> template_types]]> - $caller_identifier + @@ -791,26 +791,26 @@ specialization_key]]> - $var_id + self]]> self]]> - $appearing_method_name + - $found_generic_params - $found_generic_params - $found_generic_params - $found_generic_params - $found_generic_params - $found_generic_params - $intersection_method_id - $intersection_method_id + + + + + + + + @@ -824,16 +824,16 @@ getFQCLN()]]> - $lhs_var_id - $lhs_var_id - $lhs_var_id + + + getFQCLN()]]> - $path_to_file - $var_id + + ')]]> @@ -842,8 +842,8 @@ calling_method_id]]> self]]> - $fq_class_name - $fq_class_name + + getFullyQualifiedFunctionMethodOrNamespaceName()]]> template_extended_params]]> template_types]]> @@ -854,7 +854,7 @@ parent_class]]> - $child_fq_class_name + calling_method_id]]> self]]> self]]> @@ -863,7 +863,7 @@ self]]> - !$fq_class_name + mixin_declaring_fqcln]]> parent_class]]> parent_class]]> @@ -874,15 +874,15 @@ - $new_method_name + self]]> self]]> self]]> self]]> - $found_generic_params - $found_generic_params + + template_extended_params]]> @@ -892,9 +892,9 @@ items[1]]]> - !$arg_var_id - $arg_var_id - $assertion_var_id + + + template_extended_params]]> self]]> self]]> @@ -905,8 +905,8 @@ - $new_const_name - $new_const_name + + self]]> @@ -920,101 +920,101 @@ - !$lhs_var_name - !$object_id - !$object_id - !$this_class_name - $object_id - $property_root - $resolved_name - $resolved_name - $root_var_id - $this_class_name + + + + + + + + + + - $stmt_type - $stmt_type - $stmt_type + + + - $dim_var_id - $dim_var_id - $extended_var_id - $extended_var_id - $keyed_array_var_id - $keyed_array_var_id - $keyed_array_var_id - $keyed_array_var_id + + + + + + + + - $stmt_type + self]]> self]]> - $declaring_property_class - $declaring_property_class + + template_types]]> template_types]]> - $var_id - $var_id - $var_property_id - $var_property_id + + + + - $invalid_fetch_types[0] + - !$prop_name + calling_method_id]]> calling_method_id]]> - $declaring_property_class - $stmt_var_id - $var_id - $var_id + + + + - $new_property_name + - !$prop_name + calling_method_id]]> calling_method_id]]> calling_method_id]]> self]]> - $string_type - $var_id - $var_id + + + - $branch_point - $branch_point + + - $var_id + - !$evaled_path - !$var_id - $include_path - $left_string - $path_to_file - $right_string - $var_id + + + + + + + @@ -1024,13 +1024,13 @@ - !$switch_var_id - $switch_var_id + + - $fq_classlike_name + @@ -1046,7 +1046,7 @@ var_id]]> - $class_template_params + declaring_yield_fqcn]]> self]]> line_number]]> @@ -1056,7 +1056,7 @@ - $method_name + calling_function_id]]> @@ -1064,7 +1064,7 @@ var_id]]> calling_function_id]]> self]]> - $found_generic_params + line_number]]> type_end]]> type_start]]> @@ -1072,20 +1072,20 @@ - $root_var_id - $var_id + + - $token_list[$iter] + - $token_list[$iter] - $token_list[$iter] - $token_list[$iter] - $token_list[$iter] - $token_list[0] + + + + + @@ -1093,17 +1093,17 @@ expr->getArgs()[0]]]> - $branch_point - $new_issues + + getNamespace()]]> - $possible_traced_variable_names + fake_this_class]]> vars_to_initialize]]> - !$root_path + @@ -1113,45 +1113,45 @@ error_baseline]]> - !$paths_to_check - !$root_path + + - $baseline_file_path - $cache_directory + + threads]]> - $find_references_to - empty($baselineFile) + + - !$root_path - $paths_to_check + + - $identifier_name + - !$last_arg - !$last_arg - !$last_arg - !$root_path + + + + - !$config_file - !$end_psalm_open_tag - !$path_to_check + + + error_baseline]]> - $f_paths - $path_to_config - $stdin = fgets(STDIN) + + + getPHPVersionFromComposerJson()]]> getPhpVersionFromConfig()]]> @@ -1159,7 +1159,7 @@ - $trait + @@ -1167,39 +1167,39 @@ - $destination_name - $destination_name - $destination_name - $source_const_name - $stub + + + + + - !$calling_fq_class_name - !$insert_pos - !$insert_pos - !$insert_pos - $calling_fq_class_name - $calling_fq_class_name - $calling_fq_class_name - $calling_fq_class_name - $calling_fq_class_name - $calling_fq_class_name - $calling_fq_class_name - $calling_fq_class_name - $calling_fq_class_name - $calling_fq_class_name - $calling_method_id - $calling_method_id - $calling_method_id - $calling_method_id - $calling_method_id - $file_path - $file_path - $file_path - $file_path - $file_path - $migrated_source_fqcln - $migrated_source_fqcln + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1209,50 +1209,50 @@ - $stub + - !$checked_file_path - !$root_file_path - $args + + + cased_name]]> - $namespace + - !$return_type_string + - !$calling_class_name - !$extends - $calling_method_id - $calling_method_id - $calling_method_id - $calling_method_id - $calling_method_id - $calling_method_id - $calling_method_id - $calling_method_id - $calling_method_id - $found_generic_params - $old_method_id - $source_file_path - $source_file_path - $source_file_path - $source_file_path - $source_file_path - $source_file_path - $source_file_path - $source_file_path + + + + + + + + + + + + + + + + + + + + + - $mapped_name + template_extended_params]]> template_extended_params]]> template_extended_params]]> @@ -1262,12 +1262,12 @@ - $property_name - $property_name - $property_name - $property_name - $property_name - $property_name + + + + + + calling_method_id]]> @@ -1277,7 +1277,7 @@ - $composer_file_path + cased_name]]> cased_name]]> @@ -1299,17 +1299,17 @@ - $specialization_key + props[0]]]> stmts[0]]]> - $a_stmt_comments[0] + props[0]]]> stmts[0]]]> - $b_stmt_comments[0] + stmts]]> @@ -1318,7 +1318,7 @@ - $b[$y] + @@ -1328,21 +1328,21 @@ - $exploded[1] - $url + + - $var_end - $var_start + + new_php_return_type]]> - $last_arg_position + new_php_return_type]]> new_phpdoc_return_type]]> return_typehint_colon_start]]> @@ -1350,7 +1350,7 @@ return_typehint_end]]> return_typehint_start]]> return_typehint_start]]> - $php_type + new_phpdoc_return_type]]> new_psalm_return_type]]> return_type_description]]> @@ -1369,7 +1369,7 @@ typehint_end]]> typehint_start]]> typehint_start]]> - $preceding_semicolon_pos + new_phpdoc_type]]> new_psalm_type]]> type_description]]> @@ -1377,7 +1377,7 @@ - !$sockets + @@ -1387,7 +1387,7 @@ - empty($message) + @@ -1395,39 +1395,39 @@ TCPServerAddress]]> TCPServerAddress]]> onchangeLineLimit]]> - empty($additional_info) + - $method_id_parts[1] + - $arg_var_id - $arg_var_id - $left_var_id - $left_var_id - $right_var_id - $right_var_id - $var_id - $var_id + + + + + + + + - $cs[0] - $match[0] - $match[1] - $match[2] + + + + stmts[0]]]> - $replacement_stmts[0] - $replacement_stmts[0] - $replacement_stmts[0] + + + - !$method_contents + parser->parse( $hacky_class_fix, $error_handler, @@ -1440,25 +1440,25 @@ - $doc_line_parts[1] - $matches[0] + + children[0]]]> children[1]]]> - !$method_entry + - $l[4] - $r[4] + + - !$var_line_parts + newModifier]]> - $class_name + description]]> inheritors]]> yield]]> @@ -1478,10 +1478,10 @@ - $fq_classlike_name - $string_value - $string_value - $string_value + + + + @@ -1490,15 +1490,15 @@ getArgs()[1]]]> - !$skip_if_descendants - !$skip_if_descendants - $include_path - $path_to_file + + + + - $since_parts[1] + 0]]> @@ -1506,7 +1506,7 @@ - $source_param_string + namespace]]> @@ -1521,9 +1521,9 @@ template_types]]> template_types]]> template_types]]> - $template_types - $template_types - $template_types + + + @@ -1535,10 +1535,10 @@ aliases->namespace]]> aliases->namespace]]> template_types]]> - $fq_classlike_name - $function_id - $function_id - $method_name_lc + + + + stmts]]> stmts]]> stmts]]> @@ -1549,7 +1549,7 @@ - $type_string + @@ -1570,17 +1570,17 @@ - $cs[0] + - $offset_map + end_change]]> start_change]]> - $config_file_path !== null + getArgument('pluginName')]]> @@ -1589,7 +1589,7 @@ - $config_file_path !== null + getArgument('pluginName')]]> @@ -1598,7 +1598,7 @@ - $config_file_path !== null + getOption('config')]]> @@ -1606,8 +1606,8 @@ - !$path - $explicit_path + + psalm_header]]> psalm_tag_end_pos]]> @@ -1619,17 +1619,17 @@ - !$root_cache_directory - $file_contents - $file_path + + + - !$cache_directory - !$cache_directory - !$cache_directory - $cache_directory + + + + @@ -1639,88 +1639,88 @@ - !$root_cache_directory + - $result + - $called_method_name + - $extended_var_id + - !$cache_directory - !$root_cache_directory - !$root_cache_directory - !$root_cache_directory + + + + - !$cache_directory - !$cache_directory + + composer_lock_hash]]> - $cache_directory + - !$key_column_name + - $callable_extended_var_id + getTemplateTypeMap()]]> getTemplateTypeMap()]]> - $callable_method_name + - $class_strings ?: null + - $method_name + - $fetch_class_name + - !$call_args + - $existing_file_contents - $existing_file_contents - $existing_file_contents - $existing_statements - $existing_statements - $existing_statements - $existing_statements - $file_changes - $file_path + + + + + + + + + parse($file_contents, $error_handler)]]> parse($file_contents, $error_handler)]]> @@ -1728,14 +1728,14 @@ - $first_line_padding + - !$resolved_name - $mapped_type = $map[$offset_arg_value] ?? null - $mapped_type = $map[$offset_arg_value] ?? null + + + @@ -1757,19 +1757,19 @@ - $key - $var_id - $var_id - $var_id - $var_id - $var_id - $var_id - $var_id + + + + + + + + - isContainedBy + properties[0]]]> @@ -1778,106 +1778,106 @@ - $callable + - TCallable|TClosure|null + - !$class_name - $calling_method_id - $calling_method_id - $calling_method_id - $calling_method_id - $calling_method_id + + + + + + params]]> - $file_name - $file_name - $input_variadic_param_idx - $member_id + + + + - !($container_type_params_covariant[$i] ?? false) + - $intersection_container_type_lower + - $key - $key - $key + + + properties[0]]]> - $properties[0] - $properties[0] - $properties[0] + + + - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $var_id - $var_id - $var_id - $var_id + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - !$count - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $var_id - $var_id - $var_id - $var_id + + + + + + + + + + + + + + + + + + + + + + @@ -1887,12 +1887,12 @@ - getClassTemplateTypes + - $input_template_types + template_extended_params[$container_class])]]> template_extended_params[$base_type->as_type->value])]]> template_extended_params[$base_type->value])]]> @@ -1931,60 +1931,60 @@ value_types['string'] instanceof TNonFalsyString ? $type->value : $type->value !== '']]> - $shared_classlikes + - $fallback_params + template_types]]> - $params - $parent_class - $self_class - $self_class - $self_class - $self_class - $self_class - $self_class - $static_class_type + + + + + + + + + - $const_name - $const_name + + children[0]]]> condition->children[0]]]> - array_keys($offset_template_data)[0] - array_keys($template_type_map[$array_param_name])[0] - array_keys($template_type_map[$class_name])[0] - array_keys($template_type_map[$fq_classlike_name])[0] - array_keys($template_type_map[$template_param_name])[0] + + + + + - $extra_params + value, '::')]]> value, '::')]]> - $type_tokens[$i - 1] - $type_tokens[$i - 1] - $type_tokens[$i - 1] - $type_tokens[$i - 1] + + + + - $parent_fqcln - $self_fqcln + + - !$fq_classlike_name + template_types]]> template_types]]> calling_method_id]]> @@ -1992,23 +1992,23 @@ - $function_id + - $function_id + - $function_id + output_path]]> - $parent_issue_type + @@ -2032,47 +2032,47 @@ - CustomMetadataTrait + - traverse - traverse - traverse - traverse + + + + - $this_var_id + - !$namespace - $namespace - $namespace + + + - classOrInterfaceExists - classOrInterfaceExists - classOrInterfaceExists - getMappedGenericTypeParams - interfaceExtends - interfaceExtends - interfaceExtends - traverse - traverse + + + + + + + + + - array_keys($template_type_map[$value])[0] + - $value + @@ -2080,54 +2080,54 @@ - replace - replace - replace - replace + + + + - $params - $params + + - getMappedGenericTypeParams - replace - replace + + + type_params[1]]]> - !($container_type_params_covariant[$offset] ?? true) + - getMostSpecificTypeFromBounds + - TNonEmptyList + - replace + - !$namespace - $namespace + + - getString - getString - replace - replace + + + + value_param]]> @@ -2135,51 +2135,51 @@ - !$intersection - !$intersection + + - replace + - __construct + - !$intersection - !$intersection + + - !$intersection + - TList + getGenericValueType())]]> getGenericValueType())]]> - combine - combine - combineUnionTypes - combineUnionTypes - combineUnionTypes - combineUnionTypes - combineUnionTypes - combineUnionTypes - combineUnionTypes - replace - replace - replace - replace + + + + + + + + + + + + + possibly_undefined]]> @@ -2189,13 +2189,13 @@ properties[0]]]> - getList + - replace - replace + + type_param]]> @@ -2203,52 +2203,52 @@ - !$namespace - $namespace + + - !$intersection - $intersection + + - TList + - setCount + - replace - replace + + - !$intersection - !$intersection + + - replace + - !$intersection + - replace + - replace + @@ -2258,13 +2258,13 @@ - $allow_mutations - $by_ref - $failed_reconciliation - $from_template_default - $has_mutations - $initialized_class - $reference_free + + + + + + + @@ -2272,13 +2272,13 @@ - $const_name + - $array_key_offset - $failed_reconciliation + + ')]]> @@ -2287,40 +2287,40 @@ - $node + - visit + - $ignore_isset + - traverse - traverse - traverseArray - traverseArray + + + + - TArray|TKeyedArray|TClassStringMap + types['array']]]> - allFloatLiterals - allFloatLiterals - hasLowercaseString - hasLowercaseString + + + + - !$php_type + exact_id]]> id]]> exact_id]]> @@ -2337,8 +2337,8 @@ - $level - $php_version + + @@ -2349,11 +2349,11 @@ - $param_type_1 - $param_type_2 - $param_type_3 - $param_type_4 - $return_type + + + + + From a2980b592436bd48f9564982c2dbcf70222f0d6c Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sat, 3 Feb 2024 18:44:24 +0100 Subject: [PATCH 302/357] Drop unused local composer repo This was intended to show how you could convert legacy plugins to composer format, but it breaks `composer install` in snapshots. --- composer.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/composer.json b/composer.json index 3a0a3465c27..55a61d1ae86 100644 --- a/composer.json +++ b/composer.json @@ -94,12 +94,6 @@ "Psalm\\Tests\\": "tests/" } }, - "repositories": [ - { - "type": "path", - "url": "examples/plugins/composer-based/echo-checker" - } - ], "minimum-stability": "dev", "prefer-stable": true, "bin": [ From 526013e77e25f12cbf68a7a2f7b40ba7d40d8a33 Mon Sep 17 00:00:00 2001 From: robchett Date: Sat, 3 Feb 2024 18:09:23 +0000 Subject: [PATCH 303/357] Fix check-type when using reserved types from within a namespace --- .../Internal/Analyzer/StatementsAnalyzer.php | 13 ++++++-- tests/AssertAnnotationTest.php | 31 ++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index cb3c9b49d94..9e484ca32b5 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -45,6 +45,8 @@ use Psalm\Internal\ReferenceConstraint; use Psalm\Internal\Scanner\ParsedDocblock; use Psalm\Internal\Type\Comparator\UnionTypeComparator; +use Psalm\Internal\Type\TypeParser; +use Psalm\Internal\Type\TypeTokenizer; use Psalm\Issue\CheckType; use Psalm\Issue\ComplexFunction; use Psalm\Issue\ComplexMethod; @@ -678,11 +680,18 @@ private static function analyzeStatement( } else { try { $checked_type = $context->vars_in_scope[$checked_var_id]; - $fq_check_type_string = Type::getFQCLNFromString( + $check_tokens = TypeTokenizer::getFullyQualifiedTokens( $check_type_string, $statements_analyzer->getAliases(), + $statements_analyzer->getTemplateTypeMap(), + ); + $check_type = TypeParser::parseTokens( + $check_tokens, + null, + $statements_analyzer->getTemplateTypeMap() ?? [], + [], + true, ); - $check_type = Type::parseString($fq_check_type_string); /** @psalm-suppress InaccessibleProperty We just created this type */ $check_type->possibly_undefined = $possibly_undefined; diff --git a/tests/AssertAnnotationTest.php b/tests/AssertAnnotationTest.php index 5b619971f8e..83945999d87 100644 --- a/tests/AssertAnnotationTest.php +++ b/tests/AssertAnnotationTest.php @@ -2255,7 +2255,36 @@ function takesSomeIntFromEnum(int $foo): IntEnum function isNonEmptyString($_str): bool { return true; - }', + } + ', + ], + 'assertStringIsNonEmptyStringInNamespace' => [ + 'code' => ' [ 'code' => ' Date: Sat, 3 Feb 2024 18:26:20 +0000 Subject: [PATCH 304/357] Support user defined types for psalm-check-type --- src/Psalm/Internal/Analyzer/StatementsAnalyzer.php | 7 ++++++- tests/CheckTypeTest.php | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index 9e484ca32b5..4f89f5de1d8 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -680,16 +680,21 @@ private static function analyzeStatement( } else { try { $checked_type = $context->vars_in_scope[$checked_var_id]; + + $path = $statements_analyzer->getRootFilePath(); + $file_storage = $codebase->file_storage_provider->get($path); + $check_tokens = TypeTokenizer::getFullyQualifiedTokens( $check_type_string, $statements_analyzer->getAliases(), $statements_analyzer->getTemplateTypeMap(), + $file_storage->type_aliases, ); $check_type = TypeParser::parseTokens( $check_tokens, null, $statements_analyzer->getTemplateTypeMap() ?? [], - [], + $file_storage->type_aliases, true, ); /** @psalm-suppress InaccessibleProperty We just created this type */ diff --git a/tests/CheckTypeTest.php b/tests/CheckTypeTest.php index 457c496db83..64b65e3fc32 100644 --- a/tests/CheckTypeTest.php +++ b/tests/CheckTypeTest.php @@ -38,6 +38,15 @@ final class A {} $_a = new stdClass(); /** @psalm-check-type-exact $_a = \stdClass */', ]; + yield 'allowType' => [ + 'code' => ' Date: Sat, 3 Feb 2024 13:06:12 +1300 Subject: [PATCH 305/357] Add InvalidOverride issue --- config.xsd | 1 + docs/running_psalm/error_levels.md | 1 + docs/running_psalm/issues.md | 1 + docs/running_psalm/issues/InvalidOverride.md | 24 ++++++++++++++++++++ src/Psalm/Issue/InvalidOverride.php | 9 ++++++++ tests/DocumentationTest.php | 4 ++++ 6 files changed, 40 insertions(+) create mode 100644 docs/running_psalm/issues/InvalidOverride.md create mode 100644 src/Psalm/Issue/InvalidOverride.php diff --git a/config.xsd b/config.xsd index 0f3e88916c8..4cdb3298e7d 100644 --- a/config.xsd +++ b/config.xsd @@ -283,6 +283,7 @@ + diff --git a/docs/running_psalm/error_levels.md b/docs/running_psalm/error_levels.md index f3df22adb45..923a36eef57 100644 --- a/docs/running_psalm/error_levels.md +++ b/docs/running_psalm/error_levels.md @@ -98,6 +98,7 @@ Level 5 and above allows a more non-verifiable code, and higher levels are even - [CircularReference](issues/CircularReference.md) - [ConflictingReferenceConstraint](issues/ConflictingReferenceConstraint.md) - [ContinueOutsideLoop](issues/ContinueOutsideLoop.md) +- [InvalidOverride](issues/InvalidOverride.md) - [InvalidTypeImport](issues/InvalidTypeImport.md) - [MethodSignatureMismatch](issues/MethodSignatureMismatch.md) - [OverriddenMethodAccess](issues/OverriddenMethodAccess.md) diff --git a/docs/running_psalm/issues.md b/docs/running_psalm/issues.md index 179f9bf7b53..5f34274acc8 100644 --- a/docs/running_psalm/issues.md +++ b/docs/running_psalm/issues.md @@ -84,6 +84,7 @@ - [InvalidNamedArgument](issues/InvalidNamedArgument.md) - [InvalidNullableReturnType](issues/InvalidNullableReturnType.md) - [InvalidOperand](issues/InvalidOperand.md) + - [InvalidOverride](issues/InvalidOverride.md) - [InvalidParamDefault](issues/InvalidParamDefault.md) - [InvalidParent](issues/InvalidParent.md) - [InvalidPassByReference](issues/InvalidPassByReference.md) diff --git a/docs/running_psalm/issues/InvalidOverride.md b/docs/running_psalm/issues/InvalidOverride.md new file mode 100644 index 00000000000..a93f14b8f71 --- /dev/null +++ b/docs/running_psalm/issues/InvalidOverride.md @@ -0,0 +1,24 @@ +# InvalidOverride + +Emitted when an `Override` attribute was added to a method that does not override a method from a parent class or implemented interface. + +```php + Date: Sat, 3 Feb 2024 18:30:59 +1300 Subject: [PATCH 306/357] Emit InvalidOverride --- .../Analyzer/FunctionLikeAnalyzer.php | 19 +++++ tests/AttributeTest.php | 79 +++++++++++++++++-- 2 files changed, 93 insertions(+), 5 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index bf4378d9158..964b6495148 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -31,6 +31,7 @@ use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Internal\Type\TypeExpander; use Psalm\Issue\InvalidDocblockParamName; +use Psalm\Issue\InvalidOverride; use Psalm\Issue\InvalidParamDefault; use Psalm\Issue\InvalidThrow; use Psalm\Issue\MethodSignatureMismatch; @@ -48,6 +49,7 @@ use Psalm\Node\Expr\VirtualVariable; use Psalm\Node\Stmt\VirtualWhile; use Psalm\Plugin\EventHandler\Event\AfterFunctionLikeAnalysisEvent; +use Psalm\Storage\AttributeStorage; use Psalm\Storage\ClassLikeStorage; use Psalm\Storage\FunctionLikeParameter; use Psalm\Storage\FunctionLikeStorage; @@ -65,6 +67,7 @@ use function array_combine; use function array_diff_key; +use function array_filter; use function array_key_exists; use function array_keys; use function array_merge; @@ -1970,6 +1973,22 @@ private function getFunctionInformation( true, ); + if ($codebase->analysis_php_version_id >= 8_03_00 + && (!$overridden_method_ids || $storage->cased_name === '__construct') + && array_filter( + $storage->attributes, + static fn(AttributeStorage $s): bool => $s->fq_class_name === 'Override', + ) + ) { + IssueBuffer::maybeAdd( + new InvalidOverride( + 'Method ' . $storage->cased_name . ' does not match any parent method', + $codeLocation, + ), + $this->getSuppressedIssues(), + ); + } + if ($overridden_method_ids && !$context->collect_initializations && !$context->collect_mutations diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index ddb5b1f5fd9..4a631c5b3dd 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -295,14 +295,28 @@ class Foo ], 'override' => [ 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.3', + ], + 'overrideInterface' => [ + 'code' => ' [], @@ -527,6 +541,61 @@ function foo() : void {}', function foo(#[Pure] string $str) : void {}', 'error_message' => 'UndefinedAttributeClass - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:36', ], + 'overrideWithNoParent' => [ + 'code' => ' 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:25', + 'error_levels' => [], + 'php_version' => '8.3', + ], + 'overrideConstructor' => [ + 'code' => ' 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:25', + 'error_levels' => [], + 'php_version' => '8.3', + ], + 'overridePrivate' => [ + 'code' => ' 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:7:25', + 'error_levels' => [], + 'php_version' => '8.3', + ], + 'overrideInterfaceWithNoParent' => [ + 'code' => ' 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:25', + 'error_levels' => [], + 'php_version' => '8.3', + ], 'tooFewArgumentsToAttributeConstructor' => [ 'code' => ' Date: Sat, 3 Feb 2024 23:07:52 +0100 Subject: [PATCH 307/357] Clarify that Pull request labels failure is to be resolved by maintainers --- .github/workflows/pr-labels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index 13219480f64..81030f8654d 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -1,4 +1,4 @@ -name: Pull Request Labels +name: Pull Request Labels (to be added by maintainers) on: pull_request: types: [opened, reopened, labeled, unlabeled, synchronize] From 7d07e258a3d15757b446e2cae905e457b12b908b Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Sun, 4 Feb 2024 11:17:13 +1300 Subject: [PATCH 308/357] Add MissingOverrideAttribute issue --- config.xsd | 2 ++ docs/running_psalm/configuration.md | 8 +++++++ docs/running_psalm/error_levels.md | 1 + docs/running_psalm/issues.md | 1 + .../issues/MissingOverrideAttribute.md | 23 +++++++++++++++++++ src/Psalm/Config.php | 6 +++++ src/Psalm/Issue/MissingOverrideAttribute.php | 9 ++++++++ tests/DocumentationTest.php | 3 +++ 8 files changed, 53 insertions(+) create mode 100644 docs/running_psalm/issues/MissingOverrideAttribute.md create mode 100644 src/Psalm/Issue/MissingOverrideAttribute.php diff --git a/config.xsd b/config.xsd index 4cdb3298e7d..c15a74e6080 100644 --- a/config.xsd +++ b/config.xsd @@ -42,6 +42,7 @@ + @@ -319,6 +320,7 @@ + diff --git a/docs/running_psalm/configuration.md b/docs/running_psalm/configuration.md index f4976aaad83..d00baae88d3 100644 --- a/docs/running_psalm/configuration.md +++ b/docs/running_psalm/configuration.md @@ -273,6 +273,14 @@ When `true`, Psalm will complain when referencing an explicit string offset on a ``` When `true`, Psalm will complain when referencing an explicit integer offset on an array e.g. `$arr[7]` without a user first asserting that it exists (either via an `isset` check or via an object-like array). Defaults to `false`. +#### ensureOverrideAttribute +```xml + +``` +When `true`, Psalm will report class and interface methods that override a method on a parent, but do not have an `Override` attribute. Defaults to `false`. + #### phpVersion ```xml */ @@ -1081,6 +1086,7 @@ private static function fromXmlAndPaths( 'includePhpVersionsInErrorBaseline' => 'include_php_versions_in_error_baseline', 'ensureArrayStringOffsetsExist' => 'ensure_array_string_offsets_exist', 'ensureArrayIntOffsetsExist' => 'ensure_array_int_offsets_exist', + 'ensureOverrideAttribute' => 'ensure_override_attribute', 'reportMixedIssues' => 'show_mixed_issues', 'skipChecksOnUnresolvableIncludes' => 'skip_checks_on_unresolvable_includes', 'sealAllMethods' => 'seal_all_methods', diff --git a/src/Psalm/Issue/MissingOverrideAttribute.php b/src/Psalm/Issue/MissingOverrideAttribute.php new file mode 100644 index 00000000000..0e146e19971 --- /dev/null +++ b/src/Psalm/Issue/MissingOverrideAttribute.php @@ -0,0 +1,9 @@ +project_analyzer->getConfig()->ensure_array_string_offsets_exist = $is_array_offset_test; $this->project_analyzer->getConfig()->ensure_array_int_offsets_exist = $is_array_offset_test; + $this->project_analyzer->getConfig()->ensure_override_attribute = $error_message === 'MissingOverrideAttribute'; + foreach ($ignored_issues as $error_level) { $this->project_analyzer->getCodebase()->config->setCustomErrorLevel($error_level, Config::REPORT_SUPPRESS); } @@ -313,6 +315,7 @@ public function providerInvalidCodeParse(): array break; case 'InvalidOverride': + case 'MissingOverrideAttribute': $php_version = '8.3'; break; } From c71ad2221cae8ebe317a6dc77290b91657c70cc3 Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Sun, 4 Feb 2024 15:30:36 +1300 Subject: [PATCH 309/357] Move Override tests to separate file --- tests/AttributeTest.php | 85 ------------------------------ tests/OverrideTest.php | 111 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 85 deletions(-) create mode 100644 tests/OverrideTest.php diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index 4a631c5b3dd..f1051773882 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -293,36 +293,6 @@ class Foo 'ignored_issues' => [], 'php_version' => '8.2', ], - 'override' => [ - 'code' => ' [], - 'ignored_issues' => [], - 'php_version' => '8.3', - ], - 'overrideInterface' => [ - 'code' => ' [], - 'ignored_issues' => [], - 'php_version' => '8.3', - ], 'sensitiveParameter' => [ 'code' => ' 'UndefinedAttributeClass - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:36', ], - 'overrideWithNoParent' => [ - 'code' => ' 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:25', - 'error_levels' => [], - 'php_version' => '8.3', - ], - 'overrideConstructor' => [ - 'code' => ' 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:25', - 'error_levels' => [], - 'php_version' => '8.3', - ], - 'overridePrivate' => [ - 'code' => ' 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:7:25', - 'error_levels' => [], - 'php_version' => '8.3', - ], - 'overrideInterfaceWithNoParent' => [ - 'code' => ' 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:25', - 'error_levels' => [], - 'php_version' => '8.3', - ], 'tooFewArgumentsToAttributeConstructor' => [ 'code' => ' [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.3', + ], + 'overrideInterface' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.3', + ], + ]; + } + + public function providerInvalidCodeParse(): iterable + { + return [ + 'noParent' => [ + 'code' => ' 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:25', + 'error_levels' => [], + 'php_version' => '8.3', + ], + 'constructor' => [ + 'code' => ' 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:25', + 'error_levels' => [], + 'php_version' => '8.3', + ], + 'privateMethod' => [ + 'code' => ' 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:7:25', + 'error_levels' => [], + 'php_version' => '8.3', + ], + 'interfaceWithNoParent' => [ + 'code' => ' 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:25', + 'error_levels' => [], + 'php_version' => '8.3', + ], + ]; + } +} From 8396360d30050ee2c8565c132d5e8c5391896efb Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Sun, 4 Feb 2024 11:35:03 +1300 Subject: [PATCH 310/357] Emit MissingOverrideAttribute --- .../Analyzer/FunctionLikeAnalyzer.php | 40 ++++++++--- tests/OverrideTest.php | 69 +++++++++++++++++++ 2 files changed, 98 insertions(+), 11 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 964b6495148..70786016bf7 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -37,6 +37,7 @@ use Psalm\Issue\MethodSignatureMismatch; use Psalm\Issue\MismatchingDocblockParamType; use Psalm\Issue\MissingClosureParamType; +use Psalm\Issue\MissingOverrideAttribute; use Psalm\Issue\MissingParamType; use Psalm\Issue\MissingThrowsDocblock; use Psalm\Issue\ReferenceConstraintViolation; @@ -1973,20 +1974,37 @@ private function getFunctionInformation( true, ); - if ($codebase->analysis_php_version_id >= 8_03_00 - && (!$overridden_method_ids || $storage->cased_name === '__construct') - && array_filter( + if ($codebase->analysis_php_version_id >= 8_03_00) { + $has_override_attribute = array_filter( $storage->attributes, static fn(AttributeStorage $s): bool => $s->fq_class_name === 'Override', - ) - ) { - IssueBuffer::maybeAdd( - new InvalidOverride( - 'Method ' . $storage->cased_name . ' does not match any parent method', - $codeLocation, - ), - $this->getSuppressedIssues(), ); + + if ($has_override_attribute + && (!$overridden_method_ids || $storage->cased_name === '__construct') + ) { + IssueBuffer::maybeAdd( + new InvalidOverride( + 'Method ' . $storage->cased_name . ' does not match any parent method', + $codeLocation, + ), + $this->getSuppressedIssues(), + ); + } + + if (!$has_override_attribute + && $codebase->config->ensure_override_attribute + && $overridden_method_ids + && $storage->cased_name !== '__construct' + ) { + IssueBuffer::maybeAdd( + new MissingOverrideAttribute( + 'Method ' . $storage->cased_name . ' should have the "Override" attribute', + $codeLocation, + ), + $this->getSuppressedIssues(), + ); + } } if ($overridden_method_ids diff --git a/tests/OverrideTest.php b/tests/OverrideTest.php index 1798bf7804d..17782bc9eca 100644 --- a/tests/OverrideTest.php +++ b/tests/OverrideTest.php @@ -2,6 +2,7 @@ namespace Psalm\Tests; +use Psalm\Config; use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; @@ -12,9 +13,33 @@ class OverrideTest extends TestCase use ValidCodeAnalysisTestTrait; use InvalidCodeAnalysisTestTrait; + protected function makeConfig(): Config + { + $config = parent::makeConfig(); + $config->ensure_override_attribute = true; + return $config; + } + public function providerValidCodeParse(): iterable { return [ + 'constructor' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.3', + ], 'overrideClass' => [ 'code' => ' [], 'php_version' => '8.3', ], + 'classMissingAttribute' => [ + 'code' => ' 'MissingOverrideAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:7:25', + 'error_levels' => [], + 'php_version' => '8.3', + ], + 'classUsingTrait' => [ + 'code' => ' 'MissingOverrideAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:25', + 'error_levels' => [], + 'php_version' => '8.3', + ], 'constructor' => [ 'code' => ' [], 'php_version' => '8.3', ], + 'interfaceMissingAttribute' => [ + 'code' => ' 'MissingOverrideAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:7:25', + 'error_levels' => [], + 'php_version' => '8.3', + ], 'privateMethod' => [ 'code' => ' Date: Sun, 4 Feb 2024 18:52:20 +0100 Subject: [PATCH 311/357] Do not add `callable` as a native property type It's invalid in all PHP versions: https://3v4l.org/bXWo2 Also see php.net/manual/en/language.types.declarations.php#language.types.declarations.base.function Fixes vimeo/psalm#10650 --- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 4 ++- .../MissingPropertyTypeTest.php | 36 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index 3fcaa310d48..d40c86b2c90 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -1649,7 +1649,9 @@ private static function addOrUpdatePropertyType( $allow_native_type = !$docblock_only && $codebase->analysis_php_version_id >= 7_04_00 - && $codebase->allow_backwards_incompatible_changes; + && $codebase->allow_backwards_incompatible_changes + && !$inferred_type->hasCallableType() // PHP does not support callable properties + ; $manipulator->setType( $allow_native_type diff --git a/tests/FileManipulation/MissingPropertyTypeTest.php b/tests/FileManipulation/MissingPropertyTypeTest.php index afeb644ee88..5e0db2d6c4d 100644 --- a/tests/FileManipulation/MissingPropertyTypeTest.php +++ b/tests/FileManipulation/MissingPropertyTypeTest.php @@ -294,6 +294,42 @@ public function bar() { 'issues_to_fix' => ['MissingPropertyType'], 'safe_types' => true, ], + 'doNotAddCallablePropertyTypes' => [ + 'input' => <<<'PHP' + u = $u; + $this->v = $v; + } + } + PHP, + 'output' => <<<'PHP' + u = $u; + $this->v = $v; + } + } + PHP, + 'php_version' => '7.4', + 'issues_to_fix' => ['MissingPropertyType'], + 'safe_types' => true, + ], ]; } } From b2a2cd7884d9c92cebfb44e71516658b2ab64771 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 4 Feb 2024 19:16:42 +0100 Subject: [PATCH 312/357] Allow adding `Closure` as a native property type --- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 4 +++- src/Psalm/Type/Atomic/TClosure.php | 3 ++- .../MissingPropertyTypeTest.php | 23 +++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index d40c86b2c90..a584d9f0f97 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -1650,7 +1650,9 @@ private static function addOrUpdatePropertyType( $allow_native_type = !$docblock_only && $codebase->analysis_php_version_id >= 7_04_00 && $codebase->allow_backwards_incompatible_changes - && !$inferred_type->hasCallableType() // PHP does not support callable properties + // PHP does not support callable properties, but does allow Closure properties + // hasCallableType() treats Closure as a callable, but getCallableTypes() does not + && $inferred_type->getCallableTypes() === [] ; $manipulator->setType( diff --git a/src/Psalm/Type/Atomic/TClosure.php b/src/Psalm/Type/Atomic/TClosure.php index 94ee9446d44..07d6740cbec 100644 --- a/src/Psalm/Type/Atomic/TClosure.php +++ b/src/Psalm/Type/Atomic/TClosure.php @@ -50,7 +50,8 @@ public function __construct( public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool { - return false; + // it can, if it's just 'Closure' + return $this->params === null && $this->return_type === null && $this->is_pure === null; } /** diff --git a/tests/FileManipulation/MissingPropertyTypeTest.php b/tests/FileManipulation/MissingPropertyTypeTest.php index 5e0db2d6c4d..f32d8c2562c 100644 --- a/tests/FileManipulation/MissingPropertyTypeTest.php +++ b/tests/FileManipulation/MissingPropertyTypeTest.php @@ -330,6 +330,29 @@ public function __construct(?callable $u, callable $v) { 'issues_to_fix' => ['MissingPropertyType'], 'safe_types' => true, ], + 'addClosurePropertyType' => [ + 'input' => <<<'PHP' + u = $u; + } + } + PHP, + 'output' => <<<'PHP' + u = $u; + } + } + PHP, + 'php_version' => '7.4', + 'issues_to_fix' => ['MissingPropertyType'], + 'safe_types' => true, + ], ]; } } From 6d572a681c390bf26e183907d0389e7ea9b2c94a Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 4 Feb 2024 15:41:01 -0400 Subject: [PATCH 313/357] Apply suggestions from code review --- tests/OverrideTest.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/tests/OverrideTest.php b/tests/OverrideTest.php index 17782bc9eca..ab808a06df9 100644 --- a/tests/OverrideTest.php +++ b/tests/OverrideTest.php @@ -6,8 +6,6 @@ use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; -use const DIRECTORY_SEPARATOR; - class OverrideTest extends TestCase { use ValidCodeAnalysisTestTrait; @@ -83,7 +81,7 @@ class C { public function f(): void {} } ', - 'error_message' => 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:25', + 'error_message' => 'InvalidOverride', 'error_levels' => [], 'php_version' => '8.3', ], @@ -97,7 +95,7 @@ class C2 extends C { public function f(): void {} } ', - 'error_message' => 'MissingOverrideAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:7:25', + 'error_message' => 'MissingOverrideAttribute', 'error_levels' => [], 'php_version' => '8.3', ], @@ -113,7 +111,7 @@ class C { public function f(): void {} } ', - 'error_message' => 'MissingOverrideAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:25', + 'error_message' => 'MissingOverrideAttribute', 'error_levels' => [], 'php_version' => '8.3', ], @@ -131,7 +129,7 @@ class C2 extends C { public function __construct() {} } ', - 'error_message' => 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:25', + 'error_message' => 'InvalidOverride', 'error_levels' => [], 'php_version' => '8.3', ], @@ -145,7 +143,7 @@ interface I2 extends I { public function f(): void; } ', - 'error_message' => 'MissingOverrideAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:7:25', + 'error_message' => 'MissingOverrideAttribute', 'error_levels' => [], 'php_version' => '8.3', ], @@ -160,7 +158,7 @@ class C2 extends C { private function f(): void {} } ', - 'error_message' => 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:7:25', + 'error_message' => 'InvalidOverride', 'error_levels' => [], 'php_version' => '8.3', ], @@ -171,7 +169,7 @@ interface I { public function f(): void; } ', - 'error_message' => 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:25', + 'error_message' => 'InvalidOverride', 'error_levels' => [], 'php_version' => '8.3', ], From 52eadab971aa25bb21b38d5d2d170af4c25cf76b Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Mon, 5 Feb 2024 04:00:10 +0100 Subject: [PATCH 314/357] Late binding of enum cases Resolves a number of long-standing bugs ('Failed to infer case value ...') Fixes vimeo/psalm#10374 Fixes vimeo/psalm#10560 Fixes vimeo/psalm#10643 Fixes vimeo/psalm#8978 --- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 17 +++++----- .../Fetch/AtomicPropertyFetchAnalyzer.php | 9 ++--- .../Codebase/ConstantTypeResolver.php | 7 ++++ src/Psalm/Internal/Codebase/Methods.php | 8 +++-- .../Reflector/ClassLikeNodeScanner.php | 12 +++++-- .../GetObjectVarsReturnTypeProvider.php | 9 ++--- .../Type/SimpleAssertionReconciler.php | 11 +++--- src/Psalm/Storage/EnumCaseStorage.php | 33 ++++++++++++++++-- src/Psalm/Type/Atomic/TValueOf.php | 18 ++++++---- tests/EnumTest.php | 34 +++++++++++++++++++ 10 files changed, 124 insertions(+), 34 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index 3fcaa310d48..254c5cc53c0 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -2486,7 +2486,8 @@ private function checkEnum(): void $seen_values = []; foreach ($storage->enum_cases as $case_storage) { - if ($case_storage->value !== null && $storage->enum_type === null) { + $case_value = $case_storage->getValue($this->getCodebase()->classlikes); + if ($case_value !== null && $storage->enum_type === null) { IssueBuffer::maybeAdd( new InvalidEnumCaseValue( 'Case of a non-backed enum should not have a value', @@ -2494,7 +2495,7 @@ private function checkEnum(): void $storage->name, ), ); - } elseif ($case_storage->value === null && $storage->enum_type !== null) { + } elseif ($case_value === null && $storage->enum_type !== null) { IssueBuffer::maybeAdd( new InvalidEnumCaseValue( 'Case of a backed enum should have a value', @@ -2502,9 +2503,9 @@ private function checkEnum(): void $storage->name, ), ); - } elseif ($case_storage->value !== null) { - if ((is_int($case_storage->value) && $storage->enum_type === 'string') - || (is_string($case_storage->value) && $storage->enum_type === 'int') + } elseif ($case_value !== null) { + if ((is_int($case_value) && $storage->enum_type === 'string') + || (is_string($case_value) && $storage->enum_type === 'int') ) { IssueBuffer::maybeAdd( new InvalidEnumCaseValue( @@ -2516,8 +2517,8 @@ private function checkEnum(): void } } - if ($case_storage->value !== null) { - if (in_array($case_storage->value, $seen_values, true)) { + if ($case_value !== null) { + if (in_array($case_value, $seen_values, true)) { IssueBuffer::maybeAdd( new DuplicateEnumCaseValue( 'Enum case values should be unique', @@ -2526,7 +2527,7 @@ private function checkEnum(): void ), ); } else { - $seen_values[] = $case_storage->value; + $seen_values[] = $case_value; } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index f01f379e26a..6ad6983b76e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -1035,10 +1035,11 @@ private static function handleEnumValue( $case_values = []; foreach ($enum_cases as $enum_case) { - if (is_string($enum_case->value)) { - $case_values[] = Type::getAtomicStringFromLiteral($enum_case->value); - } elseif (is_int($enum_case->value)) { - $case_values[] = new TLiteralInt($enum_case->value); + $case_value = $enum_case->getValue($statements_analyzer->getCodebase()->classlikes); + if (is_string($case_value)) { + $case_values[] = Type::getAtomicStringFromLiteral($case_value); + } elseif (is_int($case_value)) { + $case_values[] = new TLiteralInt($case_value); } else { // this should never happen $case_values[] = new TMixed(); diff --git a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php index 4bc718e3e49..fc6940b1cfb 100644 --- a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php +++ b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php @@ -344,6 +344,13 @@ public static function resolve( return Type::getString($value)->getSingleAtomic(); } elseif (is_int($value)) { return Type::getInt(false, $value)->getSingleAtomic(); + } elseif ($value instanceof UnresolvedConstantComponent) { + return self::resolve( + $classlikes, + $value, + $statements_analyzer, + $visited_constant_ids + [$c_id => true], + ); } } elseif ($c instanceof EnumNameFetch) { return Type::getString($c->case)->getSingleAtomic(); diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php index 9648729c473..ad97dfbc65e 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -628,11 +628,13 @@ public function getMethodReturnType( ) { $types = []; foreach ($original_class_storage->enum_cases as $case_name => $case_storage) { + $case_value = $case_storage->getValue($this->classlikes); + if (UnionTypeComparator::isContainedBy( $source_analyzer->getCodebase(), - is_int($case_storage->value) ? - Type::getInt(false, $case_storage->value) : - Type::getString($case_storage->value), + is_int($case_value) ? + Type::getInt(false, $case_value) : + Type::getString($case_value), $first_arg_type, )) { $types[] = new TEnumCase($original_fq_class_name, $case_name); diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 7589c018f98..999f95df554 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -32,6 +32,7 @@ use Psalm\Internal\Provider\NodeDataProvider; use Psalm\Internal\Scanner\ClassLikeDocblockComment; use Psalm\Internal\Scanner\FileScanner; +use Psalm\Internal\Scanner\UnresolvedConstantComponent; use Psalm\Internal\Type\TypeAlias; use Psalm\Internal\Type\TypeAlias\ClassTypeAlias; use Psalm\Internal\Type\TypeAlias\InlineTypeAlias; @@ -65,7 +66,6 @@ use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Union; -use RuntimeException; use UnexpectedValueException; use function array_merge; @@ -752,6 +752,9 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool $values_types[] = Type::getAtomicStringFromLiteral($enumCaseStorage->value); } elseif (is_int($enumCaseStorage->value)) { $values_types[] = new Type\Atomic\TLiteralInt($enumCaseStorage->value); + } elseif ($enumCaseStorage->value instanceof UnresolvedConstantComponent) { + // backed enum with a type yet unknown + $values_types[] = new Type\Atomic\TMixed; } } } @@ -1462,7 +1465,12 @@ private function visitEnumDeclaration( ); } } else { - throw new RuntimeException('Failed to infer case value for ' . $stmt->name->name); + $enum_value = ExpressionResolver::getUnresolvedClassConstExpr( + $stmt->expr, + $this->aliases, + $fq_classlike_name, + $storage->parent_class, + ); } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php index 55e4f38bd42..910300497fc 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php @@ -63,10 +63,11 @@ public static function getGetObjectVarsReturnType( return new TKeyedArray($properties); } $enum_case_storage = $enum_classlike_storage->enum_cases[$object_type->case_name]; - if (is_int($enum_case_storage->value)) { - $properties['value'] = new Union([new Atomic\TLiteralInt($enum_case_storage->value)]); - } elseif (is_string($enum_case_storage->value)) { - $properties['value'] = new Union([Type::getAtomicStringFromLiteral($enum_case_storage->value)]); + $case_value = $enum_case_storage->getValue($statements_source->getCodebase()->classlikes); + if (is_int($case_value)) { + $properties['value'] = new Union([new Atomic\TLiteralInt($case_value)]); + } elseif (is_string($case_value)) { + $properties['value'] = new Union([Type::getAtomicStringFromLiteral($case_value)]); } return new TKeyedArray($properties); } diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 28807b052b7..66cfc2f9e96 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -2971,11 +2971,12 @@ private static function reconcileValueOf( // For value-of, the assertion is meant to return *ANY* value of *ANY* enum case if ($enum_case_to_assert === null) { foreach ($class_storage->enum_cases as $enum_case) { + $enum_value = $enum_case->getValue($codebase->classlikes); assert( - $enum_case->value !== null, + $enum_value !== null, 'Verified enum type above, value can not contain `null` anymore.', ); - $reconciled_types[] = Type::getLiteral($enum_case->value); + $reconciled_types[] = Type::getLiteral($enum_value); } continue; @@ -2986,8 +2987,10 @@ private static function reconcileValueOf( return null; } - assert($enum_case->value !== null, 'Verified enum type above, value can not contain `null` anymore.'); - $reconciled_types[] = Type::getLiteral($enum_case->value); + $enum_value = $enum_case->getValue($codebase->classlikes); + + assert($enum_value !== null, 'Verified enum type above, value can not contain `null` anymore.'); + $reconciled_types[] = Type::getLiteral($enum_value); } if ($reconciled_types === []) { diff --git a/src/Psalm/Storage/EnumCaseStorage.php b/src/Psalm/Storage/EnumCaseStorage.php index 4c91419a69b..ec0e6ee74b6 100644 --- a/src/Psalm/Storage/EnumCaseStorage.php +++ b/src/Psalm/Storage/EnumCaseStorage.php @@ -3,13 +3,19 @@ namespace Psalm\Storage; use Psalm\CodeLocation; +use Psalm\Internal\Codebase\ClassLikes; +use Psalm\Internal\Codebase\ConstantTypeResolver; +use Psalm\Internal\Scanner\UnresolvedConstantComponent; +use Psalm\Type\Atomic\TLiteralInt; +use Psalm\Type\Atomic\TLiteralString; +use UnexpectedValueException; final class EnumCaseStorage { use UnserializeMemoryUsageSuppressionTrait; /** - * @var int|string|null + * @var int|string|null|UnresolvedConstantComponent */ public $value; @@ -22,7 +28,7 @@ final class EnumCaseStorage public $deprecated = false; /** - * @param int|string|null $value + * @param int|string|null|UnresolvedConstantComponent $value */ public function __construct( $value, @@ -31,4 +37,27 @@ public function __construct( $this->value = $value; $this->stmt_location = $location; } + + /** @return int|string|null */ + public function getValue(ClassLikes $classlikes) + { + $case_value = $this->value; + + if ($case_value instanceof UnresolvedConstantComponent) { + $case_value = ConstantTypeResolver::resolve( + $classlikes, + $case_value, + ); + + if ($case_value instanceof TLiteralString) { + $case_value = $case_value->value; + } elseif ($case_value instanceof TLiteralInt) { + $case_value = $case_value->value; + } else { + throw new UnexpectedValueException('Failed to infer case value'); + } + } + + return $case_value; + } } diff --git a/src/Psalm/Type/Atomic/TValueOf.php b/src/Psalm/Type/Atomic/TValueOf.php index 59941bc61e5..eb8df8ce03a 100644 --- a/src/Psalm/Type/Atomic/TValueOf.php +++ b/src/Psalm/Type/Atomic/TValueOf.php @@ -32,20 +32,24 @@ public function __construct(Union $type, bool $from_docblock = false) /** * @param non-empty-array $cases */ - private static function getValueTypeForNamedObject(array $cases, TNamedObject $atomic_type): Union - { + private static function getValueTypeForNamedObject( + array $cases, + TNamedObject $atomic_type, + Codebase $codebase + ): Union { if ($atomic_type instanceof TEnumCase) { assert(isset($cases[$atomic_type->case_name]), 'Should\'ve been verified in TValueOf#getValueType'); - $value = $cases[$atomic_type->case_name]->value; + $value = $cases[$atomic_type->case_name]->getValue($codebase->classlikes); assert($value !== null, 'Backed enum must have a value.'); return new Union([ConstantTypeResolver::getLiteralTypeFromScalarValue($value)]); } return new Union(array_map( - static function (EnumCaseStorage $case): Atomic { - assert($case->value !== null); + static function (EnumCaseStorage $case) use ($codebase): Atomic { + $case_value = $case->getValue($codebase->classlikes); + assert($case_value !== null); // Backed enum must have a value - return ConstantTypeResolver::getLiteralTypeFromScalarValue($case->value); + return ConstantTypeResolver::getLiteralTypeFromScalarValue($case_value); }, array_values($cases), )); @@ -141,7 +145,7 @@ public static function getValueType( continue; } - $value_atomics = self::getValueTypeForNamedObject($cases, $atomic_type); + $value_atomics = self::getValueTypeForNamedObject($cases, $atomic_type, $codebase); } else { continue; } diff --git a/tests/EnumTest.php b/tests/EnumTest.php index 49d1693054d..f66cdae3889 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -679,6 +679,40 @@ enum Bar: int 'ignored_issues' => [], 'php_version' => '8.1', ], + 'enumWithCasesReferencingClassConstantsWhereClassIsDefinedAfterTheEnum' => [ + 'code' => <<<'PHP' + value; + PHP, + 'assertions' => [ + '$a===' => "'foo'", + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'enumWithCasesReferencingAnotherEnumCase' => [ + 'code' => <<<'PHP' + value; + } + enum Foo: string { + case FOO = "foo"; + } + $a = Bar::BAR->value; + PHP, + 'assertions' => [ + '$a===' => "'foo'", + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } From 49f5c99af0360a39d5f661ce529d308d33195dc4 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Mon, 5 Feb 2024 04:51:14 +0100 Subject: [PATCH 315/357] Add missing strict_types --- src/Psalm/Issue/InvalidOverride.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Psalm/Issue/InvalidOverride.php b/src/Psalm/Issue/InvalidOverride.php index bea55e8d0ad..84ef64040e1 100644 --- a/src/Psalm/Issue/InvalidOverride.php +++ b/src/Psalm/Issue/InvalidOverride.php @@ -1,5 +1,7 @@ Date: Sun, 4 Feb 2024 11:51:57 +0100 Subject: [PATCH 316/357] directory_separator optional wrong --- src/Psalm/Config.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index bebf471599c..3e440e3809b 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -1817,7 +1817,13 @@ private function getPluginClassForPath(Codebase $codebase, string $path, string public function shortenFileName(string $to): string { if (!is_file($to)) { - return preg_replace('/^' . preg_quote($this->base_dir . DIRECTORY_SEPARATOR, '/') . '?/', '', $to, 1); + // if cwd is the root directory it will be just the directory separator - trim it off first + return preg_replace( + '/^' . preg_quote(rtrim($this->base_dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR, '/') . '/', + '', + $to, + 1, + ); } $from = $this->base_dir; From 2008ea078a89061713010197df7a574ef3ab89f2 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Mon, 5 Feb 2024 08:19:23 +0100 Subject: [PATCH 317/357] code review add die to forbidden functions for psalm itself --- psalm.xml.dist | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/psalm.xml.dist b/psalm.xml.dist index 816cdc02e87..0843b86bcc9 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -173,4 +173,8 @@ + + + + From a827806709b4c35a5c803b7cc9fbb38fe28b22bd Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Mon, 5 Feb 2024 21:49:40 +1300 Subject: [PATCH 318/357] ParseError for dynamic constants before PHP 8.3 --- .../Statements/Expression/ClassConstAnalyzer.php | 10 ++++++++++ tests/ConstantTest.php | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php index 58b3cfb419e..92025d0bb2f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php @@ -206,6 +206,16 @@ public static function analyzeFetch( } if (!$stmt->name instanceof PhpParser\Node\Identifier) { + if ($codebase->analysis_php_version_id < 8_03_00) { + IssueBuffer::maybeAdd( + new ParseError( + 'Dynamically fetching class constants and enums requires PHP 8.3', + new CodeLocation($statements_analyzer->getSource(), $stmt), + ), + $statements_analyzer->getSuppressedIssues(), + ); + } + $was_inside_general_use = $context->inside_general_use; $context->inside_general_use = true; diff --git a/tests/ConstantTest.php b/tests/ConstantTest.php index 1bb5fa1c3d4..06e5cffe7fc 100644 --- a/tests/ConstantTest.php +++ b/tests/ConstantTest.php @@ -2476,6 +2476,18 @@ class A { PHP, 'error_message' => 'InvalidArrayOffset', ], + 'unsupportedDynamicFetch' => [ + 'code' => ' 'ParseError', + 'errors_levels' => [], + 'php_version' => '8.2', + ], ]; } } From 4cec31eba9a76baf919342611dda87bdf89cacc9 Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Mon, 5 Feb 2024 21:51:34 +1300 Subject: [PATCH 319/357] Test for dynamic enum fetch --- tests/ConstantTest.php | 2 +- tests/UnusedVariableTest.php | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/ConstantTest.php b/tests/ConstantTest.php index 06e5cffe7fc..828f2ea654e 100644 --- a/tests/ConstantTest.php +++ b/tests/ConstantTest.php @@ -2485,7 +2485,7 @@ class C { $a = C::{"A"}; ', 'error_message' => 'ParseError', - 'errors_levels' => [], + 'error_levels' => [], 'php_version' => '8.2', ], ]; diff --git a/tests/UnusedVariableTest.php b/tests/UnusedVariableTest.php index 2b88fbef301..15bf8b182a3 100644 --- a/tests/UnusedVariableTest.php +++ b/tests/UnusedVariableTest.php @@ -1029,6 +1029,25 @@ public function foo() : void { 'ignored_issues' => [], 'php_version' => '8.3', ], + 'usedAsEnumFetch' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.3', + ], 'usedAsStaticPropertyAssign' => [ 'code' => ' Date: Mon, 5 Feb 2024 20:44:09 +0100 Subject: [PATCH 320/357] Suppress `UndefinedClass` in `whatever_exists()` Fixes vimeo/psalm#7395 --- .../Expression/Call/ArgumentsAnalyzer.php | 2 +- tests/FunctionCallTest.php | 27 +++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 003355bfcd4..81843d56a15 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -191,7 +191,7 @@ public static function analyze( $toggled_class_exists = false; - if ($method_id === 'class_exists' + if (in_array($method_id, ['class_exists', 'interface_exists', 'enum_exists', 'trait_exists'], true) && $argument_offset === 0 && !$context->inside_class_exists ) { diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 27b10b28228..c2ca6040367 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -777,8 +777,31 @@ function exploder(string $d, string $s) : array { }', ], 'allowPossiblyUndefinedClassInClassExists' => [ - 'code' => ' <<<'PHP' + [ + 'code' => <<<'PHP' + [ + 'code' => <<<'PHP' + [ + 'code' => <<<'PHP' + [], + 'ignored_issues' => [], + 'php_version' => '8.1', ], 'allowConstructorAfterClassExists' => [ 'code' => ' Date: Tue, 6 Feb 2024 01:24:18 +0100 Subject: [PATCH 321/357] Flag `stdClass::__construct()` calls that have arguments Fixes vimeo/psalm#10018 --- .../Statements/Expression/Call/NewAnalyzer.php | 11 +++++++++++ tests/MethodCallTest.php | 9 ++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php index 1a9daef9fb5..1dbf404200e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php @@ -244,6 +244,17 @@ public static function analyze( new Union([$result_atomic_type]), ); + if (strtolower($fq_class_name) === 'stdclass' && $stmt->getArgs() !== []) { + IssueBuffer::maybeAdd( + new TooManyArguments( + 'stdClass::__construct() has no parameters', + new CodeLocation($statements_analyzer->getSource(), $stmt), + 'stdClass::__construct', + ), + $statements_analyzer->getSuppressedIssues(), + ); + } + if (strtolower($fq_class_name) !== 'stdclass' && $codebase->classlikes->classExists($fq_class_name) ) { diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index fa56564e790..49eebbf7546 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -1238,7 +1238,7 @@ public function x(): void {} } class Child1 extends Old {} class Child2 extends Old {} - + /** * @template IsClient of bool */ @@ -1798,6 +1798,13 @@ public function foo(callable $_a = "strlen"): void {} ', 'error_message' => 'InvalidParamDefault', ], + 'stdClassConstructorHasNoParameters' => [ + 'code' => <<<'PHP' + 'TooManyArguments', + ], ]; } } From 6b3380b05fcfad2ea146de993b90f269789eb235 Mon Sep 17 00:00:00 2001 From: Ivan Sidorov Date: Tue, 6 Feb 2024 15:20:02 +0000 Subject: [PATCH 322/357] Regression and fail tests List fail tests: ``` 1) testMethodAnnotation with data set "static (string|int)[] getArray()" Undefined array key 0 /workspaces/psalm/tests/ClassLikeDocblockParserTest.php:201 2) testMethodAnnotation with data set "static (callable() : string) getCallable()" Undefined array key 0 /workspaces/psalm/tests/ClassLikeDocblockParserTest.php:201 ``` --- tests/ClassLikeDocblockParserTest.php | 184 ++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) diff --git a/tests/ClassLikeDocblockParserTest.php b/tests/ClassLikeDocblockParserTest.php index 96a0f6911f7..4fd36cf7ae8 100644 --- a/tests/ClassLikeDocblockParserTest.php +++ b/tests/ClassLikeDocblockParserTest.php @@ -7,6 +7,8 @@ use Psalm\Aliases; use Psalm\Internal\PhpVisitor\Reflector\ClassLikeDocblockParser; +use function array_values; + class ClassLikeDocblockParserTest extends TestCase { public function testDocblockDescription(): void @@ -35,4 +37,186 @@ public function testPreferPsalmPrefixedAnnotationsOverPhpstanOnes(): void $class_docblock = ClassLikeDocblockParser::parse($node, $php_parser_doc, new Aliases()); $this->assertSame([['T', 'of', 'string', true, 33]], $class_docblock->templates); } + + /** + * @return iterable + */ + public function providerMethodAnnotation(): iterable + { + $data = [ + 'foo()' => [ + 'name' => 'foo', + 'returnType' => '', + 'is_static' => false, + 'params' => [], + ], + 'foo($a)' => [ + 'name' => 'foo', + 'returnType' => '', + 'is_static' => false, + 'params' => [ + 'a' => ['type' => ''], + ], + ], + 'string foo()' => [ + 'name' => 'foo', + 'returnType' => 'string', + 'is_static' => false, + 'params' => [], + ], + 'static string foo()' => [ + 'name' => 'foo', + 'returnType' => 'string', + 'is_static' => true, + 'params' => [], + ], + 'string foo(string $a, int $b)' => [ + 'name' => 'foo', + 'returnType' => 'string', + 'is_static' => false, + 'params' => [ + 'a' => ['type' => 'string'], + 'b' => ['type' => 'int'], + ], + ], + 'static string foo(string $a, int $b)' => [ + 'name' => 'foo', + 'returnType' => 'string', + 'is_static' => true, + 'params' => [ + 'a' => ['type' => 'string'], + 'b' => ['type' => 'int'], + ], + ], + 'static foo()' => [ + 'name' => 'foo', + 'returnType' => 'static', + 'is_static' => false, + 'params' => [], + ], + 'static static foo()' => [ + 'name' => 'foo', + 'returnType' => 'static', + 'is_static' => true, + 'params' => [], + ], + 'static foo(string $z)' => [ + 'name' => 'foo', + 'returnType' => 'static', + 'is_static' => false, + 'params' => [ + 'z' => ['type' => 'string'], + ], + ], + 'static static foo(string $z)' => [ + 'name' => 'foo', + 'returnType' => 'static', + 'is_static' => true, + 'params' => [ + 'z' => ['type' => 'string'], + ], + ], + 'self foo()' => [ + 'name' => 'foo', + 'returnType' => 'MyClass', + 'is_static' => false, + 'params' => [], + ], + 'static self foo()' => [ + 'name' => 'foo', + 'returnType' => 'MyClass', + 'is_static' => true, + 'params' => [], + ], + 'self foo(string $z)' => [ + 'name' => 'foo', + 'returnType' => 'MyClass', + 'is_static' => false, + 'params' => [ + 'z' => ['type' => 'string'], + ], + ], + 'static self foo(string $z)' => [ + 'name' => 'foo', + 'returnType' => 'MyClass', + 'is_static' => true, + 'params' => [ + 'z' => ['type' => 'string'], + ], + ], + '(string|int)[] getArray()' => [ + 'name' => 'getArray', + 'returnType' => 'array', + 'is_static' => false, + 'params' => [], + ], + 'static (string|int)[] getArray()' => [ + 'name' => 'getArray', + 'returnType' => 'array', + 'is_static' => true, + 'params' => [], + ], + '(callable() : string) getCallable()' => [ + 'name' => 'getCallable', + 'returnType' => 'callable():string', + 'is_static' => false, + 'params' => [], + ], + 'static (callable() : string) getCallable()' => [ + 'name' => 'getCallable', + 'returnType' => 'callable():string', + 'is_static' => true, + 'params' => [], + ], + ]; + + $res = []; + foreach ($data as $key => $item) { + $res[$key] = [ + 'annotation' => $key, + 'expected' => $item, + ]; + } + + return $res; + } + + /** + * @dataProvider providerMethodAnnotation + */ + public function testMethodAnnotation(string $annotation, array $expected): void + { + $full_content = <<addFile('somefile.php', $full_content); + + $codebase = $this->project_analyzer->getCodebase(); + $codebase->scanFiles(); + + $class_storage = $codebase->classlike_storage_provider->get('MyClass'); + $methods = $expected['is_static'] + ? $class_storage->pseudo_static_methods + : $class_storage->pseudo_methods; + $method = array_values($methods)[0]; + + $actual = [ + 'name' => $method->cased_name, + 'returnType' => (string) $method->return_type, + 'is_static' => $method->is_static, + 'params' => [], + ]; + foreach ($method->params as $param) { + $actual['params'][$param->name] = [ + 'type' => (string) $param->type, + ]; + } + + $this->assertEquals($expected, $actual); + } } From afaaa3d6dfb4fca8ffb13b3e7a2d49b8a797a08f Mon Sep 17 00:00:00 2001 From: Ivan Sidorov Date: Tue, 6 Feb 2024 16:09:32 +0000 Subject: [PATCH 323/357] Fix parsing magic method annotations Fix parsing for code: ``` /** * @method static (string|int)[] getArray() * @method static (callable() : string) getCallable() */ class MyClass {} ``` Resolved tests: ``` 1) testMethodAnnotation with data set "static (string|int)[] getArray()" 2) testMethodAnnotation with data set "static (callable() : string) getCallable()" ``` --- .../Reflector/ClassLikeDocblockParser.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php index 1d48be61220..2a6922627c2 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php @@ -322,14 +322,19 @@ public static function parse( $has_return = false; - if (!preg_match('/^([a-z_A-Z][a-z_0-9A-Z]+) *\(/', $method_entry, $matches)) { - $doc_line_parts = CommentAnalyzer::splitDocLine($method_entry); + $doc_line_parts = CommentAnalyzer::splitDocLine($method_entry); - if ($doc_line_parts[0] === 'static' && !strpos($doc_line_parts[1], '(')) { - $is_static = true; - array_shift($doc_line_parts); - } + if (count($doc_line_parts) > 2 + && $doc_line_parts[0] === 'static' + && !strpos($doc_line_parts[1], '(') + ) { + $is_static = true; + array_shift($doc_line_parts); + $method_entry = implode(' ', $doc_line_parts); + $doc_line_parts = CommentAnalyzer::splitDocLine($method_entry); + } + if (!preg_match('/^([a-z_A-Z][a-z_0-9A-Z]+) *\(/', $method_entry, $matches)) { if (count($doc_line_parts) > 1) { $docblock_lines[] = '@return ' . array_shift($doc_line_parts); $has_return = true; From 1c36da6ddacddd1719b3e453d0e152230276065b Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Tue, 6 Feb 2024 20:52:42 +0100 Subject: [PATCH 324/357] Strip callmap prefixes from parameter names Fixes vimeo/psalm#10662 --- .../Codebase/InternalCallMapHandler.php | 11 +++++++++- .../Codebase/InternalCallMapHandlerTest.php | 20 +++++++++++++++++++ tests/MethodSignatureTest.php | 18 +++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Codebase/InternalCallMapHandler.php b/src/Psalm/Internal/Codebase/InternalCallMapHandler.php index e5c96b624ed..308e6c9984e 100644 --- a/src/Psalm/Internal/Codebase/InternalCallMapHandler.php +++ b/src/Psalm/Internal/Codebase/InternalCallMapHandler.php @@ -285,11 +285,20 @@ public static function getCallablesFromCallMap(string $function_id): ?array $out_type = null; - if (strlen($arg_name) > 2 && $arg_name[0] === 'w' && $arg_name[1] === '_') { + if ($by_reference && strlen($arg_name) > 2 && $arg_name[0] === 'w' && $arg_name[1] === '_') { + // strip prefix that is not actually a part of the parameter name + $arg_name = substr($arg_name, 2); $out_type = $param_type; $param_type = Type::getMixed(); } + // removes `rw_` leftover from `&rw_haystack` or `&rw_needle` or `&rw_actual_name` + // it doesn't have any specific meaning apart from `&` signifying that + // the parameter is passed by reference (handled above) + if ($by_reference && strlen($arg_name) > 3 && strpos($arg_name, 'rw_') === 0) { + $arg_name = substr($arg_name, 3); + } + $function_param = new FunctionLikeParameter( $arg_name, $by_reference, diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index 3768c83ea37..e4dba7e3081 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -312,6 +312,26 @@ public function testGetcallmapReturnsAValidCallmap(): void } } + public function testGetCallablesFromCallmapRemovesRwPrefixFromParameterNames(): void + { + $entries = InternalCallMapHandler::getCallablesFromCallMap('collator_sort'); // has &rw_array parameter as second parameter + $this->assertNotNull($entries); + $collator_sort_entry = $entries[0]; + $this->assertIsArray($collator_sort_entry->params); + $this->assertArrayHasKey(1, $collator_sort_entry->params); + $this->assertEquals('array', $collator_sort_entry->params[1]->name); + } + + public function testGetCallablesFromCallmapRemovesWPrefixFromParameterNames(): void + { + $entries = InternalCallMapHandler::getCallablesFromCallMap('curl_multi_exec'); // has &w_still_running parameter as second parameter + $this->assertNotNull($entries); + $curl_multi_exec_entry = $entries[0]; + $this->assertIsArray($curl_multi_exec_entry->params); + $this->assertArrayHasKey(1, $curl_multi_exec_entry->params); + $this->assertEquals('still_running', $curl_multi_exec_entry->params[1]->name); + } + /** * @return iterable}> */ diff --git a/tests/MethodSignatureTest.php b/tests/MethodSignatureTest.php index e973c0f7e77..c47fd247020 100644 --- a/tests/MethodSignatureTest.php +++ b/tests/MethodSignatureTest.php @@ -957,6 +957,24 @@ public function &foo(): int { } ', ], + 'callmapInheritedMethodParamsDoNotHavePrefixes' => [ + 'code' => <<<'PHP' + Date: Tue, 6 Feb 2024 23:35:18 +0100 Subject: [PATCH 325/357] Fix #10667 --- dictionaries/CallMap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 191c7a6a54a..bc4a41a16dc 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -8529,7 +8529,7 @@ 'openssl_x509_parse' => ['array|false', 'certificate'=>'OpenSSLCertificate|string', 'short_names='=>'bool'], 'openssl_x509_read' => ['OpenSSLCertificate|false', 'certificate'=>'OpenSSLCertificate|string'], 'openssl_x509_verify' => ['int', 'certificate'=>'string|OpenSSLCertificate', 'public_key'=>'string|OpenSSLCertificate|OpenSSLAsymmetricKey|array'], -'ord' => ['int', 'character'=>'string'], +'ord' => ['int<0, 255>', 'character'=>'string'], 'OuterIterator::current' => ['mixed'], 'OuterIterator::getInnerIterator' => ['Iterator'], 'OuterIterator::key' => ['int|string'], From b42c2735560027c5cc85e2ff6e0e699d176c4866 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 6 Feb 2024 23:42:11 +0100 Subject: [PATCH 326/357] Revert partial mistakenly pushed fix This reverts commit 7d19f30961c7738db4882ebdebc319f27dec920f. --- dictionaries/CallMap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index bc4a41a16dc..191c7a6a54a 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -8529,7 +8529,7 @@ 'openssl_x509_parse' => ['array|false', 'certificate'=>'OpenSSLCertificate|string', 'short_names='=>'bool'], 'openssl_x509_read' => ['OpenSSLCertificate|false', 'certificate'=>'OpenSSLCertificate|string'], 'openssl_x509_verify' => ['int', 'certificate'=>'string|OpenSSLCertificate', 'public_key'=>'string|OpenSSLCertificate|OpenSSLAsymmetricKey|array'], -'ord' => ['int<0, 255>', 'character'=>'string'], +'ord' => ['int', 'character'=>'string'], 'OuterIterator::current' => ['mixed'], 'OuterIterator::getInnerIterator' => ['Iterator'], 'OuterIterator::key' => ['int|string'], From 07a64f68940a762679da72302d2a7a84c4092b78 Mon Sep 17 00:00:00 2001 From: Valentin Udaltsov Date: Tue, 8 Aug 2023 18:08:05 +0300 Subject: [PATCH 327/357] Improved Reflection stubs ReflectionParameter::getPosition() is non-negative-int. ReflectionMethod name property is always non-empty-string. ReflectionAttribute::getTarget() returns Attribute::TARGET_* instead of int-mask. Even for attributes of promoted properties (https://3v4l.org/rpM1t). *::getAttributes() always returns a list (https://3v4l.org/LYAMb). --- dictionaries/CallMap.php | 2 +- stubs/Php80.phpstub | 2 +- stubs/Reflection.phpstub | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 191c7a6a54a..37adcdea906 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -10631,7 +10631,7 @@ 'ReflectionParameter::getDefaultValue' => ['mixed'], 'ReflectionParameter::getDefaultValueConstantName' => ['?string'], 'ReflectionParameter::getName' => ['string'], -'ReflectionParameter::getPosition' => ['int'], +'ReflectionParameter::getPosition' => ['int<0, max>'], 'ReflectionParameter::getType' => ['?ReflectionType'], 'ReflectionParameter::hasType' => ['bool'], 'ReflectionParameter::isArray' => ['bool'], diff --git a/stubs/Php80.phpstub b/stubs/Php80.phpstub index 2f2d5bf0b9b..0e1404b74ae 100644 --- a/stubs/Php80.phpstub +++ b/stubs/Php80.phpstub @@ -28,7 +28,7 @@ class ReflectionAttribute /** * @psalm-pure - * @return int-mask-of + * @return Attribute::TARGET_* */ public function getTarget() : int { diff --git a/stubs/Reflection.phpstub b/stubs/Reflection.phpstub index 3e86431e581..68bc4ffd094 100644 --- a/stubs/Reflection.phpstub +++ b/stubs/Reflection.phpstub @@ -416,7 +416,7 @@ abstract class ReflectionFunctionAbstract implements Reflector * @since 8.0 * @template TClass as object * @param class-string|null $name - * @return ($name is null ? array> : array>) + * @return ($name is null ? list> : list>) */ public function getAttributes(?string $name = null, int $flags = 0): array {} } @@ -463,7 +463,7 @@ class ReflectionProperty implements Reflector * @since 8.0 * @template TClass as object * @param class-string|null $name - * @return ($name is null ? array> : array>) + * @return ($name is null ? list> : list>) */ public function getAttributes(?string $name = null, int $flags = 0): array {} @@ -540,7 +540,7 @@ class ReflectionProperty implements Reflector class ReflectionMethod extends ReflectionFunctionAbstract { /** - * @var string + * @var non-empty-string * @readonly */ public $name; @@ -599,7 +599,7 @@ class ReflectionClassConstant implements Reflector * @since 8.0 * @template TClass as object * @param class-string|null $name - * @return ($name is null ? array> : array>) + * @return ($name is null ? list> : list>) */ public function getAttributes(?string $name = null, int $flags = 0): array {} @@ -635,7 +635,7 @@ class ReflectionParameter implements Reflector { * @since 8.0 * @template TClass as object * @param class-string|null $name - * @return ($name is null ? array> : array>) + * @return ($name is null ? list> : list>) */ public function getAttributes(?string $name = null, int $flags = 0): array {} From f9ff9d34f032af26ce9997a867c27ce3232b7684 Mon Sep 17 00:00:00 2001 From: Valentin Udaltsov Date: Wed, 7 Feb 2024 02:47:49 +0300 Subject: [PATCH 328/357] Added ReflectionParameter::getPosition to CallMap_historical.php --- dictionaries/CallMap_historical.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 7a18de3c4f2..3d20da638e2 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -5969,7 +5969,7 @@ 'ReflectionParameter::getDefaultValue' => ['mixed'], 'ReflectionParameter::getDefaultValueConstantName' => ['?string'], 'ReflectionParameter::getName' => ['string'], - 'ReflectionParameter::getPosition' => ['int'], + 'ReflectionParameter::getPosition' => ['int<0, max>'], 'ReflectionParameter::getType' => ['?ReflectionType'], 'ReflectionParameter::hasType' => ['bool'], 'ReflectionParameter::isArray' => ['bool'], From f9044cb4e43d7c7991ad3d8f872f62ea0533c6fe Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Wed, 7 Feb 2024 21:49:59 +0100 Subject: [PATCH 329/357] Narrow `ord()` return type to `int<0,255>` Fixes vimeo/psalm#10667 --- dictionaries/CallMap.php | 2 +- dictionaries/CallMap_historical.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 191c7a6a54a..4e0f65ea931 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -8529,7 +8529,7 @@ 'openssl_x509_parse' => ['array|false', 'certificate'=>'OpenSSLCertificate|string', 'short_names='=>'bool'], 'openssl_x509_read' => ['OpenSSLCertificate|false', 'certificate'=>'OpenSSLCertificate|string'], 'openssl_x509_verify' => ['int', 'certificate'=>'string|OpenSSLCertificate', 'public_key'=>'string|OpenSSLCertificate|OpenSSLAsymmetricKey|array'], -'ord' => ['int', 'character'=>'string'], +'ord' => ['int<0,255>', 'character'=>'string'], 'OuterIterator::current' => ['mixed'], 'OuterIterator::getInnerIterator' => ['Iterator'], 'OuterIterator::key' => ['int|string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 7a18de3c4f2..0d5e95c8226 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -13190,7 +13190,7 @@ 'openssl_x509_free' => ['void', 'certificate'=>'resource'], 'openssl_x509_parse' => ['array|false', 'certificate'=>'string|resource', 'short_names='=>'bool'], 'openssl_x509_read' => ['resource|false', 'certificate'=>'string|resource'], - 'ord' => ['int', 'character'=>'string'], + 'ord' => ['int<0,255>', 'character'=>'string'], 'output_add_rewrite_var' => ['bool', 'name'=>'string', 'value'=>'string'], 'output_cache_disable' => ['void'], 'output_cache_disable_compression' => ['void'], From 2d48916f8afcfd49ad742db68fd4ec3beaa49574 Mon Sep 17 00:00:00 2001 From: robchett Date: Wed, 7 Feb 2024 21:14:58 +0000 Subject: [PATCH 330/357] Fix is_object false positive when 'V as int|stdClass' --- .../Type/SimpleAssertionReconciler.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 66cfc2f9e96..63733876834 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -1605,14 +1605,6 @@ private static function reconcileObject( /** @var TNamedObject|TTemplateParam|TIterable|TObjectWithProperties|TCallableObject $assertion_type */ $object_types[] = $type->addIntersectionType($assertion_type); $redundant = false; - } elseif ($type->isObjectType()) { - if ($assertion_type_is_intersectable_type - && !self::areIntersectionTypesAllowed($codebase, $type) - ) { - $redundant = false; - } else { - $object_types[] = $type; - } } elseif ($type instanceof TCallable) { $callable_object = new TCallableObject($type->from_docblock, $type); $object_types[] = $callable_object; @@ -1624,7 +1616,7 @@ private static function reconcileObject( $object_types[] = $type; $redundant = false; } elseif ($type instanceof TTemplateParam) { - if ($type->as->hasObject() || $type->as->hasMixed()) { + if ($type->as->hasObjectType() || $type->as->hasMixed()) { /** * @psalm-suppress PossiblyInvalidArgument This looks wrong, psalm assumes that $assertion_type * can contain TNamedObject due to the reconciliation above @@ -1650,6 +1642,14 @@ private static function reconcileObject( } $redundant = false; + } elseif ($type->isObjectType()) { + if ($assertion_type_is_intersectable_type + && !self::areIntersectionTypesAllowed($codebase, $type) + ) { + $redundant = false; + } else { + $object_types[] = $type; + } } elseif ($type instanceof TIterable) { $params = $type->type_params; $params[0] = self::refineArrayKey($params[0]); From 7289f642d95ac9d4efb7715aa8d96135a56dd4b0 Mon Sep 17 00:00:00 2001 From: robchett Date: Wed, 7 Feb 2024 21:35:24 +0000 Subject: [PATCH 331/357] Fix other assertion errors with union templates --- .../Type/SimpleAssertionReconciler.php | 4 ++-- .../Type/SimpleNegatedAssertionReconciler.php | 24 ++++++++++++++++--- tests/Template/FunctionTemplateTest.php | 23 ++++++++++++++++++ 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 63733876834..c8bd3c1b21d 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -1435,7 +1435,7 @@ private static function reconcileScalar( if ($type instanceof Scalar) { $scalar_types[] = $type; } elseif ($type instanceof TTemplateParam) { - if ($type->as->hasScalar() || $type->as->hasMixed()) { + if ($type->as->hasScalarType() || $type->as->hasMixed()) { $type = $type->replaceAs(self::reconcileScalar( $assertion, $type->as, @@ -1526,7 +1526,7 @@ private static function reconcileNumeric( $numeric_types[] = new TInt(); $numeric_types[] = new TNumericString(); } elseif ($type instanceof TTemplateParam) { - if ($type->as->hasNumeric() || $type->as->hasMixed()) { + if ($type->as->hasScalarType() || $type->as->hasMixed()) { $type = $type->replaceAs(self::reconcileNumeric( $assertion, $type->as, diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index 0308d741900..81e4208d7df 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -476,11 +476,29 @@ private static function reconcileBool( foreach ($existing_var_type->getAtomicTypes() as $type) { if ($type instanceof TTemplateParam) { - if (!$type->as->hasBool()) { + if (!$is_equality && !$type->as->isMixed()) { + $template_did_fail = 0; + + $type = $type->replaceAs(self::reconcileBool( + $assertion, + $type->as, + null, + false, + null, + $suppressed_issues, + $template_did_fail, + $is_equality, + )); + + $redundant = false; + + if (!$template_did_fail) { + $non_bool_types[] = $type; + } + } else { + $redundant = false; $non_bool_types[] = $type; } - - $redundant = false; } elseif (!$type instanceof TBool || ($is_equality && get_class($type) === TBool::class) ) { diff --git a/tests/Template/FunctionTemplateTest.php b/tests/Template/FunctionTemplateTest.php index 01b58efb671..4441f0e109c 100644 --- a/tests/Template/FunctionTemplateTest.php +++ b/tests/Template/FunctionTemplateTest.php @@ -808,6 +808,29 @@ function bar($foo): void { if (!is_callable($foo)) {} }', ], + 'assertOnUnionTemplatedValue' => [ + 'code' => ' [ 'code' => ' Date: Thu, 8 Feb 2024 01:20:57 +0100 Subject: [PATCH 332/357] Don't show backtrace in `InvalidDocblock` issue message Fixes vimeo/psalm#8323 --- .../Reflector/FunctionLikeDocblockScanner.php | 2 +- tests/AssertAnnotationTest.php | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php index 828d894763e..270529ee306 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php @@ -624,7 +624,7 @@ private static function getAssertionParts( ); } catch (TypeParseTreeException $e) { $storage->docblock_issues[] = new InvalidDocblock( - 'Invalid @psalm-assert union type ' . $e, + 'Invalid @psalm-assert union type: ' . $e->getMessage(), new CodeLocation($file_scanner, $stmt, null, true), ); diff --git a/tests/AssertAnnotationTest.php b/tests/AssertAnnotationTest.php index 83945999d87..972e2e3d72f 100644 --- a/tests/AssertAnnotationTest.php +++ b/tests/AssertAnnotationTest.php @@ -90,6 +90,33 @@ function requiresString(string $_str): void {} $this->analyzeFile('somefile.php', new Context()); } + public function testAssertInvalidDocblockMessageDoesNotIncludeTrace(): void + { + $this->expectException(CodeException::class); + $this->expectExceptionMessageMatches( + '!^InvalidDocblock - ' . 'somefile\\.php:10:5 - Invalid @psalm-assert union type: Invalid type \'\\$expected\'$!', + ); + + $this->addFile( + 'somefile.php', + <<<'PHP' + analyzeFile('somefile.php', new Context()); + } + + public function providerValidCodeParse(): iterable { return [ From 2f0b85ff6501fab266ef28fbaa593285919e0b86 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Wed, 7 Feb 2024 22:38:01 +0100 Subject: [PATCH 333/357] Support parsing constants in shapes Fixes vimeo/psalm#10669 --- src/Psalm/Internal/Type/ParseTreeCreator.php | 9 --------- src/Psalm/Internal/Type/TypeParser.php | 6 +++++- tests/TypeParseTest.php | 5 +++++ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Psalm/Internal/Type/ParseTreeCreator.php b/src/Psalm/Internal/Type/ParseTreeCreator.php index b9b7368d34e..1b652f09758 100644 --- a/src/Psalm/Internal/Type/ParseTreeCreator.php +++ b/src/Psalm/Internal/Type/ParseTreeCreator.php @@ -869,15 +869,6 @@ private function handleValue(array $type_token): void case '::': $nexter_token = $this->t + 2 < $this->type_token_count ? $this->type_tokens[$this->t + 2] : null; - if ($this->current_leaf instanceof ParseTree\KeyedArrayTree - && $nexter_token - && strtolower($nexter_token[0]) !== 'class' - ) { - throw new TypeParseTreeException( - ':: in array key is only allowed for ::class', - ); - } - if (!$nexter_token || (!preg_match('/^([a-zA-Z_][a-zA-Z_0-9]*\*?|\*)$/', $nexter_token[0]) && strtolower($nexter_token[0]) !== 'class') diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index ecdd5910c86..fd807f584b1 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -1468,8 +1468,12 @@ private static function getTypeFromKeyedArrayTree( if ($const_name === 'class') { $property_key = $fq_classlike_name; $class_string = true; - } else { + } elseif ($property_branch->value[0] === '"' || $property_branch->value[0] === "'") { $property_key = $property_branch->value; + } else { + throw new TypeParseTreeException( + ':: in array key is only allowed for ::class', + ); } } else { $property_key = $property_branch->value; diff --git a/tests/TypeParseTest.php b/tests/TypeParseTest.php index b65e63d2410..2156257e2e5 100644 --- a/tests/TypeParseTest.php +++ b/tests/TypeParseTest.php @@ -356,6 +356,11 @@ public function testTKeyedArrayWithQuotedKeys(): void $this->assertSame('array{\'\\"\': int, \'\\\'\': string}', (string) Type::parseString('array{"\\"": int, "\\\'": string}')); } + public function testTKeyedArrayWithClassConstantValueType(): void + { + $this->assertSame('list{A::X|A::Y, B::X}', (string) Type::parseString('list{A::X|A::Y, B::X}')); + } + public function testTKeyedArrayWithClassConstantKey(): void { $this->expectException(TypeParseTreeException::class); From 5579fd469b89c97063bb3c7062b5e9b776a3d72a Mon Sep 17 00:00:00 2001 From: robchett Date: Wed, 7 Feb 2024 20:37:00 +0000 Subject: [PATCH 334/357] Prevent mixed|null when function param is mixed with a null default value --- .../Reflector/FunctionLikeNodeScanner.php | 4 +++- tests/ArgTest.php | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index f04a90072d0..f0a8f34cc63 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -825,7 +825,9 @@ private function getTranslatedFunctionParam( $this->codebase->analysis_php_version_id, ); - if ($is_nullable) { + if ($param_type->isMixed()) { + $is_nullable = false; + } elseif ($is_nullable) { $param_type = $param_type->getBuilder()->addType(new TNull)->freeze(); } else { $is_nullable = $param_type->isNullable(); diff --git a/tests/ArgTest.php b/tests/ArgTest.php index 5fefdbef086..36efb359362 100644 --- a/tests/ArgTest.php +++ b/tests/ArgTest.php @@ -355,6 +355,20 @@ function foo($a, ...$b) {} var_caller("foo");', ], + 'mixedNullable' => [ + 'code' => 'default;', + 'assertions' => [ + '$_v===' => 'mixed', + ], + 'ignored_issues' => [], + 'php_version' => '8.0', + ], ]; } From 583e48a406923e284395ecd0a392c040a280e745 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Fri, 9 Feb 2024 23:42:08 +0100 Subject: [PATCH 335/357] CS fix --- src/Psalm/Config.php | 5 +---- src/Psalm/Internal/Codebase/InternalCallMapHandler.php | 1 + src/Psalm/Issue/MissingOverrideAttribute.php | 2 ++ tests/OverrideTest.php | 2 ++ 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index f992f3c24d7..cc07a42a6bf 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -358,10 +358,7 @@ final class Config public bool $ensure_array_int_offsets_exist = false; - /** - * @var bool - */ - public $ensure_override_attribute = false; + public bool $ensure_override_attribute = false; /** * @var array diff --git a/src/Psalm/Internal/Codebase/InternalCallMapHandler.php b/src/Psalm/Internal/Codebase/InternalCallMapHandler.php index 655562fdec1..9418d6a3e88 100644 --- a/src/Psalm/Internal/Codebase/InternalCallMapHandler.php +++ b/src/Psalm/Internal/Codebase/InternalCallMapHandler.php @@ -27,6 +27,7 @@ use function str_ends_with; use function str_starts_with; use function strlen; +use function strpos; use function strtolower; use function substr; use function version_compare; diff --git a/src/Psalm/Issue/MissingOverrideAttribute.php b/src/Psalm/Issue/MissingOverrideAttribute.php index 0e146e19971..b770a18b627 100644 --- a/src/Psalm/Issue/MissingOverrideAttribute.php +++ b/src/Psalm/Issue/MissingOverrideAttribute.php @@ -1,5 +1,7 @@ Date: Fri, 9 Feb 2024 23:43:41 +0100 Subject: [PATCH 336/357] Fixed test - Psalm now strips leading slash --- tests/StubTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/StubTest.php b/tests/StubTest.php index d8f805da27b..fdd7dfbe8bc 100644 --- a/tests/StubTest.php +++ b/tests/StubTest.php @@ -1473,7 +1473,7 @@ public function testAutoloadDefinedRequirePath(): void echo custom_taint_source();', ); - $this->expectExceptionMessage('TaintedHtml - /src/somefile.php'); + $this->expectExceptionMessage('TaintedHtml - src/somefile.php'); $this->analyzeFile($file_path, new Context()); } } From 89b6a15202ee9c682b77ee49fef7b1c886708b59 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sat, 10 Feb 2024 00:14:29 +0100 Subject: [PATCH 337/357] Fix test on Windows --- tests/StubTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/StubTest.php b/tests/StubTest.php index fdd7dfbe8bc..fff4b559eff 100644 --- a/tests/StubTest.php +++ b/tests/StubTest.php @@ -1464,7 +1464,7 @@ public function testAutoloadDefinedRequirePath(): void $this->project_analyzer->trackTaintedInputs(); - $file_path = (string) getcwd() . '/src/somefile.php'; + $file_path = (string) getcwd() . DIRECTORY_SEPARATOR . 'src/somefile.php'; $this->addFile( $file_path, From 83f04c5df2fe6366f2e28595de8eefed99017514 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sat, 10 Feb 2024 00:18:57 +0100 Subject: [PATCH 338/357] Update baseline --- psalm-baseline.xml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index e0243a23360..d192f9eb061 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + tags['variablesfrom'][0]]]> @@ -66,7 +66,6 @@ - function_id]]> @@ -1101,7 +1100,6 @@ - @@ -1112,9 +1110,6 @@ error_baseline]]> - - - threads]]> @@ -1125,7 +1120,6 @@ - @@ -1137,7 +1131,6 @@ - @@ -1462,7 +1455,6 @@ - children[0]]]> children[1]]]> From 6b405937ab20d6250af693f456c90c5bb6de4486 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sat, 10 Feb 2024 01:28:19 +0100 Subject: [PATCH 339/357] Forbid constructors from returning any values Fixes vimeo/psalm#9713 --- psalm-baseline.xml | 3 ++- .../Internal/Analyzer/Statements/ReturnAnalyzer.php | 12 ++++++++++++ tests/ReturnTypeTest.php | 11 +++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 58202505f72..8d205b058f7 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + @@ -1057,6 +1057,7 @@ + calling_function_id]]> diff --git a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php index 4f0867f169d..49160c4bf8e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php @@ -257,6 +257,18 @@ public static function analyze( $statements_analyzer, null, ); + + [, $method_name] = explode('::', $cased_method_id); + if ($method_name === '__construct') { + IssueBuffer::maybeAdd( + new InvalidReturnStatement( + 'No return values are expected for ' . $cased_method_id, + new CodeLocation($source, $stmt->expr), + ), + $statements_analyzer->getSuppressedIssues(), + ); + return; + } } else { $declared_return_type = $storage->return_type; } diff --git a/tests/ReturnTypeTest.php b/tests/ReturnTypeTest.php index dff8fbc41be..64e79d442c3 100644 --- a/tests/ReturnTypeTest.php +++ b/tests/ReturnTypeTest.php @@ -1863,6 +1863,17 @@ function foo(bool $x): never 'ignored_issues' => [], 'php_version' => '8.1', ], + 'constructorsShouldReturnVoid' => [ + 'code' => <<<'PHP' + 'InvalidReturnStatement', + ], ]; } } From 80f443c8f8ac15aaa375465cfa1edc3fded440ab Mon Sep 17 00:00:00 2001 From: robchett Date: Sat, 10 Feb 2024 09:32:45 +0000 Subject: [PATCH 340/357] Suppress scanning output in CI --- src/Psalm/Internal/Cli/Psalm.php | 8 ++++---- src/Psalm/Progress/LongProgress.php | 12 +++++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Psalm/Internal/Cli/Psalm.php b/src/Psalm/Internal/Cli/Psalm.php index 39fa79a5c4a..4d30075f1aa 100644 --- a/src/Psalm/Internal/Cli/Psalm.php +++ b/src/Psalm/Internal/Cli/Psalm.php @@ -265,7 +265,7 @@ public static function run(array $argv): void $threads = self::detectThreads($options, $config, $in_ci); - $progress = self::initProgress($options, $config); + $progress = self::initProgress($options, $config, $in_ci); self::restart($options, $threads, $progress); @@ -583,7 +583,7 @@ private static function loadConfig( return $config; } - private static function initProgress(array $options, Config $config): Progress + private static function initProgress(array $options, Config $config, bool $in_ci): Progress { $debug = array_key_exists('debug', $options) || array_key_exists('debug-by-line', $options); @@ -598,9 +598,9 @@ private static function initProgress(array $options, Config $config): Progress } else { $show_errors = !$config->error_baseline || isset($options['ignore-baseline']); if (isset($options['long-progress'])) { - $progress = new LongProgress($show_errors, $show_info); + $progress = new LongProgress($show_errors, $show_info, $in_ci); } else { - $progress = new DefaultProgress($show_errors, $show_info); + $progress = new DefaultProgress($show_errors, $show_info, $in_ci); } } // output buffered warnings diff --git a/src/Psalm/Progress/LongProgress.php b/src/Psalm/Progress/LongProgress.php index dac1d228535..707f0f8f9ed 100644 --- a/src/Psalm/Progress/LongProgress.php +++ b/src/Psalm/Progress/LongProgress.php @@ -23,14 +23,17 @@ class LongProgress extends Progress protected bool $fixed_size = false; - public function __construct(protected bool $print_errors = true, protected bool $print_infos = true) - { + public function __construct( + protected bool $print_errors = true, + protected bool $print_infos = true, + protected bool $in_ci = false, + ) { } public function startScanningFiles(): void { $this->fixed_size = false; - $this->write("\n" . 'Scanning files...' . "\n\n"); + $this->write("\n" . 'Scanning files...' . ($this->in_ci ? '' : "\n\n")); } public function startAnalyzingFiles(): void @@ -70,6 +73,9 @@ public function taskDone(int $level): void ++$this->progress; if (!$this->fixed_size) { + if ($this->in_ci) { + return; + } if ($this->progress == 1 || $this->progress == $this->number_of_tasks || $this->progress % 10 == 0) { $this->write(sprintf( "\r%s / %s...", From def0489b98f78e395b62912723a9c93d9aac8c24 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 11 Feb 2024 01:47:08 +0100 Subject: [PATCH 341/357] Process `@psalm-this-out` on `__construct()` as well Fixes vimeo/psalm#9649 --- .../Expression/Call/NewAnalyzer.php | 48 ++++++++++++++++++- tests/ThisOutTest.php | 20 ++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php index 1dbf404200e..db767a30339 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php @@ -11,6 +11,7 @@ use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Internal\Analyzer\FunctionLikeAnalyzer; use Psalm\Internal\Analyzer\NamespaceAnalyzer; +use Psalm\Internal\Analyzer\Statements\Expression\Call\Method\MethodCallReturnTypeFetcher; use Psalm\Internal\Analyzer\Statements\Expression\Call\Method\MethodVisibilityAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer; use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; @@ -21,6 +22,7 @@ use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TemplateStandinTypeReplacer; +use Psalm\Internal\Type\TypeExpander; use Psalm\Issue\AbstractInstantiation; use Psalm\Issue\DeprecatedClass; use Psalm\Issue\ImpureMethodCall; @@ -58,6 +60,7 @@ use function array_map; use function array_values; +use function count; use function in_array; use function md5; use function preg_match; @@ -429,6 +432,8 @@ private static function analyzeNamedConstructor( $declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id); + $method_storage = null; + if ($declaring_method_id) { $method_storage = $codebase->methods->getStorage($declaring_method_id); @@ -500,6 +505,7 @@ private static function analyzeNamedConstructor( } $generic_param_types = null; + $self_out_candidate = null; if ($storage->template_types) { foreach ($storage->template_types as $template_name => $base_type) { @@ -537,9 +543,49 @@ private static function analyzeNamedConstructor( 'had_template' => true, ]); } + + if ($method_storage && $method_storage->self_out_type) { + $self_out_candidate = $method_storage->self_out_type; + + if ($template_result->lower_bounds) { + $self_out_candidate = TypeExpander::expandUnion( + $codebase, + $self_out_candidate, + $fq_class_name, + null, + $storage->parent_class, + true, + false, + false, + true, + ); + } + + $self_out_candidate = MethodCallReturnTypeFetcher::replaceTemplateTypes( + $self_out_candidate, + $template_result, + $method_id, + count($stmt->getArgs()), + $codebase, + ); + + $self_out_candidate = TypeExpander::expandUnion( + $codebase, + $self_out_candidate, + $fq_class_name, + $fq_class_name, + $storage->parent_class, + true, + false, + false, + true, + ); + $statements_analyzer->node_data->setType($stmt, $self_out_candidate); + } } - if ($generic_param_types) { + // XXX: what if we need both? + if ($generic_param_types && !$self_out_candidate) { $result_atomic_type = new TGenericObject( $fq_class_name, $generic_param_types, diff --git a/tests/ThisOutTest.php b/tests/ThisOutTest.php index a537996aeae..1deb8334285 100644 --- a/tests/ThisOutTest.php +++ b/tests/ThisOutTest.php @@ -84,6 +84,26 @@ public function getData(): array { return $this->data; } '$data3===' => 'list<2|3>', ], ], + 'provideDefaultTypeToTypeArguments' => [ + 'code' => <<<'PHP' + */ + public function __construct() {} + + /** + * @psalm-if-this-is self<'idle'> + * @psalm-this-out self<'running'> + */ + public function start(): void {} + } + $app = new App(); + PHP, + 'assertions' => [ + '$app===' => "App<'idle'>", + ], + ], ]; } } From d10e384338abc1513efb7ce70050f02b0bb0fa89 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 11 Feb 2024 02:24:27 +0100 Subject: [PATCH 342/357] Report first class callables generated for unknown static methods Fixes vimeo/psalm#10170 --- .../StaticMethod/AtomicStaticCallAnalyzer.php | 14 +++++++++++++- tests/MethodCallTest.php | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php index ad9fd724f4e..0b1b187b8d7 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -30,6 +30,7 @@ use Psalm\Issue\InvalidStringClass; use Psalm\Issue\MixedMethodCall; use Psalm\Issue\UndefinedClass; +use Psalm\Issue\UndefinedMethod; use Psalm\IssueBuffer; use Psalm\Node\Expr\VirtualArray; use Psalm\Node\Expr\VirtualArrayItem; @@ -490,6 +491,7 @@ private static function handleNamedCall( $method_name_lc, ); + if ($stmt->isFirstClassCallable()) { if ($found_method_and_class_storage) { [ $method_storage ] = $found_method_and_class_storage; @@ -516,7 +518,17 @@ private static function handleNamedCall( $codebase->methods->getStorage($declaring_method_id)->pure, )]); } else { - // FIXME: perhaps Psalm should complain about nonexisting method here, or throw a logic exception? + if (IssueBuffer::accepts( + new UndefinedMethod( + 'Method ' . $method_id . ' does not exist', + new CodeLocation($statements_analyzer, $stmt), + (string) $method_id, + ), + $statements_analyzer->getSuppressedIssues(), + )) { + return false; + } + $return_type_candidate = Type::getClosure(); } } diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 49eebbf7546..def613cc537 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -1805,6 +1805,22 @@ public function foo(callable $_a = "strlen"): void {} PHP, 'error_message' => 'TooManyArguments', ], + 'firstClassCallableWithUnknownStaticMethod' => [ + 'code' => <<<'PHP' + 'UndefinedMethod', + ], + 'firstClassCallableWithUnknownInstanceMethod' => [ + 'code' => <<<'PHP' + foo(...); + PHP, + 'error_message' => 'UndefinedMethod', + ], ]; } } From a8c093aea29552d4f34f720efaaab6876b9120ee Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 11 Feb 2024 02:45:24 +0100 Subject: [PATCH 343/357] Handle taking references to unspecified magic methods --- .../Call/StaticMethod/AtomicStaticCallAnalyzer.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php index 0b1b187b8d7..8e187ab391f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -517,6 +517,19 @@ private static function handleNamedCall( $codebase->getMethodReturnType($method_id, $fq_class_name), $codebase->methods->getStorage($declaring_method_id)->pure, )]); + } elseif ($codebase->methodExists( + $call_static_method_id = new MethodIdentifier($method_id->fq_class_name, '__callstatic'), + new CodeLocation($statements_analyzer, $stmt), + null, + null, + false, + )) { + $return_type_candidate = new Union([new TClosure( + 'Closure', + null, + $codebase->getMethodReturnType($call_static_method_id, $fq_class_name), + $codebase->methods->getStorage($call_static_method_id)->pure, + )]); } else { if (IssueBuffer::accepts( new UndefinedMethod( From b3a44c5e85f718e0f7ad2fceab1998cf04e8a37c Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 11 Feb 2024 05:32:21 +0100 Subject: [PATCH 344/357] Suppress unused config suppression detection when running in partial or cache mode --- src/Psalm/Codebase.php | 3 + .../Internal/Analyzer/ProjectAnalyzer.php | 1 + src/Psalm/IssueBuffer.php | 75 +++++++++++-------- 3 files changed, 48 insertions(+), 31 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 0ce72174205..bc31ac99c01 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -181,6 +181,9 @@ final class Codebase public bool $diff_methods = false; + /** whether or not we only checked a part of the codebase */ + public bool $diff_run = false; + /** * @var array */ diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index 3187bd05e29..8b02ae9a255 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -502,6 +502,7 @@ public function check(string $base_dir, bool $is_diff = false): void $this->codebase->infer_types_from_usage = true; } else { + $this->codebase->diff_run = true; $this->progress->debug(count($diff_files) . ' changed files: ' . "\n"); $this->progress->debug(' ' . implode("\n ", $diff_files) . "\n"); diff --git a/src/Psalm/IssueBuffer.php b/src/Psalm/IssueBuffer.php index dd35480c48f..2e6690b9252 100644 --- a/src/Psalm/IssueBuffer.php +++ b/src/Psalm/IssueBuffer.php @@ -80,6 +80,7 @@ use function usort; use const DEBUG_BACKTRACE_IGNORE_ARGS; +use const PHP_EOL; use const PSALM_VERSION; use const STDERR; @@ -650,39 +651,42 @@ public static function finish( } if ($codebase->config->find_unused_issue_handler_suppression) { - foreach ($codebase->config->getIssueHandlers() as $type => $handler) { - foreach ($handler->getFilters() as $filter) { - if ($filter->suppressions > 0 && $filter->getErrorLevel() == Config::REPORT_SUPPRESS) { - continue; - } - $issues_data['config'][] = new IssueData( - IssueData::SEVERITY_ERROR, - 0, - 0, - UnusedIssueHandlerSuppression::getIssueType(), - sprintf( - 'Suppressed issue type "%s" for %s was not thrown.', - $type, - str_replace( - $codebase->config->base_dir, - '', - implode(', ', [...$filter->getFiles(), ...$filter->getDirectories()]), + if ($is_full && !$codebase->diff_run) { + foreach ($codebase->config->getIssueHandlers() as $type => $handler) { + foreach ($handler->getFilters() as $filter) { + if ($filter->suppressions > 0 && $filter->getErrorLevel() == Config::REPORT_SUPPRESS) { + continue; + } + $issues_data['config'][] = new IssueData( + IssueData::SEVERITY_ERROR, + 0, + 0, + UnusedIssueHandlerSuppression::getIssueType(), + sprintf( + 'Suppressed issue type "%s" for %s was not thrown.', + $type, + str_replace( + $codebase->config->base_dir, + '', + implode(', ', [...$filter->getFiles(), ...$filter->getDirectories()]), + ), ), - ), - $codebase->config->source_filename ?? '', - '', - '', - '', - 0, - 0, - 0, - 0, - 0, - 0, - UnusedIssueHandlerSuppression::SHORTCODE, - UnusedIssueHandlerSuppression::ERROR_LEVEL, - ); + $codebase->config->source_filename ?? '', + '', + '', + '', + 0, + 0, + 0, + 0, + 0, + 0, + UnusedIssueHandlerSuppression::SHORTCODE, + UnusedIssueHandlerSuppression::ERROR_LEVEL, + ); + } } + } else { } } @@ -827,6 +831,15 @@ public static function finish( echo "\n"; } } + + if ($codebase->config->find_unused_issue_handler_suppression && (!$is_full || $codebase->diff_run)) { + fwrite( + STDERR, + PHP_EOL . 'To whom it may concern: Psalm cannot detect unused issue handler suppressions when' + . PHP_EOL . 'analyzing individual files and folders or running in diff mode. Run on the full' + . PHP_EOL . 'project with diff mode off to enable unused issue handler detection.' . PHP_EOL, + ); + } } if ($is_full && $start_time) { From 7995ad989eecbf63da9406d5893edd1941fb61a3 Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 11 Feb 2024 05:47:56 +0100 Subject: [PATCH 345/357] Fix composer warnings These two classes weren't autoloadable because their FQCNs didn't match the file paths. --- .../Stmt/{VirtualDeclareDeclare.php => VirtualDeclareItem.php} | 0 src/Psalm/Node/VirtualUseItem.php | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/Psalm/Node/Stmt/{VirtualDeclareDeclare.php => VirtualDeclareItem.php} (100%) diff --git a/src/Psalm/Node/Stmt/VirtualDeclareDeclare.php b/src/Psalm/Node/Stmt/VirtualDeclareItem.php similarity index 100% rename from src/Psalm/Node/Stmt/VirtualDeclareDeclare.php rename to src/Psalm/Node/Stmt/VirtualDeclareItem.php diff --git a/src/Psalm/Node/VirtualUseItem.php b/src/Psalm/Node/VirtualUseItem.php index 979ec097885..f3247d07534 100644 --- a/src/Psalm/Node/VirtualUseItem.php +++ b/src/Psalm/Node/VirtualUseItem.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Psalm\Node\Stmt; +namespace Psalm\Node; use PhpParser\Node\UseItem; use Psalm\Node\VirtualNode; From 177576c8036b220a6e1a727a27939fa1577829ab Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Sun, 11 Feb 2024 21:25:00 +0100 Subject: [PATCH 346/357] Validate that all classes are autoloadable --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05602bf565c..20e23d909de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,12 @@ jobs: env: COMPOSER_ROOT_VERSION: dev-master + - name: Check all classes are autoloadable + run: | + composer dump-autoload -o --strict-psr + env: + COMPOSER_ROOT_VERSION: dev-master + - name: Cache composer cache uses: actions/cache@v4 with: From da6980382309f4a41756ef1e36468b46135ae723 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 12 Feb 2024 10:45:05 +0100 Subject: [PATCH 347/357] Report invalid number of arguments for psalm-taint-* --- .../PhpVisitor/Reflector/FunctionLikeDocblockParser.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php index e2a2cc5a14b..923313ec34b 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php @@ -238,6 +238,8 @@ public static function parse( if (count($param_parts) >= 2) { $info->taint_sink_params[] = ['name' => $param_parts[1], 'taint' => $param_parts[0]]; + } else { + throw new IncorrectDocblockException('@psalm-taint-sink expects 2 arguments'); } } } @@ -279,6 +281,8 @@ public static function parse( if ($param_parts[0]) { $info->taint_source_types[] = $param_parts[0]; + } else { + throw new IncorrectDocblockException('@psalm-taint-source expects 1 argument'); } } } elseif (isset($parsed_docblock->tags['return-taint'])) { From c3526b44639d2b7561dca010da1e6f4d456658dc Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 12 Feb 2024 14:04:50 +0100 Subject: [PATCH 348/357] fix test --- tests/Template/ConditionalReturnTypeTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Template/ConditionalReturnTypeTest.php b/tests/Template/ConditionalReturnTypeTest.php index 18669470af7..0dd32eac1cf 100644 --- a/tests/Template/ConditionalReturnTypeTest.php +++ b/tests/Template/ConditionalReturnTypeTest.php @@ -759,7 +759,7 @@ private function getBody() : string { * @template TSource as self::SOURCE_* * @param TSource $source * @return (TSource is "BODY" ? object|list : array) - * @psalm-taint-source + * @psalm-taint-source html */ public function getParams( string $source = self::SOURCE_GET From 95c70d9d3f985d2b9d781befdd63e10debff04cc Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Tue, 13 Feb 2024 00:08:07 +0100 Subject: [PATCH 349/357] Added default baseline file --- composer.json | 2 +- .../running_psalm/dealing_with_code_issues.md | 8 ++++- src/Psalm/Config.php | 1 + src/Psalm/Internal/Cli/Psalm.php | 32 +++++++++++-------- tests/EndToEnd/PsalmEndToEndTest.php | 22 +++++++++++-- 5 files changed, 47 insertions(+), 18 deletions(-) diff --git a/composer.json b/composer.json index 12e8fc8a143..6384250bc36 100644 --- a/composer.json +++ b/composer.json @@ -116,7 +116,7 @@ ], "verify-callmap": "@php phpunit tests/Internal/Codebase/InternalCallMapHandlerTest.php", "psalm": "@php ./psalm", - "psalm-set-baseline": "@php ./psalm --set-baseline=psalm-baseline.xml", + "psalm-set-baseline": "@php ./psalm --set-baseline", "tests": [ "@lint", "@cs", diff --git a/docs/running_psalm/dealing_with_code_issues.md b/docs/running_psalm/dealing_with_code_issues.md index 552dede75bc..84ac39765ee 100644 --- a/docs/running_psalm/dealing_with_code_issues.md +++ b/docs/running_psalm/dealing_with_code_issues.md @@ -95,11 +95,17 @@ If you wish to suppress all issues, you can use `@psalm-suppress all` instead of If you have a bunch of errors and you don't want to fix them all at once, Psalm can grandfather-in errors in existing code, while ensuring that new code doesn't have those same sorts of errors. +``` +vendor/bin/psalm --set-baseline +``` + +will generate a file psalm-baseline.xml containing the current errors. Alternateivly you can specify the name of your baseline file. + ``` vendor/bin/psalm --set-baseline=your-baseline.xml ``` -will generate a file containing the current errors. You should commit that generated file so that Psalm can use it when running in other places (e.g. CI). It won't complain about those errors either. +You should commit that generated file so that Psalm can use it when running in other places (e.g. CI). It won't complain about those errors either. You have two options to use the generated baseline when running psalm: diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index cc07a42a6bf..ee95eb1b00e 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -132,6 +132,7 @@ final class Config { private const DEFAULT_FILE_NAME = 'psalm.xml'; + final public const DEFAULT_BASELINE_NAME = 'psalm-baseline.xml'; final public const CONFIG_NAMESPACE = 'https://getpsalm.org/schema/config'; final public const REPORT_INFO = 'info'; final public const REPORT_ERROR = 'error'; diff --git a/src/Psalm/Internal/Cli/Psalm.php b/src/Psalm/Internal/Cli/Psalm.php index 4d30075f1aa..6a9a7bb5ed0 100644 --- a/src/Psalm/Internal/Cli/Psalm.php +++ b/src/Psalm/Internal/Cli/Psalm.php @@ -133,7 +133,7 @@ final class Psalm 'report:', 'report-show-info:', 'root:', - 'set-baseline:', + 'set-baseline::', 'show-info:', 'show-snippet:', 'stats', @@ -273,7 +273,6 @@ public static function run(array $argv): void $config->debug_emitted_issues = true; } - setlocale(LC_CTYPE, 'C'); if (isset($options['set-baseline'])) { @@ -641,7 +640,7 @@ private static function initProviders(array $options, Config $config, string $cu } /** - * @param array{"set-baseline": string, ...} $options + * @param array{"set-baseline": mixed, ...} $options * @return array}>> */ private static function generateBaseline( @@ -652,10 +651,13 @@ private static function generateBaseline( ): array { fwrite(STDERR, 'Writing error baseline to file...' . PHP_EOL); + $errorBaseline = ((string) $options['set-baseline']) + ?: $config->error_baseline ?: Config::DEFAULT_BASELINE_NAME; + try { $issue_baseline = ErrorBaseline::read( new FileProvider, - $options['set-baseline'], + $errorBaseline, ); } catch (ConfigException) { $issue_baseline = []; @@ -663,18 +665,20 @@ private static function generateBaseline( ErrorBaseline::create( new FileProvider, - $options['set-baseline'], + $errorBaseline, IssueBuffer::getIssuesData(), $config->include_php_versions_in_error_baseline || isset($options['include-php-versions']), ); - fwrite(STDERR, "Baseline saved to {$options['set-baseline']}."); + fwrite(STDERR, "Baseline saved to $errorBaseline."); - CliUtils::updateConfigFile( - $config, - $path_to_config ?? $current_dir, - $options['set-baseline'], - ); + if ($errorBaseline !== $config->error_baseline) { + CliUtils::updateConfigFile( + $config, + $path_to_config ?? $current_dir, + $errorBaseline, + ); + } fwrite(STDERR, PHP_EOL); @@ -1037,7 +1041,7 @@ private static function initBaseline( ): array { $issue_baseline = []; - if (isset($options['set-baseline']) && is_string($options['set-baseline'])) { + if (isset($options['set-baseline'])) { if ($paths_to_check !== null) { fwrite(STDERR, PHP_EOL . 'Cannot generate baseline when checking specific files' . PHP_EOL); exit(1); @@ -1285,11 +1289,13 @@ private static function getHelpText(): string Output the taint graph using the DOT language – requires --taint-analysis Issue baselines: - --set-baseline=PATH + --set-baseline[=PATH] Save all current error level issues to a file, to mark them as info in subsequent runs Add --include-php-versions to also include a list of PHP extension versions + Default value is `psalm-baseline.xml` + --use-baseline=PATH Allows you to use a baseline other than the default baseline provided in your config diff --git a/tests/EndToEnd/PsalmEndToEndTest.php b/tests/EndToEnd/PsalmEndToEndTest.php index fcb66e2b711..1e1f8124ab6 100644 --- a/tests/EndToEnd/PsalmEndToEndTest.php +++ b/tests/EndToEnd/PsalmEndToEndTest.php @@ -194,6 +194,22 @@ public function testTainting(): void $this->assertSame(2, $result['CODE']); } + public function testPsalmSetBaseline(): void + { + $this->runPsalmInit(1); + $this->runPsalm(['--set-baseline'], self::$tmpDir, true); + + $this->assertSame(0, $this->runPsalm([], self::$tmpDir)['CODE']); + } + + public function testPsalmSetBaselineWithArgument(): void + { + $this->runPsalmInit(1); + $this->runPsalm(['--set-baseline=psalm-custom-baseline.xml'], self::$tmpDir, true); + + $this->assertSame(0, $this->runPsalm([], self::$tmpDir)['CODE']); + } + public function testTaintingWithoutInit(): void { $result = $this->runPsalm(['--taint-analysis'], self::$tmpDir, true, false); @@ -210,7 +226,7 @@ public function testTaintGraphDumping(): void $result = $this->runPsalm( [ '--taint-analysis', - '--dump-taint-graph='.self::$tmpDir.'/taints.dot', + '--dump-taint-graph=' . self::$tmpDir . '/taints.dot', ], self::$tmpDir, true, @@ -219,7 +235,7 @@ public function testTaintGraphDumping(): void $this->assertSame(2, $result['CODE']); $this->assertFileEquals( __DIR__ . '/../fixtures/expected_taint_graph.dot', - self::$tmpDir.'/taints.dot', + self::$tmpDir . '/taints.dot', ); } @@ -263,7 +279,7 @@ private function runPsalmInit(?int $level = null, ?string $php_version = null): if ($level) { $args[] = 'src'; - $args[] = (string) $level; + $args[] = (string)$level; } $ret = $this->runPsalm($args, self::$tmpDir, false, false); From 955d3ee62a879a6bd3992cf1699ab8664d090923 Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Tue, 13 Feb 2024 00:11:18 +0100 Subject: [PATCH 350/357] Improve documentation --- docs/running_psalm/dealing_with_code_issues.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/running_psalm/dealing_with_code_issues.md b/docs/running_psalm/dealing_with_code_issues.md index 84ac39765ee..e995cc659a7 100644 --- a/docs/running_psalm/dealing_with_code_issues.md +++ b/docs/running_psalm/dealing_with_code_issues.md @@ -99,7 +99,7 @@ If you have a bunch of errors and you don't want to fix them all at once, Psalm vendor/bin/psalm --set-baseline ``` -will generate a file psalm-baseline.xml containing the current errors. Alternateivly you can specify the name of your baseline file. +will generate a file `psalm-baseline.xml` containing the current errors. Alternatively, you can specify the name of your baseline file. ``` vendor/bin/psalm --set-baseline=your-baseline.xml From f1d062295fb86f8ec47e0de1cda5f194bdecabbd Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Tue, 13 Feb 2024 08:58:29 +0100 Subject: [PATCH 351/357] Fix psalm warning --- src/Psalm/Internal/Cli/Psalm.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Cli/Psalm.php b/src/Psalm/Internal/Cli/Psalm.php index 6a9a7bb5ed0..d2aac75671f 100644 --- a/src/Psalm/Internal/Cli/Psalm.php +++ b/src/Psalm/Internal/Cli/Psalm.php @@ -651,8 +651,8 @@ private static function generateBaseline( ): array { fwrite(STDERR, 'Writing error baseline to file...' . PHP_EOL); - $errorBaseline = ((string) $options['set-baseline']) - ?: $config->error_baseline ?: Config::DEFAULT_BASELINE_NAME; + $errorBaseline = is_string($options['set-baseline']) ? $options['set-baseline'] : + ($config->error_baseline ?: Config::DEFAULT_BASELINE_NAME); try { $issue_baseline = ErrorBaseline::read( From f5fb9498de11c06e639d31d6fa7006f01dcdbac4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 13 Feb 2024 09:11:11 +0100 Subject: [PATCH 352/357] Use IssueBuffer::maybeAdd() instead of throwing --- .../Reflector/FunctionLikeDocblockParser.php | 14 ++++++++++++-- tests/Template/ConditionalReturnTypeTest.php | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php index 923313ec34b..7bedbf9e27f 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php @@ -239,7 +239,12 @@ public static function parse( if (count($param_parts) >= 2) { $info->taint_sink_params[] = ['name' => $param_parts[1], 'taint' => $param_parts[0]]; } else { - throw new IncorrectDocblockException('@psalm-taint-sink expects 2 arguments'); + IssueBuffer::maybeAdd( + new InvalidDocblock( + '@psalm-taint-sink expects 2 arguments', + $code_location, + ), + ); } } } @@ -282,7 +287,12 @@ public static function parse( if ($param_parts[0]) { $info->taint_source_types[] = $param_parts[0]; } else { - throw new IncorrectDocblockException('@psalm-taint-source expects 1 argument'); + IssueBuffer::maybeAdd( + new InvalidDocblock( + '@psalm-taint-source expects 1 argument', + $code_location, + ), + ); } } } elseif (isset($parsed_docblock->tags['return-taint'])) { diff --git a/tests/Template/ConditionalReturnTypeTest.php b/tests/Template/ConditionalReturnTypeTest.php index 0dd32eac1cf..cbdce780a2b 100644 --- a/tests/Template/ConditionalReturnTypeTest.php +++ b/tests/Template/ConditionalReturnTypeTest.php @@ -759,7 +759,7 @@ private function getBody() : string { * @template TSource as self::SOURCE_* * @param TSource $source * @return (TSource is "BODY" ? object|list : array) - * @psalm-taint-source html + * @psalm-taint-source input */ public function getParams( string $source = self::SOURCE_GET From a517523131716bb27e7ae5c1dd7aee281658f337 Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Tue, 13 Feb 2024 09:17:32 +0100 Subject: [PATCH 353/357] Use null coalescing operator --- src/Psalm/Internal/Cli/Psalm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Cli/Psalm.php b/src/Psalm/Internal/Cli/Psalm.php index d2aac75671f..037c547ee11 100644 --- a/src/Psalm/Internal/Cli/Psalm.php +++ b/src/Psalm/Internal/Cli/Psalm.php @@ -652,7 +652,7 @@ private static function generateBaseline( fwrite(STDERR, 'Writing error baseline to file...' . PHP_EOL); $errorBaseline = is_string($options['set-baseline']) ? $options['set-baseline'] : - ($config->error_baseline ?: Config::DEFAULT_BASELINE_NAME); + ($config->error_baseline ?? Config::DEFAULT_BASELINE_NAME); try { $issue_baseline = ErrorBaseline::read( From 69cd0420a9d4431b28f7dbef385ddd01a333ef48 Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Tue, 13 Feb 2024 18:37:54 +0100 Subject: [PATCH 354/357] Changed case of variable to snake_case --- src/Psalm/Internal/Cli/Psalm.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Psalm/Internal/Cli/Psalm.php b/src/Psalm/Internal/Cli/Psalm.php index 037c547ee11..d72caf79304 100644 --- a/src/Psalm/Internal/Cli/Psalm.php +++ b/src/Psalm/Internal/Cli/Psalm.php @@ -651,13 +651,13 @@ private static function generateBaseline( ): array { fwrite(STDERR, 'Writing error baseline to file...' . PHP_EOL); - $errorBaseline = is_string($options['set-baseline']) ? $options['set-baseline'] : + $error_baseline = is_string($options['set-baseline']) ? $options['set-baseline'] : ($config->error_baseline ?? Config::DEFAULT_BASELINE_NAME); try { $issue_baseline = ErrorBaseline::read( new FileProvider, - $errorBaseline, + $error_baseline, ); } catch (ConfigException) { $issue_baseline = []; @@ -665,18 +665,18 @@ private static function generateBaseline( ErrorBaseline::create( new FileProvider, - $errorBaseline, + $error_baseline, IssueBuffer::getIssuesData(), $config->include_php_versions_in_error_baseline || isset($options['include-php-versions']), ); - fwrite(STDERR, "Baseline saved to $errorBaseline."); + fwrite(STDERR, "Baseline saved to $error_baseline."); - if ($errorBaseline !== $config->error_baseline) { + if ($error_baseline !== $config->error_baseline) { CliUtils::updateConfigFile( $config, $path_to_config ?? $current_dir, - $errorBaseline, + $error_baseline, ); } From b238bc7dc9ebe52c7d1f2a21cbeeaaf7abe1728e Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 15 Feb 2024 11:51:41 +0100 Subject: [PATCH 355/357] Improve randomizer stubs --- stubs/extensions/random.phpstub | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/stubs/extensions/random.phpstub b/stubs/extensions/random.phpstub index 01cc8fb8028..27135afc376 100644 --- a/stubs/extensions/random.phpstub +++ b/stubs/extensions/random.phpstub @@ -87,10 +87,20 @@ namespace Random */ public function getBytes(int $length): string {} + /** + * @template TValue + * @param array $array + * @return list + */ public function shuffleArray(array $array): array {} public function shuffleBytes(string $bytes): string {} + /** + * @template TKey as array-key + * @param array $array + * @return list + */ public function pickArrayKeys(array $array, int $num): array {} public function __serialize(): array {} From 893b60ed0d123a19d600d5132b94adbce3957a37 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 15 Feb 2024 13:34:28 +0100 Subject: [PATCH 356/357] Improve parsing of psalm-type --- .../Reflector/ClassLikeNodeScanner.php | 6 ++--- tests/AnnotationTest.php | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 999f95df554..f43fc71f6ce 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -78,6 +78,7 @@ use function implode; use function is_int; use function is_string; +use function ltrim; use function preg_match; use function preg_replace; use function preg_split; @@ -1948,11 +1949,8 @@ private static function getTypeAliasesFromCommentLines( continue; } - if ($var_line_parts[0] === ' ') { - array_shift($var_line_parts); - } - $type_string = implode('', $var_line_parts); + $type_string = ltrim($type_string, "* \n\r"); try { $type_string = CommentAnalyzer::splitDocLine($type_string)[0]; } catch (DocblockParseException $e) { diff --git a/tests/AnnotationTest.php b/tests/AnnotationTest.php index 64ebf674b4e..4252e60b84e 100644 --- a/tests/AnnotationTest.php +++ b/tests/AnnotationTest.php @@ -513,6 +513,28 @@ function example(string $_x) : void {}', */ class A {}', ], + 'multipeLineGenericArray2' => [ + 'code' => ' + */ + class A { + /** @return TRelAlternate */ + public function ret(): array { return []; } + } + + $_ = (new A)->ret(); + ', + 'assertions' => [ + '$_===' => 'list', + ], + ], 'builtInClassInAShape' => [ 'code' => ' Date: Thu, 15 Feb 2024 23:23:40 +0100 Subject: [PATCH 357/357] Allow multiple spaces between type name and type definition --- .../Reflector/ClassLikeNodeScanner.php | 6 +++++- tests/TypeAnnotationTest.php | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index f43fc71f6ce..828012fa790 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -1933,7 +1933,7 @@ private static function getTypeAliasesFromCommentLines( continue; } - if ($var_line_parts[0] === ' ') { + while (isset($var_line_parts[0]) && $var_line_parts[0] === ' ') { array_shift($var_line_parts); } @@ -1949,6 +1949,10 @@ private static function getTypeAliasesFromCommentLines( continue; } + while (isset($var_line_parts[0]) && $var_line_parts[0] === ' ') { + array_shift($var_line_parts); + } + $type_string = implode('', $var_line_parts); $type_string = ltrim($type_string, "* \n\r"); try { diff --git a/tests/TypeAnnotationTest.php b/tests/TypeAnnotationTest.php index 0e101137c84..89cc4f999d2 100644 --- a/tests/TypeAnnotationTest.php +++ b/tests/TypeAnnotationTest.php @@ -916,6 +916,22 @@ public function bar(array $foo): void {} } PHP, ], + 'typeWithMultipleSpaces' => [ + 'code' => <<<'PHP' +