diff --git a/lib/configs.ts b/lib/configs.ts index c68bf483..b52bc739 100644 --- a/lib/configs.ts +++ b/lib/configs.ts @@ -1,7 +1,10 @@ import { EMOJI_CONFIGS } from './emojis.js'; -import type { Plugin, ConfigsToRules, ConfigEmojis } from './types.js'; - -const SEVERITY_ENABLED = new Set([2, 'error']); +import type { + Plugin, + ConfigsToRules, + ConfigEmojis, + RuleSeverity, +} from './types.js'; /** * Get config names that a given rule belongs to. @@ -9,7 +12,8 @@ const SEVERITY_ENABLED = new Set([2, 'error']); export function getConfigsForRule( ruleName: string, configsToRules: ConfigsToRules, - pluginPrefix: string + pluginPrefix: string, + severity: Set ) { const configNames: Array = []; @@ -18,11 +22,11 @@ export function getConfigsForRule( const value = rules[`${pluginPrefix}/${ruleName}`]; const isEnabled = ((typeof value === 'string' || typeof value === 'number') && - SEVERITY_ENABLED.has(value)) || + severity.has(value)) || (typeof value === 'object' && Array.isArray(value) && value.length > 0 && - SEVERITY_ENABLED.has(value[0])); + severity.has(value[0])); if (isEnabled) { configNames.push(configName); diff --git a/lib/rule-list.ts b/lib/rule-list.ts index 7de207f4..f1933b15 100644 --- a/lib/rule-list.ts +++ b/lib/rule-list.ts @@ -11,7 +11,7 @@ import { findSectionHeader, format } from './markdown.js'; import { getPluginRoot } from './package-json.js'; import { generateLegend } from './legend.js'; import { relative } from 'node:path'; -import { COLUMN_TYPE } from './types.js'; +import { COLUMN_TYPE, SEVERITY_ERROR } from './types.js'; import { markdownTable } from 'markdown-table'; import type { Plugin, @@ -29,12 +29,15 @@ function getConfigurationColumnValueForRule( ignoreConfig: string[] ): string { const badges: string[] = []; - const configs = getConfigsForRule(rule.name, configsToRules, pluginPrefix); - for (const configName of configs) { - if (ignoreConfig?.includes(configName)) { - // Ignore config. - continue; - } + + const configsEnabled = getConfigsForRule( + rule.name, + configsToRules, + pluginPrefix, + SEVERITY_ERROR + ).filter((configName) => !ignoreConfig?.includes(configName)); + + for (const configName of configsEnabled) { // Find the emoji for the config or otherwise use a badge that can be defined in markdown. const emoji = configEmojis.find( (configEmoji) => configEmoji.config === configName diff --git a/lib/rule-notices.ts b/lib/rule-notices.ts index 6fcbaeb2..1e65379f 100644 --- a/lib/rule-notices.ts +++ b/lib/rule-notices.ts @@ -18,7 +18,7 @@ import { RuleDocTitleFormat, RULE_DOC_TITLE_FORMAT_DEFAULT, } from './rule-doc-title-format.js'; -import { NOTICE_TYPE } from './types.js'; +import { NOTICE_TYPE, SEVERITY_ERROR, SEVERITY_OFF } from './types.js'; export const NOTICE_TYPE_DEFAULT_PRESENCE_AND_ORDERING: { [key in NOTICE_TYPE]: boolean; @@ -43,6 +43,7 @@ const RULE_NOTICES: { | undefined | ((data: { configsEnabled: string[]; + configsDisabled: string[]; configEmojis: ConfigEmojis; urlConfigs?: string; replacedBy: readonly string[] | undefined; @@ -50,7 +51,12 @@ const RULE_NOTICES: { }) => string); } = { // Configs notice varies based on whether the rule is enabled in one or more configs. - [NOTICE_TYPE.CONFIGS]: ({ configsEnabled, configEmojis, urlConfigs }) => { + [NOTICE_TYPE.CONFIGS]: ({ + configsEnabled, + configsDisabled, + configEmojis, + urlConfigs, + }) => { // Add link to configs documentation if provided. const configsLinkOrWord = urlConfigs ? `[configs](${urlConfigs})` @@ -58,31 +64,65 @@ const RULE_NOTICES: { const configLinkOrWord = urlConfigs ? `[config](${urlConfigs})` : 'config'; /* istanbul ignore next -- this shouldn't happen */ - if (!configsEnabled || configsEnabled.length === 0) { + if ( + (!configsEnabled || configsEnabled.length === 0) && + (!configsDisabled || configsDisabled.length === 0) + ) { throw new Error( - 'Should not be trying to display config notice for rule not enabled in any configs.' + 'Should not be trying to display config notice for rule not enabled/disabled in any configs.' ); } - if (configsEnabled.length > 1) { - // Rule is enabled in multiple configs. - const configs = configsEnabled - .map((configEnabled) => { - const emoji = configEmojis.find( - (configEmoji) => configEmoji.config === configEnabled - )?.emoji; - return `${emoji ? `${emoji} ` : ''}\`${configEnabled}\``; - }) - .join(', '); - return `${EMOJI_CONFIG} This rule is enabled in the following ${configsLinkOrWord}: ${configs}.`; - } else { - // Rule only enabled in one config. - const emoji = + let emoji = ''; + if (configsEnabled.length + configsDisabled.length > 1) { + emoji = EMOJI_CONFIG; + } else if (configsEnabled.length > 0) { + emoji = configEmojis.find( - (configEmoji) => configEmoji.config === configsEnabled?.[0] + (configEmoji) => configEmoji.config === configsEnabled[0] + )?.emoji ?? EMOJI_CONFIG; + } else if (configsDisabled.length > 0) { + emoji = + configEmojis.find( + (configEmoji) => configEmoji.config === configsDisabled[0] )?.emoji ?? EMOJI_CONFIG; - return `${emoji} This rule is enabled in the \`${configsEnabled?.[0]}\` ${configLinkOrWord}.`; } + + const configsEnabledCSV = configsEnabled + .map((configEnabled) => { + const emoji = configEmojis.find( + (configEmoji) => configEmoji.config === configEnabled + )?.emoji; + return `${emoji ? `${emoji} ` : ''}\`${configEnabled}\``; + }) + .join(', '); + + const configsDisabledCSV = configsDisabled + .map((configDisabled) => { + const emoji = configEmojis.find( + (configEmoji) => configEmoji.config === configDisabled + )?.emoji; + return `${emoji ? `${emoji} ` : ''}\`${configDisabled}\``; + }) + .join(', '); + + const SENTENCE_ENABLED = + configsEnabled.length > 1 + ? `This rule is enabled in the following ${configsLinkOrWord}: ${configsEnabledCSV}.` + : configsEnabled.length === 1 + ? `This rule is enabled in the \`${configsEnabled?.[0]}\` ${configLinkOrWord}.` + : ''; + + const SENTENCE_DISABLED = + configsDisabled.length > 1 + ? `This rule is disabled in the following ${configsLinkOrWord}: ${configsDisabledCSV}.` + : configsDisabled.length === 1 + ? `This rule is disabled in the \`${configsDisabled?.[0]}\` ${configLinkOrWord}.` + : ''; + + return `${emoji} ${SENTENCE_ENABLED}${ + SENTENCE_ENABLED && SENTENCE_DISABLED ? ' ' : '' // Space if two sentences. + }${SENTENCE_DISABLED}`; }, // Deprecated notice has optional "replaced by" rules list. @@ -125,13 +165,15 @@ function ruleNamesToList(ruleNames: readonly string[]) { function getNoticesForRule( rule: RuleModule, configsEnabled: string[], + configsDisabled: string[], ruleDocNotices: NOTICE_TYPE[] ) { const notices: { [key in NOTICE_TYPE]: boolean; } = { // Alphabetical order. - [NOTICE_TYPE.CONFIGS]: configsEnabled.length > 0, + [NOTICE_TYPE.CONFIGS]: + configsEnabled.length > 0 || configsDisabled.length > 0, [NOTICE_TYPE.DEPRECATED]: rule.meta.deprecated || false, // FIXABLE_AND_HAS_SUGGESTIONS potentially replaces FIXABLE and HAS_SUGGESTIONS. @@ -188,9 +230,23 @@ function getRuleNoticeLines( const configsEnabled = getConfigsForRule( ruleName, configsToRules, - pluginPrefix - ).filter((config) => !ignoreConfig?.includes(config)); - const notices = getNoticesForRule(rule, configsEnabled, ruleDocNotices); + pluginPrefix, + SEVERITY_ERROR + ).filter((configName) => !ignoreConfig?.includes(configName)); + + const configsDisabled = getConfigsForRule( + ruleName, + configsToRules, + pluginPrefix, + SEVERITY_OFF + ).filter((configName) => !ignoreConfig?.includes(configName)); + + const notices = getNoticesForRule( + rule, + configsEnabled, + configsDisabled, + ruleDocNotices + ); let noticeType: keyof typeof notices; for (noticeType in notices) { @@ -215,6 +271,7 @@ function getRuleNoticeLines( typeof ruleNoticeStrOrFn === 'function' ? ruleNoticeStrOrFn({ configsEnabled, + configsDisabled, configEmojis, urlConfigs, replacedBy: rule.meta.replacedBy, diff --git a/lib/types.ts b/lib/types.ts index 165b97a6..0f01a56b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -6,12 +6,17 @@ export type RuleModule = TSESLint.RuleModule; export type Rules = TSESLint.Linter.RulesRecord; +export type RuleSeverity = TSESLint.Linter.RuleLevel; + export type Config = TSESLint.Linter.Config; export type Plugin = TSESLint.Linter.Plugin; // Custom types. +export const SEVERITY_ERROR = new Set([2, 'error']); +export const SEVERITY_OFF = new Set([0, 'off']); + export type ConfigsToRules = Record; export interface RuleDetails { diff --git a/test/lib/__snapshots__/generator-test.ts.snap b/test/lib/__snapshots__/generator-test.ts.snap index a3020f45..ae147c4a 100644 --- a/test/lib/__snapshots__/generator-test.ts.snap +++ b/test/lib/__snapshots__/generator-test.ts.snap @@ -575,11 +575,14 @@ exports[`generator #generate rules that are disabled generates the documentation "## Rules +💼 Configurations enabled in.\\ ✅ Enabled in the \`recommended\` configuration. -| Name | Description | ✅ | +| Name | Description | 💼 | | :----------------------------- | :--------------------- | :-- | | [no-bar](docs/rules/no-bar.md) | Description of no-bar. | | +| [no-baz](docs/rules/no-baz.md) | Description of no-bar. | ✅ | +| [no-biz](docs/rules/no-biz.md) | Description of no-bar. | | | [no-foo](docs/rules/no-foo.md) | Description of no-foo. | | @@ -589,6 +592,8 @@ exports[`generator #generate rules that are disabled generates the documentation exports[`generator #generate rules that are disabled generates the documentation 2`] = ` "# Description of no-foo (\`test/no-foo\`) +✅ This rule is disabled in the \`recommended\` config. + " `; @@ -596,6 +601,26 @@ exports[`generator #generate rules that are disabled generates the documentation exports[`generator #generate rules that are disabled generates the documentation 3`] = ` "# Description of no-bar (\`test/no-bar\`) +💼 This rule is disabled in the following configs: \`other\`, ✅ \`recommended\`. + + +" +`; + +exports[`generator #generate rules that are disabled generates the documentation 4`] = ` +"# Description of no-bar (\`test/no-baz\`) + +💼 This rule is enabled in the \`recommended\` config. This rule is disabled in the \`other\` config. + + +" +`; + +exports[`generator #generate rules that are disabled generates the documentation 5`] = ` +"# Description of no-bar (\`test/no-biz\`) + +💼 This rule is disabled in the \`other\` config. + " `; diff --git a/test/lib/generator-test.ts b/test/lib/generator-test.ts index c8daf45c..d63aef93 100644 --- a/test/lib/generator-test.ts +++ b/test/lib/generator-test.ts @@ -1841,12 +1841,28 @@ describe('generator', function () { meta: { docs: { description: 'Description of no-bar.' }, }, create(context) {}, }, + 'no-baz': { + meta: { docs: { description: 'Description of no-bar.' }, }, + create(context) {}, + }, + 'no-biz': { + meta: { docs: { description: 'Description of no-bar.' }, }, + create(context) {}, + }, }, configs: { recommended: { rules: { 'test/no-foo': 'off', 'test/no-bar': 0, + 'test/no-baz': 'error', + } + }, + other: { + rules: { + 'test/no-bar': 0, + 'test/no-baz': 'off', + 'test/no-biz': 'off', } }, } @@ -1856,6 +1872,8 @@ describe('generator', function () { 'docs/rules/no-foo.md': '', 'docs/rules/no-bar.md': '', + 'docs/rules/no-baz.md': '', + 'docs/rules/no-biz.md': '', // Needed for some of the test infrastructure to work. node_modules: mockFs.load( @@ -1874,6 +1892,8 @@ describe('generator', function () { expect(readFileSync('README.md', 'utf8')).toMatchSnapshot(); expect(readFileSync('docs/rules/no-foo.md', 'utf8')).toMatchSnapshot(); expect(readFileSync('docs/rules/no-bar.md', 'utf8')).toMatchSnapshot(); + expect(readFileSync('docs/rules/no-baz.md', 'utf8')).toMatchSnapshot(); + expect(readFileSync('docs/rules/no-biz.md', 'utf8')).toMatchSnapshot(); }); });