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