blob: ecae53e3ec131bf62b0ac3aabb1140a677610996 [file] [log] [blame]
// Copyright (c) 2024, 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/lsp_protocol_extensions.dart';
import '../../utils/test_code_extensions.dart';
import '../server_abstract.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(AugmentationCodeLensTest);
defineReflectiveTests(AugmentedCodeLensTest);
});
}
@reflectiveTest
abstract class AbstractAugmentationCodeLensTest
extends AbstractLspAnalysisServerTest {
late TestCode libraryCode;
late TestCode augmentationCode;
/// The name of the setting to control whether [codeLensTitle] CodeLenses
/// are enabled.
///
/// ```js
/// "dart.codeLens": {
/// "augmented": true
/// ^^^^^^^^^
/// }
/// ```
String get codeLensSettingName;
/// The title of the [Command] in the [CodeLens]es being tested.
String get codeLensTitle;
/// The range in [sourceUri] that the CodeLens should appear for.
Range get sourceRange;
/// The document for which the CodeLens request will be sent.
Uri get sourceUri;
/// The range in [targetUri] where the CodeLens should navigate to.
Range get targetRange;
/// The document that the CodeLens should navigate to.
Uri get targetUri;
/// Expects a CodeLens in [sourceUri] at [sourceRange] with the title
/// [codeLensTitle] that navigates to [targetRange] in [targetUri].
Future<void> expectNavigationCodeLens({
Uri? sourceUri,
Range? sourceRange,
Uri? targetUri,
Range? targetRange,
}) async {
sourceUri ??= this.sourceUri;
sourceRange ??= this.sourceRange;
targetUri ??= this.targetUri;
targetRange ??= this.targetRange;
var documentCodeLenses = (await getCodeLens(sourceUri)) ?? [];
var matchingCodeLenses = documentCodeLenses.where(
(codeLens) =>
codeLens.command?.title == codeLensTitle &&
codeLens.range.containsPosition(sourceRange!.start),
);
if (matchingCodeLenses.isEmpty) {
var debugText = documentCodeLenses
.map((codeLens) => ' ${codeLens.command?.title} (${codeLens.range})')
.join('\n');
fail(
'Did not find "$codeLensTitle" ($sourceRange) CodeLens in $sourceUri.\n'
'Found:\n$debugText',
);
} else if (matchingCodeLenses.length > 1) {
var debugText = matchingCodeLenses
.map((codeLens) => ' ${codeLens.command?.title} (${codeLens.range})')
.join('\n');
fail(
'Found multiple "$codeLensTitle" ($sourceRange) CodeLens in $sourceUri.\n'
'Found:\n$debugText',
);
}
var codeLens = matchingCodeLenses.single;
expect(codeLens.range, sourceRange);
// Verify the command/args match what's documented in the our LSP readme.
// Clients will need to provide this command and will implement it as
// documented.
expect(codeLens.command!.command, ClientCommands.goToLocation);
expect(codeLens.command?.arguments, [
Location(
uri: targetUri,
range: targetRange,
).toJson(), // Args are untyped, so toJson() to compare the Map.
]);
}
/// Expects no CodeLens in [uri]/[sourceUri] with the title [codeLensTitle].
Future<void> expectNoCodeLenses([Uri? uri]) async {
var documentCodeLenses = (await getCodeLens(uri ?? sourceUri)) ?? [];
var matchingCodeLenses = documentCodeLenses.where(
(codeLens) => codeLens.command?.title == codeLensTitle,
);
expect(matchingCodeLenses, isEmpty);
}
void setAugmentationContent(String content) {
augmentationCode = TestCode.parse('''
part of 'main.dart';
$content
''');
newFile(mainFileAugmentationPath, augmentationCode.code);
}
void setLibraryContent(String content) {
libraryCode = TestCode.parse('''
part 'main_augmentation.dart';
$content
''');
newFile(mainFilePath, libraryCode.code);
}
@override
void setUp() {
super.setUp();
setClientSupportedCommands([ClientCommands.goToLocation]);
}
@SkippedTest() // TODO(scheglov): implement augmentation
test_available_class() async {
setLibraryContent(r'''
class [!A!] {}
''');
setAugmentationContent(r'''
augment class [!A!] {}
''');
await initialize();
await expectNavigationCodeLens();
}
@SkippedTest() // TODO(scheglov): implement augmentation
test_available_class_getter() async {
setLibraryContent(r'''
class A {
String get [!g!] => '';
}
''');
setAugmentationContent(r'''
augment class A {
augment String get [!g!] => '';
}
''');
await initialize();
await expectNavigationCodeLens();
}
@SkippedTest() // TODO(scheglov): implement augmentation
test_available_class_method() async {
setLibraryContent(r'''
class A {
void [!m!]() {}
}
''');
setAugmentationContent(r'''
augment class A {
augment void [!m!]() {}
}
''');
await initialize();
await expectNavigationCodeLens();
}
@SkippedTest() // TODO(scheglov): implement augmentation
test_available_class_setter() async {
setLibraryContent(r'''
class A {
set [!g!](String value) {}
}
''');
setAugmentationContent(r'''
augment class A {
augment set [!g!](String value) {}
}
''');
await initialize();
await expectNavigationCodeLens();
}
@SkippedTest() // TODO(scheglov): implement augmentation
test_available_enum() async {
setLibraryContent(r'''
enum [!A!] {
one,
}
''');
setAugmentationContent(r'''
augment enum [!A!] {
two,
}
''');
await initialize();
await expectNavigationCodeLens();
}
@SkippedTest() // TODO(scheglov): implement augmentation
test_available_enum_member() async {
setLibraryContent(r'''
enum A {
[!one!],
}
''');
setAugmentationContent(r'''
augment enum A {
augment [!one!],
}
''');
await initialize();
await expectNavigationCodeLens();
}
@SkippedTest() // TODO(scheglov): implement augmentation
test_available_topLevel_function() async {
setLibraryContent(r'''
void [!f!]() {}
''');
setAugmentationContent(r'''
augment void [!f!]() {}
''');
await initialize();
await expectNavigationCodeLens();
}
test_error_nonExistentFile() async {
await initialize();
await expectLater(
expectNoCodeLenses(nonExistentFileUri),
throwsA(
isResponseError(
ServerErrorCodes.InvalidFilePath,
message: 'File does not exist',
),
),
);
}
test_unavailable_disabledEntirely() async {
useSimpleClassAugmentation();
await provideConfig(initialize, {'codeLens': false});
await expectNoCodeLenses();
}
test_unavailable_disabledIndividually() async {
useSimpleClassAugmentation();
await provideConfig(initialize, {
'codeLens': {codeLensSettingName: false},
});
await expectNoCodeLenses();
}
test_unavailable_noClientCommand() async {
setClientSupportedCommands(null);
useSimpleClassAugmentation();
await initialize();
await expectNoCodeLenses();
}
/// Sets up a simple class with augmentation that can be used by tests where
/// the specifics are not important.
void useSimpleClassAugmentation() {
setLibraryContent(r'''
class A {}
''');
setAugmentationContent(r'''
augment class A {}
''');
}
}
/// Run all tests from [AbstractAugmentationCodeLensTest] looking for
/// "Go to Augmentation" that navigates from declarations in [mainFileUri] to
/// augmentations in [mainFileAugmentationUri].
@reflectiveTest
class AugmentationCodeLensTest extends AbstractAugmentationCodeLensTest {
@override
String get codeLensSettingName => 'augmentation';
@override
String get codeLensTitle => 'Go to Augmentation';
@override
Range get sourceRange => libraryCode.range.range;
@override
Uri get sourceUri => mainFileUri;
@override
Range get targetRange => augmentationCode.range.range;
@override
Uri get targetUri => mainFileAugmentationUri;
@SkippedTest() // TODO(scheglov): implement augmentation
test_available_class_augmentationOf_augmentationOf_declarationInAugmentationFile() async {
setLibraryContent(r'');
setAugmentationContent(r'''
class A {}
augment class /*[0*/A/*0]*/ {}
augment class /*[1*/A/*1]*/ {}
''');
await initialize();
await expectNavigationCodeLens(
sourceUri: mainFileAugmentationUri,
sourceRange: augmentationCode.ranges[0].range,
targetUri: mainFileAugmentationUri,
targetRange: augmentationCode.ranges[1].range,
);
}
@SkippedTest() // TODO(scheglov): implement augmentation
test_available_class_augmentationOf_augmentationOf_declarationInLibraryFile() async {
setLibraryContent(r'''
class A {}
''');
setAugmentationContent(r'''
augment class /*[0*/A/*0]*/ {}
augment class /*[1*/A/*1]*/ {}
''');
await initialize();
await expectNavigationCodeLens(
sourceUri: mainFileAugmentationUri,
sourceRange: augmentationCode.ranges[0].range,
targetUri: mainFileAugmentationUri,
targetRange: augmentationCode.ranges[1].range,
);
}
@SkippedTest() // TODO(scheglov): implement augmentation
test_available_class_augmentationOf_declarationInAugmentationFile() async {
setLibraryContent('');
setAugmentationContent(r'''
class /*[0*/A/*0]*/ {}
augment class /*[1*/A/*1]*/ {}
''');
await initialize();
await expectNavigationCodeLens(
sourceUri: mainFileAugmentationUri,
sourceRange: augmentationCode.ranges[0].range,
targetUri: mainFileAugmentationUri,
targetRange: augmentationCode.ranges[1].range,
);
}
}
/// Run all tests from [AbstractAugmentationCodeLensTest] looking for
/// "Go to Augmented" that navigates from augmentations in
/// [mainFileAugmentationUri] to the original declarations in
/// [mainFileAugmentationUri].
@reflectiveTest
class AugmentedCodeLensTest extends AbstractAugmentationCodeLensTest {
@override
String get codeLensSettingName => 'augmented';
@override
String get codeLensTitle => 'Go to Augmented';
@override
Range get sourceRange => augmentationCode.range.range;
@override
Uri get sourceUri => mainFileAugmentationUri;
@override
Range get targetRange => libraryCode.range.range;
@override
Uri get targetUri => mainFileUri;
@SkippedTest() // TODO(scheglov): implement augmentation
test_available_class_augmentationOf_augmentationOf_declarationInAugmentationFile() async {
setLibraryContent(r'');
setAugmentationContent(r'''
class A {}
augment class /*[0*/A/*0]*/ {}
augment class /*[1*/A/*1]*/ {}
''');
await initialize();
await expectNavigationCodeLens(
sourceUri: mainFileAugmentationUri,
sourceRange: augmentationCode.ranges[1].range,
targetUri: mainFileAugmentationUri,
targetRange: augmentationCode.ranges[0].range,
);
}
@SkippedTest() // TODO(scheglov): implement augmentation
test_available_class_augmentationOf_augmentationOf_declarationInLibraryFile() async {
setLibraryContent(r'''
class A {}
''');
setAugmentationContent(r'''
augment class /*[0*/A/*0]*/ {}
augment class /*[1*/A/*1]*/ {}
''');
await initialize();
await expectNavigationCodeLens(
sourceUri: mainFileAugmentationUri,
sourceRange: augmentationCode.ranges[1].range,
targetUri: mainFileAugmentationUri,
targetRange: augmentationCode.ranges[0].range,
);
}
@SkippedTest() // TODO(scheglov): implement augmentation
test_available_class_augmentationOf_declarationInAugmentationFile() async {
setLibraryContent('');
setAugmentationContent(r'''
class /*[0*/A/*0]*/ {}
augment class /*[1*/A/*1]*/ {}
''');
await initialize();
await expectNavigationCodeLens(
sourceUri: mainFileAugmentationUri,
sourceRange: augmentationCode.ranges[1].range,
targetUri: mainFileAugmentationUri,
targetRange: augmentationCode.ranges[0].range,
);
}
}