diff --git a/docs/examples/eslint-plugin-test/docs/rules/require-baz.md b/docs/examples/eslint-plugin-test/docs/rules/require-baz.md index ab788430..8bf1779e 100644 --- a/docs/examples/eslint-plugin-test/docs/rules/require-baz.md +++ b/docs/examples/eslint-plugin-test/docs/rules/require-baz.md @@ -2,7 +2,7 @@ 📝 Require using baz. -❌ This rule is deprecated. It was replaced by [`test/prefer-bar`](prefer-bar.md). +❌ This rule has been [deprecated](https://example.com) since v1.0.0 and will be available until v2.0.0. It was replaced by [`test/prefer-bar`](prefer-bar.md) (custom message about this replacement) ([read more](https://example.com)). Custom message about overall deprecation. 🚫 This rule is _disabled_ in the ⌨️ `typescript` config. diff --git a/lib/rule-doc-notices.ts b/lib/rule-doc-notices.ts index 2d8b44ba..8a3e7eed 100644 --- a/lib/rule-doc-notices.ts +++ b/lib/rule-doc-notices.ts @@ -12,12 +12,16 @@ import { import { findConfigEmoji, getConfigsForRule } from './plugin-configs.js'; import { getPluginRoot } from './package-json.js'; import { SEVERITY_TYPE, NOTICE_TYPE } from './types.js'; -import type { RuleModule } from './types.js'; +import type { RuleModule, DeprecatedInfo, ReplacedByInfo } from './types.js'; import type { RULE_TYPE } from './rule-type.js'; import { RULE_TYPE_MESSAGES_NOTICES } from './rule-type.js'; import type { RuleDocTitleFormat } from './rule-doc-title-format.js'; import { hasOptions } from './rule-options.js'; -import { getLinkToRule, replaceRulePlaceholder } from './rule-link.js'; +import { + getLinkToRule, + getMarkdownLink, + replaceRulePlaceholder, +} from './rule-link.js'; import { toSentenceCase, removeTrailingPeriod, @@ -76,6 +80,59 @@ function configsToNoticeSentence( return sentence; } +/** + * Build the "It was replaced by ..." sentence from DeprecatedInfo.replacedBy entries. + * Entries without a rule name are silently skipped. + */ +function replacedByToNoticeSentence( + context: Context, + deprecated: DeprecatedInfo, + pathCurrentPage: string, +): string | undefined { + if (!deprecated.replacedBy || deprecated.replacedBy.length === 0) { + return undefined; + } + + function formatReplacedByEntry(info: ReplacedByInfo): string | undefined { + if (!info.rule?.name) { + return undefined; + } + + // Prefer an explicit URL from the plugin maintainer; fall back to auto-generated link. + const replacementRule = info.rule.url + ? getMarkdownLink(info.rule.name, true, info.rule.url) + : getLinkToRule(context, info.rule.name, pathCurrentPage, true, true); + + // Only show "from " for external plugins (not ESLint core). + const externalPlugin = + info.plugin?.name && info.plugin.name !== 'eslint' + ? ` from ${getMarkdownLink(info.plugin.name, false, info.plugin.url)}` + : ''; + + return `${replacementRule}${externalPlugin}${ + info.message ? ` (${info.message})` : '' + }${info.url ? ` (${getMarkdownLink('read more', false, info.url)})` : ''}`; + } + + // Filter first, then apply conjunction, so "and" targets the correct item. + const parts = deprecated.replacedBy + .map((item) => formatReplacedByEntry(item)) + .filter((part): part is string => part !== undefined); + + if (parts.length === 0) { + return undefined; + } + + if (parts.length > 1) { + const lastIndex = parts.length - 1; + // Safe: lastIndex is guaranteed valid since parts.length > 1. + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + parts[lastIndex] = `and ${parts[lastIndex]}`; + } + + return `It was replaced by ${parts.join(', ')}.`; +} + // A few individual notices declared here just so they can be reused in multiple notices. const NOTICE_FIXABLE = `${EMOJI_FIXABLE} This rule is automatically fixable by the [\`--fix\` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).`; const NOTICE_HAS_SUGGESTIONS = `${EMOJI_HAS_SUGGESTIONS} This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).`; @@ -97,6 +154,7 @@ const RULE_NOTICES: { description?: string; fixable: boolean; hasSuggestions: boolean; + deprecated: boolean | DeprecatedInfo | undefined; replacedBy: readonly string[] | undefined; path: string; type?: RULE_TYPE; @@ -170,16 +228,49 @@ const RULE_NOTICES: { return `${emojis.join('')} ${sentences}`; }, - // Deprecated notice has optional "replaced by" rules list. - [NOTICE_TYPE.DEPRECATED]: ({ context, replacedBy, ruleName }) => { + [NOTICE_TYPE.DEPRECATED]: ({ context, deprecated, replacedBy, ruleName }) => { const { options, path } = context; const { pathRuleDoc } = options; - // pathCurrentPage must be an absolute path for relative() to work correctly in getLinkToRule. + // Derive the path of the *deprecated* rule's doc page (not the replacement's) so that + // relative() in getLinkToRule produces correct links from this page to replacements. const pathCurrentPage = join( getPluginRoot(path), replaceRulePlaceholder(pathRuleDoc, ruleName), ); + // New DeprecatedInfo object format (ESLint >=9.21.0) vs legacy boolean format. + if (typeof deprecated === 'object') { + function formatVersion(version: string) { + return version.startsWith('v') ? version : `v${version}`; + } + + const sentenceDeprecated = `${EMOJI_DEPRECATED} This rule ${ + deprecated.deprecatedSince ? 'has been' : 'is' + } ${deprecated.url ? `[deprecated](${deprecated.url})` : 'deprecated'}${ + deprecated.deprecatedSince + ? ` since ${formatVersion(deprecated.deprecatedSince)}` + : '' + }${ + deprecated.availableUntil + ? ` and will be available until ${formatVersion(deprecated.availableUntil)}` + : '' + }.`; + + const sentenceReplacedBy = replacedByToNoticeSentence( + context, + deprecated, + pathCurrentPage, + ); + + const deprecatedMessage = deprecated.message + ? addTrailingPeriod(deprecated.message) + : undefined; + + return [sentenceDeprecated, sentenceReplacedBy, deprecatedMessage] + .filter(Boolean) + .join(' '); + } + const replacementRuleList = (replacedBy ?? []).map( (replacementRuleName) => { return getLinkToRule( @@ -337,6 +428,7 @@ function getRuleNoticeLines(context: Context, ruleName: string) { configsWarn, configsOff, ); + let noticeType: keyof typeof notices; for (noticeType in notices) { @@ -369,6 +461,7 @@ function getRuleNoticeLines(context: Context, ruleName: string) { ...(description !== undefined && { description }), fixable: Boolean(rule.meta?.fixable), hasSuggestions: Boolean(rule.meta?.hasSuggestions), + deprecated: rule.meta?.deprecated, // eslint-disable-next-line @typescript-eslint/no-deprecated replacedBy: rule.meta?.replacedBy, path, diff --git a/lib/rule-link.ts b/lib/rule-link.ts index 15f6c5c9..40baaf7d 100644 --- a/lib/rule-link.ts +++ b/lib/rule-link.ts @@ -75,6 +75,16 @@ export function getUrlToRule( ); } +export function getMarkdownLink( + text: string, + includeBackticks: boolean, + url?: string, +) { + const displayedText = includeBackticks ? `\`${text}\`` : text; + + return url ? `[${displayedText}](${url})` : displayedText; +} + /** * Get the markdown link (title and URL) to the rule's documentation. */ @@ -110,11 +120,10 @@ export function getLinkToRule( const urlToRule = getUrlToRule(context, ruleName, ruleSource, pathToFile); - const ruleNameToDisplay = `${includeBackticks ? '`' : ''}${ + const ruleString = includePrefix && ruleNameWithPluginPrefix ? ruleNameWithPluginPrefix - : ruleNameWithoutPluginPrefix - }${includeBackticks ? '`' : ''}`; + : ruleNameWithoutPluginPrefix; - return urlToRule ? `[${ruleNameToDisplay}](${urlToRule})` : ruleNameToDisplay; + return getMarkdownLink(ruleString, includeBackticks, urlToRule); } diff --git a/lib/types.ts b/lib/types.ts index 49fde641..e1cb10db 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -10,6 +10,10 @@ export type Rules = TSESLint.Linter.RulesRecord; export type RuleSeverity = TSESLint.Linter.RuleLevel; +export type DeprecatedInfo = TSESLint.DeprecatedInfo; + +export type ReplacedByInfo = TSESLint.ReplacedByInfo; + // eslint-disable-next-line @typescript-eslint/no-deprecated export type Config = TSESLint.Linter.Config; diff --git a/test/lib/generate/__snapshots__/rule-deprecation-test.ts.snap b/test/lib/generate/__snapshots__/rule-deprecation-test.ts.snap index d95b6918..cd52d37a 100644 --- a/test/lib/generate/__snapshots__/rule-deprecation-test.ts.snap +++ b/test/lib/generate/__snapshots__/rule-deprecation-test.ts.snap @@ -1,13 +1,48 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`generate (deprecated rules) > DeprecatedInfo with only deprecatedSince (no availableUntil) > displays deprecatedSince without availableUntil 1`] = ` +"# test/no-foo + +📝 Description. + +❌ This rule has been deprecated since v3.0.0. + + +" +`; + +exports[`generate (deprecated rules) > DeprecatedInfo with only invalid replacedBy entries preceding a valid one > does not produce an erroneous "and" prefix when only one valid replacement remains 1`] = ` +"# test/no-foo + +📝 Description. + +❌ This rule is deprecated. It was replaced by [\`test/no-bar\`](no-bar.md). + + +" +`; + +exports[`generate (deprecated rules) > DeprecatedInfo with v-prefixed version strings > does not double the v prefix 1`] = ` +"# test/no-foo + +📝 Description. + +❌ This rule has been deprecated since v7.0.0 and will be available until v13.0.0. + + +" +`; + exports[`generate (deprecated rules) > replaced by ESLint core rule > uses correct replacement rule link 1`] = ` " ❌ Deprecated. -| Name | Description | ❌ | -| :----------------------------- | :----------- | :- | -| [no-foo](docs/rules/no-foo.md) | Description. | ❌ | +| Name | Description | ❌ | +| :------------------------------------- | :----------- | :- | +| [no-foo](docs/rules/no-foo.md) | Description. | ❌ | +| [prefer-bar](docs/rules/prefer-bar.md) | Description. | ❌ | +| [prefer-foo](docs/rules/prefer-foo.md) | Description. | ❌ | " `; @@ -23,14 +58,38 @@ exports[`generate (deprecated rules) > replaced by ESLint core rule > uses corre " `; +exports[`generate (deprecated rules) > replaced by ESLint core rule > uses correct replacement rule link 3`] = ` +"# test/prefer-foo + +📝 Description. + +❌ This rule is deprecated. It was replaced by [\`no-unused-vars\`](https://eslint.org/docs/latest/rules/no-unused-vars). + + +" +`; + +exports[`generate (deprecated rules) > replaced by ESLint core rule > uses correct replacement rule link 4`] = ` +"# test/prefer-bar + +📝 Description. + +❌ This rule is deprecated. It was replaced by [\`no-unused-vars\`](https://eslint.org/docs/latest/rules/no-unused-vars). + + +" +`; + exports[`generate (deprecated rules) > replaced by third-party plugin rule > uses correct replacement rule link 1`] = ` " ❌ Deprecated. -| Name | Description | ❌ | -| :----------------------------- | :----------- | :- | -| [no-foo](docs/rules/no-foo.md) | Description. | ❌ | +| Name | Description | ❌ | +| :------------------------------------- | :----------- | :- | +| [no-foo](docs/rules/no-foo.md) | Description. | ❌ | +| [prefer-bar](docs/rules/prefer-bar.md) | Description. | ❌ | +| [prefer-foo](docs/rules/prefer-foo.md) | Description. | ❌ | " `; @@ -46,14 +105,37 @@ exports[`generate (deprecated rules) > replaced by third-party plugin rule > use " `; +exports[`generate (deprecated rules) > replaced by third-party plugin rule > uses correct replacement rule link 3`] = ` +"# test/prefer-foo + +📝 Description. + +❌ This rule is deprecated. It was replaced by \`other-plugin/no-unused-vars\`. + + +" +`; + +exports[`generate (deprecated rules) > replaced by third-party plugin rule > uses correct replacement rule link 4`] = ` +"# test/prefer-bar + +📝 Description. + +❌ This rule is deprecated. It was replaced by \`other-plugin/no-unused-vars\` from [eslint-plugin-other-plugin](https://example.org/other-plugin). + + +" +`; + exports[`generate (deprecated rules) > replaced by third-party plugin rule with same rule name as one of our rules > uses correct replacement rule link 1`] = ` " ❌ Deprecated. -| Name | Description | ❌ | -| :----------------------------- | :----------- | :- | -| [no-foo](docs/rules/no-foo.md) | Description. | ❌ | +| Name | Description | ❌ | +| :------------------------------------- | :----------- | :- | +| [no-foo](docs/rules/no-foo.md) | Description. | ❌ | +| [prefer-foo](docs/rules/prefer-foo.md) | Description. | ❌ | " `; @@ -69,18 +151,31 @@ exports[`generate (deprecated rules) > replaced by third-party plugin rule with " `; +exports[`generate (deprecated rules) > replaced by third-party plugin rule with same rule name as one of our rules > uses correct replacement rule link 3`] = ` +"# test/prefer-foo + +📝 Description. + +❌ This rule is deprecated. It was replaced by \`other-plugin/prefer-foo\`. + + +" +`; + exports[`generate (deprecated rules) > several deprecated rules > updates the documentation 1`] = ` " ❌ Deprecated. -| Name | Description | ❌ | -| :----------------------------- | :----------- | :- | -| [no-bar](docs/rules/no-bar.md) | Description. | ❌ | -| [no-baz](docs/rules/no-baz.md) | Description. | ❌ | -| [no-biz](docs/rules/no-biz.md) | Description. | | -| [no-boz](docs/rules/no-boz.md) | Description. | ❌ | -| [no-foo](docs/rules/no-foo.md) | Description. | ❌ | +| Name | Description | ❌ | +| :------------------------------------- | :----------- | :- | +| [no-bar](docs/rules/no-bar.md) | Description. | ❌ | +| [no-baz](docs/rules/no-baz.md) | Description. | ❌ | +| [no-biz](docs/rules/no-biz.md) | Description. | | +| [no-boz](docs/rules/no-boz.md) | Description. | ❌ | +| [no-foo](docs/rules/no-foo.md) | Description. | ❌ | +| [prefer-bar](docs/rules/prefer-bar.md) | Description. | ❌ | +| [prefer-foo](docs/rules/prefer-foo.md) | Description. | ❌ | " `; @@ -138,15 +233,38 @@ exports[`generate (deprecated rules) > several deprecated rules > updates the do " `; +exports[`generate (deprecated rules) > several deprecated rules > updates the documentation 7`] = ` +"# test/prefer-foo + +📝 Description. + +❌ This rule has been [deprecated](https://example.org/blog/non-existant) since v1.0.0 and will be available until v2.0.0. Custom message about overall deprecation. + + +" +`; + +exports[`generate (deprecated rules) > several deprecated rules > updates the documentation 8`] = ` +"# test/prefer-bar + +📝 Description. + +❌ This rule is deprecated. It was replaced by [\`test/no-bar\`](no-bar.md), and [\`no-baz\`](https://example.org/rules/no-baz.md) (Custom message) ([read more](https://example.org/an-external-url)). + + +" +`; + exports[`generate (deprecated rules) > using prefix ahead of replacement rule name > uses correct replacement rule link 1`] = ` " ❌ Deprecated. -| Name | Description | ❌ | -| :----------------------------- | :----------- | :- | -| [no-bar](docs/rules/no-bar.md) | Description. | | -| [no-foo](docs/rules/no-foo.md) | Description. | ❌ | +| Name | Description | ❌ | +| :------------------------------------- | :----------- | :- | +| [no-bar](docs/rules/no-bar.md) | Description. | | +| [no-foo](docs/rules/no-foo.md) | Description. | ❌ | +| [prefer-foo](docs/rules/prefer-foo.md) | Description. | ❌ | " `; @@ -171,15 +289,28 @@ exports[`generate (deprecated rules) > using prefix ahead of replacement rule na " `; +exports[`generate (deprecated rules) > using prefix ahead of replacement rule name > uses correct replacement rule link 4`] = ` +"# test/prefer-foo + +📝 Description. + +❌ This rule is deprecated. It was replaced by [\`test/no-bar\`](no-bar.md). + + +" +`; + exports[`generate (deprecated rules) > with --path-rule-doc > has the correct links, especially replacement rule link 1`] = ` " ❌ Deprecated. -| Name | Description | ❌ | -| :------------------------------------------------ | :----------- | :- | -| [category/no-bar](docs/category/no-bar/README.md) | Description. | ❌ | -| [category/no-foo](docs/category/no-foo/README.md) | Description. | ❌ | +| Name | Description | ❌ | +| :-------------------------------------------------------- | :----------- | :- | +| [category/no-bar](docs/category/no-bar/README.md) | Description. | ❌ | +| [category/no-foo](docs/category/no-foo/README.md) | Description. | ❌ | +| [category/prefer-bar](docs/category/prefer-bar/README.md) | Description. | ❌ | +| [category/prefer-foo](docs/category/prefer-foo/README.md) | Description. | ❌ | " `; @@ -206,15 +337,39 @@ exports[`generate (deprecated rules) > with --path-rule-doc > has the correct li " `; +exports[`generate (deprecated rules) > with --path-rule-doc > has the correct links, especially replacement rule link 4`] = ` +"# test/category/prefer-foo + +📝 Description. + +❌ This rule is deprecated. It was replaced by [\`test/category/prefer-bar\`](../prefer-bar/README.md). + + +" +`; + +exports[`generate (deprecated rules) > with --path-rule-doc > has the correct links, especially replacement rule link 5`] = ` +"# test/category/prefer-bar + +📝 Description. + +❌ This rule is deprecated. It was replaced by [\`test/category/prefer-foo\`](../prefer-foo/README.md). + + +" +`; + exports[`generate (deprecated rules) > with nested rule names > has the correct links, especially replacement rule link 1`] = ` " ❌ Deprecated. -| Name | Description | ❌ | -| :----------------------------------------------- | :----------- | :- | -| [category/no-bar](docs/rules/category/no-bar.md) | Description. | ❌ | -| [category/no-foo](docs/rules/category/no-foo.md) | Description. | ❌ | +| Name | Description | ❌ | +| :------------------------------------------------------- | :----------- | :- | +| [category/no-bar](docs/rules/category/no-bar.md) | Description. | ❌ | +| [category/no-foo](docs/rules/category/no-foo.md) | Description. | ❌ | +| [category/prefer-bar](docs/rules/category/prefer-bar.md) | Description. | ❌ | +| [category/prefer-foo](docs/rules/category/prefer-foo.md) | Description. | ❌ | " `; @@ -241,6 +396,28 @@ exports[`generate (deprecated rules) > with nested rule names > has the correct " `; +exports[`generate (deprecated rules) > with nested rule names > has the correct links, especially replacement rule link 4`] = ` +"# test/category/prefer-foo + +📝 Description. + +❌ This rule is deprecated. It was replaced by [\`test/category/no-bar\`](no-bar.md). + + +" +`; + +exports[`generate (deprecated rules) > with nested rule names > has the correct links, especially replacement rule link 5`] = ` +"# test/category/prefer-bar + +📝 Description. + +❌ This rule is deprecated. It was replaced by [\`test/category/no-foo\`](no-foo.md). + + +" +`; + exports[`generate (deprecated rules) > with no rule doc but --ignore-deprecated-rules > omits the rule from the README and does not try to update its non-existent rule doc 1`] = ` " @@ -249,3 +426,50 @@ exports[`generate (deprecated rules) > with no rule doc but --ignore-deprecated- " `; + +exports[`generate (deprecated rules) > with the object type \`DeprecatedInfo\` > displays correct information 1`] = ` +" + +❌ Deprecated. + +| Name | Description | ❌ | +| :----------------------------- | :----------- | :- | +| [no-bar](docs/rules/no-bar.md) | Description. | ❌ | +| [no-baz](docs/rules/no-baz.md) | Description. | ❌ | +| [no-foo](docs/rules/no-foo.md) | Description. | ❌ | + +" +`; + +exports[`generate (deprecated rules) > with the object type \`DeprecatedInfo\` > displays correct information 2`] = ` +"# test/no-foo + +📝 Description. + +❌ This rule has been [deprecated](https://example.org/blog/non-existant) since v1.0.0 and will be available until v2.0.0. Custom message about overall deprecation. + + +" +`; + +exports[`generate (deprecated rules) > with the object type \`DeprecatedInfo\` > displays correct information 3`] = ` +"# test/no-bar + +📝 Description. + +❌ This rule is deprecated and will be available until v2.0.0. Custom message about overall deprecation. + + +" +`; + +exports[`generate (deprecated rules) > with the object type \`DeprecatedInfo\` > displays correct information 4`] = ` +"# test/no-baz + +📝 Description. + +❌ This rule is deprecated. It was replaced by [\`test/no-bar\`](no-bar.md), [\`no-baz\`](https://example.org/rules/no-baz.md), [\`test/no-baz\`](no-baz.md) (Custom message) ([read more](https://example.org/changelog.md)), and [\`@stylistic/indent\`](https://eslint.style/rules/indent) from [@sytlistic/eslint-plugin](https://eslint.style/). + + +" +`; diff --git a/test/lib/generate/rule-deprecation-test.ts b/test/lib/generate/rule-deprecation-test.ts index 63b4a772..0d512af5 100644 --- a/test/lib/generate/rule-deprecation-test.ts +++ b/test/lib/generate/rule-deprecation-test.ts @@ -50,6 +50,47 @@ describe('generate (deprecated rules)', function () { }, create(context) {} }, + 'prefer-foo': { + // With the object type 'DeprecatedInfo' + meta: { + docs: { description: 'Description.' }, + deprecated: { + message: 'Custom message about overall deprecation.', + deprecatedSince: '1.0.0', + availableUntil: '2.0.0', + url: 'https://example.org/blog/non-existant', + }, + }, + create(context) {} + }, + 'prefer-bar': { + // With the object type 'DeprecatedInfo' + meta: { + docs: { description: 'Description.' }, + deprecated: { + replacedBy: [ + { + // should not be present + rule: {} + }, + { + rule: { + name: 'no-bar', + } + }, + { + message: 'Custom message', + url: 'https://example.org/an-external-url', + rule: { + name: 'no-baz', + url: 'https://example.org/rules/no-baz.md' + } + }, + ] + }, + }, + create(context) {} + }, }, configs: {} };`, @@ -60,6 +101,8 @@ describe('generate (deprecated rules)', function () { 'docs/rules/no-baz.md': '', 'docs/rules/no-biz.md': '', 'docs/rules/no-boz.md': '', + 'docs/rules/prefer-foo.md': '', + 'docs/rules/prefer-bar.md': '', }, }); }); @@ -78,6 +121,12 @@ describe('generate (deprecated rules)', function () { expect(await fixture.readFile('docs/rules/no-baz.md')).toMatchSnapshot(); expect(await fixture.readFile('docs/rules/no-biz.md')).toMatchSnapshot(); expect(await fixture.readFile('docs/rules/no-boz.md')).toMatchSnapshot(); + expect( + await fixture.readFile('docs/rules/prefer-foo.md'), + ).toMatchSnapshot(); + expect( + await fixture.readFile('docs/rules/prefer-bar.md'), + ).toMatchSnapshot(); }); }); @@ -107,6 +156,36 @@ describe('generate (deprecated rules)', function () { }, create(context) {} }, + 'category/prefer-foo': { + meta: { + docs: { description: 'Description.' }, + deprecated: { + replacedBy: [ + { + rule: { + name: 'category/no-bar', // without plugin prefix + } + }, + ] + }, + }, + create(context) {} + }, + 'category/prefer-bar': { + meta: { + docs: { description: 'Description.' }, + deprecated: { + replacedBy: [ + { + rule: { + name: 'test/category/no-foo', // with plugin prefix + } + }, + ] + }, + }, + create(context) {} + }, }, configs: {} };`, @@ -114,6 +193,8 @@ describe('generate (deprecated rules)', function () { '', 'docs/rules/category/no-foo.md': '', 'docs/rules/category/no-bar.md': '', + 'docs/rules/category/prefer-foo.md': '', + 'docs/rules/category/prefer-bar.md': '', }, }); }); @@ -133,6 +214,12 @@ describe('generate (deprecated rules)', function () { expect( await fixture.readFile('docs/rules/category/no-bar.md'), ).toMatchSnapshot(); + expect( + await fixture.readFile('docs/rules/category/prefer-foo.md'), + ).toMatchSnapshot(); + expect( + await fixture.readFile('docs/rules/category/prefer-bar.md'), + ).toMatchSnapshot(); }); }); @@ -162,6 +249,35 @@ describe('generate (deprecated rules)', function () { }, create(context) {} }, + 'category/prefer-foo': { + meta: { + docs: { description: 'Description.' }, + deprecated: { + replacedBy: [ + { + rule: { + name: 'category/prefer-bar', // without plugin prefix + }, + }, + ], + }, + }, + }, + 'category/prefer-bar': { + meta: { + docs: { description: 'Description.' }, + deprecated: { + replacedBy: [ + { + rule: { + name: 'test/category/prefer-foo', // with plugin prefix + }, + }, + ], + }, + }, + create(context) {} + }, }, configs: {} };`, @@ -169,6 +285,8 @@ describe('generate (deprecated rules)', function () { '', 'docs/category/no-foo/README.md': '', 'docs/category/no-bar/README.md': '', + 'docs/category/prefer-foo/README.md': '', + 'docs/category/prefer-bar/README.md': '', }, }); }); @@ -188,6 +306,12 @@ describe('generate (deprecated rules)', function () { expect( await fixture.readFile('docs/category/no-bar/README.md'), ).toMatchSnapshot(); + expect( + await fixture.readFile('docs/category/prefer-foo/README.md'), + ).toMatchSnapshot(); + expect( + await fixture.readFile('docs/category/prefer-bar/README.md'), + ).toMatchSnapshot(); }); }); @@ -213,6 +337,21 @@ describe('generate (deprecated rules)', function () { meta: { docs: { description: 'Description.' }, }, create(context) {} }, + 'prefer-foo': { + meta: { + docs: { description: 'Description.' }, + deprecated: { + replacedBy: [ + { + rule: { + name: 'test/no-bar', + }, + }, + ], + }, + }, + create(context) {} + }, }, configs: {} };`, @@ -220,6 +359,7 @@ describe('generate (deprecated rules)', function () { '', 'docs/rules/no-foo.md': '', 'docs/rules/no-bar.md': '', + 'docs/rules/prefer-foo.md': '', }, }); }); @@ -235,6 +375,9 @@ describe('generate (deprecated rules)', function () { expect(await fixture.readFile('docs/rules/no-foo.md')).toMatchSnapshot(); expect(await fixture.readFile('docs/rules/no-bar.md')).toMatchSnapshot(); + expect( + await fixture.readFile('docs/rules/prefer-foo.md'), + ).toMatchSnapshot(); }); }); @@ -252,6 +395,14 @@ describe('generate (deprecated rules)', function () { meta: { deprecated: true, }, create(context) {} }, + 'no-bar': { + meta: { + deprecated: { + message: 'Custom message about overall deprecation.', + }, + }, + create(context) {} + }, }, configs: {} };`, @@ -290,11 +441,46 @@ describe('generate (deprecated rules)', function () { }, create(context) {} }, + 'prefer-foo': { + meta: { + docs: { description: 'Description.' }, + deprecated: { + replacedBy: [ + { + rule: { + name: 'no-unused-vars', + }, + }, + ], + }, + }, + create(context) {} + }, + 'prefer-bar': { + meta: { + docs: { description: 'Description.' }, + deprecated: { + replacedBy: [ + { + rule: { + name: 'no-unused-vars', + }, + plugin: { + name: 'eslint', + }, + }, + ], + }, + }, + create(context) {} + }, }, };`, 'README.md': '', 'docs/rules/no-foo.md': '', + 'docs/rules/prefer-foo.md': '', + 'docs/rules/prefer-bar.md': '', }, }); }); @@ -308,6 +494,12 @@ describe('generate (deprecated rules)', function () { expect(await fixture.readFile('README.md')).toMatchSnapshot(); expect(await fixture.readFile('docs/rules/no-foo.md')).toMatchSnapshot(); + expect( + await fixture.readFile('docs/rules/prefer-foo.md'), + ).toMatchSnapshot(); + expect( + await fixture.readFile('docs/rules/prefer-bar.md'), + ).toMatchSnapshot(); }); }); @@ -329,11 +521,47 @@ describe('generate (deprecated rules)', function () { }, create(context) {} }, + 'prefer-foo': { + meta: { + docs: { description: 'Description.' }, + deprecated: { + replacedBy: [ + { + rule: { + name: 'other-plugin/no-unused-vars' + } + }, + ], + }, + }, + create(context) {} + }, + 'prefer-bar': { + meta: { + docs: { description: 'Description.' }, + deprecated: { + replacedBy: [ + { + rule: { + name: 'other-plugin/no-unused-vars' + }, + plugin: { + name: 'eslint-plugin-other-plugin', + url: 'https://example.org/other-plugin' + } + }, + ], + }, + }, + create(context) {} + }, }, };`, 'README.md': '', 'docs/rules/no-foo.md': '', + 'docs/rules/prefer-foo.md': '', + 'docs/rules/prefer-bar.md': '', }, }); }); @@ -347,6 +575,12 @@ describe('generate (deprecated rules)', function () { expect(await fixture.readFile('README.md')).toMatchSnapshot(); expect(await fixture.readFile('docs/rules/no-foo.md')).toMatchSnapshot(); + expect( + await fixture.readFile('docs/rules/prefer-foo.md'), + ).toMatchSnapshot(); + expect( + await fixture.readFile('docs/rules/prefer-bar.md'), + ).toMatchSnapshot(); }); }); @@ -368,11 +602,27 @@ describe('generate (deprecated rules)', function () { }, create(context) {} }, + 'prefer-foo': { + meta: { + docs: { description: 'Description.' }, + deprecated: { + replacedBy: [ + { + rule: { + name: 'other-plugin/prefer-foo' + }, + }, + ], + }, + }, + create(context) {} + }, }, };`, 'README.md': '', 'docs/rules/no-foo.md': '', + 'docs/rules/prefer-foo.md': '', }, }); }); @@ -386,6 +636,239 @@ describe('generate (deprecated rules)', function () { expect(await fixture.readFile('README.md')).toMatchSnapshot(); expect(await fixture.readFile('docs/rules/no-foo.md')).toMatchSnapshot(); + expect( + await fixture.readFile('docs/rules/prefer-foo.md'), + ).toMatchSnapshot(); + }); + }); + + describe('DeprecatedInfo with only invalid replacedBy entries preceding a valid one', function () { + let fixture: FixtureContext; + + beforeAll(async function () { + fixture = await setupFixture({ + fixture: 'esm-base', + overrides: { + 'index.js': ` + export default { + rules: { + 'no-foo': { + meta: { + docs: { description: 'Description.' }, + deprecated: { + replacedBy: [ + { rule: {} }, + { rule: { name: 'no-bar' } }, + ] + }, + }, + create(context) {} + }, + 'no-bar': { + meta: { + docs: { description: 'Description.' }, + }, + create(context) {} + }, + }, + configs: {} + };`, + 'README.md': + '', + 'docs/rules/no-foo.md': '', + 'docs/rules/no-bar.md': '', + }, + }); + }); + + afterAll(async function () { + await fixture.cleanup(); + }); + + it('does not produce an erroneous "and" prefix when only one valid replacement remains', async function () { + await generate(fixture.path); + + expect(await fixture.readFile('docs/rules/no-foo.md')).toMatchSnapshot(); + }); + }); + + describe('DeprecatedInfo with only deprecatedSince (no availableUntil)', function () { + let fixture: FixtureContext; + + beforeAll(async function () { + fixture = await setupFixture({ + fixture: 'esm-base', + overrides: { + 'index.js': ` + export default { + rules: { + 'no-foo': { + meta: { + docs: { description: 'Description.' }, + deprecated: { + deprecatedSince: '3.0.0', + }, + }, + create(context) {} + }, + }, + configs: {} + };`, + 'README.md': + '', + 'docs/rules/no-foo.md': '', + }, + }); + }); + + afterAll(async function () { + await fixture.cleanup(); + }); + + it('displays deprecatedSince without availableUntil', async function () { + await generate(fixture.path); + + expect(await fixture.readFile('docs/rules/no-foo.md')).toMatchSnapshot(); + }); + }); + + describe('DeprecatedInfo with v-prefixed version strings', function () { + let fixture: FixtureContext; + + beforeAll(async function () { + fixture = await setupFixture({ + fixture: 'esm-base', + overrides: { + 'index.js': ` + export default { + rules: { + 'no-foo': { + meta: { + docs: { description: 'Description.' }, + deprecated: { + deprecatedSince: 'v7.0.0', + availableUntil: 'v13.0.0', + }, + }, + create(context) {} + }, + }, + configs: {} + };`, + 'README.md': + '', + 'docs/rules/no-foo.md': '', + }, + }); + }); + + afterAll(async function () { + await fixture.cleanup(); + }); + + it('does not double the v prefix', async function () { + await generate(fixture.path); + + expect(await fixture.readFile('docs/rules/no-foo.md')).toMatchSnapshot(); + }); + }); + + describe('with the object type `DeprecatedInfo`', function () { + let fixture: FixtureContext; + + beforeAll(async function () { + fixture = await setupFixture({ + fixture: 'esm-base', + overrides: { + 'index.js': ` + export default { + rules: { + 'no-foo': { + meta: { + docs: { description: 'Description.' }, + deprecated: { + message: 'Custom message about overall deprecation.', + deprecatedSince: '1.0.0', + availableUntil: '2.0.0', + url: 'https://example.org/blog/non-existant', + }, + }, + create(context) {} + }, + 'no-bar': { + meta: { + docs: { description: 'Description.' }, + deprecated: { + message: 'Custom message about overall deprecation.', + availableUntil: '2.0.0', + } + }, + create(context) {} + }, + 'no-baz': { + meta: { + docs: { description: 'Description.' }, + deprecated: { + replacedBy: [ + { + // should not be present + rule: {} + }, + { + rule: { + name: 'no-bar', + } + }, + { + rule: { + name: 'no-baz', + url: 'https://example.org/rules/no-baz.md' + } + }, + { + message: 'Custom message', + url: 'https://example.org/changelog.md', + rule: { + name: 'test/no-baz', + } + }, + { + rule: { + name: '@stylistic/indent', + url: 'https://eslint.style/rules/indent', + }, + plugin: { + name: '@sytlistic/eslint-plugin', + url: 'https://eslint.style/', + } + }, + ] + } + }, + create(context) {} + }, + }, + };`, + 'README.md': + '', + 'docs/rules/no-foo.md': '', + 'docs/rules/no-bar.md': '', + 'docs/rules/no-baz.md': '', + }, + }); + }); + + afterAll(async function () { + await fixture.cleanup(); + }); + + it('displays correct information', async function () { + await generate(fixture.path); + + expect(await fixture.readFile('README.md')).toMatchSnapshot(); + expect(await fixture.readFile('docs/rules/no-foo.md')).toMatchSnapshot(); + expect(await fixture.readFile('docs/rules/no-bar.md')).toMatchSnapshot(); + expect(await fixture.readFile('docs/rules/no-baz.md')).toMatchSnapshot(); }); }); });