‪TYPO3CMS  ‪main
checkNamespaceIntegrity.php
Go to the documentation of this file.
1 #!/usr/bin/env php
2 <?php
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
18 use PhpParser\Node;
19 use PhpParser\NodeTraverser;
20 use PhpParser\NodeVisitor\NameResolver;
21 use PhpParser\NodeVisitorAbstract;
22 use PhpParser\ParserFactory;
23 use Symfony\Component\Finder\Finder;
24 
25 if (PHP_SAPI !== 'cli') {
26  die('Script must be called from command line.' . chr(10));
27 }
28 
29 require __DIR__ . '/../../vendor/autoload.php';
30 
35 {
36  public function ‪scan(): int
37  {
38  $ignoreFiles = [
39  // ignored, pure fixture file
40  'typo3/sysext/core/Tests/Unit/Configuration/TypoScript/ConditionMatching/Fixtures/ConditionMatcherUserFuncs.php',
41  // ignored, pure fixture file
42  'typo3/sysext/install/Tests/Unit/ExtensionScanner/Php/Matcher/Fixtures/PropertyExistsStaticMatcherFixture.php',
43  ];
44  $ignoreNamespaceParts = ['Classes'];
45  ‪$parser = (new ParserFactory())->create(ParserFactory::ONLY_PHP7);
46  $files = $this->‪createFinder();
47  $invalidNamespaces = [];
48  foreach ($files as $file) {
50  $fullFilename = $file->getRealPath();
51  preg_match('/.*typo3\/sysext\/(.*)$/', $fullFilename, $matches);
52  $relativeFilenameFromRoot = 'typo3/sysext/' . $matches[1];
53  if (in_array($relativeFilenameFromRoot, $ignoreFiles, true)) {
54  continue;
55  }
56  $parts = explode('/', $matches[1]);
57  $sysExtName = $parts[0];
58  unset($parts[0]);
59  if (in_array($parts[1], $ignoreNamespaceParts, true)) {
60  unset($parts[1]);
61  }
62 
63  $relativeFilenameWithoutSystemExtensionRoot = substr($relativeFilenameFromRoot, (mb_strlen('typo3/sysext/' . $sysExtName . '/')));
64  $expectedFullQualifiedObjectNamespace = $this->‪determineExpectedFullQualifiedNamespace($sysExtName, $relativeFilenameWithoutSystemExtensionRoot);
65  $ast = ‪$parser->parse($file->getContents());
66  $traverser = new NodeTraverser();
67  $visitor = new NameResolver();
68  $traverser->addVisitor($visitor);
69  $visitor = new ‪NamespaceValidationVisitor();
70  $traverser->addVisitor($visitor);
71  $traverser->traverse($ast);
72 
73  $fileObjectType = $visitor->getType();
74  $fileObjectFullQualifiedObjectNamespace = $visitor->getFullQualifiedObjectNamespace();
75  if ($fileObjectType !== ''
76  && $expectedFullQualifiedObjectNamespace !== $fileObjectFullQualifiedObjectNamespace
77  ) {
78  $invalidNamespaces[$sysExtName][] = [
79  'file' => $relativeFilenameFromRoot,
80  'shouldBe' => $expectedFullQualifiedObjectNamespace,
81  'actualIs' => $fileObjectFullQualifiedObjectNamespace,
82  ];
83  }
84  }
85 
86  ‪$output = new \Symfony\Component\Console\Output\ConsoleOutput();
87  ‪$output->writeln('');
88  if ($invalidNamespaces !== []) {
89  ‪$output->writeln(' ❌ Namespace integrity broken.');
90  ‪$output->writeln('');
91  $table = new \Symfony\Component\Console\Helper\Table(‪$output);
92  $table->setHeaders([
93  'EXT',
94  'File',
95  'should be',
96  'actual is',
97  ]);
98  foreach ($invalidNamespaces as $extKey => $results) {
99  foreach ($results as $result) {
100  $table->addRow([
101  $extKey,
102  $result['file'],
103  $result['shouldBe'] ?: '❌ no proper registered PSR-4 namespace',
104  $result['actualIs'],
105  ]);
106  }
107  }
108  $table->render();
109  ‪$output->writeln('');
110  ‪$output->writeln('');
111  return 1;
112  }
113  ‪$output->writeln(' ✅ Namespace integrity is in good shape.');
114  ‪$output->writeln('');
115  return 0;
116  }
117 
119  string $systemExtensionKey,
120  string $relativeFilename,
121  ): string {
122  $namespace = '';
123  if (str_starts_with($relativeFilename, 'Classes/')) {
124  $namespace = $this->‪getExtensionClassesNamespace($systemExtensionKey, $relativeFilename);
125  } elseif (str_starts_with($relativeFilename, 'Tests/')) {
126  $namespace = $this->‪getExtensionTestsNamespaces($systemExtensionKey, $relativeFilename);
127  }
128  $ignorePartValues= ['Classes', 'Tests'];
129  if ($namespace !== '') {
130  $parts = explode('/', $relativeFilename);
131  if (in_array($parts[0], $ignorePartValues, true)) {
132  unset($parts[0]);
133  }
134  foreach ($parts as $part) {
135  if (str_ends_with($part, '.php')) {
136  $namespace .= mb_substr($part, 0, -4);
137  break;
138  }
139  $namespace .= $part . '\\';
140  }
141  }
142  return $namespace;
143  }
144 
146  string $systemExtensionKey,
147  string $relativeFilename
148  ): string {
150  $systemExtensionKey,
151  __DIR__ . '/../../typo3/sysext/' . $systemExtensionKey . '/composer.json',
152  $relativeFilename
153  );
154  }
155 
156  protected function ‪getExtensionTestsNamespaces(
157  string $systemExtensionKey,
158  string $relativeFilename
159  ): string {
161  $systemExtensionKey,
162  __DIR__ . '/../../composer.json',
163  $relativeFilename,
164  true
165  );
166  }
167 
169  string $systemExtensionKey,
170  string $fullComposerJsonFilePath,
171  string $relativeFileName,
172  bool $autoloadDev=false
173  ): string {
174  $autoloadKey = 'autoload';
175  if ($autoloadDev) {
176  $autoloadKey .= '-dev';
177  }
178  if (file_exists($fullComposerJsonFilePath)) {
179  $composerInfo = \json_decode(
180  file_get_contents($fullComposerJsonFilePath),
181  true
182  );
183  if (is_array($composerInfo)) {
184  $autoloadPSR4 = $composerInfo[$autoloadKey]['psr-4'] ?? [];
185 
186  $pathBasedAutoloadInformation = [];
187  foreach ($autoloadPSR4 as $namespace => $relativePath) {
188  $pathBasedAutoloadInformation[trim($relativePath, '/') . '/'] = $namespace;
189  }
190  $keys = array_map(mb_strlen(...), array_keys($pathBasedAutoloadInformation));
191  array_multisort($keys, SORT_DESC, $pathBasedAutoloadInformation);
192 
193  foreach ($pathBasedAutoloadInformation as $relativePath => $namespace) {
194  if ($autoloadDev && str_starts_with('typo3/sysext/' . $systemExtensionKey . '/' . $relativeFileName, $relativePath)) {
195  return $namespace;
196  }
197  if (str_starts_with($relativeFileName, $relativePath)) {
198  return $namespace;
199  }
200  }
201  }
202  }
203  return '';
204  }
205 
206  protected function ‪createFinder(): Finder
207  {
208  return (new Finder())
209  ->files()
210  ->in(
211  dirs: [
212  __DIR__ . '/../../typo3/sysext/*/Classes',
213  __DIR__ . '/../../typo3/sysext/*/Tests/Unit',
214  __DIR__ . '/../../typo3/sysext/*/Tests/UnitDeprecated',
215  __DIR__ . '/../../typo3/sysext/*/Tests/Functional',
216  __DIR__ . '/../../typo3/sysext/*/Tests/FunctionalDeprecated',
217  __DIR__ . '/../../typo3/sysext/core/Tests/Acceptance',
218  ]
219  )
220  ->notPath('typo3/sysext/core/Tests/Acceptance/Support/_generated')
221  // @todo remove fixture extensions exclude and handle properly after fixture extensions has been streamlined
222  ->notPath([
223  'Fixtures/Extensions',
224  'Fixtures/Extension',
225  'Fixture/Extensions',
226  'Fixture/Extension',
227  'Core/Fixtures/test_extension',
228  ])
229  ->name('*.php')
230  ->sortByName();
231  }
232 }
233 
237 class ‪NamespaceValidationVisitor extends NodeVisitorAbstract
238 {
239  private string ‪$type = '';
240  private string ‪$fullQualifiedObjectNamespace = '';
241 
242  public function ‪enterNode(Node $node)
243  {
244  if ($this->type === '') {
245  if ($node instanceof Node\Stmt\Class_
246  && !$node->isAnonymous()
247  ) {
248  $this->type = 'class';
249  $this->fullQualifiedObjectNamespace = (string)$node->namespacedName;
250  }
251  if ($node instanceof Node\Stmt\Interface_) {
252  $this->type = 'interface';
253  $this->fullQualifiedObjectNamespace = (string)$node->namespacedName;
254  }
255  if ($node instanceof Node\Stmt\Enum_) {
256  $this->type = 'enum';
257  $this->fullQualifiedObjectNamespace = (string)$node->namespacedName;
258  }
259  if ($node instanceof Node\Stmt\Trait_) {
260  $this->type = 'trait';
261  $this->fullQualifiedObjectNamespace = (string)$node->namespacedName;
262  }
263  }
264  }
265 
266  public function ‪getType(): string
267  {
268  return ‪$this->type;
269  }
270 
271  public function ‪getFullQualifiedObjectNamespace(): string
272  {
274  }
275 }
276 
277 // execute scan and return corresponding exit code.
278 // 0: everything ok
279 // 1: failed, one or more files has invalid namespace declaration
280 exit((new ‪CheckNamespaceIntegrity())->scan());
‪CheckNamespaceIntegrity\getExtensionTestsNamespaces
‪getExtensionTestsNamespaces(string $systemExtensionKey, string $relativeFilename)
Definition: checkNamespaceIntegrity.php:156
‪$parser
‪$parser
Definition: annotationChecker.php:108
‪NamespaceValidationVisitor
Definition: checkNamespaceIntegrity.php:238
‪CheckNamespaceIntegrity\scan
‪scan()
Definition: checkNamespaceIntegrity.php:36
‪NamespaceValidationVisitor\enterNode
‪enterNode(Node $node)
Definition: checkNamespaceIntegrity.php:242
‪NamespaceValidationVisitor\getFullQualifiedObjectNamespace
‪getFullQualifiedObjectNamespace()
Definition: checkNamespaceIntegrity.php:271
‪NamespaceValidationVisitor\$fullQualifiedObjectNamespace
‪string $fullQualifiedObjectNamespace
Definition: checkNamespaceIntegrity.php:240
‪NamespaceValidationVisitor\getType
‪getType()
Definition: checkNamespaceIntegrity.php:266
‪CheckNamespaceIntegrity
Definition: checkNamespaceIntegrity.php:35
‪CheckNamespaceIntegrity\getPSR4NamespaceFromComposerJson
‪getPSR4NamespaceFromComposerJson(string $systemExtensionKey, string $fullComposerJsonFilePath, string $relativeFileName, bool $autoloadDev=false)
Definition: checkNamespaceIntegrity.php:168
‪$output
‪$output
Definition: annotationChecker.php:119
‪CheckNamespaceIntegrity\createFinder
‪createFinder()
Definition: checkNamespaceIntegrity.php:206
‪CheckNamespaceIntegrity\getExtensionClassesNamespace
‪getExtensionClassesNamespace(string $systemExtensionKey, string $relativeFilename)
Definition: checkNamespaceIntegrity.php:145
‪NamespaceValidationVisitor\$type
‪string $type
Definition: checkNamespaceIntegrity.php:239
‪CheckNamespaceIntegrity\determineExpectedFullQualifiedNamespace
‪determineExpectedFullQualifiedNamespace(string $systemExtensionKey, string $relativeFilename,)
Definition: checkNamespaceIntegrity.php:118