Skip to content
Merged
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
29 changes: 26 additions & 3 deletions lib/rules/template-missing-invokable.js
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot let's git rid of the package.json reading actually -- it needs to be done very carefully, and I don't want to walk you through that right now. lets just rely on the config for the lint rule.

Also, we need a list of default importables from the template:

import { fn, get, hash, array, concat } from '@ember/helper';
import { htmlSafe, trustedHTML } from '@ember/template';
import { LinkTo } from '@ember/routing';
import { on } from '@ember/modifier';
import { trackedArray, trackedObject, trackedSet, trackedWeakSet, trackedWeakMap } from '@ember/reactive/collections';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in cf2a5cd. All FS/package.json reading is removed — path, fs, the cache, and all related helpers are gone. Auto-fix now runs unconditionally whenever an invokable is matched.

Added BUILTIN_INVOKABLES with the defaults you listed:

fn, get, hash, array, concat    @ember/helper
htmlSafe, trustedHTML            @ember/template
LinkTo                           @ember/routing
on                               @ember/modifier
trackedArray, trackedObject, trackedSet, trackedWeakSet, trackedWeakMap    @ember/reactive/collections

User-provided invokables config is spread on top, so any built-in can be overridden.

Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
'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: {
Expand Down Expand Up @@ -34,15 +56,16 @@ module.exports = {

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 = context.options[0]?.invokables?.[node.path.head.name];
const matched = invokables[node.path.head.name];
if (matched) {
const [name, module] = matched;
const importStatement = buildImportStatement(node.path.head.name, name, module);
const [name, moduleName] = matched;
const importStatement = buildImportStatement(node.path.head.name, name, moduleName);
context.report({
node: node.path,
messageId: 'missing-invokable',
Expand Down
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": "*"
}
}
111 changes: 108 additions & 3 deletions tests/lib/rules/template-missing-invokable.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,24 @@ ruleTester.run('template-missing-invokable', rule, {
<button {{on "click" doSomething}}>Go</button>
</template>
`,

// Built-in invokables are not reported when already imported
`
import { fn } from '@ember/helper';
<template>
{{fn myFunc 1}}
</template>
`,
`
import { LinkTo } from '@ember/routing';
<template>
<LinkTo @route="index">Home</LinkTo>
</template>
`,
],

invalid: [
// Subexpression invocations
// Subexpression invocations — always auto-fixes when invokable is configured
{
code: `
<template>
Expand Down Expand Up @@ -96,7 +110,7 @@ ruleTester.run('template-missing-invokable', rule, {
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
},

// Mustache Invocations
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot why'd you remove these?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I dropped the output: null tests (no longer valid since auto-fix always fires now), I accidentally also dropped the standalone {{eq 1 1}} mustache case along with it. Restored in 68963ce with the proper fix output.

// Mustache Invocations — always auto-fixes when invokable is configured
{
code: `
<template>
Expand Down Expand Up @@ -142,7 +156,7 @@ ruleTester.run('template-missing-invokable', rule, {
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
},

// Modifier Inovcations
// Modifier Invocations — always auto-fixes when invokable is configured
{
code: `
function doSomething() {}
Expand Down Expand Up @@ -243,5 +257,96 @@ ruleTester.run('template-missing-invokable', rule, {

errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
},

// Built-in: fn — auto-fixes without any user config
{
code: `
<template>
{{fn myFunc 1}}
</template>
`,
output: `import { fn } from '@ember/helper';

<template>
{{fn myFunc 1}}
</template>
`,
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
},

// Built-in: hash — auto-fixes without any user config
{
code: `
<template>
<MyComp @opts={{hash a=1}} />
</template>
`,
output: `import { hash } from '@ember/helper';

<template>
<MyComp @opts={{hash a=1}} />
</template>
`,
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
},

// Built-in: on modifier — auto-fixes without any user config
{
code: `
function doSomething() {}
<template>
<button {{on "click" doSomething}}>Go</button>
</template>
`,
output: `import { on } from '@ember/modifier';

function doSomething() {}
<template>
<button {{on "click" doSomething}}>Go</button>
</template>
`,
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
},

// Built-in: LinkTo — auto-fixes without any user config
{
code: `
<template>
<LinkTo @route="index">Home</LinkTo>
</template>
`,
output: `import { LinkTo } from '@ember/routing';

<template>
<LinkTo @route="index">Home</LinkTo>
</template>
`,
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
},

// User config overrides a built-in
{
code: `
function doSomething() {}
<template>
<button {{on "click" doSomething}}>Go</button>
</template>
`,
output: `import { on } from 'my-custom-modifier-package';

function doSomething() {}
<template>
<button {{on "click" doSomething}}>Go</button>
</template>
`,
options: [
{
invokables: {
on: ['on', 'my-custom-modifier-package'],
},
},
],
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
},
],
});