1 | | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
|
2 | | // for details. All rights reserved. Use of this source code is governed by a
|
3 | | // BSD-style license that can be found in the LICENSE file.
|
4 | |
|
5 | | library dart_style.src.dart_formatter;
|
6 | |
|
7 | | import 'dart:math' as math;
|
8 | |
|
9 | | import 'package:analyzer/dart/ast/ast.dart';
|
10 | | import 'package:analyzer/dart/ast/token.dart';
|
11 | | import 'package:analyzer/error/error.dart';
|
12 | | import 'package:analyzer/src/dart/scanner/reader.dart';
|
13 | | import 'package:analyzer/src/dart/scanner/scanner.dart';
|
14 | | import 'package:analyzer/src/generated/parser.dart';
|
15 | | import 'package:analyzer/src/generated/source.dart';
|
16 | | import 'package:analyzer/src/string_source.dart';
|
17 | |
|
18 | | import 'error_listener.dart';
|
19 | | import 'exceptions.dart';
|
20 | | import 'source_code.dart';
|
21 | | import 'source_visitor.dart';
|
22 | | import 'string_compare.dart' as string_compare;
|
23 | | import 'style_fix.dart';
|
24 | |
|
25 | | /// Dart source code formatter.
|
26 | | class DartFormatter {
|
27 | | /// The string that newlines should use.
|
28 | | ///
|
29 | | /// If not explicitly provided, this is inferred from the source text. If the
|
30 | | /// first newline is `\r\n` (Windows), it will use that. Otherwise, it uses
|
31 | | /// Unix-style line endings (`\n`).
|
32 | | String lineEnding;
|
33 | |
|
34 | | /// The number of characters allowed in a single line.
|
35 | | final int pageWidth;
|
36 | |
|
37 | | /// The number of characters of indentation to prefix the output lines with.
|
38 | | final int indent;
|
39 | |
|
40 | | final Set<StyleFix> fixes = new Set();
|
41 | |
|
42 | | /// Creates a new formatter for Dart code.
|
43 | | ///
|
44 | | /// If [lineEnding] is given, that will be used for any newlines in the
|
45 | | /// output. Otherwise, the line separator will be inferred from the line
|
46 | | /// endings in the source file.
|
47 | | ///
|
48 | | /// If [indent] is given, that many levels of indentation will be prefixed
|
49 | | /// before each resulting line in the output.
|
50 | | ///
|
51 | | /// While formatting, also applies any of the given [fixes].
|
52 | 3 | DartFormatter(
|
53 | | {this.lineEnding, int pageWidth, int indent, Iterable<StyleFix> fixes})
|
54 | | : pageWidth = pageWidth ?? 80,
|
55 | | indent = indent ?? 0 {
|
56 | 4 | if (fixes != null) this.fixes.addAll(fixes);
|
57 | | }
|
58 | |
|
59 | | /// Formats the given [source] string containing an entire Dart compilation
|
60 | | /// unit.
|
61 | | ///
|
62 | | /// If [uri] is given, it is a [String] or [Uri] used to identify the file
|
63 | | /// being formatted in error messages.
|
64 | 1 | String format(String source, {uri}) {
|
65 | | if (uri == null) {
|
66 | | // Do nothing.
|
67 | 1 | } else if (uri is Uri) {
|
68 | 0 | uri = uri.toString();
|
69 | 1 | } else if (uri is String) {
|
70 | | // Do nothing.
|
71 | | } else {
|
72 | 0 | throw new ArgumentError("uri must be `null`, a Uri, or a String.");
|
73 | | }
|
74 | |
|
75 | 1 | return formatSource(
|
76 | 1 | new SourceCode(source, uri: uri, isCompilationUnit: true))
|
77 | 1 | .text;
|
78 | | }
|
79 | |
|
80 | | /// Formats the given [source] string containing a single Dart statement.
|
81 | 1 | String formatStatement(String source) {
|
82 | 3 | return formatSource(new SourceCode(source, isCompilationUnit: false)).text;
|
83 | | }
|
84 | |
|
85 | | /// Formats the given [source].
|
86 | | ///
|
87 | | /// Returns a new [SourceCode] containing the formatted code and the resulting
|
88 | | /// selection, if any.
|
89 | 3 | SourceCode formatSource(SourceCode source) {
|
90 | 3 | var errorListener = new ErrorListener();
|
91 | |
|
92 | | // Tokenize the source.
|
93 | 6 | var reader = new CharSequenceReader(source.text);
|
94 | 9 | var stringSource = new StringSource(source.text, source.uri);
|
95 | 3 | var scanner = new Scanner(stringSource, reader, errorListener);
|
96 | 3 | var startToken = scanner.tokenize();
|
97 | 6 | var lineInfo = new LineInfo(scanner.lineStarts);
|
98 | |
|
99 | | // Infer the line ending if not given one. Do it here since now we know
|
100 | | // where the lines start.
|
101 | 3 | if (lineEnding == null) {
|
102 | | // If the first newline is "\r\n", use that. Otherwise, use "\n".
|
103 | 9 | if (scanner.lineStarts.length > 1 &&
|
104 | 9 | scanner.lineStarts[1] >= 2 &&
|
105 | 18 | source.text[scanner.lineStarts[1] - 2] == '\r') {
|
106 | 1 | lineEnding = "\r\n";
|
107 | | } else {
|
108 | 3 | lineEnding = "\n";
|
109 | | }
|
110 | | }
|
111 | |
|
112 | 3 | errorListener.throwIfErrors();
|
113 | |
|
114 | | // Parse it.
|
115 | 3 | var parser = new Parser(stringSource, errorListener);
|
116 | 3 | parser.enableOptionalNewAndConst = true;
|
117 | 3 | parser.enableSetLiterals = true;
|
118 | |
|
119 | | AstNode node;
|
120 | 3 | if (source.isCompilationUnit) {
|
121 | 3 | node = parser.parseCompilationUnit(startToken);
|
122 | | } else {
|
123 | 2 | node = parser.parseStatement(startToken);
|
124 | |
|
125 | | // Make sure we consumed all of the source.
|
126 | 4 | var token = node.endToken.next;
|
127 | 4 | if (token.type != TokenType.EOF) {
|
128 | 1 | var error = new AnalysisError(
|
129 | | stringSource,
|
130 | 1 | token.offset,
|
131 | 6 | math.max(token.length, 1),
|
132 | | ParserErrorCode.UNEXPECTED_TOKEN,
|
133 | 2 | [token.lexeme]);
|
134 | |
|
135 | 2 | throw new FormatterException([error]);
|
136 | | }
|
137 | | }
|
138 | |
|
139 | 3 | errorListener.throwIfErrors();
|
140 | |
|
141 | | // Format it.
|
142 | 3 | var visitor = new SourceVisitor(this, lineInfo, source);
|
143 | 3 | var output = visitor.run(node);
|
144 | |
|
145 | | // Sanity check that only whitespace was changed if that's all we expect.
|
146 | 6 | if (fixes.isEmpty &&
|
147 | 6 | !string_compare.equalIgnoringWhitespace(source.text, output.text)) {
|
148 | 3 | throw new UnexpectedOutputException(source.text, output.text);
|
149 | | }
|
150 | |
|
151 | | return output;
|
152 | | }
|
153 | | }
|