Skip to content

feat: Add new rule no-meta-replaced-by #518

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | ✅ | | | |
Expand Down
62 changes: 62 additions & 0 deletions docs/rules/no-meta-replaced-by.md
Copy link
Member

@bmish bmish Jun 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be appropriate to write

## When Not To Use It

If you need to support versions of ESLint prior to v9.

Regarding when not to use it, I don't think we necessarily need to include this section. In fact, anyone can use this rule regardless of ESLint version. As far as I know, ESLint itself does not validate nor use the meta.deprecated property. So anybody can use the object format for meta.deprecated.

Note that if someone is using TypeScript to define their rules, they would want to make sure they have up-to-date rule types (which I believe come from @types/eslint or typescript-eslint). And rule authors would want to make sure they are using up-to-date versions of any third-party tooling like eslint-doc-generator that use this property.

OR, you could include the section and just state the context I've mentioned here.

Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Disallow using the `meta.replacedBy` rule property (`eslint-plugin/no-meta-replaced-by`)

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

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)
63 changes: 63 additions & 0 deletions lib/rules/no-meta-replaced-by.js
Original file line number Diff line number Diff line change
@@ -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: {
Copy link
Member

@bmish bmish Jun 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also add an autofixer to this rule to move meta.replacedBy to meta.deprecated.replacedBy. A follow-up to do that would be fine since it could be tricky to get it right.

Copy link
Contributor Author

@error-four-o-four error-four-o-four Jun 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thought so too. But I'd have to get down to the nitty-gritty of eslint and ASTs. The autofixer should be added in a follow-up.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another fix in a follow-up might be to remove replacedBy from the defaultOrder in meta-property-order, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also thinking about the neccessity and possibilities so that the autofixer is able to add the property plugin to the replacement rule according to ReplacedByInfo

If this is not the desired behaviour of the autofixer it would simply convert the items of the old replacedBy array to the new format e.g. [{ rule: { name: 'the-new-rule' } }]

If it is the desired behaviour one would have to determine and compare the prefix of the plugin. This could be achieved by either providing an option in no-meta-replaced-by or reading the value of an exported flat config which might be trickier and not reliable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another fix in a follow-up might be to remove replacedBy from the defaultOrder in meta-property-order, right?

I think we can leave it. Doesn't hurt to continue to support the older rule properties.

Copy link
Member

@bmish bmish Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the autofixer, it would be nice if the autofixer can work without any configuration. But if there's an option that can be provided to the rule to improve the autofixer further, I'm open to that as an added bonus.

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',
});
},
};
},
};
139 changes: 139 additions & 0 deletions tests/lib/rules/no-meta-replaced-by.js
Original file line number Diff line number Diff line change
@@ -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),
});