Skip to content

Commit

Permalink
Merge pull request #4231 from Sergio0694/feature/csharp-version-diagn…
Browse files Browse the repository at this point in the history
…ostic

Add C# language version diagnostic
  • Loading branch information
michael-hawker authored Sep 8, 2021
2 parents b276329 + 15a26e4 commit 8d7f8c2
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>
propertyChangedNames = new(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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")!;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System.ComponentModel;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics
{
Expand Down Expand Up @@ -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");

/// <summary>
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when an unsupported C# language version is being used.
/// <para>
/// Format: <c>"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"</c>.
/// </para>
/// </summary>
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 <LangVersion>9.0</LangVersion> (or above) to your .csproj file.",
helpLinkUri: "https://aka.ms/mvvmtoolkit");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<SyntaxReceiver.Item, INamedTypeSymbol>(static item => item.MethodSymbol.ContainingType, SymbolEqualityComparer.Default))
{
if (items.Key.DeclaringSyntaxReferences.Length > 0 &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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<T> interface type
INamedTypeSymbol iRecipientSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Messaging.IRecipient`1")!;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,31 +241,177 @@ public partial class SampleViewModel
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0012");
}

[TestCategory("Mvvm")]
[TestMethod]
public void UnsupportedCSharpLanguageVersion_FromINotifyPropertyChangedGenerator()
{
string source = @"
using Microsoft.Toolkit.Mvvm.ComponentModel;
namespace MyApp
{
[INotifyPropertyChanged]
public partial class SampleViewModel
{
}
}";

VerifyGeneratedDiagnostics<INotifyPropertyChangedGenerator>(
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<ObservableObjectGenerator>(
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<ObservablePropertyGenerator>(
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<ObservableValidatorValidateAllPropertiesGenerator>(
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<ICommandGenerator>(
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<MyMessage>
{
public void Receive(MyMessage message)
{
}
}
}";

VerifyGeneratedDiagnostics<IMessengerRegisterAllGenerator>(
CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp7_3)),
"MVVMTK0013");
}

/// <summary>
/// Verifies the output of a source generator.
/// </summary>
/// <typeparam name="TGenerator">The generator type to use.</typeparam>
/// <param name="source">The input source to process.</param>
/// <param name="diagnosticsIds">The diagnostic ids to expect for the input source code.</param>
private void VerifyGeneratedDiagnostics<TGenerator>(string source, params string[] diagnosticsIds)
private static void VerifyGeneratedDiagnostics<TGenerator>(string source, params string[] diagnosticsIds)
where TGenerator : class, ISourceGenerator, new()
{
VerifyGeneratedDiagnostics<TGenerator>(CSharpSyntaxTree.ParseText(source), diagnosticsIds);
}

/// <summary>
/// Verifies the output of a source generator.
/// </summary>
/// <typeparam name="TGenerator">The generator type to use.</typeparam>
/// <param name="syntaxTree">The input source tree to process.</param>
/// <param name="diagnosticsIds">The diagnostic ids to expect for the input source code.</param>
private static void VerifyGeneratedDiagnostics<TGenerator>(SyntaxTree syntaxTree, params string[] diagnosticsIds)
where TGenerator : class, ISourceGenerator, new()
{
Type observableObjectType = typeof(ObservableObject);
Type validationAttributeType = typeof(ValidationAttribute);

SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source);

IEnumerable<MetadataReference> 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<Diagnostic> diagnostics);

Expand Down

0 comments on commit 8d7f8c2

Please sign in to comment.