diff --git a/src/Analyser/ScopeOps.php b/src/Analyser/ScopeOps.php index e5fb684ead..0a52507740 100644 --- a/src/Analyser/ScopeOps.php +++ b/src/Analyser/ScopeOps.php @@ -49,7 +49,12 @@ final class ScopeOps private const CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME = 'containsSuperGlobal'; /** Virtual-node key prefixes whose printers include all children verbatim. */ - private const COMPOSITIONAL_VIRTUAL_KEY_PREFIXES = ['__phpstanPossiblyImpure(', '__phpstanRemembered(']; + // A prefix may be listed only when the printer emits every getSubNodeNames() + // sub-node verbatim (or the node walks no sub-nodes at all). The foreach/ + // parameter original-value markers (__phpstanOriginalForeachKey etc.) hide a + // synthesized Variable child on purpose - reassigning the variable must + // invalidate them through containment - so they can never be listed here. + private const COMPOSITIONAL_VIRTUAL_KEY_PREFIXES = ['__phpstanForeachValueByRef(', '__phpstanIntertwinedVariableByReference(', '__phpstanPossiblyImpure(', '__phpstanPropertyInitialization(', '__phpstanRemembered(']; /** * Mirrors MutatingScope::getNodeKey(). @@ -611,7 +616,7 @@ public static function invalidateExpressionEntries( // a substring cannot belong to an expression containing the invalidated one, so // the much more expensive per-expression check can be skipped without being called. $canUseKeyPrefilter = $exprStringToInvalidate !== '$this' - && !str_contains($exprStringToInvalidate, '__phpstan') + && !self::keyMayHideSubExpressions($exprStringToInvalidate) && !str_contains($exprStringToInvalidate, '/*'); foreach ($expressionTypes as $exprString => $exprTypeHolder) { @@ -735,7 +740,7 @@ public static function invalidateMethodsOnExpression( // call's key embeds its receiver's key verbatim, so when the invalidated key // does not occur in the entry's key, the receiver cannot match and the entry // can be kept without re-printing the receiver. - $canUseKeyPrefilter = !str_contains($exprStringToInvalidate, '__phpstan') + $canUseKeyPrefilter = !self::keyMayHideSubExpressions($exprStringToInvalidate) && !str_contains($exprStringToInvalidate, '/*'); foreach ($expressionTypes as $exprString => $exprTypeHolder) { @@ -844,7 +849,7 @@ public static function shouldInvalidateExpression(MutatingScope $scope, ExprPrin // - keys carrying a getNodeKey() suffix ('/*…*/') are not plain substrings. if ( $exprStringToInvalidate !== '$this' - && !str_contains($exprStringToInvalidate, '__phpstan') + && !self::keyMayHideSubExpressions($exprStringToInvalidate) && !str_contains($exprStringToInvalidate, '/*') && !str_contains($exprString, $exprStringToInvalidate) && !self::keyMayHideSubExpressions($exprString) diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index adeb565965..26a5f31c39 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -20,7 +20,7 @@ final class TurboExtensionEnabler * version is the short SHA of the last commit touching turbo-ext/src/, * enforced by the phar.yml turbo-version job. */ - public const EXPECTED_EXTENSION_VERSION = '86312b7'; + public const EXPECTED_EXTENSION_VERSION = '1ecf6e0'; private static bool $typeCombinatorCacheEnabled = false; diff --git a/turbo-ext/src/ScopeOps.cpp b/turbo-ext/src/ScopeOps.cpp index 307f228b36..4ae5517fb9 100644 --- a/turbo-ext/src/ScopeOps.cpp +++ b/turbo-ext/src/ScopeOps.cpp @@ -795,7 +795,7 @@ class ScopeOps * containing the invalidated one, so the much more expensive * per-expression check can be skipped without being called. */ const bool canUseKeyPrefilter = !query.isThis - && !strContains(exprStringToInvalidate, "__phpstan", sizeof("__phpstan") - 1) + && !keyMayHideSubExpressions(exprStringToInvalidate) && !strContains(exprStringToInvalidate, "/*", 2); bool invalidated = false; @@ -1006,7 +1006,7 @@ class ScopeOps * method call's key embeds its receiver's key verbatim, so when the * invalidated key does not occur in the entry's key, the receiver cannot * match and the entry can be kept without re-printing the receiver. */ - const bool canUseKeyPrefilter = !strContains(exprStringToInvalidate, "__phpstan", sizeof("__phpstan") - 1) + const bool canUseKeyPrefilter = !keyMayHideSubExpressions(exprStringToInvalidate) && !strContains(exprStringToInvalidate, "/*", 2); for (auto entry : expressionTypes) { @@ -1657,8 +1657,15 @@ class ScopeOps */ static bool keyMayHideSubExpressions(zend_string *key) { + /* Mirror of ScopeOps::COMPOSITIONAL_VIRTUAL_KEY_PREFIXES - a prefix may + * be listed only when the printer emits every getSubNodeNames() sub-node + * verbatim (or the node walks no sub-nodes at all); the foreach/parameter + * original-value markers hide a synthesized Variable child on purpose. */ static const struct { const char *prefix; size_t len; } compositionalPrefixes[] = { + { "__phpstanForeachValueByRef(", sizeof("__phpstanForeachValueByRef(") - 1 }, + { "__phpstanIntertwinedVariableByReference(", sizeof("__phpstanIntertwinedVariableByReference(") - 1 }, { "__phpstanPossiblyImpure(", sizeof("__phpstanPossiblyImpure(") - 1 }, + { "__phpstanPropertyInitialization(", sizeof("__phpstanPropertyInitialization(") - 1 }, { "__phpstanRemembered(", sizeof("__phpstanRemembered(") - 1 }, }; @@ -1901,7 +1908,7 @@ class ScopeOps /* Compositional-key substring gate */ if (!query.isThis - && !strContains(query.exprStringToInvalidate, "__phpstan", sizeof("__phpstan") - 1) + && !keyMayHideSubExpressions(query.exprStringToInvalidate) && !strContains(query.exprStringToInvalidate, "/*", 2) && !strContainsStr(exprString, query.exprStringToInvalidate) && !keyMayHideSubExpressions(exprString)) {