1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-27 04:45:20 +01:00

Merge pull request #7492 from zoonru/all_template_parameters

This commit is contained in:
Bruce Weirdan 2022-01-31 12:25:51 +02:00 committed by GitHub
commit 958e3cc12b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 481 additions and 262 deletions

View File

@ -122,6 +122,7 @@
- [BC] Method `Psalm\Type\Union::hasFormerStaticObject()` was renamed to `hasStaticObject()`
- [BC] Function assertions (from `@psalm-assert Foo $bar`) have been converted from strings to specific `Assertion` objects.
- [BC] Property `Psalm\Storage\ClassLikeStorage::$invalid_dependencies` changed from `array<string>` to `array<string, true>`.
- [BC] Property `Psalm\Storage\ClassLikeStorage::$template_extended_count` was renamed to `$template_type_extends_count`, its type was changed from `int|null` to `array<string, int>|null`.
- [BC] Event classes became final and their constructors were marked `@internal`:
- `Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent`
- `Psalm\Plugin\EventHandler\Event\AfterAnalysisEvent`

View File

@ -6,5 +6,8 @@ implemented by implementing either `IteratorAggregate` or `Iterator`
```php
<?php
/**
* @implements Traversable<mixed, mixed>
*/
final class C implements Traversable {} // will cause fatal error
```

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="dev-master@093edcbe1d5c90ad574efcddd62fe741819e7cf2">
<files psalm-version="dev-master@485e516088a22efd18e902926a2efdad1b040d72">
<file src="examples/TemplateChecker.php">
<PossiblyUndefinedIntArrayOffset occurrences="2">
<code>$comment_block-&gt;tags['variablesfrom'][0]</code>
@ -18,13 +18,6 @@
<code>$symbol_parts[1]</code>
</PossiblyUndefinedIntArrayOffset>
</file>
<file src="src/Psalm/Config.php">
<DeprecatedMethod occurrences="3">
<code>getAdditionalFileExtensions</code>
<code>getAdditionalFileTypeAnalyzers</code>
<code>getAdditionalFileTypeScanners</code>
</DeprecatedMethod>
</file>
<file src="src/Psalm/Config/FileFilter.php">
<PossiblyUndefinedIntArrayOffset occurrences="1">
<code>explode('::', $method_id)[1]</code>
@ -38,10 +31,6 @@
</PossiblyUndefinedIntArrayOffset>
</file>
<file src="src/Psalm/Internal/Analyzer/ClassAnalyzer.php">
<DeprecatedProperty occurrences="2">
<code>$storage-&gt;template_extended_count</code>
<code>$storage-&gt;template_extended_count</code>
</DeprecatedProperty>
<PossiblyUndefinedIntArrayOffset occurrences="3">
<code>$comments[0]</code>
<code>$stmt-&gt;props[0]</code>
@ -61,11 +50,6 @@
<code>$iterator_atomic_type-&gt;type_params[1]</code>
</PossiblyUndefinedIntArrayOffset>
</file>
<file src="src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php">
<ReferenceConstraintViolation occurrences="1">
<code>$newly_reconciled_var_ids</code>
</ReferenceConstraintViolation>
</file>
<file src="src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php">
<PossiblyUndefinedIntArrayOffset occurrences="1">
<code>$pre_conditions[0]</code>
@ -274,9 +258,6 @@
</PossiblyUndefinedIntArrayOffset>
</file>
<file src="src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php">
<DeprecatedProperty occurrences="1">
<code>$storage-&gt;template_extended_count</code>
</DeprecatedProperty>
<PossiblyUndefinedIntArrayOffset occurrences="3">
<code>$l[4]</code>
<code>$r[4]</code>
@ -361,12 +342,6 @@
<code>VirtualConst</code>
</PropertyNotSetInConstructor>
</file>
<file src="src/Psalm/PluginRegistrationSocket.php">
<DeprecatedMethod occurrences="2">
<code>addFileExtension</code>
<code>addFileExtension</code>
</DeprecatedMethod>
</file>
<file src="src/Psalm/Type/Atomic.php">
<PossiblyUndefinedIntArrayOffset occurrences="1">
<code>array_keys($template_type_map[$value])[0]</code>

View File

@ -39,14 +39,12 @@ use Psalm\Issue\InaccessibleMethod;
use Psalm\Issue\InternalClass;
use Psalm\Issue\InvalidEnumCaseValue;
use Psalm\Issue\InvalidExtendClass;
use Psalm\Issue\InvalidTemplateParam;
use Psalm\Issue\InvalidTraversableImplementation;
use Psalm\Issue\MethodSignatureMismatch;
use Psalm\Issue\MismatchingDocblockPropertyType;
use Psalm\Issue\MissingConstructor;
use Psalm\Issue\MissingImmutableAnnotation;
use Psalm\Issue\MissingPropertyType;
use Psalm\Issue\MissingTemplateParam;
use Psalm\Issue\MutableDependency;
use Psalm\Issue\NoEnumProperties;
use Psalm\Issue\NonInvariantDocblockPropertyType;
@ -55,7 +53,6 @@ use Psalm\Issue\OverriddenPropertyAccess;
use Psalm\Issue\ParseError;
use Psalm\Issue\PropertyNotSetInConstructor;
use Psalm\Issue\ReservedWord;
use Psalm\Issue\TooManyTemplateParams;
use Psalm\Issue\UndefinedClass;
use Psalm\Issue\UndefinedInterface;
use Psalm\Issue\UndefinedTrait;
@ -87,7 +84,6 @@ use function array_keys;
use function array_map;
use function array_merge;
use function array_pop;
use function array_search;
use function array_values;
use function assert;
use function count;
@ -586,18 +582,16 @@ class ClassAnalyzer extends ClassLikeAnalyzer
$fq_trait_name_lc = strtolower($fq_trait_name);
if (isset($storage->template_type_uses_count[$fq_trait_name_lc])) {
$this->checkTemplateParams(
$codebase,
$storage,
$trait_storage,
new CodeLocation(
$this,
$trait
),
$storage->template_type_uses_count[$fq_trait_name_lc]
);
}
$this->checkTemplateParams(
$codebase,
$storage,
$trait_storage,
new CodeLocation(
$this,
$trait
),
$storage->template_type_uses_count[$fq_trait_name_lc] ?? 0
);
foreach ($trait_node->stmts as $trait_stmt) {
if ($trait_stmt instanceof PhpParser\Node\Stmt\Property) {
@ -1974,178 +1968,6 @@ class ClassAnalyzer extends ClassLikeAnalyzer
);
}
private function checkTemplateParams(
Codebase $codebase,
ClassLikeStorage $storage,
ClassLikeStorage $parent_storage,
CodeLocation $code_location,
int $given_param_count
): void {
$expected_param_count = $parent_storage->template_types === null
? 0
: count($parent_storage->template_types);
if ($expected_param_count > $given_param_count) {
IssueBuffer::maybeAdd(
new MissingTemplateParam(
$storage->name . ' has missing template params when extending ' . $parent_storage->name
. ' , expecting ' . $expected_param_count,
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
);
} elseif ($expected_param_count < $given_param_count) {
IssueBuffer::maybeAdd(
new TooManyTemplateParams(
$storage->name . ' has too many template params when extending ' . $parent_storage->name
. ' , expecting ' . $expected_param_count,
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
);
}
$storage_param_count = ($storage->template_types ? count($storage->template_types) : 0);
if ($parent_storage->enforce_template_inheritance
&& $expected_param_count !== $storage_param_count
) {
if ($expected_param_count > $storage_param_count) {
IssueBuffer::maybeAdd(
new MissingTemplateParam(
$storage->name . ' requires the same number of template params as ' . $parent_storage->name
. ' but saw ' . $storage_param_count,
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
);
} else {
IssueBuffer::maybeAdd(
new TooManyTemplateParams(
$storage->name . ' requires the same number of template params as ' . $parent_storage->name
. ' but saw ' . $storage_param_count,
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
);
}
}
if ($parent_storage->template_types && $storage->template_extended_params) {
$i = 0;
$previous_extended = [];
foreach ($parent_storage->template_types as $template_name => $type_map) {
// declares the variables
foreach ($type_map as $declaring_class => $template_type) {
}
if (isset($storage->template_extended_params[$parent_storage->name][$template_name])) {
$extended_type = $storage->template_extended_params[$parent_storage->name][$template_name];
if (isset($parent_storage->template_covariants[$i])
&& !$parent_storage->template_covariants[$i]
) {
foreach ($extended_type->getAtomicTypes() as $t) {
if ($t instanceof TTemplateParam
&& $storage->template_types
&& $storage->template_covariants
&& ($local_offset
= array_search($t->param_name, array_keys($storage->template_types)))
!== false
&& !empty($storage->template_covariants[$local_offset])
) {
IssueBuffer::maybeAdd(
new InvalidTemplateParam(
'Cannot extend an invariant template param ' . $template_name
. ' into a covariant context',
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
);
}
}
}
if ($parent_storage->enforce_template_inheritance) {
foreach ($extended_type->getAtomicTypes() as $t) {
if (!$t instanceof TTemplateParam
|| !isset($storage->template_types[$t->param_name])
) {
IssueBuffer::maybeAdd(
new InvalidTemplateParam(
'Cannot extend a strictly-enforced parent template param '
. $template_name
. ' with a non-template type',
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
);
} elseif ($storage->template_types[$t->param_name][$storage->name]->getId()
!== $template_type->getId()
) {
IssueBuffer::maybeAdd(
new InvalidTemplateParam(
'Cannot extend a strictly-enforced parent template param '
. $template_name
. ' with constraint ' . $template_type->getId()
. ' with a child template param ' . $t->param_name
. ' with different constraint '
. $storage->template_types[$t->param_name][$storage->name]->getId(),
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
);
}
}
}
if (!$template_type->isMixed()) {
$template_type_copy = clone $template_type;
$template_result = new TemplateResult(
$previous_extended ?: [],
[]
);
$template_type_copy = TemplateStandinTypeReplacer::replace(
$template_type_copy,
$template_result,
$codebase,
null,
$extended_type,
null,
null
);
if (!UnionTypeComparator::isContainedBy($codebase, $extended_type, $template_type_copy)) {
IssueBuffer::maybeAdd(
new InvalidTemplateParam(
'Extended template param ' . $template_name
. ' expects type ' . $template_type_copy->getId()
. ', type ' . $extended_type->getId() . ' given',
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
);
} else {
$previous_extended[$template_name] = [
$declaring_class => $extended_type
];
}
} else {
$previous_extended[$template_name] = [
$declaring_class => $extended_type
];
}
}
$i++;
}
}
}
/**
* @param PhpParser\Node\Stmt\Class_|PhpParser\Node\Stmt\Enum_ $class
*/
@ -2234,15 +2056,13 @@ class ClassAnalyzer extends ClassLikeAnalyzer
);
}
if (isset($storage->template_type_implements_count[$fq_interface_name_lc])) {
$this->checkTemplateParams(
$codebase,
$storage,
$interface_storage,
$code_location,
$storage->template_type_implements_count[$fq_interface_name_lc]
);
}
$this->checkTemplateParams(
$codebase,
$storage,
$interface_storage,
$code_location,
$storage->template_type_implements_count[$fq_interface_name_lc] ?? 0
);
}
foreach ($storage->class_implements as $fq_interface_name_lc => $fq_interface_name) {
@ -2555,22 +2375,20 @@ class ClassAnalyzer extends ClassLikeAnalyzer
);
}
if ($storage->template_extended_count !== null || $parent_class_storage->enforce_template_inheritance) {
$code_location = new CodeLocation(
$this,
$class->name ?: $class,
$class_context->include_location ?? null,
true
);
$code_location = new CodeLocation(
$this,
$class->name ?: $class,
$class_context->include_location ?? null,
true
);
$this->checkTemplateParams(
$codebase,
$storage,
$parent_class_storage,
$code_location,
$storage->template_extended_count ?? 0
);
}
$this->checkTemplateParams(
$codebase,
$storage,
$parent_class_storage,
$code_location,
$storage->template_type_extends_count[$parent_fq_class_name] ?? 0
);
} catch (InvalidArgumentException $e) {
// do nothing
}

View File

@ -10,10 +10,16 @@ use Psalm\Codebase;
use Psalm\Context;
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\Internal\Provider\NodeDataProvider;
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
use Psalm\Internal\Type\TemplateResult;
use Psalm\Internal\Type\TemplateStandinTypeReplacer;
use Psalm\Issue\InaccessibleProperty;
use Psalm\Issue\InvalidClass;
use Psalm\Issue\InvalidTemplateParam;
use Psalm\Issue\MissingDependency;
use Psalm\Issue\MissingTemplateParam;
use Psalm\Issue\ReservedWord;
use Psalm\Issue\TooManyTemplateParams;
use Psalm\Issue\UndefinedAttributeClass;
use Psalm\Issue\UndefinedClass;
use Psalm\Issue\UndefinedDocblockClass;
@ -22,10 +28,14 @@ use Psalm\Plugin\EventHandler\Event\AfterClassLikeExistenceCheckEvent;
use Psalm\StatementsSource;
use Psalm\Storage\ClassLikeStorage;
use Psalm\Type;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Union;
use UnexpectedValueException;
use function array_keys;
use function array_pop;
use function array_search;
use function count;
use function explode;
use function gettype;
use function implode;
@ -629,6 +639,178 @@ abstract class ClassLikeAnalyzer extends SourceAnalyzer
return $emit_issues ? null : true;
}
protected function checkTemplateParams(
Codebase $codebase,
ClassLikeStorage $storage,
ClassLikeStorage $parent_storage,
CodeLocation $code_location,
int $given_param_count
): void {
$expected_param_count = $parent_storage->template_types === null
? 0
: count($parent_storage->template_types);
if ($expected_param_count > $given_param_count) {
IssueBuffer::maybeAdd(
new MissingTemplateParam(
$storage->name . ' has missing template params when extending ' . $parent_storage->name
. ', expecting ' . $expected_param_count,
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
);
} elseif ($expected_param_count < $given_param_count) {
IssueBuffer::maybeAdd(
new TooManyTemplateParams(
$storage->name . ' has too many template params when extending ' . $parent_storage->name
. ', expecting ' . $expected_param_count,
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
);
}
$storage_param_count = ($storage->template_types ? count($storage->template_types) : 0);
if ($parent_storage->enforce_template_inheritance
&& $expected_param_count !== $storage_param_count
) {
if ($expected_param_count > $storage_param_count) {
IssueBuffer::maybeAdd(
new MissingTemplateParam(
$storage->name . ' requires the same number of template params as ' . $parent_storage->name
. ' but saw ' . $storage_param_count,
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
);
} else {
IssueBuffer::maybeAdd(
new TooManyTemplateParams(
$storage->name . ' requires the same number of template params as ' . $parent_storage->name
. ' but saw ' . $storage_param_count,
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
);
}
}
if ($parent_storage->template_types && $storage->template_extended_params) {
$i = 0;
$previous_extended = [];
foreach ($parent_storage->template_types as $template_name => $type_map) {
// declares the variables
foreach ($type_map as $declaring_class => $template_type) {
}
if (isset($storage->template_extended_params[$parent_storage->name][$template_name])) {
$extended_type = $storage->template_extended_params[$parent_storage->name][$template_name];
if (isset($parent_storage->template_covariants[$i])
&& !$parent_storage->template_covariants[$i]
) {
foreach ($extended_type->getAtomicTypes() as $t) {
if ($t instanceof TTemplateParam
&& $storage->template_types
&& $storage->template_covariants
&& ($local_offset
= array_search($t->param_name, array_keys($storage->template_types)))
!== false
&& !empty($storage->template_covariants[$local_offset])
) {
IssueBuffer::maybeAdd(
new InvalidTemplateParam(
'Cannot extend an invariant template param ' . $template_name
. ' into a covariant context',
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
);
}
}
}
if ($parent_storage->enforce_template_inheritance) {
foreach ($extended_type->getAtomicTypes() as $t) {
if (!$t instanceof TTemplateParam
|| !isset($storage->template_types[$t->param_name])
) {
IssueBuffer::maybeAdd(
new InvalidTemplateParam(
'Cannot extend a strictly-enforced parent template param '
. $template_name
. ' with a non-template type',
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
);
} elseif ($storage->template_types[$t->param_name][$storage->name]->getId()
!== $template_type->getId()
) {
IssueBuffer::maybeAdd(
new InvalidTemplateParam(
'Cannot extend a strictly-enforced parent template param '
. $template_name
. ' with constraint ' . $template_type->getId()
. ' with a child template param ' . $t->param_name
. ' with different constraint '
. $storage->template_types[$t->param_name][$storage->name]->getId(),
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
);
}
}
}
if (!$template_type->isMixed()) {
$template_type_copy = clone $template_type;
$template_result = new TemplateResult(
$previous_extended ?: [],
[]
);
$template_type_copy = TemplateStandinTypeReplacer::replace(
$template_type_copy,
$template_result,
$codebase,
null,
$extended_type,
null,
null
);
if (!UnionTypeComparator::isContainedBy($codebase, $extended_type, $template_type_copy)) {
IssueBuffer::maybeAdd(
new InvalidTemplateParam(
'Extended template param ' . $template_name
. ' expects type ' . $template_type_copy->getId()
. ', type ' . $extended_type->getId() . ' given',
$code_location
),
$storage->suppressed_issues + $this->getSuppressedIssues()
);
} else {
$previous_extended[$template_name] = [
$declaring_class => $extended_type
];
}
} else {
$previous_extended[$template_name] = [
$declaring_class => $extended_type
];
}
}
$i++;
}
}
}
/**
* @return array<string, string>
*/

View File

@ -43,6 +43,14 @@ class InterfaceAnalyzer extends ClassLikeAnalyzer
$codebase = $project_analyzer->getCodebase();
$config = $project_analyzer->getConfig();
$fq_interface_name = $this->getFQCLN();
if (!$fq_interface_name) {
throw new UnexpectedValueException('bad');
}
$class_storage = $codebase->classlike_storage_provider->get($fq_interface_name);
if ($this->class->extends) {
foreach ($this->class->extends as $extended_interface) {
$extended_interface_name = self::getFQCLNFromNameObject(
@ -66,12 +74,12 @@ class InterfaceAnalyzer extends ClassLikeAnalyzer
continue;
}
if (!$extended_interface_storage->is_interface) {
$code_location = new CodeLocation(
$this,
$extended_interface
);
$code_location = new CodeLocation(
$this,
$extended_interface
);
if (!$extended_interface_storage->is_interface) {
IssueBuffer::maybeAdd(
new UndefinedInterface(
$extended_interface_name . ' is not an interface',
@ -92,17 +100,17 @@ class InterfaceAnalyzer extends ClassLikeAnalyzer
$extended_interface_name
);
}
$this->checkTemplateParams(
$codebase,
$class_storage,
$extended_interface_storage,
$code_location,
$class_storage->template_type_extends_count[$extended_interface_name] ?? 0
);
}
}
$fq_interface_name = $this->getFQCLN();
if (!$fq_interface_name) {
throw new UnexpectedValueException('bad');
}
$class_storage = $codebase->classlike_storage_provider->get($fq_interface_name);
foreach ($class_storage->attributes as $attribute) {
AttributeAnalyzer::analyze(
$this,

View File

@ -988,8 +988,8 @@ class ClassLikeNodeScanner
$extended_type_parameters = [];
$storage->template_extended_count = count($atomic_type->type_params);
$storage->template_type_extends_count[$atomic_type->value] = count($atomic_type->type_params);
foreach ($atomic_type->type_params as $type_param) {
$extended_type_parameters[] = $type_param;
}

View File

@ -361,10 +361,10 @@ class ClassLikeStorage
public $template_extended_params;
/**
* @deprecated Will be replaced with $template_type_extends_count in Psalm v5
* @var ?int
* @var array<string, int>|null
*/
public $template_extended_count;
public $template_type_extends_count;
/**
* @var array<string, int>|null

View File

@ -788,6 +788,9 @@ class ArrayAccessTest extends TestCase
$c = isset($array["foo"]) ? $array["foo"] : null;
/**
* @psalm-suppress MissingTemplateParam
*/
class C implements ArrayAccess
{
/**
@ -944,6 +947,9 @@ class ArrayAccessTest extends TestCase
public function foo() : void {}
}
/**
* @psalm-suppress MissingTemplateParam
*/
class C implements ArrayAccess
{
/**

View File

@ -689,6 +689,9 @@ class ArrayAssignmentTest extends TestCase
],
'implementsArrayAccess' => [
'code' => '<?php
/**
* @implements \ArrayAccess<array-key, mixed>
*/
class A implements \ArrayAccess {
/**
* @param string|int $offset
@ -734,6 +737,9 @@ class ArrayAssignmentTest extends TestCase
],
'implementsArrayAccessInheritingDocblock' => [
'code' => '<?php
/**
* @implements \ArrayAccess<string, mixed>
*/
class A implements \ArrayAccess
{
/**

View File

@ -208,6 +208,9 @@ class AttributeTest extends TestCase
use IteratorAggregate;
use ReturnTypeWillChange;
/**
* @psalm-suppress MissingTemplateParam
*/
final class EmptyCollection implements IteratorAggregate
{
#[ReturnTypeWillChange]
@ -229,6 +232,9 @@ class AttributeTest extends TestCase
use IteratorAggregate;
use ReturnTypeWillChange;
/**
* @psalm-suppress MissingTemplateParam
*/
final class EmptyCollection implements IteratorAggregate
{
#[ReturnTypeWillChange]

View File

@ -613,6 +613,10 @@ class ClassTest extends TestCase
],
'allowTraversableImplementationAlongWithIteratorAggregate' => [
'code' => '<?php
/**
* @implements Traversable<int, 1>
* @implements IteratorAggregate<int, 1>
*/
final class C implements Traversable, IteratorAggregate {
public function getIterator() {
yield 1;
@ -622,6 +626,10 @@ class ClassTest extends TestCase
],
'allowTraversableImplementationAlongWithIterator' => [
'code' => '<?php
/**
* @implements Traversable<1, 1>
* @implements Iterator<1, 1>
*/
final class C implements Traversable, Iterator {
public function current() { return 1; }
public function key() { return 1; }
@ -633,11 +641,20 @@ class ClassTest extends TestCase
],
'allowTraversableImplementationOnAbstractClass' => [
'code' => '<?php
/**
* @template TKey
* @template TValue
*
* @implements Traversable<TKey, TValue>
*/
abstract class C implements Traversable {}
',
],
'allowIndirectTraversableImplementationOnAbstractClass' => [
'code' => '<?php
/**
* @extends Traversable<int, int>
*/
interface I extends Traversable {}
abstract class C implements I {}
',
@ -803,7 +820,15 @@ class ClassTest extends TestCase
],
'abstractReflectedClassMethod' => [
'code' => '<?php
/**
* @template TKey
* @template TValue
* @extends FilterIterator<TKey, TValue, Iterator<TKey, TValue>>
*/
class DedupeIterator extends FilterIterator {
/**
* @param Iterator<TKey, TValue> $i
*/
public function __construct(Iterator $i) {
parent::__construct($i);
}
@ -908,17 +933,77 @@ class ClassTest extends TestCase
],
'preventTraversableImplementation' => [
'code' => '<?php
/**
* @implements Traversable<int, int>
*/
final class C implements Traversable {}
',
'error_message' => 'InvalidTraversableImplementation',
],
'preventIndirectTraversableImplementation' => [
'code' => '<?php
/**
* @extends Traversable<int, int>
*/
interface I extends Traversable {}
final class C implements I {}
',
'error_message' => 'InvalidTraversableImplementation',
],
'detectMissingTemplateExtends' => [
'code' => '<?php
/** @template T */
abstract class A {}
final class B extends A {}
',
'error_message' => 'MissingTemplateParam',
],
'detectMissingTemplateImplements' => [
'code' => '<?php
/** @template T */
interface A {}
final class B implements A {}
',
'error_message' => 'MissingTemplateParam',
],
'detectMissingTemplateUse' => [
'code' => '<?php
/** @template T */
trait A {}
final class B {
use A;
}
',
'error_message' => 'MissingTemplateParam',
],
'detectMissingTemplateExtendsNative' => [
'code' => '<?php
final class C extends ArrayObject {}
',
'error_message' => 'MissingTemplateParam',
],
'detectMissingTemplateImplementsNative' => [
'code' => '<?php
final class C implements Iterator {
public function current(): mixed {
return 0;
}
public function key(): mixed {
return 0;
}
public function next(): void {
}
public function rewind(): void {
}
public function valid(): bool {
return false;
}
}
',
'error_message' => 'MissingTemplateParam',
],
];
}
}

View File

@ -517,6 +517,9 @@ class FunctionCallTest extends TestCase
],
'iteratorToArrayWithGetIterator' => [
'code' => '<?php
/**
* @implements IteratorAggregate<int, string>
*/
class C implements IteratorAggregate {
/**
* @return Traversable<int,string>
@ -532,9 +535,12 @@ class FunctionCallTest extends TestCase
],
'iteratorToArrayWithGetIteratorReturningList' => [
'code' => '<?php
/**
* @implements IteratorAggregate<int, string>
*/
class C implements IteratorAggregate {
/**
* @return Traversable<int,string>
* @return Traversable<int, string>
*/
public function getIterator() {
yield 1 => "1";

View File

@ -307,6 +307,11 @@ class InterfaceTest extends TestCase
],
'extendIteratorIterator' => [
'code' => '<?php
/**
* @template TKey
* @template TValue
* @extends IteratorIterator<TKey, TValue, Traversable<TKey, TValue>>
*/
class SomeIterator extends IteratorIterator {}',
],
'SKIPPED-suppressMismatch' => [
@ -395,6 +400,10 @@ class InterfaceTest extends TestCase
],
'interfaceExtendsTraversible' => [
'code' => '<?php
/**
* @extends IteratorAggregate<mixed, mixed>
* @extends ArrayAccess<mixed, mixed>
*/
interface Collection extends Countable, IteratorAggregate, ArrayAccess {}
function takesCollection(Collection $c): void {
@ -429,8 +438,14 @@ class InterfaceTest extends TestCase
],
'filterIteratorExtension' => [
'code' => '<?php
/**
* @extends Iterator<mixed, mixed>
*/
interface I2 extends Iterator {}
/**
* @extends FilterIterator<mixed, mixed, Iterator<mixed, mixed>>
*/
class DedupeIterator extends FilterIterator {
public function __construct(I2 $i) {
parent::__construct($i);
@ -964,6 +979,31 @@ class InterfaceTest extends TestCase
}',
'error_message' => 'MissingParamType'
],
'missingTemplateExtendsInterface' => [
'code' => '<?php
/** @template T */
interface A {}
interface B extends A {}
',
'error_message' => 'MissingTemplateParam',
],
'missingTemplateExtendsNativeInterface' => [
'code' => '<?php
interface a extends Iterator {
}
',
'error_message' => 'MissingTemplateParam',
],
'missingTemplateExtendsNativeMultipleInterface' => [
'code' => '<?php
/**
* @extends Iterator<mixed, mixed>
*/
interface a extends Iterator, Traversable {
}
',
'error_message' => 'MissingTemplateParam',
],
'reconcileAfterClassInstanceof' => [
'code' => '<?php
interface Base {}

View File

@ -686,6 +686,9 @@ class ForeachTest extends TestCase
],
'iteratorAggregateIteration' => [
'code' => '<?php
/**
* @psalm-suppress MissingTemplateParam
*/
class C implements IteratorAggregate
{
public function getIterator(): Iterator
@ -790,6 +793,9 @@ class ForeachTest extends TestCase
public $prop = "var";
}
/**
* @implements IteratorAggregate<array-key, Item>
*/
class Collection implements IteratorAggregate {
/**
* @var Item[]
@ -878,6 +884,9 @@ class ForeachTest extends TestCase
'code' => '<?php
class Value {}
/**
* @psalm-suppress MissingTemplateParam
*/
class ValueCollection implements \Countable, \IteratorAggregate {
/**
* @var Value[]
@ -924,6 +933,9 @@ class ForeachTest extends TestCase
}
}
/**
* @psalm-suppress MissingTemplateParam
*/
class ValueCollectionIterator implements \Countable, \Iterator
{
/**

View File

@ -59,6 +59,7 @@ class MagicMethodAnnotationTest extends TestCase
public function find() {}
}
/** @psalm-suppress MissingTemplateParam */
class B extends A {}
class Obj {}

View File

@ -604,6 +604,9 @@ class MethodSignatureTest extends TestCase
],
'allowMixedExtensionOfIteratorAggregate' => [
'code' => '<?php
/**
* @psalm-suppress MissingTemplateParam
*/
class C implements IteratorAggregate {
public function getIterator(): Iterator {
return new ArrayIterator([]);

View File

@ -118,6 +118,9 @@ class MixinAnnotationTest extends TestCase
],
'wrapCustomIterator' => [
'code' => '<?php
/**
* @implements Iterator<1, 2>
*/
class Subject implements Iterator {
/**
* the index method exists

View File

@ -197,6 +197,9 @@ class Php71Test extends TestCase
],
'traversableObject' => [
'code' => '<?php
/**
* @implements Iterator<0, mixed>
*/
class IteratorObj implements Iterator {
function rewind(): void {}
/** @return mixed */

View File

@ -335,6 +335,9 @@ class PropertyTypeInvarianceTest extends TestCase
protected $b;
}
/**
* @psalm-suppress MissingTemplateParam
*/
class FooPair extends Pair
{
/** @var Foo|null */ // Template defaults to mixed, this is invariant

View File

@ -1186,6 +1186,9 @@ class StubTest extends TestCase
public function find($id, $lockMode = null, $lockVersion = null) {}
}
/**
* @psalm-suppress MissingTemplateParam
*/
class B extends A {}
class Obj {}

View File

@ -679,6 +679,9 @@ class ClassTemplateExtendsTest extends TestCase
*/
class Collection1 extends ArrayIterator{}
/**
* @psalm-suppress MissingTemplateParam
*/
class Collection2 extends Collection1{}
class Collection3 extends Collection2{}
@ -1372,6 +1375,8 @@ class ClassTemplateExtendsTest extends TestCase
/**
* @template T
*
* @psalm-suppress MissingTemplateParam
*/
class C implements I {
/** @var array<T> */
@ -1411,6 +1416,8 @@ class ClassTemplateExtendsTest extends TestCase
/**
* @template T2
*
* @psalm-suppress MissingTemplateParam
*/
class C implements I {
/** @var array<T2> */
@ -2059,9 +2066,17 @@ class ClassTemplateExtendsTest extends TestCase
public function getIterator();
}
/**
* @template TKey as array-key
* @template TValue
* @implements ICollection<TKey, TValue>
*/
class Collection implements ICollection {
/** @var array */
/** @var array<TKey, TValue> */
private $data;
/**
* @param array<TKey, TValue> $data
*/
public function __construct(array $data) {
$this->data = $data;
}
@ -2070,7 +2085,6 @@ class ClassTemplateExtendsTest extends TestCase
}
}
/** @var ICollection<string, int> */
$c = new Collection(["a" => 1]);
foreach ($c->getIterator() as $k => $v) { atan($v); strlen($k); }',
@ -2233,6 +2247,11 @@ class ClassTemplateExtendsTest extends TestCase
}
}
/**
* @template TT
*
* @extends Container<TT>
*/
class MyContainer extends Container {}
$a = (new MyContainer("hello"))->getAnother();',
@ -2608,6 +2627,9 @@ class ClassTemplateExtendsTest extends TestCase
}
}
/**
* @psalm-suppress MissingTemplateParam
*/
class AChild extends A {
/**
* @template T3
@ -3707,6 +3729,9 @@ class ClassTemplateExtendsTest extends TestCase
public function foo();
}
/**
* @psalm-suppress MissingTemplateParam
*/
class Concrete implements Templated {
private array $t;

View File

@ -531,24 +531,37 @@ class ClassTemplateTest extends TestCase
/**
* @template TKey
* @template TValue
*
* @extends \IteratorAggregate<TKey, TValue>
*/
interface ICollection extends \IteratorAggregate {
/** @return \Traversable<TKey,TValue> */
public function getIterator();
}
/**
* @template TKey as array-key
* @template TValue
*
* @implements ICollection<TKey, TValue>
*/
class Collection implements ICollection {
/** @var array */
/** @var array<TKey, TValue> */
private $data;
/**
* @param array<TKey, TValue> $data
*/
public function __construct(array $data) {
$this->data = $data;
}
/**
* @return \Traversable<TKey, TValue>
*/
public function getIterator(): \Traversable {
return new \ArrayIterator($this->data);
}
}
/** @var ICollection<string, int> */
$c = new Collection(["a" => 1]);
foreach ($c as $k => $v) { atan($v); strlen($k); }',
@ -862,6 +875,7 @@ class ClassTemplateTest extends TestCase
}
/**
* @psalm-suppress MissingTemplateParam
* @template TKey
* @template TValue
*/
@ -926,6 +940,8 @@ class ClassTemplateTest extends TestCase
/**
* @template TKey
* @template Tv
*
* @psalm-suppress MissingTemplateParam
*/
class KeyValueContainer extends ValueContainer
{
@ -985,6 +1001,9 @@ class ClassTemplateTest extends TestCase
}
}
/**
* @psalm-suppress MissingTemplateParam
*/
class AppUser extends User {}
$au = new AppUser(-1);
@ -1732,6 +1751,7 @@ class ClassTemplateTest extends TestCase
function foo();
}
/** @psalm-suppress MissingTemplateParam */
interface IChild extends IParent {}
class C {}
@ -1766,6 +1786,8 @@ class ClassTemplateTest extends TestCase
* @psalm-param class-string<T> $className
*
* @psalm-return T&I<T>
*
* @psalm-suppress MissingTemplateParam
*/
function makeConcrete(string $className) : object
{
@ -1926,6 +1948,7 @@ class ClassTemplateTest extends TestCase
/** @psalm-param T $val */
public function set($val) : void {
$this->value = $val;
/** @psalm-suppress MissingTemplateParam */
new class extends Foo {};
}

View File

@ -480,6 +480,9 @@ class FunctionTemplateTest extends TestCase
'code' => '<?php
class Foo {}
/**
* @psalm-suppress MissingTemplateParam
*/
class SomeIterator implements IteratorAggregate
{
public function getIterator() {

View File

@ -725,6 +725,9 @@ class TraitTemplateTest extends TestCase
use T1;
}
/**
* @psalm-suppress MissingTemplateParam
*/
class D extends C {
/**
* @param mixed $offset

View File

@ -739,7 +739,7 @@ class UnusedCodeTest extends TestCase
],
'useIteratorMethodsWhenCallingForeach' => [
'code' => '<?php
/** @psalm-suppress UnimplementedInterfaceMethod */
/** @psalm-suppress UnimplementedInterfaceMethod, MissingTemplateParam */
class IterableResult implements \Iterator {
public function current() {
return null;

View File

@ -2255,6 +2255,7 @@ class UnusedVariableTest extends TestCase
'code' => '<?php
/**
* @psalm-immutable
* @psalm-suppress MissingTemplateParam
*/
class A implements IteratorAggregate
{