* Add POC source map unit tests

* Implement apply source map

* Modify tests

* Remove combine maps

* Remove ambiguous line tests. Add null test

* Tabify files

* Make tests more explicit. Add column info

* Add a no matching mappings test

* Updates to address code reviews

* Remove ICloneable

* Update README
This commit is contained in:
Tien 2017-04-15 11:27:33 -07:00 коммит произвёл Christian Gonzalez
Родитель b71be0f6f8
Коммит e44bd1cd27
8 изменённых файлов: 478 добавлений и 55 удалений

Просмотреть файл

@ -77,6 +77,42 @@ string serializedMap = generator.SerializeMapping(sourceMap);
File.WriteAllText(@"updatedSample.sourcemap", serializedMap);
```
### Chaining source maps
A common use case when dealing with source maps is multiple mapping layers. You can use `ApplySourceMap` to chain maps together to link back to the source
```csharp
SourcePosition inOriginal = new SourcePosition { ZeroBasedLineNumber = 34, ZeroBasedColumnNumber = 23 };
SourcePosition inBundled = new SourcePosition { ZeroBasedLineNumber = 23, ZeroBasedColumnNumber = 12 };
SourcePosition inMinified = new SourcePosition { ZeroBasedLineNumber = 3, ZeroBasedColumnNumber = 2 };
MappingEntry originalToBundledEntry = new MappingEntry {
GeneratedSourcePosition = inBundled,
OriginalSourcePosition = inOriginal,
OriginalFileName = "original.js"
};
MappingEntry bundledToMinifiedEntry = new MappingEntry {
GeneratedSourcePosition = inMinified,
OriginalSourcePosition = inBundled,
OriginalFileName = "bundle.js"
};
SourceMap bundledToOriginal = new SourceMap {
File = "bundled.js",
Sources = new List<string> { "original.js" },
ParsedMappings = new List<MappingEntry> { originalToBundledEntry }
}
SourceMap minifiedToBundled = new SourceMap {
File = "bundled.min.js",
Sources = new List<string> { "bundled.js" },
ParsedMappings = new List<MappingEntry> { bundledToMinifiedEntry }
}
// will contain mapping for line 3, column 2 in the minified file to line 34, column 23 in the original file
SourceMap minifiedToOriginal = minifiedToBundled.ApplySourceMap(bundledToOriginal);
```
## Call Stack Deminification
The `SourcemapToolkit.CallstackDeminifier.dll` allows for the deminification of JavaScript call stacks.
### Example

Просмотреть файл

@ -1,25 +1,47 @@
namespace SourcemapToolkit.SourcemapParser
using System;
namespace SourcemapToolkit.SourcemapParser
{
public class MappingEntry
{
/// <summary>
/// The location of the line of code in the transformed code
/// </summary>
public SourcePosition GeneratedSourcePosition;
public class MappingEntry
{
/// <summary>
/// The location of the line of code in the transformed code
/// </summary>
public SourcePosition GeneratedSourcePosition;
/// <summary>
/// The location of the code in the original source code
/// </summary>
public SourcePosition OriginalSourcePosition;
/// <summary>
/// The location of the code in the original source code
/// </summary>
public SourcePosition OriginalSourcePosition;
/// <summary>
/// The original name of the code referenced by this mapping entry
/// </summary>
public string OriginalName;
/// <summary>
/// The original name of the code referenced by this mapping entry
/// </summary>
public string OriginalName;
/// <summary>
/// The name of the file that originally contained this code
/// </summary>
public string OriginalFileName;
}
/// <summary>
/// The name of the file that originally contained this code
/// </summary>
public string OriginalFileName;
public MappingEntry Clone()
{
return new MappingEntry
{
GeneratedSourcePosition = this.GeneratedSourcePosition as SourcePosition,
OriginalSourcePosition = this.OriginalSourcePosition as SourcePosition,
OriginalFileName = this.OriginalFileName,
OriginalName = this.OriginalName
};
}
public Boolean IsValueEqual(MappingEntry anEntry)
{
return (
this.OriginalName == anEntry.OriginalName &&
this.OriginalFileName == anEntry.OriginalFileName &&
this.GeneratedSourcePosition.CompareTo(anEntry.GeneratedSourcePosition) == 0 &&
this.OriginalSourcePosition.CompareTo(anEntry.OriginalSourcePosition) == 0);
}
}
}

Просмотреть файл

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
namespace SourcemapToolkit.SourcemapParser
{
@ -35,17 +36,94 @@ namespace SourcemapToolkit.SourcemapParser
public List<MappingEntry> ParsedMappings;
/// <summary>
/// Finds the mapping entry for the generated source position. If no exact match is found, it will attempt
/// Applies the mappings of a sub source map to the current source map
/// Each mapping to the supplied source file is rewritten using the supplied source map
/// This is useful in situations where we have a to b to c, with mappings ba.map and cb.map
/// Calling cb.ApplySourceMap(ba) will return mappings from c to a (ca)
/// <param name="submap">The submap to apply</param>
/// <param name="sourceFile">The filename of the source file. If not specified, submap's File property will be used</param>
/// <returns>A new source map</returns>
/// </summary>
public SourceMap ApplySourceMap(SourceMap submap, string sourceFile = null)
{
if (submap == null)
{
throw new ArgumentNullException(nameof(submap));
}
if (sourceFile == null)
{
if (submap.File == null)
{
throw new Exception("ApplySourceMap expects either the explicit source file to the map, or submap's 'file' property");
}
sourceFile = submap.File;
}
SourceMap newSourceMap = new SourceMap
{
File = this.File,
Version = this.Version,
Sources = new List<string>(),
Names = new List<string>(),
ParsedMappings = new List<MappingEntry>()
};
// transform mappings in this source map
foreach (MappingEntry mappingEntry in this.ParsedMappings)
{
MappingEntry newMappingEntry = mappingEntry.Clone();
if (mappingEntry.OriginalFileName == sourceFile && mappingEntry.OriginalSourcePosition != null)
{
MappingEntry correspondingSubMapMappingEntry = submap.GetMappingEntryForGeneratedSourcePosition(mappingEntry.OriginalSourcePosition);
if (correspondingSubMapMappingEntry != null)
{
// Copy the mapping
newMappingEntry = new MappingEntry
{
GeneratedSourcePosition = mappingEntry.GeneratedSourcePosition.Clone(),
OriginalSourcePosition = correspondingSubMapMappingEntry.OriginalSourcePosition.Clone(),
OriginalName = correspondingSubMapMappingEntry.OriginalName?? mappingEntry.OriginalName,
OriginalFileName = correspondingSubMapMappingEntry.OriginalFileName?? mappingEntry.OriginalFileName
};
}
}
// Copy into "Sources" and "Names"
string originalFileName = newMappingEntry.OriginalFileName;
string originalName = newMappingEntry.OriginalName;
if (originalFileName != null && !newSourceMap.Sources.Contains(originalFileName))
{
newSourceMap.Sources.Add(originalFileName);
}
if (originalName != null && !newSourceMap.Names.Contains(originalName))
{
newSourceMap.Names.Add(originalName);
}
newSourceMap.ParsedMappings.Add(newMappingEntry);
};
return newSourceMap;
}
/// <summary>
/// Finds the mapping entry for the generated source position. If no exact match is found, it will attempt
/// to return a nearby mapping that should map to the same piece of code.
/// </summary>
/// <param name="generatedSourcePosition">The location in generated code for which we want to discover a mapping entry</param>
/// <returns>A mapping entry that is a close match for the desired generated code location</returns>
public virtual MappingEntry GetMappingEntryForGeneratedSourcePosition(SourcePosition generatedSourcePosition)
{
if (ParsedMappings == null)
{
return null;
}
public virtual MappingEntry GetMappingEntryForGeneratedSourcePosition(SourcePosition generatedSourcePosition)
{
if (ParsedMappings == null)
{
return null;
}
MappingEntry mappingEntryToFind = new MappingEntry
{
@ -67,6 +145,6 @@ namespace SourcemapToolkit.SourcemapParser
}
return index >= 0 ? ParsedMappings[index] : null;
}
}
}
}

Просмотреть файл

@ -2,34 +2,34 @@
namespace SourcemapToolkit.SourcemapParser
{
/// <summary>
/// Identifies the location of a piece of code in a JavaScript file
/// </summary>
public class SourcePosition : IComparable<SourcePosition>
{
public int ZeroBasedLineNumber;
/// <summary>
/// Identifies the location of a piece of code in a JavaScript file
/// </summary>
public class SourcePosition : IComparable<SourcePosition>
{
public int ZeroBasedLineNumber;
public int ZeroBasedColumnNumber;
public int ZeroBasedColumnNumber;
public int CompareTo(SourcePosition other)
{
if (this.ZeroBasedLineNumber == other.ZeroBasedLineNumber)
{
return this.ZeroBasedColumnNumber.CompareTo(other.ZeroBasedColumnNumber);
}
public int CompareTo(SourcePosition other)
{
if (this.ZeroBasedLineNumber == other.ZeroBasedLineNumber)
{
return this.ZeroBasedColumnNumber.CompareTo(other.ZeroBasedColumnNumber);
}
return this.ZeroBasedLineNumber.CompareTo(other.ZeroBasedLineNumber);
}
return this.ZeroBasedLineNumber.CompareTo(other.ZeroBasedLineNumber);
}
public static bool operator <(SourcePosition x, SourcePosition y)
{
return x.CompareTo(y) < 0;
}
public static bool operator <(SourcePosition x, SourcePosition y)
{
return x.CompareTo(y) < 0;
}
public static bool operator >(SourcePosition x, SourcePosition y)
{
return x.CompareTo(y) > 0;
}
public static bool operator >(SourcePosition x, SourcePosition y)
{
return x.CompareTo(y) > 0;
}
/// <summary>
/// Returns true if we think that the two source positions are close enough together that they may in fact be the referring to the same piece of code.
@ -57,5 +57,14 @@ namespace SourcemapToolkit.SourcemapParser
return false;
}
}
public SourcePosition Clone()
{
return new SourcePosition
{
ZeroBasedLineNumber = this.ZeroBasedLineNumber,
ZeroBasedColumnNumber = this.ZeroBasedColumnNumber
};
}
}
}

Просмотреть файл

@ -1,5 +1,6 @@
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
namespace SourcemapToolkit.SourcemapParser.UnitTests
{
@ -11,7 +12,7 @@ namespace SourcemapToolkit.SourcemapParser.UnitTests
{
// Arrange
SourceMap sourceMap = new SourceMap();
SourcePosition sourcePosition = new SourcePosition {ZeroBasedColumnNumber = 3, ZeroBasedLineNumber = 4};
SourcePosition sourcePosition = new SourcePosition {ZeroBasedColumnNumber = 3, ZeroBasedLineNumber = 4};
// Act
MappingEntry result = sourceMap.GetMappingEntryForGeneratedSourcePosition(sourcePosition);
@ -118,5 +119,270 @@ namespace SourcemapToolkit.SourcemapParser.UnitTests
// Asset
Assert.AreEqual(matchingMappingEntry, result);
}
}
private MappingEntry getSimpleEntry(SourcePosition generatedSourcePosition, SourcePosition originalSourcePosition, string originalFileName)
{
return new MappingEntry
{
GeneratedSourcePosition = generatedSourcePosition,
OriginalSourcePosition = originalSourcePosition,
OriginalFileName = originalFileName
};
}
private SourcePosition generateSourcePosition(int lineNumber, int colNumber = 0)
{
return new SourcePosition
{
ZeroBasedLineNumber = lineNumber,
ZeroBasedColumnNumber = colNumber
};
}
[TestMethod]
public void GetRootMappingEntryForGeneratedSourcePosition_NoChildren_ReturnsSameEntry()
{
// Arrange
SourcePosition generated1 = generateSourcePosition(lineNumber:2, colNumber: 5);
SourcePosition original1 = generateSourcePosition(lineNumber:1, colNumber: 5);
MappingEntry mappingEntry = getSimpleEntry(generated1, original1, "generated.js");
SourceMap sourceMap = new SourceMap
{
Sources = new List<string> { "generated.js" },
ParsedMappings = new List<MappingEntry> { mappingEntry }
};
// Act
MappingEntry rootEntry = sourceMap.GetMappingEntryForGeneratedSourcePosition(generated1);
// Assert
Assert.AreEqual(rootEntry, mappingEntry);
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void ApplyMap_NullSubmap_ThrowsException()
{
// Arrange
SourcePosition generated2 = generateSourcePosition(lineNumber:3, colNumber: 5);
SourcePosition original2 = generateSourcePosition(lineNumber:2, colNumber: 5);
MappingEntry mapping = getSimpleEntry(generated2, original2, "sourceOne.js");
SourceMap map = new SourceMap
{
File = "generated.js",
Sources = new List<string> { "sourceOne.js" },
ParsedMappings = new List<MappingEntry> { mapping }
};
// Act
SourceMap combinedMap = map.ApplySourceMap(null);
// Assert (decorated expected exception)
}
[TestMethod]
public void ApplyMap_NoMatchingSources_ReturnsSameMap()
{
// Arrange
SourcePosition generated1 = generateSourcePosition(lineNumber:2, colNumber: 3);
SourcePosition original1 = generateSourcePosition(lineNumber:1, colNumber: 2);
MappingEntry childMapping = getSimpleEntry(generated1, original1, "someOtherSource.js");
SourceMap childMap = new SourceMap
{
File = "notSourceOne.js",
Sources = new List<string> { "someOtherSource.js" },
ParsedMappings = new List<MappingEntry> { childMapping }
};
SourcePosition generated2 = generateSourcePosition(lineNumber:3, colNumber: 7);
SourcePosition original2 = generateSourcePosition(lineNumber:2, colNumber: 3);
MappingEntry parentMapping = getSimpleEntry(generated2, original2, "sourceOne.js");
SourceMap parentMap = new SourceMap
{
File = "generated.js",
Sources = new List<string> { "sourceOne.js" },
ParsedMappings = new List<MappingEntry> { parentMapping }
};
// Act
SourceMap combinedMap = parentMap.ApplySourceMap(childMap);
// Assert
Assert.IsNotNull(combinedMap);
MappingEntry firstMapping = combinedMap.ParsedMappings[0];
Assert.IsTrue(firstMapping.IsValueEqual(parentMapping));
}
[TestMethod]
public void ApplyMap_NoMatchingMappings_ReturnsSameMap()
{
// Arrange
SourcePosition generated1 = generateSourcePosition(lineNumber: 2, colNumber:2);
SourcePosition original1 = generateSourcePosition(lineNumber: 1, colNumber:10);
MappingEntry childMapping = getSimpleEntry(generated1, original1, "sourceTwo.js");
SourceMap childMap = new SourceMap
{
File = "sourceOne.js",
Sources = new List<string> { "sourceTwo.js" },
ParsedMappings = new List<MappingEntry> { childMapping }
};
SourcePosition generated2 = generateSourcePosition(lineNumber:3, colNumber:4);
SourcePosition original2 = generateSourcePosition(lineNumber:2, colNumber:5);
MappingEntry parentMapping = getSimpleEntry(generated2, original2, "sourceOne.js");
SourceMap parentMap = new SourceMap
{
File = "generated.js",
Sources = new List<string> { "sourceOne.js" },
ParsedMappings = new List<MappingEntry> { parentMapping }
};
// Act
SourceMap combinedMap = parentMap.ApplySourceMap(childMap);
// Assert
Assert.IsNotNull(combinedMap);
MappingEntry firstMapping = combinedMap.ParsedMappings[0];
Assert.IsTrue(firstMapping.IsValueEqual(parentMapping));
}
[TestMethod]
public void ApplyMap_MatchingSources_ReturnsCorrectMap()
{
// Expect mapping with same source filename as the applied source-map to be replaced
// Arrange
SourcePosition generated1 = generateSourcePosition(lineNumber:2, colNumber:4);
SourcePosition original1 = generateSourcePosition(lineNumber:1, colNumber:3);
MappingEntry childMapping = getSimpleEntry(generated1, original1, "sourceTwo.js");
SourceMap childMap = new SourceMap
{
File = "sourceOne.js",
Sources = new List<string> { "sourceTwo.js" },
ParsedMappings = new List<MappingEntry> { childMapping }
};
SourcePosition generated2 = generateSourcePosition(lineNumber:3, colNumber: 5);
SourcePosition original2 = generateSourcePosition(lineNumber:2, colNumber: 4);
MappingEntry parentMapping = getSimpleEntry(generated2, original2, "sourceOne.js");
SourceMap parentMap = new SourceMap
{
File = "generated.js",
Sources = new List<string> { "sourceOne.js" },
ParsedMappings = new List<MappingEntry> { parentMapping }
};
// Act
SourceMap combinedMap = parentMap.ApplySourceMap(childMap);
// Assert
Assert.IsNotNull(combinedMap);
Assert.AreEqual(1, combinedMap.ParsedMappings.Count);
Assert.AreEqual(1, combinedMap.Sources.Count);
MappingEntry rootMapping = combinedMap.GetMappingEntryForGeneratedSourcePosition(generated2);
Assert.AreEqual(0, rootMapping.OriginalSourcePosition.CompareTo(childMapping.OriginalSourcePosition));
}
[TestMethod]
public void ApplyMap_PartialMatchingSources_ReturnsCorrectMap()
{
// Expect mappings with same source filename as the applied source-map to be replaced
// mappings with a different source filename should stay the same
// Arrange
SourcePosition generated1 = generateSourcePosition(lineNumber:2, colNumber:10);
SourcePosition original1 = generateSourcePosition(lineNumber:1, colNumber:5);
MappingEntry childMapping = getSimpleEntry(generated1, original1, "sourceTwo.js");
SourceMap childMap = new SourceMap
{
File = "sourceOne.js",
Sources = new List<string> { "sourceTwo.js" },
ParsedMappings = new List<MappingEntry> { childMapping }
};
SourcePosition generated2 = generateSourcePosition(lineNumber:3, colNumber:2);
SourcePosition original2 = generateSourcePosition(lineNumber:2, colNumber: 10);
MappingEntry mapping = getSimpleEntry(generated2, original2, "sourceOne.js");
SourcePosition generated3 = generateSourcePosition(lineNumber:4, colNumber:3);
SourcePosition original3 = generateSourcePosition(lineNumber:3, colNumber:2);
MappingEntry mapping2 = getSimpleEntry(generated3, original3, "noMapForThis.js");
SourceMap parentMap = new SourceMap
{
File = "generated.js",
Sources = new List<string> { "sourceOne.js", "noMapForThis.js" },
ParsedMappings = new List<MappingEntry> { mapping, mapping2 }
};
// Act
SourceMap combinedMap = parentMap.ApplySourceMap(childMap);
// Assert
Assert.IsNotNull(combinedMap);
Assert.AreEqual(2, combinedMap.ParsedMappings.Count);
Assert.AreEqual(2, combinedMap.Sources.Count);
MappingEntry firstCombinedMapping = combinedMap.GetMappingEntryForGeneratedSourcePosition(generated3);
Assert.IsTrue(firstCombinedMapping.IsValueEqual(mapping2));
MappingEntry secondCombinedMapping = combinedMap.GetMappingEntryForGeneratedSourcePosition(generated2);
Assert.AreEqual(0, secondCombinedMapping.OriginalSourcePosition.CompareTo(childMapping.OriginalSourcePosition));
}
[TestMethod]
public void ApplyMap_ExactMatchDeep_ReturnsCorrectMappingEntry()
{
// Arrange
SourcePosition generated1 = generateSourcePosition(lineNumber:3, colNumber:5);
SourcePosition original1 = generateSourcePosition(lineNumber:2, colNumber:10);
MappingEntry mapLevel2 = getSimpleEntry(generated1, original1, "sourceThree.js");
SourceMap grandChildMap = new SourceMap
{
File = "sourceTwo.js",
Sources = new List<string> { "sourceThree.js" },
ParsedMappings = new List<MappingEntry> { mapLevel2 }
};
SourcePosition generated2 = generateSourcePosition(lineNumber:4, colNumber:3);
SourcePosition original2 = generateSourcePosition(lineNumber:3, colNumber:5);
MappingEntry mapLevel1 = getSimpleEntry(generated2, original2, "sourceTwo.js");
SourceMap childMap = new SourceMap
{
File = "sourceOne.js",
Sources = new List<string> { "sourceTwo.js" },
ParsedMappings = new List<MappingEntry> { mapLevel1 }
};
SourcePosition generated3 = generateSourcePosition(lineNumber:5, colNumber:5);
SourcePosition original3 = generateSourcePosition(lineNumber:4, colNumber:3);
MappingEntry mapLevel0 = getSimpleEntry(generated3, original3, "sourceOne.js");
SourceMap parentMap = new SourceMap
{
File = "generated.js",
Sources = new List<string> { "sourceOne.js" },
ParsedMappings = new List<MappingEntry> { mapLevel0 }
};
// Act
SourceMap firstCombinedMap = parentMap.ApplySourceMap(childMap);
// Assert
Assert.IsNotNull(firstCombinedMap);
SourceMap secondCombinedMap = firstCombinedMap.ApplySourceMap(grandChildMap);
Assert.IsNotNull(secondCombinedMap);
MappingEntry rootMapping = secondCombinedMap.GetMappingEntryForGeneratedSourcePosition(generated3);
Assert.AreEqual(0, rootMapping.OriginalSourcePosition.CompareTo(mapLevel2.OriginalSourcePosition));
}
}
}

Просмотреть файл

@ -80,6 +80,16 @@
<Name>SourcemapToolkit.SourcemapParser</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Content Include="child1.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Content Include="child1.js.map">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<Choose>
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">
<ItemGroup>

Просмотреть файл

@ -0,0 +1 @@
//# sourceMappingURL=child1.js.map

Просмотреть файл

@ -0,0 +1 @@
{"version":3,"sources":["source.js"],"names":[],"mappings":";AAAA","file":"child1.js"}