Skip to content

Commit 8d7f8c2

Browse files
Merge pull request #4231 from Sergio0694/feature/csharp-version-diagnostic
Add C# language version diagnostic
2 parents b276329 + 15a26e4 commit 8d7f8c2

File tree

8 files changed

+206
-6
lines changed

8 files changed

+206
-6
lines changed

Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ MVVMTK0009 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator
1717
MVVMTK0010 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error
1818
MVVMTK0011 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error
1919
MVVMTK0012 | Microsoft.Toolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
20+
MVVMTK0013 | Microsoft.CodeAnalysis.CSharp.CSharpParseOptions | Error | See https://aka.ms/mvvmtoolkit/error

Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ public void Execute(GeneratorExecutionContext context)
4141
return;
4242
}
4343

44+
// Validate the language version. Note that we're emitting this diagnostic in each generator (excluding the one
45+
// only emitting the nullability annotation attributes if missing) so that the diagnostic is emitted only when
46+
// users are using one of these generators, and not by default as soon as they add a reference to the MVVM Toolkit.
47+
// This ensures that users not using any of the source generators won't be broken when upgrading to this new version.
48+
if (context.ParseOptions is not CSharpParseOptions { LanguageVersion: >= LanguageVersion.CSharp9 })
49+
{
50+
context.ReportDiagnostic(Diagnostic.Create(UnsupportedCSharpLanguageVersionError, null));
51+
}
52+
4453
// Sets of discovered property names
4554
HashSet<string>
4655
propertyChangedNames = new(),

Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using Microsoft.CodeAnalysis.Text;
1515
using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions;
1616
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
17+
using static Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
1718

1819
namespace Microsoft.Toolkit.Mvvm.SourceGenerators
1920
{
@@ -39,6 +40,12 @@ public void Execute(GeneratorExecutionContext context)
3940
return;
4041
}
4142

43+
// Validate the language version
44+
if (context.ParseOptions is not CSharpParseOptions { LanguageVersion: >= LanguageVersion.CSharp9 })
45+
{
46+
context.ReportDiagnostic(Diagnostic.Create(UnsupportedCSharpLanguageVersionError, null));
47+
}
48+
4249
// Get the symbol for the ValidationAttribute type
4350
INamedTypeSymbol validationSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute")!;
4451

Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions;
1919
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
2020
using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle;
21+
using static Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
2122

2223
namespace Microsoft.Toolkit.Mvvm.SourceGenerators
2324
{
@@ -67,6 +68,12 @@ public void Execute(GeneratorExecutionContext context)
6768
return;
6869
}
6970

71+
// Validate the language version
72+
if (context.ParseOptions is not CSharpParseOptions { LanguageVersion: >= LanguageVersion.CSharp9 })
73+
{
74+
context.ReportDiagnostic(Diagnostic.Create(UnsupportedCSharpLanguageVersionError, null));
75+
}
76+
7077
// Load the syntax tree with the members to generate
7178
SyntaxTree sourceSyntaxTree = LoadSourceSyntaxTree();
7279

Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System.ComponentModel;
66
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CSharp;
78

89
namespace Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics
910
{
@@ -201,7 +202,23 @@ internal static class DiagnosticDescriptors
201202
category: typeof(ICommandGenerator).FullName,
202203
defaultSeverity: DiagnosticSeverity.Error,
203204
isEnabledByDefault: true,
204-
description: $"Cannot apply [ICommand] to methods with a signature that doesn't match any of the existing relay command types.",
205+
description: "Cannot apply [ICommand] to methods with a signature that doesn't match any of the existing relay command types.",
206+
helpLinkUri: "https://aka.ms/mvvmtoolkit");
207+
208+
/// <summary>
209+
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when an unsupported C# language version is being used.
210+
/// <para>
211+
/// 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>.
212+
/// </para>
213+
/// </summary>
214+
public static readonly DiagnosticDescriptor UnsupportedCSharpLanguageVersionError = new(
215+
id: "MVVMTK0013",
216+
title: "Unsupported C# language version",
217+
messageFormat: "The source generator features from the MVVM Toolkit require consuming projects to set the C# language version to at least C# 9.0",
218+
category: typeof(CSharpParseOptions).FullName,
219+
defaultSeverity: DiagnosticSeverity.Error,
220+
isEnabledByDefault: true,
221+
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.",
205222
helpLinkUri: "https://aka.ms/mvvmtoolkit");
206223
}
207224
}

Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ public void Execute(GeneratorExecutionContext context)
4343
return;
4444
}
4545

46+
// Validate the language version
47+
if (context.ParseOptions is not CSharpParseOptions { LanguageVersion: >= LanguageVersion.CSharp9 })
48+
{
49+
context.ReportDiagnostic(Diagnostic.Create(UnsupportedCSharpLanguageVersionError, null));
50+
}
51+
4652
foreach (var items in syntaxReceiver.GatheredInfo.GroupBy<SyntaxReceiver.Item, INamedTypeSymbol>(static item => item.MethodSymbol.ContainingType, SymbolEqualityComparer.Default))
4753
{
4854
if (items.Key.DeclaringSyntaxReferences.Length > 0 &&

Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Microsoft.CodeAnalysis.Text;
1414
using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions;
1515
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
16+
using static Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
1617

1718
namespace Microsoft.Toolkit.Mvvm.SourceGenerators
1819
{
@@ -38,6 +39,12 @@ public void Execute(GeneratorExecutionContext context)
3839
return;
3940
}
4041

42+
// Validate the language version
43+
if (context.ParseOptions is not CSharpParseOptions { LanguageVersion: >= LanguageVersion.CSharp9 })
44+
{
45+
context.ReportDiagnostic(Diagnostic.Create(UnsupportedCSharpLanguageVersionError, null));
46+
}
47+
4148
// Get the symbol for the IRecipient<T> interface type
4249
INamedTypeSymbol iRecipientSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Messaging.IRecipient`1")!;
4350

UnitTests/UnitTests.SourceGenerators/Test_SourceGeneratorsDiagnostics.cs

Lines changed: 151 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -241,31 +241,177 @@ public partial class SampleViewModel
241241
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0012");
242242
}
243243

244+
[TestCategory("Mvvm")]
245+
[TestMethod]
246+
public void UnsupportedCSharpLanguageVersion_FromINotifyPropertyChangedGenerator()
247+
{
248+
string source = @"
249+
using Microsoft.Toolkit.Mvvm.ComponentModel;
250+
251+
namespace MyApp
252+
{
253+
[INotifyPropertyChanged]
254+
public partial class SampleViewModel
255+
{
256+
}
257+
}";
258+
259+
VerifyGeneratedDiagnostics<INotifyPropertyChangedGenerator>(
260+
CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp7_3)),
261+
"MVVMTK0013");
262+
}
263+
264+
[TestCategory("Mvvm")]
265+
[TestMethod]
266+
public void UnsupportedCSharpLanguageVersion_FromObservableObjectGenerator()
267+
{
268+
string source = @"
269+
using Microsoft.Toolkit.Mvvm.ComponentModel;
270+
271+
namespace MyApp
272+
{
273+
[ObservableObject]
274+
public partial class SampleViewModel
275+
{
276+
}
277+
}";
278+
279+
VerifyGeneratedDiagnostics<ObservableObjectGenerator>(
280+
CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp7_3)),
281+
"MVVMTK0013");
282+
}
283+
284+
[TestCategory("Mvvm")]
285+
[TestMethod]
286+
public void UnsupportedCSharpLanguageVersion_FromObservablePropertyGenerator()
287+
{
288+
string source = @"
289+
using Microsoft.Toolkit.Mvvm.ComponentModel;
290+
291+
namespace MyApp
292+
{
293+
[INotifyPropertyChanged]
294+
public partial class SampleViewModel
295+
{
296+
[ObservableProperty]
297+
private string name;
298+
}
299+
}";
300+
301+
VerifyGeneratedDiagnostics<ObservablePropertyGenerator>(
302+
CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp7_3)),
303+
"MVVMTK0013");
304+
}
305+
306+
[TestCategory("Mvvm")]
307+
[TestMethod]
308+
public void UnsupportedCSharpLanguageVersion_FromObservableValidatorValidateAllPropertiesGenerator()
309+
{
310+
string source = @"
311+
using Microsoft.Toolkit.Mvvm.ComponentModel;
312+
313+
namespace MyApp
314+
{
315+
public partial class SampleViewModel : ObservableValidator
316+
{
317+
[Required]
318+
public string Name { get; set; }
319+
}
320+
}";
321+
322+
VerifyGeneratedDiagnostics<ObservableValidatorValidateAllPropertiesGenerator>(
323+
CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp7_3)),
324+
"MVVMTK0013");
325+
}
326+
327+
[TestCategory("Mvvm")]
328+
[TestMethod]
329+
public void UnsupportedCSharpLanguageVersion_FromICommandGenerator()
330+
{
331+
string source = @"
332+
using Microsoft.Toolkit.Mvvm.Input;
333+
334+
namespace MyApp
335+
{
336+
public partial class SampleViewModel
337+
{
338+
[ICommand]
339+
private void GreetUser(object value)
340+
{
341+
}
342+
}
343+
}";
344+
345+
VerifyGeneratedDiagnostics<ICommandGenerator>(
346+
CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp7_3)),
347+
"MVVMTK0013");
348+
}
349+
350+
[TestCategory("Mvvm")]
351+
[TestMethod]
352+
public void UnsupportedCSharpLanguageVersion_FromIMessengerRegisterAllGenerator()
353+
{
354+
string source = @"
355+
using Microsoft.Toolkit.Mvvm.Messaging;
356+
357+
namespace MyApp
358+
{
359+
public class MyMessage
360+
{
361+
}
362+
363+
public partial class SampleViewModel : IRecipient<MyMessage>
364+
{
365+
public void Receive(MyMessage message)
366+
{
367+
}
368+
}
369+
}";
370+
371+
VerifyGeneratedDiagnostics<IMessengerRegisterAllGenerator>(
372+
CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp7_3)),
373+
"MVVMTK0013");
374+
}
375+
244376
/// <summary>
245377
/// Verifies the output of a source generator.
246378
/// </summary>
247379
/// <typeparam name="TGenerator">The generator type to use.</typeparam>
248380
/// <param name="source">The input source to process.</param>
249381
/// <param name="diagnosticsIds">The diagnostic ids to expect for the input source code.</param>
250-
private void VerifyGeneratedDiagnostics<TGenerator>(string source, params string[] diagnosticsIds)
382+
private static void VerifyGeneratedDiagnostics<TGenerator>(string source, params string[] diagnosticsIds)
383+
where TGenerator : class, ISourceGenerator, new()
384+
{
385+
VerifyGeneratedDiagnostics<TGenerator>(CSharpSyntaxTree.ParseText(source), diagnosticsIds);
386+
}
387+
388+
/// <summary>
389+
/// Verifies the output of a source generator.
390+
/// </summary>
391+
/// <typeparam name="TGenerator">The generator type to use.</typeparam>
392+
/// <param name="syntaxTree">The input source tree to process.</param>
393+
/// <param name="diagnosticsIds">The diagnostic ids to expect for the input source code.</param>
394+
private static void VerifyGeneratedDiagnostics<TGenerator>(SyntaxTree syntaxTree, params string[] diagnosticsIds)
251395
where TGenerator : class, ISourceGenerator, new()
252396
{
253397
Type observableObjectType = typeof(ObservableObject);
254398
Type validationAttributeType = typeof(ValidationAttribute);
255399

256-
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source);
257-
258400
IEnumerable<MetadataReference> references =
259401
from assembly in AppDomain.CurrentDomain.GetAssemblies()
260402
where !assembly.IsDynamic
261403
let reference = MetadataReference.CreateFromFile(assembly.Location)
262404
select reference;
263405

264-
CSharpCompilation compilation = CSharpCompilation.Create("original", new SyntaxTree[] { syntaxTree }, references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
406+
CSharpCompilation compilation = CSharpCompilation.Create(
407+
"original",
408+
new SyntaxTree[] { syntaxTree },
409+
references,
410+
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
265411

266412
ISourceGenerator generator = new TGenerator();
267413

268-
CSharpGeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
414+
CSharpGeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: (CSharpParseOptions)syntaxTree.Options);
269415

270416
driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation outputCompilation, out ImmutableArray<Diagnostic> diagnostics);
271417

0 commit comments

Comments
 (0)