From bc0ed7c4662c43a685e60a36d9d5e79a871a69c1 Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Thu, 12 Sep 2024 23:39:41 +0200 Subject: [PATCH 1/2] feat: additional rule metadata for deprecations --- .../2024-deprecated-rule-metadata/README.md | 281 ++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 designs/2024-deprecated-rule-metadata/README.md diff --git a/designs/2024-deprecated-rule-metadata/README.md b/designs/2024-deprecated-rule-metadata/README.md new file mode 100644 index 00000000..f404ed15 --- /dev/null +++ b/designs/2024-deprecated-rule-metadata/README.md @@ -0,0 +1,281 @@ +- Repo: eslint/eslint +- Start Date: 2024-02-20 +- RFC PR: +- Authors: [bmish](https://github.com/bmish), [DMartens](https://github.com/DMartens) + +# Support additional metadata for rule deprecations + +## Summary + + + +This RFC suggests a format for storing additional information in rule metadata about rule deprecations and replacement rules, allowing tooling (e.g. documentation generators) to generate more informative deprecation notices. + +## Motivation + + + +There are long-time [rule meta properties](https://eslint.org/docs/latest/extend/custom-rules#rule-structure) `meta.deprecated` and `meta.replacedBy` that have been intended to document when rules are deprecated and what their replacement rule(s) are. For the most part, usage would look something like this: + +```js +module.exports = { meta: { deprecated: true, replacedBy: ['replacement-rule-name'] } }; +``` + +These properties are often used for generating plugin/rule documentation websites and in documentation tooling like [eslint-doc-generator](https://github.com/bmish/eslint-doc-generator). + +But there are some limitations to this current format: + +- Simply providing the replacement rule name as a string doesn't yield much context/explanation of the replacement/deprecation. That means documentation tooling / websites and code editors can only generate limited information to present about the situation. +- Some rules provide the replacement rule name with the plugin prefix as in `prefix/rule-name` while others just provide it as `rule-name`, which can result in ambiguity about whether the replacement rule is in the same plugin, a different third-party plugin, or ESLint core. And for third-party plugins, there's no easy way to automatically determine where their documentation is located or how to link to them. + +## Detailed Design + + + +We propose to extend `meta.deprecated` rule property schemas to reduce ambiguity and allow additional key details to be represented, described below using TypeScript types for clarity: + +```ts +type RuleMeta = { + deprecated?: + | boolean // Existing boolean option, backwards compatible. + | string // Shorthand property for general deprecation message, such as why the deprecation occurred. Empty strings are forbidden as they are falsy. + | DeprecatedInfo // Proposed extension + + /** @deprecated */ + replacedBy?: string[] // Deprecate the top-level property and "move" into the "deprecated" object +}; + +/* At least one property is required */ +type DeprecateInfo = { + info?: Message + replacedBy?: (string|ReplacedByInfo)[] // An empty array explicitly states that there is no replacement +} + +/* At least one property is required */ +type ReplacedByInfo = { + plugin?: string | Specifier // name should be "eslint" if the replacemenet is an ESLint core rule. Omit the property if the replacement is in the same plugin + rule?: string | Specifier + info?: string | Message + kind?: ReplacementKind // Defaults to "moved" if missing + deprecatedSince?: Version // Helps users gauge when to migrate and useful for documentation + availableUntil?: Version | null // The estimated version when the rule is removed (probably the next major version). null means the rule is "frozen" (will be available but will not be changed) +} + +type Message = { + message: string // General message presented to the user. Content depends on the property (e.g. for the key rule why the rule is deprecated or for info how to replace the rule) + url: // URL to more information about this deprecation in general. +} + +type Specifier = { + name: string // Name of the rule / configuration / ... + url: string // URL to more information about this deprecation in general. +} + +type ReplacementKind = + 'moved' | // The rule has moved to another plugin if plugin is set, otherwise the rule is renamed in the same plugin + 'merged' | // The rule merged with another rule + 'option' // The current rule behavior is available as an option in the replacement rule + +/* Version string of the package containing the rule */ +type Version = string +``` + +The `meta.replacedBy` property is moved into the `meta.deprecated` property as `meta.replacedBy` requires `meta.deprecated` to be set. +The reason for this is that a rule logically must be marked as deprecated to be replaced by another rule which it currently can be. + +### Example +Real-world example of how this could be used based on the situation in : + +```js +// lib/rules/semi.js +module.exports = { + meta: { + deprecated: { + message: 'Stylistic rules are being moved out of ESLint core.', + url: 'https://eslint.org/blog/2023/10/deprecating-formatting-rules/', + replacedBy: [ + { + plugin: { + name: '@stylistic/js', + url: 'https://eslint.style/', + }, + rule: 'https://eslint.style/rules/js/semi', + }, + ], + }, + }, +}; +``` + + +This data could be used by documentation websites and tooling like [eslint-doc-generator](https://github.com/bmish/eslint-doc-generator) to generate notices and links like: + +> semi (deprecated) \ +> Replaced by [semi](https://eslint.style/rules/js/semi) from [@stylistic/js](https://eslint.style/). \ +> Use the `foo` option on the new rule to achieve the same behavior as before. [Read more](https://example.com/how-to-migrate-to-the-new-semi-rule). \ +> Stylistic rules are being moved out of ESLint core. [Read more](https://eslint.org/blog/2023/10/deprecating-formatting-rules/). + +We can also support the same `meta.deprecated` and `meta.replacedBy` properties on configurations and processors (the other kinds of objects exported by ESLint plugins), replacing `rule` with `config` or `processor` as needed. This would be part of the effort to standardize documentation properties in . + +### Shorthand +The shorthand for the properties `plugin`, `rule` and `info` is just a string representing either the `name`/`message` or the `url` based on its content. +If it starts with a protocol (e.g. `https://`) the property should be interpreted as if only the `url` property is set, otherwise it should be interpreted as `name`/`message` property. +This shorthand also applies for the existing `meta.deprecated` which then applies for the `meta.deprecated.info` properties. +Some examples: +```js +{ meta: { deprecated: { plugin: 'https://eslint.style' } } } // <=> { meta: { deprecated: { plugin: { url: 'https://eslint.style' } } } } +{ meta: { deprecated: { plugin: '@eslint-stylistic/js' } } } // <=> { meta: { deprecated: { plugin: { name: '@eslint-stylistic/js' } } } } +{ meta: { deprecated: 'https://eslint.style/guide/migration' } // <=> { meta: { deprecated: { info: { url: 'https://eslint.style/guide/migration' } } } } +``` + +### Changes +In terms of actual changes inside ESLint needed for this: + +- Mention the new schema in the [custom rule documentation](https://eslint.org/docs/latest/extend/custom-rules#rule-structure) +- Ensure these properties are allowed on configurations, parsers and processors +- Add any additional information to these properties in core rules as desired (such as in ) +- Update ESLint's website generator to take into account the additional information for rule doc deprecation notices +- Update [LintResult.usedDeprecatedRules](https://github.com/eslint/eslint/blob/0f5df509a4bc00cff2c62b90fab184bdf0231322/lib/eslint/eslint.js#L197-L211) + +External changes: + +- Update the [types](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/b77d83e019025017b06953907cb77f35e4231714/types/eslint/index.d.ts#L734) in @types/eslint +- Update the [types](https://github.com/typescript-eslint/typescript-eslint/blob/82cb9dd580f62644ed988fd2bf27f519177a60bd/packages/utils/src/ts-eslint/Rule.ts#L70) in @typescript-eslint/eslint +- Update eslint-doc-generator to handle the new information: +- Update the metadata for the most common plugins +- Consider implementing an [eslint-plugin-eslint-plugin](https://github.com/eslint-community/eslint-plugin-eslint-plugin) rule to encourage more complete deprecation information to be stored in these properties + +## Documentation + + + +We don't necessarily need a formal announcement for this. The aforementioned changes to the rule documentation page and types should be sufficient. + +However, this update could be covered in a blog post about general rule documentation best practices, if anyone ever has an interest in writing something like that. + +## Drawbacks + + + +There are some limited [backwards compatibility](#backwards-compatibility-analysis) concerns for third-party tooling. + +## Backwards Compatibility Analysis + + + +Existing rules will continue to be backwards-compatible with the new format. + +Changing the format of these properties mainly affects third-party documentation tooling and websites that use this information, and not ESLint users nor ESLint plugins directly. + +For the most part, the new `meta.deprecated` format should be backwards-compatible, as code is often written to check simply for a truthy value inside of `meta.deprecated`, e.g. `if (rule.meta.deprecated) { /* ... */ }`, which will continue to work as expected. The code needs to be updated if: +- it checks specifically for the boolean `true` value in `meta.deprecated` +- it checks for whether the rule is deprecated by checking for a non-empty `meta.replacedBy` +- retrieves rule names from `meta.replacedBy` + +Overall, a limited number of third-party tools that might be affected, and these should be trivial to fix when impacts are discovered. + +We do not need to consider this to be a breaking change in terms of [ESLint's semantic versioning policy](https://github.com/eslint/eslint#semantic-versioning-policy). + +## Alternatives + + + +### Do nothing + +This would leave the current `meta.deprecated` and `meta.replacedBy` properties as they are, which would continue to be ambiguous and limited in the information they can provide. + +### Create a new property + +Create a new property, e.g. `meta.deprecation`, + +## Open Questions + + + +1. Is there additional deprecation information we'd like to represent? Note that additional information can always be added later, but it's good to consider any possible needs now. +2. Should `meta.deprecated.plugin.id` accommodate different package registries (e.g. [jsr](https://jsr.io/) with `jsr:eslint-plugin-example`) +3. Should the exact regular expression for the shorthand which decides whether it is a description or URL be specified? +4. Should the shorthand also be applied for the string form of the `meta.deprecated` property? +5. Which "extension points" (rules, processors, configurations, parsers, formatters) shold be supported? +6. Should the `rule` key be dependent on the "extension point" (e.g. `processor` for processors) or renamed (e.g. ``) so that it is the same property name for all? + +## Help Needed + + + +I should be able to handle the minimal changes needed in ESLint core, and can kick off some of the changes needed in community projects. + +## Frequently Asked Questions + +- Why not provide a property to describe how to migrate to the replacement rule which requires an option to be set? + - The options of the replacement rule could change and it is unlikely that a deprecated rules gets updated to accommodate the change + + + +## Related Discussions + + + +- [The original RFC](https://github.com/eslint/rfcs/pull/116) +- [The issue triggering this RFC](https://github.com/eslint/eslint/issues/18061) +- Inspirations + - + - +- Related + - + - + - From 83f99acf1fd05f082a79448678980e0c8053a9be Mon Sep 17 00:00:00 2001 From: Daniel Martens Date: Wed, 18 Sep 2024 12:00:43 +0200 Subject: [PATCH 2/2] fix: move deprecatedSince and availableUntil to the root deprecated object --- designs/2024-deprecated-rule-metadata/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/designs/2024-deprecated-rule-metadata/README.md b/designs/2024-deprecated-rule-metadata/README.md index f404ed15..d83b067e 100644 --- a/designs/2024-deprecated-rule-metadata/README.md +++ b/designs/2024-deprecated-rule-metadata/README.md @@ -57,6 +57,8 @@ type RuleMeta = { type DeprecateInfo = { info?: Message replacedBy?: (string|ReplacedByInfo)[] // An empty array explicitly states that there is no replacement + deprecatedSince?: Version // Helps users gauge when to migrate and useful for documentation + availableUntil?: Version | null // The estimated version when the rule is removed (probably the next major version). null means the rule is "frozen" (will be available but will not be changed) } /* At least one property is required */ @@ -65,8 +67,6 @@ type ReplacedByInfo = { rule?: string | Specifier info?: string | Message kind?: ReplacementKind // Defaults to "moved" if missing - deprecatedSince?: Version // Helps users gauge when to migrate and useful for documentation - availableUntil?: Version | null // The estimated version when the rule is removed (probably the next major version). null means the rule is "frozen" (will be available but will not be changed) } type Message = {