-
Notifications
You must be signed in to change notification settings - Fork 104
/
builder.dart
396 lines (356 loc) · 13.2 KB
/
builder.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
// Copyright (c) 2015, 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 'dart:convert';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:dart_style/dart_style.dart';
import 'package:pub_semver/pub_semver.dart';
import 'generated_output.dart';
import 'generator.dart';
import 'generator_for_annotation.dart';
import 'library.dart';
import 'utils.dart';
/// A [Builder] wrapping on one or more [Generator]s.
class _Builder extends Builder {
/// Function that determines how the generated code is formatted.
///
/// The `languageVersion` is the version to parse the file with, but it may be
/// overridden using a language version comment in the file.
final String Function(String code, Version languageVersion) formatOutput;
/// The generators run for each targeted library.
final List<Generator> _generators;
/// The [buildExtensions] configuration for `.dart`
final String _generatedExtension;
/// Whether to emit a standalone (non-`part`) file in this builder.
bool get _isLibraryBuilder => this is LibraryBuilder;
final String _header;
/// Whether to allow syntax errors in input libraries.
final bool allowSyntaxErrors;
@override
final Map<String, List<String>> buildExtensions;
/// Wrap [_generators] to form a [Builder]-compatible API.
///
/// If available, the `build_extensions` option will be extracted from
/// [options] to allow output files to be generated into a different directory
_Builder(
this._generators, {
this.formatOutput = _defaultFormatOutput,
String generatedExtension = '.g.dart',
List<String> additionalOutputExtensions = const [],
String? header,
this.allowSyntaxErrors = false,
BuilderOptions? options,
}) : _generatedExtension = generatedExtension,
buildExtensions = validatedBuildExtensionsFrom(
options != null ? Map.of(options.config) : null, {
'.dart': [
generatedExtension,
...additionalOutputExtensions,
],
}),
_header = (header ?? defaultFileHeader).trim() {
if (_generatedExtension.isEmpty || !_generatedExtension.startsWith('.')) {
throw ArgumentError.value(
_generatedExtension,
'generatedExtension',
'Extension must be in the format of .*',
);
}
if (_isLibraryBuilder && _generators.length > 1) {
throw ArgumentError(
'A standalone file can only be generated from a single Generator.',
);
}
if (options != null && additionalOutputExtensions.isNotEmpty) {
throw ArgumentError(
'Either `options` or `additionalOutputExtensions` parameter '
'can be given. Not both.',
);
}
}
@override
Future<void> build(BuildStep buildStep) async {
final resolver = buildStep.resolver;
if (!await resolver.isLibrary(buildStep.inputId)) return;
if (_generators.every((g) => g is GeneratorForAnnotation) &&
!(await _hasAnyTopLevelAnnotations(
buildStep.inputId,
resolver,
buildStep,
))) {
return;
}
final lib = await buildStep.resolver
.libraryFor(buildStep.inputId, allowSyntaxErrors: allowSyntaxErrors);
await _generateForLibrary(lib, buildStep);
}
Future<void> _generateForLibrary(
LibraryElement library,
BuildStep buildStep,
) async {
final generatedOutputs =
await _generate(library, _generators, buildStep).toList();
// Don't output useless files.
//
// NOTE: It is important to do this check _before_ checking for valid
// library/part definitions because users expect some files to be skipped
// therefore they do not have "library".
if (generatedOutputs.isEmpty) return;
final outputId = buildStep.allowedOutputs.first;
final contentBuffer = StringBuffer();
if (_header.isNotEmpty) {
contentBuffer.writeln(_header);
}
if (!_isLibraryBuilder) {
final asset = buildStep.inputId;
final partOfUri = uriOfPartial(library, asset, outputId);
contentBuffer.writeln();
if (this is PartBuilder) {
contentBuffer
..write(languageOverrideForLibrary(library))
..writeln('part of \'$partOfUri\';');
final part = computePartUrl(buildStep.inputId, outputId);
final libraryUnit =
await buildStep.resolver.compilationUnitFor(buildStep.inputId);
final hasLibraryPartDirectiveWithOutputUri =
hasExpectedPartDirective(libraryUnit, part);
if (!hasLibraryPartDirectiveWithOutputUri) {
// TODO: Upgrade to error in a future breaking change?
log.warning(
'$part must be included as a part directive in '
'the input library with:\n part \'$part\';',
);
return;
}
} else {
assert(this is SharedPartBuilder);
// For shared-part builders, `part` statements will be checked by the
// combining build step.
}
}
for (var item in generatedOutputs) {
contentBuffer
..writeln()
..writeln(_headerLine)
..writeAll(
LineSplitter.split(item.generatorDescription)
.map((line) => '// $line\n'),
)
..writeln(_headerLine)
..writeln()
..writeln(item.output);
}
var genPartContent = contentBuffer.toString();
try {
genPartContent =
formatOutput(genPartContent, library.languageVersion.effective);
} catch (e, stack) {
log.severe(
'''
An error `${e.runtimeType}` occurred while formatting the generated source for
`${library.identifier}`
which was output to
`${outputId.path}`.
This may indicate an issue in the generator, the input source code, or in the
source formatter.''',
e,
stack,
);
}
await buildStep.writeAsString(outputId, genPartContent);
}
@override
String toString() =>
'Generating $_generatedExtension: ${_generators.join(', ')}';
}
/// A [Builder] which generates content intended for `part of` files.
///
/// Generated files will be prefixed with a `partId` to ensure multiple
/// [SharedPartBuilder]s can produce non conflicting `part of` files. When the
/// `source_gen|combining_builder` is applied to the primary input these
/// snippets will be concatenated into the final `.g.dart` output.
///
/// This builder can be used when multiple generators may need to output to the
/// same part file but [PartBuilder] can't be used because the generators are
/// not all defined in the same location. As a convention most codegen which
/// generates code should use this approach to get content into a `.g.dart` file
/// instead of having individual outputs for each building package.
class SharedPartBuilder extends _Builder {
/// Wrap [generators] as a [Builder] that generates `part of` files.
///
/// [partId] indicates what files will be created for each `.dart`
/// input. This extension should be unique as to not conflict with other
/// [SharedPartBuilder]s. The resulting file will be of the form
/// `<generatedExtension>.g.part`. If any generator in [generators] will
/// create additional outputs through the [BuildStep] they should be indicated
/// in [additionalOutputExtensions].
///
/// [formatOutput] is called to format the generated code. Defaults to
/// [DartFormatter.format].
///
/// [allowSyntaxErrors] indicates whether to allow syntax errors in input
/// libraries.
SharedPartBuilder(
super.generators,
String partId, {
super.formatOutput,
super.additionalOutputExtensions,
super.allowSyntaxErrors,
}) : super(
generatedExtension: '.$partId.g.part',
header: '',
) {
if (!_partIdRegExp.hasMatch(partId)) {
throw ArgumentError.value(
partId,
'partId',
'`partId` can only contain letters, numbers, `_` and `.`. '
'It cannot start or end with `.`.',
);
}
}
}
/// A [Builder] which generates `part of` files.
///
/// This builder should be avoided - prefer using [SharedPartBuilder] and
/// generating content that can be merged with output from other builders into a
/// common `.g.dart` part file.
///
/// Each output should correspond to a `part` directive in the primary input,
/// this will be validated.
///
/// Content output by each generator is concatenated and written to the output.
/// A `part of` directive will automatically be included in the output and
/// should not need be written by any of the generators.
class PartBuilder extends _Builder {
/// Wrap [generators] as a [Builder] that generates `part of` files.
///
/// [generatedExtension] indicates what files will be created for each
/// `.dart` input. The [generatedExtension] should *not* be `.g.dart`. If you
/// wish to produce `.g.dart` files please use [SharedPartBuilder].
///
/// If any generator in [generators] will create additional outputs through
/// the [BuildStep] they should be indicated in [additionalOutputExtensions].
///
/// [formatOutput] is called to format the generated code. Defaults to
/// [DartFormatter.format].
///
/// [header] is used to specify the content at the top of each generated file.
/// If `null`, the content of [defaultFileHeader] is used.
/// If [header] is an empty `String` no header is added.
///
/// [allowSyntaxErrors] indicates whether to allow syntax errors in input
/// libraries.
///
/// If available, the `build_extensions` option will be extracted from
/// [options] to allow output files to be generated into a different directory
PartBuilder(
super.generators,
String generatedExtension, {
super.formatOutput,
super.additionalOutputExtensions,
super.header,
super.allowSyntaxErrors,
super.options,
}) : super(
generatedExtension: generatedExtension,
);
}
/// A [Builder] which generates standalone Dart library files.
///
/// A single [Generator] is responsible for generating the entirety of the
/// output since it must also output any relevant import directives at the
/// beginning of it's output.
class LibraryBuilder extends _Builder {
/// Wrap [generator] as a [Builder] that generates Dart library files.
///
/// [generatedExtension] indicates what files will be created for each `.dart`
/// input.
/// Defaults to `.g.dart`, however this should usually be changed to
/// avoid conflicts with outputs from a [SharedPartBuilder].
/// If [generator] will create additional outputs through the [BuildStep] they
/// should be indicated in [additionalOutputExtensions].
///
/// [formatOutput] is called to format the generated code. Defaults to
/// using the standard [DartFormatter].
///
/// [header] is used to specify the content at the top of each generated file.
/// If `null`, the content of [defaultFileHeader] is used.
/// If [header] is an empty `String` no header is added.
///
/// [allowSyntaxErrors] indicates whether to allow syntax errors in input
/// libraries.
LibraryBuilder(
Generator generator, {
super.formatOutput,
super.generatedExtension,
super.additionalOutputExtensions,
super.header,
super.allowSyntaxErrors,
super.options,
}) : super([generator]);
}
Stream<GeneratedOutput> _generate(
LibraryElement library,
List<Generator> generators,
BuildStep buildStep,
) async* {
final libraryReader = LibraryReader(library);
for (var i = 0; i < generators.length; i++) {
final gen = generators[i];
var msg = 'Running $gen';
if (generators.length > 1) {
msg = '$msg - ${i + 1} of ${generators.length}';
}
log.fine(msg);
var createdUnit = await gen.generate(libraryReader, buildStep);
if (createdUnit == null) {
continue;
}
createdUnit = createdUnit.trim();
if (createdUnit.isEmpty) {
continue;
}
yield GeneratedOutput(gen, createdUnit);
}
}
Future<bool> _hasAnyTopLevelAnnotations(
AssetId input,
Resolver resolver,
BuildStep buildStep,
) async {
if (!await buildStep.canRead(input)) return false;
final parsed = await resolver.compilationUnitFor(input);
final partIds = <AssetId>[];
for (var directive in parsed.directives) {
if (directive.metadata.isNotEmpty) return true;
if (directive is PartDirective) {
partIds.add(
AssetId.resolve(Uri.parse(directive.uri.stringValue!), from: input),
);
}
}
for (var declaration in parsed.declarations) {
if (declaration.metadata.isNotEmpty) return true;
}
for (var partId in partIds) {
if (await _hasAnyTopLevelAnnotations(partId, resolver, buildStep)) {
return true;
}
}
return false;
}
const defaultFileHeader = '// GENERATED CODE - DO NOT MODIFY BY HAND';
String _defaultFormatOutput(String code, Version version) =>
DartFormatter(languageVersion: version).format(code);
final _headerLine = '// '.padRight(77, '*');
const partIdRegExpLiteral = r'[A-Za-z_\d-]+';
final _partIdRegExp = RegExp('^$partIdRegExpLiteral\$');
String languageOverrideForLibrary(LibraryElement library) {
final override = library.languageVersion.override;
return override == null
? ''
: '// @dart=${override.major}.${override.minor}\n';
}