-
Notifications
You must be signed in to change notification settings - Fork 4k
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 feature to strike out obsolete symbols #72156
Conversation
Could you verify the vscode case? it would be good to make sure it's working there (at least visually) :) |
I have no idea how to do this. |
else if (classificationType == ClassificationTypeNames.ObsoleteSymbol) | ||
{ | ||
// 6. Token modifiers - each set bit will be looked up in SemanticTokensLegend.tokenModifiers | ||
modifierBits |= TokenModifiers.ObsoleteSymbol; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we have lsp specific tests to validate the data is at least what we expect?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have TestCompletionForObsoleteSymbol added in this PR, which may cover what you are looking for?
|
||
internal abstract class AbstractObsoleteSymbolService(int? dimKeywordKind) : IObsoleteSymbolService | ||
{ | ||
private readonly int? _dimKeywordKind = dimKeywordKind; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is odd enough that i'd like docs :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ Now documented
var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); | ||
var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); | ||
|
||
using var _2 = ArrayBuilder<TextSpan>.GetInstance(out var result); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: obsolete is likely so rare that usign TempArray is likely ok :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ Kept ArrayBuilder<T>
so I didn't need to reimplement RemoveDuplicates
, but rewrote the code to avoid taking the builder from the pool in the overwhelming majority case where no obsolete symbols are used in the classification region.
{ | ||
if (trivia.HasStructure) | ||
{ | ||
stack.Add(trivia.GetStructure()!); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
interesting. we classify with strikethrough in doc comments? if so, nifty. are there tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if so, nifty. are there tests?
➡️ Yes, and yes (included in TestDeclarationAndUseOfObsoleteAlias
and TestGenerics
for both languages).
} | ||
} | ||
|
||
foreach (var trivia in token.TrailingTrivia) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit. extract helper fort ehse loops. so you can just call it both with Leading/TrailingTrivia
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ Extracted helper
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you verify the vscode case? it would be good to make sure it's working there (at least visually) :)
I have no idea how to do this.
@sharwell see https://github.com/dotnet/vscode-csharp/blob/main/CONTRIBUTING.md#configuring-roslyn-language-server, should be as simple as setting dotnet.server.path
in vscode to the locally built roslyn server in artifacts/bin, then reloading the window.
Since I already have it, I tested this out. Its struck through in completion, but not in classification. I commented on the semantic tokens code for why its not working, but either it needs to use the deprecated
modifier from the LSP spec, or we have to add custom client side configuration to handle the ObsoleteSymbol
modifier.
@@ -16,5 +16,6 @@ internal enum TokenModifiers | |||
None = 0, | |||
Static = 1, | |||
ReassignedVariable = 2, | |||
ObsoleteSymbol = 4, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't appear to be a known LSP modifier - https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#semanticTokenModifiers
This probably needs to use the existing token modifier (deprecated
), or we have to add custom client side support for the modifier
See https://code.visualstudio.com/api/language-extensions/semantic-highlight-guide#custom-token-types-and-modifiers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ Now updated to use the deprecated
modifier
result.Add(aliasToken.Span); | ||
} | ||
|
||
return aliasToken; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cleaner (i think) is that you should be able to do GetDeclaredSymbol on the node to get the IAliasSymbol, and then just check the Target of that symbol. this def should work for C#... and should be good for VB i think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't quite sure what the suggestion here was.
if (!creationKeyword.Span.IsEmpty) | ||
{ | ||
var symbol = semanticModel.GetSymbolInfo(node, cancellationToken).Symbol; | ||
if (IsSymbolObsolete(symbol) || IsSymbolObsolete(symbol?.ContainingType)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe doc. up to you. but i do think it would be good to potentially say "the symbol should be constructor symbol for the type, but we want to strike it out if either the constructor or the type is obsolete".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ Docs added, and slightly tweaked the behavior
protected static bool IsSymbolObsolete([NotNullWhen(true)] ISymbol? symbol) | ||
{ | ||
// Avoid infinite recursion. Iteration limit chosen arbitrarily; cases are generally expected to complete on | ||
// the first iteration or fail completely. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit. i do not think this is actually possible. first, an alias can't target other aliases. so in the alias case, we unwrap immediately to namespace or type. and, for named types, the compiler never generates infinite types. importantly, many many many other 'symbol walkers' would outright break in many other places if that happened.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ I'm not worried about valid code here, but rather for invalid code. e.g. something like a cycle in using aliases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so aliases cannot refer to each other in our symbol model. An alias only every points to a single namespace or type. And neither of those two can ever point to an alias. Aliases are actually kind odd, and really shouldn't necessarily be a symbol in the first place (you can't get them with GetSymbolInfo for example, you can only get them with GetAliasInfo). So you should think of them more like a single pointer that points into the real symbol model, and the real symbol model never points back at them.
So loops in them aren't possible if you follow the above guidelines (like using GetDeclaredSymbol(UsingDirective) to get the alias in the first place, and then only looking at its target.
continue; | ||
} | ||
|
||
if (symbol is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T, TypeArguments: [var valueType] }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit. go whole hog and have a SymbolVisitor for this. I think we should handle things like arrays or tuples for this as well.
Basically, if the type signature (Which is not hard to traverse) has any types that are obsolete, it's fine to mark. I would not just limt this to nullable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about it, but decided against it in the end. I'll make sure there are tests added that indicate that they are simply reflecting current behavior and not necessarily one right or wrong answer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⏱️ Still need to add tests for arrays and tuples
public void GetPartsOfImplicitObjectCreationExpression(SyntaxNode node, out SyntaxToken keyword, out SyntaxNode argumentList, out SyntaxNode? initializer) | ||
{ | ||
var implicitObjectCreationExpression = (ImplicitObjectCreationExpressionSyntax)node; | ||
keyword = implicitObjectCreationExpression.NewKeyword; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit. this is only a c# case, and we know in c# it's always the first token. so could literally just hardcode taht at the use site. But i don't feel strongly. if you want to do it this way, that's fine with me as well :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ I think the use site doesn't have access to C# syntax, so I'll keep this one. To me it worked since it matches the existing GetPartsOfObjectCreationExpression
method.
...edUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb
Show resolved
Hide resolved
Return | ||
End If | ||
|
||
If localDeclaration.Declarators.Count <> 1 Then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit. i don't see why we can't do this per declarator. just striking out the identifier if it has an obsolete type seems like it would work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could, but there are some cases I wasn't sure about:
- If there are more than one declarator, are all declarators required to have the same type?
- If there are more than one item in
Names
, are all locals declared by these names required to have the same type?
Since the overwhelming majority of cases involve one declarator with one name, it seemed like there are diminishing returns to handling the other cases. Happy to add them though if it will be valuable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there are more than one declarator, are all declarators required to have the same type?
No. This is legal:
Dim x As String, y As Integer
If there are more than one item in Names, are all locals declared by these names required to have the same type?
Yes. All would have the same type.
Note: this is why i think it's better to just strikeout the name of the thing, vs the 'dim'.
Since the overwhelming majority of cases involve one declarator with one name, it seemed like there are diminishing returns to handling the other cases. Happy to add them though if it will be valuable.
I don't disagree. You are certainly covering the 99.9% case. But it seemed actually trivial to just walk the declarators, and walk the names, and then strike them out if obsolete.
The major reason i want this, tbh, is that i don't like the special-casing of hte dim
kind and all that jazz. It seems simpler, and 100% encompassing to just hit the variables themselves and mark those.
--
Note: this is a NIT. It was more curiosity and suggestion than anything else. If you want to keep as is, no problem at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ Added TestDeclarators
specifically for this, but did not alter the current behavior
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The major reason i want this, tbh, is that i don't like the special-casing of hte dim kind and all that jazz. It seems simpler, and 100% encompassing to just hit the variables themselves and mark those.
The main reason I kept the current behavior is for parity with var
in C#. Since var
is an identifier token, I didn't need to do anything special for it to work in C#.
overall this is fantastic. only very minor nits and suggestions. |
{ | ||
var preferredTag = completionItemTags[i]; | ||
if (supportedClientTags.Contains(preferredTag)) | ||
lspTag = preferredTag; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't the add be moved here in some way? Unless I'm reading it wrong, given tags on an item of "class", "foo"
and server supported tags of "class", "struct"
, this method would return "class", "class"
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ For the case you describe, this method would return "class", "foo"
. Server-supported tags only influence this method if one Roslyn tag maps to more than one LSP tag. In the current definition of RoslynTagToCompletionItemTags
, there are no such tags. Support for one-to-many mapping with server-defined precedence matches what we were already using for RoslynTagToCompletionItemKinds
.
src/Features/LanguageServer/Protocol/Handler/SemanticTokens/TokenModifiers.cs
Outdated
Show resolved
Hide resolved
@sharwell just tested it out again - mostly works, just needs some work to get the option to work, see this commit for Roslyn - 39006c0 And on the client side it needs to add the option (and configure the strikethrough behavior) - dotnet/vscode-csharp#6915 But I wanted to ask - is there any reason to not default the option value to true? At least on the vscode side there's no real point in having an additional server side option for this, users can already configure strikethrough behavior pretty easily in their settings, e.g.
If the option was defaulted in roslyn to true, we wouldn't need to expose directly it on the vscode side (and just let theme configuration handle it) |
@dibarbet I enabled the feature by default |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LSP side changes lgtm, I'll handle updating our default themes on the vscode-csharp side
This feature should also work for LSP completion in VS Code. Completion inside Visual Studio does not support deprecated indicators, so there is no change to completion in Visual Studio at this time.
Related to #26488