Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(292)

Side by Side Diff: pkg/analyzer_experimental/lib/src/services/formatter_impl.dart

Issue 17470004: Dart formatter checkpoint. (Closed) Base URL: http://dart.googlecode.com/svn/branches/bleeding_edge/dart/
Patch Set: Created 7 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a 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. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 library formatter_impl; 5 library formatter_impl;
6 6
7 7
8 import 'dart:io'; 8 import 'dart:io';
9 9
10 import 'package:analyzer_experimental/analyzer.dart'; 10 import 'package:analyzer_experimental/analyzer.dart';
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after
76 /// Format the specified portion (from [offset] with [length]) of the given 76 /// Format the specified portion (from [offset] with [length]) of the given
77 /// [source] string, optionally providing an [indentationLevel]. 77 /// [source] string, optionally providing an [indentationLevel].
78 String format(CodeKind kind, String source, {int offset, int end, 78 String format(CodeKind kind, String source, {int offset, int end,
79 int indentationLevel:0}); 79 int indentationLevel:0});
80 80
81 } 81 }
82 82
83 class CodeFormatterImpl implements CodeFormatter, AnalysisErrorListener { 83 class CodeFormatterImpl implements CodeFormatter, AnalysisErrorListener {
84 84
85 final FormatterOptions options; 85 final FormatterOptions options;
86 final List<AnalysisError> errors = <AnalysisError>[]; 86 final List<AnalysisError> errors = <AnalysisError>[];
scheglov 2013/06/19 22:12:05 Remove type annotation for the final field.
pquitslund 2013/06/20 22:34:52 Done.
87 final EditRecorder recorder;
87 88
88 CodeFormatterImpl(this.options); 89 CodeFormatterImpl(FormatterOptions options) : this.options = options,
scheglov 2013/06/19 22:12:05 May be "this.options" and remove initializer?
pquitslund 2013/06/20 22:34:52 The trouble is that I'd like to use options in my
90 recorder = new EditRecorder(options);
89 91
90 String format(CodeKind kind, String source, {int offset, int end, 92 String format(CodeKind kind, String source, {int offset, int end,
91 int indentationLevel:0}) { 93 int indentationLevel:0}) {
92 94
93 var start = tokenize(source); 95 var start = tokenize(source);
94 _checkForErrors(); 96 checkForErrors();
95 97
96 var node = parse(kind, start); 98 var node = parse(kind, start);
97 _checkForErrors(); 99 checkForErrors();
98 100
99 // To be continued... 101 var formatter = new FormattingEngine(options);
100 102 return formatter.format(source, node, start, kind, recorder);
101 return source;
102 } 103 }
103 104
104 ASTNode parse(CodeKind kind, Token start) { 105 ASTNode parse(CodeKind kind, Token start) {
105 106
106 var parser = new Parser(null, this); 107 var parser = new Parser(null, this);
107 108
108 switch (kind) { 109 switch (kind) {
109 case CodeKind.COMPILATION_UNIT: 110 case CodeKind.COMPILATION_UNIT:
110 return parser.parseCompilationUnit(start); 111 return parser.parseCompilationUnit(start);
111 case CodeKind.STATEMENT: 112 case CodeKind.STATEMENT:
112 return parser.parseStatement(start); 113 return parser.parseStatement(start);
113 } 114 }
114 115
115 throw new FormatterException('Unsupported format kind: $kind'); 116 throw new FormatterException('Unsupported format kind: $kind');
116 } 117 }
117 118
118 _checkForErrors() { 119 checkForErrors() {
119 if (errors.length > 0) { 120 if (errors.length > 0) {
120 throw new FormatterException.forError(errors); 121 throw new FormatterException.forError(errors);
121 } 122 }
122 } 123 }
123 124
124 void onError(AnalysisError error) { 125 void onError(AnalysisError error) {
125 errors.add(error); 126 errors.add(error);
126 } 127 }
127 128
128 Token tokenize(String source) { 129 Token tokenize(String source) {
129 var scanner = new StringScanner(null, source, this); 130 var scanner = new StringScanner(null, source, this);
130 return scanner.tokenize(); 131 return scanner.tokenize();
131 } 132 }
132 133
133 } 134 }
134 135
135 /// Placeholder class to hold a reference to the Class object representing
136 /// the Dart keyword void.
137 class Void extends Object {
138
139 }
140
141 136
142 /// Records a sequence of edits to a source string that will cause the string 137 /// Records a sequence of edits to a source string that will cause the string
143 /// to be formatted when applied. 138 /// to be formatted when applied.
144 class EditRecorder { 139 class EditRecorder {
145 140
146 final FormatterOptions options; 141 final FormatterOptions options;
142 final EditStore editStore;
147 143
148 int column = 0; 144 int column = 0;
149 145
150 int sourceIndex = 0; 146 int sourceIndex = 0;
151 String source = ''; 147 String source = '';
152 148
153 Token currentToken; 149 Token currentToken;
154 150
155 int indentationLevel = 0;
156 int numberOfIndentations = 0; 151 int numberOfIndentations = 0;
157 152
158 bool isIndentNeeded = false; 153 bool needsIndent = false;
159 154
160 EditRecorder(this.options); 155 EditRecorder(this.options): editStore = new EditStore();
Brian Wilkerson 2013/06/19 21:50:52 Just curiosity, but why did you move away from usi
pquitslund 2013/06/20 22:34:52 Discussed (exhaustively) in person!
156
157 EditRecorder.forStore(this.options, this.editStore);
158
159 /// Add an [Edit] that describes a textual [replacement] of a text
160 /// interval starting at the given [offset] spanning the given [length].
161 void addEdit(int offset, int length, String replacement) {
162 editStore.addEdit(offset, length, replacement);
163 }
164
165 /// Advance past the given expected [token] (or fail if not matched).
166 void advance(Token token) {
167 if (currentToken.lexeme == token.lexeme) {
168
169 // TODO(pquitslund) emit comments
170 // if (needsIndent) {
171 // advanceIndent();
172 // needsIndent = false;
173 // }
174 // Record writing a token at the current edit location
175 advanceChars(token.length);
176 currentToken = currentToken.next;
177 } else {
178 wrongToken(token.lexeme);
179 }
180 }
181
182 /// Move indices past indent, adding an edit if needed to adjust indentation
183 void advanceIndent() {
184 // var indentWidth = options.indentPerLevel * indentationLevel;
185 // var indentString = getIndentString(indentWidth);
186 // var sourceIndentWidth = 0;
187 // for (var i = 0; i < source.length; i++) {
188 // if (isIndentChar(source[sourceIndex + i])) {
189 // sourceIndentWidth += 1;
190 // } else {
191 // break;
192 // }
193 // }
194 // var hasSameIndent = sourceIndentWidth == indentWidth;
195 // if (hasSameIndent) {
196 // for (var i = 0; i < indentWidth; i++) {
197 // if (source[sourceIndex + i] != indentString[i]) {
198 // hasSameIndent = false;
199 // break;
200 // }
201 // }
202 // if (hasSameIndent) {
203 // advanceChars(indentWidth);
204 // return;
205 // }
206 // }
207 // addEdit(sourceIndex, sourceIndentWidth, indentString);
208 // column += indentWidth;
209 // sourceIndex += sourceIndentWidth;
210
211 var indent = options.indentPerLevel * numberOfIndentations;
212
213 spaces(indent);
214 }
215
216 String getIndentString(int indentWidth) {
217
218 // TODO(pquitslund) a temporary workaround
219 if (indentWidth < 0) {
220 return '';
221 }
222
223 // TODO(pquitslund) allow indent with tab chars
224
225 // Fetch a precomputed indent string
226 if (indentWidth < SPACES.length) {
227 return SPACES[indentWidth];
228 }
229
230 // Build un-precomputed strings dynamically
231 var sb = new StringBuffer();
232 for (var i=0; i < indentWidth; ++i) {
scheglov 2013/06/19 22:12:05 Whitespaces before/after '='.
pquitslund 2013/06/20 22:34:52 Done.
233 sb.write(' ');
234 }
235 return sb.toString();
236 }
237
238 /// Advance past the given expected [token] (or fail if not matched).
239 void advanceToken(String token) {
240 if (currentToken.lexeme == token) {
241 advance(currentToken);
242 } else {
243 wrongToken(token);
244 }
245 }
246
247 /// Advance [column] and [sourceIndex] indices by [len] characters.
248 void advanceChars(int len) {
249 column += len;
250 sourceIndex += len;
251 }
161 252
162 /// Count the number of whitespace chars beginning at the current 253 /// Count the number of whitespace chars beginning at the current
163 /// [sourceIndex]. 254 /// [sourceIndex].
164 int countWhitespace() { 255 int countWhitespace() {
165 var count = 0; 256 var count = 0;
166 for (var i = sourceIndex; i < source.length; ++i) { 257 for (var i = sourceIndex; i < source.length; ++i) {
167 if (isIndentChar(source[i])) { 258 if (isIndentChar(source[i])) {
168 ++count; 259 ++count;
169 } else { 260 } else {
170 break; 261 break;
171 } 262 }
172 } 263 }
173 return count; 264 return count;
174 } 265 }
175 266
176 /// Indent. 267 /// Update indent indices.
177 void indent() { 268 void indent() {
178 indentationLevel += options.indentPerLevel;
179 numberOfIndentations++; 269 numberOfIndentations++;
180 } 270 }
181 271
182 /// Test if there is a newline at the given source [index]. 272 /// Test if there is a newline at the given source [index].
183 bool isNewlineAt(int index) { 273 bool isNewlineAt(int index) {
184 if (index < 0 || index + NEW_LINE.length > source.length) { 274 if (index < 0 || index + NEW_LINE.length > source.length) {
185 return false; 275 return false;
186 } 276 }
187 for (var i = 0; i < NEW_LINE.length; i++) { 277 for (var i = 0; i < NEW_LINE.length; i++) {
188 if (source[index] != NEW_LINE[i]) { 278 if (source[index] != NEW_LINE[i]) {
189 return false; 279 return false;
190 } 280 }
191 } 281 }
192 return true; 282 return true;
193 } 283 }
194 284
285 /// Newline.
286 void newline() {
287 // TODO(pquitslund) emit comments
288 needsIndent = true;
289 // If there is a newline before the edit location, do nothing.
290 if (isNewlineAt(sourceIndex - NEW_LINE.length)) {
291 return;
292 }
293 // If there is a newline after the edit location, advance over it.
294 if (isNewlineAt(sourceIndex)) {
295 advanceChars(NEW_LINE.length);
296 return;
297 }
298 // Otherwise, replace whitespace with a newline.
299 var charsToReplace = countWhitespace();
300 if (isNewlineAt(sourceIndex + charsToReplace)) {
301 ++charsToReplace;
Brian Wilkerson 2013/06/19 21:50:52 Does this want to be incremented by NEW_LINE.lengt
pquitslund 2013/06/20 22:34:52 Yes! Good catch. :)
302 }
303 addEdit(sourceIndex, charsToReplace, NEW_LINE);
304 advanceChars(charsToReplace);
305 }
306
307
308 /// Un-indent.
309 void unindent() {
310 numberOfIndentations--;
311 }
312
313 /// Space.
314 void space() {
315 // TODO(pquitslund) emit comments
316 // // If there is a space before the edit location, do nothing.
317 // if (isSpaceAt(sourceIndex - 1)) {
318 // return;
319 // }
320 // // If there is a space after the edit location, advance over it.
321 // if (isSpaceAt(sourceIndex)) {
322 // advance(1);
323 // return;
324 // }
325 // Otherwise, replace spaces with a single space.
326 spaces(1);
327 }
328
329 /// Spaces.
330 void spaces(int num) {
331 var charsToReplace = countWhitespace();
332 addEdit(sourceIndex, charsToReplace, SPACES[num]);
333 advanceChars(charsToReplace);
334 }
335
336 wrongToken(String token) {
337 throw new FormatterException('expected token: "${token}", '
338 'actual: "${currentToken}"');
339 }
340
341 String toString() =>
342 new EditOperation().apply(editStore.edits,
343 source.substring(0, sourceIndex));
344
195 } 345 }
196 346
197 const SPACE = ' '; 347 const SPACE = ' ';
348 final SPACES = [
349 '',
350 ' ',
351 ' ',
352 ' ',
353 ' ',
354 ' ',
355 ' ',
356 ' ',
357 ' ',
358 ' ',
359 ' ',
360 ' ',
361 ' ',
362 ' ',
363 ' ',
364 ' ',
365 ' ',
366 ];
367
198 368
199 bool isIndentChar(String ch) => ch == SPACE; // TODO(pquitslund) also check tab 369 bool isIndentChar(String ch) => ch == SPACE; // TODO(pquitslund) also check tab
200 370
201 371
202 /// Manages stored [Edit]s. 372 /// Manages stored [Edit]s.
203 class EditStore { 373 class EditStore {
204 374
375 const EditStore();
Brian Wilkerson 2013/06/19 21:50:52 I wouldn't make this a 'const' constructor because
pquitslund 2013/06/20 22:34:52 Actually, I think I AM! Honestly, this is an arti
376
205 /// The underlying sequence of [Edit]s. 377 /// The underlying sequence of [Edit]s.
206 final edits = <Edit>[]; 378 final edits = <Edit>[];
207 379
208 /// Add the given [Edit] to the end of the edit sequence. 380 /// Add the given [Edit] to the end of the edit sequence.
209 void add(Edit edit) { 381 void add(Edit edit) {
210 edits.add(edit); 382 edits.add(edit);
211 } 383 }
212 384
213 /// Add an [Edit] that describes a textual [replacement] of a text interval 385 /// Add an [Edit] that describes a textual [replacement] of a text interval
214 /// starting at the given [offset] spanning the given [length]. 386 /// starting at the given [offset] spanning the given [length].
(...skipping 17 matching lines...) Expand all
232 /// Reset cached state. 404 /// Reset cached state.
233 void reset() { 405 void reset() {
234 edits.clear(); 406 edits.clear();
235 } 407 }
236 408
237 String toString() => 'EditStore( ${edits.toString()} )'; 409 String toString() => 'EditStore( ${edits.toString()} )';
238 410
239 } 411 }
240 412
241 413
242
243 /// Describes a text edit. 414 /// Describes a text edit.
244 class Edit { 415 class Edit {
245 416
246 /// The offset at which to apply the edit. 417 /// The offset at which to apply the edit.
247 final int offset; 418 final int offset;
248 419
249 /// The length of the text interval to replace. 420 /// The length of the text interval to replace.
250 final int length; 421 final int length;
251 422
252 /// The replacement text. 423 /// The replacement text.
253 final String replacement; 424 final String replacement;
254 425
255 /// Create an edit. 426 /// Create an edit.
256 const Edit(this.offset, this.length, this.replacement); 427 const Edit(this.offset, this.length, this.replacement);
257 428
258 /// Create an edit for the given [range]. 429 /// Create an edit for the given [range].
259 Edit.forRange(SourceRange range, String replacement): 430 Edit.forRange(SourceRange range, String replacement):
260 this(range.offset, range.length, replacement); 431 this(range.offset, range.length, replacement);
261 432
262 String toString() => '${offset < 0 ? '(' : 'X('} offset: ${offset} , ' 433 String toString() => '${offset < 0 ? '(' : 'X('} offset: ${offset} , '
263 'length ${length}, replacement :> ${replacement} <:)'; 434 'length ${length}, replacement :> ${replacement} <:)';
264 435
265 } 436 }
266 437
438 /// Applies a sequence of [edits] to a [document].
439 class EditOperation {
440
441 String apply(List<Edit> edits, String document) {
442
443 var edit;
444 for (var i = edits.length - 1; i >= 0; --i) {
445 edit = edits[i];
446 document = replace(document, edit.offset,
447 edit.offset + edit.length, edit.replacement);
448 }
449
450 return document;
451 }
452
453 }
454
455
456 String replace(String str, int start, int end, String replacement) =>
457 str.substring(0, start) + replacement + str.substring(end);
458
459
267 /// An AST visitor that drives formatting heuristics. 460 /// An AST visitor that drives formatting heuristics.
268 class FormattingEngine extends RecursiveASTVisitor<Void> { 461 class FormattingEngine extends RecursiveASTVisitor {
269 462
270 final FormatterOptions options; 463 final FormatterOptions options;
271 464
465 CodeKind kind;
466 EditRecorder recorder;
467
272 FormattingEngine(this.options); 468 FormattingEngine(this.options);
273 469
470 String format(String source, ASTNode node, Token start, CodeKind kind,
471 EditRecorder recorder) {
472
473 this.kind = kind;
474 this.recorder = recorder;
475
476 recorder..source = source
477 ..currentToken = start;
Brian Wilkerson 2013/06/19 21:50:52 This looks weird. I would have expected the state
pquitslund 2013/06/20 22:34:52 Agreed. Still working on how this entry point sho
478
479 node.accept(this);
480
481 var editor = new EditOperation();
482 return editor.apply(recorder.editStore.edits, source);
483 }
484
485
486 visitClassDeclaration(ClassDeclaration node) {
487
488 recorder.advanceIndent();
489
490 if (node.documentationComment != null) {
491 node.documentationComment.accept(this);
492 }
493
494 recorder..advance(node.classKeyword)..space();
495
496 node.name.accept(this);
497
498 if (node.typeParameters != null) {
499 node.typeParameters.accept(this);
500 }
501 recorder.space();
502
503 if (node.extendsClause != null) {
504 node.extendsClause.accept(this);
505 recorder.space();
506 }
507
Brian Wilkerson 2013/06/19 21:50:52 You missed the withClause.
pquitslund 2013/06/20 22:34:52 Coming real soon. (Driven by accompanying tests.)
508 if (node.implementsClause != null) {
509 node.implementsClause.accept(this);
510 recorder.space();
511 }
512
513 recorder..advance(node.leftBracket)
514 ..indent();
515
516 for (var member in node.members) {
517 recorder.newline();
Brian Wilkerson 2013/06/19 21:50:52 Do you want to advanceIndent() after the newline,
pquitslund 2013/06/20 22:34:52 Actually, yes. That is cleaner. Thanks!
518 member.accept(this);
519 }
520
521 recorder..unindent()
522 ..newline()
523 ..advanceIndent()
524 ..advance(node.rightBracket);
525 }
526
527
528 visitBlockFunctionBody(BlockFunctionBody node) {
529 recorder..advance(node.beginToken)
Brian Wilkerson 2013/06/19 21:50:52 The begin token for a BlockFunctionBody is the '{'
530 ..indent()
531 ..newline();
532 node.block.accept(this);
533 recorder..unindent()
534 ..advanceIndent()
535 ..advance(node.endToken);
536 }
537
538
539 visitBlock(Block block) {
540
541 }
542
543
544 visitExpressionFunctionBody(ExpressionFunctionBody node) {
545 recorder..advance(node.beginToken)
Brian Wilkerson 2013/06/19 21:50:52 Here too: use 'functionDefinition' and 'semicolon'
pquitslund 2013/06/20 22:34:52 Thanks!
546 ..indent()
547 ..newline();
548 node.expression.accept(this);
549 recorder..unindent()
550 ..advanceIndent()
551 ..advance(node.endToken);
552 }
553
554
555 visitMethodDeclaration(MethodDeclaration node) {
556
557 recorder.advanceIndent();
Brian Wilkerson 2013/06/19 21:50:52 The rule for AST nodes is that whitespace before a
pquitslund 2013/06/20 22:34:52 Done.
558
559 if (node.modifierKeyword != null) {
560 recorder.advance(node.modifierKeyword);
561 recorder.space();
562 }
563
564 if (node.returnType != null) {
565 recorder.advance(node.returnType.beginToken);
Brian Wilkerson 2013/06/19 21:50:52 I think you want to visit the returnType at this p
pquitslund 2013/06/20 22:34:52 Done.
566 recorder.space();
567 }
568
569 recorder.advance(node.name.beginToken);
570
571 node.parameters.accept(this);
572
573 recorder.space();
574
575 node.body.accept(this);
576 }
577
578
579 visitFormalParameterList(FormalParameterList node) {
580 recorder.advance(node.beginToken);
581 //...
582 recorder.advance(node.endToken);
583 }
584
585
586 visitSimpleIdentifier(SimpleIdentifier node) {
587 recorder.advance(node.token);
588 }
589
274 } 590 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698