Skip to content

Commit 5234d16

Browse files
NullVoxPopuliclaude
andcommitted
Fix: exempt JS-scope locals from restriction checks in gjs/gts
When in gjs/gts mode, names that resolve to JS-scope variables (imports, const, let, function params) are now treated the same as block params and exempted from restriction checks. Previously, block params were exempt but JS-scope bindings were not, which was inconsistent -- both are local bindings that the developer explicitly controls. Uses sourceCode.getScope(node) to check whether a reference resolves to an actual variable definition (ref.resolved != null), ensuring that ambient/global names are still flagged. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent eac4005 commit 5234d16

2 files changed

Lines changed: 97 additions & 1 deletion

File tree

lib/rules/template-no-restricted-invocations.js

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ module.exports = {
9292
return {};
9393
}
9494

95+
const sourceCode = context.sourceCode;
96+
9597
// Track block params in a scope stack so yielded names are not flagged.
9698
const blockParamScopes = [];
9799

@@ -112,6 +114,37 @@ module.exports = {
112114
return false;
113115
}
114116

117+
/**
118+
* In gjs/gts, check whether a name resolves to a JS-scope variable
119+
* (import, const, let, function param, etc.). If it does, it's a local
120+
* binding and should be exempt from restriction checks — same as block params.
121+
*/
122+
function isJsScopeVariable(node) {
123+
if (!sourceCode) return false;
124+
125+
try {
126+
if (node.type === 'GlimmerElementNode') {
127+
// Element nodes use parts[0] for scope lookup, and need parent scope
128+
if (!node.parts || !node.parts[0]) return false;
129+
const scope = sourceCode.getScope(node.parent);
130+
const ref = scope.references.find((r) => r.identifier === node.parts[0]);
131+
// Only exempt if the reference actually resolves to a JS variable definition
132+
return ref != null && ref.resolved != null;
133+
}
134+
135+
// For mustache/block/sub/modifier statements, check the path's head
136+
if (node.path && node.path.head) {
137+
const scope = sourceCode.getScope(node);
138+
const ref = scope.references.find((r) => r.identifier === node.path.head);
139+
return ref != null && ref.resolved != null;
140+
}
141+
} catch {
142+
// sourceCode.getScope may not be available in .hbs-only mode; ignore.
143+
}
144+
145+
return false;
146+
}
147+
115148
function isRestricted(name) {
116149
for (const item of config) {
117150
if (typeof item === 'string') {
@@ -209,6 +242,14 @@ module.exports = {
209242
return;
210243
}
211244

245+
// In gjs/gts, skip if the tag resolves to a JS-scope variable (import, const, etc.)
246+
if (isJsScopeVariable(node)) {
247+
if (node.blockParams && node.blockParams.length > 0) {
248+
pushBlockParams(node.blockParams);
249+
}
250+
return;
251+
}
252+
212253
const name = getComponentOrHelperName(node);
213254
if (name && !isBlockParam(name)) {
214255
const result = isRestricted(name);
@@ -236,6 +277,7 @@ module.exports = {
236277
modifier.path.original;
237278
if (!modName) continue;
238279
if (isBlockParam(modName)) continue;
280+
if (isJsScopeVariable(modifier)) continue;
239281

240282
const modResult = isRestricted(modName);
241283
if (modResult.restricted) {
@@ -264,6 +306,9 @@ module.exports = {
264306
if (isBlockParam(name)) {
265307
return;
266308
}
309+
if (isJsScopeVariable(node)) {
310+
return;
311+
}
267312

268313
const result = isRestricted(name);
269314
if (result.restricted) {
@@ -278,7 +323,7 @@ module.exports = {
278323

279324
GlimmerBlockStatement(node) {
280325
const name = getComponentOrHelperName(node);
281-
if (name && !isBlockParam(name)) {
326+
if (name && !isBlockParam(name) && !isJsScopeVariable(node)) {
282327
const result = isRestricted(name);
283328
if (result.restricted) {
284329
context.report({
@@ -310,6 +355,9 @@ module.exports = {
310355
if (isBlockParam(name)) {
311356
return;
312357
}
358+
if (isJsScopeVariable(node)) {
359+
return;
360+
}
313361

314362
const result = isRestricted(name);
315363
if (result.restricted) {
@@ -329,6 +377,9 @@ module.exports = {
329377
if (isBlockParam(name)) {
330378
return;
331379
}
380+
if (isJsScopeVariable(node)) {
381+
return;
382+
}
332383

333384
const result = isRestricted(name);
334385
if (result.restricted) {

tests/lib/rules/template-no-restricted-invocations.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,51 @@ ruleTester.run('template-no-restricted-invocations', rule, {
6666
'<template><Random/></template>',
6767
'<template><HelloWorld/></template>',
6868
'<template><NestedScope::Random/></template>',
69+
70+
// JS-scope variables (imports, const, let) should be exempt — same as block params.
71+
{
72+
code: `
73+
import foo from './foo';
74+
<template>{{foo}}</template>
75+
`,
76+
options: [['foo', 'bar']],
77+
},
78+
{
79+
code: `
80+
import Foo from './foo';
81+
<template><Foo /></template>
82+
`,
83+
options: [['foo', 'bar']],
84+
},
85+
{
86+
code: `
87+
const foo = () => {};
88+
<template>{{foo}}</template>
89+
`,
90+
options: [['foo', 'bar']],
91+
},
92+
{
93+
code: `
94+
import foo from './foo';
95+
<template>{{foo "hello"}}</template>
96+
`,
97+
options: [['foo', 'bar']],
98+
},
99+
{
100+
code: `
101+
import bar from './bar';
102+
<template>{{bar}}</template>
103+
`,
104+
options: [['foo', 'bar']],
105+
},
106+
{
107+
code: `
108+
import foo from './foo';
109+
import bar from './bar';
110+
<template>{{foo}}{{bar}}</template>
111+
`,
112+
options: [['foo', 'bar']],
113+
},
69114
],
70115
invalid: [
71116
{

0 commit comments

Comments
 (0)