@vhite/vhite 0.0.0-stage → 0.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/README.md +56 -2
- package/bin/cli.js +216 -0
- package/package.json +43 -3
- package/rules/console-log.js +43 -0
- package/rules/debugger-statement.js +35 -0
- package/rules/empty-block.js +40 -0
- package/rules/empty-catch.js +39 -0
- package/rules/hardcoded-secret.js +36 -0
- package/rules/index.js +17 -0
- package/rules/leftover-todo.js +26 -0
- package/rules/unused-import.js +67 -0
- package/src/approve.js +71 -0
- package/src/backup.js +73 -0
- package/src/config.js +42 -0
- package/src/engine.js +80 -0
- package/src/explanations.js +35 -0
- package/src/files.js +21 -0
- package/src/fixers/debugger-statement.js +12 -0
- package/src/fixers/index.js +9 -0
- package/src/fixers/leftover-todo.js +32 -0
- package/src/fixers/unused-import.js +18 -0
- package/src/gitignore.js +43 -0
- package/src/reporter.js +33 -0
- package/src/run-report.js +194 -0
- package/src/safe-fix.js +178 -0
- package/src/setup.js +114 -0
package/README.md
CHANGED
|
@@ -1,3 +1,57 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @vhite/vhite
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
vhite scans front-end code for common problems left behind by AI code generators, explains each one in plain English, and can safely fix some of them.
|
|
4
|
+
|
|
5
|
+
## Install and run
|
|
6
|
+
|
|
7
|
+
Run it on any project folder without installing it:
|
|
8
|
+
|
|
9
|
+
npx @vhite/vhite path/to/your/project
|
|
10
|
+
|
|
11
|
+
Or run it on the current folder:
|
|
12
|
+
|
|
13
|
+
npx @vhite/vhite .
|
|
14
|
+
|
|
15
|
+
## What it checks
|
|
16
|
+
|
|
17
|
+
- **Debugger statements:** `debugger` lines that pause the app when developer tools are open.
|
|
18
|
+
- **console.log calls:** logging left over from debugging.
|
|
19
|
+
- **Empty catch blocks:** errors that are caught and silently ignored.
|
|
20
|
+
- **Unused imports:** imports the file never uses.
|
|
21
|
+
- **Empty blocks:** `if`, `else`, and loop bodies with no code.
|
|
22
|
+
- **Hardcoded secrets:** passwords, API keys, and tokens written directly in the code.
|
|
23
|
+
- **Comments:** every comment is flagged. TODO and FIXME notes get their own message.
|
|
24
|
+
|
|
25
|
+
Supported file types: `.js`, `.jsx`, `.mjs`, `.cjs`, `.html`, and `.css`. Some checks only apply to JavaScript files.
|
|
26
|
+
|
|
27
|
+
## Safe fixes
|
|
28
|
+
|
|
29
|
+
Some problems can be fixed automatically: debugger statements, unused imports, and comments. vhite never changes a file without asking first.
|
|
30
|
+
|
|
31
|
+
1. It scans the project and lists the problems it found.
|
|
32
|
+
2. It lists the fixes it can make and asks which ones to apply. Enter numbers such as `1,3`, type `all`, or press Enter to skip.
|
|
33
|
+
3. Before each change, it saves a backup in `.vhite-fix/backup/`.
|
|
34
|
+
4. After each change, it scans the file again. If the change created a new problem or didn't fix the issue, the original is restored automatically.
|
|
35
|
+
5. At the end, it can ask whether to delete the backups from this run and whether to create a Markdown report in `.vhite-fix/reports/`.
|
|
36
|
+
|
|
37
|
+
The first time a fix is applied, vhite adds `.vhite-fix/` to your `.gitignore`, so backups and reports stay out of your repository.
|
|
38
|
+
|
|
39
|
+
Fixes only run in an interactive terminal. In scripts or CI, vhite only reports problems.
|
|
40
|
+
|
|
41
|
+
## Settings
|
|
42
|
+
|
|
43
|
+
Create a `vhite.config.json` file in your project to turn off specific checks:
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
"disabledRules": ["console-log", "leftover-todo"]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Use the rule names shown in the report, in brackets at the end of each line.
|
|
50
|
+
|
|
51
|
+
## Limits
|
|
52
|
+
|
|
53
|
+
vhite finds common patterns. It does not understand your whole app, so review each fix before you commit it. Removing an unused import also stops that module from loading, which is usually what you want, but check modules that only run for their side effects.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
ISC
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { resolve, join, relative } from "node:path";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
import { findFiles } from "../src/files.js";
|
|
7
|
+
import { runRules } from "../src/engine.js";
|
|
8
|
+
import { formatReport } from "../src/reporter.js";
|
|
9
|
+
import { loadConfig } from "../src/config.js";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
CONFIG_NAME,
|
|
13
|
+
canAskQuestions,
|
|
14
|
+
runFirstTimeSetup,
|
|
15
|
+
} from "../src/setup.js";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
proposeFixes,
|
|
19
|
+
applyApprovedFixes,
|
|
20
|
+
} from "../src/safe-fix.js";
|
|
21
|
+
|
|
22
|
+
import { chooseProposals } from "../src/approve.js";
|
|
23
|
+
import { deleteRunBackups } from "../src/backup.js";
|
|
24
|
+
import { FIX_DIR } from "../src/gitignore.js";
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
askYesNo,
|
|
28
|
+
writeRunReport,
|
|
29
|
+
} from "../src/run-report.js";
|
|
30
|
+
|
|
31
|
+
// Capture terminal messages so the report can include them.
|
|
32
|
+
const logs = [];
|
|
33
|
+
|
|
34
|
+
const originalLog = console.log;
|
|
35
|
+
const originalWarn = console.warn;
|
|
36
|
+
const originalError = console.error;
|
|
37
|
+
|
|
38
|
+
function capture(method, label = "") {
|
|
39
|
+
return (...args) => {
|
|
40
|
+
const message = args
|
|
41
|
+
.map((arg) => {
|
|
42
|
+
if (typeof arg === "string") {
|
|
43
|
+
return arg;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
return JSON.stringify(arg) ?? String(arg);
|
|
48
|
+
} catch {
|
|
49
|
+
return String(arg);
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
.join(" ");
|
|
53
|
+
|
|
54
|
+
logs.push(label ? `${label}: ${message}` : message);
|
|
55
|
+
method(...args);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log = capture(originalLog);
|
|
60
|
+
console.warn = capture(originalWarn, "WARN");
|
|
61
|
+
console.error = capture(originalError, "ERROR");
|
|
62
|
+
|
|
63
|
+
const startedAt = new Date();
|
|
64
|
+
const target = resolve(process.argv[2] ?? ".");
|
|
65
|
+
|
|
66
|
+
let files = [];
|
|
67
|
+
let issues = [];
|
|
68
|
+
let proposals = [];
|
|
69
|
+
let fixResults = [];
|
|
70
|
+
let backupCleanup = null;
|
|
71
|
+
let config = null;
|
|
72
|
+
|
|
73
|
+
console.log(
|
|
74
|
+
`vhite is installed and running
|
|
75
|
+
loading..
|
|
76
|
+
....
|
|
77
|
+
......
|
|
78
|
+
........
|
|
79
|
+
Done..`
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
if (
|
|
84
|
+
!existsSync(join(target, CONFIG_NAME)) &&
|
|
85
|
+
canAskQuestions()
|
|
86
|
+
) {
|
|
87
|
+
await runFirstTimeSetup(target);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
config = await loadConfig(target);
|
|
91
|
+
|
|
92
|
+
for (const warning of config.warnings) {
|
|
93
|
+
console.log(`Warning: ${warning}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (config.disabledRules.size > 0) {
|
|
97
|
+
console.log(
|
|
98
|
+
`Rules turned off by ${CONFIG_NAME}: ${[...config.disabledRules].join(", ")}`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
files = await findFiles(target);
|
|
103
|
+
|
|
104
|
+
console.log(
|
|
105
|
+
`vhite found ${files.length} front-end files in ${relative(process.cwd(), target) || "."}\n`
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
issues = await runRules(files, {
|
|
109
|
+
disabledRules: config.disabledRules,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
console.log(formatReport(issues, target));
|
|
113
|
+
proposals = await proposeFixes(issues);
|
|
114
|
+
|
|
115
|
+
if (proposals.length > 0 && canAskQuestions()) {
|
|
116
|
+
const chosen = await chooseProposals(target, proposals);
|
|
117
|
+
|
|
118
|
+
if (chosen.length > 0) {
|
|
119
|
+
const result = await applyApprovedFixes(
|
|
120
|
+
target,
|
|
121
|
+
chosen,
|
|
122
|
+
{ disabledRules: config.disabledRules }
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
fixResults = result.results;
|
|
126
|
+
|
|
127
|
+
if (result.gitignoreUpdated) {
|
|
128
|
+
console.log(`Added ${FIX_DIR}/ to .gitignore`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const item of fixResults) {
|
|
132
|
+
if (item.status === "applied") {
|
|
133
|
+
console.log(
|
|
134
|
+
`Applied: ${relative(target, item.proposal.file)} line ${item.proposal.line}`
|
|
135
|
+
);
|
|
136
|
+
} else {
|
|
137
|
+
const label = item.status === "skipped" ? "Skipped" : "Rolled back";
|
|
138
|
+
console.log(
|
|
139
|
+
`${label}: ${relative(target, item.proposal.file)} line ${item.proposal.line}. ${item.reason}`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Feature 1: Ask whether to remove current-run backups.
|
|
147
|
+
const hasRunBackups = fixResults.some(
|
|
148
|
+
(item) => typeof item.backupPath === "string"
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
if (canAskQuestions() && hasRunBackups) {
|
|
152
|
+
const shouldDelete = await askYesNo(
|
|
153
|
+
"Do you want to delete the backup files created during this run?"
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (shouldDelete) {
|
|
157
|
+
const result = await deleteRunBackups(fixResults);
|
|
158
|
+
backupCleanup = {
|
|
159
|
+
requested: true,
|
|
160
|
+
deleted: result.deleted,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
console.log(
|
|
164
|
+
`Deleted ${result.deleted} backup file(s) created during this run.`
|
|
165
|
+
);
|
|
166
|
+
} else {
|
|
167
|
+
backupCleanup = {
|
|
168
|
+
requested: false,
|
|
169
|
+
deleted: 0,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
console.log("Kept the backup files created during this run.");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log("\nVhite run completed.");
|
|
177
|
+
|
|
178
|
+
// Feature 2: Offer a detailed Markdown report.
|
|
179
|
+
if (canAskQuestions()) {
|
|
180
|
+
const shouldCreateReport = await askYesNo(
|
|
181
|
+
"Would you like to create a detailed Markdown report of this run?"
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
if (shouldCreateReport) {
|
|
185
|
+
// Include the log messages generated before report creation.
|
|
186
|
+
const reportLogs = [...logs];
|
|
187
|
+
const finishedAt = new Date();
|
|
188
|
+
|
|
189
|
+
const result = await writeRunReport({
|
|
190
|
+
root: target,
|
|
191
|
+
target,
|
|
192
|
+
startedAt,
|
|
193
|
+
finishedAt,
|
|
194
|
+
filesScanned: files.length,
|
|
195
|
+
issues,
|
|
196
|
+
proposals,
|
|
197
|
+
fixResults,
|
|
198
|
+
logs: reportLogs,
|
|
199
|
+
backupCleanup,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (result.gitignoreUpdated) {
|
|
203
|
+
console.log(`Added ${FIX_DIR}/ to .gitignore`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log(
|
|
207
|
+
`Report created successfully: ${relative(target, result.reportPath)}`
|
|
208
|
+
);
|
|
209
|
+
} else {
|
|
210
|
+
console.log("Report generation skipped.");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error(`Error: ${error.message}`);
|
|
215
|
+
process.exitCode = 1;
|
|
216
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vhite/vhite",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "An AI code cleaner for front-end projects that finds and safely fixes common AI-generated problems",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"author": "Victor Edem",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "bin/cli.js",
|
|
9
|
+
"bin": {
|
|
10
|
+
"vhite": "bin/cli.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"src",
|
|
15
|
+
"rules",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"ai",
|
|
26
|
+
"code-cleanup",
|
|
27
|
+
"linter",
|
|
28
|
+
"front-end",
|
|
29
|
+
"javascript"
|
|
30
|
+
],
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/Wilson-Edem/vhite"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/Wilson-Edem/vhite/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/Wilson-Edem/vhite#readme",
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "node --test test/*.test.js"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"espree": "^10.0.1",
|
|
44
|
+
"eslint-scope": "^8.4.0"
|
|
45
|
+
}
|
|
6
46
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const consoleLog = {
|
|
2
|
+
name: "console-log",
|
|
3
|
+
description: "Flags console.log calls that may have been left during debugging",
|
|
4
|
+
extensions: [".js", ".jsx", ".mjs", ".cjs"],
|
|
5
|
+
requiresAst: true,
|
|
6
|
+
check({ ast }) {
|
|
7
|
+
const issues = [];
|
|
8
|
+
|
|
9
|
+
function visit(node) {
|
|
10
|
+
if (!node || typeof node !== "object") return;
|
|
11
|
+
|
|
12
|
+
if (
|
|
13
|
+
node.type === "CallExpression" &&
|
|
14
|
+
node.callee?.type === "MemberExpression" &&
|
|
15
|
+
!node.callee.computed &&
|
|
16
|
+
node.callee.object?.type === "Identifier" &&
|
|
17
|
+
node.callee.object.name === "console" &&
|
|
18
|
+
node.callee.property?.type === "Identifier" &&
|
|
19
|
+
node.callee.property.name === "log"
|
|
20
|
+
) {
|
|
21
|
+
issues.push({
|
|
22
|
+
line: node.loc?.start.line ?? 1,
|
|
23
|
+
message: "Review or remove this console.log call",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const value of Object.values(node)) {
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
for (const child of value) {
|
|
30
|
+
if (child && typeof child.type === "string") visit(child);
|
|
31
|
+
}
|
|
32
|
+
} else if (value && typeof value.type === "string") {
|
|
33
|
+
visit(value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
visit(ast);
|
|
39
|
+
return issues;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default consoleLog;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const debuggerStatement = {
|
|
2
|
+
name: "debugger-statement",
|
|
3
|
+
description: "Flags debugger statements left in source code",
|
|
4
|
+
extensions: [".js", ".jsx", ".mjs", ".cjs"],
|
|
5
|
+
requiresAst: true,
|
|
6
|
+
check({ ast }) {
|
|
7
|
+
const issues = [];
|
|
8
|
+
|
|
9
|
+
function visit(node) {
|
|
10
|
+
if (!node || typeof node !== "object") return;
|
|
11
|
+
|
|
12
|
+
if (node.type === "DebuggerStatement") {
|
|
13
|
+
issues.push({
|
|
14
|
+
line: node.loc?.start.line ?? 1,
|
|
15
|
+
message: "Remove the debugger statement before shipping",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
for (const value of Object.values(node)) {
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
for (const child of value) {
|
|
22
|
+
if (child && typeof child.type === "string") visit(child);
|
|
23
|
+
}
|
|
24
|
+
} else if (value && typeof value.type === "string") {
|
|
25
|
+
visit(value);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
visit(ast);
|
|
31
|
+
return issues;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default debuggerStatement;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const emptyBlock = {
|
|
2
|
+
name: "empty-block",
|
|
3
|
+
description: "Flags empty control-flow blocks that may hide unfinished logic",
|
|
4
|
+
extensions: [".js", ".jsx", ".mjs", ".cjs"],
|
|
5
|
+
requiresAst: true,
|
|
6
|
+
check({ ast }) {
|
|
7
|
+
const issues = [];
|
|
8
|
+
|
|
9
|
+
function visit(node, parent = null, key = null) {
|
|
10
|
+
if (!node || typeof node !== "object") return;
|
|
11
|
+
|
|
12
|
+
if (
|
|
13
|
+
node.type === "BlockStatement" &&
|
|
14
|
+
node.body.length === 0 &&
|
|
15
|
+
parent?.type !== "CatchClause" &&
|
|
16
|
+
key !== "body"
|
|
17
|
+
) {
|
|
18
|
+
issues.push({
|
|
19
|
+
line: node.loc?.start.line ?? 1,
|
|
20
|
+
message: "Review this empty block; it may indicate missing logic",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const [childKey, value] of Object.entries(node)) {
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
for (const child of value) {
|
|
27
|
+
if (child && typeof child.type === "string") visit(child, node, childKey);
|
|
28
|
+
}
|
|
29
|
+
} else if (value && typeof value.type === "string") {
|
|
30
|
+
visit(value, node, childKey);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
visit(ast);
|
|
36
|
+
return issues;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default emptyBlock;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const emptyCatch = {
|
|
2
|
+
name: "empty-catch",
|
|
3
|
+
description: "Flags catch clauses that silently ignore errors",
|
|
4
|
+
extensions: [".js", ".jsx", ".mjs", ".cjs"],
|
|
5
|
+
requiresAst: true,
|
|
6
|
+
check({ ast }) {
|
|
7
|
+
const issues = [];
|
|
8
|
+
|
|
9
|
+
function visit(node) {
|
|
10
|
+
if (!node || typeof node !== "object") return;
|
|
11
|
+
|
|
12
|
+
if (
|
|
13
|
+
node.type === "CatchClause" &&
|
|
14
|
+
node.body?.type === "BlockStatement" &&
|
|
15
|
+
node.body.body.length === 0
|
|
16
|
+
) {
|
|
17
|
+
issues.push({
|
|
18
|
+
line: node.loc?.start.line ?? 1,
|
|
19
|
+
message: "Handle, log, or explicitly document this caught error",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const value of Object.values(node)) {
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
for (const child of value) {
|
|
26
|
+
if (child && typeof child.type === "string") visit(child);
|
|
27
|
+
}
|
|
28
|
+
} else if (value && typeof value.type === "string") {
|
|
29
|
+
visit(value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
visit(ast);
|
|
35
|
+
return issues;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default emptyCatch;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const SECRET_PATTERNS = [
|
|
2
|
+
{
|
|
3
|
+
pattern: /\b(?:api[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|secret[_-]?key|password)\b\s*[:=]\s*["'`][^"'`\s]{8,}["'`]/i,
|
|
4
|
+
label: "credential-like value",
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
pattern: /\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16})\b/,
|
|
8
|
+
label: "token-like value",
|
|
9
|
+
},
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const hardcodedSecret = {
|
|
13
|
+
name: "hardcoded-secret",
|
|
14
|
+
description: "Heuristically flags possible hardcoded credentials or API tokens",
|
|
15
|
+
extensions: [".js", ".jsx", ".mjs", ".cjs", ".html", ".css"],
|
|
16
|
+
check({ source }) {
|
|
17
|
+
const issues = [];
|
|
18
|
+
const lines = source.split("\n");
|
|
19
|
+
|
|
20
|
+
lines.forEach((text, index) => {
|
|
21
|
+
for (const item of SECRET_PATTERNS) {
|
|
22
|
+
if (item.pattern.test(text)) {
|
|
23
|
+
issues.push({
|
|
24
|
+
line: index + 1,
|
|
25
|
+
message: `Possible hardcoded secret (${item.label}); move it to a secure environment variable and rotate it if real`,
|
|
26
|
+
});
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return issues;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default hardcodedSecret;
|
package/rules/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
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";
|
|
8
|
+
|
|
9
|
+
export const rules = [
|
|
10
|
+
leftoverTodo,
|
|
11
|
+
debuggerStatement,
|
|
12
|
+
consoleLog,
|
|
13
|
+
emptyCatch,
|
|
14
|
+
unusedImport,
|
|
15
|
+
emptyBlock,
|
|
16
|
+
hardcodedSecret,
|
|
17
|
+
];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const todoPattern = /\b(TODO|FIXME)\b/;
|
|
2
|
+
|
|
3
|
+
const leftoverTodo = {
|
|
4
|
+
name: "leftover-todo",
|
|
5
|
+
description: "Flags comments left in code, with TODO and FIXME notes called out separately",
|
|
6
|
+
extensions: [".js", ".jsx", ".mjs", ".cjs"],
|
|
7
|
+
requiresAst: true,
|
|
8
|
+
check({ ast }) {
|
|
9
|
+
const issues = [];
|
|
10
|
+
for (const comment of ast.comments ?? []) {
|
|
11
|
+
const isTodo = todoPattern.test(comment.value);
|
|
12
|
+
issues.push({
|
|
13
|
+
line: comment.loc.start.line,
|
|
14
|
+
message: isTodo ? "Leftover TODO or FIXME note" : "Code comment left in the file",
|
|
15
|
+
comment: {
|
|
16
|
+
type: comment.type,
|
|
17
|
+
value: comment.value,
|
|
18
|
+
range: comment.range,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return issues;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default leftoverTodo;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { analyze } from "eslint-scope";
|
|
2
|
+
|
|
3
|
+
const unusedImport = {
|
|
4
|
+
name: "unused-import",
|
|
5
|
+
description: "Flags imported bindings that are never read",
|
|
6
|
+
extensions: [".js", ".jsx", ".mjs", ".cjs"],
|
|
7
|
+
requiresAst: true,
|
|
8
|
+
check({ ast }) {
|
|
9
|
+
const issues = [];
|
|
10
|
+
const jsxNames = new Set();
|
|
11
|
+
let hasJsx = false;
|
|
12
|
+
|
|
13
|
+
function collectJsx(node) {
|
|
14
|
+
if (!node || typeof node !== "object") return;
|
|
15
|
+
if (node.type === "JSXIdentifier") jsxNames.add(node.name);
|
|
16
|
+
if (node.type === "JSXOpeningElement" || node.type === "JSXFragment") hasJsx = true;
|
|
17
|
+
|
|
18
|
+
for (const [key, value] of Object.entries(node)) {
|
|
19
|
+
if (["loc", "range", "tokens", "comments"].includes(key)) continue;
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
for (const child of value) {
|
|
22
|
+
if (child && typeof child.type === "string") collectJsx(child);
|
|
23
|
+
}
|
|
24
|
+
} else if (value && typeof value.type === "string") {
|
|
25
|
+
collectJsx(value);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
collectJsx(ast);
|
|
31
|
+
|
|
32
|
+
let scopeManager;
|
|
33
|
+
try {
|
|
34
|
+
scopeManager = analyze(ast, {
|
|
35
|
+
ecmaVersion: 2022,
|
|
36
|
+
sourceType: "module",
|
|
37
|
+
optimistic: true,
|
|
38
|
+
ignoreEval: true,
|
|
39
|
+
});
|
|
40
|
+
} catch {
|
|
41
|
+
return issues;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const scope of scopeManager.scopes) {
|
|
45
|
+
for (const variable of scope.variables) {
|
|
46
|
+
const isImport = variable.defs?.some((definition) => definition.type === "ImportBinding");
|
|
47
|
+
if (!isImport) continue;
|
|
48
|
+
|
|
49
|
+
const hasRead = variable.references.some((reference) => reference.isRead());
|
|
50
|
+
const usedInJsx = jsxNames.has(variable.name);
|
|
51
|
+
const implicitReact = variable.name === "React" && hasJsx;
|
|
52
|
+
|
|
53
|
+
if (!hasRead && !usedInJsx && !implicitReact) {
|
|
54
|
+
const definition = variable.defs.find((item) => item.type === "ImportBinding");
|
|
55
|
+
issues.push({
|
|
56
|
+
line: definition?.name?.loc?.start.line ?? variable.identifiers[0]?.loc?.start.line ?? 1,
|
|
57
|
+
message: `Imported binding "${variable.name}" is never used`,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return issues;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export default unusedImport;
|