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