payload-doctor 0.5.4

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/dist/cli.js ADDED
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const path = __importStar(require("path"));
38
+ const fs = __importStar(require("fs"));
39
+ const ts_morph_1 = require("ts-morph");
40
+ const checks_1 = require("./checks");
41
+ const project_1 = require("./checks/project");
42
+ const score_1 = require("./score");
43
+ const report_1 = require("./report");
44
+ const suppress_1 = require("./suppress");
45
+ const version_1 = require("./version");
46
+ function parseArgs(argv) {
47
+ const o = {
48
+ target: '.',
49
+ verbose: false,
50
+ json: false,
51
+ list: false,
52
+ color: process.stdout.isTTY ?? false,
53
+ exitCode: true,
54
+ minScore: null,
55
+ help: false,
56
+ version: false,
57
+ summary: false,
58
+ fix: false,
59
+ };
60
+ const positional = [];
61
+ for (let i = 0; i < argv.length; i++) {
62
+ const a = argv[i];
63
+ switch (a) {
64
+ case '--version':
65
+ case '-V':
66
+ o.version = true;
67
+ break;
68
+ case '--verbose':
69
+ case '-v':
70
+ o.verbose = true;
71
+ break;
72
+ case '--json':
73
+ o.json = true;
74
+ break;
75
+ case '--list':
76
+ o.list = true;
77
+ break;
78
+ case '--summary':
79
+ o.summary = true;
80
+ break;
81
+ case '--fix':
82
+ o.fix = true;
83
+ break;
84
+ case '--no-color':
85
+ o.color = false;
86
+ break;
87
+ case '--color':
88
+ o.color = true;
89
+ break;
90
+ case '--no-exit-code':
91
+ o.exitCode = false;
92
+ break;
93
+ case '--min-score': {
94
+ const raw = argv[++i];
95
+ const n = Number(raw);
96
+ if (raw === undefined || raw === '' || !Number.isFinite(n) || n < 0 || n > 100) {
97
+ console.error(`Invalid --min-score value: ${raw ?? '(missing)'}. Expected a number between 0 and 100.`);
98
+ process.exit(2);
99
+ }
100
+ o.minScore = n;
101
+ break;
102
+ }
103
+ case '--help':
104
+ case '-h':
105
+ o.help = true;
106
+ break;
107
+ default:
108
+ if (!a.startsWith('-'))
109
+ positional.push(a);
110
+ }
111
+ }
112
+ if (positional[0])
113
+ o.target = positional[0];
114
+ return o;
115
+ }
116
+ const HELP = `payload-doctor v${version_1.VERSION} — static security & correctness auditor for Payload CMS
117
+
118
+ Usage:
119
+ npx -y payload-doctor@latest [path] [options]
120
+
121
+ Options:
122
+ -v, --verbose show fix hints under each finding
123
+ --json machine-readable JSON output
124
+ --list list all checks and exit
125
+ --summary show only the per-rule rollup, not every finding
126
+ --fix print a suggested fix per rule (does not modify files)
127
+ --min-score N exit non-zero if the score is below N
128
+ --no-exit-code always exit 0 (useful in pre-commit while adopting)
129
+ --no-color disable ANSI colors
130
+ -V, --version print version and exit
131
+ -h, --help show this help
132
+
133
+ Suppress intentional cases with a comment on or above the line:
134
+ // payload-doctor-disable-next-line local-api-override-access
135
+ // payload-doctor-disable side-effect-in-get (whole file; omit rule for all)
136
+
137
+ Workflow: run it, fix errors first, re-run to watch the score climb.
138
+ Score = max(0, 100 − 10·errors − 3·warnings); info findings don't affect it.
139
+ Score bands: 75-100 great · 50-74 needs work · 0-49 critical.
140
+ (10+ errors floor the score at 0 by design — watch the error/warning counts drop to gauge progress.)`;
141
+ function listChecks() {
142
+ for (const ch of checks_1.ALL_CHECKS) {
143
+ console.log(`${ch.category.padEnd(12)} payload-doctor/${ch.id}\n ${ch.describe}`);
144
+ }
145
+ }
146
+ function main() {
147
+ const opts = parseArgs(process.argv.slice(2));
148
+ (0, report_1.setColor)(opts.color);
149
+ if (opts.version) {
150
+ console.log(`payload-doctor v${version_1.VERSION}`);
151
+ process.exit(0);
152
+ }
153
+ if (opts.help) {
154
+ console.log(HELP);
155
+ process.exit(0);
156
+ }
157
+ if (opts.list) {
158
+ listChecks();
159
+ process.exit(0);
160
+ }
161
+ const root = path.resolve(opts.target);
162
+ if (!fs.existsSync(root)) {
163
+ console.error(`Path not found: ${opts.target}\n` +
164
+ 'Point payload-doctor at an existing Payload project. From the project root, run it with ".".');
165
+ process.exit(2);
166
+ }
167
+ let project;
168
+ try {
169
+ project = new ts_morph_1.Project({
170
+ skipAddingFilesFromTsConfig: true,
171
+ compilerOptions: { allowJs: true, checkJs: false },
172
+ });
173
+ if (fs.statSync(root).isFile()) {
174
+ project.addSourceFileAtPath(root);
175
+ }
176
+ else {
177
+ project.addSourceFilesAtPaths([
178
+ `${root}/**/*.ts`,
179
+ `${root}/**/*.tsx`,
180
+ `${root}/**/*.js`,
181
+ `${root}/**/*.jsx`,
182
+ `!${root}/**/node_modules/**`,
183
+ `!${root}/**/dist/**`,
184
+ `!${root}/**/.next/**`,
185
+ `!${root}/**/build/**`,
186
+ `!${root}/**/*.d.ts`,
187
+ ]);
188
+ }
189
+ }
190
+ catch (err) {
191
+ console.error(`Failed to read the project at ${root}:\n ${err instanceof Error ? err.message : String(err)}\n` +
192
+ 'Make sure the path is a readable Payload project directory (or a single .ts/.tsx file).');
193
+ process.exit(2);
194
+ }
195
+ const files = project.getSourceFiles();
196
+ if (files.length === 0) {
197
+ console.error(`No .ts/.tsx/.js/.jsx files found under ${root}.\n` +
198
+ 'Point payload-doctor at a Payload project root (where your collections/ and payload config live).');
199
+ process.exit(2);
200
+ }
201
+ const ctx = {
202
+ root,
203
+ rel: (abs) => path.relative(root, abs) || path.basename(abs),
204
+ };
205
+ const rawFindings = [];
206
+ for (const file of files) {
207
+ for (const check of checks_1.ALL_CHECKS) {
208
+ try {
209
+ rawFindings.push(...check.run(file, ctx));
210
+ }
211
+ catch {
212
+ // a single check throwing must not abort the whole scan
213
+ }
214
+ }
215
+ }
216
+ // cross-file checks (e.g. duplicate slugs across collections)
217
+ try {
218
+ rawFindings.push(...(0, project_1.projectChecks)(files, ctx));
219
+ }
220
+ catch {
221
+ // never let an aggregate check abort the scan
222
+ }
223
+ // inline suppression (// payload-doctor-disable...)
224
+ const fileTexts = new Map();
225
+ for (const file of files)
226
+ fileTexts.set(ctx.rel(file.getFilePath()), file.getFullText());
227
+ const { kept: findings, suppressed } = (0, suppress_1.applySuppressions)(rawFindings, fileTexts);
228
+ const score = (0, score_1.scoreFindings)(findings);
229
+ if (opts.json) {
230
+ console.log((0, report_1.renderJson)(findings, score, { root, filesScanned: files.length, suppressed }));
231
+ }
232
+ else {
233
+ console.log(`payload-doctor v${version_1.VERSION}\n`);
234
+ if (files.length === 0) {
235
+ console.log('No source files found. Point payload-doctor at your Payload project root.');
236
+ }
237
+ else if (findings.length === 0) {
238
+ const tail = suppressed > 0 ? ` (${suppressed} suppressed)` : '';
239
+ console.log(`Scanned ${files.length} files. No issues found. Score: 100/100${tail}`);
240
+ }
241
+ else {
242
+ console.log((0, report_1.renderText)(findings, score, { verbose: opts.verbose, suppressed, summaryOnly: opts.summary }));
243
+ }
244
+ if (opts.fix && findings.length > 0) {
245
+ const fixes = (0, report_1.renderFixes)(findings);
246
+ if (fixes)
247
+ console.log('\n' + fixes);
248
+ }
249
+ }
250
+ if (!opts.exitCode)
251
+ process.exit(0);
252
+ if (opts.minScore !== null && score.score < opts.minScore)
253
+ process.exit(1);
254
+ process.exit(score.counts.error > 0 ? 1 : 0);
255
+ }
256
+ main();
package/dist/fix.js ADDED
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fixFor = fixFor;
4
+ // Generic, copy-pasteable fix suggestions per rule. These are starting points,
5
+ // not file-specific rewrites — payload-doctor never modifies your files.
6
+ const FIXES = {
7
+ 'local-api-override-access': "await payload.find({ collection, overrideAccess: false, user: req.user })",
8
+ 'override-access-true-with-user': "// drop overrideAccess and pass the real user so access control runs",
9
+ 'collection-missing-access': "access: {\n read: () => true,\n create: ({ req }) => Boolean(req.user),\n update: ({ req }) => Boolean(req.user),\n delete: ({ req }) => req.user?.role === 'admin',\n}",
10
+ 'open-access-function': "read: ({ req }) => Boolean(req.user) // not () => true",
11
+ 'missing-owner-enforcement': "// beforeChange: data.owner = req.user.id\n// access.read: ({ req }) => ({ owner: { equals: req.user?.id } })",
12
+ 'user-writable-privileged-field': "access: { update: ({ req }) => req.user?.role === 'admin' }",
13
+ 'cron-not-fail-closed': "if (req.headers.get('authorization') !== `Bearer ${process.env.CRON_SECRET}`)\n return new Response('Unauthorized', { status: 401 })",
14
+ 'side-effect-in-get': "export async function POST() { /* move the write here, or add an idempotency guard */ }",
15
+ 'leaks-error-message': "console.error(err) // log server-side\nreturn Response.json({ error: 'Internal error' }, { status: 500 })",
16
+ 'hardcoded-secret': "secret: process.env.PAYLOAD_SECRET! // and throw at startup if missing",
17
+ 'wide-open-cors': "cors: ['https://yourdomain.com']",
18
+ 'token-field-readable': "access: { read: () => false }",
19
+ 'unsafe-richtext-render': "dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }}",
20
+ 'collection-missing-slug': "slug: 'posts',",
21
+ 'hook-missing-return': "return data // afterRead: return doc",
22
+ 'admin-hidden-not-access': "access: { read: () => false }, // admin.hidden alone leaves it in the API",
23
+ 'duplicate-slug': "// rename one collection to a unique slug",
24
+ 'dependency-version-mismatch': "// pin all payload + @payloadcms/* deps to the same exact version",
25
+ 'relationship-missing-relationTo': "{ name: 'author', type: 'relationship', relationTo: 'users' }",
26
+ 'select-without-options': "{ name: 'status', type: 'select', options: [{ label: 'Draft', value: 'draft' }] }",
27
+ 'duplicate-field-name': "// rename one of the two fields so names are unique within the level",
28
+ 'auth-weak-config': "auth: { maxLoginAttempts: 5, lockTime: 600000, tokenExpiration: 7200 }",
29
+ 'sensitive-data-logged': "// remove the log, or redact: console.log({ id: doc.id }) — never the secret itself",
30
+ 'reserved-field-name': "// rename the field (Payload owns id/_id/createdAt/updatedAt/_status); no '.' or '$' in Mongo keys",
31
+ 'excessive-max-depth': "maxDepth: 2, // request deeper only where you actually need it",
32
+ 'missing-index-on-filter-field': "{ name: 'email', type: 'email', index: true }",
33
+ 'hook-n-plus-one': "const docs = await payload.find({ collection, where: { id: { in: ids } } })\n// then map over docs.docs instead of querying per item",
34
+ 'circular-relationship': "// fine if intentional — just keep query depth small (depth: 1) to avoid populating the loop",
35
+ };
36
+ function fixFor(ruleId) {
37
+ return FIXES[ruleId];
38
+ }
package/dist/report.js ADDED
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setColor = setColor;
4
+ exports.rollupData = rollupData;
5
+ exports.renderText = renderText;
6
+ exports.renderFixes = renderFixes;
7
+ exports.renderJson = renderJson;
8
+ const version_1 = require("./version");
9
+ const fix_1 = require("./fix");
10
+ const COLOR = {
11
+ reset: '\x1b[0m',
12
+ dim: '\x1b[2m',
13
+ bold: '\x1b[1m',
14
+ red: '\x1b[31m',
15
+ yellow: '\x1b[33m',
16
+ blue: '\x1b[34m',
17
+ green: '\x1b[32m',
18
+ };
19
+ let useColor = true;
20
+ function setColor(on) {
21
+ useColor = on;
22
+ }
23
+ function c(code, s) {
24
+ return useColor ? `${code}${s}${COLOR.reset}` : s;
25
+ }
26
+ const SEV_MARK = {
27
+ error: '✗',
28
+ warning: '!',
29
+ info: 'i',
30
+ };
31
+ function sevColor(sev) {
32
+ return sev === 'error' ? COLOR.red : sev === 'warning' ? COLOR.yellow : COLOR.blue;
33
+ }
34
+ const SEV_RANK = { error: 0, warning: 1, info: 2 };
35
+ /** Aggregate findings per rule, sorted by severity then count. Shared by text + JSON. */
36
+ function rollupData(findings) {
37
+ const byRule = new Map();
38
+ for (const f of findings) {
39
+ const a = byRule.get(f.ruleId) ?? { count: 0, files: new Set(), sev: f.severity, e: 0, w: 0, i: 0 };
40
+ a.count++;
41
+ a.files.add(f.file);
42
+ if (f.severity === 'error')
43
+ a.e++;
44
+ else if (f.severity === 'warning')
45
+ a.w++;
46
+ else
47
+ a.i++;
48
+ if (SEV_RANK[f.severity] < SEV_RANK[a.sev])
49
+ a.sev = f.severity;
50
+ byRule.set(f.ruleId, a);
51
+ }
52
+ return [...byRule.entries()]
53
+ .map(([ruleId, a]) => ({
54
+ ruleId,
55
+ severity: a.sev,
56
+ count: a.count,
57
+ files: a.files.size,
58
+ errors: a.e,
59
+ warnings: a.w,
60
+ infos: a.i,
61
+ }))
62
+ .sort((x, y) => SEV_RANK[x.severity] - SEV_RANK[y.severity] || y.count - x.count);
63
+ }
64
+ /** Compact per-rule summary lines with e/w/i breakdown. */
65
+ function ruleRollup(findings) {
66
+ if (findings.length === 0)
67
+ return [];
68
+ const lines = [c(COLOR.bold, 'Summary by rule:')];
69
+ for (const r of rollupData(findings)) {
70
+ const mark = c(sevColor(r.severity), SEV_MARK[r.severity]);
71
+ const count = String(r.count).padStart(3);
72
+ const breakdown = c(COLOR.dim, `(${r.errors}e/${r.warnings}w/${r.infos}i)`);
73
+ lines.push(` ${mark} ${count} ${r.ruleId} ${c(COLOR.dim, `in ${r.files} file(s)`)} ${breakdown}`);
74
+ }
75
+ return lines;
76
+ }
77
+ function renderText(findings, score, opts) {
78
+ const lines = [];
79
+ // Summary by rule first, so the overview is visible without scrolling.
80
+ for (const l of ruleRollup(findings))
81
+ lines.push(l);
82
+ if (findings.length > 0)
83
+ lines.push('');
84
+ if (!opts.summaryOnly) {
85
+ // group by file
86
+ const byFile = new Map();
87
+ for (const f of findings) {
88
+ const arr = byFile.get(f.file) ?? [];
89
+ arr.push(f);
90
+ byFile.set(f.file, arr);
91
+ }
92
+ for (const [file, fs] of [...byFile.entries()].sort()) {
93
+ lines.push(c(COLOR.bold, file));
94
+ fs.sort((a, b) => a.line - b.line);
95
+ for (const f of fs) {
96
+ const loc = c(COLOR.dim, `${f.line}:${f.column}`);
97
+ const mark = c(sevColor(f.severity), `${SEV_MARK[f.severity]} ${f.severity}`);
98
+ lines.push(` ${loc} ${mark} ${f.message} ${c(COLOR.dim, f.ruleId)}`);
99
+ if (opts.verbose && f.hint) {
100
+ lines.push(` ${c(COLOR.dim, '↳ ' + f.hint)}`);
101
+ }
102
+ }
103
+ lines.push('');
104
+ }
105
+ } // end !summaryOnly
106
+ const bandLabel = score.band === 'great'
107
+ ? c(COLOR.green, 'Great')
108
+ : score.band === 'needs-work'
109
+ ? c(COLOR.yellow, 'Needs work')
110
+ : c(COLOR.red, 'Critical');
111
+ lines.push(c(COLOR.bold, `Score: ${score.score}/100`) +
112
+ ` ${bandLabel} ` +
113
+ c(COLOR.dim, `(${score.counts.error} errors, ${score.counts.warning} warnings, ${score.counts.info} info)`));
114
+ if (opts.suppressed && opts.suppressed > 0) {
115
+ lines.push(c(COLOR.dim, `${opts.suppressed} finding(s) suppressed by inline comments.`));
116
+ }
117
+ return lines.join('\n');
118
+ }
119
+ /** A "Suggested fixes" section: one snippet per distinct rule present. */
120
+ function renderFixes(findings) {
121
+ const order = [];
122
+ for (const f of findings)
123
+ if (!order.includes(f.ruleId))
124
+ order.push(f.ruleId);
125
+ const lines = [c(COLOR.bold, 'Suggested fixes (starting points — files are never modified):')];
126
+ let any = false;
127
+ for (const r of order) {
128
+ const group = findings.filter((f) => f.ruleId === r);
129
+ // Prefer a context-specific fix supplied by the check; else the generic template.
130
+ const fx = group.find((f) => f.fix)?.fix ?? (0, fix_1.fixFor)(r);
131
+ if (!fx)
132
+ continue;
133
+ any = true;
134
+ const first = group[0];
135
+ let where;
136
+ if (group.length === 1) {
137
+ where = `${first.file}:${first.line}`;
138
+ }
139
+ else {
140
+ const byFile = new Map();
141
+ for (const g of group)
142
+ byFile.set(g.file, (byFile.get(g.file) ?? 0) + 1);
143
+ const top = [...byFile.entries()].sort((a, b) => b[1] - a[1]);
144
+ const shown = top.slice(0, 4).map(([f, n]) => `${f} (${n})`);
145
+ const more = top.length > 4 ? `, +${top.length - 4} more` : '';
146
+ where = `${group.length}× in ${top.length} file${top.length === 1 ? '' : 's'} — ${shown.join(', ')}${more}`;
147
+ }
148
+ lines.push('');
149
+ lines.push(c(COLOR.dim, `# ${r} (${where})`));
150
+ for (const l of fx.split('\n'))
151
+ lines.push(' ' + l);
152
+ }
153
+ if (!any)
154
+ return '';
155
+ return lines.join('\n');
156
+ }
157
+ function renderJson(findings, score, meta) {
158
+ return JSON.stringify({
159
+ tool: 'payload-doctor',
160
+ toolVersion: version_1.VERSION,
161
+ schema: 2,
162
+ root: meta.root,
163
+ filesScanned: meta.filesScanned,
164
+ suppressed: meta.suppressed ?? 0,
165
+ score: score.score,
166
+ band: score.band,
167
+ counts: score.counts,
168
+ summary: rollupData(findings),
169
+ findings,
170
+ }, null, 2);
171
+ }
package/dist/score.js ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scoreFindings = scoreFindings;
4
+ const WEIGHT = {
5
+ error: 10,
6
+ warning: 3,
7
+ info: 0,
8
+ };
9
+ function scoreFindings(findings) {
10
+ const counts = { error: 0, warning: 0, info: 0 };
11
+ let penalty = 0;
12
+ for (const f of findings) {
13
+ counts[f.severity]++;
14
+ penalty += WEIGHT[f.severity];
15
+ }
16
+ const score = Math.max(0, 100 - penalty);
17
+ const band = score >= 75 ? 'great' : score >= 50 ? 'needs-work' : 'critical';
18
+ return { score, band, counts };
19
+ }
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applySuppressions = applySuppressions;
4
+ const DIRECTIVE = /payload-doctor-(disable-next-line|disable-line|disable)\b([^\n\r]*)/;
5
+ function parseRules(rest) {
6
+ const tokens = rest
7
+ .split(/[\s,]+/)
8
+ .map((t) => t.trim().replace(/^payload-doctor\//, ''))
9
+ .filter((t) => t.length > 0 && t !== '--');
10
+ if (tokens.length === 0 || tokens.includes('all'))
11
+ return 'all';
12
+ return new Set(tokens);
13
+ }
14
+ function mergeScope(existing, next) {
15
+ if (!existing)
16
+ return next;
17
+ if (existing === 'all' || next === 'all')
18
+ return 'all';
19
+ for (const r of next)
20
+ existing.add(r);
21
+ return existing;
22
+ }
23
+ function parseFile(text) {
24
+ const supp = { fileScope: null, lines: new Map() };
25
+ const lines = text.split(/\r?\n/);
26
+ lines.forEach((lineText, idx) => {
27
+ const m = lineText.match(DIRECTIVE);
28
+ if (!m)
29
+ return;
30
+ const kind = m[1];
31
+ const rules = parseRules(m[2] ?? '');
32
+ const lineNo = idx + 1;
33
+ if (kind === 'disable') {
34
+ supp.fileScope = mergeScope(supp.fileScope ?? undefined, rules);
35
+ }
36
+ else if (kind === 'disable-line') {
37
+ supp.lines.set(lineNo, mergeScope(supp.lines.get(lineNo), rules));
38
+ }
39
+ else {
40
+ supp.lines.set(lineNo + 1, mergeScope(supp.lines.get(lineNo + 1), rules));
41
+ }
42
+ });
43
+ return supp;
44
+ }
45
+ function scopeSuppresses(scope, ruleId) {
46
+ if (!scope)
47
+ return false;
48
+ if (scope === 'all')
49
+ return true;
50
+ return scope.has(ruleId);
51
+ }
52
+ function applySuppressions(findings, fileTexts) {
53
+ const cache = new Map();
54
+ const getSupp = (file) => {
55
+ if (cache.has(file))
56
+ return cache.get(file);
57
+ const text = fileTexts.get(file);
58
+ if (text === undefined)
59
+ return undefined;
60
+ const parsed = parseFile(text);
61
+ cache.set(file, parsed);
62
+ return parsed;
63
+ };
64
+ let suppressed = 0;
65
+ const kept = findings.filter((f) => {
66
+ const supp = getSupp(f.file);
67
+ if (!supp)
68
+ return true;
69
+ if (scopeSuppresses(supp.fileScope, f.ruleId) || scopeSuppresses(supp.lines.get(f.line), f.ruleId)) {
70
+ suppressed++;
71
+ return false;
72
+ }
73
+ return true;
74
+ });
75
+ return { kept, suppressed };
76
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });