diff --git a/config.xsd b/config.xsd
index ab37ca2dcd3..50d54ed9622 100644
--- a/config.xsd
+++ b/config.xsd
@@ -481,6 +481,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 1dc5fa93072..e170f677c8f 100644
--- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php
@@ -10,6 +10,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;
@@ -39,6 +40,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;
@@ -820,15 +822,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 1c64cfa871e..16916afbf63 100644
--- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php
@@ -9,6 +9,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;
@@ -34,6 +35,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;
@@ -997,17 +999,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 596710fe154..c71f38f6017 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php
@@ -233,7 +233,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;
@@ -359,7 +361,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 a9260050668..a026a9bd945 100644
--- a/src/Psalm/Internal/Codebase/ClassLikes.php
+++ b/src/Psalm/Internal/Codebase/ClassLikes.php
@@ -1634,7 +1634,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,
): ?Type\Union {
$class_name = strtolower($class_name);
@@ -1661,15 +1662,18 @@ public function getClassConstantType(
}
if ($constant_storage->unresolved_node) {
- $constant_storage->type = new Type\Union([ConstantTypeResolver::resolve(
+ $constant_storage->inferred_type = new Type\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 Type\Union([new Type\Atomic\TEnumCase($storage->name, $constant_name)]);
}
diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
index a9e09456b40..8f5b934621c 100644
--- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
+++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
@@ -1190,6 +1190,7 @@ private function visitClassConstDeclaration(
$existing_constants = $storage->constants;
$comment = $stmt->getDocComment();
+ $var_comment = null;
$deprecated = false;
$description = null;
$config = $this->config;
@@ -1202,19 +1203,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])
) {
@@ -1228,6 +1241,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()
@@ -1241,30 +1283,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()) === Type\Atomic\TString::class
+ if ($inferred_type
+ && !(
+ $const->value instanceof Concat
+ && $inferred_type->isSingle()
+ && get_class($inferred_type->getSingleAtomic()) === Type\Atomic\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 e114165bc63..4622251c350 100644
--- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php
+++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php
@@ -17,6 +17,7 @@
use Psalm\Type\Atomic\TCallableArray;
use Psalm\Type\Atomic\TCallableKeyedArray;
use Psalm\Type\Atomic\TCallableList;
+use Psalm\Type\Atomic\TClassConstant;
use Psalm\Type\Atomic\TClassString;
use Psalm\Type\Atomic\TEmpty;
use Psalm\Type\Atomic\TEmptyMixed;
@@ -1563,6 +1564,11 @@ private static function reconcileInArray(
return $existing_var_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 e1b31c29eed..8f6ebf17103 100644
--- a/src/Psalm/Internal/Type/TypeExpander.php
+++ b/src/Psalm/Internal/Type/TypeExpander.php
@@ -3,6 +3,7 @@
use Psalm\Codebase;
use Psalm\Exception\CircularReferenceException;
+use Psalm\Exception\UnresolvableConstantException;
use Psalm\Internal\Type\SimpleAssertionReconciler;
use Psalm\Internal\Type\SimpleNegatedAssertionReconciler;
use Psalm\Internal\Type\TypeParser;
@@ -336,7 +337,11 @@ function ($constant_name) use ($const_name_part): bool {
$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,
@@ -363,6 +368,8 @@ function ($constant_name) use ($const_name_part): bool {
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 f870e498b42..fc3cd9a4ba4 100644
--- a/src/Psalm/Issue/CodeIssue.php
+++ b/src/Psalm/Issue/CodeIssue.php
@@ -9,7 +9,9 @@
abstract class CodeIssue
{
+ /** @var int */
public const ERROR_LEVEL = -1;
+ /** @var int<0, max> */
public const SHORTCODE = 0;
/**
@@ -114,8 +116,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 @@
+ [
+ ' [
+ ' [
+ '
+ */
+ 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 23b6aaf54e3..ed95c8b21f6 100644
--- a/tests/Template/ClassTemplateExtendsTest.php
+++ b/tests/Template/ClassTemplateExtendsTest.php
@@ -27,6 +27,7 @@ public function providerValidCodeParse(): iterable
*/
abstract class Tuple
{
+ /** @var int */
const ARITY = 0;
/**
@@ -35,7 +36,7 @@ abstract class Tuple
*/
public function arity(): int
{
- return (int)static::ARITY;
+ return static::ARITY;
}
/**
@@ -73,7 +74,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 4c33dc8c0f8..f13a88d1367 100644
--- a/tests/TypeReconciliation/ArrayKeyExistsTest.php
+++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php
@@ -201,7 +201,6 @@ function foo(array $array = []): void {
'assertArrayKeyExistsRefinesType' => [
' */
public const DAYS = [
1 => "mon",
2 => "tue",