diff --git a/src/Immediate.Validations.Analyzers/AnalyzerReleases.Unshipped.md b/src/Immediate.Validations.Analyzers/AnalyzerReleases.Unshipped.md index 4444261..7908ac1 100644 --- a/src/Immediate.Validations.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/Immediate.Validations.Analyzers/AnalyzerReleases.Unshipped.md @@ -9,3 +9,4 @@ IV0004 | ImmediateValidations | Error | ValidatorClassAnalyzer IV0005 | ImmediateValidations | Error | ValidatorClassAnalyzer IV0006 | ImmediateValidations | Error | ValidatorClassAnalyzer IV0007 | ImmediateValidations | Error | ValidatorClassAnalyzer +IV0008 | ImmediateValidations | Error | ValidatorClassAnalyzer diff --git a/src/Immediate.Validations.Analyzers/DiagnosticIds.cs b/src/Immediate.Validations.Analyzers/DiagnosticIds.cs index 9c8a07d..764faaf 100644 --- a/src/Immediate.Validations.Analyzers/DiagnosticIds.cs +++ b/src/Immediate.Validations.Analyzers/DiagnosticIds.cs @@ -9,4 +9,5 @@ public static class DiagnosticIds public const string IV0005ValidateMethodIsMissingParameter = "IV0005"; public const string IV0006ValidateMethodHasExtraParameter = "IV0006"; public const string IV0007ValidateMethodParameterIsIncorrectType = "IV0007"; + public const string IV0008ValidatePropertyMustBeRequired = "IV0008"; } diff --git a/src/Immediate.Validations.Analyzers/ValidatorClassAnalyzer.cs b/src/Immediate.Validations.Analyzers/ValidatorClassAnalyzer.cs index ec53c0f..4a82146 100644 --- a/src/Immediate.Validations.Analyzers/ValidatorClassAnalyzer.cs +++ b/src/Immediate.Validations.Analyzers/ValidatorClassAnalyzer.cs @@ -85,6 +85,17 @@ public sealed class ValidatorClassAnalyzer : DiagnosticAnalyzer description: "`Validate()` parameters must exist as properties on the containing Validator class." ); + public static readonly DiagnosticDescriptor ValidatePropertyMustBeRequired = + new( + id: DiagnosticIds.IV0008ValidatePropertyMustBeRequired, + title: "Validator property must be `required`", + messageFormat: "Property `{0}` must have the `required` modifier", + category: "ImmediateValidations", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "`Validate()` parameters without a default value require values to be set on their matching properties." + ); + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( [ @@ -95,6 +106,7 @@ public sealed class ValidatorClassAnalyzer : DiagnosticAnalyzer ValidateMethodIsMissingParameter, ValidateMethodHasExtraParameter, ValidateMethodParameterIsIncorrectType, + ValidatePropertyMustBeRequired, ]); public override void Initialize(AnalysisContext context) @@ -236,6 +248,20 @@ private void AnalyzeSymbol(SymbolAnalysisContext context) ) ); } + + if ( + !parameter.HasExplicitDefaultValue + && !property.IsRequired + ) + { + context.ReportDiagnostic( + Diagnostic.Create( + ValidatePropertyMustBeRequired, + property.Locations[0], + property.Name + ) + ); + } } } diff --git a/tests/Immediate.Validations.Tests/AnalyzerTests/ValidatorClassAnalyzerTests.cs b/tests/Immediate.Validations.Tests/AnalyzerTests/ValidatorClassAnalyzerTests.cs index d216546..96fa509 100644 --- a/tests/Immediate.Validations.Tests/AnalyzerTests/ValidatorClassAnalyzerTests.cs +++ b/tests/Immediate.Validations.Tests/AnalyzerTests/ValidatorClassAnalyzerTests.cs @@ -140,6 +140,33 @@ public static (bool Invalid, string? DefaultMessage) ValidateProperty(int value, """ ).RunAsync(); + [Fact] + public async Task ValidateMethodGeneralParameterVarianceShouldWarn() => + await AnalyzerTestHelpers.CreateAnalyzerTest( + """ + using Immediate.Validations.Shared; + + public sealed class GreaterThanAttribute : ValidatorAttribute + { + public required int {|IV0005:Alpha|} { get; init; } + public required int {|IV0005:Charlie|} { get; init; } + public required int {|IV0005:Echo|} { get; init; } + + public static (bool Invalid, string? DefaultMessage) ValidateProperty( + int value, + int {|IV0006:bravo|}, + int {|IV0006:delta|}, + int {|IV0006:foxtrot|} + ) + { + return value <= 0 + ? (true, "Property must not be `null`.") + : default; + } + } + """ + ).RunAsync(); + [Fact] public async Task ValidateMethodMismatchTypesShouldWarn1() => await AnalyzerTestHelpers.CreateAnalyzerTest( @@ -179,4 +206,25 @@ public static (bool Invalid, string? DefaultMessage) ValidateProperty(string val } """ ).RunAsync(); + + [Fact] + public async Task ValidatePropertyMissingRequiredShouldWarn() => + await AnalyzerTestHelpers.CreateAnalyzerTest( + """ + using Immediate.Validations.Shared; + + public sealed class GreaterThanAttribute : ValidatorAttribute + { + public int {|IV0008:Operand|} { get; init; } + + public static (bool Invalid, string? DefaultMessage) ValidateProperty(int value, int operand) + { + return value <= operand + ? (true, "Property must not be `null`.") + : default; + } + } + """ + ).RunAsync(); + }