diff --git a/core/lib/Drupal/Core/Test/RunTests/TestFileParser.php b/core/lib/Drupal/Core/Test/RunTests/TestFileParser.php
new file mode 100644
index 0000000000000000000000000000000000000000..afd92df5e428857e46d9713878b419dc59213d86
--- /dev/null
+++ b/core/lib/Drupal/Core/Test/RunTests/TestFileParser.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Drupal\Core\Test\RunTests;
+
+use Drupal\simpletest\TestBase;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Parses class names from PHP files without loading them.
+ *
+ * @internal
+ */
+class TestFileParser {
+
+  /**
+   * Gets the classes from a PHP file.
+   *
+   * @param string $file
+   *   The path to the file to parse.
+   *
+   * @return string[]
+   *   Array of fully qualified class names within the PHP file.
+   */
+  public function getTestListFromFile($file) {
+    $test_list = $this->parseContents(file_get_contents($file));
+    return array_filter($test_list, function ($class) {
+      return (is_subclass_of($class, TestCase::class) || is_subclass_of($class, TestBase::class));
+    });
+  }
+
+  /**
+   * Parse class names out of PHP file contents.
+   *
+   * @param string $contents
+   *   The contents of a PHP file.
+   *
+   * @return string[]
+   *   Array of fully qualified class names within the PHP file contents.
+   */
+  protected function parseContents($contents) {
+    // Extract a potential namespace.
+    $namespace = FALSE;
+    if (preg_match('@^\s*namespace ([^ ;]+)@m', $contents, $matches)) {
+      $namespace = $matches[1];
+    }
+    $test_list = [];
+    // Extract all class names. Abstract classes are excluded on purpose.
+    preg_match_all('@^\s*(?!abstract\s+)(?:final\s+|\s*)class ([^ ]+)@m', $contents, $matches);
+    if (!$namespace) {
+      $test_list = $matches[1];
+    }
+    else {
+      foreach ($matches[1] as $class_name) {
+        $namespace_class = $namespace . '\\' . $class_name;
+        $test_list[] = $namespace_class;
+      }
+    }
+    return $test_list;
+  }
+
+}
diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh
index c3f9d1a73aa8ffbd536735bfa109bbb203f6a88e..9d3f3894ebae1730b2ddd643fefe8ef0b2e86e7c 100755
--- a/core/scripts/run-tests.sh
+++ b/core/scripts/run-tests.sh
@@ -16,6 +16,7 @@
 use Drupal\Core\StreamWrapper\PublicStream;
 use Drupal\Core\Test\EnvironmentCleaner;
 use Drupal\Core\Test\PhpUnitTestRunner;
+use Drupal\Core\Test\RunTests\TestFileParser;
 use Drupal\Core\Test\TestDatabase;
 use Drupal\Core\Test\TestRunnerKernel;
 use Drupal\simpletest\Form\SimpletestResultsForm;
@@ -1054,31 +1055,13 @@ function simpletest_script_get_test_list() {
     }
     elseif ($args['file']) {
       // Extract test case class names from specified files.
+      $parser = new TestFileParser();
       foreach ($args['test_names'] as $file) {
         if (!file_exists($file)) {
           simpletest_script_print_error('File not found: ' . $file);
           exit(SIMPLETEST_SCRIPT_EXIT_FAILURE);
         }
-        $content = file_get_contents($file);
-        // Extract a potential namespace.
-        $namespace = FALSE;
-        if (preg_match('@^namespace ([^ ;]+)@m', $content, $matches)) {
-          $namespace = $matches[1];
-        }
-        // Extract all class names.
-        // Abstract classes are excluded on purpose.
-        preg_match_all('@^class ([^ ]+)@m', $content, $matches);
-        if (!$namespace) {
-          $test_list = array_merge($test_list, $matches[1]);
-        }
-        else {
-          foreach ($matches[1] as $class_name) {
-            $namespace_class = $namespace . '\\' . $class_name;
-            if (is_subclass_of($namespace_class, '\Drupal\simpletest\TestBase') || is_subclass_of($namespace_class, TestCase::class)) {
-              $test_list[] = $namespace_class;
-            }
-          }
-        }
+        $test_list = array_merge($test_list, $parser->getTestListFromFile($file));
       }
     }
     elseif ($args['directory']) {
@@ -1112,27 +1095,9 @@ function simpletest_script_get_test_list() {
           $files[$filename] = $filename;
         }
       }
+      $parser = new TestFileParser();
       foreach ($files as $file) {
-        $content = file_get_contents($file);
-        // Extract a potential namespace.
-        $namespace = FALSE;
-        if (preg_match('@^\s*namespace ([^ ;]+)@m', $content, $matches)) {
-          $namespace = $matches[1];
-        }
-        // Extract all class names.
-        // Abstract classes are excluded on purpose.
-        preg_match_all('@^\s*class ([^ ]+)@m', $content, $matches);
-        if (!$namespace) {
-          $test_list = array_merge($test_list, $matches[1]);
-        }
-        else {
-          foreach ($matches[1] as $class_name) {
-            $namespace_class = $namespace . '\\' . $class_name;
-            if (is_subclass_of($namespace_class, '\Drupal\simpletest\TestBase') || is_subclass_of($namespace_class, TestCase::class)) {
-              $test_list[] = $namespace_class;
-            }
-          }
-        }
+        $test_list = array_merge($test_list, $parser->getTestListFromFile($file));
       }
     }
     else {
diff --git a/core/tests/Drupal/Tests/Core/Test/RunTests/TestFileParserTest.php b/core/tests/Drupal/Tests/Core/Test/RunTests/TestFileParserTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..5ba0f83c489afc8f38b7a16f43382cce2b8041bc
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Test/RunTests/TestFileParserTest.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Drupal\Tests\Core\Test\RunTests;
+
+use Drupal\Core\Test\RunTests\TestFileParser;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Test\RunTests\TestFileParser
+ * @group Test
+ * @group RunTests
+ */
+class TestFileParserTest extends UnitTestCase {
+
+  public function provideTestFileContents() {
+    return [
+      'empty' => [[], ''],
+      'no-namespace' => [['ConcreteClass'],
+       <<< 'NO_NAMESPACE'
+<?php
+
+class ConcreteClass {}
+NO_NAMESPACE
+      ],
+      'concrete' => [['Namespace\Is\Complex\ConcreteClass'],
+       <<< 'CONCRETE_CLASS'
+<?php
+
+namespace Namespace\Is\Complex;
+
+class ConcreteClass {}
+CONCRETE_CLASS
+      ],
+      'abstract' => [[],
+       <<< 'ABSTRACT_CLASS'
+<?php
+namespace Namespace\Is\Complex;
+
+abstract class AbstractClass {}
+ABSTRACT_CLASS
+      ],
+      'final' => [['Namespace\Is\Complex\FinalClass'],
+       <<< 'FINAL_CLASS'
+<?php
+namespace Namespace\Is\Complex;
+
+final class FinalClass {}
+FINAL_CLASS
+      ],
+      'compound_declarations' => [[
+        'Namespace\Is\Complex\FinalClass',
+        'Namespace\Is\Complex\AnotherClass',
+      ],
+       <<< 'COMPOUND'
+<?php
+namespace Namespace\Is\Complex;
+
+final class FinalClass {}
+
+class AnotherClass {}
+COMPOUND
+      ],
+    ];
+  }
+
+  /**
+   * @covers ::parseContents
+   * @dataProvider provideTestFileContents
+   */
+  public function testParseContents($expected, $contents) {
+    $parser = new TestFileParser();
+
+    $ref_parse = new \ReflectionMethod($parser, 'parseContents');
+    $ref_parse->setAccessible(TRUE);
+
+    $this->assertSame($expected, $ref_parse->invoke($parser, $contents));
+  }
+
+  /**
+   * @covers ::getTestListFromFile
+   */
+  public function testGetTestListFromFile() {
+    $parser = new TestFileParser();
+    $this->assertArrayEquals(
+      ['Drupal\Tests\Core\Test\RunTests\TestFileParserTest'],
+      $parser->getTestListFromFile(__FILE__)
+    );
+    // This WebTestBase test will eventually move, so we'll need to update it.
+    $this->assertArrayEquals(
+      ['Drupal\simpletest\Tests\TimeZoneTest'],
+      $parser->getTestListFromFile(__DIR__ . '/../../../../../../modules/simpletest/src/Tests/TimeZoneTest.php')
+    );
+    // Not a test.
+    $this->assertEmpty(
+      $parser->getTestListFromFile(__DIR__ . '/../../../AssertHelperTrait.php')
+    );
+  }
+
+}