circle-ir 4.15.0 → 4.16.1
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/configs/sinks/golang.json +4 -4
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +11 -7
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/constant-caller-gate.d.ts +39 -0
- package/dist/analysis/constant-caller-gate.d.ts.map +1 -0
- package/dist/analysis/constant-caller-gate.js +298 -0
- package/dist/analysis/constant-caller-gate.js.map +1 -0
- package/dist/analysis/constant-propagation/evaluator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/evaluator.js +3 -1
- package/dist/analysis/constant-propagation/evaluator.js.map +1 -1
- package/dist/analysis/constant-propagation/patterns.d.ts +19 -0
- package/dist/analysis/constant-propagation/patterns.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/patterns.js +33 -0
- package/dist/analysis/constant-propagation/patterns.js.map +1 -1
- package/dist/analysis/constant-propagation/propagator.d.ts +59 -0
- package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/propagator.js +396 -49
- package/dist/analysis/constant-propagation/propagator.js.map +1 -1
- package/dist/analysis/constant-propagation/types.d.ts +9 -0
- package/dist/analysis/constant-propagation/types.d.ts.map +1 -1
- package/dist/analysis/findings.d.ts.map +1 -1
- package/dist/analysis/findings.js +134 -0
- package/dist/analysis/findings.js.map +1 -1
- package/dist/analysis/interprocedural.d.ts.map +1 -1
- package/dist/analysis/interprocedural.js +127 -3
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/java-array-element-keys.d.ts +14 -0
- package/dist/analysis/java-array-element-keys.d.ts.map +1 -0
- package/dist/analysis/java-array-element-keys.js +36 -0
- package/dist/analysis/java-array-element-keys.js.map +1 -0
- package/dist/analysis/js-crlf-strip.d.ts +26 -0
- package/dist/analysis/js-crlf-strip.d.ts.map +1 -0
- package/dist/analysis/js-crlf-strip.js +244 -0
- package/dist/analysis/js-crlf-strip.js.map +1 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +25 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.js +157 -12
- package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts +38 -3
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +1234 -127
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.js +53 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
- package/dist/analysis/passes/plaintext-password-storage-pass.d.ts +6 -0
- package/dist/analysis/passes/plaintext-password-storage-pass.d.ts.map +1 -1
- package/dist/analysis/passes/plaintext-password-storage-pass.js +160 -0
- package/dist/analysis/passes/plaintext-password-storage-pass.js.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.d.ts +1 -1
- package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
- package/dist/analysis/passes/scan-secrets-pass.js +7 -1
- package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +326 -34
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-matcher-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-matcher-pass.js +2 -2
- package/dist/analysis/passes/taint-matcher-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +1376 -58
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/passes/weak-hash-pass.d.ts +6 -0
- package/dist/analysis/passes/weak-hash-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-hash-pass.js +191 -4
- package/dist/analysis/passes/weak-hash-pass.js.map +1 -1
- package/dist/analysis/passes/weak-random-pass.d.ts +5 -0
- package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-random-pass.js +30 -0
- package/dist/analysis/passes/weak-random-pass.js.map +1 -1
- package/dist/analysis/project-callee-escape-gate.d.ts +40 -0
- package/dist/analysis/project-callee-escape-gate.d.ts.map +1 -0
- package/dist/analysis/project-callee-escape-gate.js +243 -0
- package/dist/analysis/project-callee-escape-gate.js.map +1 -0
- package/dist/analysis/rust-literal-guard-rebind.d.ts +34 -0
- package/dist/analysis/rust-literal-guard-rebind.d.ts.map +1 -0
- package/dist/analysis/rust-literal-guard-rebind.js +197 -0
- package/dist/analysis/rust-literal-guard-rebind.js.map +1 -0
- package/dist/analysis/rust-literal-guard.d.ts +33 -0
- package/dist/analysis/rust-literal-guard.d.ts.map +1 -0
- package/dist/analysis/rust-literal-guard.js +422 -0
- package/dist/analysis/rust-literal-guard.js.map +1 -0
- package/dist/analysis/scalar-derivation.d.ts +8 -0
- package/dist/analysis/scalar-derivation.d.ts.map +1 -1
- package/dist/analysis/scalar-derivation.js +172 -0
- package/dist/analysis/scalar-derivation.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts +2 -2
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +531 -31
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts +26 -1
- package/dist/analysis/taint-propagation.d.ts.map +1 -1
- package/dist/analysis/taint-propagation.js +234 -13
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/analyzer.d.ts +16 -2
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +13 -3
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +24432 -19928
- package/dist/core/circle-ir-core.cjs +4936 -3575
- package/dist/core/circle-ir-core.js +4936 -3575
- package/dist/core/extractors/calls.d.ts +12 -1
- package/dist/core/extractors/calls.d.ts.map +1 -1
- package/dist/core/extractors/calls.js +332 -22
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/core/extractors/dfg.js +102 -10
- package/dist/core/extractors/dfg.js.map +1 -1
- package/dist/core/extractors/types.d.ts +5 -1
- package/dist/core/extractors/types.d.ts.map +1 -1
- package/dist/core/extractors/types.js +62 -0
- package/dist/core/extractors/types.js.map +1 -1
- package/dist/graph/analysis-pass.d.ts +6 -1
- package/dist/graph/analysis-pass.d.ts.map +1 -1
- package/dist/graph/analysis-pass.js +2 -1
- package/dist/graph/analysis-pass.js.map +1 -1
- package/dist/navigation/navigation-index.d.ts +134 -14
- package/dist/navigation/navigation-index.d.ts.map +1 -1
- package/dist/navigation/navigation-index.js +1158 -104
- package/dist/navigation/navigation-index.js.map +1 -1
- package/dist/navigation/types.d.ts +59 -2
- package/dist/navigation/types.d.ts.map +1 -1
- package/dist/resolution/cross-file.d.ts +9 -0
- package/dist/resolution/cross-file.d.ts.map +1 -1
- package/dist/resolution/cross-file.js +102 -43
- package/dist/resolution/cross-file.js.map +1 -1
- package/dist/resolution/type-hierarchy.d.ts +3 -0
- package/dist/resolution/type-hierarchy.d.ts.map +1 -1
- package/dist/resolution/type-hierarchy.js +45 -1
- package/dist/resolution/type-hierarchy.js.map +1 -1
- package/dist/types/index.d.ts +33 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Matches method calls and annotations against taint configurations.
|
|
5
5
|
*/
|
|
6
|
+
import { javaWikiTagInheritedContextReceiver } from './passes/language-sources-pass.js';
|
|
6
7
|
import { DOM_ASSIGNMENT_SINK_METHODS, isProvablyLiteralExpression } from './literal-expression.js';
|
|
7
8
|
import { getDefaultConfig } from './config-loader.js';
|
|
8
9
|
import { javaReplaceCrLfCoverage } from './java-crlf-strip.js';
|
|
10
|
+
import { jsChainTextAt, jsReplaceChainVerdict } from './js-crlf-strip.js';
|
|
11
|
+
import { HTML_CONTEXT_ENCODER_METHODS } from './constant-propagation/patterns.js';
|
|
9
12
|
import { resolveIdentifierType, resolveCallReturnType, isBoundedClassType, } from './arg-type-resolver.js';
|
|
10
13
|
/**
|
|
11
14
|
* Python tainted access patterns (regex-based).
|
|
@@ -80,9 +83,9 @@ function dropCSharpObjectCarriedWaypoints(sinks, calls) {
|
|
|
80
83
|
return true;
|
|
81
84
|
});
|
|
82
85
|
}
|
|
83
|
-
export function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy, language, code) {
|
|
86
|
+
export function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy, language, code, imports) {
|
|
84
87
|
const sourceLines = code !== undefined ? code.split('\n') : undefined;
|
|
85
|
-
const sources = findSources(calls, types, config.sources, sourceLines, language);
|
|
88
|
+
const sources = findSources(calls, types, config.sources, sourceLines, language, imports);
|
|
86
89
|
let sinkPatterns = expandPromisifyAliases(config.sinks, sourceLines, language);
|
|
87
90
|
sinkPatterns = expandIndirectEvalAliases(sinkPatterns, sourceLines, language);
|
|
88
91
|
const sinks = findSinks(calls, sinkPatterns, typeHierarchy, language, sourceLines, types);
|
|
@@ -265,7 +268,7 @@ export function attachSourceLineCode(sources, sinks, code) {
|
|
|
265
268
|
/**
|
|
266
269
|
* Find taint sources in method calls and annotated parameters.
|
|
267
270
|
*/
|
|
268
|
-
function findSources(calls, types, patterns, sourceLines, language) {
|
|
271
|
+
function findSources(calls, types, patterns, sourceLines, language, imports) {
|
|
269
272
|
const sources = [];
|
|
270
273
|
// #254 T2#7: pre-filter patterns by language once at function entry.
|
|
271
274
|
// Previously the same 4-clause `pattern.languages && … !includes(language)`
|
|
@@ -282,7 +285,7 @@ function findSources(calls, types, patterns, sourceLines, language) {
|
|
|
282
285
|
// Check method calls
|
|
283
286
|
for (const call of calls) {
|
|
284
287
|
for (const pattern of patternsForLanguage) {
|
|
285
|
-
if (matchesSourcePattern(call, pattern)) {
|
|
288
|
+
if (matchesSourcePattern(call, pattern, language, types, imports)) {
|
|
286
289
|
sources.push({
|
|
287
290
|
type: pattern.type,
|
|
288
291
|
location: formatCallLocation(call),
|
|
@@ -710,6 +713,10 @@ function isInterproceduralTaintableType(typeName, language) {
|
|
|
710
713
|
if (elementType === 'String' || elementType === 'Object' || elementType === 'byte') {
|
|
711
714
|
return true;
|
|
712
715
|
}
|
|
716
|
+
// C# aliases (cognium-dev#638): `string[]` / `object[]` cross-file carriers.
|
|
717
|
+
if (language === 'csharp' && (elementType === 'string' || elementType === 'object')) {
|
|
718
|
+
return true;
|
|
719
|
+
}
|
|
713
720
|
}
|
|
714
721
|
return false;
|
|
715
722
|
}
|
|
@@ -1033,6 +1040,35 @@ function processStartInfoExe(expr, sourceLines) {
|
|
|
1033
1040
|
}
|
|
1034
1041
|
return null;
|
|
1035
1042
|
}
|
|
1043
|
+
// ---------------------------------------------------------------------------
|
|
1044
|
+
// Rust `serde_json` parse into `serde_json::Value` (cognium-dev#674).
|
|
1045
|
+
//
|
|
1046
|
+
// `serde_json` is a data-only parser: parsing into the dynamic `Value` tree
|
|
1047
|
+
// cannot instantiate an attacker-chosen type, so it is not CWE-502. A typed
|
|
1048
|
+
// target (`let c: Config = serde_json::from_str(..)`), an undeclared target
|
|
1049
|
+
// and every other format crate (`serde_yaml`, `toml`, ...) keep their sink.
|
|
1050
|
+
// Positive evidence only: the statement must carry an explicit `Value`
|
|
1051
|
+
// annotation on the binding it assigns to.
|
|
1052
|
+
// ---------------------------------------------------------------------------
|
|
1053
|
+
const RUST_VALUE_LET_RE = /\blet\s+(?:mut\s+)?[A-Za-z_]\w*\s*:\s*(?:[A-Za-z_]\w*::)*Value\s*=\s*[^;]*\bfrom_(?:str|slice|reader)\b/;
|
|
1054
|
+
function isRustSerdeJsonValueParse(call, pattern, language, sourceLines) {
|
|
1055
|
+
if (language !== 'rust' || pattern.class !== 'serde_json' || pattern.type !== 'deserialization')
|
|
1056
|
+
return false;
|
|
1057
|
+
if (!sourceLines)
|
|
1058
|
+
return false;
|
|
1059
|
+
const lineNo = call.location.line;
|
|
1060
|
+
const line = sourceLines[lineNo - 1];
|
|
1061
|
+
if (line === undefined)
|
|
1062
|
+
return false;
|
|
1063
|
+
// A `let x: Value =` that rustfmt wrapped before the call lands on the previous line.
|
|
1064
|
+
const prev = lineNo >= 2 ? (sourceLines[lineNo - 2] ?? '') : '';
|
|
1065
|
+
const stmt = (prev.trimEnd().endsWith('=') ? prev + ' ' : '') + line;
|
|
1066
|
+
// Two parse calls in one statement text: cannot attribute the annotation to this call.
|
|
1067
|
+
const parseSegments = stmt.split(';').filter((seg) => /\bfrom_(?:str|slice|reader)\b/.test(seg));
|
|
1068
|
+
if (parseSegments.length !== 1)
|
|
1069
|
+
return false;
|
|
1070
|
+
return RUST_VALUE_LET_RE.test(parseSegments[0]);
|
|
1071
|
+
}
|
|
1036
1072
|
function isSafeCSharpProcessStartCall(call, pattern, language, sourceLines) {
|
|
1037
1073
|
if (language !== 'csharp')
|
|
1038
1074
|
return false;
|
|
@@ -1057,6 +1093,12 @@ function isSafeCSharpProcessStartCall(call, pattern, language, sourceLines) {
|
|
|
1057
1093
|
if (method === 'Start' && call.arguments.length === 1) {
|
|
1058
1094
|
const arg0 = call.arguments.find(a => a.position === 0);
|
|
1059
1095
|
const expr = (arg0?.expression ?? '').trim();
|
|
1096
|
+
// `proc.Start()` surfaces its receiver as the only argument (#594): the
|
|
1097
|
+
// executable rides the `Process` object (`proc.StartInfo.FileName = …`), not a
|
|
1098
|
+
// `ProcessStartInfo` of the same name built elsewhere in the file, so a name
|
|
1099
|
+
// lookup for a constant-exe construction proves nothing here.
|
|
1100
|
+
if (call.receiver != null && call.receiver.trim() === expr)
|
|
1101
|
+
return false;
|
|
1060
1102
|
return isConstNonShellExe(processStartInfoExe(expr, sourceLines));
|
|
1061
1103
|
}
|
|
1062
1104
|
return false;
|
|
@@ -2170,6 +2212,11 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
|
|
|
2170
2212
|
if (isSafeCSharpProcessStartCall(call, pattern, language, sourceLines)) {
|
|
2171
2213
|
continue;
|
|
2172
2214
|
}
|
|
2215
|
+
// Skip `serde_json::from_*` parses whose declared target is
|
|
2216
|
+
// `serde_json::Value` — a data-only parse, not CWE-502. #674.
|
|
2217
|
+
if (isRustSerdeJsonValueParse(call, pattern, language, sourceLines)) {
|
|
2218
|
+
continue;
|
|
2219
|
+
}
|
|
2173
2220
|
// Skip C# SSRF sinks dominated by an inline exact-equality host
|
|
2174
2221
|
// allowlist (`if (host == "c") sink` / `if (host != "c") return; sink`).
|
|
2175
2222
|
// cognium-ai#328 shape 1.
|
|
@@ -2368,7 +2415,7 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
|
|
|
2368
2415
|
/**
|
|
2369
2416
|
* Check if a call matches a source pattern.
|
|
2370
2417
|
*/
|
|
2371
|
-
function matchesSourcePattern(call, pattern) {
|
|
2418
|
+
function matchesSourcePattern(call, pattern, language, types, imports) {
|
|
2372
2419
|
// Method-based matching
|
|
2373
2420
|
if (pattern.method) {
|
|
2374
2421
|
if (call.method_name !== pattern.method) {
|
|
@@ -2401,6 +2448,14 @@ function matchesSourcePattern(call, pattern) {
|
|
|
2401
2448
|
return false;
|
|
2402
2449
|
}
|
|
2403
2450
|
}
|
|
2451
|
+
else if (language === 'java' &&
|
|
2452
|
+
types &&
|
|
2453
|
+
imports &&
|
|
2454
|
+
call.receiver &&
|
|
2455
|
+
pattern.class &&
|
|
2456
|
+
javaWikiTagInheritedContextReceiver(call.receiver, pattern.class, call, types, imports)) {
|
|
2457
|
+
// JSPWiki tag handler inherited m_wikiContext (cognium-dev#624)
|
|
2458
|
+
}
|
|
2404
2459
|
else if (!receiverMightBeClass(call.receiver, pattern.class)) {
|
|
2405
2460
|
// The receiver might be a variable name, not the class name
|
|
2406
2461
|
// For now, we do a simple match - in a full implementation,
|
|
@@ -2609,7 +2664,98 @@ const SINK_FQN_EXCLUSIONS = {
|
|
|
2609
2664
|
// Expression.accept(ExpressionVisitor), etc.
|
|
2610
2665
|
'net.sf.jsqlparser.',
|
|
2611
2666
|
],
|
|
2667
|
+
path_traversal: [
|
|
2668
|
+
// Spring `ResourceLoader.getResource(location)` resolves a classpath /
|
|
2669
|
+
// URL location, not a file path. A receiver named `loader` would
|
|
2670
|
+
// otherwise be taken for `ClassLoader` by the receiver-name heuristic
|
|
2671
|
+
// (the ClassLoader.getResource sink, #621).
|
|
2672
|
+
'org.springframework.core.io.ResourceLoader',
|
|
2673
|
+
'org.springframework.core.io.support.ResourcePatternResolver',
|
|
2674
|
+
],
|
|
2675
|
+
};
|
|
2676
|
+
/**
|
|
2677
|
+
* Sink classes whose receiver-name heuristic is skipped when the declared
|
|
2678
|
+
* receiver type is positively known NOT to be the sink class.
|
|
2679
|
+
* `ClassLoader.getResource` is keyed on a receiver named like `loader`, which
|
|
2680
|
+
* would otherwise also take Spring's `ResourceLoader loader` for a ClassLoader
|
|
2681
|
+
* (#621 follow-up). A declared type that is merely unresolved (a user type
|
|
2682
|
+
* from another file, a type parameter, `var`, a chained call) keeps the
|
|
2683
|
+
* heuristic: it may well be a ClassLoader subclass.
|
|
2684
|
+
*/
|
|
2685
|
+
const DECLARED_TYPE_STRICT_SINK_CLASSES = {
|
|
2686
|
+
path_traversal: ['ClassLoader'],
|
|
2612
2687
|
};
|
|
2688
|
+
/**
|
|
2689
|
+
* Declared receiver types known not to be a `ClassLoader`, matched by simple
|
|
2690
|
+
* name so a wildcard import (`import org.springframework.core.io.*;`), which
|
|
2691
|
+
* leaves `receiver_type_fqn` unresolved, is covered too.
|
|
2692
|
+
*/
|
|
2693
|
+
const KNOWN_NON_CLASSLOADER_SIMPLE_NAMES = new Set([
|
|
2694
|
+
'ResourceLoader',
|
|
2695
|
+
'ResourcePatternResolver',
|
|
2696
|
+
'Object',
|
|
2697
|
+
]);
|
|
2698
|
+
/** Spring resource abstractions: none of them is a `ClassLoader`. */
|
|
2699
|
+
const KNOWN_NON_CLASSLOADER_FQN_PREFIXES = ['org.springframework.core.io.'];
|
|
2700
|
+
/**
|
|
2701
|
+
* True when the type hierarchy knows `typeName` and its whole superclass chain,
|
|
2702
|
+
* and that chain ends without reaching a ClassLoader. Any link that is not in
|
|
2703
|
+
* the hierarchy (a parent declared in another file) means "unknown", never
|
|
2704
|
+
* "not a ClassLoader". Interfaces and enums are never ClassLoaders.
|
|
2705
|
+
*/
|
|
2706
|
+
function hierarchyKnowsNotSubtype(typeName, sinkClass, typeHierarchy) {
|
|
2707
|
+
let node = typeHierarchy.getType(typeName);
|
|
2708
|
+
if (!node)
|
|
2709
|
+
return false;
|
|
2710
|
+
const seen = new Set();
|
|
2711
|
+
while (node) {
|
|
2712
|
+
if (seen.has(node.fqn))
|
|
2713
|
+
return false;
|
|
2714
|
+
seen.add(node.fqn);
|
|
2715
|
+
if (node.name === sinkClass || typeHierarchy.isSubtypeOf(node.fqn, sinkClass))
|
|
2716
|
+
return false;
|
|
2717
|
+
if (node.kind === 'interface' || node.kind === 'enum')
|
|
2718
|
+
return true;
|
|
2719
|
+
if (!node.extends)
|
|
2720
|
+
return true;
|
|
2721
|
+
const parent = node.extends.replace(/<.*$/, '').trim();
|
|
2722
|
+
if (parent.split('.').pop() === sinkClass)
|
|
2723
|
+
return false;
|
|
2724
|
+
node = typeHierarchy.getType(parent);
|
|
2725
|
+
}
|
|
2726
|
+
return false;
|
|
2727
|
+
}
|
|
2728
|
+
/**
|
|
2729
|
+
* True when the call's declared receiver type is positively known not to be
|
|
2730
|
+
* `pattern.class` (a ClassLoader): a Spring resource loader / resolver, `Object`,
|
|
2731
|
+
* or a type the hierarchy resolves (e.g. declared in this file) without
|
|
2732
|
+
* reaching ClassLoader. Unresolved types return false so the receiver-name
|
|
2733
|
+
* heuristic still decides.
|
|
2734
|
+
*/
|
|
2735
|
+
function declaredReceiverTypeExcludes(call, pattern, typeHierarchy) {
|
|
2736
|
+
if (!pattern.class || !DECLARED_TYPE_STRICT_SINK_CLASSES[pattern.type]?.includes(pattern.class)) {
|
|
2737
|
+
return false;
|
|
2738
|
+
}
|
|
2739
|
+
const fqn = call.receiver_type_fqn?.replace(/<.*$/, '').trim();
|
|
2740
|
+
const simpleDeclared = call.receiver_type?.replace(/<.*$/, '').trim();
|
|
2741
|
+
if (fqn && KNOWN_NON_CLASSLOADER_FQN_PREFIXES.some((prefix) => fqn.startsWith(prefix))) {
|
|
2742
|
+
return true;
|
|
2743
|
+
}
|
|
2744
|
+
if (fqn === 'java.lang.Object')
|
|
2745
|
+
return true;
|
|
2746
|
+
const declared = fqn || simpleDeclared;
|
|
2747
|
+
if (!declared)
|
|
2748
|
+
return false;
|
|
2749
|
+
const simple = declared.split('.').pop() ?? '';
|
|
2750
|
+
if (simple === '' || simple === 'var')
|
|
2751
|
+
return false;
|
|
2752
|
+
// A simple name only counts when no other FQN was resolved for it
|
|
2753
|
+
// (`com.acme.ResourceLoader` is a user type, not Spring's).
|
|
2754
|
+
if (KNOWN_NON_CLASSLOADER_SIMPLE_NAMES.has(simple) && (!fqn || fqn === simple)) {
|
|
2755
|
+
return true;
|
|
2756
|
+
}
|
|
2757
|
+
return typeHierarchy !== undefined && hierarchyKnowsNotSubtype(declared, pattern.class, typeHierarchy);
|
|
2758
|
+
}
|
|
2613
2759
|
/**
|
|
2614
2760
|
* Check if a call matches a sink pattern.
|
|
2615
2761
|
*/
|
|
@@ -2710,6 +2856,13 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
|
|
|
2710
2856
|
// If the mapped return type is a subtype of the sink class, accept.
|
|
2711
2857
|
// — cognium-dev #241 Java
|
|
2712
2858
|
}
|
|
2859
|
+
else if (declaredReceiverTypeExcludes(call, pattern, typeHierarchy)) {
|
|
2860
|
+
// Declared receiver type is positively known not to be the sink class
|
|
2861
|
+
// (Spring `ResourceLoader`, `Object`, a same-file class that does not
|
|
2862
|
+
// extend ClassLoader): the receiver-name heuristic below must not turn
|
|
2863
|
+
// it into the sink class. Unresolved types still use the heuristic.
|
|
2864
|
+
return false;
|
|
2865
|
+
}
|
|
2713
2866
|
else if (call.receiver && !receiverMightBeClass(call.receiver, pattern.class)) {
|
|
2714
2867
|
// Heuristic match failed; fall back to TypeHierarchyResolver if available
|
|
2715
2868
|
if (typeHierarchy && typeHierarchy.couldBeType(call.receiver, pattern.class)) {
|
|
@@ -3262,9 +3415,20 @@ export function isInDangerousPosition(argPosition, pattern) {
|
|
|
3262
3415
|
*/
|
|
3263
3416
|
function findSanitizers(calls, types, patterns, sourceLines, language) {
|
|
3264
3417
|
const sanitizers = [];
|
|
3265
|
-
const javaReplace = (call) =>
|
|
3266
|
-
|
|
3267
|
-
|
|
3418
|
+
const javaReplace = (call) => {
|
|
3419
|
+
if (!sourceLines || (call.method_name !== 'replace' && call.method_name !== 'replaceAll'))
|
|
3420
|
+
return null;
|
|
3421
|
+
if (language === 'java') {
|
|
3422
|
+
return javaReplaceChainCredit(javaChainTextAt(sourceLines, call.location.line, call.location.column));
|
|
3423
|
+
}
|
|
3424
|
+
if (language === 'javascript' || language === 'typescript' || language === 'tsx') {
|
|
3425
|
+
// #562: the class-less `replace` row credits log_injection; keep it only
|
|
3426
|
+
// when the literal chain strips both CR and LF, or cannot be decided.
|
|
3427
|
+
const verdict = jsReplaceChainVerdict(jsChainTextAt(sourceLines, call.location.line, call.location.column));
|
|
3428
|
+
return verdict === 'leaves-crlf' ? [] : null;
|
|
3429
|
+
}
|
|
3430
|
+
return null;
|
|
3431
|
+
};
|
|
3268
3432
|
// Build a set of method names with @sanitizer annotation (from Javadoc comments)
|
|
3269
3433
|
const sanitizerMethods = new Set();
|
|
3270
3434
|
for (const type of types) {
|
|
@@ -3275,19 +3439,22 @@ function findSanitizers(calls, types, patterns, sourceLines, language) {
|
|
|
3275
3439
|
}
|
|
3276
3440
|
}
|
|
3277
3441
|
// Sprint 9 #79 (Phase L): derive wrapper sanitizers.
|
|
3278
|
-
// A method qualifies
|
|
3279
|
-
//
|
|
3442
|
+
// A method qualifies when its body is `return <sanitizer-expr>(<param>)`
|
|
3443
|
+
// (optionally `await`-prefixed), span ≤ 2 lines (signature + single return).
|
|
3280
3444
|
// We require:
|
|
3281
|
-
// 1.
|
|
3282
|
-
//
|
|
3283
|
-
//
|
|
3284
|
-
// `removes
|
|
3285
|
-
//
|
|
3286
|
-
//
|
|
3287
|
-
//
|
|
3288
|
-
//
|
|
3289
|
-
//
|
|
3445
|
+
// 1. One non-recursive call, OR every inner call is `replace` / `replaceAll`
|
|
3446
|
+
// (#517): use the call with the longest receiver for argument-aware credit.
|
|
3447
|
+
// 2. That call matches a configured sanitizer pattern with non-empty
|
|
3448
|
+
// `removes` (Java `replace` chains use `javaReplaceChainCredit`).
|
|
3449
|
+
// 3. The inner call's argument is a wrapper param, or (replace-chain only)
|
|
3450
|
+
// the receiver text starts with a param name.
|
|
3451
|
+
// 4. When source is available, the return expression is a single sanitizer
|
|
3452
|
+
// call or a param-rooted replace chain ending with `)` — rejecting shapes
|
|
3453
|
+
// like `return x + shlex.quote(x)`.
|
|
3290
3454
|
const wrapperSanitizers = new Map();
|
|
3455
|
+
// #589: wrappers whose single inner call is a context-specific HTML encoder
|
|
3456
|
+
// (every same-named wrapper, so an overload cannot lend its credit).
|
|
3457
|
+
const wrapperEncoderOnly = new Map();
|
|
3291
3458
|
for (const type of types) {
|
|
3292
3459
|
for (const method of type.methods) {
|
|
3293
3460
|
const bodySize = method.end_line - method.start_line;
|
|
@@ -3304,9 +3471,21 @@ function findSanitizers(calls, types, patterns, sourceLines, language) {
|
|
|
3304
3471
|
continue; // recursion guard
|
|
3305
3472
|
inside.push(c);
|
|
3306
3473
|
}
|
|
3307
|
-
if (inside.length
|
|
3474
|
+
if (inside.length === 0)
|
|
3308
3475
|
continue;
|
|
3309
|
-
const
|
|
3476
|
+
const allReplaceCalls = inside.every(c => c.method_name === 'replace' || c.method_name === 'replaceAll');
|
|
3477
|
+
let innerCall;
|
|
3478
|
+
let replaceChainHelper = false;
|
|
3479
|
+
if (inside.length === 1) {
|
|
3480
|
+
innerCall = inside[0];
|
|
3481
|
+
}
|
|
3482
|
+
else if (allReplaceCalls) {
|
|
3483
|
+
replaceChainHelper = true;
|
|
3484
|
+
innerCall = inside.reduce((best, c) => (c.receiver ?? '').length > (best.receiver ?? '').length ? c : best);
|
|
3485
|
+
}
|
|
3486
|
+
else {
|
|
3487
|
+
continue;
|
|
3488
|
+
}
|
|
3310
3489
|
let matched;
|
|
3311
3490
|
const replaceCredit = javaReplace(innerCall);
|
|
3312
3491
|
if (replaceCredit) {
|
|
@@ -3323,10 +3502,21 @@ function findSanitizers(calls, types, patterns, sourceLines, language) {
|
|
|
3323
3502
|
if (!matched || !matched.removes || matched.removes.length === 0)
|
|
3324
3503
|
continue;
|
|
3325
3504
|
let argOk = false;
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3505
|
+
if (replaceChainHelper) {
|
|
3506
|
+
const recv = innerCall.receiver?.trim() ?? '';
|
|
3507
|
+
for (const p of paramNames) {
|
|
3508
|
+
if (recv === p || recv.startsWith(`${p}.`)) {
|
|
3509
|
+
argOk = true;
|
|
3510
|
+
break;
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
else {
|
|
3515
|
+
for (const arg of innerCall.arguments) {
|
|
3516
|
+
if (arg.variable && paramNames.has(arg.variable)) {
|
|
3517
|
+
argOk = true;
|
|
3518
|
+
break;
|
|
3519
|
+
}
|
|
3330
3520
|
}
|
|
3331
3521
|
}
|
|
3332
3522
|
if (!argOk)
|
|
@@ -3338,14 +3528,24 @@ function findSanitizers(calls, types, patterns, sourceLines, language) {
|
|
|
3338
3528
|
if (!returnMatch)
|
|
3339
3529
|
continue;
|
|
3340
3530
|
const after = returnMatch[1].replace(/;\s*$/, '').trimEnd();
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3531
|
+
if (replaceChainHelper) {
|
|
3532
|
+
const startsWithParam = [...paramNames].some(p => after === p || after.startsWith(`${p}.`));
|
|
3533
|
+
if (!startsWithParam)
|
|
3534
|
+
continue;
|
|
3535
|
+
}
|
|
3536
|
+
else {
|
|
3537
|
+
const callPrefix = innerCall.receiver
|
|
3538
|
+
? `${innerCall.receiver}.${innerCall.method_name}(`
|
|
3539
|
+
: `${innerCall.method_name}(`;
|
|
3540
|
+
if (!after.startsWith(callPrefix))
|
|
3541
|
+
continue;
|
|
3542
|
+
}
|
|
3346
3543
|
if (!after.endsWith(')'))
|
|
3347
3544
|
continue;
|
|
3348
3545
|
}
|
|
3546
|
+
const encoderWrapper = !replaceCredit && method.parameters.length === 1 &&
|
|
3547
|
+
HTML_CONTEXT_ENCODER_METHODS.has(innerCall.method_name);
|
|
3548
|
+
wrapperEncoderOnly.set(method.name, (wrapperEncoderOnly.get(method.name) ?? true) && encoderWrapper);
|
|
3349
3549
|
const existing = wrapperSanitizers.get(method.name);
|
|
3350
3550
|
if (existing) {
|
|
3351
3551
|
const set = new Set([...existing, ...matched.removes]);
|
|
@@ -3356,18 +3556,88 @@ function findSanitizers(calls, types, patterns, sourceLines, language) {
|
|
|
3356
3556
|
}
|
|
3357
3557
|
}
|
|
3358
3558
|
}
|
|
3559
|
+
// #589: delegate to an encoder-only wrapper in one hop (wrapEncode → encodeForPage → escapeHtml).
|
|
3560
|
+
let transitiveWrapperAdded = true;
|
|
3561
|
+
while (transitiveWrapperAdded) {
|
|
3562
|
+
transitiveWrapperAdded = false;
|
|
3563
|
+
for (const type of types) {
|
|
3564
|
+
for (const method of type.methods) {
|
|
3565
|
+
if (wrapperSanitizers.has(method.name))
|
|
3566
|
+
continue;
|
|
3567
|
+
const bodySize = method.end_line - method.start_line;
|
|
3568
|
+
if (bodySize < 0 || bodySize > 2)
|
|
3569
|
+
continue;
|
|
3570
|
+
const paramNames = new Set(method.parameters.map(p => p.name));
|
|
3571
|
+
if (paramNames.size === 0)
|
|
3572
|
+
continue;
|
|
3573
|
+
const inside = [];
|
|
3574
|
+
for (const c of calls) {
|
|
3575
|
+
if (c.location.line < method.start_line || c.location.line > method.end_line)
|
|
3576
|
+
continue;
|
|
3577
|
+
if (c.method_name === method.name)
|
|
3578
|
+
continue;
|
|
3579
|
+
inside.push(c);
|
|
3580
|
+
}
|
|
3581
|
+
if (inside.length !== 1)
|
|
3582
|
+
continue;
|
|
3583
|
+
const innerCall = inside[0];
|
|
3584
|
+
const delegateRemoves = wrapperSanitizers.get(innerCall.method_name);
|
|
3585
|
+
if (!delegateRemoves || delegateRemoves.length === 0)
|
|
3586
|
+
continue;
|
|
3587
|
+
let argOk = false;
|
|
3588
|
+
for (const arg of innerCall.arguments) {
|
|
3589
|
+
if (arg.variable && paramNames.has(arg.variable)) {
|
|
3590
|
+
argOk = true;
|
|
3591
|
+
break;
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
if (!argOk)
|
|
3595
|
+
continue;
|
|
3596
|
+
if (sourceLines) {
|
|
3597
|
+
const lineText = sourceLines[innerCall.location.line - 1] ?? '';
|
|
3598
|
+
const stripped = lineText.trim();
|
|
3599
|
+
const returnMatch = stripped.match(/^return\s+(?:await\s+)?(.*)$/);
|
|
3600
|
+
if (!returnMatch)
|
|
3601
|
+
continue;
|
|
3602
|
+
const after = returnMatch[1].replace(/;\s*$/, '').trimEnd();
|
|
3603
|
+
const callPrefix = innerCall.receiver
|
|
3604
|
+
? `${innerCall.receiver}.${innerCall.method_name}(`
|
|
3605
|
+
: `${innerCall.method_name}(`;
|
|
3606
|
+
if (!after.startsWith(callPrefix) || !after.endsWith(')'))
|
|
3607
|
+
continue;
|
|
3608
|
+
}
|
|
3609
|
+
const encoderDelegate = wrapperEncoderOnly.get(innerCall.method_name) === true;
|
|
3610
|
+
wrapperEncoderOnly.set(method.name, encoderDelegate);
|
|
3611
|
+
wrapperSanitizers.set(method.name, [...delegateRemoves]);
|
|
3612
|
+
transitiveWrapperAdded = true;
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3359
3616
|
// Emit a derived-sanitizer entry at each call site to a wrapper method.
|
|
3360
3617
|
for (const call of calls) {
|
|
3361
3618
|
const removes = wrapperSanitizers.get(call.method_name);
|
|
3362
3619
|
if (!removes)
|
|
3363
3620
|
continue;
|
|
3364
3621
|
sanitizers.push({
|
|
3365
|
-
|
|
3622
|
+
// #589: a wrapper of an HTML encoder cleans `xss` but not `trust_boundary`,
|
|
3623
|
+
// like the encoder itself, so it is told apart from other wrappers.
|
|
3624
|
+
type: wrapperEncoderOnly.get(call.method_name) ? 'derived_html_encoder' : 'derived_wrapper',
|
|
3366
3625
|
method: formatSanitizerMethod(call),
|
|
3367
3626
|
line: call.location.line,
|
|
3368
3627
|
sanitizes: removes,
|
|
3369
3628
|
});
|
|
3370
3629
|
}
|
|
3630
|
+
const noopLocalHelpers = !sourceLines
|
|
3631
|
+
? new Set()
|
|
3632
|
+
: language === 'java'
|
|
3633
|
+
? findJavaNoopHelpers(types, sourceLines)
|
|
3634
|
+
: language === 'python'
|
|
3635
|
+
? findPythonNoopHelpers(types, sourceLines)
|
|
3636
|
+
: language === 'go'
|
|
3637
|
+
? findGoNoopHelpers(types, sourceLines)
|
|
3638
|
+
: language === 'javascript' || language === 'typescript' || language === 'tsx'
|
|
3639
|
+
? findJsNoopHelpers(types, sourceLines)
|
|
3640
|
+
: new Set();
|
|
3371
3641
|
// Check method calls for sanitizer methods
|
|
3372
3642
|
for (const call of calls) {
|
|
3373
3643
|
// Check if this call is to a method with @sanitizer annotation
|
|
@@ -3394,6 +3664,9 @@ function findSanitizers(calls, types, patterns, sourceLines, language) {
|
|
|
3394
3664
|
}
|
|
3395
3665
|
for (const pattern of patterns) {
|
|
3396
3666
|
if (matchesSanitizerPattern(call, pattern)) {
|
|
3667
|
+
if (!pattern.class && noopLocalHelpers.has(call.method_name) && (!call.receiver || call.receiver === 'this' || call.receiver === 'self')) {
|
|
3668
|
+
continue; // same-file helper that does no escaping: not credited by name (#581)
|
|
3669
|
+
}
|
|
3397
3670
|
sanitizers.push({
|
|
3398
3671
|
type: determineSanitizerType(pattern),
|
|
3399
3672
|
method: formatSanitizerMethod(call),
|
|
@@ -3422,6 +3695,233 @@ function findSanitizers(calls, types, patterns, sourceLines, language) {
|
|
|
3422
3695
|
}
|
|
3423
3696
|
return sanitizers;
|
|
3424
3697
|
}
|
|
3698
|
+
/**
|
|
3699
|
+
* Names of methods defined in this file whose every definition is a no-op
|
|
3700
|
+
* shape: a single `return <expr>;` built only from the method's own
|
|
3701
|
+
* parameters, string literals, `+` and the pass-through calls `trim`, `strip`,
|
|
3702
|
+
* `toString`, `toLowerCase` and `toUpperCase`. A class-less sanitizer row must
|
|
3703
|
+
* not credit a call to such a helper by name (#581). Any other body (a
|
|
3704
|
+
* `replace`, a library call, a branch, a loop) keeps the by-name credit, and so
|
|
3705
|
+
* does a name that has a non-no-op overload.
|
|
3706
|
+
*/
|
|
3707
|
+
function findJavaNoopHelpers(types, sourceLines) {
|
|
3708
|
+
const noop = new Set();
|
|
3709
|
+
const other = new Set();
|
|
3710
|
+
for (const type of types) {
|
|
3711
|
+
for (const method of type.methods) {
|
|
3712
|
+
(isJavaNoopBody(method, sourceLines) ? noop : other).add(method.name);
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
for (const name of other)
|
|
3716
|
+
noop.delete(name);
|
|
3717
|
+
return noop;
|
|
3718
|
+
}
|
|
3719
|
+
function isJavaNoopBody(method, sourceLines) {
|
|
3720
|
+
if (method.parameters.length === 0)
|
|
3721
|
+
return false;
|
|
3722
|
+
const text = sourceLines.slice(method.start_line - 1, method.end_line).join('\n');
|
|
3723
|
+
const open = text.indexOf('{');
|
|
3724
|
+
const close = text.lastIndexOf('}');
|
|
3725
|
+
if (open < 0 || close < open)
|
|
3726
|
+
return false;
|
|
3727
|
+
const m = /^\s*return\s+([^;{}]+);\s*$/.exec(text.slice(open + 1, close));
|
|
3728
|
+
if (!m)
|
|
3729
|
+
return false;
|
|
3730
|
+
const params = new Set(method.parameters.map(p => p.name));
|
|
3731
|
+
const expr = m[1]
|
|
3732
|
+
.replace(/"(?:\\.|[^"\\])*"/g, '""')
|
|
3733
|
+
.replace(/\.(?:trim|strip|toString|toLowerCase|toUpperCase)\(\)/g, '');
|
|
3734
|
+
const leftovers = expr.replace(/""|[+()\s]/g, ' ').trim();
|
|
3735
|
+
if (leftovers === '')
|
|
3736
|
+
return true;
|
|
3737
|
+
return leftovers.split(/\s+/).every(tok => params.has(tok));
|
|
3738
|
+
}
|
|
3739
|
+
/**
|
|
3740
|
+
* Python twin of `findJavaNoopHelpers` (#581): a `def` whose body is one
|
|
3741
|
+
* `return <expr>` built only from its own parameters, string literals, `+`, the
|
|
3742
|
+
* pass-through methods `strip` / `lstrip` / `rstrip` / `lower` / `upper` and the
|
|
3743
|
+
* `str()` call. Any other body (a `re.sub`, `replace`, `html.escape`, a branch,
|
|
3744
|
+
* a loop, a second statement) keeps the by-name credit, and so does a name with a
|
|
3745
|
+
* non-no-op definition elsewhere in the file.
|
|
3746
|
+
*/
|
|
3747
|
+
function findPythonNoopHelpers(types, sourceLines) {
|
|
3748
|
+
const noop = new Set();
|
|
3749
|
+
const other = new Set();
|
|
3750
|
+
for (const type of types) {
|
|
3751
|
+
for (const method of type.methods) {
|
|
3752
|
+
(isPythonNoopBody(method, sourceLines) ? noop : other).add(method.name);
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
for (const name of other)
|
|
3756
|
+
noop.delete(name);
|
|
3757
|
+
return noop;
|
|
3758
|
+
}
|
|
3759
|
+
function isPythonNoopBody(method, sourceLines) {
|
|
3760
|
+
if (method.parameters.length === 0)
|
|
3761
|
+
return false;
|
|
3762
|
+
const text = sourceLines.slice(method.start_line - 1, method.end_line).join('\n');
|
|
3763
|
+
// `def name(params) -> T:` then the body, on the same line or below. The header is
|
|
3764
|
+
// matched without crossing a newline inside a default value or annotation.
|
|
3765
|
+
const header = /^\s*(?:async\s+)?def\s+\w+\s*\(([^()]*)\)\s*(?:->\s*[\w.\[\], ]+)?\s*:/.exec(text);
|
|
3766
|
+
if (!header)
|
|
3767
|
+
return false;
|
|
3768
|
+
const body = text.slice(header[0].length).replace(/#.*$/gm, '').trim();
|
|
3769
|
+
const m = /^return\s+([^\n;]+)$/.exec(body);
|
|
3770
|
+
if (!m)
|
|
3771
|
+
return false;
|
|
3772
|
+
const params = new Set(method.parameters.map(p => p.name.replace(/^\*+/, '')));
|
|
3773
|
+
const expr = m[1]
|
|
3774
|
+
.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '""')
|
|
3775
|
+
.replace(/\.(?:strip|lstrip|rstrip|lower|upper)\(\)/g, '')
|
|
3776
|
+
.replace(/\bstr\(/g, '(');
|
|
3777
|
+
const leftovers = expr.replace(/""|[+()\s]/g, ' ').trim();
|
|
3778
|
+
if (leftovers === '')
|
|
3779
|
+
return true;
|
|
3780
|
+
return leftovers.split(/\s+/).every(tok => params.has(tok));
|
|
3781
|
+
}
|
|
3782
|
+
/**
|
|
3783
|
+
* Go twin of `findJavaNoopHelpers` (#581): a `func` whose body is one
|
|
3784
|
+
* `return <expr>` built only from its own parameters, string literals, `+`, the
|
|
3785
|
+
* pass-through calls `strings.TrimSpace` / `strings.ToLower` / `strings.ToUpper`
|
|
3786
|
+
* and the `string()` conversion. Any other body (a `strings.ReplaceAll`, an
|
|
3787
|
+
* escaper call, a branch, a loop, a second statement) keeps the by-name credit,
|
|
3788
|
+
* and so does a name with a non-no-op definition elsewhere in the file, a method
|
|
3789
|
+
* or a function alike.
|
|
3790
|
+
*/
|
|
3791
|
+
function findGoNoopHelpers(types, sourceLines) {
|
|
3792
|
+
const noop = new Set();
|
|
3793
|
+
const other = new Set();
|
|
3794
|
+
for (const type of types) {
|
|
3795
|
+
for (const method of type.methods) {
|
|
3796
|
+
(isGoNoopBody(method, sourceLines) ? noop : other).add(method.name);
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
for (const name of other)
|
|
3800
|
+
noop.delete(name);
|
|
3801
|
+
// A local of the same name (`sanitize := func(v string) string { … }`) is a
|
|
3802
|
+
// different helper whose body is not read here.
|
|
3803
|
+
const src = sourceLines.join('\n');
|
|
3804
|
+
for (const name of [...noop]) {
|
|
3805
|
+
if (new RegExp(`(?<![\\w.])${name}\\s*(?::=|=(?!=)|,\\s*\\w+\\s*:=)`).test(src))
|
|
3806
|
+
noop.delete(name);
|
|
3807
|
+
}
|
|
3808
|
+
return noop;
|
|
3809
|
+
}
|
|
3810
|
+
function isGoNoopBody(method, sourceLines) {
|
|
3811
|
+
if (method.parameters.length === 0)
|
|
3812
|
+
return false;
|
|
3813
|
+
const text = sourceLines.slice(method.start_line - 1, method.end_line).join('\n');
|
|
3814
|
+
// `func name(params) T {` — a plain function; a method (`func (s *T) name`) is judged by a
|
|
3815
|
+
// call through its receiver, which is never skipped, so it is not a no-op helper here.
|
|
3816
|
+
const header = /^\s*func\s+\w+\s*\(([^()]*)\)\s*\(?[\w.\[\]*, ]*\)?\s*\{/.exec(text);
|
|
3817
|
+
if (!header)
|
|
3818
|
+
return false;
|
|
3819
|
+
const close = text.lastIndexOf('}');
|
|
3820
|
+
if (close < header[0].length)
|
|
3821
|
+
return false;
|
|
3822
|
+
const body = text.slice(header[0].length, close).replace(/\/\/.*$/gm, '').trim();
|
|
3823
|
+
const m = /^return\s+([^;{}]+)$/.exec(body);
|
|
3824
|
+
if (!m)
|
|
3825
|
+
return false;
|
|
3826
|
+
const params = new Set(method.parameters.map(p => p.name.replace(/^\.+/, '')));
|
|
3827
|
+
for (const seg of header[1].split(',')) {
|
|
3828
|
+
const name = seg.trim().split(/\s+/)[0]?.replace(/^\.+/, '');
|
|
3829
|
+
if (name)
|
|
3830
|
+
params.add(name);
|
|
3831
|
+
}
|
|
3832
|
+
const expr = m[1]
|
|
3833
|
+
.replace(/"(?:\\.|[^"\\])*"|`[^`]*`/g, '""')
|
|
3834
|
+
.replace(/\bstrings\s*\.\s*(?:TrimSpace|ToLower|ToUpper)\s*\(/g, '(')
|
|
3835
|
+
.replace(/\bstring\s*\(/g, '(');
|
|
3836
|
+
const leftovers = expr.replace(/""|[+()\s]/g, ' ').trim();
|
|
3837
|
+
if (leftovers === '')
|
|
3838
|
+
return true;
|
|
3839
|
+
return leftovers.split(/\s+/).every(tok => params.has(tok));
|
|
3840
|
+
}
|
|
3841
|
+
/**
|
|
3842
|
+
* JavaScript / TypeScript twin of `findJavaNoopHelpers` (#581): a `function` or
|
|
3843
|
+
* method whose body is one `return <expr>` (or an arrow expression) built only
|
|
3844
|
+
* from its own parameters, string literals, `+`, and the pass-through calls
|
|
3845
|
+
* `trim` / `toLowerCase` / `toUpperCase` / `toString` / `String()`. Any other
|
|
3846
|
+
* body keeps the by-name credit, and so does a name with a non-no-op overload.
|
|
3847
|
+
*/
|
|
3848
|
+
function findJsNoopHelpers(types, sourceLines) {
|
|
3849
|
+
const fromDecl = new Set();
|
|
3850
|
+
const otherDecl = new Set();
|
|
3851
|
+
for (const type of types) {
|
|
3852
|
+
for (const method of type.methods) {
|
|
3853
|
+
(isJsNoopBody(method, sourceLines) ? fromDecl : otherDecl).add(method.name);
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
for (const name of otherDecl)
|
|
3857
|
+
fromDecl.delete(name);
|
|
3858
|
+
const src = sourceLines.join('\n');
|
|
3859
|
+
for (const name of [...fromDecl]) {
|
|
3860
|
+
if (new RegExp(`(?:const|let|var)\\s+${name}\\s*=`).test(src))
|
|
3861
|
+
fromDecl.delete(name);
|
|
3862
|
+
}
|
|
3863
|
+
const fromBinding = new Set();
|
|
3864
|
+
const otherBinding = new Set();
|
|
3865
|
+
for (const line of sourceLines) {
|
|
3866
|
+
const arrow = /(?:const|let|var)\s+(\w+)\s*=\s*\(([^)]*)\)\s*=>\s*([^;{]+)\s*;/.exec(line);
|
|
3867
|
+
if (arrow) {
|
|
3868
|
+
const params = jsParamNamesFromList(arrow[2]);
|
|
3869
|
+
if (params.size > 0 && jsNoopReturnExpr(arrow[3], params))
|
|
3870
|
+
fromBinding.add(arrow[1]);
|
|
3871
|
+
else
|
|
3872
|
+
otherBinding.add(arrow[1]);
|
|
3873
|
+
}
|
|
3874
|
+
const fnExpr = /(?:const|let|var)\s+(\w+)\s*=\s*function\s*\(([^)]*)\)\s*\{\s*return\s+([^;{}]+);\s*\}\s*;/.exec(line);
|
|
3875
|
+
if (fnExpr) {
|
|
3876
|
+
const params = jsParamNamesFromList(fnExpr[2]);
|
|
3877
|
+
if (params.size > 0 && jsNoopReturnExpr(fnExpr[3], params))
|
|
3878
|
+
fromBinding.add(fnExpr[1]);
|
|
3879
|
+
else
|
|
3880
|
+
otherBinding.add(fnExpr[1]);
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
for (const name of otherBinding)
|
|
3884
|
+
fromBinding.delete(name);
|
|
3885
|
+
return new Set([...fromDecl, ...fromBinding]);
|
|
3886
|
+
}
|
|
3887
|
+
function jsParamNamesFromList(list) {
|
|
3888
|
+
const params = new Set();
|
|
3889
|
+
for (const seg of list.split(',')) {
|
|
3890
|
+
const name = seg.trim().split(':')[0]?.trim().replace(/^\.\.\./, '');
|
|
3891
|
+
if (name)
|
|
3892
|
+
params.add(name);
|
|
3893
|
+
}
|
|
3894
|
+
return params;
|
|
3895
|
+
}
|
|
3896
|
+
function isJsNoopBody(method, sourceLines) {
|
|
3897
|
+
if (method.parameters.length === 0)
|
|
3898
|
+
return false;
|
|
3899
|
+
const text = sourceLines.slice(method.start_line - 1, method.end_line).join('\n');
|
|
3900
|
+
const params = new Set(method.parameters.map(p => p.name.replace(/^\.\.\./, '')));
|
|
3901
|
+
const arrowExpr = /(?:\([^)]*\)|[\w$]+)\s*=>\s*([^{;]+?)\s*$/.exec(text.trim());
|
|
3902
|
+
if (arrowExpr) {
|
|
3903
|
+
return jsNoopReturnExpr(arrowExpr[1], params);
|
|
3904
|
+
}
|
|
3905
|
+
const open = text.indexOf('{');
|
|
3906
|
+
const close = text.lastIndexOf('}');
|
|
3907
|
+
if (open < 0 || close < open)
|
|
3908
|
+
return false;
|
|
3909
|
+
const body = text.slice(open + 1, close).replace(/\/\/.*$/gm, '').trim();
|
|
3910
|
+
const m = /^\s*return\s+([^;{}]+);\s*$/.exec(body);
|
|
3911
|
+
if (!m)
|
|
3912
|
+
return false;
|
|
3913
|
+
return jsNoopReturnExpr(m[1], params);
|
|
3914
|
+
}
|
|
3915
|
+
function jsNoopReturnExpr(expr, params) {
|
|
3916
|
+
const stripped = expr
|
|
3917
|
+
.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`/g, '""')
|
|
3918
|
+
.replace(/\.(?:trim|toLowerCase|toUpperCase|toString)\(\)/g, '')
|
|
3919
|
+
.replace(/\bString\s*\(/g, '(');
|
|
3920
|
+
const leftovers = stripped.replace(/""|[+()\s]/g, ' ').trim();
|
|
3921
|
+
if (leftovers === '')
|
|
3922
|
+
return true;
|
|
3923
|
+
return leftovers.split(/\s+/).every(tok => params.has(tok));
|
|
3924
|
+
}
|
|
3425
3925
|
const JAVA_LIT = String.raw `(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)')`;
|
|
3426
3926
|
const JAVA_REPLACE_ARGS_RE = new RegExp(String.raw `\.(replace|replaceAll)\(\s*${JAVA_LIT}\s*,\s*${JAVA_LIT}\s*\)`, 'g');
|
|
3427
3927
|
/**
|