github.com/nuvolaris/goja@v0.0.0-20230825100449-967811910c6d/file/file.go (about)

     1  // Package file encapsulates the file abstractions used by the ast & parser.
     2  package file
     3  
     4  import (
     5  	"fmt"
     6  	"net/url"
     7  	"path"
     8  	"sort"
     9  	"strings"
    10  	"sync"
    11  
    12  	"github.com/go-sourcemap/sourcemap"
    13  )
    14  
    15  // Idx is a compact encoding of a source position within a file set.
    16  // It can be converted into a Position for a more convenient, but much
    17  // larger, representation.
    18  type Idx int
    19  
    20  // Position describes an arbitrary source position
    21  // including the filename, line, and column location.
    22  type Position struct {
    23  	Filename string // The filename where the error occurred, if any
    24  	Line     int    // The line number, starting at 1
    25  	Column   int    // The column number, starting at 1 (The character count)
    26  
    27  }
    28  
    29  // A Position is valid if the line number is > 0.
    30  
    31  func (self *Position) isValid() bool {
    32  	return self.Line > 0
    33  }
    34  
    35  // String returns a string in one of several forms:
    36  //
    37  //	file:line:column    A valid position with filename
    38  //	line:column         A valid position without filename
    39  //	file                An invalid position with filename
    40  //	-                   An invalid position without filename
    41  func (self Position) String() string {
    42  	str := self.Filename
    43  	if self.isValid() {
    44  		if str != "" {
    45  			str += ":"
    46  		}
    47  		str += fmt.Sprintf("%d:%d", self.Line, self.Column)
    48  	}
    49  	if str == "" {
    50  		str = "-"
    51  	}
    52  	return str
    53  }
    54  
    55  // FileSet
    56  
    57  // A FileSet represents a set of source files.
    58  type FileSet struct {
    59  	files []*File
    60  	last  *File
    61  }
    62  
    63  // AddFile adds a new file with the given filename and src.
    64  //
    65  // This an internal method, but exported for cross-package use.
    66  func (self *FileSet) AddFile(filename, src string) int {
    67  	base := self.nextBase()
    68  	file := &File{
    69  		name: filename,
    70  		src:  src,
    71  		base: base,
    72  	}
    73  	self.files = append(self.files, file)
    74  	self.last = file
    75  	return base
    76  }
    77  
    78  func (self *FileSet) nextBase() int {
    79  	if self.last == nil {
    80  		return 1
    81  	}
    82  	return self.last.base + len(self.last.src) + 1
    83  }
    84  
    85  func (self *FileSet) File(idx Idx) *File {
    86  	for _, file := range self.files {
    87  		if idx <= Idx(file.base+len(file.src)) {
    88  			return file
    89  		}
    90  	}
    91  	return nil
    92  }
    93  
    94  // Position converts an Idx in the FileSet into a Position.
    95  func (self *FileSet) Position(idx Idx) Position {
    96  	for _, file := range self.files {
    97  		if idx <= Idx(file.base+len(file.src)) {
    98  			return file.Position(int(idx) - file.base)
    99  		}
   100  	}
   101  	return Position{}
   102  }
   103  
   104  type File struct {
   105  	mu                sync.Mutex
   106  	name              string
   107  	src               string
   108  	base              int // This will always be 1 or greater
   109  	sourceMap         *sourcemap.Consumer
   110  	lineOffsets       []int
   111  	lastScannedOffset int
   112  }
   113  
   114  func NewFile(filename, src string, base int) *File {
   115  	return &File{
   116  		name: filename,
   117  		src:  src,
   118  		base: base,
   119  	}
   120  }
   121  
   122  func (fl *File) Name() string {
   123  	return fl.name
   124  }
   125  
   126  func (fl *File) Source() string {
   127  	return fl.src
   128  }
   129  
   130  func (fl *File) Base() int {
   131  	return fl.base
   132  }
   133  
   134  func (fl *File) SetSourceMap(m *sourcemap.Consumer) {
   135  	fl.sourceMap = m
   136  }
   137  
   138  func (fl *File) Position(offset int) Position {
   139  	var line int
   140  	var lineOffsets []int
   141  	fl.mu.Lock()
   142  	if offset > fl.lastScannedOffset {
   143  		line = fl.scanTo(offset)
   144  		lineOffsets = fl.lineOffsets
   145  		fl.mu.Unlock()
   146  	} else {
   147  		lineOffsets = fl.lineOffsets
   148  		fl.mu.Unlock()
   149  		line = sort.Search(len(lineOffsets), func(x int) bool { return lineOffsets[x] > offset }) - 1
   150  	}
   151  
   152  	var lineStart int
   153  	if line >= 0 {
   154  		lineStart = lineOffsets[line]
   155  	}
   156  
   157  	row := line + 2
   158  	col := offset - lineStart + 1
   159  
   160  	if fl.sourceMap != nil {
   161  		if source, _, row, col, ok := fl.sourceMap.Source(row, col); ok {
   162  			sourceUrlStr := source
   163  			sourceURL := ResolveSourcemapURL(fl.Name(), source)
   164  			if sourceURL != nil {
   165  				sourceUrlStr = sourceURL.String()
   166  			}
   167  
   168  			return Position{
   169  				Filename: sourceUrlStr,
   170  				Line:     row,
   171  				Column:   col,
   172  			}
   173  		}
   174  	}
   175  
   176  	return Position{
   177  		Filename: fl.name,
   178  		Line:     row,
   179  		Column:   col,
   180  	}
   181  }
   182  
   183  func ResolveSourcemapURL(basename, source string) *url.URL {
   184  	// if the url is absolute(has scheme) there is nothing to do
   185  	smURL, err := url.Parse(strings.TrimSpace(source))
   186  	if err == nil && !smURL.IsAbs() {
   187  		baseURL, err1 := url.Parse(strings.TrimSpace(basename))
   188  		if err1 == nil && path.IsAbs(baseURL.Path) {
   189  			smURL = baseURL.ResolveReference(smURL)
   190  		} else {
   191  			// pathological case where both are not absolute paths and using Resolve
   192  			// as above will produce an absolute one
   193  			smURL, _ = url.Parse(path.Join(path.Dir(basename), smURL.Path))
   194  		}
   195  	}
   196  	return smURL
   197  }
   198  
   199  func findNextLineStart(s string) int {
   200  	for pos, ch := range s {
   201  		switch ch {
   202  		case '\r':
   203  			if pos < len(s)-1 && s[pos+1] == '\n' {
   204  				return pos + 2
   205  			}
   206  			return pos + 1
   207  		case '\n':
   208  			return pos + 1
   209  		case '\u2028', '\u2029':
   210  			return pos + 3
   211  		}
   212  	}
   213  	return -1
   214  }
   215  
   216  func (fl *File) scanTo(offset int) int {
   217  	o := fl.lastScannedOffset
   218  	for o < offset {
   219  		p := findNextLineStart(fl.src[o:])
   220  		if p == -1 {
   221  			fl.lastScannedOffset = len(fl.src)
   222  			return len(fl.lineOffsets) - 1
   223  		}
   224  		o = o + p
   225  		fl.lineOffsets = append(fl.lineOffsets, o)
   226  	}
   227  	fl.lastScannedOffset = o
   228  
   229  	if o == offset {
   230  		return len(fl.lineOffsets) - 1
   231  	}
   232  
   233  	return len(fl.lineOffsets) - 2
   234  }