Files

437 lines
13 KiB
Go

package js_parser
import (
"fmt"
"net/url"
"sort"
"strings"
"github.com/evanw/esbuild/internal/ast"
"github.com/evanw/esbuild/internal/helpers"
"github.com/evanw/esbuild/internal/js_ast"
"github.com/evanw/esbuild/internal/logger"
"github.com/evanw/esbuild/internal/sourcemap"
)
// New specification: https://tc39.es/ecma426/
// Old specification: https://sourcemaps.info/spec.html
func ParseSourceMap(log logger.Log, source logger.Source) *sourcemap.SourceMap {
expr, ok := ParseJSON(log, source, JSONOptions{ErrorSuffix: " in source map"})
if !ok {
return nil
}
obj, ok := expr.Data.(*js_ast.EObject)
tracker := logger.MakeLineColumnTracker(&source)
if !ok {
log.AddError(&tracker, logger.Range{Loc: expr.Loc}, "Invalid source map")
return nil
}
type sourceMapSection struct {
lineOffset int32
columnOffset int32
sourceMap *js_ast.EObject
}
var sections []sourceMapSection
hasSections := false
for _, prop := range obj.Properties {
if !helpers.UTF16EqualsString(prop.Key.Data.(*js_ast.EString).Value, "sections") {
continue
}
if value, ok := prop.ValueOrNil.Data.(*js_ast.EArray); ok {
for _, item := range value.Items {
if element, ok := item.Data.(*js_ast.EObject); ok {
var sectionLineOffset int32
var sectionColumnOffset int32
var sectionSourceMap *js_ast.EObject
for _, sectionProp := range element.Properties {
switch helpers.UTF16ToString(sectionProp.Key.Data.(*js_ast.EString).Value) {
case "offset":
if offsetValue, ok := sectionProp.ValueOrNil.Data.(*js_ast.EObject); ok {
for _, offsetProp := range offsetValue.Properties {
switch helpers.UTF16ToString(offsetProp.Key.Data.(*js_ast.EString).Value) {
case "line":
if lineValue, ok := offsetProp.ValueOrNil.Data.(*js_ast.ENumber); ok {
sectionLineOffset = int32(lineValue.Value)
}
case "column":
if columnValue, ok := offsetProp.ValueOrNil.Data.(*js_ast.ENumber); ok {
sectionColumnOffset = int32(columnValue.Value)
}
}
}
} else {
log.AddError(&tracker, logger.Range{Loc: sectionProp.ValueOrNil.Loc}, "Expected \"offset\" to be an object")
return nil
}
case "map":
if mapValue, ok := sectionProp.ValueOrNil.Data.(*js_ast.EObject); ok {
sectionSourceMap = mapValue
} else {
log.AddError(&tracker, logger.Range{Loc: sectionProp.ValueOrNil.Loc}, "Expected \"map\" to be an object")
return nil
}
}
}
if sectionSourceMap != nil {
sections = append(sections, sourceMapSection{
lineOffset: sectionLineOffset,
columnOffset: sectionColumnOffset,
sourceMap: sectionSourceMap,
})
}
}
}
} else {
log.AddError(&tracker, logger.Range{Loc: prop.ValueOrNil.Loc}, "Expected \"sections\" to be an array")
return nil
}
hasSections = true
break
}
if !hasSections {
sections = append(sections, sourceMapSection{
sourceMap: obj,
})
}
var sources []string
var sourcesContent []sourcemap.SourceContent
var names []string
var mappings mappingArray
var generatedLine int32
var generatedColumn int32
needSort := false
for _, section := range sections {
var sourcesArray []js_ast.Expr
var sourcesContentArray []js_ast.Expr
var namesArray []js_ast.Expr
var mappingsRaw []uint16
var mappingsStart int32
var sourceRoot string
hasVersion := false
for _, prop := range section.sourceMap.Properties {
switch helpers.UTF16ToString(prop.Key.Data.(*js_ast.EString).Value) {
case "version":
if value, ok := prop.ValueOrNil.Data.(*js_ast.ENumber); ok && value.Value == 3 {
hasVersion = true
}
case "mappings":
if value, ok := prop.ValueOrNil.Data.(*js_ast.EString); ok {
mappingsRaw = value.Value
mappingsStart = prop.ValueOrNil.Loc.Start + 1
}
case "sourceRoot":
if value, ok := prop.ValueOrNil.Data.(*js_ast.EString); ok {
sourceRoot = helpers.UTF16ToString(value.Value)
}
case "sources":
if value, ok := prop.ValueOrNil.Data.(*js_ast.EArray); ok {
sourcesArray = value.Items
}
case "sourcesContent":
if value, ok := prop.ValueOrNil.Data.(*js_ast.EArray); ok {
sourcesContentArray = value.Items
}
case "names":
if value, ok := prop.ValueOrNil.Data.(*js_ast.EArray); ok {
namesArray = value.Items
}
}
}
// Silently ignore the section if the version was missing or incorrect
if !hasVersion {
continue
}
mappingsLen := len(mappingsRaw)
sourcesLen := len(sourcesArray)
namesLen := len(namesArray)
// Silently ignore the section if the source map is pointless (i.e. empty)
if mappingsLen == 0 || sourcesLen == 0 {
continue
}
if section.lineOffset < generatedLine || (section.lineOffset == generatedLine && section.columnOffset < generatedColumn) {
needSort = true
}
lineOffset := section.lineOffset
columnOffset := section.columnOffset
sourceOffset := int32(len(sources))
nameOffset := int32(len(names))
generatedLine = lineOffset
generatedColumn = columnOffset
sourceIndex := sourceOffset
var originalLine int32
var originalColumn int32
originalName := nameOffset
current := 0
errorText := ""
errorLen := 0
// Parse the mappings
for current < mappingsLen {
// Handle a line break
if mappingsRaw[current] == ';' {
generatedLine++
generatedColumn = 0
current++
continue
}
// Read the generated column
generatedColumnDelta, i, ok := sourcemap.DecodeVLQUTF16(mappingsRaw[current:])
if !ok {
errorText = "Missing generated column"
errorLen = i
break
}
if generatedColumnDelta < 0 {
// This would mess up binary search
needSort = true
}
generatedColumn += generatedColumnDelta
if (generatedLine == lineOffset && generatedColumn < columnOffset) || generatedColumn < 0 {
errorText = fmt.Sprintf("Invalid generated column value: %d", generatedColumn)
errorLen = i
break
}
current += i
// According to the specification, it's valid for a mapping to have 1,
// 4, or 5 variable-length fields. Having one field means there's no
// original location information, which is pretty useless. Just ignore
// those entries.
if current == mappingsLen {
break
}
switch mappingsRaw[current] {
case ',':
current++
continue
case ';':
continue
}
// Read the original source
sourceIndexDelta, i, ok := sourcemap.DecodeVLQUTF16(mappingsRaw[current:])
if !ok {
errorText = "Missing source index"
errorLen = i
break
}
sourceIndex += sourceIndexDelta
if sourceIndex < sourceOffset || sourceIndex >= sourceOffset+int32(sourcesLen) {
errorText = fmt.Sprintf("Invalid source index value: %d", sourceIndex)
errorLen = i
break
}
current += i
// Read the original line
originalLineDelta, i, ok := sourcemap.DecodeVLQUTF16(mappingsRaw[current:])
if !ok {
errorText = "Missing original line"
errorLen = i
break
}
originalLine += originalLineDelta
if originalLine < 0 {
errorText = fmt.Sprintf("Invalid original line value: %d", originalLine)
errorLen = i
break
}
current += i
// Read the original column
originalColumnDelta, i, ok := sourcemap.DecodeVLQUTF16(mappingsRaw[current:])
if !ok {
errorText = "Missing original column"
errorLen = i
break
}
originalColumn += originalColumnDelta
if originalColumn < 0 {
errorText = fmt.Sprintf("Invalid original column value: %d", originalColumn)
errorLen = i
break
}
current += i
// Read the original name
var optionalName ast.Index32
if originalNameDelta, i, ok := sourcemap.DecodeVLQUTF16(mappingsRaw[current:]); ok {
originalName += originalNameDelta
if originalName < nameOffset || originalName >= nameOffset+int32(namesLen) {
errorText = fmt.Sprintf("Invalid name index value: %d", originalName)
errorLen = i
break
}
optionalName = ast.MakeIndex32(uint32(originalName))
current += i
}
// Handle the next character
if current < mappingsLen {
if c := mappingsRaw[current]; c == ',' {
current++
} else if c != ';' {
errorText = fmt.Sprintf("Invalid character after mapping: %q",
helpers.UTF16ToString(mappingsRaw[current:current+1]))
errorLen = 1
break
}
}
mappings = append(mappings, sourcemap.Mapping{
GeneratedLine: generatedLine,
GeneratedColumn: generatedColumn,
SourceIndex: sourceIndex,
OriginalLine: originalLine,
OriginalColumn: originalColumn,
OriginalName: optionalName,
})
}
if errorText != "" {
r := logger.Range{Loc: logger.Loc{Start: mappingsStart + int32(current)}, Len: int32(errorLen)}
log.AddID(logger.MsgID_SourceMap_InvalidSourceMappings, logger.Warning, &tracker, r,
fmt.Sprintf("Bad \"mappings\" data in source map at character %d: %s", current, errorText))
return nil
}
// Try resolving relative source URLs into absolute source URLs.
// See https://tc39.es/ecma426/#resolving-sources for details.
var sourceURLPrefix string
var baseURL *url.URL
if sourceRoot != "" {
if index := strings.LastIndexByte(sourceRoot, '/'); index != -1 {
sourceURLPrefix = sourceRoot[:index+1]
} else {
sourceURLPrefix = sourceRoot + "/"
}
}
if source.KeyPath.Namespace == "file" {
baseURL = helpers.FileURLFromFilePath(source.KeyPath.Text)
}
for _, item := range sourcesArray {
if element, ok := item.Data.(*js_ast.EString); ok {
sourcePath := sourceURLPrefix + helpers.UTF16ToString(element.Value)
sourceURL, err := url.Parse(sourcePath)
// Ignore URL parse errors (such as "%XY" being an invalid escape)
if err != nil {
sources = append(sources, sourcePath)
continue
}
// Resolve this URL relative to the enclosing directory
if baseURL != nil {
sourceURL = baseURL.ResolveReference(sourceURL)
}
sources = append(sources, sourceURL.String())
} else {
sources = append(sources, "")
}
}
if len(sourcesContentArray) > 0 {
// It's possible that one of the source maps inside "sections" has
// different lengths for the "sources" and "sourcesContent" arrays.
// This is bad because we need to us a single index to get the name
// of the source from "sources[i]" and the content of the source
// from "sourcesContent[i]".
//
// So if a previous source map had a shorter "sourcesContent" array
// than its "sources" array (or if the previous source map just had
// no "sourcesContent" array), expand our aggregated array to the
// right length by padding it out with empty entries.
sourcesContent = append(sourcesContent, make([]sourcemap.SourceContent, int(sourceOffset)-len(sourcesContent))...)
for i, item := range sourcesContentArray {
// Make sure we don't ever record more "sourcesContent" entries
// than there are "sources" entries, which is possible because
// these are two separate arrays in the source map JSON. We need
// to avoid this because that would mess up our shared indexing
// of the "sources" and "sourcesContent" arrays. See the above
// comment for more details.
if i == sourcesLen {
break
}
if element, ok := item.Data.(*js_ast.EString); ok {
sourcesContent = append(sourcesContent, sourcemap.SourceContent{
Value: element.Value,
Quoted: source.TextForRange(source.RangeOfString(item.Loc)),
})
} else {
sourcesContent = append(sourcesContent, sourcemap.SourceContent{})
}
}
}
for _, item := range namesArray {
if element, ok := item.Data.(*js_ast.EString); ok {
names = append(names, helpers.UTF16ToString(element.Value))
} else {
names = append(names, "")
}
}
}
// Silently fail if the source map is pointless (i.e. empty)
if len(sources) == 0 || len(mappings) == 0 {
return nil
}
if needSort {
// If we get here, some mappings are out of order. Lines can't be out of
// order by construction but columns can. This is a pretty rare situation
// because almost all source map generators always write out mappings in
// order as they write the output instead of scrambling the order.
sort.Stable(mappings)
}
return &sourcemap.SourceMap{
Sources: sources,
SourcesContent: sourcesContent,
Mappings: mappings,
Names: names,
}
}
// This type is just so we can use Go's native sort function
type mappingArray []sourcemap.Mapping
func (a mappingArray) Len() int { return len(a) }
func (a mappingArray) Swap(i int, j int) { a[i], a[j] = a[j], a[i] }
func (a mappingArray) Less(i int, j int) bool {
ai := a[i]
aj := a[j]
return ai.GeneratedLine < aj.GeneratedLine || (ai.GeneratedLine == aj.GeneratedLine && ai.GeneratedColumn <= aj.GeneratedColumn)
}