Copy the file header comment when creating a new file

Change-Id: I5d864c0d138f6d9389c56a312c111da1f9671081
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/265186
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
This commit is contained in:
Brian Wilkerson 2022-10-24 21:14:22 +00:00 committed by Commit Queue
parent b8ffc08d5b
commit 8d925319ca
5 changed files with 252 additions and 3 deletions

View file

@ -4,6 +4,7 @@
import 'package:analysis_server/lsp_protocol/protocol_custom_generated.dart';
import 'package:analysis_server/src/services/refactoring/framework/refactoring_producer.dart';
import 'package:analysis_server/src/utilities/extensions/ast.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
import 'package:analyzer_plugin/utilities/range_factory.dart';
@ -96,8 +97,18 @@ class MoveTopLevelToFile extends RefactoringProducer {
if (importUri == null) {
return;
}
var destinationExists =
result.session.resourceProvider.getFile(destinationFilePath).exists;
String? fileHeader;
if (!destinationExists) {
var headerTokens = result.unit.fileHeader;
if (headerTokens.isNotEmpty) {
var offset = headerTokens.first.offset;
var end = headerTokens.last.end;
fileHeader = utils.getText(offset, end - offset);
}
}
await builder.addDartFileEdit(destinationFilePath, (builder) {
// TODO(brianwilkerson) Copy the file header to the new file.
// TODO(brianwilkerson) Use `ImportedElementsComputer` to add imports
// required by the newly copied code. Better yet, combine that with the
// import analysis used to find unused and unnecessary imports so that we
@ -106,6 +117,10 @@ class MoveTopLevelToFile extends RefactoringProducer {
// TODO(dantup): Ensure the range inserted and deleted match (allowing for
// whitespace), including handling of leading/trailing comments etc.
builder.addInsertion(0, (builder) {
if (fileHeader != null) {
builder.writeln(fileHeader);
builder.writeln();
}
builder.writeln(utils.getNodeText(member.node));
});
});

View file

@ -123,9 +123,54 @@ extension AstNodeExtensions on AstNode {
}
extension CompilationUnitExtension on CompilationUnit {
/// Is `true` if library being analyzed is non-nullable by default.
/// Return the list of tokens that comprise the file header comment for this
/// compilation unit.
///
/// Will return false if the AST structure has not been resolved.
/// If there is no file comment the list will be empty. If the file comment is
/// a block comment the list will contain a single token. If the file comment
/// is comprised of one or more single line comments, then the list will
/// contain all of the tokens, and the comment is assumed to stop at the first
/// line that contains anything other than a single line comment (either a
/// blank line, a directive, a declaration, or a multi-line comment). The list
/// will never include a documentation comment.
List<Token> get fileHeader {
final lineInfo = this.lineInfo;
var firstToken = beginToken;
if (firstToken.type == TokenType.SCRIPT_TAG) {
firstToken = firstToken.next!;
}
var firstComment = firstToken.precedingComments;
if (firstComment == null ||
firstComment.lexeme.startsWith('/**') ||
firstComment.lexeme.startsWith('///')) {
return const [];
} else if (firstComment.lexeme.startsWith('/*')) {
return [firstComment];
} else if (!firstComment.lexeme.startsWith('//')) {
return const [];
}
var header = <Token>[firstComment];
var previousLine = lineInfo.getLocation(firstComment.offset).lineNumber;
var currentToken = firstComment.next;
while (currentToken != null) {
if (!currentToken.lexeme.startsWith('//') ||
currentToken.lexeme.startsWith('///')) {
return header;
}
var currentLine = lineInfo.getLocation(currentToken.offset).lineNumber;
if (currentLine != previousLine + 1) {
return header;
}
header.add(currentToken);
currentToken = currentToken.next;
previousLine = currentLine;
}
return header;
}
/// Return `true` if library being analyzed is non-nullable by default.
///
/// Will return `false` if the AST structure has not been resolved.
bool get isNonNullableByDefault =>
declaredElement?.library.isNonNullableByDefault ?? false;
}

View file

@ -50,6 +50,8 @@ class MoveTopLevelToFileTest extends RefactoringTest {
Future<void> test_class() async {
var originalSource = '''
// File header.
class A {}
class ClassToMove^ {}
@ -57,6 +59,8 @@ class ClassToMove^ {}
class B {}
''';
var modifiedSource = '''
// File header.
class A {}
class B {}
@ -64,6 +68,8 @@ class B {}
var declarationName = 'ClassToMove';
var newFileName = 'class_to_move.dart';
var newFileContent = '''
// File header.
class ClassToMove {}
''';
await _singleDeclaration(

View file

@ -0,0 +1,181 @@
// 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/src/utilities/extensions/ast.dart';
import 'package:test/expect.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../../../services/search/search_engine_test.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(CompilationUnitFileHeaderTest);
});
}
@reflectiveTest
class CompilationUnitFileHeaderTest extends PubPackageResolutionTest {
Future<void> test_afterScriptTag_multiLine() async {
await resolveTestCode('''
#! x
/* a */
class C {}
''');
_assertTokens(['/* a */']);
}
Future<void> test_afterScriptTag_singleLine() async {
await resolveTestCode('''
#! x
// a
// b
class C {}
''');
_assertTokens(['// a', '// b']);
}
Future<void> test_afterScriptTag_withDocComment_multiLine() async {
await resolveTestCode('''
#! x
/* a */
/// b
class C {}
''');
_assertTokens(['/* a */']);
}
Future<void> test_afterScriptTag_withDocComment_singleLine() async {
await resolveTestCode('''
#! x
// a
// b
/// c
class C {}
''');
_assertTokens(['// a', '// b']);
}
Future<void> test_none() async {
await resolveTestCode('''
class C {}
''');
expect(result.unit.fileHeader, isEmpty);
}
Future<void> test_only_multiLine() async {
await resolveTestCode('''
/* a */
class C {}
''');
_assertTokens(['/* a */']);
}
Future<void> test_only_singleLine() async {
await resolveTestCode('''
// a
// b
class C {}
''');
_assertTokens(['// a', '// b']);
}
Future<void> test_onlyDocComment_multiLine() async {
await resolveTestCode('''
/** a */
class C {}
''');
expect(result.unit.fileHeader, isEmpty);
}
Future<void> test_onlyDocComment_singleLine() async {
await resolveTestCode('''
/// a
class C {}
''');
expect(result.unit.fileHeader, isEmpty);
}
Future<void> test_withDocComment_multiLine() async {
await resolveTestCode('''
/* a
*/
/// b
class C {}
''');
_assertTokens(['/* a\n*/']);
}
Future<void> test_withDocComment_singleLine() async {
await resolveTestCode('''
// a
// b
/// c
class C {}
''');
_assertTokens(['// a', '// b']);
}
Future<void> test_withDocComment_singleLine_noBlankLine() async {
await resolveTestCode('''
// a
/// b
class C {}
''');
_assertTokens(['// a']);
}
Future<void> test_withNonDocComment_multiLine_multiLine() async {
await resolveTestCode('''
/* a */
/* b */
class C {}
''');
_assertTokens(['/* a */']);
}
Future<void> test_withNonDocComment_multiLine_singleLine() async {
await resolveTestCode('''
/* a */
// b
class C {}
''');
_assertTokens(['/* a */']);
}
Future<void> test_withNonDocComment_singleLine_multiLine() async {
await resolveTestCode('''
// a
// b
/* c */
class C {}
''');
_assertTokens(['// a', '// b']);
}
Future<void> test_withNonDocComment_singleLine_singleLine() async {
await resolveTestCode('''
// a
// b
// c
class C {}
''');
_assertTokens(['// a', '// b']);
}
/// Assert that the returned tokens have lexemes that match the [expected]
/// comments.
void _assertTokens(List<String> expected) {
expect(result.unit.fileHeader.map((token) => token.lexeme),
orderedEquals(expected));
}
}

View file

@ -4,11 +4,13 @@
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'ast_test.dart' as ast;
import 'range_factory_test.dart' as range_factory;
import 'string_test.dart' as string;
void main() {
defineReflectiveSuite(() {
ast.main();
range_factory.main();
string.main();
});