File: BuildCheck\Checks\NoEnvironmentVariablePropertyCheck.cs
Web Access
Project: ..\..\..\src\Build\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 System;
using System.Collections.Generic;
using Microsoft.Build.BuildCheck.Infrastructure;
using Microsoft.Build.Shared;
 
namespace Microsoft.Build.Experimental.BuildCheck.Checks;
 
internal sealed class NoEnvironmentVariablePropertyCheck : Check
{
    public static CheckRule SupportedRule = new CheckRule(
        "BC0103",
        "NoEnvironmentVariablePropertyCheck",
        ResourceUtilities.GetResourceString("BuildCheck_BC0103_Title")!,
        ResourceUtilities.GetResourceString("BuildCheck_BC0103_MessageFmt")!,
        new CheckConfiguration() { Severity = CheckResultSeverity.Suggestion });
 
    private const string RuleId = "BC0103";
 
    private const string VerboseEnvVariableOutputKey = "allow_displaying_environment_variable_value";
 
    private readonly Queue<(string projectPath, BuildCheckDataContext<EnvironmentVariableCheckData>)> _buildCheckResults = new Queue<(string, BuildCheckDataContext<EnvironmentVariableCheckData>)>();
 
    /// <summary>
    /// Contains the list of viewed environment variables.
    /// </summary>
    private readonly HashSet<EnvironmentVariableIdentityKey> _environmentVariablesCache = new HashSet<EnvironmentVariableIdentityKey>();
 
    private bool _isVerboseEnvVarOutput;
    private EvaluationCheckScope _scope;
 
    public override string FriendlyName => "MSBuild.NoEnvironmentVariablePropertyCheck";
 
    public override IReadOnlyList<CheckRule> SupportedRules { get; } = [SupportedRule];
 
    public override void Initialize(ConfigurationContext configurationContext)
    {
        _scope = configurationContext.CheckConfig[0].EvaluationCheckScope;
        foreach (CustomConfigurationData customConfigurationData in configurationContext.CustomConfigurationData)
        {
            bool? isVerboseEnvVarOutput = GetVerboseEnvVarOutputConfig(customConfigurationData, RuleId);
            _isVerboseEnvVarOutput = isVerboseEnvVarOutput.HasValue && isVerboseEnvVarOutput.Value;
        }
 
        CheckScopeClassifier.NotifyOnScopingReadiness += HandleScopeReadiness;
    }
 
    public override void RegisterActions(IBuildCheckRegistrationContext registrationContext) => registrationContext.RegisterEnvironmentVariableReadAction(ProcessEnvironmentVariableReadAction);
 
    internal override bool IsBuiltIn => true;
 
    private void ProcessEnvironmentVariableReadAction(BuildCheckDataContext<EnvironmentVariableCheckData> context)
    {
        EnvironmentVariableIdentityKey identityKey = new(context.Data.EnvironmentVariableName, context.Data.EnvironmentVariableLocation);
        if (!_environmentVariablesCache.Contains(identityKey))
        {
            // Scope information is available after evaluation of the project file. If it is not ready, we will report the check later.
            if (!CheckScopeClassifier.IsScopingReady(_scope))
            {
                _buildCheckResults.Enqueue((context.Data.ProjectFilePath, context));
            }
            else if (CheckScopeClassifier.IsActionInObservedScope(_scope, context.Data.EnvironmentVariableLocation.File, context.Data.ProjectFilePath))
            {
                context.ReportResult(BuildCheckResult.CreateBuiltIn(
                    SupportedRule,
                    context.Data.EnvironmentVariableLocation,
                    GetFormattedMessage(context.Data.EnvironmentVariableName, context.Data.EnvironmentVariableValue)));
            }
 
            _environmentVariablesCache.Add(identityKey);
        }
    }
 
    private static bool? GetVerboseEnvVarOutputConfig(CustomConfigurationData customConfigurationData, string ruleId) => customConfigurationData.RuleId.Equals(ruleId, StringComparison.InvariantCultureIgnoreCase)
            && (customConfigurationData.ConfigurationData?.TryGetValue(VerboseEnvVariableOutputKey, out string? configVal) ?? false)
            ? bool.Parse(configVal)
            : null;
 
    private void HandleScopeReadiness()
    {
        while (_buildCheckResults.Count > 0)
        {
            (string projectPath, BuildCheckDataContext<EnvironmentVariableCheckData> context) = _buildCheckResults.Dequeue();
            if (!CheckScopeClassifier.IsActionInObservedScope(_scope, context.Data.EnvironmentVariableLocation.File, projectPath))
            {
                continue;
            }
 
            context.ReportResult(BuildCheckResult.CreateBuiltIn(
                SupportedRule,
                context.Data.EnvironmentVariableLocation,
                GetFormattedMessage(context.Data.EnvironmentVariableName, context.Data.EnvironmentVariableValue)));
        }
 
        CheckScopeClassifier.NotifyOnScopingReadiness -= HandleScopeReadiness;
    }
 
    private string GetFormattedMessage(string envVariableName, string envVariableValue) => _isVerboseEnvVarOutput ? string.Format(ResourceUtilities.GetResourceString("BuildCheck_BC0103_MessageAddendum")!, envVariableName, envVariableValue) : $"'{envVariableName}'";
 
    internal class EnvironmentVariableIdentityKey(string environmentVariableName, IMSBuildElementLocation location) : IEquatable<EnvironmentVariableIdentityKey>
    {
        public string EnvironmentVariableName { get; } = environmentVariableName;
 
        public IMSBuildElementLocation Location { get; } = location;
 
        public override bool Equals(object? obj) => Equals(obj as EnvironmentVariableIdentityKey);
 
        public bool Equals(EnvironmentVariableIdentityKey? other) =>
            other != null &&
            EnvironmentVariableName == other.EnvironmentVariableName &&
            Location.File == other.Location.File &&
            Location.Line == other.Location.Line &&
            Location.Column == other.Location.Column;
 
        public override int GetHashCode()
        {
            int hashCode = 17;
            hashCode = hashCode * 31 + (Location.File != null ? Location.File.GetHashCode() : 0);
            hashCode = hashCode * 31 + Location.Line.GetHashCode();
            hashCode = hashCode * 31 + Location.Column.GetHashCode();
 
            return hashCode;
        }
    }
}