diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs index 10029b84b..3feec713b 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs @@ -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)) { diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs index 70372a03f..e1ce546ac 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs @@ -11,7 +11,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models; /// A model with gathered info on a given command method. /// /// The name of the target method. -/// The resulting field name for the generated command. +/// The resulting field name for the generated command, or null if the is available. /// The resulting property name for the generated command. /// The command interface type name. /// The command class type name. @@ -26,7 +26,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models; /// The sequence of forwarded attributes for the generated members. internal sealed record CommandInfo( string MethodName, - string FieldName, + string? FieldName, string PropertyName, string CommandInterfaceType, string CommandClassType, diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs index 57eb1ebcf..389f07e3f 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs @@ -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(); @@ -207,25 +207,33 @@ public static ImmutableArray GetSyntax(CommandInfo comm .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) .ToArray(); - // Construct the generated field as follows: - // - // /// The backing field for - // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] - // - // private ? ; - 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($"/// The backing field for .")), SyntaxKind.OpenBracketToken, TriviaList()))) - .AddAttributeLists(forwardedFieldAttributes); + ImmutableArrayBuilder declarations = ImmutableArrayBuilder.Rent(); + + // Declare a backing field if needed + if (commandInfo.FieldName is not null) + { + // Construct the generated field as follows: + // + // /// The backing field for + // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] + // + // private ? ; + 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($"/// The backing field for .")), SyntaxKind.OpenBracketToken, TriviaList()))) + .AddAttributeLists(forwardedFieldAttributes); + + declarations.Add(fieldDeclaration); + } // Prepares the argument to pass the underlying method to invoke using ImmutableArrayBuilder commandCreationArguments = ImmutableArrayBuilder.Rent(); @@ -337,35 +345,44 @@ public static ImmutableArray 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: - // - // /// The backing field for - // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] - // private global::System.Windows.Input.ICommand? ; - 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($"/// The backing field for .")), 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: + // + // /// The backing field for + // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] + // private global::System.Windows.Input.ICommand? ; + 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($"/// The backing field for .")), SyntaxKind.OpenBracketToken, TriviaList()))); + + declarations.Add(cancelCommandFieldDeclaration); + } // Construct the generated property as follows (the explicit delegate cast is needed to avoid overload resolution conflicts): // @@ -393,7 +410,7 @@ public static ImmutableArray GetSyntax(CommandInfo comm ArrowExpressionClause( AssignmentExpression( SyntaxKind.CoalesceAssignmentExpression, - IdentifierName(cancelCommandFieldName), + cancelCommandFieldName is not null ? IdentifierName(cancelCommandFieldName) : IdentifierName("field"), InvocationExpression( MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, @@ -402,10 +419,11 @@ public static ImmutableArray GetSyntax(CommandInfo comm .AddArgumentListArguments(Argument(IdentifierName(commandInfo.PropertyName)))))) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); - return ImmutableArray.Create(fieldDeclaration, propertyDeclaration, cancelCommandFieldDeclaration, cancelCommandPropertyDeclaration); + declarations.Add(cancelCommandPropertyDeclaration); + } - return ImmutableArray.Create(fieldDeclaration, propertyDeclaration); + return declarations.ToImmutable(); } /// @@ -474,8 +492,9 @@ private static bool IsCommandDefinitionUnique(IMethodSymbol methodSymbol, in Imm /// Get the generated field and property names for the input method. /// /// The input instance to process. + /// The compilation info, used to determine language version. /// The generated field and property names for . - 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; @@ -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); diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs index 646058a7a..3a12701b3 100644 --- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs +++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs @@ -1003,6 +1003,7 @@ public void Test_ObservableProperty_WithExplicitAttributeForProperty() Assert.AreEqual((Animal)67, testAttribute2.Animal); } +#if !ROSLYN_4_12_0_OR_GREATER // See https://github.com/CommunityToolkit/dotnet/issues/446 [TestMethod] public void Test_ObservableProperty_CommandNamesThatCantBeLowered() @@ -1021,6 +1022,7 @@ public void Test_ObservableProperty_CommandNamesThatCantBeLowered() Assert.AreSame(model.c中文Command, fieldInfo?.GetValue(model)); } +#endif // See https://github.com/CommunityToolkit/dotnet/issues/375 [TestMethod] diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_RelayCommandAttribute.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_RelayCommandAttribute.cs index 24293805a..8fa2e679c 100644 --- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_RelayCommandAttribute.cs +++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_RelayCommandAttribute.cs @@ -571,6 +571,7 @@ public void Test_RelayCommandAttribute_CanExecuteWithNullabilityAnnotations() [TestMethod] public void Test_RelayCommandAttribute_WithExplicitAttributesForFieldAndProperty() { +#if !ROSLYN_4_12_0_OR_GREATER FieldInfo fooField = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetField("fooCommand", BindingFlags.Instance | BindingFlags.NonPublic)!; Assert.IsNotNull(fooField.GetCustomAttribute()); @@ -579,6 +580,7 @@ public void Test_RelayCommandAttribute_WithExplicitAttributesForFieldAndProperty Assert.IsNotNull(fooField.GetCustomAttribute()); Assert.AreEqual(100, fooField.GetCustomAttribute()!.Length); +#endif PropertyInfo fooProperty = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetProperty("FooCommand")!; Assert.IsNotNull(fooProperty.GetCustomAttribute()); @@ -618,17 +620,21 @@ static void ValidateTestAttribute(TestValidationAttribute testAttribute) Assert.AreEqual(Test_ObservablePropertyAttribute.Animal.Llama, testAttribute.Animal); } +#if !ROSLYN_4_12_0_OR_GREATER FieldInfo fooBarField = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetField("fooBarCommand", BindingFlags.Instance | BindingFlags.NonPublic)!; ValidateTestAttribute(fooBarField.GetCustomAttribute()!); +#endif PropertyInfo fooBarProperty = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetProperty("FooBarCommand")!; ValidateTestAttribute(fooBarProperty.GetCustomAttribute()!); +#if !ROSLYN_4_12_0_OR_GREATER FieldInfo barBazField = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetField("barBazCommand", BindingFlags.Instance | BindingFlags.NonPublic)!; Assert.IsNotNull(barBazField.GetCustomAttribute()); +#endif PropertyInfo barBazCommand = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetProperty("BarBazCommand")!; @@ -670,11 +676,13 @@ public void Test_RelayCommandAttribute_WithPartialCommandMethodDefinitions() _ = Assert.IsInstanceOfType(model.BazCommand); _ = Assert.IsInstanceOfType(model.FooBarCommand); +#if !ROSLYN_4_12_0_OR_GREATER FieldInfo bazField = typeof(ModelWithPartialCommandMethods).GetField("bazCommand", BindingFlags.Instance | BindingFlags.NonPublic)!; Assert.IsNotNull(bazField.GetCustomAttribute()); Assert.IsNotNull(bazField.GetCustomAttribute()); Assert.AreEqual(1, bazField.GetCustomAttribute()!.Length); +#endif PropertyInfo bazProperty = typeof(ModelWithPartialCommandMethods).GetProperty("BazCommand")!; @@ -682,11 +690,13 @@ public void Test_RelayCommandAttribute_WithPartialCommandMethodDefinitions() Assert.AreEqual(2, bazProperty.GetCustomAttribute()!.Length); Assert.IsNotNull(bazProperty.GetCustomAttribute()); +#if !ROSLYN_4_12_0_OR_GREATER FieldInfo fooBarField = typeof(ModelWithPartialCommandMethods).GetField("fooBarCommand", BindingFlags.Instance | BindingFlags.NonPublic)!; Assert.IsNotNull(fooBarField.GetCustomAttribute()); Assert.IsNotNull(fooBarField.GetCustomAttribute()); - Assert.AreEqual(1, fooBarField.GetCustomAttribute()!.Length); + Assert.AreEqual(1, fooBarField.GetCustomAttribute()!.Length); +#endif PropertyInfo fooBarProperty = typeof(ModelWithPartialCommandMethods).GetProperty("FooBarCommand")!;