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