Skip to content

Commit eaf6f9f

Browse files
Merge pull request #2573 from NullVoxPopuli/nvp/template-lint-extract-rule-template-no-shadowed-elements
Extract rule: template-no-shadowed-elements
2 parents 8a69d30 + b889329 commit eaf6f9f

4 files changed

Lines changed: 222 additions & 5 deletions

File tree

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -405,11 +405,12 @@ rules in templates can be disabled with eslint directives with mustache or html
405405

406406
### Possible Errors
407407

408-
| Name | Description | 💼 | 🔧 | 💡 |
409-
| :------------------------------------------------------------------------------------------- | :-------------------------------------------------------- | :- | :- | :- |
410-
| [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 | | | |
411-
| [template-no-jsx-attributes](docs/rules/template-no-jsx-attributes.md) | disallow JSX-style camelCase attributes | | 🔧 | |
412-
| [template-no-unbalanced-curlies](docs/rules/template-no-unbalanced-curlies.md) | disallow unbalanced mustache curlies | | | |
408+
| Name                                  | Description | 💼 | 🔧 | 💡 |
409+
| :------------------------------------------------------------------------------------------- | :---------------------------------------------------------------- | :- | :- | :- |
410+
| [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 | | | |
411+
| [template-no-jsx-attributes](docs/rules/template-no-jsx-attributes.md) | disallow JSX-style camelCase attributes | | 🔧 | |
412+
| [template-no-shadowed-elements](docs/rules/template-no-shadowed-elements.md) | disallow ambiguity with block param names shadowing HTML elements | | | |
413+
| [template-no-unbalanced-curlies](docs/rules/template-no-unbalanced-curlies.md) | disallow unbalanced mustache curlies | | | |
413414

414415
### Routes
415416

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# ember/template-no-shadowed-elements
2+
3+
<!-- end auto-generated rule header -->
4+
5+
This rule prevents ambiguity in situations where a yielded block param which starts with a lower case letter is also used within the block itself as an element name.
6+
7+
## Examples
8+
9+
This rule **forbids** the following:
10+
11+
```hbs
12+
<FooBar as |div|>
13+
<div></div>
14+
</FooBar>
15+
```
16+
17+
This rule **allows** the following:
18+
19+
```hbs
20+
{{#foo-bar as |Baz|}}
21+
<Baz />
22+
{{/foo-bar}}
23+
```
24+
25+
```hbs
26+
<FooBar as |Baz|>
27+
<Baz />
28+
</FooBar>
29+
```
30+
31+
```hbs
32+
{{#with foo=(component 'blah-zorz') as |Div|}}
33+
<Div />
34+
{{/with}}
35+
```
36+
37+
```hbs
38+
<Foo as |bar|>
39+
<bar.baz />
40+
</Foo>
41+
```
42+
43+
## References
44+
45+
- [Ember guides/block content](https://guides.emberjs.com/release/components/block-content/)
46+
- [rfcs/angle bracket invocation](https://emberjs.github.io/rfcs/0311-angle-bracket-invocation.html)
47+
- [rfcs/named blocks](https://emberjs.github.io/rfcs/0226-named-blocks.html)
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
const htmlTags = require('html-tags');
2+
3+
/** @type {import('eslint').Rule.RuleModule} */
4+
module.exports = {
5+
meta: {
6+
type: 'problem',
7+
docs: {
8+
description: 'disallow ambiguity with block param names shadowing HTML elements',
9+
category: 'Possible Errors',
10+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-shadowed-elements.md',
11+
templateMode: 'both',
12+
},
13+
fixable: null,
14+
schema: [],
15+
messages: {
16+
shadowed: 'Component name "{{name}}" shadows HTML element <{{name}}>. Use a different name.',
17+
},
18+
originallyFrom: {
19+
name: 'ember-template-lint',
20+
rule: 'lib/rules/no-shadowed-elements.js',
21+
docs: 'docs/rule/no-shadowed-elements.md',
22+
tests: 'test/unit/rules/no-shadowed-elements-test.js',
23+
},
24+
},
25+
26+
create(context) {
27+
const HTML_ELEMENTS = new Set(htmlTags);
28+
29+
const blockParamScope = [];
30+
31+
function pushScope(params) {
32+
blockParamScope.push(new Set(params || []));
33+
}
34+
35+
function popScope() {
36+
blockParamScope.pop();
37+
}
38+
39+
function isLocal(name) {
40+
for (const scope of blockParamScope) {
41+
if (scope.has(name)) {
42+
return true;
43+
}
44+
}
45+
return false;
46+
}
47+
48+
return {
49+
GlimmerBlockStatement(node) {
50+
if (node.program && node.program.blockParams) {
51+
pushScope(node.program.blockParams);
52+
}
53+
},
54+
'GlimmerBlockStatement:exit'(node) {
55+
if (node.program && node.program.blockParams) {
56+
popScope();
57+
}
58+
},
59+
60+
GlimmerElementNode(node) {
61+
// Push block params for elements with 'as |...|' syntax
62+
if (node.blockParams && node.blockParams.length > 0) {
63+
pushScope(node.blockParams);
64+
}
65+
66+
const tag = node.tag;
67+
if (!tag) {
68+
return;
69+
}
70+
71+
const containsDot = tag.includes('.');
72+
73+
if (containsDot) {
74+
// dot paths like bar.baz are not ambiguous
75+
return;
76+
}
77+
78+
// Only check lowercase elements — a lowercase tag that is a local
79+
// block param and also a native HTML element name is shadowed.
80+
// PascalCase tags (e.g. <Input>, <Form>, <Select>) are Ember/Glimmer
81+
// component invocations and should not be flagged.
82+
const firstChar = tag.charAt(0);
83+
const isLowerCase =
84+
firstChar === firstChar.toLowerCase() && firstChar !== firstChar.toUpperCase();
85+
86+
if (isLowerCase && isLocal(tag) && HTML_ELEMENTS.has(tag)) {
87+
context.report({
88+
node,
89+
messageId: 'shadowed',
90+
data: { name: tag },
91+
});
92+
}
93+
},
94+
95+
'GlimmerElementNode:exit'(node) {
96+
if (node.blockParams && node.blockParams.length > 0) {
97+
popScope();
98+
}
99+
},
100+
};
101+
},
102+
};
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//------------------------------------------------------------------------------
2+
// Requirements
3+
//------------------------------------------------------------------------------
4+
5+
const rule = require('../../../lib/rules/template-no-shadowed-elements');
6+
const RuleTester = require('eslint').RuleTester;
7+
8+
//------------------------------------------------------------------------------
9+
// Tests
10+
//------------------------------------------------------------------------------
11+
12+
const ruleTester = new RuleTester({
13+
parser: require.resolve('ember-eslint-parser'),
14+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
15+
});
16+
17+
ruleTester.run('template-no-shadowed-elements', rule, {
18+
valid: [
19+
'<template><div>content</div></template>',
20+
'<template><form><input /></form></template>',
21+
'<template>{{#foo-bar as |Baz|}}<Baz />{{/foo-bar}}</template>',
22+
'<template><FooBar as |Baz|><Baz /></FooBar></template>',
23+
'<template>{{#with foo=(component "blah-zorz") as |Div|}}<Div></Div>{{/with}}</template>',
24+
'<template><Foo as |bar|><bar.baz /></Foo></template>',
25+
],
26+
27+
invalid: [
28+
{
29+
code: '<template><FooBar as |div|><div></div></FooBar></template>',
30+
output: null,
31+
errors: [
32+
{
33+
message: 'Component name "div" shadows HTML element <div>. Use a different name.',
34+
type: 'GlimmerElementNode',
35+
},
36+
],
37+
},
38+
],
39+
});
40+
41+
const hbsRuleTester = new RuleTester({
42+
parser: require.resolve('ember-eslint-parser/hbs'),
43+
parserOptions: {
44+
ecmaVersion: 2022,
45+
sourceType: 'module',
46+
},
47+
});
48+
49+
hbsRuleTester.run('template-no-shadowed-elements', rule, {
50+
valid: [
51+
'<div>content</div>',
52+
'<form><input /></form>',
53+
'{{#foo-bar as |Baz|}}<Baz />{{/foo-bar}}',
54+
'<FooBar as |Baz|><Baz /></FooBar>',
55+
'{{#with foo=(component "blah-zorz") as |Div|}}<Div></Div>{{/with}}',
56+
'<Foo as |bar|><bar.baz /></Foo>',
57+
],
58+
invalid: [
59+
{
60+
code: '<FooBar as |div|><div></div></FooBar>',
61+
output: null,
62+
errors: [
63+
{ message: 'Component name "div" shadows HTML element <div>. Use a different name.' },
64+
],
65+
},
66+
],
67+
});

0 commit comments

Comments
 (0)