blob: a7899a014c77209d04cdb70656d61f316c54076c [file] [log] [blame]
// 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_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) {
var 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);
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) async {
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();
}
}