blob: c4c689eeb9ff13fbece5d1675c5586fc0599e3f1 [file] [log] [blame]
#!/usr/bin/env dart
// Copyright (c) 2013, 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:convert';
import 'dart:io';
import 'package:args/args.dart';
import 'package:path/path.dart' as path;
import 'package:analyzer/src/services/formatter_impl.dart';
const BINARY_NAME = 'dartfmt';
final dartFileRegExp = new RegExp(r'^[^.].*\.dart$', caseSensitive: false);
final argParser = _initArgParser();
final defaultSelection = new Selection(-1, -1);
var formatterSettings;
CodeKind kind;
bool machineFormat;
bool overwriteFileContents;
Selection selection;
final List<String> paths = [];
const HELP_FLAG = 'help';
const KIND_FLAG = 'kind';
const MACHINE_FLAG = 'machine';
const WRITE_FLAG = 'write';
const SELECTION_FLAG = 'selection';
const TRANSFORM_FLAG = 'transform';
const MAX_LINE_FLAG = 'max_line_length';
const INDENT_FLAG = 'indent';
const FOLLOW_LINKS = false;
main(args) {
var options = argParser.parse(args);
if (options['help']) {
_printUsage();
return;
}
_readOptions(options);
if (options.rest.isEmpty) {
_formatStdin(kind);
} else {
paths.addAll(options.rest);
_formatPaths(paths);
}
}
_readOptions(options) {
kind = _parseKind(options[KIND_FLAG]);
machineFormat = options[MACHINE_FLAG];
overwriteFileContents = options[WRITE_FLAG];
selection = _parseSelection(options[SELECTION_FLAG]);
formatterSettings = new FormatterOptions(
codeTransforms: options[TRANSFORM_FLAG],
tabsForIndent: _parseTabsForIndent(options[INDENT_FLAG]),
spacesPerIndent: _parseSpacesPerIndent(options[INDENT_FLAG]),
pageWidth: _parseLineLength(options[MAX_LINE_FLAG]));
}
/// Translate the indent option into spaces per indent.
int _parseSpacesPerIndent(String indentOption) {
if (indentOption == 'tab') {
return 1;
}
int spacesPerIndent = _toInt(indentOption);
if (spacesPerIndent == null) {
throw new FormatterException('Indentation is specified as an Integer or '
'the value "tab".');
}
return spacesPerIndent;
}
/// Translate the indent option into tabs for indent.
bool _parseTabsForIndent(String indentOption) => indentOption == 'tab';
CodeKind _parseKind(kindOption) {
switch(kindOption) {
case 'stmt' :
return CodeKind.STATEMENT;
default:
return CodeKind.COMPILATION_UNIT;
}
}
int _parseLineLength(String lengthOption) {
var length = _toInt(lengthOption);
if (length == null) {
var val = lengthOption.toUpperCase();
if (val == 'INF' || val == 'INFINITY') {
length = -1;
} else {
throw new FormatterException('Line length is specified as an Integer or '
'the value "Inf".');
}
}
return length;
}
Selection _parseSelection(String selectionOption) {
if(selectionOption == null) return null;
var units = selectionOption.split(',');
if (units.length == 2) {
var offset = _toInt(units[0]);
var length = _toInt(units[1]);
if (offset != null && length != null) {
return new Selection(offset, length);
}
}
throw new FormatterException('Selections are specified as integer pairs '
'(e.g., "(offset, length)".');
}
int _toInt(str) => int.parse(str, onError: (_) => null);
_formatPaths(paths) {
paths.forEach((path) {
if (FileSystemEntity.isDirectorySync(path)) {
_formatDirectory(new Directory(path));
} else {
_formatFile(new File(path));
}
});
}
_formatResource(resource) {
if (resource is Directory) {
_formatDirectory(resource);
} else if (resource is File) {
_formatFile(resource);
}
}
_formatDirectory(dir) => dir.listSync(followLinks: FOLLOW_LINKS)
.forEach((resource) => _formatResource(resource));
_formatFile(file) {
if (_isDartFile(file)) {
if (_isPatchFile(file) && !paths.contains(file.path)) {
_log('Skipping patch file "${file.path}"');
return;
}
try {
var buffer = new StringBuffer();
var rawSource = file.readAsStringSync();
var formatted = _format(rawSource, CodeKind.COMPILATION_UNIT);
if (overwriteFileContents) {
// Only touch files files whose contents will be changed
if (rawSource != formatted) {
file.writeAsStringSync(formatted);
}
} else {
print(formatted);
}
} catch (e) {
_log('Unable to format "${file.path}": $e');
}
}
}
_isPatchFile(file) => file.path.endsWith('_patch.dart');
_isDartFile(file) => dartFileRegExp.hasMatch(path.basename(file.path));
_formatStdin(kind) {
var input = new StringBuffer();
stdin.transform(new Utf8Decoder())
.listen((data) => input.write(data),
onError: (error) => _log('Error reading from stdin'),
onDone: () => print(_format(input.toString(), kind)));
}
/// Initialize the arg parser instance.
ArgParser _initArgParser() {
// NOTE: these flags are placeholders only!
var parser = new ArgParser();
parser.addFlag(WRITE_FLAG, abbr: 'w', negatable: false,
help: 'Write reformatted sources to files (overwriting contents). '
'Do not print reformatted sources to standard output.');
parser.addFlag(TRANSFORM_FLAG, abbr: 't', negatable: false,
help: 'Perform code transformations.');
parser.addOption(MAX_LINE_FLAG, abbr: 'l', defaultsTo: '80',
help: 'Wrap lines longer than this length. '
'To never wrap, specify "Infinity" or "Inf" for short.');
parser.addOption(INDENT_FLAG, abbr: 'i', defaultsTo: '2',
help: 'Specify number of spaces per indentation. '
'To indent using tabs, specify "--$INDENT_FLAG tab".'
'--- [PROVISIONAL API].', hide: true);
parser.addOption(KIND_FLAG, abbr: 'k', defaultsTo: 'cu',
help: 'Specify source snippet kind ("stmt" or "cu") '
'--- [PROVISIONAL API].', hide: true);
parser.addOption(SELECTION_FLAG, abbr: 's',
help: 'Specify selection information as an offset,length pair '
'(e.g., -s "0,4").', hide: true);
parser.addFlag(MACHINE_FLAG, abbr: 'm', negatable: false,
help: 'Produce output in a format suitable for parsing.');
parser.addFlag(HELP_FLAG, abbr: 'h', negatable: false,
help: 'Print this usage information.');
return parser;
}
/// Displays usage information.
_printUsage() {
var buffer = new StringBuffer();
buffer..write('$BINARY_NAME formats Dart programs.')
..write('\n\n')
..write('Without an explicit path, $BINARY_NAME processes the standard '
'input. Given a file, it operates on that file; given a '
'directory, it operates on all .dart files in that directory, '
'recursively. (Files starting with a period are ignored.) By '
'default, $BINARY_NAME prints the reformatted sources to '
'standard output.')
..write('\n\n')
..write('Usage: $BINARY_NAME [flags] [path...]\n\n')
..write('Supported flags are:\n')
..write('${argParser.getUsage()}\n\n');
_log(buffer.toString());
}
/// Format this [src], treating it as the given snippet [kind].
String _format(src, kind) {
var formatResult = new CodeFormatter(formatterSettings).format(
kind, src, selection: selection);
if (machineFormat) {
if (formatResult.selection == null) {
formatResult.selection = defaultSelection;
}
return _toJson(formatResult);
}
return formatResult.source;
}
_toJson(formatResult) =>
// Actual JSON format TBD
JSON.encode({'source': formatResult.source,
'selection': {
'offset': formatResult.selection.offset,
'length': formatResult.selection.length
}
});
/// Log the given [msg].
_log(String msg) {
//TODO(pquitslund): add proper log support
print(msg);
}