diff --git a/src/EditorFeatures/CSharpTest/RemoveUnnecessaryParentheses/RemoveUnnecessaryParenthesesTests.cs b/src/EditorFeatures/CSharpTest/RemoveUnnecessaryParentheses/RemoveUnnecessaryParenthesesTests.cs index d2b74a049aa4b..4abd7e10d7473 100644 --- a/src/EditorFeatures/CSharpTest/RemoveUnnecessaryParentheses/RemoveUnnecessaryParenthesesTests.cs +++ b/src/EditorFeatures/CSharpTest/RemoveUnnecessaryParentheses/RemoveUnnecessaryParenthesesTests.cs @@ -39,10 +39,10 @@ internal override bool ShouldSkipMessageDescriptionVerification(DiagnosticDescri } private DiagnosticDescription GetRemoveUnnecessaryParenthesesDiagnostic(string text, int line, int column) - { - var diagnosticId = IDEDiagnosticIds.RemoveUnnecessaryParenthesesDiagnosticId; - return TestHelpers.Diagnostic(diagnosticId, text, startLocation: new LinePosition(line, column)); - } + => TestHelpers.Diagnostic(IDEDiagnosticIds.RemoveUnnecessaryParenthesesDiagnosticId, text, startLocation: new LinePosition(line, column)); + + private DiagnosticDescription GetRemoveUnnecessaryParenthesesDiagnostic(string text, int line, int column, DiagnosticSeverity severity) + => GetRemoveUnnecessaryParenthesesDiagnostic(text, line, column).WithEffectiveSeverity(severity); [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsRemoveUnnecessaryParentheses)] public async Task TestVariableInitializer_TestWithAllOptionsSetToIgnore() @@ -2298,9 +2298,7 @@ void Test(bool a) [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsRemoveUnnecessaryParentheses)] public async Task TestUnnecessaryParenthesisDiagnosticSingleLineExpression() { - var openParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(", 4, 16); var parentheticalExpressionDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(1 + 2)", 4, 16); - var closeParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic(")", 4, 22); await TestDiagnosticsAsync( @"class C { @@ -2308,16 +2306,14 @@ void M() { int x = [|(1 + 2)|]; } -}", new TestParameters(options: RemoveAllUnnecessaryParentheses), parentheticalExpressionDiagnostic, openParenthesesDiagnostic, closeParenthesesDiagnostic); +}", new TestParameters(options: RemoveAllUnnecessaryParentheses), parentheticalExpressionDiagnostic); } [WorkItem(27925, "https://github.com/dotnet/roslyn/issues/27925")] [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsRemoveUnnecessaryParentheses)] public async Task TestUnnecessaryParenthesisDiagnosticInMultiLineExpression() { - var openParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(", 4, 16); var firstLineParentheticalExpressionDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(1 +", 4, 16); - var closeParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic(")", 5, 13); await TestDiagnosticsAsync( @"class C { @@ -2326,21 +2322,16 @@ void M() int x = [|(1 + 2)|]; } -}", new TestParameters(options: RemoveAllUnnecessaryParentheses), firstLineParentheticalExpressionDiagnostic, openParenthesesDiagnostic, closeParenthesesDiagnostic); +}", new TestParameters(options: RemoveAllUnnecessaryParentheses), firstLineParentheticalExpressionDiagnostic); } [WorkItem(27925, "https://github.com/dotnet/roslyn/issues/27925")] [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsRemoveUnnecessaryParentheses)] public async Task TestUnnecessaryParenthesisDiagnosticInNestedExpression() { - var outerOpenParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(", 4, 16); var outerParentheticalExpressionDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(1 + (2 + 3) + 4)", 4, 16); - var outerCloseParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic(")", 4, 32); - var innerOpenParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(", 4, 21); var innerParentheticalExpressionDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(2 + 3)", 4, 21); - var innerCloseParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic(")", 4, 27); - var expectedDiagnostics = new DiagnosticDescription[] { outerParentheticalExpressionDiagnostic, outerOpenParenthesesDiagnostic, - outerCloseParenthesesDiagnostic, innerParentheticalExpressionDiagnostic, innerOpenParenthesesDiagnostic, innerCloseParenthesesDiagnostic }; + var expectedDiagnostics = new DiagnosticDescription[] { outerParentheticalExpressionDiagnostic, innerParentheticalExpressionDiagnostic }; await TestDiagnosticsAsync( @"class C { @@ -2355,14 +2346,9 @@ void M() [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsRemoveUnnecessaryParentheses)] public async Task TestUnnecessaryParenthesisDiagnosticInNestedMultiLineExpression() { - var outerOpenParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(", 4, 16); var outerFirstLineParentheticalExpressionDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(1 + 2 +", 4, 16); - var outerCloseParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic(")", 6, 17); - var innerOpenParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(", 5, 12); var innerParentheticalExpressionDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(3 + 4)", 5, 12); - var innerCloseParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic(")", 5, 18); - var expectedDiagnostics = new DiagnosticDescription[] { outerFirstLineParentheticalExpressionDiagnostic, outerOpenParenthesesDiagnostic, - outerCloseParenthesesDiagnostic, innerParentheticalExpressionDiagnostic, innerOpenParenthesesDiagnostic, innerCloseParenthesesDiagnostic }; + var expectedDiagnostics = new DiagnosticDescription[] { outerFirstLineParentheticalExpressionDiagnostic, innerParentheticalExpressionDiagnostic }; await TestDiagnosticsAsync( @"class C { @@ -2375,6 +2361,33 @@ void M() }", new TestParameters(options: RemoveAllUnnecessaryParentheses), expectedDiagnostics); } + [WorkItem(39529, "https://github.com/dotnet/roslyn/issues/39529")] + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsRemoveUnnecessaryParentheses)] + public async Task TestUnnecessaryParenthesisIncludesFadeLocations() + { + var input = @"class C +{ + void M() + { + int x = [|{|expression:{|fade:(|}1 + 2{|fade:)|}|}|]; + } +}"; + + var parameters = new TestParameters(options: RemoveAllUnnecessaryParentheses); + using var workspace = CreateWorkspaceFromOptions(input, parameters); + var expectedSpans = workspace.Documents.First().AnnotatedSpans; + + var diagnostics = await GetDiagnosticsAsync(workspace, parameters).ConfigureAwait(false); + var diagnostic = diagnostics.Single(); + + Assert.Equal(3, diagnostic.AdditionalLocations.Count); + Assert.Equal(expectedSpans["expression"].Single(), diagnostic.AdditionalLocations[0].SourceSpan); + Assert.Equal(expectedSpans["fade"][0], diagnostic.AdditionalLocations[1].SourceSpan); + Assert.Equal(expectedSpans["fade"][1], diagnostic.AdditionalLocations[2].SourceSpan); + + Assert.Equal("[1,2]", diagnostic.Properties[WellKnownDiagnosticTags.Unnecessary]); + } + [WorkItem(27925, "https://github.com/dotnet/roslyn/issues/39363")] [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsRemoveUnnecessaryParentheses)] public async Task TestUnnecessaryParenthesesInSwitchExpression() diff --git a/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsTaggerProvider.cs b/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsTaggerProvider.cs index 2da2df9888fbc..8cbe3e875bdde 100644 --- a/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsTaggerProvider.cs +++ b/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsTaggerProvider.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Immutable; +using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -106,6 +108,14 @@ protected override ITaggerEventSource CreateEventSource(ITextView textViewOpt, I protected internal abstract bool IncludeDiagnostic(DiagnosticData data); protected internal abstract ITagSpan CreateTagSpan(bool isLiveUpdate, SnapshotSpan span, DiagnosticData data); + /// + /// Get the that should have the tag applied to it. + /// In most cases, this is the but overrides can change it (e.g. unnecessary classifications). + /// + /// the diagnostic containing the location(s). + /// an array of locations that should have the tag applied. + protected internal virtual ImmutableArray GetLocationsToTag(DiagnosticData diagnosticData) => ImmutableArray.Create(diagnosticData.DataLocation); + protected override Task ProduceTagsAsync(TaggerContext context, DocumentSnapshotSpan spanToTag, int? caretPosition) { ProduceTags(context, spanToTag); @@ -198,17 +208,17 @@ private void ProduceTags( // So we'll eventually reach a point where the diagnostics exactly match the // editorSnapshot. - var diagnosticSpan = diagnosticData.GetExistingOrCalculatedTextSpan(sourceText) - .ToSnapshotSpan(diagnosticSnapshot) - .TranslateTo(editorSnapshot, SpanTrackingMode.EdgeExclusive); - - if (diagnosticSpan.IntersectsWith(requestedSpan) && - !IsSuppressed(suppressedDiagnosticsSpans, diagnosticSpan)) + var diagnosticSpans = this.GetLocationsToTag(diagnosticData) + .Select(location => GetDiagnosticSnapshotSpan(location, diagnosticSnapshot, editorSnapshot, sourceText)); + foreach (var diagnosticSpan in diagnosticSpans) { - var tagSpan = this.CreateTagSpan(isLiveUpdate, diagnosticSpan, diagnosticData); - if (tagSpan != null) + if (diagnosticSpan.IntersectsWith(requestedSpan) && !IsSuppressed(suppressedDiagnosticsSpans, diagnosticSpan)) { - context.AddTag(tagSpan); + var tagSpan = this.CreateTagSpan(isLiveUpdate, diagnosticSpan, diagnosticData); + if (tagSpan != null) + { + context.AddTag(tagSpan); + } } } } @@ -221,6 +231,14 @@ private void ProduceTags( // stop crashing on such occations return; } + + static SnapshotSpan GetDiagnosticSnapshotSpan(DiagnosticDataLocation diagnosticDataLocation, ITextSnapshot diagnosticSnapshot, + ITextSnapshot editorSnapshot, SourceText sourceText) + { + return DiagnosticData.GetExistingOrCalculatedTextSpan(diagnosticDataLocation, sourceText) + .ToSnapshotSpan(diagnosticSnapshot) + .TranslateTo(editorSnapshot, SpanTrackingMode.EdgeExclusive); + } } private bool IsSuppressed(NormalizedSnapshotSpanCollection suppressedSpans, SnapshotSpan span) diff --git a/src/EditorFeatures/Core/Implementation/Diagnostics/DiagnosticsClassificationTaggerProvider.cs b/src/EditorFeatures/Core/Implementation/Diagnostics/DiagnosticsClassificationTaggerProvider.cs index 18e9937c0ad99..34a909834c71e 100644 --- a/src/EditorFeatures/Core/Implementation/Diagnostics/DiagnosticsClassificationTaggerProvider.cs +++ b/src/EditorFeatures/Core/Implementation/Diagnostics/DiagnosticsClassificationTaggerProvider.cs @@ -2,10 +2,16 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel.Composition; +using System.IO; +using System.Linq; +using System.Runtime.Serialization.Json; +using System.Text; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Editor.Shared.Options; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.Options; @@ -57,5 +63,41 @@ protected internal override bool IncludeDiagnostic(DiagnosticData data) => protected internal override ITagSpan CreateTagSpan(bool isLiveUpdate, SnapshotSpan span, DiagnosticData data) => new TagSpan(span, _classificationTag); + + protected internal override ImmutableArray GetLocationsToTag(DiagnosticData diagnosticData) + { + using var locationsToTagDisposer = PooledObjects.ArrayBuilder.GetInstance(out var locationsToTag); + + // If there are 'unnecessary' locations specified in the property bag, use those instead of the main diagnostic location. + if (diagnosticData.AdditionalLocations?.Count > 0 + && diagnosticData.Properties != null + && diagnosticData.Properties.TryGetValue(WellKnownDiagnosticTags.Unnecessary, out var unnecessaryIndices)) + { + var additionalLocations = diagnosticData.AdditionalLocations.ToImmutableArray(); + var indices = GetLocationIndices(unnecessaryIndices); + locationsToTag.AddRange(indices.Select(i => additionalLocations[i]).ToImmutableArray()); + } + else + { + locationsToTag.Add(diagnosticData.DataLocation); + } + + return locationsToTag.ToImmutable(); + + static IEnumerable GetLocationIndices(string indicesProperty) + { + try + { + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(indicesProperty)); + var serializer = new DataContractJsonSerializer(typeof(IEnumerable)); + var result = serializer.ReadObject(stream) as IEnumerable; + return result; + } + catch (Exception e) when (FatalError.ReportWithoutCrash(e)) + { + return ImmutableArray.Empty; + } + } + } } } diff --git a/src/EditorFeatures/Test/Diagnostics/DiagnosticDataTests.cs b/src/EditorFeatures/Test/Diagnostics/DiagnosticDataTests.cs index d73331ee5ebbd..05b0fb4e4bbb6 100644 --- a/src/EditorFeatures/Test/Diagnostics/DiagnosticDataTests.cs +++ b/src/EditorFeatures/Test/Diagnostics/DiagnosticDataTests.cs @@ -119,7 +119,7 @@ private static async Task VerifyTextSpanAsync(string code, int startLine, int st language: document.Project.Language); var text = await document.GetTextAsync(); - var actual = data.GetExistingOrCalculatedTextSpan(text); + var actual = DiagnosticData.GetExistingOrCalculatedTextSpan(data.DataLocation, text); Assert.Equal(span, actual); } diff --git a/src/EditorFeatures/VisualBasicTest/RemoveUnnecessaryParentheses/RemoveUnnecessaryParenthesesTests.vb b/src/EditorFeatures/VisualBasicTest/RemoveUnnecessaryParentheses/RemoveUnnecessaryParenthesesTests.vb index c4a320093ebcc..2282eba6468a4 100644 --- a/src/EditorFeatures/VisualBasicTest/RemoveUnnecessaryParentheses/RemoveUnnecessaryParenthesesTests.vb +++ b/src/EditorFeatures/VisualBasicTest/RemoveUnnecessaryParentheses/RemoveUnnecessaryParenthesesTests.vb @@ -1,7 +1,9 @@ ' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +Imports System.Linq Imports Microsoft.CodeAnalysis.CodeFixes Imports Microsoft.CodeAnalysis.Diagnostics +Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces Imports Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.Diagnostics Imports Microsoft.CodeAnalysis.Text Imports Microsoft.CodeAnalysis.VisualBasic.RemoveUnnecessaryParentheses @@ -560,43 +562,34 @@ end class", parameters:=New TestParameters(options:=RemoveAllUnnecessaryParenthe Public Async Function TestUnnecessaryParenthesisDiagnosticSingleLineExpression() As Task - Dim openParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(", 2, 16) Dim parentheticalExpressionDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(1 + 2)", 2, 16) - Dim closeParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic(")", 2, 22) Await TestDiagnosticsAsync( "class C sub M() dim x = [|(1 + 2)|] end sub -end class", New TestParameters(options:=RemoveAllUnnecessaryParentheses), parentheticalExpressionDiagnostic, openParenthesesDiagnostic, closeParenthesesDiagnostic) +end class", New TestParameters(options:=RemoveAllUnnecessaryParentheses), parentheticalExpressionDiagnostic) End Function Public Async Function TestUnnecessaryParenthesisDiagnosticInMultiLineExpression() As Task - Dim openParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(", 2, 16) Dim firstLineParentheticalExpressionDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(1 +", 2, 16) - Dim closeParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic(")", 3, 13) Await TestDiagnosticsAsync( "class C sub M() dim x = [|(1 + 2)|] end sub -end class", New TestParameters(options:=RemoveAllUnnecessaryParentheses), firstLineParentheticalExpressionDiagnostic, openParenthesesDiagnostic, closeParenthesesDiagnostic) +end class", New TestParameters(options:=RemoveAllUnnecessaryParentheses), firstLineParentheticalExpressionDiagnostic) End Function Public Async Function TestUnnecessaryParenthesisDiagnosticInNestedExpression() As Task - Dim outerOpenParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(", 2, 16) Dim outerParentheticalExpressionDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(1 + (2 + 3) + 4)", 2, 16) - Dim outerCloseParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic(")", 2, 32) - Dim innerOpenParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(", 2, 21) Dim innerParentheticalExpressionDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(2 + 3)", 2, 21) - Dim innerCloseParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic(")", 2, 27) - Dim expectedDiagnostics = New DiagnosticDescription() {outerParentheticalExpressionDiagnostic, outerOpenParenthesesDiagnostic, - outerCloseParenthesesDiagnostic, innerParentheticalExpressionDiagnostic, innerOpenParenthesesDiagnostic, innerCloseParenthesesDiagnostic} + Dim expectedDiagnostics = New DiagnosticDescription() {outerParentheticalExpressionDiagnostic, innerParentheticalExpressionDiagnostic} Await TestDiagnosticsAsync( "class C sub M() @@ -608,14 +601,9 @@ end class", New TestParameters(options:=RemoveAllUnnecessaryParentheses), expect Public Async Function TestUnnecessaryParenthesisDiagnosticInNestedMultiLineExpression() As Task - Dim outerOpenParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(", 2, 16) Dim outerFirstLineParentheticalExpressionDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(1 + 2 +", 2, 16) - Dim outerCloseParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic(")", 4, 17) - Dim innerOpenParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(", 3, 12) Dim innerParentheticalExpressionDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic("(3 + 4)", 3, 12) - Dim innerCloseParenthesesDiagnostic = GetRemoveUnnecessaryParenthesesDiagnostic(")", 3, 18) - Dim expectedDiagnostics = New DiagnosticDescription() {outerFirstLineParentheticalExpressionDiagnostic, outerOpenParenthesesDiagnostic, - outerCloseParenthesesDiagnostic, innerParentheticalExpressionDiagnostic, innerOpenParenthesesDiagnostic, innerCloseParenthesesDiagnostic} + Dim expectedDiagnostics = New DiagnosticDescription() {outerFirstLineParentheticalExpressionDiagnostic, innerParentheticalExpressionDiagnostic} Await TestDiagnosticsAsync( "class C sub M() @@ -625,5 +613,32 @@ end class", New TestParameters(options:=RemoveAllUnnecessaryParentheses), expect end sub end class", New TestParameters(options:=RemoveAllUnnecessaryParentheses), expectedDiagnostics) End Function + + + + Public Async Function TestUnnecessaryParenthesisIncludesFadeLocations() As Task + Dim input = +"class C + sub M() + dim x = [|{|expression:{|fade:(|}1 + 2{|fade:)|}|}|] + end sub +end class" + + Dim parameters = New TestParameters(options:=RemoveAllUnnecessaryParentheses) + Using workspace As TestWorkspace = CreateWorkspaceFromOptions(input, parameters) + Dim expectedSpans = workspace.Documents.First().AnnotatedSpans + + Dim diagnostics = Await GetDiagnosticsAsync(workspace, parameters).ConfigureAwait(False) + Dim diagnostic = diagnostics.Single() + + Assert.Equal(3, diagnostic.AdditionalLocations.Count) + Assert.Equal(expectedSpans.Item("expression").Item(0), diagnostic.AdditionalLocations.Item(0).SourceSpan) + Assert.Equal(expectedSpans.Item("fade").Item(0), diagnostic.AdditionalLocations.Item(1).SourceSpan) + Assert.Equal(expectedSpans.Item("fade").Item(1), diagnostic.AdditionalLocations.Item(2).SourceSpan) + + Assert.Equal("[1,2]", diagnostic.Properties.Item(WellKnownDiagnosticTags.Unnecessary)) + End Using + + End Function End Class End Namespace diff --git a/src/Features/Core/Portable/Diagnostics/DiagnosticHelper.cs b/src/Features/Core/Portable/Diagnostics/DiagnosticHelper.cs index 709dd85a8c7f1..c12c75636af0e 100644 --- a/src/Features/Core/Portable/Diagnostics/DiagnosticHelper.cs +++ b/src/Features/Core/Portable/Diagnostics/DiagnosticHelper.cs @@ -3,8 +3,12 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.IO; using System.Linq; +using System.Runtime.Serialization.Json; +using System.Text; using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.RemoveUnnecessaryParentheses; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Diagnostics @@ -55,6 +59,53 @@ public static Diagnostic Create( return CreateWithMessage(descriptor, location, effectiveSeverity, additionalLocations, properties, message); } + /// + /// Create a diagnostic that adds properties specifying a tag for a set of locations. + /// + /// A describing the diagnostic. + /// An optional primary location of the diagnostic. If null, will return . + /// Effective severity of the diagnostic. + /// + /// An optional set of additional locations related to the diagnostic. + /// Typically, these are locations of other items referenced in the message. + /// If null, will return an empty list. + /// + /// + /// a map of location tag to index in additional locations. + /// for an example of usage. + /// + /// Arguments to the message of the diagnostic. + /// The instance. + public static Diagnostic CreateWithLocationTags( + DiagnosticDescriptor descriptor, + Location location, + ReportDiagnostic effectiveSeverity, + IEnumerable additionalLocations, + IDictionary> tagIndices, + params object[] messageArgs) + { + Contract.ThrowIfTrue(additionalLocations.IsEmpty()); + Contract.ThrowIfTrue(tagIndices.IsEmpty()); + + var properties = tagIndices.Select(kvp => new KeyValuePair(kvp.Key, EncodeIndices(kvp.Value, additionalLocations.Count()))).ToImmutableDictionary(); + + return Create(descriptor, location, effectiveSeverity, additionalLocations, properties, messageArgs); + + static string EncodeIndices(IEnumerable indices, int additionalLocationsLength) + { + // Ensure that the provided tag index is a valid index into additional locations. + Contract.ThrowIfFalse(indices.All(idx => idx >= 0 && idx < additionalLocationsLength)); + + using var stream = new MemoryStream(); + var serializer = new DataContractJsonSerializer(typeof(IEnumerable)); + serializer.WriteObject(stream, indices); + + var jsonBytes = stream.ToArray(); + stream.Close(); + return Encoding.UTF8.GetString(jsonBytes, 0, jsonBytes.Length); + } + } + /// /// Creates a instance. /// diff --git a/src/Features/Core/Portable/RemoveUnnecessaryParentheses/AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer.cs b/src/Features/Core/Portable/RemoveUnnecessaryParentheses/AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer.cs index 138484ab9799f..29ce18bee3e84 100644 --- a/src/Features/Core/Portable/RemoveUnnecessaryParentheses/AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer.cs +++ b/src/Features/Core/Portable/RemoveUnnecessaryParentheses/AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Threading; @@ -20,25 +21,24 @@ internal abstract class AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer< { /// - /// A diagnostic descriptor that will fade the span (but not put a message or squiggle). + /// A diagnostic descriptor used to squiggle and message the span. /// - private static readonly DiagnosticDescriptor s_diagnosticWithFade = CreateDescriptorWithId( + private static readonly DiagnosticDescriptor s_diagnosticDescriptor = CreateDescriptorWithId( IDEDiagnosticIds.RemoveUnnecessaryParenthesesDiagnosticId, new LocalizableResourceString(nameof(FeaturesResources.Remove_unnecessary_parentheses), FeaturesResources.ResourceManager, typeof(FeaturesResources)), - string.Empty, + new LocalizableResourceString(nameof(FeaturesResources.Parentheses_can_be_removed), FeaturesResources.ResourceManager, typeof(FeaturesResources)), isUnneccessary: true); /// - /// A diagnostic descriptor used to squiggle and message the span, but will not fade. + /// This analyzer inserts the fade locations into indices 1 and 2 inside additional locations. /// - private static readonly DiagnosticDescriptor s_diagnosticWithoutFade = CreateDescriptorWithId( - IDEDiagnosticIds.RemoveUnnecessaryParenthesesDiagnosticId, - new LocalizableResourceString(nameof(FeaturesResources.Remove_unnecessary_parentheses), FeaturesResources.ResourceManager, typeof(FeaturesResources)), - new LocalizableResourceString(nameof(FeaturesResources.Parentheses_can_be_removed), FeaturesResources.ResourceManager, typeof(FeaturesResources)), - isUnneccessary: false); + private static readonly ImmutableDictionary> s_fadeLocations = new Dictionary> + { + { nameof(WellKnownDiagnosticTags.Unnecessary), new int[] { 1, 2 } }, + }.ToImmutableDictionary(); protected AbstractRemoveUnnecessaryParenthesesDiagnosticAnalyzer() - : base(ImmutableArray.Create(s_diagnosticWithFade, s_diagnosticWithoutFade)) + : base(ImmutableArray.Create(s_diagnosticDescriptor)) { } @@ -124,21 +124,15 @@ private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) var severity = preference.Notification.Severity; - var additionalLocations = ImmutableArray.Create(parenthesizedExpression.GetLocation()); - - // Fades the open parentheses character and reports the suggestion. - context.ReportDiagnostic(Diagnostic.Create(s_diagnosticWithFade, parenthesizedExpression.GetFirstToken().GetLocation(), additionalLocations)); + var additionalLocations = ImmutableArray.Create(parenthesizedExpression.GetLocation(), + parenthesizedExpression.GetFirstToken().GetLocation(), parenthesizedExpression.GetLastToken().GetLocation()); - // Generates diagnostic used to squiggle the parenthetical expression. - context.ReportDiagnostic(DiagnosticHelper.Create( - s_diagnosticWithoutFade, + context.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags( + s_diagnosticDescriptor, GetDiagnosticSquiggleLocation(parenthesizedExpression, cancellationToken), severity, additionalLocations, - properties: null)); - - // Fades the close parentheses character. - context.ReportDiagnostic(Diagnostic.Create(s_diagnosticWithFade, parenthesizedExpression.GetLastToken().GetLocation(), additionalLocations)); + s_fadeLocations)); } /// diff --git a/src/VisualStudio/LiveShare/Impl/DiagnosticHandler.cs b/src/VisualStudio/LiveShare/Impl/DiagnosticHandler.cs index 329906af9b9fe..656eb7028c55d 100644 --- a/src/VisualStudio/LiveShare/Impl/DiagnosticHandler.cs +++ b/src/VisualStudio/LiveShare/Impl/DiagnosticHandler.cs @@ -94,7 +94,7 @@ private async void DiagnosticService_DiagnosticsUpdated(object sender, Diagnosti Code = diag.Id, Message = diag.Message, Severity = ProtocolConversions.DiagnosticSeverityToLspDiagnositcSeverity(diag.Severity), - Range = ProtocolConversions.TextSpanToRange(diag.GetExistingOrCalculatedTextSpan(text), text), + Range = ProtocolConversions.TextSpanToRange(DiagnosticData.GetExistingOrCalculatedTextSpan(diag.DataLocation, text), text), Tags = diag.CustomTags.Where(s => s == "Unnecessary").ToArray() }).ToArray(); } diff --git a/src/Workspaces/Core/Portable/Diagnostics/DiagnosticData.cs b/src/Workspaces/Core/Portable/Diagnostics/DiagnosticData.cs index 623011cecbd21..a77e4ccb794ab 100644 --- a/src/Workspaces/Core/Portable/Diagnostics/DiagnosticData.cs +++ b/src/Workspaces/Core/Portable/Diagnostics/DiagnosticData.cs @@ -9,7 +9,6 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; -using System.Resources; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Options; @@ -165,8 +164,17 @@ public override string ToString() DataLocation?.OriginalStartColumn); } - public TextSpan GetExistingOrCalculatedTextSpan(SourceText text) - => HasTextSpan ? EnsureInBounds(GetTextSpan(), text) : GetTextSpan(DataLocation, text); + public static TextSpan GetExistingOrCalculatedTextSpan(DiagnosticDataLocation? diagnosticLocation, SourceText text) + { + if (diagnosticLocation?.SourceSpan != null) + { + return EnsureInBounds(diagnosticLocation.SourceSpan.Value, text); + } + else + { + return GetTextSpan(diagnosticLocation, text); + } + } private static TextSpan EnsureInBounds(TextSpan textSpan, SourceText text) => TextSpan.FromBounds(