blob: b4073a66ad9a5da33a0aa76fa4f55abc2361d79f [file] [log] [blame]
// 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.
library dart_style.src.formatter_options;
import 'dart:convert';
import 'dart:io';
import 'source_code.dart';
import 'style_fix.dart';
/// Global options that affect how the formatter produces and uses its outputs.
class FormatterOptions {
/// The [OutputReporter] used to show the formatting results.
final OutputReporter reporter;
/// The number of spaces of indentation to prefix the output with.
final int indent;
/// The number of columns that formatted output should be constrained to fit
/// within.
final int pageWidth;
/// Whether symlinks should be traversed when formatting a directory.
final bool followLinks;
/// The style fixes to apply while formatting.
final Iterable<StyleFix> fixes;
FormatterOptions(this.reporter,
{this.indent = 0,
this.pageWidth = 80,
this.followLinks = false,
this.fixes});
}
/// How the formatter reports the results it produces.
abstract class OutputReporter {
/// Prints only the names of files whose contents are different from their
/// formatted version.
static final OutputReporter dryRun = _DryRunReporter();
/// Prints the formatted results of each file to stdout.
static final OutputReporter print = _PrintReporter();
/// Prints the formatted result and selection info of each file to stdout as
/// a JSON map.
static final OutputReporter printJson = _PrintJsonReporter();
/// Overwrites each file with its formatted result.
static final OutputReporter overwrite = _OverwriteReporter();
/// Describe the directory whose contents are about to be processed.
void showDirectory(String path) {}
/// Describe the symlink at [path] that wasn't followed.
void showSkippedLink(String path) {}
/// Describe the hidden [path] that wasn't processed.
void showHiddenPath(String path) {}
/// Called when [file] is about to be formatted.
void beforeFile(File file, String label) {}
/// Describe the processed file at [path] whose formatted result is [output].
///
/// If the contents of the file are the same as the formatted output,
/// [changed] will be false.
void afterFile(File file, String label, SourceCode output, {bool changed});
}
/// Prints only the names of files whose contents are different from their
/// formatted version.
class _DryRunReporter extends OutputReporter {
@override
void afterFile(File file, String label, SourceCode output, {bool changed}) {
// Only show the changed files.
if (changed) print(label);
}
}
/// Prints the formatted results of each file to stdout.
class _PrintReporter extends OutputReporter {
@override
void showDirectory(String path) {
print('Formatting directory $path:');
}
@override
void showSkippedLink(String path) {
print('Skipping link $path');
}
@override
void showHiddenPath(String path) {
print('Skipping hidden path $path');
}
@override
void afterFile(File file, String label, SourceCode output, {bool changed}) {
// Don't add an extra newline.
stdout.write(output.text);
}
}
/// Prints the formatted result and selection info of each file to stdout as a
/// JSON map.
class _PrintJsonReporter extends OutputReporter {
@override
void afterFile(File file, String label, SourceCode output, {bool changed}) {
// TODO(rnystrom): Put an empty selection in here to remain compatible with
// the old formatter. Since there's no way to pass a selection on the
// command line, this will never be used, which is why it's hard-coded to
// -1, -1. If we add support for passing in a selection, put the real
// result here.
print(jsonEncode({
'path': label,
'source': output.text,
'selection': {
'offset': output.selectionStart ?? -1,
'length': output.selectionLength ?? -1
}
}));
}
}
/// Overwrites each file with its formatted result.
class _OverwriteReporter extends _PrintReporter {
@override
void afterFile(File file, String label, SourceCode output, {bool changed}) {
if (changed) {
try {
file.writeAsStringSync(output.text);
print('Formatted $label');
} on FileSystemException catch (err) {
stderr.writeln('Could not overwrite $label: '
'${err.osError.message} (error code ${err.osError.errorCode})');
}
} else {
print('Unchanged $label');
}
}
}
/// Base clase for a reporter that decorates an inner reporter.
abstract class _ReporterDecorator implements OutputReporter {
final OutputReporter _inner;
_ReporterDecorator(this._inner);
@override
void showDirectory(String path) {
_inner.showDirectory(path);
}
@override
void showSkippedLink(String path) {
_inner.showSkippedLink(path);
}
@override
void showHiddenPath(String path) {
_inner.showHiddenPath(path);
}
@override
void beforeFile(File file, String label) {
_inner.beforeFile(file, label);
}
@override
void afterFile(File file, String label, SourceCode output, {bool changed}) {
_inner.afterFile(file, label, output, changed: changed);
}
}
/// A decorating reporter that reports how long it took for format each file.
class ProfileReporter extends _ReporterDecorator {
/// The files that have been started but have not completed yet.
///
/// Maps a file label to the time that it started being formatted.
final Map<String, DateTime> _ongoing = {};
/// The elapsed time it took to format each completed file.
final Map<String, Duration> _elapsed = {};
/// The number of files that completed so fast that they aren't worth
/// tracking.
int _elided = 0;
ProfileReporter(OutputReporter inner) : super(inner);
/// Show the times for the slowest files to format.
void showProfile() {
// Everything should be done.
assert(_ongoing.isEmpty);
var files = _elapsed.keys.toList();
files.sort((a, b) => _elapsed[b].compareTo(_elapsed[a]));
for (var file in files) {
print('${_elapsed[file]}: $file');
}
if (_elided >= 1) {
var s = _elided > 1 ? 's' : '';
print('...$_elided more file$s each took less than 10ms.');
}
}
/// Called when [file] is about to be formatted.
@override
void beforeFile(File file, String label) {
super.beforeFile(file, label);
_ongoing[label] = DateTime.now();
}
/// Describe the processed file at [path] whose formatted result is [output].
///
/// If the contents of the file are the same as the formatted output,
/// [changed] will be false.
@override
void afterFile(File file, String label, SourceCode output, {bool changed}) {
var elapsed = DateTime.now().difference(_ongoing.remove(label));
if (elapsed.inMilliseconds >= 10) {
_elapsed[label] = elapsed;
} else {
_elided++;
}
super.afterFile(file, label, output, changed: changed);
}
}
/// A decorating reporter that sets the exit code to 1 if any changes are made.
class SetExitReporter extends _ReporterDecorator {
SetExitReporter(OutputReporter inner) : super(inner);
/// Describe the processed file at [path] whose formatted result is [output].
///
/// If the contents of the file are the same as the formatted output,
/// [changed] will be false.
@override
void afterFile(File file, String label, SourceCode output, {bool changed}) {
if (changed) exitCode = 1;
super.afterFile(file, label, output, changed: changed);
}
}