Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 import 'dart:async'; | |
| 6 | |
| 5 import 'package:analysis_server/protocol/protocol_generated.dart'; | 7 import 'package:analysis_server/protocol/protocol_generated.dart'; |
| 6 import 'package:analyzer/dart/analysis/results.dart'; | 8 import 'package:analyzer/dart/analysis/results.dart'; |
| 7 import 'package:analyzer/dart/analysis/session.dart'; | 9 import 'package:analyzer/dart/ast/ast.dart'; |
| 10 import 'package:analyzer/dart/ast/ast_factory.dart'; | |
| 11 import 'package:analyzer/dart/ast/token.dart'; | |
| 8 import 'package:analyzer/dart/element/element.dart'; | 12 import 'package:analyzer/dart/element/element.dart'; |
| 9 import 'package:analyzer_plugin/protocol/protocol_common.dart'; | 13 import 'package:analyzer/file_system/file_system.dart'; |
| 14 import 'package:analyzer/src/dart/ast/ast_factory.dart'; | |
| 15 import 'package:analyzer/src/dart/ast/token.dart'; | |
| 16 import 'package:analyzer/src/dart/resolver/scope.dart'; | |
| 17 import 'package:analyzer/src/generated/source.dart'; | |
| 18 import 'package:analyzer_plugin/protocol/protocol_common.dart' hide Element; | |
| 10 import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dar t'; | 19 import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dar t'; |
| 20 import 'package:analyzer_plugin/utilities/range_factory.dart'; | |
| 21 import 'package:front_end/src/base/syntactic_entity.dart'; | |
| 22 import 'package:path/src/context.dart'; | |
| 11 | 23 |
| 12 /** | 24 /** |
| 13 * An object used to compute the edits required to ensure that a list of | 25 * An object used to compute a set of edits to add imports to a given library in |
| 14 * elements is imported into a given library. | 26 * order to make a given set of elements visible. |
| 27 * | |
| 28 * This is used to implement the `edit.importElements` request. | |
| 15 */ | 29 */ |
| 16 class ImportElementsComputer { | 30 class ImportElementsComputer { |
| 17 /** | 31 /** |
| 18 * The analysis session used to compute the unit. | 32 * The resource provider used to access the file system. |
| 19 */ | 33 */ |
| 20 final AnalysisSession session; | 34 final ResourceProvider resourceProvider; |
| 21 | 35 |
| 22 /** | 36 /** |
| 23 * The library element representing the library to which the imports are to be | 37 * The resolution result associated with the defining compilation unit of the |
| 24 * added. | 38 * library to which imports might be added. |
| 25 */ | 39 */ |
| 26 final LibraryElement libraryElement; | 40 final ResolveResult libraryResult; |
| 27 | 41 |
| 28 /** | 42 /** |
| 29 * The path of the defining compilation unit of the library. | 43 * Initialize a newly created builder. |
| 30 */ | 44 */ |
| 31 final String path; | 45 ImportElementsComputer(this.resourceProvider, this.libraryResult); |
| 32 | 46 |
| 33 /** | 47 /** |
| 34 * The elements that are to be imported into the library. | 48 * Create the edits that will cause the list of [importedElements] to be |
| 35 */ | 49 * imported into the library at the given [path]. |
| 36 final List<ImportedElements> elements; | 50 */ |
| 37 | 51 Future<SourceChange> createEdits( |
| 38 /** | 52 List<ImportedElements> importedElementsList) async { |
| 39 * Initialize a newly created computer to compute the edits required to ensure | 53 List<ImportedElements> filteredImportedElements = |
| 40 * that the given list of [elements] is imported into a given [library]. | 54 _filterImportedElements(importedElementsList); |
| 41 */ | 55 LibraryElement libraryElement = libraryResult.libraryElement; |
| 42 ImportElementsComputer(ResolveResult result, this.path, this.elements) | 56 SourceFactory sourceFactory = libraryElement.context.sourceFactory; |
|
scheglov
2017/07/27 20:54:15
It might be worth eventually to expose SourceFacto
Brian Wilkerson
2017/07/27 20:55:33
I agree. I'll try to tackle that soon.
| |
| 43 : session = result.session, | 57 List<ImportDirective> existingImports = <ImportDirective>[]; |
| 44 libraryElement = result.libraryElement; | 58 for (var directive in libraryResult.unit.directives) { |
| 45 | 59 if (directive is ImportDirective) { |
| 46 /** | 60 existingImports.add(directive); |
| 47 * Compute and return the list of edits. | 61 } |
| 48 */ | 62 } |
| 49 List<SourceEdit> compute() { | 63 |
| 50 DartChangeBuilder builder = new DartChangeBuilder(session); | 64 DartChangeBuilder builder = new DartChangeBuilder(libraryResult.session); |
| 51 builder.addFileEdit(path, (DartFileEditBuilder builder) { | 65 await builder.addFileEdit(libraryResult.path, |
| 52 // TODO(brianwilkerson) Implement this. | 66 (DartFileEditBuilder builder) { |
| 67 for (ImportedElements importedElements in filteredImportedElements) { | |
| 68 List<ImportDirective> matchingImports = | |
| 69 _findMatchingImports(existingImports, importedElements); | |
| 70 if (matchingImports.isEmpty) { | |
| 71 // | |
| 72 // The required library is not being imported with a matching prefix, | |
| 73 // so we need to add an import. | |
| 74 // | |
| 75 File importedFile = resourceProvider.getFile(importedElements.path); | |
| 76 Uri uri = sourceFactory.restoreUri(importedFile.createSource()); | |
| 77 Source importedSource = importedFile.createSource(uri); | |
| 78 String importUri = | |
| 79 _getLibrarySourceUri(libraryElement, importedSource); | |
| 80 int offset = _offsetForInsertion(importUri); | |
| 81 builder.addInsertion(offset, (DartEditBuilder builder) { | |
| 82 builder.writeln(); | |
| 83 builder.write("import '"); | |
| 84 builder.write(importUri); | |
| 85 builder.write("'"); | |
| 86 if (importedElements.prefix.isNotEmpty) { | |
| 87 builder.write(' as '); | |
| 88 builder.write(importedElements.prefix); | |
| 89 } | |
| 90 builder.write(';'); | |
| 91 }); | |
| 92 } else { | |
| 93 // | |
| 94 // There are some imports of the library with a matching prefix. We | |
| 95 // need to determine whether the names are already visible or whether | |
| 96 // we need to make edits to make them visible. | |
| 97 // | |
| 98 // Compute the edits that need to be made. | |
| 99 // | |
| 100 Map<ImportDirective, _ImportUpdate> updateMap = | |
| 101 <ImportDirective, _ImportUpdate>{}; | |
| 102 for (String requiredName in importedElements.elements) { | |
| 103 _computeUpdate(updateMap, matchingImports, requiredName); | |
| 104 } | |
| 105 // | |
| 106 // Apply the edits. | |
| 107 // | |
| 108 for (ImportDirective directive in updateMap.keys) { | |
| 109 _ImportUpdate update = updateMap[directive]; | |
| 110 List<String> namesToUnhide = update.namesToUnhide; | |
| 111 List<String> namesToShow = update.namesToShow; | |
| 112 namesToShow.sort(); | |
| 113 NodeList<Combinator> combinators = directive.combinators; | |
| 114 int combinatorCount = combinators.length; | |
| 115 for (int combinatorIndex = 0; | |
| 116 combinatorIndex < combinatorCount; | |
| 117 combinatorIndex++) { | |
| 118 Combinator combinator = combinators[combinatorIndex]; | |
| 119 if (combinator is HideCombinator && namesToUnhide.isNotEmpty) { | |
| 120 NodeList<SimpleIdentifier> hiddenNames = combinator.hiddenNames; | |
| 121 int nameCount = hiddenNames.length; | |
| 122 int first = -1; | |
| 123 for (int nameIndex = 0; nameIndex < nameCount; nameIndex++) { | |
| 124 if (namesToUnhide.contains(hiddenNames[nameIndex].name)) { | |
| 125 if (first < 0) { | |
| 126 first = nameIndex; | |
| 127 } | |
| 128 } else { | |
| 129 if (first >= 0) { | |
| 130 // Remove a range of names. | |
| 131 builder.addDeletion(range.startStart( | |
| 132 hiddenNames[first], hiddenNames[nameIndex])); | |
| 133 first = -1; | |
| 134 } | |
| 135 } | |
| 136 } | |
| 137 if (first == 0) { | |
| 138 // Remove the whole combinator. | |
| 139 if (combinatorIndex == 0) { | |
| 140 if (combinatorCount > 1) { | |
| 141 builder.addDeletion(range.startStart( | |
| 142 combinator, combinators[combinatorIndex + 1])); | |
| 143 } else { | |
| 144 SyntacticEntity precedingNode = directive.prefix ?? | |
| 145 directive.deferredKeyword ?? | |
| 146 directive.uri; | |
| 147 if (precedingNode == null) { | |
| 148 builder.addDeletion(range.node(combinator)); | |
| 149 } else { | |
| 150 builder.addDeletion( | |
| 151 range.endEnd(precedingNode, combinator)); | |
| 152 } | |
| 153 } | |
| 154 } else { | |
| 155 builder.addDeletion(range.endEnd( | |
| 156 combinators[combinatorIndex - 1], combinator)); | |
| 157 } | |
| 158 } else if (first > 0) { | |
| 159 // Remove a range of names that includes the last name. | |
| 160 builder.addDeletion(range.endEnd( | |
| 161 hiddenNames[first - 1], hiddenNames[nameCount - 1])); | |
| 162 } | |
| 163 } else if (combinator is ShowCombinator && | |
| 164 namesToShow.isNotEmpty) { | |
| 165 // TODO(brianwilkerson) Add the names in alphabetic order. | |
| 166 builder.addInsertion(combinator.shownNames.last.end, | |
| 167 (DartEditBuilder builder) { | |
| 168 for (String nameToShow in namesToShow) { | |
| 169 builder.write(', '); | |
| 170 builder.write(nameToShow); | |
| 171 } | |
| 172 }); | |
| 173 } | |
| 174 } | |
| 175 } | |
| 176 } | |
| 177 } | |
| 53 }); | 178 }); |
| 54 return <SourceEdit>[]; // builder.sourceChange | 179 return builder.sourceChange; |
| 180 } | |
| 181 | |
| 182 /** | |
| 183 * Choose the import for which the least amount of work is required, | |
| 184 * preferring to do no work in there is an import that already makes the name | |
| 185 * visible, and preferring to remove hide combinators rather than add show | |
| 186 * combinators. | |
| 187 * | |
| 188 * The name is visible without needing any changes if: | |
| 189 * - there is an import with no combinators, | |
| 190 * - there is an import with only hide combinators and none of them hide the | |
| 191 * name, | |
| 192 * - there is an import that shows the name and doesn't subsequently hide the | |
| 193 * name. | |
| 194 */ | |
| 195 void _computeUpdate(Map<ImportDirective, _ImportUpdate> updateMap, | |
| 196 List<ImportDirective> matchingImports, String requiredName) { | |
| 197 /** | |
| 198 * Return `true` if the [requiredName] is in the given list of [names]. | |
| 199 */ | |
| 200 bool nameIn(NodeList<SimpleIdentifier> names) { | |
| 201 for (SimpleIdentifier name in names) { | |
| 202 if (name.name == requiredName) { | |
| 203 return true; | |
| 204 } | |
| 205 } | |
| 206 return false; | |
| 207 } | |
| 208 | |
| 209 ImportDirective preferredDirective = null; | |
| 210 int bestEditCount = -1; | |
| 211 bool deleteHide = false; | |
| 212 bool addShow = false; | |
| 213 | |
| 214 for (ImportDirective directive in matchingImports) { | |
| 215 NodeList<Combinator> combinators = directive.combinators; | |
| 216 if (combinators.isEmpty) { | |
| 217 return; | |
| 218 } | |
| 219 bool hasHide = false; | |
| 220 bool needsShow = false; | |
| 221 int editCount = 0; | |
| 222 for (Combinator combinator in combinators) { | |
| 223 if (combinator is HideCombinator) { | |
| 224 if (nameIn(combinator.hiddenNames)) { | |
| 225 hasHide = true; | |
| 226 editCount++; | |
| 227 } | |
| 228 } else if (combinator is ShowCombinator) { | |
| 229 if (needsShow || !nameIn(combinator.shownNames)) { | |
| 230 needsShow = true; | |
| 231 editCount++; | |
| 232 } | |
| 233 } | |
| 234 } | |
| 235 if (editCount == 0) { | |
| 236 return; | |
| 237 } else if (bestEditCount < 0 || editCount < bestEditCount) { | |
| 238 preferredDirective = directive; | |
| 239 bestEditCount = editCount; | |
| 240 deleteHide = hasHide; | |
| 241 addShow = needsShow; | |
| 242 } | |
| 243 } | |
| 244 | |
| 245 _ImportUpdate update = updateMap.putIfAbsent( | |
| 246 preferredDirective, () => new _ImportUpdate(preferredDirective)); | |
| 247 if (deleteHide) { | |
| 248 update.unhide(requiredName); | |
| 249 } | |
| 250 if (addShow) { | |
| 251 update.show(requiredName); | |
| 252 } | |
| 253 } | |
| 254 | |
| 255 /** | |
| 256 * Filter the given list of imported elements ([originalList]) so that only | |
| 257 * the names that are not already defined still remain. Names that are already | |
| 258 * defined are removed even if they might not resolve to the same name as in | |
| 259 * the original source. | |
| 260 */ | |
| 261 List<ImportedElements> _filterImportedElements( | |
| 262 List<ImportedElements> originalList) { | |
| 263 LibraryElement libraryElement = libraryResult.libraryElement; | |
| 264 LibraryScope libraryScope = new LibraryScope(libraryElement); | |
| 265 AstFactory factory = new AstFactoryImpl(); | |
| 266 List<ImportedElements> filteredList = <ImportedElements>[]; | |
| 267 for (ImportedElements elements in originalList) { | |
| 268 List<String> originalElements = elements.elements; | |
| 269 List<String> filteredElements = originalElements.toList(); | |
| 270 for (String name in originalElements) { | |
| 271 Identifier identifier = factory | |
| 272 .simpleIdentifier(new StringToken(TokenType.IDENTIFIER, name, -1)); | |
| 273 if (elements.prefix.isNotEmpty) { | |
| 274 SimpleIdentifier prefix = factory.simpleIdentifier( | |
| 275 new StringToken(TokenType.IDENTIFIER, elements.prefix, -1)); | |
| 276 Token period = new SimpleToken(TokenType.PERIOD, -1); | |
| 277 identifier = factory.prefixedIdentifier(prefix, period, identifier); | |
| 278 } | |
| 279 Element element = libraryScope.lookup(identifier, libraryElement); | |
| 280 if (element != null) { | |
| 281 filteredElements.remove(name); | |
| 282 } | |
| 283 } | |
| 284 if (originalElements.length == filteredElements.length) { | |
| 285 filteredList.add(elements); | |
| 286 } else if (filteredElements.isNotEmpty) { | |
| 287 filteredList.add(new ImportedElements( | |
| 288 elements.path, elements.prefix, filteredElements)); | |
| 289 } | |
| 290 } | |
| 291 return filteredList; | |
| 292 } | |
| 293 | |
| 294 /** | |
| 295 * Return all of the import elements in the list of [existingImports] that | |
| 296 * match the given specification of [importedElements], or an empty list if | |
| 297 * there are no such imports. | |
| 298 */ | |
| 299 List<ImportDirective> _findMatchingImports( | |
| 300 List<ImportDirective> existingImports, | |
| 301 ImportedElements importedElements) { | |
| 302 List<ImportDirective> matchingImports = <ImportDirective>[]; | |
| 303 for (ImportDirective existingImport in existingImports) { | |
| 304 if (_matches(existingImport, importedElements)) { | |
| 305 matchingImports.add(existingImport); | |
| 306 } | |
| 307 } | |
| 308 return matchingImports; | |
| 309 } | |
| 310 | |
| 311 /** | |
| 312 * Computes the best URI to import [what] into [from]. | |
| 313 * | |
| 314 * Copied from DartFileEditBuilderImpl. | |
| 315 */ | |
| 316 String _getLibrarySourceUri(LibraryElement from, Source what) { | |
| 317 String whatPath = what.fullName; | |
| 318 // check if an absolute URI (such as 'dart:' or 'package:') | |
| 319 Uri whatUri = what.uri; | |
| 320 String whatUriScheme = whatUri.scheme; | |
| 321 if (whatUriScheme != '' && whatUriScheme != 'file') { | |
| 322 return whatUri.toString(); | |
| 323 } | |
| 324 // compute a relative URI | |
| 325 Context context = resourceProvider.pathContext; | |
| 326 String fromFolder = context.dirname(from.source.fullName); | |
| 327 String relativeFile = context.relative(whatPath, from: fromFolder); | |
| 328 return context.split(relativeFile).join('/'); | |
| 329 } | |
| 330 | |
| 331 /** | |
| 332 * Return `true` if the given [import] matches the given specification of | |
| 333 * [importedElements]. They will match if they import the same library using | |
| 334 * the same prefix. | |
| 335 */ | |
| 336 bool _matches(ImportDirective import, ImportedElements importedElements) { | |
| 337 return (import.element as ImportElement).importedLibrary.source.fullName == | |
| 338 importedElements.path && | |
| 339 (import.prefix?.name ?? '') == importedElements.prefix; | |
| 340 } | |
| 341 | |
| 342 /** | |
| 343 * Return the offset at which an import of the given [importUri] should be | |
| 344 * inserted. | |
| 345 * | |
| 346 * Partially copied from DartFileEditBuilderImpl. | |
| 347 */ | |
| 348 int _offsetForInsertion(String importUri) { | |
| 349 // TODO(brianwilkerson) Fix this to find the right location. | |
| 350 // See DartFileEditBuilderImpl._addLibraryImports for inspiration. | |
| 351 CompilationUnit unit = libraryResult.unit; | |
| 352 LibraryDirective libraryDirective; | |
| 353 List<ImportDirective> importDirectives = <ImportDirective>[]; | |
| 354 for (Directive directive in unit.directives) { | |
| 355 if (directive is LibraryDirective) { | |
| 356 libraryDirective = directive; | |
| 357 } else if (directive is ImportDirective) { | |
| 358 importDirectives.add(directive); | |
| 359 } | |
| 360 } | |
| 361 if (importDirectives.isEmpty) { | |
| 362 if (libraryDirective == null) { | |
| 363 return 0; | |
| 364 } | |
| 365 return libraryDirective.end; | |
| 366 } | |
| 367 return importDirectives.last.end; | |
| 55 } | 368 } |
| 56 } | 369 } |
| 370 | |
| 371 /** | |
| 372 * Information about how a given import directive needs to be updated in order | |
| 373 * to make the required names visible. | |
| 374 */ | |
| 375 class _ImportUpdate { | |
| 376 /** | |
| 377 * The import directive to be updated. | |
| 378 */ | |
| 379 final ImportDirective import; | |
| 380 | |
| 381 /** | |
| 382 * The list of names that are currently hidden that need to not be hidden. | |
| 383 */ | |
| 384 final List<String> namesToUnhide = <String>[]; | |
| 385 | |
| 386 /** | |
| 387 * The list of names that need to be added to show clauses. | |
| 388 */ | |
| 389 final List<String> namesToShow = <String>[]; | |
| 390 | |
| 391 /** | |
| 392 * Initialize a newly created information holder to hold information about | |
| 393 * updates to the given [import]. | |
| 394 */ | |
| 395 _ImportUpdate(this.import); | |
| 396 | |
| 397 /** | |
| 398 * Record that the given [name] needs to be added to show combinators. | |
| 399 */ | |
| 400 void show(String name) { | |
| 401 namesToShow.add(name); | |
| 402 } | |
| 403 | |
| 404 /** | |
| 405 * Record that the given [name] needs to be removed from hide combinators. | |
| 406 */ | |
| 407 void unhide(String name) { | |
| 408 namesToUnhide.add(name); | |
| 409 } | |
| 410 } | |
| OLD | NEW |