Skip to content

Commit f48dd08

Browse files
committed
feat: add --rule-list-columns option
1 parent 9756bff commit f48dd08

File tree

8 files changed

+276
-53
lines changed

8 files changed

+276
-53
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ And how it looks:
139139
| `--rule-doc-section-exclude` | Disallowed section in each rule doc. Exit with failure if present. Option can be repeated. |
140140
| `--rule-doc-section-include` | Required section in each rule doc. Exit with failure if missing. Option can be repeated. |
141141
| `--rule-doc-title-format` | The format to use for rule doc titles. Defaults to `desc-parens-prefix-name`. See choices in below [table](#--rule-doc-title-format). |
142+
| `--rule-list-columns` | Ordered, comma-separated list of columns to display in rule list. Empty columns will be hidden. Choices: `configs`, `deprecated`, `description`, `fixable`, `hasSuggestions`, `name`, `requiresTypeChecking`. Default: `name,description,configs,fixable,hasSuggestions,requiresTypeChecking,deprecated`. |
142143
| `--url-configs` | Link to documentation about the ESLint configurations exported by the plugin. |
143144

144145
All options are optional.

lib/cli.ts

+13
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import {
88
RULE_DOC_TITLE_FORMAT_DEFAULT,
99
RULE_DOC_TITLE_FORMATS,
1010
} from './rule-doc-title-format.js';
11+
import {
12+
COLUMN_TYPE,
13+
COLUMN_TYPE_DEFAULT_ORDERING,
14+
} from './rule-list-columns.js';
1115
import type { PackageJson } from 'type-fest';
1216

1317
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -77,6 +81,13 @@ export function run() {
7781
.choices(RULE_DOC_TITLE_FORMATS)
7882
.default(RULE_DOC_TITLE_FORMAT_DEFAULT)
7983
)
84+
.option(
85+
'--rule-list-columns <columns>',
86+
`(optional) Ordered, comma-separated list of columns to display in rule list. Empty columns will be hidden. (choices: "${Object.values(
87+
COLUMN_TYPE
88+
).join('", "')}")`,
89+
COLUMN_TYPE_DEFAULT_ORDERING.join(',')
90+
)
8091
.option(
8192
'--url-configs <url>',
8293
'(optional) Link to documentation about the ESLint configurations exported by the plugin.'
@@ -91,6 +102,7 @@ export function run() {
91102
ruleDocSectionExclude: string[];
92103
ruleDocSectionInclude: string[];
93104
ruleDocTitleFormat: RuleDocTitleFormat;
105+
ruleListColumns: string;
94106
urlConfigs?: string;
95107
}
96108
) {
@@ -102,6 +114,7 @@ export function run() {
102114
ruleDocSectionExclude: options.ruleDocSectionExclude,
103115
ruleDocSectionInclude: options.ruleDocSectionInclude,
104116
ruleDocTitleFormat: options.ruleDocTitleFormat,
117+
ruleListColumns: options.ruleListColumns,
105118
urlConfigs: options.urlConfigs,
106119
});
107120
})

lib/configs.ts

-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import { EMOJI_CONFIG_RECOMMENDED } from './emojis.js';
22
import type { Plugin, ConfigsToRules, ConfigEmojis } from './types.js';
33

4-
export function hasAnyConfigs(configsToRules: ConfigsToRules) {
5-
return Object.keys(configsToRules).length > 0;
6-
}
7-
84
const SEVERITY_ENABLED = new Set([2, 'error']);
95

106
/**

lib/generator.ts

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { findSectionHeader, replaceOrCreateHeader } from './markdown.js';
1414
import { resolveConfigsToRules } from './config-resolution.js';
1515
import { RuleDocTitleFormat } from './rule-doc-title-format.js';
1616
import { parseConfigEmojiOptions } from './configs.js';
17+
import { parseRuleListColumnsOption } from './rule-list-columns.js';
1718
import type { RuleDetails } from './types.js';
1819

1920
/**
@@ -81,6 +82,7 @@ export async function generate(
8182
ruleDocSectionExclude?: string[];
8283
ruleDocSectionInclude?: string[];
8384
ruleDocTitleFormat?: RuleDocTitleFormat;
85+
ruleListColumns?: string;
8486
urlConfigs?: string;
8587
}
8688
) {
@@ -127,6 +129,7 @@ export async function generate(
127129
// Options.
128130
const configEmojis = parseConfigEmojiOptions(plugin, options?.configEmoji);
129131
const ignoreConfig = options?.ignoreConfig ?? [];
132+
const ruleListColumns = parseRuleListColumnsOption(options?.ruleListColumns);
130133

131134
// Update rule doc for each rule.
132135
for (const { name, description, schema } of details) {
@@ -215,6 +218,7 @@ export async function generate(
215218
path,
216219
configEmojis,
217220
ignoreConfig,
221+
ruleListColumns,
218222
options?.urlConfigs
219223
);
220224

lib/rule-list-columns.ts

+47-7
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,20 @@ export enum COLUMN_TYPE {
1313
DESCRIPTION = 'description',
1414
FIXABLE = 'fixable',
1515
HAS_SUGGESTIONS = 'hasSuggestions',
16-
NAME = 'rules',
16+
NAME = 'name',
1717
REQUIRES_TYPE_CHECKING = 'requiresTypeChecking',
1818
}
1919

20+
export const COLUMN_TYPE_DEFAULT_ORDERING = [
21+
COLUMN_TYPE.NAME,
22+
COLUMN_TYPE.DESCRIPTION,
23+
COLUMN_TYPE.CONFIGS,
24+
COLUMN_TYPE.FIXABLE,
25+
COLUMN_TYPE.HAS_SUGGESTIONS,
26+
COLUMN_TYPE.REQUIRES_TYPE_CHECKING,
27+
COLUMN_TYPE.DEPRECATED,
28+
];
29+
2030
/**
2131
* An object containing the column header for each column (as a string or function to generate the string).
2232
*/
@@ -66,27 +76,57 @@ export const COLUMN_HEADER: {
6676
export function getColumns(
6777
details: RuleDetails[],
6878
configsToRules: ConfigsToRules,
79+
ruleListColumns: COLUMN_TYPE[],
6980
ignoreConfig: string[]
70-
) {
81+
): Record<COLUMN_TYPE, boolean> {
7182
const columns: {
7283
[key in COLUMN_TYPE]: boolean;
7384
} = {
74-
// Object keys in display order.
75-
[COLUMN_TYPE.NAME]: true,
76-
[COLUMN_TYPE.DESCRIPTION]: details.some((detail) => detail.description),
85+
// Alphabetical order.
7786
// Show the configs column if there exists a non-ignored config.
7887
[COLUMN_TYPE.CONFIGS]: Object.keys(configsToRules).some(
7988
(config) => !ignoreConfig?.includes(config)
8089
),
90+
[COLUMN_TYPE.DEPRECATED]: details.some((detail) => detail.deprecated),
91+
[COLUMN_TYPE.DESCRIPTION]: details.some((detail) => detail.description),
8192
[COLUMN_TYPE.FIXABLE]: details.some((detail) => detail.fixable),
8293
[COLUMN_TYPE.HAS_SUGGESTIONS]: details.some(
8394
(detail) => detail.hasSuggestions
8495
),
96+
[COLUMN_TYPE.NAME]: true,
8597
[COLUMN_TYPE.REQUIRES_TYPE_CHECKING]: details.some(
8698
(detail) => detail.requiresTypeChecking
8799
),
88-
[COLUMN_TYPE.DEPRECATED]: details.some((detail) => detail.deprecated),
89100
};
90101

91-
return columns;
102+
// Recreate object using the ordering and presence of columns specified in ruleListColumns.
103+
return Object.fromEntries(
104+
ruleListColumns.map((column) => [column, columns[column]])
105+
) as Record<COLUMN_TYPE, boolean>;
106+
}
107+
108+
/**
109+
* Parse the option, check for errors, and set defaults.
110+
*/
111+
export function parseRuleListColumnsOption(
112+
ruleListColumns: string | undefined
113+
): COLUMN_TYPE[] {
114+
const values = ruleListColumns ? ruleListColumns.split(',') : [];
115+
const COLUMN_TYPE_VALUES = new Set(Object.values(COLUMN_TYPE).map(String));
116+
117+
// Check for invalid.
118+
const invalid = values.find((val) => !COLUMN_TYPE_VALUES.has(val));
119+
if (invalid) {
120+
throw new Error(`Invalid ruleListColumns option: ${invalid}`);
121+
}
122+
if (values.length !== new Set(values).size) {
123+
throw new Error('Duplicate value detected in ruleListColumns option.');
124+
}
125+
126+
if (values.length === 0) {
127+
// Use default columns and ordering.
128+
values.push(...COLUMN_TYPE_DEFAULT_ORDERING);
129+
}
130+
131+
return values as COLUMN_TYPE[];
92132
}

lib/rule-list.ts

+37-42
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
EMOJI_HAS_SUGGESTIONS,
66
EMOJI_REQUIRES_TYPE_CHECKING,
77
} from './emojis.js';
8-
import { getConfigsForRule, hasAnyConfigs } from './configs.js';
8+
import { getConfigsForRule } from './configs.js';
99
import { COLUMN_TYPE, getColumns, COLUMN_HEADER } from './rule-list-columns.js';
1010
import { findSectionHeader, format } from './markdown.js';
1111
import { getPluginRoot } from './package-json.js';
@@ -46,44 +46,38 @@ function buildRuleRow(
4646
rule: RuleDetails,
4747
configsToRules: ConfigsToRules,
4848
pluginPrefix: string,
49-
includeTypesColumn: boolean,
5049
configEmojis: ConfigEmojis,
5150
ignoreConfig: string[]
5251
): string[] {
53-
const columns: string[] = [];
54-
if (columnsEnabled[COLUMN_TYPE.NAME]) {
55-
columns.push(`[${rule.name}](docs/rules/${rule.name}.md)`);
56-
}
57-
if (columnsEnabled[COLUMN_TYPE.DESCRIPTION]) {
58-
columns.push(rule.description || '');
59-
}
60-
if (columnsEnabled[COLUMN_TYPE.CONFIGS] && hasAnyConfigs(configsToRules)) {
61-
columns.push(
62-
getConfigurationColumnValueForRule(
63-
rule,
64-
configsToRules,
65-
pluginPrefix,
66-
configEmojis,
67-
ignoreConfig
68-
)
69-
);
70-
}
71-
if (columnsEnabled[COLUMN_TYPE.FIXABLE]) {
72-
columns.push(rule.fixable ? EMOJI_FIXABLE : '');
73-
}
74-
if (columnsEnabled[COLUMN_TYPE.HAS_SUGGESTIONS]) {
75-
columns.push(rule.hasSuggestions ? EMOJI_HAS_SUGGESTIONS : '');
76-
}
77-
if (
78-
columnsEnabled[COLUMN_TYPE.REQUIRES_TYPE_CHECKING] &&
79-
includeTypesColumn
80-
) {
81-
columns.push(rule.requiresTypeChecking ? EMOJI_REQUIRES_TYPE_CHECKING : '');
82-
}
83-
if (columnsEnabled[COLUMN_TYPE.DEPRECATED] && rule.deprecated) {
84-
columns.push(EMOJI_DEPRECATED);
85-
}
86-
return columns;
52+
const columns: {
53+
[key in COLUMN_TYPE]: string;
54+
} = {
55+
// Alphabetical order.
56+
[COLUMN_TYPE.CONFIGS]: getConfigurationColumnValueForRule(
57+
rule,
58+
configsToRules,
59+
pluginPrefix,
60+
configEmojis,
61+
ignoreConfig
62+
),
63+
[COLUMN_TYPE.DEPRECATED]: rule.deprecated ? EMOJI_DEPRECATED : '',
64+
[COLUMN_TYPE.DESCRIPTION]: rule.description || '',
65+
[COLUMN_TYPE.FIXABLE]: rule.fixable ? EMOJI_FIXABLE : '',
66+
[COLUMN_TYPE.HAS_SUGGESTIONS]: rule.hasSuggestions
67+
? EMOJI_HAS_SUGGESTIONS
68+
: '',
69+
[COLUMN_TYPE.NAME]: `[${rule.name}](docs/rules/${rule.name}.md)`,
70+
[COLUMN_TYPE.REQUIRES_TYPE_CHECKING]: rule.requiresTypeChecking
71+
? EMOJI_REQUIRES_TYPE_CHECKING
72+
: '',
73+
};
74+
75+
// List columns using the ordering and presence of columns specified in columnsEnabled.
76+
return Object.keys(columnsEnabled).flatMap((column) =>
77+
columnsEnabled[column as COLUMN_TYPE]
78+
? [columns[column as COLUMN_TYPE]]
79+
: []
80+
);
8781
}
8882

8983
function generateRulesListMarkdown(
@@ -94,10 +88,6 @@ function generateRulesListMarkdown(
9488
configEmojis: ConfigEmojis,
9589
ignoreConfig: string[]
9690
): string {
97-
// Since such rules are rare, we'll only include the types column if at least one rule requires type checking.
98-
const includeTypesColumn = details.some(
99-
(detail: RuleDetails) => detail.requiresTypeChecking
100-
);
10191
const listHeaderRow = (
10292
Object.entries(columns) as [COLUMN_TYPE, boolean][]
10393
).flatMap(([columnType, enabled]) => {
@@ -129,7 +119,6 @@ function generateRulesListMarkdown(
129119
rule,
130120
configsToRules,
131121
pluginPrefix,
132-
includeTypesColumn,
133122
configEmojis,
134123
ignoreConfig
135124
)
@@ -149,6 +138,7 @@ export async function updateRulesList(
149138
pathToPlugin: string,
150139
configEmojis: ConfigEmojis,
151140
ignoreConfig: string[],
141+
ruleListColumns: COLUMN_TYPE[],
152142
urlConfigs?: string
153143
): Promise<string> {
154144
let listStartIndex = markdown.indexOf(BEGIN_RULE_LIST_MARKER);
@@ -187,7 +177,12 @@ export async function updateRulesList(
187177
const postList = markdown.slice(Math.max(0, listEndIndex));
188178

189179
// Determine columns to include in the rules list.
190-
const columns = getColumns(details, configsToRules, ignoreConfig);
180+
const columns = getColumns(
181+
details,
182+
configsToRules,
183+
ruleListColumns,
184+
ignoreConfig
185+
);
191186

192187
// New legend.
193188
const legend = generateLegend(

test/lib/__snapshots__/generator-test.ts.snap

+36
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ exports[`generator #generate deprecated rules updates the documentation 1`] = `
194194
| :----------------------------- | :----------- | :-- |
195195
| [no-bar](docs/rules/no-bar.md) | Description. | ❌ |
196196
| [no-baz](docs/rules/no-baz.md) | Description. | ❌ |
197+
| [no-biz](docs/rules/no-biz.md) | Description. | |
197198
| [no-foo](docs/rules/no-foo.md) | Description. | ❌ |
198199
199200
<!-- end rules list -->"
@@ -226,6 +227,13 @@ exports[`generator #generate deprecated rules updates the documentation 4`] = `
226227
"
227228
`;
228229

230+
exports[`generator #generate deprecated rules updates the documentation 5`] = `
231+
"# Description (\`test/no-biz\`)
232+
233+
<!-- end rule header -->
234+
"
235+
`;
236+
229237
exports[`generator #generate lowercase README file generates the documentation 1`] = `
230238
"<!-- begin rules list -->
231239
@@ -750,6 +758,34 @@ exports[`generator #generate with --ignore-config hides the ignored config 2`] =
750758
"
751759
`;
752760

761+
exports[`generator #generate with --rule-list-columns shows the right columns and legend 1`] = `
762+
"## Rules
763+
<!-- begin rules list -->
764+
765+
💡 Manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).\\
766+
🔧 Automatically fixable by the [\`--fix\` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).
767+
768+
| 💡 | 🔧 | Name |
769+
| :-- | :-- | :----------------------------- |
770+
| 💡 | 🔧 | [no-foo](docs/rules/no-foo.md) |
771+
772+
<!-- end rules list -->
773+
"
774+
`;
775+
776+
exports[`generator #generate with --rule-list-columns shows the right columns and legend 2`] = `
777+
"# Description for no-foo (\`test/no-foo\`)
778+
779+
❌ This rule is deprecated.
780+
781+
🔧 This rule is automatically fixable by the [\`--fix\` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
782+
783+
💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
784+
785+
<!-- end rule header -->
786+
"
787+
`;
788+
753789
exports[`generator #generate with no blank lines around comment markers generates the documentation 1`] = `
754790
"# Rules
755791

0 commit comments

Comments
 (0)