From b06406f4306e0bf202fca8a61b2526278a540bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Fri, 29 Dec 2023 21:36:11 -0500 Subject: [PATCH] MA0153 Do not log symbols decorated with DataClassificationAttribute directly --- README.md | 1 + docs/README.md | 7 + docs/Rules/MA0153.md | 26 ++ .../Internals/TypeSymbolExtensions.cs | 5 +- src/Meziantou.Analyzer/RuleIdentifiers.cs | 1 + .../Rules/DoNotLogClassifiedDataAnalyzer.cs | 159 +++++++++++++ .../DoNotLogClassifiedDataAnalyzerTests.cs | 170 +++++++++++++ .../Rules/LoggerParameterTypeAnalyzerTests.cs | 217 +---------------- ...oggerParameterTypeAnalyzer_SerilogTests.cs | 225 ++++++++++++++++++ 9 files changed, 596 insertions(+), 215 deletions(-) create mode 100644 docs/Rules/MA0153.md create mode 100644 src/Meziantou.Analyzer/Rules/DoNotLogClassifiedDataAnalyzer.cs create mode 100644 tests/Meziantou.Analyzer.Test/Rules/DoNotLogClassifiedDataAnalyzerTests.cs create mode 100644 tests/Meziantou.Analyzer.Test/Rules/LoggerParameterTypeAnalyzer_SerilogTests.cs diff --git a/README.md b/README.md index 73a1b6783..d69745bd3 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,7 @@ If you are already using other analyzers, you can check [which rules are duplica |[MA0150](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0150.md)|Design|Do not call the default object.ToString explicitly|⚠️|✔️|❌| |[MA0151](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0151.md)|Usage|DebuggerDisplay must contain valid members|⚠️|✔️|❌| |[MA0152](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0152.md)|Performance|Use Unwrap instead of using await twice|ℹ️|✔️|❌| +|[MA0153](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0153.md)|Design|Do not log symbols decorated with DataClassificationAttribute directly|⚠️|✔️|❌| diff --git a/docs/README.md b/docs/README.md index a051dcd06..57c712c8a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -152,6 +152,7 @@ |[MA0150](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0150.md)|Design|Do not call the default object.ToString explicitly|⚠️|✔️|❌| |[MA0151](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0151.md)|Usage|DebuggerDisplay must contain valid members|⚠️|✔️|❌| |[MA0152](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0152.md)|Performance|Use Unwrap instead of using await twice|ℹ️|✔️|❌| +|[MA0153](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0153.md)|Design|Do not log symbols decorated with DataClassificationAttribute directly|⚠️|✔️|❌| |Id|Suppressed rule|Justification| |--|---------------|-------------| @@ -614,6 +615,9 @@ dotnet_diagnostic.MA0151.severity = warning # MA0152: Use Unwrap instead of using await twice dotnet_diagnostic.MA0152.severity = suggestion + +# MA0153: Do not log symbols decorated with DataClassificationAttribute directly +dotnet_diagnostic.MA0153.severity = warning ``` # .editorconfig - all rules disabled @@ -1071,4 +1075,7 @@ dotnet_diagnostic.MA0151.severity = none # MA0152: Use Unwrap instead of using await twice dotnet_diagnostic.MA0152.severity = none + +# MA0153: Do not log symbols decorated with DataClassificationAttribute directly +dotnet_diagnostic.MA0153.severity = none ``` diff --git a/docs/Rules/MA0153.md b/docs/Rules/MA0153.md new file mode 100644 index 000000000..bfab8d8e9 --- /dev/null +++ b/docs/Rules/MA0153.md @@ -0,0 +1,26 @@ +# MA0153 - Do not log symbols decorated with DataClassificationAttribute directly + +Detects when a log parameter is decorated with an attribute that inherits from `Microsoft.Extensions.Compliance.Classification.DataClassificationAttribute`. +Most of the time, these values should not be used with `[LogProperties]` to redact values. + +````c# +using Microsoft.Extensions.Logging; + +ILogger logger; + +// non-compliant as Prop is decorated with an attribute that inherits from DataClassificationAttribute +logger.LogInformation("{Prop}", new Dummy().Prop); + +class Dummy +{ + [PiiAttribute] + public string Prop { get; set; } +} + +class PiiAttribute : Microsoft.Extensions.Compliance.Classification.DataClassificationAttribute +{ + public TaxonomyAttribute() : base(default) + { + } +} +```` diff --git a/src/Meziantou.Analyzer/Internals/TypeSymbolExtensions.cs b/src/Meziantou.Analyzer/Internals/TypeSymbolExtensions.cs index 9b952e39c..581def6ba 100644 --- a/src/Meziantou.Analyzer/Internals/TypeSymbolExtensions.cs +++ b/src/Meziantou.Analyzer/Internals/TypeSymbolExtensions.cs @@ -71,9 +71,12 @@ public static bool IsOrImplements(this ITypeSymbol symbol, ITypeSymbol? interfac foreach (var attribute in symbol.GetAttributes()) { + if (attribute.AttributeClass is null) + continue; + if (inherits) { - if (attributeType.IsOrInheritFrom(attribute.AttributeClass)) + if (attribute.AttributeClass.IsOrInheritFrom(attributeType)) return attribute; } else diff --git a/src/Meziantou.Analyzer/RuleIdentifiers.cs b/src/Meziantou.Analyzer/RuleIdentifiers.cs index 7087209fd..2c213f347 100644 --- a/src/Meziantou.Analyzer/RuleIdentifiers.cs +++ b/src/Meziantou.Analyzer/RuleIdentifiers.cs @@ -155,6 +155,7 @@ internal static class RuleIdentifiers public const string DoNotUseToStringIfObject = "MA0150"; public const string DebuggerDisplayAttributeShouldContainValidExpressions = "MA0151"; public const string UseTaskUnwrap = "MA0152"; + public const string DoNotLogClassifiedData = "MA0153"; public static string GetHelpUri(string identifier) { diff --git a/src/Meziantou.Analyzer/Rules/DoNotLogClassifiedDataAnalyzer.cs b/src/Meziantou.Analyzer/Rules/DoNotLogClassifiedDataAnalyzer.cs new file mode 100644 index 000000000..637bb301f --- /dev/null +++ b/src/Meziantou.Analyzer/Rules/DoNotLogClassifiedDataAnalyzer.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Meziantou.Analyzer.Rules; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class DoNotLogClassifiedDataAnalyzer : DiagnosticAnalyzer +{ + private static readonly DiagnosticDescriptor Rule = new( + RuleIdentifiers.DoNotLogClassifiedData, + title: "Do not log symbols decorated with DataClassificationAttribute directly", + messageFormat: "Do not log symbols decorated with DataClassificationAttribute directly", + RuleCategories.Design, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "", + helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.DoNotLogClassifiedData)); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.RegisterCompilationStartAction(context => + { + var ctx = new AnalyzerContext(context.Compilation); + if (!ctx.IsValid) + return; + + context.RegisterOperationAction(ctx.AnalyzeInvocationDeclaration, OperationKind.Invocation); + }); + } + + private sealed class AnalyzerContext + { + public AnalyzerContext(Compilation compilation) + { + LoggerSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.ILogger"); + if (LoggerSymbol is null) + return; + + LoggerExtensionsSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.LoggerExtensions"); + LoggerMessageSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.LoggerMessage"); + StructuredLogFieldAttributeSymbol = compilation.GetBestTypeByMetadataName("Meziantou.Analyzer.Annotations.StructuredLogFieldAttribute"); + + DataClassificationAttributeSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Compliance.Classification.DataClassificationAttribute"); + } + + public INamedTypeSymbol? StructuredLogFieldAttributeSymbol { get; private set; } + + public INamedTypeSymbol? LoggerSymbol { get; } + public INamedTypeSymbol? LoggerExtensionsSymbol { get; } + public INamedTypeSymbol? LoggerMessageSymbol { get; } + + public INamedTypeSymbol? DataClassificationAttributeSymbol { get; } + + public bool IsValid => DataClassificationAttributeSymbol is not null && LoggerSymbol is not null; + + public void AnalyzeInvocationDeclaration(OperationAnalysisContext context) + { + var operation = (IInvocationOperation)context.Operation; + if (operation.TargetMethod.ContainingType.IsEqualTo(LoggerExtensionsSymbol) && FindLogParameters(operation.TargetMethod, out var argumentsParameter)) + { + foreach (var argument in operation.Arguments) + { + var parameter = argument.Parameter; + if (parameter is null) + continue; + + if (parameter.Equals(argumentsParameter, SymbolEqualityComparer.Default)) + { + if (argument.ArgumentKind == ArgumentKind.ParamArray && argument.Value is IArrayCreationOperation arrayCreation && arrayCreation.Initializer is not null) + { + ValidateDataClassification(context, arrayCreation.Initializer.ElementValues); + } + } + } + } + } + + private void ValidateDataClassification(DiagnosticReporter diagnosticReporter, IEnumerable operations) + { + foreach (var operation in operations) + { + ValidateDataClassification(diagnosticReporter, operation); + } + } + + private void ValidateDataClassification(DiagnosticReporter diagnosticReporter, IOperation operation) + { + ValidateDataClassification(diagnosticReporter, operation, operation, DataClassificationAttributeSymbol!); + + static void ValidateDataClassification(DiagnosticReporter diagnosticReporter, IOperation operation, IOperation reportOperation, INamedTypeSymbol dataClassificationAttributeSymbol) + { + operation = operation.UnwrapConversionOperations(); + if (operation is IParameterReferenceOperation parameterReferenceOperation) + { + if (parameterReferenceOperation.Parameter.HasAttribute(dataClassificationAttributeSymbol, inherits: true)) + { + diagnosticReporter.ReportDiagnostic(Rule, reportOperation); + } + } + else if (operation is IPropertyReferenceOperation propertyReferenceOperation) + { + if (propertyReferenceOperation.Property.HasAttribute(dataClassificationAttributeSymbol, inherits: true)) + { + diagnosticReporter.ReportDiagnostic(Rule, reportOperation); + } + } + else if (operation is IFieldReferenceOperation fieldReferenceOperation) + { + if (fieldReferenceOperation.Field.HasAttribute(dataClassificationAttributeSymbol, inherits: true)) + { + diagnosticReporter.ReportDiagnostic(Rule, reportOperation); + } + } + else if (operation is IArrayElementReferenceOperation arrayElementReferenceOperation) + { + ValidateDataClassification(diagnosticReporter, arrayElementReferenceOperation.ArrayReference, reportOperation, dataClassificationAttributeSymbol); + } + } + } + + private static bool FindLogParameters(IMethodSymbol methodSymbol, out IParameterSymbol? arguments) + { + IParameterSymbol? message = null; + arguments = null; + foreach (var parameter in methodSymbol.Parameters) + { + if (parameter.Type.IsString() && + (string.Equals(parameter.Name, "message", StringComparison.Ordinal) || + string.Equals(parameter.Name, "messageFormat", StringComparison.Ordinal) || + string.Equals(parameter.Name, "formatString", StringComparison.Ordinal))) + { + message = parameter; + } + // When calling logger.BeginScope("{Param}") generic overload would be selected + else if (parameter.Type.SpecialType == SpecialType.System_String && + methodSymbol.Name is "BeginScope" && + parameter.Name is "state") + { + message = parameter; + } + else if (parameter.IsParams && + parameter.Name is "args") + { + arguments = parameter; + } + } + + return message is not null; + } + } +} diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotLogClassifiedDataAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotLogClassifiedDataAnalyzerTests.cs new file mode 100644 index 000000000..23a6f239a --- /dev/null +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotLogClassifiedDataAnalyzerTests.cs @@ -0,0 +1,170 @@ +using System.Threading.Tasks; +using Meziantou.Analyzer.Rules; +using TestHelper; +using Xunit; + +namespace Meziantou.Analyzer.Test.Rules; +public sealed class DoNotLogClassifiedDataAnalyzerTests +{ + private static ProjectBuilder CreateProjectBuilder() => new ProjectBuilder() + .WithAnalyzer() + .WithOutputKind(Microsoft.CodeAnalysis.OutputKind.ConsoleApplication) + .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.Latest) + .WithTargetFramework(TargetFramework.Net8_0) + .AddNuGetReference("Microsoft.Extensions.Logging.Abstractions", "8.0.0", "lib/net8.0") + .AddNuGetReference("Microsoft.Extensions.Compliance.Abstractions", "8.0.0", "lib/net8.0"); + + [Fact] + public async Task Logger_LogInformation_NoDataClassification() + { + const string SourceCode = """ +using Microsoft.Extensions.Logging; + +ILogger logger = null; +logger.LogInformation("{Prop}", new Dummy().Prop); + +class Dummy +{ + public string Prop { get; set; } +} + +class TaxonomyAttribute : Microsoft.Extensions.Compliance.Classification.DataClassificationAttribute +{ + public TaxonomyAttribute() : base(Microsoft.Extensions.Compliance.Classification.DataClassification.Unknown) { } +} +"""; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task Logger_LogInformation_DataClassification_Property() + { + const string SourceCode = """ +using Microsoft.Extensions.Logging; + +ILogger logger = null; +logger.LogInformation("{Prop}", [|new Dummy().Prop|]); + +class Dummy +{ + [TaxonomyAttribute()] + public string Prop { get; set; } +} + +class TaxonomyAttribute : Microsoft.Extensions.Compliance.Classification.DataClassificationAttribute +{ + public TaxonomyAttribute() : base(Microsoft.Extensions.Compliance.Classification.DataClassification.Unknown) { } +} +"""; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task Logger_LogInformation_DataClassification_Property_Array() + { + const string SourceCode = """ +using Microsoft.Extensions.Logging; + +ILogger logger = null; +logger.LogInformation("{Prop}", [|new Dummy().Prop[0]|]); + +class Dummy +{ + [TaxonomyAttribute()] + public string[] Prop { get; set; } +} + +class TaxonomyAttribute : Microsoft.Extensions.Compliance.Classification.DataClassificationAttribute +{ + public TaxonomyAttribute() : base(Microsoft.Extensions.Compliance.Classification.DataClassification.Unknown) { } +} +"""; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task Logger_LogInformation_DataClassification_Field() + { + const string SourceCode = """ +using Microsoft.Extensions.Logging; + +ILogger logger = null; +logger.LogInformation("{Prop}", [|new Dummy().Prop|]); + +class Dummy +{ + [TaxonomyAttribute()] + public string Prop; +} + +class TaxonomyAttribute : Microsoft.Extensions.Compliance.Classification.DataClassificationAttribute +{ + public TaxonomyAttribute() : base(Microsoft.Extensions.Compliance.Classification.DataClassification.Unknown) { } +} +"""; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task Logger_LogInformation_DataClassification_Parameter() + { + const string SourceCode = """ +using Microsoft.Extensions.Logging; + +ILogger logger = null; + +void A([TaxonomyAttribute]int param) +{ + logger.LogInformation("{Prop}", [|param|]); +} + +class Dummy +{ + [TaxonomyAttribute()] + public string Prop; +} + +class TaxonomyAttribute : Microsoft.Extensions.Compliance.Classification.DataClassificationAttribute +{ + public TaxonomyAttribute() : base(Microsoft.Extensions.Compliance.Classification.DataClassification.Unknown) { } +} +"""; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } + + [Fact] + public async Task Logger_BeginScope_DataClassification_Parameter() + { + const string SourceCode = """ +using Microsoft.Extensions.Logging; + + +ILogger logger = null; +logger.BeginScope("{Prop}", [|new Dummy().Prop|]); + +class Dummy +{ + [TaxonomyAttribute()] + public string Prop; +} + +class TaxonomyAttribute : Microsoft.Extensions.Compliance.Classification.DataClassificationAttribute +{ + public TaxonomyAttribute() : base(Microsoft.Extensions.Compliance.Classification.DataClassification.Unknown) { } +} +"""; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ValidateAsync(); + } +} diff --git a/tests/Meziantou.Analyzer.Test/Rules/LoggerParameterTypeAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/LoggerParameterTypeAnalyzerTests.cs index 6dcf11645..d53f99411 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/LoggerParameterTypeAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/LoggerParameterTypeAnalyzerTests.cs @@ -10,9 +10,8 @@ public sealed class LoggerParameterTypeAnalyzerTests .WithAnalyzer() .WithOutputKind(Microsoft.CodeAnalysis.OutputKind.ConsoleApplication) .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.Latest) - .WithTargetFramework(TargetFramework.Net7_0) - .AddNuGetReference("Microsoft.Extensions.Logging.Abstractions", "7.0.0", "lib/net7.0") - .AddNuGetReference("Serilog", "3.0.1", "lib/net7.0"); + .WithTargetFramework(TargetFramework.Net8_0) + .AddNuGetReference("Microsoft.Extensions.Logging.Abstractions", "8.0.0", "lib/net8.0"); [Fact] public async Task BeginScope_InvalidParameterType() @@ -354,7 +353,7 @@ await CreateProjectBuilder() """) .ValidateAsync(); } - + [Fact] public async Task Configuration_UnknownParameterType() { @@ -435,216 +434,6 @@ await CreateProjectBuilder() .WithSourceCode(SourceCode) .AddAdditionalFile("LoggerParameterTypes.txt", """ Prop;System.Int32 -""") - .ValidateAsync(); - } - - [Fact] - public async Task SeriLog_Log_Information() - { - const string SourceCode = """ -using Serilog; - -Log.Information("{Prop}", 1); -Log.Information("{Prop}", [||](int?)1); -"""; - await CreateProjectBuilder() - .WithSourceCode(SourceCode) - .AddAdditionalFile("LoggerParameterTypes.txt", """ -Prop;System.Int32 -""") - .ValidateAsync(); - } - - [Fact] - public async Task SeriLog_Log_Information_Exception() - { - const string SourceCode = """ -using Serilog; - -Log.Information((System.Exception)null, "{Prop}", 1); -Log.Information((System.Exception)null, "{Prop}", [||](int?)1); -"""; - await CreateProjectBuilder() - .WithSourceCode(SourceCode) - .AddAdditionalFile("LoggerParameterTypes.txt", """ -Prop;System.Int32 -""") - .ValidateAsync(); - } - - [Fact] - public async Task SeriLog_Log_Information_Params() - { - const string SourceCode = """ -using Serilog; - -Log.Information("{Prop}{Prop}{Prop}{Prop}", 1, 1, 1, 1); -"""; - await CreateProjectBuilder() - .WithSourceCode(SourceCode) - .AddAdditionalFile("LoggerParameterTypes.txt", """ -Prop;System.Int32 -""") - .ValidateAsync(); - } - - [Fact] - public async Task SeriLog_Log_Information_AtPrefix() - { - const string SourceCode = """ -using Serilog; - -Log.Information("{@Prop}", 1); -"""; - await CreateProjectBuilder() - .WithSourceCode(SourceCode) - .AddAdditionalFile("LoggerParameterTypes.txt", """ -Prop;System.Int32 -""") - .ValidateAsync(); - } - - [Fact] - public async Task SeriLog_Log_Information_DollarPrefix() - { - const string SourceCode = """ -using Serilog; - -Log.Information("{$Prop}", 1); -"""; - await CreateProjectBuilder() - .WithSourceCode(SourceCode) - .AddAdditionalFile("LoggerParameterTypes.txt", """ -Prop;System.Int32 -""") - .ValidateAsync(); - } - - [Fact] - public async Task SeriLog_Enrich_WithProperty() - { - const string SourceCode = """ -using Serilog; - -new LoggerConfiguration().Enrich.WithProperty("Prop", 0); -"""; - await CreateProjectBuilder() - .WithSourceCode(SourceCode) - .AddAdditionalFile("LoggerParameterTypes.txt", """ -Prop;System.Int32 -""") - .ValidateAsync(); - } - - [Fact] - public async Task SeriLog_Enrich_WithProperty_Invalid() - { - const string SourceCode = """ -using Serilog; - -new LoggerConfiguration().Enrich.WithProperty("Prop", [||]""); -"""; - await CreateProjectBuilder() - .WithSourceCode(SourceCode) - .AddAdditionalFile("LoggerParameterTypes.txt", """ -Prop;System.Int32 -""") - .ValidateAsync(); - } - - [Fact] - public async Task SeriLog_Log_ForContext() - { - const string SourceCode = """ -using Serilog; - -Log.ForContext("Prop", 0); -"""; - await CreateProjectBuilder() - .WithSourceCode(SourceCode) - .AddAdditionalFile("LoggerParameterTypes.txt", """ -Prop;System.Int32 -""") - .ValidateAsync(); - } - - [Fact] - public async Task SeriLog_Log_ForContext_Invalid() - { - const string SourceCode = """ -using Serilog; - -Log.ForContext("Prop", [||]""); -"""; - await CreateProjectBuilder() - .WithSourceCode(SourceCode) - .AddAdditionalFile("LoggerParameterTypes.txt", """ -Prop;System.Int32 -""") - .ValidateAsync(); - } - - [Fact] - public async Task SeriLog_ILogger_ForContext() - { - const string SourceCode = """ -using Serilog; - -Log.Logger.ForContext("Prop", 0); -"""; - await CreateProjectBuilder() - .WithSourceCode(SourceCode) - .AddAdditionalFile("LoggerParameterTypes.txt", """ -Prop;System.Int32 -""") - .ValidateAsync(); - } - - [Fact] - public async Task SeriLog_ILogger_ForContext_Invalid() - { - const string SourceCode = """ -using Serilog; - -Log.Logger.ForContext("Prop", [||]""); -"""; - await CreateProjectBuilder() - .WithSourceCode(SourceCode) - .AddAdditionalFile("LoggerParameterTypes.txt", """ -Prop;System.Int32 -""") - .ValidateAsync(); - } - - [Fact] - public async Task SeriLog_ILogger_ForContext_LogEventLevel() - { - const string SourceCode = """ -using Serilog; - -Log.Logger.ForContext(Serilog.Events.LogEventLevel.Warning, "Prop", 0); -"""; - await CreateProjectBuilder() - .WithSourceCode(SourceCode) - .AddAdditionalFile("LoggerParameterTypes.txt", """ -Prop;System.Int32 -""") - .ValidateAsync(); - } - - [Fact] - public async Task SeriLog_ILogger_ForContext_LogEventLevel_Invalid() - { - const string SourceCode = """ -using Serilog; - -Log.Logger.ForContext(Serilog.Events.LogEventLevel.Warning,"Prop", [||]""); -"""; - await CreateProjectBuilder() - .WithSourceCode(SourceCode) - .AddAdditionalFile("LoggerParameterTypes.txt", """ -Prop;System.Int32 """) .ValidateAsync(); } diff --git a/tests/Meziantou.Analyzer.Test/Rules/LoggerParameterTypeAnalyzer_SerilogTests.cs b/tests/Meziantou.Analyzer.Test/Rules/LoggerParameterTypeAnalyzer_SerilogTests.cs new file mode 100644 index 000000000..3d117b19b --- /dev/null +++ b/tests/Meziantou.Analyzer.Test/Rules/LoggerParameterTypeAnalyzer_SerilogTests.cs @@ -0,0 +1,225 @@ +using System.Threading.Tasks; +using Meziantou.Analyzer.Rules; +using TestHelper; +using Xunit; + +namespace Meziantou.Analyzer.Test.Rules; +public sealed class LoggerParameterTypeAnalyzer_SerilogTests +{ + private static ProjectBuilder CreateProjectBuilder() => new ProjectBuilder() + .WithAnalyzer() + .WithOutputKind(Microsoft.CodeAnalysis.OutputKind.ConsoleApplication) + .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.Latest) + .WithTargetFramework(TargetFramework.Net8_0) + .AddNuGetReference("Serilog", "3.1.1", "lib/net7.0"); + + [Fact] + public async Task SeriLog_Log_Information() + { + const string SourceCode = """ +using Serilog; + +Log.Information("{Prop}", 1); +Log.Information("{Prop}", [||](int?)1); +"""; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .AddAdditionalFile("LoggerParameterTypes.txt", """ +Prop;System.Int32 +""") + .ValidateAsync(); + } + + [Fact] + public async Task SeriLog_Log_Information_Exception() + { + const string SourceCode = """ +using Serilog; + +Log.Information((System.Exception)null, "{Prop}", 1); +Log.Information((System.Exception)null, "{Prop}", [||](int?)1); +"""; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .AddAdditionalFile("LoggerParameterTypes.txt", """ +Prop;System.Int32 +""") + .ValidateAsync(); + } + + [Fact] + public async Task SeriLog_Log_Information_Params() + { + const string SourceCode = """ +using Serilog; + +Log.Information("{Prop}{Prop}{Prop}{Prop}", 1, 1, 1, 1); +"""; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .AddAdditionalFile("LoggerParameterTypes.txt", """ +Prop;System.Int32 +""") + .ValidateAsync(); + } + + [Fact] + public async Task SeriLog_Log_Information_AtPrefix() + { + const string SourceCode = """ +using Serilog; + +Log.Information("{@Prop}", 1); +"""; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .AddAdditionalFile("LoggerParameterTypes.txt", """ +Prop;System.Int32 +""") + .ValidateAsync(); + } + + [Fact] + public async Task SeriLog_Log_Information_DollarPrefix() + { + const string SourceCode = """ +using Serilog; + +Log.Information("{$Prop}", 1); +"""; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .AddAdditionalFile("LoggerParameterTypes.txt", """ +Prop;System.Int32 +""") + .ValidateAsync(); + } + + [Fact] + public async Task SeriLog_Enrich_WithProperty() + { + const string SourceCode = """ +using Serilog; + +new LoggerConfiguration().Enrich.WithProperty("Prop", 0); +"""; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .AddAdditionalFile("LoggerParameterTypes.txt", """ +Prop;System.Int32 +""") + .ValidateAsync(); + } + + [Fact] + public async Task SeriLog_Enrich_WithProperty_Invalid() + { + const string SourceCode = """ +using Serilog; + +new LoggerConfiguration().Enrich.WithProperty("Prop", [||]""); +"""; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .AddAdditionalFile("LoggerParameterTypes.txt", """ +Prop;System.Int32 +""") + .ValidateAsync(); + } + + [Fact] + public async Task SeriLog_Log_ForContext() + { + const string SourceCode = """ +using Serilog; + +Log.ForContext("Prop", 0); +"""; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .AddAdditionalFile("LoggerParameterTypes.txt", """ +Prop;System.Int32 +""") + .ValidateAsync(); + } + + [Fact] + public async Task SeriLog_Log_ForContext_Invalid() + { + const string SourceCode = """ +using Serilog; + +Log.ForContext("Prop", [||]""); +"""; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .AddAdditionalFile("LoggerParameterTypes.txt", """ +Prop;System.Int32 +""") + .ValidateAsync(); + } + + [Fact] + public async Task SeriLog_ILogger_ForContext() + { + const string SourceCode = """ +using Serilog; + +Log.Logger.ForContext("Prop", 0); +"""; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .AddAdditionalFile("LoggerParameterTypes.txt", """ +Prop;System.Int32 +""") + .ValidateAsync(); + } + + [Fact] + public async Task SeriLog_ILogger_ForContext_Invalid() + { + const string SourceCode = """ +using Serilog; + +Log.Logger.ForContext("Prop", [||]""); +"""; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .AddAdditionalFile("LoggerParameterTypes.txt", """ +Prop;System.Int32 +""") + .ValidateAsync(); + } + + [Fact] + public async Task SeriLog_ILogger_ForContext_LogEventLevel() + { + const string SourceCode = """ +using Serilog; + +Log.Logger.ForContext(Serilog.Events.LogEventLevel.Warning, "Prop", 0); +"""; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .AddAdditionalFile("LoggerParameterTypes.txt", """ +Prop;System.Int32 +""") + .ValidateAsync(); + } + + [Fact] + public async Task SeriLog_ILogger_ForContext_LogEventLevel_Invalid() + { + const string SourceCode = """ +using Serilog; + +Log.Logger.ForContext(Serilog.Events.LogEventLevel.Warning,"Prop", [||]""); +"""; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .AddAdditionalFile("LoggerParameterTypes.txt", """ +Prop;System.Int32 +""") + .ValidateAsync(); + } +}