Skip to content

Report errors on SimpleRenderer on missing getters #2670

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 7 commits into from
Jun 8, 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
1 change: 1 addition & 0 deletions lib/src/generator/templates.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const _visibleTypes = {
Constructor,
DefinedElementType,
Documentable,
ElementType,
Enum,
Extension,
FeatureSet,
Expand Down
1,189 changes: 1,002 additions & 187 deletions lib/src/generator/templates.runtime_renderers.dart

Large diffs are not rendered by default.

94 changes: 70 additions & 24 deletions lib/src/mustachio/renderer_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,14 @@ abstract class RendererBase<T> {
/// The output buffer into which [context] is rendered, using a template.
final buffer = StringBuffer();

RendererBase(this.context, this.parent, this._template);
final Set<String> _invisibleGetters;

RendererBase(
this.context,
this.parent,
this._template, {
Set<String> invisibleGetters = const {},
}) : _invisibleGetters = invisibleGetters;

Template get template => _template;

Expand All @@ -172,21 +179,28 @@ abstract class RendererBase<T> {
if (names.length == 1 && names.single == '.') {
return context.toString();
}
var property = getProperty(names.first);
if (property != null) {
var remainingNames = [...names.skip(1)];
try {
return property.renderVariable(context, property, remainingNames);
} on PartialMustachioResolutionError catch (e) {
// The error thrown by [Property.renderVariable] does not have all of
// the names required for a decent error. We throw a new error here.
throw MustachioResolutionError(node.keySpan.message(
"Failed to resolve '${e.name}' on ${e.contextType} while resolving "
'$remainingNames as a property chain on any types in the context '
"chain: $contextChainString, after first resolving '${names.first}' "
'to a property on $T'));
var firstName = names.first;
try {
var property = getProperty(firstName);
if (property != null) {
var remainingNames = [...names.skip(1)];
try {
return property.renderVariable(context, property, remainingNames);
} on PartialMustachioResolutionError catch (e) {
// The error thrown by [Property.renderVariable] does not have all of
// the names required for a decent error. We throw a new error here.
throw MustachioResolutionError(node.keySpan.message(
"Failed to resolve '${e.name}' on ${e.contextType} while "
'resolving $remainingNames as a property chain on any types in '
'the context chain: $contextChainString, after first resolving '
"'$firstName' to a property on $T"));
}
}
} else if (parent != null) {
} on _MustachioResolutionErrorWithoutSpan catch (e) {
throw MustachioResolutionError(node.keySpan.message(e.message));
}

if (parent != null) {
return parent.getFields(node);
} else {
throw MustachioResolutionError(node.keySpan.message(
Expand Down Expand Up @@ -268,31 +282,52 @@ abstract class RendererBase<T> {
}

String renderSimple(Object context, List<MustachioNode> ast, Template template,
{RendererBase parent}) {
var renderer = SimpleRenderer(context, parent, template);
{@required RendererBase parent, Set<String> getters}) {
var renderer = SimpleRenderer(context, parent, template, getters);
renderer.renderBlock(ast);
return renderer.buffer.toString();
}

class SimpleRenderer extends RendererBase<Object> {
SimpleRenderer(Object context, RendererBase<Object> parent, Template template)
: super(context, parent, template);
SimpleRenderer(
Object context,
RendererBase<Object> parent,
Template template,
Set<String> invisibleGetters,
) : super(context, parent, template, invisibleGetters: invisibleGetters);

@override
Property<Object> getProperty(String key) => null;
Property<Object> getProperty(String key) {
if (_invisibleGetters.contains(key)) {
throw MustachioResolutionError(_failedKeyVisibilityMessage(key));
} else {
return null;
}
}

@override
String getFields(Variable node) {
var names = node.key;
if (names.length == 1 && names.single == '.') {
var firstName = node.key.first;
if (names.length == 1 && firstName == '.') {
return context.toString();
}
if (parent != null) {
} else if (_invisibleGetters.contains(firstName)) {
throw MustachioResolutionError(_failedKeyVisibilityMessage(firstName));
} else if (parent != null) {
return parent.getFields(node);
} else {
return 'null';
}
}

String _failedKeyVisibilityMessage(String name) {
var type = context.runtimeType;
return '[$name] is a getter on $type, which is not visible to Mustache. '
'To render [$name] on $type, make it visible to Mustache via the '
'`visibleTypes` parameter on `@Renderer`; to render [$name] on a '
'different type up in the context stack, perhaps provide [$name] via '
'a different name.';
}
}

/// An individual property of objects of type [T], including functions for
Expand Down Expand Up @@ -346,7 +381,7 @@ class Property<T> {
class MustachioResolutionError extends Error {
final String message;

MustachioResolutionError([this.message]);
MustachioResolutionError(this.message);

@override
String toString() => 'MustachioResolutionError: $message';
Expand All @@ -362,6 +397,17 @@ class PartialMustachioResolutionError extends Error {
PartialMustachioResolutionError(this.name, this.contextType);
}

/// A Mustachio resolution error which is thrown in a position where the AST
/// node is not known.
///
/// This error should be caught and "re-thrown" as a [MustachioResolutionError]
/// with a message derived from a [SourceSpan].
class _MustachioResolutionErrorWithoutSpan extends Error {
final String message;

_MustachioResolutionErrorWithoutSpan(this.message);
}

extension MapExtensions<T> on Map<String, Property<T>> {
Property<T> getValue(String name) {
if (containsKey(name)) {
Expand Down
1 change: 1 addition & 0 deletions test/mustachio/foo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Foo extends FooBase<Baz> {
@override
Baz baz;
Property1 p1;
int length;
}

class Bar {
Expand Down
58 changes: 50 additions & 8 deletions test/mustachio/foo.runtime_renderers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ class Renderer_Bar extends RendererBase<Bar> {
isNullValue: (CT_ c) => c.s2 == null,
renderValue:
(CT_ c, RendererBase<CT_> r, List<MustachioNode> ast) {
return renderSimple(c.s2, ast, r.template, parent: r);
return renderSimple(c.s2, ast, r.template,
parent: r, getters: _invisibleGetters['String']);
},
),
});
Expand Down Expand Up @@ -210,8 +211,20 @@ class Renderer_Foo extends RendererBase<Foo> {
self.renderSimpleVariable(c, remainingNames, 'List<int>'),
renderIterable:
(CT_ c, RendererBase<CT_> r, List<MustachioNode> ast) {
return c.l1.map(
(e) => renderSimple(e, ast, r.template, parent: r));
return c.l1.map((e) => renderSimple(e, ast, r.template,
parent: r, getters: _invisibleGetters['int']));
},
),
'length': Property(
getValue: (CT_ c) => c.length,
renderVariable: (CT_ c, Property<CT_> self,
List<String> remainingNames) =>
self.renderSimpleVariable(c, remainingNames, 'int'),
isNullValue: (CT_ c) => c.length == null,
renderValue:
(CT_ c, RendererBase<CT_> r, List<MustachioNode> ast) {
return renderSimple(c.length, ast, r.template,
parent: r, getters: _invisibleGetters['int']);
},
),
'p1': Property(
Expand Down Expand Up @@ -241,7 +254,8 @@ class Renderer_Foo extends RendererBase<Foo> {
isNullValue: (CT_ c) => c.s1 == null,
renderValue:
(CT_ c, RendererBase<CT_> r, List<MustachioNode> ast) {
return renderSimple(c.s1, ast, r.template, parent: r);
return renderSimple(c.s1, ast, r.template,
parent: r, getters: _invisibleGetters['String']);
},
),
});
Expand Down Expand Up @@ -352,7 +366,8 @@ class Renderer_Object extends RendererBase<Object> {
isNullValue: (CT_ c) => c.hashCode == null,
renderValue:
(CT_ c, RendererBase<CT_> r, List<MustachioNode> ast) {
return renderSimple(c.hashCode, ast, r.template, parent: r);
return renderSimple(c.hashCode, ast, r.template,
parent: r, getters: _invisibleGetters['int']);
},
),
'runtimeType': Property(
Expand All @@ -364,7 +379,7 @@ class Renderer_Object extends RendererBase<Object> {
renderValue:
(CT_ c, RendererBase<CT_> r, List<MustachioNode> ast) {
return renderSimple(c.runtimeType, ast, r.template,
parent: r);
parent: r, getters: _invisibleGetters['Type']);
},
),
});
Expand Down Expand Up @@ -457,7 +472,8 @@ class Renderer_Property2 extends RendererBase<Property2> {
isNullValue: (CT_ c) => c.s == null,
renderValue:
(CT_ c, RendererBase<CT_> r, List<MustachioNode> ast) {
return renderSimple(c.s, ast, r.template, parent: r);
return renderSimple(c.s, ast, r.template,
parent: r, getters: _invisibleGetters['String']);
},
),
});
Expand Down Expand Up @@ -499,7 +515,8 @@ class Renderer_Property3 extends RendererBase<Property3> {
isNullValue: (CT_ c) => c.s == null,
renderValue:
(CT_ c, RendererBase<CT_> r, List<MustachioNode> ast) {
return renderSimple(c.s, ast, r.template, parent: r);
return renderSimple(c.s, ast, r.template,
parent: r, getters: _invisibleGetters['String']);
},
),
});
Expand All @@ -517,3 +534,28 @@ class Renderer_Property3 extends RendererBase<Property3> {
}
}
}

const _invisibleGetters = {
'String': {
'hashCode',
'runtimeType',
'length',
'isEmpty',
'isNotEmpty',
'codeUnits',
'runes'
},
'Type': {'hashCode', 'runtimeType'},
'int': {
'hashCode',
'runtimeType',
'isNaN',
'isNegative',
'isInfinite',
'isFinite',
'sign',
'isEven',
'isOdd',
'bitLength'
},
};
7 changes: 4 additions & 3 deletions test/mustachio/runtime_renderer_builder_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ class Baz {}
self.renderSimpleVariable(c, remainingNames, 'List<int>'),
renderIterable:
(CT_ c, RendererBase<CT_> r, List<MustachioNode> ast) {
return c.l1.map(
(e) => renderSimple(e, ast, r.template, parent: r));
return c.l1.map((e) => renderSimple(e, ast, r.template,
parent: r, getters: _invisibleGetters['int']));
},
),
'''));
Expand All @@ -148,7 +148,8 @@ class Baz {}
isNullValue: (CT_ c) => c.s1 == null,
renderValue:
(CT_ c, RendererBase<CT_> r, List<MustachioNode> ast) {
return renderSimple(c.s1, ast, r.template, parent: r);
return renderSimple(c.s1, ast, r.template,
parent: r, getters: _invisibleGetters['String']);
},
),
'''));
Expand Down
33 changes: 32 additions & 1 deletion test/mustachio/runtime_renderer_render_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ void main() {

test('property map contains all public getters', () {
var propertyMap = Renderer_Foo.propertyMap();
expect(propertyMap.keys, hasLength(7));
expect(propertyMap.keys, hasLength(8));
expect(propertyMap['b1'], isNotNull);
expect(propertyMap['s1'], isNotNull);
expect(propertyMap['l1'], isNotNull);
expect(propertyMap['baz'], isNotNull);
expect(propertyMap['p1'], isNotNull);
expect(propertyMap['length'], isNotNull);
expect(propertyMap['hashCode'], isNotNull);
expect(propertyMap['runtimeType'], isNotNull);
});
Expand Down Expand Up @@ -516,6 +517,36 @@ line 1, column 9 of ${fooTemplateFile.path}: Failed to resolve 's2' as a propert
contains('Failed to resolve [length] property chain on String'))));
});

test('Renderer throws when a SimpleRenderer variable key shadows another key',
() async {
var fooTemplateFile = getFile('/project/foo.mustache')
..writeAsStringSync('Text {{#s1}} {{length}} {{/s1}}');
var fooTemplate = await Template.parse(fooTemplateFile);
var foo = Foo()..s1 = 'String';
expect(
() => renderFoo(foo, fooTemplate),
throwsA(const TypeMatcher<MustachioResolutionError>().having(
(e) => e.message,
'message',
contains('[length] is a getter on String, which is not visible to '
'Mustache.'))));
});

test('Renderer throws when a SimpleRenderer section key shadows another key',
() async {
var fooTemplateFile = getFile('/project/foo.mustache')
..writeAsStringSync('Text {{#s1}} {{#length}}Inner{{/length}} {{/s1}}');
var fooTemplate = await Template.parse(fooTemplateFile);
var foo = Foo()..s1 = 'String';
expect(
() => renderFoo(foo, fooTemplate),
throwsA(const TypeMatcher<MustachioResolutionError>().having(
(e) => e.message,
'message',
contains('[length] is a getter on String, which is not visible to '
'Mustache.'))));
});

test('Template parser throws when it cannot read a template', () async {
var barTemplateFile = getFile('/project/src/bar.mustache');
expect(
Expand Down
Loading