diff --git a/CommunityToolkit.App.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml b/CommunityToolkit.App.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml index f92426ca..f6618a85 100644 --- a/CommunityToolkit.App.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml +++ b/CommunityToolkit.App.Shared/Renderers/GeneratedSampleOptionsRenderer.xaml @@ -17,52 +17,52 @@ + IsOn="{x:Bind BoolValue, Mode=TwoWay}" /> + ItemsSource="{x:Bind Options}" + SelectedIndex="0" + SelectedItem="{x:Bind Value, Mode=TwoWay}" /> + Maximum="{x:Bind Max, Mode=OneWay}" + Minimum="{x:Bind Min, Mode=OneWay}" + StepFrequency="{x:Bind Step, Mode=OneWay}" + Value="{x:Bind Initial, Mode=TwoWay}" /> + Maximum="{x:Bind Max, Mode=OneWay}" + Minimum="{x:Bind Min, Mode=OneWay}" + SmallChange="{x:Bind Step, Mode=OneWay}" + SpinButtonPlacementMode="Compact" + Value="{x:Bind Initial, Mode=TwoWay}" /> + Header="{x:Bind Title, Mode=OneWay}" + Text="{x:Bind PlaceholderText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> + ItemsSource="{x:Bind SampleOptions, Mode=OneWay}"> + Spacing="12" /> diff --git a/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/InMemoryAdditionalText.cs b/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/InMemoryAdditionalText.cs new file mode 100644 index 00000000..45888822 --- /dev/null +++ b/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/InMemoryAdditionalText.cs @@ -0,0 +1,19 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace CommunityToolkit.Tooling.SampleGen.Tests.Helpers; + +internal class InMemoryAdditionalText : AdditionalText +{ + private readonly SourceText _content; + + public InMemoryAdditionalText(string path, string content) + { + Path = path; + _content = SourceText.From(content, Encoding.UTF8); + } + + public override string Path { get; } + + public override SourceText GetText(CancellationToken cancellationToken = default) => _content; +} diff --git a/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/SourceGeneratorRunResult.cs b/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/SourceGeneratorRunResult.cs new file mode 100644 index 00000000..37ac706d --- /dev/null +++ b/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/SourceGeneratorRunResult.cs @@ -0,0 +1,10 @@ +// 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. + +using Microsoft.CodeAnalysis; +using System.Collections.Immutable; + +namespace CommunityToolkit.Tooling.SampleGen.Tests.Helpers; + +public record SourceGeneratorRunResult(Compilation Compilation, ImmutableArray Diagnostics); diff --git a/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.Compilation.cs b/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.Compilation.cs new file mode 100644 index 00000000..5d309c28 --- /dev/null +++ b/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.Compilation.cs @@ -0,0 +1,60 @@ +// 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. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using System.Collections.Immutable; + +namespace CommunityToolkit.Tooling.SampleGen.Tests.Helpers; + +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; + } + + internal static SyntaxTree ToSyntaxTree(this string source) + { + return CSharpSyntaxTree.ParseText(source, + CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp10)); + } + + internal static CSharpCompilation CreateCompilation(this SyntaxTree syntaxTree, string assemblyName, IEnumerable? references = null) + { + return CSharpCompilation.Create(assemblyName, new[] { syntaxTree }, references ?? GetAllReferencedAssemblies(), new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + } + + internal static CSharpCompilation CreateCompilation(this IEnumerable syntaxTree, string assemblyName, IEnumerable? references = null) + { + return CSharpCompilation.Create(assemblyName, syntaxTree, references ?? GetAllReferencedAssemblies(), new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + } + + internal static GeneratorDriver CreateSourceGeneratorDriver(this SyntaxTree syntaxTree, params IIncrementalGenerator[] generators) + { + return CSharpGeneratorDriver.Create(generators).WithUpdatedParseOptions((CSharpParseOptions)syntaxTree.Options); + } + + internal static GeneratorDriver CreateSourceGeneratorDriver(this Compilation compilation, params IIncrementalGenerator[] generators) + { + return CSharpGeneratorDriver.Create(generators).WithUpdatedParseOptions((CSharpParseOptions)compilation.SyntaxTrees.First().Options); + } + + internal static GeneratorDriver WithMarkdown(this GeneratorDriver driver, params string[] markdownFilesToCreate) + { + foreach (var markdown in markdownFilesToCreate) + { + if (!string.IsNullOrWhiteSpace(markdown)) + { + var text = new InMemoryAdditionalText(@"C:\pathtorepo\components\experiment\samples\experiment.Samples\documentation.md", markdown); + driver = driver.AddAdditionalTexts(ImmutableArray.Create(text)); + } + } + + return driver; + } +} diff --git a/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.cs b/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.cs new file mode 100644 index 00000000..c94da675 --- /dev/null +++ b/CommunityToolkit.Tooling.SampleGen.Tests/Helpers/TestHelpers.cs @@ -0,0 +1,61 @@ +using Microsoft.CodeAnalysis; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Immutable; + +namespace CommunityToolkit.Tooling.SampleGen.Tests.Helpers; + +public static partial class TestHelpers +{ + internal static SourceGeneratorRunResult RunSourceGenerator(this string source, string assemblyName, string markdown = "") where TGenerator : class, IIncrementalGenerator, new() => RunSourceGenerator(source.ToSyntaxTree(), assemblyName, markdown); + + internal static SourceGeneratorRunResult RunSourceGenerator(this SyntaxTree syntaxTree, string assemblyName, string markdown = "") + where TGenerator : class, IIncrementalGenerator, new() + { + var compilation = syntaxTree.CreateCompilation(assemblyName); // assembly name should always be supplied in param + return RunSourceGenerator(compilation, markdown); + } + + internal static SourceGeneratorRunResult RunSourceGenerator(this Compilation compilation, string markdown = "") + where TGenerator : class, IIncrementalGenerator, new() + { + // Create a driver for the source generator + var driver = compilation + .CreateSourceGeneratorDriver(new TGenerator()) + .WithMarkdown(markdown); + + // Update the original compilation using the source generator + _ = driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation generatorCompilation, out ImmutableArray postGeneratorCompilationDiagnostics); + + return new(generatorCompilation, postGeneratorCompilationDiagnostics); + } + + internal static void AssertDiagnosticsAre(this IEnumerable diagnostics, params DiagnosticDescriptor[] expectedDiagnosticDescriptors) + { + var expectedIds = expectedDiagnosticDescriptors.Select(x => x.Id).ToHashSet(); + var resultingIds = diagnostics.Select(diagnostic => diagnostic.Id).ToHashSet(); + + Assert.IsTrue(resultingIds.SetEquals(expectedIds), $"Expected [{string.Join(", ", expectedIds)}] diagnostic Ids. Got [{string.Join(", ", resultingIds)}]"); + } + + internal static void AssertNoCompilationErrors(this Compilation outputCompilation) + { + var generatedCompilationDiagnostics = outputCompilation.GetDiagnostics(); + Assert.IsTrue(generatedCompilationDiagnostics.All(x => x.Severity != DiagnosticSeverity.Error), $"Expected no generated compilation errors. Got: \n{string.Join("\n", generatedCompilationDiagnostics.Where(x => x.Severity == DiagnosticSeverity.Error).Select(x => $"[{x.Id}: {x.GetMessage()}]"))}"); + } + + internal static string GetFileContentsByName(this Compilation compilation, string filename) + { + var generatedTree = compilation.SyntaxTrees.SingleOrDefault(tree => Path.GetFileName(tree.FilePath) == filename); + Assert.IsNotNull(generatedTree, $"No file named {filename} was generated"); + + return generatedTree.ToString(); + } + + internal static void AssertSourceGenerated(this Compilation compilation, string filename, string expectedContents) + { + } + + internal static void AssertDiagnosticsAre(this SourceGeneratorRunResult result, params DiagnosticDescriptor[] expectedDiagnosticDescriptors) => AssertDiagnosticsAre(result.Diagnostics, expectedDiagnosticDescriptors); + + internal static void AssertNoCompilationErrors(this SourceGeneratorRunResult result) => AssertNoCompilationErrors(result.Compilation); +} diff --git a/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleGeneratedPaneTests.cs b/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleGeneratedPaneTests.cs new file mode 100644 index 00000000..17d5e8ee --- /dev/null +++ b/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleGeneratedPaneTests.cs @@ -0,0 +1,385 @@ +// 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. + +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; + +[TestClass] +public partial class ToolkitSampleGeneratedPaneTests +{ + private const string SAMPLE_ASM_NAME = "PaneTests.Samples"; + + [TestMethod] + public void PaneOption_GeneratesWithoutDiagnostics() + { + var source = $@" + using System.ComponentModel; + using CommunityToolkit.Tooling.SampleGen; + using CommunityToolkit.Tooling.SampleGen.Attributes; + + namespace MyApp + {{ + [ToolkitSampleBoolOption(""Test"", false, Title = ""Toggle y"")] + [ToolkitSampleMultiChoiceOption(""TextFontFamily"", ""Segoe UI"", ""Arial"", ""Consolas"", Title = ""Font family"")] + + [ToolkitSample(id: nameof(Sample), ""Test Sample"", description: """")] + public partial class Sample : Windows.UI.Xaml.Controls.UserControl + {{ + public Sample() + {{ + var x = this.Test; + var y = this.TextFontFamily; + }} + }} + }} + + namespace Windows.UI.Xaml.Controls + {{ + public class UserControl {{ }} + }}"; + + var result = source.RunSourceGenerator(SAMPLE_ASM_NAME); + + result.AssertNoCompilationErrors(); + result.AssertDiagnosticsAre(); + } + + [TestMethod] + public void PaneOption_GeneratesTitleProperty() + { + // The sample registry is designed to be declared in the sample project, and generated in the project head where its displayed in the UI as data. + // To test the contents of the generated sample registry, we must replicate this setup. + var sampleProjectAssembly = """ + using System.ComponentModel; + using CommunityToolkit.Tooling.SampleGen; + using CommunityToolkit.Tooling.SampleGen.Attributes; + + namespace MyApp + { + [ToolkitSampleNumericOption("TextSize", 12, 8, 48, 2, false, Title = "FontSize")] + [ToolkitSample(id: nameof(Sample), "Test Sample", description: "")] + public partial class Sample : Windows.UI.Xaml.Controls.UserControl + { + public Sample() + { + } + } + } + + namespace Windows.UI.Xaml.Controls + { + public class UserControl { } + } + """.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.ToolkitSampleNumericOptionMetadataViewModel(name: "TextSize", initial: 12, min: 8, max: 48, step: 2, showAsNumberBox: false, title: "FontSize") }) + }; + } + """, "Unexpected code generated"); + } + + [TestMethod] + public void PaneOption_GeneratesProperty_DuplicatePropNamesAcrossSampleClasses() + { + var source = $@" + using System.ComponentModel; + using CommunityToolkit.Tooling.SampleGen; + using CommunityToolkit.Tooling.SampleGen.Attributes; + + namespace MyApp + {{ + [ToolkitSampleBoolOption(""Test"", false, Title = ""Toggle y"")] + [ToolkitSampleMultiChoiceOption(""TextFontFamily"", ""Segoe UI"", ""Arial"", ""Consolas"", Title = ""Font family"")] + + [ToolkitSample(id: nameof(Sample), ""Test Sample"", description: """")] + public partial class Sample : Windows.UI.Xaml.Controls.UserControl + {{ + public Sample() + {{ + var x = this.Test; + var y = this.TextFontFamily; + }} + }} + + [ToolkitSampleBoolOption(""Test"", false, Title = ""Toggle y"")] + [ToolkitSampleMultiChoiceOption(""TextFontFamily"", ""Segoe UI"", ""Arial"", ""Consolas"", Title = ""Font family"")] + + [ToolkitSample(id: nameof(Sample2), ""Test Sample"", description: """")] + public partial class Sample2 : Windows.UI.Xaml.Controls.UserControl + {{ + public Sample2() + {{ + var x = this.Test; + var y = this.TextFontFamily; + }} + }} + }} + + namespace Windows.UI.Xaml.Controls + {{ + public class UserControl {{ }} + }}"; + + var result = source.RunSourceGenerator(SAMPLE_ASM_NAME); + + result.AssertNoCompilationErrors(); + result.AssertDiagnosticsAre(); + } + + [TestMethod] + public void PaneOptionOnNonSample() + { + string source = @" + using System.ComponentModel; + using CommunityToolkit.Tooling.SampleGen.Attributes; + + namespace MyApp + { + [ToolkitSampleBoolOption(""BindToMe"", false, Title = ""Toggle visibility"")] + public partial class Sample : Windows.UI.Xaml.Controls.UserControl + { + } + } + + namespace Windows.UI.Xaml.Controls + { + public class UserControl { } + }"; + + var result = source.RunSourceGenerator(SAMPLE_ASM_NAME); + result.Diagnostics.AssertDiagnosticsAre(DiagnosticDescriptors.SamplePaneOptionAttributeOnNonSample); + } + + [DataRow("", DisplayName = "Empty string"), DataRow(" ", DisplayName = "Only whitespace"), DataRow("Test ", DisplayName = "Text with whitespace")] + [DataRow("_", DisplayName = "Underscore"), DataRow("$", DisplayName = "Dollar sign"), DataRow("%", DisplayName = "Percent symbol")] + [DataRow("class", DisplayName = "Reserved keyword 'class'"), DataRow("string", DisplayName = "Reserved keyword 'string'"), DataRow("sealed", DisplayName = "Reserved keyword 'sealed'"), DataRow("ref", DisplayName = "Reserved keyword 'ref'")] + [TestMethod] + public void PaneOptionWithBadName(string name) + { + var source = $@" + using System.ComponentModel; + using CommunityToolkit.Tooling.SampleGen; + using CommunityToolkit.Tooling.SampleGen.Attributes; + + namespace MyApp + {{ + [ToolkitSample(id: nameof(Sample), ""Test Sample"", description: """")] + [ToolkitSampleBoolOption(""{name}"", false, Title = ""Toggle visibility"")] + public partial class Sample : Windows.UI.Xaml.Controls.UserControl + {{ + }} + }} + + namespace Windows.UI.Xaml.Controls + {{ + public class UserControl {{ }} + }}"; + + var result = source.RunSourceGenerator(SAMPLE_ASM_NAME); + + result.AssertDiagnosticsAre(DiagnosticDescriptors.SamplePaneOptionWithBadName, DiagnosticDescriptors.SampleNotReferencedInMarkdown); + } + + [TestMethod] + public void PaneOptionWithConflictingPropertyName() + { + var source = $@" + using System.ComponentModel; + using CommunityToolkit.Tooling.SampleGen; + using CommunityToolkit.Tooling.SampleGen.Attributes; + + namespace MyApp + {{ + [ToolkitSampleBoolOption(""IsVisible"", false, Title = ""Toggle x"")] + [ToolkitSample(id: nameof(Sample), ""Test Sample"", description: """")] + public partial class Sample : Windows.UI.Xaml.Controls.UserControl + {{ + public string IsVisible {{ get; set; }} + }} + }} + + namespace Windows.UI.Xaml.Controls + {{ + public class UserControl {{ }} + }}"; + + var result = source.RunSourceGenerator(SAMPLE_ASM_NAME); + + result.AssertDiagnosticsAre(DiagnosticDescriptors.SamplePaneOptionWithConflictingName, DiagnosticDescriptors.SampleNotReferencedInMarkdown); + } + + [TestMethod] + public void PaneOptionWithConflictingInheritedPropertyName() + { + var source = $@" + using System.ComponentModel; + using CommunityToolkit.Tooling.SampleGen; + using CommunityToolkit.Tooling.SampleGen.Attributes; + + namespace MyApp + {{ + [ToolkitSampleBoolOption(""IsVisible"", false, Title = ""Toggle x"")] + [ToolkitSample(id: nameof(Sample), ""Test Sample"", description: """")] + public partial class Sample : Base + {{ + }} + + public class Base : Windows.UI.Xaml.Controls.UserControl + {{ + public string IsVisible {{ get; set; }} + }} + }} + + namespace Windows.UI.Xaml.Controls + {{ + public class UserControl {{ }} + }}"; + + var result = source.RunSourceGenerator(SAMPLE_ASM_NAME); + + result.AssertDiagnosticsAre(DiagnosticDescriptors.SamplePaneOptionWithConflictingName, DiagnosticDescriptors.SampleNotReferencedInMarkdown); + } + + [TestMethod] + public void PaneOptionWithDuplicateName() + { + var source = $@" + using System.ComponentModel; + using CommunityToolkit.Tooling.SampleGen; + using CommunityToolkit.Tooling.SampleGen.Attributes; + + namespace MyApp + {{ + [ToolkitSampleBoolOption(""test"", false, Title = ""Toggle x"")] + [ToolkitSampleBoolOption(""test"", false, Title = ""Toggle y"")] + [ToolkitSampleMultiChoiceOption(""TextFontFamily"", ""Segoe UI"", ""Arial"", Title = ""Text foreground"")] + + [ToolkitSample(id: nameof(Sample), ""Test Sample"", description: """")] + public partial class Sample : Windows.UI.Xaml.Controls.UserControl + {{ + }} + }} + + namespace Windows.UI.Xaml.Controls + {{ + public class UserControl {{ }} + }}"; + + var result = source.RunSourceGenerator(SAMPLE_ASM_NAME); + + result.AssertDiagnosticsAre(DiagnosticDescriptors.SamplePaneOptionWithDuplicateName, DiagnosticDescriptors.SampleNotReferencedInMarkdown); + } + + [TestMethod] + public void PaneOptionWithDuplicateName_AllowedBetweenSamples() + { + var source = $@" + using System.ComponentModel; + using CommunityToolkit.Tooling.SampleGen; + using CommunityToolkit.Tooling.SampleGen.Attributes; + + namespace MyApp + {{ + [ToolkitSampleBoolOption(""test"", false, Title = ""Toggle y"")] + + [ToolkitSample(id: nameof(Sample), ""Test Sample"", description: """")] + public partial class Sample : Windows.UI.Xaml.Controls.UserControl + {{ + }} + + [ToolkitSampleBoolOption(""test"", false, Title = ""Toggle y"")] + + [ToolkitSample(id: nameof(Sample2), ""Test Sample"", description: """")] + public partial class Sample2 : Windows.UI.Xaml.Controls.UserControl + {{ + }} + }} + + namespace Windows.UI.Xaml.Controls + {{ + public class UserControl {{ }} + }}"; + + var result = source.RunSourceGenerator(SAMPLE_ASM_NAME); + + result.AssertDiagnosticsAre(DiagnosticDescriptors.SampleNotReferencedInMarkdown); + } + + [TestMethod] + public void SampleGeneratedOptionAttributeOnUnsupportedType() + { + var source = $@" + using System.ComponentModel; + using CommunityToolkit.Tooling.SampleGen; + using CommunityToolkit.Tooling.SampleGen.Attributes; + + namespace MyApp + {{ + [ToolkitSampleMultiChoiceOption(""TextFontFamily"", ""Segoe UI"", ""Arial"", ""Consolas"", Title = ""Font family"")] + [ToolkitSampleBoolOption(""Test"", false, Title = ""Toggle visibility"")] + public partial class Sample + {{ + }} + }}"; + + var result = source.RunSourceGenerator(SAMPLE_ASM_NAME); + + result.AssertDiagnosticsAre(DiagnosticDescriptors.SampleGeneratedOptionAttributeOnUnsupportedType, DiagnosticDescriptors.SamplePaneOptionAttributeOnNonSample); + } + + [TestMethod] + public void PaneMultipleChoiceOptionWithNoChoices() + { + var source = $@" + using System.ComponentModel; + using CommunityToolkit.Tooling.SampleGen; + using CommunityToolkit.Tooling.SampleGen.Attributes; + + namespace MyApp + {{ + [ToolkitSampleMultiChoiceOption(""TextFontFamily"", Title = ""Font family"")] + + [ToolkitSample(id: nameof(Sample), ""Test Sample"", description: """")] + public partial class Sample : Windows.UI.Xaml.Controls.UserControl + {{ + }} + }} + + namespace Windows.UI.Xaml.Controls + {{ + public class UserControl {{ }} + }}"; + + var result = source.RunSourceGenerator(SAMPLE_ASM_NAME); + + result.AssertDiagnosticsAre(DiagnosticDescriptors.SamplePaneMultiChoiceOptionWithNoChoices, DiagnosticDescriptors.SampleNotReferencedInMarkdown); + } +} diff --git a/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.Documentation.cs b/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.Documentation.cs index 1b2b156e..f9c6ffef 100644 --- a/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.Documentation.cs +++ b/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.Documentation.cs @@ -7,6 +7,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.VisualStudio.TestTools.UnitTesting; +using CommunityToolkit.Tooling.SampleGen.Tests.Helpers; namespace CommunityToolkit.Tooling.SampleGen.Tests; @@ -40,7 +41,10 @@ public void MissingFrontMatterSection() Without any front matter. "; - VerifyGeneratedDiagnostics(SimpleSource, markdown, DiagnosticDescriptors.MarkdownYAMLFrontMatterException.Id, DiagnosticDescriptors.SampleNotReferencedInMarkdown.Id); + var result = SimpleSource.RunSourceGenerator(SAMPLE_ASM_NAME, markdown); + + result.AssertNoCompilationErrors(); + result.AssertDiagnosticsAre(DiagnosticDescriptors.MarkdownYAMLFrontMatterException, DiagnosticDescriptors.SampleNotReferencedInMarkdown); } [DataRow(1, DisplayName = "Title")] @@ -74,7 +78,11 @@ public void MissingFrontMatterField(int removeline) lines.RemoveAt(removeline); markdown = String.Join('\n', lines); - VerifyGeneratedDiagnostics(SimpleSource, markdown, DiagnosticDescriptors.MarkdownYAMLFrontMatterMissingField.Id, DiagnosticDescriptors.SampleNotReferencedInMarkdown.Id); // We won't see the sample reference as we bail out when the front matter fails to be complete... + var result = SimpleSource.RunSourceGenerator(SAMPLE_ASM_NAME, markdown); + + // We won't see the sample reference as we bail out when the front matter fails to be complete + result.AssertNoCompilationErrors(); + result.AssertDiagnosticsAre(DiagnosticDescriptors.MarkdownYAMLFrontMatterMissingField, DiagnosticDescriptors.SampleNotReferencedInMarkdown); } [TestMethod] @@ -97,9 +105,10 @@ public void MarkdownInvalidSampleReference() Without any front matter. "; - VerifyGeneratedDiagnostics(SimpleSource, markdown, - DiagnosticDescriptors.MarkdownSampleIdNotFound.Id, - DiagnosticDescriptors.SampleNotReferencedInMarkdown.Id); + var result = SimpleSource.RunSourceGenerator(SAMPLE_ASM_NAME, markdown); + + result.AssertNoCompilationErrors(); + result.AssertDiagnosticsAre(DiagnosticDescriptors.MarkdownSampleIdNotFound, DiagnosticDescriptors.SampleNotReferencedInMarkdown); } [TestMethod] @@ -120,9 +129,10 @@ public void DocumentationMissingSample() # This is some test documentation... Without any sample."; - VerifyGeneratedDiagnostics(SimpleSource, markdown, - DiagnosticDescriptors.DocumentationHasNoSamples.Id, - DiagnosticDescriptors.SampleNotReferencedInMarkdown.Id); + var result = SimpleSource.RunSourceGenerator(SAMPLE_ASM_NAME, markdown); + + result.AssertNoCompilationErrors(); + result.AssertDiagnosticsAre(DiagnosticDescriptors.DocumentationHasNoSamples, DiagnosticDescriptors.SampleNotReferencedInMarkdown); } [TestMethod] @@ -143,8 +153,12 @@ public void DocumentationValid() # This is some test documentation... Which is valid. > [!SAMPLE Sample]"; + + + var result = SimpleSource.RunSourceGenerator(SAMPLE_ASM_NAME, markdown); - VerifyGeneratedDiagnostics(SimpleSource, markdown); + result.AssertNoCompilationErrors(); + result.AssertDiagnosticsAre(); } [TestMethod] @@ -165,9 +179,10 @@ public void DocumentationInvalidDiscussionId() # This is some test documentation... Without an invalid discussion id."; - VerifyGeneratedDiagnostics(string.Empty, markdown, - DiagnosticDescriptors.MarkdownYAMLFrontMatterException.Id, - DiagnosticDescriptors.DocumentationHasNoSamples.Id); + var result = string.Empty.RunSourceGenerator(SAMPLE_ASM_NAME, markdown); + + result.AssertNoCompilationErrors(); + result.AssertDiagnosticsAre(DiagnosticDescriptors.MarkdownYAMLFrontMatterException, DiagnosticDescriptors.DocumentationHasNoSamples); } [TestMethod] @@ -188,8 +203,9 @@ public void DocumentationInvalidIssueId() # This is some test documentation... Without an invalid discussion id."; - VerifyGeneratedDiagnostics(string.Empty, markdown, - DiagnosticDescriptors.MarkdownYAMLFrontMatterException.Id, - DiagnosticDescriptors.DocumentationHasNoSamples.Id); + var result = string.Empty.RunSourceGenerator(SAMPLE_ASM_NAME, markdown); + + result.AssertNoCompilationErrors(); + result.AssertDiagnosticsAre(DiagnosticDescriptors.MarkdownYAMLFrontMatterException, DiagnosticDescriptors.DocumentationHasNoSamples); } } diff --git a/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.cs b/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.cs index c051c86b..5c9ddc5d 100644 --- a/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.cs +++ b/CommunityToolkit.Tooling.SampleGen.Tests/ToolkitSampleMetadataTests.cs @@ -2,11 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using CommunityToolkit.Tooling.SampleGen.Attributes; using CommunityToolkit.Tooling.SampleGen.Diagnostics; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Text; +using CommunityToolkit.Tooling.SampleGen.Tests.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Collections.Immutable; using System.ComponentModel.DataAnnotations; @@ -16,341 +13,7 @@ namespace CommunityToolkit.Tooling.SampleGen.Tests; [TestClass] public partial class ToolkitSampleMetadataTests { - [TestMethod] - public void PaneOption_GeneratesWithoutDiagnostics() - { - var source = $@" - using System.ComponentModel; - using CommunityToolkit.Tooling.SampleGen; - using CommunityToolkit.Tooling.SampleGen.Attributes; - - namespace MyApp - {{ - [ToolkitSampleBoolOption(""Test"", false, Title = ""Toggle y"")] - [ToolkitSampleMultiChoiceOption(""TextFontFamily"", ""Segoe UI"", ""Arial"", ""Consolas"", Title = ""Font family"")] - - [ToolkitSample(id: nameof(Sample), ""Test Sample"", description: """")] - public partial class Sample : Windows.UI.Xaml.Controls.UserControl - {{ - public Sample() - {{ - var x = this.Test; - var y = this.TextFontFamily; - }} - }} - }} - - namespace Windows.UI.Xaml.Controls - {{ - public class UserControl {{ }} - }}"; - - VerifyGeneratedDiagnostics(source, string.Empty); - } - - [TestMethod] - public void PaneOption_GeneratesTitleProperty() - { - var source = """ - using System.ComponentModel; - using CommunityToolkit.Tooling.SampleGen; - using CommunityToolkit.Tooling.SampleGen.Attributes; - - namespace MyApp - { - [ToolkitSampleNumericOption("TextSize", 12, 8, 48, 2, false, Title = "FontSize")] - [ToolkitSample(id: nameof(Sample), "Test Sample", description: "")] - public partial class Sample : Windows.UI.Xaml.Controls.UserControl - { - public Sample() - { - var x = this.Test; - var y = this.TextFontFamily; - } - } - } - - namespace Windows.UI.Xaml.Controls - { - public class UserControl { } - } - """; - - var result = """ - #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.ToolkitSampleNumericOptionMetadataViewModel(name: "TextSize", initial: 12, min: 8, max: 48, step: 2, showAsNumberBox: false, title: "FontSize") }) - }; - } - """; - - VerifyGenerateSources("MyApp.Tests", source, new[] { new ToolkitSampleMetadataGenerator() }, ignoreDiagnostics: true, ("ToolkitSampleRegistry.g.cs", result)); - } - - // https://github.com/CommunityToolkit/Labs-Windows/issues/175 - [TestMethod] - public void PaneOption_GeneratesProperty_DuplicatePropNamesAcrossSampleClasses() - { - var source = $@" - using System.ComponentModel; - using CommunityToolkit.Tooling.SampleGen; - using CommunityToolkit.Tooling.SampleGen.Attributes; - - namespace MyApp - {{ - [ToolkitSampleBoolOption(""Test"", false, Title = ""Toggle y"")] - [ToolkitSampleMultiChoiceOption(""TextFontFamily"", ""Segoe UI"", ""Arial"", ""Consolas"", Title = ""Font family"")] - - [ToolkitSample(id: nameof(Sample), ""Test Sample"", description: """")] - public partial class Sample : Windows.UI.Xaml.Controls.UserControl - {{ - public Sample() - {{ - var x = this.Test; - var y = this.TextFontFamily; - }} - }} - - [ToolkitSampleBoolOption(""Test"", false, Title = ""Toggle y"")] - [ToolkitSampleMultiChoiceOption(""TextFontFamily"", ""Segoe UI"", ""Arial"", ""Consolas"", Title = ""Font family"")] - - [ToolkitSample(id: nameof(Sample2), ""Test Sample"", description: """")] - public partial class Sample2 : Windows.UI.Xaml.Controls.UserControl - {{ - public Sample2() - {{ - var x = this.Test; - var y = this.TextFontFamily; - }} - }} - }} - - namespace Windows.UI.Xaml.Controls - {{ - public class UserControl {{ }} - }}"; - - VerifyGeneratedDiagnostics(source, string.Empty); - } - - [TestMethod] - public void PaneOptionOnNonSample() - { - string source = @" - using System.ComponentModel; - using CommunityToolkit.Tooling.SampleGen.Attributes; - - namespace MyApp - { - [ToolkitSampleBoolOption(""BindToMe"", false, Title = ""Toggle visibility"")] - public partial class Sample : Windows.UI.Xaml.Controls.UserControl - { - } - } - - namespace Windows.UI.Xaml.Controls - { - public class UserControl { } - }"; - - VerifyGeneratedDiagnostics(source, string.Empty, DiagnosticDescriptors.SamplePaneOptionAttributeOnNonSample.Id); - } - - [DataRow("", DisplayName = "Empty string"), DataRow(" ", DisplayName = "Only whitespace"), DataRow("Test ", DisplayName = "Text with whitespace")] - [DataRow("_", DisplayName = "Underscore"), DataRow("$", DisplayName = "Dollar sign"), DataRow("%", DisplayName = "Percent symbol")] - [DataRow("class", DisplayName = "Reserved keyword 'class'"), DataRow("string", DisplayName = "Reserved keyword 'string'"), DataRow("sealed", DisplayName = "Reserved keyword 'sealed'"), DataRow("ref", DisplayName = "Reserved keyword 'ref'")] - [TestMethod] - public void PaneOptionWithBadName(string name) - { - var source = $@" - using System.ComponentModel; - using CommunityToolkit.Tooling.SampleGen; - using CommunityToolkit.Tooling.SampleGen.Attributes; - - namespace MyApp - {{ - [ToolkitSample(id: nameof(Sample), ""Test Sample"", description: """")] - [ToolkitSampleBoolOption(""{name}"", false, Title = ""Toggle visibility"")] - public partial class Sample : Windows.UI.Xaml.Controls.UserControl - {{ - }} - }} - - namespace Windows.UI.Xaml.Controls - {{ - public class UserControl {{ }} - }}"; - - VerifyGeneratedDiagnostics(source, string.Empty, DiagnosticDescriptors.SamplePaneOptionWithBadName.Id, DiagnosticDescriptors.SampleNotReferencedInMarkdown.Id); - } - - [TestMethod] - public void PaneOptionWithConflictingPropertyName() - { - var source = $@" - using System.ComponentModel; - using CommunityToolkit.Tooling.SampleGen; - using CommunityToolkit.Tooling.SampleGen.Attributes; - - namespace MyApp - {{ - [ToolkitSampleBoolOption(""IsVisible"", false, Title = ""Toggle x"")] - [ToolkitSample(id: nameof(Sample), ""Test Sample"", description: """")] - public partial class Sample : Windows.UI.Xaml.Controls.UserControl - {{ - public string IsVisible {{ get; set; }} - }} - }} - - namespace Windows.UI.Xaml.Controls - {{ - public class UserControl {{ }} - }}"; - - VerifyGeneratedDiagnostics(source, string.Empty, DiagnosticDescriptors.SamplePaneOptionWithConflictingName.Id, DiagnosticDescriptors.SampleNotReferencedInMarkdown.Id); - } - - [TestMethod] - public void PaneOptionWithConflictingInheritedPropertyName() - { - var source = $@" - using System.ComponentModel; - using CommunityToolkit.Tooling.SampleGen; - using CommunityToolkit.Tooling.SampleGen.Attributes; - - namespace MyApp - {{ - [ToolkitSampleBoolOption(""IsVisible"", false, Title = ""Toggle x"")] - [ToolkitSample(id: nameof(Sample), ""Test Sample"", description: """")] - public partial class Sample : Base - {{ - }} - - public class Base : Windows.UI.Xaml.Controls.UserControl - {{ - public string IsVisible {{ get; set; }} - }} - }} - - namespace Windows.UI.Xaml.Controls - {{ - public class UserControl {{ }} - }}"; - - VerifyGeneratedDiagnostics(source, string.Empty, DiagnosticDescriptors.SamplePaneOptionWithConflictingName.Id, DiagnosticDescriptors.SampleNotReferencedInMarkdown.Id); - } - - [TestMethod] - public void PaneOptionWithDuplicateName() - { - var source = $@" - using System.ComponentModel; - using CommunityToolkit.Tooling.SampleGen; - using CommunityToolkit.Tooling.SampleGen.Attributes; - - namespace MyApp - {{ - [ToolkitSampleBoolOption(""test"", false, Title = ""Toggle x"")] - [ToolkitSampleBoolOption(""test"", false, Title = ""Toggle y"")] - [ToolkitSampleMultiChoiceOption(""TextFontFamily"", ""Segoe UI"", ""Arial"", Title = ""Text foreground"")] - - [ToolkitSample(id: nameof(Sample), ""Test Sample"", description: """")] - public partial class Sample : Windows.UI.Xaml.Controls.UserControl - {{ - }} - }} - - namespace Windows.UI.Xaml.Controls - {{ - public class UserControl {{ }} - }}"; - - VerifyGeneratedDiagnostics(source, string.Empty, DiagnosticDescriptors.SamplePaneOptionWithDuplicateName.Id, DiagnosticDescriptors.SampleNotReferencedInMarkdown.Id); - } - - [TestMethod] - public void PaneOptionWithDuplicateName_AllowedBetweenSamples() - { - var source = $@" - using System.ComponentModel; - using CommunityToolkit.Tooling.SampleGen; - using CommunityToolkit.Tooling.SampleGen.Attributes; - - namespace MyApp - {{ - [ToolkitSampleBoolOption(""test"", false, Title = ""Toggle y"")] - - [ToolkitSample(id: nameof(Sample), ""Test Sample"", description: """")] - public partial class Sample : Windows.UI.Xaml.Controls.UserControl - {{ - }} - - [ToolkitSampleBoolOption(""test"", false, Title = ""Toggle y"")] - - [ToolkitSample(id: nameof(Sample2), ""Test Sample"", description: """")] - public partial class Sample2 : Windows.UI.Xaml.Controls.UserControl - {{ - }} - }} - - namespace Windows.UI.Xaml.Controls - {{ - public class UserControl {{ }} - }}"; - - VerifyGeneratedDiagnostics(source, string.Empty, DiagnosticDescriptors.SampleNotReferencedInMarkdown.Id); - } - - [TestMethod] - public void PaneMultipleChoiceOptionWithNoChoices() - { - var source = $@" - using System.ComponentModel; - using CommunityToolkit.Tooling.SampleGen; - using CommunityToolkit.Tooling.SampleGen.Attributes; - - namespace MyApp - {{ - [ToolkitSampleMultiChoiceOption(""TextFontFamily"", Title = ""Font family"")] - - [ToolkitSample(id: nameof(Sample), ""Test Sample"", description: """")] - public partial class Sample : Windows.UI.Xaml.Controls.UserControl - {{ - }} - }} - - namespace Windows.UI.Xaml.Controls - {{ - public class UserControl {{ }} - }}"; - - VerifyGeneratedDiagnostics(source, string.Empty, DiagnosticDescriptors.SamplePaneMultiChoiceOptionWithNoChoices.Id, DiagnosticDescriptors.SampleNotReferencedInMarkdown.Id); - } - - [TestMethod] - public void SampleGeneratedOptionAttributeOnUnsupportedType() - { - var source = $@" - using System.ComponentModel; - using CommunityToolkit.Tooling.SampleGen; - using CommunityToolkit.Tooling.SampleGen.Attributes; - - namespace MyApp - {{ - [ToolkitSampleMultiChoiceOption(""TextFontFamily"", ""Segoe UI"", ""Arial"", ""Consolas"", Title = ""Font family"")] - [ToolkitSampleBoolOption(""Test"", false, Title = ""Toggle visibility"")] - public partial class Sample - {{ - }} - }}"; - - VerifyGeneratedDiagnostics(source, string.Empty, DiagnosticDescriptors.SampleGeneratedOptionAttributeOnUnsupportedType.Id, DiagnosticDescriptors.SamplePaneOptionAttributeOnNonSample.Id); - } + private const string SAMPLE_ASM_NAME = "MetadataTests.Samples"; [TestMethod] public void SampleAttributeOnUnsupportedType() @@ -368,7 +31,10 @@ public partial class Sample }} }}"; - VerifyGeneratedDiagnostics(source, string.Empty, DiagnosticDescriptors.SampleAttributeOnUnsupportedType.Id, DiagnosticDescriptors.SampleNotReferencedInMarkdown.Id); + var result = source.RunSourceGenerator(SAMPLE_ASM_NAME); + + result.AssertDiagnosticsAre(DiagnosticDescriptors.SampleAttributeOnUnsupportedType, DiagnosticDescriptors.SampleNotReferencedInMarkdown); + result.AssertNoCompilationErrors(); } [TestMethod] @@ -397,7 +63,10 @@ namespace Windows.UI.Xaml.Controls public class UserControl {{ }} }}"; - VerifyGeneratedDiagnostics(source, string.Empty, DiagnosticDescriptors.SampleOptionPaneAttributeOnUnsupportedType.Id, DiagnosticDescriptors.SampleNotReferencedInMarkdown.Id); + var result = source.RunSourceGenerator(SAMPLE_ASM_NAME); + + result.AssertDiagnosticsAre(DiagnosticDescriptors.SampleOptionPaneAttributeOnUnsupportedType, DiagnosticDescriptors.SampleNotReferencedInMarkdown); + result.AssertNoCompilationErrors(); } [TestMethod] @@ -422,136 +91,9 @@ namespace Windows.UI.Xaml.Controls public class UserControl {{ }} }}"; - // TODO: We should have this return the references to the registries or something so we can check the generated output? - VerifyGeneratedDiagnostics(source, string.Empty, DiagnosticDescriptors.SampleNotReferencedInMarkdown.Id); - } - - /// - /// Verifies the output of a source generator. - /// - /// The generator type to use. - /// The input source to process. - /// The input documentation info to process. - /// The diagnostic ids to expect for the input source code. - private static void VerifyGeneratedDiagnostics(string source, string markdown, params string[] diagnosticsIds) - where TGenerator : class, IIncrementalGenerator, new() - { - VerifyGeneratedDiagnostics(CSharpSyntaxTree.ParseText(source), markdown, diagnosticsIds); - } - - /// - /// Verifies the output of a source generator. - /// - /// The generator type to use. - /// The input source tree to process. - /// The input documentation info to process. - /// The diagnostic ids to expect for the input source code. - private static void VerifyGeneratedDiagnostics(SyntaxTree syntaxTree, string markdown, params string[] diagnosticsIds) - where TGenerator : class, IIncrementalGenerator, new() - { - var sampleAttributeType = typeof(ToolkitSampleAttribute); - - var references = - from assembly in AppDomain.CurrentDomain.GetAssemblies() - where !assembly.IsDynamic - let reference = MetadataReference.CreateFromFile(assembly.Location) - select reference; - - var compilation = CSharpCompilation.Create( - "original.Samples", - new[] { syntaxTree }, - references, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - - var compilationDiagnostics = compilation.GetDiagnostics(); - - IIncrementalGenerator generator = new TGenerator(); - - GeneratorDriver driver = - CSharpGeneratorDriver - .Create(generator) - .WithUpdatedParseOptions((CSharpParseOptions)syntaxTree.Options); - - if (!string.IsNullOrWhiteSpace(markdown)) - { - var text = new InMemoryAdditionalText(@"C:\pathtorepo\components\experiment\samples\experiment.Samples\documentation.md", markdown); - - driver = driver.AddAdditionalTexts(ImmutableArray.Create(text)); - } - - _ = driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation outputCompilation, out ImmutableArray diagnostics); - - HashSet resultingIds = diagnostics.Select(diagnostic => diagnostic.Id).ToHashSet(); - var generatedCompilationDiaghostics = outputCompilation.GetDiagnostics(); - - Assert.IsTrue(resultingIds.SetEquals(diagnosticsIds), $"Expected one of [{string.Join(", ", diagnosticsIds)}] diagnostic Ids. Got [{string.Join(", ", resultingIds)}]"); - Assert.IsTrue(generatedCompilationDiaghostics.All(x => x.Severity != DiagnosticSeverity.Error), $"Expected no generated compilation errors. Got: \n{string.Join("\n", generatedCompilationDiaghostics.Where(x => x.Severity == DiagnosticSeverity.Error).Select(x => $"[{x.Id}: {x.GetMessage()}]"))}"); - - GC.KeepAlive(sampleAttributeType); - } - - //// See: https://github.com/CommunityToolkit/dotnet/blob/c2053562d1a4d4829fc04b1cb86d1564c2c4a03c/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs#L103 - /// - /// Generates the requested sources - /// - /// The input source to process. - /// The generators to apply to the input syntax tree. - /// The source files to compare. - private static void VerifyGenerateSources(string assemblyName, string source, IIncrementalGenerator[] generators, bool ignoreDiagnostics = false, params (string Filename, string Text)[] results) - { - // Ensure our types are loaded - Type sampleattributeObjectType = typeof(ToolkitSampleAttribute); - - // Get all assembly references for the loaded assemblies (easy way to pull in all necessary dependencies) - IEnumerable references = - from assembly in AppDomain.CurrentDomain.GetAssemblies() - where !assembly.IsDynamic - let reference = MetadataReference.CreateFromFile(assembly.Location) - select reference; - - SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp10)); - - // Create a syntax tree with the input source - CSharpCompilation compilation = CSharpCompilation.Create( - assemblyName, - new SyntaxTree[] { syntaxTree }, - references, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - - GeneratorDriver driver = CSharpGeneratorDriver.Create(generators).WithUpdatedParseOptions((CSharpParseOptions)syntaxTree.Options); - - // Run all source generators on the input source code - _ = driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation outputCompilation, out ImmutableArray diagnostics); - - // Ensure that no diagnostics were generated - if (!ignoreDiagnostics) - { - CollectionAssert.AreEquivalent(Array.Empty(), diagnostics); - } - - foreach ((string filename, string text) in results) - { - SyntaxTree generatedTree = outputCompilation.SyntaxTrees.Single(tree => Path.GetFileName(tree.FilePath) == filename); - - Assert.AreEqual(text, generatedTree.ToString()); - } - - GC.KeepAlive(sampleattributeObjectType); - } - - // From: https://github.com/dotnet/roslyn/blob/main/src/Compilers/Test/Core/SourceGeneration/TestGenerators.cs - internal class InMemoryAdditionalText : AdditionalText - { - private readonly SourceText _content; - - public InMemoryAdditionalText(string path, string content) - { - Path = path; - _content = SourceText.From(content, Encoding.UTF8); - } - - public override string Path { get; } + var result = source.RunSourceGenerator(SAMPLE_ASM_NAME); - public override SourceText GetText(CancellationToken cancellationToken = default) => _content; + result.AssertDiagnosticsAre(DiagnosticDescriptors.SampleNotReferencedInMarkdown); + result.AssertNoCompilationErrors(); } } diff --git a/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleBoolOptionAttribute.cs b/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleBoolOptionAttribute.cs index 24577848..7b600b7d 100644 --- a/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleBoolOptionAttribute.cs +++ b/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleBoolOptionAttribute.cs @@ -23,7 +23,6 @@ public sealed class ToolkitSampleBoolOptionAttribute : ToolkitSampleOptionBaseAt public ToolkitSampleBoolOptionAttribute(string bindingName, bool defaultState) : base(bindingName, defaultState) { - } /// diff --git a/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleMultiChoiceOptionAttribute.cs b/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleMultiChoiceOptionAttribute.cs index 109b438d..406baa52 100644 --- a/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleMultiChoiceOptionAttribute.cs +++ b/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleMultiChoiceOptionAttribute.cs @@ -5,7 +5,7 @@ namespace CommunityToolkit.Tooling.SampleGen.Attributes; /// -/// Represents a boolean sample option. +/// Generates a property and multi-choice option in the sample option pane that can be used to update it. /// /// /// Using this attribute will automatically generate an -enabled property diff --git a/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleOptionBaseAttribute.cs b/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleOptionBaseAttribute.cs index 9df4dfe9..ac4dd33e 100644 --- a/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleOptionBaseAttribute.cs +++ b/CommunityToolkit.Tooling.SampleGen/Attributes/ToolkitSampleOptionBaseAttribute.cs @@ -24,7 +24,7 @@ public ToolkitSampleOptionBaseAttribute(string bindingName, object? defaultState /// /// A name that you can bind to in your XAML. /// - public string Name { get; } + public string Name { get; internal set; } /// /// The default state. diff --git a/CommunityToolkit.Tooling.SampleGen/CommunityToolkit.Tooling.SampleGen.csproj b/CommunityToolkit.Tooling.SampleGen/CommunityToolkit.Tooling.SampleGen.csproj index ae539b98..f1ab648c 100644 --- a/CommunityToolkit.Tooling.SampleGen/CommunityToolkit.Tooling.SampleGen.csproj +++ b/CommunityToolkit.Tooling.SampleGen/CommunityToolkit.Tooling.SampleGen.csproj @@ -4,7 +4,7 @@ netstandard2.0 enable nullable - 10.0 + 11.0 diff --git a/CommunityToolkit.Tooling.SampleGen/GeneratorExtensions.cs b/CommunityToolkit.Tooling.SampleGen/GeneratorExtensions.cs index 23bae29a..8f9aafaf 100644 --- a/CommunityToolkit.Tooling.SampleGen/GeneratorExtensions.cs +++ b/CommunityToolkit.Tooling.SampleGen/GeneratorExtensions.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; namespace CommunityToolkit.Tooling.SampleGen; @@ -12,18 +14,29 @@ public static class GeneratorExtensions /// Crawls a namespace and all child namespaces for all contained types. /// /// A flattened enumerable of s. - public static IEnumerable CrawlForAllNamedTypes(this INamespaceSymbol namespaceSymbol) + public static IEnumerable CrawlForAllSymbols(this INamespaceSymbol namespaceSymbol) { - foreach (var member in namespaceSymbol.GetMembers()) + // Get all classes and methods + foreach (var item in namespaceSymbol.GetTypeMembers()) + { + yield return item; + + foreach (var itemMember in item.GetMembers()) + { + if (itemMember.Kind == SymbolKind.Method) + yield return itemMember; + } + } + + foreach (var member in namespaceSymbol.GetNamespaceMembers()) { if (member is INamespaceSymbol nestedNamespace) { - foreach (var item in CrawlForAllNamedTypes(nestedNamespace)) + foreach (var item in CrawlForAllSymbols(nestedNamespace)) + { yield return item; + } } - - if (member is INamedTypeSymbol typeSymbol) - yield return typeSymbol; } } diff --git a/CommunityToolkit.Tooling.SampleGen/Metadata/ToolkitSampleButtonCommand.cs b/CommunityToolkit.Tooling.SampleGen/Metadata/ToolkitSampleButtonCommand.cs new file mode 100644 index 00000000..b27f7375 --- /dev/null +++ b/CommunityToolkit.Tooling.SampleGen/Metadata/ToolkitSampleButtonCommand.cs @@ -0,0 +1,34 @@ +// 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. + +using System.Windows.Input; + +namespace CommunityToolkit.Tooling.SampleGen.Metadata; + +/// +/// A command that invokes the provided when executed. +/// +public class ToolkitSampleButtonCommand : ICommand +{ + private readonly Action _callback; + + public ToolkitSampleButtonCommand(Action callback) + { + _callback = callback; + } + + /// + public event EventHandler? CanExecuteChanged; + + /// + public bool CanExecute(object parameter) + { + return true; + } + /// + public void Execute(object parameter) + { + _callback(); + } +} diff --git a/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Sample.cs b/CommunityToolkit.Tooling.SampleGen/ToolkitSampleMetadataGenerator.Sample.cs index d13cbbf5..0d37a907 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 Microsoft.CodeAnalysis.Text; namespace CommunityToolkit.Tooling.SampleGen; @@ -23,16 +22,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context) { var symbolsInExecutingAssembly = context.SyntaxProvider .CreateSyntaxProvider( - static (s, _) => s is ClassDeclarationSyntax c && c.AttributeLists.Count > 0, + static (s, _) => s is ClassDeclarationSyntax c && c.AttributeLists.Count > 0 || s is MethodDeclarationSyntax m && m.AttributeLists.Count > 0, static (ctx, _) => ctx.SemanticModel.GetDeclaredSymbol(ctx.Node)) .Where(static m => m is not null) .Select(static (x, _) => x!); var symbolsInReferencedAssemblies = context.CompilationProvider - .SelectMany((x, _) => x.SourceModule.ReferencedAssemblySymbols) - .SelectMany((asm, _) => asm.GlobalNamespace.CrawlForAllNamedTypes()) - .Where(x => x.TypeKind == TypeKind.Class && x.CanBeReferencedByName) - .Select((x, _) => (ISymbol)x); + .SelectMany((x, _) => x.SourceModule.ReferencedAssemblySymbols) + .SelectMany((asm, _) => asm.GlobalNamespace.CrawlForAllSymbols()); var markdownFiles = context.AdditionalTextsProvider .Where(static file => file.Path.EndsWith(".md")) @@ -56,11 +53,11 @@ void Execute(IncrementalValuesProvider types, bool skipDiagnostics = fa // Find and reconstruct generated pane option attributes + the original type symbol. var generatedPaneOptions = allAttributeData - .Select(static (x, _) => + .Select((x, _) => { (ISymbol Symbol, ToolkitSampleOptionBaseAttribute Attribute) item = default; - // Try and get base attribute of whatever sample attribute types we support. + // Reconstruct declared sample option attribute class instances from Roslyn symbols. if (x.Item2.TryReconstructAs() is ToolkitSampleBoolOptionAttribute boolOptionAttribute) { item = (x.Item1, boolOptionAttribute); @@ -78,15 +75,14 @@ void Execute(IncrementalValuesProvider types, bool skipDiagnostics = fa item = (x.Item1, textOptionAttribute); } - // Add extra property data, like Title back to Attribute - if (item.Attribute != null && x.Item2.TryGetNamedArgument(nameof(ToolkitSampleOptionBaseAttribute.Title), out string? title) && !string.IsNullOrWhiteSpace(title)) + // 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)) { item.Attribute.Title = title; } return item; }) - .Where(static x => x != default) .Collect(); // Find and reconstruct sample attributes @@ -116,12 +112,11 @@ void Execute(IncrementalValuesProvider types, bool skipDiagnostics = fa { var toolkitSampleAttributeData = data.Left.Left.Left.Right.Where(x => x != default).Distinct(); var optionsPaneAttribute = data.Left.Left.Left.Left.Where(x => x != default).Distinct(); - var generatedOptionPropertyData = data.Left.Left.Right.Where(x => x != default); + var generatedOptionPropertyData = data.Left.Left.Right.Where(x => x.Attribute is not null && x.Symbol is not null); var markdownFileData = data.Left.Right.Where(x => x != default).Distinct(); var currentAssembly = data.Right; var isExecutingInSampleProject = currentAssembly?.EndsWith(".Samples") ?? false; - var isExecutingInTestProject = currentAssembly?.EndsWith(".Tests") ?? false; // Reconstruct sample metadata from attributes var sampleMetadata = toolkitSampleAttributeData @@ -135,7 +130,7 @@ void Execute(IncrementalValuesProvider types, bool skipDiagnostics = fa sample.Attribute.Description, sample.AttachedQualifiedTypeName, optionsPaneAttribute.FirstOrDefault(x => x.Item1?.SampleId == sample.Attribute.Id).Item2?.ToString(), - generatedOptionPropertyData.Where(x => ReferenceEquals(x.Item1, sample.Symbol)).Select(x => x.Item2) + generatedOptionPropertyData.Where(x => x.Symbol.Equals(sample.Symbol, SymbolEqualityComparer.Default)).Select(x => x.Item2) ) ); @@ -147,9 +142,7 @@ void Execute(IncrementalValuesProvider types, bool skipDiagnostics = fa ReportDocumentDiagnostics(ctx, sampleMetadata, markdownFileData, toolkitSampleAttributeData, docFrontMatter); } - // For tests we need one pass to do diagnostics and registry as we're in a contrived environment that'll have both our scenarios. Though we check if we have anything to write, as we will hit both executes. - if ((!isExecutingInSampleProject && !skipRegistry) || - (isExecutingInTestProject && (docFrontMatter.Any() || sampleMetadata.Any()))) + if (!isExecutingInSampleProject && !skipRegistry) { CreateDocumentRegistry(ctx, docFrontMatter); CreateSampleRegistry(ctx, sampleMetadata); @@ -186,9 +179,9 @@ private static void ReportDiagnosticsForInvalidAttributeUsage(SourceProductionCo IEnumerable<(ToolkitSampleOptionsPaneAttribute?, ISymbol)> optionsPaneAttribute, IEnumerable<(ISymbol, ToolkitSampleOptionBaseAttribute)> generatedOptionPropertyData) { - var toolkitAttributesOnUnsupportedType = toolkitSampleAttributeData.Where(x => x.Symbol is not INamedTypeSymbol namedSym || !IsValidXamlControl(namedSym)); - var optionsAttributeOnUnsupportedType = optionsPaneAttribute.Where(x => x.Item2 is not INamedTypeSymbol namedSym || !IsValidXamlControl(namedSym)); - var generatedOptionAttributeOnUnsupportedType = generatedOptionPropertyData.Where(x => x.Item1 is not INamedTypeSymbol namedSym || !IsValidXamlControl(namedSym)); + var toolkitAttributesOnUnsupportedType = toolkitSampleAttributeData.Where(x => x.Symbol is INamedTypeSymbol namedSym && !IsValidXamlControl(namedSym)); + var optionsAttributeOnUnsupportedType = optionsPaneAttribute.Where(x => x.Item2 is INamedTypeSymbol namedSym && !IsValidXamlControl(namedSym)); + var generatedOptionAttributeOnUnsupportedType = generatedOptionPropertyData.Where(x => x.Item1 is INamedTypeSymbol namedSym && !IsValidXamlControl(namedSym)); foreach (var item in toolkitAttributesOnUnsupportedType) @@ -232,7 +225,7 @@ private static void ReportDiagnosticsGeneratedOptionsPane(SourceProductionContex ReportGeneratedMultiChoiceOptionsPaneDiagnostics(ctx, generatedOptionPropertyData); // Check for generated options which don't have a valid sample attribute - var generatedOptionsWithMissingSampleAttribute = generatedOptionPropertyData.Where(x => !toolkitSampleAttributeData.Any(sample => ReferenceEquals(sample.Symbol, x.Item1))); + var generatedOptionsWithMissingSampleAttribute = generatedOptionPropertyData.Where(x => x.Item1 is INamedTypeSymbol && !toolkitSampleAttributeData.Any(sample => ReferenceEquals(sample.Symbol, x.Item1))); foreach (var item in generatedOptionsWithMissingSampleAttribute) ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SamplePaneOptionAttributeOnNonSample, item.Item1.Locations.FirstOrDefault())); @@ -255,7 +248,7 @@ private static void ReportDiagnosticsGeneratedOptionsPane(SourceProductionContex ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SamplePaneOptionWithDuplicateName, item.SelectMany(x => x.Item1.Locations).FirstOrDefault(), item.Key)); // Check for generated options that conflict with an existing property name - var generatedOptionsWithConflictingPropertyNames = generatedOptionPropertyData.Where(x => GetAllMembers((INamedTypeSymbol)x.Item1).Any(y => x.Item2.Name == y.Name)); + var generatedOptionsWithConflictingPropertyNames = generatedOptionPropertyData.Where(x => x.Item1 is INamedTypeSymbol sym && GetAllMembers(sym).Any(y => x.Item2.Name == y.Name)); foreach (var item in generatedOptionsWithConflictingPropertyNames) ctx.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.SamplePaneOptionWithConflictingName, item.Item1.Locations.FirstOrDefault(), item.Item2.Name)); diff --git a/CommunityToolkit.Tooling.SampleGen/ToolkitSampleOptionGenerator.cs b/CommunityToolkit.Tooling.SampleGen/ToolkitSampleOptionGenerator.cs index 16fe9de7..d487eb25 100644 --- a/CommunityToolkit.Tooling.SampleGen/ToolkitSampleOptionGenerator.cs +++ b/CommunityToolkit.Tooling.SampleGen/ToolkitSampleOptionGenerator.cs @@ -23,7 +23,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) { var classes = context.SyntaxProvider .CreateSyntaxProvider( - static (s, _) => s is ClassDeclarationSyntax c && c.AttributeLists.Count > 0, + static (s, _) => s is ClassDeclarationSyntax c && c.AttributeLists.Count > 0 || s is MethodDeclarationSyntax m && m.AttributeLists.Count > 0, static (ctx, _) => ctx.SemanticModel.GetDeclaredSymbol(ctx.Node)) .Where(static m => m is not null) .Select(static (x, _) => x!); @@ -36,16 +36,16 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Select((x, _) => { if (x.Item2.TryReconstructAs() is ToolkitSampleBoolOptionAttribute boolOptionAttribute) - return (Attribute: (ToolkitSampleOptionBaseAttribute)boolOptionAttribute, ContainingClassSymbol: x.Item1, Type: typeof(ToolkitSampleBoolOptionMetadataViewModel)); + return (Attribute: (ToolkitSampleOptionBaseAttribute)boolOptionAttribute, AttachedSymbol: x.Item1, Type: typeof(ToolkitSampleBoolOptionMetadataViewModel)); if (x.Item2.TryReconstructAs() is ToolkitSampleMultiChoiceOptionAttribute multiChoiceOptionAttribute) - return (Attribute: (ToolkitSampleOptionBaseAttribute)multiChoiceOptionAttribute, ContainingClassSymbol: x.Item1, Type: typeof(ToolkitSampleMultiChoiceOptionMetadataViewModel)); + return (Attribute: (ToolkitSampleOptionBaseAttribute)multiChoiceOptionAttribute, AttachedSymbol: x.Item1, Type: typeof(ToolkitSampleMultiChoiceOptionMetadataViewModel)); - if(x.Item2.TryReconstructAs() is ToolkitSampleNumericOptionAttribute numericOptionAttribute) - return (Attribute: (ToolkitSampleOptionBaseAttribute)numericOptionAttribute, ContainingClassSymbol: x.Item1, Type: typeof(ToolkitSampleNumericOptionMetadataViewModel)); + 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, ContainingClassSymbol: x.Item1, Type: typeof(ToolkitSampleTextOptionMetadataViewModel)); + return (Attribute: (ToolkitSampleOptionBaseAttribute)textOptionAttribute, AttachedSymbol: x.Item1, Type: typeof(ToolkitSampleTextOptionMetadataViewModel)); return default; }) @@ -53,37 +53,62 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterSourceOutput(sampleAttributeOptions, (ctx, data) => { - if (_handledContainingClasses.Add(data.ContainingClassSymbol)) + var format = new SymbolDisplayFormat( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.OmittedAsContaining, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + propertyStyle: SymbolDisplayPropertyStyle.NameOnly, + memberOptions: SymbolDisplayMemberOptions.IncludeContainingType + ); + + var containingClass = data.AttachedSymbol.Kind switch { - if (data.ContainingClassSymbol is ITypeSymbol typeSym && !typeSym.AllInterfaces.Any(x => x.HasFullyQualifiedName("global::System.ComponentModel.INotifyPropertyChanged"))) + SymbolKind.Method => data.AttachedSymbol.ContainingSymbol, + SymbolKind.NamedType => data.AttachedSymbol, + _ => throw new NotSupportedException("Only methods and classes are supported here."), + }; + + var name = data.AttachedSymbol.Kind switch + { + SymbolKind.Method => $"{data.AttachedSymbol.ToDisplayString(format)}.CommandProperty.g", + SymbolKind.NamedType => $"{data.AttachedSymbol.ToDisplayString(format)}.Property.{data.Attribute.Name}.g", + _ => throw new NotSupportedException("Only methods and classes are supported here."), + }; + + // Generate property container and INPC + if (this._handledContainingClasses.Add(containingClass)) + { + if (containingClass is ITypeSymbol typeSym && !typeSym.AllInterfaces.Any(x => x.HasFullyQualifiedName("global::System.ComponentModel.INotifyPropertyChanged"))) { - var inpcImpl = BuildINotifyPropertyChangedImplementation(data.ContainingClassSymbol); - ctx.AddSource($"{data.ContainingClassSymbol}.NotifyPropertyChanged.g", inpcImpl); + var inpcImpl = BuildINotifyPropertyChangedImplementation(containingClass); + ctx.AddSource($"{containingClass}.NotifyPropertyChanged.g", inpcImpl); } - var propertyContainerSource = BuildGeneratedPropertyMetadataContainer(data.ContainingClassSymbol); - ctx.AddSource($"{data.ContainingClassSymbol}.GeneratedPropertyContainer.g", propertyContainerSource); + ctx.AddSource($"{containingClass.ToDisplayString(format)}.GeneratedPropertyContainer.g", BuildGeneratedPropertyMetadataContainer(containingClass)); } - var name = $"{data.ContainingClassSymbol}.Property.{data.Attribute.Name}.g"; - - if (_handledPropertyNames.Add(name)) + // Generate property + if (this._handledPropertyNames.Add(name)) { - var dependencyPropertySource = BuildProperty(data.ContainingClassSymbol, data.Attribute.Name, data.Attribute.TypeName, data.Type); + var dependencyPropertySource = data.AttachedSymbol.Kind switch + { + SymbolKind.NamedType => BuildProperty(containingClassSymbol: data.AttachedSymbol, data.Attribute.Name, data.Attribute.TypeName, data.Type), + _ => throw new NotSupportedException("Only methods and classes are supported here."), + }; + ctx.AddSource(name, dependencyPropertySource); } }); } - private static string BuildINotifyPropertyChangedImplementation(ISymbol containingClassSymbol) + private static string BuildINotifyPropertyChangedImplementation(ISymbol attachedSymbol) { return $@"#nullable enable using System.ComponentModel; -namespace {containingClassSymbol.ContainingNamespace} +namespace {attachedSymbol.ContainingNamespace} {{ - public partial class {containingClassSymbol.Name} : {nameof(System.ComponentModel.INotifyPropertyChanged)} + public partial class {attachedSymbol.Name} : {nameof(System.ComponentModel.INotifyPropertyChanged)} {{ public event PropertyChangedEventHandler? PropertyChanged; }} @@ -91,15 +116,15 @@ public partial class {containingClassSymbol.Name} : {nameof(System.ComponentMode "; } - private static string BuildGeneratedPropertyMetadataContainer(ISymbol containingClassSymbol) + private static string BuildGeneratedPropertyMetadataContainer(ISymbol attachedSymbol) { return $@"#nullable enable using System.ComponentModel; using System.Collections.Generic; -namespace {containingClassSymbol.ContainingNamespace} +namespace {attachedSymbol.ContainingNamespace} {{ - public partial class {containingClassSymbol.Name} : {typeof(IToolkitSampleGeneratedOptionPropertyContainer).Namespace}.{nameof(IToolkitSampleGeneratedOptionPropertyContainer)} + public partial class {attachedSymbol.Name} : {typeof(IToolkitSampleGeneratedOptionPropertyContainer).Namespace}.{nameof(IToolkitSampleGeneratedOptionPropertyContainer)} {{ private IEnumerable<{typeof(IGeneratedToolkitSampleOptionViewModel).FullName}>? _generatedPropertyMetadata;