@vhite/vhite 0.1.0 → 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 (51) hide show
  1. package/README.md +117 -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 +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 +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 +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/{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/config.js +0 -42
  43. package/src/engine.js +0 -80
  44. /package/src/{files.js → core/files.js} +0 -0
  45. /package/src/{approve.js → fix/approve.js} +0 -0
  46. /package/src/{backup.js → fix/backup.js} +0 -0
  47. /package/src/{fixers → fix/fixers}/debugger-statement.js +0 -0
  48. /package/src/{fixers → fix/fixers}/index.js +0 -0
  49. /package/src/{fixers → fix/fixers}/leftover-todo.js +0 -0
  50. /package/src/{fixers → fix/fixers}/unused-import.js +0 -0
  51. /package/src/{gitignore.js → fix/gitignore.js} +0 -0
@@ -0,0 +1,206 @@
1
+ import { resolve, join, relative } from "node:path";
2
+ import { existsSync } from "node:fs";
3
+ import { loadConfig } from "../../src/core/config.js";
4
+ import { CONFIG_NAME, canAskQuestions, runFirstTimeSetup } from "../../src/core/setup.js";
5
+ import { proposeFixes, applyApprovedFixes } from "../../src/fix/safe-fix.js";
6
+ import { chooseProposals } from "../../src/fix/approve.js";
7
+ import { deleteRunBackups } from "../../src/fix/backup.js";
8
+ import { FIX_DIR } from "../../src/fix/gitignore.js";
9
+ import { askYesNo, writeRunReport } from "../../src/output/run-report.js";
10
+ import { EXIT, decideExitCode } from "../../src/core/exit-codes.js";
11
+ import { formatJson } from "../../src/output/json-output.js";
12
+ import { formatSarif } from "../../src/output/sarif.js";
13
+ import { formatReport } from "../../src/output/reporter.js";
14
+ import { BASELINE_NAME, loadBaseline } from "../../src/scan/baseline.js";
15
+ import { readVersion } from "../../src/core/version.js";
16
+ import { BANNER } from "../output/text.js";
17
+ import {
18
+ KNOWN_RULES,
19
+ selectFiles,
20
+ collectIssues,
21
+ disabledSetFor,
22
+ summaryLine,
23
+ } from "./shared.js";
24
+
25
+ export async function runScan(options) {
26
+ const target = resolve(options.path ?? ".");
27
+ const isText = options.format === "text";
28
+ const showText = isText && !options.quiet;
29
+ const interactive = canAskQuestions() && showText && !options.watching && !options.noPrompt;
30
+ const logs = [];
31
+ const startedAt = new Date();
32
+
33
+ const say = (message = "") => {
34
+ logs.push(message);
35
+ if (showText) console.log(message);
36
+ };
37
+
38
+ const unknown = [...options.rules, ...options.disable].filter(
39
+ (name) => !KNOWN_RULES.has(name)
40
+ );
41
+ if (unknown.length > 0) {
42
+ console.error(`Error: unknown rule name(s): ${unknown.join(", ")}`);
43
+ return EXIT.ERROR;
44
+ }
45
+
46
+ let issues = [];
47
+ let files = [];
48
+ let fixResults = [];
49
+ let backupCleanup = null;
50
+
51
+ try {
52
+ if (showText && !options.quiet) {
53
+ console.log(BANNER);
54
+ }
55
+
56
+ if (!existsSync(join(target, CONFIG_NAME)) && interactive) {
57
+ await runFirstTimeSetup(target);
58
+ }
59
+
60
+ const config = await loadConfig(target);
61
+
62
+ for (const warning of config.warnings) {
63
+ say(`Warning: ${warning}`);
64
+ }
65
+
66
+ const disabled = disabledSetFor(config, options);
67
+ if (disabled.size > 0 && showText) {
68
+ say(`Rules turned off: ${[...disabled].join(", ")}`);
69
+ }
70
+
71
+ const baseline = options.useBaseline ? await loadBaseline(target) : null;
72
+
73
+ files = await selectFiles(target, options);
74
+
75
+ say(
76
+ `vhite found ${files.length} front-end files in ${relative(process.cwd(), target) || "."}\n`
77
+ );
78
+
79
+ const first = await collectIssues(files, target, disabled, config, baseline);
80
+ issues = first.issues;
81
+
82
+ say(formatReport(issues, target));
83
+ if (issues.length > 0) say(summaryLine(issues));
84
+ if (first.hidden > 0) {
85
+ say(`${first.hidden} known issue(s) hidden by ${BASELINE_NAME}`);
86
+ }
87
+ if (first.suppressed > 0) {
88
+ say(`${first.suppressed} issue(s) skipped by vhite-disable comments`);
89
+ }
90
+
91
+ const proposals = await proposeFixes(issues);
92
+ let chosen = [];
93
+
94
+ if (proposals.length > 0) {
95
+ if (options.fix) {
96
+ chosen = proposals;
97
+ } else if (interactive) {
98
+ chosen = await chooseProposals(target, proposals);
99
+ } else if (options.command === "fix") {
100
+ console.error(
101
+ "Error: vhite fix needs an interactive terminal. Use --fix to apply all safe fixes."
102
+ );
103
+ return EXIT.ERROR;
104
+ }
105
+ }
106
+
107
+ if (chosen.length > 0) {
108
+ const result = await applyApprovedFixes(target, chosen, { disabledRules: disabled });
109
+ fixResults = result.results;
110
+
111
+ if (result.gitignoreUpdated) {
112
+ say(`Added ${FIX_DIR}/ to .gitignore`);
113
+ }
114
+
115
+ for (const item of fixResults) {
116
+ const where = `${relative(target, item.proposal.file)} line ${item.proposal.line}`;
117
+ if (item.status === "applied") {
118
+ say(`Applied: ${where}`);
119
+ } else {
120
+ const label = item.status === "skipped" ? "Skipped" : "Rolled back";
121
+ say(`${label}: ${where}. ${item.reason}`);
122
+ }
123
+ }
124
+
125
+ const rescan = await collectIssues(files, target, disabled, config, baseline);
126
+ issues = rescan.issues;
127
+ say(`Final scan: ${issues.length} issue(s) remaining`);
128
+ }
129
+
130
+ const hasRunBackups = fixResults.some((item) => typeof item.backupPath === "string");
131
+
132
+ if (interactive && hasRunBackups) {
133
+ const shouldDelete = await askYesNo(
134
+ "Do you want to delete the backup files created during this run?"
135
+ );
136
+
137
+ if (shouldDelete) {
138
+ const result = await deleteRunBackups(fixResults);
139
+ backupCleanup = { requested: true, deleted: result.deleted };
140
+ say(`Deleted ${result.deleted} backup file(s) created during this run.`);
141
+ } else {
142
+ backupCleanup = { requested: false, deleted: 0 };
143
+ say("Kept the backup files created during this run.");
144
+ }
145
+ }
146
+
147
+ if (showText) say("\nVhite run completed.");
148
+
149
+ if (interactive) {
150
+ const shouldCreateReport = await askYesNo(
151
+ "Would you like to create a detailed Markdown report of this run?"
152
+ );
153
+
154
+ if (shouldCreateReport) {
155
+ const result = await writeRunReport({
156
+ root: target,
157
+ target,
158
+ startedAt,
159
+ finishedAt: new Date(),
160
+ filesScanned: files.length,
161
+ issues,
162
+ proposals: await proposeFixes(issues),
163
+ fixResults,
164
+ logs: [...logs],
165
+ backupCleanup,
166
+ });
167
+
168
+ if (result.gitignoreUpdated) {
169
+ say(`Added ${FIX_DIR}/ to .gitignore`);
170
+ }
171
+ say(`Report created successfully: ${relative(target, result.reportPath)}`);
172
+ } else {
173
+ say("Report generation skipped.");
174
+ }
175
+ }
176
+
177
+ if (options.format === "sarif") {
178
+ console.log(formatSarif({ root: target, issues, version: await readVersion() }));
179
+ }
180
+
181
+ if (options.format === "json") {
182
+ console.log(
183
+ formatJson({
184
+ root: target,
185
+ filesScanned: files.length,
186
+ issues,
187
+ fixes: fixResults,
188
+ version: await readVersion(),
189
+ })
190
+ );
191
+ }
192
+
193
+ const failing = issues.filter(
194
+ (issue) => issue.severity === "error" || issue.severity === "warning"
195
+ ).length;
196
+
197
+ return decideExitCode({
198
+ rolledBack: fixResults.filter((item) => item.status === "rolled back").length,
199
+ issues: failing,
200
+ applied: fixResults.filter((item) => item.status === "applied").length,
201
+ });
202
+ } catch (error) {
203
+ console.error(`Error: ${error.message}`);
204
+ return EXIT.ERROR;
205
+ }
206
+ }
@@ -0,0 +1,67 @@
1
+ import { resolve, relative } from "node:path";
2
+ import { findFiles } from "../../src/core/files.js";
3
+ import { runRules } from "../../src/core/engine.js";
4
+ import { applySeverity } from "../../src/core/config.js";
5
+ import { isIncluded } from "../../src/core/args.js";
6
+ import { loadIgnorePatterns, isIgnored } from "../../src/scan/ignore.js";
7
+ import { changedFiles } from "../../src/scan/git.js";
8
+ import { applySuppressions } from "../../src/scan/suppress.js";
9
+ import { applyBaseline } from "../../src/scan/baseline.js";
10
+ import { rules } from "../../rules/index.js";
11
+
12
+ export const KNOWN_RULES = new Set(rules.map((rule) => rule.name));
13
+
14
+ export function summaryLine(issues) {
15
+ const counts = { error: 0, warning: 0, info: 0 };
16
+ for (const issue of issues) {
17
+ counts[issue.severity] = (counts[issue.severity] ?? 0) + 1;
18
+ }
19
+ return `Errors: ${counts.error} Warnings: ${counts.warning} Info: ${counts.info}`;
20
+ }
21
+
22
+ export async function selectFiles(target, options) {
23
+ const patterns = await loadIgnorePatterns(target);
24
+ const allFiles = await findFiles(target);
25
+
26
+ let files = allFiles.filter((file) => {
27
+ const rel = relative(target, file);
28
+ return !isIgnored(rel, patterns) && isIncluded(rel, options.include, options.exclude);
29
+ });
30
+
31
+ if (options.changed || options.staged) {
32
+ const changed = await changedFiles(target, options.staged ? "staged" : "changed");
33
+ files = files.filter((file) => changed.has(resolve(file)));
34
+ }
35
+
36
+ return files;
37
+ }
38
+
39
+ export async function collectIssues(files, target, disabled, config, baseline) {
40
+ const found = applySeverity(
41
+ await runRules(files, { disabledRules: disabled }),
42
+ config
43
+ );
44
+
45
+ const suppressed = await applySuppressions(found, target);
46
+ let issues = suppressed.issues;
47
+ let hidden = 0;
48
+
49
+ if (baseline) {
50
+ const applied = applyBaseline(issues, baseline, target);
51
+ issues = applied.issues;
52
+ hidden = applied.hidden;
53
+ }
54
+
55
+ return { issues, suppressed: suppressed.suppressed, hidden };
56
+ }
57
+
58
+ export function disabledSetFor(config, options) {
59
+ const disabled = new Set([...config.disabledRules, ...options.disable]);
60
+ if (options.rules.length > 0) {
61
+ const only = new Set(options.rules);
62
+ for (const rule of rules) {
63
+ if (!only.has(rule.name)) disabled.add(rule.name);
64
+ }
65
+ }
66
+ return disabled;
67
+ }
@@ -0,0 +1,44 @@
1
+ import { watch } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { runScan } from "./scan.js";
4
+
5
+ const IGNORED_SEGMENTS = new Set(["node_modules", ".git", ".vhite-fix", "dist", "build", "coverage"]);
6
+ const DEBOUNCE_MS = 300;
7
+
8
+ function isIgnoredPath(filename) {
9
+ return filename.split(/[\\/]/).some((part) => IGNORED_SEGMENTS.has(part));
10
+ }
11
+
12
+ export async function runWatch(options) {
13
+ const target = resolve(options.path ?? ".");
14
+ const scanOptions = { ...options, watching: true };
15
+ let timer = null;
16
+ let running = false;
17
+ let pending = false;
18
+
19
+ const scanNow = async () => {
20
+ if (running) {
21
+ pending = true;
22
+ return;
23
+ }
24
+ running = true;
25
+ do {
26
+ pending = false;
27
+ console.log(`\n[${new Date().toLocaleTimeString()}] Scanning...`);
28
+ await runScan(scanOptions);
29
+ } while (pending);
30
+ running = false;
31
+ };
32
+
33
+ watch(target, { recursive: true }, (_event, filename) => {
34
+ if (!filename || isIgnoredPath(filename.toString())) return;
35
+ clearTimeout(timer);
36
+ timer = setTimeout(scanNow, DEBOUNCE_MS);
37
+ });
38
+
39
+ await scanNow();
40
+ console.log("\nWatching for changes. Press Ctrl+C to stop.");
41
+
42
+ await new Promise(() => {});
43
+ return 0;
44
+ }
@@ -0,0 +1,40 @@
1
+ export const USAGE = `Usage:
2
+ vhite [path] Scan a folder (default command)
3
+ vhite scan [path] Scan and report
4
+ vhite fix [path] Scan, then choose which fixes to apply
5
+ vhite baseline [path] Save current problems so only new ones are reported
6
+ vhite rules List all rules
7
+ vhite explain <rule> Explain one rule
8
+ vhite version Show the version
9
+
10
+ Options:
11
+ --format text|json Output format (default: text)
12
+ --quiet, -q Print only errors; the exit code still applies
13
+ --fix Apply all safe fixes without asking, then rescan
14
+ --changed Scan only files changed since the last commit (Git)
15
+ --staged Scan only files staged for commit (Git)
16
+ --no-baseline Ignore vhite.baseline.json for this run
17
+ --rule <name> Run only this rule (repeatable)
18
+ --disable <name> Turn off a rule (repeatable)
19
+ --include <path> Scan only files under this path (repeatable)
20
+ --exclude <path> Skip files under this path (repeatable)
21
+ --help, -h Show this help
22
+
23
+ Exit codes:
24
+ 0 no problems
25
+ 1 problems found (errors or warnings; info notes do not count)
26
+ 2 configuration or runtime error
27
+ 3 fixes applied and no problems remain
28
+ 4 fixes were rolled back
29
+
30
+ Ignoring code:
31
+ Put // vhite-disable-next-line rule-name above a line, or
32
+ // vhite-disable-line rule-name at the end of a line,
33
+ to skip that finding. Leave out the rule name to skip every rule.`;
34
+
35
+ export const BANNER = `vhite is installed and running
36
+ loading..
37
+ ....
38
+ ......
39
+ ........
40
+ Done..`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vhite/vhite",
3
- "version": "0.1.0",
3
+ "version": "0.6.0",
4
4
  "description": "An AI code cleaner for front-end projects that finds and safely fixes common AI-generated problems",
5
5
  "license": "ISC",
6
6
  "author": "Victor Edem",
@@ -21,12 +21,39 @@
21
21
  "engines": {
22
22
  "node": ">=18"
23
23
  },
24
- "keywords": [
24
+ "keywords": [
25
25
  "ai",
26
+ "ai-code",
27
+ "ai-generated-code",
28
+ "ai-code-cleanup",
29
+ "ai-code-review",
26
30
  "code-cleanup",
31
+ "code-quality",
32
+ "code-review",
27
33
  "linter",
34
+ "lint",
35
+ "static-analysis",
36
+ "code-analysis",
28
37
  "front-end",
29
- "javascript"
38
+ "frontend",
39
+ "javascript",
40
+ "jsx",
41
+ "react",
42
+ "accessibility",
43
+ "a11y",
44
+ "wcag",
45
+ "security",
46
+ "hardcoded-secrets",
47
+ "console-log",
48
+ "debugger",
49
+ "unused-imports",
50
+ "dead-code",
51
+ "cli",
52
+ "command-line",
53
+ "ci",
54
+ "github-actions",
55
+ "pre-commit",
56
+ "vibe-coding"
30
57
  ],
31
58
  "repository": {
32
59
  "type": "git",
@@ -37,10 +64,10 @@
37
64
  },
38
65
  "homepage": "https://github.com/Wilson-Edem/vhite#readme",
39
66
  "scripts": {
40
- "test": "node --test test/*.test.js"
67
+ "test": "node --test test/*/*.test.js"
41
68
  },
42
69
  "dependencies": {
43
70
  "espree": "^10.0.1",
44
71
  "eslint-scope": "^8.4.0"
45
72
  }
46
- }
73
+ }
@@ -0,0 +1,39 @@
1
+ import { childNodes, attributeNamed, hasSpread } from "../shared/jsx-helpers.js";
2
+
3
+ const CLICKABLE_TAGS = new Set(["div", "span"]);
4
+
5
+ const a11yClickDiv = {
6
+ name: "a11y-click-div",
7
+ description: "Flags clickable div or span elements that have no role",
8
+ extensions: [".js", ".jsx", ".mjs", ".cjs"],
9
+ requiresAst: true,
10
+ check({ ast }) {
11
+ const issues = [];
12
+
13
+ function visit(node) {
14
+ if (!node || typeof node !== "object") return;
15
+
16
+ if (
17
+ node.type === "JSXOpeningElement" &&
18
+ node.name?.type === "JSXIdentifier" &&
19
+ CLICKABLE_TAGS.has(node.name.name) &&
20
+ attributeNamed(node, "onClick") &&
21
+ !attributeNamed(node, "role") &&
22
+ !hasSpread(node)
23
+ ) {
24
+ issues.push({
25
+ start: node.range?.[0],
26
+ end: node.range?.[1],
27
+ message: `Clickable <${node.name.name}> is not reachable by keyboard or announced to screen readers; use a button, or add role and keyboard handling`,
28
+ });
29
+ }
30
+
31
+ for (const child of childNodes(node)) visit(child);
32
+ }
33
+
34
+ visit(ast);
35
+ return issues;
36
+ },
37
+ };
38
+
39
+ export default a11yClickDiv;
@@ -0,0 +1,36 @@
1
+ import { childNodes, attributeNamed, hasSpread } from "../shared/jsx-helpers.js";
2
+
3
+ const a11yImgAlt = {
4
+ name: "a11y-img-alt",
5
+ description: "Flags images that have no alt attribute",
6
+ extensions: [".js", ".jsx", ".mjs", ".cjs"],
7
+ requiresAst: true,
8
+ check({ ast }) {
9
+ const issues = [];
10
+
11
+ function visit(node) {
12
+ if (!node || typeof node !== "object") return;
13
+
14
+ if (
15
+ node.type === "JSXOpeningElement" &&
16
+ node.name?.type === "JSXIdentifier" &&
17
+ node.name.name === "img" &&
18
+ !hasSpread(node) &&
19
+ !attributeNamed(node, "alt")
20
+ ) {
21
+ issues.push({
22
+ start: node.range?.[0],
23
+ end: node.range?.[1],
24
+ message: "Image has no alt attribute; add a description, or alt=\"\" if it is decorative",
25
+ });
26
+ }
27
+
28
+ for (const child of childNodes(node)) visit(child);
29
+ }
30
+
31
+ visit(ast);
32
+ return issues;
33
+ },
34
+ };
35
+
36
+ export default a11yImgAlt;
@@ -0,0 +1,40 @@
1
+ import { childNodes, attributeNamed, hasSpread, literalValue } from "../shared/jsx-helpers.js";
2
+
3
+ const NO_LABEL_TYPES = new Set(["hidden", "submit", "button", "reset", "image"]);
4
+ const LABEL_ATTRIBUTES = ["aria-label", "aria-labelledby", "id", "title"];
5
+
6
+ const a11yInputLabel = {
7
+ name: "a11y-input-label",
8
+ description: "Flags form inputs with no label or accessible name attribute",
9
+ extensions: [".js", ".jsx", ".mjs", ".cjs"],
10
+ requiresAst: true,
11
+ check({ ast }) {
12
+ const issues = [];
13
+
14
+ function visit(node) {
15
+ if (!node || typeof node !== "object") return;
16
+
17
+ if (
18
+ node.type === "JSXOpeningElement" &&
19
+ node.name?.type === "JSXIdentifier" &&
20
+ node.name.name === "input" &&
21
+ !hasSpread(node) &&
22
+ !NO_LABEL_TYPES.has(literalValue(attributeNamed(node, "type"))) &&
23
+ !LABEL_ATTRIBUTES.some((name) => attributeNamed(node, name))
24
+ ) {
25
+ issues.push({
26
+ start: node.range?.[0],
27
+ end: node.range?.[1],
28
+ message: "Form input has no label; connect it to a label element, or add aria-label",
29
+ });
30
+ }
31
+
32
+ for (const child of childNodes(node)) visit(child);
33
+ }
34
+
35
+ visit(ast);
36
+ return issues;
37
+ },
38
+ };
39
+
40
+ export default a11yInputLabel;
@@ -19,7 +19,8 @@ const consoleLog = {
19
19
  node.callee.property.name === "log"
20
20
  ) {
21
21
  issues.push({
22
- line: node.loc?.start.line ?? 1,
22
+ start: node.range?.[0],
23
+ end: node.range?.[1],
23
24
  message: "Review or remove this console.log call",
24
25
  });
25
26
  }
@@ -11,7 +11,8 @@ const debuggerStatement = {
11
11
 
12
12
  if (node.type === "DebuggerStatement") {
13
13
  issues.push({
14
- line: node.loc?.start.line ?? 1,
14
+ start: node.range?.[0],
15
+ end: node.range?.[1],
15
16
  message: "Remove the debugger statement before shipping",
16
17
  });
17
18
  }
@@ -16,7 +16,8 @@ const emptyBlock = {
16
16
  key !== "body"
17
17
  ) {
18
18
  issues.push({
19
- line: node.loc?.start.line ?? 1,
19
+ start: node.range?.[0],
20
+ end: node.range?.[1],
20
21
  message: "Review this empty block; it may indicate missing logic",
21
22
  });
22
23
  }
@@ -15,7 +15,8 @@ const emptyCatch = {
15
15
  node.body.body.length === 0
16
16
  ) {
17
17
  issues.push({
18
- line: node.loc?.start.line ?? 1,
18
+ start: node.range?.[0],
19
+ end: node.range?.[1],
19
20
  message: "Handle, log, or explicitly document this caught error",
20
21
  });
21
22
  }
@@ -17,20 +17,25 @@ const hardcodedSecret = {
17
17
  const issues = [];
18
18
  const lines = source.split("\n");
19
19
 
20
- lines.forEach((text, index) => {
20
+ let lineStart = 0;
21
+ lines.forEach((text) => {
21
22
  for (const item of SECRET_PATTERNS) {
22
- if (item.pattern.test(text)) {
23
+ const match = item.pattern.exec(text);
24
+ if (match) {
25
+ const start = lineStart + match.index;
23
26
  issues.push({
24
- line: index + 1,
27
+ start,
28
+ end: start + match[0].length,
25
29
  message: `Possible hardcoded secret (${item.label}); move it to a secure environment variable and rotate it if real`,
26
30
  });
27
31
  break;
28
32
  }
29
33
  }
34
+ lineStart += text.length + 1;
30
35
  });
31
36
 
32
37
  return issues;
33
38
  },
34
39
  };
35
40
 
36
- export default hardcodedSecret;
41
+ export default hardcodedSecret;
@@ -5,12 +5,16 @@ const leftoverTodo = {
5
5
  description: "Flags comments left in code, with TODO and FIXME notes called out separately",
6
6
  extensions: [".js", ".jsx", ".mjs", ".cjs"],
7
7
  requiresAst: true,
8
- check({ ast }) {
8
+ check({ ast, source }) {
9
9
  const issues = [];
10
10
  for (const comment of ast.comments ?? []) {
11
+ if (comment.range?.[0] === 0 && source?.startsWith("#!")) continue;
12
+ if (comment.value.includes("vhite-disable")) continue;
13
+
11
14
  const isTodo = todoPattern.test(comment.value);
12
15
  issues.push({
13
- line: comment.loc.start.line,
16
+ start: comment.range?.[0],
17
+ end: comment.range?.[1],
14
18
  message: isTodo ? "Leftover TODO or FIXME note" : "Code comment left in the file",
15
19
  comment: {
16
20
  type: comment.type,
@@ -52,8 +52,10 @@ const unusedImport = {
52
52
 
53
53
  if (!hasRead && !usedInJsx && !implicitReact) {
54
54
  const definition = variable.defs.find((item) => item.type === "ImportBinding");
55
+ const target = definition?.name ?? variable.identifiers[0];
55
56
  issues.push({
56
- line: definition?.name?.loc?.start.line ?? variable.identifiers[0]?.loc?.start.line ?? 1,
57
+ start: target?.range?.[0],
58
+ end: target?.range?.[1],
57
59
  message: `Imported binding "${variable.name}" is never used`,
58
60
  });
59
61
  }
package/rules/index.js CHANGED
@@ -1,10 +1,15 @@
1
- import leftoverTodo from "./leftover-todo.js";
2
- import debuggerStatement from "./debugger-statement.js";
3
- import consoleLog from "./console-log.js";
4
- import emptyCatch from "./empty-catch.js";
5
- import unusedImport from "./unused-import.js";
6
- import emptyBlock from "./empty-block.js";
7
- import hardcodedSecret from "./hardcoded-secret.js";
1
+ import leftoverTodo from "./core/leftover-todo.js";
2
+ import debuggerStatement from "./core/debugger-statement.js";
3
+ import consoleLog from "./core/console-log.js";
4
+ import emptyCatch from "./core/empty-catch.js";
5
+ import unusedImport from "./core/unused-import.js";
6
+ import emptyBlock from "./core/empty-block.js";
7
+ import hardcodedSecret from "./core/hardcoded-secret.js";
8
+ import reactMissingKey from "./react/react-missing-key.js";
9
+ import reactDangerousHtml from "./react/react-dangerous-html.js";
10
+ import a11yImgAlt from "./a11y/a11y-img-alt.js";
11
+ import a11yInputLabel from "./a11y/a11y-input-label.js";
12
+ import a11yClickDiv from "./a11y/a11y-click-div.js";
8
13
 
9
14
  export const rules = [
10
15
  leftoverTodo,
@@ -14,4 +19,9 @@ export const rules = [
14
19
  unusedImport,
15
20
  emptyBlock,
16
21
  hardcodedSecret,
17
- ];
22
+ reactMissingKey,
23
+ reactDangerousHtml,
24
+ a11yImgAlt,
25
+ a11yInputLabel,
26
+ a11yClickDiv,
27
+ ];