Skip to content

Commit d0afd8d

Browse files
Merge pull request #2556 from ember-cli/copilot/sub-pr-2272
template-missing-invokable: add built-in invokables and always auto-fix from config
2 parents 870ee2f + 68963ce commit d0afd8d

4 files changed

Lines changed: 143 additions & 6 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,6 @@ npm-debug.log
1414

1515
# eslint-remote-tester
1616
eslint-remote-tester-results
17+
18+
# npm lock file (project uses pnpm)
19+
package-lock.json

lib/rules/template-missing-invokable.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
'use strict';
2+
3+
// Invokables that are available in every Ember project without any extra
4+
// packages. User-provided `invokables` config is merged on top of these so
5+
// any entry here can be overridden by the consuming project.
6+
const BUILTIN_INVOKABLES = {
7+
fn: ['fn', '@ember/helper'],
8+
get: ['get', '@ember/helper'],
9+
hash: ['hash', '@ember/helper'],
10+
array: ['array', '@ember/helper'],
11+
concat: ['concat', '@ember/helper'],
12+
htmlSafe: ['htmlSafe', '@ember/template'],
13+
trustedHTML: ['trustedHTML', '@ember/template'],
14+
LinkTo: ['LinkTo', '@ember/routing'],
15+
on: ['on', '@ember/modifier'],
16+
trackedArray: ['trackedArray', '@ember/reactive/collections'],
17+
trackedObject: ['trackedObject', '@ember/reactive/collections'],
18+
trackedSet: ['trackedSet', '@ember/reactive/collections'],
19+
trackedWeakSet: ['trackedWeakSet', '@ember/reactive/collections'],
20+
trackedWeakMap: ['trackedWeakMap', '@ember/reactive/collections'],
21+
};
22+
123
/** @type {import('eslint').Rule.RuleModule} */
224
module.exports = {
325
meta: {
@@ -34,15 +56,16 @@ module.exports = {
3456

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

3861
// takes a node with a `.path` property
3962
function checkInvokable(node) {
4063
if (node.path.type === 'GlimmerPathExpression' && node.path.tail.length === 0) {
4164
if (!isBound(node.path.head, sourceCode.getScope(node.path))) {
42-
const matched = context.options[0]?.invokables?.[node.path.head.name];
65+
const matched = invokables[node.path.head.name];
4366
if (matched) {
44-
const [name, module] = matched;
45-
const importStatement = buildImportStatement(node.path.head.name, name, module);
67+
const [name, moduleName] = matched;
68+
const importStatement = buildImportStatement(node.path.head.name, name, moduleName);
4669
context.report({
4770
node: node.path,
4871
messageId: 'missing-invokable',
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "has-ember-truth-helpers",
3+
"dependencies": {
4+
"ember-truth-helpers": "*"
5+
}
6+
}

tests/lib/rules/template-missing-invokable.js

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,24 @@ ruleTester.run('template-missing-invokable', rule, {
6565
<button {{on "click" doSomething}}>Go</button>
6666
</template>
6767
`,
68+
69+
// Built-in invokables are not reported when already imported
70+
`
71+
import { fn } from '@ember/helper';
72+
<template>
73+
{{fn myFunc 1}}
74+
</template>
75+
`,
76+
`
77+
import { LinkTo } from '@ember/routing';
78+
<template>
79+
<LinkTo @route="index">Home</LinkTo>
80+
</template>
81+
`,
6882
],
6983

7084
invalid: [
71-
// Subexpression invocations
85+
// Subexpression invocations — always auto-fixes when invokable is configured
7286
{
7387
code: `
7488
<template>
@@ -96,7 +110,7 @@ ruleTester.run('template-missing-invokable', rule, {
96110
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
97111
},
98112

99-
// Mustache Invocations
113+
// Mustache Invocations — always auto-fixes when invokable is configured
100114
{
101115
code: `
102116
<template>
@@ -142,7 +156,7 @@ ruleTester.run('template-missing-invokable', rule, {
142156
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
143157
},
144158

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

244258
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
245259
},
260+
261+
// Built-in: fn — auto-fixes without any user config
262+
{
263+
code: `
264+
<template>
265+
{{fn myFunc 1}}
266+
</template>
267+
`,
268+
output: `import { fn } from '@ember/helper';
269+
270+
<template>
271+
{{fn myFunc 1}}
272+
</template>
273+
`,
274+
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
275+
},
276+
277+
// Built-in: hash — auto-fixes without any user config
278+
{
279+
code: `
280+
<template>
281+
<MyComp @opts={{hash a=1}} />
282+
</template>
283+
`,
284+
output: `import { hash } from '@ember/helper';
285+
286+
<template>
287+
<MyComp @opts={{hash a=1}} />
288+
</template>
289+
`,
290+
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
291+
},
292+
293+
// Built-in: on modifier — auto-fixes without any user config
294+
{
295+
code: `
296+
function doSomething() {}
297+
<template>
298+
<button {{on "click" doSomething}}>Go</button>
299+
</template>
300+
`,
301+
output: `import { on } from '@ember/modifier';
302+
303+
function doSomething() {}
304+
<template>
305+
<button {{on "click" doSomething}}>Go</button>
306+
</template>
307+
`,
308+
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
309+
},
310+
311+
// Built-in: LinkTo — auto-fixes without any user config
312+
{
313+
code: `
314+
<template>
315+
<LinkTo @route="index">Home</LinkTo>
316+
</template>
317+
`,
318+
output: `import { LinkTo } from '@ember/routing';
319+
320+
<template>
321+
<LinkTo @route="index">Home</LinkTo>
322+
</template>
323+
`,
324+
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
325+
},
326+
327+
// User config overrides a built-in
328+
{
329+
code: `
330+
function doSomething() {}
331+
<template>
332+
<button {{on "click" doSomething}}>Go</button>
333+
</template>
334+
`,
335+
output: `import { on } from 'my-custom-modifier-package';
336+
337+
function doSomething() {}
338+
<template>
339+
<button {{on "click" doSomething}}>Go</button>
340+
</template>
341+
`,
342+
options: [
343+
{
344+
invokables: {
345+
on: ['on', 'my-custom-modifier-package'],
346+
},
347+
},
348+
],
349+
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
350+
},
246351
],
247352
});

0 commit comments

Comments
 (0)