private void GenStruct(LoggerMethod lm, string nestedIndentation)
            {
                _builder.AppendLine($@"
        {nestedIndentation}/// {s_generatedTypeSummary}
        {nestedIndentation}[{s_generatedCodeAttribute}]
        {nestedIndentation}[{s_editorBrowsableAttribute}]
        {nestedIndentation}private readonly struct __{lm.UniqueName}Struct : global::System.Collections.Generic.IReadOnlyList<global::System.Collections.Generic.KeyValuePair<string, object?>>
        {nestedIndentation}{{");
                GenFields(lm, nestedIndentation);

                if (lm.TemplateParameters.Count > 0)
                {
                    _builder.Append($@"
            {nestedIndentation}public __{lm.UniqueName}Struct(");
                    GenArguments(lm);
                    _builder.Append($@")
            {nestedIndentation}{{");
                    _builder.AppendLine();
                    GenFieldAssignments(lm, nestedIndentation);
                    _builder.Append($@"
            {nestedIndentation}}}
");
                }

                _builder.Append($@"
            {nestedIndentation}public override string ToString()
            {nestedIndentation}{{
");
                GenVariableAssignments(lm, nestedIndentation);
                _builder.Append($@"
                {nestedIndentation}return $""{ConvertEndOfLineAndQuotationCharactersToEscapeForm(lm.Message)}"";
            {nestedIndentation}}}
");
                _builder.Append($@"
            {nestedIndentation}public static readonly global::System.Func<__{lm.UniqueName}Struct, global::System.Exception?, string> Format = (state, ex) => state.ToString();

            {nestedIndentation}public int Count => {lm.TemplateParameters.Count + 1};

            {nestedIndentation}public global::System.Collections.Generic.KeyValuePair<string, object?> this[int index]
            {nestedIndentation}{{
                {nestedIndentation}get => index switch
                {nestedIndentation}{{
");
                GenCases(lm, nestedIndentation);
                _builder.Append($@"
                    {nestedIndentation}_ => throw new global::System.IndexOutOfRangeException(nameof(index)),  // return the same exception LoggerMessage.Define returns in this case
                {nestedIndentation}}};
            }}

            {nestedIndentation}public global::System.Collections.Generic.IEnumerator<global::System.Collections.Generic.KeyValuePair<string, object?>> GetEnumerator()
            {nestedIndentation}{{
                {nestedIndentation}for (int i = 0; i < {lm.TemplateParameters.Count + 1}; i++)
                {nestedIndentation}{{
                    {nestedIndentation}yield return this[i];
                {nestedIndentation}}}
            {nestedIndentation}}}

            {nestedIndentation}global::System.Collections.IEnumerator global::System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
        {nestedIndentation}}}
");
            }
            private void GenLogMethod(LoggerMethod lm, string nestedIndentation)
            {
                string level        = GetLogLevel(lm);
                string extension    = lm.IsExtensionMethod ? "this " : string.Empty;
                string eventName    = string.IsNullOrWhiteSpace(lm.EventName) ? $"nameof({lm.Name})" : $"\"{lm.EventName}\"";
                string exceptionArg = GetException(lm);
                string logger       = GetLogger(lm);

                if (UseLoggerMessageDefine(lm))
                {
                    _builder.Append($@"
        {nestedIndentation}[{s_generatedCodeAttribute}]
        {nestedIndentation}private static readonly global::System.Action<global::Microsoft.Extensions.Logging.ILogger, ");

                    GenDefineTypes(lm, brackets: false);

                    _builder.Append($@"global::System.Exception?> __{lm.UniqueName}Callback =
            {nestedIndentation}global::Microsoft.Extensions.Logging.LoggerMessage.Define");

                    GenDefineTypes(lm, brackets: true);

                    _builder.Append(@$ "({level}, new global::Microsoft.Extensions.Logging.EventId({lm.EventId}, {eventName}), " "{ConvertEndOfLineAndQuotationCharactersToEscapeForm(lm.Message)}" ", new global::Microsoft.Extensions.Logging.LogDefineOptions() {{ SkipEnabledCheck = true }}); 
");
                }

                _builder.Append($@"
        {nestedIndentation}[{s_generatedCodeAttribute}]
        {nestedIndentation}{lm.Modifiers} void {lm.Name}({extension}");

                GenParameters(lm);

                _builder.Append($@")
        {nestedIndentation}{{");

                string enabledCheckIndentation = lm.SkipEnabledCheck ? "" : "    ";

                if (!lm.SkipEnabledCheck)
                {
                    _builder.Append($@"
            {nestedIndentation}if ({logger}.IsEnabled({level}))
            {nestedIndentation}{{");
                }

                if (UseLoggerMessageDefine(lm))
                {
                    _builder.Append($@"
            {nestedIndentation}{enabledCheckIndentation}__{lm.UniqueName}Callback({logger}, ");

                    GenCallbackArguments(lm);

                    _builder.Append($"{exceptionArg});");
                }
                else
                {
                    _builder.Append($@"
            {nestedIndentation}{enabledCheckIndentation}{logger}.Log(
                {nestedIndentation}{enabledCheckIndentation}{level},
                {nestedIndentation}{enabledCheckIndentation}new global::Microsoft.Extensions.Logging.EventId({lm.EventId}, {eventName}),
                {nestedIndentation}{enabledCheckIndentation}");
                    GenHolder(lm);
                    _builder.Append($@",
                {nestedIndentation}{enabledCheckIndentation}{exceptionArg},
                {nestedIndentation}{enabledCheckIndentation}__{lm.UniqueName}Struct.Format);");
                }

                if (!lm.SkipEnabledCheck)
                {
                    _builder.Append($@"
            {nestedIndentation}}}");
                }

                _builder.Append($@"
        {nestedIndentation}}}");
Пример #3
0
            /// <summary>
            /// Gets the set of logging classes containing methods to output.
            /// </summary>
            public IReadOnlyList <LoggerClass> GetLogClasses(IEnumerable <ClassDeclarationSyntax> classes)
            {
                INamedTypeSymbol loggerMessageAttribute = _compilation.GetBestTypeByMetadataName(LoggerMessageAttribute);

                if (loggerMessageAttribute == null)
                {
                    // nothing to do if this type isn't available
                    return(Array.Empty <LoggerClass>());
                }

                INamedTypeSymbol loggerSymbol = _compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.ILogger");

                if (loggerSymbol == null)
                {
                    // nothing to do if this type isn't available
                    return(Array.Empty <LoggerClass>());
                }

                INamedTypeSymbol logLevelSymbol = _compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.LogLevel");

                if (logLevelSymbol == null)
                {
                    // nothing to do if this type isn't available
                    return(Array.Empty <LoggerClass>());
                }

                INamedTypeSymbol exceptionSymbol = _compilation.GetBestTypeByMetadataName("System.Exception");

                if (exceptionSymbol == null)
                {
                    Diag(DiagnosticDescriptors.MissingRequiredType, null, "System.Exception");
                    return(Array.Empty <LoggerClass>());
                }

                INamedTypeSymbol enumerableSymbol = _compilation.GetSpecialType(SpecialType.System_Collections_IEnumerable);
                INamedTypeSymbol stringSymbol     = _compilation.GetSpecialType(SpecialType.System_String);

                var results = new List <LoggerClass>();
                var ids     = new HashSet <int>();

                // we enumerate by syntax tree, to minimize the need to instantiate semantic models (since they're expensive)
                foreach (var group in classes.GroupBy(x => x.SyntaxTree))
                {
                    SemanticModel?sm = null;
                    foreach (ClassDeclarationSyntax classDec in group)
                    {
                        // stop if we're asked to
                        _cancellationToken.ThrowIfCancellationRequested();

                        LoggerClass?lc                   = null;
                        string      nspace               = string.Empty;
                        string?     loggerField          = null;
                        bool        multipleLoggerFields = false;

                        ids.Clear();
                        foreach (MemberDeclarationSyntax member in classDec.Members)
                        {
                            var method = member as MethodDeclarationSyntax;
                            if (method == null)
                            {
                                // we only care about methods
                                continue;
                            }

                            sm ??= _compilation.GetSemanticModel(classDec.SyntaxTree);
                            IMethodSymbol logMethodSymbol = sm.GetDeclaredSymbol(method, _cancellationToken) as IMethodSymbol;
                            Debug.Assert(logMethodSymbol != null, "log method is present.");
                            (int eventId, int?level, string message, string?eventName, bool skipEnabledCheck) = (-1, null, string.Empty, null, false);

                            foreach (AttributeListSyntax mal in method.AttributeLists)
                            {
                                foreach (AttributeSyntax ma in mal.Attributes)
                                {
                                    IMethodSymbol attrCtorSymbol = sm.GetSymbolInfo(ma, _cancellationToken).Symbol as IMethodSymbol;
                                    if (attrCtorSymbol == null || !loggerMessageAttribute.Equals(attrCtorSymbol.ContainingType, SymbolEqualityComparer.Default))
                                    {
                                        // badly formed attribute definition, or not the right attribute
                                        continue;
                                    }

                                    bool hasMisconfiguredInput = false;
                                    ImmutableArray <AttributeData>?boundAttributes = logMethodSymbol?.GetAttributes();

                                    if (boundAttributes == null || boundAttributes !.Value.Length == 0)
                                    {
                                        continue;
                                    }

                                    foreach (AttributeData attributeData in boundAttributes)
                                    {
                                        if (!SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, loggerMessageAttribute))
                                        {
                                            continue;
                                        }

                                        // supports: [LoggerMessage(0, LogLevel.Warning, "custom message")]
                                        // supports: [LoggerMessage(eventId: 0, level: LogLevel.Warning, message: "custom message")]
                                        if (attributeData.ConstructorArguments.Any())
                                        {
                                            foreach (TypedConstant typedConstant in attributeData.ConstructorArguments)
                                            {
                                                if (typedConstant.Kind == TypedConstantKind.Error)
                                                {
                                                    hasMisconfiguredInput = true;
                                                }
                                            }

                                            ImmutableArray <TypedConstant> items = attributeData.ConstructorArguments;
                                            Debug.Assert(items.Length == 3);

                                            eventId = items[0].IsNull ? -1 : (int)GetItem(items[0]);
                                            level   = items[1].IsNull ? null : (int?)GetItem(items[1]);
                                            message = items[2].IsNull ? "" : (string)GetItem(items[2]);
                                        }

                                        // argument syntax takes parameters. e.g. EventId = 0
                                        // supports: e.g. [LoggerMessage(EventId = 0, Level = LogLevel.Warning, Message = "custom message")]
                                        if (attributeData.NamedArguments.Any())
                                        {
                                            foreach (KeyValuePair <string, TypedConstant> namedArgument in attributeData.NamedArguments)
                                            {
                                                TypedConstant typedConstant = namedArgument.Value;
                                                if (typedConstant.Kind == TypedConstantKind.Error)
                                                {
                                                    hasMisconfiguredInput = true;
                                                }
                                                else
                                                {
                                                    TypedConstant value = namedArgument.Value;
                                                    switch (namedArgument.Key)
                                                    {
                                                    case "EventId":
                                                        eventId = (int)GetItem(value);
                                                        break;

                                                    case "Level":
                                                        level = value.IsNull ? null : (int?)GetItem(value);
                                                        break;

                                                    case "SkipEnabledCheck":
                                                        skipEnabledCheck = (bool)GetItem(value);
                                                        break;

                                                    case "EventName":
                                                        eventName = (string?)GetItem(value);
                                                        break;

                                                    case "Message":
                                                        message = value.IsNull ? "" : (string)GetItem(value);
                                                        break;
                                                    }
                                                }
                                            }
                                        }
                                    }

                                    if (hasMisconfiguredInput)
                                    {
                                        // skip further generator execution and let compiler generate the errors
                                        break;
                                    }

                                    IMethodSymbol?methodSymbol = sm.GetDeclaredSymbol(method, _cancellationToken);
                                    if (methodSymbol != null)
                                    {
                                        var lm = new LoggerMethod
                                        {
                                            Name              = methodSymbol.Name,
                                            Level             = level,
                                            Message           = message,
                                            EventId           = eventId,
                                            EventName         = eventName,
                                            IsExtensionMethod = methodSymbol.IsExtensionMethod,
                                            Modifiers         = method.Modifiers.ToString(),
                                            SkipEnabledCheck  = skipEnabledCheck
                                        };

                                        ExtractTemplates(message, lm.TemplateMap, lm.TemplateList);

                                        bool keepMethod = true;   // whether or not we want to keep the method definition or if it's got errors making it so we should discard it instead
                                        if (lm.Name[0] == '_')
                                        {
                                            // can't have logging method names that start with _ since that can lead to conflicting symbol names
                                            // because the generated symbols start with _
                                            Diag(DiagnosticDescriptors.InvalidLoggingMethodName, method.Identifier.GetLocation());
                                            keepMethod = false;
                                        }

                                        if (!methodSymbol.ReturnsVoid)
                                        {
                                            // logging methods must return void
                                            Diag(DiagnosticDescriptors.LoggingMethodMustReturnVoid, method.ReturnType.GetLocation());
                                            keepMethod = false;
                                        }

                                        if (method.Arity > 0)
                                        {
                                            // we don't currently support generic methods
                                            Diag(DiagnosticDescriptors.LoggingMethodIsGeneric, method.Identifier.GetLocation());
                                            keepMethod = false;
                                        }

                                        bool isStatic  = false;
                                        bool isPartial = false;
                                        foreach (SyntaxToken mod in method.Modifiers)
                                        {
                                            if (mod.IsKind(SyntaxKind.PartialKeyword))
                                            {
                                                isPartial = true;
                                            }
                                            else if (mod.IsKind(SyntaxKind.StaticKeyword))
                                            {
                                                isStatic = true;
                                            }
                                        }

                                        if (!isPartial)
                                        {
                                            Diag(DiagnosticDescriptors.LoggingMethodMustBePartial, method.GetLocation());
                                            keepMethod = false;
                                        }

                                        if (method.Body != null)
                                        {
                                            Diag(DiagnosticDescriptors.LoggingMethodHasBody, method.Body.GetLocation());
                                            keepMethod = false;
                                        }

                                        // ensure there are no duplicate ids.
                                        if (ids.Contains(lm.EventId))
                                        {
                                            Diag(DiagnosticDescriptors.ShouldntReuseEventIds, ma.GetLocation(), lm.EventId, classDec.Identifier.Text);
                                        }
                                        else
                                        {
                                            _ = ids.Add(lm.EventId);
                                        }

                                        string msg = lm.Message;
                                        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))
                                        {
                                            Diag(DiagnosticDescriptors.RedundantQualifierInMessage, ma.GetLocation(), method.Identifier.ToString());
                                        }

                                        bool foundLogger    = false;
                                        bool foundException = false;
                                        bool foundLogLevel  = level != null;
                                        foreach (IParameterSymbol paramSymbol in methodSymbol.Parameters)
                                        {
                                            string paramName   = paramSymbol.Name;
                                            bool   needsAtSign = false;
                                            if (paramSymbol.DeclaringSyntaxReferences.Length > 0)
                                            {
                                                ParameterSyntax paramSyntax = paramSymbol.DeclaringSyntaxReferences[0].GetSyntax(_cancellationToken) as ParameterSyntax;
                                                if (paramSyntax != null && !string.IsNullOrEmpty(paramSyntax.Identifier.Text))
                                                {
                                                    needsAtSign = paramSyntax.Identifier.Text[0] == '@';
                                                }
                                            }
                                            if (string.IsNullOrWhiteSpace(paramName))
                                            {
                                                // semantic problem, just bail quietly
                                                keepMethod = false;
                                                break;
                                            }

                                            ITypeSymbol paramTypeSymbol = paramSymbol !.Type;
                                            if (paramTypeSymbol is IErrorTypeSymbol)
                                            {
                                                // semantic problem, just bail quietly
                                                keepMethod = false;
                                                break;
                                            }

                                            string?qualifier = null;
                                            if (paramSymbol.RefKind == RefKind.In)
                                            {
                                                qualifier = "in";
                                            }
                                            else if (paramSymbol.RefKind == RefKind.Ref)
                                            {
                                                qualifier = "ref";
                                            }
                                            string typeName = paramTypeSymbol.ToDisplayString(
                                                SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions(
                                                    SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier));

                                            var lp = new LoggerParameter
                                            {
                                                Name         = paramName,
                                                Type         = typeName,
                                                Qualifier    = qualifier,
                                                CodeName     = needsAtSign ? "@" + paramName : paramName,
                                                IsLogger     = !foundLogger && IsBaseOrIdentity(paramTypeSymbol !, loggerSymbol),
                                                IsException  = !foundException && IsBaseOrIdentity(paramTypeSymbol !, exceptionSymbol),
                                                IsLogLevel   = !foundLogLevel && IsBaseOrIdentity(paramTypeSymbol !, logLevelSymbol),
                                                IsEnumerable = IsBaseOrIdentity(paramTypeSymbol !, enumerableSymbol) && !IsBaseOrIdentity(paramTypeSymbol !, stringSymbol),
                                            };

                                            foundLogger    |= lp.IsLogger;
                                            foundException |= lp.IsException;
                                            foundLogLevel  |= lp.IsLogLevel;

                                            bool forceAsTemplateParams = false;
                                            if (lp.IsLogger && lm.TemplateMap.ContainsKey(paramName))
                                            {
                                                Diag(DiagnosticDescriptors.ShouldntMentionLoggerInMessage, paramSymbol.Locations[0], paramName);
                                                forceAsTemplateParams = true;
                                            }
                                            else if (lp.IsException && lm.TemplateMap.ContainsKey(paramName))
                                            {
                                                Diag(DiagnosticDescriptors.ShouldntMentionExceptionInMessage, paramSymbol.Locations[0], paramName);
                                                forceAsTemplateParams = true;
                                            }
                                            else if (lp.IsLogLevel && lm.TemplateMap.ContainsKey(paramName))
                                            {
                                                Diag(DiagnosticDescriptors.ShouldntMentionLogLevelInMessage, paramSymbol.Locations[0], paramName);
                                                forceAsTemplateParams = true;
                                            }
                                            else if (lp.IsLogLevel && level != null && !lm.TemplateMap.ContainsKey(paramName))
                                            {
                                                Diag(DiagnosticDescriptors.ArgumentHasNoCorrespondingTemplate, paramSymbol.Locations[0], paramName);
                                            }
                                            else if (lp.IsTemplateParameter && !lm.TemplateMap.ContainsKey(paramName))
                                            {
                                                Diag(DiagnosticDescriptors.ArgumentHasNoCorrespondingTemplate, paramSymbol.Locations[0], paramName);
                                            }

                                            if (paramName[0] == '_')
                                            {
                                                // can't have logging method parameter names that start with _ since that can lead to conflicting symbol names
                                                // because all generated symbols start with _
                                                Diag(DiagnosticDescriptors.InvalidLoggingMethodParameterName, paramSymbol.Locations[0]);
                                            }

                                            lm.AllParameters.Add(lp);
                                            if (lp.IsTemplateParameter || forceAsTemplateParams)
                                            {
                                                lm.TemplateParameters.Add(lp);
                                            }
                                        }

                                        if (keepMethod)
                                        {
                                            if (isStatic && !foundLogger)
                                            {
                                                Diag(DiagnosticDescriptors.MissingLoggerArgument, method.GetLocation(), lm.Name);
                                                keepMethod = false;
                                            }
                                            else if (!isStatic && foundLogger)
                                            {
                                                Diag(DiagnosticDescriptors.LoggingMethodShouldBeStatic, method.GetLocation());
                                            }
                                            else if (!isStatic && !foundLogger)
                                            {
                                                if (loggerField == null)
                                                {
                                                    (loggerField, multipleLoggerFields) = FindLoggerField(sm, classDec, loggerSymbol);
                                                }

                                                if (multipleLoggerFields)
                                                {
                                                    Diag(DiagnosticDescriptors.MultipleLoggerFields, method.GetLocation(), classDec.Identifier.Text);
                                                    keepMethod = false;
                                                }
                                                else if (loggerField == null)
                                                {
                                                    Diag(DiagnosticDescriptors.MissingLoggerField, method.GetLocation(), classDec.Identifier.Text);
                                                    keepMethod = false;
                                                }
                                                else
                                                {
                                                    lm.LoggerField = loggerField;
                                                }
                                            }

                                            if (level == null && !foundLogLevel)
                                            {
                                                Diag(DiagnosticDescriptors.MissingLogLevel, method.GetLocation());
                                                keepMethod = false;
                                            }

                                            foreach (KeyValuePair <string, string> t in lm.TemplateMap)
                                            {
                                                bool found = false;
                                                foreach (LoggerParameter p in lm.AllParameters)
                                                {
                                                    if (t.Key.Equals(p.Name, StringComparison.OrdinalIgnoreCase))
                                                    {
                                                        found = true;
                                                        break;
                                                    }
                                                }

                                                if (!found)
                                                {
                                                    Diag(DiagnosticDescriptors.TemplateHasNoCorrespondingArgument, ma.GetLocation(), t.Key);
                                                }
                                            }
                                        }

                                        if (lc == null)
                                        {
                                            // determine the namespace the class is declared in, if any
                                            SyntaxNode?potentialNamespaceParent = classDec.Parent;
                                            while (potentialNamespaceParent != null &&
                                                   potentialNamespaceParent is not NamespaceDeclarationSyntax
#if ROSLYN4_0_OR_GREATER
                                                   && potentialNamespaceParent is not FileScopedNamespaceDeclarationSyntax
#endif
                                                   )
                                            {
                                                potentialNamespaceParent = potentialNamespaceParent.Parent;
                                            }

#if ROSLYN4_0_OR_GREATER
                                            if (potentialNamespaceParent is BaseNamespaceDeclarationSyntax namespaceParent)
#else
                                            if (potentialNamespaceParent is NamespaceDeclarationSyntax namespaceParent)
#endif
                                            {
                                                nspace = namespaceParent.Name.ToString();
                                                while (true)
                                                {
                                                    namespaceParent = namespaceParent.Parent as NamespaceDeclarationSyntax;
                                                    if (namespaceParent == null)
                                                    {
                                                        break;
                                                    }

                                                    nspace = $"{namespaceParent.Name}.{nspace}";
                                                }
                                            }
                                        }

                                        if (keepMethod)
                                        {
                                            lc ??= new LoggerClass
                                            {
                                                Keyword     = classDec.Keyword.ValueText,
                                                Namespace   = nspace,
                                                Name        = classDec.Identifier.ToString() + classDec.TypeParameterList,
                                                ParentClass = null,
                                            };

                                            LoggerClass currentLoggerClass = lc;
                                            var         parentLoggerClass  = (classDec.Parent as TypeDeclarationSyntax);