using System; 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 implicit conversion from Stardew Valley's Netcode types. These have very unintuitive implicit conversion rules, so mod authors should always explicitly convert the type with appropriate null checks. [DiagnosticAnalyzer(LanguageNames.CSharp)] public class ImplicitNetFieldCastAnalyzer : DiagnosticAnalyzer { /********* ** Properties *********/ /// The namespace for Stardew Valley's Netcode types. private const string NetcodeNamespace = "Netcode"; /// Describes the diagnostic rule covered by the analyzer. private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( id: "SMAPI001", title: "Netcode types shouldn't be implicitly converted", messageFormat: "This implicitly converts '{0}' from {1} to {2}, but {1} has unintuitive implicit conversion rules. Consider comparing against the actual value instead to avoid bugs. See https://smapi.io/buildmsg/SMAPI001 for details.", category: "SMAPI.CommonErrors", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "", helpLinkUri: "https://smapi.io/buildmsg/SMAPI001" ); /********* ** Accessors *********/ /// The descriptors for the diagnostics that this analyzer is capable of producing. public override ImmutableArray SupportedDiagnostics { get; } /********* ** Public methods *********/ /// Construct an instance. public ImplicitNetFieldCastAnalyzer() { this.SupportedDiagnostics = ImmutableArray.Create(ImplicitNetFieldCastAnalyzer.Rule); } /// Called once at session start to register actions in the analysis context. /// The analysis context. public override void Initialize(AnalysisContext context) { context.RegisterSyntaxNodeAction( this.Analyse, SyntaxKind.EqualsExpression, SyntaxKind.NotEqualsExpression, SyntaxKind.GreaterThanExpression, SyntaxKind.GreaterThanOrEqualExpression, SyntaxKind.LessThanExpression, SyntaxKind.LessThanOrEqualExpression ); } /********* ** Private methods *********/ /// Analyse a syntax node and add a diagnostic message if applicable. /// The analysis context. private void Analyse(SyntaxNodeAnalysisContext context) { try { BinaryExpressionSyntax node = (BinaryExpressionSyntax)context.Node; bool leftHasWarning = this.Analyze(context, node.Left); if (!leftHasWarning) this.Analyze(context, node.Right); } catch (Exception ex) { throw new InvalidOperationException($"Failed processing expression: '{context.Node}'. Exception details: {ex.ToString().Replace('\r', ' ').Replace('\n', ' ')}"); } } /// Analyse one operand in a binary expression (like a and b in a == b) and add a diagnostic message if applicable. /// The analysis context. /// The operand expression. /// Returns whether a diagnostic message was raised. private bool Analyze(SyntaxNodeAnalysisContext context, ExpressionSyntax operand) { const string netcodeNamespace = ImplicitNetFieldCastAnalyzer.NetcodeNamespace; TypeInfo operandType = context.SemanticModel.GetTypeInfo(operand); string fromNamespace = operandType.Type?.ContainingNamespace?.Name; string toNamespace = operandType.ConvertedType?.ContainingNamespace?.Name; if (fromNamespace == netcodeNamespace && fromNamespace != toNamespace && toNamespace != null) { string fromTypeName = operandType.Type.Name; string toTypeName = operandType.ConvertedType.Name; context.ReportDiagnostic(Diagnostic.Create(ImplicitNetFieldCastAnalyzer.Rule, context.Node.GetLocation(), operand, fromTypeName, toTypeName)); return true; } return false; } } }