1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

Add way of getting changes in a given file

This commit is contained in:
Brown 2018-09-25 17:52:27 -04:00 committed by Matthew Brown
parent ff893a3fb2
commit 71b7c70eb1
7 changed files with 992 additions and 0 deletions

View File

@ -20,6 +20,7 @@
<directory name="src/Psalm/Stubs" />
<directory name="tests/stubs" />
<file name="vendor/phpunit/phpunit/src/Framework/TestCase.php" />
<file name="src/Psalm/Diff/Differ.php" />
</ignoreFiles>
</projectFiles>

View File

@ -0,0 +1,148 @@
<?php
namespace Psalm\Diff;
use PhpParser;
/**
* @internal
*/
class ClassStatementsDiffer extends Differ
{
/** @var PhpParser\PrettyPrinter\Standard|null */
private static $pretty_printer;
/**
* Calculate diff (edit script) from $a to $b.
*
* @param PhpParser\Node\Stmt[] $a
* @param PhpParser\Node\Stmt[] $b New array
*
* @return array{0:array<int, string>, 1:array<int, string>}
*/
public static function diff(string $name, array $a, array $b, string $a_code, string $b_code)
{
list($trace, $x, $y, $bc) = self::calculateTrace(
function (
PhpParser\Node\Stmt $a,
PhpParser\Node\Stmt $b,
string $a_code,
string $b_code,
bool &$body_change = false
) : bool {
if (get_class($a) !== get_class($b)) {
return false;
}
$a_start = (int)$a->getAttribute('startFilePos');
$a_end = (int)$a->getAttribute('endFilePos');
$b_start = (int)$b->getAttribute('startFilePos');
$b_end = (int)$b->getAttribute('endFilePos');
$a_comments_end = $a_start;
$b_comments_end = $b_start;
$a_comments = $a->getComments();
$b_comments = $b->getComments();
$signature_change = false;
$body_change = false;
if ($a_comments) {
if (!$b_comments) {
$signature_change = true;
}
$a_start = $a_comments[0]->getFilePos();
}
if ($b_comments) {
if (!$a_comments) {
$signature_change = true;
}
$b_start = $b_comments[0]->getFilePos();
}
$a_size = $a_end - $a_start;
$b_size = $b_end - $b_start;
if (substr($a_code, $a_start, $a_size) === substr($b_code, $b_start, $b_size)) {
return true;
}
if (!$signature_change
&& substr($a_code, $a_start, $a_comments_end - $a_start)
!== substr($b_code, $b_start, $b_comments_end - $b_start)
) {
$signature_change = true;
}
if (!self::$pretty_printer) {
self::$pretty_printer = new PhpParser\PrettyPrinter\Standard;
}
if ($a instanceof PhpParser\Node\Stmt\ClassMethod && $b instanceof PhpParser\Node\Stmt\ClassMethod) {
if ((string) $a->name !== (string) $b->name) {
return false;
}
$a_stmts = $a->stmts;
$a->stmts = [];
$b_stmts = $b->stmts;
$b->stmts = [];
$body_change = $a_stmts !== $b_stmts
&& self::$pretty_printer->prettyPrint($a_stmts ?: [])
!== self::$pretty_printer->prettyPrint($b_stmts ?: []);
$signature_change = $signature_change
|| self::$pretty_printer->prettyPrint([$a]) !== self::$pretty_printer->prettyPrint([$b]);
$a->stmts = $a_stmts;
$b->stmts = $b_stmts;
} else {
$signature_change = $signature_change
|| self::$pretty_printer->prettyPrint([$a]) !== self::$pretty_printer->prettyPrint([$b]);
}
return !$signature_change;
},
$a,
$b,
$a_code,
$b_code
);
$diff = self::extractDiff($trace, $x, $y, $a, $b, $bc);
$keep = [];
$keep_signature = [];
foreach ($diff as $diff_elem) {
if ($diff_elem->type === DiffElem::TYPE_KEEP) {
if ($diff_elem->old instanceof PhpParser\Node\Stmt\ClassMethod) {
$keep[] = strtolower($name) . '::' . strtolower((string) $diff_elem->old->name);
} elseif ($diff_elem->old instanceof PhpParser\Node\Stmt\Property) {
foreach ($diff_elem->old->props as $prop) {
$keep[] = strtolower($name) . '::$' . $prop->name;
}
} elseif ($diff_elem->old instanceof PhpParser\Node\Stmt\ClassConst) {
foreach ($diff_elem->old->consts as $const) {
$keep[] = strtolower($name) . '::' . $const->name;
}
}
} elseif ($diff_elem->type === DiffElem::TYPE_KEEP_SIGNATURE) {
if ($diff_elem->old instanceof PhpParser\Node\Stmt\ClassMethod) {
$keep_signature[] = strtolower($name) . '::' . strtolower((string) $diff_elem->old->name);
} elseif ($diff_elem->old instanceof PhpParser\Node\Stmt\Property) {
foreach ($diff_elem->old->props as $prop) {
$keep_signature[] = strtolower($name) . '::$' . $prop->name;
}
}
}
};
return [$keep, $keep_signature];
}
}

View File

@ -0,0 +1,34 @@
<?php declare(strict_types=1);
namespace Psalm\Diff;
/**
* @internal
*/
class DiffElem
{
const TYPE_KEEP = 0;
const TYPE_REMOVE = 1;
const TYPE_ADD = 2;
const TYPE_REPLACE = 3;
const TYPE_KEEP_SIGNATURE = 4;
/** @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;
/**
* @param int $type
* @param mixed $old
* @param mixed $new
*/
public function __construct(int $type, $old, $new)
{
$this->type = $type;
$this->old = $old;
$this->new = $new;
}
}

110
src/Psalm/Diff/Differ.php Normal file
View File

@ -0,0 +1,110 @@
<?php declare(strict_types=1);
namespace Psalm\Diff;
use PhpParser;
/**
* Borrows from https://github.com/nikic/PHP-Parser/blob/master/lib/PhpParser/Internal/Differ.php
*
* 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
{
/**
* @param \Closure $is_equal
* @param array $a
* @param array $b
* @return array{0:array, 1: int, 2: int, 3: array<int, true>}
*/
protected static function calculateTrace(
\Closure $is_equal,
array $a,
array $b,
string $a_code,
string $b_code
) : array {
$n = \count($a);
$m = \count($b);
$max = $n + $m;
$v = [1 => 0];
$bc = [];
$trace = [];
$body_change = false;
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 && ($is_equal)($a[$x], $b[$y], $a_code, $b_code, $body_change)) {
$bc[$x] = $body_change;
$x++;
$y++;
}
$v[$k] = $x;
if ($x >= $n && $y >= $m) {
return [$trace, $x, $y, $bc];
}
}
}
throw new \Exception('Should not happen');
}
/**
* @return DiffElem[]
*/
protected static function extractDiff(array $trace, int $x, int $y, array $a, array $b, array $bc) : array
{
$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(
$bc[$x-1] ? DiffElem::TYPE_KEEP_SIGNATURE : 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);
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace Psalm\Diff;
use PhpParser;
/**
* @internal
*/
class FileStatementsDiffer extends Differ
{
/**
* Calculate diff (edit script) from $a to $b.
*
* @param PhpParser\Node\Stmt[] $a
* @param PhpParser\Node\Stmt[] $b New array
*
* @return array{0:array<int, string>, 1:array<int, string>}
*/
public static function diff(array $a, array $b, string $a_code, string $b_code)
{
list($trace, $x, $y, $bc) = self::calculateTrace(
function (PhpParser\Node\Stmt $a, PhpParser\Node\Stmt $b, string $a_code, string $b_code) : bool {
if (get_class($a) !== get_class($b)) {
return false;
}
if (($a instanceof PhpParser\Node\Stmt\Namespace_ && $b instanceof PhpParser\Node\Stmt\Namespace_)
|| ($a instanceof PhpParser\Node\Stmt\Class_ && $b instanceof PhpParser\Node\Stmt\Class_)
|| ($a instanceof PhpParser\Node\Stmt\Interface_ && $b instanceof PhpParser\Node\Stmt\Interface_)
) {
return (string)$a->name === (string)$b->name;
}
return false;
},
$a,
$b,
$a_code,
$b_code
);
$diff = self::extractDiff($trace, $x, $y, $a, $b, $bc);
$keep = [];
$keep_signature = [];
foreach ($diff as $diff_elem) {
if ($diff_elem->type === DiffElem::TYPE_KEEP) {
if ($diff_elem->old instanceof PhpParser\Node\Stmt\Namespace_
&& $diff_elem->new instanceof PhpParser\Node\Stmt\Namespace_
) {
$namespace_keep = NamespaceStatementsDiffer::diff(
(string) $diff_elem->old->name,
$diff_elem->old->stmts,
$diff_elem->new->stmts,
$a_code,
$b_code
);
$keep = array_merge($keep, $namespace_keep[0]);
$keep_signature = array_merge($keep_signature, $namespace_keep[1]);
} elseif (($diff_elem->old instanceof PhpParser\Node\Stmt\Class_
&& $diff_elem->new instanceof PhpParser\Node\Stmt\Class_)
|| ($diff_elem->old instanceof PhpParser\Node\Stmt\Interface_
&& $diff_elem->new instanceof PhpParser\Node\Stmt\Interface_)
) {
$class_keep = ClassStatementsDiffer::diff(
(string) $diff_elem->old->name,
$diff_elem->old->stmts,
$diff_elem->new->stmts,
$a_code,
$b_code
);
$keep = array_merge($keep, $class_keep[0]);
$keep_signature = array_merge($keep_signature, $class_keep[1]);
}
}
}
return [$keep, $keep_signature];
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace Psalm\Diff;
use PhpParser;
/**
* @internal
*/
class NamespaceStatementsDiffer extends Differ
{
/**
* Calculate diff (edit script) from $a to $b.
*
* @param PhpParser\Node\Stmt[] $a
* @param PhpParser\Node\Stmt[] $b New array
*
* @return array{0:array<int, string>, 1:array<int, string>}
*/
public static function diff(string $name, array $a, array $b, string $a_code, string $b_code)
{
list($trace, $x, $y, $bc) = self::calculateTrace(
function (PhpParser\Node\Stmt $a, PhpParser\Node\Stmt $b, string $a_code, string $b_code) : bool {
if (($a instanceof PhpParser\Node\Stmt\Class_ && $b instanceof PhpParser\Node\Stmt\Class_)
|| ($a instanceof PhpParser\Node\Stmt\Interface_ && $b instanceof PhpParser\Node\Stmt\Interface_)
) {
// @todo add check for comments comparison
return (string)$a->name === (string)$b->name;
}
return false;
},
$a,
$b,
$a_code,
$b_code
);
$diff = self::extractDiff($trace, $x, $y, $a, $b, $bc);
$keep = [];
$keep_signature = [];
foreach ($diff as $diff_elem) {
if ($diff_elem->type === DiffElem::TYPE_KEEP) {
if (($diff_elem->old instanceof PhpParser\Node\Stmt\Class_
&& $diff_elem->new instanceof PhpParser\Node\Stmt\Class_)
|| ($diff_elem->old instanceof PhpParser\Node\Stmt\Interface_
&& $diff_elem->new instanceof PhpParser\Node\Stmt\Interface_)
) {
$class_keep = ClassStatementsDiffer::diff(
($name ? $name . '\\' : '') . $diff_elem->old->name,
$diff_elem->old->stmts,
$diff_elem->new->stmts,
$a_code,
$b_code
);
$keep = array_merge($keep, $class_keep[0]);
$keep_signature = array_merge($keep_signature, $class_keep[1]);
}
}
}
return [$keep, $keep_signature];
}
}

547
tests/FileDiffTest.php Normal file
View File

@ -0,0 +1,547 @@
<?php
namespace Psalm\Tests;
class FileDiffTest extends TestCase
{
/**
* @dataProvider getChanges
*
* @param string $a
* @param string $b
* @param string[] $same_methods
*
* @return void
*/
public function testCode(string $a, string $b, array $same_methods, array $same_signatures = [])
{
if (strpos($this->getTestName(), 'SKIPPED-') !== false) {
$this->markTestSkipped();
}
$a_stmts = \Psalm\Provider\StatementsProvider::parseStatements($a);
$b_stmts = \Psalm\Provider\StatementsProvider::parseStatements($b);
$diff = \Psalm\Diff\FileStatementsDiffer::diff($a_stmts, $b_stmts, $a, $b);
$this->assertSame(
$same_methods,
$diff[0]
);
$this->assertSame(
$same_signatures,
$diff[1]
);
}
/**
* @return array
*/
public function getChanges()
{
return [
'sameFile' => [
'<?php
namespace Foo;
class A {
public $aB = 5;
const F = 1;
public function foo() {
$a = 1;
}
public function bar() {
$b = 1;
}
}',
'<?php
namespace Foo;
class A {
public $aB = 5;
const F = 1;
public function foo() {
$a = 1;
}
public function bar() {
$b = 1;
}
}',
['foo\a::$aB', 'foo\a::F', 'foo\a::foo', 'foo\a::bar']
],
'simpleBodyChange' => [
'<?php
namespace Foo;
class A {
public function foo() {
$a = 1;
}
public function bar() {
$b = 1;
}
}',
'<?php
namespace Foo;
class A {
public function foo() {
$a = 1;
}
public function bar() {
$b = 2;
}
}',
['foo\a::foo'],
['foo\a::bar']
],
'simpleBodyChangeWithSignatureChange' => [
'<?php
namespace Foo;
class A {
public function foo() {
$a = 1;
}
public function bar() {
$b = 1;
}
}',
'<?php
namespace Foo;
class A {
public function foo() {
$a = 1;
}
public function bar(string $a) {
$b = 1;
}
}',
['foo\a::foo'],
[]
],
'propertyChange' => [
'<?php
namespace Foo;
class A {
public $a;
}',
'<?php
namespace Foo;
class A {
public $b;
}',
[]
],
'propertyDefaultChange' => [
'<?php
namespace Foo;
class A {
public $a = 1;
}',
'<?php
namespace Foo;
class A {
public $a = 2;
}',
[]
],
'propertyDefaultAddition' => [
'<?php
namespace Foo;
class A {
public $a;
}',
'<?php
namespace Foo;
class A {
public $a = 2;
}',
[]
],
'propertySignatureChange' => [
'<?php
namespace Foo;
class A {
/** @var ?string */
public $a;
}',
'<?php
namespace Foo;
class A {
/** @var ?int */
public $a;
}',
[]
],
'addDocblock' => [
'<?php
namespace Foo;
class A {
public function foo() {
$a = 1;
}
public function bar() {
$b = 2;
}
}',
'<?php
namespace Foo;
class A {
public function foo() {
$a = 1;
}
/**
* @return void
*/
public function bar() {
$b = 2;
}
}',
['foo\a::foo']
],
'removeDocblock' => [
'<?php
namespace Foo;
class A {
public function foo() {
$a = 1;
}
/**
* @return void
*/
public function bar() {
$b = 2;
}
}',
'<?php
namespace Foo;
class A {
public function foo() {
$a = 1;
}
public function bar() {
$b = 2;
}
}',
['foo\a::foo']
],
'changeDocblock' => [
'<?php
namespace Foo;
class A {
public function foo() {
$a = 1;
}
/**
* @return string
*/
public function bar() {
$b = 2;
}
}',
'<?php
namespace Foo;
class A {
public function foo() {
$a = 1;
}
/**
* @return void
*/
public function bar() {
$b = 2;
}
}',
['foo\a::foo']
],
'removeFunctionAtEnd' => [
'<?php
namespace Foo;
class A {
public function foo() {
$a = 1;
}
public function bar() {
$b = 1;
}
public function bat() {
$c = 1;
}
}',
'<?php
namespace Foo;
class A {
public function foo() {
$a = 1;
}
public function bar() {
$b = 1;
}
}',
['foo\a::foo', 'foo\a::bar']
],
'removeFunctionAtBeginning' => [
'<?php
namespace Foo;
class A {
public function foo() {
$a = 1;
}
public function bar() {
$b = 1;
}
public function bat() {
$c = 1;
}
}',
'<?php
namespace Foo;
class A {
public function bar() {
$b = 1;
}
public function bat() {
$c = 1;
}
}',
['foo\a::bar', 'foo\a::bat']
],
'removeFunctionInMiddle' => [
'<?php
namespace Foo;
class A {
public function foo() {
$a = 1;
}
public function bar() {
$b = 1;
}
public function bat() {
$c = 1;
}
}',
'<?php
namespace Foo;
class A {
public function foo() {
$a = 1;
}
public function bat() {
$c = 1;
}
}',
['foo\a::foo', 'foo\a::bat']
],
'changeNamespace' => [
'<?php
namespace Bar;
class A {
public function foo() {
$a = 1;
}
public function bar() {
$b = 1;
}
}',
'<?php
namespace Foo;
class A {
public function bar() {
$b = 2;
}
}',
[]
],
'removeNamespace' => [
'<?php
namespace Bar;
class A {
public function foo() {
$a = 1;
}
public function bar() {
$b = 1;
}
}',
'<?php
class A {
public function bar() {
$b = 2;
}
}',
[],
],
'newFunctionAtEnd' => [
'<?php
namespace Foo;
class A {
public function foo() {
$a = 1;
}
public function bar() {
$b = 1;
}
}',
'<?php
namespace Foo;
class A {
public function foo() {
$a = 1;
}
public function bar() {
$b = 1;
}
public function bat() {
$c = 1;
}
}',
['foo\a::foo', 'foo\a::bar']
],
'newFunctionAtBeginning' => [
'<?php
namespace Foo;
class A {
public function foo() {
$a = 1;
}
public function bar() {
$b = 1;
}
}',
'<?php
namespace Foo;
class A {
public function bat() {
$c = 1;
}
public function foo() {
$a = 1;
}
public function bar() {
$b = 1;
}
}',
['foo\a::foo', 'foo\a::bar']
],
'newFunctionInMiddle' => [
'<?php
namespace Foo;
class A {
public function foo() {
$a = 1;
}
public function bar() {
$b = 1;
}
}',
'<?php
namespace Foo;
class A {
public function foo() {
$a = 1;
}
public function bat() {
$c = 1;
}
public function bar() {
$b = 1;
}
}',
['foo\a::foo', 'foo\a::bar']
],
'whiteSpaceOnly' => [
'<?php
namespace Foo;
class A {
public function foo() {
$a = 1;
}
public function bar() {
$b = 1;
}
}',
'<?php
namespace Foo;
class A {
public function foo() {
$a = 1 ;
}
public function bar() {
$b = 1;
}
}',
['foo\a::foo', 'foo\a::bar']
],
'changeDeclaredMethodId' => [
'<?php
namespace Foo;
class A {
public function __construct() {}
public static function bar() : void {}
}
class B extends A {
public static function bat() : void {}
}
class C extends B { }',
'<?php
namespace Foo;
class A {
public function __construct() {}
public static function bar() : void {}
}
class B extends A {
public function __construct() {}
public static function bar() : void {}
public static function bat() : void {}
}
class C extends B { }',
['foo\a::__construct', 'foo\a::bar', 'foo\b::bat']
],
];
}
}