blob: 43d0a5617a73b7353fe7dc956cd1e963dca0b2bf [file] [log] [blame]
// Copyright (c) 2017, 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 'dart:collection';
import 'package:analysis_server/protocol/protocol_generated.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
import 'package:meta/meta.dart';
/**
* An object used to merge partial lists of results that were contributed by
* plugins.
*
* All of the methods in this class assume that the contributions from the
* analysis server are the first partial result in the list of partial results
* to be merged.
*/
class ResultMerger {
/**
* Return a list of fixes composed by merging the lists of fixes in the
* [partialResultList].
*
* The resulting list of fixes will contain exactly one fix for every analysis
* error for which there are fixes. If two or more plugins contribute the same
* fix for a given error, the resulting list will contain duplications.
*/
List<plugin.AnalysisErrorFixes> mergeAnalysisErrorFixes(
List<List<plugin.AnalysisErrorFixes>> partialResultList) {
/**
* Return a key encoding the unique attributes of the given [error].
*/
String computeKey(AnalysisError error) {
StringBuffer buffer = new StringBuffer();
buffer.write(error.location.offset);
buffer.write(';');
buffer.write(error.code);
buffer.write(';');
buffer.write(error.message);
buffer.write(';');
buffer.write(error.correction);
return buffer.toString();
}
int count = partialResultList.length;
if (count == 0) {
return <plugin.AnalysisErrorFixes>[];
} else if (count == 1) {
return partialResultList[0];
}
Map<String, plugin.AnalysisErrorFixes> fixesMap =
<String, plugin.AnalysisErrorFixes>{};
for (plugin.AnalysisErrorFixes fix in partialResultList[0]) {
fixesMap[computeKey(fix.error)] = fix;
}
for (int i = 1; i < count; i++) {
for (plugin.AnalysisErrorFixes fix in partialResultList[i]) {
String key = computeKey(fix.error);
plugin.AnalysisErrorFixes mergedFix = fixesMap[key];
if (mergedFix == null) {
fixesMap[key] = fix;
} else {
// If more than two plugins contribute fixes for the same error, this
// will result in extra copy operations.
List<plugin.PrioritizedSourceChange> mergedChanges =
mergedFix.fixes.toList();
mergedChanges.addAll(fix.fixes);
plugin.AnalysisErrorFixes copiedFix = new plugin.AnalysisErrorFixes(
mergedFix.error,
fixes: mergedChanges);
fixesMap[key] = copiedFix;
}
}
}
List<plugin.AnalysisErrorFixes> mergedFixes = fixesMap.values.toList();
for (plugin.AnalysisErrorFixes fixes in mergedFixes) {
fixes.fixes.sort((first, second) => first.priority - second.priority);
}
return mergedFixes;
}
/**
* Return a list of errors composed by merging the lists of errors in the
* [partialResultList].
*
* The resulting list will contain all of the analysis errors from all of the
* plugins. If two or more plugins contribute the same error the resulting
* list will contain duplications.
*/
List<AnalysisError> mergeAnalysisErrors(
List<List<AnalysisError>> partialResultList) {
// TODO(brianwilkerson) Consider merging duplicate errors (same code,
// location, and messages). If we do that, we should return the logical-or
// of the hasFix fields from the merged errors.
int count = partialResultList.length;
if (count == 0) {
return <AnalysisError>[];
} else if (count == 1) {
return partialResultList[0];
}
List<AnalysisError> mergedErrors = <AnalysisError>[];
for (List<AnalysisError> partialResults in partialResultList) {
mergedErrors.addAll(partialResults);
}
return mergedErrors;
}
/**
* Return a list of suggestions composed by merging the lists of suggestions
* in the [partialResultList].
*
* The resulting list will contain all of the suggestions from all of the
* plugins. If two or more plugins contribute the same suggestion the
* resulting list will contain duplications.
*/
List<CompletionSuggestion> mergeCompletionSuggestions(
List<List<CompletionSuggestion>> partialResultList) {
int count = partialResultList.length;
if (count == 0) {
return <CompletionSuggestion>[];
} else if (count == 1) {
return partialResultList[0];
}
List<CompletionSuggestion> mergedSuggestions = <CompletionSuggestion>[];
for (List<CompletionSuggestion> partialResults in partialResultList) {
mergedSuggestions.addAll(partialResults);
}
return mergedSuggestions;
}
/**
* Return a list of regions composed by merging the lists of regions in the
* [partialResultList].
*
* The resulting list will contain all of the folding regions from all of the
* plugins. If a plugin contributes a folding region that overlaps a region
* from a previous plugin, the overlapping region will be omitted. (For these
* purposes, if either region is fully contained within the other they are not
* considered to be overlapping.)
*/
List<FoldingRegion> mergeFoldingRegions(
List<List<FoldingRegion>> partialResultList) {
int count = partialResultList.length;
if (count == 0) {
return <FoldingRegion>[];
} else if (count == 1) {
return partialResultList[0];
}
List<FoldingRegion> mergedRegions = partialResultList[0].toList();
/**
* Return `true` if the [newRegion] does not overlap any of the regions in
* the collection of [mergedRegions].
*/
bool isNonOverlapping(FoldingRegion newRegion) {
int newStart = newRegion.offset;
int newEnd = newStart + newRegion.length;
for (FoldingRegion existingRegion in mergedRegions) {
int existingStart = existingRegion.offset;
int existingEnd = existingStart + existingRegion.length;
if (overlaps(newStart, newEnd, existingStart, existingEnd,
allowNesting: true)) {
return false;
}
}
return true;
}
for (int i = 1; i < count; i++) {
List<FoldingRegion> partialResults = partialResultList[i];
for (FoldingRegion region in partialResults) {
if (isNonOverlapping(region)) {
mergedRegions.add(region);
}
}
}
return mergedRegions;
}
/**
* Return a list of regions composed by merging the lists of regions in the
* [partialResultList].
*
* The resulting list will contain all of the highlight regions from all of
* the plugins. If two or more plugins contribute the same highlight region
* the resulting list will contain duplications.
*/
List<HighlightRegion> mergeHighlightRegions(
List<List<HighlightRegion>> partialResultList) {
int count = partialResultList.length;
if (count == 0) {
return <HighlightRegion>[];
} else if (count == 1) {
return partialResultList[0];
}
List<HighlightRegion> mergedRegions = <HighlightRegion>[];
for (List<HighlightRegion> partialResults in partialResultList) {
mergedRegions.addAll(partialResults);
}
return mergedRegions;
}
/**
* Return kythe entry result parameters composed by merging the parameters in
* the [partialResultList].
*
* The resulting list will contain all of the kythe entries from all of the
* plugins. If a plugin contributes a kythe entry that is the same as the
* entry from a different plugin, the entry will appear twice in the list.
*/
KytheGetKytheEntriesResult mergeKytheEntries(
List<KytheGetKytheEntriesResult> partialResultList) {
List<KytheEntry> mergedEntries = <KytheEntry>[];
Set<String> mergedFiles = new Set<String>();
for (KytheGetKytheEntriesResult partialResult in partialResultList) {
mergedEntries.addAll(partialResult.entries);
mergedFiles.addAll(partialResult.files);
}
return new KytheGetKytheEntriesResult(mergedEntries, mergedFiles.toList());
}
/**
* Return navigation notification parameters composed by merging the
* parameters in the [partialResultList].
*
* The resulting list will contain all of the navigation regions from all of
* the plugins. If a plugin contributes a navigation region that overlaps a
* region from a previous plugin, the overlapping region will be omitted. (For
* these purposes, nested regions are considered to be overlapping.)
*/
AnalysisNavigationParams mergeNavigation(
List<AnalysisNavigationParams> partialResultList) {
int count = partialResultList.length;
if (count == 0) {
return null;
} else if (count == 1) {
return partialResultList[0];
}
AnalysisNavigationParams base = partialResultList[0];
String file = base.file;
List<NavigationRegion> mergedRegions = base.regions.toList();
List<NavigationTarget> mergedTargets = base.targets.toList();
List<String> mergedFiles = base.files.toList();
/**
* Return `true` if the [newRegion] does not overlap any of the regions in
* the collection of [mergedRegions].
*/
bool isNonOverlapping(NavigationRegion newRegion) {
int newStart = newRegion.offset;
int newEnd = newStart + newRegion.length;
for (NavigationRegion mergedRegion in mergedRegions) {
int mergedStart = mergedRegion.offset;
int mergedEnd = mergedStart + mergedRegion.length;
if (overlaps(newStart, newEnd, mergedStart, mergedEnd)) {
return false;
}
}
return true;
}
/**
* Return the index of the region in the collection of [mergedRegions] that
* covers exactly the same region as the [newRegion], or `-1` if there is no
* such region.
*/
int matchingRegion(newRegion) {
int newOffset = newRegion.offset;
int newLength = newRegion.length;
for (int i = 0; i < mergedRegions.length; i++) {
NavigationRegion mergedRegion = mergedRegions[i];
if (newOffset == mergedRegion.offset &&
newLength == mergedRegion.length) {
return i;
}
}
return -1;
}
for (int i = 1; i < count; i++) {
// For now we take the optimistic approach of assuming that most or all of
// the regions will not overlap and that we therefore don't need to remove
// any unreferenced files or targets from the lists. If that isn't true
// then this could result in server sending more data to the client than
// is necessary.
AnalysisNavigationParams result = partialResultList[i];
List<NavigationRegion> regions = result.regions;
List<NavigationTarget> targets = result.targets;
List<String> files = result.files;
//
// Merge the file data.
//
Map<int, int> fileMap = <int, int>{};
for (int j = 0; j < files.length; j++) {
String file = files[j];
int index = mergedFiles.indexOf(file);
if (index < 0) {
index = mergedFiles.length;
mergedFiles.add(file);
}
fileMap[j] = index;
}
//
// Merge the target data.
//
Map<int, int> targetMap = <int, int>{};
for (int j = 0; j < targets.length; j++) {
NavigationTarget target = targets[j];
int newIndex = fileMap[target.fileIndex];
if (target.fileIndex != newIndex) {
target = new NavigationTarget(target.kind, newIndex, target.offset,
target.length, target.startLine, target.startColumn);
}
int index = mergedTargets.indexOf(target);
if (index < 0) {
index = mergedTargets.length;
mergedTargets.add(target);
}
targetMap[j] = index;
}
//
// Merge the region data.
//
for (int j = 0; j < regions.length; j++) {
NavigationRegion region = regions[j];
List<int> newTargets = region.targets
.map((int oldTarget) => targetMap[oldTarget])
.toList();
if (region.targets != newTargets) {
region =
new NavigationRegion(region.offset, region.length, newTargets);
}
int index = matchingRegion(region);
if (index >= 0) {
NavigationRegion mergedRegion = mergedRegions[index];
List<int> mergedTargets = mergedRegion.targets;
bool added = false;
for (int target in region.targets) {
if (!mergedTargets.contains(target)) {
if (added) {
mergedTargets.add(target);
} else {
//
// This is potentially inefficient. If a merged region matches
// regions from multiple plugins it will be copied multiple
// times. The likelihood seems small enough to not warrant
// optimizing this further.
//
mergedTargets = mergedTargets.toList();
mergedTargets.add(target);
mergedRegion = new NavigationRegion(
mergedRegion.offset, mergedRegion.length, mergedTargets);
mergedRegions[index] = mergedRegion;
added = true;
}
}
}
if (added) {
mergedTargets.sort();
}
} else if (isNonOverlapping(region)) {
mergedRegions.add(region);
}
}
}
return new AnalysisNavigationParams(
file, mergedRegions, mergedTargets, mergedFiles);
}
/**
* Return a list of occurrences composed by merging the lists of occurrences
* in the [partialResultList].
*
* The resulting list of occurrences will contain exactly one occurrences for
* every element for which there is at least one occurrences. If two or more
* plugins contribute an occurrences for the same element, the resulting
* occurrences for that element will include all of the locations from all of
* the plugins without duplications.
*/
List<Occurrences> mergeOccurrences(
List<List<Occurrences>> partialResultList) {
int count = partialResultList.length;
if (count == 0) {
return <Occurrences>[];
} else if (count == 1) {
return partialResultList[0];
}
Map<Element, Set<int>> elementMap = <Element, Set<int>>{};
for (List<Occurrences> partialResults in partialResultList) {
for (Occurrences occurances in partialResults) {
Element element = occurances.element;
Set<int> offsets =
elementMap.putIfAbsent(element, () => new HashSet<int>());
offsets.addAll(occurances.offsets);
}
}
List<Occurrences> mergedOccurrences = <Occurrences>[];
elementMap.forEach((Element element, Set<int> offsets) {
List<int> sortedOffsets = offsets.toList();
sortedOffsets.sort();
mergedOccurrences
.add(new Occurrences(element, sortedOffsets, element.name.length));
});
return mergedOccurrences;
}
/**
* Return a list of outlines composed by merging the lists of outlines in the
* [partialResultList].
*
* The resulting list of outlines will contain ...
*
* Throw an exception if any of the outlines are associated with an element
* that does not have a location.
*
* Throw an exception if any outline has children that are also children of
* another outline. No exception is thrown if a plugin contributes a top-level
* outline that is a child of an outline contributed by a different plugin.
*/
List<Outline> mergeOutline(List<List<Outline>> partialResultList) {
/**
* Return a key encoding the unique attributes of the given [element].
*/
String computeKey(Element element) {
Location location = element.location;
if (location == null) {
throw new StateError(
'Elements in an outline are expected to have a location');
}
StringBuffer buffer = new StringBuffer();
buffer.write(location.offset);
buffer.write(';');
buffer.write(element.kind.name);
return buffer.toString();
}
int count = partialResultList.length;
if (count == 0) {
return <Outline>[];
} else if (count == 1) {
return partialResultList[0];
}
List<Outline> mergedOutlines = partialResultList[0].toList();
Map<String, Outline> outlineMap = <String, Outline>{};
Map<Outline, Outline> copyMap = <Outline, Outline>{};
/**
* Add the given [outline] and all of its children to the [outlineMap].
*/
void addToMap(Outline outline) {
String key = computeKey(outline.element);
if (outlineMap.containsKey(key)) {
// TODO(brianwilkerson) Decide how to handle this more gracefully.
throw new StateError('Inconsistent outlines');
}
outlineMap[key] = outline;
outline.children?.forEach(addToMap);
}
/**
* Merge the children of the [newOutline] into the list of children of the
* [mergedOutline].
*/
void mergeChildren(Outline mergedOutline, Outline newOutline) {
for (Outline newChild in newOutline.children) {
Outline mergedChild = outlineMap[computeKey(newChild.element)];
if (mergedChild == null) {
// The [newChild] isn't in the existing list.
Outline copiedOutline = copyMap.putIfAbsent(
mergedOutline,
() => new Outline(
mergedOutline.element,
mergedOutline.offset,
mergedOutline.length,
mergedOutline.codeOffset,
mergedOutline.codeLength,
children: mergedOutline.children.toList()));
copiedOutline.children.add(newChild);
addToMap(newChild);
} else {
mergeChildren(mergedChild, newChild);
}
}
}
mergedOutlines.forEach(addToMap);
for (int i = 1; i < count; i++) {
for (Outline outline in partialResultList[i]) {
Outline mergedOutline = outlineMap[computeKey(outline.element)];
if (mergedOutline == null) {
// The [outline] does not correspond to any previously merged outline.
mergedOutlines.add(outline);
addToMap(outline);
} else {
// The [outline] corresponds to a previously merged outline, so we
// just need to add its children to the merged outline's children.
mergeChildren(mergedOutline, outline);
}
}
}
/**
* Perform a depth first traversal of the outline structure rooted at the
* given [outline] item, re-building each item if any of its children have
* been updated by the merge process.
*/
Outline traverse(Outline outline) {
Outline copiedOutline = copyMap[outline];
bool isCopied = copiedOutline != null;
copiedOutline ??= outline;
List<Outline> currentChildren = copiedOutline.children;
if (currentChildren == null || currentChildren.isEmpty) {
return outline;
}
List<Outline> updatedChildren =
currentChildren.map((Outline child) => traverse(child)).toList();
if (currentChildren != updatedChildren) {
if (!isCopied) {
return new Outline(
copiedOutline.element,
copiedOutline.offset,
copiedOutline.length,
copiedOutline.codeOffset,
copiedOutline.codeLength,
children: updatedChildren);
}
copiedOutline.children = updatedChildren;
return copiedOutline;
}
return outline;
}
for (int i = 0; i < mergedOutlines.length; i++) {
mergedOutlines[i] = traverse(mergedOutlines[i]);
}
return mergedOutlines;
}
/**
* Return a list of source changes composed by merging the lists of source
* changes in the [partialResultList].
*
* The resulting list will contain all of the source changes from all of the
* plugins. If two or more plugins contribute the same source change the
* resulting list will contain duplications.
*/
List<plugin.PrioritizedSourceChange> mergePrioritizedSourceChanges(
List<List<plugin.PrioritizedSourceChange>> partialResultList) {
int count = partialResultList.length;
if (count == 0) {
return <plugin.PrioritizedSourceChange>[];
} else if (count == 1) {
return partialResultList[0];
}
List<plugin.PrioritizedSourceChange> mergedChanges =
<plugin.PrioritizedSourceChange>[];
for (List<plugin.PrioritizedSourceChange> partialResults
in partialResultList) {
mergedChanges.addAll(partialResults);
}
mergedChanges.sort((first, second) => first.priority - second.priority);
return mergedChanges;
}
/**
* Return a refactoring feedback composed by merging the refactoring feedbacks
* in the [partialResultList].
*
* The content of the resulting feedback depends on the kind of feedbacks
* being merged.
*
* Throw an exception if the refactoring feedbacks are of an unhandled type.
*
* The feedbacks in the [partialResultList] are expected to all be of the same
* type. If that expectation is violated, and exception might be thrown.
*/
RefactoringFeedback mergeRefactoringFeedbacks(
List<RefactoringFeedback> feedbacks) {
int count = feedbacks.length;
if (count == 0) {
return null;
} else if (count == 1) {
return feedbacks[0];
}
RefactoringFeedback first = feedbacks[0];
if (first is ConvertGetterToMethodFeedback) {
// The feedbacks are empty, so there's nothing to merge.
return first;
} else if (first is ConvertMethodToGetterFeedback) {
// The feedbacks are empty, so there's nothing to merge.
return first;
} else if (first is ExtractLocalVariableFeedback) {
List<int> coveringExpressionOffsets =
first.coveringExpressionOffsets == null
? <int>[]
: first.coveringExpressionOffsets.toList();
List<int> coveringExpressionLengths =
first.coveringExpressionLengths == null
? <int>[]
: first.coveringExpressionLengths.toList();
List<String> names = first.names.toList();
List<int> offsets = first.offsets.toList();
List<int> lengths = first.lengths.toList();
for (int i = 1; i < count; i++) {
ExtractLocalVariableFeedback feedback = feedbacks[i];
// TODO(brianwilkerson) This doesn't ensure that the covering data is in
// the right order and consistent.
if (feedback.coveringExpressionOffsets != null) {
coveringExpressionOffsets.addAll(feedback.coveringExpressionOffsets);
}
if (feedback.coveringExpressionLengths != null) {
coveringExpressionLengths.addAll(feedback.coveringExpressionLengths);
}
for (String name in feedback.names) {
if (!names.contains(name)) {
names.add(name);
}
}
offsets.addAll(feedback.offsets);
lengths.addAll(feedback.lengths);
}
return new ExtractLocalVariableFeedback(names.toList(), offsets, lengths,
coveringExpressionOffsets: (coveringExpressionOffsets.isEmpty
? null
: coveringExpressionOffsets),
coveringExpressionLengths: (coveringExpressionLengths.isEmpty
? null
: coveringExpressionLengths));
} else if (first is ExtractMethodFeedback) {
int offset = first.offset;
int length = first.length;
String returnType = first.returnType;
List<String> names = first.names.toList();
bool canCreateGetter = first.canCreateGetter;
List<RefactoringMethodParameter> parameters = first.parameters;
List<int> offsets = first.offsets.toList();
List<int> lengths = first.lengths.toList();
for (int i = 1; i < count; i++) {
ExtractMethodFeedback feedback = feedbacks[i];
if (returnType.isEmpty) {
returnType = feedback.returnType;
}
for (String name in feedback.names) {
if (!names.contains(name)) {
names.add(name);
}
}
canCreateGetter = canCreateGetter && feedback.canCreateGetter;
// TODO(brianwilkerson) This doesn't allow plugins to add parameters.
// TODO(brianwilkerson) This doesn't check for duplicate offsets.
offsets.addAll(feedback.offsets);
lengths.addAll(feedback.lengths);
}
return new ExtractMethodFeedback(offset, length, returnType,
names.toList(), canCreateGetter, parameters, offsets, lengths);
} else if (first is InlineLocalVariableFeedback) {
int occurrences = first.occurrences;
for (int i = 1; i < count; i++) {
occurrences +=
(feedbacks[i] as InlineLocalVariableFeedback).occurrences;
}
return new InlineLocalVariableFeedback(first.name, occurrences);
} else if (first is InlineMethodFeedback) {
// There is nothing in the feedback that can reasonably be extended or
// modified by other plugins.
return first;
} else if (first is MoveFileFeedback) {
// The feedbacks are empty, so there's nothing to merge.
return first;
} else if (first is RenameFeedback) {
// There is nothing in the feedback that can reasonably be extended or
// modified by other plugins.
return first;
}
throw new StateError(
'Unsupported class of refactoring feedback: ${first.runtimeType}');
}
/**
* Return a list of refactoring kinds composed by merging the lists of
* refactoring kinds in the [partialResultList].
*
* The resulting list will contain all of the refactoring kinds from all of
* the plugins, but will not contain duplicate elements.
*/
List<RefactoringKind> mergeRefactoringKinds(
List<List<RefactoringKind>> partialResultList) {
int count = partialResultList.length;
if (count == 0) {
return <RefactoringKind>[];
} else if (count == 1) {
return partialResultList[0];
}
Set<RefactoringKind> mergedKinds = new HashSet<RefactoringKind>();
for (List<RefactoringKind> partialResults in partialResultList) {
mergedKinds.addAll(partialResults);
}
return mergedKinds.toList();
}
/**
* Return the result for a getRefactorings request composed by merging the
* results in the [partialResultList].
*
* The returned result will contain the concatenation of the initial, options,
* and final problems. If two or more plugins produce the same problem, then
* the resulting list of problems will contain duplications.
*
* The returned result will contain a merged list of refactoring feedbacks (as
* defined by [mergeRefactoringFeedbacks]) and a merged list of source changes
* (as defined by [mergeChanges]).
*
* The returned result will contain the concatenation of the potential edits.
* If two or more plugins produce the same potential edit, then the resulting
* list of potential edits will contain duplications.
*/
EditGetRefactoringResult mergeRefactorings(
List<EditGetRefactoringResult> partialResultList) {
/**
* Return the result of merging the given list of source [changes] into a
* single source change.
*
* The resulting change will have the first non-null message and the first
* non-null selection. The linked edit groups will be a concatenation of all
* of the individual linked edit groups because there's no way to determine
* when two such groups should be merged. The resulting list of edits will
* be merged at the level of the file being edited, but will be a
* concatenation of the individual edits within each file, even if multiple
* plugins contribute duplicate or conflicting edits.
*/
SourceChange mergeChanges(List<SourceChange> changes) {
int count = changes.length;
if (count == 0) {
return null;
} else if (count == 1) {
return changes[0];
}
SourceChange first = changes[0];
String message = first.message;
Map<String, SourceFileEdit> editMap = <String, SourceFileEdit>{};
for (SourceFileEdit edit in first.edits) {
editMap[edit.file] = edit;
}
List<LinkedEditGroup> linkedEditGroups = first.linkedEditGroups.toList();
Position selection = first.selection;
for (int i = 1; i < count; i++) {
SourceChange change = changes[i];
for (SourceFileEdit edit in change.edits) {
SourceFileEdit mergedEdit = editMap[edit.file];
if (mergedEdit == null) {
editMap[edit.file] = edit;
} else {
// This doesn't detect if multiple plugins contribute the same (or
// conflicting) edits.
List<SourceEdit> edits = mergedEdit.edits.toList();
edits.addAll(edit.edits);
editMap[edit.file] = new SourceFileEdit(
mergedEdit.file, mergedEdit.fileStamp,
edits: edits);
}
}
linkedEditGroups.addAll(change.linkedEditGroups);
message ??= change.message;
selection ??= change.selection;
}
return new SourceChange(message,
edits: editMap.values.toList(),
linkedEditGroups: linkedEditGroups,
selection: selection);
}
int count = partialResultList.length;
if (count == 0) {
return null;
} else if (count == 1) {
return partialResultList[0];
}
EditGetRefactoringResult result = partialResultList[0];
List<RefactoringProblem> initialProblems = result.initialProblems.toList();
List<RefactoringProblem> optionsProblems = result.optionsProblems.toList();
List<RefactoringProblem> finalProblems = result.finalProblems.toList();
List<RefactoringFeedback> feedbacks = <RefactoringFeedback>[];
if (result.feedback != null) {
feedbacks.add(result.feedback);
}
List<SourceChange> changes = <SourceChange>[];
if (result.change != null) {
changes.add(result.change);
}
List<String> potentialEdits = result.potentialEdits.toList();
for (int i = 1; i < count; i++) {
EditGetRefactoringResult result = partialResultList[1];
initialProblems.addAll(result.initialProblems);
optionsProblems.addAll(result.optionsProblems);
finalProblems.addAll(result.finalProblems);
if (result.feedback != null) {
feedbacks.add(result.feedback);
}
if (result.change != null) {
changes.add(result.change);
}
potentialEdits.addAll(result.potentialEdits);
}
return new EditGetRefactoringResult(
initialProblems, optionsProblems, finalProblems,
feedback: mergeRefactoringFeedbacks(feedbacks),
change: mergeChanges(changes),
potentialEdits: potentialEdits);
}
/**
* Return a list of source changes composed by merging the lists of source
* changes in the [partialResultList].
*
* The resulting list will contain all of the source changes from all of the
* plugins. If two or more plugins contribute the same source change the
* resulting list will contain duplications.
*/
List<SourceChange> mergeSourceChanges(
List<List<SourceChange>> partialResultList) {
int count = partialResultList.length;
if (count == 0) {
return <SourceChange>[];
} else if (count == 1) {
return partialResultList[0];
}
List<SourceChange> mergedChanges = <SourceChange>[];
for (List<SourceChange> partialResults in partialResultList) {
mergedChanges.addAll(partialResults);
}
return mergedChanges;
}
/**
* Return `true` if a region extending from [leftStart] (inclusive) to
* [leftEnd] (exclusive) overlaps a region extending from [rightStart]
* (inclusive) to [rightEnd] (exclusive). If [allowNesting] is `true`, then
* the regions are allowed to overlap as long as one region is completely
* nested within the other region.
*/
@visibleForTesting
bool overlaps(int leftStart, int leftEnd, int rightStart, int rightEnd,
{bool allowNesting: false}) {
if (leftEnd < rightStart || leftStart > rightEnd) {
return false;
}
if (!allowNesting) {
return true;
}
return !((leftStart <= rightStart && rightEnd <= leftEnd) ||
(rightStart <= leftStart && leftEnd <= rightEnd));
}
}