Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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": "*"
}
}
132 changes: 108 additions & 24 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,28 +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.

{
code: `
<template>
{{eq 1 1}}
</template>
`,
output: `import { eq } from 'ember-truth-helpers';

<template>
{{eq 1 1}}
</template>
`,
options: [
{
invokables: {
eq: ['eq', 'ember-truth-helpers'],
},
},
],
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
},
// Mustache Invocations — always auto-fixes when invokable is configured
{
code: `
import MyComponent from 'somewhere';
Expand All @@ -142,7 +135,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 +236,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'] }],
},
],
});