Skip to content

Commit 887964e

Browse files
authored
Migrate the generated AOT renderers to null safety (#2874)
1 parent 0c86079 commit 887964e

8 files changed

+3632
-4517
lines changed

lib/src/generator/templates.aot_renderers_for_html.dart

Lines changed: 2483 additions & 3079 deletions
Large diffs are not rendered by default.

lib/src/generator/templates.aot_renderers_for_md.dart

Lines changed: 1064 additions & 1372 deletions
Large diffs are not rendered by default.

test/mustachio/aot_compiler_render_test.dart

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,43 +23,43 @@ void main() {
2323
final sdk = p.dirname(p.dirname(Platform.resolvedExecutable));
2424
final fooCode = '''
2525
class FooBase<T extends Object> {
26-
T baz;
26+
T? baz;
2727
}
2828
2929
class Foo extends FooBase<Baz> {
30-
String s1;
31-
bool b1;
32-
List<int> l1;
33-
@override
34-
Baz baz;
35-
Property1 p1;
30+
String? s1 = '';
31+
bool b1 = false;
32+
List<int> l1 = [];
33+
@override
34+
Baz? baz;
35+
Property1? p1;
3636
}
3737
3838
class Bar {
39-
Foo foo;
40-
String s2;
41-
Baz baz;
42-
bool l1;
39+
Foo? foo;
40+
String? s2;
41+
Baz? baz;
42+
bool? l1;
4343
}
4444
4545
class Baz {
46-
Bar bar;
46+
Bar? bar;
4747
}
4848
4949
class Property1 {
50-
Property2 p2;
50+
Property2? p2;
5151
}
5252
5353
class Property2 with Mixin1 {
54-
String s;
54+
String? s;
5555
}
5656
5757
mixin Mixin1 {
58-
Property3 p3;
58+
Property3? p3;
5959
}
6060
6161
class Property3 {
62-
String s;
62+
String? s;
6363
}
6464
''';
6565
InMemoryAssetWriter writer;
@@ -100,7 +100,7 @@ $mainCode
100100
'foo',
101101
Uri.directory(tempDir.path),
102102
packageUriRoot: Uri.directory(p.join(tempDir.path, 'lib')),
103-
languageVersion: LanguageVersion(2, 9),
103+
languageVersion: LanguageVersion(2, 12),
104104
)
105105
]);
106106
var dartToolDir = Directory(p.join(tempDir.path, '.dart_tool'))
@@ -368,7 +368,7 @@ void main() {
368368
'Text {{#bar}}{{bar.foo.baz.bar.foo.s1}}{{/bar}}',
369369
},
370370
'_i1.Baz()..bar = (_i1.Bar()..foo = (_i1.Foo()..s1 = "hello"));'
371-
'baz.bar.foo.baz = baz');
371+
'baz.bar!.foo!.baz = baz');
372372
expect(output, equals('Text hello'));
373373
});
374374

@@ -461,7 +461,7 @@ line 1, column 9 of package:foo/templates/html/foo.html: Failed to resolve '[s2]
461461
}, '_i1.Bar()..foo = _i1.Foo()'),
462462
throwsA(const TypeMatcher<MustachioResolutionError>()
463463
.having((e) => e.message, 'message', contains('''
464-
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
464+
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?
465465
466466
1 │ Text {{foo.x}}
467467
│ ^^^^^

test/mustachio/foo.aot_renderers_for_html.dart

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
// the variable is not used; generally when the section is checking if a
88
// non-bool, non-Iterable field is non-null.
99
// ignore_for_file: unused_local_variable
10-
// @dart=2.9
1110
// ignore_for_file: non_constant_identifier_names, unnecessary_string_escapes
1211

1312
import 'dart:convert' as _i2;
@@ -23,7 +22,7 @@ String renderFoo(_i1.Foo context0) {
2322
buffer.write('''
2423
2524
s1: ''');
26-
buffer.writeEscaped(context0.s1.toString());
25+
buffer.writeEscaped(context0.s1?.toString());
2726
buffer.writeln();
2827
buffer.write('''
2928
b1? ''');
@@ -37,13 +36,11 @@ String renderFoo(_i1.Foo context0) {
3736
buffer.write('''
3837
l1:''');
3938
var context1 = context0.l1;
40-
if (context1 != null) {
41-
for (var context2 in context1) {
42-
buffer.write('''item: ''');
43-
buffer.writeEscaped(context2.toString());
44-
}
39+
for (var context2 in context1) {
40+
buffer.write('''item: ''');
41+
buffer.writeEscaped(context2.toString());
4542
}
46-
if (context0.l1?.isEmpty ?? true) {
43+
if (context0.l1.isEmpty) {
4744
buffer.write('''no items''');
4845
}
4946
buffer.writeln();
@@ -54,7 +51,7 @@ String renderFoo(_i1.Foo context0) {
5451
buffer.writeln();
5552
buffer.write('''
5653
Baz has a ''');
57-
buffer.writeEscaped(context3.bar.s2.toString());
54+
buffer.writeEscaped(context3.bar!.s2?.toString());
5855
}
5956
if (context0.baz == null) {
6057
buffer.write('''baz is null''');
@@ -91,7 +88,7 @@ String renderBaz(_i1.Baz context0) {
9188
}
9289

9390
extension on StringBuffer {
94-
void writeEscaped(String value) {
95-
write(_i2.htmlEscape.convert(value));
91+
void writeEscaped(String? value) {
92+
write(_i2.htmlEscape.convert(value ?? ''));
9693
}
9794
}

test/mustachio/foo.aot_renderers_for_md.dart

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
// the variable is not used; generally when the section is checking if a
88
// non-bool, non-Iterable field is non-null.
99
// ignore_for_file: unused_local_variable
10-
// @dart=2.9
1110
// ignore_for_file: non_constant_identifier_names, unnecessary_string_escapes
1211

1312
import 'dart:convert' as _i2;
@@ -21,7 +20,7 @@ String renderFoo(_i1.Foo context0) {
2120
buffer.write('''
2221
2322
s1: ''');
24-
buffer.writeEscaped(context0.s1.toString());
23+
buffer.writeEscaped(context0.s1?.toString());
2524
buffer.writeln();
2625
buffer.write('''
2726
b1? ''');
@@ -35,13 +34,11 @@ b1? ''');
3534
buffer.write('''
3635
l1:''');
3736
var context1 = context0.l1;
38-
if (context1 != null) {
39-
for (var context2 in context1) {
40-
buffer.write('''item: ''');
41-
buffer.writeEscaped(context2.toString());
42-
}
37+
for (var context2 in context1) {
38+
buffer.write('''item: ''');
39+
buffer.writeEscaped(context2.toString());
4340
}
44-
if (context0.l1?.isEmpty ?? true) {
41+
if (context0.l1.isEmpty) {
4542
buffer.write('''no items''');
4643
}
4744
buffer.writeln();
@@ -52,7 +49,7 @@ baz:''');
5249
buffer.writeln();
5350
buffer.write('''
5451
Baz has a ''');
55-
buffer.writeEscaped(context3.bar.s2.toString());
52+
buffer.writeEscaped(context3.bar!.s2?.toString());
5653
}
5754
if (context0.baz == null) {
5855
buffer.write('''baz is null''');
@@ -82,7 +79,7 @@ String renderBaz(_i1.Baz context0) {
8279
}
8380

8481
extension on StringBuffer {
85-
void writeEscaped(String value) {
86-
write(_i2.htmlEscape.convert(value));
82+
void writeEscaped(String? value) {
83+
write(_i2.htmlEscape.convert(value ?? ''));
8784
}
8885
}

test/mustachio/foo.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class FooBase<T extends Object> {
1111
}
1212

1313
class Foo extends FooBase<Baz> {
14-
String s1 = '';
14+
String? s1 = '';
1515
bool b1 = false;
1616
List<int> l1 = [];
1717
@override

test/mustachio/foo.runtime_renderers.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ class Renderer_Foo extends RendererBase<Foo?> {
265265
renderVariable: (CT_ c, Property<CT_> self,
266266
List<String> remainingNames) =>
267267
self.renderSimpleVariable(c, remainingNames, 'String'),
268-
isNullValue: (CT_ c) => false,
268+
isNullValue: (CT_ c) => c.s1 == null,
269269
renderValue: (CT_ c, RendererBase<CT_> r,
270270
List<MustachioNode> ast, StringSink sink) {
271271
renderSimple(c.s1, ast, r.template, sink,

tool/mustachio/codegen_aot_compiler.dart

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,12 @@ Future<String> compileTemplatesToRenderers(
5050
..returns = refer('void')
5151
..name = 'writeEscaped'
5252
..requiredParameters.add(Parameter((b) => b
53-
..type = refer('String')
53+
..type = refer('String?')
5454
..name = 'value'))
5555
..body = refer('write').call([
5656
refer('htmlEscape', 'dart:convert')
5757
.property('convert')
58-
.call([refer('value')])
58+
.call([refer("value ?? ''")])
5959
]).statement))));
6060
});
6161
return DartFormatter().format('''
@@ -68,7 +68,6 @@ Future<String> compileTemplatesToRenderers(
6868
// the variable is not used; generally when the section is checking if a
6969
// non-bool, non-Iterable field is non-null.
7070
// ignore_for_file: unused_local_variable
71-
// @dart=2.9
7271
// ignore_for_file: non_constant_identifier_names, unnecessary_string_escapes
7372
7473
${library.accept(DartEmitter.scoped(orderDirectives: true))}
@@ -154,8 +153,7 @@ class _AotCompiler {
154153

155154
Future<List<Method>> _compileToRenderer() async {
156155
if (_contextStack.isEmpty) {
157-
var contextName = 'context0';
158-
var contextVariable = _VariableLookup(_contextType, contextName);
156+
var contextVariable = _VariableLookup(_contextType, 'context0');
159157
_contextStack.push(contextVariable);
160158
_contextNameCounter++;
161159
}
@@ -361,16 +359,24 @@ class _BlockCompiler {
361359
Future<void> _compileRepeatedSection(
362360
_VariableLookup variableLookup, List<MustachioNode> block,
363361
{bool invert = false}) async {
362+
var variableIsPotentiallyNullable =
363+
typeSystem.isPotentiallyNullable(variableLookup.type);
364364
var variableAccess = variableLookup.name;
365365
if (invert) {
366-
writeln('if ($variableAccess?.isEmpty ?? true) {');
366+
if (variableIsPotentiallyNullable) {
367+
writeln('if ($variableAccess?.isEmpty ?? true) {');
368+
} else {
369+
writeln('if ($variableAccess.isEmpty) {');
370+
}
367371
await _compile(block);
368372
writeln('}');
369373
} else {
370374
var variableAccessResult = getNewContextName();
371375
writeln('var $variableAccessResult = $variableAccess;');
372376
var newContextName = getNewContextName();
373-
writeln('if ($variableAccessResult != null) {');
377+
if (variableIsPotentiallyNullable) {
378+
writeln('if ($variableAccessResult != null) {');
379+
}
374380
writeln(' for (var $newContextName in $variableAccessResult) {');
375381
// If [loopType] is something like `C<int>` where
376382
// `class C<T> implements Queue<Future<T>>`, we need the [ClassElement]
@@ -385,7 +391,9 @@ class _BlockCompiler {
385391
await _compile(block);
386392
_contextStack.pop();
387393
writeln(' }');
388-
writeln('}');
394+
if (variableIsPotentiallyNullable) {
395+
writeln('}');
396+
}
389397
}
390398
}
391399

@@ -394,19 +402,27 @@ class _BlockCompiler {
394402
_VariableLookup variableLookup, List<MustachioNode> block,
395403
{bool invert = false}) async {
396404
var variableAccess = variableLookup.name;
405+
var variableIsPotentiallyNullable =
406+
typeSystem.isPotentiallyNullable(variableLookup.type);
397407
if (invert) {
398408
writeln('if ($variableAccess == null) {');
399409
await _compile(block);
400410
writeln('}');
401411
} else {
402412
var innerContextName = getNewContextName();
403413
writeln('var $innerContextName = $variableAccess;');
404-
writeln('if ($innerContextName != null) {');
405-
var innerContext = _VariableLookup(variableLookup.type, innerContextName);
414+
if (variableIsPotentiallyNullable) {
415+
writeln('if ($innerContextName != null) {');
416+
}
417+
var innerContext = _VariableLookup(
418+
typeSystem.promoteToNonNull(variableLookup.type) as InterfaceType,
419+
innerContextName);
406420
_contextStack.push(innerContext);
407421
await _compile(block);
408422
_contextStack.pop();
409-
writeln('}');
423+
if (variableIsPotentiallyNullable) {
424+
writeln('}');
425+
}
410426
}
411427
}
412428

@@ -429,23 +445,31 @@ class _BlockCompiler {
429445
continue;
430446
}
431447

432-
var type = getter.returnType;
433-
var contextChain = '${context.name}.$primaryName';
448+
var type = getter.returnType as InterfaceType;
449+
var contextChain = typeSystem.isPotentiallyNullable(context.type)
450+
// This is imperfect; the idea is that in our templates, we may have
451+
// `{{foo.bar.baz}}` and `foo.bar` may be nullably typed. Mustache
452+
// (and Mustachio) does not have a null-aware property access
453+
// operator, nor a null-check operator. This code translates
454+
// `foo.bar.baz` to `foo.bar!.baz` for nullable `foo.bar`.
455+
? '${context.name}!.$primaryName'
456+
: '${context.name}.$primaryName';
434457
var remainingNames = [...key.skip(1)];
435458
for (var secondaryKey in remainingNames) {
436-
getter = (type as InterfaceType)
437-
.lookUpGetter2(secondaryKey, type.element.library);
459+
getter = type.lookUpGetter2(secondaryKey, type.element.library);
438460
if (getter == null) {
439461
throw MustachioResolutionError(node.keySpan.message(
440462
"Failed to resolve '$secondaryKey' on ${context.type} while "
441463
'resolving $remainingNames as a property chain on any types in '
442464
'the context chain: $contextChain, after first resolving '
443465
"'$primaryName' to a property on $type"));
444466
}
445-
type = getter.returnType;
446-
contextChain = '$contextChain.$secondaryKey';
467+
contextChain = typeSystem.isPotentiallyNullable(type)
468+
? '$contextChain!.$secondaryKey'
469+
: '$contextChain.$secondaryKey';
470+
type = getter.returnType as InterfaceType;
447471
}
448-
return _VariableLookup(type as InterfaceType, contextChain);
472+
return _VariableLookup(type, contextChain);
449473
}
450474

451475
var contextTypes = [
@@ -491,11 +515,12 @@ class _BlockCompiler {
491515
/// The result is HTML-escaped if [escape] is true.
492516
void _writeGetter(_VariableLookup variableLookup, {bool escape = true}) {
493517
var variableAccess = variableLookup.name;
494-
if (escape) {
495-
writeln('buffer.writeEscaped($variableAccess.toString());');
496-
} else {
497-
writeln('buffer.write($variableAccess.toString());');
498-
}
518+
var toString = typeSystem.isPotentiallyNullable(variableLookup.type)
519+
? '$variableAccess?.toString()'
520+
: '$variableAccess.toString()';
521+
writeln(escape
522+
? 'buffer.writeEscaped($toString);'
523+
: 'buffer.write($toString);');
499524
}
500525
}
501526

0 commit comments

Comments
 (0)