diff --git a/packages/@glimmer-workspace/integration-tests/lib/suites/components.ts b/packages/@glimmer-workspace/integration-tests/lib/suites/components.ts index 56e989173c..d4a60da81c 100644 --- a/packages/@glimmer-workspace/integration-tests/lib/suites/components.ts +++ b/packages/@glimmer-workspace/integration-tests/lib/suites/components.ts @@ -268,7 +268,7 @@ export class GlimmerishComponents extends RenderTest { }) 'invoking dynamic component (local) via angle brackets'() { this.registerComponent('Glimmer', 'Foo', 'hello world!'); - this.render(`{{#with (component 'Foo') as |Other|}}{{/with}}`); + this.render(`{{#let (component 'Foo') as |Other|}}{{/let}}`); this.assertHTML(`hello world!`); this.assertStableRerender(); @@ -280,7 +280,7 @@ export class GlimmerishComponents extends RenderTest { 'invoking dynamic component (local path) via angle brackets'() { this.registerHelper('hash', (_positional, named) => named); this.registerComponent('Glimmer', 'Foo', 'hello world!'); - this.render(`{{#with (hash Foo=(component 'Foo')) as |Other|}}{{/with}}`); + this.render(`{{#let (hash Foo=(component 'Foo')) as |Other|}}{{/let}}`); this.assertHTML(`hello world!`); this.assertStableRerender(); @@ -291,7 +291,7 @@ export class GlimmerishComponents extends RenderTest { }) 'invoking dynamic component (local) via angle brackets (ill-advised "htmlish element name" but supported)'() { this.registerComponent('Glimmer', 'Foo', 'hello world!'); - this.render(`{{#with (component 'Foo') as |div|}}
{{/with}}`); + this.render(`{{#let (component 'Foo') as |div|}}
{{/let}}`); this.assertHTML(`hello world!`); this.assertStableRerender(); @@ -302,7 +302,7 @@ export class GlimmerishComponents extends RenderTest { }) 'invoking dynamic component (local) via angle brackets supports attributes'() { this.registerComponent('Glimmer', 'Foo', '
hello world!
'); - this.render(`{{#with (component 'Foo') as |Other|}}{{/with}}`); + this.render(`{{#let (component 'Foo') as |Other|}}{{/let}}`); this.assertHTML(`
hello world!
`); this.assertStableRerender(); @@ -313,7 +313,7 @@ export class GlimmerishComponents extends RenderTest { }) 'invoking dynamic component (local) via angle brackets supports args'() { this.registerComponent('Glimmer', 'Foo', 'hello {{@name}}!'); - this.render(`{{#with (component 'Foo') as |Other|}}{{/with}}`); + this.render(`{{#let (component 'Foo') as |Other|}}{{/let}}`); this.assertHTML(`hello world!`); this.assertStableRerender(); @@ -324,7 +324,7 @@ export class GlimmerishComponents extends RenderTest { }) 'invoking dynamic component (local) via angle brackets supports passing a block'() { this.registerComponent('Glimmer', 'Foo', 'hello {{yield}}!'); - this.render(`{{#with (component 'Foo') as |Other|}}world{{/with}}`); + this.render(`{{#let (component 'Foo') as |Other|}}world{{/let}}`); this.assertHTML(`hello world!`); this.assertStableRerender(); @@ -351,7 +351,7 @@ export class GlimmerishComponents extends RenderTest { Foo ); this.render( - `{{#with (component 'Foo') as |Other|}}template{{/with}}`, + `{{#let (component 'Foo') as |Other|}}template{{/let}}`, { outer: 'outer' } ); diff --git a/packages/@glimmer-workspace/integration-tests/lib/suites/debugger.ts b/packages/@glimmer-workspace/integration-tests/lib/suites/debugger.ts index 1b3e22cb37..c8cdea29e4 100644 --- a/packages/@glimmer-workspace/integration-tests/lib/suites/debugger.ts +++ b/packages/@glimmer-workspace/integration-tests/lib/suites/debugger.ts @@ -75,7 +75,7 @@ export class DebuggerSuite extends RenderTest { }); this.render( - '{{#with this.foo as |bar|}}{{#if this.a.b}}true{{debugger}}{{else}}false{{debugger}}{{/if}}{{/with}}', + '{{#let this.foo as |bar|}}{{#if this.a.b}}true{{debugger}}{{else}}false{{debugger}}{{/if}}{{/let}}', expectedContext ); this.assert.strictEqual(callbackExecuted, 1); diff --git a/packages/@glimmer-workspace/integration-tests/lib/suites/scope.ts b/packages/@glimmer-workspace/integration-tests/lib/suites/scope.ts index 87f8764d13..72ff264e8e 100644 --- a/packages/@glimmer-workspace/integration-tests/lib/suites/scope.ts +++ b/packages/@glimmer-workspace/integration-tests/lib/suites/scope.ts @@ -9,11 +9,11 @@ export class ScopeSuite extends RenderTest { 'correct scope - conflicting local names'() { this.render({ layout: stripTight` - {{#with @a as |item|}}{{@a}}: {{item}}, - {{#with @b as |item|}} {{@b}}: {{item}}, - {{#with @c as |item|}} {{@c}}: {{item}}{{/with}} - {{/with}} - {{/with}}`, + {{#let @a as |item|}}{{@a}}: {{item}}, + {{#let @b as |item|}} {{@b}}: {{item}}, + {{#let @c as |item|}} {{@c}}: {{item}}{{/let}} + {{/let}} + {{/let}}`, args: { a: '"A"', b: '"B"', c: '"C"' }, }); @@ -25,7 +25,7 @@ export class ScopeSuite extends RenderTest { 'correct scope - conflicting block param and attr names'() { this.render({ layout: - 'Outer: {{@conflict}} {{#with @item as |conflict|}}Inner: {{@conflict}} Block: {{conflict}}{{/with}}', + 'Outer: {{@conflict}} {{#let @item as |conflict|}}Inner: {{@conflict}} Block: {{conflict}}{{/let}}', args: { item: '"from block"', conflict: '"from attr"' }, }); diff --git a/packages/@glimmer-workspace/integration-tests/test/ember-component-test.ts b/packages/@glimmer-workspace/integration-tests/test/ember-component-test.ts index 5d5b965e91..e68e47cbce 100644 --- a/packages/@glimmer-workspace/integration-tests/test/ember-component-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/ember-component-test.ts @@ -372,14 +372,14 @@ class CurlyScopeTest extends CurlyTest { stripTight`
[Outside: {{this.zomg}}] - {{#with this.zomg as |lol|}} + {{#let this.zomg as |lol|}} [Inside: {{this.zomg}}] [Inside: {{lol}}] [Block: {{this.zomg}}] [Block: {{lol}}] - {{/with}} + {{/let}}
`, { zomg: 'zomg' } ); @@ -419,14 +419,14 @@ class CurlyScopeTest extends CurlyTest { stripTight`
[Outside: {{this.zomg}}] - {{#with this.zomg as |lol|}} + {{#let this.zomg as |lol|}} [Inside: {{this.zomg}}] [Inside: {{lol}}] {{#foo-bar foo=this.zomg}} [Block: {{this.zomg}}] [Block: {{lol}}] {{/foo-bar}} - {{/with}} + {{/let}}
`, { zomg: 'zomg' } ); @@ -991,9 +991,9 @@ class CurlyClosureComponentsTest extends CurlyTest { this.render( stripTight` - {{#with (hash comp=(component 'foo-bar')) as |my|}} + {{#let (hash comp=(component 'foo-bar')) as |my|}} {{#component my.comp arg1="World!"}}Test1{{/component}} Test2 - {{/with}} + {{/let}} ` ); @@ -1007,9 +1007,9 @@ class CurlyClosureComponentsTest extends CurlyTest { this.render( stripTight` - {{#with (hash comp=(component 'foo-bar')) as |my|}} + {{#let (hash comp=(component 'foo-bar')) as |my|}} {{#component my.comp}}World!{{/component}} Test - {{/with}} + {{/let}} ` ); @@ -1023,9 +1023,9 @@ class CurlyClosureComponentsTest extends CurlyTest { this.render( stripTight` - {{#with (hash comp=(component 'foo-bar')) as |my|}} + {{#let (hash comp=(component 'foo-bar')) as |my|}} {{component my.comp arg1="World!"}} Test - {{/with}} + {{/let}} ` ); @@ -1039,9 +1039,9 @@ class CurlyClosureComponentsTest extends CurlyTest { this.render( stripTight` - {{#with (hash comp=(component 'foo-bar')) as |my|}} + {{#let (hash comp=(component 'foo-bar')) as |my|}} {{component my.comp}} World! - {{/with}} + {{/let}} ` ); @@ -1131,9 +1131,9 @@ class CurlyClosureComponentsTest extends CurlyTest { this.render( stripTight` - {{#with (hash comp=(component 'foo-bar')) as |my|}} + {{#let (hash comp=(component 'foo-bar')) as |my|}} {{my.comp}} World! - {{/with}} + {{/let}} ` ); @@ -1190,11 +1190,11 @@ class CurlyClosureComponentsTest extends CurlyTest { this.render( stripTight` - {{#with (component "foo-bar" "outer 1" "outer 2" a="outer a" b="outer b" c="outer c" e="outer e") as |outer|}} - {{#with (component outer "inner 1" a="inner a" d="inner d" e="inner e") as |inner|}} + {{#let (component "foo-bar" "outer 1" "outer 2" a="outer a" b="outer b" c="outer c" e="outer e") as |outer|}} + {{#let (component outer "inner 1" a="inner a" d="inner d" e="inner e") as |inner|}} {{#component inner "invocation 1" "invocation 2" a="invocation a" b="invocation b"}}---{{/component}} - {{/with}} - {{/with}} + {{/let}} + {{/let}} ` ); diff --git a/packages/@glimmer-workspace/integration-tests/test/helpers/array-test.ts b/packages/@glimmer-workspace/integration-tests/test/helpers/array-test.ts index c500eda4c4..47c2635018 100644 --- a/packages/@glimmer-workspace/integration-tests/test/helpers/array-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/helpers/array-test.ts @@ -6,11 +6,11 @@ class ArrayTest extends RenderTest { @test 'returns an array'() { this.render(strip` - {{#with (array "Sergio") as |people|}} + {{#let (array "Sergio") as |people|}} {{#each people as |personName|}} {{personName}} {{/each}} - {{/with}}`); + {{/let}}`); this.assertHTML('Sergio'); @@ -20,11 +20,11 @@ class ArrayTest extends RenderTest { @test 'can have more than one value'() { this.render(strip` - {{#with (array "Sergio" "Robert") as |people|}} + {{#let (array "Sergio" "Robert") as |people|}} {{#each people as |personName|}} {{personName}}, {{/each}} - {{/with}}`); + {{/let}}`); this.assertHTML('Sergio,Robert,'); @@ -34,11 +34,11 @@ class ArrayTest extends RenderTest { @test 'binds values when variables are used'() { this.render( - strip`{{#with (array this.personOne) as |people|}} + strip`{{#let (array this.personOne) as |people|}} {{#each people as |personName|}} {{personName}} {{/each}} - {{/with}}`, + {{/let}}`, { personOne: 'Tom', } @@ -58,11 +58,11 @@ class ArrayTest extends RenderTest { @test 'binds multiple values when variables are used'() { this.render( - strip`{{#with (array this.personOne this.personTwo) as |people|}} + strip`{{#let (array this.personOne this.personTwo) as |people|}} {{#each people as |personName|}} {{personName}}, {{/each}} - {{/with}}`, + {{/let}}`, { personOne: 'Tom', personTwo: 'Yehuda', diff --git a/packages/@glimmer-workspace/integration-tests/test/modifiers/dynamic-modifiers-test.ts b/packages/@glimmer-workspace/integration-tests/test/modifiers/dynamic-modifiers-test.ts index 4b5e4250f9..0af549159d 100644 --- a/packages/@glimmer-workspace/integration-tests/test/modifiers/dynamic-modifiers-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/modifiers/dynamic-modifiers-test.ts @@ -134,7 +134,7 @@ class DynamicModifiersResolutionModeTest extends RenderTest { this.registerComponent('TemplateOnly', 'Bar', '
'); }, syntaxErrorFor( - 'You attempted to invoke a path (`{{#x.foo}}`) as a modifier, but x was not in scope. Try adding `this` to the beginning of the path', + 'You attempted to invoke a path (`{{x.foo}}`) as a modifier, but x was not in scope', '{{x.foo}}', 'an unknown module', 1, diff --git a/packages/@glimmer-workspace/integration-tests/test/strict-mode-test.ts b/packages/@glimmer-workspace/integration-tests/test/strict-mode-test.ts index 8061eabfa0..00e14ee486 100644 --- a/packages/@glimmer-workspace/integration-tests/test/strict-mode-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/strict-mode-test.ts @@ -73,15 +73,22 @@ class GeneralStrictModeTest extends RenderTest { @test 'Implicit this lookup does not work'() { - const Foo = defineComponent({}, '{{bar}}', { - definition: class extends GlimmerishComponent { - bar = 'Hello, world!'; + this.assert.throws( + () => { + defineComponent({}, '{{bar}}', { + definition: class extends GlimmerishComponent { + bar = 'Hello, world!'; + }, + }); }, - }); - - this.assert.throws(() => { - this.renderComponent(Foo); - }, /Attempted to resolve a value in a strict mode template, but that value was not in scope: bar/u); + syntaxErrorFor( + 'Attempted to resolve a value in a strict mode template, but that value was not in scope: bar', + '{{bar}}', + 'an unknown module', + 1, + 0 + ) + ); } @test @@ -395,132 +402,136 @@ class StaticStrictModeTest extends RenderTest { @test 'Throws an error if value in append position is not in scope'() { - const Bar = defineComponent({}, '{{foo}}'); - - this.assert.throws(() => { - this.renderComponent(Bar); - }, /Attempted to resolve a value in a strict mode template, but that value was not in scope: foo/u); + this.assert.throws( + () => { + defineComponent({}, '{{foo}}'); + }, + syntaxErrorFor( + 'Attempted to resolve a value in a strict mode template, but that value was not in scope: foo', + '{{foo}}', + 'an unknown module', + 1, + 0 + ) + ); } @test 'Throws an error if component or helper in append position is not in scope'() { - const Bar = defineComponent({}, '{{foo "bar"}}'); - - this.assert.throws(() => { - this.renderComponent(Bar); - }, /Attempted to resolve a component or helper in a strict mode template, but that value was not in scope: foo/u); + this.assert.throws( + () => { + defineComponent({}, '{{foo "bar"}}'); + }, + syntaxErrorFor( + 'Attempted to resolve a component or helper in a strict mode template, but that value was not in scope: foo', + '{{foo "bar"}}', + 'an unknown module', + 1, + 0 + ) + ); } @test 'Throws an error if a value in argument position is not in scope'() { const Foo = defineComponent({}, '{{@foo}}'); - const Bar = defineComponent({ Foo }, ''); - - this.assert.throws(() => { - this.renderComponent(Bar); - }, /Attempted to resolve a value in a strict mode template, but that value was not in scope: bar/u); - } - - @test - 'Throws an error if helper in argument position (with args) is not in scope'() { - const Foo = defineComponent({}, '{{@foo}}'); - const Bar = defineComponent({ Foo }, ''); - - this.assert.throws(() => { - this.renderComponent(Bar); - }, /Attempted to resolve a helper in a strict mode template, but that value was not in scope: bar/u); - } - - @test - 'Throws an error if helper in subexpression position is not in scope'() { - const foo = defineSimpleHelper((value: string) => value); - const Bar = defineComponent({ foo }, '{{foo (bar)}}'); - - this.assert.throws(() => { - this.renderComponent(Bar); - }, /Attempted to resolve a helper in a strict mode template, but that value was not in scope: bar/u); - } - @test - 'Throws an error if value in append position is not in scope, and component is registered'() { - this.registerComponent('TemplateOnly', 'foo', 'Hello, world!'); - const Bar = defineComponent({}, '{{foo}}'); - - this.assert.throws(() => { - this.renderComponent(Bar); - }, /Attempted to resolve a value in a strict mode template, but that value was not in scope: foo/u); - } - - @test - 'Throws an error if value in append position is not in scope, and helper is registered'() { - this.registerHelper('foo', () => 'Hello, world!'); - const Bar = defineComponent({}, '{{foo}}'); - - this.assert.throws(() => { - this.renderComponent(Bar); - }, /Attempted to resolve a value in a strict mode template, but that value was not in scope: foo/u); + this.assert.throws( + () => { + defineComponent({ Foo }, ''); + }, + syntaxErrorFor( + 'Attempted to resolve a value in a strict mode template, but that value was not in scope: bar', + '@foo={{bar}}', + 'an unknown module', + 1, + 5 + ) + ); } @test - 'Throws an error if component or helper in append position is not in scope, and helper is registered'() { - this.registerHelper('foo', () => 'Hello, world!'); - const Bar = defineComponent({}, '{{foo "bar"}}'); - - this.assert.throws(() => { - this.renderComponent(Bar); - }, /Attempted to resolve a component or helper in a strict mode template, but that value was not in scope: foo/u); + 'Throws an error if a value in attribute position is not in scope'() { + this.assert.throws( + () => { + defineComponent({}, '
'); + }, + syntaxErrorFor( + 'Attempted to resolve a value in a strict mode template, but that value was not in scope: foo', + 'class={{foo}}', + 'an unknown module', + 1, + 5 + ) + ); } @test - 'Throws an error if a value in argument position is not in scope, and helper is registered'() { - this.registerHelper('bar', () => 'Hello, world!'); + 'Throws an error if helper in argument position (with args) is not in scope'() { const Foo = defineComponent({}, '{{@foo}}'); - const Bar = defineComponent({ Foo }, ''); - this.assert.throws(() => { - this.renderComponent(Bar); - }, /Attempted to resolve a value in a strict mode template, but that value was not in scope: bar/u); + this.assert.throws( + () => { + defineComponent({ Foo }, ''); + }, + syntaxErrorFor( + 'Attempted to resolve a helper in a strict mode template, but that value was not in scope: bar', + '@foo={{bar "aoeu"}}', + 'an unknown module', + 1, + 5 + ) + ); } @test - 'Throws an error if helper in argument position (with args) is not in scope, and helper is registered'() { - this.registerHelper('bar', () => 'Hello, world!'); - const Foo = defineComponent({}, '{{@foo}}'); - const Bar = defineComponent({ Foo }, ''); - - this.assert.throws(() => { - this.renderComponent(Bar); - }, /Attempted to resolve a helper in a strict mode template, but that value was not in scope: bar/u); + 'Throws an error if helper in attribute position (with args) is not in scope'() { + this.assert.throws( + () => { + defineComponent({}, '
'); + }, + syntaxErrorFor( + 'Attempted to resolve a helper in a strict mode template, but that value was not in scope: foo', + 'class={{foo "bar"}}', + 'an unknown module', + 1, + 5 + ) + ); } @test - 'Throws an error if helper in subexpression position is not in scope, and helper is registered'() { - this.registerHelper('bar', () => 'Hello, world!'); + 'Throws an error if helper in subexpression position is not in scope'() { const foo = defineSimpleHelper((value: string) => value); - const Bar = defineComponent({ foo }, '{{foo (bar)}}'); - this.assert.throws(() => { - this.renderComponent(Bar); - }, /Attempted to resolve a helper in a strict mode template, but that value was not in scope: bar/u); + this.assert.throws( + () => { + defineComponent({ foo }, '{{foo (bar)}}'); + }, + syntaxErrorFor( + 'Attempted to resolve a helper in a strict mode template, but that value was not in scope: bar', + '(bar)', + 'an unknown module', + 1, + 6 + ) + ); } @test 'Throws an error if modifier is not in scope'() { - const Bar = defineComponent({}, '
'); - - this.assert.throws(() => { - this.renderComponent(Bar); - }, /Attempted to resolve a modifier in a strict mode template, but it was not in scope: foo/u); - } - - @test - 'Throws an error if modifier is not in scope, and modifier is registred'() { - this.registerModifier('name', class {}); - const Bar = defineComponent({}, '
'); - - this.assert.throws(() => { - this.renderComponent(Bar); - }, /Attempted to resolve a modifier in a strict mode template, but it was not in scope: foo/u); + this.assert.throws( + () => { + defineComponent({}, '
'); + }, + syntaxErrorFor( + 'Attempted to resolve a modifier in a strict mode template, but that value was not in scope: foo', + '{{foo}}', + 'an unknown module', + 1, + 5 + ) + ); } @test diff --git a/packages/@glimmer-workspace/integration-tests/test/syntax/argument-less-helper-paren-less-invoke-test.ts b/packages/@glimmer-workspace/integration-tests/test/syntax/argument-less-helper-paren-less-invoke-test.ts index 7428ff81c2..4fca0c0a82 100644 --- a/packages/@glimmer-workspace/integration-tests/test/syntax/argument-less-helper-paren-less-invoke-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/syntax/argument-less-helper-paren-less-invoke-test.ts @@ -1,29 +1,27 @@ -import { defineSimpleHelper, jitSuite, RenderTest, test } from '../..'; +import { defineSimpleHelper, jitSuite, preprocess, RenderTest, syntaxErrorFor, test } from '../..'; class ArgumentLessHelperParenLessInvokeTest extends RenderTest { static suiteName = 'argument-less helper paren-less invoke'; @test - 'invoking an argument-less helper without parens in named argument position is deprecated'( + 'invoking an argument-less helper without parens in named argument position is a syntax error'( assert: Assert ) { - this.registerHelper('is-string', ([value]: readonly unknown[]) => typeof value === 'string'); - - this.registerHelper('foo', () => 'Hello, world!'); - this.registerComponent('TemplateOnly', 'Bar', '[{{is-string @content}}][{{@content}}]'); - - this.render('', { foo: 'Not it!' }); - this.assertHTML('[true][Hello, world!]'); - this.assertStableRerender(); - - assert.validateDeprecations( - new RegExp( - /The `foo` helper was used in the `\(unknown template module\)` template as /.source + - /`@content=\{\{foo\}\}`\. This is ambigious between wanting the `@content` argument / - .source + - /to be the `foo` helper itself, or the result of invoking the `foo` helper /.source + - /\(current behavior\)\. This implicit invocation behavior has been deprecated\./.source, - 'u' + assert.throws( + () => { + preprocess('', { + meta: { moduleName: 'test-module' }, + }); + }, + syntaxErrorFor( + 'You attempted to pass a path as argument (`@content={{foo}}`) but foo was not in scope. Try:\n'+ + '* `@content={{this.foo}}` if this is meant to be a property lookup, or\n'+ + '* `@content={{(foo)}}` if this is meant to invoke the resolved helper, or\n'+ + '* `@content={{helper "foo"}}` if this is meant to pass the resolved helper by value', + `@content={{foo}}`, + 'test-module', + 1, + 5 ) ); } diff --git a/packages/@glimmer-workspace/integration-tests/test/syntax/keyword-errors-test.ts b/packages/@glimmer-workspace/integration-tests/test/syntax/keyword-errors-test.ts index 9c8029a46b..740b2246a0 100644 --- a/packages/@glimmer-workspace/integration-tests/test/syntax/keyword-errors-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/syntax/keyword-errors-test.ts @@ -34,7 +34,7 @@ for (let keyword of KEYWORDS) { 'keyword can be yielded as a parameter in other keywords in non-strict mode'() { preprocess( ` - {{#let value as |${keyword}|}} + {{#let this.value as |${keyword}|}} {{some-helper ${keyword}}} {{/let}} `, @@ -46,7 +46,7 @@ for (let keyword of KEYWORDS) { 'keyword can be yielded as a parameter in other keywords in strict mode'() { preprocess( ` - {{#let value as |${keyword}|}} + {{#let this.value as |${keyword}|}} {{some-helper ${keyword}}} {{/let}} `, @@ -58,7 +58,7 @@ for (let keyword of KEYWORDS) { 'keyword can be yielded as a parameter in curly invocation in non-strict mode'() { preprocess( ` - {{#my-component value as |${keyword}|}} + {{#my-component this.value as |${keyword}|}} {{some-helper ${keyword}}} {{/my-component}} `, @@ -70,7 +70,7 @@ for (let keyword of KEYWORDS) { 'keyword can be yielded as a parameter in curly invocation in strict mode'() { preprocess( ` - {{#my-component value as |${keyword}|}} + {{#my-component this.value as |${keyword}|}} {{some-helper ${keyword}}} {{/my-component}} `, diff --git a/packages/@glimmer-workspace/integration-tests/test/updating-test.ts b/packages/@glimmer-workspace/integration-tests/test/updating-test.ts index 0ebfaefc67..a08ad7ae78 100644 --- a/packages/@glimmer-workspace/integration-tests/test/updating-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/updating-test.ts @@ -560,17 +560,6 @@ class UpdatingTest extends RenderTest { this.testStatefulHelper(assert, options); } - @test - 'helpers passed as arguments to {{#with}} are not torn down when switching between blocks'() { - let options = { - template: '{{#with (stateful-foo) as |unused|}}Yes{{/with}}', - truthyValue: {}, - falsyValue: null, - }; - - this.testStatefulHelper(assert, options); - } - @test 'helpers passed as arguments to {{#each}} are not torn down when switching between blocks'() { let options = { @@ -900,7 +889,7 @@ class UpdatingTest extends RenderTest { const person = { name: new Name('Godfrey', 'Chan') }; - this.render('
{{#with this.person.name.first as |f|}}{{f}}{{/with}}
', { + this.render('
{{#let this.person.name.first as |f|}}{{f}}{{/let}}
', { person, }); @@ -943,7 +932,7 @@ class UpdatingTest extends RenderTest { ----- - {{#with this.value as |foo|}} + {{#let this.value as |foo|}} foo: "{{foo}}"; bar: "{{bar}}"; value: "{{this.value}}"; @@ -953,26 +942,26 @@ class UpdatingTest extends RenderTest { ----- - {{#with foo as |bar|}} + {{#let foo as |bar|}} foo: "{{foo}}"; bar: "{{bar}}"; value: "{{this.value}}"; echo foo: "{{echo foo}}"; echo bar: "{{echo bar}}"; echo value: "{{echo this.value}}"; - {{/with}} - {{/with}} + {{/let}} + {{/let}} ----- - {{#with this.value as |bar|}} + {{#let this.value as |bar|}} foo: "{{foo}}"; bar: "{{bar}}"; value: "{{this.value}}"; echo foo: "{{echo this.foo}}"; echo bar: "{{echo bar}}"; echo value: "{{echo this.value}}"; - {{/with}} + {{/let}}
`; @@ -1108,7 +1097,7 @@ class UpdatingTest extends RenderTest { @test 'block arguments (ensure balanced push/pop)'() { let person = { name: { first: 'Godfrey', last: 'Chan' } }; - this.render('
{{#with this.person.name.first as |f|}}{{f}}{{/with}}{{this.f}}
', { + this.render('
{{#let this.person.name.first as |f|}}{{f}}{{/let}}{{this.f}}
', { person, f: 'Outer', }); @@ -1128,9 +1117,9 @@ class UpdatingTest extends RenderTest { this.render( stripTight`
- [{{#with this.person as |name|}}{{this.name}}{{/with}}] - [{{#with this.person as |name|}}{{#with this.name as |test|}}{{test}}{{/with}}{{/with}}] - [{{#with this.person as |name|}}{{#with (noop this.name) as |test|}}{{test}}{{/with}}{{/with}}] + [{{#let this.person as |name|}}{{this.name}}{{/let}}] + [{{#let this.person as |name|}}{{#let this.name as |test|}}{{test}}{{/let}}{{/let}}] + [{{#let this.person as |name|}}{{#let (noop this.name) as |test|}}{{test}}{{/let}}{{/let}}]
`, { person: 'Yehuda', name: 'Godfrey' } @@ -1148,7 +1137,7 @@ class UpdatingTest extends RenderTest { @test 'The with helper should consider an empty array truthy'() { - this.render('
{{#with this.condition as |c|}}{{c.length}}{{/with}}
', { + this.render('
{{#let this.condition as |c|}}{{c.length}}{{/let}}
', { condition: [], }); diff --git a/packages/@glimmer/compiler/lib/builder/builder.ts b/packages/@glimmer/compiler/lib/builder/builder.ts index 51efc4c07f..04be1629d6 100644 --- a/packages/@glimmer/compiler/lib/builder/builder.ts +++ b/packages/@glimmer/compiler/lib/builder/builder.ts @@ -247,8 +247,8 @@ export function buildStatement( let builtExpr: WireFormat.Expression = buildCallHead( path, trusted - ? VariableResolutionContext.AmbiguousInvoke - : VariableResolutionContext.AmbiguousAppendInvoke, + ? VariableResolutionContext.ResolveAsHelperHead + : VariableResolutionContext.ResolveAsComponentOrHelperHead, symbols ); @@ -341,8 +341,8 @@ function buildKeyword( : null; switch (name) { - case 'with': - return [Op.With, expect(params, 'with requires params')[0], block, inverse]; + case 'let': + return [Op.Let, expect(params, 'let requires params'), block]; case 'if': return [Op.If, expect(params, 'if requires params')[0], block, inverse]; case 'each': { @@ -352,6 +352,7 @@ function buildKeyword( } default: + debugger; throw new Error('unimplemented keyword'); } } @@ -637,17 +638,18 @@ export function buildVar( switch (head.kind) { case VariableKind.Free: if (context === 'Strict') { + // debugger; op = Op.GetStrictKeyword; } else if (context === 'AppendBare') { - op = Op.GetFreeAsComponentOrHelperHeadOrThisFallback; + op = Op.GetFreeAsComponentOrHelperHead; } else if (context === 'AppendInvoke') { op = Op.GetFreeAsComponentOrHelperHead; } else if (context === 'TrustedAppendBare') { - op = Op.GetFreeAsHelperHeadOrThisFallback; + op = Op.GetFreeAsHelperHead; } else if (context === 'TrustedAppendInvoke') { op = Op.GetFreeAsHelperHead; } else if (context === 'AttrValueBare') { - op = Op.GetFreeAsHelperHeadOrThisFallback; + op = Op.GetFreeAsHelperHead; } else if (context === 'AttrValueInvoke') { op = Op.GetFreeAsHelperHead; } else if (context === 'SubExpression') { @@ -691,14 +693,11 @@ function getSymbolForVar( export function expressionContextOp(context: VariableResolutionContext): GetContextualFreeOpcode { switch (context) { case VariableResolutionContext.Strict: + debugger; return Op.GetStrictKeyword; - case VariableResolutionContext.AmbiguousAppend: - return Op.GetFreeAsComponentOrHelperHeadOrThisFallback; - case VariableResolutionContext.AmbiguousAppendInvoke: - return Op.GetFreeAsComponentOrHelperHead; - case VariableResolutionContext.AmbiguousInvoke: - return Op.GetFreeAsHelperHeadOrThisFallback; - case VariableResolutionContext.ResolveAsCallHead: + case VariableResolutionContext.ResolveAsComponentOrHelperHead: + return Op.GetFreeAsComponentOrHelperHead;; + case VariableResolutionContext.ResolveAsHelperHead: return Op.GetFreeAsHelperHead; case VariableResolutionContext.ResolveAsModifierHead: return Op.GetFreeAsModifierHead; diff --git a/packages/@glimmer/compiler/lib/passes/1-normalization/index.ts b/packages/@glimmer/compiler/lib/passes/1-normalization/index.ts index 6c26704a05..65db2b3da2 100644 --- a/packages/@glimmer/compiler/lib/passes/1-normalization/index.ts +++ b/packages/@glimmer/compiler/lib/passes/1-normalization/index.ts @@ -7,6 +7,7 @@ import type { Result } from '../../shared/result'; import * as mir from '../2-encoding/mir'; import { NormalizationState } from './context'; import { VISIT_STMTS } from './visitors/statements'; +import StrictModeValidationPass from './visitors/strict-mode'; /** * Normalize the AST from @glimmer/syntax into the HIR. The HIR has special @@ -71,7 +72,13 @@ export default function normalize( } } - return body.mapOk( + let template = body.mapOk( (body) => new mir.Template({ loc: root.loc, scope: root.table, body: body.toArray() }) ); + + if (isStrict) { + template = template.andThen(template => StrictModeValidationPass.validate(template)); + } + + return template; } diff --git a/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/block.ts b/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/block.ts index fc8a7fcd1a..8f52d24f2d 100644 --- a/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/block.ts +++ b/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/block.ts @@ -281,68 +281,6 @@ export const BLOCK_KEYWORDS = keywords('Block') ); }, }) - .kw('with', { - assert(node: ASTv2.InvokeBlock): Result<{ - value: ASTv2.ExpressionNode; - }> { - let { args } = node; - - if (!args.named.isEmpty()) { - return Err( - generateSyntaxError( - `{{#with}} cannot receive named parameters, received ${args.named.entries - .map((e) => e.name.chars) - .join(', ')}`, - args.named.loc - ) - ); - } - - if (args.positional.size > 1) { - return Err( - generateSyntaxError( - `{{#with}} can only receive one positional parameter. Received ${args.positional.size} parameters`, - args.positional.loc - ) - ); - } - - let value = args.nth(0); - - if (value === null) { - return Err( - generateSyntaxError( - `{{#with}} requires a value as its first positional parameter, did not receive any parameters`, - args.loc - ) - ); - } - - return Ok({ value }); - }, - - translate( - { node, state }: { node: ASTv2.InvokeBlock; state: NormalizationState }, - { value }: { value: ASTv2.ExpressionNode } - ): Result { - let block = node.blocks.get('default'); - let inverse = node.blocks.get('else'); - - let valueResult = VISIT_EXPRS.visit(value, state); - let blockResult = VISIT_STMTS.NamedBlock(block, state); - let inverseResult = inverse ? VISIT_STMTS.NamedBlock(inverse, state) : Ok(null); - - return Result.all(valueResult, blockResult, inverseResult).mapOk( - ([value, block, inverse]) => - new mir.With({ - loc: node.loc, - value, - block, - inverse, - }) - ); - }, - }) .kw('let', { assert(node: ASTv2.InvokeBlock): Result<{ positional: ASTv2.PositionalArguments; diff --git a/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/impl.ts b/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/impl.ts index 99e8e8a9bb..ead8ab356b 100644 --- a/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/impl.ts +++ b/packages/@glimmer/compiler/lib/passes/1-normalization/keywords/impl.ts @@ -49,14 +49,6 @@ class KeywordImpl< let path = getCalleeExpression(node); if (path !== null && path.type === 'Path' && path.ref.type === 'Free') { - if (path.tail.length > 0) { - if (path.ref.resolution.serialize() === 'Loose') { - // cannot be a keyword reference, keywords do not allow paths (must be - // relying on implicit this fallback) - return false; - } - } - return path.ref.name === this.keyword; } else { return false; diff --git a/packages/@glimmer/compiler/lib/passes/1-normalization/utils/is-node.ts b/packages/@glimmer/compiler/lib/passes/1-normalization/utils/is-node.ts index a889294c44..3f8963d38c 100644 --- a/packages/@glimmer/compiler/lib/passes/1-normalization/utils/is-node.ts +++ b/packages/@glimmer/compiler/lib/passes/1-normalization/utils/is-node.ts @@ -90,8 +90,6 @@ function printPath(path: ASTv2.ExpressionNode): string { } case 'Call': return `(${printPath(path.callee)} ...)`; - case 'DeprecatedCall': - return `${path.callee.name}`; case 'Interpolate': throw unreachable('a concat statement cannot appear as the head of an expression'); } diff --git a/packages/@glimmer/compiler/lib/passes/1-normalization/visitors/expressions.ts b/packages/@glimmer/compiler/lib/passes/1-normalization/visitors/expressions.ts index 8df5ed41c5..d4f07b4c14 100644 --- a/packages/@glimmer/compiler/lib/passes/1-normalization/visitors/expressions.ts +++ b/packages/@glimmer/compiler/lib/passes/1-normalization/visitors/expressions.ts @@ -28,8 +28,6 @@ export class NormalizeExpressions { return this.CallExpression(node, state); } - case 'DeprecatedCall': - return this.DeprecaedCallExpression(node, state); } } @@ -112,13 +110,6 @@ export class NormalizeExpressions { } } - DeprecaedCallExpression( - { arg, callee, loc }: ASTv2.DeprecatedCallExpression, - _state: NormalizationState - ): Result { - return Ok(new mir.DeprecatedCallExpression({ loc, arg, callee })); - } - Args({ positional, named, loc }: ASTv2.Args, state: NormalizationState): Result { return Result.all(this.Positional(positional, state), this.NamedArguments(named, state)).mapOk( ([positional, named]) => diff --git a/packages/@glimmer/compiler/lib/passes/1-normalization/visitors/strict-mode.ts b/packages/@glimmer/compiler/lib/passes/1-normalization/visitors/strict-mode.ts new file mode 100644 index 0000000000..bfebbb5dc8 --- /dev/null +++ b/packages/@glimmer/compiler/lib/passes/1-normalization/visitors/strict-mode.ts @@ -0,0 +1,380 @@ +import type {HasSourceSpan} from '@glimmer/syntax'; +import { generateSyntaxError, loc } from '@glimmer/syntax'; +import { CurriedTypes } from '@glimmer/vm/index'; + +import type {Result} from '../../../shared/result'; +import type * as mir from '../../2-encoding/mir'; + +import { Err , Ok } from '../../../shared/result'; + +enum ResolutionType { + Value = 'value', + Component = 'component', + Helper = 'helper', + Modifier = 'modifier', + ComponentOrHelper = 'component or helper', +} + +export default class StrictModeValidationPass { + // This is done at the end of all the keyword normalizations + // At this point any free variables that isn't a valid keyword + // in its context should be considered a syntax error. We + // probably had various opportunities to do this inline in the + // earlier passes, but this aims to produce a better syntax + // error as we don't always have the right loc-context to do + // so in the other spots. + static validate(template: mir.Template): Result { + return new this(template).validate(); + } + + private constructor(private template: mir.Template) { + } + + validate(): Result { + return this.Statements(this.template.body) + .mapOk(() => this.template); + } + + Statements(statements: mir.Statement[]): Result { + let result = Ok(null); + + for (let statement of statements) { + result = result.andThen(() => this.Statement(statement)); + } + + return result; + } + + NamedBlocks({ blocks }: mir.NamedBlocks): Result { + let result = Ok(null); + + for (let block of blocks.toArray()) { + result = result.andThen(() => this.NamedBlock(block)); + } + + return result; + } + + NamedBlock(block: mir.NamedBlock): Result { + return this.Statements(block.body); + } + + Statement(statement: mir.Statement): Result { + switch (statement.type) { + case 'InElement': + return this.InElement(statement); + + case 'Debugger': + return Ok(null); + + case 'Yield': + return this.Yield(statement); + + case 'AppendTrustedHTML': + return this.AppendTrustedHTML(statement); + + case 'AppendTextNode': + return this.AppendTextNode(statement); + + case 'Component': + return this.Component(statement); + + case 'SimpleElement': + return this.SimpleElement(statement); + + case 'InvokeBlock': + return this.InvokeBlock(statement); + + case 'AppendComment': + return Ok(null); + + case 'If': + return this.If(statement); + + case 'Each': + return this.Each(statement); + + case 'Let': + return this.Let(statement); + + case 'WithDynamicVars': + return this.WithDynamicVars(statement); + + case 'InvokeComponent': + return this.InvokeComponent(statement); + } + } + + Expressions(expressions: mir.ExpressionNode[]): Result { + let result = Ok(null); + + for (let expression of expressions) { + result = result.andThen(() => this.Expression(expression)); + } + + return result; + } + + Expression(expression: mir.ExpressionNode, span: HasSourceSpan = expression, resolution?: ResolutionType): Result { + switch (expression.type) { + case 'Literal': + case 'Missing': + case 'This': + case 'Arg': + case 'Local': + case 'HasBlock': + case 'HasBlockParams': + case 'GetDynamicVar': + return Ok(null); + + case 'PathExpression': + return this.Expression(expression.head, span, resolution); + + case 'Free': + return this.errorFor(expression.name, span, resolution); + + case 'InterpolateExpression': + return this.InterpolateExpression(expression, span, resolution); + + case 'CallExpression': + return this.CallExpression(expression, span, resolution ?? ResolutionType.Helper); + + case 'Not': + return this.Expression(expression.value, span, resolution); + + case 'IfInline': + return this.IfInline(expression); + + case 'Curry': + return this.Curry(expression); + + case 'Log': + return this.Log(expression); + } + } + + Args(args: mir.Args): Result { + return this.Positional(args.positional) + .andThen(() => this.NamedArguments(args.named)); + } + + Positional(positional: mir.Positional, span?: HasSourceSpan): Result { + let result = Ok(null); + let expressions = positional.list.toArray(); + + // For cases like {{yield foo}}, when there is only a single argument, it + // makes for a slightly better error to report that entire span. However, + // when there are more than one, we need to be specific. + if (expressions.length === 1) { + result = this.Expression(expressions[0]!, span); + } else { + result = this.Expressions(expressions); + } + + return result; + } + + NamedArguments({ entries }: mir.NamedArguments): Result { + let result = Ok(null); + + for (let arg of entries.toArray()) { + result = result.andThen(() => this.NamedArgument(arg)); + } + + return result; + } + + NamedArgument(arg: mir.NamedArgument): Result { + if (arg.value.type === 'CallExpression') { + return this.Expression(arg.value, arg, ResolutionType.Helper); + } else { + return this.Expression(arg.value, arg); + } + } + + ElementParameters({ body }: mir.ElementParameters): Result { + let result = Ok(null); + + for (let param of body.toArray()) { + result = result.andThen(() => this.ElementParameter(param)); + } + + return result; + } + + ElementParameter(param: mir.ElementParameter): Result { + switch (param.type) { + case "StaticAttr": + return Ok(null); + case "DynamicAttr": + return this.DynamicAttr(param); + case "Modifier": + return this.Modifier(param); + case "SplatAttr": + return Ok(null); + } + } + + DynamicAttr(attr: mir.DynamicAttr): Result { + if (attr.value.type === 'CallExpression') { + return this.Expression(attr.value, attr, ResolutionType.Helper); + } else { + return this.Expression(attr.value, attr); + } + } + + Modifier(modifier: mir.Modifier): Result { + return this.Expression(modifier.callee, modifier, ResolutionType.Modifier) + .andThen(() => this.Args(modifier.args)); + } + + InElement(inElement: mir.InElement): Result { + return this.Expression(inElement.destination) + // Unfortunately we lost the `insertBefore=` part of the span + .andThen(() => this.Expression(inElement.insertBefore)) + .andThen(() => this.NamedBlock(inElement.block)); + } + + Yield(statement: mir.Yield): Result { + return this.Positional(statement.positional, statement); + } + + AppendTrustedHTML(statement: mir.AppendTrustedHTML): Result { + return this.Expression(statement.html, statement); + } + + AppendTextNode(statement: mir.AppendTextNode): Result { + if (statement.text.type === 'CallExpression') { + return this.Expression(statement.text, statement, ResolutionType.ComponentOrHelper); + } else { + return this.Expression(statement.text, statement); + } + } + + Component(statement: mir.Component): Result { + return this.Expression(statement.tag, statement, ResolutionType.Component) + .andThen(() => this.ElementParameters(statement.params)) + .andThen(() => this.NamedArguments(statement.args)) + .andThen(() => this.NamedBlocks(statement.blocks)); + } + + SimpleElement(statement: mir.SimpleElement): Result { + return this.ElementParameters(statement.params) + .andThen(() => this.Statements(statement.body)); + } + + InvokeBlock(statement: mir.InvokeBlock): Result { + return this.Expression(statement.head, statement.head, ResolutionType.Component) + .andThen(() => this.Args(statement.args)) + .andThen(() => this.NamedBlocks(statement.blocks)); + } + + If(statement: mir.If): Result { + return this.Expression(statement.condition, statement) + .andThen(() => this.NamedBlock(statement.block)) + .andThen(() => { + if (statement.inverse) { + return this.NamedBlock(statement.inverse); + } else { + return Ok(null); + } + }); + } + + Each(statement: mir.Each): Result { + return this.Expression(statement.value, statement) + .andThen(() => { + if (statement.key) { + return this.Expression(statement.key, statement) + } else { + return Ok(null); + } + }) + .andThen(() => this.NamedBlock(statement.block)) + .andThen(() => { + if (statement.inverse) { + return this.NamedBlock(statement.inverse); + } else { + return Ok(null); + } + }); + } + + Let(statement: mir.Let): Result { + return this.Positional(statement.positional) + .andThen(() => this.NamedBlock(statement.block)); + } + + WithDynamicVars(statement: mir.WithDynamicVars): Result { + return this.NamedArguments(statement.named) + .andThen(() => this.NamedBlock(statement.block)); + } + + InvokeComponent(statement: mir.InvokeComponent): Result { + return this.Expression(statement.definition, statement, ResolutionType.Component) + .andThen(() => this.Args(statement.args)) + .andThen(() => { + if (statement.blocks) { + return this.NamedBlocks(statement.blocks); + } else { + return Ok(null); + } + }); + } + + + InterpolateExpression(expression: mir.InterpolateExpression, span: HasSourceSpan, resolution?: ResolutionType): Result { + let expressions = expression.parts.toArray(); + + if (expressions.length === 1) { + return this.Expression(expressions[0], span, resolution); + } else { + return this.Expressions(expressions); + } + } + + CallExpression(expression: mir.CallExpression, span: HasSourceSpan, resolution?: ResolutionType): Result { + return this.Expression(expression.callee, span, resolution) + .andThen(() => this.Args(expression.args)); + } + + IfInline(expression: mir.IfInline): Result { + return this.Expression(expression.condition) + .andThen(() => this.Expression(expression.truthy)) + .andThen(() => { + if (expression.falsy) { + return this.Expression(expression.falsy); + } else { + return Ok(null); + } + }); + } + + Curry(expression: mir.Curry): Result { + let resolution: ResolutionType; + + if (expression.curriedType === CurriedTypes.Component) { + resolution = ResolutionType.Component; + } else if (expression.curriedType === CurriedTypes.Helper) { + resolution = ResolutionType.Helper; + } else { + resolution = ResolutionType.Modifier; + } + + return this.Expression(expression.definition, expression, resolution) + .andThen(() => this.Args(expression.args)); + } + + Log(expression: mir.Log): Result { + return this.Positional(expression.positional, expression); + } + + errorFor(name: string, span: HasSourceSpan, type = ResolutionType.Value): Result { + return Err( + generateSyntaxError( + `Attempted to resolve a ${type} in a strict mode template, but that value was not in scope: ${name}`, + loc(span) + ) + ); + } +} diff --git a/packages/@glimmer/compiler/lib/passes/2-encoding/content.ts b/packages/@glimmer/compiler/lib/passes/2-encoding/content.ts index e13a92a53a..38e6aaf2ad 100644 --- a/packages/@glimmer/compiler/lib/passes/2-encoding/content.ts +++ b/packages/@glimmer/compiler/lib/passes/2-encoding/content.ts @@ -76,8 +76,6 @@ export class ContentEncoder { return this.If(stmt); case 'Each': return this.Each(stmt); - case 'With': - return this.With(stmt); case 'Let': return this.Let(stmt); case 'WithDynamicVars': @@ -212,15 +210,6 @@ export class ContentEncoder { ]; } - With({ value, block, inverse }: mir.With): WireFormat.Statements.With { - return [ - SexpOpcodes.With, - EXPR.expr(value), - CONTENT.NamedBlock(block)[1], - inverse ? CONTENT.NamedBlock(inverse)[1] : null, - ]; - } - Let({ positional, block }: mir.Let): WireFormat.Statements.Let { return [SexpOpcodes.Let, EXPR.Positional(positional), CONTENT.NamedBlock(block)[1]]; } diff --git a/packages/@glimmer/compiler/lib/passes/2-encoding/expressions.ts b/packages/@glimmer/compiler/lib/passes/2-encoding/expressions.ts index 2c15921d69..9c36287707 100644 --- a/packages/@glimmer/compiler/lib/passes/2-encoding/expressions.ts +++ b/packages/@glimmer/compiler/lib/passes/2-encoding/expressions.ts @@ -16,8 +16,6 @@ export class ExpressionEncoder { return this.Literal(expr); case 'CallExpression': return this.CallExpression(expr); - case 'DeprecatedCallExpression': - return this.DeprecatedCallExpression(expr); case 'PathExpression': return this.PathExpression(expr); case 'Arg': @@ -88,10 +86,6 @@ export class ExpressionEncoder { return [isTemplateLocal ? SexpOpcodes.GetLexicalSymbol : SexpOpcodes.GetSymbol, symbol]; } - GetWithResolver({ symbol }: mir.GetWithResolver): WireFormat.Expressions.GetContextualFree { - return [SexpOpcodes.GetFreeAsComponentOrHelperHeadOrThisFallback, symbol]; - } - PathExpression({ head, tail }: mir.PathExpression): WireFormat.Expressions.GetPath { let getOp = EXPR.expr(head) as WireFormat.Expressions.GetVar; @@ -106,13 +100,6 @@ export class ExpressionEncoder { return [SexpOpcodes.Call, EXPR.expr(callee), ...EXPR.Args(args)]; } - DeprecatedCallExpression({ - arg, - callee, - }: mir.DeprecatedCallExpression): WireFormat.Expressions.GetPathFreeAsDeprecatedHelperHeadOrThisFallback { - return [SexpOpcodes.GetFreeAsDeprecatedHelperHeadOrThisFallback, callee.symbol, [arg.chars]]; - } - Tail({ members }: mir.Tail): PresentArray { return mapPresentArray(members, (member) => member.chars); } diff --git a/packages/@glimmer/compiler/lib/passes/2-encoding/mir.ts b/packages/@glimmer/compiler/lib/passes/2-encoding/mir.ts index fe0fad55bf..8eb1f49187 100644 --- a/packages/@glimmer/compiler/lib/passes/2-encoding/mir.ts +++ b/packages/@glimmer/compiler/lib/passes/2-encoding/mir.ts @@ -43,12 +43,6 @@ export class Each extends node('Each').fields<{ inverse: NamedBlock | null; }>() {} -export class With extends node('With').fields<{ - value: ExpressionNode; - block: NamedBlock; - inverse: NamedBlock | null; -}>() {} - export class Let extends node('Let').fields<{ positional: Positional; block: NamedBlock; @@ -82,7 +76,6 @@ export class NamedBlock extends node('NamedBlock').fields<{ name: SourceSlice; body: Statement[]; }>() {} -export class EndBlock extends node('EndBlock').fields() {} export class AppendTrustedHTML extends node('AppendTrustedHTML').fields<{ html: ExpressionNode; }>() {} @@ -142,10 +135,6 @@ export class CallExpression extends node('CallExpression').fields<{ callee: ExpressionNode; args: Args; }>() {} -export class DeprecatedCallExpression extends node('DeprecatedCallExpression').fields<{ - arg: SourceSlice; - callee: ASTv2.FreeVarReference; -}>() {} export class Modifier extends node('Modifier').fields<{ callee: ExpressionNode; args: Args }>() {} export class InvokeBlock extends node('InvokeBlock').fields<{ @@ -158,19 +147,6 @@ export class PathExpression extends node('PathExpression').fields<{ head: ExpressionNode; tail: Tail; }>() {} -export class GetWithResolver extends node('GetWithResolver').fields<{ - symbol: number; -}>() {} - -export class GetSymbol extends node('GetSymbol').fields<{ symbol: number }>() {} -export class GetFreeWithContext extends node('GetFreeWithContext').fields<{ - symbol: number; - context: ASTv2.FreeVarResolution; -}>() {} -/** strict mode */ -export class GetFree extends node('GetFree').fields<{ - symbol: number; -}>() {} export class Missing extends node('Missing').fields() {} export class InterpolateExpression extends node('InterpolateExpression').fields<{ @@ -209,7 +185,6 @@ export type ExpressionNode = | ASTv2.VariableReference | InterpolateExpression | CallExpression - | DeprecatedCallExpression | Not | IfInline | HasBlock @@ -242,7 +217,6 @@ export type Statement = | AppendComment | If | Each - | With | Let | WithDynamicVars | InvokeComponent; diff --git a/packages/@glimmer/compiler/lib/wire-format-debug.ts b/packages/@glimmer/compiler/lib/wire-format-debug.ts index 69aa363630..d37861f9f8 100644 --- a/packages/@glimmer/compiler/lib/wire-format-debug.ts +++ b/packages/@glimmer/compiler/lib/wire-format-debug.ts @@ -172,22 +172,9 @@ export default class WireFormatDebugger { case Op.GetStrictKeyword: return ['get-strict-free', this.upvars[opcode[1]], opcode[2]]; - case Op.GetFreeAsComponentOrHelperHeadOrThisFallback: - return [ - 'GetFreeAsComponentOrHelperHeadOrThisFallback', - this.upvars[opcode[1]], - opcode[2], - ]; - case Op.GetFreeAsComponentOrHelperHead: return ['GetFreeAsComponentOrHelperHead', this.upvars[opcode[1]], opcode[2]]; - case Op.GetFreeAsHelperHeadOrThisFallback: - return ['GetFreeAsHelperHeadOrThisFallback', this.upvars[opcode[1]], opcode[2]]; - - case Op.GetFreeAsDeprecatedHelperHeadOrThisFallback: - return ['GetFreeAsDeprecatedHelperHeadOrThisFallback', this.upvars[opcode[1]]]; - case Op.GetFreeAsHelperHead: return ['GetFreeAsHelperHead', this.upvars[opcode[1]], opcode[2]]; @@ -232,14 +219,6 @@ export default class WireFormatDebugger { opcode[4] ? this.formatBlock(opcode[4]) : null, ]; - case Op.With: - return [ - 'with', - this.formatOpcode(opcode[1]), - this.formatBlock(opcode[2]), - opcode[3] ? this.formatBlock(opcode[3]) : null, - ]; - case Op.Let: return ['let', this.formatParams(opcode[1]), this.formatBlock(opcode[2])]; diff --git a/packages/@glimmer/compiler/test/compiler-test.ts b/packages/@glimmer/compiler/test/compiler-test.ts index fe68c1ac1d..839fa80ce1 100644 --- a/packages/@glimmer/compiler/test/compiler-test.ts +++ b/packages/@glimmer/compiler/test/compiler-test.ts @@ -78,8 +78,8 @@ test('Text curlies', '
{{title}}{{title}}
', [ test( `Smoke test (blocks don't produce 'this' fallback)`, - `{{#with person as |name|}}{{#with this.name as |test|}}{{test}}{{/with}}{{/with}}`, - ['!with', ['^person'], { as: 'name' }, [['!with', ['this.name'], { as: 'test' }, ['test']]]] + `{{#let person as |name|}}{{#let this.name as |test|}}{{test}}{{/let}}{{/let}}`, + ['!let', ['^person'], { as: 'name' }, [['!let', ['this.name'], { as: 'test' }, ['test']]]] ); test( @@ -380,9 +380,9 @@ test('curlies right next to each other', `
{{a}}{{b}}{{c}}wat{{d}}
`, [ ['^a', '^b', '^c', s`wat`, '^d'], ]); -test('paths', `
{{model.foo.bar}}{{model.foo.bar}}
`, [ +test('paths', `
{{this.model.foo.bar}}{{this.model.foo.bar}}
`, [ '
', - ['^model.foo.bar', ['', ['^model.foo.bar']]], + ['this.model.foo.bar', ['', ['this.model.foo.bar']]], ]); test('whitespace', `Hello {{ foo }} `, s`Hello `, '^foo', s` `); diff --git a/packages/@glimmer/interfaces/lib/compile/encoder.ts b/packages/@glimmer/interfaces/lib/compile/encoder.ts index b4d3a06376..9e57e967dd 100644 --- a/packages/@glimmer/interfaces/lib/compile/encoder.ts +++ b/packages/@glimmer/interfaces/lib/compile/encoder.ts @@ -19,10 +19,8 @@ export type HighLevelBuilderOpcode = HighLevelLabel | HighLevelStartLabels | Hig export type HighLevelResolveModifier = 1003; export type HighLevelResolveComponent = 1004; export type HighLevelResolveHelper = 1005; -export type HighLevelResolveOptionalHelper = 1006; export type HighLevelResolveComponentOrHelper = 1007; export type HighLevelResolveOptionalComponentOrHelper = 1008; -export type HighLevelResolveFree = 1009; export type HighLevelResolveLocal = 1010; export type HighLevelResolveTemplateLocal = 1011; export type HighLevelResolveStart = HighLevelResolveModifier; @@ -32,10 +30,8 @@ export type HighLevelResolutionOpcode = | HighLevelResolveModifier | HighLevelResolveComponent | HighLevelResolveHelper - | HighLevelResolveOptionalHelper | HighLevelResolveComponentOrHelper | HighLevelResolveOptionalComponentOrHelper - | HighLevelResolveFree | HighLevelResolveLocal | HighLevelResolveTemplateLocal; @@ -85,14 +81,6 @@ export type ResolveHelperOp = [ op2: (handle: number) => void, ]; -export type ResolveOptionalHelperOp = [ - op: HighLevelResolveOptionalHelper, - op1: WireFormat.Expressions.Expression, - op2: { - ifHelper: (handle: number, name: string, moduleName: string) => void; - }, -]; - export type ResolveOptionalComponentOrHelperOp = [ op: HighLevelResolveOptionalComponentOrHelper, op1: WireFormat.Expressions.Expression, @@ -103,8 +91,6 @@ export type ResolveOptionalComponentOrHelperOp = [ }, ]; -export type ResolveFreeOp = [op: HighLevelResolveFree, op1: number, op2: (handle: number) => void]; - export type ResolveTemplateLocalOp = [ op: HighLevelResolveTemplateLocal, op1: number, @@ -122,9 +108,7 @@ export type HighLevelResolutionOp = | ResolveComponentOp | ResolveComponentOrHelperOp | ResolveHelperOp - | ResolveOptionalHelperOp | ResolveOptionalComponentOrHelperOp - | ResolveFreeOp | ResolveTemplateLocalOp | ResolveLocalOp; diff --git a/packages/@glimmer/interfaces/lib/compile/wire-format/api.d.ts b/packages/@glimmer/interfaces/lib/compile/wire-format/api.d.ts index 83048652b9..37ad28ad85 100644 --- a/packages/@glimmer/interfaces/lib/compile/wire-format/api.d.ts +++ b/packages/@glimmer/interfaces/lib/compile/wire-format/api.d.ts @@ -21,10 +21,7 @@ import type { GetDynamicVarOpcode, GetFreeAsComponentHeadOpcode, GetFreeAsComponentOrHelperHeadOpcode, - GetFreeAsComponentOrHelperHeadOrThisFallbackOpcode, - GetFreeAsDeprecatedHelperHeadOrThisFallbackOpcode, GetFreeAsHelperHeadOpcode, - GetFreeAsHelperHeadOrThisFallbackOpcode, GetFreeAsModifierHeadOpcode, GetLexicalSymbolOpcode, GetStrictKeywordOpcode, @@ -49,7 +46,6 @@ import type { TrustingDynamicAttrOpcode, UndefinedOpcode, WithDynamicVarsOpcode, - WithOpcode, YieldOpcode, } from './opcodes.js'; @@ -100,25 +96,13 @@ export namespace Expressions { export type GetSymbol = [GetSymbolOpcode, number]; export type GetLexicalSymbol = [GetLexicalSymbolOpcode, number]; export type GetStrictFree = [GetStrictKeywordOpcode, number]; - export type GetFreeAsComponentOrHelperHeadOrThisFallback = [ - GetFreeAsComponentOrHelperHeadOrThisFallbackOpcode, - number, - ]; export type GetFreeAsComponentOrHelperHead = [GetFreeAsComponentOrHelperHeadOpcode, number]; - export type GetFreeAsHelperHeadOrThisFallback = [GetFreeAsHelperHeadOrThisFallbackOpcode, number]; - export type GetFreeAsDeprecatedHelperHeadOrThisFallback = [ - GetFreeAsDeprecatedHelperHeadOrThisFallbackOpcode, - number, - ]; export type GetFreeAsHelperHead = [GetFreeAsHelperHeadOpcode, number]; export type GetFreeAsModifierHead = [GetFreeAsModifierHeadOpcode, number]; export type GetFreeAsComponentHead = [GetFreeAsComponentHeadOpcode, number]; export type GetContextualFree = - | GetFreeAsComponentOrHelperHeadOrThisFallback | GetFreeAsComponentOrHelperHead - | GetFreeAsHelperHeadOrThisFallback - | GetFreeAsDeprecatedHelperHeadOrThisFallback | GetFreeAsHelperHead | GetFreeAsModifierHead | GetFreeAsComponentHead; @@ -128,35 +112,17 @@ export namespace Expressions { export type GetPathSymbol = [GetSymbolOpcode, number, Path]; export type GetPathTemplateSymbol = [GetLexicalSymbolOpcode, number, Path]; export type GetPathStrictFree = [GetStrictKeywordOpcode, number, Path]; - export type GetPathFreeAsComponentOrHelperHeadOrThisFallback = [ - GetFreeAsComponentOrHelperHeadOrThisFallbackOpcode, - number, - Path, - ]; export type GetPathFreeAsComponentOrHelperHead = [ GetFreeAsComponentOrHelperHeadOpcode, number, Path, ]; - export type GetPathFreeAsHelperHeadOrThisFallback = [ - GetFreeAsHelperHeadOrThisFallbackOpcode, - number, - Path, - ]; - export type GetPathFreeAsDeprecatedHelperHeadOrThisFallback = [ - GetFreeAsDeprecatedHelperHeadOrThisFallbackOpcode, - number, - Path, - ]; export type GetPathFreeAsHelperHead = [GetFreeAsHelperHeadOpcode, number, Path]; export type GetPathFreeAsModifierHead = [GetFreeAsModifierHeadOpcode, number, Path]; export type GetPathFreeAsComponentHead = [GetFreeAsComponentHeadOpcode, number, Path]; export type GetPathContextualFree = - | GetPathFreeAsComponentOrHelperHeadOrThisFallback | GetPathFreeAsComponentOrHelperHead - | GetPathFreeAsHelperHeadOrThisFallback - | GetPathFreeAsDeprecatedHelperHeadOrThisFallback | GetPathFreeAsHelperHead | GetPathFreeAsModifierHead | GetPathFreeAsComponentHead; @@ -314,13 +280,6 @@ export namespace Statements { inverse: Nullable, ]; - export type With = [ - op: WithOpcode, - value: Expression, - block: SerializedInlineBlock, - inverse: Nullable, - ]; - export type Let = [op: LetOpcode, positional: Core.Params, block: SerializedInlineBlock]; export type WithDynamicVars = [ @@ -360,7 +319,6 @@ export namespace Statements { | InElement | If | Each - | With | Let | WithDynamicVars | InvokeComponent; diff --git a/packages/@glimmer/interfaces/lib/compile/wire-format/opcodes.d.ts b/packages/@glimmer/interfaces/lib/compile/wire-format/opcodes.d.ts index 91d8270019..56781bbfff 100644 --- a/packages/@glimmer/interfaces/lib/compile/wire-format/opcodes.d.ts +++ b/packages/@glimmer/interfaces/lib/compile/wire-format/opcodes.d.ts @@ -41,14 +41,8 @@ export type GetLexicalSymbolOpcode = 32; // FIXME: Why does this make it to the wire format in the first place? export type GetStrictKeywordOpcode = 31; -// `{{x}}` in append position (might be a helper or component invocation, otherwise fall back to `this`) -export type GetFreeAsComponentOrHelperHeadOrThisFallbackOpcode = 34; // a component or helper (`{{ x}}` in append position) export type GetFreeAsComponentOrHelperHeadOpcode = 35; -// a helper or `this` fallback `attr={{x}}` -export type GetFreeAsHelperHeadOrThisFallbackOpcode = 36; -// a helper or `this` fallback (deprecated) `@arg={{x}}` -export type GetFreeAsDeprecatedHelperHeadOrThisFallbackOpcode = 99; // a call head `(x)` export type GetFreeAsHelperHeadOpcode = 37; export type GetFreeAsModifierHeadOpcode = 38; @@ -58,7 +52,6 @@ export type GetFreeAsComponentHeadOpcode = 39; export type InElementOpcode = 40; export type IfOpcode = 41; export type EachOpcode = 42; -export type WithOpcode = 43; export type LetOpcode = 44; export type WithDynamicVarsOpcode = 45; export type InvokeComponentOpcode = 46; @@ -74,14 +67,10 @@ export type LogOpcode = 54; export type GetStartOpcode = GetSymbolOpcode; export type GetEndOpcode = GetFreeAsComponentHeadOpcode; -export type GetLooseFreeStartOpcode = GetFreeAsComponentOrHelperHeadOrThisFallbackOpcode; export type GetLooseFreeEndOpcode = GetFreeAsComponentHeadOpcode; -export type GetContextualFreeStartOpcode = GetFreeAsComponentOrHelperHeadOrThisFallbackOpcode; export type GetContextualFreeOpcode = - | GetFreeAsComponentOrHelperHeadOrThisFallbackOpcode | GetFreeAsComponentOrHelperHeadOpcode - | GetFreeAsHelperHeadOrThisFallbackOpcode | GetFreeAsHelperHeadOpcode | GetFreeAsModifierHeadOpcode | GetFreeAsComponentHeadOpcode diff --git a/packages/@glimmer/interfaces/lib/compile/wire-format/resolution.d.ts b/packages/@glimmer/interfaces/lib/compile/wire-format/resolution.d.ts index 97f1b81cc2..23fce44bfc 100644 --- a/packages/@glimmer/interfaces/lib/compile/wire-format/resolution.d.ts +++ b/packages/@glimmer/interfaces/lib/compile/wire-format/resolution.d.ts @@ -2,9 +2,7 @@ * A VariableResolutionContext explains how a variable name should be resolved. */ export type StrictResolution = 0; -export type AmbiguousAppendResolution = 1; -export type AmbiguousAppendInvokeResolution = 2; -export type AmbiguousInvokeResolution = 3; -export type ResolveAsCallHeadResolution = 5; +export type ResolveAsComponentOrHelperHeadResolution = 1; +export type ResolveAsHelperHeadResolution = 5; export type ResolveAsModifierHeadResolution = 6; export type ResolveAsComponentHeadResolution = 7; diff --git a/packages/@glimmer/opcode-compiler/lib/opcode-builder/encoder.ts b/packages/@glimmer/opcode-compiler/lib/opcode-builder/encoder.ts index 0a4faf9384..0a11cefbb3 100644 --- a/packages/@glimmer/opcode-compiler/lib/opcode-builder/encoder.ts +++ b/packages/@glimmer/opcode-compiler/lib/opcode-builder/encoder.ts @@ -35,7 +35,6 @@ import { resolveHelper, resolveModifier, resolveOptionalComponentOrHelper, - resolveOptionalHelper, } from './helpers/resolution'; import { HighLevelBuilderOpcodes, HighLevelResolutionOpcodes } from './opcodes'; import { HighLevelOperands } from './operands'; @@ -83,7 +82,6 @@ export function encodeOp( return encoder.startLabels(); case HighLevelBuilderOpcodes.StopLabels: return encoder.stopLabels(); - case HighLevelResolutionOpcodes.Component: return resolveComponent(resolver, constants, meta, op); case HighLevelResolutionOpcodes.Modifier: @@ -92,8 +90,6 @@ export function encodeOp( return resolveHelper(resolver, constants, meta, op); case HighLevelResolutionOpcodes.ComponentOrHelper: return resolveComponentOrHelper(resolver, constants, meta, op); - case HighLevelResolutionOpcodes.OptionalHelper: - return resolveOptionalHelper(resolver, constants, meta, op); case HighLevelResolutionOpcodes.OptionalComponentOrHelper: return resolveOptionalComponentOrHelper(resolver, constants, meta, op); @@ -113,7 +109,7 @@ export function encodeOp( let [, valueIndex, then] = op; let value = expect( meta.scopeValues, - 'BUG: Attempted to gect a template local, but template does not have any' + 'BUG: Attempted to get a template local, but template does not have any' )[valueIndex]; then(constants.value(value)); @@ -121,19 +117,6 @@ export function encodeOp( break; } - case HighLevelResolutionOpcodes.Free: - if (import.meta.env.DEV) { - let [, upvarIndex] = op; - let freeName = expect(meta.upvars, 'BUG: attempted to resolve value but no upvars found')[ - upvarIndex - ]; - - throw new Error( - `Attempted to resolve a value in a strict mode template, but that value was not in scope: ${freeName}` - ); - } - break; - default: throw new Error(`Unexpected high level opcode ${op[0]}`); } diff --git a/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/resolution.ts b/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/resolution.ts index 6b8349eded..92a6580cd9 100644 --- a/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/resolution.ts +++ b/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/resolution.ts @@ -10,7 +10,6 @@ import type { ResolveHelperOp, ResolveModifierOp, ResolveOptionalComponentOrHelperOp, - ResolveOptionalHelperOp, SexpOpcode, } from '@glimmer/interfaces'; import { assert, debugToString, expect, unwrap } from '@glimmer/util'; @@ -46,22 +45,6 @@ export const isGetFreeComponentOrHelper = makeResolutionTypeVerifier( SexpOpcodes.GetFreeAsComponentOrHelperHead ); -export const isGetFreeOptionalHelper = makeResolutionTypeVerifier( - SexpOpcodes.GetFreeAsHelperHeadOrThisFallback -); - -export function isGetFreeDeprecatedHelper( - opcode: Expressions.Expression -): opcode is Expressions.GetPathFreeAsDeprecatedHelperHeadOrThisFallback { - return ( - Array.isArray(opcode) && opcode[0] === SexpOpcodes.GetFreeAsDeprecatedHelperHeadOrThisFallback - ); -} - -export const isGetFreeOptionalComponentOrHelper = makeResolutionTypeVerifier( - SexpOpcodes.GetFreeAsComponentOrHelperHeadOrThisFallback -); - interface ResolvedContainingMetadata extends ContainingMetadata { owner: Owner; upvars: string[]; @@ -101,6 +84,8 @@ export function resolveComponent( let type = expr[0]; if (import.meta.env.DEV && expr[0] === SexpOpcodes.GetStrictKeyword) { + assert(!meta.isStrictMode, 'Strict mode errors should already be handled at compile time'); + throw new Error( `Attempted to resolve a component in a strict mode template, but that value was not in scope: ${ meta.upvars![expr[1]] ?? '{unknown variable}' @@ -127,6 +112,8 @@ export function resolveComponent( let definition = resolver.lookupComponent(name, owner)!; if (import.meta.env.DEV && (typeof definition !== 'object' || definition === null)) { + assert(!meta.isStrictMode, 'Strict mode errors should already be handled at compile time'); + throw new Error( `Attempted to resolve \`${name}\`, which was expected to be a component, but nothing was found.` ); @@ -168,6 +155,8 @@ export function resolveHelper( let helper = resolver.lookupHelper(name, owner)!; if (import.meta.env.DEV && helper === null) { + assert(!meta.isStrictMode, 'Strict mode errors should already be handled at compile time'); + throw new Error( `Attempted to resolve \`${name}\`, which was expected to be a helper, but nothing was found.` ); @@ -205,6 +194,8 @@ export function resolveModifier( let modifier = resolver.lookupBuiltInModifier(name); if (import.meta.env.DEV && modifier === null) { + assert(!meta.isStrictMode, 'Strict mode errors should already be handled at compile time'); + throw new Error( `Attempted to resolve a modifier in a strict mode template, but it was not in scope: ${name}` ); @@ -217,6 +208,8 @@ export function resolveModifier( let modifier = resolver.lookupModifier(name, owner)!; if (import.meta.env.DEV && modifier === null) { + assert(!meta.isStrictMode, 'Strict mode errors should already be handled at compile time'); + throw new Error( `Attempted to resolve \`${name}\`, which was expected to be a modifier, but nothing was found.` ); @@ -262,6 +255,8 @@ export function resolveComponentOrHelper( let helper = constants.helper(definition as object, null, true); if (import.meta.env.DEV && helper === null) { + assert(!meta.isStrictMode, 'Strict mode errors should already be handled at compile time'); + throw new Error( `Attempted to use a value as either a component or helper, but it did not have a component manager or helper manager associated with it. The value was: ${debugToString!( definition @@ -292,6 +287,8 @@ export function resolveComponentOrHelper( let helper = resolver.lookupHelper(name, owner); if (import.meta.env.DEV && helper === null) { + assert(!meta.isStrictMode, 'Strict mode errors should already be handled at compile time'); + throw new Error( `Attempted to resolve \`${name}\`, which was expected to be a component or helper, but nothing was found.` ); @@ -302,29 +299,6 @@ export function resolveComponentOrHelper( } } -/** - * - */ -export function resolveOptionalHelper( - resolver: CompileTimeResolver, - constants: CompileTimeConstants & ResolutionTimeConstants, - meta: ContainingMetadata, - [, expr, { ifHelper }]: ResolveOptionalHelperOp -): void { - assert( - isGetFreeOptionalHelper(expr) || isGetFreeDeprecatedHelper(expr), - 'Attempted to resolve a helper with incorrect opcode' - ); - let { upvars, owner } = assertResolverInvariants(meta); - - let name = unwrap(upvars[expr[1]]); - let helper = resolver.lookupHelper(name, owner); - - if (helper) { - ifHelper(constants.helper(helper, name), name, meta.moduleName); - } -} - /** * {{maybeHelperOrComponent}} */ @@ -335,7 +309,7 @@ export function resolveOptionalComponentOrHelper( [, expr, { ifComponent, ifHelper, ifValue }]: ResolveOptionalComponentOrHelperOp ): void { assert( - isGetFreeOptionalComponentOrHelper(expr), + isGetFreeComponentOrHelper(expr), 'Attempted to resolve an optional component or helper with incorrect opcode' ); @@ -411,6 +385,8 @@ function lookupBuiltInHelper( let helper = resolver.lookupBuiltInHelper(name); if (import.meta.env.DEV && helper === null) { + assert(!meta.isStrictMode, 'Strict mode errors should already be handled at compile time'); + // Keyword helper did not exist, which means that we're attempting to use a // value of some kind that is not in scope throw new Error( diff --git a/packages/@glimmer/opcode-compiler/lib/opcode-builder/opcodes.ts b/packages/@glimmer/opcode-compiler/lib/opcode-builder/opcodes.ts index 0b7fb7d2ad..0303482c90 100644 --- a/packages/@glimmer/opcode-compiler/lib/opcode-builder/opcodes.ts +++ b/packages/@glimmer/opcode-compiler/lib/opcode-builder/opcodes.ts @@ -3,12 +3,10 @@ import type { HighLevelLabel, HighLevelResolveComponent, HighLevelResolveComponentOrHelper, - HighLevelResolveFree, HighLevelResolveHelper, HighLevelResolveLocal, HighLevelResolveModifier, HighLevelResolveOptionalComponentOrHelper, - HighLevelResolveOptionalHelper, HighLevelResolveTemplateLocal, HighLevelStart, HighLevelStartLabels, @@ -19,10 +17,8 @@ export const HighLevelResolutionOpcodes = { Modifier: 1003 satisfies HighLevelResolveModifier, Component: 1004 satisfies HighLevelResolveComponent, Helper: 1005 satisfies HighLevelResolveHelper, - OptionalHelper: 1006 satisfies HighLevelResolveOptionalHelper, ComponentOrHelper: 1007 satisfies HighLevelResolveComponentOrHelper, OptionalComponentOrHelper: 1008 satisfies HighLevelResolveOptionalComponentOrHelper, - Free: 1009 satisfies HighLevelResolveFree, Local: 1010 satisfies HighLevelResolveLocal, TemplateLocal: 1011 satisfies HighLevelResolveTemplateLocal, } as const; diff --git a/packages/@glimmer/opcode-compiler/lib/syntax/expressions.ts b/packages/@glimmer/opcode-compiler/lib/syntax/expressions.ts index a39b159f8d..1cf3e675af 100644 --- a/packages/@glimmer/opcode-compiler/lib/syntax/expressions.ts +++ b/packages/@glimmer/opcode-compiler/lib/syntax/expressions.ts @@ -1,5 +1,4 @@ import type { ExpressionSexpOpcode } from '@glimmer/interfaces'; -import { assert, deprecate } from '@glimmer/global-context'; import { $v0, MachineOp, Op } from '@glimmer/vm'; import { SexpOpcodes } from '@glimmer/wire-format'; @@ -49,62 +48,18 @@ EXPRESSIONS.add(SexpOpcodes.GetLexicalSymbol, (op, [, sym, path]) => { }); }); -EXPRESSIONS.add(SexpOpcodes.GetStrictKeyword, (op, [, sym, _path]) => { - op(HighLevelResolutionOpcodes.Free, sym, (_handle: unknown) => { - // TODO: Implement in strict mode - }); -}); - -EXPRESSIONS.add(SexpOpcodes.GetFreeAsComponentOrHelperHeadOrThisFallback, () => { - // TODO: The logic for this opcode currently exists in STATEMENTS.Append, since - // we want different wrapping logic depending on if we are invoking a component, - // helper, or {{this}} fallback. Eventually we fix the opcodes so that we can - // traverse the subexpression tree like normal in this location. - throw new Error('unimplemented opcode'); -}); - -EXPRESSIONS.add(SexpOpcodes.GetFreeAsHelperHeadOrThisFallback, (op, expr) => { - //
- +EXPRESSIONS.add(SexpOpcodes.GetStrictKeyword, (op, expr) => { op(HighLevelResolutionOpcodes.Local, expr[1], (_name: string) => { - op(HighLevelResolutionOpcodes.OptionalHelper, expr, { - ifHelper: (handle: number) => { - Call(op, handle, null, null); - }, + op(HighLevelResolutionOpcodes.Helper, expr, (handle: number) => { + Call(op, handle, null, null); }); }); }); -EXPRESSIONS.add(SexpOpcodes.GetFreeAsDeprecatedHelperHeadOrThisFallback, (op, expr) => { - // - +EXPRESSIONS.add(SexpOpcodes.GetFreeAsHelperHead, (op, expr) => { op(HighLevelResolutionOpcodes.Local, expr[1], (_name: string) => { - op(HighLevelResolutionOpcodes.OptionalHelper, expr, { - ifHelper: (handle: number, name: string, moduleName: string) => { - assert(expr[2] && expr[2].length === 1, '[BUG] Missing argument name'); - - let arg = expr[2][0]; - - deprecate( - `The \`${name}\` helper was used in the \`${moduleName}\` template as \`${arg}={{${name}}}\`. ` + - `This is ambigious between wanting the \`${arg}\` argument to be the \`${name}\` helper itself, ` + - `or the result of invoking the \`${name}\` helper (current behavior). ` + - `This implicit invocation behavior has been deprecated.\n\n` + - `Instead, please explicitly invoke the helper with parenthesis, i.e. \`${arg}={{(${name})}}\`.\n\n` + - `Note: the parenthesis are only required in this exact scenario where an ambiguity is present – where ` + - `\`${name}\` referes to a global helper (as opposed to a local variable), AND ` + - `the \`${name}\` helper invocation does not take any arguments, AND ` + - `this occurs in a named argument position of a component invocation.\n\n` + - `We expect this combination to be quite rare, as most helpers require at least one argument. ` + - `There is no need to refactor helper invocations in cases where this deprecation was not triggered.`, - false, - { - id: 'argument-less-helper-paren-less-invocation', - } - ); - - Call(op, handle, null, null); - }, + op(HighLevelResolutionOpcodes.Helper, expr, (handle: number) => { + Call(op, handle, null, null); }); }); }); diff --git a/packages/@glimmer/opcode-compiler/lib/syntax/statements.ts b/packages/@glimmer/opcode-compiler/lib/syntax/statements.ts index 4dfe248bbf..7228aa61fb 100644 --- a/packages/@glimmer/opcode-compiler/lib/syntax/statements.ts +++ b/packages/@glimmer/opcode-compiler/lib/syntax/statements.ts @@ -26,7 +26,6 @@ import { isGetFreeComponent, isGetFreeComponentOrHelper, isGetFreeModifier, - isGetFreeOptionalComponentOrHelper, } from '../opcode-builder/helpers/resolution'; import { CompilePositional, SimpleArgs } from '../opcode-builder/helpers/shared'; import { @@ -140,7 +139,7 @@ STATEMENTS.add(SexpOpcodes.Append, (op, [, value]) => { // Special case for static values if (!Array.isArray(value)) { op(Op.Text, value === null || value === undefined ? '' : String(value)); - } else if (isGetFreeOptionalComponentOrHelper(value)) { + } else if (isGetFreeComponentOrHelper(value)) { op(HighLevelResolutionOpcodes.OptionalComponentOrHelper, value, { ifComponent(component: CompileTimeComponent) { InvokeComponent(op, component, null, null, null, null); @@ -322,30 +321,6 @@ STATEMENTS.add(SexpOpcodes.Each, (op, [, value, key, block, inverse]) => ) ); -STATEMENTS.add(SexpOpcodes.With, (op, [, value, block, inverse]) => { - ReplayableIf( - op, - - () => { - expr(op, value); - op(Op.Dup, $sp, 0); - op(Op.ToBoolean); - - return 2; - }, - - () => { - InvokeStaticBlockWithStack(op, block, 1); - }, - - () => { - if (inverse) { - InvokeStaticBlock(op, inverse); - } - } - ); -}); - STATEMENTS.add(SexpOpcodes.Let, (op, [, positional, block]) => { let count = CompilePositional(op, positional); InvokeStaticBlockWithStack(op, block, count); diff --git a/packages/@glimmer/syntax/lib/keywords.ts b/packages/@glimmer/syntax/lib/keywords.ts index 56b8363593..dd8cdd5717 100644 --- a/packages/@glimmer/syntax/lib/keywords.ts +++ b/packages/@glimmer/syntax/lib/keywords.ts @@ -1,7 +1,20 @@ +export type Keywords = keyof typeof KEYWORDS_TYPES; export type KeywordType = 'Call' | 'Modifier' | 'Append' | 'Block'; -export function isKeyword(word: string): boolean { - return word in KEYWORDS_TYPES; +export function isKeyword(word: string): word is Keywords; +export function isKeyword(word: string, type: KeywordType): boolean; +export function isKeyword(word: string, type?: KeywordType): boolean { + if (word in KEYWORDS_TYPES) { + if (type === undefined) { + return true; + } else { + let types = KEYWORDS_TYPES[word as Keywords]; + // This seems like a TypeScript bug – it inferred types as never[]? + return types.includes(type as never); + } + } else { + return false; + } } /** @@ -9,6 +22,7 @@ export function isKeyword(word: string): boolean { * language, and where their valid usages are. */ export const KEYWORDS_TYPES = { + action: ['Call', 'Modifier'], component: ['Call', 'Append', 'Block'], debugger: ['Append'], 'each-in': ['Block'], @@ -19,13 +33,11 @@ export const KEYWORDS_TYPES = { if: ['Call', 'Append', 'Block'], 'in-element': ['Block'], let: ['Block'], - 'link-to': ['Append', 'Block'], log: ['Call', 'Append'], - modifier: ['Call'], + modifier: ['Call', 'Modifier'], mount: ['Append'], mut: ['Call', 'Append'], outlet: ['Append'], - 'query-params': ['Call'], readonly: ['Call', 'Append'], unbound: ['Call', 'Append'], unless: ['Call', 'Append', 'Block'], diff --git a/packages/@glimmer/syntax/lib/v2/README.md b/packages/@glimmer/syntax/lib/v2/README.md index 68f134cac3..acb6f2fc37 100644 --- a/packages/@glimmer/syntax/lib/v2/README.md +++ b/packages/@glimmer/syntax/lib/v2/README.md @@ -69,7 +69,7 @@ In `ASTv2`, every variable name is represented as a `VariableReference`. **Important Note**: The remainder of this README is a description of the loose mode rules for free variable resolution. Strict mode free variable references always refer to an in-scope JavaScript binding, regardless of their syntactic position. -RFC [#496][#496] (Handlebars Strict Mode) rationalized the rules for loose mode. This README describes the semantics of [#496][#496] in terms of namespaced free variable references and fallback semantics. +RFC [#496][#496] (Handlebars Strict Mode) rationalized the rules for loose mode. This README describes the semantics of [#496][#496] in terms of namespaced free variable references. [#496]: https://emberjs.github.io/rfcs/0496-handlebars-strict-mode.html @@ -77,10 +77,7 @@ RFC [#496][#496] (Handlebars Strict Mode) rationalized the rules for loose mode. There are two significant differences between strict and loose mode that affect the AST. -In loose mode: - -1. Certain free variable references fall back to a property lookup on `this`. -2. Free variable references in "call" positions are resolved using a contextual namespace. For example, the free variable reference `h` in `(h 123)` is resolved as `helper:h`. The free variable reference `h` in `

` is resolved as `modifier:h`. +In loose mode, free variable references in "call" positions are resolved using a contextual namespace. For example, the free variable reference `h` in `(h 123)` is resolved as `helper:h`. The free variable reference `h` in `

` is resolved as `modifier:h`. In strict mode, all free variable references refer to bindings provided by a JavaScript scope that the template will be embedded within. @@ -94,26 +91,6 @@ The `Strict` resolution applies to all free variables encountered while parsing None. Strict mode templates must be embedded in a JavaScript context where all free variable references are in scope. A compile-time error should be produced if free there are variable references that do not correspond to any in-scope variables. -### Fallback Semantics - -When a free variable resolution is said to have "fallback semantics", it means the following algorithm: - -1. Attempt to resolve the name in the namespaces for the resolution, if any. -2. If the name could not be resolved, resolve it as a property on `this`. - -> Note: A free variable resolution has fallback semantics if it's an append or attribute curly without arguments, or if it's a path in argument position. See the summary table below for full details. - -### Eval Mode Semantics - -When a free variable resolution has fallback semantics, it is also said to have "eval mode semantics", which means: - -1. If the template is evaluated in eval mode (i.e. as a partial), dynamically resolve the free variable in the context of the template that invoked the partial (the "invoker"): -1. If the variable name is in the local scope of the invoker, resolve it as a _local_ variable in the invoker's local scope -1. Otherwise: -1. if the invoker is also in eval mode, repeat the process with the invoker's invoker -1. if the invoker is not in eval mode, resolve the free variable using fallback semantics in the invoker's scope -1. Otherwise, resolve the free variable using fallback semantics in the current scope - ### Namespaced Variable Resolution | | | @@ -123,7 +100,6 @@ When a free variable resolution has fallback semantics, it is also said to have | Arguments? | Any | | | | | Namespace | see table below | -| Fallback semantics? | ⛔ | These resolutions occur in syntaxes that are definitely calls (e.g. subexpressions, blocks, modifiers, etc.). @@ -140,16 +116,15 @@ These resolutions occur in syntaxes that are definitely calls (e.g. subexpressio If the variable reference cannot be resolved in its namespace. -### Namespaced Resolution: Ambiguous Component or Helper +### Namespaced Resolution: Append | | | | ------------------- | ----------------------- | | Syntax Positions | append | | Path has dots? | ❌ | -| Arguments? | ➕ | +| Arguments? | Any | | | | | Namespace | `helper` or `component` | -| Fallback semantics? | ⛔ | This resolution occurs in append nodes with at least one argument, and when the path does not have dots (e.g. `{{hello world}}`). @@ -158,6 +133,7 @@ This resolution occurs in append nodes with at least one argument, and when the | situation | variable | namespace | | ------------------- | -------- | ------------------- | | `{{x y}}` as append | `x` | `ComponentOrHelper` | +| `{{x}}` as append | `x` | `ComponentOrHelper` | In this situation, the `x` may refer to: @@ -168,37 +144,7 @@ In this situation, the `x` may refer to: If the variable reference cannot be resolved in the `helper` or `component` namespaces. -### Ambiguous Resolution: Append Ambiguity - -| | | -| ------------------- | --------------------- | -| Syntax Positions | append | -| Path has dots? | ❌ | -| Arguments? | ❌ | -| | | -| Namespace | `helper`, `component` | -| Fallback semantics? | ✅ | - -This resolution occurs in append nodes with zero arguments, and when the path does not have dots (e.g. `{{hello}}`). - -#### Applicable Situations - -| situation | variable | ambiguity | -| ----------------- | -------- | --------- | -| `{{x}}` as append | `x` | `Append` | - -In this situation, the `x` may refer to: - -- a helper `x` -- a component `x` -- a local variable in partial scope -- `this.x`. - -#### Runtime Error Cases - -None. - -### Ambiguous Resolution: Attribute Ambiguity +### Namespaced Resolution: Attribute This resolution context occurs in attribute nodes with zero arguments, and when the path does not have dots. @@ -206,26 +152,24 @@ This resolution context occurs in attribute nodes with zero arguments, and when | ------------------- | ------------------------ | | Syntax Positions | attribute, interpolation | | Path has dots? | ❌ | -| Arguments? | ❌ | +| Arguments? | Any | | | | | Namespace | `helper` | -| Fallback semantics? | ✅ | #### Applicable Situations -| situation | variable | ambiguity | -| --------------------------------------------- | -------- | --------- | -| `

`
`` | `x` | `Attr` | +| situation | variable | namespace | +| ------------------------------------------------- | -------- | --------- | +| `

`
`
` | `x` | `Helper` | +| `

`
`
` | `x` | `Helper` | In this situation, the `x` may refer to: - a helper `x` -- a local variable in partial scope -- `this.x`. #### Runtime Error Cases -None. +If the variable reference cannot be resolved in the `helper` namespaces. ### Summary @@ -253,7 +197,6 @@ Situations that meet all three of these criteria are syntax errors: | ------------------- | --- | | Path has dots? | ❌ | | Arguments? | Any | -| Fallback semantics? | ⛔ | | Syntax Position | Example | | Namespace | | --------------- | ------------- | --- | ----------- | @@ -264,11 +207,11 @@ Situations that meet all three of these criteria are syntax errors: #### Append -| Syntax Position | Example | Dots? | Args? | | Namespace | Fallback? | -| --------------- | --------- | ----- | ----- | --- | --------------------- | --------- | -| `Append` | `{{x}}` | ❌ | ❌ | | `helper`, `component` | ✅ | -| `Append` | `{{x.y}}` | ➕ | ❌ | | None | ✅ | -| `Append` | `{{x y}}` | ❌ | ➕ | | `helper`, `component` | ⛔ | +| Syntax Position | Example | Dots? | Args? | | Namespace | +| --------------- | --------- | ----- | ----- | --- | --------------------- | +| `Append` | `{{x}}` | ❌ | ❌ | | `helper`, `component` | +| `Append` | `{{x.y}}` | ➕ | ❌ | | None | +| `Append` | `{{x y}}` | ❌ | ➕ | | `helper`, `component` | #### Attributes @@ -278,8 +221,8 @@ The `Attribute` syntax position includes: - the value of an element argument (`@title={{...}}`) - a part of an interpolation (`href="{{...}}.html"`) -| Syntax Position | Example | Dots? | Arguments? | | Namespace | Fallback? | -| --------------- | -------------- | ----- | ---------- | --- | --------- | --------- | -| `Attribute` | `href={{x}}` | ❌ | ❌ | | `helper` | ✅ | -| `Attribute` | `href={{x.y}}` | ➕ | ❌ | | None | ✅ | -| `Attribute` | `href={{x y}}` | ❌ | ➕ | | `helper` | ⛔ | +| Syntax Position | Example | Dots? | Arguments? | | Namespace | +| --------------- | -------------- | ----- | ---------- | --- | --------- | +| `Attribute` | `href={{x}}` | ❌ | ❌ | | `helper` | +| `Attribute` | `href={{x.y}}` | ➕ | ❌ | | None | +| `Attribute` | `href={{x y}}` | ❌ | ➕ | | `helper` | diff --git a/packages/@glimmer/syntax/lib/v2/builders.ts b/packages/@glimmer/syntax/lib/v2/builders.ts index 007a77ac70..3f109ac995 100644 --- a/packages/@glimmer/syntax/lib/v2/builders.ts +++ b/packages/@glimmer/syntax/lib/v2/builders.ts @@ -221,18 +221,6 @@ export class Builder { }); } - deprecatedCall( - arg: SourceSlice, - callee: ASTv2.FreeVarReference, - loc: SourceSpan - ): ASTv2.DeprecatedCallExpression { - return new ASTv2.DeprecatedCallExpression({ - loc, - arg, - callee, - }); - } - interpolate(parts: ASTv2.ExpressionNode[], loc: SourceSpan): ASTv2.InterpolateExpression { assertPresentArray(parts); diff --git a/packages/@glimmer/syntax/lib/v2/loose-resolution.ts b/packages/@glimmer/syntax/lib/v2/loose-resolution.ts index b7cb1dd888..e678059fda 100644 --- a/packages/@glimmer/syntax/lib/v2/loose-resolution.ts +++ b/packages/@glimmer/syntax/lib/v2/loose-resolution.ts @@ -34,50 +34,43 @@ export function BlockSyntaxContext(node: ASTv1.BlockStatement): ASTv2.FreeVarRes if (isSimpleCallee(node)) { return ASTv2.LooseModeResolution.namespaced(ASTv2.COMPONENT_NAMESPACE); } else { - return ASTv2.LooseModeResolution.fallback(); + return null; } } export function ComponentSyntaxContext(node: ASTv1.PathExpression): ASTv2.FreeVarResolution | null { if (isSimplePath(node)) { - return ASTv2.LooseModeResolution.namespaced(ASTv2.FreeVarNamespace.Component, true); + return ASTv2.LooseModeResolution.namespaced(ASTv2.COMPONENT_NAMESPACE, true); } else { return null; } } /** - * This corresponds to append positions (text curlies or attribute - * curlies). In strict mode, this also corresponds to arg curlies. + * This corresponds to attribute curlies (). + * In strict mode, this also corresponds to arg curlies. */ -export function AttrValueSyntaxContext(node: ASTv1.MustacheStatement): ASTv2.FreeVarResolution { - let isSimple = isSimpleCallee(node); - let isInvoke = isInvokeNode(node); - - if (isSimple) { - return isInvoke - ? ASTv2.LooseModeResolution.namespaced(ASTv2.FreeVarNamespace.Helper) - : ASTv2.LooseModeResolution.attr(); +export function AttrValueSyntaxContext(node: ASTv1.MustacheStatement): ASTv2.FreeVarResolution | null { + if (isSimpleCallee(node)) { + return ASTv2.LooseModeResolution.namespaced(ASTv2.HELPER_NAMESPACE); } else { - return isInvoke ? ASTv2.STRICT_RESOLUTION : ASTv2.LooseModeResolution.fallback(); + return null; } } /** - * This corresponds to append positions (text curlies or attribute - * curlies). In strict mode, this also corresponds to arg curlies. + * This corresponds to append positions text curlies. */ -export function AppendSyntaxContext(node: ASTv1.MustacheStatement): ASTv2.FreeVarResolution { +export function AppendSyntaxContext(node: ASTv1.MustacheStatement): ASTv2.FreeVarResolution | null { let isSimple = isSimpleCallee(node); - let isInvoke = isInvokeNode(node); let trusting = node.trusting; if (isSimple) { return trusting - ? ASTv2.LooseModeResolution.trustingAppend({ invoke: isInvoke }) - : ASTv2.LooseModeResolution.append({ invoke: isInvoke }); + ? ASTv2.LooseModeResolution.trustingAppend() + : ASTv2.LooseModeResolution.append(); } else { - return ASTv2.LooseModeResolution.fallback(); + return null; } } @@ -113,9 +106,7 @@ export type Resolution

= ( * ``` */ function isSimpleCallee(node: AstCallParts): boolean { - let path = node.path; - - return isSimplePath(path); + return isSimplePath(node.path); } type SimplePath = ASTv1.PathExpression & { head: ASTv1.VarHead }; @@ -127,10 +118,3 @@ function isSimplePath(node: ASTv1.Expression): node is SimplePath { return false; } } - -/** - * The call expression has at least one argument. - */ -function isInvokeNode(node: AstCallParts): boolean { - return node.params.length > 0 || node.hash.pairs.length > 0; -} diff --git a/packages/@glimmer/syntax/lib/v2/normalize.ts b/packages/@glimmer/syntax/lib/v2/normalize.ts index aaf120fac6..ebb814af55 100644 --- a/packages/@glimmer/syntax/lib/v2/normalize.ts +++ b/packages/@glimmer/syntax/lib/v2/normalize.ts @@ -240,13 +240,13 @@ class ExpressionNormalizer { let { path, params, hash } = parts; let callee = this.normalize(path, context); - let paramList = params.map((p) => this.normalize(p, ASTv2.ARGUMENT_RESOLUTION)); + let paramList = params.map((p) => this.normalize(p, ASTv2.STRICT_RESOLUTION)); let paramLoc = SpanList.range(paramList, callee.loc.collapse('end')); let namedLoc = this.block.loc(hash.loc); let argsLoc = SpanList.range([paramLoc, namedLoc]); let positional = this.block.builder.positional( - params.map((p) => this.normalize(p, ASTv2.ARGUMENT_RESOLUTION)), + params.map((p) => this.normalize(p, ASTv2.STRICT_RESOLUTION)), paramLoc ); @@ -268,7 +268,7 @@ class ExpressionNormalizer { return this.block.builder.namedArgument( new SourceSlice({ chars: pair.key, loc: keyOffsets }), - this.normalize(pair.value, ASTv2.ARGUMENT_RESOLUTION) + this.normalize(pair.value, ASTv2.STRICT_RESOLUTION) ); } @@ -375,6 +375,15 @@ class StatementNormalizer { let { escaped } = mustache; let loc = this.block.loc(mustache.loc); + let resolution = this.block.resolutionFor(mustache, AppendSyntaxContext); + + if (resolution.result === 'error') { + throw generateSyntaxError( + `You attempted to render a path (\`{{${resolution.path}}}\`), but ${resolution.head} was not in scope`, + loc + ); + } + // Normalize the call parts in AppendSyntaxContext let callParts = this.expr.callParts( { @@ -382,7 +391,7 @@ class StatementNormalizer { params: mustache.params, hash: mustache.hash, }, - AppendSyntaxContext(mustache) + resolution.result ); let value = callParts.args.isEmpty() @@ -519,7 +528,7 @@ class ElementNormalizer { if (resolution.result === 'error') { throw generateSyntaxError( - `You attempted to invoke a path (\`{{#${resolution.path}}}\`) as a modifier, but ${resolution.head} was not in scope. Try adding \`this\` to the beginning of the path`, + `You attempted to invoke a path (\`{{${resolution.path}}}\`) as a modifier, but ${resolution.head} was not in scope`, m.loc ); } @@ -539,8 +548,17 @@ class ElementNormalizer { */ private mustacheAttr(mustache: ASTv1.MustacheStatement): ASTv2.ExpressionNode { // Normalize the call parts in AttrValueSyntaxContext + let resolution = this.ctx.resolutionFor(mustache, AttrValueSyntaxContext); + + if (resolution.result === 'error') { + throw generateSyntaxError( + `You attempted to render a path (\`{{${resolution.path}}}\`), but ${resolution.head} was not in scope`, + mustache.loc + ); + } + let sexp = this.ctx.builder.sexp( - this.expr.callParts(mustache, AttrValueSyntaxContext(mustache)), + this.expr.callParts(mustache, resolution.result), this.ctx.loc(mustache.loc) ); @@ -597,76 +615,65 @@ class ElementNormalizer { let offsets = this.ctx.loc(m.loc); let nameSlice = offsets.sliceStartChars({ chars: m.name.length }).toSlice(m.name); - let value = this.attrValue(m.value); + return this.ctx.builder.attr( { name: nameSlice, value: value.expr, trusting: value.trusting }, offsets ); } - private maybeDeprecatedCall( - arg: SourceSlice, - part: ASTv1.MustacheStatement | ASTv1.TextNode | ASTv1.ConcatStatement - ): { expr: ASTv2.DeprecatedCallExpression; trusting: boolean } | null { - if (this.ctx.strict) { - return null; - } + // An arg curly is the same as an attribute curly for + // our purposes, except that in loose mode is an error: + private checkArgCall( + arg: ASTv1.AttrNode + ): void { + let { value } = arg; - if (part.type !== 'MustacheStatement') { - return null; + if (value.type !== 'MustacheStatement') { + return; } - let { path } = part; - - if (path.type !== 'PathExpression') { - return null; + if (value.params.length !== 0 || value.hash.pairs.length !== 0) { + return; } - if (path.head.type !== 'VarHead') { - return null; - } - - let { name } = path.head; - - if (name === 'has-block' || name === 'has-block-params') { - return null; - } + let { path } = value; - if (this.ctx.hasBinding(name)) { - return null; + if (path.type !== 'PathExpression') { + return; } - if (path.tail.length !== 0) { - return null; + if (path.tail.length > 0) { + return; } - if (part.params.length !== 0 || part.hash.pairs.length !== 0) { + let resolution = this.ctx.resolutionFor(path, () => { + // We deliberately don't want this to resolve anything. The purpose of + // calling `resolutionFor` here is to check for strict mode, in-scope + // local variables, etc. return null; - } - - let context = ASTv2.LooseModeResolution.attr(); - - let callee = this.ctx.builder.freeVar({ - name, - context, - symbol: this.ctx.table.allocateFree(name, context), - loc: path.loc, }); - return { - expr: this.ctx.builder.deprecatedCall(arg, callee, part.loc), - trusting: false, - }; + if (resolution.result === 'error' && resolution.path !== 'has-block') { + throw generateSyntaxError( + `You attempted to pass a path as argument (\`${arg.name}={{${resolution.path}}}\`) but ${resolution.head} was not in scope. Try:\n` + + `* \`${arg.name}={{this.${resolution.path}}}\` if this is meant to be a property lookup, or\n` + + `* \`${arg.name}={{(${resolution.path})}}\` if this is meant to invoke the resolved helper, or\n` + + `* \`${arg.name}={{helper "${resolution.path}"}}\` if this is meant to pass the resolved helper by value`, + arg.loc + ); + } } private arg(arg: ASTv1.AttrNode): ASTv2.ComponentArg { assert(arg.name[0] === '@', 'An arg name must start with `@`'); + this.checkArgCall(arg); let offsets = this.ctx.loc(arg.loc); let nameSlice = offsets.sliceStartChars({ chars: arg.name.length }).toSlice(arg.name); + let value = this.attrValue(arg.value); - let value = this.maybeDeprecatedCall(nameSlice, arg.value) || this.attrValue(arg.value); return this.ctx.builder.arg( { name: nameSlice, value: value.expr, trusting: value.trusting }, offsets @@ -867,7 +874,7 @@ class ElementChildren extends Children { assertElement(name: SourceSlice, hasBlockParams: boolean): ASTv2.SimpleElement { if (hasBlockParams) { throw generateSyntaxError( - `Unexpected block params in <${name}>: simple elements cannot have block params`, + `Unexpected block params in <${name.chars}>: simple elements cannot have block params`, this.loc ); } diff --git a/packages/@glimmer/syntax/lib/v2/objects/expr.ts b/packages/@glimmer/syntax/lib/v2/objects/expr.ts index 92b9b9d8ec..70f91d06c6 100644 --- a/packages/@glimmer/syntax/lib/v2/objects/expr.ts +++ b/packages/@glimmer/syntax/lib/v2/objects/expr.ts @@ -1,7 +1,7 @@ import type { PresentArray } from '@glimmer/interfaces'; import type { CallFields } from './base'; -import type { FreeVarReference, VariableReference } from './refs'; +import type { VariableReference } from './refs'; import { SourceSlice } from '../../source/slice'; import { node } from './node'; @@ -83,24 +83,6 @@ export class PathExpression extends node('Path').fields<{ */ export class CallExpression extends node('Call').fields() {} -/** - * Corresponds to a possible deprecated helper call. Must be: - * - * 1. A free variable (not this.foo, not @foo, not local). - * 2. Argument-less. - * 3. In a component invocation's named argument position. - * 4. Not parenthesized (not @bar={{(helper)}}). - * 5. Not interpolated (not @bar="{{helper}}"). - * - * ```hbs - * - * ``` - */ -export class DeprecatedCallExpression extends node('DeprecatedCall').fields<{ - arg: SourceSlice; - callee: FreeVarReference; -}>() {} - /** * Corresponds to an interpolation in attribute value position. * @@ -116,5 +98,4 @@ export type ExpressionNode = | LiteralExpression | PathExpression | CallExpression - | DeprecatedCallExpression | InterpolateExpression; diff --git a/packages/@glimmer/syntax/lib/v2/objects/resolution.ts b/packages/@glimmer/syntax/lib/v2/objects/resolution.ts index f297a3746e..a13317a976 100644 --- a/packages/@glimmer/syntax/lib/v2/objects/resolution.ts +++ b/packages/@glimmer/syntax/lib/v2/objects/resolution.ts @@ -3,7 +3,6 @@ * * 1. Strict resolution * 2. Namespaced resolution - * 3. Fallback resolution */ import type { GetContextualFreeOpcode } from '@glimmer/interfaces'; @@ -13,7 +12,7 @@ import { SexpOpcodes } from '@glimmer/wire-format'; * Strict resolution is used: * * 1. in a strict mode template - * 2. in an unambiguous invocation with dot paths + * 2. in an local variable invocation with dot paths */ export const STRICT_RESOLUTION = { resolution: (): GetContextualFreeOpcode => SexpOpcodes.GetStrictKeyword, @@ -35,13 +34,10 @@ export function isStrictResolution(value: unknown): value is StrictResolution { } /** - * A `LooseModeResolution` includes: - * - * - 0 or more namespaces to resolve the variable in - * - optional fallback behavior + * A `LooseModeResolution` includes one or more namespaces to resolve the variable in * * In practice, there are a limited number of possible combinations of these degrees of freedom, - * and they are captured by the `Ambiguity` union below. + * and they are captured by the `Namespaces` union below. */ export class LooseModeResolution { /** @@ -51,156 +47,76 @@ export class LooseModeResolution { * 2. `{{#block}}` (namespace: `Component`) * 3. `` (namespace: `Modifier`) * 4. `` (namespace: `Component`) - * - * @see {NamespacedAmbiguity} */ static namespaced(namespace: FreeVarNamespace, isAngleBracket = false): LooseModeResolution { - return new LooseModeResolution( - { - namespaces: [namespace], - fallback: false, - }, - isAngleBracket - ); - } - - /** - * Fallback resolution is used when no namespaced resolutions are possible, but fallback - * resolution is still allowed. - * - * ```hbs - * {{x.y}} - * ``` - * - * @see {FallbackAmbiguity} - */ - static fallback(): LooseModeResolution { - return new LooseModeResolution({ namespaces: [], fallback: true }); + return new LooseModeResolution([namespace], isAngleBracket); } /** * Append resolution is used when the variable should be resolved in both the `component` and - * `helper` namespaces. Fallback resolution is optional. + * `helper` namespaces. * * ```hbs * {{x}} * ``` * - * ^ `x` should be resolved in the `component` and `helper` namespaces with fallback resolution. - * * ```hbs * {{x y}} * ``` * - * ^ `x` should be resolved in the `component` and `helper` namespaces without fallback - * resolution. - * - * @see {ComponentOrHelperAmbiguity} + * ^ In either case, `x` should be resolved in the `component` and `helper` namespaces. */ - static append({ invoke }: { invoke: boolean }): LooseModeResolution { - return new LooseModeResolution({ - namespaces: [FreeVarNamespace.Component, FreeVarNamespace.Helper], - fallback: !invoke, - }); + static append(): LooseModeResolution { + return new LooseModeResolution([FreeVarNamespace.Component, FreeVarNamespace.Helper]); } /** - * Trusting append resolution is used when the variable should be resolved in both the `component` and - * `helper` namespaces. Fallback resolution is optional. + * Trusting append resolution is used when the variable should be resolved only in the + * `helper` namespaces. * * ```hbs * {{{x}}} * ``` * - * ^ `x` should be resolved in the `component` and `helper` namespaces with fallback resolution. - * * ```hbs * {{{x y}}} * ``` * - * ^ `x` should be resolved in the `component` and `helper` namespaces without fallback - * resolution. - * - * @see {HelperAmbiguity} - */ - static trustingAppend({ invoke }: { invoke: boolean }): LooseModeResolution { - return new LooseModeResolution({ - namespaces: [FreeVarNamespace.Helper], - fallback: !invoke, - }); - } - - /** - * Attribute resolution is used when the variable should be resolved as a `helper` with fallback - * resolution. - * - * ```hbs - * - * - * ``` - * - * ^ resolved in the `helper` namespace with fallback - * - * @see {HelperAmbiguity} + * ^ In either case, `x` should be resolved in the `helper` namespace. */ - static attr(): LooseModeResolution { - return new LooseModeResolution({ namespaces: [FreeVarNamespace.Helper], fallback: true }); + static trustingAppend(): LooseModeResolution { + return this.namespaced(FreeVarNamespace.Helper); } constructor( - readonly ambiguity: Ambiguity, + readonly namespaces: Namespaces, readonly isAngleBracket = false ) {} resolution(): GetContextualFreeOpcode { - if (this.ambiguity.namespaces.length === 0) { - return SexpOpcodes.GetStrictKeyword; - } else if (this.ambiguity.namespaces.length === 1) { - if (this.ambiguity.fallback) { - // simple namespaced resolution with fallback must be attr={{x}} - return SexpOpcodes.GetFreeAsHelperHeadOrThisFallback; - } else { - // simple namespaced resolution without fallback - switch (this.ambiguity.namespaces[0]) { - case FreeVarNamespace.Helper: - return SexpOpcodes.GetFreeAsHelperHead; - case FreeVarNamespace.Modifier: - return SexpOpcodes.GetFreeAsModifierHead; - case FreeVarNamespace.Component: - return SexpOpcodes.GetFreeAsComponentHead; - } + if (this.namespaces.length === 1) { + switch (this.namespaces[0]) { + case FreeVarNamespace.Helper: + return SexpOpcodes.GetFreeAsHelperHead; + case FreeVarNamespace.Modifier: + return SexpOpcodes.GetFreeAsModifierHead; + case FreeVarNamespace.Component: + return SexpOpcodes.GetFreeAsComponentHead; } - } else if (this.ambiguity.fallback) { - // component or helper + fallback ({{something}}) - return SexpOpcodes.GetFreeAsComponentOrHelperHeadOrThisFallback; } else { - // component or helper without fallback ({{something something}}) return SexpOpcodes.GetFreeAsComponentOrHelperHead; } } serialize(): SerializedResolution { - if (this.ambiguity.namespaces.length === 0) { - return 'Loose'; - } else if (this.ambiguity.namespaces.length === 1) { - if (this.ambiguity.fallback) { - // simple namespaced resolution with fallback must be attr={{x}} - return ['ambiguous', SerializedAmbiguity.Attr]; - } else { - return ['ns', this.ambiguity.namespaces[0]]; - } - } else if (this.ambiguity.fallback) { - // component or helper + fallback ({{something}}) - return ['ambiguous', SerializedAmbiguity.Append]; + if (this.namespaces.length === 1) { + return this.namespaces[0]; } else { - // component or helper without fallback ({{something something}}) - return ['ambiguous', SerializedAmbiguity.Invoke]; + return 'ComponentOrHelper'; } } } -export const ARGUMENT_RESOLUTION = LooseModeResolution.fallback(); - export enum FreeVarNamespace { Helper = 'Helper', Modifier = 'Modifier', @@ -212,116 +128,48 @@ export const MODIFIER_NAMESPACE = FreeVarNamespace.Modifier; export const COMPONENT_NAMESPACE = FreeVarNamespace.Component; /** - * A `ComponentOrHelperAmbiguity` might be a component or a helper, with an optional fallback - * - * ```hbs - * {{x}} - * ``` - * - * ^ `x` is resolved in the `component` and `helper` namespaces, with fallback - * - * ```hbs - * {{x y}} - * ``` - * - * ^ `x` is resolved in the `component` and `helper` namespaces, without fallback - */ -type ComponentOrHelperAmbiguity = { - namespaces: [FreeVarNamespace.Component, FreeVarNamespace.Helper]; - fallback: boolean; -}; - -/** - * A `HelperAmbiguity` must be a helper, but it has fallback. If it didn't have fallback, it would - * be a `NamespacedAmbiguity`. - * - * ```hbs - * - * - * ``` - * - * ^ `x` is resolved in the `helper` namespace with fallback - */ -type HelperAmbiguity = { namespaces: [FreeVarNamespace.Helper]; fallback: boolean }; - -/** - * A `NamespacedAmbiguity` must be resolved in a particular namespace, without fallback. + * A `Namespaced` must be resolved in one or more namespaces. * * ```hbs * * ``` * - * ^ `X` is resolved in the `component` namespace without fallback + * ^ `X` is resolved in the `component` namespace * * ```hbs * (x) * ``` * - * ^ `x` is resolved in the `helper` namespace without fallback + * ^ `x` is resolved in the `helper` namespace * * ```hbs * * ``` * - * ^ `x` is resolved in the `modifier` namespace without fallback + * ^ `x` is resolved in the `modifier` namespace */ -type NamespacedAmbiguity = { - namespaces: [FreeVarNamespace.Component | FreeVarNamespace.Helper | FreeVarNamespace.Modifier]; - fallback: false; -}; - -type FallbackAmbiguity = { - namespaces: []; - fallback: true; -}; - -type Ambiguity = - | ComponentOrHelperAmbiguity - | HelperAmbiguity - | NamespacedAmbiguity - | FallbackAmbiguity; +type Namespaces = + | [FreeVarNamespace.Helper] + | [FreeVarNamespace.Modifier] + | [FreeVarNamespace.Component] + | [FreeVarNamespace.Component, FreeVarNamespace.Helper]; export type FreeVarResolution = StrictResolution | HtmlResolution | LooseModeResolution; // Serialization - -const enum SerializedAmbiguity { - // {{x}} - Append = 'Append', - // href={{x}} - Attr = 'Attr', - // {{x y}} (not attr) - Invoke = 'Invoke', -} - export type SerializedResolution = | 'Strict' - | 'Loose' - | ['ns', FreeVarNamespace] - | ['ambiguous', SerializedAmbiguity]; + | 'Helper' + | 'Modifier' + | 'Component' + | 'ComponentOrHelper'; export function loadResolution(resolution: SerializedResolution): FreeVarResolution { - if (typeof resolution === 'string') { - switch (resolution) { - case 'Loose': - return LooseModeResolution.fallback(); - case 'Strict': - return STRICT_RESOLUTION; - } - } - - switch (resolution[0]) { - case 'ambiguous': - switch (resolution[1]) { - case SerializedAmbiguity.Append: - return LooseModeResolution.append({ invoke: false }); - case SerializedAmbiguity.Attr: - return LooseModeResolution.attr(); - case SerializedAmbiguity.Invoke: - return LooseModeResolution.append({ invoke: true }); - } - - case 'ns': - return LooseModeResolution.namespaced(resolution[1]); + if (resolution === 'Strict') { + return STRICT_RESOLUTION; + } else if (resolution === 'ComponentOrHelper') { + return LooseModeResolution.append(); + } else { + return LooseModeResolution.namespaced(resolution as FreeVarNamespace); } } diff --git a/packages/@glimmer/syntax/lib/v2/serialize/serialize.ts b/packages/@glimmer/syntax/lib/v2/serialize/serialize.ts index 355caef414..6c6435814a 100644 --- a/packages/@glimmer/syntax/lib/v2/serialize/serialize.ts +++ b/packages/@glimmer/syntax/lib/v2/serialize/serialize.ts @@ -9,7 +9,6 @@ import type { SerializedBlock, SerializedCallExpression, SerializedContentNode, - SerializedDeprecatedCallExpression, SerializedElementModifier, SerializedExpressionNode, SerializedFreeVarReference, @@ -98,14 +97,6 @@ export class ExprSerializer { }; } - deprecatedCall(call: ASTv2.DeprecatedCallExpression): SerializedDeprecatedCallExpression { - return { - type: 'DeprecatedCall', - loc: call.loc.serialize(), - callee: REF.free(call.callee), - }; - } - interpolate(interpolate: ASTv2.InterpolateExpression): SerializedInterpolateExpression { return { type: 'Interpolate', @@ -281,8 +272,6 @@ const visit = { return EXPR.path(expr); case 'Call': return EXPR.call(expr); - case 'DeprecatedCall': - return EXPR.deprecatedCall(expr); case 'Interpolate': return EXPR.interpolate(expr); } diff --git a/packages/@glimmer/validator/test/validators-test.ts b/packages/@glimmer/validator/test/validators-test.ts index aa29d337b7..acd783fa86 100644 --- a/packages/@glimmer/validator/test/validators-test.ts +++ b/packages/@glimmer/validator/test/validators-test.ts @@ -254,11 +254,13 @@ module('@glimmer/validator: validators', () => { test('it ensures that any tags which it is combined with are also always invalid', (assert) => { let tag2 = createTag(); + debugger; let combined = combine([VOLATILE_TAG, tag2]); bump(); let snapshot = valueForTag(combined); + debugger; assert.notOk(validateTag(combined, snapshot)); }); diff --git a/packages/@glimmer/wire-format/lib/opcodes.ts b/packages/@glimmer/wire-format/lib/opcodes.ts index 9214151f9d..824348c763 100644 --- a/packages/@glimmer/wire-format/lib/opcodes.ts +++ b/packages/@glimmer/wire-format/lib/opcodes.ts @@ -17,10 +17,7 @@ import type { GetDynamicVarOpcode, GetFreeAsComponentHeadOpcode, GetFreeAsComponentOrHelperHeadOpcode, - GetFreeAsComponentOrHelperHeadOrThisFallbackOpcode, - GetFreeAsDeprecatedHelperHeadOrThisFallbackOpcode, GetFreeAsHelperHeadOpcode, - GetFreeAsHelperHeadOrThisFallbackOpcode, GetFreeAsModifierHeadOpcode, GetLexicalSymbolOpcode, GetStrictKeywordOpcode, @@ -47,7 +44,6 @@ import type { TrustingDynamicAttrOpcode, UndefinedOpcode, WithDynamicVarsOpcode, - WithOpcode, YieldOpcode, } from '@glimmer/interfaces'; @@ -81,19 +77,13 @@ export const opcodes = { GetSymbol: 30 satisfies GetSymbolOpcode, GetLexicalSymbol: 32 satisfies GetLexicalSymbolOpcode, GetStrictKeyword: 31 satisfies GetStrictKeywordOpcode, - GetFreeAsComponentOrHelperHeadOrThisFallback: - 34 satisfies GetFreeAsComponentOrHelperHeadOrThisFallbackOpcode, GetFreeAsComponentOrHelperHead: 35 satisfies GetFreeAsComponentOrHelperHeadOpcode, - GetFreeAsHelperHeadOrThisFallback: 36 satisfies GetFreeAsHelperHeadOrThisFallbackOpcode, - GetFreeAsDeprecatedHelperHeadOrThisFallback: - 99 satisfies GetFreeAsDeprecatedHelperHeadOrThisFallbackOpcode, GetFreeAsHelperHead: 37 satisfies GetFreeAsHelperHeadOpcode, GetFreeAsModifierHead: 38 satisfies GetFreeAsModifierHeadOpcode, GetFreeAsComponentHead: 39 satisfies GetFreeAsComponentHeadOpcode, InElement: 40 satisfies InElementOpcode, If: 41 satisfies IfOpcode, Each: 42 satisfies EachOpcode, - With: 43 satisfies WithOpcode, Let: 44 satisfies LetOpcode, WithDynamicVars: 45 satisfies WithDynamicVarsOpcode, InvokeComponent: 46 satisfies InvokeComponentOpcode, diff --git a/packages/@glimmer/wire-format/lib/resolution.ts b/packages/@glimmer/wire-format/lib/resolution.ts index f68a0c387c..22d560819c 100644 --- a/packages/@glimmer/wire-format/lib/resolution.ts +++ b/packages/@glimmer/wire-format/lib/resolution.ts @@ -1,29 +1,23 @@ import type { - AmbiguousAppendInvokeResolution, - AmbiguousAppendResolution, - AmbiguousInvokeResolution, - ResolveAsCallHeadResolution, ResolveAsComponentHeadResolution, + ResolveAsComponentOrHelperHeadResolution, + ResolveAsHelperHeadResolution, ResolveAsModifierHeadResolution, StrictResolution, } from '@glimmer/interfaces'; // eslint-disable-next-line @typescript-eslint/naming-convention export type resolution = - | AmbiguousAppendInvokeResolution - | AmbiguousAppendResolution - | AmbiguousInvokeResolution - | ResolveAsCallHeadResolution + | ResolveAsComponentOrHelperHeadResolution + | ResolveAsHelperHeadResolution | ResolveAsComponentHeadResolution | ResolveAsModifierHeadResolution | StrictResolution; export const resolution = { Strict: 0 satisfies StrictResolution, - AmbiguousAppend: 1 satisfies AmbiguousAppendResolution, - AmbiguousAppendInvoke: 2 satisfies AmbiguousAppendInvokeResolution, - AmbiguousInvoke: 3 satisfies AmbiguousInvokeResolution, - ResolveAsCallHead: 5 satisfies ResolveAsCallHeadResolution, + ResolveAsComponentOrHelperHead: 1 satisfies ResolveAsComponentOrHelperHeadResolution, + ResolveAsHelperHead: 5 satisfies ResolveAsHelperHeadResolution, ResolveAsModifierHead: 6 satisfies ResolveAsModifierHeadResolution, ResolveAsComponentHead: 7 satisfies ResolveAsComponentHeadResolution, } as const;