Skip to content

Migrate the generated AOT renderers to null safety #2874

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Dec 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5,562 changes: 2,483 additions & 3,079 deletions lib/src/generator/templates.aot_renderers_for_html.dart

Large diffs are not rendered by default.

2,436 changes: 1,064 additions & 1,372 deletions lib/src/generator/templates.aot_renderers_for_md.dart

Large diffs are not rendered by default.

38 changes: 19 additions & 19 deletions test/mustachio/aot_compiler_render_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,43 +23,43 @@ void main() {
final sdk = p.dirname(p.dirname(Platform.resolvedExecutable));
final fooCode = '''
class FooBase<T extends Object> {
T baz;
T? baz;
}

class Foo extends FooBase<Baz> {
String s1;
bool b1;
List<int> l1;
@override
Baz baz;
Property1 p1;
String? s1 = '';
bool b1 = false;
List<int> l1 = [];
@override
Baz? baz;
Property1? p1;
}

class Bar {
Foo foo;
String s2;
Baz baz;
bool l1;
Foo? foo;
String? s2;
Baz? baz;
bool? l1;
}

class Baz {
Bar bar;
Bar? bar;
}

class Property1 {
Property2 p2;
Property2? p2;
}

class Property2 with Mixin1 {
String s;
String? s;
}

mixin Mixin1 {
Property3 p3;
Property3? p3;
}

class Property3 {
String s;
String? s;
}
''';
InMemoryAssetWriter writer;
Expand Down Expand Up @@ -100,7 +100,7 @@ $mainCode
'foo',
Uri.directory(tempDir.path),
packageUriRoot: Uri.directory(p.join(tempDir.path, 'lib')),
languageVersion: LanguageVersion(2, 9),
languageVersion: LanguageVersion(2, 12),
)
]);
var dartToolDir = Directory(p.join(tempDir.path, '.dart_tool'))
Expand Down Expand Up @@ -368,7 +368,7 @@ void main() {
'Text {{#bar}}{{bar.foo.baz.bar.foo.s1}}{{/bar}}',
},
'_i1.Baz()..bar = (_i1.Bar()..foo = (_i1.Foo()..s1 = "hello"));'
'baz.bar.foo.baz = baz');
'baz.bar!.foo!.baz = baz');
expect(output, equals('Text hello'));
});

Expand Down Expand Up @@ -461,7 +461,7 @@ line 1, column 9 of package:foo/templates/html/foo.html: Failed to resolve '[s2]
}, '_i1.Bar()..foo = _i1.Foo()'),
throwsA(const TypeMatcher<MustachioResolutionError>()
.having((e) => e.message, 'message', contains('''
line 1, column 8 of package:foo/templates/html/bar.html: Failed to resolve 'x' on Bar while resolving [x] as a property chain on any types in the context chain: context0.foo, after first resolving 'foo' to a property on Foo
line 1, column 8 of package:foo/templates/html/bar.html: Failed to resolve 'x' on Bar while resolving [x] as a property chain on any types in the context chain: context0.foo, after first resolving 'foo' to a property on Foo?
1 │ Text {{foo.x}}
│ ^^^^^
Expand Down
19 changes: 8 additions & 11 deletions test/mustachio/foo.aot_renderers_for_html.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
// the variable is not used; generally when the section is checking if a
// non-bool, non-Iterable field is non-null.
// ignore_for_file: unused_local_variable
// @dart=2.9
// ignore_for_file: non_constant_identifier_names, unnecessary_string_escapes

import 'dart:convert' as _i2;
Expand All @@ -23,7 +22,7 @@ String renderFoo(_i1.Foo context0) {
buffer.write('''

s1: ''');
buffer.writeEscaped(context0.s1.toString());
buffer.writeEscaped(context0.s1?.toString());
buffer.writeln();
buffer.write('''
b1? ''');
Expand All @@ -37,13 +36,11 @@ String renderFoo(_i1.Foo context0) {
buffer.write('''
l1:''');
var context1 = context0.l1;
if (context1 != null) {
for (var context2 in context1) {
buffer.write('''item: ''');
buffer.writeEscaped(context2.toString());
}
for (var context2 in context1) {
buffer.write('''item: ''');
buffer.writeEscaped(context2.toString());
}
if (context0.l1?.isEmpty ?? true) {
if (context0.l1.isEmpty) {
buffer.write('''no items''');
}
buffer.writeln();
Expand All @@ -54,7 +51,7 @@ String renderFoo(_i1.Foo context0) {
buffer.writeln();
buffer.write('''
Baz has a ''');
buffer.writeEscaped(context3.bar.s2.toString());
buffer.writeEscaped(context3.bar!.s2?.toString());
}
if (context0.baz == null) {
buffer.write('''baz is null''');
Expand Down Expand Up @@ -91,7 +88,7 @@ String renderBaz(_i1.Baz context0) {
}

extension on StringBuffer {
void writeEscaped(String value) {
write(_i2.htmlEscape.convert(value));
void writeEscaped(String? value) {
write(_i2.htmlEscape.convert(value ?? ''));
}
}
19 changes: 8 additions & 11 deletions test/mustachio/foo.aot_renderers_for_md.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
// the variable is not used; generally when the section is checking if a
// non-bool, non-Iterable field is non-null.
// ignore_for_file: unused_local_variable
// @dart=2.9
// ignore_for_file: non_constant_identifier_names, unnecessary_string_escapes

import 'dart:convert' as _i2;
Expand All @@ -21,7 +20,7 @@ String renderFoo(_i1.Foo context0) {
buffer.write('''

s1: ''');
buffer.writeEscaped(context0.s1.toString());
buffer.writeEscaped(context0.s1?.toString());
buffer.writeln();
buffer.write('''
b1? ''');
Expand All @@ -35,13 +34,11 @@ b1? ''');
buffer.write('''
l1:''');
var context1 = context0.l1;
if (context1 != null) {
for (var context2 in context1) {
buffer.write('''item: ''');
buffer.writeEscaped(context2.toString());
}
for (var context2 in context1) {
buffer.write('''item: ''');
buffer.writeEscaped(context2.toString());
}
if (context0.l1?.isEmpty ?? true) {
if (context0.l1.isEmpty) {
buffer.write('''no items''');
}
buffer.writeln();
Expand All @@ -52,7 +49,7 @@ baz:''');
buffer.writeln();
buffer.write('''
Baz has a ''');
buffer.writeEscaped(context3.bar.s2.toString());
buffer.writeEscaped(context3.bar!.s2?.toString());
}
if (context0.baz == null) {
buffer.write('''baz is null''');
Expand Down Expand Up @@ -82,7 +79,7 @@ String renderBaz(_i1.Baz context0) {
}

extension on StringBuffer {
void writeEscaped(String value) {
write(_i2.htmlEscape.convert(value));
void writeEscaped(String? value) {
write(_i2.htmlEscape.convert(value ?? ''));
}
}
2 changes: 1 addition & 1 deletion test/mustachio/foo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class FooBase<T extends Object> {
}

class Foo extends FooBase<Baz> {
String s1 = '';
String? s1 = '';
bool b1 = false;
List<int> l1 = [];
@override
Expand Down
2 changes: 1 addition & 1 deletion test/mustachio/foo.runtime_renderers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ class Renderer_Foo extends RendererBase<Foo?> {
renderVariable: (CT_ c, Property<CT_> self,
List<String> remainingNames) =>
self.renderSimpleVariable(c, remainingNames, 'String'),
isNullValue: (CT_ c) => false,
isNullValue: (CT_ c) => c.s1 == null,
renderValue: (CT_ c, RendererBase<CT_> r,
List<MustachioNode> ast, StringSink sink) {
renderSimple(c.s1, ast, r.template, sink,
Expand Down
71 changes: 48 additions & 23 deletions tool/mustachio/codegen_aot_compiler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ Future<String> compileTemplatesToRenderers(
..returns = refer('void')
..name = 'writeEscaped'
..requiredParameters.add(Parameter((b) => b
..type = refer('String')
..type = refer('String?')
..name = 'value'))
..body = refer('write').call([
refer('htmlEscape', 'dart:convert')
.property('convert')
.call([refer('value')])
.call([refer("value ?? ''")])
]).statement))));
});
return DartFormatter().format('''
Expand All @@ -68,7 +68,6 @@ Future<String> compileTemplatesToRenderers(
// the variable is not used; generally when the section is checking if a
// non-bool, non-Iterable field is non-null.
// ignore_for_file: unused_local_variable
// @dart=2.9
// ignore_for_file: non_constant_identifier_names, unnecessary_string_escapes

${library.accept(DartEmitter.scoped(orderDirectives: true))}
Expand Down Expand Up @@ -154,8 +153,7 @@ class _AotCompiler {

Future<List<Method>> _compileToRenderer() async {
if (_contextStack.isEmpty) {
var contextName = 'context0';
var contextVariable = _VariableLookup(_contextType, contextName);
var contextVariable = _VariableLookup(_contextType, 'context0');
_contextStack.push(contextVariable);
_contextNameCounter++;
}
Expand Down Expand Up @@ -361,16 +359,24 @@ class _BlockCompiler {
Future<void> _compileRepeatedSection(
_VariableLookup variableLookup, List<MustachioNode> block,
{bool invert = false}) async {
var variableIsPotentiallyNullable =
typeSystem.isPotentiallyNullable(variableLookup.type);
var variableAccess = variableLookup.name;
if (invert) {
writeln('if ($variableAccess?.isEmpty ?? true) {');
if (variableIsPotentiallyNullable) {
writeln('if ($variableAccess?.isEmpty ?? true) {');
} else {
writeln('if ($variableAccess.isEmpty) {');
}
await _compile(block);
writeln('}');
} else {
var variableAccessResult = getNewContextName();
writeln('var $variableAccessResult = $variableAccess;');
var newContextName = getNewContextName();
writeln('if ($variableAccessResult != null) {');
if (variableIsPotentiallyNullable) {
writeln('if ($variableAccessResult != null) {');
}
writeln(' for (var $newContextName in $variableAccessResult) {');
// If [loopType] is something like `C<int>` where
// `class C<T> implements Queue<Future<T>>`, we need the [ClassElement]
Expand All @@ -385,7 +391,9 @@ class _BlockCompiler {
await _compile(block);
_contextStack.pop();
writeln(' }');
writeln('}');
if (variableIsPotentiallyNullable) {
writeln('}');
}
}
}

Expand All @@ -394,19 +402,27 @@ class _BlockCompiler {
_VariableLookup variableLookup, List<MustachioNode> block,
{bool invert = false}) async {
var variableAccess = variableLookup.name;
var variableIsPotentiallyNullable =
typeSystem.isPotentiallyNullable(variableLookup.type);
if (invert) {
writeln('if ($variableAccess == null) {');
await _compile(block);
writeln('}');
} else {
var innerContextName = getNewContextName();
writeln('var $innerContextName = $variableAccess;');
writeln('if ($innerContextName != null) {');
var innerContext = _VariableLookup(variableLookup.type, innerContextName);
if (variableIsPotentiallyNullable) {
writeln('if ($innerContextName != null) {');
}
var innerContext = _VariableLookup(
typeSystem.promoteToNonNull(variableLookup.type) as InterfaceType,
innerContextName);
_contextStack.push(innerContext);
await _compile(block);
_contextStack.pop();
writeln('}');
if (variableIsPotentiallyNullable) {
writeln('}');
}
}
}

Expand All @@ -429,23 +445,31 @@ class _BlockCompiler {
continue;
}

var type = getter.returnType;
var contextChain = '${context.name}.$primaryName';
var type = getter.returnType as InterfaceType;
var contextChain = typeSystem.isPotentiallyNullable(context.type)
// This is imperfect; the idea is that in our templates, we may have
// `{{foo.bar.baz}}` and `foo.bar` may be nullably typed. Mustache
// (and Mustachio) does not have a null-aware property access
// operator, nor a null-check operator. This code translates
// `foo.bar.baz` to `foo.bar!.baz` for nullable `foo.bar`.
? '${context.name}!.$primaryName'
: '${context.name}.$primaryName';
var remainingNames = [...key.skip(1)];
for (var secondaryKey in remainingNames) {
getter = (type as InterfaceType)
.lookUpGetter2(secondaryKey, type.element.library);
getter = type.lookUpGetter2(secondaryKey, type.element.library);
if (getter == null) {
throw MustachioResolutionError(node.keySpan.message(
"Failed to resolve '$secondaryKey' on ${context.type} while "
'resolving $remainingNames as a property chain on any types in '
'the context chain: $contextChain, after first resolving '
"'$primaryName' to a property on $type"));
}
type = getter.returnType;
contextChain = '$contextChain.$secondaryKey';
contextChain = typeSystem.isPotentiallyNullable(type)
? '$contextChain!.$secondaryKey'
: '$contextChain.$secondaryKey';
type = getter.returnType as InterfaceType;
}
return _VariableLookup(type as InterfaceType, contextChain);
return _VariableLookup(type, contextChain);
}

var contextTypes = [
Expand Down Expand Up @@ -491,11 +515,12 @@ class _BlockCompiler {
/// The result is HTML-escaped if [escape] is true.
void _writeGetter(_VariableLookup variableLookup, {bool escape = true}) {
var variableAccess = variableLookup.name;
if (escape) {
writeln('buffer.writeEscaped($variableAccess.toString());');
} else {
writeln('buffer.write($variableAccess.toString());');
}
var toString = typeSystem.isPotentiallyNullable(variableLookup.type)
? '$variableAccess?.toString()'
: '$variableAccess.toString()';
writeln(escape
? 'buffer.writeEscaped($toString);'
: 'buffer.write($toString);');
}
}

Expand Down