// 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.Composition; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Shared.TestHooks; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Diagnostics { [Export(typeof(IDiagnosticService)), Shared] internal partial class DiagnosticService : IDiagnosticService { private const string DiagnosticsUpdatedEventName = "DiagnosticsUpdated"; private readonly IAsynchronousOperationListener _listener; private readonly EventMap _eventMap; private readonly SimpleTaskQueue _eventQueue; private readonly object _gate; private readonly Dictionary> _map; [ImportingConstructor] public DiagnosticService([ImportMany] IEnumerable> asyncListeners) : this() { // queue to serialize events. _eventMap = new EventMap(); _eventQueue = new SimpleTaskQueue(TaskScheduler.Default); _listener = new AggregateAsynchronousOperationListener(asyncListeners, FeatureAttribute.DiagnosticService); _gate = new object(); _map = new Dictionary>(); } public event EventHandler DiagnosticsUpdated { add { _eventMap.AddEventHandler(DiagnosticsUpdatedEventName, value); } remove { _eventMap.RemoveEventHandler(DiagnosticsUpdatedEventName, value); } } private void RaiseDiagnosticsUpdated(object sender, DiagnosticsUpdatedArgs args) { var ev = _eventMap.GetEventHandlers>(DiagnosticsUpdatedEventName); if (ev.HasHandlers) { var eventToken = _listener.BeginAsyncOperation(DiagnosticsUpdatedEventName); _eventQueue.ScheduleTask(() => { UpdateDataMap(sender, args); ev.RaiseEvent(handler => handler(sender, args)); }).CompletesAsyncOperation(eventToken); } } private void UpdateDataMap(object sender, DiagnosticsUpdatedArgs args) { var updateSource = sender as IDiagnosticUpdateSource; if (updateSource == null || updateSource.SupportGetDiagnostics) { return; } // we expect someone who uses this ability to small. lock (_gate) { Contract.Requires(_updateSources.Contains(updateSource)); var list = _map.GetOrAdd(updateSource, _ => new Dictionary()); var data = new Data(args); list.Remove(data.Id); if (list.Count == 0 && args.Diagnostics.Length == 0) { _map.Remove(updateSource); return; } list.Add(args.Id, data); } } private void OnDiagnosticsUpdated(object sender, DiagnosticsUpdatedArgs e) { AssertIfNull(e.Diagnostics); RaiseDiagnosticsUpdated(sender, e); } public IEnumerable GetDiagnostics( Workspace workspace, ProjectId projectId, DocumentId documentId, object id, bool includeSuppressedDiagnostics, CancellationToken cancellationToken) { // TODO: current design was one issue where GetDiagnostic can't get id if one doesnt already know about it. need an API that let it return all ids for existing diagnostics. if (id != null) { // get specific one return GetSpecificDiagnostics(workspace, projectId, documentId, id, includeSuppressedDiagnostics, cancellationToken); } // get aggregated ones return GetDiagnostics(workspace, projectId, documentId, includeSuppressedDiagnostics, cancellationToken); } private IEnumerable GetSpecificDiagnostics(Workspace workspace, ProjectId projectId, DocumentId documentId, object id, bool includeSuppressedDiagnostics, CancellationToken cancellationToken) { foreach (var source in _updateSources) { cancellationToken.ThrowIfCancellationRequested(); if (source.SupportGetDiagnostics) { var diagnostics = source.GetDiagnostics(workspace, projectId, documentId, id, includeSuppressedDiagnostics, cancellationToken); if (diagnostics != null && diagnostics.Length > 0) { return diagnostics; } } else { using (var pool = SharedPools.Default>().GetPooledObject()) { AppendMatchingData(source, workspace, projectId, documentId, id, pool.Object); Contract.Requires(pool.Object.Count == 0 || pool.Object.Count == 1); if (pool.Object.Count == 1) { var diagnostics = pool.Object[0].Diagnostics; return !includeSuppressedDiagnostics ? FilterSuppressedDiagnostics(diagnostics) : diagnostics; } } } } return SpecializedCollections.EmptyEnumerable(); } private static IEnumerable FilterSuppressedDiagnostics(IEnumerable diagnostics) { if (diagnostics != null) { foreach (var diagnostic in diagnostics) { if (!diagnostic.IsSuppressed) { yield return diagnostic; } } } } private IEnumerable GetDiagnostics( Workspace workspace, ProjectId projectId, DocumentId documentId, bool includeSuppressedDiagnostics, CancellationToken cancellationToken) { foreach (var source in _updateSources) { cancellationToken.ThrowIfCancellationRequested(); if (source.SupportGetDiagnostics) { foreach (var diagnostic in source.GetDiagnostics(workspace, projectId, documentId, null, includeSuppressedDiagnostics, cancellationToken)) { AssertIfNull(diagnostic); yield return diagnostic; } } else { using (var list = SharedPools.Default>().GetPooledObject()) { AppendMatchingData(source, workspace, projectId, documentId, null, list.Object); foreach (var data in list.Object) { foreach (var diagnostic in data.Diagnostics) { AssertIfNull(diagnostic); if (includeSuppressedDiagnostics || !diagnostic.IsSuppressed) { yield return diagnostic; } } } } } } } private void AppendMatchingData( IDiagnosticUpdateSource source, Workspace workspace, ProjectId projectId, DocumentId documentId, object id, List list) { lock (_gate) { Dictionary current; if (!_map.TryGetValue(source, out current)) { return; } if (id != null) { Data data; if (current.TryGetValue(id, out data)) { list.Add(data); } return; } foreach (var data in current.Values) { if (TryAddData(documentId, data, d => d.DocumentId, list) || TryAddData(projectId, data, d => d.ProjectId, list) || TryAddData(workspace, data, d => d.Workspace, list)) { continue; } } } } private bool TryAddData(T key, Data data, Func keyGetter, List result) where T : class { if (key == null) { return false; } if (key == keyGetter(data)) { result.Add(data); } return true; } [Conditional("DEBUG")] private void AssertIfNull(ImmutableArray diagnostics) { for (var i = 0; i < diagnostics.Length; i++) { AssertIfNull(diagnostics[i]); } } [Conditional("DEBUG")] private void AssertIfNull(DiagnosticData diagnostic) { if (diagnostic == null) { Contract.Requires(false, "who returns invalid data?"); } } private struct Data : IEquatable { public readonly Workspace Workspace; public readonly ProjectId ProjectId; public readonly DocumentId DocumentId; public readonly object Id; public readonly ImmutableArray Diagnostics; public Data(DiagnosticsUpdatedArgs args) { this.Workspace = args.Workspace; this.ProjectId = args.ProjectId; this.DocumentId = args.DocumentId; this.Id = args.Id; this.Diagnostics = args.Diagnostics; } public bool Equals(Data other) { return this.Workspace == other.Workspace && this.ProjectId == other.ProjectId && this.DocumentId == other.DocumentId && this.Id == other.Id; } public override bool Equals(object obj) { return (obj is Data) && Equals((Data)obj); } public override int GetHashCode() { return Hash.Combine(Workspace, Hash.Combine(ProjectId, Hash.Combine(DocumentId, Hash.Combine(Id, 1)))); } } } }