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}}}");
/// <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);