@lbroth/rothunter 1.0.0-rc.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.
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/dist/adapters/llm.d.ts +68 -0
- package/dist/adapters/llm.d.ts.map +1 -0
- package/dist/adapters/llm.js +189 -0
- package/dist/adapters/llm.js.map +1 -0
- package/dist/config.d.ts +37 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +81 -0
- package/dist/config.js.map +1 -0
- package/dist/detector-registry.d.ts +32 -0
- package/dist/detector-registry.d.ts.map +1 -0
- package/dist/detector-registry.js +74 -0
- package/dist/detector-registry.js.map +1 -0
- package/dist/detectors/api-race.d.ts +6 -0
- package/dist/detectors/api-race.d.ts.map +1 -0
- package/dist/detectors/api-race.js +222 -0
- package/dist/detectors/api-race.js.map +1 -0
- package/dist/detectors/bad-config.d.ts +6 -0
- package/dist/detectors/bad-config.d.ts.map +1 -0
- package/dist/detectors/bad-config.js +529 -0
- package/dist/detectors/bad-config.js.map +1 -0
- package/dist/detectors/console-log-prod.d.ts +6 -0
- package/dist/detectors/console-log-prod.d.ts.map +1 -0
- package/dist/detectors/console-log-prod.js +72 -0
- package/dist/detectors/console-log-prod.js.map +1 -0
- package/dist/detectors/dead-api.d.ts +10 -0
- package/dist/detectors/dead-api.d.ts.map +1 -0
- package/dist/detectors/dead-api.js +115 -0
- package/dist/detectors/dead-api.js.map +1 -0
- package/dist/detectors/dead-export.d.ts +12 -0
- package/dist/detectors/dead-export.d.ts.map +1 -0
- package/dist/detectors/dead-export.js +140 -0
- package/dist/detectors/dead-export.js.map +1 -0
- package/dist/detectors/dead-handler.d.ts +12 -0
- package/dist/detectors/dead-handler.d.ts.map +1 -0
- package/dist/detectors/dead-handler.js +40 -0
- package/dist/detectors/dead-handler.js.map +1 -0
- package/dist/detectors/dead-module.d.ts +14 -0
- package/dist/detectors/dead-module.d.ts.map +1 -0
- package/dist/detectors/dead-module.js +50 -0
- package/dist/detectors/dead-module.js.map +1 -0
- package/dist/detectors/deep-nesting.d.ts +12 -0
- package/dist/detectors/deep-nesting.d.ts.map +1 -0
- package/dist/detectors/deep-nesting.js +133 -0
- package/dist/detectors/deep-nesting.js.map +1 -0
- package/dist/detectors/duplicate-function.d.ts +9 -0
- package/dist/detectors/duplicate-function.d.ts.map +1 -0
- package/dist/detectors/duplicate-function.js +199 -0
- package/dist/detectors/duplicate-function.js.map +1 -0
- package/dist/detectors/duplicate-type.d.ts +9 -0
- package/dist/detectors/duplicate-type.d.ts.map +1 -0
- package/dist/detectors/duplicate-type.js +166 -0
- package/dist/detectors/duplicate-type.js.map +1 -0
- package/dist/detectors/hot-hub-file.d.ts +11 -0
- package/dist/detectors/hot-hub-file.d.ts.map +1 -0
- package/dist/detectors/hot-hub-file.js +42 -0
- package/dist/detectors/hot-hub-file.js.map +1 -0
- package/dist/detectors/long-file.d.ts +12 -0
- package/dist/detectors/long-file.d.ts.map +1 -0
- package/dist/detectors/long-file.js +82 -0
- package/dist/detectors/long-file.js.map +1 -0
- package/dist/detectors/long-function.d.ts +12 -0
- package/dist/detectors/long-function.d.ts.map +1 -0
- package/dist/detectors/long-function.js +45 -0
- package/dist/detectors/long-function.js.map +1 -0
- package/dist/detectors/magic-numbers.d.ts +10 -0
- package/dist/detectors/magic-numbers.d.ts.map +1 -0
- package/dist/detectors/magic-numbers.js +332 -0
- package/dist/detectors/magic-numbers.js.map +1 -0
- package/dist/detectors/mutable-globals.d.ts +6 -0
- package/dist/detectors/mutable-globals.d.ts.map +1 -0
- package/dist/detectors/mutable-globals.js +95 -0
- package/dist/detectors/mutable-globals.js.map +1 -0
- package/dist/detectors/mutation.d.ts +11 -0
- package/dist/detectors/mutation.d.ts.map +1 -0
- package/dist/detectors/mutation.js +397 -0
- package/dist/detectors/mutation.js.map +1 -0
- package/dist/detectors/public-any.d.ts +6 -0
- package/dist/detectors/public-any.d.ts.map +1 -0
- package/dist/detectors/public-any.js +52 -0
- package/dist/detectors/public-any.js.map +1 -0
- package/dist/detectors/race-condition.d.ts +6 -0
- package/dist/detectors/race-condition.d.ts.map +1 -0
- package/dist/detectors/race-condition.js +608 -0
- package/dist/detectors/race-condition.js.map +1 -0
- package/dist/detectors/shared-db-write.d.ts +6 -0
- package/dist/detectors/shared-db-write.d.ts.map +1 -0
- package/dist/detectors/shared-db-write.js +656 -0
- package/dist/detectors/shared-db-write.js.map +1 -0
- package/dist/detectors/silent-catch.d.ts +6 -0
- package/dist/detectors/silent-catch.d.ts.map +1 -0
- package/dist/detectors/silent-catch.js +167 -0
- package/dist/detectors/silent-catch.js.map +1 -0
- package/dist/detectors/similar-functions.d.ts +15 -0
- package/dist/detectors/similar-functions.d.ts.map +1 -0
- package/dist/detectors/similar-functions.js +334 -0
- package/dist/detectors/similar-functions.js.map +1 -0
- package/dist/detectors/skip-tests.d.ts +6 -0
- package/dist/detectors/skip-tests.d.ts.map +1 -0
- package/dist/detectors/skip-tests.js +69 -0
- package/dist/detectors/skip-tests.js.map +1 -0
- package/dist/detectors/todo-comments.d.ts +29 -0
- package/dist/detectors/todo-comments.d.ts.map +1 -0
- package/dist/detectors/todo-comments.js +154 -0
- package/dist/detectors/todo-comments.js.map +1 -0
- package/dist/detectors/unused-deps.d.ts +8 -0
- package/dist/detectors/unused-deps.d.ts.map +1 -0
- package/dist/detectors/unused-deps.js +115 -0
- package/dist/detectors/unused-deps.js.map +1 -0
- package/dist/extraction/api-race-confirmer.d.ts +31 -0
- package/dist/extraction/api-race-confirmer.d.ts.map +1 -0
- package/dist/extraction/api-race-confirmer.js +110 -0
- package/dist/extraction/api-race-confirmer.js.map +1 -0
- package/dist/extraction/llm-confirmer.d.ts +25 -0
- package/dist/extraction/llm-confirmer.d.ts.map +1 -0
- package/dist/extraction/llm-confirmer.js +118 -0
- package/dist/extraction/llm-confirmer.js.map +1 -0
- package/dist/extraction/mutation-confirmer.d.ts +30 -0
- package/dist/extraction/mutation-confirmer.d.ts.map +1 -0
- package/dist/extraction/mutation-confirmer.js +73 -0
- package/dist/extraction/mutation-confirmer.js.map +1 -0
- package/dist/extraction/prompt-chunking.d.ts +37 -0
- package/dist/extraction/prompt-chunking.d.ts.map +1 -0
- package/dist/extraction/prompt-chunking.js +61 -0
- package/dist/extraction/prompt-chunking.js.map +1 -0
- package/dist/extraction/race-confirmer.d.ts +28 -0
- package/dist/extraction/race-confirmer.d.ts.map +1 -0
- package/dist/extraction/race-confirmer.js +68 -0
- package/dist/extraction/race-confirmer.js.map +1 -0
- package/dist/extraction/shared-db-write-confirmer.d.ts +31 -0
- package/dist/extraction/shared-db-write-confirmer.d.ts.map +1 -0
- package/dist/extraction/shared-db-write-confirmer.js +141 -0
- package/dist/extraction/shared-db-write-confirmer.js.map +1 -0
- package/dist/extraction/triage-confirmer.d.ts +59 -0
- package/dist/extraction/triage-confirmer.d.ts.map +1 -0
- package/dist/extraction/triage-confirmer.js +104 -0
- package/dist/extraction/triage-confirmer.js.map +1 -0
- package/dist/graph/cfg.d.ts +45 -0
- package/dist/graph/cfg.d.ts.map +1 -0
- package/dist/graph/cfg.js +198 -0
- package/dist/graph/cfg.js.map +1 -0
- package/dist/graph/decorator-entries.d.ts +2 -0
- package/dist/graph/decorator-entries.d.ts.map +1 -0
- package/dist/graph/decorator-entries.js +89 -0
- package/dist/graph/decorator-entries.js.map +1 -0
- package/dist/graph/entry-points.d.ts +12 -0
- package/dist/graph/entry-points.d.ts.map +1 -0
- package/dist/graph/entry-points.js +282 -0
- package/dist/graph/entry-points.js.map +1 -0
- package/dist/graph/handler-conventions.d.ts +2 -0
- package/dist/graph/handler-conventions.d.ts.map +1 -0
- package/dist/graph/handler-conventions.js +26 -0
- package/dist/graph/handler-conventions.js.map +1 -0
- package/dist/graph/iac-entries.d.ts +2 -0
- package/dist/graph/iac-entries.d.ts.map +1 -0
- package/dist/graph/iac-entries.js +123 -0
- package/dist/graph/iac-entries.js.map +1 -0
- package/dist/graph/import-graph.d.ts +48 -0
- package/dist/graph/import-graph.d.ts.map +1 -0
- package/dist/graph/import-graph.js +86 -0
- package/dist/graph/import-graph.js.map +1 -0
- package/dist/graph/monorepo-detect.d.ts +3 -0
- package/dist/graph/monorepo-detect.d.ts.map +1 -0
- package/dist/graph/monorepo-detect.js +166 -0
- package/dist/graph/monorepo-detect.js.map +1 -0
- package/dist/graph/tsconfig-paths.d.ts +23 -0
- package/dist/graph/tsconfig-paths.d.ts.map +1 -0
- package/dist/graph/tsconfig-paths.js +217 -0
- package/dist/graph/tsconfig-paths.js.map +1 -0
- package/dist/multi-workspace-scanner.d.ts +13 -0
- package/dist/multi-workspace-scanner.d.ts.map +1 -0
- package/dist/multi-workspace-scanner.js +130 -0
- package/dist/multi-workspace-scanner.js.map +1 -0
- package/dist/normalizers/type-normalizer.d.ts +16 -0
- package/dist/normalizers/type-normalizer.d.ts.map +1 -0
- package/dist/normalizers/type-normalizer.js +189 -0
- package/dist/normalizers/type-normalizer.js.map +1 -0
- package/dist/parsers/typescript-parser.d.ts +57 -0
- package/dist/parsers/typescript-parser.d.ts.map +1 -0
- package/dist/parsers/typescript-parser.js +502 -0
- package/dist/parsers/typescript-parser.js.map +1 -0
- package/dist/reporter/json-reporter.d.ts +12 -0
- package/dist/reporter/json-reporter.d.ts.map +1 -0
- package/dist/reporter/json-reporter.js +28 -0
- package/dist/reporter/json-reporter.js.map +1 -0
- package/dist/reporter/markdown-reporter.d.ts +11 -0
- package/dist/reporter/markdown-reporter.d.ts.map +1 -0
- package/dist/reporter/markdown-reporter.js +77 -0
- package/dist/reporter/markdown-reporter.js.map +1 -0
- package/dist/rothunter.d.ts +125 -0
- package/dist/rothunter.d.ts.map +1 -0
- package/dist/rothunter.js +1038 -0
- package/dist/rothunter.js.map +1 -0
- package/dist/server/false-positives.d.ts +34 -0
- package/dist/server/false-positives.d.ts.map +1 -0
- package/dist/server/false-positives.js +85 -0
- package/dist/server/false-positives.js.map +1 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +1529 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/marked-to-fix.d.ts +16 -0
- package/dist/server/marked-to-fix.d.ts.map +1 -0
- package/dist/server/marked-to-fix.js +36 -0
- package/dist/server/marked-to-fix.js.map +1 -0
- package/dist/server/scan-store.d.ts +147 -0
- package/dist/server/scan-store.d.ts.map +1 -0
- package/dist/server/scan-store.js +291 -0
- package/dist/server/scan-store.js.map +1 -0
- package/dist/server/settings-store.d.ts +28 -0
- package/dist/server/settings-store.d.ts.map +1 -0
- package/dist/server/settings-store.js +46 -0
- package/dist/server/settings-store.js.map +1 -0
- package/dist/server/workspace-store.d.ts +39 -0
- package/dist/server/workspace-store.d.ts.map +1 -0
- package/dist/server/workspace-store.js +108 -0
- package/dist/server/workspace-store.js.map +1 -0
- package/dist/types/detector-input.d.ts +37 -0
- package/dist/types/detector-input.d.ts.map +1 -0
- package/dist/types/detector-input.js +2 -0
- package/dist/types/detector-input.js.map +1 -0
- package/dist/types.d.ts +110 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/clustering.d.ts +14 -0
- package/dist/utils/clustering.d.ts.map +1 -0
- package/dist/utils/clustering.js +56 -0
- package/dist/utils/clustering.js.map +1 -0
- package/dist/utils/gitignore.d.ts +32 -0
- package/dist/utils/gitignore.d.ts.map +1 -0
- package/dist/utils/gitignore.js +122 -0
- package/dist/utils/gitignore.js.map +1 -0
- package/dist/utils/hash.d.ts +11 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +14 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/ignore-annotation.d.ts +28 -0
- package/dist/utils/ignore-annotation.d.ts.map +1 -0
- package/dist/utils/ignore-annotation.js +46 -0
- package/dist/utils/ignore-annotation.js.map +1 -0
- package/dist/utils/llm-json.d.ts +2 -0
- package/dist/utils/llm-json.d.ts.map +1 -0
- package/dist/utils/llm-json.js +53 -0
- package/dist/utils/llm-json.js.map +1 -0
- package/dist/utils/logger.d.ts +3 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +4 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/project-conventions.d.ts +2 -0
- package/dist/utils/project-conventions.d.ts.map +1 -0
- package/dist/utils/project-conventions.js +108 -0
- package/dist/utils/project-conventions.js.map +1 -0
- package/dist/utils/regex.d.ts +9 -0
- package/dist/utils/regex.d.ts.map +1 -0
- package/dist/utils/regex.js +11 -0
- package/dist/utils/regex.js.map +1 -0
- package/dist/utils/snippet.d.ts +20 -0
- package/dist/utils/snippet.d.ts.map +1 -0
- package/dist/utils/snippet.js +28 -0
- package/dist/utils/snippet.js.map +1 -0
- package/dist/utils/source-reader.d.ts +19 -0
- package/dist/utils/source-reader.d.ts.map +1 -0
- package/dist/utils/source-reader.js +32 -0
- package/dist/utils/source-reader.js.map +1 -0
- package/logo.png +0 -0
- package/package.json +92 -0
- package/scripts/start-llm.mjs +161 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { stableHash } from '../utils/hash.js';
|
|
2
|
+
// Flag function symbols over 60 (low) / 120 (med) / 200 (high) lines.
|
|
3
|
+
// Test bodies (describe/it/test/suite) skipped.
|
|
4
|
+
export function detectLongFunctions(input) {
|
|
5
|
+
const low = input.lowThreshold ?? 60;
|
|
6
|
+
const med = input.medThreshold ?? 120;
|
|
7
|
+
const high = input.highThreshold ?? 200;
|
|
8
|
+
const findings = [];
|
|
9
|
+
for (const sym of input.symbols) {
|
|
10
|
+
if (sym.kind !== 'function')
|
|
11
|
+
continue;
|
|
12
|
+
if (isTestHarnessName(sym.name))
|
|
13
|
+
continue;
|
|
14
|
+
const lines = sym.range.endLine - sym.range.startLine + 1;
|
|
15
|
+
if (lines < low)
|
|
16
|
+
continue;
|
|
17
|
+
const severity = lines >= high ? 'high' : lines >= med ? 'medium' : 'low';
|
|
18
|
+
findings.push({
|
|
19
|
+
detectorId: 'long-function',
|
|
20
|
+
severity,
|
|
21
|
+
confidence: 0.98,
|
|
22
|
+
layer: 1,
|
|
23
|
+
title: `Long function: \`${sym.name}\` (${lines} lines) in ${sym.file}`,
|
|
24
|
+
description: `\`${sym.name}\` spans ${lines} lines (${sym.range.startLine}–${sym.range.endLine}). Functions this long are hard to test, hard to reason about, and accumulate branching that begs to be flattened (polymorphism, lookup tables, early returns).`,
|
|
25
|
+
evidence: [
|
|
26
|
+
{
|
|
27
|
+
file: sym.file,
|
|
28
|
+
range: { startLine: sym.range.startLine, endLine: sym.range.endLine },
|
|
29
|
+
snippet: firstLines(sym.source, 4),
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
suggestion: 'Extract one cohesive chunk at a time into a well-named helper. If branching dominates, replace the switch/if chain with a dispatch table keyed by the discriminator.',
|
|
33
|
+
fingerprint: `long-function:${stableHash(`${sym.file}:${sym.name}:${sym.range.startLine}`)}`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return findings;
|
|
37
|
+
}
|
|
38
|
+
const TEST_HARNESS_NAMES = new Set(['describe', 'it', 'test', 'suite', 'context']);
|
|
39
|
+
function isTestHarnessName(name) {
|
|
40
|
+
return TEST_HARNESS_NAMES.has(name);
|
|
41
|
+
}
|
|
42
|
+
function firstLines(source, n) {
|
|
43
|
+
return source.split('\n').slice(0, n).join('\n');
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=long-function.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"long-function.js","sourceRoot":"","sources":["../../src/detectors/long-function.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAY9C,sEAAsE;AACtE,gDAAgD;AAChD,MAAM,UAAU,mBAAmB,CAAC,KAAgC;IAClE,MAAM,GAAG,GAAG,KAAK,CAAC,YAAY,IAAI,EAAE,CAAC;IACrC,MAAM,GAAG,GAAG,KAAK,CAAC,YAAY,IAAI,GAAG,CAAC;IACtC,MAAM,IAAI,GAAG,KAAK,CAAC,aAAa,IAAI,GAAG,CAAC;IACxC,MAAM,QAAQ,GAAc,EAAE,CAAC;IAE/B,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QAChC,IAAI,GAAG,CAAC,IAAI,KAAK,UAAU;YAAE,SAAS;QACtC,IAAI,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAS;QAC1C,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC;QAC1D,IAAI,KAAK,GAAG,GAAG;YAAE,SAAS;QAC1B,MAAM,QAAQ,GAA8B,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC;QACrG,QAAQ,CAAC,IAAI,CAAC;YACZ,UAAU,EAAE,eAAe;YAC3B,QAAQ;YACR,UAAU,EAAE,IAAI;YAChB,KAAK,EAAE,CAAC;YACR,KAAK,EAAE,oBAAoB,GAAG,CAAC,IAAI,OAAO,KAAK,cAAc,GAAG,CAAC,IAAI,EAAE;YACvE,WAAW,EACT,KAAK,GAAG,CAAC,IAAI,YAAY,KAAK,WAAW,GAAG,CAAC,KAAK,CAAC,SAAS,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,iKAAiK;YACpP,QAAQ,EAAE;gBACR;oBACE,IAAI,EAAE,GAAG,CAAC,IAAI;oBACd,KAAK,EAAE,EAAE,SAAS,EAAE,GAAG,CAAC,KAAK,CAAC,SAAS,EAAE,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE;oBACrE,OAAO,EAAE,UAAU,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;iBACnC;aACF;YACD,UAAU,EACR,sKAAsK;YACxK,WAAW,EAAE,iBAAiB,UAAU,CAAC,GAAG,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC,EAAE;SAC7F,CAAC,CAAC;IACL,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;AAEnF,SAAS,iBAAiB,CAAC,IAAY;IACrC,OAAO,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACtC,CAAC;AAED,SAAS,UAAU,CAAC,MAAc,EAAE,CAAS;IAC3C,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACnD,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Finding } from '../types.js';
|
|
2
|
+
import type { FileWalkingDetectorInput } from '../types/detector-input.js';
|
|
3
|
+
export interface MagicNumbersDetectorInput extends FileWalkingDetectorInput {
|
|
4
|
+
/** Numbers considered "obvious" and not magic. Default `{0, 1, -1, 2, 10, 100, 1000}`. */
|
|
5
|
+
whitelist?: ReadonlySet<number>;
|
|
6
|
+
/** Per-file finding cap so a single noisy file doesn't dominate the report. Default 5. */
|
|
7
|
+
perFileCap?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function detectMagicNumbers(input: MagicNumbersDetectorInput): Finding[];
|
|
10
|
+
//# sourceMappingURL=magic-numbers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"magic-numbers.d.ts","sourceRoot":"","sources":["../../src/detectors/magic-numbers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAI3C,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAE3E,MAAM,WAAW,yBAA0B,SAAQ,wBAAwB;IAC3E,0FAA0F;IACxF,SAAS,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAChC,0FAA0F;IAC1F,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAID,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,yBAAyB,GAAG,OAAO,EAAE,CAY9E"}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { makeSourceReader } from '../utils/source-reader.js';
|
|
2
|
+
import { stableHash } from '../utils/hash.js';
|
|
3
|
+
import { hasIgnoreAnnotation } from '../utils/ignore-annotation.js';
|
|
4
|
+
// Numeric literals outside the whitelist {0,1,-1,2,10,100,1000}. Skip
|
|
5
|
+
// array indices, for-loop bounds, named-const declarations. LOW, per-file cap.
|
|
6
|
+
export function detectMagicNumbers(input) {
|
|
7
|
+
const whitelist = input.whitelist ?? DEFAULT_WHITELIST;
|
|
8
|
+
const cap = input.perFileCap ?? 5;
|
|
9
|
+
const read = makeSourceReader(input.workspaceRoot, input.project);
|
|
10
|
+
const findings = [];
|
|
11
|
+
for (const rel of input.files) {
|
|
12
|
+
if (!isAnalysable(rel))
|
|
13
|
+
continue;
|
|
14
|
+
const raw = read(rel);
|
|
15
|
+
if (raw == null)
|
|
16
|
+
continue;
|
|
17
|
+
findings.push(...analyseFile(rel, raw, whitelist, cap));
|
|
18
|
+
}
|
|
19
|
+
return findings;
|
|
20
|
+
}
|
|
21
|
+
// Default whitelist. Includes the obvious math constants (0/1/-1/2),
|
|
22
|
+
// common round numbers (10/100/1000), AND bit-width / byte-size
|
|
23
|
+
// constants (8/16/24/32/64/128/256). Encoder/decoder code (base64,
|
|
24
|
+
// chunking, hashing, network parsing) is otherwise flooded with FPs:
|
|
25
|
+
// `Math.floor(byteLength / 9 * 8)` and friends are domain math, not
|
|
26
|
+
// magic numbers. Add 512/1024/4096 too — they're page/buffer sizes
|
|
27
|
+
// every reader recognises at a glance.
|
|
28
|
+
const DEFAULT_WHITELIST = new Set([
|
|
29
|
+
0, 1, -1, 2,
|
|
30
|
+
10, 100, 1000,
|
|
31
|
+
8, 16, 24, 32, 64, 128, 256, 512, 1024, 4096,
|
|
32
|
+
]);
|
|
33
|
+
// Match positive integer literals (we treat negatives via the previous char).
|
|
34
|
+
const NUM_RE = /\b(\d+(?:\.\d+)?)\b/g;
|
|
35
|
+
function analyseFile(file, raw, whitelist, cap) {
|
|
36
|
+
const out = [];
|
|
37
|
+
// Pre-strip strings + comments AND regex literals so literals inside
|
|
38
|
+
// them aren't flagged. Regex masking matters a lot: `[A-Za-z0-9]` flags
|
|
39
|
+
// 9, `\d{15}` flags 15, `\d{1,3}` flags 3, etc. Those are charset/
|
|
40
|
+
// quantifier internals, not magic numbers.
|
|
41
|
+
const masked = maskStringsAndComments(raw);
|
|
42
|
+
for (const m of masked.matchAll(NUM_RE)) {
|
|
43
|
+
if (out.length >= cap)
|
|
44
|
+
break;
|
|
45
|
+
const positive = parseFloat(m[1]);
|
|
46
|
+
const before = masked.slice(Math.max(0, m.index - 30), m.index);
|
|
47
|
+
const after = masked.slice(m.index + m[0].length, m.index + m[0].length + 30);
|
|
48
|
+
// Distinguish UNARY minus (`-3`, `[-3]`, `f(-3)`, `return -3`) from
|
|
49
|
+
// BINARY subtraction (`x - 3`). Unary triggers when the char before
|
|
50
|
+
// the `-` is start-of-string, opening punctuation, a comma, an
|
|
51
|
+
// operator/comparison, or a `:=` assignment. Identifier or closing-
|
|
52
|
+
// bracket before `-` means subtraction → keep the value positive.
|
|
53
|
+
const unaryMinus = /(?:^|[=([{,;:?+\-*/%&|^!<>~]|\b(?:return|typeof|in|of|case|delete|void|throw|yield|await|new)\b)\s*-\s*$/.test(before);
|
|
54
|
+
const value = unaryMinus ? -positive : positive;
|
|
55
|
+
if (whitelist.has(value))
|
|
56
|
+
continue;
|
|
57
|
+
// Skip if the literal is being assigned to a constant (declaration site).
|
|
58
|
+
if (/\b(?:const|let|var|enum|readonly)\s+[A-Z_][A-Z0-9_]*\s*[:=]\s*-?\s*$/.test(before))
|
|
59
|
+
continue;
|
|
60
|
+
if (/\b(?:const|let|var)\s+\w+\s*[:=]\s*-?\s*$/.test(before))
|
|
61
|
+
continue;
|
|
62
|
+
// Skip array indices (preceded by `[`).
|
|
63
|
+
if (/\[\s*-?\s*$/.test(before))
|
|
64
|
+
continue;
|
|
65
|
+
// Skip enum members.
|
|
66
|
+
if (/=\s*-?\s*$/.test(before) && /\benum\b/.test(masked.slice(Math.max(0, m.index - 200), m.index)))
|
|
67
|
+
continue;
|
|
68
|
+
// Skip exponents (e.g. 1e-3 or 1e+3 — the `3` should not be flagged).
|
|
69
|
+
if (/e[+-]?$/i.test(before))
|
|
70
|
+
continue;
|
|
71
|
+
// Skip HTTP status code idioms — `reply.code(502)`, `res.status(404)`,
|
|
72
|
+
// `.code(401).send(...)`, `throw new HttpError(500, ...)`. The method
|
|
73
|
+
// name IS the named constant in framework code; introducing a
|
|
74
|
+
// `HTTP_BAD_GATEWAY = 502` const adds friction without clarity.
|
|
75
|
+
if (/\.(?:code|status|statusCode|sendStatus)\s*\(\s*-?\s*$/.test(before) && value >= 100 && value < 600)
|
|
76
|
+
continue;
|
|
77
|
+
// Skip HTTP success/error-tier range checks: `if (status < 200 ||
|
|
78
|
+
// status >= 300)` and friends. The 200/300/400/500 boundaries are
|
|
79
|
+
// textbook HTTP semantics; flagging them invites the reader to
|
|
80
|
+
// invent names that read worse than the literal.
|
|
81
|
+
if (value >= 100 && value < 600 && value % 100 === 0 &&
|
|
82
|
+
/(?:<=?|>=?|===?|!==?)\s*$/.test(before) &&
|
|
83
|
+
/\b(?:status|statusCode|httpStatus|http_status|code)\b/.test(lineAround(masked, m.index)))
|
|
84
|
+
continue;
|
|
85
|
+
// Skip elements of `new Set([...])` / `new Map([...])` / array literal
|
|
86
|
+
// bound to a named const. The CONST NAME documents what each value
|
|
87
|
+
// represents (`RETRYABLE_HTTP`, `ALLOWED_PORTS`, …) — naming each
|
|
88
|
+
// element individually is busywork.
|
|
89
|
+
if (insideNamedConstCollection(masked, m.index))
|
|
90
|
+
continue;
|
|
91
|
+
// Skip `key: NUMBER,` inside object literals when the value is a
|
|
92
|
+
// bare literal (no expression). `confidence: 0.95,` / `timeoutMs:
|
|
93
|
+
// 1500,` style — the KEY names the magic. Same effective semantics
|
|
94
|
+
// as `const KEY = NUMBER`. Tight bounds avoid catching subtraction.
|
|
95
|
+
if (/[:=]\s*-?\s*$/.test(before) &&
|
|
96
|
+
/^\s*-?\s*[,;)}\]\n]/.test(after) &&
|
|
97
|
+
!/\b(?:return|throw|case|new)\b\s*-?\s*$/.test(before)) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const line = lineOf(raw, m.index);
|
|
101
|
+
if (hasIgnoreAnnotation(raw, line, 'magic-numbers'))
|
|
102
|
+
continue;
|
|
103
|
+
out.push({
|
|
104
|
+
detectorId: 'magic-numbers',
|
|
105
|
+
severity: 'low',
|
|
106
|
+
confidence: 0.7,
|
|
107
|
+
layer: 1,
|
|
108
|
+
title: `Magic number \`${m[1]}\` in ${file}:${line}`,
|
|
109
|
+
description: `Numeric literal \`${m[1]}\` appears in business logic without a named constant. Re-readers must guess what it represents (a timeout? a port? a column count?).`,
|
|
110
|
+
evidence: [
|
|
111
|
+
{
|
|
112
|
+
file,
|
|
113
|
+
range: { startLine: line, endLine: line },
|
|
114
|
+
snippet: snippetAround(raw, line),
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
suggestion: 'Extract to a named const (`const RETRY_LIMIT = 3;`). If it derives from a real-world unit, encode the unit in the name (`TIMEOUT_MS`, `MAX_AGE_DAYS`).',
|
|
118
|
+
fingerprint: `magic-numbers:${stableHash(`${file}:${line}:${m[1]}`)}`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
function maskStringsAndComments(raw) {
|
|
124
|
+
let out = '';
|
|
125
|
+
let inString = null;
|
|
126
|
+
let inLineComment = false;
|
|
127
|
+
let inBlockComment = false;
|
|
128
|
+
let inRegex = false;
|
|
129
|
+
let inRegexClass = false;
|
|
130
|
+
// Track the previous non-whitespace masked-output character so we can
|
|
131
|
+
// disambiguate `/` as regex-start vs division. After punctuation /
|
|
132
|
+
// keywords a `/` opens a regex; after an identifier or closing bracket
|
|
133
|
+
// it is division.
|
|
134
|
+
let prevSig = '';
|
|
135
|
+
for (let i = 0; i < raw.length; i++) {
|
|
136
|
+
const c = raw[i];
|
|
137
|
+
const next = raw[i + 1];
|
|
138
|
+
if (inLineComment) {
|
|
139
|
+
if (c === '\n') {
|
|
140
|
+
inLineComment = false;
|
|
141
|
+
out += '\n';
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
out += ' ';
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (inBlockComment) {
|
|
149
|
+
if (c === '*' && next === '/') {
|
|
150
|
+
inBlockComment = false;
|
|
151
|
+
out += ' ';
|
|
152
|
+
i++;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
out += c === '\n' ? '\n' : ' ';
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (inString) {
|
|
160
|
+
if (c === '\\') {
|
|
161
|
+
out += ' ';
|
|
162
|
+
i++;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (c === inString) {
|
|
166
|
+
inString = null;
|
|
167
|
+
out += c;
|
|
168
|
+
prevSig = c;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
out += c === '\n' ? '\n' : ' ';
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (inRegex) {
|
|
175
|
+
// Mask everything inside the regex body so the numeric scanner sees
|
|
176
|
+
// no digits. Track `[…]` character classes only to know when the
|
|
177
|
+
// regex actually ends (slash inside class does not terminate).
|
|
178
|
+
if (c === '\\') {
|
|
179
|
+
out += ' ';
|
|
180
|
+
i++;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (c === '[' && !inRegexClass)
|
|
184
|
+
inRegexClass = true;
|
|
185
|
+
else if (c === ']' && inRegexClass)
|
|
186
|
+
inRegexClass = false;
|
|
187
|
+
if (c === '/' && !inRegexClass) {
|
|
188
|
+
inRegex = false;
|
|
189
|
+
out += c;
|
|
190
|
+
prevSig = c;
|
|
191
|
+
// Consume trailing flags (g, i, m, s, u, y, d) so they cannot be
|
|
192
|
+
// misread as identifiers in lookback.
|
|
193
|
+
while (i + 1 < raw.length && /[gimsuyd]/.test(raw[i + 1] ?? '')) {
|
|
194
|
+
out += ' ';
|
|
195
|
+
i++;
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
out += c === '\n' ? '\n' : ' ';
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (c === '/' && next === '/') {
|
|
203
|
+
inLineComment = true;
|
|
204
|
+
out += ' ';
|
|
205
|
+
i++;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (c === '/' && next === '*') {
|
|
209
|
+
inBlockComment = true;
|
|
210
|
+
out += ' ';
|
|
211
|
+
i++;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (c === '/' && canStartRegexAfter(prevSig)) {
|
|
215
|
+
inRegex = true;
|
|
216
|
+
inRegexClass = false;
|
|
217
|
+
out += c;
|
|
218
|
+
prevSig = c;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
222
|
+
inString = c;
|
|
223
|
+
out += c;
|
|
224
|
+
prevSig = c;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
out += c;
|
|
228
|
+
if (c !== ' ' && c !== '\t' && c !== '\n' && c !== '\r')
|
|
229
|
+
prevSig = c;
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Decide whether a `/` is the start of a regex literal (vs division)
|
|
235
|
+
* based on the previous non-whitespace character. After punctuation,
|
|
236
|
+
* an opening bracket / paren / brace, a comma, a return-like keyword,
|
|
237
|
+
* or an operator a slash starts a regex. After an identifier char or
|
|
238
|
+
* closing bracket it's division.
|
|
239
|
+
*/
|
|
240
|
+
function canStartRegexAfter(prev) {
|
|
241
|
+
if (prev === '')
|
|
242
|
+
return true;
|
|
243
|
+
return /[=(,;:?{[+\-*/%&|^!<>~]/.test(prev);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Detect whether a numeric literal at `idx` sits inside a collection
|
|
247
|
+
* literal (`new Set([…])`, `new Map([…])`, plain `[…]`) bound to a
|
|
248
|
+
* named const declared earlier on the same logical statement. The
|
|
249
|
+
* collection name documents each element — `RETRYABLE_HTTP = new Set
|
|
250
|
+
* ([408, 425, 429, …])` does not need every literal extracted to its
|
|
251
|
+
* own const.
|
|
252
|
+
*
|
|
253
|
+
* Walks backwards from `idx` counting `[` / `]` / `(` / `)` to find the
|
|
254
|
+
* enclosing bracket open, then checks whether the chunk between the
|
|
255
|
+
* preceding `=` and the bracket looks like a collection constructor or
|
|
256
|
+
* a bare array literal.
|
|
257
|
+
*/
|
|
258
|
+
function insideNamedConstCollection(masked, idx) {
|
|
259
|
+
let depthSq = 0;
|
|
260
|
+
let depthPar = 0;
|
|
261
|
+
for (let i = idx - 1; i >= 0; i--) {
|
|
262
|
+
const c = masked[i];
|
|
263
|
+
if (c === ']')
|
|
264
|
+
depthSq++;
|
|
265
|
+
else if (c === '[') {
|
|
266
|
+
if (depthSq === 0) {
|
|
267
|
+
// Found the enclosing array open. Look back for `=` to find the
|
|
268
|
+
// assignment target.
|
|
269
|
+
const lhs = masked.slice(Math.max(0, i - 200), i);
|
|
270
|
+
const eq = lhs.lastIndexOf('=');
|
|
271
|
+
if (eq === -1)
|
|
272
|
+
return false;
|
|
273
|
+
const declHead = lhs.slice(Math.max(0, eq - 80), eq);
|
|
274
|
+
if (!/\b(?:const|let|var|readonly)\s+\w+/.test(declHead))
|
|
275
|
+
return false;
|
|
276
|
+
const between = lhs.slice(eq + 1).trim();
|
|
277
|
+
// Bracket directly follows `=` (bare array) OR follows
|
|
278
|
+
// `new Set(` / `new Map(` / `new Array(` / `new Uint8Array(`
|
|
279
|
+
// etc. The trailing `(` is part of the constructor call —
|
|
280
|
+
// `new Set([...])` has the array inside the call parens.
|
|
281
|
+
if (between === '' || /\bnew\s+\w+\s*\(?\s*$/.test(between))
|
|
282
|
+
return true;
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
depthSq--;
|
|
286
|
+
}
|
|
287
|
+
else if (c === ')')
|
|
288
|
+
depthPar++;
|
|
289
|
+
else if (c === '(') {
|
|
290
|
+
if (depthPar === 0)
|
|
291
|
+
return false;
|
|
292
|
+
depthPar--;
|
|
293
|
+
}
|
|
294
|
+
else if (c === ';' && depthSq === 0 && depthPar === 0) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
function isAnalysable(file) {
|
|
301
|
+
const posix = file.replace(/\\/g, '/');
|
|
302
|
+
return /\.(?:ts|tsx|mts|cts)$/.test(posix)
|
|
303
|
+
&& !/\.d\.ts$/.test(posix)
|
|
304
|
+
&& !/(^|\/)node_modules\//.test(posix)
|
|
305
|
+
&& !/(?:^|\/)__tests__\//.test(posix)
|
|
306
|
+
&& !/(?:^|\/)tests?\//.test(posix)
|
|
307
|
+
&& !/(?:^|\/)scripts?\//.test(posix)
|
|
308
|
+
&& !/\.test\.(?:ts|tsx)$/.test(posix)
|
|
309
|
+
&& !/\.spec\.(?:ts|tsx)$/.test(posix)
|
|
310
|
+
// Tool config files are mostly threshold / timeout / port numbers
|
|
311
|
+
// by design — flagging every coverage threshold + chunk-size limit
|
|
312
|
+
// as a magic number is noise. Skip the common tooling configs.
|
|
313
|
+
&& !/(?:^|\/)(?:vite|vitest|jest|rollup|webpack|esbuild|tsup|playwright|cypress|drizzle|next|nuxt|astro|svelte|remix|tailwind|postcss|prettier|eslint|biome|babel|rome|tsdown|tsconfig\.[^/]*)\.config\.(?:ts|tsx|mts|cts|js|mjs|cjs)$/.test(posix)
|
|
314
|
+
&& !/(?:^|\/)\.?(?:eslint|prettier|stylelint)rc(?:\.[^/]+)?$/.test(posix);
|
|
315
|
+
}
|
|
316
|
+
function lineOf(raw, idx) {
|
|
317
|
+
return raw.slice(0, idx).split('\n').length;
|
|
318
|
+
}
|
|
319
|
+
function lineAround(raw, idx) {
|
|
320
|
+
const start = raw.lastIndexOf('\n', idx) + 1;
|
|
321
|
+
let end = raw.indexOf('\n', idx);
|
|
322
|
+
if (end === -1)
|
|
323
|
+
end = raw.length;
|
|
324
|
+
return raw.slice(start, end);
|
|
325
|
+
}
|
|
326
|
+
function snippetAround(raw, line) {
|
|
327
|
+
const lines = raw.split('\n');
|
|
328
|
+
const from = Math.max(0, line - 1);
|
|
329
|
+
const to = Math.min(lines.length, line + 1);
|
|
330
|
+
return lines.slice(from, to).join('\n');
|
|
331
|
+
}
|
|
332
|
+
//# sourceMappingURL=magic-numbers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"magic-numbers.js","sourceRoot":"","sources":["../../src/detectors/magic-numbers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Finding } from '../types.js';
|
|
2
|
+
import type { FileWalkingDetectorInput } from '../types/detector-input.js';
|
|
3
|
+
export interface MutableGlobalsDetectorInput extends FileWalkingDetectorInput {
|
|
4
|
+
}
|
|
5
|
+
export declare function detectMutableGlobals(input: MutableGlobalsDetectorInput): Finding[];
|
|
6
|
+
//# sourceMappingURL=mutable-globals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutable-globals.d.ts","sourceRoot":"","sources":["../../src/detectors/mutable-globals.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAK3C,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAE3E,MAAM,WAAW,2BAA4B,SAAQ,wBAAwB;CAAG;AAIhF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,2BAA2B,GAAG,OAAO,EAAE,CAUlF"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { makeSourceReader } from '../utils/source-reader.js';
|
|
2
|
+
import { stableHash } from '../utils/hash.js';
|
|
3
|
+
import { hasIgnoreAnnotation } from '../utils/ignore-annotation.js';
|
|
4
|
+
import { escapeForRegex } from '../utils/regex.js';
|
|
5
|
+
// Top-level `let`/`var` reassigned in the same file → shared mutable
|
|
6
|
+
// state across importers. MED. One-shot assignment at decl is skipped.
|
|
7
|
+
export function detectMutableGlobals(input) {
|
|
8
|
+
const read = makeSourceReader(input.workspaceRoot, input.project);
|
|
9
|
+
const findings = [];
|
|
10
|
+
for (const rel of input.files) {
|
|
11
|
+
if (!isAnalysable(rel))
|
|
12
|
+
continue;
|
|
13
|
+
const raw = read(rel);
|
|
14
|
+
if (raw == null)
|
|
15
|
+
continue;
|
|
16
|
+
findings.push(...analyseFile(rel, raw));
|
|
17
|
+
}
|
|
18
|
+
return findings;
|
|
19
|
+
}
|
|
20
|
+
function analyseFile(file, raw) {
|
|
21
|
+
const lines = raw.split('\n');
|
|
22
|
+
const out = [];
|
|
23
|
+
// Locate top-level let/var declarations: line starts with `let `/`var `
|
|
24
|
+
// (allow `export let`).
|
|
25
|
+
const declRe = /^(?:export\s+)?(?:let|var)\s+(\w+)\b/;
|
|
26
|
+
const declarations = [];
|
|
27
|
+
let braceDepth = 0;
|
|
28
|
+
let parenDepth = 0;
|
|
29
|
+
let lineNo = 0;
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
lineNo++;
|
|
32
|
+
// crude tracking — strings/comments are NOT masked here, so multiline
|
|
33
|
+
// template-literal-heavy files may misalign; the heuristic still works
|
|
34
|
+
// in practice because we only care about indentation-0 declarations.
|
|
35
|
+
const m = declRe.exec(line);
|
|
36
|
+
if (m && braceDepth === 0 && parenDepth === 0) {
|
|
37
|
+
declarations.push({ name: m[1], line: lineNo });
|
|
38
|
+
}
|
|
39
|
+
for (const c of line) {
|
|
40
|
+
if (c === '{')
|
|
41
|
+
braceDepth++;
|
|
42
|
+
else if (c === '}')
|
|
43
|
+
braceDepth = Math.max(0, braceDepth - 1);
|
|
44
|
+
else if (c === '(')
|
|
45
|
+
parenDepth++;
|
|
46
|
+
else if (c === ')')
|
|
47
|
+
parenDepth = Math.max(0, parenDepth - 1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
for (const d of declarations) {
|
|
51
|
+
// Look for reassignments anywhere after the declaration line.
|
|
52
|
+
const after = lines.slice(d.line).join('\n');
|
|
53
|
+
const re = new RegExp(`(?<![\\.\\w])${escapeForRegex(d.name)}\\s*(?:=(?!=)|\\+=|-=|\\*=|/=|\\?\\?=|\\|\\|=|&&=)`, 'g');
|
|
54
|
+
const matches = [...after.matchAll(re)];
|
|
55
|
+
if (matches.length === 0)
|
|
56
|
+
continue;
|
|
57
|
+
if (hasIgnoreAnnotation(raw, d.line, 'mutable-globals'))
|
|
58
|
+
continue;
|
|
59
|
+
out.push({
|
|
60
|
+
detectorId: 'mutable-globals',
|
|
61
|
+
severity: 'medium',
|
|
62
|
+
confidence: 0.8,
|
|
63
|
+
layer: 1,
|
|
64
|
+
title: `Mutable top-level binding: \`${d.name}\` in ${file}:${d.line}`,
|
|
65
|
+
description: `\`${d.name}\` is declared with \`let\`/\`var\` at module scope and reassigned later. Module-scope mutation is shared by every importer — common cause of cross-test pollution, hidden state in SSR, and bugs that only appear after the second request.`,
|
|
66
|
+
evidence: [
|
|
67
|
+
{
|
|
68
|
+
file,
|
|
69
|
+
range: { startLine: d.line, endLine: d.line },
|
|
70
|
+
snippet: snippetAround(raw, d.line),
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
suggestion: 'Encapsulate the state in a class / closure / module factory so each consumer gets its own copy, or move to `const` if no mutation is actually needed.',
|
|
74
|
+
fingerprint: `mutable-globals:${stableHash(`${file}:${d.name}`)}`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
function isAnalysable(file) {
|
|
80
|
+
const posix = file.replace(/\\/g, '/');
|
|
81
|
+
return /\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(posix)
|
|
82
|
+
&& !/\.d\.ts$/.test(posix)
|
|
83
|
+
&& !/(^|\/)node_modules\//.test(posix)
|
|
84
|
+
&& !/(?:^|\/)__tests__\//.test(posix)
|
|
85
|
+
&& !/(?:^|\/)tests?\//.test(posix)
|
|
86
|
+
&& !/\.test\.(?:ts|tsx|js|jsx)$/.test(posix)
|
|
87
|
+
&& !/\.spec\.(?:ts|tsx|js|jsx)$/.test(posix);
|
|
88
|
+
}
|
|
89
|
+
function snippetAround(raw, line) {
|
|
90
|
+
const lines = raw.split('\n');
|
|
91
|
+
const from = Math.max(0, line - 1);
|
|
92
|
+
const to = Math.min(lines.length, line + 1);
|
|
93
|
+
return lines.slice(from, to).join('\n');
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=mutable-globals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutable-globals.js","sourceRoot":"","sources":["../../src/detectors/mutable-globals.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Finding } from '../types.js';
|
|
2
|
+
import type { FileWalkingDetectorInput } from '../types/detector-input.js';
|
|
3
|
+
export interface MutationDetectorInput extends FileWalkingDetectorInput {
|
|
4
|
+
}
|
|
5
|
+
export declare function detectMutations(input: MutationDetectorInput): Finding[];
|
|
6
|
+
/**
|
|
7
|
+
* Compress the enclosing function source so the LLM prompt stays in budget.
|
|
8
|
+
* We keep the full signature line plus up to ~40 lines of body — enough to
|
|
9
|
+
* judge intent without paying for the model to read a 200-line method.
|
|
10
|
+
*/
|
|
11
|
+
//# sourceMappingURL=mutation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutation.d.ts","sourceRoot":"","sources":["../../src/detectors/mutation.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,OAAO,EAAY,MAAM,aAAa,CAAC;AACrD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAE3E,MAAM,WAAW,qBAAsB,SAAQ,wBAAwB;CAAG;AAiE1E,wBAAgB,eAAe,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,EAAE,CAsCvE;AAkUD;;;;GAIG"}
|