Skip to content

Commit 3e2374c

Browse files
committedSep 11, 2022
fix #2537: sort map keys to fix non-determinism
1 parent 112a033 commit 3e2374c

File tree

9 files changed

+161
-11
lines changed

9 files changed

+161
-11
lines changed
 

‎CHANGELOG.md

+52
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,58 @@
4141

4242
These seem to be used by Yarn to avoid the `.` and `..` path segments in the middle of relative paths. The removal of these character sequences seems relatively harmless in this case since esbuild shouldn't ever generate such path segments. This change should add support to esbuild for Yarn's [`pnpIgnorePatterns`](https://yarnpkg.com/configuration/yarnrc/#pnpIgnorePatterns) feature.
4343

44+
* Fix non-determinism issue with legacy block-level function declarations and strict mode ([#2537](https://github.com/evanw/esbuild/issues/2537))
45+
46+
When function declaration statements are nested inside a block in strict mode, they are supposed to only be available within that block's scope. But in "sloppy mode" (which is what non-strict mode is commonly called), they are supposed to be available within the whole function's scope:
47+
48+
```js
49+
// This returns 1 due to strict mode
50+
function test1() {
51+
'use strict'
52+
function fn() { return 1 }
53+
if (true) { function fn() { return 2 } }
54+
return fn()
55+
}
56+
57+
// This returns 2 due to sloppy mode
58+
function test2() {
59+
function fn() { return 1 }
60+
if (true) { function fn() { return 2 } }
61+
return fn()
62+
}
63+
```
64+
65+
To implement this, esbuild compiles these two functions differently to reflect their different semantics:
66+
67+
```js
68+
function test1() {
69+
"use strict";
70+
function fn() {
71+
return 1;
72+
}
73+
if (true) {
74+
let fn2 = function() {
75+
return 2;
76+
};
77+
}
78+
return fn();
79+
}
80+
function test2() {
81+
function fn() {
82+
return 1;
83+
}
84+
if (true) {
85+
let fn2 = function() {
86+
return 2;
87+
};
88+
var fn = fn2;
89+
}
90+
return fn();
91+
}
92+
```
93+
94+
However, the compilation had a subtle bug where the automatically-generated function-level symbols for multible hoisted block-level function declarations in the same block a sloppy-mode context were generated in a random order if the output was in strict mode, which could be the case if TypeScript's `alwaysStrict` setting was set to true. This lead to non-determinism in the output as the minifier would randomly exchange the generated names for these symbols on different runs. This bug has been fixed by sorting the keys of the unordered map before iterating over them.
95+
4496
## 0.15.7
4597

4698
* Add `--watch=forever` to allow esbuild to never terminate ([#1511](https://github.com/evanw/esbuild/issues/1511), [#1885](https://github.com/evanw/esbuild/issues/1885))

‎internal/bundler/bundler.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -1228,8 +1228,12 @@ func (s *scanner) maybeParseFile(
12281228
if resolveResult.UnusedImportFlagsTS != 0 {
12291229
optionsClone.UnusedImportFlagsTS = resolveResult.UnusedImportFlagsTS
12301230
}
1231-
optionsClone.TSTarget = resolveResult.TSTarget
1232-
optionsClone.TSAlwaysStrict = resolveResult.TSAlwaysStrict
1231+
if resolveResult.TSTarget != nil {
1232+
optionsClone.TSTarget = resolveResult.TSTarget
1233+
}
1234+
if resolveResult.TSAlwaysStrict != nil {
1235+
optionsClone.TSAlwaysStrict = resolveResult.TSAlwaysStrict
1236+
}
12331237

12341238
// Set the module type preference using node's module type rules
12351239
if strings.HasSuffix(path.Text, ".mjs") {

‎internal/bundler/bundler_default_test.go

+36
Original file line numberDiff line numberDiff line change
@@ -6796,3 +6796,39 @@ yes4.js: NOTE: This file is considered to be an ECMAScript module because of the
67966796
`,
67976797
})
67986798
}
6799+
6800+
// See: https://github.com/evanw/esbuild/issues/2537
6801+
func TestNonDeterminismIssue2537(t *testing.T) {
6802+
loader_suite.expectBundled(t, bundled{
6803+
files: map[string]string{
6804+
"/entry.ts": `
6805+
export function aap(noot: boolean, wim: number) {
6806+
let mies = "teun"
6807+
if (noot) {
6808+
function vuur(v: number) {
6809+
return v * 2
6810+
}
6811+
function schaap(s: number) {
6812+
return s / 2
6813+
}
6814+
mies = vuur(wim) + schaap(wim)
6815+
}
6816+
return mies
6817+
}
6818+
`,
6819+
"/tsconfig.json": `
6820+
{
6821+
"compilerOptions": {
6822+
"alwaysStrict": true
6823+
}
6824+
}
6825+
`,
6826+
},
6827+
entryPaths: []string{"/entry.ts"},
6828+
options: config.Options{
6829+
Mode: config.ModeBundle,
6830+
AbsOutputFile: "/out.js",
6831+
MinifyIdentifiers: true,
6832+
},
6833+
})
6834+
}

‎internal/bundler/snapshots/snapshots_loader.txt

+21
Original file line numberDiff line numberDiff line change
@@ -957,6 +957,27 @@ module.exports = foo;
957957
---------- /out/no-warnings-here.js ----------
958958
console.log(module, exports);
959959

960+
================================================================================
961+
TestNonDeterminismIssue2537
962+
---------- /out.js ----------
963+
// entry.ts
964+
function b(o, e) {
965+
let r = "teun";
966+
if (o) {
967+
let u = function(n) {
968+
return n * 2;
969+
}, t = function(n) {
970+
return n / 2;
971+
};
972+
var i = u, a = t;
973+
r = u(e) + t(e);
974+
}
975+
return r;
976+
}
977+
export {
978+
b as aap
979+
};
980+
960981
================================================================================
961982
TestRequireCustomExtensionBase64
962983
---------- /out.js ----------

‎internal/js_parser/js_parser.go

+21-1
Original file line numberDiff line numberDiff line change
@@ -1305,10 +1305,30 @@ func (p *parser) declareSymbol(kind js_ast.SymbolKind, loc logger.Loc, name stri
13051305

13061306
}
13071307

1308+
// This type is just so we can use Go's native sort function
1309+
type scopeMemberArray []js_ast.ScopeMember
1310+
1311+
func (a scopeMemberArray) Len() int { return len(a) }
1312+
func (a scopeMemberArray) Swap(i int, j int) { a[i], a[j] = a[j], a[i] }
1313+
1314+
func (a scopeMemberArray) Less(i int, j int) bool {
1315+
ai := a[i].Ref
1316+
bj := a[j].Ref
1317+
return ai.InnerIndex < bj.InnerIndex || (ai.InnerIndex == bj.InnerIndex && ai.SourceIndex < bj.SourceIndex)
1318+
}
1319+
13081320
func (p *parser) hoistSymbols(scope *js_ast.Scope) {
13091321
if !scope.Kind.StopsHoisting() {
1310-
nextMember:
1322+
// We create new symbols in the loop below, so the iteration order of the
1323+
// loop must be deterministic to avoid generating different minified names
1324+
sortedMembers := make(scopeMemberArray, 0, len(scope.Members))
13111325
for _, member := range scope.Members {
1326+
sortedMembers = append(sortedMembers, member)
1327+
}
1328+
sort.Sort(sortedMembers)
1329+
1330+
nextMember:
1331+
for _, member := range sortedMembers {
13121332
symbol := &p.symbols[member.Ref.InnerIndex]
13131333

13141334
// Handle non-hoisted collisions between catch bindings and the catch body.

‎internal/resolver/resolver.go

+1-6
Original file line numberDiff line numberDiff line change
@@ -681,12 +681,7 @@ func (r resolverQuery) finalizeResolve(result *ResolveResult) {
681681
dirInfo.enclosingTSConfigJSON.PreserveValueImports,
682682
)
683683
result.TSTarget = dirInfo.enclosingTSConfigJSON.TSTarget
684-
if tsAlwaysStrict := dirInfo.enclosingTSConfigJSON.TSAlwaysStrict; tsAlwaysStrict != nil {
685-
result.TSAlwaysStrict = tsAlwaysStrict
686-
} else {
687-
// If "alwaysStrict" is absent, it defaults to "strict" instead
688-
result.TSAlwaysStrict = dirInfo.enclosingTSConfigJSON.TSStrict
689-
}
684+
result.TSAlwaysStrict = dirInfo.enclosingTSConfigJSON.TSAlwaysStrictOrStrict()
690685

691686
if r.debugLogs != nil {
692687
r.debugLogs.addNote(fmt.Sprintf("This import is under the effect of %q",

‎internal/resolver/tsconfig_json.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ type TSConfigJSON struct {
4848
PreserveValueImports bool
4949
}
5050

51+
func (config *TSConfigJSON) TSAlwaysStrictOrStrict() *config.TSAlwaysStrict {
52+
if config.TSAlwaysStrict != nil {
53+
return config.TSAlwaysStrict
54+
}
55+
56+
// If "alwaysStrict" is absent, it defaults to "strict" instead
57+
return config.TSStrict
58+
}
59+
5160
type TSConfigPath struct {
5261
Text string
5362
Loc logger.Loc
@@ -242,7 +251,7 @@ func ParseTSConfigJSON(
242251
if valueJSON, keyLoc, ok := getProperty(compilerOptionsJSON, "alwaysStrict"); ok {
243252
if value, ok := getBool(valueJSON); ok {
244253
valueRange := js_lexer.RangeOfIdentifier(source, valueJSON.Loc)
245-
result.TSStrict = &config.TSAlwaysStrict{
254+
result.TSAlwaysStrict = &config.TSAlwaysStrict{
246255
Name: "alwaysStrict",
247256
Value: value,
248257
Source: source,

‎pkg/api/api_impl.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -1398,7 +1398,7 @@ func transformImpl(input string, transformOpts TransformOptions) TransformResult
13981398
result.PreserveValueImports,
13991399
)
14001400
tsTarget = result.TSTarget
1401-
tsAlwaysStrict = result.TSAlwaysStrict
1401+
tsAlwaysStrict = result.TSAlwaysStrictOrStrict()
14021402
}
14031403
}
14041404

@@ -1879,6 +1879,7 @@ type metafileEntry struct {
18791879
size int
18801880
}
18811881

1882+
// This type is just so we can use Go's native sort function
18821883
type metafileArray []metafileEntry
18831884

18841885
func (a metafileArray) Len() int { return len(a) }

‎scripts/js-api-tests.js

+12
Original file line numberDiff line numberDiff line change
@@ -4127,6 +4127,18 @@ let transformTests = {
41274127
assert.strictEqual(code2, `class Foo {\n foo;\n}\n`)
41284128
},
41294129

4130+
async tsconfigRawAlwaysStrict({ esbuild }) {
4131+
const input = `console.log(123)`
4132+
4133+
assert.strictEqual((await esbuild.transform(input, { loader: 'ts', tsconfigRaw: { compilerOptions: { strict: false } } })).code, `console.log(123);\n`)
4134+
assert.strictEqual((await esbuild.transform(input, { loader: 'ts', tsconfigRaw: { compilerOptions: { alwaysStrict: false } } })).code, `console.log(123);\n`)
4135+
assert.strictEqual((await esbuild.transform(input, { loader: 'ts', tsconfigRaw: { compilerOptions: { alwaysStrict: false, strict: true } } })).code, `console.log(123);\n`)
4136+
4137+
assert.strictEqual((await esbuild.transform(input, { loader: 'ts', tsconfigRaw: { compilerOptions: { strict: true } } })).code, `"use strict";\nconsole.log(123);\n`)
4138+
assert.strictEqual((await esbuild.transform(input, { loader: 'ts', tsconfigRaw: { compilerOptions: { alwaysStrict: true } } })).code, `"use strict";\nconsole.log(123);\n`)
4139+
assert.strictEqual((await esbuild.transform(input, { loader: 'ts', tsconfigRaw: { compilerOptions: { alwaysStrict: true, strict: false } } })).code, `"use strict";\nconsole.log(123);\n`)
4140+
},
4141+
41304142
async tsImplicitUseDefineForClassFields({ esbuild }) {
41314143
var { code } = await esbuild.transform(`class Foo { foo }`, {
41324144
loader: 'ts',

0 commit comments

Comments
 (0)
Please sign in to comment.