blob: b83e4ef85cb9956849d5b529a1b5b562b85ac018 [file] [log] [blame]
// Copyright (c) 2020, 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:analysis_server/src/cider/completion.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/test_utilities/function_ast_visitor.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart'
show CompletionSuggestion, CompletionSuggestionKind, ElementKind;
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'cider_service.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(CiderCompletionComputerTest);
});
}
@reflectiveTest
class CiderCompletionComputerTest extends CiderServiceTest {
final CiderCompletionCache _completionCache = CiderCompletionCache();
late CiderCompletionComputer _computer;
void Function(ResolvedUnitResult)? _testResolvedUnit;
late CiderCompletionResult _completionResult;
late List<CompletionSuggestion> _suggestions;
Future<void> test_compute() async {
await _compute(r'''
class A {}
int a = 0;
void f(int b) {
int c = 0;
^
}
''');
_assertHasCompletion(text: 'a');
_assertHasCompletion(text: 'b');
_assertHasCompletion(text: 'c');
_assertHasClass(text: 'A');
_assertHasClass(text: 'String');
_assertNoClass(text: 'Random');
}
Future<void> test_compute_performance_operations() async {
await _compute(r'''
void f() {
^
}
''');
_assertHasOperationPerformance(['resolution']);
_assertHasOperationPerformance(['suggestions']);
_assertHasOperationPerformance([
'suggestions',
'DartCompletionManager - KeywordContributor',
]);
}
Future<void> test_compute_prefixStart_beforeToken_identifier() async {
await _compute('''
const foo = 0;
class A {
@fo^
}
''');
expect(_completionResult.prefixStart.line, 3);
expect(_completionResult.prefixStart.column, 3);
}
Future<void> test_compute_prefixStart_beforeToken_keyword() async {
await _compute('''
import 'dart:async' h^;
''');
_assertHasKeyword(text: 'hide');
_assertNoKeyword(text: 'show');
expect(_completionResult.prefixStart.line, 0);
expect(_completionResult.prefixStart.column, 20);
}
Future<void> test_compute_prefixStart_identifier() async {
await _compute('''
class A {
String foobar;
}
void f(A a) {
a.foo^
}
''');
expect(_completionResult.prefixStart.line, 5);
expect(_completionResult.prefixStart.column, 4);
}
Future<void> test_compute_prefixStart_noPrefix() async {
await _compute(r'''
import 'dart:math';
^
''');
expect(_completionResult.prefixStart.line, 1);
expect(_completionResult.prefixStart.column, 0);
}
Future<void> test_compute_updateImportedLibrary() async {
var aPath = convertPath('/workspace/dart/test/lib/a.dart');
newFile(aPath, content: r'''
class A {}
''');
var content = r'''
import 'a.dart';
^
''';
await _compute(content);
_assertComputedImportedLibraries([aPath], hasCore: true);
_assertHasClass(text: 'A');
// Repeat the query, still has 'A'.
_createFileResolver();
await _compute(content);
_assertComputedImportedLibraries([]);
_assertHasClass(text: 'A');
// Update the imported library, has 'B', but not 'A'.
newFile(aPath, content: r'''
class B {}
''');
_createFileResolver();
await _compute(content);
_assertComputedImportedLibraries([aPath]);
_assertHasClass(text: 'B');
_assertNoClass(text: 'A');
}
Future<void> test_compute_updateImports() async {
var aPath = convertPath('/workspace/dart/test/lib/a.dart');
newFile(aPath, content: r'''
class A {}
''');
await _compute(r'''
var a = ^;
''');
_assertComputedImportedLibraries([], hasCore: true);
_assertHasClass(text: 'String');
// Repeat the query, still has 'A'.
_createFileResolver();
await _compute(r'''
import 'a.dart';
var a = ^;
''');
_assertComputedImportedLibraries([aPath]);
_assertHasClass(text: 'A');
_assertHasClass(text: 'String');
}
Future<void> test_compute_uriContributor_disabled() async {
newFile('/workspace/dart/test/lib/a.dart', content: '');
await _compute(r'''
import '^';
''');
expect(_suggestions, isEmpty);
}
Future<void> test_filterSort_byPattern_excludeNotMatching() async {
await _compute(r'''
var a = F^;
''');
_assertHasClass(text: 'Future');
_assertNoClass(text: 'String');
}
Future<void> test_filterSort_byPattern_location_beforeMethod() async {
await _compute(r'''
class A {
F^
void foo() {}
}
''');
_assertHasClass(text: 'Future');
_assertNoClass(text: 'String');
}
Future<void> test_filterSort_byPattern_location_functionReturnType() async {
await _compute(r'''
F^ foo() {}
''');
_assertHasClass(text: 'Future');
_assertNoClass(text: 'String');
}
Future<void> test_filterSort_byPattern_location_methodReturnType() async {
await _compute(r'''
class A {
F^ foo() {}
}
''');
_assertHasClass(text: 'Future');
_assertNoClass(text: 'String');
}
Future<void> test_filterSort_byPattern_location_parameterType() async {
await _compute(r'''
void foo(F^ a) {}
''');
_assertHasClass(text: 'Future');
_assertNoClass(text: 'String');
}
Future<void> test_filterSort_byPattern_location_parameterType2() async {
await _compute(r'''
void foo(^a) {}
''');
_assertHasClass(text: 'Future');
_assertHasClass(text: 'String');
}
Future<void> test_filterSort_byPattern_location_statement() async {
await _compute(r'''
void f() {
F^
0;
}
''');
_assertHasClass(text: 'Future');
_assertNoClass(text: 'String');
}
Future<void> test_filterSort_byPattern_preferPrefix() async {
await _compute(r'''
class Foobar {}
class Falcon {}
var a = Fo^;
''');
_assertOrder([
_assertHasClass(text: 'Foobar'),
_assertHasClass(text: 'Falcon'),
]);
}
Future<void> test_filterSort_namedArgument_hasPrefix() async {
await _compute(r'''
void foo({int aaa, int bbb});
void f() {
foo(a^);
}
''');
_assertHasNamedArgument(name: 'aaa');
_assertNoNamedArgument(name: 'bbb');
}
Future<void> test_filterSort_namedArgument_noPrefix() async {
await _compute(r'''
void foo({int aaa, int bbb});
void f() {
foo(^);
}
''');
_assertHasNamedArgument(name: 'aaa');
_assertHasNamedArgument(name: 'bbb');
}
Future<void> test_filterSort_namedArgument_noPrefix_beforeOther() async {
await _compute(r'''
void foo({int aaa = 0, int aab = 0}) {}
voif f() {
foo(
^
aaa: 0,
);
}
''');
_assertHasNamedArgument(name: 'aab');
}
Future<void> test_filterSort_preferLocal() async {
await _compute(r'''
var a = 0;
void f() {
var b = 0;
var v = ^;
}
''');
_assertOrder([
_assertHasLocalVariable(text: 'b'),
_assertHasTopLevelVariable(text: 'a'),
]);
}
Future<void> test_filterSort_sortByName() async {
await _compute(r'''
void f() {
var a = 0;
var b = 0;
var v = ^;
}
''');
_assertOrder([
_assertHasLocalVariable(text: 'b'),
_assertHasLocalVariable(text: 'a'),
]);
}
Future<void> test_limitedResolution_class_constructor_body() async {
_configureToCheckNotResolved(
identifiers: {'print'},
);
await _compute(r'''
class A<T> {
int f = 0;
A(int a) : f = 1 {
^
}
void foo() {
print(0);
}
}
''');
_assertHasClass(text: 'A');
_assertHasClass(text: 'String');
_assertHasConstructor(text: 'A');
_assertHasFunction(text: 'print');
_assertHasMethod(text: 'foo');
_assertHasParameter(text: 'a');
_assertHasTypeParameter(text: 'T');
}
Future<void> test_limitedResolution_class_field_startWithType() async {
_configureToCheckNotResolved(
identifiers: {'print'},
);
await _compute(r'''
class A {
void foo() {
print(0);
}
Str^
}
''');
_assertHasClass(text: 'String');
}
Future<void> test_limitedResolution_class_method_body() async {
_configureToCheckNotResolved(
identifiers: {'print'},
);
await _compute(r'''
class A<T> {
void foo() {
print(0);
}
void bar<U>(int a) {
^
}
}
class B {}
enum E { e }
''');
_assertHasClass(text: 'A');
_assertHasClass(text: 'B');
_assertHasClass(text: 'String');
_assertHasConstructor(text: 'A');
_assertHasConstructor(text: 'B');
_assertHasEnumConstant(text: 'E.e');
_assertHasMethod(text: 'foo');
_assertHasMethod(text: 'bar');
_assertHasParameter(text: 'a');
_assertHasTypeParameter(text: 'T');
_assertHasTypeParameter(text: 'U');
}
Future<void> test_limitedResolution_class_method_body2() async {
_configureToCheckNotResolved(
identifiers: {'print'},
);
await _compute(r'''
class A {
void foo() {}
}
abstract class B {
A get a;
void notResolved() {
print(0);
}
void completionTarget() {
a.^;
}
}
''');
_assertHasGetter(text: 'hashCode');
_assertHasMethod(text: 'foo');
}
Future<void> test_limitedResolution_class_method_parameterType() async {
_configureToCheckNotResolved(
identifiers: {'print'},
);
await _compute(r'''
class A {
void foo() {
print(0);
}
void bar(Str^) {}
}
''');
_assertHasClass(text: 'String');
}
Future<void>
test_limitedResolution_class_method_returnType_hasPartial() async {
_configureToCheckNotResolved(
identifiers: {'print'},
);
await _compute(r'''
class A {
void foo() {
print(0);
}
Str^ bar() {}
}
''');
_assertHasClass(text: 'String');
}
Future<void> test_limitedResolution_hasPart() async {
newFile('/workspace/dart/test/lib/a.dart', content: r'''
class A {}
''');
await _compute(r'''
part 'a.dart';
^
''');
_assertHasClass(text: 'int');
_assertHasClass(text: 'A');
}
Future<void> test_limitedResolution_inPart_partOfName() async {
newFile('/workspace/dart/test/lib/a.dart', content: r'''
library my_lib;
part 'test.dart';
class A {}
''');
await _compute(r'''
part of my_lib;
^
''');
_assertHasClass(text: 'int');
_assertHasClass(text: 'A');
}
Future<void> test_limitedResolution_inPart_partOfUri() async {
newFile('/workspace/dart/test/lib/a.dart', content: r'''
part 'test.dart';
class A {}
''');
await _compute(r'''
part of 'a.dart';
^
''');
_assertHasClass(text: 'int');
_assertHasClass(text: 'A');
}
Future<void> test_limitedResolution_inPart_partOfUri_doesNotExist() async {
await _compute(r'''
part of 'a.dart';
class A {}
^
''');
_assertHasClass(text: 'int');
_assertHasClass(text: 'A');
}
Future<void> test_limitedResolution_mixin_method_body() async {
_configureToCheckNotResolved(
identifiers: {'print'},
);
await _compute(r'''
class A {
void foo() {}
}
mixin M {
A get a;
void notResolved() {
print(0);
}
void completionTarget() {
a.^;
}
}
''');
_assertHasGetter(text: 'hashCode');
_assertHasMethod(text: 'foo');
}
Future<void> test_limitedResolution_path_hasSpace() async {
testPath = convertPath('/workspace/dart/test name/lib/a.dart');
await _compute(r'''
class A {}
^
''');
_assertHasClass(text: 'int');
_assertHasClass(text: 'A');
}
Future<void> test_limitedResolution_unit_function_body() async {
_configureToCheckNotResolved(
identifiers: {'print'},
);
await _compute(r'''
void foo() {
print(0);
}
void bar(int a) {
^
}
''');
_assertHasFunction(text: 'foo');
_assertHasParameter(text: 'a');
}
Future<void> test_localTypeInference() async {
await _compute(r'''
void foo() {
var a = 0;
a.^
}
''');
_assertHasGetter(text: 'isEven');
}
Future<void> test_warmUp_cachesImportedLibraries() async {
var aPath = convertPath('/workspace/dart/test/lib/a.dart');
newFile(aPath, content: r'''
class A {}
''');
var bPath = convertPath('/workspace/dart/test/lib/b.dart');
newFile(bPath, content: r'''
import 'a.dart';
''');
// Pre-cache `a.dart` using import in `b.dart`
await _newComputer().warmUp([bPath]);
_assertComputedImportedLibraries([aPath], hasCore: true);
// Now we complete in `test.dart`, and `a.dart` is already cached.
await _compute(r'''
import 'a.dart';
^
''');
_assertComputedImportedLibraries([]);
_assertHasClass(text: 'A');
}
void _assertComputedImportedLibraries(
List<String> expected, {
bool hasCore = false,
}) {
expected = expected.map(convertPath).toList();
if (hasCore) {
var corePath = convertPath('/sdk/lib/core/core.dart');
expected.add(corePath);
}
expect(
_computer.computedImportedLibraries,
unorderedEquals(expected),
);
}
CompletionSuggestion _assertHasClass({required String text}) {
var matching = _matchingCompletions(
text: text,
elementKind: ElementKind.CLASS,
);
expect(matching, hasLength(1), reason: 'Expected exactly one completion');
return matching.single;
}
void _assertHasCompletion({required String text}) {
var matching = _matchingCompletions(text: text);
expect(matching, hasLength(1), reason: 'Expected exactly one completion');
}
CompletionSuggestion _assertHasConstructor({required String text}) {
var matching = _matchingCompletions(
text: text,
elementKind: ElementKind.CONSTRUCTOR,
);
expect(matching, hasLength(1), reason: 'Expected exactly one completion');
return matching.single;
}
CompletionSuggestion _assertHasEnumConstant({required String text}) {
var matching = _matchingCompletions(
text: text,
elementKind: ElementKind.ENUM_CONSTANT,
);
expect(matching, hasLength(1), reason: 'Expected exactly one completion');
return matching.single;
}
CompletionSuggestion _assertHasFunction({required String text}) {
var matching = _matchingCompletions(
text: text,
elementKind: ElementKind.FUNCTION,
);
expect(matching, hasLength(1), reason: 'Expected exactly one completion');
return matching.single;
}
CompletionSuggestion _assertHasGetter({required String text}) {
var matching = _matchingCompletions(
text: text,
elementKind: ElementKind.GETTER,
);
expect(matching, hasLength(1), reason: 'Expected exactly one completion');
return matching.single;
}
CompletionSuggestion _assertHasKeyword({required String text}) {
var matching = _matchingKeywordCompletions(text: text);
expect(matching, hasLength(1), reason: 'Expected exactly one completion');
return matching.single;
}
CompletionSuggestion _assertHasLocalVariable({required String text}) {
var matching = _matchingCompletions(
text: text,
elementKind: ElementKind.LOCAL_VARIABLE,
);
expect(
matching,
hasLength(1),
reason: 'Expected exactly one completion in $_suggestions',
);
return matching.single;
}
CompletionSuggestion _assertHasMethod({required String text}) {
var matching = _matchingCompletions(
text: text,
elementKind: ElementKind.METHOD,
);
expect(matching, hasLength(1), reason: 'Expected exactly one completion');
return matching.single;
}
CompletionSuggestion _assertHasNamedArgument({required String name}) {
var matching = _matchingNamedArgumentSuggestions(name: name);
expect(matching, hasLength(1), reason: 'Expected exactly one completion');
return matching.single;
}
void _assertHasOperationPerformance(List<String> path) {
var current = _completionResult.performance.operations;
for (var name in path) {
var child = current.getChild(name);
expect(child, isNotNull, reason: "No '$name' in $current");
current = child!;
}
}
CompletionSuggestion _assertHasParameter({required String text}) {
var matching = _matchingCompletions(
text: text,
elementKind: ElementKind.PARAMETER,
);
expect(matching, hasLength(1), reason: 'Expected exactly one completion');
return matching.single;
}
CompletionSuggestion _assertHasTopLevelVariable({required String text}) {
var matching = _matchingCompletions(
text: text,
elementKind: ElementKind.TOP_LEVEL_VARIABLE,
);
expect(
matching,
hasLength(1),
reason: 'Expected exactly one completion in $_suggestions',
);
return matching.single;
}
CompletionSuggestion _assertHasTypeParameter({required String text}) {
var matching = _matchingCompletions(
text: text,
elementKind: ElementKind.TYPE_PARAMETER,
);
expect(matching, hasLength(1), reason: 'Expected exactly one completion');
return matching.single;
}
void _assertNoClass({required String text}) {
var matching = _matchingCompletions(
text: text,
elementKind: ElementKind.CLASS,
);
expect(matching, isEmpty, reason: 'Expected zero completions');
}
void _assertNoKeyword({required String text}) {
var matching = _matchingKeywordCompletions(text: text);
expect(matching, isEmpty, reason: 'Expected zero completions');
}
void _assertNoNamedArgument({required String name}) {
var matching = _matchingNamedArgumentSuggestions(name: name);
expect(matching, isEmpty, reason: 'Expected zero completions');
}
void _assertOrder(List<CompletionSuggestion> suggestions) {
var lastIndex = -2;
for (var suggestion in suggestions) {
var index = _suggestions.indexOf(suggestion);
expect(index, isNonNegative, reason: '$suggestion');
expect(index, greaterThan(lastIndex), reason: '$suggestion');
lastIndex = index;
}
}
Future _compute(String content) async {
var context = _updateFile(content);
_completionResult = await _newComputer().compute(
path: convertPath(testPath),
line: context.line,
column: context.character,
testResolvedUnit: _testResolvedUnit,
);
_suggestions = _completionResult.suggestions;
}
/// Configure the [CiderCompletionComputer] to check that when resolving
/// for completion we don't resolve unnecessary node.
void _configureToCheckNotResolved({required Set<String> identifiers}) {
_testResolvedUnit = (resolvedUnitResult) {
var unit = resolvedUnitResult.unit!;
unit.accept(
FunctionAstVisitor(
simpleIdentifier: (node) {
if (identifiers.contains(node.name) && node.staticElement != null) {
fail('Unexpectedly resolved node: $node');
}
},
),
);
};
}
/// TODO(scheglov) Implement incremental updating
void _createFileResolver() {
createFileResolver();
}
List<CompletionSuggestion> _matchingCompletions({
required String text,
ElementKind? elementKind,
}) {
return _suggestions.where((e) {
if (e.completion != text) {
return false;
}
if (elementKind != null && e.element!.kind != elementKind) {
return false;
}
return true;
}).toList();
}
List<CompletionSuggestion> _matchingKeywordCompletions({
required String text,
}) {
return _suggestions.where((e) {
if (e.completion != text) {
return false;
}
return e.kind == CompletionSuggestionKind.KEYWORD;
}).toList();
}
List<CompletionSuggestion> _matchingNamedArgumentSuggestions({
required String name,
}) {
return _suggestions.where((e) {
if (e.kind != CompletionSuggestionKind.NAMED_ARGUMENT) {
return false;
}
if (!e.completion.startsWith('$name:')) {
return false;
}
return true;
}).toList();
}
CiderCompletionComputer _newComputer() {
return _computer = CiderCompletionComputer(
logger,
_completionCache,
fileResolver,
);
}
_CompletionContext _updateFile(String content) {
var offset = content.indexOf('^');
expect(offset, isPositive, reason: 'Expected to find ^');
expect(content.indexOf('^', offset + 1), -1, reason: 'Expected only one ^');
var lineInfo = LineInfo.fromContent(content);
var location = lineInfo.getLocation(offset);
content = content.substring(0, offset) + content.substring(offset + 1);
newFile(testPath, content: content);
return _CompletionContext(
content,
offset,
location.lineNumber - 1,
location.columnNumber - 1,
);
}
}
class _CompletionContext {
final String content;
final int offset;
final int line;
final int character;
_CompletionContext(this.content, this.offset, this.line, this.character);
}