diff --git a/config.xsd b/config.xsd
index ad48f587e59..917968bd0f2 100644
--- a/config.xsd
+++ b/config.xsd
@@ -458,6 +458,7 @@
+
diff --git a/docs/running_psalm/issues/UnresolvableConstant.md b/docs/running_psalm/issues/UnresolvableConstant.md
new file mode 100644
index 00000000000..4a4ce88f63c
--- /dev/null
+++ b/docs/running_psalm/issues/UnresolvableConstant.md
@@ -0,0 +1,22 @@
+# UnresolvableConstant
+
+Emitted when Psalm cannot resolve a constant used in `key-of` or `value-of`. Note that `static::CONST` is considered
+unresolvable for `key-of` and `value-of`, since the literal keys and values can't be resolved due to the possibility
+of being overridden by child classes.
+
+```php
+
+ */
+ public function bar(): string
+ {
+ return self::BAR[0];
+ }
+}
+```
diff --git a/src/Psalm/Exception/UnresolvableConstantException.php b/src/Psalm/Exception/UnresolvableConstantException.php
new file mode 100644
index 00000000000..3295f663f99
--- /dev/null
+++ b/src/Psalm/Exception/UnresolvableConstantException.php
@@ -0,0 +1,23 @@
+class_name = $class_name;
+ $this->const_name = $const_name;
+ }
+}
diff --git a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php
index 5382c4e02a1..6f68fc02250 100644
--- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php
@@ -11,6 +11,7 @@
use Psalm\CodeLocation;
use Psalm\Config;
use Psalm\Context;
+use Psalm\Exception\UnresolvableConstantException;
use Psalm\Internal\Analyzer\ClassAnalyzer;
use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
use Psalm\Internal\Analyzer\InterfaceAnalyzer;
@@ -40,6 +41,7 @@
use Psalm\Issue\MixedInferredReturnType;
use Psalm\Issue\MixedReturnTypeCoercion;
use Psalm\Issue\MoreSpecificReturnType;
+use Psalm\Issue\UnresolvableConstant;
use Psalm\IssueBuffer;
use Psalm\StatementsSource;
use Psalm\Storage\FunctionLikeStorage;
@@ -821,15 +823,27 @@ public static function checkReturnType(
return null;
}
- $fleshed_out_return_type = TypeExpander::expandUnion(
- $codebase,
- $storage->return_type,
- $classlike_storage->name ?? null,
- $classlike_storage->name ?? null,
- $parent_class,
- true,
- true
- );
+ try {
+ $fleshed_out_return_type = TypeExpander::expandUnion(
+ $codebase,
+ $storage->return_type,
+ $classlike_storage->name ?? null,
+ $classlike_storage->name ?? null,
+ $parent_class,
+ true,
+ true
+ );
+ } catch (UnresolvableConstantException $e) {
+ IssueBuffer::maybeAdd(
+ new UnresolvableConstant(
+ "Could not resolve constant {$e->class_name}::{$e->const_name}",
+ $storage->return_type_location
+ ),
+ $storage->suppressed_issues,
+ true
+ );
+ $fleshed_out_return_type = $storage->return_type;
+ }
if ($fleshed_out_return_type->check(
$function_like_analyzer,
diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php
index 459db1133bd..551e7e8b73c 100644
--- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php
@@ -10,6 +10,7 @@
use Psalm\CodeLocation;
use Psalm\Codebase;
use Psalm\Context;
+use Psalm\Exception\UnresolvableConstantException;
use Psalm\FileManipulation;
use Psalm\Internal\Analyzer\FunctionLike\ReturnTypeAnalyzer;
use Psalm\Internal\Analyzer\FunctionLike\ReturnTypeCollector;
@@ -38,6 +39,7 @@
use Psalm\Issue\MissingThrowsDocblock;
use Psalm\Issue\ReferenceConstraintViolation;
use Psalm\Issue\ReservedWord;
+use Psalm\Issue\UnresolvableConstant;
use Psalm\Issue\UnusedClosureParam;
use Psalm\Issue\UnusedParam;
use Psalm\IssueBuffer;
@@ -1013,17 +1015,30 @@ private function processParams(
if ($function_param->type) {
$param_type = clone $function_param->type;
- $param_type = TypeExpander::expandUnion(
- $codebase,
- $param_type,
- $context->self,
- $context->self,
- $this->getParentFQCLN(),
- true,
- false,
- false,
- true
- );
+ try {
+ $param_type = TypeExpander::expandUnion(
+ $codebase,
+ $param_type,
+ $context->self,
+ $context->self,
+ $this->getParentFQCLN(),
+ true,
+ false,
+ false,
+ true
+ );
+ } catch (UnresolvableConstantException $e) {
+ if ($function_param->type_location !== null) {
+ IssueBuffer::maybeAdd(
+ new UnresolvableConstant(
+ "Could not resolve constant {$e->class_name}::{$e->const_name}",
+ $function_param->type_location
+ ),
+ $storage->suppressed_issues,
+ true
+ );
+ }
+ }
if ($function_param->type_location) {
if ($param_type->check(
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php
index dc6fa069753..b9dcdee3d66 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php
@@ -252,7 +252,9 @@ public static function analyze(
$fq_class_name,
$stmt->name->name,
$class_visibility,
- $statements_analyzer
+ $statements_analyzer,
+ [],
+ $stmt->class->parts[0] === "self"
);
} catch (InvalidArgumentException $_) {
return true;
@@ -378,7 +380,7 @@ public static function analyze(
);
}
- if ($first_part_lc !== 'static' || $const_class_storage->final) {
+ if ($first_part_lc !== 'static' || $const_class_storage->final || $class_constant_type->from_docblock) {
$stmt_type = clone $class_constant_type;
$statements_analyzer->node_data->setType($stmt, $stmt_type);
diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php
index aad785051c0..3d93f7a2d9a 100644
--- a/src/Psalm/Internal/Codebase/ClassLikes.php
+++ b/src/Psalm/Internal/Codebase/ClassLikes.php
@@ -1632,7 +1632,8 @@ public function getClassConstantType(
string $constant_name,
int $visibility,
?StatementsAnalyzer $statements_analyzer = null,
- array $visited_constant_ids = []
+ array $visited_constant_ids = [],
+ bool $static_binding = false,
): ?Union {
$class_name = strtolower($class_name);
@@ -1659,15 +1660,18 @@ public function getClassConstantType(
}
if ($constant_storage->unresolved_node) {
- $constant_storage->type = new Union([ConstantTypeResolver::resolve(
+ $constant_storage->inferred_type = new Union([ConstantTypeResolver::resolve(
$this,
$constant_storage->unresolved_node,
$statements_analyzer,
$visited_constant_ids
)]);
+ if ($constant_storage->type === null || !$constant_storage->type->from_docblock) {
+ $constant_storage->type = $constant_storage->inferred_type;
+ }
}
- return $constant_storage->type;
+ return $static_binding ? $constant_storage->inferred_type : $constant_storage->type;
} elseif (isset($storage->enum_cases[$constant_name])) {
return new Union([new TEnumCase($storage->name, $constant_name)]);
}
diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
index 977d7888e96..790f201ebb8 100644
--- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
+++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
@@ -1208,6 +1208,7 @@ private function visitClassConstDeclaration(
$existing_constants = $storage->constants;
$comment = $stmt->getDocComment();
+ $var_comment = null;
$deprecated = false;
$description = null;
$config = $this->config;
@@ -1220,19 +1221,31 @@ private function visitClassConstDeclaration(
}
$description = $comments->description;
+
+ try {
+ $var_comments = CommentAnalyzer::getTypeFromComment(
+ $comment,
+ $this->file_scanner,
+ $this->aliases,
+ [],
+ $this->type_aliases
+ );
+
+ $var_comment = array_pop($var_comments);
+ } catch (IncorrectDocblockException $e) {
+ $storage->docblock_issues[] = new MissingDocblockType(
+ $e->getMessage(),
+ new CodeLocation($this->file_scanner, $stmt, null, true)
+ );
+ } catch (DocblockParseException $e) {
+ $storage->docblock_issues[] = new InvalidDocblock(
+ $e->getMessage(),
+ new CodeLocation($this->file_scanner, $stmt, null, true)
+ );
+ }
}
foreach ($stmt->consts as $const) {
- $const_type = SimpleTypeInferer::infer(
- $this->codebase,
- new NodeDataProvider(),
- $const->value,
- $this->aliases,
- null,
- $existing_constants,
- $fq_classlike_name
- );
-
if (isset($storage->constants[$const->name->name])
|| isset($storage->enum_cases[$const->name->name])
) {
@@ -1246,6 +1259,35 @@ private function visitClassConstDeclaration(
continue;
}
+ $inferred_type = SimpleTypeInferer::infer(
+ $this->codebase,
+ new NodeDataProvider(),
+ $const->value,
+ $this->aliases,
+ null,
+ $existing_constants,
+ $fq_classlike_name
+ );
+
+ $type_location = null;
+ if ($var_comment !== null && $var_comment->type !== null) {
+ $const_type = $var_comment->type;
+
+ if ($var_comment->type_start !== null
+ && $var_comment->type_end !== null
+ && $var_comment->line_number !== null
+ ) {
+ $type_location = new DocblockTypeLocation(
+ $this->file_scanner,
+ $var_comment->type_start,
+ $var_comment->type_end,
+ $var_comment->line_number
+ );
+ }
+ } else {
+ $const_type = $inferred_type;
+ }
+
$storage->constants[$const->name->name] = $constant_storage = new ClassConstantStorage(
$const_type,
$stmt->isProtected()
@@ -1259,30 +1301,22 @@ private function visitClassConstDeclaration(
)
);
+ $constant_storage->inferred_type = $inferred_type;
+ $constant_storage->type_location = $type_location;
+
$constant_storage->stmt_location = new CodeLocation(
$this->file_scanner,
$const
);
- if ($const_type
- && $const->value instanceof Concat
- && $const_type->isSingle()
- && get_class($const_type->getSingleAtomic()) === TString::class
+ if ($inferred_type
+ && !(
+ $const->value instanceof Concat
+ && $inferred_type->isSingle()
+ && get_class($inferred_type->getSingleAtomic()) === TString::class
+ )
) {
- // Prefer unresolved type over inferred string from concat, so that it can later be resolved to literal.
- $const_type = null;
- }
-
- if ($const_type) {
- $existing_constants[$const->name->name] = new ClassConstantStorage(
- $const_type,
- $stmt->isProtected()
- ? ClassLikeAnalyzer::VISIBILITY_PROTECTED
- : ($stmt->isPrivate()
- ? ClassLikeAnalyzer::VISIBILITY_PRIVATE
- : ClassLikeAnalyzer::VISIBILITY_PUBLIC),
- null
- );
+ $existing_constants[$const->name->name] = $constant_storage;
} else {
$unresolved_const_expr = ExpressionResolver::getUnresolvedClassConstExpr(
$const->value,
diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php
index aaf7ce5e3c2..7a9223c548a 100644
--- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php
+++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php
@@ -1652,6 +1652,11 @@ private static function reconcileInArray(
): Union {
$new_var_type = clone $assertion->type;
+ if ($new_var_type->isSingle() && $new_var_type->getSingleAtomic() instanceof TClassConstant) {
+ // Can't do assertion on const with non-literal type
+ return $existing_var_type;
+ }
+
$intersection = Type::intersectUnionTypes($new_var_type, $existing_var_type, $codebase);
if ($intersection === null) {
diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php
index 41fbb5e9f03..c4b26fab047 100644
--- a/src/Psalm/Internal/Type/TypeExpander.php
+++ b/src/Psalm/Internal/Type/TypeExpander.php
@@ -5,6 +5,7 @@
use Psalm\Codebase;
use Psalm\Exception\CircularReferenceException;
use Psalm\Storage\Assertion\IsType;
+use Psalm\Exception\UnresolvableConstantException;
use Psalm\Type\Atomic;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TCallable;
@@ -347,7 +348,11 @@ public static function expandAtomic(
$return_type->fq_classlike_name = $self_class;
}
- if ($evaluate_class_constants && $codebase->classOrInterfaceExists($return_type->fq_classlike_name)) {
+ if ($evaluate_class_constants) {
+ if (!$codebase->classOrInterfaceExists($return_type->fq_classlike_name)) {
+ throw new UnresolvableConstantException($return_type->fq_classlike_name, $return_type->const_name);
+ }
+
try {
$class_constant_type = $codebase->classlikes->getClassConstantType(
$return_type->fq_classlike_name,
@@ -374,6 +379,8 @@ public static function expandAtomic(
return array_values($const_type_atomic->type_params[1]->getAtomicTypes());
}
}
+ } else {
+ throw new UnresolvableConstantException($return_type->fq_classlike_name, $return_type->const_name);
}
}
diff --git a/src/Psalm/Issue/CodeIssue.php b/src/Psalm/Issue/CodeIssue.php
index c6d55215edd..0b8035efd09 100644
--- a/src/Psalm/Issue/CodeIssue.php
+++ b/src/Psalm/Issue/CodeIssue.php
@@ -10,7 +10,9 @@
abstract class CodeIssue
{
+ /** @var int */
public const ERROR_LEVEL = -1;
+ /** @var int<0, max> */
public const SHORTCODE = 0;
/**
@@ -88,8 +90,8 @@ public function toIssueData(string $severity): IssueData
$snippet_bounds[1],
$location->getColumn(),
$location->getEndColumn(),
- (int) static::SHORTCODE,
- (int) static::ERROR_LEVEL,
+ static::SHORTCODE,
+ static::ERROR_LEVEL,
$this instanceof TaintedInput
? $this->getTaintTrace()
: null,
diff --git a/src/Psalm/Issue/UnresolvableConstant.php b/src/Psalm/Issue/UnresolvableConstant.php
new file mode 100644
index 00000000000..8c08586d44d
--- /dev/null
+++ b/src/Psalm/Issue/UnresolvableConstant.php
@@ -0,0 +1,8 @@
+ [
+ ' [
+ ' [],
'php_version' => '8.1',
],
+ 'returnValueofNonExistantConstant' => [
+ '
+ */
+ public function bar(): string
+ {
+ return self::BAR[0];
+ }
+ }
+ ',
+ 'error_message' => 'UnresolvableConstant',
+ ],
+ 'returnValueofStaticConstant' => [
+ '
+ */
+ public function bar(): string
+ {
+ return static::BAR[0];
+ }
+ }
+ ',
+ 'error_message' => 'UnresolvableConstant',
+ ],
+ 'takeKeyofNonExistantConstant' => [
+ ' $key
+ */
+ public function bar(int $key): string
+ {
+ return static::BAR[$key];
+ }
+ }
+ ',
+ 'error_message' => 'UnresolvableConstant',
+ ],
+ 'takeKeyofStaticConstant' => [
+ ' $key
+ */
+ public function bar(int $key): string
+ {
+ return static::BAR[$key];
+ }
+ }
+ ',
+ 'error_message' => 'UnresolvableConstant',
+ ],
+ 'SKIPPED-keyofSelfConstDoesntImplyKeyofStaticConst' => [
+ ' */
+ public const CONST = [1, 2, 3];
+
+ /**
+ * @param key-of $key
+ */
+ public function bar(int $key): int
+ {
+ return static::CONST[$key];
+ }
+ }
+ ',
+ 'error_message' => 'MixedArrayAccess',
+ ],
];
}
}
diff --git a/tests/Template/ClassTemplateExtendsTest.php b/tests/Template/ClassTemplateExtendsTest.php
index f30f842a1e9..1200e53195d 100644
--- a/tests/Template/ClassTemplateExtendsTest.php
+++ b/tests/Template/ClassTemplateExtendsTest.php
@@ -28,6 +28,7 @@ public function providerValidCodeParse(): iterable
*/
abstract class Tuple
{
+ /** @var int */
const ARITY = 0;
/**
@@ -36,7 +37,7 @@ abstract class Tuple
*/
public function arity(): int
{
- return (int)static::ARITY;
+ return static::ARITY;
}
/**
@@ -74,7 +75,7 @@ public function __construct($_0) {
*/
public function arity(): int
{
- return (int)static::ARITY;
+ return static::ARITY;
}
/**
diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php
index 457a5898de8..5eef13b14d3 100644
--- a/tests/TypeReconciliation/ArrayKeyExistsTest.php
+++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php
@@ -202,7 +202,6 @@ function foo(array $array = []): void {
'assertArrayKeyExistsRefinesType' => [
'code' => ' */
public const DAYS = [
1 => "mon",
2 => "tue",