NameContext.php 9.8 KB

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