Skip to content

Commit

Permalink
Add unit tests for first-party Analyzers
Browse files Browse the repository at this point in the history
  • Loading branch information
YoshiRulz committed Jul 1, 2024
1 parent 867b560 commit 57b5e77
Show file tree
Hide file tree
Showing 14 changed files with 424 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
<PackageVersion Include="Meziantou.Analyzer" Version="2.0.33" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="1.1.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.10.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" Version="1.1.2" />
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0" />
<PackageVersion Include="Microsoft.Data.Sqlite.Core" Version="8.0.4" />
<PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" />
Expand Down
3 changes: 3 additions & 0 deletions ExternalProjects/BizHawk.AnalyzersTests/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/bin
/obj
/TestResults
Original file line number Diff line number Diff line change
@@ -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" \
"$@"
Original file line number Diff line number Diff line change
@@ -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 {}
}
""");
}
Original file line number Diff line number Diff line change
@@ -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<int> { 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<int> { 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<int> { 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
}
""");
}
Original file line number Diff line number Diff line change
@@ -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|};
}
""");
}
Original file line number Diff line number Diff line change
@@ -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<int>;
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
}
""");
}
Original file line number Diff line number Diff line change
@@ -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<string, int> MakeDict()
=> new();
private sealed class CustomDict<K, V> {
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<string, int>) MakeDict()).TryGetValue("b", out _)|};
private static void C()
=> {|BHI1200:((IReadOnlyDictionary<string, int>) MakeDict()).TryGetValue("c", out _)|};
private static void D()
=> {|BHI1200:new CustomDict<string, int>().TryGetValue("d", out _)|};
}
""");
}
Original file line number Diff line number Diff line change
@@ -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;
}
""");
}
Loading

0 comments on commit 57b5e77

Please sign in to comment.