circle-ir 3.148.0 → 3.150.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.
@@ -0,0 +1,94 @@
1
+ /**
2
+ * CliMainReflectionSuppressPass — cognium-dev #162 Option B
3
+ *
4
+ * Drops Java reflection `code_injection` sinks in files that look like
5
+ * a fat-jar CLI tool's own artifact — files that ship a `main(String[])`
6
+ * entry point AND carry no web-framework entry-point signal anywhere
7
+ * in the file. The `main(String[])` on the same compilation unit *is*
8
+ * the trust boundary; the caller is the OS shell that invoked the jar.
9
+ *
10
+ * Motivation (from the ticket):
11
+ *
12
+ * // antlr/tool/.../TestRig.java — 5 reflection sinks reported HIGH
13
+ * public static void main(String[] args) throws Exception {
14
+ * TestRig testRig = new TestRig(args);
15
+ * ...
16
+ * }
17
+ * public void process() throws Exception {
18
+ * String lexerName = grammarName + "Lexer";
19
+ * ClassLoader cl = Thread.currentThread().getContextClassLoader();
20
+ * Class<? extends Lexer> lexerClass =
21
+ * cl.loadClass(lexerName).asSubclass(Lexer.class); // sink
22
+ * Constructor<? extends Lexer> lexerCtor =
23
+ * lexerClass.getConstructor(CharStream.class);
24
+ * Lexer lexer = lexerCtor.newInstance((CharStream)null); // sink
25
+ * ...
26
+ * }
27
+ *
28
+ * This is documented behaviour of ANTLR's `TestRig` developer CLI —
29
+ * same shape as `javac`, `java -jar`, `python -m`. The tool's whole
30
+ * purpose is "give me a grammar name from the command line and I'll
31
+ * run it." Blocked 5/49 HIGH FPs in the top-100 Java harness.
32
+ *
33
+ * ## Signal
34
+ *
35
+ * Language MUST be Java. Per-file signal (all must hold):
36
+ *
37
+ * 1. Some type in the file declares a `main(String[])` method
38
+ * (matches `looksLikeMainMethod` in `entry-point-detection.ts`).
39
+ * 2. NO type in the file carries a Tier-1 class-level web-framework
40
+ * annotation (`@RestController`, `@Controller`, `@Service`,
41
+ * `@Component`, `@Path`, `@WebServlet`, `@ServerEndpoint`,
42
+ * `@FeignClient`, `@Repository`).
43
+ * 3. NO method in the file carries a Tier-1 method-level
44
+ * web-framework annotation (Spring `@*Mapping`, `@KafkaListener`,
45
+ * `@JmsListener`, JAX-RS `@GET/@POST/…`, `@Scheduled`, `@Path`,
46
+ * `@DataBoundConstructor`/`@DataBoundSetter`).
47
+ * 4. NO type in the file extends a Tier-1 supertype (HttpServlet,
48
+ * GenericServlet, Filter, HandlerInterceptor, CommandLineRunner,
49
+ * SimpleChannelInboundHandler, etc.).
50
+ *
51
+ * ## Action
52
+ *
53
+ * When the signal fires, drop from the `SinkFilterResult.sinks` list
54
+ * (the authoritative sink array consumed by `TaintPropagationPass` and
55
+ * folded into `taint.sinks` by `analyzer.ts`) every sink whose
56
+ * `type === 'code_injection'` AND `method` is a Java reflection
57
+ * / ClassLoader surface method. The gate does NOT touch other
58
+ * `code_injection` shapes: ScriptEngine.eval / GroovyShell.evaluate /
59
+ * SpEL `parseExpression` remain flagged in CLI tools because they
60
+ * evaluate scripts (a genuinely dangerous CLI misuse), not just class
61
+ * names.
62
+ *
63
+ * ## Pipeline slot
64
+ *
65
+ * Runs after `SinkSemanticsPass` (so its curated-registry drops have
66
+ * already fired) and before `TaintPropagationPass` (so the flow
67
+ * generators never see the dropped sinks). Emits no findings.
68
+ *
69
+ * ## Pillar I / safety
70
+ *
71
+ * - Pure per-file heuristic; no LLM, no filesystem, no config knobs.
72
+ * - False-negative-safe: any web-framework signal in the file
73
+ * (annotation OR supertype OR Tier-1 method annotation) disables
74
+ * the gate. Recall preserved for `@RestController` classes,
75
+ * `HttpServlet` subclasses, Netty handlers, Jenkins plugins, and
76
+ * Kafka/JMS listeners that happen to also expose a `main`.
77
+ * - Reflection-only: `Runtime.exec`, `ProcessBuilder.start`,
78
+ * `Statement.execute`, `ObjectInputStream.readObject`, SpEL,
79
+ * Groovy, ScriptEngine — all unaffected.
80
+ * - Java-only: non-Java sources fall through untouched.
81
+ */
82
+ import type { AnalysisPass, PassContext } from '../../graph/analysis-pass.js';
83
+ export interface CliMainReflectionSuppressResult {
84
+ /** Whether the fat-jar CLI signal fired for this file. */
85
+ cliMainSignal: boolean;
86
+ /** Number of reflection `code_injection` sinks dropped. */
87
+ droppedCount: number;
88
+ }
89
+ export declare class CliMainReflectionSuppressPass implements AnalysisPass<CliMainReflectionSuppressResult> {
90
+ readonly name = "cli-main-reflection-suppress";
91
+ readonly category: "security";
92
+ run(ctx: PassContext): CliMainReflectionSuppressResult;
93
+ }
94
+ //# sourceMappingURL=cli-main-reflection-suppress-pass.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-main-reflection-suppress-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/cli-main-reflection-suppress-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgFG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAG9E,MAAM,WAAW,+BAA+B;IAC9C,0DAA0D;IAC1D,aAAa,EAAE,OAAO,CAAC;IACvB,2DAA2D;IAC3D,YAAY,EAAE,MAAM,CAAC;CACtB;AAiKD,qBAAa,6BACX,YAAW,YAAY,CAAC,+BAA+B,CAAC;IAExD,QAAQ,CAAC,IAAI,kCAAkC;IAC/C,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,+BAA+B;CA+FvD"}
@@ -0,0 +1,337 @@
1
+ /**
2
+ * CliMainReflectionSuppressPass — cognium-dev #162 Option B
3
+ *
4
+ * Drops Java reflection `code_injection` sinks in files that look like
5
+ * a fat-jar CLI tool's own artifact — files that ship a `main(String[])`
6
+ * entry point AND carry no web-framework entry-point signal anywhere
7
+ * in the file. The `main(String[])` on the same compilation unit *is*
8
+ * the trust boundary; the caller is the OS shell that invoked the jar.
9
+ *
10
+ * Motivation (from the ticket):
11
+ *
12
+ * // antlr/tool/.../TestRig.java — 5 reflection sinks reported HIGH
13
+ * public static void main(String[] args) throws Exception {
14
+ * TestRig testRig = new TestRig(args);
15
+ * ...
16
+ * }
17
+ * public void process() throws Exception {
18
+ * String lexerName = grammarName + "Lexer";
19
+ * ClassLoader cl = Thread.currentThread().getContextClassLoader();
20
+ * Class<? extends Lexer> lexerClass =
21
+ * cl.loadClass(lexerName).asSubclass(Lexer.class); // sink
22
+ * Constructor<? extends Lexer> lexerCtor =
23
+ * lexerClass.getConstructor(CharStream.class);
24
+ * Lexer lexer = lexerCtor.newInstance((CharStream)null); // sink
25
+ * ...
26
+ * }
27
+ *
28
+ * This is documented behaviour of ANTLR's `TestRig` developer CLI —
29
+ * same shape as `javac`, `java -jar`, `python -m`. The tool's whole
30
+ * purpose is "give me a grammar name from the command line and I'll
31
+ * run it." Blocked 5/49 HIGH FPs in the top-100 Java harness.
32
+ *
33
+ * ## Signal
34
+ *
35
+ * Language MUST be Java. Per-file signal (all must hold):
36
+ *
37
+ * 1. Some type in the file declares a `main(String[])` method
38
+ * (matches `looksLikeMainMethod` in `entry-point-detection.ts`).
39
+ * 2. NO type in the file carries a Tier-1 class-level web-framework
40
+ * annotation (`@RestController`, `@Controller`, `@Service`,
41
+ * `@Component`, `@Path`, `@WebServlet`, `@ServerEndpoint`,
42
+ * `@FeignClient`, `@Repository`).
43
+ * 3. NO method in the file carries a Tier-1 method-level
44
+ * web-framework annotation (Spring `@*Mapping`, `@KafkaListener`,
45
+ * `@JmsListener`, JAX-RS `@GET/@POST/…`, `@Scheduled`, `@Path`,
46
+ * `@DataBoundConstructor`/`@DataBoundSetter`).
47
+ * 4. NO type in the file extends a Tier-1 supertype (HttpServlet,
48
+ * GenericServlet, Filter, HandlerInterceptor, CommandLineRunner,
49
+ * SimpleChannelInboundHandler, etc.).
50
+ *
51
+ * ## Action
52
+ *
53
+ * When the signal fires, drop from the `SinkFilterResult.sinks` list
54
+ * (the authoritative sink array consumed by `TaintPropagationPass` and
55
+ * folded into `taint.sinks` by `analyzer.ts`) every sink whose
56
+ * `type === 'code_injection'` AND `method` is a Java reflection
57
+ * / ClassLoader surface method. The gate does NOT touch other
58
+ * `code_injection` shapes: ScriptEngine.eval / GroovyShell.evaluate /
59
+ * SpEL `parseExpression` remain flagged in CLI tools because they
60
+ * evaluate scripts (a genuinely dangerous CLI misuse), not just class
61
+ * names.
62
+ *
63
+ * ## Pipeline slot
64
+ *
65
+ * Runs after `SinkSemanticsPass` (so its curated-registry drops have
66
+ * already fired) and before `TaintPropagationPass` (so the flow
67
+ * generators never see the dropped sinks). Emits no findings.
68
+ *
69
+ * ## Pillar I / safety
70
+ *
71
+ * - Pure per-file heuristic; no LLM, no filesystem, no config knobs.
72
+ * - False-negative-safe: any web-framework signal in the file
73
+ * (annotation OR supertype OR Tier-1 method annotation) disables
74
+ * the gate. Recall preserved for `@RestController` classes,
75
+ * `HttpServlet` subclasses, Netty handlers, Jenkins plugins, and
76
+ * Kafka/JMS listeners that happen to also expose a `main`.
77
+ * - Reflection-only: `Runtime.exec`, `ProcessBuilder.start`,
78
+ * `Statement.execute`, `ObjectInputStream.readObject`, SpEL,
79
+ * Groovy, ScriptEngine — all unaffected.
80
+ * - Java-only: non-Java sources fall through untouched.
81
+ */
82
+ /**
83
+ * Java reflection + ClassLoader methods that produce `code_injection`
84
+ * sinks via `configs/sinks/code_injection.yaml`. Simple-name match on
85
+ * `sink.method`. Kept intentionally narrow — script-engine /
86
+ * expression-language sinks (`eval`, `evaluate`, `parseExpression`,
87
+ * `getBeanInfo`, `getEngineByName`) are NOT in this set, so those
88
+ * still fire in CLI artifacts.
89
+ */
90
+ const REFLECTION_SINK_METHODS = new Set([
91
+ 'forName',
92
+ 'newInstance',
93
+ 'invoke',
94
+ 'getMethod',
95
+ 'getDeclaredMethod',
96
+ 'getConstructor',
97
+ 'getDeclaredConstructor',
98
+ 'loadClass',
99
+ 'defineClass',
100
+ ]);
101
+ /**
102
+ * Tier-1 class-level web-framework annotations. Simple-name match
103
+ * (annotation strings in `TypeInfo.annotations` may include the `@`
104
+ * prefix and generic args — we strip both). Any single match kills
105
+ * the gate for this file.
106
+ */
107
+ const TIER_1_CLASS_ANNOTATIONS = new Set([
108
+ // Spring MVC
109
+ 'RestController',
110
+ 'Controller',
111
+ // Spring stereotype beans
112
+ 'Service',
113
+ 'Repository',
114
+ 'Component',
115
+ // JAX-RS resource class
116
+ 'Path',
117
+ // Servlet 3.0 annotation-based servlet
118
+ 'WebServlet',
119
+ // JSR-356 WebSocket endpoint
120
+ 'ServerEndpoint',
121
+ // Declarative HTTP client
122
+ 'FeignClient',
123
+ ]);
124
+ /**
125
+ * Tier-1 method-level web-framework annotations. Any single match
126
+ * kills the gate.
127
+ */
128
+ const TIER_1_METHOD_ANNOTATIONS = new Set([
129
+ // Spring MVC
130
+ 'RequestMapping',
131
+ 'GetMapping',
132
+ 'PostMapping',
133
+ 'PutMapping',
134
+ 'DeleteMapping',
135
+ 'PatchMapping',
136
+ // Spring messaging / WebSocket
137
+ 'MessageMapping',
138
+ 'SubscribeMapping',
139
+ // Spring messaging listeners
140
+ 'KafkaListener',
141
+ 'KafkaHandler',
142
+ 'RabbitListener',
143
+ 'RabbitHandler',
144
+ 'JmsListener',
145
+ 'StreamListener',
146
+ // Spring Cloud AWS
147
+ 'SqsListener',
148
+ 'SqsHandler',
149
+ // Spring application events
150
+ 'EventListener',
151
+ // CRON / scheduled
152
+ 'Scheduled',
153
+ // JAX-RS
154
+ 'Path',
155
+ 'GET',
156
+ 'POST',
157
+ 'PUT',
158
+ 'DELETE',
159
+ 'PATCH',
160
+ 'HEAD',
161
+ 'OPTIONS',
162
+ // Jenkins Stapler form-binding
163
+ 'DataBoundConstructor',
164
+ 'DataBoundSetter',
165
+ ]);
166
+ /**
167
+ * Tier-1 supertypes whose subclasses declare framework entry points
168
+ * via lifecycle methods (HttpServlet.doGet, ChannelHandler.channelRead,
169
+ * CommandLineRunner.run, etc.). Simple-name match on
170
+ * `TypeInfo.extends` and each `TypeInfo.implements[]` entry (generics
171
+ * stripped).
172
+ */
173
+ const TIER_1_SUPERTYPES = new Set([
174
+ 'HttpServlet',
175
+ 'GenericServlet',
176
+ 'Filter',
177
+ 'HandlerInterceptor',
178
+ 'AsyncHandlerInterceptor',
179
+ 'CommandLineRunner',
180
+ 'ApplicationRunner',
181
+ 'SimpleChannelInboundHandler',
182
+ 'ChannelInboundHandler',
183
+ 'ChannelInboundHandlerAdapter',
184
+ 'ChannelDuplexHandler',
185
+ 'NettyRequestProcessor',
186
+ 'Converter',
187
+ 'SingleValueConverter',
188
+ 'ConverterMatcher',
189
+ 'AbstractReflectionConverter',
190
+ 'AbstractSingleValueConverter',
191
+ 'AbstractCollectionConverter',
192
+ ]);
193
+ /**
194
+ * Normalize an annotation string to its simple name. Handles `@Foo`,
195
+ * `Foo`, `@Foo(...)`, `pkg.Foo`, and `Foo<T>` shapes. Returns the
196
+ * bare identifier (`Foo`) for the lookup.
197
+ */
198
+ function normalizeAnnotation(raw) {
199
+ let s = raw.trim();
200
+ if (s.startsWith('@'))
201
+ s = s.slice(1);
202
+ const parenIdx = s.indexOf('(');
203
+ if (parenIdx >= 0)
204
+ s = s.slice(0, parenIdx);
205
+ const genericIdx = s.indexOf('<');
206
+ if (genericIdx >= 0)
207
+ s = s.slice(0, genericIdx);
208
+ const dotIdx = s.lastIndexOf('.');
209
+ if (dotIdx >= 0)
210
+ s = s.slice(dotIdx + 1);
211
+ return s.trim();
212
+ }
213
+ /** Strip generics + package qualifier from a supertype reference. */
214
+ function normalizeSupertype(raw) {
215
+ let s = raw.trim();
216
+ const genericIdx = s.indexOf('<');
217
+ if (genericIdx >= 0)
218
+ s = s.slice(0, genericIdx);
219
+ const dotIdx = s.lastIndexOf('.');
220
+ if (dotIdx >= 0)
221
+ s = s.slice(dotIdx + 1);
222
+ return s.trim();
223
+ }
224
+ /**
225
+ * Detect the `main(String[] args)` entry-point shape. Static isn't
226
+ * required by the JVM launcher spec strictly (some launchers accept
227
+ * instance mains), but every real Java CLI uses `public static void
228
+ * main(String[])`. We match on the parameter shape only — one string
229
+ * array — matching `looksLikeMainMethod` in
230
+ * `src/analysis/entry-point-detection.ts`.
231
+ */
232
+ function isMainMethod(name, paramTypes) {
233
+ if (name !== 'main')
234
+ return false;
235
+ if (paramTypes.length !== 1)
236
+ return false;
237
+ const t = paramTypes[0];
238
+ if (!t)
239
+ return false;
240
+ const bare = t.replace(/\s+/g, '');
241
+ return bare === 'String[]' || bare === 'java.lang.String[]';
242
+ }
243
+ export class CliMainReflectionSuppressPass {
244
+ name = 'cli-main-reflection-suppress';
245
+ category = 'security';
246
+ run(ctx) {
247
+ const { graph, language } = ctx;
248
+ // Java-only gate. Non-Java IR falls through untouched.
249
+ if (language !== 'java') {
250
+ return { cliMainSignal: false, droppedCount: 0 };
251
+ }
252
+ const types = graph.ir.types;
253
+ if (!types || types.length === 0) {
254
+ return { cliMainSignal: false, droppedCount: 0 };
255
+ }
256
+ // Signal condition (1): main(String[]) present anywhere in file.
257
+ let hasMain = false;
258
+ // Disqualifiers (2), (3), (4): any Tier-1 web-framework signal.
259
+ let hasFrameworkSignal = false;
260
+ for (const type of types) {
261
+ // (2) class-level annotations
262
+ for (const ann of type.annotations) {
263
+ if (TIER_1_CLASS_ANNOTATIONS.has(normalizeAnnotation(ann))) {
264
+ hasFrameworkSignal = true;
265
+ break;
266
+ }
267
+ }
268
+ if (hasFrameworkSignal)
269
+ break;
270
+ // (4) supertype match — `extends` slot + every `implements` slot.
271
+ if (type.extends && TIER_1_SUPERTYPES.has(normalizeSupertype(type.extends))) {
272
+ hasFrameworkSignal = true;
273
+ break;
274
+ }
275
+ for (const impl of type.implements) {
276
+ if (TIER_1_SUPERTYPES.has(normalizeSupertype(impl))) {
277
+ hasFrameworkSignal = true;
278
+ break;
279
+ }
280
+ }
281
+ if (hasFrameworkSignal)
282
+ break;
283
+ for (const method of type.methods) {
284
+ // (3) method-level annotations
285
+ for (const ann of method.annotations) {
286
+ if (TIER_1_METHOD_ANNOTATIONS.has(normalizeAnnotation(ann))) {
287
+ hasFrameworkSignal = true;
288
+ break;
289
+ }
290
+ }
291
+ if (hasFrameworkSignal)
292
+ break;
293
+ // (1) main(String[]) shape
294
+ if (!hasMain) {
295
+ const paramTypes = method.parameters.map((p) => p.type);
296
+ if (isMainMethod(method.name, paramTypes)) {
297
+ hasMain = true;
298
+ }
299
+ }
300
+ }
301
+ if (hasFrameworkSignal)
302
+ break;
303
+ }
304
+ const cliMainSignal = hasMain && !hasFrameworkSignal;
305
+ if (!cliMainSignal) {
306
+ return { cliMainSignal: false, droppedCount: 0 };
307
+ }
308
+ // Drop reflection code_injection sinks in place. The authoritative
309
+ // sink list in the real pipeline is `SinkFilterResult.sinks` — that
310
+ // is what `analyzer.ts` assembles the final `taint.sinks` from and
311
+ // what `TaintPropagationPass` / `InterproceduralPass` consume.
312
+ // `graph.ir.taint.sinks` starts empty and is never populated by the
313
+ // pipeline, so mutating it alone would be a no-op. Prefer the
314
+ // sink-filter result; fall back to `graph.ir.taint.sinks` for
315
+ // stand-alone unit-test harnesses that don't run `SinkFilterPass`.
316
+ const sinks = ctx.hasResult('sink-filter')
317
+ ? ctx.getResult('sink-filter').sinks
318
+ : graph.ir.taint.sinks;
319
+ let droppedCount = 0;
320
+ const kept = sinks.filter((sink) => {
321
+ if (sink.type !== 'code_injection')
322
+ return true;
323
+ if (!sink.method)
324
+ return true;
325
+ if (!REFLECTION_SINK_METHODS.has(sink.method))
326
+ return true;
327
+ droppedCount++;
328
+ return false;
329
+ });
330
+ if (droppedCount > 0) {
331
+ sinks.length = 0;
332
+ sinks.push(...kept);
333
+ }
334
+ return { cliMainSignal: true, droppedCount };
335
+ }
336
+ }
337
+ //# sourceMappingURL=cli-main-reflection-suppress-pass.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-main-reflection-suppress-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/cli-main-reflection-suppress-pass.ts"],"names":[],"mappings":"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"}
@@ -68,5 +68,31 @@ export declare class ResourceLeakPass implements AnalysisPass<ResourceLeakResult
68
68
  * `void openFoo()` continue to fire.
69
69
  */
70
70
  private isFactoryMethod;
71
+ /**
72
+ * #226 — true if `variable` is passed as a constructor argument to a
73
+ * known Closeable wrapper call within the enclosing method's line
74
+ * range. Ownership of the inner stream transfers to the wrapper.
75
+ *
76
+ * The check is deliberately per-method: cross-method wrapping does
77
+ * not apply because the inner reference has already escaped the
78
+ * scope by then.
79
+ */
80
+ private isWrappedByCloseableCtor;
81
+ /**
82
+ * #227 — true if `variable` refers to a field of the enclosing class
83
+ * that is closed inside a worker-literal method (Runnable#run,
84
+ * Callable#call, Consumer#accept, ...) declared in the same enclosing
85
+ * method. Ownership transfers to the executor thread.
86
+ *
87
+ * The heuristic is intentionally conservative: it requires both
88
+ * (a) the resource variable to match a declared field name on the
89
+ * enclosing class (so a stray local named `selector` cannot
90
+ * accidentally opt in to the suppression), AND
91
+ * (b) a `<field>.close()` (or CLOSE_METHODS) call to appear on a
92
+ * line inside a method whose name is one of WORKER_METHODS and
93
+ * whose start_line is strictly within the enclosing method's
94
+ * body (nested literal indicator).
95
+ */
96
+ private isClosedInNestedWorker;
71
97
  }
72
98
  //# sourceMappingURL=resource-leak-pass.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"resource-leak-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/resource-leak-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AA+C9E,MAAM,WAAW,kBAAkB;IACjC,iDAAiD;IACjD,KAAK,EAAE,KAAK,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,UAAU,GAAG,WAAW,CAAC;KAChC,CAAC,CAAC;CACJ;AAED,qBAAa,gBAAiB,YAAW,YAAY,CAAC,kBAAkB,CAAC;IACvE,QAAQ,CAAC,IAAI,mBAAmB;IAChC,QAAQ,CAAC,QAAQ,EAAG,aAAa,CAAU;IAE3C,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,kBAAkB;IA0HzC,kFAAkF;IAClF,OAAO,CAAC,eAAe;IAOvB;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IAQ3B;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAa1B;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAgBvB;;;;;;OAMG;IACH,OAAO,CAAC,sBAAsB;IAa9B;;;;;;OAMG;IACH,OAAO,CAAC,eAAe;CAIxB"}
1
+ {"version":3,"file":"resource-leak-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/resource-leak-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAgF9E,MAAM,WAAW,kBAAkB;IACjC,iDAAiD;IACjD,KAAK,EAAE,KAAK,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,UAAU,GAAG,WAAW,CAAC;KAChC,CAAC,CAAC;CACJ;AAED,qBAAa,gBAAiB,YAAW,YAAY,CAAC,kBAAkB,CAAC;IACvE,QAAQ,CAAC,IAAI,mBAAmB;IAChC,QAAQ,CAAC,QAAQ,EAAG,aAAa,CAAU;IAE3C,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,kBAAkB;IAkJzC,kFAAkF;IAClF,OAAO,CAAC,eAAe;IAOvB;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IAQ3B;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAa1B;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAgBvB;;;;;;OAMG;IACH,OAAO,CAAC,sBAAsB;IAa9B;;;;;;OAMG;IACH,OAAO,CAAC,eAAe;IAKvB;;;;;;;;OAQG;IACH,OAAO,CAAC,wBAAwB;IAmBhC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,sBAAsB;CAqD/B"}
@@ -56,6 +56,37 @@ const CLOSE_METHODS = new Set([
56
56
  'close', 'dispose', 'shutdown', 'disconnect', 'release', 'destroy', 'free',
57
57
  'shutdownNow', 'terminate',
58
58
  ]);
59
+ /**
60
+ * #226 — Closeable wrapper constructors that document ownership transfer
61
+ * of their underlying stream/reader. When the underlying resource is passed
62
+ * as a constructor argument to one of these wrappers, closing the wrapper
63
+ * cascades to the inner stream (per JDK javadoc), so no leak exists.
64
+ * Reference: java.io + java.util.zip standard wrappers.
65
+ */
66
+ const WRAPPER_CTORS = new Set([
67
+ // java.io wrappers
68
+ 'BufferedInputStream', 'BufferedOutputStream', 'BufferedReader', 'BufferedWriter',
69
+ 'InputStreamReader', 'OutputStreamWriter', 'DataInputStream', 'DataOutputStream',
70
+ 'PrintStream', 'PrintWriter', 'LineNumberReader', 'PushbackInputStream',
71
+ 'PushbackReader', 'SequenceInputStream',
72
+ // java.util.zip wrappers
73
+ 'GZIPInputStream', 'GZIPOutputStream', 'ZipInputStream', 'ZipOutputStream',
74
+ 'InflaterInputStream', 'DeflaterOutputStream', 'CheckedInputStream',
75
+ 'CheckedOutputStream',
76
+ ]);
77
+ /**
78
+ * #227 — Anonymous-class / functional-interface literal method names that
79
+ * commonly hold long-lived resources handed off from an outer scope. When a
80
+ * resource-field is closed inside one of these literals, ownership has been
81
+ * transferred to the worker and the outer method should not be flagged.
82
+ */
83
+ const WORKER_METHODS = new Set([
84
+ 'run', // Runnable, Thread
85
+ 'call', // Callable
86
+ 'accept', // Consumer
87
+ 'get', // Supplier
88
+ 'apply', // Function
89
+ ]);
59
90
  export class ResourceLeakPass {
60
91
  name = 'resource-leak';
61
92
  category = 'reliability';
@@ -103,6 +134,24 @@ export class ResourceLeakPass {
103
134
  if (this.isFactoryMethod(methodInfo.method)) {
104
135
  continue;
105
136
  }
137
+ // #226 — suppression 4: resource is immediately passed as a
138
+ // constructor argument to a known Closeable wrapper (e.g.
139
+ // `new GZIPInputStream(fis)`, `new BufferedReader(new
140
+ // InputStreamReader(is))`). Per JDK javadoc, closing the
141
+ // wrapper cascades to the underlying stream, so ownership
142
+ // transfers to the wrapper and the inner reference is released
143
+ // through the outer chain.
144
+ if (this.isWrappedByCloseableCtor(graph.ir.calls, resourceVar, openLine, methodEnd)) {
145
+ continue;
146
+ }
147
+ // #227 — suppression 5: resource is stored to a class field
148
+ // AND that field is referenced with a close() call inside a
149
+ // worker literal (Runnable#run / Callable#call / etc.) declared
150
+ // in the same method. Idiomatic Java concurrency: outer method
151
+ // opens the resource, executor thread closes it in cleanup.
152
+ if (this.isClosedInNestedWorker(graph, codeLines, resourceVar, methodInfo, openLine, methodEnd)) {
153
+ continue;
154
+ }
106
155
  // Look for a close() call on this resource within the method
107
156
  const closeCall = graph.ir.calls.find(c => CLOSE_METHODS.has(c.method_name) &&
108
157
  c.receiver === resourceVar &&
@@ -240,5 +289,94 @@ export class ResourceLeakPass {
240
289
  return false;
241
290
  return FACTORY_METHOD_NAME_RE.test(method.name);
242
291
  }
292
+ /**
293
+ * #226 — true if `variable` is passed as a constructor argument to a
294
+ * known Closeable wrapper call within the enclosing method's line
295
+ * range. Ownership of the inner stream transfers to the wrapper.
296
+ *
297
+ * The check is deliberately per-method: cross-method wrapping does
298
+ * not apply because the inner reference has already escaped the
299
+ * scope by then.
300
+ */
301
+ isWrappedByCloseableCtor(calls, variable, fromLine, toLine) {
302
+ // WRAPPER_CTORS names are PascalCase JDK class names — safe to
303
+ // match regardless of `is_constructor` (Java tree-sitter often
304
+ // omits the flag).
305
+ for (const call of calls) {
306
+ if (!WRAPPER_CTORS.has(call.method_name))
307
+ continue;
308
+ if (call.location.line < fromLine || call.location.line > toLine)
309
+ continue;
310
+ for (const arg of call.arguments) {
311
+ if (arg.variable === variable)
312
+ return true;
313
+ }
314
+ }
315
+ return false;
316
+ }
317
+ /**
318
+ * #227 — true if `variable` refers to a field of the enclosing class
319
+ * that is closed inside a worker-literal method (Runnable#run,
320
+ * Callable#call, Consumer#accept, ...) declared in the same enclosing
321
+ * method. Ownership transfers to the executor thread.
322
+ *
323
+ * The heuristic is intentionally conservative: it requires both
324
+ * (a) the resource variable to match a declared field name on the
325
+ * enclosing class (so a stray local named `selector` cannot
326
+ * accidentally opt in to the suppression), AND
327
+ * (b) a `<field>.close()` (or CLOSE_METHODS) call to appear on a
328
+ * line inside a method whose name is one of WORKER_METHODS and
329
+ * whose start_line is strictly within the enclosing method's
330
+ * body (nested literal indicator).
331
+ */
332
+ isClosedInNestedWorker(graph, lines, variable, methodInfo, fromLine, toLine) {
333
+ // (a) resource variable must match a declared field of the class,
334
+ // or be text-assigned to a field within the outer method.
335
+ const fieldNames = new Set(methodInfo.type.fields.map(f => f.name));
336
+ let candidateField = null;
337
+ if (fieldNames.has(variable)) {
338
+ candidateField = variable;
339
+ }
340
+ else {
341
+ // Fallback: `this.<field> = variable` or `<field> = variable`
342
+ // where <field> is a declared field.
343
+ const thisAssignRe = new RegExp(`(?:\\bthis\\s*\\.\\s*)?(\\w+)\\s*=\\s*${escapeRegex(variable)}\\b`);
344
+ for (let l = fromLine; l <= toLine && l <= lines.length; l++) {
345
+ const m = thisAssignRe.exec(lines[l - 1] ?? '');
346
+ if (m && fieldNames.has(m[1])) {
347
+ candidateField = m[1];
348
+ break;
349
+ }
350
+ }
351
+ }
352
+ if (!candidateField)
353
+ return false;
354
+ // (b) find a close-style call on the candidate field whose line
355
+ // falls inside a WORKER_METHODS method nested in the enclosing
356
+ // method's body.
357
+ for (const call of graph.ir.calls) {
358
+ if (call.receiver !== candidateField)
359
+ continue;
360
+ if (!CLOSE_METHODS.has(call.method_name))
361
+ continue;
362
+ const closeLine = call.location.line;
363
+ if (closeLine < fromLine || closeLine > toLine)
364
+ continue;
365
+ const enclosing = graph.methodAtLine(closeLine);
366
+ if (!enclosing)
367
+ continue;
368
+ // Same outer method — not nested.
369
+ if (enclosing.method === methodInfo.method)
370
+ continue;
371
+ if (!WORKER_METHODS.has(enclosing.method.name))
372
+ continue;
373
+ // Nested worker start must lie inside the outer method body.
374
+ if (enclosing.method.start_line > fromLine &&
375
+ enclosing.method.start_line <= toLine) {
376
+ return true;
377
+ }
378
+ }
379
+ return false;
380
+ }
243
381
  }
244
382
  //# sourceMappingURL=resource-leak-pass.js.map