├── .editorconfig
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── 01_bug_report.yml
│ ├── 02_feature_proposal.yml
│ └── 03_blank_issue.yml
└── workflows
│ └── dotnet_build.yml
├── .gitignore
├── AutoLoggerMessage.sln
├── AutoLoggerMessage.sln.DotSettings
├── AutoLoggerMessage.slnx
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Directory.Build.props
├── LICENSE
├── README.md
├── benchmarks
├── AutoLoggerMessageGenerator.Benchmarks.csproj
├── BenchmarkFiles
│ ├── CustomNullLogger.cs
│ ├── LogCallBenchmark.cs
│ └── LogScopeBenchmark.cs
├── CommandRunner.cs
├── Directory.Build.props
├── Directory.Packages.props
├── MultiBenchmarkRunner.cs
├── PackagesProvider.cs
├── Program.cs
├── ProjectBuilder.cs
├── ProjectConfiguration.cs
├── ProjectConfigurationColumn.cs
└── TargetFrameworkMonikerDetector.cs
├── docs
├── ADR
│ ├── ADR-01_Generation_of_logger_extension_methods_overloads.md
│ ├── ADR-02_Reusing_the_existing_loggermessage_generators.md
│ ├── ADR-03_Supporting_both_old_and_new_loggermessage_generators.md
│ ├── ADR-04_Generation_of_loggermessage_methods.md
│ ├── ADR-05_Backtracking_diagnostic_location.md
│ ├── ADR-06_Skip_log_calls_with_invalid_template_parameter_names.md
│ └── ADR-07_Generation_of_BeginScope_methods.md
└── how-it-works.md
├── src
├── AutoLoggerMessageGenerator.BuildOutput
│ ├── AutoLoggerMessageGenerator.BuildOutput.csproj
│ ├── GenericLoggerExtensions.g.cs
│ └── GenericLoggerScopeExtensions.g.cs
├── AutoLoggerMessageGenerator.Sandbox
│ ├── AutoLoggerMessageGenerator.Sandbox.csproj
│ └── Program.cs
└── AutoLoggerMessageGenerator
│ ├── Analysers
│ └── InvalidTemplateParameterNameAnalyser.cs
│ ├── Assets
│ └── icon.jpg
│ ├── AutoLoggerMessageGenerator.Build.targets
│ ├── AutoLoggerMessageGenerator.Pack.csproj
│ ├── AutoLoggerMessageGenerator.Roslyn4_11.csproj
│ ├── AutoLoggerMessageGenerator.Roslyn4_8.csproj
│ ├── AutoLoggerMessageGenerator.props
│ ├── AutoLoggerMessageGenerator.targets
│ ├── Caching
│ ├── LogCallInputSourceComparer.cs
│ └── LogScopeDefinitionInputSourceComparer.cs
│ ├── Configuration
│ ├── GeneratorOptionsProvider.cs
│ ├── SourceGeneratorConfiguration.cs
│ └── SourceGeneratorConfigurationExtensions.cs
│ ├── Constants.cs
│ ├── Diagnostics
│ └── LogMessageDiagnosticReporter.cs
│ ├── Emitters
│ ├── GenericLoggerExtensionsEmitter.cs
│ ├── GenericLoggerScopeExtensionsEmitter.cs
│ ├── InterceptorAttributeEmitter.cs
│ ├── LoggerInterceptorsEmitter.cs
│ ├── LoggerScopeInterceptorsEmitter.cs
│ └── LoggerScopesEmitter.cs
│ ├── Extractors
│ ├── CallParametersExtractor.cs
│ ├── EnclosingClassExtractor.cs
│ ├── LogCallExtractor.cs
│ ├── LogLevelExtractor.cs
│ ├── LoggerScopeCallExtractor.cs
│ ├── MessageParameterNamesExtractor.cs
│ └── MessageParameterTextExtractor.cs
│ ├── Filters
│ ├── LogMessageCallFilter.cs
│ └── LoggerScopeFilter.cs
│ ├── Generators
│ ├── AutoLoggerMessageGenerator.LoggerMessage.cs
│ ├── AutoLoggerMessageGenerator.LoggerScopes.cs
│ └── AutoLoggerMessageGenerator.cs
│ ├── Import
│ ├── .editorconfig
│ ├── Microsoft.Extensions.Logging.LoggerMessage
│ │ ├── DiagnosticDescriptorHelper.cs
│ │ ├── DiagnosticDescriptors.cs
│ │ ├── GetBestTypeByMetadataName.cs
│ │ ├── LogMessageGenerator.Emitter.cs
│ │ ├── LogMessageGenerator.Parser.cs
│ │ ├── SR.cs
│ │ └── Source.md
│ └── Microsoft.Extensions.Telemetry.LoggerMessage
│ │ ├── Emission
│ │ ├── Emitter.Method.cs
│ │ ├── Emitter.Utils.cs
│ │ ├── Emitter.cs
│ │ └── StringBuilderPool.cs
│ │ ├── LogPropertiesCheck.cs
│ │ ├── Model
│ │ ├── LoggingMethod.cs
│ │ ├── LoggingMethodParameter.cs
│ │ ├── LoggingMethodParameterExtensions.cs
│ │ ├── LoggingProperty.cs
│ │ ├── LoggingType.cs
│ │ └── TagProvider.cs
│ │ ├── Parsing
│ │ ├── AttributeProcessors.cs
│ │ ├── DiagDescriptors.cs
│ │ ├── Parser.LogProperties.cs
│ │ ├── Parser.Records.cs
│ │ ├── Parser.TagProvider.cs
│ │ ├── Parser.cs
│ │ ├── Resources.Designer.cs
│ │ ├── Resources.resx
│ │ ├── SymbolHolder.cs
│ │ ├── SymbolLoader.cs
│ │ ├── TemplateProcessor.cs
│ │ └── TypeSymbolExtensions.cs
│ │ ├── Shared
│ │ ├── ClassDeclarationSyntaxReceiver.cs
│ │ ├── DiagDescriptorsBase.cs
│ │ ├── DiagnosticIds.cs
│ │ ├── EmitterBase.cs
│ │ ├── GeneratorUtilities.cs
│ │ ├── ParserUtilities.cs
│ │ ├── RoslynExtensions.cs
│ │ ├── StringBuilderPool.cs
│ │ ├── SymbolHelpers.cs
│ │ └── TypeDeclarationSyntaxReceiver.cs
│ │ └── Source.md
│ ├── LegacySupport
│ └── IsExternalInit
│ │ └── IsExternalInit.cs
│ ├── Mappers
│ └── CallLocationMapper.cs
│ ├── Models
│ ├── CallLocation.cs
│ ├── CallParameter.cs
│ ├── CallParameterType.cs
│ ├── LogMessageCall.cs
│ └── LoggerScopeCall.cs
│ ├── PostProcessing
│ └── LoggerMessageResultAdjuster.cs
│ ├── Properties
│ └── launchSettings.json
│ ├── ReferenceAnalyzer
│ ├── IncrementalGeneratorInitializationContextExtensions.cs
│ ├── MetadataReferenceExtensions.cs
│ └── Reference.cs
│ ├── Utilities
│ ├── IdentifierHelper.cs
│ ├── ReservedParameterNameResolver.cs
│ └── TypeAccessibilityChecker.cs
│ └── VirtualLoggerMessage
│ ├── LogMessageCallLocationMap.cs
│ ├── VirtualLoggerMessageClassBuilder.cs
│ └── VirtualMembersInjector.cs
└── tests
├── AutoLoggerMessageGenerator.IntegrationTests
├── AutoLoggerMessageGenerator.IntegrationTests.csproj
├── BeginScopeWithAllParameterRangeTests.cs
├── DispatchProxyExecutionVerificationDecorator.cs
├── LogPropertiesAttributeTests.cs
└── LogWithAllParameterRangeTests.cs
└── AutoLoggerMessageGenerator.UnitTests
├── AutoLoggerMessageGenerator.UnitTests.Build.targets
├── AutoLoggerMessageGenerator.UnitTests.Roslyn4_11.csproj
├── AutoLoggerMessageGenerator.UnitTests.Roslyn4_8.csproj
├── BaseSourceGeneratorTest.cs
├── Caching
└── LogCallInputSourceComparerTests.cs
├── Emitters
├── GenericLoggerExtensionsEmitterTests.Emit_ShouldGenerateValidLoggingExtensionsOverrides.verified.txt
├── GenericLoggerScopeExtensionsEmitterTests.Emit_ShouldGenerateValidLoggingScopeExtensionsOverrides.verified.txt
├── InterceptorAttributeEmitterTests.Emit_ShouldGenerateValidInterceptorAttribute_HashBasedInterceptor.verified.txt
├── InterceptorAttributeEmitterTests.Emit_ShouldGenerateValidInterceptorAttribute_PathBasedInterceptor.verified.txt
├── InterceptorAttributeEmitterTests.cs
├── LoggerExtensionsEmitterTests.Emit_ShouldGenerateValidLoggingExtensionsAttribute.verified.txt
├── LoggerExtensionsEmitterTests.cs
├── LoggerInterceptorsEmitterTests.Emit_ShouldGenerateValidLoggingExtensionsAttribute.verified.txt
├── LoggerInterceptorsEmitterTests.cs
├── LoggerScopeExtensionsEmitterTests.cs
├── LoggerScopeInterceptorsEmitterTests.Emit_WithGivenConfiguration_ShouldGenerateValidLoggerScopeInterceptors.verified.txt
├── LoggerScopeInterceptorsEmitterTests.cs
├── LoggerScopesEmitterTests.Emit_WithGivenConfiguration_ShouldGenerateValidLoggerDefineScopedFunctors.verified.txt
└── LoggerScopesEmitterTests.cs
├── Extractors
├── CallParametersExtractorTests.cs
├── EnclosingClassExtractorTests.cs
├── LogCallExtractorTests.Extract_WithLogMethodInvocationCode_ShouldTransformThemIntoLogCallObject_description=with parameters_sourceCode=HashBasedInterceptor.verified.txt
├── LogCallExtractorTests.Extract_WithLogMethodInvocationCode_ShouldTransformThemIntoLogCallObject_description=with parameters_sourceCode=PathBasedInterceptor.verified.txt
├── LogCallExtractorTests.Extract_WithLogMethodInvocationCode_ShouldTransformThemIntoLogCallObject_description=without parameters_sourceCode=HashBasedInterceptor.verified.txt
├── LogCallExtractorTests.Extract_WithLogMethodInvocationCode_ShouldTransformThemIntoLogCallObject_description=without parameters_sourceCode=PathBasedInterceptor.verified.txt
├── LogCallExtractorTests.cs
├── LogLevelExtractorTests.cs
├── LoggerScopeCallExtractorTests.Extract_WithGivenLoggerScope_ShouldTransformIntoLoggerScopeCallObject_description=with parameters_sourceCode=HashBasedInterceptor.verified.txt
├── LoggerScopeCallExtractorTests.Extract_WithGivenLoggerScope_ShouldTransformIntoLoggerScopeCallObject_description=with parameters_sourceCode=PathBasedInterceptor.verified.txt
├── LoggerScopeCallExtractorTests.cs
├── MessageParameterNamesExtractorTests.cs
└── MessageParameterTextExtractorTests.cs
├── Filters
├── LogMessageCallFilterTests.cs
└── LoggerScopeFilterTests.cs
├── MethodSpecificityRules
└── InstanceCallVsExtensionCallTests.cs
├── Scrubbers
└── GeneratedCodeAttributeScrubber.cs
├── Utilities
├── IdentifierHelperTests.cs
├── InterceptorConfigurationUtilities.cs
├── MockLogCallLocationBuilder.cs
└── ReservedParameterNameResolverTests.cs
└── VirtualLoggerMessage
├── LogCallExtractorTests.cs
├── VirtualLoggerMessageClassBuilderTests.Build_WithDifferentConfiguration_ShouldReturnLegitLoggerMessageDeclaration_all_disabled.verified.txt
├── VirtualLoggerMessageClassBuilderTests.Build_WithDifferentConfiguration_ShouldReturnLegitLoggerMessageDeclaration_all_enabled.verified.txt
├── VirtualLoggerMessageClassBuilderTests.Build_WithDifferentConfiguration_ShouldReturnLegitLoggerMessageDeclaration_only_telemetry_enabled.verified.txt
├── VirtualLoggerMessageClassBuilderTests.Build_WithDifferentConfiguration_ShouldReturnLegitLoggerMessageDeclaration_telemetry_disabled.verified.txt
├── VirtualLoggerMessageClassBuilderTests.Build_WithEscapeSequences_ShouldBuildAsItIs.verified.txt
└── VirtualLoggerMessageClassBuilderTests.cs
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 |
5 | indent_size = 4
6 | indent_style = space
7 | tab_width = 4
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.{appxmanifest,axml,build,config,proj,csproj,dbml,discomap,dtd,json,jsproj,lsproj,njsproj,nuspec,proj,props,resjson,resw,resx,StyleCop,targets,tasks,vbproj,yml,xml,xsd}]
12 | indent_style = space
13 | indent_size = 2
14 | tab_width = 2
15 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | patreon: stbychkov
2 | ko_fi: stbychkov
3 | buy_me_a_coffee: stbychkov
4 | thanks_dev: stbychkov
5 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/01_bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: Create a report to help us improve
3 | labels: ["untriaged", "bug"]
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | We welcome bug reports! Please see our [contribution guidelines](https://github.com/dotnet/stbychkov/AutoLoggerMessage/blob/main/CONTRIBUTING.md#writing-a-good-bug-report) for more information on writing a good bug report. This template will help us gather the information we need to start the triage process.
9 | - type: textarea
10 | id: background
11 | attributes:
12 | label: Description
13 | description: Please share a clear and concise description of the problem.
14 | placeholder: Description
15 | validations:
16 | required: true
17 | - type: textarea
18 | id: repro-steps
19 | attributes:
20 | label: Reproduction Steps
21 | description: |
22 | Please include minimal steps to reproduce the problem, if possible. E.g.: the smallest possible code snippet; or a small project, with steps to run it. If possible include text as text rather than screenshots (so it shows up in searches).
23 | placeholder: Minimal Reproduction
24 | validations:
25 | required: true
26 | - type: textarea
27 | id: expected-behavior
28 | attributes:
29 | label: Expected behavior
30 | description: |
31 | Provide a description of the expected behavior.
32 | placeholder: Expected behavior
33 | validations:
34 | required: true
35 | - type: textarea
36 | id: actual-behavior
37 | attributes:
38 | label: Actual behavior
39 | description: |
40 | Provide a description of the actual behavior observed. If applicable please include any error messages, exception stacktraces or memory dumps.
41 | placeholder: Actual behavior
42 | validations:
43 | required: true
44 | - type: textarea
45 | id: regression
46 | attributes:
47 | label: Regression?
48 | description: |
49 | Did this work in a previous build or release? If you can try a previous release or build to find out, that can help us narrow down the problem. If you don't know, that's OK.
50 | placeholder: Regression?
51 | validations:
52 | required: false
53 | - type: textarea
54 | id: known-workarounds
55 | attributes:
56 | label: Known Workarounds
57 | description: |
58 | Please provide a description of any known workarounds.
59 | placeholder: Known Workarounds
60 | validations:
61 | required: false
62 | - type: textarea
63 | id: configuration
64 | attributes:
65 | label: Configuration
66 | description: |
67 | Please provide more information on your .NET configuration:
68 | * Which version of .NET is the code running on? E.g., '7.0 Preview1', or daily build number, use `dotnet --info`.
69 | * Do you know whether it is specific to that configuration?
70 | placeholder: Configuration
71 | validations:
72 | required: false
73 | - type: textarea
74 | id: other-info
75 | attributes:
76 | label: Other information
77 | description: |
78 | If you have an idea where the problem might lie, let us know that here. Please include any pointers to code, relevant changes, or related issues you know of.
79 | placeholder: Other information
80 | validations:
81 | required: false
82 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/02_feature_proposal.yml:
--------------------------------------------------------------------------------
1 | name: API Suggestion
2 | description: Propose a feature request
3 | title: "[Feature Proposal]: "
4 | labels: ["untriaged", "enhancement"]
5 | body:
6 | - type: textarea
7 | id: background
8 | attributes:
9 | label: Background and motivation
10 | description: Please describe the purpose and value of the new change here.
11 | placeholder: Purpose
12 | validations:
13 | required: true
14 | - type: textarea
15 | id: feature-proposal
16 | attributes:
17 | label: Feature Proposal
18 | description: |
19 | Please provide the details about the feature changes that you are proposing.
20 | placeholder: API declaration (no method bodies)
21 | validations:
22 | required: true
23 | - type: textarea
24 | id: feature-usage
25 | attributes:
26 | label: Feature Usage
27 | description: |
28 | Please provide code examples that highlight how the proposed feature change are meant to be consumed.
29 | placeholder: API usage
30 | validations:
31 | required: true
32 | - type: textarea
33 | id: risks
34 | attributes:
35 | label: Risks
36 | description: |
37 | Please mention any risks that to your knowledge the change proposal might entail, such as breaking changes, performance regressions, etc.
38 | placeholder: Risks
39 | validations:
40 | required: false
41 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/03_blank_issue.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Blank issue
3 | about: Something that doesn't fit the other categories
4 | title: ''
5 | labels: 'untriaged'
6 | assignees: ''
7 |
8 | ---
9 |
--------------------------------------------------------------------------------
/.github/workflows/dotnet_build.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a .NET project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
3 |
4 | name: .NET
5 |
6 | on:
7 | push:
8 | branches: [ "main" ]
9 | pull_request:
10 | branches: [ "main" ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 | - name: Setup .NET
20 | uses: actions/setup-dotnet@v4
21 | with:
22 | dotnet-version: 9.0.x
23 | - name: Restore dependencies
24 | run: dotnet restore AutoLoggerMessage.slnx
25 | - name: Build
26 | run: dotnet build --no-restore AutoLoggerMessage.slnx
27 | - name: Test
28 | run: dotnet test --no-build --verbosity normal AutoLoggerMessage.slnx
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | bld/
21 | [Bb]in/
22 | [Oo]bj/
23 | [Ll]og/
24 |
25 | # MSTest test Results
26 | [Tt]est[Rr]esult*/
27 | [Bb]uild[Ll]og.*
28 |
29 | # NUNIT
30 | *.VisualState.xml
31 | TestResult.xml
32 |
33 | # Build Results of an ATL Project
34 | [Dd]ebugPS/
35 | [Rr]eleasePS/
36 | dlldata.c
37 |
38 | # DotCover is a Code Coverage Tool
39 | *.dotCover
40 |
41 | # NuGet Packages
42 | *.nupkg
43 | # The packages folder can be ignored because of Package Restore
44 | **/packages/*
45 | # except build/, which is used as an MSBuild target.
46 | !**/packages/build/
47 | # Uncomment if necessary however generally it will be regenerated when needed
48 | #!**/packages/repositories.config
49 | # NuGet v3's project.json files produces more ignoreable files
50 | *.nuget.props
51 | *.nuget.targets
52 |
53 | # Others
54 | .DS_Store
55 | ClientBin/
56 | ~$*
57 | *~
58 | *.dbmdl
59 | *.dbproj.schemaview
60 | *.jfm
61 | *.pfx
62 | *.publishsettings
63 | node_modules/
64 | package-lock.json
65 | orleans.codegen.cs
66 | .DS_Store
67 |
68 |
69 | # JetBrains Rider
70 | .idea/
71 | *.sln.iml
72 | BenchmarkDotNet.Artifacts/
73 |
74 | # Test snapshots
75 | **/*.received.*
76 |
77 | artifacts/
78 |
--------------------------------------------------------------------------------
/AutoLoggerMessage.sln.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | True
3 | True
4 | True
5 | True
6 | True
7 | True
8 | True
9 | True
10 | True
11 | True
12 |
--------------------------------------------------------------------------------
/AutoLoggerMessage.slnx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community.
4 |
5 | For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct).
6 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | enable
4 | latest
5 | enable
6 | $(InterceptorsPreviewNamespaces);Microsoft.Extensions.Logging.AutoLoggerMessage
7 | true
8 | False
9 | $(MSBuildThisFileDirectory)artifacts
10 | false
11 | true
12 | true
13 |
14 |
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 stbychkov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/benchmarks/AutoLoggerMessageGenerator.Benchmarks.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net8.0
4 | Exe
5 |
6 |
7 |
8 | AnyCPU
9 | pdbonly
10 | false
11 | true
12 | true
13 | Release
14 | false
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | PreserveNewest
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/benchmarks/BenchmarkFiles/CustomNullLogger.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Logging;
2 |
3 | public class CustomNullLogger : ILogger
4 | {
5 | private int _counter = 0;
6 | public static readonly CustomNullLogger Instance = new();
7 |
8 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
9 | {
10 | _counter++;
11 | }
12 |
13 | public bool IsEnabled(LogLevel logLevel) => true;
14 |
15 | public IDisposable BeginScope(TState state) where TState : notnull =>
16 | NullScope.Instance;
17 |
18 | private sealed class NullScope : IDisposable
19 | {
20 | public static NullScope Instance { get; } = new();
21 |
22 | public void Dispose() { }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/benchmarks/CommandRunner.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 |
3 | namespace AutoLoggerMessageGenerator.Benchmarks;
4 |
5 | internal class CommandRunner(ProcessPriorityClass priorityClass)
6 | {
7 | public async Task RunAsync(string command, string? args = null, CancellationToken cancellationToken = default)
8 | {
9 | var process = new Process();
10 |
11 | process.StartInfo = new ProcessStartInfo(command, args ?? string.Empty)
12 | {
13 | UseShellExecute = false,
14 | CreateNoWindow = true,
15 | };
16 |
17 | try
18 | {
19 | process.PriorityClass = priorityClass;
20 | }
21 | catch (Exception)
22 | {
23 | Console.WriteLine("Failed to set priority class");
24 | }
25 | process.EnableRaisingEvents = true;
26 |
27 | process.Start();
28 | await process.WaitForExitAsync(cancellationToken);
29 |
30 | if (process.ExitCode != 0)
31 | throw new InvalidOperationException("Command execution failed. Check execution logs for details");
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/benchmarks/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | enable
4 | latest
5 | enable
6 |
7 |
8 |
--------------------------------------------------------------------------------
/benchmarks/Directory.Packages.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | true
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/benchmarks/MultiBenchmarkRunner.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using BenchmarkDotNet.Attributes;
3 | using BenchmarkDotNet.Configs;
4 | using BenchmarkDotNet.Exporters;
5 | using BenchmarkDotNet.Order;
6 | using BenchmarkDotNet.Running;
7 |
8 | namespace AutoLoggerMessageGenerator.Benchmarks;
9 |
10 | internal class MultiBenchmarkRunner(ProjectConfiguration[] projectConfigurations, string[]? args = null)
11 | {
12 | private static readonly ManualConfig RunConfiguration = ManualConfig.CreateMinimumViable()
13 | .WithOptions(ConfigOptions.JoinSummary)
14 | .WithOptions(ConfigOptions.DisableLogFile)
15 | .AddColumn(new ProjectConfigurationColumn())
16 | .WithOrderer(new DefaultOrderer(SummaryOrderPolicy.Declared))
17 | .AddExporter(MarkdownExporter.GitHub);
18 |
19 | public async Task Run()
20 | {
21 | var buildResults = await BuildProjects();
22 | var types = FindAllBenchmarks(buildResults);
23 |
24 | BenchmarkRunner.Run(types, RunConfiguration, args);
25 |
26 | CleanupGeneratedProjects(buildResults);
27 | }
28 |
29 | private async Task> BuildProjects()
30 | {
31 | if (!projectConfigurations.Any())
32 | throw new InvalidOperationException("No project configurations provided");
33 |
34 | var buildResults = new LinkedList();
35 | foreach (var projectConfiguration in projectConfigurations.AsParallel())
36 | {
37 | var buildResult = await new ProjectBuilder(projectConfiguration).BuildAsync().ConfigureAwait(false);
38 | buildResults.AddLast(buildResult);
39 | }
40 |
41 | return buildResults;
42 | }
43 |
44 | private static TypeInfo[] FindAllBenchmarks(LinkedList buildResults)
45 | {
46 | var types = buildResults.Select(b => Assembly.LoadFrom(b.ExecutablePath))
47 | .SelectMany(c => c.DefinedTypes)
48 | .Where(c => c.DeclaredMethods.Any(v => v.GetCustomAttribute(typeof(BenchmarkAttribute)) is not null))
49 | .ToArray();
50 | return types;
51 | }
52 |
53 | private static void CleanupGeneratedProjects(LinkedList buildResults)
54 | {
55 | foreach (var buildResult in buildResults)
56 | Directory.Delete(buildResult.ProjectDirectory, recursive: true);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/benchmarks/PackagesProvider.cs:
--------------------------------------------------------------------------------
1 | namespace AutoLoggerMessageGenerator.Benchmarks;
2 |
3 | internal static class PackagesProvider
4 | {
5 | public const string BenchmarkPackage = """""";
6 | public const string MicrosoftExtensionsLoggingPackage = """""";
7 | public const string MicrosoftExtensionsTelemetryPackage = """""";
8 | public const string AutoLoggerMessageBuildOutput = """""";
9 | public const string AutoLoggerMessagePackage = """""";
10 | }
11 |
--------------------------------------------------------------------------------
/benchmarks/Program.cs:
--------------------------------------------------------------------------------
1 | using AutoLoggerMessageGenerator.Benchmarks;
2 |
3 | ProjectConfiguration[] projectConfigurations =
4 | [
5 | new()
6 | {
7 | Name = "MsExtensionLoggingConfiguration",
8 | References = new HashSet
9 | {
10 | PackagesProvider.BenchmarkPackage,
11 | PackagesProvider.MicrosoftExtensionsLoggingPackage
12 | }
13 | },
14 | new()
15 | {
16 | Name = "MsExtensionsTelemetryConfiguration",
17 | References = new HashSet
18 | {
19 | PackagesProvider.BenchmarkPackage,
20 | PackagesProvider.MicrosoftExtensionsLoggingPackage,
21 | PackagesProvider.MicrosoftExtensionsTelemetryPackage
22 | }
23 | },
24 | new()
25 | {
26 | Name = "AutoLoggerMessageGeneratorConfiguration",
27 | References = new HashSet
28 | {
29 | PackagesProvider.BenchmarkPackage,
30 | PackagesProvider.MicrosoftExtensionsLoggingPackage,
31 | PackagesProvider.AutoLoggerMessageBuildOutput,
32 | PackagesProvider.AutoLoggerMessagePackage
33 | }
34 | },
35 | new()
36 | {
37 | Name = "AutoLoggerMessageGeneratorWithTelemetryConfiguration",
38 | References = new HashSet
39 | {
40 | PackagesProvider.BenchmarkPackage,
41 | PackagesProvider.AutoLoggerMessagePackage,
42 | PackagesProvider.AutoLoggerMessageBuildOutput,
43 | PackagesProvider.MicrosoftExtensionsLoggingPackage,
44 | PackagesProvider.MicrosoftExtensionsTelemetryPackage,
45 | }
46 | }
47 | ];
48 |
49 | var runner = new MultiBenchmarkRunner(projectConfigurations);
50 | await runner.Run();
51 |
--------------------------------------------------------------------------------
/benchmarks/ProjectConfiguration.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Frozen;
2 |
3 | namespace AutoLoggerMessageGenerator.Benchmarks;
4 |
5 | internal record ProjectConfiguration
6 | {
7 | public required string Name { get; init; }
8 |
9 | public IReadOnlySet References { get; init; } = FrozenSet.Empty;
10 | }
11 |
--------------------------------------------------------------------------------
/benchmarks/ProjectConfigurationColumn.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Columns;
2 | using BenchmarkDotNet.Reports;
3 | using BenchmarkDotNet.Running;
4 |
5 | namespace AutoLoggerMessageGenerator.Benchmarks;
6 |
7 | public class ProjectConfigurationColumn : IColumn
8 | {
9 | public string Id => "ProjectConfiguration";
10 |
11 | public string ColumnName => "ProjectConfiguration";
12 |
13 | public bool AlwaysShow => true;
14 |
15 | public ColumnCategory Category => ColumnCategory.Job;
16 |
17 | public int PriorityInCategory => int.MaxValue;
18 |
19 | public bool IsNumeric => false;
20 |
21 | public UnitType UnitType => UnitType.Dimensionless;
22 |
23 | public string Legend => "The project configuration used to run the benchmark";
24 |
25 | public string GetValue(Summary summary, BenchmarkCase benchmarkCase) => benchmarkCase.Descriptor.Type.Assembly.GetName().Name!;
26 |
27 | public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style) => GetValue(summary, benchmarkCase);
28 |
29 | public bool IsAvailable(Summary summary) => true;
30 |
31 | public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) => false;
32 | }
33 |
--------------------------------------------------------------------------------
/benchmarks/TargetFrameworkMonikerDetector.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.Versioning;
3 |
4 | namespace AutoLoggerMessageGenerator.Benchmarks;
5 |
6 | public static class TargetFrameworkMonikerDetector
7 | {
8 | public static string Detect()
9 | {
10 | var frameworkName = Assembly.GetEntryAssembly()?
11 | .GetCustomAttribute()?
12 | .FrameworkName;
13 |
14 | if (string.IsNullOrEmpty(frameworkName))
15 | throw new InvalidOperationException("Failed to detect target framework moniker");
16 |
17 | return frameworkName switch
18 | {
19 | ".NETFramework,Version=v4.6.1" => "net461",
20 | ".NETFramework,Version=v4.6.2" => "net462",
21 | ".NETFramework,Version=v4.7" => "net47",
22 | ".NETFramework,Version=v4.7.1" => "net471",
23 | ".NETFramework,Version=v4.7.2" => "net472",
24 | ".NETFramework,Version=v4.8" => "net48",
25 | ".NETFramework,Version=v4.8.1" => "net481",
26 | ".NETCoreApp,Version=v2.0" => "netcoreapp2.0",
27 | ".NETCoreApp,Version=v2.1" => "netcoreapp2.1",
28 | ".NETCoreApp,Version=v2.2" => "netcoreapp2.2",
29 | ".NETCoreApp,Version=v3.0" => "netcoreapp3.0",
30 | ".NETCoreApp,Version=v3.1" => "netcoreapp3.1",
31 | ".NETCoreApp,Version=v5.0" => "net5.0",
32 | ".NETCoreApp,Version=v6.0" => "net6.0",
33 | ".NETCoreApp,Version=v7.0" => "net7.0",
34 | ".NETCoreApp,Version=v8.0" => "net8.0",
35 | ".NETCoreApp,Version=v9.0" => "net9.0",
36 | _ when frameworkName.StartsWith(".NETFramework") =>
37 | frameworkName.Replace(".NETFramework,Version=v", string.Empty).Replace(".", string.Empty),
38 | _ when frameworkName.StartsWith(".NETCoreApp") =>
39 | frameworkName.Replace(".NETCoreApp,Version=v", string.Empty),
40 | _ => throw new NotSupportedException($"Unsupported framework: {frameworkName}")
41 | };
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/docs/ADR/ADR-01_Generation_of_logger_extension_methods_overloads.md:
--------------------------------------------------------------------------------
1 | ### Title: ADR-01 Generation of LoggerMessage extension methods overloads
2 | ### Status: Accepted
3 | ### Context:
4 |
5 | In order to improve performance and avoid boxing of primitive types during logging, we need to generate generic overloads for logger methods.
6 | The challenge here is making sure these overloads are applied everywhere across the solution automatically while maintaining control over when and how they are generated.
7 |
8 | I initially considered generating these overloads during the PostOutputInitializationStep, but at that point, I don't have access to the configuration.
9 | This would make it impossible to disable or selectively generate the overloads, and if I always generated them, it would result in too many methods being created for every assembly using the source generator.
10 |
11 | I also can't generate the overloads during the source output generation stage because I need them to be available before the source generator begins processing the code.
12 |
13 | Given these constraints, the best solution is to generate these overloads in advance and add them to the project as a public class.
14 | By doing so, the overloads will be applied **everywhere** within the source generator assembly through parameter specificity, without generating unnecessary methods for each target assembly.
15 |
16 | ### Decision:
17 |
18 | To avoid boxing primitive types and ensure proper overload application across the entire solution, I will:
19 |
20 | * Generate the generic overloads in advance and place them in a public class within the project.
21 | * This ensures that the overloads are globally accessible across the source generator assembly, applying automatically due to parameter specificity.
22 | * The overloads will not be generated in every assembly, reducing unnecessary method generation.
23 |
24 | By placing these overloads in a public class and generating them up front, I can guarantee that they are available before the source generator begins processing, without introducing excess methods in each assembly or having to manage complex configurations.
25 |
26 | ### Consequences:
27 |
28 | * **Long-term**:
29 | * This extension methods will appear in the suggestion list for the logger instance, which might be confusing for the user.
30 | However, the user might hide them by `Filter members by [EditorBrowsable] attribute` setting in Rider. It's hidden by default in Visual Studio.
31 | * This approach adds additional 196 extension methods that ensure the overloads will replace standard calls everywhere thanks to parameter specificity.
32 | * **Risks**:
33 | * Need to check if unused extension methods can be trimmed in this scenario for AOT.
34 | * To compile source generator, ILogger class has to be resolved, which require us to add this library as a reference, so there might be some problems with package version.
35 |
36 | ### Alternatives Considered
37 |
38 | * **Generate overloads during PostOutputInitializationStep**: This would allow flexibility in enabling or disabling the overloads at a later stage.
39 | However, it’s not possible because I don’t have access to configuration during that step, meaning I can’t control whether to generate the overloads or not.
40 |
41 | * **Generate overloads during Source Output Generation Stage**: This would only work if the overloads were available before the source generator starts processing.
42 | However, I need these overloads to be available upfront, making this approach impractical.
43 |
44 |
--------------------------------------------------------------------------------
/docs/ADR/ADR-02_Reusing_the_existing_loggermessage_generators.md:
--------------------------------------------------------------------------------
1 | ### Title: ADR-02 Reusing the Existing LoggerMessage Generators
2 | ### Status: Accepted
3 | ### Context:
4 |
5 | The goal here is to automatically generate `LoggerMessage` methods for efficient logging, but without reinventing the wheel.
6 | I would love to chain source generators, but here is a catch: they cannot be chained.
7 | > Run un-ordered, each generator will see the same input compilation, with no access to files created by other source generators.
8 |
9 | This means I can't just hook my generator into the existing one seamlessly.
10 | Also, I need to have access to internal classes and some parts of the original generator that aren't exposed publicly.
11 | So, in order to make everything work, I had to copy the generator code into my solution and leave it untouched. This way, I can take advantage of its functionality but without the risk of breaking anything when updates happen.
12 |
13 | While this approach isn't the most efficient or ideal, it allows for easier maintenance and future updates since I won't have to worry about keeping track of changes in the original source code.
14 |
15 | ### Decision:
16 |
17 | Instead of modifying the existing `LoggerMessage` generator, I copied the generator into my solution and left it unchanged. This decision allows me to:
18 |
19 | * Reuse the existing functionality without modifying the original code, ensuring future updates are easy to integrate.
20 | * Work around the limitation that source generators can't be chained by duplicating the generator code into my own solution.
21 | * Ensure that any updates to the original `LoggerMessage` generator can be applied by simply replacing or merging the copied code, without worrying about custom modifications.
22 |
23 | While it’s a bit of extra work up front, this approach ensures that the source generator can evolve with future updates, without the need for extensive rework.
24 |
25 | ### Consequences:
26 |
27 | * **Short-term**: I have to duplicate the existing source generator code, which is less efficient than reusing it directly.
28 | * **Long-term**: The solution is more maintainable because it avoids direct modifications to the original `LoggerMessage` generator.
29 | Any updates to the generator can be applied by simply replacing the copied code with newer versions, making future updates less painful.
30 | * **Risks**: If the existing generator undergoes major changes, I may have to adapt my solution to fit.
31 | But since I’m using the original code as-is, the risk of breaking changes is minimized.
32 |
33 | ### Alternatives Considered
34 |
35 | Create and modify my own `LoggerMessage` generator. It gives me full control which is good, but I'm lazy for maintaining that solution, so building on top of that sounds more easier.
36 |
--------------------------------------------------------------------------------
/docs/ADR/ADR-03_Supporting_both_old_and_new_loggermessage_generators.md:
--------------------------------------------------------------------------------
1 | ### Title: ADR-03 Supporting Both Old and New LoggerMessage Generators
2 | ### Status: Accepted
3 | ### Context:
4 |
5 | The `LoggerMessage` generators in the `Microsoft.Extensions.Logging.Abstractions` package have been around for a while,
6 | but with the introduction of the new `Microsoft.Extensions.Telemetry.Abstractions` package, there's a shift toward a more
7 | efficent approach to logging. Since it's unclear how many people are using the new logging capabilities, it seems reasonable to support both the old and the new generators to ensure broad compatibility.
8 |
9 | Initially, I considered creating two separate versions of the source generator: one for the old logging package and one for the new one.
10 | However, this would complicate the setup process for users. Instead, I decided to keep things simple and automatically detect which logging package is being used in the project.
11 | By analyzing the project’s dependencies, I can determine whether the new logging capabilities are available and adjust the behavior of the source generator accordingly.
12 | This approach works seamlessly and simplifies the setup for the user.
13 |
14 | ### Decision:
15 |
16 | Instead of creating separate versions of the source generator for the old and new LoggerMessage generators, I will:
17 |
18 | * Analyze the project dependencies during the source generation process.
19 | * Detect whether the new Microsoft.Extensions.Telemetry.Abstractions package is available.
20 | * Automatically adjust the behavior of the source generator based on the presence of the new package, falling back to the old Microsoft.Extensions.Logging.Abstractions generator if the new one isn’t found.
21 |
22 | This way, users don’t need to worry about configuring which version of the generator they need — the system will automatically choose the right one based on their project setup.
23 |
24 | ### Consequences:
25 |
26 | * **Short-term**: The source generator will analyze project dependencies, which adds a bit of logic to the setup process.
27 | However, it greatly simplifies the user experience by removing the need for manual configuration.
28 | * **Long-term**: Users will benefit from an automatic detection approach that ensures compatibility with both old and new logging generators, without requiring them to manually choose which version to use.
29 | * **Risks**: There’s a small risk that the automatic detection might not work perfectly in all edge cases (for example, if dependencies are managed in unusual ways).
30 | However, this is minimal, and the fallback to the old logging package ensures the system is still functional in those cases.
31 | * **Maintenance**: If future versions of the logging packages introduce breaking changes or new features, the automatic detection logic might need to be updated.
32 | But this is a manageable task that keeps the solution flexible without requiring multiple versions of the source generator.
33 |
34 | ### Alternatives Considered
35 |
36 | * **Create Separate Versions of the Source Generator**: I could create two different versions of the generator — one for each logging package. However, this would complicate the setup and configuration for users, leading to a more cumbersome experience.
37 | * **Require Manual Configuration by the User**: Another option was to require users to specify whether they were using the old or new logging package through configuration. However, this increases the setup complexity and can lead to errors, so it was rejected in favor of automatic detection.
38 |
39 |
--------------------------------------------------------------------------------
/docs/ADR/ADR-04_Generation_of_loggermessage_methods.md:
--------------------------------------------------------------------------------
1 | ### Title: ADR-04 Generation of `LoggerMessage` methods
2 | ### Status: Accepted
3 | ### Context:
4 |
5 | The existing `LoggerMessage` generators rely on the attribute that needs to be applied to methods in the code to trigger the code generation.
6 | However, this attribute-based approach is designed to work on code that will eventually be compiled into the output.
7 | In my case, I need to trigger the code generator to create the necessary `LoggerMessage` methods, but I don’t want to include this code in the final output.
8 |
9 | After evaluating the options, I decided to create a virtual temporary class that exists only in the readonly snapshot of the compilation process.
10 | This class is not included in the final output, but it will be enough to trigger the existing `LoggerMessage` source generator and ensure the necessary methods are generated.
11 |
12 | The process involves automatically transforming the log calls in the code into virtual methods with the `LoggerMessage` attribute.
13 | These virtual methods will exist solely to trigger the code generation.
14 | The one challenge with this approach is that the existing source generator generates methods as partial.
15 | Since the temporary virtual class doesn't exist in the output, the generated partial declaration would be invalid.
16 | To fix this, I will need to adjust the code during the post-processing stage by removing the partial keyword from the final result.
17 |
18 | ### Decision:
19 |
20 | To trigger the `LoggerMessage` source generator without modifying the existing generators:
21 |
22 | * I will create a virtual temporary class that only exists in the readonly snapshot of the compilation process.
23 | * This temporary class will contain virtual methods decorated with the `LoggerMessage` attribute, which will be enough to trigger the source generator.
24 |
25 | Since the `LoggerMessage` generator creates methods as partial, and the temporary class won’t be output, I will remove the partial keyword in the post-processing stage to ensure valid method declarations.
26 |
27 | ### Consequences:
28 |
29 | * **Short-term**: This approach requires the introduction of a virtual class that will only exist during compilation.
30 | The post-processing stage will also be responsible for removing the partial keyword, which introduces some additional complexity to the process.
31 | * **Long-term**: The benefit of this approach is that it allows me to trigger the `LoggerMessage` generator without adding unnecessary code to the final output.
32 | This also maintains compatibility with the existing logging system without requiring major changes.
33 | * **Risks**: The main risk is that the post-processing step to remove the partial keyword introduces an extra stage that could introduce bugs if not handled carefully.
34 | However, this risk is manageable and should be relatively easy to fix in case of issues.
35 | * **Maintenance**: This approach allows for future updates to the source generator while keeping the solution flexible.
36 | The only maintenance required would be to ensure the post-processing step works correctly if the underlying generator is updated.
37 |
38 | ### Alternatives Considered
39 |
40 | * **Adjust the Existing LoggerMessage Generators**: I could have modified the existing `LoggerMessage` generators to accommodate my needs.
41 | [Here](./ADR-01_Reusing_the_existing_loggermessage_generators.md) is why I don't consider it.
42 |
--------------------------------------------------------------------------------
/docs/ADR/ADR-05_Backtracking_diagnostic_location.md:
--------------------------------------------------------------------------------
1 | ### Title: ADR-05 Backtracking diagnostic location
2 | ### Status: Accepted
3 | ### Context:
4 |
5 | As part of the solution, I create virtual classes ([why?](./ADR-04_Generation_of_loggermessage_methods.md)) that only exist in the readonly snapshot of the compilation process to trigger the `LoggerMessage` source generators.
6 | These virtual classes will generate diagnostic reports, but the diagnostics will be tied to a class that doesn’t actually exist in the source code.
7 | This creates a situation where the diagnostics will be reported from the virtual class, making it difficult to trace errors back to the original Log* calls in the code, where the actual issue occurred.
8 |
9 | To address this, I need to backtrack the diagnostic location and map it back to the original Log* calls. This involves:
10 | * Catching all diagnostic errors from the source generator.
11 | * Rewriting the diagnostic location to point to the original location of the Log* call rather than the temporary virtual class.
12 |
13 | ### Decision:
14 |
15 | To backtrack the diagnostic location, I will:
16 |
17 | * Map the location of the original Log* calls to the corresponding generated method.
18 | * Add a 1-to-1 comment before each generated method that contains the mapped location of the original Log* call. This will serve as a reference for the location in case diagnostics need to be traced back.
19 | * When a diagnostic error is triggered, I will use this mapped location to rewrite the diagnostic location to the closest method where the error occurred.
20 | This ensures that the diagnostic reports are more useful and point to the actual source where the issue originated.
21 | * If this method of backtracking fails, I will fall back on setting the diagnostic location to an empty location, which is also acceptable but less helpful.
22 |
23 | ### Consequences:
24 |
25 | * **Short-term**: This approach requires adding comments to the generated methods, which introduces a bit of extra work. However, this ensures that the diagnostic reports are meaningful and point to the correct source location.
26 | * **Long-term**: The mapped location comments allow for better tracing of errors to the correct source code, making the debugging process easier and more accurate for users.
27 | * **Risks**: If the mapping doesn’t work as expected or the backtracking fails, it could result in diagnostics that are harder to interpret. The fallback of an empty location is acceptable, but not ideal. I'll need to ensure the backtracking logic is solid to avoid missing any errors.
28 | * **Maintenance**: This solution should work for future updates of the source generator, as long as the core logic of generating LoggerMessage methods and triggering diagnostics remains the same. Adjustments may be necessary if the structure of diagnostics changes.
29 |
30 | ### Alternatives Considered
31 |
32 | * **Ignore Diagnostic Location**: I could have simply ignored the location of diagnostics coming from the virtual classes and accepted the fact that the error reports would be disconnected from the original source code.
33 | This would have been simpler but would result in less useful diagnostics, making debugging more difficult.
34 | * **Create Real Classes Instead of Virtual Ones**: An alternative would be to create real classes instead of virtual ones, ensuring that the generated methods exist in the source code and diagnostics could be directly linked to them.
35 | Here is [why?](./ADR-04_Generation_of_loggermessage_methods.md) I don't consider it.
36 |
--------------------------------------------------------------------------------
/docs/ADR/ADR-06_Skip_log_calls_with_invalid_template_parameter_names.md:
--------------------------------------------------------------------------------
1 | ### Title: ADR-06 Skip log calls with invalid template parameter names
2 | ### Status: Accepted
3 | ### Context:
4 |
5 | Template message parameters are parsed to generate logging code dynamically.
6 | The template parameters' names are critical for synchronizing the log message format with the corresponding parameters in the code.
7 | If a parameter name is invalid, the generated code may produce unexpected results or fail to align with the user's intended behavior.
8 |
9 | ### Decision:
10 |
11 | I will not process log calls if the template message parameters contain invalid names.
12 | Instead, I will filter these calls and report a diagnostic warning indicating the invalid parameter name.
13 |
14 | ### Consequences:
15 |
16 | * **Positive**:
17 | * Users will be notified of issues in their template messages.
18 | * The generator remains simple and reliable, reducing potential bugs and unexpected behavior.
19 |
20 | * **Negative**:
21 | * Users must fix their invalid parameter names before the log calls are processed. This might require additional effort upfront but is offset by the long-term benefits of correctness.
22 |
23 | ### Alternatives Considered
24 |
25 | Automatically rename invalid parameter names to conform to a valid format (e.g., replacing spaces with underscores).
26 |
27 | **Rejected Because**:
28 | * It introduces unexpected behavior for the user.
29 | * It adds some complexity to the generator and increases the risk of synchronization (template parameters <-> template message) errors.
30 |
--------------------------------------------------------------------------------
/docs/ADR/ADR-07_Generation_of_BeginScope_methods.md:
--------------------------------------------------------------------------------
1 | ### Title: ADR-07 Generation of `BeginScope` methods
2 | ### Status: Accepted
3 |
4 | ### Context:
5 |
6 | The existing `ILogger.BeginScope` method has the same problems as the `ILogger.Log*` methods:
7 | 1. Lack of compile-time checks for mismatches between template parameters and the actual arguments, which can lead to runtime exceptions.
8 | 2. Performance overhead due to the need to create a new scope object every time.
9 | 3. Unnecessary allocations due to boxing of template parameters, even with identical arguments.
10 |
11 | `ILogger.BeginScope` is backed by `LoggerMessage.DefineScope`, which allows generation of strongly-typed, precompiled delegates with up to 6 parameters. This enables high-performance and allocation-free scope creation, similar to how `LoggerMessage.Define` is used for logging methods.
12 |
13 | The main difference is that `ILogger.BeginScope` **with only one parameter** is an instance method, not an extension method. This means we cannot intercept or replace calls that only use a single message argument (i.e., `BeginScope("Starting operation")`) because the instance method takes precedence over extension methods. Therefore, generation will be limited to `BeginScope` calls with one or more structured parameters (i.e., key-value pairs or anonymous objects), which typically benefit the most from strongly-typed scope generation.
14 | [Reference test](https://github.com/stbychkov/AutoLoggerMessage/blob/main/tests/AutoLoggerMessageGenerator.UnitTests/MethodSpecificityRules/InstanceCallVsExtensionCallTests.cs)
15 |
16 | ### Decision:
17 |
18 | Extend the AutoLoggerMessage source generator to support generation of strongly-typed scope delegates using `LoggerMessage.DefineScope`.
19 |
20 | Specifically:
21 | - Identify all usages of `ILogger.BeginScope` in the codebase where the call includes **at least one structured argument** (excluding pure string messages).
22 | - For each identified scope usage:
23 | - Generate a static readonly field that contains the compiled scope delegate using `LoggerMessage.DefineScope`.
24 | - Generate an extension method (or internal interceptor method) that redirects the original `BeginScope` call to the generated delegate, preserving structure and performance.
25 | - Ensure that the generated methods follow the same naming, visibility, and partial class strategy as existing `Log*` method interceptors.
26 |
27 | ### Consequences:
28 |
29 | * **Short-term**:
30 | - Improves performance and reduces allocations for scoped logging with parameters.
31 | - Introduces new source generation complexity; testing must be extended to validate generated scopes.
32 |
33 | * **Long-term**:
34 | - Moves the library closer to complete compile-time safety for all common logging patterns (`Log*` and `BeginScope`).
35 |
36 | * **Risks**:
37 | - May cause confusion if developers attempt to use `BeginScope(string message)` expecting interception (which is not supported).
38 | - Reliance on exact call shapes (number and types of arguments) may introduce fragility unless thoroughly tested.
39 |
40 | * **Maintenance**:
41 | - Must track and test against future changes in `LoggerMessage.DefineScope` API (currently supports up to 6 parameters).
42 | - Increases the surface area of generated code, potentially impacting future refactors or compatibility with downstream tools.
43 |
44 | ### Alternatives Considered
45 |
46 | * **Do nothing**: Keep relying on `ILogger.BeginScope` as is. This maintains simplicity but misses out on performance and compile-time safety.
47 |
--------------------------------------------------------------------------------
/docs/how-it-works.md:
--------------------------------------------------------------------------------
1 | # Internals: How the Source Generator Works
2 |
3 | ## Overview
4 |
5 | The source generator automatically creates `LoggerMessage` methods for high-performance logging.
6 | This process is designed to simplify logging and ensure that logging code doesn't require manual maintenance or updates.
7 | The generator builds upon the existing `LoggerMessage` generators and adds functionality to support both old and new logging packages (`Microsoft.Extensions.Logging.Abstractions` and `Microsoft.Extensions.Telemetry.Abstractions`).
8 |
9 | ### Step 0: Create a set of generic logger overloads (up to 6 parameters)
10 |
11 | The first step involves generating **196** generic extension methods for `ILogger` class that will override the default logging methods ([why?](./ADR/ADR-01_Generation_of_logger_extension_methods_overloads.md)).
12 | `LoggerMessage.Define` supports up to 6 parameters, so we can limit the methods only with this amount.
13 |
14 | ### Step 1: Find all Log* methods belonging to ILogger class
15 |
16 | The generator looks for any `Log+` method (like `LogInformation`, `Log`, etc.) and captures the parameters passed to these methods.
17 |
18 | ### Step 2: Generate a virtual partial class with partial LoggerMessage methods
19 |
20 | After identifying the relevant logging methods, the generator creates a virtual class that will exist only in the readonly snapshot of the compilation process.
21 | This class contains partial LoggerMessage methods, which are virtual but will not be included in the final output.
22 | The virtual class serves solely to trigger the `LoggerMessage` source generator, which will generate the actual methods used for logging.
23 | At this point, the generator doesn't produce the methods directly but ensures that the code structure is in place to trigger the existing `LoggerMessage` generators.
24 |
25 | ### Step 3: Use existing LoggerMessage generator to generate the rest
26 |
27 | The LoggerMessage source generator from `Microsoft.Extensions.Logging.Abstractions` or `Microsoft.Extensions.Telemetry.Abstractions` will now work its magic.
28 | It takes the parameters from the virtual methods and generates the actual `LoggerMessage` methods that the application will use for logging.
29 | This works similarly to how `LoggerMessage` is typically generated, but we are leveraging a virtual class that exists only during compilation to trigger it.
30 |
31 | ### Step 4: Post-process the result with minor modifications
32 |
33 | Once the source generator produces the methods, the process enters a post-processing phase. Here, we make necessary adjustments:
34 |
35 | * Remove the partial keyword: Since the virtual class doesn't exist in the output, the generator would have attempted to declare methods as partial.
36 | In the post-processing step, we remove the partial keyword from these methods to ensure that the generated code is valid and compilable.
37 | * Backtrack diagnostic locations: During code generation, diagnostics are often tied to the temporary virtual class.
38 | In this phase, we backtrack any diagnostics to the original Log* calls by mapping the generated method to the location in the original source code.
39 | If this mapping fails, we fall back to an empty location.
40 |
41 | ### Step 5: Creating interceptors
42 |
43 | After the LoggerMessage methods are successfully generated, we proceed to create a set of interceptors.
44 | These interceptors act as a bridge between the logging calls and the generated LoggerMessage methods.
45 | The interceptors forward the logging requests to the correct generated methods, ensuring the logging system operates smoothly.
46 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator.BuildOutput/AutoLoggerMessageGenerator.BuildOutput.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator.BuildOutput/GenericLoggerScopeExtensions.g.cs:
--------------------------------------------------------------------------------
1 | //
2 | #nullable enable
3 |
4 | using System;
5 |
6 | namespace Microsoft.Extensions.Logging
7 | {
8 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("AutoLoggerMessageGenerator", "1.0.10.0")]
9 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
10 | [System.Diagnostics.DebuggerStepThrough]
11 | [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
12 | public static class GenericLoggerScopeExtensions
13 | {
14 | public static IDisposable? BeginScope(this ILogger @logger, string @message, T0 @arg0)
15 | {
16 | return Microsoft.Extensions.Logging.LoggerExtensions.BeginScope(@logger, @message, new object?[] { @arg0 });
17 | }
18 |
19 | public static IDisposable? BeginScope(this ILogger @logger, string @message, T0 @arg0, T1 @arg1)
20 | {
21 | return Microsoft.Extensions.Logging.LoggerExtensions.BeginScope(@logger, @message, new object?[] { @arg0, @arg1 });
22 | }
23 |
24 | public static IDisposable? BeginScope(this ILogger @logger, string @message, T0 @arg0, T1 @arg1, T2 @arg2)
25 | {
26 | return Microsoft.Extensions.Logging.LoggerExtensions.BeginScope(@logger, @message, new object?[] { @arg0, @arg1, @arg2 });
27 | }
28 |
29 | public static IDisposable? BeginScope(this ILogger @logger, string @message, T0 @arg0, T1 @arg1, T2 @arg2, T3 @arg3)
30 | {
31 | return Microsoft.Extensions.Logging.LoggerExtensions.BeginScope(@logger, @message, new object?[] { @arg0, @arg1, @arg2, @arg3 });
32 | }
33 |
34 | public static IDisposable? BeginScope(this ILogger @logger, string @message, T0 @arg0, T1 @arg1, T2 @arg2, T3 @arg3, T4 @arg4)
35 | {
36 | return Microsoft.Extensions.Logging.LoggerExtensions.BeginScope(@logger, @message, new object?[] { @arg0, @arg1, @arg2, @arg3, @arg4 });
37 | }
38 |
39 | public static IDisposable? BeginScope(this ILogger @logger, string @message, T0 @arg0, T1 @arg1, T2 @arg2, T3 @arg3, T4 @arg4, T5 @arg5)
40 | {
41 | return Microsoft.Extensions.Logging.LoggerExtensions.BeginScope(@logger, @message, new object?[] { @arg0, @arg1, @arg2, @arg3, @arg4, @arg5 });
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator.Sandbox/AutoLoggerMessageGenerator.Sandbox.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net8.0
4 | Exe
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator.Sandbox/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Logging;
2 |
3 | var loggerFactory = LoggerFactory.Create(builder =>
4 | builder.AddSimpleConsole(options =>
5 | {
6 | options.IncludeScopes = true;
7 | }).SetMinimumLevel(LogLevel.Trace)
8 | );
9 | var logger = loggerFactory.CreateLogger();
10 |
11 | using var parentScope = logger.BeginScope("Scope {Level}", 1);
12 |
13 | logger.LogTrace("Log message without parameters");
14 | logger.LogDebug("Log message with parameters: {Param1}, {Param2}", 42, "Hello, World!");
15 |
16 | using (logger.BeginScope("Scope {Level}", 2))
17 | {
18 | logger.LogInformation("Log message with 6 parameters: {Arg1}, {Arg2}, {Arg3}, {Arg4}, {Arg5} {Arg6}", 1, 2, 3, 4, 5, 6);
19 | logger.LogWarning(new EventId(42, "Event1"), "Event1 happened");
20 | }
21 |
22 | logger.LogError(new EventId(42, "Event1"), new Exception("Event1 error"), "Event1 happened");
23 | logger.LogCritical(new EventId(42, "Event2"), new Exception("Event2 error"), "Event2 happened {Arg1}", new EventData(123, "Event details"));
24 |
25 | internal record EventData(int Id, string Name);
26 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Assets/icon.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stbychkov/AutoLoggerMessage/a446fa1b8523d762423fe5915681c8b18dd5f145/src/AutoLoggerMessageGenerator/Assets/icon.jpg
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/AutoLoggerMessageGenerator.Build.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 | netstandard2.0
4 | true
5 | true
6 | RSEXPERIMENTAL002;RS2008
7 | 1.0.10
8 | true
9 | AutoLoggerMessageGenerator
10 | false
11 |
12 |
13 |
14 | stbychkov.AutoLoggerMessage
15 | stbychkov
16 | AutoLoggerMessage
17 | A source generator that automatically migrates your logging calls to the LoggerMessage version
18 | logging;roslyn
19 | MIT
20 | https://github.com/stbychkov/AutoLoggerMessage
21 | https://github.com/stbychkov/AutoLoggerMessage
22 | https://github.com/stbychkov/AutoLoggerMessage
23 | git
24 | README.md
25 | icon.jpg
26 | false
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
37 |
38 |
39 |
40 |
42 |
45 |
46 | LoggerExtensionMethodsEmitter.cs
47 |
48 |
49 |
50 |
51 |
52 | <_Parameter1>$(AssemblyName).UnitTests.Roslyn4_8
53 |
54 |
55 | <_Parameter1>$(AssemblyName).UnitTests.Roslyn4_11
56 |
57 |
58 | <_Parameter1>$(AssemblyName).IntegrationTests
59 |
60 |
61 | <_Parameter1>$(AssemblyName).Benchmarks
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/AutoLoggerMessageGenerator.Pack.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.11.0
4 | true
5 | AutoLoggerMessageGenerator
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
19 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/AutoLoggerMessageGenerator.Roslyn4_11.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | 4.11.0
6 | $(DefineConstants);HASH_BASED_INTERCEPTORS
7 | false
8 |
9 |
10 |
11 |
12 | AutoLoggerMessageGenerator.cs
13 |
14 |
15 | AutoLoggerMessageGenerator.cs
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/AutoLoggerMessageGenerator.Roslyn4_8.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | 4.8.0
6 | $(DefineConstants);PATH_BASED_INTERCEPTORS
7 | false
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/AutoLoggerMessageGenerator.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/AutoLoggerMessageGenerator.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | $(InterceptorsPreviewNamespaces);Microsoft.Extensions.Logging.AutoLoggerMessage
5 | $(InterceptorsPreviewNamespaces);Microsoft.Extensions.Logging.AutoLoggerMessage
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Caching/LogCallInputSourceComparer.cs:
--------------------------------------------------------------------------------
1 | using InputSource = (
2 | Microsoft.CodeAnalysis.Compilation Compilation,
3 | (
4 | AutoLoggerMessageGenerator.Configuration.SourceGeneratorConfiguration Configuration,
5 | (
6 | System.Collections.Immutable.ImmutableArray References,
7 | System.Collections.Immutable.ImmutableArray LogCalls
8 | ) Others
9 | ) Others
10 | );
11 |
12 | namespace AutoLoggerMessageGenerator.Caching;
13 |
14 | internal class LogCallInputSourceComparer : IEqualityComparer
15 | {
16 | public bool Equals(InputSource x, InputSource y) =>
17 | x.Others.Configuration == y.Others.Configuration &&
18 | x.Others.Others.References.SequenceEqual(y.Others.Others.References) &&
19 | x.Others.Others.LogCalls.SequenceEqual(y.Others.Others.LogCalls);
20 |
21 | public int GetHashCode(InputSource obj) =>
22 | obj.Item2.GetHashCode();
23 | }
24 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Caching/LogScopeDefinitionInputSourceComparer.cs:
--------------------------------------------------------------------------------
1 | using InputSource = (
2 | Microsoft.CodeAnalysis.Compilation Compilation,
3 | (
4 | AutoLoggerMessageGenerator.Configuration.SourceGeneratorConfiguration Configuration,
5 | System.Collections.Immutable.ImmutableArray LoggerScopes
6 | ) Others
7 | );
8 |
9 | namespace AutoLoggerMessageGenerator.Caching;
10 |
11 | internal class LogScopeDefinitionInputSourceComparer : IEqualityComparer
12 | {
13 | public bool Equals(InputSource x, InputSource y) =>
14 | x.Others.Configuration == y.Others.Configuration &&
15 | x.Others.LoggerScopes.SequenceEqual(y.Others.LoggerScopes);
16 |
17 | public int GetHashCode(InputSource obj) =>
18 | obj.Item2.GetHashCode();
19 | }
20 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Configuration/GeneratorOptionsProvider.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.Diagnostics;
3 |
4 | namespace AutoLoggerMessageGenerator.Configuration;
5 |
6 | internal static class GeneratorOptionsProvider
7 | {
8 | private const string CommonPrefix = $"build_property.{nameof(AutoLoggerMessageGenerator)}";
9 | private const string GenerateInterceptorAttributeKey = $"{CommonPrefix}_{nameof(SourceGeneratorConfiguration.GenerateInterceptorAttribute)}";
10 | private const string GenerateSkipEnabledCheckKey = $"{CommonPrefix}_{nameof(SourceGeneratorConfiguration.GenerateSkipEnabledCheck)}";
11 | private const string GenerateOmitReferenceNameKey = $"{CommonPrefix}_{nameof(SourceGeneratorConfiguration.GenerateOmitReferenceName)}";
12 | private const string GenerateSkipNullPropertiesKey = $"{CommonPrefix}_{nameof(SourceGeneratorConfiguration.GenerateSkipNullProperties)}";
13 | private const string GenerateTransitiveKey = $"{CommonPrefix}_{nameof(SourceGeneratorConfiguration.GenerateTransitive)}";
14 | private const string OverrideBeginScopeBehaviorKey = $"{CommonPrefix}_{nameof(SourceGeneratorConfiguration.OverrideBeginScopeBehavior)}";
15 |
16 | public static IncrementalValueProvider Provide(IncrementalGeneratorInitializationContext context) =>
17 | context.AnalyzerConfigOptionsProvider.Select((options, _) => new SourceGeneratorConfiguration(
18 | GenerateInterceptorAttribute: GetValue(options.GlobalOptions, GenerateInterceptorAttributeKey, true),
19 | GenerateSkipEnabledCheck: GetValue(options.GlobalOptions, GenerateSkipEnabledCheckKey, true),
20 | GenerateOmitReferenceName: GetValue(options.GlobalOptions, GenerateOmitReferenceNameKey, false),
21 | GenerateSkipNullProperties: GetValue(options.GlobalOptions, GenerateSkipNullPropertiesKey, false),
22 | GenerateTransitive: GetValue(options.GlobalOptions, GenerateTransitiveKey, false),
23 | OverrideBeginScopeBehavior: GetValue(options.GlobalOptions, OverrideBeginScopeBehaviorKey, true)
24 | ));
25 |
26 | private static bool GetValue(AnalyzerConfigOptions options, string key, bool defaultValue = true) =>
27 | options.TryGetValue(key, out var value) ? IsFeatureEnabled(value, defaultValue) : defaultValue;
28 |
29 | private static bool IsFeatureEnabled(string value, bool defaultValue) =>
30 | StringComparer.OrdinalIgnoreCase.Equals("enable", value)
31 | || StringComparer.OrdinalIgnoreCase.Equals("enabled", value)
32 | || StringComparer.OrdinalIgnoreCase.Equals("true", value)
33 | || (bool.TryParse(value, out var boolVal) ? boolVal : defaultValue);
34 | }
35 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Configuration/SourceGeneratorConfiguration.cs:
--------------------------------------------------------------------------------
1 | namespace AutoLoggerMessageGenerator.Configuration;
2 |
3 | internal record struct SourceGeneratorConfiguration
4 | (
5 | // You can disable generating of the interceptor attribute by setting this to false
6 | // You might need it if you already have this attribute in your project
7 | bool GenerateInterceptorAttribute,
8 | bool GenerateSkipEnabledCheck,
9 | bool GenerateOmitReferenceName,
10 | bool GenerateSkipNullProperties,
11 | bool GenerateTransitive,
12 | bool OverrideBeginScopeBehavior
13 | );
14 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Configuration/SourceGeneratorConfigurationExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace AutoLoggerMessageGenerator.Configuration;
2 |
3 | internal static class SourceGeneratorConfigurationExtensions
4 | {
5 | public static string ToLowerBooleanString(this bool value) => value ? "true" : "false";
6 | }
7 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Constants.cs:
--------------------------------------------------------------------------------
1 | using AutoLoggerMessageGenerator.Models;
2 |
3 | namespace AutoLoggerMessageGenerator;
4 |
5 | internal static class Constants
6 | {
7 | public const string DefaultLoggingNamespace = "Microsoft.Extensions.Logging";
8 | public const string GeneratorNamespace = $"{DefaultLoggingNamespace}.AutoLoggerMessage";
9 |
10 | public const string InterceptorNamespace = "System.Runtime.CompilerServices";
11 | public const string InterceptorAttributeName = "InterceptsLocationAttribute";
12 |
13 | public const string LogMethodPrefix = "Log_";
14 | public const string LogScopeMethodPrefix = "LogScope_";
15 | public const string LoggerClassName = "AutoLoggerMessage";
16 |
17 | public const string ParameterName = "@arg";
18 | public const string LoggerParameterName = "@logger";
19 | public const string EventIdParameterName = "@eventId";
20 | public const string LogLevelParameterName = "@logLevel";
21 | public const string ExceptionParameterName = "@exception";
22 | public const string MessageParameterName = "@message";
23 |
24 | public const string LoggerMessageGeneratorName = "LoggerMessage";
25 | public const string LoggerScopesGeneratorName = "LoggerScopes";
26 |
27 | public static readonly HashSet ReservedParameterNames =
28 | [
29 | LoggerParameterName, LogLevelParameterName,
30 | ExceptionParameterName, EventIdParameterName,
31 | MessageParameterName
32 | ];
33 |
34 | // List of parameters that will be moved to LoggerMessage attribute arguments
35 | public static readonly HashSet LoggerMessageAttributeParameterTypes =
36 | [CallParameterType.LogLevel, CallParameterType.Message];
37 |
38 | ///
39 | /// Support for an arbitrary number of logging parameters. LoggerMessage.Define supports a maximum of six.
40 | /// Must be synced with LoggerMessageGenerator.Emitter;
41 | ///
42 | public const int MaxLogParameters = 6;
43 |
44 | public const string ExcludeFromCoverageAttribute =
45 | "[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]";
46 | public const string DebuggerStepThroughAttribute =
47 | "[System.Diagnostics.DebuggerStepThrough]";
48 | public static readonly string GeneratedCodeAttribute =
49 | $"[global::System.CodeDom.Compiler.GeneratedCodeAttribute(" +
50 | $"\"{typeof(Generators.AutoLoggerMessageGenerator).Assembly.GetName().Name}\", " +
51 | $"\"{typeof(Generators.AutoLoggerMessageGenerator).Assembly.GetName().Version}\")]";
52 | public const string EditorNotBrowsableAttribute = "[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]";
53 |
54 | public static string GeneratedFileHeader => """
55 | //
56 | #nullable enable
57 |
58 | using System;
59 |
60 | """;
61 | }
62 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Diagnostics/LogMessageDiagnosticReporter.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using AutoLoggerMessageGenerator.Models;
3 | using AutoLoggerMessageGenerator.VirtualLoggerMessage;
4 | using Microsoft.CodeAnalysis;
5 |
6 | namespace AutoLoggerMessageGenerator.Diagnostics;
7 |
8 | internal class LogMessageDiagnosticReporter
9 | {
10 | private readonly SourceProductionContext _context;
11 | private readonly Dictionary _logCallsIndex;
12 |
13 | public LogMessageDiagnosticReporter(SourceProductionContext context, IEnumerable logCalls)
14 | {
15 | _context = context;
16 | _logCallsIndex = logCalls.ToDictionary(c => c.Id);
17 | }
18 |
19 | public void Report(Diagnostic diagnostic)
20 | {
21 | // All imported source generators use SimpleDiagnostic which is internal and doesn't support cloning with message arguments
22 | var messageArgs = typeof(Diagnostic).GetProperty("Arguments", BindingFlags.NonPublic | BindingFlags.Instance)
23 | ?.GetValue(diagnostic) as object[];
24 |
25 | var sourceText = diagnostic.Location.SourceTree?.GetText(_context.CancellationToken);
26 | var location = sourceText is not null &&
27 | LogMessageCallLocationMap.TryMapBack(sourceText, diagnostic.Location, out var logCallId) &&
28 | _logCallsIndex.TryGetValue(logCallId, out var logCall)
29 | ? logCall.Location.Context
30 | : Location.None;
31 |
32 | _context.ReportDiagnostic(Diagnostic.Create(diagnostic.Descriptor, location, messageArgs));
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Emitters/GenericLoggerScopeExtensionsEmitter.cs:
--------------------------------------------------------------------------------
1 | using System.CodeDom.Compiler;
2 | using Microsoft.Extensions.Logging;
3 | using static AutoLoggerMessageGenerator.Constants;
4 |
5 | namespace AutoLoggerMessageGenerator.Emitters;
6 |
7 | /// Generates the class.
8 | /// Provides type-specific logging method overrides to avoid boxing.
9 | /// The class is pre-generated and saved in the solution
10 | /// to prevent excessive code duplication when multiple projects use the same library
11 | /// To run this emitter you need to run LoggerScopeExtensionEmitterTests and take it from the snapshot
12 | internal static class GenericLoggerScopeExtensionsEmitter
13 | {
14 | public const string ClassName = "GenericLoggerScopeExtensions";
15 |
16 | public static string Emit()
17 | {
18 | using var sb = new IndentedTextWriter(new StringWriter());
19 |
20 | sb.WriteLine(GeneratedFileHeader);
21 |
22 | sb.WriteLine($"namespace {DefaultLoggingNamespace}");
23 | sb.WriteLine('{');
24 | sb.Indent++;
25 |
26 | sb.WriteLine(Constants.GeneratedCodeAttribute);
27 | sb.WriteLine(EditorNotBrowsableAttribute);
28 | sb.WriteLine(DebuggerStepThroughAttribute);
29 | sb.WriteLine(ExcludeFromCoverageAttribute);
30 | sb.WriteLine($"public static class {ClassName}");
31 | sb.WriteLine('{');
32 | sb.Indent++;
33 |
34 | for (int i = 1; i <= MaxLogParameters; i++)
35 | {
36 | var parameters = Enumerable.Range(0, i).ToArray();
37 |
38 | var genericTypesDefinition = string.Join(", ", parameters.Select(ix => $"T{ix}"));
39 | genericTypesDefinition = string.IsNullOrEmpty(genericTypesDefinition)
40 | ? string.Empty
41 | : $"<{genericTypesDefinition}>";
42 |
43 | var genericParametersDefinition =
44 | string.Join(", ", parameters.Select(ix => $"T{ix} {ParameterName}{ix}"));
45 | genericParametersDefinition = string.IsNullOrEmpty(genericParametersDefinition)
46 | ? string.Empty
47 | : $", {genericParametersDefinition}";
48 |
49 | var objectParameters = string.Join(", ", parameters.Select(ix => $"{ParameterName}{ix}"));
50 | objectParameters = string.IsNullOrEmpty(objectParameters)
51 | ? string.Empty
52 | : $", new object?[] {{ {objectParameters} }}";
53 |
54 | sb.WriteLine($"public static IDisposable? BeginScope{genericTypesDefinition}(this ILogger {LoggerParameterName}, string {MessageParameterName}{genericParametersDefinition})");
55 | sb.WriteLine('{');
56 | sb.Indent++;
57 |
58 | sb.WriteLine($"return {DefaultLoggingNamespace}.{nameof(LoggerExtensions)}.{nameof(LoggerExtensions.BeginScope)}({LoggerParameterName}, {MessageParameterName}{objectParameters});");
59 |
60 | sb.Indent--;
61 | sb.WriteLine('}');
62 |
63 | sb.WriteLine();
64 | }
65 |
66 | sb.Indent--;
67 | sb.WriteLine('}');
68 |
69 | sb.Indent--;
70 | sb.WriteLine('}');
71 |
72 | return sb.InnerWriter.ToString()!;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Emitters/InterceptorAttributeEmitter.cs:
--------------------------------------------------------------------------------
1 | using System.CodeDom.Compiler;
2 |
3 | namespace AutoLoggerMessageGenerator.Emitters;
4 |
5 | internal static class InterceptorAttributeEmitter
6 | {
7 | public static string Emit()
8 | {
9 | using var sb = new IndentedTextWriter(new StringWriter());
10 |
11 | sb.WriteLine(Constants.GeneratedFileHeader);
12 |
13 | sb.WriteLine($"namespace {Constants.InterceptorNamespace}");
14 | sb.WriteLine('{');
15 | sb.Indent++;
16 |
17 | sb.WriteLine(Constants.GeneratedCodeAttribute);
18 | sb.WriteLine(Constants.EditorNotBrowsableAttribute);
19 | sb.WriteLine("[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]");
20 | sb.WriteLine($"internal sealed class {Constants.InterceptorAttributeName} : Attribute");
21 | sb.WriteLine('{');
22 | sb.Indent++;
23 |
24 | #if PATH_BASED_INTERCEPTORS
25 | sb.WriteLine($"public {Constants.InterceptorAttributeName}(string filePath, int line, int character) {{}}");
26 | #elif HASH_BASED_INTERCEPTORS
27 | sb.WriteLine($"public {Constants.InterceptorAttributeName}(int version, string data) {{}}");
28 | #endif
29 |
30 | sb.Indent--;
31 | sb.WriteLine('}');
32 |
33 | sb.Indent--;
34 | sb.WriteLine('}');
35 |
36 | return sb.InnerWriter.ToString()!;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Emitters/LoggerInterceptorsEmitter.cs:
--------------------------------------------------------------------------------
1 | using System.CodeDom.Compiler;
2 | using AutoLoggerMessageGenerator.Models;
3 |
4 | namespace AutoLoggerMessageGenerator.Emitters;
5 |
6 | internal static class LoggerInterceptorsEmitter
7 | {
8 | public static string Emit(IEnumerable logCalls)
9 | {
10 | using var sb = new IndentedTextWriter(new StringWriter());
11 |
12 | sb.WriteLine(Constants.GeneratedFileHeader);
13 |
14 | sb.WriteLine($"namespace {Constants.GeneratorNamespace}");
15 | sb.WriteLine('{');
16 | sb.Indent++;
17 |
18 | sb.WriteLine(Constants.GeneratedCodeAttribute);
19 | sb.WriteLine(Constants.EditorNotBrowsableAttribute);
20 | sb.WriteLine(Constants.ExcludeFromCoverageAttribute);
21 | sb.WriteLine(Constants.DebuggerStepThroughAttribute);
22 | sb.WriteLine("internal static class LoggerInterceptors");
23 | sb.WriteLine('{');
24 | sb.Indent++;
25 |
26 | foreach (var logCall in logCalls)
27 | {
28 | sb.WriteLine(logCall.Location.InterceptableLocationSyntax);
29 |
30 | var parameters = string.Join(", ", logCall.Parameters.Select((c, i) => $"{c.NativeType} {c.Name}"));
31 | parameters = string.IsNullOrEmpty(parameters) ? string.Empty : $", {parameters}";
32 |
33 | var parameterValues = string.Join(", ", logCall.Parameters
34 | .Where(c => !Constants.LoggerMessageAttributeParameterTypes.Contains(c.Type))
35 | .Select((c, i) => c.Name));
36 | parameterValues = string.IsNullOrEmpty(parameterValues) ? string.Empty : $", {parameterValues}";
37 |
38 | sb.WriteLine($"public static void {logCall.GeneratedMethodName}(this ILogger {Constants.LoggerParameterName}{parameters})");
39 | sb.WriteLine('{');
40 | sb.Indent++;
41 |
42 | sb.WriteLine($"{Constants.GeneratorNamespace}.{Constants.LoggerClassName}.{logCall.GeneratedMethodName}({Constants.LoggerParameterName}{parameterValues});");
43 |
44 | sb.Indent--;
45 | sb.WriteLine('}');
46 | sb.WriteLine();
47 | }
48 |
49 | sb.Indent--;
50 | sb.WriteLine('}');
51 |
52 | sb.Indent--;
53 | sb.WriteLine('}');
54 |
55 | return sb.InnerWriter.ToString()!;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Emitters/LoggerScopeInterceptorsEmitter.cs:
--------------------------------------------------------------------------------
1 | using System.CodeDom.Compiler;
2 | using AutoLoggerMessageGenerator.Models;
3 |
4 | namespace AutoLoggerMessageGenerator.Emitters;
5 |
6 | internal static class LoggerScopeInterceptorsEmitter
7 | {
8 | public static string Emit(IEnumerable loggerScopes)
9 | {
10 | using var sb = new IndentedTextWriter(new StringWriter());
11 |
12 | sb.WriteLine(Constants.GeneratedFileHeader);
13 |
14 | sb.WriteLine($"namespace {Constants.GeneratorNamespace}");
15 | sb.WriteLine('{');
16 | sb.Indent++;
17 |
18 | sb.WriteLine(Constants.GeneratedCodeAttribute);
19 | sb.WriteLine(Constants.EditorNotBrowsableAttribute);
20 | sb.WriteLine(Constants.ExcludeFromCoverageAttribute);
21 | sb.WriteLine(Constants.DebuggerStepThroughAttribute);
22 | sb.WriteLine("internal static class LoggerScopeInterceptors");
23 | sb.WriteLine('{');
24 | sb.Indent++;
25 |
26 | foreach (var loggerScope in loggerScopes)
27 | {
28 | sb.WriteLine(loggerScope.Location.InterceptableLocationSyntax);
29 |
30 | var parameters = string.Join(", ", loggerScope.Parameters.Select((c, i) => $"{c.NativeType} {c.Name}"));
31 | parameters = string.IsNullOrEmpty(parameters) ? string.Empty : $", {parameters}";
32 |
33 | var parameterValues = string.Join(", ", loggerScope.Parameters
34 | .Where(c => c.Name != Constants.MessageParameterName)
35 | .Select((c, i) => c.Name));
36 | parameterValues = string.IsNullOrEmpty(parameterValues) ? string.Empty : $", {parameterValues}";
37 |
38 | sb.WriteLine($"public static IDisposable? {loggerScope.GeneratedMethodName}(this ILogger {Constants.LoggerParameterName}{parameters})");
39 | sb.WriteLine('{');
40 | sb.Indent++;
41 |
42 | sb.WriteLine($"return {Constants.GeneratorNamespace}.{Constants.LoggerScopesGeneratorName}.{loggerScope.GeneratedMethodName}({Constants.LoggerParameterName}{parameterValues});");
43 |
44 | sb.Indent--;
45 | sb.WriteLine('}');
46 | sb.WriteLine();
47 | }
48 |
49 | sb.Indent--;
50 | sb.WriteLine('}');
51 |
52 | sb.Indent--;
53 | sb.WriteLine('}');
54 |
55 | return sb.InnerWriter.ToString()!;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Emitters/LoggerScopesEmitter.cs:
--------------------------------------------------------------------------------
1 | using System.CodeDom.Compiler;
2 | using System.Collections.Immutable;
3 | using AutoLoggerMessageGenerator.Models;
4 | using static AutoLoggerMessageGenerator.Constants;
5 |
6 | namespace AutoLoggerMessageGenerator.Emitters;
7 |
8 | internal static class LoggerScopesEmitter
9 | {
10 | public static string Emit(ImmutableArray loggerScopes)
11 | {
12 | using var sb = new IndentedTextWriter(new StringWriter());
13 |
14 | sb.WriteLine(GeneratedFileHeader);
15 |
16 | sb.WriteLine($"namespace {GeneratorNamespace}");
17 | sb.WriteLine('{');
18 | sb.Indent++;
19 |
20 | sb.WriteLine(Constants.GeneratedCodeAttribute);
21 | sb.WriteLine(EditorNotBrowsableAttribute);
22 | sb.WriteLine($"public static class {LoggerScopesGeneratorName}");
23 | sb.WriteLine('{');
24 | sb.Indent++;
25 |
26 | foreach (var loggerScope in loggerScopes)
27 | {
28 | var parameters = loggerScope.Parameters.Where(c => c.Name != MessageParameterName).ToArray();
29 |
30 | var genericTypes = string.Join(", ", parameters.Select(c => c.NativeType));
31 | var defineScopeGenericTypes = genericTypes == string.Empty ? string.Empty : $"<{genericTypes}>";
32 | genericTypes = string.IsNullOrEmpty(genericTypes) ? string.Empty : $", {genericTypes}";
33 |
34 | var parameterList = string.Join(", ", parameters.Select(c => $"{c.NativeType} {c.Name}"));
35 | parameterList = string.IsNullOrEmpty(parameterList) ? string.Empty : $", {parameterList}";
36 |
37 | var parameterValues = string.Join(", ", parameters.Where(c => c.Name != MessageParameterName).Select(c => c.Name));
38 | parameterValues = string.IsNullOrEmpty(parameterValues) ? string.Empty : $", {parameterValues}";
39 |
40 | var loggerDefineFunctorName = $"_{loggerScope.GeneratedMethodName}";
41 | sb.WriteLine($"private static readonly Func {loggerDefineFunctorName} = LoggerMessage.DefineScope{defineScopeGenericTypes}(\"{loggerScope.Message}\");");
42 |
43 | sb.WriteLine($"public static IDisposable? {loggerScope.GeneratedMethodName}(ILogger @logger{parameterList})");
44 | sb.WriteLine('{');
45 | sb.Indent++;
46 |
47 | sb.WriteLine($"return {loggerDefineFunctorName}(@logger{parameterValues});");
48 |
49 | sb.Indent--;
50 | sb.WriteLine('}');
51 |
52 | sb.WriteLine();
53 | }
54 |
55 | sb.Indent--;
56 | sb.WriteLine('}');
57 |
58 | sb.Indent--;
59 | sb.WriteLine('}');
60 |
61 | return sb.InnerWriter.ToString()!;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Extractors/EnclosingClassExtractor.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.CSharp.Syntax;
3 |
4 | namespace AutoLoggerMessageGenerator.Extractors;
5 |
6 | internal static class EnclosingClassExtractor
7 | {
8 | public static (string Namespace, string ClassName) Extract(InvocationExpressionSyntax invocationExpression)
9 | {
10 | string className = string.Empty, ns = string.Empty;
11 |
12 | SyntaxNode syntaxNode = invocationExpression;
13 | while (syntaxNode.Parent is not null)
14 | {
15 | if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax)
16 | className = classDeclarationSyntax.Identifier.Text;
17 |
18 | if (syntaxNode is NamespaceDeclarationSyntax namespaceDeclarationSyntax)
19 | ns = namespaceDeclarationSyntax.Name.ToString();
20 |
21 | if (syntaxNode is FileScopedNamespaceDeclarationSyntax fileScopedNamespaceDeclarationSyntax)
22 | ns = fileScopedNamespaceDeclarationSyntax.Name.ToString();
23 |
24 | syntaxNode = syntaxNode.Parent;
25 | }
26 |
27 | return (ns, className);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Extractors/LogCallExtractor.cs:
--------------------------------------------------------------------------------
1 | using AutoLoggerMessageGenerator.Import.Microsoft.Extensions.Telemetry.LoggerMessage;
2 | using AutoLoggerMessageGenerator.Mappers;
3 | using AutoLoggerMessageGenerator.Models;
4 | using Microsoft.CodeAnalysis;
5 | using Microsoft.CodeAnalysis.CSharp.Syntax;
6 |
7 | namespace AutoLoggerMessageGenerator.Extractors;
8 |
9 | internal static class LogCallExtractor
10 | {
11 | public static LogMessageCall? Extract(IMethodSymbol methodSymbol,
12 | InvocationExpressionSyntax invocationExpression,
13 | SemanticModel semanticModel)
14 | {
15 | var (ns, className) = EnclosingClassExtractor.Extract(invocationExpression);
16 |
17 | var location = CallLocationMapper.Map(semanticModel, invocationExpression);
18 | if (location is null)
19 | return default;
20 |
21 | var logLevel = LogLevelExtractor.Extract(methodSymbol, invocationExpression);
22 | if (logLevel is null)
23 | return default;
24 |
25 | var message = MessageParameterTextExtractor.Extract(methodSymbol, invocationExpression, semanticModel);
26 | if (message is null)
27 | return default;
28 |
29 | var logPropertiesCheck = new LogPropertiesCheck(semanticModel.Compilation);
30 | var parameters = new CallParametersExtractor(logPropertiesCheck)
31 | .Extract(message, methodSymbol);
32 |
33 | if (parameters is null)
34 | return default;
35 |
36 | return new LogMessageCall(Guid.NewGuid(), location.Value, ns, className, methodSymbol.Name, logLevel, message, parameters.Value);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Extractors/LogLevelExtractor.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.CSharp.Syntax;
3 |
4 | namespace AutoLoggerMessageGenerator.Extractors;
5 |
6 | internal static class LogLevelExtractor
7 | {
8 | public static string? Extract(IMethodSymbol? methodSymbol, InvocationExpressionSyntax? invocationExpression) =>
9 | methodSymbol is null || invocationExpression is null
10 | ? null
11 | : ExtractLogLevelFromMethodName(methodSymbol) ?? ExtractLogLevelFromMethodArgument(invocationExpression);
12 |
13 | private static string? ExtractLogLevelFromMethodName(IMethodSymbol methodSymbol)
14 | {
15 | var logLevelInMethodName = methodSymbol.Name.AsSpan(3);
16 | return logLevelInMethodName.IsEmpty ? default : logLevelInMethodName.ToString();
17 | }
18 |
19 | private static string? ExtractLogLevelFromMethodArgument(InvocationExpressionSyntax invocationExpression)
20 | {
21 | var logLevelArgument = invocationExpression.ArgumentList.Arguments.SingleOrDefault(c =>
22 | c.Expression is MemberAccessExpressionSyntax { Expression: IdentifierNameSyntax { Identifier.ValueText: "LogLevel" } });
23 |
24 | if (logLevelArgument is null)
25 | return null;
26 |
27 | return ((MemberAccessExpressionSyntax) logLevelArgument.Expression).Name.GetText().ToString();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Extractors/LoggerScopeCallExtractor.cs:
--------------------------------------------------------------------------------
1 | using AutoLoggerMessageGenerator.Import.Microsoft.Extensions.Telemetry.LoggerMessage;
2 | using AutoLoggerMessageGenerator.Mappers;
3 | using AutoLoggerMessageGenerator.Models;
4 | using Microsoft.CodeAnalysis;
5 | using Microsoft.CodeAnalysis.CSharp.Syntax;
6 |
7 | namespace AutoLoggerMessageGenerator.Extractors;
8 |
9 | internal static class LoggerScopeCallExtractor
10 | {
11 | public static LoggerScopeCall? Extract(IMethodSymbol methodSymbol,
12 | InvocationExpressionSyntax invocationExpression,
13 | SemanticModel semanticModel)
14 | {
15 | var (ns, className) = EnclosingClassExtractor.Extract(invocationExpression);
16 |
17 | var location = CallLocationMapper.Map(semanticModel, invocationExpression);
18 | if (location is null)
19 | return default;
20 |
21 | var message = MessageParameterTextExtractor.Extract(methodSymbol, invocationExpression, semanticModel);
22 | if (message is null)
23 | return default;
24 |
25 | var logPropertiesCheck = new LogPropertiesCheck(semanticModel.Compilation);
26 | var parameters = new CallParametersExtractor(logPropertiesCheck)
27 | .Extract(message, methodSymbol);
28 |
29 | if (parameters is null)
30 | return default;
31 |
32 | return new LoggerScopeCall(location.Value, ns, className, methodSymbol.Name, message, parameters.Value);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Extractors/MessageParameterNamesExtractor.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Immutable;
2 | using System.Text.RegularExpressions;
3 |
4 | namespace AutoLoggerMessageGenerator.Extractors;
5 |
6 | internal static class MessageParameterNamesExtractor
7 | {
8 | public static ImmutableArray Extract(string? message) =>
9 | MessageArgumentRegex.Matches(message ?? string.Empty)
10 | .OfType()
11 | .Select(c => c.Groups[1].Value)
12 | .ToImmutableArray();
13 |
14 | private static Regex MessageArgumentRegex => new(@"\{(.*?)\}", RegexOptions.Compiled);
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Extractors/MessageParameterTextExtractor.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.CSharp;
3 | using Microsoft.CodeAnalysis.CSharp.Syntax;
4 |
5 | namespace AutoLoggerMessageGenerator.Extractors;
6 |
7 | internal static class MessageParameterTextExtractor
8 | {
9 | public static string? Extract(IMethodSymbol methodSymbol, InvocationExpressionSyntax invocationExpressionSyntax,
10 | SemanticModel semanticModel)
11 | {
12 | var messageParameter = methodSymbol.Parameters.FirstOrDefault(c => c.Name == Constants.MessageParameterName.TrimStart('@'));
13 | if (messageParameter is null)
14 | return null;
15 |
16 | var messageParameterIx = methodSymbol.Parameters.IndexOf(messageParameter);
17 |
18 | var valueExpression = invocationExpressionSyntax.ArgumentList.Arguments[messageParameterIx].Expression;
19 | return ResolveValueExpression(valueExpression, semanticModel);
20 | }
21 |
22 | private static string? ResolveBinaryExpressions(BinaryExpressionSyntax binaryExpressionSyntax, SemanticModel semanticModel)
23 | {
24 | var leftBinaryExpressionValue = ResolveValueExpression(binaryExpressionSyntax.Left, semanticModel);
25 | if (leftBinaryExpressionValue is null) return null;
26 |
27 | var rightBinaryExpressionValue = ResolveValueExpression(binaryExpressionSyntax.Right, semanticModel);
28 | if (rightBinaryExpressionValue is null) return null;
29 |
30 | return string.Concat(leftBinaryExpressionValue, rightBinaryExpressionValue);
31 | }
32 |
33 | private static string? ResolveValueExpression(ExpressionSyntax valueExpression, SemanticModel semanticModel)
34 | {
35 | if (valueExpression is LiteralExpressionSyntax literalExpression)
36 | return literalExpression.Token.Value?.ToString();
37 |
38 | if (valueExpression is IdentifierNameSyntax identifierName)
39 | {
40 | var symbolInfo = semanticModel.GetSymbolInfo(identifierName);
41 | var symbol = symbolInfo.Symbol;
42 |
43 | switch (symbol)
44 | {
45 | case IFieldSymbol fieldSymbol:
46 | return fieldSymbol.ConstantValue?.ToString();
47 | case ILocalSymbol localSymbol:
48 | return localSymbol.ConstantValue?.ToString();
49 | }
50 | }
51 |
52 | if (valueExpression is BinaryExpressionSyntax binaryExpressionSyntax)
53 | return ResolveBinaryExpressions(binaryExpressionSyntax, semanticModel);
54 |
55 | return null;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Filters/LogMessageCallFilter.cs:
--------------------------------------------------------------------------------
1 | using AutoLoggerMessageGenerator.Utilities;
2 | using Microsoft.CodeAnalysis;
3 | using Microsoft.CodeAnalysis.CSharp.Syntax;
4 | using Microsoft.Extensions.Logging;
5 | using static AutoLoggerMessageGenerator.Emitters.GenericLoggerExtensionsEmitter;
6 |
7 | namespace AutoLoggerMessageGenerator.Filters;
8 |
9 | internal static class LogMessageCallFilter
10 | {
11 | private static readonly HashSet LogMethodNames =
12 | [
13 | nameof(LoggerExtensions.Log),
14 | nameof(LoggerExtensions.LogTrace),
15 | nameof(LoggerExtensions.LogDebug),
16 | nameof(LoggerExtensions.LogInformation),
17 | nameof(LoggerExtensions.LogWarning),
18 | nameof(LoggerExtensions.LogError),
19 | nameof(LoggerExtensions.LogCritical),
20 | ];
21 |
22 | public static bool IsLoggerMethod(IMethodSymbol methodSymbol) =>
23 | methodSymbol is { ContainingType: not null, ReceiverType.Name: "ILogger", ReturnsVoid: true } &&
24 | methodSymbol.ContainingType.ToDisplayString() is $"{Constants.DefaultLoggingNamespace}.{ClassName}" &&
25 | methodSymbol.IsExtensionMethod &&
26 | methodSymbol.Parameters.All(c =>
27 | c.Type.IsAnonymousType is not true && c.Type.TypeKind is not TypeKind.TypeParameter &&
28 | TypeAccessibilityChecker.IsAccessible(c.Type)
29 | );
30 |
31 | public static bool IsLogCallInvocation(SyntaxNode node, CancellationToken cts) =>
32 | !node.SyntaxTree.FilePath.EndsWith(".g.cs") &&
33 | node is InvocationExpressionSyntax { ArgumentList.Arguments.Count: > 0 } invocationExpression &&
34 | !cts.IsCancellationRequested &&
35 | invocationExpression.Expression.DescendantNodes()
36 | .Any(c => c is IdentifierNameSyntax identifierNameSyntax && LogMethodNames.Contains(identifierNameSyntax.Identifier.Text));
37 | }
38 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Filters/LoggerScopeFilter.cs:
--------------------------------------------------------------------------------
1 | using AutoLoggerMessageGenerator.Utilities;
2 | using Microsoft.CodeAnalysis;
3 | using Microsoft.CodeAnalysis.CSharp.Syntax;
4 | using static AutoLoggerMessageGenerator.Emitters.GenericLoggerScopeExtensionsEmitter;
5 |
6 | namespace AutoLoggerMessageGenerator.Filters;
7 |
8 | internal static class LoggerScopeFilter
9 | {
10 | public static bool IsLoggerScopeMethod(IMethodSymbol methodSymbol) =>
11 | methodSymbol is { ContainingType: not null, ReceiverType.Name: "ILogger", ReturnsVoid: false, ReturnType.Name: "IDisposable" } &&
12 | methodSymbol.ContainingType.ToDisplayString() is $"{Constants.DefaultLoggingNamespace}.{ClassName}" &&
13 | methodSymbol.IsExtensionMethod &&
14 | methodSymbol.Parameters.All(c =>
15 | c.Type.IsAnonymousType is not true && c.Type.TypeKind is not TypeKind.TypeParameter &&
16 | TypeAccessibilityChecker.IsAccessible(c.Type)
17 | );
18 |
19 | public static bool IsLoggerScopeInvocation(SyntaxNode node, CancellationToken cts) =>
20 | !node.SyntaxTree.FilePath.EndsWith(".g.cs") &&
21 | node is InvocationExpressionSyntax { ArgumentList.Arguments.Count: > 1 } invocationExpression &&
22 | !cts.IsCancellationRequested &&
23 | invocationExpression.Expression.DescendantNodes()
24 | .Any(c => c is IdentifierNameSyntax { Identifier.Text: "BeginScope" });
25 | }
26 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Generators/AutoLoggerMessageGenerator.LoggerScopes.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Immutable;
2 | using System.Text;
3 | using AutoLoggerMessageGenerator.Caching;
4 | using AutoLoggerMessageGenerator.Configuration;
5 | using AutoLoggerMessageGenerator.Emitters;
6 | using AutoLoggerMessageGenerator.Extractors;
7 | using AutoLoggerMessageGenerator.Filters;
8 | using AutoLoggerMessageGenerator.Models;
9 | using Microsoft.CodeAnalysis;
10 | using Microsoft.CodeAnalysis.CSharp.Syntax;
11 | using Microsoft.CodeAnalysis.Text;
12 |
13 | namespace AutoLoggerMessageGenerator.Generators;
14 |
15 | public partial class AutoLoggerMessageGenerator
16 | {
17 | private static void GenerateLoggerScopes(IncrementalGeneratorInitializationContext context, IncrementalValueProvider configuration)
18 | {
19 | var loggerScopeProvider = context.SyntaxProvider.CreateSyntaxProvider(
20 | LoggerScopeFilter.IsLoggerScopeInvocation,
21 | static (ctx, cts) => ParseLoggerScope(ctx, cts)
22 | )
23 | .Where(static t => t.HasValue)
24 | .Select(static (t, _) => t!.Value)
25 | .Collect()
26 | .WithTrackingName("Searching for log scope definitions");
27 |
28 | var inputSource = context.CompilationProvider.Combine(configuration.Combine(loggerScopeProvider))
29 | .WithComparer(new LogScopeDefinitionInputSourceComparer());
30 |
31 | context.RegisterImplementationSourceOutput(inputSource, static (ctx, t) =>
32 | GenerateCode(ctx, t.Item2.Item1, t.Item2.Item2));
33 | }
34 |
35 | private static LoggerScopeCall? ParseLoggerScope(GeneratorSyntaxContext context, CancellationToken cts)
36 | {
37 | var semanticModel = context.SemanticModel;
38 | var invocationExpression = (InvocationExpressionSyntax)context.Node;
39 | var symbolInfo = semanticModel.GetSymbolInfo(invocationExpression);
40 |
41 | if (symbolInfo.Symbol is not IMethodSymbol methodSymbol ||
42 | !LoggerScopeFilter.IsLoggerScopeMethod(methodSymbol) ||
43 | cts.IsCancellationRequested)
44 | return default;
45 |
46 | return LoggerScopeCallExtractor.Extract(methodSymbol, invocationExpression, semanticModel);
47 | }
48 |
49 | private static void GenerateCode(SourceProductionContext context,
50 | SourceGeneratorConfiguration configuration, ImmutableArray loggerScopes)
51 | {
52 | if (context.CancellationToken.IsCancellationRequested ||
53 | loggerScopes.IsDefaultOrEmpty ||
54 | !configuration.OverrideBeginScopeBehavior)
55 | return;
56 |
57 | var generatedLoggerScopes = LoggerScopesEmitter.Emit(loggerScopes);
58 |
59 | if (!string.IsNullOrEmpty(generatedLoggerScopes))
60 | context.AddSource($"{Constants.LoggerScopesGeneratorName}.g.cs", SourceText.From(generatedLoggerScopes, Encoding.UTF8));
61 |
62 | context.AddSource("LoggerScopeInterceptors.g.cs", SourceText.From(LoggerScopeInterceptorsEmitter.Emit(loggerScopes), Encoding.UTF8));
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Generators/AutoLoggerMessageGenerator.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using AutoLoggerMessageGenerator.Configuration;
3 | using AutoLoggerMessageGenerator.Emitters;
4 | using AutoLoggerMessageGenerator.ReferenceAnalyzer;
5 | using Microsoft.CodeAnalysis;
6 | using Microsoft.CodeAnalysis.Text;
7 |
8 | namespace AutoLoggerMessageGenerator.Generators;
9 |
10 | [Generator]
11 | public partial class AutoLoggerMessageGenerator : IIncrementalGenerator
12 | {
13 | public void Initialize(IncrementalGeneratorInitializationContext context)
14 | {
15 | var configuration = GeneratorOptionsProvider.Provide(context);
16 |
17 | var modulesProvider = context.GetMetadataReferencesProvider()
18 | .SelectMany(static (reference, _) => reference.GetModules())
19 | .Collect()
20 | .WithTrackingName("Scanning project references");
21 |
22 | GenerateLoggerMessages(context, configuration, modulesProvider);
23 | GenerateLoggerScopes(context, configuration);
24 | GenerateInterceptorAttribute(context, configuration);
25 | }
26 |
27 | private static void GenerateInterceptorAttribute(IncrementalGeneratorInitializationContext context,
28 | IncrementalValueProvider configuration)
29 | {
30 | context.RegisterImplementationSourceOutput(configuration, static (ctx, configuration) =>
31 | {
32 | if (configuration.GenerateInterceptorAttribute)
33 | ctx.AddSource("InterceptorAttribute.g.cs", SourceText.From(InterceptorAttributeEmitter.Emit(), Encoding.UTF8));
34 | });
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Import/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.cs]
2 | dotnet_diagnostic.CS8600.severity = none
3 | dotnet_diagnostic.CS8601.severity = none
4 | dotnet_diagnostic.CS8602.severity = none
5 | dotnet_diagnostic.CS8603.severity = none
6 | dotnet_diagnostic.CS8604.severity = none
7 | dotnet_diagnostic.CS8618.severity = none
8 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Import/Microsoft.Extensions.Logging.LoggerMessage/DiagnosticDescriptorHelper.cs:
--------------------------------------------------------------------------------
1 | namespace Microsoft.CodeAnalysis.DotnetRuntime.Extensions
2 | {
3 | internal static partial class DiagnosticDescriptorHelper
4 | {
5 | public static DiagnosticDescriptor Create(
6 | string id,
7 | LocalizableString title,
8 | LocalizableString messageFormat,
9 | string category,
10 | DiagnosticSeverity defaultSeverity,
11 | bool isEnabledByDefault,
12 | LocalizableString? description = null,
13 | params string[] customTags)
14 | {
15 | string helpLink = $"https://learn.microsoft.com/dotnet/fundamentals/syslib-diagnostics/{id.ToLowerInvariant()}";
16 |
17 | return new DiagnosticDescriptor(id, title, messageFormat, category, defaultSeverity, isEnabledByDefault, description, helpLink, customTags);
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Import/Microsoft.Extensions.Logging.LoggerMessage/Source.md:
--------------------------------------------------------------------------------
1 | https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Logging.Abstractions/src
2 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Import/Microsoft.Extensions.Telemetry.LoggerMessage/Emission/StringBuilderPool.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 |
4 | using System.Collections.Generic;
5 | using System.Text;
6 |
7 | namespace Microsoft.Gen.Logging.Emission;
8 |
9 | internal sealed class StringBuilderPool
10 | {
11 | private readonly Stack _builders = new();
12 |
13 | public StringBuilder GetStringBuilder()
14 | {
15 | const int DefaultStringBuilderCapacity = 1024;
16 |
17 | if (_builders.Count == 0)
18 | {
19 | return new StringBuilder(DefaultStringBuilderCapacity);
20 | }
21 |
22 | var sb = _builders.Pop();
23 | _ = sb.Clear();
24 | return sb;
25 | }
26 |
27 | public void ReturnStringBuilder(StringBuilder sb)
28 | {
29 | _builders.Push(sb);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Import/Microsoft.Extensions.Telemetry.LoggerMessage/LogPropertiesCheck.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Microsoft.CodeAnalysis;
3 | using Microsoft.Gen.Logging.Parsing;
4 | using Microsoft.Gen.Shared;
5 |
6 | namespace AutoLoggerMessageGenerator.Import.Microsoft.Extensions.Telemetry.LoggerMessage;
7 |
8 | // The original code is a private local function, so the simplest way is just to copy it from the source with adjustments
9 | // source: https://github.dev/dotnet/extensions/blob/c08e5ac737d0830ff83c1c5ae8b084e6fce2b538/src/Generators/Microsoft.Gen.Logging/Parsing/Parser.LogProperties.cs#L334-L335
10 | internal class LogPropertiesCheck
11 | {
12 | private static readonly HashSet AllowedTypeKinds = [TypeKind.Class, TypeKind.Struct, TypeKind.Interface];
13 |
14 | private readonly SymbolHolder? _symbolHolder;
15 |
16 | public LogPropertiesCheck(Compilation compilation) =>
17 | _symbolHolder = SymbolLoader.LoadSymbols(compilation, static (_,_,_) => { });
18 |
19 | public bool IsApplicable(ITypeSymbol symType)
20 | {
21 | if (_symbolHolder is null) return false;
22 |
23 | var isRegularType = symType.Kind == SymbolKind.NamedType &&
24 | AllowedTypeKinds.Contains(symType.TypeKind) &&
25 | !symType.IsStatic;
26 |
27 | if (symType.IsNullableOfT())
28 | symType = ((INamedTypeSymbol)symType).TypeArguments[0];
29 |
30 | return isRegularType && !symType.IsSpecialType(_symbolHolder);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Import/Microsoft.Extensions.Telemetry.LoggerMessage/Model/LoggingMethod.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Diagnostics;
7 | using System.Linq;
8 |
9 | namespace Microsoft.Gen.Logging.Model;
10 |
11 | ///
12 | /// A logger method in a logger type.
13 | ///
14 | [DebuggerDisplay("{Name}")]
15 | internal sealed class LoggingMethod
16 | {
17 | public readonly List Parameters = [];
18 | public readonly List Templates = [];
19 | public string Name = string.Empty;
20 | public string Message = string.Empty;
21 | public int? Level;
22 | public int? EventId;
23 | public string? EventName;
24 | public bool SkipEnabledCheck;
25 | public bool IsExtensionMethod;
26 | public bool IsStatic;
27 | public string Modifiers = string.Empty;
28 | public string LoggerMember = "_logger";
29 | public bool LoggerMemberNullable;
30 | public bool HasXmlDocumentation;
31 |
32 | public LoggingMethodParameter? GetParameterForTemplate(string templateName)
33 | {
34 | foreach (var p in Parameters)
35 | {
36 | if (templateName.Equals(p.TagName, StringComparison.OrdinalIgnoreCase))
37 | {
38 | return p;
39 | }
40 | }
41 |
42 | return null;
43 | }
44 |
45 | public List GetTemplatesForParameter(LoggingMethodParameter lp)
46 | {
47 | HashSet templates = [];
48 | foreach (var t in Templates)
49 | {
50 | if (lp.TagName.Equals(t, StringComparison.OrdinalIgnoreCase))
51 | {
52 | _ = templates.Add(t);
53 | }
54 | }
55 |
56 | return templates.ToList();
57 | }
58 |
59 | public List GetTemplatesForParameter(string parameterName)
60 | {
61 | foreach (var p in Parameters)
62 | {
63 | if (parameterName == p.ParameterName)
64 | {
65 | return GetTemplatesForParameter(p);
66 | }
67 | }
68 |
69 | return [];
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Import/Microsoft.Extensions.Telemetry.LoggerMessage/Model/LoggingMethodParameter.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 |
4 | using System.Collections.Generic;
5 | using System.Diagnostics;
6 |
7 | namespace Microsoft.Gen.Logging.Model;
8 |
9 | ///
10 | /// A single parameter to a logger method.
11 | ///
12 | [DebuggerDisplay("{ParameterName}")]
13 | internal sealed class LoggingMethodParameter
14 | {
15 | public string ParameterName = string.Empty;
16 | public string TagName = string.Empty;
17 | public string Type = string.Empty;
18 | public string? Qualifier;
19 | public bool NeedsAtSign;
20 | public bool IsLogger;
21 | public bool IsException;
22 | public bool IsLogLevel;
23 | public bool IsEnumerable;
24 | public bool IsNullable;
25 | public bool IsReference;
26 | public bool ImplementsIConvertible;
27 | public bool ImplementsIFormattable;
28 | public bool ImplementsISpanFormattable;
29 | public bool HasCustomToString;
30 | public bool SkipNullProperties;
31 | public bool OmitReferenceName;
32 | public bool UsedAsTemplate;
33 | public HashSet ClassificationAttributeTypes = [];
34 | public List Properties = [];
35 | public TagProvider? TagProvider;
36 |
37 | public string ParameterNameWithAtIfNeeded => NeedsAtSign ? "@" + ParameterName : ParameterName;
38 |
39 | public string PotentiallyNullableType
40 | => (IsReference && !IsNullable)
41 | ? Type + "?"
42 | : Type;
43 |
44 | // A parameter flagged as 'normal' is not going to be taken care of specially as an argument to ILogger.Log
45 | // but instead is supposed to be taken as a normal parameter.
46 | public bool IsNormalParameter => !IsLogger && !IsException && !IsLogLevel;
47 |
48 | public bool HasDataClassification => ClassificationAttributeTypes.Count > 0;
49 | public bool HasProperties => Properties.Count > 0;
50 | public bool HasTagProvider => TagProvider is not null;
51 | public bool PotentiallyNull => (IsReference && !IsLogger) || IsNullable;
52 | public bool IsStringifiable => HasCustomToString || ImplementsIConvertible || ImplementsIFormattable || IsEnumerable;
53 | }
54 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Import/Microsoft.Extensions.Telemetry.LoggerMessage/Model/LoggingMethodParameterExtensions.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 |
7 | namespace Microsoft.Gen.Logging.Model;
8 |
9 | internal static class LoggingMethodParameterExtensions
10 | {
11 | internal static void TraverseParameterPropertiesTransitively(
12 | this LoggingMethodParameter parameter,
13 | Action, LoggingProperty> callback)
14 | {
15 | var propertyChain = new LinkedList();
16 |
17 | var firstProperty = new LoggingProperty
18 | {
19 | PropertyName = parameter.ParameterName,
20 | TagName = parameter.TagName,
21 | NeedsAtSign = parameter.NeedsAtSign,
22 | Type = parameter.Type,
23 | IsNullable = parameter.IsNullable,
24 | IsReference = parameter.IsReference,
25 | IsEnumerable = parameter.IsEnumerable
26 | };
27 |
28 | _ = propertyChain.AddFirst(firstProperty);
29 |
30 | TraverseParameterPropertiesTransitively(propertyChain, parameter.Properties, callback);
31 | }
32 |
33 | private static void TraverseParameterPropertiesTransitively(
34 | LinkedList propertyChain,
35 | IReadOnlyCollection propertiesToLog,
36 | Action, LoggingProperty> callback)
37 | {
38 | foreach (var propertyToLog in propertiesToLog)
39 | {
40 | if (propertyToLog.Properties.Count > 0)
41 | {
42 | _ = propertyChain.AddLast(propertyToLog);
43 | TraverseParameterPropertiesTransitively(propertyChain, propertyToLog.Properties, callback);
44 | propertyChain.RemoveLast();
45 | }
46 | else
47 | {
48 | callback(propertyChain, propertyToLog);
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Import/Microsoft.Extensions.Telemetry.LoggerMessage/Model/LoggingProperty.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 |
4 | using System.Collections.Generic;
5 | using System.Diagnostics;
6 |
7 | namespace Microsoft.Gen.Logging.Model;
8 |
9 | [DebuggerDisplay("{PropertyName}")]
10 | internal sealed class LoggingProperty
11 | {
12 | public string PropertyName = string.Empty;
13 | public string TagName = string.Empty;
14 | public string Type = string.Empty;
15 | public HashSet ClassificationAttributeTypes = [];
16 | public bool NeedsAtSign;
17 | public bool IsNullable;
18 | public bool IsReference;
19 | public bool IsEnumerable;
20 | public bool ImplementsIConvertible;
21 | public bool ImplementsIFormattable;
22 | public bool ImplementsISpanFormattable;
23 | public bool HasCustomToString;
24 | public List Properties = [];
25 | public bool OmitReferenceName;
26 | public TagProvider? TagProvider;
27 |
28 | public bool HasDataClassification => ClassificationAttributeTypes.Count > 0;
29 | public bool HasProperties => Properties.Count > 0;
30 | public bool HasTagProvider => TagProvider is not null;
31 | public string PropertyNameWithAt => NeedsAtSign ? "@" + PropertyName : PropertyName;
32 | public bool PotentiallyNull => IsReference || IsNullable;
33 | public bool IsStringifiable => HasCustomToString || ImplementsIConvertible || ImplementsIFormattable || IsEnumerable;
34 | }
35 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Import/Microsoft.Extensions.Telemetry.LoggerMessage/Model/LoggingType.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 |
4 | using System.Collections.Generic;
5 | using System.Diagnostics;
6 |
7 | namespace Microsoft.Gen.Logging.Model;
8 |
9 | ///
10 | /// A logger class/struct/record holding a bunch of logger methods.
11 | ///
12 | [DebuggerDisplay("{Name}")]
13 | internal sealed class LoggingType
14 | {
15 | public readonly List Methods = [];
16 | public readonly List AllMembers = [];
17 | public string Keyword = string.Empty;
18 | public string Namespace = string.Empty;
19 | public string Name = string.Empty;
20 | public LoggingType? Parent;
21 | }
22 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Import/Microsoft.Extensions.Telemetry.LoggerMessage/Model/TagProvider.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 |
4 | using System.Diagnostics.CodeAnalysis;
5 |
6 | namespace Microsoft.Gen.Logging.Model;
7 |
8 | [ExcludeFromCodeCoverage]
9 | internal sealed record class TagProvider(
10 | string MethodName,
11 | string ContainingType);
12 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Import/Microsoft.Extensions.Telemetry.LoggerMessage/Parsing/SymbolHolder.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 |
4 | using System.Collections.Generic;
5 | using System.Diagnostics.CodeAnalysis;
6 | using Microsoft.CodeAnalysis;
7 |
8 | namespace Microsoft.Gen.Logging.Parsing;
9 |
10 | [ExcludeFromCodeCoverage]
11 | internal sealed record class SymbolHolder(
12 | Compilation Compilation,
13 | INamedTypeSymbol LoggerMessageAttribute,
14 | INamedTypeSymbol LogPropertiesAttribute,
15 | INamedTypeSymbol TagProviderAttribute,
16 | INamedTypeSymbol TagNameAttribute,
17 | INamedTypeSymbol LogPropertyIgnoreAttribute,
18 | INamedTypeSymbol ITagCollectorSymbol,
19 | INamedTypeSymbol ILoggerSymbol,
20 | INamedTypeSymbol LogLevelSymbol,
21 | INamedTypeSymbol ExceptionSymbol,
22 | HashSet IgnorePropertiesSymbols,
23 | INamedTypeSymbol EnumerableSymbol,
24 | INamedTypeSymbol FormatProviderSymbol,
25 | INamedTypeSymbol? SpanFormattableSymbol,
26 | INamedTypeSymbol? DataClassificationAttribute,
27 | INamedTypeSymbol? NoDataClassificationAttribute);
28 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Import/Microsoft.Extensions.Telemetry.LoggerMessage/Shared/ClassDeclarationSyntaxReceiver.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 |
4 | using System.Collections.Generic;
5 | using Microsoft.CodeAnalysis;
6 | using Microsoft.CodeAnalysis.CSharp.Syntax;
7 |
8 | #pragma warning disable CA1716
9 | namespace Microsoft.Gen.Shared;
10 | #pragma warning restore CA1716
11 |
12 | ///
13 | /// Class declaration syntax receiver for generators.
14 | ///
15 | #if !SHARED_PROJECT
16 | [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
17 | #endif
18 | internal sealed class ClassDeclarationSyntaxReceiver : ISyntaxReceiver
19 | {
20 | internal static ISyntaxReceiver Create() => new ClassDeclarationSyntaxReceiver();
21 |
22 | ///
23 | /// Gets class declaration syntax holders after visiting nodes.
24 | ///
25 | public ICollection ClassDeclarations { get; } = new List();
26 |
27 | ///
28 | public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
29 | {
30 | if (syntaxNode is ClassDeclarationSyntax classSyntax)
31 | {
32 | ClassDeclarations.Add(classSyntax);
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Import/Microsoft.Extensions.Telemetry.LoggerMessage/Shared/DiagDescriptorsBase.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 |
4 | using System;
5 | using Microsoft.CodeAnalysis;
6 | using Microsoft.Shared.DiagnosticIds;
7 |
8 | #pragma warning disable CA1716
9 | namespace Microsoft.Gen.Shared;
10 | #pragma warning restore CA1716
11 |
12 | #if !SHARED_PROJECT
13 | [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
14 | #endif
15 | internal class DiagDescriptorsBase
16 | {
17 | protected static DiagnosticDescriptor Make(
18 | string id,
19 | string title,
20 | string messageFormat,
21 | string category,
22 | DiagnosticSeverity defaultSeverity = DiagnosticSeverity.Error,
23 | bool isEnabledByDefault = true)
24 | {
25 | #pragma warning disable CA1305 // Specify IFormatProvider
26 | #pragma warning disable CA1863 // Use 'CompositeFormat'
27 | #pragma warning disable CS0436 // Type conflicts with imported type
28 | return new(
29 | id,
30 | title,
31 | messageFormat,
32 | category,
33 | defaultSeverity,
34 | isEnabledByDefault,
35 | null,
36 | string.Format(DiagnosticIds.UrlFormat, id),
37 | Array.Empty());
38 | #pragma warning restore CS0436 // Type conflicts with imported type
39 | #pragma warning restore CA1863 // Use 'CompositeFormat'
40 | #pragma warning restore CA1305 // Specify IFormatProvider
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Import/Microsoft.Extensions.Telemetry.LoggerMessage/Shared/EmitterBase.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 |
4 | using System.Collections.Generic;
5 | using System.Text;
6 |
7 | #pragma warning disable CA1716
8 | namespace Microsoft.Gen.Shared;
9 | #pragma warning restore CA1716
10 |
11 | #if !SHARED_PROJECT
12 | [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
13 | #endif
14 | internal class EmitterBase
15 | {
16 | private const int DefaultStringBuilderCapacity = 1024;
17 | private const int IndentChars = 4;
18 |
19 | private readonly StringBuilder _sb = new(DefaultStringBuilderCapacity);
20 | private readonly string[] _padding = new string[16];
21 | private int _indent;
22 |
23 | public EmitterBase(bool emitPreamble = true)
24 | {
25 | var padding = _padding;
26 | for (int i = 0; i < padding.Length; i++)
27 | {
28 | padding[i] = new string(' ', i * IndentChars);
29 | }
30 |
31 | if (emitPreamble)
32 | {
33 | Out(GeneratorUtilities.FilePreamble);
34 | }
35 | }
36 |
37 | protected void OutOpenBrace()
38 | {
39 | OutLn("{");
40 | Indent();
41 | }
42 |
43 | protected void OutCloseBrace()
44 | {
45 | Unindent();
46 | OutLn("}");
47 | }
48 |
49 | protected void OutCloseBraceWithExtra(string extra)
50 | {
51 | Unindent();
52 | OutIndent();
53 | Out("}");
54 | Out(extra);
55 | OutLn();
56 | }
57 |
58 | protected void OutIndent()
59 | {
60 | _ = _sb.Append(_padding[_indent]);
61 | }
62 |
63 | protected string GetPaddingString(byte indent)
64 | {
65 | return _padding[indent];
66 | }
67 |
68 | protected void OutLn()
69 | {
70 | _ = _sb.AppendLine();
71 | }
72 |
73 | protected void OutLn(string line)
74 | {
75 | OutIndent();
76 | _ = _sb.AppendLine(line);
77 | }
78 |
79 | protected void OutPP(string line)
80 | {
81 | _ = _sb.AppendLine(line);
82 | }
83 |
84 | protected void OutEnumeration(IEnumerable e)
85 | {
86 | bool first = true;
87 | foreach (var item in e)
88 | {
89 | if (!first)
90 | {
91 | Out(", ");
92 | }
93 |
94 | Out(item);
95 | first = false;
96 | }
97 | }
98 |
99 | protected void Out(string text) => _ = _sb.Append(text);
100 | protected void Out(char ch) => _ = _sb.Append(ch);
101 | protected void Indent() => _indent++;
102 | protected void Unindent() => _indent--;
103 | protected void OutGeneratedCodeAttribute() => OutLn($"[{GeneratorUtilities.GeneratedCodeAttribute}]");
104 | protected string Capture() => _sb.ToString();
105 | }
106 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Import/Microsoft.Extensions.Telemetry.LoggerMessage/Shared/ParserUtilities.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 |
4 | using System.Linq;
5 | using System.Threading;
6 | using Microsoft.CodeAnalysis;
7 | using Microsoft.CodeAnalysis.CSharp;
8 | using Microsoft.CodeAnalysis.CSharp.Syntax;
9 |
10 | #pragma warning disable CA1716
11 | namespace Microsoft.Gen.Shared;
12 | #pragma warning restore CA1716
13 |
14 | #if !SHARED_PROJECT
15 | [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
16 | #endif
17 | internal static class ParserUtilities
18 | {
19 | internal static AttributeData? GetSymbolAttributeAnnotationOrDefault(ISymbol? attribute, ISymbol symbol)
20 | {
21 | if (attribute is null)
22 | {
23 | return null;
24 | }
25 |
26 | var attrs = symbol.GetAttributes();
27 | foreach (var item in attrs)
28 | {
29 | if (SymbolEqualityComparer.Default.Equals(attribute, item.AttributeClass) && item.AttributeConstructor != null)
30 | {
31 | return item;
32 | }
33 | }
34 |
35 | return null;
36 | }
37 |
38 | internal static bool PropertyHasModifier(ISymbol property, SyntaxKind modifierToSearch, CancellationToken token)
39 | => property
40 | .DeclaringSyntaxReferences
41 | .Any(x =>
42 | x.GetSyntax(token) is PropertyDeclarationSyntax syntax &&
43 | syntax.Modifiers.Any(m => m.IsKind(modifierToSearch)));
44 |
45 | internal static Location? GetLocation(this ISymbol symbol)
46 | {
47 | if (symbol is null)
48 | {
49 | return null;
50 | }
51 |
52 | return symbol.Locations.IsDefaultOrEmpty
53 | ? null
54 | : symbol.Locations[0];
55 | }
56 |
57 | internal static bool IsBaseOrIdentity(ITypeSymbol source, ITypeSymbol dest, Compilation comp)
58 | {
59 | var conversion = comp.ClassifyConversion(source, dest);
60 | return conversion.IsIdentity || (conversion.IsReference && conversion.IsImplicit);
61 | }
62 |
63 | internal static bool ImplementsInterface(this ITypeSymbol type, ITypeSymbol interfaceType)
64 | {
65 | foreach (var iface in type.AllInterfaces)
66 | {
67 | if (SymbolEqualityComparer.Default.Equals(interfaceType, iface))
68 | {
69 | return true;
70 | }
71 | }
72 |
73 | return false;
74 | }
75 |
76 | // Check if parameter has either simplified (i.e. "int?") or explicit (Nullable) nullable type declaration:
77 | internal static bool IsNullableOfT(this ITypeSymbol type)
78 | => type.SpecialType == SpecialType.System_Nullable_T || type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T;
79 | }
80 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Import/Microsoft.Extensions.Telemetry.LoggerMessage/Shared/StringBuilderPool.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 |
4 | using System.Collections.Generic;
5 | using System.Text;
6 |
7 | #pragma warning disable CA1716
8 | namespace Microsoft.Gen.Shared;
9 | #pragma warning restore CA1716
10 |
11 | #if !SHARED_PROJECT
12 | [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
13 | #endif
14 | internal sealed class StringBuilderPool
15 | {
16 | private readonly Stack _builders = new();
17 |
18 | public StringBuilder GetStringBuilder()
19 | {
20 | const int DefaultStringBuilderCapacity = 1024;
21 |
22 | if (_builders.Count == 0)
23 | {
24 | return new StringBuilder(DefaultStringBuilderCapacity);
25 | }
26 |
27 | var sb = _builders.Pop();
28 | _ = sb.Clear();
29 | return sb;
30 | }
31 |
32 | public void ReturnStringBuilder(StringBuilder sb)
33 | {
34 | _builders.Push(sb);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Import/Microsoft.Extensions.Telemetry.LoggerMessage/Shared/SymbolHelpers.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 |
4 | using Microsoft.CodeAnalysis;
5 |
6 | #pragma warning disable CA1716
7 | namespace Microsoft.Gen.Shared;
8 | #pragma warning restore CA1716
9 |
10 | #if !SHARED_PROJECT
11 | [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
12 | #endif
13 | internal static class SymbolHelpers
14 | {
15 | public static string GetFullNamespace(ISymbol symbol)
16 | {
17 | return symbol.ContainingNamespace.IsGlobalNamespace ? string.Empty : symbol.ContainingNamespace.ToString();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Import/Microsoft.Extensions.Telemetry.LoggerMessage/Shared/TypeDeclarationSyntaxReceiver.cs:
--------------------------------------------------------------------------------
1 | // Licensed to the .NET Foundation under one or more agreements.
2 | // The .NET Foundation licenses this file to you under the MIT license.
3 |
4 | using System.Collections.Generic;
5 | using Microsoft.CodeAnalysis;
6 | using Microsoft.CodeAnalysis.CSharp.Syntax;
7 |
8 | #pragma warning disable CA1716
9 | namespace Microsoft.Gen.Shared;
10 | #pragma warning restore CA1716
11 |
12 | ///
13 | /// Class/struct/record declaration syntax receiver for generators.
14 | ///
15 | #if !SHARED_PROJECT
16 | [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
17 | #endif
18 | internal sealed class TypeDeclarationSyntaxReceiver : ISyntaxReceiver
19 | {
20 | internal static ISyntaxReceiver Create() => new TypeDeclarationSyntaxReceiver();
21 |
22 | ///
23 | /// Gets class/struct/record declaration syntax holders after visiting nodes.
24 | ///
25 | public ICollection TypeDeclarations { get; } = new List();
26 |
27 | ///
28 | public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
29 | {
30 | if (syntaxNode is ClassDeclarationSyntax classSyntax)
31 | {
32 | TypeDeclarations.Add(classSyntax);
33 | }
34 | else if (syntaxNode is StructDeclarationSyntax structSyntax)
35 | {
36 | TypeDeclarations.Add(structSyntax);
37 | }
38 | else if (syntaxNode is RecordDeclarationSyntax recordSyntax)
39 | {
40 | TypeDeclarations.Add(recordSyntax);
41 | }
42 | else if (syntaxNode is InterfaceDeclarationSyntax interfaceSyntax)
43 | {
44 | TypeDeclarations.Add(interfaceSyntax);
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Import/Microsoft.Extensions.Telemetry.LoggerMessage/Source.md:
--------------------------------------------------------------------------------
1 | https://github.com/dotnet/extensions/blob/main/src/Generators/Microsoft.Gen.Logging
2 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/LegacySupport/IsExternalInit/IsExternalInit.cs:
--------------------------------------------------------------------------------
1 | #pragma warning disable IDE0079
2 | #pragma warning disable S3903
3 |
4 | // ReSharper disable once CheckNamespace
5 | namespace System.Runtime.CompilerServices;
6 |
7 | /* This enables support for C# 9/10 records on older frameworks */
8 | internal static class IsExternalInit
9 | {
10 | }
11 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Mappers/CallLocationMapper.cs:
--------------------------------------------------------------------------------
1 | using AutoLoggerMessageGenerator.Models;
2 | using Microsoft.CodeAnalysis;
3 | using Microsoft.CodeAnalysis.CSharp;
4 | using Microsoft.CodeAnalysis.CSharp.Syntax;
5 |
6 | namespace AutoLoggerMessageGenerator.Mappers;
7 |
8 | internal static class CallLocationMapper
9 | {
10 | public static CallLocation? Map(SemanticModel semanticModel, InvocationExpressionSyntax invocationExpression)
11 | {
12 | var memberAccessExpression = invocationExpression.Expression as MemberAccessExpressionSyntax;
13 | if (memberAccessExpression?.Expression is not IdentifierNameSyntax identifierName)
14 | return null;
15 |
16 | var skipSymbols = identifierName.Identifier.ValueText.Length + 1; // obj accessor + dot symbol
17 |
18 | var location = invocationExpression.GetLocation();
19 | var lineSpan = location.GetLineSpan();
20 | var linePositionSpan = location.GetLineSpan().Span;
21 |
22 | var filePath = lineSpan.Path;
23 | var line = lineSpan.StartLinePosition.Line + 1;
24 | var character = linePositionSpan.Start.Character + skipSymbols + 1;
25 |
26 | #if PATH_BASED_INTERCEPTORS
27 | var interceptableLocation = GeneratePathBasedInterceptableLocation(filePath, line, character);
28 | #elif HASH_BASED_INTERCEPTORS
29 | var interceptableLocation = semanticModel.GetInterceptableLocation(invocationExpression)?.GetInterceptsLocationAttributeSyntax();
30 | #else
31 | throw new NotSupportedException("Unknown interceptors configuration");
32 | #endif
33 |
34 | if (interceptableLocation is null)
35 | return null;
36 |
37 | return new CallLocation(filePath, line, character, interceptableLocation, location);
38 | }
39 |
40 | #if PATH_BASED_INTERCEPTORS
41 | private static string GeneratePathBasedInterceptableLocation(string filePath, int line, int character)
42 | {
43 | return $"[{Constants.InterceptorNamespace}.{Constants.InterceptorAttributeName}(" +
44 | $"filePath: @\"{filePath}\", " +
45 | $"line: {line}, " +
46 | $"character: {character}" +
47 | $")]";
48 | }
49 | #endif
50 | }
51 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Models/CallLocation.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 |
3 | namespace AutoLoggerMessageGenerator.Models;
4 |
5 | internal readonly record struct CallLocation(
6 | string FilePath,
7 | int Line,
8 | int Character,
9 | string InterceptableLocationSyntax,
10 | Location Context
11 | )
12 | {
13 | public bool Equals(CallLocation other) =>
14 | FilePath == other.FilePath &&
15 | Line == other.Line &&
16 | Character == other.Character &&
17 | InterceptableLocationSyntax == other.InterceptableLocationSyntax;
18 |
19 | public override int GetHashCode()
20 | {
21 | unchecked
22 | {
23 | var hashCode = FilePath.GetHashCode();
24 | hashCode = (hashCode * 397) ^ Line;
25 | hashCode = (hashCode * 397) ^ Character;
26 | hashCode = (hashCode * 397) ^ InterceptableLocationSyntax.GetHashCode();
27 | return hashCode;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Models/CallParameter.cs:
--------------------------------------------------------------------------------
1 | namespace AutoLoggerMessageGenerator.Models;
2 |
3 | internal record struct CallParameter
4 | (
5 | string NativeType,
6 | string Name,
7 | CallParameterType Type,
8 | bool HasPropertiesToLog = false
9 | );
10 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Models/CallParameterType.cs:
--------------------------------------------------------------------------------
1 | namespace AutoLoggerMessageGenerator.Models;
2 |
3 | internal enum CallParameterType
4 | {
5 | None,
6 | Logger,
7 | EventId,
8 | LogLevel,
9 | Exception,
10 | Message,
11 | Others
12 | };
13 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Models/LogMessageCall.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Immutable;
2 | using AutoLoggerMessageGenerator.Utilities;
3 |
4 | namespace AutoLoggerMessageGenerator.Models;
5 |
6 | internal readonly record struct LogMessageCall(
7 | // Need this property for backtracking diagnostic reports
8 | Guid Id,
9 | CallLocation Location,
10 | string Namespace,
11 | string ClassName,
12 | string MethodName,
13 | string LogLevel,
14 | string Message,
15 | ImmutableArray Parameters
16 | )
17 | {
18 | public string GeneratedMethodName =>
19 | IdentifierHelper.ToValidCSharpMethodName(
20 | $"{Constants.LogMethodPrefix}{Namespace}{ClassName}_{Location.Line}_{Location.Character}"
21 | );
22 |
23 | public bool Equals(LogMessageCall other) =>
24 | Location.Equals(other.Location) &&
25 | Namespace == other.Namespace &&
26 | ClassName == other.ClassName &&
27 | MethodName == other.MethodName &&
28 | LogLevel == other.LogLevel &&
29 | Message == other.Message &&
30 | Parameters.SequenceEqual(other.Parameters);
31 |
32 | public override int GetHashCode() =>
33 | (Location, Namespace, ClassName, MethodName, LogLevel, Message, Parameters).GetHashCode();
34 | };
35 |
36 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Models/LoggerScopeCall.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Immutable;
2 | using AutoLoggerMessageGenerator.Utilities;
3 |
4 | namespace AutoLoggerMessageGenerator.Models;
5 |
6 | internal readonly record struct LoggerScopeCall(
7 | CallLocation Location,
8 | string Namespace,
9 | string ClassName,
10 | string MethodName,
11 | string Message,
12 | ImmutableArray Parameters
13 | )
14 | {
15 | public string GeneratedMethodName =>
16 | IdentifierHelper.ToValidCSharpMethodName(
17 | $"{Constants.LogScopeMethodPrefix}{Namespace}{ClassName}_{Location.Line}_{Location.Character}"
18 | );
19 |
20 | public bool Equals(LoggerScopeCall other) =>
21 | Location.Equals(other.Location) &&
22 | Namespace == other.Namespace &&
23 | ClassName == other.ClassName &&
24 | MethodName == other.MethodName &&
25 | Message == other.Message &&
26 | Parameters.SequenceEqual(other.Parameters);
27 |
28 | public override int GetHashCode() => (Location, Namespace, ClassName, MethodName, Message, Parameters).GetHashCode();
29 | };
30 |
31 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/PostProcessing/LoggerMessageResultAdjuster.cs:
--------------------------------------------------------------------------------
1 | namespace AutoLoggerMessageGenerator.PostProcessing;
2 |
3 | internal static class LoggerMessageResultAdjuster
4 | {
5 | public static string? Adjust(string? generatedCode) =>
6 | generatedCode?.Replace(
7 | $"static partial void {Constants.LogMethodPrefix}",
8 | $"static void {Constants.LogMethodPrefix}"
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "DebugRoslynSourceGenerator": {
5 | "commandName": "DebugRoslynComponent",
6 | "targetProject": "../AutoLoggerMessageGenerator.Sandbox/AutoLoggerMessageGenerator.Sandbox.csproj"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/ReferenceAnalyzer/IncrementalGeneratorInitializationContextExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Immutable;
2 | using Microsoft.CodeAnalysis;
3 |
4 | namespace AutoLoggerMessageGenerator.ReferenceAnalyzer;
5 |
6 | public static class IncrementalGeneratorInitializationContextExtensions
7 | {
8 | public static IncrementalValuesProvider GetMetadataReferencesProvider(this IncrementalGeneratorInitializationContext context)
9 | {
10 | var metadataProviderProperty = context.GetType().GetProperty(nameof(context.MetadataReferencesProvider))
11 | ?? throw new Exception($"The property '{nameof(context.MetadataReferencesProvider)}' not found");
12 |
13 | var metadataProvider = metadataProviderProperty.GetValue(context);
14 |
15 | if (metadataProvider is IncrementalValuesProvider metadataValuesProvider)
16 | return metadataValuesProvider;
17 |
18 | if (metadataProvider is IncrementalValueProvider metadataValueProvider)
19 | return metadataValueProvider.SelectMany(static (reference, _) => ImmutableArray.Create(reference));
20 |
21 | throw new Exception($"The '{nameof(context.MetadataReferencesProvider)}' is neither an 'IncrementalValuesProvider<{nameof(MetadataReference)}>' nor an 'IncrementalValueProvider<{nameof(MetadataReference)}>.'");
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/ReferenceAnalyzer/MetadataReferenceExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 |
3 | namespace AutoLoggerMessageGenerator.ReferenceAnalyzer;
4 |
5 | public static class MetadataReferenceExtensions
6 | {
7 | public static IEnumerable GetModules(this MetadataReference metadataReference)
8 | {
9 | if (metadataReference is CompilationReference compilationReference)
10 | {
11 | return compilationReference.Compilation.Assembly.Modules
12 | .Select(m => new Reference(m.Name, compilationReference.Compilation.Assembly.Identity.Version));
13 | }
14 |
15 | if (metadataReference is PortableExecutableReference portable && portable.GetMetadata() is AssemblyMetadata assemblyMetadata)
16 | {
17 | return assemblyMetadata.GetModules()
18 | .Select(m => new Reference(m.Name, m.GetMetadataReader().GetAssemblyDefinition().Version));
19 | }
20 |
21 | return Array.Empty();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/ReferenceAnalyzer/Reference.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace AutoLoggerMessageGenerator.ReferenceAnalyzer;
4 |
5 | public readonly record struct Reference(string Name, Version Version);
6 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Utilities/IdentifierHelper.cs:
--------------------------------------------------------------------------------
1 | using System.Text.RegularExpressions;
2 |
3 | namespace AutoLoggerMessageGenerator.Utilities;
4 |
5 | internal static class IdentifierHelper
6 | {
7 | public static string ToValidCSharpMethodName(string? input)
8 | {
9 | if (string.IsNullOrEmpty(input))
10 | throw new ArgumentNullException(nameof(input), @"Method name cannot be empty");
11 |
12 | var sanitizedInput = Regex.Replace(input, @"[^a-zA-Z0-9_]", "_");
13 | if (!char.IsLetter(sanitizedInput[0]) && sanitizedInput[0] != '_')
14 | sanitizedInput = "_" + sanitizedInput;
15 |
16 | return sanitizedInput;
17 | }
18 |
19 | public static string AddAtPrefixIfNotExists(string parameterName) =>
20 | parameterName.StartsWith("@") ? parameterName : '@' + parameterName;
21 |
22 | public static bool IsValidCSharpParameterName(string? name) =>
23 | !string.IsNullOrEmpty(name) && IsValidCSharpParameterNameRegex.IsMatch(name);
24 |
25 | public const string ValidCSharpParameterNameRegex = @"^@?[a-zA-Z_][a-zA-Z0-9_]*$";
26 |
27 | private static Regex IsValidCSharpParameterNameRegex => new(ValidCSharpParameterNameRegex, RegexOptions.Compiled);
28 | }
29 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Utilities/ReservedParameterNameResolver.cs:
--------------------------------------------------------------------------------
1 | namespace AutoLoggerMessageGenerator.Utilities;
2 |
3 | internal static class ReservedParameterNameResolver
4 | {
5 | private const char UniqueNameSuffix = '_';
6 |
7 | public static string GenerateUniqueIdentifierSuffix(string[] templateParametersNames)
8 | {
9 | if (templateParametersNames.All(name => !Constants.ReservedParameterNames.Contains(name)))
10 | return string.Empty;
11 |
12 | var length = templateParametersNames.Max(name => name.Length - name.TrimEnd(UniqueNameSuffix).Length + 1);
13 | return new(UniqueNameSuffix, length);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/Utilities/TypeAccessibilityChecker.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 |
3 | namespace AutoLoggerMessageGenerator.Utilities;
4 |
5 | internal static class TypeAccessibilityChecker
6 | {
7 | public static bool IsAccessible(ITypeSymbol typeSymbol) =>
8 | typeSymbol.DeclaredAccessibility is Accessibility.Public or Accessibility.Internal &&
9 | (typeSymbol.ContainingType is null || IsAccessible(typeSymbol.ContainingType));
10 | }
11 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/VirtualLoggerMessage/LogMessageCallLocationMap.cs:
--------------------------------------------------------------------------------
1 | using AutoLoggerMessageGenerator.Models;
2 | using Microsoft.CodeAnalysis;
3 | using Microsoft.CodeAnalysis.Text;
4 |
5 | namespace AutoLoggerMessageGenerator.VirtualLoggerMessage;
6 |
7 | internal static class LogMessageCallLocationMap
8 | {
9 | private const string LogMessageCallMappingLocationFlag = "// : ";
10 |
11 | public static string CreateMapping(LogMessageCall logMessageCall) =>
12 | $"{LogMessageCallMappingLocationFlag}{logMessageCall.Id}";
13 |
14 | public static bool TryMapBack(SourceText syntaxTree, Location currentLocation, out Guid logCallId)
15 | {
16 | logCallId = default;
17 |
18 | var subText = syntaxTree.GetSubText(new TextSpan(0, currentLocation.SourceSpan.Start - 1));
19 |
20 | for (var lineIndex = subText.Lines.Count - 1; lineIndex >= 0; lineIndex--)
21 | {
22 | var line = subText.Lines[lineIndex].ToString().TrimStart();
23 | if (!line.StartsWith(LogMessageCallMappingLocationFlag)) continue;
24 |
25 | return Guid.TryParse(line.Substring(LogMessageCallMappingLocationFlag.Length), out logCallId);
26 | }
27 |
28 | return false;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/AutoLoggerMessageGenerator/VirtualLoggerMessage/VirtualMembersInjector.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.CSharp;
3 | using Microsoft.CodeAnalysis.CSharp.Syntax;
4 | using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
5 |
6 | namespace AutoLoggerMessageGenerator.VirtualLoggerMessage;
7 |
8 | internal static class VirtualMembersInjector
9 | {
10 | public static Compilation InjectMembers(Compilation injectTo, MemberDeclarationSyntax memberDeclarationSyntax)
11 | {
12 | var compilationUnit = CompilationUnit().AddMembers(memberDeclarationSyntax);
13 |
14 | var csharpOptions = injectTo.SyntaxTrees.First().Options as CSharpParseOptions;
15 | var syntaxTree = CSharpSyntaxTree.ParseText(compilationUnit.NormalizeWhitespace().ToFullString(), csharpOptions);
16 | var compilation = injectTo.AddSyntaxTrees(syntaxTree);
17 |
18 | return compilation;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.IntegrationTests/AutoLoggerMessageGenerator.IntegrationTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | false
6 |
7 |
8 |
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
22 |
23 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.IntegrationTests/BeginScopeWithAllParameterRangeTests.cs:
--------------------------------------------------------------------------------
1 |
2 | using Microsoft.Extensions.Logging;
3 |
4 | namespace AutoLoggerMessageGenerator.IntegrationTests;
5 |
6 | internal class BeginScopeWithAllParameterRangeTests
7 | {
8 | [Test]
9 | [MethodDataSource(nameof(BeginScopeMethodsWithDifferentParameters))]
10 | public async Task WithAllLogMethods_RequestShouldBeForwardedToLoggerMessageSourceGenerator(Func beginScopeCall)
11 | {
12 | ILogger logger = LoggerFactory.Create(c =>
13 | c.AddSimpleConsole(options =>
14 | {
15 | options.IncludeScopes = true;
16 | })
17 | ).CreateLogger();
18 |
19 | var proxy = DispatchProxyExecutionVerificationDecorator.Decorate(logger,
20 | Constants.LoggerScopesGeneratorName, methodName => methodName == "BeginScope");
21 |
22 | using var _ = beginScopeCall((ILogger) proxy);
23 | logger.LogInformation("Hello world");
24 |
25 | await Assert.That(proxy.ExecutionsWithoutGenerator).IsEmpty();
26 | await Assert.That(proxy.ExecutionsFromGenerator.Count).IsEqualTo(1);
27 | }
28 |
29 | [Test]
30 | [MethodDataSource(nameof(BeginScopeMethodsWithUnsupportedParametersCount))]
31 | public async Task WithUnsupportedParametersCount_RequestShouldBeForwardedToOriginalImplementation(Func beginScopeCall)
32 | {
33 | ILogger logger = LoggerFactory.Create(c =>
34 | c.AddSimpleConsole().SetMinimumLevel(LogLevel.Trace)
35 | ).CreateLogger();
36 |
37 | var proxy = DispatchProxyExecutionVerificationDecorator.Decorate(logger,
38 | Constants.LoggerScopesGeneratorName, methodName => methodName == "BeginScope");
39 |
40 | using var _ = beginScopeCall((ILogger) proxy);
41 | logger.LogInformation("Hello world");
42 |
43 | await Assert.That(proxy.ExecutionsFromGenerator).IsEmpty();
44 | await Assert.That(proxy.ExecutionsWithoutGenerator.Count).IsEqualTo(1);
45 | }
46 |
47 | public static IEnumerable>> BeginScopeMethodsWithDifferentParameters()
48 | {
49 | const string messageWith1Parameters = "Message Scope: {arg1}";
50 | const string messageWith2Parameters = "Message Scope: {arg1} {arg2}";
51 | const string messageWith3Parameters = "Message Scope: {arg1} {arg2} {arg3}";
52 | const string messageWith4Parameters = "Message Scope: {arg1} {arg2} {arg3} {arg4}";
53 | const string messageWith5Parameters = "Message Scope: {arg1} {arg2} {arg3} {arg4} {arg5}";
54 | const string messageWith6Parameters = "Message Scope: {arg1} {arg2} {arg3} {arg4} {arg5} {arg6}";
55 |
56 | return
57 | [
58 | () => l => l.BeginScope(messageWith1Parameters, 1),
59 | () => l => l.BeginScope(messageWith2Parameters, 1, 2),
60 | () => l => l.BeginScope(messageWith3Parameters, 1, 2, 3),
61 | () => l => l.BeginScope(messageWith4Parameters, 1, 2, 3, 4),
62 | () => l => l.BeginScope(messageWith5Parameters, 1, 2, 3, 4, 5),
63 | () => l => l.BeginScope(messageWith6Parameters, 1, 2, 3, 4, 5, 6),
64 | ];
65 | }
66 |
67 | public static IEnumerable>> BeginScopeMethodsWithUnsupportedParametersCount()
68 | {
69 | const string messageWithoutParameters = "Message Scope";
70 | const string messageWith7Parameters = "Message Scope: {arg1} {arg2} {arg3} {arg4} {arg5} {arg6} {arg7}";
71 |
72 | return
73 | [
74 | () => l => l.BeginScope(messageWithoutParameters),
75 | () => l => l.BeginScope(messageWith7Parameters, 1, 2, 3, 4, 5, 6, 7),
76 | ];
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.IntegrationTests/DispatchProxyExecutionVerificationDecorator.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 |
3 | namespace AutoLoggerMessageGenerator.IntegrationTests;
4 |
5 | public class DispatchProxyExecutionVerificationDecorator : DispatchProxy
6 | {
7 | private T? Target { get; set; }
8 | private string? GeneratorName { get; set; }
9 | private Func? MethodFilter { get; set; }
10 |
11 | private readonly List _executionsFromGenerator = [];
12 | private readonly List _executionsWithoutGenerator = [];
13 |
14 | public IReadOnlyList ExecutionsFromGenerator => _executionsFromGenerator.AsReadOnly();
15 | public IReadOnlyList ExecutionsWithoutGenerator => _executionsWithoutGenerator.AsReadOnly();
16 |
17 | public static DispatchProxyExecutionVerificationDecorator Decorate(T target, string generatorName, Func? methodFilter = default)
18 | {
19 | if (Create>() is not DispatchProxyExecutionVerificationDecorator proxy)
20 | throw new InvalidOperationException($"Unable to create DispatchProxyExecutionVerificationDecorator for {typeof(T).FullName}");
21 |
22 | proxy.Target = target;
23 | proxy.GeneratorName = generatorName;
24 | proxy.MethodFilter = methodFilter;
25 |
26 | return proxy;
27 | }
28 |
29 | protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
30 | {
31 | if (MethodFilter == default || (targetMethod?.Name is not null && MethodFilter(targetMethod.Name)))
32 | CaptureExecutionCall();
33 |
34 | return targetMethod?.Invoke(Target, args);
35 | }
36 |
37 | private void CaptureExecutionCall()
38 | {
39 | var stackTrace = Environment.StackTrace;
40 | var callFromGenerator = stackTrace.Contains($"{GeneratorName}.g.cs");
41 |
42 | var executionList = callFromGenerator ? _executionsFromGenerator : _executionsWithoutGenerator;
43 | executionList.Add(stackTrace);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.IntegrationTests/LogPropertiesAttributeTests.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using Microsoft.Extensions.Logging;
3 |
4 | namespace AutoLoggerMessageGenerator.IntegrationTests;
5 |
6 | internal class LogPropertiesAttributeTests
7 | {
8 | private static readonly ILogger Logger = LoggerFactory.Create(c =>
9 | c.AddSimpleConsole().SetMinimumLevel(LogLevel.Trace)
10 | ).CreateLogger();
11 |
12 | [Test]
13 | public async Task AllLogPropertiesHaveToBeLogged()
14 | {
15 | IEvent generatorCallCapturedEvent = new GeneratorCallCapturedEvent { Id = Guid.NewGuid() };
16 | var proxy = DispatchProxyExecutionVerificationDecorator.Decorate(
17 | generatorCallCapturedEvent, Constants.LoggerMessageGeneratorName
18 | );
19 | generatorCallCapturedEvent = (IEvent) proxy;
20 |
21 | var propertiesCount = typeof(GeneratorCallCapturedEvent)
22 | .GetProperties(BindingFlags.Public | BindingFlags.Instance)
23 | .Length;
24 |
25 | Logger.LogInformation("Event received: {event}", generatorCallCapturedEvent);
26 |
27 | await Assert.That(proxy.ExecutionsWithoutGenerator).IsEmpty();
28 | await Assert.That(proxy.ExecutionsFromGenerator.Count).IsEqualTo(propertiesCount);
29 | }
30 | }
31 |
32 | internal interface IEvent
33 | {
34 | Guid Id { get; }
35 |
36 | string Name { get; }
37 |
38 | long Timestamp { get; }
39 | }
40 |
41 | internal sealed record GeneratorCallCapturedEvent : IEvent
42 | {
43 | public required Guid Id { get; init; }
44 |
45 | public string Name => nameof(GeneratorCallCapturedEvent);
46 |
47 | public long Timestamp { get; init; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
48 | }
49 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/AutoLoggerMessageGenerator.UnitTests.Build.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 | net8.0;net9.0
4 | false
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/AutoLoggerMessageGenerator.UnitTests.Roslyn4_11.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 4.11.0
5 | $(DefineConstants);HASH_BASED_INTERCEPTORS
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/AutoLoggerMessageGenerator.UnitTests.Roslyn4_8.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 4.8.0
5 | $(DefineConstants);PATH_BASED_INTERCEPTORS
6 |
7 |
8 |
9 |
10 |
11 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/BaseSourceGeneratorTest.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using Microsoft.CodeAnalysis;
3 | using Microsoft.CodeAnalysis.CSharp;
4 | using Microsoft.CodeAnalysis.CSharp.Syntax;
5 |
6 | namespace AutoLoggerMessageGenerator.UnitTests;
7 |
8 | internal abstract class BaseSourceGeneratorTest
9 | {
10 | public const string LoggerName = "logger";
11 | public const string Namespace = "Foo";
12 | public const string ClassName = "Test";
13 |
14 | protected static async Task<(CSharpCompilation Compilation, SyntaxTree SyntaxTree)> CompileSourceCode(
15 | string body, string additionalClassMemberDeclarations = "",
16 | bool useGlobalNamespace = false)
17 | {
18 | var sourceCode = $$"""
19 | using System;
20 | using {{Constants.DefaultLoggingNamespace}};
21 |
22 | {{(useGlobalNamespace ? string.Empty : $"namespace {Namespace};")}}
23 |
24 | public class {{ClassName}}(ILogger {{LoggerName}})
25 | {
26 | {{additionalClassMemberDeclarations}}
27 |
28 | public void Main()
29 | {
30 | {{body}}
31 | }
32 | }
33 | """;
34 |
35 | var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode, path: "path/testFile.cs");
36 |
37 | var references = AppDomain.CurrentDomain.GetAssemblies()
38 | .Where(assembly => !assembly.IsDynamic)
39 | .Select(assembly => MetadataReference.CreateFromFile(assembly.Location))
40 | .Cast()
41 | .ToList();
42 |
43 | var loggerAssemblyLocation = MetadataReference.CreateFromFile(
44 | Path.Join(AppContext.BaseDirectory, "Microsoft.Extensions.Logging.Abstractions.dll")
45 | );
46 | var buildOutputAssembly = MetadataReference.CreateFromFile(
47 | Path.Join(AppContext.BaseDirectory, "AutoLoggerMessageGenerator.BuildOutput.dll")
48 | );
49 | references.AddRange([loggerAssemblyLocation, buildOutputAssembly]);
50 |
51 | var compilation = CSharpCompilation.Create("SourceGeneratorTests",
52 | [syntaxTree],
53 | references,
54 | new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
55 |
56 | syntaxTree = compilation.SyntaxTrees.First();
57 |
58 | var diagnostics = syntaxTree.GetDiagnostics();
59 | var compilationErrors = diagnostics.Where(c => c.Severity == DiagnosticSeverity.Error).ToArray();
60 |
61 | if (compilationErrors.Any())
62 | Debugger.Launch();
63 |
64 | await Assert.That(compilationErrors).IsEmpty();
65 |
66 | return (compilation, syntaxTree);
67 | }
68 |
69 | protected static (InvocationExpressionSyntax, IMethodSymbol?, SemanticModel?) FindMethodInvocation(
70 | Compilation? compilation, SyntaxTree syntaxTree)
71 | {
72 | var invocationExpression = syntaxTree.GetRoot().DescendantNodes().OfType().First();
73 |
74 | IMethodSymbol? methodSymbol = null;
75 | SemanticModel? semanticModel = null;
76 |
77 | if (compilation is not null)
78 | {
79 | semanticModel = compilation.GetSemanticModel(syntaxTree);
80 | methodSymbol = (IMethodSymbol) semanticModel.GetSymbolInfo(invocationExpression).Symbol!;
81 | }
82 |
83 | return (invocationExpression, methodSymbol, semanticModel);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Emitters/GenericLoggerScopeExtensionsEmitterTests.Emit_ShouldGenerateValidLoggingScopeExtensionsOverrides.verified.txt:
--------------------------------------------------------------------------------
1 | //
2 | #nullable enable
3 |
4 | using System;
5 |
6 | namespace Microsoft.Extensions.Logging
7 | {
8 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("AutoLoggerMessageGenerator", "1.2.3.4")]
9 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
10 | [System.Diagnostics.DebuggerStepThrough]
11 | [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
12 | public static class GenericLoggerScopeExtensions
13 | {
14 | public static IDisposable? BeginScope(this ILogger @logger, string @message, T0 @arg0)
15 | {
16 | return Microsoft.Extensions.Logging.LoggerExtensions.BeginScope(@logger, @message, new object?[] { @arg0 });
17 | }
18 |
19 | public static IDisposable? BeginScope(this ILogger @logger, string @message, T0 @arg0, T1 @arg1)
20 | {
21 | return Microsoft.Extensions.Logging.LoggerExtensions.BeginScope(@logger, @message, new object?[] { @arg0, @arg1 });
22 | }
23 |
24 | public static IDisposable? BeginScope(this ILogger @logger, string @message, T0 @arg0, T1 @arg1, T2 @arg2)
25 | {
26 | return Microsoft.Extensions.Logging.LoggerExtensions.BeginScope(@logger, @message, new object?[] { @arg0, @arg1, @arg2 });
27 | }
28 |
29 | public static IDisposable? BeginScope(this ILogger @logger, string @message, T0 @arg0, T1 @arg1, T2 @arg2, T3 @arg3)
30 | {
31 | return Microsoft.Extensions.Logging.LoggerExtensions.BeginScope(@logger, @message, new object?[] { @arg0, @arg1, @arg2, @arg3 });
32 | }
33 |
34 | public static IDisposable? BeginScope(this ILogger @logger, string @message, T0 @arg0, T1 @arg1, T2 @arg2, T3 @arg3, T4 @arg4)
35 | {
36 | return Microsoft.Extensions.Logging.LoggerExtensions.BeginScope(@logger, @message, new object?[] { @arg0, @arg1, @arg2, @arg3, @arg4 });
37 | }
38 |
39 | public static IDisposable? BeginScope(this ILogger @logger, string @message, T0 @arg0, T1 @arg1, T2 @arg2, T3 @arg3, T4 @arg4, T5 @arg5)
40 | {
41 | return Microsoft.Extensions.Logging.LoggerExtensions.BeginScope(@logger, @message, new object?[] { @arg0, @arg1, @arg2, @arg3, @arg4, @arg5 });
42 | }
43 |
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Emitters/InterceptorAttributeEmitterTests.Emit_ShouldGenerateValidInterceptorAttribute_HashBasedInterceptor.verified.txt:
--------------------------------------------------------------------------------
1 | //
2 | #nullable enable
3 |
4 | using System;
5 |
6 | namespace System.Runtime.CompilerServices
7 | {
8 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("AutoLoggerMessageGenerator", "1.2.3.4")]
9 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
10 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
11 | internal sealed class InterceptsLocationAttribute : Attribute
12 | {
13 | public InterceptsLocationAttribute(int version, string data) {}
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Emitters/InterceptorAttributeEmitterTests.Emit_ShouldGenerateValidInterceptorAttribute_PathBasedInterceptor.verified.txt:
--------------------------------------------------------------------------------
1 | //
2 | #nullable enable
3 |
4 | using System;
5 |
6 | namespace System.Runtime.CompilerServices
7 | {
8 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("AutoLoggerMessageGenerator", "1.2.3.4")]
9 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
10 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
11 | internal sealed class InterceptsLocationAttribute : Attribute
12 | {
13 | public InterceptsLocationAttribute(string filePath, int line, int character) {}
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Emitters/InterceptorAttributeEmitterTests.cs:
--------------------------------------------------------------------------------
1 | using AutoLoggerMessageGenerator.Emitters;
2 | using AutoLoggerMessageGenerator.UnitTests.Scrubbers;
3 | using AutoLoggerMessageGenerator.UnitTests.Utilities;
4 |
5 | namespace AutoLoggerMessageGenerator.UnitTests.Emitters;
6 |
7 | internal class InterceptorAttributeEmitterTests
8 | {
9 | [Test]
10 | public async Task Emit_ShouldGenerateValidInterceptorAttribute()
11 | {
12 | var sourceCode = InterceptorAttributeEmitter.Emit();
13 | var configuration = InterceptorConfigurationUtilities.GetInterceptorConfiguration();
14 |
15 | await Verify(sourceCode)
16 | .UseTextForParameters(configuration)
17 | .AddCodeGeneratedAttributeScrubber();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Emitters/LoggerExtensionsEmitterTests.cs:
--------------------------------------------------------------------------------
1 | using AutoLoggerMessageGenerator.Emitters;
2 | using AutoLoggerMessageGenerator.UnitTests.Scrubbers;
3 |
4 | namespace AutoLoggerMessageGenerator.UnitTests.Emitters;
5 |
6 | internal class GenericLoggerExtensionsEmitterTests
7 | {
8 | [Test]
9 | public async Task Emit_ShouldGenerateValidLoggingExtensionsOverrides()
10 | {
11 | var sourceCode = GenericLoggerExtensionsEmitter.Emit();
12 | await Verify(sourceCode).AddCodeGeneratedAttributeScrubber();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Emitters/LoggerInterceptorsEmitterTests.Emit_ShouldGenerateValidLoggingExtensionsAttribute.verified.txt:
--------------------------------------------------------------------------------
1 | //
2 | #nullable enable
3 |
4 | using System;
5 |
6 | namespace Microsoft.Extensions.Logging.AutoLoggerMessage
7 | {
8 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("AutoLoggerMessageGenerator", "1.2.3.4")]
9 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
10 | [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
11 | [System.Diagnostics.DebuggerStepThrough]
12 | internal static class LoggerInterceptors
13 | {
14 | [FakeInterceptableLocation(-1, "ZmlsZSgxLDExKQ==")]
15 | public static void Log_namespace1class1_1_11(this ILogger @logger, string @message)
16 | {
17 | Microsoft.Extensions.Logging.AutoLoggerMessage.AutoLoggerMessage.Log_namespace1class1_1_11(@logger);
18 | }
19 |
20 | [FakeInterceptableLocation(-1, "ZmlsZTIoMiwyMik=")]
21 | public static void Log_namespace2class2_2_22(this ILogger @logger, string @message, int @intParam)
22 | {
23 | Microsoft.Extensions.Logging.AutoLoggerMessage.AutoLoggerMessage.Log_namespace2class2_2_22(@logger, @intParam);
24 | }
25 |
26 | [FakeInterceptableLocation(-1, "ZmlsZTMoMywzMyk=")]
27 | public static void Log_namespace3class3_3_33(this ILogger @logger, string @message, int @intParam, bool @boolParam, SomeClass @objectParam)
28 | {
29 | Microsoft.Extensions.Logging.AutoLoggerMessage.AutoLoggerMessage.Log_namespace3class3_3_33(@logger, @intParam, @boolParam, @objectParam);
30 | }
31 |
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Emitters/LoggerInterceptorsEmitterTests.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Immutable;
2 | using AutoLoggerMessageGenerator.Emitters;
3 | using AutoLoggerMessageGenerator.Models;
4 | using AutoLoggerMessageGenerator.UnitTests.Scrubbers;
5 | using static AutoLoggerMessageGenerator.Constants;
6 |
7 | namespace AutoLoggerMessageGenerator.UnitTests.Emitters;
8 |
9 | internal class LoggerInterceptorsEmitterTests
10 | {
11 | [Test]
12 | public async Task Emit_ShouldGenerateValidLoggingExtensionsAttribute()
13 | {
14 | ImmutableArray logCalls =
15 | [
16 | new LogMessageCall(
17 | Id: Guid.NewGuid(),
18 | Location: MockLogCallLocationBuilder.Build("file", 1, 11),
19 | Namespace: "namespace1",
20 | ClassName: "class1",
21 | MethodName: "name1",
22 | LogLevel: "Information",
23 | Message: "Message1",
24 | Parameters: [new CallParameter("string", MessageParameterName, CallParameterType.Message)]
25 | ),
26 | new LogMessageCall(
27 | Id: Guid.NewGuid(),
28 | Location: MockLogCallLocationBuilder.Build("file2", 2, 22),
29 | Namespace: "namespace2",
30 | ClassName: "class2",
31 | MethodName: "name2",
32 | LogLevel: "Warning",
33 | Message: "Message2",
34 | Parameters: [
35 | new CallParameter("string", MessageParameterName, CallParameterType.Message),
36 | new CallParameter("int", "@intParam", CallParameterType.Others)
37 | ]
38 | ),
39 | new LogMessageCall(
40 | Id: Guid.NewGuid(),
41 | Location: MockLogCallLocationBuilder.Build("file3", 3, 33),
42 | Namespace: "namespace3",
43 | ClassName: "class3",
44 | MethodName: "name3",
45 | LogLevel: "Error",
46 | Message: "Message3",
47 | Parameters: [
48 | new CallParameter("string", MessageParameterName, CallParameterType.Message),
49 | new CallParameter("int", "@intParam", CallParameterType.Others),
50 | new CallParameter("bool", "@boolParam", CallParameterType.Others),
51 | new CallParameter("SomeClass", "@objectParam", CallParameterType.Others, true)
52 | ]
53 | ),
54 | ];
55 |
56 | var sourceCode = LoggerInterceptorsEmitter.Emit(logCalls);
57 | await Verify(sourceCode).AddCodeGeneratedAttributeScrubber();
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Emitters/LoggerScopeExtensionsEmitterTests.cs:
--------------------------------------------------------------------------------
1 | using AutoLoggerMessageGenerator.Emitters;
2 | using AutoLoggerMessageGenerator.UnitTests.Scrubbers;
3 |
4 | namespace AutoLoggerMessageGenerator.UnitTests.Emitters;
5 |
6 | internal class GenericLoggerScopeExtensionsEmitterTests
7 | {
8 | [Test]
9 | public async Task Emit_ShouldGenerateValidLoggingScopeExtensionsOverrides()
10 | {
11 | var sourceCode = GenericLoggerScopeExtensionsEmitter.Emit();
12 | await Verify(sourceCode).AddCodeGeneratedAttributeScrubber();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Emitters/LoggerScopeInterceptorsEmitterTests.Emit_WithGivenConfiguration_ShouldGenerateValidLoggerScopeInterceptors.verified.txt:
--------------------------------------------------------------------------------
1 | //
2 | #nullable enable
3 |
4 | using System;
5 |
6 | namespace Microsoft.Extensions.Logging.AutoLoggerMessage
7 | {
8 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("AutoLoggerMessageGenerator", "1.2.3.4")]
9 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
10 | [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
11 | [System.Diagnostics.DebuggerStepThrough]
12 | internal static class LoggerScopeInterceptors
13 | {
14 | [FakeInterceptableLocation(-1, "ZmlsZSgxLDExKQ==")]
15 | public static IDisposable? LogScope_namespace1class1_1_11(this ILogger @logger, string @message)
16 | {
17 | return Microsoft.Extensions.Logging.AutoLoggerMessage.LoggerScopes.LogScope_namespace1class1_1_11(@logger);
18 | }
19 |
20 | [FakeInterceptableLocation(-1, "ZmlsZTIoMiwyMik=")]
21 | public static IDisposable? LogScope_namespace2class2_2_22(this ILogger @logger, string @message, int @intParam)
22 | {
23 | return Microsoft.Extensions.Logging.AutoLoggerMessage.LoggerScopes.LogScope_namespace2class2_2_22(@logger, @intParam);
24 | }
25 |
26 | [FakeInterceptableLocation(-1, "ZmlsZTMoMywzMyk=")]
27 | public static IDisposable? LogScope_namespace3class3_3_33(this ILogger @logger, string @message, int @intParam, bool @boolParam, SomeClass @objectParam)
28 | {
29 | return Microsoft.Extensions.Logging.AutoLoggerMessage.LoggerScopes.LogScope_namespace3class3_3_33(@logger, @intParam, @boolParam, @objectParam);
30 | }
31 |
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Emitters/LoggerScopeInterceptorsEmitterTests.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Immutable;
2 | using AutoLoggerMessageGenerator.Emitters;
3 | using AutoLoggerMessageGenerator.Models;
4 | using AutoLoggerMessageGenerator.UnitTests.Scrubbers;
5 | using static AutoLoggerMessageGenerator.Constants;
6 |
7 | namespace AutoLoggerMessageGenerator.UnitTests.Emitters;
8 |
9 | internal class LoggerScopeInterceptorsEmitterTests
10 | {
11 | [Test]
12 | public async Task Emit_WithGivenConfiguration_ShouldGenerateValidLoggerScopeInterceptors()
13 | {
14 | ImmutableArray loggerScopes =
15 | [
16 | new(
17 | Location: MockLogCallLocationBuilder.Build("file", 1, 11),
18 | Namespace: "namespace1",
19 | ClassName: "class1",
20 | MethodName: "name1",
21 | Message: "Message1",
22 | Parameters: [new CallParameter("string", MessageParameterName, CallParameterType.Message)]
23 | ),
24 | new(
25 | Location: MockLogCallLocationBuilder.Build("file2", 2, 22),
26 | Namespace: "namespace2",
27 | ClassName: "class2",
28 | MethodName: "name2",
29 | Message: "Message2",
30 | Parameters: [
31 | new CallParameter("string", MessageParameterName, CallParameterType.Message),
32 | new CallParameter("int", "@intParam", CallParameterType.Others)
33 | ]
34 | ),
35 | new(
36 | Location: MockLogCallLocationBuilder.Build("file3", 3, 33),
37 | Namespace: "namespace3",
38 | ClassName: "class3",
39 | MethodName: "name3",
40 | Message: "Message3",
41 | Parameters: [
42 | new CallParameter("string", MessageParameterName, CallParameterType.Message),
43 | new CallParameter("int", "@intParam", CallParameterType.Others),
44 | new CallParameter("bool", "@boolParam", CallParameterType.Others),
45 | new CallParameter("SomeClass", "@objectParam", CallParameterType.Others, true)
46 | ]
47 | ),
48 | ];
49 |
50 | var sourceCode = LoggerScopeInterceptorsEmitter.Emit(loggerScopes);
51 | await Verify(sourceCode).AddCodeGeneratedAttributeScrubber();
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Emitters/LoggerScopesEmitterTests.Emit_WithGivenConfiguration_ShouldGenerateValidLoggerDefineScopedFunctors.verified.txt:
--------------------------------------------------------------------------------
1 | //
2 | #nullable enable
3 |
4 | using System;
5 |
6 | namespace Microsoft.Extensions.Logging.AutoLoggerMessage
7 | {
8 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("AutoLoggerMessageGenerator", "1.2.3.4")]
9 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
10 | public static class LoggerScopes
11 | {
12 | private static readonly Func _LogScope_namespace1class1_1_11 = LoggerMessage.DefineScope("Message1");
13 | public static IDisposable? LogScope_namespace1class1_1_11(ILogger @logger)
14 | {
15 | return _LogScope_namespace1class1_1_11(@logger);
16 | }
17 |
18 | private static readonly Func _LogScope_namespace2class2_2_22 = LoggerMessage.DefineScope("Message2");
19 | public static IDisposable? LogScope_namespace2class2_2_22(ILogger @logger, int @intParam)
20 | {
21 | return _LogScope_namespace2class2_2_22(@logger, @intParam);
22 | }
23 |
24 | private static readonly Func _LogScope_namespace3class3_3_33 = LoggerMessage.DefineScope("Message3");
25 | public static IDisposable? LogScope_namespace3class3_3_33(ILogger @logger, int @intParam, bool @boolParam, SomeClass @objectParam)
26 | {
27 | return _LogScope_namespace3class3_3_33(@logger, @intParam, @boolParam, @objectParam);
28 | }
29 |
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Emitters/LoggerScopesEmitterTests.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Immutable;
2 | using AutoLoggerMessageGenerator.Emitters;
3 | using AutoLoggerMessageGenerator.Models;
4 | using AutoLoggerMessageGenerator.UnitTests.Scrubbers;
5 | using static AutoLoggerMessageGenerator.Constants;
6 |
7 | namespace AutoLoggerMessageGenerator.UnitTests.Emitters;
8 |
9 | internal class LoggerScopesEmitterTests
10 | {
11 | [Test]
12 | public async Task Emit_WithGivenConfiguration_ShouldGenerateValidLoggerDefineScopedFunctors()
13 | {
14 | ImmutableArray loggerScopes =
15 | [
16 | new(
17 | Location: MockLogCallLocationBuilder.Build("file", 1, 11),
18 | Namespace: "namespace1",
19 | ClassName: "class1",
20 | MethodName: "name1",
21 | Message: "Message1",
22 | Parameters: [new CallParameter("string", MessageParameterName, CallParameterType.Message)]
23 | ),
24 | new(
25 | Location: MockLogCallLocationBuilder.Build("file2", 2, 22),
26 | Namespace: "namespace2",
27 | ClassName: "class2",
28 | MethodName: "name2",
29 | Message: "Message2",
30 | Parameters: [
31 | new CallParameter("string", MessageParameterName, CallParameterType.Message),
32 | new CallParameter("int", "@intParam", CallParameterType.Others)
33 | ]
34 | ),
35 | new(
36 | Location: MockLogCallLocationBuilder.Build("file3", 3, 33),
37 | Namespace: "namespace3",
38 | ClassName: "class3",
39 | MethodName: "name3",
40 | Message: "Message3",
41 | Parameters: [
42 | new CallParameter("string", MessageParameterName, CallParameterType.Message),
43 | new CallParameter("int", "@intParam", CallParameterType.Others),
44 | new CallParameter("bool", "@boolParam", CallParameterType.Others),
45 | new CallParameter("SomeClass", "@objectParam", CallParameterType.Others, true)
46 | ]
47 | ),
48 | ];
49 |
50 | var sourceCode = LoggerScopesEmitter.Emit(loggerScopes);
51 | await Verify(sourceCode).AddCodeGeneratedAttributeScrubber();
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Extractors/EnclosingClassExtractorTests.cs:
--------------------------------------------------------------------------------
1 | using AutoLoggerMessageGenerator.Extractors;
2 |
3 | namespace AutoLoggerMessageGenerator.UnitTests.Extractors;
4 |
5 | internal class EnclosingClassExtractorTests : BaseSourceGeneratorTest
6 | {
7 | [Test]
8 | [Arguments(false, Namespace, ClassName)]
9 | [Arguments(true, "", ClassName)]
10 | public async Task Extract_WithLogCallAndGivenNamespace_ShouldReturnExpectedNamespaceAndClassName(
11 | bool useGlobalNamespace, string expectedNamespace, string expectedClassName)
12 | {
13 | var message = $$"""{{LoggerName}}.LogInformation("Hello world");""";
14 | var (_, syntaxTree) = await CompileSourceCode(message, useGlobalNamespace: useGlobalNamespace);
15 | var (invocationExpression, _, _) = FindMethodInvocation(null, syntaxTree);
16 |
17 | var (ns, className) = EnclosingClassExtractor.Extract(invocationExpression);
18 |
19 | await Assert.That(ns).IsEqualTo(expectedNamespace);
20 | await Assert.That(className).IsEqualTo(expectedClassName);
21 | }
22 |
23 | [Test]
24 | public async Task Extract_WithNestedClasses_ShouldReturnOuterClassName()
25 | {
26 | const string additionalDeclaration = """
27 | class Outer
28 | {
29 | class Inner
30 | {
31 | private ILogger logger;
32 | public void Log() => logger.LogInformation("Hello world");
33 | }
34 | }
35 | """;
36 | var (_, syntaxTree) = await CompileSourceCode(string.Empty, additionalDeclaration);
37 | var (invocationExpression, _, _) = FindMethodInvocation(null, syntaxTree);
38 |
39 | var (ns, className) = EnclosingClassExtractor.Extract(invocationExpression);
40 |
41 | await Assert.That(ns).IsEqualTo(Namespace);
42 | await Assert.That(className).IsEqualTo(ClassName);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Extractors/LogCallExtractorTests.Extract_WithLogMethodInvocationCode_ShouldTransformThemIntoLogCallObject_description=with parameters_sourceCode=HashBasedInterceptor.verified.txt:
--------------------------------------------------------------------------------
1 | {
2 | Id: Guid_1,
3 | Location: {
4 | FilePath: path/testFile.cs,
5 | Line: 12,
6 | Character: 16,
7 | InterceptableLocationSyntax: [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "bg8SAsBDKqh7V1TqOZu8tZkAAAB0ZXN0RmlsZS5jcw==")],
8 | Context: {
9 | Kind: SourceFile,
10 | SourceSpan: {
11 | Start: 146,
12 | Length: 59
13 | },
14 | SourceTree: {
15 | FilePath: path/testFile.cs,
16 | Length: 214,
17 | HasCompilationUnitRoot: true,
18 | Options: {
19 | LanguageVersion: CSharp12,
20 | Language: C#,
21 | DocumentationMode: Parse,
22 | Errors: null
23 | }
24 | },
25 | IsInSource: true,
26 | IsInMetadata: false
27 | }
28 | },
29 | Namespace: Foo,
30 | ClassName: Test,
31 | MethodName: LogInformation,
32 | LogLevel: Information,
33 | Message: Hello world {arg1} {arg2},
34 | Parameters: [
35 | {
36 | NativeType: global::System.String,
37 | Name: @message,
38 | Type: Message,
39 | HasPropertiesToLog: false
40 | },
41 | {
42 | NativeType: global::System.Int32,
43 | Name: @arg1,
44 | Type: Others,
45 | HasPropertiesToLog: false
46 | },
47 | {
48 | NativeType: global::System.Boolean,
49 | Name: @arg2,
50 | Type: Others,
51 | HasPropertiesToLog: false
52 | }
53 | ],
54 | GeneratedMethodName: Log_FooTest_12_16
55 | }
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Extractors/LogCallExtractorTests.Extract_WithLogMethodInvocationCode_ShouldTransformThemIntoLogCallObject_description=with parameters_sourceCode=PathBasedInterceptor.verified.txt:
--------------------------------------------------------------------------------
1 | {
2 | Id: Guid_1,
3 | Location: {
4 | FilePath: path/testFile.cs,
5 | Line: 12,
6 | Character: 16,
7 | InterceptableLocationSyntax: [System.Runtime.CompilerServices.InterceptsLocationAttribute(filePath: @"path/testFile.cs", line: 12, character: 16)],
8 | Context: {
9 | Kind: SourceFile,
10 | SourceSpan: {
11 | Start: 146,
12 | Length: 59
13 | },
14 | SourceTree: {
15 | FilePath: path/testFile.cs,
16 | Length: 214,
17 | HasCompilationUnitRoot: true,
18 | Options: {
19 | LanguageVersion: CSharp12,
20 | Language: C#,
21 | DocumentationMode: Parse,
22 | Errors: null
23 | }
24 | },
25 | IsInSource: true,
26 | IsInMetadata: false
27 | }
28 | },
29 | Namespace: Foo,
30 | ClassName: Test,
31 | MethodName: LogInformation,
32 | LogLevel: Information,
33 | Message: Hello world {arg1} {arg2},
34 | Parameters: [
35 | {
36 | NativeType: global::System.String,
37 | Name: @message,
38 | Type: Message,
39 | HasPropertiesToLog: false
40 | },
41 | {
42 | NativeType: global::System.Int32,
43 | Name: @arg1,
44 | Type: Others,
45 | HasPropertiesToLog: false
46 | },
47 | {
48 | NativeType: global::System.Boolean,
49 | Name: @arg2,
50 | Type: Others,
51 | HasPropertiesToLog: false
52 | }
53 | ],
54 | GeneratedMethodName: Log_FooTest_12_16
55 | }
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Extractors/LogCallExtractorTests.Extract_WithLogMethodInvocationCode_ShouldTransformThemIntoLogCallObject_description=without parameters_sourceCode=HashBasedInterceptor.verified.txt:
--------------------------------------------------------------------------------
1 | {
2 | Id: Guid_1,
3 | Location: {
4 | FilePath: path/testFile.cs,
5 | Line: 12,
6 | Character: 16,
7 | InterceptableLocationSyntax: [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "VSNN9b1ciRV2FhFsas3dUJkAAAB0ZXN0RmlsZS5jcw==")],
8 | Context: {
9 | Kind: SourceFile,
10 | SourceSpan: {
11 | Start: 146,
12 | Length: 36
13 | },
14 | SourceTree: {
15 | FilePath: path/testFile.cs,
16 | Length: 191,
17 | HasCompilationUnitRoot: true,
18 | Options: {
19 | LanguageVersion: CSharp12,
20 | Language: C#,
21 | DocumentationMode: Parse,
22 | Errors: null
23 | }
24 | },
25 | IsInSource: true,
26 | IsInMetadata: false
27 | }
28 | },
29 | Namespace: Foo,
30 | ClassName: Test,
31 | MethodName: LogInformation,
32 | LogLevel: Information,
33 | Message: Hello world,
34 | Parameters: [
35 | {
36 | NativeType: global::System.String,
37 | Name: @message,
38 | Type: Message,
39 | HasPropertiesToLog: false
40 | }
41 | ],
42 | GeneratedMethodName: Log_FooTest_12_16
43 | }
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Extractors/LogCallExtractorTests.Extract_WithLogMethodInvocationCode_ShouldTransformThemIntoLogCallObject_description=without parameters_sourceCode=PathBasedInterceptor.verified.txt:
--------------------------------------------------------------------------------
1 | {
2 | Id: Guid_1,
3 | Location: {
4 | FilePath: path/testFile.cs,
5 | Line: 12,
6 | Character: 16,
7 | InterceptableLocationSyntax: [System.Runtime.CompilerServices.InterceptsLocationAttribute(filePath: @"path/testFile.cs", line: 12, character: 16)],
8 | Context: {
9 | Kind: SourceFile,
10 | SourceSpan: {
11 | Start: 146,
12 | Length: 36
13 | },
14 | SourceTree: {
15 | FilePath: path/testFile.cs,
16 | Length: 191,
17 | HasCompilationUnitRoot: true,
18 | Options: {
19 | LanguageVersion: CSharp12,
20 | Language: C#,
21 | DocumentationMode: Parse,
22 | Errors: null
23 | }
24 | },
25 | IsInSource: true,
26 | IsInMetadata: false
27 | }
28 | },
29 | Namespace: Foo,
30 | ClassName: Test,
31 | MethodName: LogInformation,
32 | LogLevel: Information,
33 | Message: Hello world,
34 | Parameters: [
35 | {
36 | NativeType: global::System.String,
37 | Name: @message,
38 | Type: Message,
39 | HasPropertiesToLog: false
40 | }
41 | ],
42 | GeneratedMethodName: Log_FooTest_12_16
43 | }
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Extractors/LogCallExtractorTests.cs:
--------------------------------------------------------------------------------
1 | using AutoLoggerMessageGenerator.Extractors;
2 | using AutoLoggerMessageGenerator.UnitTests.Utilities;
3 |
4 | namespace AutoLoggerMessageGenerator.UnitTests.Extractors;
5 |
6 | internal class LogCallExtractorTests : BaseSourceGeneratorTest
7 | {
8 | [Test]
9 | [Arguments("without parameters", $$"""{{LoggerName}}.LogInformation("Hello world");""", true)]
10 | [Arguments("with parameters", $$"""{{LoggerName}}.LogInformation("Hello world {arg1} {arg2}", 1, true);""", true)]
11 | [Arguments("with only null passed", $$"""{{LoggerName}}.LogInformation(null);""", false)]
12 | [Arguments("with parameter count mismatch", $$"""{{LoggerName}}.LogInformation("Hello world {arg1}", 1, true);""", false)]
13 | public async Task Extract_WithLogMethodInvocationCode_ShouldTransformThemIntoLogCallObject(string description, string sourceCode, bool isValidCall)
14 | {
15 | var (compilation, syntaxTree) = await CompileSourceCode(sourceCode);
16 | var (invocationExpression, methodSymbol, semanticModel) = FindMethodInvocation(compilation, syntaxTree);
17 |
18 | var logCall = LogCallExtractor.Extract(methodSymbol!, invocationExpression, semanticModel!);
19 |
20 | if (isValidCall)
21 | {
22 | var configuration = InterceptorConfigurationUtilities.GetInterceptorConfiguration();
23 | await Verify(logCall).UseParameters(description, configuration);
24 | }
25 | else
26 | {
27 | await Assert.That(logCall.HasValue).IsFalse();
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Extractors/LogLevelExtractorTests.cs:
--------------------------------------------------------------------------------
1 | using AutoLoggerMessageGenerator.Extractors;
2 | using Microsoft.Extensions.Logging;
3 |
4 | namespace AutoLoggerMessageGenerator.UnitTests.Extractors;
5 |
6 | internal class LogLevelExtractorTests : BaseSourceGeneratorTest
7 | {
8 | [Test]
9 | [Arguments("LogTrace(default)", nameof(LogLevel.Trace))]
10 | [Arguments("LogDebug(default)", nameof(LogLevel.Debug))]
11 | [Arguments("LogInformation(default)", nameof(LogLevel.Information))]
12 | [Arguments("LogWarning(default)", nameof(LogLevel.Warning))]
13 | [Arguments("LogError(default)", nameof(LogLevel.Error))]
14 | [Arguments("LogCritical(default)", nameof(LogLevel.Critical))]
15 |
16 | [Arguments("Log(LogLevel.Trace, default)", nameof(LogLevel.Trace))]
17 | [Arguments("Log(LogLevel.Debug, default)", nameof(LogLevel.Debug))]
18 | [Arguments("Log(LogLevel.Information, default)", nameof(LogLevel.Information))]
19 | [Arguments("Log(LogLevel.Warning, default)", nameof(LogLevel.Warning))]
20 | [Arguments("Log(LogLevel.Error, default)", nameof(LogLevel.Error))]
21 | [Arguments("Log(LogLevel.Critical, default)", nameof(LogLevel.Critical))]
22 |
23 | [Arguments("AnyOtherMethod(default)", null)]
24 | public async Task Extract_WithGivenMethodCall_ShouldReturnExpectedLogLevel(string methodCall, string? expectedLogLevel)
25 | {
26 | var (compilation, syntaxTree) = await CompileSourceCode($"{LoggerName}.{methodCall};");
27 | var (invocationExpression, methodSymbol, _) = FindMethodInvocation(compilation, syntaxTree);
28 |
29 | var result = LogLevelExtractor.Extract(methodSymbol!, invocationExpression);
30 |
31 | await Assert.That(result).IsEqualTo(expectedLogLevel);
32 | }
33 |
34 | [Test]
35 | public async Task Extract_WithNotConstantLogLevel_ShouldReturnNull()
36 | {
37 | var sourceCode = """
38 | var logLevel = DateTime.Now.Ticks % 2 == 0 ? LogLevel.Warning : LogLevel.Error;
39 | Log(logLevel, default);
40 | """;
41 | var (compilation, syntaxTree) = await CompileSourceCode(sourceCode);
42 | var (invocationExpression, methodSymbol, _) = FindMethodInvocation(compilation, syntaxTree);
43 |
44 | var result = LogLevelExtractor.Extract(methodSymbol!, invocationExpression);
45 |
46 | await Assert.That(result).IsEqualTo(null);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Extractors/LoggerScopeCallExtractorTests.Extract_WithGivenLoggerScope_ShouldTransformIntoLoggerScopeCallObject_description=with parameters_sourceCode=HashBasedInterceptor.verified.txt:
--------------------------------------------------------------------------------
1 | {
2 | Location: {
3 | FilePath: path/testFile.cs,
4 | Line: 12,
5 | Character: 16,
6 | InterceptableLocationSyntax: [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "nedh6WI/ifg2J0oGdnNJkZkAAAB0ZXN0RmlsZS5jcw==")],
7 | Context: {
8 | Kind: SourceFile,
9 | SourceSpan: {
10 | Start: 146,
11 | Length: 55
12 | },
13 | SourceTree: {
14 | FilePath: path/testFile.cs,
15 | Length: 210,
16 | HasCompilationUnitRoot: true,
17 | Options: {
18 | LanguageVersion: CSharp12,
19 | Language: C#,
20 | DocumentationMode: Parse,
21 | Errors: null
22 | }
23 | },
24 | IsInSource: true,
25 | IsInMetadata: false
26 | }
27 | },
28 | Namespace: Foo,
29 | ClassName: Test,
30 | MethodName: BeginScope,
31 | Message: Hello world {arg1} {arg2},
32 | Parameters: [
33 | {
34 | NativeType: global::System.String,
35 | Name: @message,
36 | Type: Message,
37 | HasPropertiesToLog: false
38 | },
39 | {
40 | NativeType: global::System.Int32,
41 | Name: @arg1,
42 | Type: Others,
43 | HasPropertiesToLog: false
44 | },
45 | {
46 | NativeType: global::System.Boolean,
47 | Name: @arg2,
48 | Type: Others,
49 | HasPropertiesToLog: false
50 | }
51 | ],
52 | GeneratedMethodName: LogScope_FooTest_12_16
53 | }
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Extractors/LoggerScopeCallExtractorTests.Extract_WithGivenLoggerScope_ShouldTransformIntoLoggerScopeCallObject_description=with parameters_sourceCode=PathBasedInterceptor.verified.txt:
--------------------------------------------------------------------------------
1 | {
2 | Location: {
3 | FilePath: path/testFile.cs,
4 | Line: 12,
5 | Character: 16,
6 | InterceptableLocationSyntax: [System.Runtime.CompilerServices.InterceptsLocationAttribute(filePath: @"path/testFile.cs", line: 12, character: 16)],
7 | Context: {
8 | Kind: SourceFile,
9 | SourceSpan: {
10 | Start: 146,
11 | Length: 55
12 | },
13 | SourceTree: {
14 | FilePath: path/testFile.cs,
15 | Length: 210,
16 | HasCompilationUnitRoot: true,
17 | Options: {
18 | LanguageVersion: CSharp12,
19 | Language: C#,
20 | DocumentationMode: Parse,
21 | Errors: null
22 | }
23 | },
24 | IsInSource: true,
25 | IsInMetadata: false
26 | }
27 | },
28 | Namespace: Foo,
29 | ClassName: Test,
30 | MethodName: BeginScope,
31 | Message: Hello world {arg1} {arg2},
32 | Parameters: [
33 | {
34 | NativeType: global::System.String,
35 | Name: @message,
36 | Type: Message,
37 | HasPropertiesToLog: false
38 | },
39 | {
40 | NativeType: global::System.Int32,
41 | Name: @arg1,
42 | Type: Others,
43 | HasPropertiesToLog: false
44 | },
45 | {
46 | NativeType: global::System.Boolean,
47 | Name: @arg2,
48 | Type: Others,
49 | HasPropertiesToLog: false
50 | }
51 | ],
52 | GeneratedMethodName: LogScope_FooTest_12_16
53 | }
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Extractors/LoggerScopeCallExtractorTests.cs:
--------------------------------------------------------------------------------
1 | using AutoLoggerMessageGenerator.Extractors;
2 | using AutoLoggerMessageGenerator.UnitTests.Utilities;
3 |
4 | namespace AutoLoggerMessageGenerator.UnitTests.Extractors;
5 |
6 | internal class LoggerScopeCallExtractorTests : BaseSourceGeneratorTest
7 | {
8 | [Test]
9 | [Arguments("with parameters", $$"""{{LoggerName}}.BeginScope("Hello world {arg1} {arg2}", 1, true);""", true)]
10 | [Arguments("with null passed", $"""{LoggerName}.BeginScope(null);""", false)]
11 | [Arguments("with parameter mismatch", $$"""{{LoggerName}}.BeginScope("Hello world {arg1}", 1, true);""", false)]
12 | public async Task Extract_WithGivenLoggerScope_ShouldTransformIntoLoggerScopeCallObject(string description, string sourceCode, bool isValidCall)
13 | {
14 | var (compilation, syntaxTree) = await CompileSourceCode(sourceCode);
15 | var (invocationExpression, methodSymbol, semanticModel) = FindMethodInvocation(compilation, syntaxTree);
16 |
17 | var loggerScope = LoggerScopeCallExtractor.Extract(methodSymbol!, invocationExpression, semanticModel!);
18 |
19 | if (isValidCall)
20 | {
21 | var configuration = InterceptorConfigurationUtilities.GetInterceptorConfiguration();
22 | await Verify(loggerScope).UseParameters(description, configuration);
23 | }
24 | else
25 | {
26 | await Assert.That(loggerScope.HasValue).IsFalse();
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Extractors/MessageParameterNamesExtractorTests.cs:
--------------------------------------------------------------------------------
1 | using AutoLoggerMessageGenerator.Extractors;
2 |
3 | namespace AutoLoggerMessageGenerator.UnitTests.Extractors;
4 |
5 | internal class MessageParameterNamesExtractorTests
6 | {
7 | [Test]
8 | [Arguments("Hello world {1}, {2}!", new[] { "1", "2" })]
9 | [Arguments("{param1} and {param2} are here", new[] { "param1", "param2" })]
10 | [Arguments("No parameters in this string", new string[0])]
11 | [Arguments("Edge case with empty braces {}", new[] { "" })]
12 | [Arguments("{0}{1}{2}{3}", new[] { "0", "1", "2", "3" })]
13 | [Arguments("{a} mixed with text {b}", new[] { "a", "b" })]
14 | [Arguments("double {{escaped braces}}", new[] {"{escaped braces"})]
15 | [Arguments("{name1}-{name2}-{name3}", new[] { "name1", "name2", "name3" })]
16 | [Arguments("Duplicate {param} and {param} again", new[] { "param", "param" })]
17 | [Arguments("", new string[0])]
18 | [Arguments(null, new string[0])]
19 | public async Task Extract_WithGivenMessage_ShouldReturnGivenMessageParameterNames(string? message, params string[] expectedMessageParameters)
20 | {
21 | var result = MessageParameterNamesExtractor.Extract(message);
22 | await Assert.That(result).IsEquivalentTo(expectedMessageParameters);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/MethodSpecificityRules/InstanceCallVsExtensionCallTests.cs:
--------------------------------------------------------------------------------
1 | namespace AutoLoggerMessageGenerator.UnitTests;
2 |
3 | internal class InstanceCallVsExtensionCallTests
4 | {
5 | [Test]
6 | public async Task BeginScope_WithOnlyMessageParameter_InstanceCallShouldBePrioritized()
7 | {
8 | ILogger logger = new Logger();
9 | var result = logger.BeginScope("Some scope");
10 |
11 | await Assert.That(result).IsEqualTo(CallSource.Instance);
12 | }
13 | }
14 |
15 | interface ILogger
16 | {
17 | CallSource BeginScope(T _);
18 | }
19 | class Logger : ILogger
20 | {
21 | public CallSource BeginScope(T _) => CallSource.Instance;
22 | }
23 |
24 | static class LoggerExtensions
25 | {
26 | public static CallSource BeginScope(this ILogger _, string __) => CallSource.Extension;
27 | }
28 |
29 | enum CallSource { Instance, Extension }
30 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Scrubbers/GeneratedCodeAttributeScrubber.cs:
--------------------------------------------------------------------------------
1 | using System.Text.RegularExpressions;
2 |
3 | namespace AutoLoggerMessageGenerator.UnitTests.Scrubbers;
4 |
5 | public static partial class GeneratedCodeAttributeScrubber
6 | {
7 | [GeneratedRegex($""""{nameof(AutoLoggerMessageGenerator)}", "\d+.\d+.\d+.\d?"""")]
8 | private static partial Regex GeneratedCodeAttributeVersionRegex();
9 |
10 | public static SettingsTask AddCodeGeneratedAttributeScrubber(this SettingsTask task)
11 | {
12 | task.ScrubLinesWithReplace(line => GeneratedCodeAttributeVersionRegex().Replace(
13 | line,
14 | $""""{nameof(AutoLoggerMessageGenerator)}", "1.2.3.4""""));
15 | return task;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Utilities/IdentifierHelperTests.cs:
--------------------------------------------------------------------------------
1 | using AutoLoggerMessageGenerator.Utilities;
2 | using TUnit.Assertions.AssertConditions.Throws;
3 |
4 | namespace AutoLoggerMessageGenerator.UnitTests.Utilities;
5 |
6 | internal class IdentifierHelperTests
7 | {
8 | [Test]
9 | [Arguments("ValidName", "ValidName")]
10 | [Arguments("_Valid__Name___", "_Valid__Name___")]
11 | [Arguments("🔥Invalid🔥Emoji🔥", "__Invalid__Emoji__")]
12 | [Arguments("123Name", "_123Name")]
13 | [Arguments("Name#With$Special%Chars", "Name_With_Special_Chars")]
14 | public async Task ToValidCSharpMethodName_ShouldSanitizeCorrectly(string input, string expected)
15 | {
16 | var result = IdentifierHelper.ToValidCSharpMethodName(input);
17 | await Assert.That(result).IsEqualTo(expected);
18 | }
19 |
20 | [Test]
21 | [Arguments(null)]
22 | [Arguments("")]
23 | public async Task ToValidCSharpMethodName_ShouldThrow_WhenInputIsNull(string? input)
24 | {
25 | string Action() => IdentifierHelper.ToValidCSharpMethodName(input);
26 | await Assert.That(Action).ThrowsExactly();
27 | }
28 |
29 | [Test]
30 | [Arguments("validName", true)]
31 | [Arguments("_validName", true)]
32 | [Arguments("ValidName123", true)]
33 | [Arguments("a", true)]
34 | [Arguments("invalid name", false)]
35 | [Arguments("123Invalid", false)]
36 | [Arguments("invalid-name", false)]
37 | [Arguments("e.invalid", false)]
38 | [Arguments("i🔥nvalid", false)]
39 | [Arguments("", false)]
40 | [Arguments(null, false)]
41 | public async Task IsValidCSharpParameterName_WithGivenParameterName_ShouldReturnTheGivenResult(string? parameterName,
42 | bool isValid)
43 | {
44 | var result = IdentifierHelper.IsValidCSharpParameterName(parameterName);
45 | await Assert.That(result).IsEqualTo(isValid);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Utilities/InterceptorConfigurationUtilities.cs:
--------------------------------------------------------------------------------
1 | namespace AutoLoggerMessageGenerator.UnitTests.Utilities;
2 |
3 | public static class InterceptorConfigurationUtilities
4 | {
5 | public static string GetInterceptorConfiguration()
6 | {
7 | #if PATH_BASED_INTERCEPTORS
8 | return "PathBasedInterceptor";
9 | #elif HASH_BASED_INTERCEPTORS
10 | return "HashBasedInterceptor";
11 | #else
12 | throw new NotSupportedException("Unknown interceptors configuration");
13 | #endif
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Utilities/MockLogCallLocationBuilder.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using AutoLoggerMessageGenerator.Models;
3 | using Microsoft.CodeAnalysis;
4 |
5 | namespace AutoLoggerMessageGenerator.UnitTests;
6 |
7 | internal static class MockLogCallLocationBuilder
8 | {
9 | internal static CallLocation Build(string filePath, int line, int character)
10 | {
11 | ArgumentException.ThrowIfNullOrEmpty(filePath);
12 |
13 | const int version = -1;
14 | var location = $"{filePath}({line},{character})";
15 | var data = Convert.ToBase64String(Encoding.UTF8.GetBytes(location));
16 | var interceptableLocationSyntax = $"""[FakeInterceptableLocation({version}, "{data}")]""";
17 |
18 | return new CallLocation(filePath, line, character, interceptableLocationSyntax, Location.None);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/Utilities/ReservedParameterNameResolverTests.cs:
--------------------------------------------------------------------------------
1 | using AutoLoggerMessageGenerator.Utilities;
2 |
3 | namespace AutoLoggerMessageGenerator.UnitTests.Utilities;
4 |
5 | internal class ReservedParameterNameResolverTests
6 | {
7 | [Test]
8 | [Arguments(0, new[] {"a","b","c"})]
9 | [Arguments(1, new[] {"@message","b","c"})]
10 | [Arguments(2, new[] {"@message_","@message","c"})]
11 | [Arguments(4, new[] {"@message", "@message___", "@message__"})]
12 | [Arguments(4, new[] {"@message", "@exception___", "@eventId_"})]
13 | public async Task WithGivenTemplateParameterNames_ShouldCalculateExpectedPrefixLength(int expectedPrefixLength, params string[] templateParameterNames)
14 | {
15 | var actualPrefixLength = ReservedParameterNameResolver.GenerateUniqueIdentifierSuffix(templateParameterNames);
16 | await Assert.That(actualPrefixLength.Length).IsEqualTo(expectedPrefixLength);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/VirtualLoggerMessage/LogCallExtractorTests.cs:
--------------------------------------------------------------------------------
1 | using AutoLoggerMessageGenerator.Models;
2 | using AutoLoggerMessageGenerator.VirtualLoggerMessage;
3 | using Microsoft.CodeAnalysis.CSharp.Syntax;
4 |
5 | namespace AutoLoggerMessageGenerator.UnitTests.VirtualLoggerMessage;
6 |
7 | internal class LogMessageCallLocationMapTests : BaseSourceGeneratorTest
8 | {
9 | [Test]
10 | public async Task MapBack_GivenMethodName_ShouldReturnLastLocationBeforeTheCurrentNode()
11 | {
12 | var logCall1 = new LogMessageCall { Id = Guid.NewGuid() };
13 | var logCall2 = new LogMessageCall { Id = Guid.NewGuid() };
14 | var logCall3 = new LogMessageCall { Id = Guid.NewGuid() };
15 |
16 | var additionalDeclarations = $$"""
17 | public void Method0(){}
18 |
19 | {{LogMessageCallLocationMap.CreateMapping(logCall1)}}
20 | public void Method1(){}
21 |
22 | {{LogMessageCallLocationMap.CreateMapping(logCall2)}}
23 | public void Method2(){}
24 |
25 | {{LogMessageCallLocationMap.CreateMapping(logCall3)}}
26 | public void Method3(){}
27 | """;
28 | var (_, syntaxTree) = await CompileSourceCode(string.Empty, additionalDeclarations);
29 |
30 | var methodDeclarations = syntaxTree.GetRoot().DescendantNodes()
31 | .OfType()
32 | .ToArray();
33 |
34 | var method0Declaration = methodDeclarations.Single(m => m.Identifier.Text == "Method0");
35 | var method1Declaration = methodDeclarations.Single(m => m.Identifier.Text == "Method1");
36 | var method2Declaration = methodDeclarations.Single(m => m.Identifier.Text == "Method2");
37 | var method3Declaration = methodDeclarations.Single(m => m.Identifier.Text == "Method3");
38 |
39 | await Assert.That(LogMessageCallLocationMap.TryMapBack(syntaxTree.GetText(), method0Declaration.GetLocation(), out var logCallId0))
40 | .IsFalse();
41 |
42 | await Assert.That(LogMessageCallLocationMap.TryMapBack(syntaxTree.GetText(), method1Declaration.GetLocation(), out var logCallId1))
43 | .IsTrue();
44 | await Assert.That(logCallId1).IsEqualTo(logCall1.Id);
45 |
46 | await Assert.That(LogMessageCallLocationMap.TryMapBack(syntaxTree.GetText(), method2Declaration.GetLocation(), out var logCallId2))
47 | .IsTrue();
48 | await Assert.That(logCallId2).IsEqualTo(logCall2.Id);
49 |
50 | await Assert.That(LogMessageCallLocationMap.TryMapBack(syntaxTree.GetText(), method3Declaration.GetLocation(), out var logCallId3))
51 | .IsTrue();
52 | await Assert.That(logCallId3).IsEqualTo(logCall3.Id);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/VirtualLoggerMessage/VirtualLoggerMessageClassBuilderTests.Build_WithDifferentConfiguration_ShouldReturnLegitLoggerMessageDeclaration_all_disabled.verified.txt:
--------------------------------------------------------------------------------
1 | namespace Microsoft.Extensions.Logging.AutoLoggerMessage
2 | {
3 | static partial class AutoLoggerMessage
4 | {
5 | // : Guid_1
6 | [Microsoft.Extensions.Logging.LoggerMessageAttribute(Level = Microsoft.Extensions.Logging.LogLevel.Critical, Message = "Hello, World!", SkipEnabledCheck = false)]
7 | internal static partial void Log_SomeNamespaceSomeClass_2_22(Microsoft.Extensions.Logging.ILogger Logger, int @intParam, string @stringParam, bool @boolParam, SomeClass @classParam, SomeStruct @structParam);
8 | // : Guid_2
9 | [Microsoft.Extensions.Logging.LoggerMessageAttribute(Level = Microsoft.Extensions.Logging.LogLevel.Trace, Message = "Goodbye, World!", SkipEnabledCheck = false)]
10 | internal static partial void Log_SomeNamespaceSomeClass_3_33(Microsoft.Extensions.Logging.ILogger Logger, int @intParam, string @stringParam, bool @boolParam, SomeClass @classParam, SomeStruct @structParam);
11 | }
12 | }
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/VirtualLoggerMessage/VirtualLoggerMessageClassBuilderTests.Build_WithDifferentConfiguration_ShouldReturnLegitLoggerMessageDeclaration_all_enabled.verified.txt:
--------------------------------------------------------------------------------
1 | namespace Microsoft.Extensions.Logging.AutoLoggerMessage
2 | {
3 | static partial class AutoLoggerMessage
4 | {
5 | // : Guid_1
6 | [Microsoft.Extensions.Logging.LoggerMessageAttribute(Level = Microsoft.Extensions.Logging.LogLevel.Critical, Message = "Hello, World!", SkipEnabledCheck = true)]
7 | internal static partial void Log_SomeNamespaceSomeClass_2_22(Microsoft.Extensions.Logging.ILogger Logger, int @intParam, string @stringParam, bool @boolParam, [Microsoft.Extensions.Logging.LogPropertiesAttribute(OmitReferenceName = true, SkipNullProperties = true, Transitive = true)] SomeClass @classParam, [Microsoft.Extensions.Logging.LogPropertiesAttribute(OmitReferenceName = true, SkipNullProperties = true, Transitive = true)] SomeStruct @structParam);
8 | // : Guid_2
9 | [Microsoft.Extensions.Logging.LoggerMessageAttribute(Level = Microsoft.Extensions.Logging.LogLevel.Trace, Message = "Goodbye, World!", SkipEnabledCheck = true)]
10 | internal static partial void Log_SomeNamespaceSomeClass_3_33(Microsoft.Extensions.Logging.ILogger Logger, int @intParam, string @stringParam, bool @boolParam, [Microsoft.Extensions.Logging.LogPropertiesAttribute(OmitReferenceName = true, SkipNullProperties = true, Transitive = true)] SomeClass @classParam, [Microsoft.Extensions.Logging.LogPropertiesAttribute(OmitReferenceName = true, SkipNullProperties = true, Transitive = true)] SomeStruct @structParam);
11 | }
12 | }
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/VirtualLoggerMessage/VirtualLoggerMessageClassBuilderTests.Build_WithDifferentConfiguration_ShouldReturnLegitLoggerMessageDeclaration_only_telemetry_enabled.verified.txt:
--------------------------------------------------------------------------------
1 | namespace Microsoft.Extensions.Logging.AutoLoggerMessage
2 | {
3 | static partial class AutoLoggerMessage
4 | {
5 | // : Guid_1
6 | [Microsoft.Extensions.Logging.LoggerMessageAttribute(Level = Microsoft.Extensions.Logging.LogLevel.Critical, Message = "Hello, World!", SkipEnabledCheck = false)]
7 | internal static partial void Log_SomeNamespaceSomeClass_2_22(Microsoft.Extensions.Logging.ILogger Logger, int @intParam, string @stringParam, bool @boolParam, [Microsoft.Extensions.Logging.LogPropertiesAttribute(OmitReferenceName = false, SkipNullProperties = false)] SomeClass @classParam, [Microsoft.Extensions.Logging.LogPropertiesAttribute(OmitReferenceName = false, SkipNullProperties = false)] SomeStruct @structParam);
8 | // : Guid_2
9 | [Microsoft.Extensions.Logging.LoggerMessageAttribute(Level = Microsoft.Extensions.Logging.LogLevel.Trace, Message = "Goodbye, World!", SkipEnabledCheck = false)]
10 | internal static partial void Log_SomeNamespaceSomeClass_3_33(Microsoft.Extensions.Logging.ILogger Logger, int @intParam, string @stringParam, bool @boolParam, [Microsoft.Extensions.Logging.LogPropertiesAttribute(OmitReferenceName = false, SkipNullProperties = false)] SomeClass @classParam, [Microsoft.Extensions.Logging.LogPropertiesAttribute(OmitReferenceName = false, SkipNullProperties = false)] SomeStruct @structParam);
11 | }
12 | }
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/VirtualLoggerMessage/VirtualLoggerMessageClassBuilderTests.Build_WithDifferentConfiguration_ShouldReturnLegitLoggerMessageDeclaration_telemetry_disabled.verified.txt:
--------------------------------------------------------------------------------
1 | namespace Microsoft.Extensions.Logging.AutoLoggerMessage
2 | {
3 | static partial class AutoLoggerMessage
4 | {
5 | // : Guid_1
6 | [Microsoft.Extensions.Logging.LoggerMessageAttribute(Level = Microsoft.Extensions.Logging.LogLevel.Critical, Message = "Hello, World!", SkipEnabledCheck = true)]
7 | internal static partial void Log_SomeNamespaceSomeClass_2_22(Microsoft.Extensions.Logging.ILogger Logger, int @intParam, string @stringParam, bool @boolParam, SomeClass @classParam, SomeStruct @structParam);
8 | // : Guid_2
9 | [Microsoft.Extensions.Logging.LoggerMessageAttribute(Level = Microsoft.Extensions.Logging.LogLevel.Trace, Message = "Goodbye, World!", SkipEnabledCheck = true)]
10 | internal static partial void Log_SomeNamespaceSomeClass_3_33(Microsoft.Extensions.Logging.ILogger Logger, int @intParam, string @stringParam, bool @boolParam, SomeClass @classParam, SomeStruct @structParam);
11 | }
12 | }
--------------------------------------------------------------------------------
/tests/AutoLoggerMessageGenerator.UnitTests/VirtualLoggerMessage/VirtualLoggerMessageClassBuilderTests.Build_WithEscapeSequences_ShouldBuildAsItIs.verified.txt:
--------------------------------------------------------------------------------
1 | namespace Microsoft.Extensions.Logging.AutoLoggerMessage
2 | {
3 | static partial class AutoLoggerMessage
4 | {
5 | // : Guid_1
6 | [Microsoft.Extensions.Logging.LoggerMessageAttribute(Level = Microsoft.Extensions.Logging.LogLevel.Trace, Message = "All characters should be passed as a string literal expression: \n\r\t", SkipEnabledCheck = false)]
7 | internal static partial void Log_SomeNamespaceSomeClass_3_33(Microsoft.Extensions.Logging.ILogger Logger, int @intParam, string @stringParam, bool @boolParam, SomeClass @classParam, SomeStruct @structParam);
8 | }
9 | }
--------------------------------------------------------------------------------