diff --git a/lib/rules/template-no-at-ember-render-modifiers.js b/lib/rules/template-no-at-ember-render-modifiers.js index 5bcd2570e1..fa93b2c776 100644 --- a/lib/rules/template-no-at-ember-render-modifiers.js +++ b/lib/rules/template-no-at-ember-render-modifiers.js @@ -1,3 +1,20 @@ +// Sub-path → canonical kebab-case modifier name (default import). +const RENDER_MODIFIER_IMPORT_PATHS = { + '@ember/render-modifiers/modifiers/did-insert': 'did-insert', + '@ember/render-modifiers/modifiers/did-update': 'did-update', + '@ember/render-modifiers/modifiers/will-destroy': 'will-destroy', +}; + +// Named exports of the root package `@ember/render-modifiers` → canonical kebab name. +const ROOT_NAMED_EXPORTS = { + didInsert: 'did-insert', + didUpdate: 'did-update', + willDestroy: 'will-destroy', +}; + +const KEBAB_NAMES = new Set(['did-insert', 'did-update', 'will-destroy']); +const ROOT_PACKAGE = '@ember/render-modifiers'; + /** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { @@ -23,24 +40,72 @@ module.exports = { }, create(context) { + const filename = context.filename; + const isStrictMode = filename.endsWith('.gjs') || filename.endsWith('.gts'); + + // local import name → canonical kebab name. Only populated in GJS/GTS. + const importedModifiers = new Map(); + + function isRenderModifier(name) { + if (isStrictMode) { + return importedModifiers.has(name); + } + // HBS: resolver-resolved by canonical kebab name + return KEBAB_NAMES.has(name); + } + + function canonicalName(name) { + return importedModifiers.get(name) || name; + } + return { + ImportDeclaration(node) { + if (!isStrictMode) { + return; + } + const source = node.source.value; + + if (source === ROOT_PACKAGE) { + // `import { didInsert, didUpdate as x } from '@ember/render-modifiers'` + for (const specifier of node.specifiers) { + if (specifier.type === 'ImportSpecifier') { + const exportedName = specifier.imported.name; + const canonical = ROOT_NAMED_EXPORTS[exportedName]; + if (canonical) { + importedModifiers.set(specifier.local.name, canonical); + } + } + } + return; + } + + // Sub-path: `import didInsert from '@ember/render-modifiers/modifiers/did-insert'` + const canonical = RENDER_MODIFIER_IMPORT_PATHS[source]; + if (!canonical) { + return; + } + for (const specifier of node.specifiers) { + if (specifier.type === 'ImportDefaultSpecifier') { + importedModifiers.set(specifier.local.name, canonical); + } + } + }, + GlimmerElementNode(node) { if (!node.modifiers) { return; } for (const modifier of node.modifiers) { - if ( - modifier.path && - modifier.path.type === 'GlimmerPathExpression' && - (modifier.path.original === 'did-insert' || - modifier.path.original === 'did-update' || - modifier.path.original === 'will-destroy') - ) { + if (modifier.path?.type !== 'GlimmerPathExpression') { + continue; + } + const name = modifier.path.original; + if (isRenderModifier(name)) { context.report({ node: modifier, messageId: 'noRenderModifier', - data: { modifier: modifier.path.original }, + data: { modifier: canonicalName(name) }, }); } } diff --git a/tests/lib/rules/template-no-at-ember-render-modifiers.js b/tests/lib/rules/template-no-at-ember-render-modifiers.js index 88e525ea38..4dcdb1a8f5 100644 --- a/tests/lib/rules/template-no-at-ember-render-modifiers.js +++ b/tests/lib/rules/template-no-at-ember-render-modifiers.js @@ -32,6 +32,25 @@ ruleTester.run('template-no-at-ember-render-modifiers', rule, { '', '', '', + + // In GJS/GTS, kebab identifiers cannot be imports; these are bare paths + // that happen to share the canonical name but are not the render modifier. + { + filename: 'test.gjs', + code: '', + }, + // Unrelated imports with matching local names should not match + { + filename: 'test.gjs', + code: `import didInsert from './my-lib'; + `, + }, + // Root-package import of an unknown named export is not a render modifier + { + filename: 'test.gjs', + code: `import { somethingElse } from '@ember/render-modifiers'; + `, + }, ], invalid: [ @@ -87,6 +106,68 @@ ruleTester.run('template-no-at-ember-render-modifiers', rule, { output: null, errors: [{ messageId: 'noRenderModifier' }], }, + + // GJS/GTS import-based forms — local name is user-chosen + { + filename: 'test.gjs', + code: `import didInsert from '@ember/render-modifiers/modifiers/did-insert'; + `, + output: null, + errors: [{ messageId: 'noRenderModifier' }], + }, + { + filename: 'test.gjs', + code: `import didUpdate from '@ember/render-modifiers/modifiers/did-update'; + `, + output: null, + errors: [{ messageId: 'noRenderModifier' }], + }, + { + filename: 'test.gjs', + code: `import willDestroy from '@ember/render-modifiers/modifiers/will-destroy'; + `, + output: null, + errors: [{ messageId: 'noRenderModifier' }], + }, + // Renamed default import still flags + { + filename: 'test.gjs', + code: `import myInsert from '@ember/render-modifiers/modifiers/did-insert'; + `, + output: null, + errors: [{ messageId: 'noRenderModifier' }], + }, + + // Root-package named imports — all three modifiers + { + filename: 'test.gjs', + code: `import { didInsert } from '@ember/render-modifiers'; + `, + output: null, + errors: [{ messageId: 'noRenderModifier' }], + }, + { + filename: 'test.gjs', + code: `import { didUpdate } from '@ember/render-modifiers'; + `, + output: null, + errors: [{ messageId: 'noRenderModifier' }], + }, + { + filename: 'test.gjs', + code: `import { willDestroy } from '@ember/render-modifiers'; + `, + output: null, + errors: [{ messageId: 'noRenderModifier' }], + }, + // Aliased root-package import still flags + { + filename: 'test.gjs', + code: `import { didInsert as myModifier } from '@ember/render-modifiers'; + `, + output: null, + errors: [{ messageId: 'noRenderModifier' }], + }, ], });