Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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());
Expand All @@ -88,22 +86,22 @@ 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);
$unionTypeNode = $this->findUnionTypeThatContainsArrayType($arrayTypeNode, $unionTypeNodes);

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(),
);
Expand Down Expand Up @@ -136,7 +134,7 @@ public function process(File $phpcsFile, $docCommentOpenPointer): void
} else {
$genericIdentifier = $this->findGenericIdentifier(
$phpcsFile,
$docCommentOpenPointer,
$pointer,
$arrayTypeNode,
$annotation->getValue(),
) ?? 'array';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<?php

/**
* MIT License
* For full license information, please view the LICENSE file that was distributed with this source code.
*/

namespace PhpCollective\Sniffs\Commenting;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PhpCollective\Traits\CommentingTrait;
use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
use PHPStan\PhpDocParser\Printer\Printer;

/**
* Disallows use of `?type` in favor of `type|null`. Reduces conflict or issues with other sniffs.
*/
class DisallowShorthandNullableTypeHintSniff implements Sniff
{
use CommentingTrait;

/**
* @var string
*/
public const CODE_DISALLOWED_SHORTHAND_TYPE_HINT = 'DisallowedShorthandTypeHint';

/**
* @inheritDoc
*/
public function register(): array
{
return [
T_DOC_COMMENT_STRING,
];
}

/**
* @inheritDoc
*/
public function process(File $phpcsFile, $pointer): void
{
$tokens = $phpcsFile->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;
}
}
5 changes: 3 additions & 2 deletions docs/sniffs.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# PhpCollective Code Sniffer


The PhpCollectiveStrict standard contains 216 sniffs
The PhpCollectiveStrict standard contains 217 sniffs

Generic (25 sniffs)
-------------------
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/**
* MIT License
* For full license information, please view the LICENSE file that was distributed with this source code.
*/

namespace PhpCollective\Test\PhpCollective\Sniffs\Commenting;

use PhpCollective\Sniffs\Commenting\DisallowShorthandNullableTypeHintSniff;
use PhpCollective\Test\TestCase;

class DisallowShorthandNullableTypeHintSniffTest extends TestCase
{
/**
* @return void
*/
public function testDocBlockConstSniffer(): void
{
$this->assertSnifferFindsErrors(new DisallowShorthandNullableTypeHintSniff(), 5);
}

/**
* @return void
*/
public function testDocBlockConstFixer(): void
{
$this->assertSnifferCanFixErrors(new DisallowShorthandNullableTypeHintSniff());
}
}
27 changes: 27 additions & 0 deletions tests/_data/DisallowShorthandNullableTypeHint/after.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php declare(strict_types = 1);

namespace PhpCollective;

class FixMe
{
/**
* @var string|null Some Comment
*/
protected $string1 = null;

/**
* @var string|int|null
*/
protected $string2 = null;

/**
* @param string|null $string1
* @param string|null $string2
*
* @return string|null Some Comment
*/
public function doSth(?string $string1, ?string $string2 = null): ?string
{
return $string1 ?: $string2;
}
}
27 changes: 27 additions & 0 deletions tests/_data/DisallowShorthandNullableTypeHint/before.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php declare(strict_types = 1);

namespace PhpCollective;

class FixMe
{
/**
* @var ?string Some Comment
*/
protected $string1 = null;

/**
* @var ?string|int
*/
protected $string2 = null;

/**
* @param ?string $string1
* @param ?string|null $string2
*
* @return ?string Some Comment
*/
public function doSth(?string $string1, ?string $string2 = null): ?string
{
return $string1 ?: $string2;
}
}
Loading