Skip to content

Commit 66a70c1

Browse files
committed
Extract rule: template-no-unused-block-params
1 parent 506418a commit 66a70c1

4 files changed

Lines changed: 407 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ rules in templates can be disabled with eslint directives with mustache or html
255255
| [template-no-obsolete-elements](docs/rules/template-no-obsolete-elements.md) | disallow obsolete HTML elements | | | |
256256
| [template-no-outlet-outside-routes](docs/rules/template-no-outlet-outside-routes.md) | disallow {{outlet}} outside of route templates | | | |
257257
| [template-no-page-title-component](docs/rules/template-no-page-title-component.md) | disallow usage of ember-page-title component | | | |
258+
| [template-no-unused-block-params](docs/rules/template-no-unused-block-params.md) | disallow unused block parameters in templates | | | |
258259
| [template-no-valueless-arguments](docs/rules/template-no-valueless-arguments.md) | disallow valueless named arguments | | | |
259260
| [template-no-whitespace-for-layout](docs/rules/template-no-whitespace-for-layout.md) | disallow using whitespace for layout purposes | | | |
260261
| [template-no-yield-block-params-to-else-inverse](docs/rules/template-no-yield-block-params-to-else-inverse.md) | disallow yielding block params to else or inverse block | | | |
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# ember/template-no-unused-block-params
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Disallow unused block parameters in templates.
6+
7+
## Rule Details
8+
9+
This rule reports block parameters that are declared but never used within the block.
10+
11+
## Examples
12+
13+
Examples of **incorrect** code for this rule:
14+
15+
```gjs
16+
<template>
17+
{{#each items as |item|}}
18+
Hello
19+
{{/each}}
20+
</template>
21+
22+
<template>
23+
{{#each items as |item index|}}
24+
{{item.name}}
25+
{{/each}}
26+
</template>
27+
```
28+
29+
Examples of **correct** code for this rule:
30+
31+
```gjs
32+
<template>
33+
{{#each items as |item|}}
34+
{{item.name}}
35+
{{/each}}
36+
</template>
37+
38+
<template>
39+
{{#each items as |item index|}}
40+
{{index}}: {{item.name}}
41+
{{/each}}
42+
</template>
43+
44+
<template>
45+
{{#let user as |u|}}
46+
{{u.name}}
47+
{{/let}}
48+
</template>
49+
```
50+
51+
## Related rules
52+
53+
- [eslint/no-unused-vars](https://eslint.org/docs/rules/no-unused-vars)
54+
55+
## References
56+
57+
- [Ember Guides - Block Parameters](https://guides.emberjs.com/release/components/block-content/)
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
function collectChildNodes(n) {
2+
const children = [];
3+
if (n.program) {
4+
children.push(n.program);
5+
}
6+
if (n.inverse) {
7+
children.push(n.inverse);
8+
}
9+
if (n.params) {
10+
children.push(...n.params);
11+
}
12+
if (n.hash?.pairs) {
13+
children.push(...n.hash.pairs.map((p) => p.value));
14+
}
15+
if (n.body) {
16+
children.push(...n.body);
17+
}
18+
if (n.path) {
19+
children.push(n.path);
20+
}
21+
if (n.attributes) {
22+
children.push(...n.attributes.map((a) => a.value));
23+
}
24+
if (n.children) {
25+
children.push(...n.children);
26+
}
27+
return children;
28+
}
29+
30+
function markParamIfUsed(name, blockParams, usedParams, shadowedParams) {
31+
const firstPart = name.split('.')[0];
32+
if (blockParams.includes(firstPart) && !shadowedParams.has(firstPart)) {
33+
usedParams.add(firstPart);
34+
}
35+
}
36+
37+
function isPartialStatement(n) {
38+
return (
39+
(n.type === 'GlimmerMustacheStatement' || n.type === 'GlimmerBlockStatement') &&
40+
n.path?.original === 'partial'
41+
);
42+
}
43+
44+
function buildShadowedSet(shadowedParams, innerBlockParams, outerBlockParams) {
45+
const newShadowed = new Set(shadowedParams);
46+
for (const p of innerBlockParams) {
47+
if (outerBlockParams.includes(p)) {
48+
newShadowed.add(p);
49+
}
50+
}
51+
return newShadowed;
52+
}
53+
54+
/** @type {import('eslint').Rule.RuleModule} */
55+
module.exports = {
56+
meta: {
57+
type: 'suggestion',
58+
docs: {
59+
description: 'disallow unused block parameters in templates',
60+
category: 'Best Practices',
61+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-unused-block-params.md',
62+
templateMode: 'both',
63+
},
64+
schema: [],
65+
messages: {
66+
unusedBlockParam: 'Block param "{{param}}" is unused',
67+
},
68+
originallyFrom: {
69+
name: 'ember-template-lint',
70+
rule: 'lib/rules/no-unused-block-params.js',
71+
docs: 'docs/rule/no-unused-block-params.md',
72+
tests: 'test/unit/rules/no-unused-block-params-test.js',
73+
},
74+
},
75+
76+
create(context) {
77+
return {
78+
GlimmerBlockStatement(node) {
79+
const blockParams = node.program?.blockParams || [];
80+
if (blockParams.length === 0) {
81+
return;
82+
}
83+
84+
const usedParams = new Set();
85+
86+
function checkNode(n, shadowedParams) {
87+
if (!n) {
88+
return;
89+
}
90+
91+
if (n.type === 'GlimmerPathExpression') {
92+
markParamIfUsed(n.original, blockParams, usedParams, shadowedParams);
93+
}
94+
95+
if (n.type === 'GlimmerElementNode') {
96+
markParamIfUsed(n.tag, blockParams, usedParams, shadowedParams);
97+
}
98+
99+
if (isPartialStatement(n)) {
100+
for (const p of blockParams) {
101+
if (!shadowedParams.has(p)) {
102+
usedParams.add(p);
103+
}
104+
}
105+
}
106+
107+
// When entering a nested block, add its blockParams to the shadowed set
108+
if (n.type === 'GlimmerBlockStatement' && n.program?.blockParams?.length > 0) {
109+
const newShadowed = buildShadowedSet(
110+
shadowedParams,
111+
n.program.blockParams,
112+
blockParams
113+
);
114+
checkBlockParts(n, blockParams, usedParams, shadowedParams, newShadowed, checkNode);
115+
return;
116+
}
117+
118+
// Recursively check children
119+
for (const child of collectChildNodes(n)) {
120+
checkNode(child, shadowedParams);
121+
}
122+
}
123+
124+
checkNode(node.program, new Set());
125+
126+
// Find the last index of a used param
127+
let lastUsedIndex = -1;
128+
for (let i = blockParams.length - 1; i >= 0; i--) {
129+
if (usedParams.has(blockParams[i])) {
130+
lastUsedIndex = i;
131+
break;
132+
}
133+
}
134+
135+
// Only report trailing unused params (after the last used one)
136+
const unusedTrailing = blockParams.slice(lastUsedIndex + 1);
137+
138+
if (unusedTrailing.length > 0) {
139+
context.report({
140+
node,
141+
messageId: 'unusedBlockParam',
142+
data: { param: unusedTrailing.join(', ') },
143+
});
144+
}
145+
},
146+
};
147+
},
148+
};
149+
150+
function checkBlockParts(n, blockParams, usedParams, shadowedParams, newShadowed, checkNodeFn) {
151+
// Check the path/params of the block statement itself with current scope
152+
if (n.path) {
153+
checkNodeFn(n.path, shadowedParams);
154+
}
155+
if (n.params) {
156+
for (const param of n.params) {
157+
checkNodeFn(param, shadowedParams);
158+
}
159+
}
160+
if (n.hash?.pairs) {
161+
for (const pair of n.hash.pairs) {
162+
checkNodeFn(pair.value, shadowedParams);
163+
}
164+
}
165+
166+
// Check the program body with the updated shadowed set
167+
if (n.program) {
168+
checkNodeFn(n.program, newShadowed);
169+
}
170+
if (n.inverse) {
171+
checkNodeFn(n.inverse, newShadowed);
172+
}
173+
}

0 commit comments

Comments
 (0)