circle-ir 4.7.2 → 4.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +34 -0
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.d.ts +1 -0
- package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.js +37 -3
- package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -1
- package/dist/analysis/passes/insecure-deserialization-config-pass.d.ts +2 -0
- package/dist/analysis/passes/insecure-deserialization-config-pass.d.ts.map +1 -1
- package/dist/analysis/passes/insecure-deserialization-config-pass.js +45 -2
- package/dist/analysis/passes/insecure-deserialization-config-pass.js.map +1 -1
- package/dist/analysis/passes/jwt-verify-disabled-pass.d.ts.map +1 -1
- package/dist/analysis/passes/jwt-verify-disabled-pass.js +48 -21
- package/dist/analysis/passes/jwt-verify-disabled-pass.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +131 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/tls-verify-disabled-pass.d.ts.map +1 -1
- package/dist/analysis/passes/tls-verify-disabled-pass.js +34 -0
- package/dist/analysis/passes/tls-verify-disabled-pass.js.map +1 -1
- package/dist/analysis/passes/weak-crypto-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-crypto-pass.js +32 -19
- package/dist/analysis/passes/weak-crypto-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +293 -25
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +14 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +660 -109
- package/dist/core/circle-ir-core.cjs +411 -68
- package/dist/core/circle-ir-core.js +411 -68
- package/dist/core/extractors/calls.js +17 -3
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/core/extractors/cfg.d.ts.map +1 -1
- package/dist/core/extractors/cfg.js +140 -40
- package/dist/core/extractors/cfg.js.map +1 -1
- package/dist/core/extractors/types.js +77 -2
- package/dist/core/extractors/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -371,9 +371,20 @@ function findSources(calls, types, patterns, sourceLines, language) {
|
|
|
371
371
|
// Check if parameter type could carry tainted data
|
|
372
372
|
// For typed languages (Java), check the type
|
|
373
373
|
// For untyped languages (JavaScript), treat all params as potentially tainted
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
374
|
+
//
|
|
375
|
+
// C# ASP.NET model binding: a `[FromBody]`/`[FromQuery]`/`[FromRoute]`/
|
|
376
|
+
// `[FromForm]`/`[FromHeader]` parameter is attacker-controlled even when
|
|
377
|
+
// its type is a custom DTO (not a primitive) — the whole object and its
|
|
378
|
+
// properties come from the request. Seed those regardless of type; the
|
|
379
|
+
// tainted-var scan bridges `d` and its member accesses (`d.Name`) to
|
|
380
|
+
// sinks. (cognium-dev#273 DTO-property propagation.)
|
|
381
|
+
const hasCSharpBindingAttr = language === 'csharp' &&
|
|
382
|
+
param.annotations.some((a) => CSHARP_BINDING_ATTRS.has(a));
|
|
383
|
+
const isTaintable = hasCSharpBindingAttr
|
|
384
|
+
? true
|
|
385
|
+
: param.type
|
|
386
|
+
? isInterproceduralTaintableType(param.type, language)
|
|
387
|
+
: true; // JavaScript/Python - no type means any value
|
|
377
388
|
if (isTaintable) {
|
|
378
389
|
// Use parameter line if available, fallback to method start line
|
|
379
390
|
const paramLine = param.line ?? method.start_line;
|
|
@@ -562,6 +573,10 @@ function findSources(calls, types, patterns, sourceLines, language) {
|
|
|
562
573
|
}
|
|
563
574
|
return result;
|
|
564
575
|
}
|
|
576
|
+
/** ASP.NET model-binding attributes — a so-annotated parameter is request-controlled. */
|
|
577
|
+
const CSHARP_BINDING_ATTRS = new Set([
|
|
578
|
+
'FromBody', 'FromQuery', 'FromRoute', 'FromForm', 'FromHeader',
|
|
579
|
+
]);
|
|
565
580
|
/**
|
|
566
581
|
* Check if a parameter type could carry tainted data in inter-procedural analysis.
|
|
567
582
|
* These are types commonly used to pass user-controlled data between methods.
|
|
@@ -908,36 +923,283 @@ function isSafeJSChildProcessCall(call, pattern, language) {
|
|
|
908
923
|
* arg) and a shell executable (`Process.Start("/bin/sh", "-c " + x)` — kept
|
|
909
924
|
* firing for #276) stay dangerous. A variable executable stays dangerous.
|
|
910
925
|
*/
|
|
911
|
-
|
|
912
|
-
|
|
926
|
+
const CSHARP_SHELL_PROGRAMS = new Set([
|
|
927
|
+
'sh', 'bash', 'zsh', 'dash', 'ash', 'ksh',
|
|
928
|
+
'cmd', 'powershell', 'pwsh',
|
|
929
|
+
]);
|
|
930
|
+
/** True when a `"prog"` / `@"C:\prog.exe"` literal names a non-shell program. */
|
|
931
|
+
function isConstNonShellExe(raw) {
|
|
932
|
+
if (!raw)
|
|
913
933
|
return false;
|
|
914
|
-
|
|
934
|
+
const t = raw.trim();
|
|
935
|
+
// constant string literal only ("git", @"C:\git.exe"); a variable exe is dangerous.
|
|
936
|
+
if (!/^@?"[^"]*"$/.test(t))
|
|
915
937
|
return false;
|
|
916
|
-
|
|
938
|
+
const program = (t.replace(/^@?"|"$/g, '').split(/[\\/]/).pop() ?? '')
|
|
939
|
+
.toLowerCase().replace(/\.exe$/, '');
|
|
940
|
+
return !CSHARP_SHELL_PROGRAMS.has(program);
|
|
941
|
+
}
|
|
942
|
+
// Extract the executable literal from a `new ProcessStartInfo("exe", …)`
|
|
943
|
+
// expression, or by resolving a bare `psi` variable back to its
|
|
944
|
+
// `psi = new ProcessStartInfo("exe", …)` construction in the file. Returns the
|
|
945
|
+
// raw literal (e.g. `"grep"`) or null when it cannot be determined statically.
|
|
946
|
+
const PSI_CTOR_EXE_RE = /\bnew\s+ProcessStartInfo\s*(?:<[^>]*>)?\s*\(\s*(@?"[^"]*")/;
|
|
947
|
+
function processStartInfoExe(expr, sourceLines) {
|
|
948
|
+
const inline = PSI_CTOR_EXE_RE.exec(expr);
|
|
949
|
+
if (inline)
|
|
950
|
+
return inline[1];
|
|
951
|
+
// Bare identifier — resolve its construction from the file text.
|
|
952
|
+
if (sourceLines && /^[A-Za-z_]\w*$/.test(expr.trim())) {
|
|
953
|
+
const varName = expr.trim();
|
|
954
|
+
const assignRe = new RegExp(`\\b${varName}\\s*=\\s*new\\s+ProcessStartInfo\\s*(?:<[^>]*>)?\\s*\\(\\s*(@?"[^"]*")`);
|
|
955
|
+
for (const line of sourceLines) {
|
|
956
|
+
const m = assignRe.exec(line);
|
|
957
|
+
if (m)
|
|
958
|
+
return m[1];
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
function isSafeCSharpProcessStartCall(call, pattern, language, sourceLines) {
|
|
964
|
+
if (language !== 'csharp')
|
|
917
965
|
return false;
|
|
918
|
-
|
|
919
|
-
if (call.arguments.length < 2)
|
|
966
|
+
if (pattern.type !== 'command_injection')
|
|
920
967
|
return false;
|
|
921
|
-
const
|
|
922
|
-
if (
|
|
968
|
+
const method = call.method_name;
|
|
969
|
+
if (method !== 'Start' && method !== 'ProcessStartInfo')
|
|
923
970
|
return false;
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
971
|
+
// argv forms: `Process.Start(exe, args)` and the inline
|
|
972
|
+
// `new ProcessStartInfo(exe, args)` constructor (cognium-ai#328 shape 2) —
|
|
973
|
+
// executable at [0], argument string at [1]. A constant non-shell exe means
|
|
974
|
+
// the arguments are passed argv (no shell), so a tainted [1] is not injection.
|
|
975
|
+
if (call.arguments.length >= 2) {
|
|
976
|
+
const fileArg = call.arguments.find(a => a.position === 0);
|
|
977
|
+
const raw = fileArg?.literal != null ? String(fileArg.literal) : fileArg?.expression;
|
|
978
|
+
return isConstNonShellExe(raw);
|
|
979
|
+
}
|
|
980
|
+
// Single-arg `Process.Start(psi)` where `psi` is an inline or variable
|
|
981
|
+
// ProcessStartInfo built with a constant non-shell exe — argv/`ArgumentList`
|
|
982
|
+
// or a non-shell `Arguments` string, still no shell (cognium-ai#328 shape 2,
|
|
983
|
+
// object-carried variant).
|
|
984
|
+
if (method === 'Start' && call.arguments.length === 1) {
|
|
985
|
+
const arg0 = call.arguments.find(a => a.position === 0);
|
|
986
|
+
const expr = (arg0?.expression ?? '').trim();
|
|
987
|
+
return isConstNonShellExe(processStartInfoExe(expr, sourceLines));
|
|
988
|
+
}
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
// ---------------------------------------------------------------------------
|
|
992
|
+
// C# SSRF inline host-allowlist guard (cognium-ai#328 shape 1).
|
|
993
|
+
//
|
|
994
|
+
// An exact string-equality check against a constant is the strongest allowlist;
|
|
995
|
+
// when it *dominates* the sink the flowing URL/host is pinned to a constant and
|
|
996
|
+
// there is no SSRF. Correctness hinges on the operator AND the sink's position,
|
|
997
|
+
// not merely the guard's presence:
|
|
998
|
+
// if (host == "c") sink(host); // == , sink INSIDE then → SAFE
|
|
999
|
+
// if (host != "c") return; sink(host);// != , then is early-exit → SAFE
|
|
1000
|
+
// if (host == "c") return; sink(host);// == , early-exit (blocklist) → UNSAFE
|
|
1001
|
+
// if (host != "c") sink(host); // != , sink INSIDE then → UNSAFE
|
|
1002
|
+
// A naive "an == guard appears above the sink" heuristic would wrongly suppress
|
|
1003
|
+
// the two blocklist forms, so we resolve the then-block structurally.
|
|
1004
|
+
/** Strip C# string/char literals and `//` comments so brace/paren scans are clean. */
|
|
1005
|
+
function stripCsLiterals(line) {
|
|
1006
|
+
return line
|
|
1007
|
+
.replace(/\/\/.*$/, '')
|
|
1008
|
+
.replace(/@?"(?:[^"\\]|\\.)*"/g, '""')
|
|
1009
|
+
.replace(/'(?:[^'\\]|\\.)'/g, "''");
|
|
1010
|
+
}
|
|
1011
|
+
/** Cumulative `{` depth before line `idx` (string/comment-safe). */
|
|
1012
|
+
function csBraceDepthBefore(lines, idx) {
|
|
1013
|
+
let depth = 0;
|
|
1014
|
+
for (let i = 0; i < idx && i < lines.length; i++) {
|
|
1015
|
+
for (const ch of stripCsLiterals(lines[i])) {
|
|
1016
|
+
if (ch === '{')
|
|
1017
|
+
depth++;
|
|
1018
|
+
else if (ch === '}')
|
|
1019
|
+
depth--;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return depth;
|
|
1023
|
+
}
|
|
1024
|
+
/** Net `{` minus `}` in a code fragment (caller strips literals first). */
|
|
1025
|
+
function netBraces(fragment) {
|
|
1026
|
+
let n = 0;
|
|
1027
|
+
for (const ch of fragment) {
|
|
1028
|
+
if (ch === '{')
|
|
1029
|
+
n++;
|
|
1030
|
+
else if (ch === '}')
|
|
1031
|
+
n--;
|
|
1032
|
+
}
|
|
1033
|
+
return n;
|
|
1034
|
+
}
|
|
1035
|
+
/** Split `if (cond) rest…` on one line; null if the condition spans lines. */
|
|
1036
|
+
function splitCsIf(line) {
|
|
1037
|
+
const m = /\bif\s*\(/.exec(line);
|
|
1038
|
+
if (!m)
|
|
1039
|
+
return null;
|
|
1040
|
+
const ifCol = m.index;
|
|
1041
|
+
let i = m.index + m[0].length;
|
|
1042
|
+
let depth = 1;
|
|
1043
|
+
let inStr = false;
|
|
1044
|
+
let strCh = '';
|
|
1045
|
+
for (; i < line.length; i++) {
|
|
1046
|
+
const c = line[i];
|
|
1047
|
+
if (inStr) {
|
|
1048
|
+
if (c === '\\') {
|
|
1049
|
+
i++;
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
if (c === strCh)
|
|
1053
|
+
inStr = false;
|
|
1054
|
+
continue;
|
|
1055
|
+
}
|
|
1056
|
+
if (c === '"' || c === "'") {
|
|
1057
|
+
inStr = true;
|
|
1058
|
+
strCh = c;
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
if (c === '(')
|
|
1062
|
+
depth++;
|
|
1063
|
+
else if (c === ')') {
|
|
1064
|
+
depth--;
|
|
1065
|
+
if (depth === 0)
|
|
1066
|
+
break;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
if (depth !== 0)
|
|
1070
|
+
return null;
|
|
1071
|
+
return { cond: line.slice(m.index + m[0].length, i), rest: line.slice(i + 1), ifCol };
|
|
1072
|
+
}
|
|
1073
|
+
/** Extract `{op, exprSide}` from a `EXPR == "lit"` / `"lit" != EXPR` condition. */
|
|
1074
|
+
function csEqualityGuard(cond) {
|
|
1075
|
+
const c = cond.trim();
|
|
1076
|
+
let m = /^(.*?)\s*(==|!=)\s*@?"[^"]*"\s*$/.exec(c);
|
|
1077
|
+
if (m)
|
|
1078
|
+
return { op: m[2], exprSide: m[1].trim() };
|
|
1079
|
+
m = /^@?"[^"]*"\s*(==|!=)\s*(.*)$/.exec(c);
|
|
1080
|
+
if (m)
|
|
1081
|
+
return { op: m[1], exprSide: m[2].trim() };
|
|
1082
|
+
return null;
|
|
1083
|
+
}
|
|
1084
|
+
const CS_IDENT_RE = /[A-Za-z_]\w*/g;
|
|
1085
|
+
function csIdentifiers(expr) {
|
|
1086
|
+
return new Set((expr.match(CS_IDENT_RE) ?? []));
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Resolve the then-block of an `if` at `ifIdx`, whose condition-line remainder
|
|
1090
|
+
* is `rest`. Returns the inclusive line range plus whether the block is an
|
|
1091
|
+
* early exit (only return/throw/continue/break).
|
|
1092
|
+
*/
|
|
1093
|
+
function csThenBlock(lines, ifIdx, rest) {
|
|
1094
|
+
const isExit = (s) => /^\s*(?:return|throw|continue|break)\b/.test(s);
|
|
1095
|
+
// The then-target begins in the if-line remainder (`rest`), else on the next
|
|
1096
|
+
// non-empty line. When it starts on the if-line we must scan only `rest`, not
|
|
1097
|
+
// the whole line — earlier braces on that line (class/method `{`) are not ours.
|
|
1098
|
+
let firstIdx;
|
|
1099
|
+
let firstText;
|
|
1100
|
+
if (rest.trim()) {
|
|
1101
|
+
firstIdx = ifIdx;
|
|
1102
|
+
firstText = rest;
|
|
927
1103
|
}
|
|
928
1104
|
else {
|
|
929
|
-
|
|
1105
|
+
let j = ifIdx + 1;
|
|
1106
|
+
while (j < lines.length && stripCsLiterals(lines[j]).trim() === '')
|
|
1107
|
+
j++;
|
|
1108
|
+
firstIdx = j;
|
|
1109
|
+
firstText = lines[j] ?? '';
|
|
1110
|
+
}
|
|
1111
|
+
if (firstText.trim().startsWith('{')) {
|
|
1112
|
+
// Braced block: brace-match from the block's own `{`, collecting the body so
|
|
1113
|
+
// early-exit is judged from the block's first statement, not the `if` line.
|
|
1114
|
+
let depth = 0;
|
|
1115
|
+
let started = false;
|
|
1116
|
+
let endLine = lines.length - 1;
|
|
1117
|
+
let body = '';
|
|
1118
|
+
for (let i = firstIdx; i < lines.length; i++) {
|
|
1119
|
+
const stripped = stripCsLiterals(i === firstIdx ? firstText : lines[i]);
|
|
1120
|
+
let broke = false;
|
|
1121
|
+
for (const ch of stripped) {
|
|
1122
|
+
if (ch === '{') {
|
|
1123
|
+
depth++;
|
|
1124
|
+
started = true;
|
|
1125
|
+
if (depth === 1)
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
else if (ch === '}') {
|
|
1129
|
+
depth--;
|
|
1130
|
+
if (depth === 0) {
|
|
1131
|
+
endLine = i;
|
|
1132
|
+
broke = true;
|
|
1133
|
+
break;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (started && depth >= 1)
|
|
1137
|
+
body += ch;
|
|
1138
|
+
}
|
|
1139
|
+
if (broke)
|
|
1140
|
+
break;
|
|
1141
|
+
if (started)
|
|
1142
|
+
body += ' ';
|
|
1143
|
+
}
|
|
1144
|
+
return { start: ifIdx, end: endLine, earlyExit: isExit(body.trim()) };
|
|
930
1145
|
}
|
|
931
|
-
//
|
|
932
|
-
|
|
1146
|
+
// Single-statement then (same line as the `if`, or the next line).
|
|
1147
|
+
return { start: ifIdx, end: firstIdx, earlyExit: isExit(firstText.trim()) };
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* True when a C# SSRF sink is dominated by an exact-equality host allowlist:
|
|
1151
|
+
* either `if (…var… == "const") <sink>` (sink inside the then-block) or
|
|
1152
|
+
* `if (…var… != "const") <early exit>` with the sink after the guard at the
|
|
1153
|
+
* same block depth. `var` is any identifier flowing into the sink argument.
|
|
1154
|
+
*/
|
|
1155
|
+
function isCSharpSsrfHostAllowlistGuarded(call, pattern, language, sourceLines) {
|
|
1156
|
+
if (language !== 'csharp')
|
|
933
1157
|
return false;
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1158
|
+
if (pattern.type !== 'ssrf')
|
|
1159
|
+
return false;
|
|
1160
|
+
if (!sourceLines || sourceLines.length === 0)
|
|
1161
|
+
return false;
|
|
1162
|
+
const candidates = new Set();
|
|
1163
|
+
for (const a of call.arguments) {
|
|
1164
|
+
if (a.variable)
|
|
1165
|
+
candidates.add(a.variable);
|
|
1166
|
+
for (const id of csIdentifiers(a.expression ?? ''))
|
|
1167
|
+
candidates.add(id);
|
|
1168
|
+
}
|
|
1169
|
+
if (candidates.size === 0)
|
|
1170
|
+
return false;
|
|
1171
|
+
const sinkIdx = call.location.line - 1;
|
|
1172
|
+
const sinkDepth = csBraceDepthBefore(sourceLines, sinkIdx);
|
|
1173
|
+
for (let i = 0; i < sinkIdx; i++) {
|
|
1174
|
+
const parts = splitCsIf(sourceLines[i]);
|
|
1175
|
+
if (!parts)
|
|
1176
|
+
continue;
|
|
1177
|
+
const guard = csEqualityGuard(parts.cond);
|
|
1178
|
+
if (!guard)
|
|
1179
|
+
continue;
|
|
1180
|
+
// The compared expression must reference a variable that flows to the sink.
|
|
1181
|
+
const guardIds = csIdentifiers(guard.exprSide);
|
|
1182
|
+
if (![...guardIds].some((id) => candidates.has(id)))
|
|
1183
|
+
continue;
|
|
1184
|
+
const block = csThenBlock(sourceLines, i, parts.rest);
|
|
1185
|
+
if (guard.op === '==') {
|
|
1186
|
+
// Positive allowlist: safe only when the sink sits inside the then-block.
|
|
1187
|
+
if (sinkIdx >= block.start && sinkIdx <= block.end)
|
|
1188
|
+
return true;
|
|
1189
|
+
}
|
|
1190
|
+
else {
|
|
1191
|
+
// Reject guard: safe only when the then-block is an early exit and the
|
|
1192
|
+
// sink follows it at the same enclosing depth (dominated by the guard).
|
|
1193
|
+
// The guard's depth includes any braces opened before `if` on its line
|
|
1194
|
+
// (compact `{ if (…)` layouts), so this is layout-independent.
|
|
1195
|
+
const guardDepth = csBraceDepthBefore(sourceLines, i) +
|
|
1196
|
+
netBraces(stripCsLiterals(sourceLines[i]).slice(0, parts.ifCol));
|
|
1197
|
+
if (block.earlyExit && sinkIdx > block.end && guardDepth === sinkDepth) {
|
|
1198
|
+
return true;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
return false;
|
|
941
1203
|
}
|
|
942
1204
|
/**
|
|
943
1205
|
* Check if a Rust `Command::new(...).arg(...).args(...).spawn().output()`
|
|
@@ -1559,7 +1821,13 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
|
|
|
1559
1821
|
// Skip C# `Process.Start(constNonShellExe, arguments)` argv calls —
|
|
1560
1822
|
// a non-shell executable receives the arguments directly (no shell),
|
|
1561
1823
|
// so tainted arguments cannot inject a command. cognium-ai#328.
|
|
1562
|
-
if (isSafeCSharpProcessStartCall(call, pattern, language)) {
|
|
1824
|
+
if (isSafeCSharpProcessStartCall(call, pattern, language, sourceLines)) {
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
// Skip C# SSRF sinks dominated by an inline exact-equality host
|
|
1828
|
+
// allowlist (`if (host == "c") sink` / `if (host != "c") return; sink`).
|
|
1829
|
+
// cognium-ai#328 shape 1.
|
|
1830
|
+
if (isCSharpSsrfHostAllowlistGuarded(call, pattern, language, sourceLines)) {
|
|
1563
1831
|
continue;
|
|
1564
1832
|
}
|
|
1565
1833
|
// Skip typed deserialization overloads where the target type is a
|