Skip to content

Commit

Permalink
feat: indicate which configs disable a rule
Browse files Browse the repository at this point in the history
  • Loading branch information
bmish committed Oct 21, 2022
1 parent 58f2269 commit 4e34b9e
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 44 deletions.
16 changes: 10 additions & 6 deletions lib/configs.ts
@@ -1,15 +1,19 @@
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.
*/
export function getConfigsForRule(
ruleName: string,
configsToRules: ConfigsToRules,
pluginPrefix: string
pluginPrefix: string,
severity: Set<RuleSeverity>
) {
const configNames: Array<keyof typeof configsToRules> = [];

Expand All @@ -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);
Expand Down
42 changes: 36 additions & 6 deletions lib/legend.ts
Expand Up @@ -6,7 +6,14 @@ import {
EMOJI_REQUIRES_TYPE_CHECKING,
EMOJI_TYPE,
} from './emojis.js';
import { COLUMN_TYPE, ConfigEmojis, Plugin } from './types.js';
import { getConfigsForRule } from './configs.js';
import {
COLUMN_TYPE,
ConfigEmojis,
Plugin,
ConfigsToRules,
SEVERITY_ERROR,
} from './types.js';
import { RULE_TYPE_MESSAGES_LEGEND, RULE_TYPES } from './rule-type.js';

/**
Expand All @@ -18,25 +25,28 @@ const LEGENDS: {
| undefined // For no legend.
| ((data: {
plugin: Plugin;
configsToRules: ConfigsToRules;
configEmojis: ConfigEmojis;
pluginPrefix: string;
ignoreConfig: string[];
urlConfigs?: string;
}) => string[]);
} = {
// Legends are included for each config. A generic config legend is also included if there are multiple configs.
[COLUMN_TYPE.CONFIGS]: ({
plugin,
configsToRules,
configEmojis,
pluginPrefix,
urlConfigs,
ignoreConfig,
}) => {
/* istanbul ignore next -- this shouldn't happen */
if (!plugin.configs) {
if (!plugin.configs || !plugin.rules) {
throw new Error(
'Should not be attempting to display configs column when there are no configs.'
'Should not be attempting to display configs column when there are no configs/rules.'
);
}
const configNames = Object.keys(plugin.configs);

// Add link to configs documentation if provided.
const configsLinkOrWord = urlConfigs
Expand All @@ -46,7 +56,21 @@ const LEGENDS: {
? `[configuration](${urlConfigs})`
: 'configuration';

const configNamesWithoutIgnored = configNames.filter(
const ruleNames = Object.keys(plugin.rules);
const configsThatEnableAnyRule = Object.entries(configsToRules)
.filter(([configName, _config]) =>
ruleNames.some((ruleName) =>
getConfigsForRule(
ruleName,
configsToRules,
pluginPrefix,
SEVERITY_ERROR
).includes(configName)
)
)
.map(([configName, _config]) => configName);

const configNamesWithoutIgnored = configsThatEnableAnyRule.filter(
(configName) => !ignoreConfig?.includes(configName)
);

Expand All @@ -57,7 +81,9 @@ const LEGENDS: {
configNamesWithoutIgnored?.includes(configEmoji.config)
)?.emoji) &&
// If any configs are using the generic config emoji, then don't display the generic config legend.
!configEmojis.some((configEmoji) => configEmoji.emoji === EMOJI_CONFIG)
!configEmojis
.filter(({ config }) => !ignoreConfig?.includes(config))
.some((configEmoji) => configEmoji.emoji === EMOJI_CONFIG)
) {
// Generic config emoji will be used if the plugin has multiple configs or the sole config has no emoji.
legends.push(`${EMOJI_CONFIG} ${configsLinkOrWord} enabled in.`);
Expand Down Expand Up @@ -121,7 +147,9 @@ const LEGENDS: {
export function generateLegend(
columns: Record<COLUMN_TYPE, boolean>,
plugin: Plugin,
configsToRules: ConfigsToRules,
configEmojis: ConfigEmojis,
pluginPrefix: string,
ignoreConfig: string[],
urlConfigs?: string
) {
Expand All @@ -139,7 +167,9 @@ export function generateLegend(
return typeof legendStrOrFn === 'function'
? legendStrOrFn({
plugin,
configsToRules,
configEmojis,
pluginPrefix,
urlConfigs,
ignoreConfig,
})
Expand Down
34 changes: 26 additions & 8 deletions lib/rule-list.ts
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -99,10 +102,23 @@ function generateRulesListMarkdown(
return [];
}
const headerStrOrFn = COLUMN_HEADER[columnType];
const ruleNames = details.map((rule) => rule.name);
const configsThatEnableAnyRule = Object.entries(configsToRules)
.filter(([configName, _config]) =>
ruleNames.some((ruleName) =>
getConfigsForRule(
ruleName,
configsToRules,
pluginPrefix,
SEVERITY_ERROR
).includes(configName)
)
)
.map(([configName, _config]) => configName);
return [
typeof headerStrOrFn === 'function'
? headerStrOrFn({
configNames: Object.keys(configsToRules),
configNames: configsThatEnableAnyRule,
configEmojis,
ignoreConfig,
details,
Expand Down Expand Up @@ -191,7 +207,9 @@ export async function updateRulesList(
const legend = generateLegend(
columns,
plugin,
configsToRules,
configEmojis,
pluginPrefix,
ignoreConfig,
urlConfigs
);
Expand Down
110 changes: 86 additions & 24 deletions lib/rule-notices.ts
Expand Up @@ -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;
Expand All @@ -43,46 +43,91 @@ const RULE_NOTICES: {
| undefined
| ((data: {
configsEnabled: string[];
configsDisabled: string[];
configEmojis: ConfigEmojis;
urlConfigs?: string;
replacedBy: readonly string[] | undefined;
type?: RULE_TYPE;
}) => 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})`
: 'configs';
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 =
// If one applicable config with an emoji, use the emoji for that config, otherwise use the general config 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}.`;
}

// List of configs that enable the rule.
const configsEnabledCSV = configsEnabled
.map((configEnabled) => {
const emoji = configEmojis.find(
(configEmoji) => configEmoji.config === configEnabled
)?.emoji;
return `${emoji ? `${emoji} ` : ''}\`${configEnabled}\``;
})
.join(', ');

// List of configs that disable the rule.
const configsDisabledCSV = configsDisabled
.map((configDisabled) => {
const emoji = configEmojis.find(
(configEmoji) => configEmoji.config === configDisabled
)?.emoji;
return `${emoji ? `${emoji} ` : ''}\`${configDisabled}\``;
})
.join(', ');

// Complete sentence for configs that enable the rule.
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}.`
: '';

// Complete sentence for configs that disable the rule.
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.
Expand Down Expand Up @@ -125,13 +170,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.
Expand Down Expand Up @@ -188,9 +235,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) {
Expand All @@ -215,6 +276,7 @@ function getRuleNoticeLines(
typeof ruleNoticeStrOrFn === 'function'
? ruleNoticeStrOrFn({
configsEnabled,
configsDisabled,
configEmojis,
urlConfigs,
replacedBy: rule.meta.replacedBy,
Expand Down
5 changes: 5 additions & 0 deletions lib/types.ts
Expand Up @@ -6,12 +6,17 @@ export type RuleModule = TSESLint.RuleModule<string, unknown[]>;

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<RuleSeverity>([2, 'error']);
export const SEVERITY_OFF = new Set<RuleSeverity>([0, 'off']);

export type ConfigsToRules = Record<string, Rules>;

export interface RuleDetails {
Expand Down

0 comments on commit 4e34b9e

Please sign in to comment.