diff --git a/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.Compilation.cs b/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.Compilation.cs index b19d6854..285bab76 100644 --- a/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.Compilation.cs +++ b/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.Compilation.cs @@ -12,21 +12,20 @@ public static partial class TestHelpers { internal static IEnumerable 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? 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, string assemblyName, IEnumerable? references = null) diff --git a/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleGeneratedPaneTests.cs b/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleGeneratedPaneTests.cs index 6189b329..e2603cbd 100644 --- a/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleGeneratedPaneTests.cs +++ b/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleGeneratedPaneTests.cs @@ -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; @@ -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("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(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("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(); + + 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 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() { @@ -77,7 +163,7 @@ namespace Windows.UI.Xaml.Controls { public class UserControl { } } - """.ToSyntaxTree() + """.ToSyntaxTree() .CreateCompilation("MyApp.Samples") .ToMetadataReference(); @@ -99,8 +185,8 @@ namespace CommunityToolkit.Tooling.SampleGen; public static class ToolkitSampleRegistry { - public static System.Collections.Generic.Dictionary Listing - { get; } = new() { + public static System.Collections.Generic.Dictionary 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") }) }; } diff --git a/CommunityToolkit.Tooling.SampleGen/Attributes/MultiChoiceOption.cs b/CommunityToolkit.Tooling.SampleGen/Attributes/MultiChoiceOption.cs index ac42b3c9..f9a46fb5 100644 --- a/CommunityToolkit.Tooling.SampleGen/Attributes/MultiChoiceOption.cs +++ b/CommunityToolkit.Tooling.SampleGen/Attributes/MultiChoiceOption.cs @@ -12,8 +12,18 @@ namespace CommunityToolkit.Tooling.SampleGen.Attributes; /// /// A label shown to the user for this option. /// The value passed to XAML when this option is selected. -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(); + } + /// /// 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. diff --git a/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleBoolOptionAttribute.cs b/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleBoolOptionAttribute.cs index 7b600b7d..79f3a4bb 100644 --- a/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleBoolOptionAttribute.cs +++ b/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleBoolOptionAttribute.cs @@ -19,7 +19,6 @@ public sealed class ToolkitSampleBoolOptionAttribute : ToolkitSampleOptionBaseAt /// /// The name of the generated property, which you can bind to in XAML. /// The initial value for the bound property. - /// A title to display on top of this option. public ToolkitSampleBoolOptionAttribute(string bindingName, bool defaultState) : base(bindingName, defaultState) { diff --git a/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleEnumOptionAttribute.cs b/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleEnumOptionAttribute.cs new file mode 100644 index 00000000..ed41b048 --- /dev/null +++ b/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleEnumOptionAttribute.cs @@ -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 : ToolkitSampleOptionBaseAttribute where TEnum : struct, Enum +{ + /// + /// Creates a new instance of . + /// + /// The name of the generated property, which you can bind to in XAML. + public ToolkitSampleEnumOptionAttribute(string bindingName) + : base(bindingName, null) + { + } + + /// + /// The source generator-friendly type name used for casting. + /// + internal override string TypeName { get; } = "int"; +} diff --git a/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleMultiChoiceOptionAttribute.cs b/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleMultiChoiceOptionAttribute.cs index 406baa52..9806ee9a 100644 --- a/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleMultiChoiceOptionAttribute.cs +++ b/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleMultiChoiceOptionAttribute.cs @@ -9,7 +9,7 @@ namespace CommunityToolkit.Tooling.SampleGen.Attributes; /// /// /// Using this attribute will automatically generate an -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. /// /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] @@ -20,10 +20,10 @@ public sealed class ToolkitSampleMultiChoiceOptionAttribute : ToolkitSampleOptio /// /// The name of the generated property, which you can bind to in XAML. /// 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. - /// A title to display on top of this option. public ToolkitSampleMultiChoiceOptionAttribute(string bindingName, params string[] choices) : base(bindingName, null) { + TypeName = "string"; Choices = choices.Select(x => { if (x.Contains(" : ")) @@ -36,6 +36,18 @@ public ToolkitSampleMultiChoiceOptionAttribute(string bindingName, params string }).ToArray(); } + /// + /// Creates a new instance of . + /// + /// The name of the generated property, which you can bind to in XAML. + /// 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. + public ToolkitSampleMultiChoiceOptionAttribute(string bindingName, List<(string, object)> choices) + : base(bindingName, null) + { + TypeName = "int"; + Choices = choices.Select(x => new MultiChoiceOption(x.Item1, x.Item2)).ToArray(); + } + /// /// A collection of choices to display in the options pane. /// @@ -44,5 +56,5 @@ public ToolkitSampleMultiChoiceOptionAttribute(string bindingName, params string /// /// The source generator-friendly type name used for casting. /// - internal override string TypeName { get; } = "string"; + internal override string TypeName { get; } } diff --git a/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleNumericOptionAttribute.cs b/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleNumericOptionAttribute.cs index 55c9687f..1063d7af 100644 --- a/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleNumericOptionAttribute.cs +++ b/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleNumericOptionAttribute.cs @@ -9,7 +9,7 @@ namespace CommunityToolkit.Tooling.SampleGen.Attributes; /// /// /// Using this attribute will automatically generate an -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. /// /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] @@ -19,8 +19,11 @@ public sealed class ToolkitSampleNumericOptionAttribute : ToolkitSampleOptionBas /// Creates a new instance of . /// /// The name of the generated property, which you can bind to in XAML. - /// 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. - /// A title to display on top of this option. + /// + /// + /// + /// + /// public ToolkitSampleNumericOptionAttribute(string bindingName, double initial = 0, double min = 0, double max = 10, double step = 1, bool showAsNumberBox = false) : base(bindingName, null) { diff --git a/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleOptionBaseAttribute.cs b/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleOptionBaseAttribute.cs index ac4dd33e..1a399228 100644 --- a/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleOptionBaseAttribute.cs +++ b/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleOptionBaseAttribute.cs @@ -14,7 +14,6 @@ public abstract class ToolkitSampleOptionBaseAttribute : Attribute /// /// The name of the generated property, which you can bind to in XAML. /// The initial value for the bound property. - /// A title to display on top of this option. public ToolkitSampleOptionBaseAttribute(string bindingName, object? defaultState) { Name = bindingName; diff --git a/CommunityToolkit.Tooling.SampleGen/GeneratorExtensions.cs b/CommunityToolkit.Tooling.SampleGen/GeneratorExtensions.cs index 8f9aafaf..01c07633 100644 --- a/CommunityToolkit.Tooling.SampleGen/GeneratorExtensions.cs +++ b/CommunityToolkit.Tooling.SampleGen/GeneratorExtensions.cs @@ -30,9 +30,9 @@ public static IEnumerable 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; } @@ -95,7 +95,7 @@ public static T ReconstructAs(this AttributeData attributeData) } /// - /// Checks whether or not a given type symbol has a specified full name. + /// Checks whether a given type symbol has a specified full name. /// /// The input instance to check. /// The full name to check. @@ -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) { @@ -163,7 +163,7 @@ public static bool HasFullyQualifiedName(this ISymbol symbol, string name) /// The target instance to check. /// The name of the argument to check. /// The resulting argument value, if present. - /// Whether or not contains an argument named with a valid value. + /// Whether contains an argument named with a valid value. public static bool TryGetNamedArgument(this AttributeData attributeData, string name, out T? value) { foreach (KeyValuePair properties in attributeData.NamedArguments) diff --git a/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Sample.cs b/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Sample.cs index c452936a..5a91e1ff 100644 --- a/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Sample.cs +++ b/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Sample.cs @@ -8,7 +8,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using System.Linq; namespace CommunityToolkit.Tooling.SampleGen; @@ -50,11 +49,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // Skip diagnostics for symbols in referenced assemblies. We can't place diagnostics here even if we tried, // and running diagnostics here will cause errors when executing in the sample project, due to not finding sample metadata in the provided symbols. Execute(symbolsInReferencedAssemblies, skipDiagnostics: true); + return; void Execute(IncrementalValuesProvider types, bool skipDiagnostics = false, bool skipRegistry = false) { // Get all attributes + the original type symbol. - var allAttributeData = types.SelectMany(static (sym, _) => sym.GetAttributes().Select(x => (sym, x))); + var allAttributeData = types.SelectMany(static (sym, _) => sym.GetAttributes().Select(x => (Symbol: sym, AttributeData: x))); // Find and reconstruct generated pane option attributes + the original type symbol. var generatedPaneOptions = allAttributeData @@ -62,39 +62,52 @@ void Execute(IncrementalValuesProvider types, bool skipDiagnostics = fa { (ISymbol Symbol, ToolkitSampleOptionBaseAttribute Attribute) item = default; + if (x.AttributeData.AttributeClass?.ContainingNamespace.ToDisplayString() == typeof(ToolkitSampleEnumOptionAttribute<>).Namespace + && x.AttributeData.AttributeClass?.MetadataName == typeof(ToolkitSampleEnumOptionAttribute<>).Name) + { + if (x.AttributeData.AttributeClass.TypeArguments.FirstOrDefault() is { } typeSymbol) + { + var parameters = x.AttributeData.ConstructorArguments.Select(GeneratorExtensions.PrepareParameterTypeForActivator).ToList(); + var members = typeSymbol.GetMembers().OfType().Select(t => (t.Name, t.ConstantValue!)).ToList(); + parameters.Add(members); + var multiChoiceOptionAttribute = (ToolkitSampleMultiChoiceOptionAttribute)Activator.CreateInstance(typeof(ToolkitSampleMultiChoiceOptionAttribute), parameters.ToArray()); + item = (x.Symbol, multiChoiceOptionAttribute); + } + } // Reconstruct declared sample option attribute class instances from Roslyn symbols. - if (x.Item2.TryReconstructAs() is ToolkitSampleBoolOptionAttribute boolOptionAttribute) + else if (x.AttributeData.TryReconstructAs() is { } boolOptionAttribute) { - item = (x.Item1, boolOptionAttribute); + item = (x.Symbol, boolOptionAttribute); } - else if (x.Item2.TryReconstructAs() is ToolkitSampleMultiChoiceOptionAttribute multiChoiceOptionAttribute) + else if (x.AttributeData.TryReconstructAs() is { } multiChoiceOptionAttribute) { - item = (x.Item1, multiChoiceOptionAttribute); + item = (x.Symbol, multiChoiceOptionAttribute); } - else if (x.Item2.TryReconstructAs() is ToolkitSampleNumericOptionAttribute numericOptionAttribute) + else if (x.AttributeData.TryReconstructAs() is { } numericOptionAttribute) { - item = (x.Item1, numericOptionAttribute); + item = (x.Symbol, numericOptionAttribute); } - else if (x.Item2.TryReconstructAs() is ToolkitSampleTextOptionAttribute textOptionAttribute) + else if (x.AttributeData.TryReconstructAs() is { } textOptionAttribute) { - item = (x.Item1, textOptionAttribute); + item = (x.Symbol, textOptionAttribute); } // Add extra property data, like Title, back to Attribute - if (item.Attribute is not null && x.Item2.TryGetNamedArgument(nameof(ToolkitSampleOptionBaseAttribute.Title), out string? title) && !string.IsNullOrWhiteSpace(title)) + if (item.Attribute is not null && x.AttributeData.TryGetNamedArgument(nameof(ToolkitSampleOptionBaseAttribute.Title), out string? title) && !string.IsNullOrWhiteSpace(title)) { item.Attribute.Title = title; } return item; }) + .Where(static x => x != default) .Collect(); // Find and reconstruct sample attributes var toolkitSampleAttributeData = allAttributeData .Select(static (data, _) => { - if (data.Item2.TryReconstructAs() is ToolkitSampleAttribute sampleAttribute) + if (data.Item2.TryReconstructAs() is { } sampleAttribute) return (Attribute: sampleAttribute, AttachedQualifiedTypeName: data.Item1.ToString(), Symbol: data.Item1); return default; @@ -119,19 +132,18 @@ void Execute(IncrementalValuesProvider types, bool skipDiagnostics = fa { var (((((optionsPaneAttributes, toolkitSampleAttributes), generatedPaneOptions), markdownFiles), csprojFiles), currentAssembly) = data; - var toolkitSampleAttributeData = toolkitSampleAttributes.Where(x => x != default).Distinct(); - var optionsPaneAttributeData = optionsPaneAttributes.Where(x => x != default).Distinct(); - var generatedOptionPropertyData = generatedPaneOptions.Where(x => x.Attribute is not null && x.Symbol is not null); + var toolkitSampleAttributeData = toolkitSampleAttributes.Distinct(); + var optionsPaneAttributeData = optionsPaneAttributes.Distinct(); - var markdownFileData = markdownFiles.Where(x => x != default).Distinct(); - var csprojFileData = csprojFiles.Where(x => x != default).Distinct(); + var markdownFileData = markdownFiles.Distinct(); + var csprojFileData = csprojFiles.Distinct(); var markdownProjPairings = markdownFileData.Select((docFile, _) => { // TODO: We use these splits a lot to extra path info, so we should probably make a helper function? - var rootPathFile = docFile.Path.Split(new string[] { @"\components\", "/components/", @"\tooling\", "/tooling/" }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault()?.Split(new char[] { '/', '\\' }).FirstOrDefault(); + var rootPathFile = docFile.Path.Split(new string[] { @"\components\", "/components/", @"\tooling\", "/tooling/" }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault()?.Split('/', '\\').FirstOrDefault(); - var csproj = csprojFileData.FirstOrDefault(csProjFile => csProjFile.Path.Split(new string[] { @"\components\", "/components/", @"\tooling\", "/tooling/" }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault()?.Split(new char[] { '/', '\\' }).FirstOrDefault() == rootPathFile); + var csproj = csprojFileData.FirstOrDefault(csProjFile => csProjFile.Path.Split(new string[] { @"\components\", "/components/", @"\tooling\", "/tooling/" }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault()?.Split('/', '\\').FirstOrDefault() == rootPathFile); return (docFile, csproj); }); @@ -150,7 +162,7 @@ void Execute(IncrementalValuesProvider types, bool skipDiagnostics = fa sample.Attribute.Description, sample.AttachedQualifiedTypeName, optionsPaneAttributeData.FirstOrDefault(x => x.Item1?.SampleId == sample.Attribute.Id).Item2?.ToString(), - generatedOptionPropertyData.Where(x => x.Symbol.Equals(sample.Symbol, SymbolEqualityComparer.Default)).Select(x => x.Item2) + generatedPaneOptions.Where(x => x.Symbol.Equals(sample.Symbol, SymbolEqualityComparer.Default)).Select(x => x.Item2) ) ); @@ -158,7 +170,7 @@ void Execute(IncrementalValuesProvider types, bool skipDiagnostics = fa if (isExecutingInSampleProject && !skipDiagnostics) { - ReportSampleDiagnostics(ctx, toolkitSampleAttributeData, optionsPaneAttributeData, generatedOptionPropertyData); + ReportSampleDiagnostics(ctx, toolkitSampleAttributeData, optionsPaneAttributeData, generatedPaneOptions); ReportDocumentDiagnostics(ctx, sampleMetadata, markdownFileData, toolkitSampleAttributeData, docFrontMatter); } @@ -181,7 +193,7 @@ private static void CreateSampleRegistry(SourceProductionContext ctx, Dictionary return; var source = BuildRegistrationCallsFromMetadata(sampleMetadata); - ctx.AddSource($"ToolkitSampleRegistry.g.cs", source); + ctx.AddSource("ToolkitSampleRegistry.g.cs", source); } private static void ReportSampleDiagnostics(SourceProductionContext ctx, @@ -232,7 +244,7 @@ private static void ReportDiagnosticsForLinkedOptionsPane(SourceProductionContex IEnumerable<(ToolkitSampleOptionsPaneAttribute?, ISymbol)> optionsPaneAttribute) { // Check for options pane attributes with no matching sample ID - var optionsPaneAttributeWithMissingOrInvalidSampleId = optionsPaneAttribute.Where(x => !toolkitSampleAttributeData.Any(sample => sample.Attribute.Id == x.Item1?.SampleId)); + var optionsPaneAttributeWithMissingOrInvalidSampleId = optionsPaneAttribute.Where(x => toolkitSampleAttributeData.All(sample => sample.Attribute.Id != x.Item1?.SampleId)); foreach (var item in optionsPaneAttributeWithMissingOrInvalidSampleId) ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.OptionsPaneAttributeWithMissingOrInvalidSampleId, item.Item2.Locations.FirstOrDefault())); @@ -287,16 +299,18 @@ private static void ReportGeneratedMultiChoiceOptionsPaneDiagnostics(SourceProdu private static string BuildRegistrationCallsFromMetadata(IDictionary sampleMetadata) { - return $@"#nullable enable -namespace CommunityToolkit.Tooling.SampleGen; + return $$""" + #nullable enable + namespace CommunityToolkit.Tooling.SampleGen; -public static class ToolkitSampleRegistry -{{ - public static System.Collections.Generic.Dictionary Listing - {{ get; }} = new() {{ - {string.Join(",\n ", sampleMetadata.Select(MetadataToRegistryCall).ToArray())} - }}; -}}"; + public static class ToolkitSampleRegistry + { + public static System.Collections.Generic.Dictionary Listing { get; } = new() + { + {{string.Join(",\n ", sampleMetadata.Select(MetadataToRegistryCall).ToArray())}} + }; + } + """; } private static string MetadataToRegistryCall(KeyValuePair kvp) @@ -315,26 +329,18 @@ private static IEnumerable BuildNewGeneratedSampleOptionMetadataSource(T { foreach (var item in sample.GeneratedSampleOptions ?? Enumerable.Empty()) { - if (item is ToolkitSampleMultiChoiceOptionAttribute multiChoiceAttr) - { - yield return $@"new {typeof(ToolkitSampleMultiChoiceOptionMetadataViewModel).FullName}(name: ""{multiChoiceAttr.Name}"", options: new[] {{ {string.Join(",", multiChoiceAttr.Choices.Select(x => $@"new {typeof(MultiChoiceOption).FullName}(""{x.Label}"", ""{x.Value}"")").ToArray())} }}, title: ""{multiChoiceAttr.Title}"")"; - } - else if (item is ToolkitSampleBoolOptionAttribute boolAttribute) - { - yield return $@"new {typeof(ToolkitSampleBoolOptionMetadataViewModel).FullName}(name: ""{boolAttribute.Name}"", defaultState: {boolAttribute.DefaultState?.ToString().ToLower()}, title: ""{boolAttribute.Title}"")"; - } - else if (item is ToolkitSampleNumericOptionAttribute numericAttribute) - { - yield return $@"new {typeof(ToolkitSampleNumericOptionMetadataViewModel).FullName}(name: ""{numericAttribute.Name}"", initial: {numericAttribute.Initial}, min: {numericAttribute.Min}, max: {numericAttribute.Max}, step: {numericAttribute.Step}, showAsNumberBox: {numericAttribute.ShowAsNumberBox.ToString().ToLower()}, title: ""{numericAttribute.Title}"")"; - } - else if (item is ToolkitSampleTextOptionAttribute textAttribute) - { - yield return $@"new {typeof(ToolkitSampleTextOptionMetadataViewModel).FullName}(name: ""{textAttribute.Name}"", placeholderText: ""{textAttribute.PlaceholderText}"", title: ""{textAttribute.Title}"")"; - } - else + yield return item switch { - throw new NotSupportedException($"Unsupported or unhandled type {item.GetType()}."); - } + ToolkitSampleMultiChoiceOptionAttribute multiChoiceAttr => + $@"new {typeof(ToolkitSampleMultiChoiceOptionMetadataViewModel).FullName}(name: ""{multiChoiceAttr.Name}"", options: new[] {{ {string.Join(",", multiChoiceAttr.Choices.Select(x => $@"new {typeof(MultiChoiceOption).FullName}(""{x.Label}"", ""{x.Value}"")").ToArray())} }}, title: ""{multiChoiceAttr.Title}"")", + ToolkitSampleBoolOptionAttribute boolAttribute => + $@"new {typeof(ToolkitSampleBoolOptionMetadataViewModel).FullName}(name: ""{boolAttribute.Name}"", defaultState: {boolAttribute.DefaultState?.ToString().ToLower()}, title: ""{boolAttribute.Title}"")", + ToolkitSampleNumericOptionAttribute numericAttribute => + $@"new {typeof(ToolkitSampleNumericOptionMetadataViewModel).FullName}(name: ""{numericAttribute.Name}"", initial: {numericAttribute.Initial}, min: {numericAttribute.Min}, max: {numericAttribute.Max}, step: {numericAttribute.Step}, showAsNumberBox: {numericAttribute.ShowAsNumberBox.ToString().ToLower()}, title: ""{numericAttribute.Title}"")", + ToolkitSampleTextOptionAttribute textAttribute => + $@"new {typeof(ToolkitSampleTextOptionMetadataViewModel).FullName}(name: ""{textAttribute.Name}"", placeholderText: ""{textAttribute.PlaceholderText}"", title: ""{textAttribute.Title}"")", + _ => throw new NotSupportedException($"Unsupported or unhandled type {item.GetType()}.") + }; } } diff --git a/CommunityToolkit.Tooling.SampleGen/ToolkitSampleOptionGenerator.cs b/CommunityToolkit.Tooling.SampleGen/ToolkitSampleOptionGenerator.cs index b1a512fb..ea363f31 100644 --- a/CommunityToolkit.Tooling.SampleGen/ToolkitSampleOptionGenerator.cs +++ b/CommunityToolkit.Tooling.SampleGen/ToolkitSampleOptionGenerator.cs @@ -29,25 +29,40 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Select(static (x, _) => x!); // Get all attributes + the original type symbol. - var allAttributeData = classes.SelectMany((sym, _) => sym.GetAttributes().Select(x => (sym, x))); + var allAttributeData = classes.SelectMany((sym, _) => sym.GetAttributes().Select(x => (Symbol: sym, AttributeData: x))); // Find and reconstruct attributes. var sampleAttributeOptions = allAttributeData .Select((x, _) => { - if (x.Item2.TryReconstructAs() is ToolkitSampleBoolOptionAttribute boolOptionAttribute) - return (Attribute: (ToolkitSampleOptionBaseAttribute)boolOptionAttribute, AttachedSymbol: x.Item1, Type: typeof(ToolkitSampleBoolOptionMetadataViewModel)); + (ToolkitSampleOptionBaseAttribute Attribute, ISymbol AttachedSymbol, Type Type) item = default; - if (x.Item2.TryReconstructAs() is ToolkitSampleMultiChoiceOptionAttribute multiChoiceOptionAttribute) - return (Attribute: (ToolkitSampleOptionBaseAttribute)multiChoiceOptionAttribute, AttachedSymbol: x.Item1, Type: typeof(ToolkitSampleMultiChoiceOptionMetadataViewModel)); - - if (x.Item2.TryReconstructAs() is ToolkitSampleNumericOptionAttribute numericOptionAttribute) - return (Attribute: (ToolkitSampleOptionBaseAttribute)numericOptionAttribute, AttachedSymbol: x.Item1, Type: typeof(ToolkitSampleNumericOptionMetadataViewModel)); - - if (x.Item2.TryReconstructAs() is ToolkitSampleTextOptionAttribute textOptionAttribute) - return (Attribute: (ToolkitSampleOptionBaseAttribute)textOptionAttribute, AttachedSymbol: x.Item1, Type: typeof(ToolkitSampleTextOptionMetadataViewModel)); + if (x.AttributeData.AttributeClass?.ContainingNamespace.ToDisplayString() == typeof(ToolkitSampleEnumOptionAttribute<>).Namespace + && x.AttributeData.AttributeClass?.MetadataName == typeof(ToolkitSampleEnumOptionAttribute<>).Name) + { + var parameters = x.AttributeData.ConstructorArguments.Select(GeneratorExtensions.PrepareParameterTypeForActivator).ToList(); + parameters.Add(new List<(string, object)>()); + var multiChoiceOptionAttribute = (ToolkitSampleMultiChoiceOptionAttribute)Activator.CreateInstance(typeof(ToolkitSampleMultiChoiceOptionAttribute), parameters.ToArray()); + item = (multiChoiceOptionAttribute, x.Symbol, typeof(ToolkitSampleMultiChoiceOptionMetadataViewModel)); + } + else if (x.AttributeData.TryReconstructAs() is { } boolOptionAttribute) + { + item = (boolOptionAttribute, x.Symbol, typeof(ToolkitSampleBoolOptionMetadataViewModel)); + } + else if (x.AttributeData.TryReconstructAs() is { } multiChoiceOptionAttribute) + { + item = (multiChoiceOptionAttribute, x.Symbol, typeof(ToolkitSampleMultiChoiceOptionMetadataViewModel)); + } + else if (x.AttributeData.TryReconstructAs() is { } numericOptionAttribute) + { + item = (numericOptionAttribute, x.Symbol, typeof(ToolkitSampleNumericOptionMetadataViewModel)); + } + else if (x.AttributeData.TryReconstructAs() is { } textOptionAttribute) + { + item = (textOptionAttribute, x.Symbol, typeof(ToolkitSampleTextOptionMetadataViewModel)); + } - return default; + return item; }) .Where(x => x != default); @@ -98,87 +113,92 @@ public void Initialize(IncrementalGeneratorInitializationContext context) ctx.AddSource(name, dependencyPropertySource); } }); - } private static string BuildINotifyPropertyChangedImplementation(ISymbol attachedSymbol) { - return $@"#nullable enable -using System.ComponentModel; - -namespace {attachedSymbol.ContainingNamespace} -{{ - public partial class {attachedSymbol.Name} : {nameof(System.ComponentModel.INotifyPropertyChanged)} - {{ - public event PropertyChangedEventHandler? PropertyChanged; - }} -}} -"; + return $$""" + #nullable enable + using System.ComponentModel; + + namespace {{attachedSymbol.ContainingNamespace}} + { + public partial class {{attachedSymbol.Name}} : {{nameof(INotifyPropertyChanged)}} + { + public event PropertyChangedEventHandler? PropertyChanged; + } + } + + """; } private static string BuildGeneratedPropertyMetadataContainer(ISymbol attachedSymbol) { - return $@"#nullable enable -using System.ComponentModel; -using System.Collections.Generic; - -namespace {attachedSymbol.ContainingNamespace} -{{ - public partial class {attachedSymbol.Name} : {typeof(IToolkitSampleGeneratedOptionPropertyContainer).Namespace}.{nameof(IToolkitSampleGeneratedOptionPropertyContainer)} - {{ - private {typeof(IGeneratedToolkitSampleOptionViewModel).FullName}[]? _generatedPropertyMetadata; - - public {typeof(IGeneratedToolkitSampleOptionViewModel).FullName}[]? GeneratedPropertyMetadata - {{ - get => _generatedPropertyMetadata; - set - {{ - if (!(_generatedPropertyMetadata is null)) - {{ - foreach (var item in _generatedPropertyMetadata) - item.PropertyChanged -= OnPropertyChanged; - }} - - if (!(value is null)) - {{ - foreach (var item in value) - item.PropertyChanged += OnPropertyChanged; - }} - - _generatedPropertyMetadata = value; - }} - }} - - private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) => PropertyChanged?.Invoke(this, e); - }} -}} -"; + return $$""" + #nullable enable + using System.ComponentModel; + using System.Collections.Generic; + + namespace {{attachedSymbol.ContainingNamespace}} + { + public partial class {{attachedSymbol.Name}} : {{typeof(IToolkitSampleGeneratedOptionPropertyContainer).Namespace}}.{{nameof(IToolkitSampleGeneratedOptionPropertyContainer)}} + { + private {{typeof(IGeneratedToolkitSampleOptionViewModel).FullName}}[]? _generatedPropertyMetadata; + + public {{typeof(IGeneratedToolkitSampleOptionViewModel).FullName}}[]? GeneratedPropertyMetadata + { + get => _generatedPropertyMetadata; + set + { + if (_generatedPropertyMetadata is not null) + { + foreach (var item in _generatedPropertyMetadata) + item.PropertyChanged -= OnPropertyChanged; + } + + if (value is not null) + { + foreach (var item in value) + item.PropertyChanged += OnPropertyChanged; + } + + _generatedPropertyMetadata = value; + } + } + + private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) => PropertyChanged?.Invoke(this, e); + } + } + + """; } private static string BuildProperty(ISymbol containingClassSymbol, string propertyName, string typeName, Type viewModelType) { - return $@"#nullable enable -using System.ComponentModel; -using System.Linq; - -namespace {containingClassSymbol.ContainingNamespace} -{{ - public partial class {containingClassSymbol.Name} - {{ - public {typeName} {propertyName} - {{ - get => (({typeName})(({viewModelType.FullName})GeneratedPropertyMetadata!.First(x => x.Name == ""{propertyName}""))!.Value!)!; - set - {{ - if (GeneratedPropertyMetadata?.FirstOrDefault(x => x.Name == nameof({propertyName})) is {viewModelType.FullName} metadata) - {{ - metadata.Value = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof({propertyName}))); - }} - }} - }} - }} -}} -"; + return $$""" + #nullable enable + using System.ComponentModel; + using System.Linq; + + namespace {{containingClassSymbol.ContainingNamespace}} + { + public partial class {{containingClassSymbol.Name}} + { + public {{typeName}} {{propertyName}} + { + get => (({{typeName}})(({{viewModelType.FullName}})GeneratedPropertyMetadata!.First(x => x.Name == "{{propertyName}}"))!.Value!)!; + set + { + if (GeneratedPropertyMetadata?.FirstOrDefault(x => x.Name == nameof({{propertyName}})) is {{viewModelType.FullName}} metadata) + { + metadata.Value = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof({{propertyName}}))); + } + } + } + } + } + + """; } }