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

Completion for unimported extension methods #39126

Merged
merged 42 commits into from
Nov 6, 2019
Merged

Conversation

genlu
Copy link
Member

@genlu genlu commented Oct 8, 2019

#35059

TODO:

  • Finalize the experience when indices are not completely available (e.g. show a banner in completion list saying still loading stuff?)
  • More tests for VB
  • Perf analysis.
  • Needs to Call into remote process for filtering, since that's where index is built.
  • Implement item description
  • Determine the bail condition (e.g. if any of the filter items isn't calculated? And/Or a timebox? ) now it bails if any of the required filter is not already created. Using expander would force build them.
  • Telemetry

There's an existing bug in SymbolTreeInfo that would cause us to miss extension methods declared in PE references which was compiled from VB source code. #39558

@CyrusNajmabadi Could you please give this a quick look and let me know if the implementation of the filter makes sense? Thanks!

@genlu genlu added the Area-IDE label Oct 8, 2019
@genlu genlu requested a review from a team as a code owner October 8, 2019 00:30
@CyrusNajmabadi
Copy link
Member

happy to look. but perf analysis is def necessary.


completionContext.AddItems(items);

telemetryCounter.TotalExtensionMethodsProvided = items.Count;
Copy link
Member

Choose a reason for hiding this comment

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

todo: review this.

return;
}

completionContext.ExpandItemsAvailable = true;
Copy link
Member

Choose a reason for hiding this comment

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

what's this?

Copy link
Member

Choose a reason for hiding this comment

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

why do you need to do this if you already exported the member that said you were IsExpandItemProvider ? I don't get the purpose of having both flags.

Copy link
Member Author

Choose a reason for hiding this comment

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

There are two possible cases if expand provider returns nothing, first is there's actually nothing, another is we might be able to return something but didn't (e.g. timed out). We want to grey out the expander in the first case but show it as selectable in the latter.

Copy link
Member

Choose a reason for hiding this comment

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

can you doc this? i honestly find it very confusing still. i would prefer ths all be unified somehow .

Copy link
Member Author

Choose a reason for hiding this comment

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

There's already doc for ExpandItemsAvailable property


public ParameterTypeInfo GetArrayType(ParameterTypeInfo elementType, ArrayShape shape) => ComplexInfo;

public ParameterTypeInfo GetSZArrayType(ParameterTypeInfo elementType) => ComplexInfo;
Copy link
Member

Choose a reason for hiding this comment

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

doc most of the code here.

// categories for filtering purpose. Whether a method is simple is determined based on if we can determine
// it's target type easily with a pure text matching. For complex methods, we will need to rely on symbol
// to decide if it's feasible.
// All primitive types, types in regular use form are considered simple.
Copy link
Member

Choose a reason for hiding this comment

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

"types in regular use form"?

Copy link
Member

Choose a reason for hiding this comment

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

please add exmaples/explanations.

@@ -407,10 +422,18 @@ private void GenerateMetadataNodes()
// we just pull in methods that have attributes on them.
Copy link
Member

Choose a reason for hiding this comment

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

i wonder if we can do better. at least see if hte attribute is called "Extension".

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure can do, it's just unclear how much benefit it would provide

Copy link
Member

Choose a reason for hiding this comment

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

it would probably greatly decrease the number of methods we pull in.

Copy link
Member

Choose a reason for hiding this comment

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

this still seems worth looking into IMO.

Copy link
Member Author

Choose a reason for hiding this comment

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

this doesn't seem to cause any issues during my test in roslyn.sln. Will file a bug and consider this in a follow-up PR

var info = await document.GetSyntaxTreeIndexAsync(loadOnly, cancellationToken).ConfigureAwait(false);
if (info == null)
{
return null;
Copy link
Member

Choose a reason for hiding this comment

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

please doc that this is an intentional decision, and you didn't accidentally forget to 'continue' here.

{
if (s_indexingTask.IsCompleted)
{
s_indexingTask = Task.Run(() => GetPossibleExtensionMethodMatchesAsync(project, allTypeNames, forceIndexCreation: false, isPrecalculation: true, CancellationToken.None));
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 we passing forceIndexCreation: false here?

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

// Don't provide anything if we don't have all the required SyntaxTreeIndex created.
if (cacheEntry == null)
{
return null;
Copy link
Member

Choose a reason for hiding this comment

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

doc this.

}
}

// We are just trying to populate the cache in background, no need to return any results.
Copy link
Member

Choose a reason for hiding this comment

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

ew. this is not pretty. a better way would be to extract out the cache computation logic and then call into that from both locations rather than having the precalculation logic cause it to happen (but hten skip some work) by calling into the actual query codepath.

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

cancellationToken.ThrowIfCancellationRequested();

var indexOfLastDot = fullyQualifiedContainerName.LastIndexOf('.');
var qualifiedNamespaceName = indexOfLastDot > 0 ? fullyQualifiedContainerName.Substring(0, indexOfLastDot) : string.Empty;
Copy link
Member

Choose a reason for hiding this comment

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

that's a pity. so we force a string-alloc on every item found? is this ok because the expetation is that the filter will hav ea small number of results?

Copy link
Member Author

Choose a reason for hiding this comment

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

yes, it's on every containing type we found. And yes, with filter, this number should be fairly small so it's unlikely to cause perf issues.

IMethodSymbol? reducedMethodSymbol = null;

if (methodSymbol.IsExtensionMethod &&
(methodNames.Count == 0 || methodNames.Contains(methodSymbol.Name)) &&
Copy link
Member

Choose a reason for hiding this comment

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

i don't understand teh methodNames.Count == 0 check. if we had zero method names, how would we get into the outer loop in the first place. I also don't understand the inner check. We only asked for methods with this speicfic name (from methodNames), so how would methodNames not contain the method name??

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch. It was a leftover from previous logic. Removed!


var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var items = GetExtensionMethodItems(assembly.GlobalNamespace, receiverTypeSymbol,
semanticModel!, position, namespaceInScope, matchedMethods, counter, cancellationToken);
Copy link
Member

Choose a reason for hiding this comment

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

i don't see how this could work. here's the issue:

  1. you looked at all dependent projects to add their pe references and get the indices for them.
  2. you're now walking your particular assembly.GlobalNamespace symbol actually looking for the extension methods.

This would not find any extension methods for pe references that this compilation doesn't itself point at.

So basically, i just don't get how this coudl work.

Copy link
Member Author

Choose a reason for hiding this comment

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

As discussed offline, transitive PE reference isn't support in C#/VB, so this is simply a mistake and I will remove related code.


/// <summary>
/// The purpose of this is to help us keeping track of conflicting types with data required to create corresponding
/// items in a tree, which is easy to use while navigating symbol tree recursively.
Copy link
Member

Choose a reason for hiding this comment

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

would be good to give an example of what counts as a 'conflict'.

Copy link
Member Author

Choose a reason for hiding this comment

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

done

/// The purpose of this is to help us keeping track of conflicting types with data required to create corresponding
/// items in a tree, which is easy to use while navigating symbol tree recursively.
/// </summary>
private class ConflictNameNode : IDisposable
Copy link
Member

Choose a reason for hiding this comment

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

i'm not really sure how conflicts arise. your indices already are associated with source or pe-references. so you can triviall walk the corresponding IAssembly for each to find your matches, and you should never run into a conflict in these specific iassembly symbols.

}

// Otherwise, this is a type item, so we don't have SymbolKey data. But we should still have all
// the data to construct its full metadata name
Copy link
Member

Choose a reason for hiding this comment

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

dumb question. why not just have the TypeItem do the same thing the extension method item does? then you can operate on both consistently.

Copy link
Member Author

Choose a reason for hiding this comment

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

Because after the cached items are created, type items don't operate on symbols at all (except for getting description), holding on to those SymbolKey data in cache seems wasteful. Whereas, we need symbols to provide extension method items every time.


namespace Microsoft.CodeAnalysis.Completion.Providers
{
internal sealed class SerializableImportCompletionItem
Copy link
Member

Choose a reason for hiding this comment

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

can these be structs to save on allocations?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure. Fixed

return TryGetSimpleTypeName(aliasQualifiedNameNode.Name, typeParameterNames, out simpleTypeName);

case QualifiedNameSyntax qualifiedNameNode:
// For an identifier to the right of a '.', it can't be a type parameter,
Copy link
Member

Choose a reason for hiding this comment

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

... that can't happen. :)

}
}

private static readonly Func<KeyValuePair<string, ArrayBuilder<int>>, string> s_getKey = kvp => kvp.Key;
private static readonly Func<KeyValuePair<string, ArrayBuilder<int>>, ImmutableArray<int>> s_getValuesAsImmutableArray = kvp => kvp.Value.ToImmutableAndFree();
Copy link
Member

Choose a reason for hiding this comment

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

this second one is a bit terrifying.

Copy link
Member Author

Choose a reason for hiding this comment

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

Moved free part to finally

/// In VB, we have a member access expression with a null expression, this may be one of the
/// following forms:
/// 1) new With { .a = 1, .b = .a .a refers to the anonymous type
/// 2) With obj : .m .m refers to the obj type
Copy link
Member

Choose a reason for hiding this comment

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

do you test thsi for extension method import?

Copy link
Member Author

Choose a reason for hiding this comment

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

I have made the change so we don't support this scenario, otherwise we'd need to check whether it's on LHS or RHS, would be fairly simple, but given how rare this is, I'd prefer to avoid the extra complexity here.

Copy link
Member

Choose a reason for hiding this comment

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

let me rephrase :) can we have a test to make sure we don't crash here :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, we have a test for this

@CyrusNajmabadi
Copy link
Member

overall looking good. however last pass did raise at least 2-3 serious concerns on my part. either things are broken based on my reading, or i'm not understanding something here. def need clarity in those areas. thanks!

@genlu
Copy link
Member Author

genlu commented Nov 6, 2019

I got verbal approval from @CyrusNajmabadi via gitter discussion. I will try the change suggested here in a separate PR and see if it can benefit perf.

@genlu genlu merged commit 2eb6c14 into dotnet:master Nov 6, 2019
@genlu genlu deleted the extCompletion branch November 6, 2019 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants