Skip to content

Commit 8aae47a

Browse files
committed
Parse @pure-unless-parameter-passed PHPDoc tag
Resolve the tag into per-parameter flags and merge them across parent PHPDocs. Until phpstan/phpdoc-parser#259 is merged the parser does not understand the tag natively, so parse it from the generic tag value and whitelist the @phpstan- alias in InvalidPHPStanDocTagRule.
1 parent 43e9d32 commit 8aae47a

3 files changed

Lines changed: 91 additions & 0 deletions

File tree

src/PhpDoc/PhpDocNodeResolver.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use PHPStan\PhpDoc\Tag\UsesTag;
2929
use PHPStan\PhpDoc\Tag\VarTag;
3030
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode;
31+
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
3132
use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode;
3233
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
3334
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
@@ -47,8 +48,10 @@
4748
use function array_reverse;
4849
use function count;
4950
use function in_array;
51+
use function ltrim;
5052
use function method_exists;
5153
use function str_starts_with;
54+
use function strpos;
5255
use function substr;
5356

5457
#[AutowiredService]
@@ -403,6 +406,46 @@ public function resolveParamPureUnlessCallableIsImpure(PhpDocNode $phpDocNode):
403406
return $parameters;
404407
}
405408

409+
/**
410+
* @return array<string, bool>
411+
*/
412+
public function resolveParamPureUnlessParameterPassed(PhpDocNode $phpDocNode): array
413+
{
414+
$parameters = [];
415+
// TODO: replace this generic-tag parsing with
416+
// $phpDocNode->getPureUnlessParameterIsPassedTagValues() once
417+
// phpstan/phpdoc-parser#259 is merged and the parser understands the tag.
418+
foreach (['@pure-unless-parameter-passed', '@phpstan-pure-unless-parameter-passed'] as $tagName) {
419+
foreach ($phpDocNode->getTags() as $tag) {
420+
if ($tag->name !== $tagName) {
421+
continue;
422+
}
423+
if (!$tag->value instanceof GenericTagValueNode) {
424+
continue;
425+
}
426+
427+
$value = ltrim($tag->value->value);
428+
if ($value === '' || $value[0] !== '$') {
429+
continue;
430+
}
431+
432+
$parameterName = substr($value, 1);
433+
$spacePosition = strpos($parameterName, ' ');
434+
if ($spacePosition !== false) {
435+
$parameterName = substr($parameterName, 0, $spacePosition);
436+
}
437+
438+
if ($parameterName === '') {
439+
continue;
440+
}
441+
442+
$parameters[$parameterName] = true;
443+
}
444+
}
445+
446+
return $parameters;
447+
}
448+
406449
/**
407450
* @return array<string, ParamClosureThisTag>
408451
*/

src/PhpDoc/ResolvedPhpDocBlock.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ final class ResolvedPhpDocBlock
9999
/** @var array<string, bool>|false */
100100
private array|false $paramsPureUnlessCallableIsImpure = false;
101101

102+
/** @var array<string, bool>|false */
103+
private array|false $paramsPureUnlessParameterPassed = false;
104+
102105
/** @var array<string, ParamClosureThisTag>|false */
103106
private array|false $paramClosureThisTags = false;
104107

@@ -224,6 +227,7 @@ public static function createEmpty(): self
224227
$self->paramOutTags = [];
225228
$self->paramsImmediatelyInvokedCallable = [];
226229
$self->paramsPureUnlessCallableIsImpure = [];
230+
$self->paramsPureUnlessParameterPassed = [];
227231
$self->paramClosureThisTags = [];
228232
$self->returnTag = null;
229233
$self->throwsTag = null;
@@ -281,6 +285,7 @@ public function merge(ResolvedPhpDocBlock $parent, InheritedPhpDocParameterMappi
281285
$result->paramOutTags = self::mergeParamOutTags($this->getParamOutTags(), $parent, $parameterMapping, $parentClass);
282286
$result->paramsImmediatelyInvokedCallable = self::mergeParamsImmediatelyInvokedCallable($this->getParamsImmediatelyInvokedCallable(), $parent, $parameterMapping);
283287
$result->paramsPureUnlessCallableIsImpure = self::mergeParamsPureUnlessCallableIsImpure($this->getParamsPureUnlessCallableIsImpure(), $parent, $parameterMapping);
288+
$result->paramsPureUnlessParameterPassed = self::mergeParamsPureUnlessParameterPassed($this->getParamsPureUnlessParameterPassed(), $parent, $parameterMapping);
284289
$result->paramClosureThisTags = self::mergeParamClosureThisTags($this->getParamClosureThisTags(), $parent, $parameterMapping, $parentClass);
285290
$result->returnTag = self::mergeReturnTags($this->getReturnTag(), $declaringClass, $parent, $parameterMapping, $parentClass);
286291
$result->throwsTag = self::mergeThrowsTags($this->getThrowsTag(), $parent);
@@ -601,6 +606,18 @@ public function getParamsPureUnlessCallableIsImpure(): array
601606
return $this->paramsPureUnlessCallableIsImpure;
602607
}
603608

609+
/**
610+
* @return array<string, bool>
611+
*/
612+
public function getParamsPureUnlessParameterPassed(): array
613+
{
614+
if ($this->paramsPureUnlessParameterPassed === false) {
615+
$this->paramsPureUnlessParameterPassed = $this->phpDocNodeResolver->resolveParamPureUnlessParameterPassed($this->phpDocNode);
616+
}
617+
618+
return $this->paramsPureUnlessParameterPassed;
619+
}
620+
604621
/**
605622
* @return array<string, ParamClosureThisTag>
606623
*/
@@ -1130,6 +1147,34 @@ private static function mergeOneParentParamPureUnlessCallableIsImpure(array $par
11301147
return $paramsPureUnlessCallableIsImpure;
11311148
}
11321149

1150+
/**
1151+
* @param array<string, bool> $paramsPureUnlessParameterPassed
1152+
* @return array<string, bool>
1153+
*/
1154+
private static function mergeParamsPureUnlessParameterPassed(array $paramsPureUnlessParameterPassed, self $parent, InheritedPhpDocParameterMapping $parameterMapping): array
1155+
{
1156+
return self::mergeOneParentParamPureUnlessParameterPassed($paramsPureUnlessParameterPassed, $parent, $parameterMapping);
1157+
}
1158+
1159+
/**
1160+
* @param array<string, bool> $paramsPureUnlessParameterPassed
1161+
* @return array<string, bool>
1162+
*/
1163+
private static function mergeOneParentParamPureUnlessParameterPassed(array $paramsPureUnlessParameterPassed, self $parent, InheritedPhpDocParameterMapping $parameterMapping): array
1164+
{
1165+
$parentPureUnlessParameterPassed = $parameterMapping->transformArrayKeysWithParameterNameMapping($parent->getParamsPureUnlessParameterPassed());
1166+
1167+
foreach ($parentPureUnlessParameterPassed as $name => $parentIsPureUnlessParameterPassed) {
1168+
if (array_key_exists($name, $paramsPureUnlessParameterPassed)) {
1169+
continue;
1170+
}
1171+
1172+
$paramsPureUnlessParameterPassed[$name] = $parentIsPureUnlessParameterPassed;
1173+
}
1174+
1175+
return $paramsPureUnlessParameterPassed;
1176+
}
1177+
11331178
/**
11341179
* @param array<string, ParamClosureThisTag> $paramsClosureThisTags
11351180
* @return array<string, ParamClosureThisTag>

src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ final class InvalidPHPStanDocTagRule implements Rule
6464
'@phpstan-param-immediately-invoked-callable',
6565
'@phpstan-param-later-invoked-callable',
6666
'@phpstan-param-closure-this',
67+
// TODO: drop this once phpstan/phpdoc-parser#259 is merged and the parser
68+
// recognizes the tag natively (like @phpstan-pure-unless-callable-is-impure).
69+
'@phpstan-pure-unless-parameter-passed',
6770
'@phpstan-all-methods-pure',
6871
'@phpstan-all-methods-impure',
6972
];

0 commit comments

Comments
 (0)