Skip to content

Commit 26a809a

Browse files
authored
Fix Options Source Gen Trimming Issues (#93088)
1 parent fa54ce8 commit 26a809a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+3819
-222
lines changed

docs/project/list-of-diagnostics.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ The diagnostic id values reserved for .NET Libraries analyzer warnings are `SYSL
251251
| __`SYSLIB1214`__ | Options validation generator: Can't validate constants, static fields or properties. |
252252
| __`SYSLIB1215`__ | Options validation generator: Validation attribute on the member is inaccessible from the validator type. |
253253
| __`SYSLIB1216`__ | C# language version not supported by the options validation source generator. |
254-
| __`SYSLIB1217`__ | *_`SYSLIB1201`-`SYSLIB1219` reserved for Microsoft.Extensions.Options.SourceGeneration.* |
254+
| __`SYSLIB1217`__ | The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types. |
255255
| __`SYSLIB1218`__ | *_`SYSLIB1201`-`SYSLIB1219` reserved for Microsoft.Extensions.Options.SourceGeneration.* |
256256
| __`SYSLIB1219`__ | *_`SYSLIB1201`-`SYSLIB1219` reserved for Microsoft.Extensions.Options.SourceGeneration.* |
257257
| __`SYSLIB1220`__ | JsonSourceGenerator encountered a [JsonConverterAttribute] with an invalid type argument. |

src/libraries/Microsoft.Extensions.Options/gen/DiagDescriptors.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,5 +112,12 @@ internal sealed class DiagDescriptors : DiagDescriptorsBase
112112
messageFormat: SR.OptionsUnsupportedLanguageVersionMessage,
113113
category: Category,
114114
defaultSeverity: DiagnosticSeverity.Error);
115+
116+
public static DiagnosticDescriptor IncompatibleWithTypeForValidationAttribute { get; } = Make(
117+
id: "SYSLIB1217",
118+
title: SR.TypeCannotBeUsedWithTheValidationAttributeTitle,
119+
messageFormat: SR.TypeCannotBeUsedWithTheValidationAttributeMessage,
120+
category: Category,
121+
defaultSeverity: DiagnosticSeverity.Warning);
115122
}
116123
}

src/libraries/Microsoft.Extensions.Options/gen/Emitter.cs

Lines changed: 417 additions & 28 deletions
Large diffs are not rendered by default.

src/libraries/Microsoft.Extensions.Options/gen/Generator.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,14 @@ private static void HandleAnnotatedTypes(Compilation compilation, ImmutableArray
3838
return;
3939
}
4040

41-
var parser = new Parser(compilation, context.ReportDiagnostic, symbolHolder!, context.CancellationToken);
41+
OptionsSourceGenContext optionsSourceGenContext = new(compilation);
42+
43+
var parser = new Parser(compilation, context.ReportDiagnostic, symbolHolder!, optionsSourceGenContext, context.CancellationToken);
4244

4345
var validatorTypes = parser.GetValidatorTypes(types);
4446
if (validatorTypes.Count > 0)
4547
{
46-
var emitter = new Emitter(compilation);
48+
var emitter = new Emitter(compilation, symbolHolder!, optionsSourceGenContext);
4749
var result = emitter.Emit(validatorTypes, context.CancellationToken);
4850

4951
context.AddSource("Validators.g.cs", SourceText.From(result, Encoding.UTF8));

src/libraries/Microsoft.Extensions.Options/gen/Microsoft.Extensions.Options.SourceGeneration.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
<Compile Include="Model\ValidatedModel.cs" />
3131
<Compile Include="Model\ValidationAttributeInfo.cs" />
3232
<Compile Include="Model\ValidatorType.cs" />
33+
<Compile Include="OptionsSourceGenContext.cs" />
3334
<Compile Include="Parser.cs" />
3435
<Compile Include="ParserUtilities.cs" />
3536
<Compile Include="SymbolHolder.cs" />
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.CodeAnalysis;
5+
using Microsoft.CodeAnalysis.CSharp;
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Diagnostics;
9+
using System.Linq;
10+
using System.Runtime.Versioning;
11+
12+
namespace Microsoft.Extensions.Options.Generators
13+
{
14+
internal sealed class OptionsSourceGenContext
15+
{
16+
public OptionsSourceGenContext(Compilation compilation)
17+
{
18+
IsLangVersion11AndAbove = ((CSharpCompilation)compilation).LanguageVersion >= Microsoft.CodeAnalysis.CSharp.LanguageVersion.CSharp11;
19+
ClassModifier = IsLangVersion11AndAbove ? "file" : "internal";
20+
Suffix = IsLangVersion11AndAbove ? "" : $"_{GetNonRandomizedHashCode(compilation.SourceModule.Name):X8}";
21+
}
22+
23+
internal string Suffix { get; }
24+
internal string ClassModifier { get; }
25+
internal bool IsLangVersion11AndAbove { get; }
26+
internal Dictionary<string, HashSet<object>?> AttributesToGenerate { get; set; } = new Dictionary<string, HashSet<object>?>();
27+
28+
internal void EnsureTrackingAttribute(string attributeName, bool createValue, out HashSet<object>? value)
29+
{
30+
bool exist = AttributesToGenerate.TryGetValue(attributeName, out value);
31+
if (value is null)
32+
{
33+
if (createValue)
34+
{
35+
value = new HashSet<object>();
36+
}
37+
38+
if (!exist || createValue)
39+
{
40+
AttributesToGenerate[attributeName] = value;
41+
}
42+
}
43+
}
44+
45+
internal static bool IsConvertibleBasicType(ITypeSymbol typeSymbol)
46+
{
47+
return typeSymbol.SpecialType switch
48+
{
49+
SpecialType.System_Boolean => true,
50+
SpecialType.System_Byte => true,
51+
SpecialType.System_Char => true,
52+
SpecialType.System_DateTime => true,
53+
SpecialType.System_Decimal => true,
54+
SpecialType.System_Double => true,
55+
SpecialType.System_Int16 => true,
56+
SpecialType.System_Int32 => true,
57+
SpecialType.System_Int64 => true,
58+
SpecialType.System_SByte => true,
59+
SpecialType.System_Single => true,
60+
SpecialType.System_UInt16 => true,
61+
SpecialType.System_UInt32 => true,
62+
SpecialType.System_UInt64 => true,
63+
SpecialType.System_String => true,
64+
_ => false,
65+
};
66+
}
67+
68+
/// <summary>
69+
/// Returns a non-randomized hash code for the given string.
70+
/// We always return a positive value.
71+
/// </summary>
72+
internal static int GetNonRandomizedHashCode(string s)
73+
{
74+
uint result = 2166136261u;
75+
foreach (char c in s)
76+
{
77+
result = (c ^ result) * 16777619;
78+
}
79+
80+
return Math.Abs((int)result);
81+
}
82+
}
83+
}

src/libraries/Microsoft.Extensions.Options/gen/Parser.cs

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,22 @@ internal sealed class Parser
2525
private readonly Compilation _compilation;
2626
private readonly Action<Diagnostic> _reportDiagnostic;
2727
private readonly SymbolHolder _symbolHolder;
28+
private readonly OptionsSourceGenContext _optionsSourceGenContext;
2829
private readonly Dictionary<ITypeSymbol, ValidatorType> _synthesizedValidators = new(SymbolEqualityComparer.Default);
2930
private readonly HashSet<ITypeSymbol> _visitedModelTypes = new(SymbolEqualityComparer.Default);
3031

3132
public Parser(
3233
Compilation compilation,
3334
Action<Diagnostic> reportDiagnostic,
3435
SymbolHolder symbolHolder,
36+
OptionsSourceGenContext optionsSourceGenContext,
3537
CancellationToken cancellationToken)
3638
{
3739
_compilation = compilation;
3840
_cancellationToken = cancellationToken;
3941
_reportDiagnostic = reportDiagnostic;
4042
_symbolHolder = symbolHolder;
43+
_optionsSourceGenContext = optionsSourceGenContext;
4144
}
4245

4346
public IReadOnlyList<ValidatorType> GetValidatorTypes(IEnumerable<(TypeDeclarationSyntax TypeSyntax, SemanticModel SemanticModel)> classes)
@@ -288,7 +291,7 @@ private List<ValidatedMember> GetMembersToValidate(ITypeSymbol modelType, bool s
288291
? memberLocation
289292
: lowerLocationInCompilation;
290293

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

307-
private ValidatedMember? GetMemberInfo(ISymbol member, bool speculate, Location location, ITypeSymbol validatorType)
310+
private ValidatedMember? GetMemberInfo(ISymbol member, bool speculate, Location location, ITypeSymbol modelType, ITypeSymbol validatorType)
308311
{
309312
ITypeSymbol memberType;
310313
switch (member)
@@ -325,7 +328,7 @@ private List<ValidatedMember> GetMembersToValidate(ITypeSymbol modelType, bool s
325328
break;
326329
*/
327330
default:
328-
// we only care about properties and fields
331+
// we only care about properties
329332
return null;
330333
}
331334

@@ -467,7 +470,26 @@ private List<ValidatedMember> GetMembersToValidate(ITypeSymbol modelType, bool s
467470
continue;
468471
}
469472

470-
var validationAttr = new ValidationAttributeInfo(attributeType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
473+
string attributeFullQualifiedName = attributeType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
474+
if (SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.MaxLengthAttributeSymbol) ||
475+
SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.MinLengthAttributeSymbol) ||
476+
(_symbolHolder.LengthAttributeSymbol is not null && SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.LengthAttributeSymbol)))
477+
{
478+
if (!LengthBasedAttributeIsTrackedForSubstitution(memberType, location, attributeType, ref attributeFullQualifiedName))
479+
{
480+
continue;
481+
}
482+
}
483+
else if (SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.CompareAttributeSymbol))
484+
{
485+
TrackCompareAttributeForSubstitution(attribute, modelType, ref attributeFullQualifiedName);
486+
}
487+
else if (SymbolEqualityComparer.Default.Equals(attributeType, _symbolHolder.RangeAttributeSymbol))
488+
{
489+
TrackRangeAttributeForSubstitution(attribute, memberType, ref attributeFullQualifiedName);
490+
}
491+
492+
var validationAttr = new ValidationAttributeInfo(attributeFullQualifiedName);
471493
validationAttrs.Add(validationAttr);
472494

473495
ImmutableArray<IParameterSymbol> parameters = attribute.AttributeConstructor?.Parameters ?? ImmutableArray<IParameterSymbol>.Empty;
@@ -567,6 +589,79 @@ private List<ValidatedMember> GetMembersToValidate(ITypeSymbol modelType, bool s
567589
return null;
568590
}
569591

592+
private bool LengthBasedAttributeIsTrackedForSubstitution(ITypeSymbol memberType, Location location, ITypeSymbol attributeType, ref string attributeFullQualifiedName)
593+
{
594+
if (memberType.SpecialType == SpecialType.System_String || ConvertTo(memberType, _symbolHolder.ICollectionSymbol))
595+
{
596+
_optionsSourceGenContext.EnsureTrackingAttribute(attributeType.Name, createValue: false, out _);
597+
}
598+
else if (ParserUtilities.TypeHasProperty(memberType, "Count", SpecialType.System_Int32))
599+
{
600+
_optionsSourceGenContext.EnsureTrackingAttribute(attributeType.Name, createValue: true, out HashSet<object>? trackedTypeList);
601+
trackedTypeList!.Add(memberType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
602+
}
603+
else
604+
{
605+
Diag(DiagDescriptors.IncompatibleWithTypeForValidationAttribute, location, attributeType.Name, memberType.Name);
606+
return false;
607+
}
608+
609+
attributeFullQualifiedName = $"{Emitter.StaticGeneratedValidationAttributesClassesNamespace}.{Emitter.StaticAttributeClassNamePrefix}{_optionsSourceGenContext.Suffix}_{attributeType.Name}";
610+
return true;
611+
}
612+
613+
private void TrackCompareAttributeForSubstitution(AttributeData attribute, ITypeSymbol modelType, ref string attributeFullQualifiedName)
614+
{
615+
ImmutableArray<IParameterSymbol> constructorParameters = attribute.AttributeConstructor?.Parameters ?? ImmutableArray<IParameterSymbol>.Empty;
616+
if (constructorParameters.Length == 1 && constructorParameters[0].Name == "otherProperty" && constructorParameters[0].Type.SpecialType == SpecialType.System_String)
617+
{
618+
_optionsSourceGenContext.EnsureTrackingAttribute(attribute.AttributeClass!.Name, createValue: true, out HashSet<object>? trackedTypeList);
619+
trackedTypeList!.Add((modelType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), (string)attribute.ConstructorArguments[0].Value!));
620+
attributeFullQualifiedName = $"{Emitter.StaticGeneratedValidationAttributesClassesNamespace}.{Emitter.StaticAttributeClassNamePrefix}{_optionsSourceGenContext.Suffix}_{attribute.AttributeClass!.Name}";
621+
}
622+
}
623+
624+
private void TrackRangeAttributeForSubstitution(AttributeData attribute, ITypeSymbol memberType, ref string attributeFullQualifiedName)
625+
{
626+
ImmutableArray<IParameterSymbol> constructorParameters = attribute.AttributeConstructor?.Parameters ?? ImmutableArray<IParameterSymbol>.Empty;
627+
SpecialType argumentSpecialType = SpecialType.None;
628+
if (constructorParameters.Length == 2)
629+
{
630+
argumentSpecialType = constructorParameters[0].Type.SpecialType;
631+
}
632+
else if (constructorParameters.Length == 3)
633+
{
634+
object? argumentValue = null;
635+
for (int i = 0; i < constructorParameters.Length; i++)
636+
{
637+
if (constructorParameters[i].Name == "type")
638+
{
639+
argumentValue = attribute.ConstructorArguments[i].Value;
640+
break;
641+
}
642+
}
643+
644+
if (argumentValue is INamedTypeSymbol namedTypeSymbol && OptionsSourceGenContext.IsConvertibleBasicType(namedTypeSymbol))
645+
{
646+
argumentSpecialType = namedTypeSymbol.SpecialType;
647+
}
648+
}
649+
650+
ITypeSymbol typeSymbol = memberType;
651+
if (typeSymbol.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
652+
{
653+
typeSymbol = ((INamedTypeSymbol)typeSymbol).TypeArguments[0];
654+
}
655+
656+
if (argumentSpecialType != SpecialType.None &&
657+
OptionsSourceGenContext.IsConvertibleBasicType(typeSymbol) &&
658+
(constructorParameters.Length != 3 || typeSymbol.SpecialType == argumentSpecialType)) // When type is provided as a parameter, it has to match the property type.
659+
{
660+
_optionsSourceGenContext.EnsureTrackingAttribute(attribute.AttributeClass!.Name, createValue: false, out _);
661+
attributeFullQualifiedName = $"{Emitter.StaticGeneratedValidationAttributesClassesNamespace}.{Emitter.StaticAttributeClassNamePrefix}{_optionsSourceGenContext.Suffix}_{attribute.AttributeClass!.Name}";
662+
}
663+
}
664+
570665
private string? AddSynthesizedValidator(ITypeSymbol modelType, ISymbol member, Location location, ITypeSymbol validatorType)
571666
{
572667
var mt = modelType.WithNullableAnnotation(NullableAnnotation.None);

src/libraries/Microsoft.Extensions.Options/gen/ParserUtilities.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,29 @@ internal static bool ImplementsInterface(this ITypeSymbol type, ITypeSymbol inte
6868
return false;
6969
}
7070

71+
internal static bool TypeHasProperty(ITypeSymbol typeSymbol, string propertyName, SpecialType returnType)
72+
{
73+
ITypeSymbol? type = typeSymbol;
74+
do
75+
{
76+
if (type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
77+
{
78+
type = ((INamedTypeSymbol)type).TypeArguments[0]; // extract the T from a Nullable<T>
79+
}
80+
81+
if (type.GetMembers(propertyName).OfType<IPropertySymbol>().Any(property =>
82+
property.Type.SpecialType == returnType && property.DeclaredAccessibility == Accessibility.Public &&
83+
!property.IsStatic && property.GetMethod != null && property.Parameters.IsEmpty))
84+
{
85+
return true;
86+
}
87+
88+
type = type.BaseType;
89+
} while (type is not null && type.SpecialType != SpecialType.System_Object);
90+
91+
return false;
92+
}
93+
7194
// Check if parameter has either simplified (i.e. "int?") or explicit (Nullable<int>) nullable type declaration:
7295
internal static bool IsNullableOfT(this ITypeSymbol type)
7396
=> type.SpecialType == SpecialType.System_Nullable_T || type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T;

src/libraries/Microsoft.Extensions.Options/gen/Resources/Strings.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,4 +213,10 @@
213213
<data name="OptionsUnsupportedLanguageVersionMessage" xml:space="preserve">
214214
<value>The options validation source generator is not available in C# {0}. Please use language version {1} or greater.</value>
215215
</data>
216+
<data name="TypeCannotBeUsedWithTheValidationAttributeTitle" xml:space="preserve">
217+
<value>The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types.</value>
218+
</data>
219+
<data name="TypeCannotBeUsedWithTheValidationAttributeMessage" xml:space="preserve">
220+
<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>
221+
</data>
216222
</root>

src/libraries/Microsoft.Extensions.Options/gen/Resources/xlf/Strings.cs.xlf

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,16 @@
152152
<target state="new">Member potentially missing transitive validation.</target>
153153
<note />
154154
</trans-unit>
155+
<trans-unit id="TypeCannotBeUsedWithTheValidationAttributeMessage">
156+
<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>
157+
<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>
158+
<note />
159+
</trans-unit>
160+
<trans-unit id="TypeCannotBeUsedWithTheValidationAttributeTitle">
161+
<source>The validation attribute is only applicable to properties of type string, array, or ICollection; it cannot be used with other types.</source>
162+
<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>
163+
<note />
164+
</trans-unit>
155165
<trans-unit id="ValidatorsNeedSimpleConstructorMessage">
156166
<source>Validator type {0} doesn't have a parameterless constructor.</source>
157167
<target state="new">Validator type {0} doesn't have a parameterless constructor.</target>

0 commit comments

Comments
 (0)