@vhite/vhite 0.1.0 → 0.8.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.
Files changed (54) hide show
  1. package/README.md +119 -22
  2. package/bin/cli.js +43 -200
  3. package/bin/commands/baseline.js +27 -0
  4. package/bin/commands/hook.js +58 -0
  5. package/bin/commands/info.js +39 -0
  6. package/bin/commands/scan.js +257 -0
  7. package/bin/commands/shared.js +67 -0
  8. package/bin/commands/watch.js +44 -0
  9. package/bin/output/text.js +42 -0
  10. package/package.json +32 -5
  11. package/rules/a11y/a11y-click-div.js +39 -0
  12. package/rules/a11y/a11y-img-alt.js +36 -0
  13. package/rules/a11y/a11y-input-label.js +40 -0
  14. package/rules/{console-log.js → core/console-log.js} +2 -1
  15. package/rules/{debugger-statement.js → core/debugger-statement.js} +2 -1
  16. package/rules/{empty-block.js → core/empty-block.js} +2 -1
  17. package/rules/{empty-catch.js → core/empty-catch.js} +2 -1
  18. package/rules/{hardcoded-secret.js → core/hardcoded-secret.js} +9 -4
  19. package/rules/{leftover-todo.js → core/leftover-todo.js} +6 -2
  20. package/rules/{unused-import.js → core/unused-import.js} +3 -1
  21. package/rules/index.js +18 -8
  22. package/rules/react/react-dangerous-html.js +30 -0
  23. package/rules/react/react-missing-key.js +56 -0
  24. package/rules/shared/jsx-helpers.js +30 -0
  25. package/src/core/args.js +140 -0
  26. package/src/core/config.js +92 -0
  27. package/src/core/engine.js +144 -0
  28. package/src/core/exit-codes.js +15 -0
  29. package/src/{setup.js → core/setup.js} +1 -1
  30. package/src/core/version.js +6 -0
  31. package/src/{safe-fix.js → fix/safe-fix.js} +1 -1
  32. package/src/output/explanations-frontend.js +22 -0
  33. package/src/{explanations.js → output/explanations.js} +2 -0
  34. package/src/output/json-output.js +47 -0
  35. package/src/{reporter.js → output/reporter.js} +20 -1
  36. package/src/{run-report.js → output/run-report.js} +1 -1
  37. package/src/output/sarif.js +60 -0
  38. package/src/scan/baseline.js +60 -0
  39. package/src/scan/git.js +28 -0
  40. package/src/scan/ignore.js +68 -0
  41. package/src/scan/suppress.js +53 -0
  42. package/src/smart/consent.js +34 -0
  43. package/src/smart/fixes.js +143 -0
  44. package/src/smart/notes.js +73 -0
  45. package/src/config.js +0 -42
  46. package/src/engine.js +0 -80
  47. /package/src/{files.js → core/files.js} +0 -0
  48. /package/src/{approve.js → fix/approve.js} +0 -0
  49. /package/src/{backup.js → fix/backup.js} +0 -0
  50. /package/src/{fixers → fix/fixers}/debugger-statement.js +0 -0
  51. /package/src/{fixers → fix/fixers}/index.js +0 -0
  52. /package/src/{fixers → fix/fixers}/leftover-todo.js +0 -0
  53. /package/src/{fixers → fix/fixers}/unused-import.js +0 -0
  54. /package/src/{gitignore.js → fix/gitignore.js} +0 -0
@@ -1,6 +1,15 @@
1
1
  import { relative } from "node:path";
2
2
  import { explanations } from "./explanations.js";
3
3
 
4
+ function caretFor(issue) {
5
+ const startColumn = issue.column ?? 1;
6
+ const width =
7
+ issue.endLine === issue.line && issue.endColumn > startColumn
8
+ ? issue.endColumn - startColumn
9
+ : Math.max(1, issue.snippet.length - startColumn + 1);
10
+ return " ".repeat(startColumn - 1) + "^".repeat(width);
11
+ }
12
+
4
13
  export function formatReport(issues, root) {
5
14
  if (issues.length === 0) return "No issues found.";
6
15
 
@@ -17,9 +26,19 @@ export function formatReport(issues, root) {
17
26
  for (const file of sortedFiles) {
18
27
  const fileIssues = byFile.get(file);
19
28
  lines.push(relative(root, file));
20
- fileIssues.sort((a, b) => a.line - b.line);
29
+ fileIssues.sort(
30
+ (a, b) => a.line - b.line || (a.column ?? 1) - (b.column ?? 1)
31
+ );
21
32
  for (const issue of fileIssues) {
22
33
  lines.push(` Line ${issue.line}: ${issue.message} [${issue.rule}]`);
34
+
35
+ if (issue.snippet !== undefined && issue.endLine !== undefined) {
36
+ const gutter = String(issue.line);
37
+ const blank = " ".repeat(gutter.length);
38
+ lines.push(` ${gutter} | ${issue.snippet}`);
39
+ lines.push(` ${blank} | ${caretFor(issue)}`);
40
+ }
41
+
23
42
  const info = explanations[issue.rule];
24
43
  if (info) {
25
44
  lines.push(` What happened: ${info.whatHappened}`);
@@ -7,7 +7,7 @@ import { createInterface } from "node:readline/promises";
7
7
  import {
8
8
  FIX_DIR,
9
9
  ensureGitignoreEntry,
10
- } from "./gitignore.js";
10
+ } from "../fix/gitignore.js";
11
11
 
12
12
  function escapeMarkdown(value) {
13
13
  return String(value ?? "")
@@ -0,0 +1,60 @@
1
+ import { relative, sep } from "node:path";
2
+ import { rules } from "../../rules/index.js";
3
+
4
+ const LEVELS = { error: "error", warning: "warning", info: "note" };
5
+
6
+ function toPosix(path) {
7
+ return path.split(sep).join("/");
8
+ }
9
+
10
+ export function formatSarif({ root, issues, version }) {
11
+ const descriptions = new Map(rules.map((rule) => [rule.name, rule.description]));
12
+ const ruleIds = [...new Set(issues.map((issue) => issue.rule))].sort();
13
+
14
+ const results = issues.map((issue) => {
15
+ const column = issue.column ?? 1;
16
+ return {
17
+ ruleId: issue.rule,
18
+ level: LEVELS[issue.severity] ?? "warning",
19
+ message: { text: issue.message },
20
+ locations: [
21
+ {
22
+ physicalLocation: {
23
+ artifactLocation: { uri: toPosix(relative(root, issue.file)) },
24
+ region: {
25
+ startLine: issue.line,
26
+ startColumn: column,
27
+ endLine: issue.endLine ?? issue.line,
28
+ endColumn: issue.endColumn ?? column,
29
+ },
30
+ },
31
+ },
32
+ ],
33
+ };
34
+ });
35
+
36
+ return JSON.stringify(
37
+ {
38
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
39
+ version: "2.1.0",
40
+ runs: [
41
+ {
42
+ tool: {
43
+ driver: {
44
+ name: "vhite",
45
+ version,
46
+ informationUri: "https://github.com/Wilson-Edem/vhite",
47
+ rules: ruleIds.map((id) => ({
48
+ id,
49
+ shortDescription: { text: descriptions.get(id) ?? id },
50
+ })),
51
+ },
52
+ },
53
+ results,
54
+ },
55
+ ],
56
+ },
57
+ null,
58
+ 2
59
+ );
60
+ }
@@ -0,0 +1,60 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { join, relative, sep } from "node:path";
3
+
4
+ export const BASELINE_NAME = "vhite.baseline.json";
5
+
6
+ function toPosix(path) {
7
+ return path.split(sep).join("/");
8
+ }
9
+
10
+ export function fingerprintOf(issue, root) {
11
+ const text = (issue.snippet ?? issue.message).trim();
12
+ return `${issue.rule}|${toPosix(relative(root, issue.file))}|${text}`;
13
+ }
14
+
15
+ export function buildBaseline(issues, root) {
16
+ const fingerprints = {};
17
+ for (const issue of issues) {
18
+ const key = fingerprintOf(issue, root);
19
+ fingerprints[key] = (fingerprints[key] ?? 0) + 1;
20
+ }
21
+ return { version: 1, fingerprints };
22
+ }
23
+
24
+ export async function saveBaseline(root, baseline) {
25
+ const path = join(root, BASELINE_NAME);
26
+ await writeFile(path, JSON.stringify(baseline, null, 2) + "\n", "utf8");
27
+ return path;
28
+ }
29
+
30
+ export async function loadBaseline(root) {
31
+ try {
32
+ const text = await readFile(join(root, BASELINE_NAME), "utf8");
33
+ const data = JSON.parse(text);
34
+ if (typeof data.fingerprints !== "object" || data.fingerprints === null) {
35
+ throw new Error(`${BASELINE_NAME} has no fingerprints`);
36
+ }
37
+ return data;
38
+ } catch (error) {
39
+ if (error.code === "ENOENT") return null;
40
+ throw error;
41
+ }
42
+ }
43
+
44
+ export function applyBaseline(issues, baseline, root) {
45
+ const remaining = { ...baseline.fingerprints };
46
+ const fresh = [];
47
+ let hidden = 0;
48
+
49
+ for (const issue of issues) {
50
+ const key = fingerprintOf(issue, root);
51
+ if ((remaining[key] ?? 0) > 0) {
52
+ remaining[key] -= 1;
53
+ hidden += 1;
54
+ } else {
55
+ fresh.push(issue);
56
+ }
57
+ }
58
+
59
+ return { issues: fresh, hidden };
60
+ }
@@ -0,0 +1,28 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { resolve } from "node:path";
4
+
5
+ const run = promisify(execFile);
6
+
7
+ async function git(root, args) {
8
+ try {
9
+ const { stdout } = await run("git", args, { cwd: root });
10
+ return stdout.split(/\r?\n/).filter((line) => line.trim() !== "");
11
+ } catch (error) {
12
+ throw new Error(
13
+ "This option needs a Git repository with at least one commit, and Git must be installed"
14
+ );
15
+ }
16
+ }
17
+
18
+ export async function changedFiles(root, mode) {
19
+ let names;
20
+ if (mode === "staged") {
21
+ names = await git(root, ["diff", "--name-only", "--relative", "--cached"]);
22
+ } else {
23
+ const tracked = await git(root, ["diff", "--name-only", "--relative", "HEAD"]);
24
+ const untracked = await git(root, ["ls-files", "--others", "--exclude-standard"]);
25
+ names = [...tracked, ...untracked];
26
+ }
27
+ return new Set(names.map((name) => resolve(root, name)));
28
+ }
@@ -0,0 +1,68 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { normalizePath } from "../core/args.js";
4
+
5
+ const DEFAULT_IGNORES = [
6
+ "node_modules",
7
+ ".git",
8
+ "dist",
9
+ "build",
10
+ "coverage",
11
+ ".next",
12
+ "vendor",
13
+ ".vhite-fix",
14
+ ];
15
+
16
+ function globToRegex(glob) {
17
+ let source = "";
18
+ for (let index = 0; index < glob.length; index += 1) {
19
+ const char = glob[index];
20
+ if (char === "*" && glob[index + 1] === "*") {
21
+ source += ".*";
22
+ index += 1;
23
+ } else if (char === "*") {
24
+ source += "[^/]*";
25
+ } else if (char === "?") {
26
+ source += "[^/]";
27
+ } else {
28
+ source += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
29
+ }
30
+ }
31
+ return new RegExp(`^${source}$`);
32
+ }
33
+
34
+ function prefixesOf(path) {
35
+ const parts = path.split("/");
36
+ return parts.map((_, index) => parts.slice(0, index + 1).join("/"));
37
+ }
38
+
39
+ function matchesPattern(relativePath, pattern) {
40
+ const anchored = pattern.includes("/");
41
+ const clean = pattern.replace(/^\//, "").replace(/\/$/, "");
42
+ const regex = globToRegex(clean);
43
+
44
+ if (anchored) {
45
+ return prefixesOf(relativePath).some((prefix) => regex.test(prefix));
46
+ }
47
+ return relativePath.split("/").some((segment) => regex.test(segment));
48
+ }
49
+
50
+ export async function loadIgnorePatterns(root) {
51
+ const patterns = [...DEFAULT_IGNORES];
52
+ try {
53
+ const text = await readFile(join(root, ".gitignore"), "utf8");
54
+ for (const raw of text.split(/\r?\n/)) {
55
+ const line = raw.trim();
56
+ if (line === "" || line.startsWith("#") || line.startsWith("!")) continue;
57
+ patterns.push(line);
58
+ }
59
+ } catch (error) {
60
+ if (error.code !== "ENOENT") throw error;
61
+ }
62
+ return patterns;
63
+ }
64
+
65
+ export function isIgnored(relativePath, patterns) {
66
+ const rel = normalizePath(relativePath);
67
+ return patterns.some((pattern) => matchesPattern(rel, pattern));
68
+ }
@@ -0,0 +1,53 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ const DIRECTIVE = /vhite-disable-(next-line|line)(?:\s+([A-Za-z0-9_,\- ]+))?/;
4
+
5
+ function parseRules(text) {
6
+ if (!text || text.trim() === "") return null;
7
+ return new Set(
8
+ text
9
+ .split(/[, ]+/)
10
+ .map((name) => name.trim())
11
+ .filter((name) => name !== "")
12
+ );
13
+ }
14
+
15
+ function directivesFor(source) {
16
+ const lines = source.split(/\r?\n/);
17
+ const map = new Map();
18
+
19
+ lines.forEach((text, index) => {
20
+ const match = DIRECTIVE.exec(text);
21
+ if (!match) return;
22
+
23
+ const target = match[1] === "next-line" ? index + 2 : index + 1;
24
+ map.set(target, parseRules(match[2]));
25
+ });
26
+
27
+ return map;
28
+ }
29
+
30
+ export async function applySuppressions(issues, root) {
31
+ const cache = new Map();
32
+ const kept = [];
33
+ let suppressed = 0;
34
+
35
+ for (const issue of issues) {
36
+ if (!cache.has(issue.file)) {
37
+ const source = await readFile(issue.file, "utf8");
38
+ cache.set(issue.file, directivesFor(source));
39
+ }
40
+
41
+ const directives = cache.get(issue.file);
42
+ const entry = directives.get(issue.line);
43
+ const hides = entry !== undefined && (entry === null || entry.has(issue.rule));
44
+
45
+ if (hides) {
46
+ suppressed += 1;
47
+ } else {
48
+ kept.push(issue);
49
+ }
50
+ }
51
+
52
+ return { issues: kept, suppressed };
53
+ }
@@ -0,0 +1,34 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { createInterface } from "node:readline/promises";
4
+
5
+ const CONFIG_NAME = "vhite.config.json";
6
+
7
+ async function readConfigObject(root) {
8
+ try {
9
+ return JSON.parse(await readFile(join(root, CONFIG_NAME), "utf8"));
10
+ } catch {
11
+ return {};
12
+ }
13
+ }
14
+
15
+ export async function readSmartFixSetting(root) {
16
+ const data = await readConfigObject(root);
17
+ return typeof data.smartFixes === "boolean" ? data.smartFixes : undefined;
18
+ }
19
+
20
+ export async function saveSmartFixSetting(root, value) {
21
+ const data = await readConfigObject(root);
22
+ data.smartFixes = value;
23
+ await writeFile(join(root, CONFIG_NAME), JSON.stringify(data, null, 2) + "\n", "utf8");
24
+ }
25
+
26
+ export async function askSmartFixConsent() {
27
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
28
+ try {
29
+ const answer = (await rl.question("Enable smart fixes for files with findings? (Y/n): ")).trim().toLowerCase();
30
+ return answer === "" || answer === "y" || answer === "yes";
31
+ } finally {
32
+ rl.close();
33
+ }
34
+ }
@@ -0,0 +1,143 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { redact } from "./notes.js";
3
+
4
+ export const DEFAULT_FIX_ENDPOINT = "https://victor-edem.vercel.app/api/smart-fixes";
5
+ const MAX_CODE_CHARS = 12000;
6
+ const MAX_FINDINGS_PER_REQUEST = 30;
7
+ const CONCURRENCY = 4;
8
+ const FIXABLE = new Set(["error", "warning"]);
9
+
10
+ export function groupByFile(issues) {
11
+ const groups = new Map();
12
+ for (const issue of issues) {
13
+ if (!FIXABLE.has(issue.severity)) continue;
14
+ if (!groups.has(issue.file)) groups.set(issue.file, []);
15
+ groups.get(issue.file).push(issue);
16
+ }
17
+ return groups;
18
+ }
19
+
20
+ export function validateEdits(edits, text, findings) {
21
+ const lines = text.split("\n");
22
+ const seen = new Set();
23
+ const accepted = [];
24
+
25
+ for (const edit of Array.isArray(edits) ? edits : []) {
26
+ if (!edit || !Number.isInteger(edit.line) || seen.has(edit.line)) continue;
27
+ const finding = findings.find((item) => item.line === edit.line);
28
+ if (!finding) continue;
29
+ if (lines[edit.line - 1] !== edit.original) continue;
30
+ if (typeof edit.replacement !== "string" || edit.replacement.includes("\n")) continue;
31
+ if (edit.replacement.trim() === edit.original.trim()) continue;
32
+ seen.add(edit.line);
33
+ accepted.push({ line: edit.line, original: edit.original, replacement: edit.replacement, finding });
34
+ }
35
+ return accepted;
36
+ }
37
+
38
+ export function toProposals(file, accepted) {
39
+ return accepted.map((edit) => ({
40
+ file,
41
+ line: edit.line,
42
+ rule: edit.finding.rule,
43
+ message: edit.finding.message,
44
+ lineText: edit.original.trim(),
45
+ original: edit.original,
46
+ replacement: edit.replacement,
47
+ smartFix: true,
48
+ }));
49
+ }
50
+
51
+ export function smartFixerFor(rule) {
52
+ return {
53
+ rule,
54
+ apply(source, issue) {
55
+ const lines = source.split("\n");
56
+ if (lines[issue.line - 1] !== issue.original) return null;
57
+ if (issue.replacement === "") {
58
+ lines.splice(issue.line - 1, 1);
59
+ } else {
60
+ lines[issue.line - 1] = issue.replacement;
61
+ }
62
+ return lines.join("\n");
63
+ },
64
+ };
65
+ }
66
+
67
+ export function smartFixersFor(proposals) {
68
+ const fixers = {};
69
+ for (const proposal of proposals) {
70
+ fixers[proposal.rule] = smartFixerFor(proposal.rule);
71
+ }
72
+ return fixers;
73
+ }
74
+
75
+ async function requestEdits(text, findings, { endpoint, fetchImpl }) {
76
+ const response = await fetchImpl(endpoint, {
77
+ method: "POST",
78
+ headers: { "Content-Type": "application/json" },
79
+ body: JSON.stringify({
80
+ language: "javascript",
81
+ code: text,
82
+ findings: findings.slice(0, MAX_FINDINGS_PER_REQUEST).map((item) => ({
83
+ line: item.line,
84
+ rule: item.rule,
85
+ message: item.message,
86
+ })),
87
+ }),
88
+ });
89
+
90
+ const data = await response.json().catch(() => ({}));
91
+ if (response.status === 429) return { limited: true, edits: [] };
92
+ if (!response.ok) throw new Error(data.error ?? `Smart service returned ${response.status}`);
93
+ return { edits: data.edits ?? [] };
94
+ }
95
+
96
+ export async function proposeSmartFixes(issues, say, { endpoint = DEFAULT_FIX_ENDPOINT, fetchImpl = fetch, skipLines = new Set() } = {}) {
97
+ const groups = [...groupByFile(issues).entries()];
98
+ const proposals = [];
99
+ let stopped = false;
100
+
101
+ const worker = async () => {
102
+ while (groups.length > 0 && !stopped) {
103
+ const [file, findings] = groups.shift();
104
+ const name = file;
105
+
106
+ let text;
107
+ try {
108
+ text = await readFile(file, "utf8");
109
+ } catch {
110
+ say(`Smart fixes skipped for ${name}: file could not be read`);
111
+ continue;
112
+ }
113
+
114
+ if (text.length > MAX_CODE_CHARS) {
115
+ say(`Smart fixes skipped for ${name}: file is too large to send`);
116
+ continue;
117
+ }
118
+ if (redact(text) !== text) {
119
+ say(`Smart fixes skipped for ${name}: it contains a secret-looking value`);
120
+ continue;
121
+ }
122
+
123
+ const usable = findings.filter((item) => !skipLines.has(`${file}:${item.line}`));
124
+ if (usable.length === 0) continue;
125
+
126
+ try {
127
+ const result = await requestEdits(text, usable, { endpoint, fetchImpl });
128
+ if (result.limited) {
129
+ say("Daily smart fix limit reached. Try again tomorrow.");
130
+ stopped = true;
131
+ return;
132
+ }
133
+ const accepted = validateEdits(result.edits, text, usable);
134
+ proposals.push(...toProposals(file, accepted));
135
+ } catch (error) {
136
+ say(`Smart fixes unavailable for ${name}: ${error.message}`);
137
+ }
138
+ }
139
+ };
140
+
141
+ await Promise.all(Array.from({ length: CONCURRENCY }, () => worker()));
142
+ return proposals;
143
+ }
@@ -0,0 +1,73 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ export const DEFAULT_ENDPOINT = "https://victor-edem.vercel.app/api/smart-notes";
4
+ const CONTEXT_LINES = 5;
5
+
6
+ const SECRET_PATTERNS = [
7
+ /\b(?:api[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|secret[_-]?key|password)\b\s*[:=]\s*["'`][^"'`\s]{8,}["'`]/gi,
8
+ /\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16})\b/g,
9
+ ];
10
+
11
+ export function redact(text) {
12
+ let output = text;
13
+ for (const pattern of SECRET_PATTERNS) {
14
+ output = output.replace(pattern, "[REDACTED]");
15
+ }
16
+ return output;
17
+ }
18
+
19
+ export function contextFor(source, line) {
20
+ const lines = source.split("\n");
21
+ const start = Math.max(0, line - 1 - CONTEXT_LINES);
22
+ const end = Math.min(lines.length, line + CONTEXT_LINES);
23
+ return lines.slice(start, end).join("\n");
24
+ }
25
+
26
+ export async function noteIssue(issue, source, { endpoint, fetchImpl = fetch } = {}) {
27
+ const snippet = redact(contextFor(source, issue.line));
28
+ const url = endpoint || process.env.VHITE_AI_URL || "https://victor-edem.vercel.app/api/smart-notes";
29
+ const response = await fetchImpl(url, {
30
+ method: "POST",
31
+ headers: { "Content-Type": "application/json" },
32
+ body: JSON.stringify({
33
+ rule: issue.rule,
34
+ message: issue.message,
35
+ snippet,
36
+ language: "javascript",
37
+ }),
38
+ });
39
+
40
+ const data = await response.json().catch(() => ({}));
41
+ if (response.status === 429) return { limited: true };
42
+ if (!response.ok) throw new Error(data.error ?? `Smart service returned ${response.status}`);
43
+ return { text: data.explanation, remaining: data.remaining };
44
+ }
45
+
46
+ export async function noteAll(issues, say, { fetchImpl = fetch, endpoint } = {}) {
47
+ const cache = new Map();
48
+ let explained = 0;
49
+
50
+ for (const issue of issues) {
51
+ if (!cache.has(issue.file)) {
52
+ cache.set(issue.file, await readFile(issue.file, "utf8"));
53
+ }
54
+
55
+ let result;
56
+ try {
57
+ result = await noteIssue(issue, cache.get(issue.file), { endpoint, fetchImpl });
58
+ } catch (error) {
59
+ say(`Smart notes unavailable: ${error.message}`);
60
+ return explained;
61
+ }
62
+
63
+ if (result.limited) {
64
+ say("Daily smart notes limit reached (20 per day). Try again tomorrow.");
65
+ return explained;
66
+ }
67
+
68
+ say(` Smart note for line ${issue.line} [${issue.rule}]: ${result.text}`);
69
+ explained += 1;
70
+ }
71
+
72
+ return explained;
73
+ }
package/src/config.js DELETED
@@ -1,42 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { rules } from "../rules/index.js";
4
-
5
- const CONFIG_NAME = "vhite.config.json";
6
- const knownRules = new Set(rules.map((rule) => rule.name));
7
-
8
- export async function loadConfig(root) {
9
- let text;
10
- try {
11
- text = await readFile(join(root, CONFIG_NAME), "utf8");
12
- } catch (error) {
13
- if (error.code === "ENOENT") return { disabledRules: new Set(), warnings: [] };
14
- throw error;
15
- }
16
-
17
- let data;
18
- try {
19
- data = JSON.parse(text);
20
- } catch (error) {
21
- throw new Error(`${CONFIG_NAME} is not valid JSON: ${error.message}`);
22
- }
23
-
24
- if (typeof data !== "object" || data === null || Array.isArray(data)) {
25
- throw new Error(`${CONFIG_NAME} must contain a JSON object`);
26
- }
27
-
28
- const requested = Array.isArray(data.disabledRules) ? data.disabledRules : [];
29
- const disabledRules = new Set();
30
- const warnings = [];
31
-
32
- for (const name of requested) {
33
- if (knownRules.has(name)) {
34
- disabledRules.add(name);
35
- } else {
36
- warnings.push(`"${name}" is not a rule name, so it was ignored`);
37
- }
38
- }
39
-
40
- return { disabledRules, warnings };
41
- }
42
-
package/src/engine.js DELETED
@@ -1,80 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import { extname } from "node:path";
3
- import { parse } from "espree";
4
- import { rules } from "../rules/index.js";
5
-
6
- function parseSource(source, file) {
7
- const extension = extname(file).toLowerCase();
8
-
9
- return parse(source, {
10
- ecmaVersion: "latest",
11
- sourceType: extension === ".cjs" ? "commonjs" : "module",
12
- ecmaFeatures: {
13
- jsx: true,
14
- },
15
- loc: true,
16
- range: true,
17
- comment: true,
18
- tokens: true,
19
- allowHashBang: true,
20
- });
21
- }
22
-
23
- export async function runRules(files, { disabledRules = new Set() } = {}) {
24
- const issues = [];
25
- const activeRules = rules.filter((rule) => !disabledRules.has(rule.name));
26
-
27
- for (const file of files) {
28
- const source = await readFile(file, "utf8");
29
- const extension = extname(file).toLowerCase();
30
-
31
- const applicableRules = activeRules.filter((rule) =>
32
- rule.extensions?.includes(extension)
33
- );
34
-
35
- if (applicableRules.length === 0) {
36
- continue;
37
- }
38
-
39
- const needsAst = applicableRules.some(
40
- (rule) => rule.requiresAst === true
41
- );
42
-
43
- let ast;
44
-
45
- if (needsAst) {
46
- try {
47
- ast = parseSource(source, file);
48
- } catch (error) {
49
- issues.push({
50
- rule: "parser",
51
- file,
52
- line: error.lineNumber ?? 1,
53
- message: `Unable to parse file: ${error.message}`,
54
- });
55
- }
56
- }
57
-
58
- for (const rule of applicableRules) {
59
- if (rule.requiresAst === true && !ast) {
60
- continue;
61
- }
62
-
63
- const found = rule.check({
64
- file,
65
- source,
66
- ...(rule.requiresAst === true ? { ast } : {}),
67
- });
68
-
69
- for (const issue of found) {
70
- issues.push({
71
- rule: rule.name,
72
- file,
73
- ...issue,
74
- });
75
- }
76
- }
77
- }
78
-
79
- return issues;
80
- }
File without changes
File without changes