‪TYPO3CMS  11.5
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 Symfony\Component\Finder\Finder;
25 
26 if (PHP_SAPI !== 'cli') {
27  die('Script must be called from command line.' . chr(10));
28 }
29 
30 require __DIR__ . '/../../vendor/autoload.php';
31 
36 {
37  public function ‪scan(): int
38  {
39  $ignoreFiles = [
40  // ignored, pure fixture file
41  'typo3/sysext/core/Tests/Unit/Configuration/TypoScript/ConditionMatching/Fixtures/ConditionMatcherUserFuncs.php',
42  // ignored, pure fixture file
43  'typo3/sysext/install/Tests/Unit/ExtensionScanner/Php/Matcher/Fixtures/PropertyExistsStaticMatcherFixture.php',
44  // ignored, pure fixture file
45  'typo3/sysext/extbase/Tests/UnitDeprecated/Object/Container/Fixtures/ContainerPropertyInjectionTestClasses.php',
46  // ignored, pure fixture file
47  'typo3/sysext/extbase/Tests/Unit/Object/Container/Fixtures/ContainerConstructorInjectionTestFixtures.php',
48  ];
49  $ignoreNamespaceParts = ['Classes'];
50  ‪$parser = (new ParserFactory())->create(ParserFactory::ONLY_PHP7);
51  $files = $this->‪createFinder();
52  $invalidNamespaces = [];
53  foreach ($files as $file) {
55  $fullFilename = $file->getRealPath();
56  preg_match('/.*typo3\/sysext\/(.*)$/', $fullFilename, $matches);
57  $relativeFilenameFromRoot = 'typo3/sysext/' . $matches[1];
58  if (in_array($relativeFilenameFromRoot, $ignoreFiles, true)) {
59  continue;
60  }
61  $parts = explode('/', $matches[1]);
62  $sysExtName = $parts[0];
63  unset($parts[0]);
64  if (in_array($parts[1], $ignoreNamespaceParts, true)) {
65  unset($parts[1]);
66  }
67 
68  $relativeFilenameWithoutSystemExtensionRoot = substr($relativeFilenameFromRoot, (mb_strlen('typo3/sysext/' . $sysExtName . '/')));
69  $expectedFullQualifiedObjectNamespace = $this->‪determineExpectedFullQualifiedNamespace($sysExtName, $relativeFilenameWithoutSystemExtensionRoot);
70  $ast = ‪$parser->parse($file->getContents());
71  $traverser = new NodeTraverser();
72  $visitor = new NameResolver();
73  $traverser->addVisitor($visitor);
74  $visitor = new ‪NamespaceValidationVisitor();
75  $traverser->addVisitor($visitor);
76  $traverser->traverse($ast);
77 
78  $fileObjectType = $visitor->getType();
79  $fileObjectFullQualifiedObjectNamespace = $visitor->getFullQualifiedObjectNamespace();
80  if ($fileObjectType !== ''
81  && $expectedFullQualifiedObjectNamespace !== $fileObjectFullQualifiedObjectNamespace
82  ) {
83  $invalidNamespaces[$sysExtName][] = [
84  'file' => $relativeFilenameFromRoot,
85  'shouldBe' => $expectedFullQualifiedObjectNamespace,
86  'actualIs' => $fileObjectFullQualifiedObjectNamespace,
87  ];
88  }
89  }
90 
91  ‪$output = new \Symfony\Component\Console\Output\ConsoleOutput();
92  ‪$output->writeln('');
93  if ($invalidNamespaces !== []) {
94  ‪$output->writeln(' ❌ Namespace integrity broken.');
95  ‪$output->writeln('');
96  $table = new \Symfony\Component\Console\Helper\Table(‪$output);
97  $table->setHeaders([
98  'EXT',
99  'File',
100  'should be',
101  'actual is',
102  ]);
103  foreach ($invalidNamespaces as $extKey => $results) {
104  foreach ($results as $result) {
105  $table->addRow([
106  $extKey,
107  $result['file'],
108  $result['shouldBe'] ?: '❌ no proper registered PSR-4 namespace',
109  $result['actualIs'],
110  ]);
111  }
112  }
113  $table->render();
114  ‪$output->writeln('');
115  ‪$output->writeln('');
116  return 1;
117  }
118  ‪$output->writeln(' ✅ Namespace integrity is in good shape.');
119  ‪$output->writeln('');
120  return 0;
121  }
122 
124  string $systemExtensionKey,
125  string $relativeFilename
126  ): string {
127  $namespace = '';
128  if (str_starts_with($relativeFilename, 'Classes/')) {
129  $namespace = $this->‪getExtensionClassesNamespace($systemExtensionKey, $relativeFilename);
130  } elseif (str_starts_with($relativeFilename, 'Tests/')) {
131  $namespace = $this->‪getExtensionTestsNamespaces($systemExtensionKey, $relativeFilename);
132  }
133  $ignorePartValues = ['Classes', 'Tests'];
134  if ($namespace !== '') {
135  $parts = explode('/', $relativeFilename);
136  if (in_array($parts[0], $ignorePartValues, true)) {
137  unset($parts[0]);
138  }
139  foreach ($parts as $part) {
140  if (str_ends_with($part, '.php')) {
141  $namespace .= mb_substr($part, 0, -4);
142  break;
143  }
144  $namespace .= $part . '\\';
145  }
146  }
147  return $namespace;
148  }
149 
151  string $systemExtensionKey,
152  string $relativeFilename
153  ): string {
155  $systemExtensionKey,
156  __DIR__ . '/../../typo3/sysext/' . $systemExtensionKey . '/composer.json',
157  $relativeFilename
158  );
159  }
160 
161  protected function ‪getExtensionTestsNamespaces(
162  string $systemExtensionKey,
163  string $relativeFilename
164  ): string {
166  $systemExtensionKey,
167  __DIR__ . '/../../composer.json',
168  $relativeFilename,
169  true
170  );
171  }
172 
174  string $systemExtensionKey,
175  string $fullComposerJsonFilePath,
176  string $relativeFileName,
177  bool $autoloadDev = false
178  ): string {
179  $autoloadKey = 'autoload';
180  if ($autoloadDev) {
181  $autoloadKey .= '-dev';
182  }
183  if (file_exists($fullComposerJsonFilePath)) {
184  $composerInfo = \json_decode(
185  file_get_contents($fullComposerJsonFilePath),
186  true
187  );
188  if (is_array($composerInfo)) {
189  $autoloadPSR4 = $composerInfo[$autoloadKey]['psr-4'] ?? [];
190 
191  $pathBasedAutoloadInformation = [];
192  foreach ($autoloadPSR4 as $namespace => $relativePath) {
193  $pathBasedAutoloadInformation[trim($relativePath, '/') . '/'] = $namespace;
194  }
195  $keys = array_map('mb_strlen', array_keys($pathBasedAutoloadInformation));
196  array_multisort($keys, SORT_DESC, $pathBasedAutoloadInformation);
197 
198  foreach ($pathBasedAutoloadInformation as $relativePath => $namespace) {
199  if ($autoloadDev && str_starts_with('typo3/sysext/' . $systemExtensionKey . '/' . $relativeFileName, $relativePath)) {
200  return $namespace;
201  }
202  if (str_starts_with($relativeFileName, $relativePath)) {
203  return $namespace;
204  }
205  }
206  }
207  }
208  return '';
209  }
210 
211  protected function ‪createFinder(): Finder
212  {
213  return (new Finder())
214  ->files()
215  ->in(
216  [
217  __DIR__ . '/../../typo3/sysext/*/Classes',
218  __DIR__ . '/../../typo3/sysext/*/Tests/Unit',
219  __DIR__ . '/../../typo3/sysext/*/Tests/UnitDeprecated',
220  __DIR__ . '/../../typo3/sysext/*/Tests/Functional',
221  __DIR__ . '/../../typo3/sysext/*/Tests/FunctionalDeprecated',
222  __DIR__ . '/../../typo3/sysext/core/Tests/Acceptance',
223  ]
224  )
225  ->notPath([
226  'typo3/sysext/core/Tests/Acceptance/Support/_generated',
227  // exclude some files not providing classes, so no namespace information is available
228  'typo3/sysext/*/Configuration',
229  ])
230  // @todo remove fixture extensions exclude and handle properly after fixture extensions has been streamlined
231  ->notPath([
232  'Fixtures/Extensions',
233  'Fixtures/Extension',
234  'Fixture/Extensions',
235  'Fixture/Extension',
236  'Core/Fixtures/test_extension',
237  'Fixtures/testclasses',
238  ])
239  ->notName('ext_emconf.php')
240  // this test extension tests missing autoload infos, so of course it will break the integrity check
241  ->exclude('Core/Fixtures/test_extension')
242  ->name('*.php')
243  ->sortByName();
244  }
245 }
246 
250 class ‪NamespaceValidationVisitor extends NodeVisitorAbstract
251 {
252  private string ‪$type = '';
253  private string ‪$fullQualifiedObjectNamespace = '';
254 
255  public function ‪enterNode(Node $node)
256  {
257  if ($this->type === '') {
258  if ($node instanceof Node\Stmt\Class_
259  && !$node->isAnonymous()
260  ) {
261  $this->type = 'class';
262  $this->fullQualifiedObjectNamespace = (string)$node->namespacedName;
263  }
264  if ($node instanceof Node\Stmt\Interface_) {
265  $this->type = 'interface';
266  $this->fullQualifiedObjectNamespace = (string)$node->namespacedName;
267  }
268  if ($node instanceof Node\Stmt\Enum_) {
269  $this->type = 'enum';
270  $this->fullQualifiedObjectNamespace = (string)$node->namespacedName;
271  }
272  if ($node instanceof Node\Stmt\Trait_) {
273  $this->type = 'trait';
274  $this->fullQualifiedObjectNamespace = (string)$node->namespacedName;
275  }
276  }
277  }
278 
279  public function ‪getType(): string
280  {
281  return ‪$this->type;
282  }
283 
284  public function ‪getFullQualifiedObjectNamespace(): string
285  {
287  }
288 }
289 
290 // execute scan and return corresponding exit code.
291 // 0: everything ok
292 // 1: failed, one or more files has invalid namespace declaration
293 exit((new ‪CheckNamespaceIntegrity())->scan());
‪CheckNamespaceIntegrity\getExtensionTestsNamespaces
‪getExtensionTestsNamespaces(string $systemExtensionKey, string $relativeFilename)
Definition: checkNamespaceIntegrity.php:161
‪$parser
‪$parser
Definition: annotationChecker.php:111
‪NamespaceValidationVisitor
Definition: checkNamespaceIntegrity.php:251
‪CheckNamespaceIntegrity\scan
‪scan()
Definition: checkNamespaceIntegrity.php:37
‪CheckNamespaceIntegrity\determineExpectedFullQualifiedNamespace
‪determineExpectedFullQualifiedNamespace(string $systemExtensionKey, string $relativeFilename)
Definition: checkNamespaceIntegrity.php:123
‪NamespaceValidationVisitor\enterNode
‪enterNode(Node $node)
Definition: checkNamespaceIntegrity.php:255
‪NamespaceValidationVisitor\getFullQualifiedObjectNamespace
‪getFullQualifiedObjectNamespace()
Definition: checkNamespaceIntegrity.php:284
‪NamespaceValidationVisitor\$fullQualifiedObjectNamespace
‪string $fullQualifiedObjectNamespace
Definition: checkNamespaceIntegrity.php:253
‪NamespaceValidationVisitor\getType
‪getType()
Definition: checkNamespaceIntegrity.php:279
‪CheckNamespaceIntegrity
Definition: checkNamespaceIntegrity.php:36
‪CheckNamespaceIntegrity\getPSR4NamespaceFromComposerJson
‪getPSR4NamespaceFromComposerJson(string $systemExtensionKey, string $fullComposerJsonFilePath, string $relativeFileName, bool $autoloadDev=false)
Definition: checkNamespaceIntegrity.php:173
‪$output
‪$output
Definition: annotationChecker.php:122
‪CheckNamespaceIntegrity\createFinder
‪createFinder()
Definition: checkNamespaceIntegrity.php:211
‪CheckNamespaceIntegrity\getExtensionClassesNamespace
‪getExtensionClassesNamespace(string $systemExtensionKey, string $relativeFilename)
Definition: checkNamespaceIntegrity.php:150
‪NamespaceValidationVisitor\$type
‪string $type
Definition: checkNamespaceIntegrity.php:252