Skip to content

Commit 788a3ba

Browse files
committed
fix #4020: change how console API dropping works
1 parent 43dfb74 commit 788a3ba

File tree

3 files changed

+90
-14
lines changed

3 files changed

+90
-14
lines changed

CHANGELOG.md

+24
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,30 @@
2828

2929
However, this prevents using esbuild with certain libraries that would otherwise work if BigInt literals were ignored, such as with old versions of the [`buffer` library](https://github.com/feross/buffer) before the library fixed support for running in environments without BigInt support. So with this release, esbuild will now turn BigInt literals into BigInt constructor calls (so `123n` becomes `BigInt(123)`) and generate a warning in this case. You can turn off the warning with `--log-override:bigint=silent` or restore the warning to an error with `--log-override:bigint=error` if needed.
3030

31+
* Change how `console` API dropping works ([#4020](https://github.com/evanw/esbuild/issues/4020))
32+
33+
Previously the `--drop:console` feature replaced all method calls off of the `console` global with `undefined` regardless of how long the property access chain was (so it applied to `console.log()` and `console.log.call(console)` and `console.log.not.a.method()`). However, it was pointed out that this breaks uses of `console.log.bind(console)`. That's also incompatible with Terser's implementation of the feature, which is where this feature originally came from (it does support `bind`). So with this release, using this feature with esbuild will now only replace one level of method call (unless extended by `call` or `apply`) and will replace the method being called with an empty function in complex cases:
34+
35+
```js
36+
// Original code
37+
const x = console.log('x')
38+
const y = console.log.call(console, 'y')
39+
const z = console.log.bind(console)('z')
40+
41+
// Old output (with --drop-console)
42+
const x = void 0;
43+
const y = void 0;
44+
const z = (void 0)("z");
45+
46+
// New output (with --drop-console)
47+
const x = void 0;
48+
const y = void 0;
49+
const z = (() => {
50+
}).bind(console)("z");
51+
```
52+
53+
This should more closely match Terser's existing behavior.
54+
3155
* The `text` loader now strips the UTF-8 BOM if present ([#3935](https://github.com/evanw/esbuild/issues/3935))
3256

3357
Some software (such as Notepad on Windows) can create text files that start with the three bytes `0xEF 0xBB 0xBF`, which is referred to as the "byte order mark". This prefix is intended to be removed before using the text. Previously esbuild's `text` loader included this byte sequence in the string, which turns into a prefix of `\uFEFF` in a JavaScript string when decoded from UTF-8. With this release, esbuild's `text` loader will now remove these bytes when they occur at the start of the file.

internal/js_parser/js_parser.go

+23-11
Original file line numberDiff line numberDiff line change
@@ -12618,6 +12618,7 @@ type exprOut struct {
1261812618

1261912619
// If true and this is used as a call target, the whole call expression
1262012620
// must be replaced with undefined.
12621+
callMustBeReplacedWithUndefined bool
1262112622
methodCallMustBeReplacedWithUndefined bool
1262212623
}
1262312624

@@ -13721,12 +13722,23 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
1372113722
return p.lowerOptionalChain(expr, in, out)
1372213723
}
1372313724

13725+
// Also erase "console.log.call(console, 123)" and "console.log.bind(console)"
13726+
if out.callMustBeReplacedWithUndefined {
13727+
if e.Name == "call" || e.Name == "apply" {
13728+
out.methodCallMustBeReplacedWithUndefined = true
13729+
} else if p.options.unsupportedJSFeatures.Has(compat.Arrow) {
13730+
e.Target.Data = &js_ast.EFunction{}
13731+
} else {
13732+
e.Target.Data = &js_ast.EArrow{}
13733+
}
13734+
}
13735+
1372413736
// Potentially rewrite this property access
1372513737
out = exprOut{
13726-
childContainsOptionalChain: containsOptionalChain,
13727-
methodCallMustBeReplacedWithUndefined: out.methodCallMustBeReplacedWithUndefined,
13728-
thisArgFunc: out.thisArgFunc,
13729-
thisArgWrapFunc: out.thisArgWrapFunc,
13738+
childContainsOptionalChain: containsOptionalChain,
13739+
callMustBeReplacedWithUndefined: out.methodCallMustBeReplacedWithUndefined,
13740+
thisArgFunc: out.thisArgFunc,
13741+
thisArgWrapFunc: out.thisArgWrapFunc,
1373013742
}
1373113743
if !in.hasChainParent {
1373213744
out.thisArgFunc = nil
@@ -13885,10 +13897,10 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
1388513897

1388613898
// Potentially rewrite this property access
1388713899
out = exprOut{
13888-
childContainsOptionalChain: containsOptionalChain,
13889-
methodCallMustBeReplacedWithUndefined: out.methodCallMustBeReplacedWithUndefined,
13890-
thisArgFunc: out.thisArgFunc,
13891-
thisArgWrapFunc: out.thisArgWrapFunc,
13900+
childContainsOptionalChain: containsOptionalChain,
13901+
callMustBeReplacedWithUndefined: out.methodCallMustBeReplacedWithUndefined,
13902+
thisArgFunc: out.thisArgFunc,
13903+
thisArgWrapFunc: out.thisArgWrapFunc,
1389213904
}
1389313905
if !in.hasChainParent {
1389413906
out.thisArgFunc = nil
@@ -14608,11 +14620,11 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
1460814620
oldIsControlFlowDead := p.isControlFlowDead
1460914621

1461014622
// If we're removing this call, don't count any arguments as symbol uses
14611-
if out.methodCallMustBeReplacedWithUndefined {
14623+
if out.callMustBeReplacedWithUndefined {
1461214624
if js_ast.IsPropertyAccess(e.Target) {
1461314625
p.isControlFlowDead = true
1461414626
} else {
14615-
out.methodCallMustBeReplacedWithUndefined = false
14627+
out.callMustBeReplacedWithUndefined = false
1461614628
}
1461714629
}
1461814630

@@ -14684,7 +14696,7 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO
1468414696
}
1468514697

1468614698
// Stop now if this call must be removed
14687-
if out.methodCallMustBeReplacedWithUndefined {
14699+
if out.callMustBeReplacedWithUndefined {
1468814700
p.isControlFlowDead = oldIsControlFlowDead
1468914701
return js_ast.Expr{Loc: expr.Loc, Data: js_ast.EUndefinedShared}, exprOut{}
1469014702
}

scripts/js-api-tests.js

+43-3
Original file line numberDiff line numberDiff line change
@@ -6080,17 +6080,57 @@ class Foo {
60806080
},
60816081

60826082
async dropConsole({ esbuild }) {
6083-
const { code } = await esbuild.transform(`
6084-
console('foo')
6083+
const { code: drop } = await esbuild.transform(`
60856084
console.log('foo')
60866085
console.log(foo())
6086+
console.log.call(console, foo())
6087+
console.log.apply(console, foo())
60876088
x = console.log(bar())
6089+
console['log']('foo')
6090+
`, { drop: ['console'] })
6091+
assert.strictEqual(drop, `x = void 0;\n`)
6092+
6093+
const { code: keepArrow } = await esbuild.transform(`
6094+
console('foo')
60886095
console.abc.xyz('foo')
60896096
console['log']('foo')
60906097
console[abc][xyz]('foo')
60916098
console[foo()][bar()]('foo')
6099+
const x = {
6100+
log: console.log.bind(console),
6101+
}
60926102
`, { drop: ['console'] })
6093-
assert.strictEqual(code, `console("foo");\nx = void 0;\n`)
6103+
assert.strictEqual(keepArrow, `console("foo");
6104+
(() => {
6105+
}).xyz("foo");
6106+
console[abc][xyz]("foo");
6107+
console[foo()][bar()]("foo");
6108+
const x = {
6109+
log: (() => {
6110+
}).bind(console)
6111+
};
6112+
`)
6113+
6114+
const { code: keepFn } = await esbuild.transform(`
6115+
console('foo')
6116+
console.abc.xyz('foo')
6117+
console['log']('foo')
6118+
console[abc][xyz]('foo')
6119+
console[foo()][bar()]('foo')
6120+
const x = {
6121+
log: console.log.bind(console),
6122+
}
6123+
`, { drop: ['console'], supported: { arrow: false } })
6124+
assert.strictEqual(keepFn, `console("foo");
6125+
(function() {
6126+
}).xyz("foo");
6127+
console[abc][xyz]("foo");
6128+
console[foo()][bar()]("foo");
6129+
const x = {
6130+
log: function() {
6131+
}.bind(console)
6132+
};
6133+
`)
60946134
},
60956135

60966136
async keepDebugger({ esbuild }) {

0 commit comments

Comments
 (0)