diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index be8c695..e6b7cc3 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -10,6 +10,7 @@
 use Drupal\Core\DrupalKernel;
 use Drupal\Core\Database\Database;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Extension\ExtensionDiscovery;
 use Drupal\Core\Utility\Title;
 use Drupal\Core\Utility\Error;
 use Symfony\Component\ClassLoader\ApcClassLoader;
@@ -700,10 +701,7 @@ function drupal_get_filename($type, $name, $filename = NULL) {
   if (!empty($filename)) {
     $files[$type][$name] = $filename;
   }
-  elseif (isset($files[$type][$name])) {
-    // nothing
-  }
-  else {
+  elseif (!isset($files[$type][$name])) {
     // Verify that we have an keyvalue service before using it. This is required
     // because this function is called during installation.
     // @todo Inject database connection into KeyValueStore\DatabaseStorage.
@@ -752,14 +750,12 @@ function drupal_get_filename($type, $name, $filename = NULL) {
 
       if (!isset($dirs[$dir][$extension])) {
         $dirs[$dir][$extension] = TRUE;
-        if (!function_exists('drupal_system_listing')) {
-          require_once __DIR__ . '/common.inc';
-        }
         // Scan the appropriate directories for all files with the requested
         // extension, not just the file we are currently looking for. This
         // prevents unnecessary scans from being repeated when this function is
         // called more than once in the same page request.
-        $matches = drupal_system_listing("/^" . DRUPAL_PHP_FUNCTION_PATTERN . "\.$extension$/", $dir);
+        $listing = new ExtensionDiscovery();
+        $matches = $listing->scan($dir);
         foreach ($matches as $matched_name => $file) {
           $files[$type][$matched_name] = $file->uri;
         }
diff --git a/core/includes/common.inc b/core/includes/common.inc
index b97cc5e..f9cea6e 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -17,7 +17,6 @@
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Datetime\DrupalDateTime;
 use Drupal\Core\Routing\GeneratorNotInitializedException;
-use Drupal\Core\SystemListingInfo;
 use Drupal\Core\Template\Attribute;
 use Drupal\Core\Render\Element;
 
@@ -3177,19 +3176,6 @@ function drupal_page_set_cache(Response $response, Request $request) {
 }
 
 /**
- * This function is kept only for backward compatibility.
- *
- * @see \Drupal\Core\SystemListing::scan().
- */
-function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1) {
-  // As SystemListing is required to build a dependency injection container
-  // from scratch and SystemListingInfo only extends SystemLising, this
-  // class needs to be hardwired.
-  $listing = new SystemListingInfo();
-  return $listing->scan($mask, $directory, $key, $min_depth);
-}
-
-/**
  * Sets the main page content value for later use.
  *
  * Given the nature of the Drupal page handling, this will be called once with
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 0a57f16..fcc01f8 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -13,7 +13,7 @@
 use Drupal\Core\Language\Language;
 use Drupal\Core\Language\LanguageManager;
 use Drupal\Core\StringTranslation\Translator\FileTranslation;
-
+use Drupal\Core\Extension\ExtensionDiscovery;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\DependencyInjection\Reference;
@@ -494,7 +494,20 @@ function install_begin_request(&$install_state) {
     // Override the module list with a minimal set of modules.
     $module_handler->setModuleList(array('system' => 'core/modules/system/system.module'));
   }
-  $module_handler->load('system');
+  // After setting up a custom and finite module list in a custom low-level
+  // bootstrap like here, ensure to use ModuleHandler::loadAll() so that
+  // ModuleHandler::isLoaded() returns TRUE, since that is a condition being
+  // checked by other subsystems (e.g., the theme system).
+  $module_handler->loadAll();
+
+  // Add list of all available profiles to the installation state.
+  $listing = new ExtensionDiscovery();
+  $install_state['profiles'] += $listing->scan('profiles');
+
+  // Prime drupal_get_filename()'s static cache.
+  foreach ($install_state['profiles'] as $name => $profile) {
+    drupal_get_filename('profile', $name, $profile->uri);
+  }
 
   // Prepare for themed output. We need to run this at the beginning of the
   // page request to avoid a different theme accidentally getting set. (We also
@@ -528,9 +541,6 @@ function install_begin_request(&$install_state) {
 
   // Modify the installation state as appropriate.
   $install_state['completed_task'] = $task;
-
-  // Add the list of available profiles to the installation state.
-  $install_state['profiles'] += drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles');
 }
 
 /**
diff --git a/core/includes/install.inc b/core/includes/install.inc
index 8c91dc8..ab9a640 100644
--- a/core/includes/install.inc
+++ b/core/includes/install.inc
@@ -9,6 +9,7 @@
 use Drupal\Component\Utility\Crypt;
 use Drupal\Core\Database\Database;
 use Drupal\Core\DrupalKernel;
+use Drupal\Core\Extension\ExtensionDiscovery;
 
 /**
  * Requirement severity -- Informational message only.
@@ -122,22 +123,17 @@ function drupal_detect_database_types() {
 }
 
 /**
- * Returns all supported database installer objects that are compiled into PHP.
+ * Returns all supported database driver installer objects.
  *
- * @return
- *  An array of database installer objects compiled into PHP.
+ * @return \Drupal\Core\Database\Install\Tasks[]
+ *   An array of available database driver installer objects.
  */
 function drupal_get_database_types() {
   $databases = array();
   $drivers = array();
 
-  // We define a driver as a directory in /core/includes/database that in turn
-  // contains a database.inc file. That allows us to drop in additional drivers
-  // without modifying the installer.
-  require_once __DIR__ . '/database.inc';
-  // Allow any valid PHP identifier.
-  // @see http://www.php.net/manual/en/language.variables.basics.php.
-  $mask = '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/';
+  // The internal database driver name is any valid PHP identifier.
+  $mask = '/^' . DRUPAL_PHP_FUNCTION_PATTERN . '$/';
   $files = file_scan_directory(DRUPAL_ROOT . '/core/lib/Drupal/Core/Database/Driver', $mask, array('recurse' => FALSE));
   if (is_dir(DRUPAL_ROOT . '/drivers/lib/Drupal/Driver/Database')) {
     $files += file_scan_directory(DRUPAL_ROOT . '/drivers/lib/Drupal/Driver/Database/', $mask, array('recurse' => FALSE));
@@ -569,15 +565,16 @@ function drupal_verify_profile($install_state) {
   }
   $info = $install_state['profile_info'];
 
-  // Get a list of modules that exist in Drupal's assorted subdirectories.
+  // Get the list of available modules for the selected installation profile.
+  $listing = new ExtensionDiscovery();
   $present_modules = array();
-  foreach (drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules') as $present_module) {
+  foreach ($listing->scan('modules') as $present_module) {
     $present_modules[] = $present_module->name;
   }
 
   // The installation profile is also a module, which needs to be installed
   // after all the other dependencies have been installed.
-  $present_modules[] = drupal_get_profile();
+  $present_modules[] = $profile;
 
   // Verify that all of the profile's required modules are present.
   $missing_modules = array_diff($info['dependencies'], $present_modules);
@@ -954,9 +951,7 @@ function drupal_requirements_url($severity) {
 function drupal_check_profile($profile, array $install_state) {
   include_once __DIR__ . '/file.inc';
 
-  $profile_file = $install_state['profiles'][$profile]->uri;
-
-  if (!isset($profile) || !file_exists($profile_file)) {
+  if (!isset($profile) || !isset($install_state['profiles'][$profile])) {
     throw new Exception(install_no_profile_error());
   }
 
diff --git a/core/includes/module.inc b/core/includes/module.inc
index 429f5db..96bfbf0 100644
--- a/core/includes/module.inc
+++ b/core/includes/module.inc
@@ -5,6 +5,7 @@
  * API for loading and interacting with Drupal modules.
  */
 
+use Drupal\Core\Extension\ExtensionDiscovery;
 
 /**
  * Builds a list of bootstrap modules and enabled modules and themes.
@@ -285,14 +286,19 @@ function module_uninstall($module_list = array(), $uninstall_dependents = TRUE)
  * Returns an array of modules required by core.
  */
 function drupal_required_modules() {
-  $files = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info.yml$/', 'modules');
+  $listing = new ExtensionDiscovery();
+  $files = $listing->scan('modules');
   $required = array();
 
-  // An installation profile is required and one must always be loaded.
-  $required[] = drupal_get_profile();
+  // Unless called by the installer, an installation profile is required and
+  // must always be loaded. drupal_get_profile() also returns the installation
+  // profile in the installer, but only after it has been selected.
+  if ($profile = drupal_get_profile()) {
+    $required[] = $profile;
+  }
 
   foreach ($files as $name => $file) {
-    $info = \Drupal::service('info_parser')->parse($file->uri);
+    $info = \Drupal::service('info_parser')->parse($file->infoSubPathname);
     if (!empty($info) && !empty($info['required']) && $info['required']) {
       $required[] = $name;
     }
diff --git a/core/lib/Drupal/Core/Config/InstallStorage.php b/core/lib/Drupal/Core/Config/InstallStorage.php
index 16bcc1c..5d05790 100644
--- a/core/lib/Drupal/Core/Config/InstallStorage.php
+++ b/core/lib/Drupal/Core/Config/InstallStorage.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\Core\Config;
 
+use Drupal\Core\Extension\ExtensionDiscovery;
+
 /**
  * Storage controller used by the Drupal installer.
  *
@@ -110,9 +112,13 @@ public function listAll($prefix = '') {
    */
   protected function getAllFolders() {
     if (!isset($this->folders)) {
-      $this->folders = $this->getComponentNames('profile', array(drupal_get_profile()));
-      $this->folders += $this->getComponentNames('module', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules', 'name', 0)));
-      $this->folders += $this->getComponentNames('theme', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info.yml$/', 'themes')));
+      $this->folders = array();
+      if ($profile = drupal_get_profile()) {
+        $this->folders += $this->getComponentNames('profile', array($profile));
+      }
+      $listing = new ExtensionDiscovery();
+      $this->folders += $this->getComponentNames('module', array_keys($listing->scan('modules')));
+      $this->folders += $this->getComponentNames('theme', array_keys($listing->scan('themes')));
     }
     return $this->folders;
   }
diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php
index 46a7fee..7fa4a36 100644
--- a/core/lib/Drupal/Core/DrupalKernel.php
+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -13,6 +13,7 @@
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\DependencyInjection\ServiceProviderInterface;
 use Drupal\Core\DependencyInjection\YamlFileLoader;
+use Drupal\Core\Extension\ExtensionDiscovery;
 use Drupal\Core\Language\Language;
 use Symfony\Component\Config\Loader\LoaderInterface;
 use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
@@ -83,7 +84,7 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
    * An array of module data objects.
    *
    * The data objects have the same data structure as returned by
-   * file_scan_directory() but only the uri property is used.
+   * ExtensionDiscovery but only the uri property is used.
    *
    * @var array
    */
@@ -297,19 +298,27 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ
   protected function moduleData($module) {
     if (!$this->moduleData) {
       // First, find profiles.
-      $profiles_scanner = new SystemListing();
-      $all_profiles = $profiles_scanner->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles');
-      $profiles = array_keys(array_intersect_key($this->moduleList, $all_profiles));
+      $listing = new ExtensionDiscovery();
+      $all_profiles = $listing->scan('profiles');
+      $profiles = array_intersect_key($all_profiles, $this->moduleList);
+
       // If a module is within a profile directory but specifies another
       // profile for testing, it needs to be found in the parent profile.
-      if (($parent_profile_config = $this->configStorage->read('simpletest.settings')) && isset($parent_profile_config['parent_profile']) && $parent_profile_config['parent_profile'] != $profiles[0]) {
+      $settings = $this->configStorage->read('simpletest.settings');
+      $parent_profile = !empty($settings['parent_profile']) ? $settings['parent_profile'] : FALSE;
+      if ($parent_profile && !isset($profiles[$parent_profile])) {
         // In case both profile directories contain the same extension, the
         // actual profile always has precedence.
-        array_unshift($profiles, $parent_profile_config['parent_profile']);
+        $profiles = array($parent_profile => $all_profiles[$parent_profile]) + $profiles;
       }
+
+      $profile_directories = array_map(function ($profile) {
+        return dirname($profile->uri);
+      }, $profiles);
+      $listing->setProfileDirectories($profile_directories);
+
       // Now find modules.
-      $modules_scanner = new SystemListing($profiles);
-      $this->moduleData = $all_profiles + $modules_scanner->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules');
+      $this->moduleData = $profiles + $listing->scan('modules');
     }
     return isset($this->moduleData[$module]) ? $this->moduleData[$module] : FALSE;
   }
diff --git a/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php b/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php
new file mode 100644
index 0000000..e8d757d
--- /dev/null
+++ b/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Extension\Discovery\RecursiveExtensionFilterIterator.
+ */
+
+namespace Drupal\Core\Extension\Discovery;
+
+class RecursiveExtensionFilterIterator extends \RecursiveFilterIterator {
+
+  protected $skipDirs = array(
+    // Object-oriented code subdirectories.
+    'src',
+    'lib',
+    'vendor',
+    // Front-end.
+    'assets',
+    'css',
+    'files',
+    'images',
+    'js',
+    'misc',
+    'templates',
+    // Legacy subdirectories.
+    'includes',
+    'scripts',
+    // Test subdirectories.
+    'fixtures',
+    // @todo ./tests/Drupal should be ./tests/src/Drupal
+    'Drupal',
+  );
+
+  protected $acceptTests = FALSE;
+
+  public function acceptTests($flag = NULL) {
+    if (isset($flag)) {
+      $this->acceptTests = $flag;
+    }
+    if (!$this->acceptTests) {
+      $this->skipDirs[] = 'tests';
+    }
+  }
+
+  public function getChildren() {
+    $filter = parent::getChildren();
+    $filter->acceptTests($this->acceptTests);
+    return $filter;
+  }
+
+  public function accept() {
+    $name = $this->current()->getFilename();
+    // The FilesystemIterator::SKIP_DOTS flag only skips '.' and '..', but not
+    // hidden directories (like '.git').
+    if ($name[0] == '.') {
+      return FALSE;
+    }
+    if ($this->isDir()) {
+      // 'config' directories are special-cased here, because every extension
+      // contains one. However, those default configuration directories cannot
+      // contain extensions. The directory name cannot be globally skipped,
+      // because core happens to have a directory of an actual module that is
+      // named 'config'. By explicitly testing for that case, we can skip all
+      // other config directories, and at the same time, still allow the core
+      // config module to be overridden/replaced in a profile/site directory
+      // (whereas it must be located directly in a modules directory).
+      if ($name == 'config') {
+        return substr($this->current()->getPathname(), -14) == 'modules/config';
+      }
+      return !in_array($name, $this->skipDirs, TRUE);
+    }
+    else {
+      return substr($name, -9) == '.info.yml';
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
new file mode 100644
index 0000000..32f4fe9
--- /dev/null
+++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php
@@ -0,0 +1,311 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Extension\ExtensionDiscovery.
+ */
+
+namespace Drupal\Core\Extension;
+
+use Drupal\Core\Extension\Discovery\RecursiveExtensionFilterIterator;
+
+/**
+ * Discovers available extensions in the filesystem.
+ */
+class ExtensionDiscovery {
+
+  /**
+   * Regular expression to match PHP function names.
+   *
+   * @see http://php.net/manual/functions.user-defined.php
+   */
+  const PHP_FUNCTION_PATTERN = '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/';
+
+  /**
+   * InfoParser instance for parsing .info.yml files.
+   *
+   * @var \Drupal\Core\Extension\InfoParser
+   */
+  protected $infoParser;
+
+  /**
+   * Previously discovered files keyed by base directory and extension type.
+   *
+   * @var array
+   */
+  protected static $files = array();
+
+  /**
+   * List of installation profile directories to additionally scan.
+   *
+   * @var array
+   */
+  protected $profileDirectories;
+
+  /**
+   * Discovers available extensions of a given type.
+   *
+   * Finds all extensions (modules, themes, etc) that exist on the site. It
+   * searches in several locations. For instance, to discover all available
+   * modules:
+   * @code
+   * $listing = new ExtensionDiscovery();
+   * $modules = $listing->scan('modules');
+   * @endcode
+   *
+   * The following directories will be searched (in the order stated):
+   * - the core modules directory; i.e., /core/modules
+   * - the profiles directories; e.g., /core/profiles/standard
+   * - the site-wide modules directory; i.e., /modules
+   * - the all-sites directory; i.e., /sites/all/modules
+   * - the site-specific directory; i.e., /sites/example.com/modules
+   *
+   * The information is returned in an associative array, keyed by the extension
+   * name (without .info.yml extension). Extensions found later in the search
+   * will take precedence over extensions found earlier - unless they are not
+   * compatible with the current version of Drupal core.
+   *
+   * @param string $directory
+   *   The subdirectory name in which to search for extensions. For example,
+   *   'modules' will recursively search all 'modules' directories for
+   *   extensions of type 'module'.
+   * @param bool $include_tests
+   *   (optional) Whether to include test extensions. Defaults to FALSE; i.e.,
+   *   all 'tests' directories are excluded in the search.
+   *
+   * @return RecursiveDirectoryIterator[]
+   *   An associative array of RecursiveDirectoryIterator (SplFileInfo) objects,
+   *   keyed by extension name.
+   */
+  public function scan($directory, $include_tests = FALSE) {
+    // @todo Fix callers to pass $type.
+    // @see drupal_get_filename()
+    if ($directory == 'themes/engines') {
+      $type = 'theme_engine';
+    }
+    else {
+      $type = substr($directory, 0, -1);
+    }
+
+    // Installation profile directories can only be scanned for extensions when
+    // not scanning for installation profiles themselves.
+    // @todo Obsolete?
+    $profileDirectories = array();
+    if ($directory != 'profiles') {
+      // Determine the installation profile directories to scan for extensions,
+      // unless explicit profile directories have been preset.
+      if (!isset($this->profileDirectories)) {
+        $profileDirectories = $this->getProfileDirectories();
+      }
+      else {
+        $profileDirectories = $this->profileDirectories;
+      }
+    }
+
+    $searchdirs = array();
+    $searchdirs[] = 'core';
+    // Search installation profile directories.
+    foreach ($profileDirectories as $profile) {
+      $searchdirs[] = $profile;
+    }
+    // Search for contributed and custom extensions in top-level directories
+    // (and the sites/all directory for legacy/BC reasons) as well as the
+    // site-specific directory.
+    $searchdirs[] = $directory;
+    $searchdirs[] = 'sites/all';
+    $searchdirs[] = conf_path();
+
+    // Unless explicitly requested by Simpletest, manually check whether we are
+    // are in a test environment, in which case test extensions must be included.
+    if (!$include_tests) {
+      // @todo Replace with drupal_valid_test_ua() once avaiable.
+      // @see https://drupal.org/node/2171683
+      $include_tests = !empty($GLOBALS['drupal_test_info']['test_run_id']);
+    }
+
+    $files = array();
+    foreach ($searchdirs as $dir) {
+      if (!isset(static::$files[$dir][$include_tests])) {
+        static::$files[$dir][$include_tests] = $this->scanDirectory($dir, $include_tests);
+      }
+      if (isset(static::$files[$dir][$include_tests][$type])) {
+        $files = array_merge($files, $this->process($files, static::$files[$dir][$include_tests][$type]));
+      }
+    }
+    return $files;
+  }
+
+  /**
+   * Returns additional installation profile directories to be scanned.
+   *
+   * @return array
+   *   A list of installation profile directory paths relative to system root.
+   */
+  public function getProfileDirectories() {
+    $searchdir = array();
+    $profile = drupal_get_profile();
+    // For SimpleTest to be able to test modules packaged together with a
+    // distribution we need to include the profile of the parent site (in
+    // which test runs are triggered).
+    if (drupal_valid_test_ua() && !drupal_installation_attempted()) {
+      $testing_profile = \Drupal::config('simpletest.settings')->get('parent_profile');
+      if ($testing_profile && $testing_profile != $profile) {
+        $searchdir[] = drupal_get_path('profile', $testing_profile);
+      }
+    }
+    // In case both profile directories contain the same extension, the actual
+    // profile always has precedence.
+    if ($profile) {
+      $searchdir[] = drupal_get_path('profile', $profile);
+    }
+    return $searchdir;
+  }
+
+  /**
+   * Sets explicit profile directories to scan.
+   *
+   * @param array $profileDirectories
+   *   A list of installation profile directories to search for extensions.
+   *
+   * @return $this
+   */
+  public function setProfileDirectories(array $paths) {
+    $this->profileDirectories = $paths;
+    return $this;
+  }
+
+  /**
+   * Process the files to add before adding them.
+   *
+   * @param array $files
+   *   Every file found so far.
+   * @param array $files_to_add
+   *   The files found in a single directory.
+   *
+   * @return array
+   *   The processed list of file objects. Extensions discovered in later search
+   *   paths and which are not not compatible with the current core version are
+   *   skipped.
+   */
+  protected function process(array $files, array $files_to_add) {
+    // Duplicate files found in later search directories take precedence over
+    // earlier ones, so we want them to overwrite keys in our resulting
+    // $files array.
+    // The exception to this is if the later file is from a module or theme not
+    // compatible with Drupal core. This may occur during upgrades of Drupal
+    // core when new modules exist in core while older contrib modules with the
+    // same name exist in a directory such as /modules.
+    foreach (array_intersect_key($files_to_add, $files) as $file_key => $file) {
+      // Parse the extension info.
+      $info = $this->getInfoParser()->parse($file->infoSubPathname);
+
+      // If the module or theme is incompatible with Drupal core, remove it
+      // from the array for the current search directory, so it is not
+      // overwritten when merged with the $files array.
+      if (isset($info['core']) && $info['core'] != \Drupal::CORE_COMPATIBILITY) {
+        unset($files_to_add[$file_key]);
+      }
+    }
+    return $files_to_add;
+  }
+
+  /**
+   * Recursively scans a base directory for the requested extension type.
+   *
+   * @param string $dir
+   *   The base directory to scan, without trailing slash.
+   * @param bool $include_tests
+   *   (optional) Whether to include test extensions. Defaults to FALSE; i.e.,
+   *   all 'tests' directories are excluded in the search.
+   *
+   * @return array
+   *   An associative array whose keys are extension types and whose values are
+   *   associative arrays of RecursiveDirectoryIterator (SplFileInfo) objects,
+   *   keyed by extension name.
+   */
+  protected function scanDirectory($dir, $include_tests = FALSE) {
+    $files = array();
+    if (!is_dir($dir)) {
+      return $files;
+    }
+    $flags = \FilesystemIterator::UNIX_PATHS;
+    $flags |= \FilesystemIterator::SKIP_DOTS;
+    $flags |= \FilesystemIterator::CURRENT_AS_SELF;
+    $flags |= \FilesystemIterator::KEY_AS_FILENAME;
+    $directory_iterator = new \RecursiveDirectoryIterator($dir, $flags);
+
+    $filter = new RecursiveExtensionFilterIterator($directory_iterator);
+    $filter->acceptTests($include_tests);
+
+    $iterator = new \RecursiveIteratorIterator($filter, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD);
+
+    foreach ($iterator as $fileinfo) {
+      // All extension names in Drupal have to be valid PHP function names due
+      // to the module hook architecture.
+      if (!preg_match(static::PHP_FUNCTION_PATTERN, $fileinfo->getBasename('.info.yml'))) {
+        continue;
+      }
+      // Determine extension type from info file.
+      // @todo Direct parsing for performance. Though it might be likely that
+      //   InfoParser will be invoked anyway on all files; needs investigation.
+      //   One typical exception would be Simpletest, which just scans for
+      //   extensions, doesn't care for info.
+      $file = $fileinfo->openFile('r');
+      while (!isset($fileinfo->type) && !$file->eof()) {
+        preg_match('@type: ?(.+)@', $file->fgets(), $matches);
+        if (isset($matches[1])) {
+          $fileinfo->type = $matches[1];
+        }
+      }
+      if (empty($fileinfo->type)) {
+        continue;
+      }
+      // Supply public properties used throughout Drupal.
+      // @todo Consider to update all code to use the built-in
+      //   RecursiveDirectoryIterator (SplFileInfo) methods.
+      $fileinfo->name = $fileinfo->getBasename('.info.yml');
+
+      $fileinfo->infoFilename = $fileinfo->getFilename();
+      // @todo The name infoSubPathname stems from the original implementation
+      //   idea of scanning the *entire* Drupal filesystem once. Now that
+      //   individual directories are scanned again (to apply the override logic),
+      //   getSubPathname() only returns the path name relative to the directory
+      //   being scanned. That's neither useful nor appropriate naming. Thus,
+      //   either rename to 'infoUri' or get rid of the 'uri' override below.
+      $fileinfo->infoSubPathname = $dir . '/' . $fileinfo->getSubPathname();
+
+      // For themes, the filename and URI is the info file itself.
+      if ($fileinfo->type == 'theme') {
+        $fileinfo->filename = $fileinfo->getFilename();
+        $fileinfo->uri = $dir . '/' . $fileinfo->getSubPathname();
+      }
+      // For theme engines, the suffix is .engine.
+      elseif ($fileinfo->type == 'theme_engine') {
+        $fileinfo->filename = $fileinfo->name . '.engine';
+        $fileinfo->uri = $dir . '/' . $fileinfo->getSubPath() . '/' . $fileinfo->filename;
+      }
+      // For all other types, it is $name.$type (e.g., node.module).
+      else {
+        $fileinfo->filename = $fileinfo->name . '.' . $fileinfo->type;
+        $fileinfo->uri = $dir . '/' . $fileinfo->getSubPath() . '/' . $fileinfo->filename;
+      }
+
+      $files[$fileinfo->type][$fileinfo->name] = $fileinfo;
+    }
+    return $files;
+  }
+
+  /**
+   * Returns a parser for parsing .info.yml files.
+   *
+   * @return \Drupal\Core\Extension\InfoParser
+   *   The InfoParser instance.
+   */
+  protected function getInfoParser() {
+    if (!isset($this->infoParser)) {
+      $this->infoParser = new InfoParser();
+    }
+    return $this->infoParser;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Extension/InfoParser.php b/core/lib/Drupal/Core/Extension/InfoParser.php
index 5bb4841..593b34d 100644
--- a/core/lib/Drupal/Core/Extension/InfoParser.php
+++ b/core/lib/Drupal/Core/Extension/InfoParser.php
@@ -22,7 +22,7 @@ class InfoParser implements InfoParserInterface {
    *
    * @var array
    */
-  protected $parsedInfos = array();
+  protected static $parsedInfos = array();
 
   /**
    * Symfony YAML parser object.
@@ -35,29 +35,29 @@ class InfoParser implements InfoParserInterface {
    * {@inheritdoc}
    */
   public function parse($filename) {
-    if (!isset($this->parsedInfos[$filename])) {
+    if (!isset(static::$parsedInfos[$filename])) {
       if (!file_exists($filename)) {
-        $this->parsedInfos[$filename] = array();
+        static::$parsedInfos[$filename] = array();
       }
       else {
         try {
-          $this->parsedInfos[$filename] = $this->getParser()->parse(file_get_contents($filename));
+          static::$parsedInfos[$filename] = $this->getParser()->parse(file_get_contents($filename));
         }
         catch (ParseException $e) {
           $message = String::format("Unable to parse !file. Parser error !error.", array('!file' => $filename, '!error' => $e->getMessage()));
           throw new InfoParserException($message, $filename);
         }
-        $missing_keys = array_diff($this->getRequiredKeys(), array_keys($this->parsedInfos[$filename]));
+        $missing_keys = array_diff($this->getRequiredKeys(), array_keys(static::$parsedInfos[$filename]));
         if (!empty($missing_keys)) {
           $message = format_plural(count($missing_keys), 'Missing required key (!missing_keys) in !file.', 'Missing required keys (!missing_keys) in !file.', array('!missing_keys' => implode(', ', $missing_keys), '!file' => $filename));
           throw new InfoParserException($message, $filename);
         }
-        if (isset($this->parsedInfos[$filename]['version']) && $this->parsedInfos[$filename]['version'] === 'VERSION') {
-          $this->parsedInfos[$filename]['version'] = \Drupal::VERSION;
+        if (isset(static::$parsedInfos[$filename]['version']) && static::$parsedInfos[$filename]['version'] === 'VERSION') {
+          static::$parsedInfos[$filename]['version'] = \Drupal::VERSION;
         }
       }
     }
-    return $this->parsedInfos[$filename];
+    return static::$parsedInfos[$filename];
   }
 
   /**
@@ -80,7 +80,7 @@ protected function getParser() {
    *   An array of required keys.
    */
   protected function getRequiredKeys() {
-    return array('name', 'type');
+    return array('type', 'core', 'name');
   }
 
 }
diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php
index d486973..0e2424c 100644
--- a/core/lib/Drupal/Core/Extension/ModuleHandler.php
+++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php
@@ -599,10 +599,6 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
         $this->load($module);
         module_load_install($module);
 
-        // Flush theme info caches, since (testing) modules can implement
-        // hook_system_theme_info() to register additional themes.
-        system_list_reset();
-
         // Update the kernel to include it.
         // This reboots the kernel to register the module's bundle and its
         // services in the service container. The $module_filenames argument is
diff --git a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php
index a74f9cd..024eeb6 100644
--- a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php
+++ b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php
@@ -71,7 +71,7 @@ public function setModuleList(array $module_list = array());
    *
    * @param array $modules
    *   An array of module objects keyed by module name. Each object contains
-   *   information discovered during a Drupal\Core\SystemListing scan.
+   *   information discovered during a Drupal\Core\Extension\ExtensionDiscovery scan.
    *
    * @return
    *   The same array with the new keys for each module:
@@ -80,7 +80,7 @@ public function setModuleList(array $module_list = array());
    *   - required_by: An array with the keys being the modules that will not work
    *     without this module.
    *
-   * @see \Drupal\Core\SystemListing
+   * @see \Drupal\Core\Extension\ExtensionDiscovery
    */
   public function buildModuleDependencies(array $modules);
 
diff --git a/core/lib/Drupal/Core/Extension/ThemeHandler.php b/core/lib/Drupal/Core/Extension/ThemeHandler.php
index 7c71eba..bbae941 100644
--- a/core/lib/Drupal/Core/Extension/ThemeHandler.php
+++ b/core/lib/Drupal/Core/Extension/ThemeHandler.php
@@ -13,7 +13,7 @@
 use Drupal\Core\Config\ConfigFactory;
 use Drupal\Core\Config\ConfigInstallerInterface;
 use Drupal\Core\Routing\RouteBuilder;
-use Drupal\Core\SystemListingInfo;
+use Drupal\Core\Extension\ExtensionDiscovery;
 
 /**
  * Default theme handler using the config system for enabled/disabled themes.
@@ -87,11 +87,11 @@ class ThemeHandler implements ThemeHandlerInterface {
   protected $routeBuilder;
 
   /**
-   * The system listing info
+   * A system listing instance.
    *
-   * @var \Drupal\Core\SystemListingInfo
+   * @var \Drupal\Core\Extension\ExtensionDiscovery
    */
-  protected $systemListingInfo;
+  protected $systemListing;
 
   /**
    * Constructs a new ThemeHandler.
@@ -110,17 +110,17 @@ class ThemeHandler implements ThemeHandlerInterface {
    *   database.
    * @param \Drupal\Core\Routing\RouteBuilder $route_builder
    *   (optional) The route builder to rebuild the routes if a theme is enabled.
-   * @param \Drupal\Core\SystemListingInfo $system_list_info
-   *   (optional) The system listing info.
+   * @param \Drupal\Core\Extension\ExtensionDiscovery $system_list
+   *   (optional) A system listing instance.
    */
-  public function __construct(ConfigFactory $config_factory, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, InfoParserInterface $info_parser, ConfigInstallerInterface $config_installer = NULL, RouteBuilder $route_builder = NULL, SystemListingInfo $system_list_info = NULL) {
+  public function __construct(ConfigFactory $config_factory, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, InfoParserInterface $info_parser, ConfigInstallerInterface $config_installer = NULL, RouteBuilder $route_builder = NULL, ExtensionDiscovery $system_list = NULL) {
     $this->configFactory = $config_factory;
     $this->moduleHandler = $module_handler;
     $this->cacheBackend = $cache_backend;
     $this->infoParser = $info_parser;
     $this->configInstaller = $config_installer;
     $this->routeBuilder = $route_builder;
-    $this->systemListingInfo = $system_list_info;
+    $this->systemListing = $system_list;
   }
 
   /**
@@ -243,24 +243,9 @@ public function reset() {
    * {@inheritdoc}
    */
   public function rebuildThemeData() {
-    // Find themes.
-    $listing = $this->getSystemListingInfo();
-    $themes = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info.yml$/', 'themes', 'name', 1);
-    // Allow modules to add further themes.
-    if ($module_themes = $this->moduleHandler->invokeAll('system_theme_info')) {
-      foreach ($module_themes as $name => $uri) {
-        // @see file_scan_directory()
-        $themes[$name] = (object) array(
-          'uri' => $uri,
-          'filename' => pathinfo($uri, PATHINFO_FILENAME),
-          'name' => $name,
-        );
-      }
-    }
-
-    // Find theme engines.
-    $listing = $this->getSystemListingInfo();
-    $engines = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.engine$/', 'themes/engines', 'name', 1);
+    $listing = $this->getSystemListing();
+    $themes = $listing->scan('themes');
+    $engines = $listing->scan('themes/engines');
 
     // Set defaults for theme info.
     $defaults = array(
@@ -286,44 +271,44 @@ public function rebuildThemeData() {
 
     $sub_themes = array();
     // Read info files for each theme.
-    foreach ($themes as $key => $theme) {
-      $themes[$key]->filename = $theme->uri;
-      $themes[$key]->info = $this->infoParser->parse($theme->uri) + $defaults;
-
-      // Skip this extension if its type is not theme.
-      if (!isset($themes[$key]->info['type']) || $themes[$key]->info['type'] != 'theme') {
-        unset($themes[$key]);
-        continue;
-      }
-
+    foreach ($themes as $key => $fileinfo) {
+      // The theme data is serialized into State. But RecursiveDirectoryIterator
+      // objects are not serializable, so replace the item with a stdClass.
+      $themes[$key] = $theme = (object) array(
+        'uri' => $fileinfo->uri,
+        // @todo Filename should hold filename, not URI.
+        'filename' => $fileinfo->uri,
+        'name' => $fileinfo->name,
+        'info' => $this->infoParser->parse($fileinfo->uri) + $defaults,
+      );
+      $info = &$theme->info;
       // Add the info file modification time, so it becomes available for
       // contributed modules to use for ordering theme lists.
-      $themes[$key]->info['mtime'] = filemtime($theme->uri);
+      $info['mtime'] = $fileinfo->getMTime();
 
       // Invoke hook_system_info_alter() to give installed modules a chance to
       // modify the data in the .info.yml files if necessary.
+      // @todo Remove $type argument, obsolete with $theme->type.
       $type = 'theme';
-      $this->moduleHandler->alter('system_info', $themes[$key]->info, $themes[$key], $type);
+      $this->moduleHandler->alter('system_info', $info, $fileinfo, $type);
 
-      if (!empty($themes[$key]->info['base theme'])) {
+      if (!empty($info['base theme'])) {
         $sub_themes[] = $key;
       }
 
-      $engine = $themes[$key]->info['engine'];
+      $engine = $info['engine'];
       if (isset($engines[$engine])) {
-        $themes[$key]->owner = $engines[$engine]->uri;
-        $themes[$key]->prefix = $engines[$engine]->name;
-        $themes[$key]->template = TRUE;
+        $theme->owner = $engines[$engine]->uri;
+        $theme->prefix = $engines[$engine]->name;
+        $theme->template = TRUE;
       }
 
-      // Prefix stylesheets and scripts with module path.
-      $path = dirname($theme->uri);
-      $theme->info['stylesheets'] = $this->themeInfoPrefixPath($theme->info['stylesheets'], $path);
-      $theme->info['scripts'] = $this->themeInfoPrefixPath($theme->info['scripts'], $path);
-
-      // Give the screenshot proper path information.
-      if (!empty($themes[$key]->info['screenshot'])) {
-        $themes[$key]->info['screenshot'] = $path . '/' . $themes[$key]->info['screenshot'];
+      // Prefix stylesheets, scripts, and screenshot with theme path.
+      $path = dirname($fileinfo->uri);
+      $info['stylesheets'] = $this->themeInfoPrefixPath($info['stylesheets'], $path);
+      $info['scripts'] = $this->themeInfoPrefixPath($info['scripts'], $path);
+      if (!empty($info['screenshot'])) {
+        $info['screenshot'] = $path . '/' . $info['screenshot'];
       }
     }
 
@@ -436,23 +421,23 @@ protected function doGetBaseThemes(array $themes, $theme, $used_themes = array()
         return array($base_key => NULL);
       }
       $used_themes[$base_key] = TRUE;
-      return $this->getBaseThemes($themes, $base_key, $used_themes) + $current_base_theme;
+      return $this->doGetBaseThemes($themes, $base_key, $used_themes) + $current_base_theme;
     }
     // If we get here, then this is our parent theme.
     return $current_base_theme;
   }
 
   /**
-   * Returns a system listing info object.
+   * Returns a system listing object.
    *
-   * @return \Drupal\Core\SystemListingInfo
+   * @return \Drupal\Core\Extension\ExtensionDiscovery
    *   The system listing object.
    */
-  protected function getSystemListingInfo() {
-    if (!isset($this->systemListingInfo)) {
-      $this->systemListingInfo = new SystemListingInfo();
+  protected function getSystemListing() {
+    if (!isset($this->systemListing)) {
+      $this->systemListing = new ExtensionDiscovery();
     }
-    return $this->systemListingInfo;
+    return $this->systemListing;
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Extension/UpdateModuleHandler.php b/core/lib/Drupal/Core/Extension/UpdateModuleHandler.php
index 65f26a0..ad30760 100644
--- a/core/lib/Drupal/Core/Extension/UpdateModuleHandler.php
+++ b/core/lib/Drupal/Core/Extension/UpdateModuleHandler.php
@@ -46,8 +46,6 @@ public function getImplementations($hook) {
       case 'stream_wrappers':
         return array('system');
 
-      // This is called during rebuild to find testing themes.
-      case 'system_theme_info':
       // Those are needed by user_access() to check access on update.php.
       case 'entity_info':
       case 'entity_load':
diff --git a/core/lib/Drupal/Core/SystemListing.php b/core/lib/Drupal/Core/SystemListing.php
deleted file mode 100644
index 20d95cb..0000000
--- a/core/lib/Drupal/Core/SystemListing.php
+++ /dev/null
@@ -1,208 +0,0 @@
-<?php
-
-/**
- * @file
- * Definition of Drupal\Core\SystemListing.
- */
-
-namespace Drupal\Core;
-
-/**
- * Returns information about system object files (modules, themes, etc.).
- *
- * This class requires the list of profiles to be scanned (see
- * \Drupal\Core\SystemListing::scan) to be passed into the constructor. Also,
- * info files are not parsed.
- */
-class SystemListing {
-
-  /**
-   * Construct this listing object.
-   *
-   * @param array $profiles
-   *   A list of profiles to search their directories for in addition to the
-   *   default directories.
-   */
-  function __construct($profiles = array()) {
-    $this->profiles = $profiles;
-  }
-
-  /**
-   * Returns information about system object files (modules, themes, etc.).
-   *
-   * This function is used to find all or some system object files (module
-   * files, theme files, etc.) that exist on the site. It searches in several
-   * locations, depending on what type of object you are looking for. For
-   * instance, if you are looking for modules and call:
-   * @code
-   * $scanner = new SystemListing();
-   * $all_modules = $scanner->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules');
-   * @endcode
-   * this function will search:
-   * - the core modules directory; i.e., /core/modules
-   * - the profiles directories as defined by the profiles() method.
-   * - the site-wide modules directory; i.e., /modules
-   * - the all-sites directory; i.e., /sites/all/modules
-   * - the site-specific directory; i.e., /sites/example.com/modules
-   * in that order, and return information about all of the files ending in
-   * .module in those directories.
-   *
-   * The information is returned in an associative array, which can be keyed
-   * on the file name ($key = 'filename'), the file name without the extension
-   * ($key = 'name'), or the full file stream URI ($key = 'uri'). If you use a
-   * key of 'filename' or 'name', files found later in the search will take
-   * precedence over files found earlier (unless they belong to a module or
-   * theme not compatible with Drupal core); if you choose a key of 'uri',
-   * you will get all files found.
-   *
-   * @param string $mask
-   *   The preg_match() regular expression for the files to find. The
-   *   expression must be anchored and use DRUPAL_PHP_FUNCTION_PATTERN for the
-   *   file name part before the extension, since the results could contain
-   *   matches that do not present valid Drupal extensions otherwise.
-   * @param string $directory
-   *   The subdirectory name in which the files are found. For example,
-   *   'modules' will search all 'modules' directories and their
-   *   sub-directories as explained above.
-   * @param string $key
-   *   (optional) The key to be used for the associative array returned.
-   *   Possible values are:
-   *   - 'uri' for the file's URI.
-   *   - 'filename' for the basename of the file.
-   *   - 'name' for the name of the file without the extension.
-   *   For 'name' and 'filename' only the highest-precedence file is returned.
-   *   Defaults to 'name'.
-   *
-   * @return array
-   *   An associative array of file objects, keyed on the chosen key. Each
-   *   element in the array is an object containing file information, with
-   *   properties:
-   *   - 'uri': Full URI of the file.
-   *   - 'filename': File name.
-   *   - 'name': Name of file without the extension.
-   */
-  function scan($mask, $directory, $key = 'name') {
-    if (!in_array($key, array('uri', 'filename', 'name'))) {
-      $key = 'uri';
-    }
-    $config = conf_path();
-
-    // Search for the directory in core.
-    $searchdir = array('core/' . $directory);
-    foreach ($this->profiles($directory) as $profile) {
-      $searchdir[] = $profile;
-    }
-
-    // Always search for contributed and custom extensions in top-level
-    // directories as well as sites/all/* directories. If the same extension is
-    // located in both directories, then the latter wins for legacy/historical
-    // reasons.
-    $searchdir[] = $directory;
-    $searchdir[] = 'sites/all/' . $directory;
-
-    if (file_exists("$config/$directory")) {
-      $searchdir[] = "$config/$directory";
-    }
-    // @todo Find a way to skip ./config directories (but not modules/config).
-    $nomask = '/^(CVS|lib|templates|css|js)$/';
-    $files = array();
-    // Get current list of items.
-    foreach ($searchdir as $dir) {
-      $files = array_merge($files, $this->process($files, $this->scanDirectory($dir, $key, $mask, $nomask)));
-    }
-    return $files;
-  }
-
-  /**
-   * List the profiles for this directory.
-   *
-   * This version only returns those passed to the constructor.
-   *
-   * @param string $directory
-   *   The current search directory like 'modules' or 'themes'.
-   *
-   * @return array
-   *   A list of profiles.
-   */
-  protected function profiles($directory) {
-    return $this->profiles;
-  }
-
-  /**
-   * Process the files to add before adding them.
-   *
-   * @param array $files
-   *   Every file found so far.
-   * @param array $files_to_add
-   *   The files found in a single directory.
-   *
-   * @return array
-   *   The processed list of file objects. For example, the SystemListingInfo
-   *   class removes files not compatible with the current core version.
-   */
-  protected function process(array $files, array $files_to_add) {
-    return $files_to_add;
-  }
-
-  /**
-   * Abbreviated version of file_scan_directory().
-   *
-   * @param $dir
-   *   The base directory or URI to scan, without trailing slash.
-   * @param $key
-   *   The key to be used for the returned associative array of files.
-   *     Possible values are 'uri', for the file's URI; 'filename', for the
-   *     basename of the file; and 'name' for the name of the file without the
-   *     extension.
-   * @param $mask
-   *   The preg_match() regular expression of the files to find.
-   * @param $nomask
-   *   The preg_match() regular expression of the files to ignore.
-   *
-   * @return array
-   *   An associative array (keyed on the chosen key) of objects with 'uri',
-   *   'filename', and 'name' members corresponding to the matching files.
-   */
-  protected function scanDirectory($dir, $key, $mask, $nomask) {
-    $files = array();
-    if (is_dir($dir)) {
-      // Avoid warnings when opendir does not have the permissions to open a
-      // directory.
-      if ($handle = @opendir($dir)) {
-        while (FALSE !== ($filename = readdir($handle))) {
-          // Skip this file if it matches the nomask or starts with a dot.
-          if ($filename[0] != '.' && !preg_match($nomask, $filename)) {
-            $uri = "$dir/$filename";
-            if (is_dir($uri)) {
-              // Give priority to files in this folder by merging them in after
-              // any subdirectory files.
-              $files = array_merge($this->scanDirectory($uri, $key, $mask, $nomask), $files);
-            }
-            elseif (preg_match($mask, $filename)) {
-              // Always use this match over anything already set in $files with
-              // the same $options['key'].
-              $file = new \stdClass();
-              $file->uri = $uri;
-              $file->filename = $filename;
-              $file->name = pathinfo($filename, PATHINFO_FILENAME);
-              $this->processFile($file);
-              $files[$file->$key] = $file;
-            }
-          }
-        }
-        closedir($handle);
-      }
-    }
-    return $files;
-  }
-
-  /**
-   * Process each file object as it is found by scanDirectory().
-   *
-   * @param $file
-   *   A file object.
-   */
-  protected function processFile($file) {
-  }
-
-}
diff --git a/core/lib/Drupal/Core/SystemListingInfo.php b/core/lib/Drupal/Core/SystemListingInfo.php
deleted file mode 100644
index d302699..0000000
--- a/core/lib/Drupal/Core/SystemListingInfo.php
+++ /dev/null
@@ -1,79 +0,0 @@
-<?php
-
-/**
- * @file
- * Definition of Drupal\Core\SystemListingInfo.
- */
-
-namespace Drupal\Core;
-
-/**
- * Returns information about system object files (modules, themes, etc.).
- *
- * This class finds the profile directories itself and also parses info files.
- */
-class SystemListingInfo extends SystemListing {
-
-  /**
-   * Overrides Drupal\Core\SystemListing::profiles().
-   */
-  protected function profiles($directory) {
-    $searchdir = array();
-    // The 'core/profiles' directory contains pristine collections of modules
-    // and themes as provided by a distribution. It is pristine in the same
-    // way that the 'core/modules' directory is pristine for core; users
-    // should avoid any modification by using the top-level or sites/<domain>
-    // directories.
-    $profile = drupal_get_profile();
-    // For SimpleTest to be able to test modules packaged together with a
-    // distribution we need to include the profile of the parent site (in
-    // which test runs are triggered).
-    if (drupal_valid_test_ua() && !drupal_installation_attempted()) {
-      $testing_profile = \Drupal::config('simpletest.settings')->get('parent_profile');
-      if ($testing_profile && $testing_profile != $profile) {
-        $searchdir[] = drupal_get_path('profile', $testing_profile) . '/' . $directory;
-      }
-    }
-    // In case both profile directories contain the same extension, the actual
-    // profile always has precedence.
-    $searchdir[] = drupal_get_path('profile', $profile) . '/' . $directory;
-    return $searchdir;
-  }
-
-  /**
-   * Overrides Drupal\Core\SystemListing::process().
-   */
-  protected function process(array $files, array $files_to_add) {
-    // Duplicate files found in later search directories take precedence over
-    // earlier ones, so we want them to overwrite keys in our resulting
-    // $files array.
-    // The exception to this is if the later file is from a module or theme not
-    // compatible with Drupal core. This may occur during upgrades of Drupal
-    // core when new modules exist in core while older contrib modules with the
-    // same name exist in a directory such as /modules.
-    foreach (array_intersect_key($files_to_add, $files) as $file_key => $file) {
-      // If it has no info file, then we just behave liberally and accept the
-      // new resource on the list for merging.
-      if (file_exists($info_file = dirname($file->uri) . '/' . $file->name . '.info.yml')) {
-        // Get the .info.yml file for the module or theme this file belongs to.
-        $info = \Drupal::service('info_parser')->parse($info_file);
-
-        // If the module or theme is incompatible with Drupal core, remove it
-        // from the array for the current search directory, so it is not
-        // overwritten when merged with the $files array.
-        if (isset($info['core']) && $info['core'] != \Drupal::CORE_COMPATIBILITY) {
-          unset($files_to_add[$file_key]);
-        }
-      }
-    }
-    return $files_to_add;
-  }
-
-  /**
-   * Overrides Drupal\Core\SystemListing::processFile().
-   */
-  protected function processFile($file) {
-    $file->name = basename($file->name, '.info');
-  }
-
-}
diff --git a/core/modules/block/tests/modules/block_test/block_test.module b/core/modules/block/tests/modules/block_test/block_test.module
index 4370202..1530f2c 100644
--- a/core/modules/block/tests/modules/block_test/block_test.module
+++ b/core/modules/block/tests/modules/block_test/block_test.module
@@ -6,14 +6,6 @@
  */
 
 /**
- * Implements hook_system_theme_info().
- */
-function block_test_system_theme_info() {
-  $themes['block_test_theme'] = drupal_get_path('module', 'block_test') . '/themes/block_test_theme/block_test_theme.info.yml';
-  return $themes;
-}
-
-/**
  * Implements hook_block_alter().
  */
 function block_test_block_alter(&$block_info) {
diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointThemeTest.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointThemeTest.php
index ed88881..153df41 100644
--- a/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointThemeTest.php
+++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointThemeTest.php
@@ -15,13 +15,6 @@
  */
 class BreakpointThemeTest extends BreakpointGroupTestBase {
 
-  /**
-   * Modules to enable.
-   *
-   * @var array
-   */
-  public static $modules = array('breakpoint_theme_test');
-
   public static function getInfo() {
     return array(
       'name' => 'Breakpoint theme functionality',
diff --git a/core/modules/breakpoint/tests/breakpoint_theme_test.info.yml b/core/modules/breakpoint/tests/breakpoint_theme_test.info.yml
deleted file mode 100644
index f035360..0000000
--- a/core/modules/breakpoint/tests/breakpoint_theme_test.info.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-name: 'Breakpoint theme test'
-type: module
-description: 'Test breakpoints provided by themes'
-package: Other
-version: VERSION
-core: 8.x
-hidden: true
-dependencies:
-  - breakpoint
diff --git a/core/modules/breakpoint/tests/breakpoint_theme_test.module b/core/modules/breakpoint/tests/breakpoint_theme_test.module
deleted file mode 100644
index 765cc50..0000000
--- a/core/modules/breakpoint/tests/breakpoint_theme_test.module
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php
-/**
- * @file
- * Test breakpoint functionality for breakpoints provided by themes.
- */
-
-/**
- * Implements hook_system_theme_info().
- */
-function breakpoint_theme_test_system_theme_info() {
-  $themes['breakpoint_test_theme'] = drupal_get_path('module', 'breakpoint_theme_test') . '/themes/breakpoint_test_theme/breakpoint_test_theme.info.yml';
-  return $themes;
-}
diff --git a/core/modules/config/tests/config_test/lib/Drupal/config_test/TestInstallStorage.php b/core/modules/config/tests/config_test/lib/Drupal/config_test/TestInstallStorage.php
index c940422..4782304 100644
--- a/core/modules/config/tests/config_test/lib/Drupal/config_test/TestInstallStorage.php
+++ b/core/modules/config/tests/config_test/lib/Drupal/config_test/TestInstallStorage.php
@@ -8,6 +8,7 @@
 namespace Drupal\config_test;
 
 use Drupal\Core\Config\InstallStorage;
+use Drupal\Core\Extension\ExtensionDiscovery;
 
 /**
  * Tests configuration of profiles, modules and themes.
@@ -22,9 +23,10 @@ class TestInstallStorage extends InstallStorage {
    */
   protected function getAllFolders() {
     if (!isset($this->folders)) {
-      $this->folders = $this->getComponentNames('profile', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles')));
-      $this->folders += $this->getComponentNames('module', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules', 'name', 0)));
-      $this->folders += $this->getComponentNames('theme', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info.yml$/', 'themes')));
+      $listing = new ExtensionDiscovery();
+      $this->folders = $this->getComponentNames('profile', array_keys($listing->scan('profiles')));
+      $this->folders += $this->getComponentNames('module', array_keys($listing->scan('modules')));
+      $this->folders += $this->getComponentNames('theme', array_keys($listing->scan('themes')));
     }
     return $this->folders;
   }
diff --git a/core/modules/config/tests/config_test/lib/Drupal/config_test/TestSchemaStorage.php b/core/modules/config/tests/config_test/lib/Drupal/config_test/TestSchemaStorage.php
index 8d240b0..bf5a6fc 100644
--- a/core/modules/config/tests/config_test/lib/Drupal/config_test/TestSchemaStorage.php
+++ b/core/modules/config/tests/config_test/lib/Drupal/config_test/TestSchemaStorage.php
@@ -8,6 +8,7 @@
 namespace Drupal\config_test;
 
 use Drupal\Core\Config\Schema\SchemaStorage;
+use Drupal\Core\Extension\ExtensionDiscovery;
 
 /**
  * Tests configuration schemas of profiles, modules and themes.
@@ -29,10 +30,11 @@ public function __construct() {
    */
   protected function getAllFolders() {
     if (!isset($this->folders)) {
+      $listing = new ExtensionDiscovery();
       $this->folders = $this->getBaseDataTypeSchema();
-      $this->folders += $this->getComponentNames('profile', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles')));
-      $this->folders += $this->getComponentNames('module', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules', 'name', 0)));
-      $this->folders += $this->getComponentNames('theme', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info.yml$/', 'themes')));
+      $this->folders += $this->getComponentNames('profile', array_keys($listing->scan('profiles')));
+      $this->folders += $this->getComponentNames('module', array_keys($listing->scan('modules')));
+      $this->folders += $this->getComponentNames('theme', array_keys($listing->scan('themes')));
     }
     return $this->folders;
   }
diff --git a/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module
index ac6fb3f..1e04a19 100644
--- a/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module
+++ b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module
@@ -70,11 +70,3 @@ function config_translation_test_form_config_translation_edit_form_alter(&$form,
     $form['#altered'] = TRUE;
   }
 }
-
-/**
- * Implements hook_system_theme_info().
- */
-function config_translation_test_system_theme_info() {
-  $themes['config_translation_test_theme'] = drupal_get_path('module', 'config_translation') . '/tests/themes/config_translation_test_theme/config_translation_test_theme.info.yml';
-  return $themes;
-}
diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
index b284941..c4eb298 100644
--- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
+++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php
@@ -777,7 +777,7 @@ protected function setUp() {
 
     // Set 'parent_profile' of simpletest to add the parent profile's
     // search path to the child site's search paths.
-    // @see drupal_system_listing()
+    // @see \Drupal\Core\Extension\ExtensionDiscovery::getProfileDirectories()
     \Drupal::config('simpletest.settings')->set('parent_profile', $this->originalProfile)->save();
 
     // Collect modules to install.
diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module
index c6b53bf..b00f11c 100644
--- a/core/modules/simpletest/simpletest.module
+++ b/core/modules/simpletest/simpletest.module
@@ -2,6 +2,7 @@
 
 use Drupal\Core\Database\Database;
 use Drupal\Core\Page\HtmlPage;
+use Drupal\Core\Extension\ExtensionDiscovery;
 use Drupal\simpletest\TestBase;
 use Symfony\Component\Process\PhpExecutableFinder;
 
@@ -444,14 +445,13 @@ function simpletest_log_read($test_id, $prefix, $test_class, $during_test = FALS
  *   @endcode
  */
 function simpletest_test_get_all($module = NULL) {
-  $all_groups = &drupal_static(__FUNCTION__);
+  static $all_groups = array();
   $cid = "simpletest:$module";
 
   if (!isset($all_groups[$cid])) {
     $all_groups[$cid] = array();
     $groups = &$all_groups[$cid];
-    // Make sure that namespaces for disabled modules are registered so that the
-    // checks below will find them.
+    // Register namespaces (extensions are not necessarily enabled).
     simpletest_classloader_register();
 
     // Load test information from cache if available, otherwise retrieve the
@@ -461,22 +461,25 @@ function simpletest_test_get_all($module = NULL) {
     }
     else {
       // Select all PSR-0 classes in the Tests namespace of all modules.
-      $classes = array();
-      $module_data = system_rebuild_module_data();
-      $all_data = $module_data + system_rebuild_theme_data();
-      $all_data += drupal_system_listing('/\.profile$/', 'profiles', 'name');
+      $listing = new ExtensionDiscovery();
+      $all_data = $listing->scan('modules', TRUE);
       // If module is set then we keep only that one module.
       if (isset($module)) {
         $all_data = array(
           $module => $all_data[$module],
         );
       }
+      else {
+        $all_data += $listing->scan('profiles', TRUE);
+        $all_data += $listing->scan('themes', TRUE);
+      }
+      $classes = array();
       foreach ($all_data as $name => $data) {
         // Build directory in which the test files would reside.
         $tests_dir = DRUPAL_ROOT . '/' . dirname($data->uri) . '/lib/Drupal/' . $name . '/Tests';
         // Scan it for test files if it exists.
         if (is_dir($tests_dir)) {
-          $files = file_scan_directory($tests_dir, '/.*\.php/');
+          $files = file_scan_directory($tests_dir, '/\.php$/');
           if (!empty($files)) {
             $basedir = DRUPAL_ROOT . '/' . dirname($data->uri) . '/lib/';
             foreach ($files as $file) {
@@ -546,23 +549,38 @@ function simpletest_test_get_all($module = NULL) {
  * Registers namespaces for disabled modules.
  */
 function simpletest_classloader_register() {
+  // Use the same cache prefix as simpletest_test_get_all().
+  $cid = "simpletest::all";
   // @see drupal_get_filename()
   $types = array(
-    'theme_engine' => array('dir' => 'themes/engines', 'extension' => 'engine'),
-    'module' => array('dir' => 'modules', 'extension' => 'module'),
-    'theme' => array('dir' => 'themes', 'extension' => 'info'),
-    'profile' => array('dir' => 'profiles', 'extension' => 'profile'),
+    'theme_engine' => 'themes/engines',
+    'module' => 'modules',
+    'theme' => 'themes',
+    'profile' => 'profiles',
   );
 
-  $classloader = drupal_classloader();
+  if ($cache = cache()->get($cid)) {
+    $extensions = $cache->data;
+  }
+  else {
+    $listing = new ExtensionDiscovery();
+    $extensions = array();
+    foreach ($types as $type => $dir) {
+      $extensions[$type] = $listing->scan($dir, TRUE);
+      foreach ($extensions[$type] as $name => $file) {
+        $extensions[$type][$name] = $file->uri;
+      }
+    }
+    cache()->set($cid, $extensions);
+  }
 
+  $classloader = drupal_classloader();
   foreach ($types as $type => $info) {
-    $matches = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.' . $info['extension'] . '$/', $info['dir']);
-    foreach ($matches as $name => $file) {
-      drupal_classloader_register($name, dirname($file->uri));
-      $classloader->add('Drupal\\' . $name . '\\Tests', DRUPAL_ROOT . '/' . dirname($file->uri) . '/tests');
+    foreach ($extensions[$type] as $name => $uri) {
+      drupal_classloader_register($name, dirname($uri));
+      $classloader->add('Drupal\\' . $name . '\\Tests', DRUPAL_ROOT . '/' . dirname($uri) . '/tests');
       // While being there, prime drupal_get_filename().
-      drupal_get_filename($type, $name, $file->uri);
+      drupal_get_filename($type, $name, $uri);
     }
   }
 
diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/SystemListingTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/SystemListingTest.php
index ee1f12b..937d7f8 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Common/SystemListingTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Common/SystemListingTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\system\Tests\Common;
 
+use Drupal\Core\Extension\ExtensionDiscovery;
 use Drupal\simpletest\WebTestBase;
 
 /**
@@ -55,7 +56,8 @@ function testDirectoryPrecedence() {
 
     // Now scan the directories and check that the files take precedence as
     // expected.
-    $files = drupal_system_listing('/\.module$/', 'modules');
+    $listing = new ExtensionDiscovery();
+    $files = $listing->scan('modules');
     foreach ($expected_directories as $module => $directories) {
       $expected_directory = array_shift($directories);
       $expected_filename = "$expected_directory/$module/$module.module";
diff --git a/core/modules/system/lib/Drupal/system/Tests/Extension/InfoParserUnitTest.php b/core/modules/system/lib/Drupal/system/Tests/Extension/InfoParserUnitTest.php
index e9cf688..8be4af9 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Extension/InfoParserUnitTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Extension/InfoParserUnitTest.php
@@ -72,7 +72,7 @@ public function testInfoParser() {
       $this->fail('Expected InfoParserException not thrown when reading missing_keys.info.txt');
     }
     catch (InfoParserException $e) {
-      $expected_message = 'Missing required keys (name, type) in core/modules/system/tests/fixtures/missing_keys.info.txt.';
+      $expected_message = "Missing required keys (type, core, name) in $filename.";
       $this->assertEqual($e->getMessage(), $expected_message);
     }
 
@@ -83,7 +83,7 @@ public function testInfoParser() {
       $this->fail('Expected InfoParserException not thrown when reading missing_key.info.txt');
     }
     catch (InfoParserException $e) {
-      $expected_message = 'Missing required key (type) in core/modules/system/tests/fixtures/missing_key.info.txt.';
+      $expected_message = "Missing required key (type) in $filename.";
       $this->assertEqual($e->getMessage(), $expected_message);
     }
 
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index 6c83611..e42c03c 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -1191,25 +1191,6 @@ function hook_system_breadcrumb_alter(array &$breadcrumb, array $attributes, arr
 }
 
 /**
- * Return additional themes provided by modules.
- *
- * Only use this hook for testing purposes. Use a hidden MYMODULE_test.module
- * to implement this hook. Testing themes should be hidden, too.
- *
- * This hook is invoked from _system_rebuild_theme_data() and allows modules to
- * register additional themes outside of the regular 'themes' directories of a
- * Drupal installation.
- *
- * @return
- *   An associative array. Each key is the system name of a theme and each value
- *   is the corresponding path to the theme's .info.yml file.
- */
-function hook_system_theme_info() {
-  $themes['mymodule_test_theme'] = drupal_get_path('module', 'mymodule') . '/mymodule_test_theme/mymodule_test_theme.info.yml';
-  return $themes;
-}
-
-/**
  * Alter the information parsed from module and theme .info.yml files
  *
  * This hook is invoked in _system_rebuild_module_data() and in
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index cc647a2..7f3704b 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -8,6 +8,7 @@
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Language\Language;
+use Drupal\Core\Extension\ExtensionDiscovery;
 use Drupal\Core\Utility\ModuleInfo;
 use Drupal\menu_link\MenuLinkInterface;
 use Drupal\user\UserInterface;
@@ -2579,18 +2580,19 @@ function system_get_module_info($property) {
  *   An associative array of module information.
  */
 function _system_rebuild_module_data() {
+  $listing = new ExtensionDiscovery();
   // Find modules
-  $modules = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules', 'name', 0);
+  $modules = $listing->scan('modules');
 
   // Find installation profiles.
-  $profiles = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles', 'name', 0);
+  $profiles = $listing->scan('profiles');
 
   // Include the installation profile in modules that are loaded.
-  $profile = drupal_get_profile();
-  $modules[$profile] = $profiles[$profile];
-
-  // Installation profile hooks are always executed last.
-  $modules[$profile]->weight = 1000;
+  if ($profile = drupal_get_profile()) {
+    $modules[$profile] = $profiles[$profile];
+    // Installation profile hooks are always executed last.
+    $modules[$profile]->weight = 1000;
+  }
 
   // Set defaults for module info.
   $defaults = array(
@@ -2643,7 +2645,7 @@ function _system_rebuild_module_data() {
   }
 
 
-  if (isset($modules[$profile])) {
+  if ($profile && isset($modules[$profile])) {
     // The installation profile is required, if it's a valid module.
     $modules[$profile]->info['required'] = TRUE;
     // Add a default distribution name if the profile did not provide one. This
diff --git a/core/modules/system/tests/fixtures/common_test.info.txt b/core/modules/system/tests/fixtures/common_test.info.txt
index ae97b16..7e57dfe 100644
--- a/core/modules/system/tests/fixtures/common_test.info.txt
+++ b/core/modules/system/tests/fixtures/common_test.info.txt
@@ -1,3 +1,4 @@
+core: 8.x
 name: common_test
 type: module
 description: 'testing info file parsing'
diff --git a/core/modules/system/tests/fixtures/missing_keys.info.txt b/core/modules/system/tests/fixtures/missing_keys.info.txt
index 56c6411..c04f34d 100644
--- a/core/modules/system/tests/fixtures/missing_keys.info.txt
+++ b/core/modules/system/tests/fixtures/missing_keys.info.txt
@@ -1,6 +1,5 @@
 # info.yml for testing missing name, description, and type keys.
 package: Core
 version: VERSION
-core: 8.x
 dependencies:
   - field
diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.module b/core/modules/system/tests/modules/ajax_test/ajax_test.module
index 2ea9eb9..8fb99e1 100644
--- a/core/modules/system/tests/modules/ajax_test/ajax_test.module
+++ b/core/modules/system/tests/modules/ajax_test/ajax_test.module
@@ -13,14 +13,6 @@
 use Drupal\Core\Ajax\HtmlCommand;
 
 /**
- * Implements hook_system_theme_info().
- */
-function ajax_test_system_theme_info() {
-  $themes['test_theme'] = drupal_get_path('module', 'system') . '/tests/themes/test_theme/test_theme.info.yml';
-  return $themes;
-}
-
-/**
  * Menu callback: Returns an element suitable for use by ajax_render().
  *
  * Additionally ensures that ajax_render() incorporates JavaScript settings
diff --git a/core/modules/system/tests/modules/theme_page_test/theme_page_test.module b/core/modules/system/tests/modules/theme_page_test/theme_page_test.module
index 6fdd7bd..1180d64 100644
--- a/core/modules/system/tests/modules/theme_page_test/theme_page_test.module
+++ b/core/modules/system/tests/modules/theme_page_test/theme_page_test.module
@@ -9,13 +9,3 @@ function theme_page_test_system_info_alter(&$info, $file, $type) {
     unset($info['hidden']);
   }
 }
-
-
-/**
- * Implements hook_system_theme_info().
- */
-function theme_page_test_system_theme_info() {
-  $themes['test_invalid_basetheme'] = drupal_get_path('module', 'system') . '/tests/themes/test_invalid_basetheme/test_invalid_basetheme.info.yml';
-  $themes['test_invalid_engine'] = drupal_get_path('module', 'system') . '/tests/themes/test_invalid_engine/test_invalid_engine.info.yml';
-  return $themes;
-}
diff --git a/core/modules/system/tests/modules/theme_test/theme_test.module b/core/modules/system/tests/modules/theme_test/theme_test.module
index bdb1404..cd4c646 100644
--- a/core/modules/system/tests/modules/theme_test/theme_test.module
+++ b/core/modules/system/tests/modules/theme_test/theme_test.module
@@ -53,17 +53,6 @@ function theme_test_theme($existing, $type, $theme, $path) {
 }
 
 /**
- * Implements hook_system_theme_info().
- */
-function theme_test_system_theme_info() {
-  $themes['test_theme'] = drupal_get_path('module', 'system') . '/tests/themes/test_theme/test_theme.info.yml';
-  $themes['test_basetheme'] = drupal_get_path('module', 'system') . '/tests/themes/test_basetheme/test_basetheme.info.yml';
-  $themes['test_subtheme'] = drupal_get_path('module', 'system') . '/tests/themes/test_subtheme/test_subtheme.info.yml';
-  $themes['test_theme_phptemplate'] = drupal_get_path('module', 'system') . '/tests/themes/test_theme_phptemplate/test_theme_phptemplate.info.yml';
-  return $themes;
-}
-
-/**
  * Implements hook_menu().
  */
 function theme_test_menu() {
diff --git a/core/modules/update/tests/modules/update_test/update_test.module b/core/modules/update/tests/modules/update_test/update_test.module
index 249ad83..ed5e008 100644
--- a/core/modules/update/tests/modules/update_test/update_test.module
+++ b/core/modules/update/tests/modules/update_test/update_test.module
@@ -9,15 +9,6 @@
  */
 
 /**
- * Implements hook_system_theme_info().
- */
-function update_test_system_theme_info() {
-  $themes['update_test_basetheme'] = drupal_get_path('module', 'update') . '/tests/themes/update_test_basetheme/update_test_basetheme.info.yml';
-  $themes['update_test_subtheme'] = drupal_get_path('module', 'update') . '/tests/themes/update_test_subtheme/update_test_subtheme.info.yml';
-  return $themes;
-}
-
-/**
  * Implements hook_system_info_alter().
  *
  * Checks the 'update_test.settings:system_info' configuration and sees if we
diff --git a/core/modules/update/update.module b/core/modules/update/update.module
index adf6ba3..aedccd5 100644
--- a/core/modules/update/update.module
+++ b/core/modules/update/update.module
@@ -629,7 +629,7 @@ function theme_update_last_check($variables) {
  * an .info.yml file which claims that the code is compatible with the current
  * version of Drupal core.
  *
- * @see drupal_system_listing()
+ * @see \Drupal\Core\Extension\ExtensionDiscovery::process()
  * @see _system_rebuild_module_data()
  */
 function update_verify_update_archive($project, $archive_file, $directory) {
diff --git a/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php b/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php
index 25901c1..5fc60da 100644
--- a/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php
+++ b/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php
@@ -67,9 +67,9 @@ class ThemeHandlerTest extends UnitTestCase {
   /**
    * The system listing info.
    *
-   * @var \Drupal\Core\SystemListingInfo|\PHPUnit_Framework_MockObject_MockObject
+   * @var \Drupal\Core\Extension\ExtensionDiscovery|\PHPUnit_Framework_MockObject_MockObject
    */
-  protected $systemListingInfo;
+  protected $systemListing;
 
   /**
    * The tested theme handler.
@@ -101,10 +101,10 @@ protected function setUp() {
     $this->routeBuilder = $this->getMockBuilder('Drupal\Core\Routing\RouteBuilder')
       ->disableOriginalConstructor()
       ->getMock();
-    $this->systemListingInfo = $this->getMockBuilder('Drupal\Core\SystemListingInfo')
+    $this->systemListing = $this->getMockBuilder('Drupal\Core\Extension\ExtensionDiscovery')
       ->disableOriginalConstructor()
       ->getMock();
-    $this->themeHandler = new TestThemeHandler($this->configFactory, $this->moduleHandler, $this->cacheBackend, $this->infoParser, $this->configInstaller, $this->routeBuilder, $this->systemListingInfo);
+    $this->themeHandler = new TestThemeHandler($this->configFactory, $this->moduleHandler, $this->cacheBackend, $this->infoParser, $this->configInstaller, $this->routeBuilder, $this->systemListing);
 
     $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
     $container->expects($this->any())
@@ -154,16 +154,11 @@ public function testEnableSingleTheme() {
       ->expects($this->once())
       ->method('save');
 
-    $this->systemListingInfo->expects($this->any())
+    $this->systemListing->expects($this->any())
       ->method('scan')
       ->will($this->returnValue(array()));
 
     // Ensure that the themes_enabled hook is fired.
-    $this->moduleHandler->expects($this->at(0))
-      ->method('invokeAll')
-      ->with('system_theme_info')
-      ->will($this->returnValue(array()));
-
     $this->moduleHandler->expects($this->at(1))
       ->method('invokeAll')
       ->with('themes_enabled', array($theme_list));
@@ -195,7 +190,7 @@ public function testEnableAndListInfo() {
       ->method('clear')
       ->will($this->returnSelf());
 
-    $this->systemListingInfo->expects($this->any())
+    $this->systemListing->expects($this->any())
       ->method('scan')
       ->will($this->returnValue(array()));
 
@@ -255,9 +250,9 @@ public function testEnableAndListInfo() {
    * @see \Drupal\Core\Extension\ThemeHandler::rebuildThemeData()
    */
   public function testRebuildThemeData() {
-    $this->systemListingInfo->expects($this->at(0))
+    $this->systemListing->expects($this->at(0))
       ->method('scan')
-      ->with($this->anything(), 'themes', 'name', 1)
+      ->with($this->anything(), 'themes', 'name')
       ->will($this->returnValue(array(
         'seven' => (object) array(
           'name' => 'seven',
diff --git a/core/themes/engines/phptemplate/phptemplate.info.yml b/core/themes/engines/phptemplate/phptemplate.info.yml
new file mode 100644
index 0000000..2bea7a0
--- /dev/null
+++ b/core/themes/engines/phptemplate/phptemplate.info.yml
@@ -0,0 +1,5 @@
+type: theme_engine
+name: PHPTemplate
+core: 8.x
+version: VERSION
+package: Core
diff --git a/core/themes/engines/twig/twig.info.yml b/core/themes/engines/twig/twig.info.yml
new file mode 100644
index 0000000..1767384
--- /dev/null
+++ b/core/themes/engines/twig/twig.info.yml
@@ -0,0 +1,5 @@
+type: theme_engine
+name: Twig
+core: 8.x
+version: VERSION
+package: Core
