diff --git a/config.xsd b/config.xsd
index ad48f587e59..cee3627fef1 100644
--- a/config.xsd
+++ b/config.xsd
@@ -255,6 +255,7 @@
+
@@ -458,6 +459,7 @@
+
@@ -602,6 +604,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/running_psalm/issues.md b/docs/running_psalm/issues.md
index 7c3da484357..dca69c9dda2 100644
--- a/docs/running_psalm/issues.md
+++ b/docs/running_psalm/issues.md
@@ -61,6 +61,7 @@
- [InvalidCatch](issues/InvalidCatch.md)
- [InvalidClass](issues/InvalidClass.md)
- [InvalidClone](issues/InvalidClone.md)
+ - [InvalidConstantAssignmentValue](issues/InvalidConstantAssignmentValue.md)
- [InvalidDocblock](issues/InvalidDocblock.md)
- [InvalidDocblockParamName](issues/InvalidDocblockParamName.md)
- [InvalidEnumBackingType](issues/InvalidEnumBackingType.md)
@@ -266,6 +267,7 @@
- [UnnecessaryVarAnnotation](issues/UnnecessaryVarAnnotation.md)
- [UnrecognizedExpression](issues/UnrecognizedExpression.md)
- [UnrecognizedStatement](issues/UnrecognizedStatement.md)
+ - [UnresolvableConstant](issues/UnresolvableConstant.md)
- [UnresolvableInclude](issues/UnresolvableInclude.md)
- [UnsafeGenericInstantiation](issues/UnsafeGenericInstantiation.md)
- [UnsafeInstantiation](issues/UnsafeInstantiation.md)
diff --git a/docs/running_psalm/issues/InvalidConstantAssignmentValue.md b/docs/running_psalm/issues/InvalidConstantAssignmentValue.md
new file mode 100644
index 00000000000..b0bf046ba39
--- /dev/null
+++ b/docs/running_psalm/issues/InvalidConstantAssignmentValue.md
@@ -0,0 +1,12 @@
+# InvalidConstantAssignmentValue
+
+Emitted when attempting to assign a value to a class constant that cannot contain that type.
+
+```php
+
+ */
+ public function bar(): string
+ {
+ return self::BAR[0];
+ }
+}
+```
diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php
index c80ca64de65..375431dfbf9 100644
--- a/src/Psalm/Config.php
+++ b/src/Psalm/Config.php
@@ -27,6 +27,7 @@
use Psalm\Internal\Provider\AddRemoveTaints\HtmlFunctionTainter;
use Psalm\Internal\Scanner\FileScanner;
use Psalm\Issue\ArgumentIssue;
+use Psalm\Issue\ClassConstantIssue;
use Psalm\Issue\ClassIssue;
use Psalm\Issue\CodeIssue;
use Psalm\Issue\ConfigIssue;
@@ -1583,6 +1584,8 @@ public function getReportingLevelForIssue(CodeIssue $e): string
$reporting_level = $this->getReportingLevelForFunction($issue_type, $e->function_id);
} elseif ($e instanceof PropertyIssue) {
$reporting_level = $this->getReportingLevelForProperty($issue_type, $e->property_id);
+ } elseif ($e instanceof ClassConstantIssue) {
+ $reporting_level = $this->getReportingLevelForClassConstant($issue_type, $e->const_id);
} elseif ($e instanceof ArgumentIssue && $e->function_id) {
$reporting_level = $this->getReportingLevelForArgument($issue_type, $e->function_id);
} elseif ($e instanceof VariableIssue) {
@@ -1794,6 +1797,15 @@ public function getReportingLevelForProperty(string $issue_type, string $propert
return null;
}
+ public function getReportingLevelForClassConstant(string $issue_type, string $constant_id): ?string
+ {
+ if (isset($this->issue_handlers[$issue_type])) {
+ return $this->issue_handlers[$issue_type]->getReportingLevelForClassConstant($constant_id);
+ }
+
+ return null;
+ }
+
public function getReportingLevelForVariable(string $issue_type, string $var_name): ?string
{
if (isset($this->issue_handlers[$issue_type])) {
diff --git a/src/Psalm/Config/FileFilter.php b/src/Psalm/Config/FileFilter.php
index 2db9bdaaee8..23948b7708e 100644
--- a/src/Psalm/Config/FileFilter.php
+++ b/src/Psalm/Config/FileFilter.php
@@ -66,6 +66,11 @@ class FileFilter
*/
protected $property_ids = [];
+ /**
+ * @var array
+ */
+ protected $class_constant_ids = [];
+
/**
* @var array
*/
@@ -326,6 +331,13 @@ public static function loadFromArray(
}
}
+ if (isset($config['referencedConstant']) && is_iterable($config['referencedConstant'])) {
+ /** @var array $referenced_constant */
+ foreach ($config['referencedConstant'] as $referenced_constant) {
+ $filter->class_constant_ids[] = strtolower((string) ($referenced_constant['name'] ?? ''));
+ }
+ }
+
if (isset($config['referencedVariable']) && is_iterable($config['referencedVariable'])) {
/** @var array $referenced_variable */
foreach ($config['referencedVariable'] as $referenced_variable) {
@@ -400,6 +412,14 @@ public static function loadFromXMLElement(
}
}
+ if ($e->referencedConstant) {
+ $config['referencedConstant'] = [];
+ /** @var SimpleXMLElement $referenced_constant */
+ foreach ($e->referencedConstant as $referenced_constant) {
+ $config['referencedConstant'][]['name'] = strtolower((string)$referenced_constant['name']);
+ }
+ }
+
if ($e->referencedVariable) {
$config['referencedVariable'] = [];
@@ -533,6 +553,11 @@ public function allowsProperty(string $property_id): bool
return in_array(strtolower($property_id), $this->property_ids, true);
}
+ public function allowsClassConstant(string $constant_id): bool
+ {
+ return in_array(strtolower($constant_id), $this->class_constant_ids, true);
+ }
+
public function allowsVariable(string $var_name): bool
{
return in_array(strtolower($var_name), $this->var_names, true);
diff --git a/src/Psalm/Config/IssueHandler.php b/src/Psalm/Config/IssueHandler.php
index d3b6a3cdf0a..db68b8af824 100644
--- a/src/Psalm/Config/IssueHandler.php
+++ b/src/Psalm/Config/IssueHandler.php
@@ -131,6 +131,17 @@ public function getReportingLevelForProperty(string $property_id): ?string
return null;
}
+ public function getReportingLevelForClassConstant(string $constant_id): ?string
+ {
+ foreach ($this->custom_levels as $custom_level) {
+ if ($custom_level->allowsClassConstant($constant_id)) {
+ return $custom_level->getErrorLevel();
+ }
+ }
+
+ return null;
+ }
+
public function getReportingLevelForVariable(string $var_name): ?string
{
foreach ($this->custom_levels as $custom_level) {
@@ -155,6 +166,7 @@ public static function getAllIssueTypes(): array
fn(string $issue_name): bool => $issue_name !== ''
&& $issue_name !== 'MethodIssue'
&& $issue_name !== 'PropertyIssue'
+ && $issue_name !== 'ClassConstantIssue'
&& $issue_name !== 'FunctionIssue'
&& $issue_name !== 'ArgumentIssue'
&& $issue_name !== 'VariableIssue'
diff --git a/src/Psalm/Exception/UnresolvableConstantException.php b/src/Psalm/Exception/UnresolvableConstantException.php
new file mode 100644
index 00000000000..10032182587
--- /dev/null
+++ b/src/Psalm/Exception/UnresolvableConstantException.php
@@ -0,0 +1,24 @@
+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/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php
index af55952a1ea..9f5e1ff305b 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php
@@ -117,6 +117,12 @@ public static function analyze(
$item_value_type = null;
}
+ if ($item_key_type === null && $item_value_type === null) {
+ $statements_analyzer->node_data->setType($stmt, Type::getEmptyArray());
+
+ return true;
+ }
+
// if this array looks like an object-like array, let's return that instead
if ($item_value_type
&& $item_key_type
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php
index dc6fa069753..10ef6434885 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php
@@ -15,11 +15,13 @@
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Analyzer\TraitAnalyzer;
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
+use Psalm\Internal\Type\Comparator\UnionTypeComparator;
use Psalm\Issue\CircularReference;
use Psalm\Issue\DeprecatedClass;
use Psalm\Issue\DeprecatedConstant;
use Psalm\Issue\InaccessibleClassConstant;
use Psalm\Issue\InternalClass;
+use Psalm\Issue\InvalidConstantAssignmentValue;
use Psalm\Issue\NonStaticSelfCall;
use Psalm\Issue\ParentNotFound;
use Psalm\Issue\UndefinedConstant;
@@ -35,6 +37,7 @@
use Psalm\Type\Union;
use ReflectionProperty;
+use function assert;
use function explode;
use function in_array;
use function strtolower;
@@ -252,7 +255,9 @@ public static function analyze(
$fq_class_name,
$stmt->name->name,
$class_visibility,
- $statements_analyzer
+ $statements_analyzer,
+ [],
+ $stmt->class->parts[0] === "static"
);
} catch (InvalidArgumentException $_) {
return true;
@@ -378,7 +383,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);
@@ -674,6 +679,32 @@ public static function analyzeClassConstAssignment(
): void {
foreach ($stmt->consts as $const) {
ExpressionAnalyzer::analyze($statements_analyzer, $const->value, $context);
+
+ assert($context->self !== null);
+ $class_storage = $statements_analyzer->getCodebase()->classlike_storage_provider->get($context->self);
+ $const_storage = $class_storage->constants[$const->name->name];
+ if ($assigned_type = $statements_analyzer->node_data->getType($const->value)) {
+ if ($const_storage->type !== null
+ && $const_storage->stmt_location !== null
+ && $assigned_type !== $const_storage->type
+ && !UnionTypeComparator::isContainedBy(
+ $statements_analyzer->getCodebase(),
+ $assigned_type,
+ $const_storage->type
+ )
+ ) {
+ IssueBuffer::maybeAdd(
+ new InvalidConstantAssignmentValue(
+ "{$context->self}::{$const->name->name} with declared type {$const_storage->type->getId()} "
+ . "cannot be assigned type {$assigned_type->getId()}",
+ $const_storage->stmt_location,
+ "{$context->self}::{$const->name->name}"
+ ),
+ $const_storage->suppressed_issues,
+ true
+ );
+ }
+ }
}
}
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/TernaryAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/TernaryAnalyzer.php
index 99ed3eb6c45..39905239d37 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/TernaryAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/TernaryAnalyzer.php
@@ -274,12 +274,12 @@ function (Clause $c) use ($mixed_var_ids, $cond_id): Clause {
);
$lhs_type = null;
-
+ $stmt_cond_type = $statements_analyzer->node_data->getType($stmt->cond);
if ($stmt->if) {
if ($stmt_if_type = $statements_analyzer->node_data->getType($stmt->if)) {
$lhs_type = $stmt_if_type;
}
- } elseif ($stmt_cond_type = $statements_analyzer->node_data->getType($stmt->cond)) {
+ } elseif ($stmt_cond_type) {
$if_return_type_reconciled = AssertionReconciler::reconcile(
new Truthy(),
clone $stmt_cond_type,
@@ -295,7 +295,13 @@ function (Clause $c) use ($mixed_var_ids, $cond_id): Clause {
}
if ($lhs_type && ($stmt_else_type = $statements_analyzer->node_data->getType($stmt->else))) {
- $statements_analyzer->node_data->setType($stmt, Type::combineUnionTypes($lhs_type, $stmt_else_type));
+ if ($stmt_cond_type !== null && $stmt_cond_type->isAlwaysFalsy()) {
+ $statements_analyzer->node_data->setType($stmt, $stmt_else_type);
+ } elseif ($stmt_cond_type !== null && $stmt_cond_type->isAlwaysTruthy()) {
+ $statements_analyzer->node_data->setType($stmt, $lhs_type);
+ } else {
+ $statements_analyzer->node_data->setType($stmt, Type::combineUnionTypes($lhs_type, $stmt_else_type));
+ }
} else {
$statements_analyzer->node_data->setType($stmt, Type::getMixed());
}
diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php
index aad785051c0..1d394e2fa37 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 $late_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 $late_static_binding ? $constant_storage->type : ($constant_storage->inferred_type ?? null);
} elseif (isset($storage->enum_cases[$constant_name])) {
return new Union([new TEnumCase($storage->name, $constant_name)]);
}
diff --git a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php
index dfc1a7f0d4e..88c9e03b189 100644
--- a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php
+++ b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php
@@ -2,6 +2,7 @@
namespace Psalm\Internal\Codebase;
+use InvalidArgumentException;
use Psalm\Exception\CircularReferenceException;
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ConstFetchAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
@@ -40,6 +41,7 @@
use ReflectionProperty;
use function ctype_digit;
+use function is_array;
use function is_float;
use function is_int;
use function is_string;
@@ -137,7 +139,9 @@ public static function resolve(
}
if ($left instanceof TKeyedArray && $right instanceof TKeyedArray) {
- return new TKeyedArray($left->properties + $right->properties);
+ $type = new TKeyedArray($left->properties + $right->properties);
+ $type->sealed = true;
+ return $type;
}
return new TMixed;
@@ -332,10 +336,27 @@ public static function resolve(
}
/**
- * @param string|int|float|bool|null $value
+ * Note: This takes an array, but any array should only contain other arrays and scalars.
+ *
+ * @param array|string|int|float|bool|null $value
*/
- private static function getLiteralTypeFromScalarValue($value): Atomic
+ public static function getLiteralTypeFromScalarValue($value, bool $sealed_array = true): Atomic
{
+ if (is_array($value)) {
+ if (empty($value)) {
+ return Type::getEmptyArray()->getSingleAtomic();
+ }
+
+ $types = [];
+ /** @var array|scalar|null $val */
+ foreach ($value as $key => $val) {
+ $types[$key] = new Union([self::getLiteralTypeFromScalarValue($val, $sealed_array)]);
+ }
+ $type = new TKeyedArray($types);
+ $type->sealed = $sealed_array;
+ return $type;
+ }
+
if (is_string($value)) {
return new TLiteralString($value);
}
@@ -349,13 +370,17 @@ private static function getLiteralTypeFromScalarValue($value): Atomic
}
if ($value === false) {
- return new TFalse;
+ return new TFalse();
}
if ($value === true) {
- return new TTrue;
+ return new TTrue();
+ }
+
+ if ($value === null) {
+ return new TNull();
}
- return new TNull;
+ throw new InvalidArgumentException('$value must be a scalar.');
}
}
diff --git a/src/Psalm/Internal/Codebase/Reflection.php b/src/Psalm/Internal/Codebase/Reflection.php
index 424c2a1cdeb..45a12766cec 100644
--- a/src/Psalm/Internal/Codebase/Reflection.php
+++ b/src/Psalm/Internal/Codebase/Reflection.php
@@ -171,6 +171,7 @@ public function registerClass(ReflectionClass $reflected_class): void
foreach ($class_constants as $name => $value) {
$storage->constants[$name] = new ClassConstantStorage(
ClassLikeAnalyzer::getTypeFromValue($value),
+ new Union([ConstantTypeResolver::getLiteralTypeFromScalarValue($value)]),
ClassLikeAnalyzer::VISIBILITY_PUBLIC,
null
);
diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
index 977d7888e96..31104071ed6 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,8 +1259,40 @@ 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;
+ $suppressed_issues = [];
+ if ($var_comment !== null && $var_comment->type !== null) {
+ $const_type = $var_comment->type;
+ $suppressed_issues = $var_comment->suppressed_issues;
+
+ 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,
+ $inferred_type,
$stmt->isProtected()
? ClassLikeAnalyzer::VISIBILITY_PROTECTED
: ($stmt->isPrivate()
@@ -1258,31 +1303,23 @@ private function visitClassConstDeclaration(
$const->name
)
);
+ $constant_storage->suppressed_issues = $suppressed_issues;
+
+ $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..230dd5d5592 100644
--- a/src/Psalm/Internal/Type/TypeExpander.php
+++ b/src/Psalm/Internal/Type/TypeExpander.php
@@ -4,6 +4,7 @@
use Psalm\Codebase;
use Psalm\Exception\CircularReferenceException;
+use Psalm\Exception\UnresolvableConstantException;
use Psalm\Storage\Assertion\IsType;
use Psalm\Type\Atomic;
use Psalm\Type\Atomic\TArray;
@@ -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/ClassConstantIssue.php b/src/Psalm/Issue/ClassConstantIssue.php
new file mode 100644
index 00000000000..fc5443caee1
--- /dev/null
+++ b/src/Psalm/Issue/ClassConstantIssue.php
@@ -0,0 +1,22 @@
+const_id = $const_id;
+ }
+}
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/InvalidConstantAssignmentValue.php b/src/Psalm/Issue/InvalidConstantAssignmentValue.php
new file mode 100644
index 00000000000..a2c00a6d48e
--- /dev/null
+++ b/src/Psalm/Issue/InvalidConstantAssignmentValue.php
@@ -0,0 +1,9 @@
+
- * @psalm-suppress PossiblyUnusedProperty
*/
public $attributes = [];
+ /**
+ * @var array
+ */
+ public $suppressed_issues = [];
+
/**
* @var ?string
*/
@@ -53,10 +76,11 @@ class ClassConstantStorage
/**
* @param ClassLikeAnalyzer::VISIBILITY_* $visibility
*/
- public function __construct(?Union $type, int $visibility, ?CodeLocation $location)
+ public function __construct(?Union $type, ?Union $inferred_type, int $visibility, ?CodeLocation $location)
{
$this->visibility = $visibility;
$this->location = $location;
$this->type = $type;
+ $this->inferred_type = $inferred_type;
}
}
diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php
index ec0c6831b93..03f4f21f49d 100644
--- a/tests/ArrayAssignmentTest.php
+++ b/tests/ArrayAssignmentTest.php
@@ -1514,9 +1514,8 @@ function unpackIterable(Traversable $data): array
$y = [];
$x = [...$x, ...$y];
-
- $x ? 1 : 0;
',
+ 'assertions' => ['$x' => 'array']
],
'unpackEmptyKeepsCorrectKeys' => [
'code' => '
+
+
+
+
+
'
)
@@ -589,6 +594,14 @@ public function testIssueHandlerWithCustomErrorLevels(): void
'b'
)
);
+
+ $this->assertSame(
+ 'suppress',
+ $config->getReportingLevelForClassConstant(
+ 'InvalidConstantAssignmentValue',
+ 'Psalm\Bodger::FOO'
+ )
+ );
}
public function testIssueHandlerSetDynamically(): void
diff --git a/tests/ConstantTest.php b/tests/ConstantTest.php
index b03a87250d4..0dcd0630670 100644
--- a/tests/ConstantTest.php
+++ b/tests/ConstantTest.php
@@ -10,6 +10,38 @@ class ConstantTest extends TestCase
use InvalidCodeAnalysisTestTrait;
use ValidCodeAnalysisTestTrait;
+ // TODO: Waiting for https://github.com/vimeo/psalm/issues/7125
+ // public function testKeyofSelfConstDoesntImplyKeyofStaticConst(): void
+ // {
+ // $this->expectException(CodeException::class);
+ // $this->expectExceptionMessage("PossiblyUndefinedIntArrayOffset");
+
+ // $this->testConfig->ensure_array_int_offsets_exist = true;
+
+ // $file_path = getcwd() . '/src/somefile.php';
+
+ // $this->addFile(
+ // $file_path,
+ // ' */
+ // public const CONST = [1, 2, 3];
+
+ // /**
+ // * @param key-of $key
+ // */
+ // public function bar(int $key): int
+ // {
+ // return static::CONST[$key];
+ // }
+ // }
+ // '
+ // );
+
+ // $this->analyzeFile($file_path, new Context());
+ // }
+
/**
* @return iterable,ignored_issues?:list, php_version?: string}>
*/
@@ -273,14 +305,17 @@ function foo(array $arg): void {}
'lateConstantResolutionParentStringConcat' => [
'code' => ' [
+ 'code' => ' [
+ 'code' => ' [
+ 'code' => ' [
+ 'code' => ' $arg */
+ function foo(array $arg): void {}
+ foo([...A::ARR]);
+ ',
+ ],
];
}
@@ -1511,6 +1601,96 @@ enum State {
'ignored_issues' => [],
'php_version' => '8.1',
],
+ 'returnValueofNonExistantConstant' => [
+ 'code' => '
+ */
+ public function bar(): string
+ {
+ return self::BAR[0];
+ }
+ }
+ ',
+ 'error_message' => 'UnresolvableConstant',
+ ],
+ 'returnValueofStaticConstant' => [
+ 'code' => '
+ */
+ public function bar(): string
+ {
+ return static::BAR[0];
+ }
+ }
+ ',
+ 'error_message' => 'UnresolvableConstant',
+ ],
+ 'takeKeyofNonExistantConstant' => [
+ 'code' => ' $key
+ */
+ public function bar(int $key): string
+ {
+ return static::BAR[$key];
+ }
+ }
+ ',
+ 'error_message' => 'UnresolvableConstant',
+ ],
+ 'takeKeyofStaticConstant' => [
+ 'code' => ' $key
+ */
+ public function bar(int $key): string
+ {
+ return static::BAR[$key];
+ }
+ }
+ ',
+ 'error_message' => 'UnresolvableConstant',
+ ],
+ 'invalidConstantAssignmentType' => [
+ 'code' => ' "InvalidConstantAssignmentValue",
+ ],
+ 'invalidConstantAssignmentTypeResolvedLate' => [
+ 'code' => ' "InvalidConstantAssignmentValue",
+ ],
];
}
}
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",