1<?php declare(strict_types=1);
2
3namespace PhpParser;
4
5use PhpParser\Node\ComplexType;
6use PhpParser\Node\Expr;
7use PhpParser\Node\Identifier;
8use PhpParser\Node\Name;
9use PhpParser\Node\Name\FullyQualified;
10use PhpParser\Node\NullableType;
11use PhpParser\Node\Scalar;
12use PhpParser\Node\Stmt;
13
14/**
15 * This class defines helpers used in the implementation of builders. Don't use it directly.
16 *
17 * @internal
18 */
19final class BuilderHelpers {
20    /**
21     * Normalizes a node: Converts builder objects to nodes.
22     *
23     * @param Node|Builder $node The node to normalize
24     *
25     * @return Node The normalized node
26     */
27    public static function normalizeNode($node): Node {
28        if ($node instanceof Builder) {
29            return $node->getNode();
30        }
31
32        if ($node instanceof Node) {
33            return $node;
34        }
35
36        throw new \LogicException('Expected node or builder object');
37    }
38
39    /**
40     * Normalizes a node to a statement.
41     *
42     * Expressions are wrapped in a Stmt\Expression node.
43     *
44     * @param Node|Builder $node The node to normalize
45     *
46     * @return Stmt The normalized statement node
47     */
48    public static function normalizeStmt($node): Stmt {
49        $node = self::normalizeNode($node);
50        if ($node instanceof Stmt) {
51            return $node;
52        }
53
54        if ($node instanceof Expr) {
55            return new Stmt\Expression($node);
56        }
57
58        throw new \LogicException('Expected statement or expression node');
59    }
60
61    /**
62     * Normalizes strings to Identifier.
63     *
64     * @param string|Identifier $name The identifier to normalize
65     *
66     * @return Identifier The normalized identifier
67     */
68    public static function normalizeIdentifier($name): Identifier {
69        if ($name instanceof Identifier) {
70            return $name;
71        }
72
73        if (\is_string($name)) {
74            return new Identifier($name);
75        }
76
77        throw new \LogicException('Expected string or instance of Node\Identifier');
78    }
79
80    /**
81     * Normalizes strings to Identifier, also allowing expressions.
82     *
83     * @param string|Identifier|Expr $name The identifier to normalize
84     *
85     * @return Identifier|Expr The normalized identifier or expression
86     */
87    public static function normalizeIdentifierOrExpr($name) {
88        if ($name instanceof Identifier || $name instanceof Expr) {
89            return $name;
90        }
91
92        if (\is_string($name)) {
93            return new Identifier($name);
94        }
95
96        throw new \LogicException('Expected string or instance of Node\Identifier or Node\Expr');
97    }
98
99    /**
100     * Normalizes a name: Converts string names to Name nodes.
101     *
102     * @param Name|string $name The name to normalize
103     *
104     * @return Name The normalized name
105     */
106    public static function normalizeName($name): Name {
107        if ($name instanceof Name) {
108            return $name;
109        }
110
111        if (is_string($name)) {
112            if (!$name) {
113                throw new \LogicException('Name cannot be empty');
114            }
115
116            if ($name[0] === '\\') {
117                return new Name\FullyQualified(substr($name, 1));
118            }
119
120            if (0 === strpos($name, 'namespace\\')) {
121                return new Name\Relative(substr($name, strlen('namespace\\')));
122            }
123
124            return new Name($name);
125        }
126
127        throw new \LogicException('Name must be a string or an instance of Node\Name');
128    }
129
130    /**
131     * Normalizes a name: Converts string names to Name nodes, while also allowing expressions.
132     *
133     * @param Expr|Name|string $name The name to normalize
134     *
135     * @return Name|Expr The normalized name or expression
136     */
137    public static function normalizeNameOrExpr($name) {
138        if ($name instanceof Expr) {
139            return $name;
140        }
141
142        if (!is_string($name) && !($name instanceof Name)) {
143            throw new \LogicException(
144                'Name must be a string or an instance of Node\Name or Node\Expr'
145            );
146        }
147
148        return self::normalizeName($name);
149    }
150
151    /**
152     * Normalizes a type: Converts plain-text type names into proper AST representation.
153     *
154     * In particular, builtin types become Identifiers, custom types become Names and nullables
155     * are wrapped in NullableType nodes.
156     *
157     * @param string|Name|Identifier|ComplexType $type The type to normalize
158     *
159     * @return Name|Identifier|ComplexType The normalized type
160     */
161    public static function normalizeType($type) {
162        if (!is_string($type)) {
163            if (
164                !$type instanceof Name && !$type instanceof Identifier &&
165                !$type instanceof ComplexType
166            ) {
167                throw new \LogicException(
168                    'Type must be a string, or an instance of Name, Identifier or ComplexType'
169                );
170            }
171            return $type;
172        }
173
174        $nullable = false;
175        if (strlen($type) > 0 && $type[0] === '?') {
176            $nullable = true;
177            $type = substr($type, 1);
178        }
179
180        $builtinTypes = [
181            'array',
182            'callable',
183            'bool',
184            'int',
185            'float',
186            'string',
187            'iterable',
188            'void',
189            'object',
190            'null',
191            'false',
192            'mixed',
193            'never',
194            'true',
195        ];
196
197        $lowerType = strtolower($type);
198        if (in_array($lowerType, $builtinTypes)) {
199            $type = new Identifier($lowerType);
200        } else {
201            $type = self::normalizeName($type);
202        }
203
204        $notNullableTypes = [
205            'void', 'mixed', 'never',
206        ];
207        if ($nullable && in_array((string) $type, $notNullableTypes)) {
208            throw new \LogicException(sprintf('%s type cannot be nullable', $type));
209        }
210
211        return $nullable ? new NullableType($type) : $type;
212    }
213
214    /**
215     * Normalizes a value: Converts nulls, booleans, integers,
216     * floats, strings and arrays into their respective nodes
217     *
218     * @param Node\Expr|bool|null|int|float|string|array|\UnitEnum $value The value to normalize
219     *
220     * @return Expr The normalized value
221     */
222    public static function normalizeValue($value): Expr {
223        if ($value instanceof Node\Expr) {
224            return $value;
225        }
226
227        if (is_null($value)) {
228            return new Expr\ConstFetch(
229                new Name('null')
230            );
231        }
232
233        if (is_bool($value)) {
234            return new Expr\ConstFetch(
235                new Name($value ? 'true' : 'false')
236            );
237        }
238
239        if (is_int($value)) {
240            return new Scalar\Int_($value);
241        }
242
243        if (is_float($value)) {
244            return new Scalar\Float_($value);
245        }
246
247        if (is_string($value)) {
248            return new Scalar\String_($value);
249        }
250
251        if (is_array($value)) {
252            $items = [];
253            $lastKey = -1;
254            foreach ($value as $itemKey => $itemValue) {
255                // for consecutive, numeric keys don't generate keys
256                if (null !== $lastKey && ++$lastKey === $itemKey) {
257                    $items[] = new Node\ArrayItem(
258                        self::normalizeValue($itemValue)
259                    );
260                } else {
261                    $lastKey = null;
262                    $items[] = new Node\ArrayItem(
263                        self::normalizeValue($itemValue),
264                        self::normalizeValue($itemKey)
265                    );
266                }
267            }
268
269            return new Expr\Array_($items);
270        }
271
272        if ($value instanceof \UnitEnum) {
273            return new Expr\ClassConstFetch(new FullyQualified(\get_class($value)), new Identifier($value->name));
274        }
275
276        throw new \LogicException('Invalid value');
277    }
278
279    /**
280     * Normalizes a doc comment: Converts plain strings to PhpParser\Comment\Doc.
281     *
282     * @param Comment\Doc|string $docComment The doc comment to normalize
283     *
284     * @return Comment\Doc The normalized doc comment
285     */
286    public static function normalizeDocComment($docComment): Comment\Doc {
287        if ($docComment instanceof Comment\Doc) {
288            return $docComment;
289        }
290
291        if (is_string($docComment)) {
292            return new Comment\Doc($docComment);
293        }
294
295        throw new \LogicException('Doc comment must be a string or an instance of PhpParser\Comment\Doc');
296    }
297
298    /**
299     * Normalizes a attribute: Converts attribute to the Attribute Group if needed.
300     *
301     * @param Node\Attribute|Node\AttributeGroup $attribute
302     *
303     * @return Node\AttributeGroup The Attribute Group
304     */
305    public static function normalizeAttribute($attribute): Node\AttributeGroup {
306        if ($attribute instanceof Node\AttributeGroup) {
307            return $attribute;
308        }
309
310        if (!($attribute instanceof Node\Attribute)) {
311            throw new \LogicException('Attribute must be an instance of PhpParser\Node\Attribute or PhpParser\Node\AttributeGroup');
312        }
313
314        return new Node\AttributeGroup([$attribute]);
315    }
316
317    /**
318     * Adds a modifier and returns new modifier bitmask.
319     *
320     * @param int $modifiers Existing modifiers
321     * @param int $modifier Modifier to set
322     *
323     * @return int New modifiers
324     */
325    public static function addModifier(int $modifiers, int $modifier): int {
326        Modifiers::verifyModifier($modifiers, $modifier);
327        return $modifiers | $modifier;
328    }
329
330    /**
331     * Adds a modifier and returns new modifier bitmask.
332     * @return int New modifiers
333     */
334    public static function addClassModifier(int $existingModifiers, int $modifierToSet): int {
335        Modifiers::verifyClassModifier($existingModifiers, $modifierToSet);
336        return $existingModifiers | $modifierToSet;
337    }
338}
339