visitor

A library that generates code matching the visitor pattern based on annotations made on classes and typedefs.

For example, the following definition in a func.dart file

import 'package:visitor/visitor.dart';

part 'func.g.dart';

@Visitor(name: "Func")
class _Func<R> {}

@VisitorBranch(of: _Func)
typedef _Func$Supplier<T, R> = T Function();

@VisitorBranch(of: _Func)
typedef _Func$UnaryOperator<T, R> = T Function(R x);

@VisitorBranch(of: _Func)
typedef _Func$BinaryOperator<T, R> = T Function(R x, R y);

generates the following code in a func.g.dart file:

abstract class Func<R> {
  const factory Func.supplier() = _OnSupplierFunc;

  const factory Func.unaryOperator(R x) = _OnUnaryOperatorFunc;

  const factory Func.binaryOperator(R x, R y) = _OnBinaryOperatorFunc;

  const Func._();

  T choose<T>({
    required _Func$Supplier<T, R> onSupplier,
    required _Func$UnaryOperator<T, R> onUnaryOperator,
    required _Func$BinaryOperator<T, R> onBinaryOperator,
  });
}

class _OnSupplierFunc<R> extends Func<R> {
  const _OnSupplierFunc() : super._();

  @override
  T choose<T>({
    required _Func$Supplier<T, R> onSupplier,
    required _Func$UnaryOperator<T, R> onUnaryOperator,
    required _Func$BinaryOperator<T, R> onBinaryOperator,
  }) {
    return onSupplier();
  }
}

class _OnUnaryOperatorFunc<R> extends Func<R> {
  final R x;

  const _OnUnaryOperatorFunc(this.x) : super._();

  @override
  T choose<T>({
    required _Func$Supplier<T, R> onSupplier,
    required _Func$UnaryOperator<T, R> onUnaryOperator,
    required _Func$BinaryOperator<T, R> onBinaryOperator,
  }) {
    return onUnaryOperator(x);
  }
}

class _OnBinaryOperatorFunc<R> extends Func<R> {
  final R x;
  final R y;

  const _OnBinaryOperatorFunc(this.x, this.y) : super._();

  @override
  T choose<T>({
    required _Func$Supplier<T, R> onSupplier,
    required _Func$UnaryOperator<T, R> onUnaryOperator,
    required _Func$BinaryOperator<T, R> onBinaryOperator,
  }) {
    return onBinaryOperator(x, y);
  }
}

You can now use the generated code:

int apply(Func<int> func) {
  final int result = func.choose<int>(
    onSupplier: () => 1,
    onUnaryOperator: (x) => 2 * x,
    onBinaryOperator: (x, y) => x + y,
  );
  return result;
}

void main() {
  Func<int> f;

  f = Func.supplier();
  print(apply(f));
  // reaches `onSupplier: () => 1`, outputs 1 
  
  f = Func.unaryOperator(2);
  print(apply(f)); 
  // reaches `onUnaryOperator: (x) => 2 * x`, outputs 4

  f = Func.binaryOperator(3, 5);
  print(apply(f));
  // reaches `onBinaryOperator: (x, y) => x + y`, outputs 8
}

Alternatively, you can think of it as a similar concept of Kotlin's sealed classes.

Features

  • Handles type parameters (as seen in the example above).
  • Handles recursive parameter types.
  • Handles named and optional parameters.

Getting started

Add this dependency to your pubspec.yaml as a dev dependency.

Usage

This library exports two annotations: Visitor and VisitorBranch.

The Visitor annotation is meant to be used in classes. The class name must be prefixed with one or more underscores (e.g. _Example).

@Visitor()
class _Example {}

The VisitorBranch annotation is meant to be used in type definitions (typedefs). It receives one required named parameter of, which should be its Visitor type.

@VisitorBranch(of: _Example)

Each typedef annotated with VisitorBranch will become a branch of its respective visitor, and

  • must alias a function with zero or more parameters;
  • must have a parameter type T, which the aliased function must return;
  • can have any name, but as a convention, it should start with _$ to avoid external referencing. If more than one visitor is being declared in a single file, to avoid name clashing, its name should be _ + the camel-cased name of its visitor + $ + the camel-cased name of the branch.
@VisitorBranch(of: _Example)
typedef _$FirstBranch<T> = T Function(int);

Below the imports of the current file, add a part statement. E.g. if the name of the file is file.dart:

part 'file.g.dart';

After adding the Visitor, its VisitorBranches and the part statement, run the following command in the root directory of your project:

dart run build_runner build

# Alternatively, if you're using Flutter
flutter pub run build_runner build

The generated code will be added in the file described in the part statement. The following changes will be applied:

  • the name of the generated visitor will be the class name without the first underscore (e.g. Example).
  • the names of the generated factory methods in the visitor will be each branch name as camel-case (e.g. firstBranch).
  • the names of the generated subclasses of the visitor will be each branch name as Pascal-case, prefixed with _On and suffixed with the Pascal-cased visitor name (e.g. _OnFirstBranchExample).
  • the names of the callbacks of the visitor will be each branch name as Pascal-case, prefixed with on (e.g. onFirstBranch).
abstract class Example implements _Example {
  const factory Example.firstBranch(int p0) = _OnFirstBranchExample;

  const Example._();

  T choose<T>({
    required T Function(int) onFirstBranch,
  });
}

class _OnFirstBranchExample extends Example {
  final int p0;
  
  const _OnFirstBranchExample(int p0) : super._();
  
  T choose<T>({
    required T Function(int) onFirstBranch,
  }) {
    return onFirstBranch(p0);
  }
}

On typedefs annotated with VisitorBranch, the linter may produce an unused element warning. To avoid that, you can add a

// ignore_for_file: unused_element

to the top of the file.

Examples

You can see the usage in the example/lib/visitor_example.dart file.

In the example/lib/examples,

  • the expression.dart shows usage on recursive parameter types;
  • the diff_change.dart shows usage on named parameters;
  • the loading_state.dart shows usage on type parameters.

Libraries

visitor