diff --git a/src/Node/Printer/Printer.php b/src/Node/Printer/Printer.php index 61ffd313960..af6787dd9e7 100644 --- a/src/Node/Printer/Printer.php +++ b/src/Node/Printer/Printer.php @@ -5,6 +5,9 @@ use Override; use PhpParser\Node; use PhpParser\Node\Expr; +use PhpParser\Node\Expr\Cast; +use PhpParser\Node\Scalar\Int_; +use PhpParser\Node\Scalar\InterpolatedString; use PhpParser\Node\Scalar\String_; use PhpParser\PrettyPrinter\Standard; use PHPStan\DependencyInjection\AutowiredService; @@ -31,6 +34,7 @@ use PHPStan\Node\MethodCallableNode; use PHPStan\Node\StaticMethodCallableNode; use PHPStan\Type\VerbosityLevel; +use function in_array; use function preg_match; use function sprintf; use function str_contains; @@ -103,6 +107,85 @@ protected function pObjectProperty(Node $node): string return parent::pObjectProperty($node); } + /** + * The `kind` attribute records how a node was spelled in the source, not + * what it means: `'k'`, `"k"` and a nowdoc holding `k` are the same string, + * `1` and `0x1` are the same integer, `array(...)` and `[...]` are the same + * array. Printing it back would make `$a['k']` and `$a["k"]` different + * expression keys, so the analyser would track them as two unrelated + * expressions and narrowing done on one would not apply to the other. + * + * Everything below therefore prints one canonical form per value, chosen + * without looking at `kind`. + */ + #[Override] + protected function pScalar_String(String_ $node): string // phpcs:ignore + { + // keep escape sequences readable and the printed form single-line + if (preg_match('/[\x00-\x1f\x7f]/', $node->value) === 1) { + return '"' . $this->escapeString($node->value, '"') . '"'; + } + + return $this->pSingleQuotedString($node->value); + } + + #[Override] + protected function pScalar_InterpolatedString(InterpolatedString $node): string // phpcs:ignore + { + return '"' . $this->pEncapsList($node->parts, '"') . '"'; + } + + #[Override] + protected function pScalar_Int(Int_ $node): string // phpcs:ignore + { + if ($node->getAttribute('kind', Int_::KIND_DEC) === Int_::KIND_DEC) { + return parent::pScalar_Int($node); + } + + return parent::pScalar_Int(new Int_($node->value)); + } + + #[Override] + protected function pExpr_Array(Expr\Array_ $node): string // phpcs:ignore + { + if ($node->getAttribute('kind') === Expr\Array_::KIND_SHORT) { + return parent::pExpr_Array($node); + } + + return parent::pExpr_Array(new Expr\Array_($node->items, ['kind' => Expr\Array_::KIND_SHORT])); + } + + #[Override] + protected function pExpr_List(Expr\List_ $node): string // phpcs:ignore + { + if ($node->getAttribute('kind') === Expr\List_::KIND_ARRAY) { + return parent::pExpr_List($node); + } + + return parent::pExpr_List(new Expr\List_($node->items, ['kind' => Expr\List_::KIND_ARRAY])); + } + + #[Override] + protected function pExpr_Cast_Double(Cast\Double $node, int $precedence, int $lhsPrecedence): string // phpcs:ignore + { + return $this->pPrefixOp(Cast\Double::class, '(float) ', $node->expr, $precedence, $lhsPrecedence); + } + + /** + * `true`, `false` and `null` are the only case-insensitive constant names + * in PHP, and `\true` means the same as `true`. + */ + #[Override] + protected function pExpr_ConstFetch(Expr\ConstFetch $node): string // phpcs:ignore + { + $lowerName = $node->name->toLowerString(); + if (in_array($lowerName, ['true', 'false', 'null'], true)) { + return $lowerName; + } + + return parent::pExpr_ConstFetch($node); + } + protected function pPHPStan_Node_TypeExpr(TypeExpr $expr): string // phpcs:ignore { return sprintf('__phpstanType(%s)', $expr->getExprType()->describe(VerbosityLevel::precise())); diff --git a/tests/PHPStan/Analyser/nsrt/bug-15060.php b/tests/PHPStan/Analyser/nsrt/bug-15060.php new file mode 100644 index 00000000000..77a859ec185 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15060.php @@ -0,0 +1,95 @@ +', $searchParams['test']); + assertType('array', $searchParams["test"]); + } + + if (is_array($searchParams["test"]) && $searchParams["test"]) { + assertType('non-empty-array', $searchParams['test']); + assertType('non-empty-array', $searchParams["test"]); + } + } +} + +function heredocAndNowdoc($a): void +{ + if (is_array($a['test'])) { + assertType('array', $a["test"]); + assertType('array', $a[<<<'TEST' +test +TEST]); + assertType('array', $a[<<', $a[<<', $a[<<', $a[0x1]); + assertType('array', $a[0b1]); + assertType('array', $a[01]); + } +} + +function arraySyntax($a, string $s): void +{ + if (is_array($a[[$s][0]])) { + assertType('array', $a[array($s)[0]]); + } +} + +function doubleCast($a, float $f): void +{ + if (is_array($a[(float) $f])) { + assertType('array', $a[(double) $f]); + } +} + +function constantCase($a): void +{ + if (is_array($a[true])) { + assertType('array', $a[TRUE]); + assertType('array', $a[\true]); + } + if (is_array($a[null])) { + assertType('array', $a[NULL]); + } + if (is_array($a[false])) { + assertType('array', $a[FALSE]); + } +} diff --git a/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php b/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php index 75293922f7f..70effcf0125 100644 --- a/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php @@ -26,7 +26,7 @@ public function testDuplicateKeys(): void define('PHPSTAN_DUPLICATE_KEY', 0); $this->analyse([__DIR__ . '/data/duplicate-keys.php'], [ [ - 'Array has 2 duplicate keys with value \'\' (null, NULL).', + 'Array has 2 duplicate keys with value \'\' (null, null).', 15, ], [ diff --git a/tests/PHPStan/Rules/Keywords/DeclareStrictTypesRuleTest.php b/tests/PHPStan/Rules/Keywords/DeclareStrictTypesRuleTest.php index 7aac5f8019e..02aa02d597e 100644 --- a/tests/PHPStan/Rules/Keywords/DeclareStrictTypesRuleTest.php +++ b/tests/PHPStan/Rules/Keywords/DeclareStrictTypesRuleTest.php @@ -98,7 +98,7 @@ public function testNonsenseBool(): void { $this->analyse([__DIR__ . '/data/declare-strict-nonsense-bool.php'], [ [ - 'Declare strict_types must have 0 or 1 as its value, \true given.', + 'Declare strict_types must have 0 or 1 as its value, true given.', 1, ], ]);