@yuu1111/quality-check 0.10.0 → 1.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/README.ja.md CHANGED
@@ -10,7 +10,10 @@ Projectごとのscriptから個別に呼んでいたBiome、型検査、Knip、c
10
10
  bun add -D @yuu1111/quality-check
11
11
  ```
12
12
 
13
- engineのbinaryは利用Projectの `node_modules/.bin` から実行時に解決する engineを自分のscriptから呼ばないProjectではKnipが `@yuu1111/comment-check` などを未使用依存として報告するため `knip.ts` の `ignoreDependencies` で理由付きに宣言する
13
+ このCLIは `comment-check`、`document-style-check`、`tsdoc-check` を、configの型がengineの公開するrule名を取り込むためpeer dependencyとして持つ
14
+ package managerはこのCLIと一緒に導入し、engineのbinaryは利用Projectの `node_modules/.bin` から実行時に解決する
15
+
16
+ `@yuu1111/code-style-check` を宣言するProjectではKnipが未使用依存として報告するため `knip.ts` の `ignoreDependencies` で理由付きに宣言する
14
17
 
15
18
  ## Usage
16
19
 
@@ -39,15 +42,11 @@ export default defineConfig({
39
42
  },
40
43
  config: {
41
44
  "comment-check": {
42
- enable: ["japanese-period"],
43
45
  ignore: ["another-project"],
46
+ rules: { "japanese-period": "on" },
44
47
  },
45
- "document-style-check": {
46
- enable: ["japanese-period"],
47
- },
48
- "tsdoc-check": {
49
- error: ["missing-doc"],
50
- },
48
+ "document-style-check": { enable: true },
49
+ "tsdoc-check": { error: true },
51
50
  },
52
51
  });
53
52
  ```
@@ -88,9 +87,9 @@ engineは `biome` → `typecheck` → `knip` → `code-style-check` → `comment
88
87
  | `typecheck` | `tsc --noEmit` | `args`、`projects` 設定は `tsconfig.json` が持つ |
89
88
  | `knip` | `knip` | `args` 設定は `knip.ts` が持つ |
90
89
  | `code-style-check` | `code-style-check --json` | `ignore`、`targets`、`args` |
91
- | `comment-check` | `comment-check --json` | `ignore`、`targets`、`args`、`enable`(有効にするrule名) |
92
- | `document-style-check` | `document-style-check lint --json` | `ignore`、`targets`、`args`、`enable`(有効にするrule名) |
93
- | `tsdoc-check` | `tsdoc-check --json` | `ignore`、`targets`、`args`、`enable`(有効にするrule名)、`error`(違反として扱うrule名) |
90
+ | `comment-check` | `comment-check --json` | `ignore`、`targets`、`args`、`enable`、`rules` |
91
+ | `document-style-check` | `document-style-check lint --json` | `ignore`、`targets`、`args`、`enable`、`rules` |
92
+ | `tsdoc-check` | `tsdoc-check --json` | `ignore`、`targets`、`args`、`enable`、`error`、`rules` |
94
93
 
95
94
  `args` はengineの既定引数の後ろへ足す
96
95
  設定fileで表せない起動条件や、engineの引数が変わったときの逃げ道として使う
@@ -102,8 +101,13 @@ engineが受け取らない条件は渡さず、その旨をそのengineのsecti
102
101
  biome: ignore skipped (biome.json holds its settings)
103
102
  ```
104
103
 
105
- `enable` は `comment-check` と `document-style-check` `tsdoc-check` の `--enable <rule>` になり、他のengineでは拒否される
106
- このCLIはengineを同梱しないため、opt-in ruleを有効にするProjectは対応するengine packageも同じ変更で更新する
104
+ `enable` と `error` はruleをまとめて選び、`rules` ruleを1つずつ選ぶ
105
+ `enable: true` はengineが既定で無効にしているruleを全て有効にする
106
+ `error: true` は `tsdoc-check` の全ruleを違反として扱い、opt-inのruleは先に有効にする 違反へ上げられるのはこのengineだけ
107
+ `rules` はrule名をkeyにして `off`、`on`、`error` のいずれかを渡す `on` はengineの既定のまま、`error` はそのruleだけpresetと同じ扱いにする
108
+ keyがrule名そのものなので導入済みengineのunionで型付けし、engineが知らない名前は起動時の失敗ではなく型errorになる
109
+ `error` を取れるのは `tsdoc-check` だけ、`off` を取れるのは既定で無効のruleだけで、既定で有効なruleを無効にする引数をengineは持たない
110
+ 選んだruleは `--enable <rule>` になり、`tsdoc-check` では `--error <rule>` にもなる 他のengineはこれらのoptionを拒否する
107
111
 
108
112
  `comment-check` はbaseline差分を無効化する未作成のpathを渡して起動する
109
113
  新規と解消済みの判定はengineごとではなく統合CLIが1つのbaseline fileで行うため、既存の `comment-baseline.json` がある場合は `--update-baseline` で移す
package/README.md CHANGED
@@ -11,8 +11,10 @@ The baseline diff lives here too.
11
11
  bun add -D @yuu1111/quality-check
12
12
  ```
13
13
 
14
- An engine binary is resolved at run time from the project's `node_modules/.bin`.
15
- A project that never calls an engine from its own scripts therefore sees Knip report `@yuu1111/comment-check` and its siblings as unused dependencies, so it declares them in `ignoreDependencies` with the reason.
14
+ `comment-check`, `document-style-check`, and `tsdoc-check` are peer dependencies of this CLI, because the config types embed the rule names those engines publish.
15
+ A package manager installs them with this CLI, and each engine binary is resolved at run time from the project's `node_modules/.bin`.
16
+
17
+ A project that declares `@yuu1111/code-style-check` sees Knip report it as an unused dependency, so it names it in `ignoreDependencies` with the reason.
16
18
 
17
19
  ## Usage
18
20
 
@@ -41,15 +43,11 @@ export default defineConfig({
41
43
  },
42
44
  config: {
43
45
  "comment-check": {
44
- enable: ["japanese-period"],
45
46
  ignore: ["another-project"],
47
+ rules: { "japanese-period": "on" },
46
48
  },
47
- "document-style-check": {
48
- enable: ["japanese-period"],
49
- },
50
- "tsdoc-check": {
51
- error: ["missing-doc"],
52
- },
49
+ "document-style-check": { enable: true },
50
+ "tsdoc-check": { error: true },
53
51
  },
54
52
  });
55
53
  ```
@@ -92,9 +90,9 @@ Each engine runs:
92
90
  | `typecheck` | `tsc --noEmit` | `args`, `projects`; `tsconfig.json` holds the settings |
93
91
  | `knip` | `knip` | `args`; `knip.ts` holds the settings |
94
92
  | `code-style-check` | `code-style-check --json` | `ignore`, `targets`, `args` |
95
- | `comment-check` | `comment-check --json` | `ignore`, `targets`, `args`, `enable` (rule names to turn on) |
96
- | `document-style-check` | `document-style-check lint --json` | `ignore`, `targets`, `args`, `enable` (rule names to turn on) |
97
- | `tsdoc-check` | `tsdoc-check --json` | `ignore`, `targets`, `args`, `enable` (rule names to turn on), `error` (rule names to fail on) |
93
+ | `comment-check` | `comment-check --json` | `ignore`, `targets`, `args`, `enable`, `rules` |
94
+ | `document-style-check` | `document-style-check lint --json` | `ignore`, `targets`, `args`, `enable`, `rules` |
95
+ | `tsdoc-check` | `tsdoc-check --json` | `ignore`, `targets`, `args`, `enable`, `error`, `rules` |
98
96
 
99
97
  `args` is appended after the engine defaults, for conditions the config cannot express and for the case where an engine changes its arguments.
100
98
 
@@ -105,8 +103,13 @@ A condition that an engine does not take is not passed on, and the section says
105
103
  biome: ignore skipped (biome.json holds its settings)
106
104
  ```
107
105
 
108
- `enable` becomes `--enable <rule>` on `comment-check`, `document-style-check`, and `tsdoc-check`, and the other engines reject the field.
109
- This CLI ships no engine of its own, so a project that turns an opt-in rule on updates the matching engine package in the same change.
106
+ `enable` and `error` pick rules in bulk, and `rules` picks one rule at a time.
107
+ `enable: true` turns on every rule the engine keeps off by default.
108
+ `error: true` turns every rule of `tsdoc-check` into an error, enabling the opt-in ones first; it is the only condition that raises a rule.
109
+ A `rules` entry names one rule and gives it `off`, `on`, or `error`, where `on` keeps the engine default and `error` means the same as the `error` preset for that rule.
110
+ The `rules` key is the rule name itself, so the installed engine's union types it and a name that engine does not know is a type error rather than a start-up failure.
111
+ Only `tsdoc-check` takes `error` in `rules`, and only rules that are off by default take `off`, because no engine can turn off a rule that is on by default.
112
+ Each selected rule becomes `--enable <rule>` and, on `tsdoc-check`, `--error <rule>`; the other engines reject the fields.
110
113
 
111
114
  `comment-check` runs with an unwritten baseline path so that it reports every finding.
112
115
  The new-and-resolved diff is done by this CLI from a single baseline file, so an existing `comment-baseline.json` is moved over with `--update-baseline`.
package/dist/cli.js CHANGED
@@ -194,6 +194,18 @@ function colorEnabled(stream, env) {
194
194
  import { existsSync as existsSync2 } from "fs";
195
195
  import { resolve } from "path";
196
196
  import { pathToFileURL } from "url";
197
+ import {
198
+ OPT_IN_RULE_IDS as COMMENT_CHECK_OPT_IN_RULE_IDS,
199
+ RULE_IDS as COMMENT_CHECK_RULE_IDS
200
+ } from "@yuu1111/comment-check/rule-ids";
201
+ import {
202
+ OPT_IN_RULE_IDS as DOCUMENT_STYLE_CHECK_OPT_IN_RULE_IDS,
203
+ RULE_IDS as DOCUMENT_STYLE_CHECK_RULE_IDS
204
+ } from "@yuu1111/document-style-check/rule-ids";
205
+ import {
206
+ OPT_IN_RULE_IDS as TSDOC_CHECK_OPT_IN_RULE_IDS,
207
+ KNOWN_RULE_NAMES as TSDOC_CHECK_RULE_NAMES
208
+ } from "@yuu1111/tsdoc-check/rule-ids";
197
209
  var ENGINE_NAMES = [
198
210
  "biome",
199
211
  "typecheck",
@@ -203,6 +215,27 @@ var ENGINE_NAMES = [
203
215
  "document-style-check",
204
216
  "tsdoc-check"
205
217
  ];
218
+ var RULE_VOCABULARY = {
219
+ biome: { all: [], optIn: [], promotes: false },
220
+ typecheck: { all: [], optIn: [], promotes: false },
221
+ knip: { all: [], optIn: [], promotes: false },
222
+ "code-style-check": { all: [], optIn: [], promotes: false },
223
+ "comment-check": {
224
+ all: COMMENT_CHECK_RULE_IDS,
225
+ optIn: COMMENT_CHECK_OPT_IN_RULE_IDS,
226
+ promotes: false
227
+ },
228
+ "document-style-check": {
229
+ all: DOCUMENT_STYLE_CHECK_RULE_IDS,
230
+ optIn: DOCUMENT_STYLE_CHECK_OPT_IN_RULE_IDS,
231
+ promotes: false
232
+ },
233
+ "tsdoc-check": {
234
+ all: TSDOC_CHECK_RULE_NAMES,
235
+ optIn: TSDOC_CHECK_OPT_IN_RULE_IDS,
236
+ promotes: true
237
+ }
238
+ };
206
239
  var DEFAULT_CONFIG_FILES = [
207
240
  "quality.config.ts",
208
241
  "quality.config.mts",
@@ -221,9 +254,9 @@ var ENGINE_OPTION_KEYS = {
221
254
  typecheck: ["args", "ignore", "projects", "targets"],
222
255
  knip: ["args", "ignore", "targets"],
223
256
  "code-style-check": ["args", "ignore", "targets"],
224
- "comment-check": ["args", "enable", "ignore", "targets"],
225
- "document-style-check": ["args", "enable", "ignore", "targets"],
226
- "tsdoc-check": ["args", "enable", "error", "ignore", "targets"]
257
+ "comment-check": ["args", "enable", "ignore", "rules", "targets"],
258
+ "document-style-check": ["args", "enable", "ignore", "rules", "targets"],
259
+ "tsdoc-check": ["args", "enable", "error", "ignore", "rules", "targets"]
227
260
  };
228
261
  function readStringArray(value, field) {
229
262
  if (value === undefined) {
@@ -234,6 +267,41 @@ function readStringArray(value, field) {
234
267
  }
235
268
  return [...value];
236
269
  }
270
+ function readBoolean(value, field) {
271
+ if (value === undefined) {
272
+ return;
273
+ }
274
+ if (typeof value !== "boolean") {
275
+ throw new Error(`${field} must be a boolean`);
276
+ }
277
+ return value;
278
+ }
279
+ function readRuleStates(value, field, name) {
280
+ if (value === undefined) {
281
+ return;
282
+ }
283
+ if (!isJsonObject2(value)) {
284
+ throw new Error(`${field} must be an object`);
285
+ }
286
+ const vocabulary = RULE_VOCABULARY[name];
287
+ const states = {};
288
+ for (const [rule, state] of Object.entries(value)) {
289
+ if (!vocabulary.all.includes(rule)) {
290
+ throw new Error(`${field} has an unknown rule: ${rule}`);
291
+ }
292
+ if (state !== "off" && state !== "on" && state !== "error") {
293
+ throw new Error(`${field}.${rule} must be "off", "on" or "error"`);
294
+ }
295
+ if (state === "error" && !vocabulary.promotes) {
296
+ throw new Error(`${field}.${rule}: ${name} cannot treat a rule as an error`);
297
+ }
298
+ if (state === "off" && !vocabulary.optIn.includes(rule)) {
299
+ throw new Error(`${field}.${rule}: ${name} cannot turn off a rule that is on by default`);
300
+ }
301
+ states[rule] = state;
302
+ }
303
+ return states;
304
+ }
237
305
  function readBaseline2(value, source) {
238
306
  if (value === undefined || value === false) {
239
307
  return value;
@@ -265,13 +333,17 @@ function parseEngines(value, source) {
265
333
  function parseEngineExtras(value, source, name) {
266
334
  const extras = {};
267
335
  if (name === "comment-check" || name === "document-style-check" || name === "tsdoc-check") {
268
- const enable = readStringArray(value.enable, `${source}: config.${name}.enable`);
336
+ const enable = readBoolean(value.enable, `${source}: config.${name}.enable`);
269
337
  if (enable !== undefined) {
270
338
  extras.enable = enable;
271
339
  }
340
+ const rules = readRuleStates(value.rules, `${source}: config.${name}.rules`, name);
341
+ if (rules !== undefined) {
342
+ extras.rules = rules;
343
+ }
272
344
  }
273
345
  if (name === "tsdoc-check") {
274
- const error = readStringArray(value.error, `${source}: config.${name}.error`);
346
+ const error = readBoolean(value.error, `${source}: config.${name}.error`);
275
347
  if (error !== undefined) {
276
348
  extras.error = error;
277
349
  }
@@ -460,19 +532,54 @@ function buildTypecheckCommand(executable, context, project) {
460
532
  ...options.args ?? []
461
533
  ];
462
534
  }
463
- function enableArguments(options) {
464
- return (options?.enable ?? []).flatMap((rule) => ["--enable", rule]);
535
+ function applyRuleState(vocabulary, selected, rule, state) {
536
+ if (state === "off") {
537
+ selected.enable.delete(rule);
538
+ selected.error.delete(rule);
539
+ return;
540
+ }
541
+ if (state === "on") {
542
+ selected.error.delete(rule);
543
+ } else {
544
+ selected.error.add(rule);
545
+ }
546
+ if (vocabulary.optIn.includes(rule)) {
547
+ selected.enable.add(rule);
548
+ }
549
+ }
550
+ function selectRules(name, options) {
551
+ const selection = options ?? {};
552
+ const vocabulary = RULE_VOCABULARY[name];
553
+ const selected = { enable: new Set, error: new Set };
554
+ if (selection.enable === true) {
555
+ for (const rule of vocabulary.optIn) {
556
+ selected.enable.add(rule);
557
+ }
558
+ }
559
+ if (selection.error === true) {
560
+ for (const rule of vocabulary.all) {
561
+ applyRuleState(vocabulary, selected, rule, "error");
562
+ }
563
+ }
564
+ for (const [rule, state] of Object.entries(selection.rules ?? {})) {
565
+ applyRuleState(vocabulary, selected, rule, state);
566
+ }
567
+ return { enable: [...selected.enable], error: [...selected.error] };
465
568
  }
466
569
  function buildEngineCommand(name, executable, context) {
467
570
  const options = engineConfig(context.config, name) ?? {};
468
571
  const limits = ENGINE_LIMITS[name];
572
+ const selection = selectRules(name, options);
573
+ const enableArguments = selection.enable.flatMap((rule) => [
574
+ "--enable",
575
+ rule
576
+ ]);
577
+ const errorArguments = selection.error.flatMap((rule) => ["--error", rule]);
469
578
  const extra = options.args ?? [];
470
579
  const ignores = limits.ignore === undefined ? [...options.ignore ?? [], ...context.overrides.ignore] : [];
471
580
  const requested = context.overrides.targets.length > 0 ? context.overrides.targets : options.targets ?? ["."];
472
581
  const targets = limits.targets === undefined ? requested : [];
473
582
  const ignoreArguments = ignores.flatMap((ignore) => ["--ignore", ignore]);
474
- const rules = engineConfig(context.config, "tsdoc-check")?.error ?? [];
475
- const errorArguments = rules.flatMap((rule) => ["--error", rule]);
476
583
  if (name === "biome") {
477
584
  return [
478
585
  executable,
@@ -494,7 +601,7 @@ function buildEngineCommand(name, executable, context) {
494
601
  "--json",
495
602
  "--baseline",
496
603
  context.rawBaseline,
497
- ...enableArguments(engineConfig(context.config, "comment-check")),
604
+ ...enableArguments,
498
605
  ...targets,
499
606
  ...ignoreArguments,
500
607
  ...extra
@@ -505,7 +612,7 @@ function buildEngineCommand(name, executable, context) {
505
612
  executable,
506
613
  "lint",
507
614
  "--json",
508
- ...enableArguments(engineConfig(context.config, "document-style-check")),
615
+ ...enableArguments,
509
616
  ...targets,
510
617
  ...ignoreArguments,
511
618
  ...extra
@@ -517,7 +624,7 @@ function buildEngineCommand(name, executable, context) {
517
624
  return [
518
625
  executable,
519
626
  "--json",
520
- ...enableArguments(engineConfig(context.config, "tsdoc-check")),
627
+ ...enableArguments,
521
628
  ...errorArguments,
522
629
  ...targets,
523
630
  ...ignoreArguments,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuu1111/quality-check",
3
- "version": "0.10.0",
3
+ "version": "1.0.0",
4
4
  "description": "Integrated quality check runner",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,12 +23,17 @@
23
23
  "dist"
24
24
  ],
25
25
  "scripts": {
26
- "build": "bun build src/cli.ts --target=bun --outdir=dist"
26
+ "build": "bun build src/cli.ts --target=bun --outdir=dist --external @yuu1111/comment-check/rule-ids --external @yuu1111/document-style-check/rule-ids --external @yuu1111/tsdoc-check/rule-ids"
27
27
  },
28
28
  "keywords": [
29
29
  "quality",
30
30
  "lint"
31
31
  ],
32
+ "peerDependencies": {
33
+ "@yuu1111/comment-check": ">=2.3.0",
34
+ "@yuu1111/document-style-check": ">=0.5.0",
35
+ "@yuu1111/tsdoc-check": ">=2.2.0"
36
+ },
32
37
  "devDependencies": {
33
38
  "@yuu1111/shared": "workspace:*"
34
39
  }
package/src/config.ts CHANGED
@@ -1,6 +1,21 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
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";
4
19
 
5
20
  /**
6
21
  * 統合CLIが起動できるengineの名前 並び順が実行順になる
@@ -36,26 +51,32 @@ export interface EngineOptions {
36
51
  * comment-checkへ渡す起動条件
37
52
  */
38
53
  export interface CommentCheckOptions extends EngineOptions {
39
- /** 既定で無効のopt-in ruleのうち有効にするrule名 */
40
- enable?: string[];
54
+ /** 既定で無効のopt-in ruleを全て有効にする */
55
+ enable?: boolean;
56
+ /** rule名ごとの状態 offで無効 onで有効 省略したruleはengineの既定に従う */
57
+ rules?: Partial<Record<CommentCheckRuleName, "off" | "on">>;
41
58
  }
42
59
 
43
60
  /**
44
61
  * document-style-checkへ渡す起動条件
45
62
  */
46
63
  export interface DocumentStyleCheckOptions extends EngineOptions {
47
- /** 既定で無効のopt-in ruleのうち有効にするrule名 */
48
- enable?: string[];
64
+ /** 既定で無効のopt-in ruleを全て有効にする */
65
+ enable?: boolean;
66
+ /** rule名ごとの状態 offで無効 onで有効 省略したruleはengineの既定に従う */
67
+ rules?: Partial<Record<DocumentStyleCheckRuleName, "off" | "on">>;
49
68
  }
50
69
 
51
70
  /**
52
71
  * TSDoc検査へ渡す起動条件
53
72
  */
54
73
  export interface TsdocCheckOptions extends EngineOptions {
55
- /** 既定で無効のopt-in ruleのうち有効にするrule名 */
56
- enable?: string[];
57
- /** 違反として扱うrule */
58
- error?: string[];
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">>;
59
80
  }
60
81
 
61
82
  /**
@@ -79,6 +100,39 @@ export interface EngineConfigMap {
79
100
  "tsdoc-check": TsdocCheckOptions;
80
101
  }
81
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
+
82
136
  /**
83
137
  * quality.config.tsが受け付ける統合検査の設定
84
138
  */
@@ -131,9 +185,9 @@ const ENGINE_OPTION_KEYS: Record<EngineName, readonly string[]> = {
131
185
  typecheck: ["args", "ignore", "projects", "targets"],
132
186
  knip: ["args", "ignore", "targets"],
133
187
  "code-style-check": ["args", "ignore", "targets"],
134
- "comment-check": ["args", "enable", "ignore", "targets"],
135
- "document-style-check": ["args", "enable", "ignore", "targets"],
136
- "tsdoc-check": ["args", "enable", "error", "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"],
137
191
  };
138
192
 
139
193
  function readStringArray(value: unknown, field: string): string[] | undefined {
@@ -149,6 +203,54 @@ function readStringArray(value: unknown, field: string): string[] | undefined {
149
203
  return [...value] as string[];
150
204
  }
151
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
+
152
254
  function readBaseline(
153
255
  value: unknown,
154
256
  source: string,
@@ -188,9 +290,10 @@ function parseEngines(
188
290
  }
189
291
 
190
292
  type ParsedEngineOptions = EngineOptions & {
191
- enable?: string[];
192
- error?: string[];
293
+ enable?: boolean;
294
+ error?: boolean;
193
295
  projects?: string[];
296
+ rules?: Record<string, RuleState>;
194
297
  };
195
298
 
196
299
  /**
@@ -207,19 +310,24 @@ function parseEngineExtras(
207
310
  name === "document-style-check" ||
208
311
  name === "tsdoc-check"
209
312
  ) {
210
- const enable = readStringArray(
313
+ const enable = readBoolean(
211
314
  value.enable,
212
315
  `${source}: config.${name}.enable`,
213
316
  );
214
317
  if (enable !== undefined) {
215
318
  extras.enable = enable;
216
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
+ }
217
328
  }
218
329
  if (name === "tsdoc-check") {
219
- const error = readStringArray(
220
- value.error,
221
- `${source}: config.${name}.error`,
222
- );
330
+ const error = readBoolean(value.error, `${source}: config.${name}.error`);
223
331
  if (error !== undefined) {
224
332
  extras.error = error;
225
333
  }
@@ -284,7 +392,7 @@ function parseEngineConfig(
284
392
  if (!isJsonObject(value)) {
285
393
  throw new Error(`${source}: config must be an object`);
286
394
  }
287
- const config: Partial<EngineConfigMap> = {};
395
+ const config: Partial<Record<EngineName, ParsedEngineOptions>> = {};
288
396
  for (const [name, options] of Object.entries(value)) {
289
397
  if (!isEngineName(name)) {
290
398
  throw new Error(`${source}: unknown engine in config: ${name}`);
@@ -294,7 +402,9 @@ function parseEngineConfig(
294
402
  }
295
403
  config[name] = parseEngineOptions(options, source, name);
296
404
  }
297
- return config;
405
+
406
+ // rule名はengineが公開するunionで縛る 実行時の入力は文字列として届くため検証済みの値をここで型へ寄せる
407
+ return config as Partial<EngineConfigMap>;
298
408
  }
299
409
 
300
410
  /**
package/src/engines.ts CHANGED
@@ -5,6 +5,8 @@ import {
5
5
  type EngineOptions,
6
6
  engineConfig,
7
7
  type QualityConfig,
8
+ RULE_VOCABULARY,
9
+ type RuleState,
8
10
  } from "./config";
9
11
 
10
12
  /**
@@ -194,12 +196,76 @@ function buildTypecheckCommand(
194
196
  }
195
197
 
196
198
  /**
197
- * opt-in ruleを有効にするコマンド引数へ展開する
199
+ * 設定fileが持つruleの指定
198
200
  */
199
- function enableArguments(
200
- options: { enable?: string[] } | null | undefined,
201
- ): string[] {
202
- return (options?.enable ?? []).flatMap((rule) => ["--enable", rule]);
201
+ interface RuleSelection {
202
+ enable?: boolean;
203
+ error?: boolean;
204
+ rules?: Record<string, RuleState>;
205
+ }
206
+
207
+ /**
208
+ * engineが公開するrule語彙
209
+ */
210
+ type RuleVocabulary = (typeof RULE_VOCABULARY)[EngineName];
211
+
212
+ /**
213
+ * 選んだruleを有効の一覧と違反の一覧へ反映する
214
+ *
215
+ * @param vocabulary - 反映先engineのrule語彙
216
+ * @param selected - 反映先のrule名の集合
217
+ * @param rule - 反映するrule名
218
+ * @param state - 反映する状態
219
+ */
220
+ function applyRuleState(
221
+ vocabulary: RuleVocabulary,
222
+ selected: { enable: Set<string>; error: Set<string> },
223
+ rule: string,
224
+ state: RuleState,
225
+ ): void {
226
+ if (state === "off") {
227
+ selected.enable.delete(rule);
228
+ selected.error.delete(rule);
229
+ return;
230
+ }
231
+ if (state === "on") {
232
+ selected.error.delete(rule);
233
+ } else {
234
+ selected.error.add(rule);
235
+ }
236
+ if (vocabulary.optIn.includes(rule)) {
237
+ selected.enable.add(rule);
238
+ }
239
+ }
240
+
241
+ /**
242
+ * ruleの一括指定と個別指定をengineへ渡すruleの一覧へ展開する
243
+ *
244
+ * @param name - 展開するengine名
245
+ * @param options - engineへ渡す起動条件
246
+ * @returns --enableへ渡すrule名と--errorへ渡すrule名の組
247
+ */
248
+ function selectRules(
249
+ name: EngineName,
250
+ options: EngineOptions | null | undefined,
251
+ ): { enable: string[]; error: string[] } {
252
+ const selection = (options ?? {}) as RuleSelection;
253
+ const vocabulary = RULE_VOCABULARY[name];
254
+ const selected = { enable: new Set<string>(), error: new Set<string>() };
255
+ if (selection.enable === true) {
256
+ for (const rule of vocabulary.optIn) {
257
+ selected.enable.add(rule);
258
+ }
259
+ }
260
+ if (selection.error === true) {
261
+ for (const rule of vocabulary.all) {
262
+ applyRuleState(vocabulary, selected, rule, "error");
263
+ }
264
+ }
265
+ for (const [rule, state] of Object.entries(selection.rules ?? {})) {
266
+ applyRuleState(vocabulary, selected, rule, state);
267
+ }
268
+ return { enable: [...selected.enable], error: [...selected.error] };
203
269
  }
204
270
 
205
271
  /**
@@ -217,6 +283,12 @@ export function buildEngineCommand(
217
283
  ): string[] {
218
284
  const options: EngineOptions = engineConfig(context.config, name) ?? {};
219
285
  const limits = ENGINE_LIMITS[name];
286
+ const selection = selectRules(name, options);
287
+ const enableArguments = selection.enable.flatMap((rule) => [
288
+ "--enable",
289
+ rule,
290
+ ]);
291
+ const errorArguments = selection.error.flatMap((rule) => ["--error", rule]);
220
292
  const extra = options.args ?? [];
221
293
  const ignores =
222
294
  limits.ignore === undefined
@@ -228,8 +300,6 @@ export function buildEngineCommand(
228
300
  : (options.targets ?? ["."]);
229
301
  const targets = limits.targets === undefined ? requested : [];
230
302
  const ignoreArguments = ignores.flatMap((ignore) => ["--ignore", ignore]);
231
- const rules = engineConfig(context.config, "tsdoc-check")?.error ?? [];
232
- const errorArguments = rules.flatMap((rule) => ["--error", rule]);
233
303
  if (name === "biome") {
234
304
  return [
235
305
  executable,
@@ -251,7 +321,7 @@ export function buildEngineCommand(
251
321
  "--json",
252
322
  "--baseline",
253
323
  context.rawBaseline,
254
- ...enableArguments(engineConfig(context.config, "comment-check")),
324
+ ...enableArguments,
255
325
  ...targets,
256
326
  ...ignoreArguments,
257
327
  ...extra,
@@ -262,7 +332,7 @@ export function buildEngineCommand(
262
332
  executable,
263
333
  "lint",
264
334
  "--json",
265
- ...enableArguments(engineConfig(context.config, "document-style-check")),
335
+ ...enableArguments,
266
336
  ...targets,
267
337
  ...ignoreArguments,
268
338
  ...extra,
@@ -274,7 +344,7 @@ export function buildEngineCommand(
274
344
  return [
275
345
  executable,
276
346
  "--json",
277
- ...enableArguments(engineConfig(context.config, "tsdoc-check")),
347
+ ...enableArguments,
278
348
  ...errorArguments,
279
349
  ...targets,
280
350
  ...ignoreArguments,