// Copyright (c) 2017, 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 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart' show alwaysThrows; import 'package:source_gen/source_gen.dart'; final _jsonKeyChecker = const TypeChecker.fromRuntime(JsonKey); DartObject jsonKeyAnnotation(FieldElement element) => _jsonKeyChecker.firstAnnotationOfExact(element) ?? (element.getter == null ? null : _jsonKeyChecker.firstAnnotationOfExact(element.getter)); /// 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 _fixCase(String input, String seperator) => input.replaceAllMapped(_upperCase, (match) { var lower = match.group(0).toLowerCase(); if (match.start > 0) { lower = '$seperator$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 : FieldRename.values[reader.objectValue.getField('index').toIntValue()]; /// 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')), generateToJsonFunction: reader.read('generateToJsonFunction').literalValue as bool, includeIfNull: reader.read('includeIfNull').literalValue as bool, nullable: reader.read('nullable').literalValue as bool, useWrappers: reader.read('useWrappers').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]. JsonSerializable mergeConfig(JsonSerializable config, ConstantReader reader) { 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, generateToJsonFunction: annotation.generateToJsonFunction ?? config.generateToJsonFunction, includeIfNull: annotation.includeIfNull ?? config.includeIfNull, nullable: annotation.nullable ?? config.nullable, useWrappers: annotation.useWrappers ?? config.useWrappers, ); } bool isEnum(DartType targetType) => targetType is InterfaceType && targetType.element.isEnum; final _enumMapExpando = Expando>(); /// If [targetType] is an enum, returns a [Map] of the [FieldElement] instances /// associated with the enum values mapped to the [String] values that represent /// the serialized output. /// /// By default, the [String] value is just the name of the enum value. /// If the enum value is annotated with [JsonKey], then the `name` property is /// used if it's set and not `null`. /// /// If [targetType] is not an enum, `null` is returned. Map enumFieldsMap(DartType targetType) { MapEntry _generateEntry(FieldElement fe) { final annotation = const TypeChecker.fromRuntime(JsonValue).firstAnnotationOfExact(fe); dynamic fieldValue; if (annotation == null) { fieldValue = fe.name; } else { final reader = ConstantReader(annotation); final valueReader = reader.read('value'); if (valueReader.isString || valueReader.isNull || valueReader.isInt) { fieldValue = valueReader.literalValue; } else { throw InvalidGenerationSourceError( 'The `JsonValue` annotation on `$targetType.${fe.name}` does ' 'not have a value of type String, int, or null.', element: fe); } } final entry = MapEntry(fe, fieldValue); return entry; } if (targetType is InterfaceType && targetType.element.isEnum) { return _enumMapExpando[targetType] ??= Map.fromEntries(targetType.element.fields .where((p) => !p.isSynthetic) .map(_generateEntry)); } return null; } /// If [targetType] is an enum, returns the [FieldElement] instances associated /// with its values. /// /// Otherwise, `null`. Iterable iterateEnumFields(DartType targetType) => enumFieldsMap(targetType)?.keys; /// 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'; }