summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/mod-build-config.md6
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Farmer.cs11
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs (renamed from src/SMAPI.ModBuildConfig.Analyzer.Tests/UnitTests.cs)14
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer.Tests/ObsoleteFieldAnalyzerTests.cs88
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs98
5 files changed, 210 insertions, 7 deletions
diff --git a/docs/mod-build-config.md b/docs/mod-build-config.md
index 00f9a356..99a567f2 100644
--- a/docs/mod-build-config.md
+++ b/docs/mod-build-config.md
@@ -194,6 +194,12 @@ field has an equivalent non-net property that avoids those issues.
Suggested fix: access the suggested property name instead.
+### SMAPI003
+**Avoid obsolete fields:**
+> The '{{old field}}' field is obsolete and should be replaced with '{{new field}}'.
+
+Your code accesses a field which is obsolete or no longer works. Use the suggested field instead.
+
## Troubleshoot
### "Failed to find the game install path"
That error means the package couldn't find your game. You can specify the game path yourself; see
diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Farmer.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Farmer.cs
new file mode 100644
index 00000000..e0f0e30c
--- /dev/null
+++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/Mock/StardewValley/Farmer.cs
@@ -0,0 +1,11 @@
+// ReSharper disable CheckNamespace, InconsistentNaming -- matches Stardew Valley's code
+using System.Collections.Generic;
+
+namespace StardewValley
+{
+ /// <summary>A simplified version of Stardew Valley's <c>StardewValley.Farmer</c> class for unit testing.</summary>
+ internal class Farmer
+ {
+ public IDictionary<string, int[]> friendships;
+ }
+}
diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/UnitTests.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs
index 8ca27847..101f4c21 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/UnitTests.cs
+++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/NetFieldAnalyzerTests.cs
@@ -6,14 +6,14 @@ using StardewModdingAPI.ModBuildConfig.Analyzer;
namespace SMAPI.ModBuildConfig.Analyzer.Tests
{
- /// <summary>Unit tests for the C# analyzers.</summary>
+ /// <summary>Unit tests for <see cref="NetFieldAnalyzer"/>.</summary>
[TestFixture]
- public class UnitTests : DiagnosticVerifier
+ public class NetFieldAnalyzerTests : 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>
+ /// <summary>Sample C# mod code, with a {{test-code}} placeholder for the code in the Entry method to test.</summary>
const string SampleProgram = @"
using System;
using StardewValley;
@@ -88,13 +88,13 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests
public void AvoidImplicitNetFieldComparisons_RaisesDiagnostic(string codeText, int column, string expression, string fromType, string toType)
{
// arrange
- string code = UnitTests.SampleProgram.Replace("{{test-code}}", codeText);
+ string code = NetFieldAnalyzerTests.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) }
+ Locations = new[] { new DiagnosticResultLocation("Test0.cs", NetFieldAnalyzerTests.SampleCodeLine, NetFieldAnalyzerTests.SampleCodeColumn + column) }
};
// assert
@@ -114,13 +114,13 @@ namespace SMAPI.ModBuildConfig.Analyzer.Tests
public void AvoidNetFields_RaisesDiagnostic(string codeText, int column, string expression, string netType, string suggestedProperty)
{
// arrange
- string code = UnitTests.SampleProgram.Replace("{{test-code}}", codeText);
+ string code = NetFieldAnalyzerTests.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) }
+ Locations = new[] { new DiagnosticResultLocation("Test0.cs", NetFieldAnalyzerTests.SampleCodeLine, NetFieldAnalyzerTests.SampleCodeColumn + column) }
};
// assert
diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/ObsoleteFieldAnalyzerTests.cs b/src/SMAPI.ModBuildConfig.Analyzer.Tests/ObsoleteFieldAnalyzerTests.cs
new file mode 100644
index 00000000..dc7476ef
--- /dev/null
+++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/ObsoleteFieldAnalyzerTests.cs
@@ -0,0 +1,88 @@
+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 <see cref="ObsoleteFieldAnalyzer"/>.</summary>
+ [TestFixture]
+ public class ObsoleteFieldAnalyzerTests : DiagnosticVerifier
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>Sample C# mod code, with a {{test-code}} placeholder for the code in the Entry method to test.</summary>
+ const string SampleProgram = @"
+ using System;
+ using StardewValley;
+ using Netcode;
+ using SObject = StardewValley.Object;
+
+ namespace SampleMod
+ {
+ class ModEntry
+ {
+ public void Entry()
+ {
+ {{test-code}}
+ }
+ }
+ }
+ ";
+
+ /// <summary>The line number where the unit tested code is injected into <see cref="SampleProgram"/>.</summary>
+ private const int SampleCodeLine = 13;
+
+ /// <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 an obsolete field reference.</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="oldName">The old field name which should be reported.</param>
+ /// <param name="newName">The new field name which should be reported.</param>
+ [TestCase("var x = new Farmer().friendships;", 8, "StardewValley.Farmer.friendships", "friendshipData")]
+ public void AvoidObsoleteField_RaisesDiagnostic(string codeText, int column, string oldName, string newName)
+ {
+ // arrange
+ string code = ObsoleteFieldAnalyzerTests.SampleProgram.Replace("{{test-code}}", codeText);
+ DiagnosticResult expected = new DiagnosticResult
+ {
+ Id = "SMAPI003",
+ Message = $"The '{oldName}' field is obsolete and should be replaced with '{newName}'. See https://smapi.io/buildmsg/smapi003 for details.",
+ Severity = DiagnosticSeverity.Warning,
+ Locations = new[] { new DiagnosticResultLocation("Test0.cs", ObsoleteFieldAnalyzerTests.SampleCodeLine, ObsoleteFieldAnalyzerTests.SampleCodeColumn + column) }
+ };
+
+ // assert
+ this.VerifyCSharpDiagnostic(code, expected);
+ }
+
+
+ /*********
+ ** Helpers
+ *********/
+ /// <summary>Get the analyzer being tested.</summary>
+ protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
+ {
+ return new ObsoleteFieldAnalyzer();
+ }
+ }
+}
diff --git a/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs
new file mode 100644
index 00000000..00565329
--- /dev/null
+++ b/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace StardewModdingAPI.ModBuildConfig.Analyzer
+{
+ /// <summary>Detects references to a field which has been replaced.</summary>
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public class ObsoleteFieldAnalyzer : DiagnosticAnalyzer
+ {
+ /*********
+ ** Properties
+ *********/
+ /// <summary>Maps obsolete fields/properties to their non-obsolete equivalent.</summary>
+ private readonly IDictionary<string, string> ReplacedFields = new Dictionary<string, string>
+ {
+ // Farmer
+ ["StardewValley.Farmer::friendships"] = "friendshipData"
+ };
+
+ /// <summary>Describes the diagnostic rule covered by the analyzer.</summary>
+ private readonly IDictionary<string, DiagnosticDescriptor> Rules = new Dictionary<string, DiagnosticDescriptor>
+ {
+ ["SMAPI003"] = new DiagnosticDescriptor(
+ id: "SMAPI003",
+ title: "Reference to obsolete field",
+ messageFormat: "The '{0}' field is obsolete and should be replaced with '{1}'. See https://smapi.io/buildmsg/smapi003 for details.",
+ category: "SMAPI.CommonErrors",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ helpLinkUri: "https://smapi.io/buildmsg/smapi003"
+ )
+ };
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The descriptors for the diagnostics that this analyzer is capable of producing.</summary>
+ public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ public ObsoleteFieldAnalyzer()
+ {
+ this.SupportedDiagnostics = ImmutableArray.CreateRange(this.Rules.Values);
+ }
+
+ /// <summary>Called once at session start to register actions in the analysis context.</summary>
+ /// <param name="context">The analysis context.</param>
+ public override void Initialize(AnalysisContext context)
+ {
+ // SMAPI003: avoid obsolete fields
+ context.RegisterSyntaxNodeAction(
+ this.AnalyzeObsoleteFields,
+ SyntaxKind.SimpleMemberAccessExpression
+ );
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Analyse a syntax node and add a diagnostic message if it references an obsolete field.</summary>
+ /// <param name="context">The analysis context.</param>
+ private void AnalyzeObsoleteFields(SyntaxNodeAnalysisContext context)
+ {
+ try
+ {
+ // get reference info
+ MemberAccessExpressionSyntax node = (MemberAccessExpressionSyntax)context.Node;
+ ITypeSymbol declaringType = context.SemanticModel.GetTypeInfo(node.Expression).Type;
+ string propertyName = node.Name.Identifier.Text;
+
+ // suggest replacement
+ for (ITypeSymbol type = declaringType; type != null; type = type.BaseType)
+ {
+ if (this.ReplacedFields.TryGetValue($"{type}::{propertyName}", out string replacement))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(this.Rules["SMAPI003"], context.Node.GetLocation(), $"{type}.{propertyName}", replacement));
+ break;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException($"Failed processing expression: '{context.Node}'. Exception details: {ex.ToString().Replace('\r', ' ').Replace('\n', ' ')}");
+ }
+ }
+ }
+}