1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use PHPStan\Php\PhpVersion;
6:
7: /**
8: * Where a native PHP type declaration is written, and for which PHP version.
9: *
10: * Passed to Type::toNativePhpTypeNode(). Instead of exposing the position itself,
11: * it answers what the position allows, e.g. isCallableAllowed() is false for
12: * properties and class constants, and isNullAllowed() is false for a standalone
13: * type before PHP 8.2.
14: *
15: * Compound types pass a derived position to their members: toUnionMember() for each
16: * member of `A|B`, toNullableInner() for `A` in `?A`, toIntersectionMember() for each
17: * member of `A&B`. That is how e.g. an intersection type knows it can only be part
18: * of a union (DNF) since PHP 8.2.
19: *
20: * ```php
21: * $type->toNativePhpTypeNode(NativeTypePosition::returnType(new PhpVersion(80100)));
22: * ```
23: *
24: * @api
25: */
26: final class NativeTypePosition
27: {
28:
29: private const PARAMETER = 1;
30: private const RETURN = 2;
31: private const PROPERTY = 3;
32: private const CLASS_CONSTANT = 4;
33:
34: private const STANDALONE = 1;
35: private const UNION_MEMBER = 2;
36: private const NULLABLE_INNER = 3;
37: private const INTERSECTION_MEMBER = 4;
38:
39: /**
40: * @param self::PARAMETER|self::RETURN|self::PROPERTY|self::CLASS_CONSTANT $declaration
41: * @param self::STANDALONE|self::UNION_MEMBER|self::NULLABLE_INNER|self::INTERSECTION_MEMBER $context
42: */
43: private function __construct(
44: private int $declaration,
45: private int $context,
46: private PhpVersion $phpVersion,
47: )
48: {
49: }
50:
51: /** @api */
52: public static function parameter(PhpVersion $phpVersion): self
53: {
54: return new self(self::PARAMETER, self::STANDALONE, $phpVersion);
55: }
56:
57: /** @api */
58: public static function returnType(PhpVersion $phpVersion): self
59: {
60: return new self(self::RETURN, self::STANDALONE, $phpVersion);
61: }
62:
63: /** @api */
64: public static function property(PhpVersion $phpVersion): self
65: {
66: return new self(self::PROPERTY, self::STANDALONE, $phpVersion);
67: }
68:
69: /** @api */
70: public static function classConstant(PhpVersion $phpVersion): self
71: {
72: return new self(self::CLASS_CONSTANT, self::STANDALONE, $phpVersion);
73: }
74:
75: /** A member of a union type, e.g. `A` in `A|B`. */
76: public function toUnionMember(): self
77: {
78: return new self($this->declaration, self::UNION_MEMBER, $this->phpVersion);
79: }
80:
81: /** The inner type of a nullable type, e.g. `A` in `?A`. */
82: public function toNullableInner(): self
83: {
84: return new self($this->declaration, self::NULLABLE_INNER, $this->phpVersion);
85: }
86:
87: /** A member of an intersection type, e.g. `A` in `A&B`. */
88: public function toIntersectionMember(): self
89: {
90: return new self($this->declaration, self::INTERSECTION_MEMBER, $this->phpVersion);
91: }
92:
93: /** Class names, also `self`, `parent`, and `Closure`. */
94: public function isClassNameAllowed(): bool
95: {
96: return $this->isTypeDeclarationAllowed();
97: }
98:
99: /** `int`, `float`, `string` and `bool`. */
100: public function isScalarAllowed(): bool
101: {
102: return $this->isKeywordAllowed() && $this->isAtLeast(70000);
103: }
104:
105: public function isArrayAllowed(): bool
106: {
107: return $this->isKeywordAllowed();
108: }
109:
110: public function isIterableAllowed(): bool
111: {
112: return $this->isKeywordAllowed() && $this->isAtLeast(70100);
113: }
114:
115: public function isCallableAllowed(): bool
116: {
117: return $this->isKeywordAllowed()
118: && ($this->declaration === self::PARAMETER || $this->declaration === self::RETURN);
119: }
120:
121: public function isObjectAllowed(): bool
122: {
123: return $this->isKeywordAllowed() && $this->isAtLeast(70200);
124: }
125:
126: public function isStaticAllowed(): bool
127: {
128: return $this->isKeywordAllowed()
129: && $this->declaration === self::RETURN
130: && $this->isAtLeast(80000);
131: }
132:
133: public function isVoidAllowed(): bool
134: {
135: return $this->isTypeDeclarationAllowed()
136: && $this->context === self::STANDALONE
137: && $this->declaration === self::RETURN
138: && $this->isAtLeast(70100);
139: }
140:
141: public function isNeverAllowed(): bool
142: {
143: return $this->isTypeDeclarationAllowed()
144: && $this->context === self::STANDALONE
145: && $this->declaration === self::RETURN
146: && $this->isAtLeast(80100);
147: }
148:
149: public function isMixedAllowed(): bool
150: {
151: return $this->isTypeDeclarationAllowed()
152: && $this->context === self::STANDALONE
153: && $this->phpVersion->supportsNativeMixed();
154: }
155:
156: public function isNullAllowed(): bool
157: {
158: if (!$this->isTypeDeclarationAllowed()) {
159: return false;
160: }
161:
162: if ($this->context === self::UNION_MEMBER) {
163: return $this->phpVersion->supportsNativeUnionTypes();
164: }
165:
166: return $this->context === self::STANDALONE && $this->phpVersion->supportsTrueFalseNullStandaloneType();
167: }
168:
169: /** `false` in a union with another non-null type since PHP 8.0, otherwise since PHP 8.2. */
170: public function isFalseAllowed(): bool
171: {
172: if (!$this->isKeywordAllowed()) {
173: return false;
174: }
175:
176: if ($this->context === self::UNION_MEMBER) {
177: return $this->phpVersion->supportsNativeUnionTypes();
178: }
179:
180: return $this->phpVersion->supportsTrueFalseNullStandaloneType();
181: }
182:
183: public function isTrueAllowed(): bool
184: {
185: return $this->isKeywordAllowed() && $this->phpVersion->supportsTrueFalseNullStandaloneType();
186: }
187:
188: /** `?A` */
189: public function isNullableAllowed(): bool
190: {
191: return $this->isTypeDeclarationAllowed()
192: && $this->context === self::STANDALONE
193: && $this->isAtLeast(70100);
194: }
195:
196: /** `A|B` */
197: public function isUnionAllowed(): bool
198: {
199: return $this->isTypeDeclarationAllowed()
200: && $this->context === self::STANDALONE
201: && $this->phpVersion->supportsNativeUnionTypes();
202: }
203:
204: /** `A&B`, also as a member of a union, `(A&B)|C`, since PHP 8.2 */
205: public function isIntersectionAllowed(): bool
206: {
207: if (!$this->isTypeDeclarationAllowed()) {
208: return false;
209: }
210:
211: if ($this->context === self::UNION_MEMBER) {
212: return $this->phpVersion->supportsDisjunctiveNormalForm();
213: }
214:
215: return $this->context === self::STANDALONE && $this->phpVersion->supportsPureIntersectionTypes();
216: }
217:
218: private function isTypeDeclarationAllowed(): bool
219: {
220: if ($this->declaration === self::RETURN) {
221: return $this->isAtLeast(70000);
222: }
223:
224: if ($this->declaration === self::PROPERTY) {
225: return $this->isAtLeast(70400);
226: }
227:
228: if ($this->declaration === self::CLASS_CONSTANT) {
229: return $this->phpVersion->supportsNativeTypesInClassConstants();
230: }
231:
232: return true;
233: }
234:
235: /** members of an intersection can only be class names */
236: private function isKeywordAllowed(): bool
237: {
238: return $this->isTypeDeclarationAllowed() && $this->context !== self::INTERSECTION_MEMBER;
239: }
240:
241: private function isAtLeast(int $versionId): bool
242: {
243: return $this->phpVersion->getVersionId() >= $versionId;
244: }
245:
246: }
247: