From 8af521051eb79d977a391931ee18f69a30f62b0a Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Wed, 1 May 2024 16:40:47 +0100
Subject: [PATCH] Issue #3444241 by alexpott, phenaproxima: Recipe
 disambiguation of recipes from other locations

(cherry picked from commit 6895717bf0869768fdfe74911672da13ba85eb3d)
---
 core/lib/Drupal/Core/Recipe/Recipe.php        |  7 +--
 .../Drupal/Core/Recipe/RecipeDiscovery.php    | 43 ++++++++++--------
 core/lib/Drupal/Core/Recipe/RecipeRunner.php  |  6 +--
 .../Core/Recipe/UnknownRecipeException.php    |  6 +--
 .../AddModerationConfigActionTest.php         |  2 +-
 .../Core/Recipe/RecipeCommandTest.php         |  6 +--
 .../Core/Recipe/RecipeTestTrait.php           | 14 ++++--
 .../Core/Recipe/RecipeConfiguratorTest.php    |  8 ++--
 .../Core/Recipe/RecipeDiscoveryTest.php       | 13 ++----
 .../Core/Recipe/RecipeEventsTest.php          |  2 +-
 .../Core/Recipe/RecipeRunnerTest.php          | 45 ++++++++++++++++---
 phpstan-level9-baseline.neon                  | 19 +++++---
 12 files changed, 110 insertions(+), 61 deletions(-)

diff --git a/core/lib/Drupal/Core/Recipe/Recipe.php b/core/lib/Drupal/Core/Recipe/Recipe.php
index 1baa8b9dec20..4da07d2743e5 100644
--- a/core/lib/Drupal/Core/Recipe/Recipe.php
+++ b/core/lib/Drupal/Core/Recipe/Recipe.php
@@ -40,7 +40,8 @@ public function __construct(
     public readonly RecipeConfigurator $recipes,
     public readonly InstallConfigurator $install,
     public readonly ConfigConfigurator $config,
-    public readonly Finder $content
+    public readonly Finder $content,
+    public readonly string $path,
   ) {
   }
 
@@ -61,7 +62,7 @@ public static function createFromDirectory(string $path): static {
     $install = new InstallConfigurator($recipe_data['install'], \Drupal::service('extension.list.module'), \Drupal::service('extension.list.theme'));
     $config = new ConfigConfigurator($recipe_data['config'], $path, \Drupal::service('config.storage'));
     $content = new Finder($path . '/content');
-    return new static($recipe_data['name'], $recipe_data['description'], $recipe_data['type'], $recipes, $install, $config, $content);
+    return new static($recipe_data['name'], $recipe_data['description'], $recipe_data['type'], $recipes, $install, $config, $content, $path);
   }
 
   /**
@@ -255,7 +256,7 @@ private static function validateRecipeExists(string $name, ExecutionContextInter
    * @return \Drupal\Core\Recipe\RecipeDiscovery
    */
   private static function getRecipeDiscovery(string $recipeDirectory): RecipeDiscovery {
-    return new RecipeDiscovery([$recipeDirectory]);
+    return new RecipeDiscovery($recipeDirectory);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Recipe/RecipeDiscovery.php b/core/lib/Drupal/Core/Recipe/RecipeDiscovery.php
index cb6de7a78104..a871dea14de5 100644
--- a/core/lib/Drupal/Core/Recipe/RecipeDiscovery.php
+++ b/core/lib/Drupal/Core/Recipe/RecipeDiscovery.php
@@ -4,8 +4,6 @@
 
 namespace Drupal\Core\Recipe;
 
-use Drupal\Component\Assertion\Inspector;
-
 /**
  * @internal
  *   This API is experimental.
@@ -15,17 +13,15 @@ final class RecipeDiscovery {
   /**
    * Constructs a recipe discovery object.
    *
-   * @param array $paths
-   *   An array of paths where to search for recipes. The path will be searched
-   *   folders containing a recipe.yml. There will be no traversal further into
-   *   the directory structure.
+   * @param string $path
+   *   The path will be searched folders containing a recipe.yml. There will be
+   *   no traversal further into the directory structure.
    */
-  public function __construct(protected readonly array $paths) {
-    assert(Inspector::assertAllStrings($paths), 'Paths must be strings.');
+  public function __construct(protected string $path) {
   }
 
   /**
-   * Constructs a RecipeDiscovery object.
+   * Gets a recipe object.
    *
    * @param string $name
    *   The machine name of the recipe to find.
@@ -37,17 +33,26 @@ public function __construct(protected readonly array $paths) {
    *   Thrown when the recipe cannot be found.
    */
   public function getRecipe(string $name): Recipe {
-    $paths = [
-      ...$this->paths,
-      DRUPAL_ROOT . '/recipes',
-      DRUPAL_ROOT . '/core/recipes',
-    ];
-    foreach ($paths as $path) {
-      if (file_exists($path . DIRECTORY_SEPARATOR . $name . DIRECTORY_SEPARATOR . 'recipe.yml')) {
-        return Recipe::createFromDirectory($path . DIRECTORY_SEPARATOR . $name);
-      }
+    // In order to allow recipes to include core provided recipes, $name can be
+    // a Drupal root relative path to a recipe folder. For example, a recipe can
+    // include the core provided 'article_tags' recipe by listing the recipe as
+    // 'core/recipes/article_tags'. It is strongly recommended not to rely on
+    // relative paths for including recipes. Required recipes should be put in
+    // the same parent directory as the recipe being applied. Note, only linux
+    // style directory separators are supported. PHP on windows can resolve the
+    // mix of directory separators.
+    if (str_contains($name, '/')) {
+      $path = \Drupal::root() . "/$name/recipe.yml";
+    }
+    else {
+      $path = $this->path . "/$name/recipe.yml";
+    }
+
+    if (file_exists($path)) {
+      return Recipe::createFromDirectory(dirname($path));
     }
-    throw new UnknownRecipeException($name, $paths, sprintf("Can not find the %s recipe, search paths: %s", $name, implode(', ', $paths)));
+    $search_path = dirname($path, 2);
+    throw new UnknownRecipeException($name, $search_path, sprintf("Can not find the %s recipe, search path: %s", $name, $search_path));
   }
 
 }
diff --git a/core/lib/Drupal/Core/Recipe/RecipeRunner.php b/core/lib/Drupal/Core/Recipe/RecipeRunner.php
index 0e08d2df16bf..35f2cec47067 100644
--- a/core/lib/Drupal/Core/Recipe/RecipeRunner.php
+++ b/core/lib/Drupal/Core/Recipe/RecipeRunner.php
@@ -151,7 +151,7 @@ public static function toBatchOperations(Recipe $recipe): array {
    * @param \Drupal\Core\Recipe\Recipe $recipe
    *   The recipe to convert to batch operations.
    * @param string[] $recipes
-   *   The recipes that have already been converted to batch operations.
+   *   The paths of the recipes that have already been converted to batch operations.
    * @param string[] $modules
    *   The modules that will already be installed due to previous recipes in the
    *   batch.
@@ -165,11 +165,11 @@ public static function toBatchOperations(Recipe $recipe): array {
    *   pass to the callable.
    */
   protected static function toBatchOperationsRecipe(Recipe $recipe, array $recipes, array &$modules, array &$themes): array {
-    if (in_array($recipe->name, $recipes, TRUE)) {
+    if (in_array($recipe->path, $recipes, TRUE)) {
       return [];
     }
     $steps = [];
-    $recipes[] = $recipe->name;
+    $recipes[] = $recipe->path;
 
     foreach ($recipe->recipes->recipes as $sub_recipe) {
       $steps = array_merge($steps, static::toBatchOperationsRecipe($sub_recipe, $recipes, $modules, $themes));
diff --git a/core/lib/Drupal/Core/Recipe/UnknownRecipeException.php b/core/lib/Drupal/Core/Recipe/UnknownRecipeException.php
index 8bfd61ea03d4..b0f63c002824 100644
--- a/core/lib/Drupal/Core/Recipe/UnknownRecipeException.php
+++ b/core/lib/Drupal/Core/Recipe/UnknownRecipeException.php
@@ -15,8 +15,8 @@ final class UnknownRecipeException extends \RuntimeException {
   /**
    * @param string $recipe
    *   The recipe's name.
-   * @param array $searchPaths
-   *   The paths searched for the recipe.
+   * @param string $searchPath
+   *   The path searched for the recipe.
    * @param string $message
    *   (optional) The exception message.
    * @param int $code
@@ -24,7 +24,7 @@ final class UnknownRecipeException extends \RuntimeException {
    * @param \Throwable|null $previous
    *   (optional) The previous exception.
    */
-  public function __construct(public readonly string $recipe, public readonly array $searchPaths, string $message = "", int $code = 0, ?\Throwable $previous = NULL) {
+  public function __construct(public readonly string $recipe, public readonly string $searchPath, string $message = "", int $code = 0, ?\Throwable $previous = NULL) {
     parent::__construct($message, $code, $previous);
   }
 
diff --git a/core/modules/content_moderation/tests/src/Kernel/ConfigAction/AddModerationConfigActionTest.php b/core/modules/content_moderation/tests/src/Kernel/ConfigAction/AddModerationConfigActionTest.php
index 374ea3e1f0d8..4d5afd395e2a 100644
--- a/core/modules/content_moderation/tests/src/Kernel/ConfigAction/AddModerationConfigActionTest.php
+++ b/core/modules/content_moderation/tests/src/Kernel/ConfigAction/AddModerationConfigActionTest.php
@@ -95,7 +95,7 @@ private function createRecipe(string $config_name): Recipe {
     $recipe = <<<YAML
 name: 'Add entity types and bundles to workflow'
 recipes:
-  - editorial_workflow
+  - core/recipes/editorial_workflow
 config:
   actions:
     $config_name:
diff --git a/core/tests/Drupal/FunctionalTests/Core/Recipe/RecipeCommandTest.php b/core/tests/Drupal/FunctionalTests/Core/Recipe/RecipeCommandTest.php
index 229d4acfdacf..39149729e78e 100644
--- a/core/tests/Drupal/FunctionalTests/Core/Recipe/RecipeCommandTest.php
+++ b/core/tests/Drupal/FunctionalTests/Core/Recipe/RecipeCommandTest.php
@@ -37,7 +37,7 @@ public function testRecipeCommand(): void {
 
     $process = $this->applyRecipe('core/tests/fixtures/recipes/install_node_with_config');
     $this->assertSame(0, $process->getExitCode());
-    $this->assertStringContainsString("6/6 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓]\nApplied Install node with config recipe.", $process->getErrorOutput());
+    $this->assertStringContainsString("Applied Install node with config recipe.", $process->getErrorOutput());
     $this->assertStringContainsString('Install node with config applied successfully', $process->getOutput());
     $this->assertTrue(\Drupal::moduleHandler()->moduleExists('node'), 'The node module is installed');
     $this->assertCheckpointsExist(["Backup before the 'Install node with config' recipe."]);
@@ -45,7 +45,7 @@ public function testRecipeCommand(): void {
     // Ensure recipes can be applied without affecting pre-existing checkpoints.
     $process = $this->applyRecipe('core/tests/fixtures/recipes/install_two_modules');
     $this->assertSame(0, $process->getExitCode());
-    $this->assertStringContainsString("1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓]\nApplied Install two modules recipe.", $process->getErrorOutput());
+    $this->assertStringContainsString("Applied Install two modules recipe.", $process->getErrorOutput());
     $this->assertStringContainsString('Install two modules applied successfully', $process->getOutput());
     $this->assertTrue(\Drupal::moduleHandler()->moduleExists('node'), 'The node module is installed');
     $this->assertCheckpointsExist([
@@ -68,7 +68,7 @@ public function testRecipeCommand(): void {
     \Drupal::service('config.storage.checkpoint')->checkpoint('Test log message');
     $process = $this->applyRecipe('core/tests/fixtures/recipes/no_extensions');
     $this->assertSame(0, $process->getExitCode());
-    $this->assertStringContainsString("1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓]\nApplied No extensions recipe.", $process->getErrorOutput());
+    $this->assertStringContainsString("Applied No extensions recipe.", $process->getErrorOutput());
     $this->assertCheckpointsExist([
       "Backup before the 'Install node with config' recipe.",
       "Backup before the 'Install two modules' recipe.",
diff --git a/core/tests/Drupal/FunctionalTests/Core/Recipe/RecipeTestTrait.php b/core/tests/Drupal/FunctionalTests/Core/Recipe/RecipeTestTrait.php
index 3ee24b85dcaa..601dced29634 100644
--- a/core/tests/Drupal/FunctionalTests/Core/Recipe/RecipeTestTrait.php
+++ b/core/tests/Drupal/FunctionalTests/Core/Recipe/RecipeTestTrait.php
@@ -21,16 +21,24 @@ trait RecipeTestTrait {
    * @param string|array<mixed> $data
    *   The contents of recipe.yml. If passed as an array, will be encoded to
    *   YAML.
+   * @param string|null $machine_name
+   *   The machine name for the recipe. Will be used as the directory name.
    *
    * @return \Drupal\Core\Recipe\Recipe
    *   The recipe object.
    */
-  protected function createRecipe(string|array $data): Recipe {
+  protected function createRecipe(string|array $data, ?string $machine_name = NULL): Recipe {
     if (is_array($data)) {
       $data = Yaml::encode($data);
     }
-    $dir = uniqid('public://');
-    mkdir($dir);
+    $recipes_dir = $this->siteDirectory . '/recipes';
+    if ($machine_name === NULL) {
+      $dir = uniqid($recipes_dir . '/');
+    }
+    else {
+      $dir = $recipes_dir . '/' . $machine_name;
+    }
+    mkdir($dir, recursive: TRUE);
     file_put_contents($dir . '/recipe.yml', $data);
 
     return Recipe::createFromDirectory($dir);
diff --git a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeConfiguratorTest.php b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeConfiguratorTest.php
index b640fd58ec52..640cc182de87 100644
--- a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeConfiguratorTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeConfiguratorTest.php
@@ -4,6 +4,7 @@
 
 namespace Drupal\KernelTests\Core\Recipe;
 
+use Drupal\Core\Recipe\Recipe;
 use Drupal\Core\Recipe\RecipeConfigurator;
 use Drupal\Core\Recipe\RecipeDiscovery;
 use Drupal\KernelTests\KernelTestBase;
@@ -15,7 +16,7 @@
 class RecipeConfiguratorTest extends KernelTestBase {
 
   public function testRecipeConfigurator(): void {
-    $discovery = new RecipeDiscovery(['core/tests/fixtures/recipes']);
+    $discovery = new RecipeDiscovery('core/tests/fixtures/recipes');
     $recipe_configurator = new RecipeConfigurator(
       ['install_two_modules', 'install_node_with_config', 'recipe_include'],
       $discovery
@@ -24,7 +25,9 @@ public function testRecipeConfigurator(): void {
     $reflection = new \ReflectionMethod('\Drupal\Core\Recipe\RecipeConfigurator', 'listAllRecipes');
 
     // Test methods.
-    $recipes_names = array_column((array) $reflection->invoke($recipe_configurator), 'name');
+    /** @var \Drupal\Core\Recipe\Recipe[] $recipes */
+    $recipes = (array) $reflection->invoke($recipe_configurator);
+    $recipes_names = array_map(fn(Recipe $recipe) => $recipe->name, $recipes);
     $recipe_extensions = $recipe_configurator->listAllExtensions();
     $expected_recipes_names = [
       'Install two modules',
@@ -45,7 +48,6 @@ public function testRecipeConfigurator(): void {
     $this->assertEquals($expected_recipe_extensions, $recipe_extensions);
     $this->assertEquals(1, array_count_values($recipes_names)['Install node with config']);
     $this->assertEquals(1, array_count_values($recipe_extensions)['field']);
-
   }
 
 }
diff --git a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeDiscoveryTest.php b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeDiscoveryTest.php
index 7eab347ca7a9..430aa8f34b85 100644
--- a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeDiscoveryTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeDiscoveryTest.php
@@ -27,7 +27,7 @@ public function providerRecipeDiscovery(): array {
    * @dataProvider providerRecipeDiscovery
    */
   public function testRecipeDiscovery(string $recipe, string $name): void {
-    $discovery = new RecipeDiscovery(['core/tests/fixtures/recipes']);
+    $discovery = new RecipeDiscovery('core/tests/fixtures/recipes');
     $recipe = $discovery->getRecipe($recipe);
     $this->assertSame($name, $recipe->name);
   }
@@ -45,20 +45,15 @@ public function providerRecipeDiscoveryException(): array {
    * @dataProvider providerRecipeDiscoveryException
    */
   public function testRecipeDiscoveryException(string $recipe): void {
-    $discovery = new RecipeDiscovery(['core/tests/fixtures/recipes']);
+    $discovery = new RecipeDiscovery('core/tests/fixtures/recipes');
     try {
       $discovery->getRecipe($recipe);
       $this->fail('Expected exception not thrown');
     }
     catch (UnknownRecipeException $e) {
-      $root = $this->getDrupalRoot();
       $this->assertSame($recipe, $e->recipe);
-      $this->assertSame([
-        'core/tests/fixtures/recipes',
-        $root . '/recipes',
-        $root . '/core/recipes',
-      ], $e->searchPaths);
-      $this->assertSame('Can not find the ' . $recipe . ' recipe, search paths: ' . implode(', ', $e->searchPaths), $e->getMessage());
+      $this->assertSame('core/tests/fixtures/recipes', $e->searchPath);
+      $this->assertSame('Can not find the ' . $recipe . ' recipe, search path: ' . $e->searchPath, $e->getMessage());
     }
   }
 
diff --git a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeEventsTest.php b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeEventsTest.php
index 002299076cf4..20d12bae4c38 100644
--- a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeEventsTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeEventsTest.php
@@ -49,7 +49,7 @@ public function register(ContainerBuilder $container): void {
   }
 
   public function testRecipeAppliedEvent(): void {
-    $recipe = Recipe::createFromDirectory($this->getDrupalRoot() . '/core/tests/fixtures/recipes/recipe_include');
+    $recipe = Recipe::createFromDirectory('core/tests/fixtures/recipes/recipe_include');
     RecipeRunner::processRecipe($recipe);
 
     $this->assertSame(['Install node with config', 'Recipe include'], $this->recipesApplied);
diff --git a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeRunnerTest.php b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeRunnerTest.php
index 09de4be4bb6a..0af066b626cd 100644
--- a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeRunnerTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeRunnerTest.php
@@ -5,6 +5,7 @@
 namespace Drupal\KernelTests\Core\Recipe;
 
 use Drupal\Component\Plugin\Exception\PluginNotFoundException;
+use Drupal\config_test\Entity\ConfigTest;
 use Drupal\Core\Recipe\Recipe;
 use Drupal\Core\Recipe\RecipePreExistingConfigException;
 use Drupal\Core\Recipe\RecipeRunner;
@@ -75,7 +76,7 @@ public function testModuleConfigurationOverride(): void {
     $this->assertTrue($this->container->get('config.storage')->exists('node.settings'), 'The node.settings configuration has been created');
     $this->assertTrue($this->container->get('config.storage')->exists('node.settings'), 'The node.settings configuration has been created');
     $this->assertTrue($this->config('node.settings')->get('use_admin_theme'), 'The node.settings:use_admin_theme is set to TRUE');
-    $this->assertSame('Test content type', NodeType::load('test')->label());
+    $this->assertSame('Test content type', NodeType::load('test')?->label());
     $node_type_data = $this->config('node.type.test')->get();
     $this->assertGreaterThan(0, strlen($node_type_data['uuid']), 'The node type configuration has been assigned a UUID.');
     // cSpell:disable-next-line
@@ -158,8 +159,8 @@ public function testRecipeInclude(): void {
 
     // Test the state after to applying the recipe.
     $this->assertTrue($this->container->get('module_handler')->moduleExists('dblog'), 'Dblog module installed');
-    $this->assertSame('Test content type', NodeType::load('test')->label());
-    $this->assertSame('Another test content type', NodeType::load('another_test')->label());
+    $this->assertSame('Test content type', NodeType::load('test')?->label());
+    $this->assertSame('Another test content type', NodeType::load('another_test')?->label());
   }
 
   public function testConfigActions() :void {
@@ -171,8 +172,8 @@ public function testConfigActions() :void {
 
     // Test the state after to applying the recipe.
     $storage = \Drupal::entityTypeManager()->getStorage('config_test');
-    /** @var \Drupal\config_test\Entity\ConfigTest $config_test_entity */
     $config_test_entity = $storage->load('recipe');
+    $this->assertInstanceOf(ConfigTest::class, $config_test_entity);
     $this->assertSame('Created by recipe', $config_test_entity->label());
     $this->assertSame('Set by recipe', $config_test_entity->getProtectedProperty());
     $this->assertSame('not bar', $this->config('config_test.system')->get('foo'));
@@ -183,8 +184,8 @@ public function testConfigActionsPreExistingConfig() :void {
     $this->installConfig(['config_test']);
     $this->assertSame('bar', $this->config('config_test.system')->get('foo'));
     $storage = \Drupal::entityTypeManager()->getStorage('config_test');
-    /** @var \Drupal\config_test\Entity\ConfigTest $config_test_entity */
     $config_test_entity = $storage->create(['id' => 'recipe', 'label' => 'Created by test']);
+    $this->assertInstanceOf(ConfigTest::class, $config_test_entity);
     $config_test_entity->setProtectedProperty('Set by test');
     $config_test_entity->save();
 
@@ -192,8 +193,8 @@ public function testConfigActionsPreExistingConfig() :void {
     RecipeRunner::processRecipe($recipe);
 
     // Test the state after to applying the recipe.
-    /** @var \Drupal\config_test\Entity\ConfigTest $config_test_entity */
     $config_test_entity = $storage->load('recipe');
+    $this->assertInstanceOf(ConfigTest::class, $config_test_entity);
     $this->assertSame('Created by test', $config_test_entity->label());
     $this->assertSame('Set by recipe', $config_test_entity->getProtectedProperty());
     $this->assertSame('not bar', $this->config('config_test.system')->get('foo'));
@@ -218,4 +219,36 @@ public function testInvalidConfigAction() :void {
     RecipeRunner::processRecipe($recipe);
   }
 
+  public function testRecipesAreDisambiguatedByPath(): void {
+    $recipe_data = <<<YAML
+name: 'Recipe include'
+recipes:
+  - core/tests/fixtures/recipes/recipe_include
+install:
+  - config_test
+YAML;
+
+    $recipe = $this->createRecipe($recipe_data, 'recipe_include');
+    RecipeRunner::processRecipe($recipe);
+
+    // Test the state after to applying the recipe.
+    $this->assertTrue($this->container->get('module_handler')->moduleExists('dblog'), 'Dblog module installed');
+    $this->assertTrue($this->container->get('module_handler')->moduleExists('config_test'), 'Config test module installed');
+    $this->assertSame('Test content type', NodeType::load('test')?->label());
+    $this->assertSame('Another test content type', NodeType::load('another_test')?->label());
+
+    $operations = RecipeRunner::toBatchOperations($recipe);
+    $this->assertSame('triggerEvent', $operations[7][0][1]);
+    $this->assertSame('Install node with config', $operations[7][1][0]->name);
+    $this->assertStringEndsWith('core/tests/fixtures/recipes/install_node_with_config', $operations[7][1][0]->path);
+
+    $this->assertSame('triggerEvent', $operations[10][0][1]);
+    $this->assertSame('Recipe include', $operations[10][1][0]->name);
+    $this->assertStringEndsWith('core/tests/fixtures/recipes/recipe_include', $operations[10][1][0]->path);
+
+    $this->assertSame('triggerEvent', $operations[12][0][1]);
+    $this->assertSame('Recipe include', $operations[12][1][0]->name);
+    $this->assertSame($this->siteDirectory . '/recipes/recipe_include', $operations[12][1][0]->path);
+  }
+
 }
diff --git a/phpstan-level9-baseline.neon b/phpstan-level9-baseline.neon
index 82f59183f735..b598bcfeae2a 100644
--- a/phpstan-level9-baseline.neon
+++ b/phpstan-level9-baseline.neon
@@ -291,7 +291,7 @@ parameters:
 			path: core/lib/Drupal/Core/Recipe/RecipeConfigurator.php
 
 		-
-			message: "#^Method Drupal\\\\Core\\\\Recipe\\\\RecipeDiscovery\\:\\:__construct\\(\\) has parameter \\$paths with no value type specified in iterable type array\\.$#"
+			message: "#^\\\\Drupal calls should be avoided in classes, use dependency injection instead$#"
 			count: 1
 			path: core/lib/Drupal/Core/Recipe/RecipeDiscovery.php
 
@@ -390,11 +390,6 @@ parameters:
 			count: 1
 			path: core/lib/Drupal/Core/Recipe/RecipeRunner.php
 
-		-
-			message: "#^Method Drupal\\\\Core\\\\Recipe\\\\UnknownRecipeException\\:\\:__construct\\(\\) has parameter \\$searchPaths with no value type specified in iterable type array\\.$#"
-			count: 1
-			path: core/lib/Drupal/Core/Recipe/UnknownRecipeException.php
-
 		-
 			message: "#^Method Drupal\\\\ckeditor5\\\\Plugin\\\\ConfigAction\\\\AddItemToToolbar\\:\\:create\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#"
 			count: 1
@@ -491,7 +486,17 @@ parameters:
 			path: core/tests/Drupal/KernelTests/Core/Recipe/RecipeRunnerTest.php
 
 		-
-			message: "#^Cannot call method label\\(\\) on Drupal\\\\node\\\\Entity\\\\NodeType\\|null\\.$#"
+			message: "#^Cannot access offset 1 on callable\\(\\)\\: mixed\\.$#"
+			count: 3
+			path: core/tests/Drupal/KernelTests/Core/Recipe/RecipeRunnerTest.php
+
+		-
+			message: "#^Cannot access property \\$name on mixed\\.$#"
+			count: 3
+			path: core/tests/Drupal/KernelTests/Core/Recipe/RecipeRunnerTest.php
+
+		-
+			message: "#^Cannot access property \\$path on mixed\\.$#"
 			count: 3
 			path: core/tests/Drupal/KernelTests/Core/Recipe/RecipeRunnerTest.php
 
-- 
GitLab