From d17757c422ab7ca9f5b245b622f44507b40b19fb Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Fri, 19 Jan 2024 10:01:32 +1300 Subject: [PATCH 01/63] 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 02/63] 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 03/63] 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:09:57 +0300 Subject: [PATCH 04/63] 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 05/63] 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 06/63] 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 07/63] 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 08/63] `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 09/63] 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 10/63] `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 11/63] 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 12/63] 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 13/63] 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 14/63] [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 98d98be4439d9b8b20757125025954df73f3b1ef Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Wed, 10 Jan 2024 21:57:56 +1300 Subject: [PATCH 15/63] 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 16/63] 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 17/63] 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 18/63] 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 19/63] 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 20/63] 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 21/63] 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 22/63] 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 23/63] 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 24/63] 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 6aa2ddfe1c9d4e29bbc3bd2d413cc5b55d17de10 Mon Sep 17 00:00:00 2001 From: fluffycondor <7ionmail@gmail.com> Date: Wed, 31 Jan 2024 15:39:54 +0600 Subject: [PATCH 25/63] 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 26/63] 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 27/63] 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 28/63] 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 29/63] 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 30/63] 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 31/63] 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 32/63] 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 33/63] 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 34/63] 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 35/63] 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 36/63] 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 37/63] 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 38/63] 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 39/63] 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 40/63] 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 41/63] 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 42/63] 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 43/63] 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 44/63] 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 45/63] 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 46/63] 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 47/63] 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 48/63] 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 49/63] 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 50/63] 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 51/63] 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 52/63] 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 53/63] 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 54/63] 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 55/63] 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 56/63] 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 57/63] 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 58/63] 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 ee7187c8d652c3c40ff4319a5a42f0aaa27c2a54 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sun, 4 Feb 2024 11:51:57 +0100 Subject: [PATCH 59/63] 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 60/63] 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 61/63] 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 62/63] 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 63/63] 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' => '