├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── benchmark.yml │ ├── ci.yml │ ├── integration.yml │ └── release.yml ├── .gitignore ├── FluentAssertions.Analyzers.sln ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── assets ├── FluentAssertions.png ├── demo.gif └── images │ └── fluent_assertions_analyzers_large_horizontal.svg ├── docs ├── FluentAssertionsAnalyzer.md ├── MsTestAnalyzer.md ├── Nunit3Analyzer.md ├── Nunit4Analyzer.md └── XunitAnalyzer.md ├── scripts ├── generate-docs.ps1 └── run-docs-tests.ps1 └── src ├── Directory.Packages.props ├── FluentAssertions.Analyzers.BenchmarkTests ├── FluentAssertions.Analyzers.BenchmarkTests.csproj ├── FluentAssertionsBenchmarks.cs └── Program.cs ├── FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Nunit3 ├── ExpectedAssertionExceptionAttribute.cs ├── FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Nunit3.csproj ├── Nunit3AnalyzerTests.cs └── Program.cs ├── FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Nunit4 ├── ExpectedAssertionExceptionAttribute.cs ├── FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Nunit4.csproj ├── Nunit4AnalyzerTests.cs └── Program.cs ├── FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Xunit ├── ExpectedAssertionExceptionAttribute.cs ├── FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Xunit.csproj ├── Program.cs └── XunitAnalyzerTests.cs ├── FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs ├── FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.csproj ├── FluentAssertionsAnalyzerTests.cs ├── MsTestAnalyzerTests.cs └── Program.cs ├── FluentAssertions.Analyzers.FluentAssertionAnalyzerDocsGenerator ├── DocsGenerator.cs ├── DocsVerifier.cs ├── FluentAssertionAnalyzerDocsUtils.cs ├── FluentAssertions.Analyzers.FluentAssertionAnalyzerDocsGenerator.csproj └── ProgramUtils.cs ├── FluentAssertions.Analyzers.TestUtils ├── CodeAnalyzersUtils.cs ├── CsProjectArguments.cs ├── CsProjectGenerator.cs ├── FluentAssertions.Analyzers.TestUtils.csproj ├── GenerateCode.cs ├── PackageReference.cs ├── SolutionExtensions.cs ├── TargetFramework.cs └── TestAnalyzerConfigOptionsProvider.cs ├── FluentAssertions.Analyzers.Tests ├── CodeFixVerifierArguments.cs ├── DiagnosticResult.cs ├── DiagnosticVerifier.cs ├── DiagnosticVerifierArguments.cs ├── FluentAssertions.Analyzers.Tests.csproj ├── TestAttributes.cs ├── TestConfiguration.cs └── Tips │ ├── AsyncVoidTests.cs │ ├── CollectionTests.cs │ ├── DictionaryTests.cs │ ├── ExceptionsTests.cs │ ├── FluentAssertionsTests.cs │ ├── MsTestTests.cs │ ├── NullConditionalAssertionTests.cs │ ├── NumericTests.cs │ ├── NunitTests.cs │ ├── SanityTests.cs │ ├── ShouldEqualsTests.cs │ ├── StringTests.cs │ └── XunitTests.cs └── FluentAssertions.Analyzers ├── AnalyzerReleases.Shipped.md ├── AnalyzerReleases.Unshipped.md ├── Constants.cs ├── FluentAssertions.Analyzers.csproj ├── Tips ├── AssertAnalyzer.cs ├── AsyncVoid.cs ├── CodeFixProviderBase.cs ├── DiagnosticMetadata.cs ├── DocumentEditorUtils.cs ├── Editing │ ├── CreateEquivalencyAssertionOptionsLambda.cs │ ├── EditAction.cs │ ├── IEditAction.cs │ ├── SkipInvocationNodeAction.cs │ ├── SkipMemberAccessNodeAction.cs │ ├── SubjectShouldAssertionAction.cs │ └── SubjectShouldGenericAssertionAction.cs ├── FluentAssertionsAnalyzer.Utils.cs ├── FluentAssertionsAnalyzer.cs ├── FluentAssertionsCodeFixProvider.EditorUtils.cs ├── FluentAssertionsCodeFixProvider.Exceptions.cs ├── FluentAssertionsCodeFixProvider.cs ├── FluentAssertionsEditAction.cs ├── FluentChainedAssertionEditAction.cs ├── MsTestCodeFixProvider.cs ├── NunitCodeFixProvider.cs ├── TestingFrameworkCodeFixProvider.cs └── XunitCodeFixProvider.cs ├── Utilities ├── OperartionExtensions.cs └── TypesExtensions.cs └── tools ├── install.ps1 └── uninstall.ps1 /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [meir017] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://www.buymeacoffee.com/meirblachmq'] 13 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: Benchmark tests 2 | on: [pull_request] 3 | 4 | permissions: 5 | contents: write 6 | deployments: write 7 | 8 | jobs: 9 | benchmark: 10 | name: Performance regression check 11 | runs-on: ubuntu-22.04 12 | if: github.repository == 'fluentassertions/fluentassertions.analyzers' 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Setup .NET 16 | uses: actions/setup-dotnet@v4 17 | with: 18 | dotnet-version: '6.x' 19 | - name: Setup .NET 20 | uses: actions/setup-dotnet@v4 21 | with: 22 | dotnet-version: '7.x' 23 | - name: Run benchmark 24 | run: cd src/FluentAssertions.Analyzers.BenchmarkTests && dotnet run -c Release --exporters json --filter '*' 25 | 26 | - name: Store benchmark result 27 | uses: benchmark-action/github-action-benchmark@v1.18.0 28 | with: 29 | name: FluentAssertions.Analyzers Benchmark 30 | tool: 'benchmarkdotnet' 31 | output-file-path: src/FluentAssertions.Analyzers.BenchmarkTests/BenchmarkDotNet.Artifacts/results/FluentAssertions.Analyzers.BenchmarkTests.FluentAssertionsBenchmarks-report.json 32 | github-token: ${{ secrets.GITHUB_TOKEN }} 33 | auto-push: true 34 | alert-threshold: '200%' 35 | comment-on-alert: true 36 | fail-on-alert: true 37 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | push: 7 | branches: 8 | - main 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | os: [ubuntu-22.04, windows-2022, macos-14] 14 | config: [Debug, Release] 15 | runs-on: ${{ matrix.os }} 16 | env: 17 | NUGET_CERT_REVOCATION_MODE: offline 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - name: Setup .NET 6 23 | uses: actions/setup-dotnet@v4 24 | with: 25 | dotnet-version: 6.0.x 26 | - run: dotnet build 27 | - run: dotnet test src/FluentAssertions.Analyzers.Tests --configuration Release --filter 'TestCategory=Completed' /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura 28 | - run: dotnet pack src/FluentAssertions.Analyzers/FluentAssertions.Analyzers.csproj 29 | -------------------------------------------------------------------------------- /.github/workflows/integration.yml: -------------------------------------------------------------------------------- 1 | name: Integration 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | push: 7 | branches: 8 | - main 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | os: [ubuntu-22.04, windows-2022, macos-14] 14 | runs-on: ${{ matrix.os }} 15 | env: 16 | NUGET_CERT_REVOCATION_MODE: offline 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - name: Setup .NET 8 22 | uses: actions/setup-dotnet@v4 23 | with: 24 | dotnet-version: 8.0.x 25 | - run: dotnet test src/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs # before formatting 26 | - name: Run the docs generator 27 | shell: pwsh 28 | run: ./scripts/generate-docs.ps1 -ValidateNoChanges 29 | - name: Run the docs tests and format, then test again 30 | shell: pwsh 31 | run: ./scripts/run-docs-tests.ps1 -FormatAndExecuteTestsAgain 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish NuGet package 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-22.04 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Setup .NET Core 16 | uses: actions/setup-dotnet@v4 17 | with: 18 | dotnet-version: '6.x' 19 | 20 | - name: Restore dependencies 21 | run: dotnet restore 22 | 23 | - name: Build & Pack NuGet package 24 | shell: pwsh 25 | run: | 26 | $tag = $env:GITHUB_REF.Substring('refs/tags/v'.Length) 27 | dotnet pack src/FluentAssertions.Analyzers/ --output out --configuration Release --include-symbols -p:Version=$tag 28 | 29 | - name: Publish NuGet package 30 | shell: pwsh 31 | run: | 32 | $symbols = Get-ChildItem out/*.symbols.nupkg | ForEach-Object FullName; 33 | foreach ($symbol in $symbols) { 34 | Write-Host "Pushing symbols $symbol"; 35 | $nupkg = $symbol.Replace(".symbols.nupkg",".nupkg"); 36 | Write-Host "Pushing nupkg $nupkg"; 37 | dotnet nuget push $nupkg --skip-duplicate --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json; 38 | } 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | /tools 263 | -------------------------------------------------------------------------------- /FluentAssertions.Analyzers.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.8.34601.278 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentAssertions.Analyzers.Tests", "src\FluentAssertions.Analyzers.Tests\FluentAssertions.Analyzers.Tests.csproj", "{979824BD-5936-4969-B43B-BF613B3C0C5F}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{1D2F0A0B-7B98-49D6-BD83-E750A2DD7FD0}" 9 | ProjectSection(SolutionItems) = preProject 10 | .gitattributes = .gitattributes 11 | .gitignore = .gitignore 12 | .github\workflows\ci.yml = .github\workflows\ci.yml 13 | LICENSE = LICENSE 14 | README.md = README.md 15 | EndProjectSection 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentAssertions.Analyzers", "src\FluentAssertions.Analyzers\FluentAssertions.Analyzers.csproj", "{3BA672F7-00D8-4E77-89A0-D46DD99D35AA}" 18 | EndProject 19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentAssertions.Analyzers.TestUtils", "src\FluentAssertions.Analyzers.TestUtils\FluentAssertions.Analyzers.TestUtils.csproj", "{BD9FC8CC-C23D-4ECC-A926-4BE35C78D338}" 20 | EndProject 21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentAssertions.Analyzers.BenchmarkTests", "src\FluentAssertions.Analyzers.BenchmarkTests\FluentAssertions.Analyzers.BenchmarkTests.csproj", "{FE6D8A05-1383-4BCD-AD65-2EF741E48F44}" 22 | EndProject 23 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8EFE7955-E63C-4055-A9FF-76C7CE0A1151}" 24 | EndProject 25 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs", "src\FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs\FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.csproj", "{2F84FE09-8CB4-48AE-A119-671C509213CF}" 26 | EndProject 27 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{48BE11CB-8BAF-4099-9D93-AE9BB74BB190}" 28 | EndProject 29 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{72192514-FA22-4699-8EE1-39D34CFE1025}" 30 | EndProject 31 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FluentAssertions.Analyzers.FluentAssertionAnalyzerDocsGenerator", "src\FluentAssertions.Analyzers.FluentAssertionAnalyzerDocsGenerator\FluentAssertions.Analyzers.FluentAssertionAnalyzerDocsGenerator.csproj", "{2871B22C-AEFC-4C33-9BBF-695699E61B57}" 32 | EndProject 33 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Nunit4", "src\FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Nunit4\FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Nunit4.csproj", "{A819A8A5-C8F7-46AD-B0F2-44868070A188}" 34 | EndProject 35 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Nunit3", "src\FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Nunit3\FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Nunit3.csproj", "{FBCDB423-7729-42CD-9279-2D0BECE37907}" 36 | EndProject 37 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Xunit", "src\FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Xunit\FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Xunit.csproj", "{94D58487-F555-45A1-8076-511D1792C1DE}" 38 | EndProject 39 | Global 40 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 41 | Debug|Any CPU = Debug|Any CPU 42 | Release|Any CPU = Release|Any CPU 43 | EndGlobalSection 44 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 45 | {979824BD-5936-4969-B43B-BF613B3C0C5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {979824BD-5936-4969-B43B-BF613B3C0C5F}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {979824BD-5936-4969-B43B-BF613B3C0C5F}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {979824BD-5936-4969-B43B-BF613B3C0C5F}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {3BA672F7-00D8-4E77-89A0-D46DD99D35AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {3BA672F7-00D8-4E77-89A0-D46DD99D35AA}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {3BA672F7-00D8-4E77-89A0-D46DD99D35AA}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {3BA672F7-00D8-4E77-89A0-D46DD99D35AA}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {BD9FC8CC-C23D-4ECC-A926-4BE35C78D338}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {BD9FC8CC-C23D-4ECC-A926-4BE35C78D338}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {BD9FC8CC-C23D-4ECC-A926-4BE35C78D338}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {BD9FC8CC-C23D-4ECC-A926-4BE35C78D338}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {FE6D8A05-1383-4BCD-AD65-2EF741E48F44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {FE6D8A05-1383-4BCD-AD65-2EF741E48F44}.Debug|Any CPU.Build.0 = Debug|Any CPU 59 | {FE6D8A05-1383-4BCD-AD65-2EF741E48F44}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {FE6D8A05-1383-4BCD-AD65-2EF741E48F44}.Release|Any CPU.Build.0 = Release|Any CPU 61 | {2F84FE09-8CB4-48AE-A119-671C509213CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 62 | {2F84FE09-8CB4-48AE-A119-671C509213CF}.Debug|Any CPU.Build.0 = Debug|Any CPU 63 | {2F84FE09-8CB4-48AE-A119-671C509213CF}.Release|Any CPU.ActiveCfg = Release|Any CPU 64 | {2F84FE09-8CB4-48AE-A119-671C509213CF}.Release|Any CPU.Build.0 = Release|Any CPU 65 | {2871B22C-AEFC-4C33-9BBF-695699E61B57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 66 | {2871B22C-AEFC-4C33-9BBF-695699E61B57}.Debug|Any CPU.Build.0 = Debug|Any CPU 67 | {2871B22C-AEFC-4C33-9BBF-695699E61B57}.Release|Any CPU.ActiveCfg = Release|Any CPU 68 | {2871B22C-AEFC-4C33-9BBF-695699E61B57}.Release|Any CPU.Build.0 = Release|Any CPU 69 | {A819A8A5-C8F7-46AD-B0F2-44868070A188}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 70 | {A819A8A5-C8F7-46AD-B0F2-44868070A188}.Debug|Any CPU.Build.0 = Debug|Any CPU 71 | {A819A8A5-C8F7-46AD-B0F2-44868070A188}.Release|Any CPU.ActiveCfg = Release|Any CPU 72 | {A819A8A5-C8F7-46AD-B0F2-44868070A188}.Release|Any CPU.Build.0 = Release|Any CPU 73 | {FBCDB423-7729-42CD-9279-2D0BECE37907}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 74 | {FBCDB423-7729-42CD-9279-2D0BECE37907}.Debug|Any CPU.Build.0 = Debug|Any CPU 75 | {FBCDB423-7729-42CD-9279-2D0BECE37907}.Release|Any CPU.ActiveCfg = Release|Any CPU 76 | {FBCDB423-7729-42CD-9279-2D0BECE37907}.Release|Any CPU.Build.0 = Release|Any CPU 77 | {94D58487-F555-45A1-8076-511D1792C1DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 78 | {94D58487-F555-45A1-8076-511D1792C1DE}.Debug|Any CPU.Build.0 = Debug|Any CPU 79 | {94D58487-F555-45A1-8076-511D1792C1DE}.Release|Any CPU.ActiveCfg = Release|Any CPU 80 | {94D58487-F555-45A1-8076-511D1792C1DE}.Release|Any CPU.Build.0 = Release|Any CPU 81 | EndGlobalSection 82 | GlobalSection(SolutionProperties) = preSolution 83 | HideSolutionNode = FALSE 84 | EndGlobalSection 85 | GlobalSection(NestedProjects) = preSolution 86 | {979824BD-5936-4969-B43B-BF613B3C0C5F} = {48BE11CB-8BAF-4099-9D93-AE9BB74BB190} 87 | {3BA672F7-00D8-4E77-89A0-D46DD99D35AA} = {8EFE7955-E63C-4055-A9FF-76C7CE0A1151} 88 | {BD9FC8CC-C23D-4ECC-A926-4BE35C78D338} = {48BE11CB-8BAF-4099-9D93-AE9BB74BB190} 89 | {FE6D8A05-1383-4BCD-AD65-2EF741E48F44} = {48BE11CB-8BAF-4099-9D93-AE9BB74BB190} 90 | {2F84FE09-8CB4-48AE-A119-671C509213CF} = {72192514-FA22-4699-8EE1-39D34CFE1025} 91 | {2871B22C-AEFC-4C33-9BBF-695699E61B57} = {72192514-FA22-4699-8EE1-39D34CFE1025} 92 | {A819A8A5-C8F7-46AD-B0F2-44868070A188} = {8EFE7955-E63C-4055-A9FF-76C7CE0A1151} 93 | {FBCDB423-7729-42CD-9279-2D0BECE37907} = {8EFE7955-E63C-4055-A9FF-76C7CE0A1151} 94 | {94D58487-F555-45A1-8076-511D1792C1DE} = {8EFE7955-E63C-4055-A9FF-76C7CE0A1151} 95 | EndGlobalSection 96 | GlobalSection(ExtensibilityGlobals) = postSolution 97 | SolutionGuid = {4BF3D005-625C-4CEC-B3FB-298B956402BE} 98 | EndGlobalSection 99 | EndGlobal 100 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | [Description of the issue] 4 | 5 | ### Complete minimal example reproducing the issue 6 | 7 | Complete means the code snippet can be copied into a unit test method in a fresh C# project and run. 8 | Minimal means it is stripped from code not related to reproducing the issue. 9 | 10 | E.g. 11 | 12 | ```csharp 13 | // Arrange 14 | string input = "MyString"; 15 | 16 | // Act 17 | char result = input[0]; 18 | 19 | // Assert 20 | result.Should().Be('M'); 21 | ``` 22 | 23 | ### Expected behavior: 24 | 25 | [What you expect to happen] 26 | 27 | ### Actual behavior: 28 | 29 | [What actually happens] 30 | 31 | ### Versions 32 | 33 | * Which version of Fluent Assertions Analyzers are you using? 34 | * Which .NET runtime and version are you targeting? E.g. .NET framework 4.6.1 or .NET Core 2.0. 35 | 36 | ### Additional Information 37 | 38 | Any additional information, configuration or data that might be necessary to reproduce the issue. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Meir Blachman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Extension methods to fluently assert the outcome of .NET tests 4 | 5 | [![CI](https://github.com/fluentassertions/fluentassertions.analyzers/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fluentassertions/fluentassertions.analyzers/actions/workflows/ci.yml) 6 | [![](https://img.shields.io/github/release/fluentassertions/fluentassertions.analyzers.svg?label=latest%20release&color=007edf)](https://github.com/fluentassertions/fluentassertions.analyzers/releases/latest) 7 | [![](https://img.shields.io/nuget/dt/fluentassertions.analyzers.svg?label=downloads&color=007edf&logo=nuget)](https://www.nuget.org/packages/fluentassertions.analyzers) 8 | [![](https://img.shields.io/librariesio/dependents/nuget/fluentassertions.analyzers.svg?label=dependent%20libraries)](https://libraries.io/nuget/fluentassertions.analyzers) 9 | [![GitHub Repo stars](https://img.shields.io/github/stars/fluentassertions/fluentassertions.analyzers)](https://github.com/fluentassertions/fluentassertions.analyzers/stargazers) 10 | [![GitHub contributors](https://img.shields.io/github/contributors/fluentassertions/fluentassertions.analyzers)](https://github.com/fluentassertions/fluentassertions.analyzers/graphs/contributors) 11 | [![GitHub last commit](https://img.shields.io/github/last-commit/fluentassertions/fluentassertions.analyzers)](https://github.com/fluentassertions/fluentassertions.analyzers) 12 | [![GitHub commit activity](https://img.shields.io/github/commit-activity/m/fluentassertions/fluentassertions.analyzers)](https://github.com/fluentassertions/fluentassertions.analyzers/graphs/commit-activity) 13 | [![open issues](https://img.shields.io/github/issues/fluentassertions/fluentassertions.analyzers)](https://github.com/fluentassertions/fluentassertions.analyzers/issues) 14 | 15 | A collection of Analyzers based on the best practices [tips](https://fluentassertions.com/tips/). 16 | 17 | ![Alt](https://repobeats.axiom.co/api/embed/92fd2e6496fc171c00616eaf672c3c757a1a29ac.svg "Repobeats analytics image") 18 | 19 | ## Analysis and Code Fix in Action 20 | 21 | ![Demo](assets/demo.gif) 22 | 23 | ## Install 24 | 25 | using the latest stable version: 26 | 27 | ```powershell 28 | dotnet add package FluentAssertions.Analyzers 29 | ``` 30 | 31 | ## Docs 32 | 33 | - [FluentAssertions Analyzer Docs](docs/FluentAssertionsAnalyzer.md) 34 | - [MsTest Analyzer Docs](docs/MsTestAnalyzer.md) 35 | - [NUnit4 Analyzer Docs](docs/Nunit4Analyzer.md) 36 | - [NUnit3 Analyzer Docs](docs/Nunit3Analyzer.md) 37 | - [Xunit Analyzer Docs](docs/XunitAnalyzer.md) 38 | 39 | ## Configuration 40 | 41 | © Thanks to https://github.com/meziantou/Meziantou.FluentAssertionsAnalyzers 42 | 43 | You can exclude assertion methods using the `.editorconfig` file: 44 | 45 | ```ini 46 | [*.cs] 47 | ffa_excluded_methods=M:NUnit.Framework.Assert.Fail;M:NUnit.Framework.Assert.Fail(System.String) 48 | ``` 49 | 50 | ## Getting Started 51 | 52 | ### Build 53 | 54 | ```bash 55 | dotnet build 56 | ``` 57 | 58 | ### Tests 59 | 60 | ```bash 61 | dotnet test --configuration Release --filter 'TestCategory=Completed' 62 | ``` 63 | 64 | ### Benchmarks 65 | 66 | https://fluentassertions.github.io/fluentassertions.analyzers/dev/bench/ 67 | 68 | ## Example Usages 69 | - https://github.com/SonarSource/sonar-dotnet/pull/2072 70 | - https://github.com/microsoft/component-detection/pull/634 71 | - https://github.com/microsoft/onefuzz/pull/3314 72 | - https://github.com/chocolatey/choco/pull/2908 73 | -------------------------------------------------------------------------------- /assets/FluentAssertions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentassertions/fluentassertions.analyzers/2fe5f040fa2d192d98f76bfd66003e157983b3be/assets/FluentAssertions.png -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluentassertions/fluentassertions.analyzers/2fe5f040fa2d192d98f76bfd66003e157983b3be/assets/demo.gif -------------------------------------------------------------------------------- /docs/XunitAnalyzer.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Xunit Analyzer Docs 6 | 7 | - [AssertTrue](#scenario-asserttrue) - `flag.Should().BeTrue();` 8 | - [AssertFalse](#scenario-assertfalse) - `flag.Should().BeFalse();` 9 | - [AssertSame](#scenario-assertsame) - `obj1.Should().BeSameAs(obj2);` 10 | - [AssertNotSame](#scenario-assertnotsame) - `obj1.Should().NotBeSameAs(obj2);` 11 | - [AssertDoubleEqual](#scenario-assertdoubleequal) - `actual.Should().BeApproximately(expected, tolerance);` 12 | - [AssertDateTimeEqual](#scenario-assertdatetimeequal) - `actual.Should().BeCloseTo(expected, TimeSpan.FromDays(3));` 13 | - [AssertObjectEqual](#scenario-assertobjectequal) - `actual.Should().Be(expected);` 14 | - [AssertObjectEqualWithComparer](#scenario-assertobjectequalwithcomparer) - `actual.Should().BeEquivalentTo(expected, options => options.Using(EqualityComparer.Default));` 15 | - [AssertObjectNotEqual](#scenario-assertobjectnotequal) - `actual.Should().NotBe(expected);` 16 | - [AssertObjectNotEqualWithComparer](#scenario-assertobjectnotequalwithcomparer) - `actual.Should().NotBeEquivalentTo(expected, options => options.Using(EqualityComparer.Default));` 17 | - [AssertStrictEqual](#scenario-assertstrictequal) - `actual.Should().Be(expected);` 18 | - [AssertNotStrictEqual](#scenario-assertnotstrictequal) - `actual.Should().NotBe(expected);` 19 | 20 | 21 | ## Scenarios 22 | 23 | ### scenario: AssertTrue 24 | 25 | ```cs 26 | // arrange 27 | var flag = true; 28 | 29 | // old assertion: 30 | Assert.True(flag); 31 | 32 | // new assertion: 33 | flag.Should().BeTrue(); 34 | ``` 35 | 36 | #### Failure messages 37 | 38 | ```cs 39 | var flag = false; 40 | 41 | // old assertion: 42 | Assert.True(flag); /* fail message: Assert.True() Failure 43 | Expected: True 44 | Actual: False */ 45 | 46 | // new assertion: 47 | flag.Should().BeTrue(); /* fail message: Expected flag to be True, but found False. */ 48 | ``` 49 | 50 | ### scenario: AssertFalse 51 | 52 | ```cs 53 | // arrange 54 | var flag = false; 55 | 56 | // old assertion: 57 | Assert.False(flag); 58 | 59 | // new assertion: 60 | flag.Should().BeFalse(); 61 | ``` 62 | 63 | #### Failure messages 64 | 65 | ```cs 66 | var flag = true; 67 | 68 | // old assertion: 69 | Assert.False(flag); /* fail message: Assert.False() Failure 70 | Expected: False 71 | Actual: True */ 72 | 73 | // new assertion: 74 | flag.Should().BeFalse(); /* fail message: Expected flag to be False, but found True. */ 75 | ``` 76 | 77 | ### scenario: AssertSame 78 | 79 | ```cs 80 | // arrange 81 | var obj1 = new object(); 82 | var obj2 = obj1; 83 | 84 | // old assertion: 85 | Assert.Same(obj1, obj2); 86 | 87 | // new assertion: 88 | obj1.Should().BeSameAs(obj2); 89 | ``` 90 | 91 | #### Failure messages 92 | 93 | ```cs 94 | object obj1 = 6; 95 | object obj2 = "foo"; 96 | 97 | // old assertion: 98 | Assert.Same(obj1, obj2); /* fail message: Assert.Same() Failure: Values are not the same instance 99 | Expected: 6 100 | Actual: "foo" */ 101 | 102 | // new assertion: 103 | obj1.Should().BeSameAs(obj2); /* fail message: Expected obj1 to refer to "foo", but found 6. */ 104 | ``` 105 | 106 | ### scenario: AssertNotSame 107 | 108 | ```cs 109 | // arrange 110 | object obj1 = 6; 111 | object obj2 = "foo"; 112 | 113 | // old assertion: 114 | Assert.NotSame(obj1, obj2); 115 | 116 | // new assertion: 117 | obj1.Should().NotBeSameAs(obj2); 118 | ``` 119 | 120 | #### Failure messages 121 | 122 | ```cs 123 | object obj1 = "foo"; 124 | object obj2 = "foo"; 125 | 126 | // old assertion: 127 | Assert.NotSame(obj1, obj2); /* fail message: Assert.NotSame() Failure: Values are the same instance */ 128 | 129 | // new assertion: 130 | obj1.Should().NotBeSameAs(obj2); /* fail message: Did not expect obj1 to refer to "foo". */ 131 | ``` 132 | 133 | ### scenario: AssertDoubleEqual 134 | 135 | ```cs 136 | // arrange 137 | double actual = 3.14; 138 | double expected = 3.141; 139 | double tolerance = 0.00159; 140 | 141 | // old assertion: 142 | Assert.Equal(expected, actual, tolerance); 143 | 144 | // new assertion: 145 | actual.Should().BeApproximately(expected, tolerance); 146 | ``` 147 | 148 | #### Failure messages 149 | 150 | ```cs 151 | double actual = 3.14; 152 | double expected = 4.2; 153 | double tolerance = 0.0001; 154 | 155 | // old assertion: 156 | Assert.Equal(expected, actual, tolerance); /* fail message: Assert.Equal() Failure: Values are not within tolerance 0.0001 157 | Expected: 4.2000000000000002 158 | Actual: 3.1400000000000001 */ 159 | 160 | // new assertion: 161 | actual.Should().BeApproximately(expected, tolerance); /* fail message: Expected actual to approximate 4.2 +/- 0.0001, but 3.14 differed by 1.06. */ 162 | ``` 163 | 164 | ### scenario: AssertDateTimeEqual 165 | 166 | ```cs 167 | // arrange 168 | var actual = new DateTime(2021, 1, 1); 169 | var expected = new DateTime(2021, 1, 2); 170 | 171 | // old assertion: 172 | Assert.Equal(expected, actual, TimeSpan.FromDays(3)); 173 | 174 | // new assertion: 175 | actual.Should().BeCloseTo(expected, TimeSpan.FromDays(3)); 176 | ``` 177 | 178 | #### Failure messages 179 | 180 | ```cs 181 | var actual = new DateTime(2021, 1, 1); 182 | var expected = new DateTime(2021, 1, 2); 183 | 184 | // old assertion: 185 | Assert.Equal(expected, actual, TimeSpan.FromHours(3)); /* fail message: Assert.Equal() Failure: Values differ 186 | Expected: 2021-01-02T00:00:00.0000000 187 | Actual: 2021-01-01T00:00:00.0000000 (difference 1.00:00:00 is larger than 03:00:00) */ 188 | 189 | // new assertion: 190 | actual.Should().BeCloseTo(expected, TimeSpan.FromHours(3)); /* fail message: Expected actual to be within 3h from <2021-01-02>, but <2021-01-01> was off by 1d. */ 191 | ``` 192 | 193 | ### scenario: AssertObjectEqual 194 | 195 | ```cs 196 | // arrange 197 | object actual = "foo"; 198 | object expected = "foo"; 199 | 200 | // old assertion: 201 | Assert.Equal(expected, actual); 202 | 203 | // new assertion: 204 | actual.Should().Be(expected); 205 | ``` 206 | 207 | #### Failure messages 208 | 209 | ```cs 210 | object actual = "foo"; 211 | object expected = 6; 212 | 213 | // old assertion: 214 | Assert.Equal(expected, actual); /* fail message: Assert.Equal() Failure: Values differ 215 | Expected: 6 216 | Actual: foo */ 217 | 218 | // new assertion: 219 | actual.Should().Be(expected); /* fail message: Expected actual to be 6, but found "foo". */ 220 | ``` 221 | 222 | ### scenario: AssertObjectEqualWithComparer 223 | 224 | ```cs 225 | // arrange 226 | object actual = "foo"; 227 | object expected = "foo"; 228 | 229 | // old assertion: 230 | Assert.Equal(expected, actual, EqualityComparer.Default); 231 | 232 | // new assertion: 233 | actual.Should().BeEquivalentTo(expected, options => options.Using(EqualityComparer.Default)); 234 | ``` 235 | 236 | #### Failure messages 237 | 238 | ```cs 239 | object actual = "foo"; 240 | object expected = 6; 241 | 242 | // old assertion: 243 | Assert.Equal(expected, actual, EqualityComparer.Default); /* fail message: Assert.Equal() Failure: Values differ 244 | Expected: 6 245 | Actual: foo */ 246 | 247 | // new assertion: 248 | actual.Should().BeEquivalentTo(expected, options => options.Using(EqualityComparer.Default)); /* fail message: Expected actual to be 6, but found "foo". 249 | 250 | With configuration: 251 | - Use declared types and members 252 | - Compare enums by value 253 | - Compare tuples by their properties 254 | - Compare anonymous types by their properties 255 | - Compare records by their members 256 | - Include non-browsable members 257 | - Match member by name (or throw) 258 | - Use System.Collections.Generic.ObjectEqualityComparer`1[System.Object] for objects of type System.Object 259 | - Be strict about the order of items in byte arrays 260 | - Without automatic conversion. 261 | */ 262 | ``` 263 | 264 | ### scenario: AssertObjectNotEqual 265 | 266 | ```cs 267 | // arrange 268 | object actual = "foo"; 269 | object expected = 6; 270 | 271 | // old assertion: 272 | Assert.NotEqual(expected, actual); 273 | 274 | // new assertion: 275 | actual.Should().NotBe(expected); 276 | ``` 277 | 278 | #### Failure messages 279 | 280 | ```cs 281 | object actual = "foo"; 282 | object expected = "foo"; 283 | 284 | // old assertion: 285 | Assert.NotEqual(expected, actual); /* fail message: Assert.NotEqual() Failure: Strings are equal 286 | Expected: Not "foo" 287 | Actual: "foo" */ 288 | 289 | // new assertion: 290 | actual.Should().NotBe(expected); /* fail message: Did not expect actual to be equal to "foo". */ 291 | ``` 292 | 293 | ### scenario: AssertObjectNotEqualWithComparer 294 | 295 | ```cs 296 | // arrange 297 | object actual = "foo"; 298 | object expected = 6; 299 | 300 | // old assertion: 301 | Assert.NotEqual(expected, actual, EqualityComparer.Default); 302 | 303 | // new assertion: 304 | actual.Should().NotBeEquivalentTo(expected, options => options.Using(EqualityComparer.Default)); 305 | ``` 306 | 307 | #### Failure messages 308 | 309 | ```cs 310 | object actual = "foo"; 311 | object expected = "foo"; 312 | 313 | // old assertion: 314 | Assert.NotEqual(expected, actual, EqualityComparer.Default); /* fail message: Assert.NotEqual() Failure: Strings are equal 315 | Expected: Not "foo" 316 | Actual: "foo" */ 317 | 318 | // new assertion: 319 | actual.Should().NotBeEquivalentTo(expected, options => options.Using(EqualityComparer.Default)); /* fail message: Expected actual not to be equivalent to "foo", but they are. */ 320 | ``` 321 | 322 | ### scenario: AssertStrictEqual 323 | 324 | ```cs 325 | // arrange 326 | object actual = "foo"; 327 | object expected = "foo"; 328 | 329 | // old assertion: 330 | Assert.StrictEqual(expected, actual); 331 | 332 | // new assertion: 333 | actual.Should().Be(expected); 334 | ``` 335 | 336 | #### Failure messages 337 | 338 | ```cs 339 | object actual = "foo"; 340 | object expected = 6; 341 | 342 | // old assertion: 343 | Assert.StrictEqual(expected, actual); /* fail message: Assert.StrictEqual() Failure: Values differ 344 | Expected: 6 345 | Actual: "foo" */ 346 | 347 | // new assertion: 348 | actual.Should().Be(expected); /* fail message: Expected actual to be 6, but found "foo". */ 349 | ``` 350 | 351 | ### scenario: AssertNotStrictEqual 352 | 353 | ```cs 354 | // arrange 355 | object actual = "foo"; 356 | object expected = 6; 357 | 358 | // old assertion: 359 | Assert.NotStrictEqual(expected, actual); 360 | 361 | // new assertion: 362 | actual.Should().NotBe(expected); 363 | ``` 364 | 365 | #### Failure messages 366 | 367 | ```cs 368 | object actual = "foo"; 369 | object expected = "foo"; 370 | 371 | // old assertion: 372 | Assert.NotStrictEqual(expected, actual); /* fail message: Assert.NotStrictEqual() Failure: Values are equal 373 | Expected: Not "foo" 374 | Actual: "foo" */ 375 | 376 | // new assertion: 377 | actual.Should().NotBe(expected); /* fail message: Did not expect actual to be equal to "foo". */ 378 | ``` 379 | 380 | 381 | -------------------------------------------------------------------------------- /scripts/generate-docs.ps1: -------------------------------------------------------------------------------- 1 | 2 | param ( 3 | [switch]$ValidateNoChanges 4 | ) 5 | 6 | if ($ValidateNoChanges) { 7 | $output = git status --porcelain=v1 -- docs 8 | if ($output) { 9 | $diff = git diff -- docs # HACK to ignore crlf changes 10 | if ($diff) { 11 | git diff -- docs 12 | throw "The docs generator has made changes to the docs folder. Please commit these changes and push them to the repository." 13 | } 14 | } 15 | } 16 | 17 | function GenerateDocs { 18 | param ( 19 | [string]$project 20 | ) 21 | 22 | Push-Location src 23 | Push-Location $project 24 | dotnet run generate 25 | Pop-Location 26 | Pop-Location 27 | } 28 | 29 | GenerateDocs -project FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs 30 | GenerateDocs -project FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Nunit4 31 | GenerateDocs -project FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Nunit3 32 | GenerateDocs -project FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Xunit -------------------------------------------------------------------------------- /scripts/run-docs-tests.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [switch]$FormatAndExecuteTestsAgain 3 | ) 4 | 5 | function RunTestsAndValidate { 6 | param ( 7 | [string]$project 8 | ) 9 | 10 | Push-Location src 11 | Push-Location $project 12 | dotnet test 13 | Pop-Location 14 | Pop-Location 15 | 16 | if ($FormatAndExecuteTestsAgain) { 17 | Push-Location src 18 | Push-Location $project 19 | 20 | $i = 1; 21 | do { 22 | Write-Host "formatting code... - Iteration $i" 23 | $out = dotnet format analyzers --diagnostics FAA0001 FAA0003 FAA0004 --severity info --verbosity normal 2>&1 | Out-String | Join-String 24 | 25 | Write-Host "-------------$i-------------" 26 | Write-Host $out 27 | Write-Host "-------------$i-------------" 28 | Write-Host "output length: $($out.Length)" 29 | 30 | $i++ 31 | } while ($out.Contains("Unable to fix FAA000")) 32 | 33 | Pop-Location 34 | Pop-Location 35 | 36 | Push-Location src 37 | Push-Location $project 38 | dotnet test 39 | Pop-Location 40 | Pop-Location 41 | } 42 | } 43 | 44 | RunTestsAndValidate -project FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs 45 | RunTestsAndValidate -project FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Nunit4 46 | RunTestsAndValidate -project FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Nunit3 47 | RunTestsAndValidate -project FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Xunit -------------------------------------------------------------------------------- /src/Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.BenchmarkTests/FluentAssertions.Analyzers.BenchmarkTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | Exe 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.BenchmarkTests/FluentAssertionsBenchmarks.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using BenchmarkDotNet.Jobs; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.Diagnostics; 5 | using System.Collections.Immutable; 6 | using FluentAssertions.Analyzers.TestUtils; 7 | using System.Threading.Tasks; 8 | using System; 9 | using System.Collections.Generic; 10 | 11 | namespace FluentAssertions.Analyzers.BenchmarkTests 12 | { 13 | [SimpleJob(RuntimeMoniker.Net60, baseline: true)] 14 | [SimpleJob(RuntimeMoniker.Net70)] 15 | [JsonExporter] 16 | public class FluentAssertionsBenchmarks 17 | { 18 | private CompilationWithAnalyzers MinimalCompilationWithAnalyzers_ObjectStatement; 19 | private CompilationWithAnalyzers SmallCompilationWithAnalyzers_StringAssertions; 20 | 21 | [GlobalSetup] 22 | public async Task GlobalSetup() 23 | { 24 | MinimalCompilationWithAnalyzers_ObjectStatement = await CreateCompilationFromAsync(GenerateCode.ObjectStatement("actual.Should().Equals(expected);")); 25 | SmallCompilationWithAnalyzers_StringAssertions = await CreateCompilationFromAsync( 26 | GenerateCode.StringAssertion("actual.StartsWith(expected).Should().BeTrue();"), 27 | GenerateCode.StringAssertion("actual.EndsWith(expected).Should().BeTrue();"), 28 | GenerateCode.StringAssertion("actual.Should().NotBeNull().And.NotBeEmpty();"), 29 | GenerateCode.StringAssertion("string.IsNullOrEmpty(actual).Should().BeTrue();"), 30 | GenerateCode.StringAssertion("string.IsNullOrEmpty(actual).Should().BeFalse();"), 31 | GenerateCode.StringAssertion("string.IsNullOrWhiteSpace(actual).Should().BeTrue();"), 32 | GenerateCode.StringAssertion("string.IsNullOrWhiteSpace(actual).Should().BeFalse();"), 33 | GenerateCode.StringAssertion("actual.Length.Should().Be(k);") 34 | ); 35 | } 36 | 37 | [Benchmark] 38 | public Task MinimalCompilation_SingleSource_ObjectStatement_Analyzing() 39 | { 40 | return MinimalCompilationWithAnalyzers_ObjectStatement.GetAnalyzerDiagnosticsAsync(); 41 | } 42 | 43 | [Benchmark] 44 | public Task SmallCompilation_MultipleSources_StringAssertions_Analyzing() 45 | { 46 | return SmallCompilationWithAnalyzers_StringAssertions.GetAnalyzerDiagnosticsAsync(); 47 | } 48 | 49 | private async Task CreateCompilationFromAsync(params string[] sources) 50 | { 51 | var project = CsProjectGenerator.CreateProject(sources); 52 | var compilation = await project.GetCompilationAsync(); 53 | 54 | if (compilation is null) 55 | { 56 | throw new InvalidOperationException("Compilation is null"); 57 | } 58 | 59 | return compilation.WithOptions(compilation.Options.WithSpecificDiagnosticOptions(new Dictionary 60 | { 61 | ["CS1701"] = ReportDiagnostic.Suppress, // Binding redirects 62 | ["CS1702"] = ReportDiagnostic.Suppress, 63 | ["CS1705"] = ReportDiagnostic.Suppress, 64 | ["CS8019"] = ReportDiagnostic.Suppress // TODO: Unnecessary using directive 65 | })).WithAnalyzers(CodeAnalyzersUtils.GetAllAnalyzers().ToImmutableArray()); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.BenchmarkTests/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Running; 2 | 3 | namespace FluentAssertions.Analyzers.BenchmarkTests 4 | { 5 | public class Program 6 | { 7 | public static void Main() 8 | => BenchmarkDotNet.Running.BenchmarkRunner.Run(); 9 | } 10 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Nunit3/ExpectedAssertionExceptionAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | using NUnit.Framework.Interfaces; 4 | using NUnit.Framework.Internal; 5 | using NUnit.Framework.Internal.Commands; 6 | 7 | namespace FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs; 8 | 9 | /// 10 | /// Based on https://github.com/nunit/nunit-csharp-samples/blob/master/ExpectedExceptionExample/ExpectedExceptionAttribute.cs 11 | /// 12 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] 13 | public class ExpectedAssertionExceptionAttribute : NUnitAttribute, IWrapTestMethod 14 | { 15 | public TestCommand Wrap(TestCommand command) 16 | { 17 | return new ExpectedExceptionCommand(command, typeof(AssertionException)); 18 | } 19 | 20 | private class ExpectedExceptionCommand : DelegatingTestCommand 21 | { 22 | private readonly Type _expectedType; 23 | 24 | public ExpectedExceptionCommand(TestCommand innerCommand, Type expectedType) 25 | : base(innerCommand) 26 | { 27 | _expectedType = expectedType; 28 | } 29 | 30 | public override TestResult Execute(TestExecutionContext context) 31 | { 32 | Type caughtType = null; 33 | 34 | try 35 | { 36 | innerCommand.Execute(context); 37 | } 38 | catch (Exception ex) 39 | { 40 | if (ex is NUnitException) 41 | ex = ex.InnerException; 42 | caughtType = ex.GetType(); 43 | } 44 | 45 | if (caughtType == _expectedType) 46 | context.CurrentResult.SetResult(ResultState.Success); 47 | else if (caughtType != null) 48 | context.CurrentResult.SetResult(ResultState.Failure, 49 | $"Expected {_expectedType.Name} but got {caughtType.Name}"); 50 | else 51 | context.CurrentResult.SetResult(ResultState.Failure, 52 | $"Expected {_expectedType.Name} but no exception was thrown"); 53 | 54 | return context.CurrentResult; 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Nunit3/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Nunit3.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | false 7 | true 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Nunit3/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Threading.Tasks; 5 | using FluentAssertions.Analyzers.FluentAssertionAnalyzerDocsGenerator; 6 | 7 | namespace FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs; 8 | 9 | public class Program 10 | { 11 | public static Task Main(string[] args) => ProgramUtils.RunMain(args); 12 | 13 | private class Nunit3DocsGenerator : DocsGenerator 14 | { 15 | protected override Assembly TestAssembly { get; } = typeof(Program).Assembly; 16 | protected override string TestAttribute => "Test"; // NUnit.Framework.TestAttribute 17 | protected override string TestFile => Path.Join(Environment.CurrentDirectory, "Nunit3AnalyzerTests.cs"); 18 | 19 | protected override void ResetTestFramework() 20 | { 21 | var testContext = typeof(NUnit.Framework.Internal.TestExecutionContext); 22 | testContext.GetProperty("CurrentContext").SetValue(null, new NUnit.Framework.Internal.TestExecutionContext.AdhocContext()); 23 | 24 | NUnit.Framework.Internal.TestExecutionContext.CurrentContext.CurrentResult.AssertionResults.Clear(); 25 | } 26 | } 27 | private class Nunit3DocsVerifier : DocsVerifier 28 | { 29 | protected override string TestAttribute => "Test"; // NUnit.Framework.TestAttribute 30 | protected override string TestFile => Path.Join(Environment.CurrentDirectory, "Nunit3AnalyzerTests.cs"); 31 | } 32 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Nunit4/ExpectedAssertionExceptionAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | using NUnit.Framework.Interfaces; 4 | using NUnit.Framework.Internal; 5 | using NUnit.Framework.Internal.Commands; 6 | 7 | namespace FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs; 8 | 9 | /// 10 | /// Based on https://github.com/nunit/nunit-csharp-samples/blob/master/ExpectedExceptionExample/ExpectedExceptionAttribute.cs 11 | /// 12 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] 13 | public class ExpectedAssertionExceptionAttribute : NUnitAttribute, IWrapTestMethod 14 | { 15 | public TestCommand Wrap(TestCommand command) 16 | { 17 | return new ExpectedExceptionCommand(command, typeof(AssertionException)); 18 | } 19 | 20 | private class ExpectedExceptionCommand : DelegatingTestCommand 21 | { 22 | private readonly Type _expectedType; 23 | 24 | public ExpectedExceptionCommand(TestCommand innerCommand, Type expectedType) 25 | : base(innerCommand) 26 | { 27 | _expectedType = expectedType; 28 | } 29 | 30 | public override TestResult Execute(TestExecutionContext context) 31 | { 32 | Type caughtType = null; 33 | 34 | try 35 | { 36 | innerCommand.Execute(context); 37 | } 38 | catch (Exception ex) 39 | { 40 | if (ex is NUnitException) 41 | ex = ex.InnerException; 42 | caughtType = ex.GetType(); 43 | } 44 | 45 | if (caughtType == _expectedType) 46 | context.CurrentResult.SetResult(ResultState.Success); 47 | else if (caughtType != null) 48 | context.CurrentResult.SetResult(ResultState.Failure, 49 | $"Expected {_expectedType.Name} but got {caughtType.Name}"); 50 | else 51 | context.CurrentResult.SetResult(ResultState.Failure, 52 | $"Expected {_expectedType.Name} but no exception was thrown"); 53 | 54 | return context.CurrentResult; 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Nunit4/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Nunit4.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | false 7 | true 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Nunit4/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Threading.Tasks; 5 | using FluentAssertions.Analyzers.FluentAssertionAnalyzerDocsGenerator; 6 | 7 | namespace FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs; 8 | 9 | public class Program 10 | { 11 | public static Task Main(string[] args) => ProgramUtils.RunMain(args); 12 | 13 | private class Nunit4DocsGenerator : DocsGenerator 14 | { 15 | protected override Assembly TestAssembly { get; } = typeof(Program).Assembly; 16 | protected override string TestAttribute => "Test"; // NUnit.Framework.TestAttribute 17 | protected override string TestFile => Path.Join(Environment.CurrentDirectory, "Nunit4AnalyzerTests.cs"); 18 | 19 | protected override void ResetTestFramework() 20 | { 21 | var testContext = typeof(NUnit.Framework.Internal.TestExecutionContext); 22 | testContext.GetProperty("CurrentContext").SetValue(null, new NUnit.Framework.Internal.TestExecutionContext.AdhocContext()); 23 | 24 | NUnit.Framework.Internal.TestExecutionContext.CurrentContext.CurrentResult.AssertionResults.Clear(); 25 | } 26 | } 27 | private class Nunit4DocsVerifier : DocsVerifier 28 | { 29 | protected override string TestAttribute => "Test"; // NUnit.Framework.TestAttribute 30 | protected override string TestFile => Path.Join(Environment.CurrentDirectory, "Nunit4AnalyzerTests.cs"); 31 | } 32 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Xunit/ExpectedAssertionExceptionAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Xunit.Abstractions; 7 | using Xunit.Sdk; 8 | 9 | [assembly: Xunit.TestFramework("FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.CustomTestFramework", "FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Xunit")] 10 | 11 | namespace FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs; 12 | 13 | // Inspired by https://andrewlock.net/tracking-down-a-hanging-xunit-test-in-ci-building-a-custom-test-framework/ with a few more customizations 14 | public class CustomTestFramework : XunitTestFramework 15 | { 16 | public CustomTestFramework(IMessageSink messageSink) : base(messageSink) 17 | { 18 | } 19 | 20 | protected override ITestFrameworkExecutor CreateExecutor(AssemblyName assemblyName) 21 | => new CustomExecutor(assemblyName, SourceInformationProvider, DiagnosticMessageSink); 22 | 23 | private class CustomExecutor : XunitTestFrameworkExecutor 24 | { 25 | public CustomExecutor(AssemblyName assemblyName, ISourceInformationProvider sourceInformationProvider, IMessageSink diagnosticMessageSink) 26 | : base(assemblyName, sourceInformationProvider, diagnosticMessageSink) 27 | { 28 | } 29 | 30 | protected override async void RunTestCases(IEnumerable testCases, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions) 31 | { 32 | using var assemblyRunner = new CustomAssemblyRunner(TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink, executionOptions); 33 | await assemblyRunner.RunAsync(); 34 | } 35 | } 36 | 37 | private class CustomAssemblyRunner : XunitTestAssemblyRunner 38 | { 39 | public CustomAssemblyRunner(ITestAssembly testAssembly, IEnumerable testCases, IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions) 40 | : base(testAssembly, testCases, diagnosticMessageSink, executionMessageSink, executionOptions) 41 | { 42 | } 43 | 44 | protected override Task RunTestCollectionAsync(IMessageBus messageBus, ITestCollection testCollection, IEnumerable testCases, CancellationTokenSource cancellationTokenSource) 45 | => new CustomTestCollectionRunner(testCollection, testCases, DiagnosticMessageSink, messageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), cancellationTokenSource).RunAsync(); 46 | } 47 | 48 | private class CustomTestCollectionRunner : XunitTestCollectionRunner 49 | { 50 | public CustomTestCollectionRunner(ITestCollection testCollection, IEnumerable testCases, IMessageSink diagnosticMessageSink, IMessageBus messageBus, ITestCaseOrderer testCaseOrderer, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) 51 | : base(testCollection, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource) 52 | { 53 | } 54 | 55 | protected override Task RunTestClassAsync(ITestClass testClass, IReflectionTypeInfo @class, IEnumerable testCases) 56 | => new CustomTestClassRunner(testClass, @class, testCases, DiagnosticMessageSink, MessageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), CancellationTokenSource, CollectionFixtureMappings).RunAsync(); 57 | } 58 | 59 | private class CustomTestClassRunner : XunitTestClassRunner 60 | { 61 | public CustomTestClassRunner(ITestClass testClass, IReflectionTypeInfo @class, IEnumerable testCases, IMessageSink diagnosticMessageSink, IMessageBus messageBus, ITestCaseOrderer testCaseOrderer, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource, IDictionary collectionFixtureMappings) 62 | : base(testClass, @class, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource, collectionFixtureMappings) 63 | { 64 | } 65 | 66 | protected override Task RunTestMethodAsync(ITestMethod testMethod, IReflectionMethodInfo method, IEnumerable testCases, object[] constructorArguments) 67 | => new CustomTestMethodRunner(testMethod, Class, method, testCases, DiagnosticMessageSink, MessageBus, new ExceptionAggregator(Aggregator), CancellationTokenSource, constructorArguments).RunAsync(); 68 | } 69 | 70 | private class CustomTestMethodRunner : XunitTestMethodRunner 71 | { 72 | private readonly object[] _constructorArguments; 73 | private readonly CancellationTokenSource _cancellationTokenSource; 74 | 75 | public CustomTestMethodRunner(ITestMethod testMethod, IReflectionTypeInfo @class, IReflectionMethodInfo method, IEnumerable testCases, IMessageSink diagnosticMessageSink, IMessageBus messageBus, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource, object[] constructorArguments) 76 | : base(testMethod, @class, method, testCases, diagnosticMessageSink, messageBus, aggregator, cancellationTokenSource, constructorArguments) 77 | { 78 | _constructorArguments = constructorArguments; 79 | _cancellationTokenSource = cancellationTokenSource; 80 | } 81 | 82 | protected override async Task RunTestCaseAsync(IXunitTestCase testCase) 83 | { 84 | return await new CustomTestCaseRunner(testCase, testCase.DisplayName, testCase.SkipReason, _constructorArguments, testCase.TestMethodArguments, MessageBus, Aggregator, _cancellationTokenSource) 85 | .RunAsync(); 86 | } 87 | } 88 | 89 | private class CustomTestCaseRunner : XunitTestCaseRunner 90 | { public CustomTestCaseRunner(IXunitTestCase testCase, string displayName, string skipReason, object[] constructorArguments, object[] testMethodArguments, IMessageBus messageBus, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) : base(testCase, displayName, skipReason, constructorArguments, testMethodArguments, messageBus, aggregator, cancellationTokenSource) 91 | { 92 | } 93 | 94 | protected override XunitTestRunner CreateTestRunner(ITest test, IMessageBus messageBus, Type testClass, object[] constructorArguments, MethodInfo testMethod, object[] testMethodArguments, string skipReason, IReadOnlyList beforeAfterAttributes, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) 95 | { 96 | return new CustomTestRunner(test, messageBus, testClass, constructorArguments, testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource); 97 | } 98 | } 99 | 100 | private class CustomTestRunner : XunitTestRunner 101 | { 102 | public CustomTestRunner(ITest test, IMessageBus messageBus, Type testClass, object[] constructorArguments, MethodInfo testMethod, object[] testMethodArguments, string skipReason, IReadOnlyList beforeAfterAttributes, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) : base(test, messageBus, testClass, constructorArguments, testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource) 103 | { 104 | } 105 | 106 | protected override Task InvokeTestMethodAsync(ExceptionAggregator aggregator) 107 | { 108 | return new CustomTestInvoker(Test, MessageBus, TestClass, ConstructorArguments, TestMethod, TestMethodArguments, BeforeAfterAttributes, aggregator, CancellationTokenSource).RunAsync(); 109 | } 110 | } 111 | 112 | private class CustomTestInvoker : XunitTestInvoker 113 | { 114 | 115 | public CustomTestInvoker(ITest test, IMessageBus messageBus, Type testClass, object[] constructorArguments, MethodInfo testMethod, object[] testMethodArguments, IReadOnlyList beforeAfterAttributes, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) : base(test, messageBus, testClass, constructorArguments, testMethod, testMethodArguments, beforeAfterAttributes, aggregator, cancellationTokenSource) 116 | { 117 | } 118 | 119 | protected override object CallTestMethod(object testClassInstance) 120 | { 121 | var isExpectingException = TestMethod.GetCustomAttribute() != null; 122 | 123 | try 124 | { 125 | var result = base.CallTestMethod(testClassInstance); 126 | if (isExpectingException) 127 | { 128 | Aggregator.Add(new XunitException($"Expected exception of type {typeof(XunitException)}, but no exception was thrown.")); 129 | } 130 | 131 | return result; 132 | } 133 | catch (TargetInvocationException ex) when (ex.InnerException is XunitException) 134 | { 135 | if (!isExpectingException) 136 | { 137 | Aggregator.Add(ex.InnerException); 138 | } 139 | 140 | return null; 141 | } 142 | } 143 | } 144 | } 145 | 146 | public class ExpectedAssertionExceptionAttribute : Attribute 147 | { 148 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Xunit/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Xunit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | false 7 | true 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.Xunit/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Threading.Tasks; 5 | using FluentAssertions.Analyzers.FluentAssertionAnalyzerDocsGenerator; 6 | 7 | namespace FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs; 8 | 9 | public class Program 10 | { 11 | public static Task Main(string[] args) => ProgramUtils.RunMain(args); 12 | 13 | private class XunitDocsGenerator : DocsGenerator 14 | { 15 | protected override Assembly TestAssembly { get; } = typeof(Program).Assembly; 16 | protected override string TestAttribute => "Fact"; // Xunit.FactAttribute 17 | protected override string TestFile => Path.Join(Environment.CurrentDirectory, "XunitAnalyzerTests.cs"); 18 | } 19 | private class XunitDocsVerifier : DocsVerifier 20 | { 21 | protected override string TestAttribute => "Fact"; // Xunit.FactAttribute 22 | protected override string TestFile => Path.Join(Environment.CurrentDirectory, "XunitAnalyzerTests.cs"); 23 | } 24 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | false 7 | true 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Threading.Tasks; 5 | using FluentAssertions.Analyzers.FluentAssertionAnalyzerDocsGenerator; 6 | 7 | namespace FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs; 8 | 9 | public class Program 10 | { 11 | public static async Task Main(string[] args) 12 | { 13 | await Task.WhenAll( 14 | ProgramUtils.RunMain(args), 15 | ProgramUtils.RunMain(args) 16 | ); 17 | } 18 | 19 | private abstract class BaseDocsDocsGenerator : DocsGenerator 20 | { 21 | protected override Assembly TestAssembly { get; } = typeof(Program).Assembly; 22 | protected override string TestAttribute => "TestMethod"; // Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute 23 | } 24 | 25 | private class MsTestDocsGenerator : BaseDocsDocsGenerator 26 | { 27 | protected override string TestFile => Path.Join(Environment.CurrentDirectory, "MsTestAnalyzerTests.cs"); 28 | } 29 | private class FluentAssertionsDocsGenerator : BaseDocsDocsGenerator 30 | { 31 | protected override string TestFile => Path.Join(Environment.CurrentDirectory, "FluentAssertionsAnalyzerTests.cs"); 32 | } 33 | 34 | 35 | private abstract class BaseDocsVerifier : DocsVerifier 36 | { 37 | protected override string TestAttribute => "TestMethod"; // Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute 38 | } 39 | 40 | private class MsTestDocsVerifier : BaseDocsVerifier 41 | { 42 | protected override string TestFile => Path.Join(Environment.CurrentDirectory, "MsTestAnalyzerTests.cs"); 43 | } 44 | private class FluentAssertionsDocsVerifier : BaseDocsVerifier 45 | { 46 | protected override string TestFile => Path.Join(Environment.CurrentDirectory, "FluentAssertionsAnalyzerTests.cs"); 47 | } 48 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocsGenerator/DocsGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Microsoft.CodeAnalysis; 8 | using Microsoft.CodeAnalysis.CSharp; 9 | using Microsoft.CodeAnalysis.CSharp.Syntax; 10 | 11 | namespace FluentAssertions.Analyzers.FluentAssertionAnalyzerDocsGenerator; 12 | 13 | public abstract class DocsGenerator 14 | { 15 | protected abstract Assembly TestAssembly { get; } 16 | protected abstract string TestAttribute { get; } 17 | protected abstract string TestFile { get; } 18 | 19 | public async Task Execute() 20 | { 21 | Console.WriteLine($"Input file: {TestFile}"); 22 | 23 | var compilation = SyntaxFactory.ParseCompilationUnit(await File.ReadAllTextAsync(TestFile)); 24 | var tree = compilation.SyntaxTree; 25 | 26 | Console.WriteLine($"File: {Path.GetFileName(TestFile)}"); 27 | 28 | var docsName = Path.GetFileNameWithoutExtension(TestFile).Replace("Tests", ".md"); 29 | 30 | var docs = new StringBuilder(); 31 | var toc = new StringBuilder(); 32 | var scenarios = new StringBuilder(); 33 | 34 | docs.AppendLine(""); 37 | docs.AppendLine(); 38 | 39 | var subject = Path.GetFileNameWithoutExtension(TestFile).Replace("AnalyzerTests", string.Empty); 40 | docs.AppendLine($"# {subject} Analyzer Docs"); 41 | docs.AppendLine(); 42 | 43 | scenarios.AppendLine("## Scenarios"); 44 | scenarios.AppendLine(); 45 | 46 | var root = await tree.GetRootAsync(); 47 | var classDef = root.DescendantNodes().OfType().First(); 48 | var methods = root.DescendantNodes().OfType(); 49 | var methodsMap = methods.ToDictionary(m => m.Identifier.Text); 50 | 51 | var classType = TestAssembly.GetType($"FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs.{classDef.Identifier.Text}"); 52 | var classInstance = Activator.CreateInstance(classType); 53 | 54 | foreach (var method in methods.Where(m => m.AttributeLists.Any(list => list.Attributes.Count is 1 && list.Attributes[0].Name.ToString() == TestAttribute))) 55 | { 56 | // success scenario: 57 | { 58 | scenarios.AppendLine($"### scenario: {method.Identifier}"); 59 | scenarios.AppendLine(); 60 | var bodyLines = method.Body.ToFullString().Split(Environment.NewLine)[1..^2]; 61 | var paddingToRemove = bodyLines[0].IndexOf(bodyLines[0].TrimStart()); 62 | var normalizedBody = bodyLines.Select(l => l.Length > paddingToRemove ? l.Substring(paddingToRemove) : l).Aggregate((a, b) => $"{a}{Environment.NewLine}{b}"); 63 | var methodBody = $"```cs{Environment.NewLine}{normalizedBody}{Environment.NewLine}```"; 64 | scenarios.AppendLine(methodBody); 65 | scenarios.AppendLine(); 66 | 67 | var newAssertion = bodyLines[^1].Trim(); 68 | 69 | toc.AppendLine($"- [{method.Identifier}](#scenario-{method.Identifier.Text.ToLower()}) - `{newAssertion}`"); 70 | } 71 | 72 | // FluentAssertion failures scenario: 73 | if (methodsMap.TryGetValue($"{method.Identifier.Text}_Failure", out var testWithFailure)) 74 | { 75 | var testMethodWithFailure = classType.GetMethod(testWithFailure.Identifier.Text); 76 | 77 | var exceptionMessageLines = GetMethodExceptionMessageLines(classInstance, testMethodWithFailure); 78 | 79 | var bodyLines = testWithFailure.Body.ToFullString().Split(Environment.NewLine)[2..^2]; 80 | var paddingToRemove = bodyLines[0].IndexOf(bodyLines[0].TrimStart()); 81 | 82 | var oldAssertionComment = testWithFailure.DescendantTrivia().First(x => x.IsKind(SyntaxKind.SingleLineCommentTrivia) && x.ToString().Equals("// old assertion:")); 83 | var newAssertionComment = testWithFailure.DescendantTrivia().First(x => x.IsKind(SyntaxKind.SingleLineCommentTrivia) && x.ToString().Equals("// new assertion:")); 84 | 85 | var statements = testWithFailure.Body.Statements.OfType(); 86 | 87 | var oldAssertions = statements.Where(x => x.Span.CompareTo(oldAssertionComment.Span) > 0 && x.Span.CompareTo(newAssertionComment.Span) < 0) 88 | .Select((x, i) => x.ToString().TrimStart() + " \t// fail message: " + exceptionMessageLines[i]); 89 | var newAssertion = statements.Single(x => x.Span.CompareTo(newAssertionComment.Span) > 0).ToString().TrimStart() + " \t// fail message: " + exceptionMessageLines[^1]; 90 | 91 | var arrange = bodyLines.TakeWhile(x => !string.IsNullOrEmpty(x)) 92 | .Select(l => l.Length > paddingToRemove ? l.Substring(paddingToRemove) : l).Aggregate((a, b) => $"{a}{Environment.NewLine}{b}"); 93 | 94 | scenarios.AppendLine($"#### Failure messages"); 95 | scenarios.AppendLine(); 96 | scenarios.AppendLine("```cs"); 97 | scenarios.AppendLine(arrange); 98 | scenarios.AppendLine(); 99 | scenarios.AppendLine($"// old assertion:"); 100 | foreach (var oldAssertion in oldAssertions) 101 | { 102 | scenarios.AppendLine(oldAssertion); 103 | } 104 | scenarios.AppendLine(); 105 | scenarios.AppendLine($"// new assertion:"); 106 | scenarios.AppendLine(newAssertion); 107 | scenarios.AppendLine("```"); 108 | scenarios.AppendLine(); 109 | } 110 | 111 | // Testing Libraries failures scenarios: 112 | if (methodsMap.TryGetValue($"{method.Identifier.Text}_Failure_NewAssertion", out var testWithFailureNewAssertion)) 113 | { 114 | var testWithFailureOldAssertions = methodsMap.Where(x => x.Key.StartsWith($"{method.Identifier.Text}_Failure_OldAssertion")).Select(x => x.Value); 115 | 116 | var testMethodWithFailureOldAssertions = testWithFailureOldAssertions.Select(m => classType.GetMethod(m.Identifier.Text)); 117 | var testMethodWithFailureNewAssertion = classType.GetMethod(testWithFailureNewAssertion.Identifier.Text); 118 | 119 | var exceptionMessageLinesOldAssertions = testMethodWithFailureOldAssertions.Select(m => GetMethodExceptionMessage(classInstance, m)).ToArray(); 120 | var exceptionMessageLinesNewAssertion = GetMethodExceptionMessage(classInstance, testMethodWithFailureNewAssertion); 121 | 122 | var oldAssertionComment = testWithFailureOldAssertions.Select(x => x.DescendantTrivia().First(x => x.IsKind(SyntaxKind.SingleLineCommentTrivia) && x.ToString().Equals("// old assertion:"))).ToArray(); 123 | var newAssertionComment = testWithFailureNewAssertion.DescendantTrivia().First(x => x.IsKind(SyntaxKind.SingleLineCommentTrivia) && x.ToString().Equals("// new assertion:")); 124 | 125 | var bodyLines = testWithFailureNewAssertion.Body.ToFullString().Split(Environment.NewLine)[2..^2]; 126 | var paddingToRemove = bodyLines[0].IndexOf(bodyLines[0].TrimStart()); 127 | 128 | var oldAssertions = testWithFailureOldAssertions.Select((x, i) => x.Body.Statements.OfType().Single(x => x.Span.CompareTo(oldAssertionComment[i].Span) > 0).ToString().TrimStart() + " /* fail message: " + FluentAssertionAnalyzerDocsUtils.ReplaceStackTrace(exceptionMessageLinesOldAssertions[i]) + " */"); 129 | var newAssertion = testWithFailureNewAssertion.Body.Statements.OfType().Single(x => x.Span.CompareTo(newAssertionComment.Span) > 0).ToString().TrimStart() + " /* fail message: " + exceptionMessageLinesNewAssertion + " */"; 130 | 131 | var arrange = bodyLines.TakeWhile(x => !string.IsNullOrEmpty(x)) 132 | .Select(l => l.Length > paddingToRemove ? l.Substring(paddingToRemove) : l).Aggregate((a, b) => $"{a}{Environment.NewLine}{b}"); 133 | 134 | scenarios.AppendLine($"#### Failure messages"); 135 | scenarios.AppendLine(); 136 | scenarios.AppendLine("```cs"); 137 | scenarios.AppendLine(arrange); 138 | scenarios.AppendLine(); 139 | scenarios.AppendLine($"// old assertion:"); 140 | foreach (var oldAssertion in oldAssertions) 141 | { 142 | scenarios.AppendLine(oldAssertion); 143 | } 144 | scenarios.AppendLine(); 145 | scenarios.AppendLine($"// new assertion:"); 146 | scenarios.AppendLine(newAssertion); 147 | scenarios.AppendLine("```"); 148 | scenarios.AppendLine(); 149 | } 150 | 151 | } 152 | 153 | docs.AppendLine(toc.ToString()); 154 | docs.AppendLine(); 155 | docs.AppendLine(scenarios.ToString()); 156 | 157 | var docsPath = Path.Combine(Environment.CurrentDirectory, "..", "..", "docs", docsName); 158 | Directory.CreateDirectory(Path.GetDirectoryName(docsPath)); 159 | await File.WriteAllTextAsync(docsPath, docs.ToString()); 160 | 161 | } 162 | 163 | private string[] GetMethodExceptionMessageLines(object instance, MethodInfo method) 164 | => GetMethodExceptionMessage(instance, method).Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); 165 | private string GetMethodExceptionMessage(object instance, MethodInfo method) 166 | { 167 | ResetTestFramework(); 168 | 169 | try 170 | { 171 | var result = method.Invoke(instance, null); 172 | if (result is Task task) 173 | { 174 | task.GetAwaiter().GetResult(); 175 | } 176 | } 177 | catch (Exception ex) when (ex.InnerException is null) 178 | { 179 | return ex.Message; 180 | } 181 | catch (Exception ex) when (ex.InnerException is Exception exception) 182 | { 183 | return exception.Message; 184 | } 185 | 186 | throw new InvalidOperationException($"Method {instance.GetType().Name}.{method.Name} did not throw an exception"); 187 | } 188 | 189 | protected virtual void ResetTestFramework() 190 | { 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocsGenerator/DocsVerifier.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Microsoft.CodeAnalysis; 8 | using Microsoft.CodeAnalysis.CSharp; 9 | using Microsoft.CodeAnalysis.CSharp.Syntax; 10 | 11 | namespace FluentAssertions.Analyzers.FluentAssertionAnalyzerDocsGenerator; 12 | 13 | public abstract class DocsVerifier 14 | { 15 | protected abstract string TestAttribute { get; } 16 | protected abstract string TestFile { get; } 17 | 18 | public async Task Execute() 19 | { 20 | var compilation = SyntaxFactory.ParseCompilationUnit(await File.ReadAllTextAsync(TestFile)); 21 | var tree = compilation.SyntaxTree; 22 | 23 | var issues = new StringBuilder(); 24 | 25 | void ValidateAssertions(IEnumerable oldAssertions, ExpressionStatementSyntax newAssertion, MethodDeclarationSyntax method) 26 | { 27 | foreach (var oldAssertion in oldAssertions) 28 | { 29 | if (!oldAssertion.WithoutTrivia().IsEquivalentTo(newAssertion.WithoutTrivia())) 30 | { 31 | issues.AppendLine($"[{TestFile.Split('\\')[^1]}] {method.Identifier} - actual: {oldAssertion.ToFullString()} expected: {newAssertion.ToFullString()}"); 32 | } 33 | } 34 | } 35 | 36 | Console.WriteLine($"File: {Path.GetFileName(TestFile)}"); 37 | 38 | var root = await tree.GetRootAsync(); 39 | var methods = root.DescendantNodes().OfType(); 40 | var methodsMap = methods.ToDictionary(m => m.Identifier.Text); 41 | 42 | foreach (var method in methods.Where(m => m.AttributeLists.Any(list => list.Attributes.Count is 1 && list.Attributes[0].Name.ToString() == TestAttribute))) 43 | { 44 | Console.WriteLine($"### scenario: {method.Identifier}"); 45 | 46 | var (oldAssertions, newAssertion) = GetAssertionsFromMethod(method); 47 | 48 | ValidateAssertions(oldAssertions, newAssertion, method); 49 | 50 | if (methodsMap.TryGetValue($"{method.Identifier.Text}_Failure", out var methodFailure)) 51 | { 52 | var (oldAssertionsFailure, newAssertionFailure) = GetAssertionsFromMethod(methodFailure); 53 | 54 | ValidateAssertions(oldAssertionsFailure, newAssertionFailure, methodFailure); 55 | } 56 | 57 | if (methodsMap.TryGetValue($"{method.Identifier.Text}_Failure_NewAssertion", out var testWithFailureNewAssertion)) 58 | { 59 | var testWithFailureOldAssertions = methodsMap.Where(x => x.Key.StartsWith($"{method.Identifier.Text}_Failure_OldAssertion")).Select(x => x.Value); 60 | 61 | var (_, newAssertionFailure) = GetAssertionsFromMethod(testWithFailureNewAssertion); 62 | foreach (var testWithFailureOldAssertion in testWithFailureOldAssertions) 63 | { 64 | var (oldAssertionsFailure, _) = GetAssertionsFromMethod(testWithFailureOldAssertion); 65 | 66 | ValidateAssertions(oldAssertionsFailure, newAssertionFailure, testWithFailureOldAssertion); 67 | } 68 | } 69 | } 70 | 71 | if (issues.Length > 0) 72 | { 73 | throw new Exception(issues.ToString()); 74 | } 75 | } 76 | 77 | (IEnumerable oldAssertions, ExpressionStatementSyntax newAssertion) GetAssertionsFromMethod(MethodDeclarationSyntax method) 78 | { 79 | var oldAssertionComment = method.DescendantTrivia().FirstOrDefault(x => x.IsKind(SyntaxKind.SingleLineCommentTrivia) && x.ToString().Equals("// old assertion:")); 80 | var newAssertionComment = method.DescendantTrivia().FirstOrDefault(x => x.IsKind(SyntaxKind.SingleLineCommentTrivia) && x.ToString().Equals("// new assertion:")); 81 | 82 | var statements = method.Body.Statements.OfType(); 83 | 84 | var oldAssertions = statements.Where(x => x.Span.CompareTo(oldAssertionComment.Span) > 0 && x.Span.CompareTo(newAssertionComment.Span) < 0); 85 | var newAssertion = statements.SingleOrDefault(x => x.Span.CompareTo(newAssertionComment.Span) > 0); 86 | 87 | return (oldAssertions, newAssertion); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocsGenerator/FluentAssertionAnalyzerDocsUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace FluentAssertions.Analyzers.FluentAssertionAnalyzerDocsGenerator; 5 | 6 | public static class FluentAssertionAnalyzerDocsUtils 7 | { 8 | private static readonly string _fluentAssertionsAnalyzersDocs = "FluentAssertions.Analyzers.FluentAssertionAnalyzerDocs"; 9 | private static readonly string _fluentAssertionsAnalyzersDocsDirectory = Path.Combine("..", _fluentAssertionsAnalyzersDocs); 10 | private static readonly string _fluentAssertionsAnalyzersProjectPath = Path.Combine(_fluentAssertionsAnalyzersDocsDirectory, _fluentAssertionsAnalyzersDocs + ".csproj"); 11 | private static readonly char _unixDirectorySeparator = '/'; 12 | private static readonly string _unixNewLine = "\n"; 13 | 14 | public static string ReplaceStackTrace(string messageIncludingStacktrace) 15 | { 16 | var currentFullPath = Path.GetFullPath(_fluentAssertionsAnalyzersDocsDirectory) + Path.DirectorySeparatorChar; 17 | var repoRootIndex = currentFullPath.LastIndexOf(Path.DirectorySeparatorChar + "fluentassertions.analyzers" + Path.DirectorySeparatorChar, StringComparison.Ordinal); 18 | var unixFullPath = currentFullPath 19 | .Replace(currentFullPath.Substring(0, repoRootIndex), "/Users/runner/work") 20 | .Replace(Path.DirectorySeparatorChar, _unixDirectorySeparator); 21 | 22 | return messageIncludingStacktrace 23 | .Replace(currentFullPath, unixFullPath) 24 | .Replace(Environment.NewLine, _unixNewLine); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocsGenerator/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocsGenerator.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | preview 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.FluentAssertionAnalyzerDocsGenerator/ProgramUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace FluentAssertions.Analyzers.FluentAssertionAnalyzerDocsGenerator; 5 | 6 | public static class ProgramUtils 7 | { 8 | public static Task RunMain(string[] args) 9 | where TDocsGenerator : DocsGenerator, new() 10 | where TDocsVerifier : DocsVerifier, new() => args switch 11 | { 12 | ["generate"] => new TDocsGenerator().Execute(), 13 | ["verify"] => new TDocsVerifier().Execute(), 14 | _ => throw new ArgumentException("Invalid arguments, use 'generate' or 'verify' as argument.") 15 | }; 16 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.TestUtils/CodeAnalyzersUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Microsoft.CodeAnalysis.Diagnostics; 4 | 5 | namespace FluentAssertions.Analyzers.TestUtils 6 | { 7 | public class CodeAnalyzersUtils 8 | { 9 | private static readonly DiagnosticAnalyzer[] AllAnalyzers = CreateAllAnalyzers(); 10 | 11 | public static DiagnosticAnalyzer[] GetAllAnalyzers() => AllAnalyzers; 12 | 13 | private static DiagnosticAnalyzer[] CreateAllAnalyzers() 14 | { 15 | var assembly = typeof(Constants).Assembly; 16 | var analyzersTypes = assembly.GetTypes() 17 | .Where(type => !type.IsAbstract && typeof(DiagnosticAnalyzer).IsAssignableFrom(type)); 18 | var analyzers = analyzersTypes.Select(type => (DiagnosticAnalyzer)Activator.CreateInstance(type)); 19 | 20 | return analyzers.ToArray(); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.TestUtils/CsProjectArguments.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.Diagnostics; 5 | 6 | namespace FluentAssertions.Analyzers.TestUtils; 7 | 8 | public class CsProjectArguments 9 | { 10 | public TargetFramework TargetFramework { get; set; } = TargetFramework.Net8_0; 11 | public string[] Sources { get; set; } 12 | public PackageReference[] PackageReferences { get; set; } = Array.Empty(); 13 | public string Language { get; set; } = LanguageNames.CSharp; 14 | public Dictionary AnalyzerConfigOptions { get; } = new(); 15 | 16 | public AnalyzerConfigOptionsProvider CreateAnalyzerConfigOptionsProvider() => new TestAnalyzerConfigOptionsProvider(AnalyzerConfigOptions); 17 | } 18 | 19 | public static class CsProjectArgumentsExtensions 20 | { 21 | public static TCsProjectArguments WithTargetFramework(this TCsProjectArguments arguments, TargetFramework targetFramework) where TCsProjectArguments : CsProjectArguments 22 | { 23 | arguments.TargetFramework = targetFramework; 24 | return arguments; 25 | } 26 | 27 | public static TCsProjectArguments WithSources(this TCsProjectArguments arguments, params string[] sources) where TCsProjectArguments : CsProjectArguments 28 | { 29 | arguments.Sources = sources; 30 | return arguments; 31 | } 32 | 33 | public static TCsProjectArguments WithPackageReferences(this TCsProjectArguments arguments, params PackageReference[] packageReferences) where TCsProjectArguments : CsProjectArguments 34 | { 35 | arguments.PackageReferences = packageReferences; 36 | return arguments; 37 | } 38 | 39 | public static TCsProjectArguments WithAnalyzerConfigOption(this TCsProjectArguments arguments, string name, string value) where TCsProjectArguments : CsProjectArguments 40 | { 41 | arguments.AnalyzerConfigOptions[name] = value; 42 | return arguments; 43 | } 44 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.TestUtils/CsProjectGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Linq; 4 | using System.Reflection; 5 | using FluentAssertions.Execution; 6 | using Microsoft.CodeAnalysis; 7 | using Microsoft.CodeAnalysis.CSharp; 8 | using Microsoft.CodeAnalysis.Text; 9 | 10 | using XunitAssert = Xunit.Assert; 11 | using System.Net.Http; 12 | using System.Collections.Concurrent; 13 | using System.Collections.ObjectModel; 14 | 15 | namespace FluentAssertions.Analyzers.TestUtils 16 | { 17 | public class CsProjectGenerator 18 | { 19 | static CsProjectGenerator() 20 | { 21 | References = new[] 22 | { 23 | typeof(object), // System.Private.CoreLib 24 | typeof(Console), // System 25 | typeof(Uri), // System.Private.Uri 26 | typeof(Enumerable), // System.Linq 27 | typeof(CSharpCompilation), // Microsoft.CodeAnalysis.CSharp 28 | typeof(Compilation), // Microsoft.CodeAnalysis 29 | typeof(AssertionScope), // FluentAssertions.Core 30 | typeof(AssertionExtensions), // FluentAssertions 31 | typeof(HttpRequestMessage), // System.Net.Http 32 | typeof(ImmutableArray), // System.Collections.Immutable 33 | typeof(ConcurrentBag<>), // System.Collections.Concurrent 34 | typeof(ReadOnlyDictionary<,>), // System.ObjectModel 35 | typeof(Microsoft.VisualStudio.TestTools.UnitTesting.Assert), // MsTest 36 | typeof(XunitAssert), // Xunit 37 | }.Select(type => type.GetTypeInfo().Assembly.Location) 38 | .Append(GetSystemAssemblyPathByName("System.Globalization.dll")) 39 | .Append(GetSystemAssemblyPathByName("System.Text.RegularExpressions.dll")) 40 | .Append(GetSystemAssemblyPathByName("System.Runtime.Extensions.dll")) 41 | .Append(GetSystemAssemblyPathByName("System.Data.Common.dll")) 42 | .Append(GetSystemAssemblyPathByName("System.Threading.Tasks.dll")) 43 | .Append(GetSystemAssemblyPathByName("System.Runtime.dll")) 44 | .Append(GetSystemAssemblyPathByName("System.Reflection.dll")) 45 | .Append(GetSystemAssemblyPathByName("System.Xml.dll")) 46 | .Append(GetSystemAssemblyPathByName("System.Xml.XDocument.dll")) 47 | .Append(GetSystemAssemblyPathByName("System.Private.Xml.Linq.dll")) 48 | .Append(GetSystemAssemblyPathByName("System.Linq.Expressions.dll")) 49 | .Append(GetSystemAssemblyPathByName("System.Collections.dll")) 50 | .Append(GetSystemAssemblyPathByName("netstandard.dll")) 51 | .Append(GetSystemAssemblyPathByName("System.Xml.ReaderWriter.dll")) 52 | .Append(GetSystemAssemblyPathByName("System.Private.Xml.dll")) 53 | .Select(location => (MetadataReference)MetadataReference.CreateFromFile(location)) 54 | .ToImmutableArray(); 55 | 56 | string GetSystemAssemblyPathByName(string assemblyName) 57 | { 58 | var root = System.IO.Path.GetDirectoryName(typeof(object).Assembly.Location); 59 | return System.IO.Path.Combine(root, assemblyName); 60 | } 61 | } 62 | 63 | private static readonly ImmutableArray References; 64 | 65 | private static readonly string DefaultFilePathPrefix = "Test"; 66 | private static readonly string CSharpDefaultFileExt = "cs"; 67 | private static readonly string VisualBasicDefaultExt = "vb"; 68 | private static readonly string TestProjectName = "TestProject"; 69 | 70 | public static Document CreateDocument(CsProjectArguments arguments) => CreateProject(arguments).Documents.First(); 71 | 72 | /// 73 | /// Create a project using the inputted strings as sources. 74 | /// 75 | /// Classes in the form of strings 76 | /// The language the source code is in 77 | /// A Project created out of the Documents created from the source strings 78 | public static Project CreateProject(string[] sources, string language = LanguageNames.CSharp) 79 | { 80 | var arguments = new CsProjectArguments 81 | { 82 | Language = language, 83 | Sources = sources, 84 | TargetFramework = TargetFramework.Net8_0, 85 | }; 86 | return CreateProject(arguments).AddMetadataReferences(References); 87 | } 88 | 89 | public static Project CreateProject(CsProjectArguments arguments) 90 | { 91 | string fileNamePrefix = DefaultFilePathPrefix; 92 | string fileExt = arguments.Language is LanguageNames.CSharp ? CSharpDefaultFileExt : VisualBasicDefaultExt; 93 | 94 | var projectId = ProjectId.CreateNewId(debugName: TestProjectName); 95 | 96 | var solution = new AdhocWorkspace() 97 | .CurrentSolution 98 | .AddProject(projectId, TestProjectName, TestProjectName, arguments.Language); 99 | foreach (var package in arguments.PackageReferences) 100 | { 101 | solution = solution.AddPackageReference(projectId, package); 102 | } 103 | 104 | solution = solution.AddTargetFrameworkReference(projectId, arguments.TargetFramework); 105 | 106 | for (int i = 0; i < arguments.Sources.Length; i++) 107 | { 108 | var newFileName = fileNamePrefix + i + "." + fileExt; 109 | var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName); 110 | solution = solution.AddDocument(documentId, newFileName, SourceText.From(arguments.Sources[i])); 111 | } 112 | return solution.GetProject(projectId); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.TestUtils/FluentAssertions.Analyzers.TestUtils.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.TestUtils/PackageReference.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | public class PackageReference 4 | { 5 | private static readonly List _allDependencies = new(); 6 | 7 | public string Name { get; } 8 | public string Version { get; } 9 | public string Path { get; } 10 | 11 | 12 | private PackageReference(string name, string version, string path) 13 | { 14 | Name = name; 15 | Version = version; 16 | Path = path; 17 | 18 | _allDependencies.Add(this); 19 | } 20 | 21 | public static PackageReference FluentAssertions_6_12_0 { get; } = new("FluentAssertions", "6.12.0", "lib/netstandard2.0/"); 22 | public static PackageReference MSTestTestFramework_3_1_1 { get; } = new("MSTest.TestFramework", "3.1.1", "lib/netstandard2.0/"); 23 | public static PackageReference XunitAssert_2_5_1 { get; } = new("xunit.assert", "2.5.1", "lib/netstandard1.1/"); 24 | public static PackageReference Nunit_3_14_0 { get; } = new("NUnit", "3.14.0", "lib/netstandard2.0/"); 25 | public static PackageReference Nunit_4_0_1 { get; } = new("NUnit", "4.0.1", "lib/net6.0/"); 26 | 27 | internal static PackageReference NETStandard2_0 = new("NETStandard.Library", "2.0.3", "build/netstandard2.0/ref/"); 28 | internal static PackageReference NETStandard2_1 = new("NETStandard.Library.Ref", "2.1.0", "ref/netstandard2.1/ref/"); 29 | internal static PackageReference DotNet6 = new("Microsoft.NETCore.App.Ref", "6.0.31", "ref/net6.0/"); 30 | internal static PackageReference DotNet7 = new("Microsoft.NETCore.App.Ref", "7.0.20", "ref/net7.0/"); 31 | internal static PackageReference DotNet8 = new("Microsoft.NETCore.App.Ref", "8.0.6", "ref/net8.0/"); 32 | 33 | internal static IEnumerable AllDependencies => _allDependencies; 34 | } 35 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.TestUtils/SolutionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Compression; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | using Microsoft.CodeAnalysis; 8 | 9 | public static class SolutionExtensions 10 | { 11 | private static readonly string NugetPackagesPath = Environment.GetEnvironmentVariable("NUGET_PACKAGES") 12 | ?? (OperatingSystem.IsWindows() ? Environment.ExpandEnvironmentVariables("%userprofile%\\.nuget\\packages") : "~/.nuget/packages"); 13 | 14 | private static readonly HttpClient HttpClient = new HttpClient(); 15 | private static readonly Task SetupPackages = Task.WhenAll(PackageReference.AllDependencies.Select(p => DownloadPackageAsync(p.Name, p.Version))); 16 | 17 | public static Solution AddPackageReference(this Solution solution, ProjectId projectId, PackageReference package) 18 | { 19 | SetupPackages.GetAwaiter().GetResult(); 20 | 21 | var packagePath = Path.Combine(NugetPackagesPath, package.Name, package.Version, package.Path); 22 | foreach (var dll in Directory.GetFiles(packagePath, "*.dll")) 23 | { 24 | solution = solution.AddMetadataReference(projectId, MetadataReference.CreateFromFile(dll)); 25 | } 26 | 27 | return solution; 28 | } 29 | 30 | public static Solution AddTargetFrameworkReference(this Solution solution, ProjectId projectId, TargetFramework targetFramework) 31 | { 32 | return targetFramework switch 33 | { 34 | TargetFramework.NetStandard2_0 => solution.AddPackageReference(projectId, PackageReference.NETStandard2_0), 35 | TargetFramework.NetStandard2_1 => solution.AddPackageReference(projectId, PackageReference.NETStandard2_1), 36 | TargetFramework.Net6_0 => solution.AddPackageReference(projectId, PackageReference.DotNet6), 37 | TargetFramework.Net7_0 => solution.AddPackageReference(projectId, PackageReference.DotNet7), 38 | TargetFramework.Net8_0 => solution.AddPackageReference(projectId, PackageReference.DotNet8), 39 | _ => throw new ArgumentOutOfRangeException(nameof(targetFramework), targetFramework, "Unknown target framework"), 40 | }; 41 | } 42 | 43 | private static async Task DownloadPackageAsync(string packageId, string version) 44 | { 45 | var packagePath = Path.Combine(NugetPackagesPath, packageId, version); 46 | if (Directory.Exists(packagePath)) 47 | { 48 | return; 49 | } 50 | 51 | await using var stream = await HttpClient.GetStreamAsync(new Uri($"https://www.nuget.org/api/v2/package/{packageId}/{version}")).ConfigureAwait(false); 52 | using var zip = new ZipArchive(stream, ZipArchiveMode.Read); 53 | 54 | Directory.CreateDirectory(packagePath); 55 | zip.ExtractToDirectory(packagePath, overwriteFiles: true); 56 | } 57 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.TestUtils/TargetFramework.cs: -------------------------------------------------------------------------------- 1 | public enum TargetFramework 2 | { 3 | NetStandard2_0, 4 | NetStandard2_1, 5 | Net6_0, 6 | Net7_0, 7 | Net8_0, 8 | } 9 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.TestUtils/TestAnalyzerConfigOptionsProvider.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 All Rights Reserved 3 | // 4 | // Gérald Barré - https://github.com/meziantou 5 | 6 | using System.Collections.Generic; 7 | using System.Collections.Immutable; 8 | using Microsoft.CodeAnalysis; 9 | using Microsoft.CodeAnalysis.Diagnostics; 10 | 11 | namespace FluentAssertions.Analyzers.TestUtils; 12 | 13 | public sealed class TestAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider 14 | { 15 | public static TestAnalyzerConfigOptionsProvider Empty { get; } = new(ImmutableDictionary.Empty); 16 | 17 | private readonly IDictionary _values; 18 | public TestAnalyzerConfigOptionsProvider(IDictionary values) => _values = values; 19 | 20 | public override AnalyzerConfigOptions GlobalOptions => new TestAnalyzerConfigOptions(_values); 21 | public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => new TestAnalyzerConfigOptions(_values); 22 | public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => new TestAnalyzerConfigOptions(_values); 23 | 24 | private sealed class TestAnalyzerConfigOptions : AnalyzerConfigOptions 25 | { 26 | private readonly IDictionary _values; 27 | 28 | public TestAnalyzerConfigOptions(IDictionary values) => _values = values; 29 | 30 | public override bool TryGetValue(string key, out string value) 31 | { 32 | return _values.TryGetValue(key, out value); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.Tests/CodeFixVerifierArguments.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using FluentAssertions.Analyzers.TestUtils; 3 | using Microsoft.CodeAnalysis.CodeFixes; 4 | using Microsoft.CodeAnalysis.Diagnostics; 5 | 6 | namespace FluentAssertions.Analyzers.Tests; 7 | 8 | public class CodeFixVerifierArguments : CsProjectArguments 9 | { 10 | public List FixedSources { get; } = new(); 11 | 12 | public List DiagnosticAnalyzers { get; } = new(); 13 | 14 | public List CodeFixProviders { get; } = new(); 15 | 16 | public CodeFixVerifierArguments() { } 17 | 18 | public CodeFixVerifierArguments WithDiagnosticAnalyzer() where TDiagnosticAnalyzer : DiagnosticAnalyzer, new() 19 | { 20 | DiagnosticAnalyzers.Add(new TDiagnosticAnalyzer()); 21 | return this; 22 | } 23 | 24 | public CodeFixVerifierArguments WithCodeFixProvider() where TCodeFixProvider : CodeFixProvider, new() 25 | { 26 | CodeFixProviders.Add(new TCodeFixProvider()); 27 | return this; 28 | } 29 | 30 | public CodeFixVerifierArguments WithFixedSources(params string[] fixedSources) 31 | { 32 | FixedSources.AddRange(fixedSources); 33 | return this; 34 | } 35 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.Tests/DiagnosticResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.CodeAnalysis; 3 | 4 | namespace FluentAssertions.Analyzers.Tests 5 | { 6 | /// 7 | /// Location where the diagnostic appears, as determined by path, line number, and column number. 8 | /// 9 | public struct DiagnosticResultLocation 10 | { 11 | public DiagnosticResultLocation(string path, int line, int column) 12 | { 13 | if (line < -1) 14 | { 15 | throw new ArgumentOutOfRangeException(nameof(line), "line must be >= -1"); 16 | } 17 | 18 | if (column < -1) 19 | { 20 | throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1"); 21 | } 22 | 23 | Path = path; 24 | Line = line; 25 | Column = column; 26 | } 27 | 28 | public string Path { get; } 29 | public int Line { get; } 30 | public int Column { get; } 31 | } 32 | 33 | /// 34 | /// Struct that stores information about a Diagnostic appearing in a source 35 | /// 36 | public struct DiagnosticResult 37 | { 38 | private DiagnosticResultLocation[] locations; 39 | 40 | public DiagnosticResultLocation[] Locations 41 | { 42 | get { return locations ?? (locations = new DiagnosticResultLocation[0]); } 43 | 44 | set 45 | { 46 | locations = value; 47 | } 48 | } 49 | 50 | public DiagnosticSeverity Severity { get; set; } 51 | 52 | public string Id { get; set; } 53 | 54 | public string Message { get; set; } 55 | 56 | public string Path => Locations.Length > 0 ? Locations[0].Path : ""; 57 | 58 | public int Line => Locations.Length > 0 ? Locations[0].Line : -1; 59 | 60 | public int Column => Locations.Length > 0 ? Locations[0].Column : -1; 61 | 62 | public string VisitorName { get; set; } 63 | } 64 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.Tests/DiagnosticVerifierArguments.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using FluentAssertions.Analyzers.TestUtils; 3 | using Microsoft.CodeAnalysis.Diagnostics; 4 | 5 | namespace FluentAssertions.Analyzers.Tests; 6 | 7 | public class DiagnosticVerifierArguments : CsProjectArguments 8 | { 9 | public List ExpectedDiagnostics { get; } = new(); 10 | 11 | public List DiagnosticAnalyzers { get; } = new(); 12 | 13 | public DiagnosticVerifierArguments() { } 14 | 15 | public DiagnosticVerifierArguments WithDiagnosticAnalyzer() where TDiagnosticAnalyzer : DiagnosticAnalyzer, new() 16 | { 17 | DiagnosticAnalyzers.Add(new TDiagnosticAnalyzer()); 18 | return this; 19 | } 20 | 21 | public DiagnosticVerifierArguments WithAllAnalyzers() 22 | { 23 | DiagnosticAnalyzers.Clear(); 24 | DiagnosticAnalyzers.AddRange(CodeAnalyzersUtils.GetAllAnalyzers()); 25 | return this; 26 | } 27 | 28 | public DiagnosticVerifierArguments WithExpectedDiagnostics(params DiagnosticResult[] expectedDiagnostics) 29 | { 30 | ExpectedDiagnostics.AddRange(expectedDiagnostics); 31 | return this; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.Tests/FluentAssertions.Analyzers.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | runtime; build; native; contentfiles; analyzers; buildtransitive 12 | all 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.Tests/TestAttributes.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | 6 | namespace FluentAssertions.Analyzers.Tests; 7 | 8 | [AttributeUsage(AttributeTargets.Method)] 9 | public class NotImplementedAttribute : TestCategoryBaseAttribute 10 | { 11 | public string Reason { get; set; } 12 | 13 | public override IList TestCategories => new[] { "NotImplemented" }; 14 | } 15 | 16 | [AttributeUsage(AttributeTargets.Method)] 17 | public class ImplementedAttribute : TestCategoryBaseAttribute 18 | { 19 | public string Reason { get; set; } 20 | 21 | public override IList TestCategories => new[] { "Completed" }; 22 | } 23 | 24 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] 25 | public class AssertionDiagnosticAttribute : Attribute, ITestDataSource 26 | { 27 | public string Assertion { get; } 28 | 29 | public AssertionDiagnosticAttribute(string assertion) => Assertion = assertion; 30 | 31 | public IEnumerable GetData(MethodInfo methodInfo) 32 | { 33 | foreach (var assertion in TestCasesInputUtils.GetTestCases(Assertion)) 34 | { 35 | yield return new object[] { assertion }; 36 | } 37 | } 38 | 39 | public string GetDisplayName(MethodInfo methodInfo, object[] data) => $"{methodInfo.Name}(\"{data[0]}\")"; 40 | } 41 | 42 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] 43 | public class IgnoreAssertionDiagnosticAttribute : Attribute 44 | { 45 | public string Assertion { get; } 46 | 47 | public IgnoreAssertionDiagnosticAttribute(string assertion) => Assertion = assertion; 48 | } 49 | 50 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] 51 | public class AssertionCodeFixAttribute : Attribute, ITestDataSource 52 | { 53 | public string OldAssertion { get; } 54 | public string NewAssertion { get; } 55 | 56 | public AssertionCodeFixAttribute(string oldAssertion, string newAssertion) => (OldAssertion, NewAssertion) = (oldAssertion, newAssertion); 57 | 58 | public IEnumerable GetData(MethodInfo methodInfo) 59 | { 60 | foreach (var (oldAssertion, newAssertion) in TestCasesInputUtils.GetTestCases(OldAssertion, NewAssertion)) 61 | { 62 | yield return new object[] { oldAssertion, newAssertion }; 63 | } 64 | } 65 | 66 | public string GetDisplayName(MethodInfo methodInfo, object[] data) => $"{methodInfo.Name}(\"old: {data[0]}\", new: {data[1]}\")"; 67 | } 68 | 69 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] 70 | public class IgnoreAssertionCodeFixAttribute : Attribute 71 | { 72 | public string OldAssertion { get; } 73 | public string NewAssertion { get; } 74 | 75 | public IgnoreAssertionCodeFixAttribute(string oldAssertion, string newAssertion) => (OldAssertion, NewAssertion) = (oldAssertion, newAssertion); 76 | } 77 | 78 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] 79 | public class AssertionMethodCodeFixAttribute : Attribute, ITestDataSource 80 | { 81 | public string MethodArguments { get; } 82 | public string OldAssertion { get; } 83 | public string NewAssertion { get; } 84 | 85 | public AssertionMethodCodeFixAttribute(string methodArguments, string oldAssertion, string newAssertion) 86 | => (MethodArguments, OldAssertion, NewAssertion) = (methodArguments, oldAssertion, newAssertion); 87 | 88 | public IEnumerable GetData(MethodInfo methodInfo) 89 | { 90 | foreach (var (oldAssertion, newAssertion) in TestCasesInputUtils.GetTestCases(OldAssertion, NewAssertion)) 91 | { 92 | yield return new object[] { MethodArguments, oldAssertion, newAssertion }; 93 | } 94 | } 95 | 96 | public string GetDisplayName(MethodInfo methodInfo, object[] data) => $"{methodInfo.Name}(\"arguments\":{data[0]}, \"old: {data[1]}\", new: {data[2]}\")"; 97 | } 98 | 99 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] 100 | public class IgnoreAssertionMethodCodeFixAttribute : Attribute 101 | { 102 | public string MethodArguments { get; } 103 | public string OldAssertion { get; } 104 | public string NewAssertion { get; } 105 | 106 | public IgnoreAssertionMethodCodeFixAttribute(string methodArguments, string oldAssertion, string newAssertion) 107 | => (MethodArguments, OldAssertion, NewAssertion) = (methodArguments, oldAssertion, newAssertion); 108 | } 109 | 110 | public static class TestCasesInputUtils 111 | { 112 | private static readonly string Empty = string.Empty; 113 | private static readonly string Because = "\"because it's possible\""; 114 | private static readonly string FormattedBecause = "\"because message with {0} placeholders {1} at {2}\", 3, \"is awesome\", DateTime.Now.Add(2.Seconds())"; 115 | public static IEnumerable GetTestCases(string assertion) 116 | { 117 | if (!assertion.Contains("{0}")) 118 | { 119 | yield return assertion; 120 | yield break; 121 | } 122 | 123 | yield return SafeFormat(assertion, Empty); 124 | yield return SafeFormat(assertion, Because); 125 | yield return SafeFormat(assertion, FormattedBecause); 126 | } 127 | public static IEnumerable<(string oldAssertion, string newAssertion)> GetTestCases(string oldAssertion, string newAssertion) 128 | { 129 | if (!oldAssertion.Contains("{0}") && !newAssertion.Contains("{0}")) 130 | { 131 | yield return (oldAssertion, newAssertion); 132 | yield break; 133 | } 134 | 135 | yield return (SafeFormat(oldAssertion, Empty), SafeFormat(newAssertion, Empty)); 136 | yield return (SafeFormat(oldAssertion, Because), SafeFormat(newAssertion, Because)); 137 | yield return (SafeFormat(oldAssertion, FormattedBecause), SafeFormat(newAssertion, FormattedBecause)); 138 | } 139 | 140 | /// 141 | /// Adds an comma before the additional argument if needed. 142 | /// 143 | private static string SafeFormat(string assertion, string arg) 144 | { 145 | var index = assertion.IndexOf("{0}"); 146 | if (!string.IsNullOrEmpty(arg) && assertion[index - 1] != '(') 147 | { 148 | return string.Format(assertion, ", " + arg); 149 | } 150 | return string.Format(assertion, arg); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.Tests/TestConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | 3 | [assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] 4 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.Tests/Tips/AsyncVoidTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions.Analyzers.TestUtils; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace FluentAssertions.Analyzers.Tests 6 | { 7 | [TestClass] 8 | public class AsyncVoidTests 9 | { 10 | [TestMethod] 11 | [Implemented] 12 | public void AssignAsyncVoidMethodToAction_TestAnalyzer() 13 | { 14 | const string statement = "Action action = AsyncVoidMethod;"; 15 | var source = GenerateCode.AsyncFunctionStatement(statement); 16 | 17 | DiagnosticVerifier.VerifyCSharpDiagnosticUsingAllAnalyzers(source); 18 | } 19 | 20 | [TestMethod] 21 | [Implemented] 22 | public void AssignVoidLambdaToAction_TestAnalyzer() 23 | { 24 | const string statement = "Action action = () => {};"; 25 | var source = GenerateCode.AsyncFunctionStatement(statement); 26 | 27 | DiagnosticVerifier.VerifyCSharpDiagnosticUsingAllAnalyzers(source); 28 | } 29 | 30 | [DataRow("Action action = async () => { await Task.CompletedTask; };")] 31 | [DataRow("Action action1 = async () => { await Task.CompletedTask; }, action2 = async () => { await Task.CompletedTask; };")] 32 | [DataRow("Action action1 = () => { }, action2 = async () => { await Task.CompletedTask; };")] 33 | [DataRow("Action action1 = async () => { await Task.CompletedTask; }, action2 = () => { };")] 34 | [DataTestMethod] 35 | [Implemented] 36 | public void AssignAsyncVoidLambdaToAction_TestAnalyzer(string assertion) 37 | { 38 | var source = GenerateCode.AsyncFunctionStatement(assertion); 39 | 40 | DiagnosticVerifier.VerifyCSharpDiagnosticUsingAllAnalyzers(source, new DiagnosticResult 41 | { 42 | Id = AsyncVoidAnalyzer.DiagnosticId, 43 | Message = AsyncVoidAnalyzer.Message, 44 | Locations = new DiagnosticResultLocation[] 45 | { 46 | new DiagnosticResultLocation("Test0.cs", 10,13) 47 | }, 48 | Severity = DiagnosticSeverity.Warning 49 | }); 50 | } 51 | 52 | [AssertionCodeFix( 53 | oldAssertion: "Action action = async () => { await Task.CompletedTask; };", 54 | newAssertion: "Func action = async () => { await Task.CompletedTask; };")] 55 | [AssertionCodeFix( 56 | oldAssertion: "Action action1 = async () => { await Task.CompletedTask; }, action2 = async () => { await Task.CompletedTask; };", 57 | newAssertion: "Func action1 = async () => { await Task.CompletedTask; }, action2 = async () => { await Task.CompletedTask; };")] 58 | [DataTestMethod] 59 | [NotImplemented] 60 | public void AssignAsyncVoidLambdaToAction_TestCodeFix(string oldAssertion, string newAssertion) 61 | { 62 | var oldSource = GenerateCode.AsyncFunctionStatement(oldAssertion); 63 | var newSource = GenerateCode.AsyncFunctionStatement(newAssertion); 64 | 65 | DiagnosticVerifier.VerifyCSharpFix(oldSource, newSource); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.Tests/Tips/FluentAssertionsTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions.Analyzers.TestUtils; 2 | using Microsoft.CodeAnalysis.CodeFixes; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace FluentAssertions.Analyzers.Tests 6 | { 7 | [TestClass] 8 | public class FluentAssertionsTests 9 | { 10 | [TestMethod] 11 | [Implemented] 12 | public void ShouldNotReturnEarly_WhenFluentAssertionsIsNotLoaded() 13 | { 14 | const string source = @" 15 | namespace TestProject 16 | { 17 | public class TestClass 18 | { 19 | } 20 | }"; 21 | 22 | DiagnosticVerifier.VerifyDiagnostic(new DiagnosticVerifierArguments() 23 | .WithDiagnosticAnalyzer() 24 | .WithSources(source) 25 | ); 26 | } 27 | 28 | [TestMethod] 29 | [Implemented] 30 | public void ShouldNotReturnEarly_WhenShouldInvocationIsNotFromFluentAssertions() 31 | { 32 | const string source = @" 33 | namespace TestProject 34 | { 35 | public class TestClass 36 | { 37 | public void TestMethod() 38 | { 39 | var test = new TestClassA(); 40 | test.Length.Should().BeTrue(); 41 | } 42 | } 43 | public class TestClassA 44 | { 45 | public TestClassA Length => this; 46 | public TestClassB Should() => new TestClassB(); 47 | } 48 | public class TestClassB 49 | { 50 | public void BeTrue() { } 51 | } 52 | }"; 53 | 54 | DiagnosticVerifier.VerifyDiagnostic(new DiagnosticVerifierArguments() 55 | .WithDiagnosticAnalyzer() 56 | .WithPackageReferences(PackageReference.FluentAssertions_6_12_0) 57 | .WithSources(source) 58 | ); 59 | } 60 | 61 | [TestMethod] 62 | [Implemented] 63 | public void ShouldAddFluentAssertionsUsing_WhenFluentAssertionIsNotInScope_ForXunit() 64 | => ShouldAddFluentAssertionsUsing_WhenFluentAssertionIsNotInScope("True", "using Xunit;", PackageReference.XunitAssert_2_5_1); 65 | 66 | [TestMethod] 67 | [Implemented] 68 | public void ShouldNotAddFluentAssertionsUsing_WhenFluentAssertionIsInGlobalScope_ForXunit() 69 | => ShouldNotAddFluentAssertionsUsing_WhenFluentAssertionIsInGlobalScope("True", "using Xunit;", PackageReference.XunitAssert_2_5_1); 70 | 71 | [TestMethod] 72 | [Implemented] 73 | public void ShouldNotAddFluentAssertionsUsing_WhenFluentAssertionIsInAnyScope_ForXunit() 74 | => ShouldNotAddFluentAssertionsUsing_WhenFluentAssertionIsInAnyScope("True", "using Xunit;", PackageReference.XunitAssert_2_5_1); 75 | 76 | [TestMethod] 77 | [Implemented] 78 | public void ShouldAddFluentAssertionsUsing_WhenFluentAssertionIsNotInScope_ForMsTest() 79 | => ShouldAddFluentAssertionsUsing_WhenFluentAssertionIsNotInScope("IsTrue", "using Microsoft.VisualStudio.TestTools.UnitTesting;", PackageReference.MSTestTestFramework_3_1_1); 80 | 81 | [TestMethod] 82 | [Implemented] 83 | public void ShouldNotAddFluentAssertionsUsing_WhenFluentAssertionIsInGlobalScope_ForMsTest() 84 | => ShouldNotAddFluentAssertionsUsing_WhenFluentAssertionIsInGlobalScope("IsTrue", "using Microsoft.VisualStudio.TestTools.UnitTesting;", PackageReference.MSTestTestFramework_3_1_1); 85 | 86 | [TestMethod] 87 | [Implemented] 88 | public void ShouldNotAddFluentAssertionsUsing_WhenFluentAssertionIsInAnyScope_ForMsTest() 89 | => ShouldNotAddFluentAssertionsUsing_WhenFluentAssertionIsInAnyScope("IsTrue", "using Microsoft.VisualStudio.TestTools.UnitTesting;", PackageReference.MSTestTestFramework_3_1_1); 90 | 91 | private void ShouldAddFluentAssertionsUsing_WhenFluentAssertionIsNotInScope(string assertTrue, string usingDirective, PackageReference testingLibraryReference) where TCodeFixProvider : CodeFixProvider, new() 92 | { 93 | string source = $@" 94 | {usingDirective} 95 | namespace TestProject 96 | {{ 97 | public class TestClass 98 | {{ 99 | public void TestMethod(bool subject) 100 | {{ 101 | Assert.{assertTrue}(subject); 102 | }} 103 | }} 104 | }}"; 105 | string newSource = @$" 106 | using FluentAssertions; 107 | {usingDirective} 108 | namespace TestProject 109 | {{ 110 | public class TestClass 111 | {{ 112 | public void TestMethod(bool subject) 113 | {{ 114 | subject.Should().BeTrue(); 115 | }} 116 | }} 117 | }}"; 118 | DiagnosticVerifier.VerifyFix(new CodeFixVerifierArguments() 119 | .WithDiagnosticAnalyzer() 120 | .WithCodeFixProvider() 121 | .WithPackageReferences(PackageReference.FluentAssertions_6_12_0, testingLibraryReference) 122 | .WithSources(source) 123 | .WithFixedSources(newSource) 124 | ); 125 | } 126 | 127 | private void ShouldNotAddFluentAssertionsUsing_WhenFluentAssertionIsInGlobalScope(string assertTrue, string usingDirective, PackageReference testingLibraryReference) where TCodeFixProvider : CodeFixProvider, new() 128 | { 129 | string source = $@" 130 | {usingDirective} 131 | namespace TestProject 132 | {{ 133 | public class TestClass 134 | {{ 135 | public void TestMethod(bool subject) 136 | {{ 137 | Assert.{assertTrue}(subject); 138 | }} 139 | }} 140 | }}"; 141 | const string globalUsings = "global using FluentAssertions;"; 142 | string newSource = @$" 143 | {usingDirective} 144 | namespace TestProject 145 | {{ 146 | public class TestClass 147 | {{ 148 | public void TestMethod(bool subject) 149 | {{ 150 | subject.Should().BeTrue(); 151 | }} 152 | }} 153 | }}"; 154 | 155 | DiagnosticVerifier.VerifyFix(new CodeFixVerifierArguments() 156 | .WithDiagnosticAnalyzer() 157 | .WithCodeFixProvider() 158 | .WithPackageReferences(PackageReference.FluentAssertions_6_12_0, testingLibraryReference) 159 | .WithSources(source, globalUsings) 160 | .WithFixedSources(newSource) 161 | ); 162 | } 163 | 164 | private void ShouldNotAddFluentAssertionsUsing_WhenFluentAssertionIsInAnyScope(string assertTrue, string usingDirective, PackageReference testingLibraryReference) where TCodeFixProvider : CodeFixProvider, new() 165 | { 166 | string source = $@" 167 | {usingDirective} 168 | namespace TestProject 169 | {{ 170 | using FluentAssertions; 171 | public class TestClass 172 | {{ 173 | public void TestMethod(bool subject) 174 | {{ 175 | Assert.{assertTrue}(subject); 176 | }} 177 | }} 178 | }}"; 179 | string newSource = @$" 180 | {usingDirective} 181 | namespace TestProject 182 | {{ 183 | using FluentAssertions; 184 | public class TestClass 185 | {{ 186 | public void TestMethod(bool subject) 187 | {{ 188 | subject.Should().BeTrue(); 189 | }} 190 | }} 191 | }}"; 192 | 193 | DiagnosticVerifier.VerifyFix(new CodeFixVerifierArguments() 194 | .WithDiagnosticAnalyzer() 195 | .WithCodeFixProvider() 196 | .WithPackageReferences(PackageReference.FluentAssertions_6_12_0, testingLibraryReference) 197 | .WithSources(source) 198 | .WithFixedSources(newSource) 199 | ); 200 | } 201 | } 202 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.Tests/Tips/NullConditionalAssertionTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions.Analyzers.TestUtils; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System.Text; 4 | 5 | namespace FluentAssertions.Analyzers.Tests.Tips 6 | { 7 | [TestClass] 8 | public class NullConditionalAssertionTests 9 | { 10 | [DataTestMethod] 11 | [AssertionDiagnostic("actual?.Should().Be(expected{0});")] 12 | [AssertionDiagnostic("actual?.MyProperty.Should().Be(\"test\"{0});")] 13 | [AssertionDiagnostic("actual.MyProperty?.Should().Be(\"test\"{0});")] 14 | [AssertionDiagnostic("(actual.MyProperty)?.Should().Be(\"test\"{0});")] 15 | [AssertionDiagnostic("(actual?.MyProperty)?.Should().Be(\"test\"{0});")] 16 | [AssertionDiagnostic("actual?.MyProperty.Should().Be(actual?.MyProperty{0});")] 17 | [AssertionDiagnostic("actual.MyList?.Where(obj => obj?.ToString() == null).Count().Should().Be(0{0});")] 18 | [Implemented] 19 | public void NullConditionalMayNotExecuteTest(string assertion) 20 | { 21 | DiagnosticVerifier.VerifyDiagnostic(new DiagnosticVerifierArguments() 22 | .WithDiagnosticAnalyzer() 23 | .WithSources(Code(assertion)) 24 | .WithPackageReferences(PackageReference.FluentAssertions_6_12_0) 25 | .WithExpectedDiagnostics(new DiagnosticResult 26 | { 27 | Id = FluentAssertionsAnalyzer.DiagnosticId, 28 | Message = DiagnosticMetadata.NullConditionalMayNotExecute.Message, 29 | Severity = Microsoft.CodeAnalysis.DiagnosticSeverity.Info, // TODO: change to warning 30 | VisitorName = nameof(DiagnosticMetadata.NullConditionalMayNotExecute), 31 | Locations = new DiagnosticResultLocation[] 32 | { 33 | new DiagnosticResultLocation("Test0.cs", 11, 13) 34 | } 35 | }) 36 | ); 37 | } 38 | 39 | [DataTestMethod] 40 | [AssertionDiagnostic("(actual?.MyProperty).Should().Be(\"test\"{0});")] 41 | [AssertionDiagnostic("actual.MyProperty.Should().Be(actual?.MyProperty{0});")] 42 | [AssertionDiagnostic("actual.MyList.Where(obj => obj?.ToString() == null).Should().HaveCount(6{0});")] 43 | [Implemented] 44 | public void NullConditionalWillStillExecuteTest(string assertion) 45 | { 46 | DiagnosticVerifier.VerifyDiagnostic(new DiagnosticVerifierArguments() 47 | .WithDiagnosticAnalyzer() 48 | .WithSources(Code(assertion)) 49 | .WithPackageReferences(PackageReference.FluentAssertions_6_12_0) 50 | ); 51 | } 52 | 53 | private static string Code(string assertion) => 54 | new StringBuilder() 55 | .AppendLine("using System;") 56 | .AppendLine("using System.Collections.Generic;") 57 | .AppendLine("using System.Linq;") 58 | .AppendLine("using FluentAssertions;using FluentAssertions.Extensions;") 59 | .AppendLine("namespace TestNamespace") 60 | .AppendLine("{") 61 | .AppendLine(" class TestClass") 62 | .AppendLine(" {") 63 | .AppendLine(" void TestMethod(MyClass actual, MyClass expected)") 64 | .AppendLine(" {") 65 | .AppendLine($" {assertion}") 66 | .AppendLine(" }") 67 | .AppendLine(" }") 68 | .AppendLine(" class MyClass") 69 | .AppendLine(" {") 70 | .AppendLine(" public string MyProperty { get; set; }") 71 | .AppendLine(" public List MyList { get; set; }") 72 | .AppendLine(" }") 73 | .AppendLine("}") 74 | .ToString(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.Tests/Tips/NumericTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions.Analyzers.TestUtils; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace FluentAssertions.Analyzers.Tests 6 | { 7 | [TestClass] 8 | public class NumericTests 9 | { 10 | [DataTestMethod] 11 | [AssertionDiagnostic("actual.Should().BeGreaterThan(0{0});")] 12 | [AssertionDiagnostic("actual.Should().BeGreaterThan(0{0}).ToString();")] 13 | [Implemented] 14 | public void NumericShouldBePositive_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic(assertion, DiagnosticMetadata.NumericShouldBePositive_ShouldBeGreaterThan); 15 | 16 | [DataTestMethod] 17 | [AssertionCodeFix( 18 | oldAssertion: "actual.Should().BeGreaterThan(0{0});", 19 | newAssertion: "actual.Should().BePositive({0});")] 20 | [AssertionCodeFix( 21 | oldAssertion: "actual.Should().BeGreaterThan(0{0}).ToString();", 22 | newAssertion: "actual.Should().BePositive({0}).ToString();")] 23 | [Implemented] 24 | public void NumericShouldBePositive_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix(oldAssertion, newAssertion); 25 | 26 | [DataTestMethod] 27 | [AssertionDiagnostic("actual.Should().BeLessThan(0{0});")] 28 | [AssertionDiagnostic("actual.Should().BeLessThan(0{0}).ToString();")] 29 | [Implemented] 30 | public void NumericShouldBeNegative_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic(assertion, DiagnosticMetadata.NumericShouldBeNegative_ShouldBeLessThan); 31 | 32 | [DataTestMethod] 33 | [AssertionCodeFix( 34 | oldAssertion: "actual.Should().BeLessThan(0{0});", 35 | newAssertion: "actual.Should().BeNegative({0});")] 36 | [AssertionCodeFix( 37 | oldAssertion: "actual.Should().BeLessThan(0{0}).ToString();", 38 | newAssertion: "actual.Should().BeNegative({0}).ToString();")] 39 | [Implemented] 40 | public void NumericShouldBeNegative_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix(oldAssertion, newAssertion); 41 | 42 | [DataTestMethod] 43 | [AssertionDiagnostic("actual.Should().BeGreaterOrEqualTo(lower{0}).And.BeLessOrEqualTo(upper);")] 44 | [AssertionDiagnostic("actual.Should().BeGreaterOrEqualTo(lower).And.BeLessOrEqualTo(upper{0});")] 45 | [Implemented] 46 | public void NumericShouldBeInRange_BeGreaterOrEqualToAndBeLessOrEqualTo_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic(assertion, DiagnosticMetadata.NumericShouldBeInRange_BeGreaterOrEqualToAndBeLessOrEqualTo); 47 | 48 | [DataTestMethod] 49 | [AssertionDiagnostic("actual.Should().BeLessOrEqualTo(upper{0}).And.BeGreaterOrEqualTo(lower);")] 50 | [AssertionDiagnostic("actual.Should().BeLessOrEqualTo(upper).And.BeGreaterOrEqualTo(lower{0});")] 51 | [Implemented] 52 | public void NumericShouldBeInRange_BeLessOrEqualToAndBeGreaterOrEqualTo_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic(assertion, DiagnosticMetadata.NumericShouldBeInRange_BeLessOrEqualToAndBeGreaterOrEqualTo); 53 | 54 | [DataTestMethod] 55 | [DataRow("actual.Should().BeLessOrEqualTo(upper, \"because reason 1\").And.BeGreaterOrEqualTo(lower, \"because reason 2\");")] 56 | [DataRow("actual.Should().BeLessOrEqualTo(upper, \"because reason 1\").And.BeGreaterOrEqualTo(lower, \"because reason 2\");")] 57 | [Implemented] 58 | public void NumericShouldBeInRange_BeLessOrEqualToAndBeGreaterOrEqualTo_WithMessagesInBothAssertions_TestAnalyzer(string assertion) 59 | { 60 | verifyNoDiagnostic("double"); 61 | verifyNoDiagnostic("float"); 62 | verifyNoDiagnostic("decimal"); 63 | 64 | void verifyNoDiagnostic(string type) 65 | { 66 | var source = GenerateCode.NumericAssertion(assertion, type); 67 | DiagnosticVerifier.VerifyDiagnostic(new DiagnosticVerifierArguments() 68 | .WithSources(source) 69 | .WithAllAnalyzers() 70 | .WithPackageReferences(PackageReference.FluentAssertions_6_12_0) 71 | ); 72 | } 73 | } 74 | 75 | [DataTestMethod] 76 | [AssertionCodeFix( 77 | oldAssertion: "actual.Should().BeGreaterOrEqualTo(lower{0}).And.BeLessOrEqualTo(upper);", 78 | newAssertion: "actual.Should().BeInRange(lower, upper{0});")] 79 | [AssertionCodeFix( 80 | oldAssertion: "actual.Should().BeGreaterOrEqualTo(lower).And.BeLessOrEqualTo(upper{0});", 81 | newAssertion: "actual.Should().BeInRange(lower, upper{0});")] 82 | [AssertionCodeFix( 83 | oldAssertion: "actual.Should().BeLessOrEqualTo(upper{0}).And.BeGreaterOrEqualTo(lower);", 84 | newAssertion: "actual.Should().BeInRange(lower, upper{0});")] 85 | [AssertionCodeFix( 86 | oldAssertion: "actual.Should().BeLessOrEqualTo(upper).And.BeGreaterOrEqualTo(lower{0});", 87 | newAssertion: "actual.Should().BeInRange(lower, upper{0});")] 88 | [NotImplemented] 89 | public void NumericShouldBeInRange_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix(oldAssertion, newAssertion); 90 | 91 | [DataTestMethod] 92 | [AssertionDiagnostic("Math.Abs(expected - actual).Should().BeLessOrEqualTo(delta{0});")] 93 | [Implemented] 94 | public void NumericShouldBeApproximately_TestAnalyzer(string assertion) => VerifyCSharpDiagnostic(assertion, DiagnosticMetadata.NumericShouldBeApproximately_MathAbsShouldBeLessOrEqualTo); 95 | 96 | [DataTestMethod] 97 | [AssertionDiagnostic("(Math.Abs(expected - actual) + 1).Should().BeLessOrEqualTo(delta{0});")] 98 | [Implemented] 99 | public void NumericShouldBeApproximately_TestNoAnalyzer(string assertion) => DiagnosticVerifier.VerifyCSharpDiagnosticUsingAllAnalyzers(GenerateCode.NumericAssertion(assertion, "double")); 100 | 101 | [DataTestMethod] 102 | [AssertionCodeFix( 103 | oldAssertion: "Math.Abs(expected - actual).Should().BeLessOrEqualTo(delta{0});", 104 | newAssertion: "actual.Should().BeApproximately(expected, delta{0});")] 105 | [Implemented] 106 | public void NumericShouldBeApproximately_TestCodeFix(string oldAssertion, string newAssertion) => VerifyCSharpFix(oldAssertion, newAssertion); 107 | 108 | private void VerifyCSharpDiagnostic(string sourceAssertion, DiagnosticMetadata metadata) 109 | { 110 | VerifyCSharpDiagnostic(sourceAssertion, metadata, "double"); 111 | VerifyCSharpDiagnostic(sourceAssertion, metadata, "float"); 112 | VerifyCSharpDiagnostic(sourceAssertion, metadata, "decimal"); 113 | } 114 | 115 | private void VerifyCSharpDiagnostic(string sourceAssertion, DiagnosticMetadata metadata, string numericType) 116 | { 117 | var source = GenerateCode.NumericAssertion(sourceAssertion, numericType); 118 | 119 | DiagnosticVerifier.VerifyDiagnostic(new DiagnosticVerifierArguments() 120 | .WithSources(source) 121 | .WithAllAnalyzers() 122 | .WithPackageReferences(PackageReference.FluentAssertions_6_12_0) 123 | .WithExpectedDiagnostics(new DiagnosticResult 124 | { 125 | Id = FluentAssertionsAnalyzer.DiagnosticId, 126 | Message = metadata.Message, 127 | VisitorName = metadata.Name, 128 | Locations = new DiagnosticResultLocation[] 129 | { 130 | new DiagnosticResultLocation("Test0.cs", 10, 13) 131 | }, 132 | Severity = DiagnosticSeverity.Info 133 | }) 134 | ); 135 | } 136 | 137 | private void VerifyCSharpFix(string oldSourceAssertion, string newSourceAssertion) 138 | { 139 | VerifyCSharpFix(oldSourceAssertion, newSourceAssertion, "double"); 140 | VerifyCSharpFix(oldSourceAssertion, newSourceAssertion, "float"); 141 | VerifyCSharpFix(oldSourceAssertion, newSourceAssertion, "decimal"); 142 | } 143 | 144 | private void VerifyCSharpFix(string oldSourceAssertion, string newSourceAssertion, string numericType) 145 | { 146 | var oldSource = GenerateCode.NumericAssertion(oldSourceAssertion, numericType); 147 | var newSource = GenerateCode.NumericAssertion(newSourceAssertion, numericType); 148 | 149 | DiagnosticVerifier.VerifyFix(new CodeFixVerifierArguments() 150 | .WithCodeFixProvider() 151 | .WithDiagnosticAnalyzer() 152 | .WithSources(oldSource) 153 | .WithFixedSources(newSource) 154 | .WithPackageReferences(PackageReference.FluentAssertions_6_12_0) 155 | ); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers.Tests/Tips/ShouldEqualsTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions.Analyzers.TestUtils; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace FluentAssertions.Analyzers.Tests.Tips 6 | { 7 | [TestClass] 8 | public class ShouldEqualsTests 9 | { 10 | [TestMethod] 11 | [Implemented] 12 | public void ShouldEquals_TestAnalyzer() 13 | => VerifyCSharpDiagnosticExpressionBody("actual.Should().Equals(expected);", DiagnosticMetadata.ShouldBe_ShouldEquals); 14 | 15 | [TestMethod] 16 | [Implemented] 17 | public void ShouldEquals_ShouldBe_ObjectType_TestCodeFix() 18 | { 19 | var oldSource = GenerateCode.ObjectStatement("actual.Should().Equals(expected);"); 20 | var newSource = GenerateCode.ObjectStatement("actual.Should().Be(expected);"); 21 | 22 | VerifyFix(oldSource, newSource); 23 | } 24 | 25 | [TestMethod] 26 | [Implemented] 27 | public void ShouldEquals_NestedInsideIfBlock_TestAnalyzer() 28 | => VerifyCSharpDiagnosticExpressionBody("if(true) { actual.Should().Equals(expected); }", 10, 24, DiagnosticMetadata.ShouldBe_ShouldEquals); 29 | 30 | [TestMethod] 31 | [Implemented] 32 | public void ShouldEquals_NestedInsideIfBlock_ShouldBe_ObjectType_TestCodeFix() 33 | { 34 | var oldSource = GenerateCode.ObjectStatement("if(true) { actual.Should().Equals(expected); }"); 35 | var newSource = GenerateCode.ObjectStatement("if(true) { actual.Should().Be(expected); }"); 36 | 37 | VerifyFix(oldSource, newSource); 38 | } 39 | 40 | [TestMethod] 41 | [Implemented] 42 | public void ShouldEquals_NestedInsideWhileBlock_TestAnalyzer() 43 | => VerifyCSharpDiagnosticExpressionBody("while(true) { actual.Should().Equals(expected); }", 10, 27, DiagnosticMetadata.ShouldBe_ShouldEquals); 44 | 45 | [TestMethod] 46 | [Implemented] 47 | public void ShouldEquals_NestedInsideWhileBlock_ShouldBe_ObjectType_TestCodeFix() 48 | { 49 | var oldSource = GenerateCode.ObjectStatement("while(true) { actual.Should().Equals(expected); }"); 50 | var newSource = GenerateCode.ObjectStatement("while(true) { actual.Should().Be(expected); }"); 51 | 52 | VerifyFix(oldSource, newSource); 53 | } 54 | 55 | [TestMethod] 56 | [Implemented] 57 | public void ShouldEquals_ActualIsMethodInvoaction_TestAnalyzer() 58 | => VerifyCSharpDiagnosticExpressionBody("object ResultSupplier() { return null; } \n" 59 | + "ResultSupplier().Should().Equals(expected);", 11, 0, DiagnosticMetadata.ShouldBe_ShouldEquals); 60 | 61 | [TestMethod] 62 | [Implemented] 63 | public void ShouldEquals_ActualIsMethodInvoaction_ShouldBe_ObjectType_TestCodeFix() 64 | { 65 | const string methodInvocation = "object ResultSupplier() { return null; } \n"; 66 | var oldSource = GenerateCode.ObjectStatement(methodInvocation + "ResultSupplier().Should().Equals(expected);"); 67 | var newSource = GenerateCode.ObjectStatement(methodInvocation + "ResultSupplier().Should().Be(expected);"); 68 | 69 | VerifyFix(oldSource, newSource); 70 | } 71 | 72 | [TestMethod] 73 | [Implemented] 74 | public void ShouldEquals_ShouldBe_NumberType_TestCodeFix() 75 | { 76 | var oldSource = GenerateCode.DoubleAssertion("actual.Should().Equals(expected);"); 77 | var newSource = GenerateCode.DoubleAssertion("actual.Should().Be(expected);"); 78 | 79 | VerifyFix(oldSource, newSource); 80 | } 81 | 82 | [TestMethod] 83 | [Implemented] 84 | public void ShouldEquals_ShouldBe_StringType_TestCodeFix() 85 | { 86 | var oldSource = GenerateCode.StringAssertion("actual.Should().Equals(expected);"); 87 | var newSource = GenerateCode.StringAssertion("actual.Should().Be(expected);"); 88 | 89 | VerifyFix(oldSource, newSource); 90 | } 91 | 92 | [TestMethod] 93 | [Implemented] 94 | public void ShouldEquals_ShouldEqual_EnumerableType_TestCodeFix() 95 | { 96 | var oldSource = GenerateCode.GenericIListCodeBlockAssertion("actual.Should().Equals(expected);"); 97 | var newSource = GenerateCode.GenericIListCodeBlockAssertion("actual.Should().Equal(expected);"); 98 | 99 | VerifyFix(oldSource, newSource); 100 | } 101 | 102 | private void VerifyCSharpDiagnosticExpressionBody(string sourceAssertion, DiagnosticMetadata metadata) => VerifyCSharpDiagnosticExpressionBody(sourceAssertion, 10, 13, metadata); 103 | private void VerifyCSharpDiagnosticExpressionBody(string sourceAssertion, int line, int column, DiagnosticMetadata metadata) 104 | { 105 | var source = GenerateCode.ObjectStatement(sourceAssertion); 106 | DiagnosticVerifier.VerifyCSharpDiagnosticUsingAllAnalyzers(source, new DiagnosticResult 107 | { 108 | Id = FluentAssertionsAnalyzer.DiagnosticId, 109 | Message = metadata.Message, 110 | VisitorName = metadata.Name, 111 | Locations = new DiagnosticResultLocation[] 112 | { 113 | new DiagnosticResultLocation("Test0.cs", line, column) 114 | }, 115 | Severity = DiagnosticSeverity.Info 116 | }); 117 | } 118 | 119 | private void VerifyFix(string oldSource, string newSource) 120 | => DiagnosticVerifier.VerifyFix(new CodeFixVerifierArguments() 121 | .WithSources(oldSource) 122 | .WithFixedSources(newSource) 123 | .WithDiagnosticAnalyzer() 124 | .WithCodeFixProvider() 125 | .WithPackageReferences(PackageReference.FluentAssertions_6_12_0) 126 | ); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/AnalyzerReleases.Shipped.md: -------------------------------------------------------------------------------- 1 | ; Shipped analyzer releases 2 | ; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md 3 | 4 | ## Release 0.28.0 5 | 6 | ### New Rules 7 | 8 | Rule ID | Category | Severity | Notes 9 | --------|----------|----------|------- 10 | FAA0001 | FluentAssertionTips | Info | Using FluentAssertions assertions better 11 | FAA0002 | FluentAssertionTips | Info | Migrate from xunit to FluentAssertions. 12 | FAA0003 | FluentAssertionTips | Info | Migration from MSTest to FluentAssertions. 13 | FAA0004 | FluentAssertionTips | Info | Migration from NUnit to FluentAssertions. 14 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/AnalyzerReleases.Unshipped.md: -------------------------------------------------------------------------------- 1 | ; Unshipped analyzer release 2 | ; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md 3 | 4 | ### New Rules 5 | 6 | Rule ID | Category | Severity | Notes 7 | --------|----------|----------|------- 8 | FluentAssertions0801 | FluentAssertionCodeSmell | Warning | AsyncVoidAnalyzer 9 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace FluentAssertions.Analyzers; 2 | 3 | public static class Constants 4 | { 5 | public static class DiagnosticProperties 6 | { 7 | public const string VisitorName = nameof(VisitorName); 8 | public const string HelpLink = nameof(HelpLink); 9 | public const string IdPrefix = "FluentAssertions"; 10 | } 11 | 12 | public static class Tips 13 | { 14 | public const string Category = "FluentAssertionTips"; 15 | } 16 | 17 | public static class CodeSmell 18 | { 19 | public const string Category = "FluentAssertionCodeSmell"; 20 | 21 | public const string AsyncVoid = $"{DiagnosticProperties.IdPrefix}0801"; 22 | public const string ShouldEquals = $"{DiagnosticProperties.IdPrefix}0802"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/FluentAssertions.Analyzers.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | preview 6 | FluentAssertions.Analyzers 7 | 8 | false 9 | true 10 | true 11 | true 12 | true 13 | 14 | 15 | 16 | FluentAssertions.Analyzers 17 | 0.17.3 18 | Meir Blachman 19 | Copyright Meir Blachman 2017-$([System.DateTime]::Now.ToString('yyyy')) 20 | 21 | Analyzers to help writing fluentassertions the right way. 22 | FluentAssertions Analyzers 23 | See https://github.com/fluentassertions/fluentassertions.analyzers/releases/ 24 | 25 | docs\README.md 26 | FluentAssertions.png 27 | MIT 28 | https://github.com/fluentassertions/fluentassertions.analyzers 29 | https://github.com/fluentassertions/fluentassertions.analyzers 30 | git 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/Tips/AsyncVoid.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CodeFixes; 3 | using Microsoft.CodeAnalysis.CSharp; 4 | using Microsoft.CodeAnalysis.CSharp.Syntax; 5 | using Microsoft.CodeAnalysis.Diagnostics; 6 | using System; 7 | using System.Collections.Immutable; 8 | using System.Composition; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | 12 | namespace FluentAssertions.Analyzers; 13 | 14 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 15 | public class AsyncVoidAnalyzer : DiagnosticAnalyzer 16 | { 17 | public const string DiagnosticId = Constants.CodeSmell.AsyncVoid; 18 | public const string Title = "Code Smell"; 19 | public const string Message = "The assertions might not be executed when assigning an async void lambda to a Action"; 20 | 21 | public static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, Message, Constants.CodeSmell.Category, DiagnosticSeverity.Warning, true); 22 | 23 | public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); 24 | 25 | public sealed override void Initialize(AnalysisContext context) 26 | { 27 | context.EnableConcurrentExecution(); 28 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); 29 | context.RegisterCodeBlockAction(AnalyzeCodeBlock); 30 | } 31 | 32 | private void AnalyzeCodeBlock(CodeBlockAnalysisContext context) 33 | { 34 | var method = context.CodeBlock as MethodDeclarationSyntax; 35 | if (method == null) return; 36 | 37 | if (method.Body != null) 38 | { 39 | foreach (var statement in method.Body.Statements.OfType()) 40 | { 41 | 42 | var diagnostic = AnalyzeStatement(context.SemanticModel, statement); 43 | if (diagnostic != null) 44 | { 45 | context.ReportDiagnostic(diagnostic); 46 | } 47 | } 48 | return; 49 | } 50 | } 51 | 52 | protected virtual Diagnostic AnalyzeStatement(SemanticModel semanticModel, LocalDeclarationStatementSyntax statement) 53 | { 54 | var symbolInfo = semanticModel.GetSymbolInfo(statement.Declaration.Type); 55 | if (symbolInfo.Symbol?.Name != nameof(Action)) return null; 56 | 57 | foreach (var variable in statement.Declaration.Variables) 58 | { 59 | if (variable.Initializer == null) continue; 60 | 61 | if (!(variable.Initializer.Value is ParenthesizedLambdaExpressionSyntax lambda)) continue; 62 | 63 | if (lambda.AsyncKeyword.IsKind(SyntaxKind.AsyncKeyword)) 64 | { 65 | return Diagnostic.Create(descriptor: Rule, location: statement.GetLocation()); 66 | } 67 | } 68 | 69 | return null; 70 | } 71 | } 72 | 73 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AsyncVoidCodeFix)), Shared] 74 | public class AsyncVoidCodeFix : CodeFixProvider 75 | { 76 | public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(AsyncVoidAnalyzer.DiagnosticId); 77 | public override Task RegisterCodeFixesAsync(CodeFixContext context) 78 | { 79 | return Task.CompletedTask; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/Tips/CodeFixProviderBase.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.CodeActions; 4 | using Microsoft.CodeAnalysis.CodeFixes; 5 | using Microsoft.CodeAnalysis.CSharp.Syntax; 6 | using Microsoft.CodeAnalysis.Operations; 7 | using CreateChangedDocument = System.Func>; 8 | 9 | namespace FluentAssertions.Analyzers; 10 | 11 | public abstract class CodeFixProviderBase : CodeFixProvider where TTestContext : class 12 | { 13 | public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; 14 | 15 | protected abstract string Title { get; } 16 | 17 | public override async Task RegisterCodeFixesAsync(CodeFixContext context) 18 | { 19 | var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken); 20 | var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken); 21 | 22 | var testContext = CreateTestContext(semanticModel); 23 | foreach (var diagnostic in context.Diagnostics) 24 | { 25 | var node = root.FindNode(diagnostic.Location.SourceSpan); 26 | if (node is not InvocationExpressionSyntax invocationExpression) 27 | { 28 | continue; 29 | } 30 | 31 | var operation = semanticModel.GetOperation(invocationExpression, context.CancellationToken); 32 | if (operation is not IInvocationOperation invocation) 33 | { 34 | continue; 35 | } 36 | 37 | var fix = TryComputeFix(invocation, context, testContext, diagnostic); 38 | if (fix is not null) 39 | { 40 | context.RegisterCodeFix(CodeAction.Create(Title, fix, equivalenceKey: Title), diagnostic); 41 | } 42 | } 43 | } 44 | 45 | protected abstract TTestContext CreateTestContext(SemanticModel semanticModel); 46 | 47 | protected abstract CreateChangedDocument TryComputeFix(IInvocationOperation invocation, CodeFixContext context, TTestContext t, Diagnostic diagnostic); 48 | } 49 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/Tips/DocumentEditorUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.CodeAnalysis; 6 | using Microsoft.CodeAnalysis.CodeFixes; 7 | using Microsoft.CodeAnalysis.CSharp.Syntax; 8 | using Microsoft.CodeAnalysis.Editing; 9 | using Microsoft.CodeAnalysis.Operations; 10 | using CreateChangedDocument = System.Func>; 11 | 12 | namespace FluentAssertions.Analyzers; 13 | 14 | public class DocumentEditorUtils 15 | { 16 | public static CreateChangedDocument RenameMethodToSubjectShouldAssertion(IInvocationOperation invocation, CodeFixContext context, string newName, int subjectIndex, int[] argumentsToRemove) 17 | { 18 | return ctx => RewriteExpressionCore(invocation, [ 19 | ..Array.ConvertAll(argumentsToRemove, arg => EditAction.RemoveInvocationArgument(arg)), 20 | EditAction.SubjectShouldAssertion(subjectIndex, newName) 21 | ], context, ctx); 22 | } 23 | 24 | public static CreateChangedDocument RenameGenericMethodToSubjectShouldGenericAssertion(IInvocationOperation invocation, CodeFixContext context, string newName, int subjectIndex, int[] argumentsToRemove) 25 | => RenameMethodToSubjectShouldGenericAssertion(invocation, invocation.TargetMethod.TypeArguments, context, newName, subjectIndex, argumentsToRemove); 26 | public static CreateChangedDocument RenameMethodToSubjectShouldGenericAssertion(IInvocationOperation invocation, ImmutableArray genericTypes, CodeFixContext context, string newName, int subjectIndex, int[] argumentsToRemove) 27 | { 28 | return ctx => RewriteExpressionCore(invocation, [ 29 | ..Array.ConvertAll(argumentsToRemove, arg => EditAction.RemoveInvocationArgument(arg)), 30 | EditAction.SubjectShouldGenericAssertion(subjectIndex, newName, genericTypes) 31 | ], context, ctx); 32 | } 33 | 34 | public static CreateChangedDocument RenameMethodToSubjectShouldAssertionWithOptionsLambda(IInvocationOperation invocation, CodeFixContext context, string newName, int subjectIndex, int optionsIndex) 35 | { 36 | return ctx => RewriteExpressionCore(invocation, [ 37 | EditAction.SubjectShouldAssertion(subjectIndex, newName), 38 | EditAction.CreateEquivalencyAssertionOptionsLambda(optionsIndex) 39 | ], context, ctx); 40 | } 41 | 42 | public static CreateChangedDocument RewriteExpression(IInvocationOperation invocation, Action[] actions, CodeFixContext context) 43 | => ctx => RewriteExpressionCore(invocation, actions, context, ctx); 44 | 45 | private static async Task RewriteExpressionCore(IInvocationOperation invocation, Action[] actions, CodeFixContext context, CancellationToken cancellationToken) 46 | { 47 | var invocationExpression = (InvocationExpressionSyntax)invocation.Syntax; 48 | 49 | var editor = await DocumentEditor.CreateAsync(context.Document, cancellationToken); 50 | var editActionContext = new EditActionContext(editor, invocationExpression); 51 | 52 | foreach (var action in actions) 53 | { 54 | action(editActionContext); 55 | } 56 | 57 | return editor.GetChangedDocument(); 58 | } 59 | } 60 | 61 | public class EditActionContext(DocumentEditor editor, InvocationExpressionSyntax invocationExpression) { 62 | public DocumentEditor Editor { get; } = editor; 63 | public InvocationExpressionSyntax InvocationExpression { get; } = invocationExpression; 64 | 65 | public InvocationExpressionSyntax FluentAssertion { get; set; } 66 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/Tips/Editing/CreateEquivalencyAssertionOptionsLambda.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.CSharp.Syntax; 2 | using Microsoft.CodeAnalysis.Editing; 3 | 4 | namespace FluentAssertions.Analyzers; 5 | 6 | public class CreateEquivalencyAssertionOptionsLambdaAction(int argumentIndex) : IEditAction 7 | { 8 | public void Apply(DocumentEditor editor, InvocationExpressionSyntax invocationExpression) 9 | { 10 | const string lambdaParameter = "options"; 11 | const string equivalencyAssertionOptionsMethod = "Using"; 12 | 13 | var generator = editor.Generator; 14 | var optionsParameter = invocationExpression.ArgumentList.Arguments[argumentIndex]; 15 | 16 | var equivalencyAssertionLambda = generator.ValueReturningLambdaExpression(lambdaParameter, generator.InvocationExpression(generator.MemberAccessExpression(generator.IdentifierName(lambdaParameter), equivalencyAssertionOptionsMethod), optionsParameter)); 17 | editor.ReplaceNode(optionsParameter.Expression, equivalencyAssertionLambda); 18 | } 19 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/Tips/Editing/EditAction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp.Syntax; 5 | using Microsoft.CodeAnalysis.Editing; 6 | 7 | namespace FluentAssertions.Analyzers; 8 | 9 | public static class EditAction 10 | { 11 | public static Action RemoveNode(SyntaxNode node) 12 | => context => context.Editor.RemoveNode(node); 13 | 14 | public static Action RemoveInvocationArgument(int argumentIndex) 15 | => context => context.Editor.RemoveNode(context.InvocationExpression.ArgumentList.Arguments[argumentIndex]); 16 | 17 | public static Action SubjectShouldAssertion(int argumentIndex, string assertion) 18 | => context => new SubjectShouldAssertionAction(argumentIndex, assertion).Apply(context); 19 | 20 | public static Action ReplaceAssertionArgument(int index, Func expressionFactory) 21 | => context => 22 | { 23 | var argument = context.InvocationExpression.ArgumentList.Arguments[index]; 24 | var newArgumentExpression = (ExpressionSyntax)expressionFactory(context.Editor.Generator); 25 | context.Editor.ReplaceNode(argument.Expression, newArgumentExpression); 26 | }; 27 | 28 | public static Action SubjectShouldGenericAssertion(int argumentIndex, string assertion, ImmutableArray genericTypes) 29 | => context => new SubjectShouldGenericAssertionAction(argumentIndex, assertion, genericTypes).Apply(context); 30 | 31 | public static Action CreateEquivalencyAssertionOptionsLambda(int optionsIndex) 32 | => context => new CreateEquivalencyAssertionOptionsLambdaAction(optionsIndex).Apply(context.Editor, context.InvocationExpression); 33 | 34 | public static Action AddArgumentToAssertionArguments(int index, Func expressionFactory) 35 | => context => 36 | { 37 | var argument = (ArgumentSyntax)context.Editor.Generator.Argument(expressionFactory(context.Editor.Generator)); 38 | var arguments = context.FluentAssertion.ArgumentList.Arguments.Insert(index, argument); 39 | context.Editor.ReplaceNode(context.InvocationExpression.ArgumentList, context.InvocationExpression.ArgumentList.WithArguments(arguments)); 40 | }; 41 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/Tips/Editing/IEditAction.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.CSharp.Syntax; 2 | using Microsoft.CodeAnalysis.Editing; 3 | 4 | namespace FluentAssertions.Analyzers; 5 | 6 | public interface IEditAction 7 | { 8 | void Apply(DocumentEditor editor, InvocationExpressionSyntax invocationExpression); 9 | } 10 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/Tips/Editing/SkipInvocationNodeAction.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.CSharp.Syntax; 2 | using Microsoft.CodeAnalysis.Editing; 3 | 4 | namespace FluentAssertions.Analyzers; 5 | 6 | public class SkipInvocationNodeAction(InvocationExpressionSyntax skipInvocation) : IEditAction 7 | { 8 | public void Apply(DocumentEditor editor, InvocationExpressionSyntax invocationExpression) 9 | { 10 | var methodMemberAccess = (MemberAccessExpressionSyntax)skipInvocation.Expression; 11 | editor.ReplaceNode(skipInvocation, methodMemberAccess.Expression); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/Tips/Editing/SkipMemberAccessNodeAction.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.CSharp.Syntax; 2 | using Microsoft.CodeAnalysis.Editing; 3 | 4 | namespace FluentAssertions.Analyzers; 5 | 6 | public class SkipMemberAccessNodeAction(MemberAccessExpressionSyntax skipMemberAccess) : IEditAction 7 | { 8 | public void Apply(DocumentEditor editor, InvocationExpressionSyntax invocationExpression) 9 | { 10 | editor.ReplaceNode(skipMemberAccess, skipMemberAccess.Expression); 11 | } 12 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/Tips/Editing/SubjectShouldAssertionAction.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | using Microsoft.CodeAnalysis.Editing; 5 | 6 | namespace FluentAssertions.Analyzers; 7 | 8 | public class SubjectShouldAssertionAction 9 | { 10 | private readonly int _argumentIndex; 11 | protected readonly string _assertion; 12 | 13 | public SubjectShouldAssertionAction(int argumentIndex, string assertion) 14 | { 15 | _argumentIndex = argumentIndex; 16 | _assertion = assertion; 17 | } 18 | 19 | public void Apply(EditActionContext context) 20 | { 21 | var generator = context.Editor.Generator; 22 | var arguments = context.InvocationExpression.ArgumentList.Arguments; 23 | 24 | var subject = arguments[_argumentIndex]; 25 | var should = generator.InvocationExpression(generator.MemberAccessExpression(subject.Expression, "Should")); 26 | context.Editor.RemoveNode(subject); 27 | 28 | var memberAccess = (MemberAccessExpressionSyntax) generator.MemberAccessExpression(should, GenerateAssertion(generator)).WithTriviaFrom(context.InvocationExpression.Expression); 29 | 30 | context.Editor.ReplaceNode(context.InvocationExpression.Expression, memberAccess); 31 | context.FluentAssertion = context.InvocationExpression 32 | .WithExpression(memberAccess) 33 | .WithArgumentList(SyntaxFactory.ArgumentList(arguments.RemoveAt(_argumentIndex))); 34 | } 35 | 36 | protected virtual SyntaxNode GenerateAssertion(SyntaxGenerator generator) => generator.IdentifierName(_assertion); 37 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/Tips/Editing/SubjectShouldGenericAssertionAction.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.Editing; 4 | 5 | namespace FluentAssertions.Analyzers; 6 | 7 | public class SubjectShouldGenericAssertionAction : SubjectShouldAssertionAction 8 | { 9 | private readonly ImmutableArray _types; 10 | 11 | public SubjectShouldGenericAssertionAction(int argumentIndex, string assertion, ImmutableArray types) : base(argumentIndex, assertion) 12 | { 13 | _types = types; 14 | } 15 | 16 | protected override SyntaxNode GenerateAssertion(SyntaxGenerator generator) => generator.GenericName(_assertion, _types); 17 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/Tips/FluentAssertionsAnalyzer.Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using Microsoft.CodeAnalysis; 8 | using Microsoft.CodeAnalysis.Editing; 9 | using Microsoft.CodeAnalysis.Operations; 10 | 11 | namespace FluentAssertions.Analyzers; 12 | 13 | public partial class FluentAssertionsAnalyzer 14 | { 15 | private static bool IsEnumerableMethodWithoutArguments(IInvocationOperation invocation, FluentAssertionsMetadata metadata) 16 | => invocation.IsContainedInType(metadata.Enumerable) && invocation.Arguments.Length == 1; 17 | 18 | private static bool IsEnumerableMethodWithPredicate(IInvocationOperation invocation, FluentAssertionsMetadata metadata) 19 | => invocation.IsContainedInType(metadata.Enumerable) && invocation.Arguments.Length == 2 && invocation.Arguments[1].IsLambda(); // invocation.Arguments[0] is `this` argument 20 | 21 | private static bool TryGetExceptionPropertyAssertion(IInvocationOperation assertion, out string fluentAssertionProperty, out string exceptionProperty, out IInvocationOperation nextAssertion) 22 | { 23 | if (assertion.Parent is IPropertyReferenceOperation chainProperty 24 | && chainProperty.Parent is IPropertyReferenceOperation exception 25 | && exception.Parent.UnwrapParentConversion() is IArgumentOperation argument 26 | && argument.Parent is IInvocationOperation { TargetMethod.Name: "Should" } should) 27 | { 28 | nextAssertion = should.Parent as IInvocationOperation; 29 | fluentAssertionProperty = chainProperty.Property.Name; 30 | exceptionProperty = exception.Property.Name; 31 | return nextAssertion is not null; 32 | } 33 | 34 | nextAssertion = default; 35 | fluentAssertionProperty = default; 36 | exceptionProperty = default; 37 | return false; 38 | } 39 | 40 | private static bool HasConditionalAccessAncestor(IInvocationOperation invocation) 41 | { 42 | var current = invocation.Parent; 43 | while (current is not null) 44 | { 45 | if (current.Kind is OperationKind.ConditionalAccess) 46 | { 47 | return true; 48 | } 49 | 50 | current = current.Parent; 51 | } 52 | 53 | return false; 54 | } 55 | 56 | private class FluentAssertionsMetadata 57 | { 58 | public FluentAssertionsMetadata(Compilation compilation) 59 | { 60 | AssertionExtensions = compilation.GetTypeByMetadataName("FluentAssertions.AssertionExtensions"); 61 | ReferenceTypeAssertionsOfT2 = compilation.GetTypeByMetadataName("FluentAssertions.Primitives.ReferenceTypeAssertions`2"); 62 | ObjectAssertionsOfT2 = compilation.GetTypeByMetadataName("FluentAssertions.Primitives.ObjectAssertions`2"); 63 | NumericAssertionsOfT2 = compilation.GetTypeByMetadataName("FluentAssertions.Numeric.NumericAssertions`2"); 64 | BooleanAssertionsOfT1 = compilation.GetTypeByMetadataName("FluentAssertions.Primitives.BooleanAssertions`1"); 65 | GenericCollectionAssertionsOfT3 = compilation.GetTypeByMetadataName("FluentAssertions.Collections.GenericCollectionAssertions`3"); 66 | GenericDictionaryAssertionsOfT4 = compilation.GetTypeByMetadataName("FluentAssertions.Collections.GenericDictionaryAssertions`4"); 67 | StringAssertionsOfT1 = compilation.GetTypeByMetadataName("FluentAssertions.Primitives.StringAssertions`1"); 68 | ExceptionAssertionsOfT1 = compilation.GetTypeByMetadataName("FluentAssertions.Specialized.ExceptionAssertions`1"); 69 | DelegateAssertionsOfT2 = compilation.GetTypeByMetadataName("FluentAssertions.Specialized.DelegateAssertions`2"); 70 | IDictionaryOfT2 = compilation.GetTypeByMetadataName(typeof(IDictionary<,>).FullName); 71 | DictionaryOfT2 = compilation.GetTypeByMetadataName(typeof(Dictionary<,>).FullName); 72 | IReadonlyDictionaryOfT2 = compilation.GetTypeByMetadataName(typeof(IReadOnlyDictionary<,>).FullName); 73 | IListOfT = compilation.GetTypeByMetadataName(typeof(IList<>).FullName); 74 | IReadonlyListOfT = compilation.GetTypeByMetadataName(typeof(IReadOnlyList<>).FullName); 75 | ICollectionOfT = compilation.GetTypeByMetadataName(typeof(ICollection<>).FullName); 76 | IReadonlyCollectionOfT = compilation.GetTypeByMetadataName(typeof(IReadOnlyCollection<>).FullName); 77 | Enumerable = compilation.GetTypeByMetadataName(typeof(Enumerable).FullName); 78 | IEnumerable = compilation.GetTypeByMetadataName(typeof(IEnumerable).FullName); 79 | Math = compilation.GetTypeByMetadataName(typeof(Math).FullName); 80 | TaskCompletionSourceOfT1 = compilation.GetTypeByMetadataName(typeof(TaskCompletionSource<>).FullName); 81 | Stream = compilation.GetTypeByMetadataName(typeof(Stream).FullName); 82 | } 83 | public INamedTypeSymbol AssertionExtensions { get; } 84 | public INamedTypeSymbol ReferenceTypeAssertionsOfT2 { get; } 85 | public INamedTypeSymbol ObjectAssertionsOfT2 { get; } 86 | public INamedTypeSymbol GenericCollectionAssertionsOfT3 { get; } 87 | public INamedTypeSymbol GenericDictionaryAssertionsOfT4 { get; } 88 | public INamedTypeSymbol StringAssertionsOfT1 { get; } 89 | public INamedTypeSymbol ExceptionAssertionsOfT1 { get; } 90 | public INamedTypeSymbol DelegateAssertionsOfT2 { get; } 91 | public INamedTypeSymbol IDictionaryOfT2 { get; } 92 | public INamedTypeSymbol DictionaryOfT2 { get; } 93 | public INamedTypeSymbol IReadonlyDictionaryOfT2 { get; } 94 | public INamedTypeSymbol IListOfT { get; } 95 | public INamedTypeSymbol IReadonlyListOfT { get; } 96 | public INamedTypeSymbol ICollectionOfT { get; } 97 | public INamedTypeSymbol IReadonlyCollectionOfT { get; } 98 | public INamedTypeSymbol BooleanAssertionsOfT1 { get; } 99 | public INamedTypeSymbol NumericAssertionsOfT2 { get; } 100 | public INamedTypeSymbol Enumerable { get; } 101 | public INamedTypeSymbol IEnumerable { get; } 102 | public INamedTypeSymbol Math { get; } 103 | public INamedTypeSymbol TaskCompletionSourceOfT1 { get; } 104 | public INamedTypeSymbol Stream { get; } 105 | } 106 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/Tips/FluentAssertionsCodeFixProvider.EditorUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.CodeAnalysis.CodeFixes; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | using Microsoft.CodeAnalysis.Editing; 5 | using Microsoft.CodeAnalysis.Operations; 6 | using CreateChangedDocument = System.Func>; 7 | 8 | namespace FluentAssertions.Analyzers; 9 | 10 | public sealed partial class FluentAssertionsCodeFixProvider 11 | { 12 | // .().Should().() 13 | private static CreateChangedDocument RewriteFluentAssertion(IInvocationOperation assertion, CodeFixContext context, Action[] actions) 14 | { 15 | var assertionExpression = (InvocationExpressionSyntax)assertion.Syntax; 16 | 17 | assertion.TryGetFirstDescendent(out var should); 18 | var subject = should?.Arguments[0].Value.UnwrapConversion(); 19 | 20 | IInvocationOperation invocationBeforeShould = default; 21 | should?.TryGetFirstDescendent(out invocationBeforeShould); 22 | 23 | var actionContext = new FluentAssertionEditActionContext(assertion, assertionExpression, should, subject, invocationBeforeShould); 24 | 25 | return async ctx => 26 | { 27 | var editor = await DocumentEditor.CreateAsync(context.Document, ctx); 28 | foreach (var action in actions) 29 | { 30 | action(editor, actionContext); 31 | } 32 | 33 | return editor.GetChangedDocument(); 34 | }; 35 | } 36 | 37 | // .Should().(argumentsA)..() 38 | private static CreateChangedDocument RewriteFluentChainedAssertion(IInvocationOperation assertionB, CodeFixContext context, Action[] actions) 39 | { 40 | var assertionExpressionB = (InvocationExpressionSyntax)assertionB.Syntax; 41 | 42 | assertionB.TryGetFirstDescendent(out var andOrWhich); 43 | 44 | andOrWhich.TryGetFirstDescendent(out var assertionA); 45 | 46 | var assertionExpressionA = (InvocationExpressionSyntax)assertionA.Syntax; 47 | 48 | assertionA.TryGetFirstDescendent(out var should); 49 | 50 | var subject = should?.Arguments[0].Value; 51 | 52 | var actionContext = new FluentChainedAssertionEditActionContext(assertionA, assertionExpressionA, andOrWhich, assertionB, assertionExpressionB, should, subject); 53 | 54 | return async ctx => 55 | { 56 | var editor = await DocumentEditor.CreateAsync(context.Document, ctx); 57 | foreach (var action in actions) 58 | { 59 | action(editor, actionContext); 60 | } 61 | 62 | return editor.GetChangedDocument(); 63 | }; 64 | } 65 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/Tips/FluentAssertionsCodeFixProvider.Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Microsoft.CodeAnalysis.CodeFixes; 4 | using Microsoft.CodeAnalysis.CSharp.Syntax; 5 | using Microsoft.CodeAnalysis.Operations; 6 | using CreateChangedDocument = System.Func>; 7 | using SF = Microsoft.CodeAnalysis.CSharp.SyntaxFactory; 8 | 9 | namespace FluentAssertions.Analyzers; 10 | 11 | public sealed partial class FluentAssertionsCodeFixProvider 12 | { 13 | // oldAssertion: .Should().Throw()..InnerException.Should().(); 14 | // newAssertion: .Should().Throw().WithInnerExceptionExactly(); 15 | private CreateChangedDocument ReplaceShouldThrowWithInnerException(IInvocationOperation assertion, CodeFixContext context, string newName) 16 | { 17 | return RewriteFluentAssertion(assertion, context, [ 18 | (editor, context) => 19 | { 20 | var generator = editor.Generator; 21 | 22 | var firstAssertion = context.InvocationBeforeShould; 23 | var andOrWhich = firstAssertion.GetFirstAncestor(); 24 | var andOrWhichExpression = (MemberAccessExpressionSyntax)andOrWhich.Syntax; 25 | 26 | var secondAssertionMemberAccess = (MemberAccessExpressionSyntax)context.AssertionExpression.Expression; 27 | 28 | var newAssertion = generator.InvocationExpression(andOrWhichExpression.WithName(secondAssertionMemberAccess.Name.WithIdentifier(SF.Identifier(newName))), context.AssertionExpression.ArgumentList.Arguments); 29 | 30 | editor.ReplaceNode(context.AssertionExpression, newAssertion); 31 | } 32 | ]); 33 | } 34 | 35 | // oldAssertion: .Should().Throw()..Message.Should().([arg1, arg2, arg3...]) 36 | // newAssertion: .Should().Throw().WithMessage([newArg1, arg2, arg3...]) 37 | private CreateChangedDocument ReplaceShouldThrowWithMessage(IInvocationOperation assertion, CodeFixContext context, string prefix = "", string postfix = "") 38 | { 39 | return RewriteFluentAssertion(assertion, context, [ 40 | (editor, context) => 41 | { 42 | var generator = editor.Generator; 43 | 44 | var firstAssertion = context.InvocationBeforeShould; 45 | var andOrWhich = firstAssertion.GetFirstAncestor(); 46 | var andOrWhichExpression = (MemberAccessExpressionSyntax)andOrWhich.Syntax; 47 | 48 | var newArgument = context.AssertionExpression.ArgumentList.Arguments[0].Expression switch 49 | { 50 | IdentifierNameSyntax identifier => (prefix is "" && postfix is "") ? identifier : SF.ParseExpression($"$\"{prefix}{{{identifier.Identifier.Text}}}{postfix}\""), 51 | LiteralExpressionSyntax literal => generator.LiteralExpression(prefix + literal.Token.ValueText + postfix), 52 | _ => throw new NotSupportedException() 53 | }; 54 | 55 | var newAssertion = generator.InvocationExpression(andOrWhichExpression.WithName((SimpleNameSyntax)generator.IdentifierName("WithMessage")), [ 56 | generator.Argument(newArgument), 57 | ..context.AssertionExpression.ArgumentList.Arguments.Skip(1) 58 | ]); 59 | 60 | editor.ReplaceNode(context.AssertionExpression, newAssertion); 61 | } 62 | ]); 63 | } 64 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/Tips/FluentAssertionsEditAction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp; 5 | using Microsoft.CodeAnalysis.CSharp.Syntax; 6 | using Microsoft.CodeAnalysis.Editing; 7 | using Microsoft.CodeAnalysis.Operations; 8 | 9 | namespace FluentAssertions.Analyzers; 10 | 11 | 12 | public record struct FluentAssertionEditActionContext( 13 | IInvocationOperation Assertion, 14 | InvocationExpressionSyntax AssertionExpression, 15 | IInvocationOperation Should, 16 | IOperation Subject, 17 | IInvocationOperation InvocationBeforeShould 18 | ); 19 | 20 | public static class FluentAssertionsEditAction 21 | { 22 | public static Action RenameAssertion(string newName) 23 | { 24 | return (DocumentEditor editor, FluentAssertionEditActionContext context) => 25 | { 26 | var newNameNode = (IdentifierNameSyntax)editor.Generator.IdentifierName(newName); 27 | var memberAccess = (MemberAccessExpressionSyntax)context.AssertionExpression.Expression; 28 | editor.ReplaceNode(memberAccess.Name, newNameNode); 29 | }; 30 | } 31 | 32 | public static Action SkipInvocationBeforeShould() 33 | { 34 | return (DocumentEditor editor, FluentAssertionEditActionContext context) => 35 | { 36 | var invocationExpressionBeforeShould = (InvocationExpressionSyntax)context.InvocationBeforeShould.Syntax; 37 | var methodMemberAccess = (MemberAccessExpressionSyntax)invocationExpressionBeforeShould.Expression; 38 | 39 | editor.ReplaceNode(invocationExpressionBeforeShould, methodMemberAccess.Expression); 40 | }; 41 | } 42 | 43 | public static Action SkipExpressionBeforeShould() 44 | { 45 | return (DocumentEditor editor, FluentAssertionEditActionContext context) => 46 | { 47 | IEditAction skipExpressionNodeAction = context.Subject switch 48 | { 49 | IInvocationOperation invocationBeforeShould => new SkipInvocationNodeAction((InvocationExpressionSyntax)invocationBeforeShould.Syntax), 50 | IPropertyReferenceOperation propertyReferenceBeforeShould => new SkipMemberAccessNodeAction((MemberAccessExpressionSyntax)propertyReferenceBeforeShould.Syntax), 51 | _ => throw new NotSupportedException("[SkipExpressionBeforeShouldEditAction] Invalid expression before should invocation") 52 | }; 53 | 54 | skipExpressionNodeAction.Apply(editor, context.AssertionExpression); 55 | }; 56 | } 57 | 58 | public static Action RemoveAssertionArgument(int index) 59 | { 60 | return (DocumentEditor editor, FluentAssertionEditActionContext context) => 61 | { 62 | editor.RemoveNode(context.AssertionExpression.ArgumentList.Arguments[index]); 63 | }; 64 | } 65 | 66 | public static Action PrependArgumentsFromInvocationBeforeShouldToAssertion(int skipAssertionArguments = 0) 67 | { 68 | return (DocumentEditor editor, FluentAssertionEditActionContext context) => 69 | { 70 | var invocationExpressionBeforeShould = (InvocationExpressionSyntax)context.InvocationBeforeShould.Syntax; 71 | var argumentList = invocationExpressionBeforeShould.ArgumentList; 72 | 73 | var combinedArguments = SyntaxFactory.ArgumentList(argumentList.Arguments.AddRange(context.AssertionExpression.ArgumentList.Arguments.Skip(skipAssertionArguments))); 74 | editor.ReplaceNode(context.AssertionExpression.ArgumentList, combinedArguments); 75 | }; 76 | } 77 | public static Action RemoveInvocationOnAssertionArgument(int assertionArgumentIndex, int invocationArgumentIndex) 78 | { 79 | return (DocumentEditor editor, FluentAssertionEditActionContext context) => 80 | { 81 | var invocationArgument = (IInvocationOperation)context.Assertion.Arguments[assertionArgumentIndex].Value; 82 | var expected = invocationArgument.Arguments[invocationArgumentIndex].Value.UnwrapConversion(); 83 | 84 | editor.ReplaceNode(invocationArgument.Syntax, expected.Syntax); 85 | }; 86 | } 87 | 88 | public static Action UnwrapInvocationOnSubject(int argumentIndex) 89 | { 90 | return (DocumentEditor editor, FluentAssertionEditActionContext context) => 91 | { 92 | var subjectReference = ((IInvocationOperation)context.Subject).Arguments[argumentIndex].Value; 93 | 94 | editor.ReplaceNode(context.Subject.Syntax, subjectReference.Syntax.WithTriviaFrom(context.Subject.Syntax)); 95 | }; 96 | } 97 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/Tips/FluentChainedAssertionEditAction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.CSharp; 4 | using Microsoft.CodeAnalysis.CSharp.Syntax; 5 | using Microsoft.CodeAnalysis.Editing; 6 | using Microsoft.CodeAnalysis.Operations; 7 | 8 | namespace FluentAssertions.Analyzers; 9 | 10 | public record struct FluentChainedAssertionEditActionContext( 11 | IInvocationOperation AssertionA, 12 | InvocationExpressionSyntax AssertionAExpression, 13 | IPropertyReferenceOperation AndOrWhich, 14 | IInvocationOperation AssertionB, 15 | InvocationExpressionSyntax AssertionBExpression, 16 | IInvocationOperation Should, 17 | IOperation Subject 18 | ); 19 | 20 | public static class FluentChainedAssertionEditAction 21 | { 22 | public static Action CombineAssertionsWithNameAndArguments(string newName, CombineAssertionArgumentsStrategy strategy) 23 | { 24 | return CombineAssertionsWithName(newName, (editor, context) => 25 | { 26 | var arguments = strategy switch 27 | { 28 | CombineAssertionArgumentsStrategy.FirstAssertionFirst => context.AssertionAExpression.ArgumentList.Arguments.AddRange(context.AssertionBExpression.ArgumentList.Arguments), 29 | CombineAssertionArgumentsStrategy.InsertFirstAssertionIntoIndex1OfSecondAssertion => context.AssertionBExpression.ArgumentList.Arguments.InsertRange(1, context.AssertionAExpression.ArgumentList.Arguments), 30 | CombineAssertionArgumentsStrategy.InsertSecondAssertionIntoIndex1OfFirstAssertion => context.AssertionAExpression.ArgumentList.Arguments.InsertRange(1, context.AssertionBExpression.ArgumentList.Arguments), 31 | _ => throw new NotImplementedException(), 32 | }; 33 | return SyntaxFactory.ArgumentList(arguments); 34 | }); 35 | } 36 | 37 | public static Action CombineAssertionsWithName(string newName, Func argumentsGenerator) 38 | { 39 | return (DocumentEditor editor, FluentChainedAssertionEditActionContext context) => 40 | { 41 | var newNameNode = (IdentifierNameSyntax)editor.Generator.IdentifierName(newName); 42 | 43 | var assertionMemberAccess = (MemberAccessExpressionSyntax)context.AssertionAExpression.Expression; 44 | 45 | var allArguments = argumentsGenerator(editor, context); 46 | var newAssertion = context.AssertionAExpression 47 | .WithExpression(assertionMemberAccess.WithName(newNameNode)) 48 | .WithArgumentList(allArguments); 49 | 50 | editor.ReplaceNode(context.AssertionBExpression, newAssertion); 51 | }; 52 | } 53 | } 54 | 55 | public enum CombineAssertionArgumentsStrategy 56 | { 57 | FirstAssertionFirst, 58 | InsertFirstAssertionIntoIndex1OfSecondAssertion, 59 | InsertSecondAssertionIntoIndex1OfFirstAssertion, 60 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/Tips/TestingFrameworkCodeFixProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using FluentAssertions.Analyzers.Utilities; 6 | using Microsoft.CodeAnalysis; 7 | using Microsoft.CodeAnalysis.CodeFixes; 8 | using Microsoft.CodeAnalysis.CSharp; 9 | using Microsoft.CodeAnalysis.CSharp.Syntax; 10 | using Microsoft.CodeAnalysis.Formatting; 11 | using Microsoft.CodeAnalysis.Operations; 12 | using Microsoft.CodeAnalysis.Simplification; 13 | 14 | namespace FluentAssertions.Analyzers; 15 | 16 | public abstract class TestingFrameworkCodeFixProvider : CodeFixProviderBase where TTestContext : TestingFrameworkCodeFixProvider.TestingFrameworkCodeFixContext 17 | { 18 | protected override string Title => "Replace with FluentAssertions"; 19 | 20 | protected override Func> TryComputeFix(IInvocationOperation invocation, CodeFixContext context, TTestContext t, Diagnostic diagnostic) 21 | { 22 | var fix = TryComputeFixCore(invocation, context, t, diagnostic); 23 | if (fix is null) 24 | { 25 | return null; 26 | } 27 | 28 | return async ctx => 29 | { 30 | const string fluentAssertionNamespace = "FluentAssertions"; 31 | var document = await fix(ctx); 32 | 33 | var model = await document.GetSemanticModelAsync(); 34 | var scopes = model.GetImportScopes(diagnostic.Location.SourceSpan.Start); 35 | 36 | var hasFluentAssertionImport = scopes.Any(scope => scope.Imports.Any(import => import.NamespaceOrType.ToString().Equals(fluentAssertionNamespace))); 37 | if (hasFluentAssertionImport) 38 | { 39 | return document; 40 | } 41 | 42 | var root = (CompilationUnitSyntax)await document.GetSyntaxRootAsync(); 43 | root = root.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(fluentAssertionNamespace)).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation)); 44 | 45 | document = document.WithSyntaxRoot(root); 46 | document = await Formatter.OrganizeImportsAsync(document); 47 | 48 | return document; 49 | }; 50 | } 51 | 52 | protected abstract Func> TryComputeFixCore(IInvocationOperation invocation, CodeFixContext context, TTestContext t, Diagnostic diagnostic); 53 | 54 | protected static bool ArgumentsAreTypeOf(IInvocationOperation invocation, params ITypeSymbol[] types) => ArgumentsAreTypeOf(invocation, 0, types); 55 | protected static bool ArgumentsAreTypeOf(IInvocationOperation invocation, int startFromIndex, params ITypeSymbol[] types) 56 | { 57 | if (invocation.TargetMethod.Parameters.Length != types.Length + startFromIndex) 58 | { 59 | return false; 60 | } 61 | 62 | for (int i = startFromIndex; i < types.Length; i++) 63 | { 64 | if (!invocation.TargetMethod.Parameters[i].Type.EqualsSymbol(types[i])) 65 | { 66 | return false; 67 | } 68 | } 69 | 70 | return true; 71 | } 72 | 73 | protected static bool ArgumentsAreGenericTypeOf(IInvocationOperation invocation, params ITypeSymbol[] types) 74 | { 75 | const int generics = 1; 76 | if (invocation.TargetMethod.Parameters.Length != types.Length) 77 | { 78 | return false; 79 | } 80 | 81 | if (invocation.TargetMethod.TypeArguments.Length != generics) 82 | { 83 | return false; 84 | } 85 | 86 | var genericType = invocation.TargetMethod.TypeArguments[0]; 87 | 88 | for (int i = 0; i < types.Length; i++) 89 | { 90 | if (invocation.TargetMethod.Parameters[i].Type is not INamedTypeSymbol parameterType) 91 | { 92 | return false; 93 | } 94 | 95 | if (parameterType.TypeArguments.IsEmpty && parameterType.EqualsSymbol(genericType)) 96 | { 97 | continue; 98 | } 99 | 100 | if (parameterType.TypeArguments.Length != generics 101 | || !(parameterType.TypeArguments[0].EqualsSymbol(genericType) && parameterType.OriginalDefinition.EqualsSymbol(types[i]))) 102 | { 103 | return false; 104 | } 105 | } 106 | 107 | return true; 108 | } 109 | 110 | protected static bool ArgumentsCount(IInvocationOperation invocation, int arguments) 111 | { 112 | return invocation.TargetMethod.Parameters.Length == arguments; 113 | } 114 | 115 | } 116 | 117 | public abstract class TestingFrameworkCodeFixProvider : TestingFrameworkCodeFixProvider 118 | { 119 | protected override TestingFrameworkCodeFixContext CreateTestContext(SemanticModel semanticModel) => new(semanticModel.Compilation); 120 | 121 | 122 | public class TestingFrameworkCodeFixContext(Compilation compilation) 123 | { 124 | public INamedTypeSymbol Object { get; } = compilation.ObjectType; 125 | public INamedTypeSymbol String { get; } = compilation.GetTypeByMetadataName("System.String"); 126 | public INamedTypeSymbol Int32 { get; } = compilation.GetTypeByMetadataName("System.Int32"); 127 | public INamedTypeSymbol UInt32 { get; } = compilation.GetTypeByMetadataName("System.UInt32"); 128 | public INamedTypeSymbol Long { get; } = compilation.GetTypeByMetadataName("System.Int64"); 129 | public INamedTypeSymbol ULong { get; } = compilation.GetTypeByMetadataName("System.UInt64"); 130 | public INamedTypeSymbol Float { get; } = compilation.GetTypeByMetadataName("System.Single"); 131 | public INamedTypeSymbol Double { get; } = compilation.GetTypeByMetadataName("System.Double"); 132 | public INamedTypeSymbol Decimal { get; } = compilation.GetTypeByMetadataName("System.Decimal"); 133 | public INamedTypeSymbol Boolean { get; } = compilation.GetTypeByMetadataName("System.Boolean"); 134 | public INamedTypeSymbol Action { get; } = compilation.GetTypeByMetadataName("System.Action"); 135 | public INamedTypeSymbol Type { get; } = compilation.GetTypeByMetadataName("System.Type"); 136 | public INamedTypeSymbol DateTime { get; } = compilation.GetTypeByMetadataName("System.DateTime"); 137 | public INamedTypeSymbol TimeSpan { get; } = compilation.GetTypeByMetadataName("System.TimeSpan"); 138 | public INamedTypeSymbol FuncOfObject { get; } = compilation.GetTypeByMetadataName("System.Func`1").Construct(compilation.ObjectType); 139 | public INamedTypeSymbol FuncOfTask { get; } = compilation.GetTypeByMetadataName("System.Func`1").Construct(compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")); 140 | public IArrayTypeSymbol ObjectArray { get; } = compilation.CreateArrayTypeSymbol(compilation.ObjectType); 141 | public INamedTypeSymbol CultureInfo { get; } = compilation.GetTypeByMetadataName("System.Globalization.CultureInfo"); 142 | public INamedTypeSymbol StringComparison { get; } = compilation.GetTypeByMetadataName("System.StringComparison"); 143 | public INamedTypeSymbol Regex { get; } = compilation.GetTypeByMetadataName("System.Text.RegularExpressions.Regex"); 144 | public INamedTypeSymbol ICollection { get; } = compilation.GetTypeByMetadataName("System.Collections.ICollection"); 145 | public INamedTypeSymbol IComparer { get; } = compilation.GetTypeByMetadataName("System.Collections.IComparer"); 146 | public INamedTypeSymbol IEqualityComparerOfT1 { get; } = compilation.GetTypeByMetadataName("System.Collections.Generic.IEqualityComparer`1"); 147 | public INamedTypeSymbol IEnumerableOfT1 { get; } = compilation.GetTypeByMetadataName("System.Collections.Generic.IEnumerable`1"); 148 | 149 | public INamedTypeSymbol Identity { get; } = null; 150 | } 151 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/Utilities/OperartionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using FluentAssertions.Analyzers.Utilities; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.Operations; 5 | 6 | namespace FluentAssertions.Analyzers; 7 | 8 | internal static class OperartionExtensions 9 | { 10 | /// 11 | /// Tries to get the first descendent of the parent operation. where each operation has only one child. 12 | /// 13 | public static bool TryGetFirstDescendent(this IOperation parent, out TOperation operation) where TOperation : IOperation 14 | { 15 | IOperation current = parent; 16 | while (current.ChildOperations.Count >= 1) 17 | { 18 | current = current.ChildOperations.First(); 19 | if (current is TOperation op) 20 | { 21 | operation = op; 22 | return true; 23 | } 24 | } 25 | 26 | operation = default; 27 | return false; 28 | } 29 | 30 | /// 31 | /// Tries to get the first descendent of the parent operation. where each operation has only one child. 32 | /// 33 | public static TOperation GetFirstDescendent(this IOperation parent) where TOperation : IOperation 34 | { 35 | IOperation current = parent; 36 | while (current.ChildOperations.Count >= 1) 37 | { 38 | current = current.ChildOperations.First(); 39 | if (current is TOperation op) 40 | { 41 | return op; 42 | } 43 | } 44 | 45 | return default; 46 | } 47 | 48 | public static TOperation GetFirstAncestor(this IOperation parent) where TOperation : IOperation 49 | { 50 | IOperation current = parent; 51 | while (current.Parent is not null) 52 | { 53 | current = current.Parent; 54 | if (current is TOperation op) 55 | { 56 | return op; 57 | } 58 | } 59 | 60 | return default; 61 | } 62 | 63 | public static bool IsContainedInType(this IPropertyReferenceOperation property, SpecialType type) 64 | => property.Property.ContainingType.ConstructedFromType(type); 65 | public static bool IsContainedInType(this IPropertyReferenceOperation property, INamedTypeSymbol type) 66 | => property.Property.ContainingType.ConstructedFromType(type); 67 | public static bool IsContainedInType(this IInvocationOperation invocation, SpecialType type) 68 | => invocation.TargetMethod.ContainingType.ConstructedFromType(type); 69 | public static bool IsContainedInType(this IInvocationOperation invocation, INamedTypeSymbol type) 70 | => invocation.TargetMethod.ContainingType.ConstructedFromType(type); 71 | public static bool ImplementsOrIsInterface(this IPropertyReferenceOperation property, SpecialType type) 72 | => property.Property.ContainingType.ImplementsOrIsInterface(type); 73 | public static bool ImplementsOrIsInterface(this IInvocationOperation invocation, SpecialType type) 74 | => invocation.TargetMethod.ContainingType.ImplementsOrIsInterface(type); 75 | public static bool ImplementsOrIsInterface(this IInvocationOperation invocation, INamedTypeSymbol type) 76 | => invocation.TargetMethod.ContainingType.ImplementsOrIsInterface(type); 77 | public static bool ImplementsOrIsInterface(this IArgumentOperation argument, SpecialType type) 78 | => argument.Value.UnwrapConversion().Type.ImplementsOrIsInterface(type); 79 | public static bool ImplementsOrIsInterface(this IArgumentOperation argument, INamedTypeSymbol type) 80 | => argument.Value.UnwrapConversion().Type.ImplementsOrIsInterface(type); 81 | public static bool IsTypeof(this IArgumentOperation argument, SpecialType type) 82 | => argument.Value.UnwrapConversion().Type.SpecialType == type; 83 | public static bool IsTypeof(this IArgumentOperation argument, INamedTypeSymbol type) 84 | => argument.Value.UnwrapConversion().Type.EqualsSymbol(type); 85 | 86 | public static bool AreMethodParameterSameTypeAsContainingTypeArguments(this IInvocationOperation invocation, params (int parameter, int typeArgument)[] parameters) 87 | { 88 | if (invocation.TargetMethod.Parameters.Length != parameters.Length) 89 | return false; 90 | 91 | if (invocation.TargetMethod.ContainingType.TypeArguments.Length < parameters.Max(x => x.typeArgument)) 92 | return false; 93 | 94 | foreach (var (parameter, typeArgument) in parameters) 95 | { 96 | if (!invocation.TargetMethod.Parameters[parameter].Type.EqualsSymbol(invocation.TargetMethod.ContainingType.TypeArguments[typeArgument])) 97 | return false; 98 | } 99 | 100 | return true; 101 | } 102 | 103 | public static bool IsSameArgumentReference(this IArgumentOperation argument1, IArgumentOperation argument2) 104 | { 105 | return argument1.TryGetFirstDescendent(out var argument1Reference) 106 | && argument2.TryGetFirstDescendent(out var argument2Reference) 107 | && argument1Reference.Parameter.EqualsSymbol(argument2Reference.Parameter); 108 | } 109 | public static bool IsSamePropertyReference(this IPropertyReferenceOperation property1, IPropertyReferenceOperation property2) 110 | { 111 | return (property1.Instance is ILocalReferenceOperation local1 112 | && property2.Instance is ILocalReferenceOperation local2 113 | && local1.Local.EqualsSymbol(local2.Local)) 114 | || 115 | (property1.Instance is IParameterReferenceOperation parameter1 116 | && property2.Instance is IParameterReferenceOperation parameter2 117 | && parameter1.Parameter.EqualsSymbol(parameter2.Parameter)); 118 | } 119 | 120 | public static bool IsStaticPropertyReference(this IArgumentOperation argument, INamedTypeSymbol type, string property) 121 | { 122 | return argument.Value is IPropertyReferenceOperation propertyReference && propertyReference.Instance is null 123 | && propertyReference.Property.Type.EqualsSymbol(type) 124 | && propertyReference.Property.Name == property; 125 | } 126 | 127 | public static bool IsLiteralValue(this IArgumentOperation argument) 128 | => UnwrapConversion(argument.Value).Kind is OperationKind.Literal; 129 | public static bool IsLiteralValue(this IArgumentOperation argument, T value) 130 | => UnwrapConversion(argument.Value) is ILiteralOperation literal && literal.ConstantValue.HasValue && literal.ConstantValue.Value.Equals(value); 131 | public static bool IsLiteralNull(this IArgumentOperation argument) 132 | => UnwrapConversion(argument.Value) is ILiteralOperation literal && literal.ConstantValue.HasValue && literal.ConstantValue.Value is null; 133 | public static bool IsLiteralValue(this IArgumentOperation argument) 134 | => UnwrapConversion(argument.Value) is ILiteralOperation literal && literal.ConstantValue.HasValue && literal.ConstantValue.Value is T; 135 | public static bool TryGetLiteralValue(this IArgumentOperation argument, out T value) 136 | { 137 | if (UnwrapConversion(argument.Value) is ILiteralOperation literal && literal.ConstantValue.HasValue && literal.ConstantValue.Value is T tValue) 138 | { 139 | value = tValue; 140 | return true; 141 | } 142 | 143 | value = default; 144 | return false; 145 | } 146 | 147 | public static bool IsReference(this IArgumentOperation argument) 148 | { 149 | var operation = UnwrapConversion(argument.Value); 150 | return operation.Kind is OperationKind.LocalReference || operation.Kind is OperationKind.ParameterReference; 151 | } 152 | 153 | public static bool IsReferenceOfType(this IArgumentOperation argument, SpecialType type) 154 | { 155 | var current = UnwrapConversion(argument.Value); 156 | return current switch 157 | { 158 | ILocalReferenceOperation local => local.Local.Type.SpecialType == type, 159 | IParameterReferenceOperation parameter => parameter.Parameter.Type.SpecialType == type, 160 | _ => false, 161 | }; 162 | } 163 | 164 | public static bool IsLambda(this IArgumentOperation argument) 165 | => argument.Value is IDelegateCreationOperation delegateCreation && delegateCreation.Target.Kind == OperationKind.AnonymousFunction; 166 | 167 | public static bool HasEmptyBecauseAndReasonArgs(this IInvocationOperation invocation, int startingIndex = 0) 168 | { 169 | if (invocation.Arguments.Length != startingIndex + 2) 170 | { 171 | return false; 172 | } 173 | 174 | return invocation.Arguments[startingIndex].Value is ILiteralOperation literal && literal.ConstantValue.HasValue && literal.ConstantValue.Value is "" 175 | && invocation.Arguments[startingIndex + 1].Value is IArrayCreationOperation arrayCreation && arrayCreation.Initializer.ElementValues.IsEmpty; 176 | } 177 | 178 | public static bool TryGetChainedInvocationAfterAndConstraint(this IInvocationOperation invocation, string chainedMethod, out IInvocationOperation chainedInvocation) 179 | { 180 | if (invocation.Parent is IPropertyReferenceOperation { Property.Name: "And" } andConstraint) 181 | { 182 | chainedInvocation = andConstraint.Parent as IInvocationOperation; 183 | return chainedInvocation?.TargetMethod?.Name == chainedMethod; 184 | } 185 | 186 | chainedInvocation = null; 187 | return false; 188 | } 189 | 190 | public static bool TryGetSingleArgumentAs(this IInvocationOperation invocation, out TOperation operation) 191 | { 192 | if (invocation.Arguments.Length is 1 && invocation.Arguments[0].Value.UnwrapConversion() is TOperation op) 193 | { 194 | operation = op; 195 | return true; 196 | } 197 | 198 | operation = default; 199 | return false; 200 | } 201 | 202 | public static bool TryGetFirstArgumentAs(this IInvocationOperation invocation, out TOperation operation) 203 | { 204 | if (invocation.Arguments.Length is >= 1 && invocation.Arguments[0].Value.UnwrapConversion() is TOperation op) 205 | { 206 | operation = op; 207 | return true; 208 | } 209 | 210 | operation = default; 211 | return false; 212 | } 213 | 214 | public static IOperation UnwrapConversion(this IOperation operation) 215 | { 216 | return operation switch 217 | { 218 | IConversionOperation conversion => conversion.Operand, 219 | _ => operation, 220 | }; 221 | } 222 | public static IOperation UnwrapParentConversion(this IOperation operation) 223 | { 224 | return operation switch 225 | { 226 | IConversionOperation conversion => conversion.Parent, 227 | _ => operation, 228 | }; 229 | } 230 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/Utilities/TypesExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System.Linq; 3 | 4 | namespace FluentAssertions.Analyzers.Utilities; 5 | 6 | internal static class TypesExtensions 7 | { 8 | public static bool IsTypeOrConstructedFromTypeOrImplementsType(this INamedTypeSymbol type, INamedTypeSymbol other) 9 | { 10 | var abstractType = type.OriginalDefinition; 11 | return abstractType.EqualsSymbol(other) 12 | || abstractType.AllInterfaces.Any(@interface => @interface.OriginalDefinition.EqualsSymbol(other)); 13 | } 14 | 15 | public static bool IsTypeOrConstructedFromTypeOrImplementsType(this ITypeSymbol type, SpecialType specialType) 16 | { 17 | return type.SpecialType == specialType 18 | || type.AllInterfaces.Any(@interface => @interface.OriginalDefinition.SpecialType == specialType); 19 | } 20 | 21 | 22 | public static bool ConstructedFromType(this INamedTypeSymbol type, INamedTypeSymbol interfaceType) 23 | { 24 | var current = type; 25 | while (!current.EqualsSymbol(current.ConstructedFrom)) 26 | { 27 | current = current.ConstructedFrom; 28 | } 29 | return current.EqualsSymbol(interfaceType); 30 | } 31 | 32 | public static bool ConstructedFromType(this INamedTypeSymbol type, SpecialType specialType) 33 | { 34 | var current = type; 35 | while (!current.EqualsSymbol(current.ConstructedFrom)) 36 | { 37 | current = current.ConstructedFrom; 38 | } 39 | return current.SpecialType == specialType; 40 | } 41 | 42 | public static bool ImplementsOrIsInterface(this ITypeSymbol type, INamedTypeSymbol interfaceType) 43 | { 44 | var current = type; 45 | while (!current.EqualsSymbol(current.OriginalDefinition)) 46 | { 47 | current = current.OriginalDefinition; 48 | } 49 | return current.EqualsSymbol(interfaceType) 50 | || current.AllInterfaces.Any(@interface => @interface.OriginalDefinition.EqualsSymbol(interfaceType)); 51 | } 52 | public static bool ImplementsOrIsInterface(this ITypeSymbol type, SpecialType specialType) 53 | { 54 | var current = type; 55 | while (!current.EqualsSymbol(current.OriginalDefinition)) 56 | { 57 | current = current.OriginalDefinition; 58 | } 59 | return current.SpecialType == specialType || current.AllInterfaces.Any(@interface => @interface.OriginalDefinition.SpecialType.Equals(specialType)); 60 | } 61 | 62 | public static bool IsOrInheritsFrom(this ITypeSymbol symbol, INamedTypeSymbol baseSymbol) 63 | { 64 | if (symbol is null || baseSymbol is null) 65 | return false; 66 | 67 | do 68 | { 69 | if (symbol.EqualsSymbol(baseSymbol)) 70 | return true; 71 | 72 | symbol = symbol.BaseType; 73 | } while (symbol is not null); 74 | 75 | return false; 76 | } 77 | 78 | public static bool EqualsSymbol(this ISymbol type, ISymbol other) => type.Equals(other, SymbolEqualityComparer.Default); 79 | } 80 | -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/tools/install.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | if($project.Object.SupportsPackageDependencyResolution) 4 | { 5 | if($project.Object.SupportsPackageDependencyResolution()) 6 | { 7 | # Do not install analyzers via install.ps1, instead let the project system handle it. 8 | return 9 | } 10 | } 11 | 12 | $analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve 13 | 14 | foreach($analyzersPath in $analyzersPaths) 15 | { 16 | if (Test-Path $analyzersPath) 17 | { 18 | # Install the language agnostic analyzers. 19 | foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) 20 | { 21 | if($project.Object.AnalyzerReferences) 22 | { 23 | $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) 24 | } 25 | } 26 | } 27 | } 28 | 29 | # $project.Type gives the language name like (C# or VB.NET) 30 | $languageFolder = "" 31 | if($project.Type -eq "C#") 32 | { 33 | $languageFolder = "cs" 34 | } 35 | if($project.Type -eq "VB.NET") 36 | { 37 | $languageFolder = "vb" 38 | } 39 | if($languageFolder -eq "") 40 | { 41 | return 42 | } 43 | 44 | foreach($analyzersPath in $analyzersPaths) 45 | { 46 | # Install language specific analyzers. 47 | $languageAnalyzersPath = join-path $analyzersPath $languageFolder 48 | if (Test-Path $languageAnalyzersPath) 49 | { 50 | foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) 51 | { 52 | if($project.Object.AnalyzerReferences) 53 | { 54 | $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/FluentAssertions.Analyzers/tools/uninstall.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | if($project.Object.SupportsPackageDependencyResolution) 4 | { 5 | if($project.Object.SupportsPackageDependencyResolution()) 6 | { 7 | # Do not uninstall analyzers via uninstall.ps1, instead let the project system handle it. 8 | return 9 | } 10 | } 11 | 12 | $analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve 13 | 14 | foreach($analyzersPath in $analyzersPaths) 15 | { 16 | # Uninstall the language agnostic analyzers. 17 | if (Test-Path $analyzersPath) 18 | { 19 | foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) 20 | { 21 | if($project.Object.AnalyzerReferences) 22 | { 23 | $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) 24 | } 25 | } 26 | } 27 | } 28 | 29 | # $project.Type gives the language name like (C# or VB.NET) 30 | $languageFolder = "" 31 | if($project.Type -eq "C#") 32 | { 33 | $languageFolder = "cs" 34 | } 35 | if($project.Type -eq "VB.NET") 36 | { 37 | $languageFolder = "vb" 38 | } 39 | if($languageFolder -eq "") 40 | { 41 | return 42 | } 43 | 44 | foreach($analyzersPath in $analyzersPaths) 45 | { 46 | # Uninstall language specific analyzers. 47 | $languageAnalyzersPath = join-path $analyzersPath $languageFolder 48 | if (Test-Path $languageAnalyzersPath) 49 | { 50 | foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) 51 | { 52 | if($project.Object.AnalyzerReferences) 53 | { 54 | try 55 | { 56 | $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) 57 | } 58 | catch 59 | { 60 | 61 | } 62 | } 63 | } 64 | } 65 | } --------------------------------------------------------------------------------