Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Failing test
  • Loading branch information
dantleech committed Dec 7, 2025
commit 263cea989556d15f4491f1f5b820c709cd5d65a1
29 changes: 22 additions & 7 deletions lib/TolerantAstDiff/AstDiff.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
use Microsoft\PhpParser\Token;
use Phpactor\TextDocument\TextEdit;
use Phpactor\TextDocument\TextEdits;
use Phpactor\WorseReflection\Core\Util\NodeUtil;
use RuntimeException;
use function DeepCopy\deep_copy;

final class AstDiff
{
Expand Down Expand Up @@ -165,11 +167,20 @@ private function applyEdit(Node $node, TextEdit $edit): void
{
$source = $this->fileSource1;
$distance = strlen($edit->replacement()) - $edit->length();
$existing = substr($source, $edit->start()->toInt(), $edit->length());

if ($existing === $edit->replacement()) {
return;
}

foreach ($source->getDescendantTokens() as $token) {
if ($token->getFullStartPosition() <= $edit->start()->toInt()) {
if ($token->getFullStartPosition() < $edit->start()->toInt()) {
continue;
}
// dump(
// Token::getTokenKindNameFromValue($token->kind),
// $token->getText($source)
// );
$token->start += $distance;
$token->fullStart += $distance;
}
Expand All @@ -193,24 +204,28 @@ private function mapNode(Node $node1, Node $node2): void
$member2 = $node2->$childName;

if (is_array($member1)) {
//foreach ($member1 as $member) {
// $lastPosition = $member->getFullStartPosition();
//}
foreach ($member1 as $member) {
assert($member instanceof Token || $member instanceof Node);
$lastPosition = $member->getFullStartPosition();
}
continue;
}

if ($member1 !== null) {
if ($member1 instanceof Node || $member1 instanceof Token) {
$lastPosition = $member1->getFullStartPosition();
}

if ($member2 instanceof Token || $member2 === null) {
$node1->$childName = $member2;

$this->applyEdit($node1, TextEdit::create(
$lastPosition,
$member1?->getFullWidth() ?? 0,
$member2?->getFullText($this->fileSource2->getFileContents()) ?? '',
));
if ($member2 !== null) {
// TODO: do we care if we modify the "new" AST by reference?
$member2 = deep_copy($member2);
}
$node1->$childName = $member2;
continue;
}
}
Expand Down
38 changes: 35 additions & 3 deletions lib/TolerantAstDiff/Tests/Unit/AstDiffTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,17 @@ public function testDiffTree(string $source1, string $source2): void
$diff = (new AstDiff());
$diff->merge($ast1, $ast2);

self::assertEquals($ast2->getText(), $ast1->getText());
self::assertSame($ast2->getText(), $ast1->getText());
self::assertSame($ast2->getFullWidth(), $ast1->getFullWidth());
}
/**
* @return Generator<string,array{string,string}>
*/
public static function provideDiffTree(): Generator
{
yield 'same' => [
'<?php function hello(): string { echo "hello"; }',
'<?php function hello(): string { echo "hello"; }',
'<?php function hello(): string { echo "hello"; } ',
'<?php function hello(): string { echo "hello"; } ',
];

yield 'remove 1' => [
Expand Down Expand Up @@ -131,5 +132,36 @@ public function bar()
}
PHP
];

yield 'misc' => [
<<<'PHP'
<?php
function a() {
if (true) {
echo 'hello';
echo 'goodbye';
}
if (true) {
echo 'coming';
echo 'going';
}
}
PHP,
<<<'PHP'
<?php
function a() {
if (true) {
echo 'hello';
echo 'goodbye';
}


if (true) {
echo 'coming';
echo 'going';
}
}
PHP,
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,17 @@ public function parseSourceFile(string $source, ?string $uri = null): SourceFile
}
if (!isset($this->documents[$uri])) {

@dantleech dantleech Dec 7, 2025 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably, for now, use a Ttl caching mechanism, but should use the #2882

$node = parent::parseSourceFile($source);
dump(sprintf('ini: %s (%s)', $node->getFullWidth(), strlen($node->getText())));
$this->documents[$uri] = $node;
return $node;
}

$node1 = $this->documents[$uri];
$node2 = parent::parseSourceFile($source);

dump(sprintf('new: %s (%s)', $node2->getFullWidth(), strlen($node2->getText())));
$this->merger->merge($node1, $node2);
dump(sprintf('upd: %s (%s)', $node1->getFullWidth(), strlen($node1->getText())));
return $node1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function a() {
$node2OriginalId = spl_object_id($node);

// new source code introduces new line between the two nodes
$ast = $parser->parseSourceFile(<<<'PHP'
$ast = $parser->parseSourceFile($source = <<<'PHP'
<?php
function a() {
if (true) {
Expand All @@ -51,11 +51,38 @@ function a() {
}
PHP, 'file://path');

self::assertEquals($source, $ast->getText());
$node = $ast->getDescendantNodeAtPosition(46);
self::assertInstanceOf(EchoStatement::class, $node);
self::assertSame($node1OriginalId, spl_object_id($node));
$node = $ast->getDescendantNodeAtPosition(115);
self::assertInstanceOf(EchoStatement::class, $node);
self::assertEquals($node2OriginalId, spl_object_id($node));

// new source code introduces a new statement
$ast = $parser->parseSourceFile($source = <<<'PHP'
<?php
function a() {
if (true) {
echo 'hello';
echo 'goodbye';
}

echo "fo";

if (true) {
echo 'coming';
echo 'going';
}
}
PHP, 'file://path');

self::assertEquals($source, $ast->getText());
$node = $ast->getDescendantNodeAtPosition(46);
self::assertInstanceOf(EchoStatement::class, $node);
self::assertSame($node1OriginalId, spl_object_id($node));
$node = $ast->getDescendantNodeAtPosition(126);
self::assertInstanceOf(EchoStatement::class, $node);
self::assertEquals($node2OriginalId, spl_object_id($node));
}
}