diff --git a/Build/Scripts/checkNamespaceIntegrity.php b/Build/Scripts/checkNamespaceIntegrity.php new file mode 100755 index 0000000000000000000000000000000000000000..0cea25112165e585fb4fbeac144c08a886de666d --- /dev/null +++ b/Build/Scripts/checkNamespaceIntegrity.php @@ -0,0 +1,287 @@ +#!/usr/bin/env php +<?php +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use PhpParser\Node; +use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor\NameResolver; +use PhpParser\NodeVisitorAbstract; +use PhpParser\ParserFactory; +use Symfony\Component\Finder\Finder; + +if (PHP_SAPI !== 'cli') { + die('Script must be called from command line.' . chr(10)); +} + +require __DIR__ . '/../../vendor/autoload.php'; + +/** + * Class to scan for invalid namespaces. + */ +class CheckNamespaceIntegrity +{ + public function scan(): int + { + $ignoreFiles = [ + // ignored, pure fixture file + 'typo3/sysext/core/Tests/Unit/Configuration/TypoScript/ConditionMatching/Fixtures/ConditionMatcherUserFuncs.php', + // ignored, pure fixture file + 'typo3/sysext/install/Tests/Unit/ExtensionScanner/Php/Matcher/Fixtures/PropertyExistsStaticMatcherFixture.php', + // ignored, pure fixture file + 'typo3/sysext/extbase/Tests/UnitDeprecated/Object/Container/Fixtures/ContainerPropertyInjectionTestClasses.php', + // ignored, pure fixture file + 'typo3/sysext/extbase/Tests/Unit/Object/Container/Fixtures/ContainerConstructorInjectionTestFixtures.php', + ]; + $ignoreNamespaceParts = ['Classes']; + $parser = (new ParserFactory())->create(ParserFactory::ONLY_PHP7); + $files = $this->createFinder(); + $invalidNamespaces = []; + foreach ($files as $file) { + /** @var $file SplFileInfo */ + $fullFilename = $file->getRealPath(); + preg_match('/.*typo3\/sysext\/(.*)$/', $fullFilename, $matches); + $relativeFilenameFromRoot = 'typo3/sysext/' . $matches[1]; + if (in_array($relativeFilenameFromRoot, $ignoreFiles, true)) { + continue; + } + $parts = explode('/', $matches[1]); + $sysExtName = $parts[0]; + unset($parts[0]); + if (in_array($parts[1], $ignoreNamespaceParts, true)) { + unset($parts[1]); + } + + $relativeFilenameWithoutSystemExtensionRoot = substr($relativeFilenameFromRoot, (mb_strlen('typo3/sysext/' . $sysExtName . '/'))); + $expectedFullQualifiedObjectNamespace = $this->determineExpectedFullQualifiedNamespace($sysExtName, $relativeFilenameWithoutSystemExtensionRoot); + $ast = $parser->parse($file->getContents()); + $traverser = new NodeTraverser(); + $visitor = new NameResolver(); + $traverser->addVisitor($visitor); + $visitor = new NamespaceValidationVisitor(); + $traverser->addVisitor($visitor); + $traverser->traverse($ast); + + $fileObjectType = $visitor->getType(); + $fileObjectFullQualifiedObjectNamespace = $visitor->getFullQualifiedObjectNamespace(); + if ($fileObjectType !== '' + && $expectedFullQualifiedObjectNamespace !== $fileObjectFullQualifiedObjectNamespace + ) { + $invalidNamespaces[$sysExtName][] = [ + 'file' => $relativeFilenameFromRoot, + 'shouldBe' => $expectedFullQualifiedObjectNamespace, + 'actualIs' => $fileObjectFullQualifiedObjectNamespace, + ]; + } + } + + $output = new \Symfony\Component\Console\Output\ConsoleOutput(); + $output->writeln(''); + if ($invalidNamespaces !== []) { + $output->writeln(' ⌠Namespace integrity broken.'); + $output->writeln(''); + $table = new \Symfony\Component\Console\Helper\Table($output); + $table->setHeaders([ + 'EXT', + 'File', + 'should be', + 'actual is', + ]); + foreach ($invalidNamespaces as $extKey => $results) { + foreach ($results as $result) { + $table->addRow([ + $extKey, + $result['file'], + $result['shouldBe'] ?: '⌠no proper registered PSR-4 namespace', + $result['actualIs'], + ]); + } + } + $table->render(); + $output->writeln(''); + $output->writeln(''); + return 1; + } + $output->writeln(' ✅ Namespace integrity is in good shape.'); + $output->writeln(''); + return 0; + } + + protected function determineExpectedFullQualifiedNamespace( + string $systemExtensionKey, + string $relativeFilename + ): string { + $namespace = ''; + if (str_starts_with($relativeFilename, 'Classes/')) { + $namespace = $this->getExtensionClassesNamespace($systemExtensionKey, $relativeFilename); + } elseif (str_starts_with($relativeFilename, 'Tests/')) { + $namespace = $this->getExtensionTestsNamespaces($systemExtensionKey, $relativeFilename); + } + $ignorePartValues= ['Classes', 'Tests']; + if ($namespace !== '') { + $parts = explode('/', $relativeFilename); + if (in_array($parts[0], $ignorePartValues, true)) { + unset($parts[0]); + } + foreach ($parts as $part) { + if (str_ends_with($part, '.php')) { + $namespace .= mb_substr($part, 0, -4); + break; + } + $namespace .= $part . '\\'; + } + } + return $namespace; + } + + protected function getExtensionClassesNamespace( + string $systemExtensionKey, + string $relativeFilename + ): string { + return $this->getPSR4NamespaceFromComposerJson( + $systemExtensionKey, + __DIR__ . '/../../typo3/sysext/' . $systemExtensionKey . '/composer.json', + $relativeFilename + ); + } + + protected function getExtensionTestsNamespaces( + string $systemExtensionKey, + string $relativeFilename + ): string { + return $this->getPSR4NamespaceFromComposerJson( + $systemExtensionKey, + __DIR__ . '/../../composer.json', + $relativeFilename, + true + ); + } + + protected function getPSR4NamespaceFromComposerJson( + string $systemExtensionKey, + string $fullComposerJsonFilePath, + string $relativeFileName, + bool $autoloadDev=false + ): string { + $autoloadKey = 'autoload'; + if ($autoloadDev) { + $autoloadKey .= '-dev'; + } + if (file_exists($fullComposerJsonFilePath)) { + $composerInfo = \json_decode( + file_get_contents($fullComposerJsonFilePath), + true + ); + if (is_array($composerInfo)) { + $autoloadPSR4 = $composerInfo[$autoloadKey]['psr-4'] ?? []; + + $pathBasedAutoloadInformation = []; + foreach ($autoloadPSR4 as $namespace => $relativePath) { + $pathBasedAutoloadInformation[trim($relativePath, '/') . '/'] = $namespace; + } + $keys = array_map('mb_strlen', array_keys($pathBasedAutoloadInformation)); + array_multisort($keys, SORT_DESC, $pathBasedAutoloadInformation); + + foreach ($pathBasedAutoloadInformation as $relativePath => $namespace) { + if ($autoloadDev && str_starts_with('typo3/sysext/' . $systemExtensionKey . '/' . $relativeFileName, $relativePath)) { + return $namespace; + } + if (str_starts_with($relativeFileName, $relativePath)) { + return $namespace; + } + } + } + } + return ''; + } + + protected function createFinder(): Finder + { + return (new Finder()) + ->files() + ->in( + [ + __DIR__ . '/../../typo3/sysext/*/Classes', + __DIR__ . '/../../typo3/sysext/*/Tests/Unit', + __DIR__ . '/../../typo3/sysext/*/Tests/UnitDeprecated', + __DIR__ . '/../../typo3/sysext/*/Tests/Functional', + __DIR__ . '/../../typo3/sysext/*/Tests/FunctionalDeprecated', + __DIR__ . '/../../typo3/sysext/core/Tests/Acceptance', + ] + ) + ->notPath([ + 'typo3/sysext/core/Tests/Acceptance/Support/_generated', + ]) + // @todo remove fixture extensions exclude and handle properly after fixture extensions has been streamlined + ->notPath([ + 'Fixtures/Extensions', + 'Fixtures/Extension', + 'Fixture/Extensions', + 'Fixture/Extension', + 'Core/Fixtures/test_extension', + 'Fixtures/testclasses', + ]) + ->name('*.php') + ->sortByName(); + } +} + +/** + * nikic/php-parser node visitor fo find namespace information + */ +class NamespaceValidationVisitor extends NodeVisitorAbstract +{ + private string $type = ''; + private string $fullQualifiedObjectNamespace = ''; + + public function enterNode(Node $node) + { + if ($this->type === '') { + if ($node instanceof Node\Stmt\Class_ + && !$node->isAnonymous() + ) { + $this->type = 'class'; + $this->fullQualifiedObjectNamespace = (string)$node->namespacedName; + } + if ($node instanceof Node\Stmt\Interface_) { + $this->type = 'interface'; + $this->fullQualifiedObjectNamespace = (string)$node->namespacedName; + } + if ($node instanceof Node\Stmt\Enum_) { + $this->type = 'enum'; + $this->fullQualifiedObjectNamespace = (string)$node->namespacedName; + } + if ($node instanceof Node\Stmt\Trait_) { + $this->type = 'trait'; + $this->fullQualifiedObjectNamespace = (string)$node->namespacedName; + } + } + } + + public function getType(): string + { + return $this->type; + } + + public function getFullQualifiedObjectNamespace(): string + { + return $this->fullQualifiedObjectNamespace; + } +} + +// execute scan and return corresponding exit code. +// 0: everything ok +// 1: failed, one or more files has invalid namespace declaration +exit((new CheckNamespaceIntegrity())->scan()); diff --git a/Build/Scripts/runTests.sh b/Build/Scripts/runTests.sh index 3bc6f2bdae21e7b300a4ad2359482ddb36721722..cee04601af504b102c66f5a1a2155a97a6481b6d 100755 --- a/Build/Scripts/runTests.sh +++ b/Build/Scripts/runTests.sh @@ -152,6 +152,7 @@ Options: - checkFilePathLength: test core file paths do not exceed maximum length - checkGitSubmodule: test core git has no sub modules defined - checkGruntClean: Verify "grunt build" is clean. Warning: Executes git commands! Usually used in CI only. + - checkNamespaceIntegrity: Verify namespace integrity in class and test code files are in good shape. - checkPermissions: test some core files for correct executable bits - checkRst: test .rst files for integrity - checkTestMethodsPrefix: check tests methods do not start with "test" @@ -650,6 +651,12 @@ case ${TEST_SUITE} in SUITE_EXIT_CODE=$? docker-compose down ;; + checkNamespaceIntegrity) + setUpDockerComposeDotEnv + docker-compose run check_namespace_integrity + SUITE_EXIT_CODE=$? + docker-compose down + ;; checkPermissions) setUpDockerComposeDotEnv docker-compose run check_permissions diff --git a/Build/gitlab-ci/nightly/integrity.yml b/Build/gitlab-ci/nightly/integrity.yml index a0ffdeddabd52ab400b7640c8f00d2f426e80651..8f9ac0294e9ca7411629677564260b431190c937 100644 --- a/Build/gitlab-ci/nightly/integrity.yml +++ b/Build/gitlab-ci/nightly/integrity.yml @@ -52,6 +52,7 @@ integration various: - Build/Scripts/runTests.sh -s checkBom -p 7.4 - Build/Scripts/runTests.sh -s checkComposer -p 7.4 - Build/Scripts/runTests.sh -s checkTestMethodsPrefix -p 7.4 + - Build/Scripts/runTests.sh -s checkNamespaceIntegrity -p 7.4 lint php 7.4: stage: integrity diff --git a/Build/gitlab-ci/pre-merge/integrity.yml b/Build/gitlab-ci/pre-merge/integrity.yml index a05aef2e51b7effb3acec07bfe63c8563a6bd0b4..98809b72d4b22844ab235122af39005695b04a23 100644 --- a/Build/gitlab-ci/pre-merge/integrity.yml +++ b/Build/gitlab-ci/pre-merge/integrity.yml @@ -56,6 +56,7 @@ integration various pre-merge: - Build/Scripts/runTests.sh -s checkBom -p 7.4 - Build/Scripts/runTests.sh -s checkComposer -p 7.4 - Build/Scripts/runTests.sh -s checkTestMethodsPrefix -p 7.4 + - Build/Scripts/runTests.sh -s checkNamespaceIntegrity -p 7.4 lint scss ts html pre-merge: stage: main diff --git a/Build/testing-docker/local/docker-compose.yml b/Build/testing-docker/local/docker-compose.yml index 252a5c83b700adf23ae250aa04bb9571db1aeb06..06db4b186b9953f5f1610cc3b3910c3ab223047f 100644 --- a/Build/testing-docker/local/docker-compose.yml +++ b/Build/testing-docker/local/docker-compose.yml @@ -611,6 +611,20 @@ services: git status | grep -q \"nothing to commit, working tree clean\" " + check_namespace_integrity: + image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest + user: "${HOST_UID}" + volumes: + - ${CORE_ROOT}:${CORE_ROOT} + working_dir: ${CORE_ROOT} + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + php -dxdebug.mode=off Build/Scripts/checkNamespaceIntegrity.php; + " + check_permissions: image: typo3/core-testing-${DOCKER_PHP_IMAGE}:latest user: "${HOST_UID}" diff --git a/typo3/sysext/core/Tests/UnitDeprecated/Database/SoftReferenceIndexTest.php b/typo3/sysext/core/Tests/UnitDeprecated/Database/SoftReferenceIndexTest.php index d7a041769958a3927255529fd0b414661291c01c..4882676d13e0cc10c102828950d00d6343f692a2 100644 --- a/typo3/sysext/core/Tests/UnitDeprecated/Database/SoftReferenceIndexTest.php +++ b/typo3/sysext/core/Tests/UnitDeprecated/Database/SoftReferenceIndexTest.php @@ -15,7 +15,7 @@ declare(strict_types=1); * The TYPO3 project - inspiring people to share! */ -namespace TYPO3\CMS\Core\Tests\Unit\Database; +namespace TYPO3\CMS\Core\Tests\UnitDeprecated\Database; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait;