2018-04-10 07:32:00 +08:00
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
{
2018-04-11 06:23:57 +08:00
/// <summary>Unit tests for <see cref="NetFieldAnalyzer"/>.</summary>
2018-04-10 07:32:00 +08:00
[TestFixture]
2018-04-11 06:23:57 +08:00
public class NetFieldAnalyzerTests : DiagnosticVerifier
2018-04-10 07:32:00 +08:00
{
/ * * * * * * * * *
* * Properties
* * * * * * * * * /
2018-04-11 06:23:57 +08:00
/// <summary>Sample C# mod code, with a {{test-code}} placeholder for the code in the Entry method to test.</summary>
2018-04-10 07:32:00 +08:00
const string SampleProgram = @ "
using System ;
2018-04-10 10:33:45 +08:00
using StardewValley ;
2018-04-10 07:32:00 +08:00
using Netcode ;
2018-04-11 06:22:16 +08:00
using SObject = StardewValley . Object ;
2018-04-10 07:32:00 +08:00
2018-04-10 10:33:45 +08:00
namespace SampleMod
{
2018-04-10 07:32:00 +08:00
class ModEntry
{
public void Entry ( )
{
2018-04-11 06:22:16 +08:00
{ { test - code } }
2018-04-10 07:32:00 +08:00
}
}
}
";
/// <summary>The line number where the unit tested code is injected into <see cref="SampleProgram"/>.</summary>
2018-04-28 07:59:41 +08:00
private const int SampleCodeLine = 13 ;
2018-04-10 07:32:00 +08:00
/// <summary>The column number where the unit tested code is injected into <see cref="SampleProgram"/>.</summary>
private const int SampleCodeColumn = 25 ;
/ * * * * * * * * *
* * Unit tests
* * * * * * * * * /
/// <summary>Test that no diagnostics are raised for an empty code block.</summary>
[TestCase]
public void EmptyCode_HasNoDiagnostics ( )
{
// arrange
string test = @"" ;
// assert
this . VerifyCSharpDiagnostic ( test ) ;
}
2018-04-10 10:33:45 +08:00
/// <summary>Test that the expected diagnostic message is raised for implicit net field comparisons.</summary>
2018-04-10 07:32:00 +08:00
/// <param name="codeText">The code line to test.</param>
/// <param name="column">The column within the code line where the diagnostic message should be reported.</param>
/// <param name="expression">The expression which should be reported.</param>
/// <param name="fromType">The source type name which should be reported.</param>
/// <param name="toType">The target type name which should be reported.</param>
2018-04-11 06:22:34 +08:00
[TestCase("Item item = null; if (item.netIntField < 42);", 22, "item.netIntField", "NetInt", "int")]
[TestCase("Item item = null; if (item.netIntField <= 42);", 22, "item.netIntField", "NetInt", "int")]
[TestCase("Item item = null; if (item.netIntField > 42);", 22, "item.netIntField", "NetInt", "int")]
[TestCase("Item item = null; if (item.netIntField >= 42);", 22, "item.netIntField", "NetInt", "int")]
[TestCase("Item item = null; if (item.netIntField == 42);", 22, "item.netIntField", "NetInt", "int")]
[TestCase("Item item = null; if (item.netIntField != 42);", 22, "item.netIntField", "NetInt", "int")]
[TestCase("Item item = null; if (item?.netIntField != 42);", 22, "item?.netIntField", "NetInt", "int")]
[TestCase("Item item = null; if (item?.netIntField != null);", 22, "item?.netIntField", "NetInt", "object")]
[TestCase("Item item = null; if (item.netIntProperty < 42);", 22, "item.netIntProperty", "NetInt", "int")]
[TestCase("Item item = null; if (item.netIntProperty <= 42);", 22, "item.netIntProperty", "NetInt", "int")]
[TestCase("Item item = null; if (item.netIntProperty > 42);", 22, "item.netIntProperty", "NetInt", "int")]
[TestCase("Item item = null; if (item.netIntProperty >= 42);", 22, "item.netIntProperty", "NetInt", "int")]
[TestCase("Item item = null; if (item.netIntProperty == 42);", 22, "item.netIntProperty", "NetInt", "int")]
[TestCase("Item item = null; if (item.netIntProperty != 42);", 22, "item.netIntProperty", "NetInt", "int")]
[TestCase("Item item = null; if (item?.netIntProperty != 42);", 22, "item?.netIntProperty", "NetInt", "int")]
[TestCase("Item item = null; if (item?.netIntProperty != null);", 22, "item?.netIntProperty", "NetInt", "object")]
[TestCase("Item item = null; if (item.netRefField == null);", 22, "item.netRefField", "NetRef", "object")]
[TestCase("Item item = null; if (item.netRefField != null);", 22, "item.netRefField", "NetRef", "object")]
[TestCase("Item item = null; if (item.netRefProperty == null);", 22, "item.netRefProperty", "NetRef", "object")]
[TestCase("Item item = null; if (item.netRefProperty != null);", 22, "item.netRefProperty", "NetRef", "object")]
[TestCase("SObject obj = null; if (obj.netIntField != 42);", 24, "obj.netIntField", "NetInt", "int")] // ↓ same as above, but inherited from base class
[TestCase("SObject obj = null; if (obj.netIntProperty != 42);", 24, "obj.netIntProperty", "NetInt", "int")]
[TestCase("SObject obj = null; if (obj.netRefField == null);", 24, "obj.netRefField", "NetRef", "object")]
[TestCase("SObject obj = null; if (obj.netRefField != null);", 24, "obj.netRefField", "NetRef", "object")]
[TestCase("SObject obj = null; if (obj.netRefProperty == null);", 24, "obj.netRefProperty", "NetRef", "object")]
[TestCase("SObject obj = null; if (obj.netRefProperty != null);", 24, "obj.netRefProperty", "NetRef", "object")]
2018-04-15 08:14:31 +08:00
[TestCase("Item item = new Item(); object list = item.netList;", 38, "item.netList", "NetList", "object")] // ↓ NetList field converted to a non-interface type
[TestCase("Item item = new Item(); object list = item.netCollection;", 38, "item.netCollection", "NetCollection", "object")]
2018-04-10 10:33:45 +08:00
public void AvoidImplicitNetFieldComparisons_RaisesDiagnostic ( string codeText , int column , string expression , string fromType , string toType )
2018-04-10 07:32:00 +08:00
{
// arrange
2018-04-11 06:23:57 +08:00
string code = NetFieldAnalyzerTests . SampleProgram . Replace ( "{{test-code}}" , codeText ) ;
2018-04-10 07:32:00 +08:00
DiagnosticResult expected = new DiagnosticResult
{
2018-04-15 05:53:58 +08:00
Id = "AvoidImplicitNetFieldCast" ,
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/avoid-implicit-net-field-cast for details." ,
2018-04-10 07:32:00 +08:00
Severity = DiagnosticSeverity . Warning ,
2018-04-11 06:23:57 +08:00
Locations = new [ ] { new DiagnosticResultLocation ( "Test0.cs" , NetFieldAnalyzerTests . SampleCodeLine , NetFieldAnalyzerTests . SampleCodeColumn + column ) }
2018-04-10 07:32:00 +08:00
} ;
// assert
this . VerifyCSharpDiagnostic ( code , expected ) ;
}
2018-04-15 07:51:50 +08:00
/// <summary>Test that the net field analyzer doesn't raise any warnings for safe member access.</summary>
/// <param name="codeText">The code line to test.</param>
2018-04-15 08:14:31 +08:00
[TestCase("Item item = new Item(); System.Collections.IEnumerable list = farmer.eventsSeen;")]
[TestCase("Item item = new Item(); System.Collections.Generic.IEnumerable<int> list = farmer.netList;")]
[TestCase("Item item = new Item(); System.Collections.Generic.IList<int> list = farmer.netList;")]
[TestCase("Item item = new Item(); System.Collections.Generic.ICollection<int> list = farmer.netCollection;")]
[TestCase("Item item = new Item(); System.Collections.Generic.IList<int> list = farmer.netObjectList;")] // subclass of NetList
2018-04-15 07:51:50 +08:00
public void AvoidImplicitNetFieldComparisons_AllowsSafeAccess ( string codeText )
{
// arrange
string code = NetFieldAnalyzerTests . SampleProgram . Replace ( "{{test-code}}" , codeText ) ;
// assert
this . VerifyCSharpDiagnostic ( code ) ;
}
2018-04-10 10:33:45 +08:00
/// <summary>Test that the expected diagnostic message is raised for avoidable net field references.</summary>
/// <param name="codeText">The code line to test.</param>
/// <param name="column">The column within the code line where the diagnostic message should be reported.</param>
/// <param name="expression">The expression which should be reported.</param>
/// <param name="netType">The net type name which should be reported.</param>
/// <param name="suggestedProperty">The suggested property name which should be reported.</param>
2018-04-11 06:22:34 +08:00
[TestCase("Item item = null; int category = item.category;", 33, "item.category", "NetInt", "Category")]
[TestCase("Item item = null; int category = (item).category;", 33, "(item).category", "NetInt", "Category")]
[TestCase("Item item = null; int category = ((Item)item).category;", 33, "((Item)item).category", "NetInt", "Category")]
[TestCase("SObject obj = null; int category = obj.category;", 35, "obj.category", "NetInt", "Category")]
2018-04-10 10:33:45 +08:00
public void AvoidNetFields_RaisesDiagnostic ( string codeText , int column , string expression , string netType , string suggestedProperty )
{
// arrange
2018-04-11 06:23:57 +08:00
string code = NetFieldAnalyzerTests . SampleProgram . Replace ( "{{test-code}}" , codeText ) ;
2018-04-10 10:33:45 +08:00
DiagnosticResult expected = new DiagnosticResult
{
2018-04-15 05:53:58 +08:00
Id = "AvoidNetField" ,
Message = $"'{expression}' is a {netType} field; consider using the {suggestedProperty} property instead. See https://smapi.io/buildmsg/avoid-net-field for details." ,
2018-04-10 10:33:45 +08:00
Severity = DiagnosticSeverity . Warning ,
2018-04-11 06:23:57 +08:00
Locations = new [ ] { new DiagnosticResultLocation ( "Test0.cs" , NetFieldAnalyzerTests . SampleCodeLine , NetFieldAnalyzerTests . SampleCodeColumn + column ) }
2018-04-10 10:33:45 +08:00
} ;
// assert
this . VerifyCSharpDiagnostic ( code , expected ) ;
}
2018-04-10 07:32:00 +08:00
/ * * * * * * * * *
* * Helpers
* * * * * * * * * /
/// <summary>Get the analyzer being tested.</summary>
protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer ( )
{
2018-04-10 10:33:45 +08:00
return new NetFieldAnalyzer ( ) ;
2018-04-10 07:32:00 +08:00
}
}
}