// 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); }