Skip to content

Commit fc4caff

Browse files
DanTupCommit Queue
authored andcommitted
[analysis_server] Allow formatter changes to remove semicolons (and consecutive allowed tokens)
As well as commas, the formatter can remove semicolons from enums. Additionally, it can remove adjacent commas and semicolons! This adds support for both, by simplifying the check to have a set of allowed tokens, and looping to check again after it advances over any tokens. Change-Id: I17a1172c803d579f01c801a372a69226d1dc3277 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/377643 Reviewed-by: Brian Wilkerson <[email protected]> Reviewed-by: Samuel Rawlins <[email protected]> Commit-Queue: Brian Wilkerson <[email protected]>
1 parent 43159ba commit fc4caff

File tree

2 files changed

+100
-46
lines changed

2 files changed

+100
-46
lines changed

pkg/analysis_server/lib/src/lsp/source_edits.dart

Lines changed: 64 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ import 'package:analyzer/src/dart/scanner/scanner.dart';
1919
import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin;
2020
import 'package:dart_style/dart_style.dart';
2121

22-
/// Checks whether a string contains only whitespace and commas.
23-
final _isWhitespaceAndCommas = RegExp(r'^[\s,]*$').hasMatch;
22+
/// Checks whether a string contains only characters that are allowed to differ
23+
/// between unformattedformatted code (such as whitespace, commas, semicolons).
24+
final _isValidFormatterChange = RegExp(r'^[\s,;]*$').hasMatch;
2425

2526
/// Transforms a sequence of LSP document change events to a sequence of source
2627
/// edits used by analysis plugins.
@@ -178,7 +179,7 @@ ErrorOr<List<TextEdit>> generateMinimalEdits(
178179
int unformattedEnd,
179180
int formattedStart,
180181
int formattedEnd, {
181-
bool allowNonWhitespaceDifferences = false,
182+
bool allowAnyContentDifferences = false,
182183
}) {
183184
var unformattedWhitespace =
184185
unformatted.substring(unformattedStart, unformattedEnd);
@@ -271,13 +272,14 @@ ErrorOr<List<TextEdit>> generateMinimalEdits(
271272
endOffset -= commonSuffixLength;
272273
}
273274

274-
// Validate we didn't find more than whitespace or commas. If this occurs,
275-
// it's likely the token offsets used were incorrect. In this case it's
276-
// better to not modify the code than potentially remove something
277-
// important.
278-
if (!allowNonWhitespaceDifferences &&
279-
(!_isWhitespaceAndCommas(oldText) ||
280-
!_isWhitespaceAndCommas(newText))) {
275+
// Unless allowing any differences, validate that the replaced and
276+
// replacement text only contain characters that we expected the formatter
277+
// to have changed. If the change contains other characters, it's likely
278+
// the token offsets used were incorrect and it's better to not modify the
279+
// code than potentially corrupt it.
280+
if (!allowAnyContentDifferences &&
281+
(!_isValidFormatterChange(oldText) ||
282+
!_isValidFormatterChange(newText))) {
281283
return;
282284
}
283285

@@ -311,7 +313,7 @@ ErrorOr<List<TextEdit>> generateMinimalEdits(
311313
var unformattedEnd = unformattedToken.offset;
312314
var formattedStart = formattedOffset;
313315
var formattedEnd = formattedToken.offset;
314-
var allowNonWhitespaceDifferences = false;
316+
var allowAnyContentDifferences = false;
315317

316318
/// Helper to advance the formatted stream by one token if it is not at
317319
/// the end.
@@ -335,41 +337,57 @@ ErrorOr<List<TextEdit>> generateMinimalEdits(
335337
}
336338
}
337339

338-
if (formattedToken.type == TokenType.COMMA &&
339-
unformattedToken.type != TokenType.COMMA) {
340-
// Advance the end of the range over the comma (and subsequent
341-
// whitespace) to the next token.
342-
advanceFormatted();
343-
} else if (unformattedToken.type == TokenType.COMMA &&
344-
formattedToken.type != TokenType.COMMA) {
345-
// Advance the end of the range over the comma (and subsequent
346-
// whitespace) to the next token.
347-
advanceUnformatted();
348-
} else if (unformattedToken is BeginToken &&
349-
unformattedToken.next == unformattedToken.endGroup) {
350-
if (unformattedToken.endGroup case var endGroup?) {
351-
// The formatter may unwrap empty collections across lines which
352-
// will change from two tokens (begin/end) to a single simple token
353-
// for the collection. If the next two unformatted tokens are
354-
// begin/end and match the formatted token, advance over both.
355-
var unformattedLexeme =
356-
'${unformattedToken.lexeme}${endGroup.lexeme}';
357-
if (unformattedLexeme == formattedToken.lexeme) {
358-
advanceUnformatted(); // open token
359-
advanceUnformatted(); // close token
360-
advanceFormatted(); // simple token (open+close)
361-
}
362-
}
363-
} else if ((unformattedToken.type == TokenType.SINGLE_LINE_COMMENT ||
364-
unformattedToken.type == TokenType.MULTI_LINE_COMMENT) &&
365-
unformattedToken.type == formattedToken.type) {
366-
// The formatter may remove trailing whitespace from comments which
367-
// are part of the lexeme (and not between tokens), so if the content is
368-
// different, allow the whole comments to be replaced.
369-
if (unformattedToken.lexeme != formattedToken.lexeme) {
370-
advanceUnformatted();
340+
// A set of tokens that the formatter may add or remove, which we will
341+
// allow and not abort for.
342+
const allowedAddOrRemovedTokens = {
343+
TokenType.COMMA,
344+
TokenType.SEMICOLON,
345+
};
346+
347+
// We may need to advance multiple times if multiple allowed tokens are
348+
// added/removed consecutively.
349+
var continueLoop = true;
350+
while (continueLoop && unformattedHasMore && formattedHasMore) {
351+
continueLoop = false;
352+
353+
if (allowedAddOrRemovedTokens.contains(formattedToken.type) &&
354+
formattedToken.type != unformattedToken.type) {
355+
// The formatter added an allowed token, advance over it.
371356
advanceFormatted();
372-
allowNonWhitespaceDifferences = true;
357+
continueLoop = true;
358+
} else if (allowedAddOrRemovedTokens.contains(unformattedToken.type) &&
359+
formattedToken.type != unformattedToken.type) {
360+
// The formatter removed an allowed token, advance over it.
361+
advanceUnformatted();
362+
continueLoop = true;
363+
} else if (unformattedToken is BeginToken &&
364+
unformattedToken.next == unformattedToken.endGroup) {
365+
if (unformattedToken.endGroup case var endGroup?) {
366+
// The formatter may unwrap empty collections across lines which
367+
// will change from two tokens (begin/end) to a single simple token
368+
// for the collection. If the next two unformatted tokens are
369+
// begin/end and match the formatted token, advance over both.
370+
var unformattedLexeme =
371+
'${unformattedToken.lexeme}${endGroup.lexeme}';
372+
if (unformattedLexeme == formattedToken.lexeme) {
373+
advanceUnformatted(); // open token
374+
advanceUnformatted(); // close token
375+
advanceFormatted(); // simple token (open+close)
376+
continueLoop = true;
377+
}
378+
}
379+
} else if ((unformattedToken.type == TokenType.SINGLE_LINE_COMMENT ||
380+
unformattedToken.type == TokenType.MULTI_LINE_COMMENT) &&
381+
unformattedToken.type == formattedToken.type) {
382+
// The formatter may remove trailing whitespace from comments which
383+
// are part of the lexeme (and not between tokens), so if the content is
384+
// different, allow the whole comments to be replaced.
385+
if (unformattedToken.lexeme != formattedToken.lexeme) {
386+
advanceUnformatted();
387+
advanceFormatted();
388+
allowAnyContentDifferences = true;
389+
continueLoop = true;
390+
}
373391
}
374392
}
375393

@@ -386,7 +404,7 @@ ErrorOr<List<TextEdit>> generateMinimalEdits(
386404
unformattedEnd,
387405
formattedStart,
388406
formattedEnd,
389-
allowNonWhitespaceDifferences: allowNonWhitespaceDifferences,
407+
allowAnyContentDifferences: allowAnyContentDifferences,
390408
);
391409

392410
// And move the pointers along to after these tokens.

pkg/analysis_server/test/lsp/source_edits_test.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,20 @@ Delete 3:1-3:2
252252
await _assertMinimalEdits(startContent, endContent, expectedEdits);
253253
}
254254

255+
Future<void> test_minimalEdits_commaAndSemicolon_remove() async {
256+
const startContent = '''
257+
enum SomeEnum { a, b, c,; }
258+
''';
259+
const endContent = '''
260+
enum SomeEnum { a, b, c }
261+
''';
262+
const expectedEdits = r'''
263+
Delete 1:24-1:26
264+
''';
265+
266+
await _assertMinimalEdits(startContent, endContent, expectedEdits);
267+
}
268+
255269
/// The formatter removes trailing whitespace from comments which results in
256270
/// differences in the comment token lexemes. This should
257271
/// be handled and not result in a full document edit.
@@ -342,6 +356,28 @@ Delete 1:18-2:1
342356
);
343357
}
344358

359+
Future<void> test_minimalEdits_semicolon_remove() async {
360+
const startContent = '''
361+
enum SomeEnum {
362+
a,
363+
b,
364+
c;
365+
}
366+
''';
367+
const endContent = '''
368+
enum SomeEnum {
369+
a,
370+
b,
371+
c
372+
}
373+
''';
374+
const expectedEdits = r'''
375+
Delete 4:4-4:5
376+
''';
377+
378+
await _assertMinimalEdits(startContent, endContent, expectedEdits);
379+
}
380+
345381
Future<void> test_minimalEdits_whitespace() async {
346382
const startContent = '''
347383
void f(){}

0 commit comments

Comments
 (0)