// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. /// Represents a mapping from a range of generated instructions to some source /// code. class SourceMapping { /// Start offset of mapped instructions. final int instructionOffset; /// Source info for the mapped instructions starting at [instructionOffset]. /// /// When `null`, the mapping effectively makes the code unmapped. This is /// useful for compiler-generated code that doesn't correstpond to any lines /// in the source. final SourceInfo? sourceInfo; SourceMapping._(this.instructionOffset, this.sourceInfo); SourceMapping( this.instructionOffset, Uri fileUri, int line, int col, String? name, ) : sourceInfo = SourceInfo(fileUri, line, col, name); SourceMapping.unmapped(this.instructionOffset) : sourceInfo = null; SourceMapping shiftBy(int shift) { if (shift == 0) return this; return SourceMapping._(shift + instructionOffset, sourceInfo); } @override String toString() => '$instructionOffset -> $sourceInfo'; } class SourceInfo { /// URI of the compiled code's file. final Uri fileUri; /// 0-based line number of the compiled code. final int line; /// 0-based column number of the compiled code. final int col; /// Name of the mapped code. This is usually the name of the function that /// contains the code. final String? name; SourceInfo(this.fileUri, this.line, this.col, this.name); @override String toString() => '$fileUri:$line:$col ($name)'; @override bool operator ==(Object other) { if (identical(this, other)) { return true; } if (other is! SourceInfo) { return false; } return fileUri == other.fileUri && line == other.line && col == other.col && name == other.name; } @override int get hashCode => Object.hash(fileUri, line, col, name); } class SourceMapSerializer { final List mappings = []; void addMapping(int instructionOffset, SourceInfo? sourceInfo) { final mapping = SourceMapping._(instructionOffset, sourceInfo); mappings.add(mapping); } void copyMappings(SourceMapSerializer other, int offset) { for (final mapping in other.mappings) { mappings.add( SourceMapping._(mapping.instructionOffset + offset, mapping.sourceInfo), ); } } Map serializeAsJson() => _sourceMapToJson(mappings); } Map _sourceMapToJson(List mappings) { final Set sourcesSet = {}; for (final mapping in mappings) { if (mapping.sourceInfo?.fileUri != null) { sourcesSet.add(mapping.sourceInfo!.fileUri); } } final List sourcesList = sourcesSet.toList(); // Maps sources to their indices in the 'sources' list. final Map sourceIndices = {}; for (Uri source in sourcesList) { sourceIndices[source] = sourceIndices.length; } final Set namesSet = {}; for (final mapping in mappings) { if (mapping.sourceInfo?.name != null) { namesSet.add(mapping.sourceInfo!.name!); } } final List namesList = namesSet.toList(); // Maps names to their index in the 'names' list. final Map nameIndices = {}; for (String name in namesList) { nameIndices[name] = nameIndices.length; } // Generate the 'mappings' field. final StringBuffer mappingsStr = StringBuffer(); int lastTargetColumn = 0; int lastSourceIndex = 0; int lastSourceLine = 0; int lastSourceColumn = 0; int lastNameIndex = 0; bool first = true; for (int i = 0; i < mappings.length; ++i) { final mapping = mappings[i]; final sourceInfo = mapping.sourceInfo; if (sourceInfo == null && first) { // Initial parts of the code will be unmapped my default, we don't need to // explicitly unmap them. More importantly, current version of binaryen // cannot handle single-segment mappings at the beginning of the mappings. // We can remove this block of code after switching to a version with // https://github.com/WebAssembly/binaryen/pull/6794. continue; } first = false; lastTargetColumn = _encodeVLQ( mappingsStr, mapping.instructionOffset, lastTargetColumn, ); if (sourceInfo != null) { final sourceIndex = sourceIndices[sourceInfo.fileUri]!; lastSourceIndex = _encodeVLQ(mappingsStr, sourceIndex, lastSourceIndex); lastSourceLine = _encodeVLQ(mappingsStr, sourceInfo.line, lastSourceLine); lastSourceColumn = _encodeVLQ( mappingsStr, sourceInfo.col, lastSourceColumn, ); if (sourceInfo.name != null) { final nameIndex = nameIndices[sourceInfo.name!]!; lastNameIndex = _encodeVLQ(mappingsStr, nameIndex, lastNameIndex); } } if (i != mappings.length - 1) { mappingsStr.write(','); } } return { "version": 3, "sources": sourcesList.map((uri) => uri.toString()).toList(), "names": namesList, "mappings": mappingsStr.toString(), }; } /// Writes the VLQ of delta between [value] and [offset] into [output] and /// return [value]. int _encodeVLQ(StringSink output, int value, int offset) { int delta = value - offset; int signBit = 0; if (delta < 0) { signBit = 1; delta = -delta; } delta = (delta << 1) | signBit; do { int digit = delta & _vlqBaseMask; delta >>= _vlqBaseShift; if (delta > 0) { digit |= _vlqContinuationBit; } output.write(_base64Digits[digit]); } while (delta > 0); return value; } const int _vlqBaseShift = 5; const int _vlqBaseMask = (1 << 5) - 1; const int _vlqContinuationBit = 1 << 5; const String _base64Digits = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn' 'opqrstuvwxyz0123456789+/';