@yuu1111/quality-check 0.5.1 → 0.7.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/package.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "name": "@yuu1111/quality-check",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "Integrated quality check runner",
5
- "license": "MIT",
6
5
  "repository": {
7
6
  "type": "git",
8
7
  "url": "git+https://github.com/yuu1111/configs.git",
@@ -10,7 +9,7 @@
10
9
  },
11
10
  "type": "module",
12
11
  "bin": {
13
- "quality-check": "src/cli.ts"
12
+ "quality-check": "dist/cli.js"
14
13
  },
15
14
  "exports": {
16
15
  ".": "./src/config.ts",
@@ -18,10 +17,18 @@
18
17
  },
19
18
  "files": [
20
19
  "README.ja.md",
21
- "src"
20
+ "src/config.ts",
21
+ "src/engines.ts",
22
+ "dist"
22
23
  ],
24
+ "scripts": {
25
+ "build": "bun build src/cli.ts --target=bun --outdir=dist"
26
+ },
23
27
  "keywords": [
24
28
  "quality",
25
29
  "lint"
26
- ]
30
+ ],
31
+ "devDependencies": {
32
+ "@yuu1111/shared": "workspace:*"
33
+ }
27
34
  }
package/src/config.ts CHANGED
@@ -31,6 +31,22 @@ export interface EngineOptions {
31
31
  args?: string[];
32
32
  }
33
33
 
34
+ /**
35
+ * comment-checkへ渡す起動条件
36
+ */
37
+ export interface CommentCheckOptions extends EngineOptions {
38
+ /** 既定で無効のopt-in ruleのうち有効にするrule名 */
39
+ enable?: string[];
40
+ }
41
+
42
+ /**
43
+ * document-style-checkへ渡す起動条件
44
+ */
45
+ export interface DocumentStyleCheckOptions extends EngineOptions {
46
+ /** 既定で無効のopt-in ruleのうち有効にするrule名 */
47
+ enable?: string[];
48
+ }
49
+
34
50
  /**
35
51
  * TSDoc検査へ渡す起動条件
36
52
  */
@@ -39,15 +55,23 @@ export interface TsdocCheckOptions extends EngineOptions {
39
55
  error?: string[];
40
56
  }
41
57
 
58
+ /**
59
+ * 型検査へ渡す起動条件
60
+ */
61
+ export interface TypecheckOptions extends EngineOptions {
62
+ /** 型検査するtsconfigのpath 省略時はカレントのtsconfig.jsonを1回だけ読む */
63
+ projects?: string[];
64
+ }
65
+
42
66
  /**
43
67
  * engine名ごとの起動条件
44
68
  */
45
69
  export interface EngineConfigMap {
46
70
  biome: EngineOptions;
47
- typecheck: EngineOptions;
71
+ typecheck: TypecheckOptions;
48
72
  knip: EngineOptions;
49
- "comment-check": EngineOptions;
50
- "document-style-check": EngineOptions;
73
+ "comment-check": CommentCheckOptions;
74
+ "document-style-check": DocumentStyleCheckOptions;
51
75
  "tsdoc-check": TsdocCheckOptions;
52
76
  }
53
77
 
@@ -97,10 +121,10 @@ function isEngineName(value: string): value is EngineName {
97
121
 
98
122
  const ENGINE_OPTION_KEYS: Record<EngineName, readonly string[]> = {
99
123
  biome: ["args", "ignore", "targets"],
100
- typecheck: ["args", "ignore", "targets"],
124
+ typecheck: ["args", "ignore", "projects", "targets"],
101
125
  knip: ["args", "ignore", "targets"],
102
- "comment-check": ["args", "ignore", "targets"],
103
- "document-style-check": ["args", "ignore", "targets"],
126
+ "comment-check": ["args", "enable", "ignore", "targets"],
127
+ "document-style-check": ["args", "enable", "ignore", "targets"],
104
128
  "tsdoc-check": ["args", "error", "ignore", "targets"],
105
129
  };
106
130
 
@@ -155,11 +179,59 @@ function parseEngines(
155
179
  return engines;
156
180
  }
157
181
 
182
+ type ParsedEngineOptions = EngineOptions & {
183
+ enable?: string[];
184
+ error?: string[];
185
+ projects?: string[];
186
+ };
187
+
188
+ /**
189
+ * engine名ごとにしか受け取らない起動条件を読み取る
190
+ */
191
+ function parseEngineExtras(
192
+ value: JsonObject,
193
+ source: string,
194
+ name: EngineName,
195
+ ): ParsedEngineOptions {
196
+ const extras: ParsedEngineOptions = {};
197
+ if (name === "comment-check" || name === "document-style-check") {
198
+ const enable = readStringArray(
199
+ value.enable,
200
+ `${source}: config.${name}.enable`,
201
+ );
202
+ if (enable !== undefined) {
203
+ extras.enable = enable;
204
+ }
205
+ }
206
+ if (name === "tsdoc-check") {
207
+ const error = readStringArray(
208
+ value.error,
209
+ `${source}: config.${name}.error`,
210
+ );
211
+ if (error !== undefined) {
212
+ extras.error = error;
213
+ }
214
+ }
215
+ if (name === "typecheck") {
216
+ const projects = readStringArray(
217
+ value.projects,
218
+ `${source}: config.${name}.projects`,
219
+ );
220
+ if (projects !== undefined) {
221
+ if (projects.length === 0) {
222
+ throw new Error(`${source}: config.${name}.projects must not be empty`);
223
+ }
224
+ extras.projects = projects;
225
+ }
226
+ }
227
+ return extras;
228
+ }
229
+
158
230
  function parseEngineOptions(
159
231
  value: JsonObject,
160
232
  source: string,
161
233
  name: EngineName,
162
- ): TsdocCheckOptions {
234
+ ): ParsedEngineOptions {
163
235
  const unknown = Object.keys(value).filter(
164
236
  (key) => !ENGINE_OPTION_KEYS[name].includes(key),
165
237
  );
@@ -168,7 +240,7 @@ function parseEngineOptions(
168
240
  `${source}: config.${name} has an unknown option: ${unknown[0]}`,
169
241
  );
170
242
  }
171
- const options: TsdocCheckOptions = {};
243
+ const options: ParsedEngineOptions = {};
172
244
  const ignore = readStringArray(
173
245
  value.ignore,
174
246
  `${source}: config.${name}.ignore`,
@@ -187,16 +259,7 @@ function parseEngineOptions(
187
259
  if (args !== undefined) {
188
260
  options.args = args;
189
261
  }
190
- if (name === "tsdoc-check") {
191
- const error = readStringArray(
192
- value.error,
193
- `${source}: config.${name}.error`,
194
- );
195
- if (error !== undefined) {
196
- options.error = error;
197
- }
198
- }
199
- return options;
262
+ return { ...options, ...parseEngineExtras(value, source, name) };
200
263
  }
201
264
 
202
265
  function parseEngineConfig(
package/src/engines.ts CHANGED
@@ -70,7 +70,7 @@ export const ENGINE_LIMITS: Record<
70
70
  biome: { ignore: "biome.json holds its settings" },
71
71
  typecheck: {
72
72
  ignore: "tsconfig.json holds its settings",
73
- targets: "tsc checks the project named by tsconfig.json",
73
+ targets: "tsconfig.json and projects hold its settings",
74
74
  },
75
75
  knip: {
76
76
  ignore: "knip.ts holds its settings",
@@ -161,6 +161,33 @@ function colorArguments(name: EngineName, color: boolean): string[] {
161
161
  return [];
162
162
  }
163
163
 
164
+ /**
165
+ * 型検査のコマンドを組み立てる projectを渡すとそのtsconfigを読む
166
+ */
167
+ function buildTypecheckCommand(
168
+ executable: string,
169
+ context: EngineCommandContext,
170
+ project: string | undefined,
171
+ ): string[] {
172
+ const options = engineConfig(context.config, "typecheck") ?? {};
173
+ return [
174
+ executable,
175
+ "--noEmit",
176
+ ...(project === undefined ? [] : ["--project", project]),
177
+ ...colorArguments("typecheck", context.color),
178
+ ...(options.args ?? []),
179
+ ];
180
+ }
181
+
182
+ /**
183
+ * opt-in ruleを有効にするコマンド引数へ展開する
184
+ */
185
+ function enableArguments(
186
+ options: { enable?: string[] } | null | undefined,
187
+ ): string[] {
188
+ return (options?.enable ?? []).flatMap((rule) => ["--enable", rule]);
189
+ }
190
+
164
191
  /**
165
192
  * engineへ渡すコマンドを組み立てる
166
193
  */
@@ -194,12 +221,7 @@ export function buildEngineCommand(
194
221
  ];
195
222
  }
196
223
  if (name === "typecheck") {
197
- return [
198
- executable,
199
- "--noEmit",
200
- ...colorArguments(name, context.color),
201
- ...extra,
202
- ];
224
+ return buildTypecheckCommand(executable, context, undefined);
203
225
  }
204
226
  if (name === "knip") {
205
227
  return [executable, ...extra];
@@ -210,6 +232,7 @@ export function buildEngineCommand(
210
232
  "--json",
211
233
  "--baseline",
212
234
  context.rawBaseline,
235
+ ...enableArguments(engineConfig(context.config, "comment-check")),
213
236
  ...targets,
214
237
  ...ignoreArguments,
215
238
  ...extra,
@@ -220,6 +243,7 @@ export function buildEngineCommand(
220
243
  executable,
221
244
  "lint",
222
245
  "--json",
246
+ ...enableArguments(engineConfig(context.config, "document-style-check")),
223
247
  ...targets,
224
248
  ...ignoreArguments,
225
249
  ...extra,
@@ -235,6 +259,26 @@ export function buildEngineCommand(
235
259
  ];
236
260
  }
237
261
 
262
+ /**
263
+ * engineの起動コマンドを順番に返す 型検査だけはprojectsごとに起動する
264
+ */
265
+ export function buildEngineCommands(
266
+ name: EngineName,
267
+ executable: string,
268
+ context: EngineCommandContext,
269
+ ): string[][] {
270
+ const projects =
271
+ name === "typecheck"
272
+ ? (engineConfig(context.config, "typecheck")?.projects ?? [])
273
+ : [];
274
+ if (projects.length === 0) {
275
+ return [buildEngineCommand(name, executable, context)];
276
+ }
277
+ return projects.map((project) =>
278
+ buildTypecheckCommand(executable, context, project),
279
+ );
280
+ }
281
+
238
282
  /**
239
283
  * Bun.spawnでengineを起動する既定のrunner
240
284
  */
package/src/baseline.ts DELETED
@@ -1,147 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import type { NormalizedFinding } from "./findings";
3
-
4
- /**
5
- * baselineへ記録する検出1件の識別情報と件数
6
- */
7
- export interface BaselineEntry {
8
- count: number;
9
- engine: string;
10
- file: string;
11
- rule: string;
12
- text: string;
13
- }
14
-
15
- /**
16
- * baseline fileの形式
17
- */
18
- export interface BaselineFile {
19
- entries: BaselineEntry[];
20
- version: 1;
21
- }
22
-
23
- /**
24
- * baselineと現在の検出を比較した結果
25
- */
26
- export interface BaselineComparison {
27
- added: NormalizedFinding[];
28
- resolved: BaselineEntry[];
29
- }
30
-
31
- type JsonObject = Record<string, unknown>;
32
-
33
- /**
34
- * 検出をbaseline上で一意に識別するkeyに使う項目
35
- */
36
- export interface BaselineKey {
37
- engine: string;
38
- file: string;
39
- rule: string;
40
- text: string;
41
- }
42
-
43
- function isJsonObject(value: unknown): value is JsonObject {
44
- return typeof value === "object" && value !== null && !Array.isArray(value);
45
- }
46
-
47
- function isBaselineEntry(value: unknown): value is BaselineEntry {
48
- if (!isJsonObject(value)) {
49
- return false;
50
- }
51
- return (
52
- typeof value.engine === "string" &&
53
- typeof value.rule === "string" &&
54
- typeof value.file === "string" &&
55
- typeof value.text === "string" &&
56
- typeof value.count === "number"
57
- );
58
- }
59
-
60
- function entryKey(entry: BaselineKey): string {
61
- return [entry.engine, entry.rule, entry.file, entry.text].join("\u0000");
62
- }
63
-
64
- function compareEntries(left: BaselineEntry, right: BaselineEntry): number {
65
- const leftKey = entryKey(left);
66
- const rightKey = entryKey(right);
67
- if (leftKey === rightKey) {
68
- return 0;
69
- }
70
- return leftKey < rightKey ? -1 : 1;
71
- }
72
-
73
- /**
74
- * 検出をengineとruleとfileと本文で集計してbaselineを作る
75
- */
76
- export function createBaseline(findings: NormalizedFinding[]): BaselineFile {
77
- const entries = new Map<string, BaselineEntry>();
78
- for (const finding of findings) {
79
- const key = entryKey(finding);
80
- const existing = entries.get(key);
81
- if (existing) {
82
- existing.count += 1;
83
- continue;
84
- }
85
- entries.set(key, {
86
- count: 1,
87
- engine: finding.engine,
88
- file: finding.file,
89
- rule: finding.rule,
90
- text: finding.text,
91
- });
92
- }
93
- return { entries: [...entries.values()].sort(compareEntries), version: 1 };
94
- }
95
-
96
- /**
97
- * 現在の検出からbaseline済みを除き、解消済みentryを求める
98
- */
99
- export function compareWithBaseline(
100
- findings: NormalizedFinding[],
101
- baseline: BaselineFile,
102
- ): BaselineComparison {
103
- const remaining = new Map<string, number>();
104
- for (const entry of baseline.entries) {
105
- const key = entryKey(entry);
106
- remaining.set(key, (remaining.get(key) ?? 0) + entry.count);
107
- }
108
- const added: NormalizedFinding[] = [];
109
- for (const finding of findings) {
110
- const key = entryKey(finding);
111
- const count = remaining.get(key) ?? 0;
112
- if (count > 0) {
113
- remaining.set(key, count - 1);
114
- continue;
115
- }
116
- added.push(finding);
117
- }
118
- const resolved = baseline.entries.filter(
119
- (entry) => (remaining.get(entryKey(entry)) ?? 0) > 0,
120
- );
121
- return { added, resolved };
122
- }
123
-
124
- /**
125
- * baseline fileを読み込む 存在しない場合は空のbaselineを返す
126
- */
127
- export function readBaseline(path: string): BaselineFile {
128
- if (!existsSync(path)) {
129
- return { entries: [], version: 1 };
130
- }
131
- const value: unknown = JSON.parse(readFileSync(path, "utf8"));
132
- if (
133
- !isJsonObject(value) ||
134
- !Array.isArray(value.entries) ||
135
- !value.entries.every(isBaselineEntry)
136
- ) {
137
- throw new Error(`${path} is not a quality baseline`);
138
- }
139
- return { entries: value.entries, version: 1 };
140
- }
141
-
142
- /**
143
- * baseline fileをタブ区切りのJSONで書き込む
144
- */
145
- export function writeBaseline(path: string, baseline: BaselineFile): void {
146
- writeFileSync(path, `${JSON.stringify(baseline, null, "\t")}\n`);
147
- }
package/src/cli.ts DELETED
@@ -1,183 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { tmpdir } from "node:os";
3
- import { join, resolve } from "node:path";
4
- import { createBaseline, readBaseline, writeBaseline } from "./baseline";
5
- import { ansiPainter, colorEnabled, plainPainter } from "./color";
6
- import {
7
- DEFAULT_BASELINE_FILE,
8
- findConfigFile,
9
- loadConfig,
10
- type QualityConfig,
11
- } from "./config";
12
- import { formatEngineSection, formatSummary, toJsonReport } from "./report";
13
- import { runEngines } from "./run";
14
-
15
- const USAGE =
16
- "Usage: quality-check [--config <path>] [--baseline <path>] [--ignore <path>] [--update-baseline] [--json] [path...]";
17
-
18
- interface Options {
19
- baselinePath: string | undefined;
20
- configPath: string | undefined;
21
- ignores: string[];
22
- json: boolean;
23
- targets: string[];
24
- update: boolean;
25
- }
26
-
27
- function applyFlagOption(options: Options, argument: string): boolean {
28
- if (argument === "--json") {
29
- options.json = true;
30
- return true;
31
- }
32
- if (argument === "--update-baseline") {
33
- options.update = true;
34
- return true;
35
- }
36
- return false;
37
- }
38
-
39
- function applyValueOption(
40
- options: Options,
41
- argument: string,
42
- argv: string[],
43
- index: number,
44
- ): number | null {
45
- if (argument === "--config") {
46
- options.configPath = argv[index + 1];
47
- return 1;
48
- }
49
- if (argument.startsWith("--config=")) {
50
- options.configPath = argument.slice("--config=".length);
51
- return 0;
52
- }
53
- if (argument === "--baseline") {
54
- options.baselinePath = argv[index + 1];
55
- return 1;
56
- }
57
- if (argument.startsWith("--baseline=")) {
58
- options.baselinePath = argument.slice("--baseline=".length);
59
- return 0;
60
- }
61
- if (argument === "--ignore") {
62
- const value = argv[index + 1];
63
- if (value !== undefined) {
64
- options.ignores.push(value);
65
- }
66
- return 1;
67
- }
68
- if (argument.startsWith("--ignore=")) {
69
- options.ignores.push(argument.slice("--ignore=".length));
70
- return 0;
71
- }
72
- return null;
73
- }
74
-
75
- function parseArguments(argv: string[]): Options {
76
- const options: Options = {
77
- baselinePath: undefined,
78
- configPath: undefined,
79
- ignores: [],
80
- json: false,
81
- targets: [],
82
- update: false,
83
- };
84
- for (let index = 0; index < argv.length; index += 1) {
85
- const argument = argv[index] ?? "";
86
- const consumed = applyValueOption(options, argument, argv, index);
87
- if (consumed !== null) {
88
- index += consumed;
89
- continue;
90
- }
91
- if (applyFlagOption(options, argument)) {
92
- continue;
93
- }
94
- if (argument.startsWith("-")) {
95
- throw new Error(`unknown option: ${argument}`);
96
- }
97
- options.targets.push(argument);
98
- }
99
- return options;
100
- }
101
-
102
- function resolveBaselinePath(
103
- options: Options,
104
- config: QualityConfig,
105
- cwd: string,
106
- ): string | null {
107
- if (options.baselinePath !== undefined) {
108
- return resolve(cwd, options.baselinePath);
109
- }
110
- if (config.baseline === false) {
111
- return null;
112
- }
113
- return resolve(cwd, config.baseline ?? DEFAULT_BASELINE_FILE);
114
- }
115
-
116
- function resolveConfigPath(options: Options, cwd: string): string | null {
117
- if (options.configPath !== undefined) {
118
- return resolve(cwd, options.configPath);
119
- }
120
- return findConfigFile(cwd);
121
- }
122
-
123
- async function main(argv: string[]): Promise<number> {
124
- if (argv.includes("--help") || argv.includes("-h")) {
125
- console.log(USAGE);
126
- return 0;
127
- }
128
- const options = parseArguments(argv);
129
- const color = colorEnabled(process.stdout, process.env);
130
- const paint = color ? ansiPainter() : plainPainter;
131
- const cwd = process.cwd();
132
- const configPath = resolveConfigPath(options, cwd);
133
- if (configPath === null) {
134
- console.error(`quality.config.ts not found in ${cwd}`);
135
- return 2;
136
- }
137
- const config = await loadConfig(configPath);
138
- const baselinePath = resolveBaselinePath(options, config, cwd);
139
- const results = await runEngines({
140
- baseline:
141
- options.update || baselinePath === null
142
- ? null
143
- : readBaseline(baselinePath),
144
- color,
145
- config,
146
- cwd,
147
- overrides: { ignore: options.ignores, targets: options.targets },
148
- rawBaseline: join(tmpdir(), `quality-check-raw-${process.pid}.json`),
149
- });
150
- if (options.update) {
151
- if (baselinePath === null) {
152
- console.error("baseline is disabled by the configuration");
153
- return 2;
154
- }
155
- const baseline = createBaseline(
156
- results.flatMap((result) => result.detected),
157
- );
158
- writeBaseline(baselinePath, baseline);
159
- console.log(
160
- paint(
161
- `Recorded ${baseline.entries.length} entries in ${baselinePath}`,
162
- "pass",
163
- ),
164
- );
165
- return 0;
166
- }
167
- if (options.json) {
168
- console.log(JSON.stringify(toJsonReport(results), null, "\t"));
169
- } else {
170
- for (const result of results) {
171
- console.log(formatEngineSection(result, paint));
172
- }
173
- console.log(formatSummary(results, paint));
174
- }
175
- return results.some((result) => result.status !== "passed") ? 1 : 0;
176
- }
177
-
178
- try {
179
- process.exit(await main(process.argv.slice(2)));
180
- } catch (error) {
181
- console.error(error instanceof Error ? error.message : String(error));
182
- process.exit(2);
183
- }
package/src/color.ts DELETED
@@ -1,47 +0,0 @@
1
- /**
2
- * 出力行へ割り当てる装飾の種類
3
- */
4
- export type Tone = "error" | "header" | "muted" | "pass" | "warn";
5
-
6
- /**
7
- * toneに応じて文字列を装飾する関数
8
- */
9
- export type Painter = (text: string, tone: Tone) => string;
10
-
11
- const ANSI_CODES: Record<Tone, string> = {
12
- error: "31",
13
- header: "36",
14
- muted: "2",
15
- pass: "32",
16
- warn: "33",
17
- };
18
-
19
- /**
20
- * 装飾を付けずにそのまま返すpainter
21
- */
22
- export function plainPainter(text: string, _tone: Tone): string {
23
- return text;
24
- }
25
-
26
- /**
27
- * ANSI escapeでtoneを色へ割り当てるpainterを作る
28
- */
29
- export function ansiPainter(): Painter {
30
- return (text, tone) => `\u001b[${ANSI_CODES[tone]}m${text}\u001b[0m`;
31
- }
32
-
33
- /**
34
- * 出力先と環境変数から装飾の可否を決める
35
- */
36
- export function colorEnabled(
37
- stream: { isTTY?: boolean | undefined },
38
- env: Record<string, string | undefined>,
39
- ): boolean {
40
- if (env.NO_COLOR !== undefined && env.NO_COLOR !== "") {
41
- return false;
42
- }
43
- if (env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== "") {
44
- return env.FORCE_COLOR !== "0";
45
- }
46
- return stream.isTTY === true;
47
- }