blob: bd64b8247d75b8ceab1b048f4ad4e04b11c93f35 [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:analysis_server/src/legacy_analysis_server.dart';
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analyzer/src/test_utilities/test_code_format.dart';
import 'package:analyzer_utilities/test/experiments/experiments.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../tool/lsp_spec/matchers.dart';
import '../utils/test_code_extensions.dart';
import 'server_abstract.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(HoverTest);
});
}
@reflectiveTest
class HoverTest extends AbstractLspAnalysisServerTest {
@override
AnalysisServerOptions get serverOptions =>
AnalysisServerOptions()..enabledExperiments = experimentsForTests;
/// Checks whether the correct types of documentation are returned in a Hover
/// based on [preference].
Future<void> assertDocumentation(
String? preference, {
required bool includesSummary,
required bool includesFull,
}) async {
var code = TestCode.parse('''
/// Summary.
///
/// Full.
class ^A {}
''');
await provideConfig(
initialize,
{
if (preference != null) 'documentation': preference,
},
);
await openFile(mainFileUri, code.code);
await initialAnalysis;
var hover = await getHover(mainFileUri, code.position.position);
var hoverContents = _getStringContents(hover!);
if (includesSummary) {
expect(hoverContents, contains('Summary.'));
} else {
expect(hoverContents, isNot(contains('Summary.')));
}
if (includesFull) {
expect(hoverContents, contains('Full.'));
} else {
expect(hoverContents, isNot(contains('Full.')));
}
}
Future<void> assertMarkdownContents(String content, Matcher matcher) async {
setHoverContentFormat([MarkupKind.Markdown]);
var code = TestCode.parse(content);
await initialize();
await openFile(mainFileUri, code.code);
await initialAnalysis;
var hover = await getHover(mainFileUri, code.position.position);
expect(hover, isNotNull);
expect(hover!.range, equals(code.range.range));
expect(hover.contents, isNotNull);
var markup = _getMarkupContents(hover);
expect(markup.kind, equals(MarkupKind.Markdown));
expect(markup.value, matcher);
}
Future<void> assertPlainTextContents(String content, Matcher matcher) async {
setHoverContentFormat([MarkupKind.PlainText]);
var code = TestCode.parse(content);
await initialize();
await openFile(mainFileUri, code.code);
await initialAnalysis;
var hover = await getHover(mainFileUri, code.position.position);
expect(hover, isNotNull);
expect(hover!.range, equals(code.range.range));
expect(hover.contents, isNotNull);
var markup = _getMarkupContents(hover);
expect(markup.kind, equals(MarkupKind.PlainText));
expect(markup.value, matcher);
}
Future<void> assertStringContents(
String content,
Matcher matcher, {
bool waitForAnalysis = false,
bool withOpenFile = true,
}) async {
var code = TestCode.parse(content);
var initialAnalysis = waitForAnalysis ? waitForAnalysisComplete() : null;
await initialize();
if (withOpenFile) {
await openFile(mainFileUri, code.code);
} else {
newFile(mainFilePath, code.code);
}
await initialAnalysis;
var hover = await getHover(mainFileUri, code.position.position);
expect(hover, isNotNull);
expect(hover!.range, equals(code.range.range));
expect(hover.contents, isNotNull);
var contents = _getStringContents(hover);
expect(contents, matcher);
}
Future<void> test_dartDoc_macros() => assertStringContents(
waitForAnalysis: true,
'''
/// {@template template_name}
/// This is shared content.
/// {@endtemplate}
const String? foo = null;
/// {@macro template_name}
const String? [!f^oo2!] = null;
''',
endsWith('This is shared content.'),
);
Future<void> test_dartDocPreference_full() =>
assertDocumentation('full', includesSummary: true, includesFull: true);
Future<void> test_dartDocPreference_none() =>
assertDocumentation('none', includesSummary: false, includesFull: false);
Future<void> test_dartDocPreference_summary() =>
assertDocumentation('summary',
includesSummary: true, includesFull: false);
/// No preference should result in full docs.
Future<void> test_dartDocPreference_unset() =>
assertDocumentation(null, includesSummary: true, includesFull: true);
Future<void> test_enum_member() async {
var content = '''
enum MyEnum { one }
void f() {
MyEnum.[!o^ne!];
}
''';
var expected = '''
```dart
MyEnum one
```
Type: `MyEnum`
*package:test/main.dart*''';
await assertStringContents(content, equals(expected));
}
Future<void> test_enum_values() async {
var content = '''
enum MyEnum { one }
void f() {
MyEnum.[!va^lues!];
}
''';
var expected = '''
```dart
List<MyEnum> get values
```
Type: `List<MyEnum>`
*package:test/main.dart*''';
await assertStringContents(content, equals(expected));
}
Future<void> test_forLoop_declaredVariable() async {
var content = '''
void f() {
for (var [!ii^i!] in <String>[]) {}
}
''';
var expected = '''
```dart
String iii
```
Type: `String`''';
await assertStringContents(content, equals(expected));
}
Future<void> test_forLoop_variableReference() async {
var content = '''
void f() {
for (var iii in <String>[]) {
print([!ii^i!]);
}
}
''';
var expected = '''
```dart
String iii
```
Type: `String`''';
await assertStringContents(content, equals(expected));
}
Future<void> test_function_startOfParameterList() => assertStringContents(
'''
/// This is a function.
void [!abc!]^() {}
''',
contains('This is a function.'),
);
Future<void> test_function_startOfTypeParameterList() => assertStringContents(
'''
/// This is a function.
void [!abc!]^<T>(T a) {}
''',
contains('This is a function.'),
);
Future<void> test_hover_bad_position() async {
await initialize();
await openFile(mainFileUri, '');
await expectLater(
() => getHover(mainFileUri, Position(line: 999, character: 999)),
throwsA(isResponseError(ServerErrorCodes.InvalidFileLineCol)),
);
}
Future<void> test_markdown_isFormattedForDisplay() async {
var content = '''
/// This is a string.
///
/// {@template foo}
/// With some [refs] and some
/// [links](https://www.dartlang.org/)
/// {@endtemplate foo}
///
/// ```dart sample
/// print();
/// ```
String? [!a^bc!];
''';
var expectedHoverContent = '''
```dart
String? abc
```
Type: `String?`
*package:test/main.dart*
---
This is a string.
With some [refs] and some
[links](https://www.dartlang.org/)
```dart
print();
```
'''
.trim();
await assertMarkdownContents(content, equals(expectedHoverContent));
}
Future<void> test_markdown_simple() => assertMarkdownContents(
'''
/// This is a string.
String [!a^bc!] = '';
''',
contains('This is a string.'),
);
Future<void> test_method_startOfParameterList() => assertStringContents(
'''
class A {
/// This is a method.
void [!abc!]^() {}
}
''',
contains('This is a method.'),
);
Future<void> test_method_startOfTypeParameterList() => assertStringContents(
'''
class A {
/// This is a method.
String [!abc!]^<T>(T a) => '';
}
''',
contains('This is a method.'),
);
Future<void> test_noElement() async {
var code = TestCode.parse('''
String? abc;
^
int? a;
''');
await initialize();
await openFile(mainFileUri, code.code);
var hover = await getHover(mainFileUri, code.position.position);
expect(hover, isNull);
}
Future<void> test_nonDartFile() async {
await initialize();
await openFile(pubspecFileUri, simplePubspecContent);
var hover = await getHover(pubspecFileUri, startOfDocPos);
expect(hover, isNull);
}
Future<void> test_nullableTypes() async {
var content = '''
String? [!a^bc!];
''';
var expectedHoverContent = '''
```dart
String? abc
```
Type: `String?`
*package:test/main.dart*
'''
.trim();
await assertStringContents(content, equals(expectedHoverContent));
}
Future<void> test_pattern_assignment_left() => assertStringContents(
'''
void f(String a, String b) {
(b, [!a^!]) = (a, b);
}
''',
contains('Type: `String`'),
);
Future<void> test_pattern_assignment_list() => assertStringContents(
'''
void f(List<int> x, num a) {
[[!a^!]] = x;
}
''',
contains('num a'),
);
Future<void> test_pattern_assignment_right() => assertStringContents(
'''
void f(String a, String b) {
(b, a) = ([!a^!], b);
}
''',
contains('Type: `String`'),
);
Future<void> test_pattern_cast_typeName() => assertStringContents(
'''
void f((num, Object) record) {
var (i as int, s as [!St^ring!]) = record;
}
''',
contains('class String'),
);
Future<void> test_pattern_map() => assertStringContents(
'''
void f(x) {
switch (x) {
case {0: [!Str^ing!] a}:
break;
}
}
''',
contains('class String'),
);
Future<void> test_pattern_map_typeArguments() => assertStringContents(
'''
void f(x) {
switch (x) {
case <int, [!Str^ing!]>{0: var a}:
break;
}
}
''',
contains('class String'),
);
Future<void> test_pattern_nullAssert() => assertStringContents(
'''
void f((int?, int?) position) {
var ([!x^!]!, y!) = position;
}
''',
contains('Type: `int`'),
);
Future<void> test_pattern_nullCheck() => assertStringContents(
'''
void f(String? maybeString) {
switch (maybeString) {
case var [!s^!]?:
}
}
''',
contains('Type: `String`'),
);
Future<void> test_pattern_object_fieldName() => assertStringContents(
'''
double calculateArea(Shape shape) =>
switch (shape) {
Square([!leng^th!]: var l) => l * l,
};
sealed class Shape { }
class Square extends Shape {
/// The length.
double get length => 0;
}
''',
allOf([
contains('double get length'),
contains('The length.'),
]),
);
Future<void> test_pattern_object_typeName() => assertStringContents(
'''
double calculateArea(Shape shape) =>
switch (shape) {
[!Squ^are!](length: var l) => l * l,
};
sealed class Shape { }
/// A square.
class Square extends Shape {
double get length => 0;
}
''',
contains('A square.'),
);
Future<void> test_pattern_record_fieldName() => assertStringContents(
'''
void f(({int foo}) x, num a) {
([!fo^o!]: a,) = x;
}
''',
contains('Type: `int`'),
);
Future<void> test_pattern_record_fieldValue() => assertStringContents(
'''
void f(({int foo}) x, num a) {
(foo: [!a^!],) = x;
}
''',
contains('Type: `num`'),
);
Future<void> test_pattern_record_variable() => assertStringContents(
'''
void f(({int foo}) x, num a) {
(foo: a,) = [!x^!];
}
''',
contains('Type: `({int foo})`'),
);
Future<void> test_pattern_relational_variable() => assertStringContents(
'''
String f(int char) {
const zero = 0;
return switch (char) {
== [!ze^ro!] => 'zero',
_ => '',
};
}
''',
contains('Type: `int`'),
);
Future<void> test_pattern_variable_wildcard() => assertStringContents(
'''
void f() {
var a = (1, 2);
var ([!^_!], _) = a;
}
''',
contains('Type: `int`'),
);
Future<void> test_pattern_variable_wildcard_annotated() =>
assertStringContents(
'''
void f() {
var a = (1, 2);
var (int [!^_!], _) = a;
}
''',
contains('Type: `int`'),
);
Future<void> test_plainText_simple() => assertPlainTextContents(
'''
/// This is a string.
String? [!a^bc!];
''',
contains('This is a string.'),
);
Future<void> test_promotedTypes() async {
var content = '''
void f(aaa) {
if (aaa is String) {
print([!aa^a!]);
}
}
''';
var expectedHoverContent = '''
```dart
dynamic aaa
```
Type: `String`
'''
.trim();
await assertStringContents(content, equals(expectedHoverContent));
}
Future<void> test_range_multiLineConstructorCall() => assertStringContents(
'''
final a = new [!Str^ing.fromCharCodes!]([
1,
2,
]);
''',
contains('String String.fromCharCodes('),
);
Future<void> test_recordLiteral_named() => assertStringContents(
r'''
void f(({int f1, int f2}) r) {
r.[!f^1!];
}
''',
contains('Type: `int`'),
);
Future<void> test_recordLiteral_positional() => assertStringContents(
r'''
void f((int, int) r) {
r.[!$^1!];
}
''',
contains('Type: `int`'),
);
Future<void> test_recordType_parameter() => assertStringContents(
'''
Object f(([!dou^ble!], double) param) {
return (1.0, 1.0);
}
''',
contains('class double'),
);
Future<void> test_recordType_return() => assertStringContents(
'''
([!dou^ble!], double) f() {
return (1.0, 1.0);
}
''',
contains('class double'),
);
Future<void> test_signatureFormatting_multiLine() => assertStringContents(
'''
class Foo {
Foo(String arg1, String arg2, [String? arg3]);
}
void f() {
var a = [!Fo^o!]('', '');
}
''',
startsWith('''
```dart
(new) Foo Foo(
String arg1,
String arg2, [
String? arg3,
])
```'''),
);
Future<void> test_signatureFormatting_singleLine() => assertStringContents(
'''
class Foo {
Foo(String a, String b);
}
void f() {
var a = [!Fo^o!]('', '');
}
''',
startsWith('''
```dart
(new) Foo Foo(String a, String b)
```'''),
);
Future<void> test_staticType_field() async {
var content = '''
class A<T> {
late final T? myField;
}
final data = A<String?>().[!myF^ield!];
''';
var expectedHoverContent = '''
```dart
T? myField
```
Type: `String?`
*package:test/main.dart*
'''
.trim();
await assertStringContents(content, equals(expectedHoverContent));
}
Future<void> test_staticType_getter() async {
var content = '''
class A {
String get myGetter => '';
}
final data = A().[!myG^etter!];
''';
var expectedHoverContent = '''
```dart
String get myGetter
```
Type: `String`
*package:test/main.dart*
'''
.trim();
await assertStringContents(content, equals(expectedHoverContent));
}
Future<void> test_staticType_getter_generic() async {
var content = '''
class A<T> {
late final T? myField;
T? get myGetter => myField;
}
final data = A<String?>().[!myG^etter!];
''';
var expectedHoverContent = '''
```dart
T? get myGetter
```
Type: `String?`
*package:test/main.dart*
'''
.trim();
await assertStringContents(content, equals(expectedHoverContent));
}
Future<void> test_staticType_setter() async {
var content = '''
class A {
set mySetter(String value) {}
}
void f() {
A().[!myS^etter!] = '';
}
''';
var expectedHoverContent = '''
```dart
set mySetter(String value)
```
Type: `String`
*package:test/main.dart*
'''
.trim();
await assertStringContents(content, equals(expectedHoverContent));
}
Future<void> test_staticType_setter_generic() async {
var content = '''
class A<T> {
set mySetter(T value) {}
}
void f() {
A<String>().[!myS^etter!] = '';
}
''';
var expectedHoverContent = '''
```dart
set mySetter(T value)
```
Type: `String`
*package:test/main.dart*
'''
.trim();
await assertStringContents(content, equals(expectedHoverContent));
}
Future<void> test_string_noDocComment() async {
var content = '''
String? [!a^bc!];
''';
var expectedHoverContent = '''
```dart
String? abc
```
Type: `String?`
*package:test/main.dart*
'''
.trim();
await assertStringContents(content, equals(expectedHoverContent));
}
Future<void> test_string_reflectsLatestEdits() async {
var original = TestCode.parse('''
/// Original string.
String? [!a^bc!];
''');
var updated = TestCode.parse('''
/// Updated string.
String? [!a^bc!];
''');
await initialize();
await openFile(mainFileUri, original.code);
var hover = await getHover(mainFileUri, original.position.position);
expect(hover, isNotNull);
var contents = _getStringContents(hover!);
expect(contents, contains('Original'));
await replaceFile(222, mainFileUri, updated.code);
hover = await getHover(mainFileUri, updated.position.position);
expect(hover, isNotNull);
contents = _getStringContents(hover!);
expect(contents, contains('Updated'));
}
Future<void> test_string_simple() async {
var content = '''
/// This is a string.
String? [!a^bc!];
''';
var expected = '''
```dart
String? abc
```
Type: `String?`
*package:test/main.dart*
---
This is a string.''';
await assertStringContents(content, equals(expected));
}
Future<void> test_unopenFile() async {
var content = '''
/// This is a string.
String? [!a^bc!];
''';
var expected = '''
```dart
String? abc
```
Type: `String?`
*package:test/main.dart*
---
This is a string.''';
await assertStringContents(withOpenFile: false, content, equals(expected));
}
MarkupContent _getMarkupContents(Hover hover) {
return hover.contents.map(
(t1) => t1,
(t2) => throw 'Hover contents were String, not MarkupContent',
);
}
String _getStringContents(Hover hover) {
return hover.contents.map(
(t1) => throw 'Hover contents were MarkupContent, not String',
(t2) => t2,
);
}
}