| File: CallAnalysis\Fixers\LegacyLoggingFixer.FixDetails.cs | Web Access |
| Project: src\src\Analyzers\Microsoft.Analyzers.Extra\Microsoft.Analyzers.Extra.csproj (Microsoft.Analyzers.Extra) |
// 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.Linq; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Operations; namespace Microsoft.Extensions.ExtraAnalyzers; public sealed partial class LegacyLoggingFixer { /// <summary> /// Tracks a bunch of metadata about a potential fix to apply. /// </summary> internal sealed class FixDetails { public int MessageParamIndex { get; } public int ExceptionParamIndex { get; } public int EventIdParamIndex { get; } public int LogLevelParamIndex { get; } public int ArgsParamIndex { get; } public string Message { get; } = string.Empty; public string Level { get; } = string.Empty; public string TargetFilename { get; } public string TargetNamespace { get; } public string TargetClassName { get; } public string TargetMethodName { get; } public IReadOnlyList<string> MessageArgs { get; } public IReadOnlyList<IOperation>? InterpolationArgs { get; } public FixDetails( IMethodSymbol method, IInvocationOperation invocationOp, string? defaultNamespace, IEnumerable<Document> docs) { (MessageParamIndex, ExceptionParamIndex, EventIdParamIndex, LogLevelParamIndex, ArgsParamIndex) = IdentifyParameters(method); if (MessageParamIndex >= 0) { var op = invocationOp.Arguments[MessageParamIndex]; var children = op.ChildOperations.ToArray(); if (children.Length == 1) { if (children[0].ConstantValue.HasValue) { Message = children[0].ConstantValue.Value as string ?? string.Empty; } else if (children[0] is IInterpolatedStringOperation inter) { var interpolationArgs = new List<IOperation>(); var messageArgs = new List<string>(); InterpolationArgs = interpolationArgs; MessageArgs = messageArgs; var sb = new StringBuilder(); int argCount = 0; foreach (var o in children[0].ChildOperations) { switch (o) { case IInterpolatedStringTextOperation stringOp: _ = sb.Append(stringOp.ChildOperations.First().ConstantValue.Value as string); break; case IInterpolationOperation intOp: var operation = intOp.ChildOperations.First(); var argName = operation.Syntax.GetLastToken().Text; if (argName.Length == 0) { argName = $"_arg{argCount++}"; } else { char firstChar = argName[0]; if (firstChar >= 'A' && firstChar <= 'Z') { argName = (char)(firstChar + ('a' - 'A')) + argName.Substring(1); } else if (firstChar < 'a' || firstChar > 'z') { argName = $"_arg{argCount++}"; } } _ = sb.Append("{" + argName + "}"); messageArgs.Add(argName); interpolationArgs.Add(intOp.ChildOperations.First()); break; } } Message = sb.ToString(); } } } if (LogLevelParamIndex > 0) { object? value = null; var op = invocationOp.Arguments[LogLevelParamIndex].Descendants().SingleOrDefault(x => x.Kind == OperationKind.Literal || x.Kind == OperationKind.FieldReference); switch (op) { case ILiteralOperation lit: value = lit.ConstantValue.Value; break; case IFieldReferenceOperation fieldRef: value = fieldRef.ConstantValue.Value; break; } if (value is int) { Level = GetLogLevelName((LogLevel)value); } } else { Level = method.Name.Substring("Log".Length); } TargetFilename = FindUniqueFilename(docs); TargetNamespace = defaultNamespace ?? string.Empty; TargetClassName = "Log"; TargetMethodName = DeriveName(Message); MessageArgs ??= ExtractTemplateArgs(Message); } public string FullTargetClassName { get { if (string.IsNullOrEmpty(TargetNamespace)) { return TargetClassName; } return $"{TargetNamespace}.{TargetClassName}"; } } internal static string GetLogLevelName(LogLevel value) { return value switch { LogLevel.Trace => "Trace", LogLevel.Debug => "Debug", LogLevel.Information => "Information", LogLevel.Warning => "Warning", LogLevel.Error => "Error", LogLevel.Critical => "Critical", _ => string.Empty, }; } private static string FindUniqueFilename(IEnumerable<Document> docs) { var targetName = "Log.cs"; int count = 2; bool duplicate; do { duplicate = false; foreach (var doc in docs) { if (string.Equals(doc.Name, targetName, StringComparison.OrdinalIgnoreCase)) { duplicate = true; targetName = $"Log{count}.cs"; count++; break; } } } while (duplicate); return targetName; } /// <summary> /// Finds the position of the well-known parameters of legacy logging methods. /// </summary> /// <returns>-1 for any parameter not present in the given overload.</returns> private static (int message, int exception, int eventId, int logLevel, int args) IdentifyParameters(IMethodSymbol method) { var message = -1; var exception = -1; var eventId = -1; var logLevel = -1; var args = -1; int index = 0; foreach (var p in method.Parameters) { switch (p.Name) { case "message": message = index; break; case "exception": exception = index; break; case "eventId": eventId = index; break; case "logLevel": logLevel = index; break; case "args": args = index; break; } index++; } return (message, exception, eventId, logLevel, args); } /// <summary> /// Given a logging message with template args, generate a reasonable logging method name. /// </summary> private static string DeriveName(string message) { var sb = new StringBuilder(); bool capitalizeNext = true; foreach (var ch in message) { if (char.IsLetter(ch) || (char.IsLetterOrDigit(ch) && sb.Length > 1)) { if (capitalizeNext) { _ = sb.Append(char.ToUpperInvariant(ch)); capitalizeNext = false; } else { _ = sb.Append(ch); } } else { capitalizeNext = true; } } return sb.ToString(); } private static readonly char[] _formatDelimiters = { ',', ':' }; /// <summary> /// Finds the template arguments contained in the message string. /// </summary> private static List<string> ExtractTemplateArgs(string message) { var args = new List<string>(); var scanIndex = 0; var endIndex = message.Length; while (scanIndex < endIndex) { var openBraceIndex = FindBraceIndex(message, '{', scanIndex, endIndex); var closeBraceIndex = FindBraceIndex(message, '}', openBraceIndex, endIndex); if (closeBraceIndex == endIndex) { scanIndex = endIndex; } else { // Format item syntax : { index[,alignment][ :formatString] }. var formatDelimiterIndex = FindIndexOfAny(message, _formatDelimiters, openBraceIndex, closeBraceIndex); args.Add(message.Substring(openBraceIndex + 1, formatDelimiterIndex - openBraceIndex - 1)); scanIndex = closeBraceIndex + 1; } } return args; } private static int FindBraceIndex(string message, char brace, int startIndex, int endIndex) { // Example: {{prefix{{{Argument}}}suffix}}. var braceIndex = endIndex; var scanIndex = startIndex; var braceOccurrenceCount = 0; while (scanIndex < endIndex) { if (braceOccurrenceCount > 0 && message[scanIndex] != brace) { #pragma warning disable S109 // Magic numbers should not be used if (braceOccurrenceCount % 2 == 0) #pragma warning restore S109 // Magic numbers should not be used { // Even number of '{' or '}' found. Proceed search with next occurrence of '{' or '}'. braceOccurrenceCount = 0; braceIndex = endIndex; } else { // An unescaped '{' or '}' found. break; } } else if (message[scanIndex] == brace) { if (brace == '}') { if (braceOccurrenceCount == 0) { // For '}' pick the first occurrence. braceIndex = scanIndex; } } else { // For '{' pick the last occurrence. braceIndex = scanIndex; } braceOccurrenceCount++; } scanIndex++; } return braceIndex; } private static int FindIndexOfAny(string message, char[] chars, int startIndex, int endIndex) { var findIndex = message.IndexOfAny(chars, startIndex, endIndex - startIndex); return findIndex == -1 ? endIndex : findIndex; } } }