@sikkuumiii/defuse 0.0.0-stage → 0.5.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.
Files changed (236) hide show
  1. package/LICENSE +235 -0
  2. package/README.md +561 -2
  3. package/dist/src/cli.d.ts +20 -0
  4. package/dist/src/cli.js +619 -0
  5. package/dist/src/cli.js.map +1 -0
  6. package/dist/src/core/analyze.d.ts +178 -0
  7. package/dist/src/core/analyze.js +834 -0
  8. package/dist/src/core/analyze.js.map +1 -0
  9. package/dist/src/core/coverage.d.ts +87 -0
  10. package/dist/src/core/coverage.js +181 -0
  11. package/dist/src/core/coverage.js.map +1 -0
  12. package/dist/src/core/finding.d.ts +196 -0
  13. package/dist/src/core/finding.js +178 -0
  14. package/dist/src/core/finding.js.map +1 -0
  15. package/dist/src/core/licence.d.ts +50 -0
  16. package/dist/src/core/licence.js +56 -0
  17. package/dist/src/core/licence.js.map +1 -0
  18. package/dist/src/core/paths.d.ts +21 -0
  19. package/dist/src/core/paths.js +58 -0
  20. package/dist/src/core/paths.js.map +1 -0
  21. package/dist/src/core/score.d.ts +137 -0
  22. package/dist/src/core/score.js +232 -0
  23. package/dist/src/core/score.js.map +1 -0
  24. package/dist/src/dast/targets.d.ts +30 -0
  25. package/dist/src/dast/targets.js +79 -0
  26. package/dist/src/dast/targets.js.map +1 -0
  27. package/dist/src/dast/zap.d.ts +42 -0
  28. package/dist/src/dast/zap.js +184 -0
  29. package/dist/src/dast/zap.js.map +1 -0
  30. package/dist/src/engine/scan.d.ts +80 -0
  31. package/dist/src/engine/scan.js +129 -0
  32. package/dist/src/engine/scan.js.map +1 -0
  33. package/dist/src/engine/shapes.d.ts +47 -0
  34. package/dist/src/engine/shapes.js +328 -0
  35. package/dist/src/engine/shapes.js.map +1 -0
  36. package/dist/src/engine/walk.d.ts +60 -0
  37. package/dist/src/engine/walk.js +156 -0
  38. package/dist/src/engine/walk.js.map +1 -0
  39. package/dist/src/parse/astDump.d.ts +31 -0
  40. package/dist/src/parse/astDump.js +66 -0
  41. package/dist/src/parse/astDump.js.map +1 -0
  42. package/dist/src/parse/languages.d.ts +70 -0
  43. package/dist/src/parse/languages.js +174 -0
  44. package/dist/src/parse/languages.js.map +1 -0
  45. package/dist/src/parse/location.d.ts +17 -0
  46. package/dist/src/parse/location.js +38 -0
  47. package/dist/src/parse/location.js.map +1 -0
  48. package/dist/src/parse/node-grammars.d.ts +12 -0
  49. package/dist/src/parse/node-grammars.js +35 -0
  50. package/dist/src/parse/node-grammars.js.map +1 -0
  51. package/dist/src/parse/parser.d.ts +126 -0
  52. package/dist/src/parse/parser.js +202 -0
  53. package/dist/src/parse/parser.js.map +1 -0
  54. package/dist/src/parse/tree-store.d.ts +139 -0
  55. package/dist/src/parse/tree-store.js +236 -0
  56. package/dist/src/parse/tree-store.js.map +1 -0
  57. package/dist/src/platform/dependency-rule.d.ts +27 -0
  58. package/dist/src/platform/dependency-rule.js +21 -0
  59. package/dist/src/platform/dependency-rule.js.map +1 -0
  60. package/dist/src/platform/deps.d.ts +86 -0
  61. package/dist/src/platform/deps.js +304 -0
  62. package/dist/src/platform/deps.js.map +1 -0
  63. package/dist/src/platform/php-facts.d.ts +49 -0
  64. package/dist/src/platform/php-facts.js +67 -0
  65. package/dist/src/platform/php-facts.js.map +1 -0
  66. package/dist/src/platform/runtime-version.d.ts +78 -0
  67. package/dist/src/platform/runtime-version.js +573 -0
  68. package/dist/src/platform/runtime-version.js.map +1 -0
  69. package/dist/src/report/colors.d.ts +50 -0
  70. package/dist/src/report/colors.js +97 -0
  71. package/dist/src/report/colors.js.map +1 -0
  72. package/dist/src/report/coverage-table.d.ts +90 -0
  73. package/dist/src/report/coverage-table.js +291 -0
  74. package/dist/src/report/coverage-table.js.map +1 -0
  75. package/dist/src/report/html-report.d.ts +66 -0
  76. package/dist/src/report/html-report.js +489 -0
  77. package/dist/src/report/html-report.js.map +1 -0
  78. package/dist/src/report/human.d.ts +44 -0
  79. package/dist/src/report/human.js +575 -0
  80. package/dist/src/report/human.js.map +1 -0
  81. package/dist/src/report/json.d.ts +24 -0
  82. package/dist/src/report/json.js +178 -0
  83. package/dist/src/report/json.js.map +1 -0
  84. package/dist/src/report/mark.d.ts +17 -0
  85. package/dist/src/report/mark.js +39 -0
  86. package/dist/src/report/mark.js.map +1 -0
  87. package/dist/src/report/pricing.d.ts +33 -0
  88. package/dist/src/report/pricing.js +343 -0
  89. package/dist/src/report/pricing.js.map +1 -0
  90. package/dist/src/report/sarif.d.ts +39 -0
  91. package/dist/src/report/sarif.js +219 -0
  92. package/dist/src/report/sarif.js.map +1 -0
  93. package/dist/src/report/site.d.ts +87 -0
  94. package/dist/src/report/site.js +742 -0
  95. package/dist/src/report/site.js.map +1 -0
  96. package/dist/src/rules/code-injection.d.ts +47 -0
  97. package/dist/src/rules/code-injection.js +326 -0
  98. package/dist/src/rules/code-injection.js.map +1 -0
  99. package/dist/src/rules/command-injection.d.ts +30 -0
  100. package/dist/src/rules/command-injection.js +286 -0
  101. package/dist/src/rules/command-injection.js.map +1 -0
  102. package/dist/src/rules/contract.d.ts +198 -0
  103. package/dist/src/rules/contract.js +39 -0
  104. package/dist/src/rules/contract.js.map +1 -0
  105. package/dist/src/rules/format-string.d.ts +46 -0
  106. package/dist/src/rules/format-string.js +146 -0
  107. package/dist/src/rules/format-string.js.map +1 -0
  108. package/dist/src/rules/hardcoded-secret.d.ts +38 -0
  109. package/dist/src/rules/hardcoded-secret.js +440 -0
  110. package/dist/src/rules/hardcoded-secret.js.map +1 -0
  111. package/dist/src/rules/lib/guards.d.ts +168 -0
  112. package/dist/src/rules/lib/guards.js +1179 -0
  113. package/dist/src/rules/lib/guards.js.map +1 -0
  114. package/dist/src/rules/lib/html-safety.d.ts +75 -0
  115. package/dist/src/rules/lib/html-safety.js +755 -0
  116. package/dist/src/rules/lib/html-safety.js.map +1 -0
  117. package/dist/src/rules/lib/local-build.d.ts +59 -0
  118. package/dist/src/rules/lib/local-build.js +320 -0
  119. package/dist/src/rules/lib/local-build.js.map +1 -0
  120. package/dist/src/rules/lib/strings.d.ts +208 -0
  121. package/dist/src/rules/lib/strings.js +1970 -0
  122. package/dist/src/rules/lib/strings.js.map +1 -0
  123. package/dist/src/rules/path-traversal.d.ts +33 -0
  124. package/dist/src/rules/path-traversal.js +96 -0
  125. package/dist/src/rules/path-traversal.js.map +1 -0
  126. package/dist/src/rules/registry.d.ts +19 -0
  127. package/dist/src/rules/registry.js +77 -0
  128. package/dist/src/rules/registry.js.map +1 -0
  129. package/dist/src/rules/sql-injection.d.ts +36 -0
  130. package/dist/src/rules/sql-injection.js +299 -0
  131. package/dist/src/rules/sql-injection.js.map +1 -0
  132. package/dist/src/rules/ssrf.d.ts +39 -0
  133. package/dist/src/rules/ssrf.js +111 -0
  134. package/dist/src/rules/ssrf.js.map +1 -0
  135. package/dist/src/rules/unbounded-copy.d.ts +37 -0
  136. package/dist/src/rules/unbounded-copy.js +147 -0
  137. package/dist/src/rules/unbounded-copy.js.map +1 -0
  138. package/dist/src/rules/unsafe-deserialization.d.ts +43 -0
  139. package/dist/src/rules/unsafe-deserialization.js +276 -0
  140. package/dist/src/rules/unsafe-deserialization.js.map +1 -0
  141. package/dist/src/rules/weak-hash.d.ts +37 -0
  142. package/dist/src/rules/weak-hash.js +197 -0
  143. package/dist/src/rules/weak-hash.js.map +1 -0
  144. package/dist/src/rules/xss.d.ts +38 -0
  145. package/dist/src/rules/xss.js +527 -0
  146. package/dist/src/rules/xss.js.map +1 -0
  147. package/dist/src/taint/content-type.d.ts +19 -0
  148. package/dist/src/taint/content-type.js +20 -0
  149. package/dist/src/taint/content-type.js.map +1 -0
  150. package/dist/src/taint/dictionaries.d.ts +23 -0
  151. package/dist/src/taint/dictionaries.js +1778 -0
  152. package/dist/src/taint/dictionaries.js.map +1 -0
  153. package/dist/src/taint/fold.d.ts +59 -0
  154. package/dist/src/taint/fold.js +1177 -0
  155. package/dist/src/taint/fold.js.map +1 -0
  156. package/dist/src/taint/java-scope.d.ts +61 -0
  157. package/dist/src/taint/java-scope.js +212 -0
  158. package/dist/src/taint/java-scope.js.map +1 -0
  159. package/dist/src/taint/local-map.d.ts +79 -0
  160. package/dist/src/taint/local-map.js +603 -0
  161. package/dist/src/taint/local-map.js.map +1 -0
  162. package/dist/src/taint/project.d.ts +172 -0
  163. package/dist/src/taint/project.js +617 -0
  164. package/dist/src/taint/project.js.map +1 -0
  165. package/dist/src/taint/tracer.d.ts +265 -0
  166. package/dist/src/taint/tracer.js +3817 -0
  167. package/dist/src/taint/tracer.js.map +1 -0
  168. package/dist/src/taint/types.d.ts +484 -0
  169. package/dist/src/taint/types.js +47 -0
  170. package/dist/src/taint/types.js.map +1 -0
  171. package/dist/src/taint/visibility.d.ts +25 -0
  172. package/dist/src/taint/visibility.js +96 -0
  173. package/dist/src/taint/visibility.js.map +1 -0
  174. package/docs/examples/defuse.json +20 -0
  175. package/docs/examples/dvwa-urls.json +14 -0
  176. package/docs/examples/github-code-scanning.yml +54 -0
  177. package/docs/examples/tiny.js +4 -0
  178. package/package.json +73 -4
  179. package/src/cli.ts +747 -0
  180. package/src/core/analyze.ts +1072 -0
  181. package/src/core/coverage.ts +269 -0
  182. package/src/core/finding.ts +311 -0
  183. package/src/core/licence.ts +61 -0
  184. package/src/core/paths.ts +59 -0
  185. package/src/core/score.ts +353 -0
  186. package/src/dast/targets.ts +121 -0
  187. package/src/dast/zap.ts +246 -0
  188. package/src/engine/scan.ts +200 -0
  189. package/src/engine/shapes.ts +348 -0
  190. package/src/engine/walk.ts +196 -0
  191. package/src/parse/astDump.ts +85 -0
  192. package/src/parse/languages.ts +232 -0
  193. package/src/parse/location.ts +44 -0
  194. package/src/parse/node-grammars.ts +41 -0
  195. package/src/parse/parser.ts +262 -0
  196. package/src/parse/tree-store.ts +301 -0
  197. package/src/platform/dependency-rule.ts +33 -0
  198. package/src/platform/deps.ts +374 -0
  199. package/src/platform/php-facts.ts +85 -0
  200. package/src/platform/runtime-version.ts +710 -0
  201. package/src/report/colors.ts +107 -0
  202. package/src/report/coverage-table.ts +359 -0
  203. package/src/report/html-report.ts +601 -0
  204. package/src/report/human.ts +811 -0
  205. package/src/report/json.ts +189 -0
  206. package/src/report/mark.ts +47 -0
  207. package/src/report/pricing.ts +374 -0
  208. package/src/report/sarif.ts +235 -0
  209. package/src/report/site.ts +760 -0
  210. package/src/rules/code-injection.ts +346 -0
  211. package/src/rules/command-injection.ts +296 -0
  212. package/src/rules/contract.ts +220 -0
  213. package/src/rules/format-string.ts +158 -0
  214. package/src/rules/hardcoded-secret.ts +485 -0
  215. package/src/rules/lib/guards.ts +1235 -0
  216. package/src/rules/lib/html-safety.ts +793 -0
  217. package/src/rules/lib/local-build.ts +329 -0
  218. package/src/rules/lib/strings.ts +2034 -0
  219. package/src/rules/path-traversal.ts +101 -0
  220. package/src/rules/registry.ts +90 -0
  221. package/src/rules/sql-injection.ts +311 -0
  222. package/src/rules/ssrf.ts +118 -0
  223. package/src/rules/unbounded-copy.ts +162 -0
  224. package/src/rules/unsafe-deserialization.ts +291 -0
  225. package/src/rules/weak-hash.ts +226 -0
  226. package/src/rules/xss.ts +572 -0
  227. package/src/taint/content-type.ts +20 -0
  228. package/src/taint/dictionaries.ts +1855 -0
  229. package/src/taint/fold.ts +1206 -0
  230. package/src/taint/java-scope.ts +200 -0
  231. package/src/taint/local-map.ts +594 -0
  232. package/src/taint/project.ts +774 -0
  233. package/src/taint/tracer.ts +4298 -0
  234. package/src/taint/types.ts +511 -0
  235. package/src/taint/visibility.ts +100 -0
  236. package/tsconfig.json +61 -0
@@ -0,0 +1,1177 @@
1
+ /**
2
+ * THE CONSTANT FOLDER, SHARED.
3
+ *
4
+ * This lived inside traceFile() for as long as only the tracer needed it. Then
5
+ * the signature rules needed it too: 65 of BenchmarkJava's remaining false
6
+ * positives are SQL strings built from a `bar` that a decidable condition has
7
+ * already set to a literal, and the SQL rule could not see that, because the
8
+ * only thing that knew how to decide a condition was sealed inside the tracer.
9
+ *
10
+ * The alternative was a second, smaller folder in rules/lib. This project has
11
+ * paid for "the same fact written twice" more than once - ASSIGNMENT_NODES below
12
+ * carries a scar from exactly that - so the folder moved out instead, and both
13
+ * callers now use the one copy. Moving it was checked with `npm run identity`,
14
+ * which hashes every finding and every proof step on twelve fixed targets: the
15
+ * move was meant to change nothing, and it had to be shown to change nothing.
16
+ *
17
+ * createFolder() returns a fresh folder per language, per file, so its caches
18
+ * never leak an answer from one file into another.
19
+ */
20
+ /** Whitespace-normalised node text. The tracer has its own copy of this one-liner. */
21
+ function text(node) {
22
+ return (node?.text ?? '').replace(/\s+/g, ' ').trim();
23
+ }
24
+ export const assignmentParts = (node) => {
25
+ const pairs = [
26
+ ['left', 'right'],
27
+ ['name', 'value'],
28
+ ['key', 'value'],
29
+ // C and C++: `char *user = getenv("X")` is an init_declarator, whose
30
+ // fields are `declarator` and `value` rather than any of the above.
31
+ ['declarator', 'value'],
32
+ ];
33
+ for (const [targetField, valueField] of pairs) {
34
+ const target = node.childForFieldName(targetField);
35
+ const value = node.childForFieldName(valueField);
36
+ if (target && value)
37
+ return { target, value };
38
+ }
39
+ return null;
40
+ };
41
+ export const ASSIGNMENT_NODES = new Set([
42
+ 'assignment_expression', // JS/TS, Java
43
+ 'augmented_assignment_expression',
44
+ 'variable_declarator', // JS/TS, Java
45
+ 'assignment', // Python
46
+ 'augmented_assignment',
47
+ 'pair',
48
+ 'public_field_definition',
49
+ 'short_var_declaration', // Go x := ...
50
+ 'assignment_statement', // Go x = ...
51
+ 'var_spec', // Go var x = ...
52
+ 'const_spec', // Go const x = ...
53
+ 'property_element', // PHP class property with a default
54
+ /*
55
+ * C/C++ declaration-with-initialiser: `char *user = getenv("X")`.
56
+ *
57
+ * THIS LIST AND THE SHAPE QUERIES IN engine/shapes.ts ARE THE SAME FACT
58
+ * WRITTEN TWICE, and this entry is what that costs. The query knew about
59
+ * init_declarator from the first commit; the tracer's own walk did not, so
60
+ * every C value bound at its declaration read as clean while direct
61
+ * arguments traced perfectly - `printf(getenv("X"))` was flow-verified and
62
+ * `char *u = getenv("X"); printf(u);` was not, which is most of real C.
63
+ *
64
+ * It failed quietly because the shapes that DO work look like success: a
65
+ * plain `p = argv[1]` is an assignment_expression and was always covered.
66
+ */
67
+ 'init_declarator',
68
+ ]);
69
+ export function createFolder(language) {
70
+ /*
71
+ * CONSTANT CONDITIONS, FOLDED - DELIBERATELY BADLY.
72
+ *
73
+ * The engine reads statements in order and does not evaluate conditions, so
74
+ * a value on the losing side of a decision already made at compile time gets
75
+ * carried anyway. That single blind spot was 128 of the 131 false positives
76
+ * still wearing the proven label.
77
+ *
78
+ * int num = 106;
79
+ * bar = (7 * 18) + num > 200 ? "constant" : param;
80
+ *
81
+ * 126 + 106 = 232. The tainted branch cannot run.
82
+ *
83
+ * THIS FOLDER IS SMALL ON PURPOSE. It understands integer and boolean
84
+ * literals, parentheses, negation, arithmetic, comparison and the logical
85
+ * connectives, plus a local variable bound exactly once to something it can
86
+ * already fold. Everything else returns undefined, which means "cannot tell",
87
+ * which means the branch is assumed live and nothing changes.
88
+ *
89
+ * The asymmetry matters. A folder that is too weak costs a false positive
90
+ * that was already there. A folder that is too clever - one that decides
91
+ * `"abc".length() > 2` by modelling String, or evaluates a call - eventually
92
+ * gets a condition wrong in the other direction and DELETES A REAL FINDING.
93
+ * A miss is the failure mode this tool exists to prevent, so every doubt
94
+ * resolves toward keeping the flow.
95
+ *
96
+ * Grammar-agnostic by construction: it reads the `operator` field and the
97
+ * node's own text rather than switching on per-language node names, because
98
+ * eight grammars spell a literal eight ways and a name this list forgot
99
+ * would fail silently toward undefined.
100
+ */
101
+ const foldLiteral = (raw) => {
102
+ const t = raw.trim();
103
+ if (t === 'true' || t === 'True')
104
+ return true;
105
+ if (t === 'false' || t === 'False')
106
+ return false;
107
+ /*
108
+ * A leading zero means OCTAL in Java, C, C++, Go, PHP and sloppy JavaScript:
109
+ * `010` is 8, and reading it as ten decided `n < 9` the wrong way round. It
110
+ * is not worth modelling - a leading-zero literal is "cannot tell".
111
+ */
112
+ if (/^-?0\d/.test(t))
113
+ return undefined;
114
+ if (/^-?\d+$/.test(t))
115
+ return Number(t);
116
+ if (/^-?\d+[lL]$/.test(t))
117
+ return Number(t.slice(0, -1));
118
+ return undefined;
119
+ };
120
+ /**
121
+ * The bare name out of whatever the grammar wrapped it in.
122
+ *
123
+ * `String bar` is Java declaring and naming in one node, so everything up to
124
+ * the last space goes. `$num` is PHP, and the folder reaches the inner `name`
125
+ * node - text `num` - while the assignment target is the outer `variable_name`
126
+ * - text `$num`. Comparing those two raw is a mismatch that looks exactly
127
+ * like "this variable is never assigned", which is a silent undefined.
128
+ */
129
+ const bareName = (raw) => raw.trim().replace(/^.*\s/, '').replace(/^\$/, '');
130
+ /**
131
+ * A local name bound exactly once, to something foldable. Two assignments and
132
+ * we stop: the value at this point would depend on order and reachability,
133
+ * which is the very thing we are not modelling.
134
+ */
135
+ /*
136
+ * One walk per name per function, not one per mention.
137
+ *
138
+ * Keyed on the body's SOURCE SPAN rather than on the node, because the
139
+ * tree-sitter binding hands back a fresh wrapper object every time a child is
140
+ * asked for - a Map keyed by Node would miss on every lookup and quietly
141
+ * degrade into no cache at all, which is the same mistake that once made the
142
+ * dead-branch skip compare two identical nodes and decide they differed.
143
+ */
144
+ const constantCache = new Map();
145
+ /*
146
+ * THE CYCLE GUARD, AND THE DEPTH THAT USED TO RESET TO ZERO.
147
+ *
148
+ * Resolving a name folds whatever it was assigned, and folding that can hit
149
+ * another name. `x = y + 1; y = x + 1` is a loop, and until now the recursion
150
+ * had nothing to stop it: foldExpression carries a depth cap of 12, but the
151
+ * call BACK into it from here passed 0, so every hop through a name reset the
152
+ * budget and the cap could never be reached.
153
+ *
154
+ * That was survivable while the folder barely reached names at all. Reading
155
+ * operators off the shape made it reach them constantly, and DVWA's packed
156
+ * sha256 - one 10,417-character line, hundreds of one-letter names assigned
157
+ * from each other - turned it into an out-of-memory on a 10KB file.
158
+ *
159
+ * Two changes, and each would have been enough on its own: the depth now
160
+ * threads through instead of resetting, and a name currently being resolved
161
+ * answers "cannot tell" if it is asked about again. Two, because the depth is
162
+ * the principle and the cycle guard is the proof - a budget that merely runs
163
+ * out still does the work first, and a loop should cost nothing at all.
164
+ */
165
+ const resolving = new Set();
166
+ /*
167
+ * "BOUND EXACTLY ONCE" WAS NOT ENOUGH, AND IT WAS DELETING REAL FINDINGS.
168
+ *
169
+ * The rule used to be: find every `=` to this name in the function, and if
170
+ * there is exactly one, the name IS that value. Probed before the switch
171
+ * folding was built on top of it, that rule was wrong in every language here,
172
+ * in ordinary code, and wrong in the dangerous direction:
173
+ *
174
+ * void f(..., int num) { if (x) num = 500; bar = num > 200 ? "safe" : param; }
175
+ * for (int i = 0; i < 10; i++) { bar = i > 5 ? param : "safe"; }
176
+ * n := 0; fmt.Sscan(s, &n); if n > 5 { bar = param }
177
+ *
178
+ * One `=` each - so `num` was 500, `i` was 0 and `n` was 0, a branch that
179
+ * really runs was declared dead, a real SQL injection went unreported, and
180
+ * the SQL rule printed a receipt calling the line PROVABLY CONSTANT. A
181
+ * parameter already holds the caller's value before any `=`; `i++` and `&n`
182
+ * write without an `=`; a field is not this function's to fix.
183
+ *
184
+ * So the single write now has to be one that CANNOT be bypassed:
185
+ *
186
+ * - a DECLARATION with its value (Java, C, C++, Go, JS let/const), and the
187
+ * use inside that declaration's block, after it. Block scoping is what
188
+ * makes it sound: nothing outside the block can name it, and nothing
189
+ * inside can read it before the declaration runs.
190
+ * - in Python, PHP and a JavaScript `var` there are no block-scoped
191
+ * declarations, so the write must sit at the TOP LEVEL of the function
192
+ * body - not inside an if, a loop, a try - and before the read. A write
193
+ * there cannot be skipped, so it replaces a parameter's value outright.
194
+ * - and nothing else may write to it: no `++`/`--`, no address taken, no
195
+ * loop or `with`/`as` binding, no `global`, and - in PHP and C++, where a
196
+ * call site does not show that an argument is passed by reference - not
197
+ * handed to any call at all.
198
+ *
199
+ * Anything else is "cannot tell". SwitchOnConstant.java and folder-soundness.*
200
+ * are the fence; every case in them was a missed vulnerability.
201
+ */
202
+ const wordIn = (haystack, name) => new RegExp(`(^|[^\\w$])\\$?${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w$])`).test(haystack);
203
+ /** Node types that write to a name without an `=`, and the field that names it. */
204
+ const OTHER_BINDERS = [
205
+ ['for_statement', 'left'], // Python for i in ...
206
+ ['for_in_statement', 'left'], // JS for (x of xs)
207
+ ['enhanced_for_statement', 'name'], // Java for (String s : xs)
208
+ ['range_clause', 'left'], // Go for i := range xs
209
+ ['for_range_loop', 'declarator'], // C++ for (auto x : xs)
210
+ ['named_expression', 'name'], // Python (x := ...)
211
+ ['as_pattern_target', null], // Python with ... as x, except E as x
212
+ ['catch_formal_parameter', 'name'], // Java
213
+ ['catch_clause', 'parameter'], // JS
214
+ ['global_statement', null], // Python
215
+ ['nonlocal_statement', null], // Python
216
+ ['global_declaration', null], // PHP
217
+ ['function_static_declaration', null], // PHP
218
+ ['static_variable_declaration', null], // PHP
219
+ ['foreach_statement', null], // PHP checked below: the part after `as`
220
+ ['case_pattern', null], // Python match x: case n: - a bare name CAPTURES
221
+ ];
222
+ const INCREMENT_NODES = new Set(['update_expression', 'inc_statement', 'dec_statement']);
223
+ const BINDER_FIELD = new Map(OTHER_BINDERS);
224
+ /** Where an address is taken, in the grammars that can take one. */
225
+ const ADDRESS_OF = new Set([
226
+ 'pointer_expression', // C, C++ &n
227
+ 'unary_expression', // Go &n
228
+ 'by_ref', // PHP foreach ($a as &$v), function f(&$x)
229
+ 'reference_assignment_expression', // PHP $y = &$n
230
+ ]);
231
+ /*
232
+ * Checked on every node of the function for every name the folder asks
233
+ * about, so it must decide by node TYPE before it reads any text: reading
234
+ * the text of a large node copies it out of the parser, and doing that for
235
+ * each node made a BenchmarkJava scan measurably slower.
236
+ */
237
+ const writesWithoutAssignment = (node, name) => {
238
+ const type = node.type;
239
+ if (INCREMENT_NODES.has(type))
240
+ return wordIn(node.text, name);
241
+ // `&n` - C, C++, Go - and PHP's `&$n`: whoever holds the address can write.
242
+ if (ADDRESS_OF.has(type)) {
243
+ // Comments and parentheses are dropped first: `&(n)` and `&/*out*/n` are
244
+ // both the address of n.
245
+ const t = node.text;
246
+ if (t.length > 200)
247
+ return false;
248
+ const flat = t.replace(/\/\*[\s\S]*?\*\//g, '').replace(/[()\s]/g, '');
249
+ return new RegExp(`&\\$?${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w$])`).test(flat);
250
+ }
251
+ // Go: `n.Set(s)` takes &n implicitly when Set has a pointer receiver.
252
+ if (language === 'go' && type === 'call_expression') {
253
+ const fn = node.childForFieldName('function');
254
+ if (fn?.type === 'selector_expression' && bareName(fn.childForFieldName('operand')?.text ?? '') === name)
255
+ return true;
256
+ }
257
+ if (language === 'c' || language === 'cpp') {
258
+ // A macro can assign to its argument - `SET_INT(n, s)` - and nothing at the
259
+ // call says so. Upper-case callees are read as macros, by convention.
260
+ if (type === 'call_expression' && /^[A-Z][A-Z0-9_]*$/.test(node.childForFieldName('function')?.text ?? '')) {
261
+ const args = node.childForFieldName('arguments');
262
+ if (args?.namedChildren.some((c) => !!c && bareName(c.text.replace(/[()\s]/g, '')) === name))
263
+ return true;
264
+ }
265
+ // `#define n ...` makes every later `n` something else entirely.
266
+ if ((type === 'preproc_def' || type === 'preproc_function_def') && node.childForFieldName('name')?.text === name) {
267
+ return true;
268
+ }
269
+ }
270
+ const field = BINDER_FIELD.get(type);
271
+ if (field !== undefined) {
272
+ if (type === 'foreach_statement') {
273
+ const afterAs = node.text.split(/\bas\b/)[1]?.split(')')[0] ?? '';
274
+ return wordIn(afterAs, name);
275
+ }
276
+ const binder = field ? node.childForFieldName(field) : node;
277
+ return !!binder && wordIn(binder.text, name);
278
+ }
279
+ // PHP and C++ pass by reference invisibly: `preg_match($re, $s, $n)` and
280
+ // `void f(int &x); f(n);` both write to n with nothing at the call site.
281
+ if (language === 'php' && type === 'argument')
282
+ return bareName(node.text) === name;
283
+ if (language === 'cpp') {
284
+ if (type === 'argument_list')
285
+ return node.namedChildren.some((c) => !!c && bareName(c.text) === name);
286
+ // `std::cin >> n`, and `int &r = n` - a second name for the same storage.
287
+ if (type === 'binary_expression' && node.childForFieldName('operator')?.text === '>>' &&
288
+ bareName(node.childForFieldName('right')?.text ?? '') === name)
289
+ return true;
290
+ if (type === 'init_declarator' && node.childForFieldName('declarator')?.type === 'reference_declarator' &&
291
+ bareName(node.childForFieldName('value')?.text ?? '') === name)
292
+ return true;
293
+ }
294
+ return false;
295
+ };
296
+ /**
297
+ * The region a single write governs, or null when that write can be bypassed.
298
+ * See the note above: a block-scoped declaration governs its block; a plain
299
+ * write governs the function only when it sits at the top of the body.
300
+ */
301
+ /*
302
+ * WHAT RANGE A DECLARED TYPE CAN HOLD, because arithmetic does not wrap here
303
+ * and does in the program. `int t = 30 * 24 * 60 * 60 * 1000` is NEGATIVE in
304
+ * Java; `unsigned n = 0 - 1` is huge in C; `signed char c = 200` is -56. A
305
+ * value outside what its type holds is "cannot tell". Null means the type is
306
+ * not one whose numbers are modelled at all - unsigned, floating point, a
307
+ * typedef, a class - and then no number bound to it is trusted.
308
+ */
309
+ const INT32 = [-2147483648, 2147483647];
310
+ const INT16 = [-32768, 32767];
311
+ const SAFE = [-(2 ** 53), 2 ** 53];
312
+ const rangeOfType = (typeText) => {
313
+ const t = (typeText ?? '').replace(/\b(?:const|constexpr|static|final|volatile|register|signed)\b/g, ' ').replace(/\s+/g, ' ').trim();
314
+ if (language === 'java') {
315
+ if (t === 'int' || t === 'long' || t === 'var')
316
+ return INT32;
317
+ if (t === 'short')
318
+ return INT16;
319
+ if (t === 'byte')
320
+ return [-128, 127];
321
+ if (t === 'char')
322
+ return [0, 65535];
323
+ return null;
324
+ }
325
+ if (language === 'c' || language === 'cpp') {
326
+ if (/^(?:int|long|long int|long long|long long int|short int|)$/.test(t))
327
+ return t === '' ? INT32 : INT32;
328
+ if (t === 'short')
329
+ return INT16;
330
+ // Plain `char` may be signed or unsigned; 0-127 is the same either way.
331
+ if (t === 'char')
332
+ return [0, 127];
333
+ return null;
334
+ }
335
+ if (language === 'go') {
336
+ if (t === '' || t === 'int' || t === 'int32' || t === 'int64' || t === 'rune')
337
+ return INT32;
338
+ if (t === 'int16')
339
+ return INT16;
340
+ if (t === 'int8')
341
+ return [-128, 127];
342
+ if (t === 'byte' || t === 'uint8')
343
+ return [0, 255];
344
+ return null;
345
+ }
346
+ return SAFE;
347
+ };
348
+ const regionOfWrite = (write, body) => {
349
+ const found = regionNodeOfWrite(write, body);
350
+ if (!found)
351
+ return null;
352
+ const parent = write.parent;
353
+ let typeText;
354
+ if (language === 'java' || language === 'c' || language === 'cpp') {
355
+ typeText = parent?.childForFieldName('type')?.text;
356
+ // C++ references and aliasing mean a mutable local cannot be read from
357
+ // the text alone - `int &r{n}` writes n with no `n` in sight. Only a
358
+ // `const` or `constexpr` local is a constant in C++.
359
+ if (language === 'cpp' && !parent?.namedChildren.some((c) => c?.type === 'type_qualifier' && /^(?:const|constexpr)$/.test(c.text))) {
360
+ return null;
361
+ }
362
+ // An unsigned or sized declaration keeps its qualifiers in the type node.
363
+ if (language !== 'java') {
364
+ const qualifiers = (parent?.namedChildren ?? []).filter((c) => c?.type === 'type_qualifier' || c?.type === 'storage_class_specifier').map((c) => c?.text ?? '');
365
+ typeText = `${qualifiers.join(' ')} ${typeText ?? ''}`;
366
+ }
367
+ }
368
+ else if (language === 'go' && write.type === 'var_spec') {
369
+ typeText = write.childForFieldName('type')?.text;
370
+ }
371
+ return { region: found, range: rangeOfType(typeText) };
372
+ };
373
+ const regionNodeOfWrite = (write, body) => {
374
+ const parent = write.parent;
375
+ if (!parent)
376
+ return null;
377
+ const same = (a, b) => !!a && a.startIndex === b.startIndex && a.endIndex === b.endIndex;
378
+ // The tracer hands over a function's BODY; the constant proof hands over the
379
+ // FUNCTION. Both mean the same scope, and the top-level tests below need the
380
+ // block either way.
381
+ const block = body.childForFieldName('body') ?? body;
382
+ if (language === 'java' && write.type === 'variable_declarator' && parent.type === 'local_variable_declaration') {
383
+ return parent.parent;
384
+ }
385
+ if ((language === 'c' || language === 'cpp') && write.type === 'init_declarator' && parent.type === 'declaration') {
386
+ return parent.parent;
387
+ }
388
+ if (language === 'go' && (write.type === 'short_var_declaration' || write.type === 'var_spec' || write.type === 'const_spec')) {
389
+ const holder = write.type === 'short_var_declaration' ? parent : parent.parent;
390
+ return holder ?? null;
391
+ }
392
+ if ((language === 'javascript' || language === 'typescript') && write.type === 'variable_declarator') {
393
+ if (parent.type === 'lexical_declaration')
394
+ return parent.parent;
395
+ // `var` is function-scoped and hoisted: only the top of the body is safe.
396
+ if (parent.type === 'variable_declaration' && same(parent.parent, block))
397
+ return block;
398
+ return null;
399
+ }
400
+ /*
401
+ * No parameter check is needed here, and that is not an oversight: a write
402
+ * at the top of the body runs before any read after it, so it replaces the
403
+ * caller's value rather than competing with it. What a parameter breaks is
404
+ * a write that might NOT run - and that is exactly what "top level" rules out.
405
+ */
406
+ if ((language === 'python' || language === 'php') && parent.type === 'expression_statement' && same(parent.parent, block)) {
407
+ return block;
408
+ }
409
+ return null;
410
+ };
411
+ const surveyCache = new Map();
412
+ /*
413
+ * NAMES THAT CANNOT BE READ FROM THE TEXT AT ALL. `with` and `eval` in
414
+ * JavaScript, `$$name`, `${...}`, extract() and eval() in PHP, exec() and
415
+ * locals() in Python: any of them in a function can write to any local in
416
+ * it, under a name that appears nowhere. One of them present, and no name in
417
+ * that function is a constant.
418
+ */
419
+ const dynamicScopeCache = new Map();
420
+ const DYNAMIC_CALLS = new Set(['eval', 'exec', 'extract', 'parse_str', 'locals', 'globals', 'vars']);
421
+ const hasDynamicScope = (body) => {
422
+ const key = `${body.startIndex}:${body.endIndex}`;
423
+ const cached = dynamicScopeCache.get(key);
424
+ if (cached !== undefined)
425
+ return cached;
426
+ /*
427
+ * A function the grammar could not fully parse may hide a write inside the
428
+ * part it gave up on - Java's grammar reads `(n) = x;` as an error, so that
429
+ * write was invisible and the declaration looked like the only one.
430
+ */
431
+ let found = body.hasError;
432
+ let walked = 0;
433
+ /*
434
+ * PYTHON FUNCTIONS ARE THE EXCEPTION. Since Python 3, exec/eval/locals()
435
+ * inside a function work on a COPY of its locals - they cannot rebind one
436
+ * (PEP 667 made that the language's word in 3.13; earlier releases behave
437
+ * the same without tracing hooks). So inside a function they only count
438
+ * when a `global`/`nonlocal` line lets a name escape to a scope exec CAN
439
+ * write. Module and class bodies are real dicts, and keep the old answer.
440
+ * Refusing here had stopped BenchmarkPython's `num = 86; if 7*42 - num > 200`
441
+ * from ever being decided in a function that went on to call exec().
442
+ */
443
+ const pyFunction = language === 'python' && (body.type === 'function_definition' || body.parent?.type === 'function_definition');
444
+ let pyDynamicCall = false;
445
+ let pyEscape = false;
446
+ const visit = (n) => {
447
+ if (found || ++walked > 20000) {
448
+ found = true;
449
+ return;
450
+ }
451
+ const type = n.type;
452
+ if (type === 'with_statement' && (language === 'javascript' || language === 'typescript'))
453
+ found = true;
454
+ else if (type === 'dynamic_variable_name')
455
+ found = true;
456
+ // C lets `goto` jump past `int n = 0;` into its block, leaving n unset.
457
+ // (C++ refuses to compile that, so only C gives up here.)
458
+ else if (type === 'goto_statement' && language === 'c')
459
+ found = true;
460
+ else if (type === 'identifier' && n.text === 'arguments' && (language === 'javascript' || language === 'typescript'))
461
+ found = true;
462
+ else if (pyFunction && (type === 'global_statement' || type === 'nonlocal_statement'))
463
+ pyEscape = true;
464
+ else if ((type === 'call_expression' || type === 'call' || type === 'function_call_expression') &&
465
+ DYNAMIC_CALLS.has((n.childForFieldName('function')?.text ?? '').replace(/^\\/, ''))) {
466
+ if (pyFunction)
467
+ pyDynamicCall = true;
468
+ else
469
+ found = true;
470
+ }
471
+ if (!found)
472
+ for (const c of n.namedChildren)
473
+ if (c)
474
+ visit(c);
475
+ };
476
+ visit(body);
477
+ if (pyDynamicCall && pyEscape)
478
+ found = true;
479
+ dynamicScopeCache.set(key, found);
480
+ return found;
481
+ };
482
+ /** Every write to `name` in `body`, and whether any came some other way. */
483
+ const survey = (name, body) => {
484
+ const key = `${body.startIndex}:${body.endIndex}#${name}`;
485
+ const cached = surveyCache.get(key);
486
+ if (cached)
487
+ return cached;
488
+ const writes = [];
489
+ let otherWrite = hasDynamicScope(body);
490
+ let walked = 0;
491
+ const visit = (node, walkDepth) => {
492
+ if (otherWrite)
493
+ return;
494
+ // Out of budget is "cannot tell", never "nothing more to see": a second
495
+ // write in the 20th arm of an else-if chain is still a second write.
496
+ if (walkDepth > 400 || ++walked > 20000) {
497
+ otherWrite = true;
498
+ return;
499
+ }
500
+ /*
501
+ * ASSIGNMENT_NODES first, and this is not belt-and-braces.
502
+ *
503
+ * assignmentParts() looks for a `left`/`right` field pair, and a
504
+ * comparison has exactly that - so `num > 200` was read as an assignment
505
+ * of 200 to num. The folder then saw two assignments to `num` and gave
506
+ * up, which is why every condition with a variable in it silently failed
507
+ * to fold. The real tracer never hit this because it only ever calls
508
+ * assignmentParts on nodes it has already checked.
509
+ */
510
+ const parts = ASSIGNMENT_NODES.has(node.type) ? assignmentParts(node) : null;
511
+ if (parts) {
512
+ const target = text(parts.target);
513
+ if (bareName(target) === name && !/[()[\]{},]/.test(target)) {
514
+ writes.push({ write: node, value: parts.value });
515
+ // `n += 1` and Python's `n += 1` are writes the constant proof must see.
516
+ if (/augmented/.test(node.type) || (node.childForFieldName('operator')?.text ?? '=') !== '=')
517
+ otherWrite = true;
518
+ }
519
+ else if (wordIn(target, name)) {
520
+ /*
521
+ * The name is PART of what is written: `(n) = x`, `n, err := f()`,
522
+ * `[n] = [x]`, `list($n) = ...`, `{ limit } = req.query`. None of those
523
+ * was counted before, so the one plain `n := 0` looked like the only
524
+ * write there was.
525
+ */
526
+ otherWrite = true;
527
+ return;
528
+ }
529
+ }
530
+ else if (writesWithoutAssignment(node, name)) {
531
+ otherWrite = true;
532
+ return;
533
+ }
534
+ for (const child of node.namedChildren)
535
+ if (child)
536
+ visit(child, walkDepth + 1);
537
+ };
538
+ visit(body, 0);
539
+ const result = { writes, otherWrite };
540
+ surveyCache.set(key, result);
541
+ return result;
542
+ };
543
+ const writeCache = new Map();
544
+ /** The one write that decides `name` in `body`, if there is exactly one sound one. */
545
+ const soleWrite = (name, body) => {
546
+ const key = `${body.startIndex}:${body.endIndex}#${name}`;
547
+ if (writeCache.has(key))
548
+ return writeCache.get(key) ?? null;
549
+ const { writes, otherWrite } = survey(name, body);
550
+ const only = writes.length === 1 && !otherWrite ? writes[0] : undefined;
551
+ const found = only ? regionOfWrite(only.write, body) : null;
552
+ const facts = only && found ? { write: only.write, value: only.value, region: found.region, range: found.range } : null;
553
+ writeCache.set(key, facts);
554
+ return facts;
555
+ };
556
+ /** For the constant proof: does `name` receive any write that is not a plain `name = value`? */
557
+ const writtenOtherwise = (name, body) => survey(bareName(name), body).otherWrite;
558
+ /** Every plain write to `name` in `body`, nested functions included. */
559
+ const plainWriteCount = (name, body) => survey(bareName(name), body).writes.length;
560
+ /** Does the sole write certainly run before `use`, and govern it? */
561
+ const writeReaches = (facts, use) => !!use &&
562
+ facts.write.endIndex <= use.startIndex &&
563
+ use.startIndex >= facts.region.startIndex &&
564
+ use.endIndex <= facts.region.endIndex;
565
+ const constantOfName = (rawName, body, depth, use) => {
566
+ const name = bareName(rawName);
567
+ const facts = soleWrite(name, body);
568
+ if (!facts || !writeReaches(facts, use))
569
+ return undefined;
570
+ const scopeKey = `${body.startIndex}:${body.endIndex}`;
571
+ let perScope = constantCache.get(scopeKey);
572
+ if (!perScope) {
573
+ perScope = new Map();
574
+ constantCache.set(scopeKey, perScope);
575
+ }
576
+ if (perScope.has(name))
577
+ return perScope.get(name);
578
+ const cycleKey = `${scopeKey}#${name}`;
579
+ if (resolving.has(cycleKey))
580
+ return undefined;
581
+ resolving.add(cycleKey);
582
+ // `depth` is how many names deep the resolution itself is.
583
+ let answer = foldExpression(facts.value, body, depth + 1);
584
+ resolving.delete(cycleKey);
585
+ if (typeof answer === 'number' && (!facts.range || answer < facts.range[0] || answer > facts.range[1])) {
586
+ answer = undefined;
587
+ }
588
+ /*
589
+ * Cached even when the answer was cut short by the depth budget. A budget
590
+ * that ran out always answers undefined, which means "cannot tell", which
591
+ * keeps the flow - so the worst a stale one can do is leave a false
592
+ * positive that was already there. Caching a NARROWER answer would be the
593
+ * dangerous direction, and undefined is never narrower.
594
+ */
595
+ perScope.set(name, answer);
596
+ return answer;
597
+ };
598
+ /*
599
+ * `s.charAt(i)` - THE ONE STRING METHOD, AND WHY ONLY THIS ONE.
600
+ *
601
+ * The note at the top says a folder that models String eventually deletes a
602
+ * real finding, and that is still the policy. This is the exception, taken
603
+ * because 299 of BenchmarkJava's cases decide a switch with it and because it
604
+ * is as exact as arithmetic: a Java String is immutable, so a string literal,
605
+ * or a local declared once with one, has one character at each index forever.
606
+ * An index out of range throws at runtime, and folds to "cannot tell".
607
+ *
608
+ * Nothing else about strings is folded - not length(), not equals(), not
609
+ * substring() - and a string that is a parameter, a field, or written twice
610
+ * is not a string this knows.
611
+ */
612
+ const CHARACTER_LITERALS = new Set(['character_literal', 'char_literal', 'rune_literal']);
613
+ const characterCode = (raw) => {
614
+ // Printable ASCII only, and no escapes: '\n' and 'é' are not worth being wrong about.
615
+ const match = /^'([\x20-\x26\x28-\x5b\x5d-\x7e])'$/.exec(raw.trim());
616
+ return match?.[1] !== undefined ? match[1].charCodeAt(0) : undefined;
617
+ };
618
+ const stringConstantOf = (node, body, depth) => {
619
+ if (depth > 12)
620
+ return undefined;
621
+ let n = node;
622
+ while (n.type === 'parenthesized_expression' && n.namedChildren.length === 1)
623
+ n = n.namedChildren[0] ?? n;
624
+ if (n.type === 'string_literal') {
625
+ const match = /^"([^"\\]*)"$/.exec(n.text);
626
+ return match?.[1];
627
+ }
628
+ if (n.type !== 'identifier')
629
+ return undefined;
630
+ const facts = soleWrite(n.text, body);
631
+ if (!facts || !writeReaches(facts, n))
632
+ return undefined;
633
+ const cycleKey = `${body.startIndex}:${body.endIndex}#string#${n.text}`;
634
+ if (resolving.has(cycleKey))
635
+ return undefined;
636
+ resolving.add(cycleKey);
637
+ const value = stringConstantOf(facts.value, body, depth + 1);
638
+ resolving.delete(cycleKey);
639
+ return value;
640
+ };
641
+ const charAtOf = (call, body, depth) => {
642
+ if (call.childForFieldName('name')?.text !== 'charAt')
643
+ return undefined;
644
+ const args = (call.childForFieldName('arguments')?.namedChildren ?? []).filter((c) => c !== null);
645
+ const receiver = call.childForFieldName('object');
646
+ if (args.length !== 1 || !receiver)
647
+ return undefined;
648
+ const s = stringConstantOf(receiver, body, depth + 1);
649
+ const index = foldExpression(args[0] ?? null, body, depth + 1);
650
+ if (s === undefined || typeof index !== 'number' || index < 0 || index >= s.length)
651
+ return undefined;
652
+ return s.charCodeAt(index);
653
+ };
654
+ /*
655
+ * THE OPERATOR IS NOT ALWAYS A FIELD, AND PYTHON IS WHY.
656
+ *
657
+ * Java, JavaScript, TypeScript, Go, PHP and the C family all name the two
658
+ * sides of `a > b` left and right, and the `>` operator. Python names none of
659
+ * the three, because Python allows `1 < x < 5` - a comparison there is a LIST
660
+ * of operands with a LIST of operators, so there is no pair to name.
661
+ *
662
+ * The consequence was total rather than partial: no Python comparison folded
663
+ * anywhere, so every constant-branch decoy in Python stayed a false positive
664
+ * and nothing said so.
665
+ *
666
+ * The fallback reads the shape instead of the field names: exactly three
667
+ * children, the outer two named, the middle one an unnamed token. That token
668
+ * is the operator, whatever the grammar calls it. A chained comparison has
669
+ * five children and returns null, which means "cannot tell", which is the
670
+ * safe direction. `a.b` and `a instanceof B` match the shape and hand over
671
+ * `.` and `instanceof`, neither of which the switch below knows, so they fold
672
+ * to undefined exactly as they did before.
673
+ */
674
+ const binaryParts = (node) => {
675
+ const left = node.childForFieldName('left');
676
+ const right = node.childForFieldName('right');
677
+ const named = node.childForFieldName('operator')?.text ?? '';
678
+ if (left && right && named)
679
+ return { left, right, operator: named };
680
+ const children = node.children.filter((c) => c !== null);
681
+ if (children.length !== 3)
682
+ return null;
683
+ const [a, op, b] = children;
684
+ if (!a || !op || !b)
685
+ return null;
686
+ if (!a.isNamed || !b.isNamed || op.isNamed)
687
+ return null;
688
+ return { left: a, right: b, operator: op.text };
689
+ };
690
+ /*
691
+ * The same problem one operand down, and this one was not failing safe.
692
+ *
693
+ * Python's not_operator has no operator field either - `not` is an unnamed
694
+ * child, like the `>` above. The unary branch needs an operator field so it
695
+ * skipped, and the single-child unwrap below picked up a node with exactly
696
+ * one named child and returned it. `not True` folded to TRUE.
697
+ *
698
+ * Everything else in this folder fails toward "cannot tell", where the worst
699
+ * outcome is a false positive that was already there. This one failed toward
700
+ * a CONFIDENT WRONG ANSWER: the engine believed a branch ran when it did not,
701
+ * skipped the arm that really ran, and stopped reporting a real flow. That is
702
+ * the failure this tool exists to prevent, and it was sitting inside the
703
+ * feature that was supposed to be about precision.
704
+ *
705
+ * Restricted to the three operators the switch can actually resolve. An
706
+ * unknown leading token is not treated as unary at all, and falls through to
707
+ * the unwrap guard, which now refuses it.
708
+ */
709
+ const UNARY_TOKENS = new Set(['!', 'not', '-']);
710
+ const unaryParts = (node) => {
711
+ const operand = node.childForFieldName('argument') ?? node.childForFieldName('operand');
712
+ const operator = node.childForFieldName('operator')?.text ?? '';
713
+ if (operand && operator)
714
+ return { operand, operator };
715
+ const children = node.children.filter((c) => c !== null);
716
+ if (children.length !== 2)
717
+ return null;
718
+ const [op, value] = children;
719
+ if (!op || !value || op.isNamed || !value.isNamed)
720
+ return null;
721
+ if (!UNARY_TOKENS.has(op.text))
722
+ return null;
723
+ return { operand: value, operator: op.text };
724
+ };
725
+ /*
726
+ * Unwrapping a single-child node is only safe when nothing was thrown away.
727
+ *
728
+ * `(x)` discards two brackets and means x. `not x` discards the word `not`
729
+ * and does NOT mean x. Both have one named child, and the old test could not
730
+ * tell them apart - which is how `not True` became true.
731
+ *
732
+ * So the test is on what gets discarded rather than on the node's name: every
733
+ * unnamed child must be pure punctuation. A discarded WORD, or any symbol not
734
+ * on this list, means the node transforms its operand in a way this folder
735
+ * has not been taught, and the answer is "cannot tell".
736
+ *
737
+ * `$` is here for PHP, whose variable_name wraps a bare name in a `$` token.
738
+ */
739
+ const WRAPPER_PUNCTUATION = new Set(['(', ')', '[', ']', '{', '}', ',', ';', '$']);
740
+ /*
741
+ * CHECK THE OPERATOR BEFORE FOLDING THE OPERANDS, AND THIS IS NOT A TIDY-UP.
742
+ *
743
+ * Reading the operator off the shape instead of a field made binaryParts
744
+ * match things it was never meant to: `a.b` is three children with the outer
745
+ * two named, so is `a instanceof B`, so is every member access in every
746
+ * minified file on earth. The switch below has no case for `.`, so all of
747
+ * them still folded to undefined and the ANSWER was never wrong.
748
+ *
749
+ * The cost was. Folding an operand that turns out to be a bare name calls
750
+ * constantOfName, and constantOfName walks the entire enclosing function
751
+ * looking for assignments. DVWA ships an inlined sha256 - one 19KB function,
752
+ * thousands of member accesses - and the scan went from finishing to running
753
+ * out of memory on 1.1MB of source with 7GB free.
754
+ *
755
+ * It did not show up in the suite, the benchmark, or the metamorphic run.
756
+ * Fixture files are small and tidy, and BenchmarkJava's generated cases are
757
+ * about forty lines each; this needed one real file written by somebody
758
+ * optimising for size. The corpus check is in the repo precisely so that
759
+ * "all tests pass" is not the last word, and this is the first time it has
760
+ * caught something the tests could not.
761
+ *
762
+ * So the operator is checked first. An operator with no case cannot change
763
+ * any answer, and now it does not cost a tree walk to find that out.
764
+ */
765
+ const FOLDABLE_OPERATORS = new Set([
766
+ '+', '-', '*', '/', '%',
767
+ '>', '<', '>=', '<=', '==', '!=',
768
+ '&&', 'and', '||', 'or',
769
+ ]);
770
+ const INTEGER_DIVISION = language === 'java' || language === 'c' || language === 'cpp' || language === 'go';
771
+ /*
772
+ * Where arithmetic stops being exact. Java, C, C++ and Go wrap at their type's
773
+ * width - checked here at 32 bits, the narrowest `int` any of them uses - and
774
+ * JavaScript, Python and PHP lose integer precision past 2^53. Outside that,
775
+ * "cannot tell".
776
+ */
777
+ const inRange = (value) => INTEGER_DIVISION
778
+ ? value >= -2147483648 && value <= 2147483647 ? value : undefined
779
+ : Math.abs(value) <= 2 ** 53 ? value : undefined;
780
+ const foldExpression = (node, body, depth) => {
781
+ if (!node || depth > 12)
782
+ return undefined;
783
+ // 'B' is 66 in Java, C, C++ and Go, where a character IS an integer. Only
784
+ // the grammars that have a character literal get this; a quoted 'B' in
785
+ // JavaScript, Python or PHP is a string, and strings are not folded.
786
+ if (CHARACTER_LITERALS.has(node.type))
787
+ return characterCode(node.text);
788
+ if (language === 'java' && node.type === 'method_invocation')
789
+ return charAtOf(node, body, depth);
790
+ const binary = binaryParts(node);
791
+ if (binary) {
792
+ const { left, right, operator } = binary;
793
+ if (!FOLDABLE_OPERATORS.has(operator))
794
+ return undefined;
795
+ const a = foldExpression(left, body, depth + 1);
796
+ const b = foldExpression(right, body, depth + 1);
797
+ if (a === undefined || b === undefined)
798
+ return undefined;
799
+ if (typeof a === 'number' && typeof b === 'number') {
800
+ switch (operator) {
801
+ case '+': return inRange(a + b);
802
+ case '-': return inRange(a - b);
803
+ case '*': return inRange(a * b);
804
+ // Integer division truncates in Java, C, C++ and Go. In JavaScript,
805
+ // Python and PHP `7 / 2` is 3.5, and in Python `-7 % 3` is 2.
806
+ case '/': return b === 0 || !INTEGER_DIVISION ? undefined : inRange(Math.trunc(a / b));
807
+ case '%': return b === 0 || (language === 'python' && (a < 0 || b < 0)) ? undefined : inRange(a % b);
808
+ case '>': return a > b;
809
+ case '<': return a < b;
810
+ case '>=': return a >= b;
811
+ case '<=': return a <= b;
812
+ case '==': return a === b;
813
+ case '!=': return a !== b;
814
+ default: return undefined;
815
+ }
816
+ }
817
+ if (typeof a === 'boolean' && typeof b === 'boolean') {
818
+ switch (operator) {
819
+ case '&&':
820
+ case 'and': return a && b;
821
+ case '||':
822
+ case 'or': return a || b;
823
+ case '==': return a === b;
824
+ case '!=': return a !== b;
825
+ default: return undefined;
826
+ }
827
+ }
828
+ return undefined;
829
+ }
830
+ // `!x`, Python's `not x`, and unary minus.
831
+ const unary = unaryParts(node);
832
+ if (unary) {
833
+ const value = foldExpression(unary.operand, body, depth + 1);
834
+ if ((unary.operator === '!' || unary.operator === 'not') && typeof value === 'boolean') {
835
+ return !value;
836
+ }
837
+ if (unary.operator === '-' && typeof value === 'number')
838
+ return inRange(-value);
839
+ return undefined;
840
+ }
841
+ // Parentheses and other single-child wrappers - see WRAPPER_PUNCTUATION.
842
+ const named = node.namedChildren.filter((c) => c !== null);
843
+ if (named.length === 1) {
844
+ const discardsOnlyPunctuation = node.children.every((c) => c === null || c.isNamed || WRAPPER_PUNCTUATION.has(c.text));
845
+ if (discardsOnlyPunctuation)
846
+ return foldExpression(named[0] ?? null, body, depth + 1);
847
+ return undefined;
848
+ }
849
+ if (named.length === 0) {
850
+ const literal = foldLiteral(text(node));
851
+ if (literal !== undefined)
852
+ return literal;
853
+ /*
854
+ * A bare word that is not a literal is a name, and asking whether it is
855
+ * bound to a constant is the whole point of constantOfName.
856
+ *
857
+ * This used to test `/identifier/` against the NODE TYPE, which is true
858
+ * of Java's `identifier` and JavaScript's `identifier` and false of PHP -
859
+ * a PHP variable is a `variable_name` wrapping a `name`, and neither
860
+ * spelling contains the word. So no PHP variable folded, and every PHP
861
+ * condition mentioning one was undecidable.
862
+ *
863
+ * Testing the TEXT covers all eight without a list of node names to keep
864
+ * up to date. A keyword like `null` or `nil` reaches constantOfName,
865
+ * finds no assignment, and returns undefined - the same answer it gave
866
+ * before, by a shorter route.
867
+ */
868
+ if (/^[A-Za-z_][\w]*$/.test(text(node).trim())) {
869
+ return constantOfName(text(node), body, depth, node);
870
+ }
871
+ return undefined;
872
+ }
873
+ return undefined;
874
+ };
875
+ /*
876
+ * THE THREE PARTS OF A TERNARY, WHICH NOT EVERY GRAMMAR AGREES TO NAME.
877
+ *
878
+ * The first version of the folder read `condition`, `consequence` and
879
+ * `alternative` as fields, which is right for Java, JavaScript, TypeScript,
880
+ * Go and the C family - and quietly wrong for two of the eight:
881
+ *
882
+ * python no fields at all, children in WRITTEN order: A if C else B,
883
+ * so the middle child is the condition and the first is the value
884
+ * php names `condition` and `alternative`, but not `consequence`
885
+ *
886
+ * Measured, not assumed. The folder was written against Java, verified
887
+ * against Java, and would have shipped carrying a language-shaped hole -
888
+ * Python would have kept tracing into dead branches while every other
889
+ * language stopped, and nothing would have failed to say so.
890
+ *
891
+ * Python is matched on the language id rather than on a shape heuristic like
892
+ * "no condition field and three children". A heuristic would be one grammar
893
+ * away from silently reading a condition as a value, and that error deletes
894
+ * findings rather than adding them.
895
+ */
896
+ const ternaryParts = (node) => {
897
+ const named = node.namedChildren.filter((c) => c !== null);
898
+ if (language === 'python' && named.length === 3) {
899
+ return {
900
+ consequence: named[0] ?? null,
901
+ condition: named[1] ?? null,
902
+ alternative: named[2] ?? null,
903
+ };
904
+ }
905
+ const condition = node.childForFieldName('condition');
906
+ const alternative = node.childForFieldName('alternative');
907
+ let consequence = node.childForFieldName('consequence');
908
+ // PHP leaves the middle one unnamed; it is whichever child is left over.
909
+ if (!consequence && condition) {
910
+ const same = (a, b) => b !== null && a.startIndex === b.startIndex && a.endIndex === b.endIndex;
911
+ consequence = named.find((c) => !same(c, condition) && !same(c, alternative)) ?? null;
912
+ /*
913
+ * `$a ?: $b` has two operands, not three, so there IS no left-over child
914
+ * and the search above finds nothing. In that form the value when the
915
+ * condition holds is the condition itself.
916
+ *
917
+ * Falling back to the condition rather than to null matters because null
918
+ * would make a decidable elvis select NOTHING, and selecting nothing is
919
+ * how a folder deletes a flow instead of merely failing to narrow one.
920
+ */
921
+ consequence ??= condition;
922
+ }
923
+ return { condition, consequence, alternative };
924
+ };
925
+ /** True or false when the condition is decidable, undefined when it is not. */
926
+ const conditionTruth = (node, body) => {
927
+ const value = foldExpression(node, body, 0);
928
+ return typeof value === 'boolean' ? value : undefined;
929
+ };
930
+ /**
931
+ * The branch of an if-statement that certainly does not run, if any.
932
+ *
933
+ * `consequence ?? body` is PHP, and it is the reason this was HALF broken
934
+ * rather than broken - which is worse, because half a feature looks like a
935
+ * working one. PHP names the else `alternative` like everyone else, so a
936
+ * condition that folded TRUE correctly dropped the dead else. It calls the
937
+ * then-block `body`, not `consequence`, so a condition that folded FALSE
938
+ * asked for a field that does not exist, got null, and kept the dead arm.
939
+ *
940
+ * Same statement, same file, same folder: one direction fixed, one direction
941
+ * silently untouched. Nothing failed, because "no dead branch here" is a
942
+ * perfectly ordinary answer.
943
+ */
944
+ const deadBranchOf = (node, body) => {
945
+ const truth = conditionTruth(node.childForFieldName('condition'), body);
946
+ if (truth === undefined)
947
+ return null;
948
+ return truth
949
+ ? node.childForFieldName('alternative')
950
+ : (node.childForFieldName('consequence') ?? node.childForFieldName('body'));
951
+ };
952
+ /*
953
+ * WHICH CASES OF A SWITCH CAN RUN.
954
+ *
955
+ * Decided only when the selector AND every case label fold to a number or a
956
+ * boolean of the same kind. One label the folder cannot read - an enum
957
+ * constant, a string, a pattern - and the whole switch is undecided, because
958
+ * that label might be the one that matches.
959
+ *
960
+ * From the matching case (or `default` when none matches, or nothing at all
961
+ * when there is no default), execution runs on into the next case until a
962
+ * `break`, `return`, `throw` or `continue` sits DIRECTLY in a case - one
963
+ * nested inside an `if` might not run, so it does not stop the fall. Go and
964
+ * Java's arrow cases do not fall through; Go's explicit `fallthrough` does.
965
+ * Every case outside that run is dead.
966
+ *
967
+ * C has one more way in: `goto`. A label inside the switch can be jumped to
968
+ * whatever the selector says, and so can a case label buried inside a nested
969
+ * block (Duff's device). Either one, and the switch is left undecided.
970
+ *
971
+ * `certain` answers a narrower question for the tracer: does this statement
972
+ * run EVERY time the switch does? Only if it is in the run and no jump of any
973
+ * kind - nested or not - comes before it in the run. A clean write that is
974
+ * certain may clear a value; one that is merely possible may not.
975
+ */
976
+ const switchDecision = (node, body) => {
977
+ const shape = switchShape(node);
978
+ if (!shape || !shape.selector)
979
+ return null;
980
+ const selector = foldExpression(shape.selector, body, 0);
981
+ if (selector === undefined)
982
+ return null;
983
+ let matched = -1;
984
+ let defaultIndex = -1;
985
+ for (let i = 0; i < shape.groups.length; i++) {
986
+ const group = shape.groups[i];
987
+ if (!group)
988
+ return null;
989
+ if (group.labels === 'default') {
990
+ if (defaultIndex === -1)
991
+ defaultIndex = i;
992
+ continue;
993
+ }
994
+ for (const label of group.labels) {
995
+ const value = foldExpression(label, body, 0);
996
+ if (value === undefined || typeof value !== typeof selector)
997
+ return null;
998
+ if (matched === -1 && value === selector)
999
+ matched = i;
1000
+ }
1001
+ }
1002
+ const start = matched !== -1 ? matched : defaultIndex;
1003
+ const live = [];
1004
+ if (start !== -1) {
1005
+ for (let i = start; i < shape.groups.length; i++) {
1006
+ const group = shape.groups[i];
1007
+ if (!group)
1008
+ break;
1009
+ live.push(i);
1010
+ if (shape.fallthrough === 'never')
1011
+ break;
1012
+ if (shape.fallthrough === 'explicit') {
1013
+ if (group.statements[group.statements.length - 1]?.type !== 'fallthrough_statement')
1014
+ break;
1015
+ continue;
1016
+ }
1017
+ if (group.statements.some((s) => TERMINATORS.has(s.type)))
1018
+ break;
1019
+ }
1020
+ }
1021
+ const liveSet = new Set(live);
1022
+ const dead = shape.groups.filter((_, i) => !liveSet.has(i)).map((g) => g.node);
1023
+ const inside = (outer, inner) => inner.startIndex >= outer.startIndex && inner.endIndex <= outer.endIndex;
1024
+ const certain = (n) => {
1025
+ for (const i of live) {
1026
+ const group = shape.groups[i];
1027
+ if (!group)
1028
+ return false;
1029
+ const jumpBefore = containsJumpBefore(group.node, n.startIndex);
1030
+ if (inside(group.node, n))
1031
+ return !jumpBefore;
1032
+ if (jumpBefore)
1033
+ return false;
1034
+ }
1035
+ return false;
1036
+ };
1037
+ const matchedGroup = matched !== -1 ? shape.groups[matched] : undefined;
1038
+ const line = node.startPosition.row + 1;
1039
+ const note = matchedGroup && matchedGroup.labels !== 'default'
1040
+ ? `the switch on line ${line} always takes \`case ${text(matchedGroup.labels.find((l) => foldExpression(l, body, 0) === selector) ?? null)}\``
1041
+ : start !== -1
1042
+ ? `the switch on line ${line} matches no case, so only \`default\` runs`
1043
+ : `the switch on line ${line} matches no case and has no default, so none of it runs`;
1044
+ return { dead, certain, note };
1045
+ };
1046
+ return { foldExpression, conditionTruth, ternaryParts, deadBranchOf, switchDecision, writtenOtherwise, plainWriteCount };
1047
+ }
1048
+ /** Every switch node type, in every grammar here - decided or not. */
1049
+ export const SWITCH_NODES = new Set([
1050
+ 'switch_statement', // JS, TS, C, C++, PHP
1051
+ 'switch_expression', // Java - the statement form too
1052
+ 'expression_switch_statement', // Go
1053
+ ]);
1054
+ /** A statement that leaves a switch when it sits directly in a case. */
1055
+ const TERMINATORS = new Set([
1056
+ 'break_statement',
1057
+ 'continue_statement',
1058
+ 'return_statement',
1059
+ 'throw_statement',
1060
+ 'yield_statement',
1061
+ 'goto_statement',
1062
+ ]);
1063
+ /** Any statement that can leave early, wherever it is nested. */
1064
+ const JUMPS = new Set([...TERMINATORS, 'labeled_statement']);
1065
+ function containsJumpBefore(node, before) {
1066
+ if (node.startIndex >= before)
1067
+ return false;
1068
+ if (JUMPS.has(node.type))
1069
+ return true;
1070
+ for (const child of node.namedChildren) {
1071
+ if (child && child.startIndex < before && containsJumpBefore(child, before))
1072
+ return true;
1073
+ }
1074
+ return false;
1075
+ }
1076
+ const named = (n) => (n?.namedChildren ?? []).filter((c) => c !== null);
1077
+ /**
1078
+ * Read a switch into one shape, whatever the grammar calls its parts. Five
1079
+ * grammars, five vocabularies - measured from each grammar's own trees, not
1080
+ * assumed from one. Null means "not a shape this knows", and an unknown shape
1081
+ * is an undecided switch.
1082
+ */
1083
+ function switchShape(node) {
1084
+ const defaultOf = (n) => /^default\b/.test(n.text.trim());
1085
+ // Java: switch_block of statement groups (falls through) or arrow rules (never).
1086
+ if (node.type === 'switch_expression') {
1087
+ const block = node.childForFieldName('body');
1088
+ const kids = named(block);
1089
+ const isGroup = (k) => k.type === 'switch_block_statement_group';
1090
+ const isRule = (k) => k.type === 'switch_rule';
1091
+ if (kids.length === 0 || !(kids.every(isGroup) || kids.every(isRule)))
1092
+ return null;
1093
+ const groups = [];
1094
+ for (const k of kids) {
1095
+ const labels = named(k).filter((c) => c.type === 'switch_label');
1096
+ const statements = named(k).filter((c) => c.type !== 'switch_label');
1097
+ if (labels.length === 0)
1098
+ return null;
1099
+ if (labels.some(defaultOf)) {
1100
+ if (labels.length > 1)
1101
+ return null; // `case 1: default:` - keep it simple
1102
+ groups.push({ node: k, labels: 'default', statements });
1103
+ }
1104
+ else {
1105
+ groups.push({ node: k, labels: labels.flatMap((l) => named(l)), statements });
1106
+ }
1107
+ }
1108
+ return {
1109
+ selector: node.childForFieldName('condition'),
1110
+ groups,
1111
+ fallthrough: kids.every(isRule) ? 'never' : 'c-style',
1112
+ };
1113
+ }
1114
+ // Go: cases directly under the switch; `fallthrough` must be asked for.
1115
+ if (node.type === 'expression_switch_statement') {
1116
+ const groups = [];
1117
+ for (const k of named(node)) {
1118
+ if (k.type === 'expression_case') {
1119
+ const value = k.childForFieldName('value');
1120
+ const labels = value?.type === 'expression_list' ? named(value) : value ? [value] : [];
1121
+ if (labels.length === 0)
1122
+ return null;
1123
+ groups.push({ node: k, labels, statements: named(k).filter((c) => !value || c.startIndex !== value.startIndex) });
1124
+ }
1125
+ else if (k.type === 'default_case') {
1126
+ groups.push({ node: k, labels: 'default', statements: named(k) });
1127
+ }
1128
+ }
1129
+ return { selector: node.childForFieldName('value'), groups, fallthrough: 'explicit' };
1130
+ }
1131
+ if (node.type !== 'switch_statement')
1132
+ return null;
1133
+ const block = node.childForFieldName('body');
1134
+ const selector = node.childForFieldName('condition') ?? node.childForFieldName('value');
1135
+ const kids = named(block);
1136
+ const groups = [];
1137
+ // A label inside the switch, or a case label that is not directly in its
1138
+ // body, is a way in that the selector does not control. Decline.
1139
+ const hasHiddenEntry = (n, depthInBody, inNestedSwitch) => {
1140
+ for (const c of named(n)) {
1141
+ if (c.type === 'labeled_statement')
1142
+ return true;
1143
+ if (!inNestedSwitch && depthInBody > 0 && (c.type === 'case_statement' || c.type === 'switch_case'))
1144
+ return true;
1145
+ // A nested switch's cases are its own; a label inside it is still a way in.
1146
+ if (hasHiddenEntry(c, depthInBody + 1, inNestedSwitch || SWITCH_NODES.has(c.type)))
1147
+ return true;
1148
+ }
1149
+ return false;
1150
+ };
1151
+ if (!block || hasHiddenEntry(block, 0, false))
1152
+ return null;
1153
+ for (const k of kids) {
1154
+ if (k.type === 'switch_case' || k.type === 'case_statement' || k.type === 'switch_default' || k.type === 'default_statement') {
1155
+ const value = k.childForFieldName('value');
1156
+ // GNU's `case 1 ... 5:` is a RANGE, and the grammar reads it as `case 1`
1157
+ // followed by an error. A label that did not parse cleanly is not read.
1158
+ if (value && (value.hasError || /\.\.\./.test(k.text.split(':')[0] ?? '')))
1159
+ return null;
1160
+ if (k.hasError)
1161
+ return null;
1162
+ const isDefault = k.type === 'switch_default' || k.type === 'default_statement' || (!value && defaultOf(k));
1163
+ const statements = named(k).filter((c) => !value || c.startIndex !== value.startIndex);
1164
+ if (isDefault)
1165
+ groups.push({ node: k, labels: 'default', statements });
1166
+ else if (value)
1167
+ groups.push({ node: k, labels: [value], statements });
1168
+ else
1169
+ return null;
1170
+ }
1171
+ else if (k.type !== 'comment') {
1172
+ return null; // a statement before the first case, or a shape we do not know
1173
+ }
1174
+ }
1175
+ return { selector, groups, fallthrough: 'c-style' };
1176
+ }
1177
+ //# sourceMappingURL=fold.js.map