| OLD | NEW |
| (Empty) |
| 1 # Copyright (c) 2011, 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 """Utilities to extract source map information | |
| 6 | |
| 7 A set of utilities to extract source map information. This python library | |
| 8 consumes source map files v3, but doesn't provide any funcitonality to produce | |
| 9 source maps. This code is an adaptation of the Java implementation originally | |
| 10 written for the closure compiler by John Lenz (johnlenz@google.com) | |
| 11 """ | |
| 12 | |
| 13 import bisect | |
| 14 import json | |
| 15 import sys | |
| 16 | |
| 17 class SourceMap(): | |
| 18 """ An in memory representation of a source map. """ | |
| 19 | |
| 20 def get_source_location(self, line, column): | |
| 21 """ Fetches the original location for a line and column. | |
| 22 | |
| 23 Args: | |
| 24 line: line in the output file to query | |
| 25 column: column in the output file to query | |
| 26 | |
| 27 Returns: | |
| 28 A tuple of the form: | |
| 29 file, src_line, src_column, opt_identifier | |
| 30 When available, opt_identifier contains the name of an identifier | |
| 31 associated with the given program location. | |
| 32 """ | |
| 33 pass # implemented by subclasses | |
| 34 | |
| 35 def parse(sourcemap_file): | |
| 36 """ Parse a file containing source map information as a json string and return | |
| 37 a source map object representing it. | |
| 38 Args: | |
| 39 sourcemap_file: path to a file (optionally containing a 'file:' prefix) | |
| 40 which can either contain a meta-level source map or an | |
| 41 actual source map. | |
| 42 """ | |
| 43 if sourcemap_file.startswith('file:'): | |
| 44 sourcemap_file = sourcemap_file[5:] | |
| 45 | |
| 46 with open(sourcemap_file, 'r') as f: | |
| 47 sourcemap_json = json.load(f) | |
| 48 | |
| 49 return _parseFromJson(sourcemap_json) | |
| 50 | |
| 51 | |
| 52 def _parseFromJson(sourcemap_json): | |
| 53 if sourcemap_json['version'] != 3: | |
| 54 raise SourceMapException("unexpected source map version") | |
| 55 | |
| 56 if not 'file' in sourcemap_json: | |
| 57 raise SourceMapException("unexpected, no file in source map file") | |
| 58 | |
| 59 if 'sections' in sourcemap_json: | |
| 60 sections = sourcemap_json['sections'] | |
| 61 # a meta file | |
| 62 if ('mappings' in sourcemap_json | |
| 63 or 'sources' in sourcemap_json | |
| 64 or 'names' in sourcemap_json): | |
| 65 raise SourceMapException("Invalid map format") | |
| 66 return _MetaSourceMap(sections) | |
| 67 | |
| 68 return _SourceMapFile(sourcemap_json) | |
| 69 | |
| 70 class _MetaSourceMap(SourceMap): | |
| 71 """ A higher-order source map containing nested source maps. """ | |
| 72 | |
| 73 def __init__(self, sections): | |
| 74 """ creates a source map instance given its json input (already parsed). """ | |
| 75 # parse a regular sourcemap file | |
| 76 self.offsets = [] | |
| 77 self.maps = [] | |
| 78 | |
| 79 for section in sections: | |
| 80 line = section['offset']['line'] | |
| 81 if section['offset']['column'] != 0: | |
| 82 # TODO(sigmund): implement if needed | |
| 83 raise Exception("unimplemented") | |
| 84 | |
| 85 if 'url' in section and 'map' in section: | |
| 86 raise SourceMapException( | |
| 87 "Invalid format: section may not contain both 'url' and 'map'") | |
| 88 | |
| 89 self.offsets.append(line) | |
| 90 if 'url' in section: | |
| 91 self.maps.append(parse(section['url'])) | |
| 92 elif 'map' in section: | |
| 93 self.maps.append(_parseFromJson(section['map'])) | |
| 94 else: | |
| 95 raise SourceMapException( | |
| 96 "Invalid format: section must contain either 'url' or 'map'") | |
| 97 | |
| 98 def get_source_location(self, line, column): | |
| 99 """ Fetches the original location from the target location. """ | |
| 100 index = bisect.bisect(self.offsets, line) - 1 | |
| 101 return self.maps[index].get_source_location( | |
| 102 line - self.offsets[index], column) | |
| 103 | |
| 104 class _SourceMapFile(SourceMap): | |
| 105 def __init__(self, sourcemap): | |
| 106 """ creates a source map instance given its json input (already parsed). """ | |
| 107 # parse a regular sourcemap file | |
| 108 self.sourcemap_file = sourcemap['file'] | |
| 109 self.sources = sourcemap['sources'] | |
| 110 self.names = sourcemap['names'] | |
| 111 self.lines = [] | |
| 112 self._build(sourcemap['mappings']) | |
| 113 | |
| 114 def get_source_location(self, line, column): | |
| 115 """ Fetches the original location from the target location. """ | |
| 116 | |
| 117 # Normalize the line and column numbers to 0. | |
| 118 line -= 1 | |
| 119 column -= 1 | |
| 120 | |
| 121 if line < 0 or line >= len(self.lines): | |
| 122 return None | |
| 123 | |
| 124 entries = self.lines[line] | |
| 125 # If the line is empty return the previous mapping. | |
| 126 if not entries or entries == [] or entries[0].gen_column > column: | |
| 127 return self._previousMapping(line) | |
| 128 | |
| 129 index = bisect.bisect(entries, _Entry(column)) - 1 | |
| 130 return self._originalEntryMapping(entries[index]) | |
| 131 | |
| 132 def _previousMapping(self, line): | |
| 133 while True: | |
| 134 if line == 0: | |
| 135 return None | |
| 136 line -= 1 | |
| 137 if self.lines[line]: | |
| 138 return self._originalEntryMapping(self.lines[line][-1]) | |
| 139 | |
| 140 def _originalEntryMapping(self, entry): | |
| 141 if entry.src_file_id is None: | |
| 142 return None | |
| 143 | |
| 144 if entry.name_id: | |
| 145 identifier = self.names[entry.name_id] | |
| 146 else: | |
| 147 identifier = None | |
| 148 | |
| 149 filename = self.sources[entry.src_file_id] | |
| 150 return filename, entry.src_line, entry.src_column, identifier | |
| 151 | |
| 152 def _build(self, linemap): | |
| 153 """ builds this source map from the sourcemap json """ | |
| 154 entries = [] | |
| 155 line = 0 | |
| 156 prev_col = 0 | |
| 157 prev_src_id = 0 | |
| 158 prev_src_line = 0 | |
| 159 prev_src_column = 0 | |
| 160 prev_name_id = 0 | |
| 161 content = _StringCharIterator(linemap) | |
| 162 while content.hasNext(): | |
| 163 # ';' denotes a new line. | |
| 164 token = content.peek() | |
| 165 if token == ';': | |
| 166 content.next() | |
| 167 # The line is complete, store the result for the line, None if empty. | |
| 168 result = entries if len(entries) > 0 else None | |
| 169 self.lines.append(result) | |
| 170 entries = [] | |
| 171 line += 1 | |
| 172 prev_col = 0 | |
| 173 else: | |
| 174 # Grab the next entry for the current line. | |
| 175 values = [] | |
| 176 while (content.hasNext() | |
| 177 and content.peek() != ',' and content.peek() != ';'): | |
| 178 values.append(_Base64VLQDecode(content)) | |
| 179 | |
| 180 # Decodes the next entry, using the previous encountered values to | |
| 181 # decode the relative values. | |
| 182 # | |
| 183 # The values, if present are in the following order: | |
| 184 # 0: the starting column in the current line of the generated file | |
| 185 # 1: the id of the original source file | |
| 186 # 2: the starting line in the original source | |
| 187 # 3: the starting column in the original source | |
| 188 # 4: the id of the original symbol name | |
| 189 # The values are relative to the previous encountered values. | |
| 190 | |
| 191 total = len(values) | |
| 192 if not(total == 1 or total == 4 or total == 5): | |
| 193 raise SourceMapException( | |
| 194 "Invalid entry in source map file: %s\nline: %d\nvalues: %s\n" | |
| 195 % (self.sourcemap_file, line, str(values))) | |
| 196 prev_col += values[0] | |
| 197 if total == 1: | |
| 198 entry = _Entry(prev_col) | |
| 199 else: | |
| 200 prev_src_id += values[1] | |
| 201 if prev_src_id >= len(self.sources): | |
| 202 raise SourceMapException( | |
| 203 "Invalid source id\nfile: %s\nline: %d\nid: %d\n" | |
| 204 % (self.sourcemap_file, line, prev_src_id)) | |
| 205 prev_src_line += values[2] | |
| 206 prev_src_column += values[3] | |
| 207 if total == 4: | |
| 208 entry = _Entry( | |
| 209 prev_col, prev_src_id, prev_src_line, prev_src_column) | |
| 210 elif total == 5: | |
| 211 prev_name_id += values[4] | |
| 212 if prev_name_id >= len(self.names): | |
| 213 raise SourceMapException( | |
| 214 "Invalid name id\nfile: %s\nline: %d\nid: %d\n" | |
| 215 % (self.sourcemap_file, line, prev_name_id)) | |
| 216 entry = _Entry( | |
| 217 prev_col, prev_src_id, prev_src_line, prev_src_column, | |
| 218 prev_name_id) | |
| 219 entries.append(entry); | |
| 220 if content.peek() == ',': | |
| 221 content.next() | |
| 222 | |
| 223 class _StringCharIterator(): | |
| 224 """ An iterator over a string that allows you to peek into the next value. """ | |
| 225 def __init__(self, string): | |
| 226 self.string = string | |
| 227 self.length = len(string) | |
| 228 self.current = 0 | |
| 229 | |
| 230 def __iter__(self): | |
| 231 return self | |
| 232 | |
| 233 def next(self): | |
| 234 res = self.string[self.current] | |
| 235 self.current += 1 | |
| 236 return res | |
| 237 | |
| 238 def peek(self): | |
| 239 return self.string[self.current] | |
| 240 | |
| 241 def hasNext(self): | |
| 242 return self.current < self.length | |
| 243 | |
| 244 | |
| 245 # Base64VLQ decoding | |
| 246 | |
| 247 VLQ_BASE_SHIFT = 5 | |
| 248 VLQ_BASE = 1 << VLQ_BASE_SHIFT | |
| 249 VLQ_BASE_MASK = VLQ_BASE - 1 | |
| 250 VLQ_CONTINUATION_BIT = VLQ_BASE | |
| 251 BASE64_MAP = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" | |
| 252 BASE64_DECODE_MAP = dict() | |
| 253 for c in range(64): | |
| 254 BASE64_DECODE_MAP[BASE64_MAP[c]] = c | |
| 255 | |
| 256 def _Base64VLQDecode(iterator): | |
| 257 """ | |
| 258 Decodes the next VLQValue from the provided char iterator. | |
| 259 | |
| 260 Sourcemaps are encoded with variable length numbers as base64 encoded strings | |
| 261 with the least significant digit coming first. Each base64 digit encodes a | |
| 262 5-bit value (0-31) and a continuation bit. Signed values can be represented | |
| 263 by using the least significant bit of the value as the | |
| 264 sign bit. | |
| 265 | |
| 266 This function only contains the decoding logic, since the encoding logic is | |
| 267 only needed to produce source maps. | |
| 268 | |
| 269 Args: | |
| 270 iterator: a _StringCharIterator | |
| 271 """ | |
| 272 result = 0 | |
| 273 stop = False | |
| 274 shift = 0 | |
| 275 while not stop: | |
| 276 c = iterator.next() | |
| 277 if c not in BASE64_DECODE_MAP: | |
| 278 raise Exception("%s not a valid char" % c) | |
| 279 digit = BASE64_DECODE_MAP[c] | |
| 280 stop = digit & VLQ_CONTINUATION_BIT == 0 | |
| 281 digit &= VLQ_BASE_MASK | |
| 282 result += (digit << shift) | |
| 283 shift += VLQ_BASE_SHIFT | |
| 284 | |
| 285 # Result uses the least significant bit as a sign bit. We convert it into a | |
| 286 # two-complement value. For example, | |
| 287 # 2 (10 binary) becomes 1 | |
| 288 # 3 (11 binary) becomes -1 | |
| 289 # 4 (100 binary) becomes 2 | |
| 290 # 5 (101 binary) becomes -2 | |
| 291 # 6 (110 binary) becomes 3 | |
| 292 # 7 (111 binary) becomes -3 | |
| 293 negate = (result & 1) == 1 | |
| 294 result = result >> 1 | |
| 295 return -result if negate else result | |
| 296 | |
| 297 | |
| 298 ERROR_DETAILS =""" | |
| 299 - gen_column = %s | |
| 300 - src_file_id = %s | |
| 301 - src_line = %s | |
| 302 - src_column = %s | |
| 303 - name_id = %s | |
| 304 """ | |
| 305 | |
| 306 class _Entry(): | |
| 307 """ An entry in a source map file. """ | |
| 308 def __init__(self, gen_column, | |
| 309 src_file_id=None, | |
| 310 src_line=None, | |
| 311 src_column=None, | |
| 312 name_id=None): | |
| 313 """ Creates an entry. Many arguments are marked as optional, but we expect | |
| 314 either all being None, or only name_id being none. | |
| 315 """ | |
| 316 | |
| 317 # gen column must be defined: | |
| 318 if gen_column is None: | |
| 319 raise SourceMapException( | |
| 320 "Invalid entry, no gen_column specified:" + | |
| 321 ERROR_DETAILS % ( | |
| 322 gen_column, src_file_id, src_line, src_column, name_id)) | |
| 323 | |
| 324 # if any field other than gen_column is defined, then file_id, line, and | |
| 325 # column must be defined: | |
| 326 if ((src_file_id is not None or src_line is not None or | |
| 327 src_column is not None or name_id is not None) and | |
| 328 (src_file_id is None or src_line is None or src_column is None)): | |
| 329 raise SourceMapException( | |
| 330 "Invalid entry, only name_id is optional:" + | |
| 331 ERROR_DETAILS % ( | |
| 332 gen_column, src_file_id, src_line, src_column, name_id)) | |
| 333 | |
| 334 self.gen_column = gen_column | |
| 335 self.src_file_id = src_file_id | |
| 336 self.src_line = src_line | |
| 337 self.src_column = src_column | |
| 338 self.name_id = name_id | |
| 339 | |
| 340 # define comparison to perform binary search on lookups | |
| 341 def __cmp__(self, other): | |
| 342 return cmp(self.gen_column, other.gen_column) | |
| 343 | |
| 344 class SourceMapException(Exception): | |
| 345 """ An exception encountered while parsing or processing source map files.""" | |
| 346 pass | |
| 347 | |
| 348 def main(): | |
| 349 """ This module is intended to be used as a library. Main is provided to | |
| 350 test the functionality on the command line. | |
| 351 """ | |
| 352 if len(sys.argv) < 3: | |
| 353 print ("Usage: %s <mapfile> line [column]" % sys.argv[0]) | |
| 354 return 1 | |
| 355 | |
| 356 sourcemap = parse(sys.argv[1]) | |
| 357 line = int(sys.argv[2]) | |
| 358 column = int(sys.argv[3]) if len(sys.argv) > 3 else 1 | |
| 359 original = sourcemap.get_source_location(line, column) | |
| 360 if not original: | |
| 361 print "Source location not found" | |
| 362 else: | |
| 363 filename, srcline, srccolumn, srcid = original | |
| 364 print "Source location is: %s, line: %d, column: %d, identifier: %s" % ( | |
| 365 filename, srcline, srccolumn, srcid) | |
| 366 | |
| 367 if __name__ == '__main__': | |
| 368 sys.exit(main()) | |
| OLD | NEW |