| // 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 'dart:async'; |
| |
| import 'package:analyzer/dart/analysis/features.dart'; |
| import 'package:analyzer_plugin/protocol/protocol_common.dart'; |
| import 'package:analyzer_plugin/src/utilities/completion/completion_core.dart'; |
| import 'package:analyzer_plugin/src/utilities/completion/completion_target.dart'; |
| import 'package:analyzer_plugin/utilities/completion/completion_core.dart'; |
| import 'package:analyzer_plugin/utilities/completion/relevance.dart'; |
| import 'package:collection/collection.dart'; |
| import 'package:test/test.dart'; |
| |
| import '../../support/abstract_context.dart'; |
| |
| int suggestionComparator(CompletionSuggestion s1, CompletionSuggestion s2) { |
| var c1 = s1.completion.toLowerCase(); |
| var c2 = s2.completion.toLowerCase(); |
| return c1.compareTo(c2); |
| } |
| |
| abstract class DartCompletionContributorTest extends AbstractContextTest { |
| static const String _UNCHECKED = '__UNCHECKED__'; |
| late String testFile; |
| int? completionOffset; |
| int? replacementOffset; |
| int? replacementLength; |
| late CompletionContributor contributor; |
| DartCompletionRequest? request; |
| List<CompletionSuggestion>? suggestions; |
| |
| /// If `true` and `null` is specified as the suggestion's expected returnType |
| /// then the actual suggestion is expected to have a `dynamic` returnType. |
| /// Newer tests return `false` so that they can distinguish between |
| /// `dynamic` and `null`. |
| /// Eventually all tests should be converted and this getter removed. |
| bool get isNullExpectedReturnTypeConsideredDynamic => true; |
| |
| void addTestSource(String content) { |
| expect(completionOffset, isNull, reason: 'Call addTestUnit exactly once'); |
| completionOffset = content.indexOf('^'); |
| expect(completionOffset, isNot(equals(-1)), reason: 'missing ^'); |
| var nextOffset = content.indexOf('^', completionOffset! + 1); |
| expect(nextOffset, equals(-1), reason: 'too many ^'); |
| content = |
| content.substring(0, completionOffset) + |
| content.substring(completionOffset! + 1); |
| addSource(testFile, content); |
| } |
| |
| void assertHasNoParameterInfo(CompletionSuggestion suggestion) { |
| expect(suggestion.parameterNames, isNull); |
| expect(suggestion.parameterTypes, isNull); |
| expect(suggestion.requiredParameterCount, isNull); |
| expect(suggestion.hasNamedParameters, isNull); |
| } |
| |
| void assertHasParameterInfo(CompletionSuggestion suggestion) { |
| expect(suggestion.parameterNames, isNotNull); |
| expect(suggestion.parameterTypes, isNotNull); |
| expect( |
| suggestion.parameterNames!.length, |
| suggestion.parameterTypes!.length, |
| ); |
| expect( |
| suggestion.requiredParameterCount, |
| lessThanOrEqualTo(suggestion.parameterNames!.length), |
| ); |
| expect(suggestion.hasNamedParameters, isNotNull); |
| } |
| |
| void assertNoSuggestions({CompletionSuggestionKind? kind}) { |
| if (kind == null) { |
| final suggestions = this.suggestions; |
| if (suggestions != null && suggestions.isNotEmpty) { |
| failedCompletion('Expected no suggestions', suggestions); |
| } |
| return; |
| } |
| var suggestion = suggestions?.firstWhereOrNull( |
| (CompletionSuggestion cs) => cs.kind == kind, |
| ); |
| if (suggestion != null) { |
| failedCompletion('did not expect completion: $completion\n $suggestion'); |
| } |
| } |
| |
| void assertNotSuggested(String completion) { |
| var suggestion = suggestions?.firstWhereOrNull( |
| (CompletionSuggestion cs) => cs.completion == completion, |
| ); |
| if (suggestion != null) { |
| failedCompletion('did not expect completion: $completion\n $suggestion'); |
| } |
| } |
| |
| CompletionSuggestion assertSuggest( |
| String completion, { |
| CompletionSuggestionKind csKind = CompletionSuggestionKind.INVOCATION, |
| int relevance = DART_RELEVANCE_DEFAULT, |
| ElementKind? elemKind, |
| bool isDeprecated = false, |
| bool isPotential = false, |
| String? elemFile, |
| int? elemOffset, |
| int? selectionOffset, |
| String? paramName, |
| String? paramType, |
| String? defaultArgListString = _UNCHECKED, |
| List<int>? defaultArgumentListTextRanges, |
| }) { |
| var cs = getSuggest( |
| completion: completion, |
| csKind: csKind, |
| elemKind: elemKind, |
| ); |
| if (cs == null) { |
| failedCompletion('expected $completion $csKind $elemKind', suggestions); |
| } |
| expect(cs!.kind, equals(csKind)); |
| if (isDeprecated) { |
| expect(cs.relevance, equals(DART_RELEVANCE_LOW)); |
| } else { |
| expect(cs.relevance, equals(relevance), reason: completion); |
| } |
| expect(cs.selectionOffset, equals(selectionOffset ?? completion.length)); |
| expect(cs.selectionLength, equals(0)); |
| expect(cs.isDeprecated, equals(isDeprecated)); |
| expect(cs.isPotential, equals(isPotential)); |
| var element = cs.element; |
| if (element != null) { |
| expect(element.location, isNotNull); |
| expect(element.location!.file, isNotNull); |
| expect(element.location!.offset, isNotNull); |
| expect(element.location!.length, isNotNull); |
| expect(element.location!.startColumn, isNotNull); |
| expect(element.location!.startLine, isNotNull); |
| } |
| if (element != null && elemFile != null) { |
| expect(element.location!.file, elemFile); |
| } |
| if (element != null && elemOffset != null) { |
| expect(element.location!.offset, elemOffset); |
| } |
| if (paramName != null) { |
| expect(cs.parameterName, paramName); |
| } |
| if (paramType != null) { |
| expect(cs.parameterType, paramType); |
| } |
| if (defaultArgListString != _UNCHECKED) { |
| expect(cs.defaultArgumentListString, defaultArgListString); |
| } |
| if (defaultArgumentListTextRanges != null) { |
| expect(cs.defaultArgumentListTextRanges, defaultArgumentListTextRanges); |
| } |
| return cs; |
| } |
| |
| CompletionSuggestion assertSuggestClass( |
| String name, { |
| int relevance = DART_RELEVANCE_DEFAULT, |
| CompletionSuggestionKind kind = CompletionSuggestionKind.INVOCATION, |
| bool isDeprecated = false, |
| String? elemFile, |
| String? elemName, |
| int? elemOffset, |
| }) { |
| var cs = assertSuggest( |
| name, |
| csKind: kind, |
| relevance: relevance, |
| isDeprecated: isDeprecated, |
| elemFile: elemFile, |
| elemOffset: elemOffset, |
| ); |
| var element = cs.element!; |
| expect(element, isNotNull); |
| expect(element.kind, equals(ElementKind.CLASS)); |
| expect(element.name, equals(elemName ?? name)); |
| expect(element.parameters, isNull); |
| expect(element.returnType, isNull); |
| assertHasNoParameterInfo(cs); |
| return cs; |
| } |
| |
| CompletionSuggestion assertSuggestClassTypeAlias( |
| String name, { |
| int relevance = DART_RELEVANCE_DEFAULT, |
| CompletionSuggestionKind kind = CompletionSuggestionKind.INVOCATION, |
| }) { |
| var cs = assertSuggest(name, csKind: kind, relevance: relevance); |
| var element = cs.element!; |
| expect(element, isNotNull); |
| expect(element.kind, equals(ElementKind.CLASS_TYPE_ALIAS)); |
| expect(element.name, equals(name)); |
| expect(element.parameters, isNull); |
| expect(element.returnType, isNull); |
| assertHasNoParameterInfo(cs); |
| return cs; |
| } |
| |
| CompletionSuggestion assertSuggestConstructor( |
| String name, { |
| int relevance = DART_RELEVANCE_DEFAULT, |
| int? elemOffset, |
| String defaultArgListString = _UNCHECKED, |
| List<int>? defaultArgumentListTextRanges, |
| }) { |
| var cs = assertSuggest( |
| name, |
| relevance: relevance, |
| elemOffset: elemOffset, |
| defaultArgListString: defaultArgListString, |
| defaultArgumentListTextRanges: defaultArgumentListTextRanges, |
| ); |
| var element = cs.element; |
| expect(element, isNotNull); |
| expect(element!.kind, equals(ElementKind.CONSTRUCTOR)); |
| var index = name.indexOf('.'); |
| expect(element.name, index >= 0 ? name.substring(index + 1) : ''); |
| return cs; |
| } |
| |
| CompletionSuggestion assertSuggestEnum( |
| String completion, { |
| bool isDeprecated = false, |
| }) { |
| var suggestion = assertSuggest(completion, isDeprecated: isDeprecated); |
| expect(suggestion.isDeprecated, isDeprecated); |
| expect(suggestion.element!.kind, ElementKind.ENUM); |
| return suggestion; |
| } |
| |
| CompletionSuggestion assertSuggestEnumConst( |
| String completion, { |
| int relevance = DART_RELEVANCE_DEFAULT, |
| bool isDeprecated = false, |
| }) { |
| var suggestion = assertSuggest( |
| completion, |
| relevance: relevance, |
| isDeprecated: isDeprecated, |
| ); |
| expect(suggestion.completion, completion); |
| expect(suggestion.isDeprecated, isDeprecated); |
| expect(suggestion.element!.kind, ElementKind.ENUM_CONSTANT); |
| return suggestion; |
| } |
| |
| CompletionSuggestion assertSuggestField( |
| String name, |
| String? type, { |
| int relevance = DART_RELEVANCE_DEFAULT, |
| CompletionSuggestionKind kind = CompletionSuggestionKind.INVOCATION, |
| bool isDeprecated = false, |
| }) { |
| var cs = assertSuggest( |
| name, |
| csKind: kind, |
| relevance: relevance, |
| elemKind: ElementKind.FIELD, |
| isDeprecated: isDeprecated, |
| ); |
| // The returnType represents the type of a field |
| expect(cs.returnType, type ?? 'dynamic'); |
| var element = cs.element!; |
| expect(element, isNotNull); |
| expect(element.kind, equals(ElementKind.FIELD)); |
| expect(element.name, equals(name)); |
| expect(element.parameters, isNull); |
| // The returnType represents the type of a field |
| expect(element.returnType, type ?? 'dynamic'); |
| assertHasNoParameterInfo(cs); |
| return cs; |
| } |
| |
| CompletionSuggestion assertSuggestFunction( |
| String name, |
| String? returnType, { |
| CompletionSuggestionKind kind = CompletionSuggestionKind.INVOCATION, |
| bool isDeprecated = false, |
| int relevance = DART_RELEVANCE_DEFAULT, |
| String defaultArgListString = _UNCHECKED, |
| List<int>? defaultArgumentListTextRanges, |
| }) { |
| var cs = assertSuggest( |
| name, |
| csKind: kind, |
| relevance: relevance, |
| isDeprecated: isDeprecated, |
| defaultArgListString: defaultArgListString, |
| defaultArgumentListTextRanges: defaultArgumentListTextRanges, |
| ); |
| if (returnType != null) { |
| expect(cs.returnType, returnType); |
| } else if (isNullExpectedReturnTypeConsideredDynamic) { |
| expect(cs.returnType, 'dynamic'); |
| } |
| var element = cs.element!; |
| expect(element, isNotNull); |
| expect(element.kind, equals(ElementKind.FUNCTION)); |
| expect(element.name, equals(name)); |
| expect(element.isDeprecated, equals(isDeprecated)); |
| var param = element.parameters!; |
| expect(param, isNotNull); |
| expect(param[0], equals('(')); |
| expect(param[param.length - 1], equals(')')); |
| if (returnType != null) { |
| expect(element.returnType, returnType); |
| } else if (isNullExpectedReturnTypeConsideredDynamic) { |
| expect(element.returnType, 'dynamic'); |
| } |
| assertHasParameterInfo(cs); |
| return cs; |
| } |
| |
| CompletionSuggestion assertSuggestFunctionTypeAlias( |
| String name, |
| String? returnType, { |
| bool isDeprecated = false, |
| int relevance = DART_RELEVANCE_DEFAULT, |
| CompletionSuggestionKind kind = CompletionSuggestionKind.INVOCATION, |
| }) { |
| var cs = assertSuggest( |
| name, |
| csKind: kind, |
| relevance: relevance, |
| isDeprecated: isDeprecated, |
| ); |
| if (returnType != null) { |
| expect(cs.returnType, returnType); |
| } else if (isNullExpectedReturnTypeConsideredDynamic) { |
| expect(cs.returnType, 'dynamic'); |
| } else { |
| expect(cs.returnType, isNull); |
| } |
| var element = cs.element!; |
| expect(element, isNotNull); |
| expect(element.kind, equals(ElementKind.FUNCTION_TYPE_ALIAS)); |
| expect(element.name, equals(name)); |
| expect(element.isDeprecated, equals(isDeprecated)); |
| // TODO(danrubel): Determine why params are null |
| // String param = element.parameters; |
| // expect(param, isNotNull); |
| // expect(param[0], equals('(')); |
| // expect(param[param.length - 1], equals(')')); |
| expect(element.returnType, equals(returnType ?? 'dynamic')); |
| // TODO(danrubel): Determine why param info is missing |
| // assertHasParameterInfo(cs); |
| return cs; |
| } |
| |
| CompletionSuggestion assertSuggestGetter( |
| String name, |
| String? returnType, { |
| int relevance = DART_RELEVANCE_DEFAULT, |
| CompletionSuggestionKind kind = CompletionSuggestionKind.INVOCATION, |
| bool isDeprecated = false, |
| }) { |
| var cs = assertSuggest( |
| name, |
| csKind: kind, |
| relevance: relevance, |
| elemKind: ElementKind.GETTER, |
| isDeprecated: isDeprecated, |
| ); |
| expect(cs.returnType, returnType ?? 'dynamic'); |
| var element = cs.element!; |
| expect(element, isNotNull); |
| expect(element.kind, equals(ElementKind.GETTER)); |
| expect(element.name, equals(name)); |
| expect(element.parameters, isNull); |
| expect(element.returnType, equals(returnType ?? 'dynamic')); |
| assertHasNoParameterInfo(cs); |
| return cs; |
| } |
| |
| CompletionSuggestion assertSuggestMethod( |
| String name, |
| String declaringType, |
| String? returnType, { |
| int relevance = DART_RELEVANCE_DEFAULT, |
| CompletionSuggestionKind kind = CompletionSuggestionKind.INVOCATION, |
| bool isDeprecated = false, |
| String? defaultArgListString = _UNCHECKED, |
| List<int>? defaultArgumentListTextRanges, |
| }) { |
| var cs = assertSuggest( |
| name, |
| csKind: kind, |
| relevance: relevance, |
| isDeprecated: isDeprecated, |
| defaultArgListString: defaultArgListString, |
| defaultArgumentListTextRanges: defaultArgumentListTextRanges, |
| ); |
| expect(cs.declaringType, equals(declaringType)); |
| expect(cs.returnType, returnType ?? 'dynamic'); |
| var element = cs.element!; |
| expect(element, isNotNull); |
| expect(element.kind, equals(ElementKind.METHOD)); |
| expect(element.name, equals(name)); |
| var param = element.parameters!; |
| expect(param, isNotNull); |
| expect(param[0], equals('(')); |
| expect(param[param.length - 1], equals(')')); |
| expect(element.returnType, returnType ?? 'dynamic'); |
| assertHasParameterInfo(cs); |
| return cs; |
| } |
| |
| CompletionSuggestion assertSuggestName( |
| String name, { |
| int relevance = DART_RELEVANCE_DEFAULT, |
| CompletionSuggestionKind kind = CompletionSuggestionKind.IDENTIFIER, |
| bool isDeprecated = false, |
| }) { |
| var cs = assertSuggest( |
| name, |
| csKind: kind, |
| relevance: relevance, |
| isDeprecated: isDeprecated, |
| ); |
| expect(cs.completion, equals(name)); |
| expect(cs.element, isNull); |
| assertHasNoParameterInfo(cs); |
| return cs; |
| } |
| |
| CompletionSuggestion assertSuggestSetter( |
| String name, { |
| int relevance = DART_RELEVANCE_DEFAULT, |
| CompletionSuggestionKind kind = CompletionSuggestionKind.INVOCATION, |
| }) { |
| var cs = assertSuggest( |
| name, |
| csKind: kind, |
| relevance: relevance, |
| elemKind: ElementKind.SETTER, |
| ); |
| var element = cs.element!; |
| expect(element, isNotNull); |
| expect(element.kind, equals(ElementKind.SETTER)); |
| expect(element.name, equals(name)); |
| // TODO(danrubel): assert setter param |
| //expect(element.parameters, isNull); |
| // TODO(danrubel): it would be better if this was always null |
| if (element.returnType != null) { |
| expect(element.returnType, 'dynamic'); |
| } |
| assertHasNoParameterInfo(cs); |
| return cs; |
| } |
| |
| CompletionSuggestion assertSuggestTopLevelVar( |
| String name, |
| String? returnType, { |
| int relevance = DART_RELEVANCE_DEFAULT, |
| CompletionSuggestionKind kind = CompletionSuggestionKind.INVOCATION, |
| }) { |
| var cs = assertSuggest(name, csKind: kind, relevance: relevance); |
| if (returnType != null) { |
| expect(cs.returnType, returnType); |
| } else if (isNullExpectedReturnTypeConsideredDynamic) { |
| expect(cs.returnType, 'dynamic'); |
| } |
| var element = cs.element!; |
| expect(element, isNotNull); |
| expect(element.kind, equals(ElementKind.TOP_LEVEL_VARIABLE)); |
| expect(element.name, equals(name)); |
| expect(element.parameters, isNull); |
| if (returnType != null) { |
| expect(element.returnType, returnType); |
| } else if (isNullExpectedReturnTypeConsideredDynamic) { |
| expect(element.returnType, 'dynamic'); |
| } |
| assertHasNoParameterInfo(cs); |
| return cs; |
| } |
| |
| Future<void> computeLibrariesContaining() { |
| return resolveFile(testFile).then((result) => null); |
| } |
| |
| Future computeSuggestions() async { |
| var result = await resolveFile(testFile); |
| request = DartCompletionRequestImpl( |
| resourceProvider, |
| completionOffset!, |
| result, |
| ); |
| |
| var target = CompletionTarget.forOffset( |
| request!.result.unit, |
| request!.offset, |
| ); |
| var range = target.computeReplacementRange( |
| request!.offset, |
| isDotShorthandEnabled: result.libraryElement.featureSet.isEnabled( |
| Feature.dot_shorthands, |
| ), |
| ); |
| replacementOffset = range.offset; |
| replacementLength = range.length; |
| |
| // Request completions |
| var collector = CompletionCollectorImpl(); |
| await contributor.computeSuggestions(request!, collector); |
| suggestions = collector.suggestions; |
| expect(suggestions, isNotNull, reason: 'expected suggestions'); |
| } |
| |
| CompletionContributor createContributor(); |
| |
| void failedCompletion( |
| String message, [ |
| Iterable<CompletionSuggestion>? completions, |
| ]) { |
| var sb = StringBuffer(message); |
| if (completions != null) { |
| sb.write('\n found'); |
| completions.toList() |
| ..sort(suggestionComparator) |
| ..forEach((CompletionSuggestion suggestion) { |
| sb.write('\n ${suggestion.completion} -> $suggestion'); |
| }); |
| } |
| fail(sb.toString()); |
| } |
| |
| CompletionSuggestion? getSuggest({ |
| String? completion, |
| CompletionSuggestionKind? csKind, |
| ElementKind? elemKind, |
| }) { |
| CompletionSuggestion? cs; |
| if (suggestions != null) { |
| suggestions?.forEach((CompletionSuggestion s) { |
| if (completion != null && completion != s.completion) { |
| return; |
| } |
| if (csKind != null && csKind != s.kind) { |
| return; |
| } |
| if (elemKind != null) { |
| var element = s.element; |
| if (element == null || elemKind != element.kind) { |
| return; |
| } |
| } |
| if (cs == null) { |
| cs = s; |
| } else { |
| failedCompletion( |
| 'expected exactly one $cs', |
| suggestions!.where((s) => s.completion == completion), |
| ); |
| } |
| }); |
| } |
| return cs; |
| } |
| |
| Future<E> performAnalysis<E>(int times, Completer<E> completer) { |
| if (completer.isCompleted) { |
| return completer.future; |
| } |
| // We use a delayed future to allow microtask events to finish. The |
| // Future.value or Future() constructors use scheduleMicrotask themselves and |
| // would therefore not wait for microtask callbacks that are scheduled after |
| // invoking this method. |
| return Future.delayed( |
| Duration.zero, |
| () => performAnalysis(times - 1, completer), |
| ); |
| } |
| |
| void resolveSource(String path, String content) { |
| addSource(path, content); |
| } |
| |
| @override |
| void setUp() { |
| super.setUp(); |
| testFile = convertPath('/home/test/lib/test.dart'); |
| contributor = createContributor(); |
| } |
| } |