Skip to content

Instantly share code, notes, and snippets.

@cpboyd
Created April 21, 2021 04:17
Show Gist options
  • Save cpboyd/ca5da14f2932645ddf72538ff7890ec9 to your computer and use it in GitHub Desktop.
Save cpboyd/ca5da14f2932645ddf72538ff7890ec9 to your computer and use it in GitHub Desktop.
Dart/Flutter directory list build_runner
/// Configuration for using `package:build`-compatible build systems.
///
/// See:
/// * [build_runner](https://pub.dev/packages/build_runner)
///
/// This library is **not** intended to be imported by typical end-users unless
/// you are creating a custom compilation pipeline. See documentation for
/// details, and `build.yaml` for how these builders are configured by default.
import 'package:build/build.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:source_gen/source_gen.dart';
import 'dir_list_generator.dart';
/// Returns a [Builder] for use within a `package:build_runner`
/// `BuildAction`.
///
/// [formatOutput] is called to format the generated code. If not provided,
/// the default Dart code formatter is used.
Builder customPartBuilder({
String Function(String code)? formatOutput,
}) {
return SharedPartBuilder(
[
const DirectoryListGenerator(),
],
'custom_builder',
formatOutput: formatOutput,
);
}
/// Supports `package:build_runner` creation
///
/// Not meant to be invoked by hand-authored code.
Builder customBuilder(BuilderOptions options) {
try {
return customPartBuilder();
} on CheckedFromJsonException catch (e) {
final lines = <String>[
'Could not parse the options provided for `custom_builder`.'
];
if (e.key != null) {
lines.add('There is a problem with "${e.key}".');
}
if (e.message != null) {
lines.add(e.message!);
} else if (e.innerError != null) {
lines.add(e.innerError.toString());
}
throw StateError(lines.join('\n'));
}
}
/// An annotation used to generate a private field containing the a list of
/// directory contents.
///
/// The annotation can be applied to any member, but usually it's applied to
/// top-level getter.
///
/// In this example, the JSON content of `data.json` is populated into a
/// top-level, final field `_$glossaryDataDirectoryList` in the generated file.
///
/// ```dart
/// @DirectoryList('assets/images')
/// List<String> get imageAssets => _$imageAssetsDirectoryList;
/// ```
class DirectoryList {
/// The relative path from the Dart file with the annotation to the directory
/// to list.
final String path;
/// `true` if the JSON literal should be written as a constant.
final bool asConst;
/// `true` if returning filenames without extensions.
final bool stripExt;
/// If not empty, only returns the specified extensions.
final List<String>? extensions;
/// Creates a new [DirectoryList] instance.
const DirectoryList(this.path,
{bool asConst = false, bool stripExt = false, this.extensions})
: asConst = asConst,
stripExt = stripExt;
}
import 'dart:async';
import 'dart:io';
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:path/path.dart' as p;
import 'package:source_gen/source_gen.dart';
import 'dir_list.dart';
import 'utils.dart';
class DirectoryListGenerator extends GeneratorForAnnotation<DirectoryList> {
const DirectoryListGenerator();
@override
Future<String> generateForAnnotatedElement(
Element element,
ConstantReader annotation,
BuildStep buildStep,
) async {
final path = annotation.read('path').stringValue;
if (p.isAbsolute(path)) {
throw ArgumentError(
'`annotation.path` must be relative path to the source file.');
}
final sourcePathDir = p.dirname(buildStep.inputId.path);
final dirList = Directory(p.join(sourcePathDir, path)).list();
final asConst = annotation.read('asConst').boolValue;
final stripExt = annotation.read('stripExt').boolValue;
final extConst = annotation.read('extensions');
final extensions = extConst.isList
? extConst.listValue
.map((e) => e.toStringValue()?.toLowerCase() ?? '')
.toList()
: <String>[];
final thing = await directoryListAsDart(dirList, stripExt, extensions);
final marked = asConst ? 'const' : 'final';
return '$marked _\$${element.name}DirectoryList = $thing;';
}
}
/// Returns a [String] representing a valid Dart literal for [value].
Future<String> directoryListAsDart(Stream<FileSystemEntity> stream,
bool stripExt, List<String> extensions) async {
if (stream == null) return 'null';
var fileList = <String>[];
await for (var entity in stream) {
if (entity is File) {
final path = entity.path;
if (extensions.isEmpty ||
extensions.contains(p.extension(path).toLowerCase())) {
fileList.add(stripExt ? p.basenameWithoutExtension(path) : p.basename(path));
}
}
}
final listItems = fileList.map(escapeDartString).join(', ');
return '[$listItems]';
}
import 'package:analyzer/dart/constant/value.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:meta/meta.dart' show alwaysThrows;
import 'package:source_gen/source_gen.dart';
const _jsonKeyChecker = TypeChecker.fromRuntime(JsonKey);
DartObject? _jsonKeyAnnotation(FieldElement element) =>
_jsonKeyChecker.firstAnnotationOf(element) ??
(element.getter == null
? null
: _jsonKeyChecker.firstAnnotationOf(element.getter!));
ConstantReader jsonKeyAnnotation(FieldElement element) =>
ConstantReader(_jsonKeyAnnotation(element));
/// Returns `true` if [element] is annotated with [JsonKey].
bool hasJsonKeyAnnotation(FieldElement element) =>
_jsonKeyAnnotation(element) != null;
final _upperCase = RegExp('[A-Z]');
String kebabCase(String input) => _fixCase(input, '-');
String snakeCase(String input) => _fixCase(input, '_');
String pascalCase(String input) {
if (input.isEmpty) {
return '';
}
return input[0].toUpperCase() + input.substring(1);
}
String _fixCase(String input, String separator) =>
input.replaceAllMapped(_upperCase, (match) {
var lower = match.group(0)!.toLowerCase();
if (match.start > 0) {
lower = '$separator$lower';
}
return lower;
});
@alwaysThrows
void throwUnsupported(FieldElement element, String message) =>
throw InvalidGenerationSourceError(
'Error with `@JsonKey` on `${element.name}`. $message',
element: element);
FieldRename? _fromDartObject(ConstantReader reader) => reader.isNull
? null
: enumValueForDartObject(
reader.objectValue,
FieldRename.values,
((f) => f.toString().split('.')[1]) as String Function(FieldRename?),
);
T enumValueForDartObject<T>(
DartObject source,
List<T> items,
String Function(T) name,
) =>
items.singleWhere((v) => source.getField(name(v)) != null);
/// Return an instance of [JsonSerializable] corresponding to a the provided
/// [reader].
JsonSerializable _valueForAnnotation(ConstantReader reader) => JsonSerializable(
anyMap: reader.read('anyMap').literalValue as bool,
checked: reader.read('checked').literalValue as bool,
createFactory: reader.read('createFactory').literalValue as bool,
createToJson: reader.read('createToJson').literalValue as bool,
disallowUnrecognizedKeys:
reader.read('disallowUnrecognizedKeys').literalValue as bool,
explicitToJson: reader.read('explicitToJson').literalValue as bool,
fieldRename: _fromDartObject(reader.read('fieldRename'))!,
genericArgumentFactories:
reader.read('genericArgumentFactories').literalValue as bool,
ignoreUnannotated: reader.read('ignoreUnannotated').literalValue as bool,
includeIfNull: reader.read('includeIfNull').literalValue as bool,
);
/// Returns a [JsonSerializable] with values from the [JsonSerializable]
/// instance represented by [reader].
///
/// For fields that are not defined in [JsonSerializable] or `null` in [reader],
/// use the values in [config].
///
/// Note: if [JsonSerializable.genericArgumentFactories] is `false` for [reader]
/// and `true` for [config], the corresponding field in the return value will
/// only be `true` if [classElement] has type parameters.
JsonSerializable mergeConfig(
JsonSerializable config,
ConstantReader reader, {
required ClassElement classElement,
}) {
final annotation = _valueForAnnotation(reader);
return JsonSerializable(
anyMap: annotation.anyMap ?? config.anyMap,
checked: annotation.checked ?? config.checked,
createFactory: annotation.createFactory ?? config.createFactory,
createToJson: annotation.createToJson ?? config.createToJson,
disallowUnrecognizedKeys:
annotation.disallowUnrecognizedKeys ?? config.disallowUnrecognizedKeys,
explicitToJson: annotation.explicitToJson ?? config.explicitToJson,
fieldRename: annotation.fieldRename ?? config.fieldRename,
genericArgumentFactories: annotation.genericArgumentFactories ??
(classElement.typeParameters.isNotEmpty &&
(config.genericArgumentFactories ?? false)),
ignoreUnannotated: annotation.ignoreUnannotated ?? config.ignoreUnannotated,
includeIfNull: annotation.includeIfNull ?? config.includeIfNull,
);
}
/// Returns a quoted String literal for [value] that can be used in generated
/// Dart code.
String escapeDartString(String value) {
var hasSingleQuote = false;
var hasDoubleQuote = false;
var hasDollar = false;
var canBeRaw = true;
value = value.replaceAllMapped(_escapeRegExp, (match) {
final value = match[0];
if (value == "'") {
hasSingleQuote = true;
return value!;
} else if (value == '"') {
hasDoubleQuote = true;
return value!;
} else if (value == r'$') {
hasDollar = true;
return value!;
}
canBeRaw = false;
return _escapeMap[value!] ?? _getHexLiteral(value);
});
if (!hasDollar) {
if (hasSingleQuote) {
if (!hasDoubleQuote) {
return '"$value"';
}
// something
} else {
// trivial!
return "'$value'";
}
}
if (hasDollar && canBeRaw) {
if (hasSingleQuote) {
if (!hasDoubleQuote) {
// quote it with single quotes!
return 'r"$value"';
}
} else {
// quote it with single quotes!
return "r'$value'";
}
}
// The only safe way to wrap the content is to escape all of the
// problematic characters - `$`, `'`, and `"`
final string = value.replaceAll(_dollarQuoteRegexp, r'\');
return "'$string'";
}
final _dollarQuoteRegexp = RegExp(r"""(?=[$'"])""");
/// A [Map] between whitespace characters & `\` and their escape sequences.
const _escapeMap = {
'\b': r'\b', // 08 - backspace
'\t': r'\t', // 09 - tab
'\n': r'\n', // 0A - new line
'\v': r'\v', // 0B - vertical tab
'\f': r'\f', // 0C - form feed
'\r': r'\r', // 0D - carriage return
'\x7F': r'\x7F', // delete
r'\': r'\\' // backslash
};
final _escapeMapRegexp = _escapeMap.keys.map(_getHexLiteral).join();
/// A [RegExp] that matches whitespace characters that should be escaped and
/// single-quote, double-quote, and `$`
final _escapeRegExp = RegExp('[\$\'"\\x00-\\x07\\x0E-\\x1F$_escapeMapRegexp]');
/// Given single-character string, return the hex-escaped equivalent.
String _getHexLiteral(String input) {
final rune = input.runes.single;
final value = rune.toRadixString(16).toUpperCase().padLeft(2, '0');
return '\\x$value';
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment