diff --git a/CHANGELOG.md b/CHANGELOG.md
index 09d8acf..2c2d4cc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,9 +4,11 @@ All notable changes to `phpcs-type-sniff` will be documented in this file.
Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
-## Unreleased
+## 81.6.0 - 2024-05-23
### Changed
- Min `phpcs` version to 3.10
+### Added
+- `IteratorItemTypeSniff` - enabled by default
## 81.5.2 - 2024-04-05
### Changed
diff --git a/README.md b/README.md
index 5b431a5..9cb76ea 100644
--- a/README.md
+++ b/README.md
@@ -331,6 +331,7 @@ String `true/false` values are automatically converted to booleans.
+
@@ -340,6 +341,7 @@ String `true/false` values are automatically converted to booleans.
+
diff --git a/src/Sniffs/CodeElement/IteratorItemTypeSniff.php b/src/Sniffs/CodeElement/IteratorItemTypeSniff.php
new file mode 100644
index 0000000..1b43c21
--- /dev/null
+++ b/src/Sniffs/CodeElement/IteratorItemTypeSniff.php
@@ -0,0 +1,74 @@
+reportType = (string)($config['reportType'] ?? 'warning');
+ $this->addViolationId = (bool)($config['addViolationId'] ?? true);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function register(): array
+ {
+ return [
+ ClassElement::class,
+ ];
+ }
+
+ /**
+ * @inheritDoc
+ * @param AbstractFqcnElement $element
+ */
+ public function process(File $file, CodeElementInterface $element, CodeElementInterface $parentElement): void
+ {
+ try {
+ $ref = new ReflectionClass($element->getFqcn());
+ } catch (Error | ReflectionException) {
+ return; // give up...
+ }
+
+ if (!in_array(IteratorAggregate::class, $ref->getInterfaceNames())) {
+ return;
+ }
+
+ if ($element->getDocBlock()->getTagsByName('template-implements')) {
+ return;
+ } elseif ($element->getDocBlock()->getTagsByName('template-extends')) {
+ return;
+ } else {
+ $originId = $this->addViolationId ? $element->getFqcn() : null;
+ SniffHelper::addViolation(
+ $file,
+ 'Classes which implement IteratorAggregate must have "@template-implements IteratorAggregate>"'
+ . ' doc tag with a specified item type or template type',
+ $element->getLine(),
+ static::CODE,
+ $this->reportType,
+ $originId
+ );
+ }
+ }
+}
diff --git a/src/Sniffs/CompositeCodeElementSniff.php b/src/Sniffs/CompositeCodeElementSniff.php
index 7c9462c..0a97b12 100644
--- a/src/Sniffs/CompositeCodeElementSniff.php
+++ b/src/Sniffs/CompositeCodeElementSniff.php
@@ -6,6 +6,7 @@
use Gskema\TypeSniff\Core\CodeElement\Element\CodeElementInterface;
use Gskema\TypeSniff\Core\CodeElement\Element\FileElement;
use Gskema\TypeSniff\Sniffs\CodeElement\FqcnDescriptionSniff;
+use Gskema\TypeSniff\Sniffs\CodeElement\IteratorItemTypeSniff;
use PHP_CodeSniffer\Files\File;
use Gskema\TypeSniff\Core\CodeElement\CodeElementDetector;
use Gskema\TypeSniff\Sniffs\CodeElement\CodeElementSniffInterface;
@@ -42,6 +43,7 @@ protected function configure(array $config): void
$config['sniffs'][] = FqcnPropSniff::class;
$config['sniffs'][] = FqcnConstSniff::class;
$config['sniffs'][] = FqcnDescriptionSniff::class;
+ $config['sniffs'][] = IteratorItemTypeSniff::class;
// CodeElementSniff(s) are saved by their short name, meaning you can't have 2 instances of same sniff.
$rawSniffs = [];
diff --git a/tests/Sniffs/CompositeCodeElementSniffTest.php b/tests/Sniffs/CompositeCodeElementSniffTest.php
index ac8c3df..845b17a 100644
--- a/tests/Sniffs/CompositeCodeElementSniffTest.php
+++ b/tests/Sniffs/CompositeCodeElementSniffTest.php
@@ -566,6 +566,18 @@ public static function dataProcess(): array
],
];
+ // #25
+ $dataSets[] = [
+ [
+ 'addViolationId' => false,
+ 'useReflection' => false,
+ ],
+ __DIR__ . '/fixtures/TestIterator0.php',
+ [
+ '009 Classes which implement IteratorAggregate must have "@template-implements IteratorAggregate>" doc tag with a specified item type or template type'
+ ],
+ ];
+
return $dataSets;
}
diff --git a/tests/Sniffs/fixtures/TestIterator0.php b/tests/Sniffs/fixtures/TestIterator0.php
new file mode 100644
index 0000000..eb65800
--- /dev/null
+++ b/tests/Sniffs/fixtures/TestIterator0.php
@@ -0,0 +1,15 @@
+