diff --git a/src/Psalm/Internal/Codebase/ClassConstantByWildcardResolver.php b/src/Psalm/Internal/Codebase/ClassConstantByWildcardResolver.php new file mode 100644 index 00000000000..85be05666dd --- /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/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 9f78143ed95..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; @@ -40,6 +41,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; @@ -70,7 +72,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 @@ -419,6 +421,15 @@ public static function reconcile( ); } + if (substr($assertion, 0, 15) === 'class-constant(') { + return self::reconcileClassConstant( + $codebase, + substr($assertion, 15, -1), + $existing_var_type, + $failed_reconciliation + ); + } + if ($existing_var_type->isSingle() && $existing_var_type->hasTemplate() && strpos($assertion, '-') === false @@ -486,7 +497,7 @@ private static function reconcileIsset( ); if (empty($existing_var_type->getAtomicTypes())) { - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getEmpty(); } } @@ -700,7 +711,7 @@ private static function reconcilePositiveNumeric( return new Type\Union($positive_types); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getEmpty(); } @@ -805,7 +816,7 @@ private static function reconcileHasMethod( return new Type\Union($object_types); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } @@ -897,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() @@ -995,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() @@ -1074,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() @@ -1149,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() @@ -1241,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() @@ -1334,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() @@ -1391,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() @@ -1462,7 +1473,7 @@ private static function reconcileCountable( return new Type\Union($iterable_types); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } @@ -1522,7 +1533,7 @@ private static function reconcileIterable( return new Type\Union($iterable_types); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } @@ -1566,7 +1577,7 @@ private static function reconcileInArray( ); } - $failed_reconciliation = 2; + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; return Type::getMixed(); } @@ -1763,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() @@ -1849,7 +1860,7 @@ private static function reconcileArray( ); if (!$did_remove_type) { - $failed_reconciliation = 1; + $failed_reconciliation = Reconciler::RECONCILIATION_REDUNDANT; } } } @@ -1858,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() @@ -1954,7 +1965,7 @@ private static function reconcileList( ); if (!$did_remove_type) { - $failed_reconciliation = 1; + $failed_reconciliation = Reconciler::RECONCILIATION_REDUNDANT; } } } @@ -1963,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() @@ -2030,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); } @@ -2090,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); } @@ -2200,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(); } @@ -2386,4 +2397,33 @@ 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, + Union $existing_type, + int &$failed_reconciliation + ) : Union { + if (strpos($class_constant_expression, '::') === false) { + return $existing_type; + } + + [$class_name, $constant_pattern] = explode('::', $class_constant_expression, 2); + + $resolver = new ClassConstantByWildcardResolver($codebase); + $matched_class_constant_types = $resolver->resolve($class_name, $constant_pattern); + if ($matched_class_constant_types === null) { + return $existing_type; + } + + if ($matched_class_constant_types === []) { + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; + return Type::getMixed(); + } + + return TypeCombiner::combine($matched_class_constant_types, $codebase); + } } 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..760b772008e 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 = self::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; } 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' => [ '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::assertNotNull($resolved); + self::assertCount(1, $resolved); + $type = $resolved[0]; + self::assertInstanceOf(TLiteralString::class, $type); + self::assertTrue($type->value === 'qoo'); + } +} diff --git a/tests/TypeReconciliation/ReconcilerTest.php b/tests/TypeReconciliation/ReconcilerTest.php index 5d31a933532..12cc75a203c 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,70 @@ 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 [ + 'constant-with-prefix' => [ + 'class-constant(ReconciliationTest\\Foo::PREFIX_*)', + '"bar"|"baz"', + ], + 'single-class-constant' => [ + 'class-constant(ReconciliationTest\\Foo::PREFIX_BAR)', + '"bar"', + ], + 'referencing-another-class-constant' => [ + 'class-constant(ReconciliationTest\\Foo::PREFIX_QOO)', + '"bar"', + ], + 'referencing-all-class-constants' => [ + 'class-constant(ReconciliationTest\\Foo::*)', + '"bar"|"baz"', + ], + 'referencing-some-class-constants-with-wildcard' => [ + 'class-constant(ReconciliationTest\\Foo::PREFIX_B*)', + '"bar"|"baz"', + ], + ]; + } }