File: Analyzers\MembersMustExistAnalyzer.cs
Web Access
Project: src\src\Microsoft.DotNet.CodeAnalysis\Microsoft.DotNet.CodeAnalysis.csproj (Microsoft.DotNet.CodeAnalysis)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
 
namespace Microsoft.DotNet.CodeAnalysis.Analyzers
{
    [DiagnosticAnalyzer(LanguageNames.CSharp)]
    public class MembersMustExistAnalyzer : BaseAnalyzer
    {
        private static string s_title = @"Ensure minimum API surface is respected";
        private static string s_analyzerName = "MembersMustExist";
        private static string s_messageFormat = @"Expected member '{0}' was not found. This member was identified as being a part of a contract between the .NET team and their partners. The '" + s_analyzerName + ".analyzerData' file will contain more information about the team that required this API.";
        private static string s_description = @"Ensures all the APIs in the '" + s_analyzerName + "' file are present.";
        private const string CommentDelimiter = "#";
 
        private HashSet<string> _apisToEnsureExist = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 
        private static readonly DiagnosticDescriptor s_memberMustExistDiagnostic =
            new DiagnosticDescriptor(DiagnosticIds.BCL0001.ToString(), s_title, s_messageFormat, s_analyzerName, DiagnosticSeverity.Error, isEnabledByDefault: true, description: s_description, customTags: WellKnownDiagnosticTags.CompilationEnd);
 
        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(s_memberMustExistDiagnostic); } }
 
        private void OnCompilationEnd(CompilationAnalysisContext context)
        {
            lock (_apisToEnsureExist)
            {
                if (_apisToEnsureExist.Count != 0)
                {
                    // If we have not cleared the list of APIs that must exist then we need to give errors about them
                    foreach (var missingAPI in _apisToEnsureExist)
                    {
                        context.ReportDiagnostic(Diagnostic.Create(s_memberMustExistDiagnostic, Location.None, missingAPI));
                    }
                }
            }
        }
 
        public override void OnCompilationStart(CompilationStartAnalysisContext context)
        {
            // Read the file line-by-line to get the terms.
            var additionalAnalyzerFiles = context.Options.AdditionalFiles.Where(af => af.Path.IndexOf(s_analyzerName, 0, StringComparison.OrdinalIgnoreCase) >= 0);
 
            if (!additionalAnalyzerFiles.Any())
                return;
 
            lock (_apisToEnsureExist)
            {
                foreach (string api in ReadRequiredAPIsFromFiles(additionalAnalyzerFiles))
                {
                    _apisToEnsureExist.Add(api);
                }
            }
 
            context.RegisterCompilationEndAction(OnCompilationEnd);
            context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Method, SymbolKind.Event);
            context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Field, SymbolKind.Event);
        }
 
        private static IEnumerable<string> ReadRequiredAPIsFromFiles(IEnumerable<AdditionalText> additionalAnalyzerFiles)
        {
            // We might have multiple files passed in. We need to make sure we read all of them
            foreach (var additionalFile in additionalAnalyzerFiles)
            {
                SourceText fileContents = additionalFile.GetText();
                foreach (TextLine line in fileContents.Lines)
                {
                    string lineStr = line.ToString();
                    if (!string.IsNullOrWhiteSpace(lineStr) && !lineStr.StartsWith(CommentDelimiter))
                    {
                        yield return lineStr;
                    }
                }
            }
        }
 
        private void AnalyzeSymbol(SymbolAnalysisContext context)
        {
            string apiDef = Helpers.GetMemberName(context.Symbol);
 
            lock (_apisToEnsureExist)
            {
                // The only thing we need to do when we identify a symbol is to remove that signature from the list of APIs we are looking for
                _apisToEnsureExist.Remove(apiDef);
            }
        }
    }
}