Files

1861 lines
50 KiB
Go

package js_ast
import (
"strconv"
"github.com/evanw/esbuild/internal/ast"
"github.com/evanw/esbuild/internal/logger"
)
// Every module (i.e. file) is parsed into a separate AST data structure. For
// efficiency, the parser also resolves all scopes and binds all symbols in the
// tree.
//
// Identifiers in the tree are referenced by a Ref, which is a pointer into the
// symbol table for the file. The symbol table is stored as a top-level field
// in the AST so it can be accessed without traversing the tree. For example,
// a renaming pass can iterate over the symbol table without touching the tree.
//
// Parse trees are intended to be immutable. That makes it easy to build an
// incremental compiler with a "watch" mode that can avoid re-parsing files
// that have already been parsed. Any passes that operate on an AST after it
// has been parsed should create a copy of the mutated parts of the tree
// instead of mutating the original tree.
type L uint8
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence
const (
LLowest L = iota
LComma
LSpread
LYield
LAssign
LConditional
LNullishCoalescing
LLogicalOr
LLogicalAnd
LBitwiseOr
LBitwiseXor
LBitwiseAnd
LEquals
LCompare
LShift
LAdd
LMultiply
LExponentiation
LPrefix
LPostfix
LNew
LCall
LMember
)
type OpCode uint8
func (op OpCode) IsPrefix() bool {
return op < UnOpPostDec
}
func (op OpCode) UnaryAssignTarget() AssignTarget {
if op >= UnOpPreDec && op <= UnOpPostInc {
return AssignTargetUpdate
}
return AssignTargetNone
}
func (op OpCode) IsLeftAssociative() bool {
return op >= BinOpAdd && op < BinOpComma && op != BinOpPow
}
func (op OpCode) IsRightAssociative() bool {
return op >= BinOpAssign || op == BinOpPow
}
func (op OpCode) BinaryAssignTarget() AssignTarget {
if op == BinOpAssign {
return AssignTargetReplace
}
if op > BinOpAssign {
return AssignTargetUpdate
}
return AssignTargetNone
}
func (op OpCode) IsShortCircuit() bool {
switch op {
case BinOpLogicalOr, BinOpLogicalOrAssign,
BinOpLogicalAnd, BinOpLogicalAndAssign,
BinOpNullishCoalescing, BinOpNullishCoalescingAssign:
return true
}
return false
}
type AssignTarget uint8
const (
AssignTargetNone AssignTarget = iota
AssignTargetReplace // "a = b"
AssignTargetUpdate // "a += b"
)
// If you add a new token, remember to add it to "OpTable" too
const (
// Prefix
UnOpPos OpCode = iota
UnOpNeg
UnOpCpl
UnOpNot
UnOpVoid
UnOpTypeof
UnOpDelete
// Prefix update
UnOpPreDec
UnOpPreInc
// Postfix update
UnOpPostDec
UnOpPostInc
// Left-associative
BinOpAdd
BinOpSub
BinOpMul
BinOpDiv
BinOpRem
BinOpPow
BinOpLt
BinOpLe
BinOpGt
BinOpGe
BinOpIn
BinOpInstanceof
BinOpShl
BinOpShr
BinOpUShr
BinOpLooseEq
BinOpLooseNe
BinOpStrictEq
BinOpStrictNe
BinOpNullishCoalescing
BinOpLogicalOr
BinOpLogicalAnd
BinOpBitwiseOr
BinOpBitwiseAnd
BinOpBitwiseXor
// Non-associative
BinOpComma
// Right-associative
BinOpAssign
BinOpAddAssign
BinOpSubAssign
BinOpMulAssign
BinOpDivAssign
BinOpRemAssign
BinOpPowAssign
BinOpShlAssign
BinOpShrAssign
BinOpUShrAssign
BinOpBitwiseOrAssign
BinOpBitwiseAndAssign
BinOpBitwiseXorAssign
BinOpNullishCoalescingAssign
BinOpLogicalOrAssign
BinOpLogicalAndAssign
)
type OpTableEntry struct {
Text string
Level L
IsKeyword bool
}
var OpTable = []OpTableEntry{
// Prefix
{"+", LPrefix, false},
{"-", LPrefix, false},
{"~", LPrefix, false},
{"!", LPrefix, false},
{"void", LPrefix, true},
{"typeof", LPrefix, true},
{"delete", LPrefix, true},
// Prefix update
{"--", LPrefix, false},
{"++", LPrefix, false},
// Postfix update
{"--", LPostfix, false},
{"++", LPostfix, false},
// Left-associative
{"+", LAdd, false},
{"-", LAdd, false},
{"*", LMultiply, false},
{"/", LMultiply, false},
{"%", LMultiply, false},
{"**", LExponentiation, false}, // Right-associative
{"<", LCompare, false},
{"<=", LCompare, false},
{">", LCompare, false},
{">=", LCompare, false},
{"in", LCompare, true},
{"instanceof", LCompare, true},
{"<<", LShift, false},
{">>", LShift, false},
{">>>", LShift, false},
{"==", LEquals, false},
{"!=", LEquals, false},
{"===", LEquals, false},
{"!==", LEquals, false},
{"??", LNullishCoalescing, false},
{"||", LLogicalOr, false},
{"&&", LLogicalAnd, false},
{"|", LBitwiseOr, false},
{"&", LBitwiseAnd, false},
{"^", LBitwiseXor, false},
// Non-associative
{",", LComma, false},
// Right-associative
{"=", LAssign, false},
{"+=", LAssign, false},
{"-=", LAssign, false},
{"*=", LAssign, false},
{"/=", LAssign, false},
{"%=", LAssign, false},
{"**=", LAssign, false},
{"<<=", LAssign, false},
{">>=", LAssign, false},
{">>>=", LAssign, false},
{"|=", LAssign, false},
{"&=", LAssign, false},
{"^=", LAssign, false},
{"??=", LAssign, false},
{"||=", LAssign, false},
{"&&=", LAssign, false},
}
type Decorator struct {
Value Expr
AtLoc logger.Loc
OmitNewlineAfter bool
}
type PropertyKind uint8
const (
PropertyField PropertyKind = iota
PropertyMethod
PropertyGetter
PropertySetter
PropertyAutoAccessor
PropertySpread
PropertyDeclareOrAbstract
PropertyClassStaticBlock
)
// This returns true if and only if this property matches the "MethodDefinition"
// grammar from the specification. That means it's one of the following forms:
//
// foo() {}
// *foo() {}
// async foo() {}
// async *foo() {}
// get foo() {}
// set foo(_) {}
//
// If this returns true, the "ValueOrNil" field of the property is always an
// "EFunction" expression and it is always printed as a method.
func (kind PropertyKind) IsMethodDefinition() bool {
return kind == PropertyMethod || kind == PropertyGetter || kind == PropertySetter
}
type ClassStaticBlock struct {
Block SBlock
Loc logger.Loc
}
type PropertyFlags uint8
const (
PropertyIsComputed PropertyFlags = 1 << iota
PropertyIsStatic
PropertyWasShorthand
PropertyPreferQuotedKey
)
func (flags PropertyFlags) Has(flag PropertyFlags) bool {
return (flags & flag) != 0
}
type Property struct {
ClassStaticBlock *ClassStaticBlock
Key Expr
// This is omitted for class fields
ValueOrNil Expr
// This is used when parsing a pattern that uses default values:
//
// [a = 1] = [];
// ({a = 1} = {});
//
// It's also used for class fields:
//
// class Foo { a = 1 }
//
InitializerOrNil Expr
Decorators []Decorator
Loc logger.Loc
CloseBracketLoc logger.Loc
Kind PropertyKind
Flags PropertyFlags
}
type PropertyBinding struct {
Key Expr
Value Binding
DefaultValueOrNil Expr
Loc logger.Loc
CloseBracketLoc logger.Loc
IsComputed bool
IsSpread bool
PreferQuotedKey bool
}
type Arg struct {
Binding Binding
DefaultOrNil Expr
Decorators []Decorator
// "constructor(public x: boolean) {}"
IsTypeScriptCtorField bool
}
type Fn struct {
Name *ast.LocRef
Args []Arg
Body FnBody
ArgumentsRef ast.Ref
OpenParenLoc logger.Loc
IsAsync bool
IsGenerator bool
HasRestArg bool
HasIfScope bool
// See: https://github.com/rollup/rollup/pull/5024
HasNoSideEffectsComment bool
// This is true if the function is a method
IsUniqueFormalParameters bool
}
type FnBody struct {
Block SBlock
Loc logger.Loc
}
type Class struct {
Decorators []Decorator
Name *ast.LocRef
ExtendsOrNil Expr
Properties []Property
ClassKeyword logger.Range
BodyLoc logger.Loc
CloseBraceLoc logger.Loc
// If true, JavaScript decorators (i.e. not TypeScript experimental
// decorators) should be lowered. This is the case either if JavaScript
// decorators are not supported in the configured target environment, or
// if "useDefineForClassFields" is set to false and this class has
// decorators on it. Note that this flag is not necessarily set to true if
// "useDefineForClassFields" is false and a class has an "accessor" even
// though the accessor feature comes from the decorator specification.
ShouldLowerStandardDecorators bool
// If true, property field initializers cannot be assumed to have no side
// effects. For example:
//
// class Foo {
// static set foo(x) { importantSideEffect(x) }
// }
// class Bar extends Foo {
// foo = 1
// }
//
// This happens in TypeScript when "useDefineForClassFields" is disabled
// because TypeScript (and esbuild) transforms the above class into this:
//
// class Foo {
// static set foo(x) { importantSideEffect(x); }
// }
// class Bar extends Foo {
// }
// Bar.foo = 1;
//
UseDefineForClassFields bool
}
type ArrayBinding struct {
Binding Binding
DefaultValueOrNil Expr
Loc logger.Loc
}
type Binding struct {
Data B
Loc logger.Loc
}
// This interface is never called. Its purpose is to encode a variant type in
// Go's type system.
type B interface{ isBinding() }
func (*BMissing) isBinding() {}
func (*BIdentifier) isBinding() {}
func (*BArray) isBinding() {}
func (*BObject) isBinding() {}
type BMissing struct{}
type BIdentifier struct{ Ref ast.Ref }
type BArray struct {
Items []ArrayBinding
CloseBracketLoc logger.Loc
HasSpread bool
IsSingleLine bool
}
type BObject struct {
Properties []PropertyBinding
CloseBraceLoc logger.Loc
IsSingleLine bool
}
type Expr struct {
Data E
Loc logger.Loc
}
// This interface is never called. Its purpose is to encode a variant type in
// Go's type system.
type E interface{ isExpr() }
func (*EArray) isExpr() {}
func (*EUnary) isExpr() {}
func (*EBinary) isExpr() {}
func (*EBoolean) isExpr() {}
func (*ESuper) isExpr() {}
func (*ENull) isExpr() {}
func (*EUndefined) isExpr() {}
func (*EThis) isExpr() {}
func (*ENew) isExpr() {}
func (*ENewTarget) isExpr() {}
func (*EImportMeta) isExpr() {}
func (*ECall) isExpr() {}
func (*EDot) isExpr() {}
func (*EIndex) isExpr() {}
func (*EArrow) isExpr() {}
func (*EFunction) isExpr() {}
func (*EClass) isExpr() {}
func (*EIdentifier) isExpr() {}
func (*EImportIdentifier) isExpr() {}
func (*EPrivateIdentifier) isExpr() {}
func (*ENameOfSymbol) isExpr() {}
func (*EJSXElement) isExpr() {}
func (*EJSXText) isExpr() {}
func (*EMissing) isExpr() {}
func (*ENumber) isExpr() {}
func (*EBigInt) isExpr() {}
func (*EObject) isExpr() {}
func (*ESpread) isExpr() {}
func (*EString) isExpr() {}
func (*ETemplate) isExpr() {}
func (*ERegExp) isExpr() {}
func (*EInlinedEnum) isExpr() {}
func (*EAnnotation) isExpr() {}
func (*EAwait) isExpr() {}
func (*EYield) isExpr() {}
func (*EIf) isExpr() {}
func (*ERequireString) isExpr() {}
func (*ERequireResolveString) isExpr() {}
func (*EImportString) isExpr() {}
func (*EImportCall) isExpr() {}
type EArray struct {
Items []Expr
CommaAfterSpread logger.Loc
CloseBracketLoc logger.Loc
IsSingleLine bool
IsParenthesized bool
}
type EUnary struct {
Value Expr
Op OpCode
// The expression "typeof (0, x)" must not become "typeof x" if "x"
// is unbound because that could suppress a ReferenceError from "x".
//
// Also if we know a typeof operator was originally an identifier, then
// we know that this typeof operator always has no side effects (even if
// we consider the identifier by itself to have a side effect).
//
// Note that there *is* actually a case where "typeof x" can throw an error:
// when "x" is being referenced inside of its TDZ (temporal dead zone). TDZ
// checks are not yet handled correctly by esbuild, so this possibility is
// currently ignored.
WasOriginallyTypeofIdentifier bool
// Similarly the expression "delete (0, x)" must not become "delete x"
// because that syntax is invalid in strict mode. We also need to make sure
// we don't accidentally change the return value:
//
// Returns false:
// "var a; delete (a)"
// "var a = Object.freeze({b: 1}); delete (a.b)"
// "var a = Object.freeze({b: 1}); delete (a?.b)"
// "var a = Object.freeze({b: 1}); delete (a['b'])"
// "var a = Object.freeze({b: 1}); delete (a?.['b'])"
//
// Returns true:
// "var a; delete (0, a)"
// "var a = Object.freeze({b: 1}); delete (true && a.b)"
// "var a = Object.freeze({b: 1}); delete (false || a?.b)"
// "var a = Object.freeze({b: 1}); delete (null ?? a?.['b'])"
// "var a = Object.freeze({b: 1}); delete (true ? a['b'] : a['b'])"
//
WasOriginallyDeleteOfIdentifierOrPropertyAccess bool
}
type EBinary struct {
Left Expr
Right Expr
Op OpCode
IsParenthesized bool
}
type EBoolean struct{ Value bool }
type EMissing struct{}
type ESuper struct{}
type ENull struct{}
type EUndefined struct{}
type EThis struct{}
type ENewTarget struct {
Range logger.Range
}
type EImportMeta struct {
RangeLen int32
}
// These help reduce unnecessary memory allocations
var BMissingShared = &BMissing{}
var EMissingShared = &EMissing{}
var ENullShared = &ENull{}
var ESuperShared = &ESuper{}
var EThisShared = &EThis{}
var EUndefinedShared = &EUndefined{}
var SDebuggerShared = &SDebugger{}
var SEmptyShared = &SEmpty{}
var STypeScriptShared = &STypeScript{}
var STypeScriptSharedWasDeclareClass = &STypeScript{WasDeclareClass: true}
type ENew struct {
Target Expr
Args []Expr
CloseParenLoc logger.Loc
IsMultiLine bool
// True if there is a comment containing "@__PURE__" or "#__PURE__" preceding
// this call expression. See the comment inside ECall for more details.
CanBeUnwrappedIfUnused bool
}
type CallKind uint8
const (
NormalCall CallKind = iota
DirectEval
TargetWasOriginallyPropertyAccess
)
type OptionalChain uint8
const (
// "a.b"
OptionalChainNone OptionalChain = iota
// "a?.b"
OptionalChainStart
// "a?.b.c" => ".c" is OptionalChainContinue
// "(a?.b).c" => ".c" is OptionalChainNone
OptionalChainContinue
)
type ECall struct {
Target Expr
Args []Expr
CloseParenLoc logger.Loc
OptionalChain OptionalChain
Kind CallKind
IsMultiLine bool
// True if there is a comment containing "@__PURE__" or "#__PURE__" preceding
// this call expression. This is an annotation used for tree shaking, and
// means that the call can be removed if it's unused. It does not mean the
// call is pure (e.g. it may still return something different if called twice).
//
// Note that the arguments are not considered to be part of the call. If the
// call itself is removed due to this annotation, the arguments must remain
// if they have side effects.
CanBeUnwrappedIfUnused bool
}
func (a *ECall) HasSameFlagsAs(b *ECall) bool {
return a.OptionalChain == b.OptionalChain &&
a.Kind == b.Kind &&
a.CanBeUnwrappedIfUnused == b.CanBeUnwrappedIfUnused
}
type EDot struct {
Target Expr
Name string
NameLoc logger.Loc
OptionalChain OptionalChain
// If true, this property access is known to be free of side-effects. That
// means it can be removed if the resulting value isn't used.
CanBeRemovedIfUnused bool
// If true, this property access is a function that, when called, can be
// unwrapped if the resulting value is unused. Unwrapping means discarding
// the call target but keeping any arguments with side effects.
CallCanBeUnwrappedIfUnused bool
// Symbol values are known to not have side effects when used as property
// names in class declarations and object literals.
IsSymbolInstance bool
}
func (a *EDot) HasSameFlagsAs(b *EDot) bool {
return a.OptionalChain == b.OptionalChain &&
a.CanBeRemovedIfUnused == b.CanBeRemovedIfUnused &&
a.CallCanBeUnwrappedIfUnused == b.CallCanBeUnwrappedIfUnused &&
a.IsSymbolInstance == b.IsSymbolInstance
}
type EIndex struct {
Target Expr
Index Expr
CloseBracketLoc logger.Loc
OptionalChain OptionalChain
// If true, this property access is known to be free of side-effects. That
// means it can be removed if the resulting value isn't used.
CanBeRemovedIfUnused bool
// If true, this property access is a function that, when called, can be
// unwrapped if the resulting value is unused. Unwrapping means discarding
// the call target but keeping any arguments with side effects.
CallCanBeUnwrappedIfUnused bool
// Symbol values are known to not have side effects when used as property
// names in class declarations and object literals.
IsSymbolInstance bool
}
func (a *EIndex) HasSameFlagsAs(b *EIndex) bool {
return a.OptionalChain == b.OptionalChain &&
a.CanBeRemovedIfUnused == b.CanBeRemovedIfUnused &&
a.CallCanBeUnwrappedIfUnused == b.CallCanBeUnwrappedIfUnused &&
a.IsSymbolInstance == b.IsSymbolInstance
}
type EArrow struct {
Args []Arg
Body FnBody
IsAsync bool
HasRestArg bool
PreferExpr bool // Use shorthand if true and "Body" is a single return statement
// V8 uses parentheses as an optimization hint: https://v8.dev/blog/preparser#pife
IsParenthesized bool
// See: https://github.com/rollup/rollup/pull/5024
HasNoSideEffectsComment bool
}
type EFunction struct {
Fn Fn
// V8 uses parentheses as an optimization hint: https://v8.dev/blog/preparser#pife
IsParenthesized bool
}
type EClass struct{ Class Class }
type EIdentifier struct {
Ref ast.Ref
// If we're inside a "with" statement, this identifier may be a property
// access. In that case it would be incorrect to remove this identifier since
// the property access may be a getter or setter with side effects.
MustKeepDueToWithStmt bool
// If true, this identifier is known to not have a side effect (i.e. to not
// throw an exception) when referenced. If false, this identifier may or may
// not have side effects when referenced. This is used to allow the removal
// of known globals such as "Object" if they aren't used.
CanBeRemovedIfUnused bool
// If true, this identifier represents a function that, when called, can be
// unwrapped if the resulting value is unused. Unwrapping means discarding
// the call target but keeping any arguments with side effects.
CallCanBeUnwrappedIfUnused bool
}
// This is similar to an EIdentifier but it represents a reference to an ES6
// import item.
//
// Depending on how the code is linked, the file containing this EImportIdentifier
// may or may not be in the same module group as the file it was imported from.
//
// If it's the same module group than we can just merge the import item symbol
// with the corresponding symbol that was imported, effectively renaming them
// to be the same thing and statically binding them together.
//
// But if it's a different module group, then the import must be dynamically
// evaluated using a property access off the corresponding namespace symbol,
// which represents the result of a require() call.
//
// It's stored as a separate type so it's not easy to confuse with a plain
// identifier. For example, it'd be bad if code trying to convert "{x: x}" into
// "{x}" shorthand syntax wasn't aware that the "x" in this case is actually
// "{x: importedNamespace.x}". This separate type forces code to opt-in to
// doing this instead of opt-out.
type EImportIdentifier struct {
Ref ast.Ref
PreferQuotedKey bool
// If true, this was originally an identifier expression such as "foo". If
// false, this could potentially have been a member access expression such
// as "ns.foo" off of an imported namespace object.
WasOriginallyIdentifier bool
}
// This is similar to EIdentifier but it represents class-private fields and
// methods. It can be used where computed properties can be used, such as
// EIndex and Property.
type EPrivateIdentifier struct {
Ref ast.Ref
}
// This represents an internal property name that can be mangled. The symbol
// referenced by this expression should be a "SymbolMangledProp" symbol.
type ENameOfSymbol struct {
Ref ast.Ref
HasPropertyKeyComment bool // If true, a preceding comment contains "@__KEY__"
}
type EJSXElement struct {
TagOrNil Expr
Properties []Property
// Note: This array may contain nil entries. Be careful about nil entries
// when iterating over this array.
//
// Each nil entry corresponds to the "JSXChildExpression_opt" part of the
// grammar (https://facebook.github.io/jsx/#prod-JSXChild):
//
// JSXChild :
// JSXText
// JSXElement
// JSXFragment
// { JSXChildExpression_opt }
//
// This is the "{}" part in "<a>{}</a>". We allow this because some people
// put comments there and then expect to be able to process them from
// esbuild's output. These absent AST nodes are completely omitted when
// JSX is transformed to JS. They are only present when JSX preservation is
// enabled.
NullableChildren []Expr
CloseLoc logger.Loc
IsTagSingleLine bool
}
// The JSX specification doesn't say how JSX text is supposed to be interpreted
// so our "preserve" JSX transform should reproduce the original source code
// verbatim. One reason why this matters is because there is no canonical way
// to interpret JSX text (Babel and TypeScript differ in what newlines mean).
// Another reason is that some people want to do custom things such as this:
// https://github.com/evanw/esbuild/issues/3605
type EJSXText struct {
Raw string
}
type ENumber struct{ Value float64 }
type EBigInt struct{ Value string }
type EObject struct {
Properties []Property
CommaAfterSpread logger.Loc
CloseBraceLoc logger.Loc
IsSingleLine bool
IsParenthesized bool
}
type ESpread struct{ Value Expr }
// This is used for both strings and no-substitution template literals to reduce
// the number of cases that need to be checked for string optimization code
type EString struct {
Value []uint16
LegacyOctalLoc logger.Loc
PreferTemplate bool
HasPropertyKeyComment bool // If true, a preceding comment contains "@__KEY__"
ContainsUniqueKey bool // If true, this string must not be wrapped
}
type TemplatePart struct {
Value Expr
TailRaw string // Only use when "TagOrNil" is not nil
TailCooked []uint16 // Only use when "TagOrNil" is nil
TailLoc logger.Loc
}
type ETemplate struct {
TagOrNil Expr
HeadRaw string // Only use when "TagOrNil" is not nil
HeadCooked []uint16 // Only use when "TagOrNil" is nil
Parts []TemplatePart
HeadLoc logger.Loc
LegacyOctalLoc logger.Loc
// True if this is a tagged template literal with a comment that indicates
// this function call can be removed if the result is unused. Note that the
// arguments are not considered to be part of the call. If the call itself
// is removed due to this annotation, the arguments must remain if they have
// side effects (including the string conversions).
CanBeUnwrappedIfUnused bool
// If the tag is present, it is expected to be a function and is called. If
// the tag is a syntactic property access, then the value for "this" in the
// function call is the object whose property was accessed (e.g. in "a.b``"
// the value for "this" in "a.b" is "a"). We need to ensure that if "a``"
// ever becomes "b.c``" later on due to optimizations, it is written as
// "(0, b.c)``" to avoid a behavior change.
TagWasOriginallyPropertyAccess bool
}
type ERegExp struct{ Value string }
type EInlinedEnum struct {
Value Expr
Comment string
}
type AnnotationFlags uint8
const (
// This is sort of like an IIFE with a "/* @__PURE__ */" comment except it's an
// inline annotation on an expression itself without the nested scope. Sometimes
// we can't easily introduce a new scope (e.g. if the expression uses "await").
CanBeRemovedIfUnusedFlag AnnotationFlags = 1 << iota
)
func (flags AnnotationFlags) Has(flag AnnotationFlags) bool {
return (flags & flag) != 0
}
type EAnnotation struct {
Value Expr
Flags AnnotationFlags
}
type EAwait struct {
Value Expr
}
type EYield struct {
ValueOrNil Expr
IsStar bool
}
type EIf struct {
Test Expr
Yes Expr
No Expr
}
type ERequireString struct {
ImportRecordIndex uint32
CloseParenLoc logger.Loc
}
type ERequireResolveString struct {
ImportRecordIndex uint32
CloseParenLoc logger.Loc
}
type EImportString struct {
ImportRecordIndex uint32
CloseParenLoc logger.Loc
}
type EImportCall struct {
Expr Expr
OptionsOrNil Expr
CloseParenLoc logger.Loc
Phase ast.ImportPhase
}
type Stmt struct {
Data S
Loc logger.Loc
}
// This interface is never called. Its purpose is to encode a variant type in
// Go's type system.
type S interface{ isStmt() }
func (*SBlock) isStmt() {}
func (*SComment) isStmt() {}
func (*SDebugger) isStmt() {}
func (*SDirective) isStmt() {}
func (*SEmpty) isStmt() {}
func (*STypeScript) isStmt() {}
func (*SExportClause) isStmt() {}
func (*SExportFrom) isStmt() {}
func (*SExportDefault) isStmt() {}
func (*SExportStar) isStmt() {}
func (*SExportEquals) isStmt() {}
func (*SLazyExport) isStmt() {}
func (*SExpr) isStmt() {}
func (*SEnum) isStmt() {}
func (*SNamespace) isStmt() {}
func (*SFunction) isStmt() {}
func (*SClass) isStmt() {}
func (*SLabel) isStmt() {}
func (*SIf) isStmt() {}
func (*SFor) isStmt() {}
func (*SForIn) isStmt() {}
func (*SForOf) isStmt() {}
func (*SDoWhile) isStmt() {}
func (*SWhile) isStmt() {}
func (*SWith) isStmt() {}
func (*STry) isStmt() {}
func (*SSwitch) isStmt() {}
func (*SImport) isStmt() {}
func (*SReturn) isStmt() {}
func (*SThrow) isStmt() {}
func (*SLocal) isStmt() {}
func (*SBreak) isStmt() {}
func (*SContinue) isStmt() {}
type SBlock struct {
Stmts []Stmt
CloseBraceLoc logger.Loc
}
type SEmpty struct{}
// This is a stand-in for a TypeScript type declaration
type STypeScript struct {
WasDeclareClass bool
}
type SComment struct {
Text string
IsLegalComment bool
}
type SDebugger struct{}
type SDirective struct {
Value []uint16
LegacyOctalLoc logger.Loc
}
type SExportClause struct {
Items []ClauseItem
IsSingleLine bool
}
type SExportFrom struct {
Items []ClauseItem
NamespaceRef ast.Ref
ImportRecordIndex uint32
IsSingleLine bool
}
type SExportDefault struct {
Value Stmt // May be a SExpr or SFunction or SClass
DefaultName ast.LocRef
}
type ExportStarAlias struct {
// Although this alias name starts off as being the same as the statement's
// namespace symbol, it may diverge if the namespace symbol name is minified.
// The original alias name is preserved here to avoid this scenario.
OriginalName string
Loc logger.Loc
}
type SExportStar struct {
Alias *ExportStarAlias
NamespaceRef ast.Ref
ImportRecordIndex uint32
}
// This is an "export = value;" statement in TypeScript
type SExportEquals struct {
Value Expr
}
// The decision of whether to export an expression using "module.exports" or
// "export default" is deferred until linking using this statement kind
type SLazyExport struct {
Value Expr
}
type SExpr struct {
Value Expr
// This is set to true for automatically-generated expressions that are part
// of class syntax lowering. A single class declaration may end up with many
// generated expressions after it (e.g. class field initializations, a call
// to keep the original value of the "name" property). When this happens we
// can't tell that the class is side-effect free anymore because all of these
// methods mutate the class. We use this annotation for that instead.
IsFromClassOrFnThatCanBeRemovedIfUnused bool
}
type EnumValue struct {
ValueOrNil Expr
Name []uint16
Ref ast.Ref
Loc logger.Loc
}
type SEnum struct {
Values []EnumValue
Name ast.LocRef
Arg ast.Ref
IsExport bool
}
type SNamespace struct {
Stmts []Stmt
Name ast.LocRef
Arg ast.Ref
IsExport bool
}
type SFunction struct {
Fn Fn
IsExport bool
}
type SClass struct {
Class Class
IsExport bool
}
type SLabel struct {
Stmt Stmt
Name ast.LocRef
IsSingleLineStmt bool
}
type SIf struct {
Test Expr
Yes Stmt
NoOrNil Stmt
IsSingleLineYes bool
IsSingleLineNo bool
}
type SFor struct {
InitOrNil Stmt // May be a SConst, SLet, SVar, or SExpr
TestOrNil Expr
UpdateOrNil Expr
Body Stmt
IsSingleLineBody bool
IsLoweredForAwait bool
}
type SForIn struct {
Init Stmt // May be a SConst, SLet, SVar, or SExpr
Value Expr
Body Stmt
IsSingleLineBody bool
}
type SForOf struct {
Init Stmt // May be a SConst, SLet, SVar, or SExpr
Value Expr
Body Stmt
Await logger.Range
IsSingleLineBody bool
}
type SDoWhile struct {
Body Stmt
Test Expr
}
type SWhile struct {
Test Expr
Body Stmt
IsSingleLineBody bool
}
type SWith struct {
Value Expr
Body Stmt
BodyLoc logger.Loc
IsSingleLineBody bool
}
type Catch struct {
BindingOrNil Binding
Block SBlock
Loc logger.Loc
BlockLoc logger.Loc
}
type Finally struct {
Block SBlock
Loc logger.Loc
}
type STry struct {
Catch *Catch
Finally *Finally
Block SBlock
BlockLoc logger.Loc
}
type Case struct {
ValueOrNil Expr // If this is nil, this is "default" instead of "case"
Body []Stmt
Loc logger.Loc
}
type SSwitch struct {
Test Expr
Cases []Case
BodyLoc logger.Loc
CloseBraceLoc logger.Loc
}
// This object represents all of these types of import statements:
//
// import 'path'
// import {item1, item2} from 'path'
// import * as ns from 'path'
// import defaultItem, {item1, item2} from 'path'
// import defaultItem, * as ns from 'path'
//
// Many parts are optional and can be combined in different ways. The only
// restriction is that you cannot have both a clause and a star namespace.
type SImport struct {
DefaultName *ast.LocRef
Items *[]ClauseItem
StarNameLoc *logger.Loc
// If this is a star import: This is a Ref for the namespace symbol. The Loc
// for the symbol is StarLoc.
//
// Otherwise: This is an auto-generated Ref for the namespace representing
// the imported file. In this case StarLoc is nil. The NamespaceRef is used
// when converting this module to a CommonJS module.
NamespaceRef ast.Ref
ImportRecordIndex uint32
IsSingleLine bool
}
type SReturn struct {
ValueOrNil Expr
}
type SThrow struct {
Value Expr
}
type LocalKind uint8
const (
LocalVar LocalKind = iota
LocalLet
LocalConst
LocalUsing
LocalAwaitUsing
)
func (kind LocalKind) IsUsing() bool {
return kind >= LocalUsing
}
type SLocal struct {
Decls []Decl
Kind LocalKind
IsExport bool
// The TypeScript compiler doesn't generate code for "import foo = bar"
// statements where the import is never used.
WasTSImportEquals bool
}
type SBreak struct {
Label *ast.LocRef
}
type SContinue struct {
Label *ast.LocRef
}
type ClauseItem struct {
Alias string
// This is the original name of the symbol stored in "Name". It's needed for
// "SExportClause" statements such as this:
//
// export {foo as bar} from 'path'
//
// In this case both "foo" and "bar" are aliases because it's a re-export.
// We need to preserve both aliases in case the symbol is renamed. In this
// example, "foo" is "OriginalName" and "bar" is "Alias".
OriginalName string
AliasLoc logger.Loc
Name ast.LocRef
}
type Decl struct {
Binding Binding
ValueOrNil Expr
}
type ScopeKind uint8
const (
ScopeBlock ScopeKind = iota
ScopeWith
ScopeLabel
ScopeClassName
ScopeClassBody
ScopeCatchBinding
// The scopes below stop hoisted variables from extending into parent scopes
ScopeEntry // This is a module, TypeScript enum, or TypeScript namespace
ScopeFunctionArgs
ScopeFunctionBody
ScopeClassStaticInit
)
func (kind ScopeKind) StopsHoisting() bool {
return kind >= ScopeEntry
}
type ScopeMember struct {
Ref ast.Ref
Loc logger.Loc
}
type Scope struct {
// This will be non-nil if this is a TypeScript "namespace" or "enum"
TSNamespace *TSNamespaceScope
Parent *Scope
Children []*Scope
Members map[string]ScopeMember
Replaced []ScopeMember
Generated []ast.Ref
// The location of the "use strict" directive for ExplicitStrictMode
UseStrictLoc logger.Loc
// This is used to store the ref of the label symbol for ScopeLabel scopes.
Label ast.LocRef
LabelStmtIsLoop bool
// If a scope contains a direct eval() expression, then none of the symbols
// inside that scope can be renamed. We conservatively assume that the
// evaluated code might reference anything that it has access to.
ContainsDirectEval bool
// This is to help forbid "arguments" inside class body scopes
ForbidArguments bool
// As a special case, we enable constant propagation for any chain of "const"
// declarations at the start of a statement list. This special case doesn't
// have any TDZ considerations because no other statements come before it.
IsAfterConstLocalPrefix bool
StrictMode StrictModeKind
Kind ScopeKind
}
type StrictModeKind uint8
const (
SloppyMode StrictModeKind = iota
ExplicitStrictMode
ImplicitStrictModeClass
ImplicitStrictModeESM
ImplicitStrictModeTSAlwaysStrict
ImplicitStrictModeJSXAutomaticRuntime
)
func (s *Scope) RecursiveSetStrictMode(kind StrictModeKind) {
if s.StrictMode == SloppyMode {
s.StrictMode = kind
for _, child := range s.Children {
child.RecursiveSetStrictMode(kind)
}
}
}
// This is for TypeScript "enum" and "namespace" blocks. Each block can
// potentially be instantiated multiple times. The exported members of each
// block are merged into a single namespace while the non-exported code is
// still scoped to just within that block:
//
// let x = 1;
// namespace Foo {
// let x = 2;
// export let y = 3;
// }
// namespace Foo {
// console.log(x); // 1
// console.log(y); // 3
// }
//
// Doing this also works inside an enum:
//
// enum Foo {
// A = 3,
// B = A + 1,
// }
// enum Foo {
// C = A + 2,
// }
// console.log(Foo.B) // 4
// console.log(Foo.C) // 5
//
// This is a form of identifier lookup that works differently than the
// hierarchical scope-based identifier lookup in JavaScript. Lookup now needs
// to search sibling scopes in addition to parent scopes. This is accomplished
// by sharing the map of exported members between all matching sibling scopes.
type TSNamespaceScope struct {
// This is shared between all sibling namespace blocks
ExportedMembers TSNamespaceMembers
// This is a lazily-generated map of identifiers that actually represent
// property accesses to this namespace's properties. For example:
//
// namespace x {
// export let y = 123
// }
// namespace x {
// export let z = y
// }
//
// This should be compiled into the following code:
//
// var x;
// (function(x2) {
// x2.y = 123;
// })(x || (x = {}));
// (function(x3) {
// x3.z = x3.y;
// })(x || (x = {}));
//
// When we try to find the symbol "y", we instead return one of these lazily
// generated proxy symbols that represent the property access "x3.y". This
// map is unique per namespace block because "x3" is the argument symbol that
// is specific to that particular namespace block.
LazilyGeneratedProperyAccesses map[string]ast.Ref
// This is specific to this namespace block. It's the argument of the
// immediately-invoked function expression that the namespace block is
// compiled into:
//
// var ns;
// (function (ns2) {
// ns2.x = 123;
// })(ns || (ns = {}));
//
// This variable is "ns2" in the above example. It's the symbol to use when
// generating property accesses off of this namespace when it's in scope.
ArgRef ast.Ref
// Even though enums are like namespaces and both enums and namespaces allow
// implicit references to properties of sibling scopes, they behave like
// separate, er, namespaces. Implicit references only work namespace-to-
// namespace and enum-to-enum. They do not work enum-to-namespace. And I'm
// not sure what's supposed to happen for the namespace-to-enum case because
// the compiler crashes: https://github.com/microsoft/TypeScript/issues/46891.
// So basically these both work:
//
// enum a { b = 1 }
// enum a { c = b }
//
// namespace x { export let y = 1 }
// namespace x { export let z = y }
//
// This doesn't work:
//
// enum a { b = 1 }
// namespace a { export let c = b }
//
// And this crashes the TypeScript compiler:
//
// namespace a { export let b = 1 }
// enum a { c = b }
//
// Therefore we only allow enum/enum and namespace/namespace interactions.
IsEnumScope bool
}
type TSNamespaceMembers map[string]TSNamespaceMember
type TSNamespaceMember struct {
Data TSNamespaceMemberData
Loc logger.Loc
IsEnumValue bool
}
type TSNamespaceMemberData interface {
isTSNamespaceMember()
}
func (TSNamespaceMemberProperty) isTSNamespaceMember() {}
func (TSNamespaceMemberNamespace) isTSNamespaceMember() {}
func (TSNamespaceMemberEnumNumber) isTSNamespaceMember() {}
func (TSNamespaceMemberEnumString) isTSNamespaceMember() {}
// "namespace ns { export let it }"
type TSNamespaceMemberProperty struct{}
// "namespace ns { export namespace it {} }"
type TSNamespaceMemberNamespace struct {
ExportedMembers TSNamespaceMembers
}
// "enum ns { it }"
type TSNamespaceMemberEnumNumber struct {
Value float64
}
// "enum ns { it = 'it' }"
type TSNamespaceMemberEnumString struct {
Value []uint16
}
type ExportsKind uint8
const (
// This file doesn't have any kind of export, so it's impossible to say what
// kind of file this is. An empty file is in this category, for example.
ExportsNone ExportsKind = iota
// The exports are stored on "module" and/or "exports". Calling "require()"
// on this module returns "module.exports". All imports to this module are
// allowed but may return undefined.
ExportsCommonJS
// All export names are known explicitly. Calling "require()" on this module
// generates an exports object (stored in "exports") with getters for the
// export names. Named imports to this module are only allowed if they are
// in the set of export names.
ExportsESM
// Some export names are known explicitly, but others fall back to a dynamic
// run-time object. This is necessary when using the "export * from" syntax
// with either a CommonJS module or an external module (i.e. a module whose
// export names are not known at compile-time).
//
// Calling "require()" on this module generates an exports object (stored in
// "exports") with getters for the export names. All named imports to this
// module are allowed. Direct named imports reference the corresponding export
// directly. Other imports go through property accesses on "exports".
ExportsESMWithDynamicFallback
)
func (kind ExportsKind) IsDynamic() bool {
return kind == ExportsCommonJS || kind == ExportsESMWithDynamicFallback
}
type ModuleType uint8
const (
ModuleUnknown ModuleType = iota
// ".cjs" or ".cts" or "type: commonjs" in package.json
ModuleCommonJS_CJS
ModuleCommonJS_CTS
ModuleCommonJS_PackageJSON
// ".mjs" or ".mts" or "type: module" in package.json
ModuleESM_MJS
ModuleESM_MTS
ModuleESM_PackageJSON
)
func (mt ModuleType) IsCommonJS() bool {
return mt >= ModuleCommonJS_CJS && mt <= ModuleCommonJS_PackageJSON
}
func (mt ModuleType) IsESM() bool {
return mt >= ModuleESM_MJS && mt <= ModuleESM_PackageJSON
}
type ModuleTypeData struct {
Source *logger.Source
Range logger.Range
Type ModuleType
}
// This is the index to the automatically-generated part containing code that
// calls "__export(exports, { ... getters ... })". This is used to generate
// getters on an exports object for ES6 export statements, and is both for
// ES6 star imports and CommonJS-style modules. All files have one of these,
// although it may contain no statements if there is nothing to export.
const NSExportPartIndex = uint32(0)
type AST struct {
ModuleTypeData ModuleTypeData
Parts []Part
Symbols []ast.Symbol
ExprComments map[logger.Loc][]string
ModuleScope *Scope
CharFreq *ast.CharFreq
// This is internal-only data used for the implementation of Yarn PnP
ManifestForYarnPnP Expr
Hashbang string
Directives []string
URLForCSS string
// Note: If you're in the linker, do not use this map directly. This map is
// filled in by the parser and is considered immutable. For performance reasons,
// the linker doesn't mutate this map (cloning a map is slow in Go). Instead the
// linker super-imposes relevant information on top in a method call. You should
// call "TopLevelSymbolToParts" instead.
TopLevelSymbolToPartsFromParser map[ast.Ref][]uint32
// This contains all top-level exported TypeScript enum constants. It exists
// to enable cross-module inlining of constant enums.
TSEnums map[ast.Ref]map[string]TSEnumValue
// This contains the values of all detected inlinable constants. It exists
// to enable cross-module inlining of these constants.
ConstValues map[ast.Ref]ConstValue
// Properties in here are represented as symbols instead of strings, which
// allows them to be renamed to smaller names.
MangledProps map[string]ast.Ref
// Properties in here are existing non-mangled properties in the source code
// and must not be used when generating mangled names to avoid a collision.
ReservedProps map[string]bool
// These are stored at the AST level instead of on individual AST nodes so
// they can be manipulated efficiently without a full AST traversal
ImportRecords []ast.ImportRecord
// These are used when bundling. They are filled in during the parser pass
// since we already have to traverse the AST then anyway and the parser pass
// is conveniently fully parallelized.
NamedImports map[ast.Ref]NamedImport
NamedExports map[string]NamedExport
ExportStarImportRecords []uint32
SourceMapComment logger.Span
// This is a list of ES6 features. They are ranges instead of booleans so
// that they can be used in log messages. Check to see if "Len > 0".
ExportKeyword logger.Range // Does not include TypeScript-specific syntax
TopLevelAwaitKeyword logger.Range
LiveTopLevelAwaitKeyword logger.Range // Excludes top-level await in dead branches
ExportsRef ast.Ref
ModuleRef ast.Ref
WrapperRef ast.Ref
ApproximateLineCount int32
NestedScopeSlotCounts ast.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
// closure. Note that this also includes top-level "return" but these aren't
// here because only the parser checks those.
UsesExportsRef bool
UsesModuleRef bool
ExportsKind ExportsKind
}
type TSEnumValue struct {
String []uint16 // Use this if it's not nil
Number float64 // Use this if "String" is nil
}
type ConstValueKind uint8
const (
ConstValueNone ConstValueKind = iota
ConstValueNull
ConstValueUndefined
ConstValueTrue
ConstValueFalse
ConstValueNumber
ConstValueString
)
type ConstValue struct {
Number float64 // Use this for "ConstValueNumber"
String []uint16 // Use this for "ConstValueString"
Kind ConstValueKind
}
func ExprToConstValue(expr Expr) ConstValue {
switch v := expr.Data.(type) {
case *ENull:
return ConstValue{Kind: ConstValueNull}
case *EUndefined:
return ConstValue{Kind: ConstValueUndefined}
case *EBoolean:
if v.Value {
return ConstValue{Kind: ConstValueTrue}
} else {
return ConstValue{Kind: ConstValueFalse}
}
case *ENumber:
// Inline integers and other small numbers. Don't inline large
// real numbers because people may not want them to be inlined
// as it will increase the minified code size by too much.
if asInt := int64(v.Value); v.Value == float64(asInt) || len(strconv.FormatFloat(v.Value, 'g', -1, 64)) <= 8 {
return ConstValue{Kind: ConstValueNumber, Number: v.Value}
}
case *EString:
// Deliberately only inline small strings. We don't want to always
// inline all strings because they can be arbitrarily long.
if len(v.Value) <= 3 {
return ConstValue{Kind: ConstValueString, String: v.Value}
}
case *EBigInt:
// I'm deliberately not inlining bigints here for the same reason (they can
// be arbitrarily long).
}
return ConstValue{}
}
func ConstValueToExpr(loc logger.Loc, value ConstValue) Expr {
switch value.Kind {
case ConstValueNull:
return Expr{Loc: loc, Data: ENullShared}
case ConstValueUndefined:
return Expr{Loc: loc, Data: EUndefinedShared}
case ConstValueTrue:
return Expr{Loc: loc, Data: &EBoolean{Value: true}}
case ConstValueFalse:
return Expr{Loc: loc, Data: &EBoolean{Value: false}}
case ConstValueNumber:
return Expr{Loc: loc, Data: &ENumber{Value: value.Number}}
case ConstValueString:
return Expr{Loc: loc, Data: &EString{Value: value.String}}
}
panic("Internal error: invalid constant value")
}
type NamedImport struct {
Alias string
// Parts within this file that use this import
LocalPartsWithUses []uint32
AliasLoc logger.Loc
NamespaceRef ast.Ref
ImportRecordIndex uint32
// If true, the alias refers to the entire export namespace object of a
// module. This is no longer represented as an alias called "*" because of
// the upcoming "Arbitrary module namespace identifier names" feature:
// https://github.com/tc39/ecma262/pull/2154
AliasIsStar bool
// It's useful to flag exported imports because if they are in a TypeScript
// file, we can't tell if they are a type or a value.
IsExported bool
}
type NamedExport struct {
Ref ast.Ref
AliasLoc logger.Loc
}
// Each file is made up of multiple parts, and each part consists of one or
// more top-level statements. Parts are used for tree shaking and code
// splitting analysis. Individual parts of a file can be discarded by tree
// shaking and can be assigned to separate chunks (i.e. output files) by code
// splitting.
type Part struct {
Stmts []Stmt
Scopes []*Scope
// Each is an index into the file-level import record list
ImportRecordIndices []uint32
// All symbols that are declared in this part. Note that a given symbol may
// have multiple declarations, and so may end up being declared in multiple
// parts (e.g. multiple "var" declarations with the same name). Also note
// that this list isn't deduplicated and may contain duplicates.
DeclaredSymbols []DeclaredSymbol
// An estimate of the number of uses of all symbols used within this part.
SymbolUses map[ast.Ref]SymbolUse
// An estimate of the number of uses of all symbols used as the target of
// function calls within this part.
SymbolCallUses map[ast.Ref]SymbolCallUse
// This tracks property accesses off of imported symbols. We don't know
// during parsing if an imported symbol is going to be an inlined enum
// value or not. This is only known during linking. So we defer adding
// a dependency on these imported symbols until we know whether the
// property access is an inlined enum value or not.
ImportSymbolPropertyUses map[ast.Ref]map[string]SymbolUse
// The indices of the other parts in this file that are needed if this part
// is needed.
Dependencies []Dependency
// If true, this part can be removed if none of the declared symbols are
// used. If the file containing this part is imported, then all parts that
// don't have this flag enabled must be included.
CanBeRemovedIfUnused bool
// This is used for generated parts that we don't want to be present if they
// aren't needed. This enables tree shaking for these parts even if global
// tree shaking isn't enabled.
ForceTreeShaking bool
// This is true if this file has been marked as live by the tree shaking
// algorithm.
IsLive bool
}
type Dependency struct {
SourceIndex uint32
PartIndex uint32
}
type DeclaredSymbol struct {
Ref ast.Ref
IsTopLevel bool
}
type SymbolUse struct {
CountEstimate uint32
}
type SymbolCallUse struct {
CallCountEstimate uint32
SingleArgNonSpreadCallCountEstimate uint32
}
// For readability, the names of certain automatically-generated symbols are
// derived from the file name. For example, instead of the CommonJS wrapper for
// a file being called something like "require273" it can be called something
// like "require_react" instead. This function generates the part of these
// identifiers that's specific to the file path. It can take both an absolute
// path (OS-specific) and a path in the source code (OS-independent).
//
// Note that these generated names do not at all relate to the correctness of
// the code as far as avoiding symbol name collisions. These names still go
// through the renaming logic that all other symbols go through to avoid name
// collisions.
func GenerateNonUniqueNameFromPath(path string) string {
// Get the file name without the extension
dir, base, _ := logger.PlatformIndependentPathDirBaseExt(path)
// If the name is "index", use the directory name instead. This is because
// many packages in npm use the file name "index.js" because it triggers
// node's implicit module resolution rules that allows you to import it by
// just naming the directory.
if base == "index" {
_, dirBase, _ := logger.PlatformIndependentPathDirBaseExt(dir)
if dirBase != "" {
base = dirBase
}
}
return EnsureValidIdentifier(base)
}
func EnsureValidIdentifier(base string) string {
// Convert it to an ASCII identifier. Note: If you change this to a non-ASCII
// identifier, you're going to potentially cause trouble with non-BMP code
// points in target environments that don't support bracketed Unicode escapes.
bytes := []byte{}
needsGap := false
for _, c := range base {
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (len(bytes) > 0 && c >= '0' && c <= '9') {
if needsGap {
bytes = append(bytes, '_')
needsGap = false
}
bytes = append(bytes, byte(c))
} else if len(bytes) > 0 {
needsGap = true
}
}
// Make sure the name isn't empty
if len(bytes) == 0 {
return "_"
}
return string(bytes)
}