diff --git a/.gitlab-ci/pipeline.yml b/.gitlab-ci/pipeline.yml
index 21b0808dca8e35ebd6dc4eab9fe8c278d78789d5..ea8af938328b5a4c92cb00eeebc8c033ba600de5 100644
--- a/.gitlab-ci/pipeline.yml
+++ b/.gitlab-ci/pipeline.yml
@@ -1,4 +1,4 @@
-# cspell:ignore drupaltestbot drupaltestbotpw
+# cspell:ignore cobertura drupaltestbot drupaltestbotpw
 
 stages:
   - 🗜️ Test
@@ -173,6 +173,35 @@ variables:
     TESTSUITE: PHPUnit-Unit
     KUBERNETES_CPU_REQUEST: "16"
 
+'✅️ PHPStan Tests':
+  <<: [ *default-job-settings ]
+  variables:
+    KUBERNETES_CPU_REQUEST: "2"
+  # Run if PHPStan files have changed, or manually.
+  rules:
+    - if: $CI_PIPELINE_SOURCE == "parent_pipeline" && $PERFORMANCE_TEST != "1"
+      changes:
+        - core/tests/PHPStan/*
+        - composer/Metapackage/PinnedDevDependencies/composer.json
+    - when: manual
+      allow_failure: true
+  # Default job settings runs a script that expects vendor to exist.
+  before_script: []
+  script:
+    - docker-php-ext-enable pcov
+    - cd core/tests/PHPStan
+    - composer install
+    - vendor/bin/phpunit tests --coverage-text --colors=never --coverage-cobertura=coverage.cobertura.xml --log-junit junit.xml
+  # Default job settings runs a script that junit files in a specific location..
+  after_script: []
+  artifacts:
+    when: always
+    reports:
+      junit: core/tests/PHPStan/junit.xml
+      coverage_report:
+        coverage_format: cobertura
+        path: core/tests/PHPStan/coverage.cobertura.xml
+
 '🦉️️️ Nightwatch':
   <<: [ *with-composer-and-yarn, *default-job-settings ]
   variables:
diff --git a/composer.json b/composer.json
index 61c5d3735acf2153e6b5f7b70a17a4a3fc54b1f2..9fef43ac90fa13a8451ead72677b295c0037df73 100644
--- a/composer.json
+++ b/composer.json
@@ -105,6 +105,11 @@
             "Drupal\\Composer\\": "composer"
         }
     },
+    "autoload-dev": {
+        "psr-4": {
+            "Drupal\\PHPStan\\Rules\\": "core/tests/PHPStan/Rules"
+        }
+    },
     "scripts": {
         "pre-install-cmd": "Drupal\\Composer\\Composer::ensureComposerVersion",
         "pre-update-cmd": "Drupal\\Composer\\Composer::ensureComposerVersion",
diff --git a/core/.cspell.json b/core/.cspell.json
index f4136d3bef9f11f460809a75e5aaa282871ba36d..229697d909a8cac824bf0e4ccbe895db265d7937 100644
--- a/core/.cspell.json
+++ b/core/.cspell.json
@@ -26,6 +26,7 @@
       "profiles/demo_umami/modules/demo_umami_content/default_content/languages/es/**/*",
       "tests/fixtures/files/*",
       "tests/Drupal/Tests/Component/Annotation/Doctrine/**",
+      "tests/PHPStan/vendor/**",
       "themes/olivero/fonts/**",
       "COPYRIGHT.txt",
       "MAINTAINERS.txt",
diff --git a/core/phpstan.neon.dist b/core/phpstan.neon.dist
index 17c3970c21ac5222cb89e6edc6313ac98ad2637e..35bcfd1e64b51a005a28a1a2ff8252241ba5ec98 100644
--- a/core/phpstan.neon.dist
+++ b/core/phpstan.neon.dist
@@ -26,6 +26,8 @@ parameters:
     - ../*/node_modules/*
     - */tests/fixtures/*.php
     - */tests/fixtures/*.php.gz
+    # Skip Drupal's own PHPStan rules test fixtures.
+    - tests/PHPStan/fixtures/*
     # Skip Drupal 6 & 7 code.
     - scripts/dump-database-d?.sh
     - scripts/generate-d?-content.sh
@@ -44,3 +46,6 @@ parameters:
     - "#Drupal calls should be avoided in classes, use dependency injection instead#"
     - "#^Plugin definitions cannot be altered.#"
     - "#^Class .* extends @internal class#"
+
+rules:
+  - Drupal\PHPStan\Rules\ComponentTestDoesNotExtendCoreTest
diff --git a/core/tests/Drupal/Tests/Listeners/DrupalComponentTestListenerTrait.php b/core/tests/Drupal/Tests/Listeners/DrupalComponentTestListenerTrait.php
deleted file mode 100644
index 90a5638e2dc0899ffbd178337f437b512484fa9c..0000000000000000000000000000000000000000
--- a/core/tests/Drupal/Tests/Listeners/DrupalComponentTestListenerTrait.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Drupal\Tests\Listeners;
-
-use Drupal\KernelTests\KernelTestBase;
-use Drupal\Tests\BrowserTestBase;
-use Drupal\Tests\UnitTestCase;
-use PHPUnit\Framework\AssertionFailedError;
-
-/**
- * Ensures that no component tests are extending a core test base class.
- *
- * @internal
- */
-trait DrupalComponentTestListenerTrait {
-
-  /**
-   * Reacts to the end of a test.
-   *
-   * @param \PHPUnit\Framework\Test $test
-   *   The test object that has ended its test run.
-   * @param float $time
-   *   The time the test took.
-   */
-  protected function componentEndTest($test, $time) {
-    /** @var \PHPUnit\Framework\Test $test */
-    if (str_starts_with($test->toString(), 'Drupal\Tests\Component')) {
-      if ($test instanceof BrowserTestBase || $test instanceof KernelTestBase || $test instanceof UnitTestCase) {
-        $error = new AssertionFailedError('Component tests should not extend a core test base class.');
-        $test->getTestResultObject()->addFailure($test, $error, $time);
-      }
-    }
-  }
-
-}
diff --git a/core/tests/Drupal/Tests/Listeners/DrupalListener.php b/core/tests/Drupal/Tests/Listeners/DrupalListener.php
index 182e4fe4e7b23a27aef9b9ccbaf940f420a7bd13..26fa1e5001e364d7c14715c4b08537cd2e271f87 100644
--- a/core/tests/Drupal/Tests/Listeners/DrupalListener.php
+++ b/core/tests/Drupal/Tests/Listeners/DrupalListener.php
@@ -18,7 +18,6 @@
 class DrupalListener implements TestListener {
 
   use TestListenerDefaultImplementation;
-  use DrupalComponentTestListenerTrait;
 
   /**
    * The wrapped Symfony test listener.
@@ -60,7 +59,6 @@ public function startTest(Test $test): void {
    */
   public function endTest(Test $test, float $time): void {
     $this->symfonyListener->endTest($test, $time);
-    $this->componentEndTest($test, $time);
   }
 
 }
diff --git a/core/tests/PHPStan/.gitignore b/core/tests/PHPStan/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..fa62fc5129cc317e061e3403ba75a6b0190ab0ec
--- /dev/null
+++ b/core/tests/PHPStan/.gitignore
@@ -0,0 +1,4 @@
+composer.lock
+coverage.cobertura.xml
+junit.xml
+vendor/
diff --git a/core/tests/PHPStan/README.md b/core/tests/PHPStan/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..dfe13aeaf99e16ca35399a1dadab5e8da55a2757
--- /dev/null
+++ b/core/tests/PHPStan/README.md
@@ -0,0 +1,43 @@
+# Drupal custom PHPStan rules
+
+This directory contains PHPStan rules specifically developed for Drupal.
+
+## Subdirectories
+
+* _Rules_: contains the actual rules.
+* _tests_: contains PHPUnit tests for the rules.
+* _fixtures_: contains fixture files for the PHPUnit tests of the rules.
+
+## Enabling rules
+
+Rules are executed when they are added to the the phpstan.neon(.dist)
+configuration file of a PHPStan scan run. You need to add them under the
+`rules` entry in the file, specifying the fully qualified class name of the
+rule. For example:
+```
+
+rules:
+  - Drupal\PHPStan\Rules\ComponentTestDoesNotExtendCoreTest
+
+```
+
+## Testing rules
+
+PHPStan rules must be tested in the context of the PHPStan testing framework,
+that differs in terms of dependencies from Drupal's one.
+
+Note that for this reason, these tests are run _separately_ from Drupal core
+tests.
+
+A _composer.json_ file is present in this directory, indicating the required
+packages for the execution of the tests. Installing via composer
+```
+$ composer install
+```
+builds a _vendor_ subdirectory that includes all the packages required. Note
+this packages' codebase is totally independent from Drupal core's one.
+
+In the context of this directory, you can then execute the rule tests like
+```
+$ vendor/bin/phpunit tests
+```
diff --git a/core/tests/PHPStan/Rules/ComponentTestDoesNotExtendCoreTest.php b/core/tests/PHPStan/Rules/ComponentTestDoesNotExtendCoreTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..4b5eb9f8062c27ee039056ab00d492d57799f6c2
--- /dev/null
+++ b/core/tests/PHPStan/Rules/ComponentTestDoesNotExtendCoreTest.php
@@ -0,0 +1,64 @@
+<?php
+
+declare(strict_types=1);
+
+// cspell:ignore analyse
+namespace Drupal\PHPStan\Rules;
+
+use Drupal\BuildTests\Framework\BuildTestBase;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\UnitTestCase;
+use PhpParser\Node;
+use PHPStan\Analyser\Scope;
+use PHPStan\Node\InClassNode;
+use PHPStan\Rules\Rule;
+use PHPStan\Rules\RuleErrorBuilder;
+
+/**
+ * Ensures that no component tests are extending a core test base class.
+ *
+ * @implements Rule<\PHPStan\Node\InClassNode>
+ *
+ * @internal
+ */
+final class ComponentTestDoesNotExtendCoreTest implements Rule {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getNodeType(): string {
+    return InClassNode::class;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processNode(Node $node, Scope $scope): array {
+    $class = $node->getClassReflection();
+
+    if (!str_starts_with($class->getName(), 'Drupal\Tests\Component')) {
+      return [];
+    }
+
+    $invalidParents = [
+      UnitTestCase::class,
+      BuildTestBase::class,
+      KernelTestBase::class,
+      BrowserTestBase::class,
+    ];
+
+    foreach ($invalidParents as $invalidParent) {
+      if ($class->isSubclassOf($invalidParent)) {
+        return [
+          RuleErrorBuilder::message("Component tests should not extend {$invalidParent}.")
+            ->line($node->getStartLine())
+            ->build(),
+        ];
+      }
+    }
+
+    return [];
+  }
+
+}
diff --git a/core/tests/PHPStan/composer.json b/core/tests/PHPStan/composer.json
new file mode 100644
index 0000000000000000000000000000000000000000..f59e19499771b4a104c61fc80431f484af18151c
--- /dev/null
+++ b/core/tests/PHPStan/composer.json
@@ -0,0 +1,20 @@
+{
+    "name": "drupal/phpstan-testing",
+    "description": "Tests Drupal core's PHPStan rules",
+    "require-dev": {
+        "phpunit/phpunit": "^9",
+        "phpstan/phpstan": "1.10.66"
+    },
+    "license": "GPL-2.0-or-later",
+    "autoload": {
+        "psr-4": {
+            "Drupal\\PHPStan\\Rules\\": "Rules/",
+            "Drupal\\BuildTests\\": "../Drupal/BuildTests/",
+            "Drupal\\FunctionalJavascriptTests\\": "../Drupal/FunctionalJavascriptTests",
+            "Drupal\\FunctionalTests\\": "../Drupal/FunctionalTests",
+            "Drupal\\KernelTests\\": "../Drupal/KernelTests/",
+            "Drupal\\Tests\\": "../Drupal/Tests/"
+        }
+    },
+    "require": {}
+}
diff --git a/core/tests/PHPStan/fixtures/component-tests.php b/core/tests/PHPStan/fixtures/component-tests.php
new file mode 100644
index 0000000000000000000000000000000000000000..e6a8da7c606c2fc38a7d48b1205f9277e7678754
--- /dev/null
+++ b/core/tests/PHPStan/fixtures/component-tests.php
@@ -0,0 +1,63 @@
+<?php
+
+// phpcs:ignoreFile
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Component\Foo {
+
+  use Drupal\BuildTests\Framework\BuildTestBase;
+  use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+  use Drupal\KernelTests\KernelTestBase;
+  use Drupal\Tests\BrowserTestBase;
+  use Drupal\Tests\UnitTestCase;
+  use PHPUnit\Framework\TestCase;
+
+  final class FooTest extends TestCase {
+  }
+
+  final class UnitTest extends UnitTestCase {
+  }
+
+  final class BuildTest extends BuildTestBase {
+  }
+
+  final class KernelTest extends KernelTestBase {
+  }
+
+  final class FunctionalTest extends BrowserTestBase {
+  }
+
+  final class FunctionalJavascriptTest extends WebDriverTestBase {
+  }
+
+}
+
+namespace Drupal\Tests\Core\Foo {
+
+  use Drupal\BuildTests\Framework\BuildTestBase;
+  use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+  use Drupal\KernelTests\KernelTestBase;
+  use Drupal\Tests\BrowserTestBase;
+  use Drupal\Tests\UnitTestCase;
+  use PHPUnit\Framework\TestCase;
+
+  final class FooTest extends TestCase {
+  }
+
+  final class UnitTest extends UnitTestCase {
+  }
+
+  final class BuildTest extends BuildTestBase {
+  }
+
+  final class KernelTest extends KernelTestBase {
+  }
+
+  final class FunctionalTest extends BrowserTestBase {
+  }
+
+  final class FunctionalJavascriptTest extends WebDriverTestBase {
+  }
+
+}
diff --git a/core/tests/PHPStan/phpunit.xml.dist b/core/tests/PHPStan/phpunit.xml.dist
new file mode 100644
index 0000000000000000000000000000000000000000..7ca6a7c507c2d414e95d52b5f88223d3dd785962
--- /dev/null
+++ b/core/tests/PHPStan/phpunit.xml.dist
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         colors="true"
+         beStrictAboutTestsThatDoNotTestAnything="true"
+         beStrictAboutOutputDuringTests="true"
+         beStrictAboutChangesToGlobalState="true"
+         failOnWarning="true"
+         cacheResult="false"
+         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
+  <php>
+    <!-- Set error reporting to E_ALL. -->
+    <ini name="error_reporting" value="32767"/>
+    <!-- Do not limit the amount of memory tests take to run. -->
+    <ini name="memory_limit" value="-1"/>
+  </php>
+  <testsuites>
+    <testsuite name="PHPStan tests">
+      <directory>tests</directory>
+    </testsuite>
+  </testsuites>
+  <!-- Settings for coverage reports. -->
+  <coverage>
+    <include>
+      <directory>Rules</directory>
+    </include>
+  </coverage>
+</phpunit>
diff --git a/core/tests/PHPStan/tests/ComponentTestDoesNotExtendCoreTestTest.php b/core/tests/PHPStan/tests/ComponentTestDoesNotExtendCoreTestTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..1169f39b8839866d1aff19e69222f975d2d16058
--- /dev/null
+++ b/core/tests/PHPStan/tests/ComponentTestDoesNotExtendCoreTestTest.php
@@ -0,0 +1,55 @@
+<?php
+
+declare(strict_types=1);
+
+// cspell:ignore analyse
+namespace Drupal\PHPStan\Tests;
+
+use Drupal\PHPStan\Rules\ComponentTestDoesNotExtendCoreTest;
+use PHPStan\Rules\Rule;
+use PHPStan\Testing\RuleTestCase;
+
+/**
+ * Tests ComponentTestDoesNotExtendCoreTest rule.
+ */
+class ComponentTestDoesNotExtendCoreTestTest extends RuleTestCase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getRule(): Rule {
+    return new ComponentTestDoesNotExtendCoreTest();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function testRule(): void {
+    $this->analyse(
+      [__DIR__ . '/../fixtures/component-tests.php'],
+      [
+        [
+          'Component tests should not extend Drupal\Tests\UnitTestCase.',
+          19,
+        ],
+        [
+          'Component tests should not extend Drupal\BuildTests\Framework\BuildTestBase.',
+          22,
+        ],
+        [
+          'Component tests should not extend Drupal\KernelTests\KernelTestBase.',
+          25,
+        ],
+        [
+          'Component tests should not extend Drupal\Tests\BrowserTestBase.',
+          28,
+        ],
+        [
+          'Component tests should not extend Drupal\Tests\BrowserTestBase.',
+          31,
+        ],
+      ]
+    );
+  }
+
+}
diff --git a/core/tests/PHPStan/tests/EnsurePHPStanVersionsMatchTest.php b/core/tests/PHPStan/tests/EnsurePHPStanVersionsMatchTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..10c539db91ba0b5ddf8ba79aaf80f21e707986f9
--- /dev/null
+++ b/core/tests/PHPStan/tests/EnsurePHPStanVersionsMatchTest.php
@@ -0,0 +1,20 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\PHPStan\Tests;
+
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Tests that PHPStan versions match.
+ */
+class EnsurePHPStanVersionsMatchTest extends TestCase {
+
+  public function testVersions(): void {
+    $test_composer = json_decode(file_get_contents(__DIR__ . '/../composer.json'), TRUE);
+    $drupal_composer = json_decode(file_get_contents(__DIR__ . '/../../../../composer/Metapackage/PinnedDevDependencies/composer.json'), TRUE);
+    $this->assertSame($test_composer['require-dev']['phpstan/phpstan'], $drupal_composer['require']['phpstan/phpstan']);
+  }
+
+}