@yuu1111/quality-check 1.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/config.ts DELETED
@@ -1,490 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { resolve } from "node:path";
3
- import { pathToFileURL } from "node:url";
4
- import {
5
- OPT_IN_RULE_IDS as COMMENT_CHECK_OPT_IN_RULE_IDS,
6
- RULE_IDS as COMMENT_CHECK_RULE_IDS,
7
- type OptInRuleId as CommentCheckRuleName,
8
- } from "@yuu1111/comment-check/rule-ids";
9
- import {
10
- OPT_IN_RULE_IDS as DOCUMENT_STYLE_CHECK_OPT_IN_RULE_IDS,
11
- RULE_IDS as DOCUMENT_STYLE_CHECK_RULE_IDS,
12
- type OptInRuleId as DocumentStyleCheckRuleName,
13
- } from "@yuu1111/document-style-check/rule-ids";
14
- import {
15
- OPT_IN_RULE_IDS as TSDOC_CHECK_OPT_IN_RULE_IDS,
16
- KNOWN_RULE_NAMES as TSDOC_CHECK_RULE_NAMES,
17
- type TsdocRule,
18
- } from "@yuu1111/tsdoc-check/rule-ids";
19
-
20
- /**
21
- * 統合CLIが起動できるengineの名前 並び順が実行順になる
22
- */
23
- export const ENGINE_NAMES = [
24
- "biome",
25
- "typecheck",
26
- "knip",
27
- "code-style-check",
28
- "comment-check",
29
- "document-style-check",
30
- "tsdoc-check",
31
- ] as const;
32
-
33
- /**
34
- * engine名のunion型
35
- */
36
- export type EngineName = (typeof ENGINE_NAMES)[number];
37
-
38
- /**
39
- * engineへ渡す起動条件
40
- */
41
- export interface EngineOptions {
42
- /** 検査から外すpath engineが受け取れないときはreportへ出す */
43
- ignore?: string[];
44
- /** 検査する対象path engineが受け取れないときはreportへ出す */
45
- targets?: string[];
46
- /** engineの既定引数の後ろへ足す引数 */
47
- args?: string[];
48
- }
49
-
50
- /**
51
- * comment-checkへ渡す起動条件
52
- */
53
- export interface CommentCheckOptions extends EngineOptions {
54
- /** 既定で無効のopt-in ruleを全て有効にする */
55
- enable?: boolean;
56
- /** rule名ごとの状態 offで無効 onで有効 省略したruleはengineの既定に従う */
57
- rules?: Partial<Record<CommentCheckRuleName, "off" | "on">>;
58
- }
59
-
60
- /**
61
- * document-style-checkへ渡す起動条件
62
- */
63
- export interface DocumentStyleCheckOptions extends EngineOptions {
64
- /** 既定で無効のopt-in ruleを全て有効にする */
65
- enable?: boolean;
66
- /** rule名ごとの状態 offで無効 onで有効 省略したruleはengineの既定に従う */
67
- rules?: Partial<Record<DocumentStyleCheckRuleName, "off" | "on">>;
68
- }
69
-
70
- /**
71
- * TSDoc検査へ渡す起動条件
72
- */
73
- export interface TsdocCheckOptions extends EngineOptions {
74
- /** 既定で無効のopt-in ruleを全て有効にする */
75
- enable?: boolean;
76
- /** 全てのruleを違反として扱う opt-in ruleは有効にしてから上げる */
77
- error?: boolean;
78
- /** rule名ごとの状態 offで無効 onで既定のseverity errorで違反として扱う */
79
- rules?: Partial<Record<TsdocRule, "off" | "on" | "error">>;
80
- }
81
-
82
- /**
83
- * 型検査へ渡す起動条件
84
- */
85
- export interface TypecheckOptions extends EngineOptions {
86
- /** 型検査するtsconfigのpath 省略時はカレントのtsconfig.jsonを1回だけ読む */
87
- projects?: string[];
88
- }
89
-
90
- /**
91
- * engine名ごとの起動条件
92
- */
93
- export interface EngineConfigMap {
94
- biome: EngineOptions;
95
- typecheck: TypecheckOptions;
96
- knip: EngineOptions;
97
- "code-style-check": EngineOptions;
98
- "comment-check": CommentCheckOptions;
99
- "document-style-check": DocumentStyleCheckOptions;
100
- "tsdoc-check": TsdocCheckOptions;
101
- }
102
-
103
- /**
104
- * rule名ごとの状態
105
- */
106
- export type RuleState = "off" | "on" | "error";
107
-
108
- /**
109
- * engineが公開するrule語彙 全ruleと既定で無効のopt-in ruleを持つ
110
- */
111
- export const RULE_VOCABULARY: Record<
112
- EngineName,
113
- { all: readonly string[]; optIn: readonly string[]; promotes: boolean }
114
- > = {
115
- biome: { all: [], optIn: [], promotes: false },
116
- typecheck: { all: [], optIn: [], promotes: false },
117
- knip: { all: [], optIn: [], promotes: false },
118
- "code-style-check": { all: [], optIn: [], promotes: false },
119
- "comment-check": {
120
- all: COMMENT_CHECK_RULE_IDS,
121
- optIn: COMMENT_CHECK_OPT_IN_RULE_IDS,
122
- promotes: false,
123
- },
124
- "document-style-check": {
125
- all: DOCUMENT_STYLE_CHECK_RULE_IDS,
126
- optIn: DOCUMENT_STYLE_CHECK_OPT_IN_RULE_IDS,
127
- promotes: false,
128
- },
129
- "tsdoc-check": {
130
- all: TSDOC_CHECK_RULE_NAMES,
131
- optIn: TSDOC_CHECK_OPT_IN_RULE_IDS,
132
- promotes: true,
133
- },
134
- };
135
-
136
- /**
137
- * quality.config.tsが受け付ける統合検査の設定
138
- */
139
- export interface QualityConfig {
140
- /** 起動するengine 値がfalseまたは未指定のengineは起動しない */
141
- engines: Partial<Record<EngineName, boolean>>;
142
- /** engineごとの起動条件 省略したengineは既定値で起動する */
143
- config?: Partial<EngineConfigMap>;
144
- /** baseline fileのpath falseなら差分判定を行わない */
145
- baseline?: string | false;
146
- }
147
-
148
- /**
149
- * 設定fileを型付けするための恒等関数
150
- *
151
- * @param config - 型付けする統合検査の設定
152
- * @returns 引数をそのまま返した統合検査の設定
153
- */
154
- export function defineConfig(config: QualityConfig): QualityConfig {
155
- return config;
156
- }
157
-
158
- /**
159
- * config fileを探索する既定のfile名
160
- */
161
- export const DEFAULT_CONFIG_FILES = [
162
- "quality.config.ts",
163
- "quality.config.mts",
164
- "quality.config.js",
165
- "quality.config.mjs",
166
- ] as const;
167
-
168
- /**
169
- * baseline fileの既定名
170
- */
171
- export const DEFAULT_BASELINE_FILE = "quality-baseline.json";
172
-
173
- type JsonObject = Record<string, unknown>;
174
-
175
- function isJsonObject(value: unknown): value is JsonObject {
176
- return typeof value === "object" && value !== null && !Array.isArray(value);
177
- }
178
-
179
- function isEngineName(value: string): value is EngineName {
180
- return (ENGINE_NAMES as readonly string[]).includes(value);
181
- }
182
-
183
- const ENGINE_OPTION_KEYS: Record<EngineName, readonly string[]> = {
184
- biome: ["args", "ignore", "targets"],
185
- typecheck: ["args", "ignore", "projects", "targets"],
186
- knip: ["args", "ignore", "targets"],
187
- "code-style-check": ["args", "ignore", "targets"],
188
- "comment-check": ["args", "enable", "ignore", "rules", "targets"],
189
- "document-style-check": ["args", "enable", "ignore", "rules", "targets"],
190
- "tsdoc-check": ["args", "enable", "error", "ignore", "rules", "targets"],
191
- };
192
-
193
- function readStringArray(value: unknown, field: string): string[] | undefined {
194
- if (value === undefined) {
195
- return undefined;
196
- }
197
- if (
198
- !Array.isArray(value) ||
199
- value.some((entry) => typeof entry !== "string" || entry === "")
200
- ) {
201
- throw new Error(`${field} must be an array of non-empty strings`);
202
- }
203
- return [...value] as string[];
204
- }
205
-
206
- function readBoolean(value: unknown, field: string): boolean | undefined {
207
- if (value === undefined) {
208
- return undefined;
209
- }
210
- if (typeof value !== "boolean") {
211
- throw new Error(`${field} must be a boolean`);
212
- }
213
- return value;
214
- }
215
-
216
- /**
217
- * rule名ごとの状態を読み取る engineが公開する語彙とengineが受け付ける状態で検証する
218
- */
219
- function readRuleStates(
220
- value: unknown,
221
- field: string,
222
- name: EngineName,
223
- ): Record<string, RuleState> | undefined {
224
- if (value === undefined) {
225
- return undefined;
226
- }
227
- if (!isJsonObject(value)) {
228
- throw new Error(`${field} must be an object`);
229
- }
230
- const vocabulary = RULE_VOCABULARY[name];
231
- const states: Record<string, RuleState> = {};
232
- for (const [rule, state] of Object.entries(value)) {
233
- if (!vocabulary.all.includes(rule)) {
234
- throw new Error(`${field} has an unknown rule: ${rule}`);
235
- }
236
- if (state !== "off" && state !== "on" && state !== "error") {
237
- throw new Error(`${field}.${rule} must be "off", "on" or "error"`);
238
- }
239
- if (state === "error" && !vocabulary.promotes) {
240
- throw new Error(
241
- `${field}.${rule}: ${name} cannot treat a rule as an error`,
242
- );
243
- }
244
- if (state === "off" && !vocabulary.optIn.includes(rule)) {
245
- throw new Error(
246
- `${field}.${rule}: ${name} cannot turn off a rule that is on by default`,
247
- );
248
- }
249
- states[rule] = state;
250
- }
251
- return states;
252
- }
253
-
254
- function readBaseline(
255
- value: unknown,
256
- source: string,
257
- ): string | false | undefined {
258
- if (value === undefined || value === false) {
259
- return value;
260
- }
261
- if (typeof value === "string" && value !== "") {
262
- return value;
263
- }
264
- throw new Error(`${source}: baseline must be a path string or false`);
265
- }
266
-
267
- function parseEngines(
268
- value: unknown,
269
- source: string,
270
- ): Partial<Record<EngineName, boolean>> {
271
- if (!isJsonObject(value)) {
272
- throw new Error(`${source} must export an engines object`);
273
- }
274
- const engines: Partial<Record<EngineName, boolean>> = {};
275
- for (const [name, enabled] of Object.entries(value)) {
276
- if (!isEngineName(name)) {
277
- throw new Error(`${source}: unknown engine: ${name}`);
278
- }
279
- if (typeof enabled !== "boolean") {
280
- throw new Error(
281
- `${source}: engines.${name} must be a boolean 起動条件はconfigへ置く`,
282
- );
283
- }
284
- engines[name] = enabled;
285
- }
286
- if (ENGINE_NAMES.every((name) => !engines[name])) {
287
- throw new Error(`${source} must enable at least one engine`);
288
- }
289
- return engines;
290
- }
291
-
292
- type ParsedEngineOptions = EngineOptions & {
293
- enable?: boolean;
294
- error?: boolean;
295
- projects?: string[];
296
- rules?: Record<string, RuleState>;
297
- };
298
-
299
- /**
300
- * engine名ごとにしか受け取らない起動条件を読み取る
301
- */
302
- function parseEngineExtras(
303
- value: JsonObject,
304
- source: string,
305
- name: EngineName,
306
- ): ParsedEngineOptions {
307
- const extras: ParsedEngineOptions = {};
308
- if (
309
- name === "comment-check" ||
310
- name === "document-style-check" ||
311
- name === "tsdoc-check"
312
- ) {
313
- const enable = readBoolean(
314
- value.enable,
315
- `${source}: config.${name}.enable`,
316
- );
317
- if (enable !== undefined) {
318
- extras.enable = enable;
319
- }
320
- const rules = readRuleStates(
321
- value.rules,
322
- `${source}: config.${name}.rules`,
323
- name,
324
- );
325
- if (rules !== undefined) {
326
- extras.rules = rules;
327
- }
328
- }
329
- if (name === "tsdoc-check") {
330
- const error = readBoolean(value.error, `${source}: config.${name}.error`);
331
- if (error !== undefined) {
332
- extras.error = error;
333
- }
334
- }
335
- if (name === "typecheck") {
336
- const projects = readStringArray(
337
- value.projects,
338
- `${source}: config.${name}.projects`,
339
- );
340
- if (projects !== undefined) {
341
- if (projects.length === 0) {
342
- throw new Error(`${source}: config.${name}.projects must not be empty`);
343
- }
344
- extras.projects = projects;
345
- }
346
- }
347
- return extras;
348
- }
349
-
350
- function parseEngineOptions(
351
- value: JsonObject,
352
- source: string,
353
- name: EngineName,
354
- ): ParsedEngineOptions {
355
- const unknown = Object.keys(value).filter(
356
- (key) => !ENGINE_OPTION_KEYS[name].includes(key),
357
- );
358
- if (unknown.length > 0) {
359
- throw new Error(
360
- `${source}: config.${name} has an unknown option: ${unknown[0]}`,
361
- );
362
- }
363
- const options: ParsedEngineOptions = {};
364
- const ignore = readStringArray(
365
- value.ignore,
366
- `${source}: config.${name}.ignore`,
367
- );
368
- if (ignore !== undefined) {
369
- options.ignore = ignore;
370
- }
371
- const targets = readStringArray(
372
- value.targets,
373
- `${source}: config.${name}.targets`,
374
- );
375
- if (targets !== undefined) {
376
- options.targets = targets;
377
- }
378
- const args = readStringArray(value.args, `${source}: config.${name}.args`);
379
- if (args !== undefined) {
380
- options.args = args;
381
- }
382
- return { ...options, ...parseEngineExtras(value, source, name) };
383
- }
384
-
385
- function parseEngineConfig(
386
- value: unknown,
387
- source: string,
388
- ): Partial<EngineConfigMap> | undefined {
389
- if (value === undefined) {
390
- return undefined;
391
- }
392
- if (!isJsonObject(value)) {
393
- throw new Error(`${source}: config must be an object`);
394
- }
395
- const config: Partial<Record<EngineName, ParsedEngineOptions>> = {};
396
- for (const [name, options] of Object.entries(value)) {
397
- if (!isEngineName(name)) {
398
- throw new Error(`${source}: unknown engine in config: ${name}`);
399
- }
400
- if (!isJsonObject(options)) {
401
- throw new Error(`${source}: config.${name} must be an object`);
402
- }
403
- config[name] = parseEngineOptions(options, source, name);
404
- }
405
-
406
- // rule名はengineが公開するunionで縛る 実行時の入力は文字列として届くため検証済みの値をここで型へ寄せる
407
- return config as Partial<EngineConfigMap>;
408
- }
409
-
410
- /**
411
- * 読み込んだ設定を検証して不足分を補う
412
- *
413
- * @param value - config fileがexportした検証前の値
414
- * @param source - errorメッセージへ載せるconfig fileのpath
415
- * @returns 検証して既定値を補った統合検査の設定
416
- */
417
- export function parseConfig(value: unknown, source: string): QualityConfig {
418
- if (!isJsonObject(value)) {
419
- throw new Error(`${source} must export a config object`);
420
- }
421
- const config: QualityConfig = {
422
- engines: parseEngines(value.engines, source),
423
- };
424
- const engineConfig = parseEngineConfig(value.config, source);
425
- if (engineConfig !== undefined) {
426
- config.config = engineConfig;
427
- }
428
- const baseline = readBaseline(value.baseline, source);
429
- if (baseline !== undefined) {
430
- config.baseline = baseline;
431
- }
432
- return config;
433
- }
434
-
435
- /**
436
- * 有効なengineを実行順で返す
437
- *
438
- * @param config - engineの有効無効を持つ統合検査の設定
439
- * @returns 有効なengine名を実行順に並べた配列
440
- */
441
- export function enabledEngines(config: QualityConfig): EngineName[] {
442
- return ENGINE_NAMES.filter((name) => Boolean(config.engines[name]));
443
- }
444
-
445
- /**
446
- * engineの起動条件を返す 無効なengineにはnullを返す
447
- *
448
- * @typeParam K - 起動条件を取り出すengine名の型
449
- * @param config - engineごとの起動条件を持つ統合検査の設定
450
- * @param name - 起動条件を取り出すengine名
451
- * @returns 指定したengineの起動条件 無効なengineならnull
452
- */
453
- export function engineConfig<K extends EngineName>(
454
- config: QualityConfig,
455
- name: K,
456
- ): EngineConfigMap[K] | null {
457
- if (!config.engines[name]) {
458
- return null;
459
- }
460
- return config.config?.[name] ?? ({} as EngineConfigMap[K]);
461
- }
462
-
463
- /**
464
- * 作業ディレクトリからconfig fileを探す
465
- *
466
- * @param cwd - 探索を開始する作業ディレクトリのpath
467
- * @returns 見つけたconfig fileのpath 見つからなければnull
468
- */
469
- export function findConfigFile(cwd: string): string | null {
470
- for (const name of DEFAULT_CONFIG_FILES) {
471
- const candidate = resolve(cwd, name);
472
- if (existsSync(candidate)) {
473
- return candidate;
474
- }
475
- }
476
- return null;
477
- }
478
-
479
- /**
480
- * config fileを読み込んで検証する
481
- *
482
- * @param path - 読み込むconfig fileのpath
483
- * @returns 読み込んで検証した統合検査の設定
484
- */
485
- export async function loadConfig(path: string): Promise<QualityConfig> {
486
- const module: unknown = await import(pathToFileURL(path).href);
487
- const value =
488
- isJsonObject(module) && "default" in module ? module.default : module;
489
- return parseConfig(value, path);
490
- }