blob: 6d2738b37b1c08a602737013d5ad19acf9cb2924 [file] [log] [blame]
// Copyright (c) 2019, 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/lsp/constants.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 '../tool/lsp_spec/matchers.dart';
import '../utils/test_code_extensions.dart';
import 'server_abstract.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(DocumentHighlightsTest);
});
}
@reflectiveTest
class DocumentHighlightsTest extends AbstractLspAnalysisServerTest {
Future<void> test_bound_topLevelVariable_wildcard() => _testMarkedContent('''
var /*[0*/_/*0]*/ = 1;
void f() {
var _ = 2;
print(/*[1*/_/*1]*/);
}
''');
Future<void> test_dartCode_issue5369_field() => _testMarkedContent('''
class A {
var /*[0*/a/*0]*/ = [''].where((_) => true).toList();
List<String> f() {
return /*[1*/a/*1]*/;
}
}
var a; // Not a reference
''');
Future<void> test_dartCode_issue5369_functionType() => _testMarkedContent('''
class A {
String m({ required String Function(String input) /*[0*/f/*0]*/ }) {
return /*[1*/f/*1]*/('');
}
}
var f; // Not a reference
''');
Future<void> test_dartCode_issue5369_localVariable() => _testMarkedContent('''
class A {
List<String> f() {
var /*[0*/a/*0]*/ = [''].where((_) => true).toList();
return /*[1*/a/*1]*/;
}
}
var a; // Not a reference
''');
Future<void> test_dartCode_issue5369_topLevelVariable() =>
_testMarkedContent('''
var /*[0*/a/*0]*/ = [''].where((_) => true).toList();
var b = /*[1*/a/*1]*/;
''');
Future<void> test_dotShorthand_class() => _testMarkedContent('''
A topA = ./*[0*/a/*0]*/;
class A {
static A get /*[1*/a/*1]*/ => A();
}
void fn(A a) => print(a);
void f() {
A a = ./*[2*/a/*2]*/;
fn(./*[3*/a/*3]*/);
A aa = A./*[4*/a/*4]*/;
}
''');
Future<void> test_dotShorthand_enum() => _testMarkedContent('''
const A constA = ./*[0*/a/*0]*/;
enum A { /*[1*/a/*1]*/ }
void fn(A a) => print(a);
void f() {
A a = ./*[2*/a/*2]*/;
fn(./*[3*/a/*3]*/);
A aa = A./*[4*/a/*4]*/;
}
''');
Future<void> test_dotShorthand_extensionType() => _testMarkedContent('''
A topA = ./*[0*/a/*0]*/;
extension type A(int x) {
static A get /*[1*/a/*1]*/ => A(1);
}
void fn(A a) => print(a);
void f() {
A a = ./*[2*/a/*2]*/;
fn(./*[3*/a/*3]*/);
A aa = A./*[4*/a/*4]*/;
}
''');
Future<void> test_forInLoop() => _testMarkedContent('''
void f() {
for (final /*[0*/x/*0]*/ in []) {
/*[1*/x/*1]*/;
}
}
''');
Future<void> test_formalParameters_closure() => _testMarkedContent('''
void f(void Function(int) _) {}
void g() => f((/*[0*/variable/*0]*/) {
print(/*[1*/variable/*1]*/);
});
''');
Future<void> test_formalParameters_function() => _testMarkedContent('''
void f(int /*[0*/parameter/*0]*/) {
print(/*[1*/parameter/*1]*/);
}
''');
Future<void> test_formalParameters_method() => _testMarkedContent('''
class C {
void m(int /*[0*/parameter/*0]*/) {
print(/*[1*/parameter/*1]*/);
}
}
''');
Future<void> test_functions() => _testMarkedContent('''
/*[0*/main/*0]*/() {
/*[1*/main/*1]*/();
}
''');
Future<void> test_invalidLineByOne() async {
// Test that requesting a line that's too high by one returns a valid
// error response instead of throwing.
const content = '// single line';
await initialize();
await openFile(mainFileUri, content);
// Lines are zero-based so 1 is invalid.
var pos = Position(line: 1, character: 0);
var request = getDocumentHighlights(mainFileUri, pos);
await expectLater(
request,
throwsA(isResponseError(ServerErrorCodes.InvalidFileLineCol)),
);
}
Future<void> test_keyword_loop_do() => _testLoop('do', '', 'while (true);');
Future<void> test_keyword_loop_for() => _testLoop('for', '(;;)', '');
Future<void> test_keyword_loop_while() => _testLoop('while', '(true)', '');
Future<void> test_keyword_loopWithSwitch_loopExit() => _testMarkedContent('''
void f(int i) {
/*[0*/for/*0]*/ (;;) {
/*[1*/break/*1]*/;
/*[2*/continue/*2]*/;
switch (i) {
case 1:
break;
/*[3*/continue/*3]*/;
case 2:
break;
/*[4*/continue/*4]*/;
}
}
}
''');
Future<void> test_keyword_loopWithSwitch_switchExit() =>
_testMarkedContent('''
void f(int i) {
for (;;) {
break;
continue;
/*[0*/switch/*0]*/ (i) {
case 1:
/*[1*/break/*1]*/;
continue;
case 2:
/*[2*/break/*2]*/;
continue;
}
}
}
''');
Future<void> test_keyword_return_function() => _testMarkedContent('''
int f() {
if (true) /*[0*/return/*0]*/ 1;
/*[1*/return/*1]*/ 2;
}
''');
Future<void> test_keyword_return_insideLoop() => _testMarkedContent('''
int f(int i) {
for (;;) {
switch (i) {
case 1:
/*[0*/return/*0]*/ 1;
case 2:
/*[1*/return/*1]*/ 2;
break;
continue;
}
}
/*[2*/return/*2]*/ 2;
}
''');
Future<void> test_keyword_return_method() => _testMarkedContent('''
class C {
int m() {
if (true) /*[0*/return/*0]*/ 1;
/*[1*/return/*1]*/ 2;
}
}
''');
Future<void> test_keyword_return_nestedClosure() => _testMarkedContent('''
int outerFunction() {
var a = () {
var b = () {
var c = () {
return 1;
};
if (true) /*[0*/return/*0]*/ 1;
/*[1*/return/*1]*/ 0;
};
return 1;
};
return 1;
}
''');
Future<void> test_keyword_return_nestedFunction() => _testMarkedContent('''
int outerFunction() {
int middleFunction() {
int innerFunction() {
return 1;
}
if (true) /*[0*/return/*0]*/ 1;
/*[1*/return/*1]*/ 2;
}
return 1;
}
''');
Future<void> test_keyword_yield_asyncGenerator() => _testMarkedContent('''
Stream<int> outerFunction() async* {
Stream<int> middleFunction() async* {
Stream<int> innerFunction() async* {
yield 1;
yield* Stream.value(0);
}
if (true) /*[0*/yield/*0]*/ 1;
if (true) /*[1*/yield/*1]*/* Stream.value(0);
/*[2*/yield/*2]*/ 2;
/*[3*/yield/*3]*/* Stream.value(0);
}
yield 1;
yield* Stream.value(0);
}
''');
Future<void> test_keyword_yield_syncGenerator() => _testMarkedContent('''
Iterable<int> outerFunction() sync* {
Iterable<int> middleFunction() sync* {
Iterable<int> innerFunction() sync* {
yield 1;
yield* Iterable.empty();
}
if (true) /*[0*/yield/*0]*/ 1;
if (true) /*[1*/yield/*1]*/* Iterable.empty();
/*[2*/yield/*2]*/ 2;
/*[3*/yield/*3]*/* Iterable.empty();
}
yield 1;
yield* Iterable.empty();
}
''');
Future<void> test_localVariable() => _testMarkedContent('''
void f() {
var /*[0*/foo/*0]*/ = 1;
print(/*[1*/foo/*1]*/);
/*[2*/foo/*2]*/ = 2;
}
''');
Future<void> test_method_underscore() => _testMarkedContent('''
class C {
/*[0*/_/*0]*/() {
/*[1*/_/*1]*/();
}
}
''');
Future<void> test_nonDartFile() async {
await initialize();
await openFile(pubspecFileUri, simplePubspecContent);
var highlights = await getDocumentHighlights(pubspecFileUri, startOfDocPos);
// Non-Dart files should return empty results, not errors.
expect(highlights, isEmpty);
}
Future<void> test_noResult() => _testMarkedContent('''
void f() {
// This one is in a ^ comment!
}
''');
Future<void> test_onlySelf() => _testMarkedContent('''
void f() {
/*[0*/print/*0]*/('');
}
''');
Future<void> test_onlySelf_wildcard() => _testMarkedContent('''
void f() {
var /*[0*/_/*0]*/ = '';
}
''');
Future<void> test_pattern_object_destructure() => _testMarkedContent('''
void f() {
final MapEntry(:/*[0*/key/*0]*/) = const MapEntry<String, int>('a', 1);
if (const MapEntry('a', 1) case MapEntry(:final /*[1*/ke^y/*1]*/)) {
/*[2*/key/*2]*/;
}
}
''');
Future<void> test_prefix() => _testMarkedContent('''
import '' as /*[0*/p/*0]*/;
class A {
void m() {
/*[1*/p/*1]*/.foo();
print(/*[2*/p/*2]*/.a);
}
}
void foo() {}
/*[3*/p/*3]*/.A? a;
''');
Future<void> test_prefixed() => _testMarkedContent('''
import '' as p;
class /*[0*/A/*0]*/ {}
p./*[1*/A/*1]*/? a;
''');
Future<void> test_shadow_inner() => _testMarkedContent('''
void f() {
var foo = 1;
func() {
var /*[0*/foo/*0]*/ = 2;
print(/*[1*/foo/*1]*/);
}
}
''');
Future<void> test_shadow_outer() => _testMarkedContent('''
void f() {
var /*[0*/foo/*0]*/ = 1;
func() {
var foo = 2;
print(foo);
}
print(/*[1*/foo/*1]*/);
}
''');
Future<void> test_topLevelVariable() => _testMarkedContent('''
String /*[0*/foo/*0]*/ = 'bar';
void f() {
print(/*[1*/foo/*1]*/);
/*[2*/foo/*2]*/ = '';
}
''');
Future<void> test_topLevelVariable_underscore() => _testMarkedContent('''
String /*[0*/_/*0]*/ = 'bar';
void f() {
print(/*[1*/_/*1]*/);
/*[2*/_/*2]*/ = '';
}
''');
Future<void> test_type_class_constructors() async {
await _testMarkedContent('''
class /*[0*/A/*0]*/ {
A(); // Unnamed constructor is own entity
/*[1*/A/*1]*/.named();
}
/*[2*/A/*2]*/ a = A(); // Unnamed constructor is own entity
var b = /*[3*/A/*3]*/.new();
var c = /*[4*/A/*4]*/.new;
''');
}
/// The type name in unnamed constructors are their own entity and not
/// part of the type name.
Future<void> test_type_class_constructors_unnamed() async {
await _testMarkedContent('''
class A {
/*[0*/A/*0]*/();
A.named();
}
A a = /*[1*/A/*1]*/();
var b = A./*[2*/new/*2]*/();
var c = A./*[3*/new/*3]*/;
''');
}
Future<void> test_typeAlias_class_declaration() => _testMarkedContent('''
class MyClass {}
mixin MyMixin {}
class /*[0*/MyAlias/*0]*/ = MyClass with MyMixin;
/*[1*/MyAlias/*1]*/? a;
''');
Future<void> test_typeAlias_class_reference() => _testMarkedContent('''
class MyClass {}
mixin MyMixin {}
class /*[0*/MyAlias/*0]*/ = MyClass with MyMixin;
/*[1*/MyAlias/*1]*/? a;
''');
Future<void> test_typeAlias_function_declaration() => _testMarkedContent('''
typedef /*[0*/myFunc/*0]*/();
/*[1*/myFunc/*1]*/? f;
''');
Future<void> test_typeAlias_function_reference() => _testMarkedContent('''
typedef /*[0*/myFunc/*0]*/();
/*[1*/myFunc/*1]*/? f;
''');
Future<void> test_typeAlias_generic_declaration() => _testMarkedContent('''
typedef /*[0*/TD/*0]*/ = String;
/*[1*/TD/*1]*/? a;
''');
Future<void> test_typeAlias_generic_reference() => _testMarkedContent('''
typedef /*[0*/TD/*0]*/ = String;
/*[1*/TD/*1]*/? a;
''');
/// Create three nested loops for this [loopKeyword] (outer/middle/inner)
/// with all combinations of `break`/`continue` (and with.without labels)
/// and verify that the middle [loopKeyword] and all exit keywords that
/// relate to that loop produce mutual ranges including each other.
Future<void> _testLoop(
String loopKeyword,
String loopStart,
String loopEnd,
) async {
var content = '''
void f() {
outer:
$loopKeyword $loopStart {
middle:
/*[0*/$loopKeyword/*0]*/ $loopStart {
inner:
$loopKeyword $loopStart {
break;
continue;
break inner;
continue inner;
/*[1*/break/*1]*/ middle;
/*[2*/continue/*2]*/ middle;
break outer;
continue outer;
} $loopEnd
/*[3*/break/*3]*/;
/*[4*/continue/*4]*/;
/*[5*/break/*5]*/ middle;
/*[6*/continue/*6]*/ middle;
break outer;
continue outer;
} $loopEnd
break;
continue;
break outer;
continue outer;
} $loopEnd
}
''';
await _testMarkedContent(content);
}
/// Tests highlights in a Dart file using the provided content.
///
/// The content should be marked up using the [TestCode] format.
///
/// If the content contains positions, they will be used to fetch highlights
/// and the resulting ranges verified.
///
/// If the content contains no positions, only ranges, then the start and end
/// of every range will be tested to ensure the full set of ranges are
/// returned mutually for each.
Future<void> _testMarkedContent(String content) async {
var code = TestCode.parse(content);
expect(
code.positions.isNotEmpty || code.ranges.isNotEmpty,
isTrue,
reason: 'At least one position or range should be marked in the content',
);
await initialize();
await openFile(mainFileUri, code.code);
var positions =
code.positions.isNotEmpty
? code.positions.map((position) => position.position)
: code.ranges.expand(
(range) => [range.range.start, range.range.end],
);
for (var position in positions) {
var highlights = await getDocumentHighlights(mainFileUri, position);
if (code.ranges.isEmpty) {
expect(highlights, isEmpty);
} else {
code.verifyRanges(highlights!.map((highlight) => highlight.range));
}
}
}
}