// 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 System.Text;
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
using CommunityToolkit.Mvvm.SourceGenerators.Models;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
namespace CommunityToolkit.Mvvm.SourceGenerators;
///
/// A source generator for a given attribute type.
///
/// The type of info gathered for each target type to process.
public abstract partial class TransitiveMembersGenerator : IIncrementalGenerator
{
///
/// The fully qualified name of the attribute type to look for.
///
private readonly string attributeType;
///
/// An instance to compare intermediate models.
///
///
/// This is needed to cache extracted info on attributes used to annotate target types.
///
private readonly IEqualityComparer comparer;
///
/// The preloaded instance with members to generate.
///
private readonly ClassDeclarationSyntax classDeclaration;
///
/// The sequence of member declarations for sealed types.
///
private ImmutableArray sealedMemberDeclarations;
///
/// The resulting sequence of member declarations for non sealed types.
///
private ImmutableArray nonSealedMemberDeclarations;
///
/// Initializes a new instance of the class.
///
/// The fully qualified name of the attribute type to look for.
/// An instance to compare intermediate models.
private protected TransitiveMembersGenerator(string attributeType, IEqualityComparer? comparer = null)
{
this.attributeType = attributeType;
this.comparer = comparer ?? EqualityComparer.Default;
this.classDeclaration = Execute.LoadClassDeclaration(attributeType);
Execute.ProcessMemberDeclarations(
GetType(),
this.classDeclaration.Members.ToImmutableArray(),
out this.sealedMemberDeclarations,
out this.nonSealedMemberDeclarations);
}
///
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Get all class declarations
IncrementalValuesProvider typeSymbols =
context.SyntaxProvider
.CreateSyntaxProvider(
static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
static (context, _) => (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!);
// Filter the types with the target attribute
IncrementalValuesProvider<(INamedTypeSymbol Symbol, AttributeData AttributeData)> typeSymbolsWithAttributeData =
typeSymbols
.Select((item, _) => (
Symbol: item,
Attribute: item.GetAttributes().FirstOrDefault(a => a.AttributeClass?.HasFullyQualifiedName(this.attributeType) == true)))
.Where(static item => item.Attribute is not null)!;
// 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> generationInfoWithErrors =
typeSymbolsWithInfo.Select((item, _) =>
{
if (ValidateTargetType(item.Symbol, item.Info, out ImmutableArray diagnostics))
{
return new Result<(HierarchyInfo, bool, TInfo)>(
(HierarchyInfo.From(item.Symbol), item.Symbol.IsSealed, item.Info),
ImmutableArray.Empty);
}
return new Result<(HierarchyInfo, bool, TInfo)>(default, diagnostics);
});
// Emit the diagnostic, if needed
context.ReportDiagnostics(generationInfoWithErrors.Select(static (item, _) => item.Errors));
// Get the filtered sequence to enable caching
IncrementalValuesProvider<(HierarchyInfo Hierarchy, bool IsSealed, TInfo Info)> generationInfo =
generationInfoWithErrors
.Where(static item => item.Errors.IsEmpty)
.Select(static (item, _) => item.Value)
.WithComparers(HierarchyInfo.Comparer.Default, EqualityComparer.Default, this.comparer);
// Generate the required members
context.RegisterSourceOutput(generationInfo, (context, item) =>
{
ImmutableArray sourceMemberDeclarations = item.IsSealed ? this.sealedMemberDeclarations : this.nonSealedMemberDeclarations;
ImmutableArray filteredMemberDeclarations = FilterDeclaredMembers(item.Info, sourceMemberDeclarations);
CompilationUnitSyntax compilationUnit = item.Hierarchy.GetCompilationUnit(filteredMemberDeclarations, this.classDeclaration.BaseList);
context.AddSource($"{item.Hierarchy.FilenameHint}.g.cs", compilationUnit.GetText(Encoding.UTF8));
});
}
///
/// Gathers info from a source input.
///
/// The instance in use.
/// The source input.
/// A transformed instance with the gathered data.
protected abstract IncrementalValuesProvider<(INamedTypeSymbol Symbol, TInfo Info)> GetInfo(
IncrementalGeneratorInitializationContext context,
IncrementalValuesProvider<(INamedTypeSymbol Symbol, AttributeData AttributeData)> source);
///
/// Validates a target type being processed.
///
/// The instance for the target type.
/// The instance with the current processing info.
/// The resulting diagnostics from the processing operation.
/// Whether or not the target type is valid and can be processed normally.
protected abstract bool ValidateTargetType(INamedTypeSymbol typeSymbol, TInfo info, out ImmutableArray diagnostics);
///
/// Filters the nodes to generate from the input parsed tree.
///
/// The instance with the current processing info.
/// The input sequence of instances to generate.
/// A sequence of nodes to emit in the generated file.
protected abstract ImmutableArray FilterDeclaredMembers(TInfo info, ImmutableArray memberDeclarations);
}