@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/src/comments.ts DELETED
@@ -1,244 +0,0 @@
1
- /** 抽出するcommentの種別 */
2
- export type CommentKind = "line" | "block";
3
-
4
- /** 抽出したcomment1件の種別と原文上の位置 */
5
- export interface CommentPiece {
6
- kind: CommentKind;
7
- text: string;
8
- start: number;
9
- end: number;
10
- }
11
-
12
- type Mode = "code" | "template" | "expression";
13
-
14
- interface ScanState {
15
- braces: number[];
16
- comments: CommentPiece[];
17
- index: number;
18
- modes: Mode[];
19
- }
20
-
21
- const REGEX_PRECEDING_CHARACTERS = new Set("(,=:[!&|?{};".split(""));
22
- const REGEX_PRECEDING_KEYWORDS = new Set([
23
- "await",
24
- "case",
25
- "delete",
26
- "do",
27
- "else",
28
- "in",
29
- "instanceof",
30
- "new",
31
- "of",
32
- "return",
33
- "throw",
34
- "typeof",
35
- "void",
36
- "yield",
37
- ]);
38
-
39
- function isIdentifierCharacter(character: string): boolean {
40
- return /[A-Za-z0-9_$]/.test(character);
41
- }
42
-
43
- function skipString(source: string, start: number, quote: string): number {
44
- let index = start + 1;
45
- while (index < source.length) {
46
- const character = source[index] ?? "";
47
- if (character === "\\") {
48
- index += 2;
49
- continue;
50
- }
51
- if (character === quote) {
52
- return index + 1;
53
- }
54
- if (character === "\n") {
55
- return index;
56
- }
57
- index += 1;
58
- }
59
- return index;
60
- }
61
-
62
- function isRegexStart(source: string, index: number): boolean {
63
- let previous = index - 1;
64
- while (previous >= 0 && /\s/.test(source[previous] ?? "")) {
65
- previous -= 1;
66
- }
67
- if (previous < 0) {
68
- return true;
69
- }
70
- const character = source[previous] ?? "";
71
- if (REGEX_PRECEDING_CHARACTERS.has(character)) {
72
- return true;
73
- }
74
- if (!isIdentifierCharacter(character)) {
75
- return false;
76
- }
77
- let wordStart = previous;
78
- while (wordStart >= 0 && isIdentifierCharacter(source[wordStart] ?? "")) {
79
- wordStart -= 1;
80
- }
81
- return REGEX_PRECEDING_KEYWORDS.has(
82
- source.slice(wordStart + 1, previous + 1),
83
- );
84
- }
85
-
86
- function skipRegex(source: string, start: number): number {
87
- let index = start + 1;
88
- let inClass = false;
89
- while (index < source.length) {
90
- const character = source[index] ?? "";
91
- if (character === "\\") {
92
- index += 2;
93
- continue;
94
- }
95
- if (character === "\n") {
96
- return start;
97
- }
98
- if (inClass) {
99
- inClass = character !== "]";
100
- index += 1;
101
- continue;
102
- }
103
- if (character === "[") {
104
- inClass = true;
105
- index += 1;
106
- continue;
107
- }
108
- if (character === "/") {
109
- return index + 1;
110
- }
111
- index += 1;
112
- }
113
- return start;
114
- }
115
-
116
- function currentMode(state: ScanState): Mode {
117
- return state.modes[state.modes.length - 1] ?? "code";
118
- }
119
-
120
- function stepComment(state: ScanState, source: string): boolean {
121
- const character = source[state.index] ?? "";
122
- const next = source[state.index + 1] ?? "";
123
- if (character !== "/" || (next !== "/" && next !== "*")) {
124
- return false;
125
- }
126
- if (next === "/") {
127
- const newline = source.indexOf("\n", state.index);
128
- const stop = newline === -1 ? source.length : newline;
129
- state.comments.push({
130
- end: stop,
131
- kind: "line",
132
- start: state.index,
133
- text: source.slice(state.index + 2, stop),
134
- });
135
- state.index = stop;
136
- return true;
137
- }
138
- const close = source.indexOf("*/", state.index + 2);
139
- const stop = close === -1 ? source.length : close + 2;
140
- state.comments.push({
141
- end: stop,
142
- kind: "block",
143
- start: state.index,
144
- text: source.slice(state.index + 2, Math.max(state.index + 2, stop - 2)),
145
- });
146
- state.index = stop;
147
- return true;
148
- }
149
-
150
- function stepString(state: ScanState, source: string): boolean {
151
- const character = source[state.index] ?? "";
152
- if (character !== "'" && character !== '"') {
153
- return false;
154
- }
155
- state.index = skipString(source, state.index, character);
156
- return true;
157
- }
158
-
159
- function stepTemplateStart(state: ScanState, source: string): boolean {
160
- if (source[state.index] !== "`") {
161
- return false;
162
- }
163
- state.modes.push("template");
164
- state.index += 1;
165
- return true;
166
- }
167
-
168
- function stepRegex(state: ScanState, source: string): boolean {
169
- if (source[state.index] !== "/" || !isRegexStart(source, state.index)) {
170
- return false;
171
- }
172
- const stop = skipRegex(source, state.index);
173
- if (stop <= state.index) {
174
- return false;
175
- }
176
- state.index = stop;
177
- return true;
178
- }
179
-
180
- function stepTemplate(state: ScanState, source: string): void {
181
- const character = source[state.index] ?? "";
182
- if (character === "\\") {
183
- state.index += 2;
184
- return;
185
- }
186
- if (character === "`") {
187
- state.modes.pop();
188
- state.index += 1;
189
- return;
190
- }
191
- if (character === "$" && source[state.index + 1] === "{") {
192
- state.modes.push("expression");
193
- state.braces.push(1);
194
- state.index += 2;
195
- return;
196
- }
197
- state.index += 1;
198
- }
199
-
200
- function stepExpression(state: ScanState, source: string): void {
201
- const character = source[state.index] ?? "";
202
- const depth = state.braces[state.braces.length - 1] ?? 0;
203
- if (character === "{") {
204
- state.braces[state.braces.length - 1] = depth + 1;
205
- } else if (character === "}") {
206
- state.braces[state.braces.length - 1] = depth - 1;
207
- if (depth - 1 === 0) {
208
- state.modes.pop();
209
- state.braces.pop();
210
- }
211
- }
212
- state.index += 1;
213
- }
214
-
215
- function step(state: ScanState, source: string): void {
216
- if (currentMode(state) === "template") {
217
- stepTemplate(state, source);
218
- return;
219
- }
220
- if (
221
- stepComment(state, source) ||
222
- stepString(state, source) ||
223
- stepTemplateStart(state, source) ||
224
- stepRegex(state, source)
225
- ) {
226
- return;
227
- }
228
- stepExpression(state, source);
229
- }
230
-
231
- /** 文字列や正規表現リテラルを除外してsourceからcommentを抽出する */
232
- export function extractComments(source: string): CommentPiece[] {
233
- const shebang = source.startsWith("#!") ? source.indexOf("\n") : 0;
234
- const state: ScanState = {
235
- braces: [],
236
- comments: [],
237
- index: Math.max(0, shebang),
238
- modes: ["code"],
239
- };
240
- while (state.index < source.length) {
241
- step(state, source);
242
- }
243
- return state.comments;
244
- }
package/src/rules.ts DELETED
@@ -1,60 +0,0 @@
1
- /** comment-checkが報告するruleの識別子一覧 */
2
- export const RULE_IDS = [
3
- "broad-suppression",
4
- "undocumented-directive",
5
- "placeholder-comment",
6
- "separator-comment",
7
- ] as const;
8
-
9
- /** RULE_IDSが定義するrule識別子のunion型 */
10
- export type RuleId = (typeof RULE_IDS)[number];
11
-
12
- /** 検出したcomment違反1件の内容と位置 */
13
- export interface Finding {
14
- rule: RuleId;
15
- file: string;
16
- line: number;
17
- column: number;
18
- text: string;
19
- }
20
-
21
- const PLACEHOLDER_PATTERN = /\b(TODO|FIXME|XXX|HACK)\b/;
22
- const SEPARATOR_PATTERN = /^[-=*_#~+./\\|]{4,}$/;
23
- const DIRECTIVE_PATTERN = /^@ts-(?:ignore|expect-error)\b([\s\S]*)$/;
24
-
25
- /** block commentの記号を除いて空白を揃えた本文を返す */
26
- export function normalizeComment(body: string): string {
27
- return body
28
- .split("\n")
29
- .map((line) => line.replace(/^\s*\*+\s?/, ""))
30
- .join(" ")
31
- .replace(/\s+/g, " ")
32
- .trim();
33
- }
34
-
35
- /** comment本文を分類し、該当するruleがなければnullを返す */
36
- export function classifyComment(body: string): RuleId | null {
37
- const text = normalizeComment(body);
38
- if (text === "") {
39
- return null;
40
- }
41
- if (
42
- /^(?:biome-ignore-all|@ts-nocheck)\b/.test(text) ||
43
- /^eslint-disable(?:-next-line|-line)?\s*$/.test(text)
44
- ) {
45
- return "broad-suppression";
46
- }
47
- const directive = DIRECTIVE_PATTERN.exec(text);
48
- if (directive) {
49
- return (directive[1] ?? "").replace(/^[\s:—-]+/, "") === ""
50
- ? "undocumented-directive"
51
- : null;
52
- }
53
- if (PLACEHOLDER_PATTERN.test(text)) {
54
- return "placeholder-comment";
55
- }
56
- if (SEPARATOR_PATTERN.test(text)) {
57
- return "separator-comment";
58
- }
59
- return null;
60
- }
package/src/scan.ts DELETED
@@ -1,145 +0,0 @@
1
- import { readdirSync, readFileSync, statSync } from "node:fs";
2
- import { extname, join, relative, resolve } from "node:path";
3
- import { extractComments } from "./comments";
4
- import { classifyComment, type Finding, normalizeComment } from "./rules";
5
-
6
- const SUPPORTED_EXTENSIONS = new Set([
7
- ".cjs",
8
- ".cts",
9
- ".js",
10
- ".jsx",
11
- ".mjs",
12
- ".mts",
13
- ".ts",
14
- ".tsx",
15
- ]);
16
- const IGNORED_DIRECTORIES = new Set([
17
- "build",
18
- "coverage",
19
- "dist",
20
- "node_modules",
21
- "out",
22
- "vendor",
23
- ]);
24
-
25
- /** 区切り文字を統一し先頭の./と末尾の/を除いたpathを返す */
26
- export function normalizePath(path: string): string {
27
- return path.split("\\").join("/").replace(/^\.\//, "").replace(/\/+$/, "");
28
- }
29
-
30
- function isIgnored(path: string, ignores: string[]): boolean {
31
- return ignores.some(
32
- (ignore) => path === ignore || path.startsWith(`${ignore}/`),
33
- );
34
- }
35
-
36
- function positionAt(
37
- source: string,
38
- offset: number,
39
- ): { line: number; column: number } {
40
- let line = 1;
41
- let lineStart = 0;
42
- for (let index = 0; index < offset; index += 1) {
43
- if (source[index] === "\n") {
44
- line += 1;
45
- lineStart = index + 1;
46
- }
47
- }
48
- return { line, column: offset - lineStart + 1 };
49
- }
50
-
51
- function compareFindings(left: Finding, right: Finding): number {
52
- if (left.file !== right.file) {
53
- return left.file < right.file ? -1 : 1;
54
- }
55
- if (left.line !== right.line) {
56
- return left.line - right.line;
57
- }
58
- return left.column - right.column;
59
- }
60
-
61
- function walk(directory: string, files: Set<string>): void {
62
- for (const entry of readdirSync(directory, { withFileTypes: true })) {
63
- if (entry.name.startsWith(".")) {
64
- continue;
65
- }
66
- const path = join(directory, entry.name);
67
- if (entry.isDirectory()) {
68
- if (!IGNORED_DIRECTORIES.has(entry.name)) {
69
- walk(path, files);
70
- }
71
- continue;
72
- }
73
- if (entry.isFile() && SUPPORTED_EXTENSIONS.has(extname(entry.name))) {
74
- files.add(path);
75
- }
76
- }
77
- }
78
-
79
- /** 対象pathを走査して検査対象のfile一覧を集める */
80
- export function collectFiles(
81
- targets: string[],
82
- cwd = process.cwd(),
83
- ignores: string[] = [],
84
- ): string[] {
85
- const files = new Set<string>();
86
- for (const target of targets) {
87
- const absolute = resolve(cwd, target);
88
- let stats: ReturnType<typeof statSync>;
89
- try {
90
- stats = statSync(absolute);
91
- } catch {
92
- continue;
93
- }
94
- if (stats.isFile()) {
95
- if (
96
- SUPPORTED_EXTENSIONS.has(extname(absolute)) &&
97
- !isIgnored(normalizePath(target), ignores)
98
- ) {
99
- files.add(absolute);
100
- }
101
- continue;
102
- }
103
- walk(absolute, files);
104
- }
105
- return [...files]
106
- .filter((file) => !isIgnored(normalizePath(relative(cwd, file)), ignores))
107
- .sort();
108
- }
109
-
110
- /** source文字列を走査してcomment違反を検出する */
111
- export function scanSource(source: string, file: string): Finding[] {
112
- const findings: Finding[] = [];
113
- for (const comment of extractComments(source)) {
114
- const rule = classifyComment(comment.text);
115
- if (rule === null) {
116
- continue;
117
- }
118
- const position = positionAt(source, comment.start);
119
- findings.push({
120
- column: position.column,
121
- file,
122
- line: position.line,
123
- rule,
124
- text: normalizeComment(comment.text),
125
- });
126
- }
127
- return findings;
128
- }
129
-
130
- /** fileを読み込んでcomment違反を検出する */
131
- export function scanFile(file: string, cwd = process.cwd()): Finding[] {
132
- return scanSource(
133
- readFileSync(file, "utf8"),
134
- normalizePath(relative(cwd, file)),
135
- );
136
- }
137
-
138
- /** 複数fileの違反をまとめて位置順に並べる */
139
- export function scanFiles(files: string[], cwd = process.cwd()): Finding[] {
140
- const findings: Finding[] = [];
141
- for (const file of files) {
142
- findings.push(...scanFile(file, cwd));
143
- }
144
- return findings.sort(compareFindings);
145
- }