1
0
mirror of https://github.com/danog/PHP-Parser.git synced 2025-01-22 05:41:23 +01:00

Add implementation of Myers differ

Intended to be used in the format-preserving pretty printer for
handling changes in node lists.
This commit is contained in:
Nikita Popov 2017-10-05 21:52:20 +02:00
parent 75880fbe2d
commit 69aec6fb5b
3 changed files with 255 additions and 0 deletions

View File

@ -0,0 +1,26 @@
<?php
namespace PhpParser\Internal;
/**
* @internal
*/
class DiffElem {
const TYPE_KEEP = 0;
const TYPE_REMOVE = 1;
const TYPE_ADD = 2;
const TYPE_REPLACE = 3;
/** @var int One of the TYPE_* constants */
public $type;
/** @var mixed Is null for add operations */
public $old;
/** @var mixed Is null for remove operations */
public $new;
public function __construct(int $type, $old, $new) {
$this->type = $type;
$this->old = $old;
$this->new = $new;
}
}

View File

@ -0,0 +1,163 @@
<?php
namespace PhpParser\Internal;
/**
* Implements the Myers diff algorithm.
*
* Myers, Eugene W. "An O (ND) difference algorithm and its variations."
* Algorithmica 1.1 (1986): 251-266.
*
* @internal
*/
class Differ {
private $isEqual;
/**
* Create differ over the given equality relation.
*
* @param callable $isEqual Equality relation with signature function($a, $b): bool
*/
public function __construct(callable $isEqual) {
$this->isEqual = $isEqual;
}
/**
* Calculate diff (edit script) from $old to $new.
*
* @param array $old Original array
* @param array $new New array
*
* @return DiffElem[] Diff (edit script)
*/
public function diff(array $old, array $new) {
list($trace, $x, $y) = $this->calculateTrace($old, $new);
return $this->extractDiff($trace, $x, $y, $old, $new);
}
/**
* Calculate diff, including "replace" operations.
*
* If a sequence of remove operations is followed by the same number of add operations, these
* will be coalesced into replace operations.
*
* @param array $old Original array
* @param array $new New array
*
* @return DiffElem[] Diff (edit script), including replace operations
*/
public function diffWithReplacements(array $old, array $new) {
return $this->coalesceReplacements($this->diff($old, $new));
}
private function calculateTrace(array $a, array $b) {
$n = \count($a);
$m = \count($b);
$max = $n + $m;
$v = [1 => 0];
$trace = [];
for ($d = 0; $d <= $max; $d++) {
$trace[] = $v;
for ($k = -$d; $k <= $d; $k += 2) {
if ($k === -$d || ($k !== $d && $v[$k-1] < $v[$k+1])) {
$x = $v[$k+1];
} else {
$x = $v[$k-1] + 1;
}
$y = $x - $k;
while ($x < $n && $y < $m && ($this->isEqual)($a[$x], $b[$y])) {
$x++;
$y++;
}
$v[$k] = $x;
if ($x >= $n && $y >= $m) {
return [$trace, $x, $y];
}
}
}
throw new \Exception('Should not happen');
}
private function extractDiff(array $trace, int $x, int $y, array $a, array $b) {
$result = [];
for ($d = \count($trace) - 1; $d >= 0; $d--) {
$v = $trace[$d];
$k = $x - $y;
if ($k === -$d || ($k !== $d && $v[$k-1] < $v[$k+1])) {
$prevK = $k + 1;
} else {
$prevK = $k - 1;
}
$prevX = $v[$prevK];
$prevY = $prevX - $prevK;
while ($x > $prevX && $y > $prevY) {
$result[] = new DiffElem(DiffElem::TYPE_KEEP, $a[$x-1], $b[$y-1]);
$x--;
$y--;
}
if ($d === 0) {
break;
}
while ($x > $prevX) {
$result[] = new DiffElem(DiffElem::TYPE_REMOVE, $a[$x-1], null);
$x--;
}
while ($y > $prevY) {
$result[] = new DiffElem(DiffElem::TYPE_ADD, null, $b[$y-1]);
$y--;
}
}
return array_reverse($result);
}
/**
* Coalesce equal-length sequences of remove+add into a replace operation.
*
* @param DiffElem[] $diff
* @return DiffElem[]
*/
private function coalesceReplacements(array $diff) {
$newDiff = [];
$c = \count($diff);
for ($i = 0; $i < $c; $i++) {
$diffType = $diff[$i]->type;
if ($diffType !== DiffElem::TYPE_REMOVE) {
$newDiff[] = $diff[$i];
continue;
}
$j = $i;
while ($j < $c && $diff[$j]->type === DiffElem::TYPE_REMOVE) {
$j++;
}
$k = $j;
while ($k < $c && $diff[$k]->type === DiffElem::TYPE_ADD) {
$k++;
}
if ($j - $i === $k - $j) {
$len = $j - $i;
for ($n = 0; $n < $len; $n++) {
$newDiff[] = new DiffElem(
DiffElem::TYPE_REPLACE, $diff[$i + $n]->old, $diff[$j + $n]->new
);
}
} else {
for (; $i < $k; $i++) {
$newDiff[] = $diff[$i];
}
}
$i = $k - 1;
}
return $newDiff;
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace PhpParser\Internal;
use PHPUnit\Framework\TestCase;
class DifferTest extends TestCase {
private function formatDiffString(array $diff) {
$diffStr = '';
foreach ($diff as $diffElem) {
switch ($diffElem->type) {
case DiffElem::TYPE_KEEP:
$diffStr .= $diffElem->old;
break;
case DiffElem::TYPE_REMOVE:
$diffStr .= '-' . $diffElem->old;
break;
case DiffElem::TYPE_ADD:
$diffStr .= '+' . $diffElem->new;
break;
case DiffElem::TYPE_REPLACE:
$diffStr .= '/' . $diffElem->old . $diffElem->new;
break;
default:
assert(false);
break;
}
}
return $diffStr;
}
/** @dataProvider provideTestDiff */
public function testDiff($oldStr, $newStr, $expectedDiffStr) {
$differ = new Differ(function($a, $b) { return $a === $b; });
$diff = $differ->diff(str_split($oldStr), str_split($newStr));
$this->assertSame($expectedDiffStr, $this->formatDiffString($diff));
}
public function provideTestDiff() {
return [
['abc', 'abc', 'abc'],
['abc', 'abcdef', 'abc+d+e+f'],
['abcdef', 'abc', 'abc-d-e-f'],
['abcdef', 'abcxyzdef', 'abc+x+y+zdef'],
['axyzb', 'ab', 'a-x-y-zb'],
['abcdef', 'abxyef', 'ab-c-d+x+yef'],
['abcdef', 'cdefab', '-a-bcdef+a+b'],
];
}
/** @dataProvider provideTestDiffWithReplacements */
public function testDiffWithReplacements($oldStr, $newStr, $expectedDiffStr) {
$differ = new Differ(function($a, $b) { return $a === $b; });
$diff = $differ->diffWithReplacements(str_split($oldStr), str_split($newStr));
$this->assertSame($expectedDiffStr, $this->formatDiffString($diff));
}
public function provideTestDiffWithReplacements() {
return [
['abcde', 'axyze', 'a/bx/cy/dze'],
['abcde', 'xbcdy', '/axbcd/ey'],
['abcde', 'axye', 'a-b-c-d+x+ye'],
['abcde', 'axyzue', 'a-b-c-d+x+y+z+ue'],
];
}
}