vibecop 0.1.2 → 0.2.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 +133 -28
- package/dist/cli.js +1188 -700
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -44,6 +44,7 @@ var __export = (target, all) => {
|
|
|
44
44
|
set: __exportSetter.bind(all, name)
|
|
45
45
|
});
|
|
46
46
|
};
|
|
47
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
47
48
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
48
49
|
|
|
49
50
|
// node_modules/commander/lib/error.js
|
|
@@ -9044,9 +9045,274 @@ var require_dist = __commonJS((exports) => {
|
|
|
9044
9045
|
exports.visitAsync = visit.visitAsync;
|
|
9045
9046
|
});
|
|
9046
9047
|
|
|
9047
|
-
// src/
|
|
9048
|
+
// src/init.ts
|
|
9049
|
+
var exports_init = {};
|
|
9050
|
+
__export(exports_init, {
|
|
9051
|
+
runInit: () => runInit
|
|
9052
|
+
});
|
|
9048
9053
|
import { execSync } from "node:child_process";
|
|
9049
|
-
import { existsSync as existsSync4, readFileSync as
|
|
9054
|
+
import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync6, writeFileSync } from "node:fs";
|
|
9055
|
+
import { join as join5 } from "node:path";
|
|
9056
|
+
function detectTools(cwd) {
|
|
9057
|
+
const tools = [];
|
|
9058
|
+
tools.push({
|
|
9059
|
+
name: "Claude Code",
|
|
9060
|
+
detected: existsSync4(join5(cwd, ".claude")),
|
|
9061
|
+
reason: existsSync4(join5(cwd, ".claude")) ? ".claude/ directory found" : "not found"
|
|
9062
|
+
});
|
|
9063
|
+
tools.push({
|
|
9064
|
+
name: "Cursor",
|
|
9065
|
+
detected: existsSync4(join5(cwd, ".cursor")),
|
|
9066
|
+
reason: existsSync4(join5(cwd, ".cursor")) ? ".cursor/ directory found" : "not found"
|
|
9067
|
+
});
|
|
9068
|
+
tools.push({
|
|
9069
|
+
name: "Codex CLI",
|
|
9070
|
+
detected: existsSync4(join5(cwd, ".codex")),
|
|
9071
|
+
reason: existsSync4(join5(cwd, ".codex")) ? ".codex/ directory found" : "not found"
|
|
9072
|
+
});
|
|
9073
|
+
let aiderInstalled = false;
|
|
9074
|
+
try {
|
|
9075
|
+
execSync("which aider", { stdio: "pipe" });
|
|
9076
|
+
aiderInstalled = true;
|
|
9077
|
+
} catch {
|
|
9078
|
+
aiderInstalled = false;
|
|
9079
|
+
}
|
|
9080
|
+
tools.push({
|
|
9081
|
+
name: "Aider",
|
|
9082
|
+
detected: aiderInstalled,
|
|
9083
|
+
reason: aiderInstalled ? "aider installed" : "not found"
|
|
9084
|
+
});
|
|
9085
|
+
tools.push({
|
|
9086
|
+
name: "Windsurf",
|
|
9087
|
+
detected: existsSync4(join5(cwd, ".windsurf")),
|
|
9088
|
+
reason: existsSync4(join5(cwd, ".windsurf")) ? ".windsurf/ directory found" : "not found"
|
|
9089
|
+
});
|
|
9090
|
+
tools.push({
|
|
9091
|
+
name: "GitHub Copilot",
|
|
9092
|
+
detected: existsSync4(join5(cwd, ".github")),
|
|
9093
|
+
reason: existsSync4(join5(cwd, ".github")) ? ".github/ directory found" : "not found"
|
|
9094
|
+
});
|
|
9095
|
+
const clineDetected = existsSync4(join5(cwd, ".cline")) || existsSync4(join5(cwd, ".clinerules"));
|
|
9096
|
+
tools.push({
|
|
9097
|
+
name: "Cline",
|
|
9098
|
+
detected: clineDetected,
|
|
9099
|
+
reason: clineDetected ? existsSync4(join5(cwd, ".cline")) ? ".cline/ directory found" : ".clinerules found" : "not found"
|
|
9100
|
+
});
|
|
9101
|
+
return tools;
|
|
9102
|
+
}
|
|
9103
|
+
function generateConfigs(cwd, tools) {
|
|
9104
|
+
const generated = [];
|
|
9105
|
+
for (const tool of tools) {
|
|
9106
|
+
if (!tool.detected)
|
|
9107
|
+
continue;
|
|
9108
|
+
switch (tool.name) {
|
|
9109
|
+
case "Claude Code": {
|
|
9110
|
+
const settingsPath = join5(cwd, ".claude", "settings.json");
|
|
9111
|
+
if (existsSync4(settingsPath)) {
|
|
9112
|
+
generated.push({
|
|
9113
|
+
path: ".claude/settings.json",
|
|
9114
|
+
description: "already exists, skipped"
|
|
9115
|
+
});
|
|
9116
|
+
} else {
|
|
9117
|
+
mkdirSync(join5(cwd, ".claude"), { recursive: true });
|
|
9118
|
+
const settings = {
|
|
9119
|
+
hooks: {
|
|
9120
|
+
PostToolUse: [
|
|
9121
|
+
{
|
|
9122
|
+
matcher: "Edit|Write|MultiEdit",
|
|
9123
|
+
hooks: [
|
|
9124
|
+
{
|
|
9125
|
+
type: "command",
|
|
9126
|
+
command: SCAN_CMD
|
|
9127
|
+
}
|
|
9128
|
+
]
|
|
9129
|
+
}
|
|
9130
|
+
]
|
|
9131
|
+
}
|
|
9132
|
+
};
|
|
9133
|
+
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
9134
|
+
`);
|
|
9135
|
+
generated.push({
|
|
9136
|
+
path: ".claude/settings.json",
|
|
9137
|
+
description: "PostToolUse hook (blocks on findings)"
|
|
9138
|
+
});
|
|
9139
|
+
}
|
|
9140
|
+
break;
|
|
9141
|
+
}
|
|
9142
|
+
case "Cursor": {
|
|
9143
|
+
mkdirSync(join5(cwd, ".cursor"), { recursive: true });
|
|
9144
|
+
const hooksPath = join5(cwd, ".cursor", "hooks.json");
|
|
9145
|
+
const hooks = {
|
|
9146
|
+
hooks: {
|
|
9147
|
+
afterFileEdit: [{ command: SCAN_CMD }]
|
|
9148
|
+
}
|
|
9149
|
+
};
|
|
9150
|
+
writeFileSync(hooksPath, `${JSON.stringify(hooks, null, 2)}
|
|
9151
|
+
`);
|
|
9152
|
+
generated.push({
|
|
9153
|
+
path: ".cursor/hooks.json",
|
|
9154
|
+
description: "afterFileEdit hook"
|
|
9155
|
+
});
|
|
9156
|
+
mkdirSync(join5(cwd, ".cursor", "rules"), { recursive: true });
|
|
9157
|
+
const rulesPath = join5(cwd, ".cursor", "rules", "vibecop.md");
|
|
9158
|
+
const rulesContent = `---
|
|
9159
|
+
trigger: always_on
|
|
9160
|
+
---
|
|
9161
|
+
|
|
9162
|
+
After every code edit, review vibecop findings and fix issues before proceeding.
|
|
9163
|
+
Run: ${SCAN_CMD}
|
|
9164
|
+
`;
|
|
9165
|
+
writeFileSync(rulesPath, rulesContent);
|
|
9166
|
+
generated.push({
|
|
9167
|
+
path: ".cursor/rules/vibecop.md",
|
|
9168
|
+
description: "always-on lint rule"
|
|
9169
|
+
});
|
|
9170
|
+
break;
|
|
9171
|
+
}
|
|
9172
|
+
case "Codex CLI": {
|
|
9173
|
+
mkdirSync(join5(cwd, ".codex"), { recursive: true });
|
|
9174
|
+
const codexPath = join5(cwd, ".codex", "hooks.json");
|
|
9175
|
+
const codexHooks = {
|
|
9176
|
+
hooks: {
|
|
9177
|
+
PostToolUse: [
|
|
9178
|
+
{
|
|
9179
|
+
matcher: "Edit|Write|MultiEdit",
|
|
9180
|
+
hooks: [
|
|
9181
|
+
{
|
|
9182
|
+
type: "command",
|
|
9183
|
+
command: SCAN_CMD
|
|
9184
|
+
}
|
|
9185
|
+
]
|
|
9186
|
+
}
|
|
9187
|
+
]
|
|
9188
|
+
}
|
|
9189
|
+
};
|
|
9190
|
+
writeFileSync(codexPath, `${JSON.stringify(codexHooks, null, 2)}
|
|
9191
|
+
`);
|
|
9192
|
+
generated.push({
|
|
9193
|
+
path: ".codex/hooks.json",
|
|
9194
|
+
description: "PostToolUse hook (blocks on findings)"
|
|
9195
|
+
});
|
|
9196
|
+
break;
|
|
9197
|
+
}
|
|
9198
|
+
case "Aider": {
|
|
9199
|
+
const aiderPath = join5(cwd, ".aider.conf.yml");
|
|
9200
|
+
const aiderContent = `lint-cmd:
|
|
9201
|
+
- "typescript: npx vibecop scan --format text"
|
|
9202
|
+
- "javascript: npx vibecop scan --format text"
|
|
9203
|
+
- "python: npx vibecop scan --format text"
|
|
9204
|
+
auto-lint: true
|
|
9205
|
+
`;
|
|
9206
|
+
writeFileSync(aiderPath, aiderContent);
|
|
9207
|
+
generated.push({
|
|
9208
|
+
path: ".aider.conf.yml",
|
|
9209
|
+
description: "lint-cmd per language"
|
|
9210
|
+
});
|
|
9211
|
+
break;
|
|
9212
|
+
}
|
|
9213
|
+
case "Windsurf": {
|
|
9214
|
+
mkdirSync(join5(cwd, ".windsurf", "rules"), { recursive: true });
|
|
9215
|
+
const windsurfPath = join5(cwd, ".windsurf", "rules", "vibecop.md");
|
|
9216
|
+
const windsurfContent = `---
|
|
9217
|
+
trigger: always_on
|
|
9218
|
+
---
|
|
9219
|
+
|
|
9220
|
+
After every code edit, review vibecop findings and fix issues before proceeding.
|
|
9221
|
+
Run: ${SCAN_CMD}
|
|
9222
|
+
`;
|
|
9223
|
+
writeFileSync(windsurfPath, windsurfContent);
|
|
9224
|
+
generated.push({
|
|
9225
|
+
path: ".windsurf/rules/vibecop.md",
|
|
9226
|
+
description: "always-on lint rule"
|
|
9227
|
+
});
|
|
9228
|
+
break;
|
|
9229
|
+
}
|
|
9230
|
+
case "GitHub Copilot": {
|
|
9231
|
+
mkdirSync(join5(cwd, ".github"), { recursive: true });
|
|
9232
|
+
const copilotPath = join5(cwd, ".github", "copilot-instructions.md");
|
|
9233
|
+
const vibecopSection = `
|
|
9234
|
+
## vibecop
|
|
9235
|
+
|
|
9236
|
+
After every code edit, run \`${SCAN_CMD}\` and fix any findings before proceeding.
|
|
9237
|
+
`;
|
|
9238
|
+
if (existsSync4(copilotPath)) {
|
|
9239
|
+
const existing = readFileSync6(copilotPath, "utf-8");
|
|
9240
|
+
if (existing.includes("vibecop")) {
|
|
9241
|
+
generated.push({
|
|
9242
|
+
path: ".github/copilot-instructions.md",
|
|
9243
|
+
description: "already contains vibecop, skipped"
|
|
9244
|
+
});
|
|
9245
|
+
} else {
|
|
9246
|
+
writeFileSync(copilotPath, existing + vibecopSection);
|
|
9247
|
+
generated.push({
|
|
9248
|
+
path: ".github/copilot-instructions.md",
|
|
9249
|
+
description: "appended vibecop instructions"
|
|
9250
|
+
});
|
|
9251
|
+
}
|
|
9252
|
+
} else {
|
|
9253
|
+
writeFileSync(copilotPath, vibecopSection);
|
|
9254
|
+
generated.push({
|
|
9255
|
+
path: ".github/copilot-instructions.md",
|
|
9256
|
+
description: "copilot instructions"
|
|
9257
|
+
});
|
|
9258
|
+
}
|
|
9259
|
+
break;
|
|
9260
|
+
}
|
|
9261
|
+
case "Cline": {
|
|
9262
|
+
const clinePath = join5(cwd, ".clinerules");
|
|
9263
|
+
const clineContent = `After every code edit, run \`${SCAN_CMD}\` and fix any findings before proceeding.
|
|
9264
|
+
`;
|
|
9265
|
+
writeFileSync(clinePath, clineContent);
|
|
9266
|
+
generated.push({
|
|
9267
|
+
path: ".clinerules",
|
|
9268
|
+
description: "always-on lint rule"
|
|
9269
|
+
});
|
|
9270
|
+
break;
|
|
9271
|
+
}
|
|
9272
|
+
}
|
|
9273
|
+
}
|
|
9274
|
+
return generated;
|
|
9275
|
+
}
|
|
9276
|
+
function padEnd(str, len) {
|
|
9277
|
+
return str + " ".repeat(Math.max(0, len - str.length));
|
|
9278
|
+
}
|
|
9279
|
+
async function runInit(cwd) {
|
|
9280
|
+
const root = cwd ?? process.cwd();
|
|
9281
|
+
console.log("");
|
|
9282
|
+
console.log(" vibecop — agent integration setup");
|
|
9283
|
+
console.log("");
|
|
9284
|
+
const tools = detectTools(root);
|
|
9285
|
+
const anyDetected = tools.some((t) => t.detected);
|
|
9286
|
+
if (!anyDetected) {
|
|
9287
|
+
console.log(" No supported AI coding tools detected.");
|
|
9288
|
+
console.log(" See docs/agent-integration.md for manual setup.");
|
|
9289
|
+
console.log("");
|
|
9290
|
+
return;
|
|
9291
|
+
}
|
|
9292
|
+
console.log(" Detected tools:");
|
|
9293
|
+
for (const tool of tools) {
|
|
9294
|
+
const icon = tool.detected ? "✓" : "✗";
|
|
9295
|
+
console.log(` ${icon} ${tool.name} (${tool.reason})`);
|
|
9296
|
+
}
|
|
9297
|
+
console.log("");
|
|
9298
|
+
const generated = generateConfigs(root, tools);
|
|
9299
|
+
if (generated.length > 0) {
|
|
9300
|
+
const maxPath = Math.max(...generated.map((g) => g.path.length));
|
|
9301
|
+
console.log(" Generated:");
|
|
9302
|
+
for (const file of generated) {
|
|
9303
|
+
console.log(` ${padEnd(file.path, maxPath)} — ${file.description}`);
|
|
9304
|
+
}
|
|
9305
|
+
console.log("");
|
|
9306
|
+
}
|
|
9307
|
+
console.log(" Done! vibecop will now run automatically in your agent workflow.");
|
|
9308
|
+
console.log("");
|
|
9309
|
+
}
|
|
9310
|
+
var SCAN_CMD = "npx vibecop scan --diff HEAD --format agent";
|
|
9311
|
+
var init_init = () => {};
|
|
9312
|
+
|
|
9313
|
+
// src/cli.ts
|
|
9314
|
+
import { execSync as execSync2 } from "node:child_process";
|
|
9315
|
+
import { existsSync as existsSync5, readFileSync as readFileSync7 } from "node:fs";
|
|
9050
9316
|
import { extname as extname2, relative as relative2, resolve as resolve4 } from "node:path";
|
|
9051
9317
|
|
|
9052
9318
|
// node_modules/commander/esm.mjs
|
|
@@ -13110,6 +13376,35 @@ function isNodeError(err) {
|
|
|
13110
13376
|
return err instanceof Error && "code" in err;
|
|
13111
13377
|
}
|
|
13112
13378
|
|
|
13379
|
+
// src/detectors/utils.ts
|
|
13380
|
+
function makeFinding(detectorId, ctx, node, message, severity, suggestion) {
|
|
13381
|
+
const range = node.range();
|
|
13382
|
+
return {
|
|
13383
|
+
detectorId,
|
|
13384
|
+
message,
|
|
13385
|
+
severity,
|
|
13386
|
+
file: ctx.file.path,
|
|
13387
|
+
line: range.start.line + 1,
|
|
13388
|
+
column: range.start.column + 1,
|
|
13389
|
+
endLine: range.end.line + 1,
|
|
13390
|
+
endColumn: range.end.column + 1,
|
|
13391
|
+
...suggestion != null && { suggestion }
|
|
13392
|
+
};
|
|
13393
|
+
}
|
|
13394
|
+
function makeLineFinding(detectorId, ctx, line, column, message, severity, suggestion, endLine, endColumn) {
|
|
13395
|
+
return {
|
|
13396
|
+
detectorId,
|
|
13397
|
+
message,
|
|
13398
|
+
severity,
|
|
13399
|
+
file: ctx.file.path,
|
|
13400
|
+
line,
|
|
13401
|
+
column,
|
|
13402
|
+
...suggestion != null && { suggestion },
|
|
13403
|
+
...endLine != null && { endLine },
|
|
13404
|
+
...endColumn != null && { endColumn }
|
|
13405
|
+
};
|
|
13406
|
+
}
|
|
13407
|
+
|
|
13113
13408
|
// src/detectors/empty-error-handler.ts
|
|
13114
13409
|
var CONSOLE_METHODS = new Set(["console.log", "console.error", "console.warn"]);
|
|
13115
13410
|
var PYTHON_LOG_FUNCTIONS = new Set(["print", "logging.debug", "logging.info", "logging.warning", "logging.error"]);
|
|
@@ -13140,19 +13435,8 @@ function detectJavaScriptCatchBlocks(ctx) {
|
|
|
13140
13435
|
const hasComment = body.children().some((ch) => ch.kind() === "comment");
|
|
13141
13436
|
if (hasComment)
|
|
13142
13437
|
continue;
|
|
13143
|
-
const range = catchNode.range();
|
|
13144
13438
|
if (bodyChildren.length === 0) {
|
|
13145
|
-
findings.push(
|
|
13146
|
-
detectorId: "empty-error-handler",
|
|
13147
|
-
message: "Empty catch block silently swallows errors",
|
|
13148
|
-
severity: "warning",
|
|
13149
|
-
file: ctx.file.path,
|
|
13150
|
-
line: range.start.line + 1,
|
|
13151
|
-
column: range.start.column + 1,
|
|
13152
|
-
endLine: range.end.line + 1,
|
|
13153
|
-
endColumn: range.end.column + 1,
|
|
13154
|
-
suggestion: "Add error handling, re-throw the error, or add a comment explaining why the error is intentionally ignored"
|
|
13155
|
-
});
|
|
13439
|
+
findings.push(makeFinding("empty-error-handler", ctx, catchNode, "Empty catch block silently swallows errors", "warning", "Add error handling, re-throw the error, or add a comment explaining why the error is intentionally ignored"));
|
|
13156
13440
|
continue;
|
|
13157
13441
|
}
|
|
13158
13442
|
if (bodyChildren.length === 1) {
|
|
@@ -13160,17 +13444,7 @@ function detectJavaScriptCatchBlocks(ctx) {
|
|
|
13160
13444
|
if (stmt.kind() === "expression_statement") {
|
|
13161
13445
|
const stmtText = stmt.text().replace(/;$/, "").trim();
|
|
13162
13446
|
if (isLogOnlyCall(stmtText)) {
|
|
13163
|
-
findings.push(
|
|
13164
|
-
detectorId: "empty-error-handler",
|
|
13165
|
-
message: "Catch block only logs the error without handling it",
|
|
13166
|
-
severity: "warning",
|
|
13167
|
-
file: ctx.file.path,
|
|
13168
|
-
line: range.start.line + 1,
|
|
13169
|
-
column: range.start.column + 1,
|
|
13170
|
-
endLine: range.end.line + 1,
|
|
13171
|
-
endColumn: range.end.column + 1,
|
|
13172
|
-
suggestion: "Add proper error handling: re-throw, return a fallback value, or propagate the error"
|
|
13173
|
-
});
|
|
13447
|
+
findings.push(makeFinding("empty-error-handler", ctx, catchNode, "Catch block only logs the error without handling it", "warning", "Add proper error handling: re-throw, return a fallback value, or propagate the error"));
|
|
13174
13448
|
}
|
|
13175
13449
|
}
|
|
13176
13450
|
}
|
|
@@ -13187,38 +13461,17 @@ function detectPythonExceptBlocks(ctx) {
|
|
|
13187
13461
|
if (!block)
|
|
13188
13462
|
continue;
|
|
13189
13463
|
const blockChildren = block.children();
|
|
13190
|
-
const range = exceptNode.range();
|
|
13191
13464
|
const exceptText = exceptNode.text();
|
|
13192
13465
|
if (exceptText.includes("#"))
|
|
13193
13466
|
continue;
|
|
13194
13467
|
if (blockChildren.length === 1 && blockChildren[0].kind() === "pass_statement") {
|
|
13195
|
-
findings.push(
|
|
13196
|
-
detectorId: "empty-error-handler",
|
|
13197
|
-
message: "Except block with only 'pass' silently swallows errors",
|
|
13198
|
-
severity: "warning",
|
|
13199
|
-
file: ctx.file.path,
|
|
13200
|
-
line: range.start.line + 1,
|
|
13201
|
-
column: range.start.column + 1,
|
|
13202
|
-
endLine: range.end.line + 1,
|
|
13203
|
-
endColumn: range.end.column + 1,
|
|
13204
|
-
suggestion: "Add error handling, re-raise the exception, or add a comment explaining why the error is intentionally ignored"
|
|
13205
|
-
});
|
|
13468
|
+
findings.push(makeFinding("empty-error-handler", ctx, exceptNode, "Except block with only 'pass' silently swallows errors", "warning", "Add error handling, re-raise the exception, or add a comment explaining why the error is intentionally ignored"));
|
|
13206
13469
|
continue;
|
|
13207
13470
|
}
|
|
13208
13471
|
if (blockChildren.length === 1 && blockChildren[0].kind() === "expression_statement") {
|
|
13209
13472
|
const stmtText = blockChildren[0].text().trim();
|
|
13210
13473
|
if (isPythonLogOnlyCall(stmtText)) {
|
|
13211
|
-
findings.push(
|
|
13212
|
-
detectorId: "empty-error-handler",
|
|
13213
|
-
message: "Except block only logs the error without handling it",
|
|
13214
|
-
severity: "warning",
|
|
13215
|
-
file: ctx.file.path,
|
|
13216
|
-
line: range.start.line + 1,
|
|
13217
|
-
column: range.start.column + 1,
|
|
13218
|
-
endLine: range.end.line + 1,
|
|
13219
|
-
endColumn: range.end.column + 1,
|
|
13220
|
-
suggestion: "Add proper error handling: re-raise, return a fallback value, or propagate the error"
|
|
13221
|
-
});
|
|
13474
|
+
findings.push(makeFinding("empty-error-handler", ctx, exceptNode, "Except block only logs the error without handling it", "warning", "Add proper error handling: re-raise, return a fallback value, or propagate the error"));
|
|
13222
13475
|
}
|
|
13223
13476
|
}
|
|
13224
13477
|
}
|
|
@@ -13297,33 +13550,12 @@ function detectJavaScriptTrivialAssertions(ctx) {
|
|
|
13297
13550
|
if (!LITERAL_KINDS_JS.has(expectArg.kind()))
|
|
13298
13551
|
continue;
|
|
13299
13552
|
const methodName = property.text();
|
|
13300
|
-
const range = call.range();
|
|
13301
13553
|
if (methodName === "toBeTruthy" && expectArg.text() === "true") {
|
|
13302
|
-
findings.push(
|
|
13303
|
-
detectorId: "trivial-assertion",
|
|
13304
|
-
message: "Trivial assertion: expect(true).toBeTruthy() always passes",
|
|
13305
|
-
severity: "warning",
|
|
13306
|
-
file: ctx.file.path,
|
|
13307
|
-
line: range.start.line + 1,
|
|
13308
|
-
column: range.start.column + 1,
|
|
13309
|
-
endLine: range.end.line + 1,
|
|
13310
|
-
endColumn: range.end.column + 1,
|
|
13311
|
-
suggestion: "Replace with a meaningful assertion that tests actual behavior"
|
|
13312
|
-
});
|
|
13554
|
+
findings.push(makeFinding("trivial-assertion", ctx, call, "Trivial assertion: expect(true).toBeTruthy() always passes", "warning", "Replace with a meaningful assertion that tests actual behavior"));
|
|
13313
13555
|
continue;
|
|
13314
13556
|
}
|
|
13315
13557
|
if (methodName === "toBeFalsy" && expectArg.text() === "false") {
|
|
13316
|
-
findings.push(
|
|
13317
|
-
detectorId: "trivial-assertion",
|
|
13318
|
-
message: "Trivial assertion: expect(false).toBeFalsy() always passes",
|
|
13319
|
-
severity: "warning",
|
|
13320
|
-
file: ctx.file.path,
|
|
13321
|
-
line: range.start.line + 1,
|
|
13322
|
-
column: range.start.column + 1,
|
|
13323
|
-
endLine: range.end.line + 1,
|
|
13324
|
-
endColumn: range.end.column + 1,
|
|
13325
|
-
suggestion: "Replace with a meaningful assertion that tests actual behavior"
|
|
13326
|
-
});
|
|
13558
|
+
findings.push(makeFinding("trivial-assertion", ctx, call, "Trivial assertion: expect(false).toBeFalsy() always passes", "warning", "Replace with a meaningful assertion that tests actual behavior"));
|
|
13327
13559
|
continue;
|
|
13328
13560
|
}
|
|
13329
13561
|
if (methodName !== "toBe" && methodName !== "toEqual")
|
|
@@ -13341,17 +13573,7 @@ function detectJavaScriptTrivialAssertions(ctx) {
|
|
|
13341
13573
|
areSame = expectArg.text() === matcherArg.text();
|
|
13342
13574
|
}
|
|
13343
13575
|
if (areSame) {
|
|
13344
|
-
findings.push({
|
|
13345
|
-
detectorId: "trivial-assertion",
|
|
13346
|
-
message: `Trivial assertion: expect(${expectArg.text()}).${methodName}(${matcherArg.text()}) always passes`,
|
|
13347
|
-
severity: "warning",
|
|
13348
|
-
file: ctx.file.path,
|
|
13349
|
-
line: range.start.line + 1,
|
|
13350
|
-
column: range.start.column + 1,
|
|
13351
|
-
endLine: range.end.line + 1,
|
|
13352
|
-
endColumn: range.end.column + 1,
|
|
13353
|
-
suggestion: "Replace with a meaningful assertion that tests actual behavior"
|
|
13354
|
-
});
|
|
13576
|
+
findings.push(makeFinding("trivial-assertion", ctx, call, `Trivial assertion: expect(${expectArg.text()}).${methodName}(${matcherArg.text()}) always passes`, "warning", "Replace with a meaningful assertion that tests actual behavior"));
|
|
13355
13577
|
}
|
|
13356
13578
|
}
|
|
13357
13579
|
}
|
|
@@ -13366,19 +13588,8 @@ function detectPythonTrivialAssertions(ctx) {
|
|
|
13366
13588
|
if (children.length < 2)
|
|
13367
13589
|
continue;
|
|
13368
13590
|
const expr = children[1];
|
|
13369
|
-
const range = assertNode.range();
|
|
13370
13591
|
if (expr.kind() === "true" || expr.kind() === "false") {
|
|
13371
|
-
findings.push({
|
|
13372
|
-
detectorId: "trivial-assertion",
|
|
13373
|
-
message: `Trivial assertion: assert ${expr.text()} always ${expr.kind() === "true" ? "passes" : "fails"}`,
|
|
13374
|
-
severity: "warning",
|
|
13375
|
-
file: ctx.file.path,
|
|
13376
|
-
line: range.start.line + 1,
|
|
13377
|
-
column: range.start.column + 1,
|
|
13378
|
-
endLine: range.end.line + 1,
|
|
13379
|
-
endColumn: range.end.column + 1,
|
|
13380
|
-
suggestion: "Replace with a meaningful assertion that tests actual behavior"
|
|
13381
|
-
});
|
|
13592
|
+
findings.push(makeFinding("trivial-assertion", ctx, assertNode, `Trivial assertion: assert ${expr.text()} always ${expr.kind() === "true" ? "passes" : "fails"}`, "warning", "Replace with a meaningful assertion that tests actual behavior"));
|
|
13382
13593
|
continue;
|
|
13383
13594
|
}
|
|
13384
13595
|
if (expr.kind() === "comparison_operator") {
|
|
@@ -13394,17 +13605,7 @@ function detectPythonTrivialAssertions(ctx) {
|
|
|
13394
13605
|
areSame = left.text() === right.text();
|
|
13395
13606
|
}
|
|
13396
13607
|
if (areSame) {
|
|
13397
|
-
findings.push({
|
|
13398
|
-
detectorId: "trivial-assertion",
|
|
13399
|
-
message: `Trivial assertion: assert ${left.text()} == ${right.text()} always passes`,
|
|
13400
|
-
severity: "warning",
|
|
13401
|
-
file: ctx.file.path,
|
|
13402
|
-
line: range.start.line + 1,
|
|
13403
|
-
column: range.start.column + 1,
|
|
13404
|
-
endLine: range.end.line + 1,
|
|
13405
|
-
endColumn: range.end.column + 1,
|
|
13406
|
-
suggestion: "Replace with a meaningful assertion that tests actual behavior"
|
|
13407
|
-
});
|
|
13608
|
+
findings.push(makeFinding("trivial-assertion", ctx, assertNode, `Trivial assertion: assert ${left.text()} == ${right.text()} always passes`, "warning", "Replace with a meaningful assertion that tests actual behavior"));
|
|
13408
13609
|
}
|
|
13409
13610
|
}
|
|
13410
13611
|
}
|
|
@@ -13465,18 +13666,7 @@ function detectRejectUnauthorized(root, ctx, findings) {
|
|
|
13465
13666
|
const key = children.find((ch) => ch.kind() === "property_identifier" || ch.kind() === "string");
|
|
13466
13667
|
const value = children.find((ch) => ch.kind() === "false");
|
|
13467
13668
|
if (key && value && key.text().replace(/["']/g, "") === "rejectUnauthorized") {
|
|
13468
|
-
|
|
13469
|
-
findings.push({
|
|
13470
|
-
detectorId: "insecure-defaults",
|
|
13471
|
-
message: "TLS certificate verification is disabled (rejectUnauthorized: false)",
|
|
13472
|
-
severity: "error",
|
|
13473
|
-
file: ctx.file.path,
|
|
13474
|
-
line: range.start.line + 1,
|
|
13475
|
-
column: range.start.column + 1,
|
|
13476
|
-
endLine: range.end.line + 1,
|
|
13477
|
-
endColumn: range.end.column + 1,
|
|
13478
|
-
suggestion: "Remove rejectUnauthorized: false to enable TLS certificate verification"
|
|
13479
|
-
});
|
|
13669
|
+
findings.push(makeFinding("insecure-defaults", ctx, pair, "TLS certificate verification is disabled (rejectUnauthorized: false)", "error", "Remove rejectUnauthorized: false to enable TLS certificate verification"));
|
|
13480
13670
|
}
|
|
13481
13671
|
}
|
|
13482
13672
|
}
|
|
@@ -13486,18 +13676,7 @@ function detectEvalUsage(root, ctx, findings) {
|
|
|
13486
13676
|
const children = call.children();
|
|
13487
13677
|
const fn = children[0];
|
|
13488
13678
|
if (fn && fn.kind() === "identifier" && fn.text() === "eval") {
|
|
13489
|
-
|
|
13490
|
-
findings.push({
|
|
13491
|
-
detectorId: "insecure-defaults",
|
|
13492
|
-
message: "eval() executes arbitrary code and is a security risk",
|
|
13493
|
-
severity: "error",
|
|
13494
|
-
file: ctx.file.path,
|
|
13495
|
-
line: range.start.line + 1,
|
|
13496
|
-
column: range.start.column + 1,
|
|
13497
|
-
endLine: range.end.line + 1,
|
|
13498
|
-
endColumn: range.end.column + 1,
|
|
13499
|
-
suggestion: "Avoid eval(). Use JSON.parse() for data, or refactor to avoid dynamic code execution"
|
|
13500
|
-
});
|
|
13679
|
+
findings.push(makeFinding("insecure-defaults", ctx, call, "eval() executes arbitrary code and is a security risk", "error", "Avoid eval(). Use JSON.parse() for data, or refactor to avoid dynamic code execution"));
|
|
13501
13680
|
}
|
|
13502
13681
|
}
|
|
13503
13682
|
}
|
|
@@ -13507,18 +13686,7 @@ function detectNewFunction(root, ctx, findings) {
|
|
|
13507
13686
|
const children = newExpr.children();
|
|
13508
13687
|
const constructorNode = children.find((ch) => ch.kind() === "identifier");
|
|
13509
13688
|
if (constructorNode && constructorNode.text() === "Function") {
|
|
13510
|
-
|
|
13511
|
-
findings.push({
|
|
13512
|
-
detectorId: "insecure-defaults",
|
|
13513
|
-
message: "new Function() creates functions from strings and is a security risk",
|
|
13514
|
-
severity: "error",
|
|
13515
|
-
file: ctx.file.path,
|
|
13516
|
-
line: range.start.line + 1,
|
|
13517
|
-
column: range.start.column + 1,
|
|
13518
|
-
endLine: range.end.line + 1,
|
|
13519
|
-
endColumn: range.end.column + 1,
|
|
13520
|
-
suggestion: "Avoid new Function(). Refactor to use static function definitions"
|
|
13521
|
-
});
|
|
13689
|
+
findings.push(makeFinding("insecure-defaults", ctx, newExpr, "new Function() creates functions from strings and is a security risk", "error", "Avoid new Function(). Refactor to use static function definitions"));
|
|
13522
13690
|
}
|
|
13523
13691
|
}
|
|
13524
13692
|
}
|
|
@@ -13546,18 +13714,7 @@ function detectHardcodedCredentialsJS(root, ctx, findings) {
|
|
|
13546
13714
|
continue;
|
|
13547
13715
|
if (looksLikeNonCredential(strContent))
|
|
13548
13716
|
continue;
|
|
13549
|
-
|
|
13550
|
-
findings.push({
|
|
13551
|
-
detectorId: "insecure-defaults",
|
|
13552
|
-
message: `Hardcoded credential detected in property '${keyName}'`,
|
|
13553
|
-
severity: "error",
|
|
13554
|
-
file: ctx.file.path,
|
|
13555
|
-
line: range.start.line + 1,
|
|
13556
|
-
column: range.start.column + 1,
|
|
13557
|
-
endLine: range.end.line + 1,
|
|
13558
|
-
endColumn: range.end.column + 1,
|
|
13559
|
-
suggestion: "Use environment variables or a secrets manager instead of hardcoding credentials"
|
|
13560
|
-
});
|
|
13717
|
+
findings.push(makeFinding("insecure-defaults", ctx, pair, `Hardcoded credential detected in property '${keyName}'`, "error", "Use environment variables or a secrets manager instead of hardcoding credentials"));
|
|
13561
13718
|
}
|
|
13562
13719
|
}
|
|
13563
13720
|
function checkCredentialAssignment(node, ctx, findings) {
|
|
@@ -13574,18 +13731,7 @@ function checkCredentialAssignment(node, ctx, findings) {
|
|
|
13574
13731
|
return;
|
|
13575
13732
|
if (looksLikeNonCredential(strContent))
|
|
13576
13733
|
return;
|
|
13577
|
-
|
|
13578
|
-
findings.push({
|
|
13579
|
-
detectorId: "insecure-defaults",
|
|
13580
|
-
message: `Hardcoded credential detected in variable '${varName}'`,
|
|
13581
|
-
severity: "error",
|
|
13582
|
-
file: ctx.file.path,
|
|
13583
|
-
line: range.start.line + 1,
|
|
13584
|
-
column: range.start.column + 1,
|
|
13585
|
-
endLine: range.end.line + 1,
|
|
13586
|
-
endColumn: range.end.column + 1,
|
|
13587
|
-
suggestion: "Use environment variables or a secrets manager instead of hardcoding credentials"
|
|
13588
|
-
});
|
|
13734
|
+
findings.push(makeFinding("insecure-defaults", ctx, node, `Hardcoded credential detected in variable '${varName}'`, "error", "Use environment variables or a secrets manager instead of hardcoding credentials"));
|
|
13589
13735
|
}
|
|
13590
13736
|
function detectWeakCiphers(root, ctx, findings) {
|
|
13591
13737
|
const callExprs = root.findAll({ rule: { kind: "call_expression" } });
|
|
@@ -13607,18 +13753,7 @@ function detectWeakCiphers(root, ctx, findings) {
|
|
|
13607
13753
|
if (firstArg.kind() === "string") {
|
|
13608
13754
|
const cipher = firstArg.text().slice(1, -1).toLowerCase();
|
|
13609
13755
|
if (WEAK_CIPHERS.has(cipher)) {
|
|
13610
|
-
|
|
13611
|
-
findings.push({
|
|
13612
|
-
detectorId: "insecure-defaults",
|
|
13613
|
-
message: `Weak cipher algorithm '${cipher}' detected`,
|
|
13614
|
-
severity: "error",
|
|
13615
|
-
file: ctx.file.path,
|
|
13616
|
-
line: range.start.line + 1,
|
|
13617
|
-
column: range.start.column + 1,
|
|
13618
|
-
endLine: range.end.line + 1,
|
|
13619
|
-
endColumn: range.end.column + 1,
|
|
13620
|
-
suggestion: "Use a strong cipher algorithm like 'aes-256-gcm' instead"
|
|
13621
|
-
});
|
|
13756
|
+
findings.push(makeFinding("insecure-defaults", ctx, call, `Weak cipher algorithm '${cipher}' detected`, "error", "Use a strong cipher algorithm like 'aes-256-gcm' instead"));
|
|
13622
13757
|
}
|
|
13623
13758
|
}
|
|
13624
13759
|
}
|
|
@@ -13641,18 +13776,7 @@ function detectPythonVerifyFalse(root, ctx, findings) {
|
|
|
13641
13776
|
const key = children.find((ch) => ch.kind() === "identifier");
|
|
13642
13777
|
const value = children.find((ch) => ch.kind() === "false");
|
|
13643
13778
|
if (key && value && key.text() === "verify") {
|
|
13644
|
-
|
|
13645
|
-
findings.push({
|
|
13646
|
-
detectorId: "insecure-defaults",
|
|
13647
|
-
message: "TLS certificate verification is disabled (verify=False)",
|
|
13648
|
-
severity: "error",
|
|
13649
|
-
file: ctx.file.path,
|
|
13650
|
-
line: range.start.line + 1,
|
|
13651
|
-
column: range.start.column + 1,
|
|
13652
|
-
endLine: range.end.line + 1,
|
|
13653
|
-
endColumn: range.end.column + 1,
|
|
13654
|
-
suggestion: "Remove verify=False to enable TLS certificate verification"
|
|
13655
|
-
});
|
|
13779
|
+
findings.push(makeFinding("insecure-defaults", ctx, kwarg, "TLS certificate verification is disabled (verify=False)", "error", "Remove verify=False to enable TLS certificate verification"));
|
|
13656
13780
|
}
|
|
13657
13781
|
}
|
|
13658
13782
|
}
|
|
@@ -13663,18 +13787,7 @@ function detectPythonShellTrue(root, ctx, findings) {
|
|
|
13663
13787
|
const key = children.find((ch) => ch.kind() === "identifier");
|
|
13664
13788
|
const value = children.find((ch) => ch.kind() === "true");
|
|
13665
13789
|
if (key && value && key.text() === "shell") {
|
|
13666
|
-
|
|
13667
|
-
findings.push({
|
|
13668
|
-
detectorId: "insecure-defaults",
|
|
13669
|
-
message: "shell=True in subprocess call allows shell injection attacks",
|
|
13670
|
-
severity: "error",
|
|
13671
|
-
file: ctx.file.path,
|
|
13672
|
-
line: range.start.line + 1,
|
|
13673
|
-
column: range.start.column + 1,
|
|
13674
|
-
endLine: range.end.line + 1,
|
|
13675
|
-
endColumn: range.end.column + 1,
|
|
13676
|
-
suggestion: "Use shell=False (the default) and pass arguments as a list"
|
|
13677
|
-
});
|
|
13790
|
+
findings.push(makeFinding("insecure-defaults", ctx, kwarg, "shell=True in subprocess call allows shell injection attacks", "error", "Use shell=False (the default) and pass arguments as a list"));
|
|
13678
13791
|
}
|
|
13679
13792
|
}
|
|
13680
13793
|
}
|
|
@@ -13684,18 +13797,7 @@ function detectPythonEval(root, ctx, findings) {
|
|
|
13684
13797
|
const children = call.children();
|
|
13685
13798
|
const fn = children[0];
|
|
13686
13799
|
if (fn && fn.kind() === "identifier" && fn.text() === "eval") {
|
|
13687
|
-
|
|
13688
|
-
findings.push({
|
|
13689
|
-
detectorId: "insecure-defaults",
|
|
13690
|
-
message: "eval() executes arbitrary code and is a security risk",
|
|
13691
|
-
severity: "error",
|
|
13692
|
-
file: ctx.file.path,
|
|
13693
|
-
line: range.start.line + 1,
|
|
13694
|
-
column: range.start.column + 1,
|
|
13695
|
-
endLine: range.end.line + 1,
|
|
13696
|
-
endColumn: range.end.column + 1,
|
|
13697
|
-
suggestion: "Avoid eval(). Use ast.literal_eval() for safe expression evaluation"
|
|
13698
|
-
});
|
|
13800
|
+
findings.push(makeFinding("insecure-defaults", ctx, call, "eval() executes arbitrary code and is a security risk", "error", "Avoid eval(). Use ast.literal_eval() for safe expression evaluation"));
|
|
13699
13801
|
}
|
|
13700
13802
|
}
|
|
13701
13803
|
}
|
|
@@ -13721,18 +13823,7 @@ function detectPythonHardcodedCredentials(root, ctx, findings) {
|
|
|
13721
13823
|
}
|
|
13722
13824
|
if (strContent.length === 0)
|
|
13723
13825
|
continue;
|
|
13724
|
-
|
|
13725
|
-
findings.push({
|
|
13726
|
-
detectorId: "insecure-defaults",
|
|
13727
|
-
message: `Hardcoded credential detected in variable '${varName}'`,
|
|
13728
|
-
severity: "error",
|
|
13729
|
-
file: ctx.file.path,
|
|
13730
|
-
line: range.start.line + 1,
|
|
13731
|
-
column: range.start.column + 1,
|
|
13732
|
-
endLine: range.end.line + 1,
|
|
13733
|
-
endColumn: range.end.column + 1,
|
|
13734
|
-
suggestion: "Use environment variables or a secrets manager instead of hardcoding credentials"
|
|
13735
|
-
});
|
|
13826
|
+
findings.push(makeFinding("insecure-defaults", ctx, assign, `Hardcoded credential detected in variable '${varName}'`, "error", "Use environment variables or a secrets manager instead of hardcoding credentials"));
|
|
13736
13827
|
}
|
|
13737
13828
|
}
|
|
13738
13829
|
var insecureDefaults = {
|
|
@@ -14248,18 +14339,7 @@ function detectJavaScriptUndeclaredImports(ctx) {
|
|
|
14248
14339
|
const nearestDeps = findNearestJsDependencies(ctx.file.absolutePath, scanRoot);
|
|
14249
14340
|
if (nearestDeps.has(packageName))
|
|
14250
14341
|
continue;
|
|
14251
|
-
|
|
14252
|
-
findings.push({
|
|
14253
|
-
detectorId: "undeclared-import",
|
|
14254
|
-
message: `Import '${packageName}' is not declared in project dependencies`,
|
|
14255
|
-
severity: "error",
|
|
14256
|
-
file: ctx.file.path,
|
|
14257
|
-
line: range.start.line + 1,
|
|
14258
|
-
column: range.start.column + 1,
|
|
14259
|
-
endLine: range.end.line + 1,
|
|
14260
|
-
endColumn: range.end.column + 1,
|
|
14261
|
-
suggestion: `Add '${packageName}' to your package.json dependencies`
|
|
14262
|
-
});
|
|
14342
|
+
findings.push(makeFinding("undeclared-import", ctx, importNode, `Import '${packageName}' is not declared in project dependencies`, "error", `Add '${packageName}' to your package.json dependencies`));
|
|
14263
14343
|
}
|
|
14264
14344
|
const callExprs = root.findAll({ rule: { kind: "call_expression" } });
|
|
14265
14345
|
for (const call of callExprs) {
|
|
@@ -14295,18 +14375,7 @@ function detectJavaScriptUndeclaredImports(ctx) {
|
|
|
14295
14375
|
const nearestDeps = findNearestJsDependencies(ctx.file.absolutePath, scanRoot);
|
|
14296
14376
|
if (nearestDeps.has(packageName))
|
|
14297
14377
|
continue;
|
|
14298
|
-
|
|
14299
|
-
findings.push({
|
|
14300
|
-
detectorId: "undeclared-import",
|
|
14301
|
-
message: `Import '${packageName}' is not declared in project dependencies`,
|
|
14302
|
-
severity: "error",
|
|
14303
|
-
file: ctx.file.path,
|
|
14304
|
-
line: range.start.line + 1,
|
|
14305
|
-
column: range.start.column + 1,
|
|
14306
|
-
endLine: range.end.line + 1,
|
|
14307
|
-
endColumn: range.end.column + 1,
|
|
14308
|
-
suggestion: `Add '${packageName}' to your package.json dependencies`
|
|
14309
|
-
});
|
|
14378
|
+
findings.push(makeFinding("undeclared-import", ctx, call, `Import '${packageName}' is not declared in project dependencies`, "error", `Add '${packageName}' to your package.json dependencies`));
|
|
14310
14379
|
}
|
|
14311
14380
|
return findings;
|
|
14312
14381
|
}
|
|
@@ -14330,18 +14399,7 @@ function detectPythonUndeclaredImports(ctx) {
|
|
|
14330
14399
|
const topLevel = fullName.split(".")[0];
|
|
14331
14400
|
if (isPythonImportDeclared(topLevel, ctx, scanRoot))
|
|
14332
14401
|
continue;
|
|
14333
|
-
|
|
14334
|
-
findings.push({
|
|
14335
|
-
detectorId: "undeclared-import",
|
|
14336
|
-
message: `Import '${topLevel}' is not declared in project dependencies`,
|
|
14337
|
-
severity: "error",
|
|
14338
|
-
file: ctx.file.path,
|
|
14339
|
-
line: range.start.line + 1,
|
|
14340
|
-
column: range.start.column + 1,
|
|
14341
|
-
endLine: range.end.line + 1,
|
|
14342
|
-
endColumn: range.end.column + 1,
|
|
14343
|
-
suggestion: `Add '${topLevel}' to your requirements.txt or pyproject.toml`
|
|
14344
|
-
});
|
|
14402
|
+
findings.push(makeFinding("undeclared-import", ctx, importNode, `Import '${topLevel}' is not declared in project dependencies`, "error", `Add '${topLevel}' to your requirements.txt or pyproject.toml`));
|
|
14345
14403
|
}
|
|
14346
14404
|
const fromImports = root.findAll({ rule: { kind: "import_from_statement" } });
|
|
14347
14405
|
for (const importNode of fromImports) {
|
|
@@ -14356,18 +14414,7 @@ function detectPythonUndeclaredImports(ctx) {
|
|
|
14356
14414
|
const topLevel = fullName.split(".")[0];
|
|
14357
14415
|
if (isPythonImportDeclared(topLevel, ctx, scanRoot))
|
|
14358
14416
|
continue;
|
|
14359
|
-
|
|
14360
|
-
findings.push({
|
|
14361
|
-
detectorId: "undeclared-import",
|
|
14362
|
-
message: `Import '${topLevel}' is not declared in project dependencies`,
|
|
14363
|
-
severity: "error",
|
|
14364
|
-
file: ctx.file.path,
|
|
14365
|
-
line: range.start.line + 1,
|
|
14366
|
-
column: range.start.column + 1,
|
|
14367
|
-
endLine: range.end.line + 1,
|
|
14368
|
-
endColumn: range.end.column + 1,
|
|
14369
|
-
suggestion: `Add '${topLevel}' to your requirements.txt or pyproject.toml`
|
|
14370
|
-
});
|
|
14417
|
+
findings.push(makeFinding("undeclared-import", ctx, importNode, `Import '${topLevel}' is not declared in project dependencies`, "error", `Add '${topLevel}' to your requirements.txt or pyproject.toml`));
|
|
14371
14418
|
}
|
|
14372
14419
|
return findings;
|
|
14373
14420
|
}
|
|
@@ -14460,33 +14507,11 @@ function detectRedundantNullChecks(ctx) {
|
|
|
14460
14507
|
if (leftCheck.variable !== rightCheck.variable)
|
|
14461
14508
|
continue;
|
|
14462
14509
|
if (!leftCheck.isLoose && !rightCheck.isLoose && (leftCheck.checksNull && rightCheck.checksUndefined || leftCheck.checksUndefined && rightCheck.checksNull)) {
|
|
14463
|
-
|
|
14464
|
-
findings.push({
|
|
14465
|
-
detectorId: "over-defensive-coding",
|
|
14466
|
-
message: `Redundant null+undefined check on '${leftCheck.variable}'. Use '${leftCheck.variable} != null' to check both.`,
|
|
14467
|
-
severity: "info",
|
|
14468
|
-
file: ctx.file.path,
|
|
14469
|
-
line: range.start.line + 1,
|
|
14470
|
-
column: range.start.column + 1,
|
|
14471
|
-
endLine: range.end.line + 1,
|
|
14472
|
-
endColumn: range.end.column + 1,
|
|
14473
|
-
suggestion: `Replace with '${leftCheck.variable} != null' which checks both null and undefined`
|
|
14474
|
-
});
|
|
14510
|
+
findings.push(makeFinding("over-defensive-coding", ctx, expr, `Redundant null+undefined check on '${leftCheck.variable}'. Use '${leftCheck.variable} != null' to check both.`, "info", `Replace with '${leftCheck.variable} != null' which checks both null and undefined`));
|
|
14475
14511
|
continue;
|
|
14476
14512
|
}
|
|
14477
14513
|
if (leftCheck.isLoose && rightCheck.isLoose && (leftCheck.checksNull && rightCheck.checksUndefined || leftCheck.checksUndefined && rightCheck.checksNull)) {
|
|
14478
|
-
|
|
14479
|
-
findings.push({
|
|
14480
|
-
detectorId: "over-defensive-coding",
|
|
14481
|
-
message: `Redundant check: '${leftCheck.variable} != null' already checks both null and undefined`,
|
|
14482
|
-
severity: "info",
|
|
14483
|
-
file: ctx.file.path,
|
|
14484
|
-
line: range.start.line + 1,
|
|
14485
|
-
column: range.start.column + 1,
|
|
14486
|
-
endLine: range.end.line + 1,
|
|
14487
|
-
endColumn: range.end.column + 1,
|
|
14488
|
-
suggestion: `Use just '${leftCheck.variable} != null' — it checks both null and undefined`
|
|
14489
|
-
});
|
|
14514
|
+
findings.push(makeFinding("over-defensive-coding", ctx, expr, `Redundant check: '${leftCheck.variable} != null' already checks both null and undefined`, "info", `Use just '${leftCheck.variable} != null' — it checks both null and undefined`));
|
|
14490
14515
|
}
|
|
14491
14516
|
}
|
|
14492
14517
|
return findings;
|
|
@@ -14519,18 +14544,7 @@ function detectJsonParseLiteralTryCatch(ctx) {
|
|
|
14519
14544
|
const arg = argNodes[0];
|
|
14520
14545
|
if (arg.kind() !== "string")
|
|
14521
14546
|
continue;
|
|
14522
|
-
|
|
14523
|
-
findings.push({
|
|
14524
|
-
detectorId: "over-defensive-coding",
|
|
14525
|
-
message: "Unnecessary try/catch around JSON.parse() with a string literal argument that cannot fail",
|
|
14526
|
-
severity: "info",
|
|
14527
|
-
file: ctx.file.path,
|
|
14528
|
-
line: range.start.line + 1,
|
|
14529
|
-
column: range.start.column + 1,
|
|
14530
|
-
endLine: range.end.line + 1,
|
|
14531
|
-
endColumn: range.end.column + 1,
|
|
14532
|
-
suggestion: "Remove the try/catch — JSON.parse with a valid string literal will never throw"
|
|
14533
|
-
});
|
|
14547
|
+
findings.push(makeFinding("over-defensive-coding", ctx, tryNode, "Unnecessary try/catch around JSON.parse() with a string literal argument that cannot fail", "info", "Remove the try/catch — JSON.parse with a valid string literal will never throw"));
|
|
14534
14548
|
}
|
|
14535
14549
|
}
|
|
14536
14550
|
}
|
|
@@ -14627,15 +14641,7 @@ var excessiveCommentRatio = {
|
|
|
14627
14641
|
if (ratio > threshold) {
|
|
14628
14642
|
const pct = Math.round(ratio * 100);
|
|
14629
14643
|
return [
|
|
14630
|
-
{
|
|
14631
|
-
detectorId: "excessive-comment-ratio",
|
|
14632
|
-
message: `File has ${pct}% comment lines (${counts.commentLines} comments, ${counts.codeLines} code lines). Threshold: ${Math.round(threshold * 100)}%`,
|
|
14633
|
-
severity: "info",
|
|
14634
|
-
file: ctx.file.path,
|
|
14635
|
-
line: 1,
|
|
14636
|
-
column: 1,
|
|
14637
|
-
suggestion: "Reduce excessive comments. Good code should be self-documenting with comments reserved for explaining 'why', not 'what'."
|
|
14638
|
-
}
|
|
14644
|
+
makeLineFinding("excessive-comment-ratio", ctx, 1, 1, `File has ${pct}% comment lines (${counts.commentLines} comments, ${counts.codeLines} code lines). Threshold: ${Math.round(threshold * 100)}%`, "info", "Reduce excessive comments. Good code should be self-documenting with comments reserved for explaining 'why', not 'what'.")
|
|
14639
14645
|
];
|
|
14640
14646
|
}
|
|
14641
14647
|
return [];
|
|
@@ -14706,15 +14712,7 @@ function detectJsMocking(ctx) {
|
|
|
14706
14712
|
const ratio = ctx.config.ratio ?? DEFAULT_RATIO;
|
|
14707
14713
|
if (mockCount > 0 && mockCount > assertionCount * ratio) {
|
|
14708
14714
|
return [
|
|
14709
|
-
{
|
|
14710
|
-
detectorId: "over-mocking",
|
|
14711
|
-
message: `Test file has more mocks (${mockCount}) than assertions (${assertionCount}). Tests that over-mock may not verify real behavior.`,
|
|
14712
|
-
severity: "warning",
|
|
14713
|
-
file: ctx.file.path,
|
|
14714
|
-
line: 1,
|
|
14715
|
-
column: 1,
|
|
14716
|
-
suggestion: "Reduce mocking and add more assertions. Consider integration tests for heavily-mocked code."
|
|
14717
|
-
}
|
|
14715
|
+
makeLineFinding("over-mocking", ctx, 1, 1, `Test file has more mocks (${mockCount}) than assertions (${assertionCount}). Tests that over-mock may not verify real behavior.`, "warning", "Reduce mocking and add more assertions. Consider integration tests for heavily-mocked code.")
|
|
14718
14716
|
];
|
|
14719
14717
|
}
|
|
14720
14718
|
return [];
|
|
@@ -14725,15 +14723,7 @@ function detectPythonMocking(ctx) {
|
|
|
14725
14723
|
const ratio = ctx.config.ratio ?? DEFAULT_RATIO;
|
|
14726
14724
|
if (mockCount > 0 && mockCount > assertionCount * ratio) {
|
|
14727
14725
|
return [
|
|
14728
|
-
{
|
|
14729
|
-
detectorId: "over-mocking",
|
|
14730
|
-
message: `Test file has more mocks (${mockCount}) than assertions (${assertionCount}). Tests that over-mock may not verify real behavior.`,
|
|
14731
|
-
severity: "warning",
|
|
14732
|
-
file: ctx.file.path,
|
|
14733
|
-
line: 1,
|
|
14734
|
-
column: 1,
|
|
14735
|
-
suggestion: "Reduce mocking and add more assertions. Consider integration tests for heavily-mocked code."
|
|
14736
|
-
}
|
|
14726
|
+
makeLineFinding("over-mocking", ctx, 1, 1, `Test file has more mocks (${mockCount}) than assertions (${assertionCount}). Tests that over-mock may not verify real behavior.`, "warning", "Reduce mocking and add more assertions. Consider integration tests for heavily-mocked code.")
|
|
14737
14727
|
];
|
|
14738
14728
|
}
|
|
14739
14729
|
return [];
|
|
@@ -14866,18 +14856,7 @@ function detectJavaScript(ctx) {
|
|
|
14866
14856
|
}
|
|
14867
14857
|
if (!isDbCall)
|
|
14868
14858
|
continue;
|
|
14869
|
-
|
|
14870
|
-
findings.push({
|
|
14871
|
-
detectorId: "n-plus-one-query",
|
|
14872
|
-
message: "Database or API call inside a loop — potential N+1 query",
|
|
14873
|
-
severity: "warning",
|
|
14874
|
-
file: ctx.file.path,
|
|
14875
|
-
line: range.start.line + 1,
|
|
14876
|
-
column: range.start.column + 1,
|
|
14877
|
-
endLine: range.end.line + 1,
|
|
14878
|
-
endColumn: range.end.column + 1,
|
|
14879
|
-
suggestion: "Batch the operation outside the loop (e.g., use WHERE IN, Promise.all, or bulk API endpoint)"
|
|
14880
|
-
});
|
|
14859
|
+
findings.push(makeFinding("n-plus-one-query", ctx, awaitExpr, "Database or API call inside a loop — potential N+1 query", "warning", "Batch the operation outside the loop (e.g., use WHERE IN, Promise.all, or bulk API endpoint)"));
|
|
14881
14860
|
}
|
|
14882
14861
|
const arrowFns = root.findAll({ rule: { kind: "arrow_function" } });
|
|
14883
14862
|
for (const arrow of arrowFns) {
|
|
@@ -14917,18 +14896,7 @@ function detectJavaScript(ctx) {
|
|
|
14917
14896
|
}
|
|
14918
14897
|
if (!isDbCall)
|
|
14919
14898
|
continue;
|
|
14920
|
-
|
|
14921
|
-
findings.push({
|
|
14922
|
-
detectorId: "n-plus-one-query",
|
|
14923
|
-
message: "Database or API call inside .map(async ...) — potential N+1 query",
|
|
14924
|
-
severity: "warning",
|
|
14925
|
-
file: ctx.file.path,
|
|
14926
|
-
line: range.start.line + 1,
|
|
14927
|
-
column: range.start.column + 1,
|
|
14928
|
-
endLine: range.end.line + 1,
|
|
14929
|
-
endColumn: range.end.column + 1,
|
|
14930
|
-
suggestion: "Batch the operation (e.g., use WHERE IN or a single bulk query) instead of per-item async calls"
|
|
14931
|
-
});
|
|
14899
|
+
findings.push(makeFinding("n-plus-one-query", ctx, innerAwait, "Database or API call inside .map(async ...) — potential N+1 query", "warning", "Batch the operation (e.g., use WHERE IN or a single bulk query) instead of per-item async calls"));
|
|
14932
14900
|
}
|
|
14933
14901
|
}
|
|
14934
14902
|
const callExprs = root.findAll({ rule: { kind: "call_expression" } });
|
|
@@ -14963,18 +14931,7 @@ function detectJavaScript(ctx) {
|
|
|
14963
14931
|
}
|
|
14964
14932
|
if (!isDbCall)
|
|
14965
14933
|
continue;
|
|
14966
|
-
|
|
14967
|
-
findings.push({
|
|
14968
|
-
detectorId: "n-plus-one-query",
|
|
14969
|
-
message: "Database or API call inside a loop — potential N+1 query",
|
|
14970
|
-
severity: "warning",
|
|
14971
|
-
file: ctx.file.path,
|
|
14972
|
-
line: range.start.line + 1,
|
|
14973
|
-
column: range.start.column + 1,
|
|
14974
|
-
endLine: range.end.line + 1,
|
|
14975
|
-
endColumn: range.end.column + 1,
|
|
14976
|
-
suggestion: "Batch the operation outside the loop (e.g., use WHERE IN, Promise.all, or bulk API endpoint)"
|
|
14977
|
-
});
|
|
14934
|
+
findings.push(makeFinding("n-plus-one-query", ctx, call, "Database or API call inside a loop — potential N+1 query", "warning", "Batch the operation outside the loop (e.g., use WHERE IN, Promise.all, or bulk API endpoint)"));
|
|
14978
14935
|
}
|
|
14979
14936
|
return findings;
|
|
14980
14937
|
}
|
|
@@ -14999,17 +14956,7 @@ function detectPython(ctx) {
|
|
|
14999
14956
|
continue;
|
|
15000
14957
|
const range = awaitExpr.range();
|
|
15001
14958
|
reported.add(range.start.line);
|
|
15002
|
-
findings.push(
|
|
15003
|
-
detectorId: "n-plus-one-query",
|
|
15004
|
-
message: "Database call inside a loop — potential N+1 query",
|
|
15005
|
-
severity: "warning",
|
|
15006
|
-
file: ctx.file.path,
|
|
15007
|
-
line: range.start.line + 1,
|
|
15008
|
-
column: range.start.column + 1,
|
|
15009
|
-
endLine: range.end.line + 1,
|
|
15010
|
-
endColumn: range.end.column + 1,
|
|
15011
|
-
suggestion: "Batch the operation outside the loop (e.g., use WHERE IN or bulk query)"
|
|
15012
|
-
});
|
|
14959
|
+
findings.push(makeFinding("n-plus-one-query", ctx, awaitExpr, "Database call inside a loop — potential N+1 query", "warning", "Batch the operation outside the loop (e.g., use WHERE IN or bulk query)"));
|
|
15013
14960
|
}
|
|
15014
14961
|
const callExprs = root.findAll({ rule: { kind: "call" } });
|
|
15015
14962
|
for (const call of callExprs) {
|
|
@@ -15022,17 +14969,7 @@ function detectPython(ctx) {
|
|
|
15022
14969
|
const callText = call.text();
|
|
15023
14970
|
if (!isDbCallPython(callText))
|
|
15024
14971
|
continue;
|
|
15025
|
-
findings.push(
|
|
15026
|
-
detectorId: "n-plus-one-query",
|
|
15027
|
-
message: "Database call inside a loop — potential N+1 query",
|
|
15028
|
-
severity: "warning",
|
|
15029
|
-
file: ctx.file.path,
|
|
15030
|
-
line: range.start.line + 1,
|
|
15031
|
-
column: range.start.column + 1,
|
|
15032
|
-
endLine: range.end.line + 1,
|
|
15033
|
-
endColumn: range.end.column + 1,
|
|
15034
|
-
suggestion: "Batch the operation outside the loop (e.g., use WHERE IN or bulk query)"
|
|
15035
|
-
});
|
|
14972
|
+
findings.push(makeFinding("n-plus-one-query", ctx, call, "Database call inside a loop — potential N+1 query", "warning", "Batch the operation outside the loop (e.g., use WHERE IN or bulk query)"));
|
|
15036
14973
|
}
|
|
15037
14974
|
return findings;
|
|
15038
14975
|
}
|
|
@@ -15130,18 +15067,7 @@ function detectJavaScript2(ctx) {
|
|
|
15130
15067
|
continue;
|
|
15131
15068
|
if (!looksLikeDbCall(callText))
|
|
15132
15069
|
continue;
|
|
15133
|
-
const
|
|
15134
|
-
findings.push({
|
|
15135
|
-
detectorId: "unchecked-db-result",
|
|
15136
|
-
message: "Database mutation result is not checked — errors will be silently ignored",
|
|
15137
|
-
severity: "warning",
|
|
15138
|
-
file: ctx.file.path,
|
|
15139
|
-
line: range.start.line + 1,
|
|
15140
|
-
column: range.start.column + 1,
|
|
15141
|
-
endLine: range.end.line + 1,
|
|
15142
|
-
endColumn: range.end.column + 1,
|
|
15143
|
-
suggestion: "Store the result and check for errors: const result = await db.insert(...)"
|
|
15144
|
-
});
|
|
15070
|
+
findings.push(makeFinding("unchecked-db-result", ctx, stmt, "Database mutation result is not checked — errors will be silently ignored", "warning", "Store the result and check for errors: const result = await db.insert(...)"));
|
|
15145
15071
|
}
|
|
15146
15072
|
return findings;
|
|
15147
15073
|
}
|
|
@@ -15189,18 +15115,7 @@ function detectPython2(ctx) {
|
|
|
15189
15115
|
}
|
|
15190
15116
|
if (!isMutation)
|
|
15191
15117
|
continue;
|
|
15192
|
-
|
|
15193
|
-
findings.push({
|
|
15194
|
-
detectorId: "unchecked-db-result",
|
|
15195
|
-
message: "Database mutation result is not checked — errors may be silently ignored",
|
|
15196
|
-
severity: "warning",
|
|
15197
|
-
file: ctx.file.path,
|
|
15198
|
-
line: range.start.line + 1,
|
|
15199
|
-
column: range.start.column + 1,
|
|
15200
|
-
endLine: range.end.line + 1,
|
|
15201
|
-
endColumn: range.end.column + 1,
|
|
15202
|
-
suggestion: "Store the result and verify the operation succeeded"
|
|
15203
|
-
});
|
|
15118
|
+
findings.push(makeFinding("unchecked-db-result", ctx, stmt, "Database mutation result is not checked — errors may be silently ignored", "warning", "Store the result and verify the operation succeeded"));
|
|
15204
15119
|
}
|
|
15205
15120
|
return findings;
|
|
15206
15121
|
}
|
|
@@ -15236,18 +15151,7 @@ function detectJavaScript3(ctx) {
|
|
|
15236
15151
|
const ifText = consequent.text().replace(/\s+/g, " ").trim();
|
|
15237
15152
|
const elseText = elseBody.text().replace(/\s+/g, " ").trim();
|
|
15238
15153
|
if (ifText === elseText && ifText.length > 4) {
|
|
15239
|
-
|
|
15240
|
-
findings.push({
|
|
15241
|
-
detectorId: "dead-code-path",
|
|
15242
|
-
message: "if and else branches are identical — condition has no effect",
|
|
15243
|
-
severity: "warning",
|
|
15244
|
-
file: ctx.file.path,
|
|
15245
|
-
line: range.start.line + 1,
|
|
15246
|
-
column: range.start.column + 1,
|
|
15247
|
-
endLine: range.end.line + 1,
|
|
15248
|
-
endColumn: range.end.column + 1,
|
|
15249
|
-
suggestion: "Remove the conditional and keep only the body, or fix the branch logic"
|
|
15250
|
-
});
|
|
15154
|
+
findings.push(makeFinding("dead-code-path", ctx, ifStmt, "if and else branches are identical — condition has no effect", "warning", "Remove the conditional and keep only the body, or fix the branch logic"));
|
|
15251
15155
|
}
|
|
15252
15156
|
}
|
|
15253
15157
|
const blocks = root.findAll({ rule: { kind: "statement_block" } });
|
|
@@ -15256,18 +15160,7 @@ function detectJavaScript3(ctx) {
|
|
|
15256
15160
|
let foundTerminator = false;
|
|
15257
15161
|
for (const stmt of stmts) {
|
|
15258
15162
|
if (foundTerminator) {
|
|
15259
|
-
|
|
15260
|
-
findings.push({
|
|
15261
|
-
detectorId: "dead-code-path",
|
|
15262
|
-
message: "Unreachable code after return/throw statement",
|
|
15263
|
-
severity: "warning",
|
|
15264
|
-
file: ctx.file.path,
|
|
15265
|
-
line: range.start.line + 1,
|
|
15266
|
-
column: range.start.column + 1,
|
|
15267
|
-
endLine: range.end.line + 1,
|
|
15268
|
-
endColumn: range.end.column + 1,
|
|
15269
|
-
suggestion: "Remove unreachable code or fix the control flow"
|
|
15270
|
-
});
|
|
15163
|
+
findings.push(makeFinding("dead-code-path", ctx, stmt, "Unreachable code after return/throw statement", "warning", "Remove unreachable code or fix the control flow"));
|
|
15271
15164
|
break;
|
|
15272
15165
|
}
|
|
15273
15166
|
if (stmt.kind() === "return_statement" || stmt.kind() === "throw_statement") {
|
|
@@ -15291,18 +15184,7 @@ function detectPython3(ctx) {
|
|
|
15291
15184
|
const ifText = blocks[0].text().replace(/\s+/g, " ").trim();
|
|
15292
15185
|
const elseText = elseBlock.text().replace(/\s+/g, " ").trim();
|
|
15293
15186
|
if (ifText === elseText && ifText.length > 4) {
|
|
15294
|
-
|
|
15295
|
-
findings.push({
|
|
15296
|
-
detectorId: "dead-code-path",
|
|
15297
|
-
message: "if and else branches are identical — condition has no effect",
|
|
15298
|
-
severity: "warning",
|
|
15299
|
-
file: ctx.file.path,
|
|
15300
|
-
line: range.start.line + 1,
|
|
15301
|
-
column: range.start.column + 1,
|
|
15302
|
-
endLine: range.end.line + 1,
|
|
15303
|
-
endColumn: range.end.column + 1,
|
|
15304
|
-
suggestion: "Remove the conditional and keep only the body, or fix the branch logic"
|
|
15305
|
-
});
|
|
15187
|
+
findings.push(makeFinding("dead-code-path", ctx, ifStmt, "if and else branches are identical — condition has no effect", "warning", "Remove the conditional and keep only the body, or fix the branch logic"));
|
|
15306
15188
|
}
|
|
15307
15189
|
}
|
|
15308
15190
|
}
|
|
@@ -15344,15 +15226,7 @@ function detect(ctx) {
|
|
|
15344
15226
|
const match = line.match(doubleAssertRe) || line.match(doubleAssertRe2);
|
|
15345
15227
|
if (!match)
|
|
15346
15228
|
continue;
|
|
15347
|
-
findings.push(
|
|
15348
|
-
detectorId: "double-type-assertion",
|
|
15349
|
-
message: "Double type assertion (as unknown as X) bypasses TypeScript's type safety",
|
|
15350
|
-
severity: "warning",
|
|
15351
|
-
file: ctx.file.path,
|
|
15352
|
-
line: i + 1,
|
|
15353
|
-
column: (match.index ?? 0) + 1,
|
|
15354
|
-
suggestion: "Fix the underlying type mismatch instead of using double assertion. Add a proper type guard or fix the type definition."
|
|
15355
|
-
});
|
|
15229
|
+
findings.push(makeLineFinding("double-type-assertion", ctx, i + 1, (match.index ?? 0) + 1, "Double type assertion (as unknown as X) bypasses TypeScript's type safety", "warning", "Fix the underlying type mismatch instead of using double assertion. Add a proper type guard or fix the type definition."));
|
|
15356
15230
|
}
|
|
15357
15231
|
return findings;
|
|
15358
15232
|
}
|
|
@@ -15417,17 +15291,7 @@ function detect2(ctx) {
|
|
|
15417
15291
|
if (anyLocations.length <= threshold)
|
|
15418
15292
|
return findings;
|
|
15419
15293
|
for (const loc of anyLocations) {
|
|
15420
|
-
findings.push({
|
|
15421
|
-
detectorId: "excessive-any",
|
|
15422
|
-
message: `Excessive use of 'any' type (${anyLocations.length} in this file) — weakens type safety`,
|
|
15423
|
-
severity: "warning",
|
|
15424
|
-
file: ctx.file.path,
|
|
15425
|
-
line: loc.line,
|
|
15426
|
-
column: loc.column,
|
|
15427
|
-
endLine: loc.endLine,
|
|
15428
|
-
endColumn: loc.endColumn,
|
|
15429
|
-
suggestion: "Replace with a specific type, unknown, or a generic type parameter"
|
|
15430
|
-
});
|
|
15294
|
+
findings.push(makeLineFinding("excessive-any", ctx, loc.line, loc.column, `Excessive use of 'any' type (${anyLocations.length} in this file) — weakens type safety`, "warning", "Replace with a specific type, unknown, or a generic type parameter", loc.endLine, loc.endColumn));
|
|
15431
15295
|
}
|
|
15432
15296
|
return findings;
|
|
15433
15297
|
}
|
|
@@ -15466,18 +15330,7 @@ function detectJavaScript4(ctx) {
|
|
|
15466
15330
|
const method = property.text();
|
|
15467
15331
|
if (!DEBUG_METHODS.has(method))
|
|
15468
15332
|
continue;
|
|
15469
|
-
|
|
15470
|
-
findings.push({
|
|
15471
|
-
detectorId: "debug-console-in-prod",
|
|
15472
|
-
message: `console.${method}() left in production code`,
|
|
15473
|
-
severity: "warning",
|
|
15474
|
-
file: ctx.file.path,
|
|
15475
|
-
line: range.start.line + 1,
|
|
15476
|
-
column: range.start.column + 1,
|
|
15477
|
-
endLine: range.end.line + 1,
|
|
15478
|
-
endColumn: range.end.column + 1,
|
|
15479
|
-
suggestion: "Remove debug logging or replace with a structured logger"
|
|
15480
|
-
});
|
|
15333
|
+
findings.push(makeFinding("debug-console-in-prod", ctx, call, `console.${method}() left in production code`, "warning", "Remove debug logging or replace with a structured logger"));
|
|
15481
15334
|
}
|
|
15482
15335
|
return findings;
|
|
15483
15336
|
}
|
|
@@ -15491,18 +15344,7 @@ function detectPython4(ctx) {
|
|
|
15491
15344
|
const callText = call.text();
|
|
15492
15345
|
if (!callText.startsWith("print("))
|
|
15493
15346
|
continue;
|
|
15494
|
-
|
|
15495
|
-
findings.push({
|
|
15496
|
-
detectorId: "debug-console-in-prod",
|
|
15497
|
-
message: "print() left in production code",
|
|
15498
|
-
severity: "info",
|
|
15499
|
-
file: ctx.file.path,
|
|
15500
|
-
line: range.start.line + 1,
|
|
15501
|
-
column: range.start.column + 1,
|
|
15502
|
-
endLine: range.end.line + 1,
|
|
15503
|
-
endColumn: range.end.column + 1,
|
|
15504
|
-
suggestion: "Remove debug print or replace with logging module"
|
|
15505
|
-
});
|
|
15347
|
+
findings.push(makeFinding("debug-console-in-prod", ctx, call, "print() left in production code", "info", "Remove debug print or replace with logging module"));
|
|
15506
15348
|
}
|
|
15507
15349
|
return findings;
|
|
15508
15350
|
}
|
|
@@ -15541,15 +15383,7 @@ function detect3(ctx) {
|
|
|
15541
15383
|
if (!isComment)
|
|
15542
15384
|
continue;
|
|
15543
15385
|
const hasSecurityImplication = SECURITY_KEYWORDS.test(line);
|
|
15544
|
-
findings.push({
|
|
15545
|
-
detectorId: "todo-in-production",
|
|
15546
|
-
message: `${match[1]} comment in production code${hasSecurityImplication ? " (security-related)" : ""}`,
|
|
15547
|
-
severity: hasSecurityImplication ? "warning" : "info",
|
|
15548
|
-
file: ctx.file.path,
|
|
15549
|
-
line: i + 1,
|
|
15550
|
-
column: (match.index ?? 0) + 1,
|
|
15551
|
-
suggestion: "Address the TODO or create a tracked issue and reference it in the comment"
|
|
15552
|
-
});
|
|
15386
|
+
findings.push(makeLineFinding("todo-in-production", ctx, i + 1, (match.index ?? 0) + 1, `${match[1]} comment in production code${hasSecurityImplication ? " (security-related)" : ""}`, hasSecurityImplication ? "warning" : "info", "Address the TODO or create a tracked issue and reference it in the comment"));
|
|
15553
15387
|
}
|
|
15554
15388
|
return findings;
|
|
15555
15389
|
}
|
|
@@ -15599,15 +15433,7 @@ function detect4(ctx) {
|
|
|
15599
15433
|
const hasContext = CONFIG_CONTEXTS.test(line);
|
|
15600
15434
|
if (!hasContext && description === "placeholder value")
|
|
15601
15435
|
continue;
|
|
15602
|
-
findings.push({
|
|
15603
|
-
detectorId: "placeholder-in-production",
|
|
15604
|
-
message: `Placeholder ${description} found: ${match[0].slice(0, 40)}`,
|
|
15605
|
-
severity: "error",
|
|
15606
|
-
file: ctx.file.path,
|
|
15607
|
-
line: i + 1,
|
|
15608
|
-
column: (match.index ?? 0) + 1,
|
|
15609
|
-
suggestion: "Replace with actual configuration value or use environment variable"
|
|
15610
|
-
});
|
|
15436
|
+
findings.push(makeLineFinding("placeholder-in-production", ctx, i + 1, (match.index ?? 0) + 1, `Placeholder ${description} found: ${match[0].slice(0, 40)}`, "error", "Replace with actual configuration value or use environment variable"));
|
|
15611
15437
|
break;
|
|
15612
15438
|
}
|
|
15613
15439
|
}
|
|
@@ -15658,18 +15484,7 @@ function detect5(ctx) {
|
|
|
15658
15484
|
if (!SENSITIVE_KEYS.test(keyArg.text()))
|
|
15659
15485
|
continue;
|
|
15660
15486
|
const storage = object.text();
|
|
15661
|
-
|
|
15662
|
-
findings.push({
|
|
15663
|
-
detectorId: "token-in-localstorage",
|
|
15664
|
-
message: `Auth token stored in ${storage} — vulnerable to XSS attacks`,
|
|
15665
|
-
severity: "error",
|
|
15666
|
-
file: ctx.file.path,
|
|
15667
|
-
line: range.start.line + 1,
|
|
15668
|
-
column: range.start.column + 1,
|
|
15669
|
-
endLine: range.end.line + 1,
|
|
15670
|
-
endColumn: range.end.column + 1,
|
|
15671
|
-
suggestion: "Use httpOnly cookies for auth tokens instead of browser storage"
|
|
15672
|
-
});
|
|
15487
|
+
findings.push(makeFinding("token-in-localstorage", ctx, call, `Auth token stored in ${storage} — vulnerable to XSS attacks`, "error", "Use httpOnly cookies for auth tokens instead of browser storage"));
|
|
15673
15488
|
}
|
|
15674
15489
|
return findings;
|
|
15675
15490
|
}
|
|
@@ -15740,15 +15555,7 @@ function detect6(ctx) {
|
|
|
15740
15555
|
if (violations.length < 2)
|
|
15741
15556
|
return [];
|
|
15742
15557
|
return [
|
|
15743
|
-
{
|
|
15744
|
-
detectorId: "god-component",
|
|
15745
|
-
message: `Component file has too many hooks (${violations.join(", ")})`,
|
|
15746
|
-
severity: "warning",
|
|
15747
|
-
file: ctx.file.path,
|
|
15748
|
-
line: 1,
|
|
15749
|
-
column: 1,
|
|
15750
|
-
suggestion: "Split this component into smaller, focused components. Extract custom hooks for related state and effects."
|
|
15751
|
-
}
|
|
15558
|
+
makeLineFinding("god-component", ctx, 1, 1, `Component file has too many hooks (${violations.join(", ")})`, "warning", "Split this component into smaller, focused components. Extract custom hooks for related state and effects.")
|
|
15752
15559
|
];
|
|
15753
15560
|
}
|
|
15754
15561
|
var godComponent = {
|
|
@@ -15855,6 +15662,9 @@ function getJsFunctionName(node) {
|
|
|
15855
15662
|
}
|
|
15856
15663
|
return "<anonymous>";
|
|
15857
15664
|
}
|
|
15665
|
+
function buildFinding(ctx, m, severity) {
|
|
15666
|
+
return makeLineFinding("god-function", ctx, m.startLine, m.startColumn, `Function '${m.name}' is too complex (${m.lines} lines, cyclomatic complexity ${m.complexity}, ${m.params} params)`, severity, "Break this function into smaller, focused functions. Extract helper methods, use early returns, and reduce branching.", m.endLine, m.endColumn);
|
|
15667
|
+
}
|
|
15858
15668
|
function detectJavaScript5(ctx) {
|
|
15859
15669
|
const findings = [];
|
|
15860
15670
|
const root = ctx.root.root();
|
|
@@ -15946,19 +15756,6 @@ function detectPython5(ctx) {
|
|
|
15946
15756
|
}
|
|
15947
15757
|
return findings;
|
|
15948
15758
|
}
|
|
15949
|
-
function buildFinding(ctx, m, severity) {
|
|
15950
|
-
return {
|
|
15951
|
-
detectorId: "god-function",
|
|
15952
|
-
message: `Function '${m.name}' is too complex (${m.lines} lines, cyclomatic complexity ${m.complexity}, ${m.params} params)`,
|
|
15953
|
-
severity,
|
|
15954
|
-
file: ctx.file.path,
|
|
15955
|
-
line: m.startLine,
|
|
15956
|
-
column: m.startColumn,
|
|
15957
|
-
endLine: m.endLine,
|
|
15958
|
-
endColumn: m.endColumn,
|
|
15959
|
-
suggestion: "Break this function into smaller, focused functions. Extract helper methods, use early returns, and reduce branching."
|
|
15960
|
-
};
|
|
15961
|
-
}
|
|
15962
15759
|
var godFunction = {
|
|
15963
15760
|
id: "god-function",
|
|
15964
15761
|
meta: {
|
|
@@ -16024,35 +15821,13 @@ function detectJavaScript6(ctx) {
|
|
|
16024
15821
|
if (firstArg.kind() === "template_string") {
|
|
16025
15822
|
const hasSubstitution = firstArg.children().some((ch) => ch.kind() === "template_substitution");
|
|
16026
15823
|
if (hasSubstitution) {
|
|
16027
|
-
|
|
16028
|
-
findings.push({
|
|
16029
|
-
detectorId: "sql-injection",
|
|
16030
|
-
message: "SQL query uses template literal with interpolation — potential SQL injection",
|
|
16031
|
-
severity: "error",
|
|
16032
|
-
file: ctx.file.path,
|
|
16033
|
-
line: range.start.line + 1,
|
|
16034
|
-
column: range.start.column + 1,
|
|
16035
|
-
endLine: range.end.line + 1,
|
|
16036
|
-
endColumn: range.end.column + 1,
|
|
16037
|
-
suggestion: "Use parameterized queries instead: db.query('SELECT * FROM users WHERE id = $1', [userId])"
|
|
16038
|
-
});
|
|
15824
|
+
findings.push(makeFinding("sql-injection", ctx, call, "SQL query uses template literal with interpolation — potential SQL injection", "error", "Use parameterized queries instead: db.query('SELECT * FROM users WHERE id = $1', [userId])"));
|
|
16039
15825
|
}
|
|
16040
15826
|
}
|
|
16041
15827
|
if (firstArg.kind() === "binary_expression") {
|
|
16042
15828
|
const text = firstArg.text();
|
|
16043
15829
|
if (/\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN|DROP|ALTER|CREATE)\b/i.test(text)) {
|
|
16044
|
-
|
|
16045
|
-
findings.push({
|
|
16046
|
-
detectorId: "sql-injection",
|
|
16047
|
-
message: "SQL query built with string concatenation — potential SQL injection",
|
|
16048
|
-
severity: "error",
|
|
16049
|
-
file: ctx.file.path,
|
|
16050
|
-
line: range.start.line + 1,
|
|
16051
|
-
column: range.start.column + 1,
|
|
16052
|
-
endLine: range.end.line + 1,
|
|
16053
|
-
endColumn: range.end.column + 1,
|
|
16054
|
-
suggestion: "Use parameterized queries instead of string concatenation"
|
|
16055
|
-
});
|
|
15830
|
+
findings.push(makeFinding("sql-injection", ctx, call, "SQL query built with string concatenation — potential SQL injection", "error", "Use parameterized queries instead of string concatenation"));
|
|
16056
15831
|
}
|
|
16057
15832
|
}
|
|
16058
15833
|
}
|
|
@@ -16077,51 +15852,18 @@ function detectPython6(ctx) {
|
|
|
16077
15852
|
continue;
|
|
16078
15853
|
const firstArg = args[0];
|
|
16079
15854
|
if (firstArg.kind() === "string" && firstArg.text().startsWith("f")) {
|
|
16080
|
-
|
|
16081
|
-
findings.push({
|
|
16082
|
-
detectorId: "sql-injection",
|
|
16083
|
-
message: "SQL query uses f-string — potential SQL injection",
|
|
16084
|
-
severity: "error",
|
|
16085
|
-
file: ctx.file.path,
|
|
16086
|
-
line: range.start.line + 1,
|
|
16087
|
-
column: range.start.column + 1,
|
|
16088
|
-
endLine: range.end.line + 1,
|
|
16089
|
-
endColumn: range.end.column + 1,
|
|
16090
|
-
suggestion: "Use parameterized queries: cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,))"
|
|
16091
|
-
});
|
|
15855
|
+
findings.push(makeFinding("sql-injection", ctx, call, "SQL query uses f-string — potential SQL injection", "error", "Use parameterized queries: cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,))"));
|
|
16092
15856
|
}
|
|
16093
15857
|
if (firstArg.kind() === "call" && firstArg.text().includes(".format(")) {
|
|
16094
15858
|
const text = firstArg.text();
|
|
16095
15859
|
if (/\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)\b/i.test(text)) {
|
|
16096
|
-
|
|
16097
|
-
findings.push({
|
|
16098
|
-
detectorId: "sql-injection",
|
|
16099
|
-
message: "SQL query uses .format() — potential SQL injection",
|
|
16100
|
-
severity: "error",
|
|
16101
|
-
file: ctx.file.path,
|
|
16102
|
-
line: range.start.line + 1,
|
|
16103
|
-
column: range.start.column + 1,
|
|
16104
|
-
endLine: range.end.line + 1,
|
|
16105
|
-
endColumn: range.end.column + 1,
|
|
16106
|
-
suggestion: "Use parameterized queries instead of .format()"
|
|
16107
|
-
});
|
|
15860
|
+
findings.push(makeFinding("sql-injection", ctx, call, "SQL query uses .format() — potential SQL injection", "error", "Use parameterized queries instead of .format()"));
|
|
16108
15861
|
}
|
|
16109
15862
|
}
|
|
16110
15863
|
if (firstArg.kind() === "binary_expression" || firstArg.kind() === "string" && args.length >= 2) {
|
|
16111
15864
|
const stmtText = call.text();
|
|
16112
15865
|
if (stmtText.includes(" % ") && /\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)\b/i.test(stmtText)) {
|
|
16113
|
-
|
|
16114
|
-
findings.push({
|
|
16115
|
-
detectorId: "sql-injection",
|
|
16116
|
-
message: "SQL query uses % formatting — potential SQL injection",
|
|
16117
|
-
severity: "error",
|
|
16118
|
-
file: ctx.file.path,
|
|
16119
|
-
line: range.start.line + 1,
|
|
16120
|
-
column: range.start.column + 1,
|
|
16121
|
-
endLine: range.end.line + 1,
|
|
16122
|
-
endColumn: range.end.column + 1,
|
|
16123
|
-
suggestion: "Use parameterized queries: cursor.execute('SELECT ... WHERE id = %s', (value,))"
|
|
16124
|
-
});
|
|
15866
|
+
findings.push(makeFinding("sql-injection", ctx, call, "SQL query uses % formatting — potential SQL injection", "error", "Use parameterized queries: cursor.execute('SELECT ... WHERE id = %s', (value,))"));
|
|
16125
15867
|
}
|
|
16126
15868
|
}
|
|
16127
15869
|
}
|
|
@@ -16159,18 +15901,7 @@ function detect7(ctx) {
|
|
|
16159
15901
|
continue;
|
|
16160
15902
|
if (name.text() !== "dangerouslySetInnerHTML")
|
|
16161
15903
|
continue;
|
|
16162
|
-
|
|
16163
|
-
findings.push({
|
|
16164
|
-
detectorId: "dangerous-inner-html",
|
|
16165
|
-
message: "dangerouslySetInnerHTML can lead to XSS attacks if the content is not sanitized",
|
|
16166
|
-
severity: "warning",
|
|
16167
|
-
file: ctx.file.path,
|
|
16168
|
-
line: range.start.line + 1,
|
|
16169
|
-
column: range.start.column + 1,
|
|
16170
|
-
endLine: range.end.line + 1,
|
|
16171
|
-
endColumn: range.end.column + 1,
|
|
16172
|
-
suggestion: "Use a sanitization library like DOMPurify: dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(content)}}"
|
|
16173
|
-
});
|
|
15904
|
+
findings.push(makeFinding("dangerous-inner-html", ctx, attr, "dangerouslySetInnerHTML can lead to XSS attacks if the content is not sanitized", "warning", "Use a sanitization library like DOMPurify: dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(content)}}"));
|
|
16174
15905
|
}
|
|
16175
15906
|
return findings;
|
|
16176
15907
|
}
|
|
@@ -16287,18 +16018,7 @@ function detectJavaScript7(ctx) {
|
|
|
16287
16018
|
}
|
|
16288
16019
|
if (chainHasLimit)
|
|
16289
16020
|
continue;
|
|
16290
|
-
|
|
16291
|
-
findings.push({
|
|
16292
|
-
detectorId: "unbounded-query",
|
|
16293
|
-
message: "Query fetches multiple records without a limit — may return excessive data",
|
|
16294
|
-
severity: "info",
|
|
16295
|
-
file: ctx.file.path,
|
|
16296
|
-
line: range.start.line + 1,
|
|
16297
|
-
column: range.start.column + 1,
|
|
16298
|
-
endLine: range.end.line + 1,
|
|
16299
|
-
endColumn: range.end.column + 1,
|
|
16300
|
-
suggestion: "Add a limit: findMany({ take: 100 }) or .limit(100)"
|
|
16301
|
-
});
|
|
16021
|
+
findings.push(makeFinding("unbounded-query", ctx, call, "Query fetches multiple records without a limit — may return excessive data", "info", "Add a limit: findMany({ take: 100 }) or .limit(100)"));
|
|
16302
16022
|
}
|
|
16303
16023
|
return findings;
|
|
16304
16024
|
}
|
|
@@ -16391,26 +16111,10 @@ function detect8(ctx) {
|
|
|
16391
16111
|
}
|
|
16392
16112
|
}
|
|
16393
16113
|
if (hasUIImport && hasDBImport) {
|
|
16394
|
-
findings.push({
|
|
16395
|
-
detectorId: "mixed-concerns",
|
|
16396
|
-
message: `File imports both UI framework (${uiImportName}) and database (${dbImportName}) — mixed concerns`,
|
|
16397
|
-
severity: "warning",
|
|
16398
|
-
file: ctx.file.path,
|
|
16399
|
-
line: 1,
|
|
16400
|
-
column: 1,
|
|
16401
|
-
suggestion: "Separate UI rendering from data access. Move database logic to a service/API layer."
|
|
16402
|
-
});
|
|
16114
|
+
findings.push(makeLineFinding("mixed-concerns", ctx, 1, 1, `File imports both UI framework (${uiImportName}) and database (${dbImportName}) — mixed concerns`, "warning", "Separate UI rendering from data access. Move database logic to a service/API layer."));
|
|
16403
16115
|
}
|
|
16404
16116
|
if (hasUIImport && hasServerImport) {
|
|
16405
|
-
findings.push({
|
|
16406
|
-
detectorId: "mixed-concerns",
|
|
16407
|
-
message: `File imports both UI framework (${uiImportName}) and server framework — mixed concerns`,
|
|
16408
|
-
severity: "warning",
|
|
16409
|
-
file: ctx.file.path,
|
|
16410
|
-
line: 1,
|
|
16411
|
-
column: 1,
|
|
16412
|
-
suggestion: "Separate UI components from server-side logic."
|
|
16413
|
-
});
|
|
16117
|
+
findings.push(makeLineFinding("mixed-concerns", ctx, 1, 1, `File imports both UI framework (${uiImportName}) and server framework — mixed concerns`, "warning", "Separate UI components from server-side logic."));
|
|
16414
16118
|
}
|
|
16415
16119
|
return findings;
|
|
16416
16120
|
}
|
|
@@ -16425,36 +16129,770 @@ var mixedConcerns = {
|
|
|
16425
16129
|
},
|
|
16426
16130
|
detect: detect8
|
|
16427
16131
|
};
|
|
16428
|
-
// src/detectors/
|
|
16429
|
-
var
|
|
16430
|
-
|
|
16431
|
-
|
|
16432
|
-
|
|
16433
|
-
|
|
16434
|
-
|
|
16435
|
-
|
|
16436
|
-
|
|
16437
|
-
|
|
16438
|
-
|
|
16439
|
-
|
|
16440
|
-
|
|
16441
|
-
|
|
16442
|
-
|
|
16443
|
-
|
|
16444
|
-
|
|
16132
|
+
// src/detectors/unsafe-shell-exec.ts
|
|
16133
|
+
var EXEC_FUNCTIONS = new Set(["exec", "execSync"]);
|
|
16134
|
+
var SUBPROCESS_METHODS = new Set(["run", "call", "Popen", "check_output", "check_call"]);
|
|
16135
|
+
function detectJavaScript8(ctx) {
|
|
16136
|
+
const findings = [];
|
|
16137
|
+
const root = ctx.root.root();
|
|
16138
|
+
const callExprs = root.findAll({ rule: { kind: "call_expression" } });
|
|
16139
|
+
for (const call of callExprs) {
|
|
16140
|
+
const children = call.children();
|
|
16141
|
+
const callee = children[0];
|
|
16142
|
+
if (!callee)
|
|
16143
|
+
continue;
|
|
16144
|
+
let isExecCall = false;
|
|
16145
|
+
if (callee.kind() === "identifier" && EXEC_FUNCTIONS.has(callee.text())) {
|
|
16146
|
+
isExecCall = true;
|
|
16147
|
+
} else if (callee.kind() === "member_expression") {
|
|
16148
|
+
const text = callee.text();
|
|
16149
|
+
for (const fn of EXEC_FUNCTIONS) {
|
|
16150
|
+
if (text.endsWith(`.${fn}`)) {
|
|
16151
|
+
isExecCall = true;
|
|
16152
|
+
break;
|
|
16153
|
+
}
|
|
16154
|
+
}
|
|
16155
|
+
}
|
|
16156
|
+
if (!isExecCall)
|
|
16157
|
+
continue;
|
|
16158
|
+
const args = children.find((ch) => ch.kind() === "arguments");
|
|
16159
|
+
if (!args)
|
|
16160
|
+
continue;
|
|
16161
|
+
const argNodes = args.children().filter((ch) => ch.kind() !== "(" && ch.kind() !== ")" && ch.kind() !== ",");
|
|
16162
|
+
if (argNodes.length === 0)
|
|
16163
|
+
continue;
|
|
16164
|
+
const firstArg = argNodes[0];
|
|
16165
|
+
const argKind = firstArg.kind();
|
|
16166
|
+
if (argKind === "string" || argKind === "string_fragment")
|
|
16167
|
+
continue;
|
|
16168
|
+
findings.push(makeFinding("unsafe-shell-exec", ctx, call, `${callee.text()}() called with dynamic argument — risk of shell injection`, "error", "Use execFile() or spawn() with an argument array instead of exec() with string interpolation"));
|
|
16169
|
+
}
|
|
16170
|
+
return findings;
|
|
16171
|
+
}
|
|
16172
|
+
function detectPython7(ctx) {
|
|
16173
|
+
const findings = [];
|
|
16174
|
+
const root = ctx.root.root();
|
|
16175
|
+
const calls = root.findAll({ rule: { kind: "call" } });
|
|
16176
|
+
for (const call of calls) {
|
|
16177
|
+
const children = call.children();
|
|
16178
|
+
const callee = children[0];
|
|
16179
|
+
if (!callee)
|
|
16180
|
+
continue;
|
|
16181
|
+
const calleeText = callee.text();
|
|
16182
|
+
let isSubprocessCall = false;
|
|
16183
|
+
for (const method of SUBPROCESS_METHODS) {
|
|
16184
|
+
if (calleeText === `subprocess.${method}` || calleeText.endsWith(`.${method}`)) {
|
|
16185
|
+
isSubprocessCall = true;
|
|
16186
|
+
break;
|
|
16187
|
+
}
|
|
16188
|
+
}
|
|
16189
|
+
if (!isSubprocessCall)
|
|
16190
|
+
continue;
|
|
16191
|
+
const argList = children.find((ch) => ch.kind() === "argument_list");
|
|
16192
|
+
if (!argList)
|
|
16193
|
+
continue;
|
|
16194
|
+
const kwargs = argList.children().filter((ch) => ch.kind() === "keyword_argument");
|
|
16195
|
+
let hasShellTrue = false;
|
|
16196
|
+
for (const kwarg of kwargs) {
|
|
16197
|
+
const kwChildren = kwarg.children();
|
|
16198
|
+
const key = kwChildren.find((ch) => ch.kind() === "identifier");
|
|
16199
|
+
const value = kwChildren.find((ch) => ch.kind() === "true");
|
|
16200
|
+
if (key && value && key.text() === "shell") {
|
|
16201
|
+
hasShellTrue = true;
|
|
16202
|
+
break;
|
|
16203
|
+
}
|
|
16204
|
+
}
|
|
16205
|
+
if (!hasShellTrue)
|
|
16206
|
+
continue;
|
|
16207
|
+
const positionalArgs = argList.children().filter((ch) => ch.kind() !== "(" && ch.kind() !== ")" && ch.kind() !== "," && ch.kind() !== "keyword_argument");
|
|
16208
|
+
if (positionalArgs.length === 0)
|
|
16209
|
+
continue;
|
|
16210
|
+
const firstArg = positionalArgs[0];
|
|
16211
|
+
const argKind = firstArg.kind();
|
|
16212
|
+
if (argKind === "string") {
|
|
16213
|
+
const text = firstArg.text();
|
|
16214
|
+
if (!text.startsWith('f"') && !text.startsWith("f'")) {
|
|
16215
|
+
continue;
|
|
16216
|
+
}
|
|
16217
|
+
} else if (argKind === "concatenated_string") {} else if (argKind !== "identifier" && argKind !== "binary_operator") {
|
|
16218
|
+
continue;
|
|
16219
|
+
}
|
|
16220
|
+
findings.push(makeFinding("unsafe-shell-exec", ctx, call, `${calleeText}() called with shell=True and dynamic argument — risk of shell injection`, "error", "Pass arguments as a list and remove shell=True: subprocess.run(['cmd', arg])"));
|
|
16221
|
+
}
|
|
16222
|
+
return findings;
|
|
16223
|
+
}
|
|
16224
|
+
var unsafeShellExec = {
|
|
16225
|
+
id: "unsafe-shell-exec",
|
|
16226
|
+
meta: {
|
|
16227
|
+
name: "Unsafe Shell Execution",
|
|
16228
|
+
description: "Detects shell command execution with dynamic arguments that may be vulnerable to injection",
|
|
16229
|
+
severity: "error",
|
|
16230
|
+
category: "security",
|
|
16231
|
+
languages: ["javascript", "typescript", "tsx", "python"],
|
|
16232
|
+
priority: 10
|
|
16233
|
+
},
|
|
16234
|
+
detect(ctx) {
|
|
16235
|
+
if (ctx.file.language === "python")
|
|
16236
|
+
return detectPython7(ctx);
|
|
16237
|
+
return detectJavaScript8(ctx);
|
|
16238
|
+
}
|
|
16239
|
+
};
|
|
16240
|
+
// src/detectors/llm-call-no-timeout.ts
|
|
16241
|
+
var LLM_CONSTRUCTORS = new Set(["OpenAI", "Anthropic"]);
|
|
16242
|
+
function hasProperty(objectNode, propName) {
|
|
16243
|
+
const pairs = objectNode.findAll({ rule: { kind: "pair" } });
|
|
16244
|
+
for (const pair of pairs) {
|
|
16245
|
+
const children = pair.children();
|
|
16246
|
+
const key = children.find((ch) => ch.kind() === "property_identifier" || ch.kind() === "string" || ch.kind() === "shorthand_property_identifier");
|
|
16247
|
+
if (key && key.text().replace(/["']/g, "") === propName) {
|
|
16248
|
+
return true;
|
|
16249
|
+
}
|
|
16250
|
+
}
|
|
16251
|
+
const shorthandProps = objectNode.findAll({ rule: { kind: "shorthand_property_identifier" } });
|
|
16252
|
+
for (const sp of shorthandProps) {
|
|
16253
|
+
if (sp.text() === propName)
|
|
16254
|
+
return true;
|
|
16255
|
+
}
|
|
16256
|
+
return false;
|
|
16257
|
+
}
|
|
16258
|
+
function detectJavaScript9(ctx) {
|
|
16259
|
+
const findings = [];
|
|
16260
|
+
const root = ctx.root.root();
|
|
16261
|
+
const newExprs = root.findAll({ rule: { kind: "new_expression" } });
|
|
16262
|
+
for (const newExpr of newExprs) {
|
|
16263
|
+
const children = newExpr.children();
|
|
16264
|
+
const constructorNode = children.find((ch) => ch.kind() === "identifier");
|
|
16265
|
+
if (!constructorNode || !LLM_CONSTRUCTORS.has(constructorNode.text()))
|
|
16266
|
+
continue;
|
|
16267
|
+
const args = children.find((ch) => ch.kind() === "arguments");
|
|
16268
|
+
if (!args) {
|
|
16269
|
+
findings.push(makeFinding("llm-call-no-timeout", ctx, newExpr, `new ${constructorNode.text()}() called without timeout option`, "warning", `Pass a timeout option: new ${constructorNode.text()}({ timeout: 30000 })`));
|
|
16270
|
+
continue;
|
|
16271
|
+
}
|
|
16272
|
+
const argNodes = args.children().filter((ch) => ch.kind() !== "(" && ch.kind() !== ")" && ch.kind() !== ",");
|
|
16273
|
+
if (argNodes.length === 0) {
|
|
16274
|
+
findings.push(makeFinding("llm-call-no-timeout", ctx, newExpr, `new ${constructorNode.text()}() called without timeout option`, "warning", `Pass a timeout option: new ${constructorNode.text()}({ timeout: 30000 })`));
|
|
16275
|
+
continue;
|
|
16276
|
+
}
|
|
16277
|
+
const firstArg = argNodes[0];
|
|
16278
|
+
if (firstArg.kind() === "object") {
|
|
16279
|
+
if (!hasProperty(firstArg, "timeout")) {
|
|
16280
|
+
findings.push(makeFinding("llm-call-no-timeout", ctx, newExpr, `new ${constructorNode.text()}() called without timeout option`, "warning", `Add timeout to options: new ${constructorNode.text()}({ timeout: 30000, ... })`));
|
|
16281
|
+
}
|
|
16282
|
+
}
|
|
16283
|
+
}
|
|
16284
|
+
const callExprs = root.findAll({ rule: { kind: "call_expression" } });
|
|
16285
|
+
for (const call of callExprs) {
|
|
16286
|
+
const children = call.children();
|
|
16287
|
+
const callee = children[0];
|
|
16288
|
+
if (!callee || callee.kind() !== "member_expression")
|
|
16289
|
+
continue;
|
|
16290
|
+
const calleeText = callee.text();
|
|
16291
|
+
if (!calleeText.endsWith(".create"))
|
|
16292
|
+
continue;
|
|
16293
|
+
if (!calleeText.includes("completions") && !calleeText.includes("messages") && !calleeText.includes("chat")) {
|
|
16294
|
+
continue;
|
|
16295
|
+
}
|
|
16296
|
+
const args = children.find((ch) => ch.kind() === "arguments");
|
|
16297
|
+
if (!args)
|
|
16298
|
+
continue;
|
|
16299
|
+
const argNodes = args.children().filter((ch) => ch.kind() !== "(" && ch.kind() !== ")" && ch.kind() !== ",");
|
|
16300
|
+
if (argNodes.length === 0)
|
|
16301
|
+
continue;
|
|
16302
|
+
const firstArg = argNodes[0];
|
|
16303
|
+
if (firstArg.kind() === "object") {
|
|
16304
|
+
if (!hasProperty(firstArg, "max_tokens")) {
|
|
16305
|
+
findings.push(makeFinding("llm-call-no-timeout", ctx, call, ".create() called without max_tokens — response size is unbounded", "warning", "Add max_tokens to limit response size: .create({ max_tokens: 1000, ... })"));
|
|
16306
|
+
}
|
|
16307
|
+
}
|
|
16308
|
+
}
|
|
16309
|
+
return findings;
|
|
16310
|
+
}
|
|
16311
|
+
function detectPython8(ctx) {
|
|
16312
|
+
const findings = [];
|
|
16313
|
+
const root = ctx.root.root();
|
|
16314
|
+
const calls = root.findAll({ rule: { kind: "call" } });
|
|
16315
|
+
for (const call of calls) {
|
|
16316
|
+
const children = call.children();
|
|
16317
|
+
const callee = children[0];
|
|
16318
|
+
if (!callee)
|
|
16319
|
+
continue;
|
|
16320
|
+
const calleeText = callee.text();
|
|
16321
|
+
if (!calleeText.endsWith(".create"))
|
|
16322
|
+
continue;
|
|
16323
|
+
if (!calleeText.includes("completions") && !calleeText.includes("Completion") && !calleeText.includes("messages") && !calleeText.includes("chat")) {
|
|
16324
|
+
continue;
|
|
16325
|
+
}
|
|
16326
|
+
const argList = children.find((ch) => ch.kind() === "argument_list");
|
|
16327
|
+
if (!argList)
|
|
16328
|
+
continue;
|
|
16329
|
+
const kwargs = argList.children().filter((ch) => ch.kind() === "keyword_argument");
|
|
16330
|
+
let hasTimeout = false;
|
|
16331
|
+
for (const kwarg of kwargs) {
|
|
16332
|
+
const kwChildren = kwarg.children();
|
|
16333
|
+
const key = kwChildren.find((ch) => ch.kind() === "identifier");
|
|
16334
|
+
if (key && key.text() === "timeout") {
|
|
16335
|
+
hasTimeout = true;
|
|
16336
|
+
break;
|
|
16337
|
+
}
|
|
16338
|
+
}
|
|
16339
|
+
if (!hasTimeout) {
|
|
16340
|
+
findings.push(makeFinding("llm-call-no-timeout", ctx, call, `${calleeText}() called without timeout — request may hang indefinitely`, "warning", "Add a timeout parameter: .create(timeout=30, ...)"));
|
|
16341
|
+
}
|
|
16342
|
+
}
|
|
16343
|
+
return findings;
|
|
16344
|
+
}
|
|
16345
|
+
var llmCallNoTimeout = {
|
|
16346
|
+
id: "llm-call-no-timeout",
|
|
16347
|
+
meta: {
|
|
16348
|
+
name: "LLM Call No Timeout",
|
|
16349
|
+
description: "Detects LLM API calls (OpenAI, Anthropic) without timeout or max_tokens configuration",
|
|
16350
|
+
severity: "warning",
|
|
16351
|
+
category: "quality",
|
|
16352
|
+
languages: ["javascript", "typescript", "tsx", "python"],
|
|
16353
|
+
priority: 10
|
|
16354
|
+
},
|
|
16355
|
+
detect(ctx) {
|
|
16356
|
+
if (ctx.file.language === "python")
|
|
16357
|
+
return detectPython8(ctx);
|
|
16358
|
+
return detectJavaScript9(ctx);
|
|
16359
|
+
}
|
|
16360
|
+
};
|
|
16361
|
+
// src/detectors/dynamic-code-exec.ts
|
|
16362
|
+
function detectJavaScript10(ctx) {
|
|
16363
|
+
const findings = [];
|
|
16364
|
+
const root = ctx.root.root();
|
|
16365
|
+
const callExprs = root.findAll({ rule: { kind: "call_expression" } });
|
|
16366
|
+
for (const call of callExprs) {
|
|
16367
|
+
const children = call.children();
|
|
16368
|
+
const callee = children[0];
|
|
16369
|
+
if (!callee || callee.kind() !== "identifier" || callee.text() !== "eval")
|
|
16370
|
+
continue;
|
|
16371
|
+
const args = children.find((ch) => ch.kind() === "arguments");
|
|
16372
|
+
if (!args)
|
|
16373
|
+
continue;
|
|
16374
|
+
const argNodes = args.children().filter((ch) => ch.kind() !== "(" && ch.kind() !== ")" && ch.kind() !== ",");
|
|
16375
|
+
if (argNodes.length === 0)
|
|
16376
|
+
continue;
|
|
16377
|
+
const firstArg = argNodes[0];
|
|
16378
|
+
if (firstArg.kind() === "string" || firstArg.kind() === "string_fragment")
|
|
16379
|
+
continue;
|
|
16380
|
+
findings.push(makeFinding("dynamic-code-exec", ctx, call, "eval() called with dynamic argument — arbitrary code execution risk", "error", "Avoid eval() with dynamic input. Use JSON.parse() for data or refactor to avoid dynamic code"));
|
|
16381
|
+
}
|
|
16382
|
+
const newExprs = root.findAll({ rule: { kind: "new_expression" } });
|
|
16383
|
+
for (const newExpr of newExprs) {
|
|
16384
|
+
const children = newExpr.children();
|
|
16385
|
+
const constructorNode = children.find((ch) => ch.kind() === "identifier");
|
|
16386
|
+
if (!constructorNode || constructorNode.text() !== "Function")
|
|
16387
|
+
continue;
|
|
16388
|
+
const args = children.find((ch) => ch.kind() === "arguments");
|
|
16389
|
+
if (!args)
|
|
16390
|
+
continue;
|
|
16391
|
+
const argNodes = args.children().filter((ch) => ch.kind() !== "(" && ch.kind() !== ")" && ch.kind() !== ",");
|
|
16392
|
+
if (argNodes.length === 0)
|
|
16393
|
+
continue;
|
|
16394
|
+
const lastArg = argNodes[argNodes.length - 1];
|
|
16395
|
+
if (lastArg.kind() === "string" || lastArg.kind() === "string_fragment")
|
|
16396
|
+
continue;
|
|
16397
|
+
findings.push(makeFinding("dynamic-code-exec", ctx, newExpr, "new Function() called with dynamic argument — arbitrary code execution risk", "error", "Avoid new Function() with dynamic input. Use static function definitions instead"));
|
|
16398
|
+
}
|
|
16399
|
+
return findings;
|
|
16400
|
+
}
|
|
16401
|
+
function detectPython9(ctx) {
|
|
16402
|
+
const findings = [];
|
|
16403
|
+
const root = ctx.root.root();
|
|
16404
|
+
const calls = root.findAll({ rule: { kind: "call" } });
|
|
16405
|
+
for (const call of calls) {
|
|
16406
|
+
const children = call.children();
|
|
16407
|
+
const callee = children[0];
|
|
16408
|
+
if (!callee || callee.kind() !== "identifier")
|
|
16409
|
+
continue;
|
|
16410
|
+
const funcName = callee.text();
|
|
16411
|
+
if (funcName !== "eval" && funcName !== "exec")
|
|
16412
|
+
continue;
|
|
16413
|
+
const argList = children.find((ch) => ch.kind() === "argument_list");
|
|
16414
|
+
if (!argList)
|
|
16415
|
+
continue;
|
|
16416
|
+
const argNodes = argList.children().filter((ch) => ch.kind() !== "(" && ch.kind() !== ")" && ch.kind() !== "," && ch.kind() !== "keyword_argument");
|
|
16417
|
+
if (argNodes.length === 0)
|
|
16418
|
+
continue;
|
|
16419
|
+
const firstArg = argNodes[0];
|
|
16420
|
+
if (firstArg.kind() === "string") {
|
|
16421
|
+
const text = firstArg.text();
|
|
16422
|
+
if (!text.startsWith('f"') && !text.startsWith("f'")) {
|
|
16423
|
+
continue;
|
|
16424
|
+
}
|
|
16425
|
+
} else if (firstArg.kind() === "identifier" || firstArg.kind() === "binary_operator" || firstArg.kind() === "concatenated_string") {} else {
|
|
16426
|
+
continue;
|
|
16427
|
+
}
|
|
16428
|
+
findings.push(makeFinding("dynamic-code-exec", ctx, call, `${funcName}() called with dynamic argument — arbitrary code execution risk`, "error", `Avoid ${funcName}() with dynamic input. Use ast.literal_eval() for safe expression evaluation`));
|
|
16429
|
+
}
|
|
16430
|
+
return findings;
|
|
16431
|
+
}
|
|
16432
|
+
var dynamicCodeExec = {
|
|
16433
|
+
id: "dynamic-code-exec",
|
|
16434
|
+
meta: {
|
|
16435
|
+
name: "Dynamic Code Execution",
|
|
16436
|
+
description: "Detects eval() and new Function() / exec() with dynamic (non-literal) arguments",
|
|
16437
|
+
severity: "error",
|
|
16438
|
+
category: "security",
|
|
16439
|
+
languages: ["javascript", "typescript", "tsx", "python"],
|
|
16440
|
+
priority: 10
|
|
16441
|
+
},
|
|
16442
|
+
detect(ctx) {
|
|
16443
|
+
if (ctx.file.language === "python")
|
|
16444
|
+
return detectPython9(ctx);
|
|
16445
|
+
return detectJavaScript10(ctx);
|
|
16446
|
+
}
|
|
16447
|
+
};
|
|
16448
|
+
// src/detectors/llm-unpinned-model.ts
|
|
16449
|
+
var DATE_SUFFIX_RE = /[-_]\d{8}$/;
|
|
16450
|
+
var DATE_DASH_SUFFIX_RE = /[-_]\d{4}-\d{2}-\d{2}$/;
|
|
16451
|
+
var UNPINNED_MODEL_PATTERNS = [
|
|
16452
|
+
/^gpt-4o$/,
|
|
16453
|
+
/^gpt-4$/,
|
|
16454
|
+
/^gpt-4o-mini$/,
|
|
16455
|
+
/^gpt-4-turbo$/,
|
|
16456
|
+
/^gpt-3\.5-turbo$/,
|
|
16457
|
+
/^o1$/,
|
|
16458
|
+
/^o1-mini$/,
|
|
16459
|
+
/^o1-preview$/,
|
|
16460
|
+
/^o3$/,
|
|
16461
|
+
/^o3-mini$/,
|
|
16462
|
+
/^o4-mini$/,
|
|
16463
|
+
/^claude-.*-latest$/,
|
|
16464
|
+
/^claude-3-opus$/,
|
|
16465
|
+
/^claude-3-haiku$/,
|
|
16466
|
+
/^claude-3-5-sonnet$/,
|
|
16467
|
+
/^claude-3-5-haiku$/,
|
|
16468
|
+
/^claude-sonnet-4$/,
|
|
16469
|
+
/^claude-opus-4$/,
|
|
16470
|
+
/^gemini-pro$/,
|
|
16471
|
+
/^gemini-1\.5-pro$/,
|
|
16472
|
+
/^gemini-1\.5-flash$/,
|
|
16473
|
+
/^gemini-2\.0-flash$/
|
|
16474
|
+
];
|
|
16475
|
+
function isPinned(model) {
|
|
16476
|
+
return DATE_SUFFIX_RE.test(model) || DATE_DASH_SUFFIX_RE.test(model);
|
|
16477
|
+
}
|
|
16478
|
+
function isUnpinnedModel(value) {
|
|
16479
|
+
if (isPinned(value))
|
|
16480
|
+
return false;
|
|
16481
|
+
return UNPINNED_MODEL_PATTERNS.some((re) => re.test(value));
|
|
16482
|
+
}
|
|
16483
|
+
function detect9(ctx) {
|
|
16484
|
+
const findings = [];
|
|
16485
|
+
const lines = ctx.source.split(`
|
|
16486
|
+
`);
|
|
16487
|
+
const stringLiteralRe = /(['"])([^'"]*)\1/g;
|
|
16488
|
+
for (let i = 0;i < lines.length; i++) {
|
|
16489
|
+
const line = lines[i];
|
|
16490
|
+
let match;
|
|
16491
|
+
stringLiteralRe.lastIndex = 0;
|
|
16492
|
+
while ((match = stringLiteralRe.exec(line)) !== null) {
|
|
16493
|
+
const value = match[2];
|
|
16494
|
+
if (isUnpinnedModel(value)) {
|
|
16495
|
+
findings.push(makeLineFinding("llm-unpinned-model", ctx, i + 1, match.index + 1, `Unpinned model alias "${value}" — model behavior may change without notice`, "warning", `Pin to a specific version with a date suffix, e.g. "${value}-YYYYMMDD"`));
|
|
16496
|
+
}
|
|
16497
|
+
}
|
|
16498
|
+
}
|
|
16499
|
+
return findings;
|
|
16500
|
+
}
|
|
16501
|
+
var llmUnpinnedModel = {
|
|
16502
|
+
id: "llm-unpinned-model",
|
|
16503
|
+
meta: {
|
|
16504
|
+
name: "LLM Unpinned Model",
|
|
16505
|
+
description: "Detects unpinned LLM model aliases that may change behavior without notice",
|
|
16506
|
+
severity: "warning",
|
|
16507
|
+
category: "quality",
|
|
16508
|
+
languages: ["javascript", "typescript", "tsx", "python"],
|
|
16509
|
+
priority: 10
|
|
16510
|
+
},
|
|
16511
|
+
detect: detect9
|
|
16512
|
+
};
|
|
16513
|
+
// src/detectors/llm-no-system-message.ts
|
|
16514
|
+
var SYSTEM_ROLE_RE = /["']system["']/;
|
|
16515
|
+
function detectJavaScript11(ctx) {
|
|
16516
|
+
const findings = [];
|
|
16517
|
+
const root = ctx.root.root();
|
|
16518
|
+
const pairs = root.findAll({ rule: { kind: "pair" } });
|
|
16519
|
+
for (const pair of pairs) {
|
|
16520
|
+
const children = pair.children();
|
|
16521
|
+
const key = children.find((ch) => ch.kind() === "property_identifier" || ch.kind() === "string" || ch.kind() === "shorthand_property_identifier");
|
|
16522
|
+
if (!key)
|
|
16523
|
+
continue;
|
|
16524
|
+
const keyName = key.text().replace(/["']/g, "");
|
|
16525
|
+
if (keyName !== "messages")
|
|
16526
|
+
continue;
|
|
16527
|
+
const value = children.find((ch) => ch.kind() === "array");
|
|
16528
|
+
if (!value)
|
|
16529
|
+
continue;
|
|
16530
|
+
const arrayText = value.text();
|
|
16531
|
+
if (!SYSTEM_ROLE_RE.test(arrayText)) {
|
|
16532
|
+
findings.push(makeFinding("llm-no-system-message", ctx, pair, "messages array has no system message — LLM behavior may be unpredictable", "info", 'Add a system message: { role: "system", content: "You are a helpful assistant." }'));
|
|
16533
|
+
}
|
|
16534
|
+
}
|
|
16535
|
+
return findings;
|
|
16536
|
+
}
|
|
16537
|
+
function detectPython10(ctx) {
|
|
16538
|
+
const findings = [];
|
|
16539
|
+
const root = ctx.root.root();
|
|
16540
|
+
const kwargs = root.findAll({ rule: { kind: "keyword_argument" } });
|
|
16541
|
+
for (const kwarg of kwargs) {
|
|
16542
|
+
const children = kwarg.children();
|
|
16543
|
+
const key = children.find((ch) => ch.kind() === "identifier");
|
|
16544
|
+
if (!key || key.text() !== "messages")
|
|
16545
|
+
continue;
|
|
16546
|
+
const value = children.find((ch) => ch.kind() === "list");
|
|
16547
|
+
if (!value)
|
|
16548
|
+
continue;
|
|
16549
|
+
const listText = value.text();
|
|
16550
|
+
if (!SYSTEM_ROLE_RE.test(listText)) {
|
|
16551
|
+
findings.push(makeFinding("llm-no-system-message", ctx, kwarg, "messages list has no system message — LLM behavior may be unpredictable", "info", 'Add a system message: {"role": "system", "content": "You are a helpful assistant."}'));
|
|
16552
|
+
}
|
|
16553
|
+
}
|
|
16554
|
+
const assignments = root.findAll({ rule: { kind: "assignment" } });
|
|
16555
|
+
for (const assign of assignments) {
|
|
16556
|
+
const children = assign.children();
|
|
16557
|
+
const nameNode = children.find((ch) => ch.kind() === "identifier");
|
|
16558
|
+
if (!nameNode || nameNode.text() !== "messages")
|
|
16559
|
+
continue;
|
|
16560
|
+
const value = children.find((ch) => ch.kind() === "list");
|
|
16561
|
+
if (!value)
|
|
16562
|
+
continue;
|
|
16563
|
+
const listText = value.text();
|
|
16564
|
+
if (!SYSTEM_ROLE_RE.test(listText)) {
|
|
16565
|
+
findings.push(makeFinding("llm-no-system-message", ctx, assign, "messages list has no system message — LLM behavior may be unpredictable", "info", 'Add a system message: {"role": "system", "content": "You are a helpful assistant."}'));
|
|
16566
|
+
}
|
|
16567
|
+
}
|
|
16568
|
+
return findings;
|
|
16569
|
+
}
|
|
16570
|
+
var llmNoSystemMessage = {
|
|
16571
|
+
id: "llm-no-system-message",
|
|
16572
|
+
meta: {
|
|
16573
|
+
name: "LLM No System Message",
|
|
16574
|
+
description: "Detects LLM chat API calls where messages array lacks a system role message",
|
|
16575
|
+
severity: "info",
|
|
16576
|
+
category: "quality",
|
|
16577
|
+
languages: ["javascript", "typescript", "tsx", "python"],
|
|
16578
|
+
priority: 10
|
|
16579
|
+
},
|
|
16580
|
+
detect(ctx) {
|
|
16581
|
+
if (ctx.file.language === "python")
|
|
16582
|
+
return detectPython10(ctx);
|
|
16583
|
+
return detectJavaScript11(ctx);
|
|
16584
|
+
}
|
|
16585
|
+
};
|
|
16586
|
+
// src/detectors/llm-temperature-not-set.ts
|
|
16587
|
+
function detectJavaScript12(ctx) {
|
|
16588
|
+
const findings = [];
|
|
16589
|
+
const root = ctx.root.root();
|
|
16590
|
+
const callExprs = root.findAll({ rule: { kind: "call_expression" } });
|
|
16591
|
+
for (const call of callExprs) {
|
|
16592
|
+
const children = call.children();
|
|
16593
|
+
const callee = children[0];
|
|
16594
|
+
if (!callee || callee.kind() !== "member_expression")
|
|
16595
|
+
continue;
|
|
16596
|
+
const calleeText = callee.text();
|
|
16597
|
+
if (!calleeText.endsWith(".create"))
|
|
16598
|
+
continue;
|
|
16599
|
+
if (!calleeText.includes("completions") && !calleeText.includes("messages") && !calleeText.includes("chat")) {
|
|
16600
|
+
continue;
|
|
16601
|
+
}
|
|
16602
|
+
const args = children.find((ch) => ch.kind() === "arguments");
|
|
16603
|
+
if (!args)
|
|
16604
|
+
continue;
|
|
16605
|
+
const argNodes = args.children().filter((ch) => ch.kind() !== "(" && ch.kind() !== ")" && ch.kind() !== ",");
|
|
16606
|
+
if (argNodes.length === 0)
|
|
16607
|
+
continue;
|
|
16608
|
+
const firstArg = argNodes[0];
|
|
16609
|
+
if (firstArg.kind() !== "object")
|
|
16610
|
+
continue;
|
|
16611
|
+
const pairs = firstArg.findAll({ rule: { kind: "pair" } });
|
|
16612
|
+
let hasTemperature = false;
|
|
16613
|
+
for (const pair of pairs) {
|
|
16614
|
+
const pairChildren = pair.children();
|
|
16615
|
+
const key = pairChildren.find((ch) => ch.kind() === "property_identifier" || ch.kind() === "string" || ch.kind() === "shorthand_property_identifier");
|
|
16616
|
+
if (key && key.text().replace(/["']/g, "") === "temperature") {
|
|
16617
|
+
hasTemperature = true;
|
|
16618
|
+
break;
|
|
16619
|
+
}
|
|
16620
|
+
}
|
|
16621
|
+
if (!hasTemperature) {
|
|
16622
|
+
const shorthandProps = firstArg.findAll({ rule: { kind: "shorthand_property_identifier" } });
|
|
16623
|
+
for (const sp of shorthandProps) {
|
|
16624
|
+
if (sp.text() === "temperature") {
|
|
16625
|
+
hasTemperature = true;
|
|
16626
|
+
break;
|
|
16627
|
+
}
|
|
16628
|
+
}
|
|
16629
|
+
}
|
|
16630
|
+
if (!hasTemperature) {
|
|
16631
|
+
findings.push(makeFinding("llm-temperature-not-set", ctx, call, ".create() called without temperature — model output randomness is not controlled", "info", "Set temperature explicitly: .create({ temperature: 0.7, ... })"));
|
|
16632
|
+
}
|
|
16633
|
+
}
|
|
16634
|
+
return findings;
|
|
16635
|
+
}
|
|
16636
|
+
function detectPython11(ctx) {
|
|
16637
|
+
const findings = [];
|
|
16638
|
+
const root = ctx.root.root();
|
|
16639
|
+
const calls = root.findAll({ rule: { kind: "call" } });
|
|
16640
|
+
for (const call of calls) {
|
|
16641
|
+
const children = call.children();
|
|
16642
|
+
const callee = children[0];
|
|
16643
|
+
if (!callee)
|
|
16644
|
+
continue;
|
|
16645
|
+
const calleeText = callee.text();
|
|
16646
|
+
if (!calleeText.endsWith(".create"))
|
|
16647
|
+
continue;
|
|
16648
|
+
if (!calleeText.includes("completions") && !calleeText.includes("Completion") && !calleeText.includes("messages") && !calleeText.includes("chat")) {
|
|
16649
|
+
continue;
|
|
16650
|
+
}
|
|
16651
|
+
const argList = children.find((ch) => ch.kind() === "argument_list");
|
|
16652
|
+
if (!argList)
|
|
16653
|
+
continue;
|
|
16654
|
+
const kwargs = argList.children().filter((ch) => ch.kind() === "keyword_argument");
|
|
16655
|
+
let hasTemperature = false;
|
|
16656
|
+
for (const kwarg of kwargs) {
|
|
16657
|
+
const kwChildren = kwarg.children();
|
|
16658
|
+
const key = kwChildren.find((ch) => ch.kind() === "identifier");
|
|
16659
|
+
if (key && key.text() === "temperature") {
|
|
16660
|
+
hasTemperature = true;
|
|
16661
|
+
break;
|
|
16662
|
+
}
|
|
16663
|
+
}
|
|
16664
|
+
if (!hasTemperature) {
|
|
16665
|
+
findings.push(makeFinding("llm-temperature-not-set", ctx, call, `${calleeText}() called without temperature — model output randomness is not controlled`, "info", "Set temperature explicitly: .create(temperature=0.7, ...)"));
|
|
16666
|
+
}
|
|
16667
|
+
}
|
|
16668
|
+
return findings;
|
|
16669
|
+
}
|
|
16670
|
+
var llmTemperatureNotSet = {
|
|
16671
|
+
id: "llm-temperature-not-set",
|
|
16672
|
+
meta: {
|
|
16673
|
+
name: "LLM Temperature Not Set",
|
|
16674
|
+
description: "Detects LLM API .create() calls without explicit temperature parameter",
|
|
16675
|
+
severity: "info",
|
|
16676
|
+
category: "quality",
|
|
16677
|
+
languages: ["javascript", "typescript", "tsx", "python"],
|
|
16678
|
+
priority: 10
|
|
16679
|
+
},
|
|
16680
|
+
detect(ctx) {
|
|
16681
|
+
if (ctx.file.language === "python")
|
|
16682
|
+
return detectPython11(ctx);
|
|
16683
|
+
return detectJavaScript12(ctx);
|
|
16684
|
+
}
|
|
16685
|
+
};
|
|
16686
|
+
// src/detectors/hallucinated-package.ts
|
|
16687
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
16688
|
+
import { fileURLToPath } from "node:url";
|
|
16689
|
+
import { dirname as dirname2, join as join2, basename as basename2 } from "node:path";
|
|
16690
|
+
var __dirname2 = dirname2(fileURLToPath(import.meta.url));
|
|
16691
|
+
var knownPackages = JSON.parse(readFileSync3(join2(__dirname2, "../data/known-packages.json"), "utf-8"));
|
|
16692
|
+
var KNOWN_SET = new Set(knownPackages);
|
|
16693
|
+
var KNOWN_SCOPES = new Set([
|
|
16694
|
+
"@types",
|
|
16695
|
+
"@babel",
|
|
16696
|
+
"@rollup",
|
|
16697
|
+
"@eslint",
|
|
16698
|
+
"@typescript-eslint",
|
|
16699
|
+
"@angular",
|
|
16700
|
+
"@vue",
|
|
16701
|
+
"@nuxt",
|
|
16702
|
+
"@svelte",
|
|
16703
|
+
"@sveltejs",
|
|
16704
|
+
"@react-native",
|
|
16705
|
+
"@react-native-community",
|
|
16706
|
+
"@aws-sdk",
|
|
16707
|
+
"@aws-cdk",
|
|
16708
|
+
"@google-cloud",
|
|
16709
|
+
"@azure",
|
|
16710
|
+
"@firebase",
|
|
16711
|
+
"@vercel",
|
|
16712
|
+
"@netlify",
|
|
16713
|
+
"@cloudflare",
|
|
16714
|
+
"@testing-library",
|
|
16715
|
+
"@storybook",
|
|
16716
|
+
"@prisma",
|
|
16717
|
+
"@trpc",
|
|
16718
|
+
"@tanstack",
|
|
16719
|
+
"@emotion",
|
|
16720
|
+
"@mui",
|
|
16721
|
+
"@chakra-ui",
|
|
16722
|
+
"@radix-ui",
|
|
16723
|
+
"@headlessui",
|
|
16724
|
+
"@sentry",
|
|
16725
|
+
"@datadog",
|
|
16726
|
+
"@opentelemetry",
|
|
16727
|
+
"@octokit",
|
|
16728
|
+
"@actions",
|
|
16729
|
+
"@nestjs",
|
|
16730
|
+
"@fastify",
|
|
16731
|
+
"@hapi",
|
|
16732
|
+
"@grpc",
|
|
16733
|
+
"@apollo",
|
|
16734
|
+
"@graphql-codegen",
|
|
16735
|
+
"@graphql-tools",
|
|
16736
|
+
"@remix-run",
|
|
16737
|
+
"@shopify",
|
|
16738
|
+
"@stripe",
|
|
16739
|
+
"@auth0",
|
|
16740
|
+
"@clerk",
|
|
16741
|
+
"@supabase",
|
|
16742
|
+
"@upstash",
|
|
16743
|
+
"@expo",
|
|
16744
|
+
"@react-navigation",
|
|
16745
|
+
"@mantine",
|
|
16746
|
+
"@floating-ui",
|
|
16747
|
+
"@dnd-kit",
|
|
16748
|
+
"@tailwindcss",
|
|
16749
|
+
"@heroicons",
|
|
16750
|
+
"@iconify",
|
|
16751
|
+
"@astrojs",
|
|
16752
|
+
"@sanity",
|
|
16753
|
+
"@contentful",
|
|
16754
|
+
"@mdx-js",
|
|
16755
|
+
"@codemirror",
|
|
16756
|
+
"@tiptap",
|
|
16757
|
+
"@monaco-editor",
|
|
16758
|
+
"@react-aria",
|
|
16759
|
+
"@react-stately",
|
|
16760
|
+
"@react-spring",
|
|
16761
|
+
"@react-three",
|
|
16762
|
+
"@fontsource",
|
|
16763
|
+
"@mapbox",
|
|
16764
|
+
"@nrwl",
|
|
16765
|
+
"@nx",
|
|
16766
|
+
"@swc",
|
|
16767
|
+
"@vitejs",
|
|
16768
|
+
"@esbuild",
|
|
16769
|
+
"@changesets",
|
|
16770
|
+
"@commitlint",
|
|
16771
|
+
"@semantic-release",
|
|
16772
|
+
"@rushstack",
|
|
16773
|
+
"@microsoft",
|
|
16774
|
+
"@sindresorhus",
|
|
16775
|
+
"@antfu",
|
|
16776
|
+
"@jridgewell",
|
|
16777
|
+
"@csstools",
|
|
16778
|
+
"@webassemblyjs",
|
|
16779
|
+
"@npmcli",
|
|
16780
|
+
"@isaacs",
|
|
16781
|
+
"@pkgr",
|
|
16782
|
+
"@nodelib",
|
|
16783
|
+
"@tsconfig",
|
|
16784
|
+
"@jest",
|
|
16785
|
+
"@vitest",
|
|
16786
|
+
"@sinonjs",
|
|
16787
|
+
"@hono",
|
|
16788
|
+
"@elysiajs",
|
|
16789
|
+
"@effect",
|
|
16790
|
+
"@langchain",
|
|
16791
|
+
"@ai-sdk",
|
|
16792
|
+
"@aws-lambda-powertools",
|
|
16793
|
+
"@middy",
|
|
16794
|
+
"@pulumi",
|
|
16795
|
+
"@builder.io",
|
|
16796
|
+
"@solidjs",
|
|
16797
|
+
"@biomejs"
|
|
16798
|
+
]);
|
|
16799
|
+
function getScope(packageName) {
|
|
16800
|
+
if (!packageName.startsWith("@"))
|
|
16801
|
+
return null;
|
|
16802
|
+
const slashIdx = packageName.indexOf("/");
|
|
16803
|
+
if (slashIdx === -1)
|
|
16804
|
+
return null;
|
|
16805
|
+
return packageName.slice(0, slashIdx);
|
|
16806
|
+
}
|
|
16807
|
+
var hallucinatedPackage = {
|
|
16808
|
+
id: "hallucinated-package",
|
|
16809
|
+
meta: {
|
|
16810
|
+
name: "Hallucinated Package",
|
|
16811
|
+
description: "Detects potentially hallucinated (non-existent) packages in package.json",
|
|
16812
|
+
severity: "info",
|
|
16813
|
+
category: "correctness",
|
|
16814
|
+
languages: ["javascript", "typescript"],
|
|
16815
|
+
priority: 10
|
|
16816
|
+
},
|
|
16817
|
+
detect(ctx) {
|
|
16818
|
+
if (basename2(ctx.file.path) !== "package.json") {
|
|
16819
|
+
return [];
|
|
16820
|
+
}
|
|
16821
|
+
let pkg;
|
|
16822
|
+
try {
|
|
16823
|
+
pkg = JSON.parse(ctx.source);
|
|
16824
|
+
} catch {
|
|
16825
|
+
return [];
|
|
16826
|
+
}
|
|
16827
|
+
const findings = [];
|
|
16828
|
+
const lines = ctx.source.split(`
|
|
16829
|
+
`);
|
|
16830
|
+
const deps = pkg.dependencies;
|
|
16831
|
+
const devDeps = pkg.devDependencies;
|
|
16832
|
+
const allDeps = [];
|
|
16833
|
+
if (deps && typeof deps === "object") {
|
|
16834
|
+
allDeps.push(...Object.keys(deps));
|
|
16835
|
+
}
|
|
16836
|
+
if (devDeps && typeof devDeps === "object") {
|
|
16837
|
+
allDeps.push(...Object.keys(devDeps));
|
|
16838
|
+
}
|
|
16839
|
+
for (const dep of allDeps) {
|
|
16840
|
+
if (KNOWN_SET.has(dep))
|
|
16841
|
+
continue;
|
|
16842
|
+
const scope = getScope(dep);
|
|
16843
|
+
if (scope && KNOWN_SCOPES.has(scope))
|
|
16844
|
+
continue;
|
|
16845
|
+
const searchStr = `"${dep}"`;
|
|
16846
|
+
let lineNum = 1;
|
|
16847
|
+
for (let i = 0;i < lines.length; i++) {
|
|
16848
|
+
if (lines[i].includes(searchStr)) {
|
|
16849
|
+
lineNum = i + 1;
|
|
16850
|
+
break;
|
|
16851
|
+
}
|
|
16852
|
+
}
|
|
16853
|
+
const col = (lines[lineNum - 1]?.indexOf(searchStr) ?? 0) + 1;
|
|
16854
|
+
findings.push(makeLineFinding("hallucinated-package", ctx, lineNum, col, `Package '${dep}' is not in the known-packages allowlist — verify it exists on npm`, "info", `Run: npm view ${dep} to check if this package exists`));
|
|
16855
|
+
}
|
|
16856
|
+
return findings;
|
|
16857
|
+
}
|
|
16858
|
+
};
|
|
16859
|
+
// src/detectors/index.ts
|
|
16860
|
+
var builtinDetectors = [
|
|
16861
|
+
emptyErrorHandler,
|
|
16862
|
+
trivialAssertion,
|
|
16863
|
+
insecureDefaults,
|
|
16864
|
+
undeclaredImport,
|
|
16865
|
+
overDefensiveCoding,
|
|
16866
|
+
excessiveCommentRatio,
|
|
16867
|
+
overMocking,
|
|
16868
|
+
nPlusOneQuery,
|
|
16869
|
+
uncheckedDbResult,
|
|
16870
|
+
deadCodePath,
|
|
16871
|
+
doubleTypeAssertion,
|
|
16872
|
+
excessiveAny,
|
|
16873
|
+
debugConsoleInProd,
|
|
16874
|
+
todoInProduction,
|
|
16875
|
+
placeholderInProduction,
|
|
16445
16876
|
tokenInLocalstorage,
|
|
16446
16877
|
godComponent,
|
|
16447
16878
|
godFunction,
|
|
16448
16879
|
sqlInjection,
|
|
16449
16880
|
dangerousInnerHtml,
|
|
16450
16881
|
unboundedQuery,
|
|
16451
|
-
mixedConcerns
|
|
16882
|
+
mixedConcerns,
|
|
16883
|
+
unsafeShellExec,
|
|
16884
|
+
llmCallNoTimeout,
|
|
16885
|
+
dynamicCodeExec,
|
|
16886
|
+
llmUnpinnedModel,
|
|
16887
|
+
llmNoSystemMessage,
|
|
16888
|
+
llmTemperatureNotSet,
|
|
16889
|
+
hallucinatedPackage
|
|
16452
16890
|
];
|
|
16453
16891
|
|
|
16454
16892
|
// src/engine.ts
|
|
16455
|
-
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as
|
|
16893
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync } from "node:fs";
|
|
16456
16894
|
import { createRequire as createRequire2 } from "node:module";
|
|
16457
|
-
import { extname, join as
|
|
16895
|
+
import { extname, join as join3, relative, resolve as resolve2 } from "node:path";
|
|
16458
16896
|
import { parse, Lang as SgLang, registerDynamicLanguage } from "@ast-grep/napi";
|
|
16459
16897
|
var EXTENSION_MAP = {
|
|
16460
16898
|
".js": "javascript",
|
|
@@ -16554,7 +16992,7 @@ function runDetectors(files, detectors, project, config, options = {}) {
|
|
|
16554
16992
|
filesProcessed++;
|
|
16555
16993
|
let source;
|
|
16556
16994
|
try {
|
|
16557
|
-
source =
|
|
16995
|
+
source = readFileSync4(file.absolutePath, "utf-8");
|
|
16558
16996
|
} catch (err) {
|
|
16559
16997
|
errors2.push({
|
|
16560
16998
|
file: file.path,
|
|
@@ -16636,13 +17074,44 @@ function runDetectors(files, detectors, project, config, options = {}) {
|
|
|
16636
17074
|
}
|
|
16637
17075
|
const totalMs = performance.now() - startTime;
|
|
16638
17076
|
const timing = options.verbose ? { totalMs, perDetector } : undefined;
|
|
17077
|
+
const dedupedFindings = dedupFindings(findings, detectors);
|
|
16639
17078
|
return {
|
|
16640
|
-
findings,
|
|
17079
|
+
findings: dedupedFindings,
|
|
16641
17080
|
filesScanned: filesProcessed,
|
|
16642
17081
|
errors: errors2,
|
|
16643
17082
|
timing
|
|
16644
17083
|
};
|
|
16645
17084
|
}
|
|
17085
|
+
function dedupFindings(findings, detectors) {
|
|
17086
|
+
const priorityMap = new Map;
|
|
17087
|
+
for (const d of detectors) {
|
|
17088
|
+
priorityMap.set(d.id, d.meta.priority ?? 0);
|
|
17089
|
+
}
|
|
17090
|
+
const groups = new Map;
|
|
17091
|
+
for (const f of findings) {
|
|
17092
|
+
const key = `${f.file}:${f.line}`;
|
|
17093
|
+
const group = groups.get(key);
|
|
17094
|
+
if (group) {
|
|
17095
|
+
group.push(f);
|
|
17096
|
+
} else {
|
|
17097
|
+
groups.set(key, [f]);
|
|
17098
|
+
}
|
|
17099
|
+
}
|
|
17100
|
+
const deduped = [];
|
|
17101
|
+
for (const group of groups.values()) {
|
|
17102
|
+
if (group.length === 1) {
|
|
17103
|
+
deduped.push(group[0]);
|
|
17104
|
+
} else {
|
|
17105
|
+
group.sort((a, b) => {
|
|
17106
|
+
const pa = priorityMap.get(a.detectorId) ?? 0;
|
|
17107
|
+
const pb = priorityMap.get(b.detectorId) ?? 0;
|
|
17108
|
+
return pb - pa;
|
|
17109
|
+
});
|
|
17110
|
+
deduped.push(group[0]);
|
|
17111
|
+
}
|
|
17112
|
+
}
|
|
17113
|
+
return deduped;
|
|
17114
|
+
}
|
|
16646
17115
|
function runWithTimeout(fn, timeoutMs, detectorId, filePath) {
|
|
16647
17116
|
const start = performance.now();
|
|
16648
17117
|
try {
|
|
@@ -16678,7 +17147,7 @@ function walkDirectory(dir, scanRoot, compiledIgnorePatterns, files) {
|
|
|
16678
17147
|
throw err;
|
|
16679
17148
|
}
|
|
16680
17149
|
for (const entry of entries) {
|
|
16681
|
-
const fullPath =
|
|
17150
|
+
const fullPath = join3(dir, entry.name);
|
|
16682
17151
|
const relativePath = relative(scanRoot, fullPath);
|
|
16683
17152
|
if (matchesIgnorePattern(relativePath, entry.name, compiledIgnorePatterns)) {
|
|
16684
17153
|
continue;
|
|
@@ -16725,9 +17194,9 @@ function matchesIgnorePattern(relativePath, baseName, compiledPatterns) {
|
|
|
16725
17194
|
return false;
|
|
16726
17195
|
}
|
|
16727
17196
|
function loadGitignore(root) {
|
|
16728
|
-
const gitignorePath =
|
|
17197
|
+
const gitignorePath = join3(root, ".gitignore");
|
|
16729
17198
|
try {
|
|
16730
|
-
const content =
|
|
17199
|
+
const content = readFileSync4(gitignorePath, "utf-8");
|
|
16731
17200
|
return content.split(`
|
|
16732
17201
|
`).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => {
|
|
16733
17202
|
if (line.startsWith("/")) {
|
|
@@ -16750,7 +17219,7 @@ function getExtension(filename) {
|
|
|
16750
17219
|
}
|
|
16751
17220
|
function isBinaryFile(filePath) {
|
|
16752
17221
|
try {
|
|
16753
|
-
const fd =
|
|
17222
|
+
const fd = readFileSync4(filePath, { encoding: null, flag: "r" });
|
|
16754
17223
|
const sample = fd.subarray(0, 512);
|
|
16755
17224
|
for (const byte of sample) {
|
|
16756
17225
|
if (byte === 0)
|
|
@@ -16792,6 +17261,19 @@ function isNodeError2(err) {
|
|
|
16792
17261
|
return err instanceof Error && "code" in err;
|
|
16793
17262
|
}
|
|
16794
17263
|
|
|
17264
|
+
// src/formatters/agent.ts
|
|
17265
|
+
function formatAgent(result) {
|
|
17266
|
+
if (result.findings.length === 0) {
|
|
17267
|
+
return "";
|
|
17268
|
+
}
|
|
17269
|
+
return result.findings.map((f) => {
|
|
17270
|
+
const location = `${f.file}:${f.line}:${f.column}`;
|
|
17271
|
+
const suffix = f.suggestion ? `. ${f.suggestion}` : "";
|
|
17272
|
+
return `${location} ${f.severity} ${f.detectorId}: ${f.message}${suffix}`;
|
|
17273
|
+
}).join(`
|
|
17274
|
+
`);
|
|
17275
|
+
}
|
|
17276
|
+
|
|
16795
17277
|
// src/formatters/github.ts
|
|
16796
17278
|
import { appendFileSync } from "node:fs";
|
|
16797
17279
|
function ghLevel(severity) {
|
|
@@ -17272,14 +17754,16 @@ function getFormatter(format, options) {
|
|
|
17272
17754
|
return formatSarif;
|
|
17273
17755
|
case "html":
|
|
17274
17756
|
return formatHtml;
|
|
17757
|
+
case "agent":
|
|
17758
|
+
return formatAgent;
|
|
17275
17759
|
default:
|
|
17276
|
-
throw new Error(`Unknown format '${format}'. Available formats: text, json, github, sarif, html`);
|
|
17760
|
+
throw new Error(`Unknown format '${format}'. Available formats: text, json, github, sarif, html, agent`);
|
|
17277
17761
|
}
|
|
17278
17762
|
}
|
|
17279
17763
|
|
|
17280
17764
|
// src/project.ts
|
|
17281
|
-
import { existsSync as existsSync3, readFileSync as
|
|
17282
|
-
import { dirname as
|
|
17765
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5 } from "node:fs";
|
|
17766
|
+
import { dirname as dirname3, join as join4, resolve as resolve3 } from "node:path";
|
|
17283
17767
|
function loadProjectInfo(scanRoot) {
|
|
17284
17768
|
const info = {
|
|
17285
17769
|
dependencies: new Set,
|
|
@@ -17306,11 +17790,11 @@ function findProjectRoot(startDir) {
|
|
|
17306
17790
|
"pyproject.toml"
|
|
17307
17791
|
];
|
|
17308
17792
|
for (const m of manifests) {
|
|
17309
|
-
if (existsSync3(
|
|
17793
|
+
if (existsSync3(join4(dir, m))) {
|
|
17310
17794
|
return dir;
|
|
17311
17795
|
}
|
|
17312
17796
|
}
|
|
17313
|
-
const parentDir =
|
|
17797
|
+
const parentDir = dirname3(dir);
|
|
17314
17798
|
if (parentDir === dir)
|
|
17315
17799
|
break;
|
|
17316
17800
|
dir = parentDir;
|
|
@@ -17318,12 +17802,12 @@ function findProjectRoot(startDir) {
|
|
|
17318
17802
|
return null;
|
|
17319
17803
|
}
|
|
17320
17804
|
function parsePackageJson(root, info) {
|
|
17321
|
-
const pkgPath =
|
|
17805
|
+
const pkgPath = join4(root, "package.json");
|
|
17322
17806
|
if (!existsSync3(pkgPath))
|
|
17323
17807
|
return;
|
|
17324
17808
|
info.manifests.push(pkgPath);
|
|
17325
17809
|
try {
|
|
17326
|
-
const raw =
|
|
17810
|
+
const raw = readFileSync5(pkgPath, "utf-8");
|
|
17327
17811
|
const pkg = JSON.parse(raw);
|
|
17328
17812
|
if (isRecord(pkg.dependencies)) {
|
|
17329
17813
|
for (const name of Object.keys(pkg.dependencies)) {
|
|
@@ -17342,11 +17826,11 @@ function parsePackageJson(root, info) {
|
|
|
17342
17826
|
} catch {}
|
|
17343
17827
|
}
|
|
17344
17828
|
function parseLockFiles(root, info) {
|
|
17345
|
-
const npmLockPath =
|
|
17829
|
+
const npmLockPath = join4(root, "package-lock.json");
|
|
17346
17830
|
if (existsSync3(npmLockPath)) {
|
|
17347
17831
|
info.manifests.push(npmLockPath);
|
|
17348
17832
|
try {
|
|
17349
|
-
const raw =
|
|
17833
|
+
const raw = readFileSync5(npmLockPath, "utf-8");
|
|
17350
17834
|
const lock = JSON.parse(raw);
|
|
17351
17835
|
if (isRecord(lock.packages)) {
|
|
17352
17836
|
for (const key of Object.keys(lock.packages)) {
|
|
@@ -17366,11 +17850,11 @@ function parseLockFiles(root, info) {
|
|
|
17366
17850
|
}
|
|
17367
17851
|
} catch {}
|
|
17368
17852
|
}
|
|
17369
|
-
const yarnLockPath =
|
|
17853
|
+
const yarnLockPath = join4(root, "yarn.lock");
|
|
17370
17854
|
if (existsSync3(yarnLockPath)) {
|
|
17371
17855
|
info.manifests.push(yarnLockPath);
|
|
17372
17856
|
try {
|
|
17373
|
-
const raw =
|
|
17857
|
+
const raw = readFileSync5(yarnLockPath, "utf-8");
|
|
17374
17858
|
for (const line of raw.split(`
|
|
17375
17859
|
`)) {
|
|
17376
17860
|
const match = line.match(/^"?(@?[^@\s"]+)@/);
|
|
@@ -17383,11 +17867,11 @@ function parseLockFiles(root, info) {
|
|
|
17383
17867
|
}
|
|
17384
17868
|
} catch {}
|
|
17385
17869
|
}
|
|
17386
|
-
const pnpmLockPath =
|
|
17870
|
+
const pnpmLockPath = join4(root, "pnpm-lock.yaml");
|
|
17387
17871
|
if (existsSync3(pnpmLockPath)) {
|
|
17388
17872
|
info.manifests.push(pnpmLockPath);
|
|
17389
17873
|
try {
|
|
17390
|
-
const raw =
|
|
17874
|
+
const raw = readFileSync5(pnpmLockPath, "utf-8");
|
|
17391
17875
|
for (const line of raw.split(`
|
|
17392
17876
|
`)) {
|
|
17393
17877
|
const match = line.match(/^\s+'?\/?(@?[^@\s':]+)@/);
|
|
@@ -17402,12 +17886,12 @@ function parseLockFiles(root, info) {
|
|
|
17402
17886
|
}
|
|
17403
17887
|
}
|
|
17404
17888
|
function parseRequirementsTxt(root, info) {
|
|
17405
|
-
const reqPath =
|
|
17889
|
+
const reqPath = join4(root, "requirements.txt");
|
|
17406
17890
|
if (!existsSync3(reqPath))
|
|
17407
17891
|
return;
|
|
17408
17892
|
info.manifests.push(reqPath);
|
|
17409
17893
|
try {
|
|
17410
|
-
const raw =
|
|
17894
|
+
const raw = readFileSync5(reqPath, "utf-8");
|
|
17411
17895
|
for (const line of raw.split(`
|
|
17412
17896
|
`)) {
|
|
17413
17897
|
const trimmed = line.trim();
|
|
@@ -17423,12 +17907,12 @@ function parseRequirementsTxt(root, info) {
|
|
|
17423
17907
|
} catch {}
|
|
17424
17908
|
}
|
|
17425
17909
|
function parsePyprojectToml(root, info) {
|
|
17426
|
-
const tomlPath =
|
|
17910
|
+
const tomlPath = join4(root, "pyproject.toml");
|
|
17427
17911
|
if (!existsSync3(tomlPath))
|
|
17428
17912
|
return;
|
|
17429
17913
|
info.manifests.push(tomlPath);
|
|
17430
17914
|
try {
|
|
17431
|
-
const raw =
|
|
17915
|
+
const raw = readFileSync5(tomlPath, "utf-8");
|
|
17432
17916
|
const lines = raw.split(`
|
|
17433
17917
|
`);
|
|
17434
17918
|
let inProjectSection = false;
|
|
@@ -17496,7 +17980,7 @@ function isRecord(value) {
|
|
|
17496
17980
|
function getVersion() {
|
|
17497
17981
|
try {
|
|
17498
17982
|
const pkgPath = new URL("../package.json", import.meta.url);
|
|
17499
|
-
const pkg = JSON.parse(
|
|
17983
|
+
const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
|
|
17500
17984
|
return pkg.version;
|
|
17501
17985
|
} catch {
|
|
17502
17986
|
return "0.0.0";
|
|
@@ -17529,7 +18013,7 @@ async function readStdinFiles() {
|
|
|
17529
18013
|
}
|
|
17530
18014
|
function getGitDiffFiles(ref, scanRoot) {
|
|
17531
18015
|
try {
|
|
17532
|
-
const output =
|
|
18016
|
+
const output = execSync2(`git diff --name-only ${ref}`, {
|
|
17533
18017
|
cwd: scanRoot,
|
|
17534
18018
|
encoding: "utf-8",
|
|
17535
18019
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -17590,7 +18074,7 @@ async function scanAction(scanPath, options) {
|
|
|
17590
18074
|
}
|
|
17591
18075
|
function checkAction(filePath, options) {
|
|
17592
18076
|
const absolutePath = resolve4(filePath);
|
|
17593
|
-
if (!
|
|
18077
|
+
if (!existsSync5(absolutePath)) {
|
|
17594
18078
|
process.stderr.write(`Error: File not found: ${filePath}
|
|
17595
18079
|
`);
|
|
17596
18080
|
process.exit(2);
|
|
@@ -17632,8 +18116,12 @@ function main() {
|
|
|
17632
18116
|
setupEpipeHandler();
|
|
17633
18117
|
const program2 = new Command;
|
|
17634
18118
|
program2.name("vibecop").description("AI code quality linter built on ast-grep").version(getVersion());
|
|
17635
|
-
program2.command("scan").description("Scan a directory for code quality issues").argument("[path]", "Directory to scan", ".").option("-f, --format <format>", "Output format (text, json, github, sarif, html)", "text").option("-c, --config <path>", "Path to config file").option("--no-config", "Disable config file loading").option("--max-findings <number>", "Maximum number of findings to report", "50").option("--verbose", "Show timing information", false).option("--diff <ref>", "Scan only files changed vs git ref").option("--stdin-files", "Read file list from stdin", false).option("--group-by <mode>", "Group findings by 'file' or 'rule'", "file").action(scanAction);
|
|
17636
|
-
program2.command("check").description("Check a single file for code quality issues").argument("<file>", "File to check").option("-f, --format <format>", "Output format (text, json, github, sarif, html)", "text").option("--max-findings <number>", "Maximum number of findings to report", "50").option("--verbose", "Show timing information", false).option("--group-by <mode>", "Group findings by 'file' or 'rule'", "file").action(checkAction);
|
|
18119
|
+
program2.command("scan").description("Scan a directory for code quality issues").argument("[path]", "Directory to scan", ".").option("-f, --format <format>", "Output format (text, json, github, sarif, html, agent)", "text").option("-c, --config <path>", "Path to config file").option("--no-config", "Disable config file loading").option("--max-findings <number>", "Maximum number of findings to report", "50").option("--verbose", "Show timing information", false).option("--diff <ref>", "Scan only files changed vs git ref").option("--stdin-files", "Read file list from stdin", false).option("--group-by <mode>", "Group findings by 'file' or 'rule'", "file").action(scanAction);
|
|
18120
|
+
program2.command("check").description("Check a single file for code quality issues").argument("<file>", "File to check").option("-f, --format <format>", "Output format (text, json, github, sarif, html, agent)", "text").option("--max-findings <number>", "Maximum number of findings to report", "50").option("--verbose", "Show timing information", false).option("--group-by <mode>", "Group findings by 'file' or 'rule'", "file").action(checkAction);
|
|
18121
|
+
program2.command("init").description("Set up vibecop integration with AI coding tools").action(async () => {
|
|
18122
|
+
const { runInit: runInit2 } = await Promise.resolve().then(() => (init_init(), exports_init));
|
|
18123
|
+
await runInit2();
|
|
18124
|
+
});
|
|
17637
18125
|
program2.parse();
|
|
17638
18126
|
}
|
|
17639
18127
|
main();
|