Skip to content

Commit 68f0d0e

Browse files
DanTupCommit Queue
authored andcommitted
[analysis_server] Fix handling of edits that require sequential processing
This moves the check for whether we need to apply edits sequentially to above some of the validation checks (because they are only applicable if not applying sequentially) and tweaks the logic slightly. It also removes a third copy of this function in analysis_server that was only used in one test, so we're down to two copies now (analyzer_plugin and analysis_server_client). Fixes #61501 Fixes #61488 Change-Id: I89ac94e8fd65e904c4eddcd8f1ebba3bf1293487 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/450380 Commit-Queue: Brian Wilkerson <[email protected]> Reviewed-by: Samuel Rawlins <[email protected]> Reviewed-by: Brian Wilkerson <[email protected]>
1 parent b485a27 commit 68f0d0e

File tree

5 files changed

+212
-95
lines changed

5 files changed

+212
-95
lines changed

pkg/analysis_server/lib/src/protocol/protocol_internal.dart

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -20,46 +20,6 @@ export 'package:analyzer_plugin/src/protocol/protocol_internal.dart'
2020
final Map<String, RefactoringKind> REQUEST_ID_REFACTORING_KINDS =
2121
HashMap<String, RefactoringKind>();
2222

23-
/// Get the result of applying a set of [edits] to the given [code]. Edits
24-
/// are applied in the order they appear in [edits]. Access via
25-
/// SourceEdit.applySequence().
26-
String applySequenceOfEdits(String code, List<SourceEdit> edits) {
27-
var buffer = StringBuffer();
28-
var start = 0;
29-
for (var i = edits.length - 1; i >= 0; i--) {
30-
var edit = edits[i];
31-
var offset = edit.offset;
32-
var length = edit.length;
33-
if (length < 0) {
34-
throw RangeError('The edit length is negative.');
35-
}
36-
if (offset + length > code.length) {
37-
throw RangeError('The edit extends past the end of the code.');
38-
}
39-
if (start > offset) {
40-
// One of the edits overlaps with another, requiring that they be applied
41-
// from largest offset to smallest. This should only be possible in code
42-
// that creates source edits without using the `ChangeBuilder` to do so.
43-
//
44-
// We should consider fixing the places where overlapping edits are
45-
// produced so that this branch can be removed. One such place is
46-
// exhibited by `_DoCompletionTest.test_noBody`.
47-
for (var edit in edits) {
48-
code = edit.apply(code);
49-
}
50-
return code;
51-
} else if (start < offset) {
52-
buffer.write(code.substring(start, offset));
53-
}
54-
buffer.write(edit.replacement);
55-
start = offset + length;
56-
}
57-
if (start < code.length) {
58-
buffer.write(code.substring(start));
59-
}
60-
return buffer.toString();
61-
}
62-
6323
/// Compare the lists [listA] and [listB], using [itemEqual] to compare
6424
/// list elements.
6525
bool listEqual<T>(

pkg/analysis_server/test/lsp/document_changes_test.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import 'dart:async';
66

77
import 'package:analysis_server/lsp_protocol/protocol.dart';
8-
import 'package:analysis_server/src/protocol/protocol_internal.dart';
98
import 'package:analyzer_plugin/protocol/protocol_common.dart' hide Position;
109
import 'package:test/test.dart';
1110
import 'package:test_reflective_loader/test_reflective_loader.dart';
@@ -60,7 +59,7 @@ class Bar {
6059
as ChangeContentOverlay;
6160

6261
expect(
63-
applySequenceOfEdits(content, notifiedChanges.edits),
62+
SourceEdit.applySequence(content, notifiedChanges.edits),
6463
contentAfterUpdate,
6564
);
6665
}

pkg/analysis_server/test/services/correction/change_test.dart

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,9 @@ class ChangeTest {
132132

133133
@reflectiveTest
134134
class EditTest {
135-
void test_applySequence() {
135+
/// There is no ambiguity in edits sorted last-to-first and the implementation
136+
/// may optimize this case.
137+
void test_applySequence_lastToFirst() {
136138
var edit1 = SourceEdit(5, 2, 'abc');
137139
var edit2 = SourceEdit(1, 0, '!');
138140
expect(
@@ -141,6 +143,124 @@ class EditTest {
141143
);
142144
}
143145

146+
void test_applySequence_lastToFirst_invalidLength_negative() {
147+
var edit1 = SourceEdit(1, 0, 'a');
148+
var edit2 = SourceEdit(0, -1, '');
149+
expect(
150+
() => SourceEdit.applySequence('', [edit1, edit2]),
151+
throwsRangeError('The edit length is negative.'),
152+
);
153+
}
154+
155+
void test_applySequence_lastToFirst_invalidLength_pastEndOfString() {
156+
var edit1 = SourceEdit(1, 100, 'a');
157+
var edit2 = SourceEdit(0, 0, '');
158+
expect(
159+
() => SourceEdit.applySequence('aa', [edit1, edit2]),
160+
throwsRangeError('The edit extends past the end of the code.'),
161+
);
162+
}
163+
164+
void test_applySequence_lastToFirst_invalidOffset_negative() {
165+
var edit1 = SourceEdit(1, 0, 'a');
166+
var edit2 = SourceEdit(-1, 0, '');
167+
expect(
168+
() => SourceEdit.applySequence('', [edit1, edit2]),
169+
throwsRangeError('The edit offset is negative.'),
170+
);
171+
}
172+
173+
void test_applySequence_lastToFirst_invalidOffset_pastEndOfString() {
174+
var edit1 = SourceEdit(1, 0, 'a');
175+
var edit2 = SourceEdit(0, 0, 'b');
176+
expect(
177+
() => SourceEdit.applySequence('', [edit1, edit2]),
178+
throwsRangeError('The edit starts past the end of the code.'),
179+
);
180+
}
181+
182+
/// Last-to-first offsets may have overlaps if edit `n+1` replaces text
183+
/// inserted by edit `n`. The result should be as if they were applied
184+
/// sequentially.
185+
///
186+
/// Although sorted last-to-first, this case will fall back to sequential
187+
/// processing.
188+
void test_applySequence_lastToFirstOffsets_overlap() {
189+
var edit1 = SourceEdit(3, 0, '1111');
190+
var edit2 = SourceEdit(1, 4, '2222'); // replaces aa11
191+
expect(SourceEdit.applySequence('aaaa', [edit1, edit2]), 'a222211a');
192+
}
193+
194+
/// Last-to-first offsets may have overlaps if edit `n+1` replaces text
195+
/// inserted by edit `n`. The result should be as if they were applied
196+
/// sequentially.
197+
///
198+
/// Although sorted last-to-first, this case will fall back to sequential
199+
/// processing.
200+
void test_applySequence_lastToFirstOffsets_touching() {
201+
var edit1 = SourceEdit(3, 0, '1111');
202+
var edit2 = SourceEdit(1, 2, '2222'); // replaces aa
203+
expect(SourceEdit.applySequence('aaaa', [edit1, edit2]), 'a22221111a');
204+
}
205+
206+
/// Edits are described sequentially, so the offsets in edit `n` assume edit
207+
/// `n-1` has been applied.
208+
void test_applySequence_sequential() {
209+
var edit1 = SourceEdit(0, 0, '1111');
210+
var edit2 = SourceEdit(2, 0, '2222');
211+
expect(SourceEdit.applySequence('', [edit1, edit2]), '11222211');
212+
}
213+
214+
void test_applySequence_sequential_invalidLength_negative() {
215+
var edit1 = SourceEdit(0, -1, '');
216+
var edit2 = SourceEdit(0, 0, '');
217+
expect(
218+
() => SourceEdit.applySequence('', [edit1, edit2]),
219+
throwsRangeError('The edit length is negative.'),
220+
);
221+
}
222+
223+
void test_applySequence_sequential_invalidLength_pastEndOfString() {
224+
var edit1 = SourceEdit(0, 0, '');
225+
var edit2 = SourceEdit(0, 100, '');
226+
expect(
227+
() => SourceEdit.applySequence('', [edit1, edit2]),
228+
throwsRangeError('The edit extends past the end of the code.'),
229+
);
230+
}
231+
232+
void test_applySequence_sequential_invalidOffset_negative() {
233+
var edit1 = SourceEdit(-1, 0, '');
234+
var edit2 = SourceEdit(0, 0, '');
235+
expect(
236+
() => SourceEdit.applySequence('', [edit1, edit2]),
237+
throwsRangeError('The edit offset is negative.'),
238+
);
239+
}
240+
241+
void test_applySequence_sequential_invalidOffset_pastEndOfString() {
242+
var edit1 = SourceEdit(0, 0, '');
243+
var edit2 = SourceEdit(100, 0, '');
244+
expect(
245+
() => SourceEdit.applySequence('', [edit1, edit2]),
246+
throwsRangeError('The edit starts past the end of the code.'),
247+
);
248+
}
249+
250+
/// Edits are described sequentially, so repeated offsets in inserts
251+
/// result in the second insert ending up in front of the first.
252+
void test_applySequence_sequential_sameOffsets() {
253+
var edit1 = SourceEdit(0, 0, '1111');
254+
var edit2 = SourceEdit(0, 0, '2222');
255+
expect(SourceEdit.applySequence('', [edit1, edit2]), '22221111');
256+
}
257+
258+
void test_applySequence_sequential_validLength_assumesPriorEdit() {
259+
var edit1 = SourceEdit(0, 0, '1111');
260+
var edit2 = SourceEdit(0, 4, ''); // Valid because it deletes 1111.
261+
expect(SourceEdit.applySequence('', [edit1, edit2]), '');
262+
}
263+
144264
void test_editFromRange() {
145265
var range = SourceRange(1, 2);
146266
var edit = newSourceEdit_range(range, 'foo');
@@ -177,6 +297,14 @@ class EditTest {
177297
var expectedJson = {OFFSET: 1, LENGTH: 2, REPLACEMENT: 'foo'};
178298
expect(edit.toJson(), expectedJson);
179299
}
300+
301+
Matcher throwsRangeError(String message) => throwsA(
302+
const TypeMatcher<RangeError>().having(
303+
(e) => e.message,
304+
'message',
305+
message,
306+
),
307+
);
180308
}
181309

182310
@reflectiveTest

pkg/analysis_server_client/lib/src/protocol/protocol_internal.dart

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -69,44 +69,41 @@ void addEditToSourceChange(
6969
fileEdit.add(edit, insertBeforeExisting: insertBeforeExisting);
7070
}
7171

72-
/// Get the result of applying the edit to the given [code]. Access via
73-
/// SourceEdit.apply().
72+
/// Get the result of applying the edit to the given [code].
73+
///
74+
/// Access via SourceEdit.apply().
7475
String applyEdit(String code, SourceEdit edit) {
75-
if (edit.length < 0) {
76-
throw RangeError('length is negative');
77-
}
76+
validateEdit(code, edit);
77+
7878
return code.replaceRange(edit.offset, edit.end, edit.replacement);
7979
}
8080

81-
/// Get the result of applying a set of [edits] to the given [code]. Edits
82-
/// are applied in the order they appear in [edits]. Access via
83-
/// SourceEdit.applySequence().
81+
/// Get the result of applying a set of [edits] to the given [code].
82+
///
83+
/// The edits are applied in the order in which they occur in the list. This
84+
/// means that the offset of each edit must be correct under the assumption
85+
/// that all previous edits have been applied.
86+
///
87+
/// Access via SourceEdit.applySequence().
8488
String applySequenceOfEdits(String code, List<SourceEdit> edits) {
89+
// This function exists in both analysis_server_client and analyzer_plugin!
90+
8591
var buffer = StringBuffer();
8692
var start = 0;
8793
for (var i = edits.length - 1; i >= 0; i--) {
8894
var edit = edits[i];
8995
var offset = edit.offset;
9096
var length = edit.length;
91-
if (length < 0) {
92-
throw RangeError('The edit length is negative.');
93-
}
94-
if (offset + length > code.length) {
95-
throw RangeError('The edit extends past the end of the code.');
97+
98+
// If this edit overlaps or is not before the next (previous in this
99+
// backwards loop) one in the sequence, fall back to sequential application.
100+
if (i > 0 && offset + length >= edits[i - 1].offset) {
101+
return edits.fold(code, (code, edit) => edit.apply(code));
96102
}
97-
if (start > offset) {
98-
// One of the edits overlaps with another, requiring that they be applied
99-
// from largest offset to smallest. This should only be possible in code
100-
// that creates source edits without using the `ChangeBuilder` to do so.
101-
//
102-
// We should consider fixing the places where overlapping edits are
103-
// produced so that this branch can be removed. One such place is
104-
// exhibited by `_DoCompletionTest.test_noBody`.
105-
for (var edit in edits) {
106-
code = edit.apply(code);
107-
}
108-
return code;
109-
} else if (start < offset) {
103+
104+
validateEdit(code, edit);
105+
106+
if (start < offset) {
110107
buffer.write(code.substring(start, offset));
111108
}
112109
buffer.write(edit.replacement);
@@ -267,6 +264,24 @@ RefactoringOptions? refactoringOptionsFromJson(JsonDecoder jsonDecoder,
267264
return null;
268265
}
269266

267+
/// Validates whether [edit] can be applied to [code].
268+
///
269+
/// Throws [RangeError] if the edit contains an invalid offset/length.
270+
void validateEdit(String code, SourceEdit edit) {
271+
if (edit.offset < 0) {
272+
throw RangeError('The edit offset is negative.');
273+
}
274+
if (edit.length < 0) {
275+
throw RangeError('The edit length is negative.');
276+
}
277+
if (edit.offset > code.length) {
278+
throw RangeError('The edit starts past the end of the code.');
279+
}
280+
if (edit.offset + edit.length > code.length) {
281+
throw RangeError('The edit extends past the end of the code.');
282+
}
283+
}
284+
270285
/// Type of callbacks used to decode parts of JSON objects. [jsonPath] is a
271286
/// string describing the part of the JSON object being decoded, and [value] is
272287
/// the part to decode.

0 commit comments

Comments
 (0)