diff --git a/.editorconfig b/.editorconfig index 6144effaa37c..978f32939e48 100644 --- a/.editorconfig +++ b/.editorconfig @@ -222,6 +222,8 @@ dotnet_style_qualification_for_method = false:suggestion # http://kent-boogaart.com/blog/editorconfig-reference-for-c-developers#dotnet_style_qualification_for_property dotnet_style_qualification_for_property = false:suggestion +csharp_style_namespace_declarations = file_scoped + ## Naming styles dotnet_naming_style.pascal_case_style.capitalization = pascal_case diff --git a/src/Uno.UWPSyncGenerator/AttributeGeneration/AttributeDataClassComparer.cs b/src/Uno.UWPSyncGenerator/AttributeGeneration/AttributeDataClassComparer.cs new file mode 100644 index 000000000000..45907ffc13f4 --- /dev/null +++ b/src/Uno.UWPSyncGenerator/AttributeGeneration/AttributeDataClassComparer.cs @@ -0,0 +1,24 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace Uno.UWPSyncGenerator.AttributeGeneration; + +internal sealed class AttributeDataClassComparer : IEqualityComparer +{ + public static AttributeDataClassComparer Instance { get; } = new(); + + private AttributeDataClassComparer() + { + } + + public bool Equals(AttributeData? x, AttributeData? y) + { + return x?.AttributeClass?.ToString() == y?.AttributeClass?.ToString(); + } + + public int GetHashCode([DisallowNull] AttributeData obj) => obj.AttributeClass?.ToString()?.GetHashCode() ?? 0; +} diff --git a/src/Uno.UWPSyncGenerator/AttributeGeneration/AttributeDescriptionBase.cs b/src/Uno.UWPSyncGenerator/AttributeGeneration/AttributeDescriptionBase.cs new file mode 100644 index 000000000000..ff6ab97254e7 --- /dev/null +++ b/src/Uno.UWPSyncGenerator/AttributeGeneration/AttributeDescriptionBase.cs @@ -0,0 +1,50 @@ +#nullable enable + +using Microsoft.CodeAnalysis; + +namespace Uno.UWPSyncGenerator.AttributeGeneration; + +internal abstract class AttributeDescriptionBase : IAttributeDescription +{ + private protected abstract bool CanHandle(string fullyQualifiedAttributeName); + + public string? TryGenerateCodeFromAttributeData(AttributeData attributeData) + { + var attributeName = attributeData.AttributeClass?.ToString(); + if (attributeName is null || !CanHandle(attributeName)) + { + return null; + } + + var parameters = TryGenerateAttributeParametersCommon(attributeData); + if (parameters is null) + { + return null; + } + else if (parameters.Length == 0) + { + return $"[global::{attributeName}]"; + } + else + { + return $"[global::{attributeName}({parameters})]"; + } + } + + private string? TryGenerateAttributeParametersCommon(AttributeData attributeData) + { + if (attributeData.ConstructorArguments.Length == 0 && attributeData.NamedArguments.Length == 0) + { + return string.Empty; + } + + return TryGenerateAttributeParameters(attributeData); + } + + /// + /// Given , try to generate parameters. If the current instance doesn't handle + /// the given , return null. + /// This method is not called when both ConstructorArguments and NamedArguments are empty. + /// + protected abstract string? TryGenerateAttributeParameters(AttributeData attributeData); +} diff --git a/src/Uno.UWPSyncGenerator/AttributeGeneration/AttributeUsageAttributeDescription.cs b/src/Uno.UWPSyncGenerator/AttributeGeneration/AttributeUsageAttributeDescription.cs new file mode 100644 index 000000000000..39e863c23d83 --- /dev/null +++ b/src/Uno.UWPSyncGenerator/AttributeGeneration/AttributeUsageAttributeDescription.cs @@ -0,0 +1,80 @@ +#nullable enable + +using System; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace Uno.UWPSyncGenerator.AttributeGeneration; + +internal class AttributeUsageAttributeDescription : AttributeDescriptionBase +{ + protected override string? TryGenerateAttributeParameters(AttributeData attributeData) + { + // The .Single() below is to assert we have a single constructor arguments, ie, AttributeTargets. + // This adds confidence that we are not ignoring any constructor arguments. + var attributeTargets = GetAttributeTargetsString((AttributeTargets)attributeData.ConstructorArguments.Single().Value!); + + // For some reason, attribute.ConstructorArguments.Single().Value can be "zero" for some attributes, e.g, OverridableAttribute. + // From documentation: https://learn.microsoft.com/en-us/uwp/api/windows.foundation.metadata.overridableattribute?view=winrt-16299 + // This is `System.AttributeTargets.InterfaceImpl`, but there is no such member in AttributeTargets, so it's probably not .NET-related. + // We ignore adding the attribute for this case. + if (attributeTargets.Length > 1) + { + var allowMultiple = false; + var isInherited = true; + foreach (var namedArgument in attributeData.NamedArguments) + { + if (namedArgument.Key.Equals("AllowMultiple", StringComparison.Ordinal)) + { + allowMultiple = (bool)namedArgument.Value.Value!; + } + else if (namedArgument.Key.Equals("Inherited", StringComparison.Ordinal)) + { + isInherited = (bool)namedArgument.Value.Value!; + } + else + { + throw new InvalidOperationException($"Unexpected named argument '{namedArgument.Key}' for 'AttributeUsageAttribute'"); + } + } + + return $"{attributeTargets}, Inherited = {isInherited.ToString().ToLowerInvariant()}, AllowMultiple = {allowMultiple.ToString().ToLowerInvariant()}"; + } + + return null; + } + + private static string GetAttributeTargetsString(AttributeTargets valueToConvert) + { + var isFirst = true; + var result = ""; + var values = Enum.GetValues(typeof(AttributeTargets)).Cast().OrderByDescending(a => (int)a); + foreach (var value in values) + { + if ((value & valueToConvert) == value) + { + if (isFirst) + { + result = $"global::System.AttributeTargets.{value}"; + } + else + { + result += $" | global::System.AttributeTargets.{value}"; + } + + isFirst = false; + valueToConvert = (AttributeTargets)(valueToConvert - value); + } + } + + if (valueToConvert != 0) + { + throw new InvalidOperationException("Something went wrong.."); + } + + return result; + } + + private protected override bool CanHandle(string fullyQualifiedAttributeName) + => fullyQualifiedAttributeName == "System.AttributeUsageAttribute"; +} diff --git a/src/Uno.UWPSyncGenerator/AttributeGeneration/BindableAttributeDescription.cs b/src/Uno.UWPSyncGenerator/AttributeGeneration/BindableAttributeDescription.cs new file mode 100644 index 000000000000..4b428c4b2744 --- /dev/null +++ b/src/Uno.UWPSyncGenerator/AttributeGeneration/BindableAttributeDescription.cs @@ -0,0 +1,21 @@ +#nullable enable + +using Microsoft.CodeAnalysis; + +namespace Uno.UWPSyncGenerator.AttributeGeneration; + +internal class BindableAttributeDescription : AttributeDescriptionBase +{ + protected override string? TryGenerateAttributeParameters(AttributeData attributeData) + { + // This attribute doesn't have any constructor/named arguments. + // It was already handled by TryGenerateAttributeParametersCommon in AttributeDescriptionBase. + // We should never hit this code path, but if we did, just don't handle the attribute. + return null; + } + + private protected override bool CanHandle(string fullyQualifiedAttributeName) + { + return fullyQualifiedAttributeName == "Windows" + ".UI.Xaml.Data.BindableAttribute"; + } +} diff --git a/src/Uno.UWPSyncGenerator/AttributeGeneration/ContentPropertyAttributeDescription.cs b/src/Uno.UWPSyncGenerator/AttributeGeneration/ContentPropertyAttributeDescription.cs new file mode 100644 index 000000000000..257f2dde07b6 --- /dev/null +++ b/src/Uno.UWPSyncGenerator/AttributeGeneration/ContentPropertyAttributeDescription.cs @@ -0,0 +1,25 @@ +#nullable enable + +using System; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace Uno.UWPSyncGenerator.AttributeGeneration; + +internal sealed class ContentPropertyAttributeDescription : AttributeDescriptionBase +{ + protected override string? TryGenerateAttributeParameters(AttributeData attributeData) + { + if (attributeData.ConstructorArguments.IsEmpty && !attributeData.NamedArguments.IsEmpty) + { + return $"Name = \"{(string)attributeData.NamedArguments.Single().Value.Value!}\""; + } + + return null; + } + + private protected override bool CanHandle(string fullyQualifiedAttributeName) + { + return fullyQualifiedAttributeName is "Windows" + ".UI.Xaml.Markup.ContentPropertyAttribute" or "Microsoft.UI.Xaml.Markup.ContentPropertyAttribute"; + } +} diff --git a/src/Uno.UWPSyncGenerator/AttributeGeneration/DeprecatedAttributeDescription.cs b/src/Uno.UWPSyncGenerator/AttributeGeneration/DeprecatedAttributeDescription.cs new file mode 100644 index 000000000000..16e829f6bbfd --- /dev/null +++ b/src/Uno.UWPSyncGenerator/AttributeGeneration/DeprecatedAttributeDescription.cs @@ -0,0 +1,18 @@ +#nullable enable + +using Microsoft.CodeAnalysis; + +namespace Uno.UWPSyncGenerator.AttributeGeneration; + +internal class DeprecatedAttributeDescription : IAttributeDescription +{ + public string? TryGenerateCodeFromAttributeData(AttributeData attributeData) + { + if (attributeData.AttributeClass?.ToString() == "Windows.Foundation.Metadata.DeprecatedAttribute") + { + return "// This type is deprecated. Consider not implementing it."; + } + + return null; + } +} diff --git a/src/Uno.UWPSyncGenerator/AttributeGeneration/FlagsAttributeDescription.cs b/src/Uno.UWPSyncGenerator/AttributeGeneration/FlagsAttributeDescription.cs new file mode 100644 index 000000000000..ae5820cf8905 --- /dev/null +++ b/src/Uno.UWPSyncGenerator/AttributeGeneration/FlagsAttributeDescription.cs @@ -0,0 +1,18 @@ +#nullable enable + +using Microsoft.CodeAnalysis; + +namespace Uno.UWPSyncGenerator.AttributeGeneration; + +internal sealed class FlagsAttributeDescription : AttributeDescriptionBase +{ + protected override string? TryGenerateAttributeParameters(AttributeData attributeData) + { + // This attribute doesn't have any constructor/named arguments. + // It was already handled by TryGenerateAttributeParametersCommon in AttributeDescriptionBase. + // We should never hit this code path, but if we did, just don't handle the attribute. + return null; + } + + private protected override bool CanHandle(string fullyQualifiedAttributeName) => fullyQualifiedAttributeName == "System.FlagsAttribute"; +} diff --git a/src/Uno.UWPSyncGenerator/AttributeGeneration/IAttributeDescription.cs b/src/Uno.UWPSyncGenerator/AttributeGeneration/IAttributeDescription.cs new file mode 100644 index 000000000000..2b3c667f4289 --- /dev/null +++ b/src/Uno.UWPSyncGenerator/AttributeGeneration/IAttributeDescription.cs @@ -0,0 +1,14 @@ +#nullable enable + +using Microsoft.CodeAnalysis; + +namespace Uno.UWPSyncGenerator.AttributeGeneration; + +internal interface IAttributeDescription +{ + /// + /// Given an , return a string representing the C# code for the attribute. If + /// the given can't be handled by this attribute description instance, returns null. + /// + string? TryGenerateCodeFromAttributeData(AttributeData attributeData); +} diff --git a/src/Uno.UWPSyncGenerator/SyncGenerator.cs b/src/Uno.UWPSyncGenerator/SyncGenerator.cs index b4f0279acd57..8ea5b603a295 100644 --- a/src/Uno.UWPSyncGenerator/SyncGenerator.cs +++ b/src/Uno.UWPSyncGenerator/SyncGenerator.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis; using Uno.Extensions; +using Uno.UWPSyncGenerator.AttributeGeneration; namespace Uno.UWPSyncGenerator { @@ -51,6 +53,14 @@ private static string KindToKeyword(TypeKind kind) }; } + private static ImmutableArray s_attributeDescriptions = ImmutableArray.Create( + new AttributeUsageAttributeDescription(), + new BindableAttributeDescription(), + new ContentPropertyAttributeDescription(), + new DeprecatedAttributeDescription(), + new FlagsAttributeDescription() + ); + private void WriteType(INamedTypeSymbol type, IndentedStringBuilder b) { var kind = type.TypeKind; @@ -67,6 +77,44 @@ private void WriteType(INamedTypeSymbol type, IndentedStringBuilder b) var writtenMethods = new List(); + var uwpAttributes = type.GetAttributes().Where(a => !IsIgnoredAttribute(a)); + var attributesToGenerate = new HashSet(); + + AddAttributesToGenerate(GetMissingAttributes(uwpAttributes, allSymbols.AndroidSymbol), attributesToGenerate); + AddAttributesToGenerate(GetMissingAttributes(uwpAttributes, allSymbols.IOSSymbol), attributesToGenerate); + AddAttributesToGenerate(GetMissingAttributes(uwpAttributes, allSymbols.MacOSSymbol), attributesToGenerate); + AddAttributesToGenerate(GetMissingAttributes(uwpAttributes, allSymbols.SkiaSymbol), attributesToGenerate); + AddAttributesToGenerate(GetMissingAttributes(uwpAttributes, allSymbols.WasmSymbol), attributesToGenerate); + AddAttributesToGenerate(GetMissingAttributes(uwpAttributes, allSymbols.UnitTestsymbol), attributesToGenerate); + AddAttributesToGenerate(GetMissingAttributes(uwpAttributes, allSymbols.NetStdReferenceSymbol), attributesToGenerate); + + static void AddAttributesToGenerate(IEnumerable missingAttributes, HashSet attributesToGenerate) + { + foreach (var missingAttribute in missingAttributes) + { + bool isHandled = false; + foreach (var attributeDescription in s_attributeDescriptions) + { + if (attributeDescription.TryGenerateCodeFromAttributeData(missingAttribute) is { } generatedCode) + { + attributesToGenerate.Add(generatedCode); + isHandled = true; + break; + } + } + + if (!isHandled) + { + throw new InvalidOperationException($"Attribute {missingAttribute} could not be handled."); + } + } + } + + foreach (var attributeToGenerate in attributesToGenerate) + { + b.AppendLineInvariant("{0}", attributeToGenerate); + } + if (type.TypeKind == TypeKind.Delegate) { BuildDelegate(type, b, allSymbols); @@ -81,11 +129,6 @@ private void WriteType(INamedTypeSymbol type, IndentedStringBuilder b) if (type.TypeKind == TypeKind.Enum) { allSymbols.AppendIf(b); - - if (type.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, FlagsAttributeSymbol))) - { - b.AppendLineInvariant($"[global::System.FlagsAttribute]"); - } } else { @@ -137,5 +180,42 @@ private void WriteType(INamedTypeSymbol type, IndentedStringBuilder b) } } } + + private static IEnumerable GetMissingAttributes(IEnumerable expected, INamedTypeSymbol type) + { + if (type is null) + { + return expected; + } + + return expected.ExceptBy(type.GetAttributes(), a => a, AttributeDataClassComparer.Instance); + } + + private static bool IsIgnoredAttribute(AttributeData attributeData) + { + return attributeData.AttributeClass.ToString() is + "Windows.Foundation.Metadata.GuidAttribute" or + "Windows.Foundation.Metadata.StaticAttribute" or + "Windows.Foundation.Metadata.AllowMultipleAttribute" or + "Windows.Foundation.Metadata.AttributeNameAttribute" or + "Windows.Foundation.Metadata.MuseAttribute" or + "Windows.Foundation.Metadata.GCPressureAttribute" or + "Windows.Foundation.Metadata.ComposableAttribute" or + "Windows.Foundation.Metadata.ContractVersionAttribute" or + "Windows.Foundation.Metadata.HasVariantAttribute" or + "Windows.Foundation.Metadata.DualApiPartitionAttribute" or + "Windows.Foundation.Metadata.ActivatableAttribute" or + "Windows.Foundation.Metadata.MarshalingBehaviorAttribute" or + "Windows.Foundation.Metadata.ThreadingAttribute" or + "Windows.Foundation.Metadata.ApiContractAttribute" or + "Windows.Foundation.Metadata.PreviousContractVersionAttribute" or + "Windows.Foundation.Metadata.VersionAttribute" or + "Windows.Foundation.Metadata.WebHostHiddenAttribute" or + "Microsoft.UI.Xaml.CustomAttributes.MUXPropertyChangedCallbackAttribute" or + "Microsoft.UI.Xaml.CustomAttributes.MUXPropertyChangedCallbackMethodNameAttribute" or + "Microsoft.UI.Xaml.CustomAttributes.MUXPropertyNeedsDependencyPropertyFieldAttribute" or + "Microsoft.UI.Xaml.CustomAttributes.MUXHasCustomActivationFactoryAttribute" or + "Microsoft.UI.Xaml.Controls.InputPropertyAttribute"; + } } }