Skip to content

Commit 685500c

Browse files
zonuexestaabm
authored andcommitted
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 6457e6d commit 685500c

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
@@ -100,6 +100,9 @@ final class ResolvedPhpDocBlock
100100
/** @var array<string, bool>|false */
101101
private array|false $paramsPureUnlessCallableIsImpure = false;
102102

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

@@ -225,6 +228,7 @@ public static function createEmpty(): self
225228
$self->paramOutTags = [];
226229
$self->paramsImmediatelyInvokedCallable = [];
227230
$self->paramsPureUnlessCallableIsImpure = [];
231+
$self->paramsPureUnlessParameterPassed = [];
228232
$self->paramClosureThisTags = [];
229233
$self->returnTag = null;
230234
$self->throwsTag = null;
@@ -282,6 +286,7 @@ public function merge(ResolvedPhpDocBlock $parent, InheritedPhpDocParameterMappi
282286
$result->paramOutTags = self::mergeParamOutTags($this->getParamOutTags(), $parent, $parameterMapping, $parentClass);
283287
$result->paramsImmediatelyInvokedCallable = self::mergeParamsImmediatelyInvokedCallable($this->getParamsImmediatelyInvokedCallable(), $parent, $parameterMapping);
284288
$result->paramsPureUnlessCallableIsImpure = self::mergeParamsPureUnlessCallableIsImpure($this->getParamsPureUnlessCallableIsImpure(), $parent, $parameterMapping);
289+
$result->paramsPureUnlessParameterPassed = self::mergeParamsPureUnlessParameterPassed($this->getParamsPureUnlessParameterPassed(), $parent, $parameterMapping);
285290
$result->paramClosureThisTags = self::mergeParamClosureThisTags($this->getParamClosureThisTags(), $parent, $parameterMapping, $parentClass);
286291
$result->returnTag = self::mergeReturnTags($this->getReturnTag(), $declaringClass, $parent, $parameterMapping, $parentClass);
287292
$result->throwsTag = self::mergeThrowsTags($this->getThrowsTag(), $parent);
@@ -606,6 +611,18 @@ public function getParamsPureUnlessCallableIsImpure(): array
606611
return $this->paramsPureUnlessCallableIsImpure;
607612
}
608613

614+
/**
615+
* @return array<string, bool>
616+
*/
617+
public function getParamsPureUnlessParameterPassed(): array
618+
{
619+
if ($this->paramsPureUnlessParameterPassed === false) {
620+
$this->paramsPureUnlessParameterPassed = $this->phpDocNodeResolver->resolveParamPureUnlessParameterPassed($this->phpDocNode);
621+
}
622+
623+
return $this->paramsPureUnlessParameterPassed;
624+
}
625+
609626
/**
610627
* @return array<string, ParamClosureThisTag>
611628
*/
@@ -1135,6 +1152,34 @@ private static function mergeOneParentParamPureUnlessCallableIsImpure(array $par
11351152
return $paramsPureUnlessCallableIsImpure;
11361153
}
11371154

1155+
/**
1156+
* @param array<string, bool> $paramsPureUnlessParameterPassed
1157+
* @return array<string, bool>
1158+
*/
1159+
private static function mergeParamsPureUnlessParameterPassed(array $paramsPureUnlessParameterPassed, self $parent, InheritedPhpDocParameterMapping $parameterMapping): array
1160+
{
1161+
return self::mergeOneParentParamPureUnlessParameterPassed($paramsPureUnlessParameterPassed, $parent, $parameterMapping);
1162+
}
1163+
1164+
/**
1165+
* @param array<string, bool> $paramsPureUnlessParameterPassed
1166+
* @return array<string, bool>
1167+
*/
1168+
private static function mergeOneParentParamPureUnlessParameterPassed(array $paramsPureUnlessParameterPassed, self $parent, InheritedPhpDocParameterMapping $parameterMapping): array
1169+
{
1170+
$parentPureUnlessParameterPassed = $parameterMapping->transformArrayKeysWithParameterNameMapping($parent->getParamsPureUnlessParameterPassed());
1171+
1172+
foreach ($parentPureUnlessParameterPassed as $name => $parentIsPureUnlessParameterPassed) {
1173+
if (array_key_exists($name, $paramsPureUnlessParameterPassed)) {
1174+
continue;
1175+
}
1176+
1177+
$paramsPureUnlessParameterPassed[$name] = $parentIsPureUnlessParameterPassed;
1178+
}
1179+
1180+
return $paramsPureUnlessParameterPassed;
1181+
}
1182+
11381183
/**
11391184
* @param array<string, ParamClosureThisTag> $paramsClosureThisTags
11401185
* @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)