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

Enhance explicit interface implementation completion provider #75568

Open
wants to merge 35 commits into
base: main
Choose a base branch
from

Conversation

Rekkonnect
Copy link
Contributor

@Rekkonnect Rekkonnect commented Oct 21, 2024

Closes #75435
Closes #72224 -- this was working but the test had a different expected outcome, this PR unskips the test
Closes #70458

Implementation details

The ExplicitInterfaceMemberCompletionProvider is now up to par with the OverrideCompletionProvider, with some abstractions to share between the two like the ItemGetter. The characters [ and < no longer commit the selected completion item since the completion provider now fills the implementation body and the signature completely.

The AbstractMemberInsertingCompletionProvider itself also became faster by doing less work when replacing the completed text in the original document via annotations. This behavioral adjustment was also necessary because of problems that arose while changing the ExplicitInterfaceMemberCompletionProvider to be an AbstractMemberInsertingCompletionProvider.

Another fixed bug is the explicit interface implementation generator for abstract conversion operators, which were not handled in the "Implement interface explicitly" code fixes. This was resolved for both the fixes and the completion provider.

When the user is typing out an explicit interface declaration member, the provider ignores the potential difference in the return type. This is justified as the user could easily mistake the return type, and so we do not want to filter out the actual candidates that can be offered. Besides, in most cases the recommended members are too few to need this filtering. If the user types out a return type and then we complete a member that has a different return type, the result includes the actual signature of the implemented member, discarding the old return type.

The spell fix suggestions for explicit interface implementation members were removed because of the complexity of handling the generated code. The justification is that it's easier for the user to manually type the member out because of the improvements to the completion provider.

The symbol generators no longer ignore the attributes on the parameters of explicit interface implementations, since they are legal to use in that context.

Tests

@Rekkonnect Rekkonnect requested a review from a team as a code owner October 21, 2024 09:25
@dotnet-issue-labeler dotnet-issue-labeler bot added Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead labels Oct 21, 2024
@dotnet-policy-service dotnet-policy-service bot added Community The pull request was submitted by a contributor who is not a Microsoft employee. VSCode labels Oct 21, 2024
{
throw new System.NotImplementedException();
}
}
""";
// TODO: Consider adding the default value too.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since default values are meaningless in explicit interface implementations, showing them in the completion item is also unimportant. Removed this comment to reflect the sanity of the current case.

var implementedMembers = await generator.GenerateExplicitlyImplementedMembersAsync(member, options.PropertyGenerationBehavior, cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();

var singleImplemented = implementedMembers[0]!;
Copy link
Member

Choose a reason for hiding this comment

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

why the !

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removing the ! and asserting not null as a contract

if (implementedMember is IPropertySymbol implementedProperty)
{
commonContainer ??= implementedProperty;
Debug.Assert(commonContainer == implementedProperty, "We should have a common property implemented");
Copy link
Member

Choose a reason for hiding this comment

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

make these intro contract calls.

"""
await new VerifyCS.Test
{
TestCode = """
using System;
Copy link
Member

Choose a reason for hiding this comment

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

i woudl indent these strings.

Options = { AllOptionsOff },

CodeActionEquivalenceKey = "True;False;False:global::IGoo;Microsoft.CodeAnalysis.ImplementInterface.AbstractImplementInterfaceService+ImplementInterfaceCodeAction;",
// 🐛 one value is generated with 0L instead of 0
Copy link
Member

Choose a reason for hiding this comment

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

not sure what this means.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See also TestIntegralAndFloatLiterals; when specifying DateTimeConstant(100), the semantic structure equivalence check does not forgive the mismatching literal types, despite the available conversion. Removing the CodeActionValidationMode causes the test to fail saying that "100" does not equal "100", where the one is int and the other is long. Apparently the generated argument is perceived as being int when the attribute's argument expects a long. I have not dug up the real cause for this problem, so I just copied the note from the other test to indicate the problem.

@@ -7622,6 +7629,29 @@ public List<UU> M<TT, UU>(Dictionary<TT, List<UU>> a, TT b, UU c) where UU : TT
[Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/994328")]
public async Task TestDisposePatternWhenAdditionalUsingsAreIntroduced2()
{
const string equatableParameter =
#if NET9_0_OR_GREATER
"[AllowNull] int other"
Copy link
Member

Choose a reason for hiding this comment

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

this seems... undesirable. why have AllowNull on something known to be a value type?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll crosscheck with the explicit interface implementation refactoring to see whether this is the existing behavior

Copy link
Member

Choose a reason for hiding this comment

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

waiting on this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Check the test GenericInterfaceNotNull1 in ImplementInterfaceTests. It generates [AllowNull] int bar, with the type of the parameter in the interface method being a generic type parameter, which is a similar case to this. I don't think it's that big of an issue.

else
{
Contract.ThrowIfNull(implementedMember);
var containingProperty = implementedMember!.ContainingSymbol as IPropertySymbol;
Copy link
Member

Choose a reason for hiding this comment

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

why the !, you just threw if it was null.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removing the !

switch (caretTarget)
{
case EventFieldDeclarationSyntax:
return caretTarget.GetLocation().SourceSpan.End;
Copy link
Member

Choose a reason for hiding this comment

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

to avoid all this repetition, just have tehse helpers return the node you want to move to the end to. The caller can do the .GetLocation().SourceSpan.End bit.

case BasePropertyDeclarationSyntax propertyDeclaration:
{
// property: no accessors; move to the end of the declaration
if (propertyDeclaration.AccessorList != null && propertyDeclaration.AccessorList.Accessors.Any())
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (propertyDeclaration.AccessorList != null && propertyDeclaration.AccessorList.Accessors.Any())
if (propertyDeclaration.AccessorList is { Accessors.Count: > 0 })

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (propertyDeclaration.AccessorList != null && propertyDeclaration.AccessorList.Accessors.Any())
if (propertyDeclaration.AccessorList is { Accessors: [var firstAccessor, ..] })

using Microsoft.CodeAnalysis.ErrorReporting;
using System.Linq;
using Roslyn.Utilities;
using Microsoft.CodeAnalysis.Editing;
Copy link
Member

Choose a reason for hiding this comment

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

sort.

}

return true;
}
Copy link
Member

Choose a reason for hiding this comment

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

all off these need docs. and docs on 'why' not 'what' :)

Comment on lines 156 to 157
catch (Exception e)
when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General))
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
catch (Exception e)
when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General))
catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, ErrorSeverity.General))

IEventSymbol eventSymbol => ToDisplayString(eventSymbol),
IPropertySymbol propertySymbol => ToDisplayString(propertySymbol, semanticModel),
IMethodSymbol methodSymbol => ToDisplayString(methodSymbol, semanticModel),
_ => "" // This shouldn't happen.
Copy link
Member

Choose a reason for hiding this comment

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

then throw.

{
RefKind.Out => "out ",
RefKind.Ref => "ref ",
RefKind.In => "in ",
Copy link
Member

Choose a reason for hiding this comment

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

what about things like scoped?

var document = context.Document;
var position = context.Position;
var cancellationToken = context.CancellationToken;
var implementInterfaceService = (AbstractImplementInterfaceService)newDocument.GetRequiredLanguageService<IImplementInterfaceService>();
Copy link
Member

Choose a reason for hiding this comment

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

why are you casting to the derived type?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

AbstractImplementInterfaceService.State.Generate requires an AbstractImplementInterfaceService, hence the cast

Copy link
Member

Choose a reason for hiding this comment

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

why are you accessing that here. it's an internal implementation detail. If you need information from it, it should be through the interface.

var semanticModel = await newDocument.GetSemanticModelAsync(cancellationToken)
.ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
Debug.Assert(semanticModel != null);
Copy link
Member

Choose a reason for hiding this comment

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

use GetRequiredSmenaitcModol, remov ethis assert.

var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
var interfaceNode = await GetInterfaceNodeInCompletionAsync(newDocument, completionItem, baseMemberInterfaceType, cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
Debug.Assert(interfaceNode != null);
Copy link
Member

Choose a reason for hiding this comment

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

remove these asserts. either make contracts, or ensure that these paths always succeed, or bail out if you can't do that.

private static async Task<SyntaxNode?> GetInterfaceNodeInCompletionAsync(Document document, CompletionItem item, INamedTypeSymbol baseMemberInterfaceType, CancellationToken cancellationToken)
{
var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
Copy link
Member

Choose a reason for hiding this comment

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

don't do this. here are hte rules on cacncellation tokens:

  1. check them on the entrypoints of methods that take them
  2. check them in the start of loops, if hte loop isn't calling anything that takes teh cancelation token.

cancellationToken.ThrowIfCancellationRequested();
var documentSemanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
Debug.Assert(documentSemanticModel != null, "Expected a semantic model out of the document");
Copy link
Member

Choose a reason for hiding this comment

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

GetRequiredCancellationToken

Copy link
Contributor Author

Choose a reason for hiding this comment

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

GetRequiredSemanticModel?

Debug.Assert(documentSemanticModel != null, "Expected a semantic model out of the document");

var compilation = documentSemanticModel.Compilation;
var node = syntaxTree.FindNode(item.Span, false, true, cancellationToken);
Copy link
Member

Choose a reason for hiding this comment

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

named parameters for false/true.

if (interfaceNode != null)
{
return interfaceNode;
}
Copy link
Member

Choose a reason for hiding this comment

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

prior to this it would be good to doc the general idea of what's going on. what you're looking for and what you expect the shape of the tree to be.

@CyrusNajmabadi
Copy link
Member

Done with pass. Also, you have a file with a conflict in it. please bring this PR up to date with main. Thanks :)

@Rekkonnect
Copy link
Contributor Author

@CyrusNajmabadi ready for review again

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-IDE Community The pull request was submitted by a contributor who is not a Microsoft employee. untriaged Issues and PRs which have not yet been triaged by a lead VSCode
Projects
None yet
2 participants