circle-ir 3.155.0 → 3.156.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,49 @@
1
+ /**
2
+ * MyBatisAnnotationSqlSinkPass — cognium-dev #241 Java
3
+ *
4
+ * Detects SQL injection sinks on MyBatis annotation-driven Mapper interface
5
+ * methods. MyBatis distinguishes two placeholder syntaxes in `@Select` /
6
+ * `@Update` / `@Insert` / `@Delete` (and their `*Provider` variants):
7
+ *
8
+ * - `#{name}` — JDBC PreparedStatement parameter binding. SAFE.
9
+ * - `${name}` — raw string interpolation into the SQL string. **SQLi.**
10
+ *
11
+ * The taint-matcher's YAML sink registry (`configs/sinks/sql.yaml`) covers
12
+ * standard Mapper method names (insert/update/select-wildcard/delete) via a
13
+ * generic `mybatis_mapper_call` discovery marker, but cannot inspect the
14
+ * contents of an annotation string. Custom method names (`findByName`,
15
+ * `getUserById`) with `${}` interpolation therefore fall through both the
16
+ * name-based Mapper matcher and any downstream SQLi flow generator.
17
+ *
18
+ * This pass closes that gap. For every Java interface whose method carries
19
+ * a MyBatis SQL annotation containing at least one `${varname}`, the pass
20
+ * records the tainted parameter positions (correlated to `@Param("name")`
21
+ * annotations or MyBatis positional convention `${param1}` / `${0}`), then
22
+ * walks `graph.calls` for call sites targeting those Mapper methods and
23
+ * emits a synthetic `sql_injection` (CWE-89) `TaintSink` at each match.
24
+ *
25
+ * The synthetic sinks are pushed onto the `TaintMatcherResult.sinks` list
26
+ * so `SinkFilterPass` (which merges taint-matcher + language-sources) and
27
+ * downstream flow generators pick them up without any registry changes.
28
+ *
29
+ * Pipeline slot: runs after `LanguageSourcesPass` and before
30
+ * `SinkFilterPass` so the synthetic sinks are visible to the four-stage
31
+ * FP-elimination filter and to `TaintPropagationPass` / `InterproceduralPass`.
32
+ *
33
+ * Language scope: Java only. Non-Java files short-circuit at run().
34
+ *
35
+ * Kill switch: opt out via `disabledPasses: ['mybatis-annotation-sql-sink']`.
36
+ */
37
+ import type { AnalysisPass, PassContext } from '../../graph/analysis-pass.js';
38
+ export interface MyBatisAnnotationSqlSinkResult {
39
+ /** Number of annotated Mapper methods with `${}` interpolation found. */
40
+ annotatedMethodCount: number;
41
+ /** Number of synthetic `sql_injection` sinks added. */
42
+ addedSinkCount: number;
43
+ }
44
+ export declare class MyBatisAnnotationSqlSinkPass implements AnalysisPass<MyBatisAnnotationSqlSinkResult> {
45
+ readonly name = "mybatis-annotation-sql-sink";
46
+ readonly category: "security";
47
+ run(ctx: PassContext): MyBatisAnnotationSqlSinkResult;
48
+ }
49
+ //# sourceMappingURL=mybatis-annotation-sql-sink-pass.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mybatis-annotation-sql-sink-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/mybatis-annotation-sql-sink-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAQH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAG9E,MAAM,WAAW,8BAA8B;IAC7C,yEAAyE;IACzE,oBAAoB,EAAE,MAAM,CAAC;IAC7B,uDAAuD;IACvD,cAAc,EAAE,MAAM,CAAC;CACxB;AAqND,qBAAa,4BACX,YAAW,YAAY,CAAC,8BAA8B,CAAC;IAEvD,QAAQ,CAAC,IAAI,iCAAiC;IAC9C,QAAQ,CAAC,QAAQ,EAAG,UAAU,CAAU;IAExC,GAAG,CAAC,GAAG,EAAE,WAAW,GAAG,8BAA8B;CA0HtD"}
@@ -0,0 +1,329 @@
1
+ /**
2
+ * MyBatisAnnotationSqlSinkPass — cognium-dev #241 Java
3
+ *
4
+ * Detects SQL injection sinks on MyBatis annotation-driven Mapper interface
5
+ * methods. MyBatis distinguishes two placeholder syntaxes in `@Select` /
6
+ * `@Update` / `@Insert` / `@Delete` (and their `*Provider` variants):
7
+ *
8
+ * - `#{name}` — JDBC PreparedStatement parameter binding. SAFE.
9
+ * - `${name}` — raw string interpolation into the SQL string. **SQLi.**
10
+ *
11
+ * The taint-matcher's YAML sink registry (`configs/sinks/sql.yaml`) covers
12
+ * standard Mapper method names (insert/update/select-wildcard/delete) via a
13
+ * generic `mybatis_mapper_call` discovery marker, but cannot inspect the
14
+ * contents of an annotation string. Custom method names (`findByName`,
15
+ * `getUserById`) with `${}` interpolation therefore fall through both the
16
+ * name-based Mapper matcher and any downstream SQLi flow generator.
17
+ *
18
+ * This pass closes that gap. For every Java interface whose method carries
19
+ * a MyBatis SQL annotation containing at least one `${varname}`, the pass
20
+ * records the tainted parameter positions (correlated to `@Param("name")`
21
+ * annotations or MyBatis positional convention `${param1}` / `${0}`), then
22
+ * walks `graph.calls` for call sites targeting those Mapper methods and
23
+ * emits a synthetic `sql_injection` (CWE-89) `TaintSink` at each match.
24
+ *
25
+ * The synthetic sinks are pushed onto the `TaintMatcherResult.sinks` list
26
+ * so `SinkFilterPass` (which merges taint-matcher + language-sources) and
27
+ * downstream flow generators pick them up without any registry changes.
28
+ *
29
+ * Pipeline slot: runs after `LanguageSourcesPass` and before
30
+ * `SinkFilterPass` so the synthetic sinks are visible to the four-stage
31
+ * FP-elimination filter and to `TaintPropagationPass` / `InterproceduralPass`.
32
+ *
33
+ * Language scope: Java only. Non-Java files short-circuit at run().
34
+ *
35
+ * Kill switch: opt out via `disabledPasses: ['mybatis-annotation-sql-sink']`.
36
+ */
37
+ /**
38
+ * MyBatis SQL annotations. The `*Provider` variants take a
39
+ * `type = X.class, method = "y"` pair whose provider method returns the
40
+ * SQL string; we still scan the annotation-attached literal (if any) for
41
+ * `${}` markers as a best-effort recall bump.
42
+ */
43
+ const MYBATIS_SQL_ANNOTATIONS = new Set([
44
+ 'Select',
45
+ 'Update',
46
+ 'Insert',
47
+ 'Delete',
48
+ 'SelectProvider',
49
+ 'UpdateProvider',
50
+ 'InsertProvider',
51
+ 'DeleteProvider',
52
+ ]);
53
+ /**
54
+ * Regex to extract `${varname}` interpolation markers from an annotation
55
+ * string. Deliberately does NOT match `#{name}` (safe binding).
56
+ * Variable names follow the MyBatis convention (identifier + optional
57
+ * dotted property access, e.g. `${user.id}`) — we only use the leading
58
+ * identifier when correlating to `@Param`.
59
+ */
60
+ const DOLLAR_BRACE_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?:\.[A-Za-z0-9_.]+)?\}/g;
61
+ /**
62
+ * MyBatis positional convention: `${param1}` refers to the first argument
63
+ * (1-based). `${0}` — some MyBatis versions — refers to the first argument
64
+ * (0-based). We map both forms to a 0-based index.
65
+ */
66
+ function parsePositionalRef(name) {
67
+ const paramMatch = /^param(\d+)$/.exec(name);
68
+ if (paramMatch) {
69
+ const oneBased = Number.parseInt(paramMatch[1], 10);
70
+ if (Number.isFinite(oneBased) && oneBased >= 1)
71
+ return oneBased - 1;
72
+ }
73
+ const zeroMatch = /^(\d+)$/.exec(name);
74
+ if (zeroMatch) {
75
+ const zeroBased = Number.parseInt(zeroMatch[1], 10);
76
+ if (Number.isFinite(zeroBased) && zeroBased >= 0)
77
+ return zeroBased;
78
+ }
79
+ return null;
80
+ }
81
+ /**
82
+ * Extract the raw annotation-argument string from a stored annotation.
83
+ * Annotations are stored as e.g. `Select("SELECT ... ${x} ...")` (without
84
+ * the leading `@`). We return the substring between the outermost
85
+ * parentheses, or `null` for a marker annotation with no arguments.
86
+ */
87
+ function extractAnnotationBody(annotation) {
88
+ const openIdx = annotation.indexOf('(');
89
+ if (openIdx < 0)
90
+ return null;
91
+ const closeIdx = annotation.lastIndexOf(')');
92
+ if (closeIdx <= openIdx)
93
+ return null;
94
+ return annotation.substring(openIdx + 1, closeIdx);
95
+ }
96
+ /**
97
+ * Given a stored annotation `Foo(...)`, return the leading identifier
98
+ * `Foo`. For marker annotations without parens, returns the whole string.
99
+ */
100
+ function annotationName(annotation) {
101
+ const openIdx = annotation.indexOf('(');
102
+ return openIdx < 0 ? annotation : annotation.substring(0, openIdx);
103
+ }
104
+ /**
105
+ * Return the value of the first string argument to a `@Param("name")`
106
+ * annotation, or `null` if the annotation is not `@Param` or has no
107
+ * literal string argument.
108
+ */
109
+ function extractParamName(annotation) {
110
+ if (annotationName(annotation) !== 'Param')
111
+ return null;
112
+ const body = extractAnnotationBody(annotation);
113
+ if (body === null)
114
+ return null;
115
+ // Match "..." or '...' at the start of the body. MyBatis @Param only
116
+ // takes a single string literal, so a simple match is sufficient.
117
+ const strMatch = /^\s*["']([^"']*)["']/.exec(body);
118
+ return strMatch ? strMatch[1] : null;
119
+ }
120
+ /**
121
+ * Extract every `${varname}` reference from a MyBatis annotation body
122
+ * (typically a single quoted string literal, sometimes concatenated).
123
+ * Returns the deduplicated ordered list of leading identifiers.
124
+ */
125
+ function extractDollarBraceRefs(annotationBody) {
126
+ const seen = new Set();
127
+ const out = [];
128
+ DOLLAR_BRACE_RE.lastIndex = 0;
129
+ let m;
130
+ while ((m = DOLLAR_BRACE_RE.exec(annotationBody)) !== null) {
131
+ const name = m[1];
132
+ if (!seen.has(name)) {
133
+ seen.add(name);
134
+ out.push(name);
135
+ }
136
+ }
137
+ return out;
138
+ }
139
+ /**
140
+ * Check if the file imports MyBatis annotations, either directly or via
141
+ * wildcard. Used to distinguish MyBatis `@Select` from unrelated libraries
142
+ * (e.g. Reactor's `@Select`, custom application `@Select`) that happen to
143
+ * share the simple annotation name.
144
+ */
145
+ function fileImportsMyBatis(imports) {
146
+ for (const imp of imports) {
147
+ const pkg = imp.from_package;
148
+ if (!pkg)
149
+ continue;
150
+ if (pkg === 'org.apache.ibatis.annotations' ||
151
+ pkg.startsWith('org.apache.ibatis.annotations.') ||
152
+ pkg === 'org.apache.ibatis' ||
153
+ pkg.startsWith('org.apache.ibatis.')) {
154
+ return true;
155
+ }
156
+ }
157
+ return false;
158
+ }
159
+ /**
160
+ * Correlate every `${name}` reference to a 0-based parameter index. When
161
+ * the method carries `@Param("name")` on any parameter, that mapping wins.
162
+ * Otherwise MyBatis positional convention is used (`${param1}`, `${0}`).
163
+ * Returns the deduplicated ascending list of tainted arg positions.
164
+ */
165
+ function correlateDollarBraceToArgPositions(method, refs) {
166
+ const paramNameToIndex = new Map();
167
+ method.parameters.forEach((param, idx) => {
168
+ for (const ann of param.annotations) {
169
+ const paramName = extractParamName(ann);
170
+ if (paramName !== null) {
171
+ paramNameToIndex.set(paramName, idx);
172
+ }
173
+ }
174
+ });
175
+ const positions = new Set();
176
+ for (const ref of refs) {
177
+ const namedIdx = paramNameToIndex.get(ref);
178
+ if (namedIdx !== undefined) {
179
+ positions.add(namedIdx);
180
+ continue;
181
+ }
182
+ const positionalIdx = parsePositionalRef(ref);
183
+ if (positionalIdx !== null &&
184
+ positionalIdx < method.parameters.length) {
185
+ positions.add(positionalIdx);
186
+ }
187
+ }
188
+ return Array.from(positions).sort((a, b) => a - b);
189
+ }
190
+ /**
191
+ * Return the fully-qualified name of a Mapper interface method for use as
192
+ * a callee lookup key. Falls back to `<simpleName>.<methodName>` when the
193
+ * interface has no declared package.
194
+ */
195
+ function mapperMethodKey(type, method) {
196
+ const iface = type.package ? `${type.package}.${type.name}` : type.name;
197
+ return `${iface}.${method.name}`;
198
+ }
199
+ /**
200
+ * Match a call site against a Mapper interface method by callee tail. We
201
+ * accept:
202
+ * - `resolution.target` equal to or ending with the mapper key
203
+ * - simple `method_name` equal to the mapper's method AND
204
+ * `receiver_type` or `receiver_type_fqn` equal to the mapper's
205
+ * interface (simple- or fully-qualified name).
206
+ *
207
+ * Conservative on both sides — unresolved receivers fall through.
208
+ */
209
+ function isMapperMethodCall(call, interfaceSimpleName, interfaceFqn, methodName) {
210
+ // Path 1 — resolution target tail match.
211
+ const target = call.resolution?.target;
212
+ if (target) {
213
+ const suffix = `${interfaceSimpleName}.${methodName}`;
214
+ const suffixFqn = `${interfaceFqn}.${methodName}`;
215
+ if (target === suffix ||
216
+ target === suffixFqn ||
217
+ target.endsWith('.' + suffix) ||
218
+ target.endsWith('.' + suffixFqn)) {
219
+ return true;
220
+ }
221
+ }
222
+ // Path 2 — method name + receiver type match.
223
+ if (call.method_name !== methodName)
224
+ return false;
225
+ if (call.receiver_type === interfaceSimpleName)
226
+ return true;
227
+ if (call.receiver_type_fqn === interfaceFqn)
228
+ return true;
229
+ return false;
230
+ }
231
+ export class MyBatisAnnotationSqlSinkPass {
232
+ name = 'mybatis-annotation-sql-sink';
233
+ category = 'security';
234
+ run(ctx) {
235
+ if (ctx.language !== 'java') {
236
+ return { annotatedMethodCount: 0, addedSinkCount: 0 };
237
+ }
238
+ const { types, imports, calls } = ctx.graph.ir;
239
+ // Gate on file-level MyBatis import to distinguish real Mapper
240
+ // interfaces from unrelated libraries reusing the `@Select` name.
241
+ if (!fileImportsMyBatis(imports)) {
242
+ return { annotatedMethodCount: 0, addedSinkCount: 0 };
243
+ }
244
+ const mapperMethods = [];
245
+ for (const type of types) {
246
+ if (type.kind !== 'interface')
247
+ continue;
248
+ for (const method of type.methods) {
249
+ // Find MyBatis SQL annotation, extract body, scan for `${}`.
250
+ let matchedRefs = [];
251
+ for (const ann of method.annotations) {
252
+ if (!MYBATIS_SQL_ANNOTATIONS.has(annotationName(ann)))
253
+ continue;
254
+ const body = extractAnnotationBody(ann);
255
+ if (body === null)
256
+ continue;
257
+ const refs = extractDollarBraceRefs(body);
258
+ if (refs.length > 0) {
259
+ matchedRefs = [...matchedRefs, ...refs];
260
+ }
261
+ }
262
+ if (matchedRefs.length === 0)
263
+ continue;
264
+ const positions = correlateDollarBraceToArgPositions(method, matchedRefs);
265
+ if (positions.length === 0)
266
+ continue;
267
+ const interfaceFqn = type.package
268
+ ? `${type.package}.${type.name}`
269
+ : type.name;
270
+ mapperMethods.push({
271
+ interfaceSimpleName: type.name,
272
+ interfaceFqn,
273
+ methodName: method.name,
274
+ taintedArgPositions: positions,
275
+ });
276
+ }
277
+ }
278
+ if (mapperMethods.length === 0) {
279
+ return {
280
+ annotatedMethodCount: 0,
281
+ addedSinkCount: 0,
282
+ };
283
+ }
284
+ // Grab the taint-matcher sinks array to append into. When the pass is
285
+ // run stand-alone (unit-test harness) without TaintMatcherPass, we
286
+ // fall back to graph.ir.taint.sinks so tests can still observe the
287
+ // added sinks.
288
+ const sinks = ctx.hasResult('taint-matcher')
289
+ ? ctx.getResult('taint-matcher').sinks
290
+ : ctx.graph.ir.taint.sinks;
291
+ let addedSinkCount = 0;
292
+ for (const call of calls) {
293
+ for (const rec of mapperMethods) {
294
+ if (!isMapperMethodCall(call, rec.interfaceSimpleName, rec.interfaceFqn, rec.methodName)) {
295
+ continue;
296
+ }
297
+ // Dedup: skip if a `sql_injection` sink already exists at this
298
+ // call site.
299
+ const line = call.location.line;
300
+ if (sinks.some((s) => s.line === line &&
301
+ s.type === 'sql_injection' &&
302
+ s.method === rec.methodName)) {
303
+ continue;
304
+ }
305
+ const receiverLoc = call.receiver
306
+ ? `${call.receiver}.${rec.methodName}()`
307
+ : `${rec.methodName}()`;
308
+ sinks.push({
309
+ type: 'sql_injection',
310
+ cwe: 'CWE-89',
311
+ location: call.in_method
312
+ ? `${receiverLoc} in ${call.in_method}`
313
+ : receiverLoc,
314
+ line,
315
+ confidence: 0.95,
316
+ method: rec.methodName,
317
+ argPositions: rec.taintedArgPositions,
318
+ class: rec.interfaceSimpleName,
319
+ });
320
+ addedSinkCount++;
321
+ }
322
+ }
323
+ return {
324
+ annotatedMethodCount: mapperMethods.length,
325
+ addedSinkCount,
326
+ };
327
+ }
328
+ }
329
+ //# sourceMappingURL=mybatis-annotation-sql-sink-pass.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mybatis-annotation-sql-sink-pass.js","sourceRoot":"","sources":["../../../src/analysis/passes/mybatis-annotation-sql-sink-pass.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"taint-matcher.d.ts","sourceRoot":"","sources":["../../src/analysis/taint-matcher.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAgB,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAkB,KAAK,EAAwB,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAClK,OAAO,KAAK,EAAE,WAAW,EAAiB,WAAW,EAAoB,MAAM,oBAAoB,CAAC;AACpG,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AA2B7E;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,QAAQ,EAAE,EACjB,KAAK,EAAE,QAAQ,EAAE,EACjB,MAAM,GAAE,WAAgC,EACxC,aAAa,CAAC,EAAE,qBAAqB,EACrC,QAAQ,CAAC,EAAE,iBAAiB,EAC5B,IAAI,CAAC,EAAE,MAAM,GACZ,KAAK,CASP;AAqID;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,WAAW,EAAE,EACtB,KAAK,EAAE,SAAS,EAAE,EAClB,IAAI,EAAE,MAAM,GACX,IAAI,CAYN;AA2qED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,WAAW,GACnB,OAAO,CAET"}
1
+ {"version":3,"file":"taint-matcher.d.ts","sourceRoot":"","sources":["../../src/analysis/taint-matcher.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAgB,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAkB,KAAK,EAAwB,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAClK,OAAO,KAAK,EAAE,WAAW,EAAiB,WAAW,EAAoB,MAAM,oBAAoB,CAAC;AACpG,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AA2B7E;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,QAAQ,EAAE,EACjB,KAAK,EAAE,QAAQ,EAAE,EACjB,MAAM,GAAE,WAAgC,EACxC,aAAa,CAAC,EAAE,qBAAqB,EACrC,QAAQ,CAAC,EAAE,iBAAiB,EAC5B,IAAI,CAAC,EAAE,MAAM,GACZ,KAAK,CASP;AAqID;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,WAAW,EAAE,EACtB,KAAK,EAAE,SAAS,EAAE,EAClB,IAAI,EAAE,MAAM,GACX,IAAI,CAYN;AA6sED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,WAAW,GACnB,OAAO,CAET"}
@@ -1799,12 +1799,42 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
1799
1799
  // Prefer the IR-resolved receiver type when the language plugin populates it
1800
1800
  // (Java/TS resolve simple types). Falls back to the receiver-name heuristic
1801
1801
  // when the type is unresolved.
1802
+ //
1803
+ // Subtype-based matches (`isSubtypeOf`) are additionally gated on the call
1804
+ // site providing the arguments the sink actually inspects — otherwise the
1805
+ // JDBC-style `PreparedStatement.executeQuery()` (0 args) would spuriously
1806
+ // match `Statement.executeQuery(String)` (arg_positions [0]) via the
1807
+ // `PreparedStatement extends Statement` fact. The direct-name path is not
1808
+ // gated to preserve legacy behaviour where sink pattern arity was checked
1809
+ // downstream. — cognium-dev #241 Java
1810
+ const subtypeArgArityOk = !pattern.arg_positions ||
1811
+ pattern.arg_positions.length === 0 ||
1812
+ pattern.arg_positions.every((pos) => pos < (call.arguments?.length ?? 0));
1802
1813
  if (call.receiver_type && call.receiver_type === pattern.class) {
1803
1814
  // Resolved type matches — accept directly.
1804
1815
  }
1816
+ else if (call.receiver_type &&
1817
+ typeHierarchy &&
1818
+ subtypeArgArityOk &&
1819
+ typeHierarchy.isSubtypeOf(call.receiver_type, pattern.class)) {
1820
+ // Resolved type is a subtype of the sink class via TypeHierarchyResolver.
1821
+ // Enables sink patterns keyed on a base interface/class (e.g.
1822
+ // `HttpClient.execute`) to match subtype receivers (Apache
1823
+ // `CloseableHttpClient`, `InternalHttpClient`, etc.) whose hierarchy is
1824
+ // pre-registered via `registerCommonLibraries()`. — cognium-dev #241 Java
1825
+ }
1805
1826
  else if (call.receiver_type_fqn && call.receiver_type_fqn.endsWith('.' + pattern.class)) {
1806
1827
  // FQN tail matches simple class name.
1807
1828
  }
1829
+ else if (call.receiver_type_fqn &&
1830
+ typeHierarchy &&
1831
+ subtypeArgArityOk &&
1832
+ typeHierarchy.isSubtypeOf(call.receiver_type_fqn, pattern.class)) {
1833
+ // FQN receiver type is a subtype of the sink class via
1834
+ // TypeHierarchyResolver. Same rationale as the simple-name subtype
1835
+ // check above; covers the case where the language plugin resolves the
1836
+ // receiver to a fully qualified name.
1837
+ }
1808
1838
  else if (call.receiver && !receiverMightBeClass(call.receiver, pattern.class)) {
1809
1839
  // Heuristic match failed; fall back to TypeHierarchyResolver if available
1810
1840
  if (typeHierarchy && typeHierarchy.couldBeType(call.receiver, pattern.class)) {