diff --git a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart index ef5cd72c4ef..3849d5aef49 100644 --- a/pkg/analysis_server/lib/src/services/correction/fix_internal.dart +++ b/pkg/analysis_server/lib/src/services/correction/fix_internal.dart @@ -46,6 +46,7 @@ import 'package:analyzer/src/generated/java_core.dart'; import 'package:analyzer/src/generated/parser.dart'; import 'package:analyzer/src/generated/source.dart'; import 'package:analyzer/src/generated/utilities_dart.dart'; +import 'package:analyzer_plugin/src/utilities/string_utilities.dart'; import 'package:analyzer_plugin/utilities/range_factory.dart'; import 'package:path/path.dart'; diff --git a/pkg/analysis_server/lib/src/services/correction/name_suggestion.dart b/pkg/analysis_server/lib/src/services/correction/name_suggestion.dart index 12d8c2950a6..4c10fec3c1e 100644 --- a/pkg/analysis_server/lib/src/services/correction/name_suggestion.dart +++ b/pkg/analysis_server/lib/src/services/correction/name_suggestion.dart @@ -2,12 +2,11 @@ // 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. -library services.src.correction.name_suggestion; - import 'package:analysis_server/src/services/correction/strings.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer_plugin/src/utilities/string_utilities.dart'; List _KNOWN_METHOD_NAME_PREFIXES = ['get', 'is', 'to']; diff --git a/pkg/analysis_server/lib/src/services/correction/strings.dart b/pkg/analysis_server/lib/src/services/correction/strings.dart index 1c38cbf60fa..f7fb39e7652 100644 --- a/pkg/analysis_server/lib/src/services/correction/strings.dart +++ b/pkg/analysis_server/lib/src/services/correction/strings.dart @@ -2,10 +2,10 @@ // 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. -library services.src.correction.strings; - import 'dart:math'; +import 'package:analyzer_plugin/src/utilities/string_utilities.dart'; + /** * "$" */ @@ -158,46 +158,6 @@ int findCommonSuffix(String a, String b) { return n; } -/** - * Returns a list of words for the given camel case string. - * - * 'getCamelWords' => ['get', 'Camel', 'Words'] - * 'getHTMLText' => ['get', 'HTML', 'Text'] - */ -List getCamelWords(String str) { - if (str == null || str.isEmpty) { - return []; - } - List parts = []; - bool wasLowerCase = false; - bool wasUpperCase = false; - int wordStart = 0; - for (int i = 0; i < str.length; i++) { - int c = str.codeUnitAt(i); - var newLowerCase = isLowerCase(c); - var newUpperCase = isUpperCase(c); - // myWord - // | ^ - if (wasLowerCase && newUpperCase) { - parts.add(str.substring(wordStart, i)); - wordStart = i; - } - // myHTMLText - // | ^ - if (wasUpperCase && - newUpperCase && - i + 1 < str.length && - isLowerCase(str.codeUnitAt(i + 1))) { - parts.add(str.substring(wordStart, i)); - wordStart = i; - } - wasLowerCase = newLowerCase; - wasUpperCase = newUpperCase; - } - parts.add(str.substring(wordStart)); - return parts; -} - /** * Checks if [str] is `null`, empty or is whitespace. */ @@ -215,10 +175,6 @@ bool isDigit(int c) { return c >= 0x30 && c <= 0x39; } -bool isEmpty(String str) { - return str == null || str.isEmpty; -} - bool isEOL(int c) { return c == 0x0D || c == 0x0A; } @@ -231,16 +187,8 @@ bool isLetterOrDigit(int c) { return isLetter(c) || isDigit(c); } -bool isLowerCase(int c) { - return c >= 0x61 && c <= 0x7A; -} - bool isSpace(int c) => c == 0x20 || c == 0x09; -bool isUpperCase(int c) { - return c >= 0x41 && c <= 0x5A; -} - bool isWhitespace(int c) { return isSpace(c) || isEOL(c); } @@ -262,16 +210,6 @@ String removeEnd(String str, String remove) { return str; } -String removeStart(String str, String remove) { - if (isEmpty(str) || isEmpty(remove)) { - return str; - } - if (str.startsWith(remove)) { - return str.substring(remove.length); - } - return str; -} - String repeat(String s, int n) { StringBuffer sb = new StringBuffer(); for (int i = 0; i < n; i++) { diff --git a/pkg/analysis_server/lib/src/services/correction/util.dart b/pkg/analysis_server/lib/src/services/correction/util.dart index 831c4b8a5f4..721c63cc22d 100644 --- a/pkg/analysis_server/lib/src/services/correction/util.dart +++ b/pkg/analysis_server/lib/src/services/correction/util.dart @@ -2,8 +2,6 @@ // 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. -library services.src.correction.util; - import 'dart:math'; import 'package:analysis_server/protocol/protocol_generated.dart' @@ -22,6 +20,7 @@ import 'package:analyzer/src/dart/scanner/scanner.dart'; import 'package:analyzer/src/generated/engine.dart'; import 'package:analyzer/src/generated/resolver.dart'; import 'package:analyzer/src/generated/source.dart'; +import 'package:analyzer_plugin/src/utilities/string_utilities.dart'; import 'package:analyzer_plugin/utilities/range_factory.dart'; import 'package:path/path.dart'; diff --git a/pkg/analysis_server/lib/src/services/refactoring/naming_conventions.dart b/pkg/analysis_server/lib/src/services/refactoring/naming_conventions.dart index b622c32db89..7691fbb32fe 100644 --- a/pkg/analysis_server/lib/src/services/refactoring/naming_conventions.dart +++ b/pkg/analysis_server/lib/src/services/refactoring/naming_conventions.dart @@ -2,10 +2,9 @@ // 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. -library services.src.refactoring.naming_conventions; - import 'package:analysis_server/src/services/correction/status.dart'; import 'package:analysis_server/src/services/correction/strings.dart'; +import 'package:analyzer_plugin/src/utilities/string_utilities.dart'; import 'package:front_end/src/scanner/token.dart' show Keyword; /** diff --git a/pkg/analysis_server/test/integration/coverage_test.dart b/pkg/analysis_server/test/integration/coverage_test.dart index 6a4f0d43bf2..630813fa4d8 100644 --- a/pkg/analysis_server/test/integration/coverage_test.dart +++ b/pkg/analysis_server/test/integration/coverage_test.dart @@ -4,7 +4,7 @@ import 'dart:io'; -import 'package:analysis_server/src/services/correction/strings.dart'; +import 'package:analyzer_plugin/src/utilities/string_utilities.dart'; import 'package:path/path.dart' as path; import 'package:test/test.dart'; diff --git a/pkg/analysis_server/test/services/correction/strings_test.dart b/pkg/analysis_server/test/services/correction/strings_test.dart index 3f44c7cf72a..6ff5aaad75a 100644 --- a/pkg/analysis_server/test/services/correction/strings_test.dart +++ b/pkg/analysis_server/test/services/correction/strings_test.dart @@ -2,8 +2,6 @@ // 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. -library test.services.correction.strings; - import 'package:analysis_server/src/services/correction/strings.dart'; import 'package:test/test.dart' hide isEmpty; import 'package:test_reflective_loader/test_reflective_loader.dart'; @@ -75,13 +73,6 @@ class StringsTest { expect(findCommonSuffix('123', 'xyz123'), 3); } - void test_getCamelWords() { - expect(getCamelWords(null), []); - expect(getCamelWords(''), []); - expect(getCamelWords('getCamelWords'), ['get', 'Camel', 'Words']); - expect(getCamelWords('getHTMLText'), ['get', 'HTML', 'Text']); - } - void test_isBlank() { expect(isBlank(null), isTrue); expect(isBlank(''), isTrue); @@ -99,13 +90,6 @@ class StringsTest { expect(isDigit('A'.codeUnitAt(0)), isFalse); } - void test_isEmpty() { - expect(isEmpty(null), isTrue); - expect(isEmpty(''), isTrue); - expect(isEmpty('X'), isFalse); - expect(isEmpty(' '), isFalse); - } - void test_isLetter() { for (int c in 'abcdefghijklmnopqrstuvwxyz'.codeUnits) { expect(isLetter(c), isTrue); @@ -131,17 +115,6 @@ class StringsTest { expect(isLetterOrDigit('.'.codeUnitAt(0)), isFalse); } - void test_isLowerCase() { - for (int c in 'abcdefghijklmnopqrstuvwxyz'.codeUnits) { - expect(isLowerCase(c), isTrue); - } - for (int c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.codeUnits) { - expect(isLowerCase(c), isFalse); - } - expect(isLowerCase(' '.codeUnitAt(0)), isFalse); - expect(isLowerCase('0'.codeUnitAt(0)), isFalse); - } - void test_isSpace() { expect(isSpace(' '.codeUnitAt(0)), isTrue); expect(isSpace('\t'.codeUnitAt(0)), isTrue); @@ -151,17 +124,6 @@ class StringsTest { expect(isSpace('A'.codeUnitAt(0)), isFalse); } - void test_isUpperCase() { - for (int c in 'abcdefghijklmnopqrstuvwxyz'.codeUnits) { - expect(isUpperCase(c), isFalse); - } - for (int c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.codeUnits) { - expect(isUpperCase(c), isTrue); - } - expect(isUpperCase(' '.codeUnitAt(0)), isFalse); - expect(isUpperCase('0'.codeUnitAt(0)), isFalse); - } - void test_isWhitespace() { expect(isWhitespace(' '.codeUnitAt(0)), isTrue); expect(isWhitespace('\t'.codeUnitAt(0)), isTrue); @@ -186,13 +148,6 @@ class StringsTest { expect(removeEnd('www.domain.com', '.com'), 'www.domain'); } - void test_removeStart() { - expect(removeStart(null, 'x'), null); - expect(removeStart('abc', null), 'abc'); - expect(removeStart('abcTest', 'abc'), 'Test'); - expect(removeStart('my abcTest', 'abc'), 'my abcTest'); - } - void test_repeat() { expect(repeat('x', 0), ''); expect(repeat('x', 5), 'xxxxx'); diff --git a/pkg/analysis_server/test/services/correction/util_test.dart b/pkg/analysis_server/test/services/correction/util_test.dart index 9927a5c95af..f8f7a5cd280 100644 --- a/pkg/analysis_server/test/services/correction/util_test.dart +++ b/pkg/analysis_server/test/services/correction/util_test.dart @@ -3,9 +3,9 @@ // BSD-style license that can be found in the LICENSE file. import 'package:analysis_server/protocol/protocol_generated.dart'; -import 'package:analysis_server/src/services/correction/strings.dart'; import 'package:analysis_server/src/services/correction/util.dart'; import 'package:analyzer/src/generated/source.dart'; +import 'package:analyzer_plugin/src/utilities/string_utilities.dart'; import 'package:test/test.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; diff --git a/pkg/analyzer/lib/src/generated/source.dart b/pkg/analyzer/lib/src/generated/source.dart index 262bfe3b2d7..4237caf1850 100644 --- a/pkg/analyzer/lib/src/generated/source.dart +++ b/pkg/analyzer/lib/src/generated/source.dart @@ -292,6 +292,14 @@ class LineInfo { } return lineStarts[lineNumber]; } + + /** + * Return the offset of the first character on the line following the line + * containing the given [offset]. + */ + int getOffsetOfLineAfter(int offset) { + return getOffsetOfLine(getLocation(offset).lineNumber + 1); + } } /** diff --git a/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_core.dart b/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_core.dart index 632b888f58f..2105df47d8e 100644 --- a/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_core.dart +++ b/pkg/analyzer_plugin/lib/src/utilities/change_builder/change_builder_core.dart @@ -4,8 +4,8 @@ import 'dart:async'; -import 'package:analysis_server/protocol/protocol_generated.dart'; import 'package:analyzer/src/generated/source.dart'; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.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 a28afbdab95..ee3b35d0c4d 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 _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 excluded, Set result, Iterable 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 excluded, Set 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 alreadyAdded) { if (type != null && @@ -557,21 +599,124 @@ 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 _getCamelWordCombinations(String name) { + List result = []; + List 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 definedNames = getImportNamespace(imp); + for (ImportElement importElement in library.imports) { + Map definedNames = _getImportNamespace(importElement); if (definedNames.containsValue(element)) { - return imp; + return importElement; } } return null; } + /** + * Return the namespace added by the given import [element]. + */ + Map _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]. @@ -582,7 +727,7 @@ class DartEditBuilderImpl extends EditBuilderImpl implements DartEditBuilder { List _getParameterNameSuggestions( Set usedNames, DartType type, Expression expression, int index) { List suggestions = - getVariableNameSuggestionsForExpression(type, expression, usedNames); + _getVariableNameSuggestionsForExpression(type, expression, usedNames); if (suggestions.length != 0) { return suggestions; } @@ -737,6 +882,45 @@ class DartEditBuilderImpl extends EditBuilderImpl implements DartEditBuilder { return sb.toString(); } + /** + * Returns possible names for a variable with the given expected type and + * expression assigned. + */ + List _getVariableNameSuggestionsForExpression(DartType expectedType, + Expression assignedExpression, Set excluded) { + Set 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 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 libraries) { + // Prepare information about existing imports. + LibraryDirective libraryDirective; + List importDirectives = []; + 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 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); +} diff --git a/pkg/analyzer_plugin/lib/src/utilities/string_utilities.dart b/pkg/analyzer_plugin/lib/src/utilities/string_utilities.dart new file mode 100644 index 00000000000..5c92f43e6e6 --- /dev/null +++ b/pkg/analyzer_plugin/lib/src/utilities/string_utilities.dart @@ -0,0 +1,76 @@ +// 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 'package:charcode/ascii.dart'; + +/** + * Returns a list of the words from which the given camel case [string] is + * composed. + * + * 'getCamelWords' => ['get', 'Camel', 'Words'] + * 'getHTMLText' => ['get', 'HTML', 'Text'] + */ +List getCamelWords(String string) { + if (string == null || string.isEmpty) { + return const []; + } + List parts = []; + bool wasLowerCase = false; + bool wasUpperCase = false; + int wordStart = 0; + for (int i = 0; i < string.length; i++) { + int c = string.codeUnitAt(i); + var newLowerCase = isLowerCase(c); + var newUpperCase = isUpperCase(c); + // myWord + // | ^ + if (wasLowerCase && newUpperCase) { + parts.add(string.substring(wordStart, i)); + wordStart = i; + } + // myHTMLText + // | ^ + if (wasUpperCase && + newUpperCase && + i + 1 < string.length && + isLowerCase(string.codeUnitAt(i + 1))) { + parts.add(string.substring(wordStart, i)); + wordStart = i; + } + wasLowerCase = newLowerCase; + wasUpperCase = newUpperCase; + } + parts.add(string.substring(wordStart)); + return parts; +} + +/** + * Return `true` if the given [string] is either `null` or empty. + */ +bool isEmpty(String string) => string == null || string.isEmpty; + +/** + * Return `true` if the given [character] is a lowercase ASCII character. + */ +bool isLowerCase(int character) => character >= $a && character <= $z; + +/** + * Return `true` if the given [character] is an uppercase ASCII character. + */ +bool isUpperCase(int character) => character >= $A && character <= $Z; + +/** + * If the given [string] starts with the text to [remove], then return the + * portion of the string after the text to remove. Otherwise, return the + * original string unmodified. + */ +String removeStart(String string, String remove) { + if (isEmpty(string) || isEmpty(remove)) { + return string; + } + if (string.startsWith(remove)) { + return string.substring(remove.length); + } + return string; +} diff --git a/pkg/analyzer_plugin/lib/utilities/change_builder/change_builder_core.dart b/pkg/analyzer_plugin/lib/utilities/change_builder/change_builder_core.dart index 2b9f928d4f0..33b250c9dc5 100644 --- a/pkg/analyzer_plugin/lib/utilities/change_builder/change_builder_core.dart +++ b/pkg/analyzer_plugin/lib/utilities/change_builder/change_builder_core.dart @@ -4,8 +4,8 @@ import 'dart:async'; -import 'package:analysis_server/protocol/protocol_generated.dart'; import 'package:analyzer/src/generated/source.dart'; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; import 'package:analyzer_plugin/src/utilities/change_builder/change_builder_core.dart'; import 'package:meta/meta.dart'; diff --git a/pkg/analyzer_plugin/pubspec.yaml b/pkg/analyzer_plugin/pubspec.yaml index 938491c458d..b9ef8f61bec 100644 --- a/pkg/analyzer_plugin/pubspec.yaml +++ b/pkg/analyzer_plugin/pubspec.yaml @@ -8,6 +8,7 @@ environment: dependencies: analyzer: ^0.30.0 + charcode: ^1.1.0 html: any path: any pub_semver: any diff --git a/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_core_test.dart b/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_core_test.dart index fe770cb6a44..1cdaee92ed8 100644 --- a/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_core_test.dart +++ b/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_core_test.dart @@ -2,8 +2,8 @@ // 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 'package:analysis_server/protocol/protocol_generated.dart'; import 'package:analyzer/src/generated/source.dart'; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; import 'package:analyzer_plugin/src/utilities/change_builder/change_builder_core.dart'; import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; import 'package:test/test.dart'; diff --git a/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_dart_test.dart b/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_dart_test.dart index 06da87cba31..3adaeca388f 100644 --- a/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_dart_test.dart +++ b/pkg/analyzer_plugin/test/src/utilities/change_builder/change_builder_dart_test.dart @@ -4,7 +4,6 @@ import 'dart:async'; -import 'package:analysis_server/protocol/protocol_generated.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/standard_resolution_map.dart'; import 'package:analyzer/dart/element/element.dart'; @@ -15,6 +14,7 @@ import 'package:analyzer/src/generated/engine.dart'; import 'package:analyzer/src/generated/resolver.dart'; import 'package:analyzer/src/generated/source.dart'; import 'package:analyzer/src/generated/testing/test_type_provider.dart'; +import 'package:analyzer_plugin/protocol/protocol_generated.dart'; import 'package:analyzer_plugin/src/utilities/change_builder/change_builder_dart.dart'; import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dart'; diff --git a/pkg/analyzer_plugin/test/src/utilities/string_utilities_test.dart b/pkg/analyzer_plugin/test/src/utilities/string_utilities_test.dart new file mode 100644 index 00000000000..6bc535055cb --- /dev/null +++ b/pkg/analyzer_plugin/test/src/utilities/string_utilities_test.dart @@ -0,0 +1,59 @@ +// 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 'package:analyzer_plugin/src/utilities/string_utilities.dart'; +import 'package:test/test.dart' hide isEmpty; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(StringUtilitiesTest); + }); +} + +@reflectiveTest +class StringUtilitiesTest { + void test_getCamelWords() { + expect(getCamelWords(null), []); + expect(getCamelWords(''), []); + expect(getCamelWords('getCamelWords'), ['get', 'Camel', 'Words']); + expect(getCamelWords('getHTMLText'), ['get', 'HTML', 'Text']); + } + + void test_isEmpty() { + expect(isEmpty(null), isTrue); + expect(isEmpty(''), isTrue); + expect(isEmpty('X'), isFalse); + expect(isEmpty(' '), isFalse); + } + + void test_isLowerCase() { + for (int c in 'abcdefghijklmnopqrstuvwxyz'.codeUnits) { + expect(isLowerCase(c), isTrue); + } + for (int c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.codeUnits) { + expect(isLowerCase(c), isFalse); + } + expect(isLowerCase(' '.codeUnitAt(0)), isFalse); + expect(isLowerCase('0'.codeUnitAt(0)), isFalse); + } + + void test_isUpperCase() { + for (int c in 'abcdefghijklmnopqrstuvwxyz'.codeUnits) { + expect(isUpperCase(c), isFalse); + } + for (int c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.codeUnits) { + expect(isUpperCase(c), isTrue); + } + expect(isUpperCase(' '.codeUnitAt(0)), isFalse); + expect(isUpperCase('0'.codeUnitAt(0)), isFalse); + } + + void test_removeStart() { + expect(removeStart(null, 'x'), null); + expect(removeStart('abc', null), 'abc'); + expect(removeStart('abcTest', 'abc'), 'Test'); + expect(removeStart('my abcTest', 'abc'), 'my abcTest'); + } +} diff --git a/pkg/analyzer_plugin/test/src/utilities/test_all.dart b/pkg/analyzer_plugin/test/src/utilities/test_all.dart index 1f4ab26fbb8..ce38d063cb9 100644 --- a/pkg/analyzer_plugin/test/src/utilities/test_all.dart +++ b/pkg/analyzer_plugin/test/src/utilities/test_all.dart @@ -5,9 +5,11 @@ import 'package:test_reflective_loader/test_reflective_loader.dart'; import 'change_builder/test_all.dart' as change_builder; +import 'string_utilities_test.dart' as string_utilities; main() { defineReflectiveSuite(() { change_builder.main(); + string_utilities.main(); }, name: 'utilities'); }