Overview
This article explains how to perform static code generation in the Dart language using build_runner
based on the build
package, mixing concepts and examples. The main topics covered are as follows:
- build
, build_config
, build_runner
, source_gen
In other words, our aim is to investigate how packages like freezed
or flutter_gen
were developed.
Examples used in this article can be viewed at the Github Repository.
Code Generation?
When a package provides a script, there can generally be two cases.
- Generation is performed using
dart run build_runner build
. - Generation is performed using
dart run <package>:<command?>
.
The former category includes packages like freezed
, while the latter includes packages like flutter_native_splash
.
In the case of the latter, it’s possible to create the package without much prior knowledge (though this doesn’t necessarily mean it’s easy).
If you look at the pubspec.yaml
of the flutter_native_splash
package, you won't find packages like build
, build_runner
, or source_gen
included.
Instead, they only provide Dart files such as create.dart
and remove.dart
in the project's bin
directory, which can be directly executed by the Dart compiler.
However, when examining the pubspec.yaml
of the freezed
package, you can see that it is replete with packages like analyzer
, build
, build_config
, source_gen
, build_test
, and build_runner
in both the dependencies
and dev_dependencies
. These are the topics we aim to cover in this article.
Why we should use build package?
In cases where you want to create a splash screen or a launcher icon in Flutter, structuring the package to include Dart scripts in the bin
directory and functioning like a CLI can be a proper option.
However, the build
package is needed in the following situations:
- When you need to incrementally access the source code and make mutations continuously.
- When you have to directly access Dart’s
AST
(Abstract Syntax Tree, Syntactic) andElement
(Semantic Tree) to perform Code Generation based on Dart syntax.
Additionally, if you want to insert any steps that act like interceptors or hooks into Dart build systems, you will need the build
package.
Package we should know
When you visit GitHub, you can find various packages along with brief descriptions of them.
Generally, to perform Code Generation, you should be familiar with the following:
build
,build_config
,build_runner
,build_test
,analyzer
There’s one more essential package to add to this list.
source_gen is a utility created using tools like build
or analyzer
. It provides a more developer-friendly Code Generation API by predefining and supplying other types of Builder
s (PartBuilder
or LibraryBuilder
) for the Builder.
source_gen
is a utility created using other packages like build
or analyzer
. It predefines different types of Builder
s (like PartBuilder
or LibraryBuilder
) and offers a more developer-friendly Code Generation API.
How to read this article?
Rather than being entirely theory-centric or entirely example-centric, I aim to proceed by appropriately mixing examples and theory depending on the situation.
Also, for well-maded examples, I always refer to json_serializable
and freezed
will attach links accordingly.
Let's get started.
Example 1 — CopyBuilder
First, let’s create an example where for all .txt
files in the lib
folder, a copied text with the content gets created next to it with the filename ending in .copy.txt
.
1. Create Dart CLI Project
Create Dart CLI project code_generation
,(name can differ.).
Add build_config
and build_runner
into dev_dependencies
of pubspec.yaml
.
Add build
into dependencies
of pubspec.yaml
.
name: code_generation
description: A sample command-line application.
version: 1.0.0
# repository: https://github.com/my_org/my_repo
environment:
sdk: ^3.1.3
# Add regular dependencies here.
dependencies:
build: ^2.4.1
# path: ^1.8.0
dev_dependencies:
build_config: ^1.1.1
build_runner: ^2.4.6
lints: ^2.1.1
test: ^1.24.8
2. Configure
build.yaml
The build.yaml
is a configuration file that the build_config
library parses and uses to customize the build process. Detailed information related to this can be found in the Docs, but you might not want to read it right away.
Therefore, you can read the above-mentioned document later. For now, let’s prepare a build.yaml
file in the project root.
I've left brief explanations as comments.
Let’s assume the package has the name code_generation
.
targets: # Target group that builders are applied
$default: # At least one target should use the package name,
# and in lieu of that, you can define the default behavior using $default.
builders: # Declare the builders to be applied to the current target (builders defined below).
code_generation|copyBuilder: # Set as $packageName|$builderName
generate_for: # Files to which the builder will be applied, using a glob pattern.
- lib/*
enabled: True # Whether the builder will be applied or not. If not defined, it follows the auto_apply attribute of the builder.
# options: declare BuilderOptions
builders: # Declare Builder
copyBuilder: # The name of builder is `copyBuilder`.
import: 'package:code_generation/code_generation.dart' # The URI that needs to be imported for the builder to run.
builder_factories: [ 'copyBuilder' ] # Top-level Builder creation functions that can be brought in from the import.
build_extensions: # List of extensions for the inputs related to the outputs produced by the Builder.
.txt:
- .copy.txt
build_to: source # source (creates output right next to input) | cache (cache directory)
auto_apply: dependents # none | dependents (applied to direct dependency) | all_packages | root_package
Create an entry file inside the
lib
directory
Since the package name is code_generation
, create code_generation.dart
directly under the lib
directory.
// code_generation.dart
import 'package:build/build.dart';
import 'copy_builder.dart';
Builder copyBuilder(BuilderOptions options) => CopyBuilder();
As you can see, the name copyBuilder
should be the same as the name provided to builder_factories
in the build.yaml
.
To avoid confusion, make sure that the name of the builder in the builders
defined in the yaml file and the name entering builder_factories
are exactly the same.
The function to be included here should be defined to match Builder <builder name>(BuilderOptions options)
.
4.
CopyBuilder
// copy_builder.dart
import 'dart:async';
import 'package:build/build.dart';
class CopyBuilder implements Builder {
@override
FutureOr<void> build(BuildStep buildStep) async {
AssetId inputId = buildStep.inputId;
var copyAssetId = inputId.changeExtension('.copy.txt');
var contents = await buildStep.readAsString(inputId);
await buildStep.writeAsString(copyAssetId, contents);
}
@override
Map<String, List<String>> get buildExtensions => {
'.txt': ['.copy.txt'],
};
}
Create a custom builder class that implements the Builder
.
buildExtensions
plays the same role as build_extensions
defined in build.yaml
, and the two should be identical.
Since we decided to convert .txt
to .copy.txt
, we write it as above.
Looking at the build
function, AssetId
represents a single file, and it's an object containing details like the file's extension, path, and content.
Thus, we just need to read and write this file. The names of the functions in the code are intuitive, so I’ll skip the explanations.
5. Generate code using
build_runner
After creating any .txt
file in the lib
, let's run dart run build_runner build
.
You can confirm that it has been successfully copied as .copy.txt
.
Example 2 — Get options from user
Let’s try to create it like a package that actual users can use.
The things to do in this example are as follows.
- Retrieve the set options and modify the Generation logic.
1. Set default option values in the
build.yaml
file (Optional).
targets:
$default:
builders:
code_generation|copyBuilder:
generate_for:
- lib/*
enabled: True
options:
name: MJ
The options defined in targets - builders
in build.yaml
are included in BuilderOptions.config
.
Pass the options to CopyBuilder
as follows.
Builder copyBuilder(BuilderOptions options) => CopyBuilder(options);
Let’s modify the code of CopyBuilder
as follows.
class CopyBuilder implements Builder {
const CopyBuilder(this.options);
final BuilderOptions options;
@override
FutureOr<void> build(BuildStep buildStep) async {
AssetId inputId = buildStep.inputId;
var copyAssetId = inputId.changeExtension('.copy.txt');
var contents = await buildStep.readAsString(inputId);
contents = '''
name: ${options.config['name'] ?? 'unknown'}
---------
$contents
''';
await buildStep.writeAsString(copyAssetId, contents);
}
@override
Map<String, List<String>> get buildExtensions => {
'.txt': ['.copy.txt'],
};
}
And if you run dart run build_runner build
, you can see that the name
is correctly entered.
If we are in the position of developing the package and there are users using it, we can request them to provide options
as follows.
Appendix 1 — Get global customization build options from user
If you look at the documentation of freezed
or the Configuration file of flutter_gen
, you'll find that when we distribute the Code Generator in the form of a package, we offer the feature allowing users to customize the operation of the Generator using their own options.
This can be grouped by two categories:
- Customizing by inserting arguments inside annotations at the code level.
- Inducing to set default values for the package globally in
build.yaml
orpubspec.yaml
.
If we focus on the latter, it might be confusing as to why these two methods are different.
Defining in build.yaml
is the standard method, while the latter approach provides convenience to users by directly reading the configuration file like pubspec.yaml
.
The method of defining in build.yaml
can be verified in YouTube tutorials. This method is convenient from the perspective of package development because you can receive and use the settings through the BuilderOptions
object directly.
By looking at the internal code of flutter_gen
's Builder, you can see that it reads pubspec.yaml
directly through File IO and simply parses it into a Map.
analyzer Package
The analyzer
is one of the most complex subjects to grasp when trying to understand Dart's code generation.
Strictly speaking, the analyzer
is a package that performs static analysis on Dart code, converting it into an AST (Abstract Syntax Tree, Syntactic Model) or an Element Model (Semantic Model). Tools like dart format
or dart doc
internally utilize this package for their operations.
Therefore, when we define a Builder in places like build
and want to analyze Dart code to perform Code Generation according to our desired logic, using the analyzer
becomes indispensable.
However, it’s also true that by using the source_gen
package, which we will explore later, one can achieve code generation in a more developer-friendly manner without delving deep into the somewhat verbose APIs of the analyzer
package.
It’s alright if you only skim through the analyzer
package section. It's not essential to delve deep into it.
After extensive searching on Google, I was able to find dart-lang/sdk/analyzer/doc/tutorial
, where I could study the concepts.
If you're not familiar with topics like compilers and AST (Abstract Syntax Tree), I recommend starting by learning how static code analysis works.
Mental Model of analyzer Package
The tutorial “Performing Analysis” describes the general mental model of the analyzer.
The main point is represented as output = analyze(input, context)
. While one might typically expect a consistent output when a same source file is given as input, the output can actually change based on external factors, such as the existence of an analysis options file or the presence of other files.
Therefore, a crucial consideration in the analyze
API is handling this context effectively to ensure consistent outputs.
This is especially important in incremental analysis environments, such as Dart’s analysis server.
If you don’t have an incremental analysis environment, there’s no need to think too deeply about the context.
The API is as follows:
The AnalysisContextCollection
is a collection of paths for the files to be analyzed.
By inputting a path into the AnalysisContextCollection
, you can obtain an AnalysisContext
, and through this, you can get an AnalysisSession
.
This session contains the context
mentioned above. If inconsistent results are returned from the same session, an exception is thrown to ensure incremental analysis.
Handling this session is continued in the AST or Element API.
AST(Abstract Syntax Tree) Model
The Abstract Syntax Tree (AST) in programming is an API that deals with the AST itself, structured as a tree of the AstNode
class.
All subclasses of AstNode
can be found in the Docs.
Simply put, the AST represents a programming language converted into a syntactic tree form.
For example, the children of a single Dart file might include import
statements, class
declarations, etc. And the children of a class
declaration might consist of class methods and fields.
The root of the tree is the CompilationUnit
, which represents a unit for compilation.
All nodes in the AST have getters for adjacent parents and children in the tree. For instance, a BinaryExpression
has a parent
, leftOperand
, and rightOperand
.
The AST can be in a resolved or unresolved state. In its resolved state, the resolution information it carries signifies the Element
and Type information combined with the AST.
Using the AnalysisSession
, you can obtain the root of the AST, the CompilationUnit
, and traverse the tree to get the information you need.
// don't require Resolution Information
Future<void> processFile(AnalysisSession session, String path) async {
var result = session.getParsedUnit(path);
if (result is ParsedUnitResult) {
CompilationUnit unit = result.unit;
}
}
// With Resolution Information
Future<void> processFile(AnalysisSession session, String path) async {
var result = await session.getResolvedUnit(path);
if (result is ResolvedUnitResult) {
CompilationUnit unit = result.unit;
}
}
Traversing the tree can be done either by manually using the aforementioned getters, checking types, and implementing by hand, or by utilizing the Visitor Pattern.
There are several Visitor classes, which are subclasses that implement the AstVisitor
.
For details on all the Visitors, one should refer to the documentation.
The commonly used ones are SimpleAstVisitor
, RecursiveAstVisitor
, and GeneralizingAstVisitor
.
SimpleAstVisitor
: Implements every visit method by doing nothing.RecursiveAstVisitor
: Causes every node in a structure to be visited.GeneralizingAstVisitor
: Makes it easy to visit general kinds of nodes, such as visiting any statement or any expression.
Typically, you can start your Visitor by subclassing one of these.
Element Model
The Type Model and its counterpart, the Element Model, are semantic representations of Dart code. What does it mean to be semantic?
It can be seen as a representation that deals more directly with the elements managed by the language itself, such as Classes, Methods, Constructors, Interfaces, etc.
It’s essential not to think of the semantic Element Model as a higher level than the syntactic AST Model just because it’s more developer-friendly.
In packages like source_gen
, while you will encounter the AST, it's more common to interact with the Element.
For instance, if you look at the code that implements GeneratorForAnnotation<T>
in json_serializable
supported by source_gen
, you'll see the generateForAnnotatedElement
function.
The first argument provided to this function is an Element, which comes from the analyzer
.
Using conditionals like element is EnumElement
, you can easily determine the type of the Element and make use of developer-friendly APIs.
The root of the Element tree is typically the LibraryElement
, which represents a library in Dart.
Since a library
in Dart can have multiple part
s, it can have one or more CompilationUnitElement
children.
Just like with the AST, if you have an AnalysisSession
, you can retrieve an Element as follows.
void analyzeSingleFile(AnalysisSession session, String path) async {
var result = await session.getUnitElement(path);
if (result is UnitElementResult) {
CompilationUnitElement element = result.element;
}
}
In the Element Model, tree traversing is not different from the AST. Just as there is ASTVisitor
in AST, there’s ElementVisitor
in the Element
Model, and there are classes with the similar prefix.
The full list of Visitor
s can be found in the Docs.
Type Model
In the AST/Element Model, if you want to check the return type of a function, you can use the Type Model.
All of them are subclasses of DartType
.
You can obtain this DartType
in both the AST and Element
Model.
In the AST, it should have Resolution Information, and you can access it through the non-null type like staticType
.
In the Element Model, the usage is more straightforward, and for example, if it’s a function, properties like returnType
all become DartType
.
analyzer
package outro
After all the explanations, the key takeaway is that by having a basic understanding of the syntax of AST, Element, and Type, you should be able to read, search, and use them effectively.
source_gen
will become your friend with just this level of understanding.
If you’re using source_gen
, you might not even need to access the AnalysisSession
directly.
Additionally, since you’re analyzing the same code, it’s note to worth that AST and Element can be transformed into each other, representing the same information.
source_gen Package
You can see from the source_gen
API docs that there isn’t much you need to know.
Our main goal was to implement the Builder
interface in the build
package.
source_gen
provides typical Builder
s and Generator
s to make it easier to implement such builders.
Builder Types
Typically, using the part
and part of
statements in Dart allows you to split code within the same library into different files.
In the context of Code Generation, this concept is actively used to generate files like a.g.dart
or a.xxx.dart
based on an existing a.dart
.
If you want to create files with the .g.dart
extension, you can use SharedPartBuilder
. As the name suggests, it's a PartBuilder
, but multiple SharedPartBuilder
s can collectively store their logic in a shared file named .g.dart
. This way, code generated by different builders from various packages can be shared in the same .g.dart
file.
On the other hand, LibraryBuilder
is a builder that simply creates a single file. You can import and use it directly in your code.
build.yaml Settings
Builder for using SharedPartBuilder
, you have to config build_to: cache
, and apply combining_builder
.
When configuring the build.yaml
for build_config
support, there are several things to keep in mind:
Builders that use SharedPartBuilder
should specify build_to: cache
and apply the combining_builder
. This ensures that generated files with .g.dart
extensions are appropriately handled.
Since there’s no guarantee that .g.dart
files will only be used internally, it's a good practice to create your files in a hidden folder. Let source_gen
's combining_builder
manage the aggregation of results into the .g.dart
file.
You can find an example that applies these considerations in the source_gen
documentation.
builders:
some_cool_builder:
import: "package:this_package/builder.dart"
builder_factories: ["someCoolBuilder"]
# The `partId` argument to `SharedPartBuilder` is "some_cool_builder"
build_extensions: {".dart": [".some_cool_builder.g.part"]}
auto_apply: dependents
build_to: cache
# To copy the `.g.part` content into `.g.dart` in the source tree
applies_builders: ["source_gen:combining_builder"]
The following is build.yaml
of freezed
package.
targets:
$default:
builders:
freezed:
enabled: true
generate_for:
exclude:
- test
- example
include:
- test/integration/*
- test/integration/**/*
source_gen|combining_builder:
options:
ignore_for_file:
- "type=lint"
builders:
freezed:
import: "package:freezed/builder.dart"
builder_factories: ["freezed"]
build_extensions: { ".dart": [".freezed.dart"] }
auto_apply: dependents
build_to: source
runs_before: ["json_serializable|json_serializable"]
freezed
uses a unique extension called .freezed.dart
and does not utilize SharedPartBuilder
, so the rules I mentioned earlier were not applied.
Let's take a look at the ignore_for_file
option in the source_gen|combining_builder
options.
This option allows you to apply lint rules to the generated file.
In fact, when we examine the files generated by freezed
, they typically start like this.
Let’s explore Generator
by creating actual examples.
Example 3 — Generation with source_gen SharedPartBuilder
Let’s now try Code Generation using source_gen
. The builder we're going to create is as follows
top_level_names
(SharedPartBuilder)
- Declare a string variable that combines the names of all global variables existing in the file.
1. Add
source_gen
,analyzer
packages
dependencies:
analyzer: '>=5.2.0 <7.0.0'
source_gen: ^1.4.0
2. Set up
build.yaml
targets:
$default:
builders:
code_generation|topLevelNamesBuilder:
generate_for:
- lib/example/*.dart
enabled: True
builders:
# The next builder is `SharedPartBuilder` instances.
# Notice they each have # `build_extensions` set to a `NAME.g.part` file.
# NAME corresponds to the second argument to the SharedPartBuilder ctor.
# `.g.part` is the extension expected of SharedPartBuilder.
# `build_to: cache` - the output is not put directly into the project
# `applies_builders` - uses the `combining_builder` from `source_gen` to
# combine the parts from each builder into one part file.
topLevelNamesBuilder:
import: 'package:code_generation/code_generation.dart'
builder_factories: [ 'topLevelNamesBuilder' ]
build_extensions: {".dart": ["topLevelNames.g.part"]}
auto_apply: dependents
build_to: cache
applies_builders: ["source_gen|combining_builder"]
Inside the example project, we will execute the example from the lib/example
directory, so make sure to specify generate_for
as indicated above.
Pay attention to the format of build_extensions
(see comments).
3.
Builder
Exporter
// code_generation.dart
import 'package:build/build.dart';
import 'package:code_generation/top_level_names_generator.dart';
import 'package:source_gen/source_gen.dart';
Builder topLevelNamesBuilder(BuilderOptions options) => SharedPartBuilder(
[TopLevelNamesGenerator()],
'topLevelNames',
);
We have now exposed the topLevelNamesBuilder
builder to build
, and here we return a SharedPartBuilder
with Generator
as the first argument and an ID (partId
) as the second argument.
It's important to note that the partId
you pass here must match what you've specified in build.yaml
under build_extensions
.
4.
Generator
A Generator
’s role is simply to receive a LibraryReader
and return generated source code.
You only need to inherit from Generator
and implement the generate
method.
// top_level_names_generator.dart
import 'dart:async';
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
class TopLevelNamesGenerator extends Generator {
@override
FutureOr<String> generate(LibraryReader library, BuildStep buildStep) {
var joinedVariableName =
library.allElements.whereType<TopLevelVariableElement>().map((e) => e.name).join(',');
var joinedClassName =
library.allElements.whereType<ClassElement>().map((e) => e.name).join(',');
return '''
// Source library: ${library.element.source.uri}
const topLevelVariableNames = '$joinedVariableName';
const classNames = '$joinedClassName';
''';
}
}
The second argument of generate
, buildStep
, is the same buildStep
that was being passed in the Builder
. The first argument, LibraryReader
, provides access to the LibraryElement
through its element
property, and it offers various utilities for reading the code within the library
.
5. Create Example file
// example/example.dart
part 'example.g.dart';
final name = 'John Doe';
final age = 95;
class Dog {
const Dog(this.name, this.age);
final String name;
final int age;
}
When using PartBuilder
, it's important to declare part ...
in advance. You can see a similar instruction in packages like freezed
and json_serializable
.
Now, when you execute dart run build_runner build
, you can confirm that our code is generated correctly.
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'example.dart';
// **************************************************************************
// TopLevelNamesGenerator
// **************************************************************************
// Source library: package:code_generation/example/example.dart
const topLevelVariableNames = 'name,age';
const classNames = 'Dog';
Example 4 — Usage of Multiple Generator, Builder Types
We will create two SharedPartBuilder
instances to confirm that two files are correctly merged into .g.dart
.
Additionally, we'll create a PartBuilder
, resulting in a total of three builders.
This is based on examples from the source_gen
package, and here's what each builder will do:
top_level_names
(SharedPartBuilder)
: Declare a string variable that combines the names of all global variables in the file.multiplier
(SharedPartBuilder)
: Declare variables for global int variables in the file that have the@Multiplier
annotation, multiplying them by the value specified in the annotation.add
(PartBuilder)
: Declare variables in a separate.add.dart
file for global int variables in the file with the@Add
annotation, adding the value specified in the annotation.
1.
build.yaml
targets:
$default:
builders:
code_generation|topLevelNamesBuilder:
generate_for:
- lib/example/*.dart
enabled: True
code_generation|multiplierBuilder:
generate_for:
- lib/example/*.dart
enabled: True
code_generation|addBuilder:
generate_for:
- lib/example/*.dart
enabled: True
builders:
addBuilder:
import: 'package:code_generation/code_generation.dart'
builder_factories: [ 'addBuilder' ]
build_extensions: { ".dart": [ ".add.dart" ] }
auto_apply: dependents
build_to: source
# The next two builders are `SharedPartBuilder` instances.
# Notice they each have # `build_extensions` set to a `NAME.g.part` file. # NAME corresponds to the second argument to the SharedPartBuilder ctor. # `.g.part` is the extension expected of SharedPartBuilder. # `build_to: cache` - the output is not put directly into the project # `applies_builders` - uses the `combining_builder` from `source_gen` to # combine the parts from each builder into one part file.
topLevelNamesBuilder:
import: 'package:code_generation/code_generation.dart'
builder_factories: [ 'topLevelNamesBuilder' ]
build_extensions: {".dart": ["topLevelNames.g.part"]}
auto_apply: dependents
build_to: cache
applies_builders: ["source_gen|combining_builder"]
multiplierBuilder:
import: 'package:code_generation/code_generation.dart'
builder_factories: [ 'multiplierBuilder' ]
build_extensions: {".dart": ["multiplier.g.part"]}
auto_apply: dependents
build_to: cache
applies_builders: ["source_gen|combining_builder"]
The reason for setting targets
in the build.yaml
file is that we didn't separate the example
project in this project.
Depending on the auto_apply
property, Code Generation packages provided as dependencies may not require explicit target declarations.
In fact, in the source_gen
example, there are no targets
declared.
2. Declare annotation
// annotations.dart
class Multiplier {
final num value;
const Multiplier(this.value);
}
class Add {
final num value;
const Add(this.value);
}
3. Export
Builder
import 'package:build/build.dart';
import 'package:code_generation/add_generator.dart';
import 'package:code_generation/multiplier_generator.dart';
import 'package:code_generation/top_level_names_generator.dart';
import 'package:source_gen/source_gen.dart';
Builder topLevelNamesBuilder(BuilderOptions options) => SharedPartBuilder(
[TopLevelNamesGenerator()],
'topLevelNames',
);
Builder multiplierBuilder(BuilderOptions options) => SharedPartBuilder(
[MultiplierGenerator()],
'multiplier',
);
Builder addBuilder(BuilderOptions options) => PartBuilder(
[AddGenerator()],
'.add.dart',
options: options,
);
Notice that we use a new builder PartBuilder
.
4.
Generator
Let’s move on and just take a look at the MultiplierGenerator
to introduce GeneratorForAnnotation<T>
// multiplier_generator.dart
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'annotations.dart';
class MultiplierGenerator extends GeneratorForAnnotation<Multiplier> {
@override
String generateForAnnotatedElement(
Element element,
ConstantReader annotation,
BuildStep buildStep,
) {
if (element is! TopLevelVariableElement || !element.type.isDartCoreInt) {
throw InvalidGenerationSourceError(
'@Multiplier can only be applied on top level integer variable. Failing element: ${element.name}',
element: element,
);
}
final numValue = annotation.read('value').literalValue as num;
return 'num ${element.name}Multiplied() => ${element.name} * $numValue;';
}
}
In generateForAnnotatedElement
, an Element
object is passed in, and you can read the annotation
's values as well.
Since it's no longer just library-level elements coming in here, if an unexpected element is encountered, you should throw an InvalidGenerationSourceError
.
In the provided code, element.type
is the DartType
object from the Type Model, as explained earlier. In practice, if we were to attach @Multiplier
to a String
type, it might look like this.
The following error occurred during the build process.
Conclusion
We explored how Code Generation works in Dart and explored the roles and usage of various packages like build
, build_config
, analyzer
, and source_gen
.
One thing I noticed when I initially learned Dart is that the language relies heavily on Static Code Generation. As I continued to use it, I understood the advantages of this approach and why it has become popular among modern languages.
In fact, in Swift 5.9 and beyond, even Macros are actively discussed, and Apple is providing developers with the means to define their own Macros, as presented at WWDC 2023. This suggests that Code Generation could become a trending(?) practice in the development world.
Where to go?
If you’re planning to create your own Code Generation package, it’s highly recommended to follow the conventions set by well-established packages like freezed
or json_serializable
. By adhering to these conventions, you can ensure that your package is familiar and compatible with existing tools and workflows in the Dart ecosystem.
Additionally, it’s a good idea to explore more complex use cases and write tests using packages like build_test
to ensure your package functions correctly. This will not only result in a robust package but also provide you with a deeper understanding of Code Generation and how it can be applied effectively in your projects.
- Github
- Website
- Medium Blog, Dev Blog, Naver Blog
- Contact: mym0404@gmail.com