Skip to content

Commit aa0b21c

Browse files
authored
Use arrays instead of CSV strings for config file types (#300)
1 parent e625531 commit aa0b21c

13 files changed

+219
-64
lines changed

lib/cli.ts

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,30 @@ import {
1212
} from './types.js';
1313
import { getCurrentPackageVersion } from './package-json.js';
1414

15-
/** Used for collecting repeated CLI options into an array. */
16-
function collect(value: string, previous: string[]) {
15+
/**
16+
* Used for collecting repeated CLI options into an array.
17+
* Example: --foo bar --foo baz => ['bar', 'baz']
18+
*/
19+
function collect(value: string, previous: string[]): string[] {
1720
return [...previous, value];
1821
}
1922

23+
/**
24+
* Used for collecting CSV CLI options into an array.
25+
* Example: --foo bar,baz,buz => ['bar', 'baz', 'buz']
26+
* */
27+
function collectCSV(value: string, previous: string[]): string[] {
28+
return [...previous, ...value.split(',')];
29+
}
30+
31+
/**
32+
* Used for collecting repeated, nested CSV CLI options into an array of arrays.
33+
* Example: --foo baz,bar --foo biz,buz => [['baz', 'bar'], ['biz', 'buz']]
34+
* */
35+
function collectCSVNested(value: string, previous: string[][]): string[][] {
36+
return [...previous, value.split(',')];
37+
}
38+
2039
function parseBoolean(value: string | undefined): boolean {
2140
return ['true', undefined].includes(value);
2241
}
@@ -39,21 +58,42 @@ async function loadConfigFileOptions(): Promise<GenerateOptions> {
3958
minLength: 1,
4059
},
4160
};
61+
62+
const schemaConfigEmoji = {
63+
// Top-level array.
64+
type: 'array',
65+
uniqueItems: true,
66+
minItems: 1,
67+
items: {
68+
// Nested array (config/emoji tuple).
69+
type: 'array',
70+
uniqueItems: true,
71+
minItems: 1, // Allowed to pass only config name to remove default emoji.
72+
maxItems: 2, // Normally, two items will be passed.
73+
items: {
74+
type: 'string',
75+
minLength: 1,
76+
},
77+
},
78+
};
79+
4280
const properties: { [key in OPTION_TYPE]: unknown } = {
4381
check: { type: 'boolean' },
44-
configEmoji: schemaStringArray,
82+
configEmoji: schemaConfigEmoji,
4583
ignoreConfig: schemaStringArray,
4684
ignoreDeprecatedRules: { type: 'boolean' },
4785
initRuleDocs: { type: 'boolean' },
4886
pathRuleDoc: { type: 'string' },
4987
pathRuleList: { anyOf: [{ type: 'string' }, schemaStringArray] },
50-
postprocess: {},
51-
ruleDocNotices: { type: 'string' },
88+
postprocess: {
89+
/* JSON Schema can't validate functions so check this later */
90+
},
91+
ruleDocNotices: schemaStringArray,
5292
ruleDocSectionExclude: schemaStringArray,
5393
ruleDocSectionInclude: schemaStringArray,
5494
ruleDocSectionOptions: { type: 'boolean' },
5595
ruleDocTitleFormat: { type: 'string' },
56-
ruleListColumns: { type: 'string' },
96+
ruleListColumns: schemaStringArray,
5797
splitBy: { type: 'string' },
5898
urlConfigs: { type: 'string' },
5999
urlRuleDoc: { type: 'string' },
@@ -116,7 +156,7 @@ export async function run(
116156
.option(
117157
'--config-emoji <config-emoji>',
118158
'(optional) Custom emoji to use for a config. Format is `config-name,emoji`. Default emojis are provided for common configs. To remove a default emoji and rely on a badge instead, provide the config name without an emoji. Option can be repeated.',
119-
collect,
159+
collectCSVNested,
120160
[]
121161
)
122162
.option(
@@ -159,7 +199,9 @@ export async function run(
159199
NOTICE_TYPE
160200
).join('", "')}") (default: ${
161201
OPTION_DEFAULTS[OPTION_TYPE.RULE_DOC_NOTICES]
162-
})`
202+
})`,
203+
collectCSV,
204+
[]
163205
)
164206
.option(
165207
'--rule-doc-section-exclude <section>',
@@ -194,7 +236,9 @@ export async function run(
194236
COLUMN_TYPE
195237
).join('", "')})" (default: ${
196238
OPTION_DEFAULTS[OPTION_TYPE.RULE_LIST_COLUMNS]
197-
})`
239+
})`,
240+
collectCSV,
241+
[]
198242
)
199243
.option(
200244
'--split-by <property>',

lib/generator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ export async function generate(path: string, options?: GenerateOptions) {
119119
options?.pathRuleList,
120120
OPTION_DEFAULTS[OPTION_TYPE.PATH_RULE_LIST]
121121
);
122+
const postprocess =
123+
options?.postprocess ?? OPTION_DEFAULTS[OPTION_TYPE.POSTPROCESS];
122124
const ruleDocNotices = parseRuleDocNoticesOption(options?.ruleDocNotices);
123125
const ruleDocSectionExclude = stringOrArrayWithFallback(
124126
options?.ruleDocSectionExclude,
@@ -140,8 +142,6 @@ export async function generate(path: string, options?: GenerateOptions) {
140142
options?.urlConfigs ?? OPTION_DEFAULTS[OPTION_TYPE.URL_CONFIGS];
141143
const urlRuleDoc =
142144
options?.urlRuleDoc ?? OPTION_DEFAULTS[OPTION_TYPE.URL_RULE_DOC];
143-
const postprocess =
144-
options?.postprocess ?? OPTION_DEFAULTS[OPTION_TYPE.POSTPROCESS];
145145

146146
// Gather details about rules.
147147
const details: RuleDetails[] = Object.entries(plugin.rules)

lib/option-parsers.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ import type { Plugin, ConfigEmojis } from './types.js';
1515
*/
1616
export function parseConfigEmojiOptions(
1717
plugin: Plugin,
18-
configEmoji?: string[]
18+
configEmoji?: string[][]
1919
): ConfigEmojis {
2020
const configsSeen = new Set<string>();
2121
const configsWithDefaultEmojiRemoved: string[] = [];
2222
const configEmojis =
2323
configEmoji?.flatMap((configEmojiItem) => {
24-
const [config, emoji, ...extra] = configEmojiItem.split(',');
24+
const [config, emoji, ...extra] = configEmojiItem;
2525

2626
// Check for duplicate configs.
2727
if (configsSeen.has(config)) {
@@ -75,9 +75,9 @@ export function parseConfigEmojiOptions(
7575
* Parse the option, check for errors, and set defaults.
7676
*/
7777
export function parseRuleListColumnsOption(
78-
ruleListColumns: string | undefined
78+
ruleListColumns: string[] | undefined
7979
): COLUMN_TYPE[] {
80-
const values = ruleListColumns ? ruleListColumns.split(',') : [];
80+
const values = ruleListColumns ?? [];
8181
const VALUES_OF_TYPE = new Set(Object.values(COLUMN_TYPE).map(String));
8282

8383
// Check for invalid.
@@ -105,9 +105,9 @@ export function parseRuleListColumnsOption(
105105
* Parse the option, check for errors, and set defaults.
106106
*/
107107
export function parseRuleDocNoticesOption(
108-
ruleDocNotices: string | undefined
108+
ruleDocNotices: string[] | undefined
109109
): NOTICE_TYPE[] {
110-
const values = ruleDocNotices ? ruleDocNotices.split(',') : [];
110+
const values = ruleDocNotices ?? [];
111111
const VALUES_OF_TYPE = new Set(Object.values(NOTICE_TYPE).map(String));
112112

113113
// Check for invalid.

lib/options.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,12 @@ export const OPTION_DEFAULTS = {
4747
[OPTION_TYPE.INIT_RULE_DOCS]: false,
4848
[OPTION_TYPE.PATH_RULE_DOC]: join('docs', 'rules', '{name}.md'),
4949
[OPTION_TYPE.PATH_RULE_LIST]: 'README.md',
50+
[OPTION_TYPE.POSTPROCESS]: (content: string) => content,
5051
[OPTION_TYPE.RULE_DOC_NOTICES]: Object.entries(
5152
NOTICE_TYPE_DEFAULT_PRESENCE_AND_ORDERING
5253
)
5354
.filter(([_col, enabled]) => enabled)
54-
.map(([col]) => col)
55-
.join(','),
55+
.map(([col]) => col),
5656
[OPTION_TYPE.RULE_DOC_SECTION_EXCLUDE]: [],
5757
[OPTION_TYPE.RULE_DOC_SECTION_INCLUDE]: [],
5858
[OPTION_TYPE.RULE_DOC_SECTION_OPTIONS]: true,
@@ -61,10 +61,8 @@ export const OPTION_DEFAULTS = {
6161
COLUMN_TYPE_DEFAULT_PRESENCE_AND_ORDERING
6262
)
6363
.filter(([_col, enabled]) => enabled)
64-
.map(([col]) => col)
65-
.join(','),
64+
.map(([col]) => col),
6665
[OPTION_TYPE.SPLIT_BY]: undefined,
6766
[OPTION_TYPE.URL_CONFIGS]: undefined,
6867
[OPTION_TYPE.URL_RULE_DOC]: undefined,
69-
[OPTION_TYPE.POSTPROCESS]: (content: string) => content,
7068
} satisfies Record<OPTION_TYPE, unknown>; // Satisfies is used to ensure all options are included, but without losing type information.

lib/types.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export enum OPTION_TYPE {
9191
INIT_RULE_DOCS = 'initRuleDocs',
9292
PATH_RULE_DOC = 'pathRuleDoc',
9393
PATH_RULE_LIST = 'pathRuleList',
94+
POSTPROCESS = 'postprocess',
9495
RULE_DOC_NOTICES = 'ruleDocNotices',
9596
RULE_DOC_SECTION_EXCLUDE = 'ruleDocSectionExclude',
9697
RULE_DOC_SECTION_INCLUDE = 'ruleDocSectionInclude',
@@ -100,7 +101,6 @@ export enum OPTION_TYPE {
100101
SPLIT_BY = 'splitBy',
101102
URL_CONFIGS = 'urlConfigs',
102103
URL_RULE_DOC = 'urlRuleDoc',
103-
POSTPROCESS = 'postprocess',
104104
}
105105

106106
// JSDocs for options should be kept in sync with README.md and the CLI runner in cli.ts.
@@ -111,11 +111,11 @@ export type GenerateOptions = {
111111
check?: boolean;
112112
/**
113113
* List of configs and their associated emojis.
114-
* Format is `config-name,emoji`.
114+
* Array of `[configName, emoji]`.
115115
* Default emojis are provided for common configs.
116116
* To remove a default emoji and rely on a badge instead, provide the config name without an emoji.
117117
*/
118-
configEmoji?: string[];
118+
configEmoji?: string[][];
119119
/** Configs to ignore from being displayed. Often used for an `all` config. */
120120
ignoreConfig?: string[];
121121
/** Whether to ignore deprecated rules from being checked, displayed, or updated. Default: `false`. */
@@ -135,12 +135,12 @@ export type GenerateOptions = {
135135
pathToFile: string
136136
) => string | Promise<string>;
137137
/**
138-
* Ordered, comma-separated list of notices to display in rule doc.
138+
* Ordered list of notices to display in rule doc.
139139
* Non-applicable notices will be hidden.
140140
* Choices: `configs`, `deprecated`, `fixable` (off by default), `fixableAndHasSuggestions`, `hasSuggestions` (off by default), `options` (off by default), `requiresTypeChecking`, `type` (off by default).
141-
* Default: `deprecated,configs,fixableAndHasSuggestions,requiresTypeChecking`.
141+
* Default: `['deprecated', 'configs', 'fixableAndHasSuggestions', 'requiresTypeChecking']`.
142142
*/
143-
ruleDocNotices?: string;
143+
ruleDocNotices?: NOTICE_TYPE[];
144144
/** Disallowed sections in each rule doc. Exit with failure if present. */
145145
ruleDocSectionExclude?: string[];
146146
/** Required sections in each rule doc. Exit with failure if missing. */
@@ -150,12 +150,12 @@ export type GenerateOptions = {
150150
/** The format to use for rule doc titles. Default: `desc-parens-prefix-name`. */
151151
ruleDocTitleFormat?: RuleDocTitleFormat;
152152
/**
153-
* Ordered, comma-separated list of columns to display in rule list.
153+
* Ordered list of columns to display in rule list.
154154
* Empty columns will be hidden.
155155
* Choices: `configsError`, `configsOff`, `configsWarn`, `deprecated`, `description`, `fixable`, `fixableAndHasSuggestions` (off by default), `hasSuggestions`, `name`, `options` (off by default), `requiresTypeChecking`, `type` (off by default).
156-
* Default: `name,description,configsError,configsWarn,configsOff,fixable,hasSuggestions,requiresTypeChecking,deprecated`.
156+
* Default: `['name', 'description', 'configsError', 'configsWarn', 'configsOff', 'fixable', 'hasSuggestions', 'requiresTypeChecking', 'deprecated']`.
157157
*/
158-
ruleListColumns?: string;
158+
ruleListColumns?: COLUMN_TYPE[];
159159
/**
160160
* Rule property to split the rules list by.
161161
* A separate list and header will be created for each value.

0 commit comments

Comments
 (0)