|
// 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
|