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