Skip to content
Open
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
24 changes: 21 additions & 3 deletions bin/functionMetadata_original.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,24 @@
* keyed by lowercase function name or "Class::method". resources/functionMetadata.php
* is generated from this file by bin/generate-function-metadata.php.
*
* Each entry is exactly one of these shapes:
* Each entry has one of these shapes:
*
* - ['hasSideEffects' => bool]
* false: the call is pure. true: the call has side effects.
* - ['pureUnlessCallableIsImpureParameters' => array<string, true>]
* the call is pure unless one of the listed callable parameters
* (keyed by parameter name) receives an impure callable, e.g. array_map()
* whose only side effects come from its 'callback' argument.
* - ['pureUnlessParameterPassedParameters' => array<string, true>]
* the call is pure unless one of the listed (by-ref out) parameters
* (keyed by parameter name) receives an argument, e.g. str_replace()
* whose only side effect is writing to its optional 'count' argument.
*
* The last two can be combined for a call that is pure unless either happens,
* e.g. preg_replace_callback() (impure callback or a passed 'count').
*/

/** @var array<string, array{hasSideEffects: bool}|array{pureUnlessCallableIsImpureParameters: array<string, bool>}> */
/** @var array<string, array{hasSideEffects: bool}|array{pureUnlessCallableIsImpureParameters: array<string, bool>}|array{pureUnlessParameterPassedParameters: array<string, bool>}|array{pureUnlessCallableIsImpureParameters: array<string, bool>, pureUnlessParameterPassedParameters: array<string, bool>}> */
return [
'abs' => ['hasSideEffects' => false],
'acos' => ['hasSideEffects' => false],
Expand Down Expand Up @@ -264,14 +271,25 @@
'output_reset_rewrite_vars' => ['hasSideEffects' => true],
'pclose' => ['hasSideEffects' => true],
'popen' => ['hasSideEffects' => true],
'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]],
'preg_filter' => ['pureUnlessParameterPassedParameters' => ['count' => true]],
// 'matches'/'subpatterns': PHP 8+ uses the php-8-stubs parameter name, PHP <8 falls
// back to the legacy functionMap.php name.
'preg_match' => ['pureUnlessParameterPassedParameters' => ['matches' => true, 'subpatterns' => true]],
'preg_match_all' => ['pureUnlessParameterPassedParameters' => ['matches' => true, 'subpatterns' => true]],
'preg_replace' => ['pureUnlessParameterPassedParameters' => ['count' => true]],
'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true], 'pureUnlessParameterPassedParameters' => ['count' => true]],
'similar_text' => ['pureUnlessParameterPassedParameters' => ['percent' => true]],
'readfile' => ['hasSideEffects' => true],
'rename' => ['hasSideEffects' => true],
'rewind' => ['hasSideEffects' => true],
'rmdir' => ['hasSideEffects' => true],
'sprintf' => ['hasSideEffects' => false],
'str_decrement' => ['hasSideEffects' => false],
'str_increment' => ['hasSideEffects' => false],
// 'count'/'replace_count': PHP 8+ uses the php-8-stubs parameter name, PHP <8 falls
// back to the legacy functionMap.php name.
'str_ireplace' => ['pureUnlessParameterPassedParameters' => ['count' => true, 'replace_count' => true]],
'str_replace' => ['pureUnlessParameterPassedParameters' => ['count' => true, 'replace_count' => true]],
'symlink' => ['hasSideEffects' => true],
'time' => ['hasSideEffects' => true],
'tempnam' => ['hasSideEffects' => true],
Expand Down
30 changes: 28 additions & 2 deletions bin/generate-function-metadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public function enterNode(Node $node)
);
}

/** @var array<string, array{hasSideEffects?: bool, pureUnlessCallableIsImpureParameters?: array<string, bool>}> $metadata */
/** @var array<string, array{hasSideEffects?: bool, pureUnlessCallableIsImpureParameters?: array<string, bool>, pureUnlessParameterPassedParameters?: array<string, bool>}> $metadata */
$metadata = require __DIR__ . '/functionMetadata_original.php';
foreach ($visitor->functions as $functionName) {
if (array_key_exists($functionName, $metadata)) {
Expand All @@ -134,6 +134,14 @@ public function enterNode(Node $node)

continue;
}

if (isset($metadata[$functionName]['pureUnlessParameterPassedParameters'])) {
$metadata[$functionName] = [
'pureUnlessParameterPassedParameters' => $metadata[$functionName]['pureUnlessParameterPassedParameters'],
];

continue;
}
}
$metadata[$functionName] = ['hasSideEffects' => false];
}
Expand Down Expand Up @@ -192,9 +200,12 @@ public function enterNode(Node $node)
* - ['pureUnlessCallableIsImpureParameters' => array<string, true>] - pure unless
* one of the listed callable parameters (keyed by parameter name) receives an
* impure callable, e.g. array_map()'s 'callback'.
* - ['pureUnlessParameterPassedParameters' => array<string, true>] - pure unless
* one of the listed (by-ref out) parameters (keyed by parameter name) receives
* an argument, e.g. str_replace()'s 'replace_count'.
*/

/** @var array<string, array{hasSideEffects: bool}|array{pureUnlessCallableIsImpureParameters: array<string, bool>}> */
/** @var array<string, array{hasSideEffects: bool}|array{pureUnlessCallableIsImpureParameters: array<string, bool>}|array{pureUnlessParameterPassedParameters: array<string, bool>}> */
return [
%s
];
Expand All @@ -216,6 +227,20 @@ public function enterNode(Node $node)
),
),
];
$encodePureUnlessParameterPassedParameters = static fn (array $meta) => [
$escape('pureUnlessParameterPassedParameters'),
sprintf(
'[%s]',
implode(
' ,',
array_map(
static fn ($key, $param) => sprintf('%s => %s', $escape($key), $escape($param)),
array_keys($meta['pureUnlessParameterPassedParameters']),
$meta['pureUnlessParameterPassedParameters'],
),
),
),
];

foreach ($metadata as $name => $meta) {
$content .= sprintf(
Expand All @@ -224,6 +249,7 @@ public function enterNode(Node $node)
...match (true) {
isset($meta['hasSideEffects']) => $encodeHasSideEffects($meta),
isset($meta['pureUnlessCallableIsImpureParameters']) => $encodePureUnlessCallableIsImpureParameters($meta),
isset($meta['pureUnlessParameterPassedParameters']) => $encodePureUnlessParameterPassedParameters($meta),
default => throw new ShouldNotHappenException($escape($meta)),
},
);
Expand Down
14 changes: 12 additions & 2 deletions resources/functionMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@
* - ['pureUnlessCallableIsImpureParameters' => array<string, true>] - pure unless
* one of the listed callable parameters (keyed by parameter name) receives an
* impure callable, e.g. array_map()'s 'callback'.
* - ['pureUnlessParameterPassedParameters' => array<string, true>] - pure unless
* one of the listed (by-ref out) parameters (keyed by parameter name) receives
* an argument, e.g. str_replace()'s 'count'.
*/

/** @var array<string, array{hasSideEffects: bool}|array{pureUnlessCallableIsImpureParameters: array<string, bool>}> */
/** @var array<string, array{hasSideEffects: bool}|array{pureUnlessCallableIsImpureParameters: array<string, bool>}|array{pureUnlessParameterPassedParameters: array<string, bool>}|array{pureUnlessCallableIsImpureParameters: array<string, bool>, pureUnlessParameterPassedParameters: array<string, bool>}> */
return [

@staabm staabm Jul 9, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add

  • exec
  • exif_thumbnail
  • flock
  • fsockopen

?

searching with '&[^']+= thru resources/functionMap.php could reveal even more

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These optional by-ref parameters do have a side effect on the caller variable scope, but these functions have side effects as their primary purpose, so their purity does not change depending on whether the parameter is passed. In some cases it can be worth treating a function as #[\NoDiscard], but these do not seem to be such cases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a broader sweep of all optional by-ref out-params in functionMap.php. preg_filter is a clean match (same signature as the already-covered preg_replace, pure regex substitution with an optional count), so I added it. The remaining plausible candidates (grapheme_extract, Spoofchecker::areConfusable/isSuspicious, IntlTimeZone::getCanonicalID) are borderline — their purity depends on ICU/locale process globals — so I left them out for now; happy to revisit if you think they qualify.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reference, here is the full breakdown of the borderline candidates I left out, in case any are worth a follow-up.

FITS but deferred (medium confidence — purity depends on ICU/locale process globals):

Function out param Why deferred
grapheme_extract next GRAPHEME_EXTR_WORD/SENTENCE use ICU break-iterators bound to the process-global default locale (Locale::setDefault()), so identical args can differ across calls.
Spoofchecker::areConfusable error Pure evaluation against ICU confusables data, but an obscure extension whose runtime behavior is hard to verify.
Spoofchecker::isSuspicious error Same as above.
IntlTimeZone::getCanonicalID isSystemID Looks up ICU's compiled tz database (no PHP-mutable setting), safer than the others but still ICU-build dependent.

Looks pure at first glance, but classified impure (clear side effect / non-determinism):

Function out param Why impure
is_callable callable_name A "Class::method" string can trigger autoloading, independent of the by-ref param.
mb_ereg / mb_eregi registers Match result depends on the mutable global mb_regex_encoding().
idn_to_ascii / idn_to_utf8 idna_info Depends on the ICU build's IDNA/UTS46 tables.
IntlDateFormatter::parse / ::localtime parse_pos Falls back to the process-global date_default_timezone_get() when the formatter has no timezone.
xdiff_string_patch error In-memory on its face, but the extension is long unmaintained and its internal temp-file usage can't be verified.
yaml_parse ndocs Its callbacks array takes arbitrary user callables (same class as preg_replace_callback); that would need pureUnlessCallableIsImpureParameters on callbacks, not this mechanism.
ldap_parse_exop, ldap_control_paged_result_response retdata/retoid, cookie/estimated Documented as decode-only, but I could not verify they don't update the connection's cached last-error/status (ldap_control_paged_result_response is also removed in PHP 8.0).

Totals across the 79 optional/required by-ref out-param entries: 12 required-by-ref (skipped), 11 already handled, 51 impure, 5 fit (of which preg_filter is added here).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for looking into it. I agree these are mostly borderline.

one stood out for me: preg_replace_callback, preg_replace_callback_array

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. preg_replace_callback now carries both flags — the two verdicts were checked in sequence, so a pure callback short-circuited before the $count check; I combined them with TrinaryLogic::and() (in both the call and new paths) and flagged its $count. preg_replace_callback_array I left out: its callbacks live inside an array<string, callable> parameter, which the pureUnlessCallableIsImpureParameters mechanism (single callable param) cannot inspect, so flagging only its $count would wrongly treat it as pure when a callback in the array is impure. That one needs a separate mechanism.

'BackedEnum::from' => ['hasSideEffects' => false],
'BackedEnum::tryFrom' => ['hasSideEffects' => false],
Expand Down Expand Up @@ -1631,11 +1634,15 @@
'posix_ttyname' => ['hasSideEffects' => false],
'posix_uname' => ['hasSideEffects' => false],
'pow' => ['hasSideEffects' => false],
'preg_filter' => ['pureUnlessParameterPassedParameters' => ['count' => true]],
'preg_grep' => ['hasSideEffects' => false],
'preg_last_error' => ['hasSideEffects' => true],
'preg_last_error_msg' => ['hasSideEffects' => true],
'preg_match' => ['pureUnlessParameterPassedParameters' => ['matches' => true, 'subpatterns' => true]],
'preg_match_all' => ['pureUnlessParameterPassedParameters' => ['matches' => true, 'subpatterns' => true]],
'preg_quote' => ['hasSideEffects' => false],
'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true]],
'preg_replace' => ['pureUnlessParameterPassedParameters' => ['count' => true]],
'preg_replace_callback' => ['pureUnlessCallableIsImpureParameters' => ['callback' => true], 'pureUnlessParameterPassedParameters' => ['count' => true]],
'preg_split' => ['hasSideEffects' => false],
'property_exists' => ['hasSideEffects' => false],
'quoted_printable_decode' => ['hasSideEffects' => false],
Expand Down Expand Up @@ -1666,6 +1673,7 @@
'rtrim' => ['hasSideEffects' => false],
'sha1' => ['hasSideEffects' => false],
'sha1_file' => ['hasSideEffects' => true],
'similar_text' => ['pureUnlessParameterPassedParameters' => ['percent' => true]],
'sin' => ['hasSideEffects' => false],
'sinh' => ['hasSideEffects' => false],
'sizeof' => ['hasSideEffects' => false],
Expand All @@ -1680,8 +1688,10 @@
'str_ends_with' => ['hasSideEffects' => false],
'str_getcsv' => ['hasSideEffects' => false],
'str_increment' => ['hasSideEffects' => false],
'str_ireplace' => ['pureUnlessParameterPassedParameters' => ['count' => true, 'replace_count' => true]],
'str_pad' => ['hasSideEffects' => false],
'str_repeat' => ['hasSideEffects' => false],
'str_replace' => ['pureUnlessParameterPassedParameters' => ['count' => true, 'replace_count' => true]],
'str_rot13' => ['hasSideEffects' => false],
'str_split' => ['hasSideEffects' => false],
'str_starts_with' => ['hasSideEffects' => false],
Expand Down
9 changes: 9 additions & 0 deletions src/Analyser/ExprHandler/NewHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -259,13 +259,22 @@ private function processConstructorReflection(string $className, New_ $expr, Mut
if ($constructorReflection !== null) {
if (!$constructorReflection->hasSideEffects()->no()) {
$certain = $constructorReflection->isPure()->no();
// A constructor can carry both flags at once, so combine the verdicts
// the same way SimpleImpurePoint::createFromVariant() does for calls:
// Yes = pure, No = impure, Maybe = possibly impure.
$verdict = SimpleImpurePoint::resolvePureUnlessCallableIsImpureVerdict($parametersAcceptor, $scope, $expr->getArgs());
$passedVerdict = SimpleImpurePoint::resolvePureUnlessParameterPassedVerdict($parametersAcceptor, $expr->getArgs());
if ($passedVerdict !== null) {
$verdict = $verdict === null ? $passedVerdict : $verdict->and($passedVerdict);
}

if ($verdict !== null && $verdict->yes()) {
return [$constructorReflection, $classReflection, $parametersAcceptor, $impurePoints];
}
if ($verdict !== null && $verdict->no()) {
$certain = true;
}

$impurePoints[] = new ImpurePoint(
$scope,
$expr,
Expand Down
7 changes: 7 additions & 0 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -1556,6 +1556,7 @@ public function enterTrait(ClassReflection $traitReflection): self
* @param array<string, bool> $immediatelyInvokedCallableParameters
* @param array<string, Type> $phpDocClosureThisTypeParameters
* @param array<string, bool> $phpDocPureUnlessCallableIsImpureParameters
* @param array<string, bool> $phpDocPureUnlessParameterPassedParameters
*/
public function enterClassMethod(
Node\Stmt\ClassMethod $classMethod,
Expand All @@ -1578,6 +1579,7 @@ public function enterClassMethod(
bool $isConstructor = false,
?ResolvedPhpDocBlock $resolvedPhpDocBlock = null,
array $phpDocPureUnlessCallableIsImpureParameters = [],
array $phpDocPureUnlessParameterPassedParameters = [],
): self
{
if (!$this->isInClass()) {
Expand Down Expand Up @@ -1614,6 +1616,7 @@ public function enterClassMethod(
$isConstructor,
$this->attributeReflectionFactory->fromAttrGroups($classMethod->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $classMethod)),
$phpDocPureUnlessCallableIsImpureParameters,
$phpDocPureUnlessParameterPassedParameters,
),
!$classMethod->isStatic(),
);
Expand Down Expand Up @@ -1704,6 +1707,7 @@ public function enterPropertyHook(
false,
$this->attributeReflectionFactory->fromAttrGroups($hook->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $hook)),
[],
[],
),
true,
);
Expand Down Expand Up @@ -1781,6 +1785,7 @@ private function getParameterAttributes(ClassMethod|Function_|PropertyHook $func
* @param array<string, bool> $immediatelyInvokedCallableParameters
* @param array<string, Type> $phpDocClosureThisTypeParameters
* @param array<string, bool> $pureUnlessCallableIsImpureParameters
* @param array<string, bool> $pureUnlessParameterPassedParameters
*/
public function enterFunction(
Node\Stmt\Function_ $function,
Expand All @@ -1799,6 +1804,7 @@ public function enterFunction(
array $immediatelyInvokedCallableParameters = [],
array $phpDocClosureThisTypeParameters = [],
array $pureUnlessCallableIsImpureParameters = [],
array $pureUnlessParameterPassedParameters = [],
): self
{
return $this->enterFunctionLike(
Expand All @@ -1825,6 +1831,7 @@ public function enterFunction(
$phpDocClosureThisTypeParameters,
$this->attributeReflectionFactory->fromAttrGroups($function->attrGroups, InitializerExprContext::fromStubParameter(null, $this->getFile(), $function)),
$pureUnlessCallableIsImpureParameters,
$pureUnlessParameterPassedParameters,
),
false,
);
Expand Down
12 changes: 8 additions & 4 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -779,7 +779,7 @@ public function processStmtNode(
$throwPoints = [];
$impurePoints = [];
$this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback);
[$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, , $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters] = $this->getPhpDocs($scope, $stmt);
[$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, , $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters, $pureUnlessParameterPassedParameters] = $this->getPhpDocs($scope, $stmt);

foreach ($stmt->params as $param) {
$this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback);
Expand Down Expand Up @@ -810,6 +810,7 @@ public function processStmtNode(
$phpDocImmediatelyInvokedCallableParameters,
$phpDocClosureThisTypeParameters,
$pureUnlessCallableIsImpureParameters,
$pureUnlessParameterPassedParameters,
);
$functionReflection = $functionScope->getFunction();
if (!$functionReflection instanceof PhpFunctionFromParserNodeReflection) {
Expand Down Expand Up @@ -875,7 +876,7 @@ public function processStmtNode(
$throwPoints = [];
$impurePoints = [];
$this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback);
[$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters] = $this->getPhpDocs($scope, $stmt);
[$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters, $pureUnlessParameterPassedParameters] = $this->getPhpDocs($scope, $stmt);

foreach ($stmt->params as $param) {
$this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback);
Expand Down Expand Up @@ -913,6 +914,7 @@ public function processStmtNode(
$isConstructor,
null,
$pureUnlessCallableIsImpureParameters,
$pureUnlessParameterPassedParameters,
);

if (!$scope->isInClass()) {
Expand Down Expand Up @@ -4943,7 +4945,7 @@ private function processNodesForCalledMethod($node, ExpressionResultStorage $sto
}

/**
* @return array{TemplateTypeMap, array<string, Type>, array<string, bool>, array<string, Type>, ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type, array<string, Type>, array<(string|int), VarTag>, bool, ?ResolvedPhpDocBlock, array<string, bool>}
* @return array{TemplateTypeMap, array<string, Type>, array<string, bool>, array<string, Type>, ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type, array<string, Type>, array<(string|int), VarTag>, bool, ?ResolvedPhpDocBlock, array<string, bool>, array<string, bool>}
*/
public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $node): array
{
Expand Down Expand Up @@ -4974,6 +4976,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n
$functionName = null;
$phpDocParameterOutTypes = [];
$phpDocPureUnlessCallableIsImpureParameters = [];
$phpDocPureUnlessParameterPassedParameters = [];

if ($node instanceof Node\Stmt\ClassMethod) {
if (!$scope->isInClass()) {
Expand Down Expand Up @@ -5108,6 +5111,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n
$selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null;
$varTags = $resolvedPhpDoc->getVarTags();
$phpDocPureUnlessCallableIsImpureParameters = $resolvedPhpDoc->getParamsPureUnlessCallableIsImpure();
$phpDocPureUnlessParameterPassedParameters = $resolvedPhpDoc->getParamsPureUnlessParameterPassed();
}

if ($acceptsNamedArguments && $scope->isInClass()) {
Expand All @@ -5131,7 +5135,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n
}
}

return [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation, $resolvedPhpDoc, $phpDocPureUnlessCallableIsImpureParameters];
return [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation, $resolvedPhpDoc, $phpDocPureUnlessCallableIsImpureParameters, $phpDocPureUnlessParameterPassedParameters];
}

private function transformStaticType(ClassReflection $declaringClass, Type $type): Type
Expand Down
16 changes: 16 additions & 0 deletions src/PhpDoc/PhpDocNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,22 @@ public function resolveParamPureUnlessCallableIsImpure(PhpDocNode $phpDocNode):
return $parameters;
}

/**
* @return array<string, bool>
*/
public function resolveParamPureUnlessParameterPassed(PhpDocNode $phpDocNode): array
{
$parameters = [];
foreach (['@pure-unless-parameter-passed', '@phpstan-pure-unless-parameter-passed'] as $tagName) {
foreach ($phpDocNode->getPureUnlessParameterIsPassedTagValues($tagName) as $tag) {
$parameterName = substr($tag->parameterName, 1);
$parameters[$parameterName] = true;
}
}

return $parameters;
}

/**
* @return array<string, ParamClosureThisTag>
*/
Expand Down
Loading
Loading