mirror of
https://github.com/hanzoai/esbuild.git
synced 2026-09-24 16:58:50 +00:00
248 lines
6.4 KiB
Go
248 lines
6.4 KiB
Go
package js_parser
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/evanw/esbuild/internal/compat"
|
|
"github.com/evanw/esbuild/internal/helpers"
|
|
"github.com/evanw/esbuild/internal/js_ast"
|
|
"github.com/evanw/esbuild/internal/js_lexer"
|
|
"github.com/evanw/esbuild/internal/logger"
|
|
)
|
|
|
|
type jsonParser struct {
|
|
log logger.Log
|
|
source logger.Source
|
|
tracker logger.LineColumnTracker
|
|
lexer js_lexer.Lexer
|
|
options JSONOptions
|
|
suppressWarningsAboutWeirdCode bool
|
|
}
|
|
|
|
func (p *jsonParser) parseMaybeTrailingComma(closeToken js_lexer.T) bool {
|
|
commaRange := p.lexer.Range()
|
|
p.lexer.Expect(js_lexer.TComma)
|
|
|
|
if p.lexer.Token == closeToken {
|
|
if p.options.Flavor == js_lexer.JSON {
|
|
p.log.AddError(&p.tracker, commaRange, "JSON does not support trailing commas")
|
|
}
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (p *jsonParser) parseExpr() js_ast.Expr {
|
|
loc := p.lexer.Loc()
|
|
|
|
switch p.lexer.Token {
|
|
case js_lexer.TFalse:
|
|
p.lexer.Next()
|
|
return js_ast.Expr{Loc: loc, Data: &js_ast.EBoolean{Value: false}}
|
|
|
|
case js_lexer.TTrue:
|
|
p.lexer.Next()
|
|
return js_ast.Expr{Loc: loc, Data: &js_ast.EBoolean{Value: true}}
|
|
|
|
case js_lexer.TNull:
|
|
p.lexer.Next()
|
|
return js_ast.Expr{Loc: loc, Data: js_ast.ENullShared}
|
|
|
|
case js_lexer.TStringLiteral:
|
|
value := p.lexer.StringLiteral()
|
|
p.lexer.Next()
|
|
return js_ast.Expr{Loc: loc, Data: &js_ast.EString{Value: value}}
|
|
|
|
case js_lexer.TNumericLiteral:
|
|
value := p.lexer.Number
|
|
p.lexer.Next()
|
|
return js_ast.Expr{Loc: loc, Data: &js_ast.ENumber{Value: value}}
|
|
|
|
case js_lexer.TMinus:
|
|
p.lexer.Next()
|
|
value := p.lexer.Number
|
|
p.lexer.Expect(js_lexer.TNumericLiteral)
|
|
return js_ast.Expr{Loc: loc, Data: &js_ast.ENumber{Value: -value}}
|
|
|
|
case js_lexer.TOpenBracket:
|
|
p.lexer.Next()
|
|
isSingleLine := !p.lexer.HasNewlineBefore
|
|
items := []js_ast.Expr{}
|
|
|
|
for p.lexer.Token != js_lexer.TCloseBracket {
|
|
if len(items) > 0 {
|
|
if p.lexer.HasNewlineBefore {
|
|
isSingleLine = false
|
|
}
|
|
if !p.parseMaybeTrailingComma(js_lexer.TCloseBracket) {
|
|
break
|
|
}
|
|
if p.lexer.HasNewlineBefore {
|
|
isSingleLine = false
|
|
}
|
|
}
|
|
|
|
item := p.parseExpr()
|
|
items = append(items, item)
|
|
}
|
|
|
|
if p.lexer.HasNewlineBefore {
|
|
isSingleLine = false
|
|
}
|
|
closeBracketLoc := p.lexer.Loc()
|
|
p.lexer.Expect(js_lexer.TCloseBracket)
|
|
return js_ast.Expr{Loc: loc, Data: &js_ast.EArray{
|
|
Items: items,
|
|
IsSingleLine: isSingleLine,
|
|
CloseBracketLoc: closeBracketLoc,
|
|
}}
|
|
|
|
case js_lexer.TOpenBrace:
|
|
p.lexer.Next()
|
|
isSingleLine := !p.lexer.HasNewlineBefore
|
|
properties := []js_ast.Property{}
|
|
duplicates := make(map[string]logger.Range)
|
|
|
|
for p.lexer.Token != js_lexer.TCloseBrace {
|
|
if len(properties) > 0 {
|
|
if p.lexer.HasNewlineBefore {
|
|
isSingleLine = false
|
|
}
|
|
if !p.parseMaybeTrailingComma(js_lexer.TCloseBrace) {
|
|
break
|
|
}
|
|
if p.lexer.HasNewlineBefore {
|
|
isSingleLine = false
|
|
}
|
|
}
|
|
|
|
keyString := p.lexer.StringLiteral()
|
|
keyRange := p.lexer.Range()
|
|
key := js_ast.Expr{Loc: keyRange.Loc, Data: &js_ast.EString{Value: keyString}}
|
|
p.lexer.Expect(js_lexer.TStringLiteral)
|
|
|
|
// Warn about duplicate keys
|
|
if !p.suppressWarningsAboutWeirdCode {
|
|
keyText := helpers.UTF16ToString(keyString)
|
|
if prevRange, ok := duplicates[keyText]; ok {
|
|
p.log.AddIDWithNotes(logger.MsgID_JS_DuplicateObjectKey, logger.Warning, &p.tracker, keyRange,
|
|
fmt.Sprintf("Duplicate key %q in object literal", keyText),
|
|
[]logger.MsgData{p.tracker.MsgData(prevRange, fmt.Sprintf("The original key %q is here:", keyText))})
|
|
} else {
|
|
duplicates[keyText] = keyRange
|
|
}
|
|
}
|
|
|
|
p.lexer.Expect(js_lexer.TColon)
|
|
value := p.parseExpr()
|
|
|
|
property := js_ast.Property{
|
|
Kind: js_ast.PropertyField,
|
|
Loc: keyRange.Loc,
|
|
Key: key,
|
|
ValueOrNil: value,
|
|
}
|
|
|
|
// The key "__proto__" must not be a string literal in JavaScript because
|
|
// that actually modifies the prototype of the object. This can be
|
|
// avoided by using a computed property key instead of a string literal.
|
|
if helpers.UTF16EqualsString(keyString, "__proto__") && !p.options.UnsupportedJSFeatures.Has(compat.ObjectExtensions) {
|
|
property.Flags |= js_ast.PropertyIsComputed
|
|
}
|
|
|
|
properties = append(properties, property)
|
|
}
|
|
|
|
if p.lexer.HasNewlineBefore {
|
|
isSingleLine = false
|
|
}
|
|
closeBraceLoc := p.lexer.Loc()
|
|
p.lexer.Expect(js_lexer.TCloseBrace)
|
|
return js_ast.Expr{Loc: loc, Data: &js_ast.EObject{
|
|
Properties: properties,
|
|
IsSingleLine: isSingleLine,
|
|
CloseBraceLoc: closeBraceLoc,
|
|
}}
|
|
|
|
case js_lexer.TBigIntegerLiteral:
|
|
if !p.options.IsForDefine {
|
|
p.lexer.Unexpected()
|
|
}
|
|
value := p.lexer.Identifier
|
|
p.lexer.Next()
|
|
return js_ast.Expr{Loc: loc, Data: &js_ast.EBigInt{Value: value.String}}
|
|
|
|
default:
|
|
p.lexer.Unexpected()
|
|
return js_ast.Expr{}
|
|
}
|
|
}
|
|
|
|
type JSONOptions struct {
|
|
UnsupportedJSFeatures compat.JSFeature
|
|
Flavor js_lexer.JSONFlavor
|
|
ErrorSuffix string
|
|
IsForDefine bool
|
|
}
|
|
|
|
func ParseJSON(log logger.Log, source logger.Source, options JSONOptions) (result js_ast.Expr, ok bool) {
|
|
ok = true
|
|
defer func() {
|
|
r := recover()
|
|
if _, isLexerPanic := r.(js_lexer.LexerPanic); isLexerPanic {
|
|
ok = false
|
|
} else if r != nil {
|
|
panic(r)
|
|
}
|
|
}()
|
|
|
|
if options.ErrorSuffix == "" {
|
|
options.ErrorSuffix = " in JSON"
|
|
}
|
|
|
|
p := &jsonParser{
|
|
log: log,
|
|
source: source,
|
|
tracker: logger.MakeLineColumnTracker(&source),
|
|
options: options,
|
|
lexer: js_lexer.NewLexerJSON(log, source, options.Flavor, options.ErrorSuffix),
|
|
suppressWarningsAboutWeirdCode: helpers.IsInsideNodeModules(source.KeyPath.Text),
|
|
}
|
|
|
|
result = p.parseExpr()
|
|
p.lexer.Expect(js_lexer.TEndOfFile)
|
|
return
|
|
}
|
|
|
|
func isValidJSON(value js_ast.Expr) bool {
|
|
switch e := value.Data.(type) {
|
|
case *js_ast.ENull, *js_ast.EBoolean, *js_ast.EString, *js_ast.ENumber:
|
|
return true
|
|
|
|
case *js_ast.EArray:
|
|
for _, item := range e.Items {
|
|
if !isValidJSON(item) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
|
|
case *js_ast.EObject:
|
|
for _, property := range e.Properties {
|
|
if property.Kind != js_ast.PropertyField || property.Flags.Has(js_ast.PropertyIsComputed) {
|
|
return false
|
|
}
|
|
if _, ok := property.Key.Data.(*js_ast.EString); !ok {
|
|
return false
|
|
}
|
|
if !isValidJSON(property.ValueOrNil) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|