|
// 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.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Diagnostics;
[DataContract]
internal sealed class DiagnosticData(
string id,
string category,
string? message,
DiagnosticSeverity severity,
DiagnosticSeverity defaultSeverity,
bool isEnabledByDefault,
int warningLevel,
ImmutableArray<string> customTags,
ImmutableDictionary<string, string?> properties,
ProjectId? projectId,
DiagnosticDataLocation location,
ImmutableArray<DiagnosticDataLocation> additionalLocations = default,
string? language = null,
string? title = null,
string? description = null,
string? helpLink = null,
bool isSuppressed = false) : IEquatable<DiagnosticData?>
{
[DataMember(Order = 0)]
public readonly string Id = id;
[DataMember(Order = 1)]
public readonly string Category = category;
[DataMember(Order = 2)]
public readonly string? Message = message;
[DataMember(Order = 3)]
public readonly DiagnosticSeverity Severity = severity;
[DataMember(Order = 4)]
public readonly DiagnosticSeverity DefaultSeverity = defaultSeverity;
[DataMember(Order = 5)]
public readonly bool IsEnabledByDefault = isEnabledByDefault;
[DataMember(Order = 6)]
public readonly int WarningLevel = warningLevel;
[DataMember(Order = 7)]
public readonly ImmutableArray<string> CustomTags = customTags;
[DataMember(Order = 8)]
public readonly ImmutableDictionary<string, string?> Properties = properties;
[DataMember(Order = 9)]
public readonly ProjectId? ProjectId = projectId;
[DataMember(Order = 10)]
public readonly DiagnosticDataLocation DataLocation = location;
[DataMember(Order = 11)]
public readonly ImmutableArray<DiagnosticDataLocation> AdditionalLocations = additionalLocations.NullToEmpty();
/// <summary>
/// Language name (<see cref="LanguageNames"/>) or null if the diagnostic is not associated with source code.
/// </summary>
[DataMember(Order = 12)]
public readonly string? Language = language;
[DataMember(Order = 13)]
public readonly string? Title = title;
[DataMember(Order = 14)]
public readonly string? Description = description;
[DataMember(Order = 15)]
public readonly string? HelpLink = helpLink;
[DataMember(Order = 16)]
public readonly bool IsSuppressed = isSuppressed;
/// <summary>
/// Properties for a diagnostic generated by an explicit build.
/// </summary>
internal static ImmutableDictionary<string, string> PropertiesForBuildDiagnostic { get; }
= ImmutableDictionary<string, string>.Empty.Add(WellKnownDiagnosticPropertyNames.Origin, WellKnownDiagnosticTags.Build);
public DiagnosticData WithLocations(DiagnosticDataLocation location, ImmutableArray<DiagnosticDataLocation> additionalLocations)
=> new(Id, Category, Message, Severity, DefaultSeverity, IsEnabledByDefault,
WarningLevel, CustomTags, Properties, ProjectId, location, additionalLocations,
Language, Title, Description, HelpLink, IsSuppressed);
public DocumentId? DocumentId => DataLocation.DocumentId;
public override bool Equals(object? obj)
=> obj is DiagnosticData data && Equals(data);
public bool Equals(DiagnosticData? other)
{
if (ReferenceEquals(this, other))
{
return true;
}
if (other is null)
{
return false;
}
// TODO: unclear why we're only looking at the OriginalFileSpan of the location, and only the start point of it.
return
DataLocation.UnmappedFileSpan.StartLinePosition == other.DataLocation.UnmappedFileSpan.StartLinePosition &&
Id == other.Id &&
Category == other.Category &&
Severity == other.Severity &&
WarningLevel == other.WarningLevel &&
IsSuppressed == other.IsSuppressed &&
ProjectId == other.ProjectId &&
DocumentId == other.DocumentId &&
Message == other.Message;
}
// TODO: unclear why we're only looking at the OriginalFileSpan of the location, and only the start point of it.
public override int GetHashCode()
=> Hash.Combine(DataLocation.UnmappedFileSpan.StartLinePosition.GetHashCode(),
Hash.Combine(Id,
Hash.Combine(Category,
Hash.Combine((int)Severity,
Hash.Combine(WarningLevel,
Hash.Combine(IsSuppressed,
Hash.Combine(ProjectId,
Hash.Combine(DocumentId,
Hash.Combine(Message, 0)))))))));
public override string ToString()
=> $"{Id} {Severity} {Message} {ProjectId} {DataLocation.MappedFileSpan} [original: {DataLocation.UnmappedFileSpan}]";
public async Task<Diagnostic> ToDiagnosticAsync(Project project, CancellationToken cancellationToken)
{
var location = await DataLocation.ConvertLocationAsync(project, cancellationToken).ConfigureAwait(false);
var additionalLocations = await AdditionalLocations.ConvertLocationsAsync(project, cancellationToken).ConfigureAwait(false);
return ToDiagnostic(location, additionalLocations);
}
public Diagnostic ToDiagnostic(Location location, ImmutableArray<Location> additionalLocations)
{
return Diagnostic.Create(
Id, Category, Message, Severity, DefaultSeverity,
IsEnabledByDefault, WarningLevel, IsSuppressed, Title, Description, HelpLink,
location, additionalLocations, customTags: CustomTags, properties: Properties);
}
private static DiagnosticDataLocation CreateLocation(TextDocument? document, Location location)
{
GetLocationInfo(out var originalLineInfo, out var mappedLineInfo);
if (!originalLineInfo.IsValid)
originalLineInfo = new FileLinePositionSpan(document?.FilePath ?? "", span: default);
return new DiagnosticDataLocation(originalLineInfo, document?.Id, mappedLineInfo);
void GetLocationInfo(out FileLinePositionSpan originalLineInfo, out FileLinePositionSpan mappedLineInfo)
{
var diagnosticSpanMappingService = document?.Project.Solution.Services.GetService<IWorkspaceVenusSpanMappingService>();
if (document != null && diagnosticSpanMappingService != null)
{
diagnosticSpanMappingService.GetAdjustedDiagnosticSpan(document.Id, location, out _, out originalLineInfo, out mappedLineInfo);
}
else
{
originalLineInfo = location.GetLineSpan();
mappedLineInfo = location.GetMappedLineSpan();
}
}
}
public static DiagnosticData Create(Solution solution, Diagnostic diagnostic, Project? project)
=> Create(diagnostic, project?.Id, project?.Language,
location: new DiagnosticDataLocation(new FileLinePositionSpan(project?.FilePath ?? solution.FilePath ?? "", span: default)),
additionalLocations: default, additionalProperties: null);
public static DiagnosticData Create(Diagnostic diagnostic, TextDocument document)
{
var project = document.Project;
var location = CreateLocation(document, diagnostic.Location);
var additionalLocations = GetAdditionalLocations(document, diagnostic);
var additionalProperties = GetAdditionalProperties(document, diagnostic);
var documentPropertiesService = document.DocumentServiceProvider.GetService<DocumentPropertiesService>();
var diagnosticsLspClientName = documentPropertiesService?.DiagnosticsLspClientName;
if (diagnosticsLspClientName != null)
{
additionalProperties ??= ImmutableDictionary.Create<string, string?>();
additionalProperties = additionalProperties.Add(nameof(documentPropertiesService.DiagnosticsLspClientName), diagnosticsLspClientName);
}
return Create(diagnostic,
project.Id,
project.Language,
location,
additionalLocations,
additionalProperties);
}
private static DiagnosticData Create(
Diagnostic diagnostic,
ProjectId? projectId,
string? language,
DiagnosticDataLocation location,
ImmutableArray<DiagnosticDataLocation> additionalLocations,
ImmutableDictionary<string, string?>? additionalProperties)
{
return new DiagnosticData(
diagnostic.Id,
diagnostic.Descriptor.Category,
diagnostic.GetMessage(CultureInfo.CurrentUICulture),
diagnostic.Severity,
diagnostic.DefaultSeverity,
diagnostic.Descriptor.IsEnabledByDefault,
diagnostic.WarningLevel,
diagnostic.Descriptor.ImmutableCustomTags(),
(additionalProperties == null) ? diagnostic.Properties : diagnostic.Properties.AddRange(additionalProperties),
projectId,
location,
additionalLocations,
language: language,
title: diagnostic.Descriptor.Title.ToString(CultureInfo.CurrentUICulture),
description: diagnostic.Descriptor.Description.ToString(CultureInfo.CurrentUICulture),
helpLink: diagnostic.Descriptor.HelpLinkUri,
isSuppressed: diagnostic.IsSuppressed);
}
private static ImmutableDictionary<string, string?>? GetAdditionalProperties(TextDocument document, Diagnostic diagnostic)
{
var service = document.Project.GetLanguageService<IDiagnosticPropertiesService>();
return service?.GetAdditionalProperties(diagnostic);
}
private static ImmutableArray<DiagnosticDataLocation> GetAdditionalLocations(TextDocument document, Diagnostic diagnostic)
{
if (diagnostic.AdditionalLocations.Count == 0)
{
return [];
}
using var _ = ArrayBuilder<DiagnosticDataLocation>.GetInstance(diagnostic.AdditionalLocations.Count, out var builder);
foreach (var location in diagnostic.AdditionalLocations)
{
if (location.IsInSource)
{
builder.AddIfNotNull(CreateLocation(document.Project.Solution.GetDocument(location.SourceTree), location));
}
else if (location.Kind == LocationKind.ExternalFile)
{
var textDocumentId = document.Project.GetDocumentForExternalLocation(location);
builder.AddIfNotNull(CreateLocation(document.Project.GetTextDocument(textDocumentId), location));
}
}
return builder.ToImmutableAndClear();
}
/// <summary>
/// Create a host/VS specific diagnostic with the given descriptor and message arguments for the given project.
/// Note that diagnostic created through this API cannot be suppressed with in-source suppression due to performance reasons (see the PERF remark below for details).
/// </summary>
public static bool TryCreate(DiagnosticDescriptor descriptor, string[] messageArguments, Project project, [NotNullWhen(true)] out DiagnosticData? diagnosticData)
{
diagnosticData = null;
DiagnosticSeverity effectiveSeverity;
if (project.SupportsCompilation)
{
// Get the effective severity of the diagnostic from the compilation options.
// PERF: We do not check if the diagnostic was suppressed by a source suppression, as this requires us to force complete the assembly attributes, which is very expensive.
var reportDiagnostic = descriptor.GetEffectiveSeverity(project.CompilationOptions!);
if (reportDiagnostic == ReportDiagnostic.Suppress)
{
// Rule is disabled by compilation options.
return false;
}
effectiveSeverity = GetEffectiveSeverity(reportDiagnostic, descriptor.DefaultSeverity);
}
else
{
effectiveSeverity = descriptor.DefaultSeverity;
}
var diagnostic = Diagnostic.Create(descriptor, Location.None, effectiveSeverity, additionalLocations: null, properties: null, messageArgs: messageArguments);
diagnosticData = Create(project.Solution, diagnostic, project);
return true;
}
private static DiagnosticSeverity GetEffectiveSeverity(ReportDiagnostic effectiveReportDiagnostic, DiagnosticSeverity defaultSeverity)
{
switch (effectiveReportDiagnostic)
{
case ReportDiagnostic.Default:
return defaultSeverity;
case ReportDiagnostic.Error:
return DiagnosticSeverity.Error;
case ReportDiagnostic.Hidden:
return DiagnosticSeverity.Hidden;
case ReportDiagnostic.Info:
return DiagnosticSeverity.Info;
case ReportDiagnostic.Warn:
return DiagnosticSeverity.Warning;
default:
throw ExceptionUtilities.Unreachable();
}
}
/// <summary>
/// Returns true if the diagnostic was generated by an explicit build, not live analysis.
/// </summary>
internal bool IsBuildDiagnostic()
{
return Properties.TryGetValue(WellKnownDiagnosticPropertyNames.Origin, out var value) &&
value == WellKnownDiagnosticTags.Build;
}
// TODO: the value stored in HelpLink should already be valid URI (https://github.com/dotnet/roslyn/issues/59205)
internal Uri? GetValidHelpLinkUri()
=> Uri.TryCreate(HelpLink, UriKind.Absolute, out var uri) ? uri : null;
// Return the diagnostic ID as the HelpKeyword, unless the diagnostic does support F1 help for keyword.
internal string? GetHelpKeyword()
=> CustomTags.Contains(WellKnownDiagnosticCustomTags.DoesNotSupportF1Help) ? null : Id;
}
|