diff --git a/src/Vogen/BuildConfigurationFromAttributes.cs b/src/Vogen/BuildConfigurationFromAttributes.cs new file mode 100644 index 0000000000..d78e2f7ee6 --- /dev/null +++ b/src/Vogen/BuildConfigurationFromAttributes.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Vogen.Diagnostics; + +// ReSharper disable NullableWarningSuppressionIsUsed + +namespace Vogen; + +internal static class BuildConfigurationFromAttributes +{ + public static VogenConfigurationBuildResult TryBuild(AttributeData matchingAttribute) + { + VogenConfigurationBuildResult buildResult = new VogenConfigurationBuildResult(); + + INamedTypeSymbol? invalidExceptionType = null; + INamedTypeSymbol? underlyingType = null; + Conversions conversions = Conversions.Default; + Customizations customizations = Customizations.None; + DeserializationStrictness deserializationStrictness = DeserializationStrictness.Default; + DebuggerAttributeGeneration debuggerAttributes = DebuggerAttributeGeneration.Default; + ComparisonGeneration comparisonGeneration = ComparisonGeneration.Default; + StringComparisonGeneration stringComparison = StringComparisonGeneration.Unspecified; + + bool hasErroredAttributes = false; + + var isBaseGenericType = matchingAttribute.AttributeClass!.BaseType!.IsGenericType; + if (!matchingAttribute.ConstructorArguments.IsEmpty || isBaseGenericType) + { + // make sure we don't have any errors + ImmutableArray args = matchingAttribute.ConstructorArguments; + + foreach (TypedConstant arg in args) + { + if (arg.Kind == TypedConstantKind.Error) + { + hasErroredAttributes = true; + } + } + + // find which constructor to use, it could be the generic attribute (> C# 11), or the non-generic. + if (matchingAttribute.AttributeClass!.IsGenericType || isBaseGenericType) + { + populateFromGenericAttribute(matchingAttribute, args); + } + else + { + populateFromNonGenericAttribute(args); + } + } + + if (hasErroredAttributes) + { + // skip further generator execution and let compiler generate the errors + return VogenConfigurationBuildResult.Null; + } + + populateDiagnosticsWithAnyValidationIssues(); + + buildResult.ResultingConfiguration = new VogenConfiguration( + underlyingType, + invalidExceptionType, + conversions, + customizations, + deserializationStrictness, + debuggerAttributes, + comparisonGeneration, + stringComparison); + + return buildResult; + + void populateFromGenericAttribute(AttributeData attributeData, ImmutableArray args) + { + INamedTypeSymbol? attrClassSymbol = attributeData.AttributeClass; + + if (attrClassSymbol is null) return; + + var isDerivedFromGenericAttribute = + attrClassSymbol.BaseType!.FullName()!.StartsWith("Vogen.ValueObjectAttribute<"); + + // Extracts the generic argument from the base type when the derived type isn't generic + // e.g. MyCustomVoAttribute : ValueObjectAttribute + underlyingType = isDerivedFromGenericAttribute && attrClassSymbol.TypeArguments.IsEmpty + ? attrClassSymbol.BaseType!.TypeArguments[0] as INamedTypeSymbol + : attrClassSymbol.TypeArguments[0] as INamedTypeSymbol; + + // there's one less argument because there's no underlying type arguments as it's specified in the generic + // declaration + + populate(args); + } + + void populateFromNonGenericAttribute(ImmutableArray args) + { + underlyingType = (INamedTypeSymbol?) args[0].Value; + + var skipped = args.Skip(1); + populate(skipped.ToImmutableArray()); + } + + void populateDiagnosticsWithAnyValidationIssues() + { + if (!conversions.IsValidFlags()) + { + var syntax = matchingAttribute.ApplicationSyntaxReference?.GetSyntax(); + if (syntax is not null) + { + buildResult.AddDiagnostic(DiagnosticsCatalogue.InvalidConversions(syntax.GetLocation())); + } + } + + if (!customizations.IsValidFlags()) + { + var syntax = matchingAttribute.ApplicationSyntaxReference?.GetSyntax(); + if (syntax is not null) + { + buildResult.AddDiagnostic(DiagnosticsCatalogue.InvalidCustomizations(syntax.GetLocation())); + } + } + + if (!deserializationStrictness.IsValidFlags()) + { + var syntax = matchingAttribute.ApplicationSyntaxReference?.GetSyntax(); + if (syntax is not null) + { + buildResult.AddDiagnostic(DiagnosticsCatalogue.InvalidDeserializationStrictness(syntax.GetLocation())); + } + } + } + + // populates all args - it doesn't expect the underlying type argument as that is: + // * not specified for the generic attribute, and + // * stripped out (skipped) for the non-generic attribute + void populate(ImmutableArray args) + { + if (args.Length > 7) + { + throw new InvalidOperationException("Too many arguments for the attribute."); + } + + for (int i = args.Length - 1; i >= 0; i--) + { + var v = args[i].Value; + + if (v is null) + continue; + + if (i == 6) + stringComparison = (StringComparisonGeneration) v; + + if (i == 5) + comparisonGeneration = (ComparisonGeneration) v; + + if (i == 4) + debuggerAttributes = (DebuggerAttributeGeneration) v; + + if (i == 3) + deserializationStrictness = (DeserializationStrictness) v; + + if (i == 2) + customizations = (Customizations) v; + + if (i == 1) + { + invalidExceptionType = (INamedTypeSymbol?) v; + + BuildAnyIssuesWithTheException(invalidExceptionType, buildResult); + } + + if (i == 0) + conversions = (Conversions) v; + } + } + } + + private static void BuildAnyIssuesWithTheException( + INamedTypeSymbol? invalidExceptionType, + VogenConfigurationBuildResult buildResult) + { + if (invalidExceptionType == null) + { + return; + } + + if (!invalidExceptionType.ImplementsInterfaceOrBaseClass(typeof(Exception))) + { + buildResult.AddDiagnostic(DiagnosticsCatalogue.CustomExceptionMustDeriveFromException(invalidExceptionType)); + } + + var allConstructors = invalidExceptionType.Constructors.Where(c => c.DeclaredAccessibility == Accessibility.Public); + + var singleParameterConstructors = allConstructors.Where(c => c.Parameters.Length == 1); + + if (singleParameterConstructors.Any(c => c.Parameters.Single().Type.Name == "String")) + { + return; + } + + buildResult.AddDiagnostic(DiagnosticsCatalogue.CustomExceptionMustHaveValidConstructor(invalidExceptionType)); + } +} \ No newline at end of file diff --git a/src/Vogen/BuildWorkItems.cs b/src/Vogen/BuildWorkItems.cs index 4ffacf0ef9..aaace71db8 100644 --- a/src/Vogen/BuildWorkItems.cs +++ b/src/Vogen/BuildWorkItems.cs @@ -51,7 +51,7 @@ internal static class BuildWorkItems // Build the configuration but only log issues as diagnostics if they would cause additional compilation errors, // such as incorrect exceptions, or invalid customizations. For other issues, there are separate analyzers. - var localBuildResult = ManageAttributes.TryBuildConfigurationFromAttribute(voAttribute); + var localBuildResult = BuildConfigurationFromAttributes.TryBuild(voAttribute); foreach (var diagnostic in localBuildResult.Diagnostics) { context.ReportDiagnostic(diagnostic); diff --git a/src/Vogen/ManageAttributes.cs b/src/Vogen/ManageAttributes.cs index f3b11f843a..9c822479dd 100644 --- a/src/Vogen/ManageAttributes.cs +++ b/src/Vogen/ManageAttributes.cs @@ -1,11 +1,9 @@ -using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Vogen.Diagnostics; + // ReSharper disable NullableWarningSuppressionIsUsed namespace Vogen; @@ -65,289 +63,11 @@ public static VogenConfigurationBuildResult GetDefaultConfigFromGlobalAttribute( return VogenConfigurationBuildResult.Null; } - VogenConfigurationBuildResult globalConfig = TryBuildConfigurationFromAttribute(matchingAttribute); + VogenConfigurationBuildResult globalConfig = BuildConfigurationFromAttributes.TryBuild(matchingAttribute); return globalConfig; } - public static VogenConfigurationBuildResult TryBuildConfigurationFromAttribute(AttributeData matchingAttribute) - { - VogenConfigurationBuildResult buildResult = new VogenConfigurationBuildResult(); - - INamedTypeSymbol? invalidExceptionType = null; - INamedTypeSymbol? underlyingType = null; - Conversions conversions = Conversions.Default; - Customizations customizations = Customizations.None; - DeserializationStrictness deserializationStrictness = DeserializationStrictness.Default; - DebuggerAttributeGeneration debuggerAttributes = DebuggerAttributeGeneration.Default; - ComparisonGeneration comparisonGeneration = ComparisonGeneration.Default; - StringComparisonGeneration stringComparison = StringComparisonGeneration.Unspecified; - - bool hasErroredAttributes = false; - - var isBaseGenericType = matchingAttribute.AttributeClass!.BaseType!.IsGenericType; - if (!matchingAttribute.ConstructorArguments.IsEmpty || isBaseGenericType) - { - // make sure we don't have any errors - ImmutableArray args = matchingAttribute.ConstructorArguments; - - foreach (TypedConstant arg in args) - { - if (arg.Kind == TypedConstantKind.Error) - { - hasErroredAttributes = true; - } - } - - // find which constructor to use, it could be the generic attribute (> C# 11), or the non-generic. - if (matchingAttribute.AttributeClass!.IsGenericType || isBaseGenericType) - { - PopulateFromGenericAttribute(matchingAttribute, args); - } - else - { - PopulateFromNonGenericAttribute(args); - } - } - - if (!matchingAttribute.NamedArguments.IsEmpty) - { - foreach (KeyValuePair arg in matchingAttribute.NamedArguments) - { - TypedConstant typedConstant = arg.Value; - if (typedConstant.Kind == TypedConstantKind.Error) - { - hasErroredAttributes = true; - } - else - { - switch (arg.Key) - { - case "underlyingType": - underlyingType = (INamedTypeSymbol?) typedConstant.Value!; - break; - case "invalidExceptionType": - invalidExceptionType = (INamedTypeSymbol?) typedConstant.Value!; - break; - case "conversions": - conversions = (Conversions) (typedConstant.Value ?? Conversions.Default); - break; - case "customizations": - customizations = (Customizations) (typedConstant.Value ?? Customizations.None); - break; - case "deserializationStrictness": - deserializationStrictness = (DeserializationStrictness) (typedConstant.Value ?? Customizations.None); - break; - case "debuggerAttributes": - debuggerAttributes = (DebuggerAttributeGeneration) (typedConstant.Value ?? DebuggerAttributeGeneration.Full); - break; - case "comparable": - comparisonGeneration = (ComparisonGeneration) (typedConstant.Value ?? ComparisonGeneration.UseUnderlying); - break; - case "stringComparison": - stringComparison = (StringComparisonGeneration) (typedConstant.Value ?? StringComparisonGeneration.Unspecified); - break; - } - } - } - } - - if (hasErroredAttributes) - { - // skip further generator execution and let compiler generate the errors - return VogenConfigurationBuildResult.Null; - } - - if (!conversions.IsValidFlags()) - { - var syntax = matchingAttribute.ApplicationSyntaxReference?.GetSyntax(); - if (syntax is not null) - { - buildResult.AddDiagnostic(DiagnosticsCatalogue.InvalidConversions(syntax.GetLocation())); - } - } - - if (!customizations.IsValidFlags()) - { - var syntax = matchingAttribute.ApplicationSyntaxReference?.GetSyntax(); - if (syntax is not null) - { - buildResult.AddDiagnostic(DiagnosticsCatalogue.InvalidCustomizations(syntax.GetLocation())); - } - } - - if (!deserializationStrictness.IsValidFlags()) - { - var syntax = matchingAttribute.ApplicationSyntaxReference?.GetSyntax(); - if (syntax is not null) - { - buildResult.AddDiagnostic(DiagnosticsCatalogue.InvalidDeserializationStrictness(syntax.GetLocation())); - } - } - - buildResult.ResultingConfiguration = new VogenConfiguration( - underlyingType, - invalidExceptionType, - conversions, - customizations, - deserializationStrictness, - debuggerAttributes, - comparisonGeneration, - stringComparison); - - return buildResult; - - void PopulateFromGenericAttribute( - AttributeData attributeData, - ImmutableArray args) - { - var isDerivedFromGenericAttribute = - attributeData.AttributeClass!.BaseType!.FullName()!.StartsWith("Vogen.ValueObjectAttribute<"); - - // Extracts the generic argument from the base type when the derived type isn't generic - // e.g. MyCustomVoAttribute : ValueObjectAttribute - var type = isDerivedFromGenericAttribute && attributeData.AttributeClass!.TypeArguments.IsEmpty - ? attributeData.AttributeClass!.BaseType!.TypeArguments[0] as INamedTypeSymbol - : attributeData.AttributeClass!.TypeArguments[0] as INamedTypeSymbol; - switch (args.Length) - { - case 7: - if (args[6].Value != null) - { - stringComparison = (StringComparisonGeneration) args[6].Value!; - } - - goto case 6; - case 6: - if (args[5].Value != null) - { - comparisonGeneration = (ComparisonGeneration) args[5].Value!; - } - - goto case 5; - case 5: - if (args[4].Value != null) - { - debuggerAttributes = (DebuggerAttributeGeneration) args[4].Value!; - } - - goto case 4; - case 4: - if (args[3].Value != null) - { - deserializationStrictness = (DeserializationStrictness) args[3].Value!; - } - - goto case 3; - case 3: - if (args[2].Value != null) - { - customizations = (Customizations) args[2].Value!; - } - - goto case 2; - case 2: - invalidExceptionType = (INamedTypeSymbol?) args[1].Value; - - BuildAnyIssuesWithTheException(invalidExceptionType, buildResult); - goto case 1; - - case 1: - if (args[0].Value != null) - { - conversions = (Conversions) args[0].Value!; - } - - break; - } - - underlyingType = type; - } - - void PopulateFromNonGenericAttribute(ImmutableArray args) - { - switch (args.Length) - { - case 8: - if (args[7].Value != null) - { - stringComparison = (StringComparisonGeneration) args[7].Value!; - } - - goto case 7; - case 7: - if (args[6].Value != null) - { - comparisonGeneration = (ComparisonGeneration) args[6].Value!; - } - - goto case 6; - case 6: - if (args[5].Value != null) - { - debuggerAttributes = (DebuggerAttributeGeneration) args[5].Value!; - } - - goto case 5; - case 5: - if (args[4].Value != null) - { - deserializationStrictness = (DeserializationStrictness) args[4].Value!; - } - - goto case 4; - case 4: - if (args[3].Value != null) - { - customizations = (Customizations) args[3].Value!; - } - - goto case 3; - case 3: - invalidExceptionType = (INamedTypeSymbol?) args[2].Value; - - BuildAnyIssuesWithTheException(invalidExceptionType, buildResult); - goto case 2; - - case 2: - if (args[1].Value != null) - { - conversions = (Conversions) args[1].Value!; - } - - goto case 1; - case 1: - underlyingType = (INamedTypeSymbol?) args[0].Value; - break; - } - } - } - - private static void BuildAnyIssuesWithTheException( - INamedTypeSymbol? invalidExceptionType, - VogenConfigurationBuildResult buildResult) - { - if (invalidExceptionType == null) - { - return; - } - - if (!invalidExceptionType.ImplementsInterfaceOrBaseClass(typeof(Exception))) - { - buildResult.AddDiagnostic(DiagnosticsCatalogue.CustomExceptionMustDeriveFromException(invalidExceptionType)); - } - - var allConstructors = invalidExceptionType.Constructors.Where(c => c.DeclaredAccessibility == Accessibility.Public); - - var singleParameterConstructors = allConstructors.Where(c => c.Parameters.Length == 1); - - if (singleParameterConstructors.Any(c => c.Parameters.Single().Type.Name == "String")) - { - return; - } - - buildResult.AddDiagnostic(DiagnosticsCatalogue.CustomExceptionMustHaveValidConstructor(invalidExceptionType)); - } /// /// Tries to get the syntax element for any matching attribute that might exist in the provided context. diff --git a/src/Vogen/Rules/AddValidationAnalyzer.cs b/src/Vogen/Rules/AddValidationAnalyzer.cs index b6aeff422b..1799370e2b 100644 --- a/src/Vogen/Rules/AddValidationAnalyzer.cs +++ b/src/Vogen/Rules/AddValidationAnalyzer.cs @@ -84,7 +84,7 @@ private static void AnalyzeSymbol(SymbolAnalysisContext context) if (attrs.Length != 1) return null; - VogenConfigurationBuildResult buildResult = ManageAttributes.TryBuildConfigurationFromAttribute(attrs[0]); + VogenConfigurationBuildResult buildResult = BuildConfigurationFromAttributes.TryBuild(attrs[0]); VogenConfiguration? vogenConfig = buildResult.ResultingConfiguration; diff --git a/src/Vogen/VogenConfiguration.cs b/src/Vogen/VogenConfiguration.cs index 5b43518750..2c3134f099 100644 --- a/src/Vogen/VogenConfiguration.cs +++ b/src/Vogen/VogenConfiguration.cs @@ -78,8 +78,16 @@ public static VogenConfiguration Combine( var validationExceptionType = localValues.ValidationExceptionType ?? globalValues?.ValidationExceptionType ?? DefaultInstance.ValidationExceptionType; var underlyingType = localValues.UnderlyingType ?? globalValues?.UnderlyingType ?? funcForDefaultUnderlyingType?.Invoke(); - - return new VogenConfiguration(underlyingType, validationExceptionType, conversions, customizations, strictness, debuggerAttributes, comparison, stringComparison); + + return new VogenConfiguration( + underlyingType, + validationExceptionType, + conversions, + customizations, + strictness, + debuggerAttributes, + comparison, + stringComparison); } public INamedTypeSymbol? UnderlyingType { get; }