diff --git a/docs/content/en/docs/Advanced Features/builder_options.md b/docs/content/en/docs/Advanced Features/builder_options.md index 5088bea4..b9cb28c1 100644 --- a/docs/content/en/docs/Advanced Features/builder_options.md +++ b/docs/content/en/docs/Advanced Features/builder_options.md @@ -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 diff --git a/moor_generator/lib/src/analyzer/options.dart b/moor_generator/lib/src/analyzer/options.dart index 8c9e7e0d..d9b20b4d 100644 --- a/moor_generator/lib/src/analyzer/options.dart +++ b/moor_generator/lib/src/analyzer/options.dart @@ -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 [], }); diff --git a/moor_generator/lib/src/analyzer/options.g.dart b/moor_generator/lib/src/analyzer/options.g.dart index 8d076437..ba971cd2 100644 --- a/moor_generator/lib/src/analyzer/options.g.dart +++ b/moor_generator/lib/src/analyzer/options.g.dart @@ -19,7 +19,8 @@ MoorOptions _$MoorOptionsFromJson(Map 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 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 json) { 'legacyTypeInference': 'legacy_type_inference', 'eagerlyLoadDartAst': 'eagerly_load_dart_ast', 'dataClassToCompanions': 'data_class_to_companions', + 'generateMutableClasses': 'mutable_classes', 'modules': 'sqlite_modules' }); } diff --git a/moor_generator/lib/src/writer/queries/result_set_writer.dart b/moor_generator/lib/src/writer/queries/result_set_writer.dart index 15be27ca..058f4cca 100644 --- a/moor_generator/lib/src/writer/queries/result_set_writer.dart +++ b/moor_generator/lib/src/writer/queries/result_set_writer.dart @@ -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); } diff --git a/moor_generator/lib/src/writer/tables/data_class_writer.dart b/moor_generator/lib/src/writer/tables/data_class_writer.dart index 8439e424..a0c1ce13 100644 --- a/moor_generator/lib/src/writer/tables/data_class_writer.dart +++ b/moor_generator/lib/src/writer/tables/data_class_writer.dart @@ -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 diff --git a/moor_generator/lib/src/writer/tables/update_companion_writer.dart b/moor_generator/lib/src/writer/tables/update_companion_writer.dart index b65c7f00..be39ec51 100644 --- a/moor_generator/lib/src/writer/tables/update_companion_writer.dart +++ b/moor_generator/lib/src/writer/tables/update_companion_writer.dart @@ -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'); } } diff --git a/moor_generator/lib/src/writer/writer.dart b/moor_generator/lib/src/writer/writer.dart index 464f9401..c8fca8d4 100644 --- a/moor_generator/lib/src/writer/writer.dart +++ b/moor_generator/lib/src/writer/writer.dart @@ -121,3 +121,7 @@ class DartScope { return other._id >= _id; } } + +extension WriterUtilsForOptions on MoorOptions { + String get fieldModifier => generateMutableClasses ? '' : 'final'; +} diff --git a/moor_generator/test/writer/mutable_classes_integration_test.dart b/moor_generator/test/writer/mutable_classes_integration_test.dart new file mode 100644 index 00000000..a7ca72d9 --- /dev/null +++ b/moor_generator/test/writer/mutable_classes_integration_test.dart @@ -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 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) { + resourceProvider.newFileWithBytes('/foo.dart', desc); + } else if (desc is String) { + resourceProvider.newFile('/foo.dart', desc); + } else { + desc['desc'] = 'Neither a List 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(); + for (final definedClass in definedClasses) { + if (expectedWithoutFinals.contains(definedClass.name.name)) { + final fields = definedClass.members.whereType(); + + 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); + } +}