Skip to content

Commit 05735ef

Browse files
authored
Merge pull request #1 from rollbar/sectioned-source-maps
add support for sectioned source maps
2 parents 8d6969a + 50af8c4 commit 05735ef

File tree

4 files changed

+281
-10
lines changed

4 files changed

+281
-10
lines changed

sourcemap/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from .exceptions import SourceMapDecodeError # NOQA
99
from .decoder import SourceMapDecoder
1010

11-
__version__ = '0.2.1'
11+
__version__ = '0.3.0'
1212

1313

1414
def load(fp, cls=None):

sourcemap/decoder.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import sys
1515
from functools import partial
1616
from .exceptions import SourceMapDecodeError
17-
from .objects import Token, SourceMapIndex
17+
from .objects import Token, SourceMapIndex, SectionedSourceMapIndex
1818
try:
1919
import simplejson as json
2020
except ImportError:
@@ -63,8 +63,10 @@ def parse_vlq(self, segment):
6363
return values
6464

6565
def decode(self, source):
66-
"""Decode a source map object into a SourceMapIndex.
66+
"""Decode a source map object into a SourceMapIndex or
67+
SectionedSourceMapIndex.
6768
69+
For SourceMapIndex:
6870
The index is keyed on (dst_line, dst_column) for lookups,
6971
and a per row index is kept to help calculate which Token to retrieve.
7072
@@ -102,6 +104,29 @@ def decode(self, source):
102104
lte to the bisect_right: 2-1 => row[2-1] => 12
103105
- At this point, we know the token location, (1, 12)
104106
- Pull (1, 12) from index => tokens[3]
107+
108+
For SectionedSourceMapIndex:
109+
The offsets are stored as tuples in sorted order:
110+
[(0, 0), (1, 10), (1, 24), (2, 0), ...]
111+
112+
For each offset there is a corresponding SourceMapIndex
113+
which operates as described above, except the tokens
114+
are relative to their own section and must have the offset
115+
replied in reverse on the destination row/col when the tokens
116+
are returned.
117+
118+
To find the token at (1, 20):
119+
- bisect_right to find the closest index (1, 20)
120+
- Supposing that returns index i, we actually want (i - 1)
121+
because the token we want is inside the map before that one
122+
- We then have a SourceMapIndex and we perform the search
123+
for (1 - offset[0], column - offset[1]). [Note this isn't
124+
exactly correct as we have to account for different lines
125+
being searched for and the found offset, so for the column
126+
we use either offset[1] or 0 depending on if line matches
127+
offset[0] or not]
128+
- The token we find we then translate dst_line += offset[0],
129+
and dst_col += offset[1].
105130
"""
106131
# According to spec (https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#heading=h.h7yy76c5il9v)
107132
# A SouceMap may be prepended with ")]}'" to cause a Javascript error.
@@ -110,6 +135,18 @@ def decode(self, source):
110135
source = source.split('\n', 1)[1]
111136

112137
smap = json.loads(source)
138+
if smap.get('sections'):
139+
offsets = []
140+
maps = []
141+
for section in smap.get('sections'):
142+
offset = section.get('offset')
143+
offsets.append((offset.get('line'), offset.get('column')))
144+
maps.append(self._decode_map(section.get('map')))
145+
return SectionedSourceMapIndex(smap, offsets, maps)
146+
else:
147+
return self._decode_map(smap)
148+
149+
def _decode_map(self, smap):
113150
sources = smap['sources']
114151
sourceRoot = smap.get('sourceRoot')
115152
names = list(map(text_type, smap['names']))

sourcemap/objects.py

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def __init__(self, raw, tokens, line_index, index, sources=None):
6161
def lookup(self, line, column):
6262
try:
6363
# Let's hope for a direct match first
64-
return self.index[(line, column)]
64+
return self.index[(line, column)], self
6565
except KeyError:
6666
pass
6767

@@ -75,7 +75,31 @@ def lookup(self, line, column):
7575
# We actually want the one less than current
7676
column = line_index[i - 1]
7777
# Return from the main index, based on the (line, column) tuple
78-
return self.index[(line, column)]
78+
return self.index[(line, column)], self
79+
80+
def columns_for_line(self, line):
81+
return self.line_index[line]
82+
83+
def total_number_of_lines(self):
84+
return len(self.line_index)
85+
86+
def files(self):
87+
f = self.raw.get('file')
88+
return [f] if f else None
89+
90+
def sources_content_map(self):
91+
result = self._source_content_array()
92+
return dict(result) if result else None
93+
94+
def raw_sources(self):
95+
return self.raw.get('sources')
96+
97+
def _source_content_array(self):
98+
sources = self.raw.get('sources')
99+
content = self.raw.get('sourcesContent')
100+
if sources and content:
101+
return zip(sources, content)
102+
return None
79103

80104
def __getitem__(self, item):
81105
return self.tokens[item]
@@ -88,3 +112,69 @@ def __len__(self):
88112

89113
def __repr__(self):
90114
return '<SourceMapIndex: %s>' % ', '.join(map(str, self.sources))
115+
116+
117+
class SectionedSourceMapIndex(object):
118+
"""The index for a source map which contains sections
119+
containing all the Tokens and precomputed indexes for
120+
searching."""
121+
122+
def __init__(self, raw, offsets, maps):
123+
self.raw = raw
124+
self.offsets = offsets
125+
self.maps = maps
126+
127+
def lookup(self, line, column):
128+
map_index = bisect_right(self.offsets, (line, column)) - 1
129+
line_offset, col_offset = self.offsets[map_index]
130+
col_offset = 0 if line != line_offset else col_offset
131+
smap = self.maps[map_index]
132+
result, _ = smap.lookup(line - line_offset, column - col_offset)
133+
result.dst_line += line_offset
134+
result.dst_col += col_offset
135+
return result, smap
136+
137+
def columns_for_line(self, line):
138+
last_map_index = bisect_right(self.offsets, (line + 1, 0))
139+
first_map_index = bisect_right(self.offsets, (line, 0)) - 1
140+
columns = []
141+
for map_index in range(first_map_index, last_map_index):
142+
smap = self.maps[map_index]
143+
line_offset, col_offset = self.offsets[map_index]
144+
smap_line = line - line_offset
145+
smap_cols = smap.columns_for_line(smap_line)
146+
columns.extend([x + col_offset for x in smap_cols])
147+
return columns
148+
149+
def total_number_of_lines(self):
150+
result = 0
151+
for smap in self.maps:
152+
result += smap.total_number_of_lines()
153+
return result
154+
155+
def files(self):
156+
files = []
157+
for smap in self.maps:
158+
smap_files = smap.files()
159+
if smap_files:
160+
files.extend(smap_files)
161+
return files if len(files) else None
162+
163+
def sources_content_map(self):
164+
content_maps = []
165+
for m in self.maps:
166+
source_content_array = m._source_content_array()
167+
if source_content_array:
168+
content_maps.extend(source_content_array)
169+
if len(content_maps):
170+
return dict(content_maps)
171+
return None
172+
173+
def raw_sources(self):
174+
sources = []
175+
for m in self.maps:
176+
sources.extend(m.raw_sources())
177+
return sources
178+
179+
def __repr__(self):
180+
return '<SectionedSourceMapIndex: %s>' % ', '.join(map(str, self.maps))

tests/test_objects.py

Lines changed: 149 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,146 @@
22
import unittest2 as unittest
33
except ImportError:
44
import unittest
5-
from sourcemap.objects import Token, SourceMapIndex
5+
from sourcemap.objects import Token, SourceMapIndex, SectionedSourceMapIndex
66

77

88
class TokenTestCase(unittest.TestCase):
99
def test_eq(self):
1010
assert Token(1, 1, 'lol.js', 1, 1, 'lol') == Token(1, 1, 'lol.js', 1, 1, 'lol')
1111
assert Token(99, 1, 'lol.js', 1, 1, 'lol') != Token(1, 1, 'lol.js', 1, 1, 'lol')
1212

13+
class SectionedSourceMapIndexTestCase(unittest.TestCase):
14+
def get_index(self):
15+
offsets = [(0, 0), (1, 14), (2, 28)]
16+
tokens0 = [
17+
Token(dst_line=0, dst_col=0),
18+
Token(dst_line=0, dst_col=5),
19+
Token(dst_line=1, dst_col=0),
20+
Token(dst_line=1, dst_col=12),
21+
]
22+
tokens1 = [
23+
Token(dst_line=0, dst_col=0),
24+
Token(dst_line=0, dst_col=5),
25+
Token(dst_line=1, dst_col=0),
26+
Token(dst_line=1, dst_col=12),
27+
]
28+
tokens2 = [
29+
Token(dst_line=0, dst_col=0),
30+
Token(dst_line=0, dst_col=5),
31+
Token(dst_line=1, dst_col=0),
32+
Token(dst_line=1, dst_col=12),
33+
]
34+
maps = [
35+
SourceMapIndex({'file': 'foo0.js'}, tokens0,
36+
[
37+
[0, 5],
38+
[0, 12],
39+
],
40+
{
41+
(0, 0): tokens0[0],
42+
(0, 5): tokens0[1],
43+
(1, 0): tokens0[2],
44+
(1, 12): tokens0[3],
45+
}),
46+
SourceMapIndex({'file': 'foo1.js'}, tokens1,
47+
[
48+
[0, 5],
49+
[0, 12],
50+
],
51+
{
52+
(0, 0): tokens1[0],
53+
(0, 5): tokens1[1],
54+
(1, 0): tokens1[2],
55+
(1, 12): tokens1[3],
56+
}),
57+
SourceMapIndex({'file': 'foo2.js'}, tokens2,
58+
[
59+
[0, 5],
60+
[0, 12],
61+
],
62+
{
63+
(0, 0): tokens2[0],
64+
(0, 5): tokens2[1],
65+
(1, 0): tokens2[2],
66+
(1, 12): tokens2[3],
67+
}),
68+
]
69+
70+
raw = {}
71+
72+
return SectionedSourceMapIndex(raw, offsets, maps), [tokens0, tokens1, tokens2]
73+
74+
def test_lookup(self):
75+
index, tokens = self.get_index()
76+
77+
for i in range(5):
78+
assert index.lookup(0, i)[0] is tokens[0][0]
79+
80+
for i in range(5, 10):
81+
assert index.lookup(0, i)[0] is tokens[0][1]
82+
83+
for i in range(12):
84+
assert index.lookup(1, i)[0] is tokens[0][2]
85+
86+
for i in range(12, 14):
87+
assert index.lookup(1, i)[0] is tokens[0][3]
88+
89+
for i in range(14, 19):
90+
assert index.lookup(1, i)[0] is tokens[1][0]
91+
92+
for i in range(19, 25):
93+
assert index.lookup(1, i)[0] is tokens[1][1]
94+
95+
for i in range(12):
96+
assert index.lookup(2, i)[0] is tokens[1][2]
97+
98+
for i in range(12, 28):
99+
assert index.lookup(2, i)[0] is tokens[1][3]
100+
101+
for i in range(28, 33):
102+
assert index.lookup(2, i)[0] is tokens[2][0]
103+
104+
for i in range(33, 40):
105+
assert index.lookup(2, i)[0] is tokens[2][1]
106+
107+
for i in range(12):
108+
assert index.lookup(3, i)[0] is tokens[2][2]
109+
110+
for i in range(12, 14):
111+
assert index.lookup(3, i)[0] is tokens[2][3]
112+
113+
def test_columns_for_line(self):
114+
index, tokens = self.get_index()
115+
cols = index.columns_for_line(0)
116+
117+
assert cols[0] is tokens[0][0].dst_col
118+
assert cols[1] is tokens[0][1].dst_col
119+
120+
cols = index.columns_for_line(1)
121+
122+
assert len(cols) is 4
123+
assert cols[0] is tokens[0][2].dst_col
124+
assert cols[1] is tokens[0][3].dst_col
125+
assert cols[2] is tokens[1][0].dst_col + index.offsets[1][1]
126+
assert cols[3] is tokens[1][1].dst_col + index.offsets[1][1]
127+
128+
cols = index.columns_for_line(2)
129+
130+
assert len(cols) is 4
131+
assert cols[0] is tokens[1][2].dst_col + index.offsets[1][1]
132+
assert cols[1] is tokens[1][3].dst_col + index.offsets[1][1]
133+
assert cols[2] is tokens[2][0].dst_col + index.offsets[2][1]
134+
assert cols[3] is tokens[2][1].dst_col + index.offsets[2][1]
135+
136+
def test_lookup_from_columns_for_line(self):
137+
index, tokens = self.get_index()
138+
cols = index.columns_for_line(2)
139+
t, _ = index.lookup(2, cols[2])
140+
assert t is tokens[2][0]
141+
142+
def test_files(self):
143+
index, _ = self.get_index()
144+
assert len(index.files()) is 3
13145

14146
class SourceMapIndexTestCase(unittest.TestCase):
15147
def get_index(self):
@@ -40,16 +172,28 @@ def test_lookup(self):
40172
index, tokens = self.get_index()
41173

42174
for i in range(5):
43-
assert index.lookup(0, i) is tokens[0]
175+
assert index.lookup(0, i)[0] is tokens[0]
44176

45177
for i in range(5, 10):
46-
assert index.lookup(0, i) is tokens[1]
178+
assert index.lookup(0, i)[0] is tokens[1]
47179

48180
for i in range(12):
49-
assert index.lookup(1, i) is tokens[2]
181+
assert index.lookup(1, i)[0] is tokens[2]
50182

51183
for i in range(12, 20):
52-
assert index.lookup(1, i) is tokens[3]
184+
assert index.lookup(1, i)[0] is tokens[3]
185+
186+
def test_columns_for_line(self):
187+
index, tokens = self.get_index()
188+
cols = index.columns_for_line(0)
189+
190+
assert cols[0] is tokens[0].dst_col
191+
assert cols[1] is tokens[1].dst_col
192+
193+
cols = index.columns_for_line(1)
194+
195+
assert cols[0] is tokens[2].dst_col
196+
assert cols[1] is tokens[3].dst_col
53197

54198
def test_getitem(self):
55199
index, tokens = self.get_index()

0 commit comments

Comments
 (0)