Skip to content

Commit 4847938

Browse files
Merge pull request #2508 from NullVoxPopuli/nvp/template-lint-extract-rule-template-deprecated-inline-view-helper
Fix locals tracking for deprecated-inline-view
2 parents 24ae541 + 8743641 commit 4847938

4 files changed

Lines changed: 102 additions & 33 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ rules in templates can be disabled with eslint directives with mustache or html
289289
| [no-observers](docs/rules/no-observers.md) | disallow usage of observers || | |
290290
| [no-old-shims](docs/rules/no-old-shims.md) | disallow usage of old shims for modules || 🔧 | |
291291
| [no-string-prototype-extensions](docs/rules/no-string-prototype-extensions.md) | disallow usage of `String` prototype extensions || | |
292-
| [template-deprecated-inline-view-helper](docs/rules/template-deprecated-inline-view-helper.md) | disallow inline {{view}} helper | | | |
292+
| [template-deprecated-inline-view-helper](docs/rules/template-deprecated-inline-view-helper.md) | disallow inline {{view}} helper | | 🔧 | |
293293
| [template-no-action](docs/rules/template-no-action.md) | disallow {{action}} helper | | | |
294294
| [template-no-attrs-in-components](docs/rules/template-no-attrs-in-components.md) | disallow attrs in component templates | | | |
295295
| [template-no-link-to-positional-params](docs/rules/template-no-link-to-positional-params.md) | disallow positional params in LinkTo component | | | |

docs/rules/template-deprecated-inline-view-helper.md

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

3+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
4+
35
> **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.
46
57
<!-- end auto-generated rule header -->

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

Lines changed: 82 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,3 @@
1-
function isViewPath(pathNode) {
2-
return (
3-
pathNode &&
4-
pathNode.type === 'GlimmerPathExpression' &&
5-
pathNode.original &&
6-
pathNode.original.startsWith('view.') &&
7-
pathNode.head?.type !== 'ThisHead' &&
8-
pathNode.head?.type !== 'AtHead'
9-
);
10-
}
11-
121
/** @type {import('eslint').Rule.RuleModule} */
132
module.exports = {
143
meta: {
@@ -19,7 +8,7 @@ module.exports = {
198
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-deprecated-inline-view-helper.md',
209
templateMode: 'loose',
2110
},
22-
fixable: null,
11+
fixable: 'code',
2312
schema: [],
2413
messages: {
2514
deprecated:
@@ -34,13 +23,52 @@ module.exports = {
3423
},
3524

3625
create(context) {
26+
const sourceCode = context.sourceCode;
27+
28+
// Track block param names to avoid false positives on locals like:
29+
// {{#each items as |view|}} {{view.name}} {{/each}}
30+
const localScopes = [];
31+
32+
function pushLocals(params) {
33+
localScopes.push(new Set(params || []));
34+
}
35+
36+
function popLocals() {
37+
localScopes.pop();
38+
}
39+
40+
function isLocal(name) {
41+
for (const scope of localScopes) {
42+
if (scope.has(name)) {
43+
return true;
44+
}
45+
}
46+
return false;
47+
}
48+
49+
function isViewPath(pathNode) {
50+
return (
51+
pathNode &&
52+
pathNode.type === 'GlimmerPathExpression' &&
53+
pathNode.original &&
54+
pathNode.original.startsWith('view.') &&
55+
pathNode.head?.type !== 'ThisHead' &&
56+
pathNode.head?.type !== 'AtHead' &&
57+
!isLocal('view')
58+
);
59+
}
60+
3761
function checkHashForViewPaths(node) {
3862
if (node.hash && node.hash.pairs) {
3963
for (const pair of node.hash.pairs) {
4064
if (isViewPath(pair.value)) {
65+
const strippedValue = pair.value.original.replace('view.', '');
4166
context.report({
4267
node,
4368
messageId: 'deprecated',
69+
fix(fixer) {
70+
return fixer.replaceText(pair.value, strippedValue);
71+
},
4472
});
4573
return true;
4674
}
@@ -52,11 +80,20 @@ module.exports = {
5280
function checkForView(node) {
5381
if (node.path && node.path.type === 'GlimmerPathExpression') {
5482
// Check for {{view ...}} with params or hash pairs
55-
if (node.path.original === 'view') {
56-
if (
57-
(node.hash && node.hash.pairs && node.hash.pairs.length > 0) ||
58-
(node.params && node.params.length > 0)
59-
) {
83+
if (node.path.original === 'view' && !isLocal('view')) {
84+
if (node.params && node.params.length > 0) {
85+
// {{view 'component-name'}} with a single string param is fixable
86+
const firstParam = node.params[0];
87+
const isFixable =
88+
node.params.length === 1 && firstParam.type === 'GlimmerStringLiteral';
89+
context.report({
90+
node,
91+
messageId: 'deprecated',
92+
fix: isFixable ? (fixer) => fixer.replaceText(node, `{{${firstParam.value}}}`) : null,
93+
});
94+
return;
95+
}
96+
if (node.hash && node.hash.pairs && node.hash.pairs.length > 0) {
6097
context.report({
6198
node,
6299
messageId: 'deprecated',
@@ -66,9 +103,13 @@ module.exports = {
66103
}
67104
// Check for {{view.something}} paths
68105
if (isViewPath(node.path)) {
106+
const strippedPath = node.path.original.replace('view.', '');
69107
context.report({
70108
node,
71109
messageId: 'deprecated',
110+
fix(fixer) {
111+
return fixer.replaceText(node.path, strippedPath);
112+
},
72113
});
73114
return;
74115
}
@@ -78,11 +119,33 @@ module.exports = {
78119
}
79120

80121
return {
81-
GlimmerMustacheStatement(node) {
122+
GlimmerBlockStatement(node) {
123+
// Push this block's params into scope so children can detect locals,
124+
// but check the block's own path/hash first (params aren't in scope yet
125+
// for the block's own arguments in real Handlebars semantics).
82126
checkForView(node);
127+
if (node.program && node.program.blockParams) {
128+
pushLocals(node.program.blockParams);
129+
}
130+
},
131+
'GlimmerBlockStatement:exit'(node) {
132+
if (node.program && node.program.blockParams) {
133+
popLocals();
134+
}
83135
},
84136

85-
GlimmerBlockStatement(node) {
137+
GlimmerElementNode(node) {
138+
if (node.blockParams && node.blockParams.length > 0) {
139+
pushLocals(node.blockParams);
140+
}
141+
},
142+
'GlimmerElementNode:exit'(node) {
143+
if (node.blockParams && node.blockParams.length > 0) {
144+
popLocals();
145+
}
146+
},
147+
148+
GlimmerMustacheStatement(node) {
86149
checkForView(node);
87150
},
88151
};

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

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,42 +24,44 @@ ruleTester.run('template-deprecated-inline-view-helper', rule, {
2424
'<template>{{this.view}}</template>',
2525
'<template>{{@view}}</template>',
2626
'<template>{{#let this.prop as |view|}} {{view}} {{/let}}</template>',
27+
// isLocal: view is a block param, view.name should not be flagged
28+
'<template>{{#each items as |view|}} {{view.name}} {{/each}}</template>',
2729
],
2830
invalid: [
2931
{
3032
code: '<template>{{view class="foo"}}</template>',
3133
output: null,
3234
errors: [{ messageId: 'deprecated' }],
3335
},
34-
3536
{
3637
code: "<template>{{view 'awful-fishsticks'}}</template>",
37-
output: null,
38+
output: '<template>{{awful-fishsticks}}</template>',
3839
errors: [{ messageId: 'deprecated' }],
3940
},
4041
{
4142
code: '<template>{{view.bad-fishsticks}}</template>',
42-
output: null,
43+
output: '<template>{{bad-fishsticks}}</template>',
4344
errors: [{ messageId: 'deprecated' }],
4445
},
4546
{
4647
code: '<template>{{view.terrible.fishsticks}}</template>',
47-
output: null,
48+
output: '<template>{{terrible.fishsticks}}</template>',
4849
errors: [{ messageId: 'deprecated' }],
4950
},
5051
{
5152
code: '<template>{{foo-bar bab=good baz=view.qux.qaz boo=okay}}</template>',
52-
output: null,
53+
output: '<template>{{foo-bar bab=good baz=qux.qaz boo=okay}}</template>',
5354
errors: [{ messageId: 'deprecated' }],
5455
},
5556
{
5657
code: '<template><div class="whatever-class" data-foo={{view.hallo}} sure=thing></div></template>',
57-
output: null,
58+
output:
59+
'<template><div class="whatever-class" data-foo={{hallo}} sure=thing></div></template>',
5860
errors: [{ messageId: 'deprecated' }],
5961
},
6062
{
6163
code: '<template>{{#foo-bar derp=view.whoops thing=whatever}}{{/foo-bar}}</template>',
62-
output: null,
64+
output: '<template>{{#foo-bar derp=whoops thing=whatever}}{{/foo-bar}}</template>',
6365
errors: [{ messageId: 'deprecated' }],
6466
},
6567
],
@@ -89,11 +91,13 @@ hbsRuleTester.run('template-deprecated-inline-view-helper', rule, {
8991
'{{this.view}}',
9092
'{{@view}}',
9193
'{{#let this.prop as |view|}} {{view}} {{/let}}',
94+
// isLocal: view is a block param, view.name should not be flagged
95+
'{{#each items as |view|}} {{view.name}} {{/each}}',
9296
],
9397
invalid: [
9498
{
9599
code: "{{view 'awful-fishsticks'}}",
96-
output: null,
100+
output: '{{awful-fishsticks}}',
97101
errors: [
98102
{
99103
message:
@@ -103,7 +107,7 @@ hbsRuleTester.run('template-deprecated-inline-view-helper', rule, {
103107
},
104108
{
105109
code: '{{view.bad-fishsticks}}',
106-
output: null,
110+
output: '{{bad-fishsticks}}',
107111
errors: [
108112
{
109113
message:
@@ -113,7 +117,7 @@ hbsRuleTester.run('template-deprecated-inline-view-helper', rule, {
113117
},
114118
{
115119
code: '{{view.terrible.fishsticks}}',
116-
output: null,
120+
output: '{{terrible.fishsticks}}',
117121
errors: [
118122
{
119123
message:
@@ -123,7 +127,7 @@ hbsRuleTester.run('template-deprecated-inline-view-helper', rule, {
123127
},
124128
{
125129
code: '{{foo-bar bab=good baz=view.qux.qaz boo=okay}}',
126-
output: null,
130+
output: '{{foo-bar bab=good baz=qux.qaz boo=okay}}',
127131
errors: [
128132
{
129133
message:
@@ -133,7 +137,7 @@ hbsRuleTester.run('template-deprecated-inline-view-helper', rule, {
133137
},
134138
{
135139
code: '<div class="whatever-class" data-foo={{view.hallo}} sure=thing></div>',
136-
output: null,
140+
output: '<div class="whatever-class" data-foo={{hallo}} sure=thing></div>',
137141
errors: [
138142
{
139143
message:
@@ -143,7 +147,7 @@ hbsRuleTester.run('template-deprecated-inline-view-helper', rule, {
143147
},
144148
{
145149
code: '{{#foo-bar derp=view.whoops thing=whatever}}{{/foo-bar}}',
146-
output: null,
150+
output: '{{#foo-bar derp=whoops thing=whatever}}{{/foo-bar}}',
147151
errors: [
148152
{
149153
message:

0 commit comments

Comments
 (0)