Created
April 21, 2021 04:17
-
-
Save cpboyd/ca5da14f2932645ddf72538ff7890ec9 to your computer and use it in GitHub Desktop.
Dart/Flutter directory list build_runner
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// 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')); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// 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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]'; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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