Coverage Report

Created: 2025-08-28 06:48

/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