From 49624169a593bd4c9abefb9205840c2bcb14c5bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Fri, 24 Sep 2021 01:47:57 +0200 Subject: [PATCH 01/13] bugfix: reconcile `class-constant` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- .../Type/SimpleAssertionReconciler.php | 56 ++++++++++++++++++ tests/TypeReconciliation/ReconcilerTest.php | 58 ++++++++++++++++++- 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 9f78143ed95..ffe93c472f9 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -45,9 +45,11 @@ use function assert; use function count; use function explode; +use function fnmatch; use function get_class; use function max; use function min; +use function reset; use function strpos; use function substr; @@ -419,6 +421,14 @@ public static function reconcile( ); } + if (substr($assertion, 0, 15) === 'class-constant(') { + return self::reconcileClassConstant( + $codebase, + substr($assertion, 15, -1), + $failed_reconciliation + ); + } + if ($existing_var_type->isSingle() && $existing_var_type->hasTemplate() && strpos($assertion, '-') === false @@ -2386,4 +2396,50 @@ private static function reconcileFalsyOrEmpty( assert($existing_var_type->getAtomicTypes() !== []); return $existing_var_type; } + + /** + * @param 0|1|2 $failed_reconciliation + */ + private static function reconcileClassConstant( + Codebase $codebase, + string $class_constant_expression, + int &$failed_reconciliation + ) : Union { + if (strpos($class_constant_expression, '::') === false) { + $failed_reconciliation = 2; + return Type::getMixed(); + } + + [$class_name, $constant_pattern] = explode('::', $class_constant_expression, 2); + + if (!$codebase->classlike_storage_provider->has($class_name)) { + $failed_reconciliation = 2; + return Type::getMixed(); + } + + $class_like_storage = $codebase->classlike_storage_provider->get($class_name); + $matched_class_constant_types = []; + + foreach ($class_like_storage->constants as $constant => $class_constant_storage) { + if (!fnmatch($constant_pattern, $constant)) { + continue; + } + + if (! $class_constant_storage->type || !$class_constant_storage->type->isSingle()) { + $matched_class_constant_types[] = new TMixed(); + continue; + } + + $types = $class_constant_storage->type->getAtomicTypes(); + $type = reset($types); + $matched_class_constant_types[] = $type; + } + + if ($matched_class_constant_types === []) { + $failed_reconciliation = 2; + return Type::getMixed(); + } + + return new Union($matched_class_constant_types); + } } diff --git a/tests/TypeReconciliation/ReconcilerTest.php b/tests/TypeReconciliation/ReconcilerTest.php index 5d31a933532..f89ee93f270 100644 --- a/tests/TypeReconciliation/ReconcilerTest.php +++ b/tests/TypeReconciliation/ReconcilerTest.php @@ -39,7 +39,6 @@ interface SomeInterface {} /** * @dataProvider providerTestReconcilation - * */ public function testReconcilation(string $expected_type, string $assertion, string $original_type): void { @@ -184,4 +183,61 @@ public function providerTestTypeIsContainedBy(): array ], ]; } + + /** + * @dataProvider constantAssertions + */ + public function testReconciliationOfClassConstantInAssertions(string $assertion, string $expected_type): void + { + $this->addFile( + 'psalm-assert.php', + ' + project_analyzer->getCodebase()->scanFiles(); + + $reconciled = \Psalm\Internal\Type\AssertionReconciler::reconcile( + $assertion, + new Type\Union([ + new Type\Atomic\TLiteralString(''), + ]), + null, + $this->statements_analyzer, + false, + [] + ); + + $this->assertSame( + $expected_type, + $reconciled->getId() + ); + } + + /** + * @return array + */ + public function constantAssertions(): array + { + return [ + 'single-constant' => [ + 'class-constant(Foo::BAR)', + '"bar"', + ], + 'referencing-constant' => [ + 'class-constant(Foo::QOO)', + '"bar"', + ], + 'wildcard-constant' => [ + 'Foo::*', + '"bar"|"baz"', + ], + ]; + } } From c6191643ab6bc537ce85fddd05414480901e6e6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sat, 25 Sep 2021 13:51:40 +0200 Subject: [PATCH 02/13] qa: add `Reconciler::RECONCILIATION_*` constants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- .../Internal/Type/AssertionReconciler.php | 9 ++-- .../Type/NegatedAssertionReconciler.php | 7 ++- .../Type/SimpleAssertionReconciler.php | 51 ++++++++++--------- .../Type/SimpleNegatedAssertionReconciler.php | 48 ++++++++--------- src/Psalm/Type/Reconciler.php | 8 ++- 5 files changed, 64 insertions(+), 59 deletions(-) diff --git a/src/Psalm/Internal/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index a4e05184b83..235a599953b 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -22,6 +22,7 @@ use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateParam; +use Psalm\Type\Reconciler; use Psalm\Type\Union; use function array_intersect_key; @@ -33,7 +34,7 @@ use function strpos; use function substr; -class AssertionReconciler extends \Psalm\Type\Reconciler +class AssertionReconciler extends Reconciler { /** * Reconciles types @@ -57,7 +58,7 @@ public static function reconcile( array $template_type_map, ?CodeLocation $code_location = null, array $suppressed_issues = [], - ?int &$failed_reconciliation = 0, + ?int &$failed_reconciliation = Reconciler::RECONCILIATION_OK, bool $negated = false ) : Union { $codebase = $statements_analyzer->getCodebase(); @@ -66,7 +67,7 @@ public static function reconcile( $is_loose_equality = false; $is_equality = false; $is_negation = false; - $failed_reconciliation = 0; + $failed_reconciliation = Reconciler::RECONCILIATION_OK; if ($assertion[0] === '!') { $assertion = substr($assertion, 1); @@ -581,7 +582,7 @@ private static function refine( } } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; } } diff --git a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php index 2e397e31abc..8a796147e2f 100644 --- a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php @@ -34,7 +34,6 @@ class NegatedAssertionReconciler extends Reconciler * @param array> $template_type_map * @param string[] $suppressed_issues * @param 0|1|2 $failed_reconciliation - * */ public static function reconcile( StatementsAnalyzer $statements_analyzer, @@ -91,7 +90,7 @@ public static function reconcile( if (!$existing_var_type->hasMixed() || $atomic instanceof Type\Atomic\TNonEmptyMixed ) { - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; if ($code_location) { if ($existing_var_type->from_static_property) { @@ -187,7 +186,7 @@ public static function reconcile( ); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; } } @@ -379,7 +378,7 @@ public static function reconcile( } } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return new Type\Union([new Type\Atomic\TEmptyMixed]); } diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index ffe93c472f9..f7a9806344f 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -40,6 +40,7 @@ use Psalm\Type\Atomic\TScalar; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateParam; +use Psalm\Type\Reconciler; use Psalm\Type\Union; use function assert; @@ -72,7 +73,7 @@ public static function reconcile( bool $negated = false, ?CodeLocation $code_location = null, array $suppressed_issues = [], - int &$failed_reconciliation = 0, + int &$failed_reconciliation = Reconciler::RECONCILIATION_OK, bool $is_equality = false, bool $is_strict_equality = false, bool $inside_loop = false @@ -496,7 +497,7 @@ private static function reconcileIsset( ); if (empty($existing_var_type->getAtomicTypes())) { - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getEmpty(); } } @@ -710,7 +711,7 @@ private static function reconcilePositiveNumeric( return new Type\Union($positive_types); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getEmpty(); } @@ -815,7 +816,7 @@ private static function reconcileHasMethod( return new Type\Union($object_types); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } @@ -907,7 +908,7 @@ private static function reconcileString( return new Type\Union($string_types); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return $existing_var_type->from_docblock ? Type::getMixed() @@ -1005,7 +1006,7 @@ private static function reconcileInt( return new Type\Union($int_types); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return $existing_var_type->from_docblock ? Type::getMixed() @@ -1084,7 +1085,7 @@ private static function reconcileBool( return new Type\Union($bool_types); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return $existing_var_type->from_docblock ? Type::getMixed() @@ -1159,7 +1160,7 @@ private static function reconcileScalar( return new Type\Union($scalar_types); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return $existing_var_type->from_docblock ? Type::getMixed() @@ -1251,7 +1252,7 @@ private static function reconcileNumeric( return new Type\Union($numeric_types); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return $existing_var_type->from_docblock ? Type::getMixed() @@ -1344,7 +1345,7 @@ private static function reconcileObject( return new Type\Union($object_types); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return $existing_var_type->from_docblock ? Type::getMixed() @@ -1401,7 +1402,7 @@ private static function reconcileResource( return new Type\Union($resource_types); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return $existing_var_type->from_docblock ? Type::getMixed() @@ -1472,7 +1473,7 @@ private static function reconcileCountable( return new Type\Union($iterable_types); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } @@ -1532,7 +1533,7 @@ private static function reconcileIterable( return new Type\Union($iterable_types); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } @@ -1576,7 +1577,7 @@ private static function reconcileInArray( ); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } @@ -1773,7 +1774,7 @@ private static function reconcileTraversable( return new Type\Union($traversable_types); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return $existing_var_type->from_docblock ? Type::getMixed() @@ -1859,7 +1860,7 @@ private static function reconcileArray( ); if (!$did_remove_type) { - $failed_reconciliation = 1; + $failed_reconciliation = Reconciler::RECONCILIATION_REDUNDANT; } } } @@ -1868,7 +1869,7 @@ private static function reconcileArray( return \Psalm\Internal\Type\TypeCombiner::combine($array_types); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return $existing_var_type->from_docblock ? Type::getMixed() @@ -1964,7 +1965,7 @@ private static function reconcileList( ); if (!$did_remove_type) { - $failed_reconciliation = 1; + $failed_reconciliation = Reconciler::RECONCILIATION_REDUNDANT; } } } @@ -1973,7 +1974,7 @@ private static function reconcileList( return \Psalm\Internal\Type\TypeCombiner::combine($array_types); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return $existing_var_type->from_docblock ? Type::getMixed() @@ -2040,7 +2041,7 @@ private static function reconcileStringArrayAccess( return new Type\Union($array_types); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed($inside_loop); } @@ -2100,7 +2101,7 @@ private static function reconcileIntArrayAccess( return \Psalm\Internal\Type\TypeCombiner::combine($array_types); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed($inside_loop); } @@ -2210,7 +2211,7 @@ private static function reconcileCallable( return \Psalm\Internal\Type\TypeCombiner::combine($callable_types); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } @@ -2406,14 +2407,14 @@ private static function reconcileClassConstant( int &$failed_reconciliation ) : Union { if (strpos($class_constant_expression, '::') === false) { - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } [$class_name, $constant_pattern] = explode('::', $class_constant_expression, 2); if (!$codebase->classlike_storage_provider->has($class_name)) { - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } @@ -2436,7 +2437,7 @@ private static function reconcileClassConstant( } if ($matched_class_constant_types === []) { - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index f2aac6afa65..8cbdc62ab02 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -56,7 +56,7 @@ public static function reconcile( bool $negated = false, ?CodeLocation $code_location = null, array $suppressed_issues = [], - int &$failed_reconciliation = 0, + int &$failed_reconciliation = Reconciler::RECONCILIATION_EMPTY, bool $is_equality = false, bool $is_strict_equality = false, bool $inside_loop = false @@ -283,7 +283,7 @@ private static function reconcileCallable( /** * @param string[] $suppressed_issues - * @param 0|1|2 $failed_reconciliation + * @param 0|1|2 $failed_reconciliation */ private static function reconcileBool( Type\Union $existing_var_type, @@ -336,7 +336,7 @@ private static function reconcileBool( } if (!$did_remove_type) { - $failed_reconciliation = 1; + $failed_reconciliation = Reconciler::RECONCILIATION_REDUNDANT; } } @@ -344,7 +344,7 @@ private static function reconcileBool( return new Type\Union($non_bool_types); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } @@ -475,7 +475,7 @@ private static function reconcileNull( } if (!$did_remove_type) { - $failed_reconciliation = 1; + $failed_reconciliation = Reconciler::RECONCILIATION_REDUNDANT; } } @@ -484,7 +484,7 @@ private static function reconcileNull( return $existing_var_type; } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } @@ -542,7 +542,7 @@ private static function reconcileFalse( } if (!$did_remove_type) { - $failed_reconciliation = 1; + $failed_reconciliation = Reconciler::RECONCILIATION_REDUNDANT; } } @@ -551,7 +551,7 @@ private static function reconcileFalse( return $existing_var_type; } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } @@ -825,7 +825,7 @@ private static function reconcileScalar( } if (!$did_remove_type) { - $failed_reconciliation = 1; + $failed_reconciliation = Reconciler::RECONCILIATION_REDUNDANT; } } @@ -837,7 +837,7 @@ private static function reconcileScalar( return $type; } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } @@ -926,7 +926,7 @@ private static function reconcileObject( } if (!$did_remove_type) { - $failed_reconciliation = 1; + $failed_reconciliation = Reconciler::RECONCILIATION_REDUNDANT; } } @@ -938,7 +938,7 @@ private static function reconcileObject( return $type; } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } @@ -1016,7 +1016,7 @@ private static function reconcileNumeric( } if (!$did_remove_type) { - $failed_reconciliation = 1; + $failed_reconciliation = Reconciler::RECONCILIATION_REDUNDANT; } } @@ -1028,7 +1028,7 @@ private static function reconcileNumeric( return $type; } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } @@ -1116,7 +1116,7 @@ private static function reconcileInt( } if (!$did_remove_type) { - $failed_reconciliation = 1; + $failed_reconciliation = Reconciler::RECONCILIATION_REDUNDANT; } } @@ -1128,7 +1128,7 @@ private static function reconcileInt( return $type; } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } @@ -1211,7 +1211,7 @@ private static function reconcileFloat( } if (!$did_remove_type) { - $failed_reconciliation = 1; + $failed_reconciliation = Reconciler::RECONCILIATION_REDUNDANT; } } @@ -1223,7 +1223,7 @@ private static function reconcileFloat( return $type; } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } @@ -1315,7 +1315,7 @@ private static function reconcileString( } if (!$did_remove_type) { - $failed_reconciliation = 1; + $failed_reconciliation = Reconciler::RECONCILIATION_REDUNDANT; } } @@ -1327,7 +1327,7 @@ private static function reconcileString( return $type; } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } @@ -1416,7 +1416,7 @@ private static function reconcileArray( } if (!$did_remove_type) { - $failed_reconciliation = 1; + $failed_reconciliation = Reconciler::RECONCILIATION_REDUNDANT; } } @@ -1428,7 +1428,7 @@ private static function reconcileArray( return $type; } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } @@ -1486,7 +1486,7 @@ private static function reconcileResource( } if (!$did_remove_type) { - $failed_reconciliation = 1; + $failed_reconciliation = Reconciler::RECONCILIATION_REDUNDANT; } } @@ -1495,7 +1495,7 @@ private static function reconcileResource( return $existing_var_type; } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 507d5eee996..f5ed70313d6 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -41,6 +41,10 @@ class Reconciler { + public const RECONCILIATION_OK = 0; + public const RECONCILIATION_REDUNDANT = 1; + public const RECONCILIATION_EMPTY = 2; + /** @var array> */ private static $broken_paths = []; @@ -154,7 +158,7 @@ public static function reconcileKeyedTypes( $before_adjustment = $result_type ? clone $result_type : null; - $failed_reconciliation = 0; + $failed_reconciliation = Reconciler::RECONCILIATION_OK; foreach ($new_type_parts as $offset => $new_type_part_parts) { $orred_type = null; @@ -280,7 +284,7 @@ public static function reconcileKeyedTypes( $changed_var_ids[$key] = true; } - if ($failed_reconciliation === 2) { + if ($failed_reconciliation === self::RECONCILIATION_EMPTY) { $result_type->failed_reconciliation = true; } From 1a543aecb97762a62f859f1f9331888b1478e14d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sat, 25 Sep 2021 18:56:44 +0200 Subject: [PATCH 03/13] qa: add namespace to test asset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- tests/TypeReconciliation/ReconcilerTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/TypeReconciliation/ReconcilerTest.php b/tests/TypeReconciliation/ReconcilerTest.php index f89ee93f270..04fc90f57b1 100644 --- a/tests/TypeReconciliation/ReconcilerTest.php +++ b/tests/TypeReconciliation/ReconcilerTest.php @@ -193,6 +193,7 @@ public function testReconciliationOfClassConstantInAssertions(string $assertion, 'psalm-assert.php', ' Date: Sun, 26 Sep 2021 13:07:30 +0200 Subject: [PATCH 04/13] refactor: expand test cases and optimize handling of invalid class-constant references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- .../Type/SimpleAssertionReconciler.php | 27 ++++++++++--------- tests/TypeReconciliation/ReconcilerTest.php | 26 +++++++++++------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index f7a9806344f..50db0595c6e 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -46,11 +46,12 @@ use function assert; use function count; use function explode; -use function fnmatch; use function get_class; use function max; use function min; -use function reset; +use function preg_match; +use function sprintf; +use function str_replace; use function strpos; use function substr; @@ -426,6 +427,7 @@ public static function reconcile( return self::reconcileClassConstant( $codebase, substr($assertion, 15, -1), + $existing_var_type, $failed_reconciliation ); } @@ -2404,36 +2406,35 @@ private static function reconcileFalsyOrEmpty( private static function reconcileClassConstant( Codebase $codebase, string $class_constant_expression, + Union $existing_type, int &$failed_reconciliation ) : Union { if (strpos($class_constant_expression, '::') === false) { - $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return Type::getMixed(); + return $existing_type; } [$class_name, $constant_pattern] = explode('::', $class_constant_expression, 2); if (!$codebase->classlike_storage_provider->has($class_name)) { - $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; - return Type::getMixed(); + return $existing_type; } + $constant_regex_pattern = sprintf('#^%s$#', str_replace('*', '.*', $constant_pattern)); + $class_like_storage = $codebase->classlike_storage_provider->get($class_name); $matched_class_constant_types = []; foreach ($class_like_storage->constants as $constant => $class_constant_storage) { - if (!fnmatch($constant_pattern, $constant)) { + if (preg_match($constant_regex_pattern, $constant) === 0) { continue; } - if (! $class_constant_storage->type || !$class_constant_storage->type->isSingle()) { - $matched_class_constant_types[] = new TMixed(); + if (! $class_constant_storage->type) { + $matched_class_constant_types[] = [new TMixed()]; continue; } - $types = $class_constant_storage->type->getAtomicTypes(); - $type = reset($types); - $matched_class_constant_types[] = $type; + $matched_class_constant_types[] = $class_constant_storage->type->getAtomicTypes(); } if ($matched_class_constant_types === []) { @@ -2441,6 +2442,6 @@ private static function reconcileClassConstant( return Type::getMixed(); } - return new Union($matched_class_constant_types); + return TypeCombiner::combine(array_values(array_merge(...$matched_class_constant_types)), $codebase); } } diff --git a/tests/TypeReconciliation/ReconcilerTest.php b/tests/TypeReconciliation/ReconcilerTest.php index 04fc90f57b1..12cc75a203c 100644 --- a/tests/TypeReconciliation/ReconcilerTest.php +++ b/tests/TypeReconciliation/ReconcilerTest.php @@ -196,9 +196,9 @@ public function testReconciliationOfClassConstantInAssertions(string $assertion, namespace ReconciliationTest; class Foo { - const BAR = \'bar\'; - const BAZ = \'baz\'; - const QOO = Foo::BAR; + const PREFIX_BAR = \'bar\'; + const PREFIX_BAZ = \'baz\'; + const PREFIX_QOO = Foo::PREFIX_BAR; } ' ); @@ -227,16 +227,24 @@ class Foo public function constantAssertions(): array { return [ - 'single-constant' => [ - 'class-constant(Foo::BAR)', + 'constant-with-prefix' => [ + 'class-constant(ReconciliationTest\\Foo::PREFIX_*)', + '"bar"|"baz"', + ], + 'single-class-constant' => [ + 'class-constant(ReconciliationTest\\Foo::PREFIX_BAR)', '"bar"', ], - 'referencing-constant' => [ - 'class-constant(Foo::QOO)', + 'referencing-another-class-constant' => [ + 'class-constant(ReconciliationTest\\Foo::PREFIX_QOO)', '"bar"', ], - 'wildcard-constant' => [ - 'Foo::*', + 'referencing-all-class-constants' => [ + 'class-constant(ReconciliationTest\\Foo::*)', + '"bar"|"baz"', + ], + 'referencing-some-class-constants-with-wildcard' => [ + 'class-constant(ReconciliationTest\\Foo::PREFIX_B*)', '"bar"|"baz"', ], ]; From a050ff2878387dbe4aa5f4dd66c82f6b83416630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Sun, 26 Sep 2021 16:23:38 +0200 Subject: [PATCH 05/13] bugfix: quote regular expression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/Psalm/Internal/Type/SimpleAssertionReconciler.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 50db0595c6e..2da74218737 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -50,6 +50,7 @@ use function max; use function min; use function preg_match; +use function preg_quote; use function sprintf; use function str_replace; use function strpos; @@ -2419,7 +2420,7 @@ private static function reconcileClassConstant( return $existing_type; } - $constant_regex_pattern = sprintf('#^%s$#', str_replace('*', '.*', $constant_pattern)); + $constant_regex_pattern = sprintf('#^%s$#', preg_quote(str_replace('*', '.*', $constant_pattern), '#')); $class_like_storage = $codebase->classlike_storage_provider->get($class_name); $matched_class_constant_types = []; From 743f570f664162a61862559859934ef3df00892f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 15 Nov 2021 18:06:24 +0100 Subject: [PATCH 06/13] bugfix: remove `preg_quote` as that will also quote wildcard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/Psalm/Internal/Type/SimpleAssertionReconciler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 2da74218737..4fffca2b5b7 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -2420,7 +2420,7 @@ private static function reconcileClassConstant( return $existing_type; } - $constant_regex_pattern = sprintf('#^%s$#', preg_quote(str_replace('*', '.*', $constant_pattern), '#')); + $constant_regex_pattern = sprintf('#^%s$#', str_replace('*', '.*', $constant_pattern)); $class_like_storage = $codebase->classlike_storage_provider->get($class_name); $matched_class_constant_types = []; From 68abcaab5cf0e6635a4802e9b8427f2d1be04b32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 15 Nov 2021 18:27:19 +0100 Subject: [PATCH 07/13] feature: extract class constant by wildcard detection into dedicated resolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- .../ClassConstantByWildcardResolver.php | 60 +++++++++++++++ .../Type/SimpleAssertionReconciler.php | 29 ++----- .../ClassConstantByWildcardResolverTest.php | 76 +++++++++++++++++++ 3 files changed, 141 insertions(+), 24 deletions(-) create mode 100644 src/Psalm/Internal/Codebase/ClassConstantByWildcardResolver.php create mode 100644 tests/Internal/Codebase/ClassConstantByWildcardResolverTest.php diff --git a/src/Psalm/Internal/Codebase/ClassConstantByWildcardResolver.php b/src/Psalm/Internal/Codebase/ClassConstantByWildcardResolver.php new file mode 100644 index 00000000000..083003125d7 --- /dev/null +++ b/src/Psalm/Internal/Codebase/ClassConstantByWildcardResolver.php @@ -0,0 +1,60 @@ +codebase = $codebase; + } + + /** + * @return list|null + */ + public function resolve(string $class_name, string $constant_pattern): ?array + { + if (!$this->codebase->classlike_storage_provider->has($class_name)) { + return null; + } + + $constant_regex_pattern = sprintf('#^%s$#', str_replace('*', '.*', $constant_pattern)); + + $class_like_storage = $this->codebase->classlike_storage_provider->get($class_name); + $matched_class_constant_types = []; + + foreach ($class_like_storage->constants as $constant => $class_constant_storage) { + if (preg_match($constant_regex_pattern, $constant) === 0) { + continue; + } + + if (! $class_constant_storage->type) { + $matched_class_constant_types[] = [new TMixed()]; + continue; + } + + $matched_class_constant_types[] = $class_constant_storage->type->getAtomicTypes(); + } + + return array_values(array_merge(...$matched_class_constant_types)); + } +} diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 4fffca2b5b7..7b944b58a1f 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -4,6 +4,7 @@ use Psalm\CodeLocation; use Psalm\Codebase; use Psalm\Exception\TypeParseTreeException; +use Psalm\Internal\Codebase\ClassConstantByWildcardResolver; use Psalm\Type; use Psalm\Type\Atomic; use Psalm\Type\Atomic\Scalar; @@ -49,10 +50,6 @@ use function get_class; use function max; use function min; -use function preg_match; -use function preg_quote; -use function sprintf; -use function str_replace; use function strpos; use function substr; @@ -2416,33 +2413,17 @@ private static function reconcileClassConstant( [$class_name, $constant_pattern] = explode('::', $class_constant_expression, 2); - if (!$codebase->classlike_storage_provider->has($class_name)) { + $resolver = new ClassConstantByWildcardResolver($codebase); + $matched_class_constant_types = $resolver->resolve($class_name, $constant_pattern); + if ($matched_class_constant_types === null) { return $existing_type; } - $constant_regex_pattern = sprintf('#^%s$#', str_replace('*', '.*', $constant_pattern)); - - $class_like_storage = $codebase->classlike_storage_provider->get($class_name); - $matched_class_constant_types = []; - - foreach ($class_like_storage->constants as $constant => $class_constant_storage) { - if (preg_match($constant_regex_pattern, $constant) === 0) { - continue; - } - - if (! $class_constant_storage->type) { - $matched_class_constant_types[] = [new TMixed()]; - continue; - } - - $matched_class_constant_types[] = $class_constant_storage->type->getAtomicTypes(); - } - if ($matched_class_constant_types === []) { $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } - return TypeCombiner::combine(array_values(array_merge(...$matched_class_constant_types)), $codebase); + return TypeCombiner::combine($matched_class_constant_types, $codebase); } } diff --git a/tests/Internal/Codebase/ClassConstantByWildcardResolverTest.php b/tests/Internal/Codebase/ClassConstantByWildcardResolverTest.php new file mode 100644 index 00000000000..caae40988e4 --- /dev/null +++ b/tests/Internal/Codebase/ClassConstantByWildcardResolverTest.php @@ -0,0 +1,76 @@ +resolver = new ClassConstantByWildcardResolver($this->project_analyzer->getCodebase()); + } + + public function testWillParseAllClassConstants(): void + { + $this->addFile( + 'psalm-assert.php', + ' + project_analyzer->getCodebase()->scanFiles(); + $resolved = $this->resolver->resolve('ReconciliationTest\\Foo', '*'); + self::assertNotEmpty($resolved); + foreach ($resolved as $type) { + self::assertInstanceOf(TLiteralString::class, $type); + self::assertTrue($type->value === 'bar' || $type->value === 'baz'); + } + } + + public function testWillParseMatchingClassConstants(): void + { + $this->addFile( + 'psalm-assert.php', + ' + project_analyzer->getCodebase()->scanFiles(); + $resolved = $this->resolver->resolve('ReconciliationTest\\Foo', 'BA*'); + self::assertNotEmpty($resolved); + foreach ($resolved as $type) { + self::assertInstanceOf(TLiteralString::class, $type); + self::assertTrue($type->value === 'bar' || $type->value === 'baz'); + } + + $resolved = $this->resolver->resolve('ReconciliationTest\\Foo', 'QOO'); + self::assertCount(1, $resolved); + $type = $resolved[0]; + self::assertInstanceOf(TLiteralString::class, $type); + self::assertTrue($type->value === 'qoo'); + } +} From a2bf87b1fcaaba524e64995f38d4ef23f93cb232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 15 Nov 2021 18:31:28 +0100 Subject: [PATCH 08/13] qa: ensure psalm is able to understand already verified types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- .../Internal/Codebase/ClassConstantByWildcardResolverTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Internal/Codebase/ClassConstantByWildcardResolverTest.php b/tests/Internal/Codebase/ClassConstantByWildcardResolverTest.php index caae40988e4..72a9eb1e58a 100644 --- a/tests/Internal/Codebase/ClassConstantByWildcardResolverTest.php +++ b/tests/Internal/Codebase/ClassConstantByWildcardResolverTest.php @@ -68,7 +68,10 @@ class Foo } $resolved = $this->resolver->resolve('ReconciliationTest\\Foo', 'QOO'); + self::assertNotNull($resolved); + /** @var list<\Psalm\Type\Atomic> */ self::assertCount(1, $resolved); + /** @var non-empty-list<\Psalm\Type\Atomic> $type */ $type = $resolved[0]; self::assertInstanceOf(TLiteralString::class, $type); self::assertTrue($type->value === 'qoo'); From 665cea7fbf86f8802ac520f521b6163bdd8d303e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 15 Nov 2021 18:36:04 +0100 Subject: [PATCH 09/13] bugfix: prevent psalm from yelling about docblock type contradiction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- .../Internal/Codebase/ClassConstantByWildcardResolverTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/Internal/Codebase/ClassConstantByWildcardResolverTest.php b/tests/Internal/Codebase/ClassConstantByWildcardResolverTest.php index 72a9eb1e58a..ff9306b6fae 100644 --- a/tests/Internal/Codebase/ClassConstantByWildcardResolverTest.php +++ b/tests/Internal/Codebase/ClassConstantByWildcardResolverTest.php @@ -73,6 +73,10 @@ class Foo self::assertCount(1, $resolved); /** @var non-empty-list<\Psalm\Type\Atomic> $type */ $type = $resolved[0]; + /** + * @psalm-suppress DocblockTypeContradiction TLiteralString has to be asserted here, + * ofc it does not match the docblock + */ self::assertInstanceOf(TLiteralString::class, $type); self::assertTrue($type->value === 'qoo'); } From 765bf93444801797ab2bbc40b0af69d698e3aeb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 15 Nov 2021 18:36:51 +0100 Subject: [PATCH 10/13] bugfix: apply coding standard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/Psalm/Type/Reconciler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index f5ed70313d6..760b772008e 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -158,7 +158,7 @@ public static function reconcileKeyedTypes( $before_adjustment = $result_type ? clone $result_type : null; - $failed_reconciliation = Reconciler::RECONCILIATION_OK; + $failed_reconciliation = self::RECONCILIATION_OK; foreach ($new_type_parts as $offset => $new_type_part_parts) { $orred_type = null; From dac82e95dcbc10856824d5b59de4248b73640a5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 15 Nov 2021 18:49:51 +0100 Subject: [PATCH 11/13] qa: re-enable skipped test for class constant assertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- tests/AssertAnnotationTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/AssertAnnotationTest.php b/tests/AssertAnnotationTest.php index 8ec4d62ec98..e2ebe95ce41 100644 --- a/tests/AssertAnnotationTest.php +++ b/tests/AssertAnnotationTest.php @@ -1241,7 +1241,7 @@ function test($a, $b): string { return substr($a, 0, 1) . substr($b, 0, 1); }' ], - 'SKIPPED-convertConstStringType' => [ + 'convertConstStringType' => [ ' Date: Mon, 15 Nov 2021 19:06:33 +0100 Subject: [PATCH 12/13] qa: remove useless `var` annotation and the psalm suppression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- .../Codebase/ClassConstantByWildcardResolverTest.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/Internal/Codebase/ClassConstantByWildcardResolverTest.php b/tests/Internal/Codebase/ClassConstantByWildcardResolverTest.php index ff9306b6fae..a60dd58333b 100644 --- a/tests/Internal/Codebase/ClassConstantByWildcardResolverTest.php +++ b/tests/Internal/Codebase/ClassConstantByWildcardResolverTest.php @@ -69,14 +69,8 @@ class Foo $resolved = $this->resolver->resolve('ReconciliationTest\\Foo', 'QOO'); self::assertNotNull($resolved); - /** @var list<\Psalm\Type\Atomic> */ self::assertCount(1, $resolved); - /** @var non-empty-list<\Psalm\Type\Atomic> $type */ $type = $resolved[0]; - /** - * @psalm-suppress DocblockTypeContradiction TLiteralString has to be asserted here, - * ofc it does not match the docblock - */ self::assertInstanceOf(TLiteralString::class, $type); self::assertTrue($type->value === 'qoo'); } From 6bf02657b6ccc47d01fa19f1ccc496480bd768c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20B=C3=B6sing?= <2189546+boesing@users.noreply.github.com> Date: Mon, 15 Nov 2021 21:10:20 +0100 Subject: [PATCH 13/13] qa: ensure `array_merge` has at least one argument MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com> --- src/Psalm/Internal/Codebase/ClassConstantByWildcardResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Codebase/ClassConstantByWildcardResolver.php b/src/Psalm/Internal/Codebase/ClassConstantByWildcardResolver.php index 083003125d7..85be05666dd 100644 --- a/src/Psalm/Internal/Codebase/ClassConstantByWildcardResolver.php +++ b/src/Psalm/Internal/Codebase/ClassConstantByWildcardResolver.php @@ -55,6 +55,6 @@ public function resolve(string $class_name, string $constant_pattern): ?array $matched_class_constant_types[] = $class_constant_storage->type->getAtomicTypes(); } - return array_values(array_merge(...$matched_class_constant_types)); + return array_values(array_merge([], ...$matched_class_constant_types)); } }