OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 /// Contains the top-level function to parse source maps version 3. |
| 6 library source_maps.parser; |
| 7 |
| 8 import 'dart:json' as json; |
| 9 |
| 10 import 'span.dart'; |
| 11 import 'src/utils.dart'; |
| 12 import 'src/vlq.dart'; |
| 13 |
| 14 /// Parses a source map directly from a json string. |
| 15 // TODO(sigmund): evaluate whether other maps should have the json parsed, or |
| 16 // the string represenation. |
| 17 Mapping parse(String jsonMap, {Map<String, Map> otherMaps}) => |
| 18 parseJson(json.parse(jsonMap), otherMaps: otherMaps); |
| 19 |
| 20 /// Parses a source map directly from a json map object. |
| 21 Mapping parseJson(Map map, {Map<String, Map> otherMaps}) { |
| 22 if (map['version'] != 3) { |
| 23 throw new ArgumentError( |
| 24 'unexpected source map version: ${map["version"]}. ' |
| 25 'Only version 3 is supported.'); |
| 26 } |
| 27 |
| 28 // TODO(sigmund): relax this? dart2js doesn't generate the file entry. |
| 29 if (!map.containsKey('file')) { |
| 30 throw new ArgumentError('missing "file" in source map'); |
| 31 } |
| 32 |
| 33 if (map.containsKey('sections')) { |
| 34 if (map.containsKey('mappings') || map.containsKey('sources') || |
| 35 map.containsKey('names')) { |
| 36 throw new FormatException('map containing "sections" ' |
| 37 'cannot contain "mappings", "sources", or "names".'); |
| 38 } |
| 39 return new MultiSectionMapping.fromJson(map['sections'], otherMaps); |
| 40 } |
| 41 return new SingleMapping.fromJson(map); |
| 42 } |
| 43 |
| 44 |
| 45 /// A mapping parsed our of a source map. |
| 46 abstract class Mapping { |
| 47 Span spanFor(int line, int column, {Map<String, SourceFile> files}); |
| 48 |
| 49 Span spanForLocation(Location loc, {Map<String, SourceFile> files}) { |
| 50 return spanFor(loc.line, loc.column, files: files); |
| 51 } |
| 52 } |
| 53 |
| 54 /// A meta-level map containing sections. |
| 55 class MultiSectionMapping extends Mapping { |
| 56 /// For each section, the start line offset. |
| 57 final List<int> _lineStart = <int>[]; |
| 58 |
| 59 /// For each section, the start column offset. |
| 60 final List<int> _columnStart = <int>[]; |
| 61 |
| 62 /// For each section, the actual source map information, which is not adjusted |
| 63 /// for offsets. |
| 64 final List<Mapping> _maps = <Mapping>[]; |
| 65 |
| 66 /// Creates a section mapping from json. |
| 67 MultiSectionMapping.fromJson(List sections, Map<String, Map> otherMaps) { |
| 68 for (var section in sections) { |
| 69 var offset = section['offset']; |
| 70 if (offset == null) throw new FormatException('section missing offset'); |
| 71 |
| 72 var line = section['offset']['line']; |
| 73 if (line == null) throw new FormatException('offset missing line'); |
| 74 |
| 75 var column = section['offset']['column']; |
| 76 if (column == null) throw new FormatException('offset missing column'); |
| 77 |
| 78 _lineStart.add(line); |
| 79 _columnStart.add(column); |
| 80 |
| 81 var url = section['url']; |
| 82 var map = section['map']; |
| 83 |
| 84 if (url != null && map != null) { |
| 85 throw new FormatException("section can't use both url and map entries"); |
| 86 } else if (url != null) { |
| 87 if (otherMaps == null || otherMaps[url] == null) { |
| 88 throw new FormatException( |
| 89 'section contains refers to $url, but no map was ' |
| 90 'given for it. Make sure a map is passed in "otherMaps"'); |
| 91 } |
| 92 _maps.add(parseJson(otherMaps[url], otherMaps: otherMaps)); |
| 93 } else if (map != null) { |
| 94 _maps.add(parseJson(map, otherMaps: otherMaps)); |
| 95 } else { |
| 96 throw new FormatException('section missing url or map'); |
| 97 } |
| 98 } |
| 99 if (_lineStart.length == 0) { |
| 100 throw new FormatException('expected at least one section'); |
| 101 } |
| 102 } |
| 103 |
| 104 int _indexFor(line, column) { |
| 105 for(int i = 0; i < _lineStart.length; i++) { |
| 106 if (line < _lineStart[i]) return i - 1; |
| 107 if (line == _lineStart[i] && column < _columnStart[i]) return i - 1; |
| 108 } |
| 109 return _lineStart.length - 1; |
| 110 } |
| 111 |
| 112 Span spanFor(int line, int column, {Map<String, SourceFile> files}) { |
| 113 int index = _indexFor(line, column); |
| 114 return _maps[index].spanFor( |
| 115 line - _lineStart[index], column - _columnStart[index], files: files); |
| 116 } |
| 117 |
| 118 String toString() { |
| 119 var buff = new StringBuffer("$runtimeType : ["); |
| 120 for (int i = 0; i < _lineStart.length; i++) { |
| 121 buff..write('(') |
| 122 ..write(_lineStart[i]) |
| 123 ..write(',') |
| 124 ..write(_columnStart[i]) |
| 125 ..write(':') |
| 126 ..write(_maps[i]) |
| 127 ..write(')'); |
| 128 } |
| 129 buff.write(']'); |
| 130 return buff.toString(); |
| 131 } |
| 132 } |
| 133 |
| 134 /// A map containing direct source mappings. |
| 135 // TODO(sigmund): integrate mapping and sourcemap builder? |
| 136 class SingleMapping extends Mapping { |
| 137 /// Url of the target file. |
| 138 final String targetUrl; |
| 139 |
| 140 /// Source urls used in the mapping, indexed by id. |
| 141 final List<String> urls; |
| 142 |
| 143 /// Source names used in the mapping, indexed by id. |
| 144 final List<String> names; |
| 145 |
| 146 /// Entries indicating the beginning of each span. |
| 147 final List<TargetLineEntry> lines = <TargetLineEntry>[]; |
| 148 |
| 149 SingleMapping.fromJson(Map map) |
| 150 : targetUrl = map['file'], |
| 151 // TODO(sigmund): add support for 'sourceRoot' |
| 152 urls = map['sources'], |
| 153 names = map['names'] { |
| 154 int line = 0; |
| 155 int column = 0; |
| 156 int srcUrlId = 0; |
| 157 int srcLine = 0; |
| 158 int srcColumn = 0; |
| 159 int srcNameId = 0; |
| 160 var tokenizer = new _MappingTokenizer(map['mappings']); |
| 161 var entries = <TargetEntry>[]; |
| 162 |
| 163 while (tokenizer.hasTokens) { |
| 164 if (tokenizer.nextKind.isNewLine) { |
| 165 if (!entries.isEmpty) { |
| 166 lines.add(new TargetLineEntry(line, entries)); |
| 167 entries = <TargetEntry>[]; |
| 168 } |
| 169 line++; |
| 170 column = 0; |
| 171 tokenizer._consumeNewLine(); |
| 172 continue; |
| 173 } |
| 174 |
| 175 // Decode the next entry, using the previous encountered values to |
| 176 // decode the relative values. |
| 177 // |
| 178 // We expect 1, 4, or 5 values. If present, values are expected in the |
| 179 // following order: |
| 180 // 0: the starting column in the current line of the generated file |
| 181 // 1: the id of the original source file |
| 182 // 2: the starting line in the original source |
| 183 // 3: the starting column in the original source |
| 184 // 4: the id of the original symbol name |
| 185 // The values are relative to the previous encountered values. |
| 186 if (tokenizer.nextKind.isNewSegment) throw _segmentError(0, line); |
| 187 column += tokenizer._consumeValue(); |
| 188 if (!tokenizer.nextKind.isValue) { |
| 189 entries.add(new TargetEntry(column)); |
| 190 } else { |
| 191 srcUrlId += tokenizer._consumeValue(); |
| 192 if (srcUrlId >= urls.length) { |
| 193 throw new StateError( |
| 194 'Invalid source url id. $targetUrl, $line, $srcUrlId'); |
| 195 } |
| 196 if (!tokenizer.nextKind.isValue) throw _segmentError(2, line); |
| 197 srcLine += tokenizer._consumeValue(); |
| 198 if (!tokenizer.nextKind.isValue) throw _segmentError(3, line); |
| 199 srcColumn += tokenizer._consumeValue(); |
| 200 if (!tokenizer.nextKind.isValue) { |
| 201 entries.add(new TargetEntry(column, srcUrlId, srcLine, srcColumn)); |
| 202 } else { |
| 203 srcNameId += tokenizer._consumeValue(); |
| 204 if (srcNameId >= names.length) { |
| 205 throw new StateError( |
| 206 'Invalid name id: $targetUrl, $line, $srcNameId'); |
| 207 } |
| 208 entries.add( |
| 209 new TargetEntry(column, srcUrlId, srcLine, srcColumn, srcNameId)); |
| 210 } |
| 211 } |
| 212 if (tokenizer.nextKind.isNewSegment) tokenizer._consumeNewSegment(); |
| 213 } |
| 214 if (!entries.isEmpty) { |
| 215 lines.add(new TargetLineEntry(line, entries)); |
| 216 } |
| 217 } |
| 218 |
| 219 _segmentError(int seen, int line) => new StateError( |
| 220 'Invalid entry in sourcemap, expected 1, 4, or 5' |
| 221 ' values, but got $seen.\ntargeturl: $targetUrl, line: $line'); |
| 222 |
| 223 /// Returns [TargetLineEntry] which includes the location in the target [line] |
| 224 /// number. In particular, the resulting entry is the last entry whose line |
| 225 /// number is lower or equal to [line]. |
| 226 TargetLineEntry _findLine(int line) { |
| 227 int index = binarySearch(lines, (e) => e.line > line); |
| 228 return (index <= 0) ? null : lines[index - 1]; |
| 229 } |
| 230 |
| 231 /// Returns [TargetEntry] which includes the location denoted by |
| 232 /// [line], [column]. If [lineEntry] corresponds to [line], then this will be |
| 233 /// the last entry whose column is lower or equal than [column]. If |
| 234 /// [lineEntry] corresponds to a line prior to [line], then the result will be |
| 235 /// the very last entry on that line. |
| 236 TargetEntry _findColumn(int line, int column, TargetLineEntry lineEntry) { |
| 237 if (lineEntry == null || lineEntry.entries.length == 0) return null; |
| 238 if (lineEntry.line != line) return lineEntry.entries.last; |
| 239 var entries = lineEntry.entries; |
| 240 int index = binarySearch(entries, (e) => e.column > column); |
| 241 return (index <= 0) ? null : entries[index - 1]; |
| 242 } |
| 243 |
| 244 Span spanFor(int line, int column, {Map<String, SourceFile> files}) { |
| 245 var lineEntry = _findLine(line); |
| 246 var entry = _findColumn(line, column, _findLine(line)); |
| 247 if (entry == null) return null; |
| 248 var url = urls[entry.sourceUrlId]; |
| 249 if (files != null && files[url] != null) { |
| 250 var file = files[url]; |
| 251 var start = file.getOffset(entry.sourceLine, entry.sourceColumn); |
| 252 if (entry.sourceNameId != null) { |
| 253 var text = names[entry.sourceNameId]; |
| 254 return new FileSpan(files[url], start, start + text.length, true); |
| 255 } else { |
| 256 return new FileSpan(files[url], start); |
| 257 } |
| 258 } else { |
| 259 // Offset and other context is not available. |
| 260 if (entry.sourceNameId != null) { |
| 261 return new FixedSpan(url, 0, entry.sourceLine, entry.sourceColumn, |
| 262 text: names[entry.sourceNameId], isIdentifier: true); |
| 263 } else { |
| 264 return new FixedSpan(url, 0, entry.sourceLine, entry.sourceColumn); |
| 265 } |
| 266 } |
| 267 } |
| 268 |
| 269 String toString() { |
| 270 return (new StringBuffer("$runtimeType : [") |
| 271 ..write('targetUrl: ') |
| 272 ..write(targetUrl) |
| 273 ..write(', urls: ') |
| 274 ..write(urls) |
| 275 ..write(', names: ') |
| 276 ..write(names) |
| 277 ..write(', lines: ') |
| 278 ..write(lines) |
| 279 ..write(']')).toString(); |
| 280 } |
| 281 |
| 282 String get debugString { |
| 283 var buff = new StringBuffer(); |
| 284 for (var lineEntry in lines) { |
| 285 var line = lineEntry.line; |
| 286 for (var entry in lineEntry.entries) { |
| 287 buff..write(targetUrl) |
| 288 ..write(': ') |
| 289 ..write(line) |
| 290 ..write(':') |
| 291 ..write(entry.column) |
| 292 ..write(' --> ') |
| 293 ..write(urls[entry.sourceUrlId]) |
| 294 ..write(': ') |
| 295 ..write(entry.sourceLine) |
| 296 ..write(':') |
| 297 ..write(entry.sourceColumn); |
| 298 if (entry.sourceNameId != null) { |
| 299 buff..write(' (') |
| 300 ..write(names[entry.sourceNameId]) |
| 301 ..write(')'); |
| 302 } |
| 303 buff.write('\n'); |
| 304 } |
| 305 } |
| 306 return buff.toString(); |
| 307 } |
| 308 } |
| 309 |
| 310 /// A line entry read from a source map. |
| 311 class TargetLineEntry { |
| 312 final int line; |
| 313 List<TargetEntry> entries = <TargetEntry>[]; |
| 314 TargetLineEntry(this.line, this.entries); |
| 315 |
| 316 String toString() => '$runtimeType: $line $entries'; |
| 317 } |
| 318 |
| 319 /// A target segment entry read from a source map |
| 320 class TargetEntry { |
| 321 final int column; |
| 322 final int sourceUrlId; |
| 323 final int sourceLine; |
| 324 final int sourceColumn; |
| 325 final int sourceNameId; |
| 326 TargetEntry(this.column, [this.sourceUrlId, this.sourceLine, |
| 327 this.sourceColumn, this.sourceNameId]); |
| 328 |
| 329 String toString() => '$runtimeType: ' |
| 330 '($column, $sourceUrlId, $sourceLine, $sourceColumn, $sourceNameId)'; |
| 331 } |
| 332 |
| 333 /** A character iterator over a string that can peek one character ahead. */ |
| 334 class _MappingTokenizer implements Iterator<String> { |
| 335 final String _internal; |
| 336 final int _length; |
| 337 int index = -1; |
| 338 _MappingTokenizer(String internal) |
| 339 : _internal = internal, |
| 340 _length = internal.length; |
| 341 |
| 342 // Iterator API is used by decodeVlq to consume VLQ entries. |
| 343 bool moveNext() => ++index < _length; |
| 344 String get current => |
| 345 (index >= 0 && index < _length) ? _internal[index] : null; |
| 346 |
| 347 bool get hasTokens => index < _length - 1 && _length > 0; |
| 348 |
| 349 _TokenKind get nextKind { |
| 350 if (!hasTokens) return _TokenKind.EOF; |
| 351 var next = _internal[index + 1]; |
| 352 if (next == ';') return _TokenKind.LINE; |
| 353 if (next == ',') return _TokenKind.SEGMENT; |
| 354 return _TokenKind.VALUE; |
| 355 } |
| 356 |
| 357 int _consumeValue() => decodeVlq(this); |
| 358 void _consumeNewLine() { ++index; } |
| 359 void _consumeNewSegment() { ++index; } |
| 360 |
| 361 // Print the state of the iterator, with colors indicating the current |
| 362 // position. |
| 363 String toString() { |
| 364 var buff = new StringBuffer(); |
| 365 for (int i = 0; i < index; i++) { |
| 366 buff.write(_internal[i]); |
| 367 } |
| 368 buff.write('[31m'); |
| 369 buff.write(current == null ? '' : current); |
| 370 buff.write('[0m'); |
| 371 for (int i = index + 1; i < _internal.length; i++) { |
| 372 buff.write(_internal[i]); |
| 373 } |
| 374 buff.write(' ($index)'); |
| 375 return buff.toString(); |
| 376 } |
| 377 } |
| 378 |
| 379 class _TokenKind { |
| 380 static const _TokenKind LINE = const _TokenKind(isNewLine: true); |
| 381 static const _TokenKind SEGMENT = const _TokenKind(isNewSegment: true); |
| 382 static const _TokenKind EOF = const _TokenKind(isEof: true); |
| 383 static const _TokenKind VALUE = const _TokenKind(); |
| 384 final bool isNewLine; |
| 385 final bool isNewSegment; |
| 386 final bool isEof; |
| 387 bool get isValue => !isNewLine && !isNewSegment && !isEof; |
| 388 |
| 389 const _TokenKind( |
| 390 {this.isNewLine: false, this.isNewSegment: false, this.isEof: false}); |
| 391 } |
OLD | NEW |