diff --git a/conf/config.neon b/conf/config.neon index aa096cc686..3146a8fd3f 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -282,6 +282,7 @@ extensions: rules: - PHPStan\Rules\Debug\DebugScopeRule + - PHPStan\Rules\Debug\DumpPhpDocTypeRule - PHPStan\Rules\Debug\DumpTypeRule - PHPStan\Rules\Debug\FileAssertRule @@ -433,6 +434,9 @@ services: usedAttributes: lines: %featureToggles.phpDocParserIncludeLines% + - + class: PHPStan\PhpDocParser\Printer\Printer + - class: PHPStan\PhpDoc\ConstExprParserFactory arguments: diff --git a/src/Rules/Debug/DumpPhpDocTypeRule.php b/src/Rules/Debug/DumpPhpDocTypeRule.php new file mode 100644 index 0000000000..be867366b0 --- /dev/null +++ b/src/Rules/Debug/DumpPhpDocTypeRule.php @@ -0,0 +1,59 @@ + + */ +final class DumpPhpDocTypeRule implements Rule +{ + + public function __construct(private ReflectionProvider $reflectionProvider, private Printer $printer) + { + } + + public function getNodeType(): string + { + return Node\Expr\FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Name) { + return []; + } + + $functionName = $this->reflectionProvider->resolveFunctionName($node->name, $scope); + if ($functionName === null) { + return []; + } + + if (strtolower($functionName) !== 'phpstan\dumpphpdoctype') { + return []; + } + + if (count($node->getArgs()) === 0) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf( + 'Dumped type: %s', + $this->printer->print($scope->getType($node->getArgs()[0]->value)->toPhpDocNode()), + ), + )->nonIgnorable()->identifier('phpstan.dumpPhpDocType')->build(), + ]; + } + +} diff --git a/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php b/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php index 3ecbecaa7f..23338924eb 100644 --- a/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php +++ b/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php @@ -29,6 +29,7 @@ final class CallToFunctionStatementWithoutSideEffectsRule implements Rule public const PHPSTAN_TESTING_FUNCTIONS = [ 'PHPStan\\dumpType', + 'PHPStan\\dumpPhpDocType', 'PHPStan\\debugScope', 'PHPStan\\Testing\\assertType', 'PHPStan\\Testing\\assertNativeType', diff --git a/src/dumpType.php b/src/dumpType.php index 7fa89bc896..3d4cda24f7 100644 --- a/src/dumpType.php +++ b/src/dumpType.php @@ -13,3 +13,15 @@ function dumpType($value) // phpcs:ignore Squiz.Functions.GlobalFunction.Found { return null; } + +/** + * @phpstan-pure + * @param mixed $value + * @return mixed + * + * @throws void + */ +function dumpPhpDocType($value) // phpcs:ignore Squiz.Functions.GlobalFunction.Found +{ + return null; +} diff --git a/tests/PHPStan/Rules/Debug/DumpPhpDocTypeRuleTest.php b/tests/PHPStan/Rules/Debug/DumpPhpDocTypeRuleTest.php new file mode 100644 index 0000000000..ed56b46bd7 --- /dev/null +++ b/tests/PHPStan/Rules/Debug/DumpPhpDocTypeRuleTest.php @@ -0,0 +1,106 @@ + + */ +class DumpPhpDocTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DumpPhpDocTypeRule($this->createReflectionProvider(), new Printer()); + } + + public function testRuleSymbols(): void + { + $this->analyse([__DIR__ . '/data/dump-phpdoc-type.php'], [ + [ + "Dumped type: array{'': ''}", + 5, + ], + [ + "Dumped type: array{'\0': 'NUL', NUL: '\0'}", + 6, + ], + [ + "Dumped type: array{'\001': 'SOH', SOH: '\001'}", + 7, + ], + [ + "Dumped type: array{'\t': 'HT', HT: '\t'}", + 8, + ], + [ + "Dumped type: array{' ': 'SP', SP: ' '}", + 11, + ], + [ + "Dumped type: array{'foo ': 'ends with SP', ' foo': 'starts with SP', ' foo ': 'surrounded by SP', foo: 'no SP'}", + 12, + ], + [ + "Dumped type: array{'foo?': 'foo?'}", + 15, + ], + [ + "Dumped type: array{shallwedance: 'yes'}", + 16, + ], + [ + "Dumped type: array{'shallwedance?': 'yes'}", + 17, + ], + [ + "Dumped type: array{'Shall we dance': 'yes'}", + 18, + ], + [ + "Dumped type: array{'Shall we dance?': 'yes'}", + 19, + ], + [ + "Dumped type: array{shall_we_dance: 'yes'}", + 20, + ], + [ + "Dumped type: array{'shall_we_dance?': 'yes'}", + 21, + ], + [ + "Dumped type: array{shall-we-dance: 'yes'}", + 22, + ], + [ + "Dumped type: array{'shall-we-dance?': 'yes'}", + 23, + ], + [ + "Dumped type: array{'Let\'s go': 'Let\'s go'}", + 24, + ], + [ + "Dumped type: array{Foo\\Bar: 'Foo\\\\Bar'}", + 25, + ], + [ + "Dumped type: array{'3.14': 3.14}", + 26, + ], + [ + 'Dumped type: array{1: true, 0: false}', + 27, + ], + [ + 'Dumped type: T', + 36, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Debug/data/dump-phpdoc-type.php b/tests/PHPStan/Rules/Debug/data/dump-phpdoc-type.php new file mode 100644 index 0000000000..e8d009fe0f --- /dev/null +++ b/tests/PHPStan/Rules/Debug/data/dump-phpdoc-type.php @@ -0,0 +1,39 @@ + '']); +dumpPhpDocType(["\0" => 'NUL', 'NUL' => "\0"]); +dumpPhpDocType(["\x01" => 'SOH', 'SOH' => "\x01"]); +dumpPhpDocType(["\t" => 'HT', 'HT' => "\t"]); + +// Space +dumpPhpDocType([" " => 'SP', 'SP' => ' ']); +dumpPhpDocType(["foo " => 'ends with SP', " foo" => 'starts with SP', " foo " => 'surrounded by SP', 'foo' => 'no SP']); + +// Punctuation marks +dumpPhpDocType(["foo?" => 'foo?']); +dumpPhpDocType(["shallwedance" => 'yes']); +dumpPhpDocType(["shallwedance?" => 'yes']); +dumpPhpDocType(["Shall we dance" => 'yes']); +dumpPhpDocType(["Shall we dance?" => 'yes']); +dumpPhpDocType(["shall_we_dance" => 'yes']); +dumpPhpDocType(["shall_we_dance?" => 'yes']); +dumpPhpDocType(["shall-we-dance" => 'yes']); +dumpPhpDocType(["shall-we-dance?" => 'yes']); +dumpPhpDocType(['Let\'s go' => "Let's go"]); +dumpPhpDocType(['Foo\\Bar' => 'Foo\\Bar']); +dumpPhpDocType(['3.14' => 3.14]); +dumpPhpDocType([true => true, false => false]); + +/** + * @template T + * @param T $value + * @return T + */ +function id(mixed $value): mixed +{ + dumpPhpDocType($value); + + return $value; +}