diff --git a/src/ReleaseHistory.md b/src/ReleaseHistory.md
index c31b155c5..ffbaf72b1 100644
--- a/src/ReleaseHistory.md
+++ b/src/ReleaseHistory.md
@@ -9,6 +9,7 @@
* BUGFIX: Fix `ArgumentException` when `--recurse` is enabled and two file target specifiers generates the same file path. [#2438](https://github.com/microsoft/sarif-sdk/pull/2438)
* BUGFIX: Fix 'InvalidOperationException' with message `Collection was modified; enumeration operation may not execute` in `MultithreadedAnalyzeCommandBase`, which is raised when analyzing with the `--hashes` switch. [#2447](https://github.com/microsoft/sarif-sdk/pull/2447)
* BUGFIX: Fix `Merge` command produces empty SARIF file in Linux when providing file name only without path. [#2408](https://github.com/microsoft/sarif-sdk/pull/2408)
+* FEATURE: Add `--sort-results` argument to the `rewrite` command to get sorted SARIF results. [#2422](https://github.com/microsoft/sarif-sdk/pull/2422)
* BUGFIX: Fix `NullReferenceException` when filing work item with a SARIF file which has no filable results. [#2412](https://github.com/microsoft/sarif-sdk/pull/2412)
* BUGFIX: Fix missing `endLine` and `endColumn` properties and remove vulnerable packages for ESLint SARIF formatter. [#2458](https://github.com/microsoft/sarif-sdk/pull/2458)
diff --git a/src/Sarif.Multitool.Library/RewriteCommand.cs b/src/Sarif.Multitool.Library/RewriteCommand.cs
index f2675e34f..c98afc99c 100644
--- a/src/Sarif.Multitool.Library/RewriteCommand.cs
+++ b/src/Sarif.Multitool.Library/RewriteCommand.cs
@@ -59,6 +59,11 @@ public int Run(RewriteOptions options)
SarifLog reformattedLog = new RemoveOptionalDataVisitor(dataToRemove).VisitSarifLog(actualLog);
reformattedLog = new InsertOptionalDataVisitor(dataToInsert, originalUriBaseIds, insertProperties: options.InsertProperties).VisitSarifLog(reformattedLog);
+ if (options.SortResults)
+ {
+ reformattedLog = new SortingVisitor().VisitSarifLog(reformattedLog);
+ }
+
if (options.SarifOutputVersion == SarifVersion.OneZeroZero)
{
var visitor = new SarifCurrentToVersionOneVisitor();
diff --git a/src/Sarif.Multitool.Library/RewriteOptions.cs b/src/Sarif.Multitool.Library/RewriteOptions.cs
index a98e70b64..b51fab304 100644
--- a/src/Sarif.Multitool.Library/RewriteOptions.cs
+++ b/src/Sarif.Multitool.Library/RewriteOptions.cs
@@ -10,5 +10,11 @@ namespace Microsoft.CodeAnalysis.Sarif.Multitool
[Verb("rewrite", HelpText = "Enrich a SARIF file with additional data.")]
public class RewriteOptions : SingleFileOptionsBase
{
+ [Option(
+ 's',
+ "sort-results",
+ Default = false,
+ HelpText = "Sort results in the final output file.")]
+ public bool SortResults { get; set; }
}
}
diff --git a/src/Sarif/Comparers/ArtifactComparer.cs b/src/Sarif/Comparers/ArtifactComparer.cs
new file mode 100644
index 000000000..f651a4294
--- /dev/null
+++ b/src/Sarif/Comparers/ArtifactComparer.cs
@@ -0,0 +1,114 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+
+///
+/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive
+/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution.
+/// Tracking by issue: https://github.com/microsoft/jschema/issues/141
+///
+namespace Microsoft.CodeAnalysis.Sarif.Comparers
+{
+ internal class ArtifactComparer : IComparer
+ {
+ internal static readonly ArtifactComparer Instance = new ArtifactComparer();
+
+ public int Compare(Artifact left, Artifact right)
+ {
+ int compareResult = 0;
+
+ if (left.TryReferenceCompares(right, out compareResult))
+ {
+ return compareResult;
+ }
+
+ compareResult = MessageComparer.Instance.Compare(left.Description, right.Description);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = ArtifactLocationComparer.Instance.Compare(left.Location, right.Location);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.ParentIndex.CompareTo(right.ParentIndex);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Offset.CompareTo(right.Offset);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Length.CompareTo(right.Length);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Roles.CompareTo(right.Roles);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.MimeType, right.MimeType);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = ArtifactContentComparer.Instance.Compare(left.Contents, right.Contents);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.Encoding, right.Encoding);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.SourceLanguage, right.SourceLanguage);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Hashes.DictionaryCompares(right.Hashes);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.LastModifiedTimeUtc.CompareTo(right.LastModifiedTimeUtc);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ // Note: There may be other properties are not compared.
+ return compareResult;
+ }
+ }
+}
diff --git a/src/Sarif/Comparers/ArtifactContentComparer.cs b/src/Sarif/Comparers/ArtifactContentComparer.cs
new file mode 100644
index 000000000..9ef7f9ae3
--- /dev/null
+++ b/src/Sarif/Comparers/ArtifactContentComparer.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+
+///
+/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive
+/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution.
+/// Tracking by issue: https://github.com/microsoft/jschema/issues/141
+///
+namespace Microsoft.CodeAnalysis.Sarif.Comparers
+{
+ internal class ArtifactContentComparer : IComparer
+ {
+ internal static readonly ArtifactContentComparer Instance = new ArtifactContentComparer();
+
+ public int Compare(ArtifactContent left, ArtifactContent right)
+ {
+ int compareResult = 0;
+
+ if (left.TryReferenceCompares(right, out compareResult))
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.Text, right.Text);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.Binary, right.Binary);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = MultiformatMessageStringComparer.Instance.Compare(left.Rendered, right.Rendered);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ // Note: There may be other properties are not compared.
+ return compareResult;
+ }
+ }
+}
diff --git a/src/Sarif/Comparers/ArtifactLocationComparer.cs b/src/Sarif/Comparers/ArtifactLocationComparer.cs
new file mode 100644
index 000000000..2d9313387
--- /dev/null
+++ b/src/Sarif/Comparers/ArtifactLocationComparer.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+
+///
+/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive
+/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution.
+/// Tracking by issue: https://github.com/microsoft/jschema/issues/141
+///
+namespace Microsoft.CodeAnalysis.Sarif.Comparers
+{
+ internal class ArtifactLocationComparer : IComparer
+ {
+ internal static readonly ArtifactLocationComparer Instance = new ArtifactLocationComparer();
+
+ public int Compare(ArtifactLocation left, ArtifactLocation right)
+ {
+ int compareResult = 0;
+
+ if (left.TryReferenceCompares(right, out compareResult))
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Uri.UriCompares(right.Uri);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.UriBaseId, right.UriBaseId);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Index.CompareTo(right.Index);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = MessageComparer.Instance.Compare(left.Description, right.Description);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ // Note: There may be other properties are not compared.
+ return compareResult;
+ }
+ }
+}
diff --git a/src/Sarif/Comparers/CodeFlowComparer.cs b/src/Sarif/Comparers/CodeFlowComparer.cs
new file mode 100644
index 000000000..7596b73d7
--- /dev/null
+++ b/src/Sarif/Comparers/CodeFlowComparer.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+
+///
+/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive
+/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution.
+/// Tracking by issue: https://github.com/microsoft/jschema/issues/141
+///
+namespace Microsoft.CodeAnalysis.Sarif.Comparers
+{
+ internal class CodeFlowComparer : IComparer
+ {
+ internal static readonly CodeFlowComparer Instance = new CodeFlowComparer();
+
+ public int Compare(CodeFlow left, CodeFlow right)
+ {
+ int compareResult = 0;
+
+ if (left.TryReferenceCompares(right, out compareResult))
+ {
+ return compareResult;
+ }
+
+ compareResult = MessageComparer.Instance.Compare(left.Message, right.Message);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.ThreadFlows.ListCompares(right.ThreadFlows, ThreadFlowComparer.Instance);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ // Note: There may be other properties are not compared.
+ return compareResult;
+ }
+ }
+}
diff --git a/src/Sarif/Comparers/ComparerHelpers.cs b/src/Sarif/Comparers/ComparerHelpers.cs
new file mode 100644
index 000000000..e40d7e0a7
--- /dev/null
+++ b/src/Sarif/Comparers/ComparerHelpers.cs
@@ -0,0 +1,168 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.CodeAnalysis.Sarif.Comparers
+{
+ internal static class ComparerExtensions
+ {
+ /// Compare 2 object by references.
+ /// Return value 'true' presents you can get a definite compare result.
+ /// The out parameter 'result' presents compare result:
+ /// 0 if both objects are the same or both are null.
+ /// -1 if the first object is null and the second object is not null.
+ /// 1 if the first object is not null and the second object is null.
+ /// Return value 'false' presents the need to compare objects other properties to get the final result.
+ public static bool TryReferenceCompares(this object left, object right, out int result)
+ {
+ result = 0;
+
+ if (object.ReferenceEquals(left, right))
+ {
+ return true;
+ }
+
+ if (left == null)
+ {
+ result = -1;
+ return true;
+ }
+
+ if (right == null)
+ {
+ result = 1;
+ return true;
+ }
+
+ return false;
+ }
+
+ public static int ListCompares(this IList left, IList right) where T : IComparable
+ {
+ return CompareListHelper(left, right, (a, b) => a.CompareTo(b));
+ }
+
+ public static int ListCompares(this IList left, IList right, IComparer comparer)
+ {
+ if (comparer == null)
+ {
+ throw new ArgumentNullException(nameof(comparer));
+ }
+
+ return CompareListHelper(left, right, comparer.Compare);
+ }
+
+ private static int CompareListHelper(IList left, IList right, Func compareFunction)
+ {
+ if (compareFunction == null)
+ {
+ throw new ArgumentNullException(nameof(compareFunction));
+ }
+
+ int compareResult = 0;
+
+ if (left.TryReferenceCompares(right, out compareResult))
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Count.CompareTo(right.Count);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ for (int i = 0; i < left.Count; ++i)
+ {
+ if (left[i].TryReferenceCompares(right[i], out compareResult) && compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = compareFunction(left[i], right[i]);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+ }
+
+ return compareResult;
+ }
+
+ public static int DictionaryCompares(this IDictionary left, IDictionary right) where T : IComparable
+ {
+ return DictionaryCompareHelper(left, right, (a, b) => a.CompareTo(b));
+ }
+
+ public static int DictionaryCompares(this IDictionary left, IDictionary right, IComparer comparer)
+ {
+ if (comparer == null)
+ {
+ throw new ArgumentNullException(nameof(comparer));
+ }
+
+ return DictionaryCompareHelper(left, right, comparer.Compare);
+ }
+
+ private static int DictionaryCompareHelper(IDictionary left, IDictionary right, Func compareFunction)
+ {
+ if (compareFunction == null)
+ {
+ throw new ArgumentNullException(nameof(compareFunction));
+ }
+
+ int compareResult = 0;
+
+ if (left.TryReferenceCompares(right, out compareResult))
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Count.CompareTo(right.Count);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ IList leftKeys = left.Keys.ToList();
+ IList rightKeys = right.Keys.ToList();
+
+ for (int i = 0; i < leftKeys.Count; ++i)
+ {
+ compareResult = leftKeys[i].CompareTo(rightKeys[i]);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = compareFunction(left[leftKeys[i]], right[rightKeys[i]]);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+ }
+
+ return compareResult;
+ }
+
+ public static int UriCompares(this Uri left, Uri right)
+ {
+ int compareResult = 0;
+
+ if (left.TryReferenceCompares(right, out compareResult))
+ {
+ return compareResult;
+ }
+
+ return left.OriginalString.CompareTo(right.OriginalString);
+ }
+ }
+}
diff --git a/src/Sarif/Comparers/LocationComparer.cs b/src/Sarif/Comparers/LocationComparer.cs
new file mode 100644
index 000000000..8c89603ba
--- /dev/null
+++ b/src/Sarif/Comparers/LocationComparer.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+
+///
+/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive
+/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution.
+/// Tracking by issue: https://github.com/microsoft/jschema/issues/141
+///
+namespace Microsoft.CodeAnalysis.Sarif.Comparers
+{
+ internal class LocationComparer : IComparer
+ {
+ internal static readonly LocationComparer Instance = new LocationComparer();
+
+ public int Compare(Location left, Location right)
+ {
+ int compareResult = 0;
+
+ if (left.TryReferenceCompares(right, out compareResult))
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Id.CompareTo(right.Id);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = PhysicalLocationComparer.Instance.Compare(left.PhysicalLocation, right.PhysicalLocation);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.LogicalLocations.ListCompares(right.LogicalLocations, LogicalLocationComparer.Instance);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ // Note: There may be other properties are not compared.
+ return compareResult;
+ }
+ }
+}
diff --git a/src/Sarif/Comparers/LogicalLocationComparer.cs b/src/Sarif/Comparers/LogicalLocationComparer.cs
new file mode 100644
index 000000000..107d6d779
--- /dev/null
+++ b/src/Sarif/Comparers/LogicalLocationComparer.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+
+///
+/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive
+/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution.
+/// Tracking by issue: https://github.com/microsoft/jschema/issues/141
+///
+namespace Microsoft.CodeAnalysis.Sarif.Comparers
+{
+ internal class LogicalLocationComparer : IComparer
+ {
+ internal static readonly LogicalLocationComparer Instance = new LogicalLocationComparer();
+
+ public int Compare(LogicalLocation left, LogicalLocation right)
+ {
+ int compareResult = 0;
+
+ if (left.TryReferenceCompares(right, out compareResult))
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.Name, right.Name);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Index.CompareTo(right.Index);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.FullyQualifiedName, right.FullyQualifiedName);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.DecoratedName, right.DecoratedName);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.ParentIndex.CompareTo(right.ParentIndex);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.Kind, right.Kind);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ // Note: There may be other properties are not compared.
+ return compareResult;
+ }
+ }
+}
diff --git a/src/Sarif/Comparers/MessageComparer.cs b/src/Sarif/Comparers/MessageComparer.cs
new file mode 100644
index 000000000..9d8f75718
--- /dev/null
+++ b/src/Sarif/Comparers/MessageComparer.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+
+///
+/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive
+/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution.
+/// Tracking by issue: https://github.com/microsoft/jschema/issues/141
+///
+namespace Microsoft.CodeAnalysis.Sarif.Comparers
+{
+ internal class MessageComparer : IComparer
+ {
+ internal static readonly MessageComparer Instance = new MessageComparer();
+
+ public int Compare(Message left, Message right)
+ {
+ int compareResult = 0;
+
+ if (left.TryReferenceCompares(right, out compareResult))
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.Text, right.Text);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.Markdown, right.Markdown);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.Id, right.Id);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Arguments.ListCompares(right.Arguments);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ // Note: There may be other properties are not compared.
+ return compareResult;
+ }
+ }
+}
diff --git a/src/Sarif/Comparers/MultiformatMessageStringComparer.cs b/src/Sarif/Comparers/MultiformatMessageStringComparer.cs
new file mode 100644
index 000000000..d5055cf6e
--- /dev/null
+++ b/src/Sarif/Comparers/MultiformatMessageStringComparer.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+
+///
+/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive
+/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution.
+/// Tracking by issue: https://github.com/microsoft/jschema/issues/141
+///
+namespace Microsoft.CodeAnalysis.Sarif.Comparers
+{
+ internal class MultiformatMessageStringComparer : IComparer
+ {
+ internal static readonly MultiformatMessageStringComparer Instance = new MultiformatMessageStringComparer();
+
+ public int Compare(MultiformatMessageString left, MultiformatMessageString right)
+ {
+ int compareResult = 0;
+
+ if (left.TryReferenceCompares(right, out compareResult))
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.Text, right.Text);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.Markdown, right.Markdown);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ // Note: There may be other properties are not compared.
+ return compareResult;
+ }
+ }
+}
diff --git a/src/Sarif/Comparers/PhysicalLocationComparer.cs b/src/Sarif/Comparers/PhysicalLocationComparer.cs
new file mode 100644
index 000000000..90c1e0bb9
--- /dev/null
+++ b/src/Sarif/Comparers/PhysicalLocationComparer.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+
+///
+/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive
+/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution.
+/// Tracking by issue: https://github.com/microsoft/jschema/issues/141
+///
+namespace Microsoft.CodeAnalysis.Sarif.Comparers
+{
+ internal class PhysicalLocationComparer : IComparer
+ {
+ internal static readonly PhysicalLocationComparer Instance = new PhysicalLocationComparer();
+
+ public int Compare(PhysicalLocation left, PhysicalLocation right)
+ {
+ int compareResult = 0;
+
+ if (left.TryReferenceCompares(right, out compareResult))
+ {
+ return compareResult;
+ }
+
+ compareResult = ArtifactLocationComparer.Instance.Compare(left.ArtifactLocation, right.ArtifactLocation);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = RegionComparer.Instance.Compare(left.Region, right.Region);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = RegionComparer.Instance.Compare(left.ContextRegion, right.ContextRegion);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ // Note: There may be other properties are not compared.
+ return compareResult;
+ }
+ }
+}
diff --git a/src/Sarif/Comparers/RegionComparer.cs b/src/Sarif/Comparers/RegionComparer.cs
new file mode 100644
index 000000000..9f03675c2
--- /dev/null
+++ b/src/Sarif/Comparers/RegionComparer.cs
@@ -0,0 +1,86 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+
+///
+/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive
+/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution.
+/// Tracking by issue: https://github.com/microsoft/jschema/issues/141
+///
+namespace Microsoft.CodeAnalysis.Sarif.Comparers
+{
+ internal class RegionComparer : IComparer
+ {
+ internal static readonly RegionComparer Instance = new RegionComparer();
+
+ public int Compare(Region left, Region right)
+ {
+ int compareResult = 0;
+
+ if (left.TryReferenceCompares(right, out compareResult))
+ {
+ return compareResult;
+ }
+
+ compareResult = left.StartLine.CompareTo(right.StartLine);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.StartColumn.CompareTo(right.StartColumn);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.EndLine.CompareTo(right.EndLine);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.EndColumn.CompareTo(right.EndColumn);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.CharOffset.CompareTo(right.CharOffset);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.CharLength.CompareTo(right.CharLength);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.ByteOffset.CompareTo(right.ByteOffset);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.ByteLength.CompareTo(right.ByteLength);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ // Note: There may be other properties are not compared.
+ return compareResult;
+ }
+ }
+}
diff --git a/src/Sarif/Comparers/ReportingConfigurationComparer.cs b/src/Sarif/Comparers/ReportingConfigurationComparer.cs
new file mode 100644
index 000000000..aa4d6973d
--- /dev/null
+++ b/src/Sarif/Comparers/ReportingConfigurationComparer.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+
+///
+/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive
+/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution.
+/// Tracking by issue: https://github.com/microsoft/jschema/issues/141
+///
+namespace Microsoft.CodeAnalysis.Sarif.Comparers
+{
+ internal class ReportingConfigurationComparer : IComparer
+ {
+ internal static readonly ReportingConfigurationComparer Instance = new ReportingConfigurationComparer();
+
+ public int Compare(ReportingConfiguration left, ReportingConfiguration right)
+ {
+ int compareResult = 0;
+
+ if (left.TryReferenceCompares(right, out compareResult))
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Enabled.CompareTo(right.Enabled);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Level.CompareTo(right.Level);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Rank.CompareTo(right.Rank);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ // Note: There may be other properties are not compared.
+ return compareResult;
+ }
+ }
+}
diff --git a/src/Sarif/Comparers/ReportingDescriptorComparer.cs b/src/Sarif/Comparers/ReportingDescriptorComparer.cs
new file mode 100644
index 000000000..af0c7d0f0
--- /dev/null
+++ b/src/Sarif/Comparers/ReportingDescriptorComparer.cs
@@ -0,0 +1,107 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+
+///
+/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive
+/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution.
+/// Tracking by issue: https://github.com/microsoft/jschema/issues/141
+///
+namespace Microsoft.CodeAnalysis.Sarif.Comparers
+{
+ internal class ReportingDescriptorComparer : IComparer
+ {
+ internal static readonly ReportingDescriptorComparer Instance = new ReportingDescriptorComparer();
+
+ public int Compare(ReportingDescriptor left, ReportingDescriptor right)
+ {
+ int compareResult = 0;
+
+ if (left.TryReferenceCompares(right, out compareResult))
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.Id, right.Id);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.DeprecatedIds.ListCompares(right.DeprecatedIds);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.Guid, right.Guid);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.DeprecatedGuids.ListCompares(right.DeprecatedGuids);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.Name, right.Name);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.DeprecatedNames.ListCompares(right.DeprecatedNames);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = MultiformatMessageStringComparer.Instance.Compare(left.ShortDescription, right.ShortDescription);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = MultiformatMessageStringComparer.Instance.Compare(left.FullDescription, right.FullDescription);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = ReportingConfigurationComparer.Instance.Compare(left.DefaultConfiguration, right.DefaultConfiguration);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.HelpUri.UriCompares(right.HelpUri);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = MultiformatMessageStringComparer.Instance.Compare(left.Help, right.Help);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ // Note: There may be other properties are not compared.
+ return compareResult;
+ }
+ }
+}
diff --git a/src/Sarif/Comparers/ResultComparer.cs b/src/Sarif/Comparers/ResultComparer.cs
new file mode 100644
index 000000000..f010dbcda
--- /dev/null
+++ b/src/Sarif/Comparers/ResultComparer.cs
@@ -0,0 +1,121 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+
+///
+/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive
+/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution.
+/// Tracking by issue: https://github.com/microsoft/jschema/issues/141
+///
+namespace Microsoft.CodeAnalysis.Sarif.Comparers
+{
+ internal class ResultComparer : IComparer
+ {
+ internal static readonly ResultComparer Instance = new ResultComparer();
+
+ public int Compare(Result left, Result right)
+ {
+ int compareResult = 0;
+
+ if (left.TryReferenceCompares(right, out compareResult))
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.RuleId, right.RuleId);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.RuleIndex.CompareTo(right.RuleIndex);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Level.CompareTo(right.Level);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Kind.CompareTo(right.Kind);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = MessageComparer.Instance.Compare(left.Message, right.Message);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = ArtifactLocationComparer.Instance.Compare(left.AnalysisTarget, right.AnalysisTarget);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Locations.ListCompares(right.Locations, LocationComparer.Instance);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.Guid, right.Guid);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.CorrelationGuid, right.CorrelationGuid);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.OccurrenceCount.CompareTo(right.OccurrenceCount);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.CodeFlows.ListCompares(right.CodeFlows, CodeFlowComparer.Instance);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.BaselineState.CompareTo(right.BaselineState);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Rank.CompareTo(right.Rank);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ // Note: There may be other properties are not compared.
+ return compareResult;
+ }
+ }
+}
diff --git a/src/Sarif/Comparers/RunComparer.cs b/src/Sarif/Comparers/RunComparer.cs
new file mode 100644
index 000000000..4110a949e
--- /dev/null
+++ b/src/Sarif/Comparers/RunComparer.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+
+///
+/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive
+/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution.
+/// Tracking by issue: https://github.com/microsoft/jschema/issues/141
+///
+namespace Microsoft.CodeAnalysis.Sarif.Comparers
+{
+ internal class RunComparer : IComparer
+ {
+ internal static readonly RunComparer Instance = new RunComparer();
+
+ public int Compare(Run left, Run right)
+ {
+ int compareResult = 0;
+
+ if (left.TryReferenceCompares(right, out compareResult))
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Artifacts.ListCompares(right.Artifacts, ArtifactComparer.Instance);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = ToolComponentComparer.Instance.Compare(left?.Tool?.Driver, right?.Tool?.Driver);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Results.ListCompares(right.Results, ResultComparer.Instance);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ // Note: There may be other properties are not compared.
+ return compareResult;
+ }
+ }
+}
diff --git a/src/Sarif/Comparers/ThreadFlowComparer.cs b/src/Sarif/Comparers/ThreadFlowComparer.cs
new file mode 100644
index 000000000..670c891f8
--- /dev/null
+++ b/src/Sarif/Comparers/ThreadFlowComparer.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+
+///
+/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive
+/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution.
+/// Tracking by issue: https://github.com/microsoft/jschema/issues/141
+///
+namespace Microsoft.CodeAnalysis.Sarif.Comparers
+{
+ internal class ThreadFlowComparer : IComparer
+ {
+ internal static readonly ThreadFlowComparer Instance = new ThreadFlowComparer();
+
+ public int Compare(ThreadFlow left, ThreadFlow right)
+ {
+ int compareResult = 0;
+
+ if (left.TryReferenceCompares(right, out compareResult))
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.Id, right.Id);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = MessageComparer.Instance.Compare(left.Message, right.Message);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Locations.ListCompares(right.Locations, ThreadFlowLocationComparer.Instance);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ // Note: There may be other properties are not compared.
+ return compareResult;
+ }
+ }
+}
diff --git a/src/Sarif/Comparers/ThreadFlowLocationComparer.cs b/src/Sarif/Comparers/ThreadFlowLocationComparer.cs
new file mode 100644
index 000000000..c0d61f331
--- /dev/null
+++ b/src/Sarif/Comparers/ThreadFlowLocationComparer.cs
@@ -0,0 +1,79 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+
+///
+/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive
+/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution.
+/// Tracking by issue: https://github.com/microsoft/jschema/issues/141
+///
+namespace Microsoft.CodeAnalysis.Sarif.Comparers
+{
+ internal class ThreadFlowLocationComparer : IComparer
+ {
+ internal static readonly ThreadFlowLocationComparer Instance = new ThreadFlowLocationComparer();
+
+ public int Compare(ThreadFlowLocation left, ThreadFlowLocation right)
+ {
+ int compareResult = 0;
+
+ if (left.TryReferenceCompares(right, out compareResult))
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Index.CompareTo(right.Index);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = LocationComparer.Instance.Compare(left.Location, right.Location);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Kinds.ListCompares(right.Kinds);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.NestingLevel.CompareTo(right.NestingLevel);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.ExecutionOrder.CompareTo(right.ExecutionOrder);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.ExecutionTimeUtc.CompareTo(right.ExecutionTimeUtc);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Importance.CompareTo(right.Importance);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ // Note: There may be other properties are not compared.
+ return compareResult;
+ }
+ }
+}
diff --git a/src/Sarif/Comparers/ToolComponentComparer.cs b/src/Sarif/Comparers/ToolComponentComparer.cs
new file mode 100644
index 000000000..7f3cfbc2d
--- /dev/null
+++ b/src/Sarif/Comparers/ToolComponentComparer.cs
@@ -0,0 +1,108 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+
+///
+/// Note: This comparer may not have all properties compared. Will be replaced by a comprehensive
+/// comparer generated by JSchema as part of EqualityComparer in a planned comprehensive solution.
+/// Tracking by issue: https://github.com/microsoft/jschema/issues/141
+///
+
+namespace Microsoft.CodeAnalysis.Sarif.Comparers
+{
+ internal class ToolComponentComparer : IComparer
+ {
+ internal static readonly ToolComponentComparer Instance = new ToolComponentComparer();
+
+ public int Compare(ToolComponent left, ToolComponent right)
+ {
+ int compareResult = 0;
+
+ if (left.TryReferenceCompares(right, out compareResult))
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.Guid, right.Guid);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.Name, right.Name);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.Organization, right.Organization);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.Product, right.Product);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.FullName, right.FullName);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.Version, right.Version);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.SemanticVersion, right.SemanticVersion);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = string.Compare(left.ReleaseDateUtc, right.ReleaseDateUtc);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.DownloadUri.UriCompares(right.DownloadUri);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.InformationUri.UriCompares(right.InformationUri);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ compareResult = left.Rules.ListCompares(right.Rules, ReportingDescriptorComparer.Instance);
+
+ if (compareResult != 0)
+ {
+ return compareResult;
+ }
+
+ // Note: There may be other properties are not compared.
+ return compareResult;
+ }
+ }
+}
diff --git a/src/Sarif/Visitors/SortingVisitor.cs b/src/Sarif/Visitors/SortingVisitor.cs
new file mode 100644
index 000000000..544cd6ddf
--- /dev/null
+++ b/src/Sarif/Visitors/SortingVisitor.cs
@@ -0,0 +1,179 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+using System.Linq;
+
+using Microsoft.CodeAnalysis.Sarif.Comparers;
+
+namespace Microsoft.CodeAnalysis.Sarif.Visitors
+{
+ public class SortingVisitor : SarifRewritingVisitor
+ {
+ private readonly IDictionary ruleIndexMap;
+ private readonly IDictionary artifactIndexMap;
+
+ public SortingVisitor()
+ {
+ this.ruleIndexMap = new Dictionary();
+ this.artifactIndexMap = new Dictionary();
+ }
+
+ public override SarifLog VisitSarifLog(SarifLog node)
+ {
+ SarifLog current = base.VisitSarifLog(node);
+
+ if (current?.Runs != null)
+ {
+ current.Runs = current.Runs.OrderBy(r => r, RunComparer.Instance).ToList();
+ }
+
+ return current;
+ }
+
+ public override Run VisitRun(Run node)
+ {
+ this.ruleIndexMap.Clear();
+ this.artifactIndexMap.Clear();
+
+ if (node?.Artifacts != null)
+ {
+ node.Artifacts = this.SortAndBuildIndexMap(node?.Artifacts, ArtifactComparer.Instance, this.artifactIndexMap);
+ }
+
+ Run current = base.VisitRun(node);
+
+ if (current?.Results != null)
+ {
+ current.Results = current.Results.OrderBy(r => r, ResultComparer.Instance).ToList();
+ }
+
+ return current;
+ }
+
+ public override ToolComponent VisitToolComponent(ToolComponent node)
+ {
+ if (node?.Rules != null)
+ {
+ node.Rules = this.SortAndBuildIndexMap(node?.Rules, ReportingDescriptorComparer.Instance, this.ruleIndexMap);
+ }
+
+ return base.VisitToolComponent(node);
+ }
+
+ public override Result VisitResult(Result node)
+ {
+ Result current = base.VisitResult(node);
+
+ if (current != null)
+ {
+ if (current.RuleIndex != -1 && this.ruleIndexMap.TryGetValue(current.RuleIndex, out int newIndex))
+ {
+ current.RuleIndex = newIndex;
+ }
+
+ if (current.Locations != null)
+ {
+ current.Locations = current.Locations.OrderBy(r => r, LocationComparer.Instance).ToList();
+ }
+
+ if (current.CodeFlows != null)
+ {
+ current.CodeFlows = current.CodeFlows.OrderBy(r => r, CodeFlowComparer.Instance).ToList();
+ }
+ }
+
+ return current;
+ }
+
+ public override CodeFlow VisitCodeFlow(CodeFlow node)
+ {
+ CodeFlow current = base.VisitCodeFlow(node);
+
+ if (current?.ThreadFlows != null)
+ {
+ current.ThreadFlows = current.ThreadFlows.OrderBy(t => t, ThreadFlowComparer.Instance).ToList();
+ }
+
+ return current;
+ }
+
+ public override ThreadFlow VisitThreadFlow(ThreadFlow node)
+ {
+ ThreadFlow current = base.VisitThreadFlow(node);
+
+ if (current?.Locations != null)
+ {
+ current.Locations = current.Locations.OrderBy(t => t, ThreadFlowLocationComparer.Instance).ToList();
+ }
+
+ return current;
+ }
+
+ public override Location VisitLocation(Location node)
+ {
+ Location current = base.VisitLocation(node);
+
+ if (current?.LogicalLocations != null)
+ {
+ current.LogicalLocations = current.LogicalLocations.OrderBy(t => t, LogicalLocationComparer.Instance).ToList();
+ }
+
+ return current;
+ }
+
+ public override ArtifactLocation VisitArtifactLocation(ArtifactLocation node)
+ {
+ ArtifactLocation current = base.VisitArtifactLocation(node);
+
+ if (current.Index != -1 && this.artifactIndexMap.TryGetValue(current.Index, out int newIndex))
+ {
+ current.Index = newIndex;
+ }
+
+ return current;
+ }
+
+ private IList SortAndBuildIndexMap(IList list, IComparer comparer, IDictionary indexMapping)
+ {
+ if (list != null)
+ {
+ IDictionary unsortedIndices = this.CacheListIndices(list);
+
+ list = list.OrderBy(r => r, comparer).ToList();
+
+ this.MapNewIndices(list, unsortedIndices, indexMapping);
+
+ unsortedIndices.Clear();
+ }
+
+ return list;
+ }
+
+ private IDictionary CacheListIndices(IList list)
+ {
+ // Assume each item in the list is unique (has different reference).
+ // According to sarif-2.1.0-rtm.5.json, artifacts array of runs and rules array of toolComponent
+ // are defined as "uniqueItems".
+ var dict = new Dictionary(capacity: list.Count);
+
+ for (int i = 0; i < list.Count; i++)
+ {
+ dict[list[i]] = i;
+ }
+
+ return dict;
+ }
+
+ private void MapNewIndices(IList newList, IDictionary oldIndices, IDictionary indexMapping)
+ {
+ for (int newIndex = 0; newIndex < newList.Count; newIndex++)
+ {
+ if (oldIndices.TryGetValue(newList[newIndex], out int oldIndex))
+ {
+ indexMapping[oldIndex] = newIndex;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Test.UnitTests.Sarif.Multitool/RewriteCommandTests.cs b/src/Test.UnitTests.Sarif.Multitool/RewriteCommandTests.cs
new file mode 100644
index 000000000..3638c34a3
--- /dev/null
+++ b/src/Test.UnitTests.Sarif.Multitool/RewriteCommandTests.cs
@@ -0,0 +1,148 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Generic;
+using System.Linq;
+
+using CommandLine;
+
+using FluentAssertions;
+
+using Xunit;
+
+namespace Microsoft.CodeAnalysis.Sarif.Multitool
+{
+ public class RewriteCommandTests
+ {
+ [Fact]
+ public void FileWorkItemsCommand_ArgumentsTests()
+ {
+ foreach (dynamic testCase in s_testCases)
+ {
+ this.VerifyCommandLineOptions(testCase.Args, testCase.Valid, testCase.ExpectedOptions);
+ }
+ }
+
+ private void VerifyCommandLineOptions(IEnumerable args, bool valid, RewriteOptions expected)
+ {
+ Parser parser = Parser.Default;
+ ParserResult result = parser.ParseArguments(args);
+
+ if (valid)
+ {
+ result.Should().BeOfType>();
+ result.Should().NotBeNull();
+ RewriteOptions parsedResult = ((Parsed)result).Value;
+ parsedResult.Should().NotBeNull();
+ parsedResult.Should().BeOfType();
+ parsedResult.Should().BeEquivalentTo(expected);
+ }
+ else
+ {
+ result.Should().BeOfType>();
+ result.Should().NotBeNull();
+ IEnumerable errors = ((NotParsed)result).Errors;
+ errors.Should().NotBeEmpty();
+ }
+ }
+
+ private static RewriteOptions SetEmptyValue(RewriteOptions option)
+ {
+ option.DataToInsert ??= new List();
+ option.DataToRemove ??= new List();
+ option.UriBaseIds ??= new List();
+ option.InsertProperties ??= new List();
+ return option;
+ }
+
+ private static readonly dynamic[] s_testCases =
+ new[] {
+ new {
+ Title = "rewrite pass case",
+ Args = new string[] {
+ "test.sarif",
+ "--output",
+ "updated.sarif",
+ "--remove",
+ "VersionControlDetails;NondeterministicProperties",
+ "--sort-results",
+ "--force"
+ },
+ Valid = true,
+ ExpectedOptions = SetEmptyValue(new RewriteOptions
+ {
+ InputFilePath = "test.sarif",
+ OutputFilePath = "updated.sarif",
+ DataToRemove = new List
+ {
+ OptionallyEmittedData.VersionControlDetails,
+ OptionallyEmittedData.NondeterministicProperties
+ },
+ SortResults = true,
+ Force = true,
+ }),
+ },
+ new {
+ Title = "pass case: different ordering",
+ Args = new string[] {
+ "--output",
+ "updated.sarif",
+ "test.sarif",
+ "--force",
+ "--sort-results",
+ },
+ Valid = true,
+ ExpectedOptions = SetEmptyValue(new RewriteOptions
+ {
+ InputFilePath = "test.sarif",
+ OutputFilePath = "updated.sarif",
+ SortResults = true,
+ Force = true,
+ }),
+ },
+ new {
+ Title = "pass case: use short argument name",
+ Args = new string[] {
+ "-o",
+ "updated.sarif",
+ "test.sarif",
+ "-s",
+ "-f"
+ },
+ Valid = true,
+ ExpectedOptions = SetEmptyValue(new RewriteOptions
+ {
+ InputFilePath = "test.sarif",
+ OutputFilePath = "updated.sarif",
+ SortResults = true,
+ Force = true,
+ }),
+ },
+ new {
+ Title = "fail case: argument value not provided",
+ Args = new string[] {
+ "test.sarif",
+ "--output",
+ "updated.sarif",
+ "--remove",
+ "--sort-results",
+ "--force"
+ },
+ Valid = false,
+ ExpectedOptions = (RewriteOptions)null,
+ },
+ new {
+ Title = "fail case: wrong arg name --sort-result(s)",
+ Args = new string[] {
+ "test.sarif",
+ "--output",
+ "updated.sarif",
+ "--sort-result",
+ "--force"
+ },
+ Valid = false,
+ ExpectedOptions = (RewriteOptions)null,
+ },
+ };
+ }
+}
diff --git a/src/Test.UnitTests.Sarif/Comparers/ComparersTests.cs b/src/Test.UnitTests.Sarif/Comparers/ComparersTests.cs
new file mode 100644
index 000000000..226faa5b3
--- /dev/null
+++ b/src/Test.UnitTests.Sarif/Comparers/ComparersTests.cs
@@ -0,0 +1,807 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+using FluentAssertions;
+
+using Microsoft.CodeAnalysis.Sarif;
+using Microsoft.CodeAnalysis.Sarif.Comparers;
+using Microsoft.CodeAnalysis.Test.Utilities.Sarif;
+
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.CodeAnalysis.Test.UnitTests.Sarif.Comparers
+{
+ public class ComparersTests
+ {
+ private readonly ITestOutputHelper output;
+
+ public ComparersTests(ITestOutputHelper outputHelper)
+ {
+ this.output = outputHelper;
+ }
+
+ [Fact]
+ public void CompareList_Shuffle_Tests()
+ {
+ Random random = RandomSarifLogGenerator.GenerateRandomAndLog(this.output);
+
+ IList originalList = Enumerable.Range(-100, 200).ToList();
+
+ IList shuffledList = originalList.ToList().Shuffle(random);
+
+ int result = 0, newResult = 0;
+
+ result = originalList.ListCompares(shuffledList);
+ result.Should().NotBe(0);
+
+ newResult = shuffledList.ListCompares(originalList);
+ newResult.Should().Be(result * -1);
+
+ IList sortedList = shuffledList.OrderBy(i => i).ToList();
+
+ result = originalList.ListCompares(sortedList);
+ result.Should().Be(0);
+
+ newResult = originalList.ListCompares(sortedList);
+ newResult.Should().Be(0);
+ }
+
+ [Fact]
+ public void CompareList_BothNull_Tests()
+ {
+ IList list1 = null;
+ IList list2 = null;
+
+ list1.ListCompares(list2).Should().Be(0);
+ list2.ListCompares(list1).Should().Be(0);
+ }
+
+ [Fact]
+ public void CompareList_CompareNullToNotNull_Tests()
+ {
+ IList list1 = null;
+ IList list2 = Enumerable.Range(-10, 20).ToList();
+
+ list1.ListCompares(list2).Should().Be(-1);
+ list2.ListCompares(list1).Should().Be(1);
+ }
+
+ [Fact]
+ public void CompareList_DifferentCount_Tests()
+ {
+ IList list1 = Enumerable.Range(0, 11).ToList();
+ IList list2 = Enumerable.Range(0, 10).ToList();
+
+ list1.ListCompares(list2).Should().Be(1);
+ list2.ListCompares(list1).Should().Be(-1);
+ }
+
+ [Fact]
+ public void CompareList_SameCountDifferentElement_Tests()
+ {
+ IList list1 = Enumerable.Range(0, 10).ToList();
+ IList list2 = Enumerable.Range(1, 10).ToList();
+
+ list1.ListCompares(list2).Should().Be(-1);
+ list2.ListCompares(list1).Should().Be(1);
+ }
+
+ [Fact]
+ public void CompareList_WithNullComparer_Tests()
+ {
+ var tool = new ToolComponent { Guid = Guid.Empty.ToString() };
+
+ IList runs1 = new[] { new Run { Tool = new Tool { Driver = tool } } };
+ IList runs2 = Array.Empty();
+
+ Action act = () => runs1.ListCompares(runs2, comparer: null);
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void CompareList_WithComparer_Tests()
+ {
+ IList run1 = new List
+ {
+ null,
+ new Run { Tool = new Tool { Driver = new ToolComponent { Guid = Guid.NewGuid().ToString() } } }
+ };
+
+ IList run2 = new List
+ {
+ new Run { Tool = new Tool { Driver = new ToolComponent { Guid = Guid.NewGuid().ToString() } } },
+ null
+ };
+
+ int result = run1.ListCompares(run2, RunComparer.Instance);
+ result.Should().Be(-1);
+
+ result = run2.ListCompares(run1, RunComparer.Instance);
+ result.Should().Be(1);
+ }
+
+ [Fact]
+ public void CompareDictionary_Shuffle_Tests()
+ {
+ var dict1 = new Dictionary
+ {
+ { "b", "b" },
+ { "c", "c" },
+ { "d", "d" }
+ };
+
+ var dict2 = new Dictionary
+ {
+ { "d", "d" },
+ { "c", "c" },
+ { "b", "b" }
+ };
+
+ int result = dict1.DictionaryCompares(dict2);
+ result.Should().Be(-1);
+
+ result = dict2.DictionaryCompares(dict1);
+ result.Should().Be(1);
+
+ dict2 = new Dictionary
+ {
+ { "a", "a" },
+ { "b", "b" },
+ { "c", "c" }
+ };
+
+ result = dict1.DictionaryCompares(dict2);
+ result.Should().Be(1);
+
+ result = dict2.DictionaryCompares(dict1);
+ result.Should().Be(-1);
+
+ dict2 = new Dictionary
+ {
+ { "b", "a" },
+ { "c", "c" },
+ { "d", "d" }
+ };
+
+ result = dict1.DictionaryCompares(dict2);
+ result.Should().Be(1);
+
+ result = dict2.DictionaryCompares(dict1);
+ result.Should().Be(-1);
+ }
+
+ [Fact]
+ public void CompareDictionary_BothNull_Tests()
+ {
+ IDictionary dict1 = null;
+ IDictionary dict2 = null;
+
+ dict1.DictionaryCompares(dict2).Should().Be(0);
+ dict2.DictionaryCompares(dict1).Should().Be(0);
+ }
+
+ [Fact]
+ public void CompareDictionary_CompareNullToNotNull_Tests()
+ {
+ IDictionary dict1 = null;
+ IDictionary dict2 = new Dictionary() { { "a", "a" } };
+
+ dict1.DictionaryCompares(dict2).Should().Be(-1);
+ dict2.DictionaryCompares(dict1).Should().Be(1);
+ }
+
+ [Fact]
+ public void CompareDictionary_DifferentCount_Tests()
+ {
+ var dict1 = new Dictionary() { { "a", "a" }, { "b", "b" } };
+ var dict2 = new Dictionary() { { "c", "c" } };
+
+ dict1.DictionaryCompares(dict2).Should().Be(1);
+ dict2.DictionaryCompares(dict1).Should().Be(-1);
+ }
+
+ [Fact]
+ public void CompareDictionary_SameCountDifferentElement_Tests()
+ {
+ var dict1 = new Dictionary() { { "a", "a" }, { "b", "b" } };
+ var dict2 = new Dictionary() { { "c", "c" }, { "d", "d" } };
+
+ dict1.DictionaryCompares(dict2).Should().Be(-1);
+ dict2.DictionaryCompares(dict1).Should().Be(1);
+
+ dict1 = new Dictionary() { { "a", "a" }, { "b", "b" }, { "c", "c" } };
+ dict2 = new Dictionary() { { "c", "c" }, { "b", "b" }, { "a", "a" } };
+
+ dict1.DictionaryCompares(dict2).Should().Be(-1);
+ dict2.DictionaryCompares(dict1).Should().Be(1);
+ }
+
+ [Fact]
+ public void CompareDictionary_WithNullComparer_Tests()
+ {
+ var loc1 = new Dictionary();
+ var loc2 = new Dictionary();
+
+ Action act = () => loc1.DictionaryCompares(loc2, comparer: null);
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void CompareDictionary_WithComparer_Tests()
+ {
+ var loc1 = new Dictionary
+ {
+ { "1", null },
+ { "2", new Location { Id = 2 } }
+ };
+
+ var loc2 = new Dictionary
+ {
+ { "1", new Location { Id = 1 } },
+ { "2", new Location { Id = 2 } }
+ };
+
+ int result = loc1.DictionaryCompares(loc2, LocationComparer.Instance);
+ result.Should().Be(-1);
+
+ result = loc2.DictionaryCompares(loc1, LocationComparer.Instance);
+ result.Should().Be(1);
+ }
+
+ [Fact]
+ public void ArtifactContentComparer_Tests()
+ {
+ var list1 = new List();
+ var list2 = new List();
+
+ list1.Add(null);
+ list2.Add(null);
+ list1.ListCompares(list2, ArtifactContentComparer.Instance).Should().Be(0);
+
+ list1.Add(new ArtifactContent() { Text = "content 1" });
+ list2.Add(new ArtifactContent() { Text = "content 2" });
+
+ list1.ListCompares(list2, ArtifactContentComparer.Instance).Should().Be(-1);
+ list2.ListCompares(list1, ArtifactContentComparer.Instance).Should().Be(1);
+ list1.Clear();
+ list2.Clear();
+
+ list1.Add(new ArtifactContent() { Binary = "WUJDMTIz" });
+ list2.Add(new ArtifactContent() { Binary = "QUJDMTIz" });
+
+ list1.ListCompares(list2, ArtifactContentComparer.Instance).Should().Be(1);
+ list2.ListCompares(list1, ArtifactContentComparer.Instance).Should().Be(-1);
+ list1.Clear();
+ list2.Clear();
+
+ list1.Add(new ArtifactContent() { Text = "content 1", Rendered = new MultiformatMessageString { Markdown = "`markdown`" } });
+ list2.Add(new ArtifactContent() { Text = "content 1", Rendered = new MultiformatMessageString { Markdown = "title" } });
+ list1.ListCompares(list2, ArtifactContentComparer.Instance).Should().Be(-1);
+ list2.ListCompares(list1, ArtifactContentComparer.Instance).Should().Be(1);
+ }
+
+ [Fact]
+ public void ReportingConfigurationComparer_Tests()
+ {
+ var rules1 = new List();
+ var rules2 = new List();
+
+ rules1.Add(null);
+ rules2.Add(null);
+
+ rules1.ListCompares(rules2, ReportingConfigurationComparer.Instance).Should().Be(0);
+
+ rules1.Add(new ReportingConfiguration() { Rank = 26.648d });
+ rules2.Add(new ReportingConfiguration() { Rank = 87.1d });
+
+ rules1.ListCompares(rules2, ReportingConfigurationComparer.Instance).Should().Be(-1);
+ rules2.ListCompares(rules1, ReportingConfigurationComparer.Instance).Should().Be(1);
+
+ rules1.Insert(0, new ReportingConfiguration() { Level = FailureLevel.Error });
+ rules2.Insert(0, new ReportingConfiguration() { Level = FailureLevel.Warning });
+
+ rules1.ListCompares(rules2, ReportingConfigurationComparer.Instance).Should().Be(1);
+ rules2.ListCompares(rules1, ReportingConfigurationComparer.Instance).Should().Be(-1);
+
+ rules1.Insert(0, new ReportingConfiguration() { Enabled = false, Rank = 80d });
+ rules2.Insert(0, new ReportingConfiguration() { Enabled = true, Rank = 80d });
+
+ rules1.ListCompares(rules2, ReportingConfigurationComparer.Instance).Should().Be(-1);
+ rules2.ListCompares(rules1, ReportingConfigurationComparer.Instance).Should().Be(1);
+ }
+
+ [Fact]
+ public void ToolComponentComparer_Tests()
+ {
+ var list1 = new List();
+ var list2 = new List();
+
+ list1.Add(null);
+ list2.Add(null);
+
+ list1.ListCompares(list2, ToolComponentComparer.Instance).Should().Be(0);
+
+ list1.Insert(0, new ToolComponent() { Guid = Guid.Empty.ToString() });
+ list2.Insert(0, new ToolComponent() { Guid = Guid.NewGuid().ToString() });
+
+ list1.ListCompares(list2, ToolComponentComparer.Instance).Should().Be(-1);
+ list2.ListCompares(list1, ToolComponentComparer.Instance).Should().Be(1);
+
+ list1.Insert(0, new ToolComponent() { Name = "scan tool" });
+ list2.Insert(0, new ToolComponent() { Name = "code scan tool" });
+
+ list1.ListCompares(list2, ToolComponentComparer.Instance).Should().Be(1);
+ list2.ListCompares(list1, ToolComponentComparer.Instance).Should().Be(-1);
+
+ list1.Insert(0, new ToolComponent() { Organization = "MS", Name = "scan tool" });
+ list2.Insert(0, new ToolComponent() { Organization = "Microsoft", Name = "scan tool" });
+
+ list1.ListCompares(list2, ToolComponentComparer.Instance).Should().Be(1);
+ list2.ListCompares(list1, ToolComponentComparer.Instance).Should().Be(-1);
+
+ list1.Insert(0, new ToolComponent() { Product = "PREfast", Name = "scan tool" });
+ list2.Insert(0, new ToolComponent() { Product = "prefast", Name = "scan tool" });
+
+ list1.ListCompares(list2, ToolComponentComparer.Instance).Should().Be(1);
+ list2.ListCompares(list1, ToolComponentComparer.Instance).Should().Be(-1);
+
+ list1.Insert(0, new ToolComponent() { FullName = "Analysis Linter", Name = "scan tool" });
+ list2.Insert(0, new ToolComponent() { FullName = "Analysis Linter Tool", Name = "scan tool" });
+
+ list1.ListCompares(list2, ToolComponentComparer.Instance).Should().Be(-1);
+ list2.ListCompares(list1, ToolComponentComparer.Instance).Should().Be(1);
+
+ list1.Insert(0, new ToolComponent() { Version = "CWR-2022-01", Name = "scan tool" });
+ list2.Insert(0, new ToolComponent() { Version = "CWR-2021-12", Name = "scan tool" });
+
+ list1.ListCompares(list2, ToolComponentComparer.Instance).Should().Be(1);
+ list2.ListCompares(list1, ToolComponentComparer.Instance).Should().Be(-1);
+
+ list1.Insert(0, new ToolComponent() { SemanticVersion = "1.0.1", Name = "scan tool" });
+ list2.Insert(0, new ToolComponent() { SemanticVersion = "1.0.3", Name = "scan tool" });
+
+ list1.ListCompares(list2, ToolComponentComparer.Instance).Should().Be(-1);
+ list2.ListCompares(list1, ToolComponentComparer.Instance).Should().Be(1);
+
+ list1.Insert(0, new ToolComponent() { ReleaseDateUtc = "2/8/2022", Name = "scan tool" });
+ list2.Insert(0, new ToolComponent() { ReleaseDateUtc = "1/1/2022", Name = "scan tool" });
+
+ list1.ListCompares(list2, ToolComponentComparer.Instance).Should().Be(1);
+ list2.ListCompares(list1, ToolComponentComparer.Instance).Should().Be(-1);
+
+ list1.Insert(0, new ToolComponent() { DownloadUri = new Uri("https://example/download/v1"), Name = "scan tool" });
+ list2.Insert(0, new ToolComponent() { DownloadUri = new Uri("https://example/download/v2"), Name = "scan tool" });
+
+ list1.ListCompares(list2, ToolComponentComparer.Instance).Should().Be(-1);
+ list2.ListCompares(list1, ToolComponentComparer.Instance).Should().Be(1);
+
+ list1.Insert(0, new ToolComponent() { Rules = new ReportingDescriptor[] { new ReportingDescriptor { Id = "TESTRULE001" } }, Name = "scan tool" });
+ list2.Insert(0, new ToolComponent() { Rules = new ReportingDescriptor[] { new ReportingDescriptor { Id = "TESTRULE002" } }, Name = "scan tool" });
+
+ list1.ListCompares(list2, ToolComponentComparer.Instance).Should().Be(-1);
+ list2.ListCompares(list1, ToolComponentComparer.Instance).Should().Be(1);
+ }
+
+ [Fact]
+ public void ReportingDescriptorComparer_Tests()
+ {
+ var rules1 = new List();
+ var rules2 = new List();
+
+ rules1.Add(null);
+ rules2.Add(null);
+
+ rules1.ListCompares(rules2, ReportingDescriptorComparer.Instance).Should().Be(0);
+
+ rules1.Insert(0, new ReportingDescriptor() { Id = "TestRule1" });
+ rules2.Insert(0, new ReportingDescriptor() { Id = "TestRule2" });
+
+ rules1.ListCompares(rules2, ReportingDescriptorComparer.Instance).Should().Be(-1);
+ rules2.ListCompares(rules1, ReportingDescriptorComparer.Instance).Should().Be(1);
+
+ rules1.Insert(0, new ReportingDescriptor() { DeprecatedIds = new string[] { "OldRuleId3" }, Id = "TestRule1" });
+ rules2.Insert(0, new ReportingDescriptor() { DeprecatedIds = new string[] { "OldRuleId2" }, Id = "TestRule1" });
+
+ rules1.ListCompares(rules2, ReportingDescriptorComparer.Instance).Should().Be(1);
+ rules2.ListCompares(rules1, ReportingDescriptorComparer.Instance).Should().Be(-1);
+
+ rules1.Insert(0, new ReportingDescriptor() { Guid = Guid.NewGuid().ToString(), Id = "TestRule1" });
+ rules2.Insert(0, new ReportingDescriptor() { Guid = Guid.Empty.ToString(), Id = "TestRule1" });
+
+ rules1.ListCompares(rules2, ReportingDescriptorComparer.Instance).Should().Be(1);
+ rules2.ListCompares(rules1, ReportingDescriptorComparer.Instance).Should().Be(-1);
+
+ rules1.Insert(0, new ReportingDescriptor() { DeprecatedIds = new string[] { Guid.Empty.ToString() }, Id = "TestRule1" });
+ rules2.Insert(0, new ReportingDescriptor() { DeprecatedIds = new string[] { Guid.NewGuid().ToString() }, Id = "TestRule1" });
+
+ rules1.ListCompares(rules2, ReportingDescriptorComparer.Instance).Should().Be(-1);
+ rules2.ListCompares(rules1, ReportingDescriptorComparer.Instance).Should().Be(1);
+
+ rules1.Insert(0, new ReportingDescriptor() { Name = "UnusedVariable", Id = "TestRule1" });
+ rules2.Insert(0, new ReportingDescriptor() { Name = "", Id = "TestRule1" });
+
+ rules1.ListCompares(rules2, ReportingDescriptorComparer.Instance).Should().Be(1);
+ rules2.ListCompares(rules1, ReportingDescriptorComparer.Instance).Should().Be(-1);
+
+ rules1.Insert(0, new ReportingDescriptor() { ShortDescription = new MultiformatMessageString { Text = "Remove unused variable" }, Id = "TestRule1" });
+ rules2.Insert(0, new ReportingDescriptor() { ShortDescription = new MultiformatMessageString { Text = "Wrong description" }, Id = "TestRule1" });
+
+ rules1.ListCompares(rules2, ReportingDescriptorComparer.Instance).Should().Be(-1);
+ rules2.ListCompares(rules1, ReportingDescriptorComparer.Instance).Should().Be(1);
+
+ rules1.Insert(0, new ReportingDescriptor() { FullDescription = new MultiformatMessageString { Text = "Remove unused variable" }, Id = "TestRule1" });
+ rules2.Insert(0, new ReportingDescriptor() { FullDescription = new MultiformatMessageString { Text = "Wrong description" }, Id = "TestRule1" });
+
+ rules1.ListCompares(rules2, ReportingDescriptorComparer.Instance).Should().Be(-1);
+ rules2.ListCompares(rules1, ReportingDescriptorComparer.Instance).Should().Be(1);
+
+ rules1.Insert(0, new ReportingDescriptor() { DefaultConfiguration = new ReportingConfiguration { Level = FailureLevel.Note }, Id = "TestRule1" });
+ rules2.Insert(0, new ReportingDescriptor() { DefaultConfiguration = new ReportingConfiguration { Level = FailureLevel.Error }, Id = "TestRule1" });
+
+ rules1.ListCompares(rules2, ReportingDescriptorComparer.Instance).Should().Be(-1);
+ rules2.ListCompares(rules1, ReportingDescriptorComparer.Instance).Should().Be(1);
+
+ rules1.Insert(0, new ReportingDescriptor() { HelpUri = new Uri("http://example.net/rule/id"), Id = "TestRule1" });
+ rules2.Insert(0, new ReportingDescriptor() { HelpUri = new Uri("http://example.net"), Id = "TestRule1" });
+
+ rules1.ListCompares(rules2, ReportingDescriptorComparer.Instance).Should().Be(1);
+ rules2.ListCompares(rules1, ReportingDescriptorComparer.Instance).Should().Be(-1);
+
+ rules1.Insert(0, new ReportingDescriptor() { Help = new MultiformatMessageString { Text = "Helping texts." }, Id = "TestRule1" });
+ rules2.Insert(0, new ReportingDescriptor() { Help = new MultiformatMessageString { Text = "For customers." }, Id = "TestRule1" });
+
+ rules1.ListCompares(rules2, ReportingDescriptorComparer.Instance).Should().Be(1);
+ rules2.ListCompares(rules1, ReportingDescriptorComparer.Instance).Should().Be(-1);
+ }
+
+ [Fact]
+ public void RegionComparer_Tests()
+ {
+ var regions1 = new List();
+ var regions2 = new List();
+
+ regions1.Add(null);
+ regions2.Add(null);
+
+ regions1.ListCompares(regions2, RegionComparer.Instance).Should().Be(0);
+
+ regions1.Insert(0, new Region() { StartLine = 0, StartColumn = 0 });
+ regions2.Insert(0, new Region() { StartLine = 1, StartColumn = 0 });
+
+ regions1.ListCompares(regions2, RegionComparer.Instance).Should().Be(-1);
+ regions2.ListCompares(regions1, RegionComparer.Instance).Should().Be(1);
+
+ regions1.Insert(0, new Region() { StartLine = 0, StartColumn = 1 });
+ regions2.Insert(0, new Region() { StartLine = 0, StartColumn = 0 });
+
+ regions1.ListCompares(regions2, RegionComparer.Instance).Should().Be(1);
+ regions2.ListCompares(regions1, RegionComparer.Instance).Should().Be(-1);
+
+ regions1.Insert(0, new Region() { StartLine = 10, EndLine = 11, StartColumn = 0 });
+ regions2.Insert(0, new Region() { StartLine = 10, EndLine = 10, StartColumn = 0 });
+
+ regions1.ListCompares(regions2, RegionComparer.Instance).Should().Be(1);
+ regions2.ListCompares(regions1, RegionComparer.Instance).Should().Be(-1);
+
+ regions1.Insert(0, new Region() { StartLine = 10, EndLine = 10, StartColumn = 5, EndColumn = 23 });
+ regions2.Insert(0, new Region() { StartLine = 10, EndLine = 10, StartColumn = 5, EndColumn = 7 });
+
+ regions1.ListCompares(regions2, RegionComparer.Instance).Should().Be(1);
+ regions2.ListCompares(regions1, RegionComparer.Instance).Should().Be(-1);
+
+ regions1.Insert(0, new Region() { CharOffset = 100, CharLength = 30 });
+ regions2.Insert(0, new Region() { CharOffset = 36, CharLength = 30 });
+
+ regions1.ListCompares(regions2, RegionComparer.Instance).Should().Be(1);
+ regions2.ListCompares(regions1, RegionComparer.Instance).Should().Be(-1);
+
+ regions1.Insert(0, new Region() { CharOffset = 100, CharLength = 47 });
+ regions2.Insert(0, new Region() { CharOffset = 100, CharLength = 326 });
+
+ regions1.ListCompares(regions2, RegionComparer.Instance).Should().Be(-1);
+ regions2.ListCompares(regions1, RegionComparer.Instance).Should().Be(1);
+
+ regions1.Insert(0, new Region() { ByteOffset = 226, ByteLength = 11 });
+ regions2.Insert(0, new Region() { ByteOffset = 1623, ByteLength = 11 });
+
+ regions1.ListCompares(regions2, RegionComparer.Instance).Should().Be(-1);
+ regions2.ListCompares(regions1, RegionComparer.Instance).Should().Be(1);
+
+ regions1.Insert(0, new Region() { ByteOffset = 67, ByteLength = 9 });
+ regions2.Insert(0, new Region() { ByteOffset = 67, ByteLength = 11 });
+
+ regions1.ListCompares(regions2, RegionComparer.Instance).Should().Be(-1);
+ regions2.ListCompares(regions1, RegionComparer.Instance).Should().Be(1);
+ }
+
+ [Fact]
+ public void ArtifactComparer_Tests()
+ {
+ var artifacts1 = new List();
+ var artifacts2 = new List();
+
+ artifacts1.Add(null);
+ artifacts2.Add(null);
+
+ artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(0);
+
+ artifacts1.Insert(0, new Artifact() { Description = new Message { Text = "Represents for an artifact" } });
+ artifacts2.Insert(0, new Artifact() { Description = new Message { Text = "A source file artifact" } });
+
+ artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(1);
+ artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(-1);
+
+ artifacts1.Insert(0, new Artifact() { Location = new ArtifactLocation { Index = 0 } });
+ artifacts2.Insert(0, new Artifact() { Location = new ArtifactLocation { Index = 1 } });
+
+ artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(-1);
+ artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(1);
+
+ artifacts1.Insert(0, new Artifact() { ParentIndex = 0 });
+ artifacts2.Insert(0, new Artifact() { ParentIndex = 1 });
+
+ artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(-1);
+ artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(1);
+
+ artifacts1.Insert(0, new Artifact() { Offset = 2 });
+ artifacts2.Insert(0, new Artifact() { Offset = 1 });
+
+ artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(1);
+ artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(-1);
+
+ artifacts1.Insert(0, new Artifact() { Length = 102542 });
+ artifacts2.Insert(0, new Artifact() { Length = -1 });
+
+ artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(1);
+ artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(-1);
+
+ artifacts1.Insert(0, new Artifact() { Roles = ArtifactRoles.AnalysisTarget | ArtifactRoles.Attachment });
+ artifacts2.Insert(0, new Artifact() { Roles = ArtifactRoles.Policy });
+
+ artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(-1);
+ artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(1);
+
+ artifacts1.Insert(0, new Artifact() { MimeType = "text" });
+ artifacts2.Insert(0, new Artifact() { MimeType = "video" });
+
+ artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(-1);
+ artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(1);
+
+ artifacts1.Insert(0, new Artifact() { Contents = new ArtifactContent { Text = "\"string\"" } });
+ artifacts2.Insert(0, new Artifact() { Contents = new ArtifactContent { Text = "var result = 0;" } });
+
+ artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(-1);
+ artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(1);
+
+ artifacts1.Insert(0, new Artifact() { Encoding = "UTF-16BE" });
+ artifacts2.Insert(0, new Artifact() { Encoding = "UTF-16LE" });
+
+ artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(-1);
+ artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(1);
+
+ artifacts1.Insert(0, new Artifact() { SourceLanguage = "html" });
+ artifacts2.Insert(0, new Artifact() { SourceLanguage = "csharp/7" });
+
+ artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(1);
+ artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(-1);
+
+ artifacts1.Insert(0, new Artifact() { Hashes = new Dictionary { { "sha-256", "..." } } });
+ artifacts2.Insert(0, new Artifact() { Hashes = new Dictionary { { "sha-512", "..." } } });
+
+ artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(-1);
+ artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(1);
+
+ artifacts1.Insert(0, new Artifact() { LastModifiedTimeUtc = DateTime.UtcNow });
+ artifacts2.Insert(0, new Artifact() { LastModifiedTimeUtc = DateTime.UtcNow.AddDays(-1) });
+
+ artifacts1.ListCompares(artifacts2, ArtifactComparer.Instance).Should().Be(1);
+ artifacts2.ListCompares(artifacts1, ArtifactComparer.Instance).Should().Be(-1);
+ }
+
+ [Fact]
+ public void ThreadFlowComparer_Tests()
+ {
+ var threadFlow1 = new List();
+ var threadFlow2 = new List();
+
+ threadFlow1.Add(null);
+ threadFlow2.Add(null);
+
+ threadFlow1.ListCompares(threadFlow2, ThreadFlowComparer.Instance).Should().Be(0);
+
+ threadFlow1.Insert(0, new ThreadFlow() { Id = "threadFlow1" });
+ threadFlow2.Insert(0, new ThreadFlow() { Id = "threadFlow2" });
+
+ threadFlow1.ListCompares(threadFlow2, ThreadFlowComparer.Instance).Should().Be(-1);
+ threadFlow2.ListCompares(threadFlow1, ThreadFlowComparer.Instance).Should().Be(1);
+
+ threadFlow1.Insert(0, new ThreadFlow() { Message = new Message { Id = "arg1" } });
+ threadFlow2.Insert(0, new ThreadFlow() { Message = new Message { Id = "fileArg" } });
+
+ threadFlow1.ListCompares(threadFlow2, ThreadFlowComparer.Instance).Should().Be(-1);
+ threadFlow2.ListCompares(threadFlow1, ThreadFlowComparer.Instance).Should().Be(1);
+
+ var loc1 = new Location
+ {
+ PhysicalLocation = new PhysicalLocation
+ {
+ ArtifactLocation = new ArtifactLocation
+ {
+ Uri = new Uri("path/to/file1.c", UriKind.Relative)
+ }
+ }
+ };
+
+ var loc2 = new Location
+ {
+ PhysicalLocation = new PhysicalLocation
+ {
+ ArtifactLocation = new ArtifactLocation
+ {
+ Uri = new Uri("path/to/file2.c", UriKind.Relative)
+ }
+ }
+ };
+
+ threadFlow1.Insert(0, new ThreadFlow() { Locations = new ThreadFlowLocation[] { new ThreadFlowLocation { Location = loc1 } } });
+ threadFlow2.Insert(0, new ThreadFlow() { Locations = new ThreadFlowLocation[] { new ThreadFlowLocation { Location = loc2 } } });
+
+ threadFlow1.ListCompares(threadFlow2, ThreadFlowComparer.Instance).Should().Be(-1);
+ threadFlow2.ListCompares(threadFlow1, ThreadFlowComparer.Instance).Should().Be(1);
+ }
+
+ [Fact]
+ public void ThreadFlowLocationComparer_Tests()
+ {
+ var locations1 = new List();
+ var locations2 = new List();
+
+ locations1.Add(null);
+ locations2.Add(null);
+
+ locations1.ListCompares(locations2, ThreadFlowLocationComparer.Instance).Should().Be(0);
+
+ var loc1 = new Location
+ {
+ PhysicalLocation = new PhysicalLocation
+ {
+ ArtifactLocation = new ArtifactLocation
+ {
+ Uri = new Uri("path/to/file1.c", UriKind.Relative)
+ }
+ }
+ };
+
+ var loc2 = new Location
+ {
+ PhysicalLocation = new PhysicalLocation
+ {
+ ArtifactLocation = new ArtifactLocation
+ {
+ Uri = new Uri("path/to/file2.c", UriKind.Relative)
+ }
+ }
+ };
+
+ locations1.Insert(0, new ThreadFlowLocation() { Location = loc1 });
+ locations2.Insert(0, new ThreadFlowLocation() { Location = loc2 });
+
+ locations1.ListCompares(locations2, ThreadFlowLocationComparer.Instance).Should().Be(-1);
+ locations2.ListCompares(locations1, ThreadFlowLocationComparer.Instance).Should().Be(1);
+
+ locations1.Insert(0, new ThreadFlowLocation() { Index = 2, Location = loc1 });
+ locations2.Insert(0, new ThreadFlowLocation() { Index = 1, Location = loc2 });
+
+ locations1.ListCompares(locations2, ThreadFlowLocationComparer.Instance).Should().Be(1);
+ locations2.ListCompares(locations1, ThreadFlowLocationComparer.Instance).Should().Be(-1);
+
+ locations1.Insert(0, new ThreadFlowLocation() { Kinds = new string[] { "memory" }, Location = loc1 });
+ locations2.Insert(0, new ThreadFlowLocation() { Kinds = new string[] { "call", "branch" }, Location = loc2 });
+
+ locations1.ListCompares(locations2, ThreadFlowLocationComparer.Instance).Should().Be(-1);
+ locations2.ListCompares(locations1, ThreadFlowLocationComparer.Instance).Should().Be(1);
+
+ locations1.Insert(0, new ThreadFlowLocation() { NestingLevel = 3 });
+ locations2.Insert(0, new ThreadFlowLocation() { NestingLevel = 2 });
+
+ locations1.ListCompares(locations2, ThreadFlowLocationComparer.Instance).Should().Be(1);
+ locations2.ListCompares(locations1, ThreadFlowLocationComparer.Instance).Should().Be(-1);
+
+ locations1.Insert(0, new ThreadFlowLocation() { ExecutionOrder = 2 });
+ locations2.Insert(0, new ThreadFlowLocation() { ExecutionOrder = 1 });
+
+ locations1.ListCompares(locations2, ThreadFlowLocationComparer.Instance).Should().Be(1);
+ locations2.ListCompares(locations1, ThreadFlowLocationComparer.Instance).Should().Be(-1);
+
+ locations1.Insert(0, new ThreadFlowLocation() { ExecutionTimeUtc = DateTime.UtcNow });
+ locations2.Insert(0, new ThreadFlowLocation() { ExecutionTimeUtc = DateTime.UtcNow.AddHours(-2) });
+
+ locations1.ListCompares(locations2, ThreadFlowLocationComparer.Instance).Should().Be(1);
+ locations2.ListCompares(locations1, ThreadFlowLocationComparer.Instance).Should().Be(-1);
+
+ locations1.Insert(0, new ThreadFlowLocation() { Importance = ThreadFlowLocationImportance.Essential });
+ locations2.Insert(0, new ThreadFlowLocation() { Importance = ThreadFlowLocationImportance.Unimportant });
+
+ locations1.ListCompares(locations2, ThreadFlowLocationComparer.Instance).Should().Be(-1);
+ locations2.ListCompares(locations1, ThreadFlowLocationComparer.Instance).Should().Be(1);
+ }
+
+ [Fact]
+ public void RunComparer_Tests()
+ {
+ var runs1 = new List();
+ var runs2 = new List();
+
+ runs1.Add(null);
+ runs2.Add(null);
+
+ runs1.ListCompares(runs2, RunComparer.Instance).Should().Be(0);
+
+ runs1.Insert(0, new Run() { Artifacts = new Artifact[] { new Artifact { Description = new Message { Text = "artifact 1" } } } });
+ runs2.Insert(0, new Run() { Artifacts = new Artifact[] { new Artifact { Description = new Message { Text = "artifact 2" } } } });
+
+ runs1.ListCompares(runs2, RunComparer.Instance).Should().Be(-1);
+ runs2.ListCompares(runs1, RunComparer.Instance).Should().Be(1);
+
+ var tool1 = new Tool { Driver = new ToolComponent { Name = "PREFast", Version = "1.0" } };
+ var tool2 = new Tool { Driver = new ToolComponent { Name = "PREFast", Version = "1.3" } };
+
+ runs1.Insert(0, new Run() { Tool = tool1 });
+ runs2.Insert(0, new Run() { Tool = tool2 });
+
+ runs1.ListCompares(runs2, RunComparer.Instance).Should().Be(-1);
+ runs2.ListCompares(runs1, RunComparer.Instance).Should().Be(1);
+
+ var result1 = new Result { RuleId = "CS001", Message = new Message { Text = "Issue of C# code" } };
+ var result2 = new Result { RuleId = "IDE692", Message = new Message { Text = "Issue by IDE" } };
+
+ runs1.Insert(0, new Run() { Results = new Result[] { result1 } });
+ runs2.Insert(0, new Run() { Results = new Result[] { result2 } });
+
+ runs1.ListCompares(runs2, RunComparer.Instance).Should().Be(-1);
+ runs2.ListCompares(runs1, RunComparer.Instance).Should().Be(1);
+ }
+
+ [Fact]
+ public void ComparerHelp_CompareUir_Tests()
+ {
+ var testUris = new List<(Uri, int)>()
+ {
+ (null, -1),
+ (null, 0),
+ (new Uri(@"", UriKind.RelativeOrAbsolute), 1),
+ (new Uri(string.Empty, UriKind.RelativeOrAbsolute), 0),
+ (new Uri(@"file.ext", UriKind.RelativeOrAbsolute), 1),
+ (new Uri(@"C:\path\file.ext", UriKind.RelativeOrAbsolute), -1),
+ (new Uri(@"\\hostname\path\file.ext", UriKind.RelativeOrAbsolute), -1),
+ (new Uri(@"file:///C:/path/file.ext", UriKind.RelativeOrAbsolute), 1),
+ (new Uri(@"\\hostname\c:\path\file.ext", UriKind.RelativeOrAbsolute), -1),
+ (new Uri(@"/home/username/path/file.ext", UriKind.RelativeOrAbsolute), -1),
+ (new Uri(@"nfs://servername/folder/file.ext", UriKind.RelativeOrAbsolute), 1),
+ (new Uri(@"file://hostname/C:/path/file.ext", UriKind.RelativeOrAbsolute), -1),
+ (new Uri(@"file:///home/username/path/file.ext", UriKind.RelativeOrAbsolute), -1),
+ (new Uri(@"ftp://ftp.example.com/folder/file.ext", UriKind.RelativeOrAbsolute), 1),
+ (new Uri(@"smb://servername/Share/folder/file.ext", UriKind.RelativeOrAbsolute), 1),
+ (new Uri(@"dav://example.hostname.com/folder/file.ext", UriKind.RelativeOrAbsolute), -1),
+ (new Uri(@"file://hostname/home/username/path/file.ext", UriKind.RelativeOrAbsolute), 1),
+ (new Uri(@"ftp://username@ftp.example.com/folder/file.ext", UriKind.RelativeOrAbsolute), 1),
+ (new Uri(@"scheme://servername.example.com/folder/file.ext", UriKind.RelativeOrAbsolute), 1),
+ (new Uri(@"https://github.com/microsoft/sarif-sdk/file.ext", UriKind.RelativeOrAbsolute), -1),
+ (new Uri(@"ssh://username@servername.example.com/folder/file.ext", UriKind.RelativeOrAbsolute), 1),
+ (new Uri(@"scheme://username@servername.example.com/folder/file.ext", UriKind.RelativeOrAbsolute), -1),
+ (new Uri(@"https://github.com/microsoft/sarif-sdk/file.ext?some-query-string", UriKind.RelativeOrAbsolute), -1),
+ };
+
+ for (int i = 1; i < testUris.Count; i++)
+ {
+ int result = testUris[i].Item1.UriCompares(testUris[i - 1].Item1);
+ result.Should().Be(testUris[i].Item2);
+ }
+ }
+ }
+}
diff --git a/src/Test.UnitTests.Sarif/RandomSarifLogGenerator.cs b/src/Test.UnitTests.Sarif/RandomSarifLogGenerator.cs
index 0d77102ae..8ec52b4b9 100644
--- a/src/Test.UnitTests.Sarif/RandomSarifLogGenerator.cs
+++ b/src/Test.UnitTests.Sarif/RandomSarifLogGenerator.cs
@@ -17,10 +17,10 @@ internal static class RandomSarifLogGenerator
{
public static string GeneratorBaseUri = @"C:\src\";
- public static Random GenerateRandomAndLog(ITestOutputHelper output, [CallerMemberName] string testName = "")
+ public static Random GenerateRandomAndLog(ITestOutputHelper output, [CallerMemberName] string testName = "", int? seed = null)
{
// Slightly roundabout. We want to randomly test this, but we also want to be able to repeat this if the test fails.
- int randomSeed = (new Random()).Next();
+ int randomSeed = seed ?? (new Random()).Next();
Random random = new Random(randomSeed);
@@ -29,7 +29,7 @@ public static Random GenerateRandomAndLog(ITestOutputHelper output, [CallerMembe
return random;
}
- public static SarifLog GenerateSarifLogWithRuns(Random randomGen, int runCount, int? resultCount = null)
+ public static SarifLog GenerateSarifLogWithRuns(Random randomGen, int runCount, int? resultCount = null, RandomDataFields dataFields = RandomDataFields.None)
{
SarifLog log = new SarifLog();
@@ -40,13 +40,13 @@ public static SarifLog GenerateSarifLogWithRuns(Random randomGen, int runCount,
for (int i = 0; i < runCount; i++)
{
- log.Runs.Add(GenerateRandomRun(randomGen, resultCount));
+ log.Runs.Add(GenerateRandomRun(randomGen, resultCount, dataFields));
}
return log;
}
- public static Run GenerateRandomRun(Random random, int? resultCount = null)
+ public static Run GenerateRandomRun(Random random, int? resultCount = null, RandomDataFields dataFields = RandomDataFields.None)
{
List ruleIds = new List() { "TEST001", "TEST002", "TEST003", "TEST004", "TEST005" };
List filePaths = GenerateFakeFiles(GeneratorBaseUri, random.Next(20) + 1).Select(a => new Uri(a)).ToList();
@@ -64,7 +64,7 @@ public static Run GenerateRandomRun(Random random, int? resultCount = null)
}
},
Artifacts = GenerateFiles(filePaths),
- Results = GenerateFakeResults(random, ruleIds, filePaths, results)
+ Results = GenerateFakeResults(random, ruleIds, filePaths, results, dataFields)
};
}
@@ -96,7 +96,7 @@ public static IEnumerable GenerateFakeFiles(string baseAddress, int coun
return results;
}
- public static IList GenerateFakeResults(Random random, List ruleIds, List filePaths, int resultCount)
+ public static IList GenerateFakeResults(Random random, List ruleIds, List filePaths, int resultCount, RandomDataFields dataFields = RandomDataFields.None)
{
List results = new List();
for (int i = 0; i < resultCount; i++)
@@ -118,9 +118,15 @@ public static IList GenerateFakeResults(Random random, List rule
Uri = filePaths[fileIndex],
Index = fileIndex
},
- }
+ },
+ LogicalLocations = dataFields.HasFlag(RandomDataFields.LogicalLocation) ?
+ GenerateLogicalLocations(random) :
+ null,
}
- }
+ },
+ CodeFlows = dataFields.HasFlag(RandomDataFields.CodeFlow) ?
+ GenerateCodeFlows(random, filePaths, dataFields) :
+ null,
});
}
return results;
@@ -161,5 +167,92 @@ public static IList GenerateRules(List ruleIds)
}
return rules;
}
+
+ public static IList GenerateCodeFlows(Random random, IList artifacts, RandomDataFields dataFields)
+ {
+ if (artifacts?.Any() != true)
+ {
+ return null;
+ }
+
+ var codeFlow = new CodeFlow
+ {
+ Message = new Message { Text = "code flow message" },
+ ThreadFlows = new[]
+ {
+ new ThreadFlow
+ {
+ Message = new Message { Text = "thread flow message" },
+ Locations = dataFields.HasFlag(RandomDataFields.ThreadFlow) ?
+ GenerateThreadFlowLocations(random, artifacts) :
+ null,
+ },
+ },
+ };
+
+ return new[] { codeFlow };
+ }
+
+ public static IList GenerateThreadFlowLocations(Random random, IList artifacts)
+ {
+ var locations = new List();
+
+ for (int i = 0; i < random.Next(10) + 1; i++)
+ {
+ locations.Add(new ThreadFlowLocation
+ {
+ Importance = RandomEnumValue(random),
+ Location = new Location
+ {
+ PhysicalLocation = new PhysicalLocation
+ {
+ ArtifactLocation = new ArtifactLocation
+ {
+ Index = random.Next(artifacts.Count),
+ },
+ Region = new Region
+ {
+ StartLine = random.Next(500),
+ StartColumn = random.Next(100),
+ },
+ },
+ },
+ });
+ }
+
+ return locations;
+ }
+
+ public static IList GenerateLogicalLocations(Random random)
+ {
+ var logicalLocations = new List();
+ for (int i = 0; i < random.Next(5); i++)
+ {
+ logicalLocations.Add(new LogicalLocation
+ {
+ Name = $"Class{i}",
+ Index = i,
+ FullyQualifiedName = "namespaceA::namespaceB::namespaceC",
+ Kind = LogicalLocationKind.Type,
+ });
+ }
+
+ return logicalLocations;
+ }
+
+ public static T RandomEnumValue(Random random) where T : Enum
+ {
+ Array enums = Enum.GetValues(typeof(T));
+ return (T)enums.GetValue(random.Next(enums.Length));
+ }
+ }
+
+ [Flags]
+ public enum RandomDataFields
+ {
+ None = 0,
+ CodeFlow = 0b1,
+ ThreadFlow = 0b10,
+ LogicalLocation = 0b100,
}
}
diff --git a/src/Test.UnitTests.Sarif/Visitors/SortingVisitorTests.cs b/src/Test.UnitTests.Sarif/Visitors/SortingVisitorTests.cs
new file mode 100644
index 000000000..b35f4dd94
--- /dev/null
+++ b/src/Test.UnitTests.Sarif/Visitors/SortingVisitorTests.cs
@@ -0,0 +1,272 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+using FluentAssertions;
+
+using Microsoft.CodeAnalysis.Sarif;
+using Microsoft.CodeAnalysis.Sarif.Visitors;
+using Microsoft.CodeAnalysis.Test.Utilities.Sarif;
+
+using Newtonsoft.Json;
+
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.CodeAnalysis.Test.UnitTests.Sarif.Visitors
+{
+ public class SortingVisitorTests : FileDiffingUnitTests
+ {
+ private readonly Random random;
+ private readonly ITestOutputHelper _outputHelper;
+
+ protected override string OutputFolderPath => Path.Combine(Path.GetDirectoryName(ThisAssembly.Location), "UnitTestOutput." + TypeUnderTest);
+
+ public SortingVisitorTests(ITestOutputHelper outputHelper) : base(outputHelper)
+ {
+ this._outputHelper = outputHelper;
+ this.random = RandomSarifLogGenerator.GenerateRandomAndLog(outputHelper);
+ }
+
+ [Fact]
+ public void SortingVisitor_ShuffleTest()
+ {
+ bool areEqual;
+
+ SarifLog originalLog = CreateTestSarifLog(this.random);
+ SarifLog shuffledLog1 = ShuffleSarifLog(originalLog, this.random);
+ SarifLog shuffledLog2 = ShuffleSarifLog(originalLog, this.random);
+
+ areEqual = SarifLogEqualityComparer.Instance.Equals(originalLog, shuffledLog1);
+ areEqual.Should().BeFalse();
+
+ areEqual = SarifLogEqualityComparer.Instance.Equals(originalLog, shuffledLog2);
+ areEqual.Should().BeFalse();
+
+ areEqual = SarifLogEqualityComparer.Instance.Equals(shuffledLog1, shuffledLog2);
+ areEqual.Should().BeFalse();
+
+ SarifLog sortedLog1 = new SortingVisitor().VisitSarifLog(shuffledLog1);
+ SarifLog sortedLog2 = new SortingVisitor().VisitSarifLog(shuffledLog2);
+
+ areEqual = this.VerifySarifLogAreSame(sortedLog1, sortedLog2);
+ areEqual.Should().BeTrue();
+
+ areEqual = SarifLogEqualityComparer.Instance.Equals(sortedLog1, sortedLog2);
+ areEqual.Should().BeTrue();
+
+ foreach (Run run in sortedLog1.Runs)
+ {
+ IList rules = run.Tool.Driver.Rules;
+ IList artifacts = run.Artifacts;
+
+ foreach (Result result in run.Results)
+ {
+ if (result.RuleIndex != -1)
+ {
+ int ruleIndex = rules.IndexOf(rules.First(r => r.Id.Equals(result.RuleId)));
+ result.RuleIndex.Should().Be(ruleIndex);
+ }
+
+ ArtifactLocation artifactLoc = result?.Locations?.First()?.PhysicalLocation?.ArtifactLocation;
+
+ if (artifactLoc != null && artifactLoc.Index != -1)
+ {
+ artifactLoc.Uri.Should().Be(artifacts[artifactLoc.Index].Location.Uri);
+ }
+ }
+ }
+ }
+
+ [Fact]
+ public void SortingVisitor_NullEmptyListTests()
+ {
+ // Create a log with all results have same values.
+ var sarifLog = new SarifLog
+ {
+ Runs = new[]
+ {
+ new Run
+ {
+ Results = new[]
+ {
+ new Result
+ {
+ RuleId = "TESTRULE001"
+ },
+ new Result
+ {
+ RuleId = "TESTRULE001"
+ },
+ new Result
+ {
+ RuleId = "TESTRULE001"
+ },
+ },
+ },
+ },
+ };
+
+ IList results = sarifLog.Runs[0].Results;
+ results[0].Locations = new[] { new Location { Message = new Message { Text = "test location" } } };
+ results[1].Locations = null;
+ results[2].Locations = new List();
+
+ SarifLog sortedLog = new SortingVisitor().VisitSarifLog(sarifLog);
+
+ results = sortedLog.Runs[0].Results;
+ results[0].Locations.Should().BeNull();
+ results[1].Locations.Should().BeEmpty();
+ results[2].Locations.Should().NotBeEmpty();
+ }
+
+ private static SarifLog CreateTestSarifLog(Random random)
+ {
+ SarifLog sarifLog = RandomSarifLogGenerator.GenerateSarifLogWithRuns(
+ randomGen: random,
+ runCount: random.Next(1, 5),
+ dataFields: RandomDataFields.CodeFlow | RandomDataFields.ThreadFlow | RandomDataFields.LogicalLocation);
+
+ return sarifLog;
+ }
+
+ private static SarifLog ShuffleSarifLog(SarifLog originalLog, Random random)
+ {
+ SarifLog logToBeShuffled = originalLog.DeepClone();
+
+ while (SarifLogEqualityComparer.Instance.Equals(logToBeShuffled, originalLog))
+ {
+ foreach (Run run in logToBeShuffled?.Runs)
+ {
+ IList rules = run?.Tool?.Driver?.Rules;
+ IList results = run?.Results;
+ IList artifacts = run?.Artifacts;
+
+ if (rules != null)
+ {
+ var ruleIndexMapping = new Dictionary();
+ rules = rules.Shuffle(random);
+ run.Tool.Driver.Rules = rules;
+
+ for (int i = 0; i < rules.Count; i++)
+ {
+ ruleIndexMapping[rules[i].Id] = i;
+ }
+
+ foreach (Result result in results.Where(r => r.RuleIndex != -1))
+ {
+ if (ruleIndexMapping.TryGetValue(result.RuleId, out int newIndex))
+ {
+ result.RuleIndex = newIndex;
+ }
+ }
+ }
+
+ if (artifacts != null)
+ {
+ var artifactIndexMapping = new Dictionary();
+ var oldMapping = new Dictionary();
+
+ for (int i = 0; i < artifacts.Count; i++)
+ {
+ oldMapping[artifacts[i]] = i;
+ }
+
+ artifacts = artifacts.Shuffle(random);
+ run.Artifacts = artifacts;
+
+ for (int i = 0; i < artifacts.Count; i++)
+ {
+ if (oldMapping.TryGetValue(artifacts[i], out int oldIndex))
+ {
+ artifactIndexMapping[oldIndex] = i;
+ }
+ }
+
+ var locToUpdate = new List();
+
+ locToUpdate.AddRange(
+ results
+ .SelectMany(r => r.Locations)
+ .Select(l => l.PhysicalLocation.ArtifactLocation));
+
+ locToUpdate.AddRange(
+ results
+ .SelectMany(r => r.CodeFlows)
+ .SelectMany(c => c.ThreadFlows)
+ .SelectMany(t => t.Locations)
+ .Select(l => l.Location.PhysicalLocation.ArtifactLocation));
+
+ foreach (ArtifactLocation artifactLocation in locToUpdate.Where(l => l.Index != -1))
+ {
+ if (artifactIndexMapping.TryGetValue(artifactLocation.Index, out int newIndex))
+ {
+ artifactLocation.Index = newIndex;
+ }
+ }
+ }
+
+ run.Results = results.Shuffle(random);
+
+ foreach (Result result in run.Results)
+ {
+ result.CodeFlows = result.CodeFlows.Shuffle(random);
+
+ foreach (CodeFlow codeFlow in result.CodeFlows)
+ {
+ codeFlow.ThreadFlows = codeFlow.ThreadFlows.Shuffle(random);
+
+ foreach (ThreadFlow threadFlow in codeFlow.ThreadFlows)
+ {
+ threadFlow.Locations = threadFlow.Locations.Shuffle(random);
+ }
+ }
+ }
+ }
+ logToBeShuffled.Runs = logToBeShuffled.Runs.Shuffle(random);
+ }
+
+ return logToBeShuffled;
+ }
+
+ private bool VerifySarifLogAreSame(SarifLog first, SarifLog second)
+ {
+ string firstLogText = ReadSarifLogAsString(first);
+ string secondLogText = ReadSarifLogAsString(second);
+
+ bool areEqual = AreEquivalent(firstLogText, secondLogText, out SarifLog _);
+
+ if (!areEqual)
+ {
+ string firstLogFile = Path.Combine(OutputFolderPath, $"{Guid.NewGuid()}.sarif");
+ string secondLogFile = Path.Combine(OutputFolderPath, $"{Guid.NewGuid()}.sarif");
+
+ File.WriteAllText(firstLogFile, firstLogText);
+ File.WriteAllText(secondLogFile, secondLogText);
+
+ var sb = new StringBuilder();
+ sb.AppendLine("The sorted Sarif logs did not match.");
+ sb.AppendLine("To compare all difference for this test suite:");
+ sb.AppendLine(FileDiffingUnitTests.GenerateDiffCommand(TypeUnderTest, firstLogFile, secondLogFile));
+ this._outputHelper.WriteLine(sb.ToString());
+ }
+
+ return areEqual;
+ }
+
+ private static string ReadSarifLogAsString(SarifLog sarifLog)
+ {
+ var settings = new JsonSerializerSettings()
+ {
+ Formatting = Formatting.Indented
+ };
+
+ return JsonConvert.SerializeObject(sarifLog, settings);
+ }
+ }
+}
diff --git a/src/Test.Utilities.Sarif/TestUtilitiesExtensions.cs b/src/Test.Utilities.Sarif/TestUtilitiesExtensions.cs
index 87dd9d33c..849a2d9f4 100644
--- a/src/Test.Utilities.Sarif/TestUtilitiesExtensions.cs
+++ b/src/Test.Utilities.Sarif/TestUtilitiesExtensions.cs
@@ -2,6 +2,8 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis.Sarif;
@@ -34,5 +36,21 @@ public static TestRuleBehaviors AccessibleWithinContextOnly(this TestRuleBehavio
// specifies this from the context object that parameterizes the call.
return behaviors & ~behaviors.AccessibleOutsideOfContextOnly();
}
+
+ public static IList Shuffle(this IList list, Random random)
+ {
+ if (list == null)
+ {
+ return null;
+ }
+
+ if (random == null)
+ {
+ // Random object with seed logged in test is required.
+ throw new ArgumentNullException(nameof(random));
+ }
+
+ return list.OrderBy(item => random.Next()).ToList();
+ }
}
}