From 44208e14c1957da0471f9f000f429ece604905b2 Mon Sep 17 00:00:00 2001 From: Jack Cherng Date: Mon, 22 Jun 2026 20:54:20 +0800 Subject: [PATCH] fix(Differ): use strict identity for old/new comparison to avoid numeric string coercion PHP's spaceship operator treats numeric strings like '0' and '00000' as equal, causing empty diffs. Replace with === and refactor to bool $oldNewAreSame property. Deprecate getOldNewComparison() in favor of getOldNewAreSame(). Fixes #94 Co-Authored-By: Claude Signed-off-by: Jack Cherng --- src/Differ.php | 25 +++++++++++++++++-------- src/Renderer/AbstractRenderer.php | 2 +- tests/DifferTest.php | 28 ++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/src/Differ.php b/src/Differ.php index f03206e..d85363f 100644 --- a/src/Differ.php +++ b/src/Differ.php @@ -39,7 +39,7 @@ final class Differ 'groupedOpcodesGnu' => [], 'oldNoEolAtEofIdx' => -1, 'newNoEolAtEofIdx' => -1, - 'oldNewComparison' => 0, + 'oldNewAreSame' => false, ]; /** @@ -84,10 +84,9 @@ final class Differ private int $newNoEolAtEofIdx = -1; /** - * @var int the result of comparing the old and the new with the spaceship operator - * `-1` means `old < new`, `0` means `old == new`, `1` means `old > new` + * @var bool the result of spaceship operator comparing old and new (0 if they are the same, 1 if they are different) */ - private int $oldNewComparison = 0; + private bool $oldNewAreSame = false; /** * @var int[][][] array containing the generated opcodes for the differences between the two items @@ -232,12 +231,22 @@ public function getNewNoEolAtEofIdx(): int return $this->finalize()->newNoEolAtEofIdx; } + /** + * Check whether the old and the new are the same. + */ + public function getOldNewAreSame(): bool + { + return $this->finalize()->oldNewAreSame; + } + /** * Compare the old and the new with the spaceship operator. + * + * @deprecated Use getOldNewAreSame() instead */ public function getOldNewComparison(): int { - return $this->finalize()->oldNewComparison; + return $this->getOldNewAreSame() ? 0 : 1; } /** @@ -302,7 +311,7 @@ public function getGroupedOpcodes(): array $this->getGroupedOpcodesPre($old, $new); - if ($this->oldNewComparison === 0 && $this->options->fullContextIfIdentical) { + if ($this->oldNewAreSame && $this->options->fullContextIfIdentical) { $opcodes = [ [ [SequenceMatcher::OP_EQ, 0, \count($old), 0, \count($new)], @@ -338,7 +347,7 @@ public function getGroupedOpcodesGnu(): array $this->getGroupedOpcodesGnuPre($old, $new); - if ($this->oldNewComparison === 0 && $this->options->fullContextIfIdentical) { + if ($this->oldNewAreSame && $this->options->fullContextIfIdentical) { $opcodes = [ [ [SequenceMatcher::OP_EQ, 0, \count($old), 0, \count($new)], @@ -483,7 +492,7 @@ private function finalize(): self $this->oldNoEolAtEofIdx = $this->getOld(-1) === [''] ? -1 : \count($this->old); $this->newNoEolAtEofIdx = $this->getNew(-1) === [''] ? -1 : \count($this->new); - $this->oldNewComparison = $this->old <=> $this->new; + $this->oldNewAreSame = $this->old === $this->new; $this->sequenceMatcher->setOptions($this->options->toSequenceMatcherOptions()); } diff --git a/src/Renderer/AbstractRenderer.php b/src/Renderer/AbstractRenderer.php index c5e5401..dc89ec2 100644 --- a/src/Renderer/AbstractRenderer.php +++ b/src/Renderer/AbstractRenderer.php @@ -114,7 +114,7 @@ final public function render(Differ $differ): string $this->changesAreRaw = true; // the "no difference" situation may happen frequently - return $differ->getOldNewComparison() === 0 && !$differ->options->fullContextIfIdentical + return $differ->getOldNewAreSame() && !$differ->options->fullContextIfIdentical ? $this->getResultForIdenticals() : $this->renderWorker($differ); } diff --git a/tests/DifferTest.php b/tests/DifferTest.php index 985e201..899c15e 100644 --- a/tests/DifferTest.php +++ b/tests/DifferTest.php @@ -67,4 +67,32 @@ public static function provideGetGroupedOpcodesCases(): iterable ], ]; } + + /** + * Test that numeric strings are compared as strings, not numbers. + * + * @covers \Jfcherng\Diff\Differ::getOldNewAreSame + * + * @see https://github.com/jfcherng/php-diff/issues/94 + */ + #[DataProvider('provideNumericStringComparisonCases')] + public function testNumericStringComparison(string $old, string $new, bool $expectedAreSame): void + { + $differ = new Differ(explode("\n", $old), explode("\n", $new)); + + self::assertSame($expectedAreSame, $differ->getOldNewAreSame()); + } + + /** + * Data provider for testNumericStringComparison. + */ + public static function provideNumericStringComparisonCases(): iterable + { + return [ + 'zero vs multiple zeros' => ['0', '00000', false], + 'identical zeros' => ['0', '0', true], + 'numeric strings with different values' => ['1', '01', false], + 'leading zeros' => ['007', '7', false], + ]; + } }