Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[release/8.0] Fix Options Source Gen Trimming Issues #93193

Merged
merged 2 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/project/list-of-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ The diagnostic id values reserved for .NET Libraries analyzer warnings are `SYSL
| __`SYSLIB1214`__ | Options validation generator: Can't validate constants, static fields or properties. |
| __`SYSLIB1215`__ | Options validation generator: Validation attribute on the member is inaccessible from the validator type. |
| __`SYSLIB1216`__ | C# language version not supported by the options validation source generator. |
| __`SYSLIB1217`__ | *_`SYSLIB1201`-`SYSLIB1219` reserved for Microsoft.Extensions.Options.SourceGeneration.* |
| __`SYSLIB1217`__ | The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. |
| __`SYSLIB1218`__ | *_`SYSLIB1201`-`SYSLIB1219` reserved for Microsoft.Extensions.Options.SourceGeneration.* |
| __`SYSLIB1219`__ | *_`SYSLIB1201`-`SYSLIB1219` reserved for Microsoft.Extensions.Options.SourceGeneration.* |
| __`SYSLIB1220`__ | JsonSourceGenerator encountered a [JsonConverterAttribute] with an invalid type argument. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,12 @@ internal sealed class DiagDescriptors : DiagDescriptorsBase
messageFormat: SR.OptionsUnsupportedLanguageVersionMessage,
category: Category,
defaultSeverity: DiagnosticSeverity.Error);

public static DiagnosticDescriptor IncompatibleWithTypeForValidationAttribute { get; } = Make(
id: "SYSLIB1217",
title: SR.TypeCannotBeUsedWithTheValidationAttributeTitle,
messageFormat: SR.TypeCannotBeUsedWithTheValidationAttributeMessage,
category: Category,
defaultSeverity: DiagnosticSeverity.Warning);
}
}
445 changes: 417 additions & 28 deletions src/libraries/Microsoft.Extensions.Options/gen/Emitter.cs

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions src/libraries/Microsoft.Extensions.Options/gen/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ private static void HandleAnnotatedTypes(Compilation compilation, ImmutableArray
return;
}

var parser = new Parser(compilation, context.ReportDiagnostic, symbolHolder!, context.CancellationToken);
OptionsSourceGenContext optionsSourceGenContext = new(compilation);

var parser = new Parser(compilation, context.ReportDiagnostic, symbolHolder!, optionsSourceGenContext, context.CancellationToken);

var validatorTypes = parser.GetValidatorTypes(types);
if (validatorTypes.Count > 0)
{
var emitter = new Emitter(compilation);
var emitter = new Emitter(compilation, symbolHolder!, optionsSourceGenContext);
var result = emitter.Emit(validatorTypes, context.CancellationToken);

context.AddSource("Validators.g.cs", SourceText.From(result, Encoding.UTF8));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<Compile Include="Model\ValidatedModel.cs" />
<Compile Include="Model\ValidationAttributeInfo.cs" />
<Compile Include="Model\ValidatorType.cs" />
<Compile Include="OptionsSourceGenContext.cs" />
<Compile Include="Parser.cs" />
<Compile Include="ParserUtilities.cs" />
<Compile Include="SymbolHolder.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.Versioning;

namespace Microsoft.Extensions.Options.Generators
{
internal sealed class OptionsSourceGenContext
{
public OptionsSourceGenContext(Compilation compilation)
{
IsLangVersion11AndAbove = ((CSharpCompilation)compilation).LanguageVersion >= Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp11;
ClassModifier = IsLangVersion11AndAbove ? "file" : "internal";
Suffix = IsLangVersion11AndAbove ? "" : $"_{GetNonRandomizedHashCode(compilation.SourceModule.Name):X8}";
}

internal string Suffix { get; }
internal string ClassModifier { get; }
internal bool IsLangVersion11AndAbove { get; }
internal Dictionary<string, HashSet<object>?> AttributesToGenerate { get; set; } = new Dictionary<string, HashSet<object>?>();

internal void EnsureTrackingAttribute(string attributeName, bool createValue, out HashSet<object>? value)
{
bool exist = AttributesToGenerate.TryGetValue(attributeName, out value);
if (value is null)
{
if (createValue)
{
value = new HashSet<object>();
}

if (!exist || createValue)
{
AttributesToGenerate[attributeName] = value;
}
}
}

internal static bool IsConvertibleBasicType(ITypeSymbol typeSymbol)
{
return typeSymbol.SpecialType switch
{
SpecialType.System_Boolean => true,
SpecialType.System_Byte => true,
SpecialType.System_Char => true,
SpecialType.System_DateTime => true,
SpecialType.System_Decimal => true,
SpecialType.System_Double => true,
SpecialType.System_Int16 => true,
SpecialType.System_Int32 => true,
SpecialType.System_Int64 => true,
SpecialType.System_SByte => true,
SpecialType.System_Single => true,
SpecialType.System_UInt16 => true,
SpecialType.System_UInt32 => true,
SpecialType.System_UInt64 => true,
SpecialType.System_String => true,
_ => false,
};
}

/// <summary>
/// Returns a non-randomized hash code for the given string.
/// We always return a positive value.
/// </summary>
internal static int GetNonRandomizedHashCode(string s)
{
uint result = 2166136261u;
foreach (char c in s)
{
result = (c ^ result) * 16777619;
}

return Math.Abs((int)result);
}
}
}
103 changes: 99 additions & 4 deletions src/libraries/Microsoft.Extensions.Options/gen/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,22 @@ internal sealed class Parser
private readonly Compilation _compilation;
private readonly Action<Diagnostic> _reportDiagnostic;
private readonly SymbolHolder _symbolHolder;
private readonly OptionsSourceGenContext _optionsSourceGenContext;
private readonly Dictionary<ITypeSymbol, ValidatorType> _synthesizedValidators = new(SymbolEqualityComparer.Default);
private readonly HashSet<ITypeSymbol> _visitedModelTypes = new(SymbolEqualityComparer.Default);

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)
Expand Down Expand Up @@ -288,7 +291,7 @@ private List<ValidatedMember> GetMembersToValidate(ITypeSymbol modelType, bool s
? memberLocation
: lowerLocationInCompilation;

var memberInfo = GetMemberInfo(member, speculate, location, validatorType);
var memberInfo = GetMemberInfo(member, speculate, location, modelType, validatorType);
if (memberInfo is not null)
{
if (member.DeclaredAccessibility != Accessibility.Public)
Expand All @@ -304,7 +307,7 @@ private List<ValidatedMember> GetMembersToValidate(ITypeSymbol modelType, bool s
return membersToValidate;
}

private ValidatedMember? GetMemberInfo(ISymbol member, bool speculate, Location location, ITypeSymbol validatorType)
private ValidatedMember? GetMemberInfo(ISymbol member, bool speculate, Location location, ITypeSymbol modelType, ITypeSymbol validatorType)
{
ITypeSymbol memberType;
switch (member)
Expand All @@ -325,7 +328,7 @@ private List<ValidatedMember> GetMembersToValidate(ITypeSymbol modelType, bool s
break;
*/
default:
// we only care about properties and fields
// we only care about properties
return null;
}

Expand Down Expand Up @@ -467,7 +470,26 @@ private List<ValidatedMember> GetMembersToValidate(ITypeSymbol modelType, bool s
continue;
}

var validationAttr = new ValidationAttributeInfo(attributeType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
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);
}

var validationAttr = new ValidationAttributeInfo(attributeFullQualifiedName);
validationAttrs.Add(validationAttr);

ImmutableArray<IParameterSymbol> parameters = attribute.AttributeConstructor?.Parameters ?? ImmutableArray<IParameterSymbol>.Empty;
Expand Down Expand Up @@ -567,6 +589,79 @@ private List<ValidatedMember> GetMembersToValidate(ITypeSymbol modelType, bool s
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;
SpecialType argumentSpecialType = SpecialType.None;
if (constructorParameters.Length == 2)
{
argumentSpecialType = constructorParameters[0].Type.SpecialType;
}
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 && OptionsSourceGenContext.IsConvertibleBasicType(namedTypeSymbol))
{
argumentSpecialType = namedTypeSymbol.SpecialType;
}
}

ITypeSymbol typeSymbol = memberType;
if (typeSymbol.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
{
typeSymbol = ((INamedTypeSymbol)typeSymbol).TypeArguments[0];
}

if (argumentSpecialType != SpecialType.None &&
OptionsSourceGenContext.IsConvertibleBasicType(typeSymbol) &&
(constructorParameters.Length != 3 || typeSymbol.SpecialType == argumentSpecialType)) // When type is provided as a parameter, it has to match the property type.
{
_optionsSourceGenContext.EnsureTrackingAttribute(attribute.AttributeClass!.Name, createValue: false, out _);
attributeFullQualifiedName = $"{Emitter.StaticGeneratedValidationAttributesClassesNamespace}.{Emitter.StaticAttributeClassNamePrefix}{_optionsSourceGenContext.Suffix}_{attribute.AttributeClass!.Name}";
}
}

private string? AddSynthesizedValidator(ITypeSymbol modelType, ISymbol member, Location location, ITypeSymbol validatorType)
{
var mt = modelType.WithNullableAnnotation(NullableAnnotation.None);
Expand Down
23 changes: 23 additions & 0 deletions src/libraries/Microsoft.Extensions.Options/gen/ParserUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,29 @@ internal static bool ImplementsInterface(this ITypeSymbol type, ITypeSymbol inte
return false;
}

internal static bool TypeHasProperty(ITypeSymbol typeSymbol, string propertyName, SpecialType returnType)
{
ITypeSymbol? type = typeSymbol;
do
{
if (type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
{
type = ((INamedTypeSymbol)type).TypeArguments[0]; // extract the T from a Nullable<T>
}

if (type.GetMembers(propertyName).OfType<IPropertySymbol>().Any(property =>
property.Type.SpecialType == returnType && property.DeclaredAccessibility == Accessibility.Public &&
!property.IsStatic && property.GetMethod != null && property.Parameters.IsEmpty))
{
return true;
}

type = type.BaseType;
} while (type is not null && type.SpecialType != SpecialType.System_Object);

return false;
}

// Check if parameter has either simplified (i.e. "int?") or explicit (Nullable<int>) nullable type declaration:
internal static bool IsNullableOfT(this ITypeSymbol type)
=> type.SpecialType == SpecialType.System_Nullable_T || type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,4 +213,10 @@
<data name="OptionsUnsupportedLanguageVersionMessage" xml:space="preserve">
<value>The options validation source generator is not available in C# {0}. Please use language version {1} or greater.</value>
</data>
<data name="TypeCannotBeUsedWithTheValidationAttributeTitle" xml:space="preserve">
<value>The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types.</value>
</data>
<data name="TypeCannotBeUsedWithTheValidationAttributeMessage" xml:space="preserve">
<value>The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,16 @@
<target state="translated">U člena potenciálně chybí přenositelné ověření.</target>
<note />
</trans-unit>
<trans-unit id="TypeCannotBeUsedWithTheValidationAttributeMessage">
<source>The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures.</source>
<target state="new">The validation attribute {0} should only be applied to properties of type string, array, or ICollection. Using it with the type {1} could lead to runtime failures.</target>
<note />
</trans-unit>
<trans-unit id="TypeCannotBeUsedWithTheValidationAttributeTitle">
<source>The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types.</source>
<target state="new">The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types.</target>
<note />
</trans-unit>
<trans-unit id="ValidatorsNeedSimpleConstructorMessage">
<source>Validator type {0} doesn't have a parameterless constructor.</source>
<target state="translated">Typ validátoru {0} nemá konstruktor bez parametrů.</target>
Expand Down
Loading
Loading