| File: CommandLine\SarifV1ErrorLogger.cs | Web Access |
| Project: src\roslyn\src\Compilers\Core\Portable\Microsoft.CodeAnalysis.csproj (Microsoft.CodeAnalysis) |
// 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.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using Microsoft.CodeAnalysis.Diagnostics; using Roslyn.Utilities; #pragma warning disable RS0013 // We need to invoke Diagnostic.Descriptor here to log all the metadata properties of the diagnostic. namespace Microsoft.CodeAnalysis { /// <summary> /// Used for logging compiler diagnostics to a stream in the unstandardized SARIF /// (Static Analysis Results Interchange Format) v1.0.0 format. /// https://github.com/sarif-standard/sarif-spec /// https://rawgit.com/sarif-standard/sarif-spec/main/Static%20Analysis%20Results%20Interchange%20Format%20(SARIF).html /// </summary> /// <remarks> /// To log diagnostics in the standardized SARIF v2.1.0 format, use the SarifV2ErrorLogger. /// </remarks> internal sealed class SarifV1ErrorLogger : SarifErrorLogger, IDisposable { private readonly DiagnosticDescriptorSet _descriptors; public SarifV1ErrorLogger(Stream stream, string toolName, string toolFileVersion, Version toolAssemblyVersion, CultureInfo culture) : base(stream, culture) { _descriptors = new DiagnosticDescriptorSet(); _writer.WriteObjectStart(); // root _writer.Write("$schema", "http://json.schemastore.org/sarif-1.0.0"); _writer.Write("version", "1.0.0"); _writer.WriteArrayStart("runs"); _writer.WriteObjectStart(); // run _writer.WriteObjectStart("tool"); _writer.Write("name", toolName); _writer.Write("version", toolAssemblyVersion.ToString()); _writer.Write("fileVersion", toolFileVersion); _writer.Write("semanticVersion", toolAssemblyVersion.ToString(fieldCount: 3)); // Emit the 'language' property only if it is a non-empty string to match the SARIF spec. if (culture.Name.Length > 0) _writer.Write("language", culture.Name); _writer.WriteObjectEnd(); // tool _writer.WriteArrayStart("results"); } protected override string PrimaryLocationPropertyName => "resultFile"; public override void LogDiagnostic(Diagnostic diagnostic, SuppressionInfo? suppressionInfo) { _writer.WriteObjectStart(); // result _writer.Write("ruleId", diagnostic.Id); string ruleKey = _descriptors.Add(diagnostic.Descriptor); if (ruleKey != diagnostic.Id) { _writer.Write("ruleKey", ruleKey); } _writer.Write("level", GetLevel(diagnostic.Severity)); string? message = diagnostic.GetMessage(_culture); if (!RoslynString.IsNullOrEmpty(message)) { _writer.Write("message", message); } if (diagnostic.IsSuppressed) { _writer.WriteArrayStart("suppressionStates"); _writer.Write("suppressedInSource"); _writer.WriteArrayEnd(); } WriteLocations(diagnostic.Location, diagnostic.AdditionalLocations); WriteResultProperties(diagnostic); _writer.WriteObjectEnd(); // result } private void WriteLocations(Location location, IReadOnlyList<Location> additionalLocations) { if (HasPath(location)) { _writer.WriteArrayStart("locations"); _writer.WriteObjectStart(); // location _writer.WriteKey(PrimaryLocationPropertyName); WritePhysicalLocation(location); _writer.WriteObjectEnd(); // location _writer.WriteArrayEnd(); // locations } // See https://github.com/dotnet/roslyn/issues/11228 for discussion around // whether this is the correct treatment of Diagnostic.AdditionalLocations // as SARIF relatedLocations. if (additionalLocations != null && additionalLocations.Count > 0 && additionalLocations.Any(l => HasPath(l))) { _writer.WriteArrayStart("relatedLocations"); foreach (var additionalLocation in additionalLocations) { if (HasPath(additionalLocation)) { _writer.WriteObjectStart(); // annotatedCodeLocation _writer.WriteKey("physicalLocation"); WritePhysicalLocation(additionalLocation); _writer.WriteObjectEnd(); // annotatedCodeLocation } } _writer.WriteArrayEnd(); // relatedLocations } } public override void AddAnalyzerDescriptorsAndExecutionTime(ImmutableArray<(DiagnosticDescriptor Descriptor, DiagnosticDescriptorErrorLoggerInfo Info)> descriptors, double totalAnalyzerExecutionTime) { // We log all analyzer descriptors only in SARIF v2+ format. } protected override void WritePhysicalLocation(Location location) { Debug.Assert(HasPath(location)); FileLinePositionSpan span = location.GetMappedLineSpan(); _writer.WriteObjectStart(); _writer.Write("uri", GetUri(span.Path)); WriteRegion(span); _writer.WriteObjectEnd(); } private void WriteRules() { if (_descriptors.Count > 0) { _writer.WriteObjectStart("rules"); foreach (var pair in _descriptors.ToSortedList()) { DiagnosticDescriptor descriptor = pair.Value; _writer.WriteObjectStart(pair.Key); // rule _writer.Write("id", descriptor.Id); string? shortDescription = descriptor.Title.ToString(_culture); if (!RoslynString.IsNullOrEmpty(shortDescription)) { _writer.Write("shortDescription", shortDescription); } string? fullDescription = descriptor.Description.ToString(_culture); if (!RoslynString.IsNullOrEmpty(fullDescription)) { _writer.Write("fullDescription", fullDescription); } _writer.Write("defaultLevel", GetLevel(descriptor.DefaultSeverity)); if (!string.IsNullOrEmpty(descriptor.HelpLinkUri)) { _writer.Write("helpUri", descriptor.HelpLinkUri); } _writer.WriteObjectStart("properties"); if (!string.IsNullOrEmpty(descriptor.Category)) { _writer.Write("category", descriptor.Category); } _writer.Write("isEnabledByDefault", descriptor.IsEnabledByDefault); if (descriptor.ImmutableCustomTags.Any()) { _writer.WriteArrayStart("tags"); foreach (string tag in descriptor.ImmutableCustomTags) { _writer.Write(tag); } _writer.WriteArrayEnd(); // tags } _writer.WriteObjectEnd(); // properties _writer.WriteObjectEnd(); // rule } _writer.WriteObjectEnd(); // rules } } public override void Dispose() { _writer.WriteArrayEnd(); // results WriteRules(); _writer.WriteObjectEnd(); // run _writer.WriteArrayEnd(); // runs _writer.WriteObjectEnd(); // root base.Dispose(); } /// <summary> /// Represents a distinct set of <see cref="DiagnosticDescriptor"/>s and provides unique string keys /// to distinguish them. /// /// The first <see cref="DiagnosticDescriptor"/> added with a given <see cref="DiagnosticDescriptor.Id"/> /// value is given that value as its unique key. Subsequent adds with the same ID will have .NNN /// appended to their with an auto-incremented numeric value. /// </summary> private sealed class DiagnosticDescriptorSet { // DiagnosticDescriptor.Id -> auto-incremented counter private readonly Dictionary<string, int> _counters = new Dictionary<string, int>(); // DiagnosticDescriptor -> unique key private readonly Dictionary<DiagnosticDescriptor, string> _keys = new Dictionary<DiagnosticDescriptor, string>(SarifDiagnosticComparer.Instance); /// <summary> /// The total number of descriptors in the set. /// </summary> public int Count => _keys.Count; /// <summary> /// Adds a descriptor to the set if not already present. /// </summary> /// <returns> /// The unique key assigned to the given descriptor. /// </returns> public string Add(DiagnosticDescriptor descriptor) { // Case 1: Descriptor has already been seen -> retrieve key from cache. if (_keys.TryGetValue(descriptor, out string? key)) { return key; } // Case 2: First time we see a descriptor with a given ID -> use its ID as the key. if (!_counters.TryGetValue(descriptor.Id, out int counter)) { _counters.Add(descriptor.Id, 0); _keys.Add(descriptor, descriptor.Id); return descriptor.Id; } // Case 3: We've already seen a different descriptor with the same ID -> generate a key. // // This will only need to loop in the corner case where there is an actual descriptor // with non-generated ID=X.NNN and more than one descriptor with ID=X. do { _counters[descriptor.Id] = ++counter; key = descriptor.Id + "-" + counter.ToString("000", CultureInfo.InvariantCulture); } while (_counters.ContainsKey(key)); _keys.Add(descriptor, key); return key; } /// <summary> /// Converts the set to a list of (key, descriptor) pairs sorted by key. /// </summary> public List<KeyValuePair<string, DiagnosticDescriptor>> ToSortedList() { Debug.Assert(Count > 0); var list = new List<KeyValuePair<string, DiagnosticDescriptor>>(Count); foreach (var pair in _keys) { Debug.Assert(list.Capacity > list.Count); list.Add(new KeyValuePair<string, DiagnosticDescriptor>(pair.Value, pair.Key)); } Debug.Assert(list.Capacity == list.Count); list.Sort((x, y) => string.CompareOrdinal(x.Key, y.Key)); return list; } } } } #pragma warning restore RS0013