|
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text;
using ILLink.Shared;
using Mono.Cecil;
namespace Mono.Linker
{
public readonly struct MessageContainer : IComparable<MessageContainer>, IEquatable<MessageContainer>
{
public static readonly MessageContainer Empty;
/// <summary>
/// Optional data with a filename, line and column that triggered
/// to output an error (or warning) message.
/// </summary>
public MessageOrigin? Origin { get; }
public MessageCategory Category { get; }
/// <summary>
/// Further categorize the message.
/// </summary>
public string SubCategory { get; }
/// <summary>
/// Code identifier for errors and warnings.
/// </summary>
public int? Code { get; }
/// <summary>
/// User friendly text describing the error or warning.
/// </summary>
public string Text { get; }
/// <summary>
/// Create an error message.
/// </summary>
/// <param name="text">Humanly readable message describing the error</param>
/// <param name="code">Unique error ID. Please see https://github.com/dotnet/runtime/blob/main/docs/tools/illink/error-codes.md
/// for the list of errors and possibly add a new one</param>
/// <param name="subcategory">Optionally, further categorize this error</param>
/// <param name="origin">Filename, line, and column where the error was found</param>
/// <returns>New MessageContainer of 'Error' category</returns>
internal static MessageContainer CreateErrorMessage (string text, int code, string subcategory = MessageSubCategory.None, MessageOrigin? origin = null)
{
if (!(code >= 1000 && code <= 2000))
throw new ArgumentOutOfRangeException (nameof (code), $"The provided code '{code}' does not fall into the error category, which is in the range of 1000 to 2000 (inclusive).");
return new MessageContainer (MessageCategory.Error, text, code, subcategory, origin);
}
/// <summary>
/// Create an error message.
/// </summary>
/// <param name="origin">Filename, line, and column where the error was found</param>
/// <param name="id">Unique error ID. Please see https://github.com/dotnet/runtime/blob/main/docs/tools/illink/error-codes.md
/// for the list of errors and possibly add a new one</param>
/// <param name="args">Additional arguments to form a humanly readable message describing the warning</param>
/// <returns>New MessageContainer of 'Error' category</returns>
internal static MessageContainer CreateErrorMessage (MessageOrigin? origin, DiagnosticId id, params string[] args)
{
if (!((int) id >= 1000 && (int) id <= 2000))
throw new ArgumentOutOfRangeException (nameof (id), $"The provided code '{(int) id}' does not fall into the error category, which is in the range of 1000 to 2000 (inclusive).");
return new MessageContainer (MessageCategory.Error, id, origin: origin, args: args);
}
/// <summary>
/// Create a custom error message.
/// </summary>
/// <param name="text">Humanly readable message describing the error</param>
/// <param name="code">A custom error ID. This code should be greater than or equal to 6001
/// to avoid any collisions with existing and future errors</param>
/// <param name="subcategory">Optionally, further categorize this error</param>
/// <param name="origin">Filename or member where the error is coming from</param>
/// <returns>Custom MessageContainer of 'Error' category</returns>
public static MessageContainer CreateCustomErrorMessage (string text, int code, string subcategory = MessageSubCategory.None, MessageOrigin? origin = null)
{
#if DEBUG
Debug.Assert (Assembly.GetCallingAssembly () != typeof (MessageContainer).Assembly,
"'CreateCustomErrorMessage' is intended to be used by external assemblies only. Use 'CreateErrorMessage' instead.");
#endif
if (code <= 6000)
throw new ArgumentOutOfRangeException (nameof (code), $"The provided code '{code}' does not fall into the permitted range for external errors. To avoid possible collisions " +
"with existing and future {Constants.ILLink} errors, external messages should use codes starting from 6001.");
return new MessageContainer (MessageCategory.Error, text, code, subcategory, origin);
}
/// <summary>
/// Create a warning message.
/// </summary>
/// <param name="context">Context with the relevant warning suppression info.</param>
/// <param name="text">Humanly readable message describing the warning</param>
/// <param name="code">Unique warning ID. Please see https://github.com/dotnet/runtime/blob/main/docs/tools/illink/error-codes.md
/// for the list of warnings and possibly add a new one</param>
/// <param name="origin">Filename or member where the warning is coming from</param>
/// <param name="subcategory">Optionally, further categorize this warning</param>
/// <param name="version">Optional warning version number. Versioned warnings can be controlled with the
/// warning wave option --warn VERSION. Unversioned warnings are unaffected by this option. </param>
/// <returns>New MessageContainer of 'Warning' category</returns>
internal static MessageContainer CreateWarningMessage (LinkContext context, string text, int code, MessageOrigin origin, WarnVersion version, string subcategory = MessageSubCategory.None)
{
if (!(code > 2000 && code <= 6000))
throw new ArgumentOutOfRangeException (nameof (code), $"The provided code '{code}' does not fall into the warning category, which is in the range of 2001 to 6000 (inclusive).");
return CreateWarningMessageContainer (context, text, code, origin, version, subcategory);
}
/// <summary>
/// Create a warning message.
/// </summary>
/// <param name="context">Context with the relevant warning suppression info.</param>
/// <param name="origin">Filename or member where the warning is coming from</param>
/// <param name="id">Unique warning ID. Please see https://github.com/dotnet/runtime/blob/main/docs/tools/illink/error-codes.md
/// for the list of warnings and possibly add a new one</param>
/// <param name="version">Optional warning version number. Versioned warnings can be controlled with the
/// warning wave option --warn VERSION. Unversioned warnings are unaffected by this option. </param>
/// <param name="args">Additional arguments to form a humanly readable message describing the warning</param>
/// <returns>New MessageContainer of 'Warning' category</returns>
internal static MessageContainer CreateWarningMessage (LinkContext context, MessageOrigin origin, DiagnosticId id, WarnVersion version, params string[] args)
{
if (!((int) id > 2000 && (int) id <= 6000))
throw new ArgumentOutOfRangeException (nameof (id), $"The provided code '{(int) id}' does not fall into the warning category, which is in the range of 2001 to 6000 (inclusive).");
return CreateWarningMessageContainer (context, origin, id, version, id.GetDiagnosticSubcategory (), args);
}
/// <summary>
/// Create a custom warning message.
/// </summary>
/// <param name="context">Context with the relevant warning suppression info.</param>
/// <param name="text">Humanly readable message describing the warning</param>
/// <param name="code">A custom warning ID. This code should be greater than or equal to 6001
/// to avoid any collisions with existing and future warnings</param>
/// <param name="origin">Filename or member where the warning is coming from</param>
/// <param name="version">Optional warning version number. Versioned warnings can be controlled with the
/// warning wave option --warn VERSION. Unversioned warnings are unaffected by this option</param>
/// <param name="subcategory"></param>
/// <returns>Custom MessageContainer of 'Warning' category</returns>
public static MessageContainer CreateCustomWarningMessage (LinkContext context, string text, int code, MessageOrigin origin, WarnVersion version, string subcategory = MessageSubCategory.None)
{
#if DEBUG
Debug.Assert (Assembly.GetCallingAssembly () != typeof (MessageContainer).Assembly,
"'CreateCustomWarningMessage' is intended to be used by external assemblies only. Use 'CreateWarningMessage' instead.");
#endif
if (code <= 6000)
throw new ArgumentOutOfRangeException (nameof (code), $"The provided code '{code}' does not fall into the permitted range for external warnings. To avoid possible collisions " +
$"with existing and future {Constants.ILLink} warnings, external messages should use codes starting from 6001.");
return CreateWarningMessageContainer (context, text, code, origin, version, subcategory);
}
private static MessageContainer CreateWarningMessageContainer (LinkContext context, string text, int code, MessageOrigin origin, WarnVersion version, string subcategory = MessageSubCategory.None)
{
if (!(version >= WarnVersion.ILLink0 && version <= WarnVersion.Latest))
throw new ArgumentException ($"The provided warning version '{version}' is invalid.");
if (context.IsWarningSuppressed (code, subcategory, origin))
return Empty;
if (version > context.WarnVersion)
return Empty;
if (TryLogSingleWarning (context, code, origin, subcategory))
return Empty;
if (context.IsWarningAsError (code))
return new MessageContainer (MessageCategory.WarningAsError, text, code, subcategory, origin);
return new MessageContainer (MessageCategory.Warning, text, code, subcategory, origin);
}
private static MessageContainer CreateWarningMessageContainer (LinkContext context, MessageOrigin origin, DiagnosticId id, WarnVersion version, string subcategory, params string[] args)
{
if (!(version >= WarnVersion.ILLink0 && version <= WarnVersion.Latest))
throw new ArgumentException ($"The provided warning version '{version}' is invalid.");
if (context.IsWarningSuppressed ((int) id, subcategory, origin))
return Empty;
if (version > context.WarnVersion)
return Empty;
if (TryLogSingleWarning (context, (int) id, origin, subcategory))
return Empty;
if (context.IsWarningAsError ((int) id))
return new MessageContainer (MessageCategory.WarningAsError, id, subcategory, origin, args);
return new MessageContainer (MessageCategory.Warning, id, subcategory, origin, args);
}
public bool IsWarningMessage ([NotNullWhen (true)] out int? code)
{
code = null;
if (Category is MessageCategory.Warning or MessageCategory.WarningAsError) {
// Warning messages always have a code.
code = Code!;
return true;
}
return false;
}
static bool TryLogSingleWarning (LinkContext context, int code, MessageOrigin origin, string subcategory)
{
if (subcategory != MessageSubCategory.TrimAnalysis)
return false;
// There are valid cases where we can't map the message to an assembly
// For example if it's caused by something in an xml file passed on the command line
// In that case, give up on single-warn collapse and just print out the warning on its own.
var assembly = origin.Provider switch {
AssemblyDefinition asm => asm,
TypeDefinition type => type.Module.Assembly,
IMemberDefinition member => member.DeclaringType.Module.Assembly,
_ => null
};
if (assembly == null)
return false;
// Any IL2026 warnings left in an assembly with an IsTrimmable attribute are considered intentional
// and should not be collapsed, so that the user-visible RUC message gets printed.
if (code == 2026 && context.IsTrimmable (assembly))
return false;
var assemblyName = assembly.Name.Name;
if (!context.IsSingleWarn (assemblyName))
return false;
if (context.AssembliesWithGeneratedSingleWarning.Add (assemblyName))
context.LogWarning (context.GetAssemblyLocation (assembly), DiagnosticId.AssemblyProducedTrimWarnings, assemblyName);
return true;
}
/// <summary>
/// Create a info message.
/// </summary>
/// <param name="text">Humanly readable message</param>
/// <returns>New MessageContainer of 'Info' category</returns>
public static MessageContainer CreateInfoMessage (string text)
{
return new MessageContainer (MessageCategory.Info, text, null);
}
internal static MessageContainer CreateInfoMessage (MessageOrigin origin, string text)
{
return new MessageContainer (MessageCategory.Info, text, null, "", origin);
}
/// <summary>
/// Create a diagnostics message.
/// </summary>
/// <param name="text">Humanly readable message</param>
/// <returns>New MessageContainer of 'Diagnostic' category</returns>
public static MessageContainer CreateDiagnosticMessage (string text)
{
return new MessageContainer (MessageCategory.Diagnostic, text, null);
}
private MessageContainer (MessageCategory category, string text, int? code, string subcategory = MessageSubCategory.None, MessageOrigin? origin = null)
{
Code = code;
Category = category;
Origin = origin;
SubCategory = subcategory;
Text = text;
}
private MessageContainer (MessageCategory category, DiagnosticId id, string subcategory = MessageSubCategory.None, MessageOrigin? origin = null, params string[] args)
{
Code = (int) id;
Category = category;
Origin = origin;
SubCategory = subcategory;
Text = new DiagnosticString (id).GetMessage (args);
}
public override string ToString () => ToMSBuildString ();
public string ToMSBuildString ()
{
const string originApp = Constants.ILLink;
string origin = Origin?.ToString () ?? originApp;
StringBuilder sb = new StringBuilder ();
sb.Append (origin).Append (':');
if (!string.IsNullOrEmpty (SubCategory))
sb.Append (' ').Append (SubCategory);
string cat;
switch (Category) {
case MessageCategory.Error:
case MessageCategory.WarningAsError:
cat = "error";
break;
case MessageCategory.Warning:
cat = "warning";
break;
default:
cat = "";
break;
}
if (!string.IsNullOrEmpty (cat)) {
sb.Append (' ')
.Append (cat)
.Append (" IL")
// Warning and error messages always have a code.
.Append (Code!.Value.ToString ("D4"))
.Append (": ");
} else {
sb.Append (' ');
}
if (Origin?.Provider != null) {
if (Origin?.Provider is MethodDefinition method)
sb.Append (method.GetDisplayName ());
else if (Origin?.Provider is MemberReference memberRef)
sb.Append (memberRef.GetDisplayName ());
else if (Origin?.Provider is IMemberDefinition member)
sb.Append (member.FullName);
else if (Origin?.Provider is AssemblyDefinition assembly)
sb.Append (assembly.Name.Name);
else
throw new NotSupportedException ();
sb.Append (": ");
}
// Expected output $"{FileName(SourceLine, SourceColumn)}: {SubCategory}{Category} IL{Code}: ({MemberDisplayName}: ){Text}");
sb.Append (Text);
return sb.ToString ();
}
public bool Equals (MessageContainer other) =>
(Category, Text, Code, SubCategory, Origin) == (other.Category, other.Text, other.Code, other.SubCategory, other.Origin);
public override bool Equals (object? obj) => obj is MessageContainer messageContainer && Equals (messageContainer);
public override int GetHashCode () => (Category, Text, Code, SubCategory, Origin).GetHashCode ();
public int CompareTo (MessageContainer other)
{
if (Origin != null && other.Origin != null) {
return Origin.Value.CompareTo (other.Origin.Value);
} else if (Origin == null && other.Origin == null) {
return (Code < other.Code) ? -1 : 1;
}
return (Origin == null) ? 1 : -1;
}
public static bool operator == (MessageContainer lhs, MessageContainer rhs) => lhs.Equals (rhs);
public static bool operator != (MessageContainer lhs, MessageContainer rhs) => !lhs.Equals (rhs);
}
}
|