@@ -19,8 +19,9 @@ import 'package:analyzer/src/dart/scanner/scanner.dart';
19
19
import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin;
20
20
import 'package:dart_style/dart_style.dart' ;
21
21
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;
24
25
25
26
/// Transforms a sequence of LSP document change events to a sequence of source
26
27
/// edits used by analysis plugins.
@@ -178,7 +179,7 @@ ErrorOr<List<TextEdit>> generateMinimalEdits(
178
179
int unformattedEnd,
179
180
int formattedStart,
180
181
int formattedEnd, {
181
- bool allowNonWhitespaceDifferences = false ,
182
+ bool allowAnyContentDifferences = false ,
182
183
}) {
183
184
var unformattedWhitespace =
184
185
unformatted.substring (unformattedStart, unformattedEnd);
@@ -271,13 +272,14 @@ ErrorOr<List<TextEdit>> generateMinimalEdits(
271
272
endOffset -= commonSuffixLength;
272
273
}
273
274
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))) {
281
283
return ;
282
284
}
283
285
@@ -311,7 +313,7 @@ ErrorOr<List<TextEdit>> generateMinimalEdits(
311
313
var unformattedEnd = unformattedToken.offset;
312
314
var formattedStart = formattedOffset;
313
315
var formattedEnd = formattedToken.offset;
314
- var allowNonWhitespaceDifferences = false ;
316
+ var allowAnyContentDifferences = false ;
315
317
316
318
/// Helper to advance the formatted stream by one token if it is not at
317
319
/// the end.
@@ -335,41 +337,57 @@ ErrorOr<List<TextEdit>> generateMinimalEdits(
335
337
}
336
338
}
337
339
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.
371
356
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
+ }
373
391
}
374
392
}
375
393
@@ -386,7 +404,7 @@ ErrorOr<List<TextEdit>> generateMinimalEdits(
386
404
unformattedEnd,
387
405
formattedStart,
388
406
formattedEnd,
389
- allowNonWhitespaceDifferences : allowNonWhitespaceDifferences ,
407
+ allowAnyContentDifferences : allowAnyContentDifferences ,
390
408
);
391
409
392
410
// And move the pointers along to after these tokens.
0 commit comments