Skip to content

Commit fbb227c

Browse files
committed
WIP Use implicit field with fieldkeyword when possible for RelayCommand source gen
1 parent ea17501 commit fbb227c

File tree

3 files changed

+85
-46
lines changed

3 files changed

+85
-46
lines changed

src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public override void Initialize(AnalysisContext context)
6060
return;
6161
}
6262

63-
(_, string propertyName) = RelayCommandGenerator.Execute.GetGeneratedFieldAndPropertyNames(methodSymbol);
63+
(_, string propertyName) = RelayCommandGenerator.Execute.GetGeneratedFieldAndPropertyNames(methodSymbol, context.Compilation);
6464

6565
if (DoesGeneratedBindableCustomPropertyAttributeIncludePropertyName(generatedBindableCustomPropertyAttribute, propertyName))
6666
{

src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
1111
/// A model with gathered info on a given command method.
1212
/// </summary>
1313
/// <param name="MethodName">The name of the target method.</param>
14-
/// <param name="FieldName">The resulting field name for the generated command.</param>
14+
/// <param name="FieldName">The resulting field name for the generated command, or null if the <see langword="field"/> is available.</param>
1515
/// <param name="PropertyName">The resulting property name for the generated command.</param>
1616
/// <param name="CommandInterfaceType">The command interface type name.</param>
1717
/// <param name="CommandClassType">The command class type name.</param>
@@ -26,7 +26,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
2626
/// <param name="ForwardedAttributes">The sequence of forwarded attributes for the generated members.</param>
2727
internal sealed record CommandInfo(
2828
string MethodName,
29-
string FieldName,
29+
string? FieldName,
3030
string PropertyName,
3131
string CommandInterfaceType,
3232
string CommandClassType,

src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs

Lines changed: 82 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public static bool TryGetInfo(
5959
token.ThrowIfCancellationRequested();
6060

6161
// Get the command field and property names
62-
(string fieldName, string propertyName) = GetGeneratedFieldAndPropertyNames(methodSymbol);
62+
(string? fieldName, string propertyName) = GetGeneratedFieldAndPropertyNames(methodSymbol, semanticModel.Compilation);
6363

6464
token.ThrowIfCancellationRequested();
6565

@@ -207,25 +207,30 @@ public static ImmutableArray<MemberDeclarationSyntax> GetSyntax(CommandInfo comm
207207
.Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax())))
208208
.ToArray();
209209

210-
// Construct the generated field as follows:
211-
//
212-
// /// <summary>The backing field for <see cref="<COMMAND_PROPERTY_NAME>"/></summary>
213-
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
214-
// <FORWARDED_ATTRIBUTES>
215-
// private <COMMAND_TYPE>? <COMMAND_FIELD_NAME>;
216-
FieldDeclarationSyntax fieldDeclaration =
217-
FieldDeclaration(
218-
VariableDeclaration(NullableType(IdentifierName(commandClassTypeName)))
219-
.AddVariables(VariableDeclarator(Identifier(commandInfo.FieldName))))
220-
.AddModifiers(Token(SyntaxKind.PrivateKeyword))
221-
.AddAttributeLists(
222-
AttributeList(SingletonSeparatedList(
223-
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
224-
.AddArgumentListArguments(
225-
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).FullName))),
226-
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).Assembly.GetName().Version.ToString()))))))
227-
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <summary>The backing field for <see cref=\"{commandInfo.PropertyName}\"/>.</summary>")), SyntaxKind.OpenBracketToken, TriviaList())))
228-
.AddAttributeLists(forwardedFieldAttributes);
210+
// Declare a backing field if needed
211+
FieldDeclarationSyntax? fieldDeclaration = null;
212+
if (commandInfo.FieldName is not null)
213+
{
214+
// Construct the generated field as follows:
215+
//
216+
// /// <summary>The backing field for <see cref="<COMMAND_PROPERTY_NAME>"/></summary>
217+
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
218+
// <FORWARDED_ATTRIBUTES>
219+
// private <COMMAND_TYPE>? <COMMAND_FIELD_NAME>;
220+
fieldDeclaration =
221+
FieldDeclaration(
222+
VariableDeclaration(NullableType(IdentifierName(commandClassTypeName)))
223+
.AddVariables(VariableDeclarator(Identifier(commandInfo.FieldName))))
224+
.AddModifiers(Token(SyntaxKind.PrivateKeyword))
225+
.AddAttributeLists(
226+
AttributeList(SingletonSeparatedList(
227+
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
228+
.AddArgumentListArguments(
229+
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).FullName))),
230+
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).Assembly.GetName().Version.ToString()))))))
231+
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <summary>The backing field for <see cref=\"{commandInfo.PropertyName}\"/>.</summary>")), SyntaxKind.OpenBracketToken, TriviaList())))
232+
.AddAttributeLists(forwardedFieldAttributes);
233+
}
229234

230235
// Prepares the argument to pass the underlying method to invoke
231236
using ImmutableArrayBuilder<ArgumentSyntax> commandCreationArguments = ImmutableArrayBuilder<ArgumentSyntax>.Rent();
@@ -337,7 +342,7 @@ public static ImmutableArray<MemberDeclarationSyntax> GetSyntax(CommandInfo comm
337342
ArrowExpressionClause(
338343
AssignmentExpression(
339344
SyntaxKind.CoalesceAssignmentExpression,
340-
IdentifierName(commandInfo.FieldName),
345+
commandInfo.FieldName is not null ? IdentifierName(commandInfo.FieldName) : IdentifierName(Token(SyntaxKind.FieldKeyword)),
341346
ObjectCreationExpression(IdentifierName(commandClassTypeName))
342347
.AddArgumentListArguments(commandCreationArguments.ToArray()))))
343348
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
@@ -346,26 +351,31 @@ public static ImmutableArray<MemberDeclarationSyntax> GetSyntax(CommandInfo comm
346351
if (commandInfo.IncludeCancelCommand)
347352
{
348353
// Prepare all necessary member and type names
349-
string cancelCommandFieldName = $"{commandInfo.FieldName.Substring(0, commandInfo.FieldName.Length - "Command".Length)}CancelCommand";
354+
string? cancelCommandFieldName = commandInfo.FieldName is not null ? $"{commandInfo.FieldName.Substring(0, commandInfo.FieldName.Length - "Command".Length)}CancelCommand" : null;
350355
string cancelCommandPropertyName = $"{commandInfo.PropertyName.Substring(0, commandInfo.PropertyName.Length - "Command".Length)}CancelCommand";
351356

352-
// Construct the generated field for the cancel command as follows:
353-
//
354-
// /// <summary>The backing field for <see cref="<COMMAND_PROPERTY_NAME>"/></summary>
355-
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
356-
// private global::System.Windows.Input.ICommand? <CANCEL_COMMAND_FIELD_NAME>;
357-
FieldDeclarationSyntax cancelCommandFieldDeclaration =
358-
FieldDeclaration(
359-
VariableDeclaration(NullableType(IdentifierName("global::System.Windows.Input.ICommand")))
360-
.AddVariables(VariableDeclarator(Identifier(cancelCommandFieldName))))
361-
.AddModifiers(Token(SyntaxKind.PrivateKeyword))
362-
.AddAttributeLists(
363-
AttributeList(SingletonSeparatedList(
364-
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
365-
.AddArgumentListArguments(
366-
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).FullName))),
367-
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).Assembly.GetName().Version.ToString()))))))
368-
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <summary>The backing field for <see cref=\"{cancelCommandPropertyName}\"/>.</summary>")), SyntaxKind.OpenBracketToken, TriviaList())));
357+
FieldDeclarationSyntax? cancelCommandFieldDeclaration = null;
358+
if (cancelCommandFieldName is not null)
359+
{
360+
// Construct the generated field for the cancel command as follows:
361+
//
362+
// /// <summary>The backing field for <see cref="<COMMAND_PROPERTY_NAME>"/></summary>
363+
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
364+
// private global::System.Windows.Input.ICommand? <CANCEL_COMMAND_FIELD_NAME>;
365+
cancelCommandFieldDeclaration =
366+
FieldDeclaration(
367+
VariableDeclaration(NullableType(IdentifierName("global::System.Windows.Input.ICommand")))
368+
.AddVariables(VariableDeclarator(Identifier(cancelCommandFieldName))))
369+
.AddModifiers(Token(SyntaxKind.PrivateKeyword))
370+
.AddAttributeLists(
371+
AttributeList(SingletonSeparatedList(
372+
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
373+
.AddArgumentListArguments(
374+
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).FullName))),
375+
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).Assembly.GetName().Version.ToString()))))))
376+
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <summary>The backing field for <see cref=\"{cancelCommandPropertyName}\"/>.</summary>")), SyntaxKind.OpenBracketToken, TriviaList())));
377+
378+
}
369379

370380
// Construct the generated property as follows (the explicit delegate cast is needed to avoid overload resolution conflicts):
371381
//
@@ -393,7 +403,7 @@ public static ImmutableArray<MemberDeclarationSyntax> GetSyntax(CommandInfo comm
393403
ArrowExpressionClause(
394404
AssignmentExpression(
395405
SyntaxKind.CoalesceAssignmentExpression,
396-
IdentifierName(cancelCommandFieldName),
406+
cancelCommandFieldName is not null ? IdentifierName(cancelCommandFieldName) : IdentifierName(Token(SyntaxKind.FieldKeyword)),
397407
InvocationExpression(
398408
MemberAccessExpression(
399409
SyntaxKind.SimpleMemberAccessExpression,
@@ -402,10 +412,20 @@ public static ImmutableArray<MemberDeclarationSyntax> GetSyntax(CommandInfo comm
402412
.AddArgumentListArguments(Argument(IdentifierName(commandInfo.PropertyName))))))
403413
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
404414

405-
return ImmutableArray.Create<MemberDeclarationSyntax>(fieldDeclaration, propertyDeclaration, cancelCommandFieldDeclaration, cancelCommandPropertyDeclaration);
415+
if (fieldDeclaration is not null && cancelCommandFieldDeclaration is not null)
416+
{
417+
return ImmutableArray.Create<MemberDeclarationSyntax>(fieldDeclaration, propertyDeclaration, cancelCommandFieldDeclaration, cancelCommandPropertyDeclaration);
418+
}
419+
420+
return ImmutableArray.Create<MemberDeclarationSyntax>(propertyDeclaration, cancelCommandPropertyDeclaration);
421+
}
422+
423+
if (fieldDeclaration is not null)
424+
{
425+
return ImmutableArray.Create<MemberDeclarationSyntax>(fieldDeclaration, propertyDeclaration);
406426
}
407427

408-
return ImmutableArray.Create<MemberDeclarationSyntax>(fieldDeclaration, propertyDeclaration);
428+
return ImmutableArray.Create<MemberDeclarationSyntax>(propertyDeclaration);
409429
}
410430

411431
/// <summary>
@@ -474,8 +494,9 @@ private static bool IsCommandDefinitionUnique(IMethodSymbol methodSymbol, in Imm
474494
/// Get the generated field and property names for the input method.
475495
/// </summary>
476496
/// <param name="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
497+
/// <param name="compilation">The compilation info, used to determine language version.</param>
477498
/// <returns>The generated field and property names for <paramref name="methodSymbol"/>.</returns>
478-
public static (string FieldName, string PropertyName) GetGeneratedFieldAndPropertyNames(IMethodSymbol methodSymbol)
499+
public static (string? FieldName, string PropertyName) GetGeneratedFieldAndPropertyNames(IMethodSymbol methodSymbol, Compilation? compilation = null)
479500
{
480501
string propertyName = methodSymbol.Name;
481502

@@ -498,6 +519,24 @@ public static (string FieldName, string PropertyName) GetGeneratedFieldAndProper
498519

499520
propertyName += "Command";
500521

522+
if (compilation is not null)
523+
{
524+
// We can use the field keyword as the generated field name if the language version is C# 14 or greater, or if it's C# 13 and the preview features are enabled.
525+
// In this case, there is no need to generate a backing field, as the property itself will be auto-generated with an underlying field.
526+
bool useFieldKeyword = false;
527+
528+
#if ROSLYN_5_0_0_OR_GREATER
529+
useFieldKeyword = compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp14);
530+
#elif ROSLYN_4_12_0_OR_GREATER
531+
useFieldKeyword = compilation.IsLanguageVersionPreview();
532+
#endif
533+
534+
if (useFieldKeyword)
535+
{
536+
return (null, propertyName);
537+
}
538+
}
539+
501540
char firstCharacter = propertyName[0];
502541
char loweredFirstCharacter = char.ToLower(firstCharacter, CultureInfo.InvariantCulture);
503542

0 commit comments

Comments
 (0)