Skip to content

Commit

Permalink
Merge pull request #746 from dotnet/custom-load-context
Browse files Browse the repository at this point in the history
Load analyzer assemlbies in their own AssemblyLoadContext
  • Loading branch information
JoeRobich committed Sep 9, 2020
2 parents 965e7e5 + b3b9bf0 commit fd64735
Show file tree
Hide file tree
Showing 34 changed files with 754 additions and 200 deletions.
4 changes: 2 additions & 2 deletions src/Analyzers/AnalyzerFinderHelpers.cs
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);
}
}
}
69 changes: 31 additions & 38 deletions src/Analyzers/AnalyzerFormatter.cs
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 @@ internal class AnalyzerFormatter : ICodeFormatter
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 @@ internal class AnalyzerFormatter : ICodeFormatter
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(solution, 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 @@ -79,7 +79,7 @@ internal class AnalyzerFormatter : ICodeFormatter
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 All @@ -92,7 +92,7 @@ internal class AnalyzerFormatter : ICodeFormatter

private async Task<ImmutableDictionary<ProjectId, ImmutableHashSet<string>>> GetProjectDiagnosticsAsync(
Solution solution,
ImmutableDictionary<Project, ImmutableArray<DiagnosticAnalyzer>> projectAnalyzers,
ImmutableDictionary<ProjectId, ImmutableArray<DiagnosticAnalyzer>> projectAnalyzers,
ImmutableHashSet<string> formattablePaths,
FormatOptions options,
DiagnosticSeverity severity,
Expand All @@ -104,7 +104,7 @@ internal class AnalyzerFormatter : ICodeFormatter
var result = new CodeAnalysisResult();
foreach (var project in solution.Projects)
{
var analyzers = projectAnalyzers[project];
var analyzers = projectAnalyzers[project.Id];
if (analyzers.IsEmpty)
{
continue;
Expand Down Expand Up @@ -153,8 +153,8 @@ static void LogDiagnosticLocations(Solution solution, IEnumerable<Diagnostic> di

private async Task<Solution> FixDiagnosticsAsync(
Solution solution,
ImmutableArray<DiagnosticAnalyzer> allAnalyzers,
ImmutableArray<CodeFixProvider> allCodefixes,
ImmutableDictionary<ProjectId, ImmutableArray<DiagnosticAnalyzer>> projectAnalyzers,
ImmutableArray<CodeFixProvider> allFixers,
ImmutableDictionary<ProjectId, ImmutableHashSet<string>> projectDiagnostics,
ImmutableHashSet<string> formattablePaths,
DiagnosticSeverity severity,
Expand All @@ -169,14 +169,11 @@ static void LogDiagnosticLocations(Solution solution, IEnumerable<Diagnostic> di
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 @@ -190,12 +187,15 @@ static void LogDiagnosticLocations(Solution solution, IEnumerable<Diagnostic> di
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.Id]
.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 @@ -215,20 +215,6 @@ static void LogDiagnosticLocations(Solution solution, IEnumerable<Diagnostic> di

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 @@ -241,22 +227,29 @@ static void LogDiagnosticLocations(Solution solution, IEnumerable<Diagnostic> di
}
}

internal static async Task<ImmutableDictionary<Project, ImmutableArray<DiagnosticAnalyzer>>> FilterBySeverityAsync(
IEnumerable<Project> projects,
ImmutableArray<DiagnosticAnalyzer> allAnalyzers,
internal static async Task<ImmutableDictionary<ProjectId, ImmutableArray<DiagnosticAnalyzer>>> FilterBySeverityAsync(
Solution solution,
ImmutableDictionary<ProjectId, 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)
var projectAnalyzers = ImmutableDictionary.CreateBuilder<ProjectId, ImmutableArray<DiagnosticAnalyzer>>();
foreach (var projectId in projectAnalyzersAndFixers.Keys)
{
var project = solution.GetProject(projectId);
if (project is null)
{
continue;
}

var analyzers = ImmutableArray.CreateBuilder<DiagnosticAnalyzer>();

// Filter analyzers by project's language
var filteredAnalyzer = allAnalyzers.Where(analyzer => DoesAnalyzerSupportLanguage(analyzer, project.Language));
var filteredAnalyzer = projectAnalyzersAndFixers[projectId].Analyzers
.Where(analyzer => DoesAnalyzerSupportLanguage(analyzer, project.Language));
foreach (var analyzer in filteredAnalyzer)
{
// Always run naming style analyzers because we cannot determine potential severity.
Expand All @@ -274,7 +267,7 @@ static void LogDiagnosticLocations(Solution solution, IEnumerable<Diagnostic> di
}
}

projectAnalyzers.Add(project, analyzers.ToImmutableArray());
projectAnalyzers.Add(projectId, analyzers.ToImmutableArray());
}

return projectAnalyzers.ToImmutableDictionary();
Expand Down
24 changes: 12 additions & 12 deletions src/Analyzers/AnalyzerOptionExtensions.cs
Expand Up @@ -36,18 +36,6 @@ internal static string GetCategoryBasedDotnetAnalyzerDiagnosticSeverityKey(strin
DiagnosticDescriptor descriptor,
out ReportDiagnostic severity)
{
// Analyzer bulk configuration does not apply to:
// 1. Disabled by default diagnostics
// 2. Compiler diagnostics
// 3. Non-configurable diagnostics
if (analyzerOptions == null ||
!descriptor.IsEnabledByDefault ||
descriptor.CustomTags.Any(tag => tag == WellKnownDiagnosticTags.Compiler || tag == WellKnownDiagnosticTags.NotConfigurable))
{
severity = default;
return false;
}

// If user has explicitly configured severity for this diagnostic ID, that should be respected.
if (compilation.Options.SpecificDiagnosticOptions.TryGetValue(descriptor.Id, out severity))
{
Expand All @@ -61,6 +49,18 @@ internal static string GetCategoryBasedDotnetAnalyzerDiagnosticSeverityKey(strin
return true;
}

// Analyzer bulk configuration does not apply to:
// 1. Disabled by default diagnostics
// 2. Compiler diagnostics
// 3. Non-configurable diagnostics
if (analyzerOptions == null ||
!descriptor.IsEnabledByDefault ||
descriptor.CustomTags.Any(tag => tag == WellKnownDiagnosticTags.Compiler || tag == WellKnownDiagnosticTags.NotConfigurable))
{
severity = default;
return false;
}

var analyzerConfigOptions = analyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(tree);

// If user has explicitly configured default severity for the diagnostic category, that should be respected.
Expand Down
85 changes: 45 additions & 40 deletions src/Analyzers/AnalyzerReferenceInformationProvider.cs
Expand Up @@ -5,75 +5,53 @@
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Tools.Utilities;
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<ProjectId, AnalyzersAndFixers> GetAnalyzersAndFixers(
Solution solution,
FormatOptions formatOptions,
ILogger logger)
{
if (!formatOptions.FixAnalyzers)
{
return (ImmutableArray<DiagnosticAnalyzer>.Empty, ImmutableArray<CodeFixProvider>.Empty);
return ImmutableDictionary<ProjectId, AnalyzersAndFixers>.Empty;
}

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

private AnalyzersAndFixers GetAnalyzersAndFixers(Project project)
{
var context = new AnalyzerLoadContext();

var analyzerAssemblies = project.AnalyzerReferences
.Select(reference => TryLoadAssemblyFrom(reference.FullPath, context))
.OfType<Assembly>()
.ToImmutableArray();

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

private Assembly? TryLoadAssemblyFrom(string? path)
private Assembly? TryLoadAssemblyFrom(string? path, AnalyzerLoadContext context)
{
// Since we are not deploying these assemblies we need to ensure the files exist.
if (path is null || !File.Exists(path))
{
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 { }
context.AssemblyFolderPath = 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 +60,32 @@ internal class AnalyzerReferenceInformationProvider : IAnalyzerInformationProvid
}

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

internal sealed class AnalyzerLoadContext : AssemblyLoadContext
{
internal string AssemblyFolderPath { get; set; } = string.Empty;

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.

try
{
// Search for assembly based on assembly name and culture within the analyzer folder.
var assembly = AssemblyResolver.TryResolveAssemblyFromPaths(this, assemblyName, AssemblyFolderPath);

if (assembly != null)
{
return assembly;
}
}
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
@@ -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;
}
}
}

0 comments on commit fd64735

Please sign in to comment.