| // 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, |
| 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, |
| 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)); |
| } |
| } |