Skip to content

Commit 849509e

Browse files
Merge branch 'helpful-generator-errors'
2 parents af8936b + ba25c58 commit 849509e

File tree

2 files changed

+61
-46
lines changed

2 files changed

+61
-46
lines changed

generator/lib/src/entity_resolver.dart

Lines changed: 60 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ class EntityResolver extends Builder {
5151
}
5252

5353
ModelEntity generateForAnnotatedElement(
54-
Element element, ConstantReader annotation) {
55-
if (element is! ClassElement) {
54+
Element classElement, ConstantReader annotation) {
55+
if (classElement is! ClassElement) {
5656
throw InvalidGenerationSourceError(
57-
"entity ${element.name}: annotated element isn't a class");
57+
"Entity '${classElement.name}': annotated element must be a class.");
5858
}
5959

6060
// process basic entity (note that allModels.createEntity is not used, as the entity will be merged)
@@ -63,23 +63,23 @@ class EntityResolver extends Builder {
6363
final entity = ModelEntity.create(
6464
IdUid(0, entityUid.isNull ? 0 : entityUid.intValue),
6565
entityRealClass.isNull
66-
? element.name
66+
? classElement.name
6767
: entityRealClass.typeValue.element!.name!,
6868
null,
6969
uidRequest: !entityUid.isNull && entityUid.intValue == 0);
7070

71-
if (_syncChecker.hasAnnotationOfExact(element)) {
71+
if (_syncChecker.hasAnnotationOfExact(classElement)) {
7272
entity.flags |= OBXEntityFlags.SYNC_ENABLED;
7373
}
7474

7575
log.info(entity);
7676

77-
entity.constructorParams = constructorParams(findConstructor(element));
78-
entity.nullSafetyEnabled = nullSafetyEnabled(element);
77+
entity.constructorParams = constructorParams(findConstructor(classElement));
78+
entity.nullSafetyEnabled = nullSafetyEnabled(classElement);
7979
if (!entity.nullSafetyEnabled) {
8080
log.warning(
81-
"Entity ${entity.name} is in a library/application that doesn't use null-safety"
82-
' - consider increasing your SDK version to Flutter 2.0/Dart 2.12');
81+
"Entity '${entity.name}' is in a package that doesn't use null-safety"
82+
' - consider increasing your SDK version to Flutter 2.0/Dart 2.12.');
8383
}
8484

8585
// Make sure all stored fields are writable when reading object from DB.
@@ -88,7 +88,7 @@ class EntityResolver extends Builder {
8888
// * don't have a corresponding argument in the constructor.
8989
// Note: `.correspondingSetter == null` is also true for `final` fields.
9090
final readOnlyFields = <String>{};
91-
for (var f in element.accessors) {
91+
for (var f in classElement.accessors) {
9292
if (f.isGetter &&
9393
f.correspondingSetter == null &&
9494
!entity.constructorParams
@@ -98,19 +98,19 @@ class EntityResolver extends Builder {
9898
}
9999

100100
// read all suitable annotated properties
101-
for (var f in element.fields) {
101+
for (var f in classElement.fields) {
102102
if (_transientChecker.hasAnnotationOfExact(f)) {
103-
log.info(' skipping property ${f.name} (annotated with @Transient)');
103+
log.info(" Skipping property '${f.name}': annotated with @Transient.");
104104
continue;
105105
}
106106

107107
if (readOnlyFields.contains(f.name) && !isRelationField(f)) {
108-
log.info(' skipping read-only/getter ${f.name}');
108+
log.info(" Skipping property '${f.name}': is read-only/getter.");
109109
continue;
110110
}
111111

112112
if (f.isPrivate) {
113-
log.info(' skipping private field ${f.name}');
113+
log.info(" Skipping property '${f.name}': is private.");
114114
continue;
115115
}
116116

@@ -160,24 +160,26 @@ class EntityResolver extends Builder {
160160
} else if (dartType.element!.name == 'DateTime') {
161161
fieldType = OBXPropertyType.Date;
162162
log.warning(
163-
" DateTime property '${f.name}' in entity '${element.name}' is stored and read using millisecond precision. "
163+
" DateTime property '${f.name}' in entity '${classElement.name}' is stored and read using millisecond precision. "
164164
'To silence this warning, add an explicit type using @Property(type: PropertyType.date) or @Property(type: PropertyType.dateNano) annotation.');
165165
} else if (isToOneRelationField(f)) {
166166
fieldType = OBXPropertyType.Relation;
167167
} else if (isToManyRelationField(f)) {
168168
isToManyRel = true;
169169
} else {
170170
log.warning(
171-
" skipping property '${f.name}' in entity '${element.name}', as it has an unsupported type: '$dartType'");
171+
" Skipping property '${f.name}': type '$dartType' not supported,"
172+
" consider creating a relation for @Entity types (https://docs.objectbox.io/relations),"
173+
" or replace with getter/setter converting to a supported type (https://docs.objectbox.io/advanced/custom-types).");
172174
continue;
173175
}
174176
}
175177

176178
String? relTargetName;
177179
if (isRelationField(f)) {
178180
if (f.type is! ParameterizedType) {
179-
log.severe(
180-
" invalid relation property '${f.name}' in entity '${element.name}' - must use ToOne/ToMany<TargetEntity>");
181+
log.severe(" Skipping property '${f.name}': invalid relation type, "
182+
"use a type like ToOne<TargetEntity> or ToMany<TargetEntity>.");
181183
continue;
182184
}
183185
relTargetName =
@@ -188,7 +190,7 @@ class EntityResolver extends Builder {
188190
if (backlinkAnnotations.isNotEmpty) {
189191
if (!isToManyRel) {
190192
log.severe(
191-
' invalid use of @Backlink() annotation - may only be used on a ToMany<> field');
193+
" Skipping property '${f.name}': @Backlink() may only be used with ToMany.");
192194
continue;
193195
}
194196
final backlinkField =
@@ -224,7 +226,7 @@ class EntityResolver extends Builder {
224226
}
225227

226228
// Index and unique annotation.
227-
processAnnotationIndexUnique(f, fieldType, element, prop);
229+
processAnnotationIndexUnique(f, fieldType, classElement, prop);
228230

229231
// for code generation
230232
prop.dartFieldType =
@@ -233,63 +235,65 @@ class EntityResolver extends Builder {
233235
}
234236
}
235237

236-
processIdProperty(entity);
238+
processIdProperty(entity, classElement);
237239

238240
// We need to check that the ID field is writable. Otherwise, generated code
239241
// for `setId()` won't compile. The only exception is when user uses
240242
// self-assigned IDs, then a different setter will be generated - one that
241243
// checks the ID being set is already the same, otherwise it must throw.
242-
final idField = element.fields
244+
final idField = classElement.fields
243245
.singleWhere((FieldElement f) => f.name == entity.idProperty.name);
244246
if (idField.setter == null) {
245247
if (!entity.idProperty.hasFlag(OBXPropertyFlags.ID_SELF_ASSIGNABLE)) {
246248
throw InvalidGenerationSourceError(
247-
"Entity ${entity.name} has an ID field '${idField.name}' that is "
248-
'not assignable (that usually means it is declared final). '
249-
"This won't work because ObjectBox needs to be able to assign "
250-
'an ID after inserting a new object (if the given ID was zero). '
251-
'If you want to assign IDs manually instead, you can annotate the '
252-
"field '${idField.name}' with `@Id(assignable: true)`. Otherwise "
253-
'please provide a setter or remove the `final` keyword.');
249+
"@Id field '${idField.name}' must be writable:"
250+
" ObjectBox uses it to set the assigned ID after inserting a new object,"
251+
" provide a setter or remove the 'final' keyword."
252+
" If your code needs to assign IDs itself,"
253+
" see https://docs.objectbox.io/advanced/object-ids#self-assigned-object-ids.",
254+
element: idField);
254255
} else {
255256
// We need to get the information to code generator (code_chunks.dart).
256257
entity.idProperty.fieldIsReadOnly = true;
257258
}
258259
}
259260

260261
// Verify there is at most 1 unique property with REPLACE strategy.
261-
ensureSingleUniqueReplace(entity);
262+
ensureSingleUniqueReplace(entity, classElement);
262263
// If sync enabled, verify all unique properties use REPLACE strategy.
263-
ifSyncEnsureAllUniqueAreReplace(entity);
264+
ifSyncEnsureAllUniqueAreReplace(entity, classElement);
264265

265266
entity.properties.forEach((p) => log.info(' $p'));
266267

267268
return entity;
268269
}
269270

270-
void processIdProperty(ModelEntity entity) {
271+
void processIdProperty(ModelEntity entity, ClassElement classElement) {
271272
// check properties explicitly annotated with @Id()
272273
final annotated =
273274
entity.properties.where((p) => p.hasFlag(OBXPropertyFlags.ID));
274275
if (annotated.length > 1) {
276+
final names = annotated.map((e) => e.name).join(", ");
275277
throw InvalidGenerationSourceError(
276-
'entity ${entity.name}: multiple fields annotated with Id(), there may only be one');
278+
"Entity '${entity.name}': multiple fields ($names) annotated with @Id(), there may only be one.",
279+
element: classElement);
277280
}
278281

279282
if (annotated.length == 1) {
280283
if (annotated.first.type != OBXPropertyType.Long) {
281284
throw InvalidGenerationSourceError(
282-
"entity ${entity.name}: Id() annotated property has invalid type, expected 'int'");
285+
"Entity '${entity.name}': @Id() property must be 'int'."
286+
" If you need to use other types, see https://docs.objectbox.io/entity-annotations#object-ids-id.",
287+
element: classElement);
283288
}
284289
} else {
285290
// if there are no annotated props, try to find one by name & type
286291
final candidates = entity.properties.where((p) =>
287292
p.name.toLowerCase() == 'id' && p.type == OBXPropertyType.Long);
288293
if (candidates.length != 1) {
289294
throw InvalidGenerationSourceError(
290-
'entity ${entity.name}: ID property not found - either define'
291-
' an integer field named ID/id/... (case insensitive) or add'
292-
' @Id annotation to any integer field');
295+
"Entity '${entity.name}': no @Id() property found, add an int field annotated with @Id().",
296+
element: classElement);
293297
}
294298
candidates.first.flags |= OBXPropertyFlags.ID;
295299
}
@@ -308,19 +312,22 @@ class EntityResolver extends Builder {
308312

309313
final indexAnnotation = _indexChecker.firstAnnotationOfExact(f);
310314
final uniqueAnnotation = _uniqueChecker.firstAnnotationOfExact(f);
311-
if (indexAnnotation == null && uniqueAnnotation == null) return null;
315+
if (indexAnnotation == null && uniqueAnnotation == null) return;
312316

313317
// Throw if property type does not support any index.
314318
if (fieldType == OBXPropertyType.Float ||
315319
fieldType == OBXPropertyType.Double ||
316320
fieldType == OBXPropertyType.ByteVector) {
317321
throw InvalidGenerationSourceError(
318-
"entity ${elementBare.name}: @Index/@Unique is not supported for type '${f.type}' of field '${f.name}'");
322+
"Entity '${elementBare.name}': @Index/@Unique is not supported for type '${f.type}' of field '${f.name}'.",
323+
element: f);
319324
}
320325

321326
if (prop.hasFlag(OBXPropertyFlags.ID)) {
322327
throw InvalidGenerationSourceError(
323-
'entity ${elementBare.name}: @Index/@Unique is not supported for ID field ${f.name}. IDs are unique by definition and automatically indexed');
328+
"Entity '${elementBare.name}': @Index/@Unique is not supported for @Id field '${f.name}'."
329+
" IDs are unique by definition and automatically indexed.",
330+
element: f);
324331
}
325332

326333
// If available use index type from annotation.
@@ -343,7 +350,8 @@ class EntityResolver extends Builder {
343350
if (!supportsHashIndex &&
344351
(indexType == IndexType.hash || indexType == IndexType.hash64)) {
345352
throw InvalidGenerationSourceError(
346-
"entity ${elementBare.name}: a hash index is not supported for type '${f.type}' of field '${f.name}'");
353+
"Entity '${elementBare.name}': a hash index is not supported for type '${f.type}' of field '${f.name}'",
354+
element: f);
347355
}
348356

349357
if (uniqueAnnotation != null && !uniqueAnnotation.isNull) {
@@ -369,28 +377,34 @@ class EntityResolver extends Builder {
369377
break;
370378
default:
371379
throw InvalidGenerationSourceError(
372-
'entity ${elementBare.name}: invalid index type: $indexType');
380+
"Entity '${elementBare.name}': index type $indexType not supported.",
381+
element: f);
373382
}
374383
}
375384

376-
void ensureSingleUniqueReplace(ModelEntity entity) {
385+
void ensureSingleUniqueReplace(
386+
ModelEntity entity, ClassElement classElement) {
377387
final uniqueReplaceProps = entity.properties
378388
.where((p) => p.hasFlag(OBXPropertyFlags.UNIQUE_ON_CONFLICT_REPLACE));
379389
if (uniqueReplaceProps.length > 1) {
380390
throw InvalidGenerationSourceError(
381-
"ConflictStrategy.replace can only be used on a single property, but found multiple in '${entity.name}':\n ${uniqueReplaceProps.join('\n ')}");
391+
"ConflictStrategy.replace can only be used on a single property, but found multiple in '${entity.name}':\n ${uniqueReplaceProps.join('\n ')}",
392+
element: classElement);
382393
}
383394
}
384395

385-
void ifSyncEnsureAllUniqueAreReplace(ModelEntity entity) {
396+
void ifSyncEnsureAllUniqueAreReplace(
397+
ModelEntity entity, ClassElement classElement) {
386398
if (!entity.hasFlag(OBXEntityFlags.SYNC_ENABLED)) return;
387399
final uniqueButNotReplaceProps = entity.properties.where((p) {
388400
return p.hasFlag(OBXPropertyFlags.UNIQUE) &&
389401
!p.hasFlag(OBXPropertyFlags.UNIQUE_ON_CONFLICT_REPLACE);
390402
});
391403
if (uniqueButNotReplaceProps.isNotEmpty) {
392404
throw InvalidGenerationSourceError(
393-
"Synced entities must use @Unique(onConflict: ConflictStrategy.replace) on all unique properties, but found others in '${entity.name}':\n ${uniqueButNotReplaceProps.join('\n ')}");
405+
"Synced entities must use @Unique(onConflict: ConflictStrategy.replace) on all unique properties,"
406+
" but found others in '${entity.name}':\n ${uniqueButNotReplaceProps.join('\n ')}",
407+
element: classElement);
394408
}
395409
}
396410

objectbox/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## latest
22

3+
* Generator messages should be more helpful, provide code location when possible and link to docs.
34

45
## 1.6.1 (2022-08-22)
56

0 commit comments

Comments
 (0)