| Index: pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart
|
| diff --git a/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart b/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart
|
| index a28afbdab9539efd319cff01628ee761878b8079..ee3b35d0c4d289cf2586582c5dda4f6ebac8df1f 100644
|
| --- a/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart
|
| +++ b/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_dart.dart
|
| @@ -4,10 +4,6 @@
|
|
|
| import 'dart:async';
|
|
|
| -import 'package:analysis_server/protocol/protocol_generated.dart'
|
| - hide Element, ElementKind;
|
| -import 'package:analysis_server/src/services/correction/name_suggestion.dart';
|
| -import 'package:analysis_server/src/services/correction/util.dart';
|
| import 'package:analyzer/dart/ast/ast.dart';
|
| import 'package:analyzer/dart/ast/token.dart';
|
| import 'package:analyzer/dart/element/element.dart';
|
| @@ -17,10 +13,15 @@ import 'package:analyzer/src/dart/ast/utilities.dart';
|
| import 'package:analyzer/src/generated/resolver.dart';
|
| import 'package:analyzer/src/generated/source.dart';
|
| import 'package:analyzer/src/generated/utilities_dart.dart';
|
| +import 'package:analyzer_plugin/protocol/protocol_generated.dart'
|
| + hide Element, ElementKind;
|
| import 'package:analyzer_plugin/src/utilities/change_builder/change_builder_core.dart';
|
| +import 'package:analyzer_plugin/src/utilities/string_utilities.dart';
|
| import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
|
| import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dart';
|
| import 'package:analyzer_plugin/utilities/range_factory.dart';
|
| +import 'package:charcode/ascii.dart';
|
| +import 'package:path/path.dart' as path;
|
|
|
| /**
|
| * A [ChangeBuilder] used to build changes in Dart files.
|
| @@ -49,6 +50,8 @@ class DartChangeBuilderImpl extends ChangeBuilderImpl
|
| * An [EditBuilder] used to build edits in Dart files.
|
| */
|
| class DartEditBuilderImpl extends EditBuilderImpl implements DartEditBuilder {
|
| + List<String> _KNOWN_METHOD_NAME_PREFIXES = ['get', 'is', 'to'];
|
| +
|
| /**
|
| * Initialize a newly created builder to build a source edit.
|
| */
|
| @@ -543,6 +546,45 @@ class DartEditBuilderImpl extends EditBuilderImpl implements DartEditBuilder {
|
| }
|
| }
|
|
|
| + /**
|
| + * Adds [toAdd] items which are not excluded.
|
| + */
|
| + void _addAll(
|
| + Set<String> excluded, Set<String> result, Iterable<String> toAdd) {
|
| + for (String item in toAdd) {
|
| + // add name based on "item", but not "excluded"
|
| + for (int suffix = 1;; suffix++) {
|
| + // prepare name, just "item" or "item2", "item3", etc
|
| + String name = item;
|
| + if (suffix > 1) {
|
| + name += suffix.toString();
|
| + }
|
| + // add once found not excluded
|
| + if (!excluded.contains(name)) {
|
| + result.add(name);
|
| + break;
|
| + }
|
| + }
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Adds to [result] either [c] or the first ASCII character after it.
|
| + */
|
| + void _addSingleCharacterName(
|
| + Set<String> excluded, Set<String> result, int c) {
|
| + while (c < $z) {
|
| + String name = new String.fromCharCode(c);
|
| + // may be done
|
| + if (!excluded.contains(name)) {
|
| + result.add(name);
|
| + break;
|
| + }
|
| + // next character
|
| + c = c + 1;
|
| + }
|
| + }
|
| +
|
| void _addSuperTypeProposals(
|
| LinkedEditBuilder builder, DartType type, Set<DartType> alreadyAdded) {
|
| if (type != null &&
|
| @@ -557,22 +599,125 @@ class DartEditBuilderImpl extends EditBuilderImpl implements DartEditBuilder {
|
| }
|
| }
|
|
|
| + String _getBaseNameFromExpression(Expression expression) {
|
| + String name = null;
|
| + // e as Type
|
| + if (expression is AsExpression) {
|
| + AsExpression asExpression = expression as AsExpression;
|
| + expression = asExpression.expression;
|
| + }
|
| + // analyze expressions
|
| + if (expression is SimpleIdentifier) {
|
| + SimpleIdentifier node = expression;
|
| + return node.name;
|
| + } else if (expression is PrefixedIdentifier) {
|
| + PrefixedIdentifier node = expression;
|
| + return node.identifier.name;
|
| + } else if (expression is PropertyAccess) {
|
| + PropertyAccess node = expression;
|
| + return node.propertyName.name;
|
| + } else if (expression is MethodInvocation) {
|
| + name = expression.methodName.name;
|
| + } else if (expression is InstanceCreationExpression) {
|
| + InstanceCreationExpression creation = expression;
|
| + ConstructorName constructorName = creation.constructorName;
|
| + TypeName typeName = constructorName.type;
|
| + if (typeName != null) {
|
| + Identifier typeNameIdentifier = typeName.name;
|
| + // new ClassName()
|
| + if (typeNameIdentifier is SimpleIdentifier) {
|
| + return typeNameIdentifier.name;
|
| + }
|
| + // new prefix.name();
|
| + if (typeNameIdentifier is PrefixedIdentifier) {
|
| + PrefixedIdentifier prefixed = typeNameIdentifier;
|
| + // new prefix.ClassName()
|
| + if (prefixed.prefix.staticElement is PrefixElement) {
|
| + return prefixed.identifier.name;
|
| + }
|
| + // new ClassName.constructorName()
|
| + return prefixed.prefix.name;
|
| + }
|
| + }
|
| + }
|
| + // strip known prefixes
|
| + if (name != null) {
|
| + for (int i = 0; i < _KNOWN_METHOD_NAME_PREFIXES.length; i++) {
|
| + String prefix = _KNOWN_METHOD_NAME_PREFIXES[i];
|
| + if (name.startsWith(prefix)) {
|
| + if (name == prefix) {
|
| + return null;
|
| + } else if (isUpperCase(name.codeUnitAt(prefix.length))) {
|
| + return name.substring(prefix.length);
|
| + }
|
| + }
|
| + }
|
| + }
|
| + // done
|
| + return name;
|
| + }
|
| +
|
| + String _getBaseNameFromLocationInParent(Expression expression) {
|
| + // value in named expression
|
| + if (expression.parent is NamedExpression) {
|
| + NamedExpression namedExpression = expression.parent as NamedExpression;
|
| + if (namedExpression.expression == expression) {
|
| + return namedExpression.name.label.name;
|
| + }
|
| + }
|
| + // positional argument
|
| + ParameterElement parameter = expression.propagatedParameterElement;
|
| + if (parameter == null) {
|
| + parameter = expression.staticParameterElement;
|
| + }
|
| + if (parameter != null) {
|
| + return parameter.displayName;
|
| + }
|
| +
|
| + // unknown
|
| + return null;
|
| + }
|
| +
|
| + /**
|
| + * Returns all variants of names by removing leading words one by one.
|
| + */
|
| + List<String> _getCamelWordCombinations(String name) {
|
| + List<String> result = [];
|
| + List<String> parts = getCamelWords(name);
|
| + for (int i = 0; i < parts.length; i++) {
|
| + String s1 = parts[i].toLowerCase();
|
| + String s2 = parts.skip(i + 1).join();
|
| + String suggestion = '$s1$s2';
|
| + result.add(suggestion);
|
| + }
|
| + return result;
|
| + }
|
| +
|
| /**
|
| * Return the import element used to import the given [element] into the given
|
| * [library], or `null` if the element was not imported, such as when the
|
| * element is declared in the same library.
|
| */
|
| ImportElement _getImportElement(Element element, LibraryElement library) {
|
| - for (ImportElement imp in library.imports) {
|
| - Map<String, Element> definedNames = getImportNamespace(imp);
|
| + for (ImportElement importElement in library.imports) {
|
| + Map<String, Element> definedNames = _getImportNamespace(importElement);
|
| if (definedNames.containsValue(element)) {
|
| - return imp;
|
| + return importElement;
|
| }
|
| }
|
| return null;
|
| }
|
|
|
| /**
|
| + * Return the namespace added by the given import [element].
|
| + */
|
| + Map<String, Element> _getImportNamespace(ImportElement element) {
|
| + NamespaceBuilder builder = new NamespaceBuilder();
|
| + Namespace namespace = builder.createImportNamespaceForDirective(element);
|
| + return namespace.definedNames;
|
| + }
|
| +
|
| + /**
|
| * Return a list containing the suggested names for a parameter with the given
|
| * [type] whose value in one location is computed by the given [expression].
|
| * The list will not contain any names in the set of [excluded] names. The
|
| @@ -582,7 +727,7 @@ class DartEditBuilderImpl extends EditBuilderImpl implements DartEditBuilder {
|
| List<String> _getParameterNameSuggestions(
|
| Set<String> usedNames, DartType type, Expression expression, int index) {
|
| List<String> suggestions =
|
| - getVariableNameSuggestionsForExpression(type, expression, usedNames);
|
| + _getVariableNameSuggestionsForExpression(type, expression, usedNames);
|
| if (suggestions.length != 0) {
|
| return suggestions;
|
| }
|
| @@ -738,6 +883,45 @@ class DartEditBuilderImpl extends EditBuilderImpl implements DartEditBuilder {
|
| }
|
|
|
| /**
|
| + * Returns possible names for a variable with the given expected type and
|
| + * expression assigned.
|
| + */
|
| + List<String> _getVariableNameSuggestionsForExpression(DartType expectedType,
|
| + Expression assignedExpression, Set<String> excluded) {
|
| + Set<String> res = new Set();
|
| + // use expression
|
| + if (assignedExpression != null) {
|
| + String nameFromExpression =
|
| + _getBaseNameFromExpression(assignedExpression);
|
| + if (nameFromExpression != null) {
|
| + nameFromExpression = removeStart(nameFromExpression, '_');
|
| + _addAll(excluded, res, _getCamelWordCombinations(nameFromExpression));
|
| + }
|
| + String nameFromParent =
|
| + _getBaseNameFromLocationInParent(assignedExpression);
|
| + if (nameFromParent != null) {
|
| + _addAll(excluded, res, _getCamelWordCombinations(nameFromParent));
|
| + }
|
| + }
|
| + // use type
|
| + if (expectedType != null && !expectedType.isDynamic) {
|
| + String typeName = expectedType.name;
|
| + if ('int' == typeName) {
|
| + _addSingleCharacterName(excluded, res, $i);
|
| + } else if ('double' == typeName) {
|
| + _addSingleCharacterName(excluded, res, $d);
|
| + } else if ('String' == typeName) {
|
| + _addSingleCharacterName(excluded, res, $s);
|
| + } else {
|
| + _addAll(excluded, res, _getCamelWordCombinations(typeName));
|
| + }
|
| + res.remove(typeName);
|
| + }
|
| + // done
|
| + return new List.from(res);
|
| + }
|
| +
|
| + /**
|
| * Checks if [type] is visible in either the [enclosingExecutable] or
|
| * [enclosingClass].
|
| */
|
| @@ -806,25 +990,10 @@ class DartFileEditBuilderImpl extends FileEditBuilderImpl
|
|
|
| @override
|
| void finalize() {
|
| - addLibraryImports(
|
| + _addLibraryImports(
|
| changeBuilder.sourceChange, unit.element.library, librariesToImport);
|
| }
|
|
|
| -// /**
|
| -// * Return the content of the file being edited.
|
| -// */
|
| -// String getContent() {
|
| -// if (_content == null) {
|
| -// CompilationUnitElement unitElement = unit.element;
|
| -// AnalysisContext context = unitElement.context;
|
| -// if (context == null) {
|
| -// throw new CancelCorrectionException();
|
| -// }
|
| -// _content = context.getContents(unitElement.source).data;
|
| -// }
|
| -// return _content;
|
| -// }
|
| -
|
| @override
|
| void importLibraries(Iterable<Source> libraries) {
|
| librariesToImport.addAll(libraries);
|
| @@ -852,6 +1021,203 @@ class DartFileEditBuilderImpl extends FileEditBuilderImpl
|
| });
|
| }
|
|
|
| + /**
|
| + * Adds edits to the given [change] that ensure that all the [libraries] are
|
| + * imported into the given [targetLibrary].
|
| + */
|
| + void _addLibraryImports(SourceChange change, LibraryElement targetLibrary,
|
| + Set<Source> libraries) {
|
| + // Prepare information about existing imports.
|
| + 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);
|
| + }
|
| + }
|
| +
|
| + // Prepare all URIs to import.
|
| + List<String> uriList = libraries
|
| + .map((library) => _getLibrarySourceUri(targetLibrary, library))
|
| + .toList();
|
| + uriList.sort((a, b) => a.compareTo(b));
|
| +
|
| + // Insert imports: between existing imports.
|
| + if (importDirectives.isNotEmpty) {
|
| + bool isFirstPackage = true;
|
| + for (String importUri in uriList) {
|
| + bool inserted = false;
|
| + bool isPackage = importUri.startsWith('package:');
|
| + bool isAfterDart = false;
|
| + for (ImportDirective existingImport in importDirectives) {
|
| + if (existingImport.uriContent.startsWith('dart:')) {
|
| + isAfterDart = true;
|
| + }
|
| + if (existingImport.uriContent.startsWith('package:')) {
|
| + isFirstPackage = false;
|
| + }
|
| + if (importUri.compareTo(existingImport.uriContent) < 0) {
|
| + addInsertion(existingImport.offset, (EditBuilder builder) {
|
| + builder.write("import '");
|
| + builder.write(importUri);
|
| + builder.writeln("';");
|
| + });
|
| + inserted = true;
|
| + break;
|
| + }
|
| + }
|
| + if (!inserted) {
|
| + addInsertion(importDirectives.last.end, (EditBuilder builder) {
|
| + if (isPackage && isFirstPackage && isAfterDart) {
|
| + builder.writeln();
|
| + }
|
| + builder.writeln();
|
| + builder.write("import '");
|
| + builder.write(importUri);
|
| + builder.write("';");
|
| + });
|
| + }
|
| + if (isPackage) {
|
| + isFirstPackage = false;
|
| + }
|
| + }
|
| + return;
|
| + }
|
| +
|
| + // Insert imports: after the library directive.
|
| + if (libraryDirective != null) {
|
| + for (int i = 0; i < uriList.length; i++) {
|
| + String importUri = uriList[i];
|
| + addInsertion(libraryDirective.end, (EditBuilder builder) {
|
| + if (i == 0) {
|
| + builder.writeln();
|
| + }
|
| + builder.writeln();
|
| + builder.write("import '");
|
| + builder.write(importUri);
|
| + builder.writeln("';");
|
| + });
|
| + }
|
| + return;
|
| + }
|
| +
|
| + // If still at the beginning of the file, skip shebang and line comments.
|
| + _InsertionDescription desc = _getInsertDescTop();
|
| + int offset = desc.offset;
|
| + for (int i = 0; i < uriList.length; i++) {
|
| + String importUri = uriList[i];
|
| + addInsertion(offset, (EditBuilder builder) {
|
| + if (i == 0 && desc.insertEmptyLineBefore) {
|
| + builder.writeln();
|
| + }
|
| + builder.write("import '");
|
| + builder.write(importUri);
|
| + builder.writeln("';");
|
| + if (i == uriList.length - 1 && desc.insertEmptyLineAfter) {
|
| + builder.writeln();
|
| + }
|
| + });
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Returns a [InsertDesc] describing where to insert a new directive or a
|
| + * top-level declaration at the top of the file.
|
| + */
|
| + _InsertionDescription _getInsertDescTop() {
|
| + // skip leading line comments
|
| + int offset = 0;
|
| + bool insertEmptyLineBefore = false;
|
| + bool insertEmptyLineAfter = false;
|
| + String source = unit.element.context.getContents(unit.element.source).data;
|
| + var lineInfo = unit.lineInfo;
|
| + // skip hash-bang
|
| + if (offset < source.length - 2) {
|
| + String linePrefix = _getText(source, offset, 2);
|
| + if (linePrefix == "#!") {
|
| + insertEmptyLineBefore = true;
|
| + offset = lineInfo.getOffsetOfLineAfter(offset);
|
| + // skip empty lines to first line comment
|
| + int emptyOffset = offset;
|
| + while (emptyOffset < source.length - 2) {
|
| + int nextLineOffset = lineInfo.getOffsetOfLineAfter(emptyOffset);
|
| + String line = source.substring(emptyOffset, nextLineOffset);
|
| + if (line.trim().isEmpty) {
|
| + emptyOffset = nextLineOffset;
|
| + continue;
|
| + } else if (line.startsWith("//")) {
|
| + offset = emptyOffset;
|
| + break;
|
| + } else {
|
| + break;
|
| + }
|
| + }
|
| + }
|
| + }
|
| + // skip line comments
|
| + while (offset < source.length - 2) {
|
| + String linePrefix = _getText(source, offset, 2);
|
| + if (linePrefix == "//") {
|
| + insertEmptyLineBefore = true;
|
| + offset = lineInfo.getOffsetOfLineAfter(offset);
|
| + } else {
|
| + break;
|
| + }
|
| + }
|
| + // determine if empty line is required after
|
| + int currentLine = lineInfo.getLocation(offset).lineNumber;
|
| + if (currentLine < lineInfo.lineCount) {
|
| + int nextLineOffset = lineInfo.getOffsetOfLine(currentLine + 1);
|
| + String insertLine = source.substring(offset, nextLineOffset);
|
| + if (!insertLine.trim().isEmpty) {
|
| + insertEmptyLineAfter = true;
|
| + }
|
| + }
|
| + return new _InsertionDescription(
|
| + offset, insertEmptyLineBefore, insertEmptyLineAfter);
|
| + }
|
| +
|
| +// /**
|
| +// * Return the content of the file being edited.
|
| +// */
|
| +// String getContent() {
|
| +// if (_content == null) {
|
| +// CompilationUnitElement unitElement = unit.element;
|
| +// AnalysisContext context = unitElement.context;
|
| +// if (context == null) {
|
| +// throw new CancelCorrectionException();
|
| +// }
|
| +// _content = context.getContents(unitElement.source).data;
|
| +// }
|
| +// return _content;
|
| +// }
|
| +
|
| + /**
|
| + * Computes the best URI to import [what] into [from].
|
| + */
|
| + 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
|
| + String fromFolder = path.dirname(from.source.fullName);
|
| + String relativeFile = path.relative(whatPath, from: fromFolder);
|
| + return path.split(relativeFile).join('/');
|
| + }
|
| +
|
| + /**
|
| + * Returns the text of the given range in the unit.
|
| + */
|
| + String _getText(String content, int offset, int length) {
|
| + return content.substring(offset, offset + length);
|
| + }
|
| +
|
| // /**
|
| // * Returns the text of the given [AstNode] in the unit.
|
| // */
|
| @@ -940,3 +1306,11 @@ class _EnclosingElementFinder {
|
| }
|
| }
|
| }
|
| +
|
| +class _InsertionDescription {
|
| + final int offset;
|
| + final bool insertEmptyLineBefore;
|
| + final bool insertEmptyLineAfter;
|
| + _InsertionDescription(
|
| + this.offset, this.insertEmptyLineBefore, this.insertEmptyLineAfter);
|
| +}
|
|
|