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 |