Skip to content

Commit

Permalink
Merge pull request #1716 from ember-template-lint/add-no-dynamic-sube…
Browse files Browse the repository at this point in the history
…xpression-invocations

Add `no-dynamic-subexpression-invocations`.
  • Loading branch information
rwjblue authored Jan 28, 2021
2 parents f7acbc9 + 0b1e1de commit acb420a
Show file tree
Hide file tree
Showing 5 changed files with 367 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ Each rule has emojis denoting:
| :white_check_mark: | [no-duplicate-attributes](./docs/rule/no-duplicate-attributes.md) |
| | [no-duplicate-id](./docs/rule/no-duplicate-id.md) |
| | [no-duplicate-landmark-elements](./docs/rule/no-duplicate-landmark-elements.md) |
| | [no-dynamic-subexpression-invocations](./docs/rule/no-dynamic-subexpression-invocations.md) |
| | [no-element-event-actions](./docs/rule/no-element-event-actions.md) |
| :white_check_mark: | [no-extra-mut-helper-argument](./docs/rule/no-extra-mut-helper-argument.md) |
| | [no-forbidden-elements](./docs/rule/no-forbidden-elements.md) |
Expand Down
67 changes: 67 additions & 0 deletions docs/rule/no-dynamic-subexpression-invocations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# no-dynamic-subexpression-invocations

When using Ember versions prior to 3.25 the usage of dynamic invocations for
helpers and modifiers did not work. Unfortunately, some versions of Ember
silently ignored additional arguments (3.16) wherease others throw a very
bizarre error (3.20) when you attempt to invoke what the rendering engine knows as a
dynamic value as if it were a helper.

For example, in versions of Ember prior to 3.25 the helper invocation here is impossible:

```hbs
{{#if (this.someHelper)}}
Hi!
{{/if}}
```

This rule helps applications using Ember versions prior to 3.25 avoid these types of invocation.

## Examples

This rule **forbids** the following:

```hbs
{{! invoking a yielded block param as if it were a helper }}
{{#let anything as |blockParamValue|}}
<button onclick={{blockParamValue someArgument}}></button>
{{/let}}
```

```hbs
{{! invoking a path as if it were a helper }}
<Foo data-any-attribute={{this.anything someArgument}} />
<Foo data-any-attribute={{some.other.path someArgument}} />
```

```hbs
{{! invoking a yielded block param as if it were a modifier }}
{{#let anything as |blockParamValue|}}
<Foo {{blockParamValue}} />
{{/let}}
```

```hbs
{{! invoking a path as if it were a modifier }}
<Foo {{this.anything}} />
<Foo {{some.other.path}} />
```

This rule **allows** the following:

```hbs
{{! use `fn` to wrap a function}}
{{#let anything as |blockParamValue|}}
<button onclick={{fn blockParamValue someArgument}}></button>
{{/let}}
```

## References

RFC's introducing this functionality in Ember > 3.25:

* [emberjs/rfcs#432](https://github.com/emberjs/rfcs/blob/master/text/0432-contextual-helpers.md)
1 change: 1 addition & 0 deletions lib/rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ module.exports = {
'no-duplicate-attributes': require('./no-duplicate-attributes'),
'no-duplicate-id': require('./no-duplicate-id'),
'no-duplicate-landmark-elements': require('./no-duplicate-landmark-elements'),
'no-dynamic-subexpression-invocations': require('./no-dynamic-subexpression-invocations'),
'no-element-event-actions': require('./no-element-event-actions'),
'no-extra-mut-helper-argument': require('./no-extra-mut-helper-argument'),
'no-forbidden-elements': require('./no-forbidden-elements'),
Expand Down
57 changes: 57 additions & 0 deletions lib/rules/no-dynamic-subexpression-invocations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use strict';

const Rule = require('./base');

module.exports = class NoDynamicSubexpressionInvocations extends Rule {
logDynamicInvocation(node, path) {
let isLocal = this.scope.isLocal(node.path);
let isPath = node.path.parts.length > 1;
let isThisPath = node.path.original.startsWith('this.');
let isNamedArgument = node.path.original.startsWith('@');
let hasArguments = node.params.length > 0 || node.hash.length > 0;
let isDynamic = isLocal || isNamedArgument || isPath || isThisPath;

switch (node.type) {
case 'ElementModifierStatement':
case 'SubExpression':
if (isDynamic) {
this.log({
message: `You cannot invoke a dynamic value in the ${node.type} position`,
line: node.loc && node.loc.start.line,
column: node.loc && node.loc.start.column,
source: this.sourceForNode(node),
});
}
break;
case 'MustacheStatement': {
let parents = [...path.parents()];
let isAttr = parents.some((it) => it.node.type === 'AttrNode');

if (isAttr && isDynamic && hasArguments) {
this.log({
message: 'You must use `fn` helper to invoke a function with arguments',
line: node.loc && node.loc.start.line,
column: node.loc && node.loc.start.column,
source: this.sourceForNode(node),
});
}
}
}
}

visitor() {
return {
MustacheStatement(node, path) {
this.logDynamicInvocation(node, path);
},

SubExpression(node, path) {
this.logDynamicInvocation(node, path);
},

ElementModifierStatement(node, path) {
this.logDynamicInvocation(node, path);
},
};
}
};
241 changes: 241 additions & 0 deletions test/unit/rules/no-dynamic-subexpression-invocations-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
'use strict';

const generateRuleTests = require('../../helpers/rule-test-harness');

generateRuleTests({
name: 'no-dynamic-subexpression-invocations',

config: true,

good: [
'{{something "here"}}',
'{{something}}',
'{{something here="goes"}}',
'<button onclick={{fn something "here"}}></button>',
'{{@thing "somearg"}}',
],

bad: [
{
template: '<Foo bar="{{@thing "some-arg"}}" />',

verifyResults(results) {
expect(results).toMatchInlineSnapshot(`
Array [
Object {
"column": 10,
"filePath": "layout.hbs",
"line": 1,
"message": "You must use \`fn\` helper to invoke a function with arguments",
"moduleId": "layout",
"rule": "no-dynamic-subexpression-invocations",
"severity": 2,
"source": "{{@thing \\"some-arg\\"}}",
},
]
`);
},
},
{
template: '<Foo {{this.foo}} />',

verifyResults(results) {
expect(results).toMatchInlineSnapshot(`
Array [
Object {
"column": 5,
"filePath": "layout.hbs",
"line": 1,
"message": "You cannot invoke a dynamic value in the ElementModifierStatement position",
"moduleId": "layout",
"rule": "no-dynamic-subexpression-invocations",
"severity": 2,
"source": "{{this.foo}}",
},
]
`);
},
},
{
template: '<Foo {{@foo}} />',

verifyResults(results) {
expect(results).toMatchInlineSnapshot(`
Array [
Object {
"column": 5,
"filePath": "layout.hbs",
"line": 1,
"message": "You cannot invoke a dynamic value in the ElementModifierStatement position",
"moduleId": "layout",
"rule": "no-dynamic-subexpression-invocations",
"severity": 2,
"source": "{{@foo}}",
},
]
`);
},
},
{
template: '<Foo {{foo.bar}} />',

verifyResults(results) {
expect(results).toMatchInlineSnapshot(`
Array [
Object {
"column": 5,
"filePath": "layout.hbs",
"line": 1,
"message": "You cannot invoke a dynamic value in the ElementModifierStatement position",
"moduleId": "layout",
"rule": "no-dynamic-subexpression-invocations",
"severity": 2,
"source": "{{foo.bar}}",
},
]
`);
},
},
{
template: '<button onclick={{@thing "some-arg"}}></button>',

verifyResults(results) {
expect(results).toMatchInlineSnapshot(`
Array [
Object {
"column": 16,
"filePath": "layout.hbs",
"line": 1,
"message": "You must use \`fn\` helper to invoke a function with arguments",
"moduleId": "layout",
"rule": "no-dynamic-subexpression-invocations",
"severity": 2,
"source": "{{@thing \\"some-arg\\"}}",
},
]
`);
},
},
{
template:
'{{#let "whatever" as |thing|}}<button onclick={{thing "some-arg"}}></button>{{/let}}',

verifyResults(results) {
expect(results).toMatchInlineSnapshot(`
Array [
Object {
"column": 46,
"filePath": "layout.hbs",
"line": 1,
"message": "You must use \`fn\` helper to invoke a function with arguments",
"moduleId": "layout",
"rule": "no-dynamic-subexpression-invocations",
"severity": 2,
"source": "{{thing \\"some-arg\\"}}",
},
]
`);
},
},
{
template: '<button onclick={{this.thing "some-arg"}}></button>',

verifyResults(results) {
expect(results).toMatchInlineSnapshot(`
Array [
Object {
"column": 16,
"filePath": "layout.hbs",
"line": 1,
"message": "You must use \`fn\` helper to invoke a function with arguments",
"moduleId": "layout",
"rule": "no-dynamic-subexpression-invocations",
"severity": 2,
"source": "{{this.thing \\"some-arg\\"}}",
},
]
`);
},
},
{
template: '<button onclick={{lol.other.path "some-arg"}}></button>',

verifyResults(results) {
expect(results).toMatchInlineSnapshot(`
Array [
Object {
"column": 16,
"filePath": "layout.hbs",
"line": 1,
"message": "You must use \`fn\` helper to invoke a function with arguments",
"moduleId": "layout",
"rule": "no-dynamic-subexpression-invocations",
"severity": 2,
"source": "{{lol.other.path \\"some-arg\\"}}",
},
]
`);
},
},
{
template: '{{if (this.foo) "true" "false"}}',

verifyResults(results) {
expect(results).toMatchInlineSnapshot(`
Array [
Object {
"column": 5,
"filePath": "layout.hbs",
"line": 1,
"message": "You cannot invoke a dynamic value in the SubExpression position",
"moduleId": "layout",
"rule": "no-dynamic-subexpression-invocations",
"severity": 2,
"source": "(this.foo)",
},
]
`);
},
},
{
template: '<Foo @bar={{@thing "some-arg"}} />',

verifyResults(results) {
expect(results).toMatchInlineSnapshot(`
Array [
Object {
"column": 10,
"filePath": "layout.hbs",
"line": 1,
"message": "You must use \`fn\` helper to invoke a function with arguments",
"moduleId": "layout",
"rule": "no-dynamic-subexpression-invocations",
"severity": 2,
"source": "{{@thing \\"some-arg\\"}}",
},
]
`);
},
},
{
template: '<Foo onclick={{@thing "some-arg"}} />',

verifyResults(results) {
expect(results).toMatchInlineSnapshot(`
Array [
Object {
"column": 13,
"filePath": "layout.hbs",
"line": 1,
"message": "You must use \`fn\` helper to invoke a function with arguments",
"moduleId": "layout",
"rule": "no-dynamic-subexpression-invocations",
"severity": 2,
"source": "{{@thing \\"some-arg\\"}}",
},
]
`);
},
},
],
});

0 comments on commit acb420a

Please sign in to comment.