Skip to content

Commit

Permalink
Update: Fix false positives/negatives in require-meta-fixable rule (#…
Browse files Browse the repository at this point in the history
…158)

False positives fixed:

* When `meta.fixable` uses a variable

False negatives fixed:

* When there's a fixer but `meta.fixable` = null
* When there's a fixer but `meta.fixable` = undefined

Also improves reporting location.
  • Loading branch information
bmish authored Jul 11, 2021
1 parent 88cb2bf commit dc29b03
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 41 deletions.
25 changes: 4 additions & 21 deletions docs/rules/require-meta-fixable.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@

✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule.

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.
ESLint requires fixable rules to specify a valid `meta.fixable` property (with value `code` or `whitespace`).

## Rule Details

This rule aims to require ESLint rules to have a `meta.fixable` property if necessary.
This rule aims to require fixable ESLint rules to have a valid `meta.fixable` property.

Examples of **incorrect** code for this rule:

```js
/* eslint eslint-plugin/require-meta-fixable: "error" */

module.exports = {
meta: {},
meta: {}, // missing `fixable` property
create (context) {
context.report({
node,
Expand Down Expand Up @@ -44,20 +44,6 @@ module.exports = {
};
```

```js
/* eslint eslint-plugin/require-meta-fixable: "error" */

module.exports = { create (context) {
context.report({
node,
message: 'foo',
fix (fixer) {
return fixer.remove(node);
},
});
} };
```

Examples of **correct** code for this rule:

```js
Expand Down Expand Up @@ -91,10 +77,7 @@ module.exports = {
};
```

## When Not To Use It

If you do not plan to implement autofixable rules, you can turn off this rule.

## Further Reading

* [ESLint's autofix API](http://eslint.org/docs/developer-guide/working-with-rules#applying-fixes)
* [ESLint's rule basics mentioning `meta.fixable`](https://eslint.org/docs/developer-guide/working-with-rules#rule-basics)
33 changes: 22 additions & 11 deletions lib/rules/require-meta-fixable.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

'use strict';

const { getStaticValue } = require('eslint-utils');
const utils = require('../utils');

// ------------------------------------------------------------------------------
Expand All @@ -20,6 +21,10 @@ module.exports = {
recommended: true,
},
schema: [],
messages: {
invalid: '`meta.fixable` must be either `code`, `whitespace` or `null`.',
missing: 'Fixable rules must export a `meta.fixable` property.',
},
},

create (context) {
Expand Down Expand Up @@ -62,19 +67,25 @@ module.exports = {
ruleInfo.meta.properties.find(prop => utils.getKeyName(prop) === 'fixable');

if (metaFixableProp) {
const VALID_VALUES = new Set(['code', 'whitespace', null, undefined]);
const valueIsValid = metaFixableProp.value.type === 'Literal'
? VALID_VALUES.has(metaFixableProp.value.value)
// eslint-disable-next-line unicorn/no-nested-ternary
: metaFixableProp.value.type === 'TemplateLiteral' && metaFixableProp.value.quasis.length === 1
? VALID_VALUES.has(metaFixableProp.value.quasis[0].value.cooked)
: metaFixableProp.value.type === 'Identifier' && metaFixableProp.value.name === 'undefined';
const staticValue = getStaticValue(metaFixableProp.value, context.getScope());
if (!staticValue) {
// Ignore non-static values since we can't determine what they look like.
return;
}

if (!['code', 'whitespace', null, undefined].includes(staticValue.value)) {
// `fixable` property has an invalid value.
context.report({ node: metaFixableProp.value, messageId: 'invalid' });
return;
}

if (!valueIsValid) {
context.report({ node: metaFixableProp, message: '`meta.fixable` must be either `code`, `whitespace` or `null`.' });
if (usesFixFunctions && !['code', 'whitespace'].includes(staticValue.value)) {
// Rule is fixable but `fixable` property does not have a fixable value.
context.report({ node: metaFixableProp.value, messageId: 'missing' });
}
} else if (usesFixFunctions) {
context.report({ node: ruleInfo.create, message: 'Fixable rules must export a `meta.fixable` property.' });
} else if (!metaFixableProp && usesFixFunctions) {
// Rule is fixable but is missing the `fixable` property.
context.report({ node: ruleInfo.meta || ruleInfo.create, messageId: 'missing' });
}
},
};
Expand Down
59 changes: 50 additions & 9 deletions tests/lib/rules/require-meta-fixable.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@
const rule = require('../../../lib/rules/require-meta-fixable');
const RuleTester = require('eslint').RuleTester;

const MISSING_ERROR = { message: 'Fixable rules must export a `meta.fixable` property.', type: 'FunctionExpression' };
const INVALID_ERROR = { message: '`meta.fixable` must be either `code`, `whitespace` or `null`.', type: 'Property' };

// ------------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------------
Expand All @@ -37,6 +34,16 @@ ruleTester.run('require-meta-fixable', rule, {
}
};
`,
// Value in variable.
`
const fixable = 'code';
module.exports = {
meta: { fixable },
create(context) {
context.report({node, message, fix: foo});
}
};
`,
`
module.exports = {
meta: { fixable: 'whitespace' },
Expand Down Expand Up @@ -93,6 +100,13 @@ ruleTester.run('require-meta-fixable', rule, {
}
};
`,
// `fixable` uses variable but no static value available.
`
module.exports = {
meta: { fixable: foo },
create(context) { context.report({node, message, fix: foo}); }
};
`,
`
module.exports = {
meta: {},
Expand Down Expand Up @@ -128,7 +142,15 @@ ruleTester.run('require-meta-fixable', rule, {
create(context) { context.report({node, message, fix: foo}); }
};
`,
errors: [MISSING_ERROR],
errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
},
{
code: `
module.exports = {
create(context) { context.report({node, message, fix: foo}); }
};
`,
errors: [{ messageId: 'missing', type: 'FunctionExpression' }],
},
{
code: `
Expand All @@ -137,7 +159,7 @@ ruleTester.run('require-meta-fixable', rule, {
create(context) { context.report(node, loc, message, data, fix); }
};
`,
errors: [MISSING_ERROR],
errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
},
{
code: `
Expand All @@ -146,7 +168,7 @@ ruleTester.run('require-meta-fixable', rule, {
create(context) { context.report({node, message}); }
};
`,
errors: [INVALID_ERROR],
errors: [{ messageId: 'invalid', type: 'Literal' }],
},
{
code: `
Expand All @@ -155,16 +177,35 @@ ruleTester.run('require-meta-fixable', rule, {
create(context) { context.report({node, message, fix: foo}); }
};
`,
errors: [INVALID_ERROR],
errors: [{ messageId: 'invalid', type: 'Literal' }],
},
{
code: `
const fixable = 'invalid';
module.exports = {
meta: { fixable },
create(context) { context.report({node, message, fix: foo}); }
};
`,
errors: [{ messageId: 'invalid', type: 'Identifier' }],
},
{
code: `
module.exports = {
meta: { fixable: null },
create(context) { context.report({node, message, fix: foo}); }
};
`,
errors: [{ messageId: 'missing', type: 'Literal' }],
},
{
code: `
module.exports = {
meta: { fixable: foo },
meta: { fixable: undefined },
create(context) { context.report({node, message, fix: foo}); }
};
`,
errors: [INVALID_ERROR],
errors: [{ messageId: 'missing', type: 'Identifier' }],
},
],
});

0 comments on commit dc29b03

Please sign in to comment.