File: BuildCheck\Checks\SharedOutputPathCheck.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.Construction;
using Microsoft.Build.Shared;
using Microsoft.Build.Collections;
 
namespace Microsoft.Build.Experimental.BuildCheck.Checks;
 
internal sealed class SharedOutputPathCheck : Check
{
    private const string RuleId = "BC0101";
    public static CheckRule SupportedRule = new CheckRule(RuleId, "ConflictingOutputPath",
        ResourceUtilities.GetResourceString("BuildCheck_BC0101_Title")!,
        ResourceUtilities.GetResourceString("BuildCheck_BC0101_MessageFmt")!,
        new CheckConfiguration() { RuleId = RuleId, Severity = CheckResultSeverity.Warning });
 
    public override string FriendlyName => "MSBuild.SharedOutputPathCheck";
 
    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);
    }
 
    internal override bool IsBuiltIn => true;
 
    private readonly Dictionary<string, string> _projectsPerOutputPath = new(MSBuildNameIgnoreCaseComparer.Default);
    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
        if (!_projectsSeen.Add(context.Data.ProjectFilePath))
        {
            return;
        }
 
        string? binPath, objPath;
 
        context.Data.EvaluatedProperties.TryGetValue("OutputPath", out binPath);
        context.Data.EvaluatedProperties.TryGetValue("IntermediateOutputPath", out objPath);
 
        string? absoluteBinPath = CheckAndAddFullOutputPath(binPath, context);
        // Check objPath only if it is different from binPath
        if (
            !string.IsNullOrEmpty(objPath) && !string.IsNullOrEmpty(absoluteBinPath) &&
            !MSBuildNameIgnoreCaseComparer.Default.Equals(objPath, binPath)
            && !MSBuildNameIgnoreCaseComparer.Default.Equals(objPath, absoluteBinPath)
        )
        {
            CheckAndAddFullOutputPath(objPath, context);
        }
    }
 
    private string? CheckAndAddFullOutputPath(string? path, BuildCheckDataContext<EvaluatedPropertiesCheckData> context)
    {
        if (string.IsNullOrEmpty(path))
        {
            return path;
        }
 
        string projectPath = context.Data.ProjectFilePath;
        path = BuildCheckUtilities.RootEvaluatedPath(path!, projectPath);
 
        if (_projectsPerOutputPath.TryGetValue(path!, out string? conflictingProject))
        {
            context.ReportResult(BuildCheckResult.CreateBuiltIn(
                SupportedRule,
                // Populating precise location tracked via https://github.com/orgs/dotnet/projects/373/views/1?pane=issue&itemId=58661732
                ElementLocation.EmptyLocation,
                Path.GetFileName(projectPath),
                Path.GetFileName(conflictingProject),
                path!));
        }
        else
        {
            _projectsPerOutputPath[path!] = projectPath;
        }
 
        return path;
    }
}