| // Copyright (c) 2014, 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:math' as math; |
| |
| import 'chunk.dart'; |
| import 'comment_type.dart'; |
| import 'constants.dart'; |
| import 'dart_formatter.dart'; |
| import 'debug.dart' as debug; |
| import 'line_writer.dart'; |
| import 'nesting_builder.dart'; |
| import 'nesting_level.dart'; |
| import 'rule/rule.dart'; |
| import 'source_code.dart'; |
| import 'source_comment.dart'; |
| import 'style_fix.dart'; |
| |
| /// Matches if the last character of a string is an identifier character. |
| final _trailingIdentifierChar = RegExp(r'[a-zA-Z0-9_]$'); |
| |
| /// Matches a JavaDoc-style doc comment that starts with "/**" and ends with |
| /// "*/" or "**/". |
| final _javaDocComment = RegExp(r'^/\*\*([^*/][\s\S]*?)\*?\*/$'); |
| |
| /// Matches the leading "*" in a line in the middle of a JavaDoc-style comment. |
| final _javaDocLine = RegExp(r'^\s*\*(.*)'); |
| |
| /// Matches spaces at the beginning of as string. |
| final _leadingIndentation = RegExp(r'^(\s*)'); |
| |
| /// Takes the incremental serialized output of [SourceVisitor]--the source text |
| /// along with any comments and preserved whitespace--and produces a coherent |
| /// tree of [Chunk]s which can then be split into physical lines. |
| /// |
| /// Keeps track of leading indentation, expression nesting, and all of the hairy |
| /// code required to seamlessly integrate existing comments into the pure |
| /// output produced by [SourceVisitor]. |
| class ChunkBuilder { |
| final DartFormatter _formatter; |
| |
| /// The builder for the code surrounding the block that this writer is for, or |
| /// `null` if this is writing the top-level code. |
| final ChunkBuilder? _parent; |
| |
| final SourceCode _source; |
| |
| final List<Chunk> _chunks; |
| |
| /// The number of newlines that should be written before the next |
| /// non-whitespace token. |
| /// |
| /// This will always be 0, 1, or 2. |
| int _pendingNewlines = 0; |
| |
| /// Whether a non-breaking space should be written before the next text. |
| bool _pendingSpace = false; |
| |
| /// Whether the next chunk should use expression nesting. |
| bool _pendingNested = false; |
| |
| /// Whether the next chunk should be flush left. |
| bool _pendingFlushLeft = false; |
| |
| /// Whether the most recently written output was a comment. |
| bool _afterComment = false; |
| |
| /// The nested stack of rules that are currently in use. |
| /// |
| /// New chunks are implicitly split by the innermost rule when the chunk is |
| /// ended. |
| final _rules = <Rule>[]; |
| |
| /// The set of rules known to contain hard splits that will in turn force |
| /// these rules to harden. |
| /// |
| /// This is accumulated lazily while chunks are being built. Then, once they |
| /// are all done, the rules are all hardened. We do this later because some |
| /// rules may not have all of their constraints fully wired up until after |
| /// the hard split appears. For example, a hard split in a positional |
| /// argument list needs to force the named arguments to split too, but we |
| /// don't create that rule until after the positional arguments are done. |
| final _hardSplitRules = <Rule>{}; |
| |
| /// The list of rules that are waiting until the next whitespace has been |
| /// written before they start. |
| final _lazyRules = <Rule>[]; |
| |
| /// The nested stack of spans that are currently being written. |
| final _openSpans = <OpenSpan>[]; |
| |
| /// The current state. |
| final _nesting = NestingBuilder(); |
| |
| /// The stack of nesting levels where block arguments may start. |
| /// |
| /// A block argument's contents will nest at the last level in this stack. |
| final _blockArgumentNesting = <NestingLevel>[]; |
| |
| /// The number of calls to [preventSplit()] that have not been ended by a |
| /// call to [endPreventSplit()]. |
| /// |
| /// Splitting is completely disabled inside string interpolation. We do want |
| /// to fix the whitespace inside interpolation, though, so we still format |
| /// them. This tracks whether we're inside an interpolation. We can't use a |
| /// simple bool because interpolation can nest. |
| /// |
| /// When this is non-zero, splits are ignored. |
| int _preventSplitNesting = 0; |
| |
| /// The number of characters of code that can fit in a single line. |
| int get pageWidth => _formatter.pageWidth; |
| |
| /// The current innermost rule. |
| Rule get rule => _rules.last; |
| |
| ChunkBuilder(this._formatter, this._source) |
| : _parent = null, |
| _chunks = [] { |
| indent(_formatter.indent); |
| startBlockArgumentNesting(); |
| } |
| |
| ChunkBuilder._(this._parent, this._formatter, this._source, this._chunks) { |
| startBlockArgumentNesting(); |
| } |
| |
| /// Writes [string], the text for a single token, to the output. |
| /// |
| /// By default, this also implicitly adds one level of nesting if we aren't |
| /// currently nested at all. We do this here so that if a comment appears |
| /// after any token within a statement or top-level form and that comment |
| /// leads to splitting, we correctly nest. Even pathological cases like: |
| /// |
| /// |
| /// import // comment |
| /// "this_gets_nested.dart"; |
| /// |
| /// If we didn't do this here, we'd have to call [nestExpression] after the |
| /// first token of practically every grammar production. |
| /// |
| /// If [mergeEmptySplits] is `true`, the default, then any pending split |
| /// information will be combined with a previously created split if no text |
| /// has been written since. This generally comes into play when text is |
| /// written after a comment. The comment may leave some pending split |
| /// information while [SourceVisitor] may have also created a split and we |
| /// want to combine those. |
| /// |
| /// It is only `false` when writing the contents of a multiline string. There, |
| /// we may *want* to have a series of empty chunks because those represent |
| /// empty lines in the multiline string. |
| void write(String string, {bool mergeEmptySplits = true}) { |
| _emitPendingWhitespace(mergeEmptySplits: mergeEmptySplits); |
| _writeText(string); |
| |
| _lazyRules.forEach(_activateRule); |
| _lazyRules.clear(); |
| |
| _nesting.commitNesting(); |
| _afterComment = false; |
| } |
| |
| /// Writes one or two hard newlines. |
| /// |
| /// Doesn't immediately write them. That way line breaking is correctly |
| /// interleaved with any comments that appear before the next token. |
| /// |
| /// If [isDouble] is `true`, inserts an extra blank line. If [flushLeft] is |
| /// `true`, the next line will start at column 1 and ignore indentation and |
| /// nesting. If [nest] is `true` then the next line will use expression |
| /// nesting. |
| void writeNewline( |
| {bool isDouble = false, bool flushLeft = false, bool nest = false}) { |
| _pendingNewlines = isDouble ? 2 : 1; |
| _pendingFlushLeft = flushLeft; |
| _pendingNested = nest; |
| } |
| |
| /// Writes a space before the subsequent non-whitespace text. |
| void writeSpace() { |
| _pendingSpace = true; |
| } |
| |
| /// Write a split owned by the current innermost rule. |
| /// |
| /// If [nest] is `false`, ignores any current expression nesting. Otherwise, |
| /// uses the current nesting level. If unsplit, it expands to a space if |
| /// [space] is `true`. |
| Chunk split({bool nest = true, bool space = false}) { |
| // If we are not allowed to split at all, don't. Returning null for the |
| // chunk is safe since the rule that uses the chunk will itself get |
| // discarded because no chunk references it. |
| if (_preventSplitNesting > 0) { |
| _pendingNewlines = 0; |
| _pendingNested = false; |
| |
| if (space) _pendingSpace = true; |
| return Chunk.dummy(); |
| } |
| |
| // If a hard split after a comment is already pending, then prefer that over |
| // a soft split. |
| if (_pendingNewlines > 0) return Chunk.dummy(); |
| |
| return _writeSplit(isHard: false, nest: nest, space: space); |
| } |
| |
| /// Outputs the series of [comments] and associated whitespace that appear |
| /// before [token] (which is not written by this). |
| /// |
| /// The list contains each comment as it appeared in the source between the |
| /// last token written and the next one that's about to be written. |
| /// |
| /// [linesBeforeToken] is the number of lines between the last comment (or |
| /// previous token if there are no comments) and the next token. |
| void writeComments( |
| List<SourceComment> comments, int linesBeforeToken, String token) { |
| // Edge case: if we require a blank line, but there exists one between |
| // some of the comments, or after the last one, then we don't need to |
| // enforce one before the first comment. Example: |
| // |
| // library foo; |
| // // comment |
| // |
| // class Bar {} |
| // |
| // Normally, a blank line is required after `library`, but since there is |
| // one after the comment, we don't need one before it. This is mainly so |
| // that commented out directives stick with their preceding group. |
| if (_pendingNewlines == 2 && comments.first.linesBefore < 2) { |
| if (linesBeforeToken > 1) { |
| writeNewline(); |
| } else { |
| for (var i = 1; i < comments.length; i++) { |
| if (comments[i].linesBefore > 1) { |
| writeNewline(); |
| break; |
| } |
| } |
| } |
| } |
| |
| // Edge case: if the previous output was also from a call to |
| // [writeComments()] which ended with a line comment, force a newline. |
| // Normally, comments are strictly interleaved with tokens and you never |
| // get two sequences of comments in a row. However, when applying a fix |
| // that removes a token (like `new`), it's possible to get two sets of |
| // comments in a row, as in: |
| // |
| // // a |
| // new // b |
| // Foo(); |
| // |
| // When that happens, we need to make sure to preserve the split at the end |
| // of the first sequence of comments if there is one. |
| if (_afterComment && _pendingNewlines > 0) { |
| comments.first.linesBefore = 1; |
| _pendingNewlines = 0; |
| } |
| |
| // Edge case: if the comments are completely inline (i.e. just a series of |
| // block comments with no newlines before, after, or between them), then |
| // they will eat any pending newlines. Make sure that doesn't happen by |
| // putting the pending whitespace before the first comment. Turns this: |
| // |
| // library foo; /* a */ /* b */ import 'a.dart'; |
| // |
| // into: |
| // |
| // library foo; |
| // |
| // /* a */ /* b */ import 'a.dart'; |
| if (linesBeforeToken == 0 && |
| _pendingNewlines > comments.first.linesBefore && |
| comments.every((comment) => comment.type == CommentType.inlineBlock)) { |
| comments.first.linesBefore = _pendingNewlines; |
| } |
| |
| // Write each comment and the whitespace between them. |
| for (var i = 0; i < comments.length; i++) { |
| var comment = comments[i]; |
| |
| // See if the comment should follow text on the current line. |
| var chunk = _chunkForComment(comment, token); |
| if (chunk != null) { |
| // The comment follows other text, so decide if it gets a space before |
| // it. |
| _pendingSpace = _needsSpaceBeforeComment(comment, chunk); |
| if (_pendingSpace && chunk != _chunks.last) { |
| // We've already created a split after the comment, so if it doesn't |
| // split, it should get a space. |
| _chunks.last.updateSplit(space: true); |
| } |
| } else { |
| // Split before the comment if it starts a line. |
| if (_pendingNewlines == 0) { |
| if (comment.linesBefore > 0 && |
| (_afterComment || comment.type != CommentType.inlineBlock)) { |
| writeNewline( |
| isDouble: _needsBlankLineBeforeComment(comment), |
| flushLeft: comment.flushLeft, |
| nest: true); |
| } else if (_chunks.isNotEmpty) { |
| _pendingSpace = _needsSpaceBeforeComment(comment, _chunks.last); |
| } |
| } else { |
| _pendingFlushLeft = comment.flushLeft; |
| } |
| |
| _emitPendingWhitespace(isDouble: _needsBlankLineBeforeComment(comment)); |
| } |
| |
| _writeCommentText(comment, chunk); |
| |
| if (comment.selectionStart != null) { |
| startSelectionFromEnd(comment.text.length - comment.selectionStart!); |
| } |
| |
| if (comment.selectionEnd != null) { |
| endSelectionFromEnd(comment.text.length - comment.selectionEnd!); |
| } |
| |
| // Make sure there is at least one newline after a line comment and allow |
| // one or two after a block comment that has nothing after it. |
| int linesAfter; |
| if (i < comments.length - 1) { |
| linesAfter = comments[i + 1].linesBefore; |
| } else { |
| linesAfter = linesBeforeToken; |
| |
| // Always force a newline after multi-line block comments. Prevents |
| // mistakes like: |
| // |
| // /** |
| // * Some doc comment. |
| // */ someFunction() { ... } |
| if (linesAfter == 0 && comments.last.text.contains('\n')) { |
| linesAfter = 1; |
| } |
| } |
| |
| if (linesAfter > 0) { |
| writeNewline( |
| isDouble: _pendingNewlines == 2 || linesAfter > 1, nest: true); |
| } |
| } |
| |
| // If the comment has text following it (aside from a grouping character), |
| // it needs a trailing space. |
| _pendingSpace = _needsSpaceAfterComment(comments.last, token); |
| _afterComment = true; |
| } |
| |
| /// Writes the text of [comment]. |
| /// |
| /// If it's a JavaDoc comment that should be fixed to use `///`, fixes it. |
| void _writeCommentText(SourceComment comment, [Chunk? chunk]) { |
| if (!_formatter.fixes.contains(StyleFix.docComments)) { |
| _writeText(comment.text, chunk); |
| return; |
| } |
| |
| // See if it's a JavaDoc comment. |
| var match = _javaDocComment.firstMatch(comment.text); |
| if (match == null) { |
| _writeText(comment.text, chunk); |
| return; |
| } |
| |
| var lines = match[1]!.split('\n').toList(); |
| var leastIndentation = comment.text.length; |
| |
| for (var i = 0; i < lines.length; i++) { |
| // Trim trailing whitespace and turn any all-whitespace lines to "". |
| var line = lines[i].trimRight(); |
| |
| // Remove any leading "*" from the middle lines. |
| if (i > 0 && i < lines.length - 1) { |
| var match = _javaDocLine.firstMatch(line); |
| if (match != null) { |
| line = match[1]!; |
| } |
| } |
| |
| // Find the line with the least indentation. |
| if (line.isNotEmpty) { |
| var indentation = _leadingIndentation.firstMatch(line)![1]!.length; |
| leastIndentation = math.min(leastIndentation, indentation); |
| } |
| |
| lines[i] = line; |
| } |
| |
| // Trim the first and last lines if empty. |
| if (lines.first.isEmpty) lines.removeAt(0); |
| if (lines.isNotEmpty && lines.last.isEmpty) lines.removeLast(); |
| |
| // Don't completely eliminate an empty block comment. |
| if (lines.isEmpty) lines.add(''); |
| |
| for (var line in lines) { |
| _writeText('///', chunk); |
| if (line.isNotEmpty) { |
| // Discard any indentation shared by all lines. |
| line = line.substring(leastIndentation); |
| _writeText(' $line', chunk); |
| } |
| |
| writeNewline(); |
| _emitPendingWhitespace(); |
| } |
| } |
| |
| /// Creates a new indentation level [spaces] deeper than the current one. |
| /// |
| /// If omitted, [spaces] defaults to [Indent.block]. |
| void indent([int? spaces]) { |
| _nesting.indent(spaces); |
| } |
| |
| /// Discards the most recent indentation level. |
| void unindent() { |
| _nesting.unindent(); |
| } |
| |
| /// Starts a new span with [cost]. |
| /// |
| /// Each call to this needs a later matching call to [endSpan]. |
| void startSpan([int cost = Cost.normal]) { |
| _openSpans.add(OpenSpan(_chunks.length, cost)); |
| } |
| |
| /// Ends the innermost span. |
| void endSpan() { |
| var openSpan = _openSpans.removeLast(); |
| |
| // A span that just covers a single chunk can't be split anyway. |
| var end = _chunks.length; |
| if (openSpan.start == end) return; |
| |
| // Add the span to every chunk that can split it. |
| var span = Span(openSpan.cost); |
| for (var i = openSpan.start; i < end; i++) { |
| var chunk = _chunks[i]; |
| if (!chunk.rule.isHardened) chunk.spans.add(span); |
| } |
| } |
| |
| /// Starts a new [Rule]. |
| /// |
| /// If omitted, defaults to a new [Rule]. |
| void startRule([Rule? rule]) { |
| rule ??= Rule(); |
| |
| // If there are any pending lazy rules, start them now so that the proper |
| // stack ordering of rules is maintained. |
| _lazyRules.forEach(_activateRule); |
| _lazyRules.clear(); |
| |
| _activateRule(rule); |
| } |
| |
| void _activateRule(Rule rule) { |
| // See if any of the rules that contain this one care if it splits. |
| for (var outer in _rules) { |
| if (!outer.splitsOnInnerRules) continue; |
| rule.constrainWhenSplit(outer); |
| } |
| _rules.add(rule); |
| } |
| |
| /// Starts a new [Rule] that comes into play *after* the next whitespace |
| /// (including comments) is written. |
| /// |
| /// This is used for operators who want to start a rule before the first |
| /// operand but not get forced to split if a comment appears before the |
| /// entire expression. |
| /// |
| /// If [rule] is omitted, defaults to a new [Rule]. |
| void startLazyRule([Rule? rule]) { |
| rule ??= Rule(); |
| |
| _lazyRules.add(rule); |
| } |
| |
| /// Ends the innermost rule. |
| void endRule() { |
| if (_lazyRules.isNotEmpty) { |
| _lazyRules.removeLast(); |
| } else { |
| _rules.removeLast(); |
| } |
| } |
| |
| /// Pre-emptively forces all of the current rules to become hard splits. |
| /// |
| /// This is called by [SourceVisitor] when it can determine that a rule will |
| /// will always be split. Turning it (and the surrounding rules) into hard |
| /// splits lets the writer break the output into smaller pieces for the line |
| /// splitter, which helps performance and avoids failing on very large input. |
| /// |
| /// In particular, it's easy for the visitor to know that collections with a |
| /// large number of items must split. Doing that early avoids crashing the |
| /// splitter when it tries to recurse on huge collection literals. |
| void forceRules() => _handleHardSplit(); |
| |
| /// Begins a new expression nesting level [indent] spaces deeper than the |
| /// current one if it splits. |
| /// |
| /// If [indent] is omitted, defaults to [Indent.expression]. If [now] is |
| /// `true`, commits the nesting change immediately instead of waiting until |
| /// after the next chunk of text is written. |
| void nestExpression({int? indent, bool? now}) { |
| now ??= false; |
| |
| _nesting.nest(indent); |
| if (now) _nesting.commitNesting(); |
| } |
| |
| /// Discards the most recent level of expression nesting. |
| /// |
| /// Expressions that are more nested will get increased indentation when split |
| /// if the previous line has a lower level of nesting. |
| /// |
| /// If [now] is `false`, does not commit the nesting change until after the |
| /// next chunk of text is written. |
| void unnest({bool? now}) { |
| now ??= true; |
| |
| _nesting.unnest(); |
| if (now) _nesting.commitNesting(); |
| } |
| |
| /// Marks the selection starting point as occurring [fromEnd] characters to |
| /// the left of the end of what's currently been written. |
| /// |
| /// It counts backwards from the end because this is called *after* the chunk |
| /// of text containing the selection has been output. |
| void startSelectionFromEnd(int fromEnd) { |
| assert(_chunks.isNotEmpty); |
| _chunks.last.startSelectionFromEnd(fromEnd); |
| } |
| |
| /// Marks the selection ending point as occurring [fromEnd] characters to the |
| /// left of the end of what's currently been written. |
| /// |
| /// It counts backwards from the end because this is called *after* the chunk |
| /// of text containing the selection has been output. |
| void endSelectionFromEnd(int fromEnd) { |
| assert(_chunks.isNotEmpty); |
| |
| // If the selection marker is right on a split, then put it before the |
| // newline. |
| if (_chunks.last.text.isNotEmpty) { |
| _chunks.last.endSelectionFromEnd(fromEnd); |
| } else { |
| _chunks[_chunks.length - 2].endSelectionFromEnd(fromEnd); |
| } |
| } |
| |
| /// Captures the current nesting level as marking where subsequent block |
| /// arguments should start. |
| void startBlockArgumentNesting() { |
| _blockArgumentNesting.add(_nesting.currentNesting); |
| } |
| |
| /// Releases the last nesting level captured by [startBlockArgumentNesting]. |
| void endBlockArgumentNesting() { |
| _blockArgumentNesting.removeLast(); |
| } |
| |
| /// Starts a new block chunk and returns the [ChunkBuilder] for it. |
| /// |
| /// Nested blocks are handled using their own independent [LineWriter]. |
| ChunkBuilder startBlock( |
| {Chunk? argumentChunk, bool indent = true, bool space = false}) { |
| // Start a block chunk for the block. It will contain the chunks for the |
| // contents of the block, and its own text will be the closing block |
| // delimiter. |
| var chunk = BlockChunk(argumentChunk, _rules.last, _nesting.indentation, |
| _blockArgumentNesting.last, |
| space: space, flushLeft: _pendingFlushLeft); |
| _chunks.add(chunk); |
| _pendingFlushLeft = false; |
| |
| var builder = ChunkBuilder._(this, _formatter, _source, chunk.children); |
| |
| if (indent) builder.indent(); |
| |
| // Create a hard split for the contents. The rule on the parent BlockChunk |
| // determines whether the body is split or not. This hard rule is only when |
| // the block's contents are split. |
| var rule = Rule.hard(); |
| builder.startRule(rule); |
| builder.split(nest: false, space: space); |
| |
| return builder; |
| } |
| |
| /// Ends this [ChunkBuilder], which must have been created by [startBlock()]. |
| /// |
| /// Forces the chunk that owns the block to split if it can tell that the |
| /// block contents will always split. It does that by looking for hard splits |
| /// in the block that aren't for top level elements in the block. If |
| /// [forceSplit] is `true`, the block always splits. |
| /// |
| /// Returns the previous writer for the surrounding block. |
| ChunkBuilder endBlock({bool forceSplit = true}) { |
| _divideChunks(); |
| |
| // If the last chunk ends with a comment that wants a newline after it, |
| // then force the block contents to split. |
| forceSplit |= _pendingNested; |
| |
| // If we don't already know if the block is going to split, see if it |
| // contains any hard splits or is longer than a page. |
| if (!forceSplit) { |
| var length = 0; |
| for (var chunk in _chunks) { |
| length += chunk.length + chunk.unsplitBlockLength; |
| if (length > _formatter.pageWidth) { |
| forceSplit = true; |
| break; |
| } |
| |
| // If there are any hardened splits in the chunks (aside from ones |
| // using the initial hard rule created by [startBlock()] which are for |
| // the top level elements in the block), then force the block to split. |
| if (chunk.rule.isHardened && chunk.rule != _rules.first) { |
| forceSplit = true; |
| break; |
| } |
| } |
| } |
| |
| // If there is a hard newline within the block, force the surrounding rule |
| // for it so that we apply that constraint. |
| var parent = _parent!; |
| if (forceSplit) parent.forceRules(); |
| return parent; |
| } |
| |
| /// Finishes writing and returns a [SourceCode] containing the final output |
| /// and updated selection, if any. |
| SourceCode end() { |
| assert(_rules.isEmpty); |
| |
| _divideChunks(); |
| |
| if (debug.traceChunkBuilder) { |
| debug.log(debug.green('\nBuilt:')); |
| debug.dumpChunks(0, _chunks); |
| debug.log(); |
| } |
| |
| var writer = LineWriter(_formatter, _chunks); |
| var result = |
| writer.writeLines(isCompilationUnit: _source.isCompilationUnit); |
| |
| int? selectionStart; |
| int? selectionLength; |
| if (_source.selectionStart != null) { |
| selectionStart = result.selectionStart; |
| var selectionEnd = result.selectionEnd; |
| |
| // If we haven't hit the beginning and/or end of the selection yet, they |
| // must be at the very end of the code. |
| selectionStart ??= writer.length; |
| selectionEnd ??= writer.length; |
| |
| selectionLength = selectionEnd - selectionStart; |
| } |
| |
| return SourceCode(result.text, |
| uri: _source.uri, |
| isCompilationUnit: _source.isCompilationUnit, |
| selectionStart: selectionStart, |
| selectionLength: selectionLength); |
| } |
| |
| void preventSplit() { |
| _preventSplitNesting++; |
| } |
| |
| void endPreventSplit() { |
| _preventSplitNesting--; |
| assert(_preventSplitNesting >= 0, 'Mismatched calls.'); |
| } |
| |
| /// Writes the current pending [Whitespace] to the output, if any. |
| /// |
| /// This should only be called after source lines have been preserved to turn |
| /// any ambiguous whitespace into a concrete choice. |
| void _emitPendingWhitespace( |
| {bool isDouble = false, bool mergeEmptySplits = true}) { |
| if (_pendingNewlines == 0) return; |
| |
| if (_pendingNewlines == 2) isDouble = true; |
| _writeSplit( |
| isDouble: isDouble, |
| nest: _pendingNested, |
| mergeEmptySplits: mergeEmptySplits); |
| } |
| |
| /// Tries to find an existing chunk to append [comment] to. |
| /// |
| /// If [comment] should be appending to an existing line (in other words, |
| /// should be moved before a split), then this returns that [Chunk]. |
| /// Otherwise, returns `null`. |
| Chunk? _chunkForComment(SourceComment comment, String token) { |
| // Not if there is nothing before it. |
| if (_chunks.isEmpty) return null; |
| |
| // Don't move a comment to a preceding line. |
| if (comment.linesBefore != 0) return null; |
| |
| // Multi-line comments are always pushed to the next line. |
| if (comment.type == CommentType.doc) return null; |
| if (comment.type == CommentType.block) return null; |
| |
| var chunk = _chunks.last; |
| |
| // We may have started a split for a new chunk but not written any text yet. |
| // In that case, the comment may get written to the previous chunk. Keep a |
| // generic method comment before '(' with the '(', so don't move it before |
| // the split. |
| if (chunk.text.isEmpty && |
| _chunks.length > 1 && |
| (!_isGenericMethodComment(comment) || token != '(')) { |
| chunk = _chunks[_chunks.length - 2]; |
| } |
| |
| // A block comment following a comma probably refers to the following item. |
| var text = chunk.text; |
| if (text.endsWith(',') && comment.type == CommentType.inlineBlock) { |
| return null; |
| } |
| |
| // If the text before the split is an open grouping character, it looks |
| // better to keep it with the elements than with the bracket itself. |
| if (text.endsWith('(') || |
| text.endsWith('[') || |
| (text.endsWith('{') && !text.endsWith('\${'))) { |
| return null; |
| } |
| |
| return chunk; |
| } |
| |
| /// Returns `true` if [comment] appears to be a magic generic method comment. |
| /// |
| /// Those get spaced a little differently to look more like real syntax: |
| /// |
| /// int f/*<S, T>*/(int x) => 3; |
| bool _isGenericMethodComment(SourceComment comment) { |
| return comment.text.startsWith('/*<') || comment.text.startsWith('/*='); |
| } |
| |
| /// Returns `true` if a space should be output between the end of the current |
| /// output and the subsequent comment which is about to be written. |
| /// |
| /// This is only called if the comment is trailing text in the unformatted |
| /// source. In most cases, a space will be output to separate the comment |
| /// from what precedes it. This returns false if: |
| /// |
| /// * This comment does begin the line in the output even if it didn't in |
| /// the source. |
| /// * The comment is a block comment immediately following a grouping |
| /// character (`(`, `[`, or `{`). This is to allow `foo(/* comment */)`, |
| /// et. al. |
| bool _needsSpaceBeforeComment(SourceComment comment, Chunk chunk) { |
| // Not at the beginning of a line. |
| var text = chunk.text; |
| if (text.isEmpty) return false; |
| |
| // Always put a space before line comments. |
| if (comment.type == CommentType.line) return true; |
| |
| // Magic generic method comments like "Foo/*<T>*/" don't get spaces. |
| if (_isGenericMethodComment(comment) && |
| _trailingIdentifierChar.hasMatch(text)) { |
| return false; |
| } |
| |
| // Block comments do not get a space if following a grouping character. |
| return !text.endsWith('(') && !text.endsWith('[') && !text.endsWith('{'); |
| } |
| |
| /// Returns `true` if a space should be output after the last comment which |
| /// was just written and the token that will be written. |
| bool _needsSpaceAfterComment(SourceComment comment, String token) { |
| // Not at the beginning of a line. |
| if (_chunks.last.text.isEmpty) return false; |
| |
| if (_pendingNewlines > 0) return false; |
| |
| // Magic generic method comments like "Foo/*<T>*/" don't get spaces. |
| if (_isGenericMethodComment(comment) && token == '(') { |
| return false; |
| } |
| |
| // Otherwise, it gets a space if the following token is not a delimiter or |
| // the empty string, for EOF. |
| return token != ')' && |
| token != ']' && |
| token != '}' && |
| token != ',' && |
| token != ';' && |
| token != ''; |
| } |
| |
| bool _needsBlankLineBeforeComment(SourceComment comment) { |
| // Only if the source code has a blank line. |
| if (comment.linesBefore < 2) return false; |
| |
| // Don't allow blank lines at the beginning of a block. |
| if (_chunks.isEmpty) return false; |
| |
| // Don't allow blank lines at the beginning of a child block. |
| var text = _chunks.last.text; |
| if (text.endsWith('{') || text.endsWith('[')) return false; |
| |
| return true; |
| } |
| |
| /// Starts a new chunk with the given split information. |
| /// |
| /// Returns the chunk. |
| Chunk _writeSplit( |
| {bool isHard = true, |
| bool isDouble = false, |
| required bool nest, |
| bool space = false, |
| bool mergeEmptySplits = true}) { |
| Chunk chunk; |
| // If we've already just created a split (i.e. we have a new chunk but it's |
| // still empty) then update that split with the new information. This avoids |
| // duplicate splits when line comments occur in places where SourceVisitor |
| // also inserts splits. |
| if (mergeEmptySplits && _chunks.isNotEmpty && _chunks.last.text.isEmpty) { |
| chunk = _chunks.last; |
| |
| // Don't allow a blank newline at the top of a block. |
| if (isDouble) { |
| if (_chunks.length > 1 && |
| _chunks[_chunks.length - 2].text.endsWith('{')) { |
| isDouble = false; |
| } |
| } |
| |
| // Must split before it so that there is a newline after the line comment. |
| chunk.rule.harden(); |
| |
| chunk.updateSplit(flushLeft: _pendingFlushLeft, isDouble: isDouble); |
| } else { |
| chunk = _startChunk(nest ? _nesting.nesting : NestingLevel(), |
| isHard: isHard, isDouble: isDouble, space: space); |
| } |
| |
| if (chunk.rule.isHardened) _handleHardSplit(); |
| |
| _pendingNewlines = 0; |
| _pendingNested = false; |
| return chunk; |
| } |
| |
| /// Writes [text] to either the current chunk or a new one if the current |
| /// chunk is complete. |
| void _writeText(String text, [Chunk? chunk]) { |
| if (chunk == null) { |
| if (_chunks.isEmpty) { |
| _startChunk(NestingLevel(), isHard: true); |
| } |
| |
| chunk = _chunks.last; |
| } |
| |
| if (_pendingSpace && chunk.text.isNotEmpty) chunk.appendText(' '); |
| _pendingSpace = false; |
| |
| chunk.appendText(text); |
| } |
| |
| Chunk _startChunk(NestingLevel nesting, |
| {required bool isHard, bool isDouble = false, bool space = false}) { |
| var rule = isHard ? Rule.hard() : _rules.last; |
| |
| var chunk = Chunk(rule, _nesting.indentation, nesting, |
| space: space, flushLeft: _pendingFlushLeft, isDouble: isDouble); |
| _chunks.add(chunk); |
| |
| _pendingFlushLeft = false; |
| return chunk; |
| } |
| |
| /// Pre-processes the chunks after they are done being written by the visitor |
| /// but before they are run through the line splitter. |
| /// |
| /// Marks ranges of chunks that can be line split independently to keep the |
| /// batches we send to [LineSplitter] small. |
| void _divideChunks() { |
| // Harden all of the rules that we know get forced by containing hard |
| // splits, along with all of the other rules they constrain. |
| _hardenRules(); |
| |
| for (var i = 0; i < _chunks.length; i++) { |
| var chunk = _chunks[i]; |
| if (!_canDivide(chunk)) chunk.preventDivide(); |
| } |
| } |
| |
| /// Returns true if we can divide the chunks at [index] and line split the |
| /// ones before and after that separately. |
| bool _canDivide(Chunk chunk) { |
| // Don't divide at the first chunk. |
| if (chunk == _chunks.first) return false; |
| |
| // Can't divide soft rules. |
| if (!chunk.rule.isHardened) return false; |
| |
| // Can't divide in the middle of expression nesting. |
| if (chunk.nesting.isNested) return false; |
| |
| // If the chunk is the ending delimiter of a block, then don't separate it |
| // and its children from the preceding beginning of the block. |
| if (chunk is BlockChunk) return false; |
| |
| return true; |
| } |
| |
| /// Hardens the active rules when a hard split occurs within them. |
| void _handleHardSplit() { |
| if (_rules.isEmpty) return; |
| |
| // If the current rule doesn't care, it will "eat" the hard split and no |
| // others will care either. |
| if (!_rules.last.splitsOnInnerRules) return; |
| |
| // Start with the innermost rule. This will traverse the other rules it |
| // constrains. |
| _hardSplitRules.add(_rules.last); |
| } |
| |
| /// Replaces all of the previously hardened rules with hard splits, along |
| /// with every rule that those constrain to also split. |
| /// |
| /// This should only be called after all chunks have been written. |
| void _hardenRules() { |
| if (_hardSplitRules.isEmpty) return; |
| |
| void walkConstraints(Rule rule) { |
| rule.harden(); |
| |
| // Follow this rule's constraints, recursively. |
| for (var other in rule.constrainedRules) { |
| if (other == rule) continue; |
| |
| if (!other.isHardened && |
| rule.constrain(rule.fullySplitValue, other) == |
| other.fullySplitValue) { |
| walkConstraints(other); |
| } |
| } |
| } |
| |
| for (var rule in _hardSplitRules) { |
| walkConstraints(rule); |
| } |
| |
| // Discard spans in hardened chunks since we know for certain they will |
| // split anyway. |
| for (var chunk in _chunks) { |
| if (chunk.rule.isHardened) { |
| chunk.spans.clear(); |
| } |
| } |
| } |
| } |