Skip to content
Closed
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
83 changes: 83 additions & 0 deletions src/Node/Printer/Printer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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()));
Expand Down
95 changes: 95 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15060.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php declare(strict_types = 1);

namespace Bug15060;

use function PHPStan\Testing\assertType;

function search($searchParams): void
{
if (isset($searchParams['test'])) {
assertType('mixed~null', $searchParams['test']);
if ($searchParams["test"]) {
assertType('mixed~(0|0.0|\'\'|\'0\'|array{}|false|null)', $searchParams['test']);
assertType('mixed~(0|0.0|\'\'|\'0\'|array{}|false|null)', $searchParams["test"]);
}

if (is_array($searchParams["test"])) {
assertType('array<mixed, mixed>', $searchParams['test']);
assertType('array<mixed, mixed>', $searchParams["test"]);
}

if (is_array($searchParams["test"]) && $searchParams["test"]) {
assertType('non-empty-array<mixed, mixed>', $searchParams['test']);
assertType('non-empty-array<mixed, mixed>', $searchParams["test"]);
}
}
}

function heredocAndNowdoc($a): void
{
if (is_array($a['test'])) {
assertType('array<mixed, mixed>', $a["test"]);
assertType('array<mixed, mixed>', $a[<<<'TEST'
test
TEST]);
assertType('array<mixed, mixed>', $a[<<<TEST
test
TEST]);
}
}

function escapeSequences($a): void
{
if (is_array($a["a\nb"])) {
assertType('array<mixed, mixed>', $a[<<<TEST
a
b
TEST]);
}
}

function interpolatedString($a, string $s): void
{
if (is_array($a["x$s"])) {
assertType('array<mixed, mixed>', $a[<<<TEST
x$s
TEST]);
}
}

function integerBases($a): void
{
if (is_array($a[1])) {
assertType('array<mixed, mixed>', $a[0x1]);
assertType('array<mixed, mixed>', $a[0b1]);
assertType('array<mixed, mixed>', $a[01]);
}
}

function arraySyntax($a, string $s): void
{
if (is_array($a[[$s][0]])) {
assertType('array<mixed, mixed>', $a[array($s)[0]]);
}
}

function doubleCast($a, float $f): void
{
if (is_array($a[(float) $f])) {
assertType('array<mixed, mixed>', $a[(double) $f]);
}
}

function constantCase($a): void
{
if (is_array($a[true])) {
assertType('array<mixed, mixed>', $a[TRUE]);
assertType('array<mixed, mixed>', $a[\true]);
}
if (is_array($a[null])) {
assertType('array<mixed, mixed>', $a[NULL]);
}
if (is_array($a[false])) {
assertType('array<mixed, mixed>', $a[FALSE]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
],
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
],
]);
Expand Down
Loading