circle-ir 4.9.13 → 4.9.15
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 +25 -5
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/literal-expression.d.ts +33 -0
- package/dist/analysis/literal-expression.d.ts.map +1 -0
- package/dist/analysis/literal-expression.js +61 -0
- package/dist/analysis/literal-expression.js.map +1 -0
- package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
- package/dist/analysis/passes/cross-file-pass.js +7 -4
- package/dist/analysis/passes/cross-file-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 +35 -1
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +131 -4
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts +48 -0
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +166 -1
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +79 -2
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analyzer.d.ts +22 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +20 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +254 -20
- package/dist/core/circle-ir-core.cjs +81 -7
- package/dist/core/circle-ir-core.js +81 -7
- package/dist/core/extractors/calls.js +19 -0
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/resolution/cross-file.d.ts +19 -3
- package/dist/resolution/cross-file.d.ts.map +1 -1
- package/dist/resolution/cross-file.js +25 -3
- package/dist/resolution/cross-file.js.map +1 -1
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -6746,6 +6746,14 @@ function buildResolutionContext(tree, cache) {
|
|
|
6746
6746
|
}
|
|
6747
6747
|
}
|
|
6748
6748
|
}
|
|
6749
|
+
const resourceDecls = getNodesFromCache(tree.rootNode, "resource", cache);
|
|
6750
|
+
for (const res of resourceDecls) {
|
|
6751
|
+
const typeNode = res.childForFieldName("type");
|
|
6752
|
+
const nameNode = res.childForFieldName("name");
|
|
6753
|
+
if (typeNode && nameNode) {
|
|
6754
|
+
context.localVarTypes.set(getNodeText(nameNode), getNodeText(typeNode));
|
|
6755
|
+
}
|
|
6756
|
+
}
|
|
6749
6757
|
const imports = getNodesFromCache(tree.rootNode, "import_declaration", cache);
|
|
6750
6758
|
for (const imp of imports) {
|
|
6751
6759
|
const text = getNodeText(imp);
|
|
@@ -10858,6 +10866,30 @@ function extractGoAddressableVarName(node) {
|
|
|
10858
10866
|
return null;
|
|
10859
10867
|
}
|
|
10860
10868
|
|
|
10869
|
+
// src/analysis/literal-expression.ts
|
|
10870
|
+
function isProvablyLiteralExpression(expr) {
|
|
10871
|
+
let e = (expr ?? "").trim();
|
|
10872
|
+
if (e.length === 0) return false;
|
|
10873
|
+
e = e.replace(/\/\/.*$/, "").replace(/;\s*$/, "").trim();
|
|
10874
|
+
if (e.length === 0) return false;
|
|
10875
|
+
if (e.includes("${")) return false;
|
|
10876
|
+
if (/[+]\s*$/.test(e)) return false;
|
|
10877
|
+
const stripped = e.replace(
|
|
10878
|
+
/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\$]|\\.)*`/g,
|
|
10879
|
+
""
|
|
10880
|
+
);
|
|
10881
|
+
if (/["'`]/.test(stripped)) return false;
|
|
10882
|
+
return /^[\s+()]*$/.test(stripped);
|
|
10883
|
+
}
|
|
10884
|
+
var DOM_ASSIGNMENT_SINK_METHODS = /* @__PURE__ */ new Set([
|
|
10885
|
+
"innerHTML",
|
|
10886
|
+
"outerHTML",
|
|
10887
|
+
"cssText",
|
|
10888
|
+
"src",
|
|
10889
|
+
"href",
|
|
10890
|
+
"textContent"
|
|
10891
|
+
]);
|
|
10892
|
+
|
|
10861
10893
|
// src/analysis/config-loader.ts
|
|
10862
10894
|
function parseConfig(content) {
|
|
10863
10895
|
return JSON.parse(content);
|
|
@@ -11767,9 +11799,27 @@ var DEFAULT_SINKS = [
|
|
|
11767
11799
|
// Command Injection (CWE-78)
|
|
11768
11800
|
{ method: "exec", class: "Runtime", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1] },
|
|
11769
11801
|
{ method: "start", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [] },
|
|
11770
|
-
// ProcessBuilder constructor
|
|
11771
|
-
|
|
11772
|
-
|
|
11802
|
+
// ProcessBuilder constructor + command(): cognium-dev#351 — BOTH take
|
|
11803
|
+
// `String...` varargs (or a single List<String>), and EVERY element is a
|
|
11804
|
+
// command token, so no position is safe. Pinning these to `[0]` missed the
|
|
11805
|
+
// canonical shell-wrapper shape, where the payload is the last argument:
|
|
11806
|
+
//
|
|
11807
|
+
// new ProcessBuilder("bash", "-c", tainted) // payload at position 2
|
|
11808
|
+
// pb.command("bash", "-c", tainted)
|
|
11809
|
+
//
|
|
11810
|
+
// `Runtime.exec(String[])` with the identical array already fired, so the
|
|
11811
|
+
// taint was available and only the sink model was wrong.
|
|
11812
|
+
//
|
|
11813
|
+
// The positions are enumerated rather than left `[]` because the two layers
|
|
11814
|
+
// that read this field disagree about what an empty array means:
|
|
11815
|
+
// `isInDangerousPosition` (taint-matcher) does `arg_positions.includes(pos)`,
|
|
11816
|
+
// so `[]` matches NOTHING, while the sink-reachability loop in
|
|
11817
|
+
// taint-propagation treats `length === 0` as "any argument".
|
|
11818
|
+
// `ProcessBuilder.start` gets away with `[]` only because it takes no
|
|
11819
|
+
// arguments at all. Enumerating is unambiguous in both, and matches how the
|
|
11820
|
+
// logging sinks in this file already spell it.
|
|
11821
|
+
{ method: "ProcessBuilder", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1, 2, 3, 4, 5, 6, 7] },
|
|
11822
|
+
{ method: "command", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1, 2, 3, 4, 5, 6, 7] },
|
|
11773
11823
|
// Commons Exec
|
|
11774
11824
|
// Note: bare class 'Executor' removed — it collided with java.util.concurrent.Executor
|
|
11775
11825
|
// (Executor.execute(Runnable) is not command injection). Apache Commons Exec users
|
|
@@ -13047,8 +13097,10 @@ var DEFAULT_SINKS = [
|
|
|
13047
13097
|
// =========================================================================
|
|
13048
13098
|
// Java CWE-Bench Enhancement Patterns (Collection/Builder)
|
|
13049
13099
|
// =========================================================================
|
|
13050
|
-
// Collection-based command injection (ProcessBuilder with List)
|
|
13051
|
-
|
|
13100
|
+
// Collection-based command injection (ProcessBuilder with List) — the
|
|
13101
|
+
// `ProcessBuilder.command` entry lives with the other ProcessBuilder sinks
|
|
13102
|
+
// above; a second copy here only risked the two drifting apart, which is
|
|
13103
|
+
// what happened (cognium-dev#351).
|
|
13052
13104
|
// ProcessBuilder.inheritIO removed in 3.83.0 (#124): no args, no command
|
|
13053
13105
|
// string flows into it. See note above next to the Process-related cluster.
|
|
13054
13106
|
// Jenkins DSL patterns
|
|
@@ -15303,6 +15355,22 @@ function isRegexReceiver(receiver, sourceLines) {
|
|
|
15303
15355
|
const decl = new RegExp(`(?:const|let|var)\\s+${escapeRe(r)}\\s*=\\s*(?:/|(?:new\\s+)?RegExp\\s*\\()`);
|
|
15304
15356
|
return sourceLines.some((l) => decl.test(l));
|
|
15305
15357
|
}
|
|
15358
|
+
function isDeclaredFunctionReference(arg, sourceLines) {
|
|
15359
|
+
if (arg.literal !== null && arg.literal !== void 0) return false;
|
|
15360
|
+
if (!sourceLines) return false;
|
|
15361
|
+
const name2 = (arg.expression ?? "").trim();
|
|
15362
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(name2)) return false;
|
|
15363
|
+
const n = escapeRe(name2);
|
|
15364
|
+
const decls = [
|
|
15365
|
+
// function connect() {} / async function connect() {}
|
|
15366
|
+
new RegExp(`\\bfunction\\s+${n}\\s*\\(`),
|
|
15367
|
+
// const connect = () => / function / async () =>
|
|
15368
|
+
new RegExp(
|
|
15369
|
+
`\\b(?:const|let|var)\\s+${n}\\s*=\\s*(?:async\\s+)?(?:function\\b|\\(|[A-Za-z_$][\\w$]*\\s*=>)`
|
|
15370
|
+
)
|
|
15371
|
+
];
|
|
15372
|
+
return sourceLines.some((l) => decls.some((re) => re.test(l)));
|
|
15373
|
+
}
|
|
15306
15374
|
function isFunctionCallbackArgument(arg) {
|
|
15307
15375
|
if (arg.literal !== null && arg.literal !== void 0) return false;
|
|
15308
15376
|
const expr = (arg.expression ?? "").trim();
|
|
@@ -15481,11 +15549,17 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
|
|
|
15481
15549
|
}
|
|
15482
15550
|
if (pattern.type === "code_injection" && (call.method_name === "setInterval" || call.method_name === "setTimeout" || call.method_name === "setImmediate")) {
|
|
15483
15551
|
const firstArg = call.arguments.find((a) => a.position === 0);
|
|
15484
|
-
if (firstArg && isFunctionCallbackArgument(firstArg)) {
|
|
15552
|
+
if (firstArg && (isFunctionCallbackArgument(firstArg) || isDeclaredFunctionReference(firstArg, sourceLines))) {
|
|
15553
|
+
continue;
|
|
15554
|
+
}
|
|
15555
|
+
}
|
|
15556
|
+
if (pattern.type === "xss" && (language === "javascript" || language === "typescript") && DOM_ASSIGNMENT_SINK_METHODS.has(call.method_name) && call.arguments.length > 0) {
|
|
15557
|
+
const rhs = call.arguments.find((a) => a.position === 0);
|
|
15558
|
+
if (rhs && isProvablyLiteralExpression(rhs.expression ?? "")) {
|
|
15485
15559
|
continue;
|
|
15486
15560
|
}
|
|
15487
15561
|
}
|
|
15488
|
-
if (pattern.type === "command_injection" && call.method_name === "exec" && (language === "javascript" || language === "typescript") && isRegexReceiver(call.receiver, sourceLines)) {
|
|
15562
|
+
if ((pattern.type === "command_injection" || pattern.type === "sql_injection") && call.method_name === "exec" && (language === "javascript" || language === "typescript") && isRegexReceiver(call.receiver, sourceLines)) {
|
|
15489
15563
|
continue;
|
|
15490
15564
|
}
|
|
15491
15565
|
if (pattern.type === "code_injection" && language === "python" && call.method_name === "compile" && call.receiver && call.receiver !== "builtins") {
|
|
@@ -6680,6 +6680,14 @@ function buildResolutionContext(tree, cache) {
|
|
|
6680
6680
|
}
|
|
6681
6681
|
}
|
|
6682
6682
|
}
|
|
6683
|
+
const resourceDecls = getNodesFromCache(tree.rootNode, "resource", cache);
|
|
6684
|
+
for (const res of resourceDecls) {
|
|
6685
|
+
const typeNode = res.childForFieldName("type");
|
|
6686
|
+
const nameNode = res.childForFieldName("name");
|
|
6687
|
+
if (typeNode && nameNode) {
|
|
6688
|
+
context.localVarTypes.set(getNodeText(nameNode), getNodeText(typeNode));
|
|
6689
|
+
}
|
|
6690
|
+
}
|
|
6683
6691
|
const imports = getNodesFromCache(tree.rootNode, "import_declaration", cache);
|
|
6684
6692
|
for (const imp of imports) {
|
|
6685
6693
|
const text = getNodeText(imp);
|
|
@@ -10792,6 +10800,30 @@ function extractGoAddressableVarName(node) {
|
|
|
10792
10800
|
return null;
|
|
10793
10801
|
}
|
|
10794
10802
|
|
|
10803
|
+
// src/analysis/literal-expression.ts
|
|
10804
|
+
function isProvablyLiteralExpression(expr) {
|
|
10805
|
+
let e = (expr ?? "").trim();
|
|
10806
|
+
if (e.length === 0) return false;
|
|
10807
|
+
e = e.replace(/\/\/.*$/, "").replace(/;\s*$/, "").trim();
|
|
10808
|
+
if (e.length === 0) return false;
|
|
10809
|
+
if (e.includes("${")) return false;
|
|
10810
|
+
if (/[+]\s*$/.test(e)) return false;
|
|
10811
|
+
const stripped = e.replace(
|
|
10812
|
+
/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\$]|\\.)*`/g,
|
|
10813
|
+
""
|
|
10814
|
+
);
|
|
10815
|
+
if (/["'`]/.test(stripped)) return false;
|
|
10816
|
+
return /^[\s+()]*$/.test(stripped);
|
|
10817
|
+
}
|
|
10818
|
+
var DOM_ASSIGNMENT_SINK_METHODS = /* @__PURE__ */ new Set([
|
|
10819
|
+
"innerHTML",
|
|
10820
|
+
"outerHTML",
|
|
10821
|
+
"cssText",
|
|
10822
|
+
"src",
|
|
10823
|
+
"href",
|
|
10824
|
+
"textContent"
|
|
10825
|
+
]);
|
|
10826
|
+
|
|
10795
10827
|
// src/analysis/config-loader.ts
|
|
10796
10828
|
function parseConfig(content) {
|
|
10797
10829
|
return JSON.parse(content);
|
|
@@ -11701,9 +11733,27 @@ var DEFAULT_SINKS = [
|
|
|
11701
11733
|
// Command Injection (CWE-78)
|
|
11702
11734
|
{ method: "exec", class: "Runtime", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1] },
|
|
11703
11735
|
{ method: "start", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [] },
|
|
11704
|
-
// ProcessBuilder constructor
|
|
11705
|
-
|
|
11706
|
-
|
|
11736
|
+
// ProcessBuilder constructor + command(): cognium-dev#351 — BOTH take
|
|
11737
|
+
// `String...` varargs (or a single List<String>), and EVERY element is a
|
|
11738
|
+
// command token, so no position is safe. Pinning these to `[0]` missed the
|
|
11739
|
+
// canonical shell-wrapper shape, where the payload is the last argument:
|
|
11740
|
+
//
|
|
11741
|
+
// new ProcessBuilder("bash", "-c", tainted) // payload at position 2
|
|
11742
|
+
// pb.command("bash", "-c", tainted)
|
|
11743
|
+
//
|
|
11744
|
+
// `Runtime.exec(String[])` with the identical array already fired, so the
|
|
11745
|
+
// taint was available and only the sink model was wrong.
|
|
11746
|
+
//
|
|
11747
|
+
// The positions are enumerated rather than left `[]` because the two layers
|
|
11748
|
+
// that read this field disagree about what an empty array means:
|
|
11749
|
+
// `isInDangerousPosition` (taint-matcher) does `arg_positions.includes(pos)`,
|
|
11750
|
+
// so `[]` matches NOTHING, while the sink-reachability loop in
|
|
11751
|
+
// taint-propagation treats `length === 0` as "any argument".
|
|
11752
|
+
// `ProcessBuilder.start` gets away with `[]` only because it takes no
|
|
11753
|
+
// arguments at all. Enumerating is unambiguous in both, and matches how the
|
|
11754
|
+
// logging sinks in this file already spell it.
|
|
11755
|
+
{ method: "ProcessBuilder", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1, 2, 3, 4, 5, 6, 7] },
|
|
11756
|
+
{ method: "command", class: "ProcessBuilder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1, 2, 3, 4, 5, 6, 7] },
|
|
11707
11757
|
// Commons Exec
|
|
11708
11758
|
// Note: bare class 'Executor' removed — it collided with java.util.concurrent.Executor
|
|
11709
11759
|
// (Executor.execute(Runnable) is not command injection). Apache Commons Exec users
|
|
@@ -12981,8 +13031,10 @@ var DEFAULT_SINKS = [
|
|
|
12981
13031
|
// =========================================================================
|
|
12982
13032
|
// Java CWE-Bench Enhancement Patterns (Collection/Builder)
|
|
12983
13033
|
// =========================================================================
|
|
12984
|
-
// Collection-based command injection (ProcessBuilder with List)
|
|
12985
|
-
|
|
13034
|
+
// Collection-based command injection (ProcessBuilder with List) — the
|
|
13035
|
+
// `ProcessBuilder.command` entry lives with the other ProcessBuilder sinks
|
|
13036
|
+
// above; a second copy here only risked the two drifting apart, which is
|
|
13037
|
+
// what happened (cognium-dev#351).
|
|
12986
13038
|
// ProcessBuilder.inheritIO removed in 3.83.0 (#124): no args, no command
|
|
12987
13039
|
// string flows into it. See note above next to the Process-related cluster.
|
|
12988
13040
|
// Jenkins DSL patterns
|
|
@@ -15237,6 +15289,22 @@ function isRegexReceiver(receiver, sourceLines) {
|
|
|
15237
15289
|
const decl = new RegExp(`(?:const|let|var)\\s+${escapeRe(r)}\\s*=\\s*(?:/|(?:new\\s+)?RegExp\\s*\\()`);
|
|
15238
15290
|
return sourceLines.some((l) => decl.test(l));
|
|
15239
15291
|
}
|
|
15292
|
+
function isDeclaredFunctionReference(arg, sourceLines) {
|
|
15293
|
+
if (arg.literal !== null && arg.literal !== void 0) return false;
|
|
15294
|
+
if (!sourceLines) return false;
|
|
15295
|
+
const name2 = (arg.expression ?? "").trim();
|
|
15296
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(name2)) return false;
|
|
15297
|
+
const n = escapeRe(name2);
|
|
15298
|
+
const decls = [
|
|
15299
|
+
// function connect() {} / async function connect() {}
|
|
15300
|
+
new RegExp(`\\bfunction\\s+${n}\\s*\\(`),
|
|
15301
|
+
// const connect = () => / function / async () =>
|
|
15302
|
+
new RegExp(
|
|
15303
|
+
`\\b(?:const|let|var)\\s+${n}\\s*=\\s*(?:async\\s+)?(?:function\\b|\\(|[A-Za-z_$][\\w$]*\\s*=>)`
|
|
15304
|
+
)
|
|
15305
|
+
];
|
|
15306
|
+
return sourceLines.some((l) => decls.some((re) => re.test(l)));
|
|
15307
|
+
}
|
|
15240
15308
|
function isFunctionCallbackArgument(arg) {
|
|
15241
15309
|
if (arg.literal !== null && arg.literal !== void 0) return false;
|
|
15242
15310
|
const expr = (arg.expression ?? "").trim();
|
|
@@ -15415,11 +15483,17 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
|
|
|
15415
15483
|
}
|
|
15416
15484
|
if (pattern.type === "code_injection" && (call.method_name === "setInterval" || call.method_name === "setTimeout" || call.method_name === "setImmediate")) {
|
|
15417
15485
|
const firstArg = call.arguments.find((a) => a.position === 0);
|
|
15418
|
-
if (firstArg && isFunctionCallbackArgument(firstArg)) {
|
|
15486
|
+
if (firstArg && (isFunctionCallbackArgument(firstArg) || isDeclaredFunctionReference(firstArg, sourceLines))) {
|
|
15487
|
+
continue;
|
|
15488
|
+
}
|
|
15489
|
+
}
|
|
15490
|
+
if (pattern.type === "xss" && (language === "javascript" || language === "typescript") && DOM_ASSIGNMENT_SINK_METHODS.has(call.method_name) && call.arguments.length > 0) {
|
|
15491
|
+
const rhs = call.arguments.find((a) => a.position === 0);
|
|
15492
|
+
if (rhs && isProvablyLiteralExpression(rhs.expression ?? "")) {
|
|
15419
15493
|
continue;
|
|
15420
15494
|
}
|
|
15421
15495
|
}
|
|
15422
|
-
if (pattern.type === "command_injection" && call.method_name === "exec" && (language === "javascript" || language === "typescript") && isRegexReceiver(call.receiver, sourceLines)) {
|
|
15496
|
+
if ((pattern.type === "command_injection" || pattern.type === "sql_injection") && call.method_name === "exec" && (language === "javascript" || language === "typescript") && isRegexReceiver(call.receiver, sourceLines)) {
|
|
15423
15497
|
continue;
|
|
15424
15498
|
}
|
|
15425
15499
|
if (pattern.type === "code_injection" && language === "python" && call.method_name === "compile" && call.receiver && call.receiver !== "builtins") {
|
|
@@ -1049,6 +1049,25 @@ function buildResolutionContext(tree, cache) {
|
|
|
1049
1049
|
}
|
|
1050
1050
|
}
|
|
1051
1051
|
}
|
|
1052
|
+
// cognium-dev#350 — try-with-resources declarations are ALSO local variables.
|
|
1053
|
+
//
|
|
1054
|
+
// try (Connection c = …; Statement s = c.createStatement()) { s.executeQuery(q); }
|
|
1055
|
+
//
|
|
1056
|
+
// A resource is a `resource` node under `resource_specification`, and it holds
|
|
1057
|
+
// its type and name directly — there is no `variable_declarator` child — so
|
|
1058
|
+
// the loop above never sees it. Without an entry here the receiver type stays
|
|
1059
|
+
// `null`, the class-scoped `Statement.executeQuery` sink cannot match, and the
|
|
1060
|
+
// call degrades to the generic `external_taint_escape` fallback: the sink is
|
|
1061
|
+
// still reported, but as CWE-668 instead of CWE-89. That is the idiomatic JDBC
|
|
1062
|
+
// form and the shape most real Spring code uses.
|
|
1063
|
+
const resourceDecls = getNodesFromCache(tree.rootNode, 'resource', cache);
|
|
1064
|
+
for (const res of resourceDecls) {
|
|
1065
|
+
const typeNode = res.childForFieldName('type');
|
|
1066
|
+
const nameNode = res.childForFieldName('name');
|
|
1067
|
+
if (typeNode && nameNode) {
|
|
1068
|
+
context.localVarTypes.set(getNodeText(nameNode), getNodeText(typeNode));
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1052
1071
|
// Collect imports — map simple class name to FQN
|
|
1053
1072
|
const imports = getNodesFromCache(tree.rootNode, 'import_declaration', cache);
|
|
1054
1073
|
for (const imp of imports) {
|