Skip to content

Commit 295d101

Browse files
styflevercel[bot]claude
authored
fix: don't crash on statically-evaluated expressions that throw (BigInt mixing) (#210)
## Problem The static evaluator (`src/utils/static-eval.js`) applies JS operators directly to known values, but some of those operations can throw at evaluation time: - **BigInt mixing**: `null + 1n` → `TypeError: Cannot mix BigInt and other types, use explicit conversions` - **Unary `+` on a BigInt**: `+1n` → `TypeError: Cannot convert a BigInt value to a number` The loader statically evaluates the RHS of every top-level-bound assignment (to track known bindings), so real-world code hits this. dd-trace 5's [`appsec/downstream_requests.js`](https://github.com/DataDog/dd-trace-js/blob/master/packages/dd-trace/src/appsec/downstream_requests.js) does: ```js let globalRequestCounter function enable (_config) { globalRequestCounter = 0n } function disable () { globalRequestCounter = null // <- registers null as the known binding value } function shouldSampleBody (req, outgoingUrl) { // evaluator computes `null + 1n` here and throws, crashing the build globalRequestCounter = (globalRequestCounter + 1n) & UINT64_MAX ... } ``` The uncaught TypeError kills the whole compilation — e.g. `ncc build` of anything depending on `dd-trace@5` dies with: ``` TypeError: Cannot mix BigInt and other types, use explicit conversions at Object.BinaryExpression (.../relocate-loader.js.cache.js:...) at walk (...) at computePureStaticValue (...) ``` This blocks bumping dd-trace 4 → 5 in any ncc-bundled project (every ncc version is affected since they all bundle this loader). ## Fix Wrap the operator application in `BinaryExpression` and `UnaryExpression` in try/catch, so a throwing expression is treated as **not statically computable** (the evaluator's existing "unknown value" semantics). `CallExpression` and `NewExpression` already handle evaluation errors exactly this way, so this follows established convention in the file. The diff is best viewed with whitespace ignored. ## Test Added `test/unit/static-eval-bigint` reproducing the dd-trace pattern (known-`null` binding + `1n`, BigInt consts, and unary `+` on a known BigInt). Without the fix it crashes the build with the errors above; with the fix the module passes through untouched. `npx jest test/index.test.js`: 85/85 pass. (`test/project.test.js` fails identically on unmodified `main` in my environment — pre-existing, unrelated.) 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Related - Related to vercel/ncc#1307 - Related to #208 Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent fa39a77 commit 295d101

4 files changed

Lines changed: 268 additions & 75 deletions

File tree

src/utils/static-eval.js

Lines changed: 92 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -78,73 +78,82 @@ const visitors = {
7878
if ('test' in l && 'test' in r)
7979
return;
8080

81-
if ('test' in l) {
82-
r = r.value;
83-
if (op === '==') return { test: l.test, then: l.then == r, else: l.else == r };
84-
if (op === '===') return { test: l.test, then: l.then === r, else: l.else === r };
85-
if (op === '!=') return { test: l.test, then: l.then != r, else: l.else != r };
86-
if (op === '!==') return { test: l.test, then: l.then !== r, else: l.else !== r };
87-
if (op === '+') return { test: l.test, then: l.then + r, else: l.else + r };
88-
if (op === '-') return { test: l.test, then: l.then - r, else: l.else - r };
89-
if (op === '*') return { test: l.test, then: l.then * r, else: l.else * r };
90-
if (op === '/') return { test: l.test, then: l.then / r, else: l.else / r };
91-
if (op === '%') return { test: l.test, then: l.then % r, else: l.else % r };
92-
if (op === '<') return { test: l.test, then: l.then < r, else: l.else < r };
93-
if (op === '<=') return { test: l.test, then: l.then <= r, else: l.else <= r };
94-
if (op === '>') return { test: l.test, then: l.then > r, else: l.else > r };
95-
if (op === '>=') return { test: l.test, then: l.then >= r, else: l.else >= r };
96-
if (op === '|') return { test: l.test, then: l.then | r, else: l.else | r };
97-
if (op === '&') return { test: l.test, then: l.then & r, else: l.else & r };
98-
if (op === '^') return { test: l.test, then: l.then ^ r, else: l.else ^ r };
99-
if (op === '&&') return { test: l.test, then: l.then && r, else: l.else && r };
100-
if (op === '||') return { test: l.test, then: l.then || r, else: l.else || r };
81+
// Evaluation can throw for known values, e.g. mixing a BigInt
82+
// operand with another type ("Cannot mix BigInt and other types").
83+
// Any such error just means the expression is not statically
84+
// computable.
85+
try {
86+
if ('test' in l) {
87+
r = r.value;
88+
if (op === '==') return { test: l.test, then: l.then == r, else: l.else == r };
89+
if (op === '===') return { test: l.test, then: l.then === r, else: l.else === r };
90+
if (op === '!=') return { test: l.test, then: l.then != r, else: l.else != r };
91+
if (op === '!==') return { test: l.test, then: l.then !== r, else: l.else !== r };
92+
if (op === '+') return { test: l.test, then: l.then + r, else: l.else + r };
93+
if (op === '-') return { test: l.test, then: l.then - r, else: l.else - r };
94+
if (op === '*') return { test: l.test, then: l.then * r, else: l.else * r };
95+
if (op === '/') return { test: l.test, then: l.then / r, else: l.else / r };
96+
if (op === '%') return { test: l.test, then: l.then % r, else: l.else % r };
97+
if (op === '<') return { test: l.test, then: l.then < r, else: l.else < r };
98+
if (op === '<=') return { test: l.test, then: l.then <= r, else: l.else <= r };
99+
if (op === '>') return { test: l.test, then: l.then > r, else: l.else > r };
100+
if (op === '>=') return { test: l.test, then: l.then >= r, else: l.else >= r };
101+
if (op === '|') return { test: l.test, then: l.then | r, else: l.else | r };
102+
if (op === '&') return { test: l.test, then: l.then & r, else: l.else & r };
103+
if (op === '^') return { test: l.test, then: l.then ^ r, else: l.else ^ r };
104+
if (op === '&&') return { test: l.test, then: l.then && r, else: l.else && r };
105+
if (op === '||') return { test: l.test, then: l.then || r, else: l.else || r };
106+
}
107+
else if ('test' in r) {
108+
l = l.value;
109+
if (op === '==') return { test: r.test, then: l == r.then, else: l == r.else };
110+
if (op === '===') return { test: r.test, then: l === r.then, else: l === r.else };
111+
if (op === '!=') return { test: r.test, then: l != r.then, else: l != r.else };
112+
if (op === '!==') return { test: r.test, then: l !== r.then, else: l !== r.else };
113+
if (op === '+') return { test: r.test, then: l + r.then, else: l + r.else };
114+
if (op === '-') return { test: r.test, then: l - r.then, else: l - r.else };
115+
if (op === '*') return { test: r.test, then: l * r.then, else: l * r.else };
116+
if (op === '/') return { test: r.test, then: l / r.then, else: l / r.else };
117+
if (op === '%') return { test: r.test, then: l % r.then, else: l % r.else };
118+
if (op === '<') return { test: r.test, then: l < r.then, else: l < r.else };
119+
if (op === '<=') return { test: r.test, then: l <= r.then, else: l <= r.else };
120+
if (op === '>') return { test: r.test, then: l > r.then, else: l > r.else };
121+
if (op === '>=') return { test: r.test, then: l >= r.then, else: l >= r.else };
122+
if (op === '|') return { test: r.test, then: l | r.then, else: l | r.else };
123+
if (op === '&') return { test: r.test, then: l & r.then, else: l & r.else };
124+
if (op === '^') return { test: r.test, then: l ^ r.then, else: l ^ r.else };
125+
if (op === '&&') return { test: r.test, then: l && r.then, else: l && r.else };
126+
if (op === '||') return { test: r.test, then: l || r.then, else: l || r.else };
127+
}
128+
else {
129+
if (op === '==') return { value: l.value == r.value };
130+
if (op === '===') return { value: l.value === r.value };
131+
if (op === '!=') return { value: l.value != r.value };
132+
if (op === '!==') return { value: l.value !== r.value };
133+
if (op === '+') {
134+
const val = { value: l.value + r.value };
135+
if (l.wildcards || r.wildcards)
136+
val.wildcards = [...l.wildcards || [], ...r.wildcards || []];
137+
return val;
138+
}
139+
if (op === '-') return { value: l.value - r.value };
140+
if (op === '*') return { value: l.value * r.value };
141+
if (op === '/') return { value: l.value / r.value };
142+
if (op === '%') return { value: l.value % r.value };
143+
if (op === '<') return { value: l.value < r.value };
144+
if (op === '<=') return { value: l.value <= r.value };
145+
if (op === '>') return { value: l.value > r.value };
146+
if (op === '>=') return { value: l.value >= r.value };
147+
if (op === '|') return { value: l.value | r.value };
148+
if (op === '&') return { value: l.value & r.value };
149+
if (op === '^') return { value: l.value ^ r.value };
150+
if (op === '&&') return { value: l.value && r.value };
151+
if (op === '||') return { value: l.value || r.value };
152+
}
101153
}
102-
else if ('test' in r) {
103-
l = l.value;
104-
if (op === '==') return { test: r.test, then: l == r.then, else: l == r.else };
105-
if (op === '===') return { test: r.test, then: l === r.then, else: l === r.else };
106-
if (op === '!=') return { test: r.test, then: l != r.then, else: l != r.else };
107-
if (op === '!==') return { test: r.test, then: l !== r.then, else: l !== r.else };
108-
if (op === '+') return { test: r.test, then: l + r.then, else: l + r.else };
109-
if (op === '-') return { test: r.test, then: l - r.then, else: l - r.else };
110-
if (op === '*') return { test: r.test, then: l * r.then, else: l * r.else };
111-
if (op === '/') return { test: r.test, then: l / r.then, else: l / r.else };
112-
if (op === '%') return { test: r.test, then: l % r.then, else: l % r.else };
113-
if (op === '<') return { test: r.test, then: l < r.then, else: l < r.else };
114-
if (op === '<=') return { test: r.test, then: l <= r.then, else: l <= r.else };
115-
if (op === '>') return { test: r.test, then: l > r.then, else: l > r.else };
116-
if (op === '>=') return { test: r.test, then: l >= r.then, else: l >= r.else };
117-
if (op === '|') return { test: r.test, then: l | r.then, else: l | r.else };
118-
if (op === '&') return { test: r.test, then: l & r.then, else: l & r.else };
119-
if (op === '^') return { test: r.test, then: l ^ r.then, else: l ^ r.else };
120-
if (op === '&&') return { test: r.test, then: l && r.then, else: l && r.else };
121-
if (op === '||') return { test: r.test, then: l || r.then, else: l || r.else };
154+
catch (e) {
155+
return;
122156
}
123-
else {
124-
if (op === '==') return { value: l.value == r.value };
125-
if (op === '===') return { value: l.value === r.value };
126-
if (op === '!=') return { value: l.value != r.value };
127-
if (op === '!==') return { value: l.value !== r.value };
128-
if (op === '+') {
129-
const val = { value: l.value + r.value };
130-
if (l.wildcards || r.wildcards)
131-
val.wildcards = [...l.wildcards || [], ...r.wildcards || []];
132-
return val;
133-
}
134-
if (op === '-') return { value: l.value - r.value };
135-
if (op === '*') return { value: l.value * r.value };
136-
if (op === '/') return { value: l.value / r.value };
137-
if (op === '%') return { value: l.value % r.value };
138-
if (op === '<') return { value: l.value < r.value };
139-
if (op === '<=') return { value: l.value <= r.value };
140-
if (op === '>') return { value: l.value > r.value };
141-
if (op === '>=') return { value: l.value >= r.value };
142-
if (op === '|') return { value: l.value | r.value };
143-
if (op === '&') return { value: l.value & r.value };
144-
if (op === '^') return { value: l.value ^ r.value };
145-
if (op === '&&') return { value: l.value && r.value };
146-
if (op === '||') return { value: l.value || r.value };
147-
}
148157
return;
149158
},
150159
CallExpression (node, walk) {
@@ -422,17 +431,25 @@ const visitors = {
422431
const val = walk(node.argument);
423432
if (!val)
424433
return;
425-
if ('value' in val && 'wildcards' in val === false) {
426-
if (node.operator === '+') return { value: +val.value };
427-
if (node.operator === '-') return { value: -val.value };
428-
if (node.operator === '~') return { value: ~val.value };
429-
if (node.operator === '!') return { value: !val.value };
434+
// Evaluation can throw for known values, e.g. unary + on a BigInt
435+
// ("Cannot convert a BigInt value to a number"). Any such error
436+
// just means the expression is not statically computable.
437+
try {
438+
if ('value' in val && 'wildcards' in val === false) {
439+
if (node.operator === '+') return { value: +val.value };
440+
if (node.operator === '-') return { value: -val.value };
441+
if (node.operator === '~') return { value: ~val.value };
442+
if (node.operator === '!') return { value: !val.value };
443+
}
444+
else if ('test' in val && 'wildcards' in val === false) {
445+
if (node.operator === '+') return { test: val.test, then: +val.then, else: +val.else };
446+
if (node.operator === '-') return { test: val.test, then: -val.then, else: -val.else };
447+
if (node.operator === '~') return { test: val.test, then: ~val.then, else: ~val.else };
448+
if (node.operator === '!') return { test: val.test, then: !val.then, else: !val.else };
449+
}
430450
}
431-
else if ('test' in val && 'wildcards' in val === false) {
432-
if (node.operator === '+') return { test: val.test, then: +val.then, else: +val.else };
433-
if (node.operator === '-') return { test: val.test, then: -val.then, else: -val.else };
434-
if (node.operator === '~') return { test: val.test, then: ~val.then, else: ~val.else };
435-
if (node.operator === '!') return { test: val.test, then: !val.then, else: !val.else };
451+
catch (e) {
452+
return;
436453
}
437454
return;
438455
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Regression test: the loader tracks `counter = null` as a known binding
2+
// value, and previously crashed statically evaluating `counter + 1n`
3+
// ("TypeError: Cannot mix BigInt and other types") instead of treating
4+
// the expression as not statically computable.
5+
// Pattern from dd-trace's packages/dd-trace/src/appsec/downstream_requests.js.
6+
const KNUTH_FACTOR = 11400714819323199488n;
7+
const UINT64_MAX = (1n << 64n) - 1n;
8+
9+
let counter;
10+
11+
function enable () {
12+
counter = 0n;
13+
}
14+
15+
function disable () {
16+
counter = null;
17+
}
18+
19+
function next () {
20+
counter = (counter + 1n) & UINT64_MAX;
21+
return (counter * KNUTH_FACTOR) % UINT64_MAX;
22+
}
23+
24+
// unary + on a known BigInt binding previously threw
25+
// "Cannot convert a BigInt value to a number"
26+
const coerced = +KNUTH_FACTOR ? 1 : 0;
27+
28+
module.exports = { enable, disable, next, coerced };
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/******/ (() => { // webpackBootstrap
2+
/******/ var __webpack_modules__ = ({
3+
4+
/***/ 635:
5+
/***/ ((module) => {
6+
7+
// Regression test: the loader tracks `counter = null` as a known binding
8+
// value, and previously crashed statically evaluating `counter + 1n`
9+
// ("TypeError: Cannot mix BigInt and other types") instead of treating
10+
// the expression as not statically computable.
11+
// Pattern from dd-trace's packages/dd-trace/src/appsec/downstream_requests.js.
12+
const KNUTH_FACTOR = 11400714819323199488n;
13+
const UINT64_MAX = (1n << 64n) - 1n;
14+
15+
let counter;
16+
17+
function enable () {
18+
counter = 0n;
19+
}
20+
21+
function disable () {
22+
counter = null;
23+
}
24+
25+
function next () {
26+
counter = (counter + 1n) & UINT64_MAX;
27+
return (counter * KNUTH_FACTOR) % UINT64_MAX;
28+
}
29+
30+
// unary + on a known BigInt binding previously threw
31+
// "Cannot convert a BigInt value to a number"
32+
const coerced = +KNUTH_FACTOR ? 1 : 0;
33+
34+
module.exports = { enable, disable, next, coerced };
35+
36+
37+
/***/ })
38+
39+
/******/ });
40+
/************************************************************************/
41+
/******/ // The module cache
42+
/******/ var __webpack_module_cache__ = {};
43+
/******/
44+
/******/ // The require function
45+
/******/ function __webpack_require__(moduleId) {
46+
/******/ // Check if module is in cache
47+
/******/ var cachedModule = __webpack_module_cache__[moduleId];
48+
/******/ if (cachedModule !== undefined) {
49+
/******/ return cachedModule.exports;
50+
/******/ }
51+
/******/ // Create a new module (and put it into the cache)
52+
/******/ var module = __webpack_module_cache__[moduleId] = {
53+
/******/ // no module.id needed
54+
/******/ // no module.loaded needed
55+
/******/ exports: {}
56+
/******/ };
57+
/******/
58+
/******/ // Execute the module function
59+
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
60+
/******/
61+
/******/ // Return the exports of the module
62+
/******/ return module.exports;
63+
/******/ }
64+
/******/
65+
/************************************************************************/
66+
/******/
67+
/******/ // startup
68+
/******/ // Load entry module and return exports
69+
/******/ // This entry module is referenced by other modules so it can't be inlined
70+
/******/ var __webpack_exports__ = __webpack_require__(635);
71+
/******/ module.exports = __webpack_exports__;
72+
/******/
73+
/******/ })()
74+
;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/******/ (() => { // webpackBootstrap
2+
/******/ var __webpack_modules__ = ({
3+
4+
/***/ 635:
5+
/***/ ((module) => {
6+
7+
// Regression test: the loader tracks `counter = null` as a known binding
8+
// value, and previously crashed statically evaluating `counter + 1n`
9+
// ("TypeError: Cannot mix BigInt and other types") instead of treating
10+
// the expression as not statically computable.
11+
// Pattern from dd-trace's packages/dd-trace/src/appsec/downstream_requests.js.
12+
const KNUTH_FACTOR = 11400714819323199488n;
13+
const UINT64_MAX = (1n << 64n) - 1n;
14+
15+
let counter;
16+
17+
function enable () {
18+
counter = 0n;
19+
}
20+
21+
function disable () {
22+
counter = null;
23+
}
24+
25+
function next () {
26+
counter = (counter + 1n) & UINT64_MAX;
27+
return (counter * KNUTH_FACTOR) % UINT64_MAX;
28+
}
29+
30+
// unary + on a known BigInt binding previously threw
31+
// "Cannot convert a BigInt value to a number"
32+
const coerced = +KNUTH_FACTOR ? 1 : 0;
33+
34+
module.exports = { enable, disable, next, coerced };
35+
36+
37+
/***/ })
38+
39+
/******/ });
40+
/************************************************************************/
41+
/******/ // The module cache
42+
/******/ var __webpack_module_cache__ = {};
43+
/******/
44+
/******/ // The require function
45+
/******/ function __webpack_require__(moduleId) {
46+
/******/ // Check if module is in cache
47+
/******/ var cachedModule = __webpack_module_cache__[moduleId];
48+
/******/ if (cachedModule !== undefined) {
49+
/******/ return cachedModule.exports;
50+
/******/ }
51+
/******/ // Create a new module (and put it into the cache)
52+
/******/ var module = __webpack_module_cache__[moduleId] = {
53+
/******/ // no module.id needed
54+
/******/ // no module.loaded needed
55+
/******/ exports: {}
56+
/******/ };
57+
/******/
58+
/******/ // Execute the module function
59+
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
60+
/******/
61+
/******/ // Return the exports of the module
62+
/******/ return module.exports;
63+
/******/ }
64+
/******/
65+
/************************************************************************/
66+
/******/
67+
/******/ // startup
68+
/******/ // Load entry module and return exports
69+
/******/ // This entry module is referenced by other modules so it can't be inlined
70+
/******/ var __webpack_exports__ = __webpack_require__(635);
71+
/******/ module.exports = __webpack_exports__;
72+
/******/
73+
/******/ })()
74+
;

0 commit comments

Comments
 (0)