Skip to content

Commit

Permalink
Merge pull request #10014 from YuliiaKovalova/dev/ykovalova/analyzers…
Browse files Browse the repository at this point in the history
…_aquisition_experience

Analyzers acquisition experience
  • Loading branch information
YuliiaKovalova committed Apr 25, 2024
2 parents 47ba51c + b2d984e commit bd0b1e4
Show file tree
Hide file tree
Showing 37 changed files with 781 additions and 176 deletions.
26 changes: 25 additions & 1 deletion src/Build.UnitTests/Evaluation/Expander_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
using System.Threading;
using System.Xml;
using Microsoft.Build.BackEnd;
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.Collections;
using Microsoft.Build.Engine.UnitTests;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Experimental.BuildCheck;
using Microsoft.Build.Framework;
using Microsoft.Build.Internal;
using Microsoft.Build.Shared;
Expand All @@ -23,7 +26,6 @@
using Microsoft.Win32;
using Shouldly;
using Xunit;
using Xunit.NetCore.Extensions;
using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException;
using ProjectHelpers = Microsoft.Build.UnitTests.BackEnd.ProjectHelpers;
using ProjectItemInstanceFactory = Microsoft.Build.Execution.ProjectItemInstance.TaskItem.ProjectItemInstanceFactory;
Expand Down Expand Up @@ -5055,5 +5057,27 @@ private static bool ICUModeAvailable()
int version = bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0];
return version != 0 && version == sortVersion.FullVersion;
}

[Fact]
public void PropertyFunctionRegisterAnalyzer()
{
using (var env = TestEnvironment.Create())
{
var logger = new MockLogger();
ILoggingService loggingService = LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1);
loggingService.RegisterLogger(logger);
var loggingContext = new MockLoggingContext(
loggingService,
new BuildEventContext(0, 0, BuildEventContext.InvalidProjectContextId, 0, 0));
var dummyAssemblyFile = env.CreateFile(env.CreateFolder(), "test.dll");

var result = new Expander<ProjectPropertyInstance, ProjectItemInstance>(new PropertyDictionary<ProjectPropertyInstance>(), FileSystems.Default)
.ExpandIntoStringLeaveEscaped($"$([MSBuild]::RegisterAnalyzer({dummyAssemblyFile.Path}))", ExpanderOptions.ExpandProperties, MockElementLocation.Instance, loggingContext);

result.ShouldBe(Boolean.TrueString);
_ = logger.AllBuildEvents.Select(be => be.ShouldBeOfType<BuildCheckAcquisitionEventArgs>());
logger.AllBuildEvents.Count.ShouldBe(1);
}
}
}
}
1 change: 1 addition & 0 deletions src/Build/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
[assembly: InternalsVisibleTo("Microsoft.Build.Conversion.Core, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]
[assembly: InternalsVisibleTo("Microsoft.Build.Conversion.Unittest, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")]
[assembly: InternalsVisibleTo("Microsoft.Build.Tasks.Cop, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")]
[assembly: InternalsVisibleTo("Microsoft.Build.BuildCheck.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")]
// DO NOT expose Internals to "Microsoft.Build.UnitTests.OM.OrcasCompatibility" as this assembly is supposed to only see public interface

// This will enable passing the SafeDirectories flag to any P/Invoke calls/implementations within the assembly,
Expand Down
12 changes: 8 additions & 4 deletions src/Build/BackEnd/Components/Logging/EvaluationLoggingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
namespace Microsoft.Build.BackEnd.Components.Logging
{
/// <summary>
/// Logging context and helpers for evaluation logging
/// Logging context and helpers for evaluation logging.
/// </summary>
internal class EvaluationLoggingContext : LoggingContext
{
private readonly string _projectFile;

public EvaluationLoggingContext(ILoggingService loggingService, BuildEventContext buildEventContext, string projectFile) :
base(
public EvaluationLoggingContext(ILoggingService loggingService, BuildEventContext buildEventContext, string projectFile)
: base(
loggingService,
loggingService.CreateEvaluationBuildEventContext(buildEventContext.NodeId, buildEventContext.SubmissionId))
{
Expand All @@ -33,8 +33,12 @@ public void LogProjectEvaluationStarted()
}

/// <summary>
/// Log that the project has finished
/// Logs that the project evaluation has finished.
/// </summary>
/// <param name="globalProperties">Global properties used in the project evaluation.</param>
/// <param name="properties">Properties used in the project evaluation.</param>
/// <param name="items">Items used in the project evaluation.</param>
/// <param name="profilerResult">Parameter contains the profiler result of the project evaluation.</param>
internal void LogProjectEvaluationFinished(IEnumerable globalProperties, IEnumerable properties, IEnumerable items, ProfilerResult? profilerResult)
{
ErrorUtilities.VerifyThrow(IsValid, "invalid");
Expand Down
1 change: 1 addition & 0 deletions src/Build/BackEnd/Components/Logging/EventSourceSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ internal void UnregisterAllEventHandlers()
StatusEventRaised = null;
AnyEventRaised = null;
TelemetryLogged = null;
BuildCheckEventRaised = null;
}

#endregion
Expand Down
8 changes: 4 additions & 4 deletions src/Build/BuildCheck/Acquisition/AnalyzerAcquisitionData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ namespace Microsoft.Build.BuildCheck.Acquisition;
// https://github.com/dotnet/msbuild/issues/9633
// Acquisition
// define the data that will be passed to the acquisition module (and remoted if needed)
internal class AnalyzerAcquisitionData(string data)
internal class AnalyzerAcquisitionData(string assemblyPath)
{
public string Data { get; init; } = data;
public string AssemblyPath { get; init; } = assemblyPath;
}

internal static class AnalyzerAcquisitionDataExtensions
{
public static AnalyzerAcquisitionData ToAnalyzerAcquisitionData(this BuildCheckAcquisitionEventArgs eventArgs) =>
new(eventArgs.AcquisitionData);
new(eventArgs.AcquisitionPath);

public static BuildCheckAcquisitionEventArgs ToBuildEventArgs(this AnalyzerAcquisitionData data) => new(data.Data);
public static BuildCheckAcquisitionEventArgs ToBuildEventArgs(this AnalyzerAcquisitionData data) => new(data.AssemblyPath);
}
70 changes: 62 additions & 8 deletions src/Build/BuildCheck/Acquisition/BuildCheckAcquisitionModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,73 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Build.BuildCheck.Analyzers;
using System.Reflection;
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.BuildCheck.Infrastructure;
using Microsoft.Build.Experimental.BuildCheck;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;

namespace Microsoft.Build.BuildCheck.Acquisition;

internal class BuildCheckAcquisitionModule
internal class BuildCheckAcquisitionModule : IBuildCheckAcquisitionModule
{
private static T Construct<T>() where T : new() => new();
public BuildAnalyzerFactory CreateBuildAnalyzerFactory(AnalyzerAcquisitionData analyzerAcquisitionData)
private readonly ILoggingService _loggingService;

internal BuildCheckAcquisitionModule(ILoggingService loggingService) => _loggingService = loggingService;

#if FEATURE_ASSEMBLYLOADCONTEXT
/// <summary>
/// AssemblyContextLoader used to load DLLs outside of msbuild.exe directory.
/// </summary>
private static readonly CoreClrAssemblyLoader s_coreClrAssemblyLoader = new();
#endif

/// <summary>
/// Creates a list of factory delegates for building analyzer rules instances from a given assembly path.
/// </summary>
public List<BuildAnalyzerFactory> CreateBuildAnalyzerFactories(AnalyzerAcquisitionData analyzerAcquisitionData, BuildEventContext buildEventContext)
{
// Acquisition module - https://github.com/dotnet/msbuild/issues/9633
return Construct<SharedOutputPathAnalyzer>;
var analyzersFactories = new List<BuildAnalyzerFactory>();

try
{
Assembly? assembly = null;
#if FEATURE_ASSEMBLYLOADCONTEXT
assembly = s_coreClrAssemblyLoader.LoadFromPath(analyzerAcquisitionData.AssemblyPath);
#else
assembly = Assembly.LoadFrom(analyzerAcquisitionData.AssemblyPath);
#endif

IEnumerable<Type> analyzerTypes = assembly.GetExportedTypes().Where(t => typeof(BuildAnalyzer).IsAssignableFrom(t));

foreach (Type analyzerType in analyzerTypes)
{
if (Activator.CreateInstance(analyzerType) is BuildAnalyzer instance)
{
analyzersFactories.Add(() => instance);
}
else
{
throw new InvalidOperationException($"Failed to create an instance of type {analyzerType.FullName} as BuildAnalyzer.");
}
}
}
catch (ReflectionTypeLoadException ex)
{
if (ex.LoaderExceptions.Length != 0)
{
foreach (Exception? loaderException in ex.LoaderExceptions)
{
_loggingService.LogComment(buildEventContext, MessageImportance.Normal, "CustomAnalyzerFailedRuleLoading", loaderException?.Message);
}
}
}
catch (Exception ex)
{
_loggingService.LogComment(buildEventContext, MessageImportance.Normal, "CustomAnalyzerFailedRuleLoading", ex?.Message);
}

return analyzersFactories;
}
}
16 changes: 16 additions & 0 deletions src/Build/BuildCheck/Acquisition/IBuildCheckAcquisitionModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// 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 Microsoft.Build.BuildCheck.Infrastructure;
using Microsoft.Build.Framework;

namespace Microsoft.Build.BuildCheck.Acquisition;

internal interface IBuildCheckAcquisitionModule
{
/// <summary>
/// Creates a list of factory delegates for building analyzer rules instances from a given assembly path.
/// </summary>
List<BuildAnalyzerFactory> CreateBuildAnalyzerFactories(AnalyzerAcquisitionData analyzerAcquisitionData, BuildEventContext buildEventContext);
}
106 changes: 58 additions & 48 deletions src/Build/BuildCheck/Infrastructure/BuildCheckConnectorLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,32 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.BuildCheck.Acquisition;
using Microsoft.Build.BuildCheck.Logging;
using Microsoft.Build.Experimental.BuildCheck;
using Microsoft.Build.Framework;
using static Microsoft.Build.BuildCheck.Infrastructure.BuildCheckManagerProvider;

namespace Microsoft.Build.BuildCheck.Infrastructure;
internal sealed class BuildCheckConnectorLogger(IBuildAnalysisLoggingContextFactory loggingContextFactory, IBuildCheckManager buildCheckManager)
: ILogger

internal sealed class BuildCheckConnectorLogger : ILogger
{
private readonly Dictionary<Type, Action<BuildEventArgs>> _eventHandlers;
private readonly IBuildCheckManager _buildCheckManager;
private readonly IBuildAnalysisLoggingContextFactory _loggingContextFactory;

internal BuildCheckConnectorLogger(
IBuildAnalysisLoggingContextFactory loggingContextFactory,
IBuildCheckManager buildCheckManager)
{
_buildCheckManager = buildCheckManager;
_loggingContextFactory = loggingContextFactory;
_eventHandlers = GetBuildEventHandlers();
}

public LoggerVerbosity Verbosity { get; set; }

public string? Parameters { get; set; }

public void Initialize(IEventSource eventSource)
Expand All @@ -29,70 +42,67 @@ public void Initialize(IEventSource eventSource)
}
}

private void EventSource_AnyEventRaised(object sender, BuildEventArgs e)
public void Shutdown()
{
if (e is ProjectEvaluationFinishedEventArgs projectEvaluationFinishedEventArgs)
{
if (projectEvaluationFinishedEventArgs.ProjectFile?.EndsWith(".metaproj") ?? false)
{
return;
}

buildCheckManager.ProcessEvaluationFinishedEventArgs(
loggingContextFactory.CreateLoggingContext(e.BuildEventContext!),
projectEvaluationFinishedEventArgs);
}

buildCheckManager.EndProjectEvaluation(BuildCheckDataSource.EventArgs, e.BuildEventContext!);
}
else if (e is ProjectEvaluationStartedEventArgs projectEvaluationStartedEventArgs)
{
// Skip autogenerated transient projects (as those are not user projects to be analyzed)
if (projectEvaluationStartedEventArgs.ProjectFile?.EndsWith(".metaproj") ?? false)
{
return;
}

buildCheckManager.StartProjectEvaluation(BuildCheckDataSource.EventArgs, e.BuildEventContext!,
projectEvaluationStartedEventArgs.ProjectFile!);
}
else if (e is ProjectStartedEventArgs projectStartedEvent)
private void HandleProjectEvaluationFinishedEvent(ProjectEvaluationFinishedEventArgs eventArgs)
{
if (!IsMetaProjFile(eventArgs.ProjectFile))
{
buildCheckManager.StartProjectRequest(BuildCheckDataSource.EventArgs, e.BuildEventContext!);
_buildCheckManager.ProcessEvaluationFinishedEventArgs(
_loggingContextFactory.CreateLoggingContext(eventArgs.BuildEventContext!),
eventArgs);

_buildCheckManager.EndProjectEvaluation(BuildCheckDataSource.EventArgs, eventArgs.BuildEventContext!);
}
else if (e is ProjectFinishedEventArgs projectFinishedEventArgs)
}

private void HandleProjectEvaluationStartedEvent(ProjectEvaluationStartedEventArgs eventArgs)
{
if (!IsMetaProjFile(eventArgs.ProjectFile))
{
buildCheckManager.EndProjectRequest(BuildCheckDataSource.EventArgs, e.BuildEventContext!);
_buildCheckManager.StartProjectEvaluation(BuildCheckDataSource.EventArgs, eventArgs.BuildEventContext!, eventArgs.ProjectFile!);
}
else if (e is BuildCheckEventArgs buildCheckBuildEventArgs)
}

private bool IsMetaProjFile(string? projectFile) => !string.IsNullOrEmpty(projectFile) && projectFile!.EndsWith(".metaproj", StringComparison.OrdinalIgnoreCase);

private void EventSource_AnyEventRaised(object sender, BuildEventArgs e)
{
if (_eventHandlers.TryGetValue(e.GetType(), out Action<BuildEventArgs>? handler))
{
if (buildCheckBuildEventArgs is BuildCheckTracingEventArgs tracingEventArgs)
{
_stats.Merge(tracingEventArgs.TracingData, (span1, span2) => span1 + span2);
}
else if (buildCheckBuildEventArgs is BuildCheckAcquisitionEventArgs acquisitionEventArgs)
{
buildCheckManager.ProcessAnalyzerAcquisition(acquisitionEventArgs.ToAnalyzerAcquisitionData());
}
handler(e);
}
}

private readonly Dictionary<string, TimeSpan> _stats = new Dictionary<string, TimeSpan>();

private void EventSource_BuildFinished(object sender, BuildFinishedEventArgs e)
{
_stats.Merge(buildCheckManager.CreateTracingStats(), (span1, span2) => span1 + span2);
_stats.Merge(_buildCheckManager.CreateTracingStats(), (span1, span2) => span1 + span2);
string msg = string.Join(Environment.NewLine, _stats.Select(a => a.Key + ": " + a.Value));

BuildEventContext buildEventContext = e.BuildEventContext ?? new BuildEventContext(
BuildEventContext.InvalidNodeId, BuildEventContext.InvalidTargetId,
BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId);
BuildEventContext buildEventContext = e.BuildEventContext
?? new BuildEventContext(
BuildEventContext.InvalidNodeId,
BuildEventContext.InvalidTargetId,
BuildEventContext.InvalidProjectContextId,
BuildEventContext.InvalidTaskId);

LoggingContext loggingContext = loggingContextFactory.CreateLoggingContext(buildEventContext);
LoggingContext loggingContext = _loggingContextFactory.CreateLoggingContext(buildEventContext);

// Tracing: https://github.com/dotnet/msbuild/issues/9629
loggingContext.LogCommentFromText(MessageImportance.High, msg);
}

public void Shutdown()
{ }
private Dictionary<Type, Action<BuildEventArgs>> GetBuildEventHandlers() => new()
{
{ typeof(ProjectEvaluationFinishedEventArgs), (BuildEventArgs e) => HandleProjectEvaluationFinishedEvent((ProjectEvaluationFinishedEventArgs) e) },
{ typeof(ProjectEvaluationStartedEventArgs), (BuildEventArgs e) => HandleProjectEvaluationStartedEvent((ProjectEvaluationStartedEventArgs) e) },
{ typeof(ProjectStartedEventArgs), (BuildEventArgs e) => _buildCheckManager.StartProjectRequest(BuildCheckDataSource.EventArgs, e.BuildEventContext!) },
{ typeof(ProjectFinishedEventArgs), (BuildEventArgs e) => _buildCheckManager.EndProjectRequest(BuildCheckDataSource.EventArgs, e.BuildEventContext!) },
{ typeof(BuildCheckTracingEventArgs), (BuildEventArgs e) => _stats.Merge(((BuildCheckTracingEventArgs)e).TracingData, (span1, span2) => span1 + span2) },
{ typeof(BuildCheckAcquisitionEventArgs), (BuildEventArgs e) => _buildCheckManager.ProcessAnalyzerAcquisition(((BuildCheckAcquisitionEventArgs)e).ToAnalyzerAcquisitionData(), e.BuildEventContext!) },
};
}
Loading

0 comments on commit bd0b1e4

Please sign in to comment.