File: Parsing\Parser.cs
Web Access
Project: src\src\Generators\Microsoft.Gen.Logging\Microsoft.Gen.Logging.csproj (Microsoft.Gen.Logging)
// 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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.Gen.Logging.Model;
using Microsoft.Gen.Shared;
 
namespace Microsoft.Gen.Logging.Parsing;
 
internal sealed partial class Parser
{
    private readonly CancellationToken _cancellationToken;
    private readonly Compilation _compilation;
    private readonly Action<Diagnostic> _reportDiagnostic;
    private bool _failedMethod;
 
    public Parser(Compilation compilation, Action<Diagnostic> reportDiagnostic, CancellationToken cancellationToken)
    {
        _compilation = compilation;
        _cancellationToken = cancellationToken;
        _reportDiagnostic = reportDiagnostic;
    }
 
    /// <summary>
    /// Gets the set of logging types containing methods to output.
    /// </summary>
    [SuppressMessage("Maintainability", "CA1505:Avoid unmaintainable code", Justification = "Fix this in a follow-up")]
    public IReadOnlyList<LoggingType> GetLogTypes(IEnumerable<TypeDeclarationSyntax> types)
    {
        Action<DiagnosticDescriptor, Location?, object?[]?> diagReport = Diag; // Keeping one instance of the delegate
        var symbols = SymbolLoader.LoadSymbols(_compilation, diagReport);
        if (symbols is null)
        {
            // nothing to do if required symbols aren't available
            return Array.Empty<LoggingType>();
        }
 
        var ids = new HashSet<int>();
        var eventNames = new HashSet<string>();
        var results = new List<LoggingType>();
        var parameterSymbols = new Dictionary<LoggingMethodParameter, IParameterSymbol>();
 
        // we enumerate by syntax tree, to minimize the need to instantiate semantic models (since they're expensive)
        foreach (var group in types.GroupBy(x => x.SyntaxTree))
        {
            SyntaxTree syntaxTree = group.Key;
            SemanticModel? sm = _compilation.GetSemanticModel(syntaxTree);
 
            foreach (var typeDec in group)
            {
                // stop if we're asked to
                _cancellationToken.ThrowIfCancellationRequested();
 
                LoggingType? lt = null;
                string nspace = string.Empty;
                string? loggerMember = null;
                bool loggerMemberNullable = false;
                ISymbol? secondLoggerMember = null;
 
                ids.Clear();
 
                foreach (MemberDeclarationSyntax member in typeDec.Members)
                {
                    var method = member as MethodDeclarationSyntax;
                    if (method is null)
                    {
                        // we only care about methods
                        continue;
                    }
 
                    var attrLoc = GetLoggerMessageAttribute(method, sm, symbols);
                    if (attrLoc is null)
                    {
                        // doesn't have the magic attribute we like, so ignore
                        continue;
                    }
 
                    var methodSymbol = sm.GetDeclaredSymbol(method, _cancellationToken)!;
 
                    _failedMethod = false;
                    var (lm, keepMethod) = ProcessMethod(method, methodSymbol, attrLoc);
 
                    parameterSymbols.Clear();
                    MethodParsingState parsingState = default;
 
                    foreach (var paramSymbol in methodSymbol.Parameters)
                    {
                        var lp = ProcessParameter(lm, paramSymbol, symbols, ref parsingState);
                        if (lp is null)
                        {
                            keepMethod = false;
                            continue;
                        }
 
                        parameterSymbols[lp] = paramSymbol;
 
                        var foundDataClassificationAttributesInProps = false;
 
                        var logPropertiesAttribute = ParserUtilities.GetSymbolAttributeAnnotationOrDefault(symbols.LogPropertiesAttribute, paramSymbol);
                        if (logPropertiesAttribute is not null)
                        {
                            if (!ProcessLogPropertiesForParameter(
                                logPropertiesAttribute,
                                lm,
                                lp,
                                paramSymbol,
                                symbols,
                                ref foundDataClassificationAttributesInProps))
                            {
                                lp.Properties.Clear();
                            }
                        }
 
                        var tagProviderAttribute = ParserUtilities.GetSymbolAttributeAnnotationOrDefault(symbols.TagProviderAttribute, paramSymbol);
                        if (tagProviderAttribute is not null)
                        {
                            if (!ProcessTagProviderForParameter(
                                tagProviderAttribute,
                                lp,
                                paramSymbol,
                                symbols))
                            {
                                lp.TagProvider = null;
                            }
                        }
 
                        if (lp.HasDataClassification && (lp.HasProperties || lp.HasTagProvider))
                        {
                            Diag(DiagDescriptors.CantUseDataClassificationWithLogPropertiesOrTagProvider, paramSymbol.GetLocation());
                            lp.ClassificationAttributeTypes.Clear();
                        }
 
                        if (lp.HasProperties && lp.HasTagProvider)
                        {
                            Diag(DiagDescriptors.CantMixAttributes, paramSymbol.GetLocation());
                            lp.Properties.Clear();
                            lp.TagProvider = null;
                        }
 
#pragma warning disable S1067 // Expressions should not be too complex
                        if (lp.IsNormalParameter
                            && (logPropertiesAttribute is null)
                            && (tagProviderAttribute is null)
                            && !lp.IsStringifiable
                            && paramSymbol.Type.Kind != SymbolKind.TypeParameter)
                        {
                            Diag(DiagDescriptors.DefaultToString, paramSymbol.GetLocation(), paramSymbol.Type, paramSymbol.Name);
                        }
#pragma warning restore S1067 // Expressions should not be too complex
 
                        bool forceAsTemplateParam = false;
 
                        bool parameterInTemplate = lm.Templates.Contains(lp.TagName, StringComparer.OrdinalIgnoreCase) ||
                            lm.Templates.Contains(lp.ParameterNameWithAtIfNeeded, StringComparer.OrdinalIgnoreCase) ||
                            lm.Templates.Contains($"@{lp.ParameterName}", StringComparer.OrdinalIgnoreCase);
 
                        var loggingProperties = logPropertiesAttribute != null || tagProviderAttribute != null;
                        if (lp.IsLogger && parameterInTemplate)
                        {
                            Diag(DiagDescriptors.ShouldntMentionLoggerInMessage, attrLoc, lp.ParameterName);
                            forceAsTemplateParam = true;
                        }
                        else if (lp.IsException && parameterInTemplate)
                        {
                            Diag(DiagDescriptors.ShouldntMentionExceptionInMessage, attrLoc, lp.ParameterName);
                            forceAsTemplateParam = true;
                        }
                        else if (lp.IsLogLevel && parameterInTemplate)
                        {
                            Diag(DiagDescriptors.ShouldntMentionLogLevelInMessage, attrLoc, lp.ParameterName);
                            forceAsTemplateParam = true;
                        }
                        else if (lp.IsNormalParameter && !parameterInTemplate && !loggingProperties && !string.IsNullOrEmpty(lm.Message))
                        {
                            Diag(DiagDescriptors.ParameterHasNoCorrespondingTemplate, paramSymbol.GetLocation(), lp.ParameterName);
                        }
 
                        var purelyStructuredLoggingParameter = loggingProperties && !parameterInTemplate;
                        if (lp.IsNormalParameter &&
                            !lp.HasDataClassification &&
                            !purelyStructuredLoggingParameter &&
                            paramSymbol.Type.IsRecord)
                        {
                            if (foundDataClassificationAttributesInProps ||
                                RecordHasSensitivePublicMembers(paramSymbol.Type, symbols))
                            {
                                Diag(DiagDescriptors.RecordTypeSensitiveArgumentIsInTemplate, paramSymbol.GetLocation(), lp.ParameterName, lm.Name);
                                keepMethod = false;
                            }
                        }
 
                        lm.Parameters.Add(lp);
                        if (lp.IsNormalParameter || forceAsTemplateParam)
                        {
                            if (parameterInTemplate)
                            {
                                lp.UsedAsTemplate = true;
                            }
                        }
                    }
 
                    if (keepMethod)
                    {
                        if (lm.IsStatic && !parsingState.FoundLogger)
                        {
                            Diag(DiagDescriptors.MissingLoggerParameter, method.ParameterList.GetLocation(), lm.Name);
                            keepMethod = false;
                        }
                        else if (!lm.IsStatic && parsingState.FoundLogger)
                        {
                            Diag(DiagDescriptors.LoggingMethodShouldBeStatic, method.Identifier.GetLocation());
                        }
                        else if (!lm.IsStatic && !parsingState.FoundLogger)
                        {
                            if (loggerMember is null)
                            {
                                (loggerMember, secondLoggerMember, loggerMemberNullable) = FindLoggerMember(sm, typeDec, symbols.ILoggerSymbol);
                            }
 
                            if (secondLoggerMember != null)
                            {
                                Diag(DiagDescriptors.MultipleLoggerMembers, secondLoggerMember.GetLocation(), typeDec.Identifier.Text);
                                keepMethod = false;
                            }
                            else if (loggerMember is null)
                            {
                                Diag(DiagDescriptors.MissingLoggerMember, method.Identifier.GetLocation(), typeDec.Identifier.Text);
                                keepMethod = false;
                            }
                            else
                            {
                                lm.LoggerMember = loggerMember;
                                lm.LoggerMemberNullable = loggerMemberNullable;
                            }
                        }
 
                        if (lm.Level is null && !parsingState.FoundLogLevel)
                        {
                            Diag(DiagDescriptors.MissingLogLevel, method.GetLocation());
 
                            lm.Level = 1;
                        }
 
                        if (keepMethod &&
                            string.IsNullOrWhiteSpace(lm.Message) &&
                            !lm.EventId.HasValue &&
                            lm.Parameters.All(x => x.IsLogger || x.IsLogLevel))
                        {
                            if (!_failedMethod)
                            {
                                Diag(DiagDescriptors.EmptyLoggingMethod, method.Identifier.GetLocation(), methodSymbol.Name);
                            }
                        }
 
                        foreach (var t in lm.Templates)
                        {
                            bool found = false;
                            foreach (LoggingMethodParameter p in lm.Parameters)
                            {
                                if (t.Equals(p.TagName, StringComparison.OrdinalIgnoreCase) ||
                                    t.Equals(p.ParameterNameWithAtIfNeeded, StringComparison.OrdinalIgnoreCase) ||
                                    (t[0] == '@' && t.Substring(1).Equals(p.ParameterNameWithAtIfNeeded, StringComparison.OrdinalIgnoreCase)))
                                {
                                    found = true;
                                    p.TagName = t;
                                    break;
                                }
                            }
 
                            if (!found)
                            {
                                Diag(DiagDescriptors.TemplateHasNoCorrespondingParameter, attrLoc, t);
                            }
                        }
 
                        CheckTagNamesAreUnique(lm, parameterSymbols);
                    }
 
                    if (lt is null)
                    {
                        // determine the namespace the class is declared in, if any
                        SyntaxNode? potentialNamespaceParent = typeDec.Parent;
                        while (potentialNamespaceParent != null &&
                            potentialNamespaceParent is not NamespaceDeclarationSyntax &&
                            potentialNamespaceParent is not FileScopedNamespaceDeclarationSyntax)
                        {
                            potentialNamespaceParent = potentialNamespaceParent.Parent;
                        }
 
                        BaseNamespaceDeclarationSyntax? namespaceParent = potentialNamespaceParent as BaseNamespaceDeclarationSyntax;
                        if (namespaceParent != null)
                        {
                            nspace = namespaceParent.Name.ToString();
                            while (true)
                            {
                                namespaceParent = namespaceParent.Parent as NamespaceDeclarationSyntax;
                                if (namespaceParent is null)
                                {
                                    break;
                                }
 
                                nspace = $"{namespaceParent.Name}.{nspace}";
                            }
                        }
                    }
 
                    if (keepMethod)
                    {
                        if (lt is null)
                        {
                            lt = new LoggingType
                            {
                                Keyword = typeDec.Keyword.ValueText,
                                Namespace = nspace,
                                Name = typeDec.Identifier.ToString() + typeDec.TypeParameterList,
                                Parent = null,
                            };
 
                            lt.AllMembers.AddRange(methodSymbol.ContainingType.GetMembers().Select(x => x.Name));
 
                            LoggingType currentLoggerClass = lt;
                            var parentLoggerClass = typeDec.Parent as TypeDeclarationSyntax;
                            var parentType = methodSymbol.ContainingType.ContainingType;
 
                            static bool IsAllowedKind(SyntaxKind kind) =>
                                kind == SyntaxKind.ClassDeclaration ||
                                kind == SyntaxKind.StructDeclaration ||
                                kind == SyntaxKind.RecordDeclaration;
 
                            while (parentLoggerClass != null && IsAllowedKind(parentLoggerClass.Kind()))
                            {
                                currentLoggerClass.Parent = new LoggingType
                                {
                                    Keyword = parentLoggerClass.Keyword.ValueText,
                                    Namespace = nspace,
                                    Name = parentLoggerClass.Identifier.ToString() + parentLoggerClass.TypeParameterList,
                                    Parent = null,
                                };
 
                                currentLoggerClass = currentLoggerClass.Parent;
                                parentLoggerClass = parentLoggerClass.Parent as TypeDeclarationSyntax;
                                parentType = parentType.ContainingType;
                            }
                        }
 
                        lt.Methods.Add(lm);
                    }
                }
 
                if (lt != null)
                {
                    results.Add(lt);
                }
            }
        }
 
        return results;
 
        (LoggingMethod lm, bool keepMethod) ProcessMethod(MethodDeclarationSyntax method, IMethodSymbol methodSymbol, Location attrLoc)
        {
            var attr = ParserUtilities.GetSymbolAttributeAnnotationOrDefault(symbols.LoggerMessageAttribute, methodSymbol)!;
 
            var (eventId, level, message, eventName, skipEnabledCheck) = AttributeProcessors.ExtractLoggerMessageAttributeValues(attr, symbols);
 
            var lm = new LoggingMethod
            {
                Name = methodSymbol.Name,
                Level = level,
                Message = message,
                EventId = eventId,
                EventName = eventName,
                SkipEnabledCheck = skipEnabledCheck,
                IsExtensionMethod = methodSymbol.IsExtensionMethod,
                IsStatic = methodSymbol.IsStatic,
                Modifiers = method.Modifiers.ToString(),
                HasXmlDocumentation = HasXmlDocumentation(method),
            };
 
            var keepMethod = true;
 
            if (!TemplateProcessor.ExtractTemplates(message, lm.Templates))
            {
                Diag(DiagDescriptors.MalformedFormatStrings, method.Identifier.GetLocation(), method.Identifier.ToString());
                keepMethod = false;
            }
 
            if (!methodSymbol.ReturnsVoid)
            {
                // logging methods must return void
                Diag(DiagDescriptors.LoggingMethodMustReturnVoid, method.ReturnType.GetLocation());
                keepMethod = false;
            }
 
            if (method.Arity > 0)
            {
                // we don't currently support generic methods
                Diag(DiagDescriptors.LoggingMethodIsGeneric, method.TypeParameterList!.GetLocation());
                keepMethod = false;
            }
 
            bool isPartial = methodSymbol.IsPartialDefinition;
            if (method.Body != null)
            {
                Diag(DiagDescriptors.LoggingMethodHasBody, method.Body.GetLocation());
                keepMethod = false;
            }
            else if (!isPartial)
            {
                Diag(DiagDescriptors.LoggingMethodMustBePartial, method.Identifier.GetLocation());
                keepMethod = false;
            }
 
            // ensure there are no duplicate ids.
            if (lm.EventId != null)
            {
                if (!ids.Add(lm.EventId.Value))
                {
                    Diag(DiagDescriptors.ShouldntReuseEventIds, attrLoc, lm.EventId.Value, methodSymbol.ContainingType.Name);
                }
            }
 
            // ensure there are no duplicate event names.
            if (lm.EventName != null)
            {
                if (!eventNames.Add(lm.EventName))
                {
                    Diag(DiagDescriptors.ShouldntReuseEventNames, attrLoc, lm.EventName, methodSymbol.ContainingType.Name);
                }
            }
 
            var msg = lm.Message;
#pragma warning disable S1067 // Expressions should not be too complex
            if (msg.StartsWith("INFORMATION:", StringComparison.OrdinalIgnoreCase)
                || msg.StartsWith("INFO:", StringComparison.OrdinalIgnoreCase)
                || msg.StartsWith("WARNING:", StringComparison.OrdinalIgnoreCase)
                || msg.StartsWith("WARN:", StringComparison.OrdinalIgnoreCase)
                || msg.StartsWith("ERROR:", StringComparison.OrdinalIgnoreCase)
                || msg.StartsWith("ERR:", StringComparison.OrdinalIgnoreCase))
#pragma warning restore S1067 // Expressions should not be too complex
            {
                Diag(DiagDescriptors.RedundantQualifierInMessage, attrLoc, methodSymbol.Name);
            }
 
            return (lm, keepMethod);
        }
    }
 
    private static bool HasXmlDocumentation(MethodDeclarationSyntax method)
    {
        var triviaList = method.GetLeadingTrivia();
        foreach (var trivia in triviaList)
        {
            if (trivia.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia))
            {
                return true;
            }
        }
 
        return false;
    }
 
    // Returns all the classification attributes attached to a symbol.
    private static List<INamedTypeSymbol> GetDataClassificationAttributes(ISymbol symbol, SymbolHolder symbols)
        => symbol
            .GetAttributes()
            .Select(static attribute => attribute.AttributeClass)
            .Where(x => x is not null && symbols.DataClassificationAttribute is not null && ParserUtilities.IsBaseOrIdentity(x, symbols.DataClassificationAttribute, symbols.Compilation))
            .Where(x => !SymbolEqualityComparer.Default.Equals(x, symbols.NoDataClassificationAttribute))
            .Select(static x => x!)
            .ToList();
 
    private void CheckTagNamesAreUnique(LoggingMethod lm, Dictionary<LoggingMethodParameter, IParameterSymbol> parameterSymbols)
    {
        var names = new HashSet<string>(StringComparer.Ordinal);
        foreach (var parameter in lm.Parameters)
        {
            if (!parameter.IsNormalParameter)
            {
                continue;
            }
 
            if (parameter.HasProperties)
            {
                parameter.TraverseParameterPropertiesTransitively((chain, leaf) =>
                {
                    if (parameter.OmitReferenceName)
                    {
                        chain = chain.Skip(1);
                    }
 
                    var fullName = string.Join("_", chain.Concat(new[] { leaf }).Select(static x => x.TagName));
                    if (!names.Add(fullName))
                    {
                        Diag(DiagDescriptors.TagNameCollision, parameterSymbols[parameter].GetLocation(), parameter.ParameterName, fullName, lm.Name);
                    }
                });
            }
            else if (!names.Add(parameter.TagName))
            {
                Diag(DiagDescriptors.TagNameCollision, parameterSymbols[parameter].GetLocation(), parameter.ParameterName, parameter.TagName, lm.Name);
            }
        }
    }
 
    private LoggingMethodParameter? ProcessParameter(
        LoggingMethod lm,
        IParameterSymbol paramSymbol,
        SymbolHolder symbols,
        ref MethodParsingState parsingState)
    {
        var paramName = paramSymbol.Name;
 
        var needsAtSign = false;
        if (!paramSymbol.DeclaringSyntaxReferences.IsDefaultOrEmpty)
        {
            var paramSyntax = paramSymbol.DeclaringSyntaxReferences[0].GetSyntax(_cancellationToken) as ParameterSyntax;
            if (!string.IsNullOrEmpty(paramSyntax!.Identifier.Text))
            {
                needsAtSign = paramSyntax.Identifier.Text[0] == '@';
            }
        }
 
        if (string.IsNullOrWhiteSpace(paramName))
        {
            // semantic problem, just bail quietly
            return null;
        }
 
        var paramTypeSymbol = paramSymbol.Type;
        if (paramTypeSymbol is IErrorTypeSymbol)
        {
            // semantic problem, just bail quietly
            return null;
        }
 
        string? qualifier = null;
        if (paramSymbol.RefKind == RefKind.In)
        {
            qualifier = "in";
        }
        else if (paramSymbol.RefKind != RefKind.None)
        {
            // Parameter has "ref", "out" modifier, no can do
            Diag(DiagDescriptors.LoggingMethodParameterRefKind, paramSymbol.GetLocation(), paramSymbol.ContainingSymbol.Name, paramName);
            return null;
        }
 
        string typeName = paramTypeSymbol.ToDisplayString(
            SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions(
                SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier | SymbolDisplayMiscellaneousOptions.UseSpecialTypes));
 
        var paramDataClassAttributes = new HashSet<string>(
            GetDataClassificationAttributes(paramSymbol, symbols)
            .Distinct(SymbolEqualityComparer.Default)
            .Select(x => x!.ToDisplayString()));
 
        var extractedType = paramTypeSymbol;
        if (paramTypeSymbol.IsNullableOfT())
        {
            // extract the T from a Nullable<T>
            extractedType = ((INamedTypeSymbol)paramTypeSymbol).TypeArguments[0];
        }
 
        var tagNameAttribute = ParserUtilities.GetSymbolAttributeAnnotationOrDefault(symbols.TagNameAttribute, paramSymbol);
        var tagName = tagNameAttribute != null
            ? AttributeProcessors.ExtractTagNameAttributeValues(tagNameAttribute)
            : lm.GetTemplatesForParameter(paramName).FirstOrDefault() ?? paramName;
 
        var lp = new LoggingMethodParameter
        {
            ParameterName = paramName,
            TagName = tagName,
            Type = typeName,
            Qualifier = qualifier,
            NeedsAtSign = needsAtSign,
            ClassificationAttributeTypes = paramDataClassAttributes,
            IsNullable = paramTypeSymbol.NullableAnnotation == NullableAnnotation.Annotated,
            IsReference = paramTypeSymbol.IsReferenceType,
            IsLogger = !parsingState.FoundLogger && ParserUtilities.IsBaseOrIdentity(paramTypeSymbol, symbols.ILoggerSymbol, _compilation),
            IsException = !parsingState.FoundException && ParserUtilities.IsBaseOrIdentity(paramTypeSymbol, symbols.ExceptionSymbol, _compilation),
            IsLogLevel = !parsingState.FoundLogLevel && SymbolEqualityComparer.Default.Equals(paramTypeSymbol, symbols.LogLevelSymbol),
            IsEnumerable = extractedType.IsEnumerable(symbols),
            ImplementsIConvertible = paramTypeSymbol.ImplementsIConvertible(symbols),
            ImplementsIFormattable = paramTypeSymbol.ImplementsIFormattable(symbols),
            ImplementsISpanFormattable = paramTypeSymbol.ImplementsISpanFormattable(symbols),
            HasCustomToString = paramTypeSymbol.HasCustomToString(),
        };
 
        parsingState.FoundLogger |= lp.IsLogger;
        parsingState.FoundException |= lp.IsException;
        parsingState.FoundLogLevel |= lp.IsLogLevel;
 
        return lp;
    }
 
    private Location? GetLoggerMessageAttribute(MethodDeclarationSyntax methodSyntax, SemanticModel sm, SymbolHolder symbols)
    {
        foreach (var mal in methodSyntax.AttributeLists)
        {
            foreach (var methodAttr in mal.Attributes)
            {
                var attrCtor = sm.GetSymbolInfo(methodAttr, _cancellationToken).Symbol;
                if (attrCtor != null && SymbolEqualityComparer.Default.Equals(attrCtor.ContainingType, symbols.LoggerMessageAttribute))
                {
                    return methodAttr.GetLocation();
                }
            }
        }
 
        return null;
    }
 
    private (string? member, ISymbol? secondMember, bool isNullable) FindLoggerMember(SemanticModel sm, TypeDeclarationSyntax classDec, ITypeSymbol loggerSymbol)
    {
        string? loggerMember = null;
        bool isNullable = false;
 
        INamedTypeSymbol? classType = sm.GetDeclaredSymbol(classDec, _cancellationToken);
        INamedTypeSymbol? currentClassType = classType;
        bool onMostDerivedType = true;
 
#pragma warning disable S125 // Sections of code should not be commented out
        /*
         We keep track of the names of all non-logger fields, since they prevent referring to logger
         primary constructor parameters with the same name. Example:
         partial class C(ILogger logger)
         {
             private readonly object logger = logger;
        
             [LoggerMessage(EventId = 0, Level = LogLevel.Debug, Message = ""M1"")]
             public partial void M1(); // The ILogger primary constructor parameter cannot be used here.
         }
        */
#pragma warning restore S125 // Sections of code should not be commented out
 
        HashSet<string> shadowedNames = new(StringComparer.Ordinal);
 
        while (currentClassType is { SpecialType: not SpecialType.System_Object })
        {
            foreach (ISymbol ms in currentClassType.GetMembers())
            {
                // we support both fields and properties
                ITypeSymbol? typeSymbol = ms switch
                {
                    IFieldSymbol fs => fs.Type,
                    IPropertySymbol ps => ps.Type,
                    _ => default,
                };
 
                if (typeSymbol is null)
                {
                    continue;
                }
 
                if (!onMostDerivedType && ms.DeclaredAccessibility == Accessibility.Private)
                {
                    continue;
                }
 
                if (!ms.CanBeReferencedByName)
                {
                    continue;
                }
 
                if (ParserUtilities.IsBaseOrIdentity(typeSymbol, loggerSymbol, _compilation))
                {
                    if (loggerMember is null)
                    {
                        loggerMember = ms.Name;
                        isNullable = typeSymbol.NullableAnnotation == NullableAnnotation.Annotated;
                    }
                    else if (!onMostDerivedType)
                    {
                        // We found a public logger member on a base class when a logger member was already found in the derived class.
                        // This prefers the existing one because it's more likely to be the one the user intended.
                        // The .NET runtime logging generator doesn't support ILogger properties, so this matches that behavior.
                        continue;
                    }
                    else
                    {
                        // This catches the case where there are multiple logger members on the same class.
                        return (member: null, secondMember: ms, isNullable);
                    }
                }
                else
                {
                    _ = shadowedNames.Add(ms.Name);
                }
            }
 
            onMostDerivedType = false;
            currentClassType = currentClassType.BaseType;
        }
 
        // We prioritize fields over primary constructor parameters and avoid warnings if both exist.
        if (loggerMember is not null)
        {
            return (member: loggerMember, secondMember: null, isNullable);
        }
 
        IEnumerable<IMethodSymbol> primaryConstructors = classType!.InstanceConstructors
            .Where(ic => ic.DeclaringSyntaxReferences
                .Any(ds => ds.GetSyntax() is ClassDeclarationSyntax));
 
        foreach (IMethodSymbol primaryConstructor in primaryConstructors)
        {
            foreach (IParameterSymbol parameter in primaryConstructor.Parameters)
            {
                if (ParserUtilities.IsBaseOrIdentity(parameter.Type, loggerSymbol, _compilation))
                {
                    if (shadowedNames.Contains(parameter.Name))
                    {
                        // Accessible fields always shadow primary constructor parameters,
                        // so we can't use the primary constructor parameter,
                        // even if the field is not a valid logger.
                        Diag(DiagDescriptors.PrimaryConstructorParameterLoggerHidden, parameter.GetLocation(), classDec.Identifier.Text);
 
                        continue;
                    }
 
                    if (loggerMember is null)
                    {
                        loggerMember = parameter.Name;
                        isNullable = parameter.Type.NullableAnnotation == NullableAnnotation.Annotated;
                    }
                    else
                    {
                        return (member: null, secondMember: parameter, isNullable);
                    }
                }
            }
        }
 
        return (member: loggerMember, secondMember: null, isNullable);
    }
 
    private void Diag(DiagnosticDescriptor desc, Location? location, params object?[]? messageArgs)
    {
        var d = Diagnostic.Create(desc, location, messageArgs);
        _reportDiagnostic(d);
 
        if (d.Severity == DiagnosticSeverity.Error)
        {
            _failedMethod = true;
        }
    }
 
    private record struct MethodParsingState(
        bool FoundLogger,
        bool FoundException,
        bool FoundLogLevel);
}