|
// 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;
#if !FEATURE_MSIOREDIST
using System.IO;
#endif
using System.Linq;
using Microsoft.Build.Shared;
using static Microsoft.Build.Experimental.BuildCheck.TaskInvocationCheckData;
#if FEATURE_MSIOREDIST
using Path = Microsoft.IO.Path;
#endif
namespace Microsoft.Build.Experimental.BuildCheck.Checks;
internal sealed class DoubleWritesCheck : Check
{
public static CheckRule SupportedRule = new CheckRule(
"BC0102",
"DoubleWrites",
ResourceUtilities.GetResourceString("BuildCheck_BC0102_Title")!,
ResourceUtilities.GetResourceString("BuildCheck_BC0102_MessageFmt")!,
new CheckConfiguration() { Severity = CheckResultSeverity.Warning });
public override string FriendlyName => "MSBuild.DoubleWritesCheck";
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.RegisterTaskInvocationAction(TaskInvocationAction);
}
internal override bool IsBuiltIn => true;
/// <summary>
/// Contains the first project file + task that wrote the given file during the build.
/// </summary>
private readonly Dictionary<string, (string projectFilePath, string taskName)> _filesWritten = new(StringComparer.CurrentCultureIgnoreCase);
private void TaskInvocationAction(BuildCheckDataContext<TaskInvocationCheckData> context)
{
// This check uses a hard-coded list of tasks known to write files.
switch (context.Data.TaskName)
{
case "Csc":
case "Vbc":
case "Fsc": CheckCompilerTask(context); break;
case "Copy": CheckCopyTask(context); break;
}
}
private void CheckCompilerTask(BuildCheckDataContext<TaskInvocationCheckData> context)
{
var taskParameters = context.Data.Parameters;
// Compiler tasks have several parameters representing files being written.
CheckParameter("OutputAssembly");
CheckParameter("OutputRefAssembly");
CheckParameter("DocumentationFile");
CheckParameter("PdbFile");
void CheckParameter(string parameterName)
{
if (taskParameters.TryGetValue(parameterName, out TaskParameter? taskParameter))
{
string outputPath = taskParameter.EnumerateStringValues().FirstOrDefault() ?? "";
CheckWrite(context, outputPath);
}
}
}
private void CheckCopyTask(BuildCheckDataContext<TaskInvocationCheckData> context)
{
var taskParameters = context.Data.Parameters;
// The destination is specified as either DestinationFolder or DestinationFiles.
if (taskParameters.TryGetValue("SourceFiles", out TaskParameter? sourceFiles) &&
taskParameters.TryGetValue("DestinationFolder", out TaskParameter? destinationFolder))
{
string destinationFolderPath = destinationFolder.EnumerateStringValues().FirstOrDefault() ?? "";
foreach (string sourceFilePath in sourceFiles.EnumerateStringValues())
{
CheckWrite(context, Path.Combine(destinationFolderPath, Path.GetFileName(sourceFilePath)));
}
}
else if (taskParameters.TryGetValue("DestinationFiles", out TaskParameter? destinationFiles))
{
foreach (string destinationFilePath in destinationFiles.EnumerateStringValues())
{
CheckWrite(context, destinationFilePath);
}
}
}
private void CheckWrite(BuildCheckDataContext<TaskInvocationCheckData> context, string fileBeingWritten)
{
if (!string.IsNullOrEmpty(fileBeingWritten))
{
// Absolutize the path. Note that if a path used during a build is relative, it is relative to the directory
// of the project being built, regardless of the project/import in which it occurs.
fileBeingWritten = Path.GetFullPath(fileBeingWritten, context.Data.ProjectFileDirectory);
if (_filesWritten.TryGetValue(fileBeingWritten, out (string projectFilePath, string taskName) existingEntry))
{
context.ReportResult(BuildCheckResult.CreateBuiltIn(
SupportedRule,
context.Data.TaskInvocationLocation,
context.Data.TaskName,
existingEntry.taskName,
Path.GetFileName(context.Data.ProjectFilePath),
Path.GetFileName(existingEntry.projectFilePath),
fileBeingWritten));
}
else
{
_filesWritten.Add(fileBeingWritten, (context.Data.ProjectFilePath, context.Data.TaskName));
}
}
}
}
|