From 447a1d7777b0bccd0383f25639076a085ae49e9c Mon Sep 17 00:00:00 2001 From: Martin Strecker <103252490+martin-strecker-sonarsource@users.noreply.github.com> Date: Mon, 27 Mar 2023 11:46:39 +0200 Subject: [PATCH] SE: Enumerable ContentHash for ordered and unordered sets (#6979) --- .../SonarAnalyzer.Common/Helpers/HashCode.cs | 14 +++- .../SymbolicExecution/Roslyn/ProgramState.cs | 4 +- .../Helpers/HashCodeTest.cs | 84 +++++++++++++++++-- 3 files changed, 90 insertions(+), 12 deletions(-) diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/HashCode.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/HashCode.cs index cf7ca207fa5..c3491366a8b 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/HashCode.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/HashCode.cs @@ -28,12 +28,22 @@ namespace SonarAnalyzer.Helpers private const uint PreMultiplier = 3266489917U; private const uint PostMultiplier = 668265263U; private const int RotateOffset = 17; + private const int IntSeed = 393241; public static int DictionaryContentHash(IDictionary dictionary) => dictionary.Aggregate(0, (seed, kvp) => seed ^ Combine(kvp.Key, kvp.Value)); - public static int EnumerableContentHash(IEnumerable enumerable) => - enumerable.Aggregate(0, (seed, x) => Combine(seed, x)); + /// + /// Calculates a hash for the enumerable based on the content. The same values in a different order produce the same hash-code. + /// + public static int EnumerableUnorderedContentHash(IEnumerable enumerable) => + enumerable.Aggregate(IntSeed, (seed, x) => seed ^ (x?.GetHashCode() ?? 0)); + + /// + /// Calculates a hash for the enumerable based on the content. The same values in a different order produce different hash-codes. + /// + public static int EnumerableOrderedContentHash(IEnumerable enumerable) => + enumerable.Aggregate(IntSeed, Combine); public static int Combine(T1 a, T2 b) => (int)Seed.AddHash(a?.GetHashCode()).AddHash(b?.GetHashCode()); diff --git a/analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/ProgramState.cs b/analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/ProgramState.cs index ecc9dc0dc96..33bc9d27c32 100644 --- a/analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/ProgramState.cs +++ b/analyzers/src/SonarAnalyzer.Common/SymbolicExecution/Roslyn/ProgramState.cs @@ -145,8 +145,8 @@ public override int GetHashCode() => HashCode.DictionaryContentHash(OperationValue), HashCode.DictionaryContentHash(SymbolValue), HashCode.DictionaryContentHash(CaptureOperation), - HashCode.EnumerableContentHash(PreservedSymbols), - HashCode.EnumerableContentHash(Exceptions)); + HashCode.EnumerableUnorderedContentHash(PreservedSymbols), + HashCode.EnumerableOrderedContentHash(Exceptions)); public bool Equals(ProgramState other) => // VisitCount is not compared, two ProgramState are equal if their current state is equal. No matter what historical path led to it. diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/HashCodeTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/HashCodeTest.cs index 9df2bb091dd..af5145fcd59 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/HashCodeTest.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/HashCodeTest.cs @@ -18,6 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +#pragma warning disable CA1825 // Avoid zero-length array allocations + +using HashCode = SonarAnalyzer.Helpers.HashCode; + namespace SonarAnalyzer.UnitTest.Helpers { [TestClass] @@ -28,10 +32,10 @@ public class HashCodeTest [DataRow("Lorem Ipsum")] public void Combine_ProducesDifferentResults(string input) { - var hash2 = SonarAnalyzer.Helpers.HashCode.Combine(input, input); - var hash3 = SonarAnalyzer.Helpers.HashCode.Combine(input, input, input); - var hash4 = SonarAnalyzer.Helpers.HashCode.Combine(input, input, input, input); - var hash5 = SonarAnalyzer.Helpers.HashCode.Combine(input, input, input, input, input); + var hash2 = HashCode.Combine(input, input); + var hash3 = HashCode.Combine(input, input, input); + var hash4 = HashCode.Combine(input, input, input, input); + var hash5 = HashCode.Combine(input, input, input, input, input); hash2.Should().NotBe(0); hash3.Should().NotBe(0).And.NotBe(hash2); @@ -45,8 +49,8 @@ public void DictionaryContentHash_StableForUnsortedDictionary() var numbers = Enumerable.Range(1, 1000); var dict1 = numbers.ToDictionary(x => x, x => x); var dict2 = numbers.OrderByDescending(x => x).ToDictionary(x => x, x => x); - var hashCode1 = SonarAnalyzer.Helpers.HashCode.DictionaryContentHash(dict1); - var hashCode2 = SonarAnalyzer.Helpers.HashCode.DictionaryContentHash(dict2); + var hashCode1 = HashCode.DictionaryContentHash(dict1); + var hashCode2 = HashCode.DictionaryContentHash(dict2); hashCode1.Should().Be(hashCode2); } @@ -56,9 +60,73 @@ public void DictionaryContentHash_StableForImmutableDictionary() var numbers = Enumerable.Range(1, 1000); var dict1 = numbers.ToImmutableDictionary(x => x, x => x); var dict2 = numbers.OrderByDescending(x => x).ToImmutableDictionary(x => x, x => x); - var hashCode1 = SonarAnalyzer.Helpers.HashCode.DictionaryContentHash(dict1); - var hashCode2 = SonarAnalyzer.Helpers.HashCode.DictionaryContentHash(dict2); + var hashCode1 = HashCode.DictionaryContentHash(dict1); + var hashCode2 = HashCode.DictionaryContentHash(dict2); hashCode1.Should().Be(hashCode2); } + + [TestMethod] + public void EnumerableUnorderedContentHash_Empty() + { + var ints = new int[0]; + var strings = new string[0]; + + HashCode.EnumerableUnorderedContentHash(ints).Should().Be(HashCode.EnumerableUnorderedContentHash(new int[0])); + HashCode.EnumerableUnorderedContentHash(strings).Should().Be(HashCode.EnumerableUnorderedContentHash(strings)); + HashCode.EnumerableUnorderedContentHash(ints).Should().Be(HashCode.EnumerableUnorderedContentHash(strings)); + } + + [TestMethod] + public void EnumerableUnorderedContentHash_Order() + { + var ints1 = new[] { 0, 1, 2 }; + var ints2 = new[] { 2, 1, 0 }; + var ints3 = new[] { 0, 1, 8 }; + + HashCode.EnumerableUnorderedContentHash(ints1).Should().Be(HashCode.EnumerableUnorderedContentHash(ints2)).And.NotBe(0); + HashCode.EnumerableUnorderedContentHash(ints1).Should().NotBe(HashCode.EnumerableUnorderedContentHash(ints3)); + } + + [TestMethod] + public void EnumerableUnorderedContentHash_DifferentLength() + { + var ints1 = new[] { 0, 1, 2 }; + var ints2 = new[] { 0, 1, 2, 3 }; + + HashCode.EnumerableUnorderedContentHash(ints1).Should().NotBe(HashCode.EnumerableUnorderedContentHash(ints2)); + } + + [TestMethod] + public void EnumerableOrderedContentHash_Empty() + { + var ints = new int[0]; + var strings = new string[0]; + + HashCode.EnumerableOrderedContentHash(ints).Should().Be(HashCode.EnumerableOrderedContentHash(new int[0])); + HashCode.EnumerableOrderedContentHash(strings).Should().Be(HashCode.EnumerableOrderedContentHash(strings)); + HashCode.EnumerableOrderedContentHash(ints).Should().Be(HashCode.EnumerableOrderedContentHash(strings)); + } + + [TestMethod] + public void EnumerableOrderedContentHash_Order() + { + var ints1 = new[] { 0, 1, 2 }; + var ints2 = new[] { 0, 1, 2 }; + var ints3 = new[] { 2, 1, 0 }; + var ints4 = new[] { 0, 1, 8 }; + + HashCode.EnumerableOrderedContentHash(ints1).Should().Be(HashCode.EnumerableOrderedContentHash(ints2)).And.NotBe(0); + HashCode.EnumerableOrderedContentHash(ints1).Should().NotBe(HashCode.EnumerableOrderedContentHash(ints3)); + HashCode.EnumerableOrderedContentHash(ints1).Should().NotBe(HashCode.EnumerableOrderedContentHash(ints4)); + } + + [TestMethod] + public void EnumerableOrderedContentHash_DifferentLength() + { + var ints1 = new[] { 0, 1, 2 }; + var ints2 = new[] { 0, 1, 2, 3 }; + + HashCode.EnumerableOrderedContentHash(ints1).Should().NotBe(HashCode.EnumerableOrderedContentHash(ints2)); + } } }