diff --git a/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTest.cs b/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTest.cs
new file mode 100644
index 00000000..42037dfd
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTest.cs
@@ -0,0 +1,323 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+
+namespace Microsoft.VisualStudio.Jdt.Tests
+{
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using Xunit;
+
+ ///
+ /// Test class for
+ ///
+ public class JsonTransformationTest
+ {
+ private static readonly string SimpleSourceString = @"{ 'A': 1 }";
+
+ private readonly JsonTransformationTestLogger logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public JsonTransformationTest()
+ {
+ // xUnit creates a new instance of the class for each test, so a new logger is created
+ this.logger = new JsonTransformationTestLogger();
+ }
+
+ ///
+ /// Tests the error caused when an invalid verb is found
+ ///
+ [Fact]
+ public void InvalidVerb()
+ {
+ string transformString = @"{
+ '@jdt.invalid': false
+ }";
+
+ this.TryTransformTest(SimpleSourceString, transformString, false);
+
+ Assert.Empty(this.logger.MessageLog);
+ Assert.Empty(this.logger.WarningLog);
+
+ // The error should be where at the location of the invalid verb
+ LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 56, true);
+ }
+
+ ///
+ /// Tests the error caused by a verb having an invalid value
+ ///
+ [Fact]
+ public void InvalidVerbValue()
+ {
+ string transformString = @"{
+ '@jdt.remove': 10
+ }";
+
+ this.TryTransformTest(SimpleSourceString, transformString, false);
+
+ Assert.Empty(this.logger.MessageLog);
+ Assert.Empty(this.logger.WarningLog);
+
+ // The error location should be at the location of the invalid value
+ LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 58, true);
+ }
+
+ ///
+ /// Tests the error caused when an invalid attribute is found within a verb
+ ///
+ [Fact]
+ public void InvalidAttribute()
+ {
+ string transformString = @"{
+ '@jdt.replace': {
+ '@jdt.invalid': false
+ }
+ }";
+
+ this.TryTransformTest(SimpleSourceString, transformString, false);
+
+ Assert.Empty(this.logger.MessageLog);
+ Assert.Empty(this.logger.WarningLog);
+
+ // The error location should be at the position of the invalid attribute
+ LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 3, 58, true);
+ }
+
+ ///
+ /// Tests the error caused when a required attribute is not found
+ ///
+ [Fact]
+ public void MissingAttribute()
+ {
+ string transformString = @"{
+ '@jdt.rename': {
+ '@jdt.path': 'A'
+ }
+ }";
+
+ this.TryTransformTest(SimpleSourceString, transformString, false);
+
+ Assert.Empty(this.logger.MessageLog);
+ Assert.Empty(this.logger.WarningLog);
+
+ // The error location should be at the beginning of the object with the missing attribute
+ LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 57, true);
+ }
+
+ ///
+ /// Tests the error caused when a verb object contains attributes and other objects
+ ///
+ [Fact]
+ public void MixedAttributes()
+ {
+ string transformString = @"{
+ '@jdt.rename': {
+ '@jdt.path': 'A',
+ '@jdt.value': 'Astar',
+ 'NotAttribute': true
+ }
+ }";
+
+ this.TryTransformTest(SimpleSourceString, transformString, false);
+
+ Assert.Empty(this.logger.MessageLog);
+ Assert.Empty(this.logger.WarningLog);
+
+ // The error location should be at the beginning of the object with the mixed attribute
+ LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 57, true);
+ }
+
+ ///
+ /// Tests the error caused when an attribute has an incorrect value
+ ///
+ [Fact]
+ public void WrongAttributeValue()
+ {
+ string transformString = @"{
+ '@jdt.remove': {
+ '@jdt.path': false
+ }
+ }";
+
+ this.TryTransformTest(SimpleSourceString, transformString, false);
+
+ Assert.Empty(this.logger.MessageLog);
+ Assert.Empty(this.logger.WarningLog);
+
+ // The error location should be at the position of the invalid value
+ LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 3, 61, true);
+ }
+
+ ///
+ /// Tests the error caused when a path attribute returns no result
+ ///
+ [Fact]
+ public void RemoveNonExistantNode()
+ {
+ string transformString = @"{
+ '@jdt.remove': {
+ '@jdt.path': 'B'
+ }
+ }";
+
+ this.TryTransformTest(SimpleSourceString, transformString, true);
+
+ Assert.Empty(this.logger.MessageLog);
+ Assert.Empty(this.logger.ErrorLog);
+
+ // The warning location should be at the position of the path value that yielded no results
+ LogHasSingleEntry(this.logger.WarningLog, ErrorLocation.Transform.ToString(), 3, 59, false);
+ }
+
+ ///
+ /// Tests the error caused when attempting to remove the root node
+ ///
+ [Fact]
+ public void RemoveRoot()
+ {
+ string transformString = @"{
+ '@jdt.remove': true
+ }";
+
+ this.TryTransformTest(SimpleSourceString, transformString, false);
+
+ Assert.Empty(this.logger.MessageLog);
+ Assert.Empty(this.logger.WarningLog);
+
+ // The error location should be at the position of the remove value
+ LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 60, true);
+ }
+
+ ///
+ /// Tests the error when a rename value is invalid
+ ///
+ [Fact]
+ public void InvalidRenameValue()
+ {
+ string transformString = @"{
+ '@jdt.rename': {
+ 'A': 10
+ }
+ }";
+
+ this.TryTransformTest(SimpleSourceString, transformString, false);
+
+ Assert.Empty(this.logger.MessageLog);
+ Assert.Empty(this.logger.WarningLog);
+
+ // The error location should be at the position of the rename property
+ LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 3, 47, true);
+ }
+
+ ///
+ /// Tests the error caused when attempting to rename a non-existant node
+ ///
+ [Fact]
+ public void RenameNonExistantNode()
+ {
+ string transformString = @"{
+ '@jdt.rename': {
+ 'B': 'Bstar'
+ }
+ }";
+
+ this.TryTransformTest(SimpleSourceString, transformString, true);
+
+ Assert.Empty(this.logger.MessageLog);
+ Assert.Empty(this.logger.ErrorLog);
+
+ // The position of the warning should be a the beginning of the rename property
+ LogHasSingleEntry(this.logger.WarningLog, ErrorLocation.Transform.ToString(), 3, 47, false);
+ }
+
+ ///
+ /// Test the error when attempting to replace the root with a non-object token
+ ///
+ [Fact]
+ public void ReplaceRoot()
+ {
+ string transformString = @"{
+ '@jdt.replace': 10
+ }";
+
+ this.TryTransformTest(SimpleSourceString, transformString, false);
+
+ Assert.Empty(this.logger.MessageLog);
+ Assert.Empty(this.logger.WarningLog);
+
+ // The position of the error should be at the value of replace that caused it
+ LogHasSingleEntry(this.logger.ErrorLog, ErrorLocation.Transform.ToString(), 2, 59, true);
+ }
+
+ ///
+ /// Tests that an exception is thrown when is called
+ ///
+ [Fact]
+ public void ThrowAndLogException()
+ {
+ string transformString = @"{
+ '@jdt.invalid': false
+ }";
+ using (var transformStream = this.GetStreamFromString(transformString))
+ using (var sourceStream = this.GetStreamFromString(SimpleSourceString))
+ {
+ JsonTransformation transform = new JsonTransformation(transformStream, this.logger);
+ var exception = Record.Exception(() => transform.Apply(sourceStream));
+ Assert.NotNull(exception);
+ Assert.IsType(exception);
+ var jdtException = exception as JdtException;
+ Assert.Contains("invalid", jdtException.Message);
+ Assert.Equal(ErrorLocation.Transform, jdtException.Location);
+ Assert.Equal(2, jdtException.LineNumber);
+ Assert.Equal(56, jdtException.LinePosition);
+ }
+ }
+
+ private static void LogHasSingleEntry(List log, string fileName, int lineNumber, int linePosition, bool fromException)
+ {
+ Assert.Single(log);
+ var errorEntry = log.Single();
+ Assert.Equal(fileName, errorEntry.FileName);
+ Assert.Equal(lineNumber, errorEntry.LineNumber);
+ Assert.Equal(linePosition, errorEntry.LinePosition);
+ Assert.Equal(fromException, errorEntry.FromException);
+ }
+
+ private void TryTransformTest(string sourceString, string transformString, bool shouldTransformSucceed)
+ {
+ using (var transformStream = this.GetStreamFromString(transformString))
+ using (var sourceStream = this.GetStreamFromString(sourceString))
+ {
+ JsonTransformation transform = new JsonTransformation(transformStream, this.logger);
+ Stream result = null;
+
+ var exception = Record.Exception(() => result = transform.Apply(sourceStream));
+
+ if (shouldTransformSucceed)
+ {
+ Assert.NotNull(result);
+ Assert.Null(exception);
+ }
+ else
+ {
+ Assert.Null(result);
+ Assert.NotNull(exception);
+ Assert.IsType(exception);
+ }
+ }
+ }
+
+ private Stream GetStreamFromString(string s)
+ {
+ MemoryStream stringStream = new MemoryStream();
+ StreamWriter stringWriter = new StreamWriter(stringStream);
+ stringWriter.Write(s);
+ stringWriter.Flush();
+ stringStream.Position = 0;
+
+ return stringStream;
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTestLogger.cs b/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTestLogger.cs
new file mode 100644
index 00000000..466fb8bd
--- /dev/null
+++ b/src/Microsoft.VisualStudio.Jdt.Tests/JsonTransformationTestLogger.cs
@@ -0,0 +1,166 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+
+namespace Microsoft.VisualStudio.Jdt.Tests
+{
+ using System;
+ using System.Collections.Generic;
+
+ ///
+ /// Mock logger to test
+ ///
+ public class JsonTransformationTestLogger : IJsonTransformationLogger
+ {
+ ///
+ /// Gets the error log
+ ///
+ public List ErrorLog { get; } = new List();
+
+ ///
+ /// Gets the warning log
+ ///
+ public List WarningLog { get; } = new List();
+
+ ///
+ /// Gets the message log
+ ///
+ public List MessageLog { get; } = new List();
+
+ ///
+ public void LogError(string message)
+ {
+ this.ErrorLog.Add(new TestLogEntry()
+ {
+ Message = message,
+ FromException = false
+ });
+ }
+
+ ///
+ public void LogError(string message, string fileName, int lineNumber, int linePosition)
+ {
+ this.ErrorLog.Add(new TestLogEntry()
+ {
+ Message = message,
+ FileName = fileName,
+ LineNumber = lineNumber,
+ LinePosition = linePosition,
+ FromException = false
+ });
+ }
+
+ ///
+ public void LogErrorFromException(Exception ex)
+ {
+ this.ErrorLog.Add(new TestLogEntry()
+ {
+ Message = ex.Message,
+ FromException = false
+ });
+ }
+
+ ///
+ public void LogErrorFromException(Exception ex, string fileName, int lineNumber, int linePosition)
+ {
+ this.ErrorLog.Add(new TestLogEntry()
+ {
+ Message = ex.Message,
+ FileName = fileName,
+ LineNumber = lineNumber,
+ LinePosition = linePosition,
+ FromException = true
+ });
+ }
+
+ ///
+ public void LogMessage(string message)
+ {
+ this.MessageLog.Add(new TestLogEntry()
+ {
+ Message = message,
+ FromException = false
+ });
+ }
+
+ ///
+ public void LogMessage(string message, string fileName, int lineNumber, int linePosition)
+ {
+ this.MessageLog.Add(new TestLogEntry()
+ {
+ Message = message,
+ FileName = fileName,
+ LineNumber = lineNumber,
+ LinePosition = linePosition,
+ FromException = false
+ });
+ }
+
+ ///
+ public void LogWarning(string message)
+ {
+ this.WarningLog.Add(new TestLogEntry()
+ {
+ Message = message,
+ FromException = false
+ });
+ }
+
+ ///
+ public void LogWarning(string message, string fileName)
+ {
+ this.WarningLog.Add(new TestLogEntry()
+ {
+ Message = message,
+ FileName = fileName,
+ LineNumber = 0,
+ LinePosition = 0,
+ FromException = false
+ });
+ }
+
+ ///
+ public void LogWarning(string message, string fileName, int lineNumber, int linePosition)
+ {
+ this.WarningLog.Add(new TestLogEntry()
+ {
+ Message = message,
+ FileName = fileName,
+ LineNumber = lineNumber,
+ LinePosition = linePosition,
+ FromException = false
+ });
+ }
+
+ ///
+ /// An test entry for the logger.
+ /// Corresponds to an error, warning or message
+ ///
+ public struct TestLogEntry
+ {
+ ///
+ /// Gets or sets the log message
+ ///
+ public string Message { get; set; }
+
+ ///
+ /// Gets or sets the file that caused the entry
+ ///
+ public string FileName { get; set; }
+
+ ///
+ /// Gets or sets the line in the file
+ ///
+ public int LineNumber { get; set; }
+
+ ///
+ /// Gets or sets the position in the line
+ ///
+ public int LinePosition { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether whether the entry was caused from an exception
+ ///
+ public bool FromException { get; set; }
+ }
+ }
+}
diff --git a/src/Microsoft.VisualStudio.Jdt/JdtExtensions.cs b/src/Microsoft.VisualStudio.Jdt/JdtExtensions.cs
index a764b0f3..d95aa3e2 100644
--- a/src/Microsoft.VisualStudio.Jdt/JdtExtensions.cs
+++ b/src/Microsoft.VisualStudio.Jdt/JdtExtensions.cs
@@ -45,5 +45,23 @@ internal static bool IsCriticalException(this Exception ex)
#endif
;
}
+
+ ///
+ /// Clones a preserving the line information
+ ///
+ /// The object to clone
+ /// A clone of the object with its line info
+ internal static JObject CloneWithLineInfo(this JObject objectToClone)
+ {
+ var loadSettings = new JsonLoadSettings()
+ {
+ LineInfoHandling = LineInfoHandling.Load
+ };
+
+ using (var objectReader = objectToClone.CreateReader())
+ {
+ return JObject.Load(objectReader, loadSettings);
+ }
+ }
}
}
diff --git a/src/Microsoft.VisualStudio.Jdt/JsonTransformation.cs b/src/Microsoft.VisualStudio.Jdt/JsonTransformation.cs
index b2def301..a4938c43 100644
--- a/src/Microsoft.VisualStudio.Jdt/JsonTransformation.cs
+++ b/src/Microsoft.VisualStudio.Jdt/JsonTransformation.cs
@@ -162,10 +162,7 @@ private void SetTransform(Stream transformStream)
this.loadSettings = new JsonLoadSettings()
{
CommentHandling = CommentHandling.Ignore,
-
- // Obs: LineInfo is handled on Ignore and not Load
- // See https://github.com/JamesNK/Newtonsoft.Json/issues/1249
- LineInfoHandling = LineInfoHandling.Ignore
+ LineInfoHandling = LineInfoHandling.Load
};
using (StreamReader transformStreamReader = new StreamReader(transformStream))
diff --git a/src/Microsoft.VisualStudio.Jdt/Processors/JdtProcessor.cs b/src/Microsoft.VisualStudio.Jdt/Processors/JdtProcessor.cs
index ee51422a..68138f53 100644
--- a/src/Microsoft.VisualStudio.Jdt/Processors/JdtProcessor.cs
+++ b/src/Microsoft.VisualStudio.Jdt/Processors/JdtProcessor.cs
@@ -70,7 +70,7 @@ internal static void ProcessTransform(JObject source, JObject transform, JsonTra
}
// Passes in a clone of the transform object because it can be altered during the transformation process
- ProcessorChain.Start(source, (JObject)transform.DeepClone(), logger);
+ ProcessorChain.Start(source, (JObject)transform.CloneWithLineInfo(), logger);
}
///