Skip to content

Commit 372f6b8

Browse files
patricklxbmish
andauthored
Add new rule template-no-let-reference (#1939)
Co-authored-by: Bryan Mishkin <[email protected]>
1 parent 2b2b24a commit 372f6b8

File tree

6 files changed

+179
-14
lines changed

6 files changed

+179
-14
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ lib/recommended-rules-gts.js
66

77
# Contains <template> in js markdown
88
docs/rules/no-empty-glimmer-component-classes.md
9+
docs/rules/template-no-let-reference.md

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ rules in templates can be disabled with eslint directives with mustache or html
241241
| [no-empty-glimmer-component-classes](docs/rules/no-empty-glimmer-component-classes.md) | disallow empty backing classes for Glimmer components || | |
242242
| [no-tracked-properties-from-args](docs/rules/no-tracked-properties-from-args.md) | disallow creating @tracked properties from this.args || | |
243243
| [template-indent](docs/rules/template-indent.md) | enforce consistent indentation for gts/gjs templates | | 🔧 | |
244+
| [template-no-let-reference](docs/rules/template-no-let-reference.md) | disallow referencing let variables in \<template\> | | | |
244245

245246
### jQuery
246247

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# ember/template-no-let-reference
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Disallows referencing let/var variables in templates.
6+
7+
```js
8+
// app/components/foo.gjs
9+
let foo = 1;
10+
11+
function increment() {
12+
foo++;
13+
}
14+
15+
export default <template>{{foo}}</template>;
16+
```
17+
18+
This does not "work" – it doesn't error, but it will essentially capture and compile in the value of the foo variable at some arbitrary point and never update again. Even if the component is torn down and a new instance is created/rendered, it will probably still hold on to the old value when the template was initially compiled.
19+
20+
So, generally speaking, one should avoid referencing let variables from within &lt;template&gt; and instead prefer to use const bindings.
21+
22+
## Rule Detail
23+
24+
Use `const` variables instead of `let`.
25+
26+
## Examples
27+
28+
Examples of **incorrect** code for this rule:
29+
30+
```js
31+
let x = 1;
32+
<template>
33+
{{x}}
34+
</template>
35+
```
36+
37+
```js
38+
let Comp = x; // SomeComponent
39+
<template>
40+
<Comp />
41+
</template>
42+
```
43+
44+
Examples of **correct** code for this rule:
45+
46+
```js
47+
const x = 1;
48+
<template>
49+
{{x}}
50+
</template>
51+
```
52+
53+
```js
54+
const Comp = x; // SomeComponent
55+
<template>
56+
<Comp />
57+
</template>
58+
```

lib/parsers/gjs-gts-parser.js

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -377,20 +377,6 @@ function convertAst(result, preprocessedResult, visitorKeys) {
377377
Object.assign(node, ast);
378378
}
379379

380-
if ('blockParams' in node) {
381-
const upperScope = findParentScope(result.scopeManager, path);
382-
const scope = result.isTypescript
383-
? new TypescriptScope.BlockScope(result.scopeManager, upperScope, node)
384-
: new Scope(result.scopeManager, 'block', upperScope, node);
385-
for (const [i, b] of node.params.entries()) {
386-
const v = new Variable(b.name, scope);
387-
v.identifiers.push(b);
388-
v.defs.push(new Definition('Parameter', b, node, node, i, 'Block Param'));
389-
scope.variables.push(v);
390-
scope.set.set(b.name, v);
391-
}
392-
}
393-
394380
if (node.type === 'GlimmerPathExpression' && node.head.type === 'VarHead') {
395381
const name = node.head.name;
396382
if (glimmer.isKeyword(name)) {
@@ -409,6 +395,20 @@ function convertAst(result, preprocessedResult, visitorKeys) {
409395
registerNodeInScope(node, scope, variable);
410396
}
411397
}
398+
399+
if ('blockParams' in node) {
400+
const upperScope = findParentScope(result.scopeManager, path);
401+
const scope = result.isTypescript
402+
? new TypescriptScope.BlockScope(result.scopeManager, upperScope, node)
403+
: new Scope(result.scopeManager, 'block', upperScope, node);
404+
for (const [i, b] of node.params.entries()) {
405+
const v = new Variable(b.name, scope);
406+
v.identifiers.push(b);
407+
v.defs.push(new Definition('Parameter', b, node, node, i, 'Block Param'));
408+
scope.variables.push(v);
409+
scope.set.set(b.name, v);
410+
}
411+
}
412412
return null;
413413
});
414414

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
module.exports = {
3+
meta: {
4+
type: 'suggestion',
5+
docs: {
6+
description: 'disallow referencing let variables in \\<template\\>',
7+
category: 'Ember Octane',
8+
recommended: false,
9+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-let-reference.md',
10+
},
11+
fixable: null,
12+
schema: [],
13+
messages: {
14+
'no-let': 'update-able variables are not supported in templates, reference a const variable',
15+
},
16+
},
17+
18+
create: (context) => {
19+
const sourceCode = context.sourceCode;
20+
21+
function checkIfWritableReferences(node, scope) {
22+
const ref = scope.references.find((v) => v.identifier === node);
23+
if (!ref) {
24+
return;
25+
}
26+
if (ref.resolved?.identifiers.some((i) => ['let', 'var'].includes(i.parent.parent.kind))) {
27+
context.report({ node, messageId: 'no-let' });
28+
}
29+
}
30+
31+
return {
32+
GlimmerPathExpression(node) {
33+
checkIfWritableReferences(node.head, sourceCode.getScope(node));
34+
},
35+
36+
GlimmerElementNode(node) {
37+
// glimmer element is in its own scope, need tp get upper scope
38+
const scope = sourceCode.getScope(node.parent);
39+
checkIfWritableReferences(node, scope);
40+
},
41+
};
42+
},
43+
};
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//------------------------------------------------------------------------------
2+
// Requirements
3+
//------------------------------------------------------------------------------
4+
5+
const rule = require('../../../lib/rules/template-no-let-reference');
6+
const RuleTester = require('eslint').RuleTester;
7+
8+
//------------------------------------------------------------------------------
9+
// Tests
10+
//------------------------------------------------------------------------------
11+
12+
const ruleTester = new RuleTester({
13+
parser: require.resolve('../../../lib/parsers/gjs-gts-parser.js'),
14+
parserOptions: { ecmaVersion: 2020, sourceType: 'module' },
15+
});
16+
ruleTester.run('template-no-let-reference', rule, {
17+
valid: [
18+
`
19+
const a = '';
20+
<template>
21+
{{a}}
22+
</template>
23+
`,
24+
`
25+
const a = '';
26+
<template>
27+
<a></a>
28+
</template>
29+
`,
30+
// make sure rule does not error out on missing references
31+
`
32+
const a = '';
33+
<template>
34+
{{ab}}
35+
<ab></ab>
36+
</template>
37+
`,
38+
],
39+
40+
invalid: [
41+
{
42+
code: `
43+
let a = '';
44+
<template>
45+
{{a}}
46+
</template>
47+
`,
48+
output: null,
49+
errors: [{ type: 'VarHead', message: rule.meta.messages['no-let'] }],
50+
},
51+
{
52+
code: `
53+
var a = '';
54+
<template>
55+
<a></a>
56+
</template>
57+
`,
58+
output: null,
59+
errors: [{ type: 'GlimmerElementNode', message: rule.meta.messages['no-let'] }],
60+
},
61+
],
62+
});

0 commit comments

Comments
 (0)