Skip to content

Commit ddeddd8

Browse files
committed
Add initial version of incremental TransitiveMembersGenerator
1 parent 8bbc309 commit ddeddd8

File tree

2 files changed

+164
-1
lines changed

2 files changed

+164
-1
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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.IO;
8+
using System.Linq;
9+
using System.Reflection;
10+
using System.Text;
11+
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
12+
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
13+
using CommunityToolkit.Mvvm.SourceGenerators.Models;
14+
using Microsoft.CodeAnalysis;
15+
using Microsoft.CodeAnalysis.CSharp;
16+
using Microsoft.CodeAnalysis.CSharp.Syntax;
17+
using Microsoft.CodeAnalysis.Text;
18+
19+
namespace CommunityToolkit.Mvvm.SourceGenerators;
20+
21+
/// <summary>
22+
/// A source generator for a given attribute type.
23+
/// </summary>
24+
/// <typeparam name="TInfo">The type of info gathered from parsed <see cref="AttributeData"/> instances.</typeparam>
25+
public abstract partial class TransitiveMembersGenerator2<TInfo> : IIncrementalGenerator
26+
where TInfo : class
27+
{
28+
/// <summary>
29+
/// The fully qualified name of the attribute type to look for.
30+
/// </summary>
31+
private readonly string attributeType;
32+
33+
/// <summary>
34+
/// An <see cref="IEqualityComparer{T}"/> instance to compare intermediate models.
35+
/// </summary>
36+
/// <remarks>
37+
/// This is needed to cache extracted info on attributes used to annotate target types.
38+
/// </remarks>
39+
private readonly IEqualityComparer<TInfo> comparer;
40+
41+
/// <summary>
42+
/// The preloaded <see cref="ClassDeclarationSyntax"/> instance with members to generate.
43+
/// </summary>
44+
private readonly ClassDeclarationSyntax classDeclaration;
45+
46+
/// <summary>
47+
/// Initializes a new instance of the <see cref="TransitiveMembersGenerator"/> class.
48+
/// </summary>
49+
/// <param name="attributeType">The fully qualified name of the attribute type to look for.</param>
50+
/// <param name="comparer">An <see cref="IEqualityComparer{T}"/> instance to compare intermediate models.</param>
51+
protected TransitiveMembersGenerator2(string attributeType, IEqualityComparer<TInfo> comparer)
52+
{
53+
this.attributeType = attributeType;
54+
this.comparer = comparer;
55+
56+
string attributeTypeName = attributeType.Split('.').Last();
57+
string filename = $"CommunityToolkit.Mvvm.SourceGenerators.EmbeddedResources.{attributeTypeName.Replace("Attribute", string.Empty)}.cs";
58+
59+
using Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(filename);
60+
using StreamReader reader = new(stream);
61+
62+
string observableObjectSource = reader.ReadToEnd();
63+
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(observableObjectSource);
64+
65+
this.classDeclaration = syntaxTree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>().First();
66+
}
67+
68+
/// <inheritdoc/>
69+
public void Initialize(IncrementalGeneratorInitializationContext context)
70+
{
71+
// Validate the language version
72+
IncrementalValueProvider<bool> isGeneratorSupported =
73+
context.ParseOptionsProvider
74+
.Select(static (item, _) => item is CSharpParseOptions { LanguageVersion: >= LanguageVersion.CSharp9 });
75+
76+
// Get all class declarations
77+
IncrementalValuesProvider<INamedTypeSymbol> typeSymbols =
78+
context.SyntaxProvider
79+
.CreateSyntaxProvider(
80+
static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
81+
static (context, _) => (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!)
82+
.Combine(isGeneratorSupported)
83+
.Where(static item => item.Right)
84+
.Select(static (item, _) => item.Left);
85+
86+
// Filter the types with the target attribute
87+
IncrementalValuesProvider<(INamedTypeSymbol Symbol, TInfo Info)> typeSymbolsWithInfo =
88+
typeSymbols
89+
.Select((item, _) => (
90+
Symbol: item,
91+
Attribute: item.GetAttributes().FirstOrDefault(a => a.AttributeClass?.HasFullyQualifiedName(this.attributeType) == true)))
92+
.Where(static item => item.Attribute is not null)!
93+
.Select((item, _) => (item.Symbol, GetInfo(item.Attribute!)));
94+
95+
// Gather all generation info, and any diagnostics
96+
IncrementalValuesProvider<Result<(HierarchyInfo Hierarchy, TInfo Info)>> generationInfoWithErrors =
97+
typeSymbolsWithInfo.Select((item, _) =>
98+
{
99+
if (ValidateTargetType(item.Symbol, item.Info, out ImmutableArray<Diagnostic> diagnostics))
100+
{
101+
return new Result<(HierarchyInfo, TInfo)>(
102+
(HierarchyInfo.From(item.Symbol), item.Info),
103+
ImmutableArray<Diagnostic>.Empty);
104+
}
105+
106+
return new Result<(HierarchyInfo Hierarchy, TInfo Info)>(default, diagnostics);
107+
});
108+
109+
// Emit the diagnostic, if needed
110+
context.ReportDiagnostics(generationInfoWithErrors.Select(static (item, _) => item.Errors));
111+
112+
// Get the filtered sequence to enable caching
113+
IncrementalValuesProvider<(HierarchyInfo Hierarchy, TInfo Info)> generationInfo =
114+
generationInfoWithErrors
115+
.Where(static item => item.Errors.IsEmpty)
116+
.Select(static (item, _) => (item.Value.Hierarchy, item.Value.Info))
117+
.WithComparers(HierarchyInfo.Comparer.Default, this.comparer);
118+
119+
// Generate the required members
120+
context.RegisterSourceOutput(generationInfo, (context, item) =>
121+
{
122+
ImmutableArray<MemberDeclarationSyntax> memberDeclarations = FilterDeclaredMembers(item.Info, this.classDeclaration);
123+
CompilationUnitSyntax compilationUnit = item.Hierarchy.GetCompilationUnit(memberDeclarations, this.classDeclaration.BaseList);
124+
125+
context.AddSource(
126+
hintName: $"{item.Hierarchy.FilenameHint}.cs",
127+
sourceText: SourceText.From(compilationUnit.ToFullString(), Encoding.UTF8));
128+
});
129+
}
130+
131+
/// <summary>
132+
/// Gets an info model from a retrieved <see cref="AttributeData"/> instance.
133+
/// </summary>
134+
/// <param name="attributeData">The input <see cref="AttributeData"/> to get info from.</param>
135+
/// <returns>A <typeparamref name="TInfo"/> instance with data extracted from <paramref name="attributeData"/>.</returns>
136+
protected abstract TInfo GetInfo(AttributeData attributeData);
137+
138+
/// <summary>
139+
/// Validates a target type being processed.
140+
/// </summary>
141+
/// <param name="typeSymbol">The <see cref="INamedTypeSymbol"/> instance for the target type.</param>
142+
/// <param name="info">The <typeparamref name="TInfo"/> instance with the current processing info.</param>
143+
/// <param name="diagnostics">The resulting diagnostics from the processing operation.</param>
144+
/// <returns>Whether or not the target type is valid and can be processed normally.</returns>
145+
protected abstract bool ValidateTargetType(INamedTypeSymbol typeSymbol, TInfo info, out ImmutableArray<Diagnostic> diagnostics);
146+
147+
/// <summary>
148+
/// Filters the <see cref="MemberDeclarationSyntax"/> nodes to generate from the input parsed tree.
149+
/// </summary>
150+
/// <param name="info">The <typeparamref name="TInfo"/> instance with the current processing info.</param>
151+
/// <param name="classDeclaration">The input <see cref="ClassDeclarationSyntax"/> with the reference trees to copy.</param>
152+
/// <returns>A sequence of <see cref="MemberDeclarationSyntax"/> nodes to emit in the generated file.</returns>
153+
protected abstract ImmutableArray<MemberDeclarationSyntax> FilterDeclaredMembers(TInfo info, ClassDeclarationSyntax classDeclaration);
154+
}

CommunityToolkit.Mvvm.SourceGenerators/Models/HierarchyInfo.Syntax.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ partial record HierarchyInfo
2121
/// Creates a <see cref="CompilationUnitSyntax"/> instance wrapping the given members.
2222
/// </summary>
2323
/// <param name="memberDeclarations">The input <see cref="MemberDeclarationSyntax"/> instances to use.</param>
24+
/// <param name="baseList">The optional <see cref="BaseListSyntax"/> instance to add to generated types.</param>
2425
/// <returns>A <see cref="CompilationUnitSyntax"/> object wrapping <paramref name="memberDeclarations"/>.</returns>
25-
public CompilationUnitSyntax GetCompilationUnit(ImmutableArray<MemberDeclarationSyntax> memberDeclarations)
26+
public CompilationUnitSyntax GetCompilationUnit(
27+
ImmutableArray<MemberDeclarationSyntax> memberDeclarations,
28+
BaseListSyntax? baseList = null)
2629
{
2730
// Create the partial type declaration with the given member declarations.
2831
// This code produces a class declaration as follows:
@@ -36,6 +39,12 @@ public CompilationUnitSyntax GetCompilationUnit(ImmutableArray<MemberDeclaration
3639
.AddModifiers(Token(SyntaxKind.PartialKeyword))
3740
.AddMembers(memberDeclarations.ToArray());
3841

42+
// Add the base list, if present
43+
if (baseList is not null)
44+
{
45+
classDeclarationSyntax = classDeclarationSyntax.WithBaseList(baseList);
46+
}
47+
3948
TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax;
4049

4150
// Add all parent types in ascending order, if any

0 commit comments

Comments
 (0)