// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.ComponentModel;
using System.Xml;
using Microsoft.Build.BackEnd;
using Microsoft.Build.Collections;
using Microsoft.Build.Shared;
namespace Microsoft.Build.Construction;
/// <summary>
/// Represents the location of an MSBuild XML element in a file.
/// </summary>
/// <remarks>
/// Instances are immutable. Editing project XML through the MSBuild APIs invalidates locations associated
/// with that XML until it is reloaded.
/// <para>
/// Keep this type and its implementations compact. A project can contain many thousands of locations, and
/// locations are transferred between build nodes.
/// </para>
/// </remarks>
[Serializable]
public abstract class ElementLocation : IElementLocation, IImmutable
{
/// <summary>
/// Gets the singleton location with no file, line, or column information.
/// </summary>
/// <remarks>
/// Use <see langword="null"/> when a location is absent. Use this value when an object has a location
/// conceptually, but no specific location is available, such as an unnamed in-memory project.
/// </remarks>
public static ElementLocation Empty { get; } = new FileOnly(string.Empty);
/// <summary>
/// Gets the singleton location with no file, line, or column information.
/// </summary>
/// <remarks>
/// This compatibility property returns <see cref="Empty"/>. Use <see cref="Empty"/> in new code.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public static ElementLocation EmptyLocation => Empty;
/// <summary>
/// Gets the file from which the element originated, or an empty string when the file is unknown.
/// </summary>
/// <remarks>
/// This path may differ from the project path when the element originated in an imported project or
/// targets file.
/// </remarks>
public abstract string File { get; }
/// <summary>
/// Gets the one-based line number, or <c>0</c> when the line is unknown.
/// </summary>
public abstract int Line { get; }
/// <summary>
/// Gets the one-based column number, or <c>0</c> when the column is unknown.
/// </summary>
public abstract int Column { get; }
/// <summary>
/// Gets the location formatted for inclusion in a message.
/// </summary>
/// <remarks>
/// The returned value has the form <c>file</c>, <c>file (line)</c>, or
/// <c>file (line,column)</c>, depending on the available coordinates. Prefer placing location information
/// at the beginning of a message rather than embedding it within the message text.
/// </remarks>
public string LocationString => GetLocationString(File, Line, Column);
/// <summary>
/// Returns a hash code for this location.
/// </summary>
/// <returns>
/// A hash code derived from the line and column. This is done to avoid breaking changes
/// with potential user-derived <see cref="ElementLocation"/> instances.
/// </returns>
public override int GetHashCode()
=> Line ^ Column;
/// <summary>
/// Determines whether an object represents the same file, line, and column as this location.
/// </summary>
/// <param name="obj">The object to compare with this location.</param>
/// <returns>
/// <see langword="true"/> when <paramref name="obj"/> is an <see cref="ElementLocation"/> with the same
/// file, line, and column; otherwise, <see langword="false"/>.
/// </returns>
/// <remarks>
/// File names are compared using <see cref="StringComparison.OrdinalIgnoreCase"/> on all platforms.
/// </remarks>
public override bool Equals(object? obj)
=> ReferenceEquals(this, obj)
|| (obj is ElementLocation other && Equals(other));
/// <summary>
/// Determines whether an <see cref="ElementLocation"/> represents the same file, line, and column as this location.
/// </summary>
/// <param name="other">The <see cref="ElementLocation"/> to compare with this location.</param>
/// <returns>
/// <see langword="true"/> when <paramref name="other"/> has the same file, line, and column; otherwise, <see langword="false"/>.
/// </returns>
/// <remarks>
/// File names are compared using <see cref="StringComparison.OrdinalIgnoreCase"/> on all platforms.
/// </remarks>
private bool Equals(ElementLocation other)
=> Line == other.Line
&& Column == other.Column
&& StringComparer.OrdinalIgnoreCase.Equals(File, other.File);
/// <summary>
/// Returns the location formatted for inclusion in a message.
/// </summary>
/// <returns>
/// The value of <see cref="LocationString"/>.
/// </returns>
public override string ToString() => LocationString;
/// <summary>
/// Writes this location to a node packet.
/// </summary>
/// <param name="translator">The translator receiving the location data.</param>
/// <remarks>
/// Line and column are always serialized as integers so that the wire format does not depend on the
/// in-memory representation.
/// </remarks>
void ITranslatable.Translate(ITranslator translator)
{
Assumed.Equal(translator.Mode, TranslationDirection.WriteToStream, "write only");
string file = File;
int line = Line;
int column = Column;
translator.Translate(ref file);
translator.Translate(ref line);
translator.Translate(ref column);
}
/// <summary>
/// Reads a location from a node packet.
/// </summary>
/// <param name="translator">The translator providing the serialized location data.</param>
/// <returns>
/// The deserialized location.
/// </returns>
/// <remarks>
/// A factory is required because <see cref="ElementLocation"/> is abstract and uses specialized
/// representations.
/// </remarks>
internal static ElementLocation FactoryForDeserialization(ITranslator translator)
{
string? file = null;
int line = 0;
int column = 0;
translator.Translate(ref file);
translator.Translate(ref line);
translator.Translate(ref column);
return Create(file, line, column);
}
/// <summary>
/// Creates a location for which only the file is known.
/// </summary>
/// <param name="file">The file associated with the location, or <see langword="null"/> if unknown.</param>
/// <returns>
/// A file-only location, or <see cref="Empty"/> when <paramref name="file"/> is <see langword="null"/> or
/// empty.
/// </returns>
/// <remarks>
/// File-only locations are used for objects that were not evaluated from XML, such as newly created items.
/// </remarks>
internal static ElementLocation Create(string? file)
=> file.IsNullOrEmpty()
? Empty
: new FileOnly(file);
/// <summary>
/// Creates a location from a file and optional line number.
/// </summary>
/// <param name="file">The file containing the element, or <see langword="null"/> if unknown.</param>
/// <param name="line">The one-based line number, or <c>0</c> if unknown.</param>
/// <returns>
/// A location containing the supplied information.
/// </returns>
internal static ElementLocation Create(string? file, int line)
{
Assumed.PositiveOrZero(line, "Use zero for unknown");
string normalizedFile = file ?? string.Empty;
if (line == 0)
{
return normalizedFile.Length == 0
? Empty
: new FileOnly(normalizedFile);
}
return line <= ushort.MaxValue
? new Small(normalizedFile, (ushort)line, 0)
: new Regular(normalizedFile, line, 0);
}
/// <summary>
/// Creates a location from a file and optional line and column coordinates.
/// </summary>
/// <param name="file">The file containing the element, or <see langword="null"/> if unknown.</param>
/// <param name="line">The one-based line number, or <c>0</c> if unknown.</param>
/// <param name="column">The one-based column number, or <c>0</c> if unknown.</param>
/// <returns>
/// A location containing the supplied information. Returns <see cref="Empty"/> when
/// <paramref name="file"/> is <see langword="null"/> or empty and both coordinates are <c>0</c>.
/// </returns>
/// <remarks>
/// The returned object uses a compact representation based on the supplied information.
/// </remarks>
public static ElementLocation Create(string? file, int line, int column)
{
Assumed.PositiveOrZero(line, "Use zero for unknown");
Assumed.PositiveOrZero(column, "Use zero for unknown");
string normalizedFile = file ?? string.Empty;
if (line == 0 && column == 0)
{
return normalizedFile.Length == 0
? Empty
: new FileOnly(normalizedFile);
}
return line <= ushort.MaxValue && column <= ushort.MaxValue
? new Small(normalizedFile, (ushort)line, (ushort)column)
: new Regular(normalizedFile, line, column);
}
/// <summary>
/// Creates a location from an XML exception.
/// </summary>
/// <param name="exception">The exception containing the location information.</param>
/// <returns>
/// A location containing the exception's source URI, line, and column.
/// </returns>
internal static ElementLocation CreateFrom(XmlException exception)
{
Assumed.NotNull(exception, "Need exception context.");
string? sourceUri = exception.SourceUri;
string file = sourceUri.IsNullOrEmpty()
? string.Empty
: new Uri(sourceUri).LocalPath;
return Create(file, exception.LineNumber, exception.LinePosition);
}
/// <summary>
/// Creates a location from a file and an XML exception.
/// </summary>
/// <param name="file">The file containing the XML, or <see langword="null"/> if unknown.</param>
/// <param name="exception">The exception containing the line and column information.</param>
/// <returns>
/// A location containing the supplied file and the exception's line and column.
/// </returns>
internal static ElementLocation CreateFrom(string? file, XmlException exception)
{
Assumed.NotNull(exception, "Need exception context.");
return Create(file, exception.LineNumber, exception.LinePosition);
}
/// <summary>
/// Formats a file and optional coordinates for inclusion in a message.
/// </summary>
/// <param name="file">The file associated with the location.</param>
/// <param name="line">The one-based line number, or <c>0</c> if unknown.</param>
/// <param name="column">The one-based column number, or <c>0</c> if unknown.</param>
/// <returns>
/// The formatted location.
/// </returns>
private static string GetLocationString(string file, int line, int column)
{
if (line == 0)
{
return file;
}
return column == 0
? $"{file} ({line})"
: $"{file} ({line},{column})";
}
/// <summary>
/// Stores a location with no line or column information.
/// </summary>
/// <param name="file">The file associated with the location. This will never be <see langword="null"/>.</param>
private sealed class FileOnly(string file) : ElementLocation
{
private readonly string _file = file;
public override string File => _file;
public override int Line => 0;
public override int Column => 0;
public override int GetHashCode() => 0;
public override bool Equals(object? obj)
=> ReferenceEquals(this, obj)
|| obj switch
{
FileOnly other => StringComparer.OrdinalIgnoreCase.Equals(File, other.File),
// FileOnly can never be equal to Small or Regular.
Small or Regular => false,
ElementLocation { Line: 0, Column: 0 } other => StringComparer.OrdinalIgnoreCase.Equals(File, other.File),
_ => false,
};
}
/// <summary>
/// Stores a location when either coordinate does not fit in an unsigned 16-bit integer.
/// </summary>
/// <param name="file">The file associated with the location. This will never be <see langword="null"/>.</param>
/// <param name="line">
/// The one-based line number, or <c>0</c> if unknown. At least one coordinate will be greater than
/// <see cref="ushort.MaxValue"/>.
/// </param>
/// <param name="column">
/// The one-based column number, or <c>0</c> if unknown. At least one coordinate will be greater than
/// <see cref="ushort.MaxValue"/>.
/// </param>
/// <remarks>
/// At most one of <paramref name="line"/> and <paramref name="column"/> may be <c>0</c>. When both are
/// <c>0</c>, the file-only representation is used instead.
/// </remarks>
private sealed class Regular(string file, int line, int column) : ElementLocation
{
private readonly string _file = file;
private readonly int _line = line;
private readonly int _column = column;
public override string File => _file;
public override int Line => _line;
public override int Column => _column;
public override int GetHashCode()
=> _line ^ _column;
public override bool Equals(object? obj)
=> ReferenceEquals(this, obj)
|| obj switch
{
Regular other
=> _line == other._line
&& _column == other._column
&& StringComparer.OrdinalIgnoreCase.Equals(_file, other._file),
// Regular can never be equal to Small or FileOnly.
Small or FileOnly => false,
ElementLocation other => Equals(other),
_ => false,
};
}
/// <summary>
/// Stores the common case in which both coordinates fit in unsigned 16-bit integers.
/// </summary>
/// <param name="file">The file associated with the location. This will never be <see langword="null"/>.</param>
/// <param name="line">
/// The one-based line number, or <c>0</c> if unknown.
/// </param>
/// <param name="column">
/// The one-based column number, or <c>0</c> if unknown.
/// </param>
/// <remarks>
/// At most one of <paramref name="line"/> and <paramref name="column"/> may be <c>0</c>. When both are
/// <c>0</c>, the file-only representation is used instead.
/// <para>
/// Using compact coordinate fields reduces memory consumption in projects containing many thousands of
/// locations.
/// </para>
/// </remarks>
private sealed class Small(string file, ushort line, ushort column) : ElementLocation
{
private readonly string _file = file;
private readonly ushort _line = line;
private readonly ushort _column = column;
public override string File => _file;
public override int Line => _line;
public override int Column => _column;
public override int GetHashCode()
=> _line ^ _column;
public override bool Equals(object? obj)
=> ReferenceEquals(this, obj)
|| obj switch
{
Small other
=> _line == other._line
&& _column == other._column
&& StringComparer.OrdinalIgnoreCase.Equals(_file, other._file),
// Small can never be equal to Regular or FileOnly.
Regular or FileOnly => false,
ElementLocation other => Equals(other),
_ => false,
};
}
}