Skip to content

Commit 59bfdd6

Browse files
New: require-meta-fixable rule
1 parent 7a74a7b commit 59bfdd6

File tree

5 files changed

+402
-0
lines changed

5 files changed

+402
-0
lines changed

docs/rules/require-meta-fixable.md

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# require rules to implement a meta.fixable property (require-meta-fixable)
2+
3+
A fixable ESLint rule must have a valid `meta.fixable` property. A rule reports a problem with a `fix()` function but does not export a `meta.fixable` property is likely to cause an unexpected error.
4+
5+
## Rule Details
6+
7+
This rule aims to require ESLint rules to have a `meta.fixable` property if necessary.
8+
9+
The following patterns are considered warnings:
10+
11+
```js
12+
13+
/* eslint eslint-plugin/require-meta-fixable: "error" */
14+
15+
module.exports = {
16+
meta: {},
17+
create(context) {
18+
context.report({
19+
node,
20+
message: 'foo',
21+
fix(fixer) {
22+
return fixer.remove(node);
23+
}
24+
});
25+
}
26+
};
27+
28+
```
29+
30+
```js
31+
32+
/* eslint eslint-plugin/require-meta-fixable: "error" */
33+
34+
module.exports = {
35+
meta: { fixable: 'not a valid meta.fixable value' },
36+
create(context) {
37+
context.report({
38+
node,
39+
message: 'foo',
40+
fix(fixer) {
41+
return fixer.remove(node);
42+
}
43+
});
44+
}
45+
};
46+
47+
```
48+
49+
```js
50+
51+
/* eslint eslint-plugin/require-meta-fixable: "error" */
52+
53+
module.exports = function create(context) {
54+
context.report({
55+
node,
56+
message: 'foo',
57+
fix(fixer) {
58+
return fixer.remove(node);
59+
}
60+
});
61+
};
62+
63+
```
64+
65+
The following patterns are not warnings:
66+
67+
```js
68+
69+
/* eslint eslint-plugin/require-meta-fixable: "error" */
70+
71+
module.exports = {
72+
meta: { fixable: 'code' },
73+
create(context) {
74+
context.report({
75+
node,
76+
message: 'foo',
77+
fix(fixer) {
78+
return fixer.remove(node);
79+
}
80+
});
81+
}
82+
};
83+
84+
```
85+
86+
```js
87+
88+
/* eslint eslint-plugin/require-meta-fixable: "error" */
89+
90+
module.exports = {
91+
meta: {},
92+
create(context) {
93+
context.report({
94+
node,
95+
message: 'foo'
96+
});
97+
}
98+
};
99+
100+
```
101+
102+
## When Not To Use It
103+
104+
If you do not plan to implement autofixable rules, you can turn off this rule.
105+
106+
## Further Reading
107+
108+
* [ESLint's autofix API](http://eslint.org/docs/developer-guide/working-with-rules#applying-fixes)

lib/rules/require-meta-fixable.js

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* @fileoverview require rules to implement a meta.fixable property
3+
* @author Teddy Katz
4+
*/
5+
6+
'use strict';
7+
8+
const utils = require('../utils');
9+
10+
// ------------------------------------------------------------------------------
11+
// Rule Definition
12+
// ------------------------------------------------------------------------------
13+
14+
module.exports = {
15+
meta: {
16+
docs: {
17+
description: 'require rules to implement a meta.fixable property',
18+
category: 'Rules',
19+
recommended: false,
20+
},
21+
schema: [],
22+
},
23+
24+
create (context) {
25+
const sourceCode = context.getSourceCode();
26+
const ruleInfo = utils.getRuleInfo(sourceCode.ast);
27+
let contextIdentifiers;
28+
let usesFixFunctions;
29+
30+
// ----------------------------------------------------------------------
31+
// Helpers
32+
// ----------------------------------------------------------------------
33+
34+
// ----------------------------------------------------------------------
35+
// Public
36+
// ----------------------------------------------------------------------
37+
38+
return {
39+
Program (node) {
40+
contextIdentifiers = utils.getContextIdentifiers(context, node);
41+
},
42+
CallExpression (node) {
43+
if (
44+
node.callee.type === 'MemberExpression' &&
45+
contextIdentifiers.has(node.callee.object) &&
46+
node.callee.property.type === 'Identifier' &&
47+
node.callee.property.name === 'report' &&
48+
(node.arguments.length > 3 || (
49+
node.arguments.length === 1 &&
50+
node.arguments[0].type === 'ObjectExpression' &&
51+
node.arguments[0].properties.some(prop => utils.getKeyName(prop) === 'fix')
52+
))
53+
) {
54+
usesFixFunctions = true;
55+
}
56+
},
57+
'Program:exit' () {
58+
const metaFixableProp = ruleInfo &&
59+
ruleInfo.meta &&
60+
ruleInfo.meta.type === 'ObjectExpression' &&
61+
ruleInfo.meta.properties.find(prop => utils.getKeyName(prop) === 'fixable');
62+
63+
if (metaFixableProp) {
64+
const VALID_VALUES = new Set(['code', 'whitespace', null, undefined]);
65+
const valueIsValid = metaFixableProp.value.type === 'Literal'
66+
? VALID_VALUES.has(metaFixableProp.value.value)
67+
: metaFixableProp.value.type === 'TemplateLiteral' && metaFixableProp.value.quasis.length === 1
68+
? VALID_VALUES.has(metaFixableProp.value.quasis[0].value.cooked)
69+
: metaFixableProp.value.type === 'Identifier' && metaFixableProp.value.name === 'undefined';
70+
71+
if (!valueIsValid) {
72+
context.report({ node: metaFixableProp, message: '`meta.fixable` must be either `code`, `whitespace` or `null`.' });
73+
}
74+
} else if (usesFixFunctions) {
75+
context.report({ node: ruleInfo.create, message: 'Fixable rules must export a `meta.fixable` property.' });
76+
}
77+
},
78+
};
79+
},
80+
};

lib/utils.js

+18
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,22 @@ module.exports = {
107107
.map(ref => ref.identifier)
108108
);
109109
},
110+
111+
/**
112+
* Gets the key name of a Property, if it can be determined statically.
113+
* @param {ASTNode} node The `Property` node
114+
* @returns {string|null} The key name, or `null` if the name cannot be determined statically.
115+
*/
116+
getKeyName (property) {
117+
if (!property.computed && property.key.type === 'Identifier') {
118+
return property.key.name;
119+
}
120+
if (property.key.type === 'Literal') {
121+
return '' + property.key.value;
122+
}
123+
if (property.key.type === 'TemplateLiteral' && property.key.quasis.length === 1) {
124+
return property.key.quasis[0].value.cooked;
125+
}
126+
return null;
127+
},
110128
};
+171
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/**
2+
* @fileoverview require rules to implement a meta.fixable property
3+
* @author Teddy Katz
4+
*/
5+
6+
'use strict';
7+
8+
// ------------------------------------------------------------------------------
9+
// Requirements
10+
// ------------------------------------------------------------------------------
11+
12+
const rule = require('../../../lib/rules/require-meta-fixable');
13+
const RuleTester = require('eslint').RuleTester;
14+
15+
const MISSING_ERROR = [{ message: 'Fixable rules must export a `meta.fixable` property.', type: 'FunctionExpression' }];
16+
const INVALID_ERROR = [{ message: '`meta.fixable` must be either `code`, `whitespace` or `null`.', type: 'Property' }];
17+
18+
// ------------------------------------------------------------------------------
19+
// Tests
20+
// ------------------------------------------------------------------------------
21+
22+
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
23+
ruleTester.run('require-meta-fixable', rule, {
24+
valid: [
25+
`
26+
module.exports = {
27+
meta: {},
28+
create(context) {}
29+
};
30+
`,
31+
'module.exports = context => {};',
32+
`
33+
module.exports = {
34+
meta: { fixable: 'code' },
35+
create(context) {
36+
context.report({node, message, fix: foo});
37+
}
38+
};
39+
`,
40+
`
41+
module.exports = {
42+
meta: { fixable: 'code' },
43+
create(context) {
44+
context.report({node, message, fix: foo});
45+
}
46+
};
47+
`,
48+
`
49+
module.exports = {
50+
meta: { fixable: 'whitespace' },
51+
create(context) {
52+
context.report({node, message, fix: foo});
53+
}
54+
};
55+
`,
56+
`
57+
module.exports = {
58+
meta: { 'fixable': 'code' },
59+
create(context) {
60+
context.report({node, message, fix: foo});
61+
}
62+
};
63+
`,
64+
`
65+
module.exports = {
66+
meta: { ['fixable']: 'code' },
67+
create(context) {
68+
context.report({node, message, fix: foo});
69+
}
70+
};
71+
`,
72+
`
73+
module.exports = {
74+
meta: { [\`fixable\`]: 'code' },
75+
create(context) {
76+
context.report({node, message, fix: foo});
77+
}
78+
};
79+
`,
80+
`
81+
module.exports = {
82+
meta: { fixable: 'code' },
83+
create(context) {
84+
context.report({node, message});
85+
}
86+
};
87+
`,
88+
`
89+
module.exports = {
90+
meta: { fixable: null },
91+
create(context) {
92+
context.report({node, message});
93+
}
94+
};
95+
`,
96+
`
97+
module.exports = {
98+
meta: { fixable: undefined },
99+
create(context) {
100+
context.report({node, message});
101+
}
102+
};
103+
`,
104+
{
105+
code: `
106+
module.exports = {
107+
meta: {},
108+
create(context) { context.report(node, loc, message); }
109+
};
110+
`,
111+
errors: [MISSING_ERROR],
112+
},
113+
],
114+
115+
invalid: [
116+
{
117+
code: `
118+
module.exports = {
119+
meta: {},
120+
create(context) { context.report({node, message, fix: foo}); }
121+
};
122+
`,
123+
errors: [MISSING_ERROR],
124+
},
125+
{
126+
code: `
127+
module.exports = {
128+
meta: {},
129+
create(context) { context.report(node, message, data, fix); }
130+
};
131+
`,
132+
errors: [MISSING_ERROR],
133+
},
134+
{
135+
code: `
136+
module.exports = {
137+
meta: {},
138+
create(context) { context.report(node, loc, message, data, fix); }
139+
};
140+
`,
141+
errors: [MISSING_ERROR],
142+
},
143+
{
144+
code: `
145+
module.exports = {
146+
meta: { fixable: 'invalid' },
147+
create(context) { context.report({node, message}); }
148+
};
149+
`,
150+
errors: [INVALID_ERROR],
151+
},
152+
{
153+
code: `
154+
module.exports = {
155+
meta: { fixable: 'invalid' },
156+
create(context) { context.report({node, message, fix: foo}); }
157+
};
158+
`,
159+
errors: [INVALID_ERROR],
160+
},
161+
{
162+
code: `
163+
module.exports = {
164+
meta: { fixable: foo },
165+
create(context) { context.report({node, message, fix: foo}); }
166+
};
167+
`,
168+
errors: [INVALID_ERROR],
169+
},
170+
],
171+
});

0 commit comments

Comments
 (0)