Index: pkg/analysis_server/lib/src/computer/import_elements_computer.dart |
diff --git a/pkg/analysis_server/lib/src/computer/import_elements_computer.dart b/pkg/analysis_server/lib/src/computer/import_elements_computer.dart |
index 0826e39c2702e9860ee72fa1ba56a71fb69ffe97..f9328f4351017bd7e3b8ac8be9cae3af4abaf707 100644 |
--- a/pkg/analysis_server/lib/src/computer/import_elements_computer.dart |
+++ b/pkg/analysis_server/lib/src/computer/import_elements_computer.dart |
@@ -2,55 +2,409 @@ |
// 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:async'; |
+ |
import 'package:analysis_server/protocol/protocol_generated.dart'; |
import 'package:analyzer/dart/analysis/results.dart'; |
-import 'package:analyzer/dart/analysis/session.dart'; |
+import 'package:analyzer/dart/ast/ast.dart'; |
+import 'package:analyzer/dart/ast/ast_factory.dart'; |
+import 'package:analyzer/dart/ast/token.dart'; |
import 'package:analyzer/dart/element/element.dart'; |
-import 'package:analyzer_plugin/protocol/protocol_common.dart'; |
+import 'package:analyzer/file_system/file_system.dart'; |
+import 'package:analyzer/src/dart/ast/ast_factory.dart'; |
+import 'package:analyzer/src/dart/ast/token.dart'; |
+import 'package:analyzer/src/dart/resolver/scope.dart'; |
+import 'package:analyzer/src/generated/source.dart'; |
+import 'package:analyzer_plugin/protocol/protocol_common.dart' hide Element; |
import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dart'; |
+import 'package:analyzer_plugin/utilities/range_factory.dart'; |
+import 'package:front_end/src/base/syntactic_entity.dart'; |
+import 'package:path/src/context.dart'; |
/** |
- * An object used to compute the edits required to ensure that a list of |
- * elements is imported into a given library. |
+ * An object used to compute a set of edits to add imports to a given library in |
+ * order to make a given set of elements visible. |
+ * |
+ * This is used to implement the `edit.importElements` request. |
*/ |
class ImportElementsComputer { |
/** |
- * The analysis session used to compute the unit. |
+ * The resource provider used to access the file system. |
*/ |
- final AnalysisSession session; |
+ final ResourceProvider resourceProvider; |
/** |
- * The library element representing the library to which the imports are to be |
- * added. |
+ * The resolution result associated with the defining compilation unit of the |
+ * library to which imports might be added. |
*/ |
- final LibraryElement libraryElement; |
+ final ResolveResult libraryResult; |
/** |
- * The path of the defining compilation unit of the library. |
+ * Initialize a newly created builder. |
*/ |
- final String path; |
+ ImportElementsComputer(this.resourceProvider, this.libraryResult); |
/** |
- * The elements that are to be imported into the library. |
+ * Create the edits that will cause the list of [importedElements] to be |
+ * imported into the library at the given [path]. |
*/ |
- final List<ImportedElements> elements; |
+ Future<SourceChange> createEdits( |
+ List<ImportedElements> importedElementsList) async { |
+ List<ImportedElements> filteredImportedElements = |
+ _filterImportedElements(importedElementsList); |
+ LibraryElement libraryElement = libraryResult.libraryElement; |
+ 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.
|
+ List<ImportDirective> existingImports = <ImportDirective>[]; |
+ for (var directive in libraryResult.unit.directives) { |
+ if (directive is ImportDirective) { |
+ existingImports.add(directive); |
+ } |
+ } |
+ |
+ DartChangeBuilder builder = new DartChangeBuilder(libraryResult.session); |
+ await builder.addFileEdit(libraryResult.path, |
+ (DartFileEditBuilder builder) { |
+ for (ImportedElements importedElements in filteredImportedElements) { |
+ List<ImportDirective> matchingImports = |
+ _findMatchingImports(existingImports, importedElements); |
+ if (matchingImports.isEmpty) { |
+ // |
+ // The required library is not being imported with a matching prefix, |
+ // so we need to add an import. |
+ // |
+ File importedFile = resourceProvider.getFile(importedElements.path); |
+ Uri uri = sourceFactory.restoreUri(importedFile.createSource()); |
+ Source importedSource = importedFile.createSource(uri); |
+ String importUri = |
+ _getLibrarySourceUri(libraryElement, importedSource); |
+ int offset = _offsetForInsertion(importUri); |
+ builder.addInsertion(offset, (DartEditBuilder builder) { |
+ builder.writeln(); |
+ builder.write("import '"); |
+ builder.write(importUri); |
+ builder.write("'"); |
+ if (importedElements.prefix.isNotEmpty) { |
+ builder.write(' as '); |
+ builder.write(importedElements.prefix); |
+ } |
+ builder.write(';'); |
+ }); |
+ } else { |
+ // |
+ // There are some imports of the library with a matching prefix. We |
+ // need to determine whether the names are already visible or whether |
+ // we need to make edits to make them visible. |
+ // |
+ // Compute the edits that need to be made. |
+ // |
+ Map<ImportDirective, _ImportUpdate> updateMap = |
+ <ImportDirective, _ImportUpdate>{}; |
+ for (String requiredName in importedElements.elements) { |
+ _computeUpdate(updateMap, matchingImports, requiredName); |
+ } |
+ // |
+ // Apply the edits. |
+ // |
+ for (ImportDirective directive in updateMap.keys) { |
+ _ImportUpdate update = updateMap[directive]; |
+ List<String> namesToUnhide = update.namesToUnhide; |
+ List<String> namesToShow = update.namesToShow; |
+ namesToShow.sort(); |
+ NodeList<Combinator> combinators = directive.combinators; |
+ int combinatorCount = combinators.length; |
+ for (int combinatorIndex = 0; |
+ combinatorIndex < combinatorCount; |
+ combinatorIndex++) { |
+ Combinator combinator = combinators[combinatorIndex]; |
+ if (combinator is HideCombinator && namesToUnhide.isNotEmpty) { |
+ NodeList<SimpleIdentifier> hiddenNames = combinator.hiddenNames; |
+ int nameCount = hiddenNames.length; |
+ int first = -1; |
+ for (int nameIndex = 0; nameIndex < nameCount; nameIndex++) { |
+ if (namesToUnhide.contains(hiddenNames[nameIndex].name)) { |
+ if (first < 0) { |
+ first = nameIndex; |
+ } |
+ } else { |
+ if (first >= 0) { |
+ // Remove a range of names. |
+ builder.addDeletion(range.startStart( |
+ hiddenNames[first], hiddenNames[nameIndex])); |
+ first = -1; |
+ } |
+ } |
+ } |
+ if (first == 0) { |
+ // Remove the whole combinator. |
+ if (combinatorIndex == 0) { |
+ if (combinatorCount > 1) { |
+ builder.addDeletion(range.startStart( |
+ combinator, combinators[combinatorIndex + 1])); |
+ } else { |
+ SyntacticEntity precedingNode = directive.prefix ?? |
+ directive.deferredKeyword ?? |
+ directive.uri; |
+ if (precedingNode == null) { |
+ builder.addDeletion(range.node(combinator)); |
+ } else { |
+ builder.addDeletion( |
+ range.endEnd(precedingNode, combinator)); |
+ } |
+ } |
+ } else { |
+ builder.addDeletion(range.endEnd( |
+ combinators[combinatorIndex - 1], combinator)); |
+ } |
+ } else if (first > 0) { |
+ // Remove a range of names that includes the last name. |
+ builder.addDeletion(range.endEnd( |
+ hiddenNames[first - 1], hiddenNames[nameCount - 1])); |
+ } |
+ } else if (combinator is ShowCombinator && |
+ namesToShow.isNotEmpty) { |
+ // TODO(brianwilkerson) Add the names in alphabetic order. |
+ builder.addInsertion(combinator.shownNames.last.end, |
+ (DartEditBuilder builder) { |
+ for (String nameToShow in namesToShow) { |
+ builder.write(', '); |
+ builder.write(nameToShow); |
+ } |
+ }); |
+ } |
+ } |
+ } |
+ } |
+ } |
+ }); |
+ return builder.sourceChange; |
+ } |
/** |
- * Initialize a newly created computer to compute the edits required to ensure |
- * that the given list of [elements] is imported into a given [library]. |
+ * Choose the import for which the least amount of work is required, |
+ * preferring to do no work in there is an import that already makes the name |
+ * visible, and preferring to remove hide combinators rather than add show |
+ * combinators. |
+ * |
+ * The name is visible without needing any changes if: |
+ * - there is an import with no combinators, |
+ * - there is an import with only hide combinators and none of them hide the |
+ * name, |
+ * - there is an import that shows the name and doesn't subsequently hide the |
+ * name. |
*/ |
- ImportElementsComputer(ResolveResult result, this.path, this.elements) |
- : session = result.session, |
- libraryElement = result.libraryElement; |
+ void _computeUpdate(Map<ImportDirective, _ImportUpdate> updateMap, |
+ List<ImportDirective> matchingImports, String requiredName) { |
+ /** |
+ * Return `true` if the [requiredName] is in the given list of [names]. |
+ */ |
+ bool nameIn(NodeList<SimpleIdentifier> names) { |
+ for (SimpleIdentifier name in names) { |
+ if (name.name == requiredName) { |
+ return true; |
+ } |
+ } |
+ return false; |
+ } |
+ |
+ ImportDirective preferredDirective = null; |
+ int bestEditCount = -1; |
+ bool deleteHide = false; |
+ bool addShow = false; |
+ |
+ for (ImportDirective directive in matchingImports) { |
+ NodeList<Combinator> combinators = directive.combinators; |
+ if (combinators.isEmpty) { |
+ return; |
+ } |
+ bool hasHide = false; |
+ bool needsShow = false; |
+ int editCount = 0; |
+ for (Combinator combinator in combinators) { |
+ if (combinator is HideCombinator) { |
+ if (nameIn(combinator.hiddenNames)) { |
+ hasHide = true; |
+ editCount++; |
+ } |
+ } else if (combinator is ShowCombinator) { |
+ if (needsShow || !nameIn(combinator.shownNames)) { |
+ needsShow = true; |
+ editCount++; |
+ } |
+ } |
+ } |
+ if (editCount == 0) { |
+ return; |
+ } else if (bestEditCount < 0 || editCount < bestEditCount) { |
+ preferredDirective = directive; |
+ bestEditCount = editCount; |
+ deleteHide = hasHide; |
+ addShow = needsShow; |
+ } |
+ } |
+ |
+ _ImportUpdate update = updateMap.putIfAbsent( |
+ preferredDirective, () => new _ImportUpdate(preferredDirective)); |
+ if (deleteHide) { |
+ update.unhide(requiredName); |
+ } |
+ if (addShow) { |
+ update.show(requiredName); |
+ } |
+ } |
/** |
- * Compute and return the list of edits. |
+ * Filter the given list of imported elements ([originalList]) so that only |
+ * the names that are not already defined still remain. Names that are already |
+ * defined are removed even if they might not resolve to the same name as in |
+ * the original source. |
*/ |
- List<SourceEdit> compute() { |
- DartChangeBuilder builder = new DartChangeBuilder(session); |
- builder.addFileEdit(path, (DartFileEditBuilder builder) { |
- // TODO(brianwilkerson) Implement this. |
- }); |
- return <SourceEdit>[]; // builder.sourceChange |
+ List<ImportedElements> _filterImportedElements( |
+ List<ImportedElements> originalList) { |
+ LibraryElement libraryElement = libraryResult.libraryElement; |
+ LibraryScope libraryScope = new LibraryScope(libraryElement); |
+ AstFactory factory = new AstFactoryImpl(); |
+ List<ImportedElements> filteredList = <ImportedElements>[]; |
+ for (ImportedElements elements in originalList) { |
+ List<String> originalElements = elements.elements; |
+ List<String> filteredElements = originalElements.toList(); |
+ for (String name in originalElements) { |
+ Identifier identifier = factory |
+ .simpleIdentifier(new StringToken(TokenType.IDENTIFIER, name, -1)); |
+ if (elements.prefix.isNotEmpty) { |
+ SimpleIdentifier prefix = factory.simpleIdentifier( |
+ new StringToken(TokenType.IDENTIFIER, elements.prefix, -1)); |
+ Token period = new SimpleToken(TokenType.PERIOD, -1); |
+ identifier = factory.prefixedIdentifier(prefix, period, identifier); |
+ } |
+ Element element = libraryScope.lookup(identifier, libraryElement); |
+ if (element != null) { |
+ filteredElements.remove(name); |
+ } |
+ } |
+ if (originalElements.length == filteredElements.length) { |
+ filteredList.add(elements); |
+ } else if (filteredElements.isNotEmpty) { |
+ filteredList.add(new ImportedElements( |
+ elements.path, elements.prefix, filteredElements)); |
+ } |
+ } |
+ return filteredList; |
+ } |
+ |
+ /** |
+ * Return all of the import elements in the list of [existingImports] that |
+ * match the given specification of [importedElements], or an empty list if |
+ * there are no such imports. |
+ */ |
+ List<ImportDirective> _findMatchingImports( |
+ List<ImportDirective> existingImports, |
+ ImportedElements importedElements) { |
+ List<ImportDirective> matchingImports = <ImportDirective>[]; |
+ for (ImportDirective existingImport in existingImports) { |
+ if (_matches(existingImport, importedElements)) { |
+ matchingImports.add(existingImport); |
+ } |
+ } |
+ return matchingImports; |
+ } |
+ |
+ /** |
+ * Computes the best URI to import [what] into [from]. |
+ * |
+ * Copied from DartFileEditBuilderImpl. |
+ */ |
+ String _getLibrarySourceUri(LibraryElement from, Source what) { |
+ String whatPath = what.fullName; |
+ // check if an absolute URI (such as 'dart:' or 'package:') |
+ Uri whatUri = what.uri; |
+ String whatUriScheme = whatUri.scheme; |
+ if (whatUriScheme != '' && whatUriScheme != 'file') { |
+ return whatUri.toString(); |
+ } |
+ // compute a relative URI |
+ Context context = resourceProvider.pathContext; |
+ String fromFolder = context.dirname(from.source.fullName); |
+ String relativeFile = context.relative(whatPath, from: fromFolder); |
+ return context.split(relativeFile).join('/'); |
+ } |
+ |
+ /** |
+ * Return `true` if the given [import] matches the given specification of |
+ * [importedElements]. They will match if they import the same library using |
+ * the same prefix. |
+ */ |
+ bool _matches(ImportDirective import, ImportedElements importedElements) { |
+ return (import.element as ImportElement).importedLibrary.source.fullName == |
+ importedElements.path && |
+ (import.prefix?.name ?? '') == importedElements.prefix; |
+ } |
+ |
+ /** |
+ * Return the offset at which an import of the given [importUri] should be |
+ * inserted. |
+ * |
+ * Partially copied from DartFileEditBuilderImpl. |
+ */ |
+ int _offsetForInsertion(String importUri) { |
+ // TODO(brianwilkerson) Fix this to find the right location. |
+ // See DartFileEditBuilderImpl._addLibraryImports for inspiration. |
+ CompilationUnit unit = libraryResult.unit; |
+ LibraryDirective libraryDirective; |
+ List<ImportDirective> importDirectives = <ImportDirective>[]; |
+ for (Directive directive in unit.directives) { |
+ if (directive is LibraryDirective) { |
+ libraryDirective = directive; |
+ } else if (directive is ImportDirective) { |
+ importDirectives.add(directive); |
+ } |
+ } |
+ if (importDirectives.isEmpty) { |
+ if (libraryDirective == null) { |
+ return 0; |
+ } |
+ return libraryDirective.end; |
+ } |
+ return importDirectives.last.end; |
+ } |
+} |
+ |
+/** |
+ * Information about how a given import directive needs to be updated in order |
+ * to make the required names visible. |
+ */ |
+class _ImportUpdate { |
+ /** |
+ * The import directive to be updated. |
+ */ |
+ final ImportDirective import; |
+ |
+ /** |
+ * The list of names that are currently hidden that need to not be hidden. |
+ */ |
+ final List<String> namesToUnhide = <String>[]; |
+ |
+ /** |
+ * The list of names that need to be added to show clauses. |
+ */ |
+ final List<String> namesToShow = <String>[]; |
+ |
+ /** |
+ * Initialize a newly created information holder to hold information about |
+ * updates to the given [import]. |
+ */ |
+ _ImportUpdate(this.import); |
+ |
+ /** |
+ * Record that the given [name] needs to be added to show combinators. |
+ */ |
+ void show(String name) { |
+ namesToShow.add(name); |
+ } |
+ |
+ /** |
+ * Record that the given [name] needs to be removed from hide combinators. |
+ */ |
+ void unhide(String name) { |
+ namesToUnhide.add(name); |
} |
} |