Skip to content

Commit d7e52d8

Browse files
committed
add template-no-let-reference rule
1 parent 19bceeb commit d7e52d8

File tree

8 files changed

+187
-15
lines changed

8 files changed

+187
-15
lines changed

.eslintignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ node_modules
33
lib/recommended-rules.js
44

55
# Contains <template> in js markdown
6-
docs/rules/no-empty-glimmer-component-classes.md
6+
docs/rules/no-empty-glimmer-component-classes.md
7+
docs/rules/template-no-let-reference.md

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ module.exports = {
164164
| [no-ember-super-in-es-classes](docs/rules/no-ember-super-in-es-classes.md) | disallow use of `this._super` in ES class methods || 🔧 | |
165165
| [no-empty-glimmer-component-classes](docs/rules/no-empty-glimmer-component-classes.md) | disallow empty backing classes for Glimmer components || | |
166166
| [no-tracked-properties-from-args](docs/rules/no-tracked-properties-from-args.md) | disallow creating @tracked properties from this.args || | |
167+
| [template-no-let-reference](docs/rules/template-no-let-reference.md) | disallow referencing let variables in \<template\> || | |
167168

168169
### jQuery
169170

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# ember/template-no-let-reference
2+
3+
💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/ember-cli/eslint-plugin-ember#-configurations).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
Disallows referencing let/var variables in templates.
8+
9+
```js
10+
// app/components/foo.gjs
11+
let foo = 1;
12+
13+
function increment() {
14+
foo++;
15+
}
16+
17+
export default <template>{{foo}}</template>;
18+
````
19+
20+
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.
21+
22+
So, generally speaking, one should avoid referencing let variables from within &lt;template&gt; and instead prefer to use const bindings.
23+
24+
## Rule Detail
25+
26+
Use `const` variables instead of `let`.
27+
28+
## Examples
29+
30+
Examples of **incorrect** code for this rule:
31+
32+
```js
33+
let x = 1;
34+
<template>
35+
{{x}}
36+
</template>
37+
```
38+
39+
```js
40+
let Comp = x; // SomeComponent
41+
<template>
42+
<Comp />
43+
</template>
44+
```
45+
46+
Examples of **correct** code for this rule:
47+
48+
```js
49+
const x = 1;
50+
<template>
51+
{{x}}
52+
</template>
53+
```
54+
55+
```js
56+
const Comp = x; // SomeComponent
57+
<template>
58+
<Comp />
59+
</template>
60+
```

lib/parsers/gjs-gts-parser.js

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

371-
if ('blockParams' in node) {
372-
const upperScope = findParentScope(result.scopeManager, path);
373-
const scope = result.isTypescript
374-
? new TypescriptScope.BlockScope(result.scopeManager, upperScope, node)
375-
: new Scope(result.scopeManager, 'block', upperScope, node);
376-
for (const [i, b] of node.params.entries()) {
377-
const v = new Variable(b.name, scope);
378-
v.identifiers.push(b);
379-
v.defs.push(new Definition('Parameter', b, node, node, i, 'Block Param'));
380-
scope.variables.push(v);
381-
scope.set.set(b.name, v);
382-
}
383-
}
384-
385371
if (node.type === 'GlimmerPathExpression' && node.head.type === 'VarHead') {
386372
const name = node.head.name;
387373
if (glimmer.isKeyword(name)) {
@@ -400,6 +386,20 @@ function convertAst(result, preprocessedResult, visitorKeys) {
400386
registerNodeInScope(node, scope, variable);
401387
}
402388
}
389+
390+
if ('blockParams' in node) {
391+
const upperScope = findParentScope(result.scopeManager, path);
392+
const scope = result.isTypescript
393+
? new TypescriptScope.BlockScope(result.scopeManager, upperScope, node)
394+
: new Scope(result.scopeManager, 'block', upperScope, node);
395+
for (const [i, b] of node.params.entries()) {
396+
const v = new Variable(b.name, scope);
397+
v.identifiers.push(b);
398+
v.defs.push(new Definition('Parameter', b, node, node, i, 'Block Param'));
399+
scope.variables.push(v);
400+
scope.set.set(b.name, v);
401+
}
402+
}
403403
return null;
404404
});
405405

lib/recommended-rules.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ module.exports = {
7373
"ember/require-tagless-components": "error",
7474
"ember/require-valid-css-selector-in-test-helpers": "error",
7575
"ember/routes-segments-snake-case": "error",
76+
"ember/template-no-let-reference": "error",
7677
"ember/use-brace-expansion": "error",
7778
"ember/use-ember-data-rfc-395-imports": "error"
7879
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
const ERROR_MESSAGE =
2+
'update-able variables are not supported in templates, reference a const variable';
3+
4+
/** @type {import('eslint').Rule.RuleModule} */
5+
module.exports = {
6+
ERROR_MESSAGE,
7+
meta: {
8+
type: 'suggestion',
9+
docs: {
10+
description: 'disallow referencing let variables in \\<template\\>',
11+
category: 'Ember Octane',
12+
recommended: true,
13+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-let-reference.md',
14+
},
15+
fixable: null,
16+
schema: [],
17+
},
18+
19+
create: (context) => {
20+
const sourceCode = context.sourceCode;
21+
22+
function checkIfWritableReferences(node, scope) {
23+
const ref = scope.references.find((v) => v.identifier === node);
24+
if (!ref) {
25+
return;
26+
}
27+
if (ref.resolved?.identifiers.some((i) => ['let', 'var'].includes(i.parent.parent.kind))) {
28+
context.report({ node, message: ERROR_MESSAGE });
29+
}
30+
}
31+
32+
return {
33+
GlimmerPathExpression(node) {
34+
checkIfWritableReferences(node.head, sourceCode.getScope(node));
35+
},
36+
37+
GlimmerElementNode(node) {
38+
// glimmer element is in its own scope, need tp get upper scope
39+
const scope = sourceCode.getScope(node.parent);
40+
checkIfWritableReferences(node, scope);
41+
},
42+
};
43+
},
44+
};

tests/__snapshots__/recommended.js.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ exports[`recommended rules has the right list 1`] = `
7070
"require-tagless-components",
7171
"require-valid-css-selector-in-test-helpers",
7272
"routes-segments-snake-case",
73+
"template-no-let-reference",
7374
"use-brace-expansion",
7475
"use-ember-data-rfc-395-imports",
7576
]
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//------------------------------------------------------------------------------
2+
// Requirements
3+
//------------------------------------------------------------------------------
4+
5+
const rule = require('../../../lib/rules/template-no-let-reference');
6+
const RuleTester = require('eslint').RuleTester;
7+
8+
const { ERROR_MESSAGE } = rule;
9+
10+
//------------------------------------------------------------------------------
11+
// Tests
12+
//------------------------------------------------------------------------------
13+
14+
const ruleTester = new RuleTester({
15+
parser: require.resolve('../../../lib/parsers/gjs-gts-parser.js'),
16+
parserOptions: { ecmaVersion: 2020, sourceType: 'module' },
17+
});
18+
ruleTester.run('template-no-let-reference', rule, {
19+
valid: [
20+
`
21+
const a = '';
22+
<template>
23+
{{a}}
24+
</template>
25+
`,
26+
`
27+
const a = '';
28+
<template>
29+
<a></a>
30+
</template>
31+
`,
32+
// make sure rule does not error out on missing references
33+
`
34+
const a = '';
35+
<template>
36+
{{ab}}
37+
<ab></ab>
38+
</template>
39+
`,
40+
],
41+
42+
invalid: [
43+
{
44+
code: `
45+
let a = '';
46+
<template>
47+
{{a}}
48+
</template>
49+
`,
50+
output: null,
51+
errors: [{ type: 'VarHead', message: ERROR_MESSAGE }],
52+
},
53+
{
54+
code: `
55+
var a = '';
56+
<template>
57+
<a></a>
58+
</template>
59+
`,
60+
output: null,
61+
errors: [{ type: 'GlimmerElementNode', message: ERROR_MESSAGE }],
62+
},
63+
],
64+
});

0 commit comments

Comments
 (0)