| File: BuildCheck\Checks\DoubleWritesCheck.cs | Web Access |
| Project: src\msbuild\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; #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)); } } } }