using System; using System.Collections.Generic; using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; namespace StardewModdingAPI.ModBuildConfig.Analyzer { /// Detects references to a field which has been replaced. [DiagnosticAnalyzer(LanguageNames.CSharp)] public class ObsoleteFieldAnalyzer : DiagnosticAnalyzer { /********* ** Fields *********/ /// 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 { ["AvoidObsoleteField"] = new DiagnosticDescriptor( id: "AvoidObsoleteField", title: "Reference to obsolete field", messageFormat: "The '{0}' field is obsolete and should be replaced with '{1}'. See https://smapi.io/buildmsg/avoid-obsolete-field for details.", category: "SMAPI.CommonErrors", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, helpLinkUri: "https://smapi.io/buildmsg/avoid-obsolete-field" ) }; /********* ** 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) { context.RegisterSyntaxNodeAction( this.AnalyzeObsoleteFields, SyntaxKind.SimpleMemberAccessExpression, SyntaxKind.ConditionalAccessExpression ); } /********* ** 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 if (!AnalyzerUtilities.TryGetMemberInfo(context.Node, context.SemanticModel, out ITypeSymbol declaringType, out TypeInfo memberType, out string memberName)) return; // suggest replacement foreach (ITypeSymbol type in AnalyzerUtilities.GetConcreteTypes(declaringType)) { if (this.ReplacedFields.TryGetValue($"{type}::{memberName}", out string replacement)) { context.ReportDiagnostic(Diagnostic.Create(this.Rules["AvoidObsoleteField"], context.Node.GetLocation(), $"{type}.{memberName}", replacement)); break; } } } catch (Exception ex) { throw new InvalidOperationException($"Failed processing expression: '{context.Node}'. Exception details: {ex.ToString().Replace('\r', ' ').Replace('\n', ' ')}"); } } } }