diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php
index 0cb00a6..6fd9ce5 100644
--- a/core/lib/Drupal/Core/Theme/Registry.php
+++ b/core/lib/Drupal/Core/Theme/Registry.php
@@ -632,6 +632,30 @@ protected function completeSuggestion($hook, array &$cache) {
   }
 
   /**
+   * Merges the preceding hook's preprocess functions into the current hook's.
+   *
+   * @param string $hook
+   *   The name of the suggestion hook to complete.
+   * @param string $complete_hook
+   *   The name of the hook to merge preprocess functions from.
+   * @param array $cache
+   *   The theme registry, as documented in
+   *   \Drupal\Core\Theme\Registry::processExtension().
+   */
+  protected function mergePreprocessFunctions($hook, $complete_hook, array &$cache) {
+    // A suggestion might be defined in a hook_theme implementation and
+    // include preprocess functions not following the pattern so we merge.
+    if (isset($cache[$hook]['preprocess functions'])) {
+      $preprocess_functions = array_merge($cache[$complete_hook]['preprocess functions'], $cache[$hook]['preprocess functions']);
+    }
+    else {
+      $preprocess_functions = $cache[$complete_hook]['preprocess functions'];
+    }
+
+    $cache[$hook]['preprocess functions'] = $preprocess_functions;
+  }
+
+  /**
    * Completes the theme registry adding discovered functions and hooks.
    *
    * @param array $cache
@@ -710,7 +734,15 @@ protected function postProcessExtension(array &$cache, ActiveTheme $theme) {
       // from a pattern. This is typically set from
       // drupal_find_theme_functions() and drupal_find_theme_templates().
       if (isset($info['incomplete preprocess functions'])) {
-        $this->completeSuggestion($hook, $cache);
+        // Suggestion hooks inherit from their preceding suggestion hooks first
+        // whereas hooks with a manually set "base hook" key can just be
+        // completed based on the hook given.
+        if (strpos($hook, '__')) {
+          $this->completeSuggestion($hook, $cache);
+        }
+        else {
+          $this->mergePreprocessFunctions($hook, $info['base hook'], $cache);
+        }
         unset($cache[$hook]['incomplete preprocess functions']);
       }
 
diff --git a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php
index 1243118..79ca932 100644
--- a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php
+++ b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php
@@ -67,6 +67,20 @@ class RegistryTest extends UnitTestCase {
   protected $themeManager;
 
   /**
+   * The theme.
+   *
+   * @var \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $theme;
+
+  /**
+   * The list of functions that get_defined_functions() should provide.
+   *
+   * @var array
+   */
+  public static $functions = [];
+
+  /**
    * {@inheritdoc}
    */
   protected function setUp() {
@@ -83,6 +97,14 @@ protected function setUp() {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  protected function tearDown() {
+    parent::tearDown();
+    static::$functions = [];
+  }
+
+  /**
    * Tests getting the theme registry defined by a module.
    */
   public function testGetRegistryForModule() {
@@ -159,6 +181,137 @@ public function testGetRegistryForModule() {
     $this->assertTrue(in_array('test_stable_preprocess_theme_test_render_element', $other_registry['theme_test_render_element']['preprocess functions']));
   }
 
+  /**
+   * Sets up the mocks needed for testing Registry::postProcessExtension.
+   */
+  public function setUpPostProcessExtension() {
+    $this->theme = $this->getMockBuilder(ActiveTheme::class)
+      ->disableOriginalConstructor()
+      ->getMock();
+    $this->theme->expects($this->atLeastOnce())
+      ->method('getBaseThemes')
+      ->will($this->returnValue([]));
+    $this->theme->expects($this->atLeastOnce())
+      ->method('getName')
+      ->will($this->returnValue('test'));
+
+    $this->moduleHandler->expects($this->atLeastOnce())
+      ->method('getModuleList')
+      ->willReturn([]);
+  }
+
+  /**
+   * Tests that preprocess functions are added when base hook is specified.
+   *
+   * @covers ::postProcessExtension
+   */
+  public function testManualBaseHook() {
+    static::$functions['user'] = ['cat', 'mouse', 'cheese'];
+
+    // Define a test case registry with mock data.
+    $hooks = [
+      'test_hook' => [
+        'preprocess functions' => ['cat', 'mouse'],
+      ],
+      'test_hook_manual' => [
+        'preprocess functions' => ['cheese'],
+        'base hook' => 'test_hook',
+        'incomplete preprocess functions' => TRUE,
+      ],
+    ];
+
+    $expected = [
+      'test_hook' => [
+        'preprocess functions' => ['cat', 'mouse'],
+      ],
+      'test_hook_manual' => [
+        'preprocess functions' => ['cat', 'mouse', 'cheese'],
+        'base hook' => 'test_hook',
+      ],
+    ];
+
+    $this->setUpPostProcessExtension();
+
+    $class = new \ReflectionClass(TestRegistry::class);
+    $reflection_method = $class->getMethod('postProcessExtension');
+    $reflection_method->setAccessible(TRUE);
+    $reflection_method->invokeArgs($this->registry, [ &$hooks, $this->theme]);
+
+    $this->assertArrayEquals($hooks, $expected);
+  }
+
+  /**
+   * Tests that a suggestion defined in hook_theme gets preprocess functions.
+   *
+   * @covers ::postProcessExtension
+   */
+  public function testManualSuggestion() {
+    $this->setUpPostProcessExtension();
+
+    $hooks = [
+      'test_hook' => [
+        'preprocess functions' => ['cat', 'mouse'],
+        'template' => 'test-hook',
+      ],
+      'test_hook__suggestion' => [
+        'preprocess functions' => ['bread'],
+        'incomplete preprocess functions' => TRUE,
+      ],
+      'test_hook__suggestion__another' => [
+        'preprocess functions' => ['cheese'],
+        'incomplete preprocess functions' => TRUE,
+      ],
+    ];
+
+    $expected = [
+      'test_hook' => [
+        'preprocess functions' => [
+          'cat',
+          'mouse',
+        ],
+        'template' => 'test-hook',
+      ],
+      'test_hook__suggestion' => [
+        'preprocess functions' => [
+          'cat',
+          'mouse',
+          'bread',
+          'test_preprocess_test_hook__suggestion',
+        ],
+        'base hook' => 'test_hook',
+        'template' => 'test-hook',
+      ],
+      'test_hook__suggestion__another' => [
+        'preprocess functions' => [
+          'cat',
+          'mouse',
+          'bread',
+          'test_preprocess_test_hook__suggestion',
+          'cheese',
+          'test_preprocess_test_hook__suggestion__another',
+        ],
+        'base hook' => 'test_hook',
+        'template' => 'test-hook',
+      ],
+    ];
+
+    static::$functions['user'] = [
+      'cat',
+      'mouse',
+      'cheese',
+      'bread',
+      'test_preprocess_test_hook__suggestion',
+      'test_preprocess_test_hook__suggestion__another',
+    ];
+
+    $class = new \ReflectionClass(TestRegistry::class);
+    $reflection_method = $class->getMethod('postProcessExtension');
+    $reflection_method->setAccessible(TRUE);
+    $reflection_method->invokeArgs($this->registry, [&$hooks, $this->theme]);
+
+    $this->assertArrayEquals($hooks, $expected);
+  }
+
   protected function setupTheme() {
     $this->registry = new TestRegistry($this->root, $this->cache, $this->lock, $this->moduleHandler, $this->themeHandler, $this->themeInitialization);
     $this->registry->setThemeManager($this->themeManager);
@@ -175,3 +328,14 @@ protected function getPath($module) {
   }
 
 }
+
+namespace Drupal\Core\Theme;
+
+use Drupal\Tests\Core\Theme\RegistryTest;
+
+/**
+ * Overrides get_defined_functions() with a configurable mock.
+ */
+function get_defined_functions() {
+  return RegistryTest::$functions ?: \get_defined_functions();
+}
