From 0630e81b936b7dec4e9b8cb8a57c430651670bcf Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 12 Dec 2023 18:15:17 +0100 Subject: [PATCH] ForbidNotNormalizedTypeRule (#185) --- README.md | 21 + rules.neon | 14 + src/Rule/ForbidNotNormalizedTypeRule.php | 586 ++++++++++++++++++ .../Rule/ForbidNotNormalizedTypeRuleTest.php | 56 ++ .../data/ForbidNotNormalizedTypeRule/code.php | 158 +++++ 5 files changed, 835 insertions(+) create mode 100644 src/Rule/ForbidNotNormalizedTypeRule.php create mode 100644 tests/Rule/ForbidNotNormalizedTypeRuleTest.php create mode 100644 tests/Rule/data/ForbidNotNormalizedTypeRule/code.php diff --git a/README.md b/README.md index a5f8266..1aa2561 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,9 @@ parameters: enabled: true forbidMethodCallOnMixed: enabled: true + forbidNotNormalizedTypeRule: + enabled: true + checkDisjunctiveNormalForm: true forbidNullInAssignOperations: enabled: true blacklist: ['??='] @@ -563,6 +566,24 @@ function example($unknown) { } ``` +### forbidNotNormalizedType +- Reports any PhpDoc or native type that is not normalized, which can be: + - when child and parent appears in its union or intersection + - when same type appears multiple times in its union or intersection + - when DNF is not used + - configurable by `checkDisjunctiveNormalForm` +- Main motivation here is that PHPStan normalizes all types before analysis, so it is better to see it in codebase the same way PHPStan does + +```php +/** + * @return mixed|false // denied, this is still just mixed + */ +public function getAttribute(string $name) +{ + return $this->attributes[$name] ?? false; +} +``` + ### forbidNullInAssignOperations - Denies using [assign operators](https://www.php.net/manual/en/language.operators.assignment.php) if null is involved on right side - You can configure which operators are ignored, by default only `??=` is excluded diff --git a/rules.neon b/rules.neon index a2dff79..2ee0e9e 100644 --- a/rules.neon +++ b/rules.neon @@ -68,6 +68,9 @@ parameters: enabled: true forbidMethodCallOnMixed: enabled: true + forbidNotNormalizedTypeRule: + enabled: true + checkDisjunctiveNormalForm: true forbidNullInAssignOperations: enabled: true blacklist: ['??='] @@ -174,6 +177,10 @@ parametersSchema: forbidMethodCallOnMixed: structure([ enabled: bool() ]) + forbidNotNormalizedTypeRule: structure([ + enabled: bool() + checkDisjunctiveNormalForm: bool() + ]) forbidNullInAssignOperations: structure([ enabled: bool() blacklist: arrayOf(string()) @@ -265,6 +272,8 @@ conditionalTags: phpstan.rules.rule: %shipmonkRules.forbidMatchDefaultArmForEnums.enabled% ShipMonk\PHPStan\Rule\ForbidMethodCallOnMixedRule: phpstan.rules.rule: %shipmonkRules.forbidMethodCallOnMixed.enabled% + ShipMonk\PHPStan\Rule\ForbidNotNormalizedTypeRule: + phpstan.rules.rule: %shipmonkRules.forbidNotNormalizedTypeRule.enabled% ShipMonk\PHPStan\Rule\ForbidNullInAssignOperationsRule: phpstan.rules.rule: %shipmonkRules.forbidNullInAssignOperations.enabled% ShipMonk\PHPStan\Rule\ForbidNullInBinaryOperationsRule: @@ -374,6 +383,11 @@ services: class: ShipMonk\PHPStan\Rule\ForbidMethodCallOnMixedRule arguments: checkExplicitMixed: %checkExplicitMixed% + + - + class: ShipMonk\PHPStan\Rule\ForbidNotNormalizedTypeRule + arguments: + checkDisjunctiveNormalForm: %shipmonkRules.forbidNotNormalizedTypeRule.checkDisjunctiveNormalForm% - class: ShipMonk\PHPStan\Rule\ForbidMatchDefaultArmForEnumsRule - diff --git a/src/Rule/ForbidNotNormalizedTypeRule.php b/src/Rule/ForbidNotNormalizedTypeRule.php new file mode 100644 index 0000000..ace2ebd --- /dev/null +++ b/src/Rule/ForbidNotNormalizedTypeRule.php @@ -0,0 +1,586 @@ + + */ +class ForbidNotNormalizedTypeRule implements Rule +{ + + private FileTypeMapper $fileTypeMapper; + + private TypeNodeResolver $typeNodeResolver; + + private PhpParserPrinter $phpParserPrinter; + + private bool $checkDisjunctiveNormalForm; + + /** + * @var array + */ + private array $processedDocComments = []; + + public function __construct( + FileTypeMapper $fileTypeMapper, + TypeNodeResolver $typeNodeResolver, + PhpParserPrinter $phpParserPrinter, + bool $checkDisjunctiveNormalForm + ) + { + $this->fileTypeMapper = $fileTypeMapper; + $this->typeNodeResolver = $typeNodeResolver; + $this->phpParserPrinter = $phpParserPrinter; + $this->checkDisjunctiveNormalForm = $checkDisjunctiveNormalForm; + } + + public function getNodeType(): string + { + return PhpParserNode::class; + } + + /** + * @return list + */ + public function processNode( + PhpParserNode $node, + Scope $scope + ): array + { + if ($node instanceof FunctionLike) { + return array_merge( + $this->checkParamAndReturnPhpDoc($node, $scope), + $this->checkParamAndReturnNativeType($node, $scope), + ); + } + + if ($node instanceof Property) { + return array_merge( + $this->checkPropertyPhpDoc($node, $scope), + $this->checkPropertyNativeType($node, $scope), + ); + } + + return $this->checkInlineVarDoc($node, $scope); + } + + /** + * @return list + */ + private function checkParamAndReturnPhpDoc( + FunctionLike $node, + Scope $scope + ): array + { + $errors = []; + + $resolvedPhpDoc = $this->resolvePhpDoc($node, $scope); + + if ($resolvedPhpDoc === null) { + return []; + } + + $nameScope = $resolvedPhpDoc->getNullableNameScope(); + + if ($nameScope === null) { + return []; + } + + foreach ($resolvedPhpDoc->getPhpDocNodes() as $phpdocNode) { + $errors = array_merge( + $errors, + $this->processParamTags($node, $phpdocNode->getParamTagValues(), $nameScope), + $this->processReturnTags($node, $phpdocNode->getReturnTagValues(), $nameScope), + ); + } + + return $errors; + } + + /** + * @return list + */ + private function checkPropertyNativeType(Property $node, Scope $scope): array + { + $errors = []; + + if ($node->type !== null) { + $propertyName = $this->getPropertyNameFromNativeNode($node); + + foreach ($this->extractUnionIntersectionPhpParserNodes($node->type) as $multiTypeNode) { + $newErrors = $this->processMultiTypePhpParserNode($multiTypeNode, $scope, "property \${$propertyName}"); + $errors = array_merge($errors, $newErrors); + } + } + + return $errors; + } + + /** + * @return list + */ + private function checkParamAndReturnNativeType(FunctionLike $node, Scope $scope): array + { + $errors = []; + + if ($node->getReturnType() !== null) { + foreach ($this->extractUnionIntersectionPhpParserNodes($node->getReturnType()) as $multiTypeNode) { + $newErrors = $this->processMultiTypePhpParserNode($multiTypeNode, $scope, 'return'); + $errors = array_merge($errors, $newErrors); + } + } + + foreach ($node->getParams() as $param) { + $paramType = $param->type; + + if ($paramType === null) { + continue; + } + + $parameterName = $this->getParameterNameFromNativeNode($param); + + foreach ($this->extractUnionIntersectionPhpParserNodes($paramType) as $multiTypeNode) { + $newErrors = $this->processMultiTypePhpParserNode($multiTypeNode, $scope, "parameter \${$parameterName}"); + $errors = array_merge($errors, $newErrors); + } + } + + return $errors; + } + + /** + * @return list + */ + private function checkPropertyPhpDoc( + Property $node, + Scope $scope + ): array + { + $errors = []; + + $resolvedPhpDoc = $this->resolvePhpDoc($node, $scope); + + if ($resolvedPhpDoc === null) { + return []; + } + + $nameScope = $resolvedPhpDoc->getNullableNameScope(); + + if ($nameScope === null) { + return []; + } + + foreach ($resolvedPhpDoc->getPhpDocNodes() as $phpdocNode) { + $errors = array_merge($errors, $this->processVarTags($node, $phpdocNode->getVarTagValues(), $nameScope)); + } + + return $errors; + } + + /** + * @return list + */ + private function checkInlineVarDoc(PhpParserNode $node, Scope $scope): array + { + $docComment = $node->getDocComment(); + + if ($docComment === null) { + return []; + } + + $docCommendSplId = spl_object_id($docComment); + + if (isset($this->processedDocComments[$docCommendSplId])) { + return []; // the instance is shared in all nodes where this vardoc is used (e.g. Expression, Assign, Variable for $a = $b) + } + + $resolvedPhpDoc = $this->resolvePhpDoc($node, $scope); + + if ($resolvedPhpDoc === null) { + return []; + } + + $nameScope = $resolvedPhpDoc->getNullableNameScope(); + + if ($nameScope === null) { + return []; + } + + $errors = []; + + foreach ($resolvedPhpDoc->getPhpDocNodes() as $phpdocNode) { + $errors = array_merge($errors, $this->processVarTags($node, $phpdocNode->getVarTagValues(), $nameScope)); + } + + $this->processedDocComments[$docCommendSplId] = true; + + return $errors; + } + + private function resolvePhpDoc( + PhpParserNode $node, + Scope $scope + ): ?ResolvedPhpDocBlock + { + $docComment = $node->getDocComment(); + + if ($docComment === null) { + return null; + } + + return $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $scope->getClassReflection() === null ? null : $scope->getClassReflection()->getName(), + $scope->getTraitReflection() === null ? null : $scope->getTraitReflection()->getName(), + $this->getFunctionName($node), + $docComment->getText(), + ); + } + + private function getFunctionName(PhpParserNode $node): ?string + { + if ($node instanceof ClassMethod || $node instanceof Function_) { + return $node->name->name; + } + + return null; + } + + /** + * @param array $paramTagValues + * @return list + */ + public function processParamTags( + PhpParserNode $sourceNode, + array $paramTagValues, + NameScope $nameSpace + ): array + { + $errors = []; + + foreach ($paramTagValues as $paramTagValue) { + foreach ($this->extractUnionAndIntersectionPhpDocTypeNodes($paramTagValue->type) as $multiTypeNode) { + $newErrors = $this->processMultiTypePhpDocNode( + $multiTypeNode, + $nameSpace, + "parameter {$paramTagValue->parameterName}", + $this->getPhpDocLine($sourceNode, $paramTagValue), + ); + $errors = array_merge($errors, $newErrors); + } + } + + return $errors; + } + + /** + * @param array $varTagValues + * @return list + */ + public function processVarTags( + PhpParserNode $originalNode, + array $varTagValues, + NameScope $nameSpace + ): array + { + $errors = []; + + foreach ($varTagValues as $varTagValue) { + foreach ($this->extractUnionAndIntersectionPhpDocTypeNodes($varTagValue->type) as $multiTypeNode) { + $identification = $varTagValue->variableName !== '' + ? "variable {$varTagValue->variableName}" + : null; + + $newErrors = $this->processMultiTypePhpDocNode( + $multiTypeNode, + $nameSpace, + $identification, + $this->getPhpDocLine($originalNode, $varTagValue), + ); + $errors = array_merge($errors, $newErrors); + } + } + + return $errors; + } + + /** + * @param array $returnTagValues + * @return list + */ + public function processReturnTags( + PhpParserNode $originalNode, + array $returnTagValues, + NameScope $nameSpace + ): array + { + $errors = []; + + foreach ($returnTagValues as $returnTagValue) { + foreach ($this->extractUnionAndIntersectionPhpDocTypeNodes($returnTagValue->type) as $multiTypeNode) { + $newErrors = $this->processMultiTypePhpDocNode($multiTypeNode, $nameSpace, 'return', $this->getPhpDocLine($originalNode, $returnTagValue)); + $errors = array_merge($errors, $newErrors); + } + } + + return $errors; + } + + /** + * @return list + */ + private function extractUnionAndIntersectionPhpDocTypeNodes(TypeNode $typeNode): array + { + $nodes = []; + $this->traversePhpDocTypeNode($typeNode, static function (TypeNode $typeNode) use (&$nodes): void { + if ($typeNode instanceof UnionTypeNode || $typeNode instanceof IntersectionTypeNode) { + $nodes[] = $typeNode; + } + + if ($typeNode instanceof NullableTypeNode) { + $nodes[] = new UnionTypeNode([$typeNode->type, new IdentifierTypeNode('null')]); + } + }); + return $nodes; + } + + /** + * @return list + */ + private function extractUnionIntersectionPhpParserNodes(PhpParserNode $node): array + { + $multiTypeNodes = []; + + if ($node instanceof NullableType) { + $multiTypeNodes[] = new UnionType([$node->type, new Identifier('null')], $node->getAttributes()); + } + + if ($node instanceof UnionType) { + $multiTypeNodes[] = $node; + + foreach ($node->types as $innerType) { + if ($innerType instanceof IntersectionType) { + $multiTypeNodes[] = $innerType; + } + } + } + + if ($node instanceof IntersectionType) { + $multiTypeNodes[] = $node; + } + + return $multiTypeNodes; + } + + /** + * @param mixed $type + * @param callable(TypeNode): void $callback + */ + private function traversePhpDocTypeNode( + $type, + callable $callback + ): void + { + if (is_array($type)) { + foreach ($type as $item) { + $this->traversePhpDocTypeNode($item, $callback); + } + } + + if ($type instanceof TypeNode) { + $callback($type); + } + + if (is_object($type)) { + $this->traversePhpDocTypeNode(get_object_vars($type), $callback); + } + } + + /** + * @param IntersectionType|UnionType $multiTypeNode + * @return list + */ + private function processMultiTypePhpParserNode( + ComplexType $multiTypeNode, + Scope $scope, + string $identification + ): array + { + $innerTypeNodes = array_values($multiTypeNode->types); + $multiTypeNodeString = $this->printPhpParserNode($multiTypeNode); + + $errors = []; + $countOfNodeTypes = count($innerTypeNodes); + + foreach ($innerTypeNodes as $i => $iValue) { + for ($j = $i + 1; $j < $countOfNodeTypes; $j++) { + $typeNodeA = $iValue; + $typeNodeB = $innerTypeNodes[$j]; + + $typeA = $scope->getFunctionType($typeNodeA, false, false); + $typeB = $scope->getFunctionType($typeNodeB, false, false); + + $typeNodeAString = $this->printPhpParserNode($typeNodeA); + $typeNodeBString = $this->printPhpParserNode($typeNodeB); + + if ($typeA->isSuperTypeOf($typeB)->yes()) { + $errors[] = RuleErrorBuilder::message("Found non-normalized type {$multiTypeNodeString} for {$identification}: {$typeNodeBString} is a subtype of {$typeNodeAString}.") + ->line($multiTypeNode->getLine()) + ->identifier('shipmonk.nonNormalizedType') + ->build(); + continue; + } + + if ($typeB->isSuperTypeOf($typeA)->yes()) { + $errors[] = RuleErrorBuilder::message("Found non-normalized type {$multiTypeNodeString} for {$identification}: {$typeNodeAString} is a subtype of {$typeNodeBString}.") + ->line($multiTypeNode->getLine()) + ->identifier('shipmonk.nonNormalizedType') + ->build(); + } + } + } + + return $errors; + } + + private function printPhpParserNode(PhpParserNode $node): string + { + $nodeCopy = clone $node; + $nodeCopy->setAttribute('comments', []); // avoid printing with surrounding line comments + return $this->phpParserPrinter->prettyPrint([$nodeCopy]); + } + + /** + * @param UnionTypeNode|IntersectionTypeNode $multiTypeNode + * @return list + */ + private function processMultiTypePhpDocNode( + TypeNode $multiTypeNode, + NameScope $nameSpace, + ?string $identification, + int $line + ): array + { + $errors = []; + $innerTypeNodes = array_values($multiTypeNode->types); // ensure list + $forWhat = $identification !== null ? " for $identification" : ''; + + if ($this->checkDisjunctiveNormalForm && $multiTypeNode instanceof IntersectionTypeNode) { + foreach ($multiTypeNode->types as $type) { + if ($type instanceof UnionTypeNode) { + $dnf = $this->typeNodeResolver->resolve($multiTypeNode, $nameSpace)->describe(VerbosityLevel::typeOnly()); + + $errors[] = RuleErrorBuilder::message("Found non-normalized type {$multiTypeNode}{$forWhat}: this is not disjunctive normal form, use {$dnf}") + ->line($line) + ->identifier('shipmonk.nonNormalizedType') + ->build(); + } + } + } + + $countOfNodeTypes = count($innerTypeNodes); + + foreach ($innerTypeNodes as $i => $iValue) { + for ($j = $i + 1; $j < $countOfNodeTypes; $j++) { + $typeNodeA = $iValue; + $typeNodeB = $innerTypeNodes[$j]; + + $typeA = $this->typeNodeResolver->resolve($typeNodeA, $nameSpace); + $typeB = $this->typeNodeResolver->resolve($typeNodeB, $nameSpace); + + if ($typeA->isSuperTypeOf($typeB)->yes()) { + $errors[] = RuleErrorBuilder::message("Found non-normalized type {$multiTypeNode}{$forWhat}: {$typeNodeB} is a subtype of {$typeNodeA}.") + ->line($line) + ->identifier('shipmonk.nonNormalizedType') + ->build(); + continue; + } + + if ($typeB->isSuperTypeOf($typeA)->yes()) { + $errors[] = RuleErrorBuilder::message("Found non-normalized type {$multiTypeNode}{$forWhat}: {$typeNodeA} is a subtype of {$typeNodeB}.") + ->line($line) + ->identifier('shipmonk.nonNormalizedType') + ->build(); + } + } + } + + return $errors; + } + + private function getPropertyNameFromNativeNode(Property $node): string + { + $propertyNames = []; + + foreach ($node->props as $propertyProperty) { + $propertyNames[] = $propertyProperty->name->name; + } + + return implode(',', $propertyNames); + } + + private function getPhpDocLine(PhpParserNode $node, PhpDocRootNode $phpDocNode): int + { + /** @var int|null $phpDocTagLine */ + $phpDocTagLine = $phpDocNode->getAttribute('startLine'); + $phpDoc = $node->getDocComment(); + + if ($phpDocTagLine === null || $phpDoc === null) { + return $node->getLine(); + } + + return $phpDoc->getStartLine() + $phpDocTagLine - 1; + } + + private function getParameterNameFromNativeNode(Param $param): string + { + if ($param->var instanceof Variable && is_string($param->var->name)) { + return $param->var->name; + } + + throw new LogicException('Unexpected parameter: ' . $this->printPhpParserNode($param)); + } + +} diff --git a/tests/Rule/ForbidNotNormalizedTypeRuleTest.php b/tests/Rule/ForbidNotNormalizedTypeRuleTest.php new file mode 100644 index 0000000..a2fe9f7 --- /dev/null +++ b/tests/Rule/ForbidNotNormalizedTypeRuleTest.php @@ -0,0 +1,56 @@ + + */ +class ForbidNotNormalizedTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): ForbidNotNormalizedTypeRule + { + return new ForbidNotNormalizedTypeRule( + new FileTypeMapper( // @phpstan-ignore-line + self::getContainer()->getByType(ReflectionProviderProvider::class), // @phpstan-ignore-line + self::getContainer()->getService('currentPhpVersionRichParser'), // @phpstan-ignore-line + new PhpDocStringResolver( // @phpstan-ignore-line + new Lexer(), + new PhpDocParser( + self::getContainer()->getByType(TypeParser::class), + self::getContainer()->getByType(ConstExprParser::class), + false, + false, + ['lines' => true], // simplify after https://github.com/phpstan/phpstan-src/pull/2807 + ), + ), + self::getContainer()->getByType(PhpDocNodeResolver::class), // @phpstan-ignore-line + self::getContainer()->getByType(AnonymousClassNameHelper::class), // @phpstan-ignore-line + self::getContainer()->getByType(FileHelper::class), + ), + self::getContainer()->getByType(TypeNodeResolver::class), + self::getContainer()->getByType(Standard::class), + true, + ); + } + + public function testRule(): void + { + $this->analyseFile(__DIR__ . '/data/ForbidNotNormalizedTypeRule/code.php'); + } + +} diff --git a/tests/Rule/data/ForbidNotNormalizedTypeRule/code.php b/tests/Rule/data/ForbidNotNormalizedTypeRule/code.php new file mode 100644 index 0000000..3704551 --- /dev/null +++ b/tests/Rule/data/ForbidNotNormalizedTypeRule/code.php @@ -0,0 +1,158 @@ + $mixed2 */ // error: Found non-normalized type (int[] | list) for variable $mixed2: list is a subtype of int[]. + foreach ($mixed2 as $key => $var) {} + + /** + * @var int|positive-int $key // error: Found non-normalized type (int | positive-int) for variable $key: positive-int is a subtype of int. + * @var int|positive-int $foo // error: Found non-normalized type (int | positive-int) for variable $foo: positive-int is a subtype of int. + * @var int|positive-int $baz // error: Found non-normalized type (int | positive-int) for variable $baz: positive-int is a subtype of int. + */ + foreach ($mixed3 as $key => [$foo, [$baz]]) { + + } + + /** @var int|0 $var */ // error: Found non-normalized type (int | 0) for variable $var: 0 is a subtype of int. + static $var; + + /** @var int|0 $var2 */ // error: Found non-normalized type (int | 0) for variable $var2: 0 is a subtype of int. + $var2 = doFoo(); + + /** + * @var int|0 // error: Found non-normalized type (int | 0): 0 is a subtype of int. + * @phpstan-var int|0 + * @psalm-var int|0 + */ + $var3 = doFoo(); // OK + } + + /** + * @param ChildOne|BaseClass $a // error: Found non-normalized type (ChildOne | BaseClass) for parameter $a: ChildOne is a subtype of BaseClass. + * @param mixed|null $b // error: Found non-normalized type (mixed | null) for parameter $b: null is a subtype of mixed. + * @param int|positive-int $c // error: Found non-normalized type (int | positive-int) for parameter $c: positive-int is a subtype of int. + * @param int[]|array $d // error: Found non-normalized type (int[] | array) for parameter $d: array is a subtype of int[]. + * @param ?mixed $e // error: Found non-normalized type (mixed | null) for parameter $e: null is a subtype of mixed. + * @param array $f // error: Found non-normalized type (mixed | int) for parameter $f: int is a subtype of mixed. + * @param list $g // error: Found non-normalized type (mixed | int) for parameter $g: int is a subtype of mixed. + * @param list|array $h // error: Found non-normalized type (list | array) for parameter $h: list is a subtype of array. + * @param ChildOne|MyInterface $i + * @param InterfaceImplementor|MyInterface $j // error: Found non-normalized type (InterfaceImplementor | MyInterface) for parameter $j: InterfaceImplementor is a subtype of MyInterface. + * @param callable(mixed|null): mixed $k // error: Found non-normalized type (mixed | null) for parameter $k: null is a subtype of mixed. + * @param callable(): (mixed|null) $l // error: Found non-normalized type (mixed | null) for parameter $l: null is a subtype of mixed. + * @param MyInterface|MyInterface $m // error: Found non-normalized type (MyInterface | MyInterface) for parameter $m: MyInterface is a subtype of MyInterface. + */ + public function testPhpDocUnions($a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l, $m): void + { + } + + + /** + * @param ChildOne&BaseClass $a // error: Found non-normalized type (ChildOne & BaseClass) for parameter $a: ChildOne is a subtype of BaseClass. + * @param mixed&null $b // error: Found non-normalized type (mixed & null) for parameter $b: null is a subtype of mixed. + * @param int&positive-int $c // error: Found non-normalized type (int & positive-int) for parameter $c: positive-int is a subtype of int. + * @param int[]&array $d // error: Found non-normalized type (int[] & array) for parameter $d: array is a subtype of int[]. + * @param array $f // error: Found non-normalized type (mixed & int) for parameter $f: int is a subtype of mixed. + * @param list $g // error: Found non-normalized type (mixed & int) for parameter $g: int is a subtype of mixed. + * @param list&array $h // error: Found non-normalized type (list & array) for parameter $h: list is a subtype of array. + * @param ChildOne&MyInterface $i + * @param InterfaceImplementor&MyInterface $j // error: Found non-normalized type (InterfaceImplementor & MyInterface) for parameter $j: InterfaceImplementor is a subtype of MyInterface. + * @param MyInterface&MyInterface $k // error: Found non-normalized type (MyInterface & MyInterface) for parameter $k: MyInterface is a subtype of MyInterface. + */ + public function testPhpDocIntersections($a, $b, $c, $d, $f, $g, $h, $i, $j, $k): void + { + } + + public function testNativeUnions( + ChildOne|BaseClass $i, // error: Found non-normalized type \ForbidNotNormalizedTypeRule\ChildOne|\ForbidNotNormalizedTypeRule\BaseClass for parameter $i: \ForbidNotNormalizedTypeRule\ChildOne is a subtype of \ForbidNotNormalizedTypeRule\BaseClass. + ChildOne|MyInterface $j, + InterfaceImplementor|MyInterface $k, // error: Found non-normalized type \ForbidNotNormalizedTypeRule\InterfaceImplementor|\ForbidNotNormalizedTypeRule\MyInterface for parameter $k: \ForbidNotNormalizedTypeRule\InterfaceImplementor is a subtype of \ForbidNotNormalizedTypeRule\MyInterface. + InterfaceImplementor|MyInterface|null $l, // error: Found non-normalized type \ForbidNotNormalizedTypeRule\InterfaceImplementor|\ForbidNotNormalizedTypeRule\MyInterface|null for parameter $l: \ForbidNotNormalizedTypeRule\InterfaceImplementor is a subtype of \ForbidNotNormalizedTypeRule\MyInterface. + + + // following are fatal errors, some reported even by native phpstan + + mixed|MyInterface $a, // error: Found non-normalized type mixed|\ForbidNotNormalizedTypeRule\MyInterface for parameter $a: \ForbidNotNormalizedTypeRule\MyInterface is a subtype of mixed. + ?mixed $b, // error: Found non-normalized type mixed|null for parameter $b: null is a subtype of mixed. + null|mixed $c, // error: Found non-normalized type null|mixed for parameter $c: null is a subtype of mixed. + true|bool $d, // error: Found non-normalized type true|bool for parameter $d: true is a subtype of bool. + true|false $e, + ): void + { + } + +}