Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support type annotations for class consts (fixes #942). #7123

Merged
20 changes: 20 additions & 0 deletions config.xsd
Expand Up @@ -255,6 +255,7 @@
<xs:element name="InvalidCatch" type="ClassIssueHandlerType" minOccurs="0" />
<xs:element name="InvalidClass" type="ClassIssueHandlerType" minOccurs="0" />
<xs:element name="InvalidClone" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InvalidConstantAssignmentValue" type="ClassConstantIssueHandlerType" minOccurs="0" />
<xs:element name="InvalidDocblock" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InvalidDocblockParamName" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InvalidEnumBackingType" type="ClassIssueHandlerType" minOccurs="0" />
Expand Down Expand Up @@ -458,6 +459,7 @@
<xs:element name="UnnecessaryVarAnnotation" type="IssueHandlerType" minOccurs="0" />
<xs:element name="UnrecognizedExpression" type="IssueHandlerType" minOccurs="0" />
<xs:element name="UnrecognizedStatement" type="IssueHandlerType" minOccurs="0" />
<xs:element name="UnresolvableConstant" type="IssueHandlerType" minOccurs="0" />
<xs:element name="UnresolvableInclude" type="IssueHandlerType" minOccurs="0" />
<xs:element name="UnsafeInstantiation" type="IssueHandlerType" minOccurs="0" />
<xs:element name="UnsafeGenericInstantiation" type="IssueHandlerType" minOccurs="0" />
Expand Down Expand Up @@ -602,6 +604,24 @@
<xs:attribute name="errorLevel" type="ErrorLevelType" />
</xs:complexType>

<xs:complexType name="ClassConstantIssueHandlerType">
<xs:sequence>
<xs:element name="errorLevel" minOccurs="0" maxOccurs="unbounded">
<xs:complexType>
<xs:choice maxOccurs="unbounded">
<xs:element name="directory" minOccurs="0" maxOccurs="unbounded" type="NameAttributeType" />
<xs:element name="file" minOccurs="0" maxOccurs="unbounded" type="NameAttributeType" />
<xs:element name="referencedConstant" minOccurs="0" maxOccurs="unbounded" type="NameAttributeType" />
</xs:choice>

<xs:attribute name="type" type="ErrorLevelType" use="required" />
</xs:complexType>
</xs:element>
</xs:sequence>

<xs:attribute name="errorLevel" type="ErrorLevelType" />
</xs:complexType>

<xs:complexType name="VariableIssueHandlerType">
<xs:sequence>
<xs:element name="errorLevel" minOccurs="0" maxOccurs="unbounded">
Expand Down
2 changes: 2 additions & 0 deletions docs/running_psalm/issues.md
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions 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
<?php

class Foo {
/** @var int */
public const BAR = "bar";
}
```
22 changes: 22 additions & 0 deletions 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
<?php

class Foo
{
public const BAR = ['bar'];

/**
* @return value-of<self::BAT>
*/
public function bar(): string
{
return self::BAR[0];
}
}
```
12 changes: 12 additions & 0 deletions src/Psalm/Config.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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])) {
Expand Down
25 changes: 25 additions & 0 deletions src/Psalm/Config/FileFilter.php
Expand Up @@ -66,6 +66,11 @@ class FileFilter
*/
protected $property_ids = [];

/**
* @var array<string>
*/
protected $class_constant_ids = [];

/**
* @var array<string>
*/
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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'] = [];

Expand Down Expand Up @@ -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);
Expand Down
12 changes: 12 additions & 0 deletions src/Psalm/Config/IssueHandler.php
Expand Up @@ -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) {
Expand All @@ -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'
Expand Down
24 changes: 24 additions & 0 deletions src/Psalm/Exception/UnresolvableConstantException.php
@@ -0,0 +1,24 @@
<?php

namespace Psalm\Exception;

use Exception;

class UnresolvableConstantException extends Exception
{
/**
* @var string
*/
public $class_name;

/**
* @var string
*/
public $const_name;

public function __construct(string $class_name, string $const_name)
{
$this->class_name = $class_name;
$this->const_name = $const_name;
}
}
32 changes: 23 additions & 9 deletions src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
37 changes: 26 additions & 11 deletions src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
Expand Up @@ -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
Expand Down