summaryrefslogtreecommitdiff
path: root/src/SMAPI.ModBuildConfig.Analyzer/ObsoleteFieldAnalyzer.cs
blob: 158d724342544e616d1b2968cdd7ca08d1a27d48 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#nullable disable

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
{
    /// <summary>Detects references to a field which has been replaced.</summary>
    [DiagnosticAnalyzer(LanguageNames.CSharp)]
    public class ObsoleteFieldAnalyzer : DiagnosticAnalyzer
    {
        /*********
        ** Fields
        *********/
        /// <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>
        {
            ["AvoidObsoleteField"] = new(
                id: "AvoidObsoleteField",
                title: "Reference to obsolete field",
                messageFormat: "The '{0}' field is obsolete and should be replaced with '{1}'. See https://smapi.io/package/avoid-obsolete-field for details.",
                category: "SMAPI.CommonErrors",
                defaultSeverity: DiagnosticSeverity.Warning,
                isEnabledByDefault: true,
                helpLinkUri: "https://smapi.io/package/avoid-obsolete-field"
            )
        };


        /*********
        ** 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)
        {
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
            context.EnableConcurrentExecution();

            context.RegisterSyntaxNodeAction(
                this.AnalyzeObsoleteFields,
                SyntaxKind.SimpleMemberAccessExpression,
                SyntaxKind.ConditionalAccessExpression
            );
        }


        /*********
        ** Private methods
        *********/
        /// <summary>Analyze 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
                if (!AnalyzerUtilities.TryGetMemberInfo(context.Node, context.SemanticModel, out ITypeSymbol declaringType, out _, 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', ' ')}");
            }
        }
    }
}