Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
wip
  • Loading branch information
zonuexe committed Aug 1, 2025
commit f262eaa213e1aa9f07e43b98d4d67c828c5db122
19 changes: 19 additions & 0 deletions src/PhpDoc/PhpDocNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
use function count;
use function in_array;
use function method_exists;
use function preg_split;
use function str_starts_with;
use function substr;

Expand Down Expand Up @@ -419,6 +420,24 @@ public function resolveParamImmediatelyInvokedCallable(PhpDocNode $phpDocNode):
return $parameters;
}

/**
* @return array<string, bool>
*/
public function resolveParamPureUnlessCallableIsImpure(PhpDocNode $phpDocNode): array
{
$parameters = [];
// TODO: implement phpstan/phpdoc-parser
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.

Will be replaced by phpstan/phpdoc-parser#253

foreach ($phpDocNode->getTagsByName('@pure-unless-callable-impure') as $tag) {
$value = preg_split('/\s/u', (string)$tag->value)[0] ?? null;
if ($value !== null && str_starts_with($value, '$')) {
$parameters[substr($value, 1)] = true;
}
}

return $parameters;
}


/**
* @return array<string, ParamClosureThisTag>
*/
Expand Down
51 changes: 51 additions & 0 deletions src/PhpDoc/ResolvedPhpDocBlock.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ final class ResolvedPhpDocBlock
/** @var array<string, bool>|false */
private array|false $paramsImmediatelyInvokedCallable = false;

/** @var array<string, bool>|false */
private array|false $paramsPureUnlessCallableIsImpure = false;

/** @var array<string, ParamClosureThisTag>|false */
private array|false $paramClosureThisTags = false;

Expand Down Expand Up @@ -216,6 +219,7 @@ public static function createEmpty(): self
$self->paramTags = [];
$self->paramOutTags = [];
$self->paramsImmediatelyInvokedCallable = [];
$self->paramsPureUnlessCallableIsImpure = [];
$self->paramClosureThisTags = [];
$self->returnTag = null;
$self->throwsTag = null;
Expand Down Expand Up @@ -281,6 +285,7 @@ public function merge(array $parents, array $parentPhpDocBlocks): self
$result->paramTags = self::mergeParamTags($this->getParamTags(), $parents, $parentPhpDocBlocks);
$result->paramOutTags = self::mergeParamOutTags($this->getParamOutTags(), $parents, $parentPhpDocBlocks);
$result->paramsImmediatelyInvokedCallable = self::mergeParamsImmediatelyInvokedCallable($this->getParamsImmediatelyInvokedCallable(), $parents, $parentPhpDocBlocks);
$result->paramsPureUnlessCallableIsImpure = self::mergeParamsPureUnlessCallableIsImpure($this->getParamsPureUnlessCallableIsImpure(), $parents, $parentPhpDocBlocks);
$result->paramClosureThisTags = self::mergeParamClosureThisTags($this->getParamClosureThisTags(), $parents, $parentPhpDocBlocks);
$result->returnTag = self::mergeReturnTags($this->getReturnTag(), $classReflection, $parents, $parentPhpDocBlocks);
$result->throwsTag = self::mergeThrowsTags($this->getThrowsTag(), $parents);
Expand Down Expand Up @@ -587,6 +592,18 @@ public function getParamsImmediatelyInvokedCallable(): array
return $this->paramsImmediatelyInvokedCallable;
}

/**
* @return array<string, bool>
*/
public function getParamsPureUnlessCallableIsImpure(): array
{
if ($this->paramsPureUnlessCallableIsImpure === false) {
$this->paramsPureUnlessCallableIsImpure = $this->phpDocNodeResolver->resolveParamPureUnlessCallableIsImpure($this->phpDocNode);
}

return $this->paramsPureUnlessCallableIsImpure;
}

/**
* @return array<string, ParamClosureThisTag>
*/
Expand Down Expand Up @@ -1154,6 +1171,40 @@ private static function mergeOneParentParamImmediatelyInvokedCallable(array $par
return $paramsImmediatelyInvokedCallable;
}

/**
* @param array<string, bool> $paramsPureUnlessCallableIsImpure
* @param array<int, self> $parents
* @param array<int, PhpDocBlock> $parentPhpDocBlocks
* @return array<string, bool>
*/
private static function mergeParamsPureUnlessCallableIsImpure(array $paramsPureUnlessCallableIsImpure, array $parents, array $parentPhpDocBlocks): array
{
foreach ($parents as $i => $parent) {
$paramsPureUnlessCallableIsImpure = self::mergeOneParentParamPureUnlessCallableIsImpure($paramsPureUnlessCallableIsImpure, $parent, $parentPhpDocBlocks[$i]);
}

return $paramsPureUnlessCallableIsImpure;
}

/**
* @param array<string, bool> $paramsPureUnlessCallableIsImpure
* @return array<string, bool>
*/
private static function mergeOneParentParamPureUnlessCallableIsImpure(array $paramsPureUnlessCallableIsImpure, self $parent, PhpDocBlock $phpDocBlock): array
{
$parentPureUnlessCallableIsImpure = $phpDocBlock->transformArrayKeysWithParameterNameMapping($parent->getParamsPureUnlessCallableIsImpure());

foreach ($parentPureUnlessCallableIsImpure as $name => $parentIsPureUnlessCallableIsImpure) {
if (array_key_exists($name, $paramsPureUnlessCallableIsImpure)) {
continue;
}

$paramsPureUnlessCallableIsImpure[$name] = $parentIsPureUnlessCallableIsImpure;
}

return $paramsPureUnlessCallableIsImpure;
}

/**
* @param array<string, ParamClosureThisTag> $paramsClosureThisTags
* @param array<int, self> $parents
Expand Down
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/TestClosureTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ public function testRule(): void
$this->analyse([__DIR__ . '/nsrt/closure-passed-to-type.php'], [
[
'Closure type: Closure(mixed): (1|2|3)',
25,
26,
],
[
'Closure type: Closure(mixed): (1|2|3)',
35,
36,
],
]);
}
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/nsrt/closure-passed-to-type.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class Foo
* @param array<T> $items
* @param callable(T): U $cb
* @return array<U>
* @pure-unless-callable-impure $cb
*/
public function doFoo(array $items, callable $cb)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace ParamPureUnlessCallableIsImpure;

use function PHPStan\Testing\assertType;

/**
* @template TValue
* @template TResult
* @param Closure(TValue): TResult $f
* @param iterable<TValue> $a
* @return array<TResult>
* @pure-unless-callable-is-impure $f
*/
function map(Closure $f, iterable $a): array
{
$result = [];
foreach ($a as $i => $v) {
$retult[$i] = $f($v);
}

return $result;
}

map('printf', []);
map('sprintf', []);

assertType('array<mixed>', map('printf', []));