Skip to content

Commit

Permalink
Merge pull request #7123 from AndrolGenhald/feature/942-type-annotate…
Browse files Browse the repository at this point in the history
…-class-constants

Support type annotations for class consts (fixes #942).
  • Loading branch information
orklah committed Jan 25, 2022
2 parents 8ab0eec + 1f1f1c5 commit 7c8441b
Show file tree
Hide file tree
Showing 29 changed files with 610 additions and 72 deletions.
20 changes: 20 additions & 0 deletions config.xsd
Expand Up @@ -260,6 +260,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 @@ -463,6 +464,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 @@ -607,6 +609,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 @@ -1585,6 +1586,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 @@ -1796,6 +1799,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

0 comments on commit 7c8441b

Please sign in to comment.