Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/Rules/MA0015.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,22 @@ void Sample(string str)
if (str == "")
throw new ArgumentException("Error message", paramName: nameof(str)); // ok
}

class Sample
{
void Test(string test)
{
ArgumentNullException.ThrowIfNull(Name); // non-compliant: 'Name' is not a parameter
}

public static string Name { get; }
}

class Sample
{
void Test(string test)
{
ArgumentNullException.ThrowIfNull(test); // ok
}
}
````
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Collections.Immutable;
using System.Collections.Immutable;
using Meziantou.Analyzer.Internals;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand Down Expand Up @@ -37,7 +37,18 @@ public override void Initialize(AnalysisContext context)
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

context.RegisterOperationAction(Analyze, OperationKind.ObjectCreation);
context.RegisterCompilationStartAction(context =>
{
var argumentExceptionType = context.Compilation.GetBestTypeByMetadataName("System.ArgumentException");
var argumentNullExceptionType = context.Compilation.GetBestTypeByMetadataName("System.ArgumentNullException");
var callerArgumentExpressionAttribute = context.Compilation.GetBestTypeByMetadataName("System.Runtime.CompilerServices.CallerArgumentExpressionAttribute");

if (argumentExceptionType is null || argumentNullExceptionType is null)
return;

context.RegisterOperationAction(Analyze, OperationKind.ObjectCreation);
context.RegisterOperationAction(ctx => AnalyzeInvocation(ctx, argumentExceptionType, argumentNullExceptionType, callerArgumentExpressionAttribute), OperationKind.Invocation);
});
}

private static void Analyze(OperationAnalysisContext context)
Expand Down Expand Up @@ -112,6 +123,112 @@ private static void Analyze(OperationAnalysisContext context)
}
}

private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTypeSymbol argumentExceptionType, INamedTypeSymbol argumentNullExceptionType, INamedTypeSymbol? callerArgumentExpressionAttribute)
{
var op = (IInvocationOperation)context.Operation;
if (op is null)
return;

var method = op.TargetMethod;
if (method is null || !method.IsStatic)
return;

// Check if this is a ThrowIfXxx method on ArgumentException or ArgumentNullException
var containingType = method.ContainingType;
if (containingType is null)
return;

if (!containingType.IsEqualTo(argumentExceptionType) && !containingType.IsEqualTo(argumentNullExceptionType))
return;

// Check if the method name starts with "ThrowIf"
if (!method.Name.StartsWith("ThrowIf", StringComparison.Ordinal))
return;

// The first parameter is the argument being validated
if (op.Arguments.Length == 0)
return;

// Find the parameter with CallerArgumentExpressionAttribute
if (callerArgumentExpressionAttribute is not null)
{
foreach (var parameter in method.Parameters)
{
if (!parameter.Type.IsString())
continue;

foreach (var attribute in parameter.GetAttributes())
{
if (attribute.AttributeClass is null || !attribute.AttributeClass.IsEqualTo(callerArgumentExpressionAttribute))
continue;

if (attribute.ConstructorArguments.Length == 0)
continue;

// Find the argument for this parameter
var paramNameArgument = op.Arguments.FirstOrDefault(arg => arg.Parameter is not null && arg.Parameter.IsEqualTo(parameter));
if (paramNameArgument is not null && paramNameArgument.Value is not null)
{
ValidateParamNameArgument(context, op, paramNameArgument);
return;
}
}
}
}

ValidateFirstArgument(context, op);
}

private static void ValidateParamNameArgument(OperationAnalysisContext context, IInvocationOperation op, IArgumentOperation paramNameArgument)
{
// Check if the argument is a constant string value
if (!paramNameArgument.Value.ConstantValue.HasValue || paramNameArgument.Value.ConstantValue.Value is not string paramNameValue)
return;

var availableParameterNames = GetParameterNames(op, context.CancellationToken);
if (availableParameterNames.Contains(paramNameValue, StringComparer.Ordinal))
{
if (paramNameArgument.Value is not INameOfOperation)
{
var properties = ImmutableDictionary<string, string?>.Empty.Add(ArgumentExceptionShouldSpecifyArgumentNameAnalyzerCommon.ArgumentNameKey, paramNameValue);
context.ReportDiagnostic(NameofRule, properties, paramNameArgument.Value);
}

return;
}

context.ReportDiagnostic(Rule, paramNameArgument, $"'{paramNameValue}' is not a valid parameter name");
}

private static void ValidateFirstArgument(OperationAnalysisContext context, IInvocationOperation op)
{
var firstArgument = op.Arguments[0];
if (firstArgument.Value is null)
return;

// Check if the argument is a parameter reference or a member access to a property/field
string? argumentName = null;

if (firstArgument.Value is IParameterReferenceOperation parameterRef)
{
argumentName = parameterRef.Parameter.Name;
}
else if (firstArgument.Value is IMemberReferenceOperation memberRef)
{
argumentName = memberRef.Member.Name;
}

if (argumentName is null)
return;

// Check if this argument name corresponds to a valid parameter in the current scope
var availableParameterNames = GetParameterNames(op, context.CancellationToken);
if (!availableParameterNames.Contains(argumentName, StringComparer.Ordinal))
{
context.ReportDiagnostic(Rule, firstArgument, $"'{argumentName}' is not a valid parameter name");
}
}

private static IEnumerable<string> GetParameterNames(IOperation operation, CancellationToken cancellationToken)
{
var symbols = operation.LookupAvailableSymbols(cancellationToken);
Expand Down
Loading