├── .github ├── pull_request_template.md └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── CONTRIBUTING.md ├── Dena.CodeAnalysis.Testing.sln ├── LICENSE ├── NuGet.config ├── README.md ├── src └── Dena.CodeAnalysis.Testing │ ├── AnalyzerActions.cs │ ├── AnalyzerRunner.cs │ ├── Assert.cs │ ├── ComposableCatalogExtensions.cs │ ├── Dena.CodeAnalysis.Testing.csproj │ ├── DiagnosticAssert.cs │ ├── DiagnosticsFormatter.cs │ ├── ExampleCode.cs │ ├── ExportProviderExtensions.cs │ ├── LICENSE │ ├── LocationAssert.cs │ ├── NullAnalyzer.cs │ ├── ReadFromFile.cs │ ├── SpyAnalyzer.cs │ ├── StubAnalyzer.cs │ ├── StubDiagnosticDescriptor.cs │ └── TestDataParser.cs └── tests └── Dena.CodeAnalysis.Testing.Tests ├── AnalyzerRunnerTests.cs ├── Dena.CodeAnalysis.Testing.Tests.csproj ├── DiagnosticAssertTest.cs ├── DiagnosticsFormatterTests.cs ├── LocationAssertTest.cs ├── LocationFactory.cs ├── ReadFromFileTest.cs ├── SpyAnalyzerTests.cs ├── StubAnalyzerTests.cs ├── TestData ├── Example.txt ├── Instantiate.txt └── TestData.cs └── TestDataParserTest.cs /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | --- 4 | 5 | ### Contribution License Agreement 6 | 7 | - [ ] By placing an "x" in the box, I hereby understand, accept and agree to be bound by the terms and conditions of the [Contribution License Agreement](https://dena.github.io/cla/). 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Setup .NET 15 | uses: actions/setup-dotnet@main 16 | with: 17 | dotnet-version: '6.0.x' 18 | - name: dotnet test 19 | run: dotnet test ./Dena.CodeAnalysis.Testing.sln 20 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'src/Dena.CodeAnalysis.Testing/Dena.CodeAnalysis.Testing.csproj' 9 | 10 | jobs: 11 | check-bump-version: 12 | runs-on: ubuntu-latest 13 | outputs: 14 | new-version: ${{ steps.diff.outputs.version }} 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 200 19 | - name: Get version from csproj 20 | run: | 21 | version="$(grep -o --color=never "[0-9]\+\.[0-9]\+\.[0-9]\+" src/Dena.CodeAnalysis.Testing/Dena.CodeAnalysis.Testing.csproj | sed 's///')" 22 | echo "version=$version" >> "$GITHUB_OUTPUT" 23 | id: diff 24 | 25 | publish: 26 | needs: check-bump-version 27 | if: ${{ needs.check-bump-version.outputs.new-version }} 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@main 31 | - name: Setup .NET 32 | uses: actions/setup-dotnet@main 33 | with: 34 | dotnet-version: '6.0.x' 35 | 36 | - name: dotnet build 37 | run: dotnet build ./Dena.CodeAnalysis.Testing.sln --configuration Release 38 | 39 | - name: Create NuPkg 40 | run: dotnet pack ./src/Dena.CodeAnalysis.Testing --include-symbols --configuration Release -o ./nupkg 41 | 42 | - name: Publish NuPkg 43 | run: dotnet nuget push ./nupkg/Dena.CodeAnalysis.Testing.*.symbols.nupkg -s https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_TOKEN }} 44 | -------------------------------------------------------------------------------- /.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 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Coverlet is a free, cross platform Code Coverage Tool 141 | coverage*[.json, .xml, .info] 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | 147 | # NCrunch 148 | _NCrunch_* 149 | .*crunch*.local.xml 150 | nCrunchTemp_* 151 | 152 | # MightyMoose 153 | *.mm.* 154 | AutoTest.Net/ 155 | 156 | # Web workbench (sass) 157 | .sass-cache/ 158 | 159 | # Installshield output folder 160 | [Ee]xpress/ 161 | 162 | # DocProject is a documentation generator add-in 163 | DocProject/buildhelp/ 164 | DocProject/Help/*.HxT 165 | DocProject/Help/*.HxC 166 | DocProject/Help/*.hhc 167 | DocProject/Help/*.hhk 168 | DocProject/Help/*.hhp 169 | DocProject/Help/Html2 170 | DocProject/Help/html 171 | 172 | # Click-Once directory 173 | publish/ 174 | 175 | # Publish Web Output 176 | *.[Pp]ublish.xml 177 | *.azurePubxml 178 | # Note: Comment the next line if you want to checkin your web deploy settings, 179 | # but database connection strings (with potential passwords) will be unencrypted 180 | *.pubxml 181 | *.publishproj 182 | 183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 184 | # checkin your Azure Web App publish settings, but sensitive information contained 185 | # in these scripts will be unencrypted 186 | PublishScripts/ 187 | 188 | # NuGet Packages 189 | *.nupkg 190 | # NuGet Symbol Packages 191 | *.snupkg 192 | # The packages folder can be ignored because of Package Restore 193 | **/[Pp]ackages/* 194 | # except build/, which is used as an MSBuild target. 195 | !**/[Pp]ackages/build/ 196 | # Uncomment if necessary however generally it will be regenerated when needed 197 | #!**/[Pp]ackages/repositories.config 198 | # NuGet v3's project.json files produces more ignorable files 199 | *.nuget.props 200 | *.nuget.targets 201 | 202 | # Microsoft Azure Build Output 203 | csx/ 204 | *.build.csdef 205 | 206 | # Microsoft Azure Emulator 207 | ecf/ 208 | rcf/ 209 | 210 | # Windows Store app package directories and files 211 | AppPackages/ 212 | BundleArtifacts/ 213 | Package.StoreAssociation.xml 214 | _pkginfo.txt 215 | *.appx 216 | *.appxbundle 217 | *.appxupload 218 | 219 | # Visual Studio cache files 220 | # files ending in .cache can be ignored 221 | *.[Cc]ache 222 | # but keep track of directories ending in .cache 223 | !?*.[Cc]ache/ 224 | 225 | # Others 226 | ClientBin/ 227 | ~$* 228 | *~ 229 | *.dbmdl 230 | *.dbproj.schemaview 231 | *.jfm 232 | *.pfx 233 | *.publishsettings 234 | orleans.codegen.cs 235 | 236 | # Including strong name files can present a security risk 237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 238 | #*.snk 239 | 240 | # Since there are multiple workflows, uncomment next line to ignore bower_components 241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 242 | #bower_components/ 243 | 244 | # RIA/Silverlight projects 245 | Generated_Code/ 246 | 247 | # Backup & report files from converting an old project file 248 | # to a newer Visual Studio version. Backup files are not needed, 249 | # because we have git ;-) 250 | _UpgradeReport_Files/ 251 | Backup*/ 252 | UpgradeLog*.XML 253 | UpgradeLog*.htm 254 | ServiceFabricBackup/ 255 | *.rptproj.bak 256 | 257 | # SQL Server files 258 | *.mdf 259 | *.ldf 260 | *.ndf 261 | 262 | # Business Intelligence projects 263 | *.rdl.data 264 | *.bim.layout 265 | *.bim_*.settings 266 | *.rptproj.rsuser 267 | *- [Bb]ackup.rdl 268 | *- [Bb]ackup ([0-9]).rdl 269 | *- [Bb]ackup ([0-9][0-9]).rdl 270 | 271 | # Microsoft Fakes 272 | FakesAssemblies/ 273 | 274 | # GhostDoc plugin setting file 275 | *.GhostDoc.xml 276 | 277 | # Node.js Tools for Visual Studio 278 | .ntvs_analysis.dat 279 | node_modules/ 280 | 281 | # Visual Studio 6 build log 282 | *.plg 283 | 284 | # Visual Studio 6 workspace options file 285 | *.opt 286 | 287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 288 | *.vbw 289 | 290 | # Visual Studio LightSwitch build output 291 | **/*.HTMLClient/GeneratedArtifacts 292 | **/*.DesktopClient/GeneratedArtifacts 293 | **/*.DesktopClient/ModelManifest.xml 294 | **/*.Server/GeneratedArtifacts 295 | **/*.Server/ModelManifest.xml 296 | _Pvt_Extensions 297 | 298 | # Paket dependency manager 299 | .paket/paket.exe 300 | paket-files/ 301 | 302 | # FAKE - F# Make 303 | .fake/ 304 | 305 | # CodeRush personal settings 306 | .cr/personal 307 | 308 | # Python Tools for Visual Studio (PTVS) 309 | __pycache__/ 310 | *.pyc 311 | 312 | # Cake - Uncomment if you are using it 313 | # tools/** 314 | # !tools/packages.config 315 | 316 | # Tabs Studio 317 | *.tss 318 | 319 | # Telerik's JustMock configuration file 320 | *.jmconfig 321 | 322 | # BizTalk build output 323 | *.btp.cs 324 | *.btm.cs 325 | *.odx.cs 326 | *.xsd.cs 327 | 328 | # OpenCover UI analysis results 329 | OpenCover/ 330 | 331 | # Azure Stream Analytics local run output 332 | ASALocalRun/ 333 | 334 | # MSBuild Binary and Structured Log 335 | *.binlog 336 | 337 | # NVidia Nsight GPU debugger configuration file 338 | *.nvuser 339 | 340 | # MFractors (Xamarin productivity tool) working folder 341 | .mfractor/ 342 | 343 | # Local History for Visual Studio 344 | .localhistory/ 345 | 346 | # BeatPulse healthcheck temp database 347 | healthchecksdb 348 | 349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 350 | MigrationBackup/ 351 | 352 | # Ionide (cross platform F# VS Code tools) working folder 353 | .ionide/ 354 | 355 | ## 356 | ## Visual studio for Mac 357 | ## 358 | 359 | 360 | # globs 361 | Makefile.in 362 | *.userprefs 363 | *.usertasks 364 | config.make 365 | config.status 366 | aclocal.m4 367 | install-sh 368 | autom4te.cache/ 369 | *.tar.gz 370 | tarballs/ 371 | test-results/ 372 | 373 | # Mac bundle stuff 374 | *.dmg 375 | *.app 376 | 377 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 378 | # General 379 | .DS_Store 380 | .AppleDouble 381 | .LSOverride 382 | 383 | # Icon must end with two \r 384 | Icon 385 | 386 | 387 | # Thumbnails 388 | ._* 389 | 390 | # Files that might appear in the root of a volume 391 | .DocumentRevisions-V100 392 | .fseventsd 393 | .Spotlight-V100 394 | .TemporaryItems 395 | .Trashes 396 | .VolumeIcon.icns 397 | .com.apple.timemachine.donotpresent 398 | 399 | # Directories potentially created on remote AFP share 400 | .AppleDB 401 | .AppleDesktop 402 | Network Trash Folder 403 | Temporary Items 404 | .apdisk 405 | 406 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 407 | # Windows thumbnail cache files 408 | Thumbs.db 409 | ehthumbs.db 410 | ehthumbs_vista.db 411 | 412 | # Dump file 413 | *.stackdump 414 | 415 | # Folder config file 416 | [Dd]esktop.ini 417 | 418 | # Recycle Bin used on file shares 419 | $RECYCLE.BIN/ 420 | 421 | # Windows Installer files 422 | *.cab 423 | *.msi 424 | *.msix 425 | *.msm 426 | *.msp 427 | 428 | # Windows shortcuts 429 | *.lnk 430 | 431 | # JetBrains Rider 432 | .idea/ 433 | *.sln.iml 434 | 435 | ## 436 | ## Visual Studio Code 437 | ## 438 | .vscode/* 439 | !.vscode/settings.json 440 | !.vscode/tasks.json 441 | !.vscode/launch.json 442 | !.vscode/extensions.json 443 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contribution Guide 2 | ================== 3 | 4 | Publish 5 | ------- 6 | 7 | ```console 8 | $ # Update a PackageVersion in csproj. 9 | $ edit ./src/Dena.CodeAnalysis.Testing/Dena.CodeAnalysis.Testing.csproj 10 | 11 | $ git add ./src/Dena.CodeAnalysis.Testing/Dena.CodeAnalysis.Testing.csproj 12 | $ git commit -m "Ready to be vx.x.x" 13 | $ git push 14 | 15 | $ ./publish 16 | ``` 17 | -------------------------------------------------------------------------------- /Dena.CodeAnalysis.Testing.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dena.CodeAnalysis.Testing", "src\Dena.CodeAnalysis.Testing\Dena.CodeAnalysis.Testing.csproj", "{5C4F5607-880B-4D33-9182-2806F7D25E12}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dena.CodeAnalysis.Testing.Tests", "tests\Dena.CodeAnalysis.Testing.Tests\Dena.CodeAnalysis.Testing.Tests.csproj", "{B50FC929-F9BF-4DBC-B7EB-A29396801725}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Debug|x64 = Debug|x64 14 | Debug|x86 = Debug|x86 15 | Release|Any CPU = Release|Any CPU 16 | Release|x64 = Release|x64 17 | Release|x86 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {5C4F5607-880B-4D33-9182-2806F7D25E12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {5C4F5607-880B-4D33-9182-2806F7D25E12}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {5C4F5607-880B-4D33-9182-2806F7D25E12}.Debug|x64.ActiveCfg = Debug|Any CPU 26 | {5C4F5607-880B-4D33-9182-2806F7D25E12}.Debug|x64.Build.0 = Debug|Any CPU 27 | {5C4F5607-880B-4D33-9182-2806F7D25E12}.Debug|x86.ActiveCfg = Debug|Any CPU 28 | {5C4F5607-880B-4D33-9182-2806F7D25E12}.Debug|x86.Build.0 = Debug|Any CPU 29 | {5C4F5607-880B-4D33-9182-2806F7D25E12}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {5C4F5607-880B-4D33-9182-2806F7D25E12}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {5C4F5607-880B-4D33-9182-2806F7D25E12}.Release|x64.ActiveCfg = Release|Any CPU 32 | {5C4F5607-880B-4D33-9182-2806F7D25E12}.Release|x64.Build.0 = Release|Any CPU 33 | {5C4F5607-880B-4D33-9182-2806F7D25E12}.Release|x86.ActiveCfg = Release|Any CPU 34 | {5C4F5607-880B-4D33-9182-2806F7D25E12}.Release|x86.Build.0 = Release|Any CPU 35 | {B50FC929-F9BF-4DBC-B7EB-A29396801725}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {B50FC929-F9BF-4DBC-B7EB-A29396801725}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {B50FC929-F9BF-4DBC-B7EB-A29396801725}.Debug|x64.ActiveCfg = Debug|Any CPU 38 | {B50FC929-F9BF-4DBC-B7EB-A29396801725}.Debug|x64.Build.0 = Debug|Any CPU 39 | {B50FC929-F9BF-4DBC-B7EB-A29396801725}.Debug|x86.ActiveCfg = Debug|Any CPU 40 | {B50FC929-F9BF-4DBC-B7EB-A29396801725}.Debug|x86.Build.0 = Debug|Any CPU 41 | {B50FC929-F9BF-4DBC-B7EB-A29396801725}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {B50FC929-F9BF-4DBC-B7EB-A29396801725}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {B50FC929-F9BF-4DBC-B7EB-A29396801725}.Release|x64.ActiveCfg = Release|Any CPU 44 | {B50FC929-F9BF-4DBC-B7EB-A29396801725}.Release|x64.Build.0 = Release|Any CPU 45 | {B50FC929-F9BF-4DBC-B7EB-A29396801725}.Release|x86.ActiveCfg = Release|Any CPU 46 | {B50FC929-F9BF-4DBC-B7EB-A29396801725}.Release|x86.Build.0 = Release|Any CPU 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ./src/Dena.CodeAnalysis.Testing/LICENSE -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Dena.CodeAnalysis.Testing 2 | ========================= 3 | [![NuGet version](https://badge.fury.io/nu/Dena.CodeAnalysis.Testing.svg)](https://www.nuget.org/packages/Dena.CodeAnalysis.Testing/) 4 | [![CI](https://github.com/DeNA/Dena.CodeAnalysis.Testing/actions/workflows/ci.yml/badge.svg?branch=master&event=push)](https://github.com/DeNA/Dena.CodeAnalysis.Testing/actions/workflows/ci.yml) 5 | 6 | 7 | This library provides TDD friendly DiagnosticAnalyzer test helpers: 8 | 9 | * DiagnosticAnalyzerRunner 10 | 11 | A runner for [`Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.diagnostics.diagnosticanalyzer?view=roslyn-dotnet). 12 | The purpose of the runner is providing another runner instead of [`Microsoft.CodeAnalysis.Analyzer.Testing.AnalyzerVerifier.VerifyAnalyzerAsync`](https://github.com/dotnet/roslyn-sdk/blob/3046d1dffafd47ced55e4b76fd865179154c87ab/src/Microsoft.CodeAnalysis.Testing/Microsoft.CodeAnalysis.Analyzer.Testing/AnalyzerVerifier%603.cs#L13-L19). 13 | 14 | Because of the `AnalyzerVerifier` has several problems: 15 | 16 | 1. Using AnalyzerVerifier, it is hard to instantiate analyzer with custom arguments (the custom args may be needed if your analyzer is composed by several smaller analyzer-like components) 17 | 2. AnalyzerVerifier may throw some exceptions because it test Diagnostics. But it should be optional because analyzer-like smaller components may not need it. If it is not optional the tests for the components become to need to wrap try-catch statements for each call of `VerifyAnalyzerAsync` 18 | 19 | * Test Doubles for DiagnosticAnalyzer 20 | * NullAnalyzer: it do nothing 21 | * StubAnalyzer: it analyze codes with a `Dena.CodeAnalysis.Testing.AnalyzerActions` 22 | * SpyAnalyzer: it analyze codes and do not report any Diagnostics, but instead it records all actions that registered via [`Microsoft.CodeAnalysis.Dignostics.AnalysisContext`](https://docs.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.diagnostics.analysiscontext?view=roslyn-dotnet) 23 | 24 | 25 | Requirements 26 | ------------ 27 | 28 | * .NET Standard 2.1 or later 29 | 30 | 31 | 32 | Usage 33 | ----- 34 | 35 | ### Run DiagnosticAnalyzer 36 | 37 | ```c# 38 | var analyzer = new YourAnalyzer(); 39 | 40 | // The analyzer get intialized and get to call registered actions. 41 | await DiagnosticAnalyzerRunner.Run( 42 | analyzer, 43 | @"public static class Foo 44 | { 45 | public static void Bar() 46 | { 47 | System.Console.WriteLine(""Hello, World!""); 48 | } 49 | }"); 50 | ``` 51 | 52 | 53 | 54 | ### Get Diagnostics 55 | 56 | ```c# 57 | var analyzer = new YourAnalyzer(); 58 | 59 | var diagnostics = await DiagnosticAnalyzerRunner.Run( 60 | analyzer, 61 | @"public static class Foo 62 | { 63 | public static void Bar() 64 | { 65 | System.Console.WriteLine(""Hello, World!""); 66 | } 67 | }"); 68 | 69 | Assert.AreEqual(0, diagnostics.Length); 70 | ``` 71 | 72 | 73 | 74 | ### Assert Locations 75 | ```c# 76 | var location = diagnostic.Location; 77 | 78 | LocationAssert.HaveTheSpan( 79 | "/0/Test0.", // Optional. Skip path assertion if the path not specified, 80 | new LinePosition(1, 0), 81 | new LinePosition(8, 5), 82 | location 83 | ); 84 | ``` 85 | 86 | 87 | 88 | ### Print Diagnostics 89 | ```c# 90 | var diagnostics = await DiagnosticAnalyzerRunner.Run( 91 | anyAnalyzer, 92 | @" 93 | internal static class Foo 94 | { 95 | internal static void Bar() 96 | { 97 | System.Console.WriteLine(""Hello, World!""); 98 | } 99 | } 100 | ERROR"); 101 | 102 | Assert.AreEqual(0, diagnostics.Length, DiagnosticsFormatter.Format(diagnostics)); 103 | // This message is like: 104 | // 105 | // // /0/Test0.cs(9,1): error CS0116: A namespace cannot directly contain members such as fields or methods 106 | // DiagnosticResult.CompilerError(""CS0116"").WithSpan(""/0/Test0.cs"", 9, 1, 9, 6), 107 | ``` 108 | 109 | 110 | 111 | ### Check whether the DiagnosticAnalyzer.Initialize have been called 112 | 113 | ```c# 114 | var spyAnalyzer = new SpyAnalyzer(); 115 | 116 | var diagnostics = await DiagnosticAnalyzerRunner.Run( 117 | spyAnalyzer, 118 | @"public static class Foo 119 | { 120 | public static void Bar() 121 | { 122 | System.Console.WriteLine(""Hello, World!""); 123 | } 124 | }"); 125 | 126 | Assert.IsTrue(spyAnalyzer.IsInitialized); 127 | ``` 128 | 129 | 130 | 131 | ### Check recorded actions 132 | 133 | ```c# 134 | var spyAnalyzer = new SpyAnalyzer(); 135 | 136 | var diagnostics = await DiagnosticAnalyzerRunner.Run( 137 | spyAnalyzer, 138 | @"public static class Foo 139 | { 140 | public static void Bar() 141 | { 142 | System.Console.WriteLine(""Hello, World!""); 143 | } 144 | }"); 145 | 146 | // CompilationActionHistory hold the Compilation object that given 147 | // to the action registered by AnalysisContext.RegisterCompilationAction. 148 | Assert.AreEqual(1, spyAnalyzer.CompilationActionHistory.Count); 149 | 150 | // Other available histories are: 151 | // 152 | // - spyAnalyzer.CodeBlockActionHistory 153 | // - spyAnalyzer.CodeBlockStartActionHistory 154 | // - spyAnalyzer.CompilationActionHistory 155 | // - spyAnalyzer.CompilationStartActionHistory 156 | // - spyAnalyzer.OperationActionHistory 157 | // - spyAnalyzer.OperationBlockActionHistory 158 | // - spyAnalyzer.OperationBlockStartAction 159 | // - spyAnalyzer.OperationBlockStartActionHistory 160 | // - spyAnalyzer.SemanticModelActionHistory 161 | // - spyAnalyzer.SymbolActionHistory 162 | // - spyAnalyzer.SymbolStartActionHistory 163 | // - spyAnalyzer.SyntaxNodeActionHistory 164 | // - spyAnalyzer.SyntaxTreeActionHistory 165 | ``` 166 | 167 | 168 | 169 | ### Do something in action 170 | 171 | ```c# 172 | var stubAnalyzer = new StubAnalyzer( 173 | new AnalyzerActions 174 | { 175 | CodeBlockStartAction = context => DoSomething() 176 | } 177 | ); 178 | 179 | await DiagnosticAnalyzerRunner.Run( 180 | stubAnalyzer, 181 | @"public static class Foo 182 | { 183 | public static void Bar() 184 | { 185 | System.Console.WriteLine(""Hello, World!""); 186 | } 187 | }"); 188 | 189 | // Other available actions are: 190 | // 191 | // - stubAnalyzer.CodeBlockAction 192 | // - stubAnalyzer.CodeBlockStartAction 193 | // - stubAnalyzer.CompilationAction 194 | // - stubAnalyzer.CompilationStartAction 195 | // - stubAnalyzer.OperationAction 196 | // - stubAnalyzer.OperationBlockAction 197 | // - stubAnalyzer.OperationBlockStartAction 198 | // - stubAnalyzer.OperationBlockStartAction 199 | // - stubAnalyzer.SemanticModelAction 200 | // - stubAnalyzer.SymbolAction 201 | // - stubAnalyzer.SymbolStartAction 202 | // - stubAnalyzer.SyntaxNodeAction 203 | // - stubAnalyzer.SyntaxTreeAction 204 | ``` 205 | 206 | ### DiagnosticAssert Class 207 | 208 | #### AreEqual 209 | 210 | `DiagnosticAssert.AreEqual` assert that collections of Diagnostics for equality. 211 | 212 | Throw an assert exception if given collections satisfy the following condition: 213 | 214 | Elements that are only contained on one side. The equivalence is based on following properties 215 | 216 | - File path (e.g., path/to/file.cs) 217 | - Location of the `Diagnostic` (starting line number, starting character position)-(finishing line number, finishing 218 | character position) 219 | - Identifier of the `DiagnosticDescriptor` (DDID) (e.g., CS0494) 220 | - `DiagnosticMessage` (e.g., The field 'C.hoge' is assigned but its value is never used) 221 | 222 | Otherwise, do nothing. 223 | 224 | ```csharp 225 | [Test] 226 | public async Task M() 227 | { 228 | var analyzer = new YourAnalyzer(); 229 | const string testData = @" 230 | class C 231 | { 232 | string {|hoge|CS0414|The field 'C.hoge' is assigned but its value is never used|} = ""Forgot semicolon string"" 233 | }"; 234 | 235 | var (source, expected) = TestDataParser.CreateSourceAndExpectedDiagnosticFromFile(testData); 236 | var actual = await DiagnosticAnalyzerRunner.Run(analyzer, source); 237 | DiagnosticsAssert.AreEqual(expected, actual); 238 | } 239 | ``` 240 | 241 | Output example 242 | 243 | ``` 244 | Missing 0 diagnostics, extra 1 diagnostics of all 2 diagnostics: 245 | extra /0/Test0.cs: (3,43)-(3,43), CS1002, ; expected 246 | ``` 247 | 248 | #### IsEmpty 249 | 250 | `DiagnosticAssert.IsEmpty` assert that the `Diagnositc` is no exist. 251 | 252 | Throw an assert exception if given collections exist any `Diagnostic`. 253 | 254 | The output format and equivalence is the same as `DiagnosticAssert.AreEqual`. 255 | 256 | Otherwise, do nothing. 257 | 258 | ```csharp 259 | [Test] 260 | public async Task M() 261 | { 262 | var analyzer = new YourAnalyzer(); 263 | var source = @" 264 | class C 265 | { 266 | }"; 267 | var actual = await DiagnosticAnalyzerRunner.Run(analyzer, source); 268 | DiagnosticsAssert.IsEmpty(actual); 269 | } 270 | ``` 271 | 272 | ### TestDataParser Class 273 | 274 | #### CreateSourceAndExpectedDiagnostics 275 | 276 | Create source and expected diagnostic from formatting embedded. 277 | 278 | ```csharp 279 | [Test] 280 | public async Task M() 281 | { 282 | var analyzer = new YourAnalyzer(); 283 | const string testData = @" 284 | class C 285 | { 286 | string {|hoge|CS0414|The field 'C.hoge' is assigned but its value is never used|} = ""Forgot semicolon string"" 287 | }"; 288 | 289 | var (source, expected) = TestDataParser.CreateSourceAndExpectedDiagnosticFromFile(testData); 290 | } 291 | ``` 292 | 293 | The `testData` variable has formatting embedded in the source. 294 | 295 | You can parse this format using `CreateSourceAndExpectedDiagnosticFromFile` and get the source and expected Diagnostics. 296 | 297 | - source 298 | 299 | ```csharp 300 | class C 301 | { 302 | string hoge = "Forgot semicolon string" 303 | } 304 | ``` 305 | 306 | - expected Diagnostics 307 | 308 | - Location 309 | - (3,11)-(3,15) 310 | - DDID 311 | - CS0414 312 | - DiagnosticMessage 313 | - The field 'C.hoge' is assigned but its value is never used 314 | 315 | Specify the part to be reported in the following format. 316 | 317 | The format is enclosed in `{ }` and separated by `|`. 318 | 319 | ``` 320 | {|source|DDID|DiagnosticMessage|} 321 | ``` 322 | 323 | 324 | 325 | License 326 | ------- 327 | 328 | [MIT license](./LICENSE) 329 | -------------------------------------------------------------------------------- /src/Dena.CodeAnalysis.Testing/AnalyzerActions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp; 5 | using Microsoft.CodeAnalysis.Diagnostics; 6 | 7 | 8 | 9 | namespace Dena.CodeAnalysis.CSharp.Testing 10 | { 11 | /// 12 | /// Actions to register to . 13 | /// 14 | public sealed class AnalyzerActions 15 | { 16 | /// 17 | /// The action for 18 | /// 19 | public Action CodeBlockAction = _ => { }; 20 | 21 | /// 22 | /// The action for 23 | /// 24 | public Action> CodeBlockStartAction = _ => { }; 25 | 26 | /// 27 | /// The action for 28 | /// 29 | public Action CompilationAction = _ => { }; 30 | 31 | /// 32 | /// The action for 33 | /// 34 | public Action CompilationStartAction = _ => { }; 35 | 36 | /// 37 | /// The action for 38 | /// 39 | public Action OperationAction = _ => { }; 40 | 41 | /// 42 | /// The action for 43 | /// 44 | public Action OperationBlockAction = _ => { }; 45 | 46 | /// 47 | /// The action for 48 | /// 49 | public Action OperationBlockStartAction = _ => { }; 50 | 51 | /// 52 | /// The action for 53 | /// 54 | public Action SemanticModelAction = _ => { }; 55 | 56 | /// 57 | /// The action for 58 | /// 59 | public Action SymbolAction = _ => { }; 60 | 61 | /// 62 | /// The action for 63 | /// 64 | public Action SymbolStartAction = _ => { }; 65 | 66 | /// 67 | /// The action for 68 | /// 69 | public Action SyntaxNodeAction = _ => { }; 70 | 71 | /// 72 | /// The action for 73 | /// 74 | public Action SyntaxTreeAction = _ => { }; 75 | 76 | 77 | /// 78 | /// Compose two actions. 79 | /// 80 | /// The first action will be executed before . 81 | /// The second action will be executed after . 82 | /// The type of the argument of and . 83 | /// Composed actions. 84 | [SuppressMessage("ReSharper", "MemberCanBePrivate.Global", Justification = "This is an exposed API")] 85 | public static Action ComposeAction(Action a, Action b) => 86 | x => 87 | { 88 | a(x); 89 | b(x); 90 | }; 91 | 92 | 93 | /// 94 | /// Compose two . 95 | /// 96 | /// The first actions will be executed before . 97 | /// The second actions will be executed after . 98 | /// Composed actions. 99 | [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "This is an exposed API")] 100 | public static AnalyzerActions Compose(AnalyzerActions a, AnalyzerActions b) => 101 | new AnalyzerActions 102 | { 103 | CodeBlockAction = ComposeAction(a.CodeBlockAction, b.CodeBlockAction), 104 | CodeBlockStartAction = ComposeAction(a.CodeBlockStartAction, b.CodeBlockStartAction), 105 | CompilationAction = ComposeAction(a.CompilationAction, b.CompilationAction), 106 | CompilationStartAction = ComposeAction(a.CompilationStartAction, b.CompilationStartAction), 107 | OperationAction = ComposeAction(a.OperationAction, b.OperationAction), 108 | OperationBlockAction = ComposeAction(a.OperationBlockAction, b.OperationBlockAction), 109 | OperationBlockStartAction = ComposeAction(a.OperationBlockStartAction, b.OperationBlockStartAction), 110 | SemanticModelAction = ComposeAction(a.SemanticModelAction, b.SemanticModelAction), 111 | SymbolAction = ComposeAction(a.SymbolAction, b.SymbolAction), 112 | SymbolStartAction = ComposeAction(a.SymbolStartAction, b.SymbolStartAction), 113 | SyntaxNodeAction = ComposeAction(a.SyntaxNodeAction, b.SyntaxNodeAction), 114 | SyntaxTreeAction = ComposeAction(a.SyntaxTreeAction, b.SyntaxTreeAction) 115 | }; 116 | 117 | 118 | /// 119 | /// Register all actions to the specified . 120 | /// 121 | /// The context that the actions register to. 122 | public void RegisterAllTo(AnalysisContext context) 123 | { 124 | context.RegisterCodeBlockAction(CodeBlockAction); 125 | context.RegisterCodeBlockStartAction(CodeBlockStartAction); 126 | context.RegisterCompilationAction(CompilationAction); 127 | context.RegisterCompilationStartAction(CompilationStartAction); 128 | context.RegisterOperationAction( 129 | OperationAction, 130 | (OperationKind[]) Enum.GetValues(typeof(OperationKind)) 131 | ); 132 | context.RegisterOperationBlockAction(OperationBlockAction); 133 | context.RegisterOperationBlockAction(OperationBlockAction); 134 | context.RegisterOperationBlockStartAction(OperationBlockStartAction); 135 | context.RegisterSemanticModelAction(SemanticModelAction); 136 | context.RegisterSymbolAction(SymbolAction, (SymbolKind[]) Enum.GetValues(typeof(SymbolKind))); 137 | 138 | foreach (SymbolKind symbolKind in Enum.GetValues(typeof(SymbolKind))) 139 | context.RegisterSymbolStartAction(SymbolStartAction, symbolKind); 140 | 141 | context.RegisterSyntaxNodeAction(SyntaxNodeAction, (SyntaxKind[]) Enum.GetValues(typeof(SyntaxKind))); 142 | context.RegisterSyntaxTreeAction(SyntaxTreeAction); 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /src/Dena.CodeAnalysis.Testing/AnalyzerRunner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.Diagnostics; 6 | using System.Collections.Immutable; 7 | using System.Diagnostics.CodeAnalysis; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Microsoft.CodeAnalysis.CSharp; 11 | using Microsoft.CodeAnalysis.Host.Mef; 12 | using Microsoft.CodeAnalysis.Testing; 13 | using Microsoft.VisualStudio.Composition; 14 | 15 | 16 | namespace Dena.CodeAnalysis.CSharp.Testing 17 | { 18 | /// 19 | /// A runner for . 20 | /// The purpose of the runner is providing another helpers instead of AnalyzerVerifier{T1, T2, T3}.VerifyAnalyzerAsync. 21 | /// The AnalyzerVerifier has several problems: 22 | /// 23 | /// 1. Using AnalyzerVerifier, it is hard to instantiate analyzer with custom arguments (it will be needed 24 | /// if your analyzer is composed by several small analyzer-like components.) 25 | /// 2. AnalyzerVerifier do diagnostics assertion, but it should be optional because analyzer-like small components 26 | /// may not need it. 27 | /// 28 | public static class DiagnosticAnalyzerRunner 29 | { 30 | /// 31 | /// Run the specified . 32 | /// 33 | /// The to run. 34 | /// The type of metadata you want to add. 35 | /// The target code that the analyze. 36 | /// ImmutableArray contains all reported . 37 | /// Throws if are empty. 38 | public static async Task> Run( 39 | DiagnosticAnalyzer analyzer, 40 | Type[] types = default, 41 | params string[] codes 42 | ) => 43 | await Run( 44 | analyzer, 45 | CancellationToken.None, 46 | ParseOptionsForLanguageVersionsDefault(), 47 | CompilationOptionsForDynamicClassLibrary(), 48 | MetadataReferencesDefault(types), 49 | "", 50 | codes 51 | ); 52 | 53 | /// 54 | /// Run the specified . 55 | /// 56 | /// The to run. 57 | /// The that the task will observe. 58 | /// The . 59 | /// The . Use if you want to analyze codes including Main() (default: ). 60 | /// The s to add to the compilation. 61 | /// The file path of the source code. This is used to set the property. 62 | /// The target code that the analyze. Use if you want to specify C# version (default: )." 63 | /// ImmutableArray contains all reported . 64 | /// Throws if are empty. 65 | [SuppressMessage("ReSharper", "MemberCanBePrivate.Global", Justification = "This is an exposed API")] 66 | public static async Task> Run( 67 | DiagnosticAnalyzer analyzer, 68 | CancellationToken cancellationToken, 69 | ParseOptions parseOptions, 70 | CompilationOptions compilationOptions, 71 | IEnumerable metadataReferences, 72 | string filePath = "", 73 | params string[] codes 74 | ) 75 | { 76 | if (!codes.Any()) throw new AtLeastOneCodeMustBeRequired(); 77 | 78 | // XXX: We can use either AdhocWorkspace (used in Microsoft.CodeAnalysis.Testing.AnalyzerTest) or 79 | // MSBuildWorkspace (used in a project template of standalone analyzers[^1]) as a workspace where 80 | // the analyzers run on. We chosen AdhocWorkspace instead of MSBuildWorkspace. 81 | // Because MSBuildWorkspace was provided for only .NET Framework or .NET Core or .NET 5, but not .NET Standard. 82 | // So it causes "CS0234: The type or namespace name 'Build' does not exist in the namespace 'Microsoft'" 83 | // if we set netstandard2.1 as the target framework of Dena.CodeAnalysis.Testing.csproj. 84 | // [^1]: https://github.com/dotnet/roslyn-sdk/blob/90e6dc7fb6307bf1bbf4acf91353fd9db22ac1ca/src/VisualStudio.Roslyn.SDK/Roslyn.SDK/ProjectTemplates/CSharp/ConsoleApplication/ConsoleApplication.csproj#L9 85 | using var workspace = CreateWorkspace(); 86 | var projectId = ProjectId.CreateNewId(); 87 | var solution = workspace 88 | .CurrentSolution 89 | .AddProject(projectId, DefaultTestProjectName, DefaultAssemblyName, Language); 90 | 91 | foreach (var code in codes) 92 | { 93 | var documentId = DocumentId.CreateNewId(projectId); 94 | var path = filePath.Length > 0 ? filePath : DefaultFilePath; 95 | solution = solution.AddDocument(documentId, path, code, filePath: path); 96 | } 97 | 98 | var noMetadataReferencedProject = solution.Projects.First(); 99 | 100 | var project = noMetadataReferencedProject 101 | .AddMetadataReferences(metadataReferences) 102 | .WithParseOptions(parseOptions) 103 | .WithCompilationOptions(compilationOptions); 104 | 105 | var compilation = await project.GetCompilationAsync(cancellationToken); 106 | 107 | var withAnalyzers = compilation!.WithAnalyzers(ImmutableArray.Create(analyzer)); 108 | return await withAnalyzers.GetAllDiagnosticsAsync(cancellationToken); 109 | } 110 | 111 | 112 | /// 113 | /// This value is equivalent to Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerTest{DiagnosticAnalyzer, IVerifier}.CreateCompilationOptions. 114 | /// 115 | [SuppressMessage("ReSharper", "MemberCanBePrivate.Global", Justification = "This is an exposed API.")] 116 | public static CompilationOptions CompilationOptionsForDynamicClassLibrary() => 117 | new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true); 118 | 119 | 120 | /// 121 | /// This value is equivalent to 122 | /// 123 | [SuppressMessage("ReSharper", "MemberCanBePrivate.Global", Justification = "This is an exposed API.")] 124 | public static ParseOptions ParseOptionsForLanguageVersionsDefault() => 125 | new CSharpParseOptions(DefaultLanguageVersion, DocumentationMode.Diagnose); 126 | 127 | private static IEnumerable MetadataReferencesDefault(Type[] types) 128 | { 129 | var metadataReferences = ReferenceAssemblies.Default.ResolveAsync(Language, CancellationToken.None).Result 130 | .ToList(); 131 | if (types != null) 132 | { 133 | foreach (var type in types) 134 | { 135 | metadataReferences.Add(MetadataReference.CreateFromFile(type.Assembly.Location)); 136 | } 137 | } 138 | 139 | return metadataReferences; 140 | } 141 | 142 | 143 | /// 144 | /// This value is equivalent to 145 | /// 146 | private const string DefaultFilePathPrefix = "/0/Test"; 147 | 148 | /// 149 | /// This value is equivalent to 150 | /// 151 | private const string DefaultTestProjectName = "TestProject"; 152 | 153 | /// 154 | /// This value is equivalent to 155 | /// 156 | private static readonly string DefaultFilePath = $"{DefaultFilePathPrefix}{0}.{DefaultFileExt}"; 157 | 158 | /// 159 | /// This value is equivalent to 160 | /// 161 | private const string DefaultFileExt = "cs"; 162 | 163 | /// 164 | /// Gets the default assembly name. 165 | /// 166 | private static readonly string DefaultAssemblyName = $"{DefaultTestProjectName}.dll"; 167 | 168 | /// 169 | /// This value is equivalent to 170 | /// 171 | private const string Language = LanguageNames.CSharp; 172 | 173 | 174 | /// 175 | /// This value is equivalent to 176 | /// 177 | private static readonly LanguageVersion DefaultLanguageVersion = 178 | Enum.TryParse("Default", out LanguageVersion version) ? version : LanguageVersion.CSharp6; 179 | 180 | private static readonly Lazy ExportProviderFactory; 181 | 182 | 183 | /// 184 | private static AdhocWorkspace CreateWorkspace() 185 | { 186 | var exportProvider = ExportProviderFactory.Value.CreateExportProvider(); 187 | var host = MefHostServices.Create(exportProvider.AsCompositionContext()); 188 | return new AdhocWorkspace(host); 189 | } 190 | 191 | 192 | /// > 193 | static DiagnosticAnalyzerRunner() 194 | { 195 | ExportProviderFactory = new Lazy( 196 | () => 197 | { 198 | var discovery = new AttributedPartDiscovery(Resolver.DefaultInstance, true); 199 | var parts = Task.Run(() => discovery.CreatePartsAsync(MefHostServices.DefaultAssemblies)) 200 | .GetAwaiter().GetResult(); 201 | var catalog = ComposableCatalog.Create(Resolver.DefaultInstance).AddParts(parts) 202 | .WithDocumentTextDifferencingService(); 203 | 204 | var configuration = CompositionConfiguration.Create(catalog); 205 | var runtimeComposition = RuntimeComposition.CreateRuntimeComposition(configuration); 206 | return runtimeComposition.CreateExportProviderFactory(); 207 | }, 208 | LazyThreadSafetyMode.ExecutionAndPublication 209 | ); 210 | } 211 | 212 | 213 | /// 214 | /// None of codes specified but at least one code must be required. 215 | /// 216 | public class AtLeastOneCodeMustBeRequired : Exception 217 | { 218 | /// 219 | /// Creates a new exception that explains "None of codes specified but at least one code must be specified". 220 | /// 221 | public AtLeastOneCodeMustBeRequired() : base( 222 | "None of codes specified but at least one code must be specified" 223 | ) 224 | { 225 | } 226 | } 227 | } 228 | } -------------------------------------------------------------------------------- /src/Dena.CodeAnalysis.Testing/Assert.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dena.CodeAnalysis.CSharp.Testing 4 | { 5 | /// 6 | /// Framework-independent assertions. 7 | /// 8 | public static class Assert 9 | { 10 | /// 11 | /// Throw an exception. 12 | /// 13 | /// The message of the exception. 14 | /// 15 | public static void Fail(string message) 16 | { 17 | throw new AssertFailedException(message); 18 | } 19 | } 20 | 21 | 22 | /// 23 | /// An exception for assertions. 24 | /// 25 | public class AssertFailedException : Exception 26 | { 27 | public AssertFailedException(string message) : base(message) 28 | { 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/Dena.CodeAnalysis.Testing/ComposableCatalogExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Reflection; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.Host; 6 | using Microsoft.CodeAnalysis.Host.Mef; 7 | using Microsoft.VisualStudio.Composition; 8 | using Microsoft.VisualStudio.Composition.Reflection; 9 | 10 | 11 | 12 | namespace Dena.CodeAnalysis.CSharp.Testing 13 | { 14 | // XXX: This is internal in Microsoft.CodeAnalysis.Testing, but it is needed by AnalyzerRunner. 15 | /// 16 | internal static class ComposableCatalogExtensions 17 | { 18 | /// 19 | public static ComposableCatalog WithDocumentTextDifferencingService(this ComposableCatalog catalog) 20 | { 21 | var assemblyQualifiedServiceTypeName = 22 | $"Microsoft.CodeAnalysis.IDocumentTextDifferencingService, {typeof(Workspace).GetTypeInfo().Assembly.GetName()}"; 23 | 24 | // Check to see if IDocumentTextDifferencingService is exported 25 | foreach (var part in catalog.Parts) 26 | foreach (var pair in part.ExportDefinitions) 27 | { 28 | var exportDefinition = pair.Value; 29 | if (exportDefinition.ContractName != "Microsoft.CodeAnalysis.Host.IWorkspaceService") continue; 30 | 31 | if (!exportDefinition.Metadata.TryGetValue("ServiceType", out var value) 32 | || !(value is string serviceType)) 33 | continue; 34 | 35 | if (serviceType != assemblyQualifiedServiceTypeName) continue; 36 | 37 | // The service is exported by default 38 | return catalog; 39 | } 40 | 41 | // If IDocumentTextDifferencingService is not exported by default, export it manually 42 | var manualExportDefinition = new ExportDefinition( 43 | typeof(IWorkspaceService).FullName, 44 | new Dictionary 45 | { 46 | {"ExportTypeIdentity", typeof(IWorkspaceService).FullName}, 47 | {nameof(ExportWorkspaceServiceAttribute.ServiceType), assemblyQualifiedServiceTypeName}, 48 | {nameof(ExportWorkspaceServiceAttribute.Layer), ServiceLayer.Default}, 49 | {typeof(CreationPolicy).FullName ?? string.Empty, CreationPolicy.Shared}, 50 | {"ContractType", typeof(IWorkspaceService)}, 51 | {"ContractName", null} 52 | } 53 | ); 54 | 55 | var serviceImplType = typeof(Workspace).GetTypeInfo().Assembly 56 | .GetType("Microsoft.CodeAnalysis.DefaultDocumentTextDifferencingService"); 57 | 58 | return catalog.AddPart( 59 | new ComposablePartDefinition( 60 | TypeRef.Get(serviceImplType, Resolver.DefaultInstance), 61 | new Dictionary {{"SharingBoundary", null}}, 62 | new[] {manualExportDefinition}, 63 | new Dictionary>(), 64 | Enumerable.Empty(), 65 | string.Empty, 66 | default, 67 | MethodRef.Get( 68 | serviceImplType.GetConstructors(BindingFlags.Instance | BindingFlags.Public).First(), 69 | Resolver.DefaultInstance 70 | ), 71 | new List(), 72 | CreationPolicy.Shared, 73 | new[] {typeof(Workspace).GetTypeInfo().Assembly.GetName()} 74 | ) 75 | ); 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /src/Dena.CodeAnalysis.Testing/Dena.CodeAnalysis.Testing.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Library 5 | netstandard2.1 6 | true 7 | true 8 | 9 | 10 | 11 | Dena.CodeAnalysis.Testing 12 | 3.0.6 13 | Kazuma Inagaki, Koji Hasegawa, Kuniwak 14 | false 15 | Test helpers for DiagnosticAnalyzers 16 | git 17 | https://github.com/DeNA/Dena.CodeAnalysis.Testing 18 | Copyright DeNA Co., Ltd. All rights reserved. 19 | DeNA Co., Ltd. 20 | analyzers 21 | true 22 | LICENSE 23 | 24 | 25 | 26 | 27 | all 28 | runtime; build; native; contentfiles; analyzers; buildtransitive 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/Dena.CodeAnalysis.Testing/DiagnosticAssert.cs: -------------------------------------------------------------------------------- 1 | // (c) 2021 DeNA Co., Ltd. 2 | 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using Microsoft.CodeAnalysis; 7 | 8 | namespace Dena.CodeAnalysis.CSharp.Testing 9 | { 10 | public static class DiagnosticsAssert 11 | { 12 | /// 13 | /// Return formatted string extract from Diagnostic. 14 | /// - File path (e.g., path/to/file.cs) 15 | /// - Location of the Diagnostic (starting line number, starting character position)-(finishing line number, finishing character position) 16 | /// - Identifier of the DiagnosticDescriptor (DDID) (e.g., CS1002) 17 | /// - DiagnosticMessage (e.g., ; expected) 18 | /// 19 | /// Diagnostic generated by the analyzer 20 | /// 21 | /// Example: 22 | /// path/to/file.cs: (start-line,start-col)-(end-line,end-col), DDID, DiagnosticMessage 23 | /// 24 | private static string FormatDiagnostic(Diagnostic diagnostic) 25 | { 26 | return $"{diagnostic.Location.GetLineSpan().Path}: " + 27 | $"{"(" + diagnostic.Location.GetLineSpan().Span.Start.Line + "," + diagnostic.Location.GetLineSpan().Span.Start.Character + ")-"}" + 28 | $"{"(" + diagnostic.Location.GetLineSpan().Span.End.Line + "," + diagnostic.Location.GetLineSpan().Span.End.Character + ")"}, " + 29 | $"{diagnostic.Id}, " + 30 | $"{diagnostic.GetMessage()}"; 31 | } 32 | 33 | /// 34 | /// DiagnosticAssert.AreEqual assert that collections of diagnostics for equality. 35 | /// Throw an AssertException if given collections satisfy the following condition: 36 | /// Elements that are only contained on one side. The equivalence is based on following properties 37 | /// - File path (e.g., path/to/file.cs) 38 | /// - Location of the Diagnostic (starting line number, starting character position)-(finishing line number, finishing character position) 39 | /// - Identifier of the DiagnosticDescriptor (DDID) (e.g., CS0494) 40 | /// - DiagnosticMessage (e.g., The field 'C.hoge' is assigned but its value is never used) 41 | /// Otherwise, do nothing. 42 | /// 43 | /// expected diagnostics 44 | /// actual diagnostics 45 | public static void AreEqual(IEnumerable expected, IEnumerable actual) 46 | { 47 | var actualDiagnostics = actual.Select(FormatDiagnostic).ToHashSet(); 48 | var expectDiagnostics = expected.Select(FormatDiagnostic).ToHashSet(); 49 | 50 | var extra = new HashSet(actualDiagnostics); 51 | extra.ExceptWith(expectDiagnostics); 52 | 53 | var missing = new HashSet(expectDiagnostics); 54 | missing.ExceptWith(actualDiagnostics); 55 | 56 | if (extra.Count == 0 && missing.Count == 0) return; 57 | Assert.Fail(CreateFailureMessageWhenAreNotEqual(missing, extra, actualDiagnostics)); 58 | } 59 | 60 | /// 61 | /// DiagnosticAssert.IsEmpty assert that the diagnostic is no exist. 62 | /// Throw an AssertException if given collections exist any Diagnostic. 63 | /// The output format and equivalence is the same as DiagnosticAssert.AreEqual. 64 | /// Otherwise, do nothing. 65 | /// 66 | /// DiagnosticReports returned by the analyzer 67 | public static void IsEmpty(IEnumerable diagnostics) 68 | { 69 | if (!diagnostics.Any()) 70 | { 71 | return; 72 | } 73 | 74 | Assert.Fail(CreateFailureMessageWhenIsNotEmpty(diagnostics)); 75 | } 76 | 77 | private static string CreateFailureMessageWhenIsNotEmpty(IEnumerable diagnostics) 78 | { 79 | var formatDiagnostics = diagnostics.Select(FormatDiagnostic).ToHashSet(); 80 | var builder = new StringBuilder(); 81 | builder.AppendLine( 82 | $"expected no diagnostics, but {formatDiagnostics.Count} diagnostics are reported" 83 | ); 84 | 85 | foreach (var formatDiagnostic in formatDiagnostics) 86 | { 87 | builder.AppendLine($"\textra\t{formatDiagnostic}"); 88 | } 89 | 90 | return builder.ToString(); 91 | } 92 | 93 | private static string CreateFailureMessageWhenAreNotEqual(HashSet missing, HashSet extra, 94 | HashSet allDiagnostics) 95 | { 96 | var builder = new StringBuilder(); 97 | builder.AppendLine( 98 | $"Missing {missing.Count} diagnostics, extra {extra.Count} diagnostics of all {allDiagnostics.Count} diagnostics:" 99 | ); 100 | 101 | foreach (var missingDiagnostic in missing) 102 | { 103 | builder.AppendLine($"\tmissing\t{missingDiagnostic}"); 104 | } 105 | 106 | foreach (var extraDiagnostic in extra) 107 | { 108 | builder.AppendLine($"\textra\t{extraDiagnostic}"); 109 | } 110 | 111 | return builder.ToString(); 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /src/Dena.CodeAnalysis.Testing/DiagnosticsFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text; 7 | using Microsoft.CodeAnalysis; 8 | 9 | 10 | 11 | namespace Dena.CodeAnalysis.CSharp.Testing 12 | { 13 | /// 14 | /// A formatter to help debugging diagnostics. 15 | /// 16 | public static class DiagnosticsFormatter 17 | { 18 | /// 19 | /// Helper method to format a into an easily readable string. 20 | /// 21 | /// A collection of s to be formatted. 22 | /// The formatted as a string. 23 | public static string Format(ImmutableArray diagnostics) => Format(diagnostics.ToArray()); 24 | 25 | 26 | /// 27 | /// Helper method to format a into an easily readable string. 28 | /// 29 | /// A collection of s to be formatted. 30 | /// The formatted as a string. 31 | public static string Format(params Diagnostic[] diagnostics) => Format("path/to/file.cs", diagnostics); 32 | 33 | 34 | /// 35 | /// See Microsoft.CodeAnalysis.Testing.AnalyzerTest{IVerifier}.FormatDiagnostics(ImmutableArray{DiagnosticAnalyzer}, string, Diagnostic[]). 36 | /// 37 | private static string Format( 38 | string defaultFilePath, 39 | params Diagnostic[] diagnostics 40 | ) 41 | { 42 | var builder = new StringBuilder(); 43 | foreach (var diagnostic in diagnostics) 44 | { 45 | var location = diagnostic.Location; 46 | 47 | builder.Append("// ").AppendLine(diagnostic.ToString()); 48 | 49 | var line = diagnostic.Severity switch 50 | { 51 | DiagnosticSeverity.Error => 52 | $"DiagnosticResult.CompilerError(\"{diagnostic.Id}\")", 53 | DiagnosticSeverity.Warning => 54 | $"DiagnosticResult.CompilerWarning(\"{diagnostic.Id}\")", 55 | _ => 56 | $"new DiagnosticResult(\"{diagnostic.Id}\", {nameof(DiagnosticSeverity)}.{diagnostic.Severity})" 57 | }; 58 | 59 | builder.Append(line); 60 | 61 | if (location == Location.None) 62 | { 63 | // No additional location data needed 64 | } 65 | else 66 | { 67 | AppendLocation(diagnostic.Location); 68 | foreach (var additionalLocation in diagnostic.AdditionalLocations) 69 | AppendLocation(additionalLocation); 70 | } 71 | 72 | var arguments = GetArguments(diagnostic); 73 | if (arguments.Count > 0) 74 | { 75 | builder.Append(".DiagnosticResult.WithArguments("); 76 | builder.Append(string.Join(", ", arguments.Select(a => $"\"{a}\""))); 77 | builder.Append(')'); 78 | } 79 | 80 | builder.AppendLine(","); 81 | } 82 | 83 | return builder.ToString(); 84 | 85 | // Local functions 86 | void AppendLocation(Location location) 87 | { 88 | var lineSpan = location.GetLineSpan(); 89 | var pathString = location.IsInSource && lineSpan.Path == defaultFilePath 90 | ? string.Empty 91 | : $"\"{lineSpan.Path}\", "; 92 | var linePosition = lineSpan.StartLinePosition; 93 | var endLinePosition = lineSpan.EndLinePosition; 94 | builder.Append( 95 | $".WithSpan({pathString}{linePosition.Line + 1}, {linePosition.Character + 1}, {endLinePosition.Line + 1}, {endLinePosition.Character + 1})" 96 | ); 97 | } 98 | } 99 | 100 | 101 | /// 102 | /// See Microsoft.CodeAnalysis.Testing.AnalyzerTest{IVerifier}.GetArguments(Diagnostic). 103 | /// 104 | private static IReadOnlyList GetArguments(Diagnostic diagnostic) => 105 | (IReadOnlyList) diagnostic.GetType().GetProperty( 106 | "Arguments", 107 | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance 108 | )?.GetValue(diagnostic) 109 | ?? Array.Empty(); 110 | } 111 | } -------------------------------------------------------------------------------- /src/Dena.CodeAnalysis.Testing/ExampleCode.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.Diagnostics; 2 | 3 | 4 | 5 | namespace Dena.CodeAnalysis.CSharp.Testing 6 | { 7 | /// 8 | /// Example codes for testing . 9 | /// 10 | public static class ExampleCode 11 | { 12 | /// 13 | /// An example code for class libraries that can be compiled successfully with no Diagnostics. 14 | /// 15 | public const string DiagnosticsFreeClassLibrary = @" 16 | internal static class Foo 17 | { 18 | internal static void Bar() 19 | { 20 | System.Console.WriteLine(""Hello, World!""); 21 | } 22 | } 23 | "; 24 | 25 | public const string UniTaskImport = @" 26 | using Cysharp.Threading.Tasks; 27 | internal static class Foo 28 | { 29 | internal static void Bar() 30 | { 31 | System.Console.WriteLine(""Hello, World!""); 32 | } 33 | }"; 34 | 35 | /// 36 | /// An example code that contains a syntax error. 37 | /// 38 | public const string ContainingSyntaxError = DiagnosticsFreeClassLibrary + "ERROR"; 39 | } 40 | } -------------------------------------------------------------------------------- /src/Dena.CodeAnalysis.Testing/ExportProviderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Composition; 4 | using System.Composition.Hosting.Core; 5 | using System.Linq; 6 | using System.Reflection; 7 | using Microsoft.VisualStudio.Composition; 8 | 9 | 10 | 11 | namespace Dena.CodeAnalysis.CSharp.Testing 12 | { 13 | // XXX: This is internal in Microsoft.CodeAnalysis.Testing, but it is needed by AnalyzerRunner. 14 | /// 15 | internal static class ExportProviderExtensions 16 | { 17 | /// 18 | public static CompositionContext AsCompositionContext(this ExportProvider exportProvider) => 19 | new CompositionContextShim(exportProvider); 20 | 21 | 22 | 23 | /// 24 | private class CompositionContextShim : CompositionContext 25 | { 26 | private readonly ExportProvider _exportProvider; 27 | 28 | 29 | public CompositionContextShim(ExportProvider exportProvider) => _exportProvider = exportProvider; 30 | 31 | 32 | public override bool TryGetExport(CompositionContract contract, out object export) 33 | { 34 | var importMany = 35 | contract.MetadataConstraints.Contains(new KeyValuePair("IsImportMany", true)); 36 | var (contractType, metadataType) = GetContractType(contract.ContractType, importMany); 37 | 38 | if (metadataType != null) 39 | { 40 | var methodInfo = (from method in _exportProvider.GetType().GetTypeInfo().GetMethods() 41 | where method.Name == nameof(ExportProvider.GetExports) 42 | where method.IsGenericMethod && method.GetGenericArguments().Length == 2 43 | where method.GetParameters().Length == 1 && 44 | method.GetParameters()[0].ParameterType == typeof(string) 45 | select method).Single(); 46 | var parameterizedMethod = methodInfo.MakeGenericMethod(contractType, metadataType); 47 | export = parameterizedMethod.Invoke(_exportProvider, new object[] {contract.ContractName}); 48 | } 49 | else 50 | { 51 | var methodInfo = (from method in _exportProvider.GetType().GetTypeInfo().GetMethods() 52 | where method.Name == nameof(ExportProvider.GetExports) 53 | where method.IsGenericMethod && method.GetGenericArguments().Length == 1 54 | where method.GetParameters().Length == 1 && 55 | method.GetParameters()[0].ParameterType == typeof(string) 56 | select method).Single(); 57 | var parameterizedMethod = methodInfo.MakeGenericMethod(contractType); 58 | export = parameterizedMethod.Invoke(_exportProvider, new object[] {contract.ContractName}); 59 | } 60 | 61 | return true; 62 | } 63 | 64 | 65 | private static (Type exportType, Type metadataType) GetContractType(Type contractType, bool importMany) 66 | { 67 | if (importMany && contractType.IsConstructedGenericType) 68 | if (contractType.GetGenericTypeDefinition() == typeof(IList<>) 69 | || contractType.GetGenericTypeDefinition() == typeof(ICollection<>) 70 | || contractType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) 71 | contractType = contractType.GenericTypeArguments[0]; 72 | 73 | if (!contractType.IsConstructedGenericType) throw new NotSupportedException(); 74 | 75 | if (contractType.GetGenericTypeDefinition() == typeof(Lazy<>)) 76 | return (contractType.GenericTypeArguments[0], null); 77 | 78 | if (contractType.GetGenericTypeDefinition() == typeof(Lazy<,>)) 79 | return (contractType.GenericTypeArguments[0], contractType.GenericTypeArguments[1]); 80 | 81 | throw new NotSupportedException(); 82 | } 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /src/Dena.CodeAnalysis.Testing/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 DeNA Co., Ltd. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | 10 | --- Below is a license code of dotnet/roslyn-sdk, because some codes in Dena.CodeAnalysis.Testing is from it --- 11 | 12 | 13 | The MIT License (MIT) 14 | 15 | Copyright (c) .NET Foundation and Contributors 16 | 17 | All rights reserved. 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy 20 | of this software and associated documentation files (the "Software"), to deal 21 | in the Software without restriction, including without limitation the rights 22 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 23 | copies of the Software, and to permit persons to whom the Software is 24 | furnished to do so, subject to the following conditions: 25 | 26 | The above copyright notice and this permission notice shall be included in all 27 | copies or substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | SOFTWARE. 36 | -------------------------------------------------------------------------------- /src/Dena.CodeAnalysis.Testing/LocationAssert.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.Text; 4 | 5 | 6 | 7 | namespace Dena.CodeAnalysis.CSharp.Testing 8 | { 9 | /// 10 | /// Assertions for . 11 | /// 12 | public static class LocationAssert 13 | { 14 | /// 15 | /// Tests whether the following 3 properties of and throws an exception 16 | /// if the properties are not match. 17 | /// 18 | /// The 3 properties are: 19 | /// 20 | /// 21 | /// 22 | /// 23 | /// 24 | /// 25 | /// The file path that expected. 26 | /// The start line position that expected. 27 | /// The end line position that expected. 28 | /// The actual location. 29 | /// 30 | /// Thrown if have wrong properties. 31 | /// 32 | public static void HaveTheSpan( 33 | string expectedPath, 34 | LinePosition expectedStart, 35 | LinePosition expectedEnd, 36 | Location actual 37 | ) 38 | { 39 | var actualSpan = actual.GetLineSpan(); 40 | 41 | var builder = new StringBuilder(); 42 | 43 | builder.AppendLine(" {"); 44 | var pathDiff = DiffPath(builder, expectedPath, actualSpan.Path); 45 | var startDiff = DiffStartLinePos( 46 | builder, 47 | expectedStart, 48 | actualSpan.StartLinePosition, 49 | expectedPath, 50 | actualSpan.Path 51 | ); 52 | var endDiff = DiffEndLinePos( 53 | builder, 54 | expectedEnd, 55 | actualSpan.EndLinePosition, 56 | expectedPath, 57 | actualSpan.Path 58 | ); 59 | builder.AppendLine(" }"); 60 | 61 | if (pathDiff || startDiff || endDiff) Assert.Fail(builder.ToString()); 62 | } 63 | 64 | 65 | /// 66 | /// Tests whether the 2 properties of and throws an exception 67 | /// if the properties are not match. 68 | /// 69 | /// The 2 major properties are: 70 | /// 71 | /// 72 | /// 73 | /// 74 | /// 75 | /// The start line position that expected. 76 | /// The end line position that expected. 77 | /// The actual location. 78 | /// 79 | /// Thrown if have wrong properties. 80 | /// 81 | public static void HaveTheSpan( 82 | LinePosition expectedStart, 83 | LinePosition expectedEnd, 84 | Location actual 85 | ) 86 | { 87 | var actualSpan = actual.GetLineSpan(); 88 | 89 | var builder = new StringBuilder(); 90 | 91 | builder.AppendLine(" {"); 92 | var startDiff = DiffStartLinePos( 93 | builder, 94 | expectedStart, 95 | actualSpan.StartLinePosition, 96 | UncheckedFilePath, 97 | UncheckedFilePath 98 | ); 99 | var endDiff = DiffEndLinePos( 100 | builder, 101 | expectedEnd, 102 | actualSpan.EndLinePosition, 103 | UncheckedFilePath, 104 | UncheckedFilePath 105 | ); 106 | builder.AppendLine(" }"); 107 | 108 | if (startDiff || endDiff) Assert.Fail(builder.ToString()); 109 | } 110 | 111 | 112 | private static bool DiffPath(StringBuilder builder, string expected, string actual) 113 | { 114 | if (actual.Equals(expected)) 115 | { 116 | builder.AppendLine($" Path = \"{actual}\""); 117 | return false; 118 | } 119 | 120 | builder.AppendLine($"- Path = \"{expected}\""); 121 | builder.AppendLine($"+ Path = \"{actual}\""); 122 | return true; 123 | } 124 | 125 | 126 | private static bool DiffStartLinePos( 127 | StringBuilder builder, 128 | LinePosition expected, 129 | LinePosition actual, 130 | string expectedPath, 131 | string actualPath 132 | ) 133 | { 134 | if (actual.Equals(expected)) 135 | { 136 | builder.AppendLine( 137 | $" // It will be shown by 1-based index like: \"{actualPath}({actual.Line + 1},{actual.Character + 1}): Lorem Ipsum ...\")" 138 | ); 139 | builder.AppendLine( 140 | $" StartLinePosition = new LinePosition({actual.Line}, {actual.Character})" 141 | ); 142 | return false; 143 | } 144 | 145 | builder.AppendLine( 146 | $"- // It will be shown by 1-based index like: \"{expectedPath}({expected.Line + 1},{expected.Character + 1}): Lorem Ipsum ...\")" 147 | ); 148 | builder.AppendLine( 149 | $"- StartLinePosition = new LinePosition({expected.Line}, {expected.Character})" 150 | ); 151 | builder.AppendLine( 152 | $"+ // It will be shown by 1-based index like: \"{actualPath}({actual.Line + 1},{actual.Character + 1}): Lorem Ipsum ...\")" 153 | ); 154 | builder.AppendLine( 155 | $"+ StartLinePosition = new LinePosition({actual.Line}, {actual.Character})" 156 | ); 157 | return true; 158 | } 159 | 160 | 161 | private static bool DiffEndLinePos( 162 | StringBuilder builder, 163 | LinePosition expected, 164 | LinePosition actual, 165 | string expectedPath, 166 | string actualPath 167 | ) 168 | { 169 | if (actual.Equals(expected)) 170 | { 171 | builder.AppendLine( 172 | $" // It will be shown by 1-based index like: \"{expectedPath}({actual.Line + 1},{actual.Character + 1}): Lorem Ipsum ...\")" 173 | ); 174 | builder.AppendLine( 175 | $" EndLinePosition = new LinePosition({actual.Line}, {actual.Character})" 176 | ); 177 | return false; 178 | } 179 | 180 | builder.AppendLine( 181 | $"- // It will be shown by 1-based index like: \"{expectedPath}({expected.Line + 1},{expected.Character + 1}): Lorem Ipsum ...\")" 182 | ); 183 | builder.AppendLine( 184 | $"- EndLinePosition = new LinePosition({expected.Line}, {expected.Character})" 185 | ); 186 | builder.AppendLine( 187 | $"+ // It will be shown by 1-based index like: \"{actualPath}({actual.Line + 1},{actual.Character + 1}): Lorem Ipsum ...\")" 188 | ); 189 | builder.AppendLine( 190 | $"+ EndLinePosition = new LinePosition({actual.Line}, {actual.Character})" 191 | ); 192 | return true; 193 | } 194 | 195 | 196 | private const string UncheckedFilePath = "/path/to/unchecked.cs"; 197 | } 198 | } -------------------------------------------------------------------------------- /src/Dena.CodeAnalysis.Testing/NullAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Diagnostics.CodeAnalysis; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.Diagnostics; 5 | 6 | 7 | 8 | namespace Dena.CodeAnalysis.CSharp.Testing 9 | { 10 | /// 11 | /// A null object class for . 12 | /// 13 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 14 | [SuppressMessage("ReSharper", "UnusedType.Global", Justification = "This is an exposed API")] 15 | public sealed class NullAnalyzer : DiagnosticAnalyzer 16 | { 17 | /// 18 | /// Do nothing. 19 | /// 20 | /// The analysis context but it will be not used. 21 | [SuppressMessage("ReSharper", "RS1025")] 22 | [SuppressMessage("ReSharper", "RS1026")] 23 | public override void Initialize(AnalysisContext context) 24 | { 25 | // Do nothing. 26 | } 27 | 28 | 29 | /// 30 | /// Returns no . 31 | /// 32 | public override ImmutableArray SupportedDiagnostics => 33 | ImmutableArray.Empty; 34 | } 35 | } -------------------------------------------------------------------------------- /src/Dena.CodeAnalysis.Testing/ReadFromFile.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Dena.CodeAnalysis.CSharp.Testing 4 | { 5 | public static class ReadFromFile 6 | { 7 | /// 8 | /// Return a contents of specified file. 9 | /// 10 | /// File relative path (relative to project root) 11 | /// 12 | /// File contents. 13 | /// 14 | public static string ReadFile(string path) 15 | { 16 | return File.ReadAllText(Path.Combine("./../../..", path)); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/Dena.CodeAnalysis.Testing/SpyAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Diagnostics.CodeAnalysis; 5 | using Microsoft.CodeAnalysis; 6 | using Microsoft.CodeAnalysis.Diagnostics; 7 | 8 | 9 | 10 | namespace Dena.CodeAnalysis.CSharp.Testing 11 | { 12 | /// 13 | /// A spy that record whether has been called. 14 | /// 15 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 16 | public sealed class SpyAnalyzer : DiagnosticAnalyzer 17 | { 18 | /// 19 | /// Whether has been called or not. 20 | /// 21 | public bool IsInitialized; 22 | 23 | private readonly AnalyzerActions _actions; 24 | 25 | 26 | /// 27 | /// Instantiate a new SpyAnalyzer. 28 | /// 29 | public SpyAnalyzer() => _actions = CreateSpyActions(this); 30 | 31 | 32 | /// 33 | /// Record that the method has been called to . 34 | /// 35 | [SuppressMessage("ReSharper", "RS1026", Justification = "Avoid exclusive access control for call histories")] 36 | public override void Initialize(AnalysisContext context) 37 | { 38 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); 39 | IsInitialized = true; 40 | _actions.RegisterAllTo(context); 41 | } 42 | 43 | 44 | /// 45 | /// Returns only . 46 | /// 47 | public override ImmutableArray SupportedDiagnostics => 48 | ImmutableArray.Create(StubDiagnosticDescriptor.ForTest); 49 | 50 | /// 51 | /// History of calls of the action registered via 52 | /// 53 | [SuppressMessage("ReSharper", "CollectionNeverQueried.Global", Justification = "This is an exposed API")] 54 | public readonly IList CodeBlockActionHistory = new List(); 55 | 56 | /// 57 | /// History of calls of the action registered via 58 | /// 59 | [SuppressMessage("ReSharper", "CollectionNeverQueried.Global", Justification = "This is an exposed API")] 60 | public readonly IList CodeBlockStartActionHistory = new List(); 61 | 62 | /// 63 | /// History of calls of the action registered via 64 | /// 65 | [SuppressMessage( 66 | "ReSharper", 67 | "RS1008", 68 | Justification = "Especially for testing, this should be able to store to do check the property" 69 | )] 70 | [SuppressMessage("ReSharper", "CollectionNeverQueried.Global", Justification = "This is an exposed API")] 71 | public readonly IList CompilationActionHistory = new List(); 72 | 73 | /// 74 | /// History of calls of the action registered via 75 | /// 76 | [SuppressMessage( 77 | "ReSharper", 78 | "RS1008", 79 | Justification = "Especially for testing, this should be able to store to do check the property" 80 | )] 81 | [SuppressMessage("ReSharper", "CollectionNeverQueried.Global", Justification = "This is an exposed API")] 82 | public readonly IList CompilationStartActionHistory = new List(); 83 | 84 | /// 85 | /// History of calls of the action registered via 86 | /// 87 | [SuppressMessage("ReSharper", "CollectionNeverQueried.Global", Justification = "This is an exposed API")] 88 | public readonly IList OperationActionHistory = new List(); 89 | 90 | /// 91 | /// History of calls of the action registered via 92 | /// 93 | [SuppressMessage("ReSharper", "CollectionNeverQueried.Global", Justification = "This is an exposed API")] 94 | public readonly IList> OperationBlockActionHistory = 95 | new List>(); 96 | 97 | /// 98 | /// History of calls of the action registered via 99 | /// 100 | [SuppressMessage("ReSharper", "CollectionNeverQueried.Global", Justification = "This is an exposed API")] 101 | public readonly IList> OperationBlockStartActionHistory = 102 | new List>(); 103 | 104 | /// 105 | /// History of calls of the action registered via 106 | /// 107 | [SuppressMessage("ReSharper", "CollectionNeverQueried.Global", Justification = "This is an exposed API")] 108 | public readonly IList SemanticModelActionHistory = new List(); 109 | 110 | /// 111 | /// History of calls of the action registered via 112 | /// 113 | [SuppressMessage("ReSharper", "CollectionNeverQueried.Global", Justification = "This is an exposed API")] 114 | public readonly IList SymbolActionHistory = new List(); 115 | 116 | /// 117 | /// History of calls of the action registered via 118 | /// 119 | [SuppressMessage("ReSharper", "CollectionNeverQueried.Global", Justification = "This is an exposed API")] 120 | public readonly IList SymbolStartActionHistory = new List(); 121 | 122 | /// 123 | /// History of calls of the action registered via 124 | /// 125 | [SuppressMessage("ReSharper", "CollectionNeverQueried.Global", Justification = "This is an exposed API")] 126 | public readonly IList SyntaxNodeActionHistory = new List(); 127 | 128 | /// 129 | /// History of calls of the action registered via 130 | /// 131 | [SuppressMessage("ReSharper", "CollectionNeverQueried.Global", Justification = "This is an exposed API")] 132 | public readonly IList SyntaxTreeActionHistory = new List(); 133 | 134 | 135 | /// 136 | /// Create an to record all events. 137 | /// 138 | /// to record all events. 139 | [SuppressMessage("ReSharper", "MemberCanBePrivate.Global", Justification = "This is an exposed API")] 140 | public static AnalyzerActions CreateSpyActions(SpyAnalyzer spy) => 141 | new AnalyzerActions 142 | { 143 | CodeBlockAction = context => spy.CodeBlockActionHistory.Add(context.CodeBlock), 144 | CodeBlockStartAction = context => spy.CodeBlockStartActionHistory.Add(context.CodeBlock), 145 | CompilationAction = context => spy.CompilationActionHistory.Add(context.Compilation), 146 | CompilationStartAction = context => spy.CompilationStartActionHistory.Add(context.Compilation), 147 | OperationAction = context => spy.OperationActionHistory.Add(context.Operation), 148 | OperationBlockAction = context => spy.OperationBlockActionHistory.Add(context.OperationBlocks), 149 | OperationBlockStartAction = 150 | context => spy.OperationBlockStartActionHistory.Add(context.OperationBlocks), 151 | SemanticModelAction = context => spy.SemanticModelActionHistory.Add(context.SemanticModel), 152 | SymbolAction = context => spy.SymbolActionHistory.Add(context.Symbol), 153 | SymbolStartAction = context => spy.SymbolStartActionHistory.Add(context.Symbol), 154 | SyntaxNodeAction = context => spy.SyntaxNodeActionHistory.Add(context.Node), 155 | SyntaxTreeAction = context => spy.SyntaxTreeActionHistory.Add(context.Tree) 156 | }; 157 | } 158 | } -------------------------------------------------------------------------------- /src/Dena.CodeAnalysis.Testing/StubAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Diagnostics.CodeAnalysis; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.Diagnostics; 5 | 6 | 7 | 8 | namespace Dena.CodeAnalysis.CSharp.Testing 9 | { 10 | /// 11 | /// A stub class for . 12 | /// 13 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 14 | [SuppressMessage("ReSharper", "UnusedType.Global", Justification = "This is an exposed API")] 15 | public sealed class StubAnalyzer : DiagnosticAnalyzer 16 | { 17 | private readonly AnalyzerActions _actions; 18 | 19 | 20 | /// 21 | /// Instantiate a stub for with no actions. 22 | /// 23 | [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "This is an exposed API")] 24 | public StubAnalyzer() => _actions = new AnalyzerActions(); 25 | 26 | 27 | /// 28 | /// Instantiate a stub for with the specified actions. 29 | /// All actions in will be registered at . 30 | /// 31 | public StubAnalyzer(AnalyzerActions actions) => _actions = actions; 32 | 33 | 34 | /// 35 | /// Register all actions specified to the constructor. 36 | /// 37 | /// 38 | public override void Initialize(AnalysisContext context) 39 | { 40 | context.EnableConcurrentExecution(); 41 | context.ConfigureGeneratedCodeAnalysis( 42 | GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics 43 | ); 44 | 45 | _actions.RegisterAllTo(context); 46 | } 47 | 48 | 49 | /// 50 | /// Return only . 51 | /// 52 | public override ImmutableArray SupportedDiagnostics => 53 | ImmutableArray.Create(StubDiagnosticDescriptor.ForTest); 54 | } 55 | } -------------------------------------------------------------------------------- /src/Dena.CodeAnalysis.Testing/StubDiagnosticDescriptor.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.Diagnostics; 4 | 5 | 6 | 7 | namespace Dena.CodeAnalysis.CSharp.Testing 8 | { 9 | /// 10 | /// A stub collections for . 11 | /// 12 | public static class StubDiagnosticDescriptor 13 | { 14 | /// 15 | /// A stub to test . 16 | /// 17 | [SuppressMessage( 18 | "ReSharper", 19 | "RS2008", 20 | Justification = "This is for only test doubles for analyzers. So release tracking is not needed" 21 | )] 22 | public static readonly DiagnosticDescriptor ForTest = new DiagnosticDescriptor( 23 | "TEST", 24 | "This is a diagnostics stub", 25 | "This is a diagnostics stub", 26 | "AnalyzerTest", 27 | DiagnosticSeverity.Info, 28 | true 29 | ); 30 | } 31 | } -------------------------------------------------------------------------------- /src/Dena.CodeAnalysis.Testing/TestDataParser.cs: -------------------------------------------------------------------------------- 1 | // (c) 2021 DeNA Co., Ltd. 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text.RegularExpressions; 6 | using Microsoft.CodeAnalysis; 7 | using Microsoft.CodeAnalysis.Text; 8 | using System.Runtime.CompilerServices; 9 | 10 | [assembly: InternalsVisibleTo("Dena.CodeAnalysis.Testing.Tests")] 11 | 12 | namespace Dena.CodeAnalysis.CSharp.Testing 13 | { 14 | public static class TestDataParser 15 | { 16 | /// 17 | /// Create source and expected diagnostic from formatting embedded. 18 | /// 19 | /// 20 | /// Separate *testData* into source and diagnostic. 21 | /// *testData* is the source enclosed by format where it is expected to be reported by the analyzer. 22 | /// Specify the part to be reported in the following format. 23 | /// The format is enclosed in { } and separated by |. 24 | /// {|source|Identifier of the DiagnosticDescriptor (DDID)|DiagnosticMessage|}/// 25 | /// e.g., {|new Stack()|CS1002|; expected|} 26 | /// 27 | /// String containing format in source 28 | /// 29 | /// 1. Source extracted from *testData* 30 | /// 2. Returns a List of Diagnostic containing Location, DiagnosticMessage, and DDID 31 | /// 32 | public static (string source, List expectedDiagnostics) CreateSourceAndExpectedDiagnostic( 33 | string testData) 34 | { 35 | var diagnostics = new List(); 36 | 37 | foreach (var (target, ddid, msg) in ExtractMaker(testData)) 38 | { 39 | var format = "{|" + target + "|" + ddid + "|" + msg + "|}"; 40 | Location location = CreateLocation(testData, format, target.Length); 41 | testData = testData.Replace(format, target); 42 | var diagnosticDescriptor = new DiagnosticDescriptor( 43 | id: ddid, 44 | title: null!, 45 | messageFormat: msg, 46 | category: "", 47 | defaultSeverity: DiagnosticSeverity.Error, 48 | isEnabledByDefault: true); 49 | var diagnostic = Diagnostic.Create( 50 | diagnosticDescriptor, 51 | location); 52 | diagnostics.Add(diagnostic); 53 | } 54 | 55 | return (testData, diagnostics); 56 | } 57 | 58 | internal static IEnumerable<(string target, string ddid, string msg)> ExtractMaker( 59 | string testData) 60 | { 61 | const string Pattern = "\\{\\|([^\\|]*)\\|([^\\|]*)\\|([^\\|]*)\\|\\}"; 62 | // TODO: Write Patterns more simply using repeat formatting. 63 | var match = Regex.Match(testData, Pattern); 64 | while (match.Success) 65 | { 66 | yield return (match.Groups[1].Value, match.Groups[2].Value, match.Groups[3].Value); 67 | match = match.NextMatch(); 68 | } 69 | } 70 | 71 | internal static Location CreateLocation(string sourceBeforeExtractFormat, string format, int targetLength) 72 | { 73 | var start = CreateLinePositionStart(sourceBeforeExtractFormat, format); 74 | var end = new LinePosition(start.Line, start.Character + targetLength); 75 | Location location = Location.Create( 76 | "/0/Test0.cs", 77 | new TextSpan( 78 | sourceBeforeExtractFormat.IndexOf(format, StringComparison.Ordinal), 79 | targetLength), 80 | new LinePositionSpan( 81 | start, 82 | end) 83 | ); 84 | return location; 85 | } 86 | 87 | internal static LinePosition CreateLinePositionStart(string sourceBeforeExtractFormat, string format) 88 | { 89 | // Currently, Line returns 0 as origin and character returns 1 as origin 90 | var (newLineCount, newLinePosition, characterCount) = (0, -1, 0); 91 | var formatIndex = sourceBeforeExtractFormat.IndexOf(format, StringComparison.Ordinal); 92 | // add a newline at the end, as without a newline after the format it would be an infinite loop 93 | sourceBeforeExtractFormat += "\n"; 94 | 95 | while (newLinePosition < formatIndex) 96 | { 97 | characterCount = formatIndex - (newLinePosition + 1); 98 | newLinePosition = 99 | sourceBeforeExtractFormat.IndexOf("\n", newLinePosition + 1, StringComparison.Ordinal); 100 | newLineCount++; 101 | } 102 | 103 | return new LinePosition(newLineCount - 1, characterCount); 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /tests/Dena.CodeAnalysis.Testing.Tests/AnalyzerRunnerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Cysharp.Threading.Tasks; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using MSTestAssert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; 5 | 6 | 7 | namespace Dena.CodeAnalysis.CSharp.Testing 8 | { 9 | [TestClass] 10 | public class AnalyzerRunnerTests 11 | { 12 | [TestMethod] 13 | public async Task WhenGivenNoCodes_ItShouldThrowAnException() 14 | { 15 | var anyAnalyzer = new NullAnalyzer(); 16 | 17 | await MSTestAssert.ThrowsExceptionAsync( 18 | async () => { await DiagnosticAnalyzerRunner.Run(anyAnalyzer); } 19 | ); 20 | } 21 | 22 | 23 | [TestMethod] 24 | public async Task WhenGivenDiagnosticCleanCode_ItShouldReturnNoDiagnostics() 25 | { 26 | var anyAnalyzer = new NullAnalyzer(); 27 | var diagnostics = await DiagnosticAnalyzerRunner.Run( 28 | anyAnalyzer, 29 | codes: ExampleCode.DiagnosticsFreeClassLibrary 30 | ); 31 | 32 | MSTestAssert.AreEqual(0, diagnostics.Length, DiagnosticsFormatter.Format(diagnostics)); 33 | } 34 | 35 | [TestMethod] 36 | public async Task WhenGivenUniTaskImport_ItShouldReturnNoDiagnostics() 37 | { 38 | var anyAnalyzer = new NullAnalyzer(); 39 | var diagnostics = await DiagnosticAnalyzerRunner.Run( 40 | anyAnalyzer, 41 | new[] { typeof(UniTask) }, 42 | ExampleCode.UniTaskImport 43 | ); 44 | 45 | MSTestAssert.AreEqual(1, diagnostics.Length, DiagnosticsFormatter.Format(diagnostics)); 46 | } 47 | 48 | [TestMethod] 49 | public async Task WhenGivenContainingASyntaxError_ItShouldReturnSeveralDiagnostics() 50 | { 51 | var anyAnalyzer = new NullAnalyzer(); 52 | var diagnostics = await DiagnosticAnalyzerRunner.Run( 53 | anyAnalyzer, 54 | codes: ExampleCode.ContainingSyntaxError 55 | ); 56 | 57 | MSTestAssert.AreNotEqual(0, diagnostics.Length); 58 | } 59 | 60 | 61 | [TestMethod] 62 | public async Task WhenGivenAnyCodes_ItShouldCallAnalyzerInitialize() 63 | { 64 | var spyAnalyzer = new SpyAnalyzer(); 65 | 66 | await DiagnosticAnalyzerRunner.Run(spyAnalyzer, codes: ExampleCode.DiagnosticsFreeClassLibrary); 67 | 68 | MSTestAssert.IsTrue(spyAnalyzer.IsInitialized); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /tests/Dena.CodeAnalysis.Testing.Tests/Dena.CodeAnalysis.Testing.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | true 7 | true 8 | false 9 | 10 | 11 | 12 | 13 | 14 | runtime; build; native; contentfiles; analyzers 15 | all 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | all 28 | runtime; build; native; contentfiles; analyzers 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/Dena.CodeAnalysis.Testing.Tests/DiagnosticAssertTest.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.Text; 4 | using NUnit.Framework; 5 | using NUnitAssert = NUnit.Framework.Assert; 6 | 7 | namespace Dena.CodeAnalysis.CSharp.Testing 8 | { 9 | [TestFixture] 10 | public class DiagnosticAssertTest 11 | { 12 | [Test] 13 | public void DiagnosticAssert_SameDiagnostic_Success() 14 | { 15 | var diagnostic1 = ImmutableArray.Create( 16 | CreateDummyDiagnostic( 17 | CreateDummyLocation(100, 200, 300, 400, "path/to/defaultFile.cs"), 18 | "ID01", 19 | "defaultMessage")); 20 | 21 | var diagnostic2 = ImmutableArray.Create( 22 | CreateDummyDiagnostic( 23 | CreateDummyLocation(100, 200, 300, 400, "path/to/defaultFile.cs"), 24 | "ID01", 25 | "defaultMessage")); 26 | 27 | NUnitAssert.DoesNotThrow( 28 | delegate { DiagnosticsAssert.AreEqual(diagnostic2, diagnostic1); }); 29 | } 30 | 31 | [Test] 32 | public void DiagnosticAssert_ActualGraterThanExpected_Failed() 33 | { 34 | var diagnostic1 = ImmutableArray.Create( 35 | CreateDummyDiagnostic( 36 | CreateDummyLocation(100, 200, 300, 400, "path/to/defaultFile.cs"), 37 | "ID01", 38 | "defaultMessage") 39 | ); 40 | 41 | var diagnostic2 = ImmutableArray.Create(); 42 | 43 | NUnitAssert.Throws(Is.TypeOf() 44 | .And.Message.EqualTo( 45 | @"Missing 0 diagnostics, extra 1 diagnostics of all 1 diagnostics: 46 | extra path/to/defaultFile.cs: (100,200)-(300,400), ID01, defaultMessage 47 | "), 48 | delegate { DiagnosticsAssert.AreEqual(diagnostic2, diagnostic1); } 49 | ); 50 | } 51 | 52 | [Test] 53 | public void DiagnosticAssert_LargeNumberOfExpected_Failed() 54 | { 55 | var diagnostic1 = ImmutableArray.Create(); 56 | 57 | var diagnostic2 = ImmutableArray.Create( 58 | CreateDummyDiagnostic( 59 | CreateDummyLocation(100, 200, 300, 400, "path/to/defaultFile.cs"), 60 | "ID01", 61 | "defaultMessage") 62 | ); 63 | 64 | NUnitAssert.Throws(Is.TypeOf() 65 | .And.Message.EqualTo( 66 | @"Missing 1 diagnostics, extra 0 diagnostics of all 0 diagnostics: 67 | missing path/to/defaultFile.cs: (100,200)-(300,400), ID01, defaultMessage 68 | "), 69 | delegate { DiagnosticsAssert.AreEqual(diagnostic2, diagnostic1); } 70 | ); 71 | } 72 | 73 | [Test] 74 | public void DiagnosticAssert_NoDiagnostics_Success() 75 | { 76 | var diagnostic1 = ImmutableArray.Create(); 77 | 78 | var diagnostic2 = ImmutableArray.Create(); 79 | 80 | NUnitAssert.DoesNotThrow( 81 | delegate { DiagnosticsAssert.AreEqual(diagnostic2, diagnostic1); } 82 | ); 83 | } 84 | 85 | [Test] 86 | public void DiagnosticAssert_NotEqualsDDID_Failed() 87 | { 88 | var diagnostic1 = ImmutableArray.Create( 89 | CreateDummyDiagnostic( 90 | CreateDummyLocation(100, 200, 300, 400, "path/to/defaultFile.cs"), 91 | "ID02", 92 | "defaultMessage") 93 | ); 94 | 95 | var diagnostic2 = ImmutableArray.Create( 96 | CreateDummyDiagnostic( 97 | CreateDummyLocation(100, 200, 300, 400, "path/to/defaultFile.cs"), 98 | "ID01", 99 | "defaultMessage") 100 | ); 101 | 102 | NUnitAssert.Throws(Is.TypeOf() 103 | .And.Message.EqualTo( 104 | @"Missing 1 diagnostics, extra 1 diagnostics of all 1 diagnostics: 105 | missing path/to/defaultFile.cs: (100,200)-(300,400), ID01, defaultMessage 106 | extra path/to/defaultFile.cs: (100,200)-(300,400), ID02, defaultMessage 107 | "), 108 | delegate { DiagnosticsAssert.AreEqual(diagnostic2, diagnostic1); } 109 | ); 110 | } 111 | 112 | [Test] 113 | public void DiagnosticAssert_NotEqualsLocationLinePositionSpan_Failed() 114 | { 115 | var diagnostic1 = ImmutableArray.Create( 116 | CreateDummyDiagnostic( 117 | CreateDummyLocation(5, 6, 7, 8, "path/to/defaultFile.cs"), 118 | "ID01", 119 | "defaultMessage") 120 | ); 121 | 122 | var diagnostic2 = ImmutableArray.Create( 123 | CreateDummyDiagnostic( 124 | CreateDummyLocation(100, 200, 300, 400, "path/to/defaultFile.cs"), 125 | "ID01", 126 | "defaultMessage") 127 | ); 128 | 129 | NUnitAssert.Throws(Is.TypeOf() 130 | .And.Message.EqualTo( 131 | @"Missing 1 diagnostics, extra 1 diagnostics of all 1 diagnostics: 132 | missing path/to/defaultFile.cs: (100,200)-(300,400), ID01, defaultMessage 133 | extra path/to/defaultFile.cs: (5,6)-(7,8), ID01, defaultMessage 134 | "), 135 | delegate { DiagnosticsAssert.AreEqual(diagnostic2, diagnostic1); } 136 | ); 137 | } 138 | 139 | [Test] 140 | public void DiagnosticAssert_NotEqualsLocationPath_Failed() 141 | { 142 | var diagnostic1 = ImmutableArray.Create( 143 | CreateDummyDiagnostic( 144 | CreateDummyLocation(100, 200, 300, 400, "hoge/fuga/moge.cs"), 145 | "ID01", 146 | "defaultMessage") 147 | ); 148 | 149 | var diagnostic2 = ImmutableArray.Create( 150 | CreateDummyDiagnostic( 151 | CreateDummyLocation(100, 200, 300, 400, "path/to/defaultFile.cs"), 152 | "ID01", 153 | "defaultMessage") 154 | ); 155 | 156 | NUnitAssert.Throws(Is.TypeOf() 157 | .And.Message.EqualTo( 158 | @"Missing 1 diagnostics, extra 1 diagnostics of all 1 diagnostics: 159 | missing path/to/defaultFile.cs: (100,200)-(300,400), ID01, defaultMessage 160 | extra hoge/fuga/moge.cs: (100,200)-(300,400), ID01, defaultMessage 161 | "), 162 | delegate { DiagnosticsAssert.AreEqual(diagnostic2, diagnostic1); } 163 | ); 164 | } 165 | 166 | [Test] 167 | public void DiagnosticAssert_NotEqualsDiagnosticMessage_Failed() 168 | { 169 | var diagnostic1 = ImmutableArray.Create( 170 | CreateDummyDiagnostic( 171 | CreateDummyLocation(100, 200, 300, 400, "path/to/defaultFile.cs"), 172 | "ID01", 173 | "message2") 174 | ); 175 | 176 | var diagnostic2 = ImmutableArray.Create( 177 | CreateDummyDiagnostic( 178 | CreateDummyLocation(100, 200, 300, 400, "path/to/defaultFile.cs"), 179 | "ID01", 180 | "defaultMessage") 181 | ); 182 | 183 | NUnitAssert.Throws(Is.TypeOf() 184 | .And.Message.EqualTo( 185 | @"Missing 1 diagnostics, extra 1 diagnostics of all 1 diagnostics: 186 | missing path/to/defaultFile.cs: (100,200)-(300,400), ID01, defaultMessage 187 | extra path/to/defaultFile.cs: (100,200)-(300,400), ID01, message2 188 | "), 189 | delegate { DiagnosticsAssert.AreEqual(diagnostic2, diagnostic1); } 190 | ); 191 | } 192 | 193 | [Test] 194 | public void DiagnosticAssert_NotEqualsMultipleDiagnostics_Failed() 195 | { 196 | var diagnostic1 = ImmutableArray.Create( 197 | CreateDummyDiagnostic( 198 | CreateDummyLocation(9, 10, 11, 12, "path/to/target3.cs"), 199 | "ID03", 200 | "message3"), 201 | CreateDummyDiagnostic( 202 | CreateDummyLocation(13, 14, 15, 16, "path/to/target4.cs"), 203 | "ID04", 204 | "message4") 205 | ); 206 | 207 | var diagnostic2 = ImmutableArray.Create( 208 | CreateDummyDiagnostic( 209 | CreateDummyLocation(100, 200, 300, 400, "path/to/defaultFile.cs"), 210 | "ID01", 211 | "defaultMessage"), 212 | CreateDummyDiagnostic( 213 | CreateDummyLocation(5, 6, 7, 8, "path/to/target2.cs"), 214 | "ID02", 215 | "message2") 216 | ); 217 | 218 | NUnitAssert.Throws(Is.TypeOf() 219 | .And.Message.EqualTo( 220 | @"Missing 2 diagnostics, extra 2 diagnostics of all 2 diagnostics: 221 | missing path/to/defaultFile.cs: (100,200)-(300,400), ID01, defaultMessage 222 | missing path/to/target2.cs: (5,6)-(7,8), ID02, message2 223 | extra path/to/target3.cs: (9,10)-(11,12), ID03, message3 224 | extra path/to/target4.cs: (13,14)-(15,16), ID04, message4 225 | "), 226 | delegate { DiagnosticsAssert.AreEqual(diagnostic2, diagnostic1); } 227 | ); 228 | } 229 | 230 | [Test] 231 | public void DiagnosticAssert_EqualsMultipleDiagnostics_Success() 232 | { 233 | var diagnostic1 = ImmutableArray.Create( 234 | CreateDummyDiagnostic( 235 | CreateDummyLocation(5, 6, 7, 8, "path/to/target2.cs"), 236 | "ID02", 237 | "message2"), 238 | CreateDummyDiagnostic( 239 | CreateDummyLocation(9, 10, 11, 12, "path/to/target3.cs"), 240 | "ID03", 241 | "message3") 242 | ); 243 | 244 | var diagnostic2 = ImmutableArray.Create( 245 | CreateDummyDiagnostic( 246 | CreateDummyLocation(5, 6, 7, 8, "path/to/target2.cs"), 247 | "ID02", 248 | "message2"), 249 | CreateDummyDiagnostic( 250 | CreateDummyLocation(9, 10, 11, 12, "path/to/target3.cs"), 251 | "ID03", 252 | "message3") 253 | ); 254 | 255 | NUnitAssert.DoesNotThrow(delegate { DiagnosticsAssert.AreEqual(diagnostic2, diagnostic1); }); 256 | } 257 | 258 | [Test] 259 | public void IsEmpty_ZeroActual_Success() 260 | { 261 | var actual = ImmutableArray.Create(); 262 | NUnitAssert.DoesNotThrow(delegate { DiagnosticsAssert.IsEmpty(actual); }); 263 | } 264 | 265 | [Test] 266 | public void IsEmpty_OneActual_Failed() 267 | { 268 | var actuals = ImmutableArray.Create( 269 | CreateDummyDiagnostic( 270 | CreateDummyLocation(100, 200, 300, 400, "path/to/defaultFile.cs"), 271 | "ID01", 272 | "defaultMessage") 273 | ); 274 | 275 | NUnitAssert.Throws(Is.TypeOf() 276 | .And.Message.EqualTo( 277 | @"expected no diagnostics, but 1 diagnostics are reported 278 | extra path/to/defaultFile.cs: (100,200)-(300,400), ID01, defaultMessage 279 | "), 280 | delegate { DiagnosticsAssert.IsEmpty(actuals); } 281 | ); 282 | } 283 | 284 | [Test] 285 | public void IsEmpty_ManyActuals_Failed() 286 | { 287 | var actuals = ImmutableArray.Create( 288 | CreateDummyDiagnostic( 289 | CreateDummyLocation(100, 200, 300, 400, "path/to/defaultFile.cs"), 290 | "ID01", 291 | "defaultMessage"), 292 | CreateDummyDiagnostic( 293 | CreateDummyLocation(5, 6, 7, 8, "path/to/target2.cs"), 294 | "ID02", 295 | "message2") 296 | ); 297 | 298 | NUnitAssert.Throws(Is.TypeOf() 299 | .And.Message.EqualTo( 300 | @"expected no diagnostics, but 2 diagnostics are reported 301 | extra path/to/defaultFile.cs: (100,200)-(300,400), ID01, defaultMessage 302 | extra path/to/target2.cs: (5,6)-(7,8), ID02, message2 303 | "), 304 | delegate { DiagnosticsAssert.IsEmpty(actuals); } 305 | ); 306 | } 307 | 308 | private static Diagnostic CreateDummyDiagnostic(Location location, string id, string message, 309 | string category = "defaultCategory", DiagnosticSeverity severity = DiagnosticSeverity.Hidden) 310 | { 311 | var diagnosticDescriptor = new DiagnosticDescriptor( 312 | id, 313 | "title", 314 | message, 315 | category, 316 | severity, 317 | true); 318 | return Diagnostic.Create( 319 | diagnosticDescriptor, 320 | location); 321 | } 322 | 323 | private static Location CreateDummyLocation(int startLine, int startCharacter, int endLine, 324 | int endCharacter, 325 | string path) 326 | { 327 | return Location.Create( 328 | path, 329 | // DiagnosticAssertに於いて、TextSpanは使用しない 330 | new TextSpan(0, 0), 331 | new LinePositionSpan(new LinePosition(startLine, startCharacter), 332 | new LinePosition(endLine, endCharacter)) 333 | ); 334 | } 335 | } 336 | } -------------------------------------------------------------------------------- /tests/Dena.CodeAnalysis.Testing.Tests/DiagnosticsFormatterTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using MSTestAssert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; 4 | 5 | 6 | namespace Dena.CodeAnalysis.CSharp.Testing 7 | { 8 | [TestClass] 9 | public class DiagnosticsFormatterTests 10 | { 11 | [TestMethod] 12 | public async Task Format() 13 | { 14 | var diagnostics = await DiagnosticAnalyzerRunner.Run( 15 | new NullAnalyzer(), 16 | codes: ExampleCode.ContainingSyntaxError 17 | ); 18 | 19 | var actual = DiagnosticsFormatter.Format(diagnostics); 20 | var expected = 21 | @"// /0/Test0.cs(9,1): error CS0116: A namespace cannot directly contain members such as fields or methods 22 | DiagnosticResult.CompilerError(""CS0116"").WithSpan(""/0/Test0.cs"", 9, 1, 9, 6), 23 | "; 24 | MSTestAssert.AreEqual(expected, actual); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /tests/Dena.CodeAnalysis.Testing.Tests/LocationAssertTest.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.CodeAnalysis.Text; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using MSTestAssert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; 5 | 6 | 7 | 8 | namespace Dena.CodeAnalysis.CSharp.Testing 9 | { 10 | [TestClass] 11 | public class LocationAssertTest 12 | { 13 | [TestMethod] 14 | public async Task HaveTheSpanWithPath_Success() 15 | { 16 | var actual = await LocationFactory.Create(); 17 | 18 | LocationAssert.HaveTheSpan( 19 | "/0/Test0.cs", 20 | new LinePosition(8, 0), 21 | new LinePosition(8, 5), 22 | actual 23 | ); 24 | } 25 | 26 | 27 | [TestMethod] 28 | public async Task HaveTheSpanWithPath_Failed() 29 | { 30 | var actual = await LocationFactory.Create(); 31 | 32 | try 33 | { 34 | LocationAssert.HaveTheSpan( 35 | "/0/Test999.cs", 36 | new LinePosition(999, 999), 37 | new LinePosition(999, 999), 38 | actual 39 | ); 40 | } 41 | catch (AssertFailedException e) 42 | { 43 | MSTestAssert.AreEqual( 44 | @" { 45 | - Path = ""/0/Test999.cs"" 46 | + Path = ""/0/Test0.cs"" 47 | - // It will be shown by 1-based index like: ""/0/Test999.cs(1000,1000): Lorem Ipsum ..."") 48 | - StartLinePosition = new LinePosition(999, 999) 49 | + // It will be shown by 1-based index like: ""/0/Test0.cs(9,1): Lorem Ipsum ..."") 50 | + StartLinePosition = new LinePosition(8, 0) 51 | - // It will be shown by 1-based index like: ""/0/Test999.cs(1000,1000): Lorem Ipsum ..."") 52 | - EndLinePosition = new LinePosition(999, 999) 53 | + // It will be shown by 1-based index like: ""/0/Test0.cs(9,6): Lorem Ipsum ..."") 54 | + EndLinePosition = new LinePosition(8, 5) 55 | } 56 | ", 57 | e.Message 58 | ); 59 | } 60 | } 61 | 62 | 63 | [TestMethod] 64 | public async Task HaveTheSpanWithoutPath_Success() 65 | { 66 | var actual = await LocationFactory.Create(); 67 | 68 | LocationAssert.HaveTheSpan( 69 | new LinePosition(8, 0), 70 | new LinePosition(8, 5), 71 | actual 72 | ); 73 | } 74 | 75 | 76 | [TestMethod] 77 | public async Task HaveTheSpanWithoutPath_Failed() 78 | { 79 | var actual = await LocationFactory.Create(); 80 | 81 | try 82 | { 83 | LocationAssert.HaveTheSpan( 84 | new LinePosition(999, 999), 85 | new LinePosition(999, 999), 86 | actual 87 | ); 88 | } 89 | catch (AssertFailedException e) 90 | { 91 | MSTestAssert.AreEqual( 92 | @" { 93 | - // It will be shown by 1-based index like: ""/path/to/unchecked.cs(1000,1000): Lorem Ipsum ..."") 94 | - StartLinePosition = new LinePosition(999, 999) 95 | + // It will be shown by 1-based index like: ""/path/to/unchecked.cs(9,1): Lorem Ipsum ..."") 96 | + StartLinePosition = new LinePosition(8, 0) 97 | - // It will be shown by 1-based index like: ""/path/to/unchecked.cs(1000,1000): Lorem Ipsum ..."") 98 | - EndLinePosition = new LinePosition(999, 999) 99 | + // It will be shown by 1-based index like: ""/path/to/unchecked.cs(9,6): Lorem Ipsum ..."") 100 | + EndLinePosition = new LinePosition(8, 5) 101 | } 102 | ", 103 | e.Message 104 | ); 105 | } 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /tests/Dena.CodeAnalysis.Testing.Tests/LocationFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.CodeAnalysis; 3 | 4 | 5 | 6 | namespace Dena.CodeAnalysis.CSharp.Testing 7 | { 8 | public static class LocationFactory 9 | { 10 | public static async Task Create() 11 | { 12 | var ds = await DiagnosticAnalyzerRunner.Run( 13 | new NullAnalyzer(), 14 | codes: ExampleCode.ContainingSyntaxError 15 | ); 16 | return ds[0].Location; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /tests/Dena.CodeAnalysis.Testing.Tests/ReadFromFileTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using NUnitAssert = NUnit.Framework.Assert; 3 | 4 | namespace Dena.CodeAnalysis.CSharp.Testing 5 | { 6 | [TestFixture] 7 | public class ReadFromFileTest 8 | { 9 | [Test] 10 | [TestCaseSource(nameof(GoodTestCases))] 11 | public void GoodTestCase(string path) 12 | { 13 | NUnitAssert.That(ReadFromFile.ReadFile(path), Does.Contain("hello-world")); 14 | } 15 | 16 | 17 | private static object[][] GoodTestCases => 18 | new[] 19 | { 20 | new object[] { TestData.GetPath("Example.txt") } 21 | }; 22 | } 23 | } -------------------------------------------------------------------------------- /tests/Dena.CodeAnalysis.Testing.Tests/SpyAnalyzerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Threading.Tasks; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using MSTestAssert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; 5 | 6 | 7 | namespace Dena.CodeAnalysis.CSharp.Testing 8 | { 9 | [TestClass] 10 | public class SpyAnalyzerTests 11 | { 12 | [TestMethod] 13 | public async Task WhenGivenAnyCodes_RecordAllActionHistory() 14 | { 15 | var spy = new SpyAnalyzer(); 16 | var builder = new StringBuilder(); 17 | var failed = false; 18 | 19 | await DiagnosticAnalyzerRunner.Run(spy, codes: ExampleCode.DiagnosticsFreeClassLibrary); 20 | 21 | if (0 == spy.CodeBlockActionHistory.Count) 22 | { 23 | failed = true; 24 | builder.AppendLine(nameof(spy.CodeBlockActionHistory)); 25 | } 26 | 27 | if (0 == spy.CodeBlockStartActionHistory.Count) 28 | { 29 | failed = true; 30 | builder.AppendLine(nameof(spy.CodeBlockStartActionHistory)); 31 | } 32 | 33 | if (0 == spy.CompilationActionHistory.Count) 34 | { 35 | failed = true; 36 | builder.AppendLine(nameof(spy.CompilationActionHistory)); 37 | } 38 | 39 | if (0 == spy.CompilationStartActionHistory.Count) 40 | { 41 | failed = true; 42 | builder.AppendLine(nameof(spy.CompilationStartActionHistory)); 43 | } 44 | 45 | if (0 == spy.OperationActionHistory.Count) 46 | { 47 | failed = true; 48 | builder.AppendLine(nameof(spy.OperationActionHistory)); 49 | } 50 | 51 | if (0 == spy.OperationBlockActionHistory.Count) 52 | { 53 | failed = true; 54 | builder.AppendLine(nameof(spy.OperationBlockActionHistory)); 55 | } 56 | 57 | if (0 == spy.OperationBlockStartActionHistory.Count) 58 | { 59 | failed = true; 60 | builder.AppendLine(nameof(spy.OperationBlockStartActionHistory)); 61 | } 62 | 63 | if (0 == spy.SemanticModelActionHistory.Count) 64 | { 65 | failed = true; 66 | builder.AppendLine(nameof(spy.SemanticModelActionHistory)); 67 | } 68 | 69 | if (0 == spy.SymbolActionHistory.Count) 70 | { 71 | failed = true; 72 | builder.AppendLine(nameof(spy.SymbolActionHistory)); 73 | } 74 | 75 | if (0 == spy.SymbolStartActionHistory.Count) 76 | { 77 | failed = true; 78 | builder.AppendLine(nameof(spy.SymbolStartActionHistory)); 79 | } 80 | 81 | if (0 == spy.SyntaxNodeActionHistory.Count) 82 | { 83 | failed = true; 84 | builder.AppendLine(nameof(spy.SyntaxNodeActionHistory)); 85 | } 86 | 87 | if (0 == spy.SyntaxTreeActionHistory.Count) 88 | { 89 | failed = true; 90 | builder.AppendLine(nameof(spy.SyntaxTreeActionHistory)); 91 | } 92 | 93 | MSTestAssert.IsFalse(failed, builder.ToString()); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /tests/Dena.CodeAnalysis.Testing.Tests/StubAnalyzerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using MSTestAssert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert; 4 | 5 | 6 | namespace Dena.CodeAnalysis.CSharp.Testing 7 | { 8 | [TestClass] 9 | public class StubAnalyzerTests 10 | { 11 | [TestMethod] 12 | public async Task WhenGivenAnyCodes_ItShouldGetToCallCodeBlockAction() 13 | { 14 | var callCount = 0; 15 | var stubAnalyzer = new StubAnalyzer( 16 | new AnalyzerActions 17 | { 18 | CodeBlockAction = _ => callCount++ 19 | } 20 | ); 21 | 22 | await DiagnosticAnalyzerRunner.Run(stubAnalyzer, codes: ExampleCode.DiagnosticsFreeClassLibrary); 23 | 24 | MSTestAssert.AreNotEqual(0, callCount); 25 | } 26 | 27 | 28 | [TestMethod] 29 | public async Task WhenGivenAnyCodes_ItShouldGetToCallCodeBlockStartAction() 30 | { 31 | var callCount = 0; 32 | var stubAnalyzer = new StubAnalyzer( 33 | new AnalyzerActions 34 | { 35 | CodeBlockStartAction = _ => callCount++ 36 | } 37 | ); 38 | 39 | await DiagnosticAnalyzerRunner.Run(stubAnalyzer, codes: ExampleCode.DiagnosticsFreeClassLibrary); 40 | 41 | MSTestAssert.AreNotEqual(0, callCount); 42 | } 43 | 44 | 45 | [TestMethod] 46 | public async Task WhenGivenAnyCodes_ItShouldGetToCallCompilationAction() 47 | { 48 | var callCount = 0; 49 | var stubAnalyzer = new StubAnalyzer( 50 | new AnalyzerActions 51 | { 52 | CompilationAction = _ => callCount++ 53 | } 54 | ); 55 | 56 | await DiagnosticAnalyzerRunner.Run(stubAnalyzer, codes: ExampleCode.DiagnosticsFreeClassLibrary); 57 | 58 | MSTestAssert.AreNotEqual(0, callCount); 59 | } 60 | 61 | 62 | [TestMethod] 63 | public async Task WhenGivenAnyCodes_ItShouldGetToCallCompilationStartAction() 64 | { 65 | var callCount = 0; 66 | var stubAnalyzer = new StubAnalyzer( 67 | new AnalyzerActions 68 | { 69 | CompilationStartAction = _ => callCount++ 70 | } 71 | ); 72 | 73 | await DiagnosticAnalyzerRunner.Run(stubAnalyzer, codes: ExampleCode.DiagnosticsFreeClassLibrary); 74 | 75 | MSTestAssert.AreNotEqual(0, callCount); 76 | } 77 | 78 | 79 | [TestMethod] 80 | public async Task WhenGivenAnyCodes_ItShouldGetToCallOperationAction() 81 | { 82 | var callCount = 0; 83 | var stubAnalyzer = new StubAnalyzer( 84 | new AnalyzerActions 85 | { 86 | OperationAction = _ => callCount++ 87 | } 88 | ); 89 | 90 | await DiagnosticAnalyzerRunner.Run(stubAnalyzer, codes: ExampleCode.DiagnosticsFreeClassLibrary); 91 | 92 | MSTestAssert.AreNotEqual(0, callCount); 93 | } 94 | 95 | 96 | [TestMethod] 97 | public async Task WhenGivenAnyCodes_ItShouldGetToCallOperationBlockAction() 98 | { 99 | var callCount = 0; 100 | var stubAnalyzer = new StubAnalyzer( 101 | new AnalyzerActions 102 | { 103 | OperationBlockAction = _ => callCount++ 104 | } 105 | ); 106 | 107 | await DiagnosticAnalyzerRunner.Run(stubAnalyzer, codes: ExampleCode.DiagnosticsFreeClassLibrary); 108 | 109 | MSTestAssert.AreNotEqual(0, callCount); 110 | } 111 | 112 | 113 | [TestMethod] 114 | public async Task WhenGivenAnyCodes_ItShouldGetToCallOperationBlockStartAction() 115 | { 116 | var callCount = 0; 117 | var stubAnalyzer = new StubAnalyzer( 118 | new AnalyzerActions 119 | { 120 | OperationBlockStartAction = _ => callCount++ 121 | } 122 | ); 123 | 124 | await DiagnosticAnalyzerRunner.Run(stubAnalyzer, codes: ExampleCode.DiagnosticsFreeClassLibrary); 125 | 126 | MSTestAssert.AreNotEqual(0, callCount); 127 | } 128 | 129 | 130 | [TestMethod] 131 | public async Task WhenGivenAnyCodes_ItShouldGetToCallSemanticModelAction() 132 | { 133 | var callCount = 0; 134 | var stubAnalyzer = new StubAnalyzer( 135 | new AnalyzerActions 136 | { 137 | SemanticModelAction = _ => callCount++ 138 | } 139 | ); 140 | 141 | await DiagnosticAnalyzerRunner.Run(stubAnalyzer, codes: ExampleCode.DiagnosticsFreeClassLibrary); 142 | 143 | MSTestAssert.AreNotEqual(0, callCount); 144 | } 145 | 146 | 147 | [TestMethod] 148 | public async Task WhenGivenAnyCodes_ItShouldGetToCallSymbolAction() 149 | { 150 | var callCount = 0; 151 | var stubAnalyzer = new StubAnalyzer( 152 | new AnalyzerActions 153 | { 154 | SymbolAction = _ => callCount++ 155 | } 156 | ); 157 | 158 | await DiagnosticAnalyzerRunner.Run(stubAnalyzer, codes: ExampleCode.DiagnosticsFreeClassLibrary); 159 | 160 | MSTestAssert.AreNotEqual(0, callCount); 161 | } 162 | 163 | 164 | [TestMethod] 165 | public async Task WhenGivenAnyCodes_ItShouldGetToCallSymbolStartAction() 166 | { 167 | var callCount = 0; 168 | var stubAnalyzer = new StubAnalyzer( 169 | new AnalyzerActions 170 | { 171 | SymbolStartAction = _ => callCount++ 172 | } 173 | ); 174 | 175 | await DiagnosticAnalyzerRunner.Run(stubAnalyzer, codes: ExampleCode.DiagnosticsFreeClassLibrary); 176 | 177 | MSTestAssert.AreNotEqual(0, callCount); 178 | } 179 | 180 | 181 | [TestMethod] 182 | public async Task WhenGivenAnyCodes_ItShouldGetToCallSyntaxNodeAction() 183 | { 184 | var callCount = 0; 185 | var stubAnalyzer = new StubAnalyzer( 186 | new AnalyzerActions 187 | { 188 | SyntaxNodeAction = _ => callCount++ 189 | } 190 | ); 191 | 192 | await DiagnosticAnalyzerRunner.Run(stubAnalyzer, codes: ExampleCode.DiagnosticsFreeClassLibrary); 193 | 194 | MSTestAssert.AreNotEqual(0, callCount); 195 | } 196 | 197 | 198 | [TestMethod] 199 | public async Task WhenGivenAnyCodes_ItShouldGetToCallSyntaxTreeAction() 200 | { 201 | var callCount = 0; 202 | var stubAnalyzer = new StubAnalyzer( 203 | new AnalyzerActions 204 | { 205 | SyntaxTreeAction = _ => callCount++ 206 | } 207 | ); 208 | 209 | await DiagnosticAnalyzerRunner.Run(stubAnalyzer, codes: ExampleCode.DiagnosticsFreeClassLibrary); 210 | 211 | MSTestAssert.AreNotEqual(0, callCount); 212 | } 213 | } 214 | } -------------------------------------------------------------------------------- /tests/Dena.CodeAnalysis.Testing.Tests/TestData/Example.txt: -------------------------------------------------------------------------------- 1 | hello-world 2 | -------------------------------------------------------------------------------- /tests/Dena.CodeAnalysis.Testing.Tests/TestData/Instantiate.txt: -------------------------------------------------------------------------------- 1 | 2 | using System.Collections; 3 | namespace BanNonGenericCollectionsAnalyzer.Test.TestData.OperationAction 4 | { 5 | public class Instantiate 6 | { 7 | private void Hoge() 8 | { 9 | var fuga = {|new Stack()|DENA001|Do not use Stack because non-generic collection|}; 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /tests/Dena.CodeAnalysis.Testing.Tests/TestData/TestData.cs: -------------------------------------------------------------------------------- 1 | // (c) 2021 DeNA Co., Ltd. 2 | 3 | using System.IO; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace Dena.CodeAnalysis.CSharp.Testing 7 | { 8 | public static class TestData 9 | { 10 | public static string GetPath(string fileName) => Path.Combine(Path.GetDirectoryName(GetFilePath())!, fileName); 11 | 12 | private static string GetFilePath([CallerFilePath] string path = "") => path; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/Dena.CodeAnalysis.Testing.Tests/TestDataParserTest.cs: -------------------------------------------------------------------------------- 1 | // (c) 2021 DeNA Co., Ltd. 2 | 3 | using System.Linq; 4 | using Microsoft.CodeAnalysis.Text; 5 | using NUnit.Framework; 6 | using NUnitAssert = NUnit.Framework.Assert; 7 | 8 | namespace Dena.CodeAnalysis.CSharp.Testing 9 | { 10 | [TestFixture] 11 | public class TestDataParserTest 12 | { 13 | private const string TestData = @" 14 | using System.Collections; 15 | namespace BanNonGenericCollectionsAnalyzer.Test.TestData.OperationAction 16 | { 17 | public class Instantiate 18 | { 19 | private void Hoge() 20 | { 21 | var fuga = {|new Stack()|DENA001|Do not use Stack because non-generic collection|}; 22 | } 23 | } 24 | }"; 25 | 26 | [Test] 27 | public void CreateSourceAndExpectedDiagnostic_HasOneReportPart_extractSourceCode() 28 | { 29 | var (source, _) = 30 | TestDataParser.CreateSourceAndExpectedDiagnostic(TestData); 31 | NUnitAssert.That(source, Is.EqualTo(@" 32 | using System.Collections; 33 | namespace BanNonGenericCollectionsAnalyzer.Test.TestData.OperationAction 34 | { 35 | public class Instantiate 36 | { 37 | private void Hoge() 38 | { 39 | var fuga = new Stack(); 40 | } 41 | } 42 | }" 43 | )); 44 | } 45 | 46 | [Test] 47 | public void CreateSourceAndExpectedDiagnosticFromFile_HasOneReportPart_CreateDiagnostic() 48 | { 49 | var (_, actualDiagnostics) = 50 | TestDataParser.CreateSourceAndExpectedDiagnostic(TestData); 51 | NUnitAssert.That(actualDiagnostics, Has.Count.EqualTo(1)); 52 | NUnitAssert.Multiple(() => 53 | { 54 | NUnitAssert.That(actualDiagnostics[0].Id, Is.EqualTo("DENA001")); 55 | NUnitAssert.That(actualDiagnostics[0].GetMessage(), 56 | Is.EqualTo("Do not use Stack because non-generic collection")); 57 | NUnitAssert.That(actualDiagnostics[0].Location.SourceSpan, Is.EqualTo(new TextSpan(198, 11))); 58 | }); 59 | } 60 | 61 | [Test] 62 | public void ExtractMakerFromTestData_HasOneReportPart_ReturnReportPoint() 63 | { 64 | var actual = TestDataParser.ExtractMaker(@" 65 | using System.Collections; 66 | namespace BanNonGenericCollectionsAnalyzer.Test.TestData.OperationAction 67 | { 68 | public class Instantiate 69 | { 70 | private void Hoge() 71 | { 72 | var fuga = {|new Stack()|DENA001|Do not use Stack because non-generic collection|}; 73 | } 74 | } 75 | }").ToList(); 76 | 77 | NUnitAssert.That(actual.Count, Is.EqualTo(1)); 78 | NUnitAssert.Multiple(() => 79 | { 80 | NUnitAssert.That(actual[0].target, Is.EqualTo("new Stack()")); 81 | NUnitAssert.That(actual[0].ddid, Is.EqualTo("DENA001")); 82 | NUnitAssert.That(actual[0].msg, Is.EqualTo("Do not use Stack because non-generic collection")); 83 | }); 84 | } 85 | 86 | [Test] 87 | public void ExtractMakerFromTestData_ContainsBracketsInMarker_ReturnReportPoint() 88 | { 89 | var actual = TestDataParser.ExtractMaker(@" 90 | using System.Collections; 91 | namespace BanNonGenericCollectionsAnalyzer.Test.TestData.OperationAction 92 | { 93 | public class Instantiate 94 | { 95 | private void Hoge() 96 | { 97 | var fuga = {|new Stack[5] {}|DENA001|Do not use Stack because non-generic collection|}; 98 | } 99 | } 100 | }").ToList(); 101 | 102 | NUnitAssert.That(actual, Has.Count.EqualTo(1)); 103 | NUnitAssert.Multiple(() => 104 | { 105 | NUnitAssert.That(actual[0].target, Is.EqualTo("new Stack[5] {}")); 106 | NUnitAssert.That(actual[0].ddid, Is.EqualTo("DENA001")); 107 | NUnitAssert.That(actual[0].msg, Is.EqualTo("Do not use Stack because non-generic collection")); 108 | }); 109 | } 110 | 111 | [Test] 112 | public void ExtractMakerFromTestData_HasNoReportPart_ReturnEmptyList() 113 | { 114 | var actual = TestDataParser.ExtractMaker(@" 115 | using System.Collections; 116 | namespace BanNonGenericCollectionsAnalyzer.Test.TestData.OperationAction 117 | { 118 | public class Instantiate 119 | { 120 | private void Hoge() 121 | { 122 | var fuga = new Stack(); 123 | } 124 | } 125 | }").Any(); 126 | 127 | NUnitAssert.That(actual, Is.False); 128 | } 129 | 130 | [Test] 131 | public void ExtractMakerFromTestData_WithTwoReportPoints_ReturnTwoReportPoints() 132 | { 133 | var actual = TestDataParser.ExtractMaker(@" 134 | using System.Collections; 135 | namespace BanNonGenericCollectionsAnalyzer.Test.TestData.OperationAction 136 | { 137 | public class Instantiate 138 | { 139 | private void Hoge() 140 | { 141 | var fuga = {|new Stack()|DENA001|Do not use Stack because non-generic collection|}; 142 | var moge = {|new ArrayList()|DENA002|Do not use ArrayList because non-generic collection|}; 143 | } 144 | } 145 | }").ToList(); 146 | 147 | NUnitAssert.That(actual, Has.Count.EqualTo(2)); 148 | NUnitAssert.Multiple(() => 149 | { 150 | NUnitAssert.That(actual[0].target, Is.EqualTo("new Stack()")); 151 | NUnitAssert.That(actual[0].ddid, Is.EqualTo("DENA001")); 152 | NUnitAssert.That(actual[0].msg, Is.EqualTo("Do not use Stack because non-generic collection")); 153 | NUnitAssert.That(actual[1].target, Is.EqualTo("new ArrayList()")); 154 | NUnitAssert.That(actual[1].ddid, Is.EqualTo("DENA002")); 155 | NUnitAssert.That(actual[1].msg, Is.EqualTo("Do not use ArrayList because non-generic collection")); 156 | }); 157 | } 158 | 159 | [Test] 160 | public void ExtractMakerFromTestData_TwoReportPointsOnTheSameLine_ReturnTwoReportPoints() 161 | { 162 | var actual = TestDataParser.ExtractMaker(@" 163 | using System.Collections; 164 | namespace BanNonGenericCollectionsAnalyzer.Test.TestData.OperationAction 165 | { 166 | public class Instantiate 167 | { 168 | private void Hoge() 169 | { 170 | {|Banned1|DENA001|Message1|} = {|Banned2|DENA002|Message2|}; 171 | } 172 | } 173 | }").ToList(); 174 | 175 | NUnitAssert.That(actual, Has.Count.EqualTo(2)); 176 | NUnitAssert.Multiple(() => 177 | { 178 | NUnitAssert.That(actual[0].target, Is.EqualTo("Banned1")); 179 | NUnitAssert.That(actual[0].ddid, Is.EqualTo("DENA001")); 180 | NUnitAssert.That(actual[0].msg, Is.EqualTo("Message1")); 181 | NUnitAssert.That(actual[1].target, Is.EqualTo("Banned2")); 182 | NUnitAssert.That(actual[1].ddid, Is.EqualTo("DENA002")); 183 | NUnitAssert.That(actual[1].msg, Is.EqualTo("Message2")); 184 | }); 185 | } 186 | 187 | [Test] 188 | public void ExtractMakerFromTestData_OneCharacterPerElementInEachReportPart_ReturnOneReportPoint() 189 | { 190 | var actual = TestDataParser.ExtractMaker(@"{|A|B|C|}").ToList(); 191 | 192 | NUnitAssert.That(actual, Has.Count.EqualTo(1)); 193 | NUnitAssert.Multiple(() => 194 | { 195 | NUnitAssert.That(actual[0].target, Is.EqualTo("A")); 196 | NUnitAssert.That(actual[0].ddid, Is.EqualTo("B")); 197 | NUnitAssert.That(actual[0].msg, Is.EqualTo("C")); 198 | }); 199 | } 200 | 201 | [Test] 202 | public void ExtractMakerFromTestData_IncludeVerticalBarsInMarker_NoReturnReportPoint() 203 | { 204 | var actual = TestDataParser.ExtractMaker(@"{||A||B||C|}").ToList(); 205 | 206 | NUnitAssert.That(actual, Is.Empty); 207 | } 208 | 209 | [Test] 210 | public void CreateLinePositionStart_BeginningReportParts_GetCorrectPosition() 211 | { 212 | var actual = TestDataParser.CreateLinePositionStart("aaa\nbbbc\nccc", "aaa"); 213 | NUnitAssert.Multiple(() => 214 | { 215 | NUnitAssert.That(actual.Line, Is.EqualTo(0)); 216 | NUnitAssert.That(actual.Character, Is.EqualTo(0)); 217 | }); 218 | } 219 | 220 | [Test] 221 | public void CreateLinePositionStart_ReportPartIsOnTheFirstLine_GetCorrectPosition() 222 | { 223 | var actual = TestDataParser.CreateLinePositionStart("hogeaaa\nbbbc\nccc", "aaa"); 224 | NUnitAssert.Multiple(() => 225 | { 226 | NUnitAssert.That(actual.Line, Is.EqualTo(0)); 227 | NUnitAssert.That(actual.Character, Is.EqualTo(4)); 228 | }); 229 | } 230 | 231 | [Test] 232 | public void CreateLinePositionStart_ReportPartExistsMiddleLineFirstCol_GetCorrectPosition() 233 | { 234 | var actual = TestDataParser.CreateLinePositionStart("aaa\nbcbb\nccc", "bc"); 235 | NUnitAssert.Multiple(() => 236 | { 237 | NUnitAssert.That(actual.Line, Is.EqualTo(1)); 238 | NUnitAssert.That(actual.Character, Is.EqualTo(0)); 239 | }); 240 | } 241 | 242 | [Test] 243 | public void CreateLinePositionStart_ReportPartExistsMiddleLineLastCol_GetCorrectPosition() 244 | { 245 | var actual = TestDataParser.CreateLinePositionStart("aaa\nbbbc\nccc", "bc"); 246 | NUnitAssert.Multiple(() => 247 | { 248 | NUnitAssert.That(actual.Line, Is.EqualTo(1)); 249 | NUnitAssert.That(actual.Character, Is.EqualTo(2)); 250 | }); 251 | } 252 | 253 | [Test] 254 | public void CreateLinePositionStart_ReportPointExistsFirstLine_GetCorrectPosition() 255 | { 256 | var actual = TestDataParser.CreateLinePositionStart("aaa\nbbbc\nccc", "ccc"); 257 | NUnitAssert.Multiple(() => 258 | { 259 | NUnitAssert.That(actual.Line, Is.EqualTo(2)); 260 | NUnitAssert.That(actual.Character, Is.EqualTo(0)); 261 | }); 262 | } 263 | 264 | [Test] 265 | public void CreateLocation_ReportPointExistsMiddleLine_GetCorrectSourceSpan() 266 | { 267 | var actual = TestDataParser.CreateLocation("aaa\nbbbc\nccc", "bc", 2); 268 | NUnitAssert.Multiple(() => 269 | { 270 | NUnitAssert.That(actual.SourceSpan.Start, Is.EqualTo(6)); 271 | NUnitAssert.That(actual.SourceSpan.End, Is.EqualTo(8)); 272 | }); 273 | } 274 | 275 | [Test] 276 | public void CreateLocation_ReportPointExistsLastLine_GetCorrectSourceSpan() 277 | { 278 | var actual = TestDataParser.CreateLocation("aaa\nbbbc\nccc", "ccc", 3); 279 | NUnitAssert.Multiple(() => 280 | { 281 | NUnitAssert.That(actual.SourceSpan.Start, Is.EqualTo(9)); 282 | NUnitAssert.That(actual.SourceSpan.End, Is.EqualTo(12)); 283 | }); 284 | } 285 | } 286 | } 287 | --------------------------------------------------------------------------------