circle-ir 3.9.7 → 3.9.8

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 (32) hide show
  1. package/dist/analysis/passes/deep-inheritance-pass.d.ts +30 -0
  2. package/dist/analysis/passes/deep-inheritance-pass.js +82 -0
  3. package/dist/analysis/passes/deep-inheritance-pass.js.map +1 -0
  4. package/dist/analysis/passes/infinite-loop-pass.d.ts +31 -0
  5. package/dist/analysis/passes/infinite-loop-pass.js +126 -0
  6. package/dist/analysis/passes/infinite-loop-pass.js.map +1 -0
  7. package/dist/analysis/passes/react-inline-jsx-pass.d.ts +36 -0
  8. package/dist/analysis/passes/react-inline-jsx-pass.js +140 -0
  9. package/dist/analysis/passes/react-inline-jsx-pass.js.map +1 -0
  10. package/dist/analysis/passes/redundant-loop-pass.d.ts +30 -0
  11. package/dist/analysis/passes/redundant-loop-pass.js +146 -0
  12. package/dist/analysis/passes/redundant-loop-pass.js.map +1 -0
  13. package/dist/analysis/passes/serial-await-pass.d.ts +36 -0
  14. package/dist/analysis/passes/serial-await-pass.js +132 -0
  15. package/dist/analysis/passes/serial-await-pass.js.map +1 -0
  16. package/dist/analysis/passes/unbounded-collection-pass.d.ts +32 -0
  17. package/dist/analysis/passes/unbounded-collection-pass.js +128 -0
  18. package/dist/analysis/passes/unbounded-collection-pass.js.map +1 -0
  19. package/dist/analyzer.d.ts +7 -1
  20. package/dist/analyzer.js +19 -1
  21. package/dist/analyzer.js.map +1 -1
  22. package/dist/browser/circle-ir.js +541 -1
  23. package/dist/graph/dominator-graph.d.ts +53 -0
  24. package/dist/graph/dominator-graph.js +256 -0
  25. package/dist/graph/dominator-graph.js.map +1 -0
  26. package/dist/graph/index.d.ts +1 -0
  27. package/dist/graph/index.js +1 -0
  28. package/dist/graph/index.js.map +1 -1
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.js +2 -0
  31. package/dist/index.js.map +1 -1
  32. package/package.json +1 -1
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Pass #29: deep-inheritance (CWE-1086, category: architecture)
3
+ *
4
+ * Detects class inheritance chains deeper than the configured threshold
5
+ * (default: 5). Deep inheritance hierarchies increase coupling, make code
6
+ * harder to understand, and indicate a design smell (violation of composition
7
+ * over inheritance).
8
+ *
9
+ * Detection strategy:
10
+ * 1. Build a parentMap: className → parentName from `ir.types[*].extends`.
11
+ * 2. For each class with a known `start_line`, walk up the parentMap
12
+ * counting depth. Stop at depth 20 to defend against cycles.
13
+ * 3. If depth > THRESHOLD, emit a finding at the class `start_line`.
14
+ *
15
+ * Languages: Java, JavaScript/TypeScript, Python. Rust/Bash do not have
16
+ * class inheritance — skipped.
17
+ */
18
+ import type { AnalysisPass, PassContext } from '../../graph/analysis-pass.js';
19
+ export interface DeepInheritanceResult {
20
+ deepClasses: Array<{
21
+ className: string;
22
+ depth: number;
23
+ line: number;
24
+ }>;
25
+ }
26
+ export declare class DeepInheritancePass implements AnalysisPass<DeepInheritanceResult> {
27
+ readonly name = "deep-inheritance";
28
+ readonly category: "architecture";
29
+ run(ctx: PassContext): DeepInheritanceResult;
30
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Pass #29: deep-inheritance (CWE-1086, category: architecture)
3
+ *
4
+ * Detects class inheritance chains deeper than the configured threshold
5
+ * (default: 5). Deep inheritance hierarchies increase coupling, make code
6
+ * harder to understand, and indicate a design smell (violation of composition
7
+ * over inheritance).
8
+ *
9
+ * Detection strategy:
10
+ * 1. Build a parentMap: className → parentName from `ir.types[*].extends`.
11
+ * 2. For each class with a known `start_line`, walk up the parentMap
12
+ * counting depth. Stop at depth 20 to defend against cycles.
13
+ * 3. If depth > THRESHOLD, emit a finding at the class `start_line`.
14
+ *
15
+ * Languages: Java, JavaScript/TypeScript, Python. Rust/Bash do not have
16
+ * class inheritance — skipped.
17
+ */
18
+ const DEPTH_THRESHOLD = 5;
19
+ const CYCLE_GUARD = 20;
20
+ export class DeepInheritancePass {
21
+ name = 'deep-inheritance';
22
+ category = 'architecture';
23
+ run(ctx) {
24
+ const { graph, language } = ctx;
25
+ if (language === 'rust' || language === 'bash') {
26
+ return { deepClasses: [] };
27
+ }
28
+ const file = graph.ir.meta.file;
29
+ const types = graph.ir.types;
30
+ if (types.length === 0)
31
+ return { deepClasses: [] };
32
+ // Build parentMap: class name → parent class name
33
+ const parentMap = new Map();
34
+ for (const typeInfo of types) {
35
+ if (typeInfo.extends) {
36
+ // Strip generic parameters (e.g., "BaseRepo<User>" → "BaseRepo")
37
+ const parentName = typeInfo.extends.replace(/<.*>/, '').trim();
38
+ if (parentName) {
39
+ parentMap.set(typeInfo.name, parentName);
40
+ }
41
+ }
42
+ }
43
+ const deepClasses = [];
44
+ for (const typeInfo of types) {
45
+ if (typeInfo.kind !== 'class')
46
+ continue;
47
+ if (typeInfo.start_line <= 0)
48
+ continue;
49
+ // Walk up the inheritance chain
50
+ let depth = 0;
51
+ let current = parentMap.get(typeInfo.name);
52
+ const visited = new Set([typeInfo.name]);
53
+ while (current !== undefined && depth < CYCLE_GUARD) {
54
+ depth++;
55
+ if (visited.has(current))
56
+ break; // cycle guard
57
+ visited.add(current);
58
+ current = parentMap.get(current);
59
+ }
60
+ if (depth > DEPTH_THRESHOLD) {
61
+ deepClasses.push({ className: typeInfo.name, depth, line: typeInfo.start_line });
62
+ ctx.addFinding({
63
+ id: `deep-inheritance-${file}-${typeInfo.start_line}`,
64
+ pass: this.name,
65
+ category: this.category,
66
+ rule_id: this.name,
67
+ cwe: 'CWE-1086',
68
+ severity: 'low',
69
+ level: 'warning',
70
+ message: `Deep inheritance: class \`${typeInfo.name}\` has inheritance depth ${depth} (threshold: ${DEPTH_THRESHOLD})`,
71
+ file,
72
+ line: typeInfo.start_line,
73
+ fix: `Refactor to prefer composition over inheritance. ` +
74
+ `Consider extracting shared behaviour into interfaces or mixins.`,
75
+ evidence: { depth, threshold: DEPTH_THRESHOLD },
76
+ });
77
+ }
78
+ }
79
+ return { deepClasses };
80
+ }
81
+ }
82
+ //# sourceMappingURL=deep-inheritance-pass.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deep-inheritance-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/deep-inheritance-pass.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Pass #28: infinite-loop (CWE-835, category: reliability)
3
+ *
4
+ * Detects loops with no reachable exit edge — i.e., loops that can run
5
+ * forever because every execution path through the loop body leads back to
6
+ * the loop header without a break, return, throw, or continue-to-outer.
7
+ *
8
+ * Detection strategy:
9
+ * 1. Identify loop headers: back-edge targets in the CFG (`edge.type === 'back'`).
10
+ * 2. For each loop, collect the loop body blocks via BFS from the header,
11
+ * stopping at back-edge sources (the "tail" blocks).
12
+ * 3. Check whether any block in the body has an outgoing edge that exits
13
+ * the loop (target not in body set and not back to header).
14
+ * 4. As a text-level fallback, scan source lines in the loop body for
15
+ * `return`, `throw`/`raise`, `break`, `System.exit` keywords.
16
+ * 5. Emit a finding at the loop header's start_line if no exit is found.
17
+ *
18
+ * Languages: Java, JavaScript, TypeScript, Python, Rust. Skip Bash.
19
+ */
20
+ import type { AnalysisPass, PassContext } from '../../graph/analysis-pass.js';
21
+ export interface InfiniteLoopResult {
22
+ potentialInfiniteLoops: Array<{
23
+ headerLine: number;
24
+ bodyEndLine: number;
25
+ }>;
26
+ }
27
+ export declare class InfiniteLoopPass implements AnalysisPass<InfiniteLoopResult> {
28
+ readonly name = "infinite-loop";
29
+ readonly category: "reliability";
30
+ run(ctx: PassContext): InfiniteLoopResult;
31
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Pass #28: infinite-loop (CWE-835, category: reliability)
3
+ *
4
+ * Detects loops with no reachable exit edge — i.e., loops that can run
5
+ * forever because every execution path through the loop body leads back to
6
+ * the loop header without a break, return, throw, or continue-to-outer.
7
+ *
8
+ * Detection strategy:
9
+ * 1. Identify loop headers: back-edge targets in the CFG (`edge.type === 'back'`).
10
+ * 2. For each loop, collect the loop body blocks via BFS from the header,
11
+ * stopping at back-edge sources (the "tail" blocks).
12
+ * 3. Check whether any block in the body has an outgoing edge that exits
13
+ * the loop (target not in body set and not back to header).
14
+ * 4. As a text-level fallback, scan source lines in the loop body for
15
+ * `return`, `throw`/`raise`, `break`, `System.exit` keywords.
16
+ * 5. Emit a finding at the loop header's start_line if no exit is found.
17
+ *
18
+ * Languages: Java, JavaScript, TypeScript, Python, Rust. Skip Bash.
19
+ */
20
+ /** Exit keywords to scan for as a text-level fallback. */
21
+ const EXIT_KEYWORDS = /\b(return|throw|raise|break|System\.exit|process\.exit|os\._exit|exit!\()\b/;
22
+ export class InfiniteLoopPass {
23
+ name = 'infinite-loop';
24
+ category = 'reliability';
25
+ run(ctx) {
26
+ const { graph, code, language } = ctx;
27
+ if (language === 'bash') {
28
+ return { potentialInfiniteLoops: [] };
29
+ }
30
+ const { blocks, edges } = graph.ir.cfg;
31
+ if (blocks.length === 0)
32
+ return { potentialInfiniteLoops: [] };
33
+ const file = graph.ir.meta.file;
34
+ const codeLines = code.split('\n');
35
+ // Build adjacency maps
36
+ const outgoing = new Map();
37
+ for (const edge of edges) {
38
+ const list = outgoing.get(edge.from) ?? [];
39
+ list.push({ to: edge.to, type: edge.type });
40
+ outgoing.set(edge.from, list);
41
+ }
42
+ // Find back-edges: each back-edge defines a loop
43
+ const backEdges = edges.filter(e => e.type === 'back');
44
+ const potentialInfiniteLoops = [];
45
+ const reportedHeaders = new Set();
46
+ for (const backEdge of backEdges) {
47
+ const headerId = backEdge.to;
48
+ const tailId = backEdge.from;
49
+ const header = graph.blockById.get(headerId);
50
+ const tail = graph.blockById.get(tailId);
51
+ if (!header || !tail)
52
+ continue;
53
+ // Deduplicate: one finding per header
54
+ if (reportedHeaders.has(headerId))
55
+ continue;
56
+ // Collect loop body blocks via BFS from header, stopping at tail
57
+ const bodyIds = new Set();
58
+ const queue = [headerId];
59
+ bodyIds.add(headerId);
60
+ while (queue.length > 0) {
61
+ const cur = queue.shift();
62
+ for (const { to, type } of outgoing.get(cur) ?? []) {
63
+ // Don't follow back edges (stay inside the loop)
64
+ if (type === 'back')
65
+ continue;
66
+ if (!bodyIds.has(to)) {
67
+ bodyIds.add(to);
68
+ queue.push(to);
69
+ }
70
+ }
71
+ // Stop expanding from tail (the back-edge source)
72
+ if (cur === tailId)
73
+ break;
74
+ }
75
+ // Check for any exit edge: an edge from a body block to a non-body block
76
+ let hasExit = false;
77
+ for (const bodyId of bodyIds) {
78
+ for (const { to, type } of outgoing.get(bodyId) ?? []) {
79
+ if (type === 'back')
80
+ continue;
81
+ if (!bodyIds.has(to)) {
82
+ hasExit = true;
83
+ break;
84
+ }
85
+ }
86
+ if (hasExit)
87
+ break;
88
+ }
89
+ if (hasExit)
90
+ continue;
91
+ // Text-level fallback: scan source lines for exit keywords
92
+ const bodyStart = header.start_line;
93
+ const bodyEnd = tail.end_line;
94
+ let hasKeywordExit = false;
95
+ for (let ln = bodyStart; ln <= bodyEnd && ln <= codeLines.length; ln++) {
96
+ if (EXIT_KEYWORDS.test(codeLines[ln - 1] ?? '')) {
97
+ hasKeywordExit = true;
98
+ break;
99
+ }
100
+ }
101
+ if (hasKeywordExit)
102
+ continue;
103
+ reportedHeaders.add(headerId);
104
+ potentialInfiniteLoops.push({ headerLine: header.start_line, bodyEndLine: bodyEnd });
105
+ const loc = bodyStart === bodyEnd
106
+ ? `line ${bodyStart}`
107
+ : `lines ${bodyStart}–${bodyEnd}`;
108
+ ctx.addFinding({
109
+ id: `infinite-loop-${file}-${header.start_line}`,
110
+ pass: this.name,
111
+ category: this.category,
112
+ rule_id: this.name,
113
+ cwe: 'CWE-835',
114
+ severity: 'medium',
115
+ level: 'warning',
116
+ message: `Potential infinite loop: no reachable break, return, or throw found in loop body (${loc})`,
117
+ file,
118
+ line: header.start_line,
119
+ end_line: bodyEnd > header.start_line ? bodyEnd : undefined,
120
+ fix: 'Ensure the loop has a reachable exit condition (break, return, or throw) on all paths',
121
+ });
122
+ }
123
+ return { potentialInfiniteLoops };
124
+ }
125
+ }
126
+ //# sourceMappingURL=infinite-loop-pass.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"infinite-loop-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/infinite-loop-pass.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Pass #33: react-inline-jsx (category: performance)
3
+ *
4
+ * Detects inline object literals or arrow functions passed as JSX props.
5
+ * These create a new reference on every render, defeating React.memo /
6
+ * shouldComponentUpdate optimisations and causing unnecessary re-renders.
7
+ *
8
+ * Detection strategy:
9
+ * 1. Only runs on JavaScript/TypeScript files that appear to contain JSX
10
+ * (quick check: source contains a `<UpperCase` JSX component pattern).
11
+ * 2. Scans each source line for:
12
+ * a. Inline object prop: propName={{ (double-brace)
13
+ * b. Inline arrow prop: propName={(...) => or propName={identifier =>
14
+ * c. Inline function prop: propName={function(
15
+ * 3. Skips:
16
+ * - `style={{` — idiomatic and near-impossible to hoist statically
17
+ * - `key=` — must be inline
18
+ * - `data-*` attribute names
19
+ * - Lines that are comments
20
+ * 4. Emits one finding per matched line.
21
+ *
22
+ * Languages: JavaScript and TypeScript only.
23
+ */
24
+ import type { AnalysisPass, PassContext } from '../../graph/analysis-pass.js';
25
+ export interface ReactInlineJsxResult {
26
+ inlineProps: Array<{
27
+ line: number;
28
+ propName: string;
29
+ kind: 'object' | 'arrow' | 'function';
30
+ }>;
31
+ }
32
+ export declare class ReactInlineJsxPass implements AnalysisPass<ReactInlineJsxResult> {
33
+ readonly name = "react-inline-jsx";
34
+ readonly category: "performance";
35
+ run(ctx: PassContext): ReactInlineJsxResult;
36
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Pass #33: react-inline-jsx (category: performance)
3
+ *
4
+ * Detects inline object literals or arrow functions passed as JSX props.
5
+ * These create a new reference on every render, defeating React.memo /
6
+ * shouldComponentUpdate optimisations and causing unnecessary re-renders.
7
+ *
8
+ * Detection strategy:
9
+ * 1. Only runs on JavaScript/TypeScript files that appear to contain JSX
10
+ * (quick check: source contains a `<UpperCase` JSX component pattern).
11
+ * 2. Scans each source line for:
12
+ * a. Inline object prop: propName={{ (double-brace)
13
+ * b. Inline arrow prop: propName={(...) => or propName={identifier =>
14
+ * c. Inline function prop: propName={function(
15
+ * 3. Skips:
16
+ * - `style={{` — idiomatic and near-impossible to hoist statically
17
+ * - `key=` — must be inline
18
+ * - `data-*` attribute names
19
+ * - Lines that are comments
20
+ * 4. Emits one finding per matched line.
21
+ *
22
+ * Languages: JavaScript and TypeScript only.
23
+ */
24
+ /** Quick heuristic: does the file contain any JSX component usage? */
25
+ const JSX_COMPONENT_RE = /<[A-Z][A-Za-z0-9]*/;
26
+ /** Inline object prop: propName={{ (but NOT style={{ ) */
27
+ const INLINE_OBJECT_RE = /\s([A-Za-z][A-Za-z0-9_]*)=\{\{/g;
28
+ /** Inline arrow function prop: propName={(...) => or propName={x => */
29
+ const INLINE_ARROW_RE = /\s([A-Za-z][A-Za-z0-9_]*)=\{(?:\(|[A-Za-z_$]).*?=>/g;
30
+ /** Inline function expression prop: propName={function( */
31
+ const INLINE_FUNCTION_RE = /\s([A-Za-z][A-Za-z0-9_]*)=\{function\s*\(/g;
32
+ /** Props to always skip regardless of value shape. */
33
+ const SKIP_PROPS = new Set(['style', 'key', 'ref', 'className', 'id']);
34
+ export class ReactInlineJsxPass {
35
+ name = 'react-inline-jsx';
36
+ category = 'performance';
37
+ run(ctx) {
38
+ const { graph, code, language } = ctx;
39
+ if (language !== 'javascript' && language !== 'typescript') {
40
+ return { inlineProps: [] };
41
+ }
42
+ // Quick file-level JSX check
43
+ if (!JSX_COMPONENT_RE.test(code)) {
44
+ return { inlineProps: [] };
45
+ }
46
+ const file = graph.ir.meta.file;
47
+ const codeLines = code.split('\n');
48
+ const inlineProps = [];
49
+ for (let i = 0; i < codeLines.length; i++) {
50
+ const lineText = codeLines[i];
51
+ const ln = i + 1;
52
+ // Skip comment lines
53
+ const trimmed = lineText.trimStart();
54
+ if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) {
55
+ continue;
56
+ }
57
+ // --- Inline object prop: propName={{ ---
58
+ INLINE_OBJECT_RE.lastIndex = 0;
59
+ let m;
60
+ while ((m = INLINE_OBJECT_RE.exec(lineText)) !== null) {
61
+ const propName = m[1];
62
+ if (SKIP_PROPS.has(propName))
63
+ continue;
64
+ if (propName.startsWith('data-'))
65
+ continue; // data-* attributes
66
+ inlineProps.push({ line: ln, propName, kind: 'object' });
67
+ ctx.addFinding({
68
+ id: `react-inline-jsx-obj-${file}-${ln}`,
69
+ pass: this.name,
70
+ category: this.category,
71
+ rule_id: this.name,
72
+ cwe: undefined,
73
+ severity: 'low',
74
+ level: 'note',
75
+ message: `Inline object in JSX prop \`${propName}\` creates a new reference on every render, ` +
76
+ `defeating memoization`,
77
+ file,
78
+ line: ln,
79
+ snippet: lineText.trim(),
80
+ fix: `Extract the object literal into a \`useMemo\` hook or a module-level constant, ` +
81
+ `then pass the reference: \`${propName}={myConstObject}\`.`,
82
+ });
83
+ }
84
+ // --- Inline arrow function prop: propName={(...) => or propName={x => ---
85
+ INLINE_ARROW_RE.lastIndex = 0;
86
+ while ((m = INLINE_ARROW_RE.exec(lineText)) !== null) {
87
+ const propName = m[1];
88
+ if (SKIP_PROPS.has(propName))
89
+ continue;
90
+ if (propName.startsWith('data'))
91
+ continue;
92
+ inlineProps.push({ line: ln, propName, kind: 'arrow' });
93
+ ctx.addFinding({
94
+ id: `react-inline-jsx-arrow-${file}-${ln}`,
95
+ pass: this.name,
96
+ category: this.category,
97
+ rule_id: this.name,
98
+ cwe: undefined,
99
+ severity: 'low',
100
+ level: 'note',
101
+ message: `Inline arrow function in JSX prop \`${propName}\` creates a new function reference on every render, ` +
102
+ `defeating memoization`,
103
+ file,
104
+ line: ln,
105
+ snippet: lineText.trim(),
106
+ fix: `Wrap the handler with \`useCallback\` or define it outside the component: ` +
107
+ `\`const handle${propName.charAt(0).toUpperCase()}${propName.slice(1)} = useCallback(...)\`.`,
108
+ });
109
+ }
110
+ // --- Inline function expression prop: propName={function( ---
111
+ INLINE_FUNCTION_RE.lastIndex = 0;
112
+ while ((m = INLINE_FUNCTION_RE.exec(lineText)) !== null) {
113
+ const propName = m[1];
114
+ if (SKIP_PROPS.has(propName))
115
+ continue;
116
+ if (propName.startsWith('data'))
117
+ continue;
118
+ inlineProps.push({ line: ln, propName, kind: 'function' });
119
+ ctx.addFinding({
120
+ id: `react-inline-jsx-fn-${file}-${ln}`,
121
+ pass: this.name,
122
+ category: this.category,
123
+ rule_id: this.name,
124
+ cwe: undefined,
125
+ severity: 'low',
126
+ level: 'note',
127
+ message: `Inline function expression in JSX prop \`${propName}\` creates a new function reference on every render, ` +
128
+ `defeating memoization`,
129
+ file,
130
+ line: ln,
131
+ snippet: lineText.trim(),
132
+ fix: `Wrap the handler with \`useCallback\` or define it outside the component: ` +
133
+ `\`const handle${propName.charAt(0).toUpperCase()}${propName.slice(1)} = useCallback(...)\`.`,
134
+ });
135
+ }
136
+ }
137
+ return { inlineProps };
138
+ }
139
+ }
140
+ //# sourceMappingURL=react-inline-jsx-pass.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react-inline-jsx-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/react-inline-jsx-pass.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Pass #30: redundant-loop-computation (CWE-1050, category: performance)
3
+ *
4
+ * Detects loop-invariant expressions that are recomputed on every iteration.
5
+ * The most common and highest-signal patterns:
6
+ * - `.length` / `.size()` / `.count()` on a variable not modified in the loop
7
+ * - `Object.keys(x)` / `Object.values(x)` / `Object.entries(x)` on invariant `x`
8
+ * - Pure math: `Math.sqrt(x)`, `Math.pow(x, n)`, `Math.abs(x)` on invariant args
9
+ *
10
+ * Detection strategy:
11
+ * 1. Identify loop bodies via `graph.loopBodies()` (CFG back-edge derived).
12
+ * 2. Build `modifiedVars`: DFG defs whose line falls inside the loop range.
13
+ * 3. Scan source lines for the invariant patterns.
14
+ * 4. If the receiver/argument variable is NOT in `modifiedVars`, emit a finding.
15
+ *
16
+ * Languages: JavaScript/TypeScript, Java, Python, Rust. Bash — skipped.
17
+ */
18
+ import type { AnalysisPass, PassContext } from '../../graph/analysis-pass.js';
19
+ export interface RedundantLoopResult {
20
+ invariants: Array<{
21
+ line: number;
22
+ expression: string;
23
+ variable: string;
24
+ }>;
25
+ }
26
+ export declare class RedundantLoopPass implements AnalysisPass<RedundantLoopResult> {
27
+ readonly name = "redundant-loop-computation";
28
+ readonly category: "performance";
29
+ run(ctx: PassContext): RedundantLoopResult;
30
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Pass #30: redundant-loop-computation (CWE-1050, category: performance)
3
+ *
4
+ * Detects loop-invariant expressions that are recomputed on every iteration.
5
+ * The most common and highest-signal patterns:
6
+ * - `.length` / `.size()` / `.count()` on a variable not modified in the loop
7
+ * - `Object.keys(x)` / `Object.values(x)` / `Object.entries(x)` on invariant `x`
8
+ * - Pure math: `Math.sqrt(x)`, `Math.pow(x, n)`, `Math.abs(x)` on invariant args
9
+ *
10
+ * Detection strategy:
11
+ * 1. Identify loop bodies via `graph.loopBodies()` (CFG back-edge derived).
12
+ * 2. Build `modifiedVars`: DFG defs whose line falls inside the loop range.
13
+ * 3. Scan source lines for the invariant patterns.
14
+ * 4. If the receiver/argument variable is NOT in `modifiedVars`, emit a finding.
15
+ *
16
+ * Languages: JavaScript/TypeScript, Java, Python, Rust. Bash — skipped.
17
+ */
18
+ // Match: varName.length or varName.size() or varName.count()
19
+ const LENGTH_PATTERN = /\b([A-Za-z_$][A-Za-z0-9_$]*)\s*\.\s*(?:length|size\(\)|count\(\))/g;
20
+ // Match: Object.keys(varName) Object.values(varName) Object.entries(varName)
21
+ const OBJECT_STATIC_PATTERN = /\bObject\s*\.\s*(?:keys|values|entries)\s*\(\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*\)/g;
22
+ // Match: Math.sqrt(varName) Math.pow(varName Math.abs(varName) Math.floor(varName) Math.ceil(varName)
23
+ const MATH_PATTERN = /\bMath\s*\.\s*(?:sqrt|pow|abs|floor|ceil|round|log|log2|log10)\s*\(\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*[,)]/g;
24
+ export class RedundantLoopPass {
25
+ name = 'redundant-loop-computation';
26
+ category = 'performance';
27
+ run(ctx) {
28
+ const { graph, code, language } = ctx;
29
+ if (language === 'bash') {
30
+ return { invariants: [] };
31
+ }
32
+ const file = graph.ir.meta.file;
33
+ const codeLines = code.split('\n');
34
+ const loops = graph.loopBodies();
35
+ if (loops.length === 0)
36
+ return { invariants: [] };
37
+ const invariants = [];
38
+ const reported = new Set(); // deduplicate by line+expression
39
+ for (const loop of loops) {
40
+ const { start_line, end_line } = loop;
41
+ // Collect variables modified (written) inside the loop body
42
+ const modifiedVars = new Set();
43
+ for (const def of graph.ir.dfg.defs) {
44
+ if (def.line >= start_line && def.line <= end_line) {
45
+ modifiedVars.add(def.variable);
46
+ }
47
+ }
48
+ // Scan each line in the loop body for invariant patterns
49
+ for (let ln = start_line; ln <= end_line && ln <= codeLines.length; ln++) {
50
+ const lineText = codeLines[ln - 1] ?? '';
51
+ // Skip blank lines
52
+ if (lineText.trim() === '')
53
+ continue;
54
+ // --- .length / .size() / .count() ---
55
+ LENGTH_PATTERN.lastIndex = 0;
56
+ let m;
57
+ while ((m = LENGTH_PATTERN.exec(lineText)) !== null) {
58
+ const varName = m[1];
59
+ if (modifiedVars.has(varName))
60
+ continue;
61
+ // Skip if used in a for-loop initialisation line (e.g., for (let i = 0; i < arr.length; i++))
62
+ // — the loop header itself scanning is expected; flag it only inside the body
63
+ const expr = m[0];
64
+ const key = `${ln}-${expr}`;
65
+ if (reported.has(key))
66
+ continue;
67
+ reported.add(key);
68
+ invariants.push({ line: ln, expression: expr, variable: varName });
69
+ ctx.addFinding({
70
+ id: `redundant-loop-computation-${file}-${ln}`,
71
+ pass: this.name,
72
+ category: this.category,
73
+ rule_id: this.name,
74
+ cwe: 'CWE-1050',
75
+ severity: 'low',
76
+ level: 'note',
77
+ message: `Loop-invariant computation: \`${expr}\` is recomputed on every iteration; hoist outside loop`,
78
+ file,
79
+ line: ln,
80
+ snippet: lineText.trim(),
81
+ fix: `Compute \`${expr}\` once before the loop and use the cached value inside.`,
82
+ evidence: { variable: varName, loop_start: start_line, loop_end: end_line },
83
+ });
84
+ }
85
+ // --- Object.keys/values/entries(x) ---
86
+ OBJECT_STATIC_PATTERN.lastIndex = 0;
87
+ while ((m = OBJECT_STATIC_PATTERN.exec(lineText)) !== null) {
88
+ const varName = m[1];
89
+ if (modifiedVars.has(varName))
90
+ continue;
91
+ const expr = m[0];
92
+ const key = `${ln}-${expr}`;
93
+ if (reported.has(key))
94
+ continue;
95
+ reported.add(key);
96
+ invariants.push({ line: ln, expression: expr, variable: varName });
97
+ ctx.addFinding({
98
+ id: `redundant-loop-computation-${file}-${ln}-obj`,
99
+ pass: this.name,
100
+ category: this.category,
101
+ rule_id: this.name,
102
+ cwe: 'CWE-1050',
103
+ severity: 'low',
104
+ level: 'note',
105
+ message: `Loop-invariant computation: \`${expr}\` allocates a new array on every iteration; hoist outside loop`,
106
+ file,
107
+ line: ln,
108
+ snippet: lineText.trim(),
109
+ fix: `Compute \`${expr}\` once before the loop.`,
110
+ evidence: { variable: varName, loop_start: start_line, loop_end: end_line },
111
+ });
112
+ }
113
+ // --- Math.*(x) ---
114
+ MATH_PATTERN.lastIndex = 0;
115
+ while ((m = MATH_PATTERN.exec(lineText)) !== null) {
116
+ const varName = m[1];
117
+ if (modifiedVars.has(varName))
118
+ continue;
119
+ const expr = m[0].replace(/[,)]?\s*$/, ')');
120
+ const key = `${ln}-${expr}`;
121
+ if (reported.has(key))
122
+ continue;
123
+ reported.add(key);
124
+ invariants.push({ line: ln, expression: expr, variable: varName });
125
+ ctx.addFinding({
126
+ id: `redundant-loop-computation-${file}-${ln}-math`,
127
+ pass: this.name,
128
+ category: this.category,
129
+ rule_id: this.name,
130
+ cwe: 'CWE-1050',
131
+ severity: 'low',
132
+ level: 'note',
133
+ message: `Loop-invariant computation: \`${expr}\` is recomputed on every iteration; hoist outside loop`,
134
+ file,
135
+ line: ln,
136
+ snippet: lineText.trim(),
137
+ fix: `Compute \`${expr}\` once before the loop.`,
138
+ evidence: { variable: varName, loop_start: start_line, loop_end: end_line },
139
+ });
140
+ }
141
+ }
142
+ }
143
+ return { invariants };
144
+ }
145
+ }
146
+ //# sourceMappingURL=redundant-loop-pass.js.map