@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/README.ja.md
CHANGED
|
@@ -24,6 +24,12 @@ src/queue.ts:18:2 undocumented-directive TypeScript directive needs a descriptio
|
|
|
24
24
|
Checked 42 files: 1 new, 0 resolved, 3 baselined
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
日本語の文末の `。` も止めるProjectはopt-inのruleを指定する
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
comment-check --enable japanese-period .
|
|
31
|
+
```
|
|
32
|
+
|
|
27
33
|
## Rules
|
|
28
34
|
|
|
29
35
|
| Rule | 検出対象 |
|
|
@@ -32,12 +38,14 @@ Checked 42 files: 1 new, 0 resolved, 3 baselined
|
|
|
32
38
|
| `undocumented-directive` | 説明の無い `@ts-ignore` と `@ts-expect-error` |
|
|
33
39
|
| `placeholder-comment` | `TODO`、`FIXME`、`XXX`、`HACK` |
|
|
34
40
|
| `separator-comment` | 記号だけで作った装飾comment |
|
|
41
|
+
| `japanese-period` | 日本語の文を終える `。` を含むcomment(opt-in) |
|
|
35
42
|
|
|
36
43
|
## Options
|
|
37
44
|
|
|
38
45
|
| Option | Description |
|
|
39
46
|
|--------|-------------|
|
|
40
47
|
| `--baseline <path>` | 読み書きするbaseline file(既定は `comment-baseline.json`) |
|
|
48
|
+
| `--enable <rule>` | opt-in ruleを実行する 複数指定できる 未知の名前は設定error |
|
|
41
49
|
| `--ignore <path>` | 検査から外すpath 複数指定できる |
|
|
42
50
|
| `--update-baseline` | baselineを現在の検出で置き換える |
|
|
43
51
|
| `--json` | 新規と解消済みの検出をJSONで出力する |
|
|
@@ -47,3 +55,5 @@ Checked 42 files: 1 new, 0 resolved, 3 baselined
|
|
|
47
55
|
生成物のディレクトリなどProjectが持つpathは `--ignore` で検査から外す 例えば `--ignore src/generated`
|
|
48
56
|
|
|
49
57
|
baselineはrule、file、comment本文をキーにするため、行が動いてもそのcommentを新規とは報告しない Biomeは `biome-ignore` の理由と未使用の抑制を既に扱うため、このcheckerはBiomeが読まないcommentの細部だけを担当する
|
|
58
|
+
|
|
59
|
+
`japanese-period` は `--enable` で指定するまで動かないため、既存Projectの検出結果は変わらない 最初の `。` の位置を行と桁で報告し、句点が複数あっても1 commentにつき1件にまとめ、comment本文は書き換えない
|
package/README.md
CHANGED
|
@@ -25,6 +25,13 @@ src/queue.ts:18:2 undocumented-directive TypeScript directive needs a descriptio
|
|
|
25
25
|
Checked 42 files: 1 new, 0 resolved, 3 baselined
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
A project that also keeps Japanese sentences free of a trailing `。` names the
|
|
29
|
+
opt-in rule:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
comment-check --enable japanese-period .
|
|
33
|
+
```
|
|
34
|
+
|
|
28
35
|
## Rules
|
|
29
36
|
|
|
30
37
|
| Rule | Detects |
|
|
@@ -33,12 +40,14 @@ Checked 42 files: 1 new, 0 resolved, 3 baselined
|
|
|
33
40
|
| `undocumented-directive` | `@ts-ignore` or `@ts-expect-error` without a description |
|
|
34
41
|
| `placeholder-comment` | `TODO`, `FIXME`, `XXX`, `HACK` |
|
|
35
42
|
| `separator-comment` | decorative comments made only of punctuation |
|
|
43
|
+
| `japanese-period` | a Japanese sentence in a comment that ends with `。` (opt-in) |
|
|
36
44
|
|
|
37
45
|
## Options
|
|
38
46
|
|
|
39
47
|
| Option | Description |
|
|
40
48
|
|--------|-------------|
|
|
41
49
|
| `--baseline <path>` | Baseline file to read or write (default `comment-baseline.json`) |
|
|
50
|
+
| `--enable <rule>` | Run an opt-in rule, repeatable; an unknown name is a configuration error |
|
|
42
51
|
| `--ignore <path>` | Path to leave out, repeatable |
|
|
43
52
|
| `--update-baseline` | Replace the baseline with the current findings |
|
|
44
53
|
| `--json` | Print new and resolved findings as JSON |
|
|
@@ -52,3 +61,8 @@ The baseline keys on the rule, the file, and the comment text, so moving a line
|
|
|
52
61
|
does not report the comment as new. Biome already requires a reason on
|
|
53
62
|
`biome-ignore` and reports unused suppressions, so this checker only covers the
|
|
54
63
|
comment trivia that Biome does not read.
|
|
64
|
+
|
|
65
|
+
`japanese-period` stays off until `--enable` names it, so an existing project
|
|
66
|
+
keeps the findings it had. The rule reports the first `。` of a comment at its
|
|
67
|
+
own line and column, reports one finding per comment however many periods it
|
|
68
|
+
holds, and never rewrites the comment.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/scan.ts
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
import { relative as relative2 } from "path";
|
|
7
|
+
|
|
8
|
+
// ../shared/src/files.ts
|
|
9
|
+
import { readdirSync, statSync } from "fs";
|
|
10
|
+
import { extname, join, relative, resolve } from "path";
|
|
11
|
+
var IGNORED_DIRECTORIES = new Set([
|
|
12
|
+
"build",
|
|
13
|
+
"coverage",
|
|
14
|
+
"dist",
|
|
15
|
+
"node_modules",
|
|
16
|
+
"out",
|
|
17
|
+
"vendor"
|
|
18
|
+
]);
|
|
19
|
+
function normalizePath(path) {
|
|
20
|
+
return path.split("\\").join("/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
21
|
+
}
|
|
22
|
+
function isIgnored(path, ignores) {
|
|
23
|
+
return ignores.some((ignore) => path === ignore || path.startsWith(`${ignore}/`));
|
|
24
|
+
}
|
|
25
|
+
function walk(directory, extensions, files) {
|
|
26
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
27
|
+
if (entry.name.startsWith(".")) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const path = join(directory, entry.name);
|
|
31
|
+
if (entry.isDirectory()) {
|
|
32
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
33
|
+
walk(path, extensions, files);
|
|
34
|
+
}
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (entry.isFile() && extensions.has(extname(entry.name))) {
|
|
38
|
+
files.add(path);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function collectFiles(targets, options) {
|
|
43
|
+
const cwd = options.cwd ?? process.cwd();
|
|
44
|
+
const ignores = options.ignores ?? [];
|
|
45
|
+
const files = new Set;
|
|
46
|
+
for (const target of targets) {
|
|
47
|
+
const absolute = resolve(cwd, target);
|
|
48
|
+
let stats;
|
|
49
|
+
try {
|
|
50
|
+
stats = statSync(absolute);
|
|
51
|
+
} catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (stats.isFile()) {
|
|
55
|
+
if (options.extensions.has(extname(absolute)) && !isIgnored(normalizePath(target), ignores)) {
|
|
56
|
+
files.add(absolute);
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
walk(absolute, options.extensions, files);
|
|
61
|
+
}
|
|
62
|
+
return [...files].filter((file) => !isIgnored(normalizePath(relative(cwd, file)), ignores)).sort();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ../shared/src/findings.ts
|
|
66
|
+
function compareFindings(left, right) {
|
|
67
|
+
if (left.file !== right.file) {
|
|
68
|
+
return left.file < right.file ? -1 : 1;
|
|
69
|
+
}
|
|
70
|
+
if (left.line !== right.line) {
|
|
71
|
+
return left.line - right.line;
|
|
72
|
+
}
|
|
73
|
+
return left.column - right.column;
|
|
74
|
+
}
|
|
75
|
+
function formatLocation(finding) {
|
|
76
|
+
return `${finding.file}:${finding.line}:${finding.column}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/comments.ts
|
|
80
|
+
var REGEX_PRECEDING_CHARACTERS = new Set("(,=:[!&|?{};".split(""));
|
|
81
|
+
var REGEX_PRECEDING_KEYWORDS = new Set([
|
|
82
|
+
"await",
|
|
83
|
+
"case",
|
|
84
|
+
"delete",
|
|
85
|
+
"do",
|
|
86
|
+
"else",
|
|
87
|
+
"in",
|
|
88
|
+
"instanceof",
|
|
89
|
+
"new",
|
|
90
|
+
"of",
|
|
91
|
+
"return",
|
|
92
|
+
"throw",
|
|
93
|
+
"typeof",
|
|
94
|
+
"void",
|
|
95
|
+
"yield"
|
|
96
|
+
]);
|
|
97
|
+
function isIdentifierCharacter(character) {
|
|
98
|
+
return /[A-Za-z0-9_$]/.test(character);
|
|
99
|
+
}
|
|
100
|
+
function skipString(source, start, quote) {
|
|
101
|
+
let index = start + 1;
|
|
102
|
+
while (index < source.length) {
|
|
103
|
+
const character = source[index] ?? "";
|
|
104
|
+
if (character === "\\") {
|
|
105
|
+
index += 2;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (character === quote) {
|
|
109
|
+
return index + 1;
|
|
110
|
+
}
|
|
111
|
+
if (character === `
|
|
112
|
+
`) {
|
|
113
|
+
return index;
|
|
114
|
+
}
|
|
115
|
+
index += 1;
|
|
116
|
+
}
|
|
117
|
+
return index;
|
|
118
|
+
}
|
|
119
|
+
function isRegexStart(source, index) {
|
|
120
|
+
let previous = index - 1;
|
|
121
|
+
while (previous >= 0 && /\s/.test(source[previous] ?? "")) {
|
|
122
|
+
previous -= 1;
|
|
123
|
+
}
|
|
124
|
+
if (previous < 0) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
const character = source[previous] ?? "";
|
|
128
|
+
if (REGEX_PRECEDING_CHARACTERS.has(character)) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
if (!isIdentifierCharacter(character)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
let wordStart = previous;
|
|
135
|
+
while (wordStart >= 0 && isIdentifierCharacter(source[wordStart] ?? "")) {
|
|
136
|
+
wordStart -= 1;
|
|
137
|
+
}
|
|
138
|
+
return REGEX_PRECEDING_KEYWORDS.has(source.slice(wordStart + 1, previous + 1));
|
|
139
|
+
}
|
|
140
|
+
function skipRegex(source, start) {
|
|
141
|
+
let index = start + 1;
|
|
142
|
+
let inClass = false;
|
|
143
|
+
while (index < source.length) {
|
|
144
|
+
const character = source[index] ?? "";
|
|
145
|
+
if (character === "\\") {
|
|
146
|
+
index += 2;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (character === `
|
|
150
|
+
`) {
|
|
151
|
+
return start;
|
|
152
|
+
}
|
|
153
|
+
if (inClass) {
|
|
154
|
+
inClass = character !== "]";
|
|
155
|
+
index += 1;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (character === "[") {
|
|
159
|
+
inClass = true;
|
|
160
|
+
index += 1;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (character === "/") {
|
|
164
|
+
return index + 1;
|
|
165
|
+
}
|
|
166
|
+
index += 1;
|
|
167
|
+
}
|
|
168
|
+
return start;
|
|
169
|
+
}
|
|
170
|
+
function currentMode(state) {
|
|
171
|
+
return state.modes[state.modes.length - 1] ?? "code";
|
|
172
|
+
}
|
|
173
|
+
function stepComment(state, source) {
|
|
174
|
+
const character = source[state.index] ?? "";
|
|
175
|
+
const next = source[state.index + 1] ?? "";
|
|
176
|
+
if (character !== "/" || next !== "/" && next !== "*") {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
if (next === "/") {
|
|
180
|
+
const newline = source.indexOf(`
|
|
181
|
+
`, state.index);
|
|
182
|
+
const stop2 = newline === -1 ? source.length : newline;
|
|
183
|
+
state.comments.push({
|
|
184
|
+
end: stop2,
|
|
185
|
+
kind: "line",
|
|
186
|
+
start: state.index,
|
|
187
|
+
text: source.slice(state.index + 2, stop2)
|
|
188
|
+
});
|
|
189
|
+
state.index = stop2;
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
const close = source.indexOf("*/", state.index + 2);
|
|
193
|
+
const stop = close === -1 ? source.length : close + 2;
|
|
194
|
+
state.comments.push({
|
|
195
|
+
end: stop,
|
|
196
|
+
kind: "block",
|
|
197
|
+
start: state.index,
|
|
198
|
+
text: source.slice(state.index + 2, Math.max(state.index + 2, stop - 2))
|
|
199
|
+
});
|
|
200
|
+
state.index = stop;
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
function stepString(state, source) {
|
|
204
|
+
const character = source[state.index] ?? "";
|
|
205
|
+
if (character !== "'" && character !== '"') {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
state.index = skipString(source, state.index, character);
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
function stepTemplateStart(state, source) {
|
|
212
|
+
if (source[state.index] !== "`") {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
state.modes.push("template");
|
|
216
|
+
state.index += 1;
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
function stepRegex(state, source) {
|
|
220
|
+
if (source[state.index] !== "/" || !isRegexStart(source, state.index)) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
const stop = skipRegex(source, state.index);
|
|
224
|
+
if (stop <= state.index) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
state.index = stop;
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
function stepTemplate(state, source) {
|
|
231
|
+
const character = source[state.index] ?? "";
|
|
232
|
+
if (character === "\\") {
|
|
233
|
+
state.index += 2;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (character === "`") {
|
|
237
|
+
state.modes.pop();
|
|
238
|
+
state.index += 1;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (character === "$" && source[state.index + 1] === "{") {
|
|
242
|
+
state.modes.push("expression");
|
|
243
|
+
state.braces.push(1);
|
|
244
|
+
state.index += 2;
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
state.index += 1;
|
|
248
|
+
}
|
|
249
|
+
function stepExpression(state, source) {
|
|
250
|
+
const character = source[state.index] ?? "";
|
|
251
|
+
const depth = state.braces[state.braces.length - 1] ?? 0;
|
|
252
|
+
if (character === "{") {
|
|
253
|
+
state.braces[state.braces.length - 1] = depth + 1;
|
|
254
|
+
} else if (character === "}") {
|
|
255
|
+
state.braces[state.braces.length - 1] = depth - 1;
|
|
256
|
+
if (depth - 1 === 0) {
|
|
257
|
+
state.modes.pop();
|
|
258
|
+
state.braces.pop();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
state.index += 1;
|
|
262
|
+
}
|
|
263
|
+
function step(state, source) {
|
|
264
|
+
if (currentMode(state) === "template") {
|
|
265
|
+
stepTemplate(state, source);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (stepComment(state, source) || stepString(state, source) || stepTemplateStart(state, source) || stepRegex(state, source)) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
stepExpression(state, source);
|
|
272
|
+
}
|
|
273
|
+
function extractComments(source) {
|
|
274
|
+
const shebang = source.startsWith("#!") ? source.indexOf(`
|
|
275
|
+
`) : 0;
|
|
276
|
+
const state = {
|
|
277
|
+
braces: [],
|
|
278
|
+
comments: [],
|
|
279
|
+
index: Math.max(0, shebang),
|
|
280
|
+
modes: ["code"]
|
|
281
|
+
};
|
|
282
|
+
while (state.index < source.length) {
|
|
283
|
+
step(state, source);
|
|
284
|
+
}
|
|
285
|
+
return state.comments;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/rules.ts
|
|
289
|
+
var OPT_IN_RULE_IDS = ["japanese-period"];
|
|
290
|
+
var PLACEHOLDER_PATTERN = /\b(TODO|FIXME|XXX|HACK)\b/;
|
|
291
|
+
var SEPARATOR_PATTERN = /^[-=*_#~+./\\|]{4,}$/;
|
|
292
|
+
var DIRECTIVE_PATTERN = /^@ts-(?:ignore|expect-error)\b([\s\S]*)$/;
|
|
293
|
+
var JAPANESE_PERIOD = "\u3002";
|
|
294
|
+
function parseEnabledRules(values) {
|
|
295
|
+
const enabled = [];
|
|
296
|
+
for (const value of values) {
|
|
297
|
+
if (!OPT_IN_RULE_IDS.includes(value)) {
|
|
298
|
+
throw new Error(`unknown rule: ${value}`);
|
|
299
|
+
}
|
|
300
|
+
const rule = value;
|
|
301
|
+
if (!enabled.includes(rule)) {
|
|
302
|
+
enabled.push(rule);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return enabled;
|
|
306
|
+
}
|
|
307
|
+
function findJapanesePeriod(text) {
|
|
308
|
+
return text.indexOf(JAPANESE_PERIOD);
|
|
309
|
+
}
|
|
310
|
+
function normalizeComment(body) {
|
|
311
|
+
return body.split(`
|
|
312
|
+
`).map((line) => line.replace(/^\s*\*+\s?/, "")).join(" ").replace(/\s+/g, " ").trim();
|
|
313
|
+
}
|
|
314
|
+
function classifyComment(body) {
|
|
315
|
+
const text = normalizeComment(body);
|
|
316
|
+
if (text === "") {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
if (/^(?:biome-ignore-all|@ts-nocheck)\b/.test(text) || /^eslint-disable(?:-next-line|-line)?\s*$/.test(text)) {
|
|
320
|
+
return "broad-suppression";
|
|
321
|
+
}
|
|
322
|
+
const directive = DIRECTIVE_PATTERN.exec(text);
|
|
323
|
+
if (directive) {
|
|
324
|
+
return (directive[1] ?? "").replace(/^[\s:\u2014-]+/, "") === "" ? "undocumented-directive" : null;
|
|
325
|
+
}
|
|
326
|
+
if (PLACEHOLDER_PATTERN.test(text)) {
|
|
327
|
+
return "placeholder-comment";
|
|
328
|
+
}
|
|
329
|
+
if (SEPARATOR_PATTERN.test(text)) {
|
|
330
|
+
return "separator-comment";
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/scan.ts
|
|
336
|
+
var SUPPORTED_EXTENSIONS = new Set([
|
|
337
|
+
".cjs",
|
|
338
|
+
".cts",
|
|
339
|
+
".js",
|
|
340
|
+
".jsx",
|
|
341
|
+
".mjs",
|
|
342
|
+
".mts",
|
|
343
|
+
".ts",
|
|
344
|
+
".tsx"
|
|
345
|
+
]);
|
|
346
|
+
function positionAt(source, offset) {
|
|
347
|
+
let line = 1;
|
|
348
|
+
let lineStart = 0;
|
|
349
|
+
for (let index = 0;index < offset; index += 1) {
|
|
350
|
+
if (source[index] === `
|
|
351
|
+
`) {
|
|
352
|
+
line += 1;
|
|
353
|
+
lineStart = index + 1;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return { line, column: offset - lineStart + 1 };
|
|
357
|
+
}
|
|
358
|
+
function scanSource(source, file, enabled = []) {
|
|
359
|
+
const findings = [];
|
|
360
|
+
for (const comment of extractComments(source)) {
|
|
361
|
+
const text = normalizeComment(comment.text);
|
|
362
|
+
const rule = classifyComment(comment.text);
|
|
363
|
+
if (rule !== null) {
|
|
364
|
+
const position2 = positionAt(source, comment.start);
|
|
365
|
+
findings.push({
|
|
366
|
+
column: position2.column,
|
|
367
|
+
file,
|
|
368
|
+
line: position2.line,
|
|
369
|
+
rule,
|
|
370
|
+
text
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
if (!enabled.includes("japanese-period")) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
const offset = findJapanesePeriod(comment.text);
|
|
377
|
+
if (offset < 0) {
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
const position = positionAt(source, comment.start + 2 + offset);
|
|
381
|
+
findings.push({
|
|
382
|
+
column: position.column,
|
|
383
|
+
file,
|
|
384
|
+
line: position.line,
|
|
385
|
+
rule: "japanese-period",
|
|
386
|
+
text
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
return findings;
|
|
390
|
+
}
|
|
391
|
+
function scanFile(file, cwd = process.cwd(), enabled = []) {
|
|
392
|
+
return scanSource(readFileSync(file, "utf8"), normalizePath(relative2(cwd, file)), enabled);
|
|
393
|
+
}
|
|
394
|
+
function scanFiles(files, cwd = process.cwd(), enabled = []) {
|
|
395
|
+
const findings = [];
|
|
396
|
+
for (const file of files) {
|
|
397
|
+
findings.push(...scanFile(file, cwd, enabled));
|
|
398
|
+
}
|
|
399
|
+
return findings.sort(compareFindings);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ../shared/src/baseline.ts
|
|
403
|
+
import { existsSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
404
|
+
|
|
405
|
+
// ../shared/src/json.ts
|
|
406
|
+
function isJsonObject(value) {
|
|
407
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ../shared/src/baseline.ts
|
|
411
|
+
function entryKey(entry) {
|
|
412
|
+
return [entry.engine ?? "", entry.rule, entry.file, entry.text].join("\x00");
|
|
413
|
+
}
|
|
414
|
+
function compareEntries(left, right) {
|
|
415
|
+
const leftKey = entryKey(left);
|
|
416
|
+
const rightKey = entryKey(right);
|
|
417
|
+
if (leftKey === rightKey) {
|
|
418
|
+
return 0;
|
|
419
|
+
}
|
|
420
|
+
return leftKey < rightKey ? -1 : 1;
|
|
421
|
+
}
|
|
422
|
+
function toEntry(finding) {
|
|
423
|
+
const entry = {
|
|
424
|
+
count: 1,
|
|
425
|
+
file: finding.file,
|
|
426
|
+
rule: finding.rule,
|
|
427
|
+
text: finding.text
|
|
428
|
+
};
|
|
429
|
+
if (finding.engine !== undefined) {
|
|
430
|
+
entry.engine = finding.engine;
|
|
431
|
+
}
|
|
432
|
+
return entry;
|
|
433
|
+
}
|
|
434
|
+
function createBaseline(findings) {
|
|
435
|
+
const entries = new Map;
|
|
436
|
+
for (const finding of findings) {
|
|
437
|
+
const key = entryKey(finding);
|
|
438
|
+
const existing = entries.get(key);
|
|
439
|
+
if (existing) {
|
|
440
|
+
existing.count += 1;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
entries.set(key, toEntry(finding));
|
|
444
|
+
}
|
|
445
|
+
return { entries: [...entries.values()].sort(compareEntries), version: 1 };
|
|
446
|
+
}
|
|
447
|
+
function compareWithBaseline(findings, baseline) {
|
|
448
|
+
const remaining = new Map;
|
|
449
|
+
for (const entry of baseline.entries) {
|
|
450
|
+
const key = entryKey(entry);
|
|
451
|
+
remaining.set(key, (remaining.get(key) ?? 0) + entry.count);
|
|
452
|
+
}
|
|
453
|
+
const added = [];
|
|
454
|
+
for (const finding of findings) {
|
|
455
|
+
const key = entryKey(finding);
|
|
456
|
+
const count = remaining.get(key) ?? 0;
|
|
457
|
+
if (count > 0) {
|
|
458
|
+
remaining.set(key, count - 1);
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
added.push(finding);
|
|
462
|
+
}
|
|
463
|
+
const resolved = baseline.entries.filter((entry) => (remaining.get(entryKey(entry)) ?? 0) > 0);
|
|
464
|
+
return { added, resolved };
|
|
465
|
+
}
|
|
466
|
+
function isBaselineEntry(value) {
|
|
467
|
+
if (!isJsonObject(value)) {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
return (value.engine === undefined || typeof value.engine === "string") && typeof value.file === "string" && typeof value.rule === "string" && typeof value.text === "string" && typeof value.count === "number";
|
|
471
|
+
}
|
|
472
|
+
function readBaseline(path, label) {
|
|
473
|
+
if (!existsSync(path)) {
|
|
474
|
+
return { entries: [], version: 1 };
|
|
475
|
+
}
|
|
476
|
+
const value = JSON.parse(readFileSync2(path, "utf8"));
|
|
477
|
+
if (!isJsonObject(value) || !Array.isArray(value.entries) || !value.entries.every(isBaselineEntry)) {
|
|
478
|
+
throw new Error(`${path} is not a ${label} baseline`);
|
|
479
|
+
}
|
|
480
|
+
return { entries: value.entries, version: 1 };
|
|
481
|
+
}
|
|
482
|
+
function writeBaseline(path, baseline) {
|
|
483
|
+
writeFileSync(path, `${JSON.stringify(baseline, null, "\t")}
|
|
484
|
+
`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ../shared/src/cli.ts
|
|
488
|
+
function addValue(values, name, value) {
|
|
489
|
+
const list = values.get(name);
|
|
490
|
+
if (list === undefined) {
|
|
491
|
+
values.set(name, [value]);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
list.push(value);
|
|
495
|
+
}
|
|
496
|
+
function addTarget(targets, argument) {
|
|
497
|
+
if (argument.startsWith("-")) {
|
|
498
|
+
throw new Error(`unknown option: ${argument}`);
|
|
499
|
+
}
|
|
500
|
+
targets.push(argument);
|
|
501
|
+
}
|
|
502
|
+
function optionName(argument) {
|
|
503
|
+
const separator = argument.indexOf("=");
|
|
504
|
+
return separator === -1 ? argument.slice(2) : argument.slice(2, separator);
|
|
505
|
+
}
|
|
506
|
+
function inlineValue(argument) {
|
|
507
|
+
const separator = argument.indexOf("=");
|
|
508
|
+
return separator === -1 ? undefined : argument.slice(separator + 1);
|
|
509
|
+
}
|
|
510
|
+
function consumeValue(values, argv, index, name) {
|
|
511
|
+
const value = argv[index + 1];
|
|
512
|
+
if (value === undefined) {
|
|
513
|
+
return 0;
|
|
514
|
+
}
|
|
515
|
+
addValue(values, name, value);
|
|
516
|
+
return 1;
|
|
517
|
+
}
|
|
518
|
+
function parseArgv(argv, spec) {
|
|
519
|
+
const flags = new Set;
|
|
520
|
+
const values = new Map;
|
|
521
|
+
const valueNames = new Set(spec.values ?? []);
|
|
522
|
+
const flagNames = new Set(spec.flags ?? []);
|
|
523
|
+
const targets = [];
|
|
524
|
+
for (let index = 0;index < argv.length; index += 1) {
|
|
525
|
+
const argument = argv[index] ?? "";
|
|
526
|
+
if (!argument.startsWith("--")) {
|
|
527
|
+
addTarget(targets, argument);
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const name = optionName(argument);
|
|
531
|
+
if (valueNames.has(name)) {
|
|
532
|
+
const inline = inlineValue(argument);
|
|
533
|
+
if (inline !== undefined) {
|
|
534
|
+
addValue(values, name, inline);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
index += consumeValue(values, argv, index, name);
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (flagNames.has(name) && argument.indexOf("=") === -1) {
|
|
541
|
+
flags.add(name);
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
throw new Error(`unknown option: ${argument}`);
|
|
545
|
+
}
|
|
546
|
+
return { flags, targets, values };
|
|
547
|
+
}
|
|
548
|
+
function wantsHelp(argv) {
|
|
549
|
+
return argv.includes("--help") || argv.includes("-h");
|
|
550
|
+
}
|
|
551
|
+
function runCli(main) {
|
|
552
|
+
async function execute() {
|
|
553
|
+
try {
|
|
554
|
+
process.exit(await main(process.argv.slice(2)));
|
|
555
|
+
} catch (error) {
|
|
556
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
557
|
+
process.exit(2);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
execute();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/cli.ts
|
|
564
|
+
var DEFAULT_BASELINE = "comment-baseline.json";
|
|
565
|
+
var RULE_MESSAGES = {
|
|
566
|
+
"broad-suppression": "file-wide suppression hides too much",
|
|
567
|
+
"japanese-period": "a Japanese sentence in a comment does not end with a period",
|
|
568
|
+
"placeholder-comment": "placeholder comment should be resolved or tracked",
|
|
569
|
+
"separator-comment": "decorative separator comment adds no information",
|
|
570
|
+
"undocumented-directive": "TypeScript directive needs a description"
|
|
571
|
+
};
|
|
572
|
+
var USAGE = "Usage: comment-check [--baseline <path>] [--enable <rule>] [--ignore <path>] [--update-baseline] [--json] [path...]";
|
|
573
|
+
function parseArguments(argv) {
|
|
574
|
+
const parsed = parseArgv(argv, {
|
|
575
|
+
flags: ["json", "update-baseline"],
|
|
576
|
+
values: ["baseline", "enable", "ignore"]
|
|
577
|
+
});
|
|
578
|
+
return {
|
|
579
|
+
baselinePath: parsed.values.get("baseline")?.at(-1) ?? DEFAULT_BASELINE,
|
|
580
|
+
enabled: parseEnabledRules(parsed.values.get("enable") ?? []),
|
|
581
|
+
ignores: (parsed.values.get("ignore") ?? []).map(normalizePath),
|
|
582
|
+
json: parsed.flags.has("json"),
|
|
583
|
+
targets: parsed.targets.length > 0 ? parsed.targets : ["."],
|
|
584
|
+
update: parsed.flags.has("update-baseline")
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function describeFinding(finding) {
|
|
588
|
+
const message = RULE_MESSAGES[finding.rule] ?? "";
|
|
589
|
+
return `${formatLocation(finding)} ${finding.rule} ${message}`.trimEnd();
|
|
590
|
+
}
|
|
591
|
+
function main(argv) {
|
|
592
|
+
if (wantsHelp(argv)) {
|
|
593
|
+
console.log(USAGE);
|
|
594
|
+
return 0;
|
|
595
|
+
}
|
|
596
|
+
const options = parseArguments(argv);
|
|
597
|
+
const files = collectFiles(options.targets, {
|
|
598
|
+
cwd: process.cwd(),
|
|
599
|
+
extensions: SUPPORTED_EXTENSIONS,
|
|
600
|
+
ignores: options.ignores
|
|
601
|
+
});
|
|
602
|
+
const findings = scanFiles(files, process.cwd(), options.enabled);
|
|
603
|
+
if (options.update) {
|
|
604
|
+
const baseline2 = createBaseline(findings);
|
|
605
|
+
writeBaseline(options.baselinePath, baseline2);
|
|
606
|
+
console.log(`Recorded ${baseline2.entries.length} entries in ${options.baselinePath}`);
|
|
607
|
+
return 0;
|
|
608
|
+
}
|
|
609
|
+
const baseline = readBaseline(options.baselinePath, "comment");
|
|
610
|
+
const comparison = compareWithBaseline(findings, baseline);
|
|
611
|
+
if (options.json) {
|
|
612
|
+
console.log(JSON.stringify({ added: comparison.added, resolved: comparison.resolved }, null, "\t"));
|
|
613
|
+
} else {
|
|
614
|
+
for (const finding of comparison.added) {
|
|
615
|
+
console.log(describeFinding(finding));
|
|
616
|
+
}
|
|
617
|
+
console.log(`Checked ${files.length} files: ${comparison.added.length} new, ${comparison.resolved.length} resolved, ${baseline.entries.length} baselined`);
|
|
618
|
+
}
|
|
619
|
+
return comparison.added.length > 0 ? 1 : 0;
|
|
620
|
+
}
|
|
621
|
+
if (import.meta.main) {
|
|
622
|
+
runCli(main);
|
|
623
|
+
}
|
|
624
|
+
export {
|
|
625
|
+
parseArguments,
|
|
626
|
+
main
|
|
627
|
+
};
|