Skip to content

Add ExprUsedAsStringNode virtual node emitted for expressions used as a string#5787

Open
phpstan-bot wants to merge 2 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-y7wgwji
Open

Add ExprUsedAsStringNode virtual node emitted for expressions used as a string#5787
phpstan-bot wants to merge 2 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-y7wgwji

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

This implements the feature request for a virtual node that lets rules hook onto every place where an expression's value is used as a string, instead of hooking on Node::class and doing instanceof casing for String_, Concat, InterpolatedString, AssignOp\Concat, InlineHTML, etc. (the use-case being e.g. detecting <script> tags without a nonce= attribute in built/echoed HTML).

A new ExprUsedAsStringNode is emitted for each expression whose value is used as a string. Crucially, a concatenation chain or interpolated string is reported once for the whole expression, solving the reporter's main pain point that hooking on BinaryOp\Concat fires for every concat in a long chain.

Changes

  • src/Node/ExprUsedAsStringNode.php — new @api virtual node wrapping the used Expr (getExpr()).
  • src/Parser/ExprUsedAsStringVisitor.php — new rich-parser visitor (auto-tagged via NodeVisitorrichParserNodeVisitor) that marks expressions used as a string with the isExprUsedAsString attribute and "claims" nested concats / interpolation parts so the whole concatenation is reported once.
  • src/Analyser/NodeScopeResolver.php — emits ExprUsedAsStringNode in processExprNode() for any marked expression, and for InlineHTML statements (wrapping a synthetic Scalar\String_).
  • Tests: tests/PHPStan/Node/ExprUsedAsStringRule.php, ExprUsedAsStringRuleTest.php, data/expr-used-as-string.php.

Contexts covered (the "used as a string" axis):

  • echo / print arguments
  • (string) cast operand
  • string concatenation . and concat-assignment .= (whole chain, once)
  • string interpolation and heredoc with interpolation (whole string, once)
  • inline HTML between ?> / <?php
  • dynamic-name expressions, all coercing their name to string: $foo->{$s}, $foo?->{$s}, $foo->{$s}(), $foo?->{$s}(), Foo::${$s}, Foo::{$s}(), Foo::{$s} (dynamic class const), and variable-variables $$s.

Root cause

Not a bug but a missing extension point. Determining "this expression ends up as a string" was previously left to each rule, which had to special-case many AST node types and de-duplicate nested concatenations itself. The pattern is centralized here: the value-as-string contexts are exactly the places where PHP coerces a value to string, so they are marked at parse time and a single virtual node is emitted from the one processExprNode() choke point. Nesting is handled by claiming child concats/interpolation parts, which makes a concatenation chain converge to a single node regardless of how the marking contexts overlap.

Analogous cases

The reported example listed only output/coercion contexts; the maintainer additionally asked for echo, print, (string) cast and dynamic-name expressions like $foo->{$s}. All of these are on the same "expression coerced to string" axis and are implemented and tested here. The "value flowing into a string-typed slot" family (passing an argument to a string parameter, assigning to a string-typed property, string literals in const/property/parameter defaults) is a distinct concept that needs target-type information unavailable at parse time, so it is intentionally not part of this node and can be added separately.

Test

ExprUsedAsStringRuleTest registers a rule on ExprUsedAsStringNode and asserts, via data/expr-used-as-string.php, that exactly one node fires per string usage across all the contexts above — including that '<script src="' . $s . '"...', $s .= ' src="' . $s . '"' and a heredoc appended with .= each fire once (the nested concat / interpolated string does not fire a second time). The test was confirmed to fail before the NodeScopeResolver change.

Fixes phpstan/phpstan#13008

staabm and others added 2 commits May 30, 2026 15:17
…as a string

- Add `PHPStan\Node\ExprUsedAsStringNode` virtual node wrapping the `Expr` whose value is used as a string.
- Add `ExprUsedAsStringVisitor` (rich parser visitor) that marks expressions used in a string context via the `isExprUsedAsString` attribute: echo/print arguments, `(string)` cast operands, string concatenation (`.` and `.=`), string interpolation/heredoc, and dynamic-name expressions (`$foo->{$s}`, `$foo->{$s}()`, `Foo::${$s}`, `Foo::{$s}()`, `Foo::{$s}`, `$$s`).
- Concatenation chains and interpolated strings are reported once for the whole expression instead of once per nested operand, by "claiming" nested concats/interpolation parts, so a rule can interpret the built string as a single unit.
- `NodeScopeResolver::processExprNode()` emits the node for any marked expression; inline HTML emits the node wrapping a synthetic `String_`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

New virtual node type when expressions are used as a string

2 participants