Fix generated code with $ in identifier names

This commit is contained in:
Simon Binder 2023-11-30 22:26:40 +01:00
parent 99bb9e0fe0
commit 5740eb8721
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
9 changed files with 371 additions and 35 deletions

View File

@ -6,6 +6,16 @@ import 'package:drift/native.dart';
part 'main.g.dart';
class Todo extends Table {
TextColumn get id => text()();
TextColumn get listid => text().nullable()();
TextColumn get text$ => text().named('text').nullable()();
BoolColumn get completed => boolean()();
}
@DataClassName('TodoCategory')
class TodoCategories extends Table {
IntColumn get id => integer().autoIncrement()();
@ -57,6 +67,7 @@ abstract class TodoItemWithCategoryNameView extends View {
}
@DriftDatabase(tables: [
Todo,
TodoItems,
TodoCategories,
], views: [

View File

@ -3,6 +3,270 @@
part of 'main.dart';
// ignore_for_file: type=lint
class $TodoTable extends Todo with TableInfo<$TodoTable, TodoData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$TodoTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<String> id = GeneratedColumn<String>(
'id', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
static const VerificationMeta _listidMeta = const VerificationMeta('listid');
@override
late final GeneratedColumn<String> listid = GeneratedColumn<String>(
'listid', aliasedName, true,
type: DriftSqlType.string, requiredDuringInsert: false);
static const VerificationMeta _text$Meta = const VerificationMeta('text\$');
@override
late final GeneratedColumn<String> text$ = GeneratedColumn<String>(
'text', aliasedName, true,
type: DriftSqlType.string, requiredDuringInsert: false);
static const VerificationMeta _completedMeta =
const VerificationMeta('completed');
@override
late final GeneratedColumn<bool> completed = GeneratedColumn<bool>(
'completed', aliasedName, false,
type: DriftSqlType.bool,
requiredDuringInsert: true,
defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("completed" IN (0, 1))'));
@override
List<GeneratedColumn> get $columns => [id, listid, text$, completed];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'todo';
@override
VerificationContext validateIntegrity(Insertable<TodoData> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
} else if (isInserting) {
context.missing(_idMeta);
}
if (data.containsKey('listid')) {
context.handle(_listidMeta,
listid.isAcceptableOrUnknown(data['listid']!, _listidMeta));
}
if (data.containsKey('text')) {
context.handle(
_text$Meta, text$.isAcceptableOrUnknown(data['text']!, _text$Meta));
}
if (data.containsKey('completed')) {
context.handle(_completedMeta,
completed.isAcceptableOrUnknown(data['completed']!, _completedMeta));
} else if (isInserting) {
context.missing(_completedMeta);
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => const {};
@override
TodoData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return TodoData(
id: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}id'])!,
listid: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}listid']),
text$: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}text']),
completed: attachedDatabase.typeMapping
.read(DriftSqlType.bool, data['${effectivePrefix}completed'])!,
);
}
@override
$TodoTable createAlias(String alias) {
return $TodoTable(attachedDatabase, alias);
}
}
class TodoData extends DataClass implements Insertable<TodoData> {
final String id;
final String? listid;
final String? text$;
final bool completed;
const TodoData(
{required this.id, this.listid, this.text$, required this.completed});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<String>(id);
if (!nullToAbsent || listid != null) {
map['listid'] = Variable<String>(listid);
}
if (!nullToAbsent || text$ != null) {
map['text'] = Variable<String>(text$);
}
map['completed'] = Variable<bool>(completed);
return map;
}
TodoCompanion toCompanion(bool nullToAbsent) {
return TodoCompanion(
id: Value(id),
listid:
listid == null && nullToAbsent ? const Value.absent() : Value(listid),
text$:
text$ == null && nullToAbsent ? const Value.absent() : Value(text$),
completed: Value(completed),
);
}
factory TodoData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return TodoData(
id: serializer.fromJson<String>(json['id']),
listid: serializer.fromJson<String?>(json['listid']),
text$: serializer.fromJson<String?>(json['text\$']),
completed: serializer.fromJson<bool>(json['completed']),
);
}
factory TodoData.fromJsonString(String encodedJson,
{ValueSerializer? serializer}) =>
TodoData.fromJson(
DataClass.parseJson(encodedJson) as Map<String, dynamic>,
serializer: serializer);
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'listid': serializer.toJson<String?>(listid),
'text\$': serializer.toJson<String?>(text$),
'completed': serializer.toJson<bool>(completed),
};
}
TodoData copyWith(
{String? id,
Value<String?> listid = const Value.absent(),
Value<String?> text$ = const Value.absent(),
bool? completed}) =>
TodoData(
id: id ?? this.id,
listid: listid.present ? listid.value : this.listid,
text$: text$.present ? text$.value : this.text$,
completed: completed ?? this.completed,
);
@override
String toString() {
return (StringBuffer('TodoData(')
..write('id: $id, ')
..write('listid: $listid, ')
..write('text\$: ${text$}, ')
..write('completed: $completed')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, listid, text$, completed);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is TodoData &&
other.id == this.id &&
other.listid == this.listid &&
other.text$ == this.text$ &&
other.completed == this.completed);
}
class TodoCompanion extends UpdateCompanion<TodoData> {
final Value<String> id;
final Value<String?> listid;
final Value<String?> text$;
final Value<bool> completed;
final Value<int> rowid;
const TodoCompanion({
this.id = const Value.absent(),
this.listid = const Value.absent(),
this.text$ = const Value.absent(),
this.completed = const Value.absent(),
this.rowid = const Value.absent(),
});
TodoCompanion.insert({
required String id,
this.listid = const Value.absent(),
this.text$ = const Value.absent(),
required bool completed,
this.rowid = const Value.absent(),
}) : id = Value(id),
completed = Value(completed);
static Insertable<TodoData> custom({
Expression<String>? id,
Expression<String>? listid,
Expression<String>? text$,
Expression<bool>? completed,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (listid != null) 'listid': listid,
if (text$ != null) 'text': text$,
if (completed != null) 'completed': completed,
if (rowid != null) 'rowid': rowid,
});
}
TodoCompanion copyWith(
{Value<String>? id,
Value<String?>? listid,
Value<String?>? text$,
Value<bool>? completed,
Value<int>? rowid}) {
return TodoCompanion(
id: id ?? this.id,
listid: listid ?? this.listid,
text$: text$ ?? this.text$,
completed: completed ?? this.completed,
rowid: rowid ?? this.rowid,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<String>(id.value);
}
if (listid.present) {
map['listid'] = Variable<String>(listid.value);
}
if (text$.present) {
map['text'] = Variable<String>(text$.value);
}
if (completed.present) {
map['completed'] = Variable<bool>(completed.value);
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('TodoCompanion(')
..write('id: $id, ')
..write('listid: $listid, ')
..write('text\$: ${text$}, ')
..write('completed: $completed, ')
..write('rowid: $rowid')
..write(')'))
.toString();
}
}
class $TodoCategoriesTable extends TodoCategories
with TableInfo<$TodoCategoriesTable, TodoCategory> {
@override
@ -685,6 +949,7 @@ class $TodoItemWithCategoryNameViewView extends ViewInfo<
abstract class _$Database extends GeneratedDatabase {
_$Database(QueryExecutor e) : super(e);
late final $TodoTable todo = $TodoTable(this);
late final $TodoCategoriesTable todoCategories = $TodoCategoriesTable(this);
late final $TodoItemsTable todoItems = $TodoItemsTable(this);
late final $TodoCategoryItemCountView todoCategoryItemCount =
@ -696,5 +961,5 @@ abstract class _$Database extends GeneratedDatabase {
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities =>
[todoCategories, todoItems, todoCategoryItemCount, customViewName];
[todo, todoCategories, todoItems, todoCategoryItemCount, customViewName];
}

View File

@ -143,7 +143,7 @@ class DataClassWriter {
for (final column in columns) {
final getter = column.nameInDart;
final jsonKey = column.getJsonKey(scope.options);
final jsonKeyString = asDartLiteral(column.getJsonKey(scope.options));
String deserialized;
final typeConverter = column.typeConverter;
@ -154,13 +154,14 @@ class DataClassWriter {
type = '$type?';
}
final fromConverter = "serializer.fromJson<$type>(json['$jsonKey'])";
final fromConverter =
"serializer.fromJson<$type>(json[$jsonKeyString])";
final converterField = _converter(column);
deserialized = '$converterField.fromJson($fromConverter)';
} else {
final type = _columnType(column);
deserialized = "serializer.fromJson<$type>(json['$jsonKey'])";
deserialized = "serializer.fromJson<$type>(json[$jsonKeyString])";
}
_buffer.write('$getter: $deserialized,');
@ -186,7 +187,7 @@ class DataClassWriter {
'return <String, dynamic>{\n');
for (final column in columns) {
final name = column.getJsonKey(scope.options);
final nameLiteral = asDartLiteral(column.getJsonKey(scope.options));
final getter = column.nameInDart;
final needsThis = getter == 'serializer';
var value = needsThis ? 'this.$getter' : getter;
@ -199,7 +200,7 @@ class DataClassWriter {
dartType = _jsonType(column);
}
_buffer.write("'$name': serializer.toJson<$dartType>($value),");
_buffer.write("$nameLiteral: serializer.toJson<$dartType>($value),");
}
_buffer.write('};}');

View File

@ -454,10 +454,11 @@ class TableWriter extends TableOrViewWriter {
void _writeColumnVerificationMeta(DriftColumn column) {
if (!_skipVerification) {
final meta = emitter.drift('VerificationMeta');
final arg = asDartLiteral(column.nameInDart);
buffer
..write('static const $meta ${_fieldNameForColumnMeta(column)} = ')
..writeln("const $meta('${column.nameInDart}');");
..writeln("const $meta($arg);");
}
}

View File

@ -26,7 +26,13 @@ void overrideToString(
for (var i = 0; i < properties.length; i++) {
final property = properties[i];
into.write("..write('$property: \$$property");
if (property.contains(r'$')) {
final asKey = property.replaceAll('\$', '\\\$');
into.write("..write('$asKey: \${$property}");
} else {
into.write("..write('$property: \$$property");
}
if (i != properties.length - 1) into.write(', ');
into.write("')");

View File

@ -5,6 +5,7 @@ import 'package:sqlparser/sqlparser.dart' as sql;
import '../analysis/options.dart';
import '../analysis/results/results.dart';
import '../utils/string_escaper.dart';
import 'import_manager.dart';
import 'queries/sql_writer.dart';
@ -408,6 +409,10 @@ class TextEmitter extends _Node {
void writeSqlByDialectMap(sql.AstNode node) {
_writeSqlByDialectMap(node, buffer);
}
void stringLiteral(String contents) {
return buffer.write(asDartLiteral(contents));
}
}
/// Options that are specific to code-generation.

View File

@ -1,6 +1,9 @@
import 'dart:convert';
import 'dart:isolate';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/file_system/memory_file_system.dart';
import 'package:build/build.dart';
import 'package:build/experiments.dart';
import 'package:build_resolvers/build_resolvers.dart';
@ -10,6 +13,7 @@ import 'package:drift_dev/integrations/build.dart';
import 'package:glob/glob.dart';
import 'package:logging/logging.dart';
import 'package:package_config/package_config.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package:test/test.dart';
import 'package:yaml/yaml.dart';
@ -190,3 +194,34 @@ class _TrackingAssetReader implements AssetReader {
return _inner.readAsString(id, encoding: encoding);
}
}
class IsValidDartFile extends CustomMatcher {
IsValidDartFile(valueOrMatcher)
: super(
'A syntactically-valid Dart source file',
'parsed unit',
valueOrMatcher,
);
@override
Object? featureValueOf(actual) {
final resourceProvider = MemoryResourceProvider();
if (actual is List<int>) {
resourceProvider.newFileWithBytes('/foo.dart', actual);
} else if (actual is String) {
resourceProvider.newFile('/foo.dart', actual);
} else {
throw 'Not a String or a List<int>';
}
return parseFile(
path: '/foo.dart',
featureSet: FeatureSet.fromEnableFlags2(
sdkLanguageVersion: Version(3, 0, 0),
flags: const [],
),
resourceProvider: resourceProvider,
throwIfDiagnostics: true,
).unit;
}
}

View File

@ -1,10 +1,6 @@
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:pub_semver/pub_semver.dart';
import 'package:test/test.dart';
import '../utils.dart';
@ -35,10 +31,10 @@ class Database extends _$Database {}
}, options: options);
checkOutputs(
const {
'a|lib/main.drift.dart': _GeneratesWithoutFinalFields(
{
'a|lib/main.drift.dart': IsValidDartFile(_WithoutFinalFields(
{'User', 'UsersCompanion', 'SomeQueryResult'},
),
)),
},
writer.dartOutputs,
writer.writer,
@ -46,10 +42,10 @@ class Database extends _$Database {}
}, tags: 'analyzer');
}
class _GeneratesWithoutFinalFields extends Matcher {
class _WithoutFinalFields extends Matcher {
final Set<String> expectedWithoutFinals;
const _GeneratesWithoutFinalFields(this.expectedWithoutFinals);
const _WithoutFinalFields(this.expectedWithoutFinals);
@override
Description describe(Description description) {
@ -58,28 +54,15 @@ class _GeneratesWithoutFinalFields extends Matcher {
}
@override
bool matches(dynamic desc, Map matchState) {
bool matches(Object? 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';
final parsed = desc;
if (parsed is! CompilationUnit) {
matchState['desc'] = 'Could not be parsed';
return false;
}
final parsed = parseFile(
path: '/foo.dart',
featureSet: FeatureSet.fromEnableFlags2(
sdkLanguageVersion: Version(2, 12, 0),
flags: const [],
),
resourceProvider: resourceProvider,
throwIfDiagnostics: true,
).unit;
final remaining = expectedWithoutFinals.toSet();
final definedClasses = parsed.declarations.whereType<ClassDeclaration>();

View File

@ -120,4 +120,33 @@ CREATE VIEW a AS SELECT nullif(bar, '') FROM foo;
}, result.dartOutputs, result.writer);
},
);
test('generates valid code for columns containing dollar signs', () async {
final result = await emulateDriftBuild(
inputs: {
'a|lib/a.dart': r'''
import 'package:drift/drift.dart';
class Todo extends Table {
TextColumn get id => text()();
TextColumn get listid => text().nullable()();
TextColumn get text$ => text().named('text').nullable()();
BoolColumn get completed => boolean()();
}
@DriftDatabase(tables: [Todo])
class MyDatabase {}
''',
},
logger: loggerThat(neverEmits(anything)),
);
// Make sure we don't generate invalid code in string literals for dollar
// signs in names - https://github.com/simolus3/drift/issues/2761.
checkOutputs(
{'a|lib/a.drift.dart': IsValidDartFile(anything)},
result.dartOutputs,
result.writer,
);
});
}