Skip to content

Commit dba5b9a

Browse files
committed
[BUGFIX lts] Restore {{this.attrs.foo}} deprecation
Previously (as of 3.28) `{{this.attrs.foo}}` in the template was deprecated with a suggestion to switch to `{{@foo}}`, since that is what we internally rewrite that syntax into. When the deprecation was [removed in 4.0][1], we correctly made the observation `this.attrs` is a real property on classic component classes, and accessing it from the template is perfectly legal and should not trigger any deprecation. However, what slipped everyone's mind is that `this.attrs.*` gives you a "Mutable Cell" object. That is not, inheriently problematic. While it's not particularly useful to render these objects directly (it stingifies into `[object Object]`), you can render the `.value` property or perhaps pass them around as values. What slipped everyone's mind however, is that that's not what the previously-deprecated `{{this.attrs.foo}}` does or mean – it gets rewritten into `{{@foo}}`, so in fact, `{{this.attrs.foo}}` is a magic syntax that automagically unwraps the Mutable Cell for you, which is indeed unexpected and the whole point of that originally deprecation. One reason why this slipped our minds and why it was so hard to spot was that the rewrite logic is hidden deeply/inconspicuously as a side-effect inside the `isAttrs()` function. The original intent (and probably too "clever") of the code was probably that: at this point, we already know we are going to throw away and rewrite this node as `{{@foo}}`, we may as well normalize away any differences between `{{attrs.foo}}` and `{{this.attrs.foo}}` to make it easier to write the code for the deprecation message. Note that even with this generous reading, the original code is _still_ incorrect. `node.original.slice(5)` is presumably trying to remove `this.` from the string, and `node.parts.shift()` is presumably trying to do the same. The latter, however, is wrong and unnecessary. `node.parts` does not contain the `this` part to begin with, so this ends up removing `attrs` from the array instead. This mistake wasn't consequential in the original deprecated code, because, as mentioned, the code is going to replace the `{{this.attrs.foo}}` node with a newly built `{{@foo}}` node anyway, and the original node is only used for the purpose of assembling the deprecation message, which doesn't do anything with `node.parts`. When the deprecation was removed though, this side-effect ends up mattering. This modification is an undefined behavior – it left `node.original` and `node.parts` disagreeing with each other (ironically, that was because the original author tried to keep them in-sync, just end up doing it incorrectly). The latest version of glimmer-vm will ensure these kind of modifications stay in-sync, but prior to that, it seems like we end up favoring `node.parts` and so, seemingly without anyone noticing, the code ends up sneakily transfoming `{{this.attrs.foo}}` into `{{this.foo}}`, which was never the intent of any version of this code, but kind of works (for classic components at least) and went unnoticed so far. This wasn't caught in the test, because `assertTransformed` doesn't do what you think. It may sound like that you are giving it the untransformed code (LHS) and assert that, after running the AST, it matches exactly the expected result (RHS). However, that is not at all what it does. The implementation: ```js this.assert.deepEqual(deloc(ast(before)), deloc(ast(after))); ``` As you can see, it does exactly the same thing with the LHS and RHS. All it is doing is confirming that either version of the source code will ultimately normalizes into the same result after running the AST plugins. Which is to say, all existing tests that checks `assertTransformed("blah", "blah")` is testing exactly nothing, because of course compiling the identical template twice will give you the same result. Those tests need to be corrected. Or better yet, change the implementation of `assertTransformed` to do what you would expect, so that we can _actually_ test no-op transforms. This commit doesn't address that and that's left for someone else to pick up. This commit refactors the code for clearity and reintroduces the original deprecation with a 6.0 removal. We probably could have just called it a bugfix, but with the next major on the horrizon anyway, it doesn't hurt to give this a proper deprecation cycle in case someone still uses it. Note that: this has nothing to do with accessing and using `this.attrs.foo` from the JavaScript class in JS context, which is not being deprecated here. [1]: #19660 (comment)
1 parent f847f79 commit dba5b9a

File tree

3 files changed

+95
-56
lines changed

3 files changed

+95
-56
lines changed

Diff for: packages/@ember/-internals/glimmer/tests/integration/components/curly-components-test.js

+39-27
Original file line numberDiff line numberDiff line change
@@ -1270,10 +1270,13 @@ moduleFor(
12701270
}, "Using {{attrs}} to reference named arguments is not supported. {{attrs.someProp}} should be updated to {{@someProp}}. ('my-app/templates/components/non-block.hbs' @ L1:C24) ");
12711271
}
12721272

1273+
// Perhaps change this test to `{{this.attrs.someProp.value}}` when removing the deprecation?
12731274
['@test non-block with properties on this.attrs']() {
1274-
this.registerComponent('non-block', {
1275-
template: 'In layout - someProp: {{this.attrs.someProp}}',
1276-
});
1275+
expectDeprecation(() => {
1276+
this.registerComponent('non-block', {
1277+
template: 'In layout - someProp: {{this.attrs.someProp}}',
1278+
});
1279+
}, /Using {{this.attrs}} to reference named arguments has been deprecated. {{this.attrs.someProp}} should be updated to {{@someProp}}./);
12771280

12781281
this.render('{{non-block someProp=this.prop}}', {
12791282
prop: 'something here',
@@ -1477,21 +1480,24 @@ moduleFor(
14771480
);
14781481
}
14791482

1483+
// Perhaps change this test to `{{this.attrs.foo.value}}` when removing the deprecation?
14801484
['@test this.attrs.foo === @foo === foo']() {
1481-
this.registerComponent('foo-bar', {
1482-
template: strip`
1483-
Args: {{this.attrs.value}} | {{@value}} | {{this.value}}
1484-
{{#each this.attrs.items as |item|}}
1485-
{{item}}
1486-
{{/each}}
1487-
{{#each @items as |item|}}
1488-
{{item}}
1489-
{{/each}}
1490-
{{#each this.items as |item|}}
1491-
{{item}}
1492-
{{/each}}
1493-
`,
1494-
});
1485+
expectDeprecation(() => {
1486+
this.registerComponent('foo-bar', {
1487+
template: strip`
1488+
Args: {{this.attrs.value}} | {{@value}} | {{this.value}}
1489+
{{#each this.attrs.items as |item|}}
1490+
{{item}}
1491+
{{/each}}
1492+
{{#each @items as |item|}}
1493+
{{item}}
1494+
{{/each}}
1495+
{{#each this.items as |item|}}
1496+
{{item}}
1497+
{{/each}}
1498+
`,
1499+
});
1500+
}, /Using {{this.attrs}} to reference named arguments has been deprecated. {{this.attrs..+}} should be updated to {{@.+}}./);
14951501

14961502
this.render('{{foo-bar value=this.model.value items=this.model.items}}', {
14971503
model: {
@@ -1576,10 +1582,13 @@ moduleFor(
15761582
}, "Using {{attrs}} to reference named arguments is not supported. {{attrs.someProp}} should be updated to {{@someProp}}. ('my-app/templates/components/with-block.hbs' @ L1:C24) ");
15771583
}
15781584

1585+
// Perhaps change this test to `{{this.attrs.someProp.value}}` when removing the deprecation?
15791586
['@test block with properties on this.attrs']() {
1580-
this.registerComponent('with-block', {
1581-
template: 'In layout - someProp: {{this.attrs.someProp}} - {{yield}}',
1582-
});
1587+
expectDeprecation(() => {
1588+
this.registerComponent('with-block', {
1589+
template: 'In layout - someProp: {{this.attrs.someProp}} - {{yield}}',
1590+
});
1591+
}, /Using {{this.attrs}} to reference named arguments has been deprecated. {{this.attrs.someProp}} should be updated to {{@someProp}}./);
15831592

15841593
this.render(
15851594
strip`
@@ -3319,16 +3328,19 @@ moduleFor(
33193328
}, "Using {{attrs}} to reference named arguments is not supported. {{attrs.myVar}} should be updated to {{@myVar}}. ('my-app/templates/components/foo-bar.hbs' @ L1:C10) ");
33203329
}
33213330

3331+
// Perhaps change this test to `{{this.attrs.myVar.value}}` when removing the deprecation?
33223332
['@test using this.attrs for positional params']() {
33233333
let MyComponent = Component.extend();
33243334

3325-
this.registerComponent('foo-bar', {
3326-
ComponentClass: MyComponent.reopenClass({
3327-
positionalParams: ['myVar'],
3328-
}),
3329-
template:
3330-
'MyVar1: {{this.attrs.myVar}} {{this.myVar}} MyVar2: {{this.myVar2}} {{this.attrs.myVar2}}',
3331-
});
3335+
expectDeprecation(() => {
3336+
this.registerComponent('foo-bar', {
3337+
ComponentClass: MyComponent.reopenClass({
3338+
positionalParams: ['myVar'],
3339+
}),
3340+
template:
3341+
'MyVar1: {{this.attrs.myVar}} {{this.myVar}} MyVar2: {{this.myVar2}} {{this.attrs.myVar2}}',
3342+
});
3343+
}, /Using {{this.attrs}} to reference named arguments has been deprecated. {{this.attrs.myVar2?}} should be updated to {{@myVar2?}}./);
33323344

33333345
this.render('{{foo-bar 1 myVar2=2}}');
33343346

Diff for: packages/ember-template-compiler/lib/plugins/assert-against-attrs.ts

+36-21
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { assert } from '@ember/debug';
1+
import { assert, deprecate } from '@ember/debug';
22
import type { AST, ASTPlugin } from '@glimmer/syntax';
33
import calculateLocationDisplay from '../system/calculate-location-display';
44
import type { EmberASTPluginEnvironment } from '../types';
@@ -56,17 +56,41 @@ export default function assertAgainstAttrs(env: EmberASTPluginEnvironment): ASTP
5656

5757
PathExpression(node: AST.PathExpression): AST.Node | void {
5858
if (isAttrs(node, stack[stack.length - 1]!)) {
59-
let path = b.path(node.original.substring(6));
60-
6159
assert(
62-
`Using {{attrs}} to reference named arguments is not supported. {{attrs.${
63-
path.original
64-
}}} should be updated to {{@${path.original}}}. ${calculateLocationDisplay(
60+
`Using {{attrs}} to reference named arguments is not supported. {{${
61+
node.original
62+
}}} should be updated to {{@${node.original.slice(6)}}}. ${calculateLocationDisplay(
63+
moduleName,
64+
node.loc
65+
)}`
66+
);
67+
} else if (isThisDotAttrs(node)) {
68+
// When removing this, ensure `{{this.attrs.foo}}` is left as-is, without triggering
69+
// any assertions/deprecations. It's perfectly legal to reference `{{this.attrs.foo}}`
70+
// in the template since it is a real property on the backing class – it will give you
71+
// a `MutableCell` wrapper object, but maybe that's what you want. And in any case,
72+
// there is no compelling to special case that property access.
73+
deprecate(
74+
`Using {{this.attrs}} to reference named arguments has been deprecated. {{${
75+
node.original
76+
}}} should be updated to {{@${node.original.slice(11)}}}. ${calculateLocationDisplay(
6577
moduleName,
6678
node.loc
6779
)}`,
68-
node.this !== false
80+
false,
81+
{
82+
id: 'attrs-arg-access',
83+
url: 'https://deprecations.emberjs.com/v3.x/#toc_attrs-arg-access',
84+
until: '6.0.0',
85+
for: 'ember-source',
86+
since: {
87+
available: '3.26.0',
88+
enabled: '3.26.0',
89+
},
90+
}
6991
);
92+
93+
return b.path(`@${node.original.slice(11)}`, node.loc);
7094
}
7195
},
7296
},
@@ -75,19 +99,10 @@ export default function assertAgainstAttrs(env: EmberASTPluginEnvironment): ASTP
7599

76100
function isAttrs(node: AST.PathExpression, symbols: string[]) {
77101
let name = node.parts[0];
102+
return node.head.type === 'VarHead' && name === 'attrs' && symbols.indexOf(name) === -1;
103+
}
78104

79-
if (name && symbols.indexOf(name) !== -1) {
80-
return false;
81-
}
82-
83-
if (name === 'attrs') {
84-
if (node.this === true) {
85-
node.parts.shift();
86-
node.original = node.original.slice(5);
87-
}
88-
89-
return true;
90-
}
91-
92-
return false;
105+
function isThisDotAttrs(node: AST.PathExpression) {
106+
let name = node.parts[0];
107+
return node.head.type === 'ThisHead' && name === 'attrs';
93108
}

Diff for: packages/ember-template-compiler/tests/plugins/assert-against-attrs-test.js

+20-8
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,26 @@ moduleFor(
2929
}, /Using {{attrs}} to reference named arguments is not supported. {{attrs.foo.bar}} should be updated to {{@foo.bar}}./);
3030
}
3131

32-
['@test it does not assert against this.attrs']() {
33-
this.assertTransformed(`{{this.attrs.foo}}`, `{{this.attrs.foo}}`);
34-
this.assertTransformed(`{{if this.attrs.foo "foo"}}`, `{{if this.attrs.foo "foo"}}`);
35-
this.assertTransformed(`{{#if this.attrs.foo}}{{/if}}`, `{{#if this.attrs.foo}}{{/if}}`);
36-
this.assertTransformed(
37-
`{{deeply (nested this.attrs.foo.bar)}}`,
38-
`{{deeply (nested this.attrs.foo.bar)}}`
39-
);
32+
// When removing the deprecation, ensure `{{this.attrs.foo}}` isn't rewritten and does NOT trigger any assertions/deprecations
33+
['@test this.attrs is deprecated']() {
34+
expectDeprecation(() => {
35+
this.assertTransformed(`{{this.attrs.foo}}`, `{{@foo}}`);
36+
}, /Using {{this.attrs}} to reference named arguments has been deprecated. {{this.attrs.foo}} should be updated to {{@foo}}./);
37+
38+
expectDeprecation(() => {
39+
this.assertTransformed(`{{if this.attrs.foo "foo"}}`, `{{if @foo "foo"}}`);
40+
}, /Using {{this.attrs}} to reference named arguments has been deprecated. {{this.attrs.foo}} should be updated to {{@foo}}./);
41+
42+
expectDeprecation(() => {
43+
this.assertTransformed(`{{#if this.attrs.foo}}{{/if}}`, `{{#if @foo}}{{/if}}`);
44+
}, /Using {{this.attrs}} to reference named arguments has been deprecated. {{this.attrs.foo}} should be updated to {{@foo}}./);
45+
46+
expectDeprecation(() => {
47+
this.assertTransformed(
48+
`{{deeply (nested this.attrs.foo.bar)}}`,
49+
`{{deeply (nested @foo.bar)}}`
50+
);
51+
}, /Using {{this.attrs}} to reference named arguments has been deprecated. {{this.attrs.foo.bar}} should be updated to {{@foo.bar}}./);
4052
}
4153
}
4254
);

0 commit comments

Comments
 (0)