summaryrefslogtreecommitdiff
path: root/src/SMAPI.ModBuildConfig.Analyzer.Tests/UnitTests.cs
blob: 92fc90747bae8f0953a6b64833ccf73f7d29ebd5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
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
{
    /// <summary>Unit tests for the C# analyzers.</summary>
    [TestFixture]
    public class UnitTests : DiagnosticVerifier
    {
        /*********
        ** Properties
        *********/
        /// <summary>Sample C# code which contains a simplified representation of Stardew Valley's <c>Netcode</c> types, and sample mod code with a {{test-code}} placeholder for the code being tested.</summary>
        const string SampleProgram = @"
            using System;
            using StardewValley;
            using Netcode;

            namespace Netcode
            {
                public class NetInt : NetFieldBase<int, NetInt> { }
                public class NetRef : NetFieldBase<object, NetRef> { }
                public class NetFieldBase<T, TSelf> where TSelf : NetFieldBase<T, TSelf>
                {
                    public T Value { get; set; }
                    public static implicit operator T(NetFieldBase<T, TSelf> 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;
                }
            }

            namespace SampleMod
            {
                class ModEntry
                {
                    public void Entry()
                    {
                        Item item = null;

                        // this line should raise diagnostics
                        {{test-code}} // line 36

                        // these lines should not
                        if (item.type.Value != 42);
                    }
                }
            }
        ";

        /// <summary>The line number where the unit tested code is injected into <see cref="SampleProgram"/>.</summary>
        private const int SampleCodeLine = 36;

        /// <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);
        }

        /// <summary>Test that the expected diagnostic message is raised for implicit net field comparisons.</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="fromType">The source type name which should be reported.</param>
        /// <param name="toType">The target type name which should be reported.</param>
        [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")]
        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);
        }

        /// <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>
        [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")]
        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
        *********/
        /// <summary>Get the analyzer being tested.</summary>
        protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
        {
            return new NetFieldAnalyzer();
        }
    }
}