blob: 03c80b6bf7df17806e72f76fdec4dcdb6118e12d [file] [edit]
// Copyright (c) 2025, 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.
part of '../expression.dart';
/// **INTERNAL**
///
/// Represents a control expression.
///
/// {@category controlFlow}
@internal
class ControlExpression extends Expression {
/// The control statement (e.g. `if`, `for`).
final String control;
/// Zero or more expressions that make up this control
/// expression's body.
///
/// If multiple expressions are provided, they will be
/// separated with [separator].
///
/// If [parenthesised] is `true`, the whole body will be
/// wrapped in parenthesis.
///
/// If [body] is `null` or empty, the body will be omitted.
/// If individual items are `null`, they will be omitted,
/// but separators will still be inserted.
final List<Expression?>? body;
/// Inserted between expressions in [body].
///
/// If body contains multiple items, a non-`null` separator is required.
/// An [ArgumentError] will be thrown if one is not provided.
///
/// A space (" ") will be appended to the separator if it is followed
/// by an expression. If an item in [body] is `null` (resulting in a
/// blank string), no space will be inserted before it.
final String? separator;
/// Whether or not the body should be wrapped in parenthesis (default: `true`)
final bool parenthesised;
@visibleForTesting
const ControlExpression(
this.control, {
this.body,
this.separator,
this.parenthesised = true,
});
@override
R accept<R>(covariant ControlBlockVisitor<R> visitor, [R? context]) =>
visitor.visitControlExpression(this, context);
factory ControlExpression.ifStatement(Expression condition) =>
ControlExpression('if', body: [condition]);
factory ControlExpression.elseStatement(Expression? condition) =>
ControlExpression(
'else',
body: condition != null ? [condition] : null,
parenthesised: false,
);
factory ControlExpression.forLoop(
Expression? initialize,
Expression? condition,
Expression? advance,
) => ControlExpression(
'for',
body: [initialize, condition, advance],
separator: ';',
);
factory ControlExpression.forInLoop(
Expression identifier,
Expression expression,
) => ControlExpression(
'for',
body: [identifier, expression],
separator: ' in',
);
factory ControlExpression.awaitForLoop(
Expression identifier,
Expression expression,
) => ControlExpression(
'await for',
body: [identifier, expression],
separator: ' in',
);
factory ControlExpression.whileLoop(Expression condition) =>
ControlExpression('while', body: [condition]);
static const doStatement = ControlExpression('do');
static const tryStatement = ControlExpression('try');
factory ControlExpression.catchStatement(
String error, [
String? stacktrace,
]) => ControlExpression(
'catch',
body: [refer(error), if (stacktrace != null) refer(stacktrace)],
separator: ',',
);
factory ControlExpression.onStatement(
Reference type, [
ControlExpression? statement,
]) => ControlExpression(
'on',
body: [type, if (statement != null) statement],
parenthesised: false,
separator: '',
);
static const finallyStatement = ControlExpression('finally');
factory ControlExpression.switchStatement(Expression value) =>
ControlExpression('switch', body: [value]);
}
/// **INTERNAL**
///
/// A collection control-flow expression
///
/// Supports chaining when used in collections via [chainTarget] and [chain].
/// These fields have no effect when used outside of collections.
///
@internal
class CollectionExpression extends BinaryExpression {
/// Whether the [CollectionExpression] that follows this in a collection
/// may chain with it. Chained expressions will not have a comma between
/// them.
final bool chainTarget;
/// Whether this [CollectionExpression] should try to chain with its
/// antecedent in collections.
final bool chain;
const CollectionExpression._({
required Expression control,
required Expression value,
this.chain = false,
this.chainTarget = false,
}) : super._(control, value, '');
}
/// Provides control-flow utilities for [Expression].
///
/// {@category controlFlow}
extension ControlFlow on Expression {
/// Returns `yield {this}`
Expression get yielded =>
BinaryExpression._(Expression._empty, this, 'yield');
/// Returns `yield* {this}`
Expression get yieldStarred =>
BinaryExpression._(Expression._empty, this, 'yield*');
/// Build a `while` loop from this expression.
///
/// ```dart
/// while (this) {
/// body
/// }
/// ```
WhileLoop loopWhile(void Function(BlockBuilder block) builder) => WhileLoop(
(loop) =>
loop
..condition = this
..body.update(builder),
);
/// Build a `do-while` loop from this expression.
///
/// ```dart
/// do {
/// body
/// } while (this);
/// ```
WhileLoop loopDoWhile(void Function(BlockBuilder block) builder) => WhileLoop(
(loop) =>
loop
..doWhile = true
..condition = this
..body.update(builder),
);
/// Build a `for-in` loop from this expression in [object].
///
/// ```dart
/// for (this in object) {
/// body
/// }
/// ```
ForInLoop loopForIn(
Expression object,
void Function(BlockBuilder block) builder,
) => ForInLoop(
(loop) =>
loop
..object = object
..variable = this
..body.update(builder),
);
/// Build this expression into a [Conditional]
///
/// ```dart
/// if (this) {
/// body
/// }
/// ```
///
/// Chain [Conditional.elseIf] and [Conditional.orElse] to
/// easily create a full `if-elseif-else` tree:
///
/// ```dart
/// literal(1)
/// .equalTo(literal(2))
/// .ifThen(
/// (body) => body.addExpression(
/// refer('print').call([literal('Bad')]))
/// ).elseIf(
/// (block) => block
/// ..condition = literal(2).equalTo(literal(2))
/// ..body.addExpression(refer('print').call([literal('Good')])),
/// ).orElse(
/// (body) => body.addExpression(
/// refer('print').call([literal('What?')])),
/// );
/// ```
///
/// Outputs:
/// ```dart
/// if (1 == 2) {
/// print('Bad');
/// } else if (2 == 2) {
/// print('Good');
/// } else {
/// print('What?');
/// }
/// ```
Conditional ifThen(void Function(BlockBuilder body) builder) => Conditional(
(tree) =>
tree..add(
(branch) =>
branch
..condition = this
..body.update(builder),
),
);
/// Returns `if (this) return value`
Expression ifThenReturn([Expression? value]) => BinaryExpression._(
ControlExpression.ifStatement(this),
value == null ? ControlFlow.returnVoid : value.returned,
'',
);
/// `return`
static const returnVoid = LiteralExpression._('return');
/// `break`
///
/// For usage with a label, use [breakLabel].
static const breakVoid = LiteralExpression._('break');
/// `continue`
///
/// For usage with a label, use [continueLabel].
static const continueVoid = LiteralExpression._('continue');
/// `rethrow`
static const rethrowVoid = LiteralExpression._('rethrow');
/// Returns a labeled `break` statement.
///
/// ```dart
/// break label
/// ```
///
/// For usage without a label, use [breakVoid].
static Expression breakLabel(String label) =>
BinaryExpression._(breakVoid, refer(label), '');
/// Returns a labeled `continue` statement.
///
/// ```dart
/// continue label
/// ```
///
/// For usage without a label, use [continueVoid].
static Expression continueLabel(String label) =>
BinaryExpression._(continueVoid, refer(label), '');
/// Returns a case match expression for an `if-case` statement,
/// matching [object] against [pattern] with optional guard clause [guard].
///
/// ```dart
/// object case pattern
/// object case pattern when guard
/// ```
///
/// See https://dart.dev/language/branches#if-case
static Expression ifCase({
required Expression object,
required Expression pattern,
Expression? guard,
}) {
final first = BinaryExpression._(object, pattern, 'case');
if (guard == null) return first;
return BinaryExpression._(first, guard, 'when');
}
/// Returns a collection-`if` expression.
///
/// ```dart
/// if (condition) value
/// ```
///
/// {@template controlflow.collection.chaining.if}
/// [collectionIf] expressions followed by [collectionElse] expressions
/// in a [literal] list, set, or map will be chained together, e.g:
///
/// ```dart
/// literalMap({
/// ControlFlow.collectionIf(
/// condition: literalTrue, value: refer('key')
/// ): refer('value'),
/// ControlFlow.collectionElse(
/// condition: literalFalse,
/// value: refer('key2')
/// ): refer('value2'),
/// ControlFlow.collectionElse(value: refer('key3')
/// ): refer('value3'),
/// });
/// ```
/// Outputs:
/// ```dart
/// {
/// if (true) key: value
/// else if (false) key2: value2
/// else key3: value3
/// }
/// ```
/// {@endtemplate}
static Expression collectionIf({
required Expression condition,
required Expression value,
}) => CollectionExpression._(
chainTarget: true,
control: ControlExpression.ifStatement(condition),
value: value,
);
/// Returns a collection-`else` expression.
///
/// If [condition] is specified, returns a collection-`else if` expression.
///
/// ```dart
/// else value
/// else if (condition) value
/// ```
///
/// {@macro controlflow.collection.chaining.if}
static Expression collectionElse({
Expression? condition,
required Expression value,
}) => CollectionExpression._(
chain: true,
chainTarget: condition != null,
// only chainable if this is an else-if statement
control: ControlExpression.elseStatement(
condition == null ? null : ControlExpression.ifStatement(condition),
),
value: value,
);
/// Returns a collection-`for` expression.
///
/// ```dart
/// for (initialize; condition; advance) value
/// ```
///
/// {@template controlflow.collection.chaining.for}
/// If [value] is chainable (a [collectionIf], [collectionElse] with a
/// condition, or a collection-`for`/`for-in` containing one of those),
/// this will also be chainable. If this is followed by a collection-`else`
/// in a [literal] collection, they will be chained together.
///
/// ```dart
/// literalMap({
/// ControlFlow.collectionForIn(
/// identifier: declareFinal('x'),
/// expression: refer('items'),
/// value: ControlFlow.collectionIf(
/// condition: refer('x').property('valid'),
/// value: refer('key')
/// )): refer('x'),
/// ControlFlow.collectionElse(value: refer('key2')
/// ): refer('fix').call([refer('x')])
/// });
/// ```
/// Outputs:
/// ```dart
/// {
/// for (final x in items) if (x.valid) key: x
/// else key2: fix
/// }
/// ```
/// {@endtemplate}
static Expression collectionFor({
Expression? initialize,
Expression? condition,
Expression? advance,
required Expression value,
}) => CollectionExpression._(
chainTarget: true,
control: ControlExpression.forLoop(initialize, condition, advance),
value: value,
);
/// Returns a collection-`for-in` expression
///
/// ```dart
/// for (identifier in expression) value
/// ```
///
/// {@macro controlflow.collection.chaining.for}
static Expression collectionForIn({
required Expression identifier,
required Expression expression,
required Expression value,
}) => CollectionExpression._(
chainTarget: value is CollectionExpression && value.chainTarget,
control: ControlExpression.forInLoop(identifier, expression),
value: value,
);
}