| /* | |
| * Copyright 2024 WebAssembly Community Group participants | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| #include "source-map.h" | |
| #include "ir/module-utils.h" | |
| #include "print-test.h" | |
| #include "wasm-builder.h" | |
| #include "gmock/gmock-matchers.h" | |
| #include "gtest/gtest.h" | |
| using namespace wasm; | |
| class SourceMapTest : public PrintTest { | |
| std::vector<char> buffer; | |
| protected: | |
| Module wasm; | |
| std::unique_ptr<SourceMapReader> reader; | |
| void SetUp() override { | |
| PrintTest::SetUp(); | |
| reader.reset(); | |
| parseWast(wasm, "(module)"); | |
| } | |
| void parseMap(std::string& sourceMap) { | |
| buffer = {sourceMap.begin(), sourceMap.end()}; | |
| reader.reset(new SourceMapReader(buffer)); | |
| reader->parse(wasm); | |
| } | |
| void ExpectDbgLocEq(size_t location, | |
| BinaryLocation file, | |
| BinaryLocation line, | |
| BinaryLocation col, | |
| std::optional<BinaryLocation> sym) { | |
| auto loc_str = std::stringstream() << "location: " << location; | |
| SCOPED_TRACE(loc_str.str()); | |
| auto loc = reader->readDebugLocationAt(location); | |
| ASSERT_TRUE(loc.has_value()); | |
| EXPECT_EQ(loc->fileIndex, file); | |
| EXPECT_EQ(loc->lineNumber, line); | |
| EXPECT_EQ(loc->columnNumber, col); | |
| EXPECT_EQ(loc->symbolNameIndex, sym); | |
| } | |
| void ExpectParseError(std::string& mapString, const char* expectedError) { | |
| SCOPED_TRACE(mapString); | |
| EXPECT_THROW(parseMap(mapString), MapParseException); | |
| try { | |
| parseMap(mapString); | |
| } catch (MapParseException ex) { | |
| EXPECT_THAT(ex.errorText, ::testing::HasSubstr(expectedError)); | |
| } | |
| } | |
| }; | |
| // Check that debug location parsers can handle single-segment mappings. | |
| TEST_F(SourceMapTest, SourceMappingSingleSegment) { | |
| // A single-segment mapping starting at offset 0. | |
| std::string sourceMap = R"( | |
| { | |
| "version": 3, | |
| "sources": [], | |
| "sourcesContent": [], | |
| "names": [], | |
| "mappings": "A" | |
| } | |
| )"; | |
| parseMap(sourceMap); | |
| auto loc = reader->readDebugLocationAt(0); | |
| EXPECT_FALSE(loc.has_value()); | |
| } | |
| TEST_F(SourceMapTest, BadSourceMaps) { | |
| // Test that a malformed JSON string throws rather than asserting. | |
| std::string sourceMap = R"( | |
| { | |
| "version": 3, | |
| "sources": ["foo.c"], | |
| "mappings": "" | |
| malformed | |
| } | |
| )"; | |
| ExpectParseError(sourceMap, "malformed value in JSON object"); | |
| // Valid JSON, but missing the version field. | |
| sourceMap = R"( | |
| { | |
| "sources": [], | |
| "names": [], | |
| "mappings": "A" | |
| } | |
| )"; | |
| ExpectParseError(sourceMap, "Source map version missing"); | |
| // Valid JSON, but a bad "sources" field. | |
| sourceMap = R"( | |
| { | |
| "version": 3, | |
| "sources": 123, | |
| "mappings": "" | |
| } | |
| )"; | |
| ExpectParseError(sourceMap, "Source map sources missing or not an array"); | |
| sourceMap = R"( | |
| { | |
| "version": 3, | |
| "sources": ["foo.c"], | |
| "mappings": "C;A" | |
| } | |
| )"; | |
| parseMap(sourceMap); | |
| // Mapping strings are parsed incrementally, so errors don't show up until a | |
| // sufficiently far-advanced location is requested to reach the problem. | |
| EXPECT_THROW(reader->readDebugLocationAt(1), MapParseException); | |
| // VLQ with too many continuation digits. 7 continuation characters ('g') | |
| // push the shift to 35, exceeding the uint32_t width. This is a malformed | |
| // VLQ that should be rejected rather than causing undefined behavior. | |
| sourceMap = R"( | |
| { | |
| "version": 3, | |
| "sources": ["foo.c"], | |
| "mappings": "gggggggA" | |
| } | |
| )"; | |
| ExpectParseError(sourceMap, "VLQ value too large"); | |
| } | |
| TEST_F(SourceMapTest, SourcesAndNames) { | |
| std::string sourceMap = R"( | |
| { | |
| "version": 3, | |
| "sources": ["foo.c", "bar.c"], | |
| "names": ["foo", "bar"], | |
| "mappings": "" | |
| } | |
| )"; | |
| parseMap(sourceMap); | |
| EXPECT_EQ(wasm.debugInfoFileNames.size(), 2); | |
| EXPECT_EQ(wasm.debugInfoFileNames[0], "foo.c"); | |
| EXPECT_EQ(wasm.debugInfoFileNames[1], "bar.c"); | |
| EXPECT_EQ(wasm.debugInfoSymbolNames.size(), 2); | |
| EXPECT_EQ(wasm.debugInfoSymbolNames[0], "foo"); | |
| EXPECT_EQ(wasm.debugInfoSymbolNames[1], "bar"); | |
| } | |
| TEST_F(SourceMapTest, OptionalFields) { | |
| // The "names" field is optional. | |
| std::string sourceMap = R"( | |
| { | |
| "version": 3, | |
| "sources": [], | |
| "mappings": "A" | |
| } | |
| )"; | |
| parseMap(sourceMap); | |
| } | |
| // This map is taken from test/fib-dbg.wasm | |
| TEST_F(SourceMapTest, Fibonacci) { | |
| // Test mapping parsing and debug locs | |
| std::string sourceMap = R"( | |
| { | |
| "version":3, | |
| "sources":["fib.c"], | |
| "names":[], | |
| "mappings": "moBAEA,4BAKA,QAJA,OADA,OAAA,uCAKA" | |
| } | |
| )"; | |
| parseMap(sourceMap); | |
| // Location before the first record has no value | |
| auto loc = reader->readDebugLocationAt(642); | |
| EXPECT_FALSE(loc.has_value()); | |
| // TODO: These column numbers show as 0 but emsymbolizer.py prints them as 1. | |
| // Figure out which is right. | |
| // First location | |
| ExpectDbgLocEq(643, 0, 3, 0, std::nullopt); | |
| // locations in between records have the same value as the previous one. | |
| for (size_t l = 644; l < 671; l++) { | |
| ExpectDbgLocEq(l, 0, 3, 0, std::nullopt); | |
| } | |
| // Subsequent ones are on record boundaries | |
| ExpectDbgLocEq(671, 0, 8, 0, std::nullopt); | |
| ExpectDbgLocEq(679, 0, 4, 0, std::nullopt); | |
| ExpectDbgLocEq(686, 0, 3, 0, std::nullopt); | |
| ExpectDbgLocEq(693, 0, 3, 0, std::nullopt); | |
| ExpectDbgLocEq(732, 0, 8, 0, std::nullopt); | |
| // Entries after the last record have the same value as the last one. | |
| ExpectDbgLocEq(733, 0, 8, 0, std::nullopt); | |
| // Should we return empty values for locations that are past the end of the | |
| // program? | |
| ExpectDbgLocEq(9999, 0, 8, 0, std::nullopt); | |
| } | |
| TEST_F(SourceMapTest, SourceMapSourceRootFile) { | |
| std::string sourceMap = R"( | |
| { | |
| "version":3, | |
| "file": "foo.wasm", | |
| "sources":[], | |
| "names":[], | |
| "mappings": "", | |
| "sourceRoot": "/foo/bar" | |
| } | |
| )"; | |
| parseMap(sourceMap); | |
| EXPECT_EQ(wasm.debugInfoSourceRoot, "/foo/bar"); | |
| EXPECT_EQ(wasm.debugInfoFile, "foo.wasm"); | |
| } | |
| TEST_F(SourceMapTest, SourcesContent) { | |
| // The backslash escapes appear in the JSON encoding, and are preserved in | |
| // the internal representation. The string values are uninterpreted in | |
| // Binaryen, and they are written directly back out without re-encoding. | |
| std::string sourceMap = R"( | |
| { | |
| "version": 3, | |
| "sources": ["foo.c"], | |
| "sourcesContent": ["#include <stdio.h> int main()\n{ printf(\"Gr\u00fc\u00df Gott, Welt!\"); return 0;}"], | |
| "mappings" : "" | |
| } | |
| )"; | |
| parseMap(sourceMap); | |
| ASSERT_EQ(wasm.debugInfoSourcesContent.size(), 1); | |
| EXPECT_EQ(wasm.debugInfoSourcesContent[0], | |
| "#include <stdio.h> int main()\\n{ printf(\\\"Gr\\u00fc\\u00df " | |
| "Gott, Welt!\\\"); return 0;}"); | |
| } | |
| // Regression test: updateSymbol calls for prologLocation/epilogLocation were | |
| // inside the debugLocations loop instead of outside. When debugLocations is | |
| // empty, the loop never executes and the locations are not updated. | |
| TEST(SourceMapCopyTest, UpdateSymbolOutsideLoop) { | |
| Module srcModule; | |
| Module dstModule; | |
| Builder builder(srcModule); | |
| auto func = builder.makeFunction( | |
| "test", {}, Signature(Type::none, Type::none), {}, builder.makeNop()); | |
| // Set prologLocation and epilogLocation with symbolNameIndex values, | |
| // but leave debugLocations empty. | |
| func->prologLocation = | |
| Function::DebugLocation{0, 1, 1, std::optional<BinaryLocation>(0)}; | |
| func->epilogLocation = | |
| Function::DebugLocation{0, 2, 1, std::optional<BinaryLocation>(1)}; | |
| // Symbol name index map: 0->10, 1->11. | |
| std::vector<Index> symbolNameIndexMap = {10, 11}; | |
| auto copied = ModuleUtils::copyFunctionWithoutAdd( | |
| func.get(), dstModule, Name(), std::nullopt, symbolNameIndexMap); | |
| ASSERT_TRUE(copied->prologLocation.has_value()); | |
| ASSERT_TRUE(copied->prologLocation->symbolNameIndex.has_value()); | |
| EXPECT_EQ(*copied->prologLocation->symbolNameIndex, 10u); | |
| ASSERT_TRUE(copied->epilogLocation.has_value()); | |
| ASSERT_TRUE(copied->epilogLocation->symbolNameIndex.has_value()); | |
| EXPECT_EQ(*copied->epilogLocation->symbolNameIndex, 11u); | |
| } | |
| // When debugLocations has multiple entries, updateSymbol should only run once | |
| // per location, not once per loop iteration (which would double-remap). | |
| TEST(SourceMapCopyTest, UpdateSymbolNotDoubleRemapped) { | |
| Module srcModule; | |
| Module dstModule; | |
| Builder builder(srcModule); | |
| auto func = builder.makeFunction( | |
| "test", {}, Signature(Type::none, Type::none), {}, builder.makeNop()); | |
| func->prologLocation = | |
| Function::DebugLocation{0, 1, 1, std::optional<BinaryLocation>(0)}; | |
| // Add multiple debug locations so the loop runs multiple times. | |
| auto* nop1 = srcModule.allocator.alloc<Nop>(); | |
| auto* nop2 = srcModule.allocator.alloc<Nop>(); | |
| func->debugLocations[nop1] = | |
| Function::DebugLocation{0, 10, 1, std::optional<BinaryLocation>(0)}; | |
| func->debugLocations[nop2] = | |
| Function::DebugLocation{0, 11, 1, std::optional<BinaryLocation>(1)}; | |
| // Map: 0->5, 1->6. | |
| std::vector<Index> symbolNameIndexMap = {5, 6}; | |
| auto copied = ModuleUtils::copyFunctionWithoutAdd( | |
| func.get(), dstModule, Name(), std::nullopt, symbolNameIndexMap); | |
| // Should be remapped exactly once: 0 -> 5. | |
| ASSERT_TRUE(copied->prologLocation.has_value()); | |
| ASSERT_TRUE(copied->prologLocation->symbolNameIndex.has_value()); | |
| EXPECT_EQ(*copied->prologLocation->symbolNameIndex, 5u); | |
| } |