VisualStudioDiagnosticListSuppressionStateService.cs 18.6 KB
Newer Older
1 2 3 4 5 6 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.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.Linq;
using System.Threading;
8
using System.Threading.Tasks;
9 10 11
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes.Suppression;
using Microsoft.CodeAnalysis.Diagnostics;
12
using Microsoft.CodeAnalysis.Text;
13 14 15 16
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Shell.TableControl;
using Microsoft.VisualStudio.Shell.TableManager;
17
using Roslyn.Utilities;
18 19 20 21 22 23

namespace Microsoft.VisualStudio.LanguageServices.Implementation.TableDataSource
{
    /// <summary>
    /// Service to maintain information about the suppression state of specific set of items in the error list.
    /// </summary>
24 25
    [Export(typeof(IVisualStudioDiagnosticListSuppressionStateService))]
    internal class VisualStudioDiagnosticListSuppressionStateService : IVisualStudioDiagnosticListSuppressionStateService
26 27 28 29 30 31 32 33 34
    {
        private readonly VisualStudioWorkspace _workspace;
        private readonly IVsUIShell _shellService;
        private readonly IWpfTableControl _tableControl;

        private int _selectedActiveItems;
        private int _selectedSuppressedItems;
        private int _selectedRoslynItems;
        private int _selectedCompilerDiagnosticItems;
35
        private int _selectedNoLocationDiagnosticItems;
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
        private int _selectedNonSuppressionStateItems;

        [ImportingConstructor]
        public VisualStudioDiagnosticListSuppressionStateService(
            SVsServiceProvider serviceProvider,
            VisualStudioWorkspace workspace)
        {
            _workspace = workspace;
            _shellService = (IVsUIShell)serviceProvider.GetService(typeof(SVsUIShell));
            var errorList = serviceProvider.GetService(typeof(SVsErrorList)) as IErrorList;
            _tableControl = errorList?.TableControl;

            ClearState();
            InitializeFromTableControlIfNeeded();
        }

        private int SelectedItems => _selectedActiveItems + _selectedSuppressedItems + _selectedNonSuppressionStateItems;

        // If we can suppress either in source or in suppression file, we enable suppress context menu.
        public bool CanSuppressSelectedEntries => CanSuppressSelectedEntriesInSource || CanSuppressSelectedEntriesInSuppressionFiles;

        // If only suppressed items are selected, we enable remove suppressions.
        public bool CanRemoveSuppressionsSelectedEntries => _selectedActiveItems == 0 && _selectedSuppressedItems > 0;

        // If only Roslyn active items are selected, we enable suppress in source.
        public bool CanSuppressSelectedEntriesInSource => _selectedActiveItems > 0 &&
            _selectedSuppressedItems == 0 &&
63 64
            _selectedRoslynItems == _selectedActiveItems &&
            (_selectedRoslynItems - _selectedNoLocationDiagnosticItems) > 0;
65 66 67 68 69 70 71 72 73 74 75 76 77

        // If only active items are selected, and there is at least one Roslyn item, we enable suppress in suppression file.
        // Also, compiler diagnostics cannot be suppressed in suppression file, so there must be at least one non-compiler item.
        public bool CanSuppressSelectedEntriesInSuppressionFiles => _selectedActiveItems > 0 &&
            _selectedSuppressedItems == 0 &&
            (_selectedRoslynItems - _selectedCompilerDiagnosticItems) > 0;

        private void ClearState()
        {
            _selectedActiveItems = 0;
            _selectedSuppressedItems = 0;
            _selectedRoslynItems = 0;
            _selectedCompilerDiagnosticItems = 0;
78
            _selectedNoLocationDiagnosticItems = 0;
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
            _selectedNonSuppressionStateItems = 0;
        }

        private void InitializeFromTableControlIfNeeded()
        {
            if (_tableControl == null)
            {
                return;
            }

            if (SelectedItems == _tableControl.SelectedEntries.Count())
            {
                // We already have up-to-date state data, so don't need to re-compute.
                return;
            }

            ClearState();
            if (ProcessEntries(_tableControl.SelectedEntries, added: true))
            {
                UpdateQueryStatus();
            }
        }

102 103 104
        /// <summary>
        /// Updates suppression state information when the selected entries change in the error list.
        /// </summary>
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
        public void ProcessSelectionChanged(TableSelectionChangedEventArgs e)
        {
            var hasAddedSuppressionStateEntry = ProcessEntries(e.AddedEntries, added: true);
            var hasRemovedSuppressionStateEntry = ProcessEntries(e.RemovedEntries, added: false);

            // If any entry that supports suppression state was ever involved, update query status since each item in the error list
            // can have different context menu.
            if (hasAddedSuppressionStateEntry || hasRemovedSuppressionStateEntry)
            {
                UpdateQueryStatus();
            }

            InitializeFromTableControlIfNeeded();
        }

        private bool ProcessEntries(IEnumerable<ITableEntryHandle> entryHandles, bool added)
        {
122
            bool isRoslynEntry, isSuppressedEntry, isCompilerDiagnosticEntry, isNoLocationDiagnosticEntry;
123 124 125
            var hasSuppressionStateEntry = false;
            foreach (var entryHandle in entryHandles)
            {
126
                if (EntrySupportsSuppressionState(entryHandle, out isRoslynEntry, out isSuppressedEntry, out isCompilerDiagnosticEntry, out isNoLocationDiagnosticEntry))
127 128
                {
                    hasSuppressionStateEntry = true;
129
                    HandleSuppressionStateEntry(isRoslynEntry, isSuppressedEntry, isCompilerDiagnosticEntry, isNoLocationDiagnosticEntry, added);
130 131 132 133 134 135 136 137 138 139
                }
                else
                {
                    HandleNonSuppressionStateEntry(added);
                }
            }

            return hasSuppressionStateEntry;
        }

140
        private static bool EntrySupportsSuppressionState(ITableEntryHandle entryHandle, out bool isRoslynEntry, out bool isSuppressedEntry, out bool isCompilerDiagnosticEntry, out bool isNoLocationDiagnosticEntry)
141
        {
142 143 144 145
            string filePath;
            isNoLocationDiagnosticEntry = !entryHandle.TryGetValue(StandardTableColumnDefinitions.DocumentName, out filePath) ||
                string.IsNullOrEmpty(filePath);

146 147
            int index;
            var roslynSnapshot = GetEntriesSnapshot(entryHandle, out index);
148 149 150 151 152 153
            if (roslynSnapshot == null)
            {
                isRoslynEntry = false;
                isCompilerDiagnosticEntry = false;
                return IsNonRoslynEntrySupportingSuppressionState(entryHandle, out isSuppressedEntry);
            }
154 155

            var diagnosticData = roslynSnapshot?.GetItem(index)?.Primary;
156
            if (!IsEntryWithConfigurableSuppressionState(diagnosticData))
157 158 159 160 161 162 163 164 165 166 167 168 169
            {
                isRoslynEntry = false;
                isSuppressedEntry = false;
                isCompilerDiagnosticEntry = false;
                return false;
            }

            isRoslynEntry = true;
            isSuppressedEntry = diagnosticData.IsSuppressed;
            isCompilerDiagnosticEntry = SuppressionHelpers.IsCompilerDiagnostic(diagnosticData);
            return true;
        }

170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
        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 &&
                !SuppressionHelpers.IsNotConfigurableDiagnostic(entry);
        }

193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
        private static AbstractTableEntriesSnapshot<DiagnosticData> GetEntriesSnapshot(ITableEntryHandle entryHandle)
        {
            int index;
            return GetEntriesSnapshot(entryHandle, out index);
        }

        private static AbstractTableEntriesSnapshot<DiagnosticData> GetEntriesSnapshot(ITableEntryHandle entryHandle, out int index)
        {
            ITableEntriesSnapshot snapshot;
            if (!entryHandle.TryGetSnapshot(out snapshot, out index))
            {
                return null;
            }

            return snapshot as AbstractTableEntriesSnapshot<DiagnosticData>;
        }

210 211 212
        /// <summary>
        /// Gets <see cref="DiagnosticData"/> objects for error list entries, filtered based on the given parameters.
        /// </summary>
213
        public async Task<ImmutableArray<DiagnosticData>> GetItemsAsync(bool selectedEntriesOnly, bool isAddSuppression, bool isSuppressionInSource, bool onlyCompilerDiagnostics, CancellationToken cancellationToken)
214 215
        {
            var builder = ImmutableArray.CreateBuilder<DiagnosticData>();
216 217 218
            Dictionary<string, Project> projectNameToProjectMapOpt = null;
            Dictionary<Project, ImmutableDictionary<string, Document>> filePathToDocumentMapOpt = null;

219 220 221 222 223 224 225 226 227 228 229
            var entries = selectedEntriesOnly ? _tableControl.SelectedEntries : _tableControl.Entries;
            foreach (var entryHandle in entries)
            {
                cancellationToken.ThrowIfCancellationRequested();

                DiagnosticData diagnosticData = null;
                int index;
                var roslynSnapshot = GetEntriesSnapshot(entryHandle, out index);
                if (roslynSnapshot != null)
                {
                    diagnosticData = roslynSnapshot.GetItem(index)?.Primary;
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
                }
                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))
249
                    {
250
                        if (projectNameToProjectMapOpt == null)
251
                        {
252 253 254 255 256 257 258 259 260 261 262 263 264
                            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
265 266 267
                            continue;
                        }

268
                        Document document = null;
269
                        var hasLocation = (entryHandle.TryGetValue(StandardTableColumnDefinitions.DocumentName, out filePath) && !string.IsNullOrEmpty(filePath)) &&
270 271
                            (entryHandle.TryGetValue(StandardTableColumnDefinitions.Line, out line) && line >= 0);
                        if (hasLocation)
272
                        {
273
                            if (string.IsNullOrEmpty(filePath) || line < 0)
274
                            {
275 276
                                // bail out
                                continue;
277
                            }
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297

                            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);
298
                        }
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314

                        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,
315
                            location: location,
316
                            customTags: SuppressionHelpers.SynthesizedExternalSourceDiagnosticCustomTags);
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336

                        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))
337 338 339 340
                        {
                            builder.Add(diagnosticData);
                        }
                    }
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362
                    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);
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380
                }
            }

            return builder.ToImmutable();
        }

        private static void UpdateSelectedItems(bool added, ref int count)
        {
            if (added)
            {
                count++;
            }
            else
            {
                count--;
            }
        }

381
        private void HandleSuppressionStateEntry(bool isRoslynEntry, bool isSuppressedEntry, bool isCompilerDiagnosticEntry, bool isNoLocationDiagnosticEntry, bool added)
382 383 384 385 386 387 388 389 390 391 392
        {
            if (isRoslynEntry)
            {
                UpdateSelectedItems(added, ref _selectedRoslynItems);
            }

            if (isCompilerDiagnosticEntry)
            {
                UpdateSelectedItems(added, ref _selectedCompilerDiagnosticItems);
            }

393 394 395 396 397
            if (isNoLocationDiagnosticEntry)
            {
                UpdateSelectedItems(added, ref _selectedNoLocationDiagnosticItems);
            }

398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
            if (isSuppressedEntry)
            {
                UpdateSelectedItems(added, ref _selectedSuppressedItems);
            }
            else
            {
                UpdateSelectedItems(added, ref _selectedActiveItems);
            }
        }

        private void HandleNonSuppressionStateEntry(bool added)
        {
            UpdateSelectedItems(added, ref _selectedNonSuppressionStateItems);
        }

        private void UpdateQueryStatus()
        {
            // Force the shell to refresh the QueryStatus for all the command since default behavior is it only does query
            // when focus on error list has changed, not individual items.
            if (_shellService != null)
            {
                _shellService.UpdateCommandUI(0);
            }
        }
    }
}