Skip to content

Commit e0f9805

Browse files
Merge pull request #2581 from NullVoxPopuli/nvp/template-lint-extract-rule-template-no-unbalanced-curlies
Extract rule: template-no-unbalanced-curlies
2 parents 6a244f4 + b0ce57a commit e0f9805

4 files changed

Lines changed: 242 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ rules in templates can be disabled with eslint directives with mustache or html
406406
| :------------------------------------------------------------------------------------------- | :-------------------------------------------------------- | :- | :- | :- |
407407
| [template-no-extra-mut-helper-argument](docs/rules/template-no-extra-mut-helper-argument.md) | disallow passing more than one argument to the mut helper | | | |
408408
| [template-no-jsx-attributes](docs/rules/template-no-jsx-attributes.md) | disallow JSX-style camelCase attributes | | 🔧 | |
409+
| [template-no-unbalanced-curlies](docs/rules/template-no-unbalanced-curlies.md) | disallow unbalanced mustache curlies | | | |
409410

410411
### Routes
411412

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# ember/template-no-unbalanced-curlies
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Normally, the compiler will find stray curlies and throw a syntax error. However, it won't be able to catch every case.
6+
7+
For example, these are all syntax errors:
8+
9+
```gjs
10+
<template>
11+
{{ x }
12+
{{ x }}}
13+
{{{ x }
14+
{{{ x }}
15+
</template>
16+
```
17+
18+
Whereas these are not:
19+
20+
```gjs
21+
<template>
22+
{ x }}
23+
{ x }
24+
}
25+
}}
26+
}}}
27+
}}}}... (any number of closing curlies past one)
28+
</template>
29+
```
30+
31+
This rule focuses on closing double `}}` and triple `}}}` curlies with no matching opening curlies.
32+
33+
## Examples
34+
35+
This rule **forbids** the following:
36+
37+
```gjs
38+
<template>
39+
foo}}
40+
{foo}}
41+
foo}}}
42+
{foo}}}
43+
</template>
44+
```
45+
46+
## Migration
47+
48+
If you have curlies in your code that you wish to show verbatim, but are flagged by this rule, you can formulate them as a handlebars expression:
49+
50+
```gjs
51+
<template>
52+
<p>This is a closing double curly: {{ '}}' }}</p>
53+
<p>This is a closing triple curly: {{ '}}}' }}</p>
54+
</template>
55+
```
56+
57+
## References
58+
59+
- [Handlebars docs/expressions](https://handlebarsjs.com/guide/expressions.html)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
const hbsParser = require('ember-eslint-parser/hbs');
2+
3+
const SUSPECT_CHARS = '}}';
4+
const reLines = /(.*?(?:\r\n?|\n|$))/gm;
5+
6+
/** @type {import('eslint').Rule.RuleModule} */
7+
module.exports = {
8+
meta: {
9+
type: 'problem',
10+
docs: {
11+
description: 'disallow unbalanced mustache curlies',
12+
category: 'Possible Errors',
13+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-unbalanced-curlies.md',
14+
templateMode: 'both',
15+
},
16+
schema: [],
17+
messages: {
18+
noUnbalancedCurlies: 'Unbalanced curlies detected',
19+
},
20+
originallyFrom: {
21+
name: 'ember-template-lint',
22+
rule: 'lib/rules/no-unbalanced-curlies.js',
23+
docs: 'docs/rule/no-unbalanced-curlies.md',
24+
tests: 'test/unit/rules/no-unbalanced-curlies-test.js',
25+
},
26+
},
27+
28+
create(context) {
29+
const sourceCode = context.sourceCode || context.getSourceCode();
30+
31+
return {
32+
GlimmerTextNode(node) {
33+
const chars = node.chars;
34+
35+
if (!chars.includes(SUSPECT_CHARS)) {
36+
return;
37+
}
38+
39+
let isMustache = false;
40+
41+
try {
42+
const ast = hbsParser.parseForESLint(chars).ast;
43+
const body = ast.body?.[0]?.body ?? ast.body ?? [];
44+
isMustache = body.length > 0 && body[0].type === 'GlimmerMustacheStatement';
45+
} catch {
46+
// Not a valid standalone mustache; continue checking for stray closing curlies.
47+
}
48+
49+
if (isMustache) {
50+
return;
51+
}
52+
53+
let lineNum = node.loc.start.line;
54+
let colNum = node.loc.start.column;
55+
const source = sourceCode.getText(node);
56+
const lines = chars.match(reLines) || [];
57+
58+
for (const line of lines) {
59+
const suspectIndex = line.indexOf(SUSPECT_CHARS);
60+
61+
if (suspectIndex !== -1) {
62+
const length = line.slice(suspectIndex).startsWith('}}}') ? 3 : 2;
63+
64+
context.report({
65+
node,
66+
messageId: 'noUnbalancedCurlies',
67+
loc: {
68+
start: { line: lineNum, column: colNum + suspectIndex },
69+
end: { line: lineNum, column: colNum + suspectIndex + length },
70+
},
71+
source,
72+
});
73+
}
74+
75+
lineNum++;
76+
colNum = 1;
77+
}
78+
},
79+
};
80+
},
81+
};
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//------------------------------------------------------------------------------
2+
// Requirements
3+
//------------------------------------------------------------------------------
4+
5+
const rule = require('../../../lib/rules/template-no-unbalanced-curlies');
6+
const RuleTester = require('eslint').RuleTester;
7+
8+
const validHbs = [
9+
'{foo}',
10+
'{{foo}}',
11+
'{{{foo}}}',
12+
`{{{foo
13+
}}}`,
14+
'\\{{foo}}',
15+
'\\{{foo}}\\{{foo}}',
16+
'\\{{foo}}{{foo}}',
17+
`\\{{foo
18+
}}`,
19+
];
20+
21+
const invalidHbs = [
22+
{
23+
code: 'foo}}',
24+
output: null,
25+
errors: [{ message: 'Unbalanced curlies detected' }],
26+
},
27+
{
28+
code: '{foo}}',
29+
output: null,
30+
errors: [{ message: 'Unbalanced curlies detected' }],
31+
},
32+
{
33+
code: 'foo}}}',
34+
output: null,
35+
errors: [{ message: 'Unbalanced curlies detected' }],
36+
},
37+
{
38+
code: '{foo}}}',
39+
output: null,
40+
errors: [{ message: 'Unbalanced curlies detected' }],
41+
},
42+
{
43+
code: `{foo
44+
}}}`,
45+
output: null,
46+
errors: [{ message: 'Unbalanced curlies detected' }],
47+
},
48+
{
49+
code: `{foo
50+
}}}
51+
bar`,
52+
output: null,
53+
errors: [{ message: 'Unbalanced curlies detected' }],
54+
},
55+
{
56+
code: '{foo\r\nbar\r\n\r\nbaz}}}',
57+
output: null,
58+
errors: [{ message: 'Unbalanced curlies detected' }],
59+
},
60+
{
61+
code: '{foo\rbar\r\rbaz}}}',
62+
output: null,
63+
errors: [{ message: 'Unbalanced curlies detected' }],
64+
},
65+
];
66+
67+
function wrapTemplate(entry) {
68+
if (typeof entry === 'string') {
69+
return `<template>${entry}</template>`;
70+
}
71+
72+
return {
73+
...entry,
74+
code: `<template>${entry.code}</template>`,
75+
output: entry.output ? `<template>${entry.output}</template>` : entry.output,
76+
errors: entry.errors.map(() => ({ messageId: 'noUnbalancedCurlies' })),
77+
};
78+
}
79+
80+
const gjsRuleTester = new RuleTester({
81+
parser: require.resolve('ember-eslint-parser'),
82+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
83+
});
84+
85+
gjsRuleTester.run('template-no-unbalanced-curlies', rule, {
86+
valid: validHbs.map(wrapTemplate),
87+
invalid: invalidHbs.map(wrapTemplate),
88+
});
89+
90+
const hbsRuleTester = new RuleTester({
91+
parser: require.resolve('ember-eslint-parser/hbs'),
92+
parserOptions: {
93+
ecmaVersion: 2022,
94+
sourceType: 'module',
95+
},
96+
});
97+
98+
hbsRuleTester.run('template-no-unbalanced-curlies', rule, {
99+
valid: validHbs,
100+
invalid: invalidHbs,
101+
});

0 commit comments

Comments
 (0)