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