1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Reflection;
4:
5: use PHPStan\PhpDoc\ResolvedPhpDocBlock;
6: use PHPStan\PhpDoc\Tag\AssertTag;
7: use PHPStan\Type\Type;
8: use PHPStan\Type\TypeCombinator;
9: use function array_filter;
10: use function array_map;
11: use function array_merge;
12: use function count;
13: use function sprintf;
14:
15: /**
16: * Collection of @phpstan-assert annotations on a function or method.
17: *
18: * PHPStan supports type assertions via PHPDoc annotations:
19: * - `@phpstan-assert Type $param` — narrows the parameter type unconditionally
20: * - `@phpstan-assert-if-true Type $param` — narrows when the method returns true
21: * - `@phpstan-assert-if-false Type $param` — narrows when the method returns false
22: *
23: * This class collects all such assertions and provides methods to retrieve them
24: * by condition type. It also handles negation: an `@phpstan-assert-if-true` assertion
25: * is automatically negated and included in the `getAssertsIfFalse()` result.
26: *
27: * Returned by ExtendedMethodReflection::getAsserts() and FunctionReflection::getAsserts().
28: *
29: * @api
30: */
31: final class Assertions
32: {
33:
34: private static ?self $empty = null;
35:
36: /**
37: * @param AssertTag[] $asserts
38: */
39: private function __construct(private array $asserts)
40: {
41: }
42:
43: /** @return AssertTag[] */
44: public function getAll(): array
45: {
46: return $this->asserts;
47: }
48:
49: /**
50: * Unconditional assertions — narrow parameter types regardless of the method's return value.
51: *
52: * @return AssertTag[]
53: */
54: public function getAsserts(): array
55: {
56: return array_filter($this->asserts, static fn (AssertTag $assert) => $assert->getIf() === AssertTag::NULL);
57: }
58:
59: /**
60: * Includes @phpstan-assert-if-true tags and negated @phpstan-assert-if-false tags.
61: *
62: * @return AssertTag[]
63: */
64: public function getAssertsIfTrue(): array
65: {
66: return array_merge(
67: array_filter($this->asserts, static fn (AssertTag $assert) => $assert->getIf() === AssertTag::IF_TRUE),
68: array_map(
69: static fn (AssertTag $assert) => $assert->negate(),
70: array_filter($this->asserts, static fn (AssertTag $assert) => $assert->getIf() === AssertTag::IF_FALSE && !$assert->isEquality()),
71: ),
72: );
73: }
74:
75: /**
76: * Includes @phpstan-assert-if-false tags and negated @phpstan-assert-if-true tags.
77: *
78: * @return AssertTag[]
79: */
80: public function getAssertsIfFalse(): array
81: {
82: return array_merge(
83: array_filter($this->asserts, static fn (AssertTag $assert) => $assert->getIf() === AssertTag::IF_FALSE),
84: array_map(
85: static fn (AssertTag $assert) => $assert->negate(),
86: array_filter($this->asserts, static fn (AssertTag $assert) => $assert->getIf() === AssertTag::IF_TRUE && !$assert->isEquality()),
87: ),
88: );
89: }
90:
91: /** @param callable(Type): Type $callable */
92: public function mapTypes(callable $callable): self
93: {
94: $assertTagsCallback = static fn (AssertTag $tag): AssertTag => $tag->withType($callable($tag->getType()));
95:
96: return self::create(array_map($assertTagsCallback, $this->asserts));
97: }
98:
99: /**
100: * @deprecated use union() or intersect() instead
101: */
102: public function intersectWith(Assertions $other): self
103: {
104: return $this->union($other);
105: }
106:
107: public function union(Assertions $other): self
108: {
109: if ($this === self::$empty) {
110: return $other;
111: }
112: if ($other === self::$empty) {
113: return $this;
114: }
115:
116: return self::create(array_merge($this->getAll(), $other->getAll()));
117: }
118:
119: public function intersect(Assertions $other): self
120: {
121: if ($this === self::$empty) {
122: return $other;
123: }
124: if ($other === self::$empty) {
125: return $this;
126: }
127:
128: $otherAsserts = $other->getAll();
129: $thisAsserts = $this->getAll();
130:
131: $merged = [];
132: foreach ($thisAsserts as $thisAssert) {
133: $key = self::getAssertKey($thisAssert);
134:
135: foreach ($otherAsserts as $otherAssert) {
136: if (self::getAssertKey($otherAssert) !== $key) {
137: continue;
138: }
139:
140: $merged[] = $thisAssert->withType(TypeCombinator::union($thisAssert->getType(), $otherAssert->getType()));
141: }
142: }
143:
144: return self::create($merged);
145: }
146:
147: private static function getAssertKey(AssertTag $assert): string
148: {
149: return sprintf(
150: '%s-%s-%s',
151: $assert->getParameter()->describe(),
152: $assert->getIf(),
153: $assert->isNegated() ? '1' : '0',
154: );
155: }
156:
157: /**
158: * @param AssertTag[] $asserts
159: */
160: private static function create(array $asserts): self
161: {
162: if (count($asserts) === 0) {
163: return self::createEmpty();
164: }
165: return new self($asserts);
166: }
167:
168: public static function createEmpty(): self
169: {
170: $empty = self::$empty;
171:
172: if ($empty !== null) {
173: return $empty;
174: }
175:
176: $empty = new self([]);
177: self::$empty = $empty;
178:
179: return $empty;
180: }
181:
182: public static function createFromResolvedPhpDocBlock(ResolvedPhpDocBlock $phpDocBlock): self
183: {
184: $tags = $phpDocBlock->getAssertTags();
185:
186: return self::create($tags);
187: }
188:
189: }
190: