| // 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:analyzer_plugin/protocol/protocol_common.dart'; |
| import 'package:test/test.dart'; |
| import 'package:test_reflective_loader/test_reflective_loader.dart'; |
| |
| import '../analysis_server_base.dart'; |
| import '../services/completion/dart/completion_check.dart'; |
| import '../services/completion/dart/completion_printer.dart' as printer; |
| import '../services/completion/dart/text_expectations.dart'; |
| import 'impl/completion_driver.dart'; |
| |
| void main() { |
| defineReflectiveSuite(() { |
| defineReflectiveTests(BasicCompletionTest); |
| defineReflectiveTests(CompletionWithSuggestionsTest); |
| }); |
| } |
| |
| typedef SuggestionMatcher = bool Function(CompletionSuggestion suggestion); |
| |
| abstract class AbstractCompletionDriverTest |
| extends PubPackageAnalysisServerTest { |
| late CompletionDriver driver; |
| |
| late List<CompletionSuggestion> suggestions; |
| |
| late CompletionResponseForTesting response; |
| |
| /// The configuration used by [assertResponse] to limit the number of |
| /// suggestions that will be compared by a test. The reasons for the filter |
| /// are |
| /// - to prevent uninteresting changes to the SDK from breaking the completion |
| /// tests, and |
| /// - to keep the expected response text shorter. |
| /// |
| /// The default filter, initialized in [setUp], prints |
| /// - identifier suggestions consisting of a single letter followed by one or |
| /// more digits, |
| /// - identifier suggestions that are in the set of [allowedIdentifiers], and |
| /// - non-identifier suggestions. |
| /// |
| /// Tests can override this configuration to change the set of suggestions to |
| /// be printed. |
| late printer.Configuration printerConfiguration; |
| |
| /// A set of identifiers that will be included in the printed version of the |
| /// suggestions. Individual tests can replace the default set. |
| Set<String> allowedIdentifiers = const {}; |
| |
| /// A set of completion kinds that should be included in the printed version |
| /// of the suggestions. Individual tests can replace the default set. |
| Set<CompletionSuggestionKind> allowedKinds = {}; |
| |
| /// Whether keywords should be included in the text to be compared. |
| bool includeKeywords = true; |
| |
| /// Return `true` if closures (suggestions starting with a left paren) should |
| /// be included in the text to be compared. |
| bool get includeClosures => false; |
| |
| /// Return `true` if overrides should be included in the text to be compared. |
| bool get includeOverrides => true; |
| |
| @override |
| Future<List<CompletionSuggestion>> addTestFile(String content, |
| {int? offset}) async { |
| driver.addTestFile(content, offset: offset); |
| |
| // Wait after adding the test file, this might affect diagnostics. |
| await pumpEventQueue(times: 1000); |
| |
| // For sanity, ensure that there are no errors recorded for project files |
| // since that may lead to unexpected results. |
| _assertNoErrorsInProjectFiles(); |
| |
| await getSuggestions(); |
| return suggestions; |
| } |
| |
| void assertNoSuggestion({ |
| required String completion, |
| ElementKind? element, |
| CompletionSuggestionKind? kind, |
| String? file, |
| }) { |
| expect( |
| suggestionsWith( |
| completion: completion, |
| element: element, |
| kind: kind, |
| file: file, |
| ), |
| isEmpty); |
| } |
| |
| /// Asserts that the [response] has the [expected] textual dump produced |
| /// using [printerConfiguration]. |
| void assertResponse(String expected, {String where = ''}) { |
| var buffer = StringBuffer(); |
| printer.CompletionResponsePrinter( |
| buffer: buffer, |
| configuration: printerConfiguration, |
| response: response, |
| ).writeResponse(); |
| var actual = buffer.toString(); |
| |
| if (actual != expected) { |
| if (where.isEmpty) { |
| var target = |
| driver.server.server.completionState.currentRequest?.target; |
| if (target != null) { |
| var containingNode = target.containingNode.runtimeType; |
| var entity = target.entity; |
| where = ' (containingNode = $containingNode, entity = $entity)'; |
| } |
| } |
| TextExpectationsCollector.add(actual); |
| fail(''' |
| The actual suggestions do not match the expected suggestions$where. |
| |
| To accept the current state change the expectation to |
| \r${'-' * 64} |
| \r${actual.trimRight().split('\n').join('\n\r')} |
| \r${'-' * 64} |
| '''); |
| } |
| } |
| |
| void assertSuggestion({ |
| required String completion, |
| ElementKind? element, |
| CompletionSuggestionKind? kind, |
| String? file, |
| String? libraryUri, |
| }) { |
| expect( |
| suggestionWith( |
| completion: completion, |
| element: element, |
| kind: kind, |
| file: file, |
| libraryUri: libraryUri, |
| ), |
| isNotNull); |
| } |
| |
| // TODO(scheglov): Use it everywhere instead of [addTestFile]. |
| // ignore:unreachable_from_main |
| Future<void> computeSuggestions( |
| String content, |
| ) async { |
| // Give the server time to create analysis contexts. |
| await pumpEventQueue(times: 1000); |
| |
| await addTestFile(content); |
| |
| // Extract the internal request object. |
| var dartRequest = server.completionState.currentRequest; |
| |
| response = CompletionResponseForTesting( |
| requestOffset: driver.completionOffset, |
| requestLocationName: dartRequest?.collectorLocationName, |
| opTypeLocationName: dartRequest?.opType.completionLocation, |
| replacementOffset: driver.replacementOffset, |
| replacementLength: driver.replacementLength, |
| isIncomplete: false, // TODO(scheglov): not correct |
| suggestions: suggestions, |
| ); |
| } |
| |
| Future<List<CompletionSuggestion>> getSuggestions() async { |
| suggestions = await driver.getSuggestions(); |
| return suggestions; |
| } |
| |
| @override |
| Future<void> setUp() async { |
| super.setUp(); |
| |
| writeTestPackagePubspecYamlFile(r''' |
| name: test |
| '''); |
| |
| driver = CompletionDriver(server: this); |
| await driver.createProject(); |
| |
| // TODO(pq): add logic (possibly to driver) that waits for SDK suggestions |
| |
| printerConfiguration = printer.Configuration( |
| filter: (suggestion) { |
| var kind = suggestion.kind; |
| if (kind == CompletionSuggestionKind.IDENTIFIER || |
| kind == CompletionSuggestionKind.INVOCATION) { |
| var completion = suggestion.completion; |
| if (includeClosures && completion.startsWith('(')) { |
| return true; |
| } |
| var periodIndex = completion.indexOf('.'); |
| if (periodIndex > 0) { |
| completion = completion.substring(0, periodIndex); |
| } |
| return RegExp(r'^_?[a-zA-Z][0-9]+$').hasMatch(completion) || |
| allowedIdentifiers.contains(completion); |
| } else if (kind == CompletionSuggestionKind.KEYWORD) { |
| return includeKeywords; |
| } else if (kind == CompletionSuggestionKind.OVERRIDE) { |
| return includeOverrides; |
| } else if (allowedKinds.contains(kind)) { |
| return true; |
| } |
| return true; |
| }, |
| ); |
| } |
| |
| SuggestionMatcher suggestionHas({ |
| required String completion, |
| ElementKind? element, |
| CompletionSuggestionKind? kind, |
| String? file, |
| String? libraryUri, |
| }) => |
| (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; |
| } |
| if (libraryUri != null && s.libraryUri != libraryUri) { |
| return false; |
| } |
| return true; |
| }; |
| |
| Iterable<CompletionSuggestion> suggestionsWith({ |
| required String completion, |
| ElementKind? element, |
| CompletionSuggestionKind? kind, |
| String? file, |
| String? libraryUri, |
| }) => |
| suggestions.where(suggestionHas( |
| completion: completion, |
| element: element, |
| kind: kind, |
| file: file, |
| libraryUri: libraryUri)); |
| |
| CompletionSuggestion suggestionWith({ |
| required String completion, |
| ElementKind? element, |
| CompletionSuggestionKind? kind, |
| String? file, |
| String? libraryUri, |
| }) { |
| var matches = suggestionsWith( |
| completion: completion, |
| element: element, |
| kind: kind, |
| file: file, |
| libraryUri: libraryUri); |
| 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 |
| with BasicCompletionTestCases {} |
| |
| mixin BasicCompletionTestCases on AbstractCompletionDriverTest { |
| /// 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 |
| with CompletionWithSuggestionsTestCases {} |
| |
| mixin CompletionWithSuggestionsTestCases on AbstractCompletionDriverTest { |
| Future<void> test_project_filterImports_defaultConstructor() async { |
| newFile('$testPackageLibPath/a.dart', r''' |
| class A {} |
| '''); |
| |
| newFile('$testPackageLibPath/b.dart', r''' |
| export 'a.dart'; |
| '''); |
| |
| await addTestFile(''' |
| import 'a.dart'; |
| void f() { |
| ^ |
| } |
| '''); |
| |
| assertSuggestion( |
| completion: 'A', |
| element: ElementKind.CONSTRUCTOR, |
| kind: CompletionSuggestionKind.INVOCATION); |
| } |
| |
| /// See: https://github.com/dart-lang/sdk/issues/40620 |
| Future<void> test_project_filterImports_enumValues() async { |
| newFile('$testPackageLibPath/a.dart', r''' |
| enum E { |
| e, |
| } |
| '''); |
| |
| newFile('$testPackageLibPath/b.dart', r''' |
| export 'a.dart'; |
| '''); |
| |
| await addTestFile(''' |
| import 'a.dart'; |
| void f() { |
| E v = ^ |
| } |
| '''); |
| assertSuggestion( |
| completion: 'E.e', |
| element: ElementKind.ENUM_CONSTANT, |
| ); |
| } |
| |
| /// See: https://github.com/dart-lang/sdk/issues/40620 |
| Future<void> test_project_filterImports_namedConstructors() async { |
| newFile('$testPackageLibPath/a.dart', r''' |
| class A { |
| A.a(); |
| } |
| '''); |
| |
| newFile('$testPackageLibPath/b.dart', r''' |
| export 'a.dart'; |
| '''); |
| |
| await addTestFile(''' |
| import 'a.dart'; |
| void f() { |
| ^ |
| } |
| '''); |
| |
| assertSuggestion( |
| completion: 'A.a', |
| element: ElementKind.CONSTRUCTOR, |
| kind: CompletionSuggestionKind.INVOCATION); |
| } |
| |
| Future<void> test_project_filterMultipleImports() async { |
| newFile('$testPackageLibPath/a.dart', r''' |
| class A {} |
| '''); |
| |
| newFile('$testPackageLibPath/b.dart', r''' |
| export 'a.dart'; |
| '''); |
| |
| await addTestFile(''' |
| import 'a.dart'; |
| import 'b.dart'; |
| void f() { |
| ^ |
| } |
| '''); |
| |
| assertSuggestion( |
| completion: 'A', |
| element: ElementKind.CLASS, |
| ); |
| } |
| |
| Future<void> test_project_lib() async { |
| newFile('$testPackageLibPath/a.dart', r''' |
| class A {} |
| enum E { |
| e, |
| } |
| extension Ex on A {} |
| mixin M { } |
| typedef T = Function(Object); |
| typedef T2 = double; |
| var v = 0; |
| '''); |
| |
| await addTestFile(''' |
| void f() { |
| ^ |
| } |
| '''); |
| |
| assertSuggestion( |
| completion: 'A', |
| element: ElementKind.CONSTRUCTOR, |
| kind: CompletionSuggestionKind.INVOCATION); |
| assertSuggestion( |
| completion: 'A', |
| element: ElementKind.CLASS, |
| ); |
| assertSuggestion( |
| completion: 'E', |
| element: ElementKind.ENUM, |
| ); |
| assertSuggestion( |
| completion: 'Ex', |
| element: ElementKind.EXTENSION, |
| ); |
| assertSuggestion( |
| completion: 'M', |
| element: ElementKind.MIXIN, |
| ); |
| assertSuggestion( |
| completion: 'T', |
| element: ElementKind.TYPE_ALIAS, |
| ); |
| assertSuggestion( |
| completion: 'T2', |
| element: ElementKind.TYPE_ALIAS, |
| ); |
| assertSuggestion( |
| completion: 'v', |
| element: ElementKind.TOP_LEVEL_VARIABLE, |
| ); |
| } |
| |
| Future<void> test_project_lib_fields_class() async { |
| newFile('$testPackageLibPath/a.dart', r''' |
| class A { |
| int f = 0; |
| } |
| '''); |
| |
| await addTestFile(''' |
| void m() { |
| ^ |
| } |
| '''); |
| |
| assertNoSuggestion(completion: 'f'); |
| } |
| |
| Future<void> test_project_lib_getters_class() async { |
| newFile('$testPackageLibPath/a.dart', r''' |
| class A { |
| int get g => 0; |
| } |
| '''); |
| |
| await addTestFile(''' |
| void f() { |
| ^ |
| } |
| '''); |
| |
| assertNoSuggestion(completion: 'g'); |
| } |
| |
| /// See: https://github.com/dart-lang/sdk/issues/40626 |
| Future<void> test_project_lib_getters_topLevel() async { |
| newFile('$testPackageLibPath/a.dart', r''' |
| int get g => 0; |
| '''); |
| |
| await addTestFile(''' |
| void f() { |
| ^ |
| } |
| '''); |
| |
| assertSuggestion( |
| completion: 'g', |
| element: ElementKind.GETTER, |
| ); |
| } |
| |
| Future<void> test_project_lib_methods_class() async { |
| newFile('$testPackageLibPath/a.dart', r''' |
| class A { |
| void foo() => 0; |
| } |
| '''); |
| |
| await addTestFile(''' |
| void f() { |
| ^ |
| } |
| '''); |
| |
| assertNoSuggestion(completion: 'A.foo'); |
| } |
| |
| Future<void> test_project_lib_methods_static() async { |
| newFile('$testPackageLibPath/a.dart', r''' |
| class A { |
| static void foo() => 0; |
| } |
| '''); |
| |
| await addTestFile(''' |
| void f() { |
| ^ |
| } |
| '''); |
| |
| assertNoSuggestion(completion: 'A.foo'); |
| } |
| |
| Future<void> test_project_lib_multipleExports() async { |
| newFile('$testPackageLibPath/a.dart', r''' |
| class A {} |
| '''); |
| |
| newFile('$testPackageLibPath/b.dart', r''' |
| export 'a.dart'; |
| '''); |
| |
| await addTestFile(''' |
| void f() { |
| ^ |
| } |
| '''); |
| |
| // Should be suggested from both libraries. |
| assertSuggestion( |
| completion: 'A', |
| libraryUri: 'package:test/a.dart', |
| element: ElementKind.CONSTRUCTOR, |
| kind: CompletionSuggestionKind.INVOCATION); |
| assertSuggestion( |
| completion: 'A', |
| libraryUri: 'package:test/b.dart', |
| element: ElementKind.CONSTRUCTOR, |
| kind: CompletionSuggestionKind.INVOCATION); |
| } |
| |
| Future<void> test_project_lib_multipleExports_filteredByImport() async { |
| newFile('$testPackageLibPath/a.dart', r''' |
| class A {} |
| '''); |
| |
| newFile('$testPackageLibPath/b.dart', r''' |
| export 'a.dart'; |
| '''); |
| |
| await addTestFile(''' |
| import 'b.dart'; |
| void f() { |
| ^ |
| } |
| '''); |
| |
| // Should be only one suggestion, which comes from the import of 'b.dart'. |
| assertSuggestion( |
| completion: 'A', |
| element: ElementKind.CLASS, |
| libraryUri: 'package:test/b.dart', |
| ); |
| } |
| |
| Future<void> test_project_lib_multipleExports_filteredByLocal() async { |
| newFile('$testPackageLibPath/a.dart', r''' |
| class A {} |
| '''); |
| |
| await addTestFile(''' |
| class A {} |
| void f() { |
| ^ |
| } |
| '''); |
| |
| // Should be only one suggestion, which comes from local declaration. |
| var suggestion = suggestionWith( |
| completion: 'A', |
| element: ElementKind.CLASS, |
| ); |
| expect(suggestion, isNotNull); |
| expect(suggestion.libraryUri, isNull); |
| } |
| |
| Future<void> test_project_lib_setters_class() async { |
| newFile('$testPackageLibPath/a.dart', r''' |
| class A { |
| set s(int s) {} |
| } |
| '''); |
| |
| await addTestFile(''' |
| void f() { |
| ^ |
| } |
| '''); |
| |
| assertNoSuggestion(completion: 's'); |
| } |
| |
| Future<void> test_project_lib_setters_static() async { |
| newFile('$testPackageLibPath/a.dart', r''' |
| class A { |
| static set foo(int _) {} |
| } |
| '''); |
| |
| await addTestFile(''' |
| void f() { |
| ^ |
| } |
| '''); |
| |
| assertNoSuggestion(completion: 'A.foo'); |
| } |
| |
| /// See: https://github.com/dart-lang/sdk/issues/40626 |
| Future<void> test_project_lib_setters_topLevel() async { |
| newFile('$testPackageLibPath/a.dart', r''' |
| set s(int s) {} |
| '''); |
| |
| await addTestFile(''' |
| void f() { |
| ^ |
| } |
| '''); |
| |
| assertSuggestion( |
| completion: 's', |
| element: ElementKind.SETTER, |
| ); |
| } |
| |
| @FailingTest(issue: 'https://github.com/dart-lang/sdk/issues/38739') |
| Future<void> |
| test_project_suggestionRelevance_constructorParameterType() async { |
| newFile('$testPackageLibPath/a.dart', r''' |
| import 'b.dart'; |
| |
| class A { |
| A.b({O o}); |
| } |
| '''); |
| |
| newFile('$testPackageLibPath/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 { |
| newFile('$testPackageLibPath/a.dart', r''' |
| class A { } |
| '''); |
| |
| await addTestFile(''' |
| import 'a.dart'; |
| |
| void f(List<String> args) { |
| var a = ^ |
| } |
| '''); |
| |
| expect( |
| suggestionWith( |
| completion: 'A', |
| element: ElementKind.CONSTRUCTOR, |
| ).relevance, |
| greaterThan( |
| suggestionWith( |
| completion: 'A', |
| element: ElementKind.CLASS, |
| ).relevance, |
| ), |
| ); |
| } |
| |
| /// See: https://github.com/dart-lang/sdk/issues/35529 |
| Future<void> test_project_suggestMixins() async { |
| newFile('$testPackageLibPath/a.dart', r''' |
| mixin M { } |
| mixin class A { } |
| '''); |
| |
| await addTestFile(''' |
| class C extends Object with ^ |
| '''); |
| |
| assertSuggestion( |
| completion: 'M', |
| element: ElementKind.MIXIN, |
| ); |
| assertSuggestion( |
| completion: 'A', |
| element: ElementKind.CLASS, |
| ); |
| } |
| |
| Future<void> test_sdk_lib_future_isNotDuplicated() async { |
| await addTestFile(''' |
| void f() { |
| ^ |
| } |
| '''); |
| |
| expect( |
| suggestionsWith( |
| completion: 'Future.value', |
| file: '/sdk/lib/async/async.dart', |
| element: ElementKind.CONSTRUCTOR, |
| kind: CompletionSuggestionKind.INVOCATION), |
| hasLength(1)); |
| } |
| |
| 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, |
| ); |
| |
| // + Top level variables. |
| assertSuggestion( |
| completion: 'pi', |
| file: '/sdk/lib/math/math.dart', |
| element: ElementKind.TOP_LEVEL_VARIABLE, |
| ); |
| |
| // (No typedefs, enums, extensions defined in the Mock SDK.) |
| } |
| } |