/src/hermes/lib/SourceMap/SourceMapGenerator.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Copyright (c) Meta Platforms, Inc. and affiliates. |
3 | | * |
4 | | * This source code is licensed under the MIT license found in the |
5 | | * LICENSE file in the root directory of this source tree. |
6 | | */ |
7 | | |
8 | | #include "hermes/SourceMap/SourceMapGenerator.h" |
9 | | |
10 | | #include "hermes/Support/Base64vlq.h" |
11 | | #include "hermes/Support/JSONEmitter.h" |
12 | | |
13 | | namespace hermes { |
14 | | |
15 | | namespace { |
16 | | |
17 | | /// A VLQ (VariableLengthQuantity) is a type of encoding used in source maps. |
18 | | /// Values are represented in a pseudo-base64 using continuation bits. |
19 | | /// The value is signed because SourceMaps are delta-encoded, so relative |
20 | | /// offsets (for example, between columns) may be negative. We wrap an int32_t |
21 | | /// in a struct so we can overload operator<< to output in the VLQ format. |
22 | | struct VLQ { |
23 | | int32_t val; |
24 | | }; |
25 | | |
26 | 0 | llvh::raw_ostream &operator<<(llvh::raw_ostream &OS, VLQ vlq) { |
27 | 0 | return base64vlq::encode(OS, vlq.val); |
28 | 0 | } |
29 | | } // namespace |
30 | | |
31 | | uint32_t SourceMapGenerator::addSource( |
32 | | llvh::StringRef filename, |
33 | 0 | llvh::Optional<SourceMap::MetadataEntry> metadata) { |
34 | 0 | uint32_t index = filenameTable_.insert(filename); |
35 | 0 | if (sourcesMetadata_.size() <= index) { |
36 | 0 | sourcesMetadata_.resize(index + 1); |
37 | 0 | } |
38 | 0 | if (metadata.hasValue() && |
39 | 0 | metadata.getValue()->getKind() != parser::JSONKind::Null) { |
40 | 0 | sourcesMetadata_[index] = metadata.getValue(); |
41 | 0 | } |
42 | 0 | return index; |
43 | 0 | } |
44 | | |
45 | | std::pair<llvh::Optional<SourceMap::Segment>, const SourceMap *> |
46 | | SourceMapGenerator::getInputSegmentForSegment( |
47 | 0 | const SourceMap::Segment &seg) const { |
48 | 0 | if (seg.representedLocation.hasValue()) { |
49 | 0 | assert( |
50 | 0 | seg.representedLocation->sourceIndex >= 0 && "Negative source index"); |
51 | 0 | } |
52 | | |
53 | 0 | const SourceMap *inputMap = nullptr; |
54 | 0 | llvh::Optional<SourceMap::Segment> inputSeg; |
55 | |
|
56 | 0 | if (seg.representedLocation.hasValue() && |
57 | 0 | (uint32_t)seg.representedLocation->sourceIndex < |
58 | 0 | inputSourceMaps_.size()) { |
59 | 0 | inputMap = inputSourceMaps_[seg.representedLocation->sourceIndex].get(); |
60 | 0 | if (inputMap) { |
61 | 0 | inputSeg = inputMap->getSegmentForAddress( |
62 | 0 | seg.representedLocation->lineIndex + 1, |
63 | 0 | seg.representedLocation->columnIndex + 1); |
64 | 0 | } |
65 | 0 | } |
66 | |
|
67 | 0 | return std::make_pair(inputSeg, inputMap); |
68 | 0 | } |
69 | | |
70 | 0 | bool SourceMapGenerator::hasSourcesMetadata() const { |
71 | 0 | for (const auto &entry : sourcesMetadata_) { |
72 | 0 | if (entry.hasValue() && |
73 | 0 | entry.getValue()->getKind() != parser::JSONKind::Null) { |
74 | 0 | return true; |
75 | 0 | } |
76 | 0 | } |
77 | 0 | return false; |
78 | 0 | } |
79 | | |
80 | | SourceMapGenerator::State SourceMapGenerator::encodeSourceLocations( |
81 | | const SourceMapGenerator::State &lastState, |
82 | | llvh::ArrayRef<SourceMap::Segment> segments, |
83 | 0 | llvh::raw_ostream &OS) { |
84 | | // Currently we only support a single source, so the source ID (and its delta) |
85 | | // is always 0. |
86 | 0 | SourceMapGenerator::State state = lastState, prevState = lastState; |
87 | 0 | bool first = true; |
88 | 0 | for (const SourceMap::Segment &seg : segments) { |
89 | | // Segments are separated by commas. |
90 | 0 | state.generatedColumn = seg.generatedColumn; |
91 | 0 | OS << (first ? "" : ",") |
92 | 0 | << VLQ{state.generatedColumn - prevState.generatedColumn}; |
93 | 0 | if (seg.representedLocation.hasValue()) { |
94 | 0 | state.sourceIndex = seg.representedLocation->sourceIndex; |
95 | 0 | state.representedLine = seg.representedLocation->lineIndex; |
96 | 0 | state.representedColumn = seg.representedLocation->columnIndex; |
97 | 0 | OS << VLQ{state.sourceIndex - prevState.sourceIndex} |
98 | 0 | << VLQ{state.representedLine - prevState.representedLine} |
99 | 0 | << VLQ{state.representedColumn - prevState.representedColumn}; |
100 | |
|
101 | 0 | if (seg.representedLocation->nameIndex.hasValue()) { |
102 | 0 | state.nameIndex = seg.representedLocation->nameIndex.getValue(); |
103 | 0 | OS << VLQ{state.nameIndex - prevState.nameIndex}; |
104 | 0 | } |
105 | 0 | } |
106 | 0 | first = false; |
107 | 0 | prevState = state; |
108 | 0 | } |
109 | 0 | return prevState; |
110 | 0 | } |
111 | | |
112 | 0 | std::string SourceMapGenerator::getVLQMappingsString() const { |
113 | 0 | std::string result; |
114 | 0 | llvh::raw_string_ostream OS(result); |
115 | 0 | State state; |
116 | 0 | for (const SourceMap::SegmentList &segments : lines_) { |
117 | | // The generated column (unlike other fields) resets with each new line. |
118 | 0 | state.generatedColumn = 0; |
119 | 0 | state = encodeSourceLocations(state, segments, OS); |
120 | 0 | OS << ';'; |
121 | 0 | } |
122 | 0 | OS.flush(); |
123 | 0 | return result; |
124 | 0 | } |
125 | | |
126 | 0 | std::vector<llvh::StringRef> SourceMapGenerator::getSources() const { |
127 | 0 | return std::vector<llvh::StringRef>( |
128 | 0 | filenameTable_.begin(), filenameTable_.end()); |
129 | 0 | } |
130 | | |
131 | 0 | SourceMapGenerator SourceMapGenerator::mergedWithInputSourceMaps() const { |
132 | 0 | assert( |
133 | 0 | !inputSourceMaps_.empty() && |
134 | 0 | "Cannot merge source maps without input source maps"); |
135 | | |
136 | 0 | auto sources = getSources(); |
137 | | |
138 | | // Create a new SourceMapGenerator with the merged data. |
139 | 0 | SourceMapGenerator merged; |
140 | |
|
141 | 0 | for (uint32_t i = 0, e = lines_.size(); i < e; ++i) { |
142 | 0 | SourceMap::SegmentList newLine{}; |
143 | |
|
144 | 0 | for (const auto &seg : lines_[i]) { |
145 | 0 | auto pair = getInputSegmentForSegment(seg); |
146 | 0 | auto inputSeg = pair.first; |
147 | 0 | auto inputMap = pair.second; |
148 | |
|
149 | 0 | SourceMap::Segment newSeg = seg; |
150 | 0 | newSeg.representedLocation = llvh::None; |
151 | |
|
152 | 0 | if (inputSeg.hasValue() && inputSeg->representedLocation.hasValue()) { |
153 | | // We have an input source map and were able to find a merged source |
154 | | // location. |
155 | |
|
156 | 0 | auto loc = inputSeg->representedLocation.getValue(); |
157 | | // Our _output_ sourceRoot is empty, so make sure to canonicalize |
158 | | // the path based on the input map's sourceRoot. |
159 | 0 | std::string filename = inputMap->getSourceFullPath(loc.sourceIndex); |
160 | |
|
161 | 0 | newSeg.representedLocation = SourceMap::Segment::SourceLocation( |
162 | 0 | merged.addSource( |
163 | 0 | filename, inputMap->getSourceMetadata(loc.sourceIndex)), |
164 | 0 | loc.lineIndex, |
165 | 0 | loc.columnIndex |
166 | | // TODO: Handle name index |
167 | 0 | ); |
168 | 0 | } |
169 | |
|
170 | 0 | if (!inputMap && !newSeg.representedLocation.hasValue() && |
171 | 0 | seg.representedLocation.hasValue()) { |
172 | | // Failed to find a merged location because there is no input source |
173 | | // map. Use the existing location, but copy over the source file name. |
174 | 0 | newSeg.representedLocation = seg.representedLocation; |
175 | 0 | newSeg.representedLocation->sourceIndex = merged.addSource( |
176 | 0 | sources[seg.representedLocation->sourceIndex], |
177 | 0 | getSourceMetadata(seg.representedLocation->sourceIndex)); |
178 | 0 | } |
179 | | |
180 | | // Push the new segment even if it has no represented location. If there |
181 | | // is an input source map, all locations we emit will be in terms of it, |
182 | | // or be explicitly unmapped. |
183 | 0 | newLine.push_back(std::move(newSeg)); |
184 | 0 | } |
185 | |
|
186 | 0 | merged.addMappingsLine(std::move(newLine), i); |
187 | 0 | } |
188 | 0 | merged.functionOffsets_ = functionOffsets_; |
189 | 0 | return merged; |
190 | 0 | } |
191 | | |
192 | 0 | void SourceMapGenerator::outputAsJSON(llvh::raw_ostream &OS) const { |
193 | 0 | if (inputSourceMaps_.empty()) { |
194 | 0 | this->outputAsJSONImpl(OS); |
195 | 0 | } else { |
196 | | // If there are input source maps, we must merge them into a new |
197 | | // SourceMapGenerator before output. |
198 | 0 | mergedWithInputSourceMaps().outputAsJSONImpl(OS); |
199 | 0 | } |
200 | 0 | } |
201 | | |
202 | 0 | void SourceMapGenerator::outputAsJSONImpl(llvh::raw_ostream &OS) const { |
203 | 0 | JSONEmitter json(OS); |
204 | 0 | json.openDict(); |
205 | 0 | json.emitKeyValue("version", 3); |
206 | |
|
207 | 0 | json.emitKey("sources"); |
208 | 0 | json.openArray(); |
209 | 0 | json.emitValues(llvh::makeArrayRef(getSources())); |
210 | 0 | json.closeArray(); |
211 | |
|
212 | 0 | if (hasSourcesMetadata()) { |
213 | 0 | json.emitKey("x_facebook_sources"); |
214 | 0 | json.openArray(); |
215 | 0 | for (const auto &source : sourcesMetadata_) { |
216 | 0 | if (source.hasValue()) { |
217 | 0 | source.getValue()->emitInto(json); |
218 | 0 | } else { |
219 | 0 | json.emitNullValue(); |
220 | 0 | } |
221 | 0 | } |
222 | 0 | json.closeArray(); |
223 | 0 | } |
224 | |
|
225 | 0 | json.emitKeyValue("mappings", getVLQMappingsString()); |
226 | |
|
227 | 0 | if (!functionOffsets_.empty()) { |
228 | 0 | json.emitKey("x_hermes_function_offsets"); |
229 | 0 | json.openDict(); |
230 | 0 | for (const auto &entry : functionOffsets_) { |
231 | 0 | const auto &segmentFunctionOffsets = entry.second; |
232 | 0 | json.emitKey(std::to_string(entry.first)); |
233 | 0 | json.openArray(); |
234 | 0 | json.emitValues((llvh::ArrayRef<uint32_t>)segmentFunctionOffsets); |
235 | 0 | json.closeArray(); |
236 | 0 | } |
237 | 0 | json.closeDict(); |
238 | 0 | } |
239 | 0 | json.closeDict(); |
240 | 0 | OS.flush(); |
241 | 0 | } |
242 | | |
243 | | void SourceMapGenerator::addFunctionOffsets( |
244 | | std::vector<uint32_t> &&functionOffsets, |
245 | 0 | uint32_t segmentID) { |
246 | 0 | assert(functionOffsets.size() > 0 && "functionOffsets can not be empty"); |
247 | 0 | functionOffsets_[segmentID] = std::move(functionOffsets); |
248 | 0 | } |
249 | | |
250 | | } // namespace hermes |