diff --git a/Build/Scripts/checkNamespaceIntegrity.php b/Build/Scripts/checkNamespaceIntegrity.php deleted file mode 100755 index a38086cc09d984ad933561b23e58edf09f391e95..0000000000000000000000000000000000000000 --- a/Build/Scripts/checkNamespaceIntegrity.php +++ /dev/null @@ -1,287 +0,0 @@ -#!/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 PhpParser\PhpVersion; -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', - ]; - $ignoreNamespaceParts = ['Classes']; - $parser = (new ParserFactory())->createForVersion(PhpVersion::fromComponents(8, 2)); - $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/')) { - // for test fixture extensions, the relativeFileName will be shortened by the sysext file path, - // therefor the variable gets passed as reference here - $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)) { - $relativePath = mb_substr($relativePath, mb_strlen('typo3/sysext/' . $systemExtensionKey . '/')); - if (str_starts_with($relativeFileName, $relativePath)) { - $relativeFileName = mb_substr($relativeFileName, mb_strlen($relativePath)); - } - return $namespace; - } - if (str_starts_with($relativeFileName, $relativePath)) { - return $namespace; - } - } - } - } - return ''; - } - - protected function createFinder(): Finder - { - return (new Finder()) - ->files() - ->in( - dirs: [ - __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(patterns: [ - 'typo3/sysext/core/Tests/Acceptance/Support/_generated', - // exclude some files not providing classes, so no namespace information is available - 'typo3/sysext/*/Configuration', - ]) - ->notName('ext_emconf.php') - // this test extension tests missing autoload infos, so of course it will break the integrity check - ->exclude('Core/Fixtures/test_extension') - ->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/phpIntegrityChecker.php b/Build/Scripts/phpIntegrityChecker.php new file mode 100755 index 0000000000000000000000000000000000000000..c438bec3ef63dcb96e94a16509162ec50942a329 --- /dev/null +++ b/Build/Scripts/phpIntegrityChecker.php @@ -0,0 +1,217 @@ +#!/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 Composer\Console\Application; +use Composer\Console\Input\InputOption; +use PhpParser\Error; +use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor\NameResolver; +use PhpParser\Parser; +use PhpParser\ParserFactory; +use PhpParser\PhpVersion; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Finder\Finder; +use TYPO3\CMS\PhpIntegrityChecks\AbstractPhpIntegrityChecker; +use TYPO3\CMS\PhpIntegrityChecks\AnnotationChecker; +use TYPO3\CMS\PhpIntegrityChecks\NamespaceChecker; +use TYPO3\CMS\PhpIntegrityChecks\TestClassFinalChecker; +use TYPO3\CMS\PhpIntegrityChecks\TestMethodPrefixChecker; + +require_once __DIR__ . '/../../vendor/autoload.php'; + +final class PhpIntegrityChecker extends Command +{ + /** + * @var class-string[] + */ + private array $registeredVisitors = [ + AnnotationChecker::class, + NamespaceChecker::class, + TestMethodPrefixChecker::class, + TestClassFinalChecker::class, + ]; + + /** + * @var string[] + */ + private array $finderFindIn = [ + __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', + ]; + + /** + * @var string[] + */ + private array $finderNotPath = [ + 'typo3/sysext/core/Tests/Acceptance/Support/_generated', + // exclude some files not providing classes + 'typo3/sysext/*/Configuration', + ]; + + /** + * @var string[] + */ + private array $finderNotName = [ + 'ext_emconf.php', + ]; + + /** + * @var array<class-string, AbstractPhpIntegrityChecker> + */ + private array $visitors = []; + + /** + * @var array<class-string, array<string, string[]>> + */ + private array $issues = []; + + protected function configure(): void + { + $this->addOption('php', 'p', InputOption::VALUE_OPTIONAL, 'the php version to use, like 8.2 or 7.4', '8.2'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->initVisitors(); + $io = new SymfonyStyle($input, $output); + $parser = $this->getParser($input->getOption('php')); + + foreach ($this->createFinder() as $file) { + $this->processFile($parser, $file); + } + + return $this->displayIssues($io); + } + + /** + * Display issues, grouped by visitor and return command exit code. + */ + private function displayIssues(SymfonyStyle $io): int + { + $exitCode = Command::SUCCESS; + foreach ($this->issues as $visitorClassName => $issueCollection) { + if ($issueCollection !== []) { + $exitCode = Command::FAILURE; + } + // PHP Syntax parsing errors are not a visitor, and thus need adjusted handling here. + if ($visitorClassName === 'parsing') { + $io->title('Parsing errors'); + $io->error(' Following files were not checked:'); + foreach ($issueCollection as $file => $issue) { + $io->writeln(' > ' . $file . ': ' . $issue); + } + continue; + } + $this->visitors[$visitorClassName]->outputResult($io, $issueCollection); + $io->newLine(); + } + + return $exitCode; + } + + /** + * Process code integrity checks for specified file. + */ + private function processFile(Parser $parser, SplFileInfo $file): void + { + try { + $ast = $parser->parse($file->getContents()); + } catch (Error $error) { + $this->issues['parsing'][$file->getRealPath()] = 'Parse error: ' . $error->getMessage(); + return; + } + + /** @var array<class-string, AbstractPhpIntegrityChecker> $usedVisitors */ + $usedVisitors = []; + $this->createTraverser($file, $usedVisitors)->traverse($ast); + $this->finishUsedVisitorsForFile($file, $usedVisitors); + } + + /** + * @param SplFileInfo $file + * @param array<class-string, AbstractPhpIntegrityChecker> &$usedVisitors + * @return NodeTraverser + */ + private function createTraverser(SplFileInfo $file, array &$usedVisitors): NodeTraverser + { + $nameResolver = new NameResolver(); + $traverser = new NodeTraverser(); + $traverser->addVisitor($nameResolver); + foreach ($this->visitors as $visitorClassName => $visitor) { + if (!$visitor->canHandle($file)) { + continue; + } + $usedVisitors[$visitorClassName] = $visitor; + $visitor->startProcessing($file); + $traverser->addVisitor($visitor); + } + return $traverser; + } + + /** + * @param array<class-string, AbstractPhpIntegrityChecker> $usedVisitors + */ + private function finishUsedVisitorsForFile(SplFileInfo $file, array $usedVisitors): void + { + foreach ($usedVisitors as $visitorClassName => $visitor) { + $visitor->finishProcessing(); + $messages = $visitor->getMessages(); + if ($messages !== []) { + $this->issues[$visitorClassName] = $messages; + } + } + } + + private function createFinder(): Finder + { + return (new Finder()) + ->files() + ->in($this->finderFindIn) + ->notPath($this->finderNotPath) + ->notName($this->finderNotName) + ->name('*.php') + ->sortByName(); + } + + private function initVisitors(): void + { + foreach ($this->registeredVisitors as $registeredVisitor) { + $this->visitors[$registeredVisitor] = new $registeredVisitor(); + $this->issues[$registeredVisitor] = []; + } + } + + private function getParser(string $phpVersion): Parser + { + $version = explode('.', $phpVersion); + return (new ParserFactory())->createForVersion(PhpVersion::fromComponents((int)$version[0], (int)$version[1])); + } +} + +$application = new Application('Integrity Check'); +$name = 'integrity_checker'; +$application->add(new PhpIntegrityChecker($name)); +$application->setDefaultCommand($name, true); +$application->run(); diff --git a/Build/Scripts/phpIntegrityChecks/AbstractPhpIntegrityChecker.php b/Build/Scripts/phpIntegrityChecks/AbstractPhpIntegrityChecker.php new file mode 100644 index 0000000000000000000000000000000000000000..1ab537b77fa0c48308588f42c89281563e3cbf64 --- /dev/null +++ b/Build/Scripts/phpIntegrityChecks/AbstractPhpIntegrityChecker.php @@ -0,0 +1,91 @@ +<?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! + */ + +namespace TYPO3\CMS\PhpIntegrityChecks; + +use PhpParser\NodeVisitorAbstract; +use Symfony\Component\Console\Style\SymfonyStyle; + +abstract class AbstractPhpIntegrityChecker extends NodeVisitorAbstract +{ + /** + * @var array<int|string, mixed> + */ + protected array $messages = []; + + /** + * @var string[] + */ + protected array $excludedDirectories = []; + + /** + * @var string[] + */ + protected array $excludedFileNames = []; + + protected \SplFileInfo $file; + + public function canHandle(\SplFileInfo $file): bool + { + if (in_array($file->getFilename(), $this->excludedFileNames, true)) { + return false; + } + foreach ($this->excludedDirectories as $path) { + if (str_starts_with($this->removeRootPathFromPath($file->getRealPath()), $path)) { + return false; + } + } + return true; + } + + public function startProcessing(\SplFileInfo $file): void + { + $this->file = $file; + $this->messages = []; + } + + public function finishProcessing(): void + { + // override in concrete classes, if needed + } + + public function getMessages(): array + { + return $this->messages; + } + + protected function getRelativeFileNameFromRepositoryRoot(): string + { + return $this->removeRootPathFromPath($this->file->getRealPath()); + } + + protected function removeRootPathFromPath(string $path): string + { + $rootPath = rtrim($this->getRootPath(), '/') . '/'; + return str_starts_with($path, $rootPath) + ? mb_substr($path, mb_strlen($rootPath)) + : $path; + } + + protected function getRootPath(): string + { + return dirname(__FILE__, 4); + } + + abstract public function outputResult(SymfonyStyle $io, array $issueCollection): void; + +} diff --git a/Build/Scripts/annotationChecker.php b/Build/Scripts/phpIntegrityChecks/AnnotationChecker.php old mode 100755 new mode 100644 similarity index 66% rename from Build/Scripts/annotationChecker.php rename to Build/Scripts/phpIntegrityChecks/AnnotationChecker.php index 6e4ef6131aad7ebb9dcf86903adbf70e27bf9d3e..35453c1ad730196c77da3265d7a7b5d7ea3d2eeb --- a/Build/Scripts/annotationChecker.php +++ b/Build/Scripts/phpIntegrityChecks/AnnotationChecker.php @@ -1,4 +1,3 @@ -#!/usr/bin/env php <?php declare(strict_types=1); @@ -16,22 +15,25 @@ declare(strict_types=1); * The TYPO3 project - inspiring people to share! */ +namespace TYPO3\CMS\PhpIntegrityChecks; + use PhpParser\Comment\Doc; -use PhpParser\Error; use PhpParser\Node; -use PhpParser\NodeTraverser; -use PhpParser\NodeVisitorAbstract; -use PhpParser\ParserFactory; -use PhpParser\PhpVersion; -use Symfony\Component\Console\Output\ConsoleOutput; - -require_once __DIR__ . '/../../vendor/autoload.php'; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Style\SymfonyStyle; -class NodeVisitor extends NodeVisitorAbstract +/** + * Check for allowed annotations in classes, report on any not whitelisted + */ +final class AnnotationChecker extends AbstractPhpIntegrityChecker { - public array $matches = []; + // black list some unit test fixture files from extension scanner that test matchers of old annotations + protected array $excludedFileNames = [ + 'MethodAnnotationMatcherFixture.php', + 'PropertyAnnotationMatcherFixture.php', + ]; - public function enterNode(Node $node) + public function enterNode(Node $node): void { switch (get_class($node)) { case Node\Stmt\Class_::class: @@ -88,9 +90,12 @@ class NodeVisitor extends NodeVisitorAbstract $matches ); if (!empty($matches['annotations'])) { - $this->matches[$node->getLine()] = array_map(static function (string $value): string { - return '@' . $value; - }, $matches['annotations']); + $this->messages[$this->getRelativeFileNameFromRepositoryRoot()][$node->getLine()] = array_map( + static function (string $value): string { + return '@' . $value; + }, + $matches['annotations'] + ); } break; @@ -98,64 +103,30 @@ class NodeVisitor extends NodeVisitorAbstract break; } } -} - -$parser = (new ParserFactory())->createForVersion(PhpVersion::fromComponents(8, 2)); - -$finder = new Symfony\Component\Finder\Finder(); -$finder->files() - ->in(__DIR__ . '/../../typo3/') - ->name('/\.php$/') - // black list some unit test fixture files from extension scanner that test matchers of old annotations - ->notName('MethodAnnotationMatcherFixture.php') - ->notName('PropertyAnnotationMatcherFixture.php') -; - -$output = new ConsoleOutput(); - -$errors = []; -foreach ($finder as $file) { - try { - $ast = $parser->parse($file->getContents()); - } catch (Error $error) { - $output->writeln('<error>Parse error: ' . $error->getMessage() . '</error>'); - exit(1); - } - $visitor = new NodeVisitor(); - - $traverser = new NodeTraverser(); - $traverser->addVisitor($visitor); - - $ast = $traverser->traverse($ast); - - if (!empty($visitor->matches)) { - $errors[$file->getRealPath()] = $visitor->matches; - $output->write('<error>F</error>'); - } else { - $output->write('<fg=green>.</>'); - } -} - -$output->writeln(''); - -if (!empty($errors)) { - $output->writeln(''); - - foreach ($errors as $file => $matchesPerLine) { - $output->writeln(''); - $output->writeln('<error>' . $file . '</error>'); - - /** - * @var array $matchesPerLine - * @var int $line - * @var array $matches - */ - foreach ($matchesPerLine as $line => $matches) { - $output->writeln($line . ': ' . implode(', ', $matches)); + public function outputResult(SymfonyStyle $io, array $issueCollection): void + { + $io->title('Annotation checker result'); + if ($issueCollection !== []) { + $io->error('Following annotations are invalid. Remove them:'); + $table = new Table($io); + $table->setHeaders([ + 'File', + 'Line', + 'Annotation(s)', + ]); + foreach ($issueCollection as $file => $issues) { + foreach ($issues as $line => $annotations) { + $table->addRow([ + $file, + $line, + implode(', ', $annotations), + ]); + } + } + $table->render(); + } else { + $io->success('Annotation integrity is in good shape.'); } } - exit(1); } - -exit(0); diff --git a/Build/Scripts/phpIntegrityChecks/NamespaceChecker.php b/Build/Scripts/phpIntegrityChecks/NamespaceChecker.php new file mode 100644 index 0000000000000000000000000000000000000000..d7e582d9cb80e46d0c36adfe042494e7e2c88b00 --- /dev/null +++ b/Build/Scripts/phpIntegrityChecks/NamespaceChecker.php @@ -0,0 +1,255 @@ +<?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! + */ + +namespace TYPO3\CMS\PhpIntegrityChecks; + +use PhpParser\Node; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * check namespaces in classes comply with PSR-4, report on any not doing so + */ +final class NamespaceChecker extends AbstractPhpIntegrityChecker +{ + /** + * @var string[] + */ + protected array $excludedFileNames = [ + 'ConditionMatcherUserFuncs.php', + 'PropertyExistsStaticMatcherFixture.php', + ]; + + /** + * @var string[] + */ + protected array $excludedDirectories = [ + 'typo3/sysext/core/Tests/Unit/Core/Fixtures/test_extension/', + ]; + + protected string $filePathPrefix = 'typo3/sysext/'; + protected string $filePathPrefixRegex = '/.*typo3\/sysext\/(.*)$/'; + + private string $type = ''; + private string $fullQualifiedObjectNamespace = ''; + private string $sysExtName; + private string $expectedFullQualifiedNamespace; + private string $relativeFilenameFromRoot; + + public function startProcessing(\SplFileInfo $file): void + { + parent::startProcessing($file); + $this->type = ''; + $this->fullQualifiedObjectNamespace = ''; + $this->sysExtName = ''; + $this->expectedFullQualifiedNamespace = ''; + $this->relativeFilenameFromRoot = ''; + $ignoreNamespaceParts = ['Classes']; + $fullFilename = $file->getRealPath(); + + preg_match($this->filePathPrefixRegex, $fullFilename, $matches); + $this->relativeFilenameFromRoot = $this->filePathPrefix . $matches[1]; + $parts = explode('/', $matches[1]); + $this->sysExtName = $parts[0]; + unset($parts[0]); + if (in_array($parts[1], $ignoreNamespaceParts, true)) { + unset($parts[1]); + } + + $relativeFilenameWithoutSystemExtensionRoot = substr($this->relativeFilenameFromRoot, (mb_strlen($this->filePathPrefix . $this->sysExtName . '/'))); + $this->expectedFullQualifiedNamespace = $this->determineExpectedFullQualifiedNamespace($this->sysExtName, $relativeFilenameWithoutSystemExtensionRoot); + } + + public function enterNode(Node $node): void + { + if ($this->type !== '') { + return; + } + if ($node instanceof Node\Stmt\Class_ && !$node->isAnonymous()) { + $this->type = 'class'; + $this->fullQualifiedObjectNamespace = (string)$node->namespacedName; + return; + } + if ($node instanceof Node\Stmt\Interface_) { + $this->type = 'interface'; + $this->fullQualifiedObjectNamespace = (string)$node->namespacedName; + return; + } + if ($node instanceof Node\Stmt\Enum_) { + $this->type = 'enum'; + $this->fullQualifiedObjectNamespace = (string)$node->namespacedName; + return; + } + if ($node instanceof Node\Stmt\Trait_) { + $this->type = 'trait'; + $this->fullQualifiedObjectNamespace = (string)$node->namespacedName; + } + } + + public function finishProcessing(): void + { + $fileObjectType = $this->getType(); + $fileObjectFullQualifiedObjectNamespace = $this->getFullQualifiedObjectNamespace(); + if ($fileObjectType !== '' + && $this->expectedFullQualifiedNamespace !== $fileObjectFullQualifiedObjectNamespace + ) { + $this->messages[$this->sysExtName][] = [ + 'file' => $this->relativeFilenameFromRoot, + 'shouldBe' => $this->expectedFullQualifiedNamespace, + 'actualIs' => $fileObjectFullQualifiedObjectNamespace, + ]; + } + } + + public function outputResult(SymfonyStyle $io, array $issueCollection): void + { + $io->title('Namespace Checker result'); + if ($issueCollection !== []) { + $io->error('Namespace integrity broken.'); + $table = new Table($io); + $table->setHeaders([ + 'EXT', + 'File', + 'should be', + 'actual is', + ]); + foreach ($issueCollection as $extKey => $issues) { + foreach ($issues as $result) { + $table->addRow([ + $extKey, + $result['file'], + $result['shouldBe'] ?: 'no proper registered PSR-4 namespace', + $result['actualIs'], + ]); + } + } + $table->render(); + } else { + $io->success(' Namespace integrity is in good shape.'); + } + } + + protected function getType(): string + { + return $this->type; + } + + protected function getFullQualifiedObjectNamespace(): string + { + return $this->fullQualifiedObjectNamespace; + } + + 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/')) { + // for test fixture extensions, the relativeFileName will be shortened by the sysext file path, + // therefor the variable gets passed as reference here + $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__ . '/../../../' . $this->filePathPrefix . $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)) { + return ''; + } + try { + $composerInfo = \json_decode( + json: (string)file_get_contents($fullComposerJsonFilePath), + flags: JSON_OBJECT_AS_ARRAY | JSON_THROW_ON_ERROR, + ); + 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($this->filePathPrefix . $systemExtensionKey . '/' . $relativeFileName, $relativePath)) { + $relativePath = mb_substr($relativePath, mb_strlen($this->filePathPrefix . $systemExtensionKey . '/')); + if (str_starts_with($relativeFileName, $relativePath)) { + $relativeFileName = mb_substr($relativeFileName, mb_strlen($relativePath)); + } + return $namespace; + } + if (str_starts_with($relativeFileName, $relativePath)) { + return $namespace; + } + } + } + } catch (\JsonException) { + } + + return ''; + } +} diff --git a/Build/Scripts/phpIntegrityChecks/TestClassFinalChecker.php b/Build/Scripts/phpIntegrityChecks/TestClassFinalChecker.php new file mode 100644 index 0000000000000000000000000000000000000000..b0e7fe5ce853153898ea11551342ccd11156d2a5 --- /dev/null +++ b/Build/Scripts/phpIntegrityChecks/TestClassFinalChecker.php @@ -0,0 +1,72 @@ +<?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! + */ + +namespace TYPO3\CMS\PhpIntegrityChecks; + +use PhpParser\Node; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * check all test classes based on phpunit are final, report on any that are not + */ +final class TestClassFinalChecker extends AbstractPhpIntegrityChecker +{ + public function canHandle(\SplFileInfo $file): bool + { + if (!str_contains($file->getRealPath(), 'Tests/Unit') && !str_contains($file->getRealPath(), 'Tests/Functional')) { + return false; + } + if (!str_ends_with($file->getBasename(), 'Test.php')) { + return false; + } + return true; + } + + public function enterNode(Node $node): void + { + if (($node instanceof Node\Stmt\Class_) && !$node->isFinal() && !$node->isAnonymous() && !$node->isAbstract()) { + $this->messages[$this->getRelativeFileNameFromRepositoryRoot()][$node->getLine()] = $node->name; + } + } + + public function outputResult(SymfonyStyle $io, array $issueCollection): void + { + $io->title('Final Checker for test classes result'); + if ($issueCollection !== []) { + $io->error('Following test classes should be marked as final:'); + $table = new Table($io); + $table->setHeaders([ + 'File', + 'Line', + 'Class', + ]); + foreach ($issueCollection as $file => $issues) { + foreach ($issues as $line => $issue) { + $table->addRow([ + $file, + $line, + $issue, + ]); + } + } + $table->render(); + } else { + $io->success('Test class \'final\' integrity is in good shape.'); + } + } +} diff --git a/Build/Scripts/phpIntegrityChecks/TestMethodPrefixChecker.php b/Build/Scripts/phpIntegrityChecks/TestMethodPrefixChecker.php new file mode 100644 index 0000000000000000000000000000000000000000..f403a16872e7d796510e70845e9c920c3b3719e0 --- /dev/null +++ b/Build/Scripts/phpIntegrityChecks/TestMethodPrefixChecker.php @@ -0,0 +1,73 @@ +<?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! + */ + +namespace TYPO3\CMS\PhpIntegrityChecks; + +use PhpParser\Node; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * check all test classes based on phpunit contain only test methods with annotation or attribute, but not with the `test` prefix. + * report on any method using a prefix + */ +final class TestMethodPrefixChecker extends AbstractPhpIntegrityChecker +{ + public function canHandle(\SplFileInfo $file): bool + { + if (!str_contains($file->getRealPath(), 'Tests/Unit') && !str_contains($file->getRealPath(), 'Tests/Functional')) { + return false; + } + if (!str_ends_with($file->getBasename(), 'Test.php')) { + return false; + } + return true; + } + + public function enterNode(Node $node): void + { + if (($node instanceof Node\Stmt\ClassMethod) && str_starts_with($node->name->name, 'test')) { + $this->messages[$this->getRelativeFileNameFromRepositoryRoot()][$node->getLine()] = $node->name->name; + } + } + + public function outputResult(SymfonyStyle $io, array $issueCollection): void + { + $io->title('Test Method prefix checker result'); + if ($issueCollection !== []) { + $io->error('Following test methods should not start with "test". Use @test phpDoc annotation or #[Test] attribute instead.'); + $table = new Table($io); + $table->setHeaders([ + 'File', + 'Line', + 'Method', + ]); + foreach ($issueCollection as $file => $issues) { + foreach ($issues as $line => $issue) { + $table->addRow([ + $file, + $line, + $issue, + ]); + } + } + $table->render(); + } else { + $io->success('Test method prefix integrity is in good shape.'); + } + } +} diff --git a/Build/Scripts/runTests.sh b/Build/Scripts/runTests.sh index ab6f1834366b2e4416e1a76a6c313b2194b34d0f..117cee9307b80af8ff5cd12c15e5bcc15d90fdf9 100755 --- a/Build/Scripts/runTests.sh +++ b/Build/Scripts/runTests.sh @@ -191,7 +191,7 @@ Options: - cglGit: test and fix latest committed patch for CGL compliance - cglHeader: test and fix file header for all core php files - cglHeaderGit: test and fix latest committed patch for CGL file header compliance - - checkAnnotations: check php code for allowed annotations + - checkIntegrity: check php code for with registered integrity rules - checkBom: check UTF-8 files do not contain BOM - checkComposer: check composer.json files for version integrity - checkExceptionCodes: test core for duplicate exception codes @@ -200,11 +200,8 @@ Options: - checkGitSubmodule: test core git has no sub modules defined - checkGruntClean: Verify "grunt build" is clean. Warning: Executes git commands! Usually used in CI only. - checkIsoDatabase: Verify "updateIsoDatabase.php" does not change anything. - - 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 - - checkTestClassFinal: check test case classes are final - - checkTestMethodsPrefix: check tests methods do not start with "test" - clean: clean up build, cache and testing related files and folders - cleanBuild: clean up build related files and folders - cleanCache: clean up cache related files and folders @@ -843,16 +840,8 @@ case ${TEST_SUITE} in ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name cgl-header-git-${SUFFIX} ${IMAGE_PHP} Build/Scripts/cglFixMyCommitFileHeader.sh ${CGLCHECK_DRY_RUN} SUITE_EXIT_CODE=$? ;; - checkAnnotations) - ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name check-annotations-${SUFFIX} ${IMAGE_PHP} php -dxdebug.mode=off Build/Scripts/annotationChecker.php - SUITE_EXIT_CODE=$? - ;; - checkTestClassFinal) - ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name check-test-classes-final-${SUFFIX} ${IMAGE_PHP} php -dxdebug.mode=off Build/Scripts/testClassFinalChecker.php - SUITE_EXIT_CODE=$? - ;; - checkTestMethodsPrefix) - ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name check-test-methods-prefix-${SUFFIX} ${IMAGE_PHP} php -dxdebug.mode=off Build/Scripts/testMethodPrefixChecker.php + checkIntegrityPhp) + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name check-annotations-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${IMAGE_PHP} php Build/Scripts/phpIntegrityChecker.php -p ${PHP_VERSION} SUITE_EXIT_CODE=$? ;; checkBom) @@ -890,10 +879,6 @@ case ${TEST_SUITE} in ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name check-iso-database-${SUFFIX} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" SUITE_EXIT_CODE=$? ;; - checkNamespaceIntegrity) - ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name check-namespaces-${SUFFIX} ${IMAGE_PHP} php -dxdebug.mode=off Build/Scripts/checkNamespaceIntegrity.php - SUITE_EXIT_CODE=$? - ;; checkPermissions) ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name check-permissions-${SUFFIX} ${IMAGE_PHP} Build/Scripts/checkFilePermissions.sh SUITE_EXIT_CODE=$? diff --git a/Build/Scripts/testClassFinalChecker.php b/Build/Scripts/testClassFinalChecker.php deleted file mode 100644 index e429c5a688017fabc6871a22eab0deb5dfc56be5..0000000000000000000000000000000000000000 --- a/Build/Scripts/testClassFinalChecker.php +++ /dev/null @@ -1,88 +0,0 @@ -<?php - -declare(strict_types=1); - -use PhpParser\Node; -use PhpParser\NodeTraverser; -use PhpParser\NodeVisitorAbstract; -use PhpParser\ParserFactory; -use PhpParser\PhpVersion; -use Symfony\Component\Console\Output\ConsoleOutput; - -require_once __DIR__ . '/../../vendor/autoload.php'; - -/** - * This script checks all class tests are declared final. - */ -class NodeVisitor extends NodeVisitorAbstract -{ - public array $matches = []; - - public function enterNode(Node $node): void - { - if (($node instanceof Node\Stmt\Class_) && !$node->isFinal() && !$node->isAnonymous() && !$node->isAbstract()) { - $this->matches[$node->getLine()] = $node->name; - } - } -} - -$parser = (new ParserFactory())->createForVersion(PhpVersion::fromComponents(8, 2)); - -$finder = new Symfony\Component\Finder\Finder(); -$finder->files() - ->in([ - __DIR__ . '/../../typo3/sysext/*/Tests/Unit/', - __DIR__ . '/../../typo3/sysext/*/Tests/UnitDeprecated/', - __DIR__ . '/../../typo3/sysext/*/Tests/Functional/', - __DIR__ . '/../../typo3/sysext/*/Tests/FunctionalDeprecated/', - ]) - ->name('/Test\.php$/'); - -$output = new ConsoleOutput(); - -$errors = []; -foreach ($finder as $file) { - try { - $ast = $parser->parse($file->getContents()); - } catch (Error $error) { - $output->writeln('<error>Parse error: ' . $error->getMessage() . '</error>'); - exit(1); - } - - $visitor = new NodeVisitor(); - - $traverser = new NodeTraverser(); - $traverser->addVisitor($visitor); - - $ast = $traverser->traverse($ast); - - if (!empty($visitor->matches)) { - $errors[$file->getRealPath()] = $visitor->matches; - $output->write('<error>F</error>'); - } else { - $output->write('<fg=green>.</>'); - } -} - -$output->writeln(''); - -if (!empty($errors)) { - $output->writeln(''); - - foreach ($errors as $file => $matchesPerLine) { - $output->writeln(''); - $output->writeln('<error>Test class should be marked as final. Found in ' . $file . '</error>'); - - /** - * @var array $matchesPerLine - * @var int $line - * @var array $matches - */ - foreach ($matchesPerLine as $line => $methodName) { - $output->writeln('Method:' . $methodName . ' Line:' . $line); - } - } - exit(1); -} - -exit(0); diff --git a/Build/Scripts/testMethodPrefixChecker.php b/Build/Scripts/testMethodPrefixChecker.php deleted file mode 100644 index ef71201e2874d51017860fa92fd6f7fa602555dd..0000000000000000000000000000000000000000 --- a/Build/Scripts/testMethodPrefixChecker.php +++ /dev/null @@ -1,92 +0,0 @@ -<?php - -declare(strict_types=1); - -use PhpParser\Node; -use PhpParser\NodeTraverser; -use PhpParser\NodeVisitorAbstract; -use PhpParser\ParserFactory; -use PhpParser\PhpVersion; -use Symfony\Component\Console\Output\ConsoleOutput; - -require_once __DIR__ . '/../../vendor/autoload.php'; - -/** - * This script checks tests do not start with "test" like - * "public function testSomething". Instead they should be - * like "public function SomethinIsLike()" and have a @test annotation. - * - * This is done for general consistency and the "splitter" - * scripts that chunk tests to multiple nodes for functional tests. - */ -class NodeVisitor extends NodeVisitorAbstract -{ - public array $matches = []; - - public function enterNode(Node $node): void - { - if (($node instanceof Node\Stmt\ClassMethod) && str_starts_with($node->name->name, 'test')) { - $this->matches[$node->getLine()] = $node->name->name; - } - } -} - -$parser = (new ParserFactory())->createForVersion(PhpVersion::fromComponents(8, 2)); - -$finder = new Symfony\Component\Finder\Finder(); -$finder->files() - ->in([ - __DIR__ . '/../../typo3/sysext/*/Tests/Unit/', - __DIR__ . '/../../typo3/sysext/*/Tests/UnitDeprecated/', - __DIR__ . '/../../typo3/sysext/*/Tests/Functional/', - ]) - ->name('/Test\.php$/'); - -$output = new ConsoleOutput(); - -$errors = []; -foreach ($finder as $file) { - try { - $ast = $parser->parse($file->getContents()); - } catch (Error $error) { - $output->writeln('<error>Parse error: ' . $error->getMessage() . '</error>'); - exit(1); - } - - $visitor = new NodeVisitor(); - - $traverser = new NodeTraverser(); - $traverser->addVisitor($visitor); - - $ast = $traverser->traverse($ast); - - if (!empty($visitor->matches)) { - $errors[$file->getRealPath()] = $visitor->matches; - $output->write('<error>F</error>'); - } else { - $output->write('<fg=green>.</>'); - } -} - -$output->writeln(''); - -if (!empty($errors)) { - $output->writeln(''); - - foreach ($errors as $file => $matchesPerLine) { - $output->writeln(''); - $output->writeln('<error>Method shoud not start "test". Use @test phpDoc annotation instead. Found in ' . $file . '</error>'); - - /** - * @var array $matchesPerLine - * @var int $line - * @var array $matches - */ - foreach ($matchesPerLine as $line => $methodName) { - $output->writeln('Method:' . $methodName . ' Line:' . $line); - } - } - exit(1); -} - -exit(0); diff --git a/Build/gitlab-ci/nightly/integrity.yml b/Build/gitlab-ci/nightly/integrity.yml index 85235bbb07e853ff0306147307a986d53f7c2ca3..df0551ef8d094812876850561f6c8945da0b68db 100644 --- a/Build/gitlab-ci/nightly/integrity.yml +++ b/Build/gitlab-ci/nightly/integrity.yml @@ -1,4 +1,4 @@ -annotations php 8.2: +integrity php 8.2: stage: integrity tags: - metal2 @@ -7,7 +7,7 @@ annotations php 8.2: - schedules script: - Build/Scripts/runTests.sh -s composerInstall -p 8.2 - - Build/Scripts/runTests.sh -s checkAnnotations -p 8.2 + - Build/Scripts/runTests.sh -s checkIntegrityPhp -p 8.2 cgl: stage: integrity @@ -58,9 +58,6 @@ integration various: - Build/Scripts/runTests.sh -s checkExtensionScannerRst -p 8.2 - Build/Scripts/runTests.sh -s checkBom -p 8.2 - Build/Scripts/runTests.sh -s checkComposer -p 8.2 - - Build/Scripts/runTests.sh -s checkTestClassFinal -p 8.2 - - Build/Scripts/runTests.sh -s checkTestMethodsPrefix -p 8.2 - - Build/Scripts/runTests.sh -s checkNamespaceIntegrity -p 8.2 iso database max: stage: integrity diff --git a/Build/gitlab-ci/pre-merge/integrity.yml b/Build/gitlab-ci/pre-merge/integrity.yml index 62ae7841e5549f233ce05bc4a9a24f41399959f5..21fd4dc576dff2a4954a497f896541d8ef1699ac 100644 --- a/Build/gitlab-ci/pre-merge/integrity.yml +++ b/Build/gitlab-ci/pre-merge/integrity.yml @@ -1,4 +1,4 @@ -annotations php 8.2 pre-merge: +integrity php 8.2 pre-merge: stage: main tags: - metal2 @@ -8,7 +8,7 @@ annotations php 8.2 pre-merge: - main script: - Build/Scripts/runTests.sh -s composerInstall -p 8.2 - - Build/Scripts/runTests.sh -s checkAnnotations -p 8.2 + - Build/Scripts/runTests.sh -s checkIntegrityPhp -p 8.2 cgl pre-merge: stage: main @@ -62,9 +62,6 @@ integration various pre-merge: - Build/Scripts/runTests.sh -s checkExtensionScannerRst -p 8.2 - Build/Scripts/runTests.sh -s checkBom -p 8.2 - Build/Scripts/runTests.sh -s checkComposer -p 8.2 - - Build/Scripts/runTests.sh -s checkTestClassFinal -p 8.2 - - Build/Scripts/runTests.sh -s checkTestMethodsPrefix -p 8.2 - - Build/Scripts/runTests.sh -s checkNamespaceIntegrity -p 8.2 lint php 8.2 pre-merge: stage: main diff --git a/composer.json b/composer.json index dc82d590e8f7dce8df2c12595a08061b1f7a223f..eb602fba8bc9bbca4f1a9a5791c55441dbbefde9 100644 --- a/composer.json +++ b/composer.json @@ -271,6 +271,7 @@ "TYPO3\\CMS\\Beuser\\Tests\\": "typo3/sysext/beuser/Tests/", "TYPO3\\CMS\\Core\\Tests\\": "typo3/sysext/core/Tests/", "TYPO3\\CMS\\Composer\\Scripts\\": "Build/Scripts/composer/", + "TYPO3\\CMS\\PhpIntegrityChecks\\": "Build/Scripts/phpIntegrityChecks/", "TYPO3\\CMS\\Dashboard\\Tests\\": "typo3/sysext/dashboard/Tests/", "TYPO3\\CMS\\Extbase\\Tests\\": "typo3/sysext/extbase/Tests/", "TYPO3\\CMS\\Extensionmanager\\Tests\\": "typo3/sysext/extensionmanager/Tests/",