private static string GenType(LoggerClass lc) { var methods = new StringBuilder(); foreach (var lm in lc.Methods) { if (lm.Parameters.Count > 0) { methods.Append(GenStruct(lm)); } methods.Append(GenEventId(lm)); methods.Append(GenExtensionLogMethod(lm)); } var namespaceStart = string.Empty; var namespaceEnd = string.Empty; if (lc.Namespace != null) { namespaceStart = $"namespace {lc.Namespace}\n{{\n"; namespaceEnd = "}\n"; } return($@" {namespaceStart} partial class {lc.Name} {{ {methods} }} {namespaceEnd} "); }
private void GenType(LoggerClass lc) { if (!string.IsNullOrWhiteSpace(lc.Namespace)) { _builder.Append($@" namespace {lc.Namespace} {{"); } _builder.Append($@" partial class {lc.Name} {lc.Constraints} {{"); foreach (LoggerMethod lm in lc.Methods) { if (!UseLoggerMessageDefine(lm)) { GenStruct(lm); } GenLogMethod(lm); } _builder.Append($@" }}"); if (!string.IsNullOrWhiteSpace(lc.Namespace)) { _builder.Append($@" }}"); } }
private static string GenType(LoggerClass lc) { var methods = new StringBuilder(); foreach (var lm in lc.Methods) { methods.Append(GenStruct(lm)); methods.Append(GenEventId(lm)); methods.Append(GenExtensionLogMethod(lm)); } var namespaceStart = string.Empty; var namespaceEnd = string.Empty; if (lc.Namespace != null) { namespaceStart = $"namespace {lc.Namespace}\n{{\n"; namespaceEnd = "}\n"; } return($@" {namespaceStart} static class {lc.Name} {{ {methods} public static {lc.OriginalInterfaceName} Wrap(this ILogger logger) => new __Wrapper__(logger); {GenWrapper(lc)} }} {namespaceEnd} "); }
private void GenEnumerationHelper(LoggerClass lc) { foreach (LoggerMethod lm in lc.Methods) { if (UseLoggerMessageDefine(lm)) { foreach (LoggerParameter p in lm.TemplateParameters) { if (p.IsEnumerable) { _builder.Append($@" [{_generatedCodeAttribute}] private static string __Enumerate(global::System.Collections.IEnumerable? enumerable) {{ if (enumerable == null) {{ return ""(null)""; }} var sb = new global::System.Text.StringBuilder(); _ = sb.Append('['); bool first = true; foreach (object e in enumerable) {{ if (!first) {{ _ = sb.Append("", ""); }} if (e == null) {{ _ = sb.Append(""(null)""); }} else {{ if (e is global::System.IFormattable fmt) {{ _ = sb.Append(fmt.ToString(null, global::System.Globalization.CultureInfo.InvariantCulture)); }} else {{ _ = sb.Append(e); }} }} first = false; }} _ = sb.Append(']'); return sb.ToString(); }} "); } } } } }
private string GenType(LoggerClass lc) { var sb = GetStringBuilder(); try { foreach (var lm in lc.Methods) { if (!UseLoggerMessageDefine(lm)) { _ = sb.Append(GenStruct(lm)); } _ = sb.Append(GenLogMethod(lm)); } _ = sb.Append(GenEnumerationHelper(lc)); if (string.IsNullOrWhiteSpace(lc.Namespace)) { return($@" partial class {lc.Name} {lc.Constraints} {{ {sb} }} "); } return($@" namespace {lc.Namespace} {{ partial class {lc.Name} {lc.Constraints} {{ {sb} }} }} "); } finally { ReturnStringBuilder(sb); } }
private static string GenWrapper(LoggerClass lc) { var methods = new StringBuilder(); foreach (var lm in lc.Methods) { methods.Append(GenInstanceLogMethod(lm)); } return($@" private sealed class __Wrapper__ : {lc.OriginalInterfaceName} {{ private readonly ILogger __logger; public __Wrapper__(ILogger logger) => __logger = logger; {methods} }} "); }
/// <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>?boundAttrbutes = logMethodSymbol?.GetAttributes(); if (boundAttrbutes == null) { continue; } foreach (AttributeData attributeData in boundAttrbutes) { // 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; 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 typeName = paramTypeSymbol.ToDisplayString( SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions( SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier)); var lp = new LoggerParameter { Name = paramName, Type = typeName, 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, Constraints = classDec.ConstraintClauses.ToString(), ParentClass = null, }; LoggerClass currentLoggerClass = lc; var parentLoggerClass = (classDec.Parent as TypeDeclarationSyntax); bool IsAllowedKind(SyntaxKind kind) => kind == SyntaxKind.ClassDeclaration || kind == SyntaxKind.StructDeclaration || kind == SyntaxKind.RecordDeclaration; while (parentLoggerClass != null && IsAllowedKind(parentLoggerClass.Kind())) { currentLoggerClass.ParentClass = new LoggerClass { Keyword = parentLoggerClass.Keyword.ValueText, Namespace = nspace, Name = parentLoggerClass.Identifier.ToString() + parentLoggerClass.TypeParameterList, Constraints = parentLoggerClass.ConstraintClauses.ToString(), ParentClass = null, }; currentLoggerClass = currentLoggerClass.ParentClass; parentLoggerClass = (parentLoggerClass.Parent as TypeDeclarationSyntax); } lc.Methods.Add(lm); } } } } } if (lc != null) { results.Add(lc); } } } return(results); }
private void GenType(LoggerClass lc) { string nestedIndentation = ""; if (!string.IsNullOrWhiteSpace(lc.Namespace)) { _builder.Append($@" namespace {lc.Namespace} {{"); } LoggerClass parent = lc.ParentClass; var parentClasses = new List <string>(); // loop until you find top level nested class while (parent != null) { parentClasses.Add($"partial {parent.Keyword} {parent.Name} "); parent = parent.ParentClass; } // write down top level nested class first for (int i = parentClasses.Count - 1; i >= 0; i--) { _builder.Append($@" {nestedIndentation}{parentClasses[i]} {nestedIndentation}{{"); nestedIndentation += " "; } _builder.Append($@" {nestedIndentation}partial {lc.Keyword} {lc.Name} {nestedIndentation}{{"); foreach (LoggerMethod lm in lc.Methods) { if (!UseLoggerMessageDefine(lm)) { GenStruct(lm, nestedIndentation); } GenLogMethod(lm, nestedIndentation); } _builder.Append($@" {nestedIndentation}}}"); parent = lc.ParentClass; while (parent != null) { nestedIndentation = new String(' ', nestedIndentation.Length - 4); _builder.Append($@" {nestedIndentation}}}"); parent = parent.ParentClass; } if (!string.IsNullOrWhiteSpace(lc.Namespace)) { _builder.Append($@" }}"); } }