blob: 25aed0da0156f03c95f53bc410790e443b8b5e01 [file] [log] [blame]
// Copyright 2017 The Fuchsia Authors. 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:async';
import 'dart:io';
import 'package:analyzer/analyzer.dart';
import 'package:source_span/source_span.dart';
/// Commands supported in this util.
enum Command {
/// Check the provided files and exit with 0 if they conform to the formatting
/// guidelines. Exit with 1 otherwise.
check,
/// Fix the formatting issues.
fix,
}
/// Check or fix the double quote issues.
Future<bool> processDoubleQuotes(
Command cmd,
File file,
SourceFile src,
CompilationUnit cu,
) async {
_DoubleQuoteVisitor visitor = new _DoubleQuoteVisitor(src);
cu.accept(visitor);
if (visitor.invalidNodes.isEmpty) {
return false;
}
switch (cmd) {
case Command.check:
reportDoubleQuotes(src, visitor.invalidNodes);
return true;
case Command.fix:
await fixDoubleQuotes(file, src, visitor.invalidNodes);
break;
}
return false;
}
/// Report double quote issues.
void reportDoubleQuotes(
SourceFile src, List<SingleStringLiteral> invalidNodes) {
for (SingleStringLiteral node in invalidNodes) {
print('${src.url}:${src.getLine(node.offset)}: '
'Prefer single quotes over double quotes: $node');
}
}
/// Fix double quote issues.
Future<Null> fixDoubleQuotes(
File file,
SourceFile src,
List<SingleStringLiteral> invalidNodes,
) async {
// Get the original code as a String.
String code = src.getText(0);
// Replace the double quotes into single quotes.
for (SingleStringLiteral node in invalidNodes) {
int openingOffset = node.offset + (node.isRaw ? 1 : 0);
code = code.replaceRange(
openingOffset,
openingOffset + (node.isMultiline ? 3 : 1),
node.isMultiline ? "'''" : "'",
);
// NOTE: node.contentsEnd value cannot be used reliably, because it returns
// an incorrect value when the string is a `StringInterpolation` and the
// contents ends with an `InterpolationExpression`.
int closingOffset = node.end - (node.isMultiline ? 3 : 1);
code = code.replaceRange(
closingOffset,
closingOffset + (node.isMultiline ? 3 : 1),
node.isMultiline ? "'''" : "'",
);
}
// Overwrite the source file.
await file.writeAsString(code);
}
class _DoubleQuoteVisitor extends GeneralizingAstVisitor<bool> {
_DoubleQuoteVisitor(this.src);
final SourceFile src;
final List<SingleStringLiteral> invalidNodes = <SingleStringLiteral>[];
@override
bool visitSingleStringLiteral(SingleStringLiteral node) {
super.visitSingleStringLiteral(node);
if (!isValidSingleStringLiteral(node)) {
invalidNodes.add(node);
}
return true;
}
bool isValidSingleStringLiteral(SingleStringLiteral node) {
return node.isSingleQuoted ||
src.getText(node.contentsOffset, node.contentsEnd).contains("'");
}
}
/// Check or fix the ordering of import and export directives.
Future<bool> processDirectives(
Command cmd,
File file,
SourceFile src,
CompilationUnit cu,
) async {
_DirectiveVisitor visitor = new _DirectiveVisitor(src);
cu.accept(visitor);
List<UriBasedDirective> directives = visitor.directives;
if (directives.isEmpty) {
return false;
}
directives.sort(
(UriBasedDirective i1, UriBasedDirective i2) => i1.offset - i2.offset);
// Start, end indices of the entire import block.
int startIndex = directives.first.offset;
int endIndex = directives.last.end;
String actual = src.getText(startIndex, endIndex);
String expected = _getOrderedDirectives(directives, src);
if (actual == expected) {
return false;
}
switch (cmd) {
case Command.check:
reportDirectives(src, startIndex, endIndex, actual, expected);
return true;
case Command.fix:
await fixDirectives(file, src, startIndex, endIndex, expected);
break;
}
return false;
}
/// Report import / export ordering issues.
void reportDirectives(
SourceFile src,
int startIndex,
int endIndex,
String actual,
String expected,
) {
print('${src.url}:${src.getLine(startIndex)}-${src.getLine(endIndex - 1)}: '
'Order import directives properly.');
print('== Actual ==');
print(actual);
print('== Expected ==');
print(expected);
print('==');
print('');
}
/// Fix the import ordering issues.
Future<Null> fixDirectives(
File file,
SourceFile src,
int startIndex,
int endIndex,
String expected,
) async {
// Get the original code as a String.
String code = src.getText(0);
// Replace the import statements with the expected.
code = code.replaceRange(startIndex, endIndex, expected);
// Overwrite the source file.
await file.writeAsString(code);
}
typedef bool _ConditionFn(UriBasedDirective directive);
class _Condition<T extends UriBasedDirective> {
final String prefix;
_Condition(this.prefix);
bool func(UriBasedDirective directive) {
return directive is T && directive.uri.stringValue.startsWith(prefix);
}
}
String _getOrderedDirectives(
List<UriBasedDirective> directives,
SourceFile src,
) {
Set<UriBasedDirective> directiveSet = directives.toSet();
List<_ConditionFn> conditions = <_ConditionFn>[
new _Condition<ImportDirective>('dart:').func,
new _Condition<ImportDirective>('package:').func,
new _Condition<ImportDirective>('').func,
new _Condition<ExportDirective>('package:').func,
new _Condition<ExportDirective>('src/').func,
new _Condition<ExportDirective>('').func,
];
return conditions
.map((_ConditionFn condition) {
// Get the group of directives with the given condition prefix, and sort
// them by their uri.
List<UriBasedDirective> group = directiveSet.where(condition).toList()
..sort((UriBasedDirective i1, UriBasedDirective i2) =>
i1.uri.stringValue.compareTo(i2.uri.stringValue));
// Remove this group from the set to avoid any duplicates.
directiveSet.removeAll(group);
// Join the import directives with a newline character.
// Use the text as appears in the original file, in order to respect the
// formatting done by dartfmt.
return group
.map((UriBasedDirective directive) =>
src.getText(directive.offset, directive.end))
.join('\n');
})
// Remove any empty groups.
.where((String s) => s.isNotEmpty)
// There should be one empty line between two import groups.
.join('\n\n');
}
class _DirectiveVisitor extends GeneralizingAstVisitor<bool> {
_DirectiveVisitor(this.src);
final SourceFile src;
final List<UriBasedDirective> directives = <UriBasedDirective>[];
@override
bool visitImportDirective(ImportDirective node) {
super.visitImportDirective(node);
directives.add(node);
return true;
}
@override
bool visitExportDirective(ExportDirective node) {
super.visitExportDirective(node);
directives.add(node);
return true;
}
}