Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public override void Initialize(AnalysisContext context)
return;
}

(_, string propertyName) = RelayCommandGenerator.Execute.GetGeneratedFieldAndPropertyNames(methodSymbol);
(_, string propertyName) = RelayCommandGenerator.Execute.GetGeneratedFieldAndPropertyNames(methodSymbol, context.Compilation);

if (DoesGeneratedBindableCustomPropertyAttributeIncludePropertyName(generatedBindableCustomPropertyAttribute, propertyName))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
/// A model with gathered info on a given command method.
/// </summary>
/// <param name="MethodName">The name of the target method.</param>
/// <param name="FieldName">The resulting field name for the generated command.</param>
/// <param name="FieldName">The resulting field name for the generated command, or null if the <see langword="field"/> is available.</param>
/// <param name="PropertyName">The resulting property name for the generated command.</param>
/// <param name="CommandInterfaceType">The command interface type name.</param>
/// <param name="CommandClassType">The command class type name.</param>
Expand All @@ -26,7 +26,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
/// <param name="ForwardedAttributes">The sequence of forwarded attributes for the generated members.</param>
internal sealed record CommandInfo(
string MethodName,
string FieldName,
string? FieldName,
string PropertyName,
string CommandInterfaceType,
string CommandClassType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public static bool TryGetInfo(
token.ThrowIfCancellationRequested();

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

token.ThrowIfCancellationRequested();

Expand Down Expand Up @@ -207,25 +207,33 @@ public static ImmutableArray<MemberDeclarationSyntax> GetSyntax(CommandInfo comm
.Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax())))
.ToArray();

// Construct the generated field as follows:
//
// /// <summary>The backing field for <see cref="<COMMAND_PROPERTY_NAME>"/></summary>
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
// <FORWARDED_ATTRIBUTES>
// private <COMMAND_TYPE>? <COMMAND_FIELD_NAME>;
FieldDeclarationSyntax fieldDeclaration =
FieldDeclaration(
VariableDeclaration(NullableType(IdentifierName(commandClassTypeName)))
.AddVariables(VariableDeclarator(Identifier(commandInfo.FieldName))))
.AddModifiers(Token(SyntaxKind.PrivateKeyword))
.AddAttributeLists(
AttributeList(SingletonSeparatedList(
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
.AddArgumentListArguments(
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).FullName))),
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).Assembly.GetName().Version.ToString()))))))
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <summary>The backing field for <see cref=\"{commandInfo.PropertyName}\"/>.</summary>")), SyntaxKind.OpenBracketToken, TriviaList())))
.AddAttributeLists(forwardedFieldAttributes);
ImmutableArrayBuilder<MemberDeclarationSyntax> declarations = ImmutableArrayBuilder<MemberDeclarationSyntax>.Rent();

// Declare a backing field if needed
if (commandInfo.FieldName is not null)
{
// Construct the generated field as follows:
//
// /// <summary>The backing field for <see cref="<COMMAND_PROPERTY_NAME>"/></summary>
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
// <FORWARDED_ATTRIBUTES>
// private <COMMAND_TYPE>? <COMMAND_FIELD_NAME>;
FieldDeclarationSyntax fieldDeclaration =
FieldDeclaration(
VariableDeclaration(NullableType(IdentifierName(commandClassTypeName)))
.AddVariables(VariableDeclarator(Identifier(commandInfo.FieldName))))
.AddModifiers(Token(SyntaxKind.PrivateKeyword))
.AddAttributeLists(
AttributeList(SingletonSeparatedList(
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
.AddArgumentListArguments(
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).FullName))),
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).Assembly.GetName().Version.ToString()))))))
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <summary>The backing field for <see cref=\"{commandInfo.PropertyName}\"/>.</summary>")), SyntaxKind.OpenBracketToken, TriviaList())))
.AddAttributeLists(forwardedFieldAttributes);

declarations.Add(fieldDeclaration);
}

// Prepares the argument to pass the underlying method to invoke
using ImmutableArrayBuilder<ArgumentSyntax> commandCreationArguments = ImmutableArrayBuilder<ArgumentSyntax>.Rent();
Expand Down Expand Up @@ -337,35 +345,44 @@ public static ImmutableArray<MemberDeclarationSyntax> GetSyntax(CommandInfo comm
ArrowExpressionClause(
AssignmentExpression(
SyntaxKind.CoalesceAssignmentExpression,
IdentifierName(commandInfo.FieldName),
commandInfo.FieldName is not null ? IdentifierName(commandInfo.FieldName) : IdentifierName("field"),
ObjectCreationExpression(IdentifierName(commandClassTypeName))
.AddArgumentListArguments(commandCreationArguments.ToArray()))))
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));

declarations.Add(propertyDeclaration);

// Conditionally declare the additional members for the cancel commands
if (commandInfo.IncludeCancelCommand)
{
// Prepare all necessary member and type names
string cancelCommandFieldName = $"{commandInfo.FieldName.Substring(0, commandInfo.FieldName.Length - "Command".Length)}CancelCommand";
string? cancelCommandFieldName = commandInfo.FieldName is not null ? $"{commandInfo.FieldName.Substring(0, commandInfo.FieldName.Length - "Command".Length)}CancelCommand" : null;
string cancelCommandPropertyName = $"{commandInfo.PropertyName.Substring(0, commandInfo.PropertyName.Length - "Command".Length)}CancelCommand";

// Construct the generated field for the cancel command as follows:
//
// /// <summary>The backing field for <see cref="<COMMAND_PROPERTY_NAME>"/></summary>
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
// private global::System.Windows.Input.ICommand? <CANCEL_COMMAND_FIELD_NAME>;
FieldDeclarationSyntax cancelCommandFieldDeclaration =
FieldDeclaration(
VariableDeclaration(NullableType(IdentifierName("global::System.Windows.Input.ICommand")))
.AddVariables(VariableDeclarator(Identifier(cancelCommandFieldName))))
.AddModifiers(Token(SyntaxKind.PrivateKeyword))
.AddAttributeLists(
AttributeList(SingletonSeparatedList(
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
.AddArgumentListArguments(
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).FullName))),
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).Assembly.GetName().Version.ToString()))))))
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <summary>The backing field for <see cref=\"{cancelCommandPropertyName}\"/>.</summary>")), SyntaxKind.OpenBracketToken, TriviaList())));
// Declare a backing field for the cancel command if needed.
// This is only needed if we can't use the field keyword for the main command, as otherwise the cancel command can just use its own field keyword.
if (cancelCommandFieldName is not null)
{
// Construct the generated field for the cancel command as follows:
//
// /// <summary>The backing field for <see cref="<COMMAND_PROPERTY_NAME>"/></summary>
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
// private global::System.Windows.Input.ICommand? <CANCEL_COMMAND_FIELD_NAME>;
FieldDeclarationSyntax cancelCommandFieldDeclaration =
FieldDeclaration(
VariableDeclaration(NullableType(IdentifierName("global::System.Windows.Input.ICommand")))
.AddVariables(VariableDeclarator(Identifier(cancelCommandFieldName))))
.AddModifiers(Token(SyntaxKind.PrivateKeyword))
.AddAttributeLists(
AttributeList(SingletonSeparatedList(
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
.AddArgumentListArguments(
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).FullName))),
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).Assembly.GetName().Version.ToString()))))))
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <summary>The backing field for <see cref=\"{cancelCommandPropertyName}\"/>.</summary>")), SyntaxKind.OpenBracketToken, TriviaList())));

declarations.Add(cancelCommandFieldDeclaration);
}

// Construct the generated property as follows (the explicit delegate cast is needed to avoid overload resolution conflicts):
//
Expand Down Expand Up @@ -393,7 +410,7 @@ public static ImmutableArray<MemberDeclarationSyntax> GetSyntax(CommandInfo comm
ArrowExpressionClause(
AssignmentExpression(
SyntaxKind.CoalesceAssignmentExpression,
IdentifierName(cancelCommandFieldName),
cancelCommandFieldName is not null ? IdentifierName(cancelCommandFieldName) : IdentifierName("field"),
InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
Expand All @@ -402,10 +419,11 @@ public static ImmutableArray<MemberDeclarationSyntax> GetSyntax(CommandInfo comm
.AddArgumentListArguments(Argument(IdentifierName(commandInfo.PropertyName))))))
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));

return ImmutableArray.Create<MemberDeclarationSyntax>(fieldDeclaration, propertyDeclaration, cancelCommandFieldDeclaration, cancelCommandPropertyDeclaration);
declarations.Add(cancelCommandPropertyDeclaration);

}

return ImmutableArray.Create<MemberDeclarationSyntax>(fieldDeclaration, propertyDeclaration);
return declarations.ToImmutable();
}

/// <summary>
Expand Down Expand Up @@ -474,8 +492,9 @@ private static bool IsCommandDefinitionUnique(IMethodSymbol methodSymbol, in Imm
/// Get the generated field and property names for the input method.
/// </summary>
/// <param name="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
/// <param name="compilation">The compilation info, used to determine language version.</param>
/// <returns>The generated field and property names for <paramref name="methodSymbol"/>.</returns>
public static (string FieldName, string PropertyName) GetGeneratedFieldAndPropertyNames(IMethodSymbol methodSymbol)
public static (string? FieldName, string PropertyName) GetGeneratedFieldAndPropertyNames(IMethodSymbol methodSymbol, Compilation? compilation = null)
{
string propertyName = methodSymbol.Name;

Expand All @@ -498,6 +517,24 @@ public static (string FieldName, string PropertyName) GetGeneratedFieldAndProper

propertyName += "Command";

if (compilation is not null)
{
// 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.
// In this case, there is no need to generate a backing field, as the property itself will be auto-generated with an underlying field.
bool useFieldKeyword = false;

#if ROSLYN_5_0_0_OR_GREATER
useFieldKeyword = compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp14);
#elif ROSLYN_4_12_0_OR_GREATER
useFieldKeyword = compilation.IsLanguageVersionPreview();
#endif

if (useFieldKeyword)
{
return (null, propertyName);
}
}

char firstCharacter = propertyName[0];
char loweredFirstCharacter = char.ToLower(firstCharacter, CultureInfo.InvariantCulture);

Expand Down
Loading