From 9ff927d52eecf65759d9ef116f8b9aee70f12979 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 01:11:31 +0000 Subject: [PATCH 01/16] Initial plan From c5c53927b4dcd5beb42f485c7337a242ee2e4f27 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 01:17:40 +0000 Subject: [PATCH 02/16] Add support for detecting ArgumentException static Throw methods in MA0015 Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com> --- ...eptionShouldSpecifyArgumentNameAnalyzer.cs | 60 +++++ ...nShouldSpecifyArgumentNameAnalyzerTests.cs | 205 ++++++++++++++++++ 2 files changed, 265 insertions(+) diff --git a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs index ce48df78..5a4bea46 100644 --- a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs @@ -38,6 +38,7 @@ public override void Initialize(AnalysisContext context) context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.RegisterOperationAction(Analyze, OperationKind.ObjectCreation); + context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation); } private static void Analyze(OperationAnalysisContext context) @@ -112,6 +113,65 @@ private static void Analyze(OperationAnalysisContext context) } } + private static void AnalyzeInvocation(OperationAnalysisContext context) + { + 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; + + var argumentExceptionType = context.Compilation.GetBestTypeByMetadataName("System.ArgumentException"); + var argumentNullExceptionType = context.Compilation.GetBestTypeByMetadataName("System.ArgumentNullException"); + + if (argumentExceptionType is null || argumentNullExceptionType 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; + + 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 parameterNames = GetParameterNames(op, context.CancellationToken); + if (!parameterNames.Contains(argumentName, StringComparer.Ordinal)) + { + context.ReportDiagnostic(Rule, firstArgument, $"'{argumentName}' is not a valid parameter name"); + } + } + private static IEnumerable GetParameterNames(IOperation operation, CancellationToken cancellationToken) { var symbols = operation.LookupAvailableSymbols(cancellationToken); diff --git a/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs index a8be3f61..6553275a 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs @@ -450,4 +450,209 @@ await CreateProjectBuilder() .ValidateAsync(); } #endif + + [Fact] + public async Task ThrowIfNull_ValidParameter_ShouldNotReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(string test) + { + ArgumentNullException.ThrowIfNull(test); + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task ThrowIfNull_InvalidParameter_ShouldReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(string test) + { + ArgumentNullException.ThrowIfNull([|Name|]); + } + + public static string Name { get; } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ShouldReportDiagnosticWithMessage("'Name' is not a valid parameter name") + .ValidateAsync(); + } + + [Fact] + public async Task ThrowIfNullOrEmpty_ValidParameter_ShouldNotReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(string test) + { + ArgumentNullException.ThrowIfNullOrEmpty(test); + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task ThrowIfNullOrEmpty_InvalidParameter_ShouldReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(string test) + { + ArgumentNullException.ThrowIfNullOrEmpty([|Name|]); + } + + public static string Name { get; } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ShouldReportDiagnosticWithMessage("'Name' is not a valid parameter name") + .ValidateAsync(); + } + + [Fact] + public async Task ThrowIfNullOrWhiteSpace_ValidParameter_ShouldNotReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(string test) + { + ArgumentNullException.ThrowIfNullOrWhiteSpace(test); + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task ThrowIfNullOrWhiteSpace_InvalidParameter_ShouldReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(string test) + { + ArgumentNullException.ThrowIfNullOrWhiteSpace([|Name|]); + } + + public static string Name { get; } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ShouldReportDiagnosticWithMessage("'Name' is not a valid parameter name") + .ValidateAsync(); + } + + [Fact] + public async Task ArgumentException_ThrowIfNullOrEmpty_ValidParameter_ShouldNotReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(string test) + { + ArgumentException.ThrowIfNullOrEmpty(test); + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task ArgumentException_ThrowIfNullOrEmpty_InvalidParameter_ShouldReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(string test) + { + ArgumentException.ThrowIfNullOrEmpty([|Name|]); + } + + public static string Name { get; } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ShouldReportDiagnosticWithMessage("'Name' is not a valid parameter name") + .ValidateAsync(); + } + + [Fact] + public async Task ArgumentException_ThrowIfNullOrWhiteSpace_ValidParameter_ShouldNotReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(string test) + { + ArgumentException.ThrowIfNullOrWhiteSpace(test); + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task ArgumentException_ThrowIfNullOrWhiteSpace_InvalidParameter_ShouldReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(string test) + { + ArgumentException.ThrowIfNullOrWhiteSpace([|Name|]); + } + + public static string Name { get; } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ShouldReportDiagnosticWithMessage("'Name' is not a valid parameter name") + .ValidateAsync(); + } } From 039aa9337486b931a723d3946e36df3330499a93 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 01:19:13 +0000 Subject: [PATCH 03/16] Update MA0015 documentation with ThrowIf method examples Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com> --- docs/Rules/MA0015.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/Rules/MA0015.md b/docs/Rules/MA0015.md index a94b5233..1cbc5822 100644 --- a/docs/Rules/MA0015.md +++ b/docs/Rules/MA0015.md @@ -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 + } +} ```` From dac42cfdafa0bc47f0276f9454c7206aaa0020d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 01:23:25 +0000 Subject: [PATCH 04/16] Fix code formatting in ArgumentExceptionShouldSpecifyArgumentNameAnalyzer Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com> --- .../ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs index 5a4bea46..5c08151e 100644 --- a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs @@ -1,4 +1,4 @@ -using System.Collections.Immutable; +using System.Collections.Immutable; using Meziantou.Analyzer.Internals; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -130,7 +130,7 @@ private static void AnalyzeInvocation(OperationAnalysisContext context) var argumentExceptionType = context.Compilation.GetBestTypeByMetadataName("System.ArgumentException"); var argumentNullExceptionType = context.Compilation.GetBestTypeByMetadataName("System.ArgumentNullException"); - + if (argumentExceptionType is null || argumentNullExceptionType is null) return; @@ -151,7 +151,7 @@ private static void AnalyzeInvocation(OperationAnalysisContext context) // 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; From b222350563413215ab518c236f223541c18156d2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 01:31:04 +0000 Subject: [PATCH 05/16] Fix file encoding in test file Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com> --- .../ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs index 6553275a..24c01455 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs @@ -1,4 +1,4 @@ -using Meziantou.Analyzer.Rules; +using Meziantou.Analyzer.Rules; using TestHelper; namespace Meziantou.Analyzer.Test.Rules; From d911bd21731e80affbdd8d915c35ac72fbdff0fe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 02:09:48 +0000 Subject: [PATCH 06/16] Use CompilationStart and validate paramName parameter in ThrowIf methods Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com> --- ...eptionShouldSpecifyArgumentNameAnalyzer.cs | 55 ++++++++--- ...nShouldSpecifyArgumentNameAnalyzerTests.cs | 99 +++++++++++++++++++ 2 files changed, 143 insertions(+), 11 deletions(-) diff --git a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs index 5c08151e..d0161b5f 100644 --- a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs @@ -37,8 +37,17 @@ public override void Initialize(AnalysisContext context) context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterOperationAction(Analyze, OperationKind.ObjectCreation); - context.RegisterOperationAction(AnalyzeInvocation, OperationKind.Invocation); + context.RegisterCompilationStartAction(context => + { + var argumentExceptionType = context.Compilation.GetBestTypeByMetadataName("System.ArgumentException"); + var argumentNullExceptionType = context.Compilation.GetBestTypeByMetadataName("System.ArgumentNullException"); + + if (argumentExceptionType is null || argumentNullExceptionType is null) + return; + + context.RegisterOperationAction(Analyze, OperationKind.ObjectCreation); + context.RegisterOperationAction(ctx => AnalyzeInvocation(ctx, argumentExceptionType, argumentNullExceptionType), OperationKind.Invocation); + }); } private static void Analyze(OperationAnalysisContext context) @@ -113,7 +122,7 @@ private static void Analyze(OperationAnalysisContext context) } } - private static void AnalyzeInvocation(OperationAnalysisContext context) + private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTypeSymbol argumentExceptionType, INamedTypeSymbol argumentNullExceptionType) { var op = (IInvocationOperation)context.Operation; if (op is null) @@ -128,12 +137,6 @@ private static void AnalyzeInvocation(OperationAnalysisContext context) if (containingType is null) return; - var argumentExceptionType = context.Compilation.GetBestTypeByMetadataName("System.ArgumentException"); - var argumentNullExceptionType = context.Compilation.GetBestTypeByMetadataName("System.ArgumentNullException"); - - if (argumentExceptionType is null || argumentNullExceptionType is null) - return; - if (!containingType.IsEqualTo(argumentExceptionType) && !containingType.IsEqualTo(argumentNullExceptionType)) return; @@ -145,6 +148,37 @@ private static void AnalyzeInvocation(OperationAnalysisContext context) if (op.Arguments.Length == 0) return; + var availableParameterNames = GetParameterNames(op, context.CancellationToken); + + // If there's a second argument (paramName) and it's not null, check that instead + if (op.Arguments.Length >= 2) + { + var secondArgument = op.Arguments[1]; + if (secondArgument.Parameter?.Type.IsString() == true && secondArgument.Value is not null) + { + // Check if the second argument is a constant string value + if (secondArgument.Value.ConstantValue.HasValue && secondArgument.Value.ConstantValue.Value is string paramNameValue) + { + if (availableParameterNames.Contains(paramNameValue, StringComparer.Ordinal)) + { + if (secondArgument.Value is not INameOfOperation) + { + var properties = ImmutableDictionary.Empty.Add(ArgumentExceptionShouldSpecifyArgumentNameAnalyzerCommon.ArgumentNameKey, paramNameValue); + context.ReportDiagnostic(NameofRule, properties, secondArgument.Value); + } + + return; + } + + context.ReportDiagnostic(Rule, secondArgument, $"'{paramNameValue}' is not a valid parameter name"); + return; + } + + // Cannot determine the value of the second argument, so we can't validate it + return; + } + } + var firstArgument = op.Arguments[0]; if (firstArgument.Value is null) return; @@ -165,8 +199,7 @@ private static void AnalyzeInvocation(OperationAnalysisContext context) return; // Check if this argument name corresponds to a valid parameter in the current scope - var parameterNames = GetParameterNames(op, context.CancellationToken); - if (!parameterNames.Contains(argumentName, StringComparer.Ordinal)) + if (!availableParameterNames.Contains(argumentName, StringComparer.Ordinal)) { context.ReportDiagnostic(Rule, firstArgument, $"'{argumentName}' is not a valid parameter name"); } diff --git a/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs index 24c01455..8ad403ef 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs @@ -655,4 +655,103 @@ await CreateProjectBuilder() .ShouldReportDiagnosticWithMessage("'Name' is not a valid parameter name") .ValidateAsync(); } + + [Fact] + public async Task ThrowIfNull_WithValidParamNameArgument_ShouldNotReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(string test) + { + ArgumentNullException.ThrowIfNull(test, nameof(test)); + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task ThrowIfNull_WithInvalidParamNameArgument_ShouldReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(string test) + { + ArgumentNullException.ThrowIfNull(test, [|"invalid"|]); + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ShouldReportDiagnosticWithMessage("'invalid' is not a valid parameter name") + .ValidateAsync(); + } + + [Fact] + public async Task ThrowIfNull_WithValidParamNameArgumentNotNameof_ShouldSuggestNameof() + { + var sourceCode = """ + using System; + class Sample + { + void Test(string test) + { + ArgumentNullException.ThrowIfNull(test, [|"test"|]); + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .WithAnalyzer(id: "MA0043") + .ShouldReportDiagnosticWithMessage("Use nameof operator") + .ValidateAsync(); + } + + [Fact] + public async Task ArgumentException_ThrowIfNullOrEmpty_WithValidParamNameArgument_ShouldNotReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(string test) + { + ArgumentException.ThrowIfNullOrEmpty(test, nameof(test)); + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task ArgumentException_ThrowIfNullOrEmpty_WithInvalidParamNameArgument_ShouldReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(string test) + { + ArgumentException.ThrowIfNullOrEmpty(test, [|"invalid"|]); + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ShouldReportDiagnosticWithMessage("'invalid' is not a valid parameter name") + .ValidateAsync(); + } } From 7251bb348445022fd6a0f8da5c49f7b43f61d988 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 02:13:14 +0000 Subject: [PATCH 07/16] Refactor invocation validation into separate methods for better readability Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com> --- ...eptionShouldSpecifyArgumentNameAnalyzer.cs | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs index d0161b5f..0ac76daf 100644 --- a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs @@ -148,37 +148,43 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy if (op.Arguments.Length == 0) return; - var availableParameterNames = GetParameterNames(op, context.CancellationToken); - // If there's a second argument (paramName) and it's not null, check that instead if (op.Arguments.Length >= 2) { var secondArgument = op.Arguments[1]; if (secondArgument.Parameter?.Type.IsString() == true && secondArgument.Value is not null) { - // Check if the second argument is a constant string value - if (secondArgument.Value.ConstantValue.HasValue && secondArgument.Value.ConstantValue.Value is string paramNameValue) - { - if (availableParameterNames.Contains(paramNameValue, StringComparer.Ordinal)) - { - if (secondArgument.Value is not INameOfOperation) - { - var properties = ImmutableDictionary.Empty.Add(ArgumentExceptionShouldSpecifyArgumentNameAnalyzerCommon.ArgumentNameKey, paramNameValue); - context.ReportDiagnostic(NameofRule, properties, secondArgument.Value); - } + ValidateParamNameArgument(context, op, secondArgument); + return; + } + } - return; - } + ValidateFirstArgument(context, op); + } - context.ReportDiagnostic(Rule, secondArgument, $"'{paramNameValue}' is not a valid parameter name"); - return; - } + 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; - // Cannot determine the value of the second argument, so we can't validate it - return; + var availableParameterNames = GetParameterNames(op, context.CancellationToken); + if (availableParameterNames.Contains(paramNameValue, StringComparer.Ordinal)) + { + if (paramNameArgument.Value is not INameOfOperation) + { + var properties = ImmutableDictionary.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; @@ -199,6 +205,7 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy 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"); From 7e955b6551681c1b5c277147dca2c89efa04fdcd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 03:00:22 +0000 Subject: [PATCH 08/16] Check CallerArgumentExpressionAttribute to find paramName parameter Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com> --- ...eptionShouldSpecifyArgumentNameAnalyzer.cs | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs index 0ac76daf..5b917c30 100644 --- a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs @@ -41,12 +41,13 @@ public override void Initialize(AnalysisContext 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), OperationKind.Invocation); + context.RegisterOperationAction(ctx => AnalyzeInvocation(ctx, argumentExceptionType, argumentNullExceptionType, callerArgumentExpressionAttribute), OperationKind.Invocation); }); } @@ -122,7 +123,7 @@ private static void Analyze(OperationAnalysisContext context) } } - private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTypeSymbol argumentExceptionType, INamedTypeSymbol argumentNullExceptionType) + private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTypeSymbol argumentExceptionType, INamedTypeSymbol argumentNullExceptionType, INamedTypeSymbol? callerArgumentExpressionAttribute) { var op = (IInvocationOperation)context.Operation; if (op is null) @@ -148,14 +149,30 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy if (op.Arguments.Length == 0) return; - // If there's a second argument (paramName) and it's not null, check that instead - if (op.Arguments.Length >= 2) + // Find the parameter with CallerArgumentExpressionAttribute + if (callerArgumentExpressionAttribute is not null) { - var secondArgument = op.Arguments[1]; - if (secondArgument.Parameter?.Type.IsString() == true && secondArgument.Value is not null) + foreach (var parameter in method.Parameters) { - ValidateParamNameArgument(context, op, secondArgument); - return; + if (!parameter.Type.IsString()) + continue; + + foreach (var attribute in parameter.GetAttributes()) + { + if (!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.IsEqualTo(parameter)); + if (paramNameArgument is not null && paramNameArgument.Value is not null) + { + ValidateParamNameArgument(context, op, paramNameArgument); + return; + } + } } } From a66c40da1dd9a52731e4228a395a46f6420b17f9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 03:02:30 +0000 Subject: [PATCH 09/16] Add null safety checks for AttributeClass and Parameter Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com> --- .../ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs index 5b917c30..7803ff6d 100644 --- a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs @@ -159,14 +159,14 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy foreach (var attribute in parameter.GetAttributes()) { - if (!attribute.AttributeClass.IsEqualTo(callerArgumentExpressionAttribute)) + 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.IsEqualTo(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); From e6e09242af5da416b0122c975b7ba3ffd2e2b506 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 03:12:41 +0000 Subject: [PATCH 10/16] Add support for ArgumentOutOfRangeException and tests for expression scenarios Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com> --- ...eptionShouldSpecifyArgumentNameAnalyzer.cs | 10 +- ...nShouldSpecifyArgumentNameAnalyzerTests.cs | 195 ++++++++++++++++++ 2 files changed, 201 insertions(+), 4 deletions(-) diff --git a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs index 7803ff6d..b541f5a1 100644 --- a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs @@ -41,13 +41,14 @@ public override void Initialize(AnalysisContext context) { var argumentExceptionType = context.Compilation.GetBestTypeByMetadataName("System.ArgumentException"); var argumentNullExceptionType = context.Compilation.GetBestTypeByMetadataName("System.ArgumentNullException"); + var argumentOutOfRangeExceptionType = context.Compilation.GetBestTypeByMetadataName("System.ArgumentOutOfRangeException"); 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); + context.RegisterOperationAction(ctx => AnalyzeInvocation(ctx, argumentExceptionType, argumentNullExceptionType, argumentOutOfRangeExceptionType, callerArgumentExpressionAttribute), OperationKind.Invocation); }); } @@ -123,7 +124,7 @@ private static void Analyze(OperationAnalysisContext context) } } - private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTypeSymbol argumentExceptionType, INamedTypeSymbol argumentNullExceptionType, INamedTypeSymbol? callerArgumentExpressionAttribute) + private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTypeSymbol argumentExceptionType, INamedTypeSymbol argumentNullExceptionType, INamedTypeSymbol? argumentOutOfRangeExceptionType, INamedTypeSymbol? callerArgumentExpressionAttribute) { var op = (IInvocationOperation)context.Operation; if (op is null) @@ -133,12 +134,13 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy if (method is null || !method.IsStatic) return; - // Check if this is a ThrowIfXxx method on ArgumentException or ArgumentNullException + // Check if this is a ThrowIfXxx method on ArgumentException, ArgumentNullException, or ArgumentOutOfRangeException var containingType = method.ContainingType; if (containingType is null) return; - if (!containingType.IsEqualTo(argumentExceptionType) && !containingType.IsEqualTo(argumentNullExceptionType)) + if (!containingType.IsEqualTo(argumentExceptionType) && !containingType.IsEqualTo(argumentNullExceptionType) && + (argumentOutOfRangeExceptionType is null || !containingType.IsEqualTo(argumentOutOfRangeExceptionType))) return; // Check if the method name starts with "ThrowIf" diff --git a/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs index 8ad403ef..320f9d53 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs @@ -754,4 +754,199 @@ await CreateProjectBuilder() .ShouldReportDiagnosticWithMessage("'invalid' is not a valid parameter name") .ValidateAsync(); } + + [Fact] + public async Task ArgumentOutOfRangeException_ThrowIfNegative_ValidParameter_ShouldNotReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(int value) + { + ArgumentOutOfRangeException.ThrowIfNegative(value); + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task ArgumentOutOfRangeException_ThrowIfNegative_InvalidParameter_ShouldReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(int value) + { + ArgumentOutOfRangeException.ThrowIfNegative([|Count|]); + } + + public static int Count { get; } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ShouldReportDiagnosticWithMessage("'Count' is not a valid parameter name") + .ValidateAsync(); + } + + [Fact] + public async Task ArgumentOutOfRangeException_ThrowIfNegativeOrZero_ValidParameter_ShouldNotReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(int value) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value); + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task ArgumentOutOfRangeException_ThrowIfGreaterThan_ValidParameter_ShouldNotReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(int value) + { + ArgumentOutOfRangeException.ThrowIfGreaterThan(value, 100); + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task ThrowIfNull_WithNullExpression_ShouldNotReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(string test) + { + ArgumentNullException.ThrowIfNull(null); + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task ThrowIfNull_WithNullExpressionAndValidParamName_ShouldNotReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(string test) + { + ArgumentNullException.ThrowIfNull(null, nameof(test)); + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task ThrowIfNull_WithNullExpressionAndInvalidParamName_ShouldReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(string test) + { + ArgumentNullException.ThrowIfNull(null, [|"invalid"|]); + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ShouldReportDiagnosticWithMessage("'invalid' is not a valid parameter name") + .ValidateAsync(); + } + + [Fact] + public async Task ThrowIfNull_WithBooleanExpression_ShouldNotReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(string test) + { + ArgumentNullException.ThrowIfNull(0 == 1); + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task ThrowIfNull_WithBooleanExpressionAndValidParamName_ShouldNotReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(string test) + { + ArgumentNullException.ThrowIfNull(0 == 1, nameof(test)); + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task ThrowIfNull_WithBooleanExpressionAndInvalidParamName_ShouldReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(string test) + { + ArgumentNullException.ThrowIfNull(0 == 1, [|"invalid"|]); + } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ShouldReportDiagnosticWithMessage("'invalid' is not a valid parameter name") + .ValidateAsync(); + } } From e6b24acdb1c07f43f637c6e09ed9a6ac6eab2584 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 03:21:24 +0000 Subject: [PATCH 11/16] Only register invocation action when CallerArgumentExpressionAttribute is available Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com> --- ...eptionShouldSpecifyArgumentNameAnalyzer.cs | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs index b541f5a1..02931015 100644 --- a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs @@ -48,7 +48,11 @@ public override void Initialize(AnalysisContext context) return; context.RegisterOperationAction(Analyze, OperationKind.ObjectCreation); - context.RegisterOperationAction(ctx => AnalyzeInvocation(ctx, argumentExceptionType, argumentNullExceptionType, argumentOutOfRangeExceptionType, callerArgumentExpressionAttribute), OperationKind.Invocation); + + if (callerArgumentExpressionAttribute is not null) + { + context.RegisterOperationAction(ctx => AnalyzeInvocation(ctx, argumentExceptionType, argumentNullExceptionType, argumentOutOfRangeExceptionType, callerArgumentExpressionAttribute), OperationKind.Invocation); + } }); } @@ -124,7 +128,7 @@ private static void Analyze(OperationAnalysisContext context) } } - private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTypeSymbol argumentExceptionType, INamedTypeSymbol argumentNullExceptionType, INamedTypeSymbol? argumentOutOfRangeExceptionType, INamedTypeSymbol? callerArgumentExpressionAttribute) + private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTypeSymbol argumentExceptionType, INamedTypeSymbol argumentNullExceptionType, INamedTypeSymbol? argumentOutOfRangeExceptionType, INamedTypeSymbol callerArgumentExpressionAttribute) { var op = (IInvocationOperation)context.Operation; if (op is null) @@ -152,28 +156,25 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy return; // Find the parameter with CallerArgumentExpressionAttribute - if (callerArgumentExpressionAttribute is not null) + foreach (var parameter in method.Parameters) { - foreach (var parameter in method.Parameters) + if (!parameter.Type.IsString()) + continue; + + foreach (var attribute in parameter.GetAttributes()) { - if (!parameter.Type.IsString()) + if (attribute.AttributeClass is null || !attribute.AttributeClass.IsEqualTo(callerArgumentExpressionAttribute)) continue; - foreach (var attribute in parameter.GetAttributes()) - { - if (attribute.AttributeClass is null || !attribute.AttributeClass.IsEqualTo(callerArgumentExpressionAttribute)) - continue; - - if (attribute.ConstructorArguments.Length == 0) - 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; - } + // 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; } } } From 4812d11556e563c9bd63a007f3085e6da69eba83 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 03:33:38 +0000 Subject: [PATCH 12/16] Refactor analyzer with IsEqualToAny, GetAttribute, and dynamic expression validation Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com> --- ...eptionShouldSpecifyArgumentNameAnalyzer.cs | 76 +++++++++++-------- ...nShouldSpecifyArgumentNameAnalyzerTests.cs | 10 ++- 2 files changed, 52 insertions(+), 34 deletions(-) diff --git a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs index 02931015..9d357c07 100644 --- a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs @@ -138,17 +138,16 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy if (method is null || !method.IsStatic) return; + // Check if the method name starts with "ThrowIf" + if (!method.Name.StartsWith("ThrowIf", StringComparison.Ordinal)) + return; + // Check if this is a ThrowIfXxx method on ArgumentException, ArgumentNullException, or ArgumentOutOfRangeException var containingType = method.ContainingType; if (containingType is null) return; - if (!containingType.IsEqualTo(argumentExceptionType) && !containingType.IsEqualTo(argumentNullExceptionType) && - (argumentOutOfRangeExceptionType is null || !containingType.IsEqualTo(argumentOutOfRangeExceptionType))) - return; - - // Check if the method name starts with "ThrowIf" - if (!method.Name.StartsWith("ThrowIf", StringComparison.Ordinal)) + if (!containingType.IsEqualToAny(argumentExceptionType, argumentNullExceptionType, argumentOutOfRangeExceptionType)) return; // The first parameter is the argument being validated @@ -161,25 +160,39 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy if (!parameter.Type.IsString()) continue; - foreach (var attribute in parameter.GetAttributes()) - { - if (attribute.AttributeClass is null || !attribute.AttributeClass.IsEqualTo(callerArgumentExpressionAttribute)) - continue; + var attribute = parameter.GetAttribute(callerArgumentExpressionAttribute); + if (attribute is null) + continue; - if (attribute.ConstructorArguments.Length == 0) - 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; - } + // Get the parameter name referenced by the CallerArgumentExpressionAttribute + var referencedParameterName = attribute.ConstructorArguments[0].Value as string; + if (string.IsNullOrEmpty(referencedParameterName)) + continue; + + // Find the parameter being referenced + var referencedParameter = method.Parameters.FirstOrDefault(p => p.Name == referencedParameterName); + if (referencedParameter is null) + continue; + + // Find the argument for the paramName 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); + // Find the argument for the referenced parameter (the one being validated) + var referencedArgument = op.Arguments.FirstOrDefault(arg => arg.Parameter is not null && arg.Parameter.IsEqualTo(referencedParameter)); + if (referencedArgument is not null) + { + ValidateExpression(context, op, referencedArgument); + return; + } + } } private static void ValidateParamNameArgument(OperationAnalysisContext context, IInvocationOperation op, IArgumentOperation paramNameArgument) @@ -203,32 +216,35 @@ private static void ValidateParamNameArgument(OperationAnalysisContext context, context.ReportDiagnostic(Rule, paramNameArgument, $"'{paramNameValue}' is not a valid parameter name"); } - private static void ValidateFirstArgument(OperationAnalysisContext context, IInvocationOperation op) + private static void ValidateExpression(OperationAnalysisContext context, IInvocationOperation op, IArgumentOperation argument) { - var firstArgument = op.Arguments[0]; - if (firstArgument.Value is null) + if (argument.Value is null) return; // Check if the argument is a parameter reference or a member access to a property/field - string? argumentName = null; + string argumentName; - if (firstArgument.Value is IParameterReferenceOperation parameterRef) + if (argument.Value is IParameterReferenceOperation parameterRef) { argumentName = parameterRef.Parameter.Name; } - else if (firstArgument.Value is IMemberReferenceOperation memberRef) + else if (argument.Value is IMemberReferenceOperation memberRef) { argumentName = memberRef.Member.Name; } - - if (argumentName is null) + else + { + // If the expression is not a parameter or member reference, report an error + // as it cannot be matched to a parameter name + context.ReportDiagnostic(Rule, argument, "The expression does not match a parameter"); 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"); + context.ReportDiagnostic(Rule, argument, $"'{argumentName}' is not a valid parameter name"); } } diff --git a/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs index 320f9d53..cd5968f1 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs @@ -835,7 +835,7 @@ await CreateProjectBuilder() } [Fact] - public async Task ThrowIfNull_WithNullExpression_ShouldNotReportError() + public async Task ThrowIfNull_WithNullExpression_ShouldReportError() { var sourceCode = """ using System; @@ -843,13 +843,14 @@ class Sample { void Test(string test) { - ArgumentNullException.ThrowIfNull(null); + ArgumentNullException.ThrowIfNull([|null|]); } } """; await CreateProjectBuilder() .WithSourceCode(sourceCode) + .ShouldReportDiagnosticWithMessage("The expression does not match a parameter") .ValidateAsync(); } @@ -893,7 +894,7 @@ await CreateProjectBuilder() } [Fact] - public async Task ThrowIfNull_WithBooleanExpression_ShouldNotReportError() + public async Task ThrowIfNull_WithBooleanExpression_ShouldReportError() { var sourceCode = """ using System; @@ -901,13 +902,14 @@ class Sample { void Test(string test) { - ArgumentNullException.ThrowIfNull(0 == 1); + ArgumentNullException.ThrowIfNull([|0 == 1|]); } } """; await CreateProjectBuilder() .WithSourceCode(sourceCode) + .ShouldReportDiagnosticWithMessage("The expression does not match a parameter") .ValidateAsync(); } From daa4cb763e267eab4f4e9181c6372b674791ae95 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 03:41:03 +0000 Subject: [PATCH 13/16] Add test for ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual with invalid parameter Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com> --- ...nShouldSpecifyArgumentNameAnalyzerTests.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs index cd5968f1..fcd8cd47 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs @@ -834,6 +834,28 @@ await CreateProjectBuilder() .ValidateAsync(); } + [Fact] + public async Task ArgumentOutOfRangeException_ThrowIfGreaterThanOrEqual_InvalidParameter_ShouldReportError() + { + var sourceCode = """ + using System; + class Sample + { + void Test(int value) + { + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual([|MaxValue|], 100); + } + + public static int MaxValue { get; } + } + """; + + await CreateProjectBuilder() + .WithSourceCode(sourceCode) + .ShouldReportDiagnosticWithMessage("'MaxValue' is not a valid parameter name") + .ValidateAsync(); + } + [Fact] public async Task ThrowIfNull_WithNullExpression_ShouldReportError() { From d0f5dd845905634502d27c3c0444b890e87e338e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 03:49:00 +0000 Subject: [PATCH 14/16] Refactor ValidateExpression: remove op parameter, unwrap conversions, simplify logic Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com> --- ...eptionShouldSpecifyArgumentNameAnalyzer.cs | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs index 9d357c07..4cc60bea 100644 --- a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs @@ -189,7 +189,7 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy var referencedArgument = op.Arguments.FirstOrDefault(arg => arg.Parameter is not null && arg.Parameter.IsEqualTo(referencedParameter)); if (referencedArgument is not null) { - ValidateExpression(context, op, referencedArgument); + ValidateExpression(context, referencedArgument); return; } } @@ -216,36 +216,35 @@ private static void ValidateParamNameArgument(OperationAnalysisContext context, context.ReportDiagnostic(Rule, paramNameArgument, $"'{paramNameValue}' is not a valid parameter name"); } - private static void ValidateExpression(OperationAnalysisContext context, IInvocationOperation op, IArgumentOperation argument) + private static void ValidateExpression(OperationAnalysisContext context, IArgumentOperation argument) { if (argument.Value is null) return; - // Check if the argument is a parameter reference or a member access to a property/field - string argumentName; + var unwrappedValue = argument.Value.UnwrapImplicitConversionOperations(); - if (argument.Value is IParameterReferenceOperation parameterRef) + // Check if the argument is a parameter reference + if (unwrappedValue is IParameterReferenceOperation) { - argumentName = parameterRef.Parameter.Name; - } - else if (argument.Value is IMemberReferenceOperation memberRef) - { - argumentName = memberRef.Member.Name; - } - else - { - // If the expression is not a parameter or member reference, report an error - // as it cannot be matched to a parameter name - context.ReportDiagnostic(Rule, argument, "The expression does not match a parameter"); + // Parameter references are always valid - no need to validate the name 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)) + // Check if the argument is a member access to a property/field + if (unwrappedValue is IMemberReferenceOperation memberRef) { - context.ReportDiagnostic(Rule, argument, $"'{argumentName}' is not a valid parameter name"); + var memberName = memberRef.Member.Name; + var availableParameterNames = GetParameterNames(argument, context.CancellationToken); + if (!availableParameterNames.Contains(memberName, StringComparer.Ordinal)) + { + context.ReportDiagnostic(Rule, argument, $"'{memberName}' is not a valid parameter name"); + } + return; } + + // If the expression is not a parameter or member reference, report an error + // as it cannot be matched to a parameter name + context.ReportDiagnostic(Rule, argument, "The expression does not match a parameter"); } private static IEnumerable GetParameterNames(IOperation operation, CancellationToken cancellationToken) From cd51ba1358df93b171b652258242d5ecc8016e8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Sat, 1 Nov 2025 23:55:52 -0400 Subject: [PATCH 15/16] Apply suggestion from @meziantou --- ...mentExceptionShouldSpecifyArgumentNameAnalyzer.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs index 4cc60bea..cceeeab3 100644 --- a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs @@ -230,18 +230,6 @@ private static void ValidateExpression(OperationAnalysisContext context, IArgume return; } - // Check if the argument is a member access to a property/field - if (unwrappedValue is IMemberReferenceOperation memberRef) - { - var memberName = memberRef.Member.Name; - var availableParameterNames = GetParameterNames(argument, context.CancellationToken); - if (!availableParameterNames.Contains(memberName, StringComparer.Ordinal)) - { - context.ReportDiagnostic(Rule, argument, $"'{memberName}' is not a valid parameter name"); - } - return; - } - // If the expression is not a parameter or member reference, report an error // as it cannot be matched to a parameter name context.ReportDiagnostic(Rule, argument, "The expression does not match a parameter"); From e11859a2ae97bc8aef02ea79fef5d49f6d21e661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Sun, 2 Nov 2025 00:09:22 -0400 Subject: [PATCH 16/16] fix --- ...eptionShouldSpecifyArgumentNameAnalyzer.cs | 27 ++++++------ ...nShouldSpecifyArgumentNameAnalyzerTests.cs | 42 ++++--------------- 2 files changed, 19 insertions(+), 50 deletions(-) diff --git a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs index cceeeab3..31b51367 100644 --- a/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzer.cs @@ -1,4 +1,4 @@ -using System.Collections.Immutable; +using System.Collections.Immutable; using Meziantou.Analyzer.Internals; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -47,7 +47,7 @@ public override void Initialize(AnalysisContext context) if (argumentExceptionType is null || argumentNullExceptionType is null) return; - context.RegisterOperationAction(Analyze, OperationKind.ObjectCreation); + context.RegisterOperationAction(AnalyzeObjectCreation, OperationKind.ObjectCreation); if (callerArgumentExpressionAttribute is not null) { @@ -56,7 +56,8 @@ public override void Initialize(AnalysisContext context) }); } - private static void Analyze(OperationAnalysisContext context) + // Validate throw new ArgumentException("message", "paramName"); + private static void AnalyzeObjectCreation(OperationAnalysisContext context) { var op = (IObjectCreationOperation)context.Operation; if (op is null) @@ -142,6 +143,10 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy if (!method.Name.StartsWith("ThrowIf", StringComparison.Ordinal)) return; + // There must be at least one argument + if (op.Arguments.Length == 0) + return; + // Check if this is a ThrowIfXxx method on ArgumentException, ArgumentNullException, or ArgumentOutOfRangeException var containingType = method.ContainingType; if (containingType is null) @@ -150,10 +155,6 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy if (!containingType.IsEqualToAny(argumentExceptionType, argumentNullExceptionType, argumentOutOfRangeExceptionType)) return; - // The first parameter is the argument being validated - if (op.Arguments.Length == 0) - return; - // Find the parameter with CallerArgumentExpressionAttribute foreach (var parameter in method.Parameters) { @@ -179,9 +180,9 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy // Find the argument for the paramName 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) + if (paramNameArgument is not null && !paramNameArgument.IsImplicit && paramNameArgument.Value is not null) { - ValidateParamNameArgument(context, op, paramNameArgument); + ValidateParamNameArgument(context, paramNameArgument); return; } @@ -195,13 +196,13 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTy } } - private static void ValidateParamNameArgument(OperationAnalysisContext context, IInvocationOperation op, IArgumentOperation paramNameArgument) + private static void ValidateParamNameArgument(OperationAnalysisContext context, 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); + var availableParameterNames = GetParameterNames(paramNameArgument, context.CancellationToken); if (availableParameterNames.Contains(paramNameValue, StringComparer.Ordinal)) { if (paramNameArgument.Value is not INameOfOperation) @@ -222,16 +223,12 @@ private static void ValidateExpression(OperationAnalysisContext context, IArgume return; var unwrappedValue = argument.Value.UnwrapImplicitConversionOperations(); - - // Check if the argument is a parameter reference if (unwrappedValue is IParameterReferenceOperation) { // Parameter references are always valid - no need to validate the name return; } - // If the expression is not a parameter or member reference, report an error - // as it cannot be matched to a parameter name context.ReportDiagnostic(Rule, argument, "The expression does not match a parameter"); } diff --git a/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs index fcd8cd47..28ae1fc9 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests.cs @@ -1,4 +1,5 @@ -using Meziantou.Analyzer.Rules; +using Meziantou.Analyzer.Rules; +using Meziantou.Analyzer.Test.Helpers; using TestHelper; namespace Meziantou.Analyzer.Test.Rules; @@ -8,7 +9,8 @@ public sealed class ArgumentExceptionShouldSpecifyArgumentNameAnalyzerTests private static ProjectBuilder CreateProjectBuilder() { return new ProjectBuilder() - .WithAnalyzer(id: "MA0015"); + .WithAnalyzer(id: "MA0015") + .WithTargetFramework(TargetFramework.NetLatest); } [Fact] @@ -488,7 +490,6 @@ void Test(string test) await CreateProjectBuilder() .WithSourceCode(sourceCode) - .ShouldReportDiagnosticWithMessage("'Name' is not a valid parameter name") .ValidateAsync(); } @@ -529,7 +530,6 @@ void Test(string test) await CreateProjectBuilder() .WithSourceCode(sourceCode) - .ShouldReportDiagnosticWithMessage("'Name' is not a valid parameter name") .ValidateAsync(); } @@ -570,7 +570,6 @@ void Test(string test) await CreateProjectBuilder() .WithSourceCode(sourceCode) - .ShouldReportDiagnosticWithMessage("'Name' is not a valid parameter name") .ValidateAsync(); } @@ -611,7 +610,6 @@ void Test(string test) await CreateProjectBuilder() .WithSourceCode(sourceCode) - .ShouldReportDiagnosticWithMessage("'Name' is not a valid parameter name") .ValidateAsync(); } @@ -652,7 +650,6 @@ void Test(string test) await CreateProjectBuilder() .WithSourceCode(sourceCode) - .ShouldReportDiagnosticWithMessage("'Name' is not a valid parameter name") .ValidateAsync(); } @@ -695,27 +692,6 @@ await CreateProjectBuilder() .ValidateAsync(); } - [Fact] - public async Task ThrowIfNull_WithValidParamNameArgumentNotNameof_ShouldSuggestNameof() - { - var sourceCode = """ - using System; - class Sample - { - void Test(string test) - { - ArgumentNullException.ThrowIfNull(test, [|"test"|]); - } - } - """; - - await CreateProjectBuilder() - .WithSourceCode(sourceCode) - .WithAnalyzer(id: "MA0043") - .ShouldReportDiagnosticWithMessage("Use nameof operator") - .ValidateAsync(); - } - [Fact] public async Task ArgumentException_ThrowIfNullOrEmpty_WithValidParamNameArgument_ShouldNotReportError() { @@ -792,7 +768,6 @@ void Test(int value) await CreateProjectBuilder() .WithSourceCode(sourceCode) - .ShouldReportDiagnosticWithMessage("'Count' is not a valid parameter name") .ValidateAsync(); } @@ -852,7 +827,6 @@ void Test(int value) await CreateProjectBuilder() .WithSourceCode(sourceCode) - .ShouldReportDiagnosticWithMessage("'MaxValue' is not a valid parameter name") .ValidateAsync(); } @@ -865,14 +839,13 @@ class Sample { void Test(string test) { - ArgumentNullException.ThrowIfNull([|null|]); + ArgumentNullException.ThrowIfNull([|""|]); } } """; await CreateProjectBuilder() .WithSourceCode(sourceCode) - .ShouldReportDiagnosticWithMessage("The expression does not match a parameter") .ValidateAsync(); } @@ -885,7 +858,7 @@ class Sample { void Test(string test) { - ArgumentNullException.ThrowIfNull(null, nameof(test)); + ArgumentNullException.ThrowIfNull("", nameof(test)); } } """; @@ -904,14 +877,13 @@ class Sample { void Test(string test) { - ArgumentNullException.ThrowIfNull(null, [|"invalid"|]); + ArgumentNullException.ThrowIfNull("", [|"invalid"|]); } } """; await CreateProjectBuilder() .WithSourceCode(sourceCode) - .ShouldReportDiagnosticWithMessage("'invalid' is not a valid parameter name") .ValidateAsync(); }