Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e752f28
Parse @pure-unless-callable-is-impure PHPDoc tag
zonuexe Jul 2, 2026
53727b9
Thread pure-unless-callable-is-impure through the reflection layer
zonuexe Jul 2, 2026
7b0f228
Wire pure-unless-callable-is-impure into the analyser
zonuexe Jul 2, 2026
da3fbca
Mark callback-accepting builtins as pure-unless-callable-is-impure
zonuexe Jul 2, 2026
e5f4c50
Add tests for @pure-unless-callable-is-impure
zonuexe Jul 2, 2026
d354666
Fix pure-unless-callable-is-impure parameter flag propagation
zonuexe Jul 2, 2026
3fe1829
Suppress call impure points when pure-unless-callable-is-impure callb…
zonuexe Jul 2, 2026
bd3febf
Fix CI: avoid named arguments, adjust no-effect fixture
zonuexe Jul 2, 2026
03e7ea4
Cover pure-unless-callable-is-impure verdict for maybe-null and maybe…
zonuexe Jul 3, 2026
dbe4eaf
Simplify pure-unless-callable-is-impure metadata reads
zonuexe Jul 3, 2026
498783b
Document the shape of the function metadata array
zonuexe Jul 3, 2026
8d7b559
Mark array_all() and array_any() as pure-unless-callable-is-impure
zonuexe Jul 3, 2026
9bdc1ea
Honor pure-unless-callable-is-impure metadata for stub-reflected func…
zonuexe Jul 3, 2026
4fd839e
Add lint version comment to the PHP 8.4 pure-unless fixture
zonuexe Jul 3, 2026
9d64ec1
Honor @pure-unless-callable-is-impure on methods
zonuexe Jul 5, 2026
d1a253d
Note the two paths that intentionally don't thread pure-unless yet
zonuexe Jul 5, 2026
0a6650a
Honor @pure-unless-callable-is-impure on constructor calls
zonuexe Jul 5, 2026
ae0e28f
Drop the native mixed property type from the pure-unless fixture
zonuexe Jul 5, 2026
1bc68ff
Restructure NewHandler pure-unless branching to match createFromVariant
zonuexe Jul 5, 2026
92e24b2
Cover a union pure-callable|callable argument on a pure-unless constr…
zonuexe Jul 6, 2026
3ec51f1
Return TrinaryLogic from isPureUnlessCallableIsImpureParameter()
zonuexe Jul 7, 2026
33926fe
Check the body of @pure-unless-callable-is-impure functions for purity
zonuexe Jul 7, 2026
160db8c
Report redundant @pure-unless-callable-is-impure on a pure callable
zonuexe Jul 7, 2026
43f61e8
Return TrinaryLogic from getPureUnlessCallableIsImpureParameters()
zonuexe Jul 7, 2026
b6f0d66
Report impure methods overriding @pure-unless-callable-is-impure methods
zonuexe Jul 7, 2026
33f825a
Add a direct unit test for resolvePureUnlessCallableIsImpureVerdict()
zonuexe Jul 7, 2026
f0cf3d4
Make SimpleImpurePointTest a self-contained unit test
zonuexe Jul 7, 2026
4859c2f
Share the pure-unless-callable verdict-to-certainty decision
zonuexe Jul 7, 2026
07722d2
Revert the direct mutation-testing unit test and verdict helper
zonuexe Jul 7, 2026
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
Prev Previous commit
Next Next commit
Return TrinaryLogic from isPureUnlessCallableIsImpureParameter()
Per review: the parameter-level flag must be a TrinaryLogic so it composes
correctly on union and intersection method variants (combineAcceptors uses
->or(), mirroring isImmediatelyInvokedCallable). Origins convert the bool
from PHPDoc/metadata with TrinaryLogic::createFromBoolean(); the consumption
in SimpleImpurePoint skips only parameters whose flag is ->no().
  • Loading branch information
zonuexe committed Jul 7, 2026
commit 3ec51f139f7f44f883ac7a9f9600f425f198f24a
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ public function checkAllowedConstants(array $constants): AllowedConstantsResult
return new AllowedConstantsResult([], [], false);
}

public function isPureUnlessCallableIsImpureParameter(): bool
public function isPureUnlessCallableIsImpureParameter(): TrinaryLogic
{
return false;
return TrinaryLogic::createNo();
}

}
2 changes: 1 addition & 1 deletion src/Reflection/Callables/SimpleImpurePoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ public static function resolvePureUnlessCallableIsImpureVerdict(ParametersAccept
if (!$parameter instanceof ExtendedParameterReflection) {
continue;
}
if (!$parameter->isPureUnlessCallableIsImpureParameter()) {
if ($parameter->isPureUnlessCallableIsImpureParameter()->no()) {
continue;
}

Expand Down
2 changes: 1 addition & 1 deletion src/Reflection/ExtendedParameterReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ public function getAllowedConstants(): ?ParameterAllowedConstants;
*/
public function checkAllowedConstants(array $constants): AllowedConstantsResult;

public function isPureUnlessCallableIsImpureParameter(): bool;
public function isPureUnlessCallableIsImpureParameter(): TrinaryLogic;

}
1 change: 1 addition & 0 deletions src/Reflection/GenericParametersAcceptorResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc
null,
[],
null,
TrinaryLogic::createNo(),
), $parameters),
$parametersAcceptor->isVariadic(),
$returnType,
Expand Down
4 changes: 2 additions & 2 deletions src/Reflection/Native/ExtendedNativeParameterReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public function __construct(
private ?Type $closureThisType,
private array $attributes,
private ?ParameterAllowedConstants $allowedConstants,
private bool $pureUnlessCallableIsImpureParameter = false,
private TrinaryLogic $pureUnlessCallableIsImpureParameter,
)
{
}
Expand Down Expand Up @@ -115,7 +115,7 @@ public function checkAllowedConstants(array $constants): AllowedConstantsResult
return $this->allowedConstants->check($constants);
}

public function isPureUnlessCallableIsImpureParameter(): bool
public function isPureUnlessCallableIsImpureParameter(): TrinaryLogic
{
return $this->pureUnlessCallableIsImpureParameter;
}
Expand Down
5 changes: 3 additions & 2 deletions src/Reflection/ParametersAcceptorSelector.php
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc
$parameter instanceof ExtendedParameterReflection ? $parameter->getClosureThisType() : null,
$parameter instanceof ExtendedParameterReflection ? $parameter->getAttributes() : [],
$parameter instanceof ExtendedParameterReflection ? $parameter->getAllowedConstants() : null,
$parameter instanceof ExtendedParameterReflection && $parameter->isPureUnlessCallableIsImpureParameter(),
$parameter instanceof ExtendedParameterReflection ? $parameter->isPureUnlessCallableIsImpureParameter() : TrinaryLogic::createNo(),
);
continue;
}
Expand Down Expand Up @@ -824,7 +824,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc
}

$pureUnlessCallableIsImpureParameter = $parameters[$i]->isPureUnlessCallableIsImpureParameter()
|| ($parameter instanceof ExtendedParameterReflection && $parameter->isPureUnlessCallableIsImpureParameter());
->or($parameter instanceof ExtendedParameterReflection ? $parameter->isPureUnlessCallableIsImpureParameter() : TrinaryLogic::createNo());

$parameters[$i] = new ExtendedDummyParameter(
$parameters[$i]->getName() !== $parameter->getName() ? sprintf('%s|%s', $parameters[$i]->getName(), $parameter->getName()) : $parameter->getName(),
Expand Down Expand Up @@ -942,6 +942,7 @@ private static function wrapParameter(ParameterReflection $parameter): ExtendedP
null,
[],
null,
TrinaryLogic::createNo(),
);
}

Expand Down
1 change: 1 addition & 0 deletions src/Reflection/Php/ClosureCallMethodReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ public function getVariants(): array
null,
// pure-unless-callable-is-impure is not threaded here: a closure's own
// parameters cannot carry the tag.
TrinaryLogic::createNo(),
), $parameters),
$this->closureType->isVariadic(),
$this->closureType->getReturnType(),
Expand Down
1 change: 1 addition & 0 deletions src/Reflection/Php/ExitFunctionReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public function getVariants(): array
null,
[],
null,
TrinaryLogic::createNo(),
),
],
false,
Expand Down
4 changes: 2 additions & 2 deletions src/Reflection/Php/ExtendedDummyParameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public function __construct(
private ?Type $closureThisType,
private array $attributes,
private ?ParameterAllowedConstants $allowedConstants,
private bool $pureUnlessCallableIsImpureParameter = false,
private TrinaryLogic $pureUnlessCallableIsImpureParameter,
)
{
parent::__construct($name, $type, $optional, $passedByReference, $variadic, $defaultValue);
Expand Down Expand Up @@ -86,7 +86,7 @@ public function checkAllowedConstants(array $constants): AllowedConstantsResult
return $this->allowedConstants->check($constants);
}

public function isPureUnlessCallableIsImpureParameter(): bool
public function isPureUnlessCallableIsImpureParameter(): TrinaryLogic
{
return $this->pureUnlessCallableIsImpureParameter;
}
Expand Down
1 change: 1 addition & 0 deletions src/Reflection/Php/PhpClassReflectionExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,7 @@ private function createNativeMethodVariant(
$this->allowedConstantsMapProvider->getForMethodParameter($declaringClassName, $methodName, $parameterSignature->getName()),
// pure-unless-callable-is-impure is not threaded here because no built-in method
// carries it (there are no Class::method entries in functionMetadata.php).
TrinaryLogic::createNo(),
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/Reflection/Php/PhpFunctionFromParserNodeReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ public function getParameters(): array
$closureThisType = null;
}

$pureUnlessCallableIsImpureParameter = $this->pureUnlessCallableIsImpureParameters[$parameter->var->name] ?? false;
$pureUnlessCallableIsImpureParameter = TrinaryLogic::createFromBoolean($this->pureUnlessCallableIsImpureParameters[$parameter->var->name] ?? false);

$parameters[] = new PhpParameterFromParserNodeReflection(
$parameter->var->name,
Expand Down
2 changes: 1 addition & 1 deletion src/Reflection/Php/PhpFunctionReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ private function getParameters(): array
$this->phpDocParameterClosureThisTypes[$reflection->getName()] ?? null,
$this->attributeReflectionFactory->fromNativeReflection($reflection->getAttributes(), InitializerExprContext::fromReflectionParameter($reflection)),
$this->allowedConstantsMapProvider->getForFunctionParameter(strtolower($this->reflection->getName()), $reflection->getName()),
$this->phpDocParameterPureUnlessCallableIsImpure[$reflection->getName()] ?? false,
TrinaryLogic::createFromBoolean($this->phpDocParameterPureUnlessCallableIsImpure[$reflection->getName()] ?? false),
);
}, $this->reflection->getParameters());
}
Expand Down
2 changes: 1 addition & 1 deletion src/Reflection/Php/PhpMethodReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ private function getParameters(): array
$this->phpDocClosureThisTypeParameters[$reflection->getName()] ?? null,
$this->attributeReflectionFactory->fromNativeReflection($reflection->getAttributes(), InitializerExprContext::fromReflectionParameter($reflection)),
$this->allowedConstantsMapProvider->getForMethodParameter($this->declaringClass->getName(), $this->reflection->getName(), $reflection->getName()),
$this->pureUnlessCallableIsImpureParameters[$reflection->getName()] ?? false,
TrinaryLogic::createFromBoolean($this->pureUnlessCallableIsImpureParameters[$reflection->getName()] ?? false),
), $this->reflection->getParameters());
}

Expand Down
4 changes: 2 additions & 2 deletions src/Reflection/Php/PhpParameterFromParserNodeReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function __construct(
private TrinaryLogic $immediatelyInvokedCallable,
private ?Type $closureThisType,
private array $attributes,
private bool $pureUnlessCallableIsImpureParameter,
private TrinaryLogic $pureUnlessCallableIsImpureParameter,
)
{
}
Expand Down Expand Up @@ -126,7 +126,7 @@ public function checkAllowedConstants(array $constants): AllowedConstantsResult
return new AllowedConstantsResult([], [], false);
}

public function isPureUnlessCallableIsImpureParameter(): bool
public function isPureUnlessCallableIsImpureParameter(): TrinaryLogic
{
return $this->pureUnlessCallableIsImpureParameter;
}
Expand Down
4 changes: 2 additions & 2 deletions src/Reflection/Php/PhpParameterReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public function __construct(
private ?Type $closureThisType,
private array $attributes,
private ?ParameterAllowedConstants $allowedConstants,
private bool $pureUnlessCallableIsImpureParameter = false,
private TrinaryLogic $pureUnlessCallableIsImpureParameter,
)
{
}
Expand Down Expand Up @@ -161,7 +161,7 @@ public function checkAllowedConstants(array $constants): AllowedConstantsResult
return $this->allowedConstants->check($constants);
}

public function isPureUnlessCallableIsImpureParameter(): bool
public function isPureUnlessCallableIsImpureParameter(): TrinaryLogic
{
return $this->pureUnlessCallableIsImpureParameter;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef
$phpDocType = null;
$immediatelyInvokedCallable = TrinaryLogic::createMaybe();
$closureThisType = null;
$pureUnlessCallableIsImpureParameter = $pureUnlessCallableIsImpureParameters[$name] ?? false;
$pureUnlessCallableIsImpureParameter = TrinaryLogic::createFromBoolean($pureUnlessCallableIsImpureParameters[$name] ?? false);
if ($phpDoc !== null) {
if (array_key_exists($parameterSignature->getName(), $phpDoc->getParamTags())) {
$phpDocType = $phpDoc->getParamTags()[$parameterSignature->getName()]->getType();
Expand All @@ -145,7 +145,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef
$closureThisType = $phpDoc->getParamClosureThisTags()[$parameterSignature->getName()]->getType();
}
if (($phpDoc->getParamsPureUnlessCallableIsImpure()[$parameterSignature->getName()] ?? false) === true) {
$pureUnlessCallableIsImpureParameter = true;
$pureUnlessCallableIsImpureParameter = TrinaryLogic::createYes();
}
}

Expand Down
1 change: 1 addition & 0 deletions src/Reflection/WrappedExtendedMethodReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public function getVariants(): array
null,
[],
null,
TrinaryLogic::createNo(),
), $variant->getParameters()),
$variant->isVariadic(),
$variant->getReturnType(),
Expand Down
2 changes: 2 additions & 0 deletions tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ public static function dataSelectFromTypes(): Generator
$parameter->getClosureThisType(),
$parameter->getAttributes(),
$parameter->getAllowedConstants(),
$parameter->isPureUnlessCallableIsImpureParameter(),
), $datePeriodConstructorVariants[0]->getParameters()),
false,
new VoidType(),
Expand Down Expand Up @@ -125,6 +126,7 @@ public static function dataSelectFromTypes(): Generator
$parameter->getClosureThisType(),
$parameter->getAttributes(),
$parameter->getAllowedConstants(),
$parameter->isPureUnlessCallableIsImpureParameter(),
), $datePeriodConstructorVariants[1]->getParameters()),
false,
new VoidType(),
Expand Down