blob: 35f317f8ea47d6e94016104adc5c108221b41385 [file] [log] [blame]
// Copyright (c) 2014, 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:analysis_server/src/provisional/completion/dart/completion_dart.dart';
import 'package:analysis_server/src/services/completion/completion_core.dart';
import 'package:analysis_server/src/services/completion/completion_performance.dart';
import 'package:analysis_server/src/services/completion/dart/completion_manager.dart'
show DartCompletionRequestImpl;
import 'package:analysis_server/src/services/completion/dart/suggestion_builder.dart';
import 'package:analysis_server/src/services/completion/dart/utilities.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:test/test.dart';
import '../../../abstract_context.dart';
SuggestionMatcher suggestionHas(
{required String completion,
ElementKind? element,
CompletionSuggestionKind? kind}) =>
(CompletionSuggestion s) {
if (s.completion == completion) {
if (element != null && s.element?.kind != element) {
return false;
}
if (kind != null && s.kind != kind) {
return false;
}
return true;
}
return false;
};
typedef SuggestionMatcher = bool Function(CompletionSuggestion suggestion);
/// Base class for tests that validate individual [DartCompletionContributor]
/// suggestions.
abstract class DartCompletionContributorTest
extends _BaseDartCompletionContributorTest {
@nonVirtual
@override
Future<List<CompletionSuggestion>> computeContributedSuggestions(
DartCompletionRequest request) async {
var builder = SuggestionBuilder(request);
var contributor = createContributor(request, builder);
await contributor.computeSuggestions();
return builder.suggestions.toList();
}
DartCompletionContributor createContributor(
DartCompletionRequest request,
SuggestionBuilder builder,
);
}
abstract class _BaseDartCompletionContributorTest extends AbstractContextTest {
static const String _UNCHECKED = '__UNCHECKED__';
late String testFile;
int _completionOffset = -1;
late int replacementOffset;
late int replacementLength;
late ResolvedUnitResult result;
/// The Dartdoc information passed to requests.
final DartdocDirectiveInfo dartdocInfo = DartdocDirectiveInfo();
late DartCompletionRequest request;
late List<CompletionSuggestion> suggestions;
/// Return the offset at which completion was requested.
int get completionOffset {
if (_completionOffset < 0) {
fail('Must call addTestSource exactly once');
}
return _completionOffset;
}
/// 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;
/// Return `true` if contributors should suggest constructors in contexts
/// where there is no `new` or `const` keyword.
bool get suggestConstructorsWithoutNew => true;
void addTestSource(String content) {
expect(_completionOffset, lessThan(0),
reason: 'Must call addTestSource exactly once');
_completionOffset = content.indexOf('^');
expect(_completionOffset, greaterThanOrEqualTo(0), 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 assertCoreTypeSuggestions() {
assertSuggest('Comparable');
assertSuggest('Comparator');
assertSuggest('DateTime');
assertSuggest('Deprecated');
assertSuggest('Duration');
assertSuggest('Error');
assertSuggest('Exception');
assertSuggest('FormatException');
assertSuggest('Function');
assertSuggest('Future');
assertSuggest('Invocation');
assertSuggest('Iterable');
assertSuggest('Iterator');
assertSuggest('List');
assertSuggest('Map');
assertSuggest('MapEntry');
assertSuggest('Null');
assertSuggest('Object');
assertSuggest('Pattern');
assertSuggest('RegExp');
assertSuggest('Set');
assertSuggest('StackTrace');
assertSuggest('Stream');
assertSuggest('String');
}
void assertHasNoParameterInfo(CompletionSuggestion suggestion) {
expect(suggestion.parameterNames, isNull);
expect(suggestion.parameterTypes, isNull);
expect(suggestion.requiredParameterCount, isNull);
expect(suggestion.hasNamedParameters, isNull);
}
void assertHasParameterInfo(CompletionSuggestion suggestion) {
var parameterNames = suggestion.parameterNames!;
var parameterTypes = suggestion.parameterTypes!;
expect(parameterNames.length, parameterTypes.length);
expect(suggestion.requiredParameterCount,
lessThanOrEqualTo(parameterNames.length));
expect(suggestion.hasNamedParameters, isNotNull);
}
void assertNoSuggestions({CompletionSuggestionKind? kind}) {
if (kind == null) {
if (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, {ElementKind? elemKind}) {
var suggestion = suggestions.firstWhereOrNull((CompletionSuggestion cs) {
if (elemKind == null) {
return cs.completion == completion;
} else {
return cs.completion == completion && cs.element!.kind == elemKind;
}
});
if (suggestion != null) {
failedCompletion('did not expect completion: $completion\n $suggestion');
}
}
CompletionSuggestion assertSuggest(String completion,
{CompletionSuggestionKind csKind = CompletionSuggestionKind.INVOCATION,
ElementKind? elemKind,
bool isDeprecated = false,
bool isPotential = false,
String? elemFile,
int? elemOffset,
int? selectionOffset,
String? paramName,
String? paramType,
String? defaultArgListString = _UNCHECKED,
List<int>? defaultArgumentListTextRanges,
bool isSynthetic = false,
bool skipLocationCheck = false}) {
var cs =
getSuggest(completion: completion, csKind: csKind, elemKind: elemKind);
if (cs == null) {
failedCompletion('expected $completion $csKind $elemKind', suggestions);
}
expect(cs.kind, equals(csKind));
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 (!isSynthetic && element != null && !skipLocationCheck) {
var location = element.location!;
expect(location.file, isNotNull);
expect(location.offset, isNotNull);
expect(location.length, isNotNull);
expect(location.startColumn, isNotNull);
expect(location.startLine, isNotNull);
}
if (elemFile != null) {
expect(element!.location!.file, convertPath(elemFile));
}
if (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,
{CompletionSuggestionKind kind = CompletionSuggestionKind.INVOCATION,
bool isDeprecated = false,
String? elemFile,
String? elemName,
int? elemOffset}) {
var cs = assertSuggest(name,
csKind: kind,
isDeprecated: isDeprecated,
elemFile: elemFile,
elemKind: ElementKind.CLASS,
elemOffset: elemOffset);
var element = cs.element!;
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,
{CompletionSuggestionKind kind = CompletionSuggestionKind.INVOCATION}) {
var cs = assertSuggest(name, csKind: kind);
var element = cs.element!;
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 suggestion,
{String? elementName,
int? elemOffset,
String defaultArgListString = _UNCHECKED,
List<int>? defaultArgumentListTextRanges}) {
elementName ??= suggestion;
var cs = assertSuggest(suggestion,
elemKind: ElementKind.CONSTRUCTOR,
elemOffset: elemOffset,
defaultArgListString: defaultArgListString,
defaultArgumentListTextRanges: defaultArgumentListTextRanges);
var element = cs.element!;
expect(element.kind, equals(ElementKind.CONSTRUCTOR));
expect(element.name, elementName);
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,
{bool isDeprecated = false}) {
var suggestion = assertSuggest(completion, 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,
{CompletionSuggestionKind kind = CompletionSuggestionKind.INVOCATION,
bool isDeprecated = false}) {
var cs = assertSuggest(name,
csKind: kind, elemKind: ElementKind.FIELD, isDeprecated: isDeprecated);
// The returnType represents the type of a field
expect(cs.returnType, type ?? 'dynamic');
var element = cs.element!;
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,
String? defaultArgListString = _UNCHECKED,
List<int>? defaultArgumentListTextRanges}) {
var cs = assertSuggest(name,
csKind: kind,
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.kind, equals(ElementKind.FUNCTION));
expect(element.name, equals(name));
expect(element.isDeprecated, equals(isDeprecated));
var param = element.parameters!;
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 assertSuggestGetter(String name, String? returnType,
{CompletionSuggestionKind kind = CompletionSuggestionKind.INVOCATION,
bool isDeprecated = false}) {
var cs = assertSuggest(name,
csKind: kind, elemKind: ElementKind.GETTER, isDeprecated: isDeprecated);
expect(cs.returnType, returnType ?? 'dynamic');
var element = cs.element!;
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 assertSuggestLocalVariable(
String name, String? returnType) {
// Local variables should only be suggested by LocalReferenceContributor
var cs = assertSuggest(name, csKind: CompletionSuggestionKind.INVOCATION);
expect(cs.returnType, returnType ?? 'dynamic');
var element = cs.element!;
expect(element.kind, equals(ElementKind.LOCAL_VARIABLE));
expect(element.name, equals(name));
expect(element.parameters, isNull);
expect(element.returnType, returnType ?? 'dynamic');
assertHasNoParameterInfo(cs);
return cs;
}
CompletionSuggestion assertSuggestMethod(
String name, String? declaringType, String? returnType,
{CompletionSuggestionKind kind = CompletionSuggestionKind.INVOCATION,
bool isDeprecated = false,
String? defaultArgListString = _UNCHECKED,
List<int>? defaultArgumentListTextRanges,
bool skipLocationCheck = false}) {
var cs = assertSuggest(name,
csKind: kind,
isDeprecated: isDeprecated,
defaultArgListString: defaultArgListString,
defaultArgumentListTextRanges: defaultArgumentListTextRanges,
skipLocationCheck: skipLocationCheck);
expect(cs.declaringType, equals(declaringType));
expect(cs.returnType, returnType ?? 'dynamic');
var element = cs.element!;
expect(element.kind, equals(ElementKind.METHOD));
expect(element.name, equals(name));
var param = element.parameters!;
expect(param[0], equals('('));
expect(param[param.length - 1], equals(')'));
expect(element.returnType, returnType ?? 'dynamic');
assertHasParameterInfo(cs);
return cs;
}
CompletionSuggestion assertSuggestMixin(String name,
{CompletionSuggestionKind kind = CompletionSuggestionKind.INVOCATION,
bool isDeprecated = false,
String? elemFile,
String? elemName,
int? elemOffset}) {
var cs = assertSuggest(name,
csKind: kind,
isDeprecated: isDeprecated,
elemFile: elemFile,
elemKind: ElementKind.MIXIN,
elemOffset: elemOffset);
var element = cs.element!;
expect(element.kind, equals(ElementKind.MIXIN));
expect(element.name, equals(elemName ?? name));
expect(element.parameters, isNull);
expect(element.returnType, isNull);
assertHasNoParameterInfo(cs);
return cs;
}
CompletionSuggestion assertSuggestName(String name,
{CompletionSuggestionKind kind = CompletionSuggestionKind.IDENTIFIER,
bool isDeprecated = false}) {
var cs = assertSuggest(name, csKind: kind, isDeprecated: isDeprecated);
expect(cs.completion, equals(name));
expect(cs.element, isNull);
assertHasNoParameterInfo(cs);
return cs;
}
CompletionSuggestion assertSuggestParameter(String name, String? returnType) {
var cs = assertSuggest(name, csKind: CompletionSuggestionKind.INVOCATION);
expect(cs.returnType, returnType ?? 'dynamic');
var element = cs.element!;
expect(element.kind, equals(ElementKind.PARAMETER));
expect(element.name, equals(name));
expect(element.parameters, isNull);
expect(element.returnType, equals(returnType ?? 'dynamic'));
return cs;
}
CompletionSuggestion assertSuggestSetter(String name,
{CompletionSuggestionKind kind = CompletionSuggestionKind.INVOCATION}) {
var cs = assertSuggest(name, csKind: kind, elemKind: ElementKind.SETTER);
var element = cs.element!;
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, {
CompletionSuggestionKind kind = CompletionSuggestionKind.INVOCATION,
}) {
var cs = assertSuggest(name, csKind: kind);
if (returnType != null) {
expect(cs.returnType, returnType);
} else if (isNullExpectedReturnTypeConsideredDynamic) {
expect(cs.returnType, 'dynamic');
}
var element = cs.element!;
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;
}
CompletionSuggestion assertSuggestTypeAlias(
String name, {
String? aliasedType,
String? returnType,
bool isDeprecated = false,
CompletionSuggestionKind kind = CompletionSuggestionKind.INVOCATION,
}) {
var cs = assertSuggest(name, csKind: kind, isDeprecated: isDeprecated);
if (returnType != null) {
expect(cs.returnType, returnType);
} else if (aliasedType != null) {
// Just to don't fall into the next 'if'.
} else if (isNullExpectedReturnTypeConsideredDynamic) {
expect(cs.returnType, 'dynamic');
} else {
expect(cs.returnType, isNull);
}
var element = cs.element!;
expect(element.kind, equals(ElementKind.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.aliasedType, aliasedType);
expect(element.returnType, returnType);
// TODO (danrubel) Determine why param info is missing
// assertHasParameterInfo(cs);
return cs;
}
CompletionSuggestion assertSuggestTypeParameter(String name) {
var cs = assertSuggest(name, csKind: CompletionSuggestionKind.IDENTIFIER);
expect(cs.returnType, isNull);
var element = cs.element!;
expect(element.kind, equals(ElementKind.TYPE_PARAMETER));
expect(element.name, equals(name));
expect(element.parameters, isNull);
expect(element.returnType, isNull);
return cs;
}
Future<List<CompletionSuggestion>> computeContributedSuggestions(
DartCompletionRequest request);
Future computeSuggestions({int times = 200}) async {
result = await session.getResolvedUnit(testFile) as ResolvedUnitResult;
var baseRequest = CompletionRequestImpl(
result, completionOffset, CompletionPerformance());
return await baseRequest.performance.runRequestOperation(
(performance) async {
// Build the request
var request = await DartCompletionRequestImpl.from(
baseRequest,
dartdocDirectiveInfo: dartdocInfo,
);
var range = request.target.computeReplacementRange(request.offset);
replacementOffset = range.offset;
replacementLength = range.length;
// Request completions
suggestions = await computeContributedSuggestions(request);
expect(suggestions, isNotNull, reason: 'expected suggestions');
},
);
}
Never failedCompletion(String message,
[Iterable<CompletionSuggestion>? completions]) {
var sb = StringBuffer(message);
if (completions != null) {
sb.write('\n found');
completions.toList()
..sort(completionComparator)
..forEach((CompletionSuggestion suggestion) {
sb.write('\n ${suggestion.completion} -> $suggestion');
});
}
fail(sb.toString());
}
CompletionSuggestion? getSuggest(
{String? completion,
CompletionSuggestionKind? csKind,
ElementKind? elemKind}) {
CompletionSuggestion? cs;
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 {
// Await a microtask. Otherwise the futures are chained and would
// resolve linearly using up the stack.
await null;
if (completer.isCompleted) {
return completer.future;
}
// We use a delayed future to allow microtask events to finish. The
// Future.value or Future.microtask() constructors use scheduleMicrotask
// themselves and would therefore not wait for microtask callbacks that
// are scheduled after invoking this method.
return Future(() => 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');
}
CompletionSuggestion suggestionWith(
{required String completion,
ElementKind? element,
CompletionSuggestionKind? kind}) {
final matches = suggestions.where(
suggestionHas(completion: completion, element: element, kind: kind));
expect(matches, hasLength(1));
return matches.first;
}
}