Skip to content

Commit 3d2e871

Browse files
committed
Merge branch 'master' of github.com:johanrd/eslint-plugin-ember into day_fix/template-require-form-method
* 'master' of github.com:johanrd/eslint-plugin-ember: (23 commits) Fix template-no-duplicate-landmark-elements false positive on dynamic aria-label Fix template-no-log false positive on JS scope bindings in GJS/GTS Fix template-no-unbound false positive in GJS/GTS Review feedback: Set, inline sourceCode, document namespace-import limitation Fix template-no-inline-linkto false positive in GJS/GTS Fix template-no-input-block false positive in GJS/GTS Fix prettier formatting Fix template-require-input-label: import-aware <Input>/<Textarea> detection in strict mode Fix template-no-input-tagname: cover angle-bracket <Input @TagName=...> with import tracking in strict mode Fix template-require-has-block-helper: skip JS scope bindings Fix template-no-implicit-this: callee detection, block param scoping, JS scope, bare {{this}} Fix template-no-class-bindings false positive in GJS/GTS Fix template-deprecated-inline-view-helper false positive in GJS/GTS Fix template-deprecated-render-helper false positive in GJS/GTS fix(template-no-invalid-interactive): align interactive element detection with upstream fix(template-no-curly-component-invocation): preserve this./@ prefixes, local names, and JS scope bindings Fix template-no-invalid-link-title: track @ember/routing LinkTo import in GJS/GTS Fix template-no-outlet-outside-routes false positive on JS scope bindings in GJS/GTS Fix template-no-empty-headings: detect this./@/dot-path component children Fix template-no-multiple-empty-lines: handle trailing empty lines ...
2 parents b1f5b6f + 753ca46 commit 3d2e871

45 files changed

Lines changed: 973 additions & 232 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/rules/template-no-dynamic-subexpression-invocations.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,6 @@ This rule disallows invoking helpers dynamically using `this` or `@` properties.
2626
</template>
2727
```
2828

29-
```gjs
30-
<template>
31-
{{this.formatter this.data}}
32-
</template>
33-
```
34-
3529
### Correct ✅
3630

3731
```gjs
@@ -52,6 +46,13 @@ This rule disallows invoking helpers dynamically using `this` or `@` properties.
5246
</template>
5347
```
5448

49+
```gjs
50+
{{! Body-position dynamic helpers are allowed }}
51+
<template>
52+
{{this.formatter this.data}}
53+
</template>
54+
```
55+
5556
## Related Rules
5657

5758
- [template-no-implicit-this](./template-no-implicit-this.md)

docs/rules/template-no-inline-linkto.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,19 @@ Examples of **correct** code for this rule:
6262
</template>
6363
```
6464

65+
```gjs
66+
// User-authored `<LinkTo>` (not from `@ember/routing`) is not flagged in
67+
// strict mode, even when childless.
68+
import LinkTo from './my-link-to-component';
69+
<template>
70+
<LinkTo />
71+
</template>
72+
```
73+
74+
## Strict-mode behavior
75+
76+
In `.gjs`/`.gts` strict mode, `<LinkTo>` only refers to Ember's router link when explicitly imported from `@ember/routing` (this also covers renamed imports such as `import { LinkTo as Link } from '@ember/routing'`). Without that import, `<LinkTo>` is treated as a user-authored component and the rule does not fire. The curly `{{link-to ...}}` form is unreachable in strict mode (`link-to` cannot be a JS identifier) and the autofix is skipped there.
77+
6578
## References
6679

6780
- [eslint-plugin-ember template-no-inline-link-to](https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/template-no-inline-link-to.md)

docs/rules/template-no-unbound.md

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

3-
> **HBS Only**: This rule applies to classic `.hbs` template files only (loose mode). It is not relevant for `gjs`/`gts` files (strict mode), where these patterns cannot occur.
4-
53
<!-- end auto-generated rule header -->
64

75
`{{unbound}}` is a legacy hold over from the days in which Ember's template engine was less performant. Its use today

lib/rules/template-deprecated-inline-view-helper.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ module.exports = {
2323
},
2424

2525
create(context) {
26+
const isStrictMode = context.filename.endsWith('.gjs') || context.filename.endsWith('.gts');
27+
if (isStrictMode) {
28+
return {};
29+
}
30+
2631
const sourceCode = context.sourceCode;
2732

2833
// Track block param names to avoid false positives on locals like:

lib/rules/template-deprecated-render-helper.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ module.exports = {
2323
},
2424

2525
create(context) {
26+
const isStrictMode = context.filename.endsWith('.gjs') || context.filename.endsWith('.gts');
27+
if (isStrictMode) {
28+
return {};
29+
}
30+
2631
const sourceCode = context.sourceCode;
2732

2833
function buildFix(node) {

lib/rules/template-no-chained-this.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,20 @@ module.exports = {
5757
node,
5858
messageId: 'noChainedThis',
5959
fix(fixer) {
60+
const fixes = [];
61+
6062
// Replace the tag name after '<'
6163
const openStart = node.range[0] + 1;
62-
return fixer.replaceTextRange([openStart, openStart + node.tag.length], fixedTag);
64+
fixes.push(fixer.replaceTextRange([openStart, openStart + node.tag.length], fixedTag));
65+
66+
// For non-self-closing elements, also fix the closing tag </tag>
67+
if (!node.selfClosing) {
68+
const closingEnd = node.range[1] - 1; // before '>'
69+
const closingStart = closingEnd - node.tag.length; // start of tag name in </tag>
70+
fixes.push(fixer.replaceTextRange([closingStart, closingEnd], fixedTag));
71+
}
72+
73+
return fixes;
6374
},
6475
});
6576
},

lib/rules/template-no-class-bindings.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ module.exports = {
2323
},
2424

2525
create(context) {
26+
const isStrictMode = context.filename.endsWith('.gjs') || context.filename.endsWith('.gts');
27+
if (isStrictMode) {
28+
return {};
29+
}
30+
2631
const FORBIDDEN_ATTR_NAMES = new Set([
2732
'classBinding',
2833
'@classBinding',

lib/rules/template-no-curly-component-invocation.js

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@ const BUILT_INS = new Set([
3838

3939
const ALWAYS_CURLY = new Set(['yield']);
4040

41-
function transformTagName(name) {
41+
function transformTagName(name, isLocal) {
42+
// Preserve this.*, @*, and local variable names as-is
43+
if (name.startsWith('@') || name.startsWith('this.') || isLocal) {
44+
return name;
45+
}
46+
4247
// Convert kebab-case to PascalCase for angle bracket syntax
4348
const parts = name.split('/');
4449
return parts
@@ -159,6 +164,34 @@ module.exports = {
159164
return blockParamStack.some((params) => params.includes(name));
160165
}
161166

167+
/**
168+
* Returns true if the mustache/block node's path head resolves to a
169+
* JavaScript scope binding (import, const, function param, etc.) —
170+
* i.e. the template reference is an explicit GJS/GTS binding, not an
171+
* implicit resolver lookup. Such references should not be flagged as
172+
* ambiguous curly invocations. Walks `scope.variables` up the chain by
173+
* name, which handles Glimmer built-in names that don't appear in
174+
* `scope.references`.
175+
*/
176+
function isJsScopeBinding(node) {
177+
const name = node.path?.original?.split('.')[0];
178+
if (!sourceCode || !name) {
179+
return false;
180+
}
181+
try {
182+
let scope = sourceCode.getScope(node);
183+
while (scope) {
184+
if (scope.variables.some((v) => v.name === name)) {
185+
return true;
186+
}
187+
scope = scope.upper;
188+
}
189+
} catch {
190+
// sourceCode.getScope may not be available in .hbs-only mode; ignore.
191+
}
192+
return false;
193+
}
194+
162195
function reportMustache(node, pathOriginal) {
163196
const angleBracketName = transformTagName(pathOriginal);
164197
context.report({
@@ -167,13 +200,19 @@ module.exports = {
167200
});
168201
}
169202

170-
function checkMustacheWithNamedArgs(node, pathOriginal, explicitThis) {
203+
function checkMustacheWithNamedArgs(node, pathOriginal, explicitThis, local) {
171204
// {{foo.bar bar=baz}} - multi-part path (not this./@ prefix) with named args
172205
if (!explicitThis && pathOriginal.includes('.')) {
173206
reportMustache(node, pathOriginal);
174207
return;
175208
}
176209

210+
// Explicit JS scope binding or block param — user already imported/scoped
211+
// the value by that exact name; converting to angle-bracket would break.
212+
if (local) {
213+
return;
214+
}
215+
177216
if (config.allow.includes(pathOriginal)) {
178217
return;
179218
}
@@ -266,12 +305,12 @@ module.exports = {
266305

267306
const explicitThis = isExplicitThisPath(pathOriginal);
268307
const firstPart = pathOriginal.split('.')[0];
269-
const local = isLocalVar(firstPart);
308+
const local = isLocalVar(firstPart) || isJsScopeBinding(node);
270309

271310
const hasNamedArguments = node.hash && node.hash.pairs && node.hash.pairs.length > 0;
272311

273312
if (hasNamedArguments) {
274-
checkMustacheWithNamedArgs(node, pathOriginal, explicitThis);
313+
checkMustacheWithNamedArgs(node, pathOriginal, explicitThis, local);
275314
} else {
276315
checkMustacheWithoutNamedArgs(node, pathOriginal, explicitThis, local);
277316
}
@@ -311,7 +350,17 @@ module.exports = {
311350
return;
312351
}
313352

314-
const angleBracketName = transformTagName(pathOriginal);
353+
const firstPart = pathOriginal.split('.')[0];
354+
const local = isLocalVar(firstPart) || isJsScopeBinding(node);
355+
356+
// Explicit JS scope binding: the user imported this exact identifier.
357+
// Converting {{#fooBar}}...{{/fooBar}} to <FooBar>...</FooBar> would
358+
// reference a different (unbound) name. Skip the report entirely.
359+
if (local && !isLocalVar(firstPart)) {
360+
return;
361+
}
362+
363+
const angleBracketName = transformTagName(pathOriginal, local);
315364
context.report({
316365
node,
317366
message: `You are using the component {{#${pathOriginal}}} with curly component syntax. You should use <${angleBracketName}> instead. If it is actually a helper you must manually add it to the 'no-curly-component-invocation' rule configuration, e.g. \`'no-curly-component-invocation': { allow: ['${pathOriginal}'] }\`.`,

lib/rules/template-no-duplicate-landmark-elements.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,15 @@ module.exports = {
157157
isInsideSectioningElement()
158158
);
159159
if (landmarkRole) {
160+
// Dynamic aria-label / aria-labelledby — can't statically determine whether
161+
// this landmark duplicates a sibling, so skip registering it entirely.
162+
const labelAttr =
163+
node.attributes?.find((attr) => attr.name === 'aria-label') ||
164+
node.attributes?.find((attr) => attr.name === 'aria-labelledby');
165+
if (labelAttr && labelAttr.value?.type !== 'GlimmerTextNode') {
166+
return;
167+
}
168+
160169
const landmarks = currentLandmarks();
161170
if (!landmarks.has(landmarkRole)) {
162171
landmarks.set(landmarkRole, []);

lib/rules/template-no-dynamic-subexpression-invocations.js

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -138,17 +138,6 @@ module.exports = {
138138
node,
139139
messageId: 'noDynamicSubexpressionInvocations',
140140
});
141-
return;
142-
}
143-
144-
if (!inAttr && hasArgs) {
145-
// In body context, only flag this.* paths (not @args)
146-
if (node.path.head?.type === 'ThisHead') {
147-
context.report({
148-
node,
149-
messageId: 'noDynamicSubexpressionInvocations',
150-
});
151-
}
152141
}
153142
}
154143
},

0 commit comments

Comments
 (0)