File: Workspace\IsolatedAnalyzerFileReference.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#if NET
 
using System;
using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Runtime.Loader;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis;
 
/// <summary>
/// Wrapper around a real <see cref="AnalyzerFileReference"/>.  An "isolated" analyzer reference is an analyzer
/// reference associated with an <see cref="AssemblyLoadContext"/> that is connected to a set of other "isolated"
/// analyzer references.  This allows for loading the analyzers and generators from it in a way that is associated with
/// that load context, keeping them separate from other analyzers and generators loaded in other load contexts, while
/// also allowing all of those instances to be collected when no longer needed.  Being isolated means that if any of the
/// underlying assembly references change, that they can be loaded side by side with the prior references.  This enables
/// functionality like live reloading of analyzers and generators when they change on disk.  Note: this is only
/// supported on .Net Core, and not .Net Framework, as only the former has <see cref="AssemblyLoadContext"/>s.
/// </summary>
/// <remarks>
/// The purpose of this type is to allow passing out a <see cref="AnalyzerReference"/> to the rest of the system that
/// then ensures that as long as it is alive (or any <see cref="DiagnosticAnalyzer"/> or <see cref="ISourceGenerator"/>
/// it passes out is alive), that the <see cref="IsolatedAnalyzerReferenceSet"/> (and its corresponding <see
/// cref="AssemblyLoadContext"/>) is kept alive as well.
/// </remarks>
internal sealed class IsolatedAnalyzerFileReference(
    IsolatedAnalyzerReferenceSet isolatedAnalyzerReferenceSet,
    AnalyzerFileReference underlyingAnalyzerReference)
    : AnalyzerReference
{
    /// <summary>
    /// Conditional weak tables that ensure that as long as a particular <see cref="DiagnosticAnalyzer"/> or <see
    /// cref="ISourceGenerator"/> is alive, that the corresponding <see cref="IsolatedAnalyzerReferenceSet"/> (and its
    /// corresponding <see cref="AssemblyLoadContext"/> is kept alive.
    /// </summary>
    private static readonly ConditionalWeakTable<DiagnosticAnalyzer, IsolatedAnalyzerReferenceSet> s_analyzerToPinnedReferenceSet = [];
 
    /// <inheritdoc cref="s_analyzerToPinnedReferenceSet"/>
    private static readonly ConditionalWeakTable<ISourceGenerator, IsolatedAnalyzerReferenceSet> s_generatorToPinnedReferenceSet = [];
 
    /// <summary>
    /// We keep a strong reference here.  As long as this <see cref="IsolatedAnalyzerFileReference"/> is passed out and
    /// held onto (say by a Project instance), it should keep the IsolatedAssemblyReferenceSet (and its ALC) alive.
    /// </summary>
    private readonly IsolatedAnalyzerReferenceSet _isolatedAnalyzerReferenceSet = isolatedAnalyzerReferenceSet;
 
    /// <summary>
    /// The actual real <see cref="AnalyzerReference"/> we defer our operations to.
    /// </summary>
    public readonly AnalyzerFileReference UnderlyingAnalyzerFileReference = underlyingAnalyzerReference;
 
    public override string Display => UnderlyingAnalyzerFileReference.Display;
    public override string? FullPath => UnderlyingAnalyzerFileReference.FullPath;
    public override object Id => UnderlyingAnalyzerFileReference.Id;
 
    public override ImmutableArray<DiagnosticAnalyzer> GetAnalyzers(string language)
        => PinAnalyzers(static (reference, language) => reference.GetAnalyzers(language), language);
 
    public override ImmutableArray<DiagnosticAnalyzer> GetAnalyzersForAllLanguages()
        => PinAnalyzers(static (reference, _) => reference.GetAnalyzersForAllLanguages(), default(VoidResult));
 
    [Obsolete]
    public override ImmutableArray<ISourceGenerator> GetGenerators()
        => PinGenerators(static (reference, _) => reference.GetGenerators(), default(VoidResult));
 
    public override ImmutableArray<ISourceGenerator> GetGenerators(string language)
        => PinGenerators(static (reference, language) => reference.GetGenerators(language), language);
 
    public override ImmutableArray<ISourceGenerator> GetGeneratorsForAllLanguages()
        => PinGenerators(static (reference, _) => reference.GetGeneratorsForAllLanguages(), default(VoidResult));
 
    private ImmutableArray<DiagnosticAnalyzer> PinAnalyzers<TArg>(Func<AnalyzerReference, TArg, ImmutableArray<DiagnosticAnalyzer>> getItems, TArg arg)
        => PinItems(s_analyzerToPinnedReferenceSet, getItems, arg);
 
    private ImmutableArray<ISourceGenerator> PinGenerators<TArg>(Func<AnalyzerReference, TArg, ImmutableArray<ISourceGenerator>> getItems, TArg arg)
        => PinItems(s_generatorToPinnedReferenceSet, getItems, arg);
 
    private ImmutableArray<TItem> PinItems<TItem, TArg>(
        ConditionalWeakTable<TItem, IsolatedAnalyzerReferenceSet> table,
        Func<AnalyzerReference, TArg, ImmutableArray<TItem>> getItems,
        TArg arg)
        where TItem : class
    {
        // Keep a reference from each generator to the IsolatedAssemblyReferenceSet.  This will ensure it (and the ALC
        // it points at) stays alive as long as the generator instance stays alive.
        var items = getItems(this.UnderlyingAnalyzerFileReference, arg);
 
        foreach (var item in items)
            table.TryAdd(item, _isolatedAnalyzerReferenceSet);
 
        // Note: we want to keep ourselves alive during this call so that neither we nor our reference set get GC'ed
        // while we're computing the items.
        GC.KeepAlive(this);
 
        return items;
    }
 
    public override bool Equals(object? obj)
        => ReferenceEquals(this, obj);
 
    public override int GetHashCode()
        => RuntimeHelpers.GetHashCode(this);
 
    public override string ToString()
        => $"{nameof(IsolatedAnalyzerFileReference)}({UnderlyingAnalyzerFileReference})";
}
 
#endif