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/",