Skip to content

Commit

Permalink
Address [NotNull] on int? (#124)
Browse files Browse the repository at this point in the history
  • Loading branch information
viceroypenguin authored Oct 11, 2024
1 parent d66ecfe commit 13a1077
Show file tree
Hide file tree
Showing 18 changed files with 435 additions and 56 deletions.
19 changes: 19 additions & 0 deletions src/Common/ITypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,25 @@ typeSymbol is
},
};

public static bool IsNotNullAttribute([NotNullWhen(returnValue: true)] this INamedTypeSymbol? typeSymbol) =>
typeSymbol is
{
Name: "NotNullAttribute",
ContainingNamespace:
{
Name: "Shared",
ContainingNamespace:
{
Name: "Validations",
ContainingNamespace:
{
Name: "Immediate",
ContainingNamespace.IsGlobalNamespace: true,
},
},
},
};

public static bool IsValidationResult(this INamedTypeSymbol? typeSymbol) =>
typeSymbol is
{
Expand Down
27 changes: 26 additions & 1 deletion src/Immediate.Validations.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,29 @@
### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
--------|----------|----------|-------
IV0001 | ImmediateValidations | Error | ValidateMethodAnalyzer
IV0002 | ImmediateValidations | Error | ValidatorClassAnalyzer
IV0003 | ImmediateValidations | Error | ValidatorClassAnalyzer
IV0004 | ImmediateValidations | Error | ValidatorClassAnalyzer
IV0005 | ImmediateValidations | Error | ValidatorClassAnalyzer
IV0006 | ImmediateValidations | Error | ValidatorClassAnalyzer
IV0007 | ImmediateValidations | Error | ValidatorClassAnalyzer
IV0008 | ImmediateValidations | Error | ValidatorClassAnalyzer
IV0009 | ImmediateValidations | Error | ValidatorClassAnalyzer
IV0010 | ImmediateValidations | Error | ValidatorClassAnalyzer
IV0011 | ImmediateValidations | Warning | AssemblyBehaviorAnalyzer
IV0012 | ImmediateValidations | Error | ValidateClassAnalyzer
IV0013 | ImmediateValidations | Warning | ValidateClassAnalyzer
IV0014 | ImmediateValidations | Warning | ValidateClassAnalyzer
IV0015 | ImmediateValidations | Warning | ValidateClassAnalyzer
IV0016 | ImmediateValidations | Warning | ValidateClassAnalyzer
IV0017 | ImmediateValidations | Error | ValidateClassAnalyzer

## Release 1.1

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
IV0018 | ImmediateValidations | Warning | ValidateClassAnalyzer
20 changes: 0 additions & 20 deletions src/Immediate.Validations.Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
@@ -1,21 +1 @@
### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
IV0001 | ImmediateValidations | Error | ValidateMethodAnalyzer
IV0002 | ImmediateValidations | Error | ValidatorClassAnalyzer
IV0003 | ImmediateValidations | Error | ValidatorClassAnalyzer
IV0004 | ImmediateValidations | Error | ValidatorClassAnalyzer
IV0005 | ImmediateValidations | Error | ValidatorClassAnalyzer
IV0006 | ImmediateValidations | Error | ValidatorClassAnalyzer
IV0007 | ImmediateValidations | Error | ValidatorClassAnalyzer
IV0008 | ImmediateValidations | Error | ValidatorClassAnalyzer
IV0009 | ImmediateValidations | Error | ValidatorClassAnalyzer
IV0010 | ImmediateValidations | Error | ValidatorClassAnalyzer
IV0011 | ImmediateValidations | Warning | AssemblyBehaviorAnalyzer
IV0012 | ImmediateValidations | Error | ValidateClassAnalyzer
IV0013 | ImmediateValidations | Warning | ValidateClassAnalyzer
IV0014 | ImmediateValidations | Warning | ValidateClassAnalyzer
IV0015 | ImmediateValidations | Warning | ValidateClassAnalyzer
IV0016 | ImmediateValidations | Warning | ValidateClassAnalyzer
IV0017 | ImmediateValidations | Error | ValidateClassAnalyzer
1 change: 1 addition & 0 deletions src/Immediate.Validations.Analyzers/DiagnosticIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ public static class DiagnosticIds
public const string IV0015ValidateParameterIncompatibleType = "IV0015";
public const string IV0016ValidateParameterPropertyIncompatibleType = "IV0016";
public const string IV0017ValidateParameterNameofInvalid = "IV0017";
public const string IV0018ValidateNotNullWhenInvalid = "IV0018";
}
46 changes: 39 additions & 7 deletions src/Immediate.Validations.Analyzers/ValidateClassAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ public sealed class ValidateClassAnalyzer : DiagnosticAnalyzer
description: "Invalid nameof will generate incorrect code."
);

public static readonly DiagnosticDescriptor ValidateNotNullWhenInvalid =
new(
id: DiagnosticIds.IV0018ValidateNotNullWhenInvalid,
title: "`[NotNull]` applied to not-null property",
messageFormat: "`[NotNull]` is applied to property `{0}`, but type `{1}` cannot be null",
category: "ImmediateValidations",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "Invalid `[NotNull]` will generate incorrect code."
);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(
[
Expand All @@ -85,6 +96,7 @@ public sealed class ValidateClassAnalyzer : DiagnosticAnalyzer
ValidateParameterIncompatibleType,
ValidateParameterPropertyIncompatibleType,
ValidateParameterNameofInvalid,
ValidateNotNullWhenInvalid,
]);

public override void Initialize(AnalysisContext context)
Expand Down Expand Up @@ -168,7 +180,7 @@ or IMethodSymbol
{
var status = ValidateAttribute(context.Compilation, property.Type, attribute.AttributeClass!, token);

if (status.Report)
if (status.IncompatibleType)
{
context.ReportDiagnostic(
Diagnostic.Create(
Expand All @@ -179,6 +191,17 @@ or IMethodSymbol
)
);
}
else if (status.InvalidNotNull)
{
context.ReportDiagnostic(
Diagnostic.Create(
ValidateNotNullWhenInvalid,
attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation(),
property.Name,
property.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)
)
);
}
else if (status.ValidateArguments)
{
ValidateArguments(
Expand All @@ -195,7 +218,8 @@ or IMethodSymbol

private sealed record AttributeValidationStatus
{
public required bool Report { get; init; }
public bool IncompatibleType { get; init; }
public bool InvalidNotNull { get; init; }
public bool ValidateArguments { get; init; }
public ImmutableArray<IParameterSymbol> ValidateParameterSymbols { get; init; }
}
Expand All @@ -210,7 +234,7 @@ CancellationToken token
token.ThrowIfCancellationRequested();

if (!attributeSymbol.ImplementsValidatorAttribute())
return new() { Report = false };
return new();

token.ThrowIfCancellationRequested();

Expand All @@ -225,16 +249,23 @@ CancellationToken token
)
{
// covered by other analyzers
return new() { Report = false };
return new();
}

token.ThrowIfCancellationRequested();

if (targetParameterType is ITypeParameterSymbol tps)
{
if (attributeSymbol.IsNotNullAttribute()
&& propertyType is { IsReferenceType: false, OriginalDefinition.SpecialType: not SpecialType.System_Nullable_T })
{
return new() { InvalidNotNull = true };
}

if (tps.SatisfiesConstraints(propertyType, compilation))
{
return new()
{
Report = false,
ValidateArguments = true,
ValidateParameterSymbols = validateMethod.Parameters,
};
Expand All @@ -255,13 +286,14 @@ CancellationToken token
{
return new()
{
Report = false,
ValidateArguments = true,
ValidateParameterSymbols = validateMethod.Parameters,
};
}
}

token.ThrowIfCancellationRequested();

return propertyType switch
{
IArrayTypeSymbol ats =>
Expand All @@ -285,7 +317,7 @@ CancellationToken token
token
),

_ => new() { Report = true, },
_ => new() { IncompatibleType = true, },
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ TypeDeclarationSyntax typeDeclarationSyntax
]))
.WithParameterList(
ParameterList(
SeparatedList(new ParameterSyntax[]
{
SeparatedList(
[
Parameter(Identifier("errors"))
.WithType(IdentifierName("ValidationResult")),
Parameter(Identifier("target"))
.WithType(IdentifierName(typeDeclarationSyntax.Identifier)),
})))
])))
.WithBody(
Block())
.WithAdditionalAnnotations(Formatter.Annotation);
Expand Down
1 change: 1 addition & 0 deletions src/Immediate.Validations.Generators/Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public sealed record ValidationTargetProperty
public required string TypeFullName { get; init; }
public required bool IsReferenceType { get; init; }
public required bool IsNullable { get; init; }
public required bool ValidateNotNull { get; init; }
public required bool IsValidationProperty { get; init; }
public required string? ValidationTypeFullName { get; init; }
public required ValidationTargetProperty? CollectionPropertyDetails { get; init; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ partial {{ class.type }} {{ class.name }}
{{~ if p.is_reference_type || p.is_nullable ~}}
if (target is not { } t)
{
{{~ if !p.is_nullable ~}}
{{~ if p.validate_not_null ~}}
errors.Add(
$"{{ get_prop_name(p.property_name, depth) }}",
$"'{{ get_prop_name(p.name, depth) }}' must not be null."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,9 @@ ImmutableArray<AttributeData> attributes
|| attributes.Any(a => a.AttributeClass.IsAllowNullAttribute()))
: propertyType.IsNullableType();

var baseType = !isReferenceType && isNullable
var validateNotNull = !isNullable || attributes.Any(a => a.AttributeClass.IsNotNullAttribute());

var baseType = propertyType.IsNullableType()
? ((INamedTypeSymbol)propertyType).TypeArguments[0]
: propertyType;

Expand Down Expand Up @@ -261,7 +263,8 @@ ImmutableArray<AttributeData> attributes
};

if (
(isNullable || !isReferenceType)
(!validateNotNull
|| (!isReferenceType && !propertyType.IsNullableType()))
&& !isValidationProperty
&& collectionPropertyDetails is null
&& validations is []
Expand All @@ -277,6 +280,7 @@ ImmutableArray<AttributeData> attributes
TypeFullName = propertyType.ToDisplayString(s_fullyQualifiedPlusNullable),
IsReferenceType = isReferenceType,
IsNullable = isNullable,
ValidateNotNull = validateNotNull,

IsValidationProperty = isValidationProperty,
ValidationTypeFullName = isValidationProperty
Expand All @@ -303,7 +307,7 @@ AttributeData attribute
_token.ThrowIfCancellationRequested();

var @class = attribute.AttributeClass;
if (!@class.ImplementsValidatorAttribute())
if (!@class.ImplementsValidatorAttribute() || @class.IsNotNullAttribute())
return null;

_token.ThrowIfCancellationRequested();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ public sealed class NotNullAttribute : ValidatorAttribute
/// <returns>
/// <see langword="true" /> if the property is valid; <see langword="false" /> otherwise.
/// </returns>
public static bool ValidateProperty<T>(T value)
where T : class => value is not null;
public static bool ValidateProperty<T>(T value) =>
value is not null;

/// <summary>
/// The default message template when the property is invalid.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,20 @@ await AnalyzerTestHelpers.CreateAnalyzerTest<ValidateClassAnalyzer>(
"""
using System.Collections.Generic;
using Immediate.Validations.Shared;

public sealed class NotNullClassAttribute : ValidatorAttribute
{
public static bool ValidateProperty<T>(T value)
where T : class =>
value is not null;

public static string DefaultMessage => ValidationConfiguration.Localizer[nameof(NotNullAttribute)].Value;
}

[Validate]
public sealed partial record Target : IValidationTarget<Target>
{
[{|IV0014:NotNull|}]
[{|IV0014:NotNullClass|}]
public required int Id { get; init; }

public static ValidationResult Validate(Target target) => [];
Expand Down Expand Up @@ -940,4 +949,64 @@ public partial interface IInterface : IBaseInterface, IValidationTarget<IInterfa
}
"""
).RunAsync();

[Fact]
public async Task NotNullOnIntShouldWarn() =>
await AnalyzerTestHelpers.CreateAnalyzerTest<ValidateClassAnalyzer>(
"""
using System;
using System.Collections.Generic;
using Immediate.Validations.Shared;

[Validate]
public sealed partial record Target : IValidationTarget<Target>
{
[{|IV0018:NotNull|}]
public required int IntProperty { get; init; }

public static ValidationResult Validate(Target target) => [];
public static ValidationResult Validate(Target target, ValidationResult errors) => [];
}
"""
).RunAsync();

[Fact]
public async Task NotNullOnNullableIntShouldNotWarn() =>
await AnalyzerTestHelpers.CreateAnalyzerTest<ValidateClassAnalyzer>(
"""
using System;
using System.Collections.Generic;
using Immediate.Validations.Shared;

[Validate]
public sealed partial record Target : IValidationTarget<Target>
{
[NotNull]
public required int? IntProperty { get; init; }

public static ValidationResult Validate(Target target) => [];
public static ValidationResult Validate(Target target, ValidationResult errors) => [];
}
"""
).RunAsync();

[Fact]
public async Task NotNullOnStringShouldNotWarn() =>
await AnalyzerTestHelpers.CreateAnalyzerTest<ValidateClassAnalyzer>(
"""
using System;
using System.Collections.Generic;
using Immediate.Validations.Shared;

[Validate]
public sealed partial record Target : IValidationTarget<Target>
{
[NotNull]
public required string StringProperty { get; init; }

public static ValidationResult Validate(Target target) => [];
public static ValidationResult Validate(Target target, ValidationResult errors) => [];
}
"""
).RunAsync();
}
Loading

0 comments on commit 13a1077

Please sign in to comment.