blob: 73bb8bf1b287f24512b38125d964b9550e1d230f [file] [log] [blame] [edit]
// Copyright (c) 2014, 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 'dart:math' as math;
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/error/error.dart';
// ignore: implementation_imports
import 'package:analyzer/src/dart/scanner/scanner.dart';
// ignore: implementation_imports
import 'package:analyzer/src/string_source.dart';
import 'package:pub_semver/pub_semver.dart';
import 'exceptions.dart';
import 'front_end/ast_node_visitor.dart';
import 'short/source_visitor.dart';
import 'source_code.dart';
import 'string_compare.dart' as string_compare;
/// Regular expression that matches a format width comment like:
///
/// // dart format width=123
final RegExp _widthCommentPattern = RegExp(r'^// dart format width=(\d+)$');
/// A Dart source code formatter.
///
/// This is a lightweight class that mostly bundles formatting options so that
/// you don't have to pass a long argument list to [format()] and
/// [formatStatement()]. You can efficiently create a new instance of this for
/// every format invocation.
final class DartFormatter {
/// The latest Dart language version that can be parsed and formatted by this
/// version of the formatter.
static final latestLanguageVersion = Version(3, 7, 0);
/// The latest Dart language version that will be formatted using the older
/// "short" style.
///
/// Any Dart code at a language version later than this will be formatted
/// using the new "tall" style.
static final latestShortStyleLanguageVersion = Version(3, 6, 0);
/// The page width that the formatter tries to fit code inside if no other
/// width is specified.
static const defaultPageWidth = 80;
/// The Dart language version that formatted code should be parsed as.
///
/// Note that a `// @dart=` comment inside the code overrides this.
final Version languageVersion;
/// The string that newlines should use.
///
/// If not explicitly provided, this is inferred from the source text. If the
/// first newline is `\r\n` (Windows), it will use that. Otherwise, it uses
/// Unix-style line endings (`\n`).
String? lineEnding;
/// The number of characters allowed in a single line.
final int pageWidth;
/// The number of characters of indentation to prefix the output lines with.
final int indent;
/// How trailing commas in various constructs should affect formatting.
///
/// The default is [TrailingCommas.automate] where the formatter is free to
/// add and remove them if it decides a constructor should be split or
/// collapsed.
final TrailingCommas trailingCommas;
/// Flags to enable experimental language features.
///
/// See dart.dev/go/experiments for details.
final List<String> experimentFlags;
/// Creates a new formatter for Dart code at [languageVersion].
///
/// If [lineEnding] is given, that will be used for any newlines in the
/// output. Otherwise, the line separator will be inferred from the line
/// endings in the source file.
///
/// If [indent] is given, that many levels of indentation will be prefixed
/// before each resulting line in the output.
DartFormatter({
required this.languageVersion,
this.lineEnding,
int? pageWidth,
int? indent,
TrailingCommas? trailingCommas,
List<String>? experimentFlags,
}) : pageWidth = pageWidth ?? defaultPageWidth,
indent = indent ?? 0,
trailingCommas = trailingCommas ?? TrailingCommas.automate,
experimentFlags = [...?experimentFlags];
/// Formats the given [source] string containing an entire Dart compilation
/// unit.
///
/// If [uri] is given, it is a [String] or [Uri] used to identify the file
/// being formatted in error messages.
String format(String source, {Object? uri}) {
var uriString = switch (uri) {
null => null,
Uri() => uri.toString(),
String() => uri,
_ => throw ArgumentError('uri must be `null`, a Uri, or a String.'),
};
return formatSource(
SourceCode(source, uri: uriString, isCompilationUnit: true),
).text;
}
/// Formats the given [source] string containing a single Dart statement.
String formatStatement(String source) {
return formatSource(SourceCode(source, isCompilationUnit: false)).text;
}
/// Formats the given [source].
///
/// Returns a new [SourceCode] containing the formatted code and the resulting
/// selection, if any.
SourceCode formatSource(SourceCode source) {
var inputOffset = 0;
var text = source.text;
var unitSourceCode = source;
// If we're parsing a single statement, wrap the source in a fake function.
if (!source.isCompilationUnit) {
var prefix = 'void foo() { ';
inputOffset = prefix.length;
text = '$prefix$text\n }';
unitSourceCode = SourceCode(
text,
uri: source.uri,
isCompilationUnit: false,
selectionStart:
source.selectionStart != null
? source.selectionStart! + inputOffset
: null,
selectionLength: source.selectionLength,
);
}
var featureSet = FeatureSet.fromEnableFlags2(
sdkLanguageVersion: languageVersion,
flags: experimentFlags,
);
// Parse it.
var parseResult = parseString(
content: text,
featureSet: featureSet,
path: source.uri,
throwIfDiagnostics: false,
);
// Infer the line ending if not given one. Do it here since now we know
// where the lines start.
if (lineEnding == null) {
// If the first newline is "\r\n", use that. Otherwise, use "\n".
var lineStarts = parseResult.lineInfo.lineStarts;
if (lineStarts.length > 1 &&
lineStarts[1] >= 2 &&
text[lineStarts[1] - 2] == '\r') {
lineEnding = '\r\n';
} else {
lineEnding = '\n';
}
}
// Throw if there are syntactic errors.
var syntacticErrors =
parseResult.errors.where((error) {
return error.errorCode.type == ErrorType.SYNTACTIC_ERROR;
}).toList();
if (syntacticErrors.isNotEmpty) {
throw FormatterException(syntacticErrors);
}
AstNode node;
if (source.isCompilationUnit) {
node = parseResult.unit;
} else {
var function = parseResult.unit.declarations[0] as FunctionDeclaration;
var body = function.functionExpression.body as BlockFunctionBody;
node = body.block.statements[0];
// Make sure we consumed all of the source.
var token = node.endToken.next!;
if (token.type != TokenType.CLOSE_CURLY_BRACKET) {
var stringSource = StringSource(text, source.uri);
var error = AnalysisError.tmp(
source: stringSource,
offset: token.offset - inputOffset,
length: math.max(token.length, 1),
errorCode: ParserErrorCode.UNEXPECTED_TOKEN,
arguments: [token.lexeme],
);
throw FormatterException([error]);
}
}
// Format it.
var lineInfo = parseResult.lineInfo;
// If the code has an `@dart=` comment, use that to determine the style.
var sourceLanguageVersion = languageVersion;
if (parseResult.unit.languageVersionToken case var token?) {
sourceLanguageVersion = Version(token.major, token.minor, 0);
}
// Use language version to determine what formatting style to apply.
SourceCode output;
if (sourceLanguageVersion > latestShortStyleLanguageVersion) {
// Look for a page width comment before the code.
int? pageWidthFromComment;
for (
Token? comment = node.beginToken.precedingComments;
comment != null;
comment = comment.next
) {
if (_widthCommentPattern.firstMatch(comment.lexeme) case var match?) {
// If integer parsing fails for some reason, the returned `null`
// means we correctly ignore the comment.
pageWidthFromComment = int.tryParse(match[1]!);
break;
}
}
var visitor = AstNodeVisitor(this, lineInfo, unitSourceCode);
output = visitor.run(unitSourceCode, node, pageWidthFromComment);
} else {
// Use the old style.
var visitor = SourceVisitor(this, lineInfo, unitSourceCode);
output = visitor.run(node);
}
// Sanity check that only whitespace was changed if that's all we expect.
if (!string_compare.equalIgnoringWhitespace(source.text, output.text)) {
throw UnexpectedOutputException(source.text, output.text);
}
return output;
}
}
/// Configuration for how trailing commas should be handled by the formatter.
///
/// Note that this only applies when using the new formatter to format code at
/// language version 3.7 or later. On older versions, it always behaves as if
/// it were [TrailingCommas.preserve].
enum TrailingCommas {
/// The formatter will add a trailing comma if a construct is split and
/// remove the trailing comma and collapse the construct if it decides to do
/// so.
automate,
/// The formatter will add a trailing comma if a construct splits. If the
/// construct has a trailing comma, it will always be forced to split and the
/// trailing comma is preserved.
preserve,
}