Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Load analyzer assemlbies in their own AssemblyLoadContext #746

Merged
merged 15 commits into from Sep 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
}
}
}