using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using NUnit.Framework; using SMAPI.ModBuildConfig.Analyzer.Tests.Framework; using StardewModdingAPI.ModBuildConfig.Analyzer; namespace SMAPI.ModBuildConfig.Analyzer.Tests { /// Unit tests for the C# analyzers. [TestFixture] public class UnitTests : DiagnosticVerifier { /********* ** Properties *********/ /// Sample C# code which contains a simplified representation of Stardew Valley's Netcode types, and sample mod code with a {{test-code}} placeholder for the code being tested. const string SampleProgram = @" using System; using StardewValley; using Netcode; namespace Netcode { public class NetInt : NetFieldBase { } public class NetRef : NetFieldBase { } public class NetFieldBase where TSelf : NetFieldBase { public T Value { get; set; } public static implicit operator T(NetFieldBase field) => field.Value; } } namespace StardewValley { class Item { public NetInt category { get; } = new NetInt { Value = 42 }; // SMAPI002: use Category instead public NetInt type { get; } = new NetInt { Value = 42 }; public NetRef refField { get; } = null; } class SObject : Item { } } namespace SampleMod { class ModEntry { public void Entry() { Item item = null; SObject obj = null; // this line should raise diagnostics {{test-code}} // line 38 // these lines should not if (item.type.Value != 42); } } } "; /// The line number where the unit tested code is injected into . private const int SampleCodeLine = 38; /// The column number where the unit tested code is injected into . private const int SampleCodeColumn = 25; /********* ** Unit tests *********/ /// Test that no diagnostics are raised for an empty code block. [TestCase] public void EmptyCode_HasNoDiagnostics() { // arrange string test = @""; // assert this.VerifyCSharpDiagnostic(test); } /// Test that the expected diagnostic message is raised for implicit net field comparisons. /// The code line to test. /// The column within the code line where the diagnostic message should be reported. /// The expression which should be reported. /// The source type name which should be reported. /// The target type name which should be reported. [TestCase("if (item.type < 42);", 4, "item.type", "NetInt", "int")] [TestCase("if (item.type <= 42);", 4, "item.type", "NetInt", "int")] [TestCase("if (item.type > 42);", 4, "item.type", "NetInt", "int")] [TestCase("if (item.type >= 42);", 4, "item.type", "NetInt", "int")] [TestCase("if (item.type == 42);", 4, "item.type", "NetInt", "int")] [TestCase("if (item.type != 42);", 4, "item.type", "NetInt", "int")] [TestCase("if (item.refField != null);", 4, "item.refField", "NetRef", "object")] [TestCase("if (item?.type != 42);", 4, "item?.type", "NetInt", "int")] [TestCase("if (obj.type != 42);", 4, "obj.type", "NetInt", "int")] public void AvoidImplicitNetFieldComparisons_RaisesDiagnostic(string codeText, int column, string expression, string fromType, string toType) { // arrange string code = UnitTests.SampleProgram.Replace("{{test-code}}", codeText); DiagnosticResult expected = new DiagnosticResult { Id = "SMAPI001", Message = $"This implicitly converts '{expression}' from {fromType} to {toType}, but {fromType} has unintuitive implicit conversion rules. Consider comparing against the actual value instead to avoid bugs. See https://smapi.io/buildmsg/SMAPI001 for details.", Severity = DiagnosticSeverity.Warning, Locations = new[] { new DiagnosticResultLocation("Test0.cs", UnitTests.SampleCodeLine, UnitTests.SampleCodeColumn + column) } }; // assert this.VerifyCSharpDiagnostic(code, expected); } /// Test that the expected diagnostic message is raised for avoidable net field references. /// The code line to test. /// The column within the code line where the diagnostic message should be reported. /// The expression which should be reported. /// The net type name which should be reported. /// The suggested property name which should be reported. [TestCase("int category = item.category;", 15, "item.category", "NetInt", "Category")] [TestCase("int category = (item).category;", 15, "(item).category", "NetInt", "Category")] [TestCase("int category = ((Item)item).category;", 15, "((Item)item).category", "NetInt", "Category")] [TestCase("int category = obj.category;", 15, "obj.category", "NetInt", "Category")] public void AvoidNetFields_RaisesDiagnostic(string codeText, int column, string expression, string netType, string suggestedProperty) { // arrange string code = UnitTests.SampleProgram.Replace("{{test-code}}", codeText); DiagnosticResult expected = new DiagnosticResult { Id = "SMAPI002", Message = $"'{expression}' is a {netType} field; consider using the {suggestedProperty} property instead. See https://smapi.io/buildmsg/SMAPI002 for details.", Severity = DiagnosticSeverity.Warning, Locations = new[] { new DiagnosticResultLocation("Test0.cs", UnitTests.SampleCodeLine, UnitTests.SampleCodeColumn + column) } }; // assert this.VerifyCSharpDiagnostic(code, expected); } /********* ** Helpers *********/ /// Get the analyzer being tested. protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() { return new NetFieldAnalyzer(); } } }