‪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  // for test fixture extensions, the relativeFileName will be shortened by the sysext file path,
127  // therefor the variable gets passed as reference here
128  $namespace = $this->‪getExtensionTestsNamespaces($systemExtensionKey, $relativeFilename);
129  }
130  $ignorePartValues = ['Classes', 'Tests'];
131  if ($namespace !== '') {
132  $parts = explode('/', $relativeFilename);
133  if (in_array($parts[0], $ignorePartValues, true)) {
134  unset($parts[0]);
135  }
136  foreach ($parts as $part) {
137  if (str_ends_with($part, '.php')) {
138  $namespace .= mb_substr($part, 0, -4);
139  break;
140  }
141  $namespace .= $part . '\\';
142  }
143  }
144  return $namespace;
145  }
146 
148  string $systemExtensionKey,
149  string $relativeFilename
150  ): string {
152  $systemExtensionKey,
153  __DIR__ . '/../../typo3/sysext/' . $systemExtensionKey . '/composer.json',
154  $relativeFilename
155  );
156  }
157 
158  protected function ‪getExtensionTestsNamespaces(
159  string $systemExtensionKey,
160  string &$relativeFilename
161  ): string {
163  $systemExtensionKey,
164  __DIR__ . '/../../composer.json',
165  $relativeFilename,
166  true
167  );
168  }
169 
171  string $systemExtensionKey,
172  string $fullComposerJsonFilePath,
173  string &$relativeFileName,
174  bool $autoloadDev = false
175  ): string {
176  $autoloadKey = 'autoload';
177  if ($autoloadDev) {
178  $autoloadKey .= '-dev';
179  }
180  if (file_exists($fullComposerJsonFilePath)) {
181  $composerInfo = \json_decode(
182  file_get_contents($fullComposerJsonFilePath),
183  true
184  );
185  if (is_array($composerInfo)) {
186  $autoloadPSR4 = $composerInfo[$autoloadKey]['psr-4'] ?? [];
187 
188  $pathBasedAutoloadInformation = [];
189  foreach ($autoloadPSR4 as $namespace => $relativePath) {
190  $pathBasedAutoloadInformation[trim($relativePath, '/') . '/'] = $namespace;
191  }
192  $keys = array_map(mb_strlen(...), array_keys($pathBasedAutoloadInformation));
193  array_multisort($keys, SORT_DESC, $pathBasedAutoloadInformation);
194 
195  foreach ($pathBasedAutoloadInformation as $relativePath => $namespace) {
196  if ($autoloadDev && str_starts_with('typo3/sysext/' . $systemExtensionKey . '/' . $relativeFileName, $relativePath)) {
197  $relativePath = mb_substr($relativePath, mb_strlen('typo3/sysext/' . $systemExtensionKey . '/'));
198  if (str_starts_with($relativeFileName, $relativePath)) {
199  $relativeFileName = mb_substr($relativeFileName, mb_strlen($relativePath));
200  }
201  return $namespace;
202  }
203  if (str_starts_with($relativeFileName, $relativePath)) {
204  return $namespace;
205  }
206  }
207  }
208  }
209  return '';
210  }
211 
212  protected function ‪createFinder(): Finder
213  {
214  return (new Finder())
215  ->files()
216  ->in(
217  dirs: [
218  __DIR__ . '/../../typo3/sysext/*/Classes',
219  __DIR__ . '/../../typo3/sysext/*/Tests/Unit',
220  __DIR__ . '/../../typo3/sysext/*/Tests/UnitDeprecated',
221  __DIR__ . '/../../typo3/sysext/*/Tests/Functional',
222  __DIR__ . '/../../typo3/sysext/*/Tests/FunctionalDeprecated',
223  __DIR__ . '/../../typo3/sysext/core/Tests/Acceptance',
224  ]
225  )
226  ->notPath(patterns: [
227  'typo3/sysext/core/Tests/Acceptance/Support/_generated',
228  // exclude some files not providing classes, so no namespace information is available
229  'typo3/sysext/*/Configuration',
230  ])
231  ->notName('ext_emconf.php')
232  // this test extension tests missing autoload infos, so of course it will break the integrity check
233  ->exclude('Core/Fixtures/test_extension')
234  ->name('*.php')
235  ->sortByName();
236  }
237 }
238 
242 class ‪NamespaceValidationVisitor extends NodeVisitorAbstract
243 {
244  private string ‪$type = '';
245  private string ‪$fullQualifiedObjectNamespace = '';
246 
247  public function ‪enterNode(Node $node)
248  {
249  if ($this->type === '') {
250  if ($node instanceof Node\Stmt\Class_
251  && !$node->isAnonymous()
252  ) {
253  $this->type = 'class';
254  $this->fullQualifiedObjectNamespace = (string)$node->namespacedName;
255  }
256  if ($node instanceof Node\Stmt\Interface_) {
257  $this->type = 'interface';
258  $this->fullQualifiedObjectNamespace = (string)$node->namespacedName;
259  }
260  if ($node instanceof Node\Stmt\Enum_) {
261  $this->type = 'enum';
262  $this->fullQualifiedObjectNamespace = (string)$node->namespacedName;
263  }
264  if ($node instanceof Node\Stmt\Trait_) {
265  $this->type = 'trait';
266  $this->fullQualifiedObjectNamespace = (string)$node->namespacedName;
267  }
268  }
269  }
270 
271  public function ‪getType(): string
272  {
273  return ‪$this->type;
274  }
275 
276  public function ‪getFullQualifiedObjectNamespace(): string
277  {
279  }
280 }
281 
282 // execute scan and return corresponding exit code.
283 // 0: everything ok
284 // 1: failed, one or more files has invalid namespace declaration
285 exit((new ‪CheckNamespaceIntegrity())->scan());
‪$parser
‪$parser
Definition: annotationChecker.php:108
‪NamespaceValidationVisitor
Definition: checkNamespaceIntegrity.php:243
‪CheckNamespaceIntegrity\scan
‪scan()
Definition: checkNamespaceIntegrity.php:36
‪NamespaceValidationVisitor\enterNode
‪enterNode(Node $node)
Definition: checkNamespaceIntegrity.php:247
‪NamespaceValidationVisitor\getFullQualifiedObjectNamespace
‪getFullQualifiedObjectNamespace()
Definition: checkNamespaceIntegrity.php:276
‪NamespaceValidationVisitor\$fullQualifiedObjectNamespace
‪string $fullQualifiedObjectNamespace
Definition: checkNamespaceIntegrity.php:245
‪NamespaceValidationVisitor\getType
‪getType()
Definition: checkNamespaceIntegrity.php:271
‪CheckNamespaceIntegrity
Definition: checkNamespaceIntegrity.php:35
‪$output
‪$output
Definition: annotationChecker.php:119
‪CheckNamespaceIntegrity\getPSR4NamespaceFromComposerJson
‪getPSR4NamespaceFromComposerJson(string $systemExtensionKey, string $fullComposerJsonFilePath, string &$relativeFileName, bool $autoloadDev=false)
Definition: checkNamespaceIntegrity.php:170
‪CheckNamespaceIntegrity\createFinder
‪createFinder()
Definition: checkNamespaceIntegrity.php:212
‪CheckNamespaceIntegrity\getExtensionTestsNamespaces
‪getExtensionTestsNamespaces(string $systemExtensionKey, string &$relativeFilename)
Definition: checkNamespaceIntegrity.php:158
‪CheckNamespaceIntegrity\getExtensionClassesNamespace
‪getExtensionClassesNamespace(string $systemExtensionKey, string $relativeFilename)
Definition: checkNamespaceIntegrity.php:147
‪NamespaceValidationVisitor\$type
‪string $type
Definition: checkNamespaceIntegrity.php:244
‪CheckNamespaceIntegrity\determineExpectedFullQualifiedNamespace
‪determineExpectedFullQualifiedNamespace(string $systemExtensionKey, string $relativeFilename,)
Definition: checkNamespaceIntegrity.php:118