Skip to content

Commit

Permalink
Format configurations in imports and exports. (#1265)
Browse files Browse the repository at this point in the history
Format configurations in imports and exports.

This also brings in InfixPiece which will be used for other infix
operators and infix-like constructs.
  • Loading branch information
munificent authored Sep 14, 2023
1 parent 473a21d commit bcdafc0
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 13 deletions.
22 changes: 14 additions & 8 deletions lib/src/front_end/ast_node_visitor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,19 @@ class AstNodeVisitor extends ThrowingAstVisitor<void> with PieceFactory {

@override
void visitConfiguration(Configuration node) {
throw UnimplementedError();
token(node.ifKeyword);
writer.space();
token(node.leftParenthesis);

if (node.equalToken case var equals?) {
createInfix(node.name, equals, node.value!, hanging: true);
} else {
visit(node.name);
}

token(node.rightParenthesis);
writer.space();
visit(node.uri);
}

@override
Expand Down Expand Up @@ -238,7 +250,7 @@ class AstNodeVisitor extends ThrowingAstVisitor<void> with PieceFactory {

@override
void visitDottedName(DottedName node) {
throw UnimplementedError();
createDotted(node.components);
}

@override
Expand Down Expand Up @@ -268,9 +280,6 @@ class AstNodeVisitor extends ThrowingAstVisitor<void> with PieceFactory {

@override
void visitExportDirective(ExportDirective node) {
// TODO(tall): Format configurations.
if (node.configurations.isNotEmpty) throw UnimplementedError();

createImport(node, node.exportKeyword);
}

Expand Down Expand Up @@ -416,9 +425,6 @@ class AstNodeVisitor extends ThrowingAstVisitor<void> with PieceFactory {

@override
void visitImportDirective(ImportDirective node) {
// TODO(tall): Format configurations.
if (node.configurations.isNotEmpty) throw UnimplementedError();

createImport(node, node.importKeyword,
deferredKeyword: node.deferredKeyword,
asKeyword: node.asKeyword,
Expand Down
42 changes: 41 additions & 1 deletion lib/src/front_end/piece_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';

import '../piece/import.dart';
import '../piece/infix.dart';
import '../piece/piece.dart';
import '../piece/postfix.dart';
import '../piece/sequence.dart';
Expand Down Expand Up @@ -72,6 +73,18 @@ mixin PieceFactory {
visit(directive.uri);
var directivePiece = writer.pop();

Piece? configurationsPiece;
if (directive.configurations.isNotEmpty) {
var configurations = <Piece>[];
for (var configuration in directive.configurations) {
writer.split();
visit(configuration);
configurations.add(writer.pop());
}

configurationsPiece = PostfixPiece(configurations);
}

Piece? asClause;
if (asKeyword != null) {
writer.split();
Expand Down Expand Up @@ -112,7 +125,34 @@ mixin PieceFactory {

token(directive.semicolon);

writer.push(ImportPiece(directivePiece, asClause, combinator));
writer.push(
ImportPiece(directivePiece, configurationsPiece, asClause, combinator));
}

/// Creates a single infix operation.
///
/// If [hanging] is `true` then the operator goes at the end of the first
/// line, like `+`. Otherwise, it goes at the beginning of the second, like
/// `as`.
void createInfix(AstNode left, Token operator, AstNode right,
{bool hanging = false}) {
var operands = <Piece>[];
visit(left);
operands.add(writer.pop());

if (hanging) {
writer.space();
token(operator);
writer.split();
} else {
writer.split();
token(operator);
writer.space();
}

visit(right);
operands.add(writer.pop());
writer.push(InfixPiece(operands));
}

/// Emit [token], along with any comments and formatted whitespace that comes
Expand Down
10 changes: 8 additions & 2 deletions lib/src/piece/import.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ import 'piece.dart';
/// imports, the configurations (`if` clauses), and combinators (`show` and
/// `hide`).
class ImportPiece extends Piece {
/// The directive keyword and its URI.
/// The main directive and its URI.
final Piece directive;

/// If the directive has `if` configurations, this is them.
final Piece? configurations;

/// The `as` clause for this directive.
///
/// Null if this is not an import or it has no library prefix.
Expand All @@ -23,21 +26,24 @@ class ImportPiece extends Piece {
/// The piece for the `show` and/or `hide` combinators.
final Piece? combinator;

ImportPiece(this.directive, this.asClause, this.combinator);
ImportPiece(
this.directive, this.configurations, this.asClause, this.combinator);

@override
int get stateCount => 1;

@override
void format(CodeWriter writer, int state) {
writer.format(directive);
writer.formatOptional(configurations);
writer.formatOptional(asClause);
writer.formatOptional(combinator);
}

@override
void forEachChild(void Function(Piece piece) callback) {
callback(directive);
if (configurations case var configurations?) callback(configurations);
if (asClause case var asClause?) callback(asClause);
if (combinator case var combinator?) callback(combinator);
}
Expand Down
54 changes: 54 additions & 0 deletions lib/src/piece/infix.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) 2023, 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 '../back_end/code_writer.dart';
import '../constants.dart';
import 'piece.dart';

/// A piece for a series of binary expressions at the same precedence, like:
///
/// ```
/// a + b + c
/// ```
class InfixPiece extends Piece {
/// The series of operands.
///
/// Since we don't split on both sides of the operator, the operators will be
/// embedded in the operand pieces. If the operator is a hanging one, it will
/// be in the preceding operand, so `1 + 2` becomes "Infix(`1 +`, `2`)".
/// A leading operator like `foo as int` becomes "Infix(`foo`, `as int`)".
final List<Piece> operands;

InfixPiece(this.operands);

@override
int get stateCount => 2;

@override
void format(CodeWriter writer, int state) {
switch (state) {
case 0:
writer.setAllowNewlines(false);
for (var i = 0; i < operands.length; i++) {
writer.format(operands[i]);

if (i < operands.length - 1) writer.space();
}

case 1:
writer.setNesting(Indent.expression);
for (var i = 0; i < operands.length; i++) {
writer.format(operands[i]);
if (i < operands.length - 1) writer.newline();
}
}
}

@override
void forEachChild(void Function(Piece piece) callback) {
operands.forEach(callback);
}

@override
String toString() => 'Infix';
}
7 changes: 6 additions & 1 deletion test/top_level/export.unit
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ export 'package:some/very/long/export/path.dart';
### here to make sure that export directives handle the combinators.
export 'a.dart'show Ape,Bear hide Cat;
<<<
export 'a.dart' show Ape, Bear hide Cat;
export 'a.dart' show Ape, Bear hide Cat;
>>> Configuration.
### More detailed configuration tests are handled under import.
export'a'if(b . c=='d' )'e';
<<<
export 'a' if (b.c == 'd') 'e';
36 changes: 35 additions & 1 deletion test/top_level/import.unit
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,38 @@ import 'package:foo/foo.dart'
import 'package:foo/some/path/foo.dart' deferred as very_long_identifier_path;
<<<
import 'package:foo/some/path/foo.dart'
deferred as very_long_identifier_path;
deferred as very_long_identifier_path;
>>> Dotted identifier in configuration.
import'a'if(b . c . d)'e';
<<<
import 'a' if (b.c.d) 'e';
>>> Multiple configurations on one line.
import 'a' if (b) 'b' if (c) 'c';
<<<
import 'a' if (b) 'b' if (c) 'c';
>>> If configurations don't fit, they all split.
import 'long/import/url.dart' if (b) 'b' if (c) 'c';
<<<
import 'long/import/url.dart'
if (b) 'b'
if (c) 'c';
>>> Configurations don't split before URI.
import 'long/import/url.dart' if (config) 'very/long/configured/import/url.dart';
<<<
import 'long/import/url.dart'
if (config) 'very/long/configured/import/url.dart';
>>> Unsplit configuration with `==`.
import 'a.dart' if (b == 's') 'c';
<<<
import 'a.dart' if (b == 's') 'c';
>>> Split before `if` before `==`.
import 'some/uri.dart' if (debug == 'string') 'c';
<<<
import 'some/uri.dart'
if (debug == 'string') 'c';
>>> Split before `==` in configuration.
import 'some/uri.dart' if (config.name.debug == 'string') 'c';
<<<
import 'some/uri.dart'
if (config.name.debug ==
'string') 'c';

0 comments on commit bcdafc0

Please sign in to comment.