├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── appveyor.yml ├── build.cake ├── images └── SER001.png └── src ├── StackExchange.Redis.Analyzer.sln └── StackExchange.Redis.Analyzer ├── StackExchange.Redis.Analyzer.Test ├── Helpers │ ├── DiagnosticResult.cs │ └── DiagnosticVerifier.Helper.cs ├── SendingCommandsInLoopAnalyzerTests.cs ├── StackExchange.Redis.Analyzer.Test.csproj ├── TestData │ ├── GettingDataInLoopAnalyzer │ │ ├── GetConstantStringInLoop.cs │ │ ├── GetStringInLoop.cs │ │ ├── KeyExistsInLoop.cs │ │ ├── SetCombineInForeach.cs │ │ ├── SetContainsInForeach.cs │ │ ├── SetContainsWihArrayOverloadInForeach.cs │ │ └── SetRemoveInForeach.cs │ └── TransactionDeadlockAnalyzer │ │ ├── AwaitStringGetAsyncTestData.cs │ │ ├── ContinueWithCallbackTestData.cs │ │ ├── ResultStringGetAsyncTestData.cs │ │ ├── TaskWaitAllStringGetAsyncTestData.cs │ │ └── WaitStringGetAsyncTestData.cs ├── TransactionDeadlockAnalyzerTests.cs └── Verifiers │ └── DiagnosticVerifier.cs ├── StackExchange.Redis.Analyzer.Vsix ├── StackExchange.Redis.Analyzer.Vsix.csproj └── source.extension.vsixmanifest └── StackExchange.Redis.Analyzer ├── SendingCommandsInLoopAnalyzer.cs ├── StackExchange.Redis.Analyzer.csproj ├── TransactionDeadlockAnalyzer.cs └── tools ├── install.ps1 └── uninstall.ps1 /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | dotnet_sort_system_directives_first = true 3 | 4 | dotnet_separate_import_directive_groups = true 5 | 6 | indent_size = 4 7 | 8 | [*.csproj] 9 | 10 | indent_size = 2 11 | 12 | [*] 13 | 14 | charset = utf-8 15 | 16 | indent_style = space 17 | 18 | insert_final_newline = true 19 | 20 | trim_trailing_whitespace = true 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | 332 | # SonarQube 333 | .sonarqube 334 | 335 | # Cake 336 | tools 337 | 338 | # Coverlet 339 | coverage.opencover.xml 340 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Oleg Shevchenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StackExchange.Redis.Analyzer 2 | 3 | [![Build status](https://ci.appveyor.com/api/projects/status/jyrrv262f1h9ipfn?svg=true)](https://ci.appveyor.com/project/olsh/stack-exchange-redis-analyzer) 4 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=stack-exchange-redis-analyzer&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=stack-exchange-redis-analyzer) 5 | [![codecov](https://codecov.io/gh/olsh/stack-exchange-redis-analyzer/branch/master/graph/badge.svg)](https://codecov.io/gh/olsh/stack-exchange-redis-analyzer) 6 | [![NuGet](https://img.shields.io/nuget/v/StackExchange.Redis.Analyzer.svg)](https://www.nuget.org/packages/StackExchange.Redis.Analyzer/) 7 | [![Visual Studio Marketplace](https://img.shields.io/vscode-marketplace/v/olsh.StackExchangeRedisAnalyzer.svg)](https://marketplace.visualstudio.com/items?itemName=olsh.StackExchangeRedisAnalyzer) 8 | 9 | Roslyn-based analyzer for StackExchange.Redis library 10 | 11 | 12 | ## SER001 13 | 14 | Async methods on ITransaction type shouldn't be blocked 15 | 16 | Noncompliant Code Example: 17 | ```csharp 18 | var transaction = db.CreateTransaction(); 19 | await transaction.StringSetAsync("key", "value").ConfigureAwait(false); 20 | 21 | await transaction.ExecuteAsync().ConfigureAwait(false); 22 | ``` 23 | 24 | Compliant Solution: 25 | ```csharp 26 | var transaction = db.CreateTransaction(); 27 | transaction.StringSetAsync("key", "value").ConfigureAwait(false); 28 | 29 | await transaction.ExecuteAsync().ConfigureAwait(false); 30 | ``` 31 | 32 | ## SER002 33 | 34 | Sending commands in a loop can be slow, batch overload with array of keys instead 35 | 36 | Noncompliant Code Example: 37 | ```csharp 38 | foreach (var key in new[] { "one", "two" }) 39 | { 40 | var value = db.StringGetAsync(key); 41 | } 42 | ``` 43 | 44 | Compliant Solution: 45 | ```csharp 46 | var results = db.StringGetAsync(new[] { "one", "two" }); 47 | ``` 48 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.{build} 2 | image: Visual Studio 2022 3 | install: 4 | - SET JAVA_HOME=C:\Program Files\Java\jdk17 5 | - SET PATH=%JAVA_HOME%\bin;%PATH% 6 | - dotnet tool install -g Cake.Tool --version 3.0.0 7 | 8 | build_script: 9 | - cmd: dotnet cake --Target=CI 10 | test: off 11 | cache: 12 | - '%USERPROFILE%\.sonar\cache' 13 | - '%USERPROFILE%\.nuget\packages -> **\*.csproj' 14 | - 'tools -> build.cake' 15 | -------------------------------------------------------------------------------- /build.cake: -------------------------------------------------------------------------------- 1 | #tool nuget:?package=Codecov&version=1.13.0 2 | #addin nuget:?package=Cake.Codecov&version=1.0.1 3 | 4 | #tool nuget:?package=MSBuild.SonarQube.Runner.Tool&version=4.8.0 5 | #addin nuget:?package=Cake.Sonar&version=1.1.31 6 | 7 | var target = Argument("target", "Default"); 8 | 9 | var buildConfiguration = "Release"; 10 | 11 | var projectName = "StackExchange.Redis.Analyzer"; 12 | var testProjectName = "StackExchange.Redis.Analyzer.Test"; 13 | 14 | var solutionFile = string.Format("./src/{0}.sln", projectName); 15 | var projectFolder = string.Format("./src/{0}/{0}/", projectName); 16 | var vsixProjectFolder = string.Format("./src/{0}/StackExchange.Redis.Analyzer.Vsix/", projectName); 17 | var testProjectFolder = string.Format("./src/{0}/{1}/", projectName, testProjectName); 18 | var testProjectFile = string.Format("{0}{1}.csproj", testProjectFolder, testProjectName); 19 | 20 | var vsixFile = string.Format("{0}bin/{1}/{2}.vsix", vsixProjectFolder, buildConfiguration, projectName); 21 | 22 | var projectFile = string.Format("{0}{1}.csproj", projectFolder, projectName); 23 | var extensionsVersion = XmlPeek(projectFile, "Project/PropertyGroup/Version/text()"); 24 | 25 | var nugetPackage = string.Format("{0}bin/{1}/{2}.{3}.nupkg", projectFolder, buildConfiguration, projectName, extensionsVersion); 26 | 27 | Information(nugetPackage); 28 | Information(vsixFile); 29 | 30 | Task("UpdateBuildVersion") 31 | .WithCriteria(BuildSystem.AppVeyor.IsRunningOnAppVeyor) 32 | .Does(() => 33 | { 34 | var buildNumber = BuildSystem.AppVeyor.Environment.Build.Number; 35 | 36 | BuildSystem.AppVeyor.UpdateBuildVersion(string.Format("{0}.{1}", extensionsVersion, buildNumber)); 37 | }); 38 | 39 | Task("Build") 40 | .Does(() => 41 | { 42 | var settings = new MSBuildSettings 43 | { 44 | Configuration = buildConfiguration, 45 | ToolVersion = MSBuildToolVersion.VS2022, 46 | MSBuildPlatform = MSBuildPlatform.x86, 47 | Restore = true, 48 | Verbosity = Verbosity.Minimal 49 | }; 50 | 51 | MSBuild(solutionFile, settings); 52 | }); 53 | 54 | Task("Test") 55 | .IsDependentOn("Build") 56 | .Does(() => 57 | { 58 | var settings = new DotNetTestSettings 59 | { 60 | Configuration = buildConfiguration 61 | }; 62 | 63 | DotNetTest(testProjectFile, settings); 64 | }); 65 | 66 | Task("CodeCoverage") 67 | .IsDependentOn("Build") 68 | .Does(() => 69 | { 70 | var settings = new DotNetTestSettings 71 | { 72 | Configuration = buildConfiguration, 73 | ArgumentCustomization = args => args 74 | .Append("/p:CollectCoverage=true") 75 | .Append("/p:CoverletOutputFormat=opencover") 76 | }; 77 | 78 | DotNetTest(testProjectFile, settings); 79 | 80 | Codecov(string.Format("{0}coverage.opencover.xml", testProjectFolder), EnvironmentVariable("codecov:token")); 81 | }); 82 | 83 | Task("CreateArtifact") 84 | .IsDependentOn("Build") 85 | .WithCriteria(BuildSystem.AppVeyor.IsRunningOnAppVeyor) 86 | .Does(() => 87 | { 88 | BuildSystem.AppVeyor.UploadArtifact(nugetPackage); 89 | BuildSystem.AppVeyor.UploadArtifact(vsixFile); 90 | }); 91 | 92 | Task("SonarBegin") 93 | .Does(() => { 94 | SonarBegin(new SonarBeginSettings { 95 | Url = "https://sonarcloud.io", 96 | Login = EnvironmentVariable("sonar:apikey"), 97 | Key = "stack-exchange-redis-analyzer", 98 | Name = "StackExchange.Redis.Analyzer", 99 | ArgumentCustomization = args => args 100 | .Append($"/o:olsh-github"), 101 | Version = "1.0.0.0" 102 | }); 103 | }); 104 | 105 | Task("SonarEnd") 106 | .Does(() => { 107 | SonarEnd(new SonarEndSettings { 108 | Login = EnvironmentVariable("sonar:apikey") 109 | }); 110 | }); 111 | 112 | Task("Sonar") 113 | .IsDependentOn("SonarBegin") 114 | .IsDependentOn("Build") 115 | .IsDependentOn("SonarEnd"); 116 | 117 | Task("Default") 118 | .IsDependentOn("Test"); 119 | 120 | Task("CI") 121 | .IsDependentOn("UpdateBuildVersion") 122 | .IsDependentOn("Sonar") 123 | .IsDependentOn("CodeCoverage") 124 | .IsDependentOn("CreateArtifact"); 125 | 126 | RunTarget(target); 127 | -------------------------------------------------------------------------------- /images/SER001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olsh/stack-exchange-redis-analyzer/0f4136c5f8268e445e727119441e0a373a19a6c4/images/SER001.png -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.136 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StackExchange.Redis.Analyzer", "StackExchange.Redis.Analyzer\StackExchange.Redis.Analyzer\StackExchange.Redis.Analyzer.csproj", "{B574F990-768E-4546-9C53-ECDD0D0A7513}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StackExchange.Redis.Analyzer.Test", "StackExchange.Redis.Analyzer\StackExchange.Redis.Analyzer.Test\StackExchange.Redis.Analyzer.Test.csproj", "{CBE903DE-E879-4463-B793-565B31893E43}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StackExchange.Redis.Analyzer.Vsix", "StackExchange.Redis.Analyzer\StackExchange.Redis.Analyzer.Vsix\StackExchange.Redis.Analyzer.Vsix.csproj", "{CCBECB4F-1068-4322-8EE6-42D647817CC2}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DE61556E-092D-4F53-909B-38FED011FC90}" 13 | ProjectSection(SolutionItems) = preProject 14 | ..\appveyor.yml = ..\appveyor.yml 15 | ..\build.cake = ..\build.cake 16 | ..\README.md = ..\README.md 17 | EndProjectSection 18 | EndProject 19 | Global 20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 21 | Debug|Any CPU = Debug|Any CPU 22 | Release|Any CPU = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {B574F990-768E-4546-9C53-ECDD0D0A7513}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {B574F990-768E-4546-9C53-ECDD0D0A7513}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {B574F990-768E-4546-9C53-ECDD0D0A7513}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {B574F990-768E-4546-9C53-ECDD0D0A7513}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {CBE903DE-E879-4463-B793-565B31893E43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {CBE903DE-E879-4463-B793-565B31893E43}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {CBE903DE-E879-4463-B793-565B31893E43}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {CBE903DE-E879-4463-B793-565B31893E43}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {CCBECB4F-1068-4322-8EE6-42D647817CC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {CCBECB4F-1068-4322-8EE6-42D647817CC2}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {CCBECB4F-1068-4322-8EE6-42D647817CC2}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {CCBECB4F-1068-4322-8EE6-42D647817CC2}.Release|Any CPU.Build.0 = Release|Any CPU 37 | EndGlobalSection 38 | GlobalSection(SolutionProperties) = preSolution 39 | HideSolutionNode = FALSE 40 | EndGlobalSection 41 | GlobalSection(ExtensibilityGlobals) = postSolution 42 | SolutionGuid = {00A184F8-D59D-4AD9-A7F4-76C827AF71C2} 43 | EndGlobalSection 44 | EndGlobal 45 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer.Test/Helpers/DiagnosticResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Microsoft.CodeAnalysis; 4 | // ReSharper disable All 5 | 6 | namespace TestHelper 7 | { 8 | /// 9 | /// Location where the diagnostic appears, as determined by path, line number, and column number. 10 | /// 11 | public struct DiagnosticResultLocation 12 | { 13 | public DiagnosticResultLocation(string path, int line, int column) 14 | { 15 | if (line < -1) 16 | { 17 | throw new ArgumentOutOfRangeException(nameof(line), "line must be >= -1"); 18 | } 19 | 20 | if (column < -1) 21 | { 22 | throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1"); 23 | } 24 | 25 | this.Path = path; 26 | this.Line = line; 27 | this.Column = column; 28 | } 29 | 30 | public string Path { get; } 31 | public int Line { get; } 32 | public int Column { get; } 33 | } 34 | 35 | /// 36 | /// Struct that stores information about a Diagnostic appearing in a source 37 | /// 38 | public struct DiagnosticResult 39 | { 40 | private DiagnosticResultLocation[] locations; 41 | 42 | public DiagnosticResultLocation[] Locations 43 | { 44 | get 45 | { 46 | if (this.locations == null) 47 | { 48 | this.locations = new DiagnosticResultLocation[] { }; 49 | } 50 | return this.locations; 51 | } 52 | 53 | set 54 | { 55 | this.locations = value; 56 | } 57 | } 58 | 59 | public DiagnosticSeverity Severity { get; set; } 60 | 61 | public string Id { get; set; } 62 | 63 | public string Message { get; set; } 64 | 65 | public string Path 66 | { 67 | get 68 | { 69 | return this.Locations.Length > 0 ? this.Locations[0].Path : ""; 70 | } 71 | } 72 | 73 | public int Line 74 | { 75 | get 76 | { 77 | return this.Locations.Length > 0 ? this.Locations[0].Line : -1; 78 | } 79 | } 80 | 81 | public int Column 82 | { 83 | get 84 | { 85 | return this.Locations.Length > 0 ? this.Locations[0].Column : -1; 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer.Test/Helpers/DiagnosticVerifier.Helper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | using Microsoft.CodeAnalysis; 9 | using Microsoft.CodeAnalysis.CSharp; 10 | using Microsoft.CodeAnalysis.Diagnostics; 11 | using Microsoft.CodeAnalysis.Text; 12 | 13 | using StackExchange.Redis; 14 | // ReSharper disable All 15 | 16 | namespace TestHelper 17 | { 18 | /// 19 | /// Class for turning strings into documents and getting the diagnostics on them 20 | /// All methods are static 21 | /// 22 | public abstract partial class DiagnosticVerifier 23 | { 24 | internal static string CSharpDefaultFileExt = "cs"; 25 | 26 | internal static string DefaultFilePathPrefix = "Test"; 27 | 28 | internal static string TestProjectName = "TestProject"; 29 | 30 | internal static string VisualBasicDefaultExt = "vb"; 31 | 32 | private static readonly MetadataReference CodeAnalysisReference = 33 | MetadataReference.CreateFromFile(typeof(Compilation).Assembly.Location); 34 | 35 | private static readonly MetadataReference CorlibReference = 36 | MetadataReference.CreateFromFile(typeof(object).Assembly.Location); 37 | 38 | private static readonly MetadataReference CSharpSymbolsReference = 39 | MetadataReference.CreateFromFile(typeof(CSharpCompilation).Assembly.Location); 40 | 41 | private static readonly MetadataReference SystemCoreReference = 42 | MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location); 43 | 44 | private static readonly MetadataReference SystemIoReference = 45 | MetadataReference.CreateFromFile(typeof(TextWriter).Assembly.Location); 46 | 47 | private static readonly MetadataReference StackExchangeRedisReference = 48 | MetadataReference.CreateFromFile(typeof(IDatabase).Assembly.Location); 49 | 50 | private static readonly MetadataReference NetStandard = MetadataReference.CreateFromFile(Assembly.Load("netstandard, Version=2.0.0.0").Location); 51 | 52 | private static readonly MetadataReference SystemRuntimeReference = MetadataReference.CreateFromFile(Assembly.Load("System.Runtime, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a").Location); 53 | 54 | /// 55 | /// Create a Document from a string through creating a project that contains it. 56 | /// 57 | /// Classes in the form of a string 58 | /// The language the source code is in 59 | /// A Document created from the source string 60 | protected static Document CreateDocument(string source, string language = LanguageNames.CSharp) 61 | { 62 | return CreateProject(new[] { source }, language) 63 | .Documents.First(); 64 | } 65 | 66 | /// 67 | /// Given an analyzer and a document to apply it to, run the analyzer and gather an array of diagnostics found in it. 68 | /// The returned diagnostics are then ordered by location in the source document. 69 | /// 70 | /// The analyzer to run on the documents 71 | /// The Documents that the analyzer will be run on 72 | /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location 73 | protected static Diagnostic[] GetSortedDiagnosticsFromDocuments( 74 | DiagnosticAnalyzer analyzer, 75 | Document[] documents) 76 | { 77 | var projects = new HashSet(); 78 | foreach (var document in documents) 79 | { 80 | projects.Add(document.Project); 81 | } 82 | 83 | var diagnostics = new List(); 84 | foreach (var project in projects) 85 | { 86 | var compilationWithAnalyzers = project.GetCompilationAsync() 87 | .Result.WithAnalyzers(ImmutableArray.Create(analyzer)); 88 | var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync() 89 | .Result; 90 | foreach (var diag in diags) 91 | { 92 | if (diag.Location == Location.None || diag.Location.IsInMetadata) 93 | { 94 | diagnostics.Add(diag); 95 | } 96 | else 97 | { 98 | for (var i = 0; i < documents.Length; i++) 99 | { 100 | var document = documents[i]; 101 | var tree = document.GetSyntaxTreeAsync() 102 | .Result; 103 | if (tree == diag.Location.SourceTree) 104 | { 105 | diagnostics.Add(diag); 106 | } 107 | } 108 | } 109 | } 110 | } 111 | 112 | var results = SortDiagnostics(diagnostics); 113 | diagnostics.Clear(); 114 | return results; 115 | } 116 | 117 | /// 118 | /// Create a project using the inputted strings as sources. 119 | /// 120 | /// Classes in the form of strings 121 | /// The language the source code is in 122 | /// A Project created out of the Documents created from the source strings 123 | private static Project CreateProject(string[] sources, string language = LanguageNames.CSharp) 124 | { 125 | var fileNamePrefix = DefaultFilePathPrefix; 126 | var fileExt = language == LanguageNames.CSharp ? CSharpDefaultFileExt : VisualBasicDefaultExt; 127 | 128 | var projectId = ProjectId.CreateNewId(TestProjectName); 129 | 130 | var solution = new AdhocWorkspace().CurrentSolution.AddProject( 131 | projectId, 132 | TestProjectName, 133 | TestProjectName, 134 | language) 135 | .AddMetadataReference(projectId, CorlibReference) 136 | .AddMetadataReference(projectId, SystemCoreReference) 137 | .AddMetadataReference(projectId, NetStandard) 138 | .AddMetadataReference(projectId, SystemIoReference) 139 | .AddMetadataReference(projectId, SystemRuntimeReference) 140 | .AddMetadataReference(projectId, CSharpSymbolsReference) 141 | .AddMetadataReference(projectId, StackExchangeRedisReference) 142 | .AddMetadataReference(projectId, CodeAnalysisReference); 143 | 144 | var count = 0; 145 | foreach (var source in sources) 146 | { 147 | var newFileName = fileNamePrefix + count + "." + fileExt; 148 | var documentId = DocumentId.CreateNewId(projectId, newFileName); 149 | solution = solution.AddDocument(documentId, newFileName, SourceText.From(source)); 150 | count++; 151 | } 152 | 153 | return solution.GetProject(projectId); 154 | } 155 | 156 | /// 157 | /// Given an array of strings as sources and a language, turn them into a project and return the documents and spans of it. 158 | /// 159 | /// Classes in the form of strings 160 | /// The language the source code is in 161 | /// A Tuple containing the Documents produced from the sources and their TextSpans if relevant 162 | private static Document[] GetDocuments(string[] sources, string language) 163 | { 164 | if (language != LanguageNames.CSharp && language != LanguageNames.VisualBasic) 165 | { 166 | throw new ArgumentException("Unsupported Language"); 167 | } 168 | 169 | var project = CreateProject(sources, language); 170 | var documents = project.Documents.ToArray(); 171 | 172 | if (sources.Length != documents.Length) 173 | { 174 | throw new InvalidOperationException("Amount of sources did not match amount of Documents created"); 175 | } 176 | 177 | return documents; 178 | } 179 | 180 | /// 181 | /// Given classes in the form of strings, their language, and an IDiagnosticAnalyzer to apply to it, return the diagnostics 182 | /// found in the string after converting it to a document. 183 | /// 184 | /// Classes in the form of strings 185 | /// The language the source classes are in 186 | /// The analyzer to be run on the sources 187 | /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location 188 | private static Diagnostic[] GetSortedDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer) 189 | { 190 | return GetSortedDiagnosticsFromDocuments(analyzer, GetDocuments(sources, language)); 191 | } 192 | 193 | /// 194 | /// Sort diagnostics by location in source document 195 | /// 196 | /// The list of Diagnostics to be sorted 197 | /// An IEnumerable containing the Diagnostics in order of Location 198 | private static Diagnostic[] SortDiagnostics(IEnumerable diagnostics) 199 | { 200 | return diagnostics.OrderBy(d => d.Location.SourceSpan.Start) 201 | .ToArray(); 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer.Test/SendingCommandsInLoopAnalyzerTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.Diagnostics; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using TestHelper; 5 | 6 | namespace StackExchange.Redis.Analyzer.Test; 7 | 8 | [TestClass] 9 | public class SendingCommandsInLoopAnalyzerTests : DiagnosticVerifier 10 | { 11 | protected override string TestDataFolder => "GettingDataInLoopAnalyzer"; 12 | 13 | [TestMethod] 14 | public void Empty_NotTriggered() 15 | { 16 | const string test = @""; 17 | 18 | VerifyCSharpDiagnostic(test); 19 | } 20 | 21 | [TestMethod] 22 | public void GetStringInLoop_AnalyzerTriggered() 23 | { 24 | var code = ReadTestData("GetStringInLoop.cs"); 25 | var expected = new DiagnosticResult 26 | { 27 | Id = SendingCommandsInLoopAnalyzer.DiagnosticId, 28 | Message = string.Format(SendingCommandsInLoopAnalyzer.MessageFormat, "StringGetAsync", "StackExchange.Redis.IDatabaseAsync.StringGetAsync(StackExchange.Redis.RedisKey[], StackExchange.Redis.CommandFlags)"), 29 | Severity = DiagnosticSeverity.Warning, 30 | Locations = new[] { new DiagnosticResultLocation("Test0.cs", 13, 35) } 31 | }; 32 | 33 | VerifyCSharpDiagnostic(code, expected); 34 | } 35 | 36 | [TestMethod] 37 | public void SetContainsInForeach_AnalyzerTriggered() 38 | { 39 | var code = ReadTestData("SetContainsInForeach.cs"); 40 | var expected = new DiagnosticResult 41 | { 42 | Id = SendingCommandsInLoopAnalyzer.DiagnosticId, 43 | Message = string.Format(SendingCommandsInLoopAnalyzer.MessageFormat, "SetContains", "StackExchange.Redis.IDatabase.SetContains(StackExchange.Redis.RedisKey, StackExchange.Redis.RedisValue[], StackExchange.Redis.CommandFlags)"), 44 | Severity = DiagnosticSeverity.Warning, 45 | Locations = new[] { new DiagnosticResultLocation("Test0.cs", 17, 29) } 46 | }; 47 | 48 | VerifyCSharpDiagnostic(code, expected); 49 | } 50 | 51 | [TestMethod] 52 | public void SetCombineInForeach_NotTriggered() 53 | { 54 | var code = ReadTestData("SetCombineInForeach.cs"); 55 | 56 | VerifyCSharpDiagnostic(code); 57 | } 58 | 59 | [TestMethod] 60 | public void SetRemoveInForeach_NotTriggered() 61 | { 62 | var code = ReadTestData("SetRemoveInForeach.cs"); 63 | 64 | VerifyCSharpDiagnostic(code); 65 | } 66 | 67 | [TestMethod] 68 | public void GetConstantStringInLoop_NotTriggered() 69 | { 70 | var code = ReadTestData("GetConstantStringInLoop.cs"); 71 | 72 | VerifyCSharpDiagnostic(code); 73 | } 74 | 75 | [TestMethod] 76 | public void KeyExistsInLoop_NotTriggered() 77 | { 78 | var code = ReadTestData("KeyExistsInLoop.cs"); 79 | 80 | VerifyCSharpDiagnostic(code); 81 | } 82 | 83 | protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() 84 | { 85 | return new SendingCommandsInLoopAnalyzer(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer.Test/StackExchange.Redis.Analyzer.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer.Test/TestData/GettingDataInLoopAnalyzer/GetConstantStringInLoop.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace StackExchange.Redis.Analyzer.Test.TestData 4 | { 5 | class Program 6 | { 7 | static async Task Main(string[] args) 8 | { 9 | ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost"); 10 | IDatabase db = redis.GetDatabase(); 11 | for (int i = 0; i < 5; i++) 12 | { 13 | const var demo = "constant"; 14 | var value = await db.StringGetAsync(demo); 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer.Test/TestData/GettingDataInLoopAnalyzer/GetStringInLoop.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace StackExchange.Redis.Analyzer.Test.TestData 4 | { 5 | class Program 6 | { 7 | static async Task Main(string[] args) 8 | { 9 | ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost"); 10 | IDatabase db = redis.GetDatabase(); 11 | for (int i = 0; i < 5; i++) 12 | { 13 | var value = await db.StringGetAsync(i.ToString()); 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer.Test/TestData/GettingDataInLoopAnalyzer/KeyExistsInLoop.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace StackExchange.Redis.Analyzer.Test.TestData 4 | { 5 | class Program 6 | { 7 | static async Task Main(string[] args) 8 | { 9 | ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost"); 10 | IDatabase db = redis.GetDatabase(); 11 | for (int i = 0; i < 5; i++) 12 | { 13 | var value = await db.KeyExists(i.ToString()); 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer.Test/TestData/GettingDataInLoopAnalyzer/SetCombineInForeach.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace StackExchange.Redis.Analyzer.Test.TestData 4 | { 5 | class Program 6 | { 7 | static void Main(string[] args) 8 | { 9 | ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost"); 10 | IDatabase db = redis.GetDatabase(); 11 | foreach (var setValue in new[] { "one", "two" }) 12 | { 13 | var value = db.SetCombine(SetOperation.Intersect, "key", setValue); 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer.Test/TestData/GettingDataInLoopAnalyzer/SetContainsInForeach.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace StackExchange.Redis.Analyzer.Test.TestData 4 | { 5 | class Program 6 | { 7 | static void Main(string[] args) 8 | { 9 | ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost"); 10 | IDatabase db = redis.GetDatabase(); 11 | var key = "key"; 12 | 13 | // This should not be reported 14 | var value = db.SetContains(key, "one"); 15 | foreach (var setValue in new[] { "one", "two" }) 16 | { 17 | var value = db.SetContains(key, setValue); 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer.Test/TestData/GettingDataInLoopAnalyzer/SetContainsWihArrayOverloadInForeach.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace StackExchange.Redis.Analyzer.Test.TestData 4 | { 5 | class Program 6 | { 7 | static void Main(string[] args) 8 | { 9 | ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost"); 10 | IDatabase db = redis.GetDatabase(); 11 | foreach (var key in new[] { "one", "two" }) 12 | { 13 | var value = db.SetContains(key, new []{ "1", "2" }, CommandFlags.None); 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer.Test/TestData/GettingDataInLoopAnalyzer/SetRemoveInForeach.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace StackExchange.Redis.Analyzer.Test.TestData 4 | { 5 | class Program 6 | { 7 | static void Main(string[] args) 8 | { 9 | ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost"); 10 | IDatabase db = redis.GetDatabase(); 11 | var value = "deleted"; 12 | foreach (var setKey in new[] { "one", "two" }) 13 | { 14 | db.SetRemove(setKey, value); 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer.Test/TestData/TransactionDeadlockAnalyzer/AwaitStringGetAsyncTestData.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace StackExchange.Redis.Analyzer.Test.TestData 4 | { 5 | class Program 6 | { 7 | static async Task Main(string[] args) 8 | { 9 | ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost"); 10 | IDatabase db = redis.GetDatabase(); 11 | var tran = db.CreateTransaction(); 12 | var value = await tran.StringGetAsync("test", CommandFlags.None); 13 | await tran.ExecuteAsync(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer.Test/TestData/TransactionDeadlockAnalyzer/ContinueWithCallbackTestData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace StackExchange.Redis.Analyzer.Test.TestData 5 | { 6 | internal class Program 7 | { 8 | private static async Task Main(string[] args) 9 | { 10 | var redis = await ConnectionMultiplexer.ConnectAsync("localhost") 11 | .ConfigureAwait(false); 12 | var db = redis.GetDatabase(); 13 | 14 | await Task.WhenAll(Task.CompletedTask) 15 | .ContinueWith( 16 | async task => 17 | { 18 | var transaction = db.CreateTransaction(); 19 | 20 | var tasks = new List { transaction.SetAddAsync("test", "test") }; 21 | 22 | await transaction.ExecuteAsync().ConfigureAwait(false); 23 | await Task.WhenAll(tasks).ConfigureAwait(false); 24 | }) 25 | .ConfigureAwait(false); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer.Test/TestData/TransactionDeadlockAnalyzer/ResultStringGetAsyncTestData.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace StackExchange.Redis.Analyzer.Test.TestData 4 | { 5 | class Program 6 | { 7 | static async Task Main(string[] args) 8 | { 9 | ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost"); 10 | IDatabase db = redis.GetDatabase(); 11 | var tran = db.CreateTransaction(); 12 | var s = tran.StringGetAsync("test", CommandFlags.None).Result; 13 | await tran.ExecuteAsync(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer.Test/TestData/TransactionDeadlockAnalyzer/TaskWaitAllStringGetAsyncTestData.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace StackExchange.Redis.Analyzer.Test.TestData 4 | { 5 | class Program 6 | { 7 | static async Task Main(string[] args) 8 | { 9 | ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost"); 10 | IDatabase db = redis.GetDatabase(); 11 | var tran = db.CreateTransaction(); 12 | Task.WaitAll(tran.StringGetAsync("test")); 13 | await tran.ExecuteAsync(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer.Test/TestData/TransactionDeadlockAnalyzer/WaitStringGetAsyncTestData.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace StackExchange.Redis.Analyzer.Test.TestData 4 | { 5 | class Program 6 | { 7 | static async Task Main(string[] args) 8 | { 9 | ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost"); 10 | IDatabase db = redis.GetDatabase(); 11 | var tran = db.CreateTransaction(); 12 | tran.StringGetAsync("test", CommandFlags.None).Wait(); 13 | await tran.ExecuteAsync(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer.Test/TransactionDeadlockAnalyzerTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.Diagnostics; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | using TestHelper; 6 | 7 | namespace StackExchange.Redis.Analyzer.Test 8 | { 9 | [TestClass] 10 | public class TransactionDeadlockAnalyzerTests : DiagnosticVerifier 11 | { 12 | protected override string TestDataFolder => "TransactionDeadlockAnalyzer"; 13 | 14 | [TestMethod] 15 | public void Empty_NotTriggered() 16 | { 17 | const string test = @""; 18 | 19 | VerifyCSharpDiagnostic(test); 20 | } 21 | 22 | [TestMethod] 23 | public void AwaitStringGetAsync_AnalyzerTriggered() 24 | { 25 | var code = ReadTestData("AwaitStringGetAsyncTestData.cs"); 26 | 27 | var expected = new DiagnosticResult 28 | { 29 | Id = TransactionDeadlockAnalyzer.DiagnosticId, 30 | Message = string.Format(TransactionDeadlockAnalyzer.MessageFormat, "StringGetAsync"), 31 | Severity = DiagnosticSeverity.Warning, 32 | Locations = new[] { new DiagnosticResultLocation("Test0.cs", 12, 25) } 33 | }; 34 | 35 | VerifyCSharpDiagnostic(code, expected); 36 | } 37 | 38 | [TestMethod] 39 | public void ResultStringGetAsync_AnalyzerTriggered() 40 | { 41 | var code = ReadTestData("ResultStringGetAsyncTestData.cs"); 42 | 43 | var expected = new DiagnosticResult 44 | { 45 | Id = TransactionDeadlockAnalyzer.DiagnosticId, 46 | Message = string.Format(TransactionDeadlockAnalyzer.MessageFormat, "StringGetAsync"), 47 | Severity = DiagnosticSeverity.Warning, 48 | Locations = new[] { new DiagnosticResultLocation("Test0.cs", 12, 21) } 49 | }; 50 | 51 | VerifyCSharpDiagnostic(code, expected); 52 | } 53 | 54 | [TestMethod] 55 | public void WaitStringGetAsync_AnalyzerTriggered() 56 | { 57 | var code = ReadTestData("WaitStringGetAsyncTestData.cs"); 58 | 59 | var expected = new DiagnosticResult 60 | { 61 | Id = TransactionDeadlockAnalyzer.DiagnosticId, 62 | Message = string.Format(TransactionDeadlockAnalyzer.MessageFormat, "StringGetAsync"), 63 | Severity = DiagnosticSeverity.Warning, 64 | Locations = new[] { new DiagnosticResultLocation("Test0.cs", 12, 13) } 65 | }; 66 | 67 | VerifyCSharpDiagnostic(code, expected); 68 | } 69 | 70 | [TestMethod] 71 | public void TaskWaitAllStringGetAsync_AnalyzerTriggered() 72 | { 73 | var code = ReadTestData("TaskWaitAllStringGetAsyncTestData.cs"); 74 | 75 | var expected = new DiagnosticResult 76 | { 77 | Id = TransactionDeadlockAnalyzer.DiagnosticId, 78 | Message = string.Format(TransactionDeadlockAnalyzer.MessageFormat, "StringGetAsync"), 79 | Severity = DiagnosticSeverity.Warning, 80 | Locations = new[] { new DiagnosticResultLocation("Test0.cs", 12, 26) } 81 | }; 82 | 83 | VerifyCSharpDiagnostic(code, expected); 84 | } 85 | 86 | [TestMethod] 87 | public void ContinueWithCallback_NotTriggered() 88 | { 89 | var code = ReadTestData("ContinueWithCallbackTestData.cs"); 90 | 91 | VerifyCSharpDiagnostic(code); 92 | } 93 | 94 | protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() 95 | { 96 | return new TransactionDeadlockAnalyzer(); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer.Test/Verifiers/DiagnosticVerifier.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text; 6 | 7 | using Microsoft.CodeAnalysis; 8 | using Microsoft.CodeAnalysis.Diagnostics; 9 | using Microsoft.VisualStudio.TestTools.UnitTesting; 10 | // ReSharper disable All 11 | 12 | namespace TestHelper 13 | { 14 | /// 15 | /// Superclass of all Unit Tests for DiagnosticAnalyzers 16 | /// 17 | public abstract partial class DiagnosticVerifier 18 | { 19 | protected abstract string TestDataFolder { get; } 20 | 21 | public string ReadTestData(string testDataFileName) 22 | { 23 | var assembly = Assembly.GetExecutingAssembly(); 24 | var resourceName = $"StackExchange.Redis.Analyzer.Test.TestData.{TestDataFolder}.{testDataFileName}"; 25 | 26 | using (var stream = assembly.GetManifestResourceStream(resourceName)) 27 | using (var reader = new StreamReader(stream)) 28 | { 29 | return reader.ReadToEnd(); 30 | } 31 | } 32 | 33 | #region To be implemented by Test classes 34 | /// 35 | /// Get the CSharp analyzer being tested - to be implemented in non-abstract class 36 | /// 37 | protected virtual DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() 38 | { 39 | return null; 40 | } 41 | 42 | /// 43 | /// Get the Visual Basic analyzer being tested (C#) - to be implemented in non-abstract class 44 | /// 45 | protected virtual DiagnosticAnalyzer GetBasicDiagnosticAnalyzer() 46 | { 47 | return null; 48 | } 49 | #endregion 50 | 51 | #region Verifier wrappers 52 | 53 | /// 54 | /// Called to test a C# DiagnosticAnalyzer when applied on the single inputted string as a source 55 | /// Note: input a DiagnosticResult for each Diagnostic expected 56 | /// 57 | /// A class in the form of a string to run the analyzer on 58 | /// DiagnosticResults that should appear after the analyzer is run on the source 59 | protected void VerifyCSharpDiagnostic(string source, params DiagnosticResult[] expected) 60 | { 61 | VerifyDiagnostics(new[] { source }, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected); 62 | } 63 | 64 | /// 65 | /// Called to test a VB DiagnosticAnalyzer when applied on the single inputted string as a source 66 | /// Note: input a DiagnosticResult for each Diagnostic expected 67 | /// 68 | /// A class in the form of a string to run the analyzer on 69 | /// DiagnosticResults that should appear after the analyzer is run on the source 70 | protected void VerifyBasicDiagnostic(string source, params DiagnosticResult[] expected) 71 | { 72 | VerifyDiagnostics(new[] { source }, LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), expected); 73 | } 74 | 75 | /// 76 | /// Called to test a C# DiagnosticAnalyzer when applied on the inputted strings as a source 77 | /// Note: input a DiagnosticResult for each Diagnostic expected 78 | /// 79 | /// An array of strings to create source documents from to run the analyzers on 80 | /// DiagnosticResults that should appear after the analyzer is run on the sources 81 | protected void VerifyCSharpDiagnostic(string[] sources, params DiagnosticResult[] expected) 82 | { 83 | VerifyDiagnostics(sources, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected); 84 | } 85 | 86 | /// 87 | /// Called to test a VB DiagnosticAnalyzer when applied on the inputted strings as a source 88 | /// Note: input a DiagnosticResult for each Diagnostic expected 89 | /// 90 | /// An array of strings to create source documents from to run the analyzers on 91 | /// DiagnosticResults that should appear after the analyzer is run on the sources 92 | protected void VerifyBasicDiagnostic(string[] sources, params DiagnosticResult[] expected) 93 | { 94 | VerifyDiagnostics(sources, LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), expected); 95 | } 96 | 97 | /// 98 | /// General method that gets a collection of actual diagnostics found in the source after the analyzer is run, 99 | /// then verifies each of them. 100 | /// 101 | /// An array of strings to create source documents from to run the analyzers on 102 | /// The language of the classes represented by the source strings 103 | /// The analyzer to be run on the source code 104 | /// DiagnosticResults that should appear after the analyzer is run on the sources 105 | private void VerifyDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expected) 106 | { 107 | var diagnostics = GetSortedDiagnostics(sources, language, analyzer); 108 | VerifyDiagnosticResults(diagnostics, analyzer, expected); 109 | } 110 | 111 | #endregion 112 | 113 | #region Actual comparisons and verifications 114 | /// 115 | /// Checks each of the actual Diagnostics found and compares them with the corresponding DiagnosticResult in the array of expected results. 116 | /// Diagnostics are considered equal only if the DiagnosticResultLocation, Id, Severity, and Message of the DiagnosticResult match the actual diagnostic. 117 | /// 118 | /// The Diagnostics found by the compiler after running the analyzer on the source code 119 | /// The analyzer that was being run on the sources 120 | /// Diagnostic Results that should have appeared in the code 121 | private static void VerifyDiagnosticResults(IEnumerable actualResults, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expectedResults) 122 | { 123 | int expectedCount = expectedResults.Count(); 124 | int actualCount = actualResults.Count(); 125 | 126 | if (expectedCount != actualCount) 127 | { 128 | string diagnosticsOutput = actualResults.Any() ? FormatDiagnostics(analyzer, actualResults.ToArray()) : " NONE."; 129 | 130 | Assert.IsTrue(false, 131 | string.Format("Mismatch between number of diagnostics returned, expected \"{0}\" actual \"{1}\"\r\n\r\nDiagnostics:\r\n{2}\r\n", expectedCount, actualCount, diagnosticsOutput)); 132 | } 133 | 134 | for (int i = 0; i < expectedResults.Length; i++) 135 | { 136 | var actual = actualResults.ElementAt(i); 137 | var expected = expectedResults[i]; 138 | 139 | if (expected.Line == -1 && expected.Column == -1) 140 | { 141 | if (actual.Location != Location.None) 142 | { 143 | Assert.IsTrue(false, 144 | string.Format("Expected:\nA project diagnostic with No location\nActual:\n{0}", 145 | FormatDiagnostics(analyzer, actual))); 146 | } 147 | } 148 | else 149 | { 150 | VerifyDiagnosticLocation(analyzer, actual, actual.Location, expected.Locations.First()); 151 | var additionalLocations = actual.AdditionalLocations.ToArray(); 152 | 153 | if (additionalLocations.Length != expected.Locations.Length - 1) 154 | { 155 | Assert.IsTrue(false, 156 | string.Format("Expected {0} additional locations but got {1} for Diagnostic:\r\n {2}\r\n", 157 | expected.Locations.Length - 1, additionalLocations.Length, 158 | FormatDiagnostics(analyzer, actual))); 159 | } 160 | 161 | for (int j = 0; j < additionalLocations.Length; ++j) 162 | { 163 | VerifyDiagnosticLocation(analyzer, actual, additionalLocations[j], expected.Locations[j + 1]); 164 | } 165 | } 166 | 167 | if (actual.Id != expected.Id) 168 | { 169 | Assert.IsTrue(false, 170 | string.Format("Expected diagnostic id to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", 171 | expected.Id, actual.Id, FormatDiagnostics(analyzer, actual))); 172 | } 173 | 174 | if (actual.Severity != expected.Severity) 175 | { 176 | Assert.IsTrue(false, 177 | string.Format("Expected diagnostic severity to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", 178 | expected.Severity, actual.Severity, FormatDiagnostics(analyzer, actual))); 179 | } 180 | 181 | if (actual.GetMessage() != expected.Message) 182 | { 183 | Assert.IsTrue(false, 184 | string.Format("Expected diagnostic message to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", 185 | expected.Message, actual.GetMessage(), FormatDiagnostics(analyzer, actual))); 186 | } 187 | } 188 | } 189 | 190 | /// 191 | /// Helper method to VerifyDiagnosticResult that checks the location of a diagnostic and compares it with the location in the expected DiagnosticResult. 192 | /// 193 | /// The analyzer that was being run on the sources 194 | /// The diagnostic that was found in the code 195 | /// The Location of the Diagnostic found in the code 196 | /// The DiagnosticResultLocation that should have been found 197 | private static void VerifyDiagnosticLocation(DiagnosticAnalyzer analyzer, Diagnostic diagnostic, Location actual, DiagnosticResultLocation expected) 198 | { 199 | var actualSpan = actual.GetLineSpan(); 200 | 201 | Assert.IsTrue(actualSpan.Path == expected.Path || (actualSpan.Path != null && actualSpan.Path.Contains("Test0.") && expected.Path.Contains("Test.")), 202 | string.Format("Expected diagnostic to be in file \"{0}\" was actually in file \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", 203 | expected.Path, actualSpan.Path, FormatDiagnostics(analyzer, diagnostic))); 204 | 205 | var actualLinePosition = actualSpan.StartLinePosition; 206 | 207 | // Only check line position if there is an actual line in the real diagnostic 208 | if (actualLinePosition.Line > 0) 209 | { 210 | if (actualLinePosition.Line + 1 != expected.Line) 211 | { 212 | Assert.IsTrue(false, 213 | string.Format("Expected diagnostic to be on line \"{0}\" was actually on line \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", 214 | expected.Line, actualLinePosition.Line + 1, FormatDiagnostics(analyzer, diagnostic))); 215 | } 216 | } 217 | 218 | // Only check column position if there is an actual column position in the real diagnostic 219 | if (actualLinePosition.Character > 0) 220 | { 221 | if (actualLinePosition.Character + 1 != expected.Column) 222 | { 223 | Assert.IsTrue(false, 224 | string.Format("Expected diagnostic to start at column \"{0}\" was actually at column \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", 225 | expected.Column, actualLinePosition.Character + 1, FormatDiagnostics(analyzer, diagnostic))); 226 | } 227 | } 228 | } 229 | #endregion 230 | 231 | #region Formatting Diagnostics 232 | /// 233 | /// Helper method to format a Diagnostic into an easily readable string 234 | /// 235 | /// The analyzer that this verifier tests 236 | /// The Diagnostics to be formatted 237 | /// The Diagnostics formatted as a string 238 | private static string FormatDiagnostics(DiagnosticAnalyzer analyzer, params Diagnostic[] diagnostics) 239 | { 240 | var builder = new StringBuilder(); 241 | for (int i = 0; i < diagnostics.Length; ++i) 242 | { 243 | builder.AppendLine("// " + diagnostics[i].ToString()); 244 | 245 | var analyzerType = analyzer.GetType(); 246 | var rules = analyzer.SupportedDiagnostics; 247 | 248 | foreach (var rule in rules) 249 | { 250 | if (rule != null && rule.Id == diagnostics[i].Id) 251 | { 252 | var location = diagnostics[i].Location; 253 | if (location == Location.None) 254 | { 255 | builder.AppendFormat("GetGlobalResult({0}.{1})", analyzerType.Name, rule.Id); 256 | } 257 | else 258 | { 259 | Assert.IsTrue(location.IsInSource, 260 | $"Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata: {diagnostics[i]}\r\n"); 261 | 262 | string resultMethodName = diagnostics[i].Location.SourceTree.FilePath.EndsWith(".cs") ? "GetCSharpResultAt" : "GetBasicResultAt"; 263 | var linePosition = diagnostics[i].Location.GetLineSpan().StartLinePosition; 264 | 265 | builder.AppendFormat("{0}({1}, {2}, {3}.{4})", 266 | resultMethodName, 267 | linePosition.Line + 1, 268 | linePosition.Character + 1, 269 | analyzerType.Name, 270 | rule.Id); 271 | } 272 | 273 | if (i != diagnostics.Length - 1) 274 | { 275 | builder.Append(','); 276 | } 277 | 278 | builder.AppendLine(); 279 | break; 280 | } 281 | } 282 | } 283 | return builder.ToString(); 284 | } 285 | #endregion 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer.Vsix/StackExchange.Redis.Analyzer.Vsix.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 15.0 6 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 7 | 8 | 9 | 14.0 10 | 11 | 12 | 13 | 14 | Debug 15 | AnyCPU 16 | AnyCPU 17 | 2.0 18 | {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 19 | {CCBECB4F-1068-4322-8EE6-42D647817CC2} 20 | Library 21 | Properties 22 | StackExchange.Redis.Analyzer.Vsix 23 | StackExchange.Redis.Analyzer 24 | v4.6.1 25 | false 26 | false 27 | false 28 | false 29 | false 30 | false 31 | Roslyn 32 | 33 | 34 | true 35 | full 36 | false 37 | bin\Debug\ 38 | DEBUG;TRACE 39 | prompt 40 | 4 41 | 42 | 43 | pdbonly 44 | true 45 | bin\Release\ 46 | TRACE 47 | prompt 48 | 4 49 | 50 | 51 | Program 52 | $(DevEnvDir)devenv.exe 53 | /rootsuffix Roslyn 54 | 55 | 56 | 57 | Designer 58 | 59 | 60 | 61 | 62 | {B574F990-768E-4546-9C53-ECDD0D0A7513} 63 | StackExchange.Redis.Analyzer 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer.Vsix/source.extension.vsixmanifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | StackExchange.Redis.Analyzer 6 | Roslyn-based analyzer for StackExchange.Redis library 7 | https://github.com/olsh/stack-exchange-redis-analyzer 8 | https://github.com/olsh/stack-exchange-redis-analyzer 9 | https://github.com/olsh/stack-exchange-redis-analyzer/releases 10 | redis analyzer roslyn stackexchange 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer/SendingCommandsInLoopAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Linq; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp; 5 | using Microsoft.CodeAnalysis.CSharp.Syntax; 6 | using Microsoft.CodeAnalysis.Diagnostics; 7 | 8 | namespace StackExchange.Redis.Analyzer 9 | { 10 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 11 | public class SendingCommandsInLoopAnalyzer : DiagnosticAnalyzer 12 | { 13 | public const string DiagnosticId = "SER002"; 14 | 15 | public const string MessageFormat = "Method {0} is called in a loop, consider using a batch overload {1} instead"; 16 | 17 | private const string Category = "API Guidance"; 18 | 19 | private static readonly LocalizableString Description = 20 | "Sending commands in a loop can be slow, consider using a batch overload with array of keys instead."; 21 | 22 | private const string Title = "Sending commands in a loop can be slow, consider using a batch overload with array of keys instead"; 23 | 24 | private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( 25 | #pragma warning disable RS2008 26 | DiagnosticId, 27 | #pragma warning restore RS2008 28 | Title, 29 | MessageFormat, 30 | Category, 31 | DiagnosticSeverity.Warning, 32 | true, 33 | Description, 34 | "https://github.com/olsh/stack-exchange-redis-analyzer#ser002"); 35 | 36 | private readonly string[] _ignoredMethods = { "KeyExists" }; 37 | 38 | public override void Initialize(AnalysisContext context) 39 | { 40 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); 41 | context.EnableConcurrentExecution(); 42 | context.RegisterSyntaxNodeAction(AnalyzeInvocationExpression, SyntaxKind.InvocationExpression); 43 | } 44 | 45 | private void AnalyzeInvocationExpression(SyntaxNodeAnalysisContext context) 46 | { 47 | var expressionSyntax = (InvocationExpressionSyntax)context.Node; 48 | var memberAccessExpressionSyntax = expressionSyntax.Expression as MemberAccessExpressionSyntax; 49 | if (memberAccessExpressionSyntax == null) 50 | { 51 | return; 52 | } 53 | 54 | var outerLoop = FindOuterLoop(expressionSyntax); 55 | if (outerLoop == null) 56 | { 57 | return; 58 | } 59 | 60 | var overload = FindMethodWithArrayOverload(expressionSyntax, outerLoop, context.SemanticModel); 61 | if (overload == null) 62 | { 63 | return; 64 | } 65 | 66 | context.ReportDiagnostic(Diagnostic.Create(Rule, context.Node.GetLocation(), memberAccessExpressionSyntax.Name, overload.OriginalDefinition.ToString())); 67 | } 68 | 69 | private StatementSyntax FindOuterLoop(InvocationExpressionSyntax expressionSyntax) 70 | { 71 | return expressionSyntax.FirstAncestorOrSelf() 72 | ?? (StatementSyntax)expressionSyntax.FirstAncestorOrSelf() 73 | ?? expressionSyntax.FirstAncestorOrSelf(); 74 | } 75 | 76 | // ReSharper disable once CognitiveComplexity 77 | private IMethodSymbol FindMethodWithArrayOverload( 78 | InvocationExpressionSyntax expressionSyntax, 79 | StatementSyntax outerLoopSyntax, 80 | SemanticModel contextSemanticModel) 81 | { 82 | var methodSymbol = contextSemanticModel.GetSymbolInfo(expressionSyntax).Symbol as IMethodSymbol; 83 | var containingType = methodSymbol?.ContainingType; 84 | if (containingType == null) 85 | { 86 | return null; 87 | } 88 | 89 | if (containingType.Name != "IDatabase" && containingType.Name != "IDatabaseAsync") 90 | { 91 | return null; 92 | } 93 | 94 | var containingNamespace = containingType.ContainingNamespace; 95 | if (containingNamespace == null) 96 | { 97 | return null; 98 | } 99 | 100 | if (containingNamespace.Name != "Redis" || containingNamespace.ContainingNamespace?.Name != "StackExchange") 101 | { 102 | return null; 103 | } 104 | 105 | // Check if method is ignored 106 | if (_ignoredMethods.Contains(methodSymbol.Name) 107 | || _ignoredMethods.Contains($"{methodSymbol.Name}Async")) 108 | { 109 | return null; 110 | } 111 | 112 | var parameters = methodSymbol.Parameters; 113 | if (parameters.Length == 0) 114 | { 115 | return null; 116 | } 117 | 118 | // Check method overloads 119 | var overloads = containingType 120 | .GetMembers(methodSymbol.Name) 121 | .OfType() 122 | .Where(o => o.Equals(methodSymbol, SymbolEqualityComparer.Default) == false) 123 | .ToArray(); 124 | 125 | // Get assignments inside the loop 126 | var assignments = outerLoopSyntax 127 | .DescendantNodes() 128 | .OfType() 129 | .ToArray(); 130 | 131 | foreach (var overload in overloads) 132 | { 133 | if (IsSuitableBatchOverload(methodSymbol, overload, expressionSyntax, outerLoopSyntax, assignments, 134 | contextSemanticModel)) 135 | { 136 | return overload; 137 | } 138 | } 139 | 140 | return null; 141 | } 142 | 143 | // ReSharper disable once CognitiveComplexity 144 | private bool IsSuitableBatchOverload( 145 | IMethodSymbol sourceMethod, 146 | IMethodSymbol overload, 147 | InvocationExpressionSyntax expressionSyntax, 148 | StatementSyntax outerLoopSyntax, 149 | AssignmentExpressionSyntax[] assignments, 150 | SemanticModel contextSemanticModel) 151 | { 152 | var hasArrayParameter = false; 153 | foreach (var parameter in sourceMethod.Parameters) 154 | { 155 | var overloadParameter = overload.Parameters.SingleOrDefault(p => p.Ordinal == parameter.Ordinal); 156 | if (overloadParameter == null) 157 | { 158 | return false; 159 | } 160 | 161 | if (overloadParameter.Type.Equals(parameter.Type, SymbolEqualityComparer.Default)) 162 | { 163 | continue; 164 | } 165 | 166 | if (IsSuitableBatchOverload(sourceMethod, overload)) 167 | { 168 | var argumentExpression = expressionSyntax.ArgumentList.Arguments[parameter.Ordinal].Expression; 169 | if (IsParameterIsModifiedInsideTheLoop(contextSemanticModel, assignments, outerLoopSyntax, argumentExpression)) 170 | { 171 | hasArrayParameter = true; 172 | } 173 | } 174 | else 175 | { 176 | return false; 177 | } 178 | } 179 | 180 | return hasArrayParameter; 181 | } 182 | 183 | // ReSharper disable once CognitiveComplexity 184 | private bool IsParameterIsModifiedInsideTheLoop( 185 | SemanticModel contextSemanticModel, 186 | AssignmentExpressionSyntax[] assignments, 187 | StatementSyntax outerLoopSyntax, 188 | ExpressionSyntax parameter) 189 | { 190 | // Check if the parameter is an invocation expression 191 | if (parameter is InvocationExpressionSyntax) 192 | { 193 | return true; 194 | } 195 | 196 | var parameterSymbol = contextSemanticModel.GetSymbolInfo(parameter).Symbol; 197 | if (parameterSymbol == null) 198 | { 199 | return false; 200 | } 201 | 202 | // Check if parameter is a constant 203 | if (parameterSymbol is ILocalSymbol localSymbol && localSymbol.HasConstantValue) 204 | { 205 | return false; 206 | } 207 | 208 | var constantValue = contextSemanticModel.GetConstantValue(parameter); 209 | if (constantValue.HasValue && constantValue.Value != null) 210 | { 211 | return false; 212 | } 213 | 214 | // Check if parameter is modified inside the loop 215 | foreach (var declaringSyntaxReference in parameterSymbol.DeclaringSyntaxReferences) 216 | { 217 | if (outerLoopSyntax.Contains(declaringSyntaxReference.GetSyntax())) 218 | { 219 | return true; 220 | } 221 | } 222 | 223 | foreach (var assignment in assignments) 224 | { 225 | var assignmentSymbol = contextSemanticModel.GetSymbolInfo(assignment.Left).Symbol; 226 | if (assignmentSymbol == null) 227 | { 228 | continue; 229 | } 230 | 231 | if (assignmentSymbol.Equals(parameterSymbol, SymbolEqualityComparer.Default)) 232 | { 233 | return true; 234 | } 235 | } 236 | 237 | return false; 238 | } 239 | 240 | private bool IsSuitableBatchOverload(IMethodSymbol sourceMethod, IMethodSymbol overload) 241 | { 242 | var hasArrayParameter = false; 243 | foreach (var parameter in sourceMethod.Parameters) 244 | { 245 | var overloadParameter = overload.Parameters.SingleOrDefault(p => p.Ordinal == parameter.Ordinal); 246 | if (overloadParameter == null) 247 | { 248 | return false; 249 | } 250 | 251 | if (overloadParameter.Type.Equals(parameter.Type, SymbolEqualityComparer.Default)) 252 | { 253 | continue; 254 | } 255 | 256 | if (IsMethodIsArrayOf(overloadParameter, parameter)) 257 | { 258 | hasArrayParameter = true; 259 | } 260 | else 261 | { 262 | return false; 263 | } 264 | } 265 | 266 | return hasArrayParameter; 267 | } 268 | 269 | private bool IsMethodIsArrayOf(IParameterSymbol arrayParameter, IParameterSymbol baseParameter) 270 | { 271 | if (!(arrayParameter.Type is IArrayTypeSymbol arrayElementType)) 272 | { 273 | return false; 274 | } 275 | 276 | return arrayElementType.ElementType.Equals(baseParameter.Type, SymbolEqualityComparer.Default); 277 | } 278 | 279 | public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | false 6 | True 7 | true 8 | 9 | 10 | 11 | StackExchange.Redis.Analyzer 12 | Oleg Shevchenko 13 | MIT 14 | https://github.com/olsh/stack-exchange-redis-analyzer 15 | https://github.com/olsh/stack-exchange-redis-analyzer 16 | false 17 | Roslyn-based analyzer for StackExchange.Redis library 18 | https://github.com/olsh/stack-exchange-redis-analyzer/releases 19 | stackexchange, redis, analyzers 20 | true 21 | 1.3.3 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer/TransactionDeadlockAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Linq; 4 | 5 | using Microsoft.CodeAnalysis; 6 | using Microsoft.CodeAnalysis.CSharp; 7 | using Microsoft.CodeAnalysis.CSharp.Syntax; 8 | using Microsoft.CodeAnalysis.Diagnostics; 9 | 10 | namespace StackExchange.Redis.Analyzer 11 | { 12 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 13 | public class TransactionDeadlockAnalyzer : DiagnosticAnalyzer 14 | { 15 | public const string DiagnosticId = "SER001"; 16 | 17 | public const string MessageFormat = "Method {0} is blocked"; 18 | 19 | private const string Category = "API Guidance"; 20 | 21 | private static readonly LocalizableString Description = 22 | "Async methods on ITransaction type shouldn't be blocked."; 23 | 24 | private const string Title = "Async method is blocked on transaction before the transaction execution"; 25 | 26 | private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( 27 | #pragma warning disable RS2008 28 | DiagnosticId, 29 | #pragma warning restore RS2008 30 | Title, 31 | MessageFormat, 32 | Category, 33 | DiagnosticSeverity.Warning, 34 | true, 35 | Description, 36 | "https://github.com/olsh/stack-exchange-redis-analyzer#ser001"); 37 | 38 | private static readonly string[] BlockingTaskMethods = { "WaitAll", "WhenAll", "WhenAny", "WaitAny" }; 39 | 40 | public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); 41 | 42 | public override void Initialize(AnalysisContext context) 43 | { 44 | context.EnableConcurrentExecution(); 45 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); 46 | 47 | context.RegisterSyntaxNodeAction(AnalyzeAwaitExpression, SyntaxKind.AwaitExpression); 48 | context.RegisterSyntaxNodeAction(AnalyzeInvocationExpression, SyntaxKind.InvocationExpression); 49 | context.RegisterSyntaxNodeAction(AnalyzeArgumentExpression, SyntaxKind.Argument); 50 | } 51 | 52 | private void AnalyzeArgumentExpression(SyntaxNodeAnalysisContext context) 53 | { 54 | var argumentSyntax = (ArgumentSyntax)context.Node; 55 | 56 | foreach (var accessExpressionSyntax in argumentSyntax 57 | .DescendantNodes(node => !node.IsKind(SyntaxKind.ArgumentList)) 58 | .OfType()) 59 | { 60 | if (IsDangerousMethod(context, accessExpressionSyntax) 61 | && IsArgumentOfBlockedMethod(context, argumentSyntax)) 62 | { 63 | context.ReportDiagnostic( 64 | Diagnostic.Create(Rule, context.Node.GetLocation(), accessExpressionSyntax.Name.ToString())); 65 | 66 | return; 67 | } 68 | } 69 | } 70 | 71 | private void AnalyzeAwaitExpression(SyntaxNodeAnalysisContext context) 72 | { 73 | var awaitExpressionSyntax = (AwaitExpressionSyntax)context.Node; 74 | 75 | foreach (var memberAccessExpressionSyntax in awaitExpressionSyntax 76 | .DescendantNodes(node => !node.IsKind(SyntaxKind.ArgumentList)) 77 | .OfType()) 78 | { 79 | if (IsDangerousMethod(context, memberAccessExpressionSyntax)) 80 | { 81 | context.ReportDiagnostic( 82 | Diagnostic.Create( 83 | Rule, 84 | context.Node.GetLocation(), 85 | memberAccessExpressionSyntax.Name.ToString())); 86 | 87 | return; 88 | } 89 | } 90 | } 91 | 92 | private void AnalyzeInvocationExpression(SyntaxNodeAnalysisContext context) 93 | { 94 | var invocationExpressionSyntax = (InvocationExpressionSyntax)context.Node; 95 | 96 | var memberAccessExpressionSyntax = invocationExpressionSyntax.Expression as MemberAccessExpressionSyntax; 97 | if (memberAccessExpressionSyntax == null) 98 | { 99 | return; 100 | } 101 | 102 | if (IsDangerousMethod(context, memberAccessExpressionSyntax) 103 | && invocationExpressionSyntax.Parent is MemberAccessExpressionSyntax parentMemberAccessExpressionSyntax 104 | && IsBlockedMethod(parentMemberAccessExpressionSyntax)) 105 | { 106 | context.ReportDiagnostic( 107 | Diagnostic.Create(Rule, context.Node.GetLocation(), memberAccessExpressionSyntax.Name.ToString())); 108 | } 109 | } 110 | 111 | private bool IsArgumentOfBlockedMethod(SyntaxNodeAnalysisContext context, ArgumentSyntax argumentSyntax) 112 | { 113 | var taskType = context.SemanticModel.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task"); 114 | 115 | bool BlockedTaskMethod(ISymbol symbol) 116 | { 117 | if (!(symbol is IMethodSymbol methodSymbol)) 118 | { 119 | return false; 120 | } 121 | 122 | if (methodSymbol.ContainingType.Equals(taskType, SymbolEqualityComparer.Default) && BlockingTaskMethods.Contains(methodSymbol.MetadataName)) 123 | { 124 | return true; 125 | } 126 | 127 | return false; 128 | } 129 | 130 | var invocationExpressionSyntax = argumentSyntax.Parent?.Parent as InvocationExpressionSyntax; 131 | if (invocationExpressionSyntax == null) 132 | { 133 | return false; 134 | } 135 | 136 | var symbolInfo = context.SemanticModel.GetSymbolInfo(invocationExpressionSyntax); 137 | 138 | return BlockedTaskMethod(symbolInfo.Symbol) || (symbolInfo.CandidateSymbols.Length > 0 && symbolInfo.CandidateSymbols.All(BlockedTaskMethod)); 139 | } 140 | 141 | private bool IsBlockedMethod(MemberAccessExpressionSyntax invocationExpressionSyntax) 142 | { 143 | var name = invocationExpressionSyntax.Name.ToString(); 144 | 145 | return name.Equals("wait", StringComparison.InvariantCultureIgnoreCase) || name.Equals( 146 | "result", 147 | StringComparison.InvariantCultureIgnoreCase); 148 | } 149 | 150 | private bool IsDangerousMethod( 151 | SyntaxNodeAnalysisContext context, 152 | MemberAccessExpressionSyntax memberAccessExpressionSyntax) 153 | { 154 | var transactionType = 155 | context.SemanticModel.Compilation.GetTypeByMetadataName("StackExchange.Redis.ITransaction"); 156 | var memberName = memberAccessExpressionSyntax.Expression; 157 | var symbolInfo = context.SemanticModel.GetSymbolInfo(memberName); 158 | var methodName = memberAccessExpressionSyntax.Name.ToString(); 159 | 160 | return symbolInfo.Symbol is ILocalSymbol singleSymbol && singleSymbol.Type.Equals(transactionType, SymbolEqualityComparer.Default) 161 | && methodName != "ExecuteAsync"; 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer/tools/install.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | if($project.Object.SupportsPackageDependencyResolution) 4 | { 5 | if($project.Object.SupportsPackageDependencyResolution()) 6 | { 7 | # Do not install analyzers via install.ps1, instead let the project system handle it. 8 | return 9 | } 10 | } 11 | 12 | $analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve 13 | 14 | foreach($analyzersPath in $analyzersPaths) 15 | { 16 | if (Test-Path $analyzersPath) 17 | { 18 | # Install the language agnostic analyzers. 19 | foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) 20 | { 21 | if($project.Object.AnalyzerReferences) 22 | { 23 | $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) 24 | } 25 | } 26 | } 27 | } 28 | 29 | # $project.Type gives the language name like (C# or VB.NET) 30 | $languageFolder = "" 31 | if($project.Type -eq "C#") 32 | { 33 | $languageFolder = "cs" 34 | } 35 | if($project.Type -eq "VB.NET") 36 | { 37 | $languageFolder = "vb" 38 | } 39 | if($languageFolder -eq "") 40 | { 41 | return 42 | } 43 | 44 | foreach($analyzersPath in $analyzersPaths) 45 | { 46 | # Install language specific analyzers. 47 | $languageAnalyzersPath = join-path $analyzersPath $languageFolder 48 | if (Test-Path $languageAnalyzersPath) 49 | { 50 | foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) 51 | { 52 | if($project.Object.AnalyzerReferences) 53 | { 54 | $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/StackExchange.Redis.Analyzer/StackExchange.Redis.Analyzer/tools/uninstall.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | if($project.Object.SupportsPackageDependencyResolution) 4 | { 5 | if($project.Object.SupportsPackageDependencyResolution()) 6 | { 7 | # Do not uninstall analyzers via uninstall.ps1, instead let the project system handle it. 8 | return 9 | } 10 | } 11 | 12 | $analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve 13 | 14 | foreach($analyzersPath in $analyzersPaths) 15 | { 16 | # Uninstall the language agnostic analyzers. 17 | if (Test-Path $analyzersPath) 18 | { 19 | foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) 20 | { 21 | if($project.Object.AnalyzerReferences) 22 | { 23 | $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) 24 | } 25 | } 26 | } 27 | } 28 | 29 | # $project.Type gives the language name like (C# or VB.NET) 30 | $languageFolder = "" 31 | if($project.Type -eq "C#") 32 | { 33 | $languageFolder = "cs" 34 | } 35 | if($project.Type -eq "VB.NET") 36 | { 37 | $languageFolder = "vb" 38 | } 39 | if($languageFolder -eq "") 40 | { 41 | return 42 | } 43 | 44 | foreach($analyzersPath in $analyzersPaths) 45 | { 46 | # Uninstall language specific analyzers. 47 | $languageAnalyzersPath = join-path $analyzersPath $languageFolder 48 | if (Test-Path $languageAnalyzersPath) 49 | { 50 | foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) 51 | { 52 | if($project.Object.AnalyzerReferences) 53 | { 54 | try 55 | { 56 | $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) 57 | } 58 | catch 59 | { 60 | 61 | } 62 | } 63 | } 64 | } 65 | } --------------------------------------------------------------------------------