├── .gitattributes ├── .gitignore ├── Github └── serilog-configuration-refactoring.gif ├── LICENSE ├── README.md ├── SerilogAnalyzer.sln └── SerilogAnalyzer ├── SerilogAnalyzer.Test ├── AnalyzerTests.cs ├── ConvertMessageTemplateAnalyzerTests.cs ├── CorrectLoggerContextTests.cs ├── DestructuringHintTests.cs ├── Helpers │ ├── CodeFixVerifier.Helper.cs │ ├── DiagnosticResult.cs │ └── DiagnosticVerifier.Helper.cs ├── PascalCaseAnalyzerTests.cs ├── Properties │ └── AssemblyInfo.cs ├── RefactoringTests.cs ├── SerilogAnalyzer.Test.csproj ├── Verifiers │ ├── CodeFixVerifier.cs │ ├── CodeRefactoringVerifier.cs │ └── DiagnosticVerifier.cs └── packages.config ├── SerilogAnalyzer.Vsix ├── SerilogAnalyzer.Vsix.csproj ├── analyzer-200px.png ├── analyzer-32px.png └── source.extension.vsixmanifest └── SerilogAnalyzer ├── AnalyzingMessageTemplateParser.cs ├── CodeFixProvider.cs ├── ConfigurationModel.cs ├── ConvertToMessageTemplateCodeRefactoringProvider.StringConcat.cs ├── ConvertToMessageTemplateCodeRefactoringProvider.StringFormat.cs ├── ConvertToMessageTemplateCodeRefactoringProvider.StringInterpolation.cs ├── ConvertToMessageTemplateCodeRefactoringProvider.cs ├── CorrectLoggerContextCodeFixProvider.cs ├── DestructuringHintCodeFixProvider.cs ├── Diagnostic.nuspec ├── DiagnosticAnalyzer.cs ├── Extensions.cs ├── MessageTemplateToken.cs ├── ObjectDisplay.cs ├── Properties └── AssemblyInfo.cs ├── PropertyBindingAnalyzer.cs ├── PropertyToken.cs ├── ReadMe.txt ├── Resources.Designer.cs ├── Resources.resx ├── RoslynHelper.cs ├── SerilogAnalyzer.csproj ├── SerilogAnalyzerPascalCaseCodeFixProvider.cs ├── ShowConfigCodeRefactoringProvider.cs ├── SourceArgument.cs ├── packages.config └── tools ├── install.ps1 └── uninstall.ps1 /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.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 | [Xx]64/ 19 | [Xx]86/ 20 | [Bb]uild/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | 85 | # Visual Studio profiler 86 | *.psess 87 | *.vsp 88 | *.vspx 89 | *.sap 90 | 91 | # TFS 2012 Local Workspace 92 | $tf/ 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | *.DotSettings.user 101 | 102 | # JustCode is a .NET coding add-in 103 | .JustCode 104 | 105 | # TeamCity is a build add-in 106 | _TeamCity* 107 | 108 | # DotCover is a Code Coverage Tool 109 | *.dotCover 110 | 111 | # NCrunch 112 | _NCrunch_* 113 | .*crunch*.local.xml 114 | nCrunchTemp_* 115 | 116 | # MightyMoose 117 | *.mm.* 118 | AutoTest.Net/ 119 | 120 | # Web workbench (sass) 121 | .sass-cache/ 122 | 123 | # Installshield output folder 124 | [Ee]xpress/ 125 | 126 | # DocProject is a documentation generator add-in 127 | DocProject/buildhelp/ 128 | DocProject/Help/*.HxT 129 | DocProject/Help/*.HxC 130 | DocProject/Help/*.hhc 131 | DocProject/Help/*.hhk 132 | DocProject/Help/*.hhp 133 | DocProject/Help/Html2 134 | DocProject/Help/html 135 | 136 | # Click-Once directory 137 | publish/ 138 | 139 | # Publish Web Output 140 | *.[Pp]ublish.xml 141 | *.azurePubxml 142 | 143 | # TODO: Un-comment the next line if you do not want to checkin 144 | # your web deploy settings because they may include unencrypted 145 | # passwords 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # NuGet Packages 150 | *.nupkg 151 | # The packages folder can be ignored because of Package Restore 152 | **/packages/* 153 | # except build/, which is used as an MSBuild target. 154 | !**/packages/build/ 155 | # Uncomment if necessary however generally it will be regenerated when needed 156 | #!**/packages/repositories.config 157 | # NuGet v3's project.json files produces more ignoreable files 158 | *.nuget.props 159 | *.nuget.targets 160 | 161 | # Microsoft Azure Build Output 162 | csx/ 163 | *.build.csdef 164 | 165 | # Microsoft Azure Emulator 166 | ecf/ 167 | rcf/ 168 | 169 | # Microsoft Azure ApplicationInsights config file 170 | ApplicationInsights.config 171 | 172 | # Windows Store app package directory 173 | AppPackages/ 174 | BundleArtifacts/ 175 | 176 | # Visual Studio cache files 177 | # files ending in .cache can be ignored 178 | *.[Cc]ache 179 | # but keep track of directories ending in .cache 180 | !*.[Cc]ache/ 181 | 182 | # Others 183 | ClientBin/ 184 | [Ss]tyle[Cc]op.* 185 | ~$* 186 | *~ 187 | *.dbmdl 188 | *.dbproj.schemaview 189 | *.pfx 190 | *.publishsettings 191 | node_modules/ 192 | orleans.codegen.cs 193 | 194 | # RIA/Silverlight projects 195 | Generated_Code/ 196 | 197 | # Backup & report files from converting an old project file 198 | # to a newer Visual Studio version. Backup files are not needed, 199 | # because we have git ;-) 200 | _UpgradeReport_Files/ 201 | Backup*/ 202 | UpgradeLog*.XML 203 | UpgradeLog*.htm 204 | 205 | # SQL Server files 206 | *.mdf 207 | *.ldf 208 | 209 | # Business Intelligence projects 210 | *.rdl.data 211 | *.bim.layout 212 | *.bim_*.settings 213 | 214 | # Microsoft Fakes 215 | FakesAssemblies/ 216 | 217 | # GhostDoc plugin setting file 218 | *.GhostDoc.xml 219 | 220 | # Node.js Tools for Visual Studio 221 | .ntvs_analysis.dat 222 | 223 | # Visual Studio 6 build log 224 | *.plg 225 | 226 | # Visual Studio 6 workspace options file 227 | *.opt 228 | 229 | # Visual Studio LightSwitch build output 230 | **/*.HTMLClient/GeneratedArtifacts 231 | **/*.DesktopClient/GeneratedArtifacts 232 | **/*.DesktopClient/ModelManifest.xml 233 | **/*.Server/GeneratedArtifacts 234 | **/*.Server/ModelManifest.xml 235 | _Pvt_Extensions 236 | 237 | # LightSwitch generated files 238 | GeneratedArtifacts/ 239 | ModelManifest.xml 240 | 241 | # Paket dependency manager 242 | .paket/paket.exe 243 | 244 | # FAKE - F# Make 245 | .fake/ -------------------------------------------------------------------------------- /Github/serilog-configuration-refactoring.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suchiman/SerilogAnalyzer/03309d5151df784d6b987873fc66815ca362138b/Github/serilog-configuration-refactoring.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SerilogAnalyzer 2 | 3 | Roslyn-based analysis for code using the [Serilog](http://serilog.net) logging library. Checks for common mistakes and usage problems. 4 | 5 | ![SerilogAnalyzer](https://raw.githubusercontent.com/nblumhardt/images/master/serilog-analyzer-example.gif) 6 | 7 | ## Installing (Visual Studio) 8 | 9 | You can get the SerilogAnalyzer from various sources: 10 | - Grab the VSIX from the [releases list](https://github.com/Suchiman/SerilogAnalyzer/releases). 11 | - Install it from [Visual Studio Gallery](https://marketplace.visualstudio.com/items?itemName=Suchiman.SerilogAnalyzer). 12 | - Install the [NuGet package](https://www.nuget.org/packages/SerilogAnalyzer) into your project. 13 | 14 | ## Analyses 15 | 16 | ### `Serilog001`: Exception Usage 17 | 18 | Checks that exceptions are passed to the `exception` argument, and not as a normal property, with a code fix to correct it. 19 | 20 | **Detected incorrect usage:** 21 | 22 | ```csharp 23 | catch (Exception ex) 24 | { 25 | Log.Error("Could not save {File}: {Error}", file, ex); 26 | } 27 | ``` 28 | 29 | The `ex` parameter is an exception, which Serilog has special handling for if passed as the first argument. 30 | 31 | **Correct usage:** 32 | 33 | ```csharp 34 | catch (Exception ex) 35 | { 36 | Log.Error(ex, "Could not save {File}", file); 37 | } 38 | ``` 39 | 40 | ### `Serilog002`: Message Template Syntax Verifier 41 | 42 | Checks message templates for correct syntax and emits an error if there's a violation of the templating syntax. 43 | 44 | **Detected incorrect usage:** 45 | 46 | ```csharp 47 | Log.Information("Saving {File to {Directory}", file, directory); 48 | ``` 49 | 50 | The first property token in the message template, `File`, is malformed. 51 | 52 | **Correct usage:** 53 | 54 | ```csharp 55 | Log.Information("Saving {File} to {Directory}", file, directory); 56 | ``` 57 | 58 | ### `Serilog003`: Property Binding Verifier 59 | 60 | Checks coherence between the message template tokens and the supplied arguments. 61 | 62 | **Detected incorrect usage:** 63 | 64 | ```csharp 65 | Log.Information("Saving {File} to {Directory}", file); 66 | ``` 67 | 68 | Here the number of arguments passed to the method (1) is less than the number of tokens in the message template (2), so the second token in the message template, `{Directory}`, will have no value. 69 | 70 | **Correct usage:** 71 | 72 | ```csharp 73 | Log.Information("Saving {File} to {Directory}", file, directory); 74 | ``` 75 | 76 | Each property named in the message template needs to correspond to exactly one argument. 77 | 78 | ### `Serilog004`: Constant Message Template Verifier 79 | 80 | Checks that message templates are constant strings. This ensures that events with different data/format arguments can still be detected as instances of the same event. 81 | 82 | **Detected incorrect usage:** 83 | 84 | ```csharp 85 | var errorMessage = TryToCheckOutOrder(...); // etc. 86 | Log.Error(errorMessage); 87 | ``` 88 | 89 | Because `errorMessage` generally contains failure-specific text (`"Couldn't find order 123"` ... then `124`, then `125`) the group of occurrences can't be located using the message template/event type. 90 | 91 | This also degrades Serilog performance by filling its internal message template cache. 92 | 93 | **Correct usage:** 94 | 95 | ```csharp 96 | Log.Error("Order handler failed with {HandlerError}", errorMessage); 97 | ``` 98 | 99 | Correct usage is to always pass any variable data as a property to a message template. 100 | A CodeFix is provided that converts string interpolation (`$"{...}"`), `String.Format(...)` and string concat (`"value: " + value`) to a message template 101 | 102 | ### `Serilog005`: Unique Property Name Verifier 103 | 104 | Checks that all property names in a message template are unique. 105 | 106 | **Detected incorrect usage:** 107 | 108 | ```csharp 109 | Log.Information("Saving {Path} to {Path}", file, directory); 110 | ``` 111 | 112 | In this example, because both properties in the message template have the same name, Serilog can only record one of them. 113 | 114 | **Correct usage:** 115 | 116 | ```csharp 117 | Log.Information("Saving {File} to {Directory}", file, directory); 118 | ``` 119 | 120 | Each property in a message template must have a unique name. 121 | 122 | ### `Serilog006`: Pascal Cased Property Verifier 123 | 124 | Checks that all property names in a message template are PascalCased. 125 | 126 | **Detected incorrect usage:** 127 | 128 | ```csharp 129 | Log.Information("Saving {file} to {directory}", file, directory); 130 | ``` 131 | 132 | A CodeFix is provided, that applies pascal casing. 133 | 134 | **Correct usage:** 135 | 136 | ```csharp 137 | Log.Information("Saving {File} to {Directory}", file, directory); 138 | ``` 139 | 140 | ### `Serilog007`: Anonymous objects use destructuring Verifier 141 | 142 | Checks that all anonymous objects passed to the logger are destructured. 143 | 144 | **Detected incorrect usage:** 145 | 146 | ```csharp 147 | Log.Information("Saving {File} to {Directory}", new { Name = name, Size = size }, directory); 148 | ``` 149 | 150 | A CodeFix is provided, that applies the destructuring hint. 151 | 152 | **Correct usage:** 153 | 154 | ```csharp 155 | Log.Information("Saving {@File} to {Directory}", new { Name = name, Size = size }, directory); 156 | ``` 157 | 158 | ### `Serilog008`: Correct contextual Logger Verifier 159 | 160 | Checks that contextual loggers are constructed with the correct type. 161 | 162 | **Detected incorrect usage:** 163 | 164 | ```csharp 165 | class A 166 | { 167 | private static readonly ILogger Logger = Logger.ForContext(); 168 | } 169 | 170 | class B {} 171 | ``` 172 | 173 | A CodeFix is provided, that uses the correct type. 174 | 175 | **Correct usage:** 176 | 177 | ```csharp 178 | class A 179 | { 180 | private static readonly ILogger Logger = Logger.ForContext(); 181 | } 182 | 183 | class B {} 184 | ``` 185 | 186 | ## Refactors 187 | Performs static analysis on a fluent LoggerConfiguration call to generate configuration for use with either [``](https://github.com/serilog/serilog-settings-appsettings) or [`appSettings.json`](https://github.com/serilog/serilog-settings-configuration) 188 | 189 | ![Serilog Configuration Refactoring](./Github/serilog-configuration-refactoring.gif) 190 | -------------------------------------------------------------------------------- /SerilogAnalyzer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28315.86 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SerilogAnalyzer", "SerilogAnalyzer\SerilogAnalyzer\SerilogAnalyzer.csproj", "{1666D457-D350-4322-B1BD-4DBDF530A882}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SerilogAnalyzer.Test", "SerilogAnalyzer\SerilogAnalyzer.Test\SerilogAnalyzer.Test.csproj", "{22F07147-DF31-42B8-AD86-E391FB7D489F}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SerilogAnalyzer.Vsix", "SerilogAnalyzer\SerilogAnalyzer.Vsix\SerilogAnalyzer.Vsix.csproj", "{5910AC72-0F22-437F-902A-177E0C2318A8}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {1666D457-D350-4322-B1BD-4DBDF530A882}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {1666D457-D350-4322-B1BD-4DBDF530A882}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {1666D457-D350-4322-B1BD-4DBDF530A882}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {1666D457-D350-4322-B1BD-4DBDF530A882}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {22F07147-DF31-42B8-AD86-E391FB7D489F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {22F07147-DF31-42B8-AD86-E391FB7D489F}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {22F07147-DF31-42B8-AD86-E391FB7D489F}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {22F07147-DF31-42B8-AD86-E391FB7D489F}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {5910AC72-0F22-437F-902A-177E0C2318A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {5910AC72-0F22-437F-902A-177E0C2318A8}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {5910AC72-0F22-437F-902A-177E0C2318A8}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {5910AC72-0F22-437F-902A-177E0C2318A8}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {0DF51104-03D5-402D-A6A5-75BA36A741CF} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer.Test/CorrectLoggerContextTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Robin Sue 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | using System; 16 | using Microsoft.CodeAnalysis; 17 | using Microsoft.CodeAnalysis.CodeFixes; 18 | using Microsoft.CodeAnalysis.Diagnostics; 19 | using Microsoft.VisualStudio.TestTools.UnitTesting; 20 | using TestHelper; 21 | 22 | namespace SerilogAnalyzer.Test 23 | { 24 | [TestClass] 25 | public class CorrectLoggerContextTests : CodeFixVerifier 26 | { 27 | [TestMethod] 28 | public void TestNoCode() 29 | { 30 | var test = @""; 31 | 32 | VerifyCSharpDiagnostic(test); 33 | } 34 | 35 | [TestMethod] 36 | public void TestCorrectContextGeneric() 37 | { 38 | var test = @" 39 | using Serilog; 40 | 41 | namespace ConsoleApplication1 42 | { 43 | class A 44 | { 45 | private static readonly ILogger Logger = Logger.ForContext(); 46 | } 47 | 48 | class B {} 49 | }"; 50 | 51 | VerifyCSharpDiagnostic(test); 52 | } 53 | 54 | [TestMethod] 55 | public void TestWrongContextGeneric() 56 | { 57 | var test = @" 58 | using Serilog; 59 | 60 | namespace ConsoleApplication1 61 | { 62 | class A 63 | { 64 | private static readonly ILogger Logger = Logger.ForContext(); 65 | } 66 | 67 | class B {} 68 | }"; 69 | 70 | var expected007 = new DiagnosticResult 71 | { 72 | Id = "Serilog008", 73 | Message = String.Format("Logger '{0}' should use {1} instead of {2}", "Logger", "ForContext()", "ForContext()"), 74 | Severity = DiagnosticSeverity.Warning, 75 | Locations = new[] 76 | { 77 | new DiagnosticResultLocation("Test0.cs", 8, 72, 1) 78 | } 79 | }; 80 | 81 | VerifyCSharpDiagnostic(test, expected007); 82 | 83 | var fixtest = @" 84 | using Serilog; 85 | 86 | namespace ConsoleApplication1 87 | { 88 | class A 89 | { 90 | private static readonly ILogger Logger = Logger.ForContext(); 91 | } 92 | 93 | class B {} 94 | }"; 95 | VerifyCSharpFix(test, fixtest); 96 | } 97 | 98 | [TestMethod] 99 | public void TestCorrectContextTypeof() 100 | { 101 | var test = @" 102 | using Serilog; 103 | 104 | namespace ConsoleApplication1 105 | { 106 | class A 107 | { 108 | private static readonly ILogger Logger = Logger.ForContext(typeof(A)); 109 | } 110 | 111 | class B {} 112 | }"; 113 | 114 | VerifyCSharpDiagnostic(test); 115 | } 116 | 117 | [TestMethod] 118 | public void TestWrongContextTypeof() 119 | { 120 | var test = @" 121 | using Serilog; 122 | 123 | namespace ConsoleApplication1 124 | { 125 | class A 126 | { 127 | private static readonly ILogger Logger = Logger.ForContext(typeof(B)); 128 | } 129 | 130 | class B {} 131 | }"; 132 | 133 | var expected007 = new DiagnosticResult 134 | { 135 | Id = "Serilog008", 136 | Message = String.Format("Logger '{0}' should use {1} instead of {2}", "Logger", "ForContext(typeof(ConsoleApplication1.A))", "ForContext(typeof(ConsoleApplication1.B))"), 137 | Severity = DiagnosticSeverity.Warning, 138 | Locations = new[] 139 | { 140 | new DiagnosticResultLocation("Test0.cs", 8, 79, 1) 141 | } 142 | }; 143 | 144 | VerifyCSharpDiagnostic(test, expected007); 145 | 146 | var fixtest = @" 147 | using Serilog; 148 | 149 | namespace ConsoleApplication1 150 | { 151 | class A 152 | { 153 | private static readonly ILogger Logger = Logger.ForContext(typeof(A)); 154 | } 155 | 156 | class B {} 157 | }"; 158 | VerifyCSharpFix(test, fixtest); 159 | } 160 | 161 | [TestMethod] 162 | public void TestDoesntTriggerInMethod() 163 | { 164 | var test = @" 165 | using Serilog; 166 | 167 | namespace ConsoleApplication1 168 | { 169 | class A 170 | { 171 | void Main() 172 | { 173 | ILogger Logger = Logger.ForContext(typeof(B)); 174 | } 175 | } 176 | 177 | class B {} 178 | }"; 179 | 180 | VerifyCSharpDiagnostic(test); 181 | } 182 | 183 | [TestMethod] 184 | public void TestDoesntTriggerOnMultipleLoggers() 185 | { 186 | var test = @" 187 | using Serilog; 188 | 189 | namespace ConsoleApplication1 190 | { 191 | class A 192 | { 193 | private static readonly ILogger Logger1 = Logger.ForContext(typeof(B)); 194 | private static readonly ILogger Logger2 = Logger.ForContext(typeof(C)); 195 | } 196 | 197 | class B {} 198 | class C {} 199 | }"; 200 | 201 | VerifyCSharpDiagnostic(test); 202 | } 203 | 204 | protected override CodeFixProvider GetCSharpCodeFixProvider() 205 | { 206 | return new CorrectLoggerContextCodeFixProvider(); 207 | } 208 | 209 | protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() 210 | { 211 | return new SerilogAnalyzerAnalyzer(); 212 | } 213 | } 214 | } -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer.Test/DestructuringHintTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Robin Sue 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | using System; 16 | using Microsoft.CodeAnalysis; 17 | using Microsoft.CodeAnalysis.CodeFixes; 18 | using Microsoft.CodeAnalysis.Diagnostics; 19 | using Microsoft.VisualStudio.TestTools.UnitTesting; 20 | using TestHelper; 21 | 22 | namespace SerilogAnalyzer.Test 23 | { 24 | [TestClass] 25 | public class DestructuringHintTests : CodeFixVerifier 26 | { 27 | [TestMethod] 28 | public void TestNoCode() 29 | { 30 | var test = @""; 31 | 32 | VerifyCSharpDiagnostic(test); 33 | } 34 | 35 | [TestMethod] 36 | public void TestMissingDestructuringOnAnonymousObject() 37 | { 38 | var test = @" 39 | using Serilog; 40 | 41 | namespace ConsoleApplication1 42 | { 43 | class TypeName 44 | { 45 | public static void Test() 46 | { 47 | Log.Warning(""Hello World {Some}"", new { Meh = 42 }); 48 | } 49 | } 50 | }"; 51 | 52 | var expected007 = new DiagnosticResult 53 | { 54 | Id = "Serilog007", 55 | Message = String.Format("Property '{0}' should use destructuring because the argument is an anonymous object", "Some"), 56 | Severity = DiagnosticSeverity.Warning, 57 | Locations = new[] 58 | { 59 | new DiagnosticResultLocation("Test0.cs", 10, 42, 6) 60 | } 61 | }; 62 | 63 | VerifyCSharpDiagnostic(test, expected007); 64 | 65 | var fixtest = @" 66 | using Serilog; 67 | 68 | namespace ConsoleApplication1 69 | { 70 | class TypeName 71 | { 72 | public static void Test() 73 | { 74 | Log.Warning(""Hello World {@Some}"", new { Meh = 42 }); 75 | } 76 | } 77 | }"; 78 | VerifyCSharpFix(test, fixtest); 79 | } 80 | 81 | protected override CodeFixProvider GetCSharpCodeFixProvider() 82 | { 83 | return new DestructuringHintCodeFixProvider(); 84 | } 85 | 86 | protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() 87 | { 88 | return new SerilogAnalyzerAnalyzer(); 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer.Test/Helpers/CodeFixVerifier.Helper.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Robin Sue 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | using Microsoft.CodeAnalysis; 16 | using System.Collections.Generic; 17 | using System.Linq; 18 | 19 | namespace TestHelper 20 | { 21 | /// 22 | /// Diagnostic Producer class with extra methods dealing with applying codefixes 23 | /// All methods are static 24 | /// 25 | public abstract partial class CodeFixVerifier : DiagnosticVerifier 26 | { 27 | /// 28 | /// Compare two collections of Diagnostics,and return a list of any new diagnostics that appear only in the second collection. 29 | /// Note: Considers Diagnostics to be the same if they have the same Ids. In the case of multiple diagnostics with the same Id in a row, 30 | /// this method may not necessarily return the new one. 31 | /// 32 | /// The Diagnostics that existed in the code before the CodeFix was applied 33 | /// The Diagnostics that exist in the code after the CodeFix was applied 34 | /// A list of Diagnostics that only surfaced in the code after the CodeFix was applied 35 | private static IEnumerable GetNewDiagnostics(IEnumerable diagnostics, IEnumerable newDiagnostics) 36 | { 37 | var oldArray = diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); 38 | var newArray = newDiagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); 39 | 40 | int oldIndex = 0; 41 | int newIndex = 0; 42 | 43 | while (newIndex < newArray.Length) 44 | { 45 | if (oldIndex < oldArray.Length && oldArray[oldIndex].Id == newArray[newIndex].Id) 46 | { 47 | ++oldIndex; 48 | ++newIndex; 49 | } 50 | else 51 | { 52 | yield return newArray[newIndex++]; 53 | } 54 | } 55 | } 56 | 57 | /// 58 | /// Get the existing compiler diagnostics on the inputted document. 59 | /// 60 | /// The Document to run the compiler diagnostic analyzers on 61 | /// The compiler diagnostics that were found in the code 62 | private static IEnumerable GetCompilerDiagnostics(Document document) 63 | { 64 | return document.GetSemanticModelAsync().Result.GetDiagnostics(); 65 | } 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer.Test/Helpers/DiagnosticResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Robin Sue 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | using Microsoft.CodeAnalysis; 16 | using System; 17 | 18 | namespace TestHelper 19 | { 20 | /// 21 | /// Location where the diagnostic appears, as determined by path, line number, and column number. 22 | /// 23 | public struct DiagnosticResultLocation 24 | { 25 | public DiagnosticResultLocation(string path, int line, int column, int length = -1) 26 | { 27 | if (line < -1) 28 | { 29 | throw new ArgumentOutOfRangeException(nameof(line), "line must be >= -1"); 30 | } 31 | 32 | if (column < -1) 33 | { 34 | throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1"); 35 | } 36 | 37 | this.Path = path; 38 | this.Line = line; 39 | this.Column = column; 40 | this.Length = length; 41 | } 42 | 43 | public string Path { get; } 44 | public int Line { get; } 45 | public int Column { get; } 46 | public int Length { get; } 47 | } 48 | 49 | /// 50 | /// Struct that stores information about a Diagnostic appearing in a source 51 | /// 52 | public struct DiagnosticResult 53 | { 54 | private DiagnosticResultLocation[] locations; 55 | 56 | public DiagnosticResultLocation[] Locations 57 | { 58 | get 59 | { 60 | if (this.locations == null) 61 | { 62 | this.locations = new DiagnosticResultLocation[] { }; 63 | } 64 | return this.locations; 65 | } 66 | 67 | set 68 | { 69 | this.locations = value; 70 | } 71 | } 72 | 73 | public DiagnosticSeverity Severity { get; set; } 74 | 75 | public string Id { get; set; } 76 | 77 | public string Message { get; set; } 78 | 79 | public string Path 80 | { 81 | get 82 | { 83 | return this.Locations.Length > 0 ? this.Locations[0].Path : ""; 84 | } 85 | } 86 | 87 | public int Line 88 | { 89 | get 90 | { 91 | return this.Locations.Length > 0 ? this.Locations[0].Line : -1; 92 | } 93 | } 94 | 95 | public int Column 96 | { 97 | get 98 | { 99 | return this.Locations.Length > 0 ? this.Locations[0].Column : -1; 100 | } 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer.Test/Helpers/DiagnosticVerifier.Helper.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Robin Sue 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | using System; 16 | using System.Collections.Generic; 17 | using System.Collections.Immutable; 18 | using System.Linq; 19 | using System.Threading; 20 | using Microsoft.CodeAnalysis; 21 | using Microsoft.CodeAnalysis.CodeActions; 22 | using Microsoft.CodeAnalysis.CSharp; 23 | using Microsoft.CodeAnalysis.Diagnostics; 24 | using Microsoft.CodeAnalysis.Formatting; 25 | using Microsoft.CodeAnalysis.Simplification; 26 | using Microsoft.CodeAnalysis.Text; 27 | using Serilog; 28 | 29 | namespace TestHelper 30 | { 31 | /// 32 | /// Class for turning strings into documents and getting the diagnostics on them 33 | /// All methods are static 34 | /// 35 | public abstract partial class DiagnosticVerifier 36 | { 37 | private static readonly MetadataReference CorlibReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); 38 | private static readonly MetadataReference SystemCoreReference = MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location); 39 | private static readonly MetadataReference CSharpSymbolsReference = MetadataReference.CreateFromFile(typeof(CSharpCompilation).Assembly.Location); 40 | private static readonly MetadataReference CodeAnalysisReference = MetadataReference.CreateFromFile(typeof(Compilation).Assembly.Location); 41 | private static readonly MetadataReference SerilogReference = MetadataReference.CreateFromFile(typeof(ILogger).Assembly.Location); 42 | private static readonly MetadataReference SerilogConsoleReference = MetadataReference.CreateFromFile(typeof(ConsoleLoggerConfigurationExtensions).Assembly.Location); 43 | private static readonly MetadataReference SerilogLiterateReference = MetadataReference.CreateFromFile(typeof(LoggerConfigurationLiterateExtensions).Assembly.Location); 44 | private static readonly MetadataReference SerilogFileReference = MetadataReference.CreateFromFile(typeof(FileLoggerConfigurationExtensions).Assembly.Location); 45 | private static readonly MetadataReference SerilogRollingFileReference = MetadataReference.CreateFromFile(typeof(RollingFileLoggerConfigurationExtensions).Assembly.Location); 46 | private static readonly MetadataReference SerilogFilterExpressionsReference = MetadataReference.CreateFromFile(typeof(LoggerFilterConfigurationExtensions).Assembly.Location); 47 | 48 | internal static string DefaultFilePathPrefix = "Test"; 49 | internal static string CSharpDefaultFileExt = "cs"; 50 | internal static string VisualBasicDefaultExt = "vb"; 51 | internal static string TestProjectName = "TestProject"; 52 | 53 | #region Get Diagnostics 54 | 55 | /// 56 | /// Given classes in the form of strings, their language, and an IDiagnosticAnlayzer to apply to it, return the diagnostics found in the string after converting it to a document. 57 | /// 58 | /// Classes in the form of strings 59 | /// The language the source classes are in 60 | /// The analyzer to be run on the sources 61 | /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location 62 | private static Diagnostic[] GetSortedDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer) 63 | { 64 | return GetSortedDiagnosticsFromDocuments(analyzer, GetDocuments(sources, language)); 65 | } 66 | 67 | /// 68 | /// Given an analyzer and a document to apply it to, run the analyzer and gather an array of diagnostics found in it. 69 | /// The returned diagnostics are then ordered by location in the source document. 70 | /// 71 | /// The analyzer to run on the documents 72 | /// The Documents that the analyzer will be run on 73 | /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location 74 | protected static Diagnostic[] GetSortedDiagnosticsFromDocuments(DiagnosticAnalyzer analyzer, Document[] documents) 75 | { 76 | var projects = new HashSet(); 77 | foreach (var document in documents) 78 | { 79 | projects.Add(document.Project); 80 | } 81 | 82 | var diagnostics = new List(); 83 | foreach (var project in projects) 84 | { 85 | var compilationWithAnalyzers = project.GetCompilationAsync().Result.WithAnalyzers(ImmutableArray.Create(analyzer)); 86 | var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result; 87 | foreach (var diag in diags) 88 | { 89 | if (diag.Location == Location.None || diag.Location.IsInMetadata) 90 | { 91 | diagnostics.Add(diag); 92 | } 93 | else 94 | { 95 | for (int i = 0; i < documents.Length; i++) 96 | { 97 | var document = documents[i]; 98 | var tree = document.GetSyntaxTreeAsync().Result; 99 | if (tree == diag.Location.SourceTree) 100 | { 101 | diagnostics.Add(diag); 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | var results = SortDiagnostics(diagnostics); 109 | diagnostics.Clear(); 110 | return results; 111 | } 112 | 113 | /// 114 | /// Sort diagnostics by location in source document 115 | /// 116 | /// The list of Diagnostics to be sorted 117 | /// An IEnumerable containing the Diagnostics in order of Location 118 | private static Diagnostic[] SortDiagnostics(IEnumerable diagnostics) 119 | { 120 | return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); 121 | } 122 | 123 | #endregion 124 | 125 | /// 126 | /// Apply the inputted CodeAction to the inputted document. 127 | /// 128 | /// The Document to apply the action on 129 | /// A CodeAction that will be applied to the Document. 130 | /// A Document with the changes from the CodeAction 131 | protected static Document ApplyCodeAction(Document document, CodeAction codeAction) 132 | { 133 | var operations = codeAction.GetOperationsAsync(CancellationToken.None).Result; 134 | var solution = operations.OfType().Single().ChangedSolution; 135 | return solution.GetDocument(document.Id); 136 | } 137 | 138 | /// 139 | /// Given a document, turn it into a string based on the syntax root 140 | /// 141 | /// The Document to be converted to a string 142 | /// A string containing the syntax of the Document after formatting 143 | protected static string GetStringFromDocument(Document document) 144 | { 145 | var simplifiedDoc = Simplifier.ReduceAsync(document, Simplifier.Annotation).Result; 146 | var root = simplifiedDoc.GetSyntaxRootAsync().Result; 147 | root = Formatter.Format(root, Formatter.Annotation, simplifiedDoc.Project.Solution.Workspace); 148 | return root.GetText().ToString(); 149 | } 150 | 151 | #region Set up compilation and documents 152 | /// 153 | /// Given an array of strings as sources and a language, turn them into a project and return the documents and spans of it. 154 | /// 155 | /// Classes in the form of strings 156 | /// The language the source code is in 157 | /// A Tuple containing the Documents produced from the sources and their TextSpans if relevant 158 | private static Document[] GetDocuments(string[] sources, string language) 159 | { 160 | if (language != LanguageNames.CSharp && language != LanguageNames.VisualBasic) 161 | { 162 | throw new ArgumentException("Unsupported Language"); 163 | } 164 | 165 | var project = CreateProject(sources, language); 166 | var documents = project.Documents.ToArray(); 167 | 168 | if (sources.Length != documents.Length) 169 | { 170 | throw new SystemException("Amount of sources did not match amount of Documents created"); 171 | } 172 | 173 | return documents; 174 | } 175 | 176 | /// 177 | /// Create a Document from a string through creating a project that contains it. 178 | /// 179 | /// Classes in the form of a string 180 | /// The language the source code is in 181 | /// A Document created from the source string 182 | protected static Document CreateDocument(string source, string language = LanguageNames.CSharp) 183 | { 184 | return CreateProject(new[] { source }, language).Documents.First(); 185 | } 186 | 187 | /// 188 | /// Create a project using the inputted strings as sources. 189 | /// 190 | /// Classes in the form of strings 191 | /// The language the source code is in 192 | /// A Project created out of the Documents created from the source strings 193 | private static Project CreateProject(string[] sources, string language = LanguageNames.CSharp) 194 | { 195 | string fileNamePrefix = DefaultFilePathPrefix; 196 | string fileExt = language == LanguageNames.CSharp ? CSharpDefaultFileExt : VisualBasicDefaultExt; 197 | 198 | var projectId = ProjectId.CreateNewId(debugName: TestProjectName); 199 | 200 | var solution = new AdhocWorkspace() 201 | .CurrentSolution 202 | .AddProject(projectId, TestProjectName, TestProjectName, language) 203 | .AddMetadataReference(projectId, CorlibReference) 204 | .AddMetadataReference(projectId, SystemCoreReference) 205 | .AddMetadataReference(projectId, CSharpSymbolsReference) 206 | .AddMetadataReference(projectId, CodeAnalysisReference) 207 | .AddMetadataReference(projectId, SerilogReference) 208 | .AddMetadataReference(projectId, SerilogConsoleReference) 209 | .AddMetadataReference(projectId, SerilogLiterateReference) 210 | .AddMetadataReference(projectId, SerilogFileReference) 211 | .AddMetadataReference(projectId, SerilogRollingFileReference) 212 | .AddMetadataReference(projectId, SerilogFilterExpressionsReference); 213 | 214 | int count = 0; 215 | foreach (var source in sources) 216 | { 217 | var newFileName = fileNamePrefix + count + "." + fileExt; 218 | var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName); 219 | solution = solution.AddDocument(documentId, newFileName, SourceText.From(source)); 220 | count++; 221 | } 222 | return solution.GetProject(projectId); 223 | } 224 | #endregion 225 | } 226 | } 227 | 228 | -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer.Test/PascalCaseAnalyzerTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Serilog Analyzer Contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | using Microsoft.CodeAnalysis; 16 | using Microsoft.CodeAnalysis.CodeFixes; 17 | using Microsoft.CodeAnalysis.Diagnostics; 18 | using Microsoft.VisualStudio.TestTools.UnitTesting; 19 | using System; 20 | using TestHelper; 21 | 22 | namespace SerilogAnalyzer.Test 23 | { 24 | [TestClass] 25 | public class PascalCaseAnalyzerTests : CodeFixVerifier 26 | { 27 | protected override CodeFixProvider GetCSharpCodeFixProvider() 28 | { 29 | return new SerilogAnalyzerPascalCaseCodeFixProvider(); 30 | } 31 | 32 | protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() 33 | { 34 | return new SerilogAnalyzerAnalyzer(); 35 | } 36 | 37 | [TestMethod] 38 | public void TestPascalCaseFixForString() 39 | { 40 | string src = @" 41 | using Serilog; 42 | 43 | class TypeName 44 | { 45 | public static void Test() 46 | { 47 | var foo = ""tester""; 48 | Log.Verbose(""Hello {tester}"", foo)); 49 | } 50 | }"; 51 | var expected = new DiagnosticResult 52 | { 53 | Id = "Serilog006", 54 | Message = String.Format("Property name '{0}' should be pascal case", "tester"), 55 | Severity = DiagnosticSeverity.Warning, 56 | Locations = new[] 57 | { 58 | new DiagnosticResultLocation("Test0.cs", 9, 28, 8) 59 | } 60 | }; 61 | VerifyCSharpDiagnostic(src, expected); 62 | 63 | var fixtest = @" 64 | using Serilog; 65 | 66 | class TypeName 67 | { 68 | public static void Test() 69 | { 70 | var foo = ""tester""; 71 | Log.Verbose(""Hello {Tester}"", foo)); 72 | } 73 | }"; 74 | VerifyCSharpFix(src, fixtest); 75 | } 76 | 77 | [TestMethod] 78 | public void TestPascalCaseFixForStringWithException() 79 | { 80 | string src = @" 81 | using Serilog; 82 | using System; 83 | 84 | class TypeName 85 | { 86 | public static void Test() 87 | { 88 | var dwgName = ""tester""; 89 | Exception crashAndBurn = null; 90 | Log.Fatal(crashAndBurn, 91 | messageTemplate: ""{DwgFileName} Crashed and burned. {stackTrace}"", 92 | propertyValue0: dwgName, 93 | propertyValue1: crashAndBurn.StackTrace); 94 | } 95 | }"; 96 | var expected = new DiagnosticResult 97 | { 98 | Id = "Serilog006", 99 | Message = String.Format("Property name '{0}' should be pascal case", "stackTrace"), 100 | Severity = DiagnosticSeverity.Warning, 101 | Locations = new[] 102 | { 103 | new DiagnosticResultLocation("Test0.cs", 12, 91, 12) 104 | } 105 | }; 106 | VerifyCSharpDiagnostic(src, expected); 107 | 108 | var fixtest = @" 109 | using Serilog; 110 | using System; 111 | 112 | class TypeName 113 | { 114 | public static void Test() 115 | { 116 | var dwgName = ""tester""; 117 | Exception crashAndBurn = null; 118 | Log.Fatal(crashAndBurn, 119 | messageTemplate: ""{DwgFileName} Crashed and burned. {StackTrace}"", 120 | propertyValue0: dwgName, 121 | propertyValue1: crashAndBurn.StackTrace); 122 | } 123 | }"; 124 | VerifyCSharpFix(src, fixtest); 125 | } 126 | 127 | [TestMethod] 128 | public void TestPascalCaseFixForSnakeCaseString() 129 | { 130 | string src = @" 131 | using Serilog; 132 | 133 | class TypeName 134 | { 135 | public static void Test() 136 | { 137 | var foo = ""tester""; 138 | Log.Verbose(""Hello {tester_name}"", foo)); 139 | } 140 | }"; 141 | var expected = new DiagnosticResult 142 | { 143 | Id = "Serilog006", 144 | Message = String.Format("Property name '{0}' should be pascal case", "tester_name"), 145 | Severity = DiagnosticSeverity.Warning, 146 | Locations = new[] 147 | { 148 | new DiagnosticResultLocation("Test0.cs", 9, 28, 13) 149 | } 150 | }; 151 | VerifyCSharpDiagnostic(src, expected); 152 | 153 | var fixtest = @" 154 | using Serilog; 155 | 156 | class TypeName 157 | { 158 | public static void Test() 159 | { 160 | var foo = ""tester""; 161 | Log.Verbose(""Hello {TesterName}"", foo)); 162 | } 163 | }"; 164 | VerifyCSharpFix(src, fixtest); 165 | } 166 | 167 | [TestMethod] 168 | public void TestPascalCaseFixForStringWithEscapes() 169 | { 170 | string src = @" 171 | using Serilog; 172 | 173 | class TypeName 174 | { 175 | public static void Test() 176 | { 177 | var foo = ""tester""; 178 | Log.Verbose(""Hello \""{tester}\"""", foo)); 179 | } 180 | }"; 181 | var expected = new DiagnosticResult 182 | { 183 | Id = "Serilog006", 184 | Message = String.Format("Property name '{0}' should be pascal case", "tester"), 185 | Severity = DiagnosticSeverity.Warning, 186 | Locations = new[] 187 | { 188 | new DiagnosticResultLocation("Test0.cs", 9, 30, 9) 189 | } 190 | }; 191 | VerifyCSharpDiagnostic(src, expected); 192 | 193 | var fixtest = @" 194 | using Serilog; 195 | 196 | class TypeName 197 | { 198 | public static void Test() 199 | { 200 | var foo = ""tester""; 201 | Log.Verbose(""Hello \""{Tester}\"""", foo)); 202 | } 203 | }"; 204 | VerifyCSharpFix(src, fixtest); 205 | } 206 | 207 | [TestMethod] 208 | public void TestPascalCaseFixForStringWithVerbatimEscapes() 209 | { 210 | string src = @" 211 | using Serilog; 212 | 213 | class TypeName 214 | { 215 | public static void Test() 216 | { 217 | var foo = ""tester""; 218 | Log.Verbose(@""Hello """"{tester}"""""", foo)); 219 | } 220 | }"; 221 | var expected = new DiagnosticResult 222 | { 223 | Id = "Serilog006", 224 | Message = String.Format("Property name '{0}' should be pascal case", "tester"), 225 | Severity = DiagnosticSeverity.Warning, 226 | Locations = new[] 227 | { 228 | new DiagnosticResultLocation("Test0.cs", 9, 31, 9) 229 | } 230 | }; 231 | VerifyCSharpDiagnostic(src, expected); 232 | 233 | var fixtest = @" 234 | using Serilog; 235 | 236 | class TypeName 237 | { 238 | public static void Test() 239 | { 240 | var foo = ""tester""; 241 | Log.Verbose(@""Hello """"{Tester}"""""", foo)); 242 | } 243 | }"; 244 | VerifyCSharpFix(src, fixtest); 245 | } 246 | 247 | [TestMethod] 248 | public void TestPascalCaseFixForObject() 249 | { 250 | string src = @" 251 | using Serilog; 252 | 253 | class TypeName 254 | { 255 | public static void Test() 256 | { 257 | var foo = ""tester""; 258 | Log.Verbose(""Hello {@tester}"", foo)); 259 | } 260 | }"; 261 | var expected = new DiagnosticResult 262 | { 263 | Id = "Serilog006", 264 | Message = $"Property name '{"tester"}' should be pascal case", 265 | Severity = DiagnosticSeverity.Warning, 266 | Locations = new[] 267 | { 268 | new DiagnosticResultLocation("Test0.cs", 9, 28, 9) 269 | } 270 | }; 271 | VerifyCSharpDiagnostic(src, expected); 272 | 273 | var fixtest = @" 274 | using Serilog; 275 | 276 | class TypeName 277 | { 278 | public static void Test() 279 | { 280 | var foo = ""tester""; 281 | Log.Verbose(""Hello {@Tester}"", foo)); 282 | } 283 | }"; 284 | VerifyCSharpFix(src, fixtest); 285 | } 286 | 287 | [TestMethod] 288 | public void TestPascalCaseFixForStringification() 289 | { 290 | string src = @" 291 | using Serilog; 292 | 293 | class TypeName 294 | { 295 | public static void Test() 296 | { 297 | var foo = ""tester""; 298 | Log.Verbose(""Hello {$tester}"", foo)); 299 | } 300 | }"; 301 | var expected = new DiagnosticResult 302 | { 303 | Id = "Serilog006", 304 | Message = $"Property name '{"tester"}' should be pascal case", 305 | Severity = DiagnosticSeverity.Warning, 306 | Locations = new[] 307 | { 308 | new DiagnosticResultLocation("Test0.cs", 9, 28, 9) 309 | } 310 | }; 311 | VerifyCSharpDiagnostic(src, expected); 312 | 313 | var fixtest = @" 314 | using Serilog; 315 | 316 | class TypeName 317 | { 318 | public static void Test() 319 | { 320 | var foo = ""tester""; 321 | Log.Verbose(""Hello {$Tester}"", foo)); 322 | } 323 | }"; 324 | VerifyCSharpFix(src, fixtest); 325 | } 326 | 327 | [TestMethod] 328 | public void TestPascalCaseFixForEscapedString() 329 | { 330 | string src = @" 331 | using Serilog; 332 | 333 | class TypeName 334 | { 335 | public static void Test() 336 | { 337 | var foo = ""tester""; 338 | Log.Verbose(""Hello {{tester}} you will {foo}"", bar)); 339 | } 340 | }"; 341 | var expected = new DiagnosticResult 342 | { 343 | Id = "Serilog006", 344 | Message = $"Property name '{"tester"}' should be pascal case", 345 | Severity = DiagnosticSeverity.Warning, 346 | Locations = new[] 347 | { 348 | new DiagnosticResultLocation("Test0.cs", 9, 28, 9) 349 | } 350 | }; 351 | 352 | // should ignore as the variable is escaped 353 | VerifyCSharpFix(src, src); 354 | } 355 | } 356 | } -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer.Test/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("SerilogAnalyzer.Test")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("SerilogAnalyzer.Test")] 12 | [assembly: AssemblyCopyright("Copyright © 2016")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // Version information for an assembly consists of the following four values: 22 | // 23 | // Major Version 24 | // Minor Version 25 | // Build Number 26 | // Revision 27 | // 28 | // You can specify all the values or you can default the Build and Revision Numbers 29 | // by using the '*' as shown below: 30 | // [assembly: AssemblyVersion("1.0.*")] 31 | [assembly: AssemblyVersion("1.0.0.0")] 32 | [assembly: AssemblyFileVersion("1.0.0.0")] -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer.Test/SerilogAnalyzer.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | AnyCPU 6 | 8.0.30703 7 | 2.0 8 | {22F07147-DF31-42B8-AD86-E391FB7D489F} 9 | Library 10 | Properties 11 | SerilogAnalyzer.Test 12 | SerilogAnalyzer.Test 13 | v4.6.1 14 | 512 15 | 16 | 17 | 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | false 26 | 27 | 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | false 35 | 36 | 37 | 38 | ..\..\packages\Microsoft.CodeAnalysis.Common.1.0.0\lib\net45\Microsoft.CodeAnalysis.dll 39 | True 40 | 41 | 42 | ..\..\packages\Microsoft.CodeAnalysis.CSharp.1.0.0\lib\net45\Microsoft.CodeAnalysis.CSharp.dll 43 | True 44 | 45 | 46 | ..\..\packages\Microsoft.CodeAnalysis.CSharp.Workspaces.1.0.0\lib\net45\Microsoft.CodeAnalysis.CSharp.Workspaces.dll 47 | True 48 | 49 | 50 | ..\..\packages\Microsoft.CodeAnalysis.VisualBasic.1.0.0\lib\net45\Microsoft.CodeAnalysis.VisualBasic.dll 51 | True 52 | 53 | 54 | ..\..\packages\Microsoft.CodeAnalysis.VisualBasic.Workspaces.1.0.0\lib\net45\Microsoft.CodeAnalysis.VisualBasic.Workspaces.dll 55 | True 56 | 57 | 58 | ..\..\packages\Microsoft.CodeAnalysis.Workspaces.Common.1.0.0\lib\net45\Microsoft.CodeAnalysis.Workspaces.dll 59 | True 60 | 61 | 62 | ..\..\packages\Microsoft.CodeAnalysis.Workspaces.Common.1.0.0\lib\net45\Microsoft.CodeAnalysis.Workspaces.Desktop.dll 63 | True 64 | 65 | 66 | ..\..\packages\Serilog.2.8.0\lib\net46\Serilog.dll 67 | 68 | 69 | ..\..\packages\Serilog.Filters.Expressions.2.0.0\lib\net45\Serilog.Filters.Expressions.dll 70 | 71 | 72 | ..\..\packages\Serilog.Sinks.Console.3.1.1\lib\net45\Serilog.Sinks.Console.dll 73 | 74 | 75 | ..\..\packages\Serilog.Sinks.File.4.0.0\lib\net45\Serilog.Sinks.File.dll 76 | 77 | 78 | ..\..\packages\Serilog.Sinks.Literate.3.0.0\lib\net45\Serilog.Sinks.Literate.dll 79 | 80 | 81 | ..\..\packages\Serilog.Sinks.RollingFile.3.3.0\lib\net45\Serilog.Sinks.RollingFile.dll 82 | 83 | 84 | ..\..\packages\Superpower.2.0.0\lib\net45\Superpower.dll 85 | 86 | 87 | 88 | ..\..\packages\System.Collections.Immutable.1.1.36\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll 89 | True 90 | 91 | 92 | ..\..\packages\Microsoft.Composition.1.0.27\lib\portable-net45+win8+wp8+wpa81\System.Composition.AttributedModel.dll 93 | True 94 | 95 | 96 | ..\..\packages\Microsoft.Composition.1.0.27\lib\portable-net45+win8+wp8+wpa81\System.Composition.Convention.dll 97 | True 98 | 99 | 100 | ..\..\packages\Microsoft.Composition.1.0.27\lib\portable-net45+win8+wp8+wpa81\System.Composition.Hosting.dll 101 | True 102 | 103 | 104 | ..\..\packages\Microsoft.Composition.1.0.27\lib\portable-net45+win8+wp8+wpa81\System.Composition.Runtime.dll 105 | True 106 | 107 | 108 | ..\..\packages\Microsoft.Composition.1.0.27\lib\portable-net45+win8+wp8+wpa81\System.Composition.TypedParts.dll 109 | True 110 | 111 | 112 | 113 | ..\..\packages\System.Reflection.Metadata.1.0.21\lib\portable-net45+win8\System.Reflection.Metadata.dll 114 | True 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | false 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | Designer 143 | 144 | 145 | 146 | 147 | {1666D457-D350-4322-B1BD-4DBDF530A882} 148 | SerilogAnalyzer 149 | 150 | 151 | 152 | 153 | 154 | 155 | 162 | -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer.Test/Verifiers/CodeFixVerifier.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Robin Sue 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | using Microsoft.CodeAnalysis; 16 | using Microsoft.CodeAnalysis.CodeActions; 17 | using Microsoft.CodeAnalysis.CodeFixes; 18 | using Microsoft.CodeAnalysis.Diagnostics; 19 | using Microsoft.CodeAnalysis.Formatting; 20 | using Microsoft.VisualStudio.TestTools.UnitTesting; 21 | using System.Collections.Generic; 22 | using System.Linq; 23 | using System.Threading; 24 | 25 | namespace TestHelper 26 | { 27 | /// 28 | /// Superclass of all Unit tests made for diagnostics with codefixes. 29 | /// Contains methods used to verify correctness of codefixes 30 | /// 31 | public abstract partial class CodeFixVerifier : DiagnosticVerifier 32 | { 33 | /// 34 | /// Returns the codefix being tested (C#) - to be implemented in non-abstract class 35 | /// 36 | /// The CodeFixProvider to be used for CSharp code 37 | protected virtual CodeFixProvider GetCSharpCodeFixProvider() 38 | { 39 | return null; 40 | } 41 | 42 | /// 43 | /// Returns the codefix being tested (VB) - to be implemented in non-abstract class 44 | /// 45 | /// The CodeFixProvider to be used for VisualBasic code 46 | protected virtual CodeFixProvider GetBasicCodeFixProvider() 47 | { 48 | return null; 49 | } 50 | 51 | /// 52 | /// Called to test a C# codefix when applied on the inputted string as a source 53 | /// 54 | /// A class in the form of a string before the CodeFix was applied to it 55 | /// A class in the form of a string after the CodeFix was applied to it 56 | /// Index determining which codefix to apply if there are multiple 57 | /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied 58 | protected void VerifyCSharpFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false) 59 | { 60 | VerifyFix(LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), GetCSharpCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics); 61 | } 62 | 63 | /// 64 | /// Called to test a VB codefix when applied on the inputted string as a source 65 | /// 66 | /// A class in the form of a string before the CodeFix was applied to it 67 | /// A class in the form of a string after the CodeFix was applied to it 68 | /// Index determining which codefix to apply if there are multiple 69 | /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied 70 | protected void VerifyBasicFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false) 71 | { 72 | VerifyFix(LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), GetBasicCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics); 73 | } 74 | 75 | /// 76 | /// General verifier for codefixes. 77 | /// Creates a Document from the source string, then gets diagnostics on it and applies the relevant codefixes. 78 | /// Then gets the string after the codefix is applied and compares it with the expected result. 79 | /// Note: If any codefix causes new diagnostics to show up, the test fails unless allowNewCompilerDiagnostics is set to true. 80 | /// 81 | /// The language the source code is in 82 | /// The analyzer to be applied to the source code 83 | /// The codefix to be applied to the code wherever the relevant Diagnostic is found 84 | /// A class in the form of a string before the CodeFix was applied to it 85 | /// A class in the form of a string after the CodeFix was applied to it 86 | /// Index determining which codefix to apply if there are multiple 87 | /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied 88 | private void VerifyFix(string language, DiagnosticAnalyzer analyzer, CodeFixProvider codeFixProvider, string oldSource, string newSource, int? codeFixIndex, bool allowNewCompilerDiagnostics) 89 | { 90 | var document = CreateDocument(oldSource, language); 91 | var analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document }); 92 | var compilerDiagnostics = GetCompilerDiagnostics(document); 93 | var attempts = analyzerDiagnostics.Length; 94 | 95 | for (int i = 0; i < attempts; ++i) 96 | { 97 | var actions = new List(); 98 | var context = new CodeFixContext(document, analyzerDiagnostics[0], (a, d) => actions.Add(a), CancellationToken.None); 99 | codeFixProvider.RegisterCodeFixesAsync(context).Wait(); 100 | 101 | if (!actions.Any()) 102 | { 103 | break; 104 | } 105 | 106 | if (codeFixIndex != null) 107 | { 108 | document = ApplyCodeAction(document, actions.ElementAt((int)codeFixIndex)); 109 | break; 110 | } 111 | 112 | document = ApplyCodeAction(document, actions.ElementAt(0)); 113 | analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document }); 114 | 115 | var newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document)); 116 | 117 | //check if applying the code fix introduced any new compiler diagnostics 118 | if (!allowNewCompilerDiagnostics && newCompilerDiagnostics.Any()) 119 | { 120 | // Format and get the compiler diagnostics again so that the locations make sense in the output 121 | document = document.WithSyntaxRoot(Formatter.Format(document.GetSyntaxRootAsync().Result, Formatter.Annotation, document.Project.Solution.Workspace)); 122 | newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document)); 123 | 124 | Assert.IsTrue(false, 125 | string.Format("Fix introduced new compiler diagnostics:\r\n{0}\r\n\r\nNew document:\r\n{1}\r\n", 126 | string.Join("\r\n", newCompilerDiagnostics.Select(d => d.ToString())), 127 | document.GetSyntaxRootAsync().Result.ToFullString())); 128 | } 129 | 130 | //check if there are analyzer diagnostics left after the code fix 131 | if (!analyzerDiagnostics.Any()) 132 | { 133 | break; 134 | } 135 | } 136 | 137 | //after applying all of the code fixes, compare the resulting string to the inputted one 138 | var actual = GetStringFromDocument(document); 139 | Assert.AreEqual(newSource, actual); 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer.Test/Verifiers/CodeRefactoringVerifier.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Robin Sue 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | using System; 16 | using System.Collections.Generic; 17 | using System.Linq; 18 | using System.Text; 19 | using System.Threading; 20 | using Microsoft.CodeAnalysis; 21 | using Microsoft.CodeAnalysis.CodeActions; 22 | using Microsoft.CodeAnalysis.CodeRefactorings; 23 | using Microsoft.CodeAnalysis.Text; 24 | using Microsoft.VisualStudio.TestTools.UnitTesting; 25 | 26 | namespace TestHelper 27 | { 28 | /// 29 | /// Superclass of all Unit tests made for code refactorings. 30 | /// Contains methods used to verify correctness of code refactorings 31 | /// 32 | public abstract class CodeRefactoringVerifier : DiagnosticVerifier 33 | { 34 | /// 35 | /// Returns the CodeRefactoring being tested (C#) - to be implemented in non-abstract class 36 | /// 37 | /// The CodeRefactoringProvider to be used for CSharp code 38 | protected virtual CodeRefactoringProvider GetCSharpCodeRefactoringProvider() 39 | { 40 | return null; 41 | } 42 | 43 | /// 44 | /// Returns the CodeRefactoring being tested (VB) - to be implemented in non-abstract class 45 | /// 46 | /// The CodeRefactoringProvider to be used for VisualBasic code 47 | protected virtual CodeRefactoringProvider GetBasicCodeRefactoringProvider() 48 | { 49 | return null; 50 | } 51 | 52 | /// 53 | /// Called to test a C# CodeRefactoring when applied on the inputted string as a source 54 | /// 55 | /// A class in the form of a string with [| |] selection markings before the CodeRefactoring was applied to it 56 | /// A class in the form of a string after the CodeRefactoring was applied to it 57 | /// Titel determining which CodeRefactoring to apply if there are multiple 58 | protected void VerifyCSharpRefactoring(string oldSource, string newSource, string codeRefactoringTitle = null) 59 | { 60 | VerifyRefactoring(LanguageNames.CSharp, GetCSharpCodeRefactoringProvider(), oldSource, newSource, codeRefactoringTitle); 61 | } 62 | 63 | /// 64 | /// Called to test a VB CodeRefactoring when applied on the inputted string as a source 65 | /// 66 | /// A class in the form of a string with [| |] selection markings before the CodeRefactoring was applied to it 67 | /// A class in the form of a string after the CodeRefactoring was applied to it 68 | /// Titel determining which CodeRefactoring to apply if there are multiple 69 | protected void VerifyBasicRefactoring(string oldSource, string newSource, string codeRefactoringTitle = null) 70 | { 71 | VerifyRefactoring(LanguageNames.VisualBasic, GetBasicCodeRefactoringProvider(), oldSource, newSource, codeRefactoringTitle); 72 | } 73 | 74 | /// 75 | /// General verifier for code refactorings. 76 | /// Creates a Document from the source string and applies the relevant code refactorings to the marked selection. 77 | /// Then gets the string after the code refactoring is applied and compares it with the expected result. 78 | /// 79 | /// The language the source code is in 80 | /// The code refactoring to be applied to the code 81 | /// A class in the form of a string with [| |] selection markings before the CodeRefactoring was applied to it 82 | /// A class in the form of a string after the CodeRefactoring was applied to it 83 | /// Titel determining which CodeRefactoring to apply if there are multiple 84 | private void VerifyRefactoring(string language, CodeRefactoringProvider codeRefactoringProvider, string oldSource, string newSource, string codeRefactoringTitle) 85 | { 86 | TextSpan span; 87 | if (!TryGetCodeAndSpanFromMarkup(oldSource, out oldSource, out span)) 88 | { 89 | throw new ArgumentException("There are no selection markings in the code", nameof(oldSource)); 90 | } 91 | 92 | var document = CreateDocument(oldSource, language); 93 | 94 | var actions = new List(); 95 | var context = new CodeRefactoringContext(document, span, (a) => actions.Add(a), CancellationToken.None); 96 | codeRefactoringProvider.ComputeRefactoringsAsync(context).Wait(); 97 | 98 | if (actions.Any()) 99 | { 100 | if (codeRefactoringTitle != null) 101 | { 102 | CodeAction codeAction = actions.FirstOrDefault(x => x.Title == codeRefactoringTitle); 103 | if (codeAction != null) 104 | { 105 | document = ApplyCodeAction(document, codeAction); 106 | } 107 | } 108 | else 109 | { 110 | document = ApplyCodeAction(document, actions.First()); 111 | } 112 | } 113 | 114 | //after applying all of the code actions, compare the resulting string to the inputted one 115 | var actual = GetStringFromDocument(document); 116 | Assert.AreEqual(newSource, actual); 117 | } 118 | 119 | /// 120 | /// Gets the selection from marked code as a TextSpan and the code without markings 121 | /// 122 | /// Code marked with [| |] 123 | /// Code without markings 124 | /// TextSpan representing the marked code 125 | /// 126 | public static bool TryGetCodeAndSpanFromMarkup(string markupCode, out string code, out TextSpan span) 127 | { 128 | code = null; 129 | span = default(TextSpan); 130 | 131 | var builder = new StringBuilder(); 132 | 133 | var start = markupCode.IndexOf("[|", StringComparison.Ordinal); 134 | if (start < 0) 135 | { 136 | return false; 137 | } 138 | 139 | builder.Append(markupCode.Substring(0, start)); 140 | 141 | var end = markupCode.IndexOf("|]", StringComparison.Ordinal); 142 | if (end < 0) 143 | { 144 | return false; 145 | } 146 | 147 | builder.Append(markupCode.Substring(start + 2, end - start - 2)); 148 | builder.Append(markupCode.Substring(end + 2)); 149 | 150 | code = builder.ToString(); 151 | span = TextSpan.FromBounds(start, end - 2); 152 | 153 | return true; 154 | } 155 | } 156 | } -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer.Test/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer.Vsix/SerilogAnalyzer.Vsix.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 16.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | Debug 10 | AnyCPU 11 | 2.0 12 | {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 13 | {5910AC72-0F22-437F-902A-177E0C2318A8} 14 | Library 15 | Properties 16 | SerilogAnalyzer.Vsix 17 | SerilogAnalyzer.Vsix 18 | v4.6.1 19 | false 20 | false 21 | false 22 | false 23 | false 24 | false 25 | Roslyn 26 | 27 | 28 | true 29 | full 30 | false 31 | bin\Debug\ 32 | DEBUG;TRACE 33 | prompt 34 | 4 35 | 36 | 37 | pdbonly 38 | true 39 | bin\Release\ 40 | TRACE 41 | prompt 42 | 4 43 | 44 | 45 | Program 46 | $(DevEnvDir)devenv.exe 47 | /rootsuffix Roslyn 48 | 49 | 50 | 51 | Designer 52 | 53 | 54 | 55 | 56 | {1666D457-D350-4322-B1BD-4DBDF530A882} 57 | SerilogAnalyzer 58 | 59 | 60 | 61 | 62 | Always 63 | true 64 | 65 | 66 | Always 67 | true 68 | 69 | 70 | 71 | 72 | 79 | -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer.Vsix/analyzer-200px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suchiman/SerilogAnalyzer/03309d5151df784d6b987873fc66815ca362138b/SerilogAnalyzer/SerilogAnalyzer.Vsix/analyzer-200px.png -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer.Vsix/analyzer-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Suchiman/SerilogAnalyzer/03309d5151df784d6b987873fc66815ca362138b/SerilogAnalyzer/SerilogAnalyzer.Vsix/analyzer-32px.png -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer.Vsix/source.extension.vsixmanifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Serilog Analyzer 6 | Roslyn-based analysis for code using the Serilog logging library. Checks for common mistakes and usage problems. 7 | https://github.com/Suchiman/SerilogAnalyzer 8 | https://github.com/Suchiman/SerilogAnalyzer 9 | https://github.com/Suchiman/SerilogAnalyzer/releases/tag/0.15 10 | analyzer-32px.png 11 | analyzer-200px.png 12 | Serilog, Refactoring, Free, code, Open Source, C#, analyzer, Quality, refactor, roslyn, fix, improve 13 | 14 | 15 | 16 | amd64 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/AnalyzingMessageTemplateParser.cs: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/serilog/serilog/blob/e97b3c028bdb28e4430512b84dc2082e6f98dcc7/src/Serilog/Parsing/MessageTemplateParser.cs 2 | // Copyright 2013-2015 Serilog Contributors 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | using System; 17 | using System.Collections.Generic; 18 | 19 | namespace SerilogAnalyzer 20 | { 21 | static class AnalyzingMessageTemplateParser 22 | { 23 | public static IEnumerable Analyze(string messageTemplate) 24 | { 25 | if (messageTemplate == null) 26 | throw new ArgumentNullException(nameof(messageTemplate)); 27 | 28 | if (messageTemplate == "") 29 | yield break; 30 | 31 | var nextIndex = 0; 32 | while (true) 33 | { 34 | ParseTextToken(nextIndex, messageTemplate, out nextIndex); 35 | 36 | if (nextIndex == messageTemplate.Length) 37 | yield break; 38 | 39 | var beforeProp = nextIndex; 40 | var pt = ParsePropertyToken(nextIndex, messageTemplate, out nextIndex); 41 | if (beforeProp < nextIndex) 42 | yield return pt; 43 | 44 | if (nextIndex == messageTemplate.Length) 45 | yield break; 46 | } 47 | } 48 | 49 | static MessageTemplateToken ParsePropertyToken(int startAt, string messageTemplate, out int next) 50 | { 51 | var first = startAt; 52 | startAt++; 53 | while (startAt < messageTemplate.Length && IsValidInPropertyTag(messageTemplate[startAt])) 54 | startAt++; 55 | 56 | if (startAt == messageTemplate.Length) 57 | { 58 | next = startAt; 59 | return new MessageTemplateDiagnostic(first, next - first, "Encountered end of messageTemplate while parsing property"); 60 | } 61 | 62 | if (messageTemplate[startAt] != '}') 63 | { 64 | next = startAt; 65 | return new MessageTemplateDiagnostic(startAt, 1, "Found invalid character '" + messageTemplate[startAt].ToString() + "' in property"); 66 | } 67 | 68 | next = startAt + 1; 69 | 70 | var rawText = messageTemplate.Substring(first, next - first); 71 | var tagContent = messageTemplate.Substring(first + 1, next - (first + 2)); 72 | if (tagContent.Length == 0) 73 | return new MessageTemplateDiagnostic(first, rawText.Length, "Found property without name"); 74 | 75 | string propertyNameAndDestructuring, format, alignment; 76 | MessageTemplateDiagnostic tagContentDiagnostic; 77 | if (!TrySplitTagContent(tagContent, out propertyNameAndDestructuring, out format, out alignment, out tagContentDiagnostic)) 78 | { 79 | tagContentDiagnostic.StartIndex += first + 1; 80 | return tagContentDiagnostic; 81 | } 82 | 83 | var propertyName = propertyNameAndDestructuring; 84 | bool hasDestructuring = IsValidInDestructuringHint(propertyName[0]); 85 | if (hasDestructuring) 86 | propertyName = propertyName.Substring(1); 87 | 88 | if (propertyName == "") 89 | return new MessageTemplateDiagnostic(first, rawText.Length, "Found property with destructuring hint but without name"); 90 | 91 | for (var i = 0; i < propertyName.Length; ++i) 92 | { 93 | var c = propertyName[i]; 94 | if (!IsValidInPropertyName(c)) 95 | return new MessageTemplateDiagnostic(first + (hasDestructuring ? 1 : 0) + 1 + i, 1, "Found invalid character '" + c.ToString() + "' in property name"); 96 | } 97 | 98 | if (format != null) 99 | { 100 | for (var i = 0; i < format.Length; ++i) 101 | { 102 | var c = format[i]; 103 | if (!IsValidInFormat(c)) 104 | return new MessageTemplateDiagnostic(first + propertyNameAndDestructuring.Length + (alignment?.Length + 1 ?? 0) + 2 + i, 1, "Found invalid character '" + c.ToString() + "' in property format"); 105 | } 106 | } 107 | 108 | if (alignment != null) 109 | { 110 | for (var i = 0; i < alignment.Length; ++i) 111 | { 112 | var c = alignment[i]; 113 | if (!IsValidInAlignment(c)) 114 | return new MessageTemplateDiagnostic(first + propertyNameAndDestructuring.Length + 2 + i, 1, "Found invalid character '" + c.ToString() + "' in property alignment"); 115 | } 116 | 117 | var lastDash = alignment.LastIndexOf('-'); 118 | if (lastDash > 0) 119 | return new MessageTemplateDiagnostic(first + propertyNameAndDestructuring.Length + 2 + lastDash, 1, "'-' character must be the first in alignment"); 120 | 121 | var width = lastDash == -1 ? 122 | int.Parse(alignment) : 123 | int.Parse(alignment.Substring(1)); 124 | 125 | if (width == 0) 126 | return new MessageTemplateDiagnostic(first + propertyNameAndDestructuring.Length + 2, alignment.Length, "Found zero size alignment"); 127 | } 128 | 129 | return new PropertyToken(first, propertyName, rawText); 130 | } 131 | 132 | static bool TrySplitTagContent(string tagContent, out string propertyNameAndDestructuring, out string format, out string alignment, out MessageTemplateDiagnostic diagnostic) 133 | { 134 | var formatDelim = tagContent.IndexOf(':'); 135 | var alignmentDelim = tagContent.IndexOf(','); 136 | if (formatDelim == -1 && alignmentDelim == -1) 137 | { 138 | propertyNameAndDestructuring = tagContent; 139 | format = null; 140 | alignment = null; 141 | diagnostic = null; 142 | } 143 | else 144 | { 145 | if (alignmentDelim == -1 || (formatDelim != -1 && alignmentDelim > formatDelim)) 146 | { 147 | propertyNameAndDestructuring = tagContent.Substring(0, formatDelim); 148 | format = formatDelim == tagContent.Length - 1 ? 149 | null : 150 | tagContent.Substring(formatDelim + 1); 151 | alignment = null; 152 | diagnostic = null; 153 | } 154 | else 155 | { 156 | propertyNameAndDestructuring = tagContent.Substring(0, alignmentDelim); 157 | if (formatDelim == -1) 158 | { 159 | if (alignmentDelim == tagContent.Length - 1) 160 | { 161 | alignment = format = null; 162 | diagnostic = new MessageTemplateDiagnostic(alignmentDelim, 1, "Found alignment specifier without alignment"); 163 | return false; 164 | } 165 | 166 | format = null; 167 | alignment = tagContent.Substring(alignmentDelim + 1); 168 | } 169 | else 170 | { 171 | if (alignmentDelim == formatDelim - 1) 172 | { 173 | alignment = format = null; 174 | diagnostic = new MessageTemplateDiagnostic(alignmentDelim, 1, "Found alignment specifier without alignment"); 175 | return false; 176 | } 177 | 178 | alignment = tagContent.Substring(alignmentDelim + 1, formatDelim - alignmentDelim - 1); 179 | format = formatDelim == tagContent.Length - 1 ? 180 | null : 181 | tagContent.Substring(formatDelim + 1); 182 | } 183 | } 184 | } 185 | 186 | diagnostic = null; 187 | return true; 188 | } 189 | 190 | static bool IsValidInPropertyTag(char c) 191 | { 192 | return IsValidInDestructuringHint(c) || 193 | IsValidInPropertyName(c) || 194 | IsValidInFormat(c) || 195 | c == ':'; 196 | } 197 | 198 | static bool IsValidInPropertyName(char c) 199 | { 200 | return char.IsLetterOrDigit(c) || c == '_'; 201 | } 202 | 203 | static bool IsValidInDestructuringHint(char c) 204 | { 205 | return c == '@' || 206 | c == '$'; 207 | } 208 | 209 | static bool IsValidInAlignment(char c) 210 | { 211 | return char.IsDigit(c) || 212 | c == '-'; 213 | } 214 | 215 | static bool IsValidInFormat(char c) 216 | { 217 | return c != '}' && 218 | (char.IsLetterOrDigit(c) || 219 | char.IsPunctuation(c) || 220 | c == ' '); 221 | } 222 | 223 | static void ParseTextToken(int startAt, string messageTemplate, out int next) 224 | { 225 | var first = startAt; 226 | 227 | do 228 | { 229 | var nc = messageTemplate[startAt]; 230 | if (nc == '{') 231 | { 232 | if (startAt + 1 < messageTemplate.Length && 233 | messageTemplate[startAt + 1] == '{') 234 | { 235 | startAt++; 236 | } 237 | else 238 | { 239 | break; 240 | } 241 | } 242 | else 243 | { 244 | if (nc == '}') 245 | { 246 | if (startAt + 1 < messageTemplate.Length && 247 | messageTemplate[startAt + 1] == '}') 248 | { 249 | startAt++; 250 | } 251 | } 252 | } 253 | 254 | startAt++; 255 | } while (startAt < messageTemplate.Length); 256 | 257 | next = startAt; 258 | } 259 | } 260 | 261 | class MessageTemplateDiagnostic : MessageTemplateToken 262 | { 263 | public string Diagnostic { get; set; } 264 | public bool MustBeRemapped { get; set; } 265 | 266 | public MessageTemplateDiagnostic(int startIndex, int length, string diagnostic = null, bool mustBeRemapped = true) 267 | : base(startIndex, length) 268 | { 269 | Diagnostic = diagnostic; 270 | MustBeRemapped = mustBeRemapped; 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/CodeFixProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Robin Sue 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | using System.Collections.Immutable; 16 | using System.Composition; 17 | using System.Linq; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | using Microsoft.CodeAnalysis; 21 | using Microsoft.CodeAnalysis.CodeActions; 22 | using Microsoft.CodeAnalysis.CodeFixes; 23 | using Microsoft.CodeAnalysis.CSharp.Syntax; 24 | 25 | namespace SerilogAnalyzer 26 | { 27 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SerilogAnalyzerCodeFixProvider)), Shared] 28 | public class SerilogAnalyzerCodeFixProvider : CodeFixProvider 29 | { 30 | private const string title = "Make exception the first argument"; 31 | 32 | public sealed override ImmutableArray FixableDiagnosticIds 33 | { 34 | get { return ImmutableArray.Create(SerilogAnalyzerAnalyzer.ExceptionDiagnosticId); } 35 | } 36 | 37 | public sealed override FixAllProvider GetFixAllProvider() 38 | { 39 | return WellKnownFixAllProviders.BatchFixer; 40 | } 41 | 42 | public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) 43 | { 44 | var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); 45 | 46 | var diagnostic = context.Diagnostics.First(); 47 | var diagnosticSpan = diagnostic.Location.SourceSpan; 48 | 49 | var declaration = root.FindNode(diagnosticSpan) as ArgumentSyntax; 50 | 51 | context.RegisterCodeFix( 52 | CodeAction.Create( 53 | title: title, 54 | createChangedSolution: c => MoveExceptionFirstAsync(context.Document, declaration, c), 55 | equivalenceKey: title), 56 | diagnostic); 57 | } 58 | 59 | private async Task MoveExceptionFirstAsync(Document document, ArgumentSyntax argument, CancellationToken cancellationToken) 60 | { 61 | var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); 62 | var argumentList = argument.AncestorsAndSelf().OfType().First(); 63 | 64 | var newList = argumentList.Arguments.Remove(argument); 65 | 66 | var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); 67 | var symbolInfo = semanticModel.GetSymbolInfo(argumentList.Parent); 68 | 69 | if (symbolInfo.Symbol is IMethodSymbol methodSymbol && methodSymbol.IsExtensionMethod && methodSymbol.IsStatic) 70 | { 71 | // This is a static method invocation of an extension method, so the first parameter is the 72 | // extended type itself; hence we insert at the second position 73 | newList = newList.Insert(1, argument); 74 | } 75 | else 76 | { 77 | newList = newList.Insert(0, argument); 78 | } 79 | 80 | root = root.ReplaceNode(argumentList, argumentList.WithArguments(newList)); 81 | document = document.WithSyntaxRoot(root); 82 | 83 | return document.Project.Solution; 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/ConfigurationModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.CSharp; 4 | 5 | namespace SerilogAnalyzer 6 | { 7 | class LoggerConfiguration 8 | { 9 | public string MinimumLevel { get; set; } 10 | public string MinimumLevelControlledBy { get; set; } 11 | public Dictionary MinimumLevelOverrides { get; set; } = new Dictionary(); 12 | public Dictionary LevelSwitches { get; set; } = new Dictionary(); 13 | public List Filter { get; set; } = new List(); 14 | public List Destructure { get; set; } = new List(); 15 | public List WriteTo { get; set; } = new List(); 16 | public List AuditTo { get; set; } = new List(); 17 | public List Enrich { get; set; } = new List(); 18 | public Dictionary EnrichWithProperty { get; set; } = new Dictionary(); 19 | 20 | public List ErrorLog { get; set; } = new List(); 21 | public bool HasParsingErrors => ErrorLog.Count > 0; 22 | 23 | public void AddError(string message, CSharpSyntaxNode syntax) 24 | { 25 | ErrorLog.Add($"{(syntax == null ? "unknown" : FormatLineSpan(syntax.GetLocation().GetMappedLineSpan()))}: `{syntax}` -> {message}"); 26 | } 27 | 28 | public void AddNonConstantError(CSharpSyntaxNode syntax) 29 | { 30 | ErrorLog.Add($"{(syntax == null ? "unknown" : FormatLineSpan(syntax.GetLocation().GetMappedLineSpan()))}: `{syntax}` -> Can't statically determine value of expression"); 31 | } 32 | 33 | private static string FormatLineSpan(FileLinePositionSpan span) 34 | { 35 | return $"{span.Path}: ({span.StartLinePosition.Line + 1},{span.StartLinePosition.Character})-({span.EndLinePosition.Line + 1},{span.EndLinePosition.Character})"; 36 | } 37 | } 38 | 39 | class ExtensibleMethod 40 | { 41 | public string AssemblyName { get; set; } 42 | public string MethodName { get; set; } 43 | public Dictionary Arguments { get; set; } = new Dictionary(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/ConvertToMessageTemplateCodeRefactoringProvider.StringConcat.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Robin Sue 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | using Microsoft.CodeAnalysis; 16 | using Microsoft.CodeAnalysis.CSharp; 17 | using Microsoft.CodeAnalysis.CSharp.Syntax; 18 | using System.Collections.Generic; 19 | using System.Linq; 20 | using System.Text; 21 | using System.Threading; 22 | using System.Threading.Tasks; 23 | 24 | namespace SerilogAnalyzer 25 | { 26 | public partial class ConvertToMessageTemplateCodeFixProvider 27 | { 28 | private async Task ConvertStringConcatToMessageTemplateAsync(Document document, ExpressionSyntax stringConcat, InvocationExpressionSyntax logger, CancellationToken cancellationToken) 29 | { 30 | GetFormatStringAndExpressionsFromStringConcat(stringConcat, out var format, out var expressions); 31 | 32 | return await InlineFormatAndArgumentsIntoLoggerStatementAsync(document, stringConcat, logger, format, expressions, cancellationToken).ConfigureAwait(false); 33 | } 34 | 35 | private static void GetFormatStringAndExpressionsFromStringConcat(ExpressionSyntax stringConcat, out InterpolatedStringExpressionSyntax format, out List expressions) 36 | { 37 | var concatExpressions = new List(); 38 | void FindExpressions(ExpressionSyntax exp) 39 | { 40 | switch (exp) 41 | { 42 | case BinaryExpressionSyntax binary when binary.OperatorToken.IsKind(SyntaxKind.PlusToken): 43 | FindExpressions(binary.Left); 44 | FindExpressions(binary.Right); 45 | break; 46 | case ParenthesizedExpressionSyntax parens: 47 | FindExpressions(parens.Expression); 48 | break; 49 | case LiteralExpressionSyntax literal: 50 | concatExpressions.Add(literal); 51 | break; 52 | default: 53 | concatExpressions.Add(exp.Parent is ParenthesizedExpressionSyntax paren ? paren : exp); 54 | break; 55 | } 56 | } 57 | FindExpressions(stringConcat); 58 | 59 | var sb = new StringBuilder(); 60 | var replacements = new List(); 61 | bool shouldUseVerbatim = false; 62 | int argumentPosition = 0; 63 | foreach (var child in concatExpressions) 64 | { 65 | switch (child) 66 | { 67 | case LiteralExpressionSyntax literal: 68 | sb.Append(literal.Token.ValueText); 69 | shouldUseVerbatim |= literal.Token.Text.StartsWith("@", System.StringComparison.Ordinal) && ContainsQuotesOrLineBreaks(literal.Token.ValueText); 70 | break; 71 | case ExpressionSyntax exp: 72 | 73 | sb.Append("{"); 74 | sb.Append(ConversionName); 75 | sb.Append(argumentPosition++); 76 | sb.Append("}"); 77 | 78 | break; 79 | } 80 | } 81 | 82 | if (shouldUseVerbatim) 83 | { 84 | for (int i = 0; i < sb.Length; i++) 85 | { 86 | if (IsForbiddenInVerbatimString(sb[i])) 87 | { 88 | shouldUseVerbatim = false; 89 | break; 90 | } 91 | } 92 | } 93 | 94 | var text = ObjectDisplay.FormatLiteral(sb.ToString(), useQuotes: true, escapeNonPrintable: !shouldUseVerbatim); 95 | 96 | format = (InterpolatedStringExpressionSyntax)SyntaxFactory.ParseExpression("$" + text); 97 | expressions = concatExpressions.Where(x => !(x is LiteralExpressionSyntax)).ToList(); 98 | } 99 | 100 | private static bool IsForbiddenInVerbatimString(char c) 101 | { 102 | switch (c) 103 | { 104 | case '\a': 105 | case '\b': 106 | case '\f': 107 | case '\v': 108 | case '\0': 109 | return true; 110 | } 111 | 112 | return false; 113 | } 114 | 115 | private static bool ContainsQuotesOrLineBreaks(string s) 116 | { 117 | foreach (char c in s) 118 | { 119 | if (c == '\r' || c == '\n' || c == '"') 120 | { 121 | return true; 122 | } 123 | } 124 | 125 | return false; 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/ConvertToMessageTemplateCodeRefactoringProvider.StringFormat.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Robin Sue 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | using Microsoft.CodeAnalysis; 16 | using Microsoft.CodeAnalysis.CSharp; 17 | using Microsoft.CodeAnalysis.CSharp.Syntax; 18 | using System; 19 | using System.Collections.Generic; 20 | using System.Linq; 21 | using System.Text; 22 | using System.Threading; 23 | using System.Threading.Tasks; 24 | 25 | namespace SerilogAnalyzer 26 | { 27 | public partial class ConvertToMessageTemplateCodeFixProvider 28 | { 29 | private async Task ConvertStringFormatToMessageTemplateAsync(Document document, InvocationExpressionSyntax stringFormat, InvocationExpressionSyntax logger, CancellationToken cancellationToken) 30 | { 31 | GetFormatStringAndExpressionsFromStringFormat(stringFormat, out var format, out var expressions); 32 | 33 | return await InlineFormatAndArgumentsIntoLoggerStatementAsync(document, stringFormat, logger, format, expressions, cancellationToken).ConfigureAwait(false); 34 | } 35 | 36 | private static void GetFormatStringAndExpressionsFromStringFormat(InvocationExpressionSyntax stringFormat, out InterpolatedStringExpressionSyntax format, out List expressions) 37 | { 38 | var arguments = stringFormat.ArgumentList.Arguments; 39 | var formatString = ((LiteralExpressionSyntax)arguments[0].Expression).Token.ToString(); 40 | var interpolatedString = (InterpolatedStringExpressionSyntax)SyntaxFactory.ParseExpression("$" + formatString); 41 | 42 | var sb = new StringBuilder(); 43 | var replacements = new List(); 44 | foreach (var child in interpolatedString.Contents) 45 | { 46 | switch (child) 47 | { 48 | case InterpolatedStringTextSyntax text: 49 | sb.Append(text.TextToken.ToString()); 50 | break; 51 | case InterpolationSyntax interpolation: 52 | int argumentPosition; 53 | if (interpolation.Expression is LiteralExpressionSyntax literal && literal.IsKind(SyntaxKind.NumericLiteralExpression)) 54 | { 55 | argumentPosition = (int)literal.Token.Value; 56 | } 57 | else 58 | { 59 | argumentPosition = -1; 60 | } 61 | 62 | sb.Append("{"); 63 | sb.Append(replacements.Count); 64 | sb.Append("}"); 65 | 66 | replacements.Add($"{{{ConversionName}{argumentPosition}{interpolation.AlignmentClause}{interpolation.FormatClause}}}"); 67 | 68 | break; 69 | } 70 | } 71 | 72 | format = (InterpolatedStringExpressionSyntax)SyntaxFactory.ParseExpression("$\"" + String.Format(sb.ToString(), replacements.ToArray()) + "\""); 73 | expressions = arguments.Skip(1).Select(x => x.Expression).ToList(); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/ConvertToMessageTemplateCodeRefactoringProvider.StringInterpolation.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Robin Sue 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | using Microsoft.CodeAnalysis; 16 | using Microsoft.CodeAnalysis.CSharp; 17 | using Microsoft.CodeAnalysis.CSharp.Syntax; 18 | using System; 19 | using System.Collections.Generic; 20 | using System.Text; 21 | using System.Threading; 22 | using System.Threading.Tasks; 23 | 24 | namespace SerilogAnalyzer 25 | { 26 | public partial class ConvertToMessageTemplateCodeFixProvider 27 | { 28 | private async Task ConvertInterpolationToMessageTemplateAsync(Document document, InterpolatedStringExpressionSyntax interpolatedString, InvocationExpressionSyntax logger, CancellationToken cancellationToken) 29 | { 30 | GetFormatStringAndExpressionsFromInterpolation(interpolatedString, out var format, out var expressions); 31 | 32 | return await InlineFormatAndArgumentsIntoLoggerStatementAsync(document, interpolatedString, logger, format, expressions, cancellationToken).ConfigureAwait(false); 33 | } 34 | 35 | private static void GetFormatStringAndExpressionsFromInterpolation(InterpolatedStringExpressionSyntax interpolatedString, out InterpolatedStringExpressionSyntax format, out List expressions) 36 | { 37 | var sb = new StringBuilder(); 38 | var replacements = new List(); 39 | var interpolations = new List(); 40 | foreach (var child in interpolatedString.Contents) 41 | { 42 | switch (child) 43 | { 44 | case InterpolatedStringTextSyntax text: 45 | sb.Append(text.TextToken.ToString()); 46 | break; 47 | case InterpolationSyntax interpolation: 48 | int argumentPosition = interpolations.Count; 49 | interpolations.Add(interpolation.Expression); 50 | 51 | sb.Append("{"); 52 | sb.Append(replacements.Count); 53 | sb.Append("}"); 54 | 55 | replacements.Add($"{{{ConversionName}{argumentPosition}{interpolation.AlignmentClause}{interpolation.FormatClause}}}"); 56 | 57 | break; 58 | } 59 | } 60 | 61 | format = (InterpolatedStringExpressionSyntax)SyntaxFactory.ParseExpression("$\"" + String.Format(sb.ToString(), replacements.ToArray()) + "\""); 62 | expressions = interpolations; 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/ConvertToMessageTemplateCodeRefactoringProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Robin Sue 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | using Microsoft.CodeAnalysis; 16 | using Microsoft.CodeAnalysis.CodeActions; 17 | using Microsoft.CodeAnalysis.CodeFixes; 18 | using Microsoft.CodeAnalysis.CSharp; 19 | using Microsoft.CodeAnalysis.CSharp.Syntax; 20 | using Microsoft.CodeAnalysis.Formatting; 21 | using System; 22 | using System.Collections.Generic; 23 | using System.Collections.Immutable; 24 | using System.Composition; 25 | using System.Linq; 26 | using System.Text; 27 | using System.Threading; 28 | using System.Threading.Tasks; 29 | 30 | namespace SerilogAnalyzer 31 | { 32 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ConvertToMessageTemplateCodeFixProvider)), Shared] 33 | public partial class ConvertToMessageTemplateCodeFixProvider : CodeFixProvider 34 | { 35 | private const string title = "Convert to MessageTemplate"; 36 | private const string ConversionName = "SerilogAnalyzer-"; 37 | 38 | public sealed override ImmutableArray FixableDiagnosticIds 39 | { 40 | get { return ImmutableArray.Create(SerilogAnalyzerAnalyzer.ConstantMessageTemplateDiagnosticId); } 41 | } 42 | 43 | public sealed override FixAllProvider GetFixAllProvider() 44 | { 45 | return WellKnownFixAllProviders.BatchFixer; 46 | } 47 | 48 | public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) 49 | { 50 | var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); 51 | 52 | var diagnostic = context.Diagnostics.First(); 53 | var diagnosticSpan = diagnostic.Location.SourceSpan; 54 | 55 | var declaration = root.FindNode(diagnosticSpan) as ArgumentSyntax; 56 | 57 | if (declaration.Parent.Parent is InvocationExpressionSyntax logger) 58 | { 59 | var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); 60 | if (declaration.Expression is InvocationExpressionSyntax inv && semanticModel.GetSymbolInfo(inv.Expression).Symbol is IMethodSymbol symbol && symbol.ToString().StartsWith("string.Format(") && inv.ArgumentList?.Arguments.Count > 0) 61 | { 62 | context.RegisterCodeFix(CodeAction.Create(title, c => ConvertStringFormatToMessageTemplateAsync(context.Document, inv, logger, c), title), diagnostic); 63 | } 64 | else if (declaration.Expression is InterpolatedStringExpressionSyntax interpolatedString) 65 | { 66 | context.RegisterCodeFix(CodeAction.Create(title, c => ConvertInterpolationToMessageTemplateAsync(context.Document, interpolatedString, logger, c), title), diagnostic); 67 | } 68 | else if (declaration.Expression.DescendantNodesAndSelf().OfType().Any()) 69 | { 70 | context.RegisterCodeFix(CodeAction.Create(title, c => ConvertStringConcatToMessageTemplateAsync(context.Document, declaration.Expression, logger, c), title), diagnostic); 71 | } 72 | } 73 | } 74 | 75 | private static async Task InlineFormatAndArgumentsIntoLoggerStatementAsync(Document document, ExpressionSyntax originalTemplateExpression, InvocationExpressionSyntax logger, InterpolatedStringExpressionSyntax format, List expressions, CancellationToken cancellationToken) 76 | { 77 | var loggerArguments = logger.ArgumentList.Arguments; 78 | var argumentIndex = loggerArguments.IndexOf(x => x.Expression == originalTemplateExpression); 79 | 80 | var sb = new StringBuilder(); 81 | if (format.StringStartToken.ValueText.Contains("@")) 82 | { 83 | sb.Append('@'); 84 | } 85 | sb.Append('"'); 86 | 87 | var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); 88 | var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); 89 | 90 | var usedNames = new HashSet(); 91 | var argumentExpressions = new List(); 92 | 93 | int indexFromOriginalLoggingArguments = argumentIndex + 1; 94 | foreach (var child in format.Contents) 95 | { 96 | switch (child) 97 | { 98 | case InterpolatedStringTextSyntax text: 99 | sb.Append(text.TextToken.ToString()); 100 | break; 101 | case InterpolationSyntax interpolation: 102 | string expressionText = interpolation.Expression.ToString(); 103 | ExpressionSyntax correspondingArgument = null; 104 | string name; 105 | if (expressionText.StartsWith(ConversionName, StringComparison.Ordinal) && Int32.TryParse(expressionText.Substring(ConversionName.Length), out int index)) 106 | { 107 | correspondingArgument = expressions.ElementAtOrDefault(index); 108 | 109 | if (correspondingArgument != null) 110 | { 111 | name = RoslynHelper.GenerateNameForExpression(semanticModel, correspondingArgument, true).NullWhenWhitespace() ?? "Error"; 112 | } 113 | else // in case this string.format is faulty 114 | { 115 | correspondingArgument = SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression); 116 | name = "Error"; 117 | } 118 | } 119 | else 120 | { 121 | correspondingArgument = loggerArguments.ElementAtOrDefault(indexFromOriginalLoggingArguments++)?.Expression; 122 | if (!String.IsNullOrWhiteSpace(expressionText)) 123 | { 124 | name = expressionText; 125 | } 126 | else if (correspondingArgument != null) 127 | { 128 | name = RoslynHelper.GenerateNameForExpression(semanticModel, correspondingArgument, true).NullWhenWhitespace() ?? "Error"; 129 | } 130 | else // in case this string.format is faulty 131 | { 132 | correspondingArgument = SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression); 133 | name = "Error"; 134 | } 135 | } 136 | 137 | argumentExpressions.Add(correspondingArgument); 138 | 139 | sb.Append("{"); 140 | 141 | int attempt = 0; 142 | string lastAttempt; 143 | while (!usedNames.Add(lastAttempt = (attempt == 0 ? name : name + attempt))) 144 | { 145 | attempt++; 146 | } 147 | 148 | sb.Append(lastAttempt); 149 | 150 | if (interpolation.AlignmentClause != null) 151 | sb.Append(interpolation.AlignmentClause); 152 | 153 | if (interpolation.FormatClause != null) 154 | sb.Append(interpolation.FormatClause); 155 | 156 | sb.Append("}"); 157 | break; 158 | } 159 | } 160 | 161 | sb.Append('"'); 162 | var messageTemplate = SyntaxFactory.Argument(SyntaxFactory.ParseExpression(sb.ToString())); 163 | 164 | var seperatedSyntax = loggerArguments.Replace(loggerArguments[argumentIndex], messageTemplate); 165 | 166 | // remove any arguments that we've put into argumentExpressions 167 | if (indexFromOriginalLoggingArguments > argumentIndex + 1) 168 | { 169 | for (int i = Math.Min(indexFromOriginalLoggingArguments, seperatedSyntax.Count) - 1; i > argumentIndex; i--) 170 | { 171 | seperatedSyntax = seperatedSyntax.RemoveAt(i); 172 | } 173 | } 174 | 175 | seperatedSyntax = seperatedSyntax.InsertRange(argumentIndex + 1, argumentExpressions.Select(x => SyntaxFactory.Argument(x ?? SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression)))); 176 | 177 | var newLogger = logger.WithArgumentList(SyntaxFactory.ArgumentList(seperatedSyntax)).WithAdditionalAnnotations(Formatter.Annotation); 178 | return document.WithSyntaxRoot(syntaxRoot.ReplaceNode(logger, newLogger)); 179 | } 180 | } 181 | } -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/CorrectLoggerContextCodeFixProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Robin Sue 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | using System.Collections.Immutable; 16 | using System.Composition; 17 | using System.Linq; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | using Microsoft.CodeAnalysis; 21 | using Microsoft.CodeAnalysis.CodeActions; 22 | using Microsoft.CodeAnalysis.CodeFixes; 23 | using Microsoft.CodeAnalysis.CSharp; 24 | using Microsoft.CodeAnalysis.CSharp.Syntax; 25 | using Microsoft.CodeAnalysis.Simplification; 26 | 27 | namespace SerilogAnalyzer 28 | { 29 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CorrectLoggerContextCodeFixProvider)), Shared] 30 | public class CorrectLoggerContextCodeFixProvider : CodeFixProvider 31 | { 32 | private const string title = "Use correct context for logger"; 33 | 34 | public sealed override ImmutableArray FixableDiagnosticIds 35 | { 36 | get { return ImmutableArray.Create(SerilogAnalyzerAnalyzer.UseCorrectContextualLoggerDiagnosticId); } 37 | } 38 | 39 | public sealed override FixAllProvider GetFixAllProvider() 40 | { 41 | return WellKnownFixAllProviders.BatchFixer; 42 | } 43 | 44 | public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) 45 | { 46 | var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); 47 | 48 | var diagnostic = context.Diagnostics.First(); 49 | var diagnosticSpan = diagnostic.Location.SourceSpan; 50 | 51 | var declaration = root.FindNode(diagnosticSpan) as TypeSyntax; 52 | 53 | context.RegisterCodeFix( 54 | CodeAction.Create( 55 | title: title, 56 | createChangedSolution: c => UseCorrectType(context.Document, declaration, c), 57 | equivalenceKey: title), 58 | diagnostic); 59 | } 60 | 61 | private async Task UseCorrectType(Document document, TypeSyntax syntax, CancellationToken cancellationToken) 62 | { 63 | var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); 64 | var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); 65 | 66 | var argumentList = syntax.Ancestors().OfType().First(); 67 | var symbol = semanticModel.GetDeclaredSymbol(argumentList, cancellationToken); 68 | 69 | var identifier = SyntaxFactory.ParseName(symbol.ToString()).WithAdditionalAnnotations(Simplifier.Annotation); 70 | root = root.ReplaceNode(syntax, identifier); 71 | document = document.WithSyntaxRoot(root); 72 | 73 | return document.Project.Solution; 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/DestructuringHintCodeFixProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Robin Sue 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | using System.Collections.Immutable; 16 | using System.Composition; 17 | using System.Linq; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | using Microsoft.CodeAnalysis; 21 | using Microsoft.CodeAnalysis.CodeActions; 22 | using Microsoft.CodeAnalysis.CodeFixes; 23 | using Microsoft.CodeAnalysis.Text; 24 | 25 | namespace SerilogAnalyzer 26 | { 27 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DestructuringHintCodeFixProvider)), Shared] 28 | public class DestructuringHintCodeFixProvider : CodeFixProvider 29 | { 30 | private const string title = "Add destructuring hint for anonymous object"; 31 | 32 | public sealed override ImmutableArray FixableDiagnosticIds 33 | { 34 | get { return ImmutableArray.Create(SerilogAnalyzerAnalyzer.DestructureAnonymousObjectsDiagnosticId); } 35 | } 36 | 37 | public sealed override FixAllProvider GetFixAllProvider() 38 | { 39 | return WellKnownFixAllProviders.BatchFixer; 40 | } 41 | 42 | public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) 43 | { 44 | var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); 45 | 46 | var diagnostic = context.Diagnostics.First(); 47 | var diagnosticSpan = diagnostic.Location.SourceSpan; 48 | 49 | context.RegisterCodeFix( 50 | CodeAction.Create( 51 | title: title, 52 | createChangedSolution: c => AddDestructuringHint(context.Document, diagnosticSpan, c), 53 | equivalenceKey: title), 54 | diagnostic); 55 | } 56 | 57 | private async Task AddDestructuringHint(Document document, TextSpan textSpan, CancellationToken cancellationToken) 58 | { 59 | var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); 60 | text = text.Replace(start: textSpan.Start + 1, length: 0, newText: "@"); // textSpan: "{Name}" -> "{@Name}" 61 | document = document.WithText(text); 62 | 63 | return document.Project.Solution; 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/Diagnostic.nuspec: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | SerilogAnalyzer 5 | 1.0.0.0 6 | Serilog Analyzer 7 | Robin 8 | Robin 9 | https://github.com/Suchiman/SerilogAnalyzer/blob/master/LICENSE 10 | https://github.com/Suchiman/SerilogAnalyzer 11 | https://raw.githubusercontent.com/Suchiman/SerilogAnalyzer/master/SerilogAnalyzer/SerilogAnalyzer.Vsix/analyzer-200px.png 12 | false 13 | true 14 | Roslyn-based analysis for code using the Serilog logging library. Checks for common mistakes and usage problems. 15 | Copyright 2016 Robin Sue 16 | Serilog, analyzers 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SerilogAnalyzer 4 | { 5 | public static class Extensions 6 | { 7 | public static string NullWhenWhitespace(this string source) 8 | { 9 | return String.IsNullOrWhiteSpace(source) ? null : source; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/MessageTemplateToken.cs: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/serilog/serilog/blob/e87b99937417700bfb9ba473b017fa104259bba4/src/Serilog/Parsing/MessageTemplateToken.cs 2 | // Copyright 2013-2015 Serilog Contributors 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | namespace SerilogAnalyzer 17 | { 18 | /// 19 | /// An element parsed from a message template string. 20 | /// 21 | abstract class MessageTemplateToken 22 | { 23 | /// 24 | /// Construct a . 25 | /// 26 | /// The token's start index in the template. 27 | protected MessageTemplateToken(int startIndex, int length) 28 | { 29 | StartIndex = startIndex; 30 | Length = length; 31 | } 32 | 33 | /// 34 | /// The token's start index in the template. 35 | /// 36 | public int StartIndex { get; set; } 37 | 38 | /// 39 | /// The token's length. 40 | /// 41 | public int Length { get; set; } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/ObjectDisplay.cs: -------------------------------------------------------------------------------- 1 | // lifted from https://github.com/dotnet/roslyn/blob/e7869bdaa642214a142d27e6a77263e0e3ba1a66/src/Compilers/CSharp/Portable/SymbolDisplay/ObjectDisplay.cs 2 | // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.CodeAnalysis.CSharp; 5 | using System; 6 | using System.Globalization; 7 | using System.Text; 8 | 9 | namespace SerilogAnalyzer 10 | { 11 | #pragma warning disable RS0010 12 | /// 13 | /// Displays a value in the C# style. 14 | /// 15 | /// 16 | /// Separate from because we want to link this functionality into 17 | /// the Formatter project and we don't want it to be public there. 18 | /// 19 | /// 20 | #pragma warning restore RS0010 21 | class ObjectDisplay 22 | { 23 | /// 24 | /// Returns true if the character should be replaced and sets 25 | /// to the replacement text. 26 | /// 27 | private static bool TryReplaceChar(char c, out string replaceWith) 28 | { 29 | replaceWith = null; 30 | switch (c) 31 | { 32 | case '\\': 33 | replaceWith = "\\\\"; 34 | break; 35 | case '\0': 36 | replaceWith = "\\0"; 37 | break; 38 | case '\a': 39 | replaceWith = "\\a"; 40 | break; 41 | case '\b': 42 | replaceWith = "\\b"; 43 | break; 44 | case '\f': 45 | replaceWith = "\\f"; 46 | break; 47 | case '\n': 48 | replaceWith = "\\n"; 49 | break; 50 | case '\r': 51 | replaceWith = "\\r"; 52 | break; 53 | case '\t': 54 | replaceWith = "\\t"; 55 | break; 56 | case '\v': 57 | replaceWith = "\\v"; 58 | break; 59 | } 60 | 61 | if (replaceWith != null) 62 | { 63 | return true; 64 | } 65 | 66 | if (NeedsEscaping(CharUnicodeInfo.GetUnicodeCategory(c))) 67 | { 68 | replaceWith = "\\u" + ((int)c).ToString("x4"); 69 | return true; 70 | } 71 | 72 | return false; 73 | } 74 | 75 | private static bool NeedsEscaping(UnicodeCategory category) 76 | { 77 | switch (category) 78 | { 79 | case UnicodeCategory.Control: 80 | case UnicodeCategory.OtherNotAssigned: 81 | case UnicodeCategory.ParagraphSeparator: 82 | case UnicodeCategory.LineSeparator: 83 | case UnicodeCategory.Surrogate: 84 | return true; 85 | default: 86 | return false; 87 | } 88 | } 89 | 90 | /// 91 | /// Returns a C# string literal with the given value. 92 | /// 93 | /// The value that the resulting string literal should have. 94 | /// A string literal with the given value. 95 | /// 96 | /// Optionally escapes non-printable characters. 97 | /// 98 | public static string FormatLiteral(string value, bool useQuotes, bool escapeNonPrintable) 99 | { 100 | if (value == null) 101 | { 102 | throw new ArgumentNullException(nameof(value)); 103 | } 104 | 105 | const char quote = '"'; 106 | 107 | var builder = new StringBuilder(); 108 | var isVerbatim = useQuotes && !escapeNonPrintable && ContainsNewLine(value); 109 | 110 | if (useQuotes) 111 | { 112 | if (isVerbatim) 113 | { 114 | builder.Append('@'); 115 | } 116 | builder.Append(quote); 117 | } 118 | 119 | for (int i = 0; i < value.Length; i++) 120 | { 121 | char c = value[i]; 122 | if (escapeNonPrintable && CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.Surrogate) 123 | { 124 | var category = CharUnicodeInfo.GetUnicodeCategory(value, i); 125 | if (category == UnicodeCategory.Surrogate) 126 | { 127 | // an unpaired surrogate 128 | builder.Append("\\u" + ((int)c).ToString("x4")); 129 | } 130 | else if (NeedsEscaping(category)) 131 | { 132 | // a surrogate pair that needs to be escaped 133 | var unicode = char.ConvertToUtf32(value, i); 134 | builder.Append("\\U" + unicode.ToString("x8")); 135 | i++; // skip the already-encoded second surrogate of the pair 136 | } 137 | else 138 | { 139 | // copy a printable surrogate pair directly 140 | builder.Append(c); 141 | builder.Append(value[++i]); 142 | } 143 | } 144 | else if (escapeNonPrintable && TryReplaceChar(c, out var replaceWith)) 145 | { 146 | builder.Append(replaceWith); 147 | } 148 | else if (useQuotes && c == quote) 149 | { 150 | if (isVerbatim) 151 | { 152 | builder.Append(quote); 153 | builder.Append(quote); 154 | } 155 | else 156 | { 157 | builder.Append('\\'); 158 | builder.Append(quote); 159 | } 160 | } 161 | else 162 | { 163 | builder.Append(c); 164 | } 165 | } 166 | 167 | if (useQuotes) 168 | { 169 | builder.Append(quote); 170 | } 171 | 172 | return builder.ToString(); 173 | } 174 | 175 | private static bool ContainsNewLine(string s) 176 | { 177 | foreach (char c in s) 178 | { 179 | if (SyntaxFacts.IsNewLine(c)) 180 | { 181 | return true; 182 | } 183 | } 184 | 185 | return false; 186 | } 187 | } 188 | } -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("SerilogAnalyzer")] 8 | [assembly: AssemblyDescription("Roslyn-based analysis for code using the Serilog logging library. Checks for common mistakes and usage problems.")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("SerilogAnalyzer")] 12 | [assembly: AssemblyCopyright("Copyright © Robin Sue 2016")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // Version information for an assembly consists of the following four values: 22 | // 23 | // Major Version 24 | // Minor Version 25 | // Build Number 26 | // Revision 27 | // 28 | // You can specify all the values or you can default the Build and Revision Numbers 29 | // by using the '*' as shown below: 30 | [assembly: AssemblyVersion("0.15.0.0")] 31 | [assembly: AssemblyFileVersion("0.15.0.0")] 32 | -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/PropertyBindingAnalyzer.cs: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/serilog/serilog/blob/e97b3c028bdb28e4430512b84dc2082e6f98dcc7/src/Serilog/Parameters/PropertyBinder.cs 2 | // Copyright 2013-2015 Serilog Contributors 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | using System; 17 | using System.Collections.Generic; 18 | 19 | namespace SerilogAnalyzer 20 | { 21 | static class PropertyBindingAnalyzer 22 | { 23 | static readonly List NoProperties = new List(0); 24 | 25 | public static List AnalyzeProperties(List propertyTokens, List arguments) 26 | { 27 | if (propertyTokens.Count > 0) 28 | { 29 | var allPositional = true; 30 | var anyPositional = false; 31 | foreach (var propertyToken in propertyTokens) 32 | { 33 | if (propertyToken.IsPositional) 34 | anyPositional = true; 35 | else 36 | allPositional = false; 37 | } 38 | 39 | var diagnostics = new List(); 40 | if (allPositional) 41 | { 42 | AnalyzePositionalProperties(diagnostics, propertyTokens, arguments); 43 | } 44 | else 45 | { 46 | if (anyPositional) 47 | { 48 | foreach (var propertyToken in propertyTokens) 49 | { 50 | if (propertyToken.IsPositional) 51 | { 52 | diagnostics.Add(new MessageTemplateDiagnostic(propertyToken.StartIndex, propertyToken.Length, "Positional properties are not allowed, when named properties are being used")); 53 | } 54 | } 55 | } 56 | 57 | AnalyzeNamedProperties(diagnostics, propertyTokens, arguments); 58 | } 59 | 60 | return diagnostics; 61 | } 62 | else if (arguments.Count > 0) 63 | { 64 | var diagnostics = new List(); 65 | foreach (var argument in arguments) 66 | { 67 | diagnostics.Add(new MessageTemplateDiagnostic(argument.StartIndex, argument.Length, "There is no property that corresponds to this argument", false)); 68 | } 69 | return diagnostics; 70 | } 71 | 72 | return NoProperties; 73 | } 74 | 75 | static void AnalyzePositionalProperties(List diagnostics, List positionalProperties, List arguments) 76 | { 77 | var mapped = new List>(); 78 | foreach (var property in positionalProperties) 79 | { 80 | int position; 81 | if (property.TryGetPositionalValue(out position)) 82 | { 83 | if (position < 0) 84 | { 85 | diagnostics.Add(new MessageTemplateDiagnostic(property.StartIndex, property.Length, "Positional index cannot be negative")); 86 | } 87 | 88 | if (position >= arguments.Count) 89 | { 90 | diagnostics.Add(new MessageTemplateDiagnostic(property.StartIndex, property.Length, "There is no argument that corresponds to the positional property " + position.ToString())); 91 | } 92 | 93 | mapped.Add(new KeyValuePair(position, property)); 94 | } 95 | else 96 | { 97 | diagnostics.Add(new MessageTemplateDiagnostic(property.StartIndex, property.Length, "Couldn't get the position of this property while analyzing")); 98 | } 99 | } 100 | 101 | for (var i = 0; i < arguments.Count; ++i) 102 | { 103 | bool indexMatched = false; 104 | for (int m = 0; m < mapped.Count; m++) 105 | { 106 | if (mapped[m].Key == i) 107 | { 108 | indexMatched = true; 109 | break; 110 | } 111 | } 112 | 113 | if (!indexMatched) 114 | { 115 | diagnostics.Add(new MessageTemplateDiagnostic(arguments[i].StartIndex, arguments[i].Length, "There is no positional property that corresponds to this argument", false)); 116 | } 117 | } 118 | } 119 | 120 | static void AnalyzeNamedProperties(List diagnostics, List namedProperties, List arguments) 121 | { 122 | var matchedRun = Math.Min(namedProperties.Count, arguments.Count); 123 | 124 | // could still possibly work when it hits a name of a contextual property but it's better practice to be explicit at the callsite 125 | for (int i = matchedRun; i < namedProperties.Count; i++) 126 | { 127 | var namedProperty = namedProperties[i]; 128 | diagnostics.Add(new MessageTemplateDiagnostic(namedProperty.StartIndex, namedProperty.Length, "There is no argument that corresponds to the named property '" + namedProperty.PropertyName + "'")); 129 | } 130 | 131 | for (int i = matchedRun; i < arguments.Count; i++) 132 | { 133 | var argument = arguments[i]; 134 | diagnostics.Add(new MessageTemplateDiagnostic(argument.StartIndex, argument.Length, "There is no named property that corresponds to this argument", false)); 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/PropertyToken.cs: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/serilog/serilog/blob/e205c078b9fd0704e7bd778e2a214049472535df/src/Serilog/Parsing/PropertyToken.cs 2 | // Copyright 2013-2015 Serilog Contributors 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | using System; 17 | using System.Globalization; 18 | 19 | namespace SerilogAnalyzer 20 | { 21 | /// 22 | /// A message template token representing a log event property. 23 | /// 24 | sealed class PropertyToken : MessageTemplateToken 25 | { 26 | readonly string _rawText; 27 | readonly int? _position; 28 | 29 | public PropertyToken(int startIndex, string propertyName, string rawText) 30 | : base(startIndex, rawText.Length) 31 | { 32 | PropertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName)); 33 | _rawText = rawText ?? throw new ArgumentNullException(nameof(rawText)); 34 | 35 | int position; 36 | if (int.TryParse(PropertyName, NumberStyles.None, CultureInfo.InvariantCulture, out position) && 37 | position >= 0) 38 | { 39 | _position = position; 40 | } 41 | } 42 | 43 | /// 44 | /// The property name. 45 | /// 46 | public string PropertyName { get; } 47 | 48 | /// 49 | /// True if the property name is a positional index; otherwise, false. 50 | /// 51 | public bool IsPositional => _position.HasValue; 52 | 53 | internal string RawText => _rawText; 54 | 55 | /// 56 | /// Try to get the integer value represented by the property name. 57 | /// 58 | /// The integer value, if present. 59 | /// True if the property is positional, otherwise false. 60 | public bool TryGetPositionalValue(out int position) 61 | { 62 | if (_position == null) 63 | { 64 | position = 0; 65 | return false; 66 | } 67 | 68 | position = _position.Value; 69 | return true; 70 | } 71 | 72 | /// 73 | /// Returns a string that represents the current object. 74 | /// 75 | /// 76 | /// A string that represents the current object. 77 | /// 78 | /// 2 79 | public override string ToString() => _rawText; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/ReadMe.txt: -------------------------------------------------------------------------------- 1 |  2 | Building this project will produce an analyzer .dll, as well as the 3 | following two ways you may wish to package that analyzer: 4 | * A NuGet package (.nupkg file) that will add your assembly as a 5 | project-local analyzer that participates in builds. 6 | * A VSIX extension (.vsix file) that will apply your analyzer to all projects 7 | and works just in the IDE. 8 | 9 | To debug your analyzer, make sure the default project is the VSIX project and 10 | start debugging. This will deploy the analyzer as a VSIX into another instance 11 | of Visual Studio, which is useful for debugging, even if you intend to produce 12 | a NuGet package. 13 | 14 | 15 | TRYING OUT YOUR NUGET PACKAGE 16 | 17 | To try out the NuGet package: 18 | 1. Create a local NuGet feed by following the instructions here: 19 | > http://docs.nuget.org/docs/creating-packages/hosting-your-own-nuget-feeds 20 | 2. Copy the .nupkg file into that folder. 21 | 3. Open the target project in Visual Studio 2015. 22 | 4. Right-click on the project node in Solution Explorer and choose Manage 23 | NuGet Packages. 24 | 5. Select the NuGet feed you created on the left. 25 | 6. Choose your analyzer from the list and click Install. 26 | 27 | If you want to automatically deploy the .nupkg file to the local feed folder 28 | when you build this project, follow these steps: 29 | 1. Right-click on this project in Solution Explorer and choose 'Unload Project'. 30 | 2. Right-click on this project and click "Edit". 31 | 3. Scroll down to the "AfterBuild" target. 32 | 4. In the "Exec" task, change the value inside "Command" after the -OutputDirectory 33 | path to point to your local NuGet feed folder. -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Checks that MessageTemplates are constant values which is recommended practice 122 | An optional longer localizable description of the diagnostic. 123 | 124 | 125 | MessageTemplate argument {0} is not constant 126 | The format-able message the diagnostic displays. 127 | 128 | 129 | Constant MessageTemplate verifier 130 | The title of the diagnostic. 131 | 132 | 133 | Exceptions should be passed in the Exception Parameter 134 | An optional longer localizable description of the diagnostic. 135 | 136 | 137 | The exception '{0}' should be passed as first argument 138 | The format-able message the diagnostic displays. 139 | 140 | 141 | Exception not passed as first argument 142 | The title of the diagnostic. 143 | 144 | 145 | Checks wether properties and arguments match up 146 | An optional longer localizable description of the diagnostic. 147 | 148 | 149 | Error while binding properties: {0} 150 | The format-able message the diagnostic displays. 151 | 152 | 153 | Property binding verifier 154 | The title of the diagnostic. 155 | 156 | 157 | Checks for errors in the MessageTemplate 158 | An optional longer localizable description of the diagnostic. 159 | 160 | 161 | Error while parsing MessageTemplate: {0} 162 | The format-able message the diagnostic displays. 163 | 164 | 165 | MessageTemplate verifier 166 | The title of the diagnostic. 167 | 168 | 169 | Checks that all property names in a MessageTemplates are unique 170 | An optional longer localizable description of the diagnostic. 171 | 172 | 173 | Property name '{0}' is not unique in this MessageTemplate 174 | The format-able message the diagnostic displays. 175 | 176 | 177 | Unique Property name verifier 178 | The title of the diagnostic. 179 | 180 | 181 | Pascal Property name verifier 182 | The title of the diagnostic. 183 | 184 | 185 | Property name '{0}' should be pascal case 186 | The format-able message the diagnostic displays. 187 | 188 | 189 | Checks that all property names in a MessageTemplates are Pascal Case 190 | An optional longer localizable description of the diagnostic. 191 | 192 | 193 | Anonymous objects use destructuring hint verifier 194 | The title of the diagnostic. 195 | 196 | 197 | Property '{0}' should use destructuring because the argument is an anonymous object 198 | The format-able message the diagnostic displays. 199 | 200 | 201 | Checks that properties that are passed anonymous objects use the destructuring hint 202 | An optional longer localizable description of the diagnostic. 203 | 204 | 205 | Contextual loggers use the correct context verifier 206 | The title of the diagnostic. 207 | 208 | 209 | Logger '{0}' should use {1} instead of {2} 210 | The format-able message the diagnostic displays. 211 | 212 | 213 | Checks that loggers are initialized with the current class as ForContext 214 | An optional longer localizable description of the diagnostic. 215 | 216 | -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/RoslynHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Robin Sue 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | using System; 16 | using System.Collections.Immutable; 17 | using System.Linq; 18 | using System.Threading; 19 | using Microsoft.CodeAnalysis; 20 | using Microsoft.CodeAnalysis.CSharp; 21 | using Microsoft.CodeAnalysis.CSharp.Syntax; 22 | 23 | namespace SerilogAnalyzer 24 | { 25 | static class RoslynHelper 26 | { 27 | /// 28 | /// Returns the parameter to which this argument is passed. If 29 | /// is true, the last parameter will be returned if it is params parameter and the index of 30 | /// the specified argument is greater than the number of parameters. 31 | /// 32 | /// Lifted from http://source.roslyn.io/#Microsoft.CodeAnalysis.CSharp.Workspaces/Extensions/ArgumentSyntaxExtensions.cs,af94352fb5da7056 33 | public static IParameterSymbol DetermineParameter(ArgumentSyntax argument, SemanticModel semanticModel, bool allowParams = false, CancellationToken cancellationToken = default(CancellationToken)) 34 | { 35 | var argumentList = argument.Parent as BaseArgumentListSyntax; 36 | if (argumentList == null) 37 | { 38 | return null; 39 | } 40 | 41 | var invocableExpression = argumentList.Parent as ExpressionSyntax; 42 | if (invocableExpression == null) 43 | { 44 | return null; 45 | } 46 | 47 | var symbol = semanticModel.GetSymbolInfo(invocableExpression, cancellationToken).Symbol; 48 | if (symbol == null) 49 | { 50 | return null; 51 | } 52 | 53 | var parameters = (symbol as IMethodSymbol)?.Parameters ?? (symbol as IPropertySymbol)?.Parameters ?? ImmutableArray.Empty; 54 | 55 | // Handle named argument 56 | if (argument.NameColon != null && !argument.NameColon.IsMissing) 57 | { 58 | var name = argument.NameColon.Name.Identifier.ValueText; 59 | return parameters.FirstOrDefault(p => p.Name == name); 60 | } 61 | 62 | // Handle positional argument 63 | var index = argumentList.Arguments.IndexOf(argument); 64 | if (index < 0) 65 | { 66 | return null; 67 | } 68 | 69 | if (index < parameters.Length) 70 | { 71 | return parameters[index]; 72 | } 73 | 74 | if (allowParams) 75 | { 76 | var lastParameter = parameters.LastOrDefault(); 77 | if (lastParameter == null) 78 | { 79 | return null; 80 | } 81 | 82 | if (lastParameter.IsParams) 83 | { 84 | return lastParameter; 85 | } 86 | } 87 | 88 | return null; 89 | } 90 | 91 | /// 92 | /// Given an expression node, tries to generate an appropriate name that can be used for 93 | /// that expression. 94 | /// 95 | /// Lifted from https://github.com/dotnet/roslyn/blob/c5c72d57af0ee9c615ee6a810394ea4e92d8d913/src/Workspaces/CSharp/Portable/Extensions/SemanticModelExtensions.cs#L200 96 | public static string GenerateNameForExpression( 97 | this SemanticModel semanticModel, ExpressionSyntax expression, bool capitalize = false) 98 | { 99 | // Try to find a usable name node that we can use to name the 100 | // parameter. If we have an expression that has a name as part of it 101 | // then we try to use that part. 102 | var current = expression; 103 | while (true) 104 | { 105 | current = current.WalkDownParentheses(); 106 | 107 | if (current.Kind() == SyntaxKind.IdentifierName) 108 | { 109 | return ((IdentifierNameSyntax)current).Identifier.ValueText.ToPascalCase(); 110 | } 111 | else if (current is MemberAccessExpressionSyntax) 112 | { 113 | return ((MemberAccessExpressionSyntax)current).Name.Identifier.ValueText.ToPascalCase(); 114 | } 115 | else if (current is MemberBindingExpressionSyntax) 116 | { 117 | return ((MemberBindingExpressionSyntax)current).Name.Identifier.ValueText.ToPascalCase(); 118 | } 119 | else if (current is ConditionalAccessExpressionSyntax) 120 | { 121 | current = ((ConditionalAccessExpressionSyntax)current).WhenNotNull; 122 | } 123 | else if (current is CastExpressionSyntax) 124 | { 125 | current = ((CastExpressionSyntax)current).Expression; 126 | } 127 | //else if (current is DeclarationExpressionSyntax) 128 | //{ 129 | // var decl = (DeclarationExpressionSyntax)current; 130 | // var name = decl.Designation as SingleVariableDesignationSyntax; 131 | // if (name == null) 132 | // { 133 | // break; 134 | // } 135 | 136 | // return name.Identifier.ValueText.ToCamelCase(); 137 | //} 138 | else 139 | { 140 | break; 141 | } 142 | } 143 | 144 | // Otherwise, figure out the type of the expression and generate a name from that 145 | // instead. 146 | var info = semanticModel.GetTypeInfo(expression); 147 | 148 | // If we can't determine the type, then fallback to some placeholders. 149 | var type = info.Type; 150 | return type.CreateParameterName(capitalize); 151 | } 152 | 153 | public static string CreateParameterName(this ITypeSymbol type, bool capitalize = false) 154 | { 155 | while (true) 156 | { 157 | if (type is IArrayTypeSymbol arrayType) 158 | { 159 | type = arrayType.ElementType; 160 | continue; 161 | } 162 | 163 | if (type is IPointerTypeSymbol pointerType) 164 | { 165 | type = pointerType.PointedAtType; 166 | continue; 167 | } 168 | 169 | break; 170 | } 171 | 172 | var shortName = GetParameterName(type); 173 | return capitalize ? shortName.ToPascalCase() : shortName.ToCamelCase(); 174 | } 175 | 176 | private const string DefaultParameterName = "p"; 177 | private const string DefaultBuiltInParameterName = "v"; 178 | private static string GetParameterName(ITypeSymbol type) 179 | { 180 | if (type == null || type.IsAnonymousType /*|| type.IsTupleType*/) 181 | { 182 | return DefaultParameterName; 183 | } 184 | 185 | if (type.IsSpecialType() || type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) 186 | { 187 | return DefaultBuiltInParameterName; 188 | } 189 | 190 | var shortName = type.GetShortName(); 191 | return shortName.Length == 0 192 | ? DefaultParameterName 193 | : shortName; 194 | } 195 | 196 | public static bool IsSpecialType(this ITypeSymbol symbol) 197 | { 198 | if (symbol != null) 199 | { 200 | switch (symbol.SpecialType) 201 | { 202 | case SpecialType.System_Object: 203 | case SpecialType.System_Void: 204 | case SpecialType.System_Boolean: 205 | case SpecialType.System_SByte: 206 | case SpecialType.System_Byte: 207 | case SpecialType.System_Decimal: 208 | case SpecialType.System_Single: 209 | case SpecialType.System_Double: 210 | case SpecialType.System_Int16: 211 | case SpecialType.System_Int32: 212 | case SpecialType.System_Int64: 213 | case SpecialType.System_Char: 214 | case SpecialType.System_String: 215 | case SpecialType.System_UInt16: 216 | case SpecialType.System_UInt32: 217 | case SpecialType.System_UInt64: 218 | return true; 219 | } 220 | } 221 | 222 | return false; 223 | } 224 | 225 | private static readonly SymbolDisplayFormat s_shortNameFormat = new SymbolDisplayFormat(miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes | SymbolDisplayMiscellaneousOptions.ExpandNullable); 226 | 227 | public static string GetShortName(this INamespaceOrTypeSymbol symbol) 228 | { 229 | return symbol.ToDisplayString(s_shortNameFormat); 230 | } 231 | 232 | public static ExpressionSyntax WalkDownParentheses(this ExpressionSyntax expression) 233 | { 234 | while (expression.IsKind(SyntaxKind.ParenthesizedExpression)) 235 | { 236 | expression = ((ParenthesizedExpressionSyntax)expression).Expression; 237 | } 238 | 239 | return expression; 240 | } 241 | 242 | private static readonly Func s_toLower = char.ToLower; 243 | private static readonly Func s_toUpper = char.ToUpper; 244 | 245 | public static string ToPascalCase( 246 | this string shortName, 247 | bool trimLeadingTypePrefix = true) 248 | { 249 | return ConvertCase(shortName, trimLeadingTypePrefix, s_toUpper); 250 | } 251 | 252 | public static string ToCamelCase( 253 | this string shortName, 254 | bool trimLeadingTypePrefix = true) 255 | { 256 | return ConvertCase(shortName, trimLeadingTypePrefix, s_toLower); 257 | } 258 | 259 | private static string ConvertCase( 260 | this string shortName, 261 | bool trimLeadingTypePrefix, 262 | Func convert) 263 | { 264 | // Special case the common .net pattern of "IFoo" as a type name. In this case we 265 | // want to generate "foo" as the parameter name. 266 | if (!string.IsNullOrEmpty(shortName)) 267 | { 268 | if (trimLeadingTypePrefix && (shortName.LooksLikeInterfaceName() || shortName.LooksLikeTypeParameterName())) 269 | { 270 | return convert(shortName[1]) + shortName.Substring(2); 271 | } 272 | 273 | if (convert(shortName[0]) != shortName[0]) 274 | { 275 | return convert(shortName[0]) + shortName.Substring(1); 276 | } 277 | } 278 | 279 | return shortName; 280 | } 281 | 282 | public static bool LooksLikeInterfaceName(this string name) 283 | { 284 | return name.Length >= 3 && name[0] == 'I' && char.IsUpper(name[1]) && char.IsLower(name[2]); 285 | } 286 | 287 | public static bool LooksLikeTypeParameterName(this string name) 288 | { 289 | return name.Length >= 3 && name[0] == 'T' && char.IsUpper(name[1]) && char.IsLower(name[2]); 290 | } 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/SerilogAnalyzer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 11.0 6 | Debug 7 | AnyCPU 8 | {1666D457-D350-4322-B1BD-4DBDF530A882} 9 | Library 10 | Properties 11 | SerilogAnalyzer 12 | SerilogAnalyzer 13 | {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 14 | Profile7 15 | v4.5 16 | 17 | 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | True 55 | True 56 | Resources.resx 57 | 58 | 59 | 60 | 61 | 62 | 63 | ResXFileCodeGenerator 64 | Resources.Designer.cs 65 | Designer 66 | 67 | 68 | 69 | 70 | Designer 71 | PreserveNewest 72 | 73 | 74 | 75 | PreserveNewest 76 | 77 | 78 | PreserveNewest 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | ..\..\packages\Microsoft.CodeAnalysis.Common.1.0.0\lib\portable-net45+win8\Microsoft.CodeAnalysis.dll 89 | False 90 | 91 | 92 | ..\..\packages\Microsoft.CodeAnalysis.CSharp.1.0.0\lib\portable-net45+win8\Microsoft.CodeAnalysis.CSharp.dll 93 | False 94 | 95 | 96 | ..\..\packages\Microsoft.CodeAnalysis.CSharp.Workspaces.1.0.0\lib\portable-net45+win8\Microsoft.CodeAnalysis.CSharp.Workspaces.dll 97 | False 98 | 99 | 100 | ..\..\packages\Microsoft.CodeAnalysis.Workspaces.Common.1.0.0\lib\portable-net45+win8\Microsoft.CodeAnalysis.Workspaces.dll 101 | False 102 | 103 | 104 | ..\..\packages\System.Collections.Immutable.1.1.36\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll 105 | False 106 | 107 | 108 | ..\..\packages\Microsoft.Composition.1.0.27\lib\portable-net45+win8+wp8+wpa81\System.Composition.AttributedModel.dll 109 | False 110 | 111 | 112 | ..\..\packages\Microsoft.Composition.1.0.27\lib\portable-net45+win8+wp8+wpa81\System.Composition.Convention.dll 113 | False 114 | 115 | 116 | ..\..\packages\Microsoft.Composition.1.0.27\lib\portable-net45+win8+wp8+wpa81\System.Composition.Hosting.dll 117 | False 118 | 119 | 120 | ..\..\packages\Microsoft.Composition.1.0.27\lib\portable-net45+win8+wp8+wpa81\System.Composition.Runtime.dll 121 | False 122 | 123 | 124 | ..\..\packages\Microsoft.Composition.1.0.27\lib\portable-net45+win8+wp8+wpa81\System.Composition.TypedParts.dll 125 | False 126 | 127 | 128 | ..\..\packages\System.Reflection.Metadata.1.0.21\lib\portable-net45+win8\System.Reflection.Metadata.dll 129 | False 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 148 | -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/SerilogAnalyzerPascalCaseCodeFixProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Serilog Analyzer Contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | using Microsoft.CodeAnalysis; 16 | using Microsoft.CodeAnalysis.CodeActions; 17 | using Microsoft.CodeAnalysis.CodeFixes; 18 | using Microsoft.CodeAnalysis.CSharp; 19 | using Microsoft.CodeAnalysis.CSharp.Syntax; 20 | using System; 21 | using System.Collections.Immutable; 22 | using System.Composition; 23 | using System.Linq; 24 | using System.Text; 25 | using System.Threading; 26 | using System.Threading.Tasks; 27 | 28 | namespace SerilogAnalyzer 29 | { 30 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SerilogAnalyzerPascalCaseCodeFixProvider)), Shared] 31 | public class SerilogAnalyzerPascalCaseCodeFixProvider : CodeFixProvider 32 | { 33 | private const char stringificationPrefix = '$'; 34 | private const char destructuringPrefix = '@'; 35 | private const string title = "Pascal case the property"; 36 | 37 | public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(SerilogAnalyzerAnalyzer.PascalPropertyNameDiagnosticId); 38 | 39 | public override async Task RegisterCodeFixesAsync(CodeFixContext context) 40 | { 41 | var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); 42 | 43 | var diagnostic = context.Diagnostics.First(); 44 | var diagnosticSpan = diagnostic.Location.SourceSpan; 45 | 46 | var declaration = root.FindNode(diagnosticSpan); 47 | 48 | context.RegisterCodeFix( 49 | CodeAction.Create( 50 | title: title, 51 | createChangedSolution: c => this.PascalCaseTheProperties(context.Document, declaration.DescendantNodesAndSelf().OfType().First(), c), 52 | equivalenceKey: title), 53 | diagnostic); 54 | } 55 | 56 | private async Task PascalCaseTheProperties(Document document, LiteralExpressionSyntax node, CancellationToken cancellationToken) 57 | { 58 | var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); 59 | var oldToken = node.Token; 60 | 61 | var sb = new StringBuilder(); 62 | if (oldToken.Text.StartsWith("@", StringComparison.Ordinal)) 63 | { 64 | sb.Append('@'); 65 | } 66 | sb.Append('"'); 67 | 68 | var interpolatedString = (InterpolatedStringExpressionSyntax)SyntaxFactory.ParseExpression("$" + oldToken.ToString()); 69 | foreach (var child in interpolatedString.Contents) 70 | { 71 | switch (child) 72 | { 73 | case InterpolatedStringTextSyntax text: 74 | sb.Append(text.TextToken.ToString()); 75 | break; 76 | case InterpolationSyntax interpolation: 77 | AppendAsPascalCase(sb, interpolation.ToString()); 78 | break; 79 | } 80 | } 81 | sb.Append('"'); 82 | 83 | var newToken = SyntaxFactory.ParseToken(sb.ToString()); 84 | root = root.ReplaceToken(oldToken, newToken); 85 | 86 | document = document.WithSyntaxRoot(root); 87 | return document.Project.Solution; 88 | } 89 | 90 | private static void AppendAsPascalCase(StringBuilder sb, string input) 91 | { 92 | bool uppercaseChar = true; 93 | bool skipTheRest = false; 94 | for (int i = 0; i < input.Length; i++) 95 | { 96 | char current = input[i]; 97 | if (i < 2 && current == '{' || current == stringificationPrefix || current == destructuringPrefix) 98 | { 99 | sb.Append(current); 100 | continue; 101 | } 102 | if (skipTheRest || current == ',' || current == ':' || current == '}') 103 | { 104 | skipTheRest = true; 105 | sb.Append(current); 106 | continue; 107 | } 108 | if (current == '_') 109 | { 110 | uppercaseChar = true; 111 | continue; 112 | } 113 | sb.Append(uppercaseChar ? Char.ToUpper(current) : current); 114 | uppercaseChar = false; 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/SourceArgument.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Robin Sue 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | using Microsoft.CodeAnalysis.CSharp.Syntax; 16 | 17 | namespace SerilogAnalyzer 18 | { 19 | class SourceArgument 20 | { 21 | public ArgumentSyntax Argument { get; set; } 22 | public int StartIndex { get; set; } 23 | public int Length { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/tools/install.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | $analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers" ) * -Resolve 4 | 5 | foreach($analyzersPath in $analyzersPaths) 6 | { 7 | # Install the language agnostic analyzers. 8 | if (Test-Path $analyzersPath) 9 | { 10 | foreach ($analyzerFilePath in Get-ChildItem $analyzersPath -Filter *.dll) 11 | { 12 | if($project.Object.AnalyzerReferences) 13 | { 14 | $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) 15 | } 16 | } 17 | } 18 | } 19 | 20 | # $project.Type gives the language name like (C# or VB.NET) 21 | $languageFolder = "" 22 | if($project.Type -eq "C#") 23 | { 24 | $languageFolder = "cs" 25 | } 26 | if($project.Type -eq "VB.NET") 27 | { 28 | $languageFolder = "vb" 29 | } 30 | if($languageFolder -eq "") 31 | { 32 | return 33 | } 34 | 35 | foreach($analyzersPath in $analyzersPaths) 36 | { 37 | # Install language specific analyzers. 38 | $languageAnalyzersPath = join-path $analyzersPath $languageFolder 39 | if (Test-Path $languageAnalyzersPath) 40 | { 41 | foreach ($analyzerFilePath in Get-ChildItem $languageAnalyzersPath -Filter *.dll) 42 | { 43 | if($project.Object.AnalyzerReferences) 44 | { 45 | $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /SerilogAnalyzer/SerilogAnalyzer/tools/uninstall.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | $analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers" ) * -Resolve 4 | 5 | foreach($analyzersPath in $analyzersPaths) 6 | { 7 | # Uninstall the language agnostic analyzers. 8 | if (Test-Path $analyzersPath) 9 | { 10 | foreach ($analyzerFilePath in Get-ChildItem $analyzersPath -Filter *.dll) 11 | { 12 | if($project.Object.AnalyzerReferences) 13 | { 14 | $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) 15 | } 16 | } 17 | } 18 | } 19 | 20 | # $project.Type gives the language name like (C# or VB.NET) 21 | $languageFolder = "" 22 | if($project.Type -eq "C#") 23 | { 24 | $languageFolder = "cs" 25 | } 26 | if($project.Type -eq "VB.NET") 27 | { 28 | $languageFolder = "vb" 29 | } 30 | if($languageFolder -eq "") 31 | { 32 | return 33 | } 34 | 35 | foreach($analyzersPath in $analyzersPaths) 36 | { 37 | # Uninstall language specific analyzers. 38 | $languageAnalyzersPath = join-path $analyzersPath $languageFolder 39 | if (Test-Path $languageAnalyzersPath) 40 | { 41 | foreach ($analyzerFilePath in Get-ChildItem $languageAnalyzersPath -Filter *.dll) 42 | { 43 | if($project.Object.AnalyzerReferences) 44 | { 45 | try 46 | { 47 | $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) 48 | } 49 | catch 50 | { 51 | 52 | } 53 | } 54 | } 55 | } 56 | } --------------------------------------------------------------------------------