package renamer import ( "sort" "strconv" "sync" "github.com/evanw/esbuild/internal/js_ast" "github.com/evanw/esbuild/internal/js_lexer" ) func ComputeReservedNames(moduleScopes []*js_ast.Scope, symbols js_ast.SymbolMap) map[string]uint32 { names := make(map[string]uint32) // All keywords and strict mode reserved words are reserved names for k := range js_lexer.Keywords { names[k] = 1 } for k := range js_lexer.StrictModeReservedWords { names[k] = 1 } // All unbound symbols must be reserved names for _, scope := range moduleScopes { for _, member := range scope.Members { symbol := symbols.Get(member.Ref) if symbol.Kind == js_ast.SymbolUnbound || symbol.MustNotBeRenamed { names[symbol.OriginalName] = 1 } } for _, ref := range scope.Generated { symbol := symbols.Get(ref) if symbol.Kind == js_ast.SymbolUnbound || symbol.MustNotBeRenamed { names[symbol.OriginalName] = 1 } } } return names } type Renamer interface { NameForSymbol(ref js_ast.Ref) string } //////////////////////////////////////////////////////////////////////////////// // noOpRenamer type noOpRenamer struct { symbols js_ast.SymbolMap } func NewNoOpRenamer(symbols js_ast.SymbolMap) Renamer { return &noOpRenamer{ symbols: symbols, } } func (r *noOpRenamer) NameForSymbol(ref js_ast.Ref) string { ref = js_ast.FollowSymbols(r.symbols, ref) return r.symbols.Get(ref).OriginalName } //////////////////////////////////////////////////////////////////////////////// // MinifyRenamer type symbolSlot struct { name string count uint32 } type MinifyRenamer struct { symbols js_ast.SymbolMap sortedBuffer StableRefArray reservedNames map[string]uint32 slots [3][]symbolSlot symbolToSlot map[js_ast.Ref]uint32 } func NewMinifyRenamer(symbols js_ast.SymbolMap, firstTopLevelSlots js_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[js_ast.Ref]uint32), } } func (r *MinifyRenamer) NameForSymbol(ref js_ast.Ref) string { // Follow links to get to the underlying symbol ref = js_ast.FollowSymbols(r.symbols, ref) symbol := r.symbols.Get(ref) // Skip this symbol if the name is pinned ns := symbol.SlotNamespace() if ns == js_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[js_ast.Ref]js_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 js_ast.Ref, count uint32) { // Follow links to get to the underlying symbol ref = js_ast.FollowSymbols(r.symbols, ref) symbol := r.symbols.Get(ref) for symbol.NamespaceAlias != nil { ref = js_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 == js_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(minifier *js_ast.NameMinifier) { 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 := minifier.NumberToMinifiedName(nextName) nextName++ // Make sure we never generate a reserved name. We only have to worry // about collisions with reserved identifiers for normal symbols, and we // only have to worry about collisions with keywords for labels. We do // not have to worry about either for private names because they start // with a "#" character. switch js_ast.SlotNamespace(ns) { case js_ast.SlotDefault: for r.reservedNames[name] != 0 { name = minifier.NumberToMinifiedName(nextName) nextName++ } case js_ast.SlotLabel: for js_lexer.Keywords[name] != 0 { name = minifier.NumberToMinifiedName(nextName) nextName++ } } // Private names must be prefixed with "#" if js_ast.SlotNamespace(ns) == js_ast.SlotPrivateName { name = "#" + name } slots[data.slot].name = name } } } // Returns the number of nested slots func AssignNestedScopeSlots(moduleScope *js_ast.Scope, symbols []js_ast.Symbol) (slotCounts js_ast.SlotCounts) { // Temporarily set the nested scope slots of top-level symbols to non-zero so // they aren't renamed in nested scopes. This prevents us from accidentally // assigning nested scope slots to variables declared using "var" in a nested // scope that are actually hoisted up to the module scope to become a top- // level symbol. for _, member := range moduleScope.Members { symbols[member.Ref.InnerIndex].NestedScopeSlot = 1 } for _, ref := range moduleScope.Generated { symbols[ref.InnerIndex].NestedScopeSlot = 1 } // Assign nested scope slots independently for each nested scope for _, child := range moduleScope.Children { slotCounts.UnionMax(assignNestedScopeSlotsHelper(child, symbols, js_ast.SlotCounts{})) } // Then set the nested scope slots of top-level symbols back to zero. Top- // level symbols are not supposed to have nested scope slots. for _, member := range moduleScope.Members { symbols[member.Ref.InnerIndex].NestedScopeSlot = 0 } for _, ref := range moduleScope.Generated { symbols[ref.InnerIndex].NestedScopeSlot = 0 } return } func assignNestedScopeSlotsHelper(scope *js_ast.Scope, symbols []js_ast.Symbol, slot js_ast.SlotCounts) js_ast.SlotCounts { // Sort member map keys for determinism sortedMembers := make([]int, 0, len(scope.Members)) for _, member := range scope.Members { sortedMembers = append(sortedMembers, int(member.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 != js_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 != js_ast.SlotMustNotBeRenamed && symbol.NestedScopeSlot == 0 { symbol.NestedScopeSlot = ^slot[ns] slot[ns]++ } } // Labels are always declared in a nested scope, so we don't need to check. if scope.LabelRef != js_ast.InvalidRef { symbol := &symbols[scope.LabelRef.InnerIndex] symbol.NestedScopeSlot = ^slot[js_ast.SlotLabel] slot[js_ast.SlotLabel]++ } // Assign slots for the symbols of child scopes slotCounts := slot for _, child := range scope.Children { slotCounts.UnionMax(assignNestedScopeSlotsHelper(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 js_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 js_ast.SymbolMap names [][]string root numberScope } func NewNumberRenamer(symbols js_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 js_ast.Ref) string { ref = js_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 js_ast.Ref) { r.assignName(&r.root, ref) } func (r *NumberRenamer) assignName(scope *numberScope, ref js_ast.Ref) { ref = js_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() != js_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 *js_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 _, member := range scope.Members { *sorted = append(*sorted, int(member.Ref.InnerIndex)) } sort.Ints(*sorted) // Rename all symbols in this scope for _, innerIndex := range *sorted { r.assignName(s, js_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][]*js_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 []*js_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 { name = js_lexer.ForceValidIdentifier(name) 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 := js_ast.DefaultNameMinifier.NumberToMinifiedName(r.count) r.count++ return name }