File: ReservedPropertyNames.cs
Web Access
Project: Microsoft.Build.csproj (Microsoft.Build)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Build.Collections;
using Microsoft.Build.Shared;

namespace Microsoft.Build.Internal;

/// <summary>
///  Contains names for reserved and other well-known properties, together with related configuration
///  constants used by MSBuild.
/// </summary>
internal static class ReservedPropertyNames
{
    private const string MSBuild = "MSBuild";
    private const string ProjectPrefix = "MSBuildProject";
    private const string ThisFilePrefix = "MSBuildThisFile";

    // Keep these reserved property constants synchronized with ReservedPropertyKind and TryGetKind.
    internal const string ProjectDirectory = $"{ProjectPrefix}Directory";
    internal const string ProjectDirectoryNoRoot = $"{ProjectPrefix}DirectoryNoRoot";
    internal const string ProjectFile = $"{ProjectPrefix}File";
    internal const string ProjectExtension = $"{ProjectPrefix}Extension";
    internal const string ProjectFullPath = $"{ProjectPrefix}FullPath";
    internal const string ProjectName = $"{ProjectPrefix}Name";

    internal const string ThisFileDirectory = $"{ThisFilePrefix}Directory";
    internal const string ThisFileDirectoryNoRoot = $"{ThisFilePrefix}DirectoryNoRoot";
    internal const string ThisFile = ThisFilePrefix; // We intentionally use MSBuildThisFile rather than MSBuildThisFileFile.
    internal const string ThisFileExtension = $"{ThisFilePrefix}Extension";
    internal const string ThisFileFullPath = $"{ThisFilePrefix}FullPath";
    internal const string ThisFileName = $"{ThisFilePrefix}Name";

    internal const string BinPath = $"{MSBuild}BinPath";
    internal const string ProjectDefaultTargets = $"{MSBuild}ProjectDefaultTargets";
    internal const string ToolsPath = MSBuildConstants.ToolsPath;
    internal const string ToolsVersion = $"{MSBuild}ToolsVersion";
    internal const string MSBuildRuntimeType = $"{MSBuild}RuntimeType";
    internal const string StartupDirectory = $"{MSBuild}StartupDirectory";
    internal const string BuildNodeCount = $"{MSBuild}NodeCount";
    internal const string LastTaskResult = $"{MSBuild}LastTaskResult";
    internal const string ProgramFiles32 = $"{MSBuild}ProgramFiles32";
    internal const string AssemblyVersion = $"{MSBuild}AssemblyVersion";
    internal const string Version = $"{MSBuild}Version";
    internal const string Interactive = $"{MSBuild}Interactive";
    internal const string MSBuildDisableFeaturesFromVersion = $"{MSBuild}DisableFeaturesFromVersion";

    // These property names are intentionally not reserved.
    internal const string ExtensionsPath = $"{MSBuild}ExtensionsPath";
    internal const string ExtensionsPath32 = $"{MSBuild}ExtensionsPath32";
    internal const string ExtensionsPath64 = $"{MSBuild}ExtensionsPath64";
    internal const string UserExtensionsPath = $"{MSBuild}UserExtensionsPath";
    internal const string LocalAppData = "LocalAppData";
    internal const string FileVersion = $"{MSBuild}FileVersion";
    internal const string SemanticVersion = $"{MSBuild}SemanticVersion";
    internal const string OSName = "OS";
    internal const string FrameworkToolsRoot = $"{MSBuild}FrameworkToolsRoot";

    // Related configuration names and path suffixes.
    internal const string OverrideTasksPath = $"{MSBuild}OverrideTasksPath";
    internal const string DefaultOverrideToolsVersion = "DefaultOverrideToolsVersion";
    internal const string ExtensionsPathSuffix = MSBuild;
    internal const string UserExtensionsPathSuffix = $@"Microsoft\{MSBuild}";

    /// <summary>
    ///  Determines whether a property name starts with <c>MSBuild</c>, ignoring case.
    /// </summary>
    /// <param name="propertyName">The property name to examine.</param>
    /// <returns>
    ///  <see langword="true"/> if <paramref name="propertyName"/> starts with <c>MSBuild</c>
    ///  and contains at least one additional character; otherwise, <see langword="false"/>.
    /// </returns>
    public static bool StartsWithMSBuild(string propertyName)
        => StartsWithMSBuild(propertyName, startIndex: 0, propertyName.Length);

    /// <summary>
    ///  Determines whether a segment of a string starts with <c>MSBuild</c>, ignoring case.
    /// </summary>
    /// <param name="propertyName">The string containing the property name.</param>
    /// <param name="startIndex">The zero-based start index of the property name.</param>
    /// <param name="length">The length of the property name.</param>
    /// <returns>
    ///  <see langword="true"/> if the specified segment starts with <c>MSBuild</c> and contains
    ///  at least one additional character; otherwise, <see langword="false"/>.
    /// </returns>
    public static bool StartsWithMSBuild(string propertyName, int startIndex, int length)
        => length > MSBuild.Length && MSBuildNameIgnoreCaseComparer.Default.Equals(MSBuild, propertyName, startIndex, MSBuild.Length);

    /// <summary>
    ///  Determines whether a property may be set only by MSBuild.
    /// </summary>
    /// <param name="property">The property name to examine.</param>
    /// <returns>
    ///  <see langword="true"/> if <paramref name="property"/> is reserved; otherwise,
    ///  <see langword="false"/>.
    /// </returns>
    internal static bool IsReservedProperty(string property)
        => property is not null && TryGetKind(property, out _);

    /// <summary>
    ///  Attempts to identify a reserved MSBuild property.
    /// </summary>
    /// <param name="propertyName">The property name to examine.</param>
    /// <param name="result">The identified property, or <see cref="ReservedPropertyKind.None"/>.</param>
    /// <returns>
    ///  <see langword="true"/> if <paramref name="propertyName"/> is reserved; otherwise,
    ///  <see langword="false"/>.
    /// </returns>
    /// <remarks>
    ///  The <c>MSBuildExtensionsPath*</c> and <c>MSBuildUserExtensionsPath</c> properties are
    ///  intentionally excluded because tasks must be able to override them.
    /// </remarks>
    public static bool TryGetKind(string propertyName, out ReservedPropertyKind result)
        => TryGetKind(propertyName, 0, propertyName.Length, out result);

    /// <summary>
    ///  Attempts to identify a reserved MSBuild property in a segment of a string.
    /// </summary>
    /// <param name="propertyName">The string containing the property name.</param>
    /// <param name="startIndex">The zero-based start index of the property name.</param>
    /// <param name="length">The length of the property name.</param>
    /// <param name="result">The identified property, or <see cref="ReservedPropertyKind.None"/>.</param>
    /// <returns>
    ///  <see langword="true"/> if the specified segment identifies a reserved property; otherwise,
    ///  <see langword="false"/>.
    /// </returns>
    public static bool TryGetKind(string propertyName, int startIndex, int length, out ReservedPropertyKind result)
    {
        const int PrefixLength = 7; // MSBuild.Length

        int endIndex = startIndex + length;

        if (length < 14 || !MatchesAt(propertyName, MSBuild, startIndex, endIndex))
        {
            result = ReservedPropertyKind.None;
            return false;
        }

        char nextChar = propertyName[startIndex + PrefixLength];

        if (nextChar is 'P' or 'p' && MatchesAt(propertyName, "Project", startIndex + PrefixLength, endIndex))
        {
            result = GetProjectPropertyKind(propertyName, startIndex, length);
            return result is not ReservedPropertyKind.None;
        }

        if (nextChar is 'T' or 't' && MatchesAt(propertyName, "ThisFile", startIndex + PrefixLength, endIndex))
        {
            result = GetThisFilePropertyKind(propertyName, startIndex, length);
            return result is not ReservedPropertyKind.None;
        }

        result = length switch
        {
            14 => nextChar switch
            {
                // MSBuildBinPath
                'B' or 'b' when MatchesAt(propertyName, "BinPath", startIndex + PrefixLength, endIndex)
                    => ReservedPropertyKind.BinPath,

                // MSBuildVersion
                'V' or 'v' when MatchesAt(propertyName, "Version", startIndex + PrefixLength, endIndex)
                    => ReservedPropertyKind.Version,

                _ => ReservedPropertyKind.None,
            },

            16 => nextChar switch
            {
                // MSBuildNodeCount
                'N' or 'n' when MatchesAt(propertyName, "NodeCount", startIndex + PrefixLength, endIndex)
                    => ReservedPropertyKind.BuildNodeCount,

                // MSBuildToolsPath
                'T' or 't' when MatchesAt(propertyName, "ToolsPath", startIndex + PrefixLength, endIndex)
                    => ReservedPropertyKind.ToolsPath,

                _ => ReservedPropertyKind.None,
            },

            18 => nextChar switch
            {
                // MSBuildInteractive
                'I' or 'i' when MatchesAt(propertyName, "Interactive", startIndex + PrefixLength, endIndex)
                    => ReservedPropertyKind.Interactive,

                // MSBuildRuntimeType
                'R' or 'r' when MatchesAt(propertyName, "RuntimeType", startIndex + PrefixLength, endIndex)
                    => ReservedPropertyKind.RuntimeType,

                _ => ReservedPropertyKind.None,
            },

            // MSBuildToolsVersion
            19 when MatchesAt(propertyName, "ToolsVersion", startIndex + PrefixLength, endIndex)
                => ReservedPropertyKind.ToolsVersion,

            21 => nextChar switch
            {
                // MSBuildLastTaskResult
                'L' or 'l' when MatchesAt(propertyName, "LastTaskResult", startIndex + PrefixLength, endIndex)
                    => ReservedPropertyKind.LastTaskResult,

                // MSBuildProgramFiles32
                'P' or 'p' when MatchesAt(propertyName, "ProgramFiles32", startIndex + PrefixLength, endIndex)
                    => ReservedPropertyKind.ProgramFiles32,

                _ => ReservedPropertyKind.None,
            },

            // MSBuildAssemblyVersion
            22 when MatchesAt(propertyName, "AssemblyVersion", startIndex + PrefixLength, endIndex)
                => ReservedPropertyKind.AssemblyVersion,

            // MSBuildStartupDirectory
            23 when MatchesAt(propertyName, "StartupDirectory", startIndex + PrefixLength, endIndex)
                => ReservedPropertyKind.StartupDirectory,

            // MSBuildDisableFeaturesFromVersion
            33 when MatchesAt(propertyName, "DisableFeaturesFromVersion", startIndex + PrefixLength, endIndex)
                => ReservedPropertyKind.DisableFeaturesFromVersion,

            _ => ReservedPropertyKind.None,
        };

        return result is not ReservedPropertyKind.None;

        static ReservedPropertyKind GetProjectPropertyKind(string propertyName, int startIndex, int length)
        {
            const int ProjectPrefixLength = 14; // ProjectPrefix.Length

            int endIndex = startIndex + length;
            int suffixStartIndex = startIndex + ProjectPrefixLength;

            return length switch
            {
                18 => propertyName[suffixStartIndex] switch
                {
                    // MSBuildProjectFile
                    'F' or 'f' when MatchesAt(propertyName, "File", suffixStartIndex, endIndex)
                        => ReservedPropertyKind.ProjectFile,

                    // MSBuildProjectName
                    'N' or 'n' when MatchesAt(propertyName, "Name", suffixStartIndex, endIndex)
                        => ReservedPropertyKind.ProjectName,

                    _ => ReservedPropertyKind.None,
                },

                // MSBuildProjectFullPath
                22 when MatchesAt(propertyName, "FullPath", suffixStartIndex, endIndex)
                    => ReservedPropertyKind.ProjectFullPath,

                23 => propertyName[suffixStartIndex] switch
                {
                    // MSBuildProjectDirectory
                    'D' or 'd' when MatchesAt(propertyName, "Directory", suffixStartIndex, endIndex)
                        => ReservedPropertyKind.ProjectDirectory,

                    // MSBuildProjectExtension
                    'E' or 'e' when MatchesAt(propertyName, "Extension", suffixStartIndex, endIndex)
                        => ReservedPropertyKind.ProjectExtension,

                    _ => ReservedPropertyKind.None,
                },

                // MSBuildProjectDefaultTargets
                28 when MatchesAt(propertyName, "DefaultTargets", suffixStartIndex, endIndex)
                    => ReservedPropertyKind.ProjectDefaultTargets,

                // MSBuildProjectDirectoryNoRoot
                29 when MatchesAt(propertyName, "DirectoryNoRoot", suffixStartIndex, endIndex)
                    => ReservedPropertyKind.ProjectDirectoryNoRoot,

                _ => ReservedPropertyKind.None,
            };
        }

        static ReservedPropertyKind GetThisFilePropertyKind(string propertyName, int startIndex, int length)
        {
            const int ThisFilePrefixLength = 15; // ThisFilePrefix.Length

            int endIndex = startIndex + length;
            int suffixStartIndex = startIndex + ThisFilePrefixLength;

            return length switch
            {
                // MSBuildThisFile
                15 => ReservedPropertyKind.ThisFile,

                // MSBuildThisFileName
                19 when MatchesAt(propertyName, "Name", suffixStartIndex, endIndex)
                    => ReservedPropertyKind.ThisFileName,

                // MSBuildThisFileFullPath
                23 when MatchesAt(propertyName, "FullPath", suffixStartIndex, endIndex)
                    => ReservedPropertyKind.ThisFileFullPath,

                24 => propertyName[suffixStartIndex] switch
                {
                    // MSBuildThisFileDirectory
                    'D' or 'd' when MatchesAt(propertyName, "Directory", suffixStartIndex, endIndex)
                        => ReservedPropertyKind.ThisFileDirectory,

                    // MSBuildThisFileExtension
                    'E' or 'e' when MatchesAt(propertyName, "Extension", suffixStartIndex, endIndex)
                        => ReservedPropertyKind.ThisFileExtension,

                    _ => ReservedPropertyKind.None,
                },

                // MSBuildThisFileDirectoryNoRoot
                30 when MatchesAt(propertyName, "DirectoryNoRoot", suffixStartIndex, endIndex)
                    => ReservedPropertyKind.ThisFileDirectoryNoRoot,

                _ => ReservedPropertyKind.None,
            };
        }

        static bool MatchesAt(string propertyName, string expected, int startIndex, int endIndex)
            => endIndex >= startIndex + expected.Length
                && MSBuildNameIgnoreCaseComparer.Default.Equals(expected, propertyName, startIndex, expected.Length);
    }
}