diff --git a/README.md b/README.md index 32f86899..f6eeb235 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ module.exports = [ | [meta-property-ordering](docs/rules/meta-property-ordering.md) | enforce the order of meta properties | | 🔧 | | | | [no-deprecated-context-methods](docs/rules/no-deprecated-context-methods.md) | disallow usage of deprecated methods on rule context objects | ✅ | 🔧 | | | | [no-deprecated-report-api](docs/rules/no-deprecated-report-api.md) | disallow the version of `context.report()` with multiple arguments | ✅ | 🔧 | | | +| [no-meta-replaced-by](docs/rules/no-meta-replaced-by.md) | disallow using the `meta.replacedBy` rule property | | | | | | [no-meta-schema-default](docs/rules/no-meta-schema-default.md) | disallow rules `meta.schema` properties to include defaults | | | | | | [no-missing-message-ids](docs/rules/no-missing-message-ids.md) | disallow `messageId`s that are missing from `meta.messages` | ✅ | | | | | [no-missing-placeholders](docs/rules/no-missing-placeholders.md) | disallow missing placeholders in rule report messages | ✅ | | | | diff --git a/docs/rules/no-meta-replaced-by.md b/docs/rules/no-meta-replaced-by.md new file mode 100644 index 00000000..070fa5ef --- /dev/null +++ b/docs/rules/no-meta-replaced-by.md @@ -0,0 +1,62 @@ +# Disallow using the `meta.replacedBy` rule property (`eslint-plugin/no-meta-replaced-by`) + + + +As of ESLint v9.21.0, the rule property `meta.deprecated` can be either a boolean or an object of type `DeprecatedInfo`. The `DeprecatedInfo` type includes an optional `replacedBy` array that replaces the now-deprecated `meta.replacedBy` property. + +Examples of correct usage: + +- [array-bracket-newline](https://github.com/eslint/eslint/blob/4112fd09531092e9651e9981205bcd603dc56acf/lib/rules/array-bracket-newline.js#L18-L38) +- [typescript-eslint/no-empty-interface](https://github.com/typescript-eslint/typescript-eslint/blob/af94f163a1d6447a84c5571fff5e38e4c700edb9/packages/eslint-plugin/src/rules/no-empty-interface.ts#L19-L30) + +## Rule Details + +This rule disallows the `meta.replacedBy` property in a rule. + +Examples of **incorrect** code for this rule: + +```js +/* eslint eslint-plugin/no-meta-replaced-by: error */ + +module.exports = { + meta: { + deprecated: true, + replacedBy: ['the-new-rule'], + }, + create() {}, +}; +``` + +Examples of **correct** code for this rule: + +```js +/* eslint eslint-plugin/no-meta-replaced-by: error */ + +module.exports = { + meta: { + deprecated: { + message: 'The new rule adds more functionality', + replacedBy: [ + { + rule: { + name: 'the-new-rule', + }, + }, + ], + }, + }, + create() {}, +}; + +module.exports = { + meta: { + deprecated: true, + }, + create() {}, +}; +``` + +## Further Reading + +- [ESLint docs: `DeprecatedInfo`](https://eslint.org/docs/latest/extend/rule-deprecation#-deprecatedinfo-type) +- [RFC introducing `DeprecatedInfo` type](https://github.com/eslint/rfcs/tree/main/designs/2024-deprecated-rule-metadata) diff --git a/lib/rules/no-meta-replaced-by.js b/lib/rules/no-meta-replaced-by.js new file mode 100644 index 00000000..36aa1297 --- /dev/null +++ b/lib/rules/no-meta-replaced-by.js @@ -0,0 +1,63 @@ +/** + * @fileoverview Disallows the usage of `meta.replacedBy` property + */ + +'use strict'; + +const utils = require('../utils'); + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'disallow using the `meta.replacedBy` rule property', + category: 'Rules', + recommended: false, + url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-meta-replaced-by.md', + }, + schema: [], + messages: { + useNewFormat: + 'Use `meta.deprecated.replacedBy` instead of `meta.replacedBy`', + }, + }, + create(context) { + const sourceCode = utils.getSourceCode(context); + const ruleInfo = utils.getRuleInfo(sourceCode); + + if (!ruleInfo) { + return {}; + } + + return { + Program() { + const metaNode = ruleInfo.meta; + + if (!metaNode) { + return; + } + + const replacedByNode = utils + .evaluateObjectProperties(metaNode, sourceCode.scopeManager) + .find( + (p) => + p.type === 'Property' && utils.getKeyName(p) === 'replacedBy', + ); + + if (!replacedByNode) { + return; + } + + context.report({ + node: replacedByNode, + messageId: 'useNewFormat', + }); + }, + }; + }, +}; diff --git a/tests/lib/rules/no-meta-replaced-by.js b/tests/lib/rules/no-meta-replaced-by.js new file mode 100644 index 00000000..d72f41ac --- /dev/null +++ b/tests/lib/rules/no-meta-replaced-by.js @@ -0,0 +1,139 @@ +/** + * @fileoverview Disallows the usage of `meta.replacedBy` property + */ + +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/no-meta-replaced-by'); +const RuleTester = require('../eslint-rule-tester').RuleTester; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const valid = [ + 'module.exports = {};', + ` + module.exports = { + create(context) {}, + }; + `, + ` + module.exports = { + meta: {}, + create(context) {}, + }; + `, + ` + module.exports = { + meta: { + deprecated: true, + }, + create(context) {}, + }; + `, + { + code: ` + module.exports = { + meta: { + deprecated: { + replacedBy: [ + { + rule: { + name: 'foo', + }, + }, + ], + }, + }, + create(context) {}, + }; + `, + errors: 0, + }, +]; + +const invalid = [ + { + code: ` + module.exports = { + meta: { + replacedBy: ['the-new-rule'], + }, + create(context) {}, + }; + `, + errors: [ + { + messageId: 'useNewFormat', + line: 4, + endLine: 4, + }, + ], + }, + { + code: ` + const meta = { + replacedBy: null, + }; + + module.exports = { + meta, + create(context) {}, + }; + `, + errors: [ + { + messageId: 'useNewFormat', + line: 3, + endLine: 3, + }, + ], + }, + { + code: ` + const spread = { + replacedBy: null, + }; + + module.exports = { + meta: { + ...spread, + }, + create(context) {}, + }; + `, + errors: [{ messageId: 'useNewFormat' }], + }, +]; + +const testToESM = (test) => { + if (typeof test === 'string') { + return test.replace('module.exports =', 'export default'); + } + + const code = test.code.replace('module.exports =', 'export default'); + + return { + ...test, + code, + }; +}; + +new RuleTester({ + languageOptions: { sourceType: 'commonjs' }, +}).run('no-meta-replaced-by', rule, { + valid, + invalid, +}); + +new RuleTester({ + languageOptions: { sourceType: 'module' }, +}).run('no-meta-replaced-by', rule, { + valid: valid.map(testToESM), + invalid: invalid.map(testToESM), +});