| // 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) { | |
| var buffer = 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(); | |
| } | |
| var count = partialResultList.length; | |
| if (count == 0) { | |
| return <plugin.AnalysisErrorFixes>[]; | |
| } else if (count == 1) { | |
| return partialResultList[0]; | |
| } | |
| var fixesMap = <String, plugin.AnalysisErrorFixes>{}; | |
| for (var fix in partialResultList[0]) { | |
| fixesMap[computeKey(fix.error)] = fix; | |
| } | |
| for (var i = 1; i < count; i++) { | |
| for (var fix in partialResultList[i]) { | |
| var key = computeKey(fix.error); | |
| var 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. | |
| var mergedChanges = mergedFix.fixes.toList(); | |
| mergedChanges.addAll(fix.fixes); | |
| var copiedFix = plugin.AnalysisErrorFixes( | |
| mergedFix.error, | |
| fixes: mergedChanges, | |
| ); | |
| fixesMap[key] = copiedFix; | |
| } | |
| } | |
| } | |
| var mergedFixes = fixesMap.values.toList(); | |
| for (var 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. | |
| var count = partialResultList.length; | |
| if (count == 0) { | |
| return <AnalysisError>[]; | |
| } else if (count == 1) { | |
| return partialResultList[0]; | |
| } | |
| var mergedErrors = <AnalysisError>[]; | |
| for (var 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, | |
| ) { | |
| var count = partialResultList.length; | |
| if (count == 0) { | |
| return <CompletionSuggestion>[]; | |
| } else if (count == 1) { | |
| return partialResultList[0]; | |
| } | |
| var mergedSuggestions = <CompletionSuggestion>[]; | |
| for (var 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, | |
| ) { | |
| var count = partialResultList.length; | |
| if (count == 0) { | |
| return <FoldingRegion>[]; | |
| } else if (count == 1) { | |
| return partialResultList[0]; | |
| } | |
| var 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) { | |
| var newStart = newRegion.offset; | |
| var newEnd = newStart + newRegion.length; | |
| for (var existingRegion in mergedRegions) { | |
| var existingStart = existingRegion.offset; | |
| var existingEnd = existingStart + existingRegion.length; | |
| if (overlaps( | |
| newStart, | |
| newEnd, | |
| existingStart, | |
| existingEnd, | |
| allowNesting: true, | |
| )) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| for (var i = 1; i < count; i++) { | |
| var partialResults = partialResultList[i]; | |
| for (var 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, | |
| ) { | |
| var count = partialResultList.length; | |
| if (count == 0) { | |
| return <HighlightRegion>[]; | |
| } else if (count == 1) { | |
| return partialResultList[0]; | |
| } | |
| var mergedRegions = <HighlightRegion>[]; | |
| for (var partialResults in partialResultList) { | |
| mergedRegions.addAll(partialResults); | |
| } | |
| return mergedRegions; | |
| } | |
| /// 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, | |
| ) { | |
| var count = partialResultList.length; | |
| if (count == 0) { | |
| return null; | |
| } else if (count == 1) { | |
| return partialResultList[0]; | |
| } | |
| var base = partialResultList[0]; | |
| var file = base.file; | |
| var mergedRegions = base.regions.toList(); | |
| var mergedTargets = base.targets.toList(); | |
| var 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) { | |
| var newStart = newRegion.offset; | |
| var newEnd = newStart + newRegion.length; | |
| for (var mergedRegion in mergedRegions) { | |
| var mergedStart = mergedRegion.offset; | |
| var 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(NavigationRegion newRegion) { | |
| var newOffset = newRegion.offset; | |
| var newLength = newRegion.length; | |
| for (var i = 0; i < mergedRegions.length; i++) { | |
| var mergedRegion = mergedRegions[i]; | |
| if (newOffset == mergedRegion.offset && | |
| newLength == mergedRegion.length) { | |
| return i; | |
| } | |
| } | |
| return -1; | |
| } | |
| for (var 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. | |
| var result = partialResultList[i]; | |
| var regions = result.regions; | |
| var targets = result.targets; | |
| var files = result.files; | |
| // | |
| // Merge the file data. | |
| // | |
| var fileMap = <int, int>{}; | |
| for (var j = 0; j < files.length; j++) { | |
| var file = files[j]; | |
| var index = mergedFiles.indexOf(file); | |
| if (index < 0) { | |
| index = mergedFiles.length; | |
| mergedFiles.add(file); | |
| } | |
| fileMap[j] = index; | |
| } | |
| // | |
| // Merge the target data. | |
| // | |
| var targetMap = <int, int>{}; | |
| for (var j = 0; j < targets.length; j++) { | |
| var target = targets[j]; | |
| var newIndex = fileMap[target.fileIndex]; | |
| if (newIndex != null && target.fileIndex != newIndex) { | |
| target = NavigationTarget( | |
| target.kind, | |
| newIndex, | |
| target.offset, | |
| target.length, | |
| target.startLine, | |
| target.startColumn, | |
| codeOffset: target.codeOffset, | |
| codeLength: target.codeLength, | |
| ); | |
| } | |
| var index = mergedTargets.indexOf(target); | |
| if (index < 0) { | |
| index = mergedTargets.length; | |
| mergedTargets.add(target); | |
| } | |
| targetMap[j] = index; | |
| } | |
| // | |
| // Merge the region data. | |
| // | |
| for (var j = 0; j < regions.length; j++) { | |
| var region = regions[j]; | |
| var newTargets = region.targets | |
| .map((int oldTarget) => targetMap[oldTarget]) | |
| .toList() | |
| .cast<int>(); | |
| if (region.targets != newTargets) { | |
| region = NavigationRegion(region.offset, region.length, newTargets); | |
| } | |
| var index = matchingRegion(region); | |
| if (index >= 0) { | |
| var mergedRegion = mergedRegions[index]; | |
| var mergedTargets = mergedRegion.targets; | |
| var added = false; | |
| for (var 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 = NavigationRegion( | |
| mergedRegion.offset, | |
| mergedRegion.length, | |
| mergedTargets, | |
| ); | |
| mergedRegions[index] = mergedRegion; | |
| added = true; | |
| } | |
| } | |
| } | |
| if (added) { | |
| mergedTargets.sort(); | |
| } | |
| } else if (isNonOverlapping(region)) { | |
| mergedRegions.add(region); | |
| } | |
| } | |
| } | |
| return 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, | |
| ) { | |
| var count = partialResultList.length; | |
| if (count == 0) { | |
| return <Occurrences>[]; | |
| } else if (count == 1) { | |
| return partialResultList[0]; | |
| } | |
| var elementMap = <Element, Set<int>>{}; | |
| for (var partialResults in partialResultList) { | |
| for (var occurrences in partialResults) { | |
| var element = occurrences.element; | |
| var offsets = elementMap.putIfAbsent(element, () => HashSet<int>()); | |
| offsets.addAll(occurrences.offsets); | |
| } | |
| } | |
| var mergedOccurrences = <Occurrences>[]; | |
| elementMap.forEach((Element element, Set<int> offsets) { | |
| var sortedOffsets = offsets.toList(); | |
| sortedOffsets.sort(); | |
| mergedOccurrences.add( | |
| 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) { | |
| var location = element.location; | |
| if (location == null) { | |
| throw StateError( | |
| 'Elements in an outline are expected to have a location', | |
| ); | |
| } | |
| var buffer = StringBuffer(); | |
| buffer.write(location.offset); | |
| buffer.write(';'); | |
| buffer.write(element.kind.name); | |
| return buffer.toString(); | |
| } | |
| var count = partialResultList.length; | |
| if (count == 0) { | |
| return <Outline>[]; | |
| } else if (count == 1) { | |
| return partialResultList[0]; | |
| } | |
| var mergedOutlines = partialResultList[0].toList(); | |
| var outlineMap = <String, Outline>{}; | |
| var copyMap = <Outline, Outline>{}; | |
| /// Add the given [outline] and all of its children to the [outlineMap]. | |
| void addToMap(Outline outline) { | |
| var key = computeKey(outline.element); | |
| if (outlineMap.containsKey(key)) { | |
| // TODO(brianwilkerson): Decide how to handle this more gracefully. | |
| throw 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) { | |
| var newChildren = newOutline.children; | |
| if (newChildren != null) { | |
| for (var newChild in newChildren) { | |
| var mergedChild = outlineMap[computeKey(newChild.element)]; | |
| if (mergedChild == null) { | |
| // The [newChild] isn't in the existing list. | |
| var mergedChildren = _copyList(mergedOutline.children); | |
| mergedChildren.add(newChild); | |
| copyMap.putIfAbsent( | |
| mergedOutline, | |
| () => Outline( | |
| mergedOutline.element, | |
| mergedOutline.offset, | |
| mergedOutline.length, | |
| mergedOutline.codeOffset, | |
| mergedOutline.codeLength, | |
| children: mergedChildren, | |
| ), | |
| ); | |
| addToMap(newChild); | |
| } else { | |
| mergeChildren(mergedChild, newChild); | |
| } | |
| } | |
| } | |
| } | |
| mergedOutlines.forEach(addToMap); | |
| for (var i = 1; i < count; i++) { | |
| for (var outline in partialResultList[i]) { | |
| var 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) { | |
| var copiedOutline = copyMap[outline]; | |
| var isCopied = copiedOutline != null; | |
| copiedOutline ??= outline; | |
| var currentChildren = copiedOutline.children; | |
| if (currentChildren == null || currentChildren.isEmpty) { | |
| return outline; | |
| } | |
| var updatedChildren = currentChildren | |
| .map((Outline child) => traverse(child)) | |
| .toList(); | |
| if (currentChildren != updatedChildren) { | |
| if (!isCopied) { | |
| return Outline( | |
| copiedOutline.element, | |
| copiedOutline.offset, | |
| copiedOutline.length, | |
| copiedOutline.codeOffset, | |
| copiedOutline.codeLength, | |
| children: updatedChildren, | |
| ); | |
| } | |
| copiedOutline.children = updatedChildren; | |
| return copiedOutline; | |
| } | |
| return outline; | |
| } | |
| for (var 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, | |
| ) { | |
| var count = partialResultList.length; | |
| if (count == 0) { | |
| return <plugin.PrioritizedSourceChange>[]; | |
| } else if (count == 1) { | |
| return partialResultList[0]; | |
| } | |
| var mergedChanges = <plugin.PrioritizedSourceChange>[]; | |
| for (var 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 [feedbacks]. | |
| /// | |
| /// 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] are expected to all be of the same type. If that | |
| /// expectation is violated, and exception might be thrown. | |
| RefactoringFeedback? mergeRefactoringFeedbacks( | |
| List<RefactoringFeedback> feedbacks, | |
| ) { | |
| var count = feedbacks.length; | |
| if (count == 0) { | |
| return null; | |
| } else if (count == 1) { | |
| return feedbacks[0]; | |
| } | |
| var 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) { | |
| var coveringExpressionOffsets = _copyList( | |
| first.coveringExpressionOffsets, | |
| ); | |
| var coveringExpressionLengths = _copyList( | |
| first.coveringExpressionLengths, | |
| ); | |
| var names = first.names.toList(); | |
| var offsets = first.offsets.toList(); | |
| var lengths = first.lengths.toList(); | |
| for (var i = 1; i < count; i++) { | |
| var feedback = feedbacks[i] as ExtractLocalVariableFeedback; | |
| // TODO(brianwilkerson): This doesn't ensure that the covering data is in | |
| // the right order and consistent. | |
| var coveringOffsets = feedback.coveringExpressionOffsets; | |
| if (coveringOffsets != null) { | |
| coveringExpressionOffsets.addAll(coveringOffsets); | |
| } | |
| var coveringLengths = feedback.coveringExpressionLengths; | |
| if (coveringLengths != null) { | |
| coveringExpressionLengths.addAll(coveringLengths); | |
| } | |
| for (var name in feedback.names) { | |
| if (!names.contains(name)) { | |
| names.add(name); | |
| } | |
| } | |
| offsets.addAll(feedback.offsets); | |
| lengths.addAll(feedback.lengths); | |
| } | |
| return ExtractLocalVariableFeedback( | |
| names.toList(), | |
| offsets, | |
| lengths, | |
| coveringExpressionOffsets: coveringExpressionOffsets.isEmpty | |
| ? null | |
| : coveringExpressionOffsets, | |
| coveringExpressionLengths: coveringExpressionLengths.isEmpty | |
| ? null | |
| : coveringExpressionLengths, | |
| ); | |
| } else if (first is ExtractMethodFeedback) { | |
| var offset = first.offset; | |
| var length = first.length; | |
| var returnType = first.returnType; | |
| var names = first.names.toList(); | |
| var canCreateGetter = first.canCreateGetter; | |
| var parameters = first.parameters; | |
| var offsets = first.offsets.toList(); | |
| var lengths = first.lengths.toList(); | |
| for (var i = 1; i < count; i++) { | |
| var feedback = feedbacks[i] as ExtractMethodFeedback; | |
| if (returnType.isEmpty) { | |
| returnType = feedback.returnType; | |
| } | |
| for (var 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 ExtractMethodFeedback( | |
| offset, | |
| length, | |
| returnType, | |
| names.toList(), | |
| canCreateGetter, | |
| parameters, | |
| offsets, | |
| lengths, | |
| ); | |
| } else if (first is InlineLocalVariableFeedback) { | |
| var occurrences = first.occurrences; | |
| for (var i = 1; i < count; i++) { | |
| occurrences += | |
| (feedbacks[i] as InlineLocalVariableFeedback).occurrences; | |
| } | |
| return 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 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, | |
| ) { | |
| var count = partialResultList.length; | |
| if (count == 0) { | |
| return <RefactoringKind>[]; | |
| } else if (count == 1) { | |
| return partialResultList[0]; | |
| } | |
| Set<RefactoringKind> mergedKinds = HashSet<RefactoringKind>(); | |
| for (var 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) { | |
| var count = changes.length; | |
| if (count == 0) { | |
| return null; | |
| } else if (count == 1) { | |
| return changes[0]; | |
| } | |
| var first = changes[0]; | |
| var message = first.message; | |
| var editMap = <String, SourceFileEdit>{}; | |
| for (var edit in first.edits) { | |
| editMap[edit.file] = edit; | |
| } | |
| var linkedEditGroups = first.linkedEditGroups.toList(); | |
| var selection = first.selection; | |
| for (var i = 1; i < count; i++) { | |
| var change = changes[i]; | |
| for (var edit in change.edits) { | |
| var 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. | |
| var edits = mergedEdit.edits.toList(); | |
| edits.addAll(edit.edits); | |
| editMap[edit.file] = SourceFileEdit( | |
| mergedEdit.file, | |
| mergedEdit.fileStamp, | |
| edits: edits, | |
| ); | |
| } | |
| } | |
| linkedEditGroups.addAll(change.linkedEditGroups); | |
| selection ??= change.selection; | |
| } | |
| return SourceChange( | |
| message, | |
| edits: editMap.values.toList(), | |
| linkedEditGroups: linkedEditGroups, | |
| selection: selection, | |
| ); | |
| } | |
| var count = partialResultList.length; | |
| if (count == 0) { | |
| return null; | |
| } else if (count == 1) { | |
| return partialResultList[0]; | |
| } | |
| var result = partialResultList[0]; | |
| var initialProblems = result.initialProblems.toList(); | |
| var optionsProblems = result.optionsProblems.toList(); | |
| var finalProblems = result.finalProblems.toList(); | |
| var feedbacks = <RefactoringFeedback>[]; | |
| var feedback = result.feedback; | |
| if (feedback != null) { | |
| feedbacks.add(feedback); | |
| } | |
| var changes = <SourceChange>[]; | |
| var change = result.change; | |
| if (change != null) { | |
| changes.add(change); | |
| } | |
| var potentialEdits = _copyList(result.potentialEdits); | |
| for (var i = 1; i < count; i++) { | |
| var result = partialResultList[1]; | |
| initialProblems.addAll(result.initialProblems); | |
| optionsProblems.addAll(result.optionsProblems); | |
| finalProblems.addAll(result.finalProblems); | |
| var feedback = result.feedback; | |
| if (feedback != null) { | |
| feedbacks.add(feedback); | |
| } | |
| var change = result.change; | |
| if (change != null) { | |
| changes.add(change); | |
| } | |
| var edits = result.potentialEdits; | |
| if (edits != null) { | |
| potentialEdits.addAll(edits); | |
| } | |
| } | |
| return 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, | |
| ) { | |
| var count = partialResultList.length; | |
| if (count == 0) { | |
| return <SourceChange>[]; | |
| } else if (count == 1) { | |
| return partialResultList[0]; | |
| } | |
| var mergedChanges = <SourceChange>[]; | |
| for (var 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)); | |
| } | |
| /// Return a copy of the [list], or an empty list if [list] is `null`. | |
| List<E> _copyList<E>(List<E>? list) => list == null ? <E>[] : list.toList(); | |
| } |