Skip to content

Commit 3142567

Browse files
Merge pull request #2587 from NullVoxPopuli/nvp/template-lint-extract-rule-template-no-unnecessary-curly-parens
Extract rule: template-no-unnecessary-curly-parens
2 parents 58a690f + e6ec1a5 commit 3142567

4 files changed

Lines changed: 181 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ rules in templates can be disabled with eslint directives with mustache or html
256256
| [template-no-obsolete-elements](docs/rules/template-no-obsolete-elements.md) | disallow obsolete HTML elements | | | |
257257
| [template-no-outlet-outside-routes](docs/rules/template-no-outlet-outside-routes.md) | disallow {{outlet}} outside of route templates | | | |
258258
| [template-no-page-title-component](docs/rules/template-no-page-title-component.md) | disallow usage of ember-page-title component | | | |
259+
| [template-no-unnecessary-curly-parens](docs/rules/template-no-unnecessary-curly-parens.md) | disallow unnecessary parentheses enclosing statements in curlies | | 🔧 | |
259260
| [template-no-unused-block-params](docs/rules/template-no-unused-block-params.md) | disallow unused block parameters in templates | | | |
260261
| [template-no-valueless-arguments](docs/rules/template-no-valueless-arguments.md) | disallow valueless named arguments | | | |
261262
| [template-no-whitespace-for-layout](docs/rules/template-no-whitespace-for-layout.md) | disallow using whitespace for layout purposes | | | |
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# ember/template-no-unnecessary-curly-parens
2+
3+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
Disallow unnecessary parentheses enclosing statements in curlies. When invoking a helper with arguments, the outer parentheses around the entire expression are unnecessary.
8+
9+
## Examples
10+
11+
This rule **forbids** the following:
12+
13+
```gjs
14+
<template>
15+
{{(concat "a" "b")}}
16+
</template>
17+
```
18+
19+
```gjs
20+
<template>
21+
{{(helper a="b")}}
22+
</template>
23+
```
24+
25+
This rule **allows** the following:
26+
27+
```gjs
28+
<template>
29+
{{foo}}
30+
{{(foo)}}
31+
{{concat "a" "b"}}
32+
{{concat (capitalize "foo") "-bar"}}
33+
</template>
34+
```
35+
36+
## References
37+
38+
- Ember's [Helper Functions](https://guides.emberjs.com/release/components/helper-functions/) guide
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
function isFixableMustache(node) {
2+
// Check if the mustache's "path" is actually a SubExpression with params or hash
3+
// e.g., {{(helper arg)}} where the path is (helper arg)
4+
return (
5+
node.path?.type === 'GlimmerSubExpression' &&
6+
((node.path.params && node.path.params.length > 0) ||
7+
(node.path.hash?.pairs && node.path.hash.pairs.length > 0))
8+
);
9+
}
10+
11+
/** @type {import('eslint').Rule.RuleModule} */
12+
module.exports = {
13+
meta: {
14+
type: 'suggestion',
15+
docs: {
16+
description: 'disallow unnecessary parentheses enclosing statements in curlies',
17+
category: 'Best Practices',
18+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-unnecessary-curly-parens.md',
19+
templateMode: 'both',
20+
},
21+
fixable: 'code',
22+
schema: [],
23+
messages: {
24+
noUnnecessaryCurlyParens: 'Unnecessary parentheses enclosing statement',
25+
},
26+
originallyFrom: {
27+
name: 'ember-template-lint',
28+
rule: 'lib/rules/no-unnecessary-curly-parens.js',
29+
docs: 'docs/rule/no-unnecessary-curly-parens.md',
30+
tests: 'test/unit/rules/no-unnecessary-curly-parens-test.js',
31+
},
32+
},
33+
34+
create(context) {
35+
const sourceCode = context.sourceCode || context.getSourceCode();
36+
37+
return {
38+
GlimmerMustacheStatement(node) {
39+
if (isFixableMustache(node)) {
40+
const subExpr = node.path;
41+
context.report({
42+
node,
43+
messageId: 'noUnnecessaryCurlyParens',
44+
fix(fixer) {
45+
// Replace {{(helper params hash)}} with {{helper params hash}}
46+
const helperName = subExpr.path?.original || '';
47+
let replacement = `{{${helperName}`;
48+
49+
for (const param of subExpr.params || []) {
50+
replacement += ` ${sourceCode.getText(param)}`;
51+
}
52+
for (const pair of subExpr.hash?.pairs || []) {
53+
replacement += ` ${sourceCode.getText(pair)}`;
54+
}
55+
replacement += '}}';
56+
57+
return fixer.replaceText(node, replacement);
58+
},
59+
});
60+
}
61+
},
62+
};
63+
},
64+
};
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//------------------------------------------------------------------------------
2+
// Requirements
3+
//------------------------------------------------------------------------------
4+
5+
const rule = require('../../../lib/rules/template-no-unnecessary-curly-parens');
6+
const RuleTester = require('eslint').RuleTester;
7+
8+
const validHbs = [
9+
'{{foo}}',
10+
'{{this.foo}}',
11+
'{{(helper)}}',
12+
'{{(this.helper)}}',
13+
'{{concat "a" "b"}}',
14+
'{{concat (capitalize "foo") "-bar"}}',
15+
];
16+
17+
const invalidHbs = [
18+
{
19+
code: '<FooBar @x="{{index}}X{{(someHelper foo)}}" />',
20+
output: '<FooBar @x="{{index}}X{{someHelper foo}}" />',
21+
errors: [{ messageId: 'noUnnecessaryCurlyParens' }],
22+
},
23+
{
24+
code: '<FooBar @x="{{index}}XY{{(someHelper foo)}}" />',
25+
output: '<FooBar @x="{{index}}XY{{someHelper foo}}" />',
26+
errors: [{ messageId: 'noUnnecessaryCurlyParens' }],
27+
},
28+
{
29+
code: '<FooBar @x="{{index}}--{{(someHelper foo)}}" />',
30+
output: '<FooBar @x="{{index}}--{{someHelper foo}}" />',
31+
errors: [{ messageId: 'noUnnecessaryCurlyParens' }],
32+
},
33+
{
34+
code: '{{(concat "a" "b")}}',
35+
output: '{{concat "a" "b"}}',
36+
errors: [{ messageId: 'noUnnecessaryCurlyParens' }],
37+
},
38+
{
39+
code: '{{(helper a="b")}}',
40+
output: '{{helper a="b"}}',
41+
errors: [{ messageId: 'noUnnecessaryCurlyParens' }],
42+
},
43+
];
44+
45+
function wrapTemplate(entry) {
46+
if (typeof entry === 'string') {
47+
return `<template>${entry}</template>`;
48+
}
49+
50+
return {
51+
...entry,
52+
code: `<template>${entry.code}</template>`,
53+
output: entry.output ? `<template>${entry.output}</template>` : entry.output,
54+
};
55+
}
56+
57+
const gjsRuleTester = new RuleTester({
58+
parser: require.resolve('ember-eslint-parser'),
59+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
60+
});
61+
62+
gjsRuleTester.run('template-no-unnecessary-curly-parens', rule, {
63+
valid: validHbs.map(wrapTemplate),
64+
invalid: invalidHbs.map(wrapTemplate),
65+
});
66+
67+
const hbsRuleTester = new RuleTester({
68+
parser: require.resolve('ember-eslint-parser/hbs'),
69+
parserOptions: {
70+
ecmaVersion: 2022,
71+
sourceType: 'module',
72+
},
73+
});
74+
75+
hbsRuleTester.run('template-no-unnecessary-curly-parens', rule, {
76+
valid: validHbs,
77+
invalid: invalidHbs,
78+
});

0 commit comments

Comments
 (0)