Skip to content

Commit 2eacae7

Browse files
committed
Initial draft implementation of ICommandGenerator
1 parent 30d91ea commit 2eacae7

File tree

3 files changed

+218
-1
lines changed

3 files changed

+218
-1
lines changed

Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration(
324324
PropertyDeclaration(IdentifierName(typeName), Identifier(propertyName))
325325
.AddAttributeLists(
326326
AttributeList(SingletonSeparatedList(
327-
Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode"))
327+
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
328328
.AddArgumentListArguments(
329329
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).FullName))),
330330
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString())))))),
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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.Linq;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.CSharp;
9+
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
using Microsoft.Toolkit.Mvvm.Input;
11+
12+
namespace Microsoft.Toolkit.Mvvm.SourceGenerators
13+
{
14+
/// <inheritdoc cref="ICommandGenerator"/>
15+
public sealed partial class ICommandGenerator
16+
{
17+
/// <summary>
18+
/// An <see cref="ISyntaxContextReceiver"/> that selects candidate nodes to process.
19+
/// </summary>
20+
private sealed class SyntaxReceiver : ISyntaxContextReceiver
21+
{
22+
/// <summary>
23+
/// The list of info gathered during exploration.
24+
/// </summary>
25+
private readonly List<IMethodSymbol> gatheredInfo = new();
26+
27+
/// <summary>
28+
/// Gets the collection of gathered info to process.
29+
/// </summary>
30+
public IReadOnlyCollection<IMethodSymbol> GatheredInfo => this.gatheredInfo;
31+
32+
/// <inheritdoc/>
33+
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
34+
{
35+
if (context.Node is MethodDeclarationSyntax methodDeclaration &&
36+
context.SemanticModel.GetDeclaredSymbol(methodDeclaration) is IMethodSymbol methodSymbol &&
37+
context.SemanticModel.Compilation.GetTypeByMetadataName(typeof(ICommandAttribute).FullName) is INamedTypeSymbol iCommandSymbol &&
38+
methodSymbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, iCommandSymbol)))
39+
{
40+
this.gatheredInfo.Add(methodSymbol);
41+
}
42+
}
43+
}
44+
}
45+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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.Diagnostics.Contracts;
7+
using System.Linq;
8+
using System.Text;
9+
using Microsoft.CodeAnalysis;
10+
using Microsoft.CodeAnalysis.CSharp;
11+
using Microsoft.CodeAnalysis.CSharp.Syntax;
12+
using Microsoft.CodeAnalysis.Text;
13+
using Microsoft.Toolkit.Mvvm.Input;
14+
using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions;
15+
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
16+
using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle;
17+
18+
namespace Microsoft.Toolkit.Mvvm.SourceGenerators
19+
{
20+
/// <summary>
21+
/// A source generator for generating command properties from annotated methods.
22+
/// </summary>
23+
[Generator]
24+
public sealed partial class ICommandGenerator : ISourceGenerator
25+
{
26+
/// <inheritdoc/>
27+
public void Initialize(GeneratorInitializationContext context)
28+
{
29+
context.RegisterForSyntaxNotifications(static () => new SyntaxReceiver());
30+
}
31+
32+
/// <inheritdoc/>
33+
public void Execute(GeneratorExecutionContext context)
34+
{
35+
// Get the syntax receiver with the candidate nodes
36+
if (context.SyntaxContextReceiver is not SyntaxReceiver syntaxReceiver ||
37+
syntaxReceiver.GatheredInfo.Count == 0)
38+
{
39+
return;
40+
}
41+
42+
foreach (var items in syntaxReceiver.GatheredInfo.GroupBy<IMethodSymbol, INamedTypeSymbol>(static item => item.ContainingType, SymbolEqualityComparer.Default))
43+
{
44+
if (items.Key.DeclaringSyntaxReferences.Length > 0 &&
45+
items.Key.DeclaringSyntaxReferences.First().GetSyntax() is ClassDeclarationSyntax classDeclaration)
46+
{
47+
try
48+
{
49+
OnExecute(context, classDeclaration, items.Key, items);
50+
}
51+
catch
52+
{
53+
// TODO
54+
}
55+
}
56+
}
57+
}
58+
59+
/// <summary>
60+
/// Processes a given target type.
61+
/// </summary>
62+
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
63+
/// <param name="classDeclaration">The <see cref="ClassDeclarationSyntax"/> node to process.</param>
64+
/// <param name="classDeclarationSymbol">The <see cref="INamedTypeSymbol"/> for <paramref name="classDeclaration"/>.</param>
65+
/// <param name="methodSymbols">The sequence of <see cref="IMethodSymbol"/> instances to process.</param>
66+
private static void OnExecute(
67+
GeneratorExecutionContext context,
68+
ClassDeclarationSyntax classDeclaration,
69+
INamedTypeSymbol classDeclarationSymbol,
70+
IEnumerable<IMethodSymbol> methodSymbols)
71+
{
72+
// Create the class declaration for the user type. This will produce a tree as follows:
73+
//
74+
// <MODIFIERS> <CLASS_NAME>
75+
// {
76+
// <MEMBERS>
77+
// }
78+
var classDeclarationSyntax =
79+
ClassDeclaration(classDeclarationSymbol.Name)
80+
.WithModifiers(classDeclaration.Modifiers)
81+
.AddMembers(methodSymbols.Select(item => CreateCommandMembers(context, default, item)).SelectMany(static g => g).ToArray());
82+
83+
TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax;
84+
85+
// Add all parent types in ascending order, if any
86+
foreach (var parentType in classDeclaration.Ancestors().OfType<TypeDeclarationSyntax>())
87+
{
88+
typeDeclarationSyntax = parentType
89+
.WithMembers(SingletonList<MemberDeclarationSyntax>(typeDeclarationSyntax))
90+
.WithConstraintClauses(List<TypeParameterConstraintClauseSyntax>())
91+
.WithBaseList(null)
92+
.WithAttributeLists(List<AttributeListSyntax>())
93+
.WithoutTrivia();
94+
}
95+
96+
// Create the compilation unit with the namespace and target member.
97+
// From this, we can finally generate the source code to output.
98+
var namespaceName = classDeclarationSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces));
99+
100+
// Create the final compilation unit to generate (with leading trivia)
101+
var source =
102+
CompilationUnit().AddMembers(
103+
NamespaceDeclaration(IdentifierName(namespaceName)).WithLeadingTrivia(TriviaList(
104+
Comment("// Licensed to the .NET Foundation under one or more agreements."),
105+
Comment("// The .NET Foundation licenses this file to you under the MIT license."),
106+
Comment("// See the LICENSE file in the project root for more information."),
107+
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true))))
108+
.AddMembers(typeDeclarationSyntax))
109+
.NormalizeWhitespace()
110+
.ToFullString();
111+
112+
// Add the partial type
113+
context.AddSource($"[{typeof(ICommandAttribute).Name}]_[{classDeclarationSymbol.GetFullMetadataNameForFileName()}].cs", SourceText.From(source, Encoding.UTF8));
114+
}
115+
116+
/// <summary>
117+
/// Creates the <see cref="MemberDeclarationSyntax"/> instances for a specified command.
118+
/// </summary>
119+
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
120+
/// <param name="leadingTrivia">The leading trivia for the field to process.</param>
121+
/// <param name="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
122+
/// <returns>The <see cref="MemberDeclarationSyntax"/> instances for the input command.</returns>
123+
[Pure]
124+
private static IEnumerable<MemberDeclarationSyntax> CreateCommandMembers(GeneratorExecutionContext context, SyntaxTriviaList leadingTrivia, IMethodSymbol methodSymbol)
125+
{
126+
// Construct the generated field as follows:
127+
//
128+
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
129+
// private <COMMAND_TYPE>? <COMMAND_FIELD_NAME>;
130+
FieldDeclarationSyntax fieldDeclaration =
131+
FieldDeclaration(
132+
VariableDeclaration(NullableType(IdentifierName("IRelayCommand")))
133+
.AddVariables(VariableDeclarator(Identifier("fooCommand"))))
134+
.AddModifiers(Token(SyntaxKind.PrivateKeyword))
135+
.AddAttributeLists(AttributeList(SingletonSeparatedList(
136+
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
137+
.AddArgumentListArguments(
138+
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).FullName))),
139+
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).Assembly.GetName().Version.ToString())))))));
140+
141+
// Construct the generated property as follows:
142+
//
143+
// <METHOD_TRIVIA>
144+
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
145+
// [global::System.Diagnostics.DebuggerNonUserCode]
146+
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
147+
// public <COMMAND_TYPE> <COMMAND_PROPERTY_NAME> => <COMMAND_FIELD_NAME> ??= new <RELAY_COMMAND_TYPE>(new <DELEGATE_TYPE>(<METHOD_NAME>));
148+
PropertyDeclarationSyntax propertyDeclaration =
149+
PropertyDeclaration(IdentifierName("IRelayCommand"), Identifier("FooCommand"))
150+
.AddModifiers(Token(SyntaxKind.PublicKeyword))
151+
.AddAttributeLists(
152+
AttributeList(SingletonSeparatedList(
153+
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
154+
.AddArgumentListArguments(
155+
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).FullName))),
156+
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).Assembly.GetName().Version.ToString())))))),
157+
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))),
158+
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))))
159+
.WithExpressionBody(
160+
ArrowExpressionClause(
161+
AssignmentExpression(
162+
SyntaxKind.CoalesceAssignmentExpression,
163+
IdentifierName("fooCommand"),
164+
ObjectCreationExpression(IdentifierName("RelayCommand"))
165+
.AddArgumentListArguments(Argument(
166+
ObjectCreationExpression(IdentifierName("Action"))
167+
.AddArgumentListArguments(Argument(IdentifierName("Foo"))))))));
168+
169+
return new MemberDeclarationSyntax[] { fieldDeclaration, propertyDeclaration };
170+
}
171+
}
172+
}

0 commit comments

Comments
 (0)