File: BuildCheck\Checks\CopyAlwaysCheck.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.Collections.Generic;
using System.IO;
using Microsoft.Build.Collections;
using Microsoft.Build.Construction;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
 
namespace Microsoft.Build.Experimental.BuildCheck.Checks;
internal class CopyAlwaysCheck : Check
{
    private const string RuleId = "BC0106";
    public static CheckRule SupportedRule = new CheckRule(RuleId, "AvoidCopyAlways",
        ResourceUtilities.GetResourceString("BuildCheck_BC0106_Title")!,
        ResourceUtilities.GetResourceString("BuildCheck_BC0106_MessageFmt")!,
        new CheckConfiguration() { RuleId = RuleId, Severity = CheckResultSeverity.Warning });
 
    public override string FriendlyName => "MSBuild.CopyAlwaysCheck";
 
    public override IReadOnlyList<CheckRule> SupportedRules { get; } = [SupportedRule];
 
    public override void Initialize(ConfigurationContext configurationContext)
    {
        /* This is it - no custom configuration */
    }
 
    public override void RegisterActions(IBuildCheckRegistrationContext registrationContext)
    {
        registrationContext.RegisterEvaluatedPropertiesAction(EvaluatedPropertiesAction);
        registrationContext.RegisterEvaluatedItemsAction(EvaluatedItemsAction);
    }
 
    internal override bool IsBuiltIn => true;
 
    private readonly HashSet<string> _projectsSeen = new(MSBuildNameIgnoreCaseComparer.Default);
 
    private void EvaluatedPropertiesAction(BuildCheckDataContext<EvaluatedPropertiesCheckData> context)
    {
        // We want to avoid repeated checking of a same project (as it might be evaluated multiple times)
        //  for this reason we use a hashset with already seen projects.
        // We want to do the same prevention for both registered actions: EvaluatedPropertiesAction and EvaluatedItemsAction.
        //  To avoid the need to have separate hashset for each of those functions - we use a single one and we use the fact that
        //  both functions are always called after each other (EvaluatedPropertiesAction first, then EvaluatedItemsAction),
        //  so this function just checks the hashset (not to prevent execution of EvaluatedItemsAction) and EvaluatedItemsAction
        //  updates the hashset.
        if (_projectsSeen.Contains(context.Data.ProjectFilePath))
        {
            return;
        }
 
        context.Data.EvaluatedProperties.TryGetValue("SkipUnchangedFilesOnCopyAlways", out string? skipUnchanged);
 
        if (skipUnchanged.IsMSBuildTrueString())
        {
            // Now we know that copy logic is optimized - so we do not need to check items. Avoiding the items check by inserting into lookup.
            _projectsSeen.Add(context.Data.ProjectFilePath);
        }
    }
 
    private void EvaluatedItemsAction(BuildCheckDataContext<EvaluatedItemsCheckData> context)
    {
        // We want to avoid repeated checking of a same project (as it might be evaluated multiple times)
        //  for this reason we use a hashset with already seen projects.
        if (!_projectsSeen.Add(context.Data.ProjectFilePath))
        {
            return;
        }
 
        foreach (ItemData itemData in context.Data.EnumerateItemsOfTypes([ItemNames.Content, ItemNames.Compile, ItemNames.None, ItemNames.EmbeddedResource]))
        {
            foreach (KeyValuePair<string, string> keyValuePair in itemData.EnumerateMetadata())
            {
                if (MSBuildNameIgnoreCaseComparer.Default.Equals(keyValuePair.Key, ItemMetadataNames.copyToOutputDirectory))
                {
                    if (MSBuildNameIgnoreCaseComparer.Default.Equals(keyValuePair.Value, ItemMetadataNames.copyAlways))
                    {
                        // Project {0} specifies '{0}' item '{1}', ...
                        context.ReportResult(BuildCheckResult.Create(
                            SupportedRule,
                            // Populating precise location tracked via https://github.com/dotnet/msbuild/issues/10383
                            ElementLocation.EmptyLocation,
                            Path.GetFileName(context.Data.ProjectFilePath),
                            itemData.Type,
                            itemData.EvaluatedInclude));
                    }
 
                    break;
                }
            }
        }
    }
}