Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:

steps:
- uses: actions/checkout@v2
- uses: actions/setup-dotnet@v5
- name: Build BulkAnalysisRunner
run: dotnet build src --configuration ${{ matrix.configuration }}
- name: Test BulkAnalysisRunner
Expand Down
3 changes: 2 additions & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<WarningLevel>4</WarningLevel>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
<LangVersion>11</LangVersion>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>

<Version>4.0.0.0</Version>
Expand All @@ -19,6 +19,7 @@
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)\build\CodeAnalysis.ruleset</CodeAnalysisRuleSet>
<WTGAnalyzersWarnAll>true</WTGAnalyzersWarnAll>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<EnableNETAnalyzers>true</EnableNETAnalyzers>

<OutputPath>$(MSBuildThisFileDirectory)bin</OutputPath>
</PropertyGroup>
Expand Down
6 changes: 6 additions & 0 deletions global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"sdk": {
"version": "10.0.300",
"rollForward": "latestPatch"
}
}
230 changes: 143 additions & 87 deletions src/WTG.BulkAnalysis.Core/Internal/AnalyzerCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,153 +13,198 @@ namespace WTG.BulkAnalysis.Core
{
abstract class AnalyzerCache
{
public static AnalyzerCache Create(ImmutableHashSet<string> diagnosticIds, string loadDir, ImmutableArray<string> loadList)
protected AnalyzerCache(ImmutableHashSet<string> diagnosticIds, ILog log)
{
this.diagnosticIds = diagnosticIds;
this.log = log;
analyzerFilter = a => a.SupportedDiagnostics.Any(x => diagnosticIds.Contains(x.Id));
providerFilter = p => p.FixableDiagnosticIds.Any(diagnosticIds.Contains);
providerLookup = new ConcurrentDictionary<string, ImmutableArray<CodeFixProvider>>();
referenceCache = new ConcurrentDictionary<string, AnalyzerFileReference>(StringComparer.OrdinalIgnoreCase);
subscribed = new HashSet<AnalyzerFileReference>();
}

readonly ImmutableHashSet<string> diagnosticIds;
readonly ILog log;
readonly Predicate<DiagnosticAnalyzer> analyzerFilter;
readonly Predicate<CodeFixProvider> providerFilter;
readonly ConcurrentDictionary<string, ImmutableArray<CodeFixProvider>> providerLookup;
readonly ConcurrentDictionary<string, AnalyzerFileReference> referenceCache;
readonly HashSet<AnalyzerFileReference> subscribed;

public static AnalyzerCache Create(ImmutableHashSet<string> diagnosticIds, string loadDir, ImmutableArray<string> loadList, ILog log)
{
if (loadList.Length > 0)
{
return new Explicit(diagnosticIds, loadDir, loadList);
return new Explicit(diagnosticIds, loadDir, loadList, log);
}
else
{
return new Implicit(diagnosticIds, loadDir);
return new Implicit(diagnosticIds, loadDir, log);
}
}

public abstract ImmutableArray<DiagnosticAnalyzer> GetAnalyzers(Project project);
public abstract ImmutableDictionary<string, ImmutableList<CodeFixProvider>> GetAllCodeFixProviders(Project project);

static IEnumerable<T> Get<T>(string assemblyPath, Predicate<T> filter)
protected ImmutableArray<DiagnosticAnalyzer> CollectAnalyzers(IEnumerable<AnalyzerFileReference> references, string? language)
{
var assembly = Assembly.LoadFile(assemblyPath);
var builder = ImmutableArray.CreateBuilder<DiagnosticAnalyzer>();

foreach (var type in assembly.GetTypes())
foreach (var reference in references)
{
if (!type.IsAbstract && type.IsSubclassOf(typeof(T)))
EnsureSubscribed(reference);

var analyzers = language is not null
? reference.GetAnalyzers(language)
: reference.GetAnalyzersForAllLanguages();

builder.AddRange(analyzers.Where(a => analyzerFilter(a)));
}

return builder.ToImmutable();
}

protected ImmutableDictionary<string, ImmutableList<CodeFixProvider>> CollectCodeFixProviders(IEnumerable<AnalyzerFileReference> references)
{
return ImmutableDictionary.ToImmutableDictionary(
from reference in references
from provider in GetCodeFixProviders(reference)
from id in provider.FixableDiagnosticIds
where diagnosticIds.Contains(id)
group provider by id into g
select g,
x => x.Key,
x => x.ToImmutableList());
}

protected AnalyzerFileReference GetOrCreateReference(string path, IAnalyzerAssemblyLoader loader)
=> referenceCache.GetOrAdd(path, p => new AnalyzerFileReference(p, loader));

ImmutableArray<CodeFixProvider> GetCodeFixProviders(AnalyzerFileReference reference)
=> providerLookup.GetOrAdd(reference.FullPath, key => LoadCodeFixProviders(reference));

ImmutableArray<CodeFixProvider> LoadCodeFixProviders(AnalyzerFileReference reference)
{
var builder = ImmutableArray.CreateBuilder<CodeFixProvider>();

foreach (var type in GetLoadableTypes(reference))
{
if (!type.IsAbstract && type.IsSubclassOf(typeof(CodeFixProvider)))
{
var instance = (T)Activator.CreateInstance(type);
var provider = (CodeFixProvider)Activator.CreateInstance(type);

if (filter(instance))
if (providerFilter(provider))
Comment thread
yaakov-h marked this conversation as resolved.
{
yield return instance;
builder.Add(provider);
}
}
}

return builder.ToImmutable();
}

sealed class Implicit : AnalyzerCache
IEnumerable<Type> GetLoadableTypes(AnalyzerFileReference reference)
{
public Implicit(ImmutableHashSet<string> diagnosticIds, string loadDir)
Assembly assembly;

try
{
this.loadDir = loadDir;
analyzerFilter = a => a.SupportedDiagnostics.Any(x => diagnosticIds.Contains(x.Id));
providerFilter = p => p.FixableDiagnosticIds.Any(diagnosticIds.Contains);
analyzerLookup = new ConcurrentDictionary<string, ImmutableArray<DiagnosticAnalyzer>>();
providerLookup = new ConcurrentDictionary<string, ImmutableArray<CodeFixProvider>>();
assembly = reference.GetAssembly();
}

public override ImmutableArray<DiagnosticAnalyzer> GetAnalyzers(Project project)
catch (Exception ex)
{
var paths = GetAnalyzerRefs(project);

if (!string.IsNullOrEmpty(loadDir))
{
paths = Remap(paths, loadDir);
}
log.WriteFormatted($" - Unable to load code fixes from '{reference.FullPath}': {ex.Message}", LogLevel.Warning);
return [];
}

return ImmutableArray.CreateRange(
from analyzerRef in paths
from analyzer in GetAnalyzers(analyzerRef)
select analyzer);
try
{
return assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
// A provider whose dependencies can't be resolved appears as a null entry here;
// keep the types that did load. This mirrors how Roslyn's AnalyzerFileReference
// tolerates partial load failures when enumerating analyzers.
return ex.Types.Where(t => t != null).ToArray()!;
}
}

public override ImmutableDictionary<string, ImmutableList<CodeFixProvider>> GetAllCodeFixProviders(Project project)
void EnsureSubscribed(AnalyzerFileReference reference)
{
if (subscribed.Add(reference))
{
var paths = GetAnalyzerRefs(project);
reference.AnalyzerLoadFailed += OnAnalyzerLoadFailed;
}
}

if (!string.IsNullOrEmpty(loadDir))
{
paths = Remap(paths, loadDir);
}
void OnAnalyzerLoadFailed(object? sender, AnalyzerLoadFailureEventArgs e)
{
var path = (sender as AnalyzerFileReference)?.FullPath;
var detail = string.IsNullOrEmpty(e.Message) ? e.Exception?.Message : e.Message;
var suffix = string.IsNullOrEmpty(detail) ? string.Empty : $": {detail}";
log.WriteFormatted($" - Skipping an analyzer in '{path}' ({e.ErrorCode}){suffix}", LogLevel.Warning);
}

return ImmutableDictionary.ToImmutableDictionary(
from analyzerRef in paths
from codeFixProvider in GetCodeFixProviders(analyzerRef)
from diagnosticId in codeFixProvider.FixableDiagnosticIds
group codeFixProvider by diagnosticId into g
select g,
x => x.Key,
x => x.ToImmutableList());
sealed class Implicit : AnalyzerCache
{
public Implicit(ImmutableHashSet<string> diagnosticIds, string loadDir, ILog log)
: base(diagnosticIds, log)
{
this.loadDir = loadDir;
}

ImmutableArray<DiagnosticAnalyzer> GetAnalyzers(string assemblyName) => GetCached(assemblyName, analyzerLookup, analyzerFilter);
ImmutableArray<CodeFixProvider> GetCodeFixProviders(string assemblyName) => GetCached(assemblyName, providerLookup, providerFilter);
public override ImmutableArray<DiagnosticAnalyzer> GetAnalyzers(Project project)
=> CollectAnalyzers(GetReferences(project), project.Language);

public override ImmutableDictionary<string, ImmutableList<CodeFixProvider>> GetAllCodeFixProviders(Project project)
=> CollectCodeFixProviders(GetReferences(project));

static ImmutableArray<T> GetCached<T>(string assemblyName, ConcurrentDictionary<string, ImmutableArray<T>> lookup, Predicate<T> filter)
IEnumerable<AnalyzerFileReference> GetReferences(Project project)
{
if (!lookup.TryGetValue(assemblyName, out var result))
if (string.IsNullOrEmpty(loadDir))
{
result = Get(assemblyName, filter).ToImmutableArray();
result = lookup.GetOrAdd(assemblyName, result);
return project.AnalyzerReferences.OfType<AnalyzerFileReference>();
}

return result;
return Remap(project, loadDir);
}

static IEnumerable<string> GetAnalyzerRefs(Project project)
IEnumerable<AnalyzerFileReference> Remap(Project project, string loadDir)
{
var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

foreach (var reference in project.AnalyzerReferences)
foreach (var reference in project.AnalyzerReferences.OfType<AnalyzerFileReference>())
{
if (!string.IsNullOrEmpty(reference.FullPath))
if (string.IsNullOrEmpty(reference.FullPath))
{
result.Add(reference.FullPath!);
continue;
}
}

return result;
}

static IEnumerable<string> Remap(IEnumerable<string> source, string loadDir)
{
foreach (var item in source)
{
var proposal = Path.Combine(loadDir, Path.GetFileName(item));
var proposal = Path.Combine(loadDir, Path.GetFileName(reference.FullPath));

if (File.Exists(proposal))
{
yield return proposal;
var loader = reference.AssemblyLoader;
loader.AddDependencyLocation(proposal);
yield return GetOrCreateReference(proposal, loader);
}
}
}

readonly string loadDir;
readonly Predicate<DiagnosticAnalyzer> analyzerFilter;
readonly Predicate<CodeFixProvider> providerFilter;
readonly ConcurrentDictionary<string, ImmutableArray<DiagnosticAnalyzer>> analyzerLookup;
readonly ConcurrentDictionary<string, ImmutableArray<CodeFixProvider>> providerLookup;
}

sealed class Explicit : AnalyzerCache
{
public Explicit(ImmutableHashSet<string> diagnosticIds, string loadDir, ImmutableArray<string> loadList)
public Explicit(ImmutableHashSet<string> diagnosticIds, string loadDir, ImmutableArray<string> loadList, ILog log)
: base(diagnosticIds, log)
{
Predicate<DiagnosticAnalyzer> analyzerFilter = a => a.SupportedDiagnostics.Any(x => diagnosticIds.Contains(x.Id));
Predicate<CodeFixProvider> providerFilter = p => p.FixableDiagnosticIds.Any(diagnosticIds.Contains);

var paths = PrefixPaths(loadDir, loadList);

analyzers = paths.SelectMany(x => Get(x, analyzerFilter)).ToImmutableArray();

providers = ImmutableDictionary.ToImmutableDictionary(
from path in paths
from provider in Get(path, providerFilter)
from id in provider.FixableDiagnosticIds
where diagnosticIds.Contains(id)
group provider by id into g
select g,
x => x.Key,
x => x.ToImmutableList());
var references = PrefixPaths(loadDir, loadList)
.Select(path => GetOrCreateReference(path, FallbackAssemblyLoader.Instance))
.ToImmutableArray();

analyzers = CollectAnalyzers(references, language: null);
providers = CollectCodeFixProviders(references);
}

public override ImmutableArray<DiagnosticAnalyzer> GetAnalyzers(Project project) => analyzers;
Expand All @@ -180,5 +225,16 @@ static IEnumerable<string> PrefixPaths(string loadDir, ImmutableArray<string> lo
readonly ImmutableArray<DiagnosticAnalyzer> analyzers;
readonly ImmutableDictionary<string, ImmutableList<CodeFixProvider>> providers;
}

sealed class FallbackAssemblyLoader : IAnalyzerAssemblyLoader
{
public static readonly FallbackAssemblyLoader Instance = new FallbackAssemblyLoader();

public void AddDependencyLocation(string fullPath)
{
}

public Assembly LoadFromPath(string fullPath) => Assembly.LoadFrom(fullPath);
}
}
}
2 changes: 1 addition & 1 deletion src/WTG.BulkAnalysis.Core/Processor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public static async Task ProcessAsync(RunContext context)

var counter = 0;
var numSolutions = context.SolutionPaths.Length;
var cache = AnalyzerCache.Create(context.RuleIds, context.LoadDir, context.LoadList);
var cache = AnalyzerCache.Create(context.RuleIds, context.LoadDir, context.LoadList, context.Log);

foreach (var solutionPath in context.SolutionPaths)
{
Expand Down
14 changes: 7 additions & 7 deletions src/WTG.BulkAnalysis.Core/WTG.BulkAnalysis.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.4.0" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="7.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.4.0" />
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.507">
<InternalsVisibleTo Include="WTG.BulkAnalysis.Test" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis" Version="5.3.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="5.3.0" />
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
Loading
Loading