Skip to content

Commit

Permalink
add EnumOptionAttr
Browse files Browse the repository at this point in the history
  • Loading branch information
Poker-sang committed Dec 24, 2024
1 parent eb7c442 commit b0b3f1a
Show file tree
Hide file tree
Showing 11 changed files with 319 additions and 158 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,20 @@ public static partial class TestHelpers
{
internal static IEnumerable<MetadataReference> GetAllReferencedAssemblies()
{
return from assembly in AppDomain.CurrentDomain.GetAssemblies()
where !assembly.IsDynamic
let reference = MetadataReference.CreateFromFile(assembly.Location)
select reference;
return AppDomain.CurrentDomain.GetAssemblies()
.Where(assembly => !assembly.IsDynamic)
.Select(assembly => MetadataReference.CreateFromFile(assembly.Location));
}

internal static SyntaxTree ToSyntaxTree(this string source)
{
return CSharpSyntaxTree.ParseText(source,
CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp10));
CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp12));
}

internal static CSharpCompilation CreateCompilation(this SyntaxTree syntaxTree, string assemblyName, IEnumerable<MetadataReference>? references = null)
{
return CSharpCompilation.Create(assemblyName, new[] { syntaxTree }, references ?? GetAllReferencedAssemblies(), new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
return CSharpCompilation.Create(assemblyName, [syntaxTree], references ?? GetAllReferencedAssemblies(), new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
}

internal static CSharpCompilation CreateCompilation(this IEnumerable<SyntaxTree> syntaxTree, string assemblyName, IEnumerable<MetadataReference>? references = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@

using CommunityToolkit.Tooling.SampleGen.Diagnostics;
using CommunityToolkit.Tooling.SampleGen.Tests.Helpers;
using Microsoft.CodeAnalysis;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;

namespace CommunityToolkit.Tooling.SampleGen.Tests;

Expand Down Expand Up @@ -51,6 +49,94 @@ public class UserControl { }
result.AssertDiagnosticsAre();
}

[TestMethod]
public void PaneOption_GeneratesEnumWithoutDiagnostics()
{
var source = """
using Windows.UI.Xaml;
using CommunityToolkit.Tooling.SampleGen;
using CommunityToolkit.Tooling.SampleGen.Attributes;
namespace MyApp
{
[ToolkitSampleEnumOption<Windows.UI.Xaml.Controls.Visibility>("MyVisibility", Title = "Visibility")]
[ToolkitSample(id: nameof(Sample), "Test Sample", description: "")]
public partial class Sample : Windows.UI.Xaml.Controls.UserControl
{
}
}
namespace Windows.UI.Xaml.Controls
{
public class UserControl { }
public enum Visibility { Visible = 3, Collapsed = 7 }
}
""";

var result = source.RunSourceGenerator<ToolkitSampleOptionGenerator>(SAMPLE_ASM_NAME);

result.AssertNoCompilationErrors();
result.AssertDiagnosticsAre();
}

[TestMethod]
public void PaneOption_GeneratesEnumProperty()
{
var sampleProjectAssembly = """
using Windows.UI.Xaml;
using CommunityToolkit.Tooling.SampleGen;
using CommunityToolkit.Tooling.SampleGen.Attributes;
namespace MyApp
{
[ToolkitSampleEnumOption<Windows.UI.Xaml.Controls.Visibility>("MyVisibility", Title = "Visibility")]
[ToolkitSample(id: nameof(Sample), "Test Sample", description: "")]
public partial class Sample : Windows.UI.Xaml.Controls.UserControl
{
public Sample()
{
var y = this.MyVisibility;
}
}
}
namespace Windows.UI.Xaml.Controls
{
public class UserControl { }
public enum Visibility { Visible = 3, Collapsed = 7 }
}
""".ToSyntaxTree()
.CreateCompilation("MyApp.Samples")
.ToMetadataReference();

// Create application head that references generated sample project
var headCompilation = string.Empty
.ToSyntaxTree()
.CreateCompilation("MyApp.Head")
.AddReferences(sampleProjectAssembly);

// Run source generator
var result = headCompilation.RunSourceGenerator<ToolkitSampleMetadataGenerator>();

result.AssertDiagnosticsAre();
result.AssertNoCompilationErrors();

Assert.AreEqual(result.Compilation.GetFileContentsByName("ToolkitSampleRegistry.g.cs"), """
#nullable enable
namespace CommunityToolkit.Tooling.SampleGen;
public static class ToolkitSampleRegistry
{
public static System.Collections.Generic.Dictionary<string, CommunityToolkit.Tooling.SampleGen.Metadata.ToolkitSampleMetadata> Listing { get; } = new()
{
["Sample"] = new CommunityToolkit.Tooling.SampleGen.Metadata.ToolkitSampleMetadata("Sample", "Test Sample", "", typeof(MyApp.Sample), () => new MyApp.Sample(), null, null, new CommunityToolkit.Tooling.SampleGen.Metadata.IGeneratedToolkitSampleOptionViewModel[] { new CommunityToolkit.Tooling.SampleGen.Metadata.ToolkitSampleMultiChoiceOptionMetadataViewModel(name: "MyVisibility", options: new[] { new CommunityToolkit.Tooling.SampleGen.Attributes.MultiChoiceOption("Visible", "3"),new CommunityToolkit.Tooling.SampleGen.Attributes.MultiChoiceOption("Collapsed", "7") }, title: "Visibility") })
};
}
""", "Unexpected code generated");
}

[TestMethod]
public void PaneOption_GeneratesTitleProperty()
{
Expand All @@ -77,7 +163,7 @@ namespace Windows.UI.Xaml.Controls
{
public class UserControl { }
}
""".ToSyntaxTree()
""".ToSyntaxTree()
.CreateCompilation("MyApp.Samples")
.ToMetadataReference();

Expand All @@ -99,8 +185,8 @@ namespace CommunityToolkit.Tooling.SampleGen;
public static class ToolkitSampleRegistry
{
public static System.Collections.Generic.Dictionary<string, CommunityToolkit.Tooling.SampleGen.Metadata.ToolkitSampleMetadata> Listing
{ get; } = new() {
public static System.Collections.Generic.Dictionary<string, CommunityToolkit.Tooling.SampleGen.Metadata.ToolkitSampleMetadata> Listing { get; } = new()
{
["Sample"] = new CommunityToolkit.Tooling.SampleGen.Metadata.ToolkitSampleMetadata("Sample", "Test Sample", "", typeof(MyApp.Sample), () => new MyApp.Sample(), null, null, new CommunityToolkit.Tooling.SampleGen.Metadata.IGeneratedToolkitSampleOptionViewModel[] { new CommunityToolkit.Tooling.SampleGen.Metadata.ToolkitSampleNumericOptionMetadataViewModel(name: "TextSize", initial: 12, min: 8, max: 48, step: 2, showAsNumberBox: false, title: "FontSize") })
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,18 @@ namespace CommunityToolkit.Tooling.SampleGen.Attributes;
/// </summary>
/// <param name="Label">A label shown to the user for this option.</param>
/// <param name="Value">The value passed to XAML when this option is selected.</param>
public record MultiChoiceOption(string Label, string Value)
public record MultiChoiceOption(string Label, object Value)
{
public virtual bool Equals(MultiChoiceOption? other)
{
return other is not null && (ReferenceEquals(this, other) || Value.Equals(other.Value));
}

public override int GetHashCode()
{
return Value.GetHashCode();
}

/// <remarks>
/// The string has been overriden to display the label only,
/// especially so the data can be easily displayed in XAML without a custom template, converter or code behind.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ public sealed class ToolkitSampleBoolOptionAttribute : ToolkitSampleOptionBaseAt
/// </summary>
/// <param name="bindingName">The name of the generated property, which you can bind to in XAML.</param>
/// <param name="defaultState">The initial value for the bound property.</param>
/// <param name="title">A title to display on top of this option.</param>
public ToolkitSampleBoolOptionAttribute(string bindingName, bool defaultState)
: base(bindingName, defaultState)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#region Copyright

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#endregion

namespace CommunityToolkit.Tooling.SampleGen.Attributes;

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class ToolkitSampleEnumOptionAttribute<TEnum> : ToolkitSampleOptionBaseAttribute where TEnum : struct, Enum
{
/// <summary>
/// Creates a new instance of <see cref="ToolkitSampleEnumOptionAttribute{TEnum}"/>.
/// </summary>
/// <param name="bindingName">The name of the generated property, which you can bind to in XAML.</param>
public ToolkitSampleEnumOptionAttribute(string bindingName)
: base(bindingName, null)
{
}

/// <summary>
/// The source generator-friendly type name used for casting.
/// </summary>
internal override string TypeName { get; } = "int";
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace CommunityToolkit.Tooling.SampleGen.Attributes;
/// </summary>
/// <remarks>
/// Using this attribute will automatically generate an <see cref="INotifyPropertyChanged"/>-enabled property
/// that you can bind to in XAML, and displays an options pane alonside your sample which allows the user to manipulate the property.
/// that you can bind to in XAML, and displays an options pane alongside your sample which allows the user to manipulate the property.
/// <para/>
/// </remarks>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
Expand All @@ -20,10 +20,10 @@ public sealed class ToolkitSampleMultiChoiceOptionAttribute : ToolkitSampleOptio
/// </summary>
/// <param name="bindingName">The name of the generated property, which you can bind to in XAML.</param>
/// <param name="choices">A list of the choices to display to the user. Can be literal values, or labeled values. Use a " : " separator (single colon surrounded by at least 1 whitespace) to separate a label from a value.</param>
/// <param name="title">A title to display on top of this option.</param>
public ToolkitSampleMultiChoiceOptionAttribute(string bindingName, params string[] choices)
: base(bindingName, null)
{
TypeName = "string";
Choices = choices.Select(x =>
{
if (x.Contains(" : "))
Expand All @@ -36,6 +36,18 @@ public ToolkitSampleMultiChoiceOptionAttribute(string bindingName, params string
}).ToArray();
}

/// <summary>
/// Creates a new instance of <see cref="ToolkitSampleMultiChoiceOptionAttribute"/>.
/// </summary>
/// <param name="bindingName">The name of the generated property, which you can bind to in XAML.</param>
/// <param name="choices">A list of the choices to display to the user. Can be literal values, or labeled values. Use a " : " separator (single colon surrounded by at least 1 whitespace) to separate a label from a value.</param>
public ToolkitSampleMultiChoiceOptionAttribute(string bindingName, List<(string, object)> choices)
: base(bindingName, null)
{
TypeName = "int";
Choices = choices.Select(x => new MultiChoiceOption(x.Item1, x.Item2)).ToArray();
}

/// <summary>
/// A collection of choices to display in the options pane.
/// </summary>
Expand All @@ -44,5 +56,5 @@ public ToolkitSampleMultiChoiceOptionAttribute(string bindingName, params string
/// <summary>
/// The source generator-friendly type name used for casting.
/// </summary>
internal override string TypeName { get; } = "string";
internal override string TypeName { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace CommunityToolkit.Tooling.SampleGen.Attributes;
/// </summary>
/// <remarks>
/// Using this attribute will automatically generate an <see cref="INotifyPropertyChanged"/>-enabled property
/// that you can bind to in XAML, and displays an options pane alonside your sample which allows the user to manipulate the property.
/// that you can bind to in XAML, and displays an options pane alongside your sample which allows the user to manipulate the property.
/// <para/>
/// </remarks>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
Expand All @@ -19,8 +19,11 @@ public sealed class ToolkitSampleNumericOptionAttribute : ToolkitSampleOptionBas
/// Creates a new instance of <see cref="ToolkitSampleNumericOptionAttribute"/>.
/// </summary>
/// <param name="bindingName">The name of the generated property, which you can bind to in XAML.</param>
/// <param name="choices">A list of the choices to display to the user. Can be literal values, or labeled values. Use a " : " separator (single colon surrounded by at least 1 whitespace) to separate a label from a value.</param>
/// <param name="title">A title to display on top of this option.</param>
/// <param name="initial"></param>
/// <param name="min"></param>
/// <param name="max"></param>
/// <param name="step"></param>
/// <param name="showAsNumberBox"></param>
public ToolkitSampleNumericOptionAttribute(string bindingName, double initial = 0, double min = 0, double max = 10, double step = 1, bool showAsNumberBox = false)
: base(bindingName, null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ public abstract class ToolkitSampleOptionBaseAttribute : Attribute
/// </summary>
/// <param name="bindingName">The name of the generated property, which you can bind to in XAML.</param>
/// <param name="defaultState">The initial value for the bound property.</param>
/// <param name="title">A title to display on top of this option.</param>
public ToolkitSampleOptionBaseAttribute(string bindingName, object? defaultState)
{
Name = bindingName;
Expand Down
10 changes: 5 additions & 5 deletions CommunityToolkit.Tooling.SampleGen/GeneratorExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ public static IEnumerable<ISymbol> CrawlForAllSymbols(this INamespaceSymbol name

foreach (var member in namespaceSymbol.GetNamespaceMembers())
{
if (member is INamespaceSymbol nestedNamespace)
if (member is not null)
{
foreach (var item in CrawlForAllSymbols(nestedNamespace))
foreach (var item in CrawlForAllSymbols(member))
{
yield return item;
}
Expand Down Expand Up @@ -95,7 +95,7 @@ public static T ReconstructAs<T>(this AttributeData attributeData)
}

/// <summary>
/// Checks whether or not a given type symbol has a specified full name.
/// Checks whether a given type symbol has a specified full name.
/// </summary>
/// <param name="symbol">The input <see cref="ISymbol"/> instance to check.</param>
/// <param name="name">The full name to check.</param>
Expand Down Expand Up @@ -123,7 +123,7 @@ public static bool HasFullyQualifiedName(this ISymbol symbol, string name)

// Enums arrive as the underlying integer type, which doesn't work as a param for Activator.CreateInstance()
if (argType != null && parameterTypedConstant.Kind == TypedConstantKind.Enum)
return Enum.Parse(argType, parameterTypedConstant.Value?.ToString());
return Enum.Parse(argType, parameterTypedConstant.Value!.ToString());

if (parameterTypedConstant.Kind == TypedConstantKind.Array)
{
Expand Down Expand Up @@ -163,7 +163,7 @@ public static bool HasFullyQualifiedName(this ISymbol symbol, string name)
/// <param name="attributeData">The target <see cref="AttributeData"/> instance to check.</param>
/// <param name="name">The name of the argument to check.</param>
/// <param name="value">The resulting argument value, if present.</param>
/// <returns>Whether or not <paramref name="attributeData"/> contains an argument named <paramref name="name"/> with a valid value.</returns>
/// <returns>Whether <paramref name="attributeData"/> contains an argument named <paramref name="name"/> with a valid value.</returns>
public static bool TryGetNamedArgument<T>(this AttributeData attributeData, string name, out T? value)
{
foreach (KeyValuePair<string, TypedConstant> properties in attributeData.NamedArguments)
Expand Down
Loading

0 comments on commit b0b3f1a

Please sign in to comment.