PsrAutoloadingFixer.php 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of PHP CS Fixer.
  5. *
  6. * (c) Fabien Potencier <fabien@symfony.com>
  7. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  8. *
  9. * This source file is subject to the MIT license that is bundled
  10. * with this source code in the file LICENSE.
  11. */
  12. namespace PhpCsFixer\Fixer\Basic;
  13. use PhpCsFixer\AbstractFixer;
  14. use PhpCsFixer\DocBlock\TypeExpression;
  15. use PhpCsFixer\Fixer\ConfigurableFixerInterface;
  16. use PhpCsFixer\Fixer\ConfigurableFixerTrait;
  17. use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
  18. use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
  19. use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
  20. use PhpCsFixer\FixerDefinition\FileSpecificCodeSample;
  21. use PhpCsFixer\FixerDefinition\FixerDefinition;
  22. use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
  23. use PhpCsFixer\Preg;
  24. use PhpCsFixer\StdinFileInfo;
  25. use PhpCsFixer\Tokenizer\Token;
  26. use PhpCsFixer\Tokenizer\Tokens;
  27. use PhpCsFixer\Tokenizer\TokensAnalyzer;
  28. /**
  29. * @author Jordi Boggiano <j.boggiano@seld.be>
  30. * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  31. * @author Bram Gotink <bram@gotink.me>
  32. * @author Graham Campbell <hello@gjcampbell.co.uk>
  33. * @author Kuba Werłos <werlos@gmail.com>
  34. *
  35. * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
  36. *
  37. * @phpstan-type _AutogeneratedInputConfiguration array{
  38. * dir?: null|string
  39. * }
  40. * @phpstan-type _AutogeneratedComputedConfiguration array{
  41. * dir: null|string
  42. * }
  43. */
  44. final class PsrAutoloadingFixer extends AbstractFixer implements ConfigurableFixerInterface
  45. {
  46. /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
  47. use ConfigurableFixerTrait;
  48. public function getDefinition(): FixerDefinitionInterface
  49. {
  50. return new FixerDefinition(
  51. 'Classes must be in a path that matches their namespace, be at least one namespace deep and the class name should match the file name.',
  52. [
  53. new FileSpecificCodeSample(
  54. '<?php
  55. namespace PhpCsFixer\FIXER\Basic;
  56. class InvalidName {}
  57. ',
  58. new \SplFileInfo(__FILE__)
  59. ),
  60. new FileSpecificCodeSample(
  61. '<?php
  62. namespace PhpCsFixer\FIXER\Basic;
  63. class InvalidName {}
  64. ',
  65. new \SplFileInfo(__FILE__),
  66. ['dir' => './src']
  67. ),
  68. ],
  69. null,
  70. 'This fixer may change your class name, which will break the code that depends on the old name.'
  71. );
  72. }
  73. public function isCandidate(Tokens $tokens): bool
  74. {
  75. return $tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds());
  76. }
  77. public function isRisky(): bool
  78. {
  79. return true;
  80. }
  81. /**
  82. * {@inheritdoc}
  83. *
  84. * Must run before SelfAccessorFixer.
  85. */
  86. public function getPriority(): int
  87. {
  88. return -10;
  89. }
  90. public function supports(\SplFileInfo $file): bool
  91. {
  92. if ($file instanceof StdinFileInfo) {
  93. return false;
  94. }
  95. if (
  96. // ignore file with extension other than php
  97. ('php' !== $file->getExtension())
  98. // ignore file with name that cannot be a class name
  99. || !Preg::match('/^'.TypeExpression::REGEX_IDENTIFIER.'$/', $file->getBasename('.php'))
  100. ) {
  101. return false;
  102. }
  103. try {
  104. $tokens = Tokens::fromCode(\sprintf('<?php class %s {}', $file->getBasename('.php')));
  105. if ($tokens[3]->isKeyword() || $tokens[3]->isMagicConstant()) {
  106. // name cannot be a class name - detected by PHP 5.x
  107. return false;
  108. }
  109. } catch (\ParseError $e) {
  110. // name cannot be a class name - detected by PHP 7.x
  111. return false;
  112. }
  113. // ignore stubs/fixtures, since they typically contain invalid files for various reasons
  114. return !Preg::match('{[/\\\](stub|fixture)s?[/\\\]}i', $file->getRealPath());
  115. }
  116. protected function configurePostNormalisation(): void
  117. {
  118. if (null !== $this->configuration['dir']) {
  119. $realpath = realpath($this->configuration['dir']);
  120. if (false === $realpath) {
  121. throw new \InvalidArgumentException(\sprintf('Failed to resolve configured directory "%s".', $this->configuration['dir']));
  122. }
  123. $this->configuration['dir'] = $realpath;
  124. }
  125. }
  126. protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
  127. {
  128. return new FixerConfigurationResolver([
  129. (new FixerOptionBuilder('dir', 'If provided, the directory where the project code is placed.'))
  130. ->setAllowedTypes(['null', 'string'])
  131. ->setDefault(null)
  132. ->getOption(),
  133. ]);
  134. }
  135. protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
  136. {
  137. $tokenAnalyzer = new TokensAnalyzer($tokens);
  138. if (null !== $this->configuration['dir'] && !str_starts_with($file->getRealPath(), $this->configuration['dir'])) {
  139. return;
  140. }
  141. $namespace = null;
  142. $namespaceStartIndex = null;
  143. $namespaceEndIndex = null;
  144. $classyName = null;
  145. $classyIndex = null;
  146. foreach ($tokens as $index => $token) {
  147. if ($token->isGivenKind(T_NAMESPACE)) {
  148. if (null !== $namespace) {
  149. return;
  150. }
  151. $namespaceStartIndex = $tokens->getNextMeaningfulToken($index);
  152. $namespaceEndIndex = $tokens->getNextTokenOfKind($namespaceStartIndex, [';']);
  153. $namespace = trim($tokens->generatePartialCode($namespaceStartIndex, $namespaceEndIndex - 1));
  154. } elseif ($token->isClassy()) {
  155. if ($tokenAnalyzer->isAnonymousClass($index)) {
  156. continue;
  157. }
  158. if (null !== $classyName) {
  159. return;
  160. }
  161. $classyIndex = $tokens->getNextMeaningfulToken($index);
  162. $classyName = $tokens[$classyIndex]->getContent();
  163. }
  164. }
  165. if (null === $classyName) {
  166. return;
  167. }
  168. $expectedClassyName = $this->calculateClassyName($file, $namespace, $classyName);
  169. if ($classyName !== $expectedClassyName) {
  170. $tokens[$classyIndex] = new Token([T_STRING, $expectedClassyName]);
  171. }
  172. if (null === $this->configuration['dir'] || null === $namespace) {
  173. return;
  174. }
  175. if (!is_dir($this->configuration['dir'])) {
  176. return;
  177. }
  178. $configuredDir = realpath($this->configuration['dir']);
  179. $fileDir = \dirname($file->getRealPath());
  180. if (\strlen($configuredDir) >= \strlen($fileDir)) {
  181. return;
  182. }
  183. $newNamespace = substr(str_replace('/', '\\', $fileDir), \strlen($configuredDir) + 1);
  184. $originalNamespace = substr($namespace, -\strlen($newNamespace));
  185. if ($originalNamespace !== $newNamespace && strtolower($originalNamespace) === strtolower($newNamespace)) {
  186. $tokens->clearRange($namespaceStartIndex, $namespaceEndIndex);
  187. $namespace = substr($namespace, 0, -\strlen($newNamespace)).$newNamespace;
  188. $newNamespace = Tokens::fromCode('<?php namespace '.$namespace.';');
  189. $newNamespace->clearRange(0, 2);
  190. $newNamespace->clearEmptyTokens();
  191. $tokens->insertAt($namespaceStartIndex, $newNamespace);
  192. }
  193. }
  194. private function calculateClassyName(\SplFileInfo $file, ?string $namespace, string $currentName): string
  195. {
  196. $name = $file->getBasename('.php');
  197. $maxNamespace = $this->calculateMaxNamespace($file, $namespace);
  198. if (null !== $this->configuration['dir']) {
  199. return ('' !== $maxNamespace ? (str_replace('\\', '_', $maxNamespace).'_') : '').$name;
  200. }
  201. $namespaceParts = array_reverse(explode('\\', $maxNamespace));
  202. foreach ($namespaceParts as $namespacePart) {
  203. $nameCandidate = \sprintf('%s_%s', $namespacePart, $name);
  204. if (strtolower($nameCandidate) !== strtolower(substr($currentName, -\strlen($nameCandidate)))) {
  205. break;
  206. }
  207. $name = $nameCandidate;
  208. }
  209. return $name;
  210. }
  211. private function calculateMaxNamespace(\SplFileInfo $file, ?string $namespace): string
  212. {
  213. if (null === $this->configuration['dir']) {
  214. $root = \dirname($file->getRealPath());
  215. while ($root !== \dirname($root)) {
  216. $root = \dirname($root);
  217. }
  218. } else {
  219. $root = realpath($this->configuration['dir']);
  220. }
  221. $namespaceAccordingToFileLocation = trim(str_replace(\DIRECTORY_SEPARATOR, '\\', substr(\dirname($file->getRealPath()), \strlen($root))), '\\');
  222. if (null === $namespace) {
  223. return $namespaceAccordingToFileLocation;
  224. }
  225. $namespaceAccordingToFileLocationPartsReversed = array_reverse(explode('\\', $namespaceAccordingToFileLocation));
  226. $namespacePartsReversed = array_reverse(explode('\\', $namespace));
  227. foreach ($namespacePartsReversed as $key => $namespaceParte) {
  228. if (!isset($namespaceAccordingToFileLocationPartsReversed[$key])) {
  229. break;
  230. }
  231. if (strtolower($namespaceParte) !== strtolower($namespaceAccordingToFileLocationPartsReversed[$key])) {
  232. break;
  233. }
  234. unset($namespaceAccordingToFileLocationPartsReversed[$key]);
  235. }
  236. return implode('\\', array_reverse($namespaceAccordingToFileLocationPartsReversed));
  237. }
  238. }