Skip to content
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

Ensure correct link used for deprecated rule replacement link when replacement rule is from ESLint core or third-party plugin #367

Merged
merged 1 commit into from Dec 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/rule-doc-notices.ts
Expand Up @@ -15,6 +15,7 @@ import {
ConfigEmojis,
SEVERITY_TYPE,
NOTICE_TYPE,
RULE_SOURCE,
} from './types.js';
import { RULE_TYPE, RULE_TYPE_MESSAGES_NOTICES } from './rule-type.js';
import { RuleDocTitleFormat } from './rule-doc-title-format.js';
Expand Down Expand Up @@ -88,6 +89,7 @@ const RULE_NOTICES: {
hasSuggestions: boolean;
urlConfigs?: string;
replacedBy: readonly string[] | undefined;
plugin: Plugin;
pluginPrefix: string;
pathPlugin: string;
pathRuleDoc: string;
Expand Down Expand Up @@ -164,6 +166,7 @@ const RULE_NOTICES: {
// Deprecated notice has optional "replaced by" rules list.
[NOTICE_TYPE.DEPRECATED]: ({
replacedBy,
plugin,
pluginPrefix,
pathPlugin,
pathRuleDoc,
Expand All @@ -172,15 +175,21 @@ const RULE_NOTICES: {
}) => {
const urlCurrentPage = getUrlToRule(
ruleName,
RULE_SOURCE.self,
pluginPrefix,
pathPlugin,
pathRuleDoc,
pathPlugin,
urlRuleDoc
);
/* istanbul ignore next -- this shouldn't happen */
if (!urlCurrentPage) {
throw new Error('Missing URL to our own rule');
}
const replacementRuleList = (replacedBy ?? []).map((replacementRuleName) =>
getLinkToRule(
replacementRuleName,
plugin,
pluginPrefix,
pathPlugin,
pathRuleDoc,
Expand Down Expand Up @@ -370,6 +379,7 @@ function getRuleNoticeLines(
hasSuggestions: Boolean(rule.meta?.hasSuggestions),
urlConfigs,
replacedBy: rule.meta?.replacedBy,
plugin,
pluginPrefix,
pathPlugin,
pathRuleDoc,
Expand Down
47 changes: 41 additions & 6 deletions lib/rule-link.ts
@@ -1,5 +1,6 @@
import { countOccurrencesInString } from './string.js';
import { join, sep, relative } from 'node:path';
import { Plugin, RULE_SOURCE } from './types.js';

export function replaceRulePlaceholder(pathOrUrl: string, ruleName: string) {
return pathOrUrl.replace(/\{name\}/gu, ruleName);
Expand Down Expand Up @@ -30,12 +31,24 @@ function pathToUrl(path: string): string {
*/
export function getUrlToRule(
ruleName: string,
ruleSource: RULE_SOURCE,
pluginPrefix: string,
pathPlugin: string,
pathRuleDoc: string,
urlCurrentPage: string,
urlRuleDoc?: string
) {
switch (ruleSource) {
case RULE_SOURCE.eslintCore:
return `https://eslint.org/docs/latest/rules/${ruleName}`;
case RULE_SOURCE.thirdPartyPlugin:
// We don't know the documentation URL to third-party plugins.
return undefined; // eslint-disable-line unicorn/no-useless-undefined
default:
// Fallthrough to remaining logic in function.
break;
}

const nestingDepthOfCurrentPage = countOccurrencesInString(
relative(pathPlugin, urlCurrentPage),
sep
Expand Down Expand Up @@ -63,6 +76,7 @@ export function getUrlToRule(
*/
export function getLinkToRule(
ruleName: string,
plugin: Plugin,
pluginPrefix: string,
pathPlugin: string,
pathRuleDoc: string,
Expand All @@ -71,21 +85,42 @@ export function getLinkToRule(
includePrefix: boolean,
urlRuleDoc?: string
) {
const ruleNameWithPluginPrefix = ruleName.startsWith(`${pluginPrefix}/`)
? ruleName
: `${pluginPrefix}/${ruleName}`;
const ruleNameWithoutPluginPrefix = ruleName.startsWith(`${pluginPrefix}/`)
? ruleName.slice(pluginPrefix.length + 1)
: ruleName;

// Determine what plugin this rule comes from.
let ruleSource: RULE_SOURCE;
if (plugin.rules?.[ruleNameWithoutPluginPrefix]) {
ruleSource = RULE_SOURCE.self;
} else if (ruleName.includes('/')) {
// Assume a slash is for the plugin prefix (ESLint core doesn't have any nested rules).
ruleSource = RULE_SOURCE.thirdPartyPlugin;
} else {
ruleSource = RULE_SOURCE.eslintCore;
}

const ruleNameWithPluginPrefix = ruleName.startsWith(`${pluginPrefix}/`)
? ruleName
: ruleSource === RULE_SOURCE.self
? `${pluginPrefix}/${ruleName}`
: undefined;

const urlToRule = getUrlToRule(
ruleName,
ruleSource,
pluginPrefix,
pathPlugin,
pathRuleDoc,
urlCurrentPage,
urlRuleDoc
);
return `[${includeBackticks ? '`' : ''}${
includePrefix ? ruleNameWithPluginPrefix : ruleNameWithoutPluginPrefix
}${includeBackticks ? '`' : ''}](${urlToRule})`;

const ruleNameToDisplay = `${includeBackticks ? '`' : ''}${
includePrefix && ruleNameWithPluginPrefix
? ruleNameWithPluginPrefix
: ruleNameWithoutPluginPrefix
}${includeBackticks ? '`' : ''}`;

return urlToRule ? `[${ruleNameToDisplay}](${urlToRule})` : ruleNameToDisplay;
}
7 changes: 7 additions & 0 deletions lib/rule-list.ts
Expand Up @@ -99,6 +99,7 @@ function buildRuleRow(
rule: RuleModule,
columnsEnabled: Record<COLUMN_TYPE, boolean>,
configsToRules: ConfigsToRules,
plugin: Plugin,
pluginPrefix: string,
pathPlugin: string,
pathRuleDoc: string,
Expand Down Expand Up @@ -147,6 +148,7 @@ function buildRuleRow(
[COLUMN_TYPE.NAME]() {
return getLinkToRule(
ruleName,
plugin,
pluginPrefix,
pathPlugin,
pathRuleDoc,
Expand Down Expand Up @@ -180,6 +182,7 @@ function generateRulesListMarkdown(
ruleNamesAndRules: RuleNamesAndRules,
columns: Record<COLUMN_TYPE, boolean>,
configsToRules: ConfigsToRules,
plugin: Plugin,
pluginPrefix: string,
pathPlugin: string,
pathRuleDoc: string,
Expand Down Expand Up @@ -211,6 +214,7 @@ function generateRulesListMarkdown(
rule,
columns,
configsToRules,
plugin,
pluginPrefix,
pathPlugin,
pathRuleDoc,
Expand All @@ -233,6 +237,7 @@ function generateRuleListMarkdownForRulesAndHeaders(
headerLevel: number,
columns: Record<COLUMN_TYPE, boolean>,
configsToRules: ConfigsToRules,
plugin: Plugin,
pluginPrefix: string,
pathPlugin: string,
pathRuleDoc: string,
Expand All @@ -252,6 +257,7 @@ function generateRuleListMarkdownForRulesAndHeaders(
rules,
columns,
configsToRules,
plugin,
pluginPrefix,
pathPlugin,
pathRuleDoc,
Expand Down Expand Up @@ -509,6 +515,7 @@ export function updateRulesList(
ruleListSplitHeaderLevel,
columns,
configsToRules,
plugin,
pluginPrefix,
pathPlugin,
pathRuleDoc,
Expand Down
9 changes: 9 additions & 0 deletions lib/types.ts
Expand Up @@ -15,6 +15,15 @@ export type Plugin = TSESLint.Linter.Plugin;

// Custom types.

/**
* Where a rule comes from (where it's defined).
*/
export enum RULE_SOURCE {
'self' = 'self', // From this plugin.
'eslintCore' = 'eslintCore',
'thirdPartyPlugin' = 'thirdPartyPlugin',
}

export const SEVERITY_ERROR = new Set<RuleSeverity>([2, 'error']);
export const SEVERITY_WARN = new Set<RuleSeverity>([1, 'warn']);
export const SEVERITY_OFF = new Set<RuleSeverity>([0, 'off']);
Expand Down
63 changes: 63 additions & 0 deletions test/lib/generate/__snapshots__/rule-deprecation-test.ts.snap
@@ -1,5 +1,68 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`generate (deprecated rules) replaced by ESLint core rule uses correct replacement rule link 1`] = `
"<!-- begin auto-generated rules list -->

❌ Deprecated.

| Name | Description | ❌ |
| :----------------------------- | :----------- | :- |
| [no-foo](docs/rules/no-foo.md) | Description. | ❌ |

<!-- end auto-generated rules list -->"
`;

exports[`generate (deprecated rules) replaced by ESLint core rule uses correct replacement rule link 2`] = `
"# Description (\`test/no-foo\`)

❌ This rule is deprecated. It was replaced by [\`no-unused-vars\`](https://eslint.org/docs/latest/rules/no-unused-vars).

<!-- end auto-generated rule header -->
"
`;

exports[`generate (deprecated rules) replaced by third-party plugin rule uses correct replacement rule link 1`] = `
"<!-- begin auto-generated rules list -->

❌ Deprecated.

| Name | Description | ❌ |
| :----------------------------- | :----------- | :- |
| [no-foo](docs/rules/no-foo.md) | Description. | ❌ |

<!-- end auto-generated rules list -->"
`;

exports[`generate (deprecated rules) replaced by third-party plugin rule uses correct replacement rule link 2`] = `
"# Description (\`test/no-foo\`)

❌ This rule is deprecated. It was replaced by \`other-plugin/no-unused-vars\`.

<!-- end auto-generated rule header -->
"
`;

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`] = `
"<!-- begin auto-generated rules list -->

❌ Deprecated.

| Name | Description | ❌ |
| :----------------------------- | :----------- | :- |
| [no-foo](docs/rules/no-foo.md) | Description. | ❌ |

<!-- end auto-generated rules list -->"
`;

exports[`generate (deprecated rules) replaced by third-party plugin rule with same rule name as one of our rules uses correct replacement rule link 2`] = `
"# Description (\`test/no-foo\`)

❌ This rule is deprecated. It was replaced by \`other-plugin/no-foo\`.

<!-- end auto-generated rule header -->
"
`;

exports[`generate (deprecated rules) several deprecated rules updates the documentation 1`] = `
"<!-- begin auto-generated rules list -->

Expand Down