File: ApiLifecycle\ApiLifecycleAnalyzer.cs
Web Access
Project: src\src\Analyzers\Microsoft.Analyzers.Local\Microsoft.Analyzers.Local.csproj (Microsoft.Analyzers.Local)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.Extensions.LocalAnalyzers.ApiLifecycle.Model;
using Microsoft.Extensions.LocalAnalyzers.Utilities;
 
namespace Microsoft.Extensions.LocalAnalyzers.ApiLifecycle;
 
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ApiLifecycleAnalyzer : DiagnosticAnalyzer
{
    private const string ExperimentalAttributeFullName = "global::System.Diagnostics.CodeAnalysis.ExperimentalAttribute";
    private const string ObsoleteAttributeFullName = "System.ObsoleteAttribute";
 
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
        => ImmutableArray.Create(
                DiagDescriptors.NewSymbolsMustBeMarkedExperimental,
                DiagDescriptors.ExperimentalSymbolsCantBeMarkedObsolete,
                DiagDescriptors.PublishedSymbolsCantBeMarkedExperimental,
                DiagDescriptors.PublishedSymbolsCantBeDeleted,
                DiagDescriptors.PublishedSymbolsCantChange);
 
    public override void Initialize(AnalysisContext context)
    {
        context.EnableConcurrentExecution();
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 
        context.RegisterCompilationStartAction(start =>
        {
            var compilation = start.Compilation;
 
            if (ModelLoader.TryLoadAssemblyModel(start, out var assemblyModel))
            {
                start.RegisterCompilationEndAction(endContext => ReportDiagnosticForModel(endContext, Analyze(endContext.Compilation, assemblyModel)));
            }
            else if (assemblyModel == null)
            {
                start.RegisterCompilationEndAction(endContext => CheckAllPublicTypesAreExperimentalAndNotObsolete(endContext));
            }
        });
    }
 
    private static AssemblyAnalysis Analyze(Compilation compilation, Assembly? assemblyModel)
    {
        var types = compilation
            .GetSymbolsWithName(_ => true)
            .Where(symbol => symbol.IsExternallyVisible() && symbol.Kind == SymbolKind.NamedType)
            .Cast<INamedTypeSymbol>();
 
        var assemblyAnalysis = new AssemblyAnalysis(assemblyModel ?? Assembly.Empty);
        foreach (var type in types)
        {
            assemblyAnalysis.AnalyzeType(type);
        }
 
        return assemblyAnalysis;
    }
 
    private static void ReportDiagnosticForModel(CompilationAnalysisContext context, AssemblyAnalysis assemblyAnalysis)
    {
        var compilation = context.Compilation;
        var obsoleteAttribute = compilation.GetTypeByMetadataName(ObsoleteAttributeFullName);
 
        // #1. flag symbols found in the code, but not in the model
 
        foreach (var symbol in assemblyAnalysis.NotFoundInBaseline)
        {
            if (!symbol.IsContaminated(ExperimentalAttributeFullName))
            {
                context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.NewSymbolsMustBeMarkedExperimental, symbol.Locations.FirstOrDefault(), symbol));
            }
        }
 
        // #2. flag any stable or deprecated API in the model, but not in the assembly
 
        foreach (var type in assemblyAnalysis.MissingTypes.Where(x => x.Stage != Stage.Experimental))
        {
            context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.PublishedSymbolsCantBeDeleted, null, type.ModifiersAndName));
        }
 
        foreach (var method in assemblyAnalysis.MissingMethods.Where(x => x.Stage != Stage.Experimental))
        {
            context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.PublishedSymbolsCantBeDeleted, null, method.Member));
        }
 
        foreach (var prop in assemblyAnalysis.MissingProperties.Where(x => x.Stage != Stage.Experimental))
        {
            context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.PublishedSymbolsCantBeDeleted, null, prop.Member));
        }
 
        foreach (var field in assemblyAnalysis.MissingFields.Where(x => x.Stage != Stage.Experimental))
        {
            context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.PublishedSymbolsCantBeDeleted, null, field.Member));
        }
 
        // #3. make sure attributes are applied correctly
 
        foreach (var (symbol, stage) in assemblyAnalysis.FoundInBaseline)
        {
            var isMarkedExperimental = symbol.IsContaminated(ExperimentalAttributeFullName);
            var isMarkedObsolete = symbol.IsContaminated(obsoleteAttribute);
 
            if (stage == Stage.Experimental)
            {
                if (isMarkedObsolete)
                {
                    context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.ExperimentalSymbolsCantBeMarkedObsolete, symbol.Locations.FirstOrDefault(), symbol));
                }
            }
            else
            {
                if (isMarkedExperimental)
                {
                    context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.PublishedSymbolsCantBeMarkedExperimental, symbol.Locations.FirstOrDefault(), symbol));
                }
 
                if (assemblyAnalysis.MissingConstraints.TryGetValue(symbol, out var missingContraintsForSymbol))
                {
                    if (missingContraintsForSymbol.Count > 0)
                    {
                        context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.PublishedSymbolsCantChange, symbol.Locations.FirstOrDefault(), symbol));
                    }
                }
 
                if (assemblyAnalysis.MissingBaseTypes.TryGetValue(symbol, out var missingBaseForSymbol))
                {
                    if (missingBaseForSymbol.Count > 0)
                    {
                        context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.PublishedSymbolsCantChange, symbol.Locations.FirstOrDefault(), symbol));
                    }
                }
            }
        }
    }
 
    private static void CheckAllPublicTypesAreExperimentalAndNotObsolete(CompilationAnalysisContext context)
    {
        var types = context
            .Compilation
            .GetSymbolsWithName(_ => true)
            .Where(symbol => symbol.IsExternallyVisible() && symbol.Kind == SymbolKind.NamedType)
            .Cast<INamedTypeSymbol>();
 
        var obsoleteAttribute = context.Compilation.GetTypeByMetadataName(ObsoleteAttributeFullName);
 
        foreach (var type in types)
        {
            if (!type.IsContaminated(ExperimentalAttributeFullName))
            {
                context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.NewSymbolsMustBeMarkedExperimental, type.Locations.FirstOrDefault(), type));
            }
            else if (type.IsContaminated(obsoleteAttribute))
            {
                context.ReportDiagnostic(Diagnostic.Create(DiagDescriptors.ExperimentalSymbolsCantBeMarkedObsolete, type.Locations.FirstOrDefault(), type));
            }
        }
    }
}