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

Indicate which configs disable a rule #156

Merged
merged 1 commit into from Oct 21, 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
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