From f52f7ca36f2ecf3d4478c5bc1e9cd95e5ff53929 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 9 Apr 2018 19:32:00 -0400 Subject: add mod code analyzers to detect implicit net field conversion issues (#471) --- .../ImplicitNetFieldCastAnalyzer.cs | 107 +++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 src/SMAPI.ModBuildConfig.Analyzer/ImplicitNetFieldCastAnalyzer.cs (limited to 'src/SMAPI.ModBuildConfig.Analyzer/ImplicitNetFieldCastAnalyzer.cs') diff --git a/src/SMAPI.ModBuildConfig.Analyzer/ImplicitNetFieldCastAnalyzer.cs b/src/SMAPI.ModBuildConfig.Analyzer/ImplicitNetFieldCastAnalyzer.cs new file mode 100644 index 00000000..d23bdc2e --- /dev/null +++ b/src/SMAPI.ModBuildConfig.Analyzer/ImplicitNetFieldCastAnalyzer.cs @@ -0,0 +1,107 @@ +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; + } + } +} -- cgit