diff --git a/lib/src/front_end/ast_node_visitor.dart b/lib/src/front_end/ast_node_visitor.dart index f8a4e418..7f9a2627 100644 --- a/lib/src/front_end/ast_node_visitor.dart +++ b/lib/src/front_end/ast_node_visitor.dart @@ -37,12 +37,11 @@ class AstNodeVisitor extends ThrowingAstVisitor final LineInfo lineInfo; @override - final PieceWriter writer; + final PieceWriter pieces; - /// Initialize a newly created visitor to write source code representing - /// the visited nodes to the given [writer]. + /// Create a new visitor that will be called to visit the code in [source]. AstNodeVisitor(DartFormatter formatter, this.lineInfo, SourceCode source) - : writer = PieceWriter(formatter, source); + : pieces = PieceWriter(formatter, source); /// Visits [node] and returns the formatted result. /// @@ -86,10 +85,10 @@ class AstNodeVisitor extends ThrowingAstVisitor // Write any comments at the end of the code. sequence.addCommentsBefore(node.endToken.next!); - writer.push(sequence.build()); + pieces.give(sequence.build()); // Finish writing and return the complete result. - return writer.finish(); + return pieces.finish(); } @override @@ -130,7 +129,7 @@ class AstNodeVisitor extends ThrowingAstVisitor @override void visitAssignmentExpression(AssignmentExpression node) { visit(node.leftHandSide); - writer.space(); + space(); finishAssignment(node.operator, node.rightHandSide); } @@ -219,19 +218,17 @@ class AstNodeVisitor extends ThrowingAstVisitor @override void visitConditionalExpression(ConditionalExpression node) { visit(node.condition); - var condition = writer.pop(); - writer.split(); + var condition = pieces.split(); token(node.question); - writer.space(); + space(); visit(node.thenExpression); - var thenBranch = writer.pop(); - writer.split(); + var thenBranch = pieces.split(); token(node.colon); - writer.space(); + space(); visit(node.elseExpression); - var elseBranch = writer.pop(); + var elseBranch = pieces.take(); var piece = InfixPiece([condition, thenBranch, elseBranch]); @@ -243,13 +240,13 @@ class AstNodeVisitor extends ThrowingAstVisitor piece.pin(State.split); } - writer.push(piece); + pieces.give(piece); } @override void visitConfiguration(Configuration node) { token(node.ifKeyword); - writer.space(); + space(); token(node.leftParenthesis); if (node.equalToken case var equals?) { @@ -259,7 +256,7 @@ class AstNodeVisitor extends ThrowingAstVisitor } token(node.rightParenthesis); - writer.space(); + space(); visit(node.uri); } @@ -311,20 +308,19 @@ class AstNodeVisitor extends ThrowingAstVisitor @override void visitDoStatement(DoStatement node) { token(node.doKeyword); - writer.space(); + space(); visit(node.body); - writer.space(); + space(); token(node.whileKeyword); - var body = writer.pop(); - writer.split(); + var body = pieces.split(); token(node.leftParenthesis); visit(node.condition); token(node.rightParenthesis); token(node.semicolon); - var condition = writer.pop(); + var condition = pieces.take(); - writer.push(DoWhilePiece(body, condition)); + pieces.give(DoWhilePiece(body, condition)); } @override @@ -417,7 +413,7 @@ class AstNodeVisitor extends ThrowingAstVisitor } builder.rightBracket(node.rightParenthesis, delimiter: node.rightDelimiter); - writer.push(builder.build()); + pieces.give(builder.build()); } @override @@ -428,8 +424,7 @@ class AstNodeVisitor extends ThrowingAstVisitor @override void visitForStatement(ForStatement node) { token(node.forKeyword); - writer.split(); - var forKeyword = writer.pop(); + var forKeyword = pieces.split(); // Treat the for loop parts sort of as an argument list where each clause // is a separate argument. This means that when they split, they split like: @@ -485,13 +480,13 @@ class AstNodeVisitor extends ThrowingAstVisitor } partsList.rightBracket(node.rightParenthesis); - writer.split(); var forPartsPiece = partsList.build(); + pieces.split(); visit(node.body); - var body = writer.pop(); + var body = pieces.take(); - writer.push(ForPiece(forKeyword, forPartsPiece, body, + pieces.give(ForPiece(forKeyword, forPartsPiece, body, hasBlockBody: node.body is Block)); } @@ -649,14 +644,14 @@ class AstNodeVisitor extends ThrowingAstVisitor } sequence.visit(node.statement); - writer.push(sequence.build()); + pieces.give(sequence.build()); } @override void visitLibraryDirective(LibraryDirective node) { createDirectiveMetadata(node); token(node.libraryKeyword); - visit(node.name2, before: writer.space); + visit(node.name2, before: space); token(node.semicolon); } @@ -794,7 +789,7 @@ class AstNodeVisitor extends ThrowingAstVisitor void visitPartDirective(PartDirective node) { createDirectiveMetadata(node); token(node.partKeyword); - writer.space(); + space(); visit(node.uri); token(node.semicolon); } @@ -803,9 +798,9 @@ class AstNodeVisitor extends ThrowingAstVisitor void visitPartOfDirective(PartOfDirective node) { createDirectiveMetadata(node); token(node.partKeyword); - writer.space(); + space(); token(node.ofKeyword); - writer.space(); + space(); // Part-of may have either a name or a URI. Only one of these will be // non-null. We visit both since visit() ignores null. @@ -908,7 +903,7 @@ class AstNodeVisitor extends ThrowingAstVisitor token(node.returnKeyword); if (node.expression case var expression) { - writer.space(); + space(); visit(expression); } @@ -922,7 +917,7 @@ class AstNodeVisitor extends ThrowingAstVisitor // come at the top of the file, we don't have to worry about preceding // comments or whitespace. // TODO(new-ir): Update selection if inside the script tag. - writer.write(node.scriptTag.lexeme.trim()); + pieces.write(node.scriptTag.lexeme.trim()); } @override @@ -948,14 +943,12 @@ class AstNodeVisitor extends ThrowingAstVisitor if ((node.type, node.name) case (var type?, var name?)) { // Have both a type and name, so allow splitting between them. visit(type); - var typePiece = writer.pop(); - writer.split(); + var typePiece = pieces.split(); token(name); - var namePiece = writer.pop(); - writer.split(); + var namePiece = pieces.take(); - writer.push(VariablePiece(typePiece, [namePiece], hasType: true)); + pieces.give(VariablePiece(typePiece, [namePiece], hasType: true)); } else { // Only one of name or type so just write whichever there is. visit(node.type); @@ -1005,7 +998,7 @@ class AstNodeVisitor extends ThrowingAstVisitor createSwitchValue(node.switchKeyword, node.leftParenthesis, node.expression, node.rightParenthesis); - writer.space(); + space(); list.leftBracket(node.leftBracket); for (var member in node.cases) { @@ -1013,7 +1006,7 @@ class AstNodeVisitor extends ThrowingAstVisitor } list.rightBracket(node.rightBracket); - writer.push(list.build()); + pieces.give(list.build()); } @override @@ -1021,7 +1014,7 @@ class AstNodeVisitor extends ThrowingAstVisitor if (node.guardedPattern.whenClause != null) throw UnimplementedError(); visit(node.guardedPattern.pattern); - writer.space(); + space(); finishAssignment(node.arrow, node.expression); } @@ -1032,10 +1025,9 @@ class AstNodeVisitor extends ThrowingAstVisitor // Attach the ` {` after the `)` in the [ListPiece] created by // [createSwitchValue()]. - writer.space(); + space(); token(node.leftBracket); - writer.split(); - var switchPiece = writer.pop(); + var switchPiece = pieces.split(); var sequence = SequenceBuilder(this); for (var member in node.members) { @@ -1047,10 +1039,10 @@ class AstNodeVisitor extends ThrowingAstVisitor token(member.keyword); if (member is SwitchCase) { - writer.space(); + space(); visit(member.expression); } else if (member is SwitchPatternCase) { - writer.space(); + space(); if (member.guardedPattern.whenClause != null) { throw UnimplementedError(); @@ -1063,8 +1055,7 @@ class AstNodeVisitor extends ThrowingAstVisitor } token(member.colon); - var casePiece = writer.pop(); - writer.split(); + var casePiece = pieces.split(); // Don't allow any blank lines between the `case` line and the first // statement in the case (or the next case if this case has no body). @@ -1079,9 +1070,9 @@ class AstNodeVisitor extends ThrowingAstVisitor sequence.addCommentsBefore(node.rightBracket); token(node.rightBracket); - var rightBracketPiece = writer.pop(); + var rightBracketPiece = pieces.take(); - writer.push(BlockPiece(switchPiece, sequence.build(), rightBracketPiece, + pieces.give(BlockPiece(switchPiece, sequence.build(), rightBracketPiece, alwaysSplit: node.members.isNotEmpty)); } @@ -1126,7 +1117,7 @@ class AstNodeVisitor extends ThrowingAstVisitor void visitTypeParameter(TypeParameter node) { token(node.name); if (node.bound case var bound?) { - writer.space(); + space(); modifier(node.extendsKeyword); visit(bound); } @@ -1157,17 +1148,17 @@ class AstNodeVisitor extends ThrowingAstVisitor // argument list or a function type's parameter list) affect the indentation // and splitting of the surrounding variable declaration. visit(node.type); - var header = writer.pop(); + var header = pieces.take(); var variables = []; for (var variable in node.variables) { - writer.split(); + pieces.split(); visit(variable); commaAfter(variable); - variables.add(writer.pop()); + variables.add(pieces.take()); } - writer.push(VariablePiece(header, variables, hasType: node.type != null)); + pieces.give(VariablePiece(header, variables, hasType: node.type != null)); } @override @@ -1179,19 +1170,18 @@ class AstNodeVisitor extends ThrowingAstVisitor @override void visitWhileStatement(WhileStatement node) { token(node.whileKeyword); - writer.space(); + space(); token(node.leftParenthesis); visit(node.condition); token(node.rightParenthesis); - var condition = writer.pop(); - writer.split(); + var condition = pieces.split(); visit(node.body); - var body = writer.pop(); + var body = pieces.take(); var piece = IfPiece(); piece.add(condition, body, isBlock: node.body is Block); - writer.push(piece); + pieces.give(piece); } @override diff --git a/lib/src/front_end/comment_writer.dart b/lib/src/front_end/comment_writer.dart index ac1912d3..bf45b60d 100644 --- a/lib/src/front_end/comment_writer.dart +++ b/lib/src/front_end/comment_writer.dart @@ -43,7 +43,7 @@ import 'piece_writer.dart'; // TODO(tall): When argument lists and their comment handling is supported, // mention that here. mixin CommentWriter { - PieceWriter get writer; + PieceWriter get pieces; LineInfo get lineInfo; @@ -78,20 +78,20 @@ mixin CommentWriter { if (comments.isHanging(i)) { // Attach the comment to the previous token. - writer.space(); - writer.writeComment(comment, hanging: true); + pieces.writeSpace(); + pieces.writeComment(comment, hanging: true); } else { - writer.writeNewline(); - writer.writeComment(comment); + pieces.writeNewline(); + pieces.writeComment(comment); } if (comment.type == CommentType.line || comment.type == CommentType.doc) { - writer.writeNewline(); + pieces.writeNewline(); } } if (comments.isNotEmpty && _needsSpaceAfterComment(token.lexeme)) { - writer.space(); + pieces.writeSpace(); } } diff --git a/lib/src/front_end/delimited_list_builder.dart b/lib/src/front_end/delimited_list_builder.dart index e7c743f7..00dc6cdb 100644 --- a/lib/src/front_end/delimited_list_builder.dart +++ b/lib/src/front_end/delimited_list_builder.dart @@ -60,8 +60,7 @@ class DelimitedListBuilder { void leftBracket(Token bracket, {Token? delimiter}) { _visitor.token(bracket); _visitor.token(delimiter); - _leftBracket = _visitor.writer.pop(); - _visitor.writer.split(); + _leftBracket = _visitor.pieces.split(); } /// Adds the closing [bracket] to the built list along with any comments that @@ -99,7 +98,7 @@ class DelimitedListBuilder { _visitor.token(delimiter); _visitor.token(bracket); - _rightBracket = _visitor.writer.pop(); + _rightBracket = _visitor.pieces.take(); } /// Adds [piece] to the built list. @@ -128,8 +127,7 @@ class DelimitedListBuilder { // Traverse the element itself. _visitor.visit(element); - _visitor.writer.split(); - add(_visitor.writer.pop()); + add(_visitor.pieces.split()); var nextToken = element.endToken.next!; if (nextToken.lexeme == ',') { @@ -191,20 +189,19 @@ class DelimitedListBuilder { // Add any hanging inline block comments to the previous element before the // subsequent ",". for (var comment in inlineComments) { - _visitor.writer.space(); - _visitor.writer.writeComment(comment, hanging: true); + _visitor.space(); + _visitor.pieces.writeComment(comment, hanging: true); } // Add any remaining hanging line comments to the previous element after // the ",". if (hangingComments.isNotEmpty) { for (var comment in hangingComments) { - _visitor.writer.space(); - _visitor.writer.writeComment(comment); + _visitor.space(); + _visitor.pieces.writeComment(comment); } - _elements.last = _elements.last.withComment(_visitor.writer.pop()); - _visitor.writer.split(); + _elements.last = _elements.last.withComment(_visitor.pieces.split()); } // Comments that are neither hanging nor leading are treated like their own @@ -215,15 +212,14 @@ class DelimitedListBuilder { _blanksAfter.add(_elements.last); } - _visitor.writer.writeComment(comment); - _elements.add(ListElement.comment(_visitor.writer.pop())); - _visitor.writer.split(); + _visitor.pieces.writeComment(comment); + _elements.add(ListElement.comment(_visitor.pieces.split())); } // Leading comments are written before the next element. for (var comment in leadingComments) { - _visitor.writer.writeComment(comment); - _visitor.writer.space(); + _visitor.pieces.writeComment(comment); + _visitor.space(); } } diff --git a/lib/src/front_end/piece_factory.dart b/lib/src/front_end/piece_factory.dart index a6e91afc..f7783185 100644 --- a/lib/src/front_end/piece_factory.dart +++ b/lib/src/front_end/piece_factory.dart @@ -60,8 +60,7 @@ mixin PieceFactory implements CommentWriter { /// ``` void createBlock(Block block, {bool forceSplit = false}) { token(block.leftBracket); - var leftBracketPiece = writer.pop(); - writer.split(); + var leftBracketPiece = pieces.split(); var sequence = SequenceBuilder(this); for (var node in block.statements) { @@ -72,9 +71,9 @@ mixin PieceFactory implements CommentWriter { sequence.addCommentsBefore(block.rightBracket); token(block.rightBracket); - var rightBracketPiece = writer.pop(); + var rightBracketPiece = pieces.take(); - writer.push(BlockPiece( + pieces.give(BlockPiece( leftBracketPiece, sequence.build(), rightBracketPiece, alwaysSplit: forceSplit || block.statements.isNotEmpty)); } @@ -83,7 +82,7 @@ mixin PieceFactory implements CommentWriter { void createBreak(Token keyword, SimpleIdentifier? label, Token semicolon) { token(keyword); if (label != null) { - writer.space(); + space(); visit(label); } token(semicolon); @@ -141,8 +140,7 @@ mixin PieceFactory implements CommentWriter { Piece? returnTypePiece; if (returnType != null) { visit(returnType); - returnTypePiece = writer.pop(); - writer.split(); + returnTypePiece = pieces.split(); } token(functionKeywordOrName); @@ -152,8 +150,8 @@ mixin PieceFactory implements CommentWriter { // Allow splitting after the return type. if (returnTypePiece != null) { - var parametersPiece = writer.pop(); - writer.push(FunctionTypePiece(returnTypePiece, parametersPiece)); + var parametersPiece = pieces.take(); + pieces.give(FunctionTypePiece(returnTypePiece, parametersPiece)); } } @@ -166,12 +164,11 @@ mixin PieceFactory implements CommentWriter { // chain handled by a single [IfPiece]. void traverse(IfStatement node) { token(node.ifKeyword); - writer.space(); + space(); token(node.leftParenthesis); visit(node.expression); token(node.rightParenthesis); - var condition = writer.pop(); - writer.split(); + var condition = pieces.split(); // Edge case: When the then branch is a block and there is an else clause // after it, we want to force the block to split even if empty, like: @@ -189,8 +186,7 @@ mixin PieceFactory implements CommentWriter { visit(node.thenStatement); } - var thenStatement = writer.pop(); - writer.split(); + var thenStatement = pieces.split(); piece.add(condition, thenStatement, isBlock: node.thenStatement is Block); switch (node.elseStatement) { @@ -198,26 +194,24 @@ mixin PieceFactory implements CommentWriter { // Hit an else-if, so flatten it into the chain with the `else` // becoming part of the next section's header. token(node.elseKeyword); - writer.space(); + space(); traverse(elseIf); case var elseStatement?: // Any other kind of else body ends the chain, with the header for // the last section just being the `else` keyword. token(node.elseKeyword); - var header = writer.pop(); - writer.split(); + var header = pieces.split(); visit(elseStatement); - var statement = writer.pop(); - writer.split(); + var statement = pieces.take(); piece.add(header, statement, isBlock: elseStatement is Block); } } traverse(ifStatement); - writer.push(piece); + pieces.give(piece); } /// Creates an [ImportPiece] for an import or export directive. @@ -225,17 +219,17 @@ mixin PieceFactory implements CommentWriter { {Token? deferredKeyword, Token? asKeyword, SimpleIdentifier? prefix}) { createDirectiveMetadata(directive); token(keyword); - writer.space(); + space(); visit(directive.uri); - var directivePiece = writer.pop(); + var directivePiece = pieces.take(); Piece? configurationsPiece; if (directive.configurations.isNotEmpty) { var configurations = []; for (var configuration in directive.configurations) { - writer.split(); + pieces.split(); visit(configuration); - configurations.add(writer.pop()); + configurations.add(pieces.take()); } configurationsPiece = PostfixPiece(configurations); @@ -243,29 +237,29 @@ mixin PieceFactory implements CommentWriter { Piece? asClause; if (asKeyword != null) { - writer.split(); - token(deferredKeyword, after: writer.space); + pieces.split(); + token(deferredKeyword, after: space); token(asKeyword); - writer.space(); + space(); visit(prefix); - asClause = PostfixPiece([writer.pop()]); + asClause = PostfixPiece([pieces.take()]); } var combinators = []; for (var combinatorNode in directive.combinators) { - writer.split(); + pieces.split(); token(combinatorNode.keyword); - var combinator = ImportCombinator(writer.pop()); + var combinator = ImportCombinator(pieces.take()); combinators.add(combinator); switch (combinatorNode) { case HideCombinator(hiddenNames: var names): case ShowCombinator(shownNames: var names): for (var name in names) { - writer.split(); + pieces.split(); token(name.token); commaAfter(name); - combinator.names.add(writer.pop()); + combinator.names.add(pieces.take()); } default: throw StateError('Unknown combinator type $combinatorNode.'); @@ -274,7 +268,7 @@ mixin PieceFactory implements CommentWriter { token(directive.semicolon); - writer.push(ImportPiece( + pieces.give(ImportPiece( directivePiece, configurationsPiece, asClause, combinators)); } @@ -289,24 +283,24 @@ mixin PieceFactory implements CommentWriter { void createInfix(AstNode left, Token operator, AstNode right, {bool hanging = false, Token? operator2}) { var operands = []; + visit(left); - operands.add(writer.pop()); if (hanging) { - writer.space(); + space(); token(operator); token(operator2); - writer.split(); + operands.add(pieces.split()); } else { - writer.split(); + operands.add(pieces.split()); token(operator); token(operator2); - writer.space(); + space(); } visit(right); - operands.add(writer.pop()); - writer.push(InfixPiece(operands)); + operands.add(pieces.take()); + pieces.give(InfixPiece(operands)); } /// Creates a chained infix operation: a binary operator expression, or @@ -330,30 +324,28 @@ mixin PieceFactory implements CommentWriter { var operands = []; void traverse(AstNode e) { - if (e is! T) { - visit(e); - operands.add(writer.pop()); - } else { + // If the node is one if our infix operators, then recurse into the + // operands. + if (e is T) { var (left, operator, right) = destructure(e); - if (precedence != null && operator.type.precedence != precedence) { - // Binary node, but a different precedence, so don't flatten. - visit(e); - operands.add(writer.pop()); - } else { + if (precedence == null || operator.type.precedence == precedence) { traverse(left); - - writer.space(); + space(); token(operator); - - writer.split(); + pieces.split(); traverse(right); + return; } } + + // Otherwise, just write the node itself. + visit(e); + operands.add(pieces.take()); } traverse(node); - writer.push(InfixPiece(operands)); + pieces.give(InfixPiece(operands)); } /// Creates a [ListPiece] for the given bracket-delimited set of elements. @@ -365,7 +357,7 @@ mixin PieceFactory implements CommentWriter { if (leftBracket != null) builder.leftBracket(leftBracket); elements.forEach(builder.visit); if (rightBracket != null) builder.rightBracket(rightBracket); - writer.push(builder.build()); + pieces.give(builder.build()); } /// Visits the `switch (expr)` part of a switch statement or expression. @@ -378,13 +370,13 @@ mixin PieceFactory implements CommentWriter { // Attach the `switch ` as part of the `(`. token(switchKeyword); - writer.space(); + space(); builder.leftBracket(leftParenthesis); builder.visit(value); builder.rightBracket(rightParenthesis); - writer.push(builder.build()); + pieces.give(builder.build()); } /// Creates a [ListPiece] for a type argument or type parameter list. @@ -419,15 +411,14 @@ mixin PieceFactory implements CommentWriter { /// This method assumes the code to the left of the `=` or `:` has already /// been visited. void finishAssignment(Token operator, Expression rightHandSide) { - if (operator.type == TokenType.EQ) writer.space(); + if (operator.type == TokenType.EQ) space(); token(operator); - var target = writer.pop(); - writer.split(); + var target = pieces.split(); visit(rightHandSide); - var initializer = writer.pop(); - writer.push(AssignPiece(target, initializer, + var initializer = pieces.take(); + pieces.give(AssignPiece(target, initializer, isValueDelimited: rightHandSide.isDelimited)); } @@ -435,8 +426,7 @@ mixin PieceFactory implements CommentWriter { /// subclass's initializer clause has been written. void finishForParts(ForParts forLoopParts, DelimitedListBuilder partsList) { token(forLoopParts.leftSeparator); - writer.split(); - partsList.add(writer.pop()); + partsList.add(pieces.split()); // The condition clause. if (forLoopParts.condition case var conditionExpression?) { @@ -447,21 +437,25 @@ mixin PieceFactory implements CommentWriter { } token(forLoopParts.rightSeparator); - writer.split(); - partsList.add(writer.pop()); + partsList.add(pieces.split()); // The update clauses. if (forLoopParts.updaters.isNotEmpty) { partsList.addCommentsBefore(forLoopParts.updaters.first.beginToken); createList(forLoopParts.updaters, style: const ListStyle(commas: Commas.nonTrailing)); - partsList.add(writer.pop()); + partsList.add(pieces.split()); } } /// Writes an optional modifier that precedes other code. void modifier(Token? keyword) { - token(keyword, after: writer.space); + token(keyword, after: space); + } + + /// Write a single space. + void space() { + pieces.writeSpace(); } /// Emit [token], along with any comments and formatted whitespace that comes @@ -483,7 +477,7 @@ mixin PieceFactory implements CommentWriter { /// Writes the raw [lexeme] to the current text piece. void writeLexeme(String lexeme) { // TODO(tall): Preserve selection. - writer.write(lexeme); + pieces.write(lexeme); } /// Writes a comma after [node], if there is one. diff --git a/lib/src/front_end/piece_writer.dart b/lib/src/front_end/piece_writer.dart index e4da8cd7..5f922e48 100644 --- a/lib/src/front_end/piece_writer.dart +++ b/lib/src/front_end/piece_writer.dart @@ -37,14 +37,75 @@ import 'comment_writer.dart'; /// ``` /// /// Note how the infix operator is attached to the preceding piece (which -/// happens to just be text but could be a more complex piece if the left -/// operand was a nested expression). Notice also that there is no piece for -/// the expression statement and instead, the `;` is just appended to the last -/// piece which is conceptually deeply nested inside the binary expression. +/// happens to just be an identifier but could be a more complex piece if the +/// left operand was a nested expression). Notice also that there is no piece +/// for the expression statement and, instead, the `;` is just appended to the +/// trailing TextPiece which may be deeply nested inside the binary expression. /// -/// This class implements the "slippage" between these two representations. It +/// This class implements that "slippage" between the two representations. It /// has mutable state to allow incrementally building up pieces while traversing /// the source AST nodes. +/// +/// To visit an AST node and translate it to pieces, call [token()] and +/// [visit()] to process the individual tokens and subnodes of the current +/// node. Those will ultimately bottom out on calls to [write()], which appends +/// literal text to the current [TextPiece] being written. +/// +/// Those [TextPiece]s are aggregated into a tree of composite pieces which +/// break the code into separate sections for line splitting. The main API for +/// composing those pieces is [split()], [give()], and [take()]. +/// +/// Here is a simplified example of how they work: +/// +/// ``` +/// visitIfStatement(IfStatement node) { +/// // No split() here. The caller may have code they want to prepend to the +/// // first piece in this one. +/// visit(node.condition); +/// +/// // Call split() because we may want to split between the condition and +/// // then branches and we know there will be a then branch. +/// var conditionPiece = pieces.split(); +/// +/// visit(node.thenBranch); +/// // Call take() instead of split() because there may not be an else branch. +/// // If there isn't, then the thenBranch will be the trailing piece created +/// // by this function and we want to allow the caller to append to its +/// // innermost TextPiece. +/// var thenPiece = pieces.take(); +/// +/// Piece? elsePiece; +/// if (node.elseBranch case var elseBranch?) { +/// // Call split() here because it turns out we do have something after +/// // the thenPiece and we want to be able to split between the then and +/// // else parts. +/// pieces.split(); +/// visit(elseBranch); +/// +/// // Use take() to capture the else branch while allowing the caller to +/// // append more code to it. +/// elsePiece = pieces.take(); +/// } +/// +/// // Create a new aggregate piece out of the subpieces and allow the caller +/// // to get it. +/// pieces.give(IfPiece(conditionPiece, thenPiece, elsePiece)); +/// } +/// ``` +/// +/// The basic rules are: +/// +/// - Use [split()] to insert a point where a line break can occur and +/// capture the piece for the code you've just written. You'll usually call +/// this when you have already traversed some part of an AST node and have +/// more to traverse after it. +/// +/// - Use [take()] to capture the current piece while allowing further code to +/// be appended to it. You'll usually call this to grab the last part of an +/// AST node where there is no more subsequent code. +/// +/// - Use [give()] to return the newly created aggregate piece so that the +/// caller can capture it with a later call to [split()] or [take()]. class PieceWriter { final DartFormatter _formatter; @@ -55,13 +116,9 @@ class PieceWriter { TextPiece? get currentText => _currentText; TextPiece? _currentText; - /// The most recently pushed piece, waiting to be taken by some surrounding + /// The most recently given piece, waiting to be taken by some surrounding /// piece. - /// - /// Since we traverse the AST in syntax order and pop built pieces on the way - /// back up, the "stack" of completed pieces is only ever one deep at the - /// most, so we model it with just a single field. - Piece? _pushed; + Piece? _given; /// Whether we should write a space before the next text that is written. bool _pendingSpace = false; @@ -76,55 +133,73 @@ class PieceWriter { PieceWriter(this._formatter, this._source); /// Gives the builder a newly completed [piece], to be taken by a later call - /// to [pop] from some surrounding piece. - void push(Piece piece) { - // Should never push more than one piece. - assert(_pushed == null); - - _pushed = piece; + /// to [take()] or [split()] from some surrounding piece. + void give(Piece piece) { + // Any previously given piece should already be taken (and used as a child + // of [piece]). + assert(_given == null); + _given = piece; } - /// Captures the most recently created complete [Piece]. + /// Yields the most recent piece. + /// + /// If a completed piece was added through a call to [give()], then returns + /// that piece. A specific given piece will only be returned once from either + /// a call to [take()] or [split()]. + /// + /// If there is no given piece to return, returns the most recently created + /// [TextPiece]. In this case, it still allows more text to be written to + /// that piece. For example, in: /// - /// If the most recent operation was [push], then this returns the piece given - /// by that call. Otherwise, returns the piece created by the preceding calls - /// to [write] since the last split. - Piece pop() { - if (_pushed case var piece?) { - _pushed = null; + /// ``` + /// a + b; + /// ``` + /// + /// The code for the infix expression will call [take()] to capture the second + /// `b` operand. Then the surrounding code for the expression statement will + /// call [token()] for the `;`, which will correctly append it to the + /// [TextPiece] for `b`. + Piece take() { + if (_given case var piece?) { + _given = null; return piece; } return _currentText!; } - /// Ends the current text piece and (lazily) begins a new one. + /// Takes the most recent piece and begins a new one. /// - /// The previous text piece should already be taken. - void split() { + /// Any text written after this will go into a new [TextPiece] instead of + /// being appended to the end of the taken one. Call this wherever a line + /// break may be inserted by a piece during line splitting. + Piece split() { _pendingSplit = true; - } - - /// Writes a space to the current [TextPiece]. - void space() { - _pendingSpace = true; + return take(); } /// Writes [text] raw text to the current innermost [TextPiece]. Starts a new /// one if needed. - /// - /// If [hanging] is `true`, then [text] is appended to the current line even - /// if a split is pending. This is used for writing a comment that should be - /// on the end of a line. - /// - /// If [text] internally contains a newline, then [containsNewline] should - /// be `true`. void write(String text) { _write(text); } - /// Write the contents of [comment] to the current innnermost [TextPiece], + /// Writes a space to the current [TextPiece]. + void writeSpace() { + _pendingSpace = true; + } + + /// Writes a mandatory newline from a comment to the current [TextPiece]. + void writeNewline() { + _pendingNewline = true; + } + + /// Write the contents of [comment] to the current innermost [TextPiece], /// handling any newlines that may appear in it. + /// + /// If [hanging] is `true`, then the comment is appended to the current line + /// even if call to [split()] has just happened. This is used for writing a + /// comment that should be on the end of a line. void writeComment(SourceComment comment, {bool hanging = false}) { _write(comment.text, containsNewline: comment.containsNewline, hanging: hanging); @@ -152,17 +227,12 @@ class PieceWriter { if (!hanging) _pendingSplit = false; } - /// Writes a mandatory newline from a comment to the current [TextPiece]. - void writeNewline() { - _pendingNewline = true; - } - /// Finishes writing and returns a [SourceCode] containing the final output /// and updated selection, if any. SourceCode finish() { var formatter = Solver(_formatter.pageWidth); - var piece = pop(); + var piece = take(); if (debug.tracePieceBuilder) { print(debug.pieceTree(piece)); diff --git a/lib/src/front_end/sequence_builder.dart b/lib/src/front_end/sequence_builder.dart index 01a9d7be..cdb050d0 100644 --- a/lib/src/front_end/sequence_builder.dart +++ b/lib/src/front_end/sequence_builder.dart @@ -58,8 +58,7 @@ class SequenceBuilder { addCommentsBefore(token); _visitor.visit(node); - add(_visitor.writer.pop(), indent: indent); - _visitor.writer.split(); + add(_visitor.pieces.split(), indent: indent); } /// Appends a blank line before the next piece in the sequence. @@ -96,20 +95,19 @@ class SequenceBuilder { var comment = comments[i]; if (_elements.isNotEmpty && comments.isHanging(i)) { // Attach the comment to the previous token. - _visitor.writer.space(); + _visitor.space(); - _visitor.writer.writeComment(comment, hanging: true); + _visitor.pieces.writeComment(comment, hanging: true); } else { // Write the comment as its own sequence piece. - _visitor.writer.writeComment(comment); + _visitor.pieces.writeComment(comment); if (comments.linesBefore(i) > 1) { // Always preserve a blank line above sequence-level comments. _allowBlank = true; addBlank(); } - add(_visitor.writer.pop()); - _visitor.writer.split(); + add(_visitor.pieces.split()); } }