Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(141)

Side by Side Diff: pkg/analysis_server/lib/src/plugin/result_merger.dart

Issue 2678673002: Add support for merging results from plugins (Closed)
Patch Set: Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | pkg/analysis_server/test/src/plugin/result_merger_test.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file.
4
5 import 'dart:collection';
6
7 import 'package:analysis_server/plugin/protocol/protocol.dart';
8 import 'package:meta/meta.dart';
9
10 /**
11 * An object used to merge partial lists of results that were contributed by
12 * plugins.
13 *
14 * All of the methods in this class assume that the contributions from the
15 * analysis server are the first partial result in the list of partial results
16 * to be merged.
17 */
18 class ResultMerger {
19 /**
20 * Return a list of fixes composed by merging the lists of fixes in the
21 * [partialResultList].
22 *
23 * The resulting list of fixes will contain exactly one fix for every analysis
24 * error for which there are fixes. If two or more plugins contribute the same
25 * fix for a given error, the resulting list will contain duplications.
26 */
27 List<AnalysisErrorFixes> mergeAnalysisErrorFixes(
28 List<List<AnalysisErrorFixes>> partialResultList) {
29 /**
30 * Return a key encoding the unique attributes of the given [error].
31 */
32 String computeKey(AnalysisError error) {
33 StringBuffer buffer = new StringBuffer();
34 buffer.write(error.location.offset);
35 buffer.write(';');
36 buffer.write(error.code);
37 buffer.write(';');
38 buffer.write(error.message);
39 buffer.write(';');
40 buffer.write(error.correction);
41 return buffer.toString();
42 }
43
44 int count = partialResultList.length;
45 if (count == 0) {
46 return <AnalysisErrorFixes>[];
47 } else if (count == 1) {
48 return partialResultList[0];
49 }
50 List<AnalysisErrorFixes> mergedFixes =
51 new List<AnalysisErrorFixes>.from(partialResultList[0]);
52 Map<String, AnalysisErrorFixes> fixesMap = <String, AnalysisErrorFixes>{};
53 for (AnalysisErrorFixes fix in mergedFixes) {
54 fixesMap[computeKey(fix.error)] = fix;
55 }
56 for (int i = 1; i < count; i++) {
57 for (AnalysisErrorFixes fix in partialResultList[i]) {
58 String key = computeKey(fix.error);
59 AnalysisErrorFixes mergedFix = fixesMap[key];
60 if (mergedFix == null) {
61 mergedFixes.add(fix);
62 fixesMap[key] = fix;
63 } else {
64 // If more than two plugins contribute fixes for the same error, this
65 // will result in extra copy operations.
66 List<SourceChange> mergedChanges =
67 new List<SourceChange>.from(mergedFix.fixes);
68 mergedChanges.addAll(fix.fixes);
69 AnalysisErrorFixes copiedFix =
70 new AnalysisErrorFixes(mergedFix.error, fixes: mergedChanges);
71 mergedFixes[mergedFixes.indexOf(mergedFix)] = copiedFix;
scheglov 2017/02/05 20:11:02 Maybe we could use just map and return fixesMap.va
Brian Wilkerson 2017/02/06 15:11:29 The question is: does the client use the order of
scheglov 2017/02/06 17:18:40 Yes, the order matters. We sort fixes and assists
Brian Wilkerson 2017/02/07 16:06:40 I think that's a good idea. I'll do that in a foll
72 fixesMap[key] = copiedFix;
73 }
74 }
75 }
76 return mergedFixes;
77 }
78
79 /**
80 * Return a list of errors composed by merging the lists of errors in the
81 * [partialResultList].
82 *
83 * The resulting list will contain all of the analysis errors from all of the
84 * plugins. If two or more plugins contribute the same error the resulting
85 * list will contain duplications.
86 */
87 List<AnalysisError> mergeAnalysisErrors(
88 List<List<AnalysisError>> partialResultList) {
89 // TODO(brianwilkerson) Consider merging duplicate errors (same code,
90 // location, and messages). If we do that, we should return the logical-or
91 // of the hasFix fields from the merged errors.
92 int count = partialResultList.length;
93 if (count == 0) {
94 return <AnalysisError>[];
95 } else if (count == 1) {
96 return partialResultList[0];
97 }
98 List<AnalysisError> mergedErrors = <AnalysisError>[];
99 for (List<AnalysisError> partialResults in partialResultList) {
100 mergedErrors.addAll(partialResults);
101 }
102 return mergedErrors;
103 }
104
105 /**
106 * Return a list of suggestions composed by merging the lists of suggestions
107 * in the [partialResultList].
108 *
109 * The resulting list will contain all of the suggestions from all of the
110 * plugins. If two or more plugins contribute the same suggestion the
111 * resulting list will contain duplications.
112 */
113 List<CompletionSuggestion> mergeCompletionSuggestions(
114 List<List<CompletionSuggestion>> partialResultList) {
115 int count = partialResultList.length;
116 if (count == 0) {
117 return <CompletionSuggestion>[];
118 } else if (count == 1) {
119 return partialResultList[0];
120 }
121 List<CompletionSuggestion> mergedSuggestions = <CompletionSuggestion>[];
122 for (List<CompletionSuggestion> partialResults in partialResultList) {
123 mergedSuggestions.addAll(partialResults);
124 }
125 return mergedSuggestions;
126 }
127
128 /**
129 * Return a list of regions composed by merging the lists of regions in the
130 * [partialResultList].
131 *
132 * The resulting list will contain all of the folding regions from all of the
133 * plugins. If a plugin contributes a folding region that overlaps a region
134 * from a previous plugin, the overlapping region will be omitted. (For these
135 * purposes, if either region is fully contained within the other they are not
136 * considered to be overlapping.)
137 */
138 List<FoldingRegion> mergeFoldingRegions(
139 List<List<FoldingRegion>> partialResultList) {
140 int count = partialResultList.length;
141 if (count == 0) {
142 return <FoldingRegion>[];
143 } else if (count == 1) {
144 return partialResultList[0];
145 }
146 List<FoldingRegion> mergedRegions =
147 new List<FoldingRegion>.from(partialResultList[0]);
148
149 /**
150 * Return `true` if the [newRegion] does not overlap any of the regions in
151 * the collection of [mergedRegions].
152 */
153 bool isNonOverlapping(FoldingRegion newRegion) {
154 int newStart = newRegion.offset;
155 int newEnd = newStart + newRegion.length;
156 for (FoldingRegion existingRegion in mergedRegions) {
157 int existingStart = existingRegion.offset;
158 int existingEnd = existingStart + existingRegion.length;
159 if (overlaps(newStart, newEnd, existingStart, existingEnd,
160 allowNesting: true)) {
161 return false;
162 }
163 }
164 return true;
165 }
166
167 for (int i = 1; i < count; i++) {
168 List<FoldingRegion> partialResults = partialResultList[i];
169 for (FoldingRegion region in partialResults) {
170 if (isNonOverlapping(region)) {
171 mergedRegions.add(region);
172 }
173 }
174 }
175 return mergedRegions;
176 }
177
178 /**
179 * Return a list of regions composed by merging the lists of regions in the
180 * [partialResultList].
181 *
182 * The resulting list will contain all of the highlight regions from all of
183 * the plugins. If two or more plugins contribute the same highlight region
184 * the resulting list will contain duplications.
185 */
186 List<HighlightRegion> mergeHighlightRegions(
187 List<List<HighlightRegion>> partialResultList) {
188 int count = partialResultList.length;
189 if (count == 0) {
190 return <HighlightRegion>[];
191 } else if (count == 1) {
192 return partialResultList[0];
193 }
194 List<HighlightRegion> mergedRegions = <HighlightRegion>[];
195 for (List<HighlightRegion> partialResults in partialResultList) {
196 mergedRegions.addAll(partialResults);
197 }
198 return mergedRegions;
199 }
200
201 /**
202 * Return navigation notification parameters composed by merging the
203 * parameters in the [partialResultList].
204 *
205 * The resulting list will contain all of the navigation regions from all of
206 * the plugins. If a plugin contributes a navigation region that overlaps a
207 * region from a previous plugin, the overlapping region will be omitted. (For
208 * these purposes, nested regions are considered to be overlapping.)
209 */
210 AnalysisNavigationParams mergeNavigation(
211 List<AnalysisNavigationParams> partialResultList) {
212 int count = partialResultList.length;
213 if (count == 0) {
214 return null;
215 } else if (count == 1) {
216 return partialResultList[0];
217 }
218 AnalysisNavigationParams base = partialResultList[0];
219 String file = base.file;
220 List<NavigationRegion> mergedRegions =
221 new List<NavigationRegion>.from(base.regions);
scheglov 2017/02/05 20:11:02 BTW, base.regions.toList() would be easier.
Brian Wilkerson 2017/02/06 15:11:29 I keep forgetting that `toList` *always* creates a
222 List<NavigationTarget> mergedTargets =
223 new List<NavigationTarget>.from(base.targets);
224 List<String> mergedFiles = new List<String>.from(base.files);
225
226 /**
227 * Return `true` if the [newRegion] does not overlap any of the regions in
228 * the collection of [mergedRegions].
229 */
230 bool isNonOverlapping(NavigationRegion newRegion) {
231 int newStart = newRegion.offset;
232 int newEnd = newStart + newRegion.length;
233 for (NavigationRegion mergedRegion in mergedRegions) {
234 int mergedStart = mergedRegion.offset;
235 int mergedEnd = mergedStart + mergedRegion.length;
236 if (overlaps(newStart, newEnd, mergedStart, mergedEnd)) {
237 return false;
238 }
239 }
240 return true;
241 }
242
243 /**
244 * Return the index of the region in the collection of [mergedRegions] that
245 * covers exactly the same region as the [newRegion], or `-1` if there is no
246 * such region.
247 */
248 int matchingRegion(newRegion) {
249 int newOffset = newRegion.offset;
250 int newLength = newRegion.length;
251 for (int i = 0; i < mergedRegions.length; i++) {
252 NavigationRegion mergedRegion = mergedRegions[i];
253 if (newOffset == mergedRegion.offset &&
254 newLength == mergedRegion.length) {
255 return i;
256 }
257 }
258 return -1;
259 }
260
261 for (int i = 1; i < count; i++) {
262 // For now we take the optimistic approach of assuming that most or all of
263 // the regions will not overlap and that we therefore don't need to remove
264 // any unreferenced files or targets from the lists. If that isn't true
265 // then this could result in server sending more data to the client than
266 // is necessary.
267 AnalysisNavigationParams result = partialResultList[i];
268 List<NavigationRegion> regions = result.regions;
269 List<NavigationTarget> targets = result.targets;
270 List<String> files = result.files;
271 //
272 // Merge the file data.
273 //
274 Map<int, int> fileMap = <int, int>{};
275 for (int j = 0; j < files.length; j++) {
276 String file = files[j];
277 int index = mergedFiles.indexOf(file);
278 if (index < 0) {
279 index = mergedFiles.length;
280 mergedFiles.add(file);
281 }
282 fileMap[j] = index;
283 }
284 //
285 // Merge the target data.
286 //
287 Map<int, int> targetMap = <int, int>{};
288 for (int j = 0; j < targets.length; j++) {
289 NavigationTarget target = targets[j];
290 int newIndex = fileMap[target.fileIndex];
291 if (target.fileIndex != newIndex) {
292 target = new NavigationTarget(target.kind, newIndex, target.offset,
293 target.length, target.startLine, target.startColumn);
294 }
295 int index = mergedTargets.indexOf(target);
296 if (index < 0) {
297 index = mergedTargets.length;
298 mergedTargets.add(target);
299 }
300 targetMap[j] = index;
301 }
302 //
303 // Merge the region data.
304 //
305 for (int j = 0; j < regions.length; j++) {
306 NavigationRegion region = regions[j];
307 List<int> newTargets = region.targets
308 .map((int oldTarget) => targetMap[oldTarget])
309 .toList();
310 if (region.targets != newTargets) {
311 region =
312 new NavigationRegion(region.offset, region.length, newTargets);
313 }
314 int index = matchingRegion(region);
315 if (index >= 0) {
316 NavigationRegion mergedRegion = mergedRegions[index];
317 List<int> mergedTargets = mergedRegion.targets;
318 bool added = false;
319 for (int target in region.targets) {
320 if (!mergedTargets.contains(target)) {
321 if (added) {
322 mergedTargets.add(target);
323 } else {
324 //
325 // This is potentially inefficient. If a merged region matches
326 // regions from multiple plugins it will be copied multiple
327 // times. The likelihood seems small enough to not warrant
328 // optimizing this further.
329 //
330 mergedTargets = new List<int>.from(mergedTargets);
331 mergedTargets.add(target);
332 mergedRegion = new NavigationRegion(
333 mergedRegion.offset, mergedRegion.length, mergedTargets);
334 mergedRegions[index] = mergedRegion;
335 added = true;
336 }
337 }
338 }
339 if (added) {
340 mergedTargets.sort();
341 }
342 } else if (isNonOverlapping(region)) {
343 mergedRegions.add(region);
344 }
345 }
346 }
347 return new AnalysisNavigationParams(
348 file, mergedRegions, mergedTargets, mergedFiles);
349 }
350
351 /**
352 * Return a list of occurrences composed by merging the lists of occurrences
353 * in the [partialResultList].
354 *
355 * The resulting list of occurrences will contain exactly one occurrences for
356 * every element for which there is at least one occurrences. If two or more
357 * plugins contribute an occurrences for the same element, the resulting
358 * occurrences for that element will include all of the locations from all of
359 * the plugins without duplications.
360 */
361 List<Occurrences> mergeOccurrences(
362 List<List<Occurrences>> partialResultList) {
363 int count = partialResultList.length;
364 if (count == 0) {
365 return <Occurrences>[];
366 } else if (count == 1) {
367 return partialResultList[0];
368 }
369 Map<Element, Set<int>> elementMap = <Element, Set<int>>{};
370 for (List<Occurrences> partialResults in partialResultList) {
371 for (Occurrences occurances in partialResults) {
372 Element element = occurances.element;
373 Set<int> offsets =
374 elementMap.putIfAbsent(element, () => new HashSet<int>());
375 offsets.addAll(occurances.offsets);
376 }
377 }
378 List<Occurrences> mergedOccurrences = <Occurrences>[];
379 elementMap.forEach((Element element, Set<int> offsets) {
380 List<int> sortedOffsets = offsets.toList();
381 sortedOffsets.sort();
382 mergedOccurrences
383 .add(new Occurrences(element, sortedOffsets, element.name.length));
384 });
385 return mergedOccurrences;
386 }
387
388 /**
389 * Return a list of outlines composed by merging the lists of outlines in the
390 * [partialResultList].
391 *
392 * The resulting list of outlines will contain ...
393 *
394 * Throw an exception if any of the outlines are associated with an element
395 * that does not have a location.
396 *
397 * Throw an exception if any outline has children that are also children of
398 * another outline. No exception is thrown if a plugin contributes a top-level
399 * outline that is a child of an outline contributed by a different plugin.
400 */
401 List<Outline> mergeOutline(List<List<Outline>> partialResultList) {
402 /**
403 * Return a key encoding the unique attributes of the given [element].
404 */
405 String computeKey(Element element) {
406 Location location = element.location;
407 if (location == null) {
408 throw new StateError(
409 'Elements in an outline are expected to have a location');
410 }
411 StringBuffer buffer = new StringBuffer();
412 buffer.write(location.offset);
413 buffer.write(';');
414 buffer.write(element.kind.name);
415 return buffer.toString();
416 }
417
418 int count = partialResultList.length;
419 if (count == 0) {
420 return <Outline>[];
421 } else if (count == 1) {
422 return partialResultList[0];
423 }
424 List<Outline> mergedOutlines = new List<Outline>.from(partialResultList[0]);
425 Map<String, Outline> outlineMap = <String, Outline>{};
426 Map<Outline, Outline> copyMap = <Outline, Outline>{};
427
428 /**
429 * Add the given [outline] and all of its children to the [outlineMap].
430 */
431 void addToMap(Outline outline) {
432 String key = computeKey(outline.element);
433 if (outlineMap.containsKey(key)) {
434 // TODO(brianwilkerson) Decide how to handle this more gracefully.
435 throw new StateError('Inconsistent outlines');
436 }
437 outlineMap[key] = outline;
438 outline.children?.forEach(addToMap);
439 }
440
441 /**
442 * Merge the children of the [newOutline] into the list of children of the
443 * [mergedOutline].
444 */
445 void mergeChildren(Outline mergedOutline, Outline newOutline) {
446 for (Outline newChild in newOutline.children) {
447 Outline mergedChild = outlineMap[computeKey(newChild.element)];
448 if (mergedChild == null) {
449 // The [newChild] isn't in the existing list.
450 Outline copiedOutline = copyMap.putIfAbsent(
451 mergedOutline,
452 () => new Outline(mergedOutline.element, mergedOutline.offset,
453 mergedOutline.length,
454 children: new List<Outline>.from(mergedOutline.children)));
455 copiedOutline.children.add(newChild);
456 addToMap(newChild);
457 } else {
458 mergeChildren(mergedChild, newChild);
459 }
460 }
461 }
462
463 mergedOutlines.forEach(addToMap);
464 for (int i = 1; i < count; i++) {
465 for (Outline outline in partialResultList[i]) {
466 Outline mergedOutline = outlineMap[computeKey(outline.element)];
467 if (mergedOutline == null) {
468 // The [outline] does not correspond to any previously merged outline.
469 mergedOutlines.add(outline);
470 addToMap(outline);
471 } else {
472 // The [outline] corresponds to a previously merged outline, so we
473 // just need to add its children to the merged outline's children.
474 mergeChildren(mergedOutline, outline);
475 }
476 }
477 }
478
479 /**
480 * Perform a depth first traversal of the outline structure rooted at the
481 * given [outline] item, re-building each item if any of its children have
482 * been updated by the merge process.
483 */
484 Outline traverse(Outline outline) {
485 Outline copiedOutline = copyMap[outline];
486 bool isCopied = copiedOutline != null;
487 copiedOutline ??= outline;
488 List<Outline> currentChildren = copiedOutline.children;
489 if (currentChildren.isEmpty) {
490 return outline;
491 }
492 Iterable<Outline> updatedChildren =
493 currentChildren.map((Outline child) => traverse(child));
494 if (currentChildren != updatedChildren) {
495 if (!isCopied) {
496 return new Outline(
497 copiedOutline.element, copiedOutline.offset, copiedOutline.length,
498 children: updatedChildren.toList());
499 }
500 copiedOutline.children = updatedChildren.toList();
501 return copiedOutline;
502 }
503 return outline;
504 }
505
506 for (int i = 0; i < mergedOutlines.length; i++) {
507 mergedOutlines[i] = traverse(mergedOutlines[i]);
508 }
509 return mergedOutlines;
510 }
511
512 /**
513 * Return a refactoring feedback composed by merging the refactoring feedbacks
514 * in the [partialResultList].
515 *
516 * The content of the resulting feedback depends on the kind of feedbacks
517 * being merged.
518 *
519 * Throw an exception if the refactoring feedbacks are of an unhandled type.
520 *
521 * The feedbacks in the [partialResultList] are expected to all be of the same
522 * type. If that expectation is violated, and exception might be thrown.
523 */
524 RefactoringFeedback mergeRefactoringFeedbacks(
525 List<RefactoringFeedback> feedbacks) {
526 int count = feedbacks.length;
527 if (count == 0) {
528 return null;
529 } else if (count == 1) {
530 return feedbacks[0];
531 }
532 RefactoringFeedback first = feedbacks[0];
533 if (first is ConvertGetterToMethodFeedback) {
534 // The feedbacks are empty, so there's nothing to merge.
535 return first;
536 } else if (first is ConvertMethodToGetterFeedback) {
537 // The feedbacks are empty, so there's nothing to merge.
538 return first;
539 } else if (first is ExtractLocalVariableFeedback) {
540 List<int> coveringExpressionOffsets =
541 first.coveringExpressionOffsets == null
542 ? <int>[]
543 : new List<int>.from(first.coveringExpressionOffsets);
544 List<int> coveringExpressionLengths =
545 first.coveringExpressionLengths == null
546 ? <int>[]
547 : new List<int>.from(first.coveringExpressionLengths);
548 List<String> names = new List<String>.from(first.names);
549 List<int> offsets = new List<int>.from(first.offsets);
550 List<int> lengths = new List<int>.from(first.lengths);
551 for (int i = 1; i < count; i++) {
552 ExtractLocalVariableFeedback feedback = feedbacks[i];
553 // TODO(brianwilkerson) This doesn't ensure that the covering data is in
554 // the right order and consistent.
555 if (feedback.coveringExpressionOffsets != null) {
556 coveringExpressionOffsets.addAll(feedback.coveringExpressionOffsets);
557 }
558 if (feedback.coveringExpressionLengths != null) {
559 coveringExpressionLengths.addAll(feedback.coveringExpressionLengths);
560 }
561 for (String name in feedback.names) {
562 if (!names.contains(name)) {
563 names.add(name);
564 }
565 }
566 offsets.addAll(feedback.offsets);
567 lengths.addAll(feedback.lengths);
568 }
569 return new ExtractLocalVariableFeedback(names.toList(), offsets, lengths,
570 coveringExpressionOffsets: (coveringExpressionOffsets.isEmpty
571 ? null
572 : coveringExpressionOffsets),
573 coveringExpressionLengths: (coveringExpressionLengths.isEmpty
574 ? null
575 : coveringExpressionLengths));
576 } else if (first is ExtractMethodFeedback) {
577 int offset = first.offset;
578 int length = first.length;
579 String returnType = first.returnType;
580 List<String> names = new List<String>.from(first.names);
581 bool canCreateGetter = first.canCreateGetter;
582 List<RefactoringMethodParameter> parameters = first.parameters;
583 List<int> offsets = new List<int>.from(first.offsets);
584 List<int> lengths = new List<int>.from(first.lengths);
585 for (int i = 1; i < count; i++) {
586 ExtractMethodFeedback feedback = feedbacks[i];
587 if (returnType.isEmpty) {
588 returnType = feedback.returnType;
589 }
590 for (String name in feedback.names) {
591 if (!names.contains(name)) {
592 names.add(name);
593 }
594 }
595 canCreateGetter = canCreateGetter && feedback.canCreateGetter;
596 // TODO(brianwilkerson) This doesn't allow plugins to add parameters.
597 // TODO(brianwilkerson) This doesn't check for duplicate offsets.
598 offsets.addAll(feedback.offsets);
599 lengths.addAll(feedback.lengths);
600 }
601 return new ExtractMethodFeedback(offset, length, returnType,
602 names.toList(), canCreateGetter, parameters, offsets, lengths);
603 } else if (first is InlineLocalVariableFeedback) {
604 int occurrences = first.occurrences;
605 for (int i = 1; i < count; i++) {
606 occurrences +=
607 (feedbacks[i] as InlineLocalVariableFeedback).occurrences;
608 }
609 return new InlineLocalVariableFeedback(first.name, occurrences);
610 } else if (first is InlineMethodFeedback) {
611 // There is nothing in the feedback that can reasonably be extended or
612 // modified by other plugins.
613 return first;
614 } else if (first is MoveFileFeedback) {
615 // The feedbacks are empty, so there's nothing to merge.
616 return first;
617 } else if (first is RenameFeedback) {
618 // There is nothing in the feedback that can reasonably be extended or
619 // modified by other plugins.
620 return first;
621 }
622 throw new StateError(
623 'Unsupported class of refactoring feedback: ${first.runtimeType}');
624 }
625
626 /**
627 * Return a list of refactoring kinds composed by merging the lists of
628 * refactoring kinds in the [partialResultList].
629 *
630 * The resulting list will contain all of the refactoring kinds from all of
631 * the plugins, but will not contain duplicate elements.
632 */
633 List<RefactoringKind> mergeRefactoringKinds(
634 List<List<RefactoringKind>> partialResultList) {
635 int count = partialResultList.length;
636 if (count == 0) {
637 return <RefactoringKind>[];
638 } else if (count == 1) {
639 return partialResultList[0];
640 }
641 Set<RefactoringKind> mergedKinds = new HashSet<RefactoringKind>();
642 for (List<RefactoringKind> partialResults in partialResultList) {
643 mergedKinds.addAll(partialResults);
644 }
645 return mergedKinds.toList();
646 }
647
648 /**
649 * Return the result for a getRefactorings request composed by merging the
650 * results in the [partialResultList].
651 *
652 * The returned result will contain the concatenation of the initial, options,
653 * and final problems. If two or more plugins produce the same problem, then
654 * the resulting list of problems will contain duplications.
655 *
656 * The returned result will contain a merged list of refactoring feedbacks (as
657 * defined by [mergeRefactoringFeedbacks]) and a merged list of source changes
658 * (as defined by [mergeChanges]).
659 *
660 * The returned result will contain the concatenation of the potential edits.
661 * If two or more plugins produce the same potential edit, then the resulting
662 * list of potential edits will contain duplications.
663 */
664 EditGetRefactoringResult mergeRefactorings(
665 List<EditGetRefactoringResult> partialResultList) {
666 /**
667 * Return the result of merging the given list of source [changes] into a
668 * single source change.
669 *
670 * The resulting change will have the first non-null message and the first
671 * non-null selection. The linked edit groups will be a concatenation of all
672 * of the individual linked edit groups because there's no way to determine
673 * when two such groups should be merged. The resulting list of edits will
674 * be merged at the level of the file being edited, but will be a
675 * concatenation of the individual edits within each file, even if multiple
676 * plugins contribute duplicate or conflicting edits.
677 */
678 SourceChange mergeChanges(List<SourceChange> changes) {
679 int count = changes.length;
680 if (count == 0) {
681 return null;
682 } else if (count == 1) {
683 return changes[0];
684 }
685 SourceChange first = changes[0];
686 String message = first.message;
687 Map<String, SourceFileEdit> editMap = <String, SourceFileEdit>{};
688 for (SourceFileEdit edit in first.edits) {
689 editMap[edit.file] = edit;
690 }
691 List<LinkedEditGroup> linkedEditGroups =
692 new List<LinkedEditGroup>.from(first.linkedEditGroups);
693 Position selection = first.selection;
694 for (int i = 1; i < count; i++) {
695 SourceChange change = changes[i];
696 for (SourceFileEdit edit in change.edits) {
697 SourceFileEdit mergedEdit = editMap[edit.file];
698 if (mergedEdit == null) {
699 editMap[edit.file] = edit;
700 } else {
701 // This doesn't detect if multiple plugins contribute the same (or
702 // conflicting) edits.
703 List<SourceEdit> edits =
704 new List<SourceEdit>.from(mergedEdit.edits);
705 edits.addAll(edit.edits);
706 editMap[edit.file] = new SourceFileEdit(
707 mergedEdit.file, mergedEdit.fileStamp,
708 edits: edits);
709 }
710 }
711 linkedEditGroups.addAll(change.linkedEditGroups);
712 message ??= change.message;
713 selection ??= change.selection;
714 }
715 return new SourceChange(message,
716 edits: editMap.values.toList(),
717 linkedEditGroups: linkedEditGroups,
718 selection: selection);
719 }
720
721 int count = partialResultList.length;
722 if (count == 0) {
723 return null;
724 } else if (count == 1) {
725 return partialResultList[0];
726 }
727 EditGetRefactoringResult result = partialResultList[0];
728 List<RefactoringProblem> initialProblems =
729 new List<RefactoringProblem>.from(result.initialProblems);
730 List<RefactoringProblem> optionsProblems =
731 new List<RefactoringProblem>.from(result.optionsProblems);
732 List<RefactoringProblem> finalProblems =
733 new List<RefactoringProblem>.from(result.finalProblems);
734 List<RefactoringFeedback> feedbacks = <RefactoringFeedback>[];
735 if (result.feedback != null) {
736 feedbacks.add(result.feedback);
737 }
738 List<SourceChange> changes = <SourceChange>[];
739 if (result.change != null) {
740 changes.add(result.change);
741 }
742 List<String> potentialEdits = new List<String>.from(result.potentialEdits);
743 for (int i = 1; i < count; i++) {
744 EditGetRefactoringResult result = partialResultList[1];
745 initialProblems.addAll(result.initialProblems);
746 optionsProblems.addAll(result.optionsProblems);
747 finalProblems.addAll(result.finalProblems);
748 if (result.feedback != null) {
749 feedbacks.add(result.feedback);
750 }
751 if (result.change != null) {
752 changes.add(result.change);
753 }
754 potentialEdits.addAll(result.potentialEdits);
755 }
756 return new EditGetRefactoringResult(
757 initialProblems, optionsProblems, finalProblems,
758 feedback: mergeRefactoringFeedbacks(feedbacks),
759 change: mergeChanges(changes),
760 potentialEdits: potentialEdits);
761 }
762
763 /**
764 * Return a list of source changes composed by merging the lists of source
765 * changes in the [partialResultList].
766 *
767 * The resulting list will contain all of the source changes from all of the
768 * plugins. If two or more plugins contribute the same source change the
769 * resulting list will contain duplications.
770 */
771 List<SourceChange> mergeSourceChanges(
772 List<List<SourceChange>> partialResultList) {
773 int count = partialResultList.length;
774 if (count == 0) {
775 return <SourceChange>[];
776 } else if (count == 1) {
777 return partialResultList[0];
778 }
779 List<SourceChange> mergedChanges = <SourceChange>[];
780 for (List<SourceChange> partialResults in partialResultList) {
781 mergedChanges.addAll(partialResults);
782 }
783 return mergedChanges;
784 }
785
786 /**
787 * Return `true` if a region extending from [leftStart] (inclusive) to
788 * [leftEnd] (exclusive) overlaps a region extending from [rightStart]
789 * (inclusive) to [rightEnd] (exclusive). If [allowNesting] is `true`, then
790 * the regions are allowed to overlap as long as one region is completely
791 * nested within the other region.
792 */
793 @visibleForTesting
794 bool overlaps(int leftStart, int leftEnd, int rightStart, int rightEnd,
795 {bool allowNesting: false}) {
796 if (leftEnd < rightStart || leftStart > rightEnd) {
797 return false;
798 }
799 if (!allowNesting) {
800 return true;
801 }
802 return !((leftStart <= rightStart && rightEnd <= leftEnd) ||
803 (rightStart <= leftStart && leftEnd <= rightEnd));
804 }
805 }
OLDNEW
« no previous file with comments | « no previous file | pkg/analysis_server/test/src/plugin/result_merger_test.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698