Skip to content

Commit 82ccd6c

Browse files
authored
Merge pull request #433 from CommunityToolkit/dev/language-filter-analyzer
Move language diagnostics to diagnostic analyzers
2 parents 5be269e + 57832d1 commit 82ccd6c

12 files changed

+286
-142
lines changed

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
using Microsoft.CodeAnalysis;
1313
using Microsoft.CodeAnalysis.CSharp;
1414
using Microsoft.CodeAnalysis.CSharp.Syntax;
15-
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
1615

1716
namespace CommunityToolkit.Mvvm.SourceGenerators;
1817

@@ -30,8 +29,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
3029
context.SyntaxProvider
3130
.CreateSyntaxProvider(
3231
static (node, _) => node is FieldDeclarationSyntax { Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 },
33-
static (context, _) => ((FieldDeclarationSyntax)context.Node).Declaration.Variables.Select(v => (IFieldSymbol)context.SemanticModel.GetDeclaredSymbol(v)!))
34-
.SelectMany(static (item, _) => item);
32+
static (context, _) =>
33+
{
34+
if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8))
35+
{
36+
return default;
37+
}
38+
39+
return ((FieldDeclarationSyntax)context.Node).Declaration.Variables.Select(v => (IFieldSymbol)context.SemanticModel.GetDeclaredSymbol(v)!);
40+
})
41+
.Where(static items => items is not null)
42+
.SelectMany(static (item, _) => item!)!;
3543

3644
// Filter the fields using [ObservableProperty]
3745
IncrementalValuesProvider<IFieldSymbol> fieldSymbolsWithAttribute =
@@ -52,9 +60,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
5260
// Output the diagnostics
5361
context.ReportDiagnostics(fieldSymbolsWithOrphanedDependentAttributeWithErrors);
5462

55-
// Filter by language version
56-
context.FilterWithLanguageVersion(ref fieldSymbolsWithAttribute, LanguageVersion.CSharp8, UnsupportedCSharpLanguageVersionError);
57-
5863
// Gather info for all annotated fields
5964
IncrementalValuesProvider<(HierarchyInfo Hierarchy, Result<PropertyInfo?> Info)> propertyInfoWithErrors =
6065
fieldSymbolsWithAttribute

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
2828
context.SyntaxProvider
2929
.CreateSyntaxProvider(
3030
static (node, _) => node is ClassDeclarationSyntax,
31-
static (context, _) => (context.Node, Symbol: (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!))
31+
static (context, _) =>
32+
{
33+
if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8))
34+
{
35+
return default;
36+
}
37+
38+
return (context.Node, Symbol: (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!);
39+
})
3240
.Where(static item => item.Symbol is { IsAbstract: false, IsGenericType: false } && item.Node.IsFirstSyntaxDeclarationForSymbol(item.Symbol))
3341
.Select(static (item, _) => item.Symbol);
3442

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,16 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
7575
context.SyntaxProvider
7676
.CreateSyntaxProvider(
7777
static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
78-
static (context, _) => (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!);
78+
static (context, _) =>
79+
{
80+
if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8))
81+
{
82+
return default;
83+
}
84+
85+
return (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!;
86+
})
87+
.Where(static item => item is not null)!;
7988

8089
// Filter the types with the target attribute
8190
IncrementalValuesProvider<(INamedTypeSymbol Symbol, AttributeData AttributeData)> typeSymbolsWithAttributeData =
@@ -88,9 +97,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
8897
// Transform the input data
8998
IncrementalValuesProvider<(INamedTypeSymbol Symbol, TInfo Info)> typeSymbolsWithInfo = GetInfo(context, typeSymbolsWithAttributeData);
9099

91-
// Filter by language version
92-
context.FilterWithLanguageVersion(ref typeSymbolsWithInfo, LanguageVersion.CSharp8, UnsupportedCSharpLanguageVersionError);
93-
94100
// Gather all generation info, and any diagnostics
95101
IncrementalValuesProvider<Result<(HierarchyInfo Hierarchy, bool IsSealed, TInfo Info)>> generationInfoWithErrors =
96102
typeSymbolsWithInfo.Select((item, _) =>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Generic;
6+
using System.Collections.Immutable;
7+
using System.Linq;
8+
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
9+
using Microsoft.CodeAnalysis;
10+
using Microsoft.CodeAnalysis.CSharp;
11+
using Microsoft.CodeAnalysis.Diagnostics;
12+
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
13+
14+
namespace CommunityToolkit.Mvvm.SourceGenerators;
15+
16+
/// <summary>
17+
/// A diagnostic analyzer that generates an error whenever a source-generator attribute is used with not high enough C# version enabled.
18+
/// </summary>
19+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
20+
public sealed class UnsupportedCSharpLanguageVersionAnalyzer : DiagnosticAnalyzer
21+
{
22+
/// <summary>
23+
/// The mapping of target attributes that will trigger the analyzer.
24+
/// </summary>
25+
private static readonly ImmutableDictionary<string, string> GeneratorAttributeNamesToFullyQualifiedNamesMap = ImmutableDictionary.CreateRange(new[]
26+
{
27+
new KeyValuePair<string, string>("INotifyPropertyChangedAttribute", "CommunityToolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute"),
28+
new KeyValuePair<string, string>("NotifyCanExecuteChangedForAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyCanExecuteChangedForAttribute"),
29+
new KeyValuePair<string, string>("NotifyDataErrorInfoAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyDataErrorInfoAttribute"),
30+
new KeyValuePair<string, string>("NotifyPropertyChangedForAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedForAttribute"),
31+
new KeyValuePair<string, string>("NotifyPropertyChangedRecipientsAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedRecipientsAttribute"),
32+
new KeyValuePair<string, string>("ObservableObjectAttribute", "CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute"),
33+
new KeyValuePair<string, string>("ObservablePropertyAttribute", "CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute"),
34+
new KeyValuePair<string, string>("ObservableRecipientAttribute", "CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute"),
35+
new KeyValuePair<string, string>("RelayCommandAttribute", "CommunityToolkit.Mvvm.Input.RelayCommandAttribute"),
36+
});
37+
38+
/// <inheritdoc/>
39+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(UnsupportedCSharpLanguageVersionError);
40+
41+
/// <inheritdoc/>
42+
public override void Initialize(AnalysisContext context)
43+
{
44+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
45+
context.EnableConcurrentExecution();
46+
47+
// Defer the callback registration to when the compilation starts, so we can execute more
48+
// preliminary checks and skip registering any kind of symbol analysis at all if not needed.
49+
context.RegisterCompilationStartAction(static context =>
50+
{
51+
// Check that the language version is not high enough, otherwise no diagnostic should ever be produced
52+
if (context.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8))
53+
{
54+
return;
55+
}
56+
57+
context.RegisterSymbolAction(static context =>
58+
{
59+
// The possible attribute targets are only fields, classes and methods
60+
if (context.Symbol is not (IFieldSymbol or INamedTypeSymbol { TypeKind: TypeKind.Class, IsImplicitlyDeclared: false } or IMethodSymbol))
61+
{
62+
return;
63+
}
64+
65+
ImmutableArray<AttributeData> attributes = context.Symbol.GetAttributes();
66+
67+
// If the symbol has no attributes, there's nothing left to do
68+
if (attributes.IsEmpty)
69+
{
70+
return;
71+
}
72+
73+
foreach (AttributeData attribute in attributes)
74+
{
75+
// Go over each attribute on the target symbol, and check if the attribute type name is a candidate.
76+
// If it is, double check by actually resolving the symbol from the compilation and comparing against it.
77+
// This minimizes the calls to CompilationGetTypeByMetadataName(string) to only cases where it's almost
78+
// guaranteed we'll actually get a match. If we do have one, then we can emit the diagnostic for the symbol.
79+
if (attribute.AttributeClass is { Name: string attributeName } attributeClass &&
80+
GeneratorAttributeNamesToFullyQualifiedNamesMap.TryGetValue(attributeName, out string? fullyQualifiedAttributeName) &&
81+
context.Compilation.GetTypeByMetadataName(fullyQualifiedAttributeName) is INamedTypeSymbol attributeSymbol &&
82+
SymbolEqualityComparer.Default.Equals(attributeClass, attributeSymbol))
83+
{
84+
context.ReportDiagnostic(Diagnostic.Create(UnsupportedCSharpLanguageVersionError, context.Symbol.Locations.FirstOrDefault()));
85+
86+
// If we created a diagnostic for this symbol, we can stop. Even if there's multiple attributes, no need for repeated errors
87+
return;
88+
}
89+
}
90+
}, SymbolKind.Field, SymbolKind.NamedType, SymbolKind.Method);
91+
});
92+
}
93+
}

CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the LICENSE file in the project root for more information.
44

55
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
67

78
namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
89

@@ -11,6 +12,17 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
1112
/// </summary>
1213
internal static class CompilationExtensions
1314
{
15+
/// <summary>
16+
/// Checks whether a given compilation (assumed to be for C#) is using at least a given language version.
17+
/// </summary>
18+
/// <param name="compilation">The <see cref="Compilation"/> to consider for analysis.</param>
19+
/// <param name="languageVersion">The minimum language version to check.</param>
20+
/// <returns>Whether <paramref name="compilation"/> is using at least the specified language version.</returns>
21+
public static bool HasLanguageVersionAtLeastEqualTo(this Compilation compilation, LanguageVersion languageVersion)
22+
{
23+
return ((CSharpCompilation)compilation).LanguageVersion >= languageVersion;
24+
}
25+
1426
/// <summary>
1527
/// <para>
1628
/// Checks whether or not a type with a specified metadata name is accessible from a given <see cref="Compilation"/> instance.

CommunityToolkit.Mvvm.SourceGenerators/Extensions/IncrementalGeneratorInitializationContextExtensions.cs

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System;
66
using System.Collections.Immutable;
77
using Microsoft.CodeAnalysis;
8-
using Microsoft.CodeAnalysis.CSharp;
98

109
namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
1110

@@ -14,54 +13,6 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
1413
/// </summary>
1514
internal static class IncrementalGeneratorInitializationContextExtensions
1615
{
17-
/// <summary>
18-
/// Implements a gate for a language version over items in an input <see cref="IncrementalValuesProvider{TValues}"/> source.
19-
/// </summary>
20-
/// <typeparam name="T">The type of items in the input <see cref="IncrementalValuesProvider{TValues}"/> source.</typeparam>
21-
/// <param name="context">The input <see cref="IncrementalGeneratorInitializationContext"/> value being used.</param>
22-
/// <param name="source">The source <see cref="IncrementalValuesProvider{TValues}"/> instance.</param>
23-
/// <param name="languageVersion">The minimum language version to gate for.</param>
24-
/// <param name="diagnosticDescriptor">The <see cref="DiagnosticDescriptor"/> to emit if the gate detects invalid usage.</param>
25-
/// <remarks>
26-
/// Items in <paramref name="source"/> will be filtered out if the gate fails. If it passes, items will remain untouched.
27-
/// </remarks>
28-
public static void FilterWithLanguageVersion<T>(
29-
this IncrementalGeneratorInitializationContext context,
30-
ref IncrementalValuesProvider<T> source,
31-
LanguageVersion languageVersion,
32-
DiagnosticDescriptor diagnosticDescriptor)
33-
{
34-
// Check whether the target language version is supported
35-
IncrementalValueProvider<bool> isGeneratorSupported =
36-
context.ParseOptionsProvider
37-
.Select((item, _) => item is CSharpParseOptions options && options.LanguageVersion >= languageVersion);
38-
39-
// Combine each data item with the supported flag
40-
IncrementalValuesProvider<(T Data, bool IsGeneratorSupported)> dataWithSupportedInfo =
41-
source
42-
.Combine(isGeneratorSupported);
43-
44-
// Get a marker node to show whether an invalid attribute is used
45-
IncrementalValueProvider<bool> isUnsupportedAttributeUsed =
46-
dataWithSupportedInfo
47-
.Select(static (item, _) => item.IsGeneratorSupported)
48-
.Where(static item => !item)
49-
.Collect()
50-
.Select(static (item, _) => item.Length > 0);
51-
52-
// Report them to the output
53-
context.RegisterConditionalSourceOutput(isUnsupportedAttributeUsed, context =>
54-
{
55-
context.ReportDiagnostic(Diagnostic.Create(diagnosticDescriptor, null));
56-
});
57-
58-
// Only let data through if the minimum language version is supported
59-
source =
60-
dataWithSupportedInfo
61-
.Where(static item => item.IsGeneratorSupported)
62-
.Select(static (item, _) => item.Data);
63-
}
64-
6516
/// <summary>
6617
/// Conditionally invokes <see cref="IncrementalGeneratorInitializationContext.RegisterSourceOutput{TSource}(IncrementalValueProvider{TSource}, Action{SourceProductionContext, TSource})"/>
6718
/// if the value produced by the input <see cref="IncrementalValueProvider{TValue}"/> is <see langword="true"/>.

CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
using Microsoft.CodeAnalysis;
1212
using Microsoft.CodeAnalysis.CSharp;
1313
using Microsoft.CodeAnalysis.CSharp.Syntax;
14-
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
1514

1615
namespace CommunityToolkit.Mvvm.SourceGenerators;
1716

@@ -29,7 +28,16 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
2928
context.SyntaxProvider
3029
.CreateSyntaxProvider(
3130
static (node, _) => node is MethodDeclarationSyntax { Parent: ClassDeclarationSyntax, AttributeLists.Count: > 0 },
32-
static (context, _) => (IMethodSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!);
31+
static (context, _) =>
32+
{
33+
if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8))
34+
{
35+
return default;
36+
}
37+
38+
return (IMethodSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!;
39+
})
40+
.Where(static item => item is not null)!;
3341

3442
// Filter the methods using [RelayCommand]
3543
IncrementalValuesProvider<(IMethodSymbol Symbol, AttributeData Attribute)> methodSymbolsWithAttributeData =
@@ -39,9 +47,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
3947
Attribute: item.GetAttributes().FirstOrDefault(a => a.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.Input.RelayCommandAttribute") == true)))
4048
.Where(static item => item.Attribute is not null)!;
4149

42-
// Filter by language version
43-
context.FilterWithLanguageVersion(ref methodSymbolsWithAttributeData, LanguageVersion.CSharp8, UnsupportedCSharpLanguageVersionError);
44-
4550
// Gather info for all annotated command methods
4651
IncrementalValuesProvider<(HierarchyInfo Hierarchy, Result<CommandInfo?> Info)> commandInfoWithErrors =
4752
methodSymbolsWithAttributeData

CommunityToolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
3131
context.SyntaxProvider
3232
.CreateSyntaxProvider(
3333
static (node, _) => node is ClassDeclarationSyntax,
34-
static (context, _) => (context.Node, Symbol: (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!))
34+
static (context, _) =>
35+
{
36+
if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8))
37+
{
38+
return default;
39+
}
40+
41+
return (context.Node, Symbol: (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!);
42+
})
3543
.Where(static item => item.Symbol is { IsAbstract: false, IsGenericType: false } && item.Node.IsFirstSyntaxDeclarationForSymbol(item.Symbol))
3644
.Select(static (item, _) => item.Symbol);
3745

azure-pipelines.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,12 @@ jobs:
4747
displayName: Run .NET 6 unit tests
4848

4949
# Run the .NET 6 MVVM Toolkit tests targeting Roslyn 4.0.1
50-
- script: dotnet test tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.csproj -c Release -f net6.0 -l "trx;LogFileName=VSTestResults_net6.0_mvvmtoolkit_roslyn401.trx"
50+
- script: dotnet test tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.csproj -c Release -f net6.0 -p:MvvmToolkitSourceGeneratorRoslynVersion=4.0.1 -l "trx;LogFileName=VSTestResults_net6.0_mvvmtoolkit_roslyn401.trx"
5151
displayName: Run CommunityToolkit.Mvvm.UnitTests unit tests with Roslyn 4.0.1
5252

5353
# Run the .NET 6 MVVM Toolkit source generator tests targeting Roslyn 4.0.1
54-
- script: dotnet test tests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests.csproj -c Release -f net6.0 -l "trx;LogFileName=VSTestResults_net6.0_mvvmtoolkit_generators_roslyn401.trx"
55-
displayName: Run CommunityToolkit.Mvvm.UnitTests unit tests with Roslyn 4.0.1
54+
- script: dotnet test tests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests.csproj -c Release -f net6.0 -p:MvvmToolkitSourceGeneratorRoslynVersion=4.0.1 -l "trx;LogFileName=VSTestResults_net6.0_mvvmtoolkit_generators_roslyn401.trx"
55+
displayName: Run CommunityToolkit.Mvvm.SourceGenerators.UnitTests unit tests with Roslyn 4.0.1
5656

5757
# Run .NET Core 3.1 tests
5858
- script: dotnet test -c Release -f netcoreapp3.1 -l "trx;LogFileName=VSTestResults_netcoreapp3.1.trx"

tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
</PropertyGroup>
66

77
<ItemGroup>
8+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.MSTest" Version="1.1.1" />
89
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.0.1" />
910
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
1011
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />

0 commit comments

Comments
 (0)