| // Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'dart:math'; |
| |
| /// Generates a "focused" unified diff, showing only the changed lines |
| /// plus a few lines of context around them. |
| List<String> generateFocusedDiff( |
| String expected, |
| String actual, { |
| int contextLines = 3, |
| }) { |
| var fullDiff = _createDiff(expected, actual); |
| var outputLines = <String>[]; |
| |
| var indicesToInclude = <int>{}; |
| |
| for (var (i, result) in fullDiff.indexed) { |
| if (result.type != _DiffType.common) { |
| indicesToInclude.add(i); |
| for ( |
| int j = max(0, i - contextLines); |
| j < min(fullDiff.length, i + contextLines + 1); |
| j++ |
| ) { |
| indicesToInclude.add(j); |
| } |
| } |
| } |
| |
| if (indicesToInclude.isEmpty) { |
| return ['No differences found.']; |
| } |
| |
| int lastIncludedIndex = -1; |
| var sortedIndices = indicesToInclude.toList()..sort(); |
| |
| for (int i in sortedIndices) { |
| if (lastIncludedIndex != -1 && i > lastIncludedIndex + 1) { |
| outputLines.add('...'); |
| } |
| |
| outputLines.add(_formatLine(fullDiff[i])); |
| lastIncludedIndex = i; |
| } |
| |
| return outputLines; |
| } |
| |
| /// Generates a "full" unified diff, showing every line from the comparison. |
| List<String> generateFullDiff(String expected, String actual) { |
| var diffResults = _createDiff(expected, actual); |
| return diffResults.map(_formatLine).toList(); |
| } |
| |
| void printPrettyDiff(String expected, String actual, {int context = 3}) { |
| var full = generateFullDiff(expected, actual); |
| var short = generateFocusedDiff(expected, actual); |
| |
| if (full.length * 0.3 >= short.length) { |
| print('-------- Short diff --------'); |
| print(short.join('\n')); |
| } |
| print('-------- Full diff ---------'); |
| print(full.join('\n')); |
| print('---------- Actual ----------'); |
| print(actual.trimRight()); |
| print('----------------------------'); |
| } |
| |
| /// Backtracks through the LCS table to build the list of diff results. |
| List<_DiffResult> _backtrack( |
| List<List<int>> table, |
| List<String> list1, |
| List<String> list2, |
| ) { |
| var diff = <_DiffResult>[]; |
| int i = list1.length; |
| int j = list2.length; |
| |
| while (i > 0 || j > 0) { |
| if (i == 0) { |
| diff.add(_DiffResult(_DiffType.added, list2[j - 1])); |
| j--; |
| continue; |
| } |
| if (j == 0) { |
| diff.add(_DiffResult(_DiffType.removed, list1[i - 1])); |
| i--; |
| continue; |
| } |
| if (list1[i - 1] == list2[j - 1]) { |
| diff.add(_DiffResult(_DiffType.common, list1[i - 1])); |
| i--; |
| j--; |
| } else if (table[i - 1][j] >= table[i][j - 1]) { |
| diff.add(_DiffResult(_DiffType.removed, list1[i - 1])); |
| i--; |
| } else { |
| diff.add(_DiffResult(_DiffType.added, list2[j - 1])); |
| j--; |
| } |
| } |
| |
| return diff.reversed.toList(); |
| } |
| |
| List<List<int>> _computeLcsTable(List<String> list1, List<String> list2) { |
| var n = list1.length; |
| var m = list2.length; |
| |
| var table = List.generate(n + 1, (_) => List.filled(m + 1, 0)); |
| |
| for (int i = 1; i <= n; i++) { |
| for (int j = 1; j <= m; j++) { |
| if (list1[i - 1] == list2[j - 1]) { |
| table[i][j] = table[i - 1][j - 1] + 1; |
| } else { |
| table[i][j] = max(table[i - 1][j], table[i][j - 1]); |
| } |
| } |
| } |
| return table; |
| } |
| |
| /// Returns a line by line difference between [actual] and [expected]. |
| /// |
| /// It uses Longest Common Sequence Algorithm to generate a table |
| /// (https://en.wikipedia.org/wiki/Longest_common_subsequence) |
| List<_DiffResult> _createDiff(String expected, String actual) { |
| var expectedLines = expected.split('\n'); |
| var actualLines = actual.split('\n'); |
| |
| var lcsTable = _computeLcsTable(expectedLines, actualLines); |
| |
| return _backtrack(lcsTable, expectedLines, actualLines); |
| } |
| |
| String _formatLine(_DiffResult result) { |
| switch (result.type) { |
| case _DiffType.added: |
| return '+ ${result.line}'; |
| case _DiffType.removed: |
| return '- ${result.line}'; |
| case _DiffType.common: |
| return ' ${result.line}'; |
| } |
| } |
| |
| class _DiffResult { |
| final _DiffType type; |
| final String line; |
| |
| _DiffResult(this.type, this.line); |
| } |
| |
| enum _DiffType { added, removed, common } |