提交 23872bf6 编写于 作者: M Manish Vasani

This change enables "Remove Suppression(s)" context menu command in the error...

This change enables "Remove Suppression(s)" context menu command in the error list to enable bulk selecting suppressed issues and removing the suppressions on them.
上级 515c3f96
......@@ -5,13 +5,16 @@
using System.ComponentModel.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes.Suppression;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Shell.TableControl;
using Microsoft.VisualStudio.Shell.TableManager;
using Roslyn.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Implementation.TableDataSource
{
......@@ -135,9 +138,15 @@ private static bool EntrySupportsSuppressionState(ITableEntryHandle entryHandle,
{
int index;
var roslynSnapshot = GetEntriesSnapshot(entryHandle, out index);
if (roslynSnapshot == null)
{
isRoslynEntry = false;
isCompilerDiagnosticEntry = false;
return IsNonRoslynEntrySupportingSuppressionState(entryHandle, out isSuppressedEntry);
}
var diagnosticData = roslynSnapshot?.GetItem(index)?.Primary;
if (diagnosticData == null || !diagnosticData.HasTextSpan || SuppressionHelpers.IsNotConfigurableDiagnostic(diagnosticData))
if (!IsEntryWithConfigurableSuppressionState(diagnosticData))
{
isRoslynEntry = false;
isSuppressedEntry = false;
......@@ -151,6 +160,30 @@ private static bool EntrySupportsSuppressionState(ITableEntryHandle entryHandle,
return true;
}
private static bool IsNonRoslynEntrySupportingSuppressionState(ITableEntryHandle entryHandle, out bool isSuppressedEntry)
{
string suppressionStateValue;
if (entryHandle.TryGetValue(SuppressionStateColumnDefinition.ColumnName, out suppressionStateValue))
{
isSuppressedEntry = suppressionStateValue == ServicesVSResources.SuppressionStateSuppressed;
return true;
}
isSuppressedEntry = false;
return false;
}
/// <summary>
/// Returns true if an entry's suppression state can be modified.
/// </summary>
/// <returns></returns>
private static bool IsEntryWithConfigurableSuppressionState(DiagnosticData entry)
{
return entry != null &&
entry.HasTextSpan &&
!SuppressionHelpers.IsNotConfigurableDiagnostic(entry);
}
private static AbstractTableEntriesSnapshot<DiagnosticData> GetEntriesSnapshot(ITableEntryHandle entryHandle)
{
int index;
......@@ -171,9 +204,12 @@ private static AbstractTableEntriesSnapshot<DiagnosticData> GetEntriesSnapshot(I
/// <summary>
/// Gets <see cref="DiagnosticData"/> objects for error list entries, filtered based on the given parameters.
/// </summary>
public ImmutableArray<DiagnosticData> GetItems(bool selectedEntriesOnly, bool isAddSuppression, bool isSuppressionInSource, bool onlyCompilerDiagnostics, CancellationToken cancellationToken)
public async Task<ImmutableArray<DiagnosticData>> GetItemsAsync(bool selectedEntriesOnly, bool isAddSuppression, bool isSuppressionInSource, bool onlyCompilerDiagnostics, CancellationToken cancellationToken)
{
var builder = ImmutableArray.CreateBuilder<DiagnosticData>();
Dictionary<string, Project> projectNameToProjectMapOpt = null;
Dictionary<Project, ImmutableDictionary<string, Document>> filePathToDocumentMapOpt = null;
var entries = selectedEntriesOnly ? _tableControl.SelectedEntries : _tableControl.Entries;
foreach (var entryHandle in entries)
{
......@@ -185,28 +221,138 @@ public ImmutableArray<DiagnosticData> GetItems(bool selectedEntriesOnly, bool is
if (roslynSnapshot != null)
{
diagnosticData = roslynSnapshot.GetItem(index)?.Primary;
if (diagnosticData != null && diagnosticData.HasTextSpan)
}
else if (!isAddSuppression)
{
// For suppression removal, we also need to handle FxCop entries.
bool isSuppressedEntry;
if (!IsNonRoslynEntrySupportingSuppressionState(entryHandle, out isSuppressedEntry) ||
!isSuppressedEntry)
{
continue;
}
string errorCode = null, category = null, message = null, filePath = null, projectName = null;
int line = -1; // FxCop only supports line, not column.
var location = Location.None;
if (entryHandle.TryGetValue(StandardTableColumnDefinitions.ErrorCode, out errorCode) && !string.IsNullOrEmpty(errorCode) &&
entryHandle.TryGetValue(StandardTableColumnDefinitions.ErrorCategory, out category) && !string.IsNullOrEmpty(category) &&
entryHandle.TryGetValue(StandardTableColumnDefinitions.Text, out message) && !string.IsNullOrEmpty(message) &&
entryHandle.TryGetValue(StandardTableColumnDefinitions.ProjectName, out projectName) && !string.IsNullOrEmpty(projectName))
{
var isCompilerDiagnostic = SuppressionHelpers.IsCompilerDiagnostic(diagnosticData);
if (onlyCompilerDiagnostics && !isCompilerDiagnostic)
if (projectNameToProjectMapOpt == null)
{
projectNameToProjectMapOpt = new Dictionary<string, Project>();
foreach (var p in _workspace.CurrentSolution.Projects)
{
projectNameToProjectMapOpt[p.Name] = p;
}
}
cancellationToken.ThrowIfCancellationRequested();
Project project;
if (!projectNameToProjectMapOpt.TryGetValue(projectName, out project))
{
// bail out
continue;
}
if (isAddSuppression)
Document document = null;
var hasLocation = (entryHandle.TryGetValue(StandardTableColumnDefinitions.DocumentName, out filePath) && !string.IsNullOrEmpty(filePath)) ||
(entryHandle.TryGetValue(StandardTableColumnDefinitions.Line, out line) && line >= 0);
if (hasLocation)
{
// Compiler diagnostics can only be suppressed in source.
if (!diagnosticData.IsSuppressed &&
(isSuppressionInSource || !isCompilerDiagnostic))
if (string.IsNullOrEmpty(filePath) || line < 0)
{
builder.Add(diagnosticData);
// bail out
continue;
}
ImmutableDictionary<string, Document> filePathMap;
filePathToDocumentMapOpt = filePathToDocumentMapOpt ?? new Dictionary<Project, ImmutableDictionary<string, Document>>();
if (!filePathToDocumentMapOpt.TryGetValue(project, out filePathMap))
{
filePathMap = await GetFilePathToDocumentMapAsync(project, cancellationToken).ConfigureAwait(false);
filePathToDocumentMapOpt[project] = filePathMap;
}
if (!filePathMap.TryGetValue(filePath, out document))
{
// bail out
continue;
}
var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
var linePosition = new LinePosition(line, 0);
var linePositionSpan = new LinePositionSpan(start: linePosition, end: linePosition);
var textSpan = (await tree.GetTextAsync(cancellationToken).ConfigureAwait(false)).Lines.GetTextSpan(linePositionSpan);
location = tree.GetLocation(textSpan);
}
else if (diagnosticData.IsSuppressed)
Contract.ThrowIfNull(project);
Contract.ThrowIfFalse((document != null) == location.IsInSource);
// Create a diagnostic with correct values for fields we care about: id, category, message, isSuppressed, location
// and default values for the rest of the fields (not used by suppression fixer).
var diagnostic = Diagnostic.Create(
id: errorCode,
category: category,
message: message,
severity: DiagnosticSeverity.Warning,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
warningLevel: 1,
isSuppressed: isSuppressedEntry,
title: message,
location: location);
diagnosticData = document != null ?
DiagnosticData.Create(document, diagnostic) :
DiagnosticData.Create(project, diagnostic);
}
}
if (IsEntryWithConfigurableSuppressionState(diagnosticData))
{
var isCompilerDiagnostic = SuppressionHelpers.IsCompilerDiagnostic(diagnosticData);
if (onlyCompilerDiagnostics && !isCompilerDiagnostic)
{
continue;
}
if (isAddSuppression)
{
// Compiler diagnostics can only be suppressed in source.
if (!diagnosticData.IsSuppressed &&
(isSuppressionInSource || !isCompilerDiagnostic))
{
builder.Add(diagnosticData);
}
}
else if (diagnosticData.IsSuppressed)
{
builder.Add(diagnosticData);
}
}
}
return builder.ToImmutable();
}
private static async Task<ImmutableDictionary<string, Document>> GetFilePathToDocumentMapAsync(Project project, CancellationToken cancellationToken)
{
var builder = ImmutableDictionary.CreateBuilder<string, Document>();
foreach (var document in project.Documents)
{
cancellationToken.ThrowIfCancellationRequested();
var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
var filePath = tree.FilePath;
if (filePath != null)
{
builder.Add(filePath, document);
}
}
......
......@@ -38,9 +38,7 @@ private void AddSuppressionsCommandHandlers(IMenuCommandService menuCommandServi
AddCommand(menuCommandService, ID.RoslynCommands.AddSuppressions, delegate { }, OnAddSuppressionsStatus);
AddCommand(menuCommandService, ID.RoslynCommands.AddSuppressionsInSource, OnAddSuppressionsInSource, OnAddSuppressionsInSourceStatus);
AddCommand(menuCommandService, ID.RoslynCommands.AddSuppressionsInSuppressionFile, OnAddSuppressionsInSuppressionFile, OnAddSuppressionsInSuppressionFileStatus);
// TODO: RemoveSupressions NYI
//AddCommand(menuCommandService, ID.RoslynCommands.RemoveSuppressions, OnRemoveSuppressions, OnRemoveSuppressionsStatus);
AddCommand(menuCommandService, ID.RoslynCommands.RemoveSuppressions, OnRemoveSuppressions, OnRemoveSuppressionsStatus);
}
/// <summary>
......
......@@ -75,6 +75,17 @@ public void AddSuppressions(bool selectedErrorListEntriesOnly, bool suppressInSo
ApplySuppressionFix(shouldFixInProject, selectedErrorListEntriesOnly, isAddSuppression: true, isSuppressionInSource: suppressInSource, onlyCompilerDiagnostics: false, showPreviewChangesDialog: true);
}
public void RemoveSuppressions(bool selectedErrorListEntriesOnly, IVsHierarchy projectHierarchyOpt)
{
if (_tableControl == null)
{
return;
}
Func<Project, bool> shouldFixInProject = GetShouldFixInProjectDelegate(_workspace, projectHierarchyOpt);
ApplySuppressionFix(shouldFixInProject, selectedErrorListEntriesOnly, isAddSuppression: false, isSuppressionInSource: false, onlyCompilerDiagnostics: false, showPreviewChangesDialog: true);
}
private static Func<Project, bool> GetShouldFixInProjectDelegate(VisualStudioWorkspaceImpl workspace, IVsHierarchy projectHierarchyOpt)
{
if (projectHierarchyOpt == null)
......@@ -92,35 +103,29 @@ public void AddSuppressions(bool selectedErrorListEntriesOnly, bool suppressInSo
}
}
public void RemoveSuppressions(bool selectedErrorListEntriesOnly, IVsHierarchy projectHierarchyOpt)
{
if (_tableControl == null)
{
return;
}
// TODO
}
private void ApplySuppressionFix(Func<Project, bool> shouldFixInProject, bool selectedEntriesOnly, bool isAddSuppression, bool isSuppressionInSource, bool onlyCompilerDiagnostics, bool showPreviewChangesDialog)
{
ImmutableDictionary<Document, ImmutableArray<Diagnostic>> diagnosticsToFixMap = null;
var waitDialogAndPreviewChangesTitle = isAddSuppression ? ServicesVSResources.SuppressMultipleOccurrences : ServicesVSResources.RemoveSuppressMultipleOccurrences;
var waitDialogMessage = isAddSuppression ? ServicesVSResources.ComputingSuppressionFix : ServicesVSResources.ComputingRemoveSuppressionFix;
// Get the diagnostics to fix from the suppression state service.
var result = _waitIndicator.Wait(
ServicesVSResources.SuppressMultipleOccurrences,
ServicesVSResources.ComputingSuppressionFix,
waitDialogAndPreviewChangesTitle,
waitDialogMessage,
allowCancel: true,
action: waitContext =>
{
try
{
var diagnosticsToFix = _suppressionStateService.GetItems(
var diagnosticsToFix = _suppressionStateService.GetItemsAsync(
selectedEntriesOnly,
isAddSuppression,
isSuppressionInSource,
onlyCompilerDiagnostics,
waitContext.CancellationToken);
waitContext.CancellationToken)
.WaitAndGetResult(waitContext.CancellationToken);
if (diagnosticsToFix.IsEmpty)
{
......@@ -132,6 +137,7 @@ private void ApplySuppressionFix(Func<Project, bool> shouldFixInProject, bool se
}
catch (OperationCanceledException)
{
diagnosticsToFixMap = null;
}
});
......@@ -142,7 +148,11 @@ private void ApplySuppressionFix(Func<Project, bool> shouldFixInProject, bool se
return;
}
var equivalenceKey = isSuppressionInSource ? FeaturesResources.SuppressWithPragma : FeaturesResources.SuppressWithGlobalSuppressMessage;
// Equivalence key determines what fix will be applied.
// Make sure we don't include any specific diagnostic ID, as we want all of the given diagnostics (which can have varied ID) to be fixed.
var equivalenceKey = isAddSuppression ?
(isSuppressionInSource ? FeaturesResources.SuppressWithPragma : FeaturesResources.SuppressWithGlobalSuppressMessage) :
FeaturesResources.RemoveSuppressionEquivalenceKeyPrefix;
// We have different suppression fixers for every language.
// So we need to group diagnostics by the containing project language and apply fixes separately.
......@@ -155,8 +165,20 @@ private void ApplySuppressionFix(Func<Project, bool> shouldFixInProject, bool se
foreach (var group in groups)
{
var language = group.Key;
var waitDialogAndPreviewChangesTitle = hasMultipleLangauges ? string.Format(ServicesVSResources.SuppressMultipleOccurrencesForLanguage, language) : ServicesVSResources.SuppressMultipleOccurrences;
var waitDialogMessage = hasMultipleLangauges ? string.Format(ServicesVSResources.ComputingSuppressionFixForLanguage, language) : ServicesVSResources.ComputingSuppressionFix;
if (hasMultipleLangauges)
{
// Change the dialog title and wait message appropriately.
if (isAddSuppression)
{
waitDialogAndPreviewChangesTitle = string.Format(ServicesVSResources.SuppressMultipleOccurrencesForLanguage, language);
waitDialogMessage = string.Format(ServicesVSResources.ComputingSuppressionFixForLanguage, language);
}
else
{
waitDialogAndPreviewChangesTitle = string.Format(ServicesVSResources.RemoveSuppressMultipleOccurrencesForLanguage, language);
waitDialogMessage = string.Format(ServicesVSResources.ComputingRemoveSuppressionFixForLanguage, language);
}
}
ImmutableDictionary<Document, ImmutableArray<Diagnostic>> documentDiagnosticsPerLanguage = null;
CodeFixProvider suppressionFixer = null;
......
......@@ -132,6 +132,24 @@ internal class ServicesVSResources {
}
}
/// <summary>
/// Looks up a localized string similar to Computing remove suppressions fix....
/// </summary>
internal static string ComputingRemoveSuppressionFix {
get {
return ResourceManager.GetString("ComputingRemoveSuppressionFix", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Computing remove suppressions fix (&apos;{0}&apos;)....
/// </summary>
internal static string ComputingRemoveSuppressionFixForLanguage {
get {
return ResourceManager.GetString("ComputingRemoveSuppressionFixForLanguage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Computing suppressions fix....
/// </summary>
......@@ -975,6 +993,24 @@ internal class ServicesVSResources {
}
}
/// <summary>
/// Looks up a localized string similar to Remove suppressions.
/// </summary>
internal static string RemoveSuppressMultipleOccurrences {
get {
return ResourceManager.GetString("RemoveSuppressMultipleOccurrences", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Remove suppressions (&apos;{0}&apos;).
/// </summary>
internal static string RemoveSuppressMultipleOccurrencesForLanguage {
get {
return ResourceManager.GetString("RemoveSuppressMultipleOccurrencesForLanguage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Resetting Interactive.
/// </summary>
......
......@@ -510,4 +510,16 @@ Use the dropdown to view and switch to other projects this file may belong to.</
<data name="ComputingSuppressionFixForLanguage" xml:space="preserve">
<value>Computing suppressions fix ('{0}')...</value>
</data>
<data name="RemoveSuppressMultipleOccurrences" xml:space="preserve">
<value>Remove suppressions</value>
</data>
<data name="RemoveSuppressMultipleOccurrencesForLanguage" xml:space="preserve">
<value>Remove suppressions ('{0}')</value>
</data>
<data name="ComputingRemoveSuppressionFix" xml:space="preserve">
<value>Computing remove suppressions fix...</value>
</data>
<data name="ComputingRemoveSuppressionFixForLanguage" xml:space="preserve">
<value>Computing remove suppressions fix ('{0}')...</value>
</data>
</root>
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册