Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ npm-debug.log

# eslint-remote-tester
eslint-remote-tester-results

# npm lock file (project uses pnpm)
package-lock.json
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ rules in templates can be disabled with eslint directives with mustache or html
| [no-empty-glimmer-component-classes](docs/rules/no-empty-glimmer-component-classes.md) | disallow empty backing classes for Glimmer components | ✅ | | |
| [no-tracked-properties-from-args](docs/rules/no-tracked-properties-from-args.md) | disallow creating @tracked properties from this.args | ✅ | | |
| [template-indent](docs/rules/template-indent.md) | enforce consistent indentation for gts/gjs templates | | 🔧 | |
| [template-missing-invokable](docs/rules/template-missing-invokable.md) | disallow missing helpers, modifiers, or components in \<template\> with auto-fix to import them | | 🔧 | |
| [template-no-let-reference](docs/rules/template-no-let-reference.md) | disallow referencing let variables in \<template\> | ![gjs logo](/docs/svgs/gjs.svg) ![gts logo](/docs/svgs/gts.svg) | | |

### jQuery
Expand Down
30 changes: 30 additions & 0 deletions docs/rules/template-missing-invokable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# ember/template-missing-invokable

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->

Auto-fixes missing imports for helpers, modifiers, and components in your \<template> tags.

If you refer to `on` without importing it:

```gjs
<template>
<button {{on "click" doSomething}}>Do Something</button>
</template>
```

The auto-fix will create the import:

```gjs
import { on } from '@ember/modifier';
<template>
<button {{on "click" doSomething}}>Do Something</button>
</template>
```

## Examples

## Config

- invokables
111 changes: 111 additions & 0 deletions lib/rules/template-missing-invokable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
'use strict';

// Invokables that are available in every Ember project without any extra
// packages. User-provided `invokables` config is merged on top of these so
// any entry here can be overridden by the consuming project.
const BUILTIN_INVOKABLES = {
fn: ['fn', '@ember/helper'],
get: ['get', '@ember/helper'],
hash: ['hash', '@ember/helper'],
array: ['array', '@ember/helper'],
concat: ['concat', '@ember/helper'],
htmlSafe: ['htmlSafe', '@ember/template'],
trustedHTML: ['trustedHTML', '@ember/template'],
LinkTo: ['LinkTo', '@ember/routing'],
on: ['on', '@ember/modifier'],
trackedArray: ['trackedArray', '@ember/reactive/collections'],
trackedObject: ['trackedObject', '@ember/reactive/collections'],
trackedSet: ['trackedSet', '@ember/reactive/collections'],
trackedWeakSet: ['trackedWeakSet', '@ember/reactive/collections'],
trackedWeakMap: ['trackedWeakMap', '@ember/reactive/collections'],
};

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'disallow missing helpers, modifiers, or components in \\<template\\> with auto-fix to import them',
category: 'Ember Octane',
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-missing-invokable.md',
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
invokables: {
type: 'object',
additionalProperties: {
type: 'array',
prefixItems: [
{ type: 'string', description: 'The name to import from the module' },
{ type: 'string', description: 'The module to import from' },
],
},
},
},
},
],
messages: {
'missing-invokable':
'Not in scope. Did you forget to import this? Auto-fix may be configured.',
},
},

create: (context) => {
const sourceCode = context.sourceCode;
const invokables = { ...BUILTIN_INVOKABLES, ...context.options[0]?.invokables };

// takes a node with a `.path` property
function checkInvokable(node) {
if (node.path.type === 'GlimmerPathExpression' && node.path.tail.length === 0) {
if (!isBound(node.path.head, sourceCode.getScope(node.path))) {
const matched = invokables[node.path.head.name];
if (matched) {
const [name, moduleName] = matched;
const importStatement = buildImportStatement(node.path.head.name, name, moduleName);
context.report({
node: node.path,
messageId: 'missing-invokable',
fix(fixer) {
return fixer.insertTextBeforeRange([0, 0], `${importStatement};\n`);
},
});
}
}
}
}

return {
GlimmerSubExpression(node) {
return checkInvokable(node);
},
GlimmerElementModifierStatement(node) {
return checkInvokable(node);
},
GlimmerMustacheStatement(node) {
return checkInvokable(node);
},
};
},
};

function isBound(node, scope) {
const ref = scope.references.find((v) => v.identifier === node);
if (!ref) {
return false;
}
return Boolean(ref.resolved);
}

function buildImportStatement(consumedName, exportedName, module) {
if (exportedName === 'default') {
return `import ${consumedName} from '${module}'`;
} else {
return consumedName === exportedName
? `import { ${consumedName} } from '${module}'`
: `import { ${exportedName} as ${consumedName} } from '${module}'`;
}
}
6 changes: 6 additions & 0 deletions tests/fixtures/projects/has-ember-truth-helpers/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "has-ember-truth-helpers",
"dependencies": {
"ember-truth-helpers": "*"
}
}
Loading