From 21e08252ef932228a373a4a2c8a9c5fc7046d16f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 8 Sep 2021 15:58:57 +0200 Subject: [PATCH 1/2] Added diagnostic for unsupported C# language version --- .../AnalyzerReleases.Unshipped.md | 1 + .../ObservablePropertyGenerator.cs | 9 +++++++++ ...ValidatorValidateAllPropertiesGenerator.cs | 7 +++++++ .../TransitiveMembersGenerator.cs | 7 +++++++ .../Diagnostics/DiagnosticDescriptors.cs | 19 ++++++++++++++++++- .../Input/ICommandGenerator.cs | 6 ++++++ .../IMessengerRegisterAllGenerator.cs | 7 +++++++ 7 files changed, 55 insertions(+), 1 deletion(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md b/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md index b27b97bcbc6..09be8eafd5f 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -17,3 +17,4 @@ MVVMTK0009 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator MVVMTK0010 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error MVVMTK0011 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error MVVMTK0012 | Microsoft.Toolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error +MVVMTK0013 | Microsoft.CodeAnalysis.CSharp.CSharpParseOptions | Error | See https://aka.ms/mvvmtoolkit/error diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index 037dc98fa78..af3d2e43222 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -41,6 +41,15 @@ public void Execute(GeneratorExecutionContext context) return; } + // Validate the language version. Note that we're emitting this diagnostic in each generator (excluding the one + // only emitting the nullability annotation attributes if missing) so that the diagnostic is emitted only when + // users are using one of these generators, and not by default as soon as they add a reference to the MVVM Toolkit. + // This ensures that users not using any of the source generators won't be broken when upgrading to this new version. + if (context.ParseOptions is not CSharpParseOptions { LanguageVersion: >= LanguageVersion.CSharp9 }) + { + context.ReportDiagnostic(Diagnostic.Create(UnsupportedCSharpLanguageVersionError, null)); + } + // Sets of discovered property names HashSet propertyChangedNames = new(), diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs index 45a6bb3d449..b1849b515bd 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs @@ -14,6 +14,7 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; namespace Microsoft.Toolkit.Mvvm.SourceGenerators { @@ -39,6 +40,12 @@ public void Execute(GeneratorExecutionContext context) return; } + // Validate the language version + if (context.ParseOptions is not CSharpParseOptions { LanguageVersion: >= LanguageVersion.CSharp9 }) + { + context.ReportDiagnostic(Diagnostic.Create(UnsupportedCSharpLanguageVersionError, null)); + } + // Get the symbol for the ValidationAttribute type INamedTypeSymbol validationSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute")!; diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs index bd3f48e284c..6c848347727 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs @@ -18,6 +18,7 @@ using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle; +using static Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; namespace Microsoft.Toolkit.Mvvm.SourceGenerators { @@ -67,6 +68,12 @@ public void Execute(GeneratorExecutionContext context) return; } + // Validate the language version + if (context.ParseOptions is not CSharpParseOptions { LanguageVersion: >= LanguageVersion.CSharp9 }) + { + context.ReportDiagnostic(Diagnostic.Create(UnsupportedCSharpLanguageVersionError, null)); + } + // Load the syntax tree with the members to generate SyntaxTree sourceSyntaxTree = LoadSourceSyntaxTree(); diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 81fa09ee7ee..52ab2c33079 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; namespace Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics { @@ -201,7 +202,23 @@ internal static class DiagnosticDescriptors category: typeof(ICommandGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: $"Cannot apply [ICommand] to methods with a signature that doesn't match any of the existing relay command types.", + description: "Cannot apply [ICommand] to methods with a signature that doesn't match any of the existing relay command types.", + helpLinkUri: "https://aka.ms/mvvmtoolkit"); + + /// + /// Gets a indicating when an unsupported C# language version is being used. + /// + /// Format: "The method {0}.{1} cannot be used to generate a command property, as its signature isn't compatible with any of the existing relay command types". + /// + /// + public static readonly DiagnosticDescriptor UnsupportedCSharpLanguageVersionError = new( + id: "MVVMTK0013", + title: "Unsupported C# language version", + messageFormat: "The source generator features from the MVVM Toolkit require consuming projects to set the C# language version to at least C# 9.0", + category: typeof(CSharpParseOptions).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "The source generator features from the MVVM Toolkit require consuming projects to set the C# language version to at least C# 9.0. Make sure to add 9.0 (or above) to your .csproj file.", helpLinkUri: "https://aka.ms/mvvmtoolkit"); } } diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs index ffcca24f30a..9095f93f36c 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs @@ -43,6 +43,12 @@ public void Execute(GeneratorExecutionContext context) return; } + // Validate the language version + if (context.ParseOptions is not CSharpParseOptions { LanguageVersion: >= LanguageVersion.CSharp9 }) + { + context.ReportDiagnostic(Diagnostic.Create(UnsupportedCSharpLanguageVersionError, null)); + } + foreach (var items in syntaxReceiver.GatheredInfo.GroupBy(static item => item.MethodSymbol.ContainingType, SymbolEqualityComparer.Default)) { if (items.Key.DeclaringSyntaxReferences.Length > 0 && diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs index 91e4925dd99..9faeecf3ac5 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs @@ -13,6 +13,7 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; namespace Microsoft.Toolkit.Mvvm.SourceGenerators { @@ -38,6 +39,12 @@ public void Execute(GeneratorExecutionContext context) return; } + // Validate the language version + if (context.ParseOptions is not CSharpParseOptions { LanguageVersion: >= LanguageVersion.CSharp9 }) + { + context.ReportDiagnostic(Diagnostic.Create(UnsupportedCSharpLanguageVersionError, null)); + } + // Get the symbol for the IRecipient interface type INamedTypeSymbol iRecipientSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Messaging.IRecipient`1")!; From 15a26e481f91e226dcf87d6be565ad5988bafbd7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 8 Sep 2021 17:50:24 +0200 Subject: [PATCH 2/2] Added unit tests for new diagnostic --- .../Test_SourceGeneratorsDiagnostics.cs | 156 +++++++++++++++++- 1 file changed, 151 insertions(+), 5 deletions(-) diff --git a/UnitTests/UnitTests.SourceGenerators/Test_SourceGeneratorsDiagnostics.cs b/UnitTests/UnitTests.SourceGenerators/Test_SourceGeneratorsDiagnostics.cs index 36bdb455eb7..7cb6bad19e0 100644 --- a/UnitTests/UnitTests.SourceGenerators/Test_SourceGeneratorsDiagnostics.cs +++ b/UnitTests/UnitTests.SourceGenerators/Test_SourceGeneratorsDiagnostics.cs @@ -241,31 +241,177 @@ public partial class SampleViewModel VerifyGeneratedDiagnostics(source, "MVVMTK0012"); } + [TestCategory("Mvvm")] + [TestMethod] + public void UnsupportedCSharpLanguageVersion_FromINotifyPropertyChangedGenerator() + { + string source = @" + using Microsoft.Toolkit.Mvvm.ComponentModel; + + namespace MyApp + { + [INotifyPropertyChanged] + public partial class SampleViewModel + { + } + }"; + + VerifyGeneratedDiagnostics( + CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp7_3)), + "MVVMTK0013"); + } + + [TestCategory("Mvvm")] + [TestMethod] + public void UnsupportedCSharpLanguageVersion_FromObservableObjectGenerator() + { + string source = @" + using Microsoft.Toolkit.Mvvm.ComponentModel; + + namespace MyApp + { + [ObservableObject] + public partial class SampleViewModel + { + } + }"; + + VerifyGeneratedDiagnostics( + CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp7_3)), + "MVVMTK0013"); + } + + [TestCategory("Mvvm")] + [TestMethod] + public void UnsupportedCSharpLanguageVersion_FromObservablePropertyGenerator() + { + string source = @" + using Microsoft.Toolkit.Mvvm.ComponentModel; + + namespace MyApp + { + [INotifyPropertyChanged] + public partial class SampleViewModel + { + [ObservableProperty] + private string name; + } + }"; + + VerifyGeneratedDiagnostics( + CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp7_3)), + "MVVMTK0013"); + } + + [TestCategory("Mvvm")] + [TestMethod] + public void UnsupportedCSharpLanguageVersion_FromObservableValidatorValidateAllPropertiesGenerator() + { + string source = @" + using Microsoft.Toolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableValidator + { + [Required] + public string Name { get; set; } + } + }"; + + VerifyGeneratedDiagnostics( + CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp7_3)), + "MVVMTK0013"); + } + + [TestCategory("Mvvm")] + [TestMethod] + public void UnsupportedCSharpLanguageVersion_FromICommandGenerator() + { + string source = @" + using Microsoft.Toolkit.Mvvm.Input; + + namespace MyApp + { + public partial class SampleViewModel + { + [ICommand] + private void GreetUser(object value) + { + } + } + }"; + + VerifyGeneratedDiagnostics( + CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp7_3)), + "MVVMTK0013"); + } + + [TestCategory("Mvvm")] + [TestMethod] + public void UnsupportedCSharpLanguageVersion_FromIMessengerRegisterAllGenerator() + { + string source = @" + using Microsoft.Toolkit.Mvvm.Messaging; + + namespace MyApp + { + public class MyMessage + { + } + + public partial class SampleViewModel : IRecipient + { + public void Receive(MyMessage message) + { + } + } + }"; + + VerifyGeneratedDiagnostics( + CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp7_3)), + "MVVMTK0013"); + } + /// /// Verifies the output of a source generator. /// /// The generator type to use. /// The input source to process. /// The diagnostic ids to expect for the input source code. - private void VerifyGeneratedDiagnostics(string source, params string[] diagnosticsIds) + private static void VerifyGeneratedDiagnostics(string source, params string[] diagnosticsIds) + where TGenerator : class, ISourceGenerator, new() + { + VerifyGeneratedDiagnostics(CSharpSyntaxTree.ParseText(source), diagnosticsIds); + } + + /// + /// Verifies the output of a source generator. + /// + /// The generator type to use. + /// The input source tree to process. + /// The diagnostic ids to expect for the input source code. + private static void VerifyGeneratedDiagnostics(SyntaxTree syntaxTree, params string[] diagnosticsIds) where TGenerator : class, ISourceGenerator, new() { Type observableObjectType = typeof(ObservableObject); Type validationAttributeType = typeof(ValidationAttribute); - SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source); - IEnumerable references = from assembly in AppDomain.CurrentDomain.GetAssemblies() where !assembly.IsDynamic let reference = MetadataReference.CreateFromFile(assembly.Location) select reference; - CSharpCompilation compilation = CSharpCompilation.Create("original", new SyntaxTree[] { syntaxTree }, references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + CSharpCompilation compilation = CSharpCompilation.Create( + "original", + new SyntaxTree[] { syntaxTree }, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); ISourceGenerator generator = new TGenerator(); - CSharpGeneratorDriver driver = CSharpGeneratorDriver.Create(generator); + CSharpGeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: (CSharpParseOptions)syntaxTree.Options); driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation outputCompilation, out ImmutableArray diagnostics);