Skip to content

Commit

Permalink
Load analyzer assemlbies in their own AssemblyLoadContext
Browse files Browse the repository at this point in the history
  • Loading branch information
JoeRobich committed Aug 26, 2020
1 parent 971dcca commit 9fcf025
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 97 deletions.
4 changes: 2 additions & 2 deletions src/Analyzers/AnalyzerFinderHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Microsoft.CodeAnalysis.Tools.Analyzers
{
internal static class AnalyzerFinderHelpers
{
public static (ImmutableArray<DiagnosticAnalyzer> Analyzers, ImmutableArray<CodeFixProvider> Fixers) LoadAnalyzersAndFixers(IEnumerable<Assembly> assemblies)
public static AnalyzersAndFixers LoadAnalyzersAndFixers(IEnumerable<Assembly> assemblies)
{
var types = assemblies
.SelectMany(assembly => assembly.GetTypes()
Expand All @@ -32,7 +32,7 @@ public static (ImmutableArray<DiagnosticAnalyzer> Analyzers, ImmutableArray<Code
.OfType<DiagnosticAnalyzer>()
.ToImmutableArray();

return (diagnosticAnalyzers, codeFixProviders);
return new AnalyzersAndFixers(diagnosticAnalyzers, codeFixProviders);
}
}
}
52 changes: 19 additions & 33 deletions src/Analyzers/AnalyzerFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ namespace Microsoft.CodeAnalysis.Tools.Analyzers
{
internal class AnalyzerFormatter : ICodeFormatter
{
private static readonly ImmutableArray<string> s_supportedLanguages = ImmutableArray.Create(LanguageNames.CSharp, LanguageNames.VisualBasic);

private readonly string _name;
private readonly IAnalyzerInformationProvider _informationProvider;
private readonly IAnalyzerRunner _runner;
Expand All @@ -43,14 +41,16 @@ public async Task<Solution> FormatAsync(
List<FormattedFile> formattedFiles,
CancellationToken cancellationToken)
{
var (analyzers, fixers) = _informationProvider.GetAnalyzersAndFixers(solution, formatOptions, logger);
if (analyzers.IsEmpty && fixers.IsEmpty)
var projectAnalyzersAndFixers = _informationProvider.GetAnalyzersAndFixers(solution, formatOptions, logger);
if (projectAnalyzersAndFixers.IsEmpty)
{
return solution;
}

var allFixers = projectAnalyzersAndFixers.Values.SelectMany(analyzersAndFixers => analyzersAndFixers.Fixers).ToImmutableArray();

// Only include compiler diagnostics if we have a fixer that can fix them.
var includeCompilerDiagnostics = fixers.Any(
var includeCompilerDiagnostics = allFixers.Any(
codefix => codefix.FixableDiagnosticIds.Any(
id => id.StartsWith("CS") || id.StartsWith("BC")));

Expand All @@ -65,7 +65,7 @@ public async Task<Solution> FormatAsync(
var severity = _informationProvider.GetSeverity(formatOptions);

// Filter to analyzers that report diagnostics with equal or greater severity.
var projectAnalyzers = await FilterBySeverityAsync(solution.Projects, analyzers, formattablePaths, severity, cancellationToken).ConfigureAwait(false);
var projectAnalyzers = await FilterBySeverityAsync(projectAnalyzersAndFixers, formattablePaths, severity, cancellationToken).ConfigureAwait(false);

// Determine which diagnostics are being reported for each project.
var projectDiagnostics = await GetProjectDiagnosticsAsync(solution, projectAnalyzers, formattablePaths, formatOptions, severity, includeCompilerDiagnostics, logger, formattedFiles, cancellationToken).ConfigureAwait(false);
Expand All @@ -76,7 +76,7 @@ public async Task<Solution> FormatAsync(
logger.LogTrace(Resources.Fixing_diagnostics);

// Run each analyzer individually and apply fixes if possible.
solution = await FixDiagnosticsAsync(solution, analyzers, fixers, projectDiagnostics, formattablePaths, severity, includeCompilerDiagnostics, logger, cancellationToken).ConfigureAwait(false);
solution = await FixDiagnosticsAsync(solution, projectAnalyzers, allFixers, projectDiagnostics, formattablePaths, severity, includeCompilerDiagnostics, logger, cancellationToken).ConfigureAwait(false);

var fixDiagnosticsMS = analysisStopwatch.ElapsedMilliseconds - projectDiagnosticsMS;
logger.LogTrace(Resources.Complete_in_0_ms, fixDiagnosticsMS);
Expand Down Expand Up @@ -149,8 +149,8 @@ static void LogDiagnosticLocations(Solution solution, IEnumerable<Diagnostic> di

private async Task<Solution> FixDiagnosticsAsync(
Solution solution,
ImmutableArray<DiagnosticAnalyzer> allAnalyzers,
ImmutableArray<CodeFixProvider> allCodefixes,
ImmutableDictionary<Project, ImmutableArray<DiagnosticAnalyzer>> projectAnalyzers,
ImmutableArray<CodeFixProvider> allFixers,
ImmutableDictionary<ProjectId, ImmutableHashSet<string>> projectDiagnostics,
ImmutableHashSet<string> formattablePaths,
DiagnosticSeverity severity,
Expand All @@ -165,14 +165,11 @@ private async Task<Solution> FixDiagnosticsAsync(
return solution;
}

// Build maps between diagnostic id and the associated analyzers and codefixes
var analyzersByIdAndLanguage = CreateAnalyzerMap(reportedDiagnostics, allAnalyzers);
var fixersById = CreateFixerMap(reportedDiagnostics, allCodefixes);
var fixersById = CreateFixerMap(reportedDiagnostics, allFixers);

// We need to run each codefix iteratively so ensure that all diagnostics are found and fixed.
foreach (var diagnosticId in reportedDiagnostics)
{
var analyzersByLanguage = analyzersByIdAndLanguage[diagnosticId];
var codefixes = fixersById[diagnosticId];

// If there is no codefix, there is no reason to run analysis again.
Expand All @@ -186,12 +183,15 @@ private async Task<Solution> FixDiagnosticsAsync(
foreach (var project in solution.Projects)
{
// Only run analysis on projects that had previously reported the diagnostic
if (!projectDiagnostics.TryGetValue(project.Id, out var diagnosticIds))
if (!projectDiagnostics.TryGetValue(project.Id, out var diagnosticIds)
|| !diagnosticIds.Contains(diagnosticId))
{
continue;
}

var analyzers = analyzersByLanguage[project.Language];
var analyzers = projectAnalyzers[project]
.Where(analyzer => analyzer.SupportedDiagnostics.Any(descriptor => descriptor.Id == diagnosticId))
.ToImmutableArray();
await _runner.RunCodeAnalysisAsync(result, analyzers, project, formattablePaths, severity, includeCompilerDiagnostics, logger, cancellationToken).ConfigureAwait(false);
}

Expand All @@ -211,20 +211,6 @@ private async Task<Solution> FixDiagnosticsAsync(

return solution;

static ImmutableDictionary<string, ImmutableDictionary<string, ImmutableArray<DiagnosticAnalyzer>>> CreateAnalyzerMap(
ImmutableArray<string> diagnosticIds,
ImmutableArray<DiagnosticAnalyzer> analyzers)
{
return diagnosticIds.ToImmutableDictionary(
id => id,
id => s_supportedLanguages.ToImmutableDictionary(
language => language,
language => analyzers
.Where(analyzer => DoesAnalyzerSupportLanguage(analyzer, language))
.Where(analyzer => analyzer.SupportedDiagnostics.Any(diagnostic => diagnostic.Id == id))
.ToImmutableArray()));
}

static ImmutableDictionary<string, ImmutableArray<CodeFixProvider>> CreateFixerMap(
ImmutableArray<string> diagnosticIds,
ImmutableArray<CodeFixProvider> fixers)
Expand All @@ -238,21 +224,21 @@ static ImmutableDictionary<string, ImmutableArray<CodeFixProvider>> CreateFixerM
}

internal static async Task<ImmutableDictionary<Project, ImmutableArray<DiagnosticAnalyzer>>> FilterBySeverityAsync(
IEnumerable<Project> projects,
ImmutableArray<DiagnosticAnalyzer> allAnalyzers,
ImmutableDictionary<Project, AnalyzersAndFixers> projectAnalyzersAndFixers,
ImmutableHashSet<string> formattablePaths,
DiagnosticSeverity minimumSeverity,
CancellationToken cancellationToken)
{
// We only want to run analyzers for each project that have the potential for reporting a diagnostic with
// a severity equal to or greater than specified.
var projectAnalyzers = ImmutableDictionary.CreateBuilder<Project, ImmutableArray<DiagnosticAnalyzer>>();
foreach (var project in projects)
foreach (var project in projectAnalyzersAndFixers.Keys)
{
var analyzers = ImmutableArray.CreateBuilder<DiagnosticAnalyzer>();

// Filter analyzers by project's language
var filteredAnalyzer = allAnalyzers.Where(analyzer => DoesAnalyzerSupportLanguage(analyzer, project.Language));
var filteredAnalyzer = projectAnalyzersAndFixers[project].Analyzers
.Where(analyzer => DoesAnalyzerSupportLanguage(analyzer, project.Language));
foreach (var analyzer in filteredAnalyzer)
{
// Always run naming style analyzers because we cannot determine potential severity.
Expand Down
86 changes: 47 additions & 39 deletions src/Analyzers/AnalyzerReferenceInformationProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,34 @@
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.Extensions.Logging;

namespace Microsoft.CodeAnalysis.Tools.Analyzers
{
internal class AnalyzerReferenceInformationProvider : IAnalyzerInformationProvider
{
private static readonly string[] s_roslynCodeStyleAssmeblies = new[]
{
"Microsoft.CodeAnalysis.CodeStyle",
"Microsoft.CodeAnalysis.CodeStyle.Fixes",
"Microsoft.CodeAnalysis.CSharp.CodeStyle",
"Microsoft.CodeAnalysis.CSharp.CodeStyle.Fixes",
"Microsoft.CodeAnalysis.VisualBasic.CodeStyle",
"Microsoft.CodeAnalysis.VisualBasic.CodeStyle.Fixes"
};

public (ImmutableArray<DiagnosticAnalyzer> Analyzers, ImmutableArray<CodeFixProvider> Fixers) GetAnalyzersAndFixers(
public ImmutableDictionary<Project, AnalyzersAndFixers> GetAnalyzersAndFixers(
Solution solution,
FormatOptions formatOptions,
ILogger logger)
{
if (!formatOptions.FixAnalyzers)
{
return (ImmutableArray<DiagnosticAnalyzer>.Empty, ImmutableArray<CodeFixProvider>.Empty);
return ImmutableDictionary<Project, AnalyzersAndFixers>.Empty;
}

var assemblies = solution.Projects
.SelectMany(project => project.AnalyzerReferences.Select(reference => reference.FullPath))
.Distinct()
.Select(TryLoadAssemblyFrom)
return solution.Projects
.ToImmutableDictionary(project => project, GetAnalyzersAndFixers);
}

private AnalyzersAndFixers GetAnalyzersAndFixers(Project project)
{
var analyzerAssemblies = project.AnalyzerReferences
.Select(reference => TryLoadAssemblyFrom(reference.FullPath))
.OfType<Assembly>()
.ToImmutableArray();

return AnalyzerFinderHelpers.LoadAnalyzersAndFixers(assemblies);
return AnalyzerFinderHelpers.LoadAnalyzersAndFixers(analyzerAssemblies);
}

private Assembly? TryLoadAssemblyFrom(string? path)
Expand All @@ -52,28 +43,12 @@ internal class AnalyzerReferenceInformationProvider : IAnalyzerInformationProvid
return null;
}

// Roslyn CodeStyle analysis is handled with the --fix-style option.
var assemblyFileName = Path.GetFileNameWithoutExtension(path);
if (s_roslynCodeStyleAssmeblies.Contains(assemblyFileName))
{
return null;
}

try
{
// First try loading the assembly from disk.
return AssemblyLoadContext.Default.LoadFromAssemblyPath(path);
}
catch { }
var context = new AnalyzerLoadContext(Path.GetDirectoryName(path));

try
{
// Next see if this assembly has already been loaded into our context.
var assemblyName = AssemblyLoadContext.GetAssemblyName(path);
if (assemblyName?.Name != null)
{
return AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(assemblyName.Name));
}
// First try loading the assembly from disk.
return context.LoadFromAssemblyPath(path);
}
catch { }

Expand All @@ -82,5 +57,38 @@ internal class AnalyzerReferenceInformationProvider : IAnalyzerInformationProvid
}

public DiagnosticSeverity GetSeverity(FormatOptions formatOptions) => formatOptions.AnalyzerSeverity;

internal sealed class AnalyzerLoadContext : AssemblyLoadContext
{
private readonly string _assemblyFolderPath;

public AnalyzerLoadContext(string assemblyFolderPath)
{
_assemblyFolderPath = assemblyFolderPath;
}

protected override Assembly Load(AssemblyName assemblyName)
{
// Since we build against .NET Core 2.1 we do not have access to the
// AssemblyDependencyResolver which resolves depenendency assembly paths
// from AssemblyName by using the .deps.json.

// We will instead do the simplest thing by looking for the requested assembly
// by name in the same folder as the assembly being loaded.
var possibleAssemblyFileName = $"{assemblyName.Name}.dll";
var possibleAssemblyPath = Path.Combine(_assemblyFolderPath, possibleAssemblyFileName);
try
{
if (File.Exists(possibleAssemblyPath))
{
return LoadFromAssemblyPath(possibleAssemblyPath);
}
}
catch { }

// Try to load the requested assembly from the default load context.
return AssemblyLoadContext.Default.LoadFromAssemblyName(assemblyName);
}
}
}
}
28 changes: 28 additions & 0 deletions src/Analyzers/AnalyzersAndFixers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System.Collections.Immutable;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Microsoft.CodeAnalysis.Tools.Analyzers
{
internal struct AnalyzersAndFixers
{
public ImmutableArray<DiagnosticAnalyzer> Analyzers { get; }
public ImmutableArray<CodeFixProvider> Fixers { get; }

public AnalyzersAndFixers(ImmutableArray<DiagnosticAnalyzer> analyzers, ImmutableArray<CodeFixProvider> fixers)
{
Analyzers = analyzers;
Fixers = fixers;
}

public void Deconstruct(
out ImmutableArray<DiagnosticAnalyzer> analyzers,
out ImmutableArray<CodeFixProvider> fixers)
{
analyzers = Analyzers;
fixers = Fixers;
}
}
}
10 changes: 5 additions & 5 deletions src/Analyzers/CodeStyleInformationProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.Extensions.Logging;

namespace Microsoft.CodeAnalysis.Tools.Analyzers
Expand All @@ -18,14 +16,14 @@ internal class CodeStyleInformationProvider : IAnalyzerInformationProvider
private readonly string _featuresCSharpPath = Path.Combine(s_executingPath, "Microsoft.CodeAnalysis.CSharp.Features.dll");
private readonly string _featuresVisualBasicPath = Path.Combine(s_executingPath, "Microsoft.CodeAnalysis.VisualBasic.Features.dll");

public (ImmutableArray<DiagnosticAnalyzer> Analyzers, ImmutableArray<CodeFixProvider> Fixers) GetAnalyzersAndFixers(
public ImmutableDictionary<Project, AnalyzersAndFixers> GetAnalyzersAndFixers(
Solution solution,
FormatOptions options,
ILogger logger)
{
if (!options.FixCodeStyle)
{
return (ImmutableArray<DiagnosticAnalyzer>.Empty, ImmutableArray<CodeFixProvider>.Empty);
return ImmutableDictionary<Project, AnalyzersAndFixers>.Empty;
}

var assemblies = new[]
Expand All @@ -35,7 +33,9 @@ internal class CodeStyleInformationProvider : IAnalyzerInformationProvider
_featuresVisualBasicPath
}.Select(path => Assembly.LoadFrom(path));

return AnalyzerFinderHelpers.LoadAnalyzersAndFixers(assemblies);
var analyzersAndFixers = AnalyzerFinderHelpers.LoadAnalyzersAndFixers(assemblies);
return solution.Projects
.ToImmutableDictionary(project => project, project => analyzersAndFixers);
}

public DiagnosticSeverity GetSeverity(FormatOptions formatOptions) => formatOptions.CodeStyleSeverity;
Expand Down
4 changes: 1 addition & 3 deletions src/Analyzers/Interfaces/IAnalyzerInformationProvider.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System.Collections.Immutable;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.Extensions.Logging;

namespace Microsoft.CodeAnalysis.Tools.Analyzers
Expand All @@ -11,7 +9,7 @@ internal interface IAnalyzerInformationProvider
{
DiagnosticSeverity GetSeverity(FormatOptions formatOptions);

(ImmutableArray<DiagnosticAnalyzer> Analyzers, ImmutableArray<CodeFixProvider> Fixers) GetAnalyzersAndFixers(
ImmutableDictionary<Project, AnalyzersAndFixers> GetAnalyzersAndFixers(
Solution solution,
FormatOptions formatOptions,
ILogger logger);
Expand Down
Loading

0 comments on commit 9fcf025

Please sign in to comment.