diff --git a/src/Features/CSharp/Portable/CSharpFeaturesResources.resx b/src/Features/CSharp/Portable/CSharpFeaturesResources.resx index e14e82032ac9c..9840f4ec58e31 100644 --- a/src/Features/CSharp/Portable/CSharpFeaturesResources.resx +++ b/src/Features/CSharp/Portable/CSharpFeaturesResources.resx @@ -619,4 +619,7 @@ do-while loop {Locked="do"}{Locked="while"} "do" and "while" are C# keywords and should not be localized. + + required property + \ No newline at end of file diff --git a/src/Features/CSharp/Portable/Snippets/AbstractCSharpAutoPropertySnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/AbstractCSharpAutoPropertySnippetProvider.cs index 9e6a532dd26f2..71db65b2cab5a 100644 --- a/src/Features/CSharp/Portable/Snippets/AbstractCSharpAutoPropertySnippetProvider.cs +++ b/src/Features/CSharp/Portable/Snippets/AbstractCSharpAutoPropertySnippetProvider.cs @@ -30,6 +30,8 @@ internal abstract class AbstractCSharpAutoPropertySnippetProvider : AbstractProp protected virtual AccessorDeclarationSyntax? GenerateSetAccessorDeclaration(CSharpSyntaxContext syntaxContext, SyntaxGenerator generator, CancellationToken cancellationToken) => (AccessorDeclarationSyntax)generator.SetAccessorDeclaration(); + protected virtual SyntaxToken[] GetAdditionalPropertyModifiers(CSharpSyntaxContext? syntaxContext) => []; + protected override bool IsValidSnippetLocationCore(SnippetContext context, CancellationToken cancellationToken) { return context.SyntaxContext.SyntaxTree.IsMemberDeclarationContext(context.Position, (CSharpSyntaxContext)context.SyntaxContext, @@ -58,6 +60,8 @@ protected override async Task GenerateSnippetSyntaxAs modifiers = SyntaxTokenList.Create(PublicKeyword); } + modifiers = modifiers.AddRange(GetAdditionalPropertyModifiers(syntaxContext)); + return SyntaxFactory.PropertyDeclaration( attributeLists: default, modifiers: modifiers, diff --git a/src/Features/CSharp/Portable/Snippets/CSharpProprSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpProprSnippetProvider.cs new file mode 100644 index 0000000000000..d1444d0fa1984 --- /dev/null +++ b/src/Features/CSharp/Portable/Snippets/CSharpProprSnippetProvider.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Composition; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Snippets; +using Microsoft.CodeAnalysis.Snippets.SnippetProviders; +using static Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTokens; + +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +[ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class CSharpProprSnippetProvider() : AbstractCSharpAutoPropertySnippetProvider +{ + public override string Identifier => CSharpSnippetIdentifiers.RequiredProperty; + + public override string Description => CSharpFeaturesResources.required_property; + + protected override SyntaxToken[] GetAdditionalPropertyModifiers(CSharpSyntaxContext? syntaxContext) => [RequiredKeyword]; + + protected override bool IsValidSnippetLocationCore(SnippetContext context, CancellationToken cancellationToken) + { + if (!base.IsValidSnippetLocationCore(context, cancellationToken)) + return false; + + var syntaxContext = (CSharpSyntaxContext)context.SyntaxContext; + var precedingModifiers = syntaxContext.PrecedingModifiers; + + // The required modifier can't be applied to members of an interface + if (syntaxContext.ContainingTypeDeclaration is InterfaceDeclarationSyntax) + return false; + + // "protected internal" modifiers are valid for required property + if (precedingModifiers.IsSupersetOf([SyntaxKind.ProtectedKeyword, SyntaxKind.InternalKeyword])) + return true; + + // "private", "private protected", "protected" and "private protected" modifiers are NOT valid for required property + if (precedingModifiers.Any(syntaxKind => syntaxKind is SyntaxKind.PrivateKeyword or SyntaxKind.ProtectedKeyword)) + return false; + + return true; + } + + protected override AccessorDeclarationSyntax? GenerateSetAccessorDeclaration(CSharpSyntaxContext syntaxContext, SyntaxGenerator generator, CancellationToken cancellationToken) + { + // Having a property with `set` accessor in a readonly struct leads to a compiler error. + // So if user executes snippet inside a readonly struct the right thing to do is to not generate `set` accessor at all + if (syntaxContext.ContainingTypeDeclaration is StructDeclarationSyntax structDeclaration && + syntaxContext.SemanticModel.GetDeclaredSymbol(structDeclaration, cancellationToken) is { IsReadOnly: true }) + { + return null; + } + + return base.GenerateSetAccessorDeclaration(syntaxContext, generator, cancellationToken); + } +} diff --git a/src/Features/CSharp/Portable/Snippets/CSharpSnippetIdentifiers.cs b/src/Features/CSharp/Portable/Snippets/CSharpSnippetIdentifiers.cs index f2c39f292e8d3..406a380e2ca8a 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpSnippetIdentifiers.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpSnippetIdentifiers.cs @@ -18,6 +18,7 @@ internal static class CSharpSnippetIdentifiers public const string ReversedFor = "forr"; public const string ForEach = "foreach"; public const string InitOnlyProperty = "propi"; + public const string RequiredProperty = "propr"; public const string If = "if"; public const string Interface = "interface"; public const string Lock = "lock"; diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.cs.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.cs.xlf index 7c277d113addf..a52b85765ae30 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.cs.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.cs.xlf @@ -432,6 +432,11 @@ struktura záznamu + + required property + required property + + reversed for loop obrácená smyčka typu for diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.de.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.de.xlf index 43abdee07e7c8..1a1367d42d040 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.de.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.de.xlf @@ -432,6 +432,11 @@ Datensatzstruktur + + required property + required property + + reversed for loop reversed for-Schleife diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.es.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.es.xlf index fb1506e889134..c93710402b6aa 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.es.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.es.xlf @@ -432,6 +432,11 @@ registro de estructuras + + required property + required property + + reversed for loop bucle for invertido diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.fr.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.fr.xlf index c3660c23351bb..343234c206710 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.fr.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.fr.xlf @@ -432,6 +432,11 @@ struct d’enregistrement + + required property + required property + + reversed for loop boucle for inversée diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.it.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.it.xlf index d781f9904657c..c80b27b78ea29 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.it.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.it.xlf @@ -432,6 +432,11 @@ struct di record + + required property + required property + + reversed for loop invertito for loop diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ja.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ja.xlf index 261bbf1413967..f52983cb5b515 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ja.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ja.xlf @@ -432,6 +432,11 @@ レコード構造体 + + required property + required property + + reversed for loop 逆 for ループ diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ko.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ko.xlf index 5bc2e5b7bcf2e..d9ecd34b88674 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ko.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ko.xlf @@ -432,6 +432,11 @@ 레코드 구조체 + + required property + required property + + reversed for loop for 루프 대해 반전 diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pl.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pl.xlf index af1a6a9a74558..e42d24a561225 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pl.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pl.xlf @@ -432,6 +432,11 @@ struktura rekordów + + required property + required property + + reversed for loop odwrócona pętla for diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pt-BR.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pt-BR.xlf index 8a38cb15bb1ec..9a67ba4d06d54 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pt-BR.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pt-BR.xlf @@ -432,6 +432,11 @@ registrar struct + + required property + required property + + reversed for loop loop for invertido diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ru.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ru.xlf index fba75ab606895..b22d9ae9aa674 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ru.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ru.xlf @@ -432,6 +432,11 @@ структура записей + + required property + required property + + reversed for loop перевернутый for цикла diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.tr.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.tr.xlf index ee5a5cf4b1dcb..dbbe9998b322c 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.tr.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.tr.xlf @@ -432,6 +432,11 @@ kayıt yapısı + + required property + required property + + reversed for loop ters for döngüsü diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hans.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hans.xlf index 241b9c403b1b2..5ce1d189d6f4c 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hans.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hans.xlf @@ -432,6 +432,11 @@ 记录结构 + + required property + required property + + reversed for loop 反转的 for 循环 diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hant.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hant.xlf index 645b87cd0be55..bb5d3d9f4a30b 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hant.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hant.xlf @@ -432,6 +432,11 @@ 記錄結構 + + required property + required property + + reversed for loop 反轉 for 迴圈 diff --git a/src/Features/CSharpTest/Snippets/AbstractCSharpAutoPropertySnippetProviderTests.cs b/src/Features/CSharpTest/Snippets/AbstractCSharpAutoPropertySnippetProviderTests.cs index 8dbe4ef2d7abb..81c5812d9be3a 100644 --- a/src/Features/CSharpTest/Snippets/AbstractCSharpAutoPropertySnippetProviderTests.cs +++ b/src/Features/CSharpTest/Snippets/AbstractCSharpAutoPropertySnippetProviderTests.cs @@ -56,11 +56,14 @@ class MyClass """); } - [Fact] - public async Task InsertSnippetInRecordTest() + [Theory] + [InlineData("record")] + [InlineData("record struct")] + [InlineData("record class")] + public async Task InsertSnippetInRecordTest(string recordType) { - await VerifyDefaultPropertyAsync(""" - record MyRecord + await VerifyDefaultPropertyAsync($$""" + {{recordType}} MyRecord { $$ } @@ -90,7 +93,7 @@ struct MyStruct // This case might produce non-default results for different snippets (e.g. no `set` accessor in 'propg' snippet), // so it is tested separately for all of them [Fact] - public abstract Task InsertSnippetInInterfaceTest(); + public abstract Task VerifySnippetInInterfaceTest(); [Fact] public async Task InsertSnippetNamingTest() @@ -143,9 +146,7 @@ public Program() """); } - [Theory] - [MemberData(nameof(CommonSnippetTestData.AllAccessibilityModifiers), MemberType = typeof(CommonSnippetTestData))] - public async Task InsertSnippetAfterAccessibilityModifierTest(string modifier) + public virtual async Task InsertSnippetAfterAllowedAccessibilityModifierTest(string modifier) { await VerifyPropertyAsync($$""" class Program @@ -162,6 +163,6 @@ protected async Task VerifyPropertyAsync([StringSyntax(PredefinedEmbeddedLanguag await VerifySnippetAsync(markup, expectedCode); } - protected Task VerifyDefaultPropertyAsync([StringSyntax(PredefinedEmbeddedLanguageNames.CSharpTest)] string markup, string propertyName = "MyProperty") + protected virtual Task VerifyDefaultPropertyAsync([StringSyntax(PredefinedEmbeddedLanguageNames.CSharpTest)] string markup, string propertyName = "MyProperty") => VerifyPropertyAsync(markup, $$"""public {|0:int|} {|1:{{propertyName}}|} {{DefaultPropertyBlockText}}"""); } diff --git a/src/Features/CSharpTest/Snippets/CSharpPropSnippetProviderTests.cs b/src/Features/CSharpTest/Snippets/CSharpPropSnippetProviderTests.cs index 903f9d6a2b942..5b13ab35d4a6e 100644 --- a/src/Features/CSharpTest/Snippets/CSharpPropSnippetProviderTests.cs +++ b/src/Features/CSharpTest/Snippets/CSharpPropSnippetProviderTests.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Threading.Tasks; +using Xunit; namespace Microsoft.CodeAnalysis.CSharp.UnitTests.Snippets; @@ -56,7 +57,7 @@ readonly partial struct MyStruct """, "public {|0:int|} {|1:MyProperty|} { get; }"); } - public override async Task InsertSnippetInInterfaceTest() + public override async Task VerifySnippetInInterfaceTest() { await VerifyDefaultPropertyAsync(""" interface MyInterface @@ -65,4 +66,9 @@ interface MyInterface } """); } + + [Theory] + [MemberData(nameof(CommonSnippetTestData.AllAccessibilityModifiers), MemberType = typeof(CommonSnippetTestData))] + public override Task InsertSnippetAfterAllowedAccessibilityModifierTest(string modifier) + => base.InsertSnippetAfterAllowedAccessibilityModifierTest(modifier); } diff --git a/src/Features/CSharpTest/Snippets/CSharpPropgSnippetProviderTests.cs b/src/Features/CSharpTest/Snippets/CSharpPropgSnippetProviderTests.cs index d4413d32e9bc5..2f4a46a5ea84d 100644 --- a/src/Features/CSharpTest/Snippets/CSharpPropgSnippetProviderTests.cs +++ b/src/Features/CSharpTest/Snippets/CSharpPropgSnippetProviderTests.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Threading.Tasks; +using Xunit; namespace Microsoft.CodeAnalysis.CSharp.UnitTests.Snippets; @@ -56,7 +57,7 @@ readonly partial struct MyStruct """, "public {|0:int|} {|1:MyProperty|} { get; }"); } - public override async Task InsertSnippetInInterfaceTest() + public override async Task VerifySnippetInInterfaceTest() { // Ensure we don't generate redundant `set` accessor when executed in interface await VerifyPropertyAsync(""" @@ -66,4 +67,9 @@ interface MyInterface } """, "public {|0:int|} {|1:MyProperty|} { get; }"); } + + [Theory] + [MemberData(nameof(CommonSnippetTestData.AllAccessibilityModifiers), MemberType = typeof(CommonSnippetTestData))] + public override Task InsertSnippetAfterAllowedAccessibilityModifierTest(string modifier) + => base.InsertSnippetAfterAllowedAccessibilityModifierTest(modifier); } diff --git a/src/Features/CSharpTest/Snippets/CSharpPropiSnippetProviderTests.cs b/src/Features/CSharpTest/Snippets/CSharpPropiSnippetProviderTests.cs index 880dcf3a70927..38ab037b43245 100644 --- a/src/Features/CSharpTest/Snippets/CSharpPropiSnippetProviderTests.cs +++ b/src/Features/CSharpTest/Snippets/CSharpPropiSnippetProviderTests.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Threading.Tasks; +using Xunit; namespace Microsoft.CodeAnalysis.CSharp.UnitTests.Snippets; @@ -50,7 +51,7 @@ readonly partial struct MyStruct """); } - public override async Task InsertSnippetInInterfaceTest() + public override async Task VerifySnippetInInterfaceTest() { await VerifyDefaultPropertyAsync(""" interface MyInterface @@ -59,4 +60,9 @@ interface MyInterface } """); } + + [Theory] + [MemberData(nameof(CommonSnippetTestData.AllAccessibilityModifiers), MemberType = typeof(CommonSnippetTestData))] + public override Task InsertSnippetAfterAllowedAccessibilityModifierTest(string modifier) + => base.InsertSnippetAfterAllowedAccessibilityModifierTest(modifier); } diff --git a/src/Features/CSharpTest/Snippets/CSharpProprSnippetProviderTests.cs b/src/Features/CSharpTest/Snippets/CSharpProprSnippetProviderTests.cs new file mode 100644 index 0000000000000..5f75aef2b5a01 --- /dev/null +++ b/src/Features/CSharpTest/Snippets/CSharpProprSnippetProviderTests.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.CSharp.UnitTests.Snippets; + +public sealed class CSharpProprSnippetProviderTests : AbstractCSharpAutoPropertySnippetProviderTests +{ + protected override string SnippetIdentifier => "propr"; + + protected override string DefaultPropertyBlockText => "{ get; set; }"; + + public override async Task InsertSnippetInReadonlyStructTest() + { + // Ensure we don't generate redundant `set` accessor when executed in readonly struct + await VerifyPropertyAsync(""" + readonly struct MyStruct + { + $$ + } + """, "public required {|0:int|} {|1:MyProperty|} { get; }"); + } + + public override async Task InsertSnippetInReadonlyStructTest_ReadonlyModifierInOtherPartialDeclaration() + { + // Ensure we don't generate redundant `set` accessor when executed in readonly struct + await VerifyPropertyAsync(""" + partial struct MyStruct + { + $$ + } + + readonly partial struct MyStruct + { + } + """, "public required {|0:int|} {|1:MyProperty|} { get; }"); + } + + public override async Task InsertSnippetInReadonlyStructTest_ReadonlyModifierInOtherPartialDeclaration_MissingPartialModifier() + { + // Even though there is no `partial` modifier on the first declaration + // compiler still treats the whole type as partial since it is more likely that + // the user's intent was to have a partial type and they just forgot the modifier. + // Thus we still recognize that as `readonly` context and don't generate a setter + await VerifyPropertyAsync(""" + struct MyStruct + { + $$ + } + + readonly partial struct MyStruct + { + } + """, "public required {|0:int|} {|1:MyProperty|} { get; }"); + } + + public override async Task VerifySnippetInInterfaceTest() + { + await VerifySnippetIsAbsentAsync(""" + interface MyInterface + { + $$ + } + """); + } + + [Theory] + [InlineData("public")] + [InlineData("internal")] + [InlineData("protected internal")] + public override async Task InsertSnippetAfterAllowedAccessibilityModifierTest(string modifier) + { + await VerifyPropertyAsync($$""" + class Program + { + {{modifier}} $$ + } + """, $$"""required {|0:int|} {|1:MyProperty|} {{DefaultPropertyBlockText}}"""); + } + + [Theory] + [InlineData("private")] + [InlineData("protected")] + [InlineData("private protected")] + public async Task NoSnippetAfterWrongAccessibilityModifierTest(string modifier) + { + await VerifySnippetIsAbsentAsync($$""" + class Program + { + {{modifier}} $$ + } + """); + } + + protected override Task VerifyDefaultPropertyAsync([StringSyntax(PredefinedEmbeddedLanguageNames.CSharpTest)] string markup, string propertyName = "MyProperty") + => VerifyPropertyAsync(markup, $$"""public required {|0:int|} {|1:{{propertyName}}|} {{DefaultPropertyBlockText}}"""); +}