blob: c1b168689584cb917c49a6b8e82fee879b1310e3 [file] [log] [blame]
// Copyright (c) 2018, 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/lsp_protocol/protocol.dart';
import 'package:analyzer/src/test_utilities/test_code_format.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../utils/test_code_extensions.dart';
import 'server_abstract.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(SignatureHelpTest);
});
}
mixin SignatureHelpMixin on AbstractLspAnalysisServerTest {
Future<void> expectNoSignature(
String content, {
Uri? fileUri,
Position? position,
SignatureHelpContext? context,
}) async {
var code = TestCode.parse(content);
fileUri ??= mainFileUri;
position ??= code.position.position;
await initialize();
await openFile(fileUri, code.code);
var res = await getSignatureHelp(fileUri, position, context);
expect(res, isNull);
}
Future<void> _expectSignature(
String content,
String expectedLabel,
String? expectedDoc,
List<ParameterInformation> expectedParams, {
MarkupKind? expectedFormat = MarkupKind.Markdown,
SignatureHelpContext? context,
_FileState state = _FileState.open,
}) async {
var code = TestCode.parse(content);
if (state == _FileState.closed) {
newFile(mainFilePath, code.code);
}
await initialize();
if (state == _FileState.open) {
await openFile(mainFileUri, code.code);
}
await initialAnalysis;
var res =
(await getSignatureHelp(mainFileUri, code.position.position, context))!;
// TODO(dantup): Update this when there is clarification on how to handle
// no valid selected parameter.
expect(res.activeParameter, -1);
expect(res.activeSignature, equals(0));
expect(res.signatures, hasLength(1));
var sig = res.signatures.first;
expect(sig.label, equals(expectedLabel));
expect(sig.parameters, equals(expectedParams));
if (expectedDoc != null) {
// Test the format matches the tests expectation.
// For clients that don't support MarkupContent it'll be a plain string,
// but otherwise it'll be a MarkupContent of type PlainText or Markdown.
var doc = sig.documentation!;
if (expectedFormat == null) {
// Plain string.
expect(doc.valueEquals(expectedDoc), isTrue);
} else {
var expected = MarkupContent(kind: expectedFormat, value: expectedDoc);
expect(doc.valueEquals(expected), isTrue);
}
}
}
}
@reflectiveTest
class SignatureHelpTest extends AbstractLspAnalysisServerTest
with SignatureHelpMixin {
Future<void> assertArgsDocumentation(
String? preference, {
required bool includesSummary,
required bool includesFull,
}) {
var content = '''
class A {
/// Summary.
///
/// Full.
A();
}
final a = A(^);
''';
return assertDocumentation(
preference,
content,
includesSummary: includesSummary,
includesFull: includesFull,
);
}
/// Checks whether the correct types of documentation are returned in
/// signature help for function arguments based on [preference].
Future<void> assertDocumentation(
String? preference,
String content, {
required bool includesSummary,
required bool includesFull,
}) async {
var code = TestCode.parse(content);
await provideConfig(
initialize,
{
if (preference != null) 'documentation': preference,
},
);
await openFile(mainFileUri, code.code);
await initialAnalysis;
var signatureHelp =
await getSignatureHelp(mainFileUri, code.position.position);
var docs = signatureHelp!.signatures.single.documentation?.map(
(markup) => markup.value,
(string) => string,
);
if (includesSummary) {
expect(docs, contains('Summary.'));
} else {
expect(docs, isNot(contains('Summary.')));
}
if (includesFull) {
expect(docs, contains('Full.'));
} else {
expect(docs, isNot(contains('Full.')));
}
}
@override
void setUp() {
super.setUp();
// Signature help tests have incomplete signatures.
failTestOnErrorDiagnostic = false;
setSignatureHelpContentFormat([MarkupKind.Markdown]);
}
Future<void> test_augmentation_method() async {
var content = '''
import augment 'a.dart';
class Foo {}
void bar() {
Foo().myMethod(^);
}
''';
var augmentationFilePath = join(projectFolderPath, 'lib', 'a.dart');
var augmentationCode = '''
augment library 'main.dart';
augment class Foo {
/// My method.
void myMethod(String s) {}
}
''';
newFile(augmentationFilePath, augmentationCode);
var expectedLabel = 'myMethod(String s)';
var expectedDoc = 'My method.';
await _expectSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'String s'),
],
);
}
Future<void> test_callableClass() async {
var content = '''
class Foo {
/// Does foo.
int call(String s, int i) {
var foo = Foo();
return foo(^);
}
}
''';
var expectedLabel = 'foo(String s, int i)';
var expectedDoc = 'Does foo.';
await _expectSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'String s'),
ParameterInformation(label: 'int i'),
],
);
}
Future<void> test_dartDocMacro() async {
setSignatureHelpContentFormat(null);
var content = '''
/// {@template template_name}
/// This is shared content.
/// {@endtemplate}
const String bar = null;
/// {@macro template_name}
foo(String s, int i) {
foo(^);
}
''';
var expectedLabel = 'foo(String s, int i)';
var expectedDoc = 'This is shared content.';
await _expectSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'String s'),
ParameterInformation(label: 'int i'),
],
expectedFormat: null,
);
}
Future<void> test_dartDocPreference_full() => assertArgsDocumentation('full',
includesSummary: true, includesFull: true);
Future<void> test_dartDocPreference_none() => assertArgsDocumentation('none',
includesSummary: false, includesFull: false);
Future<void> test_dartDocPreference_summary() =>
assertArgsDocumentation('summary',
includesSummary: true, includesFull: false);
/// No preference should result in full docs.
Future<void> test_dartDocPreference_unset() =>
assertArgsDocumentation(null, includesSummary: true, includesFull: true);
Future<void> test_error_methodInvocation_importPrefix() async {
var content = '''
import 'dart:async' as prefix;
void f() {
prefix(^);
}
''';
// Expect no result.
await expectNoSignature(content);
}
Future<void> test_extensionType() async {
var content = '''
class A {
void f(int a) {}
}
extension type E(A a) {
void f(int e) {}
}
void f() {
final e = E(A());
e.f(^);
}
''';
await _expectSignature(
content,
'f(int e)',
null,
[
ParameterInformation(label: 'int e'),
],
);
}
Future<void> test_formats_markdown() async {
var content = '''
/// Does foo.
foo(String s, int i) {
foo(^);
}
''';
var expectedLabel = 'foo(String s, int i)';
var expectedDoc = 'Does foo.';
await _expectSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'String s'),
ParameterInformation(label: 'int i'),
],
);
}
Future<void> test_formats_notSupported() async {
setSignatureHelpContentFormat(null);
var content = '''
/// Does foo.
foo(String s, int i) {
foo(^);
}
''';
var expectedLabel = 'foo(String s, int i)';
var expectedDoc = 'Does foo.';
await _expectSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'String s'),
ParameterInformation(label: 'int i'),
],
expectedFormat: null,
);
}
Future<void> test_formats_plainTextOnly() async {
setSignatureHelpContentFormat([MarkupKind.PlainText]);
var content = '''
/// Does foo.
foo(String s, int i) {
foo(^);
}
''';
var expectedLabel = 'foo(String s, int i)';
var expectedDoc = 'Does foo.';
await _expectSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'String s'),
ParameterInformation(label: 'int i'),
],
expectedFormat: MarkupKind.PlainText,
);
}
Future<void> test_formats_plainTextPreferred() async {
setSignatureHelpContentFormat([MarkupKind.PlainText, MarkupKind.Markdown]);
var content = '''
/// Does foo.
foo(String s, int i) {
foo(^);
}
''';
var expectedLabel = 'foo(String s, int i)';
var expectedDoc = 'Does foo.';
// We say we prefer PlainText as a client, but since we only really
// support Markdown and the client supports it, we expect the server
// to provide Markdown.
await _expectSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'String s'),
ParameterInformation(label: 'int i'),
],
);
}
Future<void> test_functionExpression_local() async {
var content = '''
void f() {
var foo = (String s, int i) => s;
foo(^);
}
''';
var expectedLabel = 'foo(String s, int i)';
await _expectSignature(
content,
expectedLabel,
null, // expectedDoc, not dartDocs on local vars.
[
ParameterInformation(label: 'String s'),
ParameterInformation(label: 'int i'),
],
);
}
Future<void> test_functionExpression_topLevel() async {
var content = '''
/// Does foo.
var foo = (String s, int i) => s;
void f() {
foo(^);
}
''';
var expectedLabel = 'foo(String s, int i)';
var expectedDoc = 'Does foo.';
await _expectSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'String s'),
ParameterInformation(label: 'int i'),
],
);
}
Future<void> test_manualTrigger_invalidLocation() async {
// If the user invokes signature help, we should show it even if it's a
// location where we wouldn't automatically trigger (for example in a string).
var content = '''
/// Does foo.
foo(String s, int i) {
foo('this is a (^test');
}
''';
var expectedLabel = 'foo(String s, int i)';
var expectedDoc = 'Does foo.';
await _expectSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'String s'),
ParameterInformation(label: 'int i'),
],
context: SignatureHelpContext(
triggerKind: SignatureHelpTriggerKind.Invoked,
isRetrigger: false,
));
}
Future<void> test_noDefaultConstructor() async {
var content = '''
class A {
A._();
}
final a = A(^);
''';
await expectNoSignature(content);
}
Future<void> test_nonDartFile() async {
await expectNoSignature(
simplePubspecContent,
fileUri: pubspecFileUri,
position: startOfDocPos,
);
}
Future<void> test_params_multipleNamed() async {
var content = '''
/// Does foo.
foo(String s, {bool b = true, bool a}) {
foo(^);
}
''';
var expectedLabel = 'foo(String s, {bool b = true, bool a})';
var expectedDoc = 'Does foo.';
await _expectSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'String s'),
ParameterInformation(label: 'bool b = true'),
ParameterInformation(label: 'bool a'),
],
);
}
Future<void> test_params_multipleNamed_retrigger() async {
var content = '''
/// Does foo.
foo(String s, {bool b = true, bool a}) {
foo('s',^);
}
''';
var expectedLabel = 'foo(String s, {bool b = true, bool a})';
var expectedDoc = 'Does foo.';
await _expectSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'String s'),
ParameterInformation(label: 'bool b = true'),
ParameterInformation(label: 'bool a'),
],
);
}
Future<void> test_params_multipleOptional() async {
var content = '''
/// Does foo.
foo(String s, [bool b = true, bool a]) {
foo(^);
}
''';
var expectedLabel = 'foo(String s, [bool b = true, bool a])';
var expectedDoc = 'Does foo.';
await _expectSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'String s'),
ParameterInformation(label: 'bool b = true'),
ParameterInformation(label: 'bool a'),
],
);
}
Future<void> test_params_named() async {
var content = '''
/// Does foo.
foo(String s, {bool b = true}) {
foo(^);
}
''';
var expectedLabel = 'foo(String s, {bool b = true})';
var expectedDoc = 'Does foo.';
await _expectSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'String s'),
ParameterInformation(label: 'bool b = true'),
],
);
}
Future<void> test_params_optional() async {
var content = '''
/// Does foo.
foo(String s, [bool b = true]) {
foo(^);
}
''';
var expectedLabel = 'foo(String s, [bool b = true])';
var expectedDoc = 'Does foo.';
await _expectSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'String s'),
ParameterInformation(label: 'bool b = true'),
],
);
}
Future<void> test_params_recordType() async {
var content = '''
/// Does something.
void f((String, int) r) {
f(^);
}
''';
var expectedLabel = 'f((String, int) r)';
var expectedDoc = 'Does something.';
await _expectSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: '(String, int) r'),
],
);
}
Future<void> test_params_requiredNamed() async {
// This test requires support for the "required" keyword.
var content = '''
/// Does foo.
foo(String s, {bool? b = true, required bool a}) {
foo(^);
}
''';
var expectedLabel = 'foo(String s, {bool? b = true, required bool a})';
var expectedDoc = 'Does foo.';
await _expectSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'String s'),
ParameterInformation(label: 'bool? b = true'),
ParameterInformation(label: 'required bool a'),
],
);
}
Future<void> test_simple() async {
var content = '''
/// Does foo.
foo(String s, int i) {
foo(^);
}
''';
var expectedLabel = 'foo(String s, int i)';
var expectedDoc = 'Does foo.';
await _expectSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'String s'),
ParameterInformation(label: 'int i'),
],
);
}
Future<void> test_triggerCharacter_invalidLocation() async {
// The client will automatically trigger when the user types ( so we need to
// ignore it when we're not in a suitable location.
var content = '''
/// Does foo.
foo(String s, int i) {
foo('this is a (^test');
}
''';
// Expect no result.
await expectNoSignature(
content,
context: SignatureHelpContext(
triggerKind: SignatureHelpTriggerKind.TriggerCharacter,
isRetrigger: false,
),
);
}
Future<void> test_triggerCharacter_validLocation() async {
var content = '''
/// Does foo.
foo(String s, int i) {
foo(^
}
''';
var expectedLabel = 'foo(String s, int i)';
var expectedDoc = 'Does foo.';
await _expectSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'String s'),
ParameterInformation(label: 'int i'),
],
context: SignatureHelpContext(
triggerKind: SignatureHelpTriggerKind.Invoked,
isRetrigger: false,
));
}
Future<void> test_typeArgs_dartDocPreference_full() =>
assertArgsDocumentation('full',
includesSummary: true, includesFull: true);
Future<void> test_typeArgs_dartDocPreference_none() =>
assertArgsDocumentation('none',
includesSummary: false, includesFull: false);
Future<void> test_typeArgs_dartDocPreference_summary() =>
assertArgsDocumentation('summary',
includesSummary: true, includesFull: false);
/// No preference should result in full docs.
Future<void> test_typeArgs_dartDocPreference_unset() =>
assertArgsDocumentation(null, includesSummary: true, includesFull: true);
Future<void> test_typeParams_class() async {
var content = '''
/// My Foo.
class Foo<T1, T2 extends String> {}
class Bar extends Foo<^> {}
''';
const expectedLabel = 'class Foo<T1, T2 extends String>';
const expectedDoc = 'My Foo.';
await _expectSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'T1'),
ParameterInformation(label: 'T2 extends String'),
],
);
}
Future<void> test_typeParams_function() async {
var content = '''
/// My Foo.
void foo<T1, T2 extends String>() {
foo<^>();
}
''';
const expectedLabel = 'void foo<T1, T2 extends String>()';
const expectedDoc = 'My Foo.';
await _expectSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'T1'),
ParameterInformation(label: 'T2 extends String'),
],
);
}
Future<void> test_typeParams_method() async {
var content = '''
class Foo {
/// My Foo.
void foo<T1, T2 extends String>() {
foo<^>();
}
}
''';
const expectedLabel = 'void foo<T1, T2 extends String>()';
const expectedDoc = 'My Foo.';
await _expectSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'T1'),
ParameterInformation(label: 'T2 extends String'),
],
);
}
Future<void> test_unopenFile() async {
var content = '''
/// Does foo.
foo(String s, int i) {
foo(^);
}
''';
var expectedLabel = 'foo(String s, int i)';
var expectedDoc = 'Does foo.';
await _expectSignature(
content,
expectedLabel,
expectedDoc,
[
ParameterInformation(label: 'String s'),
ParameterInformation(label: 'int i'),
],
state: _FileState.closed,
);
}
}
enum _FileState { open, closed }