Skip to content

Commit

Permalink
[release/8.0] Fix Options Source Gen Trimming Issues (#93193)
Browse files Browse the repository at this point in the history
* Fix Options Source Gen Trimming Issues

* Make Emitted Attribute Order Deterministic in Options Source Generator (#93260)

* Make Emitted Attribute Order Deterministic in Options Source Generator

* Use ordinal comparison when ordering the list

---------

Co-authored-by: Tarek Mahmoud Sayed <[email protected]>
  • Loading branch information
github-actions[bot] and tarekgh authored Oct 12, 2023
1 parent 705221b commit ba51641
Show file tree
Hide file tree
Showing 41 changed files with 3,819 additions and 222 deletions.
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

0 comments on commit ba51641

Please sign in to comment.