Skip to content

Commit 70951cf

Browse files
committed
More tests
1 parent 9ebee24 commit 70951cf

16 files changed

Lines changed: 403 additions & 57 deletions

lib/rules/template-no-capital-arguments.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
const RESERVED = new Set(['@arguments', '@args', '@block', '@else']);
2+
13
function isInvalidArgName(name) {
24
return typeof name === 'string' && name.startsWith('@') && /^@[A-Z_]/.test(name);
35
}
46

7+
function isReservedArgName(name) {
8+
return typeof name === 'string' && RESERVED.has(name);
9+
}
10+
511
/** @type {import('eslint').Rule.RuleModule} */
612
module.exports = {
713
meta: {
@@ -18,6 +24,7 @@ module.exports = {
1824
messages: {
1925
noCapitalArguments:
2026
'Argument names should start with lowercase. Use @{{lowercase}} instead of @{{name}}.',
27+
reservedArgument: '{{name}} is reserved argument name, try to use another',
2128
},
2229
originallyFrom: {
2330
name: 'ember-template-lint',
@@ -40,15 +47,29 @@ module.exports = {
4047
});
4148
}
4249

50+
function reportReserved(node, name) {
51+
context.report({
52+
node,
53+
messageId: 'reservedArgument',
54+
data: {
55+
name,
56+
},
57+
});
58+
}
59+
4360
return {
4461
GlimmerPathExpression(node) {
4562
const name = node.head?.name || node.head;
46-
if (isInvalidArgName(name)) {
63+
if (isReservedArgName(name)) {
64+
reportReserved(node, name);
65+
} else if (isInvalidArgName(name)) {
4766
report(node, name);
4867
}
4968
},
5069
GlimmerAttrNode(node) {
51-
if (isInvalidArgName(node.name)) {
70+
if (isReservedArgName(node.name)) {
71+
reportReserved(node, node.name);
72+
} else if (isInvalidArgName(node.name)) {
5273
report(node, node.name);
5374
}
5475
},

lib/rules/template-no-html-comments.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,22 @@ module.exports = {
3333
const htmlCommentRegex = /<!--([\S\s]*?)-->/g;
3434
let match;
3535

36+
const hasTemplateTag = text.includes('<template>');
37+
3638
while ((match = htmlCommentRegex.exec(text)) !== null) {
37-
// Check if this comment is within a template
38-
const beforeComment = text.slice(0, match.index);
39-
const templateStart = beforeComment.lastIndexOf('<template>');
40-
const templateEnd = text.indexOf('</template>', match.index);
39+
let isInTemplate = false;
40+
41+
if (hasTemplateTag) {
42+
const beforeComment = text.slice(0, match.index);
43+
const templateStart = beforeComment.lastIndexOf('<template>');
44+
const templateEnd = text.indexOf('</template>', match.index);
45+
isInTemplate = templateStart !== -1 && templateEnd !== -1;
46+
} else {
47+
// Standalone .hbs file — entire file is template content
48+
isInTemplate = true;
49+
}
4150

42-
if (templateStart !== -1 && templateEnd !== -1) {
51+
if (isInTemplate) {
4352
const commentContent = match[1];
4453
const startIndex = match.index;
4554
const endIndex = match.index + match[0].length;

lib/rules/template-no-yield-only.js

Lines changed: 42 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,23 @@
1+
const IGNORABLE_TYPES = new Set(['GlimmerTextNode', 'GlimmerMustacheCommentStatement']);
2+
3+
function isBareYield(node) {
4+
return (
5+
node.type === 'GlimmerMustacheStatement' &&
6+
node.path &&
7+
node.path.type === 'GlimmerPathExpression' &&
8+
node.path.original === 'yield' &&
9+
(!node.params || node.params.length === 0) &&
10+
(!node.hash || !node.hash.pairs || node.hash.pairs.length === 0)
11+
);
12+
}
13+
14+
function isMeaningfulContent(node) {
15+
if (node.type === 'GlimmerTextNode') {
16+
return node.chars && node.chars.trim().length > 0;
17+
}
18+
return !IGNORABLE_TYPES.has(node.type);
19+
}
20+
121
/** @type {import('eslint').Rule.RuleModule} */
222
module.exports = {
323
meta: {
@@ -23,56 +43,35 @@ module.exports = {
2343
},
2444

2545
create(context) {
26-
return {
27-
// eslint-disable-next-line complexity
28-
GlimmerTemplate(node) {
29-
// The template body contains a GlimmerElementNode for <template>
30-
// The actual content is in its children
31-
if (!node.body || node.body.length === 0) {
32-
return;
33-
}
46+
function checkChildren(children) {
47+
let yieldNode = null;
3448

35-
const templateElement = node.body[0];
36-
if (!templateElement || templateElement.type !== 'GlimmerElementNode') {
49+
for (const child of children) {
50+
if (isBareYield(child)) {
51+
yieldNode = child;
52+
} else if (isMeaningfulContent(child)) {
3753
return;
3854
}
55+
}
3956

40-
const children = templateElement.children || [];
41-
42-
let hasYield = false;
43-
let yieldNode = null;
44-
let hasOtherContent = false;
57+
if (yieldNode) {
58+
context.report({ node: yieldNode, messageId: 'noYieldOnly' });
59+
}
60+
}
4561

46-
for (const child of children) {
47-
if (child.type === 'GlimmerMustacheStatement') {
48-
if (
49-
child.path &&
50-
child.path.type === 'GlimmerPathExpression' &&
51-
child.path.original === 'yield' &&
52-
(!child.params || child.params.length === 0) &&
53-
(!child.hash || !child.hash.pairs || child.hash.pairs.length === 0)
54-
) {
55-
hasYield = true;
56-
yieldNode = child;
57-
} else {
58-
hasOtherContent = true;
59-
}
60-
} else if (child.type === 'GlimmerElementNode') {
61-
hasOtherContent = true;
62-
} else if (
63-
child.type === 'GlimmerTextNode' &&
64-
child.chars &&
65-
child.chars.trim().length > 0
66-
) {
67-
hasOtherContent = true;
68-
}
62+
return {
63+
GlimmerTemplate(node) {
64+
if (!node.body || node.body.length === 0) {
65+
return;
6966
}
7067

71-
if (hasYield && !hasOtherContent && yieldNode) {
72-
context.report({
73-
node: yieldNode,
74-
messageId: 'noYieldOnly',
75-
});
68+
const firstChild = node.body[0];
69+
if (firstChild && firstChild.type === 'GlimmerElementNode') {
70+
// gjs/gts: body[0] is the <template> element, check its children
71+
checkChildren(firstChild.children || []);
72+
} else {
73+
// hbs: body directly contains the template nodes
74+
checkChildren(node.body);
7675
}
7776
},
7877
};

tests/lib/rules/template-no-capital-arguments.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,5 +162,45 @@ hbsRuleTester.run('template-no-capital-arguments (hbs)', rule, {
162162
output: null,
163163
errors: [{ messageId: 'noCapitalArguments' }],
164164
},
165+
{
166+
code: '{{@arguments}}',
167+
output: null,
168+
errors: [{ messageId: 'reservedArgument' }],
169+
},
170+
{
171+
code: '{{@args}}',
172+
output: null,
173+
errors: [{ messageId: 'reservedArgument' }],
174+
},
175+
{
176+
code: '{{@block}}',
177+
output: null,
178+
errors: [{ messageId: 'reservedArgument' }],
179+
},
180+
{
181+
code: '{{@else}}',
182+
output: null,
183+
errors: [{ messageId: 'reservedArgument' }],
184+
},
185+
{
186+
code: '<MyComponent @arguments={{42}} />',
187+
output: null,
188+
errors: [{ messageId: 'reservedArgument' }],
189+
},
190+
{
191+
code: '<MyComponent @args={{42}} />',
192+
output: null,
193+
errors: [{ messageId: 'reservedArgument' }],
194+
},
195+
{
196+
code: '<MyComponent @block={{42}} />',
197+
output: null,
198+
errors: [{ messageId: 'reservedArgument' }],
199+
},
200+
{
201+
code: '<MyComponent @else={{42}} />',
202+
output: null,
203+
errors: [{ messageId: 'reservedArgument' }],
204+
},
165205
],
166206
});

tests/lib/rules/template-no-html-comments.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,17 @@ hbsRuleTester.run('template-no-html-comments', rule, {
5353
'{{!--comment here--}}',
5454
'{{! template-lint-disable no-bare-strings }}',
5555
'{{! template-lint-disable }}',
56-
'{{! template-lint-disable no-html-comments }}<!-- lol -->',
5756
],
58-
invalid: [],
57+
invalid: [
58+
{
59+
code: '<!-- comment here -->',
60+
output: '{{! comment here }}',
61+
errors: [{ messageId: 'noHtmlComments' }],
62+
},
63+
{
64+
code: '<!--comment here-->',
65+
output: '{{!comment here}}',
66+
errors: [{ messageId: 'noHtmlComments' }],
67+
},
68+
],
5969
});

tests/lib/rules/template-no-implicit-this.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,41 @@ const hbsRuleTester = new RuleTester({
107107
});
108108

109109
hbsRuleTester.run('template-no-implicit-this', rule, {
110-
valid: [],
110+
valid: [
111+
// Built-in helpers/keywords
112+
'{{yield}}',
113+
'{{outlet}}',
114+
'{{has-block}}',
115+
'{{has-block-params}}',
116+
'{{debugger}}',
117+
118+
// Named arguments
119+
'{{@book}}',
120+
'{{@book.author}}',
121+
122+
// Explicit this
123+
'{{this.book}}',
124+
'{{this.book.author}}',
125+
126+
// Helpers invoked with arguments
127+
'{{helper argument=true}}',
128+
'{{some-helper argument=true}}',
129+
130+
// PascalCase components
131+
'<WelcomePage />',
132+
'<MyComponent @prop={{@value}} />',
133+
'{{MyComponent}}',
134+
135+
// Named arguments in various positions
136+
'{{@book argument=true}}',
137+
'{{helper argument=@book}}',
138+
'{{#helper argument=@book}}{{/helper}}',
139+
140+
// Explicit this in various positions
141+
'{{this.book argument=true}}',
142+
'{{helper argument=this.book}}',
143+
'{{#helper argument=this.book}}{{/helper}}',
144+
],
111145
invalid: [
112146
{
113147
code: '{{book}}',

tests/lib/rules/template-no-inline-styles.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,10 @@ hbsRuleTester.run('template-no-inline-styles', rule, {
6666
options: [{ allowDynamicStyles: false }],
6767
errors: [{ messageId: 'noInlineStyles' }],
6868
},
69+
{
70+
code: '<div style></div>',
71+
output: null,
72+
errors: [{ messageId: 'noInlineStyles' }],
73+
},
6974
],
7075
});

tests/lib/rules/template-no-invalid-interactive.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ hbsRuleTester.run('template-no-invalid-interactive', rule, {
173173
'<div {{on "scroll" this.handleScroll}}></div>',
174174
'<code {{on "copy" (action @onCopy)}}></code>',
175175
'<img {{on "load" this.onLoad}} {{on "error" this.onError}}>',
176+
'<select {{on "change" this.handleChange}}></select>',
177+
'<details {{on "toggle" this.handleToggle}}></details>',
178+
'<video {{on "pause" this.onPause}}></video>',
179+
'<img {{action "foo" on="load"}}>',
180+
'<img {{action "foo" on="error"}}>',
176181
// additionalInteractiveTags config
177182
{
178183
code: '<div {{on "click" this.onClick}}></div>',
@@ -201,6 +206,25 @@ hbsRuleTester.run('template-no-invalid-interactive', rule, {
201206
},
202207
],
203208
invalid: [
209+
{
210+
code: '<div {{action "foo"}}></div>',
211+
output: null,
212+
errors: [
213+
{
214+
message:
215+
'Non-interactive element <div> should not have interactive handler "{{action}}".',
216+
},
217+
],
218+
},
219+
{
220+
code: '<div onclick={{action "foo"}}></div>',
221+
output: null,
222+
errors: [
223+
{
224+
message: 'Non-interactive element <div> should not have interactive handler "onclick".',
225+
},
226+
],
227+
},
204228
{
205229
code: '<div onclick={{pipe-action "foo"}}></div>',
206230
output: null,

tests/lib/rules/template-no-multiple-empty-lines.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@ hbsRuleTester.run('template-no-multiple-empty-lines', rule, {
131131
code: `<div>foo</div>
132132
133133
134+
<div>bar</div>`,
135+
output: null,
136+
errors: [{ message: 'More than 1 blank line not allowed.' }],
137+
},
138+
{
139+
code: `<div>foo</div>
140+
141+
134142
135143
136144
<div>bar</div>`,

0 commit comments

Comments
 (0)