| // 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. |
| |
| //TODO(kevmoo): https://github.com/dart-lang/linter/issues/3563 |
| // ignore_for_file: use_super_parameters |
| |
| 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 '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. |
| final String Function(String) 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, { |
| String Function(String code)? formatOutput, |
| 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, |
| ] |
| }), |
| formatOutput = formatOutput ?? _formatter.format, |
| _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); |
| } 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( |
| List<Generator> generators, |
| String partId, { |
| String Function(String code)? formatOutput, |
| List<String> additionalOutputExtensions = const [], |
| bool allowSyntaxErrors = false, |
| }) : super( |
| generators, |
| formatOutput: formatOutput, |
| generatedExtension: '.$partId.g.part', |
| additionalOutputExtensions: additionalOutputExtensions, |
| header: '', |
| allowSyntaxErrors: allowSyntaxErrors, |
| ) { |
| 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( |
| List<Generator> generators, |
| String generatedExtension, { |
| String Function(String code)? formatOutput, |
| List<String> additionalOutputExtensions = const [], |
| String? header, |
| bool allowSyntaxErrors = false, |
| BuilderOptions? options, |
| }) : super( |
| generators, |
| formatOutput: formatOutput, |
| generatedExtension: generatedExtension, |
| additionalOutputExtensions: additionalOutputExtensions, |
| header: header, |
| allowSyntaxErrors: allowSyntaxErrors, |
| options: options, |
| ); |
| } |
| |
| /// 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 |
| /// [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. |
| LibraryBuilder( |
| Generator generator, { |
| String Function(String code)? formatOutput, |
| String generatedExtension = '.g.dart', |
| List<String> additionalOutputExtensions = const [], |
| String? header, |
| bool allowSyntaxErrors = false, |
| BuilderOptions? options, |
| }) : super( |
| [generator], |
| formatOutput: formatOutput, |
| generatedExtension: generatedExtension, |
| additionalOutputExtensions: additionalOutputExtensions, |
| header: header, |
| allowSyntaxErrors: allowSyntaxErrors, |
| options: options, |
| ); |
| } |
| |
| 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; |
| } |
| |
| final _formatter = DartFormatter(fixes: [StyleFix.singleCascadeStatements]); |
| |
| const defaultFileHeader = '// GENERATED CODE - DO NOT MODIFY BY HAND'; |
| |
| 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'; |
| } |