blob: c28a131afe4634815c86c4140d6f4020fd1ca5a8 [file] [log] [blame]
// Copyright (c) 2022, 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/services/refactoring/move_top_level_to_file.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'refactoring_test_support.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(MoveTopLevelToFileTest);
});
}
@reflectiveTest
class MoveTopLevelToFileTest extends RefactoringTest {
/// Simple file content with a single class named 'A'.
static const simpleClassContent = '''
class ^A {}
''';
/// The title of the refactor when using [simpleClassContent].
static const simpleClassRefactorTitle = "Move 'A' to file";
@override
String get refactoringName => MoveTopLevelToFile.commandName;
/// Replaces the "Save URI" argument in [action].
void replaceSaveUriArgument(CodeAction action, Uri newFileUri) {
final arguments = getRefactorCommandArguments(action);
// The filename is the first item we prompt for so is first in the
// arguments.
arguments[0] = newFileUri.toString();
}
@failingTest
Future<void> test_copyFileHeader() async {
var originalSource = '''
// File header.
class A {}
class ClassToMove^ {}
class B {}
''';
var modifiedSource = '''
// File header.
class A {}
class B {}
''';
var declarationName = 'ClassToMove';
var newFileName = 'class_to_move.dart';
var newFileContent = '''
// File header.
class ClassToMove {}
''';
await _singleDeclaration(
originalSource: originalSource,
modifiedSource: modifiedSource,
declarationName: declarationName,
newFileName: newFileName,
newFileContent: newFileContent);
}
Future<void> test_existingFile() async {
addTestSource(simpleClassContent);
/// Existing new file contents where 'ClassToMove' will be moved to.
final newFilePath = join(projectFolderPath, 'lib', 'a.dart');
addSource(newFilePath, '''
int? a;
''');
/// Expected updated new file contents.
const expectedNewFileContent = '''
class A {}
int? a;
''';
await initializeServer();
final action = await expectCodeAction(simpleClassRefactorTitle);
await executeRefactor(action);
expect(content[newFilePath], expectedNewFileContent);
}
Future<void> test_imports_referenceFromMovingToImported() async {
var originalSource = '''
import 'dart:io';
class A {}
class B^ {
File? f;
}
''';
var modifiedSource = '''
import 'dart:io';
class A {}
''';
var declarationName = 'B';
var newFileName = 'b.dart';
var newFileContent = '''
import 'dart:io';
class B {
File? f;
}
''';
await _singleDeclaration(
originalSource: originalSource,
modifiedSource: modifiedSource,
declarationName: declarationName,
newFileName: newFileName,
newFileContent: newFileContent);
}
Future<void> test_imports_referenceFromMovingToStaying() async {
var originalSource = '''
class A {}
class ClassToMove^ extends A {}
''';
var modifiedSource = '''
class A {}
''';
var declarationName = 'ClassToMove';
var newFileName = 'class_to_move.dart';
var newFileContent = '''
import 'package:test/main.dart';
class ClassToMove extends A {}
''';
await _singleDeclaration(
originalSource: originalSource,
modifiedSource: modifiedSource,
declarationName: declarationName,
newFileName: newFileName,
newFileContent: newFileContent);
}
Future<void> test_imports_referenceFromStayingToMoving() async {
var originalSource = '''
class A extends B {}
class B^ {}
''';
var modifiedSource = '''
import 'package:test/b.dart';
class A extends B {}
''';
var declarationName = 'B';
var newFileName = 'b.dart';
var newFileContent = '''
class B {}
''';
await _singleDeclaration(
originalSource: originalSource,
modifiedSource: modifiedSource,
declarationName: declarationName,
newFileName: newFileName,
newFileContent: newFileContent);
}
Future<void> test_imports_referenceInThirdFile_noPrefix() async {
var originalSource = '''
class A {}
class B^ {}
''';
var modifiedSource = '''
class A {}
''';
var declarationName = 'B';
var newFileName = 'b.dart';
var newFileContent = '''
class B {}
''';
var otherFilePath = '$projectFolderPath/lib/c.dart';
var otherFileContent = '''
import 'package:test/main.dart';
B? b;
''';
var modifiedOtherFileContent = '''
import 'package:test/b.dart';
import 'package:test/main.dart';
B? b;
''';
await _singleDeclaration(
originalSource: originalSource,
modifiedSource: modifiedSource,
declarationName: declarationName,
newFileName: newFileName,
newFileContent: newFileContent,
otherFilePath: otherFilePath,
otherFileContent: otherFileContent,
modifiedOtherFileContent: modifiedOtherFileContent);
}
@failingTest
Future<void> test_imports_referenceInThirdFile_withMultiplePrefixes() async {
// This fails for two reasons:
// 1. The indexer isn't recording when a top-level element is referenced
// without a prefix.
// 2. The method `DartFileEditBuilderImpl._importLibrary` doesn't support
// importing the same URI with multiple prefixes.
var originalSource = '''
class A {}
class B^ {}
''';
var modifiedSource = '''
class A {}
''';
var declarationName = 'B';
var newFileName = 'b.dart';
var newFileContent = '''
class B {}
''';
var otherFilePath = '$projectFolderPath/lib/c.dart';
var otherFileContent = '''
import 'package:test/main.dart';
import 'package:test/main.dart' as p;
import 'package:test/main.dart' as q;
void f(p.B b, q.B b, B b) {}
''';
var modifiedOtherFileContent = '''
import 'package:test/b.dart';
import 'package:test/b.dart' as p;
import 'package:test/b.dart' as q;
import 'package:test/main.dart';
import 'package:test/main.dart' as p;
import 'package:test/main.dart' as q;
void f(p.B b, q.B b, B b) {}
''';
await _singleDeclaration(
originalSource: originalSource,
modifiedSource: modifiedSource,
declarationName: declarationName,
newFileName: newFileName,
newFileContent: newFileContent,
otherFilePath: otherFilePath,
otherFileContent: otherFileContent,
modifiedOtherFileContent: modifiedOtherFileContent);
}
Future<void> test_imports_referenceInThirdFile_withSinglePrefix() async {
var originalSource = '''
class A {}
class B^ {}
''';
var modifiedSource = '''
class A {}
''';
var declarationName = 'B';
var newFileName = 'b.dart';
var newFileContent = '''
class B {}
''';
var otherFilePath = '$projectFolderPath/lib/c.dart';
var otherFileContent = '''
import 'package:test/main.dart' as p;
p.B? b;
''';
var modifiedOtherFileContent = '''
import 'package:test/b.dart' as p;
import 'package:test/main.dart' as p;
p.B? b;
''';
await _singleDeclaration(
originalSource: originalSource,
modifiedSource: modifiedSource,
declarationName: declarationName,
newFileName: newFileName,
newFileContent: newFileContent,
otherFilePath: otherFilePath,
otherFileContent: otherFileContent,
modifiedOtherFileContent: modifiedOtherFileContent);
}
Future<void> test_kind_class() async {
var originalSource = '''
class A {}
class ClassToMove^ {}
class B {}
''';
var modifiedSource = '''
class A {}
class B {}
''';
var declarationName = 'ClassToMove';
var newFileName = 'class_to_move.dart';
var newFileContent = '''
class ClassToMove {}
''';
await _singleDeclaration(
originalSource: originalSource,
modifiedSource: modifiedSource,
declarationName: declarationName,
newFileName: newFileName,
newFileContent: newFileContent);
}
Future<void> test_multiple() async {
var originalSource = '''
class A {}
class ClassTo[!Move1 {}
class ClassTo!]Move2 {}
class B {}
''';
var modifiedSource = '''
class A {}
class B {}
''';
var newFileName = 'class_to_move1.dart';
var newFileContent = '''
class ClassToMove1 {}
class ClassToMove2 {}
''';
await _multipleDeclarations(
originalSource: originalSource,
modifiedSource: modifiedSource,
count: 2,
newFileName: newFileName,
newFileContent: newFileContent);
}
Future<void> test_none_comment() async {
addTestSource('''
// Comm^ent
class A {}
''');
await initializeServer(experimentalOptInFlag: false);
await expectNoCodeAction(null);
}
Future<void> test_none_directive() async {
addTestSource('''
imp^ort 'dart:core';
class A {}
''');
await initializeServer(experimentalOptInFlag: false);
await expectNoCodeAction(null);
}
Future<void>
test_protocol_available_withClientCommandParameterSupport() async {
addTestSource(simpleClassContent);
await initializeServer();
await expectCodeAction(simpleClassRefactorTitle);
}
Future<void>
test_protocol_available_withoutClientCommandParameterSupport() async {
addTestSource(simpleClassContent);
await initializeServer(commandParameterSupportedKinds: null);
// This refactor is available without command parameter support because
// it has defaults.
await expectCodeAction(simpleClassRefactorTitle);
}
Future<void> test_protocol_clientModifiedValues() async {
addTestSource(simpleClassContent);
/// Filename to inject to replace default.
final newFilePath = join(projectFolderPath, 'lib', 'my_new_class.dart');
final newFileUri = Uri.file(newFilePath);
/// Expected new file content.
const expectedNewFileContent = '''
class A {}
''';
await initializeServer();
final action = await expectCodeAction(simpleClassRefactorTitle);
// Replace the file URI argument with our custom path.
replaceSaveUriArgument(action, newFileUri);
await executeRefactor(action);
expect(content[newFilePath], expectedNewFileContent);
}
Future<void> test_protocol_unavailable_withoutExperimentalOptIn() async {
addTestSource(simpleClassContent);
await initializeServer(experimentalOptInFlag: false);
await expectNoCodeAction(simpleClassRefactorTitle);
}
Future<void> test_protocol_unavailable_withoutFileCreateSupport() async {
addTestSource(simpleClassContent);
await initializeServer(fileCreateSupport: false);
await expectNoCodeAction(simpleClassRefactorTitle);
}
Future<void> test_single_class_withTypeParameters() async {
var originalSource = '''
class A {}
class ClassToMove^<T> {}
class B {}
''';
var modifiedSource = '''
class A {}
class B {}
''';
var declarationName = 'ClassToMove';
var newFileName = 'class_to_move.dart';
var newFileContent = '''
class ClassToMove<T> {}
''';
await _singleDeclaration(
originalSource: originalSource,
modifiedSource: modifiedSource,
declarationName: declarationName,
newFileName: newFileName,
newFileContent: newFileContent);
}
Future<void> test_single_enum() async {
var originalSource = '''
class A {}
enum EnumToMove^ { a, b }
class B {}
''';
var modifiedSource = '''
class A {}
class B {}
''';
var declarationName = 'EnumToMove';
var newFileName = 'enum_to_move.dart';
var newFileContent = '''
enum EnumToMove { a, b }
''';
await _singleDeclaration(
originalSource: originalSource,
modifiedSource: modifiedSource,
declarationName: declarationName,
newFileName: newFileName,
newFileContent: newFileContent);
}
Future<void> test_single_extension() async {
var originalSource = '''
class A {}
extension ExtensionToMove^ on int { }
class B {}
''';
var modifiedSource = '''
class A {}
class B {}
''';
var declarationName = 'ExtensionToMove';
var newFileName = 'extension_to_move.dart';
var newFileContent = '''
extension ExtensionToMove on int { }
''';
await _singleDeclaration(
originalSource: originalSource,
modifiedSource: modifiedSource,
declarationName: declarationName,
newFileName: newFileName,
newFileContent: newFileContent);
}
Future<void> test_single_function_endOfName() async {
var originalSource = '''
class A {}
void functionToMove^() { }
class B {}
''';
var modifiedSource = '''
class A {}
class B {}
''';
var declarationName = 'functionToMove';
var newFileName = 'function_to_move.dart';
var newFileContent = '''
void functionToMove() { }
''';
await _singleDeclaration(
originalSource: originalSource,
modifiedSource: modifiedSource,
declarationName: declarationName,
newFileName: newFileName,
newFileContent: newFileContent);
}
Future<void> test_single_function_middleOfName() async {
var originalSource = '''
class A {}
void functionToMo^ve() { }
class B {}
''';
var modifiedSource = '''
class A {}
class B {}
''';
var declarationName = 'functionToMove';
var newFileName = 'function_to_move.dart';
var newFileContent = '''
void functionToMove() { }
''';
await _singleDeclaration(
originalSource: originalSource,
modifiedSource: modifiedSource,
declarationName: declarationName,
newFileName: newFileName,
newFileContent: newFileContent);
}
Future<void> test_single_mixin() async {
var originalSource = '''
class A {}
mixin MixinToMove^ { }
class B {}
''';
var modifiedSource = '''
class A {}
class B {}
''';
var declarationName = 'MixinToMove';
var newFileName = 'mixin_to_move.dart';
var newFileContent = '''
mixin MixinToMove { }
''';
await _singleDeclaration(
originalSource: originalSource,
modifiedSource: modifiedSource,
declarationName: declarationName,
newFileName: newFileName,
newFileContent: newFileContent);
}
Future<void> test_single_typedef() async {
var originalSource = '''
class A {}
typedef TypeToMove^ = void Function();
class B {}
''';
var modifiedSource = '''
class A {}
class B {}
''';
var declarationName = 'TypeToMove';
var newFileName = 'type_to_move.dart';
var newFileContent = '''
typedef TypeToMove = void Function();
''';
await _singleDeclaration(
originalSource: originalSource,
modifiedSource: modifiedSource,
declarationName: declarationName,
newFileName: newFileName,
newFileContent: newFileContent);
}
Future<void> test_single_variable() async {
var originalSource = '''
class A {}
int variableT^oMove = 3;
class B {}
''';
var modifiedSource = '''
class A {}
class B {}
''';
var declarationName = 'variableToMove';
var newFileName = 'variable_to_move.dart';
var newFileContent = '''
int variableToMove = 3;
''';
await _singleDeclaration(
originalSource: originalSource,
modifiedSource: modifiedSource,
declarationName: declarationName,
newFileName: newFileName,
newFileContent: newFileContent);
}
Future<void> _multipleDeclarations(
{required String originalSource,
required String modifiedSource,
required int count,
required String newFileName,
required String newFileContent,
String? otherFilePath,
String? otherFileContent,
String? modifiedOtherFileContent}) async {
await _refactor(
originalSource: originalSource,
modifiedSource: modifiedSource,
actionTitle: "Move $count declarations to file",
newFileName: newFileName,
newFileContent: newFileContent,
otherFilePath: otherFilePath,
otherFileContent: otherFileContent,
modifiedOtherFileContent: modifiedOtherFileContent);
}
Future<void> _refactor(
{required String originalSource,
required String modifiedSource,
required String actionTitle,
required String newFileName,
required String newFileContent,
String? otherFilePath,
String? otherFileContent,
String? modifiedOtherFileContent}) async {
addTestSource(originalSource);
if (otherFilePath != null) {
addSource(otherFilePath, otherFileContent!);
}
/// Expected new file path/content.
final expectedNewFilePath = join(projectFolderPath, 'lib', newFileName);
await initializeServer();
final action = await expectCodeAction(actionTitle);
await executeRefactor(action);
expect(content[mainFilePath], modifiedSource);
// Check the new file was added to `content`. If no CreateFile resource
// was sent, the executeRefactor helper would've thrown when trying to
// apply the changes.
expect(content[expectedNewFilePath], newFileContent);
if (modifiedOtherFileContent != null) {
expect(content[convertPath(otherFilePath!)], modifiedOtherFileContent);
}
}
Future<void> _singleDeclaration(
{required String originalSource,
required String modifiedSource,
required String declarationName,
required String newFileName,
required String newFileContent,
String? otherFilePath,
String? otherFileContent,
String? modifiedOtherFileContent}) async {
await _refactor(
originalSource: originalSource,
modifiedSource: modifiedSource,
actionTitle: "Move '$declarationName' to file",
newFileName: newFileName,
newFileContent: newFileContent,
otherFilePath: otherFilePath,
otherFileContent: otherFileContent,
modifiedOtherFileContent: modifiedOtherFileContent);
}
}