Skip to content

Commit 1501355

Browse files
ondrejmirtesclaude
andcommitted
Remember an expression's printed form as the printer produces it
Every level of a chain is printed for its own expression key, and printing a level descends through everything below it, so a chain of depth N was printed O(N^2) times over — the levels were already printed as part of the level above, just not remembered. p() is where the printer descends into every construct, so remembering each expression's printed form there makes the levels below it O(1), and it fills the same cache ExprPrinter reads: a key printed once is never rebuilt, no matter which expression it was first reached from. Only the standalone form is remembered — at a lower precedence the printer may parenthesise, and a form containing a newline was indented for the level it was printed at, while expression keys are printed at the top level. ArgumentsNormalizer copied the whole attribute set onto the calls it rebuilds, which handed a reordered call the printed form of the original, named arguments and all. That was already so before this change (28 occurrences in the Analyser test suite), harmless because nothing recomputed the key; remembering more of them would have made it reachable, so the derived form is now left behind. Interleaved A/B on a quiet machine, user CPU, result cache cleared, turbo extension enabled: -32.6% on tests/bench/data/nullsafe-chain-walk.php (2.27s -> 1.53s, 8/8 pairs, paired t=12.4). Ordinary code and peak memory are unaffected (src/Type -0.09%, t=0.2; peak RSS identical). Analysis output is unchanged. See phpstan/phpstan#14991 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016XhKccs7xeB1wRTAQEemc2
1 parent 4b6d203 commit 1501355

2 files changed

Lines changed: 69 additions & 7 deletions

File tree

‎src/Analyser/ArgumentsNormalizer.php‎

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\Analyser;
44

5+
use PhpParser\Node as PhpParserNode;
56
use PhpParser\Node\Arg;
67
use PhpParser\Node\Expr\Array_;
78
use PhpParser\Node\Expr\FuncCall;
@@ -12,6 +13,7 @@
1213
use PhpParser\Node\Scalar\Int_;
1314
use PhpParser\Node\Scalar\String_;
1415
use PHPStan\Node\Expr\TypeExpr;
16+
use PHPStan\Node\Printer\ExprPrinter;
1517
use PHPStan\Reflection\ParametersAcceptor;
1618
use PHPStan\Reflection\ParametersAcceptorSelector;
1719
use PHPStan\ShouldNotHappenException;
@@ -91,7 +93,7 @@ public static function reorderCallUserFuncArguments(
9193
return [$parametersAcceptor, new FuncCall(
9294
$callbackArg->value,
9395
$passThruArgs,
94-
$callUserFuncCall->getAttributes(),
96+
self::attributesWithoutPrintedForm($callUserFuncCall),
9597
), $acceptsNamedArguments];
9698
}
9799

@@ -161,7 +163,7 @@ public static function reorderCallUserFuncArrayArguments(
161163
$item->value,
162164
$item->byRef,
163165
$item->unpack,
164-
$item->getAttributes(),
166+
self::attributesWithoutPrintedForm($item),
165167
is_string($key) ? new Identifier($key) : null,
166168
);
167169
}
@@ -187,7 +189,7 @@ public static function reorderCallUserFuncArrayArguments(
187189
return [$parametersAcceptor, new FuncCall(
188190
$callbackArg->value,
189191
$passThruArgs,
190-
$callUserFuncArrayCall->getAttributes(),
192+
self::attributesWithoutPrintedForm($callUserFuncArrayCall),
191193
), $acceptsNamedArguments];
192194
}
193195

@@ -211,7 +213,7 @@ public static function reorderFuncArguments(
211213
return new FuncCall(
212214
$functionCall->name,
213215
$reorderedArgs,
214-
$functionCall->getAttributes(),
216+
self::attributesWithoutPrintedForm($functionCall),
215217
);
216218
}
217219

@@ -236,7 +238,7 @@ public static function reorderMethodArguments(
236238
$methodCall->var,
237239
$methodCall->name,
238240
$reorderedArgs,
239-
$methodCall->getAttributes(),
241+
self::attributesWithoutPrintedForm($methodCall),
240242
);
241243
}
242244

@@ -261,7 +263,7 @@ public static function reorderStaticCallArguments(
261263
$staticCall->class,
262264
$staticCall->name,
263265
$reorderedArgs,
264-
$staticCall->getAttributes(),
266+
self::attributesWithoutPrintedForm($staticCall),
265267
);
266268
}
267269

@@ -285,7 +287,7 @@ public static function reorderNewArguments(
285287
return new New_(
286288
$new->class,
287289
$reorderedArgs,
288-
$new->getAttributes(),
290+
self::attributesWithoutPrintedForm($new),
289291
);
290292
}
291293

@@ -439,4 +441,20 @@ public static function reorderArgs(ParametersAcceptor $parametersAcceptor, array
439441
return $reorderedArgs;
440442
}
441443

444+
/**
445+
* The printed form of an expression is derived from its own arguments, so
446+
* it must not travel to a node whose arguments were just reordered — the
447+
* normalized call would report the original's expression key, named
448+
* arguments and all.
449+
*
450+
* @return array<string, mixed>
451+
*/
452+
private static function attributesWithoutPrintedForm(PhpParserNode $node): array
453+
{
454+
$attributes = $node->getAttributes();
455+
unset($attributes[ExprPrinter::ATTRIBUTE_CACHE_KEY]);
456+
457+
return $attributes;
458+
}
459+
442460
}

‎src/Node/Printer/Printer.php‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Override;
66
use PhpParser\Node;
7+
use PhpParser\Node\Expr;
78
use PhpParser\Node\Scalar\String_;
89
use PhpParser\PrettyPrinter\Standard;
910
use PHPStan\DependencyInjection\AutowiredService;
@@ -36,6 +37,7 @@
3637
use PHPStan\Type\VerbosityLevel;
3738
use function preg_match;
3839
use function sprintf;
40+
use function str_contains;
3941

4042
/**
4143
* @api
@@ -44,6 +46,48 @@
4446
final class Printer extends Standard
4547
{
4648

49+
/**
50+
* Every construct the printer descends into comes back through here, and
51+
* each level of a chain is printed again in turn for its own expression
52+
* key, so a chain of depth N costs O(N^2). Remembering each expression's
53+
* printed form on its node makes the levels below it O(1) — and it is the
54+
* very same cache ExprPrinter fills, so a key printed once is never
55+
* rebuilt, wherever it was first reached from.
56+
*
57+
* Only the standalone form is remembered: at a lower precedence the
58+
* printer may parenthesise, and a form containing a newline was indented
59+
* for the level it was printed at, while expression keys are printed at
60+
* the top level.
61+
*/
62+
#[Override]
63+
protected function p(
64+
Node $node,
65+
int $precedence = self::MAX_PRECEDENCE,
66+
int $lhsPrecedence = self::MAX_PRECEDENCE,
67+
bool $parentFormatPreserved = false,
68+
): string
69+
{
70+
if (
71+
!$node instanceof Expr
72+
|| $precedence !== self::MAX_PRECEDENCE
73+
|| $lhsPrecedence !== self::MAX_PRECEDENCE
74+
) {
75+
return parent::p($node, $precedence, $lhsPrecedence, $parentFormatPreserved);
76+
}
77+
78+
$printed = $node->getAttribute(ExprPrinter::ATTRIBUTE_CACHE_KEY);
79+
if ($printed !== null) {
80+
return $printed;
81+
}
82+
83+
$printed = parent::p($node, parentFormatPreserved: $parentFormatPreserved);
84+
if (!str_contains($printed, "\n")) {
85+
$node->setAttribute(ExprPrinter::ATTRIBUTE_CACHE_KEY, $printed);
86+
}
87+
88+
return $printed;
89+
}
90+
4791
/**
4892
* Normalize curly-brace member access with a constant string name to the
4993
* bareword form, so that e.g. `$obj->{'n'}` and `$obj->n` (or `$obj->{'n'}()`

0 commit comments

Comments
 (0)