Skip to content

Commit fbeee03

Browse files
committed
Auto-fixable missing invokable rule
Working on an auto-fixed rule for inserting imports when you are missing an invokable (helper, modifier, component).
1 parent ea0e380 commit fbeee03

2 files changed

Lines changed: 253 additions & 0 deletions

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
module.exports = {
3+
meta: {
4+
type: 'suggestion',
5+
docs: {
6+
description: 'disallow referencing let variables in \\<template\\>',
7+
category: 'Ember Octane',
8+
recommendedGjs: false,
9+
recommendedGts: false,
10+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-missing-invokable.md',
11+
},
12+
fixable: 'code',
13+
schema: [],
14+
messages: {
15+
'missing-invokable':
16+
'Not in scope. Did you forget to import this? Auto-fix may be configured.',
17+
},
18+
},
19+
20+
create: (context) => {
21+
const sourceCode = context.sourceCode;
22+
23+
// TODO make real config
24+
const config = {
25+
eq: { name: 'eq', module: 'ember-truth-helpers' },
26+
on: { name: 'on', module: '@ember/modifier' },
27+
};
28+
29+
// takes a node with a `.path` property
30+
function checkInvokable(node) {
31+
if (node.path.type === 'GlimmerPathExpression' && node.path.tail.length === 0) {
32+
if (!isBound(node.path.head, sourceCode.getScope(node.path))) {
33+
const matched = config[node.path.head.name];
34+
if (matched) {
35+
context.report({
36+
node: node.path,
37+
messageId: 'missing-invokable',
38+
fix(fixer) {
39+
return fixer.insertTextBeforeRange(
40+
[0, 0],
41+
`import { ${matched.name} } from '${matched.module}';\n`
42+
);
43+
},
44+
});
45+
}
46+
}
47+
}
48+
}
49+
50+
return {
51+
GlimmerSubExpression(node) {
52+
return checkInvokable(node);
53+
},
54+
GlimmerElementModifierStatement(node) {
55+
return checkInvokable(node);
56+
},
57+
GlimmerMustacheStatement(node) {
58+
return checkInvokable(node);
59+
},
60+
};
61+
},
62+
};
63+
64+
function isBound(node, scope) {
65+
const ref = scope.references.find((v) => v.identifier === node);
66+
if (!ref) {
67+
// TODO: can we make a test case for this?
68+
return false;
69+
}
70+
return Boolean(ref.resolved);
71+
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
//------------------------------------------------------------------------------
2+
// Requirements
3+
//------------------------------------------------------------------------------
4+
5+
const rule = require('../../../lib/rules/template-missing-invokable');
6+
const RuleTester = require('eslint').RuleTester;
7+
8+
//------------------------------------------------------------------------------
9+
// Tests
10+
//------------------------------------------------------------------------------
11+
12+
const ruleTester = new RuleTester({
13+
parser: require.resolve('ember-eslint-parser'),
14+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
15+
});
16+
ruleTester.run('template-missing-invokable', rule, {
17+
valid: [
18+
// Subexpression Invocations
19+
`
20+
import { eq } from 'somewhere';
21+
<template>
22+
{{#if (eq 1 1)}}
23+
They're equal
24+
{{/if}}
25+
</template>
26+
`,
27+
`
28+
function eq() {}
29+
<template>
30+
{{#if (eq 1 1)}}
31+
They're equal
32+
{{/if}}
33+
</template>
34+
`,
35+
`
36+
function x(eq) {
37+
<template>
38+
{{#if (eq 1 1)}}
39+
They're equal
40+
{{/if}}
41+
</template>
42+
}
43+
`,
44+
45+
// Mustache Invocations
46+
`
47+
import { eq } from 'somewhere';
48+
<template>
49+
{{eq 1 1}}
50+
</template>
51+
`,
52+
`
53+
import { eq } from 'somewhere';
54+
import MyComponent from 'somewhere';
55+
<template>
56+
<MyComponent @flag={{eq 1 1}} />
57+
</template>
58+
`,
59+
60+
// Modifier Invocations
61+
`
62+
import { on } from 'somewhere';
63+
function doSomething() {}
64+
<template>
65+
<button {{on "click" doSomething}}>Go</button>
66+
</template>
67+
`,
68+
],
69+
70+
invalid: [
71+
// Subexpression invocations
72+
{
73+
code: `
74+
<template>
75+
{{#if (eq 1 1)}}
76+
They're equal
77+
{{/if}}
78+
</template>
79+
`,
80+
output: `import { eq } from 'ember-truth-helpers';
81+
82+
<template>
83+
{{#if (eq 1 1)}}
84+
They're equal
85+
{{/if}}
86+
</template>
87+
`,
88+
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
89+
},
90+
91+
// Mustache Invocations
92+
{
93+
code: `
94+
<template>
95+
{{eq 1 1}}
96+
</template>
97+
`,
98+
output: `import { eq } from 'ember-truth-helpers';
99+
100+
<template>
101+
{{eq 1 1}}
102+
</template>
103+
`,
104+
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
105+
},
106+
{
107+
code: `
108+
import MyComponent from 'somewhere';
109+
<template>
110+
<MyComponent @flag={{eq 1 1}} />
111+
</template>
112+
`,
113+
output: `import { eq } from 'ember-truth-helpers';
114+
115+
import MyComponent from 'somewhere';
116+
<template>
117+
<MyComponent @flag={{eq 1 1}} />
118+
</template>
119+
`,
120+
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
121+
},
122+
123+
// Modifier Inovcations
124+
{
125+
code: `
126+
function doSomething() {}
127+
<template>
128+
<button {{on "click" doSomething}}>Go</button>
129+
</template>
130+
`,
131+
output: `import { on } from '@ember/modifier';
132+
133+
function doSomething() {}
134+
<template>
135+
<button {{on "click" doSomething}}>Go</button>
136+
</template>
137+
`,
138+
errors: [{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] }],
139+
},
140+
// Multiple copies of a fixable invocation
141+
{
142+
code: `
143+
let other = <template>
144+
{{#if (eq 3 3) }}
145+
three is three
146+
{{/if}}
147+
</template>
148+
149+
<template>
150+
{{#if (eq 1 1) }}
151+
one is one
152+
{{/if}}
153+
{{#if (eq 2 2) }}
154+
two is two
155+
{{/if}}
156+
</template>
157+
`,
158+
output: `import { eq } from 'ember-truth-helpers';
159+
160+
let other = <template>
161+
{{#if (eq 3 3) }}
162+
three is three
163+
{{/if}}
164+
</template>
165+
166+
<template>
167+
{{#if (eq 1 1) }}
168+
one is one
169+
{{/if}}
170+
{{#if (eq 2 2) }}
171+
two is two
172+
{{/if}}
173+
</template>
174+
`,
175+
errors: [
176+
{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] },
177+
{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] },
178+
{ type: 'GlimmerPathExpression', message: rule.meta.messages['missing-invokable'] },
179+
],
180+
},
181+
],
182+
});

0 commit comments

Comments
 (0)