warn for fields which no longer work (#471)

This commit is contained in:
Jesse Plamondon-Willard 2018-04-10 18:23:57 -04:00
parent c6c2302baf
commit 13f31e8b72
5 changed files with 210 additions and 7 deletions

View File

@ -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

View File

@ -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;
}
}

View File

@ -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

View File

@ -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();
}
}
}

View File

@ -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', ' ')}");
}
}
}
}