Skip to content

Commit

Permalink
fix(SyncGenerator): Generate missing attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
Youssef1313 committed May 17, 2023
1 parent 3056a4b commit 72ac37e
Show file tree
Hide file tree
Showing 10 changed files with 339 additions and 7 deletions.
2 changes: 2 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AttributeData>
{
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;
}
Original file line number Diff line number Diff line change
@@ -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);
}

/// <summary>
/// Given <see cref="AttributeData"/>, try to generate parameters. If the current instance doesn't handle
/// the given <paramref name="attributeData"/>, return null.
/// This method is not called when both ConstructorArguments and NamedArguments are empty.
/// </summary>
protected abstract string? TryGenerateAttributeParameters(AttributeData attributeData);
}
Original file line number Diff line number Diff line change
@@ -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<AttributeTargets>().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";
}
Original file line number Diff line number Diff line change
@@ -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";
}
}
Original file line number Diff line number Diff line change
@@ -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";
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#nullable enable

using Microsoft.CodeAnalysis;

namespace Uno.UWPSyncGenerator.AttributeGeneration;

internal interface IAttributeDescription
{
/// <summary>
/// Given an <see cref="AttributeData"/>, return a string representing the C# code for the attribute. If
/// the given <paramref name="attributeData"/> can't be handled by this attribute description instance, returns null.
/// </summary>
string? TryGenerateCodeFromAttributeData(AttributeData attributeData);
}
94 changes: 87 additions & 7 deletions src/Uno.UWPSyncGenerator/SyncGenerator.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -51,6 +53,14 @@ private static string KindToKeyword(TypeKind kind)
};
}

private static ImmutableArray<IAttributeDescription> s_attributeDescriptions = ImmutableArray.Create<IAttributeDescription>(
new AttributeUsageAttributeDescription(),
new BindableAttributeDescription(),
new ContentPropertyAttributeDescription(),
new DeprecatedAttributeDescription(),
new FlagsAttributeDescription()
);

private void WriteType(INamedTypeSymbol type, IndentedStringBuilder b)
{
var kind = type.TypeKind;
Expand All @@ -67,6 +77,44 @@ private void WriteType(INamedTypeSymbol type, IndentedStringBuilder b)

var writtenMethods = new List<IMethodSymbol>();

var uwpAttributes = type.GetAttributes().Where(a => !IsIgnoredAttribute(a));
var attributesToGenerate = new HashSet<string>();

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<AttributeData> missingAttributes, HashSet<string> 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);
Expand All @@ -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
{
Expand Down Expand Up @@ -137,5 +180,42 @@ private void WriteType(INamedTypeSymbol type, IndentedStringBuilder b)
}
}
}

private static IEnumerable<AttributeData> GetMissingAttributes(IEnumerable<AttributeData> 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";
}
}
}

0 comments on commit 72ac37e

Please sign in to comment.