Skip to content

Refactor for reuse #122

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,5 @@
// 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.

String commonNullPrefix(
bool nullable, String expression, String unsafeExpression) =>
nullable
? '$expression == null ? null : $unsafeExpression'
: unsafeExpression;
final toJsonMapVarName = 'val';
final toJsonMapHelperName = 'writeNotNull';
248 changes: 40 additions & 208 deletions json_serializable/lib/src/json_serializable_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,10 @@ import 'dart:collection';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';

// ignore: implementation_imports
import 'package:analyzer/src/dart/resolver/inheritance_manager.dart'
show InheritanceManager;
import 'package:analyzer/analyzer.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:source_gen/source_gen.dart';

import 'constants.dart';
import 'type_helper.dart';
import 'type_helpers/date_time_helper.dart';
import 'type_helpers/enum_helper.dart';
Expand Down Expand Up @@ -81,52 +78,60 @@ class JsonSerializableGenerator
var classElement = element as ClassElement;

// Get all of the fields that need to be assigned
// TODO: support overriding the field set with an annotation option
var fieldList = _listFields(classElement);
var fieldsList = fieldList
.where((field) => field.isPublic && _jsonKeyFor(field).ignore != true)
.toList();

var undefinedFields =
fieldsList.where((fe) => fe.type.isUndefined).toList();
if (undefinedFields.isNotEmpty) {
var description =
undefinedFields.map((fe) => '`${fe.displayName}`').join(', ');
var sortedFieldList = listFields(classElement);

throw new InvalidGenerationSourceError(
'At least one field has an invalid type: $description.',
todo: 'Check names and imports.');
}
// Used to keep track of why a field is ignored. Useful for providing
// helpful errors when generating constructor calls that try to use one of
// these fields.
var unavailableReasons = <String, String>{};

var accessibleFieldList = sortedFieldList.where((field) {
if (!field.isPublic) {
unavailableReasons[field.name] = 'It is assigned to a private field.';
return false;
}

// Sort these in the order in which they appear in the class
// Sadly, `classElement.fields` puts properties after fields
fieldsList.sort(_sortByLocation);
if (_jsonKeyFor(field).ignore == true) {
unavailableReasons[field.name] = 'It is assigned to an ignored field.';
return false;
}

return true;
}).toList();

// Explicitly using `LinkedHashMap` – we want these ordered.
var fields = new LinkedHashMap<String, FieldElement>.fromIterable(
fieldsList,
accessibleFieldList,
key: (f) => (f as FieldElement).name);

// Get the constructor to use for the factory

var prefix = '_\$${classElement.name}';

var buffer = new StringBuffer();

final classAnnotation = _valueForAnnotation(annotation);

if (classAnnotation.createFactory) {
var ignoredFields = new Map<String, FieldElement>.fromIterable(
fieldList.where(
(field) => !field.isPublic || _jsonKeyFor(field).ignore == true),
key: (f) => (f as FieldElement).name);

var fieldsSetByFactory = _writeFactory(buffer, classElement, fields,
ignoredFields, prefix, classAnnotation.nullable);
buffer.writeln();
buffer.writeln(
'${classElement.name} ${prefix}FromJson(Map<String, dynamic> json) =>');

String deserializeFun(String paramOrFieldName,
{ParameterElement ctorParam}) =>
_deserializeForField(
fields[paramOrFieldName], classAnnotation.nullable,
ctorParam: ctorParam);

var fieldsSetByFactory = writeConstructorInvocation(
buffer,
classElement,
fields.keys.toSet(),
fields.values.where((fe) => !fe.isFinal).map((fe) => fe.name).toSet(),
unavailableReasons,
deserializeFun);

// If there are fields that are final – that are not set via the generated
// constructor, then don't output them when generating the `toJson` call.
fields.removeWhere((key, field) => !fieldsSetByFactory.contains(field));
fields.removeWhere((key, field) => !fieldsSetByFactory.contains(key));
}

// Now we check for duplicate JSON keys due to colliding annotations.
Expand Down Expand Up @@ -161,8 +166,8 @@ class JsonSerializableGenerator

buffer.write(' Map<String, dynamic> toJson() ');

var writeNaive =
fieldsList.every((e) => _writeJsonValueNaive(e, classAnnotation));
var writeNaive = accessibleFieldList
.every((e) => _writeJsonValueNaive(e, classAnnotation));

if (useWrappers) {
buffer.writeln('=> new $helpClassName(this);');
Expand Down Expand Up @@ -322,123 +327,6 @@ void $toJsonMapHelperName(String key, dynamic value) {
buffer.writeln(' };');
}

/// Returns the set of fields that are written to via factory.
/// Includes final fields that are written to in the constructor parameter list
/// Excludes remaining final fields, as they can't be set in the factory body
/// and shoudn't generated with toJson
Set<FieldElement> _writeFactory(
StringBuffer buffer,
ClassElement classElement,
Map<String, FieldElement> fields,
Map<String, FieldElement> ignoredFields,
String prefix,
bool classSupportNullable) {
var fieldsSetByFactory = new Set<FieldElement>();
var className = classElement.displayName;
// Create the factory method

// Get the default constructor
// TODO: allow overriding the ctor used for the factory
var ctor = classElement.unnamedConstructor;
if (ctor == null) {
throw new UnsupportedError(
'The class `${classElement.name}` has no default constructor.');
}

var ctorArguments = <ParameterElement>[];
var ctorNamedArguments = <ParameterElement>[];

for (var arg in ctor.parameters) {
var field = fields[arg.name];

if (field == null) {
// ignore: deprecated_member_use
if (arg.parameterKind == ParameterKind.REQUIRED) {
var additionalInfo = '';
var ignoredField = ignoredFields[arg.name];
if (ignoredField != null) {
if (_jsonKeyFor(ignoredField).ignore == true) {
additionalInfo = ' It it assigns to an ignored field.';
} else if (!ignoredField.isPublic) {
additionalInfo = ' It it assigns to a non public field.';
}
}

throw new UnsupportedError('Cannot populate the required constructor '
'argument: ${arg.displayName}.$additionalInfo');
}

continue;
}

// TODO: validate that the types match!
// ignore: deprecated_member_use
if (arg.parameterKind == ParameterKind.NAMED) {
ctorNamedArguments.add(arg);
} else {
ctorArguments.add(arg);
}
fieldsSetByFactory.add(field);
}

var undefinedArgs = [ctorArguments, ctorNamedArguments]
.expand((l) => l)
.where((pe) => pe.type.isUndefined)
.toList();
if (undefinedArgs.isNotEmpty) {
var description =
undefinedArgs.map((fe) => '`${fe.displayName}`').join(', ');

throw new InvalidGenerationSourceError(
'At least one constructor argument has an invalid type: $description.',
todo: 'Check names and imports.');
}

// find fields that aren't already set by the constructor and that aren't final
var remainingFieldsForFactoryBody = fields.values
.where((field) => !field.isFinal)
.toSet()
.difference(fieldsSetByFactory);

//
// Generate the static factory method
//
buffer.writeln();
buffer
.writeln('$className ${prefix}FromJson(Map<String, dynamic> json) =>');
buffer.write(' new $className(');
buffer.writeAll(
ctorArguments.map((paramElement) => _deserializeForField(
fields[paramElement.name], classSupportNullable,
ctorParam: paramElement)),
', ');
if (ctorArguments.isNotEmpty && ctorNamedArguments.isNotEmpty) {
buffer.write(', ');
}
buffer.writeAll(ctorNamedArguments.map((paramElement) {
var value = _deserializeForField(
fields[paramElement.name], classSupportNullable,
ctorParam: paramElement);
return '${paramElement.name}: $value';
}), ', ');

buffer.write(')');
if (remainingFieldsForFactoryBody.isEmpty) {
buffer.writeln(';');
} else {
for (var field in remainingFieldsForFactoryBody) {
buffer.writeln();
buffer.write(' ..${field.name} = ');
buffer.write(_deserializeForField(field, classSupportNullable));
fieldsSetByFactory.add(field);
}
buffer.writeln(';');
}
buffer.writeln();

return fieldsSetByFactory;
}

Iterable<TypeHelper> get _allHelpers =>
[_typeHelpers, _coreHelpers].expand((e) => e);

Expand Down Expand Up @@ -562,39 +450,6 @@ final _jsonKeyExpando = new Expando<JsonKey>();

final _jsonKeyChecker = const TypeChecker.fromRuntime(JsonKey);

final _dartCoreObjectChecker = const TypeChecker.fromRuntime(Object);

int _sortByLocation(FieldElement a, FieldElement b) {
var checkerA = new TypeChecker.fromStatic(a.enclosingElement.type);

if (!checkerA.isExactly(b.enclosingElement)) {
// in this case, you want to prioritize the enclosingElement that is more
// "super".

if (checkerA.isSuperOf(b.enclosingElement)) {
return -1;
}

var checkerB = new TypeChecker.fromStatic(b.enclosingElement.type);

if (checkerB.isSuperOf(a.enclosingElement)) {
return 1;
}
}

/// Returns the offset of given field/property in its source file – with a
/// preference for the getter if it's defined.
int _offsetFor(FieldElement e) {
if (e.getter != null && e.getter.nameOffset != e.nameOffset) {
assert(e.nameOffset == -1);
return e.getter.nameOffset;
}
return e.nameOffset;
}

return _offsetFor(a).compareTo(_offsetFor(b));
}

final _notSupportedWithTypeHelpersMsg =
'None of the provided `TypeHelper` instances support the defined type.';

Expand All @@ -608,26 +463,3 @@ InvalidGenerationSourceError _createInvalidGenerationError(
return new InvalidGenerationSourceError(message,
todo: 'Make sure all of the types are serializable.');
}

/// Returns a list of all instance, [FieldElement] items for [element] and
/// super classes.
List<FieldElement> _listFields(ClassElement element) {
// Get all of the fields that need to be assigned
// TODO: support overriding the field set with an annotation option
var fieldsList = element.fields.where((e) => !e.isStatic).toList();

var manager = new InheritanceManager(element.library);

for (var v in manager.getMembersInheritedFromClasses(element).values) {
assert(v is! FieldElement);
if (_dartCoreObjectChecker.isExactly(v.enclosingElement)) {
continue;
}

if (v is PropertyAccessorElement && v.variable is FieldElement) {
fieldsList.add(v.variable as FieldElement);
}
}

return fieldsList;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import 'package:analyzer/dart/element/type.dart';
import 'package:source_gen/source_gen.dart' show TypeChecker;
import '../type_helper.dart';
import 'type_helper_utils.dart';
import '../utils.dart';

class DateTimeHelper extends TypeHelper {
const DateTimeHelper();
Expand Down
2 changes: 1 addition & 1 deletion json_serializable/lib/src/type_helpers/enum_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import 'package:analyzer/dart/element/type.dart';
import '../type_helper.dart';
import 'type_helper_utils.dart';
import '../utils.dart';

final simpleExpression = new RegExp('^[a-zA-Z_]+\$');

Expand Down
2 changes: 1 addition & 1 deletion json_serializable/lib/src/type_helpers/json_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import '../type_helper.dart';
import 'type_helper_utils.dart';
import '../utils.dart';

class JsonHelper extends TypeHelper {
const JsonHelper();
Expand Down
2 changes: 1 addition & 1 deletion json_serializable/lib/src/type_helpers/map_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import 'package:analyzer/dart/element/type.dart';
import 'package:source_gen/source_gen.dart' show TypeChecker;
import '../type_helper.dart';
import 'type_helper_utils.dart';
import '../utils.dart';

/// Name used for closure argument when generating calls to `map`.
final _closureArg = 'e';
Expand Down
Loading