Option to generate mutable data classes (#551)

This commit is contained in:
Simon Binder 2020-06-02 22:26:59 +02:00
parent 896d62d76c
commit f6ab5f64a8
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
8 changed files with 133 additions and 6 deletions

View File

@ -62,6 +62,8 @@ At the moment, moor supports these options:
If you're using this flag, please open an issue and explain how the new inference isn't working for you, thanks! If you're using this flag, please open an issue and explain how the new inference isn't working for you, thanks!
* `data_class_to_companions` (defaults to `true`): Controls whether moor will write the `toCompanion` method in generated * `data_class_to_companions` (defaults to `true`): Controls whether moor will write the `toCompanion` method in generated
data classes. data classes.
* `mutable_classes` (defaults to `false`): The fields generated in generated data, companion and result set classes are final
by default. You can make them mutable by setting `mutable_classes: true`.
## Available extensions ## Available extensions

View File

@ -68,6 +68,9 @@ class MoorOptions {
@JsonKey(name: 'data_class_to_companions', defaultValue: true) @JsonKey(name: 'data_class_to_companions', defaultValue: true)
final bool dataClassToCompanions; final bool dataClassToCompanions;
@JsonKey(name: 'mutable_classes', defaultValue: false)
final bool generateMutableClasses;
/// Whether the [module] has been enabled in this configuration. /// Whether the [module] has been enabled in this configuration.
bool hasModule(SqlModule module) => modules.contains(module); bool hasModule(SqlModule module) => modules.contains(module);
@ -82,6 +85,7 @@ class MoorOptions {
this.legacyTypeInference = false, this.legacyTypeInference = false,
this.eagerlyLoadDartAst = false, this.eagerlyLoadDartAst = false,
this.dataClassToCompanions = true, this.dataClassToCompanions = true,
this.generateMutableClasses = false,
this.modules = const [], this.modules = const [],
}); });

View File

@ -19,7 +19,8 @@ MoorOptions _$MoorOptionsFromJson(Map<String, dynamic> json) {
'legacy_type_inference', 'legacy_type_inference',
'sqlite_modules', 'sqlite_modules',
'eagerly_load_dart_ast', 'eagerly_load_dart_ast',
'data_class_to_companions' 'data_class_to_companions',
'mutable_classes'
]); ]);
final val = MoorOptions( final val = MoorOptions(
generateFromJsonStringConstructor: $checkedConvert( generateFromJsonStringConstructor: $checkedConvert(
@ -54,6 +55,8 @@ MoorOptions _$MoorOptionsFromJson(Map<String, dynamic> json) {
dataClassToCompanions: dataClassToCompanions:
$checkedConvert(json, 'data_class_to_companions', (v) => v as bool) ?? $checkedConvert(json, 'data_class_to_companions', (v) => v as bool) ??
true, true,
generateMutableClasses:
$checkedConvert(json, 'mutable_classes', (v) => v as bool) ?? false,
modules: $checkedConvert( modules: $checkedConvert(
json, json,
'sqlite_modules', 'sqlite_modules',
@ -76,6 +79,7 @@ MoorOptions _$MoorOptionsFromJson(Map<String, dynamic> json) {
'legacyTypeInference': 'legacy_type_inference', 'legacyTypeInference': 'legacy_type_inference',
'eagerlyLoadDartAst': 'eagerly_load_dart_ast', 'eagerlyLoadDartAst': 'eagerly_load_dart_ast',
'dataClassToCompanions': 'data_class_to_companions', 'dataClassToCompanions': 'data_class_to_companions',
'generateMutableClasses': 'mutable_classes',
'modules': 'sqlite_modules' 'modules': 'sqlite_modules'
}); });
} }

View File

@ -14,11 +14,14 @@ class ResultSetWriter {
final into = scope.leaf(); final into = scope.leaf();
into.write('class $className {\n'); into.write('class $className {\n');
final modifier = scope.options.fieldModifier;
// write fields // write fields
for (final column in query.resultSet.columns) { for (final column in query.resultSet.columns) {
final name = query.resultSet.dartNameFor(column); final name = query.resultSet.dartNameFor(column);
final runtimeType = column.dartType; final runtimeType = column.dartType;
into.write('final $runtimeType $name\n;');
into.write('$modifier $runtimeType $name\n;');
fieldNames.add(name); fieldNames.add(name);
} }
@ -27,7 +30,7 @@ class ResultSetWriter {
final typeName = nested.table.dartTypeName; final typeName = nested.table.dartTypeName;
final fieldName = nested.dartFieldName; final fieldName = nested.dartFieldName;
into.write('final $typeName $fieldName;\n'); into.write('$modifier $typeName $fieldName;\n');
fieldNames.add(fieldName); fieldNames.add(fieldName);
} }

View File

@ -19,8 +19,9 @@ class DataClassWriter {
// write individual fields // write individual fields
for (final column in table.columns) { for (final column in table.columns) {
_buffer final modifier = scope.options.fieldModifier;
.write('final ${column.dartTypeName} ${column.dartGetterName}; \n'); _buffer.write(
'$modifier ${column.dartTypeName} ${column.dartGetterName}; \n');
} }
// write constructor with named optional fields // write constructor with named optional fields

View File

@ -30,7 +30,8 @@ class UpdateCompanionWriter {
void _writeFields() { void _writeFields() {
for (final column in table.columns) { for (final column in table.columns) {
_buffer.write('final Value<${column.dartTypeName}>' final modifier = scope.options.fieldModifier;
_buffer.write('$modifier Value<${column.dartTypeName}>'
' ${column.dartGetterName};\n'); ' ${column.dartGetterName};\n');
} }
} }

View File

@ -121,3 +121,7 @@ class DartScope {
return other._id >= _id; return other._id >= _id;
} }
} }
extension WriterUtilsForOptions on MoorOptions {
String get fieldModifier => generateMutableClasses ? '' : 'final';
}

View File

@ -0,0 +1,108 @@
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/file_system/memory_file_system.dart';
import 'package:build/build.dart';
import 'package:build_test/build_test.dart';
import 'package:moor_generator/src/backends/build/moor_builder.dart';
import 'package:test/test.dart';
const _testInput = r'''
import 'package:moor/moor.dart';
part 'main.moor.dart';
class Users extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
}
@UseMoor(
tables: [Users],
queries: {
'someQuery': 'SELECT 1 AS foo, 2 AS bar;',
},
)
class Database extends _$Database {}
''';
void main() {
test('generates mutable classes if needed', () async {
await testBuilder(
MoorPartBuilder(const BuilderOptions({'mutable_classes': true})),
const {'a|lib/main.dart': _testInput},
reader: await PackageAssetReader.currentIsolate(),
outputs: const {
'a|lib/main.moor.dart': _GeneratesWithoutFinalFields(
{'User', 'UsersCompanion', 'SomeQueryResult'},
),
},
);
}, tags: 'analyzer');
}
class _GeneratesWithoutFinalFields extends Matcher {
final Set<String> expectedWithoutFinals;
const _GeneratesWithoutFinalFields(this.expectedWithoutFinals);
@override
Description describe(Description description) {
return description.add('generates classes $expectedWithoutFinals without '
'final fields.');
}
@override
bool matches(dynamic desc, Map matchState) {
// Parse the file, assure we don't have final fields in data classes.
final resourceProvider = MemoryResourceProvider();
if (desc is List<int>) {
resourceProvider.newFileWithBytes('/foo.dart', desc);
} else if (desc is String) {
resourceProvider.newFile('/foo.dart', desc);
} else {
desc['desc'] = 'Neither a List<int> or String - cannot be parsed';
return false;
}
final parsed = parseFile(
path: '/foo.dart',
featureSet: FeatureSet.forTesting(),
resourceProvider: resourceProvider,
).unit;
final remaining = expectedWithoutFinals.toSet();
final definedClasses = parsed.declarations.whereType<ClassDeclaration>();
for (final definedClass in definedClasses) {
if (expectedWithoutFinals.contains(definedClass.name.name)) {
final fields = definedClass.members.whereType<FieldDeclaration>();
for (final field in fields) {
if (field.fields.isFinal) {
matchState['desc'] =
'Field ${field.fields.variables.first.name.name} in '
'${definedClass.name.name} is final.';
return false;
}
}
remaining.remove(definedClass.name.name);
}
}
// Also ensure that all expected classes were generated.
if (remaining.isNotEmpty) {
matchState['desc'] = 'Did not generate $remaining classes';
return false;
}
return true;
}
@override
Description describeMismatch(dynamic item, Description mismatchDescription,
Map matchState, bool verbose) {
return mismatchDescription.add(matchState['desc'] as String);
}
}