Skip to content

Commit 05687c0

Browse files
authored
Use readonly for external/array types (#303)
1 parent e625e30 commit 05687c0

13 files changed

+133
-128
lines changed

lib/cli.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,32 @@ import { getCurrentPackageVersion } from './package-json.js';
1616
* Used for collecting repeated CLI options into an array.
1717
* Example: --foo bar --foo baz => ['bar', 'baz']
1818
*/
19-
function collect(value: string, previous: string[]): string[] {
19+
function collect(
20+
value: string,
21+
previous: readonly string[]
22+
): readonly string[] {
2023
return [...previous, value];
2124
}
2225

2326
/**
2427
* Used for collecting CSV CLI options into an array.
2528
* Example: --foo bar,baz,buz => ['bar', 'baz', 'buz']
2629
* */
27-
function collectCSV(value: string, previous: string[]): string[] {
30+
function collectCSV(
31+
value: string,
32+
previous: readonly string[]
33+
): readonly string[] {
2834
return [...previous, ...value.split(',')];
2935
}
3036

3137
/**
3238
* Used for collecting repeated, nested CSV CLI options into an array of arrays.
3339
* Example: --foo baz,bar --foo biz,buz => [['baz', 'bar'], ['biz', 'buz']]
3440
* */
35-
function collectCSVNested(value: string, previous: string[][]): string[][] {
41+
function collectCSVNested(
42+
value: string,
43+
previous: readonly string[][]
44+
): readonly string[][] {
3645
return [...previous, value.split(',')];
3746
}
3847

@@ -116,13 +125,18 @@ async function loadConfigFileOptions(): Promise<GenerateOptions> {
116125
);
117126
}
118127

119-
if (
120-
explorerResults.config.postprocess &&
121-
typeof explorerResults.config.postprocess !== 'function'
122-
) {
128+
const config = explorerResults.config;
129+
130+
// Additional validation that couldn't be handled by ajv.
131+
if (config.postprocess && typeof config.postprocess !== 'function') {
123132
throw new Error('postprocess must be a function');
124133
}
125134

135+
// Perform any normalization.
136+
if (typeof config.pathRuleList === 'string') {
137+
config.pathRuleList = [config.pathRuleList];
138+
}
139+
126140
return explorerResults.config;
127141
}
128142
return {};
@@ -135,7 +149,7 @@ async function loadConfigFileOptions(): Promise<GenerateOptions> {
135149
* Note: Does not introduce default values. Default values should be handled in the callback function.
136150
*/
137151
export async function run(
138-
argv: string[],
152+
argv: readonly string[],
139153
cb: (path: string, options: GenerateOptions) => Promise<void>
140154
) {
141155
const program = new Command();
@@ -259,11 +273,6 @@ export async function run(
259273
// Default values should be handled in the callback function.
260274
const configFileOptions = await loadConfigFileOptions();
261275

262-
// Perform any normalization needed ahead of merging.
263-
if (typeof configFileOptions.pathRuleList === 'string') {
264-
configFileOptions.pathRuleList = [configFileOptions.pathRuleList];
265-
}
266-
267276
const generateOptions = merge(configFileOptions, options); // Recursive merge.
268277

269278
// Invoke callback.

lib/generator.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function expectContent(
5757
function expectSectionHeader(
5858
ruleName: string,
5959
contents: string,
60-
possibleHeaders: string[],
60+
possibleHeaders: readonly string[],
6161
expected: boolean
6262
) {
6363
const found = possibleHeaders.some((header) =>
@@ -84,7 +84,7 @@ function expectSectionHeader(
8484
}
8585
}
8686

87-
function stringOrArrayWithFallback<T extends string | string[]>(
87+
function stringOrArrayWithFallback<T extends string | readonly string[]>(
8888
stringOrArray: undefined | T,
8989
fallback: T
9090
): T {
@@ -145,7 +145,7 @@ export async function generate(path: string, options?: GenerateOptions) {
145145
options?.urlRuleDoc ?? OPTION_DEFAULTS[OPTION_TYPE.URL_RULE_DOC];
146146

147147
// Gather details about rules.
148-
const details: RuleDetails[] = Object.entries(plugin.rules)
148+
const details: readonly RuleDetails[] = Object.entries(plugin.rules)
149149
.map(([name, rule]): RuleDetails => {
150150
return typeof rule === 'object'
151151
? // Object-style rule.
@@ -177,6 +177,9 @@ export async function generate(path: string, options?: GenerateOptions) {
177177
.filter(
178178
// Filter out deprecated rules from being checked, displayed, or updated if the option is set.
179179
(details) => !ignoreDeprecatedRules || !details.deprecated
180+
)
181+
.sort(({ name: a }, { name: b }) =>
182+
a.toLowerCase().localeCompare(b.toLowerCase())
180183
);
181184

182185
// Update rule doc for each rule.

lib/option-parsers.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type { Plugin, ConfigEmojis } from './types.js';
1515
*/
1616
export function parseConfigEmojiOptions(
1717
plugin: Plugin,
18-
configEmoji?: string[][]
18+
configEmoji?: readonly string[][]
1919
): ConfigEmojis {
2020
const configsSeen = new Set<string>();
2121
const configsWithDefaultEmojiRemoved: string[] = [];
@@ -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
79-
): COLUMN_TYPE[] {
80-
const values = ruleListColumns ?? [];
78+
ruleListColumns: readonly string[] | undefined
79+
): readonly COLUMN_TYPE[] {
80+
const values = [...(ruleListColumns ?? [])];
8181
const VALUES_OF_TYPE = new Set(Object.values(COLUMN_TYPE).map(String));
8282

8383
// Check for invalid.
@@ -98,16 +98,16 @@ export function parseRuleListColumnsOption(
9898
);
9999
}
100100

101-
return values as COLUMN_TYPE[];
101+
return values as readonly COLUMN_TYPE[];
102102
}
103103

104104
/**
105105
* Parse the option, check for errors, and set defaults.
106106
*/
107107
export function parseRuleDocNoticesOption(
108-
ruleDocNotices: string[] | undefined
109-
): NOTICE_TYPE[] {
110-
const values = ruleDocNotices ?? [];
108+
ruleDocNotices: readonly string[] | undefined
109+
): readonly NOTICE_TYPE[] {
110+
const values = [...(ruleDocNotices ?? [])];
111111
const VALUES_OF_TYPE = new Set(Object.values(NOTICE_TYPE).map(String));
112112

113113
// Check for invalid.
@@ -128,5 +128,5 @@ export function parseRuleDocNoticesOption(
128128
);
129129
}
130130

131-
return values as NOTICE_TYPE[];
131+
return values as readonly NOTICE_TYPE[];
132132
}

lib/package-json.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,8 @@ export async function loadPlugin(path: string): Promise<Plugin> {
4343
) {
4444
// Check various properties on the `exports` object.
4545
// https://nodejs.org/api/packages.html#conditional-exports
46-
const propertiesToCheck: (keyof PackageJson.ExportConditions)[] = [
47-
'.',
48-
'node',
49-
'import',
50-
'require',
51-
'default',
52-
];
46+
const propertiesToCheck: readonly (keyof PackageJson.ExportConditions)[] =
47+
['.', 'node', 'import', 'require', 'default'];
5348
for (const prop of propertiesToCheck) {
5449
// @ts-expect-error -- The union type for the object is causing trouble.
5550
const value = exports[prop];

lib/plugin-config-resolution.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ async function resolveConfigRules(config: Config): Promise<Rules> {
3333
}
3434

3535
async function resolveConfigExtends(
36-
extendItems: string[] | string
36+
extendItems: readonly string[] | string
3737
): Promise<Rules> {
3838
const rules: Rules = {};
3939
for (const extend of Array.isArray(extendItems)

lib/plugin-configs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export function getConfigsThatSetARule(
1010
plugin: Plugin,
1111
configsToRules: ConfigsToRules,
1212
pluginPrefix: string,
13-
ignoreConfig: string[],
13+
ignoreConfig: readonly string[],
1414
severityType?: SEVERITY_TYPE
1515
) {
1616
/* istanbul ignore next -- this shouldn't happen */

lib/rule-doc-notices.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ function severityToTerminology(severity: SEVERITY_TYPE) {
3737
}
3838

3939
function configsToNoticeSentence(
40-
configs: string[],
40+
configs: readonly string[],
4141
severity: SEVERITY_TYPE,
4242
configsLinkOrWord: string,
4343
configLinkOrWord: string,
@@ -75,9 +75,9 @@ const RULE_NOTICES: {
7575
| undefined
7676
| ((data: {
7777
ruleName: string;
78-
configsError: string[];
79-
configsWarn: string[];
80-
configsOff: string[];
78+
configsError: readonly string[];
79+
configsWarn: readonly string[];
80+
configsOff: readonly string[];
8181
configEmojis: ConfigEmojis;
8282
fixable: boolean;
8383
hasSuggestions: boolean;
@@ -228,10 +228,10 @@ const RULE_NOTICES: {
228228
*/
229229
function getNoticesForRule(
230230
rule: RuleModule,
231-
configsError: string[],
232-
configsWarn: string[],
233-
configsOff: string[],
234-
ruleDocNotices: NOTICE_TYPE[]
231+
configsError: readonly string[],
232+
configsWarn: readonly string[],
233+
configsOff: readonly string[],
234+
ruleDocNotices: readonly NOTICE_TYPE[]
235235
) {
236236
const notices: {
237237
[key in NOTICE_TYPE]: boolean;
@@ -272,8 +272,8 @@ function getRuleNoticeLines(
272272
pathPlugin: string,
273273
pathRuleDoc: string,
274274
configEmojis: ConfigEmojis,
275-
ignoreConfig: string[],
276-
ruleDocNotices: NOTICE_TYPE[],
275+
ignoreConfig: readonly string[],
276+
ruleDocNotices: readonly NOTICE_TYPE[],
277277
urlConfigs?: string,
278278
urlRuleDoc?: string
279279
) {
@@ -429,8 +429,8 @@ export function generateRuleHeaderLines(
429429
pathPlugin: string,
430430
pathRuleDoc: string,
431431
configEmojis: ConfigEmojis,
432-
ignoreConfig: string[],
433-
ruleDocNotices: NOTICE_TYPE[],
432+
ignoreConfig: readonly string[],
433+
ruleDocNotices: readonly NOTICE_TYPE[],
434434
ruleDocTitleFormat: RuleDocTitleFormat,
435435
urlConfigs?: string,
436436
urlRuleDoc?: string

lib/rule-list-columns.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import type { RuleDetails, ConfigsToRules, Plugin } from './types.js';
1717
* An object containing the column header for each column (as a string or function to generate the string).
1818
*/
1919
export const COLUMN_HEADER: {
20-
[key in COLUMN_TYPE]: string | ((data: { details: RuleDetails[] }) => string);
20+
[key in COLUMN_TYPE]:
21+
| string
22+
| ((data: { details: readonly RuleDetails[] }) => string);
2123
} = {
2224
[COLUMN_TYPE.NAME]: ({ details }) => {
2325
const ruleNames = details.map((detail) => detail.name);
@@ -65,11 +67,11 @@ export const COLUMN_HEADER: {
6567
*/
6668
export function getColumns(
6769
plugin: Plugin,
68-
details: RuleDetails[],
70+
details: readonly RuleDetails[],
6971
configsToRules: ConfigsToRules,
70-
ruleListColumns: COLUMN_TYPE[],
72+
ruleListColumns: readonly COLUMN_TYPE[],
7173
pluginPrefix: string,
72-
ignoreConfig: string[]
74+
ignoreConfig: readonly string[]
7375
): Record<COLUMN_TYPE, boolean> {
7476
const columns: {
7577
[key in COLUMN_TYPE]: boolean;

lib/rule-list-legend.ts

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,16 @@ const LEGEND_HAS_SUGGESTIONS = `${EMOJI_HAS_SUGGESTIONS} Manually fixable by [ed
3434
*/
3535
const LEGENDS: {
3636
[key in COLUMN_TYPE]:
37-
| string[]
37+
| readonly string[]
3838
| undefined // For no legend.
3939
| ((data: {
4040
plugin: Plugin;
4141
configsToRules: ConfigsToRules;
4242
configEmojis: ConfigEmojis;
4343
pluginPrefix: string;
44-
ignoreConfig: string[];
44+
ignoreConfig: readonly string[];
4545
urlConfigs?: string;
46-
}) => string[]);
46+
}) => readonly string[]);
4747
} = {
4848
[COLUMN_TYPE.CONFIGS_ERROR]: ({
4949
plugin,
@@ -155,7 +155,7 @@ function getLegendForConfigColumnOfSeverity({
155155
configsToRules: ConfigsToRules;
156156
configEmojis: ConfigEmojis;
157157
pluginPrefix: string;
158-
ignoreConfig: string[];
158+
ignoreConfig: readonly string[];
159159
severityType: SEVERITY_TYPE;
160160
urlConfigs?: string;
161161
}): string {
@@ -186,9 +186,9 @@ function getLegendsForIndividualConfigs({
186186
configsToRules: ConfigsToRules;
187187
configEmojis: ConfigEmojis;
188188
pluginPrefix: string;
189-
ignoreConfig: string[];
189+
ignoreConfig: readonly string[];
190190
urlConfigs?: string;
191-
}): string[] {
191+
}): readonly string[] {
192192
/* istanbul ignore next -- this shouldn't happen */
193193
if (!plugin.configs || !plugin.rules) {
194194
throw new Error(
@@ -225,32 +225,32 @@ export function generateLegend(
225225
configsToRules: ConfigsToRules,
226226
configEmojis: ConfigEmojis,
227227
pluginPrefix: string,
228-
ignoreConfig: string[],
228+
ignoreConfig: readonly string[],
229229
urlConfigs?: string
230230
) {
231-
const legends = (Object.entries(columns) as [COLUMN_TYPE, boolean][]).flatMap(
232-
([columnType, enabled]) => {
233-
if (!enabled) {
234-
// This column is turned off.
235-
return [];
236-
}
237-
const legendArrayOrFn = LEGENDS[columnType];
238-
if (!legendArrayOrFn) {
239-
// No legend specified for this column.
240-
return [];
241-
}
242-
return typeof legendArrayOrFn === 'function'
243-
? legendArrayOrFn({
244-
plugin,
245-
configsToRules,
246-
configEmojis,
247-
pluginPrefix,
248-
urlConfigs,
249-
ignoreConfig,
250-
})
251-
: legendArrayOrFn;
231+
const legends = (
232+
Object.entries(columns) as readonly [COLUMN_TYPE, boolean][]
233+
).flatMap(([columnType, enabled]) => {
234+
if (!enabled) {
235+
// This column is turned off.
236+
return [];
252237
}
253-
);
238+
const legendArrayOrFn = LEGENDS[columnType];
239+
if (!legendArrayOrFn) {
240+
// No legend specified for this column.
241+
return [];
242+
}
243+
return typeof legendArrayOrFn === 'function'
244+
? legendArrayOrFn({
245+
plugin,
246+
configsToRules,
247+
configEmojis,
248+
pluginPrefix,
249+
urlConfigs,
250+
ignoreConfig,
251+
})
252+
: legendArrayOrFn;
253+
});
254254

255255
if (legends.some((legend) => legend.includes('Configurations'))) {
256256
// Add legends for individual configs after the config column legend(s).

0 commit comments

Comments
 (0)