@yuu1111/comment-check 1.0.1 → 2.1.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 +10 -0
- package/README.md +14 -0
- package/dist/cli.js +627 -0
- package/dist/scan.js +404 -0
- package/package.json +11 -6
- package/src/baseline.ts +0 -87
- package/src/cli.ts +0 -184
- package/src/comments.ts +0 -244
- package/src/rules.ts +0 -60
- package/src/scan.ts +0 -145
package/dist/scan.js
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/scan.ts
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
import { relative as relative2 } from "path";
|
|
5
|
+
|
|
6
|
+
// ../shared/src/files.ts
|
|
7
|
+
import { readdirSync, statSync } from "fs";
|
|
8
|
+
import { extname, join, relative, resolve } from "path";
|
|
9
|
+
var IGNORED_DIRECTORIES = new Set([
|
|
10
|
+
"build",
|
|
11
|
+
"coverage",
|
|
12
|
+
"dist",
|
|
13
|
+
"node_modules",
|
|
14
|
+
"out",
|
|
15
|
+
"vendor"
|
|
16
|
+
]);
|
|
17
|
+
function normalizePath(path) {
|
|
18
|
+
return path.split("\\").join("/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
19
|
+
}
|
|
20
|
+
function isIgnored(path, ignores) {
|
|
21
|
+
return ignores.some((ignore) => path === ignore || path.startsWith(`${ignore}/`));
|
|
22
|
+
}
|
|
23
|
+
function walk(directory, extensions, files) {
|
|
24
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
25
|
+
if (entry.name.startsWith(".")) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const path = join(directory, entry.name);
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
31
|
+
walk(path, extensions, files);
|
|
32
|
+
}
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (entry.isFile() && extensions.has(extname(entry.name))) {
|
|
36
|
+
files.add(path);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function collectFiles(targets, options) {
|
|
41
|
+
const cwd = options.cwd ?? process.cwd();
|
|
42
|
+
const ignores = options.ignores ?? [];
|
|
43
|
+
const files = new Set;
|
|
44
|
+
for (const target of targets) {
|
|
45
|
+
const absolute = resolve(cwd, target);
|
|
46
|
+
let stats;
|
|
47
|
+
try {
|
|
48
|
+
stats = statSync(absolute);
|
|
49
|
+
} catch {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (stats.isFile()) {
|
|
53
|
+
if (options.extensions.has(extname(absolute)) && !isIgnored(normalizePath(target), ignores)) {
|
|
54
|
+
files.add(absolute);
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
walk(absolute, options.extensions, files);
|
|
59
|
+
}
|
|
60
|
+
return [...files].filter((file) => !isIgnored(normalizePath(relative(cwd, file)), ignores)).sort();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ../shared/src/findings.ts
|
|
64
|
+
function compareFindings(left, right) {
|
|
65
|
+
if (left.file !== right.file) {
|
|
66
|
+
return left.file < right.file ? -1 : 1;
|
|
67
|
+
}
|
|
68
|
+
if (left.line !== right.line) {
|
|
69
|
+
return left.line - right.line;
|
|
70
|
+
}
|
|
71
|
+
return left.column - right.column;
|
|
72
|
+
}
|
|
73
|
+
function formatLocation(finding) {
|
|
74
|
+
return `${finding.file}:${finding.line}:${finding.column}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/comments.ts
|
|
78
|
+
var REGEX_PRECEDING_CHARACTERS = new Set("(,=:[!&|?{};".split(""));
|
|
79
|
+
var REGEX_PRECEDING_KEYWORDS = new Set([
|
|
80
|
+
"await",
|
|
81
|
+
"case",
|
|
82
|
+
"delete",
|
|
83
|
+
"do",
|
|
84
|
+
"else",
|
|
85
|
+
"in",
|
|
86
|
+
"instanceof",
|
|
87
|
+
"new",
|
|
88
|
+
"of",
|
|
89
|
+
"return",
|
|
90
|
+
"throw",
|
|
91
|
+
"typeof",
|
|
92
|
+
"void",
|
|
93
|
+
"yield"
|
|
94
|
+
]);
|
|
95
|
+
function isIdentifierCharacter(character) {
|
|
96
|
+
return /[A-Za-z0-9_$]/.test(character);
|
|
97
|
+
}
|
|
98
|
+
function skipString(source, start, quote) {
|
|
99
|
+
let index = start + 1;
|
|
100
|
+
while (index < source.length) {
|
|
101
|
+
const character = source[index] ?? "";
|
|
102
|
+
if (character === "\\") {
|
|
103
|
+
index += 2;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (character === quote) {
|
|
107
|
+
return index + 1;
|
|
108
|
+
}
|
|
109
|
+
if (character === `
|
|
110
|
+
`) {
|
|
111
|
+
return index;
|
|
112
|
+
}
|
|
113
|
+
index += 1;
|
|
114
|
+
}
|
|
115
|
+
return index;
|
|
116
|
+
}
|
|
117
|
+
function isRegexStart(source, index) {
|
|
118
|
+
let previous = index - 1;
|
|
119
|
+
while (previous >= 0 && /\s/.test(source[previous] ?? "")) {
|
|
120
|
+
previous -= 1;
|
|
121
|
+
}
|
|
122
|
+
if (previous < 0) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
const character = source[previous] ?? "";
|
|
126
|
+
if (REGEX_PRECEDING_CHARACTERS.has(character)) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
if (!isIdentifierCharacter(character)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
let wordStart = previous;
|
|
133
|
+
while (wordStart >= 0 && isIdentifierCharacter(source[wordStart] ?? "")) {
|
|
134
|
+
wordStart -= 1;
|
|
135
|
+
}
|
|
136
|
+
return REGEX_PRECEDING_KEYWORDS.has(source.slice(wordStart + 1, previous + 1));
|
|
137
|
+
}
|
|
138
|
+
function skipRegex(source, start) {
|
|
139
|
+
let index = start + 1;
|
|
140
|
+
let inClass = false;
|
|
141
|
+
while (index < source.length) {
|
|
142
|
+
const character = source[index] ?? "";
|
|
143
|
+
if (character === "\\") {
|
|
144
|
+
index += 2;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (character === `
|
|
148
|
+
`) {
|
|
149
|
+
return start;
|
|
150
|
+
}
|
|
151
|
+
if (inClass) {
|
|
152
|
+
inClass = character !== "]";
|
|
153
|
+
index += 1;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (character === "[") {
|
|
157
|
+
inClass = true;
|
|
158
|
+
index += 1;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (character === "/") {
|
|
162
|
+
return index + 1;
|
|
163
|
+
}
|
|
164
|
+
index += 1;
|
|
165
|
+
}
|
|
166
|
+
return start;
|
|
167
|
+
}
|
|
168
|
+
function currentMode(state) {
|
|
169
|
+
return state.modes[state.modes.length - 1] ?? "code";
|
|
170
|
+
}
|
|
171
|
+
function stepComment(state, source) {
|
|
172
|
+
const character = source[state.index] ?? "";
|
|
173
|
+
const next = source[state.index + 1] ?? "";
|
|
174
|
+
if (character !== "/" || next !== "/" && next !== "*") {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
if (next === "/") {
|
|
178
|
+
const newline = source.indexOf(`
|
|
179
|
+
`, state.index);
|
|
180
|
+
const stop2 = newline === -1 ? source.length : newline;
|
|
181
|
+
state.comments.push({
|
|
182
|
+
end: stop2,
|
|
183
|
+
kind: "line",
|
|
184
|
+
start: state.index,
|
|
185
|
+
text: source.slice(state.index + 2, stop2)
|
|
186
|
+
});
|
|
187
|
+
state.index = stop2;
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
const close = source.indexOf("*/", state.index + 2);
|
|
191
|
+
const stop = close === -1 ? source.length : close + 2;
|
|
192
|
+
state.comments.push({
|
|
193
|
+
end: stop,
|
|
194
|
+
kind: "block",
|
|
195
|
+
start: state.index,
|
|
196
|
+
text: source.slice(state.index + 2, Math.max(state.index + 2, stop - 2))
|
|
197
|
+
});
|
|
198
|
+
state.index = stop;
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
function stepString(state, source) {
|
|
202
|
+
const character = source[state.index] ?? "";
|
|
203
|
+
if (character !== "'" && character !== '"') {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
state.index = skipString(source, state.index, character);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
function stepTemplateStart(state, source) {
|
|
210
|
+
if (source[state.index] !== "`") {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
state.modes.push("template");
|
|
214
|
+
state.index += 1;
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
function stepRegex(state, source) {
|
|
218
|
+
if (source[state.index] !== "/" || !isRegexStart(source, state.index)) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
const stop = skipRegex(source, state.index);
|
|
222
|
+
if (stop <= state.index) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
state.index = stop;
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
function stepTemplate(state, source) {
|
|
229
|
+
const character = source[state.index] ?? "";
|
|
230
|
+
if (character === "\\") {
|
|
231
|
+
state.index += 2;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (character === "`") {
|
|
235
|
+
state.modes.pop();
|
|
236
|
+
state.index += 1;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (character === "$" && source[state.index + 1] === "{") {
|
|
240
|
+
state.modes.push("expression");
|
|
241
|
+
state.braces.push(1);
|
|
242
|
+
state.index += 2;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
state.index += 1;
|
|
246
|
+
}
|
|
247
|
+
function stepExpression(state, source) {
|
|
248
|
+
const character = source[state.index] ?? "";
|
|
249
|
+
const depth = state.braces[state.braces.length - 1] ?? 0;
|
|
250
|
+
if (character === "{") {
|
|
251
|
+
state.braces[state.braces.length - 1] = depth + 1;
|
|
252
|
+
} else if (character === "}") {
|
|
253
|
+
state.braces[state.braces.length - 1] = depth - 1;
|
|
254
|
+
if (depth - 1 === 0) {
|
|
255
|
+
state.modes.pop();
|
|
256
|
+
state.braces.pop();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
state.index += 1;
|
|
260
|
+
}
|
|
261
|
+
function step(state, source) {
|
|
262
|
+
if (currentMode(state) === "template") {
|
|
263
|
+
stepTemplate(state, source);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (stepComment(state, source) || stepString(state, source) || stepTemplateStart(state, source) || stepRegex(state, source)) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
stepExpression(state, source);
|
|
270
|
+
}
|
|
271
|
+
function extractComments(source) {
|
|
272
|
+
const shebang = source.startsWith("#!") ? source.indexOf(`
|
|
273
|
+
`) : 0;
|
|
274
|
+
const state = {
|
|
275
|
+
braces: [],
|
|
276
|
+
comments: [],
|
|
277
|
+
index: Math.max(0, shebang),
|
|
278
|
+
modes: ["code"]
|
|
279
|
+
};
|
|
280
|
+
while (state.index < source.length) {
|
|
281
|
+
step(state, source);
|
|
282
|
+
}
|
|
283
|
+
return state.comments;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/rules.ts
|
|
287
|
+
var OPT_IN_RULE_IDS = ["japanese-period"];
|
|
288
|
+
var PLACEHOLDER_PATTERN = /\b(TODO|FIXME|XXX|HACK)\b/;
|
|
289
|
+
var SEPARATOR_PATTERN = /^[-=*_#~+./\\|]{4,}$/;
|
|
290
|
+
var DIRECTIVE_PATTERN = /^@ts-(?:ignore|expect-error)\b([\s\S]*)$/;
|
|
291
|
+
var JAPANESE_PERIOD = "\u3002";
|
|
292
|
+
function parseEnabledRules(values) {
|
|
293
|
+
const enabled = [];
|
|
294
|
+
for (const value of values) {
|
|
295
|
+
if (!OPT_IN_RULE_IDS.includes(value)) {
|
|
296
|
+
throw new Error(`unknown rule: ${value}`);
|
|
297
|
+
}
|
|
298
|
+
const rule = value;
|
|
299
|
+
if (!enabled.includes(rule)) {
|
|
300
|
+
enabled.push(rule);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return enabled;
|
|
304
|
+
}
|
|
305
|
+
function findJapanesePeriod(text) {
|
|
306
|
+
return text.indexOf(JAPANESE_PERIOD);
|
|
307
|
+
}
|
|
308
|
+
function normalizeComment(body) {
|
|
309
|
+
return body.split(`
|
|
310
|
+
`).map((line) => line.replace(/^\s*\*+\s?/, "")).join(" ").replace(/\s+/g, " ").trim();
|
|
311
|
+
}
|
|
312
|
+
function classifyComment(body) {
|
|
313
|
+
const text = normalizeComment(body);
|
|
314
|
+
if (text === "") {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
if (/^(?:biome-ignore-all|@ts-nocheck)\b/.test(text) || /^eslint-disable(?:-next-line|-line)?\s*$/.test(text)) {
|
|
318
|
+
return "broad-suppression";
|
|
319
|
+
}
|
|
320
|
+
const directive = DIRECTIVE_PATTERN.exec(text);
|
|
321
|
+
if (directive) {
|
|
322
|
+
return (directive[1] ?? "").replace(/^[\s:\u2014-]+/, "") === "" ? "undocumented-directive" : null;
|
|
323
|
+
}
|
|
324
|
+
if (PLACEHOLDER_PATTERN.test(text)) {
|
|
325
|
+
return "placeholder-comment";
|
|
326
|
+
}
|
|
327
|
+
if (SEPARATOR_PATTERN.test(text)) {
|
|
328
|
+
return "separator-comment";
|
|
329
|
+
}
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/scan.ts
|
|
334
|
+
var SUPPORTED_EXTENSIONS = new Set([
|
|
335
|
+
".cjs",
|
|
336
|
+
".cts",
|
|
337
|
+
".js",
|
|
338
|
+
".jsx",
|
|
339
|
+
".mjs",
|
|
340
|
+
".mts",
|
|
341
|
+
".ts",
|
|
342
|
+
".tsx"
|
|
343
|
+
]);
|
|
344
|
+
function positionAt(source, offset) {
|
|
345
|
+
let line = 1;
|
|
346
|
+
let lineStart = 0;
|
|
347
|
+
for (let index = 0;index < offset; index += 1) {
|
|
348
|
+
if (source[index] === `
|
|
349
|
+
`) {
|
|
350
|
+
line += 1;
|
|
351
|
+
lineStart = index + 1;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return { line, column: offset - lineStart + 1 };
|
|
355
|
+
}
|
|
356
|
+
function scanSource(source, file, enabled = []) {
|
|
357
|
+
const findings = [];
|
|
358
|
+
for (const comment of extractComments(source)) {
|
|
359
|
+
const text = normalizeComment(comment.text);
|
|
360
|
+
const rule = classifyComment(comment.text);
|
|
361
|
+
if (rule !== null) {
|
|
362
|
+
const position2 = positionAt(source, comment.start);
|
|
363
|
+
findings.push({
|
|
364
|
+
column: position2.column,
|
|
365
|
+
file,
|
|
366
|
+
line: position2.line,
|
|
367
|
+
rule,
|
|
368
|
+
text
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
if (!enabled.includes("japanese-period")) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const offset = findJapanesePeriod(comment.text);
|
|
375
|
+
if (offset < 0) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const position = positionAt(source, comment.start + 2 + offset);
|
|
379
|
+
findings.push({
|
|
380
|
+
column: position.column,
|
|
381
|
+
file,
|
|
382
|
+
line: position.line,
|
|
383
|
+
rule: "japanese-period",
|
|
384
|
+
text
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
return findings;
|
|
388
|
+
}
|
|
389
|
+
function scanFile(file, cwd = process.cwd(), enabled = []) {
|
|
390
|
+
return scanSource(readFileSync(file, "utf8"), normalizePath(relative2(cwd, file)), enabled);
|
|
391
|
+
}
|
|
392
|
+
function scanFiles(files, cwd = process.cwd(), enabled = []) {
|
|
393
|
+
const findings = [];
|
|
394
|
+
for (const file of files) {
|
|
395
|
+
findings.push(...scanFile(file, cwd, enabled));
|
|
396
|
+
}
|
|
397
|
+
return findings.sort(compareFindings);
|
|
398
|
+
}
|
|
399
|
+
export {
|
|
400
|
+
scanSource,
|
|
401
|
+
scanFiles,
|
|
402
|
+
scanFile,
|
|
403
|
+
SUPPORTED_EXTENSIONS
|
|
404
|
+
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yuu1111/comment-check",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Shared comment and suppression checker",
|
|
5
|
-
"license": "MIT",
|
|
6
5
|
"repository": {
|
|
7
6
|
"type": "git",
|
|
8
7
|
"url": "git+https://github.com/yuu1111/configs.git",
|
|
@@ -10,18 +9,24 @@
|
|
|
10
9
|
},
|
|
11
10
|
"type": "module",
|
|
12
11
|
"bin": {
|
|
13
|
-
"comment-check": "
|
|
12
|
+
"comment-check": "dist/cli.js"
|
|
14
13
|
},
|
|
15
14
|
"exports": {
|
|
16
|
-
".": "./
|
|
15
|
+
".": "./dist/scan.js"
|
|
17
16
|
},
|
|
18
17
|
"files": [
|
|
19
18
|
"README.ja.md",
|
|
20
|
-
"
|
|
19
|
+
"dist"
|
|
21
20
|
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "bun build src/cli.ts src/scan.ts --target=bun --outdir=dist"
|
|
23
|
+
},
|
|
22
24
|
"keywords": [
|
|
23
25
|
"comment",
|
|
24
26
|
"lint",
|
|
25
27
|
"suppression"
|
|
26
|
-
]
|
|
28
|
+
],
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@yuu1111/shared": "workspace:*"
|
|
31
|
+
}
|
|
27
32
|
}
|
package/src/baseline.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import type { Finding } from "./rules";
|
|
2
|
-
|
|
3
|
-
/** baselineに記録する指摘1件の識別情報と件数 */
|
|
4
|
-
export interface BaselineEntry {
|
|
5
|
-
rule: string;
|
|
6
|
-
file: string;
|
|
7
|
-
text: string;
|
|
8
|
-
count: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** baseline fileの形式と記録済みentryの一覧 */
|
|
12
|
-
export interface BaselineFile {
|
|
13
|
-
version: 1;
|
|
14
|
-
entries: BaselineEntry[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** baselineと現在の指摘を比較した結果 */
|
|
18
|
-
export interface BaselineComparison {
|
|
19
|
-
added: Finding[];
|
|
20
|
-
resolved: BaselineEntry[];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** entryをbaseline上で一意に識別するkeyを返す */
|
|
24
|
-
export function entryKey(entry: {
|
|
25
|
-
rule: string;
|
|
26
|
-
file: string;
|
|
27
|
-
text: string;
|
|
28
|
-
}): string {
|
|
29
|
-
return [entry.rule, entry.file, entry.text].join("\u0000");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function compareEntries(left: BaselineEntry, right: BaselineEntry): number {
|
|
33
|
-
const leftKey = entryKey(left);
|
|
34
|
-
const rightKey = entryKey(right);
|
|
35
|
-
if (leftKey === rightKey) {
|
|
36
|
-
return 0;
|
|
37
|
-
}
|
|
38
|
-
return leftKey < rightKey ? -1 : 1;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** 指摘一覧を件数付きentryへ集計してbaselineを作成する */
|
|
42
|
-
export function createBaseline(findings: Finding[]): BaselineFile {
|
|
43
|
-
const entries = new Map<string, BaselineEntry>();
|
|
44
|
-
for (const finding of findings) {
|
|
45
|
-
const key = entryKey(finding);
|
|
46
|
-
const existing = entries.get(key);
|
|
47
|
-
if (existing) {
|
|
48
|
-
existing.count += 1;
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
entries.set(key, {
|
|
52
|
-
rule: finding.rule,
|
|
53
|
-
file: finding.file,
|
|
54
|
-
text: finding.text,
|
|
55
|
-
count: 1,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
return { version: 1, entries: [...entries.values()].sort(compareEntries) };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** 現在の指摘とbaselineを突き合わせ、新規追加分と解消済み分を求める */
|
|
62
|
-
export function compareWithBaseline(
|
|
63
|
-
findings: Finding[],
|
|
64
|
-
baseline: BaselineFile,
|
|
65
|
-
): BaselineComparison {
|
|
66
|
-
const remaining = new Map<string, number>();
|
|
67
|
-
for (const entry of baseline.entries) {
|
|
68
|
-
const key = entryKey(entry);
|
|
69
|
-
remaining.set(key, (remaining.get(key) ?? 0) + entry.count);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const added: Finding[] = [];
|
|
73
|
-
for (const finding of findings) {
|
|
74
|
-
const key = entryKey(finding);
|
|
75
|
-
const count = remaining.get(key) ?? 0;
|
|
76
|
-
if (count > 0) {
|
|
77
|
-
remaining.set(key, count - 1);
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
added.push(finding);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const resolved = baseline.entries.filter(
|
|
84
|
-
(entry) => (remaining.get(entryKey(entry)) ?? 0) > 0,
|
|
85
|
-
);
|
|
86
|
-
return { added, resolved };
|
|
87
|
-
}
|
package/src/cli.ts
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import {
|
|
4
|
-
type BaselineEntry,
|
|
5
|
-
type BaselineFile,
|
|
6
|
-
compareWithBaseline,
|
|
7
|
-
createBaseline,
|
|
8
|
-
} from "./baseline";
|
|
9
|
-
import type { Finding } from "./rules";
|
|
10
|
-
import { collectFiles, normalizePath, scanFiles } from "./scan";
|
|
11
|
-
|
|
12
|
-
const DEFAULT_BASELINE = "comment-baseline.json";
|
|
13
|
-
|
|
14
|
-
const RULE_MESSAGES: Record<string, string> = {
|
|
15
|
-
"broad-suppression": "file-wide suppression hides too much",
|
|
16
|
-
"placeholder-comment": "placeholder comment should be resolved or tracked",
|
|
17
|
-
"separator-comment": "decorative separator comment adds no information",
|
|
18
|
-
"undocumented-directive": "TypeScript directive needs a description",
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
interface Options {
|
|
22
|
-
baselinePath: string;
|
|
23
|
-
ignores: string[];
|
|
24
|
-
json: boolean;
|
|
25
|
-
targets: string[];
|
|
26
|
-
update: boolean;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
type JsonObject = Record<string, unknown>;
|
|
30
|
-
|
|
31
|
-
function isJsonObject(value: unknown): value is JsonObject {
|
|
32
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isBaselineEntry(value: unknown): value is BaselineEntry {
|
|
36
|
-
if (!isJsonObject(value)) {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
return (
|
|
40
|
-
typeof value.rule === "string" &&
|
|
41
|
-
typeof value.file === "string" &&
|
|
42
|
-
typeof value.text === "string" &&
|
|
43
|
-
typeof value.count === "number"
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function applyFlagOption(options: Options, argument: string): boolean {
|
|
48
|
-
if (argument === "--update-baseline") {
|
|
49
|
-
options.update = true;
|
|
50
|
-
return true;
|
|
51
|
-
}
|
|
52
|
-
if (argument === "--json") {
|
|
53
|
-
options.json = true;
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function applyValueOption(
|
|
60
|
-
options: Options,
|
|
61
|
-
argument: string,
|
|
62
|
-
argv: string[],
|
|
63
|
-
index: number,
|
|
64
|
-
): number | null {
|
|
65
|
-
if (argument === "--baseline") {
|
|
66
|
-
options.baselinePath = argv[index + 1] ?? DEFAULT_BASELINE;
|
|
67
|
-
return 1;
|
|
68
|
-
}
|
|
69
|
-
if (argument.startsWith("--baseline=")) {
|
|
70
|
-
options.baselinePath = argument.slice("--baseline=".length);
|
|
71
|
-
return 0;
|
|
72
|
-
}
|
|
73
|
-
if (argument === "--ignore") {
|
|
74
|
-
const value = argv[index + 1];
|
|
75
|
-
if (value !== undefined) {
|
|
76
|
-
options.ignores.push(normalizePath(value));
|
|
77
|
-
}
|
|
78
|
-
return 1;
|
|
79
|
-
}
|
|
80
|
-
if (argument.startsWith("--ignore=")) {
|
|
81
|
-
options.ignores.push(normalizePath(argument.slice("--ignore=".length)));
|
|
82
|
-
return 0;
|
|
83
|
-
}
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function parseArguments(argv: string[]): Options {
|
|
88
|
-
const options: Options = {
|
|
89
|
-
baselinePath: DEFAULT_BASELINE,
|
|
90
|
-
ignores: [],
|
|
91
|
-
json: false,
|
|
92
|
-
targets: [],
|
|
93
|
-
update: false,
|
|
94
|
-
};
|
|
95
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
96
|
-
const argument = argv[index] ?? "";
|
|
97
|
-
const consumed = applyValueOption(options, argument, argv, index);
|
|
98
|
-
if (consumed !== null) {
|
|
99
|
-
index += consumed;
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
if (applyFlagOption(options, argument)) {
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
if (argument.startsWith("-")) {
|
|
106
|
-
throw new Error(`unknown option: ${argument}`);
|
|
107
|
-
}
|
|
108
|
-
options.targets.push(argument);
|
|
109
|
-
}
|
|
110
|
-
if (options.targets.length === 0) {
|
|
111
|
-
options.targets.push(".");
|
|
112
|
-
}
|
|
113
|
-
return options;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function readBaseline(path: string): BaselineFile {
|
|
117
|
-
if (!existsSync(path)) {
|
|
118
|
-
return { entries: [], version: 1 };
|
|
119
|
-
}
|
|
120
|
-
const value: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
121
|
-
if (
|
|
122
|
-
!isJsonObject(value) ||
|
|
123
|
-
!Array.isArray(value.entries) ||
|
|
124
|
-
!value.entries.every(isBaselineEntry)
|
|
125
|
-
) {
|
|
126
|
-
throw new Error(`${path} is not a comment baseline`);
|
|
127
|
-
}
|
|
128
|
-
return { entries: value.entries, version: 1 };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function describeFinding(finding: Finding): string {
|
|
132
|
-
return `${finding.file}:${finding.line}:${finding.column} ${finding.rule} ${RULE_MESSAGES[finding.rule] ?? ""}`.trimEnd();
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function main(argv: string[]): number {
|
|
136
|
-
if (argv.includes("--help")) {
|
|
137
|
-
console.log(
|
|
138
|
-
"Usage: comment-check [--baseline <path>] [--ignore <path>] [--update-baseline] [--json] [path...]",
|
|
139
|
-
);
|
|
140
|
-
return 0;
|
|
141
|
-
}
|
|
142
|
-
const options = parseArguments(argv);
|
|
143
|
-
const files = collectFiles(options.targets, process.cwd(), options.ignores);
|
|
144
|
-
const findings = scanFiles(files);
|
|
145
|
-
|
|
146
|
-
if (options.update) {
|
|
147
|
-
const baseline = createBaseline(findings);
|
|
148
|
-
writeFileSync(
|
|
149
|
-
options.baselinePath,
|
|
150
|
-
`${JSON.stringify(baseline, null, "\t")}\n`,
|
|
151
|
-
);
|
|
152
|
-
console.log(
|
|
153
|
-
`Recorded ${baseline.entries.length} entries in ${options.baselinePath}`,
|
|
154
|
-
);
|
|
155
|
-
return 0;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const baseline = readBaseline(options.baselinePath);
|
|
159
|
-
const comparison = compareWithBaseline(findings, baseline);
|
|
160
|
-
if (options.json) {
|
|
161
|
-
console.log(
|
|
162
|
-
JSON.stringify(
|
|
163
|
-
{ added: comparison.added, resolved: comparison.resolved },
|
|
164
|
-
null,
|
|
165
|
-
"\t",
|
|
166
|
-
),
|
|
167
|
-
);
|
|
168
|
-
} else {
|
|
169
|
-
for (const finding of comparison.added) {
|
|
170
|
-
console.log(describeFinding(finding));
|
|
171
|
-
}
|
|
172
|
-
console.log(
|
|
173
|
-
`Checked ${files.length} files: ${comparison.added.length} new, ${comparison.resolved.length} resolved, ${baseline.entries.length} baselined`,
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
return comparison.added.length > 0 ? 1 : 0;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
process.exit(main(process.argv.slice(2)));
|
|
181
|
-
} catch (error) {
|
|
182
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
183
|
-
process.exit(2);
|
|
184
|
-
}
|