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 85f77d7
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 38 deletions.
16 changes: 10 additions & 6 deletions lib/configs.ts
Original file line number Diff line number Diff line change
@@ -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
17 changes: 10 additions & 7 deletions lib/rule-list.ts
Original file line number Diff line number Diff line change
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
105 changes: 81 additions & 24 deletions lib/rule-notices.ts
Original file line number Diff line number Diff line change
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,86 @@ 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 =
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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand All @@ -215,6 +271,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
Original file line number Diff line number Diff line change
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
27 changes: 26 additions & 1 deletion test/lib/__snapshots__/generator-test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -575,11 +575,14 @@ exports[`generator #generate rules that are disabled generates the documentation
"## Rules
<!-- begin auto-generated rules list -->
💼 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. | |
<!-- end auto-generated rules list -->
Expand All @@ -589,13 +592,35 @@ 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.
<!-- end auto-generated rule header -->
"
`;

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\`.
<!-- end auto-generated rule header -->
"
`;

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.
<!-- end auto-generated rule header -->
"
`;

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.
<!-- end auto-generated rule header -->
"
`;
Expand Down
20 changes: 20 additions & 0 deletions test/lib/generator-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
},
}
Expand All @@ -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(
Expand All @@ -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();
});
});

Expand Down

0 comments on commit 85f77d7

Please sign in to comment.