diff --git a/PhpCollective/Sniffs/Commenting/DisallowArrayTypeHintSyntaxSniff.php b/PhpCollective/Sniffs/Commenting/DisallowArrayTypeHintSyntaxSniff.php index 547b83a..1916fee 100644 --- a/PhpCollective/Sniffs/Commenting/DisallowArrayTypeHintSyntaxSniff.php +++ b/PhpCollective/Sniffs/Commenting/DisallowArrayTypeHintSyntaxSniff.php @@ -32,9 +32,7 @@ use SlevomatCodingStandard\Helpers\TypeHintHelper; /** - * Fixed version of Slevomatic, touching collection objects the right way. - * - * @see https://github.com/slevomat/coding-standard/issues/1296 + * Disallows use of `?type` in favor of `type|null`. Reduces conflict or issues with other sniffs. */ class DisallowArrayTypeHintSyntaxSniff implements Sniff { @@ -66,9 +64,9 @@ public function register(): array /** * @inheritDoc */ - public function process(File $phpcsFile, $docCommentOpenPointer): void + public function process(File $phpcsFile, $pointer): void { - $annotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenPointer); + $annotations = AnnotationHelper::getAnnotations($phpcsFile, $pointer); foreach ($annotations as $annotation) { $arrayTypeNodes = $this->getArrayTypeNodes($annotation->getValue()); @@ -88,7 +86,7 @@ public function process(File $phpcsFile, $docCommentOpenPointer): void } /** @var \SlevomatCodingStandard\Helpers\ParsedDocComment $parsedDocComment */ - $parsedDocComment = DocCommentHelper::parseDocComment($phpcsFile, $docCommentOpenPointer); + $parsedDocComment = DocCommentHelper::parseDocComment($phpcsFile, $pointer); /** @var list<\PHPStan\PhpDocParser\Ast\Type\UnionTypeNode> $unionTypeNodes */ $unionTypeNodes = AnnotationHelper::getAnnotationNodesByType($annotation->getNode(), UnionTypeNode::class); @@ -96,14 +94,14 @@ public function process(File $phpcsFile, $docCommentOpenPointer): void if ($unionTypeNode !== null) { if ($this->isUnionTypeGenericObjectCollection($unionTypeNodes[0])) { - $this->fixGenericObjectCollection($phpcsFile, $annotation, $docCommentOpenPointer, $arrayTypeNode, $unionTypeNodes); + $this->fixGenericObjectCollection($phpcsFile, $annotation, $pointer, $arrayTypeNode, $unionTypeNodes); continue; } $genericIdentifier = $this->findGenericIdentifier( $phpcsFile, - $docCommentOpenPointer, + $pointer, $unionTypeNode, $annotation->getValue(), ); @@ -136,7 +134,7 @@ public function process(File $phpcsFile, $docCommentOpenPointer): void } else { $genericIdentifier = $this->findGenericIdentifier( $phpcsFile, - $docCommentOpenPointer, + $pointer, $arrayTypeNode, $annotation->getValue(), ) ?? 'array'; diff --git a/PhpCollective/Sniffs/Commenting/DisallowShorthandNullableTypeHintSniff.php b/PhpCollective/Sniffs/Commenting/DisallowShorthandNullableTypeHintSniff.php new file mode 100644 index 0000000..470bcf4 --- /dev/null +++ b/PhpCollective/Sniffs/Commenting/DisallowShorthandNullableTypeHintSniff.php @@ -0,0 +1,201 @@ +getTokens(); + $docCommentContent = $tokens[$pointer]['content']; + + /** @var \PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode|\PHPStan\PhpDocParser\Ast\PhpDoc\TypelessParamTagValueNode|\PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode|\PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode|\PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode $valueNode */ + $valueNode = static::getValueNode($tokens[$pointer - 2]['content'], $docCommentContent); + + $printer = new Printer(); + $before = $printer->print($valueNode); + + // Check if the value node is invalid and handle it + if ($valueNode instanceof InvalidTagValueNode) { + // Attempt to clean up and process invalid types + $fixedNode = $this->fixInvalidTagValueNode($valueNode); + if ($fixedNode) { + $valueNode = $fixedNode; + } + } + + if ($valueNode instanceof InvalidTagValueNode) { + return; + } + + // Traverse and fix the nullable types + $this->traversePhpDocNode($valueNode); + + $after = $printer->print($valueNode); + + if ($after === $before) { + return; + } + + $message = sprintf('Shorthand nullable `%s` invalid, use `%s` instead.', $before, $after); + $fixable = $phpcsFile->addFixableError($message, $pointer, static::CODE_DISALLOWED_SHORTHAND_TYPE_HINT); + if ($fixable) { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($pointer, $after); + $phpcsFile->fixer->endChangeset(); + } + } + + /** + * Attempt to fix an InvalidTagValueNode by parsing and correcting the types manually. + * + * @param \PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode $invalidNode + * + * @return \PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode|null + */ + protected function fixInvalidTagValueNode(InvalidTagValueNode $invalidNode): ?PhpDocTagValueNode + { + $value = $invalidNode->value; + $rest = ''; + if (str_contains($value, '$')) { + $string = trim(substr($value, 0, (int)strpos($value, '$'))); + $rest = trim(substr($value, strlen($string))); + $value = $string; + } + + // Try to parse and correct the invalid node's type (e.g., `?string|null`) + if (str_contains($value, '|')) { + // Split the types + $types = explode('|', $value); + + $transformedTypes = []; + $hasNullable = false; + + foreach ($types as $type) { + $type = trim($type); + + // Handle `?Type` shorthand + if (str_starts_with($type, '?')) { + $type = substr($type, 1); // Remove leading '?' + $transformedTypes[] = new IdentifierTypeNode($type); + $hasNullable = true; // Mark as nullable + } elseif (strtolower($type) === 'null') { + // If 'null' is encountered, mark as nullable but don't add now + $hasNullable = true; + } else { + $transformedTypes[] = new IdentifierTypeNode($type); + } + } + + // Add `null` at the end if the type is nullable + if ($hasNullable) { + $transformedTypes[] = new IdentifierTypeNode('null'); + } + + // Create a new UnionTypeNode with the transformed types + return new ParamTagValueNode( + new UnionTypeNode($transformedTypes), + false, + $rest, + '', + ); + } + + return null; + } + + /** + * Traverse and transform the PHPDoc AST. + * + * @param \PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode $phpDocNode + * + * @return void + */ + protected function traversePhpDocNode(PhpDocTagValueNode $phpDocNode): void + { + if ( + $phpDocNode instanceof ParamTagValueNode + || $phpDocNode instanceof ReturnTagValueNode + || $phpDocNode instanceof VarTagValueNode + ) { + echo PHP_EOL . 'processing...' . PHP_EOL; + $phpDocNode->type = $this->transformNullableType($phpDocNode->type); + } + + echo PHP_EOL . PHP_EOL; + } + + /** + * Traverse and transform nullable types. + * + * @param \PHPStan\PhpDocParser\Ast\Type\TypeNode $typeNode + * + * @return \PHPStan\PhpDocParser\Ast\Type\TypeNode + */ + protected function transformNullableType(TypeNode $typeNode): TypeNode + { + if ($typeNode instanceof NullableTypeNode) { + $innerType = $typeNode->type; + + // Convert `?Type` to `Type|null` + return new UnionTypeNode([ + $innerType, + new IdentifierTypeNode('null'), + ]); + } + + // Handle UnionTypeNode (e.g., `Type|null`) + if ($typeNode instanceof UnionTypeNode) { + $transformedTypes = []; + foreach ($typeNode->types as $subType) { + $transformedTypes[] = $this->transformNullableType($subType); // Recursively transform + } + + return new UnionTypeNode($transformedTypes); + } + + return $typeNode; + } +} diff --git a/docs/sniffs.md b/docs/sniffs.md index c6325c1..e9ce769 100644 --- a/docs/sniffs.md +++ b/docs/sniffs.md @@ -1,7 +1,7 @@ # PhpCollective Code Sniffer -The PhpCollectiveStrict standard contains 216 sniffs +The PhpCollectiveStrict standard contains 217 sniffs Generic (25 sniffs) ------------------- @@ -38,7 +38,7 @@ PEAR (4 sniffs) - PEAR.Functions.ValidDefaultValue - PEAR.NamingConventions.ValidClassName -PhpCollective (79 sniffs) +PhpCollective (80 sniffs) ------------------------- - PhpCollective.Arrays.DisallowImplicitArrayCreation - PhpCollective.Classes.ClassFileName @@ -52,6 +52,7 @@ PhpCollective (79 sniffs) - PhpCollective.Classes.SelfAccessor - PhpCollective.Commenting.Attributes - PhpCollective.Commenting.DisallowArrayTypeHintSyntax +- PhpCollective.Commenting.DisallowShorthandNullableTypeHint - PhpCollective.Commenting.DocBlock - PhpCollective.Commenting.DocBlockConst - PhpCollective.Commenting.DocBlockConstructor diff --git a/tests/PhpCollective/Sniffs/Commenting/DisallowShorthandNullableTypeHintSniffTest.php b/tests/PhpCollective/Sniffs/Commenting/DisallowShorthandNullableTypeHintSniffTest.php new file mode 100644 index 0000000..6d9c666 --- /dev/null +++ b/tests/PhpCollective/Sniffs/Commenting/DisallowShorthandNullableTypeHintSniffTest.php @@ -0,0 +1,30 @@ +assertSnifferFindsErrors(new DisallowShorthandNullableTypeHintSniff(), 5); + } + + /** + * @return void + */ + public function testDocBlockConstFixer(): void + { + $this->assertSnifferCanFixErrors(new DisallowShorthandNullableTypeHintSniff()); + } +} diff --git a/tests/_data/DisallowShorthandNullableTypeHint/after.php b/tests/_data/DisallowShorthandNullableTypeHint/after.php new file mode 100644 index 0000000..a12fb1c --- /dev/null +++ b/tests/_data/DisallowShorthandNullableTypeHint/after.php @@ -0,0 +1,27 @@ +