| 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.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)); } } } }