Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 37 additions & 16 deletions lib/rules/template-no-shadowed-elements.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,32 @@
const htmlTags = require('html-tags');
// Mirror upstream ember-template-lint's inverse-of-isAngleBracketComponent logic.
// A tag is treated as an HTML element only when it:
// - does NOT contain ':' (named blocks like <:slot>)
// - does NOT contain '.' (path/namespaced invocations like <foo.bar>)
// - does NOT start with '@' (argument invocations like <@foo>)
// - has NO uppercase letters (component invocations like <MyThing>)
// - does NOT contain '-' (HTML custom elements like <my-element>)
// Everything else is a component / custom-element / slot — not a plain HTML element.
function isHtmlElement(tagName) {
if (!tagName) {
return false;
}
if (tagName.startsWith('@')) {
return false;
}
if (tagName.includes(':')) {
return false;
}
if (tagName.includes('.')) {
return false;
}
if (tagName.includes('-')) {
return false;
}
if (tagName !== tagName.toLowerCase()) {
return false;
}
return true;
}

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
Expand All @@ -24,8 +52,6 @@ module.exports = {
},

create(context) {
const HTML_ELEMENTS = new Set(htmlTags);

const blockParamScope = [];

function pushScope(params) {
Expand Down Expand Up @@ -68,22 +94,17 @@ module.exports = {
return;
}

const containsDot = tag.includes('.');

if (containsDot) {
// dot paths like bar.baz are not ambiguous
// Mirror upstream: if the tag is an angle-bracket-component (i.e.
// not a plain HTML element — contains '.', is PascalCase, has a
// hyphen, etc.) it cannot be a shadow of a native HTML element.
// Only a lowercase / simple tag that is a local block param is
// considered shadowed. This also covers tags not in any static
// html-tags list (upstream does not restrict to a known set).
if (!isHtmlElement(tag)) {
return;
}

// Only check lowercase elements — a lowercase tag that is a local
// block param and also a native HTML element name is shadowed.
// PascalCase tags (e.g. <Input>, <Form>, <Select>) are Ember/Glimmer
// component invocations and should not be flagged.
const firstChar = tag.charAt(0);
const isLowerCase =
firstChar === firstChar.toLowerCase() && firstChar !== firstChar.toUpperCase();

if (isLowerCase && isLocal(tag) && HTML_ELEMENTS.has(tag)) {
if (isLocal(tag)) {
context.report({
node,
messageId: 'shadowed',
Expand Down
21 changes: 21 additions & 0 deletions tests/lib/rules/template-no-shadowed-elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ ruleTester.run('template-no-shadowed-elements', rule, {
},
],
},
// Upstream flags any lowercase local block-param invocation, not just
// names present in a static html-tags list.
{
code: '<template><FooBar as |foo|><foo></foo></FooBar></template>',
output: null,
errors: [
{
message: 'Component name "foo" shadows HTML element <foo>. Use a different name.',
type: 'GlimmerElementNode',
},
],
},
],
});

Expand Down Expand Up @@ -63,5 +75,14 @@ hbsRuleTester.run('template-no-shadowed-elements', rule, {
{ message: 'Component name "div" shadows HTML element <div>. Use a different name.' },
],
},
// Upstream flags any lowercase local block-param invocation, not just
// names present in a static html-tags list.
{
code: '<FooBar as |foo|><foo></foo></FooBar>',
output: null,
errors: [
{ message: 'Component name "foo" shadows HTML element <foo>. Use a different name.' },
],
},
],
});
Loading