xref: /PHP-Parser/lib/PhpParser/NameContext.php (revision 2d3dd4e2)
1<?php declare(strict_types=1);
2
3namespace PhpParser;
4
5use PhpParser\Node\Name;
6use PhpParser\Node\Name\FullyQualified;
7use PhpParser\Node\Stmt;
8
9class NameContext {
10    /** @var null|Name Current namespace */
11    protected ?Name $namespace;
12
13    /** @var Name[][] Map of format [aliasType => [aliasName => originalName]] */
14    protected array $aliases = [];
15
16    /** @var Name[][] Same as $aliases but preserving original case */
17    protected array $origAliases = [];
18
19    /** @var ErrorHandler Error handler */
20    protected ErrorHandler $errorHandler;
21
22    /**
23     * Create a name context.
24     *
25     * @param ErrorHandler $errorHandler Error handling used to report errors
26     */
27    public function __construct(ErrorHandler $errorHandler) {
28        $this->errorHandler = $errorHandler;
29    }
30
31    /**
32     * Start a new namespace.
33     *
34     * This also resets the alias table.
35     *
36     * @param Name|null $namespace Null is the global namespace
37     */
38    public function startNamespace(?Name $namespace = null): void {
39        $this->namespace = $namespace;
40        $this->origAliases = $this->aliases = [
41            Stmt\Use_::TYPE_NORMAL   => [],
42            Stmt\Use_::TYPE_FUNCTION => [],
43            Stmt\Use_::TYPE_CONSTANT => [],
44        ];
45    }
46
47    /**
48     * Add an alias / import.
49     *
50     * @param Name $name Original name
51     * @param string $aliasName Aliased name
52     * @param Stmt\Use_::TYPE_* $type One of Stmt\Use_::TYPE_*
53     * @param array<string, mixed> $errorAttrs Attributes to use to report an error
54     */
55    public function addAlias(Name $name, string $aliasName, int $type, array $errorAttrs = []): void {
56        // Constant names are case sensitive, everything else case insensitive
57        if ($type === Stmt\Use_::TYPE_CONSTANT) {
58            $aliasLookupName = $aliasName;
59        } else {
60            $aliasLookupName = strtolower($aliasName);
61        }
62
63        if (isset($this->aliases[$type][$aliasLookupName])) {
64            $typeStringMap = [
65                Stmt\Use_::TYPE_NORMAL   => '',
66                Stmt\Use_::TYPE_FUNCTION => 'function ',
67                Stmt\Use_::TYPE_CONSTANT => 'const ',
68            ];
69
70            $this->errorHandler->handleError(new Error(
71                sprintf(
72                    'Cannot use %s%s as %s because the name is already in use',
73                    $typeStringMap[$type], $name, $aliasName
74                ),
75                $errorAttrs
76            ));
77            return;
78        }
79
80        $this->aliases[$type][$aliasLookupName] = $name;
81        $this->origAliases[$type][$aliasName] = $name;
82    }
83
84    /**
85     * Get current namespace.
86     *
87     * @return null|Name Namespace (or null if global namespace)
88     */
89    public function getNamespace(): ?Name {
90        return $this->namespace;
91    }
92
93    /**
94     * Get resolved name.
95     *
96     * @param Name $name Name to resolve
97     * @param Stmt\Use_::TYPE_* $type One of Stmt\Use_::TYPE_{FUNCTION|CONSTANT}
98     *
99     * @return null|Name Resolved name, or null if static resolution is not possible
100     */
101    public function getResolvedName(Name $name, int $type): ?Name {
102        // don't resolve special class names
103        if ($type === Stmt\Use_::TYPE_NORMAL && $name->isSpecialClassName()) {
104            if (!$name->isUnqualified()) {
105                $this->errorHandler->handleError(new Error(
106                    sprintf("'\\%s' is an invalid class name", $name->toString()),
107                    $name->getAttributes()
108                ));
109            }
110            return $name;
111        }
112
113        // fully qualified names are already resolved
114        if ($name->isFullyQualified()) {
115            return $name;
116        }
117
118        // Try to resolve aliases
119        if (null !== $resolvedName = $this->resolveAlias($name, $type)) {
120            return $resolvedName;
121        }
122
123        if ($type !== Stmt\Use_::TYPE_NORMAL && $name->isUnqualified()) {
124            if (null === $this->namespace) {
125                // outside of a namespace unaliased unqualified is same as fully qualified
126                return new FullyQualified($name, $name->getAttributes());
127            }
128
129            // Cannot resolve statically
130            return null;
131        }
132
133        // if no alias exists prepend current namespace
134        return FullyQualified::concat($this->namespace, $name, $name->getAttributes());
135    }
136
137    /**
138     * Get resolved class name.
139     *
140     * @param Name $name Class ame to resolve
141     *
142     * @return Name Resolved name
143     */
144    public function getResolvedClassName(Name $name): Name {
145        return $this->getResolvedName($name, Stmt\Use_::TYPE_NORMAL);
146    }
147
148    /**
149     * Get possible ways of writing a fully qualified name (e.g., by making use of aliases).
150     *
151     * @param string $name Fully-qualified name (without leading namespace separator)
152     * @param Stmt\Use_::TYPE_* $type One of Stmt\Use_::TYPE_*
153     *
154     * @return Name[] Possible representations of the name
155     */
156    public function getPossibleNames(string $name, int $type): array {
157        $lcName = strtolower($name);
158
159        if ($type === Stmt\Use_::TYPE_NORMAL) {
160            // self, parent and static must always be unqualified
161            if ($lcName === "self" || $lcName === "parent" || $lcName === "static") {
162                return [new Name($name)];
163            }
164        }
165
166        // Collect possible ways to write this name, starting with the fully-qualified name
167        $possibleNames = [new FullyQualified($name)];
168
169        if (null !== $nsRelativeName = $this->getNamespaceRelativeName($name, $lcName, $type)) {
170            // Make sure there is no alias that makes the normally namespace-relative name
171            // into something else
172            if (null === $this->resolveAlias($nsRelativeName, $type)) {
173                $possibleNames[] = $nsRelativeName;
174            }
175        }
176
177        // Check for relevant namespace use statements
178        foreach ($this->origAliases[Stmt\Use_::TYPE_NORMAL] as $alias => $orig) {
179            $lcOrig = $orig->toLowerString();
180            if (0 === strpos($lcName, $lcOrig . '\\')) {
181                $possibleNames[] = new Name($alias . substr($name, strlen($lcOrig)));
182            }
183        }
184
185        // Check for relevant type-specific use statements
186        foreach ($this->origAliases[$type] as $alias => $orig) {
187            if ($type === Stmt\Use_::TYPE_CONSTANT) {
188                // Constants are are complicated-sensitive
189                $normalizedOrig = $this->normalizeConstName($orig->toString());
190                if ($normalizedOrig === $this->normalizeConstName($name)) {
191                    $possibleNames[] = new Name($alias);
192                }
193            } else {
194                // Everything else is case-insensitive
195                if ($orig->toLowerString() === $lcName) {
196                    $possibleNames[] = new Name($alias);
197                }
198            }
199        }
200
201        return $possibleNames;
202    }
203
204    /**
205     * Get shortest representation of this fully-qualified name.
206     *
207     * @param string $name Fully-qualified name (without leading namespace separator)
208     * @param Stmt\Use_::TYPE_* $type One of Stmt\Use_::TYPE_*
209     *
210     * @return Name Shortest representation
211     */
212    public function getShortName(string $name, int $type): Name {
213        $possibleNames = $this->getPossibleNames($name, $type);
214
215        // Find shortest name
216        $shortestName = null;
217        $shortestLength = \INF;
218        foreach ($possibleNames as $possibleName) {
219            $length = strlen($possibleName->toCodeString());
220            if ($length < $shortestLength) {
221                $shortestName = $possibleName;
222                $shortestLength = $length;
223            }
224        }
225
226        return $shortestName;
227    }
228
229    private function resolveAlias(Name $name, int $type): ?FullyQualified {
230        $firstPart = $name->getFirst();
231
232        if ($name->isQualified()) {
233            // resolve aliases for qualified names, always against class alias table
234            $checkName = strtolower($firstPart);
235            if (isset($this->aliases[Stmt\Use_::TYPE_NORMAL][$checkName])) {
236                $alias = $this->aliases[Stmt\Use_::TYPE_NORMAL][$checkName];
237                return FullyQualified::concat($alias, $name->slice(1), $name->getAttributes());
238            }
239        } elseif ($name->isUnqualified()) {
240            // constant aliases are case-sensitive, function aliases case-insensitive
241            $checkName = $type === Stmt\Use_::TYPE_CONSTANT ? $firstPart : strtolower($firstPart);
242            if (isset($this->aliases[$type][$checkName])) {
243                // resolve unqualified aliases
244                return new FullyQualified($this->aliases[$type][$checkName], $name->getAttributes());
245            }
246        }
247
248        // No applicable aliases
249        return null;
250    }
251
252    private function getNamespaceRelativeName(string $name, string $lcName, int $type): ?Name {
253        if (null === $this->namespace) {
254            return new Name($name);
255        }
256
257        if ($type === Stmt\Use_::TYPE_CONSTANT) {
258            // The constants true/false/null always resolve to the global symbols, even inside a
259            // namespace, so they may be used without qualification
260            if ($lcName === "true" || $lcName === "false" || $lcName === "null") {
261                return new Name($name);
262            }
263        }
264
265        $namespacePrefix = strtolower($this->namespace . '\\');
266        if (0 === strpos($lcName, $namespacePrefix)) {
267            return new Name(substr($name, strlen($namespacePrefix)));
268        }
269
270        return null;
271    }
272
273    private function normalizeConstName(string $name): string {
274        $nsSep = strrpos($name, '\\');
275        if (false === $nsSep) {
276            return $name;
277        }
278
279        // Constants have case-insensitive namespace and case-sensitive short-name
280        $ns = substr($name, 0, $nsSep);
281        $shortName = substr($name, $nsSep + 1);
282        return strtolower($ns) . '\\' . $shortName;
283    }
284}
285