Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add records support to XmlDocCommentCompletion and ChangeSignature #53052

Merged
merged 22 commits into from
Jul 12, 2021
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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 @@ -9,7 +9,6 @@
using Microsoft.CodeAnalysis.Editor.UnitTests.ChangeSignature;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Test.Utilities.ChangeSignature;
using Roslyn.Test.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.ChangeSignature
Expand Down Expand Up @@ -412,5 +411,26 @@ public override int M(string x, int newIntegerParameter, int y)
}";
await TestChangeSignatureViaCommandAsync(LanguageNames.CSharp, markup, updatedSignature: permutation, expectedUpdatedInvocationDocumentCode: updatedCode);
}

[Fact(Skip = "https://github.com/dotnet/roslyn/issues/53091"), Trait(Traits.Feature, Traits.Features.ChangeSignature)]
public async Task AddParameter_Cascade_Record()
{
var markup = @"
record $$BaseR(int A, int B);

record DerivedR() : BaseR(0, 1);";
var permutation = new AddedParameterOrExistingIndex[]
{
new(1),
new(new AddedParameter(null, "int", "C", CallSiteKind.Value, "3"), "int"),
new(0)
};
var updatedCode = @"
record BaseR(int B, int C, int A);

record DerivedR() : BaseR(1, 3, 0);";

await TestChangeSignatureViaCommandAsync(LanguageNames.CSharp, markup, updatedSignature: permutation, expectedUpdatedInvocationDocumentCode: updatedCode);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1261,5 +1261,39 @@ public void M()
}";
await TestChangeSignatureViaCommandAsync(LanguageNames.CSharp, markup, updatedSignature: permutation, expectedUpdatedInvocationDocumentCode: updatedCode);
}

[Fact, Trait(Traits.Feature, Traits.Features.ChangeSignature)]
[WorkItem(44558, "https://github.com/dotnet/roslyn/issues/44558")]
public async Task AddParameters_Record()
{
var markup = @"
/// <param name=""First""></param>
/// <param name=""Second""></param>
/// <param name=""Third""></param>
record $$R(int First, int Second, int Third)
{
static R M() => new R(1, 2, 3);
}
";
var updatedSignature = new AddedParameterOrExistingIndex[]
{
new(0),
new(2),
new(1),
new(new AddedParameter(null, "int", "Forth", CallSiteKind.Value, "12345"), "System.Int32")
};
var updatedCode = @"
/// <param name=""First""></param>
/// <param name=""Third""></param>
/// <param name=""Second""></param>
/// <param name=""Forth""></param>
record R(int First, int Third, int Second, int Forth)
{
static R M() => new R(1, 3, 2, 12345);
}
";

await TestChangeSignatureViaCommandAsync(LanguageNames.CSharp, markup, updatedSignature: updatedSignature, expectedUpdatedInvocationDocumentCode: updatedCode);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -951,5 +951,29 @@ class D : C, I

await TestChangeSignatureViaCommandAsync(LanguageNames.CSharp, markup, updatedSignature: permutation, expectedUpdatedInvocationDocumentCode: updatedCode);
}

[Fact, Trait(Traits.Feature, Traits.Features.ChangeSignature)]
public async Task ReorderParamTagsInDocComments_Record()
{
var markup = @"
/// <param name=""A""></param>
/// <param name=""B""></param>
/// <param name=""C""></param>
record $$R(int A, int B, int C)
{
public static R Instance = new(0, 1, 2);
}";
var permutation = new[] { 2, 1, 0 };
var updatedCode = @"
/// <param name=""C""></param>
/// <param name=""B""></param>
/// <param name=""A""></param>
record R(int C, int B, int A)
{
public static R Instance = new(2, 1, 0);
}";

await TestChangeSignatureViaCommandAsync(LanguageNames.CSharp, markup, updatedSignature: permutation, expectedUpdatedInvocationDocumentCode: updatedCode);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1012,5 +1012,27 @@ static void Goo()
await VerifyItemExistsAsync(text, "term");
await VerifyItemExistsAsync(text, "description");
}

[WorkItem(52738, "https://github.com/dotnet/roslyn/issues/52738")]
[Fact, Trait(Traits.Feature, Traits.Features.Completion)]
public async Task RecordParam()
{
await VerifyItemsExistAsync(@"
/// $$
public record Goo<T>(string MyParameter);
", "param name=\"MyParameter\"", "typeparam name=\"T\"");
}

[WorkItem(52738, "https://github.com/dotnet/roslyn/issues/52738")]
[Fact, Trait(Traits.Feature, Traits.Features.Completion)]
public async Task RecordParamRef()
{
await VerifyItemsExistAsync(@"
/// <summary>
/// $$
/// <summary>
public record Goo<T>(string MyParameter);
", "paramref name=\"MyParameter\"", "typeparamref name=\"T\"");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Diagnostics;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Xunit;
using Xunit.Abstractions;

Expand Down Expand Up @@ -785,5 +786,29 @@ public void Fizz(int i, int j, int k) {}

await TestAsync(initial, expected);
}

[WorkItem(52738, "https://github.com/dotnet/roslyn/issues/52738")]
[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsAddDocCommentNodes)]
public async Task AddsParamTag_Record()
{
var initial = @"
/// <summary>
///
/// </summary>
/// <param name=""Second""></param>
record R(int [|First|], int Second, int Third);
";

var expected = @"
/// <summary>
///
/// </summary>
/// <param name=""First""></param>
/// <param name=""Second""></param>
/// <param name=""Third""></param>
record R(int First, int Second, int Third);
";
await TestAsync(initial, expected);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,25 @@ class c<T>
End Using
End Function

<WpfTheory, CombinatorialData, Trait(Traits.Feature, Traits.Features.Completion)>
Public Async Function CommitParam_Record(showCompletionInArgumentLists As Boolean) As Task
Using state = TestStateFactory.CreateCSharpTestState(
<Document><![CDATA[
/// <param$$
record R(int I);
]]></Document>, showCompletionInArgumentLists:=showCompletionInArgumentLists)

state.SendInvokeCompletionList()
Await state.AssertCompletionSession()
Await state.AssertSelectedCompletionItem(displayText:="param name=""I""")
state.SendReturn()
Await state.AssertNoCompletionSession()

' /// <param name="I"$$
Await state.AssertLineTextAroundCaret("/// <param name=""I""", "")
End Using
End Function

<WpfTheory, CombinatorialData, Trait(Traits.Feature, Traits.Features.Completion)>
Public Async Function CommitParamNoOpenAngle(showCompletionInArgumentLists As Boolean) As Task

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -825,5 +825,36 @@ End Class]]></Text>.NormalizedValue()

Await TestChangeSignatureViaCommandAsync(LanguageNames.VisualBasic, markup, updatedSignature:=permutation, expectedUpdatedInvocationDocumentCode:=updatedCode)
End Function

<WorkItem(49941, "https://github.com/dotnet/roslyn/issues/49941")>
<Fact, Trait(Traits.Feature, Traits.Features.ChangeSignature)>
Public Async Function TestAddParameter_NoLastWhitespaceTrivia() As Task

Dim markup = <Text><![CDATA[
Class C
''' <summary>
''' </summary>
''' <param name="a"></param>
Sub $$M(a As Integer)
End Sub
End Class]]></Text>.NormalizedValue()
Dim permutation =
{
New AddedParameterOrExistingIndex(0),
New AddedParameterOrExistingIndex(New AddedParameter(Nothing, "Integer", "b", CallSiteKind.Value), "Integer")
}

Dim updatedCode = <Text><![CDATA[
Class C
''' <summary>
''' </summary>
''' <param name="a"></param>
''' <param name="b"></param>
Sub M(a As Integer, b As Integer)
End Sub
End Class]]></Text>.NormalizedValue()

Await TestChangeSignatureViaCommandAsync(LanguageNames.VisualBasic, markup, updatedSignature:=permutation, expectedUpdatedInvocationDocumentCode:=updatedCode)
End Function
End Class
End Namespace
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -42,7 +43,9 @@ internal sealed class CSharpChangeSignatureService : AbstractChangeSignatureServ
SyntaxKind.DelegateDeclaration,
SyntaxKind.SimpleLambdaExpression,
SyntaxKind.ParenthesizedLambdaExpression,
SyntaxKind.LocalFunctionStatement);
SyntaxKind.LocalFunctionStatement,
// TODO: Record structs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to create + link a work item here so we can track adding record struct support in the future?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@allisonchou It should fall into the existing test plan issue for record structs (#51199). Tagging @jcouv to edit the issue

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have record structs now right? so can we just add that in this PR?

SyntaxKind.RecordDeclaration);

private static readonly ImmutableArray<SyntaxKind> _declarationAndInvocableKinds =
_declarationKinds.Concat(ImmutableArray.Create(
Expand Down Expand Up @@ -85,7 +88,9 @@ internal sealed class CSharpChangeSignatureService : AbstractChangeSignatureServ
SyntaxKind.NameMemberCref,
SyntaxKind.AnonymousMethodExpression,
SyntaxKind.ParenthesizedLambdaExpression,
SyntaxKind.SimpleLambdaExpression);
SyntaxKind.SimpleLambdaExpression,
// TODO: record structs
SyntaxKind.RecordDeclaration);

[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
Expand Down Expand Up @@ -265,12 +270,13 @@ public override async Task<SyntaxNode> ChangeSignatureAsync(
CancellationToken cancellationToken)
{
var updatedNode = potentiallyUpdatedNode as CSharpSyntaxNode;

allisonchou marked this conversation as resolved.
Show resolved Hide resolved
// Update <param> tags.
// TODO: Record structs
if (updatedNode.IsKind(SyntaxKind.MethodDeclaration) ||
updatedNode.IsKind(SyntaxKind.ConstructorDeclaration) ||
updatedNode.IsKind(SyntaxKind.IndexerDeclaration) ||
updatedNode.IsKind(SyntaxKind.DelegateDeclaration))
updatedNode.IsKind(SyntaxKind.DelegateDeclaration) ||
updatedNode.IsKind(SyntaxKind.RecordDeclaration))
{
var updatedLeadingTrivia = UpdateParamTagsInLeadingTrivia(document, updatedNode, declarationSymbol, signaturePermutation);
if (updatedLeadingTrivia != default && !updatedLeadingTrivia.IsEmpty)
Expand All @@ -286,6 +292,13 @@ public override async Task<SyntaxNode> ChangeSignatureAsync(
return method.WithParameterList(method.ParameterList.WithParameters(updatedParameters).WithAdditionalAnnotations(changeSignatureFormattingAnnotation));
}

// TODO: Record structs
if (updatedNode.IsKind(SyntaxKind.RecordDeclaration, out RecordDeclarationSyntax? record) && record.ParameterList is not null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just don't do a kind check, and do a syntax-check, and you'll be all good here.

Suggested change
if (updatedNode.IsKind(SyntaxKind.RecordDeclaration, out RecordDeclarationSyntax? record) && record.ParameterList is not null)
if (updatedNode is RecordDeclarationSyntax { ParameterList: not null })

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

@CyrusNajmabadi Nullable analysis doesn't seem to handle patterns correctly.

{
var updatedParameters = UpdateDeclaration(record.ParameterList.Parameters, signaturePermutation, CreateNewParameterSyntax);
return record.WithParameterList(record.ParameterList.WithParameters(updatedParameters).WithAdditionalAnnotations(changeSignatureFormattingAnnotation));
}

if (updatedNode.IsKind(SyntaxKind.LocalFunctionStatement, out LocalFunctionStatementSyntax? localFunction))
{
var updatedParameters = UpdateDeclaration(localFunction.ParameterList.Parameters, signaturePermutation, CreateNewParameterSyntax);
Expand Down Expand Up @@ -745,13 +758,14 @@ private ImmutableArray<SyntaxTrivia> UpdateParamTagsInLeadingTrivia(Document doc
return GetPermutedDocCommentTrivia(document, node, permutedParamNodes);
}

private static ImmutableArray<SyntaxNode> VerifyAndPermuteParamNodes(IEnumerable<XmlElementSyntax> paramNodes, ISymbol declarationSymbol, SignatureChange updatedSignature)
private ImmutableArray<SyntaxNode> VerifyAndPermuteParamNodes(IEnumerable<XmlElementSyntax> paramNodes, ISymbol declarationSymbol, SignatureChange updatedSignature)
{
// Only reorder if count and order match originally.
var originalParameters = updatedSignature.OriginalConfiguration.ToListOfParameters();
var reorderedParameters = updatedSignature.UpdatedConfiguration.ToListOfParameters();

var declaredParameters = declarationSymbol.GetParameters();
var declaredParameters = GetParameters(declarationSymbol);

if (paramNodes.Count() != declaredParameters.Length)
{
return ImmutableArray<SyntaxNode>.Empty;
Expand Down Expand Up @@ -875,5 +889,35 @@ protected override bool SupportsOptionalAndParamsArrayParametersSimultaneously()

protected override SyntaxToken CommaTokenWithElasticSpace()
=> Token(SyntaxKind.CommaToken).WithTrailingTrivia(ElasticSpace);

protected override bool TryGetRecordPrimaryConstructor(INamedTypeSymbol typeSymbol, [NotNullWhen(true)] out IMethodSymbol? primaryConstructor)
{
Debug.Assert(typeSymbol.IsRecord);
primaryConstructor = typeSymbol.InstanceConstructors.FirstOrDefault(
c => c.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is RecordDeclarationSyntax);
return primaryConstructor is not null;
}

protected override ImmutableArray<IParameterSymbol> GetParameters(ISymbol declarationSymbol)
{
var declaredParameters = declarationSymbol.GetParameters();
allisonchou marked this conversation as resolved.
Show resolved Hide resolved
if (declarationSymbol is INamedTypeSymbol { IsRecord: true } recordSymbol)
{
Debug.Assert(declaredParameters.IsDefaultOrEmpty, "If GetParameters extension handles record, we can remove the handling here.");
// A bit hacky to determine the parameters of primary constructor associated with a given record.
// Simplifying is tracked by: https://github.com/dotnet/roslyn/issues/53092.
// Note: When the issue is handled, we can remove the logic here and handle things in GetParameters. BUT
allisonchou marked this conversation as resolved.
Show resolved Hide resolved
// make sure none of the other callers doesn't need records to be handled.
allisonchou marked this conversation as resolved.
Show resolved Hide resolved
var primaryConstructor = recordSymbol.InstanceConstructors.FirstOrDefault(
c => c.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is RecordDeclarationSyntax);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider extracting helper for this (you do the same work higher up).


if (primaryConstructor is not null)
{
declaredParameters = primaryConstructor.Parameters;
}
}

return declaredParameters;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -365,6 +366,28 @@ private string GetAttributeValue(XmlAttributeSyntax attribute)
}
}

protected override ImmutableArray<IParameterSymbol> GetParameters(ISymbol declarationSymbol)
{
var declaredParameters = declarationSymbol.GetParameters();
allisonchou marked this conversation as resolved.
Show resolved Hide resolved
if (declarationSymbol is INamedTypeSymbol { IsRecord: true } recordSymbol)
{
Debug.Assert(declaredParameters.IsDefaultOrEmpty, "If GetParameters extension handles record, we can remove the handling here.");
// A bit hacky to determine the parameters of primary constructor associated with a given record.
// Simplifying is tracked by: https://github.com/dotnet/roslyn/issues/53092.
// Note: When the issue is handled, we can remove the logic here and handle things in GetParameters. BUT
// make sure none of the other callers doesn't need records to be handled.
var primaryConstructor = recordSymbol.InstanceConstructors.FirstOrDefault(
c => c.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is RecordDeclarationSyntax);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

used again. consider an extension now.


if (primaryConstructor is not null)
{
declaredParameters = primaryConstructor.Parameters;
}
}

return declaredParameters;
}

private static readonly CompletionItemRules s_defaultRules =
CompletionItemRules.Create(
filterCharacterRules: FilterRules,
Expand Down
Loading