diff --git a/UPGRADING.md b/UPGRADING.md index a26b6f861e4..1b4b7e07161 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -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` to `array`. +- [BC] Property `Psalm\Storage\ClassLikeStorage::$template_extended_count` was renamed to `$template_type_extends_count`, its type was changed from `int|null` to `array|null`. - [BC] Event classes became final and their constructors were marked `@internal`: - `Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent` - `Psalm\Plugin\EventHandler\Event\AfterAnalysisEvent` diff --git a/docs/running_psalm/issues/InvalidTraversableImplementation.md b/docs/running_psalm/issues/InvalidTraversableImplementation.md index 52fc8b02046..e275818f702 100644 --- a/docs/running_psalm/issues/InvalidTraversableImplementation.md +++ b/docs/running_psalm/issues/InvalidTraversableImplementation.md @@ -6,5 +6,8 @@ implemented by implementing either `IteratorAggregate` or `Iterator` ```php + */ final class C implements Traversable {} // will cause fatal error ``` diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 1f7e035e144..0aa49787025 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + $comment_block->tags['variablesfrom'][0] @@ -18,13 +18,6 @@ $symbol_parts[1] - - - getAdditionalFileExtensions - getAdditionalFileTypeAnalyzers - getAdditionalFileTypeScanners - - explode('::', $method_id)[1] @@ -38,10 +31,6 @@ - - $storage->template_extended_count - $storage->template_extended_count - $comments[0] $stmt->props[0] @@ -61,11 +50,6 @@ $iterator_atomic_type->type_params[1] - - - $newly_reconciled_var_ids - - $pre_conditions[0] @@ -274,9 +258,6 @@ - - $storage->template_extended_count - $l[4] $r[4] @@ -361,12 +342,6 @@ VirtualConst - - - addFileExtension - addFileExtension - - array_keys($template_type_map[$value])[0] diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index aa6e9f2c863..ec805929ca3 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -39,14 +39,12 @@ 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\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_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 @@ public function analyze( $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 @@ public static function analyzeClassMethodReturnType( ); } - 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 @@ private function checkImplementedInterfaces( ); } - 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 @@ private function checkParentClass( ); } - 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 } diff --git a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php index 339d5079f10..e964cce71db 100644 --- a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php @@ -10,10 +10,16 @@ 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\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 @@ public static function checkPropertyVisibility( 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 */ diff --git a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php index 53135a7ac9b..69ee47355fc 100644 --- a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php @@ -43,6 +43,14 @@ public function analyze(): void $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 @@ public function analyze(): void 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 @@ public function analyze(): void $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, diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 31104071ed6..0cb67909e86 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -988,8 +988,8 @@ private function extendTemplatedType( $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; } diff --git a/src/Psalm/Storage/ClassLikeStorage.php b/src/Psalm/Storage/ClassLikeStorage.php index 810662e0395..381f9c912a2 100644 --- a/src/Psalm/Storage/ClassLikeStorage.php +++ b/src/Psalm/Storage/ClassLikeStorage.php @@ -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|null */ - public $template_extended_count; + public $template_type_extends_count; + /** * @var array|null diff --git a/tests/ArrayAccessTest.php b/tests/ArrayAccessTest.php index c8f9b171776..7acb964070c 100644 --- a/tests/ArrayAccessTest.php +++ b/tests/ArrayAccessTest.php @@ -788,6 +788,9 @@ function getArray() : array { return []; } $c = isset($array["foo"]) ? $array["foo"] : null; + /** + * @psalm-suppress MissingTemplateParam + */ class C implements ArrayAccess { /** @@ -944,6 +947,9 @@ class A { public function foo() : void {} } + /** + * @psalm-suppress MissingTemplateParam + */ class C implements ArrayAccess { /** diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 8ba4c768e6b..c79bc1ae6ea 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -689,6 +689,9 @@ function getThings(): array { ], 'implementsArrayAccess' => [ 'code' => ' + */ class A implements \ArrayAccess { /** * @param string|int $offset @@ -734,6 +737,9 @@ public function offsetGet($offset) { ], 'implementsArrayAccessInheritingDocblock' => [ 'code' => ' + */ class A implements \ArrayAccess { /** diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index d9067b676a3..fe678c296d4 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -208,6 +208,9 @@ class Baz {} use IteratorAggregate; use ReturnTypeWillChange; + /** + * @psalm-suppress MissingTemplateParam + */ final class EmptyCollection implements IteratorAggregate { #[ReturnTypeWillChange] @@ -229,6 +232,9 @@ public function getIterator() use IteratorAggregate; use ReturnTypeWillChange; + /** + * @psalm-suppress MissingTemplateParam + */ final class EmptyCollection implements IteratorAggregate { #[ReturnTypeWillChange] diff --git a/tests/ClassTest.php b/tests/ClassTest.php index 45e767a3fc1..782660aa593 100644 --- a/tests/ClassTest.php +++ b/tests/ClassTest.php @@ -613,6 +613,10 @@ function intersect(A $a) { ], 'allowTraversableImplementationAlongWithIteratorAggregate' => [ 'code' => ' + * @implements IteratorAggregate + */ final class C implements Traversable, IteratorAggregate { public function getIterator() { yield 1; @@ -622,6 +626,10 @@ public function getIterator() { ], 'allowTraversableImplementationAlongWithIterator' => [ 'code' => ' + * @implements Iterator<1, 1> + */ final class C implements Traversable, Iterator { public function current() { return 1; } public function key() { return 1; } @@ -633,11 +641,20 @@ public function valid() { return false; } ], 'allowTraversableImplementationOnAbstractClass' => [ 'code' => ' + */ abstract class C implements Traversable {} ', ], 'allowIndirectTraversableImplementationOnAbstractClass' => [ 'code' => ' + */ interface I extends Traversable {} abstract class C implements I {} ', @@ -803,7 +820,15 @@ class B extends A { }', ], 'abstractReflectedClassMethod' => [ 'code' => '> + */ class DedupeIterator extends FilterIterator { + /** + * @param Iterator $i + */ public function __construct(Iterator $i) { parent::__construct($i); } @@ -908,17 +933,77 @@ public function foo() : void {} ], 'preventTraversableImplementation' => [ 'code' => ' + */ final class C implements Traversable {} ', 'error_message' => 'InvalidTraversableImplementation', ], 'preventIndirectTraversableImplementation' => [ 'code' => ' + */ interface I extends Traversable {} final class C implements I {} ', 'error_message' => 'InvalidTraversableImplementation', ], + 'detectMissingTemplateExtends' => [ + 'code' => ' 'MissingTemplateParam', + ], + 'detectMissingTemplateImplements' => [ + 'code' => ' 'MissingTemplateParam', + ], + 'detectMissingTemplateUse' => [ + 'code' => ' 'MissingTemplateParam', + ], + + 'detectMissingTemplateExtendsNative' => [ + 'code' => ' 'MissingTemplateParam', + ], + + 'detectMissingTemplateImplementsNative' => [ + 'code' => ' 'MissingTemplateParam', + ], ]; } } diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 8e55a23af2c..1a7dd1c7332 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -517,6 +517,9 @@ function generator(): Generator { ], 'iteratorToArrayWithGetIterator' => [ 'code' => ' + */ class C implements IteratorAggregate { /** * @return Traversable @@ -532,9 +535,12 @@ public function getIterator() { ], 'iteratorToArrayWithGetIteratorReturningList' => [ 'code' => ' + */ class C implements IteratorAggregate { /** - * @return Traversable + * @return Traversable */ public function getIterator() { yield 1 => "1"; diff --git a/tests/InterfaceTest.php b/tests/InterfaceTest.php index c7e0d41f0ea..cef0d576a1d 100644 --- a/tests/InterfaceTest.php +++ b/tests/InterfaceTest.php @@ -307,6 +307,11 @@ function foo(A $i): B { ], 'extendIteratorIterator' => [ 'code' => '> + */ class SomeIterator extends IteratorIterator {}', ], 'SKIPPED-suppressMismatch' => [ @@ -395,6 +400,10 @@ public function blah() { ], 'interfaceExtendsTraversible' => [ 'code' => ' + * @extends ArrayAccess + */ interface Collection extends Countable, IteratorAggregate, ArrayAccess {} function takesCollection(Collection $c): void { @@ -429,8 +438,14 @@ function bar(Throwable $e): void { ], 'filterIteratorExtension' => [ 'code' => ' + */ interface I2 extends Iterator {} + /** + * @extends FilterIterator> + */ class DedupeIterator extends FilterIterator { public function __construct(I2 $i) { parent::__construct($i); @@ -964,6 +979,31 @@ public function withoutAnyReturnType($s) : void; }', 'error_message' => 'MissingParamType' ], + 'missingTemplateExtendsInterface' => [ + 'code' => ' 'MissingTemplateParam', + ], + 'missingTemplateExtendsNativeInterface' => [ + 'code' => ' 'MissingTemplateParam', + ], + 'missingTemplateExtendsNativeMultipleInterface' => [ + 'code' => ' + */ + interface a extends Iterator, Traversable { + } + ', + 'error_message' => 'MissingTemplateParam', + ], 'reconcileAfterClassInstanceof' => [ 'code' => ' [ 'code' => ' + */ class Collection implements IteratorAggregate { /** * @var Item[] @@ -878,6 +884,9 @@ function Foo(int $a, int $b, int ...$ints) : array { 'code' => ' [ 'code' => ' [ 'code' => ' + */ class Subject implements Iterator { /** * the index method exists diff --git a/tests/Php71Test.php b/tests/Php71Test.php index 572696bf8e5..4467a991e89 100644 --- a/tests/Php71Test.php +++ b/tests/Php71Test.php @@ -197,6 +197,9 @@ function iterator(iterable $iter): void ], 'traversableObject' => [ 'code' => ' + */ class IteratorObj implements Iterator { function rewind(): void {} /** @return mixed */ diff --git a/tests/PropertyTypeInvarianceTest.php b/tests/PropertyTypeInvarianceTest.php index 179a0e40719..d85e0de5883 100644 --- a/tests/PropertyTypeInvarianceTest.php +++ b/tests/PropertyTypeInvarianceTest.php @@ -335,6 +335,9 @@ class Pair protected $b; } + /** + * @psalm-suppress MissingTemplateParam + */ class FooPair extends Pair { /** @var Foo|null */ // Template defaults to mixed, this is invariant diff --git a/tests/StubTest.php b/tests/StubTest.php index f2b8f3d33f1..736943df32f 100644 --- a/tests/StubTest.php +++ b/tests/StubTest.php @@ -1186,6 +1186,9 @@ class A { public function find($id, $lockMode = null, $lockVersion = null) {} } + /** + * @psalm-suppress MissingTemplateParam + */ class B extends A {} class Obj {} diff --git a/tests/Template/ClassTemplateExtendsTest.php b/tests/Template/ClassTemplateExtendsTest.php index 85531986aba..50ed1af3a24 100644 --- a/tests/Template/ClassTemplateExtendsTest.php +++ b/tests/Template/ClassTemplateExtendsTest.php @@ -679,6 +679,9 @@ function getFooCollection() : Collection { */ class Collection1 extends ArrayIterator{} + /** + * @psalm-suppress MissingTemplateParam + */ class Collection2 extends Collection1{} class Collection3 extends Collection2{} @@ -1372,6 +1375,8 @@ public function add($t) : void; /** * @template T + * + * @psalm-suppress MissingTemplateParam */ class C implements I { /** @var array */ @@ -1411,6 +1416,8 @@ public function add($t) : void; /** * @template T2 + * + * @psalm-suppress MissingTemplateParam */ class C implements I { /** @var array */ @@ -2059,9 +2066,17 @@ interface ICollection extends \IteratorAggregate { public function getIterator(); } + /** + * @template TKey as array-key + * @template TValue + * @implements ICollection + */ class Collection implements ICollection { - /** @var array */ + /** @var array */ private $data; + /** + * @param array $data + */ public function __construct(array $data) { $this->data = $data; } @@ -2070,7 +2085,6 @@ public function getIterator(): \Traversable { } } - /** @var ICollection */ $c = new Collection(["a" => 1]); foreach ($c->getIterator() as $k => $v) { atan($v); strlen($k); }', @@ -2233,6 +2247,11 @@ public function getAnother() { } } + /** + * @template TT + * + * @extends Container + */ class MyContainer extends Container {} $a = (new MyContainer("hello"))->getAnother();', @@ -2608,6 +2627,9 @@ public function get($t) { } } + /** + * @psalm-suppress MissingTemplateParam + */ class AChild extends A { /** * @template T3 @@ -3707,6 +3729,9 @@ interface Templated { public function foo(); } + /** + * @psalm-suppress MissingTemplateParam + */ class Concrete implements Templated { private array $t; diff --git a/tests/Template/ClassTemplateTest.php b/tests/Template/ClassTemplateTest.php index 2f963663edf..3f55dfcea7a 100644 --- a/tests/Template/ClassTemplateTest.php +++ b/tests/Template/ClassTemplateTest.php @@ -531,24 +531,37 @@ function(Collection $elt): bool { return true; } /** * @template TKey * @template TValue + * + * @extends \IteratorAggregate */ interface ICollection extends \IteratorAggregate { /** @return \Traversable */ public function getIterator(); } + /** + * @template TKey as array-key + * @template TValue + * + * @implements ICollection + */ class Collection implements ICollection { - /** @var array */ + /** @var array */ private $data; + /** + * @param array $data + */ public function __construct(array $data) { $this->data = $data; } + /** + * @return \Traversable + */ public function getIterator(): \Traversable { return new \ArrayIterator($this->data); } } - /** @var ICollection */ $c = new Collection(["a" => 1]); foreach ($c as $k => $v) { atan($v); strlen($k); }', @@ -862,6 +875,7 @@ public function getValue() } /** + * @psalm-suppress MissingTemplateParam * @template TKey * @template TValue */ @@ -926,6 +940,8 @@ public function getValue() /** * @template TKey * @template Tv + * + * @psalm-suppress MissingTemplateParam */ class KeyValueContainer extends ValueContainer { @@ -985,6 +1001,9 @@ public function getID() } } + /** + * @psalm-suppress MissingTemplateParam + */ class AppUser extends User {} $au = new AppUser(-1); @@ -1732,6 +1751,7 @@ interface IParent { function foo(); } + /** @psalm-suppress MissingTemplateParam */ interface IChild extends IParent {} class C {} @@ -1766,6 +1786,8 @@ class C {} * @psalm-param class-string $className * * @psalm-return T&I + * + * @psalm-suppress MissingTemplateParam */ function makeConcrete(string $className) : object { @@ -1926,6 +1948,7 @@ class Foo { /** @psalm-param T $val */ public function set($val) : void { $this->value = $val; + /** @psalm-suppress MissingTemplateParam */ new class extends Foo {}; } diff --git a/tests/Template/FunctionTemplateTest.php b/tests/Template/FunctionTemplateTest.php index 5c63b22b805..772dee8afa0 100644 --- a/tests/Template/FunctionTemplateTest.php +++ b/tests/Template/FunctionTemplateTest.php @@ -480,6 +480,9 @@ function () : int { 'code' => ' [ 'code' => ' '