| 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:collection'; | |
| 9 import 'dart:convert'; | |
| 10 | |
| 11 import 'package:source_span/source_span.dart'; | |
| 12 | |
| 13 import 'builder.dart' as builder; | |
| 14 import 'src/source_map_span.dart'; | |
| 15 import 'src/utils.dart'; | |
| 16 import 'src/vlq.dart'; | |
| 17 | |
| 18 /// Parses a source map directly from a json string. | |
| 19 // TODO(sigmund): evaluate whether other maps should have the json parsed, or | |
| 20 // the string represenation. | |
| 21 // TODO(tjblasi): Ignore the first line of [jsonMap] if the JSON safety string | |
| 22 // `)]}'` begins the string representation of the map. | |
| 23 Mapping parse(String jsonMap, {Map<String, Map> otherMaps}) => | |
| 24 parseJson(JSON.decode(jsonMap), otherMaps: otherMaps); | |
| 25 | |
| 26 /// Parses a source map directly from a json map object. | |
| 27 Mapping parseJson(Map map, {Map<String, Map> otherMaps}) { | |
| 28 if (map['version'] != 3) { | |
| 29 throw new ArgumentError( | |
| 30 'unexpected source map version: ${map["version"]}. ' | |
| 31 'Only version 3 is supported.'); | |
| 32 } | |
| 33 | |
| 34 if (map.containsKey('sections')) { | |
| 35 if (map.containsKey('mappings') || map.containsKey('sources') || | |
| 36 map.containsKey('names')) { | |
| 37 throw new FormatException('map containing "sections" ' | |
| 38 'cannot contain "mappings", "sources", or "names".'); | |
| 39 } | |
| 40 return new MultiSectionMapping.fromJson(map['sections'], otherMaps); | |
| 41 } | |
| 42 return new SingleMapping.fromJson(map); | |
| 43 } | |
| 44 | |
| 45 | |
| 46 /// A mapping parsed out of a source map. | |
| 47 abstract class Mapping { | |
| 48 /// Returns the span associated with [line] and [column]. | |
| 49 SourceMapSpan spanFor(int line, int column, {Map<String, SourceFile> files}); | |
| 50 | |
| 51 /// Returns the span associated with [location]. | |
| 52 SourceMapSpan spanForLocation(SourceLocation location, | |
| 53 {Map<String, SourceFile> files}) { | |
| 54 return spanFor(location.line, location.column, files: files); | |
| 55 } | |
| 56 } | |
| 57 | |
| 58 /// A meta-level map containing sections. | |
| 59 class MultiSectionMapping extends Mapping { | |
| 60 /// For each section, the start line offset. | |
| 61 final List<int> _lineStart = <int>[]; | |
| 62 | |
| 63 /// For each section, the start column offset. | |
| 64 final List<int> _columnStart = <int>[]; | |
| 65 | |
| 66 /// For each section, the actual source map information, which is not adjusted | |
| 67 /// for offsets. | |
| 68 final List<Mapping> _maps = <Mapping>[]; | |
| 69 | |
| 70 /// Creates a section mapping from json. | |
| 71 MultiSectionMapping.fromJson(List sections, Map<String, Map> otherMaps) { | |
| 72 for (var section in sections) { | |
| 73 var offset = section['offset']; | |
| 74 if (offset == null) throw new FormatException('section missing offset'); | |
| 75 | |
| 76 var line = section['offset']['line']; | |
| 77 if (line == null) throw new FormatException('offset missing line'); | |
| 78 | |
| 79 var column = section['offset']['column']; | |
| 80 if (column == null) throw new FormatException('offset missing column'); | |
| 81 | |
| 82 _lineStart.add(line); | |
| 83 _columnStart.add(column); | |
| 84 | |
| 85 var url = section['url']; | |
| 86 var map = section['map']; | |
| 87 | |
| 88 if (url != null && map != null) { | |
| 89 throw new FormatException("section can't use both url and map entries"); | |
| 90 } else if (url != null) { | |
| 91 if (otherMaps == null || otherMaps[url] == null) { | |
| 92 throw new FormatException( | |
| 93 'section contains refers to $url, but no map was ' | |
| 94 'given for it. Make sure a map is passed in "otherMaps"'); | |
| 95 } | |
| 96 _maps.add(parseJson(otherMaps[url], otherMaps: otherMaps)); | |
| 97 } else if (map != null) { | |
| 98 _maps.add(parseJson(map, otherMaps: otherMaps)); | |
| 99 } else { | |
| 100 throw new FormatException('section missing url or map'); | |
| 101 } | |
| 102 } | |
| 103 if (_lineStart.length == 0) { | |
| 104 throw new FormatException('expected at least one section'); | |
| 105 } | |
| 106 } | |
| 107 | |
| 108 int _indexFor(line, column) { | |
| 109 for(int i = 0; i < _lineStart.length; i++) { | |
| 110 if (line < _lineStart[i]) return i - 1; | |
| 111 if (line == _lineStart[i] && column < _columnStart[i]) return i - 1; | |
| 112 } | |
| 113 return _lineStart.length - 1; | |
| 114 } | |
| 115 | |
| 116 SourceMapSpan spanFor(int line, int column, {Map<String, SourceFile> files}) { | |
| 117 int index = _indexFor(line, column); | |
| 118 return _maps[index].spanFor( | |
| 119 line - _lineStart[index], column - _columnStart[index], files: files); | |
| 120 } | |
| 121 | |
| 122 String toString() { | |
| 123 var buff = new StringBuffer("$runtimeType : ["); | |
| 124 for (int i = 0; i < _lineStart.length; i++) { | |
| 125 buff..write('(') | |
| 126 ..write(_lineStart[i]) | |
| 127 ..write(',') | |
| 128 ..write(_columnStart[i]) | |
| 129 ..write(':') | |
| 130 ..write(_maps[i]) | |
| 131 ..write(')'); | |
| 132 } | |
| 133 buff.write(']'); | |
| 134 return buff.toString(); | |
| 135 } | |
| 136 } | |
| 137 | |
| 138 /// A map containing direct source mappings. | |
| 139 class SingleMapping extends Mapping { | |
| 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; | |
| 148 | |
| 149 /// Url of the target file. | |
| 150 String targetUrl; | |
| 151 | |
| 152 /// Source root appended to the start of all entries in [urls]. | |
| 153 String sourceRoot; | |
| 154 | |
| 155 SingleMapping._(this.targetUrl, this.urls, this.names, this.lines); | |
| 156 | |
| 157 factory SingleMapping.fromEntries( | |
| 158 Iterable<builder.Entry> entries, [String fileUrl]) { | |
| 159 // The entries needs to be sorted by the target offsets. | |
| 160 var sourceEntries = new List.from(entries)..sort(); | |
| 161 var lines = <TargetLineEntry>[]; | |
| 162 | |
| 163 // Indices associated with file urls that will be part of the source map. We | |
| 164 // use a linked hash-map so that `_urls.keys[_urls[u]] == u` | |
| 165 var urls = new LinkedHashMap<String, int>(); | |
| 166 | |
| 167 // Indices associated with identifiers that will be part of the source map. | |
| 168 // We use a linked hash-map so that `_names.keys[_names[n]] == n` | |
| 169 var names = new LinkedHashMap<String, int>(); | |
| 170 | |
| 171 var lineNum; | |
| 172 var targetEntries; | |
| 173 for (var sourceEntry in sourceEntries) { | |
| 174 if (lineNum == null || sourceEntry.target.line > lineNum) { | |
| 175 lineNum = sourceEntry.target.line; | |
| 176 targetEntries = <TargetEntry>[]; | |
| 177 lines.add(new TargetLineEntry(lineNum, targetEntries)); | |
| 178 } | |
| 179 | |
| 180 if (sourceEntry.source == null) { | |
| 181 targetEntries.add(new TargetEntry(sourceEntry.target.column)); | |
| 182 } else { | |
| 183 var sourceUrl = sourceEntry.source.sourceUrl; | |
| 184 var urlId = urls.putIfAbsent( | |
| 185 sourceUrl == null ? '' : sourceUrl.toString(), () => urls.length); | |
| 186 var srcNameId = sourceEntry.identifierName == null ? null : | |
| 187 names.putIfAbsent(sourceEntry.identifierName, () => names.length); | |
| 188 targetEntries.add(new TargetEntry( | |
| 189 sourceEntry.target.column, | |
| 190 urlId, | |
| 191 sourceEntry.source.line, | |
| 192 sourceEntry.source.column, | |
| 193 srcNameId)); | |
| 194 } | |
| 195 } | |
| 196 return new SingleMapping._( | |
| 197 fileUrl, urls.keys.toList(), names.keys.toList(), lines); | |
| 198 } | |
| 199 | |
| 200 SingleMapping.fromJson(Map map) | |
| 201 : targetUrl = map['file'], | |
| 202 urls = map['sources'], | |
| 203 names = map['names'], | |
| 204 sourceRoot = map['sourceRoot'], | |
| 205 lines = <TargetLineEntry>[] { | |
| 206 int line = 0; | |
| 207 int column = 0; | |
| 208 int srcUrlId = 0; | |
| 209 int srcLine = 0; | |
| 210 int srcColumn = 0; | |
| 211 int srcNameId = 0; | |
| 212 var tokenizer = new _MappingTokenizer(map['mappings']); | |
| 213 var entries = <TargetEntry>[]; | |
| 214 | |
| 215 while (tokenizer.hasTokens) { | |
| 216 if (tokenizer.nextKind.isNewLine) { | |
| 217 if (!entries.isEmpty) { | |
| 218 lines.add(new TargetLineEntry(line, entries)); | |
| 219 entries = <TargetEntry>[]; | |
| 220 } | |
| 221 line++; | |
| 222 column = 0; | |
| 223 tokenizer._consumeNewLine(); | |
| 224 continue; | |
| 225 } | |
| 226 | |
| 227 // Decode the next entry, using the previous encountered values to | |
| 228 // decode the relative values. | |
| 229 // | |
| 230 // We expect 1, 4, or 5 values. If present, values are expected in the | |
| 231 // following order: | |
| 232 // 0: the starting column in the current line of the generated file | |
| 233 // 1: the id of the original source file | |
| 234 // 2: the starting line in the original source | |
| 235 // 3: the starting column in the original source | |
| 236 // 4: the id of the original symbol name | |
| 237 // The values are relative to the previous encountered values. | |
| 238 if (tokenizer.nextKind.isNewSegment) throw _segmentError(0, line); | |
| 239 column += tokenizer._consumeValue(); | |
| 240 if (!tokenizer.nextKind.isValue) { | |
| 241 entries.add(new TargetEntry(column)); | |
| 242 } else { | |
| 243 srcUrlId += tokenizer._consumeValue(); | |
| 244 if (srcUrlId >= urls.length) { | |
| 245 throw new StateError( | |
| 246 'Invalid source url id. $targetUrl, $line, $srcUrlId'); | |
| 247 } | |
| 248 if (!tokenizer.nextKind.isValue) throw _segmentError(2, line); | |
| 249 srcLine += tokenizer._consumeValue(); | |
| 250 if (!tokenizer.nextKind.isValue) throw _segmentError(3, line); | |
| 251 srcColumn += tokenizer._consumeValue(); | |
| 252 if (!tokenizer.nextKind.isValue) { | |
| 253 entries.add(new TargetEntry(column, srcUrlId, srcLine, srcColumn)); | |
| 254 } else { | |
| 255 srcNameId += tokenizer._consumeValue(); | |
| 256 if (srcNameId >= names.length) { | |
| 257 throw new StateError( | |
| 258 'Invalid name id: $targetUrl, $line, $srcNameId'); | |
| 259 } | |
| 260 entries.add(new TargetEntry(column, srcUrlId, srcLine, srcColumn, | |
| 261 srcNameId)); | |
| 262 } | |
| 263 } | |
| 264 if (tokenizer.nextKind.isNewSegment) tokenizer._consumeNewSegment(); | |
| 265 } | |
| 266 if (!entries.isEmpty) { | |
| 267 lines.add(new TargetLineEntry(line, entries)); | |
| 268 } | |
| 269 } | |
| 270 | |
| 271 /// Encodes the Mapping mappings as a json map. | |
| 272 Map toJson() { | |
| 273 var buff = new StringBuffer(); | |
| 274 var line = 0; | |
| 275 var column = 0; | |
| 276 var srcLine = 0; | |
| 277 var srcColumn = 0; | |
| 278 var srcUrlId = 0; | |
| 279 var srcNameId = 0; | |
| 280 var first = true; | |
| 281 | |
| 282 for (var entry in lines) { | |
| 283 int nextLine = entry.line; | |
| 284 if (nextLine > line) { | |
| 285 for (int i = line; i < nextLine; ++i) { | |
| 286 buff.write(';'); | |
| 287 } | |
| 288 line = nextLine; | |
| 289 column = 0; | |
| 290 first = true; | |
| 291 } | |
| 292 | |
| 293 for (var segment in entry.entries) { | |
| 294 if (!first) buff.write(','); | |
| 295 first = false; | |
| 296 column = _append(buff, column, segment.column); | |
| 297 | |
| 298 // Encoding can be just the column offset if there is no source | |
| 299 // information. | |
| 300 var newUrlId = segment.sourceUrlId; | |
| 301 if (newUrlId == null) continue; | |
| 302 srcUrlId = _append(buff, srcUrlId, newUrlId); | |
| 303 srcLine = _append(buff, srcLine, segment.sourceLine); | |
| 304 srcColumn = _append(buff, srcColumn, segment.sourceColumn); | |
| 305 | |
| 306 if (segment.sourceNameId == null) continue; | |
| 307 srcNameId = _append(buff, srcNameId, segment.sourceNameId); | |
| 308 } | |
| 309 } | |
| 310 | |
| 311 var result = { | |
| 312 'version': 3, | |
| 313 'sourceRoot': sourceRoot == null ? '' : sourceRoot, | |
| 314 'sources': urls, | |
| 315 'names' : names, | |
| 316 'mappings' : buff.toString() | |
| 317 }; | |
| 318 if (targetUrl != null) { | |
| 319 result['file'] = targetUrl; | |
| 320 } | |
| 321 return result; | |
| 322 } | |
| 323 | |
| 324 /// Appends to [buff] a VLQ encoding of [newValue] using the difference | |
| 325 /// between [oldValue] and [newValue] | |
| 326 static int _append(StringBuffer buff, int oldValue, int newValue) { | |
| 327 buff.writeAll(encodeVlq(newValue - oldValue)); | |
| 328 return newValue; | |
| 329 } | |
| 330 | |
| 331 _segmentError(int seen, int line) => new StateError( | |
| 332 'Invalid entry in sourcemap, expected 1, 4, or 5' | |
| 333 ' values, but got $seen.\ntargeturl: $targetUrl, line: $line'); | |
| 334 | |
| 335 /// Returns [TargetLineEntry] which includes the location in the target [line] | |
| 336 /// number. In particular, the resulting entry is the last entry whose line | |
| 337 /// number is lower or equal to [line]. | |
| 338 TargetLineEntry _findLine(int line) { | |
| 339 int index = binarySearch(lines, (e) => e.line > line); | |
| 340 return (index <= 0) ? null : lines[index - 1]; | |
| 341 } | |
| 342 | |
| 343 /// Returns [TargetEntry] which includes the location denoted by | |
| 344 /// [line], [column]. If [lineEntry] corresponds to [line], then this will be | |
| 345 /// the last entry whose column is lower or equal than [column]. If | |
| 346 /// [lineEntry] corresponds to a line prior to [line], then the result will be | |
| 347 /// the very last entry on that line. | |
| 348 TargetEntry _findColumn(int line, int column, TargetLineEntry lineEntry) { | |
| 349 if (lineEntry == null || lineEntry.entries.length == 0) return null; | |
| 350 if (lineEntry.line != line) return lineEntry.entries.last; | |
| 351 var entries = lineEntry.entries; | |
| 352 int index = binarySearch(entries, (e) => e.column > column); | |
| 353 return (index <= 0) ? null : entries[index - 1]; | |
| 354 } | |
| 355 | |
| 356 SourceMapSpan spanFor(int line, int column, {Map<String, SourceFile> files}) { | |
| 357 var entry = _findColumn(line, column, _findLine(line)); | |
| 358 if (entry == null || entry.sourceUrlId == null) return null; | |
| 359 var url = urls[entry.sourceUrlId]; | |
| 360 if (sourceRoot != null) { | |
| 361 url = '${sourceRoot}${url}'; | |
| 362 } | |
| 363 if (files != null && files[url] != null) { | |
| 364 var file = files[url]; | |
| 365 var start = file.getOffset(entry.sourceLine, entry.sourceColumn); | |
| 366 if (entry.sourceNameId != null) { | |
| 367 var text = names[entry.sourceNameId]; | |
| 368 return new SourceMapFileSpan( | |
| 369 files[url].span(start, start + text.length), | |
| 370 isIdentifier: true); | |
| 371 } else { | |
| 372 return new SourceMapFileSpan(files[url].location(start).pointSpan()); | |
| 373 } | |
| 374 } else { | |
| 375 var start = new SourceLocation(0, | |
| 376 sourceUrl: url, line: entry.sourceLine, column: entry.sourceColumn); | |
| 377 // Offset and other context is not available. | |
| 378 if (entry.sourceNameId != null) { | |
| 379 return new SourceMapSpan.identifier(start, names[entry.sourceNameId]); | |
| 380 } else { | |
| 381 return new SourceMapSpan(start, start, ''); | |
| 382 } | |
| 383 } | |
| 384 } | |
| 385 | |
| 386 String toString() { | |
| 387 return (new StringBuffer("$runtimeType : [") | |
| 388 ..write('targetUrl: ') | |
| 389 ..write(targetUrl) | |
| 390 ..write(', sourceRoot: ') | |
| 391 ..write(sourceRoot) | |
| 392 ..write(', urls: ') | |
| 393 ..write(urls) | |
| 394 ..write(', names: ') | |
| 395 ..write(names) | |
| 396 ..write(', lines: ') | |
| 397 ..write(lines) | |
| 398 ..write(']')).toString(); | |
| 399 } | |
| 400 | |
| 401 String get debugString { | |
| 402 var buff = new StringBuffer(); | |
| 403 for (var lineEntry in lines) { | |
| 404 var line = lineEntry.line; | |
| 405 for (var entry in lineEntry.entries) { | |
| 406 buff..write(targetUrl) | |
| 407 ..write(': ') | |
| 408 ..write(line) | |
| 409 ..write(':') | |
| 410 ..write(entry.column); | |
| 411 if (entry.sourceUrlId != null) { | |
| 412 buff..write(' --> ') | |
| 413 ..write(sourceRoot) | |
| 414 ..write(urls[entry.sourceUrlId]) | |
| 415 ..write(': ') | |
| 416 ..write(entry.sourceLine) | |
| 417 ..write(':') | |
| 418 ..write(entry.sourceColumn); | |
| 419 } | |
| 420 if (entry.sourceNameId != null) { | |
| 421 buff..write(' (') | |
| 422 ..write(names[entry.sourceNameId]) | |
| 423 ..write(')'); | |
| 424 } | |
| 425 buff.write('\n'); | |
| 426 } | |
| 427 } | |
| 428 return buff.toString(); | |
| 429 } | |
| 430 } | |
| 431 | |
| 432 /// A line entry read from a source map. | |
| 433 class TargetLineEntry { | |
| 434 final int line; | |
| 435 List<TargetEntry> entries; | |
| 436 TargetLineEntry(this.line, this.entries); | |
| 437 | |
| 438 String toString() => '$runtimeType: $line $entries'; | |
| 439 } | |
| 440 | |
| 441 /// A target segment entry read from a source map | |
| 442 class TargetEntry { | |
| 443 final int column; | |
| 444 final int sourceUrlId; | |
| 445 final int sourceLine; | |
| 446 final int sourceColumn; | |
| 447 final int sourceNameId; | |
| 448 | |
| 449 TargetEntry(this.column, [this.sourceUrlId, this.sourceLine, | |
| 450 this.sourceColumn, this.sourceNameId]); | |
| 451 | |
| 452 String toString() => '$runtimeType: ' | |
| 453 '($column, $sourceUrlId, $sourceLine, $sourceColumn, $sourceNameId)'; | |
| 454 } | |
| 455 | |
| 456 /** A character iterator over a string that can peek one character ahead. */ | |
| 457 class _MappingTokenizer implements Iterator<String> { | |
| 458 final String _internal; | |
| 459 final int _length; | |
| 460 int index = -1; | |
| 461 _MappingTokenizer(String internal) | |
| 462 : _internal = internal, | |
| 463 _length = internal.length; | |
| 464 | |
| 465 // Iterator API is used by decodeVlq to consume VLQ entries. | |
| 466 bool moveNext() => ++index < _length; | |
| 467 String get current => | |
| 468 (index >= 0 && index < _length) ? _internal[index] : null; | |
| 469 | |
| 470 bool get hasTokens => index < _length - 1 && _length > 0; | |
| 471 | |
| 472 _TokenKind get nextKind { | |
| 473 if (!hasTokens) return _TokenKind.EOF; | |
| 474 var next = _internal[index + 1]; | |
| 475 if (next == ';') return _TokenKind.LINE; | |
| 476 if (next == ',') return _TokenKind.SEGMENT; | |
| 477 return _TokenKind.VALUE; | |
| 478 } | |
| 479 | |
| 480 int _consumeValue() => decodeVlq(this); | |
| 481 void _consumeNewLine() { ++index; } | |
| 482 void _consumeNewSegment() { ++index; } | |
| 483 | |
| 484 // Print the state of the iterator, with colors indicating the current | |
| 485 // position. | |
| 486 String toString() { | |
| 487 var buff = new StringBuffer(); | |
| 488 for (int i = 0; i < index; i++) { | |
| 489 buff.write(_internal[i]); | |
| 490 } | |
| 491 buff.write('[31m'); | |
| 492 buff.write(current == null ? '' : current); | |
| 493 buff.write('[0m'); | |
| 494 for (int i = index + 1; i < _internal.length; i++) { | |
| 495 buff.write(_internal[i]); | |
| 496 } | |
| 497 buff.write(' ($index)'); | |
| 498 return buff.toString(); | |
| 499 } | |
| 500 } | |
| 501 | |
| 502 class _TokenKind { | |
| 503 static const _TokenKind LINE = const _TokenKind(isNewLine: true); | |
| 504 static const _TokenKind SEGMENT = const _TokenKind(isNewSegment: true); | |
| 505 static const _TokenKind EOF = const _TokenKind(isEof: true); | |
| 506 static const _TokenKind VALUE = const _TokenKind(); | |
| 507 final bool isNewLine; | |
| 508 final bool isNewSegment; | |
| 509 final bool isEof; | |
| 510 bool get isValue => !isNewLine && !isNewSegment && !isEof; | |
| 511 | |
| 512 const _TokenKind( | |
| 513 {this.isNewLine: false, this.isNewSegment: false, this.isEof: false}); | |
| 514 } | |
| OLD | NEW |