Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ module.exports = [
| [prefer-replace-text](docs/rules/prefer-replace-text.md) | require using `replaceText()` instead of `replaceTextRange()` | | | | |
| [report-message-format](docs/rules/report-message-format.md) | enforce a consistent format for rule report messages | | | | |
| [require-meta-docs-description](docs/rules/require-meta-docs-description.md) | require rules to implement a `meta.docs.description` property with the correct format | | | | |
| [require-meta-docs-recommended](docs/rules/require-meta-docs-recommended.md) | require rules to implement a `meta.docs.recommended` property | | | | |
| [require-meta-docs-url](docs/rules/require-meta-docs-url.md) | require rules to implement a `meta.docs.url` property | | 🔧 | | |
| [require-meta-fixable](docs/rules/require-meta-fixable.md) | require rules to implement a `meta.fixable` property | ✅ | | | |
| [require-meta-has-suggestions](docs/rules/require-meta-has-suggestions.md) | require suggestable rules to implement a `meta.hasSuggestions` property | ✅ | 🔧 | | |
Expand Down
39 changes: 39 additions & 0 deletions docs/rules/require-meta-docs-recommended.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Require rules to implement a `meta.docs.recommended` property (`eslint-plugin/require-meta-docs-recommended`)

<!-- end auto-generated rule header -->

Defining a whether recommended value for each rule can help developers understand whether they're recommended.

## Rule Details

This rule requires ESLint rules to have a valid `meta.docs.recommended` property.

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

```js
/* eslint eslint-plugin/require-meta-docs-recommended: error */

module.exports = {
meta: {},
create(context) {
/* ... */
},
};
```

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

```js
/* eslint eslint-plugin/require-meta-docs-recommended: error */

module.exports = {
meta: { recommended: true },
create(context) {
/* ... */
},
};
```

## Further Reading

- [working-with-rules#options-schemas](https://eslint.org/docs/developer-guide/working-with-rules#options-schemas)
31 changes: 16 additions & 15 deletions lib/rules/require-meta-docs-description.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,19 @@ module.exports = {
const scope = sourceCode.getScope?.(ast) || context.getScope(); // TODO: just use sourceCode.getScope() when we drop support for ESLint < v9.0.0
const { scopeManager } = sourceCode;

const pattern =
context.options[0] && context.options[0].pattern
? new RegExp(context.options[0].pattern)
: DEFAULT_PATTERN;
const {
docsNode,
metaNode,
metaPropertyNode: descriptionNode,
} = utils.getMetaProperty('description', ruleInfo, scopeManager);

const metaNode = ruleInfo.meta;
const docsNode = utils
.evaluateObjectProperties(metaNode, scopeManager)
.find((p) => p.type === 'Property' && utils.getKeyName(p) === 'docs');

const descriptionNode = utils
.evaluateObjectProperties(docsNode && docsNode.value, scopeManager)
.find(
(p) =>
p.type === 'Property' && utils.getKeyName(p) === 'description'
);
if (!descriptionNode) {
context.report({
node: docsNode || metaNode || ruleInfo.create,
messageId: 'missing',
});
return;
}

if (!descriptionNode) {
context.report({
Expand All @@ -87,6 +84,10 @@ module.exports = {
return;
}

const pattern = context.options[0]?.pattern
? new RegExp(context.options[0].pattern)
: DEFAULT_PATTERN;

if (typeof staticValue.value !== 'string' || staticValue.value === '') {
context.report({
node: descriptionNode.value,
Expand Down
46 changes: 46 additions & 0 deletions lib/rules/require-meta-docs-recommended.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use strict';

const utils = require('../utils');

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'require rules to implement a `meta.docs.recommended` property',
category: 'Rules',
recommended: false,
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/require-meta-docs-recommended.md',
},
fixable: null,
schema: [],
messages: {
missing: '`meta.docs.recommended` is required.',
},
},

create(context) {
const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9
const ruleInfo = utils.getRuleInfo(sourceCode);
if (!ruleInfo) {
return {};
}

const { scopeManager } = sourceCode;
const {
docsNode,
metaNode,
metaPropertyNode: descriptionNode,
} = utils.getMetaProperty('recommended', ruleInfo, scopeManager);

if (!descriptionNode) {
context.report({
node: docsNode || metaNode || ruleInfo.create,
messageId: 'missing',
});
}

return {};
},
};
34 changes: 13 additions & 21 deletions lib/rules/require-meta-docs-url.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
// -----------------------------------------------------------------------------

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

// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -77,7 +77,7 @@ module.exports = {
}

const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9
const ruleInfo = util.getRuleInfo(sourceCode);
const ruleInfo = utils.getRuleInfo(sourceCode);
if (!ruleInfo) {
return {};
}
Expand All @@ -87,16 +87,11 @@ module.exports = {
const scope = sourceCode.getScope?.(ast) || context.getScope(); // TODO: just use sourceCode.getScope() when we drop support for ESLint < v9.0.0
const { scopeManager } = sourceCode;

const metaNode = ruleInfo.meta;
const docsPropNode = util
.evaluateObjectProperties(metaNode, scopeManager)
.find((p) => p.type === 'Property' && util.getKeyName(p) === 'docs');
const urlPropNode = util
.evaluateObjectProperties(
docsPropNode && docsPropNode.value,
scopeManager
)
.find((p) => p.type === 'Property' && util.getKeyName(p) === 'url');
const {
docsNode,
metaNode,
metaPropertyNode: urlPropNode,
} = utils.getMetaProperty('url', ruleInfo, scopeManager);

const staticValue = urlPropNode
? getStaticValue(urlPropNode.value, scope)
Expand All @@ -113,7 +108,7 @@ module.exports = {
context.report({
node:
(urlPropNode && urlPropNode.value) ||
(docsPropNode && docsPropNode.value) ||
(docsNode && docsNode.value) ||
metaNode ||
ruleInfo.create,

Expand Down Expand Up @@ -143,22 +138,19 @@ module.exports = {
) {
return fixer.replaceText(urlPropNode.value, urlString);
}
} else if (
docsPropNode &&
docsPropNode.value.type === 'ObjectExpression'
) {
return util.insertProperty(
} else if (docsNode && docsNode.value.type === 'ObjectExpression') {
return utils.insertProperty(
fixer,
docsPropNode.value,
docsNode.value,
`url: ${urlString}`,
sourceCode
);
} else if (
!docsPropNode &&
!docsNode &&
metaNode &&
metaNode.type === 'ObjectExpression'
) {
return util.insertProperty(
return utils.insertProperty(
fixer,
metaNode,
`docs: {\nurl: ${urlString}\n}`,
Expand Down
19 changes: 19 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,25 @@ module.exports = {
});
},

getMetaProperty(propertyName, ruleInfo, scopeManager) {
const metaNode = ruleInfo.meta;

const docsNode = module.exports
.evaluateObjectProperties(metaNode, scopeManager)
.find(
(p) => p.type === 'Property' && module.exports.getKeyName(p) === 'docs'
);

const metaPropertyNode = module.exports
.evaluateObjectProperties(docsNode?.value, scopeManager)
.find(
(p) =>
p.type === 'Property' && module.exports.getKeyName(p) === propertyName
);

return { docsNode, metaNode, metaPropertyNode };
},

/**
* Get the `meta.messages` node from a rule.
* @param {RuleInfo} ruleInfo
Expand Down
107 changes: 107 additions & 0 deletions tests/lib/rules/require-meta-docs-recommended.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
'use strict';

const rule = require('../../../lib/rules/require-meta-docs-recommended');
const RuleTester = require('eslint').RuleTester;

const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 9 } });
ruleTester.run('require-meta-docs-recommended', rule, {
valid: [
'foo()',
'module.exports = {};',
`
module.exports = {
meta: { docs: { recommended: true } },
create(context) {}
};
`,
{
code: `
export default {
meta: { docs: { recommended: 'disallow unused variables' } },
create(context) {}
};
`,
parserOptions: { sourceType: 'module' },
},
`
const RECOMMENDED = true;
module.exports = {
meta: { docs: { recommended: RECOMMENDED } },
create(context) {}
};
`,

`
const meta = { docs: { recommended: 'enforce foo' } };
module.exports = {
meta,
create(context) {}
};
`,
`
const extraDocs = { recommended: 123 };
const extraMeta = { docs: { ...extraDocs } };
module.exports = {
meta: { ...extraMeta },
create(context) {}
};
`,
],

invalid: [
{
code: 'module.exports = { create(context) {} };',
output: null,
errors: [{ messageId: 'missing', type: 'FunctionExpression' }],
},
{
code: `
module.exports = {
meta: {},
create(context) {}
};
`,
output: null,
errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
},
{
code: `
const extraDocs = { };
const extraMeta = { docs: { ...extraDocs } };
module.exports = {
meta: { ...extraMeta },
create(context) {}
};
`,
output: null,
errors: [{ messageId: 'missing', type: 'Property' }],
},
],
});

const ruleTesterTypeScript = new RuleTester({
parserOptions: { sourceType: 'module' },
parser: require.resolve('@typescript-eslint/parser'),
});
ruleTesterTypeScript.run('require-meta-docs-recommended (TypeScript)', rule, {
valid: [
`
export default createESLintRule<Options, MessageIds>({
meta: { docs: { recommended: true } },
create(context) {}
});
`,
],
invalid: [
{
code: `
export default createESLintRule<Options, MessageIds>({
meta: {},
create(context) {}
});
`,
output: null,
errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
},
],
});