blob: aef2204aa801411842ee7d70200193835d37ad1d [file] [log] [blame]
// Copyright (c) 2016, Google Inc. 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.
library built_value_generator.enum_source_class;
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:built_collection/built_collection.dart';
import 'package:built_value/built_value.dart';
import 'package:built_value_generator/src/dart_types.dart';
import 'package:built_value_generator/src/enum_source_field.dart';
import 'package:built_value_generator/src/strings.dart';
import 'package:quiver/iterables.dart';
part 'enum_source_class.g.dart';
abstract class EnumSourceClass
implements Built<EnumSourceClass, EnumSourceClassBuilder> {
ClassElement get element;
factory EnumSourceClass(
ParsedLibraryResult parsedLibrary, ClassElement element) =>
_$EnumSourceClass._(element: element);
EnumSourceClass._();
@memoized
ParsedLibraryResult get parsedLibrary =>
element.library.session.getParsedLibraryByElement(element.library);
@memoized
String get name => element.name;
@memoized
String get wireName => settings.wireName ?? name;
@memoized
BuiltValueEnum get settings {
var annotations = element.metadata
.map((annotation) => annotation.computeConstantValue())
.where((value) => DartTypes.getName(value?.type) == 'BuiltValueEnum');
if (annotations.isEmpty) return const BuiltValueEnum();
var annotation = annotations.single;
return BuiltValueEnum(
wireName: annotation.getField('wireName').toStringValue());
}
@memoized
bool get isAbstract => element.isAbstract;
@memoized
BuiltList<EnumSourceField> get fields =>
EnumSourceField.fromClassElement(parsedLibrary, element);
@memoized
BuiltList<String> get constructors =>
BuiltList<String>(element.constructors.map((element) {
final declaration = parsedLibrary.getElementDeclaration(element);
return declaration?.node?.toSource() ?? '';
}));
@memoized
String get valuesIdentifier {
var getter = element.getGetter('values');
if (getter == null) return null;
var source = parsedLibrary.getElementDeclaration(getter).node.toSource();
var matches = RegExp(r'static BuiltSet<' +
element.displayName +
r'> get values => (_\$\w+)\;')
.allMatches(source);
return matches.isEmpty ? null : matches.first.group(1);
}
@memoized
String get valueOfIdentifier {
var getter = element.getMethod('valueOf');
if (getter == null) return null;
var source = parsedLibrary.getElementDeclaration(getter).node.toSource();
var matches = RegExp(r'static ' +
element.displayName +
r' valueOf\(String name\) \=\> (\_\$\w+)\(name\)\;')
.allMatches(source);
return matches.isEmpty ? null : matches.first.group(1);
}
@memoized
bool get usesMixin => element.library.getType(name + 'Mixin') != null;
@memoized
String get mixinDeclaration {
var mixinElement = element.library.getType(name + 'Mixin');
if (mixinElement == null) return null;
return parsedLibrary.getElementDeclaration(mixinElement).node.toSource();
}
@memoized
Iterable<String> get identifiers {
return concat([
[valuesIdentifier, valueOfIdentifier],
fields.map((field) => field.generatedIdentifier)
]);
}
static bool needsEnumClass(ClassElement classElement) {
// `Object` and mixins return `null` for `supertype`.
return DartTypes.getName(classElement.supertype) == 'EnumClass';
}
Iterable<String> computeErrors() {
return concat([
_checkAbstract(),
_checkFields(),
_checkFallbackFields(),
_checkConstructor(),
_checkValuesGetter(),
_checkValueOf(),
_checkMixin(),
]).toList();
}
Iterable<String> _checkAbstract() {
return isAbstract ? ['Make $name concrete; remove "abstract".'] : [];
}
Iterable<String> _checkFields() {
return concat(fields.map((field) => field.errors));
}
Iterable<String> _checkFallbackFields() {
var result = <String>[];
var fallbackFields =
fields.where((field) => field.settings.fallback).toList();
if (fallbackFields.length > 1) {
result.add('Remove `fallback = true` '
'so that at most one constant is the fallback. '
'Currently on "$name" fields '
'${fallbackFields.map((field) => '"${field.name}"').join(', ')}.');
}
return result;
}
Iterable<String> _checkConstructor() {
var expectedCode = 'const $name._(String name) : super(name);';
return constructors.length == 1 && constructors.single == expectedCode
? <String>[]
: <String>['Have exactly one constructor: $expectedCode'];
}
Iterable<String> _checkValuesGetter() {
var result = <String>[];
if (valuesIdentifier == null) {
result.add('Add getter: static BuiltSet<$name> get values => _\$values');
}
return result;
}
Iterable<String> _checkValueOf() {
var result = <String>[];
if (valueOfIdentifier == null) {
result.add('Add method: '
'static $name valueOf(String name) => _\$valueOf(name)');
}
return result;
}
Iterable<String> _checkMixin() {
if (usesMixin) {
final expectedCode =
'abstract class ${name}Mixin = Object with _\$${name}Mixin;';
if (!mixinDeclaration.contains(expectedCode)) {
return ['Remove mixin or declare using exactly: $expectedCode'];
}
}
return [];
}
String generateCode() {
var result = StringBuffer();
for (var field in fields) {
result.writeln('const $name ${field.generatedIdentifier} = '
'const $name._(\'${escapeString(field.name)}\');');
}
result.writeln('');
result.writeln('$name $valueOfIdentifier(String name) {'
'switch (name) {');
for (var field in fields) {
result.writeln("case '${escapeString(field.name)}':"
' return ${field.generatedIdentifier};');
}
var fallback = fields.firstWhere((field) => field.settings.fallback,
orElse: () => null);
if (fallback == null) {
result.writeln('default: throw new ArgumentError(name);');
} else {
result.writeln('default: return ${fallback.generatedIdentifier};');
}
result.writeln('}}');
result.writeln('');
result.writeln('final BuiltSet<$name> $valuesIdentifier ='
'new BuiltSet<$name>(const <$name>[');
for (var field in fields) {
result.writeln('${field.generatedIdentifier},');
}
result.writeln(']);');
if (usesMixin) {
result.write(_generateMixin());
}
return result.toString();
}
String _generateMixin() {
var result = StringBuffer();
result
..writeln('class _\$${name}Meta {')
..writeln('const _\$${name}Meta();');
for (var field in fields) {
result
.writeln('$name get ${field.name} => ${field.generatedIdentifier};');
}
result
..writeln('$name valueOf(String name) => $valueOfIdentifier(name);')
..writeln('BuiltSet<$name> get values => $valuesIdentifier;')
..writeln('}')
..writeln('abstract class _\$${name}Mixin {')
..writeln(' // ignore: non_constant_identifier_names')
..writeln('_\$${name}Meta get $name => const _\$${name}Meta();')
..writeln('}');
return result.toString();
}
}