querydig 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,474 @@
1
+ import {
2
+ RULES,
3
+ RULE_MAP,
4
+ SUPPORTED_EXTENSIONS,
5
+ analyzeFile
6
+ } from "./chunk-VAYEO4RI.js";
7
+
8
+ // src/config.ts
9
+ import { existsSync, readFileSync } from "fs";
10
+ import { join } from "path";
11
+ var CONFIG_FILES = ["querydig.config.json", ".querydigrc.json", ".querydigrc"];
12
+ var DEFAULT_IGNORE = [
13
+ "**/node_modules/**",
14
+ "**/dist/**",
15
+ "**/build/**",
16
+ "**/out/**",
17
+ "**/.next/**",
18
+ "**/.nuxt/**",
19
+ "**/coverage/**",
20
+ "**/vendor/**",
21
+ "**/.git/**",
22
+ "**/.venv/**",
23
+ "**/venv/**",
24
+ "**/__pycache__/**",
25
+ "**/site-packages/**",
26
+ "**/*.min.js",
27
+ "**/*.bundle.js",
28
+ "**/*.d.ts",
29
+ "**/.querydig/**"
30
+ ];
31
+ var TEST_IGNORE = [
32
+ "**/*.test.*",
33
+ "**/*.spec.*",
34
+ "**/*_test.go",
35
+ "**/test_*.py",
36
+ "**/*_test.py",
37
+ "**/tests/**",
38
+ "**/test/**",
39
+ "**/__tests__/**",
40
+ "**/__mocks__/**",
41
+ "**/spec/**",
42
+ "**/specs/**",
43
+ "**/benchmark/**",
44
+ "**/benchmarks/**",
45
+ "**/bench/**",
46
+ "**/e2e/**",
47
+ "**/fixtures/**",
48
+ "**/migrations/**",
49
+ "**/migration/**",
50
+ "**/*migration*/**",
51
+ "**/alembic/**",
52
+ "**/tests.py",
53
+ "**/conftest.py",
54
+ "**/testdata/**",
55
+ "**/testutil/**",
56
+ "**/unittest/**",
57
+ "**/testing/**",
58
+ "**/seed/**",
59
+ "**/seeds/**",
60
+ "**/seeders/**"
61
+ ];
62
+ function loadConfig(root, explicit) {
63
+ const candidates = explicit ? [explicit] : CONFIG_FILES.map((f) => join(root, f));
64
+ for (const file of candidates) {
65
+ if (!existsSync(file)) continue;
66
+ try {
67
+ return JSON.parse(readFileSync(file, "utf8"));
68
+ } catch (e) {
69
+ throw new Error(`querydig: invalid config ${file}: ${e.message}`);
70
+ }
71
+ }
72
+ return {};
73
+ }
74
+
75
+ // src/report/formats.ts
76
+ function renderJson(s) {
77
+ return JSON.stringify(s, null, 2);
78
+ }
79
+ var SARIF_LEVEL = { critical: "error", high: "error", medium: "warning", low: "note", info: "note" };
80
+ var SECURITY_SEVERITY = { critical: "9.0", high: "7.0", medium: "5.0", low: "3.0", info: "1.0" };
81
+ function renderSarif(s) {
82
+ const used = new Set(s.findings.map((f) => f.ruleId));
83
+ const rules = RULES.filter((r) => used.has(r.id));
84
+ const sarif = {
85
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
86
+ version: "2.1.0",
87
+ runs: [{
88
+ tool: {
89
+ driver: {
90
+ name: "querydig",
91
+ version: s.version,
92
+ informationUri: "https://www.npmjs.com/package/querydig",
93
+ rules: rules.map((r) => ({
94
+ id: r.id,
95
+ name: r.name,
96
+ shortDescription: { text: r.title },
97
+ fullDescription: { text: r.why },
98
+ help: { text: r.fix, markdown: `**Why:** ${r.why}
99
+
100
+ **Fix:** ${r.fix}` },
101
+ defaultConfiguration: { level: SARIF_LEVEL[r.severity] },
102
+ properties: { tags: ["performance", "database", r.db], "security-severity": SECURITY_SEVERITY[r.severity] }
103
+ }))
104
+ }
105
+ },
106
+ results: s.findings.map((f) => ({
107
+ ruleId: f.ruleId,
108
+ ruleIndex: rules.findIndex((r) => r.id === f.ruleId),
109
+ level: SARIF_LEVEL[f.severity],
110
+ message: { text: `${f.message} Fix: ${f.fix}` },
111
+ locations: [{ physicalLocation: { artifactLocation: { uri: f.file }, region: { startLine: f.line, startColumn: f.column } } }],
112
+ partialFingerprints: { querydig: f.fingerprint },
113
+ properties: { severity: f.severity, db: f.db, confidence: f.confidence }
114
+ }))
115
+ }]
116
+ };
117
+ return JSON.stringify(sarif, null, 2);
118
+ }
119
+ var EMOJI = { critical: "\u{1F534}", high: "\u{1F7E0}", medium: "\u{1F7E1}", low: "\u{1F535}", info: "\u26AA" };
120
+ function renderMarkdown(s, limit = 50) {
121
+ const o = [];
122
+ o.push(`## querydig \u2014 query performance report`);
123
+ o.push("");
124
+ o.push(`**Health: ${s.grade} (${s.score}/100)** \xB7 ${s.filesScanned} files \xB7 ${s.findings.length} issue(s)` + (s.suppressedByBaseline ? ` \xB7 ${s.suppressedByBaseline} hidden by baseline` : ""));
125
+ o.push("");
126
+ o.push("| Severity | Count |");
127
+ o.push("|---|---|");
128
+ for (const k of ["critical", "high", "medium", "low"]) o.push(`| ${EMOJI[k]} ${k} | ${s.bySeverity[k]} |`);
129
+ o.push("");
130
+ if (!s.findings.length) {
131
+ o.push("\u2705 No query performance issues found.");
132
+ return o.join("\n");
133
+ }
134
+ o.push("| | Rule | Location | Issue |");
135
+ o.push("|---|---|---|---|");
136
+ for (const f of s.findings.slice(0, limit)) {
137
+ o.push(`| ${EMOJI[f.severity]} | \`${f.ruleId}\` ${RULE_MAP.get(f.ruleId)?.name ?? ""} | \`${f.file}:${f.line}\` | ${f.message.replace(/\|/g, "\\|")} |`);
138
+ }
139
+ if (s.findings.length > limit) o.push(`
140
+ \u2026and ${s.findings.length - limit} more.`);
141
+ o.push("");
142
+ o.push("<details><summary>How to fix</summary>\n");
143
+ for (const id of Object.keys(s.byRule)) {
144
+ const r = RULE_MAP.get(id);
145
+ if (r) o.push(`- **${r.id} ${r.title}** \u2014 ${r.fix}`);
146
+ }
147
+ o.push("\n</details>");
148
+ return o.join("\n");
149
+ }
150
+
151
+ // src/report/html.ts
152
+ function renderHtml(s) {
153
+ const used = new Set(s.findings.map((f) => f.ruleId));
154
+ const data = {
155
+ summary: { root: s.root, files: s.filesScanned, ms: s.durationMs, grade: s.grade, score: s.score, bySeverity: s.bySeverity, baseline: s.suppressedByBaseline, version: s.version, generated: (/* @__PURE__ */ new Date()).toISOString() },
156
+ findings: s.findings,
157
+ rules: RULES.filter((r) => used.has(r.id))
158
+ };
159
+ const json = JSON.stringify(data).replace(/</g, "\\u003c");
160
+ return `<!doctype html>
161
+ <html lang="en">
162
+ <head>
163
+ <meta charset="utf-8">
164
+ <meta name="viewport" content="width=device-width, initial-scale=1">
165
+ <title>querydig report</title>
166
+ <style>
167
+ :root{--bg:#f7f7f5;--panel:#fff;--ink:#1b1b1f;--muted:#6b6b76;--line:#e4e4e0;--code:#f1f1ee;
168
+ --crit:#b3261e;--high:#d9480f;--med:#b7791f;--low:#2b6cb0;--ok:#2f855a;--chip:#efefec}
169
+ @media (prefers-color-scheme: dark){:root{--bg:#121214;--panel:#1b1b1f;--ink:#ececf1;--muted:#9a9aa6;--line:#2c2c33;--code:#232329;
170
+ --crit:#ff6b60;--high:#ff922b;--med:#f2c94c;--low:#74b0ff;--ok:#5fd39a;--chip:#26262c}}
171
+ *{box-sizing:border-box}
172
+ body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}
173
+ main{max-width:1100px;margin:0 auto;padding:32px 16px 64px}
174
+ h1{font-size:22px;margin:0 0 4px;letter-spacing:-.01em}
175
+ .sub{color:var(--muted);margin-bottom:24px;word-break:break-all}
176
+ .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:24px}
177
+ .card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px 16px}
178
+ .card .k{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.06em}
179
+ .card .v{font-size:28px;font-weight:650;font-variant-numeric:tabular-nums}
180
+ .grade .v{font-size:34px}
181
+ .c-critical{color:var(--crit)}.c-high{color:var(--high)}.c-medium{color:var(--med)}.c-low{color:var(--low)}
182
+ .bar{display:flex;height:8px;border-radius:4px;overflow:hidden;background:var(--chip);margin-bottom:24px}
183
+ .bar span{display:block}
184
+ .controls{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px;align-items:center}
185
+ .controls input,.controls select{background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 10px;font:inherit}
186
+ .controls input{flex:1;min-width:200px}
187
+ .chip{border:1px solid var(--line);background:var(--panel);color:var(--ink);border-radius:999px;padding:5px 12px;cursor:pointer;font:inherit}
188
+ .chip[aria-pressed=false]{opacity:.45}
189
+ .f{background:var(--panel);border:1px solid var(--line);border-left:4px solid var(--line);border-radius:10px;padding:14px 16px;margin-bottom:10px}
190
+ .f.critical{border-left-color:var(--crit)}.f.high{border-left-color:var(--high)}.f.medium{border-left-color:var(--med)}.f.low{border-left-color:var(--low)}
191
+ .f h3{margin:0;font-size:15px;display:flex;gap:8px;flex-wrap:wrap;align-items:baseline}
192
+ .sev{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em}
193
+ .rid{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--muted);font-size:12px}
194
+ .loc{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:var(--muted);margin:4px 0 8px;word-break:break-all}
195
+ pre{background:var(--code);border-radius:8px;padding:10px 12px;overflow-x:auto;margin:8px 0;font:12.5px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace}
196
+ .fix{color:var(--ok)}
197
+ .tag{background:var(--chip);border-radius:6px;padding:1px 7px;font-size:11px;color:var(--muted)}
198
+ details{margin-top:6px}summary{cursor:pointer;color:var(--muted)}
199
+ .empty{text-align:center;color:var(--muted);padding:48px 0}
200
+ footer{color:var(--muted);font-size:12px;margin-top:32px}
201
+ </style>
202
+ </head>
203
+ <body>
204
+ <main>
205
+ <h1>Query performance report</h1>
206
+ <div class="sub" id="sub"></div>
207
+ <div class="cards" id="cards"></div>
208
+ <div class="bar" id="bar"></div>
209
+ <div class="controls">
210
+ <input id="q" type="search" placeholder="Filter by file, rule or message\u2026" aria-label="Filter findings">
211
+ <select id="rule" aria-label="Rule"><option value="">All rules</option></select>
212
+ <select id="db" aria-label="Database"><option value="">All databases</option><option>mongodb</option><option>sql</option><option>unknown</option></select>
213
+ <span id="sevchips"></span>
214
+ </div>
215
+ <div id="list"></div>
216
+ <footer id="foot"></footer>
217
+ </main>
218
+ <script>
219
+ const D=${json};
220
+ const SEV=['critical','high','medium','low','info'];
221
+ const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
222
+ const S=D.summary, on=new Set(['critical','high','medium','low','info']);
223
+ document.getElementById('sub').textContent=S.root+' \xB7 '+S.files+' files \xB7 '+(S.ms/1000).toFixed(2)+'s'+(S.baseline?' \xB7 '+S.baseline+' hidden by baseline':'');
224
+ const gc={A:'var(--ok)',B:'var(--ok)',C:'var(--med)',D:'var(--high)',F:'var(--crit)'}[S.grade];
225
+ document.getElementById('cards').innerHTML='<div class="card grade"><div class="k">Health</div><div class="v" style="color:'+gc+'">'+S.grade+' <small style="font-size:14px;color:var(--muted)">'+S.score+'/100</small></div></div>'+
226
+ ['critical','high','medium','low'].map(k=>'<div class="card"><div class="k">'+k+'</div><div class="v c-'+k+'">'+S.bySeverity[k]+'</div></div>').join('');
227
+ const tot=D.findings.length||1;
228
+ document.getElementById('bar').innerHTML=['critical','high','medium','low'].map(k=>'<span style="width:'+(S.bySeverity[k]/tot*100)+'%;background:var(--'+({critical:'crit',high:'high',medium:'med',low:'low'}[k])+')"></span>').join('');
229
+ const rsel=document.getElementById('rule');
230
+ D.rules.forEach(r=>{const o=document.createElement('option');o.value=r.id;o.textContent=r.id+' '+r.title;rsel.appendChild(o)});
231
+ const chips=document.getElementById('sevchips');
232
+ ['critical','high','medium','low'].forEach(k=>{const b=document.createElement('button');b.className='chip c-'+k;b.textContent=k;b.setAttribute('aria-pressed','true');
233
+ b.onclick=()=>{on.has(k)?on.delete(k):on.add(k);b.setAttribute('aria-pressed',on.has(k));render()};chips.appendChild(b)});
234
+ const RM=Object.fromEntries(D.rules.map(r=>[r.id,r]));
235
+ function render(){
236
+ const q=document.getElementById('q').value.toLowerCase(), rule=rsel.value, db=document.getElementById('db').value;
237
+ const fs=D.findings.filter(f=>on.has(f.severity)&&(!rule||f.ruleId===rule)&&(!db||f.db===db)&&(!q||(f.file+' '+f.ruleId+' '+f.message+' '+f.title).toLowerCase().includes(q)));
238
+ const L=document.getElementById('list');
239
+ if(!fs.length){L.innerHTML='<div class="empty">'+(D.findings.length?'No findings match the filters.':'\u2714 No query performance issues found.')+'</div>';return}
240
+ L.innerHTML=fs.slice(0,1000).map(f=>{const r=RM[f.ruleId]||{};return '<div class="f '+f.severity+'"><h3><span class="sev c-'+f.severity+'">'+f.severity+'</span><span class="rid">'+f.ruleId+'</span>'+esc(f.title)+' <span class="tag">'+f.db+'</span><span class="tag">'+f.confidence+' confidence</span></h3>'+
241
+ '<div class="loc">'+esc(f.file)+':'+f.line+':'+f.column+'</div><pre>'+esc(f.snippet)+'</pre><div>'+esc(f.message)+'</div><div class="fix"><b>Fix:</b> '+esc(f.fix)+'</div>'+
242
+ (r.why?'<details><summary>Why is this slow?</summary><p>'+esc(r.why)+'</p>'+(r.example?'<div>Before</div><pre>'+esc(r.example.bad)+'</pre><div>After</div><pre>'+esc(r.example.good)+'</pre>':'')+'</details>':'')+'</div>'}).join('')+(fs.length>1000?'<div class="empty">Showing first 1000 of '+fs.length+'</div>':'');
243
+ }
244
+ ['q','db'].forEach(id=>document.getElementById(id).addEventListener('input',render));rsel.addEventListener('input',render);
245
+ document.getElementById('foot').textContent='Generated by querydig v'+S.version+' on '+new Date(S.generated).toLocaleString();
246
+ render();
247
+ </script>
248
+ </body>
249
+ </html>
250
+ `;
251
+ }
252
+
253
+ // src/report/terminal.ts
254
+ import pc from "picocolors";
255
+ var SEV_COLOR = {
256
+ critical: (s) => pc.bgRed(pc.white(pc.bold(s))),
257
+ high: (s) => pc.red(pc.bold(s)),
258
+ medium: (s) => pc.yellow(s),
259
+ low: (s) => pc.cyan(s),
260
+ info: (s) => pc.dim(s)
261
+ };
262
+ var GRADE_COLOR = { A: pc.green, B: pc.green, C: pc.yellow, D: pc.red, F: pc.red };
263
+ function renderTerminal(s, opts = {}) {
264
+ const out = [];
265
+ const limit = opts.limit ?? 25;
266
+ out.push("");
267
+ out.push(pc.bold(`querydig ${pc.dim("v" + s.version)} \u2014 slow-query scan of ${s.root}`));
268
+ out.push(pc.dim(`${s.filesScanned} files in ${(s.durationMs / 1e3).toFixed(2)}s`));
269
+ out.push("");
270
+ if (!s.findings.length) {
271
+ out.push(pc.green(pc.bold("\u2714 No query performance issues found.")));
272
+ if (s.suppressedByBaseline) out.push(pc.dim(` (${s.suppressedByBaseline} known issue(s) hidden by baseline)`));
273
+ out.push("");
274
+ return out.join("\n");
275
+ }
276
+ const shown = s.findings.slice(0, limit);
277
+ for (const f of shown) out.push(renderFinding(f, opts.verbose));
278
+ if (s.findings.length > shown.length) {
279
+ out.push(pc.dim(`\u2026 ${s.findings.length - shown.length} more. Use --limit 0 to show all, or --format html for the full report.`));
280
+ out.push("");
281
+ }
282
+ const sev = (k) => `${SEV_COLOR[k](` ${k.toUpperCase()} `)} ${s.bySeverity[k]}`;
283
+ out.push(pc.bold("Summary"));
284
+ out.push(` ${sev("critical")} ${sev("high")} ${sev("medium")} ${sev("low")}`);
285
+ const g = (GRADE_COLOR[s.grade] ?? pc.white)(pc.bold(`${s.grade} (${s.score}/100)`));
286
+ out.push(` Query health: ${g}`);
287
+ const top = Object.entries(s.byRule).sort((a, b) => b[1] - a[1]).slice(0, 5);
288
+ out.push(` Top rules: ${top.map(([r, n]) => `${r}\xD7${n}`).join(", ")}`);
289
+ if (s.suppressedByBaseline) out.push(pc.dim(` ${s.suppressedByBaseline} known issue(s) hidden by baseline`));
290
+ if (s.errors.length) out.push(pc.yellow(` ${s.errors.length} file(s) could not be analyzed (run with --verbose)`));
291
+ if (opts.verbose) for (const e of s.errors) out.push(pc.dim(` ${e.file}: ${e.error}`));
292
+ out.push(pc.dim(" Explain a rule: npx querydig explain <RULE>"));
293
+ out.push("");
294
+ return out.join("\n");
295
+ }
296
+ function renderFinding(f, verbose) {
297
+ const lines = [];
298
+ const db = f.db === "unknown" ? "" : pc.dim(` [${f.db}]`);
299
+ lines.push(`${SEV_COLOR[f.severity](` ${f.severity.toUpperCase()} `)} ${pc.bold(f.ruleId)} ${f.title}${db}`);
300
+ lines.push(` ${pc.underline(`${f.file}:${f.line}:${f.column}`)}`);
301
+ for (const l of f.snippet.split("\n").slice(0, 3)) lines.push(pc.dim(" \u2502 ") + l);
302
+ lines.push(` ${f.message}`);
303
+ lines.push(` ${pc.green("fix:")} ${f.fix}`);
304
+ if (verbose) lines.push(pc.dim(` confidence: ${f.confidence} fingerprint: ${f.fingerprint}`));
305
+ lines.push("");
306
+ return lines.join("\n");
307
+ }
308
+
309
+ // src/types.ts
310
+ var SEVERITIES = ["critical", "high", "medium", "low", "info"];
311
+ var SEVERITY_RANK = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };
312
+
313
+ // src/version.ts
314
+ var VERSION = true ? "0.1.1" : "0.0.0-dev";
315
+
316
+ // src/scan.ts
317
+ import { execFileSync } from "child_process";
318
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
319
+ import { availableParallelism } from "os";
320
+ import { join as join2, relative, resolve, sep } from "path";
321
+ import { fileURLToPath } from "url";
322
+ import { Worker } from "worker_threads";
323
+ import fg from "fast-glob";
324
+ import ignore from "ignore";
325
+ async function listFiles(opts) {
326
+ const root = resolve(opts.root);
327
+ const patterns = opts.include?.length ? opts.include : [`**/*{${SUPPORTED_EXTENSIONS.join(",")}}`];
328
+ const ignorePatterns = [...DEFAULT_IGNORE, ...opts.includeTests ? [] : TEST_IGNORE, ...opts.ignore ?? []];
329
+ let files = await fg(patterns, { cwd: root, ignore: ignorePatterns, dot: false, onlyFiles: true, followSymbolicLinks: false });
330
+ const gi = join2(root, ".gitignore");
331
+ if (existsSync2(gi)) {
332
+ const ig = ignore().add(readFileSync2(gi, "utf8"));
333
+ files = files.filter((f) => !ig.ignores(f));
334
+ }
335
+ if (opts.diff) {
336
+ const changed = new Set(gitChanged(root, opts.diff));
337
+ files = files.filter((f) => changed.has(f));
338
+ }
339
+ if (opts.files?.length) {
340
+ const only = new Set(opts.files.map((f) => relative(root, resolve(f)).split(sep).join("/")));
341
+ files = files.filter((f) => only.has(f));
342
+ }
343
+ return files.sort();
344
+ }
345
+ function gitChanged(root, ref) {
346
+ const top = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: root, encoding: "utf8" }).trim();
347
+ const out = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACMR", `${ref}...HEAD`], { cwd: root, encoding: "utf8" });
348
+ const untracked = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], { cwd: root, encoding: "utf8" });
349
+ const staged = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACMR", "HEAD"], { cwd: root, encoding: "utf8" });
350
+ return [...out.split("\n"), ...untracked.split("\n"), ...staged.split("\n")].filter(Boolean).map((f) => relative(root, join2(top, f)).split(sep).join("/"));
351
+ }
352
+ async function runPool(root, files, threads, onProgress) {
353
+ const workerUrl = new URL("./worker.js", import.meta.url);
354
+ const canUseWorkers = workerUrl.protocol === "file:" && existsSync2(fileURLToPath(workerUrl));
355
+ const results = new Array(files.length);
356
+ let done = 0;
357
+ if (!canUseWorkers || threads <= 1 || files.length < 64) {
358
+ let next2 = 0;
359
+ const lanes = Array.from({ length: Math.min(8, files.length) }, async () => {
360
+ while (next2 < files.length) {
361
+ const i = next2++;
362
+ results[i] = await analyzeFile(root, files[i]);
363
+ onProgress?.(++done, files.length);
364
+ }
365
+ });
366
+ await Promise.all(lanes);
367
+ return results;
368
+ }
369
+ const n = Math.min(threads, Math.ceil(files.length / 32));
370
+ let next = 0;
371
+ await Promise.all(
372
+ Array.from({ length: n }, () => new Promise((resolveLane, reject) => {
373
+ const w = new Worker(workerUrl);
374
+ const send = () => {
375
+ if (next >= files.length) {
376
+ void w.terminate();
377
+ resolveLane();
378
+ return;
379
+ }
380
+ const id = next++;
381
+ w.postMessage({ id, root, file: files[id] });
382
+ };
383
+ w.on("message", (m) => {
384
+ results[m.id] = m.result;
385
+ onProgress?.(++done, files.length);
386
+ send();
387
+ });
388
+ w.on("error", reject);
389
+ send();
390
+ }))
391
+ );
392
+ return results;
393
+ }
394
+ function gradeFor(findings, files) {
395
+ const weights = { critical: 10, high: 5, medium: 2, low: 0.5, info: 0 };
396
+ const penalty = findings.reduce((s, f) => s + weights[f.severity], 0);
397
+ const density = penalty / Math.max(1, Math.sqrt(Math.max(files, 1)));
398
+ const score = Math.max(0, Math.round(100 - density * 4));
399
+ const grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 65 ? "C" : score >= 50 ? "D" : "F";
400
+ return { grade, score };
401
+ }
402
+ async function scan(opts) {
403
+ const started = Date.now();
404
+ const root = resolve(opts.root);
405
+ const files = await listFiles({ ...opts, root });
406
+ const threads = opts.threads ?? Math.max(1, availableParallelism() - 1);
407
+ const results = await runPool(root, files, threads, opts.onProgress);
408
+ let findings = [];
409
+ const errors = [];
410
+ for (const r of results) {
411
+ if (!r) continue;
412
+ if (r.error) errors.push({ file: r.file, error: r.error });
413
+ findings.push(...r.findings);
414
+ }
415
+ if (opts.rules) {
416
+ findings = findings.flatMap((f) => {
417
+ const o = opts.rules[f.ruleId];
418
+ if (o === "off") return [];
419
+ return o ? [{ ...f, severity: o }] : [f];
420
+ });
421
+ }
422
+ if (opts.minSeverity) findings = findings.filter((f) => SEVERITY_RANK[f.severity] >= SEVERITY_RANK[opts.minSeverity]);
423
+ let suppressedByBaseline = 0;
424
+ if (opts.baseline) {
425
+ const remaining = /* @__PURE__ */ new Map();
426
+ if (opts.baseline instanceof Map) for (const [k, v] of opts.baseline) remaining.set(k, v);
427
+ else for (const k of opts.baseline) remaining.set(k, (remaining.get(k) ?? 0) + 1);
428
+ findings = findings.filter((f) => {
429
+ const c = remaining.get(f.fingerprint) ?? 0;
430
+ if (c > 0) {
431
+ remaining.set(f.fingerprint, c - 1);
432
+ suppressedByBaseline++;
433
+ return false;
434
+ }
435
+ return true;
436
+ });
437
+ }
438
+ findings.sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity] || a.file.localeCompare(b.file) || a.line - b.line);
439
+ const bySeverity = Object.fromEntries(SEVERITIES.map((s) => [s, 0]));
440
+ const byRule = {};
441
+ for (const f of findings) {
442
+ bySeverity[f.severity]++;
443
+ byRule[f.ruleId] = (byRule[f.ruleId] ?? 0) + 1;
444
+ }
445
+ const { grade, score } = gradeFor(findings, files.length);
446
+ return {
447
+ root,
448
+ filesScanned: files.length,
449
+ durationMs: Date.now() - started,
450
+ findings,
451
+ bySeverity,
452
+ byRule,
453
+ grade,
454
+ score,
455
+ suppressedByBaseline,
456
+ errors,
457
+ version: VERSION
458
+ };
459
+ }
460
+
461
+ export {
462
+ CONFIG_FILES,
463
+ loadConfig,
464
+ renderJson,
465
+ renderSarif,
466
+ renderMarkdown,
467
+ renderHtml,
468
+ renderTerminal,
469
+ SEVERITIES,
470
+ SEVERITY_RANK,
471
+ VERSION,
472
+ listFiles,
473
+ scan
474
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ CONFIG_FILES,
4
+ SEVERITIES,
5
+ SEVERITY_RANK,
6
+ VERSION,
7
+ loadConfig,
8
+ renderHtml,
9
+ renderJson,
10
+ renderMarkdown,
11
+ renderSarif,
12
+ renderTerminal,
13
+ scan
14
+ } from "./chunk-WTZLJ5ZV.js";
15
+ import {
16
+ RULES,
17
+ findRule
18
+ } from "./chunk-VAYEO4RI.js";
19
+
20
+ // src/cli.ts
21
+ import { existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
22
+ import { resolve } from "path";
23
+ import { Command, Option } from "commander";
24
+ import pc from "picocolors";
25
+
26
+ // src/baseline.ts
27
+ import { existsSync, readFileSync, writeFileSync } from "fs";
28
+ function readBaseline(path) {
29
+ if (!existsSync(path)) return void 0;
30
+ const data = JSON.parse(readFileSync(path, "utf8"));
31
+ return new Map(Object.entries(data.fingerprints ?? {}).map(([k, v]) => [k, v.count]));
32
+ }
33
+ function writeBaseline(path, findings) {
34
+ const fingerprints = {};
35
+ for (const f of findings) {
36
+ const e = fingerprints[f.fingerprint] ??= { count: 0, ruleId: f.ruleId, file: f.file };
37
+ e.count++;
38
+ }
39
+ const data = { version: 1, generatedAt: (/* @__PURE__ */ new Date()).toISOString(), fingerprints };
40
+ writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
41
+ return findings.length;
42
+ }
43
+
44
+ // src/cli.ts
45
+ var program = new Command();
46
+ program.name("querydig").description("Find slow and unoptimized database queries (MongoDB, SQL) in JS/TS, Python and Go code.").version(VERSION);
47
+ program.command("scan", { isDefault: true }).description("Scan a project for slow query patterns").argument("[path]", "project root", ".").addOption(new Option("-f, --format <format>", "output format").choices(["terminal", "json", "sarif", "md", "html"]).default("terminal")).option("-o, --output <file>", "write report to a file (default for html: querydig-report.html)").addOption(new Option("--fail-on <severity>", "exit with code 1 if any finding is at or above this severity").choices([...SEVERITIES, "none"])).addOption(new Option("--min-severity <severity>", "hide findings below this severity").choices(SEVERITIES)).option("--baseline <file>", "ignore findings recorded in this baseline file").option("--update-baseline [file]", "write all current findings to a baseline file and exit 0").option("--diff <ref>", "only scan files changed since a git ref (e.g. origin/main)").option("--include-tests", "also scan tests, fixtures, migrations and seeds", false).option("--ignore <globs...>", "extra glob patterns to ignore").option("--rules <list>", "only report these rule ids (comma separated)").option("--disable <list>", "disable these rule ids (comma separated)").option("-c, --config <file>", "path to a config file").option("--limit <n>", "max findings shown in terminal (0 = all)", "25").option("--threads <n>", "worker threads").option("-v, --verbose", "show confidence, fingerprints and parse errors", false).action(async (path, o) => {
48
+ const root = resolve(path);
49
+ if (!existsSync2(root)) {
50
+ console.error(pc.red(`querydig: path not found: ${root}`));
51
+ process.exit(2);
52
+ }
53
+ const cfg = loadConfig(root, o.config);
54
+ const rules = { ...cfg.rules ?? {} };
55
+ for (const id of splitList(o.disable)) rules[id] = "off";
56
+ const only = splitList(o.rules);
57
+ if (only.length) {
58
+ for (const r of RULES) if (!only.includes(r.id)) rules[r.id] = "off";
59
+ }
60
+ const baselinePath = o.baseline ? resolve(o.baseline) : void 0;
61
+ const updating = o.updateBaseline !== void 0;
62
+ const baseline = !updating && baselinePath ? readBaseline(baselinePath) : void 0;
63
+ if (baselinePath && !updating && !baseline) console.error(pc.yellow(`querydig: baseline ${baselinePath} not found \u2014 reporting all findings`));
64
+ const interactive = o.format === "terminal" && process.stderr.isTTY;
65
+ let last = 0;
66
+ const summary = await scan({
67
+ root,
68
+ ignore: [...cfg.ignore ?? [], ...o.ignore ?? []],
69
+ include: cfg.include,
70
+ includeTests: o.includeTests || cfg.includeTests,
71
+ rules,
72
+ minSeverity: o.minSeverity ?? cfg.minSeverity,
73
+ diff: o.diff,
74
+ baseline,
75
+ threads: o.threads ? Number(o.threads) : void 0,
76
+ onProgress: interactive ? (d, t) => {
77
+ const now = Date.now();
78
+ if (now - last > 80 || d === t) {
79
+ last = now;
80
+ process.stderr.write(`\r${pc.dim(`scanning ${d}/${t}`)}${d === t ? "\r\x1B[K" : ""}`);
81
+ }
82
+ } : void 0
83
+ });
84
+ if (updating) {
85
+ const file = resolve(typeof o.updateBaseline === "string" ? o.updateBaseline : baselinePath ?? ".querydig-baseline.json");
86
+ const n = writeBaseline(file, summary.findings);
87
+ console.log(pc.green(`querydig: wrote ${n} finding(s) to ${file}`));
88
+ return;
89
+ }
90
+ let text;
91
+ switch (o.format) {
92
+ case "json":
93
+ text = renderJson(summary);
94
+ break;
95
+ case "sarif":
96
+ text = renderSarif(summary);
97
+ break;
98
+ case "md":
99
+ text = renderMarkdown(summary);
100
+ break;
101
+ case "html":
102
+ text = renderHtml(summary);
103
+ break;
104
+ default:
105
+ text = renderTerminal(summary, { limit: Number(o.limit) || Infinity, verbose: o.verbose });
106
+ }
107
+ const outFile = o.output ?? (o.format === "html" ? "querydig-report.html" : void 0);
108
+ if (outFile) {
109
+ writeFileSync2(resolve(outFile), text);
110
+ if (o.format === "html" || o.format === "terminal") {
111
+ console.log(renderTerminal(summary, { limit: 0 === Number(o.limit) ? Infinity : 5 }).trimEnd());
112
+ }
113
+ console.error(pc.green(`report written to ${resolve(outFile)}`));
114
+ } else {
115
+ process.stdout.write(text.endsWith("\n") ? text : text + "\n");
116
+ }
117
+ const failOn = o.failOn ?? cfg.failOn;
118
+ if (failOn && failOn !== "none") {
119
+ const failing = summary.findings.filter((f) => SEVERITY_RANK[f.severity] >= SEVERITY_RANK[failOn]);
120
+ if (failing.length) {
121
+ console.error(pc.red(`querydig: ${failing.length} finding(s) at or above "${failOn}" \u2014 failing.`));
122
+ process.exitCode = 1;
123
+ }
124
+ }
125
+ });
126
+ program.command("rules").description("List all rules").action(() => {
127
+ for (const r of RULES) console.log(`${pc.bold(r.id)} ${r.severity.padEnd(8)} ${pc.dim(r.db.padEnd(7))} ${r.title}`);
128
+ });
129
+ program.command("explain").description("Explain a rule and how to fix it").argument("<rule>", "rule id or name, e.g. QD001 or n-plus-one").action((id) => {
130
+ const r = findRule(id);
131
+ if (!r) {
132
+ console.error(pc.red(`Unknown rule ${id}. Run \`querydig rules\`.`));
133
+ process.exit(2);
134
+ }
135
+ console.log(`
136
+ ${pc.bold(`${r.id} \xB7 ${r.title}`)} ${pc.dim(`(${r.name}, default ${r.severity}, ${r.db})`)}
137
+ `);
138
+ console.log(`${pc.bold("Why it is slow")}
139
+ ${r.why}
140
+ `);
141
+ console.log(`${pc.bold("How to fix")}
142
+ ${r.fix}
143
+ `);
144
+ if (r.example) {
145
+ console.log(pc.red("Before"));
146
+ console.log(r.example.bad.replace(/^/gm, " "));
147
+ console.log(pc.green("\nAfter"));
148
+ console.log(r.example.good.replace(/^/gm, " ") + "\n");
149
+ }
150
+ });
151
+ program.command("init").description("Create a querydig.config.json").action(() => {
152
+ const file = resolve(CONFIG_FILES[0]);
153
+ if (existsSync2(file)) {
154
+ console.error(pc.yellow(`${file} already exists`));
155
+ return;
156
+ }
157
+ writeFileSync2(file, JSON.stringify({ ignore: [], includeTests: false, failOn: "high", rules: { MQ010: "off", MQ011: "off" } }, null, 2) + "\n");
158
+ console.log(pc.green(`created ${file}`));
159
+ });
160
+ function splitList(v) {
161
+ return v ? v.split(",").map((s) => s.trim().toUpperCase()).filter(Boolean) : [];
162
+ }
163
+ program.parseAsync().catch((e) => {
164
+ console.error(pc.red(`querydig: ${e.message}`));
165
+ process.exit(2);
166
+ });
@@ -0,0 +1,14 @@
1
+ import type { Severity } from './types.js';
2
+ export interface QuerydigConfig {
3
+ include?: string[];
4
+ ignore?: string[];
5
+ includeTests?: boolean;
6
+ /** rule id -> 'off' | severity override */
7
+ rules?: Record<string, 'off' | Severity>;
8
+ failOn?: Severity;
9
+ minSeverity?: Severity;
10
+ }
11
+ export declare const CONFIG_FILES: string[];
12
+ export declare const DEFAULT_IGNORE: string[];
13
+ export declare const TEST_IGNORE: string[];
14
+ export declare function loadConfig(root: string, explicit?: string): QuerydigConfig;
@@ -0,0 +1,2 @@
1
+ import type { FileResult } from './types.js';
2
+ export declare function analyzeFile(root: string, rel: string): Promise<FileResult>;
@@ -0,0 +1,7 @@
1
+ export { scan, listFiles } from './scan.js';
2
+ export { analyzeSource } from './analyzer.js';
3
+ export { RULES, findRule } from './rules.js';
4
+ export { renderJson, renderSarif, renderMarkdown } from './report/formats.js';
5
+ export { renderHtml } from './report/html.js';
6
+ export { renderTerminal } from './report/terminal.js';
7
+ export type * from './types.js';