diff --git a/README.md b/README.md
index c6191f2562..d9944fa5af 100644
--- a/README.md
+++ b/README.md
@@ -261,6 +261,7 @@ rules in templates can be disabled with eslint directives with mustache or html
| [template-no-page-title-component](docs/rules/template-no-page-title-component.md) | disallow usage of ember-page-title component | | | |
| [template-no-positional-data-test-selectors](docs/rules/template-no-positional-data-test-selectors.md) | disallow positional data-test-* params in curly invocations | | | |
| [template-no-potential-path-strings](docs/rules/template-no-potential-path-strings.md) | disallow potential path strings in attribute values | | | |
+| [template-no-restricted-invocations](docs/rules/template-no-restricted-invocations.md) | disallow certain components, helpers or modifiers from being used | | | |
| [template-no-splattributes-with-class](docs/rules/template-no-splattributes-with-class.md) | disallow splattributes with class attribute | | | |
| [template-no-this-in-template-only-components](docs/rules/template-no-this-in-template-only-components.md) | disallow this in template-only components (gjs/gts) | | 🔧 | |
| [template-no-trailing-spaces](docs/rules/template-no-trailing-spaces.md) | disallow trailing whitespace at the end of lines in templates | | 🔧 | |
diff --git a/docs/rules/template-no-restricted-invocations.md b/docs/rules/template-no-restricted-invocations.md
new file mode 100644
index 0000000000..c7c1a05b2f
--- /dev/null
+++ b/docs/rules/template-no-restricted-invocations.md
@@ -0,0 +1,49 @@
+# ember/template-no-restricted-invocations
+
+
+
+Disallow certain components, helpers or modifiers from being used.
+
+Use cases include:
+
+- You bring in some addon like ember-composable-helpers, but your team deems one or many of the helpers not suitable and wants to guard against their usage
+- You want to discourage use of a deprecated component
+
+## Examples
+
+Given a config of:
+
+```json
+["foo-bar"]
+```
+
+This rule **forbids** the following:
+
+```hbs
+{{foo-bar}}
+```
+
+```hbs
+{{#foo-bar}}{{/foo-bar}}
+```
+
+```hbs
+
+```
+
+## Configuration
+
+One of these:
+
+- string[] - helpers or components to disallow (using kebab-case names like `nested-scope/component-name`)
+- object[] - with the following keys:
+ - `names` - string[] - helpers or components to disallow (using kebab-case names like `nested-scope/component-name`)
+ - `message` - string - custom error message to report for violations (typically a deprecation notice / explanation of why not to use it and a recommended replacement)
+
+## Related Rules
+
+- [ember/no-restricted-service-injections](https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/no-restricted-service-injections.md)
+
+## References
+
+- [ember-cli-deprecation-workflow](https://github.com/mixonic/ember-cli-deprecation-workflow)
diff --git a/lib/rules/template-no-restricted-invocations.js b/lib/rules/template-no-restricted-invocations.js
new file mode 100644
index 0000000000..1ddc1c00c3
--- /dev/null
+++ b/lib/rules/template-no-restricted-invocations.js
@@ -0,0 +1,400 @@
+/* eslint-disable unicorn/consistent-function-scoping, unicorn/prefer-switch, curly */
+const COMPONENT_HELPER_NAME = 'component';
+
+function dasherize(str) {
+ return str
+ .split('::')
+ .map((segment) =>
+ segment
+ .replaceAll(/([A-Z])/g, '-$1')
+ .toLowerCase()
+ .replace(/^-/, '')
+ )
+ .join('/');
+}
+
+function parseConfig(config) {
+ // If config is not provided, disable the rule
+ if (config === false || config === undefined) {
+ return false;
+ }
+
+ // If it's true, use empty array
+ if (config === true) {
+ return [];
+ }
+
+ // If it's an array, validate it
+ if (Array.isArray(config)) {
+ return config;
+ }
+
+ return false;
+}
+
+/** @type {import('eslint').Rule.RuleModule} */
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'disallow certain components, helpers or modifiers from being used',
+ category: 'Best Practices',
+ recommended: false,
+ url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-restricted-invocations.md',
+ templateMode: 'both',
+ },
+ fixable: null,
+ schema: [
+ {
+ oneOf: [
+ {
+ type: 'array',
+ items: {
+ oneOf: [
+ {
+ type: 'string',
+ },
+ {
+ type: 'object',
+ properties: {
+ names: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
+ message: {
+ type: 'string',
+ },
+ },
+ required: ['names', 'message'],
+ additionalProperties: false,
+ },
+ ],
+ },
+ },
+ ],
+ },
+ ],
+ messages: {},
+ originallyFrom: {
+ name: 'ember-template-lint',
+ rule: 'lib/rules/no-restricted-invocations.js',
+ docs: 'docs/rule/no-restricted-invocations.md',
+ tests: 'test/unit/rules/no-restricted-invocations-test.js',
+ },
+ },
+
+ create(context) {
+ const config = parseConfig(context.options[0]);
+
+ if (config === false) {
+ return {};
+ }
+
+ const sourceCode = context.sourceCode;
+
+ // Track block params in a scope stack so yielded names are not flagged.
+ const blockParamScopes = [];
+
+ function pushBlockParams(params) {
+ blockParamScopes.push(new Set(params || []));
+ }
+
+ function popBlockParams() {
+ blockParamScopes.pop();
+ }
+
+ function isBlockParam(name) {
+ for (const scope of blockParamScopes) {
+ if (scope.has(name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * In gjs/gts, check whether a name resolves to a JS-scope variable
+ * (import, const, let, function param, etc.). If it does, it's a local
+ * binding and should be exempt from restriction checks — same as block params.
+ */
+ function isJsScopeVariable(node) {
+ if (!sourceCode) return false;
+
+ try {
+ if (node.type === 'GlimmerElementNode') {
+ // Element nodes use parts[0] for scope lookup, and need parent scope
+ if (!node.parts || !node.parts[0]) return false;
+ const scope = sourceCode.getScope(node.parent);
+ const ref = scope.references.find((r) => r.identifier === node.parts[0]);
+ // Only exempt if the reference actually resolves to a JS variable definition
+ return (
+ ref !== null && ref !== undefined && ref.resolved !== null && ref.resolved !== undefined
+ );
+ }
+
+ // For mustache/block/sub/modifier statements, check the path's head
+ if (node.path && node.path.head) {
+ const scope = sourceCode.getScope(node);
+ const ref = scope.references.find((r) => r.identifier === node.path.head);
+ return (
+ ref !== null && ref !== undefined && ref.resolved !== null && ref.resolved !== undefined
+ );
+ }
+ } catch {
+ // sourceCode.getScope may not be available in .hbs-only mode; ignore.
+ }
+
+ return false;
+ }
+
+ function isRestricted(name) {
+ for (const item of config) {
+ if (typeof item === 'string') {
+ if (item === name) {
+ return { restricted: true, message: null };
+ }
+ } else if (item.names && item.names.includes(name)) {
+ return { restricted: true, message: item.message };
+ }
+ }
+ return { restricted: false };
+ }
+
+ function getComponentOrHelperName(node) {
+ if (node.type === 'GlimmerElementNode') {
+ // Convert angle-bracket names to kebab-case
+ return dasherize(node.tag);
+ }
+
+ if (node.type === 'GlimmerMustacheStatement' || node.type === 'GlimmerBlockStatement') {
+ // Check if it's the component helper
+ if (node.path.original === COMPONENT_HELPER_NAME && node.params && node.params[0]) {
+ // component helper with first param
+ if (node.params[0].type === 'GlimmerStringLiteral') {
+ return node.params[0].value;
+ }
+ }
+ return node.path.original;
+ }
+
+ if (node.type === 'GlimmerModifierStatement') {
+ return node.path.original;
+ }
+
+ if (node.type === 'GlimmerSubExpression') {
+ if (node.path.original === COMPONENT_HELPER_NAME && node.params && node.params[0]) {
+ if (node.params[0].type === 'GlimmerStringLiteral') {
+ return node.params[0].value;
+ }
+ }
+ return node.path.original;
+ }
+
+ return null;
+ }
+
+ function getNodeName(node) {
+ switch (node.type) {
+ case 'GlimmerElementNode': {
+ return `<${node.tag} />`;
+ }
+ case 'GlimmerMustacheStatement': {
+ if (
+ node.path.original === COMPONENT_HELPER_NAME &&
+ node.params?.[0]?.type === 'GlimmerStringLiteral'
+ ) {
+ return `{{component "${node.params[0].value}"}}`;
+ }
+ return `{{${node.path.original}}}`;
+ }
+ case 'GlimmerBlockStatement': {
+ if (
+ node.path.original === COMPONENT_HELPER_NAME &&
+ node.params?.[0]?.type === 'GlimmerStringLiteral'
+ ) {
+ return `{{#component "${node.params[0].value}"}}`;
+ }
+ return `{{#${node.path.original}}}`;
+ }
+ case 'GlimmerModifierStatement': {
+ return `{{${node.path.original}}}`;
+ }
+ case 'GlimmerSubExpression': {
+ if (
+ node.path.original === COMPONENT_HELPER_NAME &&
+ node.params?.[0]?.type === 'GlimmerStringLiteral'
+ ) {
+ return `(component "${node.params[0].value}")`;
+ }
+ return `(${node.path.original})`;
+ }
+ // No default
+ }
+ return '';
+ }
+
+ function checkElementModifiers(node) {
+ if (!node.modifiers) {
+ return;
+ }
+ for (const modifier of node.modifiers) {
+ const modName =
+ modifier.path && modifier.path.type === 'GlimmerPathExpression' && modifier.path.original;
+ if (!modName) continue;
+ if (isBlockParam(modName)) continue;
+ if (isJsScopeVariable(modifier)) continue;
+
+ const modResult = isRestricted(modName);
+ if (modResult.restricted) {
+ context.report({
+ node: modifier,
+ message:
+ modResult.message ||
+ `Cannot use disallowed helper, component or modifier '{{${modName}}}'`,
+ });
+ }
+ }
+ }
+
+ function trackBlockParams(node) {
+ if (node.blockParams && node.blockParams.length > 0) {
+ pushBlockParams(node.blockParams);
+ }
+ }
+
+ return {
+ GlimmerElementNode(node) {
+ // For element nodes, check the raw tag against block params before dasherizing.
+ if (node.tag && isBlockParam(node.tag)) {
+ trackBlockParams(node);
+ return;
+ }
+
+ // In gjs/gts, skip if the tag resolves to a JS-scope variable (import, const, etc.)
+ if (isJsScopeVariable(node)) {
+ trackBlockParams(node);
+ return;
+ }
+
+ const name = getComponentOrHelperName(node);
+ if (name && !isBlockParam(name)) {
+ const result = isRestricted(name);
+ if (result.restricted) {
+ context.report({
+ node,
+ message:
+ result.message ||
+ `Cannot use disallowed helper, component or modifier '${getNodeName(node)}'`,
+ });
+ }
+ }
+
+ trackBlockParams(node);
+ checkElementModifiers(node);
+ },
+
+ 'GlimmerElementNode:exit'(node) {
+ if (node.blockParams && node.blockParams.length > 0) {
+ popBlockParams();
+ }
+ },
+
+ GlimmerMustacheStatement(node) {
+ const name = getComponentOrHelperName(node);
+ if (!name) {
+ return;
+ }
+ if (isBlockParam(name)) {
+ return;
+ }
+ if (isJsScopeVariable(node)) {
+ return;
+ }
+
+ const result = isRestricted(name);
+ if (result.restricted) {
+ context.report({
+ node,
+ message:
+ result.message ||
+ `Cannot use disallowed helper, component or modifier '${getNodeName(node)}'`,
+ });
+ }
+ },
+
+ GlimmerBlockStatement(node) {
+ const name = getComponentOrHelperName(node);
+ if (name && !isBlockParam(name) && !isJsScopeVariable(node)) {
+ const result = isRestricted(name);
+ if (result.restricted) {
+ context.report({
+ node,
+ message:
+ result.message ||
+ `Cannot use disallowed helper, component or modifier '${getNodeName(node)}'`,
+ });
+ }
+ }
+
+ // Track block params (e.g. {{#each items as |item|}}).
+ if (node.program && node.program.blockParams) {
+ pushBlockParams(node.program.blockParams);
+ }
+ },
+
+ 'GlimmerBlockStatement:exit'(node) {
+ if (node.program && node.program.blockParams) {
+ popBlockParams();
+ }
+ },
+
+ GlimmerModifierStatement(node) {
+ const name = getComponentOrHelperName(node);
+ if (!name) {
+ return;
+ }
+ if (isBlockParam(name)) {
+ return;
+ }
+ if (isJsScopeVariable(node)) {
+ return;
+ }
+
+ const result = isRestricted(name);
+ if (result.restricted) {
+ context.report({
+ node,
+ message:
+ result.message || `Cannot use disallowed helper, component or modifier '{{${name}}}'`,
+ });
+ }
+ },
+
+ GlimmerSubExpression(node) {
+ const name = getComponentOrHelperName(node);
+ if (!name) {
+ return;
+ }
+ if (isBlockParam(name)) {
+ return;
+ }
+ if (isJsScopeVariable(node)) {
+ return;
+ }
+
+ const result = isRestricted(name);
+ if (result.restricted) {
+ context.report({
+ node,
+ message:
+ result.message ||
+ `Cannot use disallowed helper, component or modifier '${getNodeName(node)}'`,
+ });
+ }
+ },
+ };
+ },
+};
+/* eslint-enable unicorn/consistent-function-scoping, unicorn/prefer-switch, curly */
diff --git a/tests/lib/rules/template-no-restricted-invocations.js b/tests/lib/rules/template-no-restricted-invocations.js
new file mode 100644
index 0000000000..cd5eaa0573
--- /dev/null
+++ b/tests/lib/rules/template-no-restricted-invocations.js
@@ -0,0 +1,615 @@
+const rule = require('../../../lib/rules/template-no-restricted-invocations');
+const RuleTester = require('eslint').RuleTester;
+
+const ruleTester = new RuleTester({
+ parser: require.resolve('ember-eslint-parser'),
+ parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
+});
+
+ruleTester.run('template-no-restricted-invocations', rule, {
+ valid: [
+ {
+ code: '{{baz}}',
+ output: null,
+ options: [['foo', 'bar']],
+ },
+ {
+ code: '{{baz foo=bar}}',
+ output: null,
+ options: [['foo', 'bar']],
+ },
+ {
+ code: '{{#baz}}{{/baz}}',
+ output: null,
+ options: [['foo', 'bar']],
+ },
+ {
+ code: '{{component "baz"}}',
+ output: null,
+ options: [['foo', 'bar']],
+ },
+ {
+ code: '{{other-component}}',
+ output: null,
+ options: [['foo', 'bar']],
+ },
+ {
+ code: '',
+ output: null,
+ options: [['foo', 'bar']],
+ },
+ {
+ code: '',
+ output: null,
+ options: [['foo', 'bar']],
+ },
+
+ '{{baz foo=(baz)}}',
+ '{{#baz foo=bar}}{{/baz}}',
+ '{{#baz foo=(baz)}}{{/baz}}',
+ '{{component}}',
+ '{{component "baz" foo=bar}}',
+ '{{component "baz" foo=(baz)}}',
+ '{{#component "baz"}}{{/component}}',
+ '{{#component "baz" foo=bar}}{{/component}}',
+ '{{#component "baz" foo=(baz)}}{{/component}}',
+ '{{yield (component "baz")}}',
+ '{{yield (component "baz" foo=bar)}}',
+ '{{yield (component "baz" foo=(baz))}}',
+ '{{yield (baz (baz (baz) bar))}}',
+ '{{yield (baz (baz (baz) (baz)))}}',
+ '{{yield (baz (baz (baz) foo=(baz)))}}',
+ '{{#baz as |foo|}}{{foo}}{{/baz}}',
+ '{{#with (component "blah") as |Foo|}} {{/with}}',
+ '{{other/foo-bar}}',
+ '{{nested-scope/other}}',
+ '',
+ '',
+ '',
+
+ // JS-scope variables (imports, const, let) should be exempt — same as block params.
+ {
+ code: `
+ import foo from './foo';
+ {{foo}}
+ `,
+ options: [['foo', 'bar']],
+ },
+ {
+ code: `
+ import Foo from './foo';
+
+ `,
+ options: [['foo', 'bar']],
+ },
+ {
+ code: `
+ const foo = () => {};
+ {{foo}}
+ `,
+ options: [['foo', 'bar']],
+ },
+ {
+ code: `
+ import foo from './foo';
+ {{foo "hello"}}
+ `,
+ options: [['foo', 'bar']],
+ },
+ {
+ code: `
+ import bar from './bar';
+ {{bar}}
+ `,
+ options: [['foo', 'bar']],
+ },
+ {
+ code: `
+ import foo from './foo';
+ import bar from './bar';
+ {{foo}}{{bar}}
+ `,
+ options: [['foo', 'bar']],
+ },
+ ],
+ invalid: [
+ {
+ code: '{{foo}}',
+ output: null,
+ options: [['foo', 'bar']],
+ errors: [
+ {
+ message: "Cannot use disallowed helper, component or modifier '{{foo}}'",
+ },
+ ],
+ },
+ {
+ code: '{{#foo}}{{/foo}}',
+ output: null,
+ options: [['foo', 'bar']],
+ errors: [
+ {
+ message: "Cannot use disallowed helper, component or modifier '{{#foo}}'",
+ },
+ ],
+ },
+ {
+ code: '',
+ output: null,
+ options: [['foo', 'bar']],
+ errors: [
+ {
+ message: "Cannot use disallowed helper, component or modifier ''",
+ },
+ ],
+ },
+ {
+ code: '{{bar foo=bar}}',
+ output: null,
+ options: [['foo', 'bar']],
+ errors: [
+ {
+ message: "Cannot use disallowed helper, component or modifier '{{bar}}'",
+ },
+ ],
+ },
+ {
+ code: '{{deprecated-component}}',
+ output: null,
+ options: [
+ [
+ 'foo',
+ {
+ names: ['deprecated-component'],
+ message: 'Use new-component instead',
+ },
+ ],
+ ],
+ errors: [
+ {
+ message: 'Use new-component instead',
+ },
+ ],
+ },
+
+ {
+ code: '',
+ output: null,
+ options: [['foo', 'nested-scope/foo-bar']],
+ errors: [{ message: "Cannot use disallowed helper, component or modifier '{{foo}}'" }],
+ },
+ {
+ code: '{{foo foo=bar}}',
+ output: null,
+ options: [['foo', 'nested-scope/foo-bar']],
+ errors: [{ message: "Cannot use disallowed helper, component or modifier '{{foo}}'" }],
+ },
+ {
+ code: '{{foo foo=(baz)}}',
+ output: null,
+ options: [['foo', 'nested-scope/foo-bar']],
+ errors: [{ message: "Cannot use disallowed helper, component or modifier '{{foo}}'" }],
+ },
+ {
+ code: '{{#foo foo=bar}}{{/foo}}',
+ output: null,
+ options: [['foo', 'nested-scope/foo-bar']],
+ errors: [{ message: "Cannot use disallowed helper, component or modifier '{{#foo}}'" }],
+ },
+ {
+ code: '{{#foo foo=(baz)}}{{/foo}}',
+ output: null,
+ options: [['foo', 'nested-scope/foo-bar']],
+ errors: [{ message: "Cannot use disallowed helper, component or modifier '{{#foo}}'" }],
+ },
+ {
+ code: '{{component "foo"}}',
+ output: null,
+ options: [['foo', 'nested-scope/foo-bar']],
+ errors: [
+ { message: 'Cannot use disallowed helper, component or modifier \'{{component "foo"}}\'' },
+ ],
+ },
+ {
+ code: '{{component "foo" foo=bar}}',
+ output: null,
+ options: [['foo', 'nested-scope/foo-bar']],
+ errors: [
+ { message: 'Cannot use disallowed helper, component or modifier \'{{component "foo"}}\'' },
+ ],
+ },
+ {
+ code: '{{component "foo" foo=(baz)}}',
+ output: null,
+ options: [['foo', 'nested-scope/foo-bar']],
+ errors: [
+ { message: 'Cannot use disallowed helper, component or modifier \'{{component "foo"}}\'' },
+ ],
+ },
+ {
+ code: '{{#component "foo"}}{{/component}}',
+ output: null,
+ options: [['foo', 'nested-scope/foo-bar']],
+ errors: [
+ { message: 'Cannot use disallowed helper, component or modifier \'{{#component "foo"}}\'' },
+ ],
+ },
+ {
+ code: '{{#component "foo" foo=bar}}{{/component}}',
+ output: null,
+ options: [['foo', 'nested-scope/foo-bar']],
+ errors: [
+ { message: 'Cannot use disallowed helper, component or modifier \'{{#component "foo"}}\'' },
+ ],
+ },
+ {
+ code: '{{#component "foo" foo=(baz)}}{{/component}}',
+ output: null,
+ options: [['foo', 'nested-scope/foo-bar']],
+ errors: [
+ { message: 'Cannot use disallowed helper, component or modifier \'{{#component "foo"}}\'' },
+ ],
+ },
+ {
+ code: '{{yield (component "foo")}}',
+ output: null,
+ options: [['foo', 'nested-scope/foo-bar']],
+ errors: [
+ { message: 'Cannot use disallowed helper, component or modifier \'(component "foo")\'' },
+ ],
+ },
+ {
+ code: '{{yield (component "foo" foo=bar)}}',
+ output: null,
+ options: [['foo', 'nested-scope/foo-bar']],
+ errors: [
+ { message: 'Cannot use disallowed helper, component or modifier \'(component "foo")\'' },
+ ],
+ },
+ {
+ code: '{{yield (component "foo" foo=(baz))}}',
+ output: null,
+ options: [['foo', 'nested-scope/foo-bar']],
+ errors: [
+ { message: 'Cannot use disallowed helper, component or modifier \'(component "foo")\'' },
+ ],
+ },
+ {
+ code: '{{yield (baz (foo (baz) bar))}}',
+ output: null,
+ options: [['foo', 'nested-scope/foo-bar']],
+ errors: [{ message: "Cannot use disallowed helper, component or modifier '(foo)'" }],
+ },
+ {
+ code: '{{yield (baz (baz (baz) (foo)))}}',
+ output: null,
+ options: [['foo', 'nested-scope/foo-bar']],
+ errors: [{ message: "Cannot use disallowed helper, component or modifier '(foo)'" }],
+ },
+ {
+ code: '{{yield (baz (baz (baz) foo=(foo)))}}',
+ output: null,
+ options: [['foo', 'nested-scope/foo-bar']],
+ errors: [{ message: "Cannot use disallowed helper, component or modifier '(foo)'" }],
+ },
+ {
+ code: '{{#baz as |bar|}}{{bar foo=(foo)}}{{/baz}}',
+ output: null,
+ options: [['foo', 'nested-scope/foo-bar']],
+ errors: [{ message: "Cannot use disallowed helper, component or modifier '(foo)'" }],
+ },
+ {
+ code: '{{nested-scope/foo-bar}}',
+ output: null,
+ options: [['foo', 'nested-scope/foo-bar']],
+ errors: [
+ {
+ message: "Cannot use disallowed helper, component or modifier '{{nested-scope/foo-bar}}'",
+ },
+ ],
+ },
+ {
+ code: '',
+ output: null,
+ options: [['foo', 'nested-scope/foo-bar']],
+ errors: [
+ {
+ message: "Cannot use disallowed helper, component or modifier ''",
+ },
+ ],
+ },
+ ],
+});
+
+const hbsRuleTester = new RuleTester({
+ parser: require.resolve('ember-eslint-parser/hbs'),
+ parserOptions: {
+ ecmaVersion: 2022,
+ sourceType: 'module',
+ },
+});
+
+hbsRuleTester.run('template-no-restricted-invocations', rule, {
+ valid: [
+ '{{baz}}',
+ '{{baz foo=bar}}',
+ '{{baz foo=(baz)}}',
+ '{{#baz}}{{/baz}}',
+ '{{#baz foo=bar}}{{/baz}}',
+ '{{#baz foo=(baz)}}{{/baz}}',
+ '{{component}}',
+ '{{component "baz"}}',
+ '{{component "baz" foo=bar}}',
+ '{{component "baz" foo=(baz)}}',
+ '{{#component "baz"}}{{/component}}',
+ '{{#component "baz" foo=bar}}{{/component}}',
+ '{{#component "baz" foo=(baz)}}{{/component}}',
+ '{{yield (component "baz")}}',
+ '{{yield (component "baz" foo=bar)}}',
+ '{{yield (component "baz" foo=(baz))}}',
+ '{{yield (baz (baz (baz) bar))}}',
+ '{{yield (baz (baz (baz) (baz)))}}',
+ '{{yield (baz (baz (baz) foo=(baz)))}}',
+ '{{#baz as |foo|}}{{foo}}{{/baz}}',
+ '{{#with (component "blah") as |Foo|}} {{/with}}',
+ '{{other/foo-bar}}',
+ '{{nested-scope/other}}',
+ '',
+ '',
+ '',
+ ].map((code) => ({
+ code: typeof code === 'string' ? code : code.code,
+ options: [
+ [
+ 'foo',
+ 'bar',
+ 'nested-scope/foo-bar',
+ {
+ names: ['deprecated-component'],
+ message: 'This component is deprecated; use component ABC instead.',
+ },
+ ],
+ ],
+ })),
+ invalid: [
+ // Modifier on element.
+ {
+ code: '
',
+ output: null,
+ errors: [
+ {
+ message: "Cannot use disallowed helper, component or modifier '{{foo}}'",
+ },
+ ],
+ },
+ // Mustache invocations.
+ {
+ code: '{{foo}}',
+ output: null,
+ errors: [
+ {
+ message: "Cannot use disallowed helper, component or modifier '{{foo}}'",
+ },
+ ],
+ },
+ {
+ code: '{{foo foo=bar}}',
+ output: null,
+ errors: [
+ {
+ message: "Cannot use disallowed helper, component or modifier '{{foo}}'",
+ },
+ ],
+ },
+ {
+ code: '{{foo foo=(baz)}}',
+ output: null,
+ errors: [
+ {
+ message: "Cannot use disallowed helper, component or modifier '{{foo}}'",
+ },
+ ],
+ },
+ // Angle bracket invocation.
+ {
+ code: '',
+ output: null,
+ errors: [
+ {
+ message: "Cannot use disallowed helper, component or modifier ''",
+ },
+ ],
+ },
+ // Block invocations.
+ {
+ code: '{{#foo}}{{/foo}}',
+ output: null,
+ errors: [
+ {
+ message: "Cannot use disallowed helper, component or modifier '{{#foo}}'",
+ },
+ ],
+ },
+ {
+ code: '{{#foo foo=bar}}{{/foo}}',
+ output: null,
+ errors: [
+ {
+ message: "Cannot use disallowed helper, component or modifier '{{#foo}}'",
+ },
+ ],
+ },
+ {
+ code: '{{#foo foo=(baz)}}{{/foo}}',
+ output: null,
+ errors: [
+ {
+ message: "Cannot use disallowed helper, component or modifier '{{#foo}}'",
+ },
+ ],
+ },
+ // Component helper invocations.
+ {
+ code: '{{component "foo"}}',
+ output: null,
+ errors: [
+ {
+ message: 'Cannot use disallowed helper, component or modifier \'{{component "foo"}}\'',
+ },
+ ],
+ },
+ {
+ code: '{{component "foo" foo=bar}}',
+ output: null,
+ errors: [
+ {
+ message: 'Cannot use disallowed helper, component or modifier \'{{component "foo"}}\'',
+ },
+ ],
+ },
+ {
+ code: '{{component "foo" foo=(baz)}}',
+ output: null,
+ errors: [
+ {
+ message: 'Cannot use disallowed helper, component or modifier \'{{component "foo"}}\'',
+ },
+ ],
+ },
+ {
+ code: '{{#component "foo"}}{{/component}}',
+ output: null,
+ errors: [
+ {
+ message: 'Cannot use disallowed helper, component or modifier \'{{#component "foo"}}\'',
+ },
+ ],
+ },
+ {
+ code: '{{#component "foo" foo=bar}}{{/component}}',
+ output: null,
+ errors: [
+ {
+ message: 'Cannot use disallowed helper, component or modifier \'{{#component "foo"}}\'',
+ },
+ ],
+ },
+ {
+ code: '{{#component "foo" foo=(baz)}}{{/component}}',
+ output: null,
+ errors: [
+ {
+ message: 'Cannot use disallowed helper, component or modifier \'{{#component "foo"}}\'',
+ },
+ ],
+ },
+ // Subexpression with component helper.
+ {
+ code: '{{yield (component "foo")}}',
+ output: null,
+ errors: [
+ {
+ message: 'Cannot use disallowed helper, component or modifier \'(component "foo")\'',
+ },
+ ],
+ },
+ {
+ code: '{{yield (component "foo" foo=bar)}}',
+ output: null,
+ errors: [
+ {
+ message: 'Cannot use disallowed helper, component or modifier \'(component "foo")\'',
+ },
+ ],
+ },
+ {
+ code: '{{yield (component "foo" foo=(baz))}}',
+ output: null,
+ errors: [
+ {
+ message: 'Cannot use disallowed helper, component or modifier \'(component "foo")\'',
+ },
+ ],
+ },
+ // Nested subexpressions.
+ {
+ code: '{{yield (baz (foo (baz) bar))}}',
+ output: null,
+ errors: [
+ {
+ message: "Cannot use disallowed helper, component or modifier '(foo)'",
+ },
+ ],
+ },
+ {
+ code: '{{yield (baz (baz (baz) (foo)))}}',
+ output: null,
+ errors: [
+ {
+ message: "Cannot use disallowed helper, component or modifier '(foo)'",
+ },
+ ],
+ },
+ {
+ code: '{{yield (baz (baz (baz) foo=(foo)))}}',
+ output: null,
+ errors: [
+ {
+ message: "Cannot use disallowed helper, component or modifier '(foo)'",
+ },
+ ],
+ },
+ {
+ code: '{{#baz as |bar|}}{{bar foo=(foo)}}{{/baz}}',
+ output: null,
+ errors: [
+ {
+ message: "Cannot use disallowed helper, component or modifier '(foo)'",
+ },
+ ],
+ },
+ // Nested scope (slash path).
+ {
+ code: '{{nested-scope/foo-bar}}',
+ output: null,
+ errors: [
+ {
+ message: "Cannot use disallowed helper, component or modifier '{{nested-scope/foo-bar}}'",
+ },
+ ],
+ },
+ // Nested scope (angle bracket with ::).
+ {
+ code: '',
+ output: null,
+ errors: [
+ {
+ message: "Cannot use disallowed helper, component or modifier ''",
+ },
+ ],
+ },
+ // Custom message from config object.
+ {
+ code: '{{deprecated-component}}',
+ output: null,
+ errors: [
+ {
+ message: 'This component is deprecated; use component ABC instead.',
+ },
+ ],
+ },
+ ].map((test) => ({
+ ...test,
+ options: [
+ [
+ 'foo',
+ 'bar',
+ 'nested-scope/foo-bar',
+ {
+ names: ['deprecated-component'],
+ message: 'This component is deprecated; use component ABC instead.',
+ },
+ ],
+ ],
+ })),
+});