Skip to content

Commit 3ec56cf

Browse files
Merge pull request #2668 from johanrd/night_fix/template-no-unbound
Post-merge-review: Fix `template-no-unbound` false positive in GJS/GTS
2 parents 55cd985 + 51d70ca commit 3ec56cf

3 files changed

Lines changed: 67 additions & 6 deletions

File tree

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-no-unbound.js

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ module.exports = {
66
description: 'disallow {{unbound}} helper',
77
category: 'Deprecations',
88
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-unbound.md',
9-
templateMode: 'loose',
9+
templateMode: 'both',
1010
},
1111
schema: [],
1212
messages: { unexpected: 'Unexpected {{unboundHelper}} usage.' },
@@ -18,15 +18,46 @@ module.exports = {
1818
},
1919
},
2020
create(context) {
21+
// `unbound` is an ambient strict-mode keyword (registered in Ember's
22+
// STRICT_MODE_KEYWORDS, backed by BUILTIN_KEYWORD_HELPERS.unbound), so
23+
// `{{unbound foo}}` works in .gjs/.gts without an import. Flag it
24+
// everywhere unless shadowed by a JS binding or template block param —
25+
// ember-eslint-parser registers template block params in scope, so a
26+
// single getScope walk covers both.
27+
const sourceCode = context.sourceCode;
28+
29+
function isInScope(node, name) {
30+
if (!sourceCode) {
31+
return false;
32+
}
33+
try {
34+
let scope = sourceCode.getScope(node);
35+
while (scope) {
36+
if (scope.variables.some((v) => v.name === name)) {
37+
return true;
38+
}
39+
scope = scope.upper;
40+
}
41+
} catch {
42+
// getScope not available in .hbs-only mode
43+
}
44+
return false;
45+
}
46+
2147
function check(node) {
22-
if (node.path?.type === 'GlimmerPathExpression' && node.path.original === 'unbound') {
48+
if (
49+
node.path?.type === 'GlimmerPathExpression' &&
50+
node.path.original === 'unbound' &&
51+
!isInScope(node, 'unbound')
52+
) {
2353
context.report({
2454
node,
2555
messageId: 'unexpected',
2656
data: { unboundHelper: '{{unbound}}' },
2757
});
2858
}
2959
}
60+
3061
return {
3162
GlimmerMustacheStatement: check,
3263
GlimmerBlockStatement: check,

tests/lib/rules/template-no-unbound.js

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,40 @@ const gjsRuleTester = new RuleTester({
3535
});
3636

3737
gjsRuleTester.run('template-no-unbound', rule, {
38-
valid: validHbs.map(wrapTemplate),
39-
invalid: invalidHbs.map(wrapTemplate),
38+
valid: [
39+
...validHbs.map(wrapTemplate),
40+
// JS-scope shadowing: a user-imported `unbound` is not the Glimmer keyword.
41+
{
42+
filename: 'test.gjs',
43+
code: "import unbound from './my-unbound-helper';\n<template>{{unbound foo}}</template>",
44+
},
45+
{
46+
filename: 'test.gts',
47+
code: "import unbound from '@some/addon';\n<template>{{my-thing foo=(unbound foo)}}</template>",
48+
},
49+
// Local block-param shadowing.
50+
{
51+
filename: 'test.gjs',
52+
code: '<template>{{#let (component "foo") as |unbound|}}{{unbound}}{{/let}}</template>',
53+
},
54+
],
55+
invalid: [
56+
...invalidHbs.map(wrapTemplate),
57+
// `unbound` is an ambient Glimmer keyword in strict mode — flag bare uses
58+
// without a shadowing import or block param.
59+
{
60+
filename: 'test.gjs',
61+
code: '<template>{{unbound foo}}</template>',
62+
output: null,
63+
errors: [{ messageId: 'unexpected' }],
64+
},
65+
{
66+
filename: 'test.gts',
67+
code: '<template>{{my-thing foo=(unbound foo)}}</template>',
68+
output: null,
69+
errors: [{ messageId: 'unexpected' }],
70+
},
71+
],
4072
});
4173

4274
const hbsRuleTester = new RuleTester({

0 commit comments

Comments
 (0)