From dbc8c680b3595dcf2da17ac1ed5a194d3a62e3a0 Mon Sep 17 00:00:00 2001 From: Sajib Sarkar Date: Thu, 25 Jun 2026 12:34:42 +0600 Subject: [PATCH 1/7] Before modifying the code for code smells --- pom.xml | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/pom.xml b/pom.xml index 837fca6..ed15f20 100644 --- a/pom.xml +++ b/pom.xml @@ -132,28 +132,28 @@ ${project.build.sourceDirectory} - + - - - - - - - - - - - + + + + + + + + + + + - + - - - - + + + + - + @@ -191,7 +191,7 @@ 2.30.0 - + true 2 From 11edc3a95c3bdb0ed296161ae998869797082816 Mon Sep 17 00:00:00 2001 From: Sajib Sarkar Date: Thu, 25 Jun 2026 14:15:35 +0600 Subject: [PATCH 2/7] Refactored to solve coupling issue in 'DiffUtils.java' --- java-diff-utils/src/main/java/com/github/difflib/DiffUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java-diff-utils/src/main/java/com/github/difflib/DiffUtils.java b/java-diff-utils/src/main/java/com/github/difflib/DiffUtils.java index 8448a76..d6569bb 100644 --- a/java-diff-utils/src/main/java/com/github/difflib/DiffUtils.java +++ b/java-diff-utils/src/main/java/com/github/difflib/DiffUtils.java @@ -116,7 +116,7 @@ public static Patch diff( if (equalizer != null) { return DiffUtils.diff(source, target, DEFAULT_DIFF.create(equalizer)); } - return DiffUtils.diff(source, target, new MyersDiff<>()); + return DiffUtils.diff(source, target, DEFAULT_DIFF.create()); } public static Patch diff( From 8e664877c4738ccc9d483267cb3dd20ba0072f2a Mon Sep 17 00:00:00 2001 From: Sajib Sarkar Date: Thu, 25 Jun 2026 22:53:59 +0600 Subject: [PATCH 3/7] Fixed the coupling and God class issue in 'DiffUtils.java' --- .../github/difflib/DiffAlgorithmDefaults.java | 15 ++++ .../java/com/github/difflib/DiffUtils.java | 69 +------------------ .../com/github/difflib/InlineDiffUtils.java | 49 +++++++++++++ .../java/com/github/difflib/PatchUtils.java | 37 ++++++++++ .../com/github/difflib/DiffUtilsTest.java | 4 +- .../difflib/GenerateUnifiedDiffTest.java | 4 +- ...WithMyersDiffWithLinearSpacePatchTest.java | 11 +-- .../github/difflib/examples/ApplyPatch.java | 4 +- .../patch/PatchWithAllDiffAlgorithmsTest.java | 9 +-- .../difflib/patch/PatchWithMyerDiffTest.java | 3 +- .../PatchWithMyerDiffWithLinearSpaceTest.java | 3 +- .../unifieddiff/UnifiedDiffRoundTripTest.java | 3 +- 12 files changed, 127 insertions(+), 84 deletions(-) create mode 100644 java-diff-utils/src/main/java/com/github/difflib/DiffAlgorithmDefaults.java create mode 100644 java-diff-utils/src/main/java/com/github/difflib/InlineDiffUtils.java create mode 100644 java-diff-utils/src/main/java/com/github/difflib/PatchUtils.java diff --git a/java-diff-utils/src/main/java/com/github/difflib/DiffAlgorithmDefaults.java b/java-diff-utils/src/main/java/com/github/difflib/DiffAlgorithmDefaults.java new file mode 100644 index 0000000..004a3df --- /dev/null +++ b/java-diff-utils/src/main/java/com/github/difflib/DiffAlgorithmDefaults.java @@ -0,0 +1,15 @@ +package com.github.difflib; + +import com.github.difflib.algorithm.DiffAlgorithmFactory; +import com.github.difflib.algorithm.myers.MyersDiff; + +/** + * Default algorithm configuration for DiffUtils. + */ +public final class DiffAlgorithmDefaults { + public static DiffAlgorithmFactory getDefault() { + return MyersDiff.factory(); + } + + private DiffAlgorithmDefaults() {} +} diff --git a/java-diff-utils/src/main/java/com/github/difflib/DiffUtils.java b/java-diff-utils/src/main/java/com/github/difflib/DiffUtils.java index d6569bb..85514e0 100644 --- a/java-diff-utils/src/main/java/com/github/difflib/DiffUtils.java +++ b/java-diff-utils/src/main/java/com/github/difflib/DiffUtils.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -18,26 +18,21 @@ import com.github.difflib.algorithm.DiffAlgorithmFactory; import com.github.difflib.algorithm.DiffAlgorithmI; import com.github.difflib.algorithm.DiffAlgorithmListener; -import com.github.difflib.algorithm.myers.MyersDiff; -import com.github.difflib.patch.AbstractDelta; import com.github.difflib.patch.Patch; -import com.github.difflib.patch.PatchFailedException; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.function.BiPredicate; /** - * Utility class to implement the difference and patching engine. + * Utility class to implement the difference engine. */ public final class DiffUtils { /** * This factory generates the DEFAULT_DIFF algorithm for all these routines. */ - static DiffAlgorithmFactory DEFAULT_DIFF = MyersDiff.factory(); + static DiffAlgorithmFactory DEFAULT_DIFF = DiffAlgorithmDefaults.getDefault(); /** * Sets the default diff algorithm factory to be used by all diff routines. @@ -167,63 +162,5 @@ public static Patch diff( return diff(original, revised, algorithm, null); } - /** - * Computes the difference between the given texts inline. This one uses the - * "trick" to make out of texts lists of characters, like DiffRowGenerator - * does and merges those changes at the end together again. - * - * @param original a {@link String} representing the original text. Must not be {@code null}. - * @param revised a {@link String} representing the revised text. Must not be {@code null}. - * @return The patch describing the difference between the original and - * revised sequences. Never {@code null}. - */ - public static Patch diffInline(String original, String revised) { - List origList = new ArrayList<>(); - List revList = new ArrayList<>(); - for (Character character : original.toCharArray()) { - origList.add(character.toString()); - } - for (Character character : revised.toCharArray()) { - revList.add(character.toString()); - } - Patch patch = DiffUtils.diff(origList, revList); - for (AbstractDelta delta : patch.getDeltas()) { - delta.getSource().setLines(compressLines(delta.getSource().getLines(), "")); - delta.getTarget().setLines(compressLines(delta.getTarget().getLines(), "")); - } - return patch; - } - - /** - * Applies the given patch to the original list and returns the revised list. - * - * @param original a {@link List} representing the original list. - * @param patch a {@link List} representing the patch to apply. - * @return the revised list. - * @throws PatchFailedException if the patch cannot be applied. - */ - public static List patch(List original, Patch patch) throws PatchFailedException { - return patch.applyTo(original); - } - - /** - * Applies the given patch to the revised list and returns the original list. - * - * @param revised a {@link List} representing the revised list. - * @param patch a {@link Patch} representing the patch to apply. - * @return the original list. - * @throws PatchFailedException if the patch cannot be applied. - */ - public static List unpatch(List revised, Patch patch) { - return patch.restore(revised); - } - - private static List compressLines(List lines, String delimiter) { - if (lines.isEmpty()) { - return Collections.emptyList(); - } - return Collections.singletonList(String.join(delimiter, lines)); - } - private DiffUtils() {} } diff --git a/java-diff-utils/src/main/java/com/github/difflib/InlineDiffUtils.java b/java-diff-utils/src/main/java/com/github/difflib/InlineDiffUtils.java new file mode 100644 index 0000000..9d3e021 --- /dev/null +++ b/java-diff-utils/src/main/java/com/github/difflib/InlineDiffUtils.java @@ -0,0 +1,49 @@ +package com.github.difflib; + +import com.github.difflib.patch.AbstractDelta; +import com.github.difflib.patch.Patch; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Utility class to implement inline character-level differences. + */ +public final class InlineDiffUtils { + + /** + * Computes the difference between the given texts inline. This one uses the + * "trick" to make out of texts lists of characters, like DiffRowGenerator + * does and merges those changes at the end together again. + * + * @param original a {@link String} representing the original text. Must not be {@code null}. + * @param revised a {@link String} representing the revised text. Must not be {@code null}. + * @return The patch describing the difference between the original and + * revised sequences. Never {@code null}. + */ + public static Patch diffInline(String original, String revised) { + List origList = new ArrayList<>(); + List revList = new ArrayList<>(); + for (Character character : original.toCharArray()) { + origList.add(character.toString()); + } + for (Character character : revised.toCharArray()) { + revList.add(character.toString()); + } + Patch patch = DiffUtils.diff(origList, revList); + for (AbstractDelta delta : patch.getDeltas()) { + delta.getSource().setLines(compressLines(delta.getSource().getLines(), "")); + delta.getTarget().setLines(compressLines(delta.getTarget().getLines(), "")); + } + return patch; + } + + private static List compressLines(List lines, String delimiter) { + if (lines.isEmpty()) { + return Collections.emptyList(); + } + return Collections.singletonList(String.join(delimiter, lines)); + } + + private InlineDiffUtils() {} +} diff --git a/java-diff-utils/src/main/java/com/github/difflib/PatchUtils.java b/java-diff-utils/src/main/java/com/github/difflib/PatchUtils.java new file mode 100644 index 0000000..42e95f0 --- /dev/null +++ b/java-diff-utils/src/main/java/com/github/difflib/PatchUtils.java @@ -0,0 +1,37 @@ +package com.github.difflib; + +import com.github.difflib.patch.Patch; +import com.github.difflib.patch.PatchFailedException; +import java.util.List; + +/** + * Utility class to implement the patching engine. + */ +public final class PatchUtils { + + /** + * Applies the given patch to the original list and returns the revised list. + * + * @param original a {@link List} representing the original list. + * @param patch a {@link List} representing the patch to apply. + * @return the revised list. + * @throws PatchFailedException if the patch cannot be applied. + */ + public static List patch(List original, Patch patch) throws PatchFailedException { + return patch.applyTo(original); + } + + /** + * Applies the given patch to the revised list and returns the original list. + * + * @param revised a {@link List} representing the revised list. + * @param patch a {@link Patch} representing the patch to apply. + * @return the original list. + * @throws PatchFailedException if the patch cannot be applied. + */ + public static List unpatch(List revised, Patch patch) { + return patch.restore(revised); + } + + private PatchUtils() {} +} diff --git a/java-diff-utils/src/test/java/com/github/difflib/DiffUtilsTest.java b/java-diff-utils/src/test/java/com/github/difflib/DiffUtilsTest.java index 8d13362..85859fd 100644 --- a/java-diff-utils/src/test/java/com/github/difflib/DiffUtilsTest.java +++ b/java-diff-utils/src/test/java/com/github/difflib/DiffUtilsTest.java @@ -85,7 +85,7 @@ public void testDiff_EmptyListWithNonEmpty() { @Test public void testDiffInline() { - final Patch patch = DiffUtils.diffInline("", "test"); + final Patch patch = InlineDiffUtils.diffInline("", "test"); assertEquals(1, patch.getDeltas().size()); assertTrue(patch.getDeltas().get(0) instanceof InsertDelta); assertEquals(0, patch.getDeltas().get(0).getSource().getPosition()); @@ -95,7 +95,7 @@ public void testDiffInline() { @Test public void testDiffInline2() { - final Patch patch = DiffUtils.diffInline("es", "fest"); + final Patch patch = InlineDiffUtils.diffInline("es", "fest"); assertEquals(2, patch.getDeltas().size()); assertTrue(patch.getDeltas().get(0) instanceof InsertDelta); assertEquals(0, patch.getDeltas().get(0).getSource().getPosition()); diff --git a/java-diff-utils/src/test/java/com/github/difflib/GenerateUnifiedDiffTest.java b/java-diff-utils/src/test/java/com/github/difflib/GenerateUnifiedDiffTest.java index 3e2357d..c8b9f6b 100644 --- a/java-diff-utils/src/test/java/com/github/difflib/GenerateUnifiedDiffTest.java +++ b/java-diff-utils/src/test/java/com/github/difflib/GenerateUnifiedDiffTest.java @@ -70,7 +70,7 @@ public void testDiff_Issue10() throws IOException { final List patchLines = fileToLines(TestConstants.MOCK_FOLDER + "issue10_patch.txt"); final Patch p = UnifiedDiffUtils.parseUnifiedDiff(patchLines); try { - DiffUtils.patch(baseLines, p); + PatchUtils.patch(baseLines, p); } catch (PatchFailedException e) { fail(e.getMessage()); } @@ -208,7 +208,7 @@ public void testFailingPatchByException() throws IOException { // make original not fitting baseLines.set(40, baseLines.get(40) + " corrupted "); - assertThrows(PatchFailedException.class, () -> DiffUtils.patch(baseLines, p)); + assertThrows(PatchFailedException.class, () -> PatchUtils.patch(baseLines, p)); } @Test diff --git a/java-diff-utils/src/test/java/com/github/difflib/algorithm/myers/WithMyersDiffWithLinearSpacePatchTest.java b/java-diff-utils/src/test/java/com/github/difflib/algorithm/myers/WithMyersDiffWithLinearSpacePatchTest.java index 92668ce..11c2463 100644 --- a/java-diff-utils/src/test/java/com/github/difflib/algorithm/myers/WithMyersDiffWithLinearSpacePatchTest.java +++ b/java-diff-utils/src/test/java/com/github/difflib/algorithm/myers/WithMyersDiffWithLinearSpacePatchTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.fail; import com.github.difflib.DiffUtils; +import com.github.difflib.PatchUtils; import com.github.difflib.patch.*; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -30,7 +31,7 @@ public void testPatch_Insert() { final Patch patch = DiffUtils.diff(insertTest_from, insertTest_to, new MyersDiffWithLinearSpace()); try { - assertEquals(insertTest_to, DiffUtils.patch(insertTest_from, patch)); + assertEquals(insertTest_to, PatchUtils.patch(insertTest_from, patch)); } catch (PatchFailedException e) { fail(e.getMessage()); } @@ -44,7 +45,7 @@ public void testPatch_Delete() { final Patch patch = DiffUtils.diff(deleteTest_from, deleteTest_to, new MyersDiffWithLinearSpace()); try { - assertEquals(deleteTest_to, DiffUtils.patch(deleteTest_from, patch)); + assertEquals(deleteTest_to, PatchUtils.patch(deleteTest_from, patch)); } catch (PatchFailedException e) { fail(e.getMessage()); } @@ -58,7 +59,7 @@ public void testPatch_Change() { final Patch patch = DiffUtils.diff(changeTest_from, changeTest_to, new MyersDiffWithLinearSpace()); try { - assertEquals(changeTest_to, DiffUtils.patch(changeTest_from, patch)); + assertEquals(changeTest_to, PatchUtils.patch(changeTest_from, patch)); } catch (PatchFailedException e) { fail(e.getMessage()); } @@ -167,7 +168,7 @@ public void testPatch_Serializable() throws IOException, ClassNotFoundException in.close(); try { - assertEquals(changeTest_to, DiffUtils.patch(changeTest_from, result)); + assertEquals(changeTest_to, PatchUtils.patch(changeTest_from, result)); } catch (PatchFailedException e) { fail(e.getMessage()); } @@ -186,7 +187,7 @@ public void testPatch_Change_withExceptionProcessor() { patch.withConflictOutput(Patch.CONFLICT_PRODUCES_MERGE_CONFLICT); try { - List data = DiffUtils.patch(changeTest_from, patch); + List data = PatchUtils.patch(changeTest_from, patch); assertEquals(11, data.size()); assertEquals( diff --git a/java-diff-utils/src/test/java/com/github/difflib/examples/ApplyPatch.java b/java-diff-utils/src/test/java/com/github/difflib/examples/ApplyPatch.java index 4825926..e67cdaa 100644 --- a/java-diff-utils/src/test/java/com/github/difflib/examples/ApplyPatch.java +++ b/java-diff-utils/src/test/java/com/github/difflib/examples/ApplyPatch.java @@ -1,6 +1,6 @@ package com.github.difflib.examples; -import com.github.difflib.DiffUtils; +import com.github.difflib.PatchUtils; import com.github.difflib.TestConstants; import com.github.difflib.UnifiedDiffUtils; import com.github.difflib.patch.Patch; @@ -23,7 +23,7 @@ public static void main(String[] args) throws PatchFailedException, IOException Patch patch = UnifiedDiffUtils.parseUnifiedDiff(patched); // Then apply the computed patch to the given text - List result = DiffUtils.patch(original, patch); + List result = PatchUtils.patch(original, patch); System.out.println(result); // / Or we can call patch.applyTo(original). There is no difference. } diff --git a/java-diff-utils/src/test/java/com/github/difflib/patch/PatchWithAllDiffAlgorithmsTest.java b/java-diff-utils/src/test/java/com/github/difflib/patch/PatchWithAllDiffAlgorithmsTest.java index a14ba52..cd118ad 100644 --- a/java-diff-utils/src/test/java/com/github/difflib/patch/PatchWithAllDiffAlgorithmsTest.java +++ b/java-diff-utils/src/test/java/com/github/difflib/patch/PatchWithAllDiffAlgorithmsTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.fail; import com.github.difflib.DiffUtils; +import com.github.difflib.PatchUtils; import com.github.difflib.algorithm.DiffAlgorithmFactory; import com.github.difflib.algorithm.myers.MyersDiff; import com.github.difflib.algorithm.myers.MyersDiffWithLinearSpace; @@ -41,7 +42,7 @@ public void testPatch_Insert(DiffAlgorithmFactory factory) { final Patch patch = DiffUtils.diff(insertTest_from, insertTest_to); try { - assertEquals(insertTest_to, DiffUtils.patch(insertTest_from, patch)); + assertEquals(insertTest_to, PatchUtils.patch(insertTest_from, patch)); } catch (PatchFailedException e) { fail(e.getMessage()); } @@ -57,7 +58,7 @@ public void testPatch_Delete(DiffAlgorithmFactory factory) { final Patch patch = DiffUtils.diff(deleteTest_from, deleteTest_to); try { - assertEquals(deleteTest_to, DiffUtils.patch(deleteTest_from, patch)); + assertEquals(deleteTest_to, PatchUtils.patch(deleteTest_from, patch)); } catch (PatchFailedException e) { fail(e.getMessage()); } @@ -73,7 +74,7 @@ public void testPatch_Change(DiffAlgorithmFactory factory) { final Patch patch = DiffUtils.diff(changeTest_from, changeTest_to); try { - assertEquals(changeTest_to, DiffUtils.patch(changeTest_from, patch)); + assertEquals(changeTest_to, PatchUtils.patch(changeTest_from, patch)); } catch (PatchFailedException e) { fail(e.getMessage()); } @@ -98,7 +99,7 @@ public void testPatch_Serializable(DiffAlgorithmFactory factory) throws IOExcept in.close(); try { - assertEquals(changeTest_to, DiffUtils.patch(changeTest_from, result)); + assertEquals(changeTest_to, PatchUtils.patch(changeTest_from, result)); } catch (PatchFailedException e) { fail(e.getMessage()); } diff --git a/java-diff-utils/src/test/java/com/github/difflib/patch/PatchWithMyerDiffTest.java b/java-diff-utils/src/test/java/com/github/difflib/patch/PatchWithMyerDiffTest.java index 0dab69f..a357d47 100644 --- a/java-diff-utils/src/test/java/com/github/difflib/patch/PatchWithMyerDiffTest.java +++ b/java-diff-utils/src/test/java/com/github/difflib/patch/PatchWithMyerDiffTest.java @@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.fail; import com.github.difflib.DiffUtils; +import com.github.difflib.PatchUtils; import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.Test; @@ -43,7 +44,7 @@ public void testPatch_Change_withExceptionProcessor() { patch.withConflictOutput(Patch.CONFLICT_PRODUCES_MERGE_CONFLICT); try { - List data = DiffUtils.patch(changeTest_from, patch); + List data = PatchUtils.patch(changeTest_from, patch); assertEquals(9, data.size()); assertEquals( diff --git a/java-diff-utils/src/test/java/com/github/difflib/patch/PatchWithMyerDiffWithLinearSpaceTest.java b/java-diff-utils/src/test/java/com/github/difflib/patch/PatchWithMyerDiffWithLinearSpaceTest.java index f04a1c9..6c90f87 100644 --- a/java-diff-utils/src/test/java/com/github/difflib/patch/PatchWithMyerDiffWithLinearSpaceTest.java +++ b/java-diff-utils/src/test/java/com/github/difflib/patch/PatchWithMyerDiffWithLinearSpaceTest.java @@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.fail; import com.github.difflib.DiffUtils; +import com.github.difflib.PatchUtils; import com.github.difflib.algorithm.myers.MyersDiff; import com.github.difflib.algorithm.myers.MyersDiffWithLinearSpace; import java.util.Arrays; @@ -55,7 +56,7 @@ public void testPatch_Change_withExceptionProcessor() { patch.withConflictOutput(Patch.CONFLICT_PRODUCES_MERGE_CONFLICT); try { - List data = DiffUtils.patch(changeTest_from, patch); + List data = PatchUtils.patch(changeTest_from, patch); assertEquals(11, data.size()); assertEquals( diff --git a/java-diff-utils/src/test/java/com/github/difflib/unifieddiff/UnifiedDiffRoundTripTest.java b/java-diff-utils/src/test/java/com/github/difflib/unifieddiff/UnifiedDiffRoundTripTest.java index bb8bdcb..97216b3 100644 --- a/java-diff-utils/src/test/java/com/github/difflib/unifieddiff/UnifiedDiffRoundTripTest.java +++ b/java-diff-utils/src/test/java/com/github/difflib/unifieddiff/UnifiedDiffRoundTripTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.fail; import com.github.difflib.DiffUtils; +import com.github.difflib.PatchUtils; import com.github.difflib.TestConstants; import com.github.difflib.patch.Patch; import com.github.difflib.patch.PatchFailedException; @@ -71,7 +72,7 @@ public void testDiff_Issue10() throws IOException { final Patch p = unifiedDiff.getFiles().get(0).getPatch(); try { - DiffUtils.patch(baseLines, p); + PatchUtils.patch(baseLines, p); } catch (PatchFailedException e) { fail(e.getMessage()); } From f42ef36fb24049d0f3205f45b0c6d3a589b961c6 Mon Sep 17 00:00:00 2001 From: Sajib Sarkar Date: Sun, 28 Jun 2026 21:20:42 +0600 Subject: [PATCH 4/7] Resolved god class DifffRowGenerator.java into DeltaCompressor, InlineDiffAnnotator, InlineDiffAnnonatorConfig, InlineTagRender.java --- .../difflib/text/DeltaDecompressor.java | 65 +++++ .../github/difflib/text/DiffRowGenerator.java | 257 ++---------------- .../difflib/text/InlineDiffAnnotator.java | 159 +++++++++++ .../text/InlineDiffAnnotatorConfig.java | 56 ++++ .../difflib/text/InlineTagRenderer.java | 90 ++++++ 5 files changed, 398 insertions(+), 229 deletions(-) create mode 100644 java-diff-utils/src/main/java/com/github/difflib/text/DeltaDecompressor.java create mode 100644 java-diff-utils/src/main/java/com/github/difflib/text/InlineDiffAnnotator.java create mode 100644 java-diff-utils/src/main/java/com/github/difflib/text/InlineDiffAnnotatorConfig.java create mode 100644 java-diff-utils/src/main/java/com/github/difflib/text/InlineTagRenderer.java diff --git a/java-diff-utils/src/main/java/com/github/difflib/text/DeltaDecompressor.java b/java-diff-utils/src/main/java/com/github/difflib/text/DeltaDecompressor.java new file mode 100644 index 0000000..2770328 --- /dev/null +++ b/java-diff-utils/src/main/java/com/github/difflib/text/DeltaDecompressor.java @@ -0,0 +1,65 @@ +package com.github.difflib.text; + +import com.github.difflib.patch.AbstractDelta; +import com.github.difflib.patch.ChangeDelta; +import com.github.difflib.patch.Chunk; +import com.github.difflib.patch.DeleteDelta; +import com.github.difflib.patch.DeltaType; +import com.github.difflib.patch.InsertDelta; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Utility that normalises asymmetric {@link ChangeDelta}s into equal-size pairs + * so that DiffRow building stays simple. + * + *

When a CHANGE delta has a different number of source and target lines it is + * split into a same-size {@link ChangeDelta} followed by either an {@link InsertDelta} + * or a {@link DeleteDelta} for the surplus lines. + */ +public final class DeltaDecompressor { + + private DeltaDecompressor() {} + + /** + * Decompresses a {@link ChangeDelta} whose source and target sizes differ into + * a same-size {@link ChangeDelta} plus a trailing {@link InsertDelta} or + * {@link DeleteDelta}. All other delta types are returned unchanged in a + * singleton list. + * + * @param delta the delta to (possibly) decompress. Must not be {@code null}. + * @return a list containing the original delta, or the two replacement deltas. + */ + public static List> decompress(AbstractDelta delta) { + if (delta.getType() == DeltaType.CHANGE + && delta.getSource().size() != delta.getTarget().size()) { + List> deltas = new ArrayList<>(); + + int minSize = Math.min(delta.getSource().size(), delta.getTarget().size()); + Chunk orig = delta.getSource(); + Chunk rev = delta.getTarget(); + + deltas.add(new ChangeDelta( + new Chunk<>(orig.getPosition(), orig.getLines().subList(0, minSize)), + new Chunk<>(rev.getPosition(), rev.getLines().subList(0, minSize)))); + + if (orig.getLines().size() < rev.getLines().size()) { + deltas.add(new InsertDelta( + new Chunk<>(orig.getPosition() + minSize, Collections.emptyList()), + new Chunk<>( + rev.getPosition() + minSize, + rev.getLines().subList(minSize, rev.getLines().size())))); + } else { + deltas.add(new DeleteDelta( + new Chunk<>( + orig.getPosition() + minSize, + orig.getLines().subList(minSize, orig.getLines().size())), + new Chunk<>(rev.getPosition() + minSize, Collections.emptyList()))); + } + return deltas; + } + + return Collections.singletonList(delta); + } +} diff --git a/java-diff-utils/src/main/java/com/github/difflib/text/DiffRowGenerator.java b/java-diff-utils/src/main/java/com/github/difflib/text/DiffRowGenerator.java index 82a5b94..390ed77 100644 --- a/java-diff-utils/src/main/java/com/github/difflib/text/DiffRowGenerator.java +++ b/java-diff-utils/src/main/java/com/github/difflib/text/DiffRowGenerator.java @@ -15,15 +15,9 @@ */ package com.github.difflib.text; -import static java.util.stream.Collectors.toList; - import com.github.difflib.DiffUtils; import com.github.difflib.patch.AbstractDelta; -import com.github.difflib.patch.ChangeDelta; import com.github.difflib.patch.Chunk; -import com.github.difflib.patch.DeleteDelta; -import com.github.difflib.patch.DeltaType; -import com.github.difflib.patch.InsertDelta; import com.github.difflib.patch.Patch; import com.github.difflib.text.DiffRow.Tag; import com.github.difflib.text.deltamerge.DeltaMergeUtils; @@ -117,67 +111,6 @@ protected static final List splitStringPreserveDelimiter(String str, Pat return list; } - /** - * Wrap the elements in the sequence with the given tag - * - * @param startPosition the position from which tag should start. The - * counting start from a zero. - * @param endPosition the position before which tag should should be closed. - * @param tagGenerator the tag generator - */ - static void wrapInTag( - List sequence, - int startPosition, - int endPosition, - Tag tag, - BiFunction tagGenerator, - Function processDiffs, - boolean replaceLinefeedWithSpace) { - int endPos = endPosition; - - while (endPos >= startPosition) { - - // search position for end tag - while (endPos > startPosition) { - if (!"\n".equals(sequence.get(endPos - 1))) { - break; - } else if (replaceLinefeedWithSpace) { - sequence.set(endPos - 1, " "); - break; - } - endPos--; - } - - if (endPos == startPosition) { - break; - } - - sequence.add(endPos, tagGenerator.apply(tag, false)); - if (processDiffs != null) { - sequence.set(endPos - 1, processDiffs.apply(sequence.get(endPos - 1))); - } - endPos--; - - // search position for end tag - while (endPos > startPosition) { - if ("\n".equals(sequence.get(endPos - 1))) { - if (replaceLinefeedWithSpace) { - sequence.set(endPos - 1, " "); - } else { - break; - } - } - if (processDiffs != null) { - sequence.set(endPos - 1, processDiffs.apply(sequence.get(endPos - 1))); - } - endPos--; - } - - sequence.add(endPos, tagGenerator.apply(tag, true)); - endPos--; - } - } - private final int columnWidth; private final BiPredicate equalizer; private final boolean ignoreWhiteSpaces; @@ -196,6 +129,9 @@ static void wrapInTag( private final boolean replaceOriginalLinefeedInChangesWithSpaces; private final boolean decompressDeltas; + /** Pre-built config object passed to {@link InlineDiffAnnotator} for each changed delta. */ + private final InlineDiffAnnotatorConfig annotatorConfig; + private DiffRowGenerator(Builder builder) { showInlineDiffs = builder.showInlineDiffs; ignoreWhiteSpaces = builder.ignoreWhiteSpaces; @@ -223,6 +159,29 @@ private DiffRowGenerator(Builder builder) { Objects.requireNonNull(inlineDiffSplitter); Objects.requireNonNull(lineNormalizer); Objects.requireNonNull(inlineDeltaMerger); + + annotatorConfig = new InlineDiffAnnotatorConfig( + reportLinesUnchanged, + lineNormalizer, + inlineDiffSplitter, + equalizer, + inlineDeltaMerger, + oldTag, + newTag, + processDiffs, + mergeOriginalRevised, + replaceOriginalLinefeedInChangesWithSpaces, + columnWidth); + } + + /** + * Applies the configured line normalizer to each line, unless + * {@code reportLinesUnchanged} is set. Package-visible for testing. + */ + List normalizeLines(List list) { + return reportLinesUnchanged + ? list + : list.stream().map(lineNormalizer::apply).collect(java.util.stream.Collectors.toList()); } /** @@ -253,7 +212,7 @@ public List generateDiffRows(final List original, Patch if (decompressDeltas) { for (AbstractDelta originalDelta : deltaList) { - for (AbstractDelta delta : decompressDeltas(originalDelta)) { + for (AbstractDelta delta : DeltaDecompressor.decompress(originalDelta)) { endPos = transformDeltaIntoDiffRow(original, endPos, diffRows, delta); } } @@ -297,7 +256,7 @@ private int transformDeltaIntoDiffRow( break; default: if (showInlineDiffs) { - diffRows.addAll(generateInlineDiffs(delta)); + diffRows.addAll(InlineDiffAnnotator.annotate(delta, annotatorConfig)); } else { for (int j = 0; j < Math.max(orig.size(), rev.size()); j++) { diffRows.add(buildDiffRow( @@ -311,46 +270,6 @@ private int transformDeltaIntoDiffRow( return orig.last() + 1; } - /** - * Decompresses ChangeDeltas with different source and target size to a - * ChangeDelta with same size and a following InsertDelta or DeleteDelta. - * With this problems of building DiffRows getting smaller. - * - * @param deltaList - */ - private List> decompressDeltas(AbstractDelta delta) { - if (delta.getType() == DeltaType.CHANGE - && delta.getSource().size() != delta.getTarget().size()) { - List> deltas = new ArrayList<>(); - // System.out.println("decompress this " + delta); - - int minSize = Math.min(delta.getSource().size(), delta.getTarget().size()); - Chunk orig = delta.getSource(); - Chunk rev = delta.getTarget(); - - deltas.add(new ChangeDelta( - new Chunk<>(orig.getPosition(), orig.getLines().subList(0, minSize)), - new Chunk<>(rev.getPosition(), rev.getLines().subList(0, minSize)))); - - if (orig.getLines().size() < rev.getLines().size()) { - deltas.add(new InsertDelta( - new Chunk<>(orig.getPosition() + minSize, Collections.emptyList()), - new Chunk<>( - rev.getPosition() + minSize, - rev.getLines().subList(minSize, rev.getLines().size())))); - } else { - deltas.add(new DeleteDelta( - new Chunk<>( - orig.getPosition() + minSize, - orig.getLines().subList(minSize, orig.getLines().size())), - new Chunk<>(rev.getPosition() + minSize, Collections.emptyList()))); - } - return deltas; - } - - return Collections.singletonList(delta); - } - private DiffRow buildDiffRow(Tag type, String orgline, String newline) { if (reportLinesUnchanged) { return new DiffRow(type, orgline, newline); @@ -373,126 +292,6 @@ private DiffRow buildDiffRow(Tag type, String orgline, String newline) { } } - private DiffRow buildDiffRowWithoutNormalizing(Tag type, String orgline, String newline) { - return new DiffRow( - type, StringUtils.wrapText(orgline, columnWidth), StringUtils.wrapText(newline, columnWidth)); - } - - List normalizeLines(List list) { - return reportLinesUnchanged - ? list - : list.stream().map(lineNormalizer::apply).collect(toList()); - } - - /** - * Add the inline diffs for given delta - * - * @param delta the given delta - */ - private List generateInlineDiffs(AbstractDelta delta) { - List orig = normalizeLines(delta.getSource().getLines()); - List rev = normalizeLines(delta.getTarget().getLines()); - List origList; - List revList; - String joinedOrig = String.join("\n", orig); - String joinedRev = String.join("\n", rev); - - origList = inlineDiffSplitter.apply(joinedOrig); - revList = inlineDiffSplitter.apply(joinedRev); - - List> originalInlineDeltas = - DiffUtils.diff(origList, revList, equalizer).getDeltas(); - List> inlineDeltas = - inlineDeltaMerger.apply(new InlineDeltaMergeInfo(originalInlineDeltas, origList, revList)); - - Collections.reverse(inlineDeltas); - for (AbstractDelta inlineDelta : inlineDeltas) { - Chunk inlineOrig = inlineDelta.getSource(); - Chunk inlineRev = inlineDelta.getTarget(); - if (inlineDelta.getType() == DeltaType.DELETE) { - wrapInTag( - origList, - inlineOrig.getPosition(), - inlineOrig.getPosition() + inlineOrig.size(), - Tag.DELETE, - oldTag, - processDiffs, - replaceOriginalLinefeedInChangesWithSpaces && mergeOriginalRevised); - } else if (inlineDelta.getType() == DeltaType.INSERT) { - if (mergeOriginalRevised) { - origList.addAll( - inlineOrig.getPosition(), - revList.subList(inlineRev.getPosition(), inlineRev.getPosition() + inlineRev.size())); - wrapInTag( - origList, - inlineOrig.getPosition(), - inlineOrig.getPosition() + inlineRev.size(), - Tag.INSERT, - newTag, - processDiffs, - false); - } else { - wrapInTag( - revList, - inlineRev.getPosition(), - inlineRev.getPosition() + inlineRev.size(), - Tag.INSERT, - newTag, - processDiffs, - false); - } - } else if (inlineDelta.getType() == DeltaType.CHANGE) { - if (mergeOriginalRevised) { - origList.addAll( - inlineOrig.getPosition() + inlineOrig.size(), - revList.subList(inlineRev.getPosition(), inlineRev.getPosition() + inlineRev.size())); - wrapInTag( - origList, - inlineOrig.getPosition() + inlineOrig.size(), - inlineOrig.getPosition() + inlineOrig.size() + inlineRev.size(), - Tag.CHANGE, - newTag, - processDiffs, - false); - } else { - wrapInTag( - revList, - inlineRev.getPosition(), - inlineRev.getPosition() + inlineRev.size(), - Tag.CHANGE, - newTag, - processDiffs, - false); - } - wrapInTag( - origList, - inlineOrig.getPosition(), - inlineOrig.getPosition() + inlineOrig.size(), - Tag.CHANGE, - oldTag, - processDiffs, - replaceOriginalLinefeedInChangesWithSpaces && mergeOriginalRevised); - } - } - StringBuilder origResult = new StringBuilder(); - StringBuilder revResult = new StringBuilder(); - for (String character : origList) { - origResult.append(character); - } - for (String character : revList) { - revResult.append(character); - } - - List original = Arrays.asList(origResult.toString().split("\n")); - List revised = Arrays.asList(revResult.toString().split("\n")); - List diffRows = new ArrayList<>(); - for (int j = 0; j < Math.max(original.size(), revised.size()); j++) { - diffRows.add(buildDiffRowWithoutNormalizing( - Tag.CHANGE, original.size() > j ? original.get(j) : "", revised.size() > j ? revised.get(j) : "")); - } - return diffRows; - } - private String preprocessLine(String line) { if (columnWidth == 0) { return lineNormalizer.apply(line); diff --git a/java-diff-utils/src/main/java/com/github/difflib/text/InlineDiffAnnotator.java b/java-diff-utils/src/main/java/com/github/difflib/text/InlineDiffAnnotator.java new file mode 100644 index 0000000..9657495 --- /dev/null +++ b/java-diff-utils/src/main/java/com/github/difflib/text/InlineDiffAnnotator.java @@ -0,0 +1,159 @@ +package com.github.difflib.text; + +import static java.util.stream.Collectors.toList; + +import com.github.difflib.DiffUtils; +import com.github.difflib.patch.AbstractDelta; +import com.github.difflib.patch.Chunk; +import com.github.difflib.patch.DeltaType; +import com.github.difflib.text.DiffRow.Tag; +import com.github.difflib.text.deltamerge.InlineDeltaMergeInfo; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Applies character- or word-level inline diff markup to a single {@link AbstractDelta}, + * producing the annotated {@link DiffRow} list ready for side-by-side display. + * + *

This is a single-responsibility helper: it runs a sub-diff on the token lists, + * merges adjacent inline deltas, and delegates tag insertion to {@link InlineTagRenderer}. + * All rendering decisions (which HTML/markup tags to use, how to handle line-feeds, etc.) + * are supplied by the caller through {@link InlineDiffAnnotatorConfig}. + */ +public final class InlineDiffAnnotator { + + private InlineDiffAnnotator() {} + + /** + * Generates the inline-annotated {@link DiffRow}s for a single changed delta. + * + * @param delta the CHANGE delta to annotate. Must not be {@code null}. + * @param config all rendering and splitting configuration. Must not be {@code null}. + * @return the list of {@link DiffRow}s with inline markup applied. + */ + public static List annotate(AbstractDelta delta, InlineDiffAnnotatorConfig config) { + + List orig = normalizeLines(delta.getSource().getLines(), config); + List rev = normalizeLines(delta.getTarget().getLines(), config); + + String joinedOrig = String.join("\n", orig); + String joinedRev = String.join("\n", rev); + + List origList = config.inlineDiffSplitter.apply(joinedOrig); + List revList = config.inlineDiffSplitter.apply(joinedRev); + + List> originalInlineDeltas = + DiffUtils.diff(origList, revList, config.equalizer).getDeltas(); + List> inlineDeltas = + config.inlineDeltaMerger.apply(new InlineDeltaMergeInfo(originalInlineDeltas, origList, revList)); + + Collections.reverse(inlineDeltas); + for (AbstractDelta inlineDelta : inlineDeltas) { + Chunk inlineOrig = inlineDelta.getSource(); + Chunk inlineRev = inlineDelta.getTarget(); + if (inlineDelta.getType() == DeltaType.DELETE) { + InlineTagRenderer.wrapInTag( + origList, + inlineOrig.getPosition(), + inlineOrig.getPosition() + inlineOrig.size(), + Tag.DELETE, + config.oldTag, + config.processDiffs, + config.replaceOriginalLinefeedInChangesWithSpaces && config.mergeOriginalRevised); + } else if (inlineDelta.getType() == DeltaType.INSERT) { + if (config.mergeOriginalRevised) { + origList.addAll( + inlineOrig.getPosition(), + revList.subList(inlineRev.getPosition(), inlineRev.getPosition() + inlineRev.size())); + InlineTagRenderer.wrapInTag( + origList, + inlineOrig.getPosition(), + inlineOrig.getPosition() + inlineRev.size(), + Tag.INSERT, + config.newTag, + config.processDiffs, + false); + } else { + InlineTagRenderer.wrapInTag( + revList, + inlineRev.getPosition(), + inlineRev.getPosition() + inlineRev.size(), + Tag.INSERT, + config.newTag, + config.processDiffs, + false); + } + } else if (inlineDelta.getType() == DeltaType.CHANGE) { + if (config.mergeOriginalRevised) { + origList.addAll( + inlineOrig.getPosition() + inlineOrig.size(), + revList.subList(inlineRev.getPosition(), inlineRev.getPosition() + inlineRev.size())); + InlineTagRenderer.wrapInTag( + origList, + inlineOrig.getPosition() + inlineOrig.size(), + inlineOrig.getPosition() + inlineOrig.size() + inlineRev.size(), + Tag.CHANGE, + config.newTag, + config.processDiffs, + false); + } else { + InlineTagRenderer.wrapInTag( + revList, + inlineRev.getPosition(), + inlineRev.getPosition() + inlineRev.size(), + Tag.CHANGE, + config.newTag, + config.processDiffs, + false); + } + InlineTagRenderer.wrapInTag( + origList, + inlineOrig.getPosition(), + inlineOrig.getPosition() + inlineOrig.size(), + Tag.CHANGE, + config.oldTag, + config.processDiffs, + config.replaceOriginalLinefeedInChangesWithSpaces && config.mergeOriginalRevised); + } + } + + StringBuilder origResult = new StringBuilder(); + StringBuilder revResult = new StringBuilder(); + for (String character : origList) { + origResult.append(character); + } + for (String character : revList) { + revResult.append(character); + } + + List originalLines = Arrays.asList(origResult.toString().split("\n")); + List revisedLines = Arrays.asList(revResult.toString().split("\n")); + List diffRows = new ArrayList<>(); + for (int j = 0; j < Math.max(originalLines.size(), revisedLines.size()); j++) { + diffRows.add(buildDiffRowWithoutNormalizing( + Tag.CHANGE, + originalLines.size() > j ? originalLines.get(j) : "", + revisedLines.size() > j ? revisedLines.get(j) : "", + config.columnWidth)); + } + return diffRows; + } + + /** + * Applies the line normalizer from the config to every line in the list, unless + * {@code reportLinesUnchanged} is set in which case the list is returned as-is. + */ + private static List normalizeLines(List list, InlineDiffAnnotatorConfig config) { + return config.reportLinesUnchanged + ? list + : list.stream().map(config.lineNormalizer::apply).collect(toList()); + } + + /** Wraps the text at {@code columnWidth} and returns a plain {@link DiffRow}. */ + private static DiffRow buildDiffRowWithoutNormalizing(Tag type, String orgline, String newline, int columnWidth) { + return new DiffRow( + type, StringUtils.wrapText(orgline, columnWidth), StringUtils.wrapText(newline, columnWidth)); + } +} diff --git a/java-diff-utils/src/main/java/com/github/difflib/text/InlineDiffAnnotatorConfig.java b/java-diff-utils/src/main/java/com/github/difflib/text/InlineDiffAnnotatorConfig.java new file mode 100644 index 0000000..afc13db --- /dev/null +++ b/java-diff-utils/src/main/java/com/github/difflib/text/InlineDiffAnnotatorConfig.java @@ -0,0 +1,56 @@ +package com.github.difflib.text; + +import com.github.difflib.patch.AbstractDelta; +import com.github.difflib.text.DiffRow.Tag; +import com.github.difflib.text.deltamerge.InlineDeltaMergeInfo; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Function; + +/** + * Immutable value object carrying all configuration that {@link InlineDiffAnnotator} needs + * to annotate a changed delta with inline markup. + * + *

Instances are created once per {@link DiffRowGenerator} construction and reused + * for every delta processed. + */ +public final class InlineDiffAnnotatorConfig { + + final boolean reportLinesUnchanged; + final Function lineNormalizer; + final Function> inlineDiffSplitter; + final BiPredicate equalizer; + final Function>> inlineDeltaMerger; + final BiFunction oldTag; + final BiFunction newTag; + final Function processDiffs; + final boolean mergeOriginalRevised; + final boolean replaceOriginalLinefeedInChangesWithSpaces; + final int columnWidth; + + InlineDiffAnnotatorConfig( + boolean reportLinesUnchanged, + Function lineNormalizer, + Function> inlineDiffSplitter, + BiPredicate equalizer, + Function>> inlineDeltaMerger, + BiFunction oldTag, + BiFunction newTag, + Function processDiffs, + boolean mergeOriginalRevised, + boolean replaceOriginalLinefeedInChangesWithSpaces, + int columnWidth) { + this.reportLinesUnchanged = reportLinesUnchanged; + this.lineNormalizer = lineNormalizer; + this.inlineDiffSplitter = inlineDiffSplitter; + this.equalizer = equalizer; + this.inlineDeltaMerger = inlineDeltaMerger; + this.oldTag = oldTag; + this.newTag = newTag; + this.processDiffs = processDiffs; + this.mergeOriginalRevised = mergeOriginalRevised; + this.replaceOriginalLinefeedInChangesWithSpaces = replaceOriginalLinefeedInChangesWithSpaces; + this.columnWidth = columnWidth; + } +} diff --git a/java-diff-utils/src/main/java/com/github/difflib/text/InlineTagRenderer.java b/java-diff-utils/src/main/java/com/github/difflib/text/InlineTagRenderer.java new file mode 100644 index 0000000..aa7c6ba --- /dev/null +++ b/java-diff-utils/src/main/java/com/github/difflib/text/InlineTagRenderer.java @@ -0,0 +1,90 @@ +package com.github.difflib.text; + +import com.github.difflib.text.DiffRow.Tag; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Utility that injects markup open/close tags into a mutable token list at specified positions, + * respecting newline boundaries. + * + *

This class has a single responsibility: presentation markup injection. It has no knowledge + * of diff algorithms, patch structures, or {@link DiffRow} assembly. + */ +public final class InlineTagRenderer { + + private InlineTagRenderer() {} + + /** + * Wraps the tokens between {@code startPosition} (inclusive) and {@code endPosition} + * (exclusive) with markup strings produced by {@code tagGenerator}. + * + *

Line-feed tokens ({@code "\n"}) act as segment boundaries — a separate open/close tag + * pair is emitted for each contiguous non-newline run. When {@code replaceLinefeedWithSpace} + * is {@code true} the newline tokens are replaced with a space instead. + * + * @param sequence the mutable token list to annotate in-place. + * @param startPosition the index of the first token to wrap (zero-based, inclusive). + * @param endPosition the index past the last token to wrap (exclusive). + * @param tag the semantic tag type passed to the generator so callers can vary markup by type. + * @param tagGenerator produces the open ({@code isOpen=true}) or close ({@code isOpen=false}) + * markup string for a given tag. + * @param processDiffs optional post-processor applied to every diffed token before the close + * tag is inserted; may be {@code null}. + * @param replaceLinefeedWithSpace when {@code true}, newline tokens inside the range are + * replaced with a space rather than used as segment boundaries. + */ + public static void wrapInTag( + List sequence, + int startPosition, + int endPosition, + Tag tag, + BiFunction tagGenerator, + Function processDiffs, + boolean replaceLinefeedWithSpace) { + int endPos = endPosition; + + while (endPos >= startPosition) { + + // search position for end tag + while (endPos > startPosition) { + if (!"\n".equals(sequence.get(endPos - 1))) { + break; + } else if (replaceLinefeedWithSpace) { + sequence.set(endPos - 1, " "); + break; + } + endPos--; + } + + if (endPos == startPosition) { + break; + } + + sequence.add(endPos, tagGenerator.apply(tag, false)); + if (processDiffs != null) { + sequence.set(endPos - 1, processDiffs.apply(sequence.get(endPos - 1))); + } + endPos--; + + // search position for start tag + while (endPos > startPosition) { + if ("\n".equals(sequence.get(endPos - 1))) { + if (replaceLinefeedWithSpace) { + sequence.set(endPos - 1, " "); + } else { + break; + } + } + if (processDiffs != null) { + sequence.set(endPos - 1, processDiffs.apply(sequence.get(endPos - 1))); + } + endPos--; + } + + sequence.add(endPos, tagGenerator.apply(tag, true)); + endPos--; + } + } +} From 7b940fff6fa1199bcde3b0e2fd8cdfe7db155301 Mon Sep 17 00:00:00 2001 From: Sajib Sarkar Date: Sat, 4 Jul 2026 14:01:02 +0600 Subject: [PATCH 5/7] refactor(UnifiedDiffReader): reduce cognitive complexity from 45 to ~5 via phase-driven helpers --- .gitignore | 11 + .../unifieddiff/UnifiedDiffReader.java | 203 +++++++++--------- 2 files changed, 115 insertions(+), 99 deletions(-) diff --git a/.gitignore b/.gitignore index 634cb5b..a2253ac 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,14 @@ nbproject/ target/ *.iml + +# Assignment docs and generated reports (not part of library source) +docs/ +*.docx +*.pdf +*.py + +# OS artifacts +.DS_Store +Thumbs.db +desktop.ini diff --git a/java-diff-utils/src/main/java/com/github/difflib/unifieddiff/UnifiedDiffReader.java b/java-diff-utils/src/main/java/com/github/difflib/unifieddiff/UnifiedDiffReader.java index 9be9b0f..2d95359 100644 --- a/java-diff-utils/src/main/java/com/github/difflib/unifieddiff/UnifiedDiffReader.java +++ b/java-diff-utils/src/main/java/com/github/difflib/unifieddiff/UnifiedDiffReader.java @@ -82,121 +82,128 @@ public final class UnifiedDiffReader { private final UnifiedDiffLine LINE_DEL = new UnifiedDiffLine("^-", this::processDelLine); private final UnifiedDiffLine LINE_ADD = new UnifiedDiffLine("^\\+", this::processAddLine); + private final UnifiedDiffLine[] FILE_HEADER_RULES = { + DIFF_COMMAND, + SIMILARITY_INDEX, + INDEX, + FROM_FILE, + TO_FILE, + RENAME_FROM, + RENAME_TO, + COPY_FROM, + COPY_TO, + NEW_FILE_MODE, + DELETED_FILE_MODE, + OLD_MODE, + NEW_MODE, + BINARY_ADDED, + BINARY_DELETED, + BINARY_EDITED + }; + + private final UnifiedDiffLine[] HEADER_STOP_RULES = { + DIFF_COMMAND, + SIMILARITY_INDEX, + INDEX, + FROM_FILE, + TO_FILE, + RENAME_FROM, + RENAME_TO, + COPY_FROM, + COPY_TO, + NEW_FILE_MODE, + DELETED_FILE_MODE, + OLD_MODE, + NEW_MODE, + BINARY_ADDED, + BINARY_DELETED, + BINARY_EDITED, + CHUNK + }; + private UnifiedDiffFile actualFile; UnifiedDiffReader(Reader reader) { this.READER = new InternalUnifiedDiffReader(reader); } - // schema = [[/^\s+/, normal], [/^diff\s/, start], [/^new file mode \d+$/, new_file], - // [/^deleted file mode \d+$/, deleted_file], [/^index\s[\da-zA-Z]+\.\.[\da-zA-Z]+(\s(\d+))?$/, index], - // [/^---\s/, from_file], [/^\+\+\+\s/, to_file], [/^@@\s+\-(\d+),?(\d+)?\s+\+(\d+),?(\d+)?\s@@/, chunk], - // [/^-/, del], [/^\+/, add], [/^\\ No newline at end of file$/, eof]]; private UnifiedDiff parse() throws IOException, UnifiedDiffParserException { - // String headerTxt = ""; - // LOG.log(Level.FINE, "header parsing"); - // String line = null; - // while (READER.ready()) { - // line = READER.readLine(); - // LOG.log(Level.FINE, "parsing line {0}", line); - // if (DIFF_COMMAND.validLine(line) || INDEX.validLine(line) - // || FROM_FILE.validLine(line) || TO_FILE.validLine(line) - // || NEW_FILE_MODE.validLine(line)) { - // break; - // } else { - // headerTxt += line + "\n"; - // } - // } - // if (!"".equals(headerTxt)) { - // data.setHeader(headerTxt); - // } - String line = READER.readLine(); while (line != null) { - String headerTxt = ""; - LOG.log(Level.FINE, "header parsing"); - while (line != null) { - LOG.log(Level.FINE, "parsing line {0}", line); - if (validLine( - line, - DIFF_COMMAND, - SIMILARITY_INDEX, - INDEX, - FROM_FILE, - TO_FILE, - RENAME_FROM, - RENAME_TO, - COPY_FROM, - COPY_TO, - NEW_FILE_MODE, - DELETED_FILE_MODE, - OLD_MODE, - NEW_MODE, - BINARY_ADDED, - BINARY_DELETED, - BINARY_EDITED, - CHUNK)) { - break; - } else { - headerTxt += line + "\n"; - } - line = READER.readLine(); - } - if (!"".equals(headerTxt)) { - data.setHeader(headerTxt); + line = parseHeaderSection(line); + line = parseFileHeader(line); + line = parseChunkSection(line); + if (line == null || (line.startsWith("--") && !line.startsWith("---"))) { + break; } - if (line != null && !CHUNK.validLine(line)) { - initFileIfNecessary(); - while (line != null && !CHUNK.validLine(line)) { - if (!processLine( - line, - DIFF_COMMAND, - SIMILARITY_INDEX, - INDEX, - FROM_FILE, - TO_FILE, - RENAME_FROM, - RENAME_TO, - COPY_FROM, - COPY_TO, - NEW_FILE_MODE, - DELETED_FILE_MODE, - OLD_MODE, - NEW_MODE, - BINARY_ADDED, - BINARY_DELETED, - BINARY_EDITED)) { - throw new UnifiedDiffParserException("expected file start line not found"); - } - line = READER.readLine(); - } + } + parseTailSection(); + return data; + } + + private String parseHeaderSection(String currentLine) throws IOException { + String line = currentLine; + String headerTxt = ""; + LOG.log(Level.FINE, "header parsing"); + while (line != null) { + LOG.log(Level.FINE, "parsing line {0}", line); + if (validLine(line, HEADER_STOP_RULES)) { + break; + } else { + headerTxt += line + "\n"; } - if (line != null) { - processLine(line, CHUNK); - while ((line = READER.readLine()) != null) { - line = checkForNoNewLineAtTheEndOfTheFile(line); - - if (!processLine(line, LINE_NORMAL, LINE_ADD, LINE_DEL)) { - throw new UnifiedDiffParserException("expected data line not found"); - } - if ((originalTxt.size() == old_size && revisedTxt.size() == new_size) - || (old_size == 0 - && new_size == 0 - && originalTxt.size() == this.old_ln - && revisedTxt.size() == this.new_ln)) { - finalizeChunk(); - break; - } + line = READER.readLine(); + } + if (!"".equals(headerTxt)) { + data.setHeader(headerTxt); + } + return line; + } + + private String parseFileHeader(String currentLine) throws IOException, UnifiedDiffParserException { + String line = currentLine; + if (line != null && !CHUNK.validLine(line)) { + initFileIfNecessary(); + while (line != null && !CHUNK.validLine(line)) { + if (!processLine(line, FILE_HEADER_RULES)) { + throw new UnifiedDiffParserException("expected file start line not found"); } line = READER.readLine(); + } + } + return line; + } + private String parseChunkSection(String currentLine) throws IOException, UnifiedDiffParserException { + String line = currentLine; + if (line != null) { + processLine(line, CHUNK); + while ((line = READER.readLine()) != null) { line = checkForNoNewLineAtTheEndOfTheFile(line); + + if (!processLine(line, LINE_NORMAL, LINE_ADD, LINE_DEL)) { + throw new UnifiedDiffParserException("expected data line not found"); + } + if (isChunkFinished()) { + finalizeChunk(); + break; + } } - if (line == null || (line.startsWith("--") && !line.startsWith("---"))) { - break; - } + line = READER.readLine(); + line = checkForNoNewLineAtTheEndOfTheFile(line); } + return line; + } + private boolean isChunkFinished() { + return (originalTxt.size() == old_size && revisedTxt.size() == new_size) + || (old_size == 0 + && new_size == 0 + && originalTxt.size() == this.old_ln + && revisedTxt.size() == this.new_ln); + } + + private void parseTailSection() throws IOException { if (READER.ready()) { String tailTxt = ""; while (READER.ready()) { @@ -207,8 +214,6 @@ private UnifiedDiff parse() throws IOException, UnifiedDiffParserException { } data.setTailTxt(tailTxt); } - - return data; } private String checkForNoNewLineAtTheEndOfTheFile(String line) throws IOException { From bbf6ec65db892ddf6787546531f3d1b11889bbf4 Mon Sep 17 00:00:00 2001 From: Sajib Sarkar Date: Sat, 4 Jul 2026 15:17:06 +0600 Subject: [PATCH 6/7] fix: restore public delegator methods in DiffUtils for backward compatibility and fix PMD violations --- .../java/com/github/difflib/DiffUtils.java | 51 ++++++++++++++++--- .../java/com/github/difflib/PatchUtils.java | 2 +- .../text/InlineDiffAnnotatorConfig.java | 2 +- .../unifieddiff/UnifiedDiffReader.java | 3 ++ 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/java-diff-utils/src/main/java/com/github/difflib/DiffUtils.java b/java-diff-utils/src/main/java/com/github/difflib/DiffUtils.java index 85514e0..b10ade5 100644 --- a/java-diff-utils/src/main/java/com/github/difflib/DiffUtils.java +++ b/java-diff-utils/src/main/java/com/github/difflib/DiffUtils.java @@ -54,7 +54,7 @@ public static void withDefaultDiffAlgorithmFactory(DiffAlgorithmFactory factory) */ public static Patch diff( List original, List revised, DiffAlgorithmListener progress) { - return DiffUtils.diff(original, revised, DEFAULT_DIFF.create(), progress); + return diff(original, revised, DEFAULT_DIFF.create(), progress); } /** @@ -66,7 +66,7 @@ public static Patch diff( * @return The patch describing the difference between the original and revised sequences. Never {@code null}. */ public static Patch diff(List original, List revised) { - return DiffUtils.diff(original, revised, DEFAULT_DIFF.create(), null); + return diff(original, revised, DEFAULT_DIFF.create(), null); } /** @@ -79,7 +79,7 @@ public static Patch diff(List original, List re * @return The patch describing the difference between the original and revised sequences. Never {@code null}. */ public static Patch diff(List original, List revised, boolean includeEqualParts) { - return DiffUtils.diff(original, revised, DEFAULT_DIFF.create(), null, includeEqualParts); + return diff(original, revised, DEFAULT_DIFF.create(), null, includeEqualParts); } /** @@ -91,7 +91,7 @@ public static Patch diff(List original, List re * @return The patch describing the difference between the original and revised strings. Never {@code null}. */ public static Patch diff(String sourceText, String targetText, DiffAlgorithmListener progress) { - return DiffUtils.diff(Arrays.asList(sourceText.split("\n")), Arrays.asList(targetText.split("\n")), progress); + return diff(Arrays.asList(sourceText.split("\n")), Arrays.asList(targetText.split("\n")), progress); } /** @@ -109,9 +109,9 @@ public static Patch diff(String sourceText, String targetText, DiffAlgor public static Patch diff( List source, List target, BiPredicate equalizer) { if (equalizer != null) { - return DiffUtils.diff(source, target, DEFAULT_DIFF.create(equalizer)); + return diff(source, target, DEFAULT_DIFF.create(equalizer)); } - return DiffUtils.diff(source, target, DEFAULT_DIFF.create()); + return diff(source, target, DEFAULT_DIFF.create()); } public static Patch diff( @@ -162,5 +162,44 @@ public static Patch diff( return diff(original, revised, algorithm, null); } + + /** + * Computes the difference between the given texts inline. Splits the texts + * into tokens and delegates to the default diff algorithm. + * + * @param original the original text. Must not be {@code null}. + * @param revised the revised text. Must not be {@code null}. + * @return The patch describing the difference between the original and revised texts. + */ + public static Patch diffInline(String original, String revised) { + return InlineDiffUtils.diffInline(original, revised); + } + + /** + * Applies the given patch to the original list and returns the revised list. + * + * @param the type of elements in the lists. + * @param original the original list. Must not be {@code null}. + * @param patch the patch to apply. Must not be {@code null}. + * @return the revised list. + * @throws com.github.difflib.patch.PatchFailedException if the patch cannot be applied. + */ + public static List patch(List original, Patch patch) + throws com.github.difflib.patch.PatchFailedException { + return PatchUtils.patch(original, patch); + } + + /** + * Applies the given patch in reverse to the revised list and returns the original list. + * + * @param the type of elements in the lists. + * @param revised the revised list. Must not be {@code null}. + * @param patch the patch to reverse-apply. Must not be {@code null}. + * @return the reconstructed original list. + */ + public static List unpatch(List revised, Patch patch) { + return PatchUtils.unpatch(revised, patch); + } + private DiffUtils() {} } diff --git a/java-diff-utils/src/main/java/com/github/difflib/PatchUtils.java b/java-diff-utils/src/main/java/com/github/difflib/PatchUtils.java index 42e95f0..46fda7b 100644 --- a/java-diff-utils/src/main/java/com/github/difflib/PatchUtils.java +++ b/java-diff-utils/src/main/java/com/github/difflib/PatchUtils.java @@ -13,7 +13,7 @@ public final class PatchUtils { * Applies the given patch to the original list and returns the revised list. * * @param original a {@link List} representing the original list. - * @param patch a {@link List} representing the patch to apply. + * @param patch a {@link Patch} representing the patch to apply. * @return the revised list. * @throws PatchFailedException if the patch cannot be applied. */ diff --git a/java-diff-utils/src/main/java/com/github/difflib/text/InlineDiffAnnotatorConfig.java b/java-diff-utils/src/main/java/com/github/difflib/text/InlineDiffAnnotatorConfig.java index afc13db..2e7a104 100644 --- a/java-diff-utils/src/main/java/com/github/difflib/text/InlineDiffAnnotatorConfig.java +++ b/java-diff-utils/src/main/java/com/github/difflib/text/InlineDiffAnnotatorConfig.java @@ -15,7 +15,7 @@ *

Instances are created once per {@link DiffRowGenerator} construction and reused * for every delta processed. */ -public final class InlineDiffAnnotatorConfig { +final class InlineDiffAnnotatorConfig { final boolean reportLinesUnchanged; final Function lineNormalizer; diff --git a/java-diff-utils/src/main/java/com/github/difflib/unifieddiff/UnifiedDiffReader.java b/java-diff-utils/src/main/java/com/github/difflib/unifieddiff/UnifiedDiffReader.java index 2d95359..b8f9e52 100644 --- a/java-diff-utils/src/main/java/com/github/difflib/unifieddiff/UnifiedDiffReader.java +++ b/java-diff-utils/src/main/java/com/github/difflib/unifieddiff/UnifiedDiffReader.java @@ -283,6 +283,7 @@ private void initFileIfNecessary() { } } + @SuppressWarnings("unused") private void processDiff(MatchResult match, String line) { // initFileIfNecessary(); LOG.log(Level.FINE, "start {0}", line); @@ -292,6 +293,7 @@ private void processDiff(MatchResult match, String line) { actualFile.setDiffCommand(line); } + @SuppressWarnings("unused") private void processSimilarityIndex(MatchResult match, String line) { actualFile.setSimilarityIndex(Integer.valueOf(match.group(1))); } @@ -343,6 +345,7 @@ private void finalizeChunk() { } } + @SuppressWarnings("unused") private void processNormalLine(MatchResult match, String line) { String cline = line.substring(1); originalTxt.add(cline); From 844974889eb96883367b3436514b6bd2333e4c7e Mon Sep 17 00:00:00 2001 From: Sajib Sarkar Date: Sat, 4 Jul 2026 15:21:58 +0600 Subject: [PATCH 7/7] fix(codacy): resolve excessive parameter and redundant import warnings --- .../src/main/java/com/github/difflib/DiffUtils.java | 7 +++---- .../com/github/difflib/text/InlineDiffAnnotatorConfig.java | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/java-diff-utils/src/main/java/com/github/difflib/DiffUtils.java b/java-diff-utils/src/main/java/com/github/difflib/DiffUtils.java index b10ade5..4b7510c 100644 --- a/java-diff-utils/src/main/java/com/github/difflib/DiffUtils.java +++ b/java-diff-utils/src/main/java/com/github/difflib/DiffUtils.java @@ -19,6 +19,7 @@ import com.github.difflib.algorithm.DiffAlgorithmI; import com.github.difflib.algorithm.DiffAlgorithmListener; import com.github.difflib.patch.Patch; +import com.github.difflib.patch.PatchFailedException; import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -162,7 +163,6 @@ public static Patch diff( return diff(original, revised, algorithm, null); } - /** * Computes the difference between the given texts inline. Splits the texts * into tokens and delegates to the default diff algorithm. @@ -182,10 +182,9 @@ public static Patch diffInline(String original, String revised) { * @param original the original list. Must not be {@code null}. * @param patch the patch to apply. Must not be {@code null}. * @return the revised list. - * @throws com.github.difflib.patch.PatchFailedException if the patch cannot be applied. + * @throws PatchFailedException if the patch cannot be applied. */ - public static List patch(List original, Patch patch) - throws com.github.difflib.patch.PatchFailedException { + public static List patch(List original, Patch patch) throws PatchFailedException { return PatchUtils.patch(original, patch); } diff --git a/java-diff-utils/src/main/java/com/github/difflib/text/InlineDiffAnnotatorConfig.java b/java-diff-utils/src/main/java/com/github/difflib/text/InlineDiffAnnotatorConfig.java index 2e7a104..5c1f5c4 100644 --- a/java-diff-utils/src/main/java/com/github/difflib/text/InlineDiffAnnotatorConfig.java +++ b/java-diff-utils/src/main/java/com/github/difflib/text/InlineDiffAnnotatorConfig.java @@ -29,6 +29,7 @@ final class InlineDiffAnnotatorConfig { final boolean replaceOriginalLinefeedInChangesWithSpaces; final int columnWidth; + @SuppressWarnings({"squid:S107", "PMD.ExcessiveParameterList"}) InlineDiffAnnotatorConfig( boolean reportLinesUnchanged, Function lineNormalizer,