Skip to content

Commit

Permalink
Add analyzers for a Validate class (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
viceroypenguin authored May 13, 2024
1 parent 6cc9bf3 commit 22c2dfe
Show file tree
Hide file tree
Showing 19 changed files with 815 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ IV0005 | ImmediateValidations | Error | ValidatorClassAnalyzer
IV0006 | ImmediateValidations | Error | ValidatorClassAnalyzer
IV0007 | ImmediateValidations | Error | ValidatorClassAnalyzer
IV0008 | ImmediateValidations | Error | ValidatorClassAnalyzer
IV0011 | ImmediateValidations | Error | ValidateClassAnalyzer
IV0012 | ImmediateValidations | Warning | ValidateClassAnalyzer
IV0013 | ImmediateValidations | Warning | ValidateClassAnalyzer
4 changes: 4 additions & 0 deletions src/Immediate.Validations.Analyzers/DiagnosticIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ public static class DiagnosticIds
public const string IV0006ValidateMethodHasExtraParameter = "IV0006";
public const string IV0007ValidateMethodParameterIsIncorrectType = "IV0007";
public const string IV0008ValidatePropertyMustBeRequired = "IV0008";

public const string IV0011ValidateAttributeMissing = "IV0011";
public const string IV0012IValidationTargetMissing = "IV0012";
public const string IV0013ValidatePropertyIncompatibleType = "IV0013";
}
83 changes: 82 additions & 1 deletion src/Immediate.Validations.Analyzers/ITypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;

namespace Immediate.Validations.Analyzers;

internal static class ITypeSymbolExtensions
{
public static bool IsValidator(this INamedTypeSymbol? typeSymbol) =>
public static bool IsValidatorAttribute([NotNullWhen(returnValue: true)] this INamedTypeSymbol? typeSymbol) =>
typeSymbol is
{
Name: "ValidatorAttribute",
Expand All @@ -23,6 +24,48 @@ typeSymbol is
},
};

public static bool ImplementsValidatorAttribute([NotNullWhen(true)] this INamedTypeSymbol? typeSymbol) =>
typeSymbol.IsValidatorAttribute()
|| (typeSymbol?.BaseType is not null && ImplementsValidatorAttribute(typeSymbol.BaseType.OriginalDefinition));

public static bool IsValidateAttribute(this INamedTypeSymbol? typeSymbol) =>
typeSymbol is
{
Name: "ValidateAttribute",
ContainingNamespace:
{
Name: "Shared",
ContainingNamespace:
{
Name: "Validations",
ContainingNamespace:
{
Name: "Immediate",
ContainingNamespace.IsGlobalNamespace: true,
},
},
},
};

public static bool IsIValidationTarget(this INamedTypeSymbol? typeSymbol) =>
typeSymbol is
{
MetadataName: "IValidationTarget`1",
ContainingNamespace:
{
Name: "Shared",
ContainingNamespace:
{
Name: "Validations",
ContainingNamespace:
{
Name: "Immediate",
ContainingNamespace.IsGlobalNamespace: true,
},
},
},
};

public static bool IsValidValidatorReturn(this ITypeSymbol? typeSymbol) =>
typeSymbol is INamedTypeSymbol
{
Expand All @@ -38,4 +81,42 @@ typeSymbol is INamedTypeSymbol
{ SpecialType: SpecialType.System_String, NullableAnnotation: NullableAnnotation.Annotated or NullableAnnotation.None },
]
};

public static bool IsICollection1(this INamedTypeSymbol typeSymbol) =>
typeSymbol is
{
MetadataName: "ICollection`1",
ContainingNamespace:
{
Name: "Generic",
ContainingNamespace:
{
Name: "Collections",
ContainingNamespace:
{
Name: "System",
ContainingNamespace.IsGlobalNamespace: true,
},
},
},
};

public static bool IsIReadOnlyCollection1(this INamedTypeSymbol typeSymbol) =>
typeSymbol is
{
MetadataName: "IReadOnlyCollection`1",
ContainingNamespace:
{
Name: "Generic",
ContainingNamespace:
{
Name: "Collections",
ContainingNamespace:
{
Name: "System",
ContainingNamespace.IsGlobalNamespace: true,
},
},
},
};
}
158 changes: 158 additions & 0 deletions src/Immediate.Validations.Analyzers/Utility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace Immediate.Validations.Analyzers;

[ExcludeFromCodeCoverage]
internal static class Utility
{
public static T? SingleValue<T>(this IEnumerable<T> source)
{
using var enumerator = source.GetEnumerator();
if (!enumerator.MoveNext())
return default;

var c = enumerator.Current;
if (enumerator.MoveNext())
return default;

return c;
}

public static bool SatisfiesConstraints(ITypeParameterSymbol typeParameter, ITypeSymbol typeArgument, Compilation compilation)
{
if (typeArgument.IsPointerOrFunctionPointer() || typeArgument.IsRefLikeType)
return false;

if ((typeParameter.HasReferenceTypeConstraint && !typeArgument.IsReferenceType)
|| (typeParameter.HasValueTypeConstraint && !typeArgument.IsNonNullableValueType())
|| (typeParameter.HasUnmanagedTypeConstraint && !(typeArgument.IsUnmanagedType && typeArgument.IsNonNullableValueType()))
|| (typeParameter.HasConstructorConstraint && !SatisfiesConstructorConstraint(typeArgument)))
{
return false;
}

foreach (var typeConstraint in typeParameter.ConstraintTypes)
{
var substitutedConstraintType = SubstituteType(compilation, typeConstraint, typeParameter, typeArgument);
var conversion = compilation.ClassifyConversion(typeArgument, substitutedConstraintType);

if (typeArgument.IsNullableType()
|| conversion is not ({ IsIdentity: true } or { IsImplicit: true, IsReference: true } or { IsBoxing: true }))
{
return false;
}
}

return true;
}

public static bool IsNonNullableValueType(this ITypeSymbol typeArgument)
{
if (!typeArgument.IsValueType)
return false;

return !IsNullableTypeOrTypeParameter(typeArgument);
}

public static bool IsNullableTypeOrTypeParameter(this ITypeSymbol? type)
{
if (type is null)
return false;

if (type.TypeKind == TypeKind.TypeParameter)
{
var constraintTypes = ((ITypeParameterSymbol)type).ConstraintTypes;
foreach (var constraintType in constraintTypes)
{
if (constraintType.IsNullableTypeOrTypeParameter())
return true;
}

return false;
}

return type.IsNullableType();
}

/// <summary>
/// Is this System.Nullable`1 type, or its substitution.
///
/// To check whether a type is System.Nullable`1 or is a type parameter constrained to System.Nullable`1
/// use <see cref="IsNullableTypeOrTypeParameter" /> instead.
/// </summary>
public static bool IsNullableType(this ITypeSymbol type) =>
type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T;

public static bool IsPointerOrFunctionPointer(this ITypeSymbol type) =>
type.TypeKind is TypeKind.Pointer or TypeKind.FunctionPointer;

[SuppressMessage("Style", "IDE0072:Add missing cases")]
private static bool SatisfiesConstructorConstraint(ITypeSymbol typeArgument) =>
typeArgument.TypeKind switch
{
TypeKind.Struct => true,
TypeKind.Enum => true,
TypeKind.Dynamic => true,

TypeKind.Class =>
HasPublicParameterlessConstructor((INamedTypeSymbol)typeArgument) && !typeArgument.IsAbstract,

TypeKind.TypeParameter =>
typeArgument is ITypeParameterSymbol tps
&& (tps.HasConstructorConstraint || tps.IsValueType),

_ => false,
};

private static bool HasPublicParameterlessConstructor(INamedTypeSymbol type)
{
foreach (var constructor in type.InstanceConstructors)
{
if (constructor.Parameters.Length == 0)
return constructor.DeclaredAccessibility == Accessibility.Public;
}

return false;
}

private static ITypeSymbol SubstituteType(Compilation compilation, ITypeSymbol type, ITypeParameterSymbol typeParameter, ITypeSymbol typeArgument)
{
return Visit(type);

ITypeSymbol Visit(ITypeSymbol type)
{
switch (type)
{
case ITypeParameterSymbol typeParameterSymbol:
return SymbolEqualityComparer.Default.Equals(typeParameterSymbol, typeParameter)
? typeArgument
: type;

case IArrayTypeSymbol { ElementType: var elementType, Rank: var rank } arrayTypeSymbol:
var visitedElementType = Visit(elementType);
return ReferenceEquals(elementType, visitedElementType)
? arrayTypeSymbol
: compilation.CreateArrayTypeSymbol(visitedElementType, rank);

case INamedTypeSymbol { OriginalDefinition: var originalDefinition, TypeArguments: var typeArguments } namedTypeSymbol:
var visitedTypeArguments = new ITypeSymbol[typeArguments.Length];
var anyChanged = false;
for (var i = 0; i < typeArguments.Length; i++)
{
var typeArgument = typeArguments[i];
var visited = Visit(typeArgument);
if (!ReferenceEquals(visited, typeArgument))
anyChanged = true;
visitedTypeArguments[i] = visited;
}

return anyChanged ? originalDefinition.Construct(visitedTypeArguments) : namedTypeSymbol;

default:
return type;
}
}
}
}
Loading

0 comments on commit 22c2dfe

Please sign in to comment.