Skip to content

Commit

Permalink
Backlink support (#996)
Browse files Browse the repository at this point in the history
* Add Backlink annotation

* Support back-links in generator

* Test back-link generator support

* Support backlinks

* Backlink tests

* Update CHANGELOG

* Harden generator wrt. backlinks

* Handle @mapto on backlink targets

* Increase analyzer constraint to 4.7.0^

* Apply suggestions from code review

Co-authored-by: blagoev <[email protected]>
  • Loading branch information
nielsenko and blagoev authored Oct 28, 2022
1 parent defa102 commit fcc825b
Show file tree
Hide file tree
Showing 36 changed files with 597 additions and 80 deletions.
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"dart.lineLength": 160,
"cSpell.words": [
"apikeys",
"backlinks",
"BEGINSWITH",
"bson",
"deallocated",
Expand All @@ -31,7 +32,8 @@
"unmanaged",
"upsert",
"usercode",
"userdata"
"userdata",
"writeln"
],
"cmake.statusbar.advanced": {
"ctest": {
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* Added `User.functions`. This is the entry point for calling Atlas App functions. Functions allow you to define and execute server-side logic for your application. Atlas App functions are created on the server, written in modern JavaScript (ES6+) and executed in a serverless manner. When you call a function, you can dynamically access components of the current application as well as information about the request to execute the function and the logged in user that sent the request. ([#973](https://github.com/realm/realm-dart/pull/973))
* Support results of primitives, ie. `RealmResult<int>`. (Issue [#162](https://github.com/realm/realm-dart/issues/162))
* Support notifications on all managed realm lists, including list of primitives, ie. `RealmList<int>.changes` is supported. ([#893](https://github.com/realm/realm-dart/pull/893))
* Support named backlinks on realm models. You can now add and annotate a realm object iterator field with `@Backlink(#fieldName)`. ([#996](https://github.com/realm/realm-dart/pull/996))

### Fixed
* Fixed a wrong mapping for `AuthProviderType` returned by `User.provider` for google, facebook and apple credentials.
Expand Down
8 changes: 8 additions & 0 deletions common/lib/src/realm_common_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,11 @@ class Indexed {
class Ignored {
const Ignored();
}

/// Indicates that the field it decorates is the inverse end of a relationship.
/// {@category Annotations}
class Backlink {
/// The name of the field in the other class that links to this class.
final Symbol fieldName;
const Backlink(this.fieldName);
}
18 changes: 11 additions & 7 deletions generator/lib/src/class_element_ex.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,7 @@ extension on Iterable<FieldElement> {
element: field,
primarySpan: field.span!,
primaryLabel: 'second primary key',
secondarySpans: {
...{for (final p in primaryKeys..removeAt(1)) p.fieldElement.span!: ''},
},
secondarySpans: {for (final p in primaryKeys..removeAt(1)) p.fieldElement.span!: ''},
);
}
}
Expand Down Expand Up @@ -87,8 +85,13 @@ extension ClassElementEx on ClassElement {
}

if (!modelName.endsWith(suffix)) {
throw RealmInvalidGenerationSourceError('Missing suffix on realm model name',
element: this, primarySpan: span, primaryLabel: 'missing suffix', todo: 'Align class name to have suffix $suffix,');
throw RealmInvalidGenerationSourceError(
'Missing suffix on realm model name',
element: this,
primarySpan: span,
primaryLabel: 'missing suffix',
todo: 'Align class name to have suffix $suffix,',
);
}

// Remove suffix and prefix, if any.
Expand Down Expand Up @@ -149,7 +152,8 @@ extension ClassElementEx on ClassElement {

final objectType = ObjectType.values[modelInfo.value.getField('type')!.getField('index')!.toIntValue()!];

final mappedFields = fields.realmInfo.toList();
// Realm Core requires computed properties at the end so we sort them at generation time versus doing it at runtime every time.
final mappedFields = fields.realmInfo.toList()..sort((a, b) => a.isComputed ^ b.isComputed ? (a.isComputed ? 1 : -1) : -1);

if (objectType == ObjectType.embeddedObject && mappedFields.any((field) => field.isPrimaryKey)) {
final pkSpan = fields.firstWhere((field) => field.realmInfo?.isPrimaryKey == true).span;
Expand All @@ -167,7 +171,7 @@ extension ClassElementEx on ClassElement {
} catch (e, s) {
// Fallback. Not perfect, but better than just forwarding original error.
throw RealmInvalidGenerationSourceError(
'$e \n $s',
'$e\n$s',
todo: //
'Unexpected error. Please open an issue on: '
'https://github.com/realm/realm-dart',
Expand Down
23 changes: 16 additions & 7 deletions generator/lib/src/dart_type_ex.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import 'dart:ffi';
import 'dart:typed_data';

import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:realm_common/realm_common.dart';
import 'package:realm_generator/src/pseudo_type.dart';
Expand All @@ -30,12 +31,12 @@ extension DartTypeEx on DartType {
bool isExactly<T>() => TypeChecker.fromRuntime(T).isExactlyType(this);

bool get isRealmAny => const TypeChecker.fromRuntime(RealmAny).isAssignableFromType(this);
bool get isRealmBacklink => false; // TODO: Implement Backlink support https://github.com/realm/realm-dart/issues/693
bool get isRealmCollection => realmCollectionType != RealmCollectionType.none;
bool get isRealmModel => element != null ? realmModelChecker.annotationsOfExact(element!).isNotEmpty : false;
bool get isRealmModel => element2 != null ? realmModelChecker.annotationsOfExact(element2!).isNotEmpty : false;

bool get isNullable => session.typeSystem.isNullable(this);
DartType get asNonNullable => session.typeSystem.promoteToNonNull(this);
DartType get asNullable => session.typeSystem.leastUpperBound(this, session.typeProvider.nullType);

RealmCollectionType get realmCollectionType {
if (isDartCoreSet) return RealmCollectionType.set;
Expand All @@ -49,10 +50,11 @@ extension DartTypeEx on DartType {
DartType? get nullIfDynamic => isDynamic ? null : this;

DartType get basicType {
if (isRealmCollection) {
return (this as ParameterizedType).typeArguments.last;
final self = this;
if (self is ParameterizedType && (isRealmCollection || isDartCoreIterable)) {
return self.typeArguments.last;
}
return asNonNullable;
return this;
}

String get basicMappedName => basicType.mappedName;
Expand All @@ -61,9 +63,9 @@ extension DartTypeEx on DartType {
final self = this;
if (isRealmCollection) {
if (self is ParameterizedType) {
final provider = session.typeProvider;
final mapped = self.typeArguments.last.mappedType;
if (self != mapped) {
final provider = session.typeProvider;
if (self.isDartCoreList) {
final mappedList = provider.listType(mapped);
return PseudoType('Realm${mappedList.getDisplayString(withNullability: true)}', nullabilitySuffix: mappedList.nullabilitySuffix);
Expand All @@ -78,6 +80,13 @@ extension DartTypeEx on DartType {
}
}
}
} else if (isDartCoreIterable) {
if (self is ParameterizedType) {
final mapped = self.typeArguments.last.mappedType;
if (self != mapped) {
return PseudoType('RealmResults<${mapped.basicMappedName}>', nullabilitySuffix: NullabilitySuffix.none);
}
}
} else if (isRealmModel) {
return PseudoType(
getDisplayString(withNullability: false).replaceAll(session.prefix, ''),
Expand Down Expand Up @@ -105,7 +114,7 @@ extension DartTypeEx on DartType {
if (isDartCoreNum || isDartCoreDouble) return RealmPropertyType.double;
if (isExactly<Decimal128>()) return RealmPropertyType.decimal128;
if (isRealmModel) return RealmPropertyType.object;
if (isRealmBacklink) return RealmPropertyType.linkingObjects;
if (isDartCoreIterable) return RealmPropertyType.linkingObjects;
if (isExactly<ObjectId>()) return RealmPropertyType.objectid;
if (isExactly<Uuid>()) return RealmPropertyType.uuid;

Expand Down
1 change: 0 additions & 1 deletion generator/lib/src/element.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import 'package:realm_generator/src/expanded_context_span.dart';
import 'package:source_gen/source_gen.dart';
import 'package:source_span/source_span.dart';

import 'annotation_value.dart';
import 'class_element_ex.dart';
import 'error.dart';
import 'field_element_ex.dart';
Expand Down
2 changes: 1 addition & 1 deletion generator/lib/src/error.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class RealmInvalidGenerationSourceError extends InvalidGenerationSourceError {
color = color ?? session.color,
super(message, todo: todo, element: element) {
if (element is FieldElement || element is ConstructorElement) {
final classElement = element.enclosingElement!;
final classElement = element.enclosingElement3!;
this.secondarySpans.addAll({
classElement.span!: "in realm model for '${session.mapping.entries.where((e) => e.value == classElement).singleOrNull?.key}'",
});
Expand Down
75 changes: 63 additions & 12 deletions generator/lib/src/field_element_ex.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:build/build.dart';
import 'package:realm_common/realm_common.dart';
import 'package:realm_generator/src/expanded_context_span.dart';
import 'package:realm_generator/src/pseudo_type.dart';
import 'package:realm_generator/src/utils.dart';
import 'package:source_gen/source_gen.dart';
import 'package:source_span/source_span.dart';

Expand All @@ -33,7 +35,6 @@ import 'format_spans.dart';
import 'realm_field_info.dart';
import 'session.dart';
import 'type_checkers.dart';
import 'utils.dart';

extension FieldElementEx on FieldElement {
FieldDeclaration get declarationAstNode => getDeclarationFromElement(this)!.node.parent!.parent as FieldDeclaration;
Expand All @@ -44,9 +45,11 @@ extension FieldElementEx on FieldElement {

AnnotationValue? get indexedInfo => annotationInfoOfExact(indexedChecker);

AnnotationValue? get backlinkInfo => annotationInfoOfExact(backlinkChecker);

TypeAnnotation? get typeAnnotation => declarationAstNode.fields.type;

Expression? get initializerExpression => declarationAstNode.fields.variables.singleWhere((v) => v.name.name == name).initializer;
Expression? get initializerExpression => declarationAstNode.fields.variables.singleWhere((v) => v.name2.toString() == name).initializer;

FileSpan? typeSpan(SourceFile file) => ExpandedContextSpan(
ExpandedContextSpan(
Expand Down Expand Up @@ -76,6 +79,7 @@ extension FieldElementEx on FieldElement {

final primaryKey = primaryKeyInfo;
final indexed = indexedInfo;
final backlink = backlinkInfo;

// Check for as-of-yet unsupported type
if (type.isDartCoreSet || //
Expand Down Expand Up @@ -112,7 +116,7 @@ extension FieldElementEx on FieldElement {
//
// However, this may change in the future. Either as the dart language team change this
// blemish. Or perhaps we can avoid the late modifier, once static meta programming lands
// in dart. Therefor we keep the code outcommented for later.
// in dart. Therefore we keep the code out-commented for later.
/*
if (!isFinal) {
throw RealmInvalidGenerationSourceError(
Expand Down Expand Up @@ -146,12 +150,14 @@ extension FieldElementEx on FieldElement {
);
}

String? linkOriginProperty;

// Validate field type
final modelSpan = enclosingElement.span!;
final modelSpan = enclosingElement3.span!;
final file = modelSpan.file;
final realmType = type.realmType;
if (realmType == null) {
final notARealmTypeSpan = type.element?.span;
final notARealmTypeSpan = type.element2?.span;
String todo;
if (notARealmTypeSpan != null) {
todo = //
Expand All @@ -169,18 +175,19 @@ extension FieldElementEx on FieldElement {
primarySpan: typeSpan(file),
primaryLabel: '$modelTypeName is not a realm model type',
secondarySpans: {
modelSpan: "in realm model '${enclosingElement.displayName}'",
modelSpan: "in realm model '${enclosingElement3.displayName}'",
// may go both above and below, or stem from another file
if (notARealmTypeSpan != null) notARealmTypeSpan: ''
},
todo: todo,
);
} else {
// Validate collections
if (type.isRealmCollection) {
// Validate collections and backlinks
if (type.isRealmCollection || backlink != null) {
final typeDescription = type.isRealmCollection ? 'collections' : 'backlinks';
if (type.isNullable) {
throw RealmInvalidGenerationSourceError(
'Realm collections cannot be nullable',
'Realm $typeDescription cannot be nullable',
primarySpan: typeSpan(file),
primaryLabel: 'is nullable',
todo: '',
Expand All @@ -189,16 +196,59 @@ extension FieldElementEx on FieldElement {
}
final itemType = type.basicType;
if (itemType.isRealmModel && itemType.isNullable) {
throw RealmInvalidGenerationSourceError('Nullable realm objects are not allowed in collections',
throw RealmInvalidGenerationSourceError('Nullable realm objects are not allowed in $typeDescription',
primarySpan: typeSpan(file),
primaryLabel: 'which has a nullable realm object element type',
element: this,
todo: 'Ensure element type is non-nullable');
}
}

// Validate backlinks
if (backlink != null) {
if (!type.isDartCoreIterable || !(type as ParameterizedType).typeArguments.first.isRealmModel) {
throw RealmInvalidGenerationSourceError(
'Backlink must be an iterable of realm objects',
primarySpan: typeSpan(file),
primaryLabel: '$modelTypeName is not an iterable of realm objects',
todo: '',
element: this,
);
}

final sourceFieldName = backlink.value.getField('fieldName')?.toSymbolValue();
final sourceType = (type as ParameterizedType).typeArguments.first;
final sourceField = (sourceType.element2 as ClassElement?)?.fields.where((f) => f.name == sourceFieldName).singleOrNull;

if (sourceField == null) {
throw RealmInvalidGenerationSourceError(
'Backlink must point to a valid field',
primarySpan: typeSpan(file),
primaryLabel: '$sourceType does not have a field named $sourceFieldName',
todo: '',
element: this,
);
}

final thisType = (enclosingElement3 as ClassElement).thisType;
final linkType = thisType.asNullable;
final listOf = session.typeProvider.listType(thisType);
if (sourceField.type != linkType && sourceField.type != listOf) {
throw RealmInvalidGenerationSourceError(
'Incompatible backlink type',
primarySpan: typeSpan(file),
primaryLabel: "$sourceType.$sourceFieldName is not a '$linkType' or '$listOf'",
todo: '',
element: this,
);
}

// everything is kosher, just need to account for @MapTo!
linkOriginProperty = sourceField.annotationInfoOfExact(mapToChecker)?.value.getField('name')?.toStringValue() ?? sourceField.name;
}

// Validate object references
else if (realmType == RealmPropertyType.object) {
else if (realmType == RealmPropertyType.object && !type.isRealmCollection) {
if (!type.isNullable) {
throw RealmInvalidGenerationSourceError(
'Realm object references must be nullable',
Expand All @@ -217,13 +267,14 @@ extension FieldElementEx on FieldElement {
isPrimaryKey: primaryKey != null,
mapTo: remappedRealmName,
realmType: realmType,
linkOriginProperty: linkOriginProperty,
);
} on InvalidGenerationSourceError catch (_) {
rethrow;
} catch (e, s) {
// Fallback. Not perfect, but better than just forwarding original error.
throw RealmInvalidGenerationSourceError(
'$e \n $s',
'$e\n$s',
todo: //
'Unexpected error. Please open an issue on: '
'https://github.com/realm/realm-dart',
Expand Down
2 changes: 1 addition & 1 deletion generator/lib/src/format_spans.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ String formatSpans(
color: color,
);
buffer
..write('\n' * 2 + 'in: ')
..write('${'\n' * 2}in: ')
..writeln(span.start.toolString)
..write(formatted);
}
Expand Down
10 changes: 5 additions & 5 deletions generator/lib/src/pseudo_type.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ class PseudoType extends TypeImpl {

// Private Symbols are suffixed with a secret '@<some int>'
// .. hence this ugly trick ヽ(ಠ_ಠ)ノ
final _writeSymbol = im.type.instanceMembers.keys.firstWhere((m) => '$m'.contains('"_write"'));
im.invoke(_writeSymbol, <dynamic>[_name]); // #_write won't work
final writeSymbol = im.type.instanceMembers.keys.firstWhere((m) => '$m'.contains('"_write"'));
im.invoke(writeSymbol, <dynamic>[_name]); // #_write won't work

final _writeNullability = im.type.instanceMembers.keys.firstWhere((m) => '$m'.contains('"_writeNullability"'));
im.invoke(_writeNullability, <dynamic>[nullabilitySuffix]); // #_writeNullability won't work
final writeNullability = im.type.instanceMembers.keys.firstWhere((m) => '$m'.contains('"_writeNullability"'));
im.invoke(writeNullability, <dynamic>[nullabilitySuffix]); // #_writeNullability won't work
}

@override
Expand All @@ -52,5 +52,5 @@ class PseudoType extends TypeImpl {
}

@override
Element? get element2 => throw UnimplementedError();
Element? get element2 => null;
}
Loading

0 comments on commit fcc825b

Please sign in to comment.