blob: 13d807c94d83e953208d6966e5aad1d8b90657b9 [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/analysis_server.dart';
import 'package:analysis_server/src/services/completion/dart/utilities.dart';
import 'package:analyzer/src/test_utilities/resource_provider_mixin.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:meta/meta.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../services/completion/dart/completion_contributor_util.dart';
import 'impl/completion_driver.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(BasicCompletionTest);
defineReflectiveTests(CompletionWithSuggestionsTest);
});
}
abstract class AbstractCompletionDriverTest with ResourceProviderMixin {
late CompletionDriver driver;
Map<String, String> packageRoots = {};
late List<CompletionSuggestion> suggestions;
String get projectName => 'project';
String get projectPath => '/$projectName';
AnalysisServerOptions get serverOptions => AnalysisServerOptions();
bool get supportsAvailableSuggestions;
String get testFilePath => '$projectPath/bin/test.dart';
Future<void> addProjectFile(String relativePath, String content) async {
newFile('$projectPath/$relativePath', content: content);
// todo (pq): handle more than lib
expect(relativePath, startsWith('lib/'));
var packageRelativePath = relativePath.substring(4);
var uriStr = 'package:$projectName/$packageRelativePath';
await driver.waitForSetWithUri(uriStr);
}
Future<List<CompletionSuggestion>> addTestFile(String content,
{int? offset}) async {
driver.addTestFile(content, offset: offset);
await getSuggestions();
// For sanity, ensure that there are no errors recorded for project files
// since that may lead to unexpected results.
_assertNoErrorsInProjectFiles();
return suggestions;
}
void assertNoSuggestion({
required String completion,
ElementKind? element,
CompletionSuggestionKind? kind,
String? file,
}) {
expect(
suggestionsWith(
completion: completion,
element: element,
kind: kind,
file: file,
),
isEmpty);
}
void assertSuggestion({
bool allowMultiple = false,
required String completion,
ElementKind? element,
CompletionSuggestionKind? kind,
String? file,
}) {
expect(
suggestionWith(
allowMultiple: allowMultiple,
completion: completion,
element: element,
kind: kind,
file: file,
),
isNotNull);
}
void assertSuggestions({
required String completion,
ElementKind? element,
CompletionSuggestionKind? kind,
String? file,
}) {
expect(
suggestionWith(
completion: completion,
element: element,
kind: kind,
file: file,
),
isNotNull);
}
Future<List<CompletionSuggestion>> getSuggestions() async {
if (supportsAvailableSuggestions) {
// todo (pq): consider moving
const internalLibs = [
'dart:async2',
'dart:_interceptors',
'dart:_internal',
];
for (var lib in driver.sdk.sdkLibraries) {
var uri = lib.shortName;
if (!internalLibs.contains(uri)) {
await driver.waitForSetWithUri(uri);
}
}
}
suggestions = await driver.getSuggestions();
return suggestions;
}
/// Display sorted suggestions.
void printSuggestions() {
suggestions.sort(completionComparator);
for (var s in suggestions) {
print(
'[${s.relevance}] ${s.completion} • ${s.element?.kind.name ?? ""} ${s.kind.name} ${s.element?.location?.file ?? ""}');
}
}
@mustCallSuper
void setUp() {
driver = CompletionDriver(
supportsAvailableSuggestions: supportsAvailableSuggestions,
projectPath: projectPath,
testFilePath: testFilePath,
resourceProvider: resourceProvider,
serverOptions: serverOptions,
);
driver.createProject(packageRoots: packageRoots);
newPubspecYamlFile(projectPath, '');
newDotPackagesFile(projectPath, content: '''
project:${toUri('$projectPath/lib')}
''');
// todo (pq): add logic (possibly to driver) that waits for SDK suggestions
}
SuggestionMatcher suggestionHas({
required String completion,
ElementKind? element,
CompletionSuggestionKind? kind,
String? file,
}) =>
(CompletionSuggestion s) {
if (s.completion != completion) {
return false;
}
if (element != null && s.element?.kind != element) {
return false;
}
if (kind != null && s.kind != kind) {
return false;
}
if (file != null && s.element?.location?.file != convertPath(file)) {
return false;
}
return true;
};
Iterable<CompletionSuggestion> suggestionsWith({
required String completion,
ElementKind? element,
CompletionSuggestionKind? kind,
String? file,
}) =>
suggestions.where(suggestionHas(
completion: completion, element: element, kind: kind, file: file));
CompletionSuggestion suggestionWith({
bool allowMultiple = false,
required String completion,
ElementKind? element,
CompletionSuggestionKind? kind,
String? file,
}) {
final matches = suggestionsWith(
completion: completion, element: element, kind: kind, file: file);
if (!allowMultiple) {
expect(matches, hasLength(1));
}
return matches.first;
}
void _assertNoErrorsInProjectFiles() {
var errors = <AnalysisError>[];
driver.filesErrors.forEach((file, fileErrors) {
// Completion test files are likely to be incomplete and so may have
// errors
if (file != convertPath(testFilePath)) {
errors.addAll(fileErrors);
}
});
expect(errors, isEmpty);
}
}
@reflectiveTest
class BasicCompletionTest extends AbstractCompletionDriverTest {
@override
bool get supportsAvailableSuggestions => false;
/// Duplicates (and potentially replaces DeprecatedMemberRelevanceTest).
Future<void> test_deprecated_member_relevance() async {
await addTestFile('''
class A {
void a1() { }
@deprecated
void a2() { }
}
void f() {
var a = A();
a.^
}
''');
expect(
suggestionWith(
completion: 'a2',
element: ElementKind.METHOD,
kind: CompletionSuggestionKind.INVOCATION)
.relevance,
lessThan(suggestionWith(
completion: 'a1',
element: ElementKind.METHOD,
kind: CompletionSuggestionKind.INVOCATION)
.relevance));
}
}
@reflectiveTest
class CompletionWithSuggestionsTest extends AbstractCompletionDriverTest {
@override
bool get supportsAvailableSuggestions => true;
@override
String get testFilePath => '$projectPath/lib/test.dart';
Future<void> test_project_filterImports_defaultConstructor() async {
await addProjectFile('lib/a.dart', r'''
class A {}
''');
await addProjectFile('lib/b.dart', r'''
export 'a.dart';
''');
await addTestFile('''
import 'a.dart';
void f() {
^
}
''');
// TODO(brianwilkerson) There should be a single suggestion here after we
// figure out how to stop the duplication.
assertSuggestion(
allowMultiple: true,
completion: 'A',
element: ElementKind.CONSTRUCTOR,
kind: CompletionSuggestionKind.INVOCATION);
}
/// See: https://github.com/dart-lang/sdk/issues/40620
Future<void> test_project_filterImports_enumValues() async {
await addProjectFile('lib/a.dart', r'''
enum E {
e,
}
''');
await addProjectFile('lib/b.dart', r'''
export 'a.dart';
''');
await addTestFile('''
import 'a.dart';
void f() {
^
}
''');
// TODO(brianwilkerson) There should be a single suggestion here after we
// figure out how to stop the duplication.
assertSuggestion(
allowMultiple: true,
completion: 'E.e',
element: ElementKind.ENUM_CONSTANT,
kind: CompletionSuggestionKind.INVOCATION);
}
/// See: https://github.com/dart-lang/sdk/issues/40620
Future<void> test_project_filterImports_namedConstructors() async {
await addProjectFile('lib/a.dart', r'''
class A {
A.a();
}
''');
await addProjectFile('lib/b.dart', r'''
export 'a.dart';
''');
await addTestFile('''
import 'a.dart';
void f() {
^
}
''');
// TODO(brianwilkerson) There should be a single suggestion here after we
// figure out how to stop the duplication.
assertSuggestion(
allowMultiple: true,
completion: 'A.a',
element: ElementKind.CONSTRUCTOR,
kind: CompletionSuggestionKind.INVOCATION);
}
Future<void> test_project_filterMultipleImports() async {
await addProjectFile('lib/a.dart', r'''
class A {}
''');
await addProjectFile('lib/b.dart', r'''
export 'a.dart';
''');
await addTestFile('''
import 'a.dart';
import 'b.dart';
void f() {
^
}
''');
assertSuggestion(
completion: 'A',
element: ElementKind.CLASS,
kind: CompletionSuggestionKind.INVOCATION);
}
Future<void> test_project_lib() async {
await addProjectFile('lib/a.dart', r'''
class A {}
enum E {
e,
}
extension Ex on A {}
mixin M { }
typedef T = Function(Object);
var v = 0;
''');
await addTestFile('''
void f() {
^
}
''');
assertSuggestion(
completion: 'A',
element: ElementKind.CONSTRUCTOR,
kind: CompletionSuggestionKind.INVOCATION);
assertSuggestion(
completion: 'A',
element: ElementKind.CLASS,
kind: CompletionSuggestionKind.INVOCATION);
assertSuggestion(
completion: 'E',
element: ElementKind.ENUM,
kind: CompletionSuggestionKind.INVOCATION);
assertSuggestion(
completion: 'Ex',
element: ElementKind.EXTENSION,
kind: CompletionSuggestionKind.INVOCATION);
assertSuggestion(
completion: 'M',
element: ElementKind.MIXIN,
kind: CompletionSuggestionKind.INVOCATION);
assertSuggestion(
completion: 'T',
element: ElementKind.FUNCTION_TYPE_ALIAS,
kind: CompletionSuggestionKind.INVOCATION);
assertSuggestion(
completion: 'v',
element: ElementKind.TOP_LEVEL_VARIABLE,
kind: CompletionSuggestionKind.INVOCATION);
}
Future<void> test_project_lib_fields_class() async {
await addProjectFile('lib/a.dart', r'''
class A {
int f = 0;
}
''');
await addTestFile('''
void m() {
^
}
''');
assertNoSuggestion(completion: 'f');
}
Future<void> test_project_lib_fields_static() async {
await addProjectFile('lib/a.dart', r'''
class A {
static int f = 0;
}
''');
await addTestFile('''
void f() {
^
}
''');
assertSuggestion(
completion: 'A.f',
element: ElementKind.FIELD,
kind: CompletionSuggestionKind.INVOCATION);
}
Future<void> test_project_lib_getters_class() async {
await addProjectFile('lib/a.dart', r'''
class A {
int get g => 0;
}
''');
await addTestFile('''
void f() {
^
}
''');
assertNoSuggestion(completion: 'g');
}
Future<void> test_project_lib_getters_static() async {
await addProjectFile('lib/a.dart', r'''
class A {
static int get g => 0;
}
''');
await addTestFile('''
void f() {
^
}
''');
assertSuggestion(
completion: 'A.g',
element: ElementKind.GETTER,
kind: CompletionSuggestionKind.INVOCATION);
}
/// See: https://github.com/dart-lang/sdk/issues/40626
Future<void> test_project_lib_getters_topLevel() async {
await addProjectFile('lib/a.dart', r'''
int get g => 0;
''');
await addTestFile('''
void f() {
^
}
''');
assertSuggestion(
completion: 'g',
element: ElementKind.GETTER,
kind: CompletionSuggestionKind.INVOCATION);
}
@failingTest
Future<void> test_project_lib_multipleExports() async {
await addProjectFile('lib/a.dart', r'''
class A {}
''');
await addProjectFile('lib/b.dart', r'''
export 'a.dart';
''');
await addTestFile('''
void f() {
^
}
''');
// Should only have one suggestion.
assertSuggestion(
completion: 'A',
element: ElementKind.CONSTRUCTOR,
kind: CompletionSuggestionKind.INVOCATION);
}
Future<void> test_project_lib_setters_class() async {
await addProjectFile('lib/a.dart', r'''
class A {
set s(int s) {}
}
''');
await addTestFile('''
void f() {
^
}
''');
assertNoSuggestion(completion: 's');
}
Future<void> test_project_lib_setters_static() async {
await addProjectFile('lib/a.dart', r'''
class A {
static set g(int g) {}
}
''');
await addTestFile('''
void f() {
^
}
''');
assertNoSuggestion(completion: 'A.g');
}
/// See: https://github.com/dart-lang/sdk/issues/40626
Future<void> test_project_lib_setters_topLevel() async {
await addProjectFile('lib/a.dart', r'''
set s(int s) {}
''');
await addTestFile('''
void f() {
^
}
''');
assertSuggestion(
completion: 's',
element: ElementKind.SETTER,
kind: CompletionSuggestionKind.INVOCATION);
}
@FailingTest(issue: 'https://github.com/dart-lang/sdk/issues/38739')
Future<void>
test_project_suggestionRelevance_constructorParameterType() async {
await addProjectFile('lib/a.dart', r'''
import 'b.dart';
class A {
A.b({O o});
}
''');
await addProjectFile('lib/b.dart', r'''
class O { }
''');
await addTestFile('''
import 'a.dart';
void f(List<String> args) {
var a = A.b(o: ^)
}
''');
expect(
suggestionWith(
completion: 'O',
element: ElementKind.CONSTRUCTOR,
kind: CompletionSuggestionKind.INVOCATION)
.relevance,
greaterThan(suggestionWith(
completion: 'args',
element: ElementKind.PARAMETER,
kind: CompletionSuggestionKind.INVOCATION)
.relevance));
}
Future<void> test_project_suggestionRelevance_constructorsAndTypes() async {
await addProjectFile('lib/a.dart', r'''
class A { }
''');
await addTestFile('''
import 'a.dart';
void f(List<String> args) {
var a = ^
}
''');
expect(
suggestionWith(
completion: 'A',
element: ElementKind.CONSTRUCTOR,
kind: CompletionSuggestionKind.INVOCATION)
.relevance,
greaterThan(suggestionWith(
completion: 'A',
element: ElementKind.CLASS,
kind: CompletionSuggestionKind.INVOCATION)
.relevance));
}
/// See: https://github.com/dart-lang/sdk/issues/35529
Future<void> test_project_suggestMixins() async {
await addProjectFile('lib/a.dart', r'''
mixin M { }
class A { }
''');
await addTestFile('''
class C extends Object with ^
''');
assertSuggestion(
completion: 'M',
element: ElementKind.MIXIN,
kind: CompletionSuggestionKind.INVOCATION);
assertSuggestion(
completion: 'A',
element: ElementKind.CLASS,
kind: CompletionSuggestionKind.INVOCATION);
}
Future<void> test_sdk_lib_future_isNotDuplicated() async {
await addTestFile('''
void f() {
^
}
''');
// TODO(brianwilkerson) There should be a single suggestion here after we
// figure out how to stop the duplication.
expect(
suggestionsWith(
completion: 'Future.value',
file: '/sdk/lib/async/async.dart',
element: ElementKind.CONSTRUCTOR,
kind: CompletionSuggestionKind.INVOCATION),
hasLength(2));
}
Future<void> test_sdk_lib_suggestions() async {
await addTestFile('''
void f() {
^
}
''');
// A kind-filtered set of SDK suggestions.
// Constructors should be visible.
assertSuggestion(
completion: 'Timer',
file: '/sdk/lib/async/async.dart',
element: ElementKind.CONSTRUCTOR,
kind: CompletionSuggestionKind.INVOCATION);
// But not methods.
assertNoSuggestion(
// dart:async (StreamSubscription)
completion: 'asFuture',
element: ElementKind.METHOD,
kind: CompletionSuggestionKind.INVOCATION);
// + Functions.
assertSuggestion(
completion: 'print',
file: '/sdk/lib/core/core.dart',
element: ElementKind.FUNCTION,
kind: CompletionSuggestionKind.INVOCATION);
assertSuggestion(
completion: 'tan',
file: '/sdk/lib/math/math.dart',
element: ElementKind.FUNCTION,
kind: CompletionSuggestionKind.INVOCATION);
// + Classes.
assertSuggestion(
completion: 'HashMap',
file: '/sdk/lib/collection/collection.dart',
element: ElementKind.CLASS,
kind: CompletionSuggestionKind.INVOCATION);
// + Top level variables.
assertSuggestion(
completion: 'pi',
file: '/sdk/lib/math/math.dart',
element: ElementKind.TOP_LEVEL_VARIABLE,
kind: CompletionSuggestionKind.INVOCATION);
// (No typedefs, enums, extensions defined in the Mock SDK.)
}
}