Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
83 changes: 61 additions & 22 deletions tool/dart_skills_lint/lib/src/validation_session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'dart:convert';
import 'dart:io';

import 'package:diff_match_patch/diff_match_patch.dart' as dmp;
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
Expand Down Expand Up @@ -462,29 +463,9 @@ class ValidationSession {
return result;
}

/// Prints a simple line-by-line diff between [original] and [modified].
///
/// **Limitation**: This naive diff algorithm does not handle line additions
/// or removals well, as it compares lines at the same index. It is
/// sufficient for current fixers that only modify existing lines, but
/// should be replaced with a more robust diffing solution (e.g.,
/// `package:diff`) if future fixers add or remove lines.
/// Prints a line-level diff between [original] and [modified] to the logger.
void _printDiff(String original, String modified) {
final List<String> origLines = original.split('\n');
final List<String> modLines = modified.split('\n');
final int maxLines = origLines.length > modLines.length ? origLines.length : modLines.length;
for (var i = 0; i < maxLines; i++) {
final String orig = i < origLines.length ? origLines[i] : '';
final String mod = i < modLines.length ? modLines[i] : '';
if (orig != mod) {
if (orig.isNotEmpty) {
_log.info('- Line ${i + 1}: $orig');
}
if (mod.isNotEmpty) {
_log.info('+ Line ${i + 1}: $mod');
}
}
}
computeLineDiff(original, modified).forEach(_log.info);
}

/// Mutates [ignores] in place to add baseline entries for any non-ignored
Expand Down Expand Up @@ -557,3 +538,61 @@ class ValidationSession {
return path;
}
}

/// Computes a line-level diff between [original] and [modified] suitable for
/// dry-run preview output.
///
/// Returns a list of formatted strings, one per changed line:
/// `'- Line N: <text>'` for removed lines (numbered against [original]) and
/// `'+ Line N: <text>'` for added lines (numbered against [modified]).
/// Unchanged lines are not emitted.
///
/// Handles insertions, deletions, and modifications correctly. Each line of
/// each input is treated atomically (no intra-line diffs).
@visibleForTesting
List<String> computeLineDiff(String original, String modified) {
final lineArray = <String>[];
final lineToIndex = <String, int>{};

String tokenize(String text) {
final buf = StringBuffer();
for (final String line in text.split('\n')) {
final int idx = lineToIndex.putIfAbsent(line, () {
lineArray.add(line);
return lineArray.length - 1;
});
// Offset by 1 so the first line never collides with char code 0 (NUL).
buf.writeCharCode(idx + 1);
}
return buf.toString();
}

final String tokenizedOriginal = tokenize(original);
final String tokenizedModified = tokenize(modified);

final List<dmp.Diff> diffs = dmp.diff(tokenizedOriginal, tokenizedModified, checklines: false);

final output = <String>[];
var originalLineNumber = 1;
var modifiedLineNumber = 1;

for (final d in diffs) {
final List<int> codes = d.text.codeUnits;
switch (d.operation) {
case dmp.DIFF_EQUAL:
originalLineNumber += codes.length;
modifiedLineNumber += codes.length;
case dmp.DIFF_DELETE:
for (final c in codes) {
output.add('- Line $originalLineNumber: ${lineArray[c - 1]}');
originalLineNumber++;
}
case dmp.DIFF_INSERT:
for (final c in codes) {
output.add('+ Line $modifiedLineNumber: ${lineArray[c - 1]}');
modifiedLineNumber++;
}
}
}
return output;
}
1 change: 1 addition & 0 deletions tool/dart_skills_lint/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ environment:

dependencies:
args: ^2.4.0
diff_match_patch: ^0.4.1
io: ^1.0.4
logging: ^1.2.0
meta: ^1.11.0
Expand Down
74 changes: 74 additions & 0 deletions tool/dart_skills_lint/test/diff_printer_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) 2026, 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 'package:dart_skills_lint/src/validation_session.dart';
import 'package:test/test.dart';

void main() {
group('computeLineDiff', () {
test('returns empty list when texts are identical', () {
expect(computeLineDiff('a\nb\nc', 'a\nb\nc'), isEmpty);
});

test('emits paired - and + lines for a pure modification', () {
const original = 'line1\nline2 \nline3';
const modified = 'line1\nline2\nline3';

expect(computeLineDiff(original, modified), <String>['- Line 2: line2 ', '+ Line 2: line2']);
});

test('emits only + lines for a pure insertion at the end', () {
const original = 'a\nb';
const modified = 'a\nb\nc';

expect(computeLineDiff(original, modified), <String>['+ Line 3: c']);
});

test('emits only + lines for a pure insertion in the middle', () {
const original = 'a\nc';
const modified = 'a\nb\nc';

expect(computeLineDiff(original, modified), <String>['+ Line 2: b']);
});

test('emits only - lines for a pure deletion', () {
const original = 'a\nb\nc';
const modified = 'a\nc';

expect(computeLineDiff(original, modified), <String>['- Line 2: b']);
});

test('handles mixed modification + insertion with correct line numbers', () {
const original = 'header\nbody\nfooter';
const modified = 'header\nbody-fixed\nextra\nfooter';

expect(computeLineDiff(original, modified), <String>[
'- Line 2: body',
'+ Line 2: body-fixed',
'+ Line 3: extra',
]);
});

test('handles deletion followed by modification with correct line numbers', () {
const original = 'a\nb\nc\nd';
const modified = 'a\nc-fixed\nd';

expect(computeLineDiff(original, modified), <String>[
'- Line 2: b',
'- Line 3: c',
'+ Line 2: c-fixed',
]);
});

test('preserves significant whitespace in line contents', () {
const original = '\t indented\n spaced';
const modified = '\t fixed\n spaced';

expect(computeLineDiff(original, modified), <String>[
'- Line 1: \t indented',
'+ Line 1: \t fixed',
]);
});
});
}
Loading