Skip to content

Commit b5ab4aa

Browse files
committed
add template-no-let-reference rule
1 parent 8543535 commit b5ab4aa

File tree

7 files changed

+183
-14
lines changed

7 files changed

+183
-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
@@ -240,6 +240,7 @@ rules in templates can be disabled with eslint directives with mustache or html
240240
| [no-ember-super-in-es-classes](docs/rules/no-ember-super-in-es-classes.md) | disallow use of `this._super` in ES class methods || 🔧 | |
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 || | |
243+
| [template-no-let-reference](docs/rules/template-no-let-reference.md) | disallow referencing let variables in \<template\> | | | |
243244

244245
### jQuery
245246

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: 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: false,
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)