From e9bee5456b0542da1898be0127fa5fe06c2fec3e Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 10 Apr 2026 11:33:05 -0400 Subject: [PATCH 1/3] RFC#561 - {{lt}}, {{lte}}, {{gt}}, {{gte}} as keywords Add comparison helpers and register them as built-in keywords so they no longer need to be imported in strict-mode (gjs/gts) templates. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/@ember/helper/index.ts | 100 ++++++++++++++++++ .../template-compiler/lib/compile-options.ts | 6 +- .../lib/plugins/auto-import-builtins.ts | 52 +++++++++ .../test/keywords/gt-runtime-test.ts | 60 +++++++++++ .../test/keywords/gt-test.ts | 60 +++++++++++ .../test/keywords/gte-runtime-test.ts | 40 +++++++ .../test/keywords/gte-test.ts | 60 +++++++++++ .../test/keywords/lt-runtime-test.ts | 60 +++++++++++ .../test/keywords/lt-test.ts | 60 +++++++++++ .../test/keywords/lte-runtime-test.ts | 40 +++++++ .../test/keywords/lte-test.ts | 60 +++++++++++ packages/@glimmer/runtime/index.ts | 4 + packages/@glimmer/runtime/lib/helpers/gt.ts | 9 ++ packages/@glimmer/runtime/lib/helpers/gte.ts | 9 ++ packages/@glimmer/runtime/lib/helpers/lt.ts | 9 ++ packages/@glimmer/runtime/lib/helpers/lte.ts | 9 ++ smoke-tests/scenarios/basic-test.ts | 32 ++++++ 17 files changed, 669 insertions(+), 1 deletion(-) create mode 100644 packages/@glimmer-workspace/integration-tests/test/keywords/gt-runtime-test.ts create mode 100644 packages/@glimmer-workspace/integration-tests/test/keywords/gt-test.ts create mode 100644 packages/@glimmer-workspace/integration-tests/test/keywords/gte-runtime-test.ts create mode 100644 packages/@glimmer-workspace/integration-tests/test/keywords/gte-test.ts create mode 100644 packages/@glimmer-workspace/integration-tests/test/keywords/lt-runtime-test.ts create mode 100644 packages/@glimmer-workspace/integration-tests/test/keywords/lt-test.ts create mode 100644 packages/@glimmer-workspace/integration-tests/test/keywords/lte-runtime-test.ts create mode 100644 packages/@glimmer-workspace/integration-tests/test/keywords/lte-test.ts create mode 100644 packages/@glimmer/runtime/lib/helpers/gt.ts create mode 100644 packages/@glimmer/runtime/lib/helpers/gte.ts create mode 100644 packages/@glimmer/runtime/lib/helpers/lt.ts create mode 100644 packages/@glimmer/runtime/lib/helpers/lte.ts diff --git a/packages/@ember/helper/index.ts b/packages/@ember/helper/index.ts index bdf745a9974..06f93955567 100644 --- a/packages/@ember/helper/index.ts +++ b/packages/@ember/helper/index.ts @@ -12,6 +12,10 @@ import { get as glimmerGet, fn as glimmerFn, neq as glimmerNeq, + gt as glimmerGt, + gte as glimmerGte, + lt as glimmerLt, + lte as glimmerLte, } from '@glimmer/runtime'; import { element as glimmerElement, uniqueId as glimmerUniqueId } from '@ember/-internals/glimmer'; import { type Opaque } from '@ember/-internals/utility-types'; @@ -472,6 +476,102 @@ export interface GetHelper extends Opaque<'helper:get'> {} export const fn = glimmerFn as FnHelper; export interface FnHelper extends Opaque<'helper:fn'> {} +/** + * The `{{gt}}` helper returns `true` if the first argument is greater than + * the second argument. + * + * ```js + * import { gt } from '@ember/helper'; + * + * + * ``` + * + * In strict-mode (gjs/gts) templates, `gt` is available as a keyword and + * does not need to be imported. + * + * @method gt + * @param {number} left + * @param {number} right + * @return {boolean} + * @public + */ +export const gt = glimmerGt as unknown as GtHelper; +export interface GtHelper extends Opaque<'helper:gt'> {} + +/** + * The `{{gte}}` helper returns `true` if the first argument is greater than + * or equal to the second argument. + * + * ```js + * import { gte } from '@ember/helper'; + * + * + * ``` + * + * In strict-mode (gjs/gts) templates, `gte` is available as a keyword and + * does not need to be imported. + * + * @method gte + * @param {number} left + * @param {number} right + * @return {boolean} + * @public + */ +export const gte = glimmerGte as unknown as GteHelper; +export interface GteHelper extends Opaque<'helper:gte'> {} + +/** + * The `{{lt}}` helper returns `true` if the first argument is less than + * the second argument. + * + * ```js + * import { lt } from '@ember/helper'; + * + * + * ``` + * + * In strict-mode (gjs/gts) templates, `lt` is available as a keyword and + * does not need to be imported. + * + * @method lt + * @param {number} left + * @param {number} right + * @return {boolean} + * @public + */ +export const lt = glimmerLt as unknown as LtHelper; +export interface LtHelper extends Opaque<'helper:lt'> {} + +/** + * The `{{lte}}` helper returns `true` if the first argument is less than + * or equal to the second argument. + * + * ```js + * import { lte } from '@ember/helper'; + * + * + * ``` + * + * In strict-mode (gjs/gts) templates, `lte` is available as a keyword and + * does not need to be imported. + * + * @method lte + * @param {number} left + * @param {number} right + * @return {boolean} + * @public + */ +export const lte = glimmerLte as unknown as LteHelper; +export interface LteHelper extends Opaque<'helper:lte'> {} + /** * The `element` helper lets you dynamically set the tag name of an element. * diff --git a/packages/@ember/template-compiler/lib/compile-options.ts b/packages/@ember/template-compiler/lib/compile-options.ts index 10fb8e0bf1a..03a5f3e78a6 100644 --- a/packages/@ember/template-compiler/lib/compile-options.ts +++ b/packages/@ember/template-compiler/lib/compile-options.ts @@ -1,4 +1,4 @@ -import { array, eq, fn, hash, neq } from '@ember/helper'; +import { array, eq, fn, hash, neq, lt, lte, gt, gte } from '@ember/helper'; import { on } from '@ember/modifier'; import { assert } from '@ember/debug'; import { @@ -30,6 +30,10 @@ export const keywords: Record = { fn, hash, neq, + gt, + gte, + lt, + lte, on, }; diff --git a/packages/@ember/template-compiler/lib/plugins/auto-import-builtins.ts b/packages/@ember/template-compiler/lib/plugins/auto-import-builtins.ts index bf3ee5b993d..3ae501b7e6c 100644 --- a/packages/@ember/template-compiler/lib/plugins/auto-import-builtins.ts +++ b/packages/@ember/template-compiler/lib/plugins/auto-import-builtins.ts @@ -42,6 +42,18 @@ export default function autoImportBuiltins(env: EmberASTPluginEnvironment): ASTP if (isNeq(node, hasLocal)) { rewriteKeyword(env, node, 'neq', '@ember/helper'); } + if (isGt(node, hasLocal)) { + rewriteKeyword(env, node, 'gt', '@ember/helper'); + } + if (isGte(node, hasLocal)) { + rewriteKeyword(env, node, 'gte', '@ember/helper'); + } + if (isLt(node, hasLocal)) { + rewriteKeyword(env, node, 'lt', '@ember/helper'); + } + if (isLte(node, hasLocal)) { + rewriteKeyword(env, node, 'lte', '@ember/helper'); + } }, MustacheStatement(node: AST.MustacheStatement) { if (isArray(node, hasLocal)) { @@ -59,6 +71,18 @@ export default function autoImportBuiltins(env: EmberASTPluginEnvironment): ASTP if (isNeq(node, hasLocal)) { rewriteKeyword(env, node, 'neq', '@ember/helper'); } + if (isGt(node, hasLocal)) { + rewriteKeyword(env, node, 'gt', '@ember/helper'); + } + if (isGte(node, hasLocal)) { + rewriteKeyword(env, node, 'gte', '@ember/helper'); + } + if (isLt(node, hasLocal)) { + rewriteKeyword(env, node, 'lt', '@ember/helper'); + } + if (isLte(node, hasLocal)) { + rewriteKeyword(env, node, 'lte', '@ember/helper'); + } }, }, }; @@ -120,3 +144,31 @@ function isNeq( ): node is (AST.MustacheStatement | AST.SubExpression) & { path: AST.PathExpression } { return isPath(node.path) && node.path.original === 'neq' && !hasLocal('neq'); } + +function isGt( + node: AST.MustacheStatement | AST.SubExpression, + hasLocal: (k: string) => boolean +): node is (AST.MustacheStatement | AST.SubExpression) & { path: AST.PathExpression } { + return isPath(node.path) && node.path.original === 'gt' && !hasLocal('gt'); +} + +function isGte( + node: AST.MustacheStatement | AST.SubExpression, + hasLocal: (k: string) => boolean +): node is (AST.MustacheStatement | AST.SubExpression) & { path: AST.PathExpression } { + return isPath(node.path) && node.path.original === 'gte' && !hasLocal('gte'); +} + +function isLt( + node: AST.MustacheStatement | AST.SubExpression, + hasLocal: (k: string) => boolean +): node is (AST.MustacheStatement | AST.SubExpression) & { path: AST.PathExpression } { + return isPath(node.path) && node.path.original === 'lt' && !hasLocal('lt'); +} + +function isLte( + node: AST.MustacheStatement | AST.SubExpression, + hasLocal: (k: string) => boolean +): node is (AST.MustacheStatement | AST.SubExpression) & { path: AST.PathExpression } { + return isPath(node.path) && node.path.original === 'lte' && !hasLocal('lte'); +} diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/gt-runtime-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/gt-runtime-test.ts new file mode 100644 index 00000000000..14ae5e655c0 --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/gt-runtime-test.ts @@ -0,0 +1,60 @@ +import { + GlimmerishComponent, + jitSuite, + RenderTest, + test, +} from '@glimmer-workspace/integration-tests'; + +import { template } from '@ember/template-compiler/runtime'; + +class KeywordGtRuntime extends RenderTest { + static suiteName = 'keyword helper: gt (runtime)'; + + @test + 'explicit scope'() { + const compiled = template('{{if (gt a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ a: 3, b: 2 }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'implicit scope (eval)'() { + let a = 3; + let b = 2; + hide(a); + hide(b); + const compiled = template('{{if (gt a b) "yes" "no"}}', { + strictMode: true, + eval() { + return eval(arguments[0]); + }, + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'no eval and no scope'() { + class Foo extends GlimmerishComponent { + a = 3; + b = 2; + static { + template('{{if (gt this.a this.b) "yes" "no"}}', { + strictMode: true, + component: this, + }); + } + } + this.renderComponent(Foo); + this.assertHTML('yes'); + } +} + +jitSuite(KeywordGtRuntime); + +const hide = (variable: unknown) => { + new Function(`return (${JSON.stringify(variable)});`); +}; diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/gt-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/gt-test.ts new file mode 100644 index 00000000000..c55bfdd8b9b --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/gt-test.ts @@ -0,0 +1,60 @@ +import { DEBUG } from '@glimmer/env'; +import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests'; + +import { template } from '@ember/template-compiler'; +import { gt } from '@ember/helper'; + +class KeywordGt extends RenderTest { + static suiteName = 'keyword helper: gt'; + + @test + 'returns true when first arg is greater'() { + let a = 3; + let b = 2; + const compiled = template('{{if (gt a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ gt, a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'returns false when first arg is equal'() { + let a = 2; + let b = 2; + const compiled = template('{{if (gt a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ gt, a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + + @test + 'returns false when first arg is less'() { + let a = 1; + let b = 2; + const compiled = template('{{if (gt a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ gt, a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + + @test({ skip: !DEBUG }) + 'throws if not called with exactly two arguments'(assert: Assert) { + let a = 1; + const compiled = template('{{gt a}}', { + strictMode: true, + scope: () => ({ gt, a }), + }); + + assert.throws(() => { + this.renderComponent(compiled); + }, /`gt` expects exactly two arguments/); + } +} + +jitSuite(KeywordGt); diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/gte-runtime-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/gte-runtime-test.ts new file mode 100644 index 00000000000..3b440a23080 --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/gte-runtime-test.ts @@ -0,0 +1,40 @@ +import { + GlimmerishComponent, + jitSuite, + RenderTest, + test, +} from '@glimmer-workspace/integration-tests'; + +import { template } from '@ember/template-compiler/runtime'; + +class KeywordGteRuntime extends RenderTest { + static suiteName = 'keyword helper: gte (runtime)'; + + @test + 'explicit scope'() { + const compiled = template('{{if (gte a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ a: 2, b: 2 }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'no eval and no scope'() { + class Foo extends GlimmerishComponent { + a = 2; + b = 2; + static { + template('{{if (gte this.a this.b) "yes" "no"}}', { + strictMode: true, + component: this, + }); + } + } + this.renderComponent(Foo); + this.assertHTML('yes'); + } +} + +jitSuite(KeywordGteRuntime); diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/gte-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/gte-test.ts new file mode 100644 index 00000000000..bcaca7d7035 --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/gte-test.ts @@ -0,0 +1,60 @@ +import { DEBUG } from '@glimmer/env'; +import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests'; + +import { template } from '@ember/template-compiler'; +import { gte } from '@ember/helper'; + +class KeywordGte extends RenderTest { + static suiteName = 'keyword helper: gte'; + + @test + 'returns true when first arg is greater'() { + let a = 3; + let b = 2; + const compiled = template('{{if (gte a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ gte, a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'returns true when first arg is equal'() { + let a = 2; + let b = 2; + const compiled = template('{{if (gte a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ gte, a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'returns false when first arg is less'() { + let a = 1; + let b = 2; + const compiled = template('{{if (gte a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ gte, a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + + @test({ skip: !DEBUG }) + 'throws if not called with exactly two arguments'(assert: Assert) { + let a = 1; + const compiled = template('{{gte a}}', { + strictMode: true, + scope: () => ({ gte, a }), + }); + + assert.throws(() => { + this.renderComponent(compiled); + }, /`gte` expects exactly two arguments/); + } +} + +jitSuite(KeywordGte); diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/lt-runtime-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/lt-runtime-test.ts new file mode 100644 index 00000000000..bf1a86230e1 --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/lt-runtime-test.ts @@ -0,0 +1,60 @@ +import { + GlimmerishComponent, + jitSuite, + RenderTest, + test, +} from '@glimmer-workspace/integration-tests'; + +import { template } from '@ember/template-compiler/runtime'; + +class KeywordLtRuntime extends RenderTest { + static suiteName = 'keyword helper: lt (runtime)'; + + @test + 'explicit scope'() { + const compiled = template('{{if (lt a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ a: 1, b: 2 }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'implicit scope (eval)'() { + let a = 1; + let b = 2; + hide(a); + hide(b); + const compiled = template('{{if (lt a b) "yes" "no"}}', { + strictMode: true, + eval() { + return eval(arguments[0]); + }, + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'no eval and no scope'() { + class Foo extends GlimmerishComponent { + a = 1; + b = 2; + static { + template('{{if (lt this.a this.b) "yes" "no"}}', { + strictMode: true, + component: this, + }); + } + } + this.renderComponent(Foo); + this.assertHTML('yes'); + } +} + +jitSuite(KeywordLtRuntime); + +const hide = (variable: unknown) => { + new Function(`return (${JSON.stringify(variable)});`); +}; diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/lt-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/lt-test.ts new file mode 100644 index 00000000000..122ee8a6592 --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/lt-test.ts @@ -0,0 +1,60 @@ +import { DEBUG } from '@glimmer/env'; +import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests'; + +import { template } from '@ember/template-compiler'; +import { lt } from '@ember/helper'; + +class KeywordLt extends RenderTest { + static suiteName = 'keyword helper: lt'; + + @test + 'returns true when first arg is less'() { + let a = 1; + let b = 2; + const compiled = template('{{if (lt a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ lt, a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'returns false when first arg is equal'() { + let a = 2; + let b = 2; + const compiled = template('{{if (lt a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ lt, a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + + @test + 'returns false when first arg is greater'() { + let a = 3; + let b = 2; + const compiled = template('{{if (lt a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ lt, a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + + @test({ skip: !DEBUG }) + 'throws if not called with exactly two arguments'(assert: Assert) { + let a = 1; + const compiled = template('{{lt a}}', { + strictMode: true, + scope: () => ({ lt, a }), + }); + + assert.throws(() => { + this.renderComponent(compiled); + }, /`lt` expects exactly two arguments/); + } +} + +jitSuite(KeywordLt); diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/lte-runtime-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/lte-runtime-test.ts new file mode 100644 index 00000000000..a5007816398 --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/lte-runtime-test.ts @@ -0,0 +1,40 @@ +import { + GlimmerishComponent, + jitSuite, + RenderTest, + test, +} from '@glimmer-workspace/integration-tests'; + +import { template } from '@ember/template-compiler/runtime'; + +class KeywordLteRuntime extends RenderTest { + static suiteName = 'keyword helper: lte (runtime)'; + + @test + 'explicit scope'() { + const compiled = template('{{if (lte a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ a: 2, b: 2 }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'no eval and no scope'() { + class Foo extends GlimmerishComponent { + a = 2; + b = 2; + static { + template('{{if (lte this.a this.b) "yes" "no"}}', { + strictMode: true, + component: this, + }); + } + } + this.renderComponent(Foo); + this.assertHTML('yes'); + } +} + +jitSuite(KeywordLteRuntime); diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/lte-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/lte-test.ts new file mode 100644 index 00000000000..fd985049ff3 --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/lte-test.ts @@ -0,0 +1,60 @@ +import { DEBUG } from '@glimmer/env'; +import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests'; + +import { template } from '@ember/template-compiler'; +import { lte } from '@ember/helper'; + +class KeywordLte extends RenderTest { + static suiteName = 'keyword helper: lte'; + + @test + 'returns true when first arg is less'() { + let a = 1; + let b = 2; + const compiled = template('{{if (lte a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ lte, a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'returns true when first arg is equal'() { + let a = 2; + let b = 2; + const compiled = template('{{if (lte a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ lte, a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + + @test + 'returns false when first arg is greater'() { + let a = 3; + let b = 2; + const compiled = template('{{if (lte a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ lte, a, b }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + + @test({ skip: !DEBUG }) + 'throws if not called with exactly two arguments'(assert: Assert) { + let a = 1; + const compiled = template('{{lte a}}', { + strictMode: true, + scope: () => ({ lte, a }), + }); + + assert.throws(() => { + this.renderComponent(compiled); + }, /`lte` expects exactly two arguments/); + } +} + +jitSuite(KeywordLte); diff --git a/packages/@glimmer/runtime/index.ts b/packages/@glimmer/runtime/index.ts index 617edd8ae30..a46d04a7266 100644 --- a/packages/@glimmer/runtime/index.ts +++ b/packages/@glimmer/runtime/index.ts @@ -36,7 +36,11 @@ export { concat } from './lib/helpers/concat'; export { eq } from './lib/helpers/eq'; export { fn } from './lib/helpers/fn'; export { get } from './lib/helpers/get'; +export { gt } from './lib/helpers/gt'; +export { gte } from './lib/helpers/gte'; export { hash } from './lib/helpers/hash'; +export { lt } from './lib/helpers/lt'; +export { lte } from './lib/helpers/lte'; export { invokeHelper } from './lib/helpers/invoke'; export { neq } from './lib/helpers/neq'; export { on } from './lib/modifiers/on'; diff --git a/packages/@glimmer/runtime/lib/helpers/gt.ts b/packages/@glimmer/runtime/lib/helpers/gt.ts new file mode 100644 index 00000000000..38101c1b566 --- /dev/null +++ b/packages/@glimmer/runtime/lib/helpers/gt.ts @@ -0,0 +1,9 @@ +import { DEBUG } from '@glimmer/env'; + +export const gt = (...args: unknown[]) => { + if (DEBUG && args.length !== 2) { + throw new Error(`\`gt\` expects exactly two arguments, but received ${args.length}.`); + } + + return (args[0] as number) > (args[1] as number); +}; diff --git a/packages/@glimmer/runtime/lib/helpers/gte.ts b/packages/@glimmer/runtime/lib/helpers/gte.ts new file mode 100644 index 00000000000..2b69a96e28e --- /dev/null +++ b/packages/@glimmer/runtime/lib/helpers/gte.ts @@ -0,0 +1,9 @@ +import { DEBUG } from '@glimmer/env'; + +export const gte = (...args: unknown[]) => { + if (DEBUG && args.length !== 2) { + throw new Error(`\`gte\` expects exactly two arguments, but received ${args.length}.`); + } + + return (args[0] as number) >= (args[1] as number); +}; diff --git a/packages/@glimmer/runtime/lib/helpers/lt.ts b/packages/@glimmer/runtime/lib/helpers/lt.ts new file mode 100644 index 00000000000..949b3c96ad8 --- /dev/null +++ b/packages/@glimmer/runtime/lib/helpers/lt.ts @@ -0,0 +1,9 @@ +import { DEBUG } from '@glimmer/env'; + +export const lt = (...args: unknown[]) => { + if (DEBUG && args.length !== 2) { + throw new Error(`\`lt\` expects exactly two arguments, but received ${args.length}.`); + } + + return (args[0] as number) < (args[1] as number); +}; diff --git a/packages/@glimmer/runtime/lib/helpers/lte.ts b/packages/@glimmer/runtime/lib/helpers/lte.ts new file mode 100644 index 00000000000..d67a5441318 --- /dev/null +++ b/packages/@glimmer/runtime/lib/helpers/lte.ts @@ -0,0 +1,9 @@ +import { DEBUG } from '@glimmer/env'; + +export const lte = (...args: unknown[]) => { + if (DEBUG && args.length !== 2) { + throw new Error(`\`lte\` expects exactly two arguments, but received ${args.length}.`); + } + + return (args[0] as number) <= (args[1] as number); +}; diff --git a/smoke-tests/scenarios/basic-test.ts b/smoke-tests/scenarios/basic-test.ts index e1c1afc3cc3..3a766b125c7 100644 --- a/smoke-tests/scenarios/basic-test.ts +++ b/smoke-tests/scenarios/basic-test.ts @@ -442,6 +442,38 @@ function basicTest(scenarios: Scenarios, appName: string) { }); }); `, + 'comparison-helpers-as-keyword-test.gjs': ` + import { module, test } from 'qunit'; + import { setupRenderingTest } from 'ember-qunit'; + import { render } from '@ember/test-helpers'; + + import Component from '@glimmer/component'; + + class LtDemo extends Component { + + } + + module('comparison helpers as keywords', function(hooks) { + setupRenderingTest(hooks); + + test('lt, lte, gt, gte work without imports', async function(assert) { + await render(LtDemo); + assert.dom('[data-test="lt"]').hasText('yes'); + assert.dom('[data-test="lte-equal"]').hasText('yes'); + assert.dom('[data-test="gt"]').hasText('yes'); + assert.dom('[data-test="gte-equal"]').hasText('yes'); + assert.dom('[data-test="lt-false"]').hasText('no'); + assert.dom('[data-test="gt-false"]').hasText('no'); + }); + }); + `, 'fn-as-keyword-but-its-shadowed-test.gjs': ` import QUnit, { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; From a7d6ec5056e2473039c463cacec59785d335476b Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sun, 26 Apr 2026 12:52:13 -0400 Subject: [PATCH 2/3] Cleanup --- .../test/keywords/gt-runtime-test.ts | 14 ++++- .../test/keywords/gt-test.ts | 60 ++++++++++++++++-- .../test/keywords/gte-runtime-test.ts | 34 +++++++++- .../test/keywords/gte-test.ts | 60 ++++++++++++++++-- .../test/keywords/lt-runtime-test.ts | 14 ++++- .../test/keywords/lt-test.ts | 60 ++++++++++++++++-- .../test/keywords/lte-runtime-test.ts | 34 +++++++++- .../test/keywords/lte-test.ts | 60 ++++++++++++++++-- packages/@glimmer/runtime/index.ts | 2 +- smoke-tests/scenarios/basic-test.ts | 63 ++++++++++++------- 10 files changed, 350 insertions(+), 51 deletions(-) diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/gt-runtime-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/gt-runtime-test.ts index 14ae5e655c0..a4503e4979b 100644 --- a/packages/@glimmer-workspace/integration-tests/test/keywords/gt-runtime-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/gt-runtime-test.ts @@ -20,6 +20,16 @@ class KeywordGtRuntime extends RenderTest { this.assertHTML('yes'); } + @test + 'explicit scope (shadowed)'() { + const compiled = template('{{if (gt a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ gt: () => false, a: 3, b: 2 }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + @test 'implicit scope (eval)'() { let a = 3; @@ -39,14 +49,14 @@ class KeywordGtRuntime extends RenderTest { @test 'no eval and no scope'() { class Foo extends GlimmerishComponent { - a = 3; - b = 2; static { template('{{if (gt this.a this.b) "yes" "no"}}', { strictMode: true, component: this, }); } + a = 3; + b = 2; } this.renderComponent(Foo); this.assertHTML('yes'); diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/gt-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/gt-test.ts index c55bfdd8b9b..530ba7b1c2d 100644 --- a/packages/@glimmer-workspace/integration-tests/test/keywords/gt-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/gt-test.ts @@ -2,18 +2,64 @@ import { DEBUG } from '@glimmer/env'; import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests'; import { template } from '@ember/template-compiler'; -import { gt } from '@ember/helper'; class KeywordGt extends RenderTest { static suiteName = 'keyword helper: gt'; + @test + 'explicit scope'() { + let a = 3; + let b = 2; + + const compiled = template('{{gt a b}}', { + strictMode: true, + scope: () => ({ a, b }), + }); + + this.renderComponent(compiled); + this.assertHTML('true'); + } + + @test + 'explicit scope (shadowed)'() { + let a = 3; + let b = 2; + let gt = () => 'surprise'; + const compiled = template('{{gt a b}}', { + strictMode: true, + scope: () => ({ gt, a, b }), + }); + + this.renderComponent(compiled); + this.assertHTML('surprise'); + } + + @test + 'implicit scope (eval)'() { + let a = 3; + let b = 2; + + hide(a); + hide(b); + + const compiled = template('{{if (gt a b) "yes" "no"}}', { + strictMode: true, + eval() { + return eval(arguments[0]); + }, + }); + + this.renderComponent(compiled); + this.assertHTML('yes'); + } + @test 'returns true when first arg is greater'() { let a = 3; let b = 2; const compiled = template('{{if (gt a b) "yes" "no"}}', { strictMode: true, - scope: () => ({ gt, a, b }), + scope: () => ({ a, b }), }); this.renderComponent(compiled); this.assertHTML('yes'); @@ -25,7 +71,7 @@ class KeywordGt extends RenderTest { let b = 2; const compiled = template('{{if (gt a b) "yes" "no"}}', { strictMode: true, - scope: () => ({ gt, a, b }), + scope: () => ({ a, b }), }); this.renderComponent(compiled); this.assertHTML('no'); @@ -37,7 +83,7 @@ class KeywordGt extends RenderTest { let b = 2; const compiled = template('{{if (gt a b) "yes" "no"}}', { strictMode: true, - scope: () => ({ gt, a, b }), + scope: () => ({ a, b }), }); this.renderComponent(compiled); this.assertHTML('no'); @@ -48,7 +94,7 @@ class KeywordGt extends RenderTest { let a = 1; const compiled = template('{{gt a}}', { strictMode: true, - scope: () => ({ gt, a }), + scope: () => ({ a }), }); assert.throws(() => { @@ -58,3 +104,7 @@ class KeywordGt extends RenderTest { } jitSuite(KeywordGt); + +const hide = (variable: unknown) => { + new Function(`return (${JSON.stringify(variable)});`); +}; diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/gte-runtime-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/gte-runtime-test.ts index 3b440a23080..9edbba57685 100644 --- a/packages/@glimmer-workspace/integration-tests/test/keywords/gte-runtime-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/gte-runtime-test.ts @@ -20,17 +20,43 @@ class KeywordGteRuntime extends RenderTest { this.assertHTML('yes'); } + @test + 'explicit scope (shadowed)'() { + const compiled = template('{{if (gte a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ gte: () => false, a: 2, b: 2 }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + + @test + 'implicit scope (eval)'() { + let a = 2; + let b = 2; + hide(a); + hide(b); + const compiled = template('{{if (gte a b) "yes" "no"}}', { + strictMode: true, + eval() { + return eval(arguments[0]); + }, + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + @test 'no eval and no scope'() { class Foo extends GlimmerishComponent { - a = 2; - b = 2; static { template('{{if (gte this.a this.b) "yes" "no"}}', { strictMode: true, component: this, }); } + a = 2; + b = 2; } this.renderComponent(Foo); this.assertHTML('yes'); @@ -38,3 +64,7 @@ class KeywordGteRuntime extends RenderTest { } jitSuite(KeywordGteRuntime); + +const hide = (variable: unknown) => { + new Function(`return (${JSON.stringify(variable)});`); +}; diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/gte-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/gte-test.ts index bcaca7d7035..b13d97b0524 100644 --- a/packages/@glimmer-workspace/integration-tests/test/keywords/gte-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/gte-test.ts @@ -2,18 +2,64 @@ import { DEBUG } from '@glimmer/env'; import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests'; import { template } from '@ember/template-compiler'; -import { gte } from '@ember/helper'; class KeywordGte extends RenderTest { static suiteName = 'keyword helper: gte'; + @test + 'explicit scope'() { + let a = 2; + let b = 2; + + const compiled = template('{{gte a b}}', { + strictMode: true, + scope: () => ({ a, b }), + }); + + this.renderComponent(compiled); + this.assertHTML('true'); + } + + @test + 'explicit scope (shadowed)'() { + let a = 3; + let b = 2; + let gte = () => 'surprise'; + const compiled = template('{{gte a b}}', { + strictMode: true, + scope: () => ({ gte, a, b }), + }); + + this.renderComponent(compiled); + this.assertHTML('surprise'); + } + + @test + 'implicit scope (eval)'() { + let a = 2; + let b = 2; + + hide(a); + hide(b); + + const compiled = template('{{if (gte a b) "yes" "no"}}', { + strictMode: true, + eval() { + return eval(arguments[0]); + }, + }); + + this.renderComponent(compiled); + this.assertHTML('yes'); + } + @test 'returns true when first arg is greater'() { let a = 3; let b = 2; const compiled = template('{{if (gte a b) "yes" "no"}}', { strictMode: true, - scope: () => ({ gte, a, b }), + scope: () => ({ a, b }), }); this.renderComponent(compiled); this.assertHTML('yes'); @@ -25,7 +71,7 @@ class KeywordGte extends RenderTest { let b = 2; const compiled = template('{{if (gte a b) "yes" "no"}}', { strictMode: true, - scope: () => ({ gte, a, b }), + scope: () => ({ a, b }), }); this.renderComponent(compiled); this.assertHTML('yes'); @@ -37,7 +83,7 @@ class KeywordGte extends RenderTest { let b = 2; const compiled = template('{{if (gte a b) "yes" "no"}}', { strictMode: true, - scope: () => ({ gte, a, b }), + scope: () => ({ a, b }), }); this.renderComponent(compiled); this.assertHTML('no'); @@ -48,7 +94,7 @@ class KeywordGte extends RenderTest { let a = 1; const compiled = template('{{gte a}}', { strictMode: true, - scope: () => ({ gte, a }), + scope: () => ({ a }), }); assert.throws(() => { @@ -58,3 +104,7 @@ class KeywordGte extends RenderTest { } jitSuite(KeywordGte); + +const hide = (variable: unknown) => { + new Function(`return (${JSON.stringify(variable)});`); +}; diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/lt-runtime-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/lt-runtime-test.ts index bf1a86230e1..5b0913c5e1c 100644 --- a/packages/@glimmer-workspace/integration-tests/test/keywords/lt-runtime-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/lt-runtime-test.ts @@ -20,6 +20,16 @@ class KeywordLtRuntime extends RenderTest { this.assertHTML('yes'); } + @test + 'explicit scope (shadowed)'() { + const compiled = template('{{if (lt a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ lt: () => false, a: 1, b: 2 }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + @test 'implicit scope (eval)'() { let a = 1; @@ -39,14 +49,14 @@ class KeywordLtRuntime extends RenderTest { @test 'no eval and no scope'() { class Foo extends GlimmerishComponent { - a = 1; - b = 2; static { template('{{if (lt this.a this.b) "yes" "no"}}', { strictMode: true, component: this, }); } + a = 1; + b = 2; } this.renderComponent(Foo); this.assertHTML('yes'); diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/lt-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/lt-test.ts index 122ee8a6592..49e14c56170 100644 --- a/packages/@glimmer-workspace/integration-tests/test/keywords/lt-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/lt-test.ts @@ -2,18 +2,64 @@ import { DEBUG } from '@glimmer/env'; import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests'; import { template } from '@ember/template-compiler'; -import { lt } from '@ember/helper'; class KeywordLt extends RenderTest { static suiteName = 'keyword helper: lt'; + @test + 'explicit scope'() { + let a = 1; + let b = 2; + + const compiled = template('{{lt a b}}', { + strictMode: true, + scope: () => ({ a, b }), + }); + + this.renderComponent(compiled); + this.assertHTML('true'); + } + + @test + 'explicit scope (shadowed)'() { + let a = 1; + let b = 2; + let lt = () => 'surprise'; + const compiled = template('{{lt a b}}', { + strictMode: true, + scope: () => ({ lt, a, b }), + }); + + this.renderComponent(compiled); + this.assertHTML('surprise'); + } + + @test + 'implicit scope (eval)'() { + let a = 1; + let b = 2; + + hide(a); + hide(b); + + const compiled = template('{{if (lt a b) "yes" "no"}}', { + strictMode: true, + eval() { + return eval(arguments[0]); + }, + }); + + this.renderComponent(compiled); + this.assertHTML('yes'); + } + @test 'returns true when first arg is less'() { let a = 1; let b = 2; const compiled = template('{{if (lt a b) "yes" "no"}}', { strictMode: true, - scope: () => ({ lt, a, b }), + scope: () => ({ a, b }), }); this.renderComponent(compiled); this.assertHTML('yes'); @@ -25,7 +71,7 @@ class KeywordLt extends RenderTest { let b = 2; const compiled = template('{{if (lt a b) "yes" "no"}}', { strictMode: true, - scope: () => ({ lt, a, b }), + scope: () => ({ a, b }), }); this.renderComponent(compiled); this.assertHTML('no'); @@ -37,7 +83,7 @@ class KeywordLt extends RenderTest { let b = 2; const compiled = template('{{if (lt a b) "yes" "no"}}', { strictMode: true, - scope: () => ({ lt, a, b }), + scope: () => ({ a, b }), }); this.renderComponent(compiled); this.assertHTML('no'); @@ -48,7 +94,7 @@ class KeywordLt extends RenderTest { let a = 1; const compiled = template('{{lt a}}', { strictMode: true, - scope: () => ({ lt, a }), + scope: () => ({ a }), }); assert.throws(() => { @@ -58,3 +104,7 @@ class KeywordLt extends RenderTest { } jitSuite(KeywordLt); + +const hide = (variable: unknown) => { + new Function(`return (${JSON.stringify(variable)});`); +}; diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/lte-runtime-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/lte-runtime-test.ts index a5007816398..98145ec893f 100644 --- a/packages/@glimmer-workspace/integration-tests/test/keywords/lte-runtime-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/lte-runtime-test.ts @@ -20,17 +20,43 @@ class KeywordLteRuntime extends RenderTest { this.assertHTML('yes'); } + @test + 'explicit scope (shadowed)'() { + const compiled = template('{{if (lte a b) "yes" "no"}}', { + strictMode: true, + scope: () => ({ lte: () => false, a: 2, b: 2 }), + }); + this.renderComponent(compiled); + this.assertHTML('no'); + } + + @test + 'implicit scope (eval)'() { + let a = 2; + let b = 2; + hide(a); + hide(b); + const compiled = template('{{if (lte a b) "yes" "no"}}', { + strictMode: true, + eval() { + return eval(arguments[0]); + }, + }); + this.renderComponent(compiled); + this.assertHTML('yes'); + } + @test 'no eval and no scope'() { class Foo extends GlimmerishComponent { - a = 2; - b = 2; static { template('{{if (lte this.a this.b) "yes" "no"}}', { strictMode: true, component: this, }); } + a = 2; + b = 2; } this.renderComponent(Foo); this.assertHTML('yes'); @@ -38,3 +64,7 @@ class KeywordLteRuntime extends RenderTest { } jitSuite(KeywordLteRuntime); + +const hide = (variable: unknown) => { + new Function(`return (${JSON.stringify(variable)});`); +}; diff --git a/packages/@glimmer-workspace/integration-tests/test/keywords/lte-test.ts b/packages/@glimmer-workspace/integration-tests/test/keywords/lte-test.ts index fd985049ff3..7cd01eeaf4b 100644 --- a/packages/@glimmer-workspace/integration-tests/test/keywords/lte-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/keywords/lte-test.ts @@ -2,18 +2,64 @@ import { DEBUG } from '@glimmer/env'; import { jitSuite, RenderTest, test } from '@glimmer-workspace/integration-tests'; import { template } from '@ember/template-compiler'; -import { lte } from '@ember/helper'; class KeywordLte extends RenderTest { static suiteName = 'keyword helper: lte'; + @test + 'explicit scope'() { + let a = 2; + let b = 2; + + const compiled = template('{{lte a b}}', { + strictMode: true, + scope: () => ({ a, b }), + }); + + this.renderComponent(compiled); + this.assertHTML('true'); + } + + @test + 'explicit scope (shadowed)'() { + let a = 1; + let b = 2; + let lte = () => 'surprise'; + const compiled = template('{{lte a b}}', { + strictMode: true, + scope: () => ({ lte, a, b }), + }); + + this.renderComponent(compiled); + this.assertHTML('surprise'); + } + + @test + 'implicit scope (eval)'() { + let a = 2; + let b = 2; + + hide(a); + hide(b); + + const compiled = template('{{if (lte a b) "yes" "no"}}', { + strictMode: true, + eval() { + return eval(arguments[0]); + }, + }); + + this.renderComponent(compiled); + this.assertHTML('yes'); + } + @test 'returns true when first arg is less'() { let a = 1; let b = 2; const compiled = template('{{if (lte a b) "yes" "no"}}', { strictMode: true, - scope: () => ({ lte, a, b }), + scope: () => ({ a, b }), }); this.renderComponent(compiled); this.assertHTML('yes'); @@ -25,7 +71,7 @@ class KeywordLte extends RenderTest { let b = 2; const compiled = template('{{if (lte a b) "yes" "no"}}', { strictMode: true, - scope: () => ({ lte, a, b }), + scope: () => ({ a, b }), }); this.renderComponent(compiled); this.assertHTML('yes'); @@ -37,7 +83,7 @@ class KeywordLte extends RenderTest { let b = 2; const compiled = template('{{if (lte a b) "yes" "no"}}', { strictMode: true, - scope: () => ({ lte, a, b }), + scope: () => ({ a, b }), }); this.renderComponent(compiled); this.assertHTML('no'); @@ -48,7 +94,7 @@ class KeywordLte extends RenderTest { let a = 1; const compiled = template('{{lte a}}', { strictMode: true, - scope: () => ({ lte, a }), + scope: () => ({ a }), }); assert.throws(() => { @@ -58,3 +104,7 @@ class KeywordLte extends RenderTest { } jitSuite(KeywordLte); + +const hide = (variable: unknown) => { + new Function(`return (${JSON.stringify(variable)});`); +}; diff --git a/packages/@glimmer/runtime/index.ts b/packages/@glimmer/runtime/index.ts index a46d04a7266..2012ced0f31 100644 --- a/packages/@glimmer/runtime/index.ts +++ b/packages/@glimmer/runtime/index.ts @@ -39,9 +39,9 @@ export { get } from './lib/helpers/get'; export { gt } from './lib/helpers/gt'; export { gte } from './lib/helpers/gte'; export { hash } from './lib/helpers/hash'; +export { invokeHelper } from './lib/helpers/invoke'; export { lt } from './lib/helpers/lt'; export { lte } from './lib/helpers/lte'; -export { invokeHelper } from './lib/helpers/invoke'; export { neq } from './lib/helpers/neq'; export { on } from './lib/modifiers/on'; export { renderComponent, renderMain, renderSync } from './lib/render'; diff --git a/smoke-tests/scenarios/basic-test.ts b/smoke-tests/scenarios/basic-test.ts index 3a766b125c7..587b3bbcaf3 100644 --- a/smoke-tests/scenarios/basic-test.ts +++ b/smoke-tests/scenarios/basic-test.ts @@ -442,35 +442,54 @@ function basicTest(scenarios: Scenarios, appName: string) { }); }); `, - 'comparison-helpers-as-keyword-test.gjs': ` + 'lt-lte-gt-gte-as-keyword-test.gjs': ` import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render } from '@ember/test-helpers'; - import Component from '@glimmer/component'; + module('{{lt}} / {{lte}} / {{gt}} / {{gte}} as keywords', function(hooks) { + setupRenderingTest(hooks); - class LtDemo extends Component { - - } + test('it works', async function(assert) { + let a = 1; + let b = 2; - module('comparison helpers as keywords', function(hooks) { - setupRenderingTest(hooks); + await render( + + ); + + assert.dom('[data-lt]').hasText('true'); + assert.dom('[data-lte]').hasText('true'); + assert.dom('[data-gt]').hasText('true'); + assert.dom('[data-gte]').hasText('true'); + }); + + test('can be shadowed', async function (assert) { + let a = 1; + let b = 2; + let lt = () => 'surprise:lt'; + let lte = () => 'surprise:lte'; + let gt = () => 'surprise:gt'; + let gte = () => 'surprise:gte'; + + await render( + + ); - test('lt, lte, gt, gte work without imports', async function(assert) { - await render(LtDemo); - assert.dom('[data-test="lt"]').hasText('yes'); - assert.dom('[data-test="lte-equal"]').hasText('yes'); - assert.dom('[data-test="gt"]').hasText('yes'); - assert.dom('[data-test="gte-equal"]').hasText('yes'); - assert.dom('[data-test="lt-false"]').hasText('no'); - assert.dom('[data-test="gt-false"]').hasText('no'); + assert.dom('[data-lt]').hasText('surprise:lt'); + assert.dom('[data-lte]').hasText('surprise:lte'); + assert.dom('[data-gt]').hasText('surprise:gt'); + assert.dom('[data-gte]').hasText('surprise:gte'); }); }); `, From c7c70b2e12e6d2f3dfafbe9ce71207336a5d0918 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:53:37 -0400 Subject: [PATCH 3/3] use more explicit argument signature --- packages/@glimmer/runtime/lib/helpers/gt.ts | 15 ++++++++++----- packages/@glimmer/runtime/lib/helpers/gte.ts | 15 ++++++++++----- packages/@glimmer/runtime/lib/helpers/lt.ts | 15 ++++++++++----- packages/@glimmer/runtime/lib/helpers/lte.ts | 15 ++++++++++----- 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/packages/@glimmer/runtime/lib/helpers/gt.ts b/packages/@glimmer/runtime/lib/helpers/gt.ts index 38101c1b566..cca687dceb6 100644 --- a/packages/@glimmer/runtime/lib/helpers/gt.ts +++ b/packages/@glimmer/runtime/lib/helpers/gt.ts @@ -1,9 +1,14 @@ import { DEBUG } from '@glimmer/env'; -export const gt = (...args: unknown[]) => { - if (DEBUG && args.length !== 2) { - throw new Error(`\`gt\` expects exactly two arguments, but received ${args.length}.`); +/** + * Performs a greater than comparison. + * + * left > right + */ +export function gt(left: unknown, right: unknown) { + if (DEBUG && arguments.length !== 2) { + throw new Error(`\`gt\` expects exactly two arguments, but received ${arguments.length}.`); } - return (args[0] as number) > (args[1] as number); -}; + return (left as number) > (right as number); +} diff --git a/packages/@glimmer/runtime/lib/helpers/gte.ts b/packages/@glimmer/runtime/lib/helpers/gte.ts index 2b69a96e28e..78621a3383d 100644 --- a/packages/@glimmer/runtime/lib/helpers/gte.ts +++ b/packages/@glimmer/runtime/lib/helpers/gte.ts @@ -1,9 +1,14 @@ import { DEBUG } from '@glimmer/env'; -export const gte = (...args: unknown[]) => { - if (DEBUG && args.length !== 2) { - throw new Error(`\`gte\` expects exactly two arguments, but received ${args.length}.`); +/** + * Performs a greater than or equal comparison. + * + * left >= right + */ +export function gte(left: unknown, right: unknown) { + if (DEBUG && arguments.length !== 2) { + throw new Error(`\`gte\` expects exactly two arguments, but received ${arguments.length}.`); } - return (args[0] as number) >= (args[1] as number); -}; + return (left as number) >= (right as number); +} diff --git a/packages/@glimmer/runtime/lib/helpers/lt.ts b/packages/@glimmer/runtime/lib/helpers/lt.ts index 949b3c96ad8..95e7e39035b 100644 --- a/packages/@glimmer/runtime/lib/helpers/lt.ts +++ b/packages/@glimmer/runtime/lib/helpers/lt.ts @@ -1,9 +1,14 @@ import { DEBUG } from '@glimmer/env'; -export const lt = (...args: unknown[]) => { - if (DEBUG && args.length !== 2) { - throw new Error(`\`lt\` expects exactly two arguments, but received ${args.length}.`); +/** + * Performs a less than comparison. + * + * left < right + */ +export function lt(left: unknown, right: unknown) { + if (DEBUG && arguments.length !== 2) { + throw new Error(`\`lt\` expects exactly two arguments, but received ${arguments.length}.`); } - return (args[0] as number) < (args[1] as number); -}; + return (left as number) < (right as number); +} diff --git a/packages/@glimmer/runtime/lib/helpers/lte.ts b/packages/@glimmer/runtime/lib/helpers/lte.ts index d67a5441318..5393cf76139 100644 --- a/packages/@glimmer/runtime/lib/helpers/lte.ts +++ b/packages/@glimmer/runtime/lib/helpers/lte.ts @@ -1,9 +1,14 @@ import { DEBUG } from '@glimmer/env'; -export const lte = (...args: unknown[]) => { - if (DEBUG && args.length !== 2) { - throw new Error(`\`lte\` expects exactly two arguments, but received ${args.length}.`); +/** + * Performs a less than or equal comparison. + * + * left <= right + */ +export function lte(left: unknown, right: unknown) { + if (DEBUG && arguments.length !== 2) { + throw new Error(`\`lte\` expects exactly two arguments, but received ${arguments.length}.`); } - return (args[0] as number) <= (args[1] as number); -}; + return (left as number) <= (right as number); +}