Skip to content

Commit c8cb6fc

Browse files
committed
Fixes
1 parent d453b65 commit c8cb6fc

9 files changed

Lines changed: 389 additions & 9 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ rules in templates can be disabled with eslint directives with mustache or html
242242
| [template-no-at-ember-render-modifiers](docs/rules/template-no-at-ember-render-modifiers.md) | disallow usage of @ember/render-modifiers | ![badge-strict-gjs][] ![badge-strict-gts][] | | |
243243
| [template-no-attribute-splat-on-html-element](docs/rules/template-no-attribute-splat-on-html-element.md) | disallow ...attributes on HTML elements | ![badge-strict-gjs][] ![badge-strict-gts][] | | |
244244
| [template-no-bare-strings](docs/rules/template-no-bare-strings.md) | disallow bare strings in templates (require translation/localization) | ![badge-strict-gjs][] ![badge-strict-gts][] | | |
245-
| [template-no-bare-yield](docs/rules/template-no-bare-yield.md) | disallow {{yield}} without parameters outside of contextual components | | | |
245+
| [template-no-bare-yield](docs/rules/template-no-bare-yield.md) | disallow {{yield}} without parameters outside of contextual components | ![badge-strict-gjs][] ![badge-strict-gts][] | | |
246246
| [template-no-block-params](docs/rules/template-no-block-params.md) | disallow yielding/invoking a component block without parameters | ![badge-strict-gjs][] ![badge-strict-gts][] | | |
247247
| [template-no-block-params-for-html-elements](docs/rules/template-no-block-params-for-html-elements.md) | disallow block params on HTML elements | ![badge-strict-gjs][] ![badge-strict-gts][] | | |
248248
| [template-no-builtin-form-components](docs/rules/template-no-builtin-form-components.md) | disallow usage of built-in form components | ![badge-strict-gjs][] ![badge-strict-gts][] | | |

docs/rules/template-no-bare-yield.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# ember/template-no-bare-yield
22

3+
💼 This rule is enabled in the following [configs](https://github.com/ember-cli/eslint-plugin-ember#-configurations): `strict-gjs`, `strict-gts`.
4+
35
<!-- end auto-generated rule header -->
46

57
✅ The `extends: 'plugin:ember/strict-gjs'` or `extends: 'plugin:ember/strict-gts'` property in a configuration file enables this rule.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ module.exports = {
66
description: 'disallow {{yield}} without parameters outside of contextual components',
77
category: 'Best Practices',
88
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-bare-yield.md',
9+
strictGjs: true,
10+
strictGts: true,
911
},
1012
schema: [],
1113
messages: {
1214
noBareYield: 'yield should have parameters or be used in contextual components only.',
1315
},
14-
strictGjs: true,
15-
strictGts: true,
1616
},
1717

1818
create(context) {

lib/rules/template-sort-invocations.js

Lines changed: 232 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,239 @@ module.exports = {
1212
},
1313
fixable: null,
1414
schema: [],
15-
messages: {},
15+
messages: {
16+
attributeOrder:
17+
'`{{attributeName}}` must appear after `{{expectedAfter}}`',
18+
modifierOrder: '`{{{{modifierName}}}}` must appear after `{{{{expectedAfter}}}}`',
19+
hashPairOrder: '`{{hashPairName}}` must appear after `{{expectedAfter}}`',
20+
splattributesOrder: '`...attributes` must appear after modifiers',
21+
},
1622
},
1723

18-
create() {
19-
// For now, this is a stub implementation
20-
// A full implementation would check attribute/modifier ordering
21-
return {};
24+
create(context) {
25+
function getAttributeName(node) {
26+
return node.name;
27+
}
28+
29+
function getAttributePosition(node) {
30+
const name = getAttributeName(node);
31+
32+
if (name.startsWith('@')) {
33+
return 1; // Arguments first
34+
}
35+
36+
if (name === '...attributes') {
37+
return 3; // Splattributes last
38+
}
39+
40+
return 2; // Regular attributes in the middle
41+
}
42+
43+
function getHashPairName(node) {
44+
return node.key;
45+
}
46+
47+
function getModifierName(node) {
48+
if (node.path.type !== 'GlimmerPathExpression') {
49+
return '';
50+
}
51+
52+
return node.path.original;
53+
}
54+
55+
function compareAttributes(a, b) {
56+
const positionA = getAttributePosition(a);
57+
const positionB = getAttributePosition(b);
58+
59+
if (positionA !== positionB) {
60+
return positionA - positionB;
61+
}
62+
63+
const nameA = getAttributeName(a);
64+
const nameB = getAttributeName(b);
65+
66+
return nameA.localeCompare(nameB);
67+
}
68+
69+
function compareHashPairs(a, b) {
70+
const nameA = getHashPairName(a);
71+
const nameB = getHashPairName(b);
72+
73+
return nameA.localeCompare(nameB);
74+
}
75+
76+
function compareModifiers(a, b) {
77+
const nameA = getModifierName(a);
78+
const nameB = getModifierName(b);
79+
80+
if (nameA !== nameB) {
81+
return nameA.localeCompare(nameB);
82+
}
83+
84+
// For 'on' modifiers, sort by event name
85+
if (nameA === 'on' && a.params && b.params && a.params.length > 0 && b.params.length > 0) {
86+
const eventA = a.params[0];
87+
const eventB = b.params[0];
88+
89+
if (eventA.type === 'GlimmerStringLiteral' && eventB.type === 'GlimmerStringLiteral') {
90+
return eventA.value.localeCompare(eventB.value);
91+
}
92+
}
93+
94+
return 0;
95+
}
96+
97+
function getUnsortedAttributeIndex(attributes) {
98+
return attributes.findIndex((attribute, index) => {
99+
if (index === attributes.length - 1) {
100+
return false;
101+
}
102+
103+
return compareAttributes(attribute, attributes[index + 1]) > 0;
104+
});
105+
}
106+
107+
function getUnsortedHashPairIndex(pairs) {
108+
return pairs.findIndex((hashPair, index) => {
109+
if (index === pairs.length - 1) {
110+
return false;
111+
}
112+
113+
return compareHashPairs(hashPair, pairs[index + 1]) > 0;
114+
});
115+
}
116+
117+
function getUnsortedModifierIndex(modifiers) {
118+
return modifiers.findIndex((modifier, index) => {
119+
if (index === modifiers.length - 1) {
120+
return false;
121+
}
122+
123+
return compareModifiers(modifier, modifiers[index + 1]) > 0;
124+
});
125+
}
126+
127+
function canSkipSplattributesLast(node) {
128+
const { attributes, modifiers } = node;
129+
130+
if (!attributes || attributes.length === 0 || !modifiers || modifiers.length === 0) {
131+
return true;
132+
}
133+
134+
const splattributes = attributes[attributes.length - 1];
135+
const lastModifier = modifiers[modifiers.length - 1];
136+
137+
if (!splattributes || splattributes.name !== '...attributes' || !lastModifier) {
138+
return true;
139+
}
140+
141+
// Check that ...attributes appears after the last modifier
142+
const splattributesPosition = splattributes.loc.start;
143+
const lastModifierPosition = lastModifier.loc.start;
144+
145+
if (splattributesPosition.line > lastModifierPosition.line) {
146+
return true;
147+
}
148+
149+
return (
150+
splattributesPosition.line === lastModifierPosition.line &&
151+
splattributesPosition.column > lastModifierPosition.column
152+
);
153+
}
154+
155+
return {
156+
GlimmerElementNode(node) {
157+
const { attributes, modifiers } = node;
158+
159+
if (attributes && attributes.length > 1) {
160+
const index = getUnsortedAttributeIndex(attributes);
161+
162+
if (index !== -1) {
163+
context.report({
164+
node: attributes[index],
165+
messageId: 'attributeOrder',
166+
data: {
167+
attributeName: getAttributeName(attributes[index]),
168+
expectedAfter: getAttributeName(attributes[index + 1]),
169+
},
170+
});
171+
}
172+
}
173+
174+
if (modifiers && modifiers.length > 1) {
175+
const index = getUnsortedModifierIndex(modifiers);
176+
177+
if (index !== -1) {
178+
context.report({
179+
node: modifiers[index],
180+
messageId: 'modifierOrder',
181+
data: {
182+
modifierName: getModifierName(modifiers[index]),
183+
expectedAfter: getModifierName(modifiers[index + 1]),
184+
},
185+
});
186+
}
187+
}
188+
189+
if (!canSkipSplattributesLast(node)) {
190+
const splattributes = attributes[attributes.length - 1];
191+
context.report({
192+
node: splattributes,
193+
messageId: 'splattributesOrder',
194+
});
195+
}
196+
},
197+
198+
GlimmerBlockStatement(node) {
199+
if (node.hash && node.hash.pairs && node.hash.pairs.length > 1) {
200+
const index = getUnsortedHashPairIndex(node.hash.pairs);
201+
202+
if (index !== -1) {
203+
context.report({
204+
node: node.hash.pairs[index],
205+
messageId: 'hashPairOrder',
206+
data: {
207+
hashPairName: getHashPairName(node.hash.pairs[index]),
208+
expectedAfter: getHashPairName(node.hash.pairs[index + 1]),
209+
},
210+
});
211+
}
212+
}
213+
},
214+
215+
GlimmerMustacheStatement(node) {
216+
if (node.hash && node.hash.pairs && node.hash.pairs.length > 1) {
217+
const index = getUnsortedHashPairIndex(node.hash.pairs);
218+
219+
if (index !== -1) {
220+
context.report({
221+
node: node.hash.pairs[index],
222+
messageId: 'hashPairOrder',
223+
data: {
224+
hashPairName: getHashPairName(node.hash.pairs[index]),
225+
expectedAfter: getHashPairName(node.hash.pairs[index + 1]),
226+
},
227+
});
228+
}
229+
}
230+
},
231+
232+
GlimmerSubExpression(node) {
233+
if (node.hash && node.hash.pairs && node.hash.pairs.length > 1) {
234+
const index = getUnsortedHashPairIndex(node.hash.pairs);
235+
236+
if (index !== -1) {
237+
context.report({
238+
node: node.hash.pairs[index],
239+
messageId: 'hashPairOrder',
240+
data: {
241+
hashPairName: getHashPairName(node.hash.pairs[index]),
242+
expectedAfter: getHashPairName(node.hash.pairs[index + 1]),
243+
},
244+
});
245+
}
246+
}
247+
},
248+
};
22249
},
23250
};

lib/strict-rules-gjs.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ module.exports = {
2929
"ember/template-no-attrs-in-components": "error",
3030
"ember/template-no-autofocus-attribute": "error",
3131
"ember/template-no-bare-strings": "error",
32+
"ember/template-no-bare-yield": "error",
3233
"ember/template-no-block-params-for-html-elements": "error",
3334
"ember/template-no-block-params": "error",
3435
"ember/template-no-builtin-form-components": "error",

lib/strict-rules-gts.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ module.exports = {
2929
"ember/template-no-attrs-in-components": "error",
3030
"ember/template-no-autofocus-attribute": "error",
3131
"ember/template-no-bare-strings": "error",
32+
"ember/template-no-bare-yield": "error",
3233
"ember/template-no-block-params-for-html-elements": "error",
3334
"ember/template-no-block-params": "error",
3435
"ember/template-no-builtin-form-components": "error",

tests/__snapshots__/recommended.js.snap

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@ exports[`recommended rules > has the right list 1`] = `
8282
"require-tagless-components",
8383
"require-valid-css-selector-in-test-helpers",
8484
"routes-segments-snake-case",
85+
"template-no-curly-component-invocation",
86+
"template-no-extra-mut-helper-argument",
87+
"template-no-index-component-invocation",
88+
"template-no-redundant-role",
89+
"template-no-splattributes-with-class",
90+
"template-no-yield-block-params-to-else-inverse",
91+
"template-require-iframe-src-attribute",
92+
"template-require-splattributes",
93+
"template-require-valid-named-block-naming-format",
94+
"template-simple-modifiers",
8595
"use-brace-expansion",
8696
"use-ember-data-rfc-395-imports",
8797
]

tests/lib/rules-preprocessor/gjs-gts-parser-test.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ function initESLint(parser = gjsGtsParser) {
5252
],
5353
rules: {
5454
'no-trailing-spaces': 'error',
55+
// Disable newly added recommended rules that would cause test failures
56+
'ember/template-no-curly-component-invocation': 'off',
57+
'ember/template-no-extra-mut-helper-argument': 'off',
58+
'ember/template-no-index-component-invocation': 'off',
59+
'ember/template-no-redundant-role': 'off',
60+
'ember/template-no-splattributes-with-class': 'off',
61+
'ember/template-no-yield-block-params-to-else-inverse': 'off',
62+
'ember/template-require-iframe-src-attribute': 'off',
63+
'ember/template-require-splattributes': 'off',
64+
'ember/template-require-valid-named-block-naming-format': 'off',
65+
'ember/template-simple-modifiers': 'off',
5566
},
5667
},
5768
],
@@ -65,6 +76,18 @@ function initESLint(parser = gjsGtsParser) {
6576
'ember/no-get': 'off',
6677
'ember/no-array-prototype-extensions': 'error',
6778
'ember/no-unused-services': 'error',
79+
// Disable newly added recommended rules that would cause test failures
80+
// These tests are for parser functionality, not for these specific rules
81+
'ember/template-no-curly-component-invocation': 'off',
82+
'ember/template-no-extra-mut-helper-argument': 'off',
83+
'ember/template-no-index-component-invocation': 'off',
84+
'ember/template-no-redundant-role': 'off',
85+
'ember/template-no-splattributes-with-class': 'off',
86+
'ember/template-no-yield-block-params-to-else-inverse': 'off',
87+
'ember/template-require-iframe-src-attribute': 'off',
88+
'ember/template-require-splattributes': 'off',
89+
'ember/template-require-valid-named-block-naming-format': 'off',
90+
'ember/template-simple-modifiers': 'off',
6891
},
6992
},
7093
});
@@ -804,6 +827,17 @@ describe('multiple tokens in same file', () => {
804827
rules: {
805828
'no-trailing-spaces': 'error',
806829
'@typescript-eslint/prefer-string-starts-ends-with': 'error',
830+
// Disable newly added recommended rules that would cause test failures
831+
'ember/template-no-curly-component-invocation': 'off',
832+
'ember/template-no-extra-mut-helper-argument': 'off',
833+
'ember/template-no-index-component-invocation': 'off',
834+
'ember/template-no-redundant-role': 'off',
835+
'ember/template-no-splattributes-with-class': 'off',
836+
'ember/template-no-yield-block-params-to-else-inverse': 'off',
837+
'ember/template-require-iframe-src-attribute': 'off',
838+
'ember/template-require-splattributes': 'off',
839+
'ember/template-require-valid-named-block-naming-format': 'off',
840+
'ember/template-simple-modifiers': 'off',
807841
},
808842
},
809843
{
@@ -820,6 +854,17 @@ describe('multiple tokens in same file', () => {
820854
],
821855
rules: {
822856
'no-trailing-spaces': 'error',
857+
// Disable newly added recommended rules that would cause test failures
858+
'ember/template-no-curly-component-invocation': 'off',
859+
'ember/template-no-extra-mut-helper-argument': 'off',
860+
'ember/template-no-index-component-invocation': 'off',
861+
'ember/template-no-redundant-role': 'off',
862+
'ember/template-no-splattributes-with-class': 'off',
863+
'ember/template-no-yield-block-params-to-else-inverse': 'off',
864+
'ember/template-require-iframe-src-attribute': 'off',
865+
'ember/template-require-splattributes': 'off',
866+
'ember/template-require-valid-named-block-naming-format': 'off',
867+
'ember/template-simple-modifiers': 'off',
823868
},
824869
},
825870
],

0 commit comments

Comments
 (0)