diff --git a/src/EditorFeatures/CSharpTest/Classification/SemanticClassifierTests.cs b/src/EditorFeatures/CSharpTest/Classification/SemanticClassifierTests.cs index a538fb0385c45..c9a0a881e7b99 100644 --- a/src/EditorFeatures/CSharpTest/Classification/SemanticClassifierTests.cs +++ b/src/EditorFeatures/CSharpTest/Classification/SemanticClassifierTests.cs @@ -642,7 +642,8 @@ class C """, testHost, Namespace("System"), - Class("Obsolete")); + Class("Obsolete"), + Obsolete("C")); } [Theory, CombinatorialData] diff --git a/src/EditorFeatures/CSharpTest/ObsoleteSymbol/CSharpObsoleteSymbolTests.cs b/src/EditorFeatures/CSharpTest/ObsoleteSymbol/CSharpObsoleteSymbolTests.cs new file mode 100644 index 0000000000000..52499ead36c21 --- /dev/null +++ b/src/EditorFeatures/CSharpTest/ObsoleteSymbol/CSharpObsoleteSymbolTests.cs @@ -0,0 +1,200 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editor.UnitTests.ObsoleteSymbol; +using Microsoft.CodeAnalysis.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.ObsoleteSymbol; + +public class CSharpObsoleteSymbolTests : AbstractObsoleteSymbolTests +{ + protected override EditorTestWorkspace CreateWorkspace(string markup) + => EditorTestWorkspace.CreateCSharp(markup); + + [Theory] + [InlineData("class")] + [InlineData("struct")] + [InlineData("record")] + [InlineData("record class")] + [InlineData("record struct")] + [InlineData("interface")] + [InlineData("enum")] + public async Task TestObsoleteTypeDefinition(string keyword) + { + await TestAsync( + $$""" + [System.Obsolete] + {{keyword}} [|ObsoleteType|] + { + } + + {{keyword}} NonObsoleteType + { + } + """); + } + + [Fact] + public async Task TestObsoleteDelegateTypeDefinition() + { + await TestAsync( + """ + [System.Obsolete] + delegate void [|ObsoleteType|](); + + delegate void NonObsoleteType(); + """); + } + + [Fact] + public async Task TestDeclarationAndUseOfObsoleteAlias() + { + await TestAsync( + """ + using [|ObsoleteAlias|] = [|ObsoleteType|]; + + [System.Obsolete] + class [|ObsoleteType|]; + + /// + /// + class NonObsoleteType + { + [|ObsoleteAlias|] field = new [|ObsoleteType|](); + } + """); + } + + [Fact] + public async Task TestParametersAndReturnTypes() + { + await TestAsync( + """ + [System.Obsolete] + class [|ObsoleteType|]; + + class NonObsoleteType([|ObsoleteType|] field2) + { + [|ObsoleteType|] Method([|ObsoleteType|] arg) => [|new|](); + + System.Func<[|ObsoleteType|], [|ObsoleteType|]> field = [|ObsoleteType|] ([|ObsoleteType|] arg) => [|new|](); + } + """); + } + + [Fact] + public async Task TestImplicitType() + { + await TestAsync( + """ + [System.Obsolete] + class [|ObsoleteType|] + { + public ObsoleteType() { } + + [System.Obsolete] + public [|ObsoleteType|](int x) { } + } + + class ObsoleteCtor + { + public ObsoleteCtor() { } + + [System.Obsolete] + public [|ObsoleteCtor|](int x) { } + } + + class C + { + void Method() + { + [|var|] t1 = new [|ObsoleteType|](); + [|var|] t2 = [|new|] [|ObsoleteType|](3); + [|ObsoleteType|] t3 = [|new|](); + [|ObsoleteType|] t4 = [|new|](3); + [|var|] t5 = CreateObsoleteType(); + var t6 = nameof([|ObsoleteType|]); + + var u1 = new ObsoleteCtor(); + var u2 = [|new|] ObsoleteCtor(3); + ObsoleteCtor u3 = new(); + ObsoleteCtor u4 = [|new|](3); + var u6 = nameof(ObsoleteCtor); + + [|ObsoleteType|] CreateObsoleteType() => [|new|](); + } + } + """); + } + + [Fact] + public async Task TestExtensionMethods() + { + await TestAsync( + """ + [System.Obsolete] + static class [|ObsoleteType|] + { + public static void ObsoleteMember1(this C ignored) { } + + [System.Obsolete] + public static void [|ObsoleteMember2|](this C ignored) { } + } + + class C + { + void Method() + { + this.ObsoleteMember1(); + this.[|ObsoleteMember2|](); + [|ObsoleteType|].ObsoleteMember1(this); + [|ObsoleteType|].[|ObsoleteMember2|](this); + } + } + """); + } + + [Fact] + public async Task TestGenerics() + { + await TestAsync( + """ + [System.Obsolete] + class [|ObsoleteType|]; + + [System.Obsolete] + struct [|ObsoleteValueType|]; + + class G + { + } + + class C + { + void M() { } + + /// + /// This looks like a reference to an obsolete type, but it's actually just an identifier alias for the + /// generic type parameter 'T'. + /// + /// + void Method() + { + _ = new G<[|ObsoleteType|]>(); + _ = new G>(); + M<[|ObsoleteType|]>(); + M>(); + M>>(); + + // Mark 'var' as obsolete even when it points to Nullable where T is obsolete + [|var|] nullableValue = CreateNullableValueType(); + + [|ObsoleteValueType|]? CreateNullableValueType() => new [|ObsoleteValueType|](); + } + } + """); + } +} diff --git a/src/EditorFeatures/Core.Wpf/Classification/ClassificationTypeFormatDefinitions.cs b/src/EditorFeatures/Core.Wpf/Classification/ClassificationTypeFormatDefinitions.cs index 9775e704ed1cf..654802b6f5c00 100644 --- a/src/EditorFeatures/Core.Wpf/Classification/ClassificationTypeFormatDefinitions.cs +++ b/src/EditorFeatures/Core.Wpf/Classification/ClassificationTypeFormatDefinitions.cs @@ -146,6 +146,25 @@ public ReassignedVariableFormatDefinition() } #endregion + #region Obsolete Symobl + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = ClassificationTypeNames.ObsoleteSymbol)] + [Name(ClassificationTypeNames.ObsoleteSymbol)] + [Order(After = Priority.High)] + [UserVisible(false)] + [ExcludeFromCodeCoverage] + private class ObsoleteSymbolFormatDefinition : ClassificationFormatDefinition + { + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ObsoleteSymbolFormatDefinition() + { + this.DisplayName = EditorFeaturesResources.Obsolete_symbol; + this.TextDecorations = System.Windows.TextDecorations.Strikethrough; + } + } + #endregion + #region Symbol - Static [Export(typeof(EditorFormatDefinition))] [ClassificationType(ClassificationTypeNames = ClassificationTypeNames.StaticSymbol)] diff --git a/src/EditorFeatures/Core/Classification/ClassificationTypeDefinitions.cs b/src/EditorFeatures/Core/Classification/ClassificationTypeDefinitions.cs index f7829ff3920bf..b3e7160423526 100644 --- a/src/EditorFeatures/Core/Classification/ClassificationTypeDefinitions.cs +++ b/src/EditorFeatures/Core/Classification/ClassificationTypeDefinitions.cs @@ -423,6 +423,13 @@ internal sealed class ClassificationTypeDefinitions internal readonly ClassificationTypeDefinition ReassignedVariableTypeDefinition; #endregion + #region Obsolete Symbol + [Export] + [Name(ClassificationTypeNames.ObsoleteSymbol)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal readonly ClassificationTypeDefinition ObsoleteSymbolTypeDefinition; + #endregion + #region Static Symbol [Export] [Name(ClassificationTypeNames.StaticSymbol)] diff --git a/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs b/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs index 88b1ff86d4d79..275ae3569a345 100644 --- a/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs +++ b/src/EditorFeatures/Core/Classification/Semantic/AbstractSemanticOrEmbeddedClassificationViewTaggerProvider.cs @@ -63,7 +63,8 @@ protected sealed override ITaggerEventSource CreateEventSource(ITextView textVie TaggerEventSources.OnViewSpanChanged(ThreadingContext, textView), TaggerEventSources.OnWorkspaceChanged(subjectBuffer, AsyncListener), TaggerEventSources.OnDocumentActiveContextChanged(subjectBuffer), - TaggerEventSources.OnGlobalOptionChanged(_globalOptions, ClassificationOptionsStorage.ClassifyReassignedVariables)); + TaggerEventSources.OnGlobalOptionChanged(_globalOptions, ClassificationOptionsStorage.ClassifyReassignedVariables), + TaggerEventSources.OnGlobalOptionChanged(_globalOptions, ClassificationOptionsStorage.ClassifyObsoleteSymbols)); } protected sealed override Task ProduceTagsAsync( diff --git a/src/EditorFeatures/Core/EditorFeaturesResources.resx b/src/EditorFeatures/Core/EditorFeaturesResources.resx index 6b30388240e00..4ac3a6e33e98d 100644 --- a/src/EditorFeatures/Core/EditorFeaturesResources.resx +++ b/src/EditorFeatures/Core/EditorFeaturesResources.resx @@ -923,4 +923,7 @@ Do you want to proceed? Roslyn Test Code Markup + + Obsolete symbol + \ No newline at end of file diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.cs.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.cs.xlf index 296a70007ea64..056b55a7c22c6 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.cs.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.cs.xlf @@ -202,6 +202,11 @@ Ne + + Obsolete symbol + Obsolete symbol + + Operator - Overloaded Operátor – přetížení diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.de.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.de.xlf index 826dbc3dc0ec4..8d448ee1d8f3a 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.de.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.de.xlf @@ -202,6 +202,11 @@ Nein + + Obsolete symbol + Obsolete symbol + + Operator - Overloaded Operator - überladen diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.es.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.es.xlf index dd538dd8a5c67..077b691c264a0 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.es.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.es.xlf @@ -202,6 +202,11 @@ No + + Obsolete symbol + Obsolete symbol + + Operator - Overloaded Operador: sobrecargado diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.fr.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.fr.xlf index e7a49dbd61378..d636cc43f8042 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.fr.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.fr.xlf @@ -202,6 +202,11 @@ Non + + Obsolete symbol + Obsolete symbol + + Operator - Overloaded Opérateur - surchargé diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.it.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.it.xlf index f1fd95dd838d6..46de62c4f81ad 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.it.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.it.xlf @@ -202,6 +202,11 @@ No + + Obsolete symbol + Obsolete symbol + + Operator - Overloaded Operatore - Overload diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ja.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ja.xlf index 972db8cfab01a..400ab0cfb78f9 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ja.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ja.xlf @@ -202,6 +202,11 @@ いいえ + + Obsolete symbol + Obsolete symbol + + Operator - Overloaded 演算子 - オーバーロード diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ko.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ko.xlf index 205f935e256fa..ade34d7bbd3fe 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ko.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ko.xlf @@ -202,6 +202,11 @@ 아니요 + + Obsolete symbol + Obsolete symbol + + Operator - Overloaded 연산자 - 오버로드됨 diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pl.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pl.xlf index 44844805ab126..375dbb079ce43 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pl.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pl.xlf @@ -202,6 +202,11 @@ Nie + + Obsolete symbol + Obsolete symbol + + Operator - Overloaded Operator — przeciążony diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pt-BR.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pt-BR.xlf index d4abbbcf97c5f..be6bca5e72436 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pt-BR.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pt-BR.xlf @@ -202,6 +202,11 @@ Não + + Obsolete symbol + Obsolete symbol + + Operator - Overloaded Operador – Sobrecarregado diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ru.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ru.xlf index e16baca18ecbe..49cfb8f71c190 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ru.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ru.xlf @@ -202,6 +202,11 @@ Нет + + Obsolete symbol + Obsolete symbol + + Operator - Overloaded Оператор — перегружен diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.tr.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.tr.xlf index da6365927fcd9..0d9ceef26fee7 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.tr.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.tr.xlf @@ -202,6 +202,11 @@ Hayır + + Obsolete symbol + Obsolete symbol + + Operator - Overloaded İşleç - Aşırı Yüklenmiş diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hans.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hans.xlf index 877b5031ed46b..4d094fa12f88b 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hans.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hans.xlf @@ -202,6 +202,11 @@ + + Obsolete symbol + Obsolete symbol + + Operator - Overloaded 运算符-重载 diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hant.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hant.xlf index 94cf68abd76d5..9e5cb84ddc787 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hant.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hant.xlf @@ -202,6 +202,11 @@ + + Obsolete symbol + Obsolete symbol + + Operator - Overloaded 運算子 - 多載 diff --git a/src/EditorFeatures/TestUtilities/Classification/FormattedClassifications.cs b/src/EditorFeatures/TestUtilities/Classification/FormattedClassifications.cs index b962707454438..717b35ad533ba 100644 --- a/src/EditorFeatures/TestUtilities/Classification/FormattedClassifications.cs +++ b/src/EditorFeatures/TestUtilities/Classification/FormattedClassifications.cs @@ -90,6 +90,10 @@ public static FormattedClassification Property(string text) public static FormattedClassification Event(string text) => New(text, ClassificationTypeNames.EventName); + [DebuggerStepThrough] + public static FormattedClassification Obsolete(string text) + => New(text, ClassificationTypeNames.ObsoleteSymbol); + [DebuggerStepThrough] public static FormattedClassification Static(string text) => New(text, ClassificationTypeNames.StaticSymbol); diff --git a/src/EditorFeatures/TestUtilities/ObsoleteSymbol/AbstractObsoleteSymbolTests.cs b/src/EditorFeatures/TestUtilities/ObsoleteSymbol/AbstractObsoleteSymbolTests.cs new file mode 100644 index 0000000000000..05057c372ad75 --- /dev/null +++ b/src/EditorFeatures/TestUtilities/ObsoleteSymbol/AbstractObsoleteSymbolTests.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ObsoleteSymbol; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Test.Utilities; + +namespace Microsoft.CodeAnalysis.Editor.UnitTests.ObsoleteSymbol; + +[UseExportProvider] +public abstract class AbstractObsoleteSymbolTests +{ + protected abstract EditorTestWorkspace CreateWorkspace(string markup); + + protected async Task TestAsync(string markup) + { + using var workspace = CreateWorkspace(markup); + + var project = workspace.CurrentSolution.Projects.Single(); + var language = project.Language; + var documents = project.Documents.ToImmutableArray(); + + for (var i = 0; i < documents.Length; i++) + { + var document = documents[i]; + var text = await document.GetTextAsync(); + + var service = document.GetRequiredLanguageService(); + var textSpans = ImmutableArray.Create(new TextSpan(0, text.Length)); + var result = await service.GetLocationsAsync(document, textSpans, CancellationToken.None); + + var expectedSpans = workspace.Documents[i].SelectedSpans.OrderBy(s => s.Start); + var actualSpans = result.OrderBy(s => s.Start); + + AssertEx.EqualOrDiff( + string.Join(Environment.NewLine, expectedSpans), + string.Join(Environment.NewLine, actualSpans)); + } + } +} diff --git a/src/EditorFeatures/VisualBasicTest/ObsoleteSymbol/VisualBasicObsoleteSymbolTests.vb b/src/EditorFeatures/VisualBasicTest/ObsoleteSymbol/VisualBasicObsoleteSymbolTests.vb new file mode 100644 index 0000000000000..fd6fee9bb0372 --- /dev/null +++ b/src/EditorFeatures/VisualBasicTest/ObsoleteSymbol/VisualBasicObsoleteSymbolTests.vb @@ -0,0 +1,218 @@ +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. +' See the LICENSE file in the project root for more information. + +Imports Microsoft.CodeAnalysis.Editor.UnitTests.ObsoleteSymbol + +Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.ObsoleteSymbol + Public Class VisualBasicObsoleteSymbolTests + Inherits AbstractObsoleteSymbolTests + + Protected Overrides Function CreateWorkspace(markup As String) As EditorTestWorkspace + Return EditorTestWorkspace.CreateVisualBasic(markup) + End Function + + + + + + + + Public Async Function TestObsoleteTypeDefinition(keyword As String) As Task + Await TestAsync( + $" + + {keyword} [|ObsoleteType|] + End {keyword} + + {keyword} NonObsoleteType + End {keyword} + ") + End Function + + + Public Async Function TestObsoleteDelegateTypeDefinition() As Task + Await TestAsync( + " + + Delegate Sub [|ObsoleteType|]() + + Delegate Sub NonObsoleteType() + ") + End Function + + + Public Async Function TestDeclarationAndUseOfObsoleteAlias() As Task + Await TestAsync( + " + Imports [|ObsoleteAlias|] = [|ObsoleteType|] + + + Class [|ObsoleteType|] + End Class + + ''' + ''' + Class NonObsoleteType + Dim field As [|ObsoleteAlias|] = New [|ObsoleteType|]() + End Class + ") + End Function + + + Public Async Function TestParametersAndReturnTypes() As Task + Await TestAsync( + " + + Class [|ObsoleteType|] + End Class + + Class NonObsoleteType + Function Method(arg As [|ObsoleteType|]) As [|ObsoleteType|] + Return New [|ObsoleteType|]() + End Function + + Dim field As System.Func(Of [|ObsoleteType|], [|ObsoleteType|]) = Function(arg As [|ObsoleteType|]) New [|ObsoleteType|]() + End Class + ") + End Function + + + Public Async Function TestImplicitType() As Task + Await TestAsync( + " + + Class [|ObsoleteType|] + Public Sub New() + End Sub + + + Public Sub New(x As Integer) + End Sub + End Class + + Class ObsoleteCtor + Public Sub New() + End Sub + + + Public Sub New(x As Integer) + End Sub + End Class + + Class NonObsoleteType + Sub Method() + Dim t1 As New [|ObsoleteType|]() + Dim t2 As [|New|] [|ObsoleteType|](3) + [|Dim|] t3 = New [|ObsoleteType|]() + [|Dim|] t4 = [|New|] [|ObsoleteType|](3) + Dim t5 As [|ObsoleteType|] = New [|ObsoleteType|]() + Dim t6 As [|ObsoleteType|] = [|New|] [|ObsoleteType|](3) + [|Dim|] t7 = CreateObsoleteType() + Dim t8 = NameOf([|ObsoleteType|]) + + Dim u1 As New ObsoleteCtor() + Dim u2 As [|New|] ObsoleteCtor(3) + Dim u3 = New ObsoleteCtor() + Dim u4 = [|New|] ObsoleteCtor(3) + Dim u5 As ObsoleteCtor = New ObsoleteCtor() + Dim u6 As ObsoleteCtor = [|New|] ObsoleteCtor(3) + Dim u8 = NameOf(ObsoleteCtor) + End Sub + + Function CreateObsoleteType() As [|ObsoleteType|] + Return New [|ObsoleteType|]() + End Function + End Class + ") + End Function + + + Public Async Function TestDeclarators() As Task + Await TestAsync( + " + + Class [|ObsoleteType|] + End Class + + Class NonObsoleteType + Sub Method() + ' In this method, only t5 has an implicit type, but the Dim keyword applies to all declared + ' variables. Currently this feature does not analyze a Dim keyword when more than one variable + ' is declared. + Dim t1, t2 As New [|ObsoleteType|](), t3, t4 As [|ObsoleteType|], t5 = New [|ObsoleteType|]() + End Sub + End Class + ") + End Function + + + Public Async Function TestExtensionMethods() As Task + Await TestAsync( + " + + Module [|ObsoleteType|] + + Public Shared Sub ObsoleteMember1(ignored As C) + End Sub + + + + Public Shared Sub [|ObsoleteMember2|](ignored As C) + End Sub + End Module + + Class C + Sub Method() + Me.ObsoleteMember1() + Me.[|ObsoleteMember2|]() + [|ObsoleteType|].ObsoleteMember1(Me) + [|ObsoleteType|].[|ObsoleteMember2|](Me) + End Sub + End Class + ") + End Function + + + Public Async Function TestGenerics() As Task + Await TestAsync( + " + + Class [|ObsoleteType|] + End Class + + + Structure [|ObsoleteValueType|] + End Structure + + Class G(Of T) + End Class + + Class C + Sub M(Of T)() + End Sub + + ''' + ''' Visual Basic, unlike C#, resolves concrete type names in generic argument positions in doc + ''' comment references. + ''' + ''' + Sub Method() + Dim x1 = New G(Of [|ObsoleteType|])() + Dim x2 = New G(Of G(Of [|ObsoleteType|]))() + M(Of [|ObsoleteType|])() + M(Of G(Of [|ObsoleteType|]))() + M(Of G(Of G(Of [|ObsoleteType|])))() + + ' Mark 'Dim' as obsolete even when it points to Nullable(Of T) where T is obsolete + [|Dim|] nullableValue = CreateNullableValueType() + End Sub + + Function CreateNullableValueType() As [|ObsoleteValueType|]? + Return New [|ObsoleteValueType|]() + End Function + End Class + ") + End Function + End Class +End Namespace diff --git a/src/Features/Core/Portable/Completion/Providers/SymbolCompletionItem.cs b/src/Features/Core/Portable/Completion/Providers/SymbolCompletionItem.cs index 1f3c651245e43..0990ee2947f07 100644 --- a/src/Features/Core/Portable/Completion/Providers/SymbolCompletionItem.cs +++ b/src/Features/Core/Portable/Completion/Providers/SymbolCompletionItem.cs @@ -13,6 +13,7 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Utilities; +using Microsoft.CodeAnalysis.Tags; namespace Microsoft.CodeAnalysis.Completion.Providers; @@ -56,6 +57,10 @@ private static CompletionItem CreateWorker( AddSupportedPlatforms(builder, supportedPlatforms); symbolEncoder(symbols, builder); + tags = tags.NullToEmpty(); + if (symbols.All(symbol => symbol.IsObsolete()) && !tags.Contains(WellKnownTags.Deprecated)) + tags = tags.Add(WellKnownTags.Deprecated); + var firstSymbol = symbols[0]; var item = CommonCompletionItem.Create( displayText: displayText, diff --git a/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs b/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs index 3a9f045e2e595..c6a06c30f422c 100644 --- a/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs +++ b/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs @@ -95,6 +95,17 @@ internal static partial class ProtocolConversions { WellKnownTags.NuGet, ImmutableArray.Create(LSP.CompletionItemKind.Text) } }.ToImmutableDictionary(); + /// + /// Mapping from tags to LSP completion item tags. The value lists the potential LSP tags from + /// least-preferred to most preferred. More preferred kinds will be chosen if the client states they support + /// it. This mapping allows values including extensions to the kinds defined by VS (but not in the core LSP + /// spec). + /// + public static readonly ImmutableDictionary> RoslynTagToCompletionItemTags = new Dictionary>() + { + { WellKnownTags.Deprecated, ImmutableArray.Create(LSP.CompletionItemTag.Deprecated) }, + }.ToImmutableDictionary(); + // TO-DO: More LSP.CompletionTriggerKind mappings are required to properly map to Roslyn CompletionTriggerKinds. // https://dev.azure.com/devdiv/DevDiv/_workitems/edit/1178726 public static async Task LSPToRoslynCompletionTriggerAsync( diff --git a/src/Features/LanguageServer/Protocol/Features/Options/ClassificationOptionsStorage.cs b/src/Features/LanguageServer/Protocol/Features/Options/ClassificationOptionsStorage.cs index 87d9faa3241fd..194765e18320e 100644 --- a/src/Features/LanguageServer/Protocol/Features/Options/ClassificationOptionsStorage.cs +++ b/src/Features/LanguageServer/Protocol/Features/Options/ClassificationOptionsStorage.cs @@ -12,6 +12,7 @@ public static ClassificationOptions GetClassificationOptions(this IOptionsReader => new() { ClassifyReassignedVariables = globalOptions.GetOption(ClassifyReassignedVariables, language), + ClassifyObsoleteSymbols = globalOptions.GetOption(ClassifyObsoleteSymbols, language), ColorizeRegexPatterns = globalOptions.GetOption(ColorizeRegexPatterns, language), ColorizeJsonPatterns = globalOptions.GetOption(ColorizeJsonPatterns, language), // ForceFrozenPartialSemanticsForCrossProcessOperations not stored in global options @@ -23,6 +24,9 @@ public static OptionsProvider GetClassificationOptionsPro public static PerLanguageOption2 ClassifyReassignedVariables = new("dotnet_classify_reassigned_variables", ClassificationOptions.Default.ClassifyReassignedVariables); + public static PerLanguageOption2 ClassifyObsoleteSymbols = + new("dotnet_classify_obsolete_symbols", ClassificationOptions.Default.ClassifyObsoleteSymbols); + public static PerLanguageOption2 ColorizeRegexPatterns = new("dotnet_colorize_regex_patterns", ClassificationOptions.Default.ColorizeRegexPatterns); diff --git a/src/Features/LanguageServer/Protocol/Handler/Completion/AbstractLspCompletionResultCreationService.cs b/src/Features/LanguageServer/Protocol/Handler/Completion/AbstractLspCompletionResultCreationService.cs index 8cdfd83df2e1f..7f3ceaa0a5795 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Completion/AbstractLspCompletionResultCreationService.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Completion/AbstractLspCompletionResultCreationService.cs @@ -15,6 +15,7 @@ using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; @@ -121,6 +122,7 @@ internal abstract class AbstractLspCompletionResultCreationService : ILspComplet lspItem.FilterText = item.FilterText; lspItem.Kind = GetCompletionKind(item.Tags, capabilityHelper.SupportedItemKinds); + lspItem.Tags = GetCompletionTags(item.Tags, capabilityHelper.SupportedItemTags); lspItem.Preselect = item.Rules.MatchPriority == MatchPriority.Preselect; if (lspVSClientCapability) @@ -179,6 +181,37 @@ static LSP.CompletionItemKind GetCompletionKind( return LSP.CompletionItemKind.Text; } + static LSP.CompletionItemTag[]? GetCompletionTags( + ImmutableArray tags, + ISet supportedClientTags) + { + using var result = TemporaryArray.Empty; + + foreach (var tag in tags) + { + if (ProtocolConversions.RoslynTagToCompletionItemTags.TryGetValue(tag, out var completionItemTags)) + { + // Always at least pick the core tag provided. + var lspTag = completionItemTags[0]; + + // If better kinds are preferred, return them if the client supports them. + for (var i = 1; i < completionItemTags.Length; i++) + { + var preferredTag = completionItemTags[i]; + if (supportedClientTags.Contains(preferredTag)) + lspTag = preferredTag; + } + + result.Add(lspTag); + } + } + + if (result.Count == 0) + return null; + + return [.. result.ToImmutableAndClear()]; + } + static string[] GetCommitCharacters( CompletionItem item, Dictionary, string[]> currentRuleCache) diff --git a/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionCapabilityHelper.cs b/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionCapabilityHelper.cs index bd565c39807ac..7bf5905039f3d 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionCapabilityHelper.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionCapabilityHelper.cs @@ -30,6 +30,7 @@ internal sealed class CompletionCapabilityHelper public bool SupportSnippets { get; } public bool SupportsMarkdownDocumentation { get; } public ISet SupportedItemKinds { get; } + public ISet SupportedItemTags { get; } public CompletionCapabilityHelper(ClientCapabilities clientCapabilities) { @@ -42,6 +43,7 @@ public CompletionCapabilityHelper(ClientCapabilities clientCapabilities) SupportCompletionListData = _completionSetting?.CompletionListSetting?.ItemDefaults?.Contains(DataPropertyName) == true; SupportDefaultCommitCharacters = _completionSetting?.CompletionListSetting?.ItemDefaults?.Contains(CommitCharactersPropertyName) == true; SupportedItemKinds = _completionSetting?.CompletionItemKind?.ValueSet?.ToSet() ?? SpecializedCollections.EmptySet(); + SupportedItemTags = _completionSetting?.CompletionItem?.TagSupport?.ValueSet?.ToSet() ?? SpecializedCollections.EmptySet(); // internal VS LSP if (clientCapabilities.HasVisualStudioLspCapability()) diff --git a/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs b/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs index a3d07daa66148..428806dc57f70 100644 --- a/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs +++ b/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs @@ -320,16 +320,21 @@ private static int ComputeNextToken( if (classificationType == ClassificationTypeNames.StaticSymbol) { // 4. Token modifiers - each set bit will be looked up in SemanticTokensLegend.tokenModifiers - modifierBits = TokenModifiers.Static; + modifierBits |= TokenModifiers.Static; } else if (classificationType == ClassificationTypeNames.ReassignedVariable) { // 5. Token modifiers - each set bit will be looked up in SemanticTokensLegend.tokenModifiers - modifierBits = TokenModifiers.ReassignedVariable; + modifierBits |= TokenModifiers.ReassignedVariable; + } + else if (classificationType == ClassificationTypeNames.ObsoleteSymbol) + { + // 6. Token modifiers - each set bit will be looked up in SemanticTokensLegend.tokenModifiers + modifierBits |= TokenModifiers.Deprecated; } else { - // 6. Token type - looked up in SemanticTokensLegend.tokenTypes (language server defined mapping + // 7. Token type - looked up in SemanticTokensLegend.tokenTypes (language server defined mapping // from integer to LSP token types). tokenTypeIndex = GetTokenTypeIndex(classificationType); } diff --git a/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensSchema.cs b/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensSchema.cs index 94cdb33c0955e..752ec12df2eeb 100644 --- a/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensSchema.cs +++ b/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensSchema.cs @@ -130,7 +130,8 @@ public static SemanticTokensSchema LegacyTokensSchemaForLSIF [ // This must be in the same order as SemanticTokens.TokenModifiers, but skip the "None" item SemanticTokenModifiers.Static, - nameof(SemanticTokens.TokenModifiers.ReassignedVariable) + nameof(SemanticTokens.TokenModifiers.ReassignedVariable), + SemanticTokenModifiers.Deprecated, ]; } } diff --git a/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/TokenModifiers.cs b/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/TokenModifiers.cs index c93cf9c6c35ce..b5f820e1d18a7 100644 --- a/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/TokenModifiers.cs +++ b/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/TokenModifiers.cs @@ -16,5 +16,6 @@ internal enum TokenModifiers None = 0, Static = 1, ReassignedVariable = 2, + Deprecated = 4, } } diff --git a/src/Features/LanguageServer/Protocol/Protocol/CompletionItem.cs b/src/Features/LanguageServer/Protocol/Protocol/CompletionItem.cs index acd63d0657304..f416ae767d3d1 100644 --- a/src/Features/LanguageServer/Protocol/Protocol/CompletionItem.cs +++ b/src/Features/LanguageServer/Protocol/Protocol/CompletionItem.cs @@ -52,6 +52,17 @@ public CompletionItemKind Kind set; } = CompletionItemKind.None; + /// + /// Tags for this completion item. + /// + [DataMember(Name = "tags")] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public CompletionItemTag[]? Tags + { + get; + set; + } + /// /// Gets or sets the completion detail. /// diff --git a/src/Features/LanguageServer/Protocol/Protocol/Internal/Efficiency/OptimizedVSCompletionListJsonConverter.cs b/src/Features/LanguageServer/Protocol/Protocol/Internal/Efficiency/OptimizedVSCompletionListJsonConverter.cs index 8369864bfd38f..2dbe0839d6d76 100644 --- a/src/Features/LanguageServer/Protocol/Protocol/Internal/Efficiency/OptimizedVSCompletionListJsonConverter.cs +++ b/src/Features/LanguageServer/Protocol/Protocol/Internal/Efficiency/OptimizedVSCompletionListJsonConverter.cs @@ -176,6 +176,12 @@ private static void WriteCompletionItem(JsonWriter writer, CompletionItem comple writer.WritePropertyName("kind"); writer.WriteValue(completionItem.Kind); + if (completionItem.Tags != null) + { + writer.WritePropertyName("tags"); + serializer.Serialize(writer, completionItem.Tags); + } + if (completionItem.Detail != null) { writer.WritePropertyName("detail"); diff --git a/src/Features/LanguageServer/ProtocolUnitTests/Completion/CompletionFeaturesTests.cs b/src/Features/LanguageServer/ProtocolUnitTests/Completion/CompletionFeaturesTests.cs index 97d60c6a0957c..00dfa0293382c 100644 --- a/src/Features/LanguageServer/ProtocolUnitTests/Completion/CompletionFeaturesTests.cs +++ b/src/Features/LanguageServer/ProtocolUnitTests/Completion/CompletionFeaturesTests.cs @@ -496,6 +496,79 @@ public async Task TestUsingServerDefaultCommitCharacters(bool mutatingLspWorkspa } } + [Theory] + [CombinatorialData] + [WorkItem("https://github.com/dotnet/roslyn/issues/26488")] + public async Task TestCompletionForObsoleteSymbol(bool mutatingLspWorkspace) + { + var markup = + """ + using System; + + [Obsolete] + class ObsoleteType; + + class A + { + void M() + { + ObsoleteType{|caret:|} + } + } + """; + + await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, DefaultClientCapabilities); + var completionParams = CreateCompletionParams( + testLspServer.GetLocations("caret").Single(), + invokeKind: LSP.VSInternalCompletionInvokeKind.Explicit, + triggerCharacter: "\0", + triggerKind: LSP.CompletionTriggerKind.Invoked); + + var document = testLspServer.GetCurrentSolution().Projects.First().Documents.First(); + + var completionResult = await testLspServer.ExecuteRequestAsync(LSP.Methods.TextDocumentCompletionName, completionParams, CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(completionResult.ItemDefaults.EditRange); + Assert.NotNull(completionResult.ItemDefaults.Data); + Assert.NotNull(completionResult.ItemDefaults.CommitCharacters); + + var actualItem = completionResult.Items.First(i => i.Label == "ObsoleteType"); + Assert.Null(actualItem.LabelDetails); + Assert.Null(actualItem.SortText); + Assert.Equal(CompletionItemKind.Class, actualItem.Kind); + Assert.Equal([CompletionItemTag.Deprecated], actualItem.Tags); + Assert.Null(actualItem.FilterText); + Assert.Null(actualItem.TextEdit); + Assert.Null(actualItem.TextEditText); + Assert.Null(actualItem.AdditionalTextEdits); + Assert.Null(actualItem.Command); + Assert.Null(actualItem.CommitCharacters); + Assert.Null(actualItem.Data); + Assert.Null(actualItem.Detail); + Assert.Null(actualItem.Documentation); + + actualItem.Data = completionResult.ItemDefaults.Data; + + var resolvedItem = await testLspServer.ExecuteRequestAsync(LSP.Methods.TextDocumentCompletionResolveName, actualItem, CancellationToken.None).ConfigureAwait(false); + Assert.Null(resolvedItem.LabelDetails); + Assert.Null(resolvedItem.SortText); + Assert.Equal(CompletionItemKind.Class, resolvedItem.Kind); + Assert.Equal([CompletionItemTag.Deprecated], resolvedItem.Tags); + + Assert.Null(resolvedItem.AdditionalTextEdits); + Assert.Null(resolvedItem.FilterText); + Assert.Null(resolvedItem.TextEdit); + Assert.Null(resolvedItem.TextEditText); + Assert.Null(resolvedItem.Command); + Assert.Null(resolvedItem.Detail); + + var expectedDocumentation = new MarkupContent() + { + Kind = LSP.MarkupKind.PlainText, + Value = "[deprecated] class ObsoleteType" + }; + AssertJsonEquals(resolvedItem.Documentation, expectedDocumentation); + } + private sealed class CSharpLspMockCompletionService : CompletionService { private CSharpLspMockCompletionService(SolutionServices services, IAsynchronousOperationListenerProvider listenerProvider) : base(services, listenerProvider) diff --git a/src/Features/Lsif/Generator/Generator.cs b/src/Features/Lsif/Generator/Generator.cs index 61929ec437f86..87db8cfbcc8f6 100644 --- a/src/Features/Lsif/Generator/Generator.cs +++ b/src/Features/Lsif/Generator/Generator.cs @@ -86,7 +86,7 @@ public static Generator CreateAndWriteCapabilitiesVertex(ILsifJsonWriter lsifJso DocumentSymbolProvider, FoldingRangeProvider, DiagnosticProvider, - new SemanticTokensCapabilities(SemanticTokensSchema.LegacyTokensSchemaForLSIF.AllTokenTypes, [SemanticTokenModifiers.Static])); + new SemanticTokensCapabilities(SemanticTokensSchema.LegacyTokensSchemaForLSIF.AllTokenTypes, [SemanticTokenModifiers.Static, SemanticTokenModifiers.Deprecated])); generator._lsifJsonWriter.Write(capabilitiesVertex); return generator; } diff --git a/src/Features/Lsif/GeneratorTest/OutputFormatTests.vb b/src/Features/Lsif/GeneratorTest/OutputFormatTests.vb index 2db321450b74f..5b01bbd5725c8 100644 --- a/src/Features/Lsif/GeneratorTest/OutputFormatTests.vb +++ b/src/Features/Lsif/GeneratorTest/OutputFormatTests.vb @@ -27,7 +27,7 @@ Namespace Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.UnitTests , openDocuments:=False, composition:=TestLsifOutput.TestComposition), jsonWriter) AssertEx.EqualOrDiff( -"{""hoverProvider"":true,""declarationProvider"":false,""definitionProvider"":true,""referencesProvider"":true,""typeDefinitionProvider"":false,""documentSymbolProvider"":true,""foldingRangeProvider"":true,""diagnosticProvider"":false,""semanticTokensProvider"":{""tokenTypes"":[""namespace"",""type"",""class"",""enum"",""interface"",""struct"",""typeParameter"",""parameter"",""variable"",""property"",""enumMember"",""event"",""function"",""method"",""macro"",""keyword"",""modifier"",""comment"",""string"",""number"",""regexp"",""operator"",""class name"",""constant name"",""delegate name"",""enum member name"",""enum name"",""event name"",""excluded code"",""extension method name"",""field name"",""interface name"",""json - array"",""json - comment"",""json - constructor name"",""json - keyword"",""json - number"",""json - object"",""json - operator"",""json - property name"",""json - punctuation"",""json - string"",""json - text"",""keyword - control"",""label name"",""local name"",""method name"",""module name"",""namespace name"",""operator - overloaded"",""parameter name"",""preprocessor keyword"",""preprocessor text"",""property name"",""punctuation"",""record class name"",""record struct name"",""regex - alternation"",""regex - anchor"",""regex - character class"",""regex - comment"",""regex - grouping"",""regex - other escape"",""regex - quantifier"",""regex - self escaped character"",""regex - text"",""roslyn test code markdown"",""string - escape character"",""string - verbatim"",""struct name"",""text"",""type parameter name"",""whitespace"",""xml doc comment - attribute name"",""xml doc comment - attribute quotes"",""xml doc comment - attribute value"",""xml doc comment - cdata section"",""xml doc comment - comment"",""xml doc comment - delimiter"",""xml doc comment - entity reference"",""xml doc comment - name"",""xml doc comment - processing instruction"",""xml doc comment - text"",""xml literal - attribute name"",""xml literal - attribute quotes"",""xml literal - attribute value"",""xml literal - cdata section"",""xml literal - comment"",""xml literal - delimiter"",""xml literal - embedded expression"",""xml literal - entity reference"",""xml literal - name"",""xml literal - processing instruction"",""xml literal - text""],""tokenModifiers"":[""static""]},""id"":1,""type"":""vertex"",""label"":""capabilities""} +"{""hoverProvider"":true,""declarationProvider"":false,""definitionProvider"":true,""referencesProvider"":true,""typeDefinitionProvider"":false,""documentSymbolProvider"":true,""foldingRangeProvider"":true,""diagnosticProvider"":false,""semanticTokensProvider"":{""tokenTypes"":[""namespace"",""type"",""class"",""enum"",""interface"",""struct"",""typeParameter"",""parameter"",""variable"",""property"",""enumMember"",""event"",""function"",""method"",""macro"",""keyword"",""modifier"",""comment"",""string"",""number"",""regexp"",""operator"",""class name"",""constant name"",""delegate name"",""enum member name"",""enum name"",""event name"",""excluded code"",""extension method name"",""field name"",""interface name"",""json - array"",""json - comment"",""json - constructor name"",""json - keyword"",""json - number"",""json - object"",""json - operator"",""json - property name"",""json - punctuation"",""json - string"",""json - text"",""keyword - control"",""label name"",""local name"",""method name"",""module name"",""namespace name"",""operator - overloaded"",""parameter name"",""preprocessor keyword"",""preprocessor text"",""property name"",""punctuation"",""record class name"",""record struct name"",""regex - alternation"",""regex - anchor"",""regex - character class"",""regex - comment"",""regex - grouping"",""regex - other escape"",""regex - quantifier"",""regex - self escaped character"",""regex - text"",""roslyn test code markdown"",""string - escape character"",""string - verbatim"",""struct name"",""text"",""type parameter name"",""whitespace"",""xml doc comment - attribute name"",""xml doc comment - attribute quotes"",""xml doc comment - attribute value"",""xml doc comment - cdata section"",""xml doc comment - comment"",""xml doc comment - delimiter"",""xml doc comment - entity reference"",""xml doc comment - name"",""xml doc comment - processing instruction"",""xml doc comment - text"",""xml literal - attribute name"",""xml literal - attribute quotes"",""xml literal - attribute value"",""xml literal - cdata section"",""xml literal - comment"",""xml literal - delimiter"",""xml literal - embedded expression"",""xml literal - entity reference"",""xml literal - name"",""xml literal - processing instruction"",""xml literal - text""],""tokenModifiers"":[""static"",""deprecated""]},""id"":1,""type"":""vertex"",""label"":""capabilities""} {""kind"":""csharp"",""resource"":""file:///Z:/%EE%89%9B/TestProject.csproj"",""name"":""TestProject"",""id"":2,""type"":""vertex"",""label"":""project""} {""kind"":""begin"",""scope"":""project"",""data"":2,""id"":3,""type"":""vertex"",""label"":""$event""} {""uri"":""file:///Z:/%EE%89%9B/a.cs"",""languageId"":""csharp"",""id"":4,""type"":""vertex"",""label"":""document""} @@ -168,7 +168,8 @@ Namespace Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.UnitTests ""xml literal - text"" ], ""tokenModifiers"": [ - ""static"" + ""static"", + ""deprecated"" ] }, ""id"": 1, diff --git a/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml b/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml index e1416d98bc310..d8ae01d553462 100644 --- a/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml +++ b/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml @@ -218,6 +218,8 @@ Content="{x:Static local:AdvancedOptionPageStrings.Option_Report_invalid_placeholders_in_string_dot_format_calls}" /> + diff --git a/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml.cs b/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml.cs index 859461ef020cb..9cedb77ba732c 100644 --- a/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml.cs +++ b/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageControl.xaml.cs @@ -137,6 +137,7 @@ public AdvancedOptionPageControl(OptionStore optionStore, IComponentModel compon BindToOption(Fix_text_pasted_into_string_literals_experimental, StringCopyPasteOptionsStorage.AutomaticallyFixStringContentsOnPaste, LanguageNames.CSharp); BindToOption(Report_invalid_placeholders_in_string_dot_format_calls, IdeAnalyzerOptionsStorage.ReportInvalidPlaceholdersInStringDotFormatCalls, LanguageNames.CSharp); BindToOption(Underline_reassigned_variables, ClassificationOptionsStorage.ClassifyReassignedVariables, LanguageNames.CSharp); + BindToOption(Strike_out_obsolete_symbols, ClassificationOptionsStorage.ClassifyObsoleteSymbols, LanguageNames.CSharp); BindToOption(Enable_all_features_in_opened_files_from_source_generators, WorkspaceConfigurationOptionsStorage.EnableOpeningSourceGeneratedFilesInWorkspace, () => { // If the option has not been set by the user, check if the option is enabled from experimentation. diff --git a/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageStrings.cs b/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageStrings.cs index 2f53e9718508f..81950708cc7ce 100644 --- a/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageStrings.cs +++ b/src/VisualStudio/CSharp/Impl/Options/AdvancedOptionPageStrings.cs @@ -150,6 +150,9 @@ public static string Option_DisplayLineSeparators public static string Option_Underline_reassigned_variables => ServicesVSResources.Underline_reassigned_variables; + public static string Option_Strike_out_obsolete_symbols + => ServicesVSResources.Strike_out_obsolete_symbols; + public static string Option_DontPutOutOrRefOnStruct => CSharpVSResources.Don_t_put_ref_or_out_on_custom_struct; diff --git a/src/VisualStudio/Core/Def/Options/VisualStudioOptionStorage.cs b/src/VisualStudio/Core/Def/Options/VisualStudioOptionStorage.cs index 8b5162180b5d2..85f9166a2a8ad 100644 --- a/src/VisualStudio/Core/Def/Options/VisualStudioOptionStorage.cs +++ b/src/VisualStudio/Core/Def/Options/VisualStudioOptionStorage.cs @@ -130,6 +130,7 @@ public bool TryFetch(LocalUserRegistryOptionPersister persister, OptionKey2 opti {"dotnet_show_outlining_for_declaration_level_constructs", new RoamingProfileStorage("TextEditor.%LANGUAGE%.Specific.ShowOutliningForDeclarationLevelConstructs")}, {"csharp_format_on_close_brace", new RoamingProfileStorage("TextEditor.%LANGUAGE%.Specific.Auto Formatting On Close Brace")}, {"dotnet_classify_reassigned_variables", new RoamingProfileStorage("TextEditor.%LANGUAGE%.Specific.ClassificationOptions.ClassifyReassignedVariables")}, + {"dotnet_classify_obsolete_symbols", new RoamingProfileStorage("TextEditor.%LANGUAGE%.Specific.ClassificationOptions.ClassifyObsoleteSymbols")}, {"dotnet_prefer_system_hash_code", new RoamingProfileStorage("TextEditor.%LANGUAGE%.Specific.PreferSystemHashCode")}, {"visual_studio_color_scheme_name", new RoamingProfileStorage("TextEditor.Roslyn.ColorSchemeName")}, {"visual_studio_color_scheme_use_legacy_enhanced_colors", new RoamingProfileStorage("WindowManagement.Options.UseEnhancedColorsForManagedLanguages")}, diff --git a/src/VisualStudio/Core/Def/ServicesVSResources.resx b/src/VisualStudio/Core/Def/ServicesVSResources.resx index aca53283fce6f..949f6dabad60c 100644 --- a/src/VisualStudio/Core/Def/ServicesVSResources.resx +++ b/src/VisualStudio/Core/Def/ServicesVSResources.resx @@ -1894,6 +1894,9 @@ Additional information: {1} When types loosely match + + Strike out obsolete symbols + {0} ({1}) diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.cs.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.cs.xlf index d1e7bfbf1c0bb..b9c2853834d7b 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.cs.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.cs.xlf @@ -1377,6 +1377,11 @@ Trasování zásobníku: {0} Header for numbered stack trace view tabs + + Strike out obsolete symbols + Strike out obsolete symbols + + Suggestion Návrh diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.de.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.de.xlf index 545fe7c9d2db4..17bb6edd8ed5d 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.de.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.de.xlf @@ -1377,6 +1377,11 @@ Stapelüberwachung {0} Header for numbered stack trace view tabs + + Strike out obsolete symbols + Strike out obsolete symbols + + Suggestion Vorschlag diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.es.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.es.xlf index a096c54a69956..7a35b47219bd9 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.es.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.es.xlf @@ -1377,6 +1377,11 @@ Seguimiento de la pila {0} Header for numbered stack trace view tabs + + Strike out obsolete symbols + Strike out obsolete symbols + + Suggestion Sugerencia diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.fr.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.fr.xlf index 6265ab847ad9d..b9f9e6550eb68 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.fr.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.fr.xlf @@ -1377,6 +1377,11 @@ Trace de la pile {0} Header for numbered stack trace view tabs + + Strike out obsolete symbols + Strike out obsolete symbols + + Suggestion Suggestion diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.it.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.it.xlf index bac04d1a313bf..367ae1e8d6683 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.it.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.it.xlf @@ -1377,6 +1377,11 @@ Analisi dello stack {0} Header for numbered stack trace view tabs + + Strike out obsolete symbols + Strike out obsolete symbols + + Suggestion Suggerimento diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ja.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ja.xlf index 6627fa184ad96..931841ff6d5d1 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ja.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ja.xlf @@ -1377,6 +1377,11 @@ スタック トレース {0} Header for numbered stack trace view tabs + + Strike out obsolete symbols + Strike out obsolete symbols + + Suggestion 提案事項 diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ko.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ko.xlf index 19f7d6be1920a..ef33d01d7750a 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ko.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ko.xlf @@ -1377,6 +1377,11 @@ 스택 추적 {0}개 Header for numbered stack trace view tabs + + Strike out obsolete symbols + Strike out obsolete symbols + + Suggestion 제안 diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.pl.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.pl.xlf index 7f6c51ab3a166..e021f154164f8 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.pl.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.pl.xlf @@ -1377,6 +1377,11 @@ Ślad stosu {0} Header for numbered stack trace view tabs + + Strike out obsolete symbols + Strike out obsolete symbols + + Suggestion Sugestia diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.pt-BR.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.pt-BR.xlf index 4ae3db17961c4..c32fe83ef17b2 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.pt-BR.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.pt-BR.xlf @@ -1377,6 +1377,11 @@ Rastreamento de Pilha{0} Header for numbered stack trace view tabs + + Strike out obsolete symbols + Strike out obsolete symbols + + Suggestion Sugestão diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ru.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ru.xlf index 7f92908b97a19..7cfe12ffe0459 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ru.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ru.xlf @@ -1377,6 +1377,11 @@ Трассировка стека {0} Header for numbered stack trace view tabs + + Strike out obsolete symbols + Strike out obsolete symbols + + Suggestion Рекомендация diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.tr.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.tr.xlf index 88b236ef47313..38bac0145467f 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.tr.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.tr.xlf @@ -1377,6 +1377,11 @@ Yığın İzleme {0} Header for numbered stack trace view tabs + + Strike out obsolete symbols + Strike out obsolete symbols + + Suggestion Öneri diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.zh-Hans.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.zh-Hans.xlf index b84781af1c18d..d673741b2bf03 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.zh-Hans.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.zh-Hans.xlf @@ -1377,6 +1377,11 @@ 堆栈跟踪: {0} Header for numbered stack trace view tabs + + Strike out obsolete symbols + Strike out obsolete symbols + + Suggestion 建议 diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.zh-Hant.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.zh-Hant.xlf index 6acce6f8d8af4..889e73b2f0df3 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.zh-Hant.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.zh-Hant.xlf @@ -1377,6 +1377,11 @@ 堆疊追蹤 {0} Header for numbered stack trace view tabs + + Strike out obsolete symbols + Strike out obsolete symbols + + Suggestion 建議 diff --git a/src/VisualStudio/VisualBasic/Impl/Options/AdvancedOptionPageControl.xaml b/src/VisualStudio/VisualBasic/Impl/Options/AdvancedOptionPageControl.xaml index 4d762d6809e6f..4084ee0418f20 100644 --- a/src/VisualStudio/VisualBasic/Impl/Options/AdvancedOptionPageControl.xaml +++ b/src/VisualStudio/VisualBasic/Impl/Options/AdvancedOptionPageControl.xaml @@ -205,6 +205,8 @@ Content="{x:Static local:AdvancedOptionPageStrings.Option_Report_invalid_placeholders_in_string_dot_format_calls}" /> + diff --git a/src/VisualStudio/VisualBasic/Impl/Options/AdvancedOptionPageControl.xaml.vb b/src/VisualStudio/VisualBasic/Impl/Options/AdvancedOptionPageControl.xaml.vb index 899bb86301916..17375fb645916 100644 --- a/src/VisualStudio/VisualBasic/Impl/Options/AdvancedOptionPageControl.xaml.vb +++ b/src/VisualStudio/VisualBasic/Impl/Options/AdvancedOptionPageControl.xaml.vb @@ -127,6 +127,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic.Options BindToOption(ShowRemarksInQuickInfo, QuickInfoOptionsStorage.ShowRemarksInQuickInfo, LanguageNames.VisualBasic) BindToOption(Report_invalid_placeholders_in_string_dot_format_calls, IdeAnalyzerOptionsStorage.ReportInvalidPlaceholdersInStringDotFormatCalls, LanguageNames.VisualBasic) BindToOption(Underline_reassigned_variables, ClassificationOptionsStorage.ClassifyReassignedVariables, LanguageNames.VisualBasic) + BindToOption(Strike_out_obsolete_symbols, ClassificationOptionsStorage.ClassifyObsoleteSymbols, LanguageNames.VisualBasic) ' Go To Definition BindToOption(NavigateToObjectBrowser, VisualStudioNavigationOptionsStorage.NavigateToObjectBrowser, LanguageNames.VisualBasic) diff --git a/src/VisualStudio/VisualBasic/Impl/Options/AdvancedOptionPageStrings.vb b/src/VisualStudio/VisualBasic/Impl/Options/AdvancedOptionPageStrings.vb index 545513dcf7094..d9a8e91fcdcbb 100644 --- a/src/VisualStudio/VisualBasic/Impl/Options/AdvancedOptionPageStrings.vb +++ b/src/VisualStudio/VisualBasic/Impl/Options/AdvancedOptionPageStrings.vb @@ -85,6 +85,9 @@ Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic.Options Public ReadOnly Property Option_Underline_reassigned_variables As String = ServicesVSResources.Underline_reassigned_variables + Public ReadOnly Property Option_Strike_out_obsolete_symbols As String = + ServicesVSResources.Strike_out_obsolete_symbols + Public ReadOnly Property Option_Display_all_hints_while_pressing_Alt_F1 As String = ServicesVSResources.Display_all_hints_while_pressing_Alt_F1 diff --git a/src/Workspaces/CSharp/Portable/ObsoleteSymbol/CSharpObsoleteSymbolService.cs b/src/Workspaces/CSharp/Portable/ObsoleteSymbol/CSharpObsoleteSymbolService.cs new file mode 100644 index 0000000000000..db62ff996c5f7 --- /dev/null +++ b/src/Workspaces/CSharp/Portable/ObsoleteSymbol/CSharpObsoleteSymbolService.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Composition; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.ObsoleteSymbol; + +namespace Microsoft.CodeAnalysis.CSharp.ObsoleteSymbol; + +[ExportLanguageService(typeof(IObsoleteSymbolService), LanguageNames.CSharp)] +[Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class CSharpObsoleteSymbolService() : AbstractObsoleteSymbolService(dimKeywordKind: null) +{ +} diff --git a/src/Workspaces/Core/Portable/Classification/AbstractClassificationService.cs b/src/Workspaces/Core/Portable/Classification/AbstractClassificationService.cs index f112e3dbb5cf9..176d0ebd7aac7 100644 --- a/src/Workspaces/Core/Portable/Classification/AbstractClassificationService.cs +++ b/src/Workspaces/Core/Portable/Classification/AbstractClassificationService.cs @@ -10,6 +10,7 @@ using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.Extensions; using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.ObsoleteSymbol; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.ReassignedVariable; using Microsoft.CodeAnalysis.Remote; @@ -139,6 +140,7 @@ public static async Task AddClassificationsInCurrentProcessAsync( { var classificationService = document.GetRequiredLanguageService(); var reassignedVariableService = document.GetRequiredLanguageService(); + var obsoleteSymbolService = document.GetRequiredLanguageService(); var extensionManager = document.Project.Solution.Services.GetRequiredService(); var classifiers = classificationService.GetDefaultSyntaxClassifiers(); @@ -155,6 +157,13 @@ await classificationService.AddSemanticClassificationsAsync( foreach (var span in reassignedVariableSpans) result.Add(new ClassifiedSpan(span, ClassificationTypeNames.ReassignedVariable)); } + + if (options.ClassifyObsoleteSymbols) + { + var obsoleteSymbolSpans = await obsoleteSymbolService.GetLocationsAsync(document, textSpans, cancellationToken).ConfigureAwait(false); + foreach (var span in obsoleteSymbolSpans) + result.Add(new ClassifiedSpan(span, ClassificationTypeNames.ObsoleteSymbol)); + } } else if (type == ClassificationType.EmbeddedLanguage) { diff --git a/src/Workspaces/Core/Portable/Classification/ClassificationOptions.cs b/src/Workspaces/Core/Portable/Classification/ClassificationOptions.cs index ef812b4362d53..417ec439686f0 100644 --- a/src/Workspaces/Core/Portable/Classification/ClassificationOptions.cs +++ b/src/Workspaces/Core/Portable/Classification/ClassificationOptions.cs @@ -10,6 +10,7 @@ namespace Microsoft.CodeAnalysis.Classification; internal readonly record struct ClassificationOptions { [DataMember] public bool ClassifyReassignedVariables { get; init; } = false; + [DataMember] public bool ClassifyObsoleteSymbols { get; init; } = true; [DataMember] public bool ColorizeRegexPatterns { get; init; } = true; [DataMember] public bool ColorizeJsonPatterns { get; init; } = true; [DataMember] public bool ForceFrozenPartialSemanticsForCrossProcessOperations { get; init; } = false; diff --git a/src/Workspaces/Core/Portable/Classification/ClassificationTypeNames.cs b/src/Workspaces/Core/Portable/Classification/ClassificationTypeNames.cs index 27bce3937550a..e8a52b10f219f 100644 --- a/src/Workspaces/Core/Portable/Classification/ClassificationTypeNames.cs +++ b/src/Workspaces/Core/Portable/Classification/ClassificationTypeNames.cs @@ -11,7 +11,7 @@ public static class ClassificationTypeNames /// /// Additive classifications types supply additional context to other classifications. /// - public static ImmutableArray AdditiveTypeNames { get; } = [StaticSymbol, ReassignedVariable, TestCode]; + public static ImmutableArray AdditiveTypeNames { get; } = [StaticSymbol, ReassignedVariable, ObsoleteSymbol, TestCode]; public static ImmutableArray AllTypeNames { get; } = [ @@ -28,6 +28,7 @@ public static class ClassificationTypeNames WhiteSpace, Text, ReassignedVariable, + ObsoleteSymbol, StaticSymbol, PreprocessorText, Punctuation, @@ -112,6 +113,7 @@ public static class ClassificationTypeNames public const string Text = "text"; internal const string ReassignedVariable = "reassigned variable"; + internal const string ObsoleteSymbol = "obsolete symbol"; public const string StaticSymbol = "static symbol"; public const string PreprocessorText = "preprocessor text"; diff --git a/src/Workspaces/Core/Portable/ObsoleteSymbol/AbstractObsoleteSymbolService.cs b/src/Workspaces/Core/Portable/ObsoleteSymbol/AbstractObsoleteSymbolService.cs new file mode 100644 index 0000000000000..d3349b8b75c30 --- /dev/null +++ b/src/Workspaces/Core/Portable/ObsoleteSymbol/AbstractObsoleteSymbolService.cs @@ -0,0 +1,218 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.LanguageService; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.ObsoleteSymbol; + +internal abstract class AbstractObsoleteSymbolService(int? dimKeywordKind) : IObsoleteSymbolService +{ + /// + /// The of the keyword in Visual Basic, or + /// for C# scenarios. This value is used to improve performance in the token classification + /// fast-path by avoiding unnecessary calls to . + /// + private readonly int? _dimKeywordKind = dimKeywordKind; + + protected virtual void ProcessDimKeyword(ref ArrayBuilder? result, SemanticModel semanticModel, SyntaxToken token, CancellationToken cancellationToken) + { + // Take no action by default + } + + public async Task> GetLocationsAsync(Document document, ImmutableArray textSpans, CancellationToken cancellationToken) + { + var syntaxFacts = document.GetRequiredLanguageService(); + + var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + // Avoid taking a builder from the pool in the common case where there are no references to obsolete symbols + // currently on screen. + ArrayBuilder? result = null; + try + { + var semanticModel = compilation.GetSemanticModel(root.SyntaxTree); + + foreach (var span in textSpans) + { + Recurse(span, semanticModel); + } + + if (result is null) + return ImmutableArray.Empty; + + result.RemoveDuplicates(); + return result.ToImmutableAndClear(); + } + finally + { + result?.Free(); + } + + void Recurse(TextSpan span, SemanticModel semanticModel) + { + using var _ = ArrayBuilder.GetInstance(out var stack); + + // Walk through all the nodes in the provided span. Directly analyze local or parameter declaration. And + // also analyze any identifiers which might be reference to locals or parameters. Note that we might hit + // locals/parameters without any references in the span, or references that don't have the declarations in + // the span + stack.Add(root.FindNode(span)); + + // Use a stack so we don't blow out the stack with recursion. + while (stack.Count > 0) + { + var current = stack.Last(); + stack.RemoveLast(); + + if (current.Span.IntersectsWith(span)) + { + var tokenFromNode = ProcessNode(semanticModel, current); + + foreach (var child in current.ChildNodesAndTokens()) + { + if (child.IsNode) + { + stack.Add(child.AsNode()!); + } + + var token = child.AsToken(); + if (token != tokenFromNode) + ProcessToken(semanticModel, child.AsToken()); + + ExtractStructureFromTrivia(stack, token.LeadingTrivia); + ExtractStructureFromTrivia(stack, token.TrailingTrivia); + } + } + } + } + + static void ExtractStructureFromTrivia(ArrayBuilder stack, SyntaxTriviaList triviaList) + { + foreach (var trivia in triviaList) + { + if (trivia.HasStructure) + { + stack.Add(trivia.GetStructure()!); + } + } + } + + void AddResult(TextSpan span) + { + result ??= ArrayBuilder.GetInstance(); + result.Add(span); + } + + SyntaxToken ProcessNode(SemanticModel semanticModel, SyntaxNode node) + { + if (syntaxFacts.IsUsingAliasDirective(node)) + { + syntaxFacts.GetPartsOfUsingAliasDirective(node, out _, out var aliasToken, out var name); + if (!aliasToken.Span.IsEmpty) + { + // Use 'name.Parent' because VB can't resolve the declared symbol directly from 'node' + var symbol = semanticModel.GetDeclaredSymbol(name.GetRequiredParent(), cancellationToken); + if (IsSymbolObsolete(symbol)) + AddResult(aliasToken.Span); + } + + return aliasToken; + } + else if (syntaxFacts.IsObjectCreationExpression(node)) + { + syntaxFacts.GetPartsOfObjectCreationExpression(node, out var creationKeyword, out _, out _, out _); + if (!creationKeyword.Span.IsEmpty) + { + // For syntax like the following + // + // SomeType value = new SomeType(); + // + // We classify 'new' as obsolete only if the specific constructor is obsolete. If the containing + // type is obsolete, the classification will be applied to 'SomeType' instead. + var symbol = semanticModel.GetSymbolInfo(node, cancellationToken).Symbol; + if (IsSymbolObsolete(symbol)) + AddResult(creationKeyword.Span); + } + } + else if (syntaxFacts.IsImplicitObjectCreationExpression(node)) + { + syntaxFacts.GetPartsOfImplicitObjectCreationExpression(node, out var creationKeyword, out _, out _); + if (!creationKeyword.Span.IsEmpty) + { + // For syntax like the following + // + // SomeType value = new(); + // + // We classify 'new' as obsolete if either the type or the specific constructor is obsolete. + var symbol = semanticModel.GetSymbolInfo(node, cancellationToken).Symbol; + if (IsSymbolObsolete(symbol) || IsSymbolObsolete(symbol?.ContainingType)) + AddResult(creationKeyword.Span); + } + } + + return default; + } + + void ProcessToken(SemanticModel semanticModel, SyntaxToken token) + { + if (syntaxFacts.IsIdentifier(token)) + { + ProcessIdentifier(semanticModel, token); + } + else if (token.RawKind == _dimKeywordKind) + { + ProcessDimKeyword(ref result, semanticModel, token, cancellationToken); + } + } + + void ProcessIdentifier(SemanticModel semanticModel, SyntaxToken token) + { + if (syntaxFacts.IsDeclaration(token.Parent)) + { + var symbol = semanticModel.GetDeclaredSymbol(token.Parent, cancellationToken); + if (IsSymbolObsolete(symbol)) + AddResult(token.Span); + } + else + { + var symbol = semanticModel.GetSymbolInfo(token, cancellationToken).Symbol; + if (IsSymbolObsolete(symbol)) + AddResult(token.Span); + } + } + } + + 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. + for (var i = 0; i < 5; i++) + { + if (symbol is IAliasSymbol alias) + { + symbol = alias.Target; + continue; + } + + if (symbol is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T, TypeArguments: [var valueType] }) + { + symbol = valueType; + continue; + } + + return symbol?.IsObsolete() ?? false; + } + + // Unable to determine whether the symbol is considered obsolete + return false; + } +} diff --git a/src/Workspaces/Core/Portable/ObsoleteSymbol/IObsoleteSymbolService.cs b/src/Workspaces/Core/Portable/ObsoleteSymbol/IObsoleteSymbolService.cs new file mode 100644 index 0000000000000..7e1d65363baff --- /dev/null +++ b/src/Workspaces/Core/Portable/ObsoleteSymbol/IObsoleteSymbolService.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.ObsoleteSymbol; + +/// +/// Service which can analyze a span of a document and identify all locations of declarations or references to +/// symbols which are marked . +/// +internal interface IObsoleteSymbolService : ILanguageService +{ + Task> GetLocationsAsync(Document document, ImmutableArray textSpans, CancellationToken cancellationToken); +} diff --git a/src/Workspaces/Core/Portable/Tags/WellKnownTags.cs b/src/Workspaces/Core/Portable/Tags/WellKnownTags.cs index 13b86fe85581e..4552c97465f7d 100644 --- a/src/Workspaces/Core/Portable/Tags/WellKnownTags.cs +++ b/src/Workspaces/Core/Portable/Tags/WellKnownTags.cs @@ -50,6 +50,8 @@ public static class WellKnownTags public const string Error = nameof(Error); public const string Warning = nameof(Warning); + internal const string Deprecated = nameof(Deprecated); + internal const string StatusInformation = nameof(StatusInformation); internal const string AddReference = nameof(AddReference); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs index b03846c9f11a5..792289b5df9da 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs @@ -1681,14 +1681,23 @@ public void GetPartsOfNamedMemberInitializer(SyntaxNode node, out SyntaxNode ide expression = assignment.Right; } - public void GetPartsOfObjectCreationExpression(SyntaxNode node, out SyntaxNode type, out SyntaxNode? argumentList, out SyntaxNode? initializer) + public void GetPartsOfObjectCreationExpression(SyntaxNode node, out SyntaxToken keyword, out SyntaxNode type, out SyntaxNode? argumentList, out SyntaxNode? initializer) { var objectCreationExpression = (ObjectCreationExpressionSyntax)node; + keyword = objectCreationExpression.NewKeyword; type = objectCreationExpression.Type; argumentList = objectCreationExpression.ArgumentList; initializer = objectCreationExpression.Initializer; } + public void GetPartsOfImplicitObjectCreationExpression(SyntaxNode node, out SyntaxToken keyword, out SyntaxNode argumentList, out SyntaxNode? initializer) + { + var implicitObjectCreationExpression = (ImplicitObjectCreationExpressionSyntax)node; + keyword = implicitObjectCreationExpression.NewKeyword; + argumentList = implicitObjectCreationExpression.ArgumentList; + initializer = implicitObjectCreationExpression.Initializer; + } + public void GetPartsOfParameter(SyntaxNode node, out SyntaxToken identifier, out SyntaxNode? @default) { var parameter = (ParameterSyntax)node; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs index 969b8f3878681..3fc36df6dda1b 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFacts.cs @@ -528,7 +528,8 @@ void GetPartsOfTupleExpression(SyntaxNode node, void GetPartsOfIsPatternExpression(SyntaxNode node, out SyntaxNode left, out SyntaxToken isToken, out SyntaxNode right); void GetPartsOfMemberAccessExpression(SyntaxNode node, out SyntaxNode expression, out SyntaxToken operatorToken, out SyntaxNode name); void GetPartsOfNamedMemberInitializer(SyntaxNode node, out SyntaxNode name, out SyntaxNode expression); - void GetPartsOfObjectCreationExpression(SyntaxNode node, out SyntaxNode type, out SyntaxNode? argumentList, out SyntaxNode? initializer); + void GetPartsOfObjectCreationExpression(SyntaxNode node, out SyntaxToken keyword, out SyntaxNode type, out SyntaxNode? argumentList, out SyntaxNode? initializer); + void GetPartsOfImplicitObjectCreationExpression(SyntaxNode node, out SyntaxToken keyword, out SyntaxNode argumentList, out SyntaxNode? initializer); void GetPartsOfParameter(SyntaxNode node, out SyntaxToken identifier, out SyntaxNode? @default); void GetPartsOfParenthesizedExpression(SyntaxNode node, out SyntaxToken openParen, out SyntaxNode expression, out SyntaxToken closeParen); void GetPartsOfPrefixUnaryExpression(SyntaxNode node, out SyntaxToken operatorToken, out SyntaxNode operand); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFactsExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFactsExtensions.cs index 29deeaeb60fc6..3f2af4340e6e9 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFactsExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFactsExtensions.cs @@ -595,7 +595,7 @@ public static SeparatedSyntaxList GetTypeArgumentsOfGenericName(this public static SyntaxNode GetTypeOfObjectCreationExpression(this ISyntaxFacts syntaxFacts, SyntaxNode node) { - syntaxFacts.GetPartsOfObjectCreationExpression(node, out var type, out _, out _); + syntaxFacts.GetPartsOfObjectCreationExpression(node, out _, out var type, out _, out _); return type; } @@ -648,7 +648,7 @@ public static bool IsTypeOfObjectCreationExpression(this ISyntaxFacts syntaxFact if (!syntaxFacts.IsObjectCreationExpression(parent)) return false; - syntaxFacts.GetPartsOfObjectCreationExpression(parent, out var type, out _, out _); + syntaxFacts.GetPartsOfObjectCreationExpression(parent, out _, out var type, out _, out _); return type == node; } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb index 0bb0d7c271698..a805b26656095 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SyntaxFacts/VisualBasicSyntaxFacts.vb @@ -1895,13 +1895,18 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.LanguageService expression = namedField.Expression End Sub - Public Sub GetPartsOfObjectCreationExpression(node As SyntaxNode, ByRef type As SyntaxNode, ByRef argumentList As SyntaxNode, ByRef initializer As SyntaxNode) Implements ISyntaxFacts.GetPartsOfObjectCreationExpression + Public Sub GetPartsOfObjectCreationExpression(node As SyntaxNode, ByRef keyword As SyntaxToken, ByRef type As SyntaxNode, ByRef argumentList As SyntaxNode, ByRef initializer As SyntaxNode) Implements ISyntaxFacts.GetPartsOfObjectCreationExpression Dim objectCreationExpression = DirectCast(node, ObjectCreationExpressionSyntax) + keyword = objectCreationExpression.NewKeyword type = objectCreationExpression.Type argumentList = objectCreationExpression.ArgumentList initializer = objectCreationExpression.Initializer End Sub + Public Sub GetPartsOfImplicitObjectCreationExpression(node As SyntaxNode, ByRef keyword As SyntaxToken, ByRef argumentList As SyntaxNode, ByRef initializer As SyntaxNode) Implements ISyntaxFacts.GetPartsOfImplicitObjectCreationExpression + Throw New InvalidOperationException(DoesNotExistInVBErrorMessage) + End Sub + Public Sub GetPartsOfParameter(node As SyntaxNode, ByRef identifier As SyntaxToken, ByRef [default] As SyntaxNode) Implements ISyntaxFacts.GetPartsOfParameter Dim parameter = DirectCast(node, ParameterSyntax) identifier = parameter.Identifier.Identifier diff --git a/src/Workspaces/VisualBasic/Portable/ObsoleteSymbol/VisualBasicObsoleteSymbolService.vb b/src/Workspaces/VisualBasic/Portable/ObsoleteSymbol/VisualBasicObsoleteSymbolService.vb new file mode 100644 index 0000000000000..f20879b1fdce4 --- /dev/null +++ b/src/Workspaces/VisualBasic/Portable/ObsoleteSymbol/VisualBasicObsoleteSymbolService.vb @@ -0,0 +1,57 @@ +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. +' See the LICENSE file in the project root for more information. + +Imports System.Composition +Imports System.Threading +Imports Microsoft.CodeAnalysis.Host.Mef +Imports Microsoft.CodeAnalysis.ObsoleteSymbol +Imports Microsoft.CodeAnalysis.PooledObjects +Imports Microsoft.CodeAnalysis.Text +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax + +Namespace Microsoft.CodeAnalysis.VisualBasic.ObsoleteSymbol + + <[Shared]> + Friend Class VisualBasicObsoleteSymbolService + Inherits AbstractObsoleteSymbolService + + + + Public Sub New() + MyBase.New(SyntaxKind.DimKeyword) + End Sub + + Protected Overrides Sub ProcessDimKeyword(ByRef result As ArrayBuilder(Of TextSpan), semanticModel As SemanticModel, token As SyntaxToken, cancellationToken As CancellationToken) + Dim localDeclaration = TryCast(token.Parent, LocalDeclarationStatementSyntax) + If localDeclaration Is Nothing Then + Return + End If + + If localDeclaration.Declarators.Count <> 1 Then + Return + End If + + Dim declarator = localDeclaration.Declarators(0) + If declarator.AsClause IsNot Nothing Then + ' This is an explicitly typed variable, so no need to mark 'Dim' obsolete + Return + End If + + If declarator.Names.Count <> 1 Then + ' More than one variable is declared + Return + End If + + ' Only one variable is declared + Dim localSymbol = TryCast(semanticModel.GetDeclaredSymbol(declarator.Names(0), cancellationToken), ILocalSymbol) + If IsSymbolObsolete(localSymbol?.Type) Then + If result Is Nothing Then + result = ArrayBuilder(Of TextSpan).GetInstance() + End If + + result.Add(token.Span) + End If + End Sub + End Class +End Namespace