Skip to content

Commit d2aa4eb

Browse files
committed
fix lexically-declared names in strict mode
1 parent 0c15c1e commit d2aa4eb

File tree

5 files changed

+97
-32
lines changed

5 files changed

+97
-32
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
The character tables that determine which characters form valid JavaScript identifiers have been updated from Unicode version 14.0.0 to the newly-released Unicode version 15.0.0. I'm not putting an example in the release notes because all of the new characters will likely just show up as little squares since fonts haven't been updated yet. But you can read https://www.unicode.org/versions/Unicode15.0.0/#Summary for more information about the changes.
88

9-
* Disallow duplicate lexically-declared names in nested blocks
9+
* Disallow duplicate lexically-declared names in nested blocks and in strict mode
1010

11-
It's supposed to be a syntax error for a nested block to declare two symbols with the same name unless all duplicate entries are either `function` declarations or all `var` declarations. However, esbuild was overly permissive and allowed this when duplicate entries were either `function` declarations or `var` declarations (even if they were mixed). This check has now been made more restrictive to match the JavaScript specification:
11+
In strict mode or in a nested block, it's supposed to be a syntax error to declare two symbols with the same name unless all duplicate entries are either `function` declarations or all `var` declarations. However, esbuild was overly permissive and allowed this when duplicate entries were either `function` declarations or `var` declarations (even if they were mixed). This check has now been made more restrictive to match the JavaScript specification:
1212

1313
```js
1414
// JavaScript allows this

internal/js_ast/js_ast.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1548,6 +1548,7 @@ type Scope struct {
15481548
Parent *Scope
15491549
Children []*Scope
15501550
Members map[string]ScopeMember
1551+
Replaced []ScopeMember
15511552
Generated []Ref
15521553

15531554
// The location of the "use strict" directive for ExplicitStrictMode

internal/js_parser/js_parser.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,6 +1231,7 @@ func (p *parser) declareSymbol(kind js_ast.SymbolKind, loc logger.Loc, name stri
12311231

12321232
case mergeReplaceWithNew:
12331233
symbol.Link = ref
1234+
p.currentScope.Replaced = append(p.currentScope.Replaced, existing)
12341235

12351236
// If these are both functions, remove the overwritten declaration
12361237
if p.options.minifySyntax && kind.IsFunction() && symbol.Kind.IsFunction() {
@@ -1268,6 +1269,28 @@ func (a scopeMemberArray) Less(i int, j int) bool {
12681269
}
12691270

12701271
func (p *parser) hoistSymbols(scope *js_ast.Scope) {
1272+
if scope.StrictMode != js_ast.SloppyMode {
1273+
for _, replaced := range scope.Replaced {
1274+
symbol := &p.symbols[replaced.Ref.InnerIndex]
1275+
if symbol.Kind.IsFunction() {
1276+
if member, ok := scope.Members[symbol.OriginalName]; ok && p.symbols[member.Ref.InnerIndex].Kind.IsFunction() {
1277+
where, notes := p.whyStrictMode(scope)
1278+
notes[0].Text = fmt.Sprintf("Duplicate lexically-declared names are not allowed %s. %s", where, notes[0].Text)
1279+
1280+
p.log.AddErrorWithNotes(&p.tracker,
1281+
js_lexer.RangeOfIdentifier(p.source, member.Loc),
1282+
fmt.Sprintf("The symbol %q has already been declared", symbol.OriginalName),
1283+
1284+
append([]logger.MsgData{p.tracker.MsgData(
1285+
js_lexer.RangeOfIdentifier(p.source, replaced.Loc),
1286+
fmt.Sprintf("The symbol %q was originally declared here:", symbol.OriginalName),
1287+
)}, notes...),
1288+
)
1289+
}
1290+
}
1291+
}
1292+
}
1293+
12711294
if !scope.Kind.StopsHoisting() {
12721295
// We create new symbols in the loop below, so the iteration order of the
12731296
// loop must be deterministic to avoid generating different minified names

internal/js_parser/js_parser_lower.go

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ const (
170170
legacyOctalEscape
171171
ifElseFunctionStmt
172172
labelFunctionStmt
173+
duplicateLexicallyDeclaredNames
173174
)
174175

175176
func (p *parser) markStrictModeFeature(feature strictModeFeature, r logger.Range, detail string) {
@@ -205,47 +206,54 @@ func (p *parser) markStrictModeFeature(feature strictModeFeature, r logger.Range
205206
case labelFunctionStmt:
206207
text = "Function declarations inside labels"
207208

209+
case duplicateLexicallyDeclaredNames:
210+
text = "Duplicate lexically-declared names"
211+
208212
default:
209213
text = "This feature"
210214
}
211215

212216
if p.isStrictMode() {
213-
var notes []logger.MsgData
214-
where := "in strict mode"
217+
where, notes := p.whyStrictMode(p.currentScope)
218+
p.log.AddErrorWithNotes(&p.tracker, r,
219+
fmt.Sprintf("%s cannot be used %s", text, where), notes)
220+
} else if !canBeTransformed && p.isStrictModeOutputFormat() {
221+
p.log.AddError(&p.tracker, r,
222+
fmt.Sprintf("%s cannot be used with the \"esm\" output format due to strict mode", text))
223+
}
224+
}
215225

216-
switch p.currentScope.StrictMode {
217-
case js_ast.ImplicitStrictModeClass:
218-
notes = []logger.MsgData{p.tracker.MsgData(p.enclosingClassKeyword,
219-
"All code inside a class is implicitly in strict mode")}
226+
func (p *parser) whyStrictMode(scope *js_ast.Scope) (where string, notes []logger.MsgData) {
227+
where = "in strict mode"
220228

221-
case js_ast.ImplicitStrictModeTSAlwaysStrict:
222-
tsAlwaysStrict := p.options.tsAlwaysStrict
223-
t := logger.MakeLineColumnTracker(&tsAlwaysStrict.Source)
224-
notes = []logger.MsgData{t.MsgData(tsAlwaysStrict.Range, fmt.Sprintf(
225-
"TypeScript's %q setting was enabled here:", tsAlwaysStrict.Name))}
229+
switch scope.StrictMode {
230+
case js_ast.ImplicitStrictModeClass:
231+
notes = []logger.MsgData{p.tracker.MsgData(p.enclosingClassKeyword,
232+
"All code inside a class is implicitly in strict mode")}
226233

227-
case js_ast.ImplicitStrictModeJSXAutomaticRuntime:
228-
notes = []logger.MsgData{p.tracker.MsgData(logger.Range{Loc: p.firstJSXElementLoc, Len: 1},
229-
"This file is implicitly in strict mode due to the JSX element here:"),
230-
{Text: "When React's \"automatic\" JSX transform is enabled, using a JSX element automatically inserts " +
231-
"an \"import\" statement at the top of the file for the corresponding the JSX helper function. " +
232-
"This means the file is considered an ECMAScript module, and all ECMAScript modules use strict mode."}}
234+
case js_ast.ImplicitStrictModeTSAlwaysStrict:
235+
tsAlwaysStrict := p.options.tsAlwaysStrict
236+
t := logger.MakeLineColumnTracker(&tsAlwaysStrict.Source)
237+
notes = []logger.MsgData{t.MsgData(tsAlwaysStrict.Range, fmt.Sprintf(
238+
"TypeScript's %q setting was enabled here:", tsAlwaysStrict.Name))}
233239

234-
case js_ast.ExplicitStrictMode:
235-
notes = []logger.MsgData{p.tracker.MsgData(p.source.RangeOfString(p.currentScope.UseStrictLoc),
236-
"Strict mode is triggered by the \"use strict\" directive here:")}
240+
case js_ast.ImplicitStrictModeJSXAutomaticRuntime:
241+
notes = []logger.MsgData{p.tracker.MsgData(logger.Range{Loc: p.firstJSXElementLoc, Len: 1},
242+
"This file is implicitly in strict mode due to the JSX element here:"),
243+
{Text: "When React's \"automatic\" JSX transform is enabled, using a JSX element automatically inserts " +
244+
"an \"import\" statement at the top of the file for the corresponding the JSX helper function. " +
245+
"This means the file is considered an ECMAScript module, and all ECMAScript modules use strict mode."}}
237246

238-
case js_ast.ImplicitStrictModeESM:
239-
_, notes = p.whyESModule()
240-
where = "in an ECMAScript module"
241-
}
247+
case js_ast.ExplicitStrictMode:
248+
notes = []logger.MsgData{p.tracker.MsgData(p.source.RangeOfString(scope.UseStrictLoc),
249+
"Strict mode is triggered by the \"use strict\" directive here:")}
242250

243-
p.log.AddErrorWithNotes(&p.tracker, r,
244-
fmt.Sprintf("%s cannot be used %s", text, where), notes)
245-
} else if !canBeTransformed && p.isStrictModeOutputFormat() {
246-
p.log.AddError(&p.tracker, r,
247-
fmt.Sprintf("%s cannot be used with the \"esm\" output format due to strict mode", text))
251+
case js_ast.ImplicitStrictModeESM:
252+
_, notes = p.whyESModule()
253+
where = "in an ECMAScript module"
248254
}
255+
256+
return
249257
}
250258

251259
func (p *parser) markAsyncFn(asyncRange logger.Range, isGenerator bool) (didGenerateError bool) {

internal/js_parser/js_parser_test.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,40 @@ func TestStrictMode(t *testing.T) {
533533
expectParseError(t, "with (y) z; await 0", tlaKeyword)
534534
expectParseError(t, "for await (x of y); with (y) z", tlaKeyword)
535535
expectParseError(t, "with (y) z; for await (x of y);", tlaKeyword)
536+
537+
cases := []string{
538+
"function f() {} function f() {}",
539+
"function f() {} function *f() {}",
540+
"function *f() {} function f() {}",
541+
"function f() {} async function f() {}",
542+
"async function f() {} function f() {}",
543+
"function f() {} async function *f() {}",
544+
"async function *f() {} function f() {}",
545+
"{ function f() {} function f() {} }",
546+
"switch (0) { case 1: function f() {} default: function f() {} }",
547+
}
548+
549+
fAlreadyDeclaredError :=
550+
"<stdin>: ERROR: The symbol \"f\" has already been declared\n" +
551+
"<stdin>: NOTE: The symbol \"f\" was originally declared here:\n" +
552+
"<stdin>: NOTE: Duplicate lexically-declared names are not allowed"
553+
554+
for _, c := range cases {
555+
expectParseError(t, c, "")
556+
557+
expectParseError(t, "'use strict'; "+c, fAlreadyDeclaredError+" in strict mode. "+
558+
"Strict mode is triggered by the \"use strict\" directive here:\n")
559+
560+
expectParseError(t, "function foo() { 'use strict'; "+c+" }", fAlreadyDeclaredError+" in strict mode. "+
561+
"Strict mode is triggered by the \"use strict\" directive here:\n")
562+
563+
expectParseError(t, c+" export {}", fAlreadyDeclaredError+" in an ECMAScript module. "+
564+
"This file is considered to be an ECMAScript module because of the \"export\" keyword here:\n")
565+
}
566+
567+
expectParseError(t, "var x; var x", "")
568+
expectParseError(t, "'use strict'; var x; var x", "")
569+
expectParseError(t, "var x; var x; export {}", "")
536570
}
537571

538572
func TestExponentiation(t *testing.T) {
@@ -1445,7 +1479,6 @@ func TestFunction(t *testing.T) {
14451479
expectPrintedMangle(t, "var f; function f() { x() } function f() { y() }", "var f;\nfunction f() {\n y();\n}\n")
14461480
expectPrintedMangle(t, "function f() { x() } function f() { y() } var f", "function f() {\n y();\n}\nvar f;\n")
14471481
expectPrintedMangle(t, "function f() { x() } var f; function f() { y() }", "function f() {\n x();\n}\nvar f;\nfunction f() {\n y();\n}\n")
1448-
expectPrintedMangle(t, "export function f() { x() } function f() { y() }", "export function f() {\n x();\n}\nfunction f() {\n y();\n}\n")
14491482

14501483
redeclaredError := "<stdin>: ERROR: The symbol \"f\" has already been declared\n" +
14511484
"<stdin>: NOTE: The symbol \"f\" was originally declared here:\n"

0 commit comments

Comments
 (0)