Fix declaration name completion when local functions present.#35914
Fix declaration name completion when local functions present.#35914dibarbet merged 4 commits intodotnet:masterfrom
Conversation
There was a problem hiding this comment.
this all seems like something that GenerateUniqueName should do. That's basically teh point of it. You say "here's the name I'd like, and i'm in this location" and it contains all the smarts.
We'd only want the logic here if it would only apply to this feature.
There was a problem hiding this comment.
Generate unique name does something similar, the main difference being it doesn't look at local functions.
GenerateUniqueName is used for both VB and C#, so I can't directly move this check into there since as far as I know VB doesn't have local functions.
I can have the logic in the declaration name completion provider (as it is), or I could potentially pass another filter to GenerateUniqueName and pass it along to semanticModel.GetExistingSymbols(...)
@CyrusNajmabadi any thoughts on which is better?
There was a problem hiding this comment.
GenerateUniqueName is used for both VB and C#, so I can't directly move this check into there since as far as I know VB doesn't have local functions.
You def can update it. it's already in an inheritance hierarchy in case there's some VB or C# specific behavior that is needed.
There was a problem hiding this comment.
Also... this logic doesn't seem correct to me... you're filteirng out local functions. But it's certainly the case that a local function has a name, and you shouldn't introduce a variable with the same name as that local function.
There was a problem hiding this comment.
looking at the bug you're trying to fix, the root cause is that we're taking the declared symbols for the parameters and treating it as if that means those names are rserved in other scopes. that's really not how local function parameters work. First, it's acceptable for there to be a collision with them and outer variables (i.e. local function parameters were cahnged to hide outer variables). Second, local function params are only in scope inside the local funciton, not in the containing block (like how other symbols work). We really need to fixup GenerateUniqueName (or whatever helper it is using) to understand this.
There was a problem hiding this comment.
GenerateUniqueName is used for both VB and C#, so I can't directly move this check into there since as far as I know VB doesn't have local functions.
You def can update it. it's already in an inheritance hierarchy in case there's some VB or C# specific behavior that is needed.
Hmm it looks possible, I'll have to mess around with it a bit. Both the the C# and VB implementation delegate to the same implementation in AbstractSemanticFactsService
To get C# specific behavior, I'll need to make the methods AbstractSemanticFactsService virtual and then override them in the C# implementation. I'll try that out.
There was a problem hiding this comment.
Note: once you do, please do a FAR on GenerateUniqueName to find the feautres that use it. Then try to write at least one test per feature that uses local-functions/parameters in a sensible manner. It will help ensure that we haven't regressed anything for those features (and perhaps will show that we've fixed a bug for them where they used to do things incorrectly).
cbacb18 to
b4be2dc
Compare
b4be2dc to
64be2a3
Compare
|
@CyrusNajmabadi I refactored a tiny bit and added the check to the csharp generate unique name method. I additionally added tests that should address the concern around local function method names and other references to generate unique name. |
| var rules = await document.GetNamingRulesAsync(FallbackNamingRules.CompletionOfferingRules, cancellationToken).ConfigureAwait(false); | ||
| var result = new Dictionary<string, SymbolKind>(); | ||
| var semanticFactsService = context.GetLanguageService<ISemanticFactsService>(); | ||
| var syntaxFactsService = context.GetLanguageService<ISyntaxFactsService>(); |
| var uniqueName = semanticFactsService.GenerateUniqueName( | ||
| context.SemanticModel, | ||
| context.TargetToken.Parent, | ||
| targetToken.Parent, |
There was a problem hiding this comment.
can be reverted? (up to you).
| // b) Local function symbols are only in scope inside the local function. | ||
| // Relevant ones (e.g. method name) would be returned by a). | ||
| var symbolsInBlock = semanticModel.GetExistingSymbols(container, cancellationToken, | ||
| filter: (SyntaxNode n) => !n.IsKind(SyntaxKind.LocalFunctionStatement)); |
There was a problem hiding this comment.
odd indentation. consider just doing +4 off of the var
There was a problem hiding this comment.
i haven't read the rest of this, but 'filter' reads weird. is this supposed to mean somethnig more like 'descendsInto'.
| { | ||
| GetAllDeclaredSymbols(semanticModel, child.AsNode(), symbols, cancellationToken); | ||
| var childNode = child.AsNode(); | ||
| if (ApplyFilter(childNode, filter)) |
There was a problem hiding this comment.
ah, based on thsi, we should really call the variable descendInto. I would also update the comment on the first file to say that we basically don't want to dive into a local function as they are not affected by outer variables, and do not contribute to the outer scope as well.
There was a problem hiding this comment.
Yep, I couldn't think of a better name at the time, but descendInto is good. Will update!
| // Walk through the enclosing block to find them, but avoid exploring local functions because | ||
| // a) Visible local function symbols would be returned from LookupSymbols | ||
| // (e.g. location is inside a local function or the local function method name). | ||
| // b) Local function symbols are not affected by outer variables and do not contribute to the outer scope. |
There was a problem hiding this comment.
i think this could be worded slightly better. when you say "local function symbols" it sounds like you're talking about the local function itself. I think we should be clearer and say that "symbols declared inside a local function do not cause collisions with symbols declared outside teh local function. And so, should not be considered here.
Note: GetUsedSymbols is not a great name either. it should be something like GetCollidableSymbols since htat's what we're really asking for her.e We should also doc teh GetCollidableSymbls method to state what the intent of it is. That will help ensure that we have a clear path going forward in the future for how to fix it up if we get something wrong (or as the langauge changes).
CyrusNajmabadi
left a comment
There was a problem hiding this comment.
Have some tiny doc/naming suggestions. but otherwise things LGTM!
Approve
Submit feedback approving these changes.
| // (e.g. location is inside a local function or the local function method name). | ||
| // b) Symbols declared inside a local function do not cause collisions with symbols declared outside the local function, so avoid considering them. | ||
| var symbolsInBlock = semanticModel.GetExistingSymbols(container, cancellationToken, | ||
| descendInto: n => !n.IsKind(SyntaxKind.LocalFunctionStatement)); |
There was a problem hiding this comment.
How about lambda functions?
There was a problem hiding this comment.
Yeah, it looks like those don't collide either. So we should do the same. Note: i just realized this collision logic depends on language version. So we need to actually check the lang version and only do this if allowed.
There was a problem hiding this comment.
How about lambda functions?
THis should be handled here - http://source.roslyn.io/#Microsoft.CodeAnalysis.Workspaces/Shared/Extensions/SemanticModelExtensions.cs,240 but I am working on writing a test for it. Encountering issues with have a lambda next to a markup span in test files...
There was a problem hiding this comment.
Yeah, it looks like those don't collide either. So we should do the same. Note: i just realized this collision logic depends on language version. So we need to actually check the lang version and only do this if allowed.
I'm don't quite get this - do you mean that local functions only apply to C# 7? Wouldn't that automatically be handled because we'd never see the SyntaxKind.LocalFunctionStatement?
There was a problem hiding this comment.
Feel free to hit me up on gitter, but allow me to explain:
For the purposes of explanation i'm going to make up a version number (since i don't actually know when things cahnged). So, in C# 6.0 this was illegal:
int i;
Action a = () => { int i; }This causes a collision between the two int i variables that hte language explicitly disallows. At some point (maybe 7.0, 7.1, 7.2, 7.3 or 8.0, i honestly don't know) we decided to relax this and state that there would be no collision here.
However, this helper function is being used to say "what are the symbols i woud collide with" so that features can decide what names are available. So, if you have this code:
$$ // <-- location of interest
Action a = () => { int i; }then in C# 6.0 we need to say "int i would collide here, so you can't use i". But in c# 7.0 we would say that "int i is not in scope here, so you can use i".
In effect, the decision we make (and what the APi returns) is language version dependent. The rules literally changed between versions, and that impacts what features are allowed to do.
Let me know if you need more info. Cheers!
There was a problem hiding this comment.
Ahhh, I didn't realize that the first case used to be illegal. So yup it makes sense that we only want to exclude lambda functions based on the language version. I'll go ahead and figure out when that happened and update the PR.
That was a perfect explanation, thanks!
There was a problem hiding this comment.
Glad to help! Note: to make your life easier, we have an IsAnonymousOrLocalFunction helper that you can use for this purpose.
There was a problem hiding this comment.
According to my testing on VS, it would be C# 8 that relaxes those restrictions.
You can just reference |
Yep, I just realized that as soon as I posted my comment, so I deleted it. I'm just blind. |
👓 . Happens all the time. I swear i've gone looking for functions or code that were literally right on the original screen i started on :D |
…is greater than or equal to c# 8.
Resolves #35891