From ce28a4273805e846a5ce90eb87360c028f0ea472 Mon Sep 17 00:00:00 2001
From: Dave Long <dave@longwaveconsulting.com>
Date: Mon, 13 Feb 2023 11:52:53 +0000
Subject: [PATCH] Issue #3299678 by mondrake, catch, longwave: Deprecate
 DiffEngine and replace with sebastian/diff

---
 composer.lock                                 | 135 ++++++++---------
 .../Metapackage/CoreRecommended/composer.json |   1 +
 .../PinnedDevDependencies/composer.json       |   1 -
 core/composer.json                            |   3 +-
 core/lib/Drupal/Component/Diff/Diff.php       |  54 +++++--
 .../Component/Diff/DiffOpOutputBuilder.php    | 138 ++++++++++++++++++
 .../Component/Diff/Engine/DiffEngine.php      |   9 ++
 .../Drupal/Component/Diff/Engine/DiffOp.php   |  21 +++
 .../Component/Diff/Engine/DiffOpAdd.php       |   7 +
 .../Component/Diff/Engine/DiffOpChange.php    |   7 +
 .../Component/Diff/Engine/DiffOpCopy.php      |   7 +
 .../Component/Diff/Engine/DiffOpDelete.php    |   7 +
 core/lib/Drupal/Component/Diff/composer.json  |   3 +-
 core/misc/cspell/dictionary.txt               |   1 +
 .../Diff/DiffOpOutputBuilderTest.php          | 114 +++++++++++++++
 .../Component/Diff/Engine/DiffEngineTest.php  |   6 +
 .../Component/Diff/Engine/DiffOpTest.php      |   5 +
 17 files changed, 439 insertions(+), 80 deletions(-)
 create mode 100644 core/lib/Drupal/Component/Diff/DiffOpOutputBuilder.php
 create mode 100644 core/tests/Drupal/Tests/Component/Diff/DiffOpOutputBuilderTest.php

diff --git a/composer.lock b/composer.lock
index 83028d37ed51..8300ec52bd4e 100644
--- a/composer.lock
+++ b/composer.lock
@@ -446,7 +446,7 @@
             "dist": {
                 "type": "path",
                 "url": "core",
-                "reference": "9e2d1532ca02ec3d0fb258d23a5c404533bcac43"
+                "reference": "4a6aa3700723fe42a3a8add8c27af45f8710ddb2"
             },
             "require": {
                 "asm89/stack-cors": "^2.1",
@@ -474,6 +474,7 @@
                 "pear/archive_tar": "^1.4.14",
                 "php": ">=8.1.0",
                 "psr/log": "^3.0",
+                "sebastian/diff": "^4",
                 "symfony/console": "^6.2",
                 "symfony/dependency-injection": "^6.2",
                 "symfony/event-dispatcher": "^6.2",
@@ -1824,6 +1825,72 @@
             },
             "time": "2019-03-08T08:55:37+00:00"
         },
+        {
+            "name": "sebastian/diff",
+            "version": "4.0.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/diff.git",
+                "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d",
+                "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.3"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.3",
+                "symfony/process": "^4.2 || ^5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                },
+                {
+                    "name": "Kore Nordmann",
+                    "email": "mail@kore-nordmann.de"
+                }
+            ],
+            "description": "Diff implementation",
+            "homepage": "https://github.com/sebastianbergmann/diff",
+            "keywords": [
+                "diff",
+                "udiff",
+                "unidiff",
+                "unified diff"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/diff/issues",
+                "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-10-26T13:10:38+00:00"
+        },
         {
             "name": "symfony/console",
             "version": "v6.2.5",
@@ -6765,72 +6832,6 @@
             ],
             "time": "2020-10-26T15:52:27+00:00"
         },
-        {
-            "name": "sebastian/diff",
-            "version": "4.0.4",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/sebastianbergmann/diff.git",
-                "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d",
-                "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.3"
-            },
-            "require-dev": {
-                "phpunit/phpunit": "^9.3",
-                "symfony/process": "^4.2 || ^5"
-            },
-            "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-master": "4.0-dev"
-                }
-            },
-            "autoload": {
-                "classmap": [
-                    "src/"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "BSD-3-Clause"
-            ],
-            "authors": [
-                {
-                    "name": "Sebastian Bergmann",
-                    "email": "sebastian@phpunit.de"
-                },
-                {
-                    "name": "Kore Nordmann",
-                    "email": "mail@kore-nordmann.de"
-                }
-            ],
-            "description": "Diff implementation",
-            "homepage": "https://github.com/sebastianbergmann/diff",
-            "keywords": [
-                "diff",
-                "udiff",
-                "unidiff",
-                "unified diff"
-            ],
-            "support": {
-                "issues": "https://github.com/sebastianbergmann/diff/issues",
-                "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4"
-            },
-            "funding": [
-                {
-                    "url": "https://github.com/sebastianbergmann",
-                    "type": "github"
-                }
-            ],
-            "time": "2020-10-26T13:10:38+00:00"
-        },
         {
             "name": "sebastian/environment",
             "version": "5.1.4",
diff --git a/composer/Metapackage/CoreRecommended/composer.json b/composer/Metapackage/CoreRecommended/composer.json
index 13f29dc1beda..ecddbf123556 100644
--- a/composer/Metapackage/CoreRecommended/composer.json
+++ b/composer/Metapackage/CoreRecommended/composer.json
@@ -30,6 +30,7 @@
         "psr/http-message": "~1.0.1",
         "psr/log": "~3.0.0",
         "ralouphie/getallheaders": "~3.0.3",
+        "sebastian/diff": "~4.0.4",
         "symfony/console": "~v6.2.5",
         "symfony/dependency-injection": "~v6.2.6",
         "symfony/deprecation-contracts": "~v3.2.0",
diff --git a/composer/Metapackage/PinnedDevDependencies/composer.json b/composer/Metapackage/PinnedDevDependencies/composer.json
index f2d192bd7101..6d6d88f48413 100644
--- a/composer/Metapackage/PinnedDevDependencies/composer.json
+++ b/composer/Metapackage/PinnedDevDependencies/composer.json
@@ -49,7 +49,6 @@
         "sebastian/code-unit-reverse-lookup": "2.0.3",
         "sebastian/comparator": "4.0.8",
         "sebastian/complexity": "2.0.2",
-        "sebastian/diff": "4.0.4",
         "sebastian/environment": "5.1.4",
         "sebastian/exporter": "4.0.5",
         "sebastian/global-state": "5.0.5",
diff --git a/core/composer.json b/core/composer.json
index 371672103611..92a7ab8e82c9 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -42,7 +42,8 @@
         "asm89/stack-cors": "^2.1",
         "pear/archive_tar": "^1.4.14",
         "psr/log": "^3.0",
-        "mck89/peast": "^1.14"
+        "mck89/peast": "^1.14",
+        "sebastian/diff": "^4"
     },
     "conflict": {
         "drush/drush": "<8.1.10"
diff --git a/core/lib/Drupal/Component/Diff/Diff.php b/core/lib/Drupal/Component/Diff/Diff.php
index 289bab5f0e7c..1b5c40a70514 100644
--- a/core/lib/Drupal/Component/Diff/Diff.php
+++ b/core/lib/Drupal/Component/Diff/Diff.php
@@ -2,17 +2,15 @@
 
 namespace Drupal\Component\Diff;
 
-use Drupal\Component\Diff\Engine\DiffEngine;
+use SebastianBergmann\Diff\Differ;
 
 /**
  * Class representing a 'diff' between two sequences of strings.
- * @todo document
- * @subpackage DifferenceEngine
  *
- * Copied from https://www.drupal.org/project/diff which was based PHP diff
- * engine for phpwiki. (Taken from phpwiki-1.3.3) The original code in phpwiki
- * was copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org> and
- * licensed under GPL.
+ * Component code originally taken from https://www.drupal.org/project/diff
+ * which was itself based on the PHP diff engine for phpwiki. The original code
+ * in phpwiki was copyright (C) 2000, 2001 Geoffrey T. Dairiki
+ * <dairiki@dairiki.org> and licensed under GPL.
  */
 class Diff {
 
@@ -34,9 +32,9 @@ class Diff {
    *   An array of strings.
    */
   public function __construct($from_lines, $to_lines) {
-    $eng = new DiffEngine();
-    $this->edits = $eng->diff($from_lines, $to_lines);
-    //$this->_check($from_lines, $to_lines);
+    $diffOpBuilder = new DiffOpOutputBuilder();
+    $differ = new Differ($diffOpBuilder);
+    $this->edits = $diffOpBuilder->toOpsArray($differ->diffToArray($from_lines, $to_lines));
   }
 
   /**
@@ -48,8 +46,14 @@ public function __construct($from_lines, $to_lines) {
    *  $rev = $diff->reverse();
    * @return object
    *   A Diff object representing the inverse of the original diff.
+   *
+   * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no
+   *   replacement.
+   *
+   * @see https://www.drupal.org/node/3337942
    */
   public function reverse() {
+    @trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3337942', E_USER_DEPRECATED);
     $rev = $this;
     $rev->edits = [];
     foreach ($this->edits as $edit) {
@@ -62,8 +66,14 @@ public function reverse() {
    * Check for empty diff.
    *
    * @return bool True iff two sequences were identical.
+   *
+   * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no
+   *   replacement.
+   *
+   * @see https://www.drupal.org/node/3337942
    */
   public function isEmpty() {
+    @trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3337942', E_USER_DEPRECATED);
     foreach ($this->edits as $edit) {
       if ($edit->type != 'copy') {
         return FALSE;
@@ -78,8 +88,14 @@ public function isEmpty() {
    * This is mostly for diagnostic purposed.
    *
    * @return int The length of the LCS.
+   *
+   * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no
+   *   replacement.
+   *
+   * @see https://www.drupal.org/node/3337942
    */
   public function lcs() {
+    @trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3337942', E_USER_DEPRECATED);
     $lcs = 0;
     foreach ($this->edits as $edit) {
       if ($edit->type == 'copy') {
@@ -96,8 +112,14 @@ public function lcs() {
    * constructor.
    *
    * @return array The original sequence of strings.
+   *
+   * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no
+   *   replacement.
+   *
+   * @see https://www.drupal.org/node/3337942
    */
   public function orig() {
+    @trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3337942', E_USER_DEPRECATED);
     $lines = [];
 
     foreach ($this->edits as $edit) {
@@ -115,8 +137,14 @@ public function orig() {
    * constructor.
    *
    * @return array The sequence of strings.
+   *
+   * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no
+   *   replacement.
+   *
+   * @see https://www.drupal.org/node/3337942
    */
   public function closing() {
+    @trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3337942', E_USER_DEPRECATED);
     $lines = [];
 
     foreach ($this->edits as $edit) {
@@ -131,8 +159,14 @@ public function closing() {
    * Check a Diff for validity.
    *
    * This is here only for debugging purposes.
+   *
+   * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no
+   *   replacement.
+   *
+   * @see https://www.drupal.org/node/3337942
    */
   public function check($from_lines, $to_lines) {
+    @trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3337942', E_USER_DEPRECATED);
     if (serialize($from_lines) != serialize($this->orig())) {
       trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
     }
diff --git a/core/lib/Drupal/Component/Diff/DiffOpOutputBuilder.php b/core/lib/Drupal/Component/Diff/DiffOpOutputBuilder.php
new file mode 100644
index 000000000000..c872a92f50e8
--- /dev/null
+++ b/core/lib/Drupal/Component/Diff/DiffOpOutputBuilder.php
@@ -0,0 +1,138 @@
+<?php declare(strict_types=1);
+
+namespace Drupal\Component\Diff;
+
+use Drupal\Component\Diff\Engine\DiffOp;
+use Drupal\Component\Diff\Engine\DiffOpAdd;
+use Drupal\Component\Diff\Engine\DiffOpChange;
+use Drupal\Component\Diff\Engine\DiffOpCopy;
+use Drupal\Component\Diff\Engine\DiffOpDelete;
+use SebastianBergmann\Diff\Differ;
+use SebastianBergmann\Diff\Output\DiffOutputBuilderInterface;
+
+/**
+ * Returns a diff as an array of DiffOp operations.
+ */
+final class DiffOpOutputBuilder implements DiffOutputBuilderInterface {
+
+  /**
+   * A constant to manage removal+addition as a single operation.
+   */
+  private const CHANGED = 999;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDiff(array $diff): string {
+    return serialize($this->toOpsArray($diff));
+  }
+
+  /**
+   * Converts the output of Differ to an array of DiffOp* value objects.
+   *
+   * @param array $diff
+   *   The array output of Differ::diffToArray().
+   *
+   * @return \Drupal\Component\Diff\Engine\DiffOp[]
+   *   An array of DiffOp* value objects.
+   */
+  public function toOpsArray(array $diff): array {
+    $ops = [];
+    $hunkMode = NULL;
+    $hunkSource = [];
+    $hunkTarget = [];
+
+    for ($i = 0; $i < count($diff); $i++) {
+
+      // Handle a sequence of removals + additions as a sequence of changes, and
+      // manages the tail if required.
+      if ($diff[$i][1] === Differ::REMOVED) {
+        if ($hunkMode !== NULL) {
+          $ops[] = $this->hunkOp($hunkMode, $hunkSource, $hunkTarget);
+          $hunkSource = [];
+          $hunkTarget = [];
+        }
+        for ($n = $i; $n < count($diff) && $diff[$n][1] === Differ::REMOVED; $n++) {
+          $hunkSource[] = $diff[$n][0];
+        }
+        for (; $n < count($diff) && $diff[$n][1] === Differ::ADDED; $n++) {
+          $hunkTarget[] = $diff[$n][0];
+        }
+        if (count($hunkTarget) === 0) {
+          $ops[] = $this->hunkOp(Differ::REMOVED, $hunkSource, $hunkTarget);
+        }
+        elseif (count($hunkSource) === count($hunkTarget)) {
+          $ops[] = $this->hunkOp(self::CHANGED, $hunkSource, $hunkTarget);
+        }
+        elseif (count($hunkSource) > count($hunkTarget)) {
+          $ops[] = $this->hunkOp(self::CHANGED, array_slice($hunkSource, 0, count($hunkTarget)), $hunkTarget);
+          $ops[] = $this->hunkOp(Differ::REMOVED, array_slice($hunkSource, count($hunkTarget)), []);
+        }
+        else {
+          $ops[] = $this->hunkOp(self::CHANGED, $hunkSource, array_slice($hunkTarget, 0, count($hunkSource)));
+          $ops[] = $this->hunkOp(Differ::ADDED, array_slice($hunkTarget, count($hunkSource)), []);
+        }
+        $hunkMode = NULL;
+        $hunkSource = [];
+        $hunkTarget = [];
+        $i = $n - 1;
+        continue;
+      }
+
+      // When here, we are adding or copying the item. Removing or changing is
+      // managed above.
+      if ($hunkMode === NULL) {
+        $hunkMode = $diff[$i][1];
+      }
+      elseif ($hunkMode !== $diff[$i][1]) {
+        $ops[] = $this->hunkOp($hunkMode, $hunkSource, $hunkTarget);
+        $hunkMode = $diff[$i][1];
+        $hunkSource = [];
+        $hunkTarget = [];
+      }
+
+      $hunkSource[] = $diff[$i][0];
+    }
+
+    if ($hunkMode !== NULL) {
+      $ops[] = $this->hunkOp($hunkMode, $hunkSource, $hunkTarget);
+    }
+
+    return $ops;
+  }
+
+  /**
+   * Returns the proper DiffOp object based on the hunk mode.
+   *
+   * @param int $mode
+   *   A Differ constant or self::CHANGED.
+   * @param string[] $source
+   *   An array of strings to be changed/added/removed/copied.
+   * @param string[] $source
+   *   The array of strings to be changed to when self::CHANGED is specified.
+   *
+   * @return \Drupal\Component\Diff\Engine\DiffOp
+   *   A DiffOp* value object.
+   *
+   * @throw \InvalidArgumentException
+   *   When $mode is not valid.
+   */
+  private function hunkOp(int $mode, array $source, array $target): DiffOp {
+    switch ($mode) {
+      case Differ::OLD:
+        return new DiffOpCopy($source);
+
+      case self::CHANGED:
+        return new DiffOpChange($source, $target);
+
+      case Differ::ADDED:
+        return new DiffOpAdd($source);
+
+      case Differ::REMOVED:
+        return new DiffOpDelete($source);
+
+    }
+    throw new \InvalidArgumentException("Invalid \$mode {$mode} specified");
+  }
+
+}
diff --git a/core/lib/Drupal/Component/Diff/Engine/DiffEngine.php b/core/lib/Drupal/Component/Diff/Engine/DiffEngine.php
index 75dfc46ee5a0..1289f96ebaf1 100644
--- a/core/lib/Drupal/Component/Diff/Engine/DiffEngine.php
+++ b/core/lib/Drupal/Component/Diff/Engine/DiffEngine.php
@@ -24,6 +24,11 @@
  * @author Geoffrey T. Dairiki, Tim Starling
  * @private
  * @subpackage DifferenceEngine
+ *
+ * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use
+ *   sebastianbergmann/diff instead.
+ *
+ * @see https://www.drupal.org/node/3337942
  */
 #[\AllowDynamicProperties]
 class DiffEngine {
@@ -32,6 +37,10 @@ class DiffEngine {
 
   const MAX_XREF_LENGTH = 10000;
 
+  public function __construct() {
+    @trigger_error('Drupal\Component\Diff\Engine\DiffEngine is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use sebastianbergmann/diff instead. See https://www.drupal.org/node/3337942', E_USER_DEPRECATED);
+  }
+
   public function diff($from_lines, $to_lines) {
 
     $n_from = sizeof($from_lines);
diff --git a/core/lib/Drupal/Component/Diff/Engine/DiffOp.php b/core/lib/Drupal/Component/Diff/Engine/DiffOp.php
index 29d749dc7c84..735669dc147c 100644
--- a/core/lib/Drupal/Component/Diff/Engine/DiffOp.php
+++ b/core/lib/Drupal/Component/Diff/Engine/DiffOp.php
@@ -12,15 +12,36 @@ class DiffOp {
   public $orig;
   public $closing;
 
+  /**
+   * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no
+   *   replacement.
+   *
+   * @see https://www.drupal.org/node/3337942
+   */
   public function reverse() {
+    @trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3337942', E_USER_DEPRECATED);
     trigger_error('pure virtual', E_USER_ERROR);
   }
 
+  /**
+   * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no
+   *   replacement.
+   *
+   * @see https://www.drupal.org/node/3337942
+   */
   public function norig() {
+    @trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3337942', E_USER_DEPRECATED);
     return $this->orig ? sizeof($this->orig) : 0;
   }
 
+  /**
+   * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no
+   *   replacement.
+   *
+   * @see https://www.drupal.org/node/3337942
+   */
   public function nclosing() {
+    @trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3337942', E_USER_DEPRECATED);
     return $this->closing ? sizeof($this->closing) : 0;
   }
 
diff --git a/core/lib/Drupal/Component/Diff/Engine/DiffOpAdd.php b/core/lib/Drupal/Component/Diff/Engine/DiffOpAdd.php
index 14429c251d62..df954302f6cc 100644
--- a/core/lib/Drupal/Component/Diff/Engine/DiffOpAdd.php
+++ b/core/lib/Drupal/Component/Diff/Engine/DiffOpAdd.php
@@ -15,7 +15,14 @@ public function __construct($lines) {
     $this->orig = FALSE;
   }
 
+  /**
+   * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no
+   *   replacement.
+   *
+   * @see https://www.drupal.org/node/3337942
+   */
   public function reverse() {
+    @trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3337942', E_USER_DEPRECATED);
     return new DiffOpDelete($this->closing);
   }
 
diff --git a/core/lib/Drupal/Component/Diff/Engine/DiffOpChange.php b/core/lib/Drupal/Component/Diff/Engine/DiffOpChange.php
index 4abd6acc01f1..93d15db885b9 100644
--- a/core/lib/Drupal/Component/Diff/Engine/DiffOpChange.php
+++ b/core/lib/Drupal/Component/Diff/Engine/DiffOpChange.php
@@ -15,7 +15,14 @@ public function __construct($orig, $closing) {
     $this->closing = $closing;
   }
 
+  /**
+   * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no
+   *   replacement.
+   *
+   * @see https://www.drupal.org/node/3337942
+   */
   public function reverse() {
+    @trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3337942', E_USER_DEPRECATED);
     return new DiffOpChange($this->closing, $this->orig);
   }
 
diff --git a/core/lib/Drupal/Component/Diff/Engine/DiffOpCopy.php b/core/lib/Drupal/Component/Diff/Engine/DiffOpCopy.php
index 4128d573291a..c82e2a61b996 100644
--- a/core/lib/Drupal/Component/Diff/Engine/DiffOpCopy.php
+++ b/core/lib/Drupal/Component/Diff/Engine/DiffOpCopy.php
@@ -18,7 +18,14 @@ public function __construct($orig, $closing = FALSE) {
     $this->closing = $closing;
   }
 
+  /**
+   * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no
+   *   replacement.
+   *
+   * @see https://www.drupal.org/node/3337942
+   */
   public function reverse() {
+    @trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3337942', E_USER_DEPRECATED);
     return new DiffOpCopy($this->closing, $this->orig);
   }
 
diff --git a/core/lib/Drupal/Component/Diff/Engine/DiffOpDelete.php b/core/lib/Drupal/Component/Diff/Engine/DiffOpDelete.php
index e402d66b61f7..4aa84be1300e 100644
--- a/core/lib/Drupal/Component/Diff/Engine/DiffOpDelete.php
+++ b/core/lib/Drupal/Component/Diff/Engine/DiffOpDelete.php
@@ -15,7 +15,14 @@ public function __construct($lines) {
     $this->closing = FALSE;
   }
 
+  /**
+   * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no
+   *   replacement.
+   *
+   * @see https://www.drupal.org/node/3337942
+   */
   public function reverse() {
+    @trigger_error(__METHOD__ . '() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3337942', E_USER_DEPRECATED);
     return new DiffOpAdd($this->orig);
   }
 
diff --git a/core/lib/Drupal/Component/Diff/composer.json b/core/lib/Drupal/Component/Diff/composer.json
index 972c5e2f771b..57a07592ff85 100644
--- a/core/lib/Drupal/Component/Diff/composer.json
+++ b/core/lib/Drupal/Component/Diff/composer.json
@@ -7,7 +7,8 @@
     "homepage": "https://www.drupal.org/project/drupal",
     "license": "GPL-2.0-or-later",
     "require": {
-        "php": ">=8.1.0"
+        "php": ">=8.1.0",
+        "sebastian/diff": "^4"
     },
     "autoload": {
         "psr-4": {
diff --git a/core/misc/cspell/dictionary.txt b/core/misc/cspell/dictionary.txt
index 66fb08a96f1b..f12aa133a462 100644
--- a/core/misc/cspell/dictionary.txt
+++ b/core/misc/cspell/dictionary.txt
@@ -1043,6 +1043,7 @@ screenreaders
 scriptable
 scrollbars
 searchdirs
+sebastianbergmann
 sebe
 seld
 selectbox
diff --git a/core/tests/Drupal/Tests/Component/Diff/DiffOpOutputBuilderTest.php b/core/tests/Drupal/Tests/Component/Diff/DiffOpOutputBuilderTest.php
new file mode 100644
index 000000000000..bb0f70de2e19
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Diff/DiffOpOutputBuilderTest.php
@@ -0,0 +1,114 @@
+<?php
+
+namespace Drupal\Tests\Component\Diff;
+
+use Drupal\Component\Diff\DiffOpOutputBuilder;
+use Drupal\Component\Diff\Engine\DiffOpAdd;
+use Drupal\Component\Diff\Engine\DiffOpCopy;
+use Drupal\Component\Diff\Engine\DiffOpChange;
+use Drupal\Component\Diff\Engine\DiffOpDelete;
+use PHPUnit\Framework\TestCase;
+use SebastianBergmann\Diff\Differ;
+
+/**
+ * @coversDefaultClass \Drupal\Component\Diff\DiffOpOutputBuilder
+ *
+ * @group Diff
+ */
+class DiffOpOutputBuilderTest extends TestCase {
+
+  /**
+   * @return array
+   *   - Expected output in terms of return class. A list of class names
+   *     expected to be returned by DiffEngine::diff().
+   *   - An array of strings to change from.
+   *   - An array of strings to change to.
+   */
+  public function provideTestDiff(): array {
+    return [
+      'empty' => [[], [], []],
+      'add' => [[new DiffOpAdd(['a'])], [], ['a']],
+      'copy' => [[new DiffOpCopy(['a'])], ['a'], ['a']],
+      'change' => [[new DiffOpChange(['a'], ['b'])], ['a'], ['b']],
+      'copy-and-change' => [
+        [
+          new DiffOpCopy(['a']),
+          new DiffOpChange(['b'], ['c']),
+        ],
+        ['a', 'b'],
+        ['a', 'c'],
+      ],
+      'copy-change-copy' => [
+        [
+          new DiffOpCopy(['a']),
+          new DiffOpChange(['b'], ['c']),
+          new DiffOpCopy(['d']),
+        ],
+        ['a', 'b', 'd'],
+        ['a', 'c', 'd'],
+      ],
+      'copy-change-copy-add' => [
+        [
+          new DiffOpCopy(['a']),
+          new DiffOpChange(['b'], ['c']),
+          new DiffOpCopy(['d']),
+          new DiffOpAdd(['e']),
+        ],
+        ['a', 'b', 'd'],
+        ['a', 'c', 'd', 'e'],
+      ],
+      'copy-delete' => [
+        [
+          new DiffOpCopy(['a']),
+          new DiffOpDelete(['b', 'd']),
+        ],
+        ['a', 'b', 'd'],
+        ['a'],
+      ],
+    ];
+  }
+
+  /**
+   * Tests whether op classes returned match expectations.
+   *
+   * @covers ::toOpsArray
+   * @dataProvider provideTestDiff
+   */
+  public function testToOpsArray(array $expected, array $from, array $to): void {
+    $diffOpBuilder = new DiffOpOutputBuilder();
+    $differ = new Differ($diffOpBuilder);
+    $diff = $differ->diffToArray($from, $to);
+    $this->assertEquals($expected, $diffOpBuilder->toOpsArray($diff));
+  }
+
+  /**
+   * @covers ::getDiff
+   * @dataProvider provideTestDiff
+   */
+  public function testGetDiff(array $expected, array $from, array $to): void {
+    $differ = new Differ(new DiffOpOutputBuilder());
+    $diff = $differ->diff($from, $to);
+    $this->assertEquals($expected, unserialize($diff));
+  }
+
+  /**
+   * Tests that two files can be successfully diffed.
+   *
+   * @covers ::toOpsArray
+   */
+  public function testDiffInfiniteLoop(): void {
+    $from = explode("\n", file_get_contents(__DIR__ . '/Engine/fixtures/file1.txt'));
+    $to = explode("\n", file_get_contents(__DIR__ . '/Engine/fixtures/file2.txt'));
+    $diffOpBuilder = new DiffOpOutputBuilder();
+    $differ = new Differ($diffOpBuilder);
+    $diff = $differ->diffToArray($from, $to);
+    $diffOps = $diffOpBuilder->toOpsArray($diff);
+    $this->assertCount(5, $diffOps);
+    $this->assertEquals($diffOps[0], new DiffOpAdd(['    - image.style.max_325x325']));
+    $this->assertEquals($diffOps[1], new DiffOpCopy(['    - image.style.max_650x650']));
+    $this->assertEquals($diffOps[2], new DiffOpChange(['    - image.style.max_325x325'], ['_core:']));
+    $this->assertEquals($diffOps[3], new DiffOpAdd(['  default_config_hash: 3mjM9p-kQ8syzH7N8T0L9OnCJDSPvHAZoi3q6jcXJKM']));
+    $this->assertEquals($diffOps[4], new DiffOpCopy(['fallback_image_style: max_325x325', '']));
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Diff/Engine/DiffEngineTest.php b/core/tests/Drupal/Tests/Component/Diff/Engine/DiffEngineTest.php
index 895faebeb504..1020373e6ac6 100644
--- a/core/tests/Drupal/Tests/Component/Diff/Engine/DiffEngineTest.php
+++ b/core/tests/Drupal/Tests/Component/Diff/Engine/DiffEngineTest.php
@@ -8,6 +8,7 @@
 use Drupal\Component\Diff\Engine\DiffOpChange;
 use Drupal\Component\Diff\Engine\DiffOpDelete;
 use PHPUnit\Framework\TestCase;
+use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
 
 /**
  * Test DiffEngine class.
@@ -15,9 +16,12 @@
  * @coversDefaultClass \Drupal\Component\Diff\Engine\DiffEngine
  *
  * @group Diff
+ * @group legacy
  */
 class DiffEngineTest extends TestCase {
 
+  use ExpectDeprecationTrait;
+
   /**
    * @return array
    *   - Expected output in terms of return class. A list of class names
@@ -76,6 +80,7 @@ public function provideTestDiff() {
    * @dataProvider provideTestDiff
    */
   public function testDiff($expected, $from, $to) {
+    $this->expectDeprecation('Drupal\Component\Diff\Engine\DiffEngine is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use sebastianbergmann/diff instead. See https://www.drupal.org/node/3337942');
     $diff_engine = new DiffEngine();
     $diff = $diff_engine->diff($from, $to);
     // Make sure we have the same number of results as expected.
@@ -92,6 +97,7 @@ public function testDiff($expected, $from, $to) {
    * @covers ::diff
    */
   public function testDiffInfiniteLoop() {
+    $this->expectDeprecation('Drupal\Component\Diff\Engine\DiffEngine is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use sebastianbergmann/diff instead. See https://www.drupal.org/node/3337942');
     $from = explode("\n", file_get_contents(__DIR__ . '/fixtures/file1.txt'));
     $to = explode("\n", file_get_contents(__DIR__ . '/fixtures/file2.txt'));
     $diff_engine = new DiffEngine();
diff --git a/core/tests/Drupal/Tests/Component/Diff/Engine/DiffOpTest.php b/core/tests/Drupal/Tests/Component/Diff/Engine/DiffOpTest.php
index 76f0064e7d96..a8c7ac72bafa 100644
--- a/core/tests/Drupal/Tests/Component/Diff/Engine/DiffOpTest.php
+++ b/core/tests/Drupal/Tests/Component/Diff/Engine/DiffOpTest.php
@@ -4,6 +4,7 @@
 
 use Drupal\Component\Diff\Engine\DiffOp;
 use PHPUnit\Framework\TestCase;
+use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
 
 /**
  * Test DiffOp base class.
@@ -15,15 +16,19 @@
  * @coversDefaultClass \Drupal\Component\Diff\Engine\DiffOp
  *
  * @group Diff
+ * @group legacy
  */
 class DiffOpTest extends TestCase {
 
+  use ExpectDeprecationTrait;
+
   /**
    * DiffOp::reverse() always throws an error.
    *
    * @covers ::reverse
    */
   public function testReverse() {
+    $this->expectDeprecation('Drupal\Component\Diff\Engine\DiffOp::reverse() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3337942');
     $this->expectError();
     $op = new DiffOp();
     $result = $op->reverse();
-- 
GitLab