diff --git a/CHANGELOG.md b/CHANGELOG.md index f8fef6787a..aad701e667 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Unreleased + +* Symbols are now renamed separately per chunk ([#16](https://github.com/evanw/esbuild/issues/16)) + + Previously, bundling with code splitting assigned minified names using a single frequency distribution calculated across all chunks. This meant that typical code changes in one chunk would often cause the contents of all chunks to change, which negated some of the benefits of the browser cache. + + Now symbol renaming (both minified and not minified) is done separately per chunk. It was challenging to implement this without making esbuild a lot slower and causing it to use a lot more memory. Symbol renaming has been mostly rewritten to accomplish this and appears to actually usually use a little less memory and run a bit faster than before, even for code splitting builds that generate a lot of chunks. In addition, minified chunks are now slightly smaller because a given minified name can now be reused by multiple chunks. + ## 0.6.19 * Reduce memory usage for large builds by 30-40% ([#304](https://github.com/evanw/esbuild/issues/304)) diff --git a/internal/ast/ast.go b/internal/ast/ast.go index 4105021932..1aa234188d 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -1027,6 +1027,9 @@ const ( SymbolPrivateStaticSet SymbolPrivateStaticGetSetPair + // Labels are in their own namespace + SymbolLabel + // TypeScript enums can merge with TypeScript namespaces and other TypeScript // enums. SymbolTSEnum @@ -1100,9 +1103,12 @@ const ( ImportItemMissing ) -// Note: the order of valies in this struct matters to reduce struct size. +// Note: the order of values in this struct matters to reduce struct size. type Symbol struct { - Name string + // This is the name that came from the parser. Printed names may be renamed + // during minification or to avoid name collisions. Do not use the original + // name during printing. + OriginalName string // This is used for symbols that represent items in the import clause of an // ES6 import statement. These should always be referenced by EImportIdentifier @@ -1123,17 +1129,31 @@ type Symbol struct { // FollowSymbols to get the real one. Link Ref - // An estimate of the number of uses of this symbol. This is used for - // minification (to prefer shorter names for more frequently used symbols). - // The reason why this is an estimate instead of an accurate count is that - // it's not updated during dead code elimination for speed. I figure that - // even without updating after parsing it's still a pretty good heuristic. + // An estimate of the number of uses of this symbol. This is used to detect + // whether a symbol is used or not. For example, TypeScript imports that are + // unused must be removed because they are probably type-only imports. This + // is an estimate and may not be completely accurate due to oversights in the + // code. But it should always be non-zero when the symbol is used. UseCountEstimate uint32 - // This is for code splitting. Stored as one's complement so the zero value - // is invalid. + // This is for generating cross-chunk imports and exports for code splitting. + // It's stored as one's complement so the zero value is invalid. ChunkIndex uint32 + // This is used for minification. Symbols that are declared in sibling scopes + // can share a name. A good heuristic (from Google Closure Compiler) is to + // assign names to symbols from sibling scopes in declaration order. That way + // local variable names are reused in each global function like this, which + // improves gzip compression: + // + // function x(a, b) { ... } + // function y(a, b, c) { ... } + // + // The parser fills this in for symbols inside nested scopes. There are three + // slot namespaces: regular symbols, label symbols, and private symbols. This + // is stored as one's complement so the zero value is invalid. + NestedScopeSlot uint32 + Kind SymbolKind // Certain symbols must not be renamed or minified. For example, the @@ -1161,6 +1181,40 @@ type Symbol struct { ImportItemStatus ImportItemStatus } +type SlotNamespace uint8 + +const ( + SlotDefault SlotNamespace = iota + SlotLabel + SlotPrivateName + SlotMustNotBeRenamed +) + +func (s *Symbol) SlotNamespace() SlotNamespace { + if s.Kind == SymbolUnbound || s.MustNotBeRenamed { + return SlotMustNotBeRenamed + } + if s.Kind.IsPrivate() { + return SlotPrivateName + } + if s.Kind == SymbolLabel { + return SlotLabel + } + return SlotDefault +} + +type SlotCounts [3]uint32 + +func (a *SlotCounts) UnionMax(b SlotCounts) { + for i := range *a { + ai := &(*a)[i] + bi := b[i] + if *ai < bi { + *ai = bi + } + } +} + type NamespaceAlias struct { NamespaceRef Ref Alias string @@ -1265,8 +1319,9 @@ type ImportRecord struct { } type AST struct { - ApproximateLineCount int32 - HasLazyExport bool + ApproximateLineCount int32 + NestedScopeSlotCounts SlotCounts + HasLazyExport bool // This is a list of CommonJS features. When a file uses CommonJS features, // it's not a candidate for "flat bundling" and must be wrapped in its own @@ -1336,7 +1391,8 @@ type NamedImport struct { // shaking and can be assigned to separate chunks (i.e. output files) by code // splitting. type Part struct { - Stmts []Stmt + Stmts []Stmt + Scopes []*Scope // Each is an index into the file-level import record list ImportRecordIndices []uint32 diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index e1aaef3864..63c51c0961 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -852,9 +852,10 @@ func (b *Bundle) generateMetadataJSON(results []OutputFile) []byte { } type runtimeCacheKey struct { - MangleSyntax bool - ES6 bool - Platform config.Platform + MangleSyntax bool + MinifyIdentifiers bool + ES6 bool + Platform config.Platform } type runtimeCache struct { @@ -870,9 +871,10 @@ var globalRuntimeCache runtimeCache func (cache *runtimeCache) parseRuntime(options *config.Options) (source logging.Source, runtimeAST ast.AST, ok bool) { key := runtimeCacheKey{ // All configuration options that the runtime code depends on must go here - MangleSyntax: options.MangleSyntax, - Platform: options.Platform, - ES6: runtime.CanUseES6(options.UnsupportedFeatures), + MangleSyntax: options.MangleSyntax, + MinifyIdentifiers: options.MinifyIdentifiers, + Platform: options.Platform, + ES6: runtime.CanUseES6(options.UnsupportedFeatures), } // Determine which source to use @@ -898,9 +900,10 @@ func (cache *runtimeCache) parseRuntime(options *config.Options) (source logging log := logging.NewDeferLog() runtimeAST, ok = parser.Parse(log, source, config.Options{ // These configuration options must only depend on the key - MangleSyntax: key.MangleSyntax, - Platform: key.Platform, - Defines: cache.processedDefines(key.Platform), + MangleSyntax: key.MangleSyntax, + MinifyIdentifiers: key.MinifyIdentifiers, + Platform: key.Platform, + Defines: cache.processedDefines(key.Platform), // Always do tree shaking for the runtime because we never want to // include unnecessary runtime code diff --git a/internal/bundler/linker.go b/internal/bundler/linker.go index 84086125da..410082e2c3 100644 --- a/internal/bundler/linker.go +++ b/internal/bundler/linker.go @@ -11,6 +11,7 @@ import ( "sync" "github.com/evanw/esbuild/internal/compat" + "github.com/evanw/esbuild/internal/renamer" "github.com/evanw/esbuild/internal/resolver" "github.com/evanw/esbuild/internal/ast" @@ -74,6 +75,11 @@ type linkerContext struct { // to help ensure deterministic builds (source indices are random). reachableFiles []uint32 + // This maps from unstable source index to stable reachable file index. This + // is useful as a deterministic key for sorting if you need to sort something + // containing a source index (such as "ast.Ref" symbol references). + stableSourceIndices []uint32 + // We may need to refer to the CommonJS "module" symbol for exports unboundModuleRef ast.Ref } @@ -317,12 +323,6 @@ func newLinkerContext( c.symbols.Outer[sourceIndex] = fileSymbols file.ast.Symbols = nil - // Zero out the use count statistics. These will be recomputed later after - // taking tree shaking into account. - for i := range fileSymbols { - fileSymbols[i].UseCountEstimate = 0 - } - // Clone the parts file.ast.Parts = append([]ast.Part{}, file.ast.Parts...) for i, part := range file.ast.Parts { @@ -381,6 +381,12 @@ func newLinkerContext( } } + // Create a way to convert source indices to a stable ordering + c.stableSourceIndices = make([]uint32, len(sources)) + for stableIndex, sourceIndex := range c.reachableFiles { + c.stableSourceIndices[sourceIndex] = uint32(stableIndex) + } + // Mark all entry points so we don't add them again for import() expressions for _, sourceIndex := range entryPoints { fileMeta := &c.fileMeta[sourceIndex] @@ -398,9 +404,9 @@ func newLinkerContext( runtimeSymbols := &c.symbols.Outer[runtime.SourceIndex] c.unboundModuleRef = ast.Ref{OuterIndex: runtime.SourceIndex, InnerIndex: uint32(len(*runtimeSymbols))} *runtimeSymbols = append(*runtimeSymbols, ast.Symbol{ - Kind: ast.SymbolUnbound, - Name: "module", - Link: ast.InvalidRef, + Kind: ast.SymbolUnbound, + OriginalName: "module", + Link: ast.InvalidRef, }) } @@ -512,8 +518,6 @@ func (c *linkerContext) link() []OutputFile { // won't hit concurrent map mutation hazards ast.FollowAllSymbols(c.symbols) - c.renameOrMinifyAllSymbols() - return c.generateChunksInParallel(chunks) } @@ -757,17 +761,14 @@ func (c *linkerContext) computeCrossChunkDependencies(chunks []chunkInfo) { chunk.exportsToOtherChunks = make(map[ast.Ref]string) switch c.options.OutputFormat { case config.FormatESModule: - r := exportRenamer{ - symbols: c.symbols, - used: make(map[string]uint32), - } + r := renamer.ExportRenamer{} var items []ast.ClauseItem for _, export := range c.sortedCrossChunkExportItems(chunkMetas[chunkIndex].exports) { var alias string if c.options.MinifyIdentifiers { - alias = r.nextMinifiedName() + alias = r.NextMinifiedName() } else { - alias = r.nextRenamedName(c.symbols.Get(export.ref).Name) + alias = r.NextRenamedName(c.symbols.Get(export.ref).OriginalName) } items = append(items, ast.ClauseItem{Name: ast.LocRef{Ref: export.ref}, Alias: alias}) chunk.exportsToOtherChunks[export.ref] = alias @@ -1147,6 +1148,17 @@ func (c *linkerContext) scanImportsAndExports() { file := &c.files[sourceIndex] fileMeta := &c.fileMeta[sourceIndex] + // If this isn't CommonJS, then rename the unused "exports" and "module" + // variables to avoid them causing the identically-named variables in + // actual CommonJS files from being renamed. This is purely about + // aesthetics and is not about correctness. This is done here because by + // this point, we know the CommonJS status will not change further. + if !fileMeta.cjsWrap && !fileMeta.cjsStyleExports { + name := c.sources[sourceIndex].IdentifierName + c.symbols.Get(file.ast.ExportsRef).OriginalName = name + "_exports" + c.symbols.Get(file.ast.ModuleRef).OriginalName = name + "_module" + } + // Include the "__export" symbol from the runtime if it was used in the // previous step. The previous step can't do this because it's running in // parallel and can't safely mutate the "importsToBind" map of another file. @@ -1220,7 +1232,7 @@ func (c *linkerContext) generateCodeForLazyExport(sourceIndex uint32, file *file // Generate a new symbol inner := &c.symbols.Outer[sourceIndex] ref := ast.Ref{OuterIndex: sourceIndex, InnerIndex: uint32(len(*inner))} - *inner = append(*inner, ast.Symbol{Kind: ast.SymbolOther, Name: name, Link: ast.InvalidRef}) + *inner = append(*inner, ast.Symbol{Kind: ast.SymbolOther, OriginalName: name, Link: ast.InvalidRef}) file.ast.ModuleScope.Generated = append(file.ast.ModuleScope.Generated, ref) // Generate an ES6 export @@ -1325,9 +1337,9 @@ func (c *linkerContext) createExportsForFile(sourceIndex uint32) { inner := &c.symbols.Outer[sourceIndex] tempRef := ast.Ref{OuterIndex: sourceIndex, InnerIndex: uint32(len(*inner))} *inner = append(*inner, ast.Symbol{ - Kind: ast.SymbolOther, - Name: "export_" + alias, - Link: ast.InvalidRef, + Kind: ast.SymbolOther, + OriginalName: "export_" + alias, + Link: ast.InvalidRef, }) // Stick it on the module scope so it gets renamed and minified @@ -1997,11 +2009,6 @@ func (c *linkerContext) handleCrossChunkAssignments() { } } -func (c *linkerContext) accumulateSymbolCount(ref ast.Ref, count uint32) { - ref = ast.FollowSymbols(c.symbols, ref) - c.symbols.Get(ref).UseCountEstimate += count -} - func (c *linkerContext) includeFile(sourceIndex uint32, entryPointBit uint, distanceFromEntryPoint uint32) { fileMeta := &c.fileMeta[sourceIndex] @@ -2017,15 +2024,7 @@ func (c *linkerContext) includeFile(sourceIndex uint32, entryPointBit uint, dist } fileMeta.entryBits.setBit(entryPointBit) - // Accumulate symbol usage counts file := &c.files[sourceIndex] - if file.ast.UsesExportsRef { - c.accumulateSymbolCount(file.ast.ExportsRef, 1) - } - if file.ast.UsesModuleRef { - c.accumulateSymbolCount(file.ast.ModuleRef, 1) - } - for partIndex, part := range file.ast.Parts { canBeRemovedIfUnused := part.CanBeRemovedIfUnused @@ -2211,16 +2210,6 @@ func (c *linkerContext) includePart(sourceIndex uint32, partIndex uint32, entryP } } c.includePartsForRuntimeSymbol(part, fileMeta, exportStarUses, "__exportStar", entryPointBit, distanceFromEntryPoint) - - // Accumulate symbol usage counts. Do this last to also include - // automatically-generated usages from the code above. - for ref, use := range part.SymbolUses { - c.accumulateSymbolCount(ref, use.CountEstimate) - } - for _, declared := range part.DeclaredSymbols { - // Make sure to also count the declaration in addition to the uses - c.accumulateSymbolCount(declared.Ref, 1) - } } func (c *linkerContext) computeChunks() []chunkInfo { @@ -2688,6 +2677,7 @@ type stmtList struct { } func (c *linkerContext) generateCodeForFileInChunk( + r renamer.Renamer, waitGroup *sync.WaitGroup, sourceIndex uint32, entryBits bitSet, @@ -2803,7 +2793,7 @@ func (c *linkerContext) generateCodeForFileInChunk( tree := file.ast tree.Parts = []ast.Part{{Stmts: stmts}} *result = compileResult{ - PrintResult: printer.Print(tree, c.symbols, printOptions), + PrintResult: printer.Print(tree, c.symbols, r, printOptions), sourceIndex: sourceIndex, } @@ -2813,13 +2803,133 @@ func (c *linkerContext) generateCodeForFileInChunk( if len(stmtList.entryPointTail) > 0 { tree := file.ast tree.Parts = []ast.Part{{Stmts: stmtList.entryPointTail}} - entryPointTail := printer.Print(tree, c.symbols, printOptions) + entryPointTail := printer.Print(tree, c.symbols, r, printOptions) result.entryPointTail = &entryPointTail } waitGroup.Done() } +func (c *linkerContext) renameSymbolsInChunk(chunk *chunkInfo, filesInOrder []uint32) renamer.Renamer { + // Determine the reserved names (e.g. can't generate the name "if") + moduleScopes := make([]*ast.Scope, len(filesInOrder)) + for i, sourceIndex := range filesInOrder { + moduleScopes[i] = c.files[sourceIndex].ast.ModuleScope + } + reservedNames := renamer.ComputeReservedNames(moduleScopes, c.symbols) + + // These are used to implement bundling, and need to be free for use + if c.options.IsBundling { + reservedNames["require"] = 1 + reservedNames["Promise"] = 1 + } + + // Minification uses frequency analysis to give shorter names to more frequent symbols + if c.options.MinifyIdentifiers { + // Determine the first top-level slot (i.e. not in a nested scope) + var firstTopLevelSlots ast.SlotCounts + for _, sourceIndex := range filesInOrder { + firstTopLevelSlots.UnionMax(c.files[sourceIndex].ast.NestedScopeSlotCounts) + } + r := renamer.NewMinifyRenamer(c.symbols, firstTopLevelSlots, reservedNames) + + // Accumulate symbol usage counts into their slots + for _, sourceIndex := range filesInOrder { + file := &c.files[sourceIndex] + fileMeta := &c.fileMeta[sourceIndex] + if file.ast.UsesExportsRef { + r.AccumulateSymbolCount(file.ast.ExportsRef, 1) + } + if file.ast.UsesModuleRef { + r.AccumulateSymbolCount(file.ast.ModuleRef, 1) + } + + for partIndex, part := range file.ast.Parts { + if !chunk.entryBits.equals(fileMeta.partMeta[partIndex].entryBits) { + // Skip the part if it's not in this chunk + continue + } + + // Accumulate symbol use counts + r.AccumulateSymbolUseCounts(part.SymbolUses, c.stableSourceIndices) + + // Make sure to also count the declaration in addition to the uses + for _, declared := range part.DeclaredSymbols { + r.AccumulateSymbolCount(declared.Ref, 1) + } + } + } + + r.AssignNamesByFrequency() + return r + } + + // When we're not minifying, just append numbers to symbol names to avoid collisions + r := renamer.NewNumberRenamer(c.symbols, reservedNames) + nestedScopes := make(map[uint32][]*ast.Scope) + + // Make sure imports get a chance to be renamed + var sorted renamer.StableRefArray + for _, imports := range chunk.importsFromOtherChunks { + for _, item := range imports { + sorted = append(sorted, renamer.StableRef{ + StableOuterIndex: c.stableSourceIndices[item.ref.OuterIndex], + Ref: item.ref, + }) + } + } + sort.Sort(sorted) + for _, stable := range sorted { + r.AddTopLevelSymbol(stable.Ref) + } + + for _, sourceIndex := range filesInOrder { + file := &c.files[sourceIndex] + fileMeta := &c.fileMeta[sourceIndex] + var scopes []*ast.Scope + + // Modules wrapped in a CommonJS closure look like this: + // + // // foo.js + // var require_foo = __commonJS((exports, module) => { + // ... + // }); + // + // The symbol "require_foo" is stored in "file.ast.WrapperRef". We want + // to be able to minify everything inside the closure without worrying + // about collisions with other CommonJS modules. Set up the scopes such + // that it appears as if the file was structured this way all along. It's + // not completely accurate (e.g. we don't set the parent of the module + // scope to this new top-level scope) but it's good enough for the + // renaming code. + if fileMeta.cjsWrap { + r.AddTopLevelSymbol(file.ast.WrapperRef) + nestedScopes[sourceIndex] = []*ast.Scope{file.ast.ModuleScope} + continue + } + + // Rename each top-level symbol declaration in this chunk + for partIndex, part := range file.ast.Parts { + if chunk.entryBits.equals(fileMeta.partMeta[partIndex].entryBits) { + for _, declared := range part.DeclaredSymbols { + if declared.IsTopLevel { + r.AddTopLevelSymbol(declared.Ref) + } + } + scopes = append(scopes, part.Scopes...) + } + } + + nestedScopes[sourceIndex] = scopes + } + + // Recursively rename symbols in child scopes now that all top-level + // symbols have been renamed. This is done in parallel because the symbols + // inside nested scopes are independent and can't conflict. + r.AssignNamesByScope(nestedScopes) + return r +} + func (c *linkerContext) generateChunk(chunk *chunkInfo) func([]ast.ImportRecord) []OutputFile { var results []OutputFile filesInChunkInOrder := c.chunkFileOrder(chunk) @@ -2827,6 +2937,7 @@ func (c *linkerContext) generateChunk(chunk *chunkInfo) func([]ast.ImportRecord) runtimeMembers := c.files[runtime.SourceIndex].ast.ModuleScope.Members commonJSRef := ast.FollowSymbols(c.symbols, runtimeMembers["__commonJS"]) toModuleRef := ast.FollowSymbols(c.symbols, runtimeMembers["__toModule"]) + r := c.renameSymbolsInChunk(chunk, filesInChunkInOrder) // Generate JavaScript for each file in parallel waitGroup := sync.WaitGroup{} @@ -2847,6 +2958,7 @@ func (c *linkerContext) generateChunk(chunk *chunkInfo) func([]ast.ImportRecord) compileResult := &compileResults[len(compileResults)-1] waitGroup.Add(1) go c.generateCodeForFileInChunk( + r, &waitGroup, sourceIndex, chunk.entryBits, @@ -2875,10 +2987,10 @@ func (c *linkerContext) generateChunk(chunk *chunkInfo) func([]ast.ImportRecord) crossChunkPrefix = printer.Print(ast.AST{ ImportRecords: crossChunkImportRecords, Parts: []ast.Part{{Stmts: chunk.crossChunkPrefixStmts}}, - }, c.symbols, printOptions).JS + }, c.symbols, r, printOptions).JS crossChunkSuffix = printer.Print(ast.AST{ Parts: []ast.Part{{Stmts: chunk.crossChunkSuffixStmts}}, - }, c.symbols, printOptions).JS + }, c.symbols, r, printOptions).JS } waitGroup.Wait() @@ -3251,74 +3363,6 @@ func (c *linkerContext) markExportsAsUnbound(sourceIndex uint32) { } } -func (c *linkerContext) renameOrMinifyAllSymbols() { - topLevelScopes := make([]*ast.Scope, 0, len(c.files)) - moduleScopes := make([]*ast.Scope, 0, len(c.files)) - - // Combine all file scopes - for _, sourceIndex := range c.reachableFiles { - file := &c.files[sourceIndex] - fileMeta := &c.fileMeta[sourceIndex] - moduleScopes = append(moduleScopes, file.ast.ModuleScope) - if fileMeta.cjsWrap { - // Modules wrapped in a CommonJS closure look like this: - // - // // foo.js - // var require_foo = __commonJS((exports, module) => { - // ... - // }); - // - // The symbol "require_foo" is stored in "file.ast.WrapperRef". We want - // to be able to minify everything inside the closure without worrying - // about collisions with other CommonJS modules. Set up the scopes such - // that it appears as if the file was structured this way all along. It's - // not completely accurate (e.g. we don't set the parent of the module - // scope to this new top-level scope) but it's good enough for the - // renaming code. - // - // Note: make sure to not mutate the original scope since it's supposed - // to be immutable. - fakeTopLevelScope := &ast.Scope{ - Members: make(map[string]ast.Ref), - Generated: []ast.Ref{file.ast.WrapperRef}, - Children: []*ast.Scope{file.ast.ModuleScope}, - } - - // The unbound symbols are stored in the module scope. We need them for - // computing reserved names. Avoid needing to copy them all into the new - // fake top-level scope by still scanning the real module scope for - // unbound symbols. - topLevelScopes = append(topLevelScopes, fakeTopLevelScope) - } else { - topLevelScopes = append(topLevelScopes, file.ast.ModuleScope) - - // If this isn't CommonJS, then rename the unused "exports" and "module" - // variables to avoid them causing the identically-named variables in - // actual CommonJS files from being renamed. This is purely about - // aesthetics and is not about correctness. - if !fileMeta.cjsStyleExports { - name := c.sources[sourceIndex].IdentifierName - c.symbols.Get(file.ast.ExportsRef).Name = name + "_exports" - c.symbols.Get(file.ast.ModuleRef).Name = name + "_module" - } - } - } - - // Avoid collisions with any unbound symbols in this module group - reservedNames := computeReservedNames(moduleScopes, c.symbols) - if c.options.IsBundling { - // These are used to implement bundling, and need to be free for use - reservedNames["require"] = true - reservedNames["Promise"] = true - } - - if c.options.MinifyIdentifiers { - minifyAllSymbols(reservedNames, topLevelScopes, c.symbols) - } else { - renameAllSymbols(reservedNames, topLevelScopes, c.symbols) - } -} - func (c *linkerContext) generateSourceMapForChunk(results []compileResult) []byte { j := printer.Joiner{} j.AddString("{\n \"version\": 3") diff --git a/internal/bundler/renamer.go b/internal/bundler/renamer.go deleted file mode 100644 index 6fb9c6aa47..0000000000 --- a/internal/bundler/renamer.go +++ /dev/null @@ -1,448 +0,0 @@ -package bundler - -import ( - "sort" - "strconv" - "sync" - "sync/atomic" - - "github.com/evanw/esbuild/internal/ast" - "github.com/evanw/esbuild/internal/lexer" -) - -func computeReservedNames(moduleScopes []*ast.Scope, symbols ast.SymbolMap) map[string]bool { - names := make(map[string]bool) - - // All keywords are reserved names - for k := range lexer.Keywords() { - names[k] = true - } - - // All unbound symbols must be reserved names - for _, scope := range moduleScopes { - for _, ref := range scope.Members { - symbol := symbols.Get(ref) - if symbol.Kind == ast.SymbolUnbound { - names[symbol.Name] = true - } - } - for _, ref := range scope.Generated { - symbol := symbols.Get(ref) - if symbol.Kind == ast.SymbolUnbound { - names[symbol.Name] = true - } - } - } - - return names -} - -func sortedSymbolsInScope(scope *ast.Scope) uint64Array { - // Sort for determinism - sorted := uint64Array(make([]uint64, 0, len(scope.Members)+len(scope.Generated))) - for _, ref := range scope.Members { - sorted = append(sorted, (uint64(ref.OuterIndex)<<32)|uint64(ref.InnerIndex)) - } - for _, ref := range scope.Generated { - sorted = append(sorted, (uint64(ref.OuterIndex)<<32)|uint64(ref.InnerIndex)) - } - sort.Sort(sorted) - return sorted -} - -type exportRenamer struct { - symbols ast.SymbolMap - count int - used map[string]uint32 -} - -func (r *exportRenamer) nextRenamedName(name string) string { - if tries, ok := r.used[name]; ok { - prefix := name - for { - tries++ - name = prefix + strconv.Itoa(int(tries)) - if _, ok := r.used[name]; !ok { - break - } - } - r.used[name] = tries - } else { - r.used[name] = 1 - } - return name -} - -func (r *exportRenamer) nextMinifiedName() string { - name := lexer.NumberToMinifiedName(r.count) - r.count++ - return name -} - -//////////////////////////////////////////////////////////////////////////////// -// renameAllSymbols() implementation - -type renamer struct { - parent *renamer - - // This is used as a set of used names in this scope. This also maps the name - // to the number of times the name has experienced a collision. When a name - // collides with an already-used name, we need to rename it. This is done by - // incrementing a number at the end until the name is unused. We save the - // count here so that subsequent collisions can start counting from where the - // previous collision ended instead of having to start counting from 1. - nameCounts map[string]uint32 -} - -type nameUse uint8 - -const ( - nameUnused nameUse = iota - nameUsed - nameUsedInSameScope -) - -func (r *renamer) findNameUse(name string) nameUse { - original := r - for { - if _, ok := r.nameCounts[name]; ok { - if r == original { - return nameUsedInSameScope - } - return nameUsed - } - r = r.parent - if r == nil { - return nameUnused - } - } -} - -func (r *renamer) findUnusedName(name string) string { - if use := r.findNameUse(name); use != nameUnused { - // If the name is already in use, generate a new name by appending a number - tries := uint32(1) - if use == nameUsedInSameScope { - // To avoid O(n^2) behavior, the number must start off being the number - // that we used last time there was a collision with this name. Otherwise - // if there are many collisions with the same name, each name collision - // would have to increment the counter past all previous name collisions - // which is a O(n^2) time algorithm. Only do this if this symbol comes - // from the same scope as the previous one since sibling scopes can reuse - // the same name without problems. - tries = r.nameCounts[name] - } - prefix := name - - // Keep incrementing the number until the name is unused - for { - tries++ - name = prefix + strconv.Itoa(int(tries)) - - // Make sure this new name is unused - if r.findNameUse(name) == nameUnused { - // Store the count so we can start here next time instead of starting - // from 1. This means we avoid O(n^2) behavior. - if use == nameUsedInSameScope { - r.nameCounts[prefix] = tries - } - break - } - } - } - - // Each name starts off with a count of 1 so that the first collision with - // "name" is called "name2" - r.nameCounts[name] = 1 - return name -} - -func renameAllSymbols(reservedNames map[string]bool, moduleScopes []*ast.Scope, symbols ast.SymbolMap) { - reservedNameCounts := make(map[string]uint32) - for name := range reservedNames { - // Each name starts off with a count of 1 so that the first collision with - // "name" is called "name2" - reservedNameCounts[name] = 1 - } - r := &renamer{nil, reservedNameCounts} - - // This is essentially a "map[ast.Ref]bool" but we use an array instead of a - // map so we can mutate it concurrently from multiple threads. - alreadyRenamed := make([][]bool, len(symbols.Outer)) - for sourceIndex, inner := range symbols.Outer { - alreadyRenamed[sourceIndex] = make([]bool, len(inner)) - } - - // Rename top-level symbols across all files all at once since after - // bundling, they will all be in the same scope - for _, scope := range moduleScopes { - r.renameSymbolsInScope(scope, symbols, alreadyRenamed) - } - - // Symbols in child scopes may also have to be renamed to avoid conflicts. - // Since child scopes in different files are isolated from each other, we - // can process each file independently in parallel. - waitGroup := sync.WaitGroup{} - waitGroup.Add(len(moduleScopes)) - for _, scope := range moduleScopes { - go func(scope *ast.Scope) { - for _, child := range scope.Children { - r.renameAllSymbolsRecursive(child, symbols, alreadyRenamed) - } - waitGroup.Done() - }(scope) - } - waitGroup.Wait() -} - -func (r *renamer) renameSymbolsInScope(scope *ast.Scope, symbols ast.SymbolMap, alreadyRenamed [][]bool) { - sorted := sortedSymbolsInScope(scope) - - // Rename all symbols in this scope - for _, i := range sorted { - ref := ast.Ref{OuterIndex: uint32(i >> 32), InnerIndex: uint32(i)} - ref = ast.FollowSymbols(symbols, ref) - - // Don't rename the same symbol more than once - if alreadyRenamed[ref.OuterIndex][ref.InnerIndex] { - continue - } - alreadyRenamed[ref.OuterIndex][ref.InnerIndex] = true - - symbol := symbols.Get(ref) - - // Don't rename unbound symbols and symbols marked as reserved names - if symbol.Kind == ast.SymbolUnbound || symbol.MustNotBeRenamed { - continue - } - - symbol.Name = r.findUnusedName(symbol.Name) - } -} - -func (parent *renamer) renameAllSymbolsRecursive(scope *ast.Scope, symbols ast.SymbolMap, alreadyRenamed [][]bool) { - r := &renamer{parent, make(map[string]uint32)} - r.renameSymbolsInScope(scope, symbols, alreadyRenamed) - - // Symbols in child scopes may also have to be renamed to avoid conflicts - for _, child := range scope.Children { - r.renameAllSymbolsRecursive(child, symbols, alreadyRenamed) - } -} - -//////////////////////////////////////////////////////////////////////////////// -// minifyAllSymbols() implementation - -func minifyAllSymbols(reservedNames map[string]bool, moduleScopes []*ast.Scope, symbols ast.SymbolMap) { - g := minifyGroup{make([][]minifyInfo, len(symbols.Outer))} - for sourceIndex, inner := range symbols.Outer { - g.symbolToMinifyInfo[sourceIndex] = make([]minifyInfo, len(inner)) - } - next := uint32(0) - nextPrivate := uint32(0) - - // Allocate a slot for every symbol in each top-level scope. These slots must - // not overlap between files because the bundler may smoosh everything - // together into a single scope. - for _, scope := range moduleScopes { - next, nextPrivate = g.countSymbolsInScope(scope, symbols, next, nextPrivate) - } - - // Allocate a slot for every symbol in each nested scope. Since it's - // impossible for symbols from nested scopes to conflict, symbols from - // different nested scopes can reuse the same slots (and therefore get the - // same minified names). - // - // One good heuristic is to merge slots from different nested scopes using - // sequential assignment. Then top-level function statements will always have - // the same argument names, which is better for gzip compression. - // - // This code uses atomics to avoid counting the same symbol twice, so it can - // be parallelized across multiple threads. - waitGroup := sync.WaitGroup{} - waitGroup.Add(len(moduleScopes)) - for _, scope := range moduleScopes { - go func(scope *ast.Scope) { - for _, child := range scope.Children { - // Deliberately don't update "next" and "nextPrivate" here. Sibling - // scopes can't collide and so can reuse slots. - g.countSymbolsRecursive(child, symbols, next, nextPrivate, 0) - } - waitGroup.Done() - }(scope) - } - waitGroup.Wait() - - // Find the largest slot value - maxSlot := uint32(0) - maxPrivateSlot := uint32(0) - for outer, array := range g.symbolToMinifyInfo { - for inner, data := range array { - if data.used != 0 { - if symbols.Outer[outer][inner].Kind.IsPrivate() { - if data.slot > maxPrivateSlot { - maxPrivateSlot = data.slot - } - } else { - if data.slot > maxSlot { - maxSlot = data.slot - } - } - } - } - } - - // Allocate one count for each slot - slotToCount := make([]uint32, maxSlot+1) - privateSlotToCount := make([]uint32, maxPrivateSlot+1) - for outer, array := range g.symbolToMinifyInfo { - for inner, data := range array { - if data.used != 0 { - if symbols.Outer[outer][inner].Kind.IsPrivate() { - privateSlotToCount[data.slot] += data.count - } else { - slotToCount[data.slot] += data.count - } - } - } - } - - // Sort slot indices descending by the count for that slot - sorted := slotAndCountArray(make([]slotAndCount, len(slotToCount))) - privateSorted := slotAndCountArray(make([]slotAndCount, len(privateSlotToCount))) - for slot, count := range slotToCount { - sorted[slot] = slotAndCount{uint32(slot), count} - } - for slot, count := range privateSlotToCount { - privateSorted[slot] = slotAndCount{uint32(slot), count} - } - sort.Sort(sorted) - sort.Sort(privateSorted) - - // Assign names sequentially in order so the most frequent symbols get the - // shortest names - nextName := 0 - names := make([]string, len(sorted)) - privateNames := make([]string, len(privateSorted)) - for _, data := range sorted { - name := lexer.NumberToMinifiedName(nextName) - nextName++ - - // Make sure we never generate a reserved name - for reservedNames[name] { - name = lexer.NumberToMinifiedName(nextName) - nextName++ - } - - names[data.slot] = name - } - for i, data := range privateSorted { - // Don't need to worry about collisions with reserved names here - privateNames[data.slot] = "#" + lexer.NumberToMinifiedName(i) - } - - // Copy the names to the appropriate symbols - for outer, array := range g.symbolToMinifyInfo { - for inner, data := range array { - if data.used != 0 { - symbol := &symbols.Outer[outer][inner] - if symbol.Kind.IsPrivate() { - symbol.Name = privateNames[data.slot] - } else { - symbol.Name = names[data.slot] - } - } - } - } -} - -type minifyGroup struct { - symbolToMinifyInfo [][]minifyInfo -} - -type minifyInfo struct { - used uint32 - slot uint32 - count uint32 -} - -func (g *minifyGroup) countSymbol(slot uint32, ref ast.Ref, count uint32) bool { - // Don't double-count symbols that have already been counted - minifyInfo := &g.symbolToMinifyInfo[ref.OuterIndex][ref.InnerIndex] - if !atomic.CompareAndSwapUint32(&minifyInfo.used, 0, 1) { - return false - } - - // Count this symbol in this slot - minifyInfo.slot = slot - minifyInfo.count = count - return true -} - -func (g *minifyGroup) countSymbolsInScope(scope *ast.Scope, symbols ast.SymbolMap, next uint32, nextPrivate uint32) (uint32, uint32) { - sorted := sortedSymbolsInScope(scope) - - for _, i := range sorted { - ref := ast.Ref{OuterIndex: uint32(i >> 32), InnerIndex: uint32(i)} - ref = ast.FollowSymbols(symbols, ref) - symbol := symbols.Get(ref) - - // Don't rename unbound symbols and symbols marked as reserved names - if symbol.Kind == ast.SymbolUnbound || symbol.MustNotBeRenamed { - continue - } - - // Private symbols are in a different namespace - if symbol.Kind.IsPrivate() { - if g.countSymbol(nextPrivate, ref, symbol.UseCountEstimate) { - nextPrivate++ - } - } else { - if g.countSymbol(next, ref, symbol.UseCountEstimate) { - next++ - } - } - } - - return next, nextPrivate -} - -func (g *minifyGroup) countSymbolsRecursive( - scope *ast.Scope, symbols ast.SymbolMap, next uint32, nextPrivate uint32, labelCount uint32, -) (uint32, uint32) { - next, nextPrivate = g.countSymbolsInScope(scope, symbols, next, nextPrivate) - - // Labels are in a separate namespace from symbols - if scope.Kind == ast.ScopeLabel { - symbol := symbols.Get(scope.LabelRef) - g.countSymbol(labelCount, scope.LabelRef, symbol.UseCountEstimate+1) // +1 for the label itself - labelCount += 1 - } - - for _, child := range scope.Children { - // Deliberately don't update "next" and "nextPrivate" here. Sibling scopes - // can't collide and so can reuse slots. - g.countSymbolsRecursive(child, symbols, next, nextPrivate, labelCount) - } - return next, nextPrivate -} - -type slotAndCount struct { - slot uint32 - count uint32 -} - -// These types are just so we can use Go's native sort function -type uint64Array []uint64 -type slotAndCountArray []slotAndCount - -func (a uint64Array) Len() int { return len(a) } -func (a uint64Array) Swap(i int, j int) { a[i], a[j] = a[j], a[i] } -func (a uint64Array) Less(i int, j int) bool { return a[i] < a[j] } - -func (a slotAndCountArray) Len() int { return len(a) } -func (a slotAndCountArray) Swap(i int, j int) { a[i], a[j] = a[j], a[i] } -func (a slotAndCountArray) Less(i int, j int) bool { - ai, aj := a[i], a[j] - return ai.count > aj.count || (ai.count == aj.count && ai.slot < aj.slot) -} diff --git a/internal/bundler/snapshots/snapshots_default.txt b/internal/bundler/snapshots/snapshots_default.txt index 5c705f7b2e..89a7ba5591 100644 --- a/internal/bundler/snapshots/snapshots_default.txt +++ b/internal/bundler/snapshots/snapshots_default.txt @@ -523,21 +523,21 @@ export default require_entry(); TestExportsAndModuleFormatCommonJS ---------- /out.js ---------- // /foo/test.js -const test_exports2 = {}; -__export(test_exports2, { +const test_exports = {}; +__export(test_exports, { foo: () => foo }); let foo = 123; // /bar/test.js -const test_exports = {}; -__export(test_exports, { +const test_exports2 = {}; +__export(test_exports2, { bar: () => bar }); let bar = 123; // /entry.js -console.log(exports, module.exports, test_exports2, test_exports); +console.log(exports, module.exports, test_exports, test_exports2); ================================================================================ TestExternalModuleExclusionPackage @@ -1016,7 +1016,7 @@ console.log(shared_default); ================================================================================ TestMinifiedBundleCommonJS ---------- /out.js ---------- -var c=e(b=>{b.foo=function(){return 123}});var d=e((b,a)=>{a.exports={test:!0}});const{foo:f}=c();console.log(f(),d()); +var d=b(c=>{c.foo=function(){return 123}});var f=b((k,e)=>{e.exports={test:!0}});const{foo:h}=d();console.log(h(),f()); ================================================================================ TestMinifiedBundleES6 @@ -1032,21 +1032,21 @@ TestMinifiedBundleEndingWithImportantSemicolon TestMinifiedExportsAndModuleFormatCommonJS ---------- /out.js ---------- // /foo/test.js -const c = {}; -e(c, { +const b = {}; +g(b, { foo: () => i }); let i = 123; // /bar/test.js -const b = {}; -e(b, { - bar: () => h +const c = {}; +g(c, { + bar: () => j }); -let h = 123; +let j = 123; // /entry.js -console.log(exports, module.exports, c, b); +console.log(exports, module.exports, b, c); ================================================================================ TestMinifyPrivateIdentifiersNoBundle @@ -1486,9 +1486,9 @@ TestRenamePrivateIdentifiersNoBundle class Foo { #foo; foo = class { + #foo; #foo2; - #foo22; - #bar2; + #bar; }; get #bar() { } @@ -1499,8 +1499,8 @@ class Bar { #foo; foo = class { #foo2; - #foo3; - #bar2; + #foo; + #bar; }; get #bar() { } @@ -1798,9 +1798,9 @@ var require_es6_ns_export_variable = __commonJS((exports) => { var require_es6_ns_export_function = __commonJS((exports) => { var ns2; (function(ns3) { - function foo4() { + function foo() { } - ns3.foo = foo4; + ns3.foo = foo; })(ns2 || (ns2 = {})); console.log(exports); }); @@ -1809,9 +1809,9 @@ var require_es6_ns_export_function = __commonJS((exports) => { var require_es6_ns_export_async_function = __commonJS((exports) => { var ns2; (function(ns3) { - async function foo4() { + async function foo() { } - ns3.foo = foo4; + ns3.foo = foo; })(ns2 || (ns2 = {})); console.log(exports); }); @@ -1820,9 +1820,9 @@ var require_es6_ns_export_async_function = __commonJS((exports) => { var require_es6_ns_export_enum = __commonJS((exports) => { var ns2; (function(ns3) { - let Foo5; - (function(Foo6) { - })(Foo5 = ns3.Foo || (ns3.Foo = {})); + let Foo3; + (function(Foo4) { + })(Foo3 = ns3.Foo || (ns3.Foo = {})); })(ns2 || (ns2 = {})); console.log(exports); }); @@ -1831,9 +1831,9 @@ var require_es6_ns_export_enum = __commonJS((exports) => { var require_es6_ns_export_const_enum = __commonJS((exports) => { var ns2; (function(ns3) { - let Foo5; - (function(Foo6) { - })(Foo5 = ns3.Foo || (ns3.Foo = {})); + let Foo3; + (function(Foo4) { + })(Foo3 = ns3.Foo || (ns3.Foo = {})); })(ns2 || (ns2 = {})); console.log(exports); }); @@ -1852,9 +1852,9 @@ var require_es6_ns_export_namespace = __commonJS((exports) => { var require_es6_ns_export_class = __commonJS((exports) => { var ns2; (function(ns3) { - class Foo5 { + class Foo3 { } - ns3.Foo = Foo5; + ns3.Foo = Foo3; })(ns2 || (ns2 = {})); console.log(exports); }); @@ -1863,19 +1863,19 @@ var require_es6_ns_export_class = __commonJS((exports) => { var require_es6_ns_export_abstract_class = __commonJS((exports) => { var ns2; (function(ns3) { - class Foo5 { + class Foo3 { } - ns3.Foo = Foo5; + ns3.Foo = Foo3; })(ns2 || (ns2 = {})); console.log(exports); }); // /es6-import-stmt.js -const dummy2 = __toModule(require_dummy()); +const dummy = __toModule(require_dummy()); console.log(void 0); // /es6-import-assign.ts -const x2 = require_dummy(); +const x = require_dummy(); console.log(void 0); // /es6-import-dynamic.js @@ -1902,15 +1902,15 @@ console.log(void 0); console.log(void 0); // /es6-export-enum.ts -var Foo4; -(function(Foo5) { -})(Foo4 || (Foo4 = {})); +var Foo; +(function(Foo3) { +})(Foo || (Foo = {})); console.log(void 0); // /es6-export-const-enum.ts -var Foo3; -(function(Foo5) { -})(Foo3 || (Foo3 = {})); +var Foo2; +(function(Foo3) { +})(Foo2 || (Foo2 = {})); console.log(void 0); // /es6-export-module.ts @@ -1932,7 +1932,7 @@ console.log(void 0); console.log(void 0); // /es6-export-clause-from.js -const dummy = __toModule(require_dummy()); +const dummy2 = __toModule(require_dummy()); console.log(void 0); // /es6-export-star-as.js @@ -1940,7 +1940,7 @@ const ns = __toModule(require_dummy()); console.log(void 0); // /es6-export-import-assign.ts -const x = require_dummy(); +const x2 = require_dummy(); console.log(void 0); // /entry.js diff --git a/internal/bundler/snapshots/snapshots_importstar.txt b/internal/bundler/snapshots/snapshots_importstar.txt index 3478cd5ad9..7ed360fcad 100644 --- a/internal/bundler/snapshots/snapshots_importstar.txt +++ b/internal/bundler/snapshots/snapshots_importstar.txt @@ -113,8 +113,8 @@ module.exports = require_entry(); TestExportSelfCommonJSMinified ---------- /out.js ---------- // /entry.js -var b = d((c, a) => { - a.exports = {foo: 123}; +var b = e((g, c) => { + c.exports = {foo: 123}; console.log(b()); }); module.exports = b(); @@ -247,13 +247,13 @@ TestImportStarCapture // /foo.js const foo_exports = {}; __export(foo_exports, { - foo: () => foo2 + foo: () => foo }); -const foo2 = 123; +const foo = 123; // /entry.js -let foo = 234; -console.log(foo_exports, foo2, foo); +let foo2 = 234; +console.log(foo_exports, foo, foo2); ================================================================================ TestImportStarCommonJSCapture @@ -265,8 +265,8 @@ var require_foo = __commonJS((exports) => { // /entry.js const ns = __toModule(require_foo()); -let foo = 234; -console.log(ns, ns.foo, foo); +let foo2 = 234; +console.log(ns, ns.foo, foo2); ================================================================================ TestImportStarCommonJSNoCapture @@ -278,8 +278,8 @@ var require_foo = __commonJS((exports) => { // /entry.js const ns = __toModule(require_foo()); -let foo = 234; -console.log(ns.foo, ns.foo, foo); +let foo2 = 234; +console.log(ns.foo, ns.foo, foo2); ================================================================================ TestImportStarCommonJSUnused @@ -300,15 +300,15 @@ TestImportStarExportImportStarCapture // /foo.js const foo_exports = {}; __export(foo_exports, { - foo: () => foo2 + foo: () => foo }); -const foo2 = 123; +const foo = 123; // /bar.js // /entry.js -let foo = 234; -console.log(foo_exports, foo_exports.foo, foo); +let foo2 = 234; +console.log(foo_exports, foo_exports.foo, foo2); ================================================================================ TestImportStarExportImportStarNoCapture @@ -316,15 +316,15 @@ TestImportStarExportImportStarNoCapture // /foo.js const foo_exports = {}; __export(foo_exports, { - foo: () => foo2 + foo: () => foo }); -const foo2 = 123; +const foo = 123; // /bar.js // /entry.js -let foo = 234; -console.log(foo_exports.foo, foo_exports.foo, foo); +let foo2 = 234; +console.log(foo_exports.foo, foo_exports.foo, foo2); ================================================================================ TestImportStarExportImportStarUnused @@ -343,15 +343,15 @@ TestImportStarExportStarAsCapture // /foo.js const foo_exports = {}; __export(foo_exports, { - foo: () => foo2 + foo: () => foo }); -const foo2 = 123; +const foo = 123; // /bar.js // /entry.js -let foo = 234; -console.log(foo_exports, foo_exports.foo, foo); +let foo2 = 234; +console.log(foo_exports, foo_exports.foo, foo2); ================================================================================ TestImportStarExportStarAsNoCapture @@ -359,15 +359,15 @@ TestImportStarExportStarAsNoCapture // /foo.js const foo_exports = {}; __export(foo_exports, { - foo: () => foo2 + foo: () => foo }); -const foo2 = 123; +const foo = 123; // /bar.js // /entry.js -let foo = 234; -console.log(foo_exports.foo, foo_exports.foo, foo); +let foo2 = 234; +console.log(foo_exports.foo, foo_exports.foo, foo2); ================================================================================ TestImportStarExportStarAsUnused @@ -384,29 +384,29 @@ console.log(foo); TestImportStarExportStarCapture ---------- /out.js ---------- // /foo.js -const foo2 = 123; +const foo = 123; // /bar.js const bar_exports = {}; __export(bar_exports, { - foo: () => foo2 + foo: () => foo }); // /entry.js -let foo = 234; -console.log(bar_exports, foo2, foo); +let foo2 = 234; +console.log(bar_exports, foo, foo2); ================================================================================ TestImportStarExportStarNoCapture ---------- /out.js ---------- // /foo.js -const foo2 = 123; +const foo = 123; // /bar.js // /entry.js -let foo = 234; -console.log(foo2, foo2, foo); +let foo2 = 234; +console.log(foo, foo, foo2); ================================================================================ TestImportStarExportStarOmitAmbiguous @@ -484,11 +484,11 @@ console.log(foo); TestImportStarNoCapture ---------- /out.js ---------- // /foo.js -const foo2 = 123; +const foo = 123; // /entry.js -let foo = 234; -console.log(foo2, foo2, foo); +let foo2 = 234; +console.log(foo, foo, foo2); ================================================================================ TestImportStarOfExportStarAs @@ -609,22 +609,22 @@ console.log(void 0); TestOtherFileExportSelfAsNamespaceUnusedES6 ---------- /out.js ---------- // /foo.js -const foo2 = 123; +const foo = 123; // /entry.js export { - foo2 as foo + foo }; ================================================================================ TestOtherFileImportExportSelfAsNamespaceUnusedES6 ---------- /out.js ---------- // /foo.js -const foo2 = 123; +const foo = 123; // /entry.js export { - foo2 as foo + foo }; ================================================================================ diff --git a/internal/bundler/snapshots/snapshots_importstar_ts.txt b/internal/bundler/snapshots/snapshots_importstar_ts.txt index 7087ddf839..166ce5beb3 100644 --- a/internal/bundler/snapshots/snapshots_importstar_ts.txt +++ b/internal/bundler/snapshots/snapshots_importstar_ts.txt @@ -19,13 +19,13 @@ TestTSImportStarCapture // /foo.ts const foo_exports = {}; __export(foo_exports, { - foo: () => foo2 + foo: () => foo }); -const foo2 = 123; +const foo = 123; // /entry.ts -let foo = 234; -console.log(foo_exports, foo2, foo); +let foo2 = 234; +console.log(foo_exports, foo, foo2); ================================================================================ TestTSImportStarCommonJSCapture @@ -37,8 +37,8 @@ var require_foo = __commonJS((exports) => { // /entry.ts const ns = __toModule(require_foo()); -let foo = 234; -console.log(ns, ns.foo, foo); +let foo2 = 234; +console.log(ns, ns.foo, foo2); ================================================================================ TestTSImportStarCommonJSNoCapture @@ -50,8 +50,8 @@ var require_foo = __commonJS((exports) => { // /entry.ts const ns = __toModule(require_foo()); -let foo = 234; -console.log(ns.foo, ns.foo, foo); +let foo2 = 234; +console.log(ns.foo, ns.foo, foo2); ================================================================================ TestTSImportStarCommonJSUnused @@ -66,15 +66,15 @@ TestTSImportStarExportImportStarCapture // /foo.ts const foo_exports = {}; __export(foo_exports, { - foo: () => foo2 + foo: () => foo }); -const foo2 = 123; +const foo = 123; // /bar.ts // /entry.ts -let foo = 234; -console.log(foo_exports, foo_exports.foo, foo); +let foo2 = 234; +console.log(foo_exports, foo_exports.foo, foo2); ================================================================================ TestTSImportStarExportImportStarNoCapture @@ -82,15 +82,15 @@ TestTSImportStarExportImportStarNoCapture // /foo.ts const foo_exports = {}; __export(foo_exports, { - foo: () => foo2 + foo: () => foo }); -const foo2 = 123; +const foo = 123; // /bar.ts // /entry.ts -let foo = 234; -console.log(foo_exports.foo, foo_exports.foo, foo); +let foo2 = 234; +console.log(foo_exports.foo, foo_exports.foo, foo2); ================================================================================ TestTSImportStarExportImportStarUnused @@ -105,15 +105,15 @@ TestTSImportStarExportStarAsCapture // /foo.ts const foo_exports = {}; __export(foo_exports, { - foo: () => foo2 + foo: () => foo }); -const foo2 = 123; +const foo = 123; // /bar.ts // /entry.ts -let foo = 234; -console.log(foo_exports, foo_exports.foo, foo); +let foo2 = 234; +console.log(foo_exports, foo_exports.foo, foo2); ================================================================================ TestTSImportStarExportStarAsNoCapture @@ -121,15 +121,15 @@ TestTSImportStarExportStarAsNoCapture // /foo.ts const foo_exports = {}; __export(foo_exports, { - foo: () => foo2 + foo: () => foo }); -const foo2 = 123; +const foo = 123; // /bar.ts // /entry.ts -let foo = 234; -console.log(foo_exports.foo, foo_exports.foo, foo); +let foo2 = 234; +console.log(foo_exports.foo, foo_exports.foo, foo2); ================================================================================ TestTSImportStarExportStarAsUnused @@ -142,29 +142,29 @@ console.log(foo); TestTSImportStarExportStarCapture ---------- /out.js ---------- // /foo.ts -const foo2 = 123; +const foo = 123; // /bar.ts const bar_exports = {}; __export(bar_exports, { - foo: () => foo2 + foo: () => foo }); // /entry.ts -let foo = 234; -console.log(bar_exports, foo2, foo); +let foo2 = 234; +console.log(bar_exports, foo, foo2); ================================================================================ TestTSImportStarExportStarNoCapture ---------- /out.js ---------- // /foo.ts -const foo2 = 123; +const foo = 123; // /bar.ts // /entry.ts -let foo = 234; -console.log(foo2, foo2, foo); +let foo2 = 234; +console.log(foo, foo, foo2); ================================================================================ TestTSImportStarExportStarUnused @@ -217,11 +217,11 @@ console.log(foo); TestTSImportStarNoCapture ---------- /out.js ---------- // /foo.ts -const foo2 = 123; +const foo = 123; // /entry.ts -let foo = 234; -console.log(foo2, foo2, foo); +let foo2 = 234; +console.log(foo, foo, foo2); ================================================================================ TestTSImportStarUnused diff --git a/internal/bundler/snapshots/snapshots_splitting.txt b/internal/bundler/snapshots/snapshots_splitting.txt index 1e51ffb985..6f3381150c 100644 --- a/internal/bundler/snapshots/snapshots_splitting.txt +++ b/internal/bundler/snapshots/snapshots_splitting.txt @@ -270,7 +270,7 @@ export default require_foo(); TestSplittingDynamicES6IntoES6 ---------- /out/entry.js ---------- // /entry.js -import("./foo.js").then(({bar: bar2}) => console.log(bar2)); +import("./foo.js").then(({bar}) => console.log(bar)); ---------- /out/foo.js ---------- // /foo.js @@ -411,8 +411,8 @@ import { } from "./chunk.W46Rb0pk.js"; // /b.js -const {foo: foo2} = require_shared(); -console.log(foo2); +const {foo} = require_shared(); +console.log(foo); ---------- /out/chunk.W46Rb0pk.js ---------- // /shared.js diff --git a/internal/bundler/snapshots/snapshots_ts.txt b/internal/bundler/snapshots/snapshots_ts.txt index 12bce47926..65a17993cf 100644 --- a/internal/bundler/snapshots/snapshots_ts.txt +++ b/internal/bundler/snapshots/snapshots_ts.txt @@ -75,17 +75,17 @@ var type_nested_default = foo; let bar5 = 123; // /keep/value-namespace.ts -var foo4; +var foo3; (function(foo5) { foo5.num = 0; -})(foo4 || (foo4 = {})); +})(foo3 || (foo3 = {})); let bar6 = 123; // /keep/value-namespace-merged.ts -var foo3; +var foo4; (function(foo5) { foo5.num = 0; -})(foo3 || (foo3 = {})); +})(foo4 || (foo4 = {})); let bar7 = 123; // /remove/interface.ts @@ -240,7 +240,7 @@ console.log(a, b, c, d, e, real); ================================================================================ TestTSMinifiedBundleCommonJS ---------- /out.js ---------- -var c=e(b=>{b.foo=function(){return 123}});var d=e((b,a)=>{a.exports={test:!0}});const{foo:f}=c();console.log(f(),d()); +var d=b(c=>{c.foo=function(){return 123}});var f=b((k,e)=>{e.exports={test:!0}});const{foo:h}=d();console.log(h(),f()); ================================================================================ TestTSMinifiedBundleES6 @@ -443,39 +443,39 @@ e_default = __decorate([ var e_default2 = e_default; // /f.ts -let f2 = class { +let f = class { fn() { - return new f2(); + return new f(); } }; -f2.z = new f2(); -f2 = __decorate([ +f.z = new f(); +f = __decorate([ x(() => 0), y(() => 1) -], f2); -var f_default = f2; +], f); +var f_default = f; // /g.ts -let g_default2 = class { +let g_default = class { }; -g_default2 = __decorate([ +g_default = __decorate([ x(() => 0), y(() => 1) -], g_default2); -var g_default = g_default2; +], g_default); +var g_default2 = g_default; // /h.ts -let h2 = class { +let h = class { fn() { - return new h2(); + return new h(); } }; -h2.z = new h2(); -h2 = __decorate([ +h.z = new h(); +h = __decorate([ x(() => 0), y(() => 1) -], h2); -var h_default = h2; +], h); +var h_default = h; // /i.ts class i_class { @@ -484,17 +484,17 @@ __decorate([ x(() => 0), y(() => 1) ], i_class.prototype, "foo", 2); -let i2 = i_class; +let i = i_class; // /j.ts -class j2 { +class j { foo() { } } __decorate([ x(() => 0), y(() => 1) -], j2.prototype, "foo", 1); +], j.prototype, "foo", 1); // /k.ts class k_default { @@ -507,4 +507,4 @@ __decorate([ ], k_default.prototype, "foo", 1); // /entry.js -console.log(all_default, all_computed_default, a, b, c, d, e_default2, f_default, g_default, h_default, i2, j2, k_default); +console.log(all_default, all_computed_default, a, b, c, d, e_default2, f_default, g_default2, h_default, i, j, k_default); diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 327ed0fbad..0abb3f242f 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -13,6 +13,7 @@ import ( "github.com/evanw/esbuild/internal/config" "github.com/evanw/esbuild/internal/lexer" "github.com/evanw/esbuild/internal/logging" + "github.com/evanw/esbuild/internal/renamer" "github.com/evanw/esbuild/internal/runtime" ) @@ -43,6 +44,7 @@ type parser struct { allocatedNames []string latestArrowArgLoc ast.Loc currentScope *ast.Scope + scopesForCurrentPart []*ast.Scope symbols []ast.Symbol tsUseCounts []uint32 exportsRef ast.Ref @@ -479,9 +481,9 @@ func (p *parser) discardScopesUpTo(scopeIndex int) { func (p *parser) newSymbol(kind ast.SymbolKind, name string) ast.Ref { ref := ast.Ref{OuterIndex: p.source.Index, InnerIndex: uint32(len(p.symbols))} p.symbols = append(p.symbols, ast.Symbol{ - Kind: kind, - Name: name, - Link: ast.InvalidRef, + Kind: kind, + OriginalName: name, + Link: ast.InvalidRef, }) if p.TS.Parse { p.tsUseCounts = append(p.tsUseCounts, 0) @@ -5128,6 +5130,7 @@ func (p *parser) pushScopeForVisitPass(kind ast.ScopeKind, loc ast.Loc) { p.scopesInOrder = p.scopesInOrder[1:] p.currentScope = order.scope + p.scopesForCurrentPart = append(p.scopesForCurrentPart, order.scope) } type findSymbolResult struct { @@ -5176,7 +5179,7 @@ func (p *parser) findSymbol(name string) findSymbolResult { func (p *parser) findLabelSymbol(loc ast.Loc, name string) ast.Ref { for s := p.currentScope; s != nil && !s.Kind.StopsHoisting(); s = s.Parent { - if s.Kind == ast.ScopeLabel && name == p.symbols[s.LabelRef.InnerIndex].Name { + if s.Kind == ast.ScopeLabel && name == p.symbols[s.LabelRef.InnerIndex].OriginalName { // Track how many times we've referenced this symbol p.recordUsage(s.LabelRef) return s.LabelRef @@ -5851,7 +5854,7 @@ func (p *parser) visitAndAppendStmt(stmts []ast.Stmt, stmt ast.Stmt) []ast.Stmt if p.TS.Parse { if id, ok := (*s.Value.Expr).Data.(*ast.EIdentifier); ok { symbol := p.symbols[id.Ref.InnerIndex] - if symbol.Kind == ast.SymbolUnbound && p.localTypeNames[symbol.Name] { + if symbol.Kind == ast.SymbolUnbound && p.localTypeNames[symbol.OriginalName] { return stmts } } @@ -5902,7 +5905,7 @@ func (p *parser) visitAndAppendStmt(stmts []ast.Stmt, stmt ast.Stmt) []ast.Stmt case *ast.SLabel: p.pushScopeForVisitPass(ast.ScopeLabel, stmt.Loc) name := p.loadNameFromRef(s.Name.Ref) - ref := p.newSymbol(ast.SymbolOther, name) + ref := p.newSymbol(ast.SymbolLabel, name) s.Name.Ref = ref p.currentScope.LabelRef = ref s.Stmt = p.visitSingleStmt(s.Stmt) @@ -5943,7 +5946,7 @@ func (p *parser) visitAndAppendStmt(stmts []ast.Stmt, stmt ast.Stmt) []ast.Stmt p.recordUsage(*p.enclosingNamespaceRef) return ast.Expr{Loc: loc, Data: &ast.EDot{ Target: ast.Expr{Loc: loc, Data: &ast.EIdentifier{Ref: *p.enclosingNamespaceRef}}, - Name: p.symbols[ref.InnerIndex].Name, + Name: p.symbols[ref.InnerIndex].OriginalName, NameLoc: loc, }} } @@ -6152,7 +6155,7 @@ func (p *parser) visitAndAppendStmt(stmts []ast.Stmt, stmt ast.Stmt) []ast.Stmt stmts = append(stmts, stmt, ast.AssignStmt( ast.Expr{Loc: stmt.Loc, Data: &ast.EDot{ Target: ast.Expr{Loc: stmt.Loc, Data: &ast.EIdentifier{Ref: *p.enclosingNamespaceRef}}, - Name: p.symbols[s.Fn.Name.Ref.InnerIndex].Name, + Name: p.symbols[s.Fn.Name.Ref.InnerIndex].OriginalName, NameLoc: s.Fn.Name.Loc, }}, ast.Expr{Loc: s.Fn.Name.Loc, Data: &ast.EIdentifier{Ref: s.Fn.Name.Ref}}, @@ -6161,7 +6164,6 @@ func (p *parser) visitAndAppendStmt(stmts []ast.Stmt, stmt ast.Stmt) []ast.Stmt } case *ast.SClass: - p.recordDeclaredSymbol(s.Class.Name.Ref) p.visitClass(&s.Class) // Remove the export flag inside a namespace @@ -6179,7 +6181,7 @@ func (p *parser) visitAndAppendStmt(stmts []ast.Stmt, stmt ast.Stmt) []ast.Stmt stmts = append(stmts, ast.AssignStmt( ast.Expr{Loc: stmt.Loc, Data: &ast.EDot{ Target: ast.Expr{Loc: stmt.Loc, Data: &ast.EIdentifier{Ref: *p.enclosingNamespaceRef}}, - Name: p.symbols[s.Class.Name.Ref.InnerIndex].Name, + Name: p.symbols[s.Class.Name.Ref.InnerIndex].OriginalName, NameLoc: s.Class.Name.Loc, }}, ast.Expr{Loc: s.Class.Name.Loc, Data: &ast.EIdentifier{Ref: s.Class.Name.Ref}}, @@ -6193,6 +6195,7 @@ func (p *parser) visitAndAppendStmt(stmts []ast.Stmt, stmt ast.Stmt) []ast.Stmt p.recordDeclaredSymbol(s.Name.Ref) p.pushScopeForVisitPass(ast.ScopeEntry, stmt.Loc) defer p.popScope() + p.recordDeclaredSymbol(s.Arg) // Scan ahead for any variables inside this namespace. This must be done // ahead of time before visiting any statements inside the namespace @@ -6327,6 +6330,7 @@ func (p *parser) visitAndAppendStmt(stmts []ast.Stmt, stmt ast.Stmt) []ast.Stmt oldEnclosingNamespaceRef := p.enclosingNamespaceRef p.enclosingNamespaceRef = &s.Arg p.pushScopeForVisitPass(ast.ScopeEntry, stmt.Loc) + p.recordDeclaredSymbol(s.Arg) stmtsInsideNamespace := p.visitStmtsAndPrependTempRefs(s.Stmts) p.popScope() p.enclosingNamespaceRef = oldEnclosingNamespaceRef @@ -6515,6 +6519,10 @@ func (p *parser) visitTSDecorators(tsDecorators []ast.Expr) []ast.Expr { func (p *parser) visitClass(class *ast.Class) { class.TSDecorators = p.visitTSDecorators(class.TSDecorators) + if class.Name != nil { + p.recordDeclaredSymbol(class.Name.Ref) + } + if class.Extends != nil { *class.Extends = p.visitExpr(*class.Extends) } @@ -7400,7 +7408,7 @@ func (p *parser) visitExprInOut(expr ast.Expr, in exprIn) (ast.Expr, exprOut) { p.log.AddRangeError(&p.source, r, fmt.Sprintf("Cannot assign to import %q", lexer.UTF16ToString(str.Value))) } else { r := lexer.RangeOfIdentifier(p.source, e.Target.Loc) - p.log.AddRangeError(&p.source, r, fmt.Sprintf("Cannot assign to property on import %q", p.symbols[id.Ref.InnerIndex].Name)) + p.log.AddRangeError(&p.source, r, fmt.Sprintf("Cannot assign to property on import %q", p.symbols[id.Ref.InnerIndex].OriginalName)) } } } @@ -7678,7 +7686,7 @@ func (p *parser) visitExprInOut(expr ast.Expr, in exprIn) (ast.Expr, exprOut) { // but that doesn't mean it should become a direct eval. if wasIdentifierBeforeVisit { if id, ok := e.Target.Data.(*ast.EIdentifier); ok { - if symbol := p.symbols[id.Ref.InnerIndex]; symbol.Name == "eval" { + if symbol := p.symbols[id.Ref.InnerIndex]; symbol.OriginalName == "eval" { e.IsDirectEval = true // Mark this scope and all parent scopes as containing a direct eval. @@ -7842,7 +7850,7 @@ func (p *parser) handleIdentifier(loc ast.Loc, assignTarget ast.AssignTarget, e if p.symbols[ref.InnerIndex].Kind == ast.SymbolImport { // Create an error for assigning to an import namespace r := lexer.RangeOfIdentifier(p.source, loc) - p.log.AddRangeError(&p.source, r, fmt.Sprintf("Cannot assign to import %q", p.symbols[ref.InnerIndex].Name)) + p.log.AddRangeError(&p.source, r, fmt.Sprintf("Cannot assign to import %q", p.symbols[ref.InnerIndex].OriginalName)) } else { // Remember that this part assigns to this symbol for code splitting use := p.symbolUses[ref] @@ -7859,7 +7867,7 @@ func (p *parser) handleIdentifier(loc ast.Loc, assignTarget ast.AssignTarget, e // Substitute a namespace export reference now if appropriate if p.TS.Parse { if nsRef, ok := p.isExportedInsideNamespace[ref]; ok { - name := p.symbols[ref.InnerIndex].Name + name := p.symbols[ref.InnerIndex].OriginalName // If this is a known enum value, inline the value of the enum if enumValueMap, ok := p.knownEnumValues[nsRef]; ok { @@ -8035,8 +8043,17 @@ func (p *parser) scanForImportsAndExports(stmts []ast.Stmt, isBundling bool) []a if items == nil { items = &[]ast.ClauseItem{} } - for alias, name := range importItems { - originalName := p.symbols[name.Ref.InnerIndex].Name + + // Sort keys for determinism + sorted := make([]string, 0, len(importItems)) + for alias := range importItems { + sorted = append(sorted, alias) + } + sort.Strings(sorted) + + for _, alias := range sorted { + name := importItems[alias] + originalName := p.symbols[name.Ref.InnerIndex].OriginalName *items = append(*items, ast.ClauseItem{ Alias: alias, AliasLoc: name.Loc, @@ -8130,7 +8147,7 @@ func (p *parser) scanForImportsAndExports(stmts []ast.Stmt, isBundling bool) []a // exported alias. This is somewhat confusing because each // SExportFrom statement is basically SImport + SExportClause in one. p.namedImports[item.Name.Ref] = ast.NamedImport{ - Alias: p.symbols[item.Name.Ref.InnerIndex].Name, + Alias: p.symbols[item.Name.Ref.InnerIndex].OriginalName, AliasLoc: item.Name.Loc, NamespaceRef: s.NamespaceRef, ImportRecordIndex: s.ImportRecordIndex, @@ -8176,6 +8193,7 @@ func (p *parser) appendPart(parts []ast.Part, stmts []ast.Stmt) []ast.Part { p.symbolUses = make(map[ast.Ref]ast.SymbolUse) p.declaredSymbols = nil p.importRecordsForCurrentPart = nil + p.scopesForCurrentPart = nil part := ast.Part{ Stmts: p.visitStmtsAndPrependTempRefs(stmts), SymbolUses: p.symbolUses, @@ -8184,6 +8202,7 @@ func (p *parser) appendPart(parts []ast.Part, stmts []ast.Stmt) []ast.Part { part.CanBeRemovedIfUnused = p.stmtsCanBeRemovedIfUnused(part.Stmts) part.DeclaredSymbols = p.declaredSymbols part.ImportRecordIndices = p.importRecordsForCurrentPart + part.Scopes = p.scopesForCurrentPart parts = append(parts, part) } return parts @@ -8937,6 +8956,17 @@ func (p *parser) toAST(source logging.Source, parts []ast.Part, hashbang string, // Make a wrapper symbol in case we need to be wrapped in a closure wrapperRef := p.newSymbol(ast.SymbolOther, "require_"+p.source.IdentifierName) + // Assign slots to symbols in nested scopes. This is some precomputation for + // the symbol renaming pass that will happen later in the linker. It's done + // now in the parser because we want it to be done in parallel per file and + // we're already executing code in a dedicated goroutine for this file. + var nestedScopeSlotCounts ast.SlotCounts + if p.MinifyIdentifiers { + for _, child := range p.moduleScope.Children { + nestedScopeSlotCounts.UnionMax(renamer.AssignNestedScopeSlots(child, p.symbols, ast.SlotCounts{})) + } + } + return ast.AST{ Parts: parts, ModuleScope: p.moduleScope, @@ -8948,6 +8978,7 @@ func (p *parser) toAST(source logging.Source, parts []ast.Part, hashbang string, Directive: directive, NamedImports: p.namedImports, NamedExports: p.namedExports, + NestedScopeSlotCounts: nestedScopeSlotCounts, TopLevelSymbolToParts: p.topLevelSymbolToParts, ExportStarImportRecords: p.exportStarImportRecords, ImportRecords: p.importRecords, diff --git a/internal/parser/parser_json_test.go b/internal/parser/parser_json_test.go index e565692803..11b98e40be 100644 --- a/internal/parser/parser_json_test.go +++ b/internal/parser/parser_json_test.go @@ -39,7 +39,7 @@ func expectPrintedJSON(t *testing.T, contents string, expected string) { if !ok { t.Fatal("Parse error") } - js := printer.PrintExpr(expr, ast.SymbolMap{}, printer.PrintOptions{ + js := printer.PrintExpr(expr, ast.SymbolMap{}, nil, printer.PrintOptions{ RemoveWhitespace: true, }).JS test.AssertEqual(t, string(js), expected) @@ -59,7 +59,7 @@ func expectPrintedJSONWithWarning(t *testing.T, contents string, warning string, if !ok { t.Fatal("Parse error") } - js := printer.PrintExpr(expr, ast.SymbolMap{}, printer.PrintOptions{ + js := printer.PrintExpr(expr, ast.SymbolMap{}, nil, printer.PrintOptions{ RemoveWhitespace: true, }).JS test.AssertEqual(t, string(js), expected) diff --git a/internal/parser/parser_lower.go b/internal/parser/parser_lower.go index bf2e84319d..576d977653 100644 --- a/internal/parser/parser_lower.go +++ b/internal/parser/parser_lower.go @@ -1532,7 +1532,7 @@ func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) var expr ast.Expr if mustLowerPrivate { // Generate a new symbol for this private field - ref := p.generateTempRef(tempRefNeedsDeclare, "_"+p.symbols[private.Ref.InnerIndex].Name[1:]) + ref := p.generateTempRef(tempRefNeedsDeclare, "_"+p.symbols[private.Ref.InnerIndex].OriginalName[1:]) p.symbols[private.Ref.InnerIndex].Link = ref // Initialize the private field to a new WeakMap @@ -1608,7 +1608,7 @@ func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) // Don't generate a symbol for a getter/setter pair twice if p.symbols[private.Ref.InnerIndex].Link == ast.InvalidRef { // Generate a new symbol for this private method - ref := p.generateTempRef(tempRefNeedsDeclare, "_"+p.symbols[private.Ref.InnerIndex].Name[1:]) + ref := p.generateTempRef(tempRefNeedsDeclare, "_"+p.symbols[private.Ref.InnerIndex].OriginalName[1:]) p.symbols[private.Ref.InnerIndex].Link = ref // Initialize the private method to a new WeakSet @@ -1676,7 +1676,7 @@ func (p *parser) lowerClass(stmt ast.Stmt, expr ast.Expr) ([]ast.Stmt, ast.Expr) parameterFields = append(parameterFields, ast.AssignStmt( ast.Expr{Loc: arg.Binding.Loc, Data: &ast.EDot{ Target: ast.Expr{Loc: arg.Binding.Loc, Data: &ast.EThis{}}, - Name: p.symbols[id.Ref.InnerIndex].Name, + Name: p.symbols[id.Ref.InnerIndex].OriginalName, NameLoc: arg.Binding.Loc, }}, ast.Expr{Loc: arg.Binding.Loc, Data: &ast.EIdentifier{Ref: id.Ref}}, diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index 1b985bb628..a778c32ecc 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -10,6 +10,7 @@ import ( "github.com/evanw/esbuild/internal/lexer" "github.com/evanw/esbuild/internal/logging" "github.com/evanw/esbuild/internal/printer" + "github.com/evanw/esbuild/internal/renamer" "github.com/evanw/esbuild/internal/test" ) @@ -60,7 +61,8 @@ func expectPrinted(t *testing.T, contents string, expected string) { } symbols := ast.NewSymbolMap(1) symbols.Outer[0] = tree.Symbols - js := printer.Print(tree, symbols, printer.PrintOptions{}).JS + r := renamer.NewNoOpRenamer(symbols) + js := printer.Print(tree, symbols, r, printer.PrintOptions{}).JS test.AssertEqual(t, string(js), expected) }) } @@ -82,7 +84,8 @@ func expectPrintedMangle(t *testing.T, contents string, expected string) { } symbols := ast.NewSymbolMap(1) symbols.Outer[0] = tree.Symbols - js := printer.Print(tree, symbols, printer.PrintOptions{}).JS + r := renamer.NewNoOpRenamer(symbols) + js := printer.Print(tree, symbols, r, printer.PrintOptions{}).JS test.AssertEqual(t, string(js), expected) }) } @@ -109,7 +112,8 @@ func expectPrintedTarget(t *testing.T, esVersion int, contents string, expected } symbols := ast.NewSymbolMap(1) symbols.Outer[0] = tree.Symbols - js := printer.Print(tree, symbols, printer.PrintOptions{ + r := renamer.NewNoOpRenamer(symbols) + js := printer.Print(tree, symbols, r, printer.PrintOptions{ UnsupportedFeatures: unsupportedFeatures, }).JS test.AssertEqual(t, string(js), expected) @@ -141,7 +145,8 @@ func expectPrintedTargetStrict(t *testing.T, esVersion int, contents string, exp } symbols := ast.NewSymbolMap(1) symbols.Outer[0] = tree.Symbols - js := printer.Print(tree, symbols, printer.PrintOptions{}).JS + r := renamer.NewNoOpRenamer(symbols) + js := printer.Print(tree, symbols, r, printer.PrintOptions{}).JS test.AssertEqual(t, string(js), expected) }) } @@ -182,7 +187,8 @@ func expectPrintedJSX(t *testing.T, contents string, expected string) { } symbols := ast.NewSymbolMap(1) symbols.Outer[0] = tree.Symbols - js := printer.Print(tree, symbols, printer.PrintOptions{}).JS + r := renamer.NewNoOpRenamer(symbols) + js := printer.Print(tree, symbols, r, printer.PrintOptions{}).JS test.AssertEqual(t, string(js), expected) }) } diff --git a/internal/parser/parser_ts.go b/internal/parser/parser_ts.go index f6d5a9ce32..ad298e00b7 100644 --- a/internal/parser/parser_ts.go +++ b/internal/parser/parser_ts.go @@ -887,7 +887,7 @@ func (p *parser) generateClosureForTypeScriptNamespaceOrEnum( var argExpr ast.Expr if isExport && p.enclosingNamespaceRef != nil { // "name = enclosing.name || (enclosing.name = {})" - name := p.symbols[nameRef.InnerIndex].Name + name := p.symbols[nameRef.InnerIndex].OriginalName argExpr = ast.Assign( ast.Expr{Loc: nameLoc, Data: &ast.EIdentifier{Ref: nameRef}}, ast.Expr{Loc: nameLoc, Data: &ast.EBinary{ @@ -932,7 +932,6 @@ func (p *parser) generateClosureForTypeScriptNamespaceOrEnum( }}}, Args: []ast.Expr{argExpr}, }}}}) - p.recordDeclaredSymbol(argRef) return stmts } diff --git a/internal/parser/parser_ts_test.go b/internal/parser/parser_ts_test.go index 6e7aca5bea..8060e3a26a 100644 --- a/internal/parser/parser_ts_test.go +++ b/internal/parser/parser_ts_test.go @@ -7,6 +7,7 @@ import ( "github.com/evanw/esbuild/internal/config" "github.com/evanw/esbuild/internal/logging" "github.com/evanw/esbuild/internal/printer" + "github.com/evanw/esbuild/internal/renamer" "github.com/evanw/esbuild/internal/test" ) @@ -46,7 +47,8 @@ func expectPrintedTS(t *testing.T, contents string, expected string) { } symbols := ast.NewSymbolMap(1) symbols.Outer[0] = tree.Symbols - js := printer.Print(tree, symbols, printer.PrintOptions{}).JS + r := renamer.NewNoOpRenamer(symbols) + js := printer.Print(tree, symbols, r, printer.PrintOptions{}).JS test.AssertEqual(t, string(js), expected) }) } @@ -93,7 +95,8 @@ func expectPrintedTSX(t *testing.T, contents string, expected string) { } symbols := ast.NewSymbolMap(1) symbols.Outer[0] = tree.Symbols - js := printer.Print(tree, symbols, printer.PrintOptions{}).JS + r := renamer.NewNoOpRenamer(symbols) + js := printer.Print(tree, symbols, r, printer.PrintOptions{}).JS test.AssertEqual(t, string(js), expected) }) } diff --git a/internal/printer/printer.go b/internal/printer/printer.go index 774890ec59..7df9c2245d 100644 --- a/internal/printer/printer.go +++ b/internal/printer/printer.go @@ -13,6 +13,7 @@ import ( "github.com/evanw/esbuild/internal/config" "github.com/evanw/esbuild/internal/lexer" "github.com/evanw/esbuild/internal/logging" + "github.com/evanw/esbuild/internal/renamer" "github.com/evanw/esbuild/internal/sourcemap" ) @@ -395,6 +396,7 @@ func (p *printer) printQuotedUTF16(text []uint16, quote rune) { type printer struct { symbols ast.SymbolMap + renamer renamer.Renamer importRecords []ast.ImportRecord options PrintOptions extractedComments map[string]bool @@ -687,16 +689,9 @@ func (p *printer) printIndent() { } } -func (p *printer) symbolName(ref ast.Ref) string { - ref = ast.FollowSymbols(p.symbols, ref) - return p.symbols.Get(ref).Name -} - func (p *printer) printSymbol(ref ast.Ref) { - ref = ast.FollowSymbols(p.symbols, ref) - symbol := p.symbols.Get(ref) p.printSpaceBeforeIdentifier() - p.print(symbol.Name) + p.print(p.renamer.NameForSymbol(ref)) } func (p *printer) printBinding(binding ast.Binding) { @@ -797,7 +792,7 @@ func (p *printer) printBinding(binding ast.Binding) { p.printUTF16(str.Value) // Use a shorthand property if the names are the same - if id, ok := property.Value.Data.(*ast.BIdentifier); ok && lexer.UTF16EqualsString(str.Value, p.symbolName(id.Ref)) { + if id, ok := property.Value.Data.(*ast.BIdentifier); ok && lexer.UTF16EqualsString(str.Value, p.renamer.NameForSymbol(id.Ref)) { if property.DefaultValue != nil { p.printSpace() p.print("=") @@ -1044,7 +1039,7 @@ func (p *printer) printProperty(item ast.Property) { if !p.options.UnsupportedFeatures.Has(compat.ObjectExtensions) && item.Value != nil { switch e := item.Value.Data.(type) { case *ast.EIdentifier: - if lexer.UTF16EqualsString(key.Value, p.symbolName(e.Ref)) { + if lexer.UTF16EqualsString(key.Value, p.renamer.NameForSymbol(e.Ref)) { if item.Initializer != nil { p.printSpace() p.print("=") @@ -1058,7 +1053,7 @@ func (p *printer) printProperty(item ast.Property) { // Make sure we're not using a property access instead of an identifier ref := ast.FollowSymbols(p.symbols, e.Ref) symbol := p.symbols.Get(ref) - if symbol.NamespaceAlias == nil && lexer.UTF16EqualsString(key.Value, symbol.Name) { + if symbol.NamespaceAlias == nil && lexer.UTF16EqualsString(key.Value, p.renamer.NameForSymbol(e.Ref)) { if item.Initializer != nil { p.printSpace() p.print("=") @@ -1770,7 +1765,7 @@ func (p *printer) printExpr(expr ast.Expr, level ast.L, flags int) { p.print(symbol.NamespaceAlias.Alias) } else { p.printSpaceBeforeIdentifier() - p.print(symbol.Name) + p.print(p.renamer.NameForSymbol(e.Ref)) } case *ast.EAwait: @@ -1924,8 +1919,9 @@ func (p *printer) printExpr(expr ast.Expr, level ast.L, flags int) { func (p *printer) isUnboundEvalIdentifier(value ast.Expr) bool { if id, ok := value.Data.(*ast.EIdentifier); ok { + // Using the original name here is ok since unbound symbols are not renamed symbol := p.symbols.Get(ast.FollowSymbols(p.symbols, id.Ref)) - return symbol.Kind == ast.SymbolUnbound && symbol.Name == "eval" + return symbol.Kind == ast.SymbolUnbound && symbol.OriginalName == "eval" } return false } @@ -2428,7 +2424,7 @@ func (p *printer) printStmt(stmt ast.Stmt) { p.printNewline() p.printIndent() } - name := p.symbolName(item.Name.Ref) + name := p.renamer.NameForSymbol(item.Name.Ref) p.print(name) if name != item.Alias { p.print(" as ") @@ -2722,7 +2718,7 @@ func (p *printer) printStmt(stmt ast.Stmt) { p.printIndent() } p.print(item.Alias) - name := p.symbolName(item.Name.Ref) + name := p.renamer.NameForSymbol(item.Name.Ref) if name != item.Alias { p.print(" as ") p.print(name) @@ -2888,12 +2884,14 @@ type SourceMapChunk struct { func createPrinter( symbols ast.SymbolMap, + r renamer.Renamer, importRecords []ast.ImportRecord, options PrintOptions, approximateLineCount int32, ) *printer { p := &printer{ symbols: symbols, + renamer: r, importRecords: importRecords, options: options, stmtStart: -1, @@ -2938,8 +2936,8 @@ type PrintResult struct { ExtractedComments map[string]bool } -func Print(tree ast.AST, symbols ast.SymbolMap, options PrintOptions) PrintResult { - p := createPrinter(symbols, tree.ImportRecords, options, tree.ApproximateLineCount) +func Print(tree ast.AST, symbols ast.SymbolMap, r renamer.Renamer, options PrintOptions) PrintResult { + p := createPrinter(symbols, r, tree.ImportRecords, options, tree.ApproximateLineCount) for _, part := range tree.Parts { for _, stmt := range part.Stmts { @@ -2963,8 +2961,8 @@ func Print(tree ast.AST, symbols ast.SymbolMap, options PrintOptions) PrintResul } } -func PrintExpr(expr ast.Expr, symbols ast.SymbolMap, options PrintOptions) PrintResult { - p := createPrinter(symbols, nil, options, 0) +func PrintExpr(expr ast.Expr, symbols ast.SymbolMap, r renamer.Renamer, options PrintOptions) PrintResult { + p := createPrinter(symbols, r, nil, options, 0) p.printExpr(expr, ast.LLowest, 0) diff --git a/internal/printer/printer_test.go b/internal/printer/printer_test.go index a2bf6465ed..9b7625f2f7 100644 --- a/internal/printer/printer_test.go +++ b/internal/printer/printer_test.go @@ -3,6 +3,8 @@ package printer import ( "testing" + "github.com/evanw/esbuild/internal/renamer" + "github.com/evanw/esbuild/internal/ast" "github.com/evanw/esbuild/internal/compat" "github.com/evanw/esbuild/internal/config" @@ -32,7 +34,8 @@ func expectPrintedCommon(t *testing.T, name string, contents string, expected st } symbols := ast.NewSymbolMap(1) symbols.Outer[0] = tree.Symbols - js := Print(tree, symbols, options).JS + r := renamer.NewNoOpRenamer(symbols) + js := Print(tree, symbols, r, options).JS assertEqual(t, string(js), expected) }) } diff --git a/internal/renamer/renamer.go b/internal/renamer/renamer.go new file mode 100644 index 0000000000..659d64a8b5 --- /dev/null +++ b/internal/renamer/renamer.go @@ -0,0 +1,485 @@ +package renamer + +import ( + "sort" + "strconv" + "sync" + + "github.com/evanw/esbuild/internal/ast" + "github.com/evanw/esbuild/internal/lexer" +) + +func ComputeReservedNames(moduleScopes []*ast.Scope, symbols ast.SymbolMap) map[string]uint32 { + names := make(map[string]uint32) + + // All keywords are reserved names + for k := range lexer.Keywords() { + names[k] = 1 + } + + // All unbound symbols must be reserved names + for _, scope := range moduleScopes { + for _, ref := range scope.Members { + symbol := symbols.Get(ref) + if symbol.Kind == ast.SymbolUnbound || symbol.MustNotBeRenamed { + names[symbol.OriginalName] = 1 + } + } + for _, ref := range scope.Generated { + symbol := symbols.Get(ref) + if symbol.Kind == ast.SymbolUnbound || symbol.MustNotBeRenamed { + names[symbol.OriginalName] = 1 + } + } + } + + return names +} + +type Renamer interface { + NameForSymbol(ref ast.Ref) string +} + +//////////////////////////////////////////////////////////////////////////////// +// noOpRenamer + +type noOpRenamer struct { + symbols ast.SymbolMap +} + +func NewNoOpRenamer(symbols ast.SymbolMap) Renamer { + return &noOpRenamer{ + symbols: symbols, + } +} + +func (r *noOpRenamer) NameForSymbol(ref ast.Ref) string { + ref = ast.FollowSymbols(r.symbols, ref) + return r.symbols.Get(ref).OriginalName +} + +//////////////////////////////////////////////////////////////////////////////// +// MinifyRenamer + +type symbolSlot struct { + name string + count uint32 +} + +type MinifyRenamer struct { + symbols ast.SymbolMap + sortedBuffer StableRefArray + reservedNames map[string]uint32 + slots [3][]symbolSlot + symbolToSlot map[ast.Ref]uint32 +} + +func NewMinifyRenamer(symbols ast.SymbolMap, firstTopLevelSlots ast.SlotCounts, reservedNames map[string]uint32) *MinifyRenamer { + return &MinifyRenamer{ + symbols: symbols, + reservedNames: reservedNames, + slots: [3][]symbolSlot{ + make([]symbolSlot, firstTopLevelSlots[0]), + make([]symbolSlot, firstTopLevelSlots[1]), + make([]symbolSlot, firstTopLevelSlots[2]), + }, + symbolToSlot: make(map[ast.Ref]uint32), + } +} + +func (r *MinifyRenamer) NameForSymbol(ref ast.Ref) string { + // Follow links to get to the underlying symbol + ref = ast.FollowSymbols(r.symbols, ref) + symbol := r.symbols.Get(ref) + + // Skip this symbol if the name is pinned + ns := symbol.SlotNamespace() + if ns == ast.SlotMustNotBeRenamed { + return symbol.OriginalName + } + + // Check if it's a nested scope symbol + i := ^symbol.NestedScopeSlot + + // If it's not (i.e. it's in a top-level scope), look up the slot + if i == ^uint32(0) { + var ok bool + i, ok = r.symbolToSlot[ref] + if !ok { + // If we get here, then we're printing a symbol that never had any + // recorded uses. This is odd but can happen in certain scenarios. + // For example, code in a branch with dead control flow won't mark + // any uses but may still be printed. In that case it doesn't matter + // what name we use since it's dead code. + return symbol.OriginalName + } + } + + return r.slots[ns][i].name +} + +func (r *MinifyRenamer) AccumulateSymbolUseCounts(symbolUses map[ast.Ref]ast.SymbolUse, stableSourceIndices []uint32) { + // Sort symbol uses for determinism, reusing a shared memory buffer + r.sortedBuffer = r.sortedBuffer[:0] + for ref := range symbolUses { + r.sortedBuffer = append(r.sortedBuffer, StableRef{ + StableOuterIndex: stableSourceIndices[ref.OuterIndex], + Ref: ref, + }) + } + sort.Sort(r.sortedBuffer) + + // Accumulate symbol use counts + for _, stable := range r.sortedBuffer { + r.AccumulateSymbolCount(stable.Ref, symbolUses[stable.Ref].CountEstimate) + } +} + +func (r *MinifyRenamer) AccumulateSymbolCount(ref ast.Ref, count uint32) { + // Follow links to get to the underlying symbol + ref = ast.FollowSymbols(r.symbols, ref) + symbol := r.symbols.Get(ref) + for symbol.NamespaceAlias != nil { + ref = ast.FollowSymbols(r.symbols, symbol.NamespaceAlias.NamespaceRef) + symbol = r.symbols.Get(ref) + } + + // Skip this symbol if the name is pinned + ns := symbol.SlotNamespace() + if ns == ast.SlotMustNotBeRenamed { + return + } + + // Check if it's a nested scope symbol + slots := &r.slots[ns] + i := ^symbol.NestedScopeSlot + + // If it's not (i.e. it's in a top-level scope), allocate a slot for it + if i == ^uint32(0) { + var ok bool + i, ok = r.symbolToSlot[ref] + if !ok { + i = uint32(len(*slots)) + *slots = append(*slots, symbolSlot{}) + r.symbolToSlot[ref] = i + } + } + + (*slots)[i].count += count +} + +func (r *MinifyRenamer) AssignNamesByFrequency() { + for ns, slots := range r.slots { + // Sort symbols by count + sorted := make(slotAndCountArray, len(slots)) + for i, item := range slots { + sorted[i] = slotAndCount{slot: uint32(i), count: item.count} + } + sort.Sort(sorted) + + // Assign names to symbols + nextName := 0 + for _, data := range sorted { + name := lexer.NumberToMinifiedName(nextName) + nextName++ + + // Make sure we never generate a reserved name. We only have to worry + // about this for normal symbols, not for labels or private names. + if ast.SlotNamespace(ns) == ast.SlotDefault { + for r.reservedNames[name] != 0 { + name = lexer.NumberToMinifiedName(nextName) + nextName++ + } + } + + // Private names must be prefixed with "#" + if ast.SlotNamespace(ns) == ast.SlotPrivateName { + name = "#" + name + } + + slots[data.slot].name = name + } + } +} + +// Returns the number of nested slots +func AssignNestedScopeSlots(scope *ast.Scope, symbols []ast.Symbol, slot ast.SlotCounts) ast.SlotCounts { + // Sort member map keys for determinism + sortedMembers := make([]int, 0, len(scope.Members)) + for _, ref := range scope.Members { + sortedMembers = append(sortedMembers, int(ref.InnerIndex)) + } + sort.Ints(sortedMembers) + + // Assign slots for this scope's symbols. Only do this if the slot is 0 (i.e. + // not already assigned). Nested scopes have copies of symbols from parent + // scopes and we want to use the slot from the parent scope, not child scopes. + for _, innerIndex := range sortedMembers { + symbol := &symbols[innerIndex] + if ns := symbol.SlotNamespace(); ns != ast.SlotMustNotBeRenamed && symbol.NestedScopeSlot == 0 { + symbol.NestedScopeSlot = ^slot[ns] + slot[ns]++ + } + } + for _, ref := range scope.Generated { + symbol := &symbols[ref.InnerIndex] + if ns := symbol.SlotNamespace(); ns != ast.SlotMustNotBeRenamed && symbol.NestedScopeSlot == 0 { + symbol.NestedScopeSlot = ^slot[ns] + slot[ns]++ + } + } + + // Assign slots for the symbols of child scopes + slotCounts := slot + for _, child := range scope.Children { + slotCounts.UnionMax(AssignNestedScopeSlots(child, symbols, slot)) + } + return slotCounts +} + +type slotAndCount struct { + slot uint32 + count uint32 +} + +// This type is just so we can use Go's native sort function +type slotAndCountArray []slotAndCount + +func (a slotAndCountArray) Len() int { return len(a) } +func (a slotAndCountArray) Swap(i int, j int) { a[i], a[j] = a[j], a[i] } +func (a slotAndCountArray) Less(i int, j int) bool { + ai, aj := a[i], a[j] + return ai.count > aj.count || (ai.count == aj.count && ai.slot < aj.slot) +} + +type StableRef struct { + StableOuterIndex uint32 + Ref ast.Ref +} + +// This type is just so we can use Go's native sort function +type StableRefArray []StableRef + +func (a StableRefArray) Len() int { return len(a) } +func (a StableRefArray) Swap(i int, j int) { a[i], a[j] = a[j], a[i] } +func (a StableRefArray) Less(i int, j int) bool { + ai, aj := a[i], a[j] + return ai.StableOuterIndex > aj.StableOuterIndex || (ai.StableOuterIndex == aj.StableOuterIndex && ai.Ref.InnerIndex < aj.Ref.InnerIndex) +} + +//////////////////////////////////////////////////////////////////////////////// +// NumberRenamer + +type NumberRenamer struct { + symbols ast.SymbolMap + names [][]string + root numberScope +} + +func NewNumberRenamer(symbols ast.SymbolMap, reservedNames map[string]uint32) *NumberRenamer { + return &NumberRenamer{ + symbols: symbols, + names: make([][]string, len(symbols.Outer)), + root: numberScope{nameCounts: reservedNames}, + } +} + +func (r *NumberRenamer) NameForSymbol(ref ast.Ref) string { + ref = ast.FollowSymbols(r.symbols, ref) + if inner := r.names[ref.OuterIndex]; inner != nil { + if name := inner[ref.InnerIndex]; name != "" { + return name + } + } + return r.symbols.Get(ref).OriginalName +} + +func (r *NumberRenamer) AddTopLevelSymbol(ref ast.Ref) { + r.assignName(&r.root, ref) +} + +func (r *NumberRenamer) assignName(scope *numberScope, ref ast.Ref) { + ref = ast.FollowSymbols(r.symbols, ref) + + // Don't rename the same symbol more than once + inner := r.names[ref.OuterIndex] + if inner != nil && inner[ref.InnerIndex] != "" { + return + } + + // Don't rename unbound symbols, symbols marked as reserved names, labels, or private names + symbol := r.symbols.Get(ref) + if symbol.SlotNamespace() != ast.SlotDefault { + return + } + + // Compute a new name + name := scope.findUnusedName(symbol.OriginalName) + + // Store the new name + if inner == nil { + // Note: This should not be a data race even though this method is run from + // multiple threads. The parallel part only looks at symbols defined in + // nested scopes, and those can only ever be accessed from within the file. + // References to those symbols should never spread across files. + // + // While we could avoid the data race by densely preallocating the entire + // "names" array ahead of time, that will waste a lot more memory for + // builds that make heavy use of code splitting and have many chunks. Doing + // things lazily like this means we use less memory but still stay safe. + inner = make([]string, len(r.symbols.Outer[ref.OuterIndex])) + r.names[ref.OuterIndex] = inner + } + inner[ref.InnerIndex] = name +} + +func (r *NumberRenamer) assignNamesRecursive(scope *ast.Scope, sourceIndex uint32, parent *numberScope, sorted *[]int) { + s := &numberScope{parent: parent, nameCounts: make(map[string]uint32)} + + // Sort member map keys for determinism, reusing a shared memory buffer + *sorted = (*sorted)[:0] + for _, ref := range scope.Members { + *sorted = append(*sorted, int(ref.InnerIndex)) + } + sort.Ints(*sorted) + + // Rename all symbols in this scope + for _, innerIndex := range *sorted { + r.assignName(s, ast.Ref{OuterIndex: sourceIndex, InnerIndex: uint32(innerIndex)}) + } + for _, ref := range scope.Generated { + r.assignName(s, ref) + } + + // Symbols in child scopes may also have to be renamed to avoid conflicts + for _, child := range scope.Children { + r.assignNamesRecursive(child, sourceIndex, s, sorted) + } +} + +func (r *NumberRenamer) AssignNamesByScope(nestedScopes map[uint32][]*ast.Scope) { + waitGroup := sync.WaitGroup{} + waitGroup.Add(len(nestedScopes)) + + // Rename nested scopes from separate files in parallel + for sourceIndex, scopes := range nestedScopes { + go func(sourceIndex uint32, scopes []*ast.Scope) { + var sorted []int + for _, scope := range scopes { + r.assignNamesRecursive(scope, sourceIndex, &r.root, &sorted) + } + waitGroup.Done() + }(sourceIndex, scopes) + } + + waitGroup.Wait() +} + +type numberScope struct { + parent *numberScope + + // This is used as a set of used names in this scope. This also maps the name + // to the number of times the name has experienced a collision. When a name + // collides with an already-used name, we need to rename it. This is done by + // incrementing a number at the end until the name is unused. We save the + // count here so that subsequent collisions can start counting from where the + // previous collision ended instead of having to start counting from 1. + nameCounts map[string]uint32 +} + +type nameUse uint8 + +const ( + nameUnused nameUse = iota + nameUsed + nameUsedInSameScope +) + +func (s *numberScope) findNameUse(name string) nameUse { + original := s + for { + if _, ok := s.nameCounts[name]; ok { + if s == original { + return nameUsedInSameScope + } + return nameUsed + } + s = s.parent + if s == nil { + return nameUnused + } + } +} + +func (s *numberScope) findUnusedName(name string) string { + if use := s.findNameUse(name); use != nameUnused { + // If the name is already in use, generate a new name by appending a number + tries := uint32(1) + if use == nameUsedInSameScope { + // To avoid O(n^2) behavior, the number must start off being the number + // that we used last time there was a collision with this name. Otherwise + // if there are many collisions with the same name, each name collision + // would have to increment the counter past all previous name collisions + // which is a O(n^2) time algorithm. Only do this if this symbol comes + // from the same scope as the previous one since sibling scopes can reuse + // the same name without problems. + tries = s.nameCounts[name] + } + prefix := name + + // Keep incrementing the number until the name is unused + for { + tries++ + name = prefix + strconv.Itoa(int(tries)) + + // Make sure this new name is unused + if s.findNameUse(name) == nameUnused { + // Store the count so we can start here next time instead of starting + // from 1. This means we avoid O(n^2) behavior. + if use == nameUsedInSameScope { + s.nameCounts[prefix] = tries + } + break + } + } + } + + // Each name starts off with a count of 1 so that the first collision with + // "name" is called "name2" + s.nameCounts[name] = 1 + return name +} + +//////////////////////////////////////////////////////////////////////////////// +// ExportRenamer + +type ExportRenamer struct { + count int + used map[string]uint32 +} + +func (r *ExportRenamer) NextRenamedName(name string) string { + if r.used == nil { + r.used = make(map[string]uint32) + } + if tries, ok := r.used[name]; ok { + prefix := name + for { + tries++ + name = prefix + strconv.Itoa(int(tries)) + if _, ok := r.used[name]; !ok { + break + } + } + r.used[name] = tries + } else { + r.used[name] = 1 + } + return name +} + +func (r *ExportRenamer) NextMinifiedName() string { + name := lexer.NumberToMinifiedName(r.count) + r.count++ + return name +}