@yuu1111/comment-check 0.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.md ADDED
@@ -0,0 +1,52 @@
1
+ # @yuu1111/comment-check
2
+
3
+ Small comment checker with a baseline, used to keep suppressions and placeholder
4
+ comments from spreading.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ bun add -D @yuu1111/comment-check
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ Record the current findings once, then fail only when a new one appears:
15
+
16
+ ```bash
17
+ comment-check --update-baseline .
18
+ comment-check .
19
+ ```
20
+
21
+ ```
22
+ src/queue.ts:18:2 undocumented-directive TypeScript directive needs a description
23
+ Checked 42 files: 1 new, 0 resolved, 3 baselined
24
+ ```
25
+
26
+ ## Rules
27
+
28
+ | Rule | Detects |
29
+ |------|---------|
30
+ | `broad-suppression` | `biome-ignore-all`, `@ts-nocheck`, and rule-less `eslint-disable` |
31
+ | `undocumented-directive` | `@ts-ignore` or `@ts-expect-error` without a description |
32
+ | `placeholder-comment` | `TODO`, `FIXME`, `XXX`, `HACK` |
33
+ | `separator-comment` | decorative comments made only of punctuation |
34
+
35
+ ## Options
36
+
37
+ | Option | Description |
38
+ |--------|-------------|
39
+ | `--baseline <path>` | Baseline file to read or write (default `comment-baseline.json`) |
40
+ | `--ignore <path>` | Path to leave out, repeatable |
41
+ | `--update-baseline` | Replace the baseline with the current findings |
42
+ | `--json` | Print new and resolved findings as JSON |
43
+
44
+ ## Notes
45
+
46
+ Generated directories and other paths that the project owns stay out of the
47
+ check through `--ignore`, for example `--ignore src/generated`.
48
+
49
+ The baseline keys on the rule, the file, and the comment text, so moving a line
50
+ does not report the comment as new. Biome already requires a reason on
51
+ `biome-ignore` and reports unused suppressions, so this checker only covers the
52
+ comment trivia that Biome does not read.
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@yuu1111/comment-check",
3
+ "version": "0.0.0",
4
+ "description": "Shared comment and suppression checker",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/yuu1111/configs.git",
9
+ "directory": "packages/comment-check"
10
+ },
11
+ "type": "module",
12
+ "bin": {
13
+ "comment-check": "src/cli.ts"
14
+ },
15
+ "exports": {
16
+ ".": "./src/scan.ts"
17
+ },
18
+ "files": [
19
+ "src"
20
+ ],
21
+ "keywords": [
22
+ "comment",
23
+ "lint",
24
+ "suppression"
25
+ ]
26
+ }
@@ -0,0 +1,81 @@
1
+ import type { Finding } from "./rules";
2
+
3
+ export interface BaselineEntry {
4
+ rule: string;
5
+ file: string;
6
+ text: string;
7
+ count: number;
8
+ }
9
+
10
+ export interface BaselineFile {
11
+ version: 1;
12
+ entries: BaselineEntry[];
13
+ }
14
+
15
+ export interface BaselineComparison {
16
+ added: Finding[];
17
+ resolved: BaselineEntry[];
18
+ }
19
+
20
+ export function entryKey(entry: {
21
+ rule: string;
22
+ file: string;
23
+ text: string;
24
+ }): string {
25
+ return [entry.rule, entry.file, entry.text].join("\u0000");
26
+ }
27
+
28
+ function compareEntries(left: BaselineEntry, right: BaselineEntry): number {
29
+ const leftKey = entryKey(left);
30
+ const rightKey = entryKey(right);
31
+ if (leftKey === rightKey) {
32
+ return 0;
33
+ }
34
+ return leftKey < rightKey ? -1 : 1;
35
+ }
36
+
37
+ export function createBaseline(findings: Finding[]): BaselineFile {
38
+ const entries = new Map<string, BaselineEntry>();
39
+ for (const finding of findings) {
40
+ const key = entryKey(finding);
41
+ const existing = entries.get(key);
42
+ if (existing) {
43
+ existing.count += 1;
44
+ continue;
45
+ }
46
+ entries.set(key, {
47
+ rule: finding.rule,
48
+ file: finding.file,
49
+ text: finding.text,
50
+ count: 1,
51
+ });
52
+ }
53
+ return { version: 1, entries: [...entries.values()].sort(compareEntries) };
54
+ }
55
+
56
+ export function compareWithBaseline(
57
+ findings: Finding[],
58
+ baseline: BaselineFile,
59
+ ): BaselineComparison {
60
+ const remaining = new Map<string, number>();
61
+ for (const entry of baseline.entries) {
62
+ const key = entryKey(entry);
63
+ remaining.set(key, (remaining.get(key) ?? 0) + entry.count);
64
+ }
65
+
66
+ const added: Finding[] = [];
67
+ for (const finding of findings) {
68
+ const key = entryKey(finding);
69
+ const count = remaining.get(key) ?? 0;
70
+ if (count > 0) {
71
+ remaining.set(key, count - 1);
72
+ continue;
73
+ }
74
+ added.push(finding);
75
+ }
76
+
77
+ const resolved = baseline.entries.filter(
78
+ (entry) => (remaining.get(entryKey(entry)) ?? 0) > 0,
79
+ );
80
+ return { added, resolved };
81
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,184 @@
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
+ }
@@ -0,0 +1,241 @@
1
+ export type CommentKind = "line" | "block";
2
+
3
+ export interface CommentPiece {
4
+ kind: CommentKind;
5
+ text: string;
6
+ start: number;
7
+ end: number;
8
+ }
9
+
10
+ type Mode = "code" | "template" | "expression";
11
+
12
+ interface ScanState {
13
+ braces: number[];
14
+ comments: CommentPiece[];
15
+ index: number;
16
+ modes: Mode[];
17
+ }
18
+
19
+ const REGEX_PRECEDING_CHARACTERS = new Set("(,=:[!&|?{};".split(""));
20
+ const REGEX_PRECEDING_KEYWORDS = new Set([
21
+ "await",
22
+ "case",
23
+ "delete",
24
+ "do",
25
+ "else",
26
+ "in",
27
+ "instanceof",
28
+ "new",
29
+ "of",
30
+ "return",
31
+ "throw",
32
+ "typeof",
33
+ "void",
34
+ "yield",
35
+ ]);
36
+
37
+ function isIdentifierCharacter(character: string): boolean {
38
+ return /[A-Za-z0-9_$]/.test(character);
39
+ }
40
+
41
+ function skipString(source: string, start: number, quote: string): number {
42
+ let index = start + 1;
43
+ while (index < source.length) {
44
+ const character = source[index] ?? "";
45
+ if (character === "\\") {
46
+ index += 2;
47
+ continue;
48
+ }
49
+ if (character === quote) {
50
+ return index + 1;
51
+ }
52
+ if (character === "\n") {
53
+ return index;
54
+ }
55
+ index += 1;
56
+ }
57
+ return index;
58
+ }
59
+
60
+ function isRegexStart(source: string, index: number): boolean {
61
+ let previous = index - 1;
62
+ while (previous >= 0 && /\s/.test(source[previous] ?? "")) {
63
+ previous -= 1;
64
+ }
65
+ if (previous < 0) {
66
+ return true;
67
+ }
68
+ const character = source[previous] ?? "";
69
+ if (REGEX_PRECEDING_CHARACTERS.has(character)) {
70
+ return true;
71
+ }
72
+ if (!isIdentifierCharacter(character)) {
73
+ return false;
74
+ }
75
+ let wordStart = previous;
76
+ while (wordStart >= 0 && isIdentifierCharacter(source[wordStart] ?? "")) {
77
+ wordStart -= 1;
78
+ }
79
+ return REGEX_PRECEDING_KEYWORDS.has(
80
+ source.slice(wordStart + 1, previous + 1),
81
+ );
82
+ }
83
+
84
+ function skipRegex(source: string, start: number): number {
85
+ let index = start + 1;
86
+ let inClass = false;
87
+ while (index < source.length) {
88
+ const character = source[index] ?? "";
89
+ if (character === "\\") {
90
+ index += 2;
91
+ continue;
92
+ }
93
+ if (character === "\n") {
94
+ return start;
95
+ }
96
+ if (inClass) {
97
+ inClass = character !== "]";
98
+ index += 1;
99
+ continue;
100
+ }
101
+ if (character === "[") {
102
+ inClass = true;
103
+ index += 1;
104
+ continue;
105
+ }
106
+ if (character === "/") {
107
+ return index + 1;
108
+ }
109
+ index += 1;
110
+ }
111
+ return start;
112
+ }
113
+
114
+ function currentMode(state: ScanState): Mode {
115
+ return state.modes[state.modes.length - 1] ?? "code";
116
+ }
117
+
118
+ function stepComment(state: ScanState, source: string): boolean {
119
+ const character = source[state.index] ?? "";
120
+ const next = source[state.index + 1] ?? "";
121
+ if (character !== "/" || (next !== "/" && next !== "*")) {
122
+ return false;
123
+ }
124
+ if (next === "/") {
125
+ const newline = source.indexOf("\n", state.index);
126
+ const stop = newline === -1 ? source.length : newline;
127
+ state.comments.push({
128
+ end: stop,
129
+ kind: "line",
130
+ start: state.index,
131
+ text: source.slice(state.index + 2, stop),
132
+ });
133
+ state.index = stop;
134
+ return true;
135
+ }
136
+ const close = source.indexOf("*/", state.index + 2);
137
+ const stop = close === -1 ? source.length : close + 2;
138
+ state.comments.push({
139
+ end: stop,
140
+ kind: "block",
141
+ start: state.index,
142
+ text: source.slice(state.index + 2, Math.max(state.index + 2, stop - 2)),
143
+ });
144
+ state.index = stop;
145
+ return true;
146
+ }
147
+
148
+ function stepString(state: ScanState, source: string): boolean {
149
+ const character = source[state.index] ?? "";
150
+ if (character !== "'" && character !== '"') {
151
+ return false;
152
+ }
153
+ state.index = skipString(source, state.index, character);
154
+ return true;
155
+ }
156
+
157
+ function stepTemplateStart(state: ScanState, source: string): boolean {
158
+ if (source[state.index] !== "`") {
159
+ return false;
160
+ }
161
+ state.modes.push("template");
162
+ state.index += 1;
163
+ return true;
164
+ }
165
+
166
+ function stepRegex(state: ScanState, source: string): boolean {
167
+ if (source[state.index] !== "/" || !isRegexStart(source, state.index)) {
168
+ return false;
169
+ }
170
+ const stop = skipRegex(source, state.index);
171
+ if (stop <= state.index) {
172
+ return false;
173
+ }
174
+ state.index = stop;
175
+ return true;
176
+ }
177
+
178
+ function stepTemplate(state: ScanState, source: string): void {
179
+ const character = source[state.index] ?? "";
180
+ if (character === "\\") {
181
+ state.index += 2;
182
+ return;
183
+ }
184
+ if (character === "`") {
185
+ state.modes.pop();
186
+ state.index += 1;
187
+ return;
188
+ }
189
+ if (character === "$" && source[state.index + 1] === "{") {
190
+ state.modes.push("expression");
191
+ state.braces.push(1);
192
+ state.index += 2;
193
+ return;
194
+ }
195
+ state.index += 1;
196
+ }
197
+
198
+ function stepExpression(state: ScanState, source: string): void {
199
+ const character = source[state.index] ?? "";
200
+ const depth = state.braces[state.braces.length - 1] ?? 0;
201
+ if (character === "{") {
202
+ state.braces[state.braces.length - 1] = depth + 1;
203
+ } else if (character === "}") {
204
+ state.braces[state.braces.length - 1] = depth - 1;
205
+ if (depth - 1 === 0) {
206
+ state.modes.pop();
207
+ state.braces.pop();
208
+ }
209
+ }
210
+ state.index += 1;
211
+ }
212
+
213
+ function step(state: ScanState, source: string): void {
214
+ if (currentMode(state) === "template") {
215
+ stepTemplate(state, source);
216
+ return;
217
+ }
218
+ if (
219
+ stepComment(state, source) ||
220
+ stepString(state, source) ||
221
+ stepTemplateStart(state, source) ||
222
+ stepRegex(state, source)
223
+ ) {
224
+ return;
225
+ }
226
+ stepExpression(state, source);
227
+ }
228
+
229
+ export function extractComments(source: string): CommentPiece[] {
230
+ const shebang = source.startsWith("#!") ? source.indexOf("\n") : 0;
231
+ const state: ScanState = {
232
+ braces: [],
233
+ comments: [],
234
+ index: Math.max(0, shebang),
235
+ modes: ["code"],
236
+ };
237
+ while (state.index < source.length) {
238
+ step(state, source);
239
+ }
240
+ return state.comments;
241
+ }
package/src/rules.ts ADDED
@@ -0,0 +1,55 @@
1
+ export const RULE_IDS = [
2
+ "broad-suppression",
3
+ "undocumented-directive",
4
+ "placeholder-comment",
5
+ "separator-comment",
6
+ ] as const;
7
+
8
+ export type RuleId = (typeof RULE_IDS)[number];
9
+
10
+ export interface Finding {
11
+ rule: RuleId;
12
+ file: string;
13
+ line: number;
14
+ column: number;
15
+ text: string;
16
+ }
17
+
18
+ const PLACEHOLDER_PATTERN = /\b(TODO|FIXME|XXX|HACK)\b/;
19
+ const SEPARATOR_PATTERN = /^[-=*_#~+./\\|]{4,}$/;
20
+ const DIRECTIVE_PATTERN = /^@ts-(?:ignore|expect-error)\b([\s\S]*)$/;
21
+
22
+ export function normalizeComment(body: string): string {
23
+ return body
24
+ .split("\n")
25
+ .map((line) => line.replace(/^\s*\*+\s?/, ""))
26
+ .join(" ")
27
+ .replace(/\s+/g, " ")
28
+ .trim();
29
+ }
30
+
31
+ export function classifyComment(body: string): RuleId | null {
32
+ const text = normalizeComment(body);
33
+ if (text === "") {
34
+ return null;
35
+ }
36
+ if (
37
+ /^(?:biome-ignore-all|@ts-nocheck)\b/.test(text) ||
38
+ /^eslint-disable(?:-next-line|-line)?\s*$/.test(text)
39
+ ) {
40
+ return "broad-suppression";
41
+ }
42
+ const directive = DIRECTIVE_PATTERN.exec(text);
43
+ if (directive) {
44
+ return (directive[1] ?? "").replace(/^[\s:—-]+/, "") === ""
45
+ ? "undocumented-directive"
46
+ : null;
47
+ }
48
+ if (PLACEHOLDER_PATTERN.test(text)) {
49
+ return "placeholder-comment";
50
+ }
51
+ if (SEPARATOR_PATTERN.test(text)) {
52
+ return "separator-comment";
53
+ }
54
+ return null;
55
+ }
package/src/scan.ts ADDED
@@ -0,0 +1,140 @@
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
+ export function normalizePath(path: string): string {
26
+ return path.split("\\").join("/").replace(/^\.\//, "").replace(/\/+$/, "");
27
+ }
28
+
29
+ function isIgnored(path: string, ignores: string[]): boolean {
30
+ return ignores.some(
31
+ (ignore) => path === ignore || path.startsWith(`${ignore}/`),
32
+ );
33
+ }
34
+
35
+ function positionAt(
36
+ source: string,
37
+ offset: number,
38
+ ): { line: number; column: number } {
39
+ let line = 1;
40
+ let lineStart = 0;
41
+ for (let index = 0; index < offset; index += 1) {
42
+ if (source[index] === "\n") {
43
+ line += 1;
44
+ lineStart = index + 1;
45
+ }
46
+ }
47
+ return { line, column: offset - lineStart + 1 };
48
+ }
49
+
50
+ function compareFindings(left: Finding, right: Finding): number {
51
+ if (left.file !== right.file) {
52
+ return left.file < right.file ? -1 : 1;
53
+ }
54
+ if (left.line !== right.line) {
55
+ return left.line - right.line;
56
+ }
57
+ return left.column - right.column;
58
+ }
59
+
60
+ function walk(directory: string, files: Set<string>): void {
61
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
62
+ if (entry.name.startsWith(".")) {
63
+ continue;
64
+ }
65
+ const path = join(directory, entry.name);
66
+ if (entry.isDirectory()) {
67
+ if (!IGNORED_DIRECTORIES.has(entry.name)) {
68
+ walk(path, files);
69
+ }
70
+ continue;
71
+ }
72
+ if (entry.isFile() && SUPPORTED_EXTENSIONS.has(extname(entry.name))) {
73
+ files.add(path);
74
+ }
75
+ }
76
+ }
77
+
78
+ export function collectFiles(
79
+ targets: string[],
80
+ cwd = process.cwd(),
81
+ ignores: string[] = [],
82
+ ): string[] {
83
+ const files = new Set<string>();
84
+ for (const target of targets) {
85
+ const absolute = resolve(cwd, target);
86
+ let stats: ReturnType<typeof statSync>;
87
+ try {
88
+ stats = statSync(absolute);
89
+ } catch {
90
+ continue;
91
+ }
92
+ if (stats.isFile()) {
93
+ if (
94
+ SUPPORTED_EXTENSIONS.has(extname(absolute)) &&
95
+ !isIgnored(normalizePath(target), ignores)
96
+ ) {
97
+ files.add(absolute);
98
+ }
99
+ continue;
100
+ }
101
+ walk(absolute, files);
102
+ }
103
+ return [...files]
104
+ .filter((file) => !isIgnored(normalizePath(relative(cwd, file)), ignores))
105
+ .sort();
106
+ }
107
+
108
+ export function scanSource(source: string, file: string): Finding[] {
109
+ const findings: Finding[] = [];
110
+ for (const comment of extractComments(source)) {
111
+ const rule = classifyComment(comment.text);
112
+ if (rule === null) {
113
+ continue;
114
+ }
115
+ const position = positionAt(source, comment.start);
116
+ findings.push({
117
+ column: position.column,
118
+ file,
119
+ line: position.line,
120
+ rule,
121
+ text: normalizeComment(comment.text),
122
+ });
123
+ }
124
+ return findings;
125
+ }
126
+
127
+ export function scanFile(file: string, cwd = process.cwd()): Finding[] {
128
+ return scanSource(
129
+ readFileSync(file, "utf8"),
130
+ normalizePath(relative(cwd, file)),
131
+ );
132
+ }
133
+
134
+ export function scanFiles(files: string[], cwd = process.cwd()): Finding[] {
135
+ const findings: Finding[] = [];
136
+ for (const file of files) {
137
+ findings.push(...scanFile(file, cwd));
138
+ }
139
+ return findings.sort(compareFindings);
140
+ }