// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Microsoft.Extensions.Options.Generators
{
/// <summary>
/// Holds an internal parser class that extracts necessary information for generating IValidateOptions.
/// </summary>
internal sealed class Parser
{
private const int NumValidationMethodArgs = 2;
private readonly CancellationToken _cancellationToken;
private readonly Compilation _compilation;
private readonly Action<Diagnostic> _reportDiagnostic;
private readonly SymbolHolder _symbolHolder;
private readonly OptionsSourceGenContext _optionsSourceGenContext;
// Synthesized validators are cached per model type. A model reached from a synchronous root and a model
// reached from an asynchronous root produce different generated types (the latter also emits ValidateAsync),
// so each model caches its synchronous and asynchronous validators separately. Keying by model type alone
// would let an asynchronous root reuse a synchronous-only validator (or vice versa) depending on discovery
// order, which would silently drop nested async validation.
private readonly Dictionary<ITypeSymbol, (ValidatorType? Sync, ValidatorType? Async)> _synthesizedValidators = new(SymbolEqualityComparer.Default);
private readonly HashSet<ITypeSymbol> _visitedModelTypes = new(SymbolEqualityComparer.Default);
// Tracks synthesized validator type names already handed out per namespace, so that a synchronous and an
// asynchronous synthesized validator whose default names collide (e.g. model "Foo" validated asynchronously
// and model "FooAsync" validated synchronously both default to "__FooAsyncValidator__") get deterministically
// uniquified instead of emitting two same-named types in the same namespace. Names are preserved unchanged
// when no collision occurs.
private readonly Dictionary<string, HashSet<string>> _usedSynthesizedValidatorNames = new(StringComparer.Ordinal);
public Parser(
Compilation compilation,
Action<Diagnostic> reportDiagnostic,
SymbolHolder symbolHolder,
OptionsSourceGenContext optionsSourceGenContext,
CancellationToken cancellationToken)
{
_compilation = compilation;
_cancellationToken = cancellationToken;
_reportDiagnostic = reportDiagnostic;
_symbolHolder = symbolHolder;
_optionsSourceGenContext = optionsSourceGenContext;
}
public IReadOnlyList<ValidatorType> GetValidatorTypes(IEnumerable<(TypeDeclarationSyntax TypeSyntax, SemanticModel SemanticModel)> classes)
{
var results = new List<ValidatorType>();
foreach (var group in classes.GroupBy(x => x.TypeSyntax.SyntaxTree))
{
SemanticModel? sm = null;
foreach (var typeDec in group)
{
TypeDeclarationSyntax syntax = typeDec.TypeSyntax;
_cancellationToken.ThrowIfCancellationRequested();
sm ??= typeDec.SemanticModel;
var validatorType = sm.GetDeclaredSymbol(syntax) as ITypeSymbol;
if (validatorType is not null)
{
if (validatorType.IsStatic)
{
Diag(DiagDescriptors.CantBeStaticClass, syntax.GetLocation());
continue;
}
_visitedModelTypes.Clear();
var modelTypes = GetModelTypes(validatorType);
if (modelTypes.Count == 0)
{
// validator doesn't implement IValidateOptions
Diag(DiagDescriptors.DoesntImplementIValidateOptions, syntax.GetLocation(), validatorType.Name);
continue;
}
var modelsValidatorTypeValidates = new List<ValidatedModel>(modelTypes.Count);
foreach (var modelType in modelTypes)
{
if (modelType.Kind == SymbolKind.ErrorType)
{
// the compiler will report this error for us
continue;
}
else
{
// keep track of the models we look at, to detect loops
_ = _visitedModelTypes.Add(modelType.WithNullableAnnotation(NullableAnnotation.None));
}
// Decide, per model, whether we additionally emit a ValidateAsync method. We do so when the
// validator type explicitly implements IAsyncValidateOptions<T> for this specific model. A
// multi-model validator therefore only gets async validation for the models it opted into.
// The async requirement is propagated to any synthesized child validators reached from this model.
bool wantsAsync = GetAsyncValidateOptionsInterfaceFor(validatorType, modelType) is not null;
bool generateValidate = true;
if (AlreadyImplementsValidateMethod(validatorType, modelType))
{
// A user-supplied Validate conflicts with generation. For an async validator, continue
// parsing so ValidateAsync can still be emitted and the compiler doesn't also report CS0535.
Diag(DiagDescriptors.AlreadyImplementsValidateMethod, syntax.GetLocation(), validatorType.Name);
generateValidate = false;
}
if (wantsAsync && AlreadyImplementsValidateAsyncMethod(validatorType, modelType))
{
// The user hand-wrote a matching ValidateAsync on an [OptionsValidator] type. This is an
// error (symmetric with SYSLIB1205 for a hand-written Validate): a generated synchronous
// Validate combined with a hand-written ValidateAsync would validate differently on the
// sync vs async access paths, silently skipping the generated attribute validation on the
// async path. Report it and skip only the async generation - the synchronous Validate is
// still generated so the sole failure surfaced is this diagnostic, not a cascading CS0535.
Diag(DiagDescriptors.AlreadyImplementsValidateAsyncMethod, syntax.GetLocation(), validatorType.Name);
wantsAsync = false;
}
bool generateAsync = wantsAsync;
if (!generateValidate && !generateAsync)
{
continue;
}
Location? modelTypeLocation = modelType.GetLocation();
Location lowerLocationInCompilation = modelTypeLocation is not null && modelTypeLocation.SourceTree is not null && _compilation.ContainsSyntaxTree(modelTypeLocation.SourceTree)
? modelTypeLocation
: syntax.GetLocation();
var membersToValidate = GetMembersToValidate(modelType, true, lowerLocationInCompilation, validatorType, generateAsync);
bool selfValidate = ModelSelfValidates(modelType);
bool selfValidateAsync = generateAsync && ModelSelfValidatesAsync(modelType);
if (membersToValidate.Count == 0 && !selfValidate && !selfValidateAsync)
{
// this type lacks any eligible members
Diag(DiagDescriptors.NoEligibleMembersFromValidator, syntax.GetLocation(), modelType.ToString(), validatorType.ToString());
continue;
}
modelsValidatorTypeValidates.Add(new ValidatedModel(
GetFQN(modelType),
modelType.Name,
selfValidate,
selfValidateAsync,
generateValidate,
generateAsync,
membersToValidate));
}
string keyword = GetTypeKeyword(validatorType);
// following code establishes the containment hierarchy for the generated type in terms of nested types
var parents = new List<string>();
var parent = syntax.Parent as TypeDeclarationSyntax;
while (parent is not null && IsAllowedKind(parent.Kind()))
{
parents.Add($"partial {GetTypeKeyword(parent)} {parent.Identifier}{parent.TypeParameterList} {parent.ConstraintClauses}");
parent = parent.Parent as TypeDeclarationSyntax;
}
parents.Reverse();
results.Add(new ValidatorType(
GetNamespace(validatorType),
GetMinimalFQN(validatorType),
GetMinimalFQNWithoutGenerics(validatorType),
keyword,
parents,
false,
modelsValidatorTypeValidates));
}
}
}
foreach (var entry in _synthesizedValidators.Values)
{
if (entry.Sync is not null)
{
results.Add(entry.Sync);
}
if (entry.Async is not null)
{
results.Add(entry.Async);
}
}
_synthesizedValidators.Clear();
_usedSynthesizedValidatorNames.Clear();
if (results.Count > 0 && _compilation is CSharpCompilation { LanguageVersion : LanguageVersion version and < LanguageVersion.CSharp8 })
{
// we only support C# 8.0 and above
Diag(DiagDescriptors.OptionsUnsupportedLanguageVersion, null, version.ToDisplayString(), LanguageVersion.CSharp8.ToDisplayString());
return new List<ValidatorType>();
}
return results;
}
private static bool IsAllowedKind(SyntaxKind kind) =>
kind == SyntaxKind.ClassDeclaration ||
kind == SyntaxKind.StructDeclaration ||
kind == SyntaxKind.RecordStructDeclaration ||
kind == SyntaxKind.RecordDeclaration;
private static string GetTypeKeyword(ITypeSymbol type)
{
if (type.IsReferenceType)
{
return type.IsRecord ? "record class" : "class";
}
return type.IsRecord ? "record struct" : "struct";
}
private static string GetTypeKeyword(TypeDeclarationSyntax type) =>
type.Kind() switch
{
SyntaxKind.ClassDeclaration => "class",
SyntaxKind.RecordDeclaration => "record class",
SyntaxKind.RecordStructDeclaration => "record struct",
_ => type.Keyword.ValueText,
};
// The namespace flows into namespace declarations and "global::"-qualified references in the generated
// code, so keyword segments (e.g. a namespace named @class) must be escaped to keep the output compiling.
private static readonly SymbolDisplayFormat _namespaceDisplayFormat = new(
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers);
private static string GetNamespace(ITypeSymbol type)
=> type.ContainingNamespace.IsGlobalNamespace ? string.Empty : type.ContainingNamespace.ToDisplayString(_namespaceDisplayFormat);
// AddMiscellaneousOptions must be used here rather than WithMiscellaneousOptions, which would replace
// the format's default options and drop EscapeKeywordIdentifiers, emitting keyword-named identifiers
// (e.g. a type declared as @class) unescaped into code that does not compile.
private static string GetFQN(ISymbol type)
=> type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier));
private static string GetMinimalFQN(ISymbol type)
=> type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat.AddGenericsOptions(SymbolDisplayGenericsOptions.IncludeTypeParameters));
private static string GetMinimalFQNWithoutGenerics(ISymbol type)
=> type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat.WithGenericsOptions(SymbolDisplayGenericsOptions.None));
/// <summary>
/// Checks whether the given validator already declares the generated <c>Validate</c> method.
/// </summary>
private bool AlreadyImplementsValidateMethod(INamespaceOrTypeSymbol validatorType, ISymbol modelType)
=> validatorType
.GetMembers()
.OfType<IMethodSymbol>()
.Any(m => MatchesGeneratedValidateSignature(m, "Validate", NumValidationMethodArgs, modelType, requireCancellationToken: false));
// Detects a user-supplied ValidateAsync(string, TModel, CancellationToken) overload so the generator doesn't emit
// a duplicate member for a validator that already provides its own asynchronous implementation.
private bool AlreadyImplementsValidateAsyncMethod(INamespaceOrTypeSymbol validatorType, ISymbol modelType)
=> validatorType
.GetMembers()
.OfType<IMethodSymbol>()
.Any(m => MatchesGeneratedValidateSignature(m, "ValidateAsync", NumValidationMethodArgs + 1, modelType, requireCancellationToken: true));
// Matches a method with the signature the generator would emit, whether declared implicitly or as an explicit
// interface implementation. The return type is intentionally ignored (C# does not overload on return type), and
// no display-string comparison is used.
//
// An explicit interface implementation is only considered a match when the interface member it implements
// belongs to the actual IValidateOptions{T}/IAsyncValidateOptions{T} interface for this specific model type.
// Otherwise an unrelated interface that happens to declare a same-named/same-shaped method (e.g. some
// application-defined IFoo.Validate(string, TModel)) would incorrectly suppress generation.
private bool MatchesGeneratedValidateSignature(IMethodSymbol method, string name, int parameterCount, ISymbol modelType, bool requireCancellationToken)
{
bool nameMatches;
if (method.ExplicitInterfaceImplementations.Length > 0)
{
INamedTypeSymbol? expectedInterface = requireCancellationToken ? _symbolHolder.AsyncValidateOptionsSymbol : _symbolHolder.ValidateOptionsSymbol;
nameMatches = expectedInterface is not null
&& method.ExplicitInterfaceImplementations.Any(impl =>
impl.Name == name
&& SymbolEqualityComparer.Default.Equals(impl.ContainingType.OriginalDefinition, expectedInterface)
&& impl.ContainingType.TypeArguments.Length == 1
&& SymbolEqualityComparer.Default.Equals(impl.ContainingType.TypeArguments[0], modelType));
}
else
{
nameMatches = method.Name == name;
}
if (method.Arity != 0
|| !nameMatches
|| method.Parameters.Length != parameterCount
|| method.Parameters[0].Type.SpecialType != SpecialType.System_String
|| method.Parameters.Any(p => p.RefKind != RefKind.None)
|| !SymbolEqualityComparer.Default.Equals(method.Parameters[1].Type, modelType))
{
return false;
}
return !requireCancellationToken || SymbolEqualityComparer.Default.Equals(method.Parameters[parameterCount - 1].Type, _symbolHolder.CancellationTokenSymbol);
}
/// <summary>
/// Checks whether the given type contain any unbound generic type arguments.
/// </summary>
private static bool HasOpenGenerics(ITypeSymbol type, out string genericType)
{
if (type is INamedTypeSymbol mt)
{
if (mt.IsGenericType)
{
foreach (var ta in mt.TypeArguments)
{
if (ta.TypeKind == TypeKind.TypeParameter)
{
genericType = ta.Name;
return true;
}
}
}
}
else if (type is ITypeParameterSymbol)
{
genericType = type.Name;
return true;
}
else if (type is IArrayTypeSymbol ats)
{
return HasOpenGenerics(ats.ElementType, out genericType);
}
genericType = string.Empty;
return false;
}
private ITypeSymbol? GetEnumeratedType(ITypeSymbol type)
{
if (type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
{
// extract the T from a Nullable<T>
type = ((INamedTypeSymbol)type).TypeArguments[0];
}
// Check first if the type is IEnumerable<T> interface
if (SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, _symbolHolder.GenericIEnumerableSymbol))
{
return ((INamedTypeSymbol)type).TypeArguments[0];
}
// Check first if the type implement IEnumerable<T> interface
foreach (var implementingInterface in type.AllInterfaces)
{
if (SymbolEqualityComparer.Default.Equals(implementingInterface.OriginalDefinition, _compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T)))
{
return implementingInterface.TypeArguments.First();
}
}
return null;
}
private List<ValidatedMember> GetMembersToValidate(ITypeSymbol modelType, bool speculate, Location lowerLocationInCompilation, ITypeSymbol validatorType, bool isAsync)
{
// make a list of the most derived members in the model type
if (modelType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
{
// extract the T from a Nullable<T>
modelType = ((INamedTypeSymbol)modelType).TypeArguments[0];
}
var members = modelType.GetMembers().ToList();
var addedMembers = new HashSet<string>(members.Select(m => m.Name));
var baseType = modelType.BaseType;
while (baseType is not null && baseType.SpecialType != SpecialType.System_Object
// We ascend the hierarchy only if the base type is a user-defined type, as validating properties of system types is unnecessary.
// This approach prevents generating warnings for properties defined in system types.
// For example, in the case of `MyModel : Dictionary<string, string>`, this avoids warnings for properties like Keys and Values,
// where a missing ValidateEnumeratedItemsAttribute might be incorrectly inferred.
&& !baseType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).StartsWith("global::System.", StringComparison.Ordinal))
{
var baseMembers = baseType.GetMembers().Where(m => !addedMembers.Contains(m.Name));
members.AddRange(baseMembers);
addedMembers.UnionWith(baseMembers.Select(m => m.Name));
baseType = baseType.BaseType;
}
var membersToValidate = new List<ValidatedMember>();
foreach (var member in members)
{
Location? memberLocation = member.GetLocation();
Location location = memberLocation is not null && memberLocation.SourceTree is not null && _compilation.ContainsSyntaxTree(memberLocation.SourceTree)
? memberLocation
: lowerLocationInCompilation;
var memberInfo = GetMemberInfo(member, speculate, location, modelType, validatorType, isAsync);
if (memberInfo is not null)
{
if (member.DeclaredAccessibility != Accessibility.Public)
{
Diag(DiagDescriptors.MemberIsInaccessible, member.Locations.First(), member.Name);
continue;
}
membersToValidate.Add(memberInfo);
}
}
return membersToValidate;
}
private ValidatedMember? GetMemberInfo(ISymbol member, bool speculate, Location location, ITypeSymbol modelType, ITypeSymbol validatorType, bool isAsync)
{
ITypeSymbol memberType;
switch (member)
{
case IPropertySymbol prop:
memberType = prop.Type;
break;
/* The runtime doesn't support fields validation yet. If we allow that in the future, we need to add the following code back.
case IFieldSymbol field:
if (field.AssociatedSymbol is not null)
{
// a backing field for a property, don't need those
return null;
}
memberType = field.Type;
break;
*/
default:
// we only care about properties
return null;
}
var validationAttrs = new List<ValidationAttributeInfo>();
string? transValidatorTypeName = null;
string? enumerationValidatorTypeName = null;
var enumeratedIsNullable = false;
var enumeratedIsValueType = false;
var enumeratedMayBeNull = false;
var transValidatorIsSynthetic = false;
var enumerationValidatorIsSynthetic = false;
var transValidatorEmitsAsync = false;
var enumerationValidatorEmitsAsync = false;
string? transValidatorAsyncInterfaceType = null;
string? enumerationValidatorAsyncInterfaceType = null;
foreach (var attribute in member.GetAttributes().Where(a => a.AttributeClass is not null))
{
var attributeType = attribute.AttributeClass!;
var attrLoc = attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation();
if (SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.ValidateObjectMembersAttributeSymbol))
{
if (HasOpenGenerics(memberType, out var genericType))
{
Diag(DiagDescriptors.CantUseWithGenericTypes, attrLoc, genericType);
#pragma warning disable S1226 // Method parameters, caught exceptions and foreach variables' initial values should not be ignored
speculate = false;
#pragma warning restore S1226 // Method parameters, caught exceptions and foreach variables' initial values should not be ignored
continue;
}
if (attribute.ConstructorArguments.Length == 1)
{
var transValidatorType = attribute.ConstructorArguments[0].Value as INamedTypeSymbol;
if (transValidatorType is not null)
{
if (CanValidate(transValidatorType, memberType))
{
if (transValidatorType.Constructors.Where(c => !c.Parameters.Any()).Any())
{
transValidatorTypeName = transValidatorType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
INamedTypeSymbol? asyncInterface = isAsync ? GetAsyncValidateOptionsInterfaceFor(transValidatorType, memberType) : null;
transValidatorEmitsAsync = asyncInterface is not null;
transValidatorAsyncInterfaceType = asyncInterface is null ? null : GetFQN(asyncInterface);
}
else
{
Diag(DiagDescriptors.ValidatorsNeedSimpleConstructor, attrLoc, transValidatorType.Name);
}
}
else
{
Diag(DiagDescriptors.DoesntImplementIValidateOptions, attrLoc, transValidatorType.Name, memberType.Name);
}
}
else
{
Diag(DiagDescriptors.NullValidatorType, attrLoc);
}
}
else if (!_visitedModelTypes.Add(memberType.WithNullableAnnotation(NullableAnnotation.None)))
{
Diag(DiagDescriptors.CircularTypeReferences, attrLoc, memberType.ToString());
speculate = false;
continue;
}
if (transValidatorTypeName == null)
{
transValidatorIsSynthetic = true;
transValidatorTypeName = AddSynthesizedValidator(memberType, member, location, validatorType, isAsync, out transValidatorEmitsAsync);
}
// pop the stack
_ = _visitedModelTypes.Remove(memberType.WithNullableAnnotation(NullableAnnotation.None));
}
else if (SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.ValidateEnumeratedItemsAttributeSymbol))
{
var enumeratedType = GetEnumeratedType(memberType);
if (enumeratedType == null)
{
Diag(DiagDescriptors.NotEnumerableType, attrLoc, memberType);
speculate = false;
continue;
}
enumeratedIsNullable = enumeratedType.IsReferenceType || enumeratedType.NullableAnnotation == NullableAnnotation.Annotated;
enumeratedIsValueType = enumeratedType.IsValueType;
enumeratedMayBeNull = enumeratedType.NullableAnnotation == NullableAnnotation.Annotated;
if (HasOpenGenerics(enumeratedType, out var genericType))
{
Diag(DiagDescriptors.CantUseWithGenericTypes, attrLoc, genericType);
speculate = false;
continue;
}
if (attribute.ConstructorArguments.Length == 1)
{
var enumerationValidatorType = attribute.ConstructorArguments[0].Value as INamedTypeSymbol;
if (enumerationValidatorType is not null)
{
if (CanValidate(enumerationValidatorType, enumeratedType))
{
if (enumerationValidatorType.Constructors.Where(c => c.Parameters.Length == 0).Any())
{
enumerationValidatorTypeName = enumerationValidatorType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
INamedTypeSymbol? asyncInterface = isAsync ? GetAsyncValidateOptionsInterfaceFor(enumerationValidatorType, enumeratedType) : null;
enumerationValidatorEmitsAsync = asyncInterface is not null;
enumerationValidatorAsyncInterfaceType = asyncInterface is null ? null : GetFQN(asyncInterface);
}
else
{
Diag(DiagDescriptors.ValidatorsNeedSimpleConstructor, attrLoc, enumerationValidatorType.Name);
}
}
else
{
Diag(DiagDescriptors.DoesntImplementIValidateOptions, attrLoc, enumerationValidatorType.Name, enumeratedType.Name);
}
}
else
{
Diag(DiagDescriptors.NullValidatorType, attrLoc);
}
}
else if (!_visitedModelTypes.Add(enumeratedType.WithNullableAnnotation(NullableAnnotation.None)))
{
Diag(DiagDescriptors.CircularTypeReferences, attrLoc, enumeratedType.ToString());
speculate = false;
continue;
}
if (enumerationValidatorTypeName == null)
{
enumerationValidatorIsSynthetic = true;
enumerationValidatorTypeName = AddSynthesizedValidator(enumeratedType, member, location, validatorType, isAsync, out enumerationValidatorEmitsAsync);
}
// pop the stack
_ = _visitedModelTypes.Remove(enumeratedType.WithNullableAnnotation(NullableAnnotation.None));
}
else if (ConvertTo(attributeType, _symbolHolder.ValidationAttributeSymbol))
{
if (!_compilation.IsSymbolAccessibleWithin(attributeType, validatorType))
{
Diag(DiagDescriptors.InaccessibleValidationAttribute, location, attributeType.Name, member.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), validatorType.Name);
continue;
}
string attributeFullQualifiedName = attributeType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
if (SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.MaxLengthAttributeSymbol) ||
SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.MinLengthAttributeSymbol) ||
(_symbolHolder.LengthAttributeSymbol is not null && SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.LengthAttributeSymbol)))
{
if (!LengthBasedAttributeIsTrackedForSubstitution(memberType, location, attributeType, ref attributeFullQualifiedName))
{
continue;
}
}
else if (SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.CompareAttributeSymbol))
{
TrackCompareAttributeForSubstitution(attribute, modelType, ref attributeFullQualifiedName);
}
else if (SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.RangeAttributeSymbol))
{
TrackRangeAttributeForSubstitution(attribute, memberType, ref attributeFullQualifiedName);
}
bool isAsyncAttribute = _symbolHolder.AsyncValidationAttributeSymbol is not null
&& ConvertTo(attributeType, _symbolHolder.AsyncValidationAttributeSymbol);
var validationAttr = new ValidationAttributeInfo(attributeFullQualifiedName, isAsyncAttribute);
validationAttrs.Add(validationAttr);
ImmutableArray<IParameterSymbol> parameters = attribute.AttributeConstructor?.Parameters ?? ImmutableArray<IParameterSymbol>.Empty;
bool lastParameterDeclaredWithParamsKeyword = parameters.Length > 0 && parameters[parameters.Length - 1].IsParams;
ImmutableArray<TypedConstant> arguments = attribute.ConstructorArguments;
for (int i = 0; i < arguments.Length; i++)
{
TypedConstant argument = arguments[i];
if (argument.Kind == TypedConstantKind.Array)
{
bool isParams = lastParameterDeclaredWithParamsKeyword && i == arguments.Length - 1;
validationAttr.ConstructorArguments.Add(GetArrayArgumentExpression(argument.Values, isParams));
}
else
{
validationAttr.ConstructorArguments.Add(GetArgumentExpression(argument.Type!, argument.Value));
}
}
foreach (var namedArgument in attribute.NamedArguments)
{
if (namedArgument.Value.Kind == TypedConstantKind.Array)
{
bool isParams = lastParameterDeclaredWithParamsKeyword && namedArgument.Key == parameters[parameters.Length - 1].Name;
validationAttr.Properties.Add(namedArgument.Key, GetArrayArgumentExpression(namedArgument.Value.Values, isParams));
}
else
{
validationAttr.Properties.Add(namedArgument.Key, GetArgumentExpression(namedArgument.Value.Type!, namedArgument.Value.Value));
}
}
}
}
bool validationAttributeIsApplied = validationAttrs.Count > 0 || transValidatorTypeName is not null || enumerationValidatorTypeName is not null;
if (member.IsStatic)
{
// generate a warning if the member is const/static and has a validation attribute applied
if (validationAttributeIsApplied)
{
Diag(DiagDescriptors.CantValidateStaticOrConstMember, location, member.Name);
}
// don't validate the member in any case
return null;
}
// generate a warning if the field/property seems like it should be transitively validated
if (transValidatorTypeName == null && speculate && memberType.SpecialType == SpecialType.None)
{
if (!HasOpenGenerics(memberType, out var genericType))
{
var membersToValidate = GetMembersToValidate(memberType, false, location, validatorType, isAsync);
if (membersToValidate.Count > 0)
{
Diag(DiagDescriptors.PotentiallyMissingTransitiveValidation, location, memberType.Name, member.Name);
}
}
}
// generate a warning if the field/property seems like it should be enumerated
if (enumerationValidatorTypeName == null && speculate && memberType.SpecialType != SpecialType.System_String)
{
var enumeratedType = GetEnumeratedType(memberType);
if (enumeratedType is not null)
{
if (!HasOpenGenerics(enumeratedType, out var genericType))
{
var membersToValidate = GetMembersToValidate(enumeratedType, false, location, validatorType, isAsync);
if (membersToValidate.Count > 0)
{
Diag(DiagDescriptors.PotentiallyMissingEnumerableValidation, location, enumeratedType.Name, member.Name);
}
}
}
}
if (validationAttributeIsApplied)
{
return new(
member.Name,
validationAttrs,
transValidatorTypeName,
transValidatorIsSynthetic,
enumerationValidatorTypeName,
enumerationValidatorIsSynthetic,
memberType.IsReferenceType || memberType.NullableAnnotation == NullableAnnotation.Annotated,
memberType.IsValueType,
enumeratedIsNullable,
enumeratedIsValueType,
enumeratedMayBeNull,
transValidatorEmitsAsync,
enumerationValidatorEmitsAsync,
transValidatorAsyncInterfaceType,
enumerationValidatorAsyncInterfaceType);
}
return null;
}
private bool LengthBasedAttributeIsTrackedForSubstitution(ITypeSymbol memberType, Location location, ITypeSymbol attributeType, ref string attributeFullQualifiedName)
{
if (memberType.SpecialType == SpecialType.System_String || ConvertTo(memberType, _symbolHolder.ICollectionSymbol))
{
_optionsSourceGenContext.EnsureTrackingAttribute(attributeType.Name, createValue: false, out _);
}
else if (ParserUtilities.TypeHasProperty(memberType, "Count", SpecialType.System_Int32))
{
_optionsSourceGenContext.EnsureTrackingAttribute(attributeType.Name, createValue: true, out HashSet<object>? trackedTypeList);
trackedTypeList!.Add(memberType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
}
else
{
Diag(DiagDescriptors.IncompatibleWithTypeForValidationAttribute, location, attributeType.Name, memberType.Name);
return false;
}
attributeFullQualifiedName = $"{Emitter.StaticGeneratedValidationAttributesClassesNamespace}.{Emitter.StaticAttributeClassNamePrefix}{_optionsSourceGenContext.Suffix}_{attributeType.Name}";
return true;
}
private void TrackCompareAttributeForSubstitution(AttributeData attribute, ITypeSymbol modelType, ref string attributeFullQualifiedName)
{
ImmutableArray<IParameterSymbol> constructorParameters = attribute.AttributeConstructor?.Parameters ?? ImmutableArray<IParameterSymbol>.Empty;
if (constructorParameters.Length == 1 && constructorParameters[0].Name == "otherProperty" && constructorParameters[0].Type.SpecialType == SpecialType.System_String)
{
_optionsSourceGenContext.EnsureTrackingAttribute(attribute.AttributeClass!.Name, createValue: true, out HashSet<object>? trackedTypeList);
trackedTypeList!.Add((modelType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), (string)attribute.ConstructorArguments[0].Value!));
attributeFullQualifiedName = $"{Emitter.StaticGeneratedValidationAttributesClassesNamespace}.{Emitter.StaticAttributeClassNamePrefix}{_optionsSourceGenContext.Suffix}_{attribute.AttributeClass!.Name}";
}
}
private void TrackRangeAttributeForSubstitution(AttributeData attribute, ITypeSymbol memberType, ref string attributeFullQualifiedName)
{
ImmutableArray<IParameterSymbol> constructorParameters = attribute.AttributeConstructor?.Parameters ?? ImmutableArray<IParameterSymbol>.Empty;
ITypeSymbol? argumentType = null;
bool hasTimeSpanType = false;
ITypeSymbol typeSymbol = memberType;
if (typeSymbol.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
{
typeSymbol = ((INamedTypeSymbol)typeSymbol).TypeArguments[0];
}
if (constructorParameters.Length == 2)
{
if (OptionsSourceGenContext.IsConvertibleBasicType(typeSymbol))
{
argumentType = constructorParameters[0].Type;
}
}
else if (constructorParameters.Length == 3)
{
object? argumentValue = null;
for (int i = 0; i < constructorParameters.Length; i++)
{
if (constructorParameters[i].Name == "type")
{
argumentValue = attribute.ConstructorArguments[i].Value;
break;
}
}
if (argumentValue is INamedTypeSymbol namedTypeSymbol)
{
// When type is provided as a parameter, it has to match the property type.
if (OptionsSourceGenContext.IsConvertibleBasicType(namedTypeSymbol) && typeSymbol.SpecialType == namedTypeSymbol.SpecialType)
{
argumentType = namedTypeSymbol;
}
else if (SymbolEqualityComparer.Default.Equals(namedTypeSymbol, _symbolHolder.TimeSpanSymbol) &&
(SymbolEqualityComparer.Default.Equals(typeSymbol, _symbolHolder.TimeSpanSymbol) || typeSymbol.SpecialType == SpecialType.System_String))
{
hasTimeSpanType = true;
argumentType = _symbolHolder.TimeSpanSymbol;
}
}
}
if (argumentType is not null)
{
_optionsSourceGenContext.EnsureTrackingAttribute(attribute.AttributeClass!.Name, createValue: hasTimeSpanType, out _);
attributeFullQualifiedName = $"{Emitter.StaticGeneratedValidationAttributesClassesNamespace}.{Emitter.StaticAttributeClassNamePrefix}{_optionsSourceGenContext.Suffix}_{attribute.AttributeClass!.Name}";
}
}
private string? AddSynthesizedValidator(ITypeSymbol modelType, ISymbol member, Location location, ITypeSymbol validatorType, bool isAsync, out bool emitsAsync)
{
emitsAsync = false;
var mt = modelType.WithNullableAnnotation(NullableAnnotation.None);
if (mt.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
{
// extract the T from a Nullable<T>
mt = ((INamedTypeSymbol)mt).TypeArguments[0];
}
_synthesizedValidators.TryGetValue(mt, out var cached);
ValidatorType? reusable = isAsync ? cached.Async : cached.Sync;
if (reusable is not null)
{
// A validator matching the requested capability was already synthesized for this model, so reuse it.
emitsAsync = isAsync;
return "global::" + (reusable.Namespace.Length > 0 ? reusable.Namespace + "." + reusable.Name : reusable.Name);
}
bool selfValidate = ModelSelfValidates(mt);
bool selfValidateAsync = isAsync && ModelSelfValidatesAsync(mt);
var membersToValidate = GetMembersToValidate(mt, true, location, validatorType, isAsync);
if (membersToValidate.Count == 0 && !selfValidate && !selfValidateAsync)
{
// this type lacks any eligible members
Diag(DiagDescriptors.NoEligibleMember, location, mt.ToString(), member.ToString());
return null;
}
var model = new ValidatedModel(
GetFQN(mt),
mt.Name,
selfValidate,
selfValidateAsync,
GenerateValidateMethod: true,
isAsync,
membersToValidate);
// Asynchronous validators get a distinct name so a model reached from both a synchronous and an
// asynchronous root can emit two non-conflicting synthesized types. Synchronous naming is unchanged.
string @namespace = GetNamespace(mt);
string candidateName = "__" + mt.Name + (isAsync ? "AsyncValidator__" : "Validator__");
string validatorTypeName = GetUniqueSynthesizedValidatorName(@namespace, candidateName);
var result = new ValidatorType(
@namespace,
validatorTypeName,
validatorTypeName,
"class",
new List<string>(),
true,
new[] { model });
_synthesizedValidators[mt] = isAsync ? (cached.Sync, result) : (result, cached.Async);
emitsAsync = isAsync;
return "global::" + (result.Namespace.Length > 0 ? result.Namespace + "." + result.Name : result.Name);
}
// Returns candidateName unchanged unless it was already handed out in the same namespace, in which case a
// deterministic numeric suffix is appended until the name is unique. This keeps existing generated names
// stable when no collision occurs, while still guaranteeing the emitted source compiles when two distinct
// model types happen to produce the same default synthesized validator name (see field comment above).
private string GetUniqueSynthesizedValidatorName(string @namespace, string candidateName)
{
if (!_usedSynthesizedValidatorNames.TryGetValue(@namespace, out var namesInNamespace))
{
namesInNamespace = new HashSet<string>(StringComparer.Ordinal);
_usedSynthesizedValidatorNames[@namespace] = namesInNamespace;
}
string uniqueName = candidateName;
for (int suffix = 2; !namesInNamespace.Add(uniqueName); suffix++)
{
uniqueName = candidateName + "_" + suffix.ToString(CultureInfo.InvariantCulture);
}
return uniqueName;
}
private bool ConvertTo(ITypeSymbol source, ITypeSymbol dest)
{
var conversion = _compilation.ClassifyConversion(source, dest);
return conversion.IsReference && conversion.IsImplicit;
}
private bool ModelSelfValidates(ITypeSymbol modelType)
{
foreach (var implementingInterface in modelType.AllInterfaces)
{
if (SymbolEqualityComparer.Default.Equals(implementingInterface.OriginalDefinition, _symbolHolder.IValidatableObjectSymbol))
{
return true;
}
}
return false;
}
private bool ModelSelfValidatesAsync(ITypeSymbol modelType)
{
if (_symbolHolder.IAsyncValidatableObjectSymbol is null)
{
return false;
}
foreach (var implementingInterface in modelType.AllInterfaces)
{
if (SymbolEqualityComparer.Default.Equals(implementingInterface.OriginalDefinition, _symbolHolder.IAsyncValidatableObjectSymbol))
{
return true;
}
}
return false;
}
private INamedTypeSymbol? GetAsyncValidateOptionsInterfaceFor(ITypeSymbol validatorType, ITypeSymbol modelType)
{
if (_symbolHolder.AsyncValidateOptionsSymbol is null)
{
return null;
}
foreach (var implementingInterface in validatorType.AllInterfaces)
{
if (SymbolEqualityComparer.Default.Equals(implementingInterface.OriginalDefinition, _symbolHolder.AsyncValidateOptionsSymbol)
&& SymbolEqualityComparer.Default.Equals(implementingInterface.TypeArguments.First(), modelType))
{
return implementingInterface;
}
}
return null;
}
private List<ITypeSymbol> GetModelTypes(ITypeSymbol validatorType)
{
var result = new List<ITypeSymbol>();
foreach (var implementingInterface in validatorType.AllInterfaces)
{
if (SymbolEqualityComparer.Default.Equals(implementingInterface.OriginalDefinition, _symbolHolder.ValidateOptionsSymbol))
{
result.Add(implementingInterface.TypeArguments.First());
}
}
return result;
}
private bool CanValidate(ITypeSymbol validatorType, ISymbol modelType)
{
foreach (var implementingInterface in validatorType.AllInterfaces)
{
if (SymbolEqualityComparer.Default.Equals(implementingInterface.OriginalDefinition, _symbolHolder.ValidateOptionsSymbol))
{
var t = implementingInterface.TypeArguments.First();
if (SymbolEqualityComparer.Default.Equals(modelType, t))
{
return true;
}
}
}
return false;
}
private string GetArrayArgumentExpression(ImmutableArray<Microsoft.CodeAnalysis.TypedConstant> value, bool isParams)
{
var sb = new StringBuilder();
if (!isParams)
{
sb.Append("new[] { ");
}
for (int i = 0; i < value.Length; i++)
{
sb.Append(GetArgumentExpression(value[i].Type!, value[i].Value));
if (i < value.Length - 1)
{
sb.Append(", ");
}
}
if (!isParams)
{
sb.Append(" }");
}
return sb.ToString();
}
private string GetArgumentExpression(ITypeSymbol type, object? value)
{
if (value == null)
{
return "null";
}
if (type.SpecialType == SpecialType.System_Boolean)
{
return (bool)value ? "true" : "false";
}
if (SymbolEqualityComparer.Default.Equals(type, _symbolHolder.TypeSymbol) &&
value is INamedTypeSymbol sym)
{
return $"typeof({sym.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)})";
}
if (type.SpecialType == SpecialType.System_String)
{
return $@"""{EscapeString(value.ToString()!)}""";
}
if (type.SpecialType == SpecialType.System_Char)
{
return $@"'{EscapeString(value.ToString()!)}'";
}
return $"({type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}){Convert.ToString(value, CultureInfo.InvariantCulture)}";
}
private static readonly char[] _specialChars = { '\n', '\r', '"', '\\' };
private static string EscapeString(string s)
{
int index = s.IndexOfAny(_specialChars);
if (index < 0)
{
return s;
}
var sb = new StringBuilder(s.Length);
_ = sb.Append(s, 0, index);
while (index < s.Length)
{
_ = s[index] switch
{
'\n' => sb.Append("\\n"),
'\r' => sb.Append("\\r"),
'"' => sb.Append("\\\""),
'\\' => sb.Append("\\\\"),
var other => sb.Append(other),
};
index++;
}
return sb.ToString();
}
private void Diag(DiagnosticDescriptor desc, Location? location) =>
_reportDiagnostic(Diagnostic.Create(desc, location, Array.Empty<object?>()));
private void Diag(DiagnosticDescriptor desc, Location? location, params object?[]? messageArgs) =>
_reportDiagnostic(Diagnostic.Create(desc, location, messageArgs));
}
}