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!
* `data_class_to_companions` (defaults to `true`): Controls whether moor will write the `toCompanion` method in generated
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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,7 +30,8 @@ class UpdateCompanionWriter {
void _writeFields() {
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');
}
}

View File

@ -121,3 +121,7 @@ class DartScope {
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);
}
}