mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Add @psalm-check-type
and @psalm-check-type-exact
.
I initially added these as part of my TryAnalyzer rewrite to allow testing complicated `finally` types like this: ``` $foo = 1; try { $foo = 2; } catch (Exception $_) { $foo = 3; } finally { $bar = $foo; /** @psalm-check-type-exact $bar = 1|2|3 */; } /** @psalm-check-type-exact $bar = 2|3 */; ``` Using the `'assertions'` in tests doesn't work since the type is different inside the `finally`. I decided to extract the new annotation from the rest of my changes and do a separate pull request since I think others may find it useful, and it should be much easier to review than the entire TryAnalyzer rewrite.
This commit is contained in:
parent
1ee764894a
commit
d09e420939
@ -193,6 +193,7 @@
|
||||
<xs:element name="AmbiguousConstantInheritance" type="ClassConstantIssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="ArgumentTypeCoercion" type="ArgumentIssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="AssignmentToVoid" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="CheckType" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="CircularReference" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="ComplexFunction" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="ComplexMethod" type="IssueHandlerType" minOccurs="0" />
|
||||
|
@ -448,6 +448,31 @@ $username = $_GET['username']; // prints something like "test.php:4 $username: m
|
||||
|
||||
*Note*: it throws [special low-level issue](../running_psalm/issues/Trace.md), so you have to set errorLevel to 1, override it in config or invoke Psalm with `--show-info=true`.
|
||||
|
||||
### `@psalm-check-type`
|
||||
|
||||
You can use this annotation to ensure the inferred type matches what you expect.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
/** @psalm-check-type $foo = int */
|
||||
$foo = 1; // No issue
|
||||
|
||||
/** @psalm-check-type $bar = int */
|
||||
$bar = "not-an-int"; // Checked variable $bar = int does not match $bar = 'not-an-int'
|
||||
```
|
||||
|
||||
### `@psalm-check-type-exact`
|
||||
|
||||
Like `@psalm-check-type`, but checks the exact type of the variable without allowing subtypes.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
/** @psalm-check-type-exact $foo = int */
|
||||
$foo = 1; // Checked variable $foo = int does not match $foo = 1
|
||||
```
|
||||
|
||||
### `@psalm-taint-*`
|
||||
|
||||
See [Security Analysis annotations](../security_analysis/annotations.md).
|
||||
|
@ -5,6 +5,7 @@
|
||||
- [AmbiguousConstantInheritance](issues/AmbiguousConstantInheritance.md)
|
||||
- [ArgumentTypeCoercion](issues/ArgumentTypeCoercion.md)
|
||||
- [AssignmentToVoid](issues/AssignmentToVoid.md)
|
||||
- [CheckType](issues/CheckType.md)
|
||||
- [CircularReference](issues/CircularReference.md)
|
||||
- [ComplexFunction](issues/ComplexFunction.md)
|
||||
- [ComplexMethod](issues/ComplexMethod.md)
|
||||
|
19
docs/running_psalm/issues/CheckType.md
Normal file
19
docs/running_psalm/issues/CheckType.md
Normal file
@ -0,0 +1,19 @@
|
||||
# CheckType
|
||||
|
||||
Checks if a variable matches a specific type.
|
||||
Similar to [Trace](./Trace.md), but only shows if the type does not match the expected type.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
/** @psalm-check-type $x = 1 */
|
||||
$x = 2; // Checked variable $x = 1 does not match $x = 2
|
||||
```
|
||||
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
/** @psalm-check-type-exact $x = int */
|
||||
$x = 2; // Checked variable $x = int does not match $x = 2
|
||||
```
|
@ -32,7 +32,7 @@ final class DocComment
|
||||
'yield', 'trace', 'import-type', 'flow', 'taint-specialize', 'taint-escape',
|
||||
'taint-unescape', 'self-out', 'consistent-constructor', 'stub-override',
|
||||
'require-extends', 'require-implements', 'param-out', 'ignore-var',
|
||||
'consistent-templates', 'if-this-is', 'this-out'
|
||||
'consistent-templates', 'if-this-is', 'this-out', 'check-type', 'check-type-exact',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -10,6 +10,7 @@ use Psalm\Context;
|
||||
use Psalm\DocComment;
|
||||
use Psalm\Exception\DocblockParseException;
|
||||
use Psalm\Exception\IncorrectDocblockException;
|
||||
use Psalm\Exception\TypeParseTreeException;
|
||||
use Psalm\FileManipulation;
|
||||
use Psalm\Internal\Analyzer\Statements\Block\DoAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Block\ForAnalyzer;
|
||||
@ -42,6 +43,8 @@ use Psalm\Internal\FileManipulation\FileManipulationBuffer;
|
||||
use Psalm\Internal\Provider\NodeDataProvider;
|
||||
use Psalm\Internal\ReferenceConstraint;
|
||||
use Psalm\Internal\Scanner\ParsedDocblock;
|
||||
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
use Psalm\Issue\CheckType;
|
||||
use Psalm\Issue\ComplexFunction;
|
||||
use Psalm\Issue\ComplexMethod;
|
||||
use Psalm\Issue\InvalidDocblock;
|
||||
@ -64,9 +67,11 @@ use function array_change_key_case;
|
||||
use function array_column;
|
||||
use function array_combine;
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_search;
|
||||
use function count;
|
||||
use function explode;
|
||||
use function fwrite;
|
||||
use function get_class;
|
||||
use function in_array;
|
||||
@ -370,6 +375,7 @@ class StatementsAnalyzer extends SourceAnalyzer
|
||||
$new_issues = null;
|
||||
$traced_variables = [];
|
||||
|
||||
$checked_types = [];
|
||||
if ($docblock = $stmt->getDocComment()) {
|
||||
$statements_analyzer->parseStatementDocblock($docblock, $stmt, $context);
|
||||
|
||||
@ -387,6 +393,13 @@ class StatementsAnalyzer extends SourceAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($statements_analyzer->parsed_docblock->tags['psalm-check-type'] ?? [] as $inexact_check) {
|
||||
$checked_types[] = [$inexact_check, false];
|
||||
}
|
||||
foreach ($statements_analyzer->parsed_docblock->tags['psalm-check-type-exact'] ?? [] as $exact_check) {
|
||||
$checked_types[] = [$exact_check, true];
|
||||
}
|
||||
|
||||
if (isset($statements_analyzer->parsed_docblock->tags['psalm-ignore-variable-method'])) {
|
||||
$context->ignore_variable_method = $ignore_variable_method = true;
|
||||
}
|
||||
@ -660,6 +673,66 @@ class StatementsAnalyzer extends SourceAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($checked_types as [$check_type_line, $is_exact]) {
|
||||
/** @var string|null $check_type_string (incorrectly inferred) */
|
||||
[$checked_var, $check_type_string] = array_map('trim', explode('=', $check_type_line));
|
||||
|
||||
if ($check_type_string === null) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidDocblock(
|
||||
"Invalid format for @psalm-check-type" . ($is_exact ? "-exact" : ""),
|
||||
new CodeLocation($statements_analyzer->source, $stmt),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
} else {
|
||||
$checked_var_id = $checked_var;
|
||||
$possibly_undefined = strrpos($checked_var_id, "?") === strlen($checked_var_id) - 1;
|
||||
if ($possibly_undefined) {
|
||||
$checked_var_id = substr($checked_var_id, 0, strlen($checked_var_id) - 1);
|
||||
}
|
||||
|
||||
if (!isset($context->vars_in_scope[$checked_var_id])) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidDocblock(
|
||||
"Attempt to check undefined variable $checked_var_id",
|
||||
new CodeLocation($statements_analyzer->source, $stmt),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
$checked_type = $context->vars_in_scope[$checked_var_id];
|
||||
$check_type = Type::parseString($check_type_string);
|
||||
$check_type->possibly_undefined = $possibly_undefined;
|
||||
|
||||
if ($check_type->possibly_undefined !== $checked_type->possibly_undefined
|
||||
|| !UnionTypeComparator::isContainedBy($codebase, $checked_type, $check_type)
|
||||
|| ($is_exact && !UnionTypeComparator::isContainedBy($codebase, $check_type, $checked_type))
|
||||
) {
|
||||
$check_var = $checked_var_id . ($checked_type->possibly_undefined ? "?" : "");
|
||||
IssueBuffer::maybeAdd(
|
||||
new CheckType(
|
||||
"Checked variable $checked_var = {$check_type->getId()} does not match "
|
||||
. "$check_var = {$checked_type->getId()}",
|
||||
new CodeLocation($statements_analyzer->source, $stmt),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
} catch (TypeParseTreeException $e) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidDocblock(
|
||||
$e->getMessage(),
|
||||
new CodeLocation($statements_analyzer->source, $stmt),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
9
src/Psalm/Issue/CheckType.php
Normal file
9
src/Psalm/Issue/CheckType.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Issue;
|
||||
|
||||
final class CheckType extends CodeIssue
|
||||
{
|
||||
public const ERROR_LEVEL = 8;
|
||||
public const SHORTCODE = 311;
|
||||
}
|
74
tests/CheckTypeTest.php
Normal file
74
tests/CheckTypeTest.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Tests;
|
||||
|
||||
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
|
||||
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
|
||||
|
||||
class CheckTypeTest extends TestCase
|
||||
{
|
||||
use InvalidCodeAnalysisTestTrait;
|
||||
use ValidCodeAnalysisTestTrait;
|
||||
|
||||
/**
|
||||
* @return iterable<string,array{code:string,assertions?:array<string,string>,ignored_issues?:list<string>,php_version?:string}>
|
||||
*/
|
||||
public function providerValidCodeParse(): iterable
|
||||
{
|
||||
yield 'allowSubtype' => [
|
||||
'code' => '<?php
|
||||
/** @psalm-check-type $foo = int */
|
||||
$foo = 1;
|
||||
',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string,array{code:string,error_message:string,ignored_issues?:list<string>,php_version?:string}>
|
||||
*/
|
||||
public function providerInvalidCodeParse(): iterable
|
||||
{
|
||||
yield 'checkType' => [
|
||||
'code' => '<?php
|
||||
$foo = 1;
|
||||
/** @psalm-check-type $foo = 2 */;
|
||||
',
|
||||
'error_message' => 'CheckType',
|
||||
];
|
||||
yield 'checkTypeExact' => [
|
||||
'code' => '<?php
|
||||
/** @psalm-check-type-exact $foo = int */
|
||||
$foo = 1;
|
||||
',
|
||||
'error_message' => 'CheckType',
|
||||
];
|
||||
yield 'checkMultipleTypesFirstCorrect' => [
|
||||
'code' => '<?php
|
||||
$foo = 1;
|
||||
$bar = 2;
|
||||
/**
|
||||
* @psalm-check-type $foo = 1
|
||||
* @psalm-check-type $bar = 3
|
||||
*/;
|
||||
',
|
||||
'error_message' => 'CheckType',
|
||||
];
|
||||
yield 'possiblyUnset' => [
|
||||
'code' => '<?php
|
||||
try {
|
||||
$foo = 1;
|
||||
} catch (Exception $_) {
|
||||
}
|
||||
/** @psalm-check-type $foo = 1 */;
|
||||
',
|
||||
'error_message' => 'Checked variable $foo = 1 does not match $foo? = 1',
|
||||
];
|
||||
yield 'notPossiblyUnset' => [
|
||||
'code' => '<?php
|
||||
$foo = 1;
|
||||
/** @psalm-check-type $foo? = 1 */;
|
||||
',
|
||||
'error_message' => 'Checked variable $foo? = 1 does not match $foo = 1',
|
||||
];
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user