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 { /// Detects references to a field which has been replaced. [DiagnosticAnalyzer(LanguageNames.CSharp)] public class ObsoleteFieldAnalyzer : DiagnosticAnalyzer { /********* ** Properties *********/ /// Maps obsolete fields/properties to their non-obsolete equivalent. private readonly IDictionary ReplacedFields = new Dictionary { // Farmer ["StardewValley.Farmer::friendships"] = "friendshipData" }; /// Describes the diagnostic rule covered by the analyzer. private readonly IDictionary Rules = new Dictionary { ["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 *********/ /// The descriptors for the diagnostics that this analyzer is capable of producing. public override ImmutableArray SupportedDiagnostics { get; } /********* ** Public methods *********/ /// Construct an instance. public ObsoleteFieldAnalyzer() { this.SupportedDiagnostics = ImmutableArray.CreateRange(this.Rules.Values); } /// Called once at session start to register actions in the analysis context. /// The analysis context. public override void Initialize(AnalysisContext context) { // SMAPI003: avoid obsolete fields context.RegisterSyntaxNodeAction( this.AnalyzeObsoleteFields, SyntaxKind.SimpleMemberAccessExpression ); } /********* ** Private methods *********/ /// Analyse a syntax node and add a diagnostic message if it references an obsolete field. /// The analysis context. 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', ' ')}"); } } } }