| OLD | NEW |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | 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 | 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 dart_style.src.chunk_builder; | 5 library dart_style.src.chunk_builder; |
| 6 | 6 |
| 7 import 'chunk.dart'; | 7 import 'chunk.dart'; |
| 8 import 'dart_formatter.dart'; | 8 import 'dart_formatter.dart'; |
| 9 import 'debug.dart' as debug; | 9 import 'debug.dart' as debug; |
| 10 import 'line_writer.dart'; | 10 import 'line_writer.dart'; |
| 11 import 'nesting_level.dart'; | 11 import 'nesting_level.dart'; |
| 12 import 'nesting_builder.dart'; | 12 import 'nesting_builder.dart'; |
| 13 import 'rule/rule.dart'; | 13 import 'rule/rule.dart'; |
| 14 import 'source_code.dart'; | 14 import 'source_code.dart'; |
| 15 import 'whitespace.dart'; | 15 import 'whitespace.dart'; |
| 16 | 16 |
| 17 /// Matches if the last character of a string is an identifier character. |
| 18 final _trailingIdentifierChar = new RegExp(r"[a-zA-Z0-9_]$"); |
| 19 |
| 17 /// Takes the incremental serialized output of [SourceVisitor]--the source text | 20 /// Takes the incremental serialized output of [SourceVisitor]--the source text |
| 18 /// along with any comments and preserved whitespace--and produces a coherent | 21 /// along with any comments and preserved whitespace--and produces a coherent |
| 19 /// tree of [Chunk]s which can then be split into physical lines. | 22 /// tree of [Chunk]s which can then be split into physical lines. |
| 20 /// | 23 /// |
| 21 /// Keeps track of leading indentation, expression nesting, and all of the hairy | 24 /// Keeps track of leading indentation, expression nesting, and all of the hairy |
| 22 /// code required to seamlessly integrate existing comments into the pure | 25 /// code required to seamlessly integrate existing comments into the pure |
| 23 /// output produced by [SourceVisitor]. | 26 /// output produced by [SourceVisitor]. |
| 24 class ChunkBuilder { | 27 class ChunkBuilder { |
| 25 final DartFormatter _formatter; | 28 final DartFormatter _formatter; |
| 26 | 29 |
| (...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 91 bool _firstFlushLeft = false; | 94 bool _firstFlushLeft = false; |
| 92 | 95 |
| 93 /// Whether there is pending whitespace that depends on the number of | 96 /// Whether there is pending whitespace that depends on the number of |
| 94 /// newlines in the source. | 97 /// newlines in the source. |
| 95 /// | 98 /// |
| 96 /// This is used to avoid calculating the newlines between tokens unless | 99 /// This is used to avoid calculating the newlines between tokens unless |
| 97 /// actually needed since doing so is slow when done between every single | 100 /// actually needed since doing so is slow when done between every single |
| 98 /// token pair. | 101 /// token pair. |
| 99 bool get needsToPreserveNewlines => | 102 bool get needsToPreserveNewlines => |
| 100 _pendingWhitespace == Whitespace.oneOrTwoNewlines || | 103 _pendingWhitespace == Whitespace.oneOrTwoNewlines || |
| 101 _pendingWhitespace == Whitespace.spaceOrNewline || | |
| 102 _pendingWhitespace == Whitespace.splitOrNewline; | 104 _pendingWhitespace == Whitespace.splitOrNewline; |
| 103 | 105 |
| 104 /// The number of characters of code that can fit in a single line. | 106 /// The number of characters of code that can fit in a single line. |
| 105 int get pageWidth => _formatter.pageWidth; | 107 int get pageWidth => _formatter.pageWidth; |
| 106 | 108 |
| 107 /// The current innermost rule. | 109 /// The current innermost rule. |
| 108 Rule get rule => _rules.last; | 110 Rule get rule => _rules.last; |
| 109 | 111 |
| 110 ChunkBuilder(this._formatter, this._source) | 112 ChunkBuilder(this._formatter, this._source) |
| 111 : _parent = null, | 113 : _parent = null, |
| (...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 164 /// Outputs the series of [comments] and associated whitespace that appear | 166 /// Outputs the series of [comments] and associated whitespace that appear |
| 165 /// before [token] (which is not written by this). | 167 /// before [token] (which is not written by this). |
| 166 /// | 168 /// |
| 167 /// The list contains each comment as it appeared in the source between the | 169 /// The list contains each comment as it appeared in the source between the |
| 168 /// last token written and the next one that's about to be written. | 170 /// last token written and the next one that's about to be written. |
| 169 /// | 171 /// |
| 170 /// [linesBeforeToken] is the number of lines between the last comment (or | 172 /// [linesBeforeToken] is the number of lines between the last comment (or |
| 171 /// previous token if there are no comments) and the next token. | 173 /// previous token if there are no comments) and the next token. |
| 172 void writeComments( | 174 void writeComments( |
| 173 List<SourceComment> comments, int linesBeforeToken, String token) { | 175 List<SourceComment> comments, int linesBeforeToken, String token) { |
| 174 // Corner case: if we require a blank line, but there exists one between | 176 // Edge case: if we require a blank line, but there exists one between |
| 175 // some of the comments, or after the last one, then we don't need to | 177 // some of the comments, or after the last one, then we don't need to |
| 176 // enforce one before the first comment. Example: | 178 // enforce one before the first comment. Example: |
| 177 // | 179 // |
| 178 // library foo; | 180 // library foo; |
| 179 // // comment | 181 // // comment |
| 180 // | 182 // |
| 181 // class Bar {} | 183 // class Bar {} |
| 182 // | 184 // |
| 183 // Normally, a blank line is required after `library`, but since there is | 185 // Normally, a blank line is required after `library`, but since there is |
| 184 // one after the comment, we don't need one before it. This is mainly so | 186 // one after the comment, we don't need one before it. This is mainly so |
| 185 // that commented out directives stick with their preceding group. | 187 // that commented out directives stick with their preceding group. |
| 186 if (_pendingWhitespace == Whitespace.twoNewlines && | 188 if (_pendingWhitespace == Whitespace.twoNewlines && |
| 187 comments.first.linesBefore < 2) { | 189 comments.first.linesBefore < 2) { |
| 188 if (linesBeforeToken > 1) { | 190 if (linesBeforeToken > 1) { |
| 189 _pendingWhitespace = Whitespace.newline; | 191 _pendingWhitespace = Whitespace.newline; |
| 190 } else { | 192 } else { |
| 191 for (var i = 1; i < comments.length; i++) { | 193 for (var i = 1; i < comments.length; i++) { |
| 192 if (comments[i].linesBefore > 1) { | 194 if (comments[i].linesBefore > 1) { |
| 193 _pendingWhitespace = Whitespace.newline; | 195 _pendingWhitespace = Whitespace.newline; |
| 194 break; | 196 break; |
| 195 } | 197 } |
| 196 } | 198 } |
| 197 } | 199 } |
| 198 } | 200 } |
| 199 | 201 |
| 200 // Corner case: if the comments are completely inline (i.e. just a series | 202 // Edge case: if the comments are completely inline (i.e. just a series of |
| 201 // of block comments with no newlines before, after, or between them), then | 203 // block comments with no newlines before, after, or between them), then |
| 202 // they will eat any pending newlines. Make sure that doesn't happen by | 204 // they will eat any pending newlines. Make sure that doesn't happen by |
| 203 // putting the pending whitespace before the first comment and moving them | 205 // putting the pending whitespace before the first comment and moving them |
| 204 // to their own line. Turns this: | 206 // to their own line. Turns this: |
| 205 // | 207 // |
| 206 // library foo; /* a */ /* b */ import 'a.dart'; | 208 // library foo; /* a */ /* b */ import 'a.dart'; |
| 207 // | 209 // |
| 208 // into: | 210 // into: |
| 209 // | 211 // |
| 210 // library foo; | 212 // library foo; |
| 211 // | 213 // |
| (...skipping 22 matching lines...) Expand all Loading... |
| 234 | 236 |
| 235 if (comment.linesBefore == 0) { | 237 if (comment.linesBefore == 0) { |
| 236 // If we're sitting on a split, move the comment before it to adhere it | 238 // If we're sitting on a split, move the comment before it to adhere it |
| 237 // to the preceding text. | 239 // to the preceding text. |
| 238 if (_shouldMoveCommentBeforeSplit(comment.text)) { | 240 if (_shouldMoveCommentBeforeSplit(comment.text)) { |
| 239 _chunks.last.allowText(); | 241 _chunks.last.allowText(); |
| 240 } | 242 } |
| 241 | 243 |
| 242 // The comment follows other text, so we need to decide if it gets a | 244 // The comment follows other text, so we need to decide if it gets a |
| 243 // space before it or not. | 245 // space before it or not. |
| 244 if (_needsSpaceBeforeComment(isLineComment: comment.isLineComment)) { | 246 if (_needsSpaceBeforeComment(comment)) _writeText(" "); |
| 245 _writeText(" "); | |
| 246 } | |
| 247 } else { | 247 } else { |
| 248 // The comment starts a line, so make sure it stays on its own line. | 248 // The comment starts a line, so make sure it stays on its own line. |
| 249 _writeHardSplit( | 249 _writeHardSplit( |
| 250 flushLeft: comment.flushLeft, | 250 flushLeft: comment.flushLeft, |
| 251 isDouble: comment.linesBefore > 1, | 251 isDouble: comment.linesBefore > 1, |
| 252 nest: true); | 252 nest: true); |
| 253 } | 253 } |
| 254 | 254 |
| 255 _writeText(comment.text); | 255 _writeText(comment.text); |
| 256 | 256 |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 293 preserveNewlines(linesBeforeToken); | 293 preserveNewlines(linesBeforeToken); |
| 294 } | 294 } |
| 295 | 295 |
| 296 /// If the current pending whitespace allows some source discretion, pins | 296 /// If the current pending whitespace allows some source discretion, pins |
| 297 /// that down given that the source contains [numLines] newlines at that | 297 /// that down given that the source contains [numLines] newlines at that |
| 298 /// point. | 298 /// point. |
| 299 void preserveNewlines(int numLines) { | 299 void preserveNewlines(int numLines) { |
| 300 // If we didn't know how many newlines the user authored between the last | 300 // If we didn't know how many newlines the user authored between the last |
| 301 // token and this one, now we do. | 301 // token and this one, now we do. |
| 302 switch (_pendingWhitespace) { | 302 switch (_pendingWhitespace) { |
| 303 case Whitespace.spaceOrNewline: | |
| 304 if (numLines > 0) { | |
| 305 _pendingWhitespace = Whitespace.nestedNewline; | |
| 306 } else { | |
| 307 _pendingWhitespace = Whitespace.space; | |
| 308 } | |
| 309 break; | |
| 310 | |
| 311 case Whitespace.splitOrNewline: | 303 case Whitespace.splitOrNewline: |
| 312 if (numLines > 0) { | 304 if (numLines > 0) { |
| 313 _pendingWhitespace = Whitespace.nestedNewline; | 305 _pendingWhitespace = Whitespace.nestedNewline; |
| 314 } else { | 306 } else { |
| 315 _pendingWhitespace = Whitespace.none; | 307 _pendingWhitespace = Whitespace.none; |
| 316 split(space: true); | 308 split(space: true); |
| 317 } | 309 } |
| 318 break; | 310 break; |
| 319 | 311 |
| 320 case Whitespace.oneOrTwoNewlines: | 312 case Whitespace.oneOrTwoNewlines: |
| (...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 362 } | 354 } |
| 363 } | 355 } |
| 364 | 356 |
| 365 /// Starts a new [Rule]. | 357 /// Starts a new [Rule]. |
| 366 /// | 358 /// |
| 367 /// If omitted, defaults to a new [Rule]. | 359 /// If omitted, defaults to a new [Rule]. |
| 368 void startRule([Rule rule]) { | 360 void startRule([Rule rule]) { |
| 369 if (rule == null) rule = new Rule(); | 361 if (rule == null) rule = new Rule(); |
| 370 | 362 |
| 371 // See if any of the rules that contain this one care if it splits. | 363 // See if any of the rules that contain this one care if it splits. |
| 372 _rules.forEach((outer) => outer.contain(rule)); | 364 _rules.forEach((outer) { |
| 365 if (!outer.splitsOnInnerRules) return; |
| 366 rule.imply(outer); |
| 367 }); |
| 373 _rules.add(rule); | 368 _rules.add(rule); |
| 374 } | 369 } |
| 375 | 370 |
| 376 /// Starts a new [Rule] that comes into play *after* the next whitespace | 371 /// Starts a new [Rule] that comes into play *after* the next whitespace |
| 377 /// (including comments) is written. | 372 /// (including comments) is written. |
| 378 /// | 373 /// |
| 379 /// This is used for binary operators who want to start a rule before the | 374 /// This is used for operators who want to start a rule before the first |
| 380 /// first operand but not get forced to split if a comment appears before the | 375 /// operand but not get forced to split if a comment appears before the |
| 381 /// entire expression. | 376 /// entire expression. |
| 382 /// | 377 /// |
| 383 /// If [rule] is omitted, defaults to a new [Rule]. | 378 /// If [rule] is omitted, defaults to a new [Rule]. |
| 384 void startLazyRule([Rule rule]) { | 379 void startLazyRule([Rule rule]) { |
| 385 if (rule == null) rule = new Rule(); | 380 if (rule == null) rule = new Rule(); |
| 386 | 381 |
| 387 _lazyRules.add(rule); | 382 _lazyRules.add(rule); |
| 388 } | 383 } |
| 389 | 384 |
| 390 /// Ends the innermost rule. | 385 /// Ends the innermost rule. |
| 391 void endRule() { | 386 void endRule() { |
| 392 _rules.removeLast(); | 387 if (_lazyRules.isNotEmpty) { |
| 388 _lazyRules.removeLast(); |
| 389 } else { |
| 390 _rules.removeLast(); |
| 391 } |
| 393 } | 392 } |
| 394 | 393 |
| 395 /// Pre-emptively forces all of the current rules to become hard splits. | 394 /// Pre-emptively forces all of the current rules to become hard splits. |
| 396 /// | 395 /// |
| 397 /// This is called by [SourceVisitor] when it can determine that a rule will | 396 /// This is called by [SourceVisitor] when it can determine that a rule will |
| 398 /// will always be split. Turning it (and the surrounding rules) into hard | 397 /// will always be split. Turning it (and the surrounding rules) into hard |
| 399 /// splits lets the writer break the output into smaller pieces for the line | 398 /// splits lets the writer break the output into smaller pieces for the line |
| 400 /// splitter, which helps performance and avoids failing on very large input. | 399 /// splitter, which helps performance and avoids failing on very large input. |
| 401 /// | 400 /// |
| 402 /// In particular, it's easy for the visitor to know that collections with a | 401 /// In particular, it's easy for the visitor to know that collections with a |
| (...skipping 185 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 588 break; | 587 break; |
| 589 | 588 |
| 590 case Whitespace.newlineFlushLeft: | 589 case Whitespace.newlineFlushLeft: |
| 591 _writeHardSplit(flushLeft: true, nest: true); | 590 _writeHardSplit(flushLeft: true, nest: true); |
| 592 break; | 591 break; |
| 593 | 592 |
| 594 case Whitespace.twoNewlines: | 593 case Whitespace.twoNewlines: |
| 595 _writeHardSplit(isDouble: true); | 594 _writeHardSplit(isDouble: true); |
| 596 break; | 595 break; |
| 597 | 596 |
| 598 case Whitespace.spaceOrNewline: | |
| 599 case Whitespace.splitOrNewline: | 597 case Whitespace.splitOrNewline: |
| 600 case Whitespace.oneOrTwoNewlines: | 598 case Whitespace.oneOrTwoNewlines: |
| 601 // We should have pinned these down before getting here. | 599 // We should have pinned these down before getting here. |
| 602 assert(false); | 600 assert(false); |
| 603 break; | 601 break; |
| 604 } | 602 } |
| 605 | 603 |
| 606 _pendingWhitespace = Whitespace.none; | 604 _pendingWhitespace = Whitespace.none; |
| 607 } | 605 } |
| 608 | 606 |
| 609 /// Returns `true` if the last chunk is a split that should be moved after the | 607 /// Returns `true` if the last chunk is a split that should be moved after the |
| 610 /// comment that is about to be written. | 608 /// comment that is about to be written. |
| 611 bool _shouldMoveCommentBeforeSplit(String comment) { | 609 bool _shouldMoveCommentBeforeSplit(String comment) { |
| 612 // Not if there is nothing before it. | 610 // Not if there is nothing before it. |
| 613 if (_chunks.isEmpty) return false; | 611 if (_chunks.isEmpty) return false; |
| 614 | 612 |
| 615 // Multi-line comments are always pushed to the next line. | 613 // Multi-line comments are always pushed to the next line. |
| 616 if (comment.contains("\n")) return false; | 614 if (comment.contains("\n")) return false; |
| 617 | 615 |
| 618 // If the text before the split is an open grouping character, we don't | |
| 619 // want to adhere the comment to that. | |
| 620 var text = _chunks.last.text; | 616 var text = _chunks.last.text; |
| 617 |
| 618 // A block comment following a comma probably refers to the following item. |
| 619 if (text.endsWith(",") && comment.startsWith("/*")) return false; |
| 620 |
| 621 // If the text before the split is an open grouping character, it looks |
| 622 // better to keep it with the elements than with the bracket itself. |
| 621 return !text.endsWith("(") && !text.endsWith("[") && !text.endsWith("{"); | 623 return !text.endsWith("(") && !text.endsWith("[") && !text.endsWith("{"); |
| 622 } | 624 } |
| 623 | 625 |
| 626 /// Returns `true` if [comment] appears to be a magic generic method comment. |
| 627 /// |
| 628 /// Those get spaced a little differently to look more like real syntax: |
| 629 /// |
| 630 /// int f/*<S, T>*/(int x) => 3; |
| 631 bool _isGenericMethodComment(SourceComment comment) { |
| 632 return comment.text.startsWith("/*<") || comment.text.startsWith("/*="); |
| 633 } |
| 634 |
| 624 /// Returns `true` if a space should be output between the end of the current | 635 /// Returns `true` if a space should be output between the end of the current |
| 625 /// output and the subsequent comment which is about to be written. | 636 /// output and the subsequent comment which is about to be written. |
| 626 /// | 637 /// |
| 627 /// This is only called if the comment is trailing text in the unformatted | 638 /// This is only called if the comment is trailing text in the unformatted |
| 628 /// source. In most cases, a space will be output to separate the comment | 639 /// source. In most cases, a space will be output to separate the comment |
| 629 /// from what precedes it. This returns false if: | 640 /// from what precedes it. This returns false if: |
| 630 /// | 641 /// |
| 631 /// * This comment does begin the line in the output even if it didn't in | 642 /// * This comment does begin the line in the output even if it didn't in |
| 632 /// the source. | 643 /// the source. |
| 633 /// * The comment is a block comment immediately following a grouping | 644 /// * The comment is a block comment immediately following a grouping |
| 634 /// character (`(`, `[`, or `{`). This is to allow `foo(/* comment */)`, | 645 /// character (`(`, `[`, or `{`). This is to allow `foo(/* comment */)`, |
| 635 /// et. al. | 646 /// et. al. |
| 636 bool _needsSpaceBeforeComment({bool isLineComment}) { | 647 bool _needsSpaceBeforeComment(SourceComment comment) { |
| 637 // Not at the start of the file. | 648 // Not at the start of the file. |
| 638 if (_chunks.isEmpty) return false; | 649 if (_chunks.isEmpty) return false; |
| 639 | 650 |
| 640 // Not at the start of a line. | 651 // Not at the start of a line. |
| 641 if (!_chunks.last.canAddText) return false; | 652 if (!_chunks.last.canAddText) return false; |
| 642 | 653 |
| 643 var text = _chunks.last.text; | 654 var text = _chunks.last.text; |
| 644 if (text.endsWith("\n")) return false; | 655 if (text.endsWith("\n")) return false; |
| 645 | 656 |
| 646 // Always put a space before line comments. | 657 // Always put a space before line comments. |
| 647 if (isLineComment) return true; | 658 if (comment.isLineComment) return true; |
| 659 |
| 660 // Magic generic method comments like "Foo/*<T>*/" don't get spaces. |
| 661 if (_isGenericMethodComment(comment) && |
| 662 _trailingIdentifierChar.hasMatch(text)) { |
| 663 return false; |
| 664 } |
| 648 | 665 |
| 649 // Block comments do not get a space if following a grouping character. | 666 // Block comments do not get a space if following a grouping character. |
| 650 return !text.endsWith("(") && !text.endsWith("[") && !text.endsWith("{"); | 667 return !text.endsWith("(") && !text.endsWith("[") && !text.endsWith("{"); |
| 651 } | 668 } |
| 652 | 669 |
| 653 /// Returns `true` if a space should be output after the last comment which | 670 /// Returns `true` if a space should be output after the last comment which |
| 654 /// was just written and the token that will be written. | 671 /// was just written and the token that will be written. |
| 655 bool _needsSpaceAfterLastComment(List<SourceComment> comments, String token) { | 672 bool _needsSpaceAfterLastComment(List<SourceComment> comments, String token) { |
| 656 // Not if there are no comments. | 673 // Not if there are no comments. |
| 657 if (comments.isEmpty) return false; | 674 if (comments.isEmpty) return false; |
| 658 | 675 |
| 659 // Not at the beginning of a line. | 676 // Not at the beginning of a line. |
| 660 if (!_chunks.last.canAddText) return false; | 677 if (!_chunks.last.canAddText) return false; |
| 661 | 678 |
| 679 // Magic generic method comments like "Foo/*<T>*/" don't get spaces. |
| 680 if (_isGenericMethodComment(comments.last) && token == "(") { |
| 681 return false; |
| 682 } |
| 683 |
| 662 // Otherwise, it gets a space if the following token is not a delimiter or | 684 // Otherwise, it gets a space if the following token is not a delimiter or |
| 663 // the empty string, for EOF. | 685 // the empty string, for EOF. |
| 664 return token != ")" && | 686 return token != ")" && |
| 665 token != "]" && | 687 token != "]" && |
| 666 token != "}" && | 688 token != "}" && |
| 667 token != "," && | 689 token != "," && |
| 668 token != ";" && | 690 token != ";" && |
| 669 token != ""; | 691 token != ""; |
| 670 } | 692 } |
| 671 | 693 |
| (...skipping 116 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 788 | 810 |
| 789 // Discard spans in hardened chunks since we know for certain they will | 811 // Discard spans in hardened chunks since we know for certain they will |
| 790 // split anyway. | 812 // split anyway. |
| 791 for (var chunk in _chunks) { | 813 for (var chunk in _chunks) { |
| 792 if (chunk.rule != null && chunk.rule.isHardened) { | 814 if (chunk.rule != null && chunk.rule.isHardened) { |
| 793 chunk.spans.clear(); | 815 chunk.spans.clear(); |
| 794 } | 816 } |
| 795 } | 817 } |
| 796 } | 818 } |
| 797 } | 819 } |
| OLD | NEW |