Skip to content

Commit

Permalink
Added "propr" snippet for required properties (#75339)
Browse files Browse the repository at this point in the history
* added prop snippet

* updated xlf files

* test fixes

* fixed spacing

* reverted localization

* moved snipped name to CSharpSnippetIdentifiers

* addressed PR comments

* handled interface case

* fix formatting

* fixed PR comments

* fixed PR comments

* fixed PR comments

* moved required prop key in CSharpFeaturesResources
  • Loading branch information
victor-pogor authored Oct 8, 2024
1 parent bbd787e commit 55a809f
Show file tree
Hide file tree
Showing 22 changed files with 271 additions and 12 deletions.
3 changes: 3 additions & 0 deletions src/Features/CSharp/Portable/CSharpFeaturesResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -619,4 +619,7 @@
<value>do-while loop</value>
<comment>{Locked="do"}{Locked="while"} "do" and "while" are C# keywords and should not be localized.</comment>
</data>
<data name="required_property" xml:space="preserve">
<value>required property</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -58,6 +60,8 @@ protected override async Task<PropertyDeclarationSyntax> GenerateSnippetSyntaxAs
modifiers = SyntaxTokenList.Create(PublicKeyword);
}

modifiers = modifiers.AddRange(GetAdditionalPropertyModifiers(syntaxContext));

return SyntaxFactory.PropertyDeclaration(
attributeLists: default,
modifiers: modifiers,
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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
{
$$
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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}}""");
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand All @@ -65,4 +66,9 @@ interface MyInterface
}
""");
}

[Theory]
[MemberData(nameof(CommonSnippetTestData.AllAccessibilityModifiers), MemberType = typeof(CommonSnippetTestData))]
public override Task InsertSnippetAfterAllowedAccessibilityModifierTest(string modifier)
=> base.InsertSnippetAfterAllowedAccessibilityModifierTest(modifier);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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("""
Expand All @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -50,7 +51,7 @@ readonly partial struct MyStruct
""");
}

public override async Task InsertSnippetInInterfaceTest()
public override async Task VerifySnippetInInterfaceTest()
{
await VerifyDefaultPropertyAsync("""
interface MyInterface
Expand All @@ -59,4 +60,9 @@ interface MyInterface
}
""");
}

[Theory]
[MemberData(nameof(CommonSnippetTestData.AllAccessibilityModifiers), MemberType = typeof(CommonSnippetTestData))]
public override Task InsertSnippetAfterAllowedAccessibilityModifierTest(string modifier)
=> base.InsertSnippetAfterAllowedAccessibilityModifierTest(modifier);
}
Loading

0 comments on commit 55a809f

Please sign in to comment.