@vhite/vhite 0.0.0-stage → 0.6.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 (49) hide show
  1. package/README.md +151 -2
  2. package/bin/cli.js +59 -0
  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 +206 -0
  7. package/bin/commands/shared.js +67 -0
  8. package/bin/commands/watch.js +44 -0
  9. package/bin/output/text.js +40 -0
  10. package/package.json +71 -4
  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/core/console-log.js +44 -0
  15. package/rules/core/debugger-statement.js +36 -0
  16. package/rules/core/empty-block.js +41 -0
  17. package/rules/core/empty-catch.js +40 -0
  18. package/rules/core/hardcoded-secret.js +41 -0
  19. package/rules/core/leftover-todo.js +30 -0
  20. package/rules/core/unused-import.js +69 -0
  21. package/rules/index.js +27 -0
  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 +134 -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/core/files.js +21 -0
  30. package/src/core/setup.js +114 -0
  31. package/src/core/version.js +6 -0
  32. package/src/fix/approve.js +71 -0
  33. package/src/fix/backup.js +73 -0
  34. package/src/fix/fixers/debugger-statement.js +12 -0
  35. package/src/fix/fixers/index.js +9 -0
  36. package/src/fix/fixers/leftover-todo.js +32 -0
  37. package/src/fix/fixers/unused-import.js +18 -0
  38. package/src/fix/gitignore.js +43 -0
  39. package/src/fix/safe-fix.js +178 -0
  40. package/src/output/explanations-frontend.js +22 -0
  41. package/src/output/explanations.js +37 -0
  42. package/src/output/json-output.js +47 -0
  43. package/src/output/reporter.js +52 -0
  44. package/src/output/run-report.js +194 -0
  45. package/src/output/sarif.js +60 -0
  46. package/src/scan/baseline.js +60 -0
  47. package/src/scan/git.js +28 -0
  48. package/src/scan/ignore.js +68 -0
  49. package/src/scan/suppress.js +53 -0
@@ -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
+ }