warn for fields which no longer work (#471)
This commit is contained in:
parent
c6c2302baf
commit
13f31e8b72
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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', ' ')}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue