diff --git a/config.xsd b/config.xsd
index 917968bd0f2..cee3627fef1 100644
--- a/config.xsd
+++ b/config.xsd
@@ -255,6 +255,7 @@
+
@@ -603,6 +604,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
+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/Internal/Analyzer/ClassLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php
index 254c87fd4f7..66a3dd7ea21 100644
--- a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php
@@ -22,6 +22,7 @@
use Psalm\StatementsSource;
use Psalm\Storage\ClassLikeStorage;
use Psalm\Type;
+use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Union;
use UnexpectedValueException;
@@ -30,6 +31,7 @@
use function gettype;
use function implode;
use function in_array;
+use function is_array;
use function is_float;
use function is_int;
use function is_string;
@@ -519,10 +521,26 @@ public static function getTypeFromValue($value): Union
* Gets the Psalm literal type from a particular value
*
* @param array|scalar|null $value
+ * @throws InvalidArgumentException
*
*/
- public static function getLiteralTypeFromValue($value): Type\Union
+ public static function getLiteralTypeFromValue($value, bool $sealed_array = true): Type\Union
{
+ if (is_array($value)) {
+ if (empty($value)) {
+ return Type::getEmptyArray();
+ }
+
+ $types = [];
+ /** @var array|scalar|null $val */
+ foreach ($value as $key => $val) {
+ $types[$key] = self::getLiteralTypeFromValue($val, $sealed_array);
+ }
+ $type = new TKeyedArray($types);
+ $type->sealed = $sealed_array;
+ return new Type\Union([$type]);
+ }
+
if (is_string($value)) {
return Type::getString($value);
}
@@ -543,7 +561,11 @@ public static function getLiteralTypeFromValue($value): Type\Union
return Type::getTrue();
}
- return Type::getNull();
+ if ($value === null) {
+ return Type::getNull();
+ }
+
+ throw new InvalidArgumentException();
}
/**
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 5ed42002c5a..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;
@@ -676,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/Codebase/ConstantTypeResolver.php b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php
index dfc1a7f0d4e..20ebf069953 100644
--- a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php
+++ b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php
@@ -137,7 +137,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;
diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
index 35ae6b1fd9c..31104071ed6 100644
--- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
+++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
@@ -1270,8 +1270,10 @@ private function visitClassConstDeclaration(
);
$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
@@ -1301,6 +1303,7 @@ private function visitClassConstDeclaration(
$const->name
)
);
+ $constant_storage->suppressed_issues = $suppressed_issues;
$constant_storage->type_location = $type_location;
diff --git a/src/Psalm/Issue/ClassConstantIssue.php b/src/Psalm/Issue/ClassConstantIssue.php
new file mode 100644
index 00000000000..e747e78ac78
--- /dev/null
+++ b/src/Psalm/Issue/ClassConstantIssue.php
@@ -0,0 +1,21 @@
+const_id = $const_id;
+ }
+}
diff --git a/src/Psalm/Issue/InvalidConstantAssignmentValue.php b/src/Psalm/Issue/InvalidConstantAssignmentValue.php
new file mode 100644
index 00000000000..32922b5e37f
--- /dev/null
+++ b/src/Psalm/Issue/InvalidConstantAssignmentValue.php
@@ -0,0 +1,8 @@
+
+ */
+ public $suppressed_issues = [];
+
/**
* @var ?string
*/
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 54fb1ff41ff..35e00879dda 100644
--- a/tests/ConstantTest.php
+++ b/tests/ConstantTest.php
@@ -619,6 +619,8 @@ class B extends A {
class Clazz {
/**
+ * @var 0|1
+ *
* @psalm-suppress RedundantCondition
*/
const cons2 = (cons1) ? 1 : 0;
@@ -1293,6 +1295,30 @@ public function bar(): string
}
',
],
+ 'classConstSuppress' => [
+ ' [
+ ' $arg */
+ function foo(array $arg): void {}
+ foo([...A::ARR]);
+ ',
+ ],
];
}
@@ -1642,6 +1668,28 @@ public function bar(int $key): string
',
'error_message' => 'UnresolvableConstant',
],
+ 'invalidConstantAssignmentType' => [
+ ' "InvalidConstantAssignmentValue",
+ ],
+ 'invalidConstantAssignmentTypeResolvedLate' => [
+ ' "InvalidConstantAssignmentValue",
+ ],
];
}
}