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