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