Skip to content

Commit

Permalink
Add MethodArguments analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
peter-r-g committed May 14, 2023
1 parent ed6fe5f commit c43388f
Show file tree
Hide file tree
Showing 6 changed files with 476 additions and 3 deletions.
12 changes: 11 additions & 1 deletion SboxAnalyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
; Unshipped analyzer release
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

## Release 3.0.0

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
SB9009 | Events | Error | Incorrect event listener parameter count.
SB9010 | Events | Error | Incorrect event listener parameter type.
SB9011 | Events | Warning | Listener uses event with no MethodArgumentsAttribute.
64 changes: 64 additions & 0 deletions SboxAnalyzers/Diagnostics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,70 @@ public static class AccessList
internal static readonly ImmutableArray<DiagnosticDescriptor> AllRules = ImmutableArray.Create( Rule );
}

/// <summary>
/// Contains information for analyzers relating to event method arguments.
/// </summary>
public static class MethodArguments
{
/// <summary>
/// An error diagnostic to notify that an event listeners parameter count is incorrect.
/// </summary>
public const string ListenerParameterCountMismatchId = "SB9009";
/// <summary>
/// An error diagnostic to notify that an event listeners parameter type is incorrect.
/// </summary>
public const string ListenerParameterTypeMismatchId = "SB9010";
/// <summary>
/// A warning diagnostic to notify that an event listener has parameters but the event has no MethodArgumentsAttribute to compare against.
/// </summary>
public const string ListenerMethodArgumentsUndefinedId = "SB9011";
/// <summary>
/// The category that this diagnostic falls under.
/// </summary>
private const string Category = "Events";

private static readonly LocalizableString ListenerParameterCountMismatchTitle = new LocalizableResourceString( nameof( MethodArgumentsResources.ListenerParameterCountMismatchTitle ), MethodArgumentsResources.ResourceManager, typeof( MethodArgumentsResources ) );
private static readonly LocalizableString ListenerParameterCountMismatchMessageFormat = new LocalizableResourceString( nameof( MethodArgumentsResources.ListenerParameterCountMismatchMessageFormat ), MethodArgumentsResources.ResourceManager, typeof( MethodArgumentsResources ) );
private static readonly LocalizableString ListenerParameterCountMismatchDescription = new LocalizableResourceString( nameof( MethodArgumentsResources.ListenerParameterCountMismatchDescription ), MethodArgumentsResources.ResourceManager, typeof( MethodArgumentsResources ) );
public static readonly DiagnosticDescriptor ListenerParameterCountMismatchRule = new(
ListenerParameterCountMismatchId,
ListenerParameterCountMismatchTitle,
ListenerParameterCountMismatchMessageFormat,
Category,
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: ListenerParameterCountMismatchDescription );

private static readonly LocalizableString ListenerParameterTypeMismatchTitle = new LocalizableResourceString( nameof( MethodArgumentsResources.ListenerParameterTypeMismatchTitle ), MethodArgumentsResources.ResourceManager, typeof( MethodArgumentsResources ) );
private static readonly LocalizableString ListenerParameterTypeMismatchMessageFormat = new LocalizableResourceString( nameof( MethodArgumentsResources.ListenerParameterTypeMismatchMessageFormat ), MethodArgumentsResources.ResourceManager, typeof( MethodArgumentsResources ) );
private static readonly LocalizableString ListenerParameterTypeMismatchDescription = new LocalizableResourceString( nameof( MethodArgumentsResources.ListenerParameterTypeMismatchDescription ), MethodArgumentsResources.ResourceManager, typeof( MethodArgumentsResources ) );
public static readonly DiagnosticDescriptor ListenerParameterTypeMismatchRule = new(
ListenerParameterTypeMismatchId,
ListenerParameterTypeMismatchTitle,
ListenerParameterTypeMismatchMessageFormat,
Category,
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: ListenerParameterTypeMismatchDescription );

private static readonly LocalizableString ListenerMethodArgumentsUndefinedTitle = new LocalizableResourceString( nameof( MethodArgumentsResources.ListenerMethodArgumentsUndefinedTitle ), MethodArgumentsResources.ResourceManager, typeof( MethodArgumentsResources ) );
private static readonly LocalizableString ListenerMethodArgumentsUndefinedMessageFormat = new LocalizableResourceString( nameof( MethodArgumentsResources.ListenerMethodArgumentsUndefinedMessageFormat ), MethodArgumentsResources.ResourceManager, typeof( MethodArgumentsResources ) );
private static readonly LocalizableString ListenerMethodArgumentsUndefinedDescription = new LocalizableResourceString( nameof( MethodArgumentsResources.ListenerMethodArgumentsUndefinedDescription ), MethodArgumentsResources.ResourceManager, typeof( MethodArgumentsResources ) );
public static readonly DiagnosticDescriptor ListenerMethodArgumentsUndefinedRule = new(
ListenerMethodArgumentsUndefinedId,
ListenerMethodArgumentsUndefinedTitle,
ListenerMethodArgumentsUndefinedMessageFormat,
Category,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: ListenerMethodArgumentsUndefinedDescription );

internal static readonly ImmutableArray<DiagnosticDescriptor> AllRules = ImmutableArray.Create(
ListenerParameterCountMismatchRule,
ListenerParameterTypeMismatchRule,
ListenerMethodArgumentsUndefinedRule );
}

/// <summary>
/// Contains information for analyzers relating to networked properties.
/// </summary>
Expand Down
97 changes: 97 additions & 0 deletions SboxAnalyzers/MethodArgumentsAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using SboxAnalyzers.Extensions;
using System.Collections.Immutable;
using System.Linq;

namespace SboxAnalyzers;

/// <summary>
/// A Roslyn analyzer for checking event method arguments.
/// </summary>
[DiagnosticAnalyzer( LanguageNames.CSharp )]
public class MethodArgumentsAnalyzer : DiagnosticAnalyzer
{
/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => Diagnostics.MethodArguments.AllRules;

/// <inheritdoc/>
public override void Initialize( AnalysisContext context )
{
context.ConfigureGeneratedCodeAnalysis( GeneratedCodeAnalysisFlags.None );
context.EnableConcurrentExecution();

context.RegisterSyntaxNodeAction( AnalyzeMethodDeclaration, SyntaxKind.MethodDeclaration );
}

/// <summary>
/// Analyzes a <see cref="MethodDeclarationSyntax"/> to check if it is a correctly implemented event listener.
/// </summary>
/// <param name="context">The context relating to the syntax node being analyzed.</param>
private static void AnalyzeMethodDeclaration( SyntaxNodeAnalysisContext context )
{
var methodDeclaration = (MethodDeclarationSyntax)context.Node;
var methodParameters = methodDeclaration.ParameterList?.Parameters;

// Get all event attributes on the method.
foreach ( var eventAttribute in methodDeclaration.GetAttributesOfType( "Event", context.SemanticModel ) )
{
var eventTypeSymbol = context.SemanticModel.GetSymbolInfo( eventAttribute ).Symbol!.ContainingType;
var methodArgumentsAttribute = eventTypeSymbol.GetAttributes()
.FirstOrDefault( a => a.AttributeClass?.Name == "MethodArgumentsAttribute" );

if ( methodArgumentsAttribute is null )
{
// Warn if no method arguments attribute on event and containing one or more parameters.
if ( methodParameters is not null && methodParameters.Value.Count > 0 )
{
var diagnostic = Diagnostic.Create( Diagnostics.MethodArguments.ListenerMethodArgumentsUndefinedRule,
methodDeclaration.ParameterList!.GetLocation() );
context.ReportDiagnostic( diagnostic );
}

continue;
}

var typeArguments = methodArgumentsAttribute.ConstructorArguments[0].Values;

if ( methodParameters is null || typeArguments.Length != methodParameters.Value.Count )
{
// Error if method parameter count does not match method arguments type count.
if ( typeArguments.Length != (methodParameters?.Count ?? 0) )
{
var diagnostic = Diagnostic.Create( Diagnostics.MethodArguments.ListenerParameterCountMismatchRule,
methodDeclaration.Identifier.GetLocation(),
typeArguments.Length );
context.ReportDiagnostic( diagnostic );
}

continue;
}

// Check each parameter for type match.
for ( var i = 0; i < typeArguments.Length; i++ )
{
var argument = typeArguments[i];
if ( argument.Value is not ITypeSymbol argumentSymbol )
continue;

var parameter = methodParameters.Value[i];
if ( parameter.Type is null )
continue;

var parameterSymbol = context.SemanticModel.GetSymbolInfo( parameter ).Symbol;
if ( SymbolEqualityComparer.Default.Equals( parameterSymbol, argumentSymbol ) )
continue;

// Error when method parameter type does not match.
var diagnostic = Diagnostic.Create( Diagnostics.MethodArguments.ListenerParameterTypeMismatchRule,
parameter.Type.GetLocation(),
argumentSymbol.ToNameString( true ) );
context.ReportDiagnostic( diagnostic );
}
}
}
}
144 changes: 144 additions & 0 deletions SboxAnalyzers/MethodArgumentsResources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit c43388f

Please sign in to comment.