diff --git a/Directory.Packages.props b/Directory.Packages.props index 284615d5c8..4721f9bd6d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,6 +10,8 @@ + + diff --git a/ExternalProjects/BizHawk.AnalyzersTests/.gitignore b/ExternalProjects/BizHawk.AnalyzersTests/.gitignore new file mode 100644 index 0000000000..cfac4d0ad2 --- /dev/null +++ b/ExternalProjects/BizHawk.AnalyzersTests/.gitignore @@ -0,0 +1,3 @@ +/bin +/obj +/TestResults diff --git a/ExternalProjects/BizHawk.AnalyzersTests/.run_tests_with_configuration.sh b/ExternalProjects/BizHawk.AnalyzersTests/.run_tests_with_configuration.sh new file mode 100755 index 0000000000..51430928f4 --- /dev/null +++ b/ExternalProjects/BizHawk.AnalyzersTests/.run_tests_with_configuration.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e +config="$1" +shift +dotnet test -c "$config" \ + -l "junit;LogFilePath=TestResults/{assembly}.coverage.xml;MethodFormat=Class;FailureBodyFormat=Verbose" \ + -l "console;verbosity=detailed" \ + "$@" diff --git a/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.Analyzer/FeatureNotImplementedAnalyzerTests.cs b/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.Analyzer/FeatureNotImplementedAnalyzerTests.cs new file mode 100644 index 0000000000..01966574e2 --- /dev/null +++ b/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.Analyzer/FeatureNotImplementedAnalyzerTests.cs @@ -0,0 +1,61 @@ +namespace BizHawk.Tests.Analyzers; + +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< + BizHawk.Analyzers.FeatureNotImplementedAnalyzer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + +[TestClass] +public sealed class FeatureNotImplementedAnalyzerTests +{ + [TestMethod] + public Task CheckMisuseOfFeatureNotImplementedAttr() + => Verify.VerifyAnalyzerAsync(""" + using System; + using BizHawk.Emulation.Common; + public static class Cases { + [FeatureNotImplemented] private static int X => throw new NotImplementedException(); + private static int Y { + [FeatureNotImplemented] get => throw new NotImplementedException(); + [FeatureNotImplemented] set => throw new NotImplementedException(); + } + [FeatureNotImplemented] private static int Z() + => throw new NotImplementedException(); + {|BHI3300:[FeatureNotImplemented] private static int A => default;|} + private static int B { + {|BHI3300:[FeatureNotImplemented] get => default;|} + {|BHI3300:[FeatureNotImplemented] set => _ = value;|} + } + {|BHI3300:[FeatureNotImplemented] private static int C() + => default;|} + // wrong exception type, same code but different message: + [FeatureNotImplemented] private static int D => {|BHI3300:throw new InvalidOperationException()|}; + private static int E { + [FeatureNotImplemented] get => {|BHI3300:throw new InvalidOperationException()|}; + [FeatureNotImplemented] set => {|BHI3300:throw new InvalidOperationException()|}; + } + [FeatureNotImplemented] private static int F() + => {|BHI3300:throw new InvalidOperationException()|}; + // same code but different message, since only the simplest of expected syntaxes is checked for: + [FeatureNotImplemented] private static int G => {|BHI3300:throw (new NotImplementedException())|}; + private static int H { + [FeatureNotImplemented] get => {|BHI3300:throw (new NotImplementedException())|}; + [FeatureNotImplemented] set => {|BHI3300:throw (new NotImplementedException())|}; + } + [FeatureNotImplemented] private static int I() + => {|BHI3300:throw (new NotImplementedException())|}; + // the "wat" cases (at least the ones that are reachable in practice) + {|BHI3300:[FeatureNotImplemented] private static int K { + get => default; + set => _ = value; + }|} + } + namespace BizHawk.Emulation.Common { + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)] + public sealed class FeatureNotImplementedAttribute: Attribute {} + } + """); +} diff --git a/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.Analyzer/FirstOrDefaultOnStructAnalyzerTests.cs b/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.Analyzer/FirstOrDefaultOnStructAnalyzerTests.cs new file mode 100644 index 0000000000..b36bef47e4 --- /dev/null +++ b/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.Analyzer/FirstOrDefaultOnStructAnalyzerTests.cs @@ -0,0 +1,37 @@ +namespace BizHawk.Tests.Analyzers; + +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< + BizHawk.Analyzers.FirstOrDefaultOnStructAnalyzer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + +[TestClass] +public sealed class FirstOrDefaultOnStructAnalyzerTests +{ + [TestMethod] + public Task CheckMisuseOfFirstOrDefault() + => Verify.VerifyAnalyzerAsync(""" + using System.Collections.Generic; + using System.Linq; + public static class Cases { + private static string? Y() + => new[] { 0x80.ToString(), 0x20.ToString(), 0x40.ToString() }.FirstOrDefault(static s => s.Length > 2); + private static string? Z() + => new List { 0x80, 0x20, 0x40 }.Select(static n => n.ToString()).FirstOrDefault(); + private static int A() + => {|BHI3100:new[] { 0x80, 0x20, 0x40 }.FirstOrDefault()|}; + private static int B() + => {|BHI3100:new List { 0x80, 0x20, 0x40 }.FirstOrDefault()|}; + private static int C() + => {|BHI3100:new[] { 0x80, 0x20, 0x40 }.FirstOrDefault(static n => n.ToString().Length > 2)|}; + private static int D() + => {|BHI3100:new List { 0x80, 0x20, 0x40 }.FirstOrDefault(static n => n.ToString().Length > 2)|}; + } + namespace BizHawk.Common.CollectionExtensions { + public static class CollectionExtensions {} // Analyzer short-circuits if this doesn't exist, since that's where the extension lives + } + """); +} diff --git a/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.Analyzer/HawkSourceAnalyzerTests.cs b/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.Analyzer/HawkSourceAnalyzerTests.cs new file mode 100644 index 0000000000..472e1a2402 --- /dev/null +++ b/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.Analyzer/HawkSourceAnalyzerTests.cs @@ -0,0 +1,146 @@ +namespace BizHawk.Tests.Analyzers; + +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< + BizHawk.Analyzers.HawkSourceAnalyzer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + +[TestClass] +public sealed class HawkSourceAnalyzerTests +{ + [TestMethod] + public Task CheckMisuseOfAnonymousClasses() + => Verify.VerifyAnalyzerAsync(""" + using System.Linq; + public static class Cases { + private static int Z() + => new[] { 0x80, 0x20, 0x40 } + .Select(static n => (N: n, StrLen: n.ToString().Length)) + .Where(static pair => pair.StrLen < 3) + .Sum(static pair => pair.N - pair.StrLen); + private static int A() + => new[] { 0x80, 0x20, 0x40 } + .Select(static n => {|BHI1002:new { N = n, StrLen = n.ToString().Length }|}) + .Where(static pair => pair.StrLen < 3) + .Sum(static pair => pair.N - pair.StrLen); + } + """); + + [TestMethod] + public Task CheckMisuseOfAnonymousDelegates() + => Verify.VerifyAnalyzerAsync(""" + using System.Linq; + public static class Cases { + private static int Z() + => new[] { 0x80, 0x20, 0x40 } + .Where(static n => n.ToString().Length < 3) + .Sum(); + private static int A() + => new[] { 0x80, 0x20, 0x40 } + .Where({|BHI1001:static delegate(int n) { return n.ToString().Length < 3; }|}) + .Sum(); + } + """); + + [TestMethod] + public Task CheckMisuseOfDefaultSwitchBranches() + => Verify.VerifyAnalyzerAsync(""" + using System; + public static class Cases { + private static int Y(string s) { + switch (s) { + case "zero": + return 0; + case "one": + return 1; + default: + throw new InvalidOperationException(); + } + } + private static int Z(string s) + => s switch { + "zero" => 0, + "one" => 1, + _ => throw new InvalidOperationException() + }; + private static int A(string s) { + switch (s) { + case "zero": + return 0; + case "one": + return 1; + default: + throw new NotImplementedException(); //TODO checking switch blocks was never implemented in the Analyzer + } + } + private static int B(string s) + => s switch { + "zero" => 0, + "one" => 1, + _ => {|BHI1005:throw new NotImplementedException()|} + }; + private static int C(string s) + => s switch { + "zero" => 0, + "one" => 1, + _ => {|BHI1005:throw (new NotImplementedException())|} // same code but different message, since only the simplest of expected syntaxes is checked for + }; + } + """); + + [TestMethod] + public Task CheckMisuseOfDiscards() + => Verify.VerifyAnalyzerAsync(""" + public static class Cases { + private static void Z() { + _ = string.Empty; + } + private static void A() { + var s = string.Empty; + {|BHI1006:_ = s|}; + } + } + """); + + [TestMethod] + public Task CheckMisuseOfInterpolatedString() + => Verify.VerifyAnalyzerAsync(""" + public static class Cases { + private static readonly int Z = $@"{0x100}".Length; + private static readonly int A = {|BHI1004:@$"{0x100}"|}.Length; + } + """); + + [TestMethod] + public Task CheckMisuseOfListSyntaxes() + => Verify.VerifyAnalyzerAsync(""" + public static class Cases { + private static readonly int[] Y = [ 0x80, 0x20, 0x40 ]; + private static readonly bool Z = Y is [ _, > 20, .. ]; + private static readonly int[] A = {|BHI1110:[0x80, 0x20, 0x40 ]|}; + private static readonly bool B = A is {|BHI1110:[ _, > 20, ..]|}; + private static readonly bool C = A is {|BHI1110:[_, > 20, ..]|}; + } + """); + + [TestMethod] + public Task CheckMisuseOfQuerySyntax() + => Verify.VerifyAnalyzerAsync(""" + using System.Linq; + using L = System.Collections.Generic.IEnumerable<(int N, int StrLen)>; + public static class Cases { + private static L Z() + => new[] { 0x80, 0x20, 0x40 } + .Select(static n => (N: n, StrLen: n.ToString().Length)) + .OrderBy(static pair => pair.StrLen); + private static L A() + => {|BHI1003:from n in new[] { 0x80, 0x20, 0x40 } + let pair = (N: n, StrLen: n.ToString().Length) + orderby pair.StrLen + select pair|}; + } + """); +} diff --git a/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.Analyzer/OrderBySelfAnalyzerTests.cs b/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.Analyzer/OrderBySelfAnalyzerTests.cs new file mode 100644 index 0000000000..3d03eca028 --- /dev/null +++ b/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.Analyzer/OrderBySelfAnalyzerTests.cs @@ -0,0 +1,42 @@ +namespace BizHawk.Tests.Analyzers; + +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< + BizHawk.Analyzers.OrderBySelfAnalyzer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + +[TestClass] +public sealed class OrderBySelfAnalyzerTests +{ + [TestMethod] + public Task CheckMisuseOfOrderBy() + => Verify.VerifyAnalyzerAsync(""" + using System.Linq; + using L = System.Collections.Generic.IEnumerable; + public static class Cases { + private static readonly int[] Numbers = [ 0x80, 0x20, 0x40 ]; + private static L Y() + => Numbers.OrderBy(static delegate(int n) { return n.ToString().Length; }); + private static L Z() + => Numbers.OrderByDescending(static n => n.ToString().Length); + private static L A() + => Numbers.OrderBy({|BHI3101:static delegate(int n) { return n; }|}); + private static L B() + => Numbers.OrderByDescending({|BHI3101:static delegate(int n) { return n; }|}); + private static L C() + => Numbers.OrderBy({|BHI3101:static (n) => n|}); + private static L D() + => Numbers.OrderByDescending({|BHI3101:static (n) => n|}); + private static L E() + => Numbers.OrderBy({|BHI3101:static n => n|}); + private static L F() + => Numbers.OrderByDescending({|BHI3101:static n => n|}); + } + namespace BizHawk.Common.CollectionExtensions { + public static class CollectionExtensions {} // Analyzer short-circuits if this doesn't exist, since that's where our backport lives + } + """); +} diff --git a/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.Analyzer/TryGetValueImplicitDiscardAnalyzerTests.cs b/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.Analyzer/TryGetValueImplicitDiscardAnalyzerTests.cs new file mode 100644 index 0000000000..936a6ec062 --- /dev/null +++ b/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.Analyzer/TryGetValueImplicitDiscardAnalyzerTests.cs @@ -0,0 +1,39 @@ +namespace BizHawk.Tests.Analyzers; + +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< + BizHawk.Analyzers.TryGetValueImplicitDiscardAnalyzer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + +[TestClass] +public sealed class TryGetValueImplicitDiscardAnalyzerTests +{ + [TestMethod] + public Task CheckMisuseOfTryGetValue() + => Verify.VerifyAnalyzerAsync(""" + using System.Collections.Generic; + public static class Cases { + private static Dictionary MakeDict() + => new(); + private sealed class CustomDict { + public bool TryGetValue(K key, out V val) { + val = default; + return false; + } + } + private static void Z() + => _ = MakeDict().TryGetValue("z", out _); + private static void A() + => {|BHI1200:MakeDict().TryGetValue("a", out _)|}; + private static void B() + => {|BHI1200:((IDictionary) MakeDict()).TryGetValue("b", out _)|}; + private static void C() + => {|BHI1200:((IReadOnlyDictionary) MakeDict()).TryGetValue("c", out _)|}; + private static void D() + => {|BHI1200:new CustomDict().TryGetValue("d", out _)|}; + } + """); +} diff --git a/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.Analyzer/UseNameofOperatorAnalyzerTests.cs b/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.Analyzer/UseNameofOperatorAnalyzerTests.cs new file mode 100644 index 0000000000..db7ce9b3dc --- /dev/null +++ b/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.Analyzer/UseNameofOperatorAnalyzerTests.cs @@ -0,0 +1,26 @@ +namespace BizHawk.Tests.Analyzers; + +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< + BizHawk.Analyzers.UseNameofOperatorAnalyzer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + +[TestClass] +public sealed class UseNameofOperatorAnalyzerTests +{ + [TestMethod] + public Task CheckMisuseOfNameofOperator() + => Verify.VerifyAnalyzerAsync(""" + public static class Cases { + private static readonly int Z = typeof(Cases).FullName.Length; + private static readonly int A = {|BHI1102:typeof(Cases).Name|}.Length; + private static readonly int B = {|BHI1103:typeof(Cases).ToString|}().Length; // the diagnostic is added to the method group part (MemberAccessExpressionSyntax) and not the invoke part; won't matter in practice + private static readonly int C = $"{{|BHI1103:typeof(Cases)|}}".Length; + private static readonly int D = (">" + {|BHI1103:typeof(Cases)|}).Length; + private static readonly int E = ({|BHI1103:typeof(Cases)|} + "<").Length; + } + """); +} diff --git a/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.Analyzer/UseTypeofOperatorAnalyzerTests.cs b/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.Analyzer/UseTypeofOperatorAnalyzerTests.cs new file mode 100644 index 0000000000..36325cc620 --- /dev/null +++ b/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.Analyzer/UseTypeofOperatorAnalyzerTests.cs @@ -0,0 +1,32 @@ +namespace BizHawk.Tests.Analyzers; + +using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< + BizHawk.Analyzers.UseTypeofOperatorAnalyzer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + +[TestClass] +public sealed class UseTypeofOperatorAnalyzerTests +{ + [TestMethod] + public Task CheckMisuseOfTypeofOperator() + => Verify.VerifyAnalyzerAsync(""" + public class Parent { + private string Z() + => 3.GetType().FullName; + private string A() + => {|BHI1101:this.GetType()|}.FullName; + } + public sealed class Child: Parent { + private string B() + => {|BHI1100:this.GetType()|}.FullName; + } + public readonly struct Struct { + private string C() + => {|BHI1100:this.GetType()|}.FullName; + } + """); +} diff --git a/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.AnalyzersTests.csproj b/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.AnalyzersTests.csproj new file mode 100644 index 0000000000..91ab46b7bb --- /dev/null +++ b/ExternalProjects/BizHawk.AnalyzersTests/BizHawk.AnalyzersTests.csproj @@ -0,0 +1,21 @@ + + + net8.0 + + + + true + $(NoWarn);IDE0065;SA1200 + Exe + + + + + + + + + + + + diff --git a/ExternalProjects/BizHawk.AnalyzersTests/Properties/AssemblyInfo.cs b/ExternalProjects/BizHawk.AnalyzersTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..ae411c7a1b --- /dev/null +++ b/ExternalProjects/BizHawk.AnalyzersTests/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] diff --git a/ExternalProjects/BizHawk.AnalyzersTests/run_tests_debug.sh b/ExternalProjects/BizHawk.AnalyzersTests/run_tests_debug.sh new file mode 100755 index 0000000000..6bb3fec83e --- /dev/null +++ b/ExternalProjects/BizHawk.AnalyzersTests/run_tests_debug.sh @@ -0,0 +1,2 @@ +#!/bin/sh +cd "$(dirname "$(realpath "$0")")" && ./.run_tests_with_configuration.sh "Debug" "$@" diff --git a/ExternalProjects/BizHawk.AnalyzersTests/run_tests_release.sh b/ExternalProjects/BizHawk.AnalyzersTests/run_tests_release.sh new file mode 100755 index 0000000000..ef5dbe332c --- /dev/null +++ b/ExternalProjects/BizHawk.AnalyzersTests/run_tests_release.sh @@ -0,0 +1,2 @@ +#!/bin/sh +cd "$(dirname "$(realpath "$0")")" && ./.run_tests_with_configuration.sh "Release" "$@"