circle-ir 3.12.1 → 3.14.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 (30) hide show
  1. package/dist/analysis/passes/blocking-main-thread-pass.d.ts +40 -0
  2. package/dist/analysis/passes/blocking-main-thread-pass.js +112 -0
  3. package/dist/analysis/passes/blocking-main-thread-pass.js.map +1 -0
  4. package/dist/analysis/passes/excessive-allocation-pass.d.ts +29 -0
  5. package/dist/analysis/passes/excessive-allocation-pass.js +85 -0
  6. package/dist/analysis/passes/excessive-allocation-pass.js.map +1 -0
  7. package/dist/analysis/passes/feature-envy-pass.d.ts +54 -0
  8. package/dist/analysis/passes/feature-envy-pass.js +132 -0
  9. package/dist/analysis/passes/feature-envy-pass.js.map +1 -0
  10. package/dist/analysis/passes/god-class-pass.d.ts +58 -0
  11. package/dist/analysis/passes/god-class-pass.js +197 -0
  12. package/dist/analysis/passes/god-class-pass.js.map +1 -0
  13. package/dist/analysis/passes/missing-guard-dom-pass.d.ts +18 -0
  14. package/dist/analysis/passes/missing-guard-dom-pass.js +18 -0
  15. package/dist/analysis/passes/missing-guard-dom-pass.js.map +1 -1
  16. package/dist/analysis/passes/missing-stream-pass.d.ts +28 -0
  17. package/dist/analysis/passes/missing-stream-pass.js +173 -0
  18. package/dist/analysis/passes/missing-stream-pass.js.map +1 -0
  19. package/dist/analysis/passes/naming-convention-pass.d.ts +62 -0
  20. package/dist/analysis/passes/naming-convention-pass.js +169 -0
  21. package/dist/analysis/passes/naming-convention-pass.js.map +1 -0
  22. package/dist/analysis/passes/serial-await-pass.js +3 -2
  23. package/dist/analysis/passes/serial-await-pass.js.map +1 -1
  24. package/dist/analyzer.d.ts +28 -12
  25. package/dist/analyzer.js +30 -14
  26. package/dist/analyzer.js.map +1 -1
  27. package/dist/browser/circle-ir.js +635 -91
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.js.map +1 -1
  30. package/package.json +1 -1
@@ -20377,11 +20377,11 @@ var SerialAwaitPass = class {
20377
20377
  cwe: void 0,
20378
20378
  severity: "low",
20379
20379
  level: "note",
20380
- message: `Serial awaits: \`${expr1}\` (line ${a1.line}) and \`${expr2}\` (line ${a2.line}) have no data dependency; consider using Promise.all()`,
20380
+ message: `Serial awaits: \`${expr1}\` (line ${a1.line}) and \`${expr2}\` (line ${a2.line}) appear to have no data dependency \u2014 verify ordering requirements before parallelising`,
20381
20381
  file,
20382
20382
  line: a1.line,
20383
20383
  end_line: a2.line,
20384
- fix: `const [result1, result2] = await Promise.all([operation1, operation2]);`,
20384
+ fix: `If the operations are truly independent and have no ordering constraints, consider: const [result1, result2] = await Promise.all([operation1, operation2]);`,
20385
20385
  evidence: {
20386
20386
  first_await_line: a1.line,
20387
20387
  second_await_line: a2.line,
@@ -20914,94 +20914,6 @@ var UseAfterClosePass = class {
20914
20914
  }
20915
20915
  };
20916
20916
 
20917
- // src/analysis/passes/missing-guard-dom-pass.ts
20918
- var AUTH_METHODS = /* @__PURE__ */ new Set([
20919
- "authenticate",
20920
- "isAuthenticated",
20921
- "isAuthorized",
20922
- "isAdmin",
20923
- "checkAuth",
20924
- "hasPermission",
20925
- "requiresAuth",
20926
- "verifyToken",
20927
- "validateToken",
20928
- "checkRole",
20929
- "authorize",
20930
- "isLoggedIn"
20931
- ]);
20932
- var SENSITIVE_METHODS = /* @__PURE__ */ new Set([
20933
- "delete",
20934
- "deleteById",
20935
- "drop",
20936
- "truncate",
20937
- "executeUpdate",
20938
- "createUser",
20939
- "createAdmin",
20940
- "modifyPermission",
20941
- "grantRole",
20942
- "setAdmin",
20943
- "elevatePrivilege"
20944
- ]);
20945
- var MissingGuardDomPass = class {
20946
- name = "missing-guard-dom";
20947
- category = "security";
20948
- run(ctx) {
20949
- const { graph, language } = ctx;
20950
- if (language !== "java") return { findings: 0 };
20951
- const { cfg, calls } = graph.ir;
20952
- if (cfg.blocks.length === 0 || cfg.edges.length === 0) return { findings: 0 };
20953
- const dom = new DominatorGraph(cfg);
20954
- const file = graph.ir.meta.file;
20955
- const authCallLines = [];
20956
- const sensitiveOps = [];
20957
- for (const call of calls) {
20958
- if (AUTH_METHODS.has(call.method_name)) {
20959
- authCallLines.push(call.location.line);
20960
- }
20961
- if (SENSITIVE_METHODS.has(call.method_name)) {
20962
- sensitiveOps.push({ line: call.location.line, method: call.method_name });
20963
- }
20964
- }
20965
- if (sensitiveOps.length === 0) return { findings: 0 };
20966
- const blockContainingLine = (line) => cfg.blocks.find((b) => b.start_line <= line && line <= b.end_line) ?? null;
20967
- const reportedMethods = /* @__PURE__ */ new Set();
20968
- let count = 0;
20969
- for (const op of sensitiveOps) {
20970
- const opBlock = blockContainingLine(op.line);
20971
- if (!opBlock) continue;
20972
- const methodInfo = graph.methodAtLine(op.line);
20973
- if (!methodInfo) continue;
20974
- const methodKey = `${methodInfo.type.name}::${methodInfo.method.name}`;
20975
- if (reportedMethods.has(methodKey)) continue;
20976
- const { start_line, end_line } = methodInfo.method;
20977
- const authInMethod = authCallLines.filter((l) => l >= start_line && l <= end_line);
20978
- const dominated = authInMethod.some((authLine) => {
20979
- const authBlock = blockContainingLine(authLine);
20980
- return authBlock !== null && dom.dominates(authBlock.id, opBlock.id);
20981
- });
20982
- if (!dominated) {
20983
- reportedMethods.add(methodKey);
20984
- count++;
20985
- ctx.addFinding({
20986
- id: `missing-guard-dom-${file}-${op.line}`,
20987
- pass: this.name,
20988
- category: this.category,
20989
- rule_id: "missing-guard-dom",
20990
- cwe: "CWE-285",
20991
- severity: "high",
20992
- level: "error",
20993
- message: `Sensitive operation \`${op.method}()\` at line ${op.line} is not dominated by an authentication check`,
20994
- file,
20995
- line: op.line,
20996
- fix: `Add authentication/authorization check on all paths leading to line ${op.line}`,
20997
- evidence: { method: op.method }
20998
- });
20999
- }
21000
- }
21001
- return { findings: count };
21002
- }
21003
- };
21004
-
21005
20917
  // src/analysis/passes/cleanup-verify-pass.ts
21006
20918
  var RESOURCE_CTORS4 = /* @__PURE__ */ new Set([
21007
20919
  "FileInputStream",
@@ -21233,6 +21145,638 @@ var UnusedInterfaceMethodPass = class {
21233
21145
  }
21234
21146
  };
21235
21147
 
21148
+ // src/analysis/passes/blocking-main-thread-pass.ts
21149
+ var HTTP_DECORATORS = /* @__PURE__ */ new Set([
21150
+ "Get",
21151
+ "Post",
21152
+ "Put",
21153
+ "Patch",
21154
+ "Delete",
21155
+ "All",
21156
+ "Options",
21157
+ "Head",
21158
+ "Route",
21159
+ "Handler"
21160
+ ]);
21161
+ var HANDLER_PARAM_NAMES = /* @__PURE__ */ new Set([
21162
+ "req",
21163
+ "res",
21164
+ "request",
21165
+ "response",
21166
+ "ctx",
21167
+ "c",
21168
+ "event"
21169
+ ]);
21170
+ var HANDLER_METHOD_NAMES = /* @__PURE__ */ new Set([
21171
+ "handle",
21172
+ "handler",
21173
+ "dispatch",
21174
+ "invoke",
21175
+ "serve"
21176
+ ]);
21177
+ var CRYPTO_BLOCKING_METHODS = /* @__PURE__ */ new Set([
21178
+ "createHash",
21179
+ "hashSync",
21180
+ "pbkdf2Sync",
21181
+ "scryptSync",
21182
+ "generateKeyPairSync",
21183
+ "generateKeySync",
21184
+ "deriveKeySync"
21185
+ ]);
21186
+ var SYNC_SUFFIX_RE2 = /Sync$/;
21187
+ var BlockingMainThreadPass = class {
21188
+ name = "blocking-main-thread";
21189
+ category = "performance";
21190
+ run(ctx) {
21191
+ const { graph, language } = ctx;
21192
+ if (language !== "javascript" && language !== "typescript") {
21193
+ return { blockingInHandlers: [] };
21194
+ }
21195
+ const file = graph.ir.meta.file;
21196
+ const handlerRanges = [];
21197
+ for (const type of graph.ir.types) {
21198
+ for (const method of type.methods) {
21199
+ if (this.isRequestHandler(method)) {
21200
+ handlerRanges.push({
21201
+ start: method.start_line,
21202
+ end: method.end_line,
21203
+ name: method.name
21204
+ });
21205
+ }
21206
+ }
21207
+ }
21208
+ if (handlerRanges.length === 0) return { blockingInHandlers: [] };
21209
+ const blockingInHandlers = [];
21210
+ for (const call of graph.ir.calls) {
21211
+ const name2 = call.method_name;
21212
+ const isCrypto = CRYPTO_BLOCKING_METHODS.has(name2);
21213
+ const isSyncSuffix = SYNC_SUFFIX_RE2.test(name2);
21214
+ if (!isCrypto && !isSyncSuffix) continue;
21215
+ const line = call.location.line;
21216
+ const range = handlerRanges.find((r) => line >= r.start && line <= r.end);
21217
+ if (!range) continue;
21218
+ const reason = isCrypto ? "crypto" : "sync-suffix";
21219
+ blockingInHandlers.push({ line, method: name2, handler: range.name, reason });
21220
+ ctx.addFinding({
21221
+ id: `blocking-main-thread-${file}-${line}`,
21222
+ pass: this.name,
21223
+ category: this.category,
21224
+ rule_id: this.name,
21225
+ cwe: "CWE-1050",
21226
+ severity: "medium",
21227
+ level: "warning",
21228
+ message: `Blocking call \`${name2}()\` inside request handler '${range.name}' stalls the event loop under concurrent load`,
21229
+ file,
21230
+ line,
21231
+ fix: "Move to an async equivalent or offload to a worker thread",
21232
+ evidence: { handler: range.name, blocking_method: name2, reason }
21233
+ });
21234
+ }
21235
+ return { blockingInHandlers };
21236
+ }
21237
+ isRequestHandler(method) {
21238
+ if (method.annotations.some((a) => HTTP_DECORATORS.has(a))) return true;
21239
+ if (HANDLER_METHOD_NAMES.has(method.name)) return true;
21240
+ const paramNames = method.parameters.map((p) => p.name.toLowerCase());
21241
+ return paramNames.some((n) => HANDLER_PARAM_NAMES.has(n));
21242
+ }
21243
+ };
21244
+
21245
+ // src/analysis/passes/excessive-allocation-pass.ts
21246
+ var ALLOC_PATTERNS = {
21247
+ javascript: /\bnew\s+(Array|Map|Set|Object|WeakMap|WeakSet|Error|RegExp|Date|Buffer|Uint8Array|Int8Array|Float32Array|ArrayBuffer)\s*[(<]|\bArray\.from\s*\(|\bstructuredClone\s*\(|\bObject\.create\s*\(/,
21248
+ typescript: /\bnew\s+(Array|Map|Set|Object|WeakMap|WeakSet|Error|RegExp|Date|Buffer|Uint8Array|Int8Array|Float32Array|ArrayBuffer)\s*[(<]|\bArray\.from\s*\(|\bstructuredClone\s*\(|\bObject\.create\s*\(/,
21249
+ java: /\bnew\s+(ArrayList|HashMap|HashSet|LinkedList|TreeMap|TreeSet|PriorityQueue|ArrayDeque|StringBuilder|StringBuffer|CopyOnWriteArrayList|ConcurrentHashMap)\s*[(<]|\bnew\s+\w[\w.<>]*\[\s*[a-zA-Z]\w*/,
21250
+ python: /\b(list|dict|set|tuple|bytearray|defaultdict|OrderedDict|Counter|deque)\s*\(\s*\)|\[\s*\]|\{\s*\}(?!\s*[}\]])/,
21251
+ rust: /\b(Vec|HashMap|HashSet|BTreeMap|BTreeSet|VecDeque|LinkedList|String|Box|Rc|Arc)\s*::\s*new\s*\(/
21252
+ };
21253
+ var BENIGN_RE = /\bpool\b|\bcache\b|\breuse\b|\bpreallocat|\brecycl/i;
21254
+ var ExcessiveAllocationPass = class {
21255
+ name = "excessive-allocation";
21256
+ category = "performance";
21257
+ run(ctx) {
21258
+ const { graph, code, language } = ctx;
21259
+ if (language === "bash") {
21260
+ return { allocationsInLoops: [] };
21261
+ }
21262
+ const pattern = ALLOC_PATTERNS[language];
21263
+ if (!pattern) return { allocationsInLoops: [] };
21264
+ const loops = graph.loopBodies();
21265
+ if (loops.length === 0) return { allocationsInLoops: [] };
21266
+ const file = graph.ir.meta.file;
21267
+ const codeLines = code.split("\n");
21268
+ const allocationsInLoops = [];
21269
+ const reported = /* @__PURE__ */ new Set();
21270
+ for (const loop of loops) {
21271
+ for (let ln = loop.start_line; ln <= loop.end_line; ln++) {
21272
+ if (reported.has(ln)) continue;
21273
+ const src = codeLines[ln - 1] ?? "";
21274
+ const match = pattern.exec(src);
21275
+ if (!match) continue;
21276
+ if (BENIGN_RE.test(src)) continue;
21277
+ const allocLabel = match[0].replace(/\s+/g, " ").trim();
21278
+ allocationsInLoops.push({ line: ln, pattern: allocLabel });
21279
+ reported.add(ln);
21280
+ ctx.addFinding({
21281
+ id: `excessive-allocation-${file}-${ln}`,
21282
+ pass: this.name,
21283
+ category: this.category,
21284
+ rule_id: this.name,
21285
+ cwe: "CWE-770",
21286
+ severity: "medium",
21287
+ level: "warning",
21288
+ message: `Repeated allocation inside loop (lines ${loop.start_line}\u2013${loop.end_line}): \`${allocLabel}\` creates GC pressure on every iteration`,
21289
+ file,
21290
+ line: ln,
21291
+ snippet: src.trim(),
21292
+ fix: "Pre-allocate outside the loop and reset/reuse the collection each iteration",
21293
+ evidence: {
21294
+ allocation: allocLabel,
21295
+ loop_start: loop.start_line,
21296
+ loop_end: loop.end_line
21297
+ }
21298
+ });
21299
+ }
21300
+ }
21301
+ return { allocationsInLoops };
21302
+ }
21303
+ };
21304
+
21305
+ // src/analysis/passes/missing-stream-pass.ts
21306
+ var JS_WHOLE_LOAD_RE = /\b(?:readFileSync|fs\.readFile\b|response\.text\b|response\.json\b|res\.text\b|res\.json\b|body\.text\b|body\.json\b)\s*\(/;
21307
+ var JS_STREAM_RE = /\.pipe\s*\(|\.on\s*\(\s*['"]data['"]|for\s+await\s*\(|\bcreateReadStream\b|\bstream\b/i;
21308
+ var JAVA_WHOLE_READ_RE = /\bFiles\.readAllBytes\s*\(|\bFiles\.readAllLines\s*\(|\bFiles\.readString\s*\(|\bnew\s+BufferedReader\s*\(|\bFileInputStream\b/;
21309
+ var PYTHON_WHOLE_READ_RE = /\.\s*read\s*\(\s*\)/;
21310
+ var MissingStreamPass = class {
21311
+ name = "missing-stream";
21312
+ category = "performance";
21313
+ run(ctx) {
21314
+ const { graph, code, language } = ctx;
21315
+ if (language === "bash" || language === "rust") {
21316
+ return { wholeFileReads: [] };
21317
+ }
21318
+ const file = graph.ir.meta.file;
21319
+ const codeLines = code.split("\n");
21320
+ const wholeFileReads = [];
21321
+ const reported = /* @__PURE__ */ new Set();
21322
+ if (language === "javascript" || language === "typescript") {
21323
+ for (const type of graph.ir.types) {
21324
+ for (const method of type.methods) {
21325
+ const start2 = method.start_line;
21326
+ const end = method.end_line;
21327
+ const methodSrc = codeLines.slice(start2 - 1, end).join("\n");
21328
+ if (JS_STREAM_RE.test(methodSrc)) continue;
21329
+ for (let i2 = start2 - 1; i2 < end && i2 < codeLines.length; i2++) {
21330
+ const ln = i2 + 1;
21331
+ if (reported.has(ln)) continue;
21332
+ const src = codeLines[i2];
21333
+ const match = JS_WHOLE_LOAD_RE.exec(src);
21334
+ if (!match) continue;
21335
+ const methodName = match[0].replace(/\s*\(.*/, "").trim();
21336
+ wholeFileReads.push({ line: ln, method: methodName });
21337
+ reported.add(ln);
21338
+ ctx.addFinding({
21339
+ id: `missing-stream-${file}-${ln}`,
21340
+ pass: this.name,
21341
+ category: this.category,
21342
+ rule_id: this.name,
21343
+ severity: "low",
21344
+ level: "note",
21345
+ message: `\`${methodName}()\` loads the entire file/response into memory. Use a streaming API for large payloads.`,
21346
+ file,
21347
+ line: ln,
21348
+ snippet: src.trim(),
21349
+ fix: "Replace with fs.createReadStream / response.body (async iterator) to process data in chunks",
21350
+ evidence: { method: methodName }
21351
+ });
21352
+ }
21353
+ }
21354
+ }
21355
+ if (graph.ir.types.length === 0) {
21356
+ if (!JS_STREAM_RE.test(code)) {
21357
+ for (let i2 = 0; i2 < codeLines.length; i2++) {
21358
+ const ln = i2 + 1;
21359
+ if (reported.has(ln)) continue;
21360
+ const src = codeLines[i2];
21361
+ const match = JS_WHOLE_LOAD_RE.exec(src);
21362
+ if (!match) continue;
21363
+ const methodName = match[0].replace(/\s*\(.*/, "").trim();
21364
+ wholeFileReads.push({ line: ln, method: methodName });
21365
+ reported.add(ln);
21366
+ ctx.addFinding({
21367
+ id: `missing-stream-${file}-${ln}`,
21368
+ pass: this.name,
21369
+ category: this.category,
21370
+ rule_id: this.name,
21371
+ severity: "low",
21372
+ level: "note",
21373
+ message: `\`${methodName}()\` loads the entire file/response into memory. Use a streaming API for large payloads.`,
21374
+ file,
21375
+ line: ln,
21376
+ snippet: src.trim(),
21377
+ fix: "Replace with fs.createReadStream / response.body (async iterator) to process data in chunks",
21378
+ evidence: { method: methodName }
21379
+ });
21380
+ }
21381
+ }
21382
+ }
21383
+ } else if (language === "java") {
21384
+ for (let i2 = 0; i2 < codeLines.length; i2++) {
21385
+ const ln = i2 + 1;
21386
+ if (reported.has(ln)) continue;
21387
+ const src = codeLines[i2];
21388
+ const match = JAVA_WHOLE_READ_RE.exec(src);
21389
+ if (!match) continue;
21390
+ const matchText = match[0].replace(/\s*\(.*/, "").trim();
21391
+ wholeFileReads.push({ line: ln, method: matchText });
21392
+ reported.add(ln);
21393
+ ctx.addFinding({
21394
+ id: `missing-stream-${file}-${ln}`,
21395
+ pass: this.name,
21396
+ category: this.category,
21397
+ rule_id: this.name,
21398
+ severity: "low",
21399
+ level: "note",
21400
+ message: `Whole-file read at line ${ln}: \`${matchText}\` loads the entire file into memory. Consider NIO Channels or InputStream for large files.`,
21401
+ file,
21402
+ line: ln,
21403
+ snippet: src.trim(),
21404
+ fix: "Use Files.lines() for line streaming, or InputStream / NIO channels for byte streaming",
21405
+ evidence: { method: matchText }
21406
+ });
21407
+ }
21408
+ } else if (language === "python") {
21409
+ for (let i2 = 0; i2 < codeLines.length; i2++) {
21410
+ const ln = i2 + 1;
21411
+ if (reported.has(ln)) continue;
21412
+ const src = codeLines[i2];
21413
+ if (!PYTHON_WHOLE_READ_RE.test(src)) continue;
21414
+ if (/^\s*#/.test(src)) continue;
21415
+ wholeFileReads.push({ line: ln, method: "read" });
21416
+ reported.add(ln);
21417
+ ctx.addFinding({
21418
+ id: `missing-stream-${file}-${ln}`,
21419
+ pass: this.name,
21420
+ category: this.category,
21421
+ rule_id: this.name,
21422
+ severity: "low",
21423
+ level: "note",
21424
+ message: `\`.read()\` loads the entire file into memory. Iterate over the file object instead for line-by-line streaming.`,
21425
+ file,
21426
+ line: ln,
21427
+ snippet: src.trim(),
21428
+ fix: "Iterate the file object: `for line in f:` instead of `data = f.read()`",
21429
+ evidence: { method: "read" }
21430
+ });
21431
+ }
21432
+ }
21433
+ return { wholeFileReads };
21434
+ }
21435
+ };
21436
+
21437
+ // src/analysis/passes/god-class-pass.ts
21438
+ var WMC_THRESHOLD = 47;
21439
+ var LCOM2_THRESHOLD = 0.8;
21440
+ var CBO_THRESHOLD = 14;
21441
+ var PRIMITIVES = /* @__PURE__ */ new Set([
21442
+ "void",
21443
+ "boolean",
21444
+ "byte",
21445
+ "short",
21446
+ "int",
21447
+ "long",
21448
+ "float",
21449
+ "double",
21450
+ "char",
21451
+ "string",
21452
+ "number",
21453
+ "boolean",
21454
+ "object",
21455
+ "any",
21456
+ "never",
21457
+ "unknown",
21458
+ "String",
21459
+ "Integer",
21460
+ "Long",
21461
+ "Double",
21462
+ "Boolean",
21463
+ "Object",
21464
+ "Number",
21465
+ "null",
21466
+ "undefined"
21467
+ ]);
21468
+ var GodClassPass = class {
21469
+ name = "god-class";
21470
+ category = "architecture";
21471
+ run(ctx) {
21472
+ const { graph, language } = ctx;
21473
+ if (language === "bash" || language === "rust") {
21474
+ return { godClasses: [] };
21475
+ }
21476
+ const file = graph.ir.meta.file;
21477
+ const godClasses = [];
21478
+ for (const type of graph.ir.types) {
21479
+ if (type.kind !== "class") continue;
21480
+ if (type.methods.length < 2) continue;
21481
+ const wmc = this.computeWMC(graph.ir.cfg.blocks, graph.ir.cfg.edges, type);
21482
+ const lcom2 = this.computeLCOM2(graph.ir.dfg, type);
21483
+ const cbo = this.computeCBO(graph.ir.calls, type);
21484
+ const violations = [
21485
+ wmc > WMC_THRESHOLD,
21486
+ lcom2 > LCOM2_THRESHOLD,
21487
+ cbo > CBO_THRESHOLD
21488
+ ].filter(Boolean).length;
21489
+ if (violations < 2) continue;
21490
+ godClasses.push({ className: type.name, line: type.start_line, wmc, lcom2, cbo });
21491
+ const lcom2Str = lcom2.toFixed(2);
21492
+ ctx.addFinding({
21493
+ id: `god-class-${file}-${type.start_line}`,
21494
+ pass: this.name,
21495
+ category: this.category,
21496
+ rule_id: this.name,
21497
+ cwe: "CWE-1060",
21498
+ severity: "medium",
21499
+ level: "warning",
21500
+ message: `God class detected: \`${type.name}\` exceeds ${violations}/3 thresholds (WMC=${wmc}, LCOM2=${lcom2Str}, CBO=${cbo})`,
21501
+ file,
21502
+ line: type.start_line,
21503
+ fix: "Break into focused classes: extract cohesive method groups into separate types. Apply Single Responsibility Principle.",
21504
+ evidence: { wmc, lcom2: parseFloat(lcom2Str), cbo }
21505
+ });
21506
+ }
21507
+ return { godClasses };
21508
+ }
21509
+ /** Compute WMC = Σ v(G) for all methods. v(G) = edges − nodes + 2. */
21510
+ computeWMC(blocks, edges, type) {
21511
+ let wmc = 0;
21512
+ for (const method of type.methods) {
21513
+ const methodBlockIds = new Set(
21514
+ blocks.filter((b) => b.start_line >= method.start_line && b.end_line <= method.end_line).map((b) => b.id)
21515
+ );
21516
+ if (methodBlockIds.size === 0) {
21517
+ wmc += 1;
21518
+ continue;
21519
+ }
21520
+ const methodEdges = edges.filter(
21521
+ (e2) => methodBlockIds.has(e2.from) && methodBlockIds.has(e2.to)
21522
+ );
21523
+ const n = methodBlockIds.size;
21524
+ const e = methodEdges.length;
21525
+ const vG = Math.max(1, e - n + 2);
21526
+ wmc += vG;
21527
+ }
21528
+ return wmc;
21529
+ }
21530
+ /**
21531
+ * Compute LCOM2 = (P − Q) / max(1, m*(m−1)/2) clamped to [0, 1].
21532
+ * Uses DFG variable names intersected with declared field names.
21533
+ */
21534
+ computeLCOM2(dfg, type) {
21535
+ const m = type.methods.length;
21536
+ if (m < 2) return 0;
21537
+ const fieldNames = new Set(type.fields.map((f) => f.name));
21538
+ if (fieldNames.size === 0) return 0;
21539
+ const methodFields = type.methods.map((method) => {
21540
+ const accessed = /* @__PURE__ */ new Set();
21541
+ const start2 = method.start_line;
21542
+ const end = method.end_line;
21543
+ for (const def of dfg.defs) {
21544
+ if (def.line >= start2 && def.line <= end && fieldNames.has(def.variable)) {
21545
+ accessed.add(def.variable);
21546
+ }
21547
+ }
21548
+ for (const use of dfg.uses) {
21549
+ if (use.line >= start2 && use.line <= end && fieldNames.has(use.variable)) {
21550
+ accessed.add(use.variable);
21551
+ }
21552
+ }
21553
+ return accessed;
21554
+ });
21555
+ let P = 0;
21556
+ let Q = 0;
21557
+ for (let i2 = 0; i2 < m; i2++) {
21558
+ for (let j = i2 + 1; j < m; j++) {
21559
+ const shared = [...methodFields[i2]].some((f) => methodFields[j].has(f));
21560
+ if (shared) {
21561
+ Q++;
21562
+ } else {
21563
+ P++;
21564
+ }
21565
+ }
21566
+ }
21567
+ const total = m * (m - 1) / 2;
21568
+ const raw = (P - Q) / Math.max(1, total);
21569
+ return Math.min(1, Math.max(0, raw));
21570
+ }
21571
+ /**
21572
+ * Compute CBO = count of distinct external type names referenced in the class.
21573
+ * Sources: call receiver_type, method parameter types, field types.
21574
+ */
21575
+ computeCBO(calls, type) {
21576
+ const externalTypes = /* @__PURE__ */ new Set();
21577
+ const ownName = type.name.toLowerCase();
21578
+ const addType = (t) => {
21579
+ if (!t) return;
21580
+ const base = t.replace(/<.*>/, "").replace(/\[\]/g, "").replace(/\?/g, "").trim();
21581
+ if (!base) return;
21582
+ if (PRIMITIVES.has(base)) return;
21583
+ if (base.toLowerCase() === ownName) return;
21584
+ externalTypes.add(base);
21585
+ };
21586
+ for (const field of type.fields) {
21587
+ addType(field.type);
21588
+ }
21589
+ for (const method of type.methods) {
21590
+ for (const param of method.parameters) {
21591
+ addType(param.type);
21592
+ }
21593
+ }
21594
+ const classStart = type.methods.reduce(
21595
+ (mn, m) => Math.min(mn, m.start_line),
21596
+ Infinity
21597
+ );
21598
+ const classEnd = type.methods.reduce(
21599
+ (mx, m) => Math.max(mx, m.end_line),
21600
+ 0
21601
+ );
21602
+ for (const call of calls) {
21603
+ const ln = call.location.line;
21604
+ if (ln >= classStart && ln <= classEnd) {
21605
+ addType(call.receiver_type ?? null);
21606
+ }
21607
+ }
21608
+ return externalTypes.size;
21609
+ }
21610
+ };
21611
+
21612
+ // src/analysis/passes/naming-convention-pass.ts
21613
+ var PASCAL_CASE_RE = /^[A-Z][A-Za-z0-9]*$/;
21614
+ var CAMEL_CASE_RE = /^[a-z][a-zA-Z0-9]*$/;
21615
+ var SNAKE_CASE_RE = /^[a-z_][a-z0-9_]*$/;
21616
+ var UPPER_SNAKE_RE = /^[A-Z][A-Z0-9_]*$/;
21617
+ var I_PREFIX_RE = /^I[A-Z]/;
21618
+ var DUNDER_RE = /^__\w+__$/;
21619
+ var GENERIC_NAMES = /* @__PURE__ */ new Set(["T", "K", "V", "E", "R", "N", "S", "U", "W"]);
21620
+ var EXEMPT_METHODS = /* @__PURE__ */ new Set([
21621
+ "main",
21622
+ "toString",
21623
+ "hashCode",
21624
+ "equals",
21625
+ "compareTo",
21626
+ "valueOf",
21627
+ "of",
21628
+ "from",
21629
+ "create",
21630
+ "build",
21631
+ "get",
21632
+ "set",
21633
+ "is",
21634
+ "has"
21635
+ ]);
21636
+ var MAX_FINDINGS = 20;
21637
+ function shouldSkipName(name2) {
21638
+ if (name2.length <= 2) return true;
21639
+ if (name2.startsWith("_") || name2.startsWith("$")) return true;
21640
+ if (DUNDER_RE.test(name2)) return true;
21641
+ if (GENERIC_NAMES.has(name2)) return true;
21642
+ return false;
21643
+ }
21644
+ var NamingConventionPass = class {
21645
+ name = "naming-convention";
21646
+ category = "maintainability";
21647
+ enforceIPrefix;
21648
+ constructor(options = {}) {
21649
+ this.enforceIPrefix = options.enforceIPrefix ?? false;
21650
+ }
21651
+ run(ctx) {
21652
+ const { graph, language } = ctx;
21653
+ const file = graph.ir.meta.file;
21654
+ const violations = [];
21655
+ let findingCount = 0;
21656
+ const addViolation = (entity, name2, line, expected, message) => {
21657
+ if (findingCount >= MAX_FINDINGS) return;
21658
+ violations.push({ entity, name: name2, line, expected, actual: name2 });
21659
+ findingCount++;
21660
+ ctx.addFinding({
21661
+ id: `naming-convention-${file}-${line}-${name2}`,
21662
+ pass: this.name,
21663
+ category: this.category,
21664
+ rule_id: this.name,
21665
+ severity: "low",
21666
+ level: "note",
21667
+ message,
21668
+ file,
21669
+ line,
21670
+ fix: `Rename \`${name2}\` to follow ${expected} convention`,
21671
+ evidence: { name: name2, expected_convention: expected }
21672
+ });
21673
+ };
21674
+ for (const type of graph.ir.types) {
21675
+ if (findingCount >= MAX_FINDINGS) break;
21676
+ if (shouldSkipName(type.name)) continue;
21677
+ if (language === "java" || language === "typescript" || language === "javascript") {
21678
+ if (type.kind === "class" && !PASCAL_CASE_RE.test(type.name)) {
21679
+ addViolation(
21680
+ "class",
21681
+ type.name,
21682
+ type.start_line,
21683
+ "PascalCase",
21684
+ `Class \`${type.name}\` should be PascalCase`
21685
+ );
21686
+ }
21687
+ if (type.kind === "interface") {
21688
+ if (!PASCAL_CASE_RE.test(type.name)) {
21689
+ addViolation(
21690
+ "interface",
21691
+ type.name,
21692
+ type.start_line,
21693
+ "PascalCase",
21694
+ `Interface \`${type.name}\` should be PascalCase`
21695
+ );
21696
+ } else if (this.enforceIPrefix && I_PREFIX_RE.test(type.name)) {
21697
+ addViolation(
21698
+ "interface",
21699
+ type.name,
21700
+ type.start_line,
21701
+ "PascalCase (no I-prefix)",
21702
+ `Interface \`${type.name}\` uses I-prefix which is not idiomatic \u2014 prefer plain PascalCase`
21703
+ );
21704
+ }
21705
+ }
21706
+ for (const method of type.methods) {
21707
+ if (findingCount >= MAX_FINDINGS) break;
21708
+ if (shouldSkipName(method.name)) continue;
21709
+ if (EXEMPT_METHODS.has(method.name)) continue;
21710
+ if (!CAMEL_CASE_RE.test(method.name)) {
21711
+ addViolation(
21712
+ "method",
21713
+ method.name,
21714
+ method.start_line,
21715
+ "camelCase",
21716
+ `Method \`${type.name}.${method.name}()\` should be camelCase`
21717
+ );
21718
+ }
21719
+ }
21720
+ if (language === "java") {
21721
+ for (const field of type.fields) {
21722
+ if (findingCount >= MAX_FINDINGS) break;
21723
+ if (shouldSkipName(field.name)) continue;
21724
+ const isConstant = field.modifiers.includes("final") && field.modifiers.includes("static");
21725
+ if (isConstant && !UPPER_SNAKE_RE.test(field.name)) {
21726
+ addViolation(
21727
+ "field",
21728
+ field.name,
21729
+ type.start_line,
21730
+ "UPPER_SNAKE_CASE",
21731
+ `Static final field \`${field.name}\` should be UPPER_SNAKE_CASE`
21732
+ );
21733
+ }
21734
+ }
21735
+ }
21736
+ } else if (language === "python") {
21737
+ if (type.kind === "class" && !PASCAL_CASE_RE.test(type.name)) {
21738
+ addViolation(
21739
+ "class",
21740
+ type.name,
21741
+ type.start_line,
21742
+ "PascalCase",
21743
+ `Class \`${type.name}\` should be PascalCase`
21744
+ );
21745
+ }
21746
+ for (const method of type.methods) {
21747
+ if (findingCount >= MAX_FINDINGS) break;
21748
+ if (shouldSkipName(method.name)) continue;
21749
+ if (DUNDER_RE.test(method.name)) continue;
21750
+ if (!SNAKE_CASE_RE.test(method.name)) {
21751
+ addViolation(
21752
+ "method",
21753
+ method.name,
21754
+ method.start_line,
21755
+ "snake_case",
21756
+ `Method \`${type.name}.${method.name}()\` should be snake_case`
21757
+ );
21758
+ }
21759
+ }
21760
+ } else if (language === "bash" || language === "rust") {
21761
+ for (const method of type.methods) {
21762
+ if (findingCount >= MAX_FINDINGS) break;
21763
+ if (shouldSkipName(method.name)) continue;
21764
+ if (!SNAKE_CASE_RE.test(method.name)) {
21765
+ addViolation(
21766
+ "method",
21767
+ method.name,
21768
+ method.start_line,
21769
+ "snake_case",
21770
+ `Function \`${method.name}\` should be snake_case`
21771
+ );
21772
+ }
21773
+ }
21774
+ }
21775
+ }
21776
+ return { violations };
21777
+ }
21778
+ };
21779
+
21236
21780
  // src/analysis/metrics/passes/size-metrics-pass.ts
21237
21781
  var SizeMetricsPass = class {
21238
21782
  name = "size-metrics";
@@ -22032,7 +22576,7 @@ async function analyze(code, filePath, language, options = {}) {
22032
22576
  enriched: {}
22033
22577
  });
22034
22578
  const config = options.taintConfig ?? getDefaultConfig();
22035
- const { results, findings } = new AnalysisPipeline().add(new TaintMatcherPass()).add(new ConstantPropagationPass(tree)).add(new LanguageSourcesPass()).add(new SinkFilterPass()).add(new TaintPropagationPass()).add(new InterproceduralPass()).add(new DeadCodePass()).add(new MissingAwaitPass()).add(new NPlusOnePass()).add(new MissingPublicDocPass()).add(new TodoInProdPass()).add(new StringConcatLoopPass()).add(new SyncIoAsyncPass()).add(new UncheckedReturnPass()).add(new NullDerefPass()).add(new ResourceLeakPass()).add(new VariableShadowingPass()).add(new LeakedGlobalPass()).add(new UnusedVariablePass()).add(new DependencyFanOutPass()).add(new StaleDocRefPass()).add(new InfiniteLoopPass()).add(new DeepInheritancePass()).add(new RedundantLoopPass()).add(new UnboundedCollectionPass()).add(new SerialAwaitPass()).add(new ReactInlineJsxPass()).add(new SwallowedExceptionPass()).add(new BroadCatchPass()).add(new UnhandledExceptionPass()).add(new DoubleClosePass()).add(new UseAfterClosePass()).add(new MissingGuardDomPass()).add(new CleanupVerifyPass()).add(new MissingOverridePass()).add(new UnusedInterfaceMethodPass()).run(graph, code, language, config);
22579
+ const { results, findings } = new AnalysisPipeline().add(new TaintMatcherPass()).add(new ConstantPropagationPass(tree)).add(new LanguageSourcesPass()).add(new SinkFilterPass()).add(new TaintPropagationPass()).add(new InterproceduralPass()).add(new DeadCodePass()).add(new MissingAwaitPass()).add(new NPlusOnePass()).add(new MissingPublicDocPass()).add(new TodoInProdPass()).add(new StringConcatLoopPass()).add(new SyncIoAsyncPass()).add(new UncheckedReturnPass()).add(new NullDerefPass()).add(new ResourceLeakPass()).add(new VariableShadowingPass()).add(new LeakedGlobalPass()).add(new UnusedVariablePass()).add(new DependencyFanOutPass()).add(new StaleDocRefPass()).add(new InfiniteLoopPass()).add(new DeepInheritancePass()).add(new RedundantLoopPass()).add(new UnboundedCollectionPass()).add(new SerialAwaitPass()).add(new ReactInlineJsxPass()).add(new SwallowedExceptionPass()).add(new BroadCatchPass()).add(new UnhandledExceptionPass()).add(new DoubleClosePass()).add(new UseAfterClosePass()).add(new CleanupVerifyPass()).add(new MissingOverridePass()).add(new UnusedInterfaceMethodPass()).add(new BlockingMainThreadPass()).add(new ExcessiveAllocationPass()).add(new MissingStreamPass()).add(new GodClassPass()).add(new NamingConventionPass(options.passOptions?.namingConvention)).run(graph, code, language, config);
22036
22580
  const sinkFilter = results.get("sink-filter");
22037
22581
  const interProc = results.get("interprocedural");
22038
22582
  const taint = {