Skip to content

Commit

Permalink
Merge pull request #433 from CommunityToolkit/dev/language-filter-ana…
Browse files Browse the repository at this point in the history
…lyzer

Move language diagnostics to diagnostic analyzers
  • Loading branch information
Sergio0694 authored Sep 10, 2022
2 parents 5be269e + 57832d1 commit 82ccd6c
Show file tree
Hide file tree
Showing 12 changed files with 286 additions and 142 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;

namespace CommunityToolkit.Mvvm.SourceGenerators;

Expand All @@ -30,8 +29,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
context.SyntaxProvider
.CreateSyntaxProvider(
static (node, _) => node is FieldDeclarationSyntax { Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 },
static (context, _) => ((FieldDeclarationSyntax)context.Node).Declaration.Variables.Select(v => (IFieldSymbol)context.SemanticModel.GetDeclaredSymbol(v)!))
.SelectMany(static (item, _) => item);
static (context, _) =>
{
if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8))
{
return default;
}
return ((FieldDeclarationSyntax)context.Node).Declaration.Variables.Select(v => (IFieldSymbol)context.SemanticModel.GetDeclaredSymbol(v)!);
})
.Where(static items => items is not null)
.SelectMany(static (item, _) => item!)!;

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

// Filter by language version
context.FilterWithLanguageVersion(ref fieldSymbolsWithAttribute, LanguageVersion.CSharp8, UnsupportedCSharpLanguageVersionError);

// Gather info for all annotated fields
IncrementalValuesProvider<(HierarchyInfo Hierarchy, Result<PropertyInfo?> Info)> propertyInfoWithErrors =
fieldSymbolsWithAttribute
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
context.SyntaxProvider
.CreateSyntaxProvider(
static (node, _) => node is ClassDeclarationSyntax,
static (context, _) => (context.Node, Symbol: (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!))
static (context, _) =>
{
if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8))
{
return default;
}
return (context.Node, Symbol: (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!);
})
.Where(static item => item.Symbol is { IsAbstract: false, IsGenericType: false } && item.Node.IsFirstSyntaxDeclarationForSymbol(item.Symbol))
.Select(static (item, _) => item.Symbol);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,16 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
context.SyntaxProvider
.CreateSyntaxProvider(
static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
static (context, _) => (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!);
static (context, _) =>
{
if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8))
{
return default;
}
return (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!;
})
.Where(static item => item is not null)!;

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

// Filter by language version
context.FilterWithLanguageVersion(ref typeSymbolsWithInfo, LanguageVersion.CSharp8, UnsupportedCSharpLanguageVersionError);

// Gather all generation info, and any diagnostics
IncrementalValuesProvider<Result<(HierarchyInfo Hierarchy, bool IsSealed, TInfo Info)>> generationInfoWithErrors =
typeSymbolsWithInfo.Select((item, _) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// 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.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;

namespace CommunityToolkit.Mvvm.SourceGenerators;

/// <summary>
/// A diagnostic analyzer that generates an error whenever a source-generator attribute is used with not high enough C# version enabled.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class UnsupportedCSharpLanguageVersionAnalyzer : DiagnosticAnalyzer
{
/// <summary>
/// The mapping of target attributes that will trigger the analyzer.
/// </summary>
private static readonly ImmutableDictionary<string, string> GeneratorAttributeNamesToFullyQualifiedNamesMap = ImmutableDictionary.CreateRange(new[]
{
new KeyValuePair<string, string>("INotifyPropertyChangedAttribute", "CommunityToolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute"),
new KeyValuePair<string, string>("NotifyCanExecuteChangedForAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyCanExecuteChangedForAttribute"),
new KeyValuePair<string, string>("NotifyDataErrorInfoAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyDataErrorInfoAttribute"),
new KeyValuePair<string, string>("NotifyPropertyChangedForAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedForAttribute"),
new KeyValuePair<string, string>("NotifyPropertyChangedRecipientsAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedRecipientsAttribute"),
new KeyValuePair<string, string>("ObservableObjectAttribute", "CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute"),
new KeyValuePair<string, string>("ObservablePropertyAttribute", "CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute"),
new KeyValuePair<string, string>("ObservableRecipientAttribute", "CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute"),
new KeyValuePair<string, string>("RelayCommandAttribute", "CommunityToolkit.Mvvm.Input.RelayCommandAttribute"),
});

/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(UnsupportedCSharpLanguageVersionError);

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();

// Defer the callback registration to when the compilation starts, so we can execute more
// preliminary checks and skip registering any kind of symbol analysis at all if not needed.
context.RegisterCompilationStartAction(static context =>
{
// Check that the language version is not high enough, otherwise no diagnostic should ever be produced
if (context.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8))
{
return;
}
context.RegisterSymbolAction(static context =>
{
// The possible attribute targets are only fields, classes and methods
if (context.Symbol is not (IFieldSymbol or INamedTypeSymbol { TypeKind: TypeKind.Class, IsImplicitlyDeclared: false } or IMethodSymbol))
{
return;
}
ImmutableArray<AttributeData> attributes = context.Symbol.GetAttributes();
// If the symbol has no attributes, there's nothing left to do
if (attributes.IsEmpty)
{
return;
}
foreach (AttributeData attribute in attributes)
{
// Go over each attribute on the target symbol, and check if the attribute type name is a candidate.
// If it is, double check by actually resolving the symbol from the compilation and comparing against it.
// This minimizes the calls to CompilationGetTypeByMetadataName(string) to only cases where it's almost
// guaranteed we'll actually get a match. If we do have one, then we can emit the diagnostic for the symbol.
if (attribute.AttributeClass is { Name: string attributeName } attributeClass &&
GeneratorAttributeNamesToFullyQualifiedNamesMap.TryGetValue(attributeName, out string? fullyQualifiedAttributeName) &&
context.Compilation.GetTypeByMetadataName(fullyQualifiedAttributeName) is INamedTypeSymbol attributeSymbol &&
SymbolEqualityComparer.Default.Equals(attributeClass, attributeSymbol))
{
context.ReportDiagnostic(Diagnostic.Create(UnsupportedCSharpLanguageVersionError, context.Symbol.Locations.FirstOrDefault()));
// If we created a diagnostic for this symbol, we can stop. Even if there's multiple attributes, no need for repeated errors
return;
}
}
}, SymbolKind.Field, SymbolKind.NamedType, SymbolKind.Method);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;

Expand All @@ -11,6 +12,17 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
/// </summary>
internal static class CompilationExtensions
{
/// <summary>
/// Checks whether a given compilation (assumed to be for C#) is using at least a given language version.
/// </summary>
/// <param name="compilation">The <see cref="Compilation"/> to consider for analysis.</param>
/// <param name="languageVersion">The minimum language version to check.</param>
/// <returns>Whether <paramref name="compilation"/> is using at least the specified language version.</returns>
public static bool HasLanguageVersionAtLeastEqualTo(this Compilation compilation, LanguageVersion languageVersion)
{
return ((CSharpCompilation)compilation).LanguageVersion >= languageVersion;
}

/// <summary>
/// <para>
/// Checks whether or not a type with a specified metadata name is accessible from a given <see cref="Compilation"/> instance.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;

Expand All @@ -14,54 +13,6 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
/// </summary>
internal static class IncrementalGeneratorInitializationContextExtensions
{
/// <summary>
/// Implements a gate for a language version over items in an input <see cref="IncrementalValuesProvider{TValues}"/> source.
/// </summary>
/// <typeparam name="T">The type of items in the input <see cref="IncrementalValuesProvider{TValues}"/> source.</typeparam>
/// <param name="context">The input <see cref="IncrementalGeneratorInitializationContext"/> value being used.</param>
/// <param name="source">The source <see cref="IncrementalValuesProvider{TValues}"/> instance.</param>
/// <param name="languageVersion">The minimum language version to gate for.</param>
/// <param name="diagnosticDescriptor">The <see cref="DiagnosticDescriptor"/> to emit if the gate detects invalid usage.</param>
/// <remarks>
/// Items in <paramref name="source"/> will be filtered out if the gate fails. If it passes, items will remain untouched.
/// </remarks>
public static void FilterWithLanguageVersion<T>(
this IncrementalGeneratorInitializationContext context,
ref IncrementalValuesProvider<T> source,
LanguageVersion languageVersion,
DiagnosticDescriptor diagnosticDescriptor)
{
// Check whether the target language version is supported
IncrementalValueProvider<bool> isGeneratorSupported =
context.ParseOptionsProvider
.Select((item, _) => item is CSharpParseOptions options && options.LanguageVersion >= languageVersion);

// Combine each data item with the supported flag
IncrementalValuesProvider<(T Data, bool IsGeneratorSupported)> dataWithSupportedInfo =
source
.Combine(isGeneratorSupported);

// Get a marker node to show whether an invalid attribute is used
IncrementalValueProvider<bool> isUnsupportedAttributeUsed =
dataWithSupportedInfo
.Select(static (item, _) => item.IsGeneratorSupported)
.Where(static item => !item)
.Collect()
.Select(static (item, _) => item.Length > 0);

// Report them to the output
context.RegisterConditionalSourceOutput(isUnsupportedAttributeUsed, context =>
{
context.ReportDiagnostic(Diagnostic.Create(diagnosticDescriptor, null));
});

// Only let data through if the minimum language version is supported
source =
dataWithSupportedInfo
.Where(static item => item.IsGeneratorSupported)
.Select(static (item, _) => item.Data);
}

/// <summary>
/// Conditionally invokes <see cref="IncrementalGeneratorInitializationContext.RegisterSourceOutput{TSource}(IncrementalValueProvider{TSource}, Action{SourceProductionContext, TSource})"/>
/// if the value produced by the input <see cref="IncrementalValueProvider{TValue}"/> is <see langword="true"/>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;

namespace CommunityToolkit.Mvvm.SourceGenerators;

Expand All @@ -29,7 +28,16 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
context.SyntaxProvider
.CreateSyntaxProvider(
static (node, _) => node is MethodDeclarationSyntax { Parent: ClassDeclarationSyntax, AttributeLists.Count: > 0 },
static (context, _) => (IMethodSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!);
static (context, _) =>
{
if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8))
{
return default;
}
return (IMethodSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!;
})
.Where(static item => item is not null)!;

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

// Filter by language version
context.FilterWithLanguageVersion(ref methodSymbolsWithAttributeData, LanguageVersion.CSharp8, UnsupportedCSharpLanguageVersionError);

// Gather info for all annotated command methods
IncrementalValuesProvider<(HierarchyInfo Hierarchy, Result<CommandInfo?> Info)> commandInfoWithErrors =
methodSymbolsWithAttributeData
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
context.SyntaxProvider
.CreateSyntaxProvider(
static (node, _) => node is ClassDeclarationSyntax,
static (context, _) => (context.Node, Symbol: (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!))
static (context, _) =>
{
if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8))
{
return default;
}
return (context.Node, Symbol: (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!);
})
.Where(static item => item.Symbol is { IsAbstract: false, IsGenericType: false } && item.Node.IsFirstSyntaxDeclarationForSymbol(item.Symbol))
.Select(static (item, _) => item.Symbol);

Expand Down
6 changes: 3 additions & 3 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ jobs:
displayName: Run .NET 6 unit tests

# Run the .NET 6 MVVM Toolkit tests targeting Roslyn 4.0.1
- 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"
- 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"
displayName: Run CommunityToolkit.Mvvm.UnitTests unit tests with Roslyn 4.0.1

# Run the .NET 6 MVVM Toolkit source generator tests targeting Roslyn 4.0.1
- 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"
displayName: Run CommunityToolkit.Mvvm.UnitTests unit tests with Roslyn 4.0.1
- 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"
displayName: Run CommunityToolkit.Mvvm.SourceGenerators.UnitTests unit tests with Roslyn 4.0.1

# Run .NET Core 3.1 tests
- script: dotnet test -c Release -f netcoreapp3.1 -l "trx;LogFileName=VSTestResults_netcoreapp3.1.trx"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.MSTest" Version="1.1.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
Expand Down
Loading

0 comments on commit 82ccd6c

Please sign in to comment.