├── .gitignore ├── LICENSE.txt ├── NOTICE.txt ├── README.md ├── RelaseNotes.md ├── assets ├── AnalyzerNoWarningWithJustification.png ├── AnalyzerWarningAndCodeFix.png ├── NullForgivingMenu.png └── NullForgivingToolWindow.png ├── azure-pipelines.yml └── src ├── .editorconfig ├── Directory.Build.props ├── IntegrationTest ├── .editorconfig ├── Build.cmd ├── IntegrationTest.sln ├── IntegrationTest │ ├── Class1.cs │ ├── IntegrationTest.csproj │ └── ScreenShots.cs └── NuGet.config ├── NuGet.config ├── Nullable.Extended.Analyzer ├── Nullable.Extended.Analyzer.Package │ └── Nullable.Extended.Analyzer.Package.csproj ├── Nullable.Extended.Analyzer.Test │ ├── NullForgivingCodeFixProviderTests.cs │ ├── NullForgivingDetectionTests.cs │ ├── Nullable.Extended.Analyzer.Test.csproj │ └── Verifiers │ │ ├── CSharpAnalyzerVerifier`1+Test.cs │ │ ├── CSharpAnalyzerVerifier`1.cs │ │ ├── CSharpCodeFixVerifier`2+Test.cs │ │ ├── CSharpCodeFixVerifier`2.cs │ │ └── CSharpVerifierHelper.cs └── Nullable.Extended.Analyzer │ ├── Key.snk │ ├── NullForgivingDetectionAnalyzer.cs │ ├── NullForgivingDetectionAnalyzerCodeFixProvider.cs │ ├── Nullable.Extended.Analyzer.csproj │ └── Nullable.Extended.Analyzer.targets ├── Nullable.Extended.Extension ├── Nullable.Extended.Extension.sln ├── Nullable.Extended.Extension.sln.DotSettings ├── Nullable.Extended.Extension │ ├── AnalyzerFramework │ │ ├── AnalysisContext.cs │ │ ├── AnalysisResult.cs │ │ ├── AnalysisResultsChangedArgs.cs │ │ ├── AnalyzerEngine.cs │ │ ├── ExtensionMethods.cs │ │ ├── IAnalyzerEngine.cs │ │ ├── ISyntaxAnalysisPostProcessor.cs │ │ └── ISyntaxTreeAnalyzer.cs │ ├── Extension │ │ ├── ExtensionPackage.cs │ │ ├── OpenToolWindowCommand.cs │ │ └── ProvideToolboxControlAttribute.cs │ ├── FodyWeavers.xml │ ├── FodyWeavers.xsd │ ├── NullForgivingAnalyzer │ │ ├── ExtensionMethods.cs │ │ ├── NullForgivingAnalysisResult.cs │ │ ├── NullForgivingContext.cs │ │ ├── NullForgivingDetectionAnalyzer.cs │ │ └── NullForgivingDetectionPostProcessor.cs │ ├── Nullable.Extended.Extension.csproj │ ├── Package.vsct │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Resources │ │ ├── Icons.png │ │ ├── VSColorScheme.xaml │ │ ├── delete.png │ │ └── refresh.png │ ├── VSPackage.resx │ ├── Views │ │ ├── AnalyzerResultViewModel.cs │ │ ├── AnalyzerViewModel.cs │ │ ├── ExtensionMethods.cs │ │ ├── NullForgivingToolWindow.cs │ │ ├── NullForgivingToolWindowView.xaml │ │ ├── NullForgivingToolWindowView.xaml.cs │ │ ├── NullForgivingToolWindowViewModel.cs │ │ ├── ThemeManager.cs │ │ ├── ToolWindowShell.xaml │ │ └── ToolWindowShell.xaml.cs │ └── source.extension.vsixmanifest ├── Sample │ ├── Class1.cs │ └── Sample.csproj └── Test │ ├── Test.csproj │ └── UnitTest1.cs ├── Nullable.Extended.sln ├── Nullable.Extended.sln.DotSettings ├── Nullable.Shared ├── ExtensionMethods.cs ├── Justification.cs ├── Nullable.Shared.projitems ├── Nullable.Shared.shproj └── Nullable.cs ├── clean.cmd └── package_icon.png /.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 | 8 | # Build results 9 | [Dd]ebug/ 10 | [Rr]elease/ 11 | [Bb]in/ 12 | [Oo]bj/ 13 | [Dd]eploy/ 14 | [Dd]ocumentation/[Hh]elp 15 | [Pp]ublish/ 16 | 17 | # MSTest test Results 18 | [Tt]est[Rr]esult*/ 19 | [Bb]uild[Ll]og.* 20 | 21 | # Nuget 22 | [Pp]ackages/ 23 | 24 | # Visual Studio 25 | .vs/ 26 | 27 | _ReSharper.Caches/ 28 | 29 | msbuild.binlog 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2021 tom-englert.de 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 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Nullable.Extended.Analyzer 2 | Copyright (C) 2020 tom-englert.de 3 | mailto:github@tom-englert.de 4 | 5 | Nullable.Extended.Analyzer uses third-party libraries or other resources that may be distributed under licenses different than the Nullable.Extended.Analyzer softwares. 6 | 7 | In the event that we accidentally failed to list a required notice, please bring it to our attention by posting an issue. 8 | 9 | This project uses other open source projects, which are used under the terms of the following license(s). 10 | -------------------------------------------------- 11 | 12 | Sonar DotNet Analyzer 13 | 14 | Copyright (c) SonarSource (http://www.sonarsource.com/). 15 | 16 | Licensed under the GNU LESSER GENERAL PUBLIC LICENSE; you may not use 17 | these files except in compliance with the License. A copy of the license 18 | is distributed with this project 19 | 20 | 21 | StyleCopAnalyzers - Lightup 22 | 23 | Copyright (c) Tunnel Vision Laboratories, LLC. All rights reserved. 24 | 25 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 26 | these files except in compliance with the License. You may obtain a copy of the 27 | License at 28 | 29 | http://www.apache.org/licenses/LICENSE-2.0 30 | 31 | Unless required by applicable law or agreed to in writing, software distributed 32 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 33 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 34 | specific language governing permissions and limitations under the License. 35 | 36 | .NET Compiler Platform ("Roslyn") 37 | 38 | Copyright Microsoft. 39 | 40 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 41 | these files except in compliance with the License. You may obtain a copy of the 42 | License at 43 | 44 | http://www.apache.org/licenses/LICENSE-2.0 45 | 46 | Unless required by applicable law or agreed to in writing, software distributed 47 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 48 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 49 | specific language governing permissions and limitations under the License. 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nullable Extended 2 | [![Build Status](https://dev.azure.com/tom-englert/Open%20Source/_apis/build/status/Nullable.Extended?branchName=master)](https://dev.azure.com/tom-englert/Open%20Source/_build/latest?definitionId=39&branchName=master) 3 | [![NuGet Status](https://img.shields.io/nuget/v/Nullable.Extended.Analyzer.svg)](https://www.nuget.org/packages/Nullable.Extended.Analyzer/) 4 | 5 | ## Roslyn Tools and Analyzers to improve the experience when coding with Nullable Reference Types. 6 | 7 | [Nullable reference types](https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references) 8 | are a great feature introduced with C# 8.0. It really helps in writing better code. 9 | 10 | This project helps to improve the experience when coding with nullable reference types. 11 | 12 | ### Managing Null-Forgiving Operators 13 | 14 | Sometimes it is necessary to use the 15 | [null-forgiving operator "!"](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-forgiving) 16 | to suppress a false warning. 17 | However using too many of them will render the value of nullable checks almost useless. 18 | 19 | It's hard to find the null forgiving operators in code, because the `!` symbol is so unobtrusive, and has multiple meanings in code. 20 | 21 | Another obstacle is that after some time you or your colleagues won't remember why you have 22 | added a null-forgiving operator, or if its usage has been validated, so you will have to look at the code again and again. 23 | 24 | This toolset will assist you in managing these problems. 25 | It will detect and show occurrences of the null-forgiving operator and encourage you to annotate them with a justification comment, 26 | so you and your colleagues will instantly know why this usage of the null-forgiving operator is required. 27 | 28 | The [Visual Studio Extension](#visual-studio-extension) will show you all occurrences in a list, 29 | while the [analyzer](#roslyn-analyzer) will generate a warning for every occurrence that is not annotated with a justification comment, 30 | and provides a code fix to add the scaffold for the justification comment. 31 | 32 | Justification comments are one or more single line comments above the statement or declaration that start with `\\ ! `, followed by some justification text: 33 | ```c# 34 | // ! This is only needed to demo the feature 35 | var text = value1!.ToString(); 36 | ``` 37 | #### There are two tools available: 38 | - A [Visual Studio Extension](#visual-studio-extension) that analyzes your sources and tracks usage of null forgiving operators. 39 | - A [Roslyn Analyzer](#roslyn-analyzer) that shows a warning for every unjustified usage of the null-forgiving operator. 40 | 41 | ## Visual Studio Extension 42 | 43 | The [Visual Studio Extension](#visual-studio-extension) lists all occurrences, 44 | categorizes them, and even detects those that are still present in code but no longer needed. 45 | 46 | Occurrences are grouped into three categories, to reflect their different contexts: 47 | - General usages of the null-forgiving operator. 48 | - Null-forgiving operator on the `null` or `default` literals. 49 | - Null-forgiving operator inside lambda expressions. 50 | 51 | > e.g. general usages can be mostly avoided by cleaning up the code, 52 | while inside lambda expressions they are often unavoidable 53 | 54 | ### Installation 55 | 56 | Install the extension from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=TomEnglert.NullableExtended) or from the [Releases](../../releases) page. 57 | The latest CI build is available from the [Open VSIX Gallery](https://www.vsixgallery.com/extension/Nullable.Extended.75a92614-c590-4401-b04b-04926c0e21cf) 58 | 59 | ### Usage 60 | 61 | Start the tool window from the tools menu: 62 | 63 | ![menu](assets/NullForgivingMenu.png) 64 | 65 | In the tool window you can see all occurrences of the null-forgiving operator in your solution and their justification comments. 66 | It also shows if the null-forgiving operator is required to suppress nullable warnings. 67 | If the "Required" column is not checked, it is a hint that the null-forgiving operator might be obsolete and can probably be removed. 68 | 69 | ![window](assets/NullForgivingToolWindow.png) 70 | 71 | - Click the refresh button to scan your solution. 72 | - Double click any entry to navigate to the corresponding source code. 73 | 74 | --- 75 | ## Roslyn Analyzer 76 | 77 | ### Installation 78 | 79 | Simply install the [NuGet Package](https://www.nuget.org/packages/Nullable.Extended.Analyzer/) in your projects. 80 | 81 | ### Show unjustified occurrences of the null-forgiving operator as warnings 82 | 83 | The analyzer will generate a warning for every unjustified occurrence of the null-forgiving operator. Depending on the context the code will be NX0001 to NX0004. 84 | 85 | A warning will be shown for unjustified occurrences: 86 | 87 | ![warning](assets/AnalyzerWarningAndCodeFix.png) 88 | 89 | The warning will disappear after a justification comment has been added: 90 | 91 | ![no_warning](assets/AnalyzerNoWarningWithJustification.png) 92 | 93 | > You can easily configure the warning levels via you editor.config file. 94 | 95 | -------------------------------------------------------------------------------- /RelaseNotes.md: -------------------------------------------------------------------------------- 1 | [Next] 2 | 3 | 1.15.2 4 | - Show also NX0004 (Init only) context in VS extension. 5 | 6 | 1.15.1 7 | - The codefix suggestion will use the same line ending as the offending code. 8 | - Fix crash when suggesting a fix for un-indented code. 9 | 10 | 1.15 11 | - Default severity of analyzers should be warning to be suppressable. 12 | 13 | 1.14 14 | - Introduce InitRule for "init only" properties; 15 | - Default severity is now Error. 16 | - Remove the outdated sonar-based suppressor. 17 | 18 | 1.13 19 | - Adjust colors to better match VS2022 20 | 21 | 1.12 22 | - Update DGX to improve composite filter experience 23 | - Improve reliability of the "Required" flag by analyzing each result individually. 24 | 25 | 1.11 26 | - Remove the "Remove all not required tokens" command, it's of no practical use 27 | 28 | 1.10 29 | - improve data grid filtering experience 30 | - Fix: detect justification comment on field declaration 31 | 32 | 1.9 33 | - #10: Add a code fix provider 34 | 35 | 1.8 36 | - #10: Fix justification, comments were not always detected properly 37 | 38 | 1.7 39 | - #8: Improve keyboard navigation 40 | - #9: Navigate to operator crashes VS2022 41 | - #10: Recover the NullForgivingDetectionAnalyzer and support suppressions via `"// ! comment"` 42 | 43 | 1.6 44 | - Fix #7: XAML assembly resolving fails for DGX 45 | 46 | 1.5 47 | - Analysis results may depend on framework version: use the most relevant result. 48 | 49 | 1.4 50 | - Support VS2022 51 | - Fix #5: Add an icon to the tool window 52 | - Fix multiple choice filter 53 | - Fix detection of required null-forgiving operators 54 | 55 | 1.3 56 | - Fix installation issues 57 | 58 | 1.2 59 | - Use multiple choice filter for appropriate columns 60 | - Show lean project name 61 | 62 | 1.1 63 | - Fix #3: False negative when null forgiving operator is preceded by whitespace. 64 | - Fix #2: Duplicate entries when project targets multiple frameworks 65 | - Fix threading issues 66 | 67 | 1.0 68 | - Initial release -------------------------------------------------------------------------------- /assets/AnalyzerNoWarningWithJustification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tom-englert/Nullable.Extended/cef7dd65770648240c1edc09597439b35e74d35b/assets/AnalyzerNoWarningWithJustification.png -------------------------------------------------------------------------------- /assets/AnalyzerWarningAndCodeFix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tom-englert/Nullable.Extended/cef7dd65770648240c1edc09597439b35e74d35b/assets/AnalyzerWarningAndCodeFix.png -------------------------------------------------------------------------------- /assets/NullForgivingMenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tom-englert/Nullable.Extended/cef7dd65770648240c1edc09597439b35e74d35b/assets/NullForgivingMenu.png -------------------------------------------------------------------------------- /assets/NullForgivingToolWindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tom-englert/Nullable.Extended/cef7dd65770648240c1edc09597439b35e74d35b/assets/NullForgivingToolWindow.png -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | pool: 2 | vmImage: 'windows-latest' 3 | 4 | variables: 5 | solution: 'src/Nullable.Extended.sln' 6 | buildPlatform: 'Any CPU' 7 | buildConfiguration: 'Release' 8 | 9 | steps: 10 | - task: PowerShell@1 11 | displayName: 'Set build version' 12 | inputs: 13 | scriptType: inlineScript 14 | inlineScript: | 15 | (new-object Net.WebClient).DownloadString("https://raw.github.com/tom-englert/BuildScripts/master/BuildScripts.ps1") | iex 16 | Project-SetVersion "src\Directory.Build.props" | Build-AppendVersionToBuildNumber 17 | condition: eq(variables['Build.SourceBranch'], 'refs/heads/master') 18 | 19 | - task: VSBuild@1 20 | displayName: Build 21 | inputs: 22 | solution: '$(solution)' 23 | platform: '$(buildPlatform)' 24 | configuration: '$(buildConfiguration)' 25 | msbuildArgs: '-restore' 26 | env: 27 | DeployExtension: False 28 | 29 | - task: DotNetCoreCLI@2 30 | displayName: Test 31 | inputs: 32 | command: 'test' 33 | projects: '$(solution)' 34 | arguments: '--no-build --configuration $(buildConfiguration)' 35 | 36 | - task: CopyFiles@2 37 | inputs: 38 | SourceFolder: 'src' 39 | Contents: '**\$(buildConfiguration)\*.nupkg' 40 | TargetFolder: '$(Build.ArtifactStagingDirectory)\Nuget' 41 | flattenFolders: true 42 | 43 | - task: CopyFiles@2 44 | inputs: 45 | SourceFolder: 'src' 46 | Contents: '**\$(buildConfiguration)\**\*.vsix' 47 | TargetFolder: '$(Build.ArtifactStagingDirectory)\Vsix' 48 | flattenFolders: true 49 | 50 | - task: PublishBuildArtifacts@1 51 | inputs: 52 | PathtoPublish: '$(Build.ArtifactStagingDirectory)\Nuget' 53 | ArtifactName: 'Package' 54 | publishLocation: 'Container' 55 | 56 | - task: PublishBuildArtifacts@1 57 | inputs: 58 | PathtoPublish: '$(Build.ArtifactStagingDirectory)\Vsix' 59 | ArtifactName: 'Vsix' 60 | publishLocation: 'Container' 61 | 62 | - task: PowerShell@1 63 | displayName: 'Publish to vsix-gallery' 64 | enabled: 'true' 65 | condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master')) 66 | inputs: 67 | scriptType: inlineScript 68 | inlineScript: | 69 | (new-object Net.WebClient).DownloadString("https://raw.github.com/tom-englert/BuildScripts/master/BuildScripts.ps1") | iex 70 | Vsix-PublishToGallery "$(Build.ArtifactStagingDirectory)\Vsix\Nullable.Extended.Extension.vsix" 71 | 72 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | 7 | # Code files 8 | [*.{cs,csx,vb,vbx}] 9 | indent_size = 4 10 | insert_final_newline = true 11 | charset = utf-8-bom 12 | 13 | # Code style 14 | [*.{cs,vb}] 15 | # Organize usings 16 | dotnet_sort_system_directives_first = true 17 | dotnet_separate_import_directive_groups = true 18 | csharp_indent_case_contents_when_block = false 19 | 20 | # Diagnostic configuration 21 | [*.cs] 22 | 23 | # RS2008: Enable analyzer release tracking 24 | dotnet_diagnostic.rs2008.severity = none 25 | 26 | # Microsoft .NET properties 27 | dotnet_separate_import_directive_groups = true 28 | 29 | # ReSharper properties 30 | resharper_csharp_wrap_lines = false 31 | 32 | # IDE0008: Use explicit type 33 | csharp_style_var_elsewhere = true 34 | 35 | # CA2007: Consider calling ConfigureAwait on the awaited task => using ConfigureAwait.Fody 36 | dotnet_diagnostic.CA2007.severity = none 37 | 38 | # CA1062: Validate arguments of public methods 39 | dotnet_diagnostic.CA1062.severity = none 40 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.15.2 4 | latest 5 | enable 6 | enable 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/IntegrationTest/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # NX0003: Find usages of the NullForgiving operator inside lambda expressions. 4 | dotnet_diagnostic.NX0003.severity = warning 5 | 6 | # NX0001: Find general usages of the NullForgiving operator. 7 | dotnet_diagnostic.NX0001.severity = warning 8 | -------------------------------------------------------------------------------- /src/IntegrationTest/Build.cmd: -------------------------------------------------------------------------------- 1 | "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild" -restore /t:Rebuild /p:platform="Any CPU" /p:configuration="Release" /p:VisualStudioVersion="16.0" 2 | Pause -------------------------------------------------------------------------------- /src/IntegrationTest/IntegrationTest.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30804.86 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTest", "IntegrationTest\IntegrationTest.csproj", "{708117A4-DC1F-4C3B-998B-E944FAD1255E}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4D928DA1-6A13-4E5F-9377-B0DFE4E76749}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | NuGet.config = NuGet.config 12 | EndProjectSection 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {708117A4-DC1F-4C3B-998B-E944FAD1255E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {708117A4-DC1F-4C3B-998B-E944FAD1255E}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {708117A4-DC1F-4C3B-998B-E944FAD1255E}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {708117A4-DC1F-4C3B-998B-E944FAD1255E}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {6D5934E1-1062-4F64-934C-12160DD7E600} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /src/IntegrationTest/IntegrationTest/Class1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace IntegrationTest 7 | { 8 | public class Class1 9 | { 10 | public void M1(string value1, object value2) 11 | { 12 | } 13 | 14 | public void M2(object? value1, object? value2) 15 | { 16 | var text = value1?.ToString(); 17 | 18 | if (text == null) 19 | return; 20 | 21 | M1(text, value1); 22 | 23 | // ! This is just to demo the feature 24 | var x = Enumerable.Range(0, 1) 25 | .Select(i => value2) 26 | .Where(item => item != null) 27 | .Select(item => item!.ToString()) 28 | .FirstOrDefault()!; 29 | } 30 | 31 | public void M3(object? value1, object? value2) 32 | { 33 | // ! This is only needed to demo the feature 34 | var text = value1!.ToString(); 35 | 36 | if (text == null) 37 | return; 38 | 39 | M1(text, value1); 40 | 41 | // ! This is just to demo the feature 42 | var x = Enumerable.Range(0, 1) 43 | .Select(i => value2) 44 | .Where(item => item != null) 45 | .Select(item => item!.ToString()) 46 | .FirstOrDefault()!; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/IntegrationTest/IntegrationTest/IntegrationTest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | false 5 | latest 6 | enable 7 | nullable 8 | 9 | c:\temp\NullableExtendedAnalyzer.$(MSBuildProjectName).log 10 | 11 | true 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/IntegrationTest/IntegrationTest/ScreenShots.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace IntegrationTest 6 | { 7 | class ScreenShots 8 | { 9 | static void Test(object value1, object? value2) 10 | { 11 | var target1 = value1!.ToString(); 12 | var target2 = value2!.ToString(); 13 | 14 | if (target1 != null && target2 != null) 15 | { 16 | Console.WriteLine("Don't blame me for that sample code"); 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/IntegrationTest/NuGet.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/NuGet.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Analyzer/Nullable.Extended.Analyzer.Package/Nullable.Extended.Analyzer.Package.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | false 6 | true 7 | true 8 | tom-englert 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Nullable.Extended.Analyzer 24 | tom-englert.de 25 | false 26 | 27 | A Roslyn analyzer to improve the experience when working with nullable reference types. 28 | (c) 2020, tom-englert.de 29 | nullable, analyzers, roslyn 30 | true 31 | 32 | @(PackageIconFiles->'%(Filename)%(Extension)') 33 | @(PackageLicenseFiles->'%(Filename)%(Extension)') 34 | http://github.com/$(GitHubOrganization)/$(PackageId) 35 | $(PackageIconFileName) 36 | $(PackageLicenseFileName) 37 | 38 | $(TargetsForTfmSpecificContentInPackage);_AddAnalyzersToOutput 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Analyzer/Nullable.Extended.Analyzer.Test/NullForgivingCodeFixProviderTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.Testing; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | using static Nullable.Extended.Analyzer.Test.Verifiers.CSharpCodeFixVerifier< 6 | Nullable.Extended.Analyzer.NullForgivingDetectionAnalyzer, 7 | Nullable.Extended.Analyzer.NullForgivingDetectionAnalyzerCodeFixProvider>; 8 | 9 | 10 | namespace Nullable.Extended.Analyzer.Test 11 | { 12 | [TestClass] 13 | public class NullForgivingCodeFixProviderTests 14 | { 15 | [TestMethod] 16 | [DataRow("\n"), DataRow("\r\n")] 17 | public async Task CommentIsAddedToBareItem(string lineEnding) 18 | { 19 | const string source = """ 20 | class C { 21 | void M(object? item) 22 | { 23 | item{|#0:!|}.ToString(); 24 | } 25 | } 26 | """; 27 | 28 | const string fixedSource = """ 29 | class C { 30 | void M(object? item) 31 | { 32 | // ! TODO: 33 | item!.ToString(); 34 | } 35 | } 36 | """; 37 | 38 | var expected = new[] 39 | { 40 | DiagnosticResult.CompilerWarning(NullForgivingDetectionAnalyzer.GeneralDiagnosticId).WithLocation(0) 41 | }; 42 | 43 | await VerifyCodeFixAsync(source.ReplaceLineEndings(lineEnding), expected, fixedSource.ReplaceLineEndings(lineEnding)); 44 | } 45 | 46 | [TestMethod] 47 | [DataRow("\n"), DataRow("\r\n")] 48 | public async Task CommentIsAddedToItemThatAlreadyHasAComment(string lineEnding) 49 | { 50 | const string source = """ 51 | class C { 52 | void M(object? item) 53 | { 54 | // Some other comment 55 | item{|#0:!|}.ToString(); 56 | } 57 | } 58 | """; 59 | 60 | const string fixedSource = """ 61 | class C { 62 | void M(object? item) 63 | { 64 | // Some other comment 65 | // ! TODO: 66 | item!.ToString(); 67 | } 68 | } 69 | """; 70 | 71 | var expected = new[] 72 | { 73 | DiagnosticResult.CompilerWarning(NullForgivingDetectionAnalyzer.GeneralDiagnosticId).WithLocation(0) 74 | }; 75 | 76 | await VerifyCodeFixAsync(source.ReplaceLineEndings(lineEnding), expected, fixedSource.ReplaceLineEndings(lineEnding)); 77 | } 78 | 79 | [TestMethod] 80 | public async Task CommentIsAddedToTopLevelStatement() 81 | { 82 | const string source = """ 83 | System.Console.WriteLine("Hello, World!".ToString(){|#0:!|}); 84 | """; 85 | 86 | const string fixedSource = """ 87 | // ! TODO: 88 | System.Console.WriteLine("Hello, World!".ToString()!); 89 | """; 90 | 91 | var expected = new[] 92 | { 93 | DiagnosticResult.CompilerWarning(NullForgivingDetectionAnalyzer.GeneralDiagnosticId).WithLocation(0) 94 | }; 95 | 96 | await VerifyCodeFixAsync(source, expected, fixedSource, outputKind: OutputKind.ConsoleApplication); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Analyzer/Nullable.Extended.Analyzer.Test/NullForgivingDetectionTests.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | using Microsoft.CodeAnalysis.Testing; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | using static Nullable.Extended.Analyzer.Test.Verifiers.CSharpAnalyzerVerifier; 7 | 8 | namespace Nullable.Extended.Analyzer.Test 9 | { 10 | [TestClass] 11 | public class NullForgivingDetectionTests 12 | { 13 | [TestMethod] 14 | public async Task FindNullForgivingOperator() 15 | { 16 | const string source = """ 17 | class C { 18 | void M(object? item) 19 | { 20 | item{|#0:!|}.ToString(); 21 | } 22 | } 23 | """; 24 | var expected = new[] 25 | { 26 | DiagnosticResult.CompilerWarning(NullForgivingDetectionAnalyzer.GeneralDiagnosticId).WithLocation(0) 27 | }; 28 | 29 | await VerifyAnalyzerAsync(source, expected); 30 | } 31 | 32 | [TestMethod] 33 | public async Task IgnoreJustifiedNullForgivingOperator() 34 | { 35 | const string source = """ 36 | class C { 37 | void M(object? item) 38 | { 39 | item{|#0:!|}.ToString(); 40 | // ! some justification text 41 | item{|#1:!|}.ToString(); 42 | } 43 | } 44 | """; 45 | 46 | var expected = new[] 47 | { 48 | DiagnosticResult.CompilerWarning(NullForgivingDetectionAnalyzer.GeneralDiagnosticId).WithLocation(0) 49 | }; 50 | 51 | await VerifyAnalyzerAsync(source, expected); 52 | } 53 | 54 | [TestMethod] 55 | public async Task NullForgivingDetectOnNullOrDefault() 56 | { 57 | const string source = """ 58 | class C { 59 | string M() 60 | { 61 | string item = null{|#0:!|}; 62 | item = default{|#1:!|}; 63 | item = default(string){|#2:!|}; 64 | return item; 65 | } 66 | } 67 | """; 68 | var expected = new[] 69 | { 70 | DiagnosticResult.CompilerWarning(NullForgivingDetectionAnalyzer.NullOrDefaultDiagnosticId).WithLocation(0), 71 | DiagnosticResult.CompilerWarning(NullForgivingDetectionAnalyzer.NullOrDefaultDiagnosticId).WithLocation(1), 72 | DiagnosticResult.CompilerWarning(NullForgivingDetectionAnalyzer.NullOrDefaultDiagnosticId).WithLocation(2), 73 | }; 74 | 75 | await VerifyAnalyzerAsync(source, expected); 76 | } 77 | 78 | [TestMethod] 79 | public async Task NullForgivingDetectOnNullOrDefaultOnInitOnlyProperty() 80 | { 81 | const string source = """ 82 | class C1 83 | { 84 | public string Id { get; init; } = null{|#0:!|}; 85 | } 86 | """; 87 | var expected = new[] 88 | { 89 | DiagnosticResult.CompilerWarning(NullForgivingDetectionAnalyzer.InitDiagnosticId).WithLocation(0), 90 | }; 91 | 92 | await VerifyAnalyzerAsync(source, expected); 93 | } 94 | 95 | [TestMethod] 96 | public async Task NullForgivingDetectInLambda() 97 | { 98 | const string source = """ 99 | using System.Collections.Generic; 100 | using System.Linq; 101 | 102 | class C { 103 | string M() 104 | { 105 | var x = Enumerable.Range(0, 10) 106 | .Select(i => ((object?)i.ToString(), (object?)i.ToString())) 107 | .Select(item => item.Item1{|#0:!|}.ToString() + item.Item2{|#1:!|}.ToString()) 108 | .FirstOrDefault(){|#2:!|}; 109 | 110 | return x{|#3:!|}; 111 | } 112 | } 113 | """; 114 | var expected = new[] 115 | { 116 | DiagnosticResult.CompilerWarning(NullForgivingDetectionAnalyzer.LambdaDiagnosticId).WithLocation(0), 117 | DiagnosticResult.CompilerWarning(NullForgivingDetectionAnalyzer.LambdaDiagnosticId).WithLocation(1), 118 | DiagnosticResult.CompilerWarning(NullForgivingDetectionAnalyzer.GeneralDiagnosticId).WithLocation(2), 119 | DiagnosticResult.CompilerWarning(NullForgivingDetectionAnalyzer.GeneralDiagnosticId).WithLocation(3), 120 | }; 121 | 122 | await VerifyAnalyzerAsync(source, expected); 123 | } 124 | 125 | [TestMethod] 126 | public async Task IgnoreJustifiedInWholeDeclaration() 127 | { 128 | const string source = """ 129 | using System.Collections.Generic; 130 | using System.Linq; 131 | 132 | class C { 133 | string M() 134 | { 135 | // ! Justifies the whole declaration 136 | var x = Enumerable.Range(0, 10) 137 | .Select(i => ((object?)i.ToString(), (object?)i.ToString())) 138 | .Select(item => item.Item1{|#0:!|}.ToString() + item.Item2{|#1:!|}.ToString()) 139 | .FirstOrDefault(){|#2:!|}; 140 | 141 | return x{|#3:!|}; 142 | } 143 | } 144 | """; 145 | var expected = new[] 146 | { 147 | DiagnosticResult.CompilerWarning(NullForgivingDetectionAnalyzer.GeneralDiagnosticId).WithLocation(3), 148 | }; 149 | 150 | await VerifyAnalyzerAsync(source, expected); 151 | } 152 | 153 | [TestMethod] 154 | public async Task JustificationOnPropertyAfterAttribute() 155 | { 156 | const string source = """ 157 | using System.Collections.Generic; 158 | using System.ComponentModel; 159 | using System.Linq; 160 | 161 | class C { 162 | [ReadOnly(true)] 163 | // ! Id is never null, always serialized 164 | public virtual string Id { get; set; } = null{|#0:!|}; 165 | } 166 | """; 167 | 168 | var expected = DiagnosticResult.EmptyDiagnosticResults; 169 | 170 | await VerifyAnalyzerAsync(source, expected); 171 | } 172 | 173 | [TestMethod] 174 | public async Task JustificationOnFieldAssignment() 175 | { 176 | const string source = """ 177 | using System.Collections.Generic; 178 | using System.ComponentModel; 179 | using System.Linq; 180 | 181 | class C { 182 | // ! Id is never null, always serialized 183 | public string _id = null{|#0:!|}; 184 | } 185 | """; 186 | 187 | var expected = DiagnosticResult.EmptyDiagnosticResults; 188 | 189 | await VerifyAnalyzerAsync(source, expected); 190 | } 191 | } 192 | 193 | #nullable enable 194 | class C1 195 | { 196 | public string Id { get; init; } = null!; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Analyzer/Nullable.Extended.Analyzer.Test/Nullable.Extended.Analyzer.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net60 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 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Analyzer/Nullable.Extended.Analyzer.Test/Verifiers/CSharpAnalyzerVerifier`1+Test.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp; 5 | using Microsoft.CodeAnalysis.CSharp.Testing; 6 | using Microsoft.CodeAnalysis.Diagnostics; 7 | using Microsoft.CodeAnalysis.Testing; 8 | 9 | namespace Nullable.Extended.Analyzer.Test.Verifiers 10 | { 11 | public static partial class CSharpAnalyzerVerifier 12 | where TAnalyzer : DiagnosticAnalyzer, new() 13 | { 14 | public class Test : CSharpAnalyzerTest 15 | { 16 | public Test(string source, ReferenceAssemblies? referenceAssemblies = null) 17 | { 18 | TestCode = source; 19 | ReferenceAssemblies = referenceAssemblies ?? ReferenceAssemblies.Net.Net60; 20 | } 21 | 22 | public bool ReportSuppressedDiagnostics { get; set; } 23 | 24 | protected override CompilationOptions CreateCompilationOptions() 25 | { 26 | var compilationOptions = (CSharpCompilationOptions)base.CreateCompilationOptions(); 27 | 28 | return compilationOptions 29 | .WithSpecificDiagnosticOptions(CSharpVerifierHelper.NullableWarnings) 30 | .WithGeneralDiagnosticOption(ReportDiagnostic.Error) 31 | .WithNullableContextOptions(NullableContextOptions.Enable); 32 | } 33 | 34 | protected override ParseOptions CreateParseOptions() 35 | { 36 | return new CSharpParseOptions(LanguageVersion.CSharp11, DocumentationMode.Diagnose); 37 | } 38 | 39 | protected override CompilationWithAnalyzers CreateCompilationWithAnalyzers(Compilation compilation, ImmutableArray analyzers, AnalyzerOptions options, CancellationToken cancellationToken) 40 | { 41 | return compilation.WithAnalyzers(analyzers, new CompilationWithAnalyzersOptions(options, null, true, false, ReportSuppressedDiagnostics)); 42 | } 43 | } 44 | } 45 | 46 | public static partial class CSharpSuppressorVerifier 47 | { 48 | public class Test : CSharpAnalyzerVerifier.Test 49 | { 50 | public Test(string source, ReferenceAssemblies? referenceAssemblies = null) 51 | : base(source, referenceAssemblies) 52 | { 53 | ReportSuppressedDiagnostics = true; 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Analyzer/Nullable.Extended.Analyzer.Test/Verifiers/CSharpAnalyzerVerifier`1.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.Diagnostics; 2 | using Microsoft.CodeAnalysis.Testing; 3 | 4 | namespace Nullable.Extended.Analyzer.Test.Verifiers 5 | { 6 | public static partial class CSharpAnalyzerVerifier 7 | where TAnalyzer : DiagnosticAnalyzer, new() 8 | { 9 | public static Task VerifyAnalyzerAsync(string source, IEnumerable diagnostics, ReferenceAssemblies? referenceAssemblies = null) 10 | { 11 | return VerifyAnalyzerAsync(new Test(source, referenceAssemblies), diagnostics); 12 | } 13 | 14 | public static async Task VerifyAnalyzerAsync(Test test, IEnumerable diagnostics) 15 | { 16 | test.ExpectedDiagnostics.AddRange(diagnostics); 17 | 18 | await test.RunAsync(CancellationToken.None); 19 | } 20 | } 21 | 22 | public static partial class CSharpSuppressorVerifier 23 | where TAnalyzer : DiagnosticSuppressor, new() 24 | { 25 | public static Task VerifyAnalyzerAsync(string source, IEnumerable diagnostics, ReferenceAssemblies? referenceAssemblies = null) 26 | { 27 | var test = new CSharpAnalyzerVerifier.Test(source, referenceAssemblies) 28 | { 29 | ReportSuppressedDiagnostics = true 30 | }; 31 | 32 | return CSharpAnalyzerVerifier.VerifyAnalyzerAsync(test, diagnostics); 33 | } 34 | 35 | public static Task VerifyAnalyzerAsync(CSharpAnalyzerVerifier.Test test, IEnumerable diagnostics) 36 | { 37 | return CSharpAnalyzerVerifier.VerifyAnalyzerAsync(test, diagnostics); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Analyzer/Nullable.Extended.Analyzer.Test/Verifiers/CSharpCodeFixVerifier`2+Test.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CodeFixes; 3 | using Microsoft.CodeAnalysis.CSharp; 4 | using Microsoft.CodeAnalysis.CSharp.Testing; 5 | using Microsoft.CodeAnalysis.Diagnostics; 6 | using Microsoft.CodeAnalysis.Testing; 7 | 8 | namespace Nullable.Extended.Analyzer.Test.Verifiers 9 | { 10 | public static partial class CSharpCodeFixVerifier 11 | where TAnalyzer : DiagnosticAnalyzer, new() 12 | where TCodeFix : CodeFixProvider, new() 13 | { 14 | public class Test : CSharpCodeFixTest 15 | { 16 | public Test(string testCode, string? fixedCode = null, ReferenceAssemblies? referenceAssemblies = null) 17 | { 18 | TestCode = testCode; 19 | FixedCode = fixedCode!; 20 | ReferenceAssemblies = referenceAssemblies ?? ReferenceAssemblies.Net.Net60; 21 | } 22 | 23 | protected override CompilationOptions CreateCompilationOptions() 24 | { 25 | var compilationOptions = (CSharpCompilationOptions)base.CreateCompilationOptions(); 26 | 27 | return compilationOptions 28 | .WithSpecificDiagnosticOptions(CSharpVerifierHelper.NullableWarnings) 29 | .WithGeneralDiagnosticOption(ReportDiagnostic.Error) 30 | .WithNullableContextOptions(NullableContextOptions.Enable); 31 | } 32 | 33 | protected override ParseOptions CreateParseOptions() 34 | { 35 | return new CSharpParseOptions(LanguageVersion.CSharp10, DocumentationMode.Diagnose); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Analyzer/Nullable.Extended.Analyzer.Test/Verifiers/CSharpCodeFixVerifier`2.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CodeFixes; 3 | using Microsoft.CodeAnalysis.Diagnostics; 4 | using Microsoft.CodeAnalysis.Testing; 5 | 6 | namespace Nullable.Extended.Analyzer.Test.Verifiers 7 | { 8 | public static partial class CSharpCodeFixVerifier 9 | where TAnalyzer : DiagnosticAnalyzer, new() 10 | where TCodeFix : CodeFixProvider, new() 11 | { 12 | /// 13 | public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) 14 | { 15 | var test = new Test(source); 16 | 17 | test.ExpectedDiagnostics.AddRange(expected); 18 | await test.RunAsync(CancellationToken.None); 19 | } 20 | 21 | /// 22 | public static async Task VerifyCodeFixAsync(string source, string? fixedSource, ReferenceAssemblies? referenceAssemblies = null) 23 | => await VerifyCodeFixAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource, referenceAssemblies); 24 | 25 | /// 26 | public static async Task VerifyCodeFixAsync(string source, DiagnosticResult expected, string? fixedSource, ReferenceAssemblies? referenceAssemblies = null) 27 | => await VerifyCodeFixAsync(source, new[] { expected }, fixedSource, referenceAssemblies); 28 | 29 | /// 30 | public static async Task VerifyCodeFixAsync(string source, IEnumerable expected, string? fixedSource, ReferenceAssemblies? referenceAssemblies = null, OutputKind? outputKind = null) 31 | { 32 | var test = new Test(source, fixedSource, referenceAssemblies) 33 | { 34 | TestState = { OutputKind = outputKind } 35 | }; 36 | 37 | test.ExpectedDiagnostics.AddRange(expected); 38 | 39 | await test.RunAsync(CancellationToken.None); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Analyzer/Nullable.Extended.Analyzer.Test/Verifiers/CSharpVerifierHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Reflection; 3 | 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CSharp; 6 | using Microsoft.CodeAnalysis.Testing; 7 | 8 | namespace Nullable.Extended.Analyzer.Test.Verifiers 9 | { 10 | internal static class CSharpVerifierHelper 11 | { 12 | /// 13 | /// By default, the compiler reports diagnostics for nullable reference types at 14 | /// , and the analyzer test framework defaults to only validating 15 | /// diagnostics at . This map contains all compiler diagnostic IDs 16 | /// related to nullability mapped to , which is then used to enable all 17 | /// of these warnings for default validation during analyzer and code fix tests. 18 | /// 19 | internal static ImmutableDictionary NullableWarnings { get; } = GetNullableWarningsFromCompiler(); 20 | 21 | private static ImmutableDictionary GetNullableWarningsFromCompiler() 22 | { 23 | string[] args = { "/warnaserror:nullable" }; 24 | var commandLineArguments = CSharpCommandLineParser.Default.Parse(args, baseDirectory: Environment.CurrentDirectory, sdkDirectory: Environment.CurrentDirectory); 25 | return commandLineArguments.CompilationOptions.SpecificDiagnosticOptions; 26 | } 27 | 28 | public static AnalyzerTest AddSolutionTransform(this AnalyzerTest test, Func transform) 29 | where TVerifier : IVerifier, new() 30 | { 31 | test.SolutionTransforms.Add((solution, projectId) => 32 | { 33 | var project = solution.GetProject(projectId); 34 | return project == null ? solution : transform(solution, project); 35 | }); 36 | 37 | return test; 38 | } 39 | 40 | public static AnalyzerTest AddReference(this AnalyzerTest test, params Assembly[] localReferences) 41 | where TVerifier : IVerifier, new() 42 | { 43 | test.AddSolutionTransform((solution, project) => 44 | { 45 | var localMetadataReferences = localReferences 46 | .Distinct() 47 | .Select(assembly => MetadataReference.CreateFromFile(assembly.Location)); 48 | 49 | solution = solution.WithProjectMetadataReferences(project.Id, project.MetadataReferences.Concat(localMetadataReferences)); 50 | 51 | return solution; 52 | }); 53 | 54 | return test; 55 | } 56 | 57 | public static AnalyzerTest AddPackages(this AnalyzerTest test, params PackageIdentity[] packages) 58 | where TVerifier : IVerifier, new() 59 | { 60 | test.ReferenceAssemblies = test.ReferenceAssemblies.WithPackages(packages.ToImmutableArray()); 61 | 62 | return test; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Analyzer/Nullable.Extended.Analyzer/Key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tom-englert/Nullable.Extended/cef7dd65770648240c1edc09597439b35e74d35b/src/Nullable.Extended.Analyzer/Nullable.Extended.Analyzer/Key.snk -------------------------------------------------------------------------------- /src/Nullable.Extended.Analyzer/Nullable.Extended.Analyzer/NullForgivingDetectionAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp; 5 | using Microsoft.CodeAnalysis.CSharp.Syntax; 6 | using Microsoft.CodeAnalysis.Diagnostics; 7 | 8 | using Nullable.Shared; 9 | 10 | namespace Nullable.Extended.Analyzer 11 | { 12 | #pragma warning disable RS1038 // Analyzer should be in a separate project 13 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 14 | public class NullForgivingDetectionAnalyzer : DiagnosticAnalyzer 15 | { 16 | public const string GeneralDiagnosticId = "NX0001"; 17 | public const string NullOrDefaultDiagnosticId = "NX0002"; 18 | public const string LambdaDiagnosticId = "NX0003"; 19 | public const string InitDiagnosticId = "NX0004"; 20 | 21 | private const string GeneralTitle = "Find general usages of the NullForgiving operator"; 22 | private const string NullOrDefaultTitle = "Find usages of the NullForgiving operator on null or default expression"; 23 | private const string LambdaTitle = "Find usages of the NullForgiving operator inside lambda expressions"; 24 | private const string InitTitle = "Find usages of the NullForgiving operator on null or default expression at init only property"; 25 | private const string MessageFormat = "Instance of NullForgiving operator without justification detected"; 26 | private const string Category = "nullable"; 27 | private const string HelpLink = "https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-forgiving"; 28 | 29 | private static readonly DiagnosticDescriptor GeneralRule = new(GeneralDiagnosticId, GeneralTitle, MessageFormat, Category, DiagnosticSeverity.Warning, true, null, HelpLink); 30 | private static readonly DiagnosticDescriptor NullOrDefaultRule = new(NullOrDefaultDiagnosticId, NullOrDefaultTitle, MessageFormat, Category, DiagnosticSeverity.Warning, true, null, HelpLink); 31 | private static readonly DiagnosticDescriptor LambdaRule = new(LambdaDiagnosticId, LambdaTitle, MessageFormat, Category, DiagnosticSeverity.Warning, true, null, HelpLink); 32 | private static readonly DiagnosticDescriptor InitRule = new(InitDiagnosticId, InitTitle, MessageFormat, Category, DiagnosticSeverity.Warning, true, null, HelpLink); 33 | 34 | public static ImmutableArray SupportedDiagnosticIds { get; } = ImmutableArray.Create(GeneralDiagnosticId, NullOrDefaultDiagnosticId, LambdaDiagnosticId); 35 | 36 | public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(GeneralRule, NullOrDefaultRule, LambdaRule, InitRule); 37 | 38 | public override void Initialize(AnalysisContext context) 39 | { 40 | context.RegisterSyntaxNodeAction(OnSuppressNullableWarningExpression, SyntaxKind.SuppressNullableWarningExpression); 41 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); 42 | context.EnableConcurrentExecution(); 43 | } 44 | 45 | private static void OnSuppressNullableWarningExpression(SyntaxNodeAnalysisContext context) 46 | { 47 | var node = (PostfixUnaryExpressionSyntax)context.Node; 48 | if (node.HasJustificationText()) 49 | return; 50 | 51 | context.ReportDiagnostic(Diagnostic.Create(GetDiagnosticDescriptor(node), node.GetDiagnosticLocation())); 52 | } 53 | 54 | private static DiagnosticDescriptor GetDiagnosticDescriptor(SyntaxNode node) 55 | { 56 | if (node.IsNullOrDefaultExpression(out var expression)) 57 | { 58 | return expression.IsInitOnlyPropertyAssignment() ? InitRule : NullOrDefaultRule; 59 | } 60 | 61 | if (node.IsInsideLambdaExpression()) 62 | { 63 | return LambdaRule; 64 | } 65 | 66 | return GeneralRule; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Analyzer/Nullable.Extended.Analyzer/NullForgivingDetectionAnalyzerCodeFixProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Composition; 3 | 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CodeActions; 6 | using Microsoft.CodeAnalysis.CodeFixes; 7 | using Microsoft.CodeAnalysis.CSharp; 8 | using Microsoft.CodeAnalysis.CSharp.Syntax; 9 | 10 | using Nullable.Shared; 11 | 12 | namespace Nullable.Extended.Analyzer 13 | { 14 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(NullForgivingDetectionAnalyzerCodeFixProvider)), Shared] 15 | public class NullForgivingDetectionAnalyzerCodeFixProvider : CodeFixProvider 16 | { 17 | private const string CodeFixPlaceholderText = Justification.SuppressionCommentPrefix + "TODO:"; 18 | private const string CodeFixTitle = "Suppress with comment"; 19 | 20 | private static readonly SyntaxTriviaList CodeFixPlaceholderTrivia = CSharpSyntaxTree.ParseText(CodeFixPlaceholderText).GetRoot().GetLeadingTrivia(); 21 | 22 | public sealed override ImmutableArray FixableDiagnosticIds => NullForgivingDetectionAnalyzer.SupportedDiagnosticIds; 23 | 24 | public override FixAllProvider? GetFixAllProvider() 25 | { 26 | // we should edit each comment individually 27 | return null; 28 | } 29 | 30 | public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) 31 | { 32 | var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); 33 | if (root == null) 34 | return; 35 | 36 | var diagnostic = context.Diagnostics.First(); 37 | var diagnosticSpan = diagnostic.Location.SourceSpan; 38 | var syntax = root.FindToken(diagnosticSpan.Start).Parent as PostfixUnaryExpressionSyntax; 39 | 40 | var target = syntax?.FindSuppressionCommentTarget(); 41 | if (target == null) 42 | return; 43 | 44 | var codeAction = CodeAction.Create(CodeFixTitle, token => ApplyFix(context.Document, root, target, token), CodeFixTitle); 45 | 46 | context.RegisterCodeFix(codeAction, diagnostic); 47 | } 48 | 49 | private async Task ApplyFix(Document document, SyntaxNode root, CSharpSyntaxNode targetNode, CancellationToken token) 50 | { 51 | var leadingTrivia = targetNode.GetLeadingTrivia(); 52 | var trailingTrivia = targetNode.GetTrailingTrivia(); 53 | var indent = leadingTrivia.LastOrDefault(item => item.IsKind(SyntaxKind.WhitespaceTrivia)); 54 | 55 | IEnumerable triviaList = [..leadingTrivia, ..trailingTrivia, SyntaxFactory.CarriageReturnLineFeed]; 56 | 57 | var newline = triviaList.First(trivia => trivia.IsKind(SyntaxKind.EndOfLineTrivia)); 58 | 59 | leadingTrivia = leadingTrivia.AddRange(CodeFixPlaceholderTrivia.Add(newline)); 60 | 61 | if (indent != default) 62 | { 63 | leadingTrivia = leadingTrivia.Add(indent); 64 | } 65 | 66 | var statement = targetNode.WithLeadingTrivia(leadingTrivia); 67 | 68 | root = root.ReplaceNode(targetNode, statement); 69 | 70 | document = document.WithSyntaxRoot(root); 71 | 72 | return await Task.FromResult(document); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Analyzer/Nullable.Extended.Analyzer/Nullable.Extended.Analyzer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | false 6 | 7 | 8 | *$(MSBuildProjectFullPath)* 9 | enable 10 | embedded 11 | true 12 | true 13 | Key.snk 14 | true 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | PreserveNewest 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Analyzer/Nullable.Extended.Analyzer/Nullable.Extended.Analyzer.targets: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31710.8 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nullable.Extended.Extension", "Nullable.Extended.Extension\Nullable.Extended.Extension.csproj", "{46A78692-0A62-4894-BD3F-84D8493C4093}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{384C3816-0901-42FB-A7A2-BB236A199CC3}" 9 | ProjectSection(SolutionItems) = preProject 10 | ..\.editorconfig = ..\.editorconfig 11 | ..\directory.build.props = ..\directory.build.props 12 | ..\..\README.md = ..\..\README.md 13 | ..\..\RelaseNotes.md = ..\..\RelaseNotes.md 14 | EndProjectSection 15 | EndProject 16 | Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Nullable.Shared", "..\Nullable.Shared\Nullable.Shared.shproj", "{850B0F8F-F2C0-4BE3-B141-2CD93EA896D8}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{B73304A2-3D19-4401-831E-3EFD053A8EA7}" 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample", "Sample\Sample.csproj", "{19CCBB5E-8425-4C0E-AE47-2F41D249FA45}" 21 | EndProject 22 | Global 23 | GlobalSection(SharedMSBuildProjectFiles) = preSolution 24 | ..\Nullable.Shared\Nullable.Shared.projitems*{46a78692-0a62-4894-bd3f-84d8493c4093}*SharedItemsImports = 5 25 | ..\Nullable.Shared\Nullable.Shared.projitems*{850b0f8f-f2c0-4be3-b141-2cd93ea896d8}*SharedItemsImports = 13 26 | EndGlobalSection 27 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 28 | Debug|Any CPU = Debug|Any CPU 29 | Release|Any CPU = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 32 | {46A78692-0A62-4894-BD3F-84D8493C4093}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {46A78692-0A62-4894-BD3F-84D8493C4093}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {46A78692-0A62-4894-BD3F-84D8493C4093}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {46A78692-0A62-4894-BD3F-84D8493C4093}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {B73304A2-3D19-4401-831E-3EFD053A8EA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {B73304A2-3D19-4401-831E-3EFD053A8EA7}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {B73304A2-3D19-4401-831E-3EFD053A8EA7}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {B73304A2-3D19-4401-831E-3EFD053A8EA7}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {19CCBB5E-8425-4C0E-AE47-2F41D249FA45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {19CCBB5E-8425-4C0E-AE47-2F41D249FA45}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {19CCBB5E-8425-4C0E-AE47-2F41D249FA45}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {19CCBB5E-8425-4C0E-AE47-2F41D249FA45}.Release|Any CPU.Build.0 = Release|Any CPU 44 | EndGlobalSection 45 | GlobalSection(SolutionProperties) = preSolution 46 | HideSolutionNode = FALSE 47 | EndGlobalSection 48 | GlobalSection(ExtensibilityGlobals) = postSolution 49 | SolutionGuid = {7C7C8ADB-9774-40E0-9344-6F77A22B28A9} 50 | EndGlobalSection 51 | EndGlobal 52 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | WARNING -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/AnalyzerFramework/AnalysisContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.Text; 3 | 4 | namespace Nullable.Extended.Extension.AnalyzerFramework 5 | { 6 | /// 7 | /// Contains the information about the context within which the syntax tree was analyzed. 8 | /// The syntax tree belongs to a document, which is contained within a project. 9 | /// 10 | public sealed class AnalysisContext 11 | { 12 | public AnalysisContext(Document document, SyntaxTree syntaxTree, SyntaxNode syntaxRoot) 13 | { 14 | Document = document; 15 | SyntaxTree = syntaxTree; 16 | SyntaxRoot = syntaxRoot; 17 | Text = syntaxRoot.GetText(); 18 | } 19 | 20 | public Document Document { get; } 21 | 22 | public SyntaxTree SyntaxTree { get; } 23 | 24 | public SyntaxNode SyntaxRoot { get; } 25 | 26 | public SourceText Text { get; set; } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/AnalyzerFramework/AnalysisResult.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.IO; 3 | using System.Runtime.CompilerServices; 4 | 5 | using Microsoft.CodeAnalysis; 6 | 7 | namespace Nullable.Extended.Extension.AnalyzerFramework 8 | { 9 | public abstract class AnalysisResult : IComparable, INotifyPropertyChanged 10 | { 11 | internal AnalysisResult(AnalysisContext analysisContext, SyntaxNode node, Location location) 12 | { 13 | Node = node; 14 | Location = location; 15 | AnalysisContext = analysisContext; 16 | Position = location.GetLineSpan(); 17 | } 18 | 19 | public AnalysisContext AnalysisContext { get; } 20 | 21 | public bool HasCompilationErrors { get; set; } 22 | 23 | // Project.Name may have target framework as suffix, Project.AssemblyName may contain full path and/or target framework 24 | // => use file name 25 | public string ProjectName => Path.GetFileNameWithoutExtension(AnalysisContext.Document.Project.FilePath); 26 | 27 | public string FilePath => AnalysisContext.SyntaxTree.FilePath; 28 | 29 | public string RelativeFilePath 30 | { 31 | get 32 | { 33 | var solutionPath = Path.GetDirectoryName(AnalysisContext.Document.Project.Solution.FilePath); 34 | var documentPath = FilePath; 35 | var solutionPathLength = solutionPath?.Length ?? 0; 36 | 37 | if (solutionPathLength > 0 && documentPath.StartsWith(solutionPath, StringComparison.OrdinalIgnoreCase)) 38 | { 39 | return documentPath.Substring(solutionPathLength); 40 | } 41 | 42 | return documentPath; 43 | } 44 | } 45 | 46 | public FileLinePositionSpan Position { get; } 47 | 48 | public int Line => Position.StartLinePosition.Line + 1; 49 | 50 | public int Column => Position.StartLinePosition.Character + 1; 51 | 52 | public SyntaxNode Node { get; } 53 | 54 | public Location Location { get; } 55 | 56 | public abstract int CompareTo(object? obj); 57 | 58 | public event PropertyChangedEventHandler? PropertyChanged; 59 | 60 | protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) 61 | { 62 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 63 | } 64 | 65 | public override string ToString() 66 | { 67 | return Location + ": " + Node; 68 | } 69 | } 70 | 71 | public abstract class AnalysisResult : AnalysisResult 72 | where T : SyntaxNode 73 | { 74 | internal AnalysisResult(AnalysisContext analysisContext, T node, Location location) 75 | : base(analysisContext, node, location) 76 | { 77 | } 78 | 79 | public new T Node => (T)base.Node; 80 | 81 | public abstract override int CompareTo(object? obj); 82 | } 83 | } -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/AnalyzerFramework/AnalysisResultsChangedArgs.cs: -------------------------------------------------------------------------------- 1 | namespace Nullable.Extended.Extension.AnalyzerFramework 2 | { 3 | internal class AnalysisResultsChangedArgs : EventArgs 4 | { 5 | public AnalysisResultsChangedArgs(IReadOnlyList results) 6 | { 7 | Results = results; 8 | } 9 | 10 | public IReadOnlyList Results { get; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/AnalyzerFramework/AnalyzerEngine.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Composition; 3 | 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.Diagnostics; 6 | 7 | using TomsToolbox.Composition; 8 | 9 | namespace Nullable.Extended.Extension.AnalyzerFramework 10 | { 11 | [Export(typeof(IAnalyzerEngine))] 12 | internal class AnalyzerEngine : IAnalyzerEngine 13 | { 14 | private readonly ICollection _syntaxTreeAnalyzers; 15 | private readonly ICollection _postProcessors; 16 | 17 | public AnalyzerEngine(IExportProvider exportProvider) 18 | { 19 | _syntaxTreeAnalyzers = exportProvider.GetExportedValues().ToArray(); 20 | _postProcessors = exportProvider.GetExportedValues().ToArray(); 21 | } 22 | 23 | public async Task> AnalyzeAsync(IEnumerable documents, CancellationToken cancellationToken) 24 | { 25 | var documentTasks = documents.Select(doc => AnalyzeDocumentAsync(doc, cancellationToken)); 26 | 27 | var analysisResults = (await Task.WhenAll(documentTasks)) 28 | .SelectMany(r => r) 29 | .ToList() 30 | .AsReadOnly(); 31 | 32 | var resultsByProject = analysisResults.GroupBy(result => result.AnalysisContext.Document.Project); 33 | 34 | var projectTasks = resultsByProject.Select(results => PostProcessProjectAsync(results, cancellationToken)); 35 | 36 | await Task.WhenAll(projectTasks); 37 | 38 | analysisResults = analysisResults 39 | .GroupBy(r => r.Position) 40 | .Select(g => g.OrderBy(r => r).First()) 41 | .ToList() 42 | .AsReadOnly(); 43 | 44 | return analysisResults; 45 | } 46 | 47 | private Task PostProcessProjectAsync(IGrouping analysisResults, CancellationToken cancellationToken) 48 | { 49 | return Task.Run(async () => 50 | { 51 | if (!_postProcessors.Any()) 52 | return; 53 | 54 | try 55 | { 56 | var project = analysisResults.Key; 57 | 58 | IReadOnlyCollection results = analysisResults.ToArray(); 59 | 60 | var analyzers = project.AnalyzerReferences 61 | .SelectMany(r => r.GetAnalyzers(LanguageNames.CSharp)) 62 | .OfType() 63 | .Cast() 64 | .ToImmutableArray(); 65 | 66 | async Task> GetDiagnosticsAsync(Compilation compilation) 67 | { 68 | return analyzers.Any() 69 | ? await compilation.WithAnalyzers(analyzers).GetAllDiagnosticsAsync(cancellationToken) 70 | : compilation.GetDiagnostics(); 71 | } 72 | 73 | var compilation = await project.GetCompilationAsync(cancellationToken) ?? throw new InvalidOperationException("Error getting compilation of project"); 74 | var diagnostics = await GetDiagnosticsAsync(compilation); 75 | var errors = diagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error 76 | && !diagnostic.IsSuppressed 77 | && !_postProcessors.Any(processor => processor.IsSpecificDiagnostic(diagnostic, results))) 78 | .ToImmutableArray(); 79 | 80 | var diagnosticLocations = new HashSet(diagnostics.Select(d => d.Location.GetLineSpan())); 81 | 82 | foreach (var resultsByContext in results.GroupBy(r => r.AnalysisContext)) 83 | { 84 | try 85 | { 86 | var context = resultsByContext.Key; 87 | var document = context.Document; 88 | var filePath = document.FilePath; 89 | var syntaxRoot = context.SyntaxRoot; 90 | 91 | if (errors.Any(diagnostic => string.Equals(diagnostic.Location.GetLineSpan().Path, filePath, StringComparison.OrdinalIgnoreCase))) 92 | throw new InvalidOperationException("Document has errors"); 93 | 94 | foreach (var analyzer in _postProcessors) 95 | { 96 | await analyzer.PostProcessAsync(project, document, syntaxRoot, diagnosticLocations, GetDiagnosticsAsync, resultsByContext, cancellationToken); 97 | } 98 | } 99 | catch 100 | { 101 | foreach (var result in resultsByContext) 102 | { 103 | result.HasCompilationErrors = true; 104 | } 105 | } 106 | } 107 | } 108 | catch 109 | { 110 | foreach (var result in analysisResults) 111 | { 112 | result.HasCompilationErrors = true; 113 | } 114 | } 115 | }, cancellationToken); 116 | } 117 | 118 | private Task> AnalyzeDocumentAsync(Document document, CancellationToken cancellationToken) 119 | { 120 | return Task.Run(async () => 121 | { 122 | var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken); 123 | if (syntaxTree == null) 124 | return (IReadOnlyCollection)Array.Empty(); 125 | 126 | var syntaxRoot = await syntaxTree.GetRootAsync(cancellationToken); 127 | if (syntaxRoot.BeginsWithAutoGeneratedComment()) 128 | return Array.Empty(); 129 | 130 | var analysisContext = new AnalysisContext(document, syntaxTree, syntaxRoot); 131 | 132 | var tasks = _syntaxTreeAnalyzers 133 | .Select(analyzer => analyzer.AnalyzeAsync(analysisContext, cancellationToken)); 134 | 135 | var results = await Task.WhenAll(tasks); 136 | 137 | return results 138 | .SelectMany(r => r) 139 | .ToArray(); 140 | }, cancellationToken); 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/AnalyzerFramework/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp; 5 | 6 | using TomsToolbox.Essentials; 7 | 8 | using Project = Microsoft.CodeAnalysis.Project; 9 | using Solution = Microsoft.CodeAnalysis.Solution; 10 | 11 | namespace Nullable.Extended.Extension.AnalyzerFramework 12 | { 13 | public static class ExtensionMethods 14 | { 15 | private static readonly char[] Separators = [Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar, Path.VolumeSeparatorChar]; 16 | 17 | private const string AutoGeneratedTag = ""; 18 | private static readonly int AutoGeneratedCommentMinLength = AutoGeneratedTag.Length + 2; 19 | 20 | private static bool IsGenerated(this Document document) 21 | { 22 | return IsGeneratedFile(document.FilePath); 23 | } 24 | 25 | private static bool IsGeneratedAssemblyInfo(this Document document) 26 | { 27 | var filePath = document.FilePath; 28 | 29 | if (filePath.IsNullOrWhiteSpace()) 30 | return false; 31 | 32 | // Generated AssemblyInfo files have the name of the form: 33 | // .AssemblyInfo.cs 34 | // In addition, the file has to be in the obj output folder 35 | // but we will ignore that check at the moment. 36 | // It is enough to insist on the expected file name form. 37 | 38 | if (!filePath.EndsWith(".AssemblyInfo.cs")) 39 | return false; 40 | 41 | var directorySeparatorIndex = filePath.LastIndexOfAny(Separators); 42 | 43 | return string.Compare(document.Project.Name, 0, filePath, directorySeparatorIndex + 1, 44 | document.Project.Name.Length, StringComparison.OrdinalIgnoreCase) == 0; 45 | } 46 | 47 | // The implementation has been adapted from Josef Pihrt's Roslynator: 48 | // https://github.com/JosefPihrt/Roslynator/blob/a6ed824a390831fa67e0dbb3710418239654a88e/src/CSharp/GeneratedCodeUtility.cs#L1 49 | private static bool IsGeneratedFile(string? filePath) 50 | { 51 | if (filePath.IsNullOrWhiteSpace()) return false; 52 | 53 | var directorySeparatorIndex = filePath.LastIndexOfAny(Separators); 54 | 55 | if (string.Compare("TemporaryGeneratedFile_", 0, filePath, directorySeparatorIndex + 1, 56 | "TemporaryGeneratedFile_".Length, StringComparison.OrdinalIgnoreCase) == 0) 57 | return true; 58 | 59 | var dotIndex = filePath.LastIndexOf(".", filePath.Length - 1, filePath.Length - directorySeparatorIndex - 1, 60 | StringComparison.Ordinal); 61 | 62 | if (dotIndex == -1) 63 | return false; 64 | 65 | return IsMatch(".Designer") 66 | || IsMatch(".Generated") 67 | || IsMatch(".g") 68 | || IsMatch(".g.i") 69 | || IsMatch(".AssemblyAttributes"); 70 | 71 | bool IsMatch(string value) 72 | { 73 | var length = value.Length; 74 | 75 | var index = dotIndex - length; 76 | 77 | return index >= 0 78 | && string.Compare(value, 0, filePath, index, length, StringComparison.OrdinalIgnoreCase) == 0; 79 | } 80 | } 81 | 82 | // The implementation has been adapted from Josef Pihrt's Roslynator: 83 | // https://github.com/JosefPihrt/Roslynator/blob/b2c2493c880ccd06215a11fa7b42b64a1fea0470/src/CSharp/CSharp/CSharpGeneratedCodeAnalyzer.cs#L21 84 | internal static bool BeginsWithAutoGeneratedComment(this SyntaxNode syntaxRoot) 85 | { 86 | var leadingTrivia = syntaxRoot.GetLeadingTrivia(); 87 | 88 | if (!leadingTrivia.Any()) return false; 89 | 90 | foreach (var trivia in leadingTrivia) 91 | { 92 | if (!trivia.IsKind(SyntaxKind.SingleLineCommentTrivia)) 93 | continue; 94 | 95 | var text = trivia.ToString(); 96 | 97 | if (text.Length < AutoGeneratedCommentMinLength 98 | || text[0] != '/' 99 | || text[1] != '/') 100 | continue; 101 | 102 | var index = 2; 103 | 104 | while (index < text.Length 105 | && char.IsWhiteSpace(text[index])) 106 | { 107 | index++; 108 | } 109 | 110 | if (string.Compare(text, index, AutoGeneratedTag, 0, AutoGeneratedTag.Length, 111 | StringComparison.OrdinalIgnoreCase) == 0) 112 | return true; 113 | } 114 | 115 | return false; 116 | } 117 | 118 | private static bool IsCSharpProject(this Project project) 119 | { 120 | return project.Language == "C#"; 121 | } 122 | 123 | public static bool ShouldBeAnalyzed(this Document document) 124 | { 125 | return document.SupportsSyntaxTree && 126 | document.SupportsSemanticModel && 127 | !document.IsGenerated() && 128 | !document.IsGeneratedAssemblyInfo(); 129 | } 130 | 131 | public static IEnumerable GetDocumentsToAnalyze(this Solution solution) 132 | { 133 | return solution 134 | .Projects 135 | .Where(project => project.IsCSharpProject()) 136 | .SelectMany(project => project.GetDocumentsToAnalyze()); 137 | } 138 | 139 | private static IEnumerable GetDocumentsToAnalyze(this Project project) 140 | { 141 | return project.Documents.Where(document => document.ShouldBeAnalyzed()); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/AnalyzerFramework/IAnalyzerEngine.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace Nullable.Extended.Extension.AnalyzerFramework 4 | { 5 | public interface IAnalyzerEngine 6 | { 7 | Task> AnalyzeAsync(IEnumerable documents, CancellationToken cancellationToken); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/AnalyzerFramework/ISyntaxAnalysisPostProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | 3 | using Microsoft.CodeAnalysis; 4 | 5 | namespace Nullable.Extended.Extension.AnalyzerFramework 6 | { 7 | internal interface ISyntaxAnalysisPostProcessor 8 | { 9 | Task PostProcessAsync(Project project, Document document, SyntaxNode syntaxRoot, ICollection diagnosticLocations, 10 | Func>> getDiagnosticsAsync, 11 | IEnumerable analysisResults, CancellationToken cancellationToken); 12 | 13 | bool IsSpecificDiagnostic(Diagnostic diagnostic, IReadOnlyCollection results); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/AnalyzerFramework/ISyntaxTreeAnalyzer.cs: -------------------------------------------------------------------------------- 1 | namespace Nullable.Extended.Extension.AnalyzerFramework 2 | { 3 | internal interface ISyntaxTreeAnalyzer 4 | { 5 | Task> AnalyzeAsync(AnalysisContext analysisContext, CancellationToken cancellationToken); 6 | } 7 | } -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/Extension/ExtensionPackage.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | using Microsoft.VisualStudio.Shell; 4 | 5 | using Ninject; 6 | 7 | using Nullable.Extended.Extension.Views; 8 | 9 | using TomsToolbox.Composition; 10 | using TomsToolbox.Composition.Ninject; 11 | 12 | using Task = System.Threading.Tasks.Task; 13 | 14 | namespace Nullable.Extended.Extension.Extension 15 | { 16 | [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] 17 | [Guid(PackageGuidString)] 18 | [InstalledProductRegistration(@"Nullable Extended Extension", @"Tools to keep your nullability annotations lean and mean.", "Nullable.Extended")] 19 | [ProvideMenuResource("Menus.ctmenu", 1)] 20 | [ProvideToolWindow(typeof(NullForgivingToolWindow))] 21 | public sealed class ExtensionPackage : AsyncPackage 22 | { 23 | public const string PackageGuidString = "e8b6cb89-75cb-433f-a8d9-52719840e6fe"; 24 | 25 | private readonly IKernel _kernel = new StandardKernel(); 26 | 27 | public ExtensionPackage() 28 | { 29 | ExportProvider = new ExportProvider(_kernel); 30 | } 31 | 32 | protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) 33 | { 34 | _kernel.BindExports(GetType().Assembly); 35 | _kernel.Bind().ToConstant(ExportProvider); 36 | _kernel.Bind().ToConstant(this); 37 | 38 | await OpenToolWindowCommand.InitializeAsync(this); 39 | } 40 | 41 | public IExportProvider ExportProvider { get; } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/Extension/OpenToolWindowCommand.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.Design; 2 | 3 | using Microsoft.VisualStudio.Shell; 4 | using Microsoft.VisualStudio.Shell.Interop; 5 | 6 | using Nullable.Extended.Extension.Views; 7 | 8 | using Task = System.Threading.Tasks.Task; 9 | 10 | namespace Nullable.Extended.Extension.Extension 11 | { 12 | /// 13 | /// Command handler 14 | /// 15 | internal sealed class OpenToolWindowCommand 16 | { 17 | /// 18 | /// Command ID. 19 | /// 20 | public const int CommandId = 0x0100; 21 | 22 | /// 23 | /// Command menu group (command set GUID). 24 | /// 25 | public static readonly Guid CommandSet = new("64068b4a-85d8-46a8-adb6-78dc86efbd95"); 26 | 27 | /// 28 | /// VS Package that provides this command, not null. 29 | /// 30 | private readonly AsyncPackage _package; 31 | 32 | /// 33 | /// Initializes a new instance of the class. 34 | /// Adds our command handlers for menu (commands must exist in the command table file) 35 | /// 36 | /// Owner package, not null. 37 | /// Command service to add command to, not null. 38 | private OpenToolWindowCommand(AsyncPackage package, OleMenuCommandService commandService) 39 | { 40 | _package = package ?? throw new ArgumentNullException(nameof(package)); 41 | commandService = commandService ?? throw new ArgumentNullException(nameof(commandService)); 42 | 43 | var menuCommandID = new CommandID(CommandSet, CommandId); 44 | var menuItem = new MenuCommand(Execute, menuCommandID); 45 | commandService.AddCommand(menuItem); 46 | } 47 | 48 | /// 49 | /// Gets the instance of the command. 50 | /// 51 | public static OpenToolWindowCommand? Instance 52 | { 53 | get; 54 | private set; 55 | } 56 | 57 | /// 58 | /// Initializes the singleton instance of the command. 59 | /// 60 | /// Owner package, not null. 61 | public static async Task InitializeAsync(AsyncPackage package) 62 | { 63 | // Switch to the main thread - the call to AddCommand in OpenToolWindowCommand's constructor requires 64 | // the UI thread. 65 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken); 66 | 67 | var commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService; 68 | Instance = new OpenToolWindowCommand(package, commandService!); 69 | } 70 | 71 | /// 72 | /// This function is the callback used to execute the command when the menu item is clicked. 73 | /// See the constructor to see how the menu item is associated with this function using 74 | /// OleMenuCommandService service and MenuCommand class. 75 | /// 76 | /// Event sender. 77 | /// Event args. 78 | private void Execute(object sender, EventArgs e) 79 | { 80 | ThreadHelper.ThrowIfNotOnUIThread(); 81 | 82 | // Get the instance number 0 of this tool window. This window is single instance so this instance 83 | // is actually the only one. 84 | // The last flag is set to true so that if the tool window does not exists it will be created. 85 | var window = _package.FindToolWindow(typeof(NullForgivingToolWindow), 0, true); 86 | if (window?.Frame == null) 87 | { 88 | throw new NotSupportedException("Cannot create tool window"); 89 | } 90 | 91 | var windowFrame = (IVsWindowFrame)window.Frame; 92 | Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(windowFrame.Show()); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/Extension/ProvideToolboxControlAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Runtime.InteropServices; 3 | 4 | using Microsoft.VisualStudio.Shell; 5 | 6 | namespace Nullable.Extended.Extension.Extension 7 | { 8 | /// 9 | /// This attribute adds a ToolboxControlsInstaller key for the assembly to install toolbox controls from the assembly. 10 | /// 11 | /// 12 | /// For example 13 | /// [$(Rootkey)\ToolboxControlsInstaller\$FullAssemblyName$] 14 | /// "Codebase"="$path$" 15 | /// "WpfControls"="1" 16 | /// 17 | [AttributeUsage(AttributeTargets.Class)] 18 | [ComVisible(false)] 19 | public sealed class ProvideToolboxControlAttribute : RegistrationAttribute 20 | { 21 | private const string ToolboxControlsInstallerPath = "ToolboxControlsInstaller"; 22 | private readonly string name; 23 | 24 | /// 25 | /// Creates a new ProvideToolboxControl attribute to register the assembly for toolbox controls installer. 26 | /// 27 | /// The name of the toolbox controls. 28 | public ProvideToolboxControlAttribute(string name) 29 | { 30 | this.name = name; 31 | } 32 | 33 | /// 34 | /// Called to register this attribute with the given context. The context 35 | /// contains the location where the registration information should be placed. 36 | /// It also contains other information such as the type being registered and path information. 37 | /// 38 | /// Given context to register in. 39 | public override void Register(RegistrationContext context) 40 | { 41 | if (context == null) 42 | { 43 | throw new ArgumentNullException(nameof(context)); 44 | } 45 | 46 | using var key = context.CreateKey(string.Format(CultureInfo.InvariantCulture, "{0}\\{1}", ToolboxControlsInstallerPath, context.ComponentType.Assembly.FullName)); 47 | key.SetValue(string.Empty, name); 48 | key.SetValue("Codebase", context.CodeBase); 49 | key.SetValue("WPFControls", "1"); 50 | } 51 | 52 | /// 53 | /// Called to unregister this attribute with the given context. 54 | /// 55 | /// A registration context provided by an external registration tool. 56 | /// The context can be used to remove registry keys, log registration activity, and obtain information 57 | /// about the component being registered. 58 | public override void Unregister(RegistrationContext? context) 59 | { 60 | context?.RemoveKey(string.Format(CultureInfo.InvariantCulture, "{0}\\{1}", 61 | ToolboxControlsInstallerPath, 62 | context.ComponentType.Assembly.FullName)); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/FodyWeavers.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/FodyWeavers.xsd: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Used to control if the On_PropertyName_Changed feature is enabled. 13 | 14 | 15 | 16 | 17 | Used to control if the Dependent properties feature is enabled. 18 | 19 | 20 | 21 | 22 | Used to control if the IsChanged property feature is enabled. 23 | 24 | 25 | 26 | 27 | Used to change the name of the method that fires the notify event. This is a string that accepts multiple values in a comma separated form. 28 | 29 | 30 | 31 | 32 | Used to control if equality checks should be inserted. If false, equality checking will be disabled for the project. 33 | 34 | 35 | 36 | 37 | Used to control if equality checks should use the Equals method resolved from the base class. 38 | 39 | 40 | 41 | 42 | Used to control if equality checks should use the static Equals method resolved from the base class. 43 | 44 | 45 | 46 | 47 | Used to turn off build warnings from this weaver. 48 | 49 | 50 | 51 | 52 | Used to turn off build warnings about mismatched On_PropertyName_Changed methods. 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. 66 | 67 | 68 | 69 | 70 | A comma-separated list of error codes that can be safely ignored in assembly verification. 71 | 72 | 73 | 74 | 75 | 'false' to turn off automatic generation of the XML Schema file. 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/NullForgivingAnalyzer/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp; 5 | using Microsoft.CodeAnalysis.CSharp.Syntax; 6 | 7 | using Nullable.Shared; 8 | 9 | namespace Nullable.Extended.Extension.NullForgivingAnalyzer 10 | { 11 | public static class ExtensionMethods 12 | { 13 | public static string ReplaceNullForgivingToken(this string value) 14 | { 15 | var index = value.LastIndexOf('!'); 16 | return new StringBuilder(value) { [index] = ' ' }.ToString(); 17 | } 18 | 19 | public static NullForgivingContext GetContext(this PostfixUnaryExpressionSyntax node) 20 | { 21 | if (node.IsNullOrDefaultExpression(out var expression)) 22 | { 23 | return expression.IsInitOnlyPropertyAssignment() ? NullForgivingContext.Init : NullForgivingContext.NullOrDefault; 24 | } 25 | 26 | if (node.IsInsideLambdaExpression()) 27 | { 28 | return NullForgivingContext.Lambda; 29 | } 30 | 31 | return NullForgivingContext.General; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/NullForgivingAnalyzer/NullForgivingAnalysisResult.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.CSharp.Syntax; 2 | 3 | using Nullable.Extended.Extension.AnalyzerFramework; 4 | using Nullable.Shared; 5 | 6 | using TomsToolbox.Essentials; 7 | 8 | namespace Nullable.Extended.Extension.NullForgivingAnalyzer 9 | { 10 | public class NullForgivingAnalysisResult : AnalysisResult, IComparable 11 | { 12 | public NullForgivingAnalysisResult(AnalysisContext analysisContext, PostfixUnaryExpressionSyntax node, NullForgivingContext context) 13 | : base(analysisContext, node, node.OperatorToken.GetLocation()) 14 | { 15 | Context = context; 16 | Justification = node.GetJustificationText(); 17 | } 18 | 19 | public string? Justification { get; } 20 | 21 | public bool IsJustified => !Justification.IsNullOrEmpty(); 22 | 23 | public NullForgivingContext Context { get; set; } 24 | 25 | public bool? IsRequired { get; set; } 26 | 27 | public override int CompareTo(object? other) 28 | { 29 | return CompareTo(other as NullForgivingAnalysisResult); 30 | } 31 | 32 | public int CompareTo(NullForgivingAnalysisResult? other) 33 | { 34 | if (other is null) 35 | return 1; 36 | 37 | return GetSeverity(IsRequired) - GetSeverity(other.IsRequired); 38 | } 39 | 40 | private static int GetSeverity(bool? value) 41 | { 42 | return value switch 43 | { 44 | null => 0, 45 | true => 1, 46 | false => 2 47 | }; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/NullForgivingAnalyzer/NullForgivingContext.cs: -------------------------------------------------------------------------------- 1 | namespace Nullable.Extended.Extension.NullForgivingAnalyzer 2 | { 3 | public enum NullForgivingContext 4 | { 5 | Invalid, 6 | General, 7 | NullOrDefault, 8 | Lambda, 9 | Init 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/NullForgivingAnalyzer/NullForgivingDetectionAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System.Composition; 2 | 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp; 5 | using Microsoft.CodeAnalysis.CSharp.Syntax; 6 | 7 | using Nullable.Extended.Extension.AnalyzerFramework; 8 | 9 | namespace Nullable.Extended.Extension.NullForgivingAnalyzer 10 | { 11 | [Export(typeof(ISyntaxTreeAnalyzer))] 12 | internal class NullForgivingDetectionAnalyzer : ISyntaxTreeAnalyzer 13 | { 14 | public async Task> AnalyzeAsync(AnalysisContext analysisContext, CancellationToken cancellationToken) 15 | { 16 | var root = analysisContext.SyntaxRoot; 17 | 18 | var items = root 19 | .DescendantNodesAndSelf() 20 | .OfType() 21 | .Where(node => node.IsKind(SyntaxKind.SuppressNullableWarningExpression)) 22 | .Select(item => MapResult(analysisContext, item)) 23 | .ToArray(); 24 | 25 | return await Task.FromResult(items); 26 | } 27 | 28 | private static AnalysisResult MapResult(AnalysisContext analysisContext, PostfixUnaryExpressionSyntax node) 29 | { 30 | return new NullForgivingAnalysisResult(analysisContext, node, node.GetContext()); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/NullForgivingAnalyzer/NullForgivingDetectionPostProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Composition; 3 | 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CSharp; 6 | 7 | using Nullable.Extended.Extension.AnalyzerFramework; 8 | 9 | namespace Nullable.Extended.Extension.NullForgivingAnalyzer 10 | { 11 | [Export(typeof(ISyntaxAnalysisPostProcessor))] 12 | internal class NullForgivingDetectionPostProcessor : ISyntaxAnalysisPostProcessor 13 | { 14 | private const string FirstNullableDiagnostic = "CS8600"; 15 | private const string LastNullableDiagnostic = "CS8900"; 16 | 17 | public async Task PostProcessAsync(Project project, Document document, SyntaxNode syntaxRoot, 18 | ICollection diagnosticLocations, 19 | Func>> getDiagnosticsAsync, 20 | IEnumerable analysisResults, 21 | CancellationToken cancellationToken) 22 | { 23 | var nullForgivingAnalysisResults = analysisResults 24 | .OfType() 25 | .ToList() 26 | .AsReadOnly(); 27 | 28 | try 29 | { 30 | foreach (var analysisResult in nullForgivingAnalysisResults) 31 | { 32 | var node = analysisResult.Node; 33 | var rewrittenSyntaxRoot = syntaxRoot.ReplaceNode(node, RewriteNullForgivingNode(node)); 34 | // var sourceCode = rewrittenSyntaxRoot.GetText().ToString(); 35 | var compilation = await project 36 | .RemoveDocument(document.Id) 37 | .AddDocument(document.Name, rewrittenSyntaxRoot, document.Folders, document.FilePath) 38 | .Project 39 | .GetCompilationAsync(cancellationToken) ?? throw new InvalidOperationException("Error getting compilation of project"); 40 | 41 | var allDiagnostics = await getDiagnosticsAsync(compilation); 42 | 43 | bool IsNewDiagnosticInCurrentDocument(Diagnostic d) 44 | { 45 | var span = d.Location.GetLineSpan(); 46 | return span.Path == document.FilePath && !diagnosticLocations.Contains(span); 47 | } 48 | 49 | var newNullableDiagnostics = allDiagnostics 50 | .Where(d => !d.IsSuppressed) 51 | .Where(IsNullableDiagnostic) 52 | .Where(IsNewDiagnosticInCurrentDocument); 53 | 54 | analysisResult.IsRequired = newNullableDiagnostics.Any(); 55 | } 56 | } 57 | catch (InvalidOperationException) 58 | { 59 | SetAllInvalid(nullForgivingAnalysisResults); 60 | } 61 | } 62 | 63 | public bool IsSpecificDiagnostic(Diagnostic diagnostic, IReadOnlyCollection results) 64 | { 65 | return IsNullableDiagnostic(diagnostic); 66 | } 67 | 68 | private static bool IsNullableDiagnostic(Diagnostic d) 69 | { 70 | return IsNullableDiagnosticId(d.Id); 71 | } 72 | 73 | private static bool IsNullableDiagnosticId(string id) 74 | { 75 | return string.Compare(id, FirstNullableDiagnostic, StringComparison.OrdinalIgnoreCase) >= 0 76 | && string.Compare(id, LastNullableDiagnostic, StringComparison.OrdinalIgnoreCase) <= 0; 77 | } 78 | 79 | private static void SetAllInvalid(IEnumerable items) 80 | { 81 | foreach (var item in items) 82 | { 83 | item.Context = NullForgivingContext.Invalid; 84 | } 85 | } 86 | 87 | private static SyntaxNode RewriteNullForgivingNode(SyntaxNode n) 88 | { 89 | var sourceCode = n.ToFullString().ReplaceNullForgivingToken(); 90 | return SyntaxFactory.ParseExpression(sourceCode); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/Nullable.Extended.Extension.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | true 4 | net48 5 | 16.0 6 | win 7 | MSB3277 8 | 9 | 10 | Program 11 | $(DevEnvDir)devenv.exe 12 | /rootsuffix Exp 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | LICENSE.txt 44 | Always 45 | true 46 | 47 | 48 | package_icon.png 49 | Always 50 | true 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/Package.vsct: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.Shell; 2 | 3 | [assembly: ProvideCodeBase(CodeBase = "$PackageFolder$\\Ninject.dll")] 4 | [assembly: ProvideCodeBase(CodeBase = "$PackageFolder$\\TomsToolbox.Composition.dll")] 5 | [assembly: ProvideCodeBase(CodeBase = "$PackageFolder$\\TomsToolbox.Composition.Ninject.dll")] 6 | [assembly: ProvideCodeBase(CodeBase = "$PackageFolder$\\TomsToolbox.Essentials.dll")] 7 | [assembly: ProvideCodeBase(CodeBase = "$PackageFolder$\\TomsToolbox.ObservableCollections.dll")] 8 | [assembly: ProvideCodeBase(CodeBase = "$PackageFolder$\\TomsToolbox.Wpf.Composition.dll")] 9 | [assembly: ProvideCodeBase(CodeBase = "$PackageFolder$\\TomsToolbox.Wpf.Composition.AttributedModel.dll")] 10 | [assembly: ProvideCodeBase(CodeBase = "$PackageFolder$\\TomsToolbox.Wpf.dll")] 11 | [assembly: ProvideCodeBase(CodeBase = "$PackageFolder$\\TomsToolbox.Wpf.Styles.dll")] 12 | [assembly: ProvideCodeBase(CodeBase = "$PackageFolder$\\Microsoft.Xaml.Behaviors.dll")] 13 | [assembly: ProvideCodeBase(CodeBase = "$PackageFolder$\\DataGridExtensions.dll")] -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/Resources/Icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tom-englert/Nullable.Extended/cef7dd65770648240c1edc09597439b35e74d35b/src/Nullable.Extended.Extension/Nullable.Extended.Extension/Resources/Icons.png -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/Resources/VSColorScheme.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 19 | 21 | 22 | 24 | 26 | 27 | 29 | 31 | 32 | 34 | 36 | 38 | 39 | 41 | 43 | 44 | 46 | 48 | 50 | 51 | 53 | 55 | 57 | 58 | 60 | 62 | 63 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/Resources/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tom-englert/Nullable.Extended/cef7dd65770648240c1edc09597439b35e74d35b/src/Nullable.Extended.Extension/Nullable.Extended.Extension/Resources/delete.png -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/Resources/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tom-englert/Nullable.Extended/cef7dd65770648240c1edc09597439b35e74d35b/src/Nullable.Extended.Extension/Nullable.Extended.Extension/Resources/refresh.png -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/VSPackage.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | Resources\Icons.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 123 | 124 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/Views/AnalyzerResultViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Runtime.CompilerServices; 3 | using System.Windows.Input; 4 | 5 | using Microsoft.VisualStudio.Shell; 6 | 7 | using TomsToolbox.Wpf; 8 | 9 | namespace Nullable.Extended.Extension.AnalyzerFramework 10 | { 11 | internal abstract class AnalyzerResultViewModel : INotifyPropertyChanged where TResult : AnalysisResult 12 | { 13 | protected AnalyzerResultViewModel(AnalyzerViewModel analyzerViewModel) 14 | { 15 | ThreadHelper.ThrowIfNotOnUIThread(); 16 | 17 | AnalyzerViewModel = analyzerViewModel; 18 | SetResults(analyzerViewModel.AnalysisResults); 19 | AnalyzerViewModel.AnalysisResultsChanged += AnalyzerViewModel_AnalysisResultsChanged; 20 | } 21 | 22 | private void AnalyzerViewModel_AnalysisResultsChanged(object sender, AnalysisResultsChangedArgs e) 23 | { 24 | SetResults(e.Results); 25 | } 26 | 27 | protected void SetResults(IEnumerable results) 28 | { 29 | AnalysisResults = results 30 | .OfType() 31 | .ToList() 32 | .AsReadOnly(); 33 | } 34 | 35 | public AnalyzerViewModel AnalyzerViewModel { get; } 36 | 37 | public IReadOnlyList AnalysisResults { get; private set; } = Array.Empty(); 38 | 39 | public ICommand AnalyzeCommand => new DelegateCommand(CanAnalyze, AnalyzeSolution); 40 | 41 | public static ICommand OpenInDocumentCommand => new DelegateCommand(Views.ExtensionMethods.OpenInDocument); 42 | 43 | private bool CanAnalyze() 44 | { 45 | return AnalyzerViewModel.CanAnalyze; 46 | } 47 | 48 | private void AnalyzeSolution() 49 | { 50 | AnalyzerViewModel.AnalyzeSolution(); 51 | } 52 | 53 | public event PropertyChangedEventHandler? PropertyChanged; 54 | 55 | protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) 56 | { 57 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/Views/AnalyzerViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.ComponentModel; 3 | using System.Composition; 4 | using System.Runtime.CompilerServices; 5 | 6 | using Microsoft.CodeAnalysis; 7 | using Microsoft.VisualStudio.ComponentModelHost; 8 | using Microsoft.VisualStudio.LanguageServices; 9 | 10 | using PropertyChanged; 11 | 12 | using Throttle; 13 | 14 | using TomsToolbox.Essentials; 15 | 16 | #pragma warning disable VSTHRD100 // Avoid async void methods 17 | 18 | namespace Nullable.Extended.Extension.AnalyzerFramework 19 | { 20 | [Export, Shared] 21 | internal class AnalyzerViewModel : INotifyPropertyChanged 22 | { 23 | private readonly IAnalyzerEngine _analyzerEngine; 24 | private readonly VisualStudioWorkspace _workspace; 25 | 26 | private HashSet _changedDocuments = new(); 27 | 28 | public AnalyzerViewModel(IAnalyzerEngine analyzerEngine) 29 | { 30 | _analyzerEngine = analyzerEngine; 31 | var componentModel = (IComponentModel)Microsoft.VisualStudio.Shell.Package.GetGlobalService(typeof(SComponentModel)); 32 | 33 | _workspace = componentModel.GetService(); 34 | _workspace.WorkspaceChanged += Workspace_WorkspaceChanged; 35 | } 36 | 37 | public bool CanAnalyze => !IsAnalyzing && _workspace.CurrentSolution.GetDocumentsToAnalyze().Any(); 38 | 39 | public bool IsAnalyzing { get; private set; } 40 | 41 | [OnChangedMethod(nameof(OnAnalysisResultsChanged))] 42 | public ImmutableList AnalysisResults { get; private set; } = ImmutableList.Empty; 43 | 44 | public async void AnalyzeSolution() 45 | { 46 | if (IsAnalyzing) 47 | return; 48 | 49 | var documentsToAnalyze = _workspace.CurrentSolution.GetDocumentsToAnalyze().ToImmutableList(); 50 | 51 | try 52 | { 53 | AnalysisResults = (await AnalyzeAsync(documentsToAnalyze)).ToImmutableList(); 54 | } 55 | catch 56 | { 57 | // just do nothing 58 | } 59 | } 60 | 61 | private void Workspace_WorkspaceChanged(object sender, WorkspaceChangeEventArgs e) 62 | { 63 | var documentId = e.DocumentId; 64 | 65 | if (e.Kind == WorkspaceChangeKind.DocumentChanged && documentId != null) 66 | { 67 | _changedDocuments.Add(documentId); 68 | AnalyzeChanges(); 69 | } 70 | } 71 | 72 | [Throttled(typeof(TomsToolbox.Wpf.Throttle), 2000)] 73 | private async void AnalyzeChanges() 74 | { 75 | if (IsAnalyzing) 76 | return; 77 | 78 | try 79 | { 80 | var changedDocuments = Interlocked.Exchange(ref _changedDocuments, new HashSet()); 81 | 82 | var documents = changedDocuments 83 | .Select(documentId => _workspace.CurrentSolution.GetDocument(documentId)) 84 | .ExceptNullItems() 85 | .ToArray(); 86 | 87 | var documentFiles = new HashSet(documents.Select(d => d.FilePath), StringComparer.OrdinalIgnoreCase); 88 | 89 | var documentsToAnalyze = documents.Where(document => document.ShouldBeAnalyzed()); 90 | 91 | var updated = await AnalyzeAsync(documentsToAnalyze); 92 | 93 | AnalysisResults = AnalysisResults 94 | .RemoveAll(existing => documentFiles.Contains(existing.AnalysisContext.Document.FilePath)) 95 | .AddRange(updated.Where(d => documentFiles.Contains(d.AnalysisContext.Document.FilePath))); 96 | } 97 | catch 98 | { 99 | // 100 | } 101 | } 102 | 103 | private async Task> AnalyzeAsync(IEnumerable documentsToAnalyze) 104 | { 105 | if (IsAnalyzing) 106 | throw new InvalidOperationException("Already analyzing!"); 107 | 108 | try 109 | { 110 | IsAnalyzing = true; 111 | 112 | return await _analyzerEngine.AnalyzeAsync(documentsToAnalyze, CancellationToken.None).ConfigureAwait(true); 113 | } 114 | finally 115 | { 116 | IsAnalyzing = false; 117 | } 118 | } 119 | 120 | private void OnAnalysisResultsChanged() 121 | { 122 | AnalysisResultsChanged?.Invoke(this, new AnalysisResultsChangedArgs(AnalysisResults)); 123 | } 124 | 125 | public event EventHandler? AnalysisResultsChanged; 126 | 127 | public event PropertyChangedEventHandler? PropertyChanged; 128 | 129 | protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) 130 | { 131 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/Views/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | using System.Windows.Media; 3 | 4 | using Community.VisualStudio.Toolkit; 5 | 6 | using Microsoft.VisualStudio.Text; 7 | 8 | using Nullable.Extended.Extension.AnalyzerFramework; 9 | 10 | using TomsToolbox.Composition; 11 | using TomsToolbox.Wpf.Composition.XamlExtensions; 12 | 13 | namespace Nullable.Extended.Extension.Views 14 | { 15 | public static class ExtensionMethods 16 | { 17 | public static double ToGray(this Color? color) 18 | { 19 | return color?.R * 0.3 + color?.G * 0.6 + color?.B * 0.1 ?? 0.0; 20 | } 21 | 22 | public static Control GetToolWindowShell(this IExportProvider exportProvider, string regionId) 23 | { 24 | var shell = exportProvider.GetExportedValue(); 25 | 26 | VisualComposition.SetRegionId(shell, regionId); 27 | 28 | return shell; 29 | } 30 | 31 | 32 | #pragma warning disable VSTHRD100 // Avoid async void methods 33 | public static async void OpenInDocument(this AnalysisResult result) 34 | { 35 | try 36 | { 37 | var textView = (await VS.Documents.OpenAsync(result.FilePath).ConfigureAwait(true))?.TextView; 38 | if (textView == null) 39 | return; 40 | 41 | var snapshot = textView.FormattedLineSource.SourceTextSnapshot; 42 | var line = snapshot.Lines.Skip(result.Line - 1).FirstOrDefault(); 43 | var span = new SnapshotSpan(new VirtualSnapshotPoint(line, result.Column - 1).Position, new VirtualSnapshotPoint(line, result.Column).Position); 44 | 45 | textView.Selection.Select(span, false); 46 | textView.ViewScroller.EnsureSpanVisible(span); 47 | } 48 | catch 49 | { 50 | // Probably the document has already changed and the span is no longer valid. User can simply retry. 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/Views/NullForgivingToolWindow.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using System.Windows.Input; 3 | 4 | using Microsoft.VisualStudio.Shell; 5 | 6 | using Nullable.Extended.Extension.Extension; 7 | 8 | namespace Nullable.Extended.Extension.Views 9 | { 10 | [Guid("5e194576-2306-4098-ab46-1cdd4e8e8579")] 11 | public class NullForgivingToolWindow : ToolWindowPane 12 | { 13 | /// 14 | /// Initializes a new instance of the class. 15 | /// 16 | public NullForgivingToolWindow() : base(null) 17 | { 18 | Caption = "Null Forgiving Operators"; 19 | BitmapResourceID = 301; 20 | BitmapIndex = 0; 21 | } 22 | 23 | protected override void Initialize() 24 | { 25 | base.Initialize(); 26 | 27 | Content = ((ExtensionPackage)Package).ExportProvider.GetToolWindowShell(nameof(NullForgivingToolWindow)); 28 | } 29 | 30 | protected override bool PreProcessMessage(ref System.Windows.Forms.Message m) 31 | { 32 | // Do not pass CTRL+'F' to visual studio, we do our own search 33 | if (m.Msg != 0x0100 34 | || m.WParam != (IntPtr)0x46 && m.WParam != (IntPtr)0x66 35 | || Keyboard.Modifiers != ModifierKeys.Control) 36 | { 37 | return base.PreProcessMessage(ref m); 38 | } 39 | 40 | var keyboardDevice = Keyboard.PrimaryDevice; 41 | 42 | var e = new KeyEventArgs(keyboardDevice, keyboardDevice.ActiveSource, 0, Key.F) 43 | { 44 | RoutedEvent = Keyboard.KeyDownEvent 45 | }; 46 | 47 | _ = InputManager.Current.ProcessInput(e); 48 | 49 | return true; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/Views/NullForgivingToolWindowView.xaml: -------------------------------------------------------------------------------- 1 |  14 | 15 | 16 | 17 | 18 | 26 | 27 | 30 | 31 | 38 | 39 | 40 | 41 | 45 | 46 | 49 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 77 | 78 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/Views/NullForgivingToolWindowView.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Composition; 2 | using System.Windows; 3 | using System.Windows.Controls; 4 | using System.Windows.Input; 5 | 6 | using DataGridExtensions; 7 | 8 | using Nullable.Extended.Extension.AnalyzerFramework; 9 | 10 | using TomsToolbox.Composition; 11 | using TomsToolbox.Wpf.Composition; 12 | using TomsToolbox.Wpf.Composition.AttributedModel; 13 | 14 | namespace Nullable.Extended.Extension.Views 15 | { 16 | /// 17 | /// Interaction logic for NullForgivingToolWindowView.xaml 18 | /// 19 | [DataTemplate(typeof(NullForgivingToolWindowViewModel))] 20 | [Shared] 21 | public partial class NullForgivingToolWindowView 22 | { 23 | public NullForgivingToolWindowView(IExportProvider exportProvider) 24 | { 25 | this.SetExportProvider(exportProvider); 26 | 27 | InitializeComponent(); 28 | } 29 | 30 | private void DataGridRow_OnKeyDown(object sender, KeyEventArgs e) 31 | { 32 | if (e.Key is Key.Space or Key.Enter && Keyboard.Modifiers == ModifierKeys.None) 33 | { 34 | var item = (sender as DataGridRow)?.DataContext; 35 | if (item is AnalysisResult analysisResult) 36 | { 37 | analysisResult.OpenInDocument(); 38 | } 39 | e.Handled = true; 40 | } 41 | } 42 | 43 | private void DataGrid_IsKeyboardFocusWithinChanged(object sender, DependencyPropertyChangedEventArgs e) 44 | { 45 | if (sender is not DataGrid dataGrid) 46 | return; 47 | 48 | var lastFocusedCell = dataGrid.GetLastFocusedCell(); 49 | 50 | if (true.Equals(e.NewValue) && lastFocusedCell != null) 51 | { 52 | _ = lastFocusedCell.Focus(); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/Views/NullForgivingToolWindowViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Composition; 2 | 3 | using Nullable.Extended.Extension.AnalyzerFramework; 4 | using Nullable.Extended.Extension.NullForgivingAnalyzer; 5 | 6 | using TomsToolbox.Wpf.Composition.AttributedModel; 7 | 8 | namespace Nullable.Extended.Extension.Views 9 | { 10 | [VisualCompositionExport(nameof(NullForgivingToolWindow))] 11 | [Shared] 12 | internal class NullForgivingToolWindowViewModel : AnalyzerResultViewModel 13 | { 14 | public NullForgivingToolWindowViewModel(AnalyzerViewModel analyzerViewModel) 15 | : base(analyzerViewModel) 16 | { 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/Views/ThemeManager.cs: -------------------------------------------------------------------------------- 1 | namespace Nullable.Extended.Extension.Views 2 | { 3 | public interface IThemeManager 4 | { 5 | public bool IsDarkTheme { get; } 6 | } 7 | } -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/Views/ToolWindowShell.xaml: -------------------------------------------------------------------------------- 1 |  7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/Views/ToolWindowShell.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Composition; 2 | using System.Windows; 3 | using System.Windows.Media; 4 | 5 | using TomsToolbox.Composition; 6 | using TomsToolbox.Wpf.Composition; 7 | using TomsToolbox.Wpf.Styles; 8 | 9 | namespace Nullable.Extended.Extension.Views 10 | { 11 | /// 12 | /// Interaction logic for ToolWindowShell. 13 | /// 14 | [Export, Export(typeof(IThemeManager))] 15 | public partial class ToolWindowShell : IThemeManager 16 | { 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | public ToolWindowShell(IExportProvider exportProvider) 21 | { 22 | this.SetExportProvider(exportProvider); 23 | 24 | InitializeComponent(); 25 | 26 | Resources.MergedDictionaries.Add(DataTemplateManager.CreateDynamicDataTemplates(exportProvider)); 27 | Resources.MergedDictionaries.Insert(0, WpfStyles.GetDefaultStyles()); 28 | } 29 | 30 | public static readonly DependencyProperty IsDarkThemeProperty = DependencyProperty.Register( 31 | "IsDarkTheme", typeof(bool), typeof(ToolWindowShell), new PropertyMetadata(default(bool))); 32 | 33 | public bool IsDarkTheme 34 | { 35 | get => (bool)GetValue(IsDarkThemeProperty); 36 | set => SetValue(IsDarkThemeProperty, value); 37 | } 38 | 39 | protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) 40 | { 41 | base.OnPropertyChanged(e); 42 | 43 | if ((e.Property != ForegroundProperty) && (e.Property != BackgroundProperty)) 44 | return; 45 | 46 | var foreground = ((Foreground as SolidColorBrush)?.Color).ToGray(); 47 | var background = ((Background as SolidColorBrush)?.Color).ToGray(); 48 | 49 | IsDarkTheme = background < foreground; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Nullable.Extended.Extension/source.extension.vsixmanifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Nullable.Extended 6 | Tools to keep your nullability annotations lean and mean. 7 | https://github.com/tom-englert/Nullable.Extended 8 | LICENSE.txt 9 | package_icon.png 10 | package_icon.png 11 | nullable c# 12 | 13 | 14 | 15 | amd64 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Sample/Class1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.Json.Nodes; 5 | 6 | namespace Sample 7 | { 8 | public class ClassWithWarnings 9 | { 10 | private readonly IReadOnlyCollection _mappings; 11 | 12 | public ClassWithWarnings() 13 | { 14 | _mappings = Enumerable.Empty() 15 | .Where(item => item.Value != null) 16 | .OrderBy(item => item.Value.Length) 17 | .ToList() 18 | .AsReadOnly(); 19 | } 20 | 21 | public void Test(JsonObject request) 22 | { 23 | foreach (var (key, value) in request) 24 | { 25 | var mapping = _mappings.FirstOrDefault(item => "something".StartsWith(item.Value, StringComparison.OrdinalIgnoreCase)); 26 | } 27 | } 28 | } 29 | 30 | public class ClassWithSuppressions 31 | { 32 | private readonly IReadOnlyCollection _mappings; 33 | 34 | public ClassWithSuppressions() 35 | { 36 | _mappings = Enumerable.Empty() 37 | .Where(item => item.Value != null) 38 | .OrderBy(item => item.Value!.Length) 39 | .ToList() 40 | .AsReadOnly(); 41 | } 42 | 43 | public void Test(JsonObject request) 44 | { 45 | foreach (var (key, value) in request) 46 | { 47 | var mapping = _mappings.FirstOrDefault(item => "something".StartsWith(item.Value!, StringComparison.OrdinalIgnoreCase)); 48 | } 49 | } 50 | 51 | } 52 | class Mapping 53 | { 54 | public string? Key; 55 | public string? Value { get; set; } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Sample/Sample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net60 5 | enable 6 | enable 7 | 10.0 8 | disable 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Test/Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0-windows 5 | enable 6 | false 7 | 10.0 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Nullable.Extended.Extension/Test/UnitTest1.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | using Microsoft.Build.Locator; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.Diagnostics; 6 | using Microsoft.CodeAnalysis.MSBuild; 7 | 8 | using Xunit; 9 | 10 | namespace Test 11 | { 12 | public class UnitTest1 13 | { 14 | [Fact] 15 | public async Task Test1() 16 | { 17 | var project = await OpenProject(); 18 | var documents = project.Documents; 19 | var compilation = await project.GetCompilationAsync(); 20 | 21 | Assert.Single(documents); 22 | 23 | var diagnostics = compilation!.GetDiagnostics(); 24 | 25 | var errors = diagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error && !diagnostic.IsSuppressed); 26 | 27 | Assert.Empty(errors); 28 | 29 | var nullables = diagnostics.Where(IsNullableDiagnostic); 30 | 31 | Assert.Equal(2, nullables.Count()); 32 | } 33 | 34 | async Task OpenProject([CallerFilePath] string? thisFile = null) 35 | { 36 | MSBuildLocator.RegisterDefaults(); 37 | var workspace = MSBuildWorkspace.Create(); 38 | workspace.LoadMetadataForReferencedProjects = true; 39 | 40 | Directory.SetCurrentDirectory(Path.Combine(Path.GetDirectoryName(thisFile)!, @"..\Sample")); 41 | 42 | return await workspace.OpenProjectAsync("Sample.csproj"); 43 | } 44 | 45 | private static bool IsNullableDiagnostic(Diagnostic d) 46 | { 47 | return IsNullableDiagnosticId(d.Id); 48 | } 49 | 50 | private const string FirstNullableDiagnostic = "CS8600"; 51 | private const string LastNullableDiagnostic = "CS8900"; 52 | 53 | private static bool IsNullableDiagnosticId(string id) 54 | { 55 | return string.Compare(id, FirstNullableDiagnostic, StringComparison.OrdinalIgnoreCase) >= 0 56 | && string.Compare(id, LastNullableDiagnostic, StringComparison.OrdinalIgnoreCase) <= 0; 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/Nullable.Extended.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31612.314 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nullable.Extended.Analyzer", "Nullable.Extended.Analyzer\Nullable.Extended.Analyzer\Nullable.Extended.Analyzer.csproj", "{4F1551EA-DF15-4E7E-9511-88146E869B70}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nullable.Extended.Analyzer.Package", "Nullable.Extended.Analyzer\Nullable.Extended.Analyzer.Package\Nullable.Extended.Analyzer.Package.csproj", "{6E9FB024-8538-4054-A771-D7C07B082DD5}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nullable.Extended.Analyzer.Test", "Nullable.Extended.Analyzer\Nullable.Extended.Analyzer.Test\Nullable.Extended.Analyzer.Test.csproj", "{C948FA6A-8290-41F2-AA63-BFAEA89DD911}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{99A09AA7-58BE-4D61-BCDA-7DA7FDF1300B}" 13 | ProjectSection(SolutionItems) = preProject 14 | .editorconfig = .editorconfig 15 | ..\azure-pipelines.yml = ..\azure-pipelines.yml 16 | Directory.Build.props = Directory.Build.props 17 | ..\LICENSE.txt = ..\LICENSE.txt 18 | ..\README.md = ..\README.md 19 | ..\RelaseNotes.md = ..\RelaseNotes.md 20 | EndProjectSection 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nullable.Extended.Extension", "Nullable.Extended.Extension\Nullable.Extended.Extension\Nullable.Extended.Extension.csproj", "{46A78692-0A62-4894-BD3F-84D8493C4093}" 23 | EndProject 24 | Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Nullable.Shared", "Nullable.Shared\Nullable.Shared.shproj", "{850B0F8F-F2C0-4BE3-B141-2CD93EA896D8}" 25 | EndProject 26 | Global 27 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 28 | Debug|Any CPU = Debug|Any CPU 29 | Release|Any CPU = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 32 | {4F1551EA-DF15-4E7E-9511-88146E869B70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {4F1551EA-DF15-4E7E-9511-88146E869B70}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {4F1551EA-DF15-4E7E-9511-88146E869B70}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {4F1551EA-DF15-4E7E-9511-88146E869B70}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {6E9FB024-8538-4054-A771-D7C07B082DD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {6E9FB024-8538-4054-A771-D7C07B082DD5}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {6E9FB024-8538-4054-A771-D7C07B082DD5}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {6E9FB024-8538-4054-A771-D7C07B082DD5}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {C948FA6A-8290-41F2-AA63-BFAEA89DD911}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {C948FA6A-8290-41F2-AA63-BFAEA89DD911}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {C948FA6A-8290-41F2-AA63-BFAEA89DD911}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {C948FA6A-8290-41F2-AA63-BFAEA89DD911}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {46A78692-0A62-4894-BD3F-84D8493C4093}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {46A78692-0A62-4894-BD3F-84D8493C4093}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {46A78692-0A62-4894-BD3F-84D8493C4093}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {46A78692-0A62-4894-BD3F-84D8493C4093}.Release|Any CPU.Build.0 = Release|Any CPU 48 | EndGlobalSection 49 | GlobalSection(SolutionProperties) = preSolution 50 | HideSolutionNode = FALSE 51 | EndGlobalSection 52 | GlobalSection(ExtensibilityGlobals) = postSolution 53 | RESX_ShowErrorsInErrorList = False 54 | SolutionGuid = {7D3F265F-3672-482A-9F3F-6CE8488584C6} 55 | EndGlobalSection 56 | GlobalSection(SharedMSBuildProjectFiles) = preSolution 57 | Nullable.Shared\Nullable.Shared.projitems*{46a78692-0a62-4894-bd3f-84d8493c4093}*SharedItemsImports = 5 58 | Nullable.Shared\Nullable.Shared.projitems*{4f1551ea-df15-4e7e-9511-88146e869b70}*SharedItemsImports = 5 59 | Nullable.Shared\Nullable.Shared.projitems*{850b0f8f-f2c0-4be3-b141-2cd93ea896d8}*SharedItemsImports = 13 60 | EndGlobalSection 61 | EndGlobal 62 | -------------------------------------------------------------------------------- /src/Nullable.Extended.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | 1 4 | <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="" Style="aaBb" /></Policy> 5 | <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="" Style="aaBb" /></Policy></Policy> 6 | LIVE_MONITOR 7 | True 8 | True 9 | True 10 | True 11 | True 12 | True 13 | True 14 | True 15 | True -------------------------------------------------------------------------------- /src/Nullable.Shared/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp; 5 | using Microsoft.CodeAnalysis.CSharp.Syntax; 6 | 7 | namespace Nullable.Shared 8 | { 9 | internal static class ExtensionMethods 10 | { 11 | public static Location GetDiagnosticLocation(this PostfixUnaryExpressionSyntax node) 12 | { 13 | return node.OperatorToken.GetLocation(); 14 | } 15 | 16 | public static bool IsInsideLambdaExpression(this SyntaxNode node) 17 | { 18 | return node.AncestorsAndSelf().Any(n => n is LambdaExpressionSyntax); 19 | } 20 | 21 | public static bool IsNullOrDefaultExpression(this SyntaxNode node, [NotNullWhen(true)] out PostfixUnaryExpressionSyntax? expression) 22 | { 23 | expression = node as PostfixUnaryExpressionSyntax; 24 | 25 | if (expression is null) 26 | return false; 27 | 28 | switch (expression.Operand) 29 | { 30 | case LiteralExpressionSyntax l: 31 | #pragma warning disable IDE0010 32 | // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault 33 | switch (l.Kind()) 34 | { 35 | case SyntaxKind.DefaultLiteralExpression: 36 | case SyntaxKind.NullLiteralExpression: 37 | return true; 38 | } 39 | 40 | break; 41 | 42 | case DefaultExpressionSyntax: 43 | return true; 44 | } 45 | 46 | return false; 47 | } 48 | 49 | public static bool IsInitOnlyPropertyAssignment(this PostfixUnaryExpressionSyntax node) 50 | { 51 | return node.Parent is EqualsValueClauseSyntax { Parent: PropertyDeclarationSyntax propertyDeclaration } && propertyDeclaration.AccessorList?.Accessors.Any(item => item.IsKind(SyntaxKind.InitAccessorDeclaration)) == true; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Nullable.Shared/Justification.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | 5 | namespace Nullable.Shared 6 | { 7 | internal static class Justification 8 | { 9 | public const string SuppressionCommentPrefix = "// ! "; 10 | 11 | private static readonly HashSet SuppressionCommentTargetKind = 12 | new(Enum.GetValues(typeof(SyntaxKind)).Cast().Where(IsSuppressionCommentTargetCandidate)); 13 | 14 | public static bool HasJustificationText(this PostfixUnaryExpressionSyntax node) 15 | { 16 | return node 17 | .FindSuppressionCommentTarget() 18 | ?.GetReversJustificationLines(node) 19 | ?.Any() 20 | ?? false; 21 | } 22 | 23 | public static string? GetJustificationText(this PostfixUnaryExpressionSyntax node) 24 | { 25 | return node 26 | .FindSuppressionCommentTarget() 27 | ?.GetJustificationText(node); 28 | } 29 | 30 | private static IReadOnlyCollection? GetReversJustificationLines(this SyntaxNode ancestor, PostfixUnaryExpressionSyntax node) 31 | { 32 | var lines = ancestor.DescendantNodesAndTokensAndSelf() 33 | .TakeWhile(item => item != node) 34 | .Where(item => item.Parent == ancestor) 35 | .Select(GetReversJustificationLines) 36 | .FirstOrDefault(item => item?.Count > 0); 37 | 38 | return lines; 39 | } 40 | 41 | private static string? GetJustificationText(this SyntaxNode ancestor, PostfixUnaryExpressionSyntax node) 42 | { 43 | var lines = ancestor.GetJustificationLines(node); 44 | return lines == null ? null : string.Join("\r\n", lines); 45 | } 46 | 47 | private static IEnumerable? GetJustificationLines(this SyntaxNode ancestor, PostfixUnaryExpressionSyntax node) 48 | { 49 | return ancestor 50 | .GetReversJustificationLines(node) 51 | ?.Reverse(); 52 | } 53 | 54 | private static IReadOnlyCollection? GetReversJustificationLines(this SyntaxNodeOrToken node) 55 | { 56 | if (!node.HasLeadingTrivia) 57 | return null; 58 | 59 | var lines = node.GetLeadingTrivia() 60 | .Where(t => t.IsKind(SyntaxKind.SingleLineCommentTrivia)) 61 | .Select(t => t.ToString()) 62 | .Reverse() 63 | .TakeWhile(value => value.StartsWith(SuppressionCommentPrefix)) 64 | .Select(s => s.Substring(SuppressionCommentPrefix.Length)) 65 | .ToList() 66 | .AsReadOnly(); 67 | 68 | return !lines.Any() || lines.All(string.IsNullOrWhiteSpace) ? null : lines; 69 | } 70 | 71 | public static CSharpSyntaxNode? FindSuppressionCommentTarget(this PostfixUnaryExpressionSyntax node) 72 | { 73 | return node 74 | .AncestorsAndSelf() 75 | .OfType() 76 | .FirstOrDefault(i => SuppressionCommentTargetKind.Contains(i.Kind())); 77 | } 78 | 79 | private static bool IsSuppressionCommentTargetCandidate(SyntaxKind syntaxKind) 80 | { 81 | if (syntaxKind == SyntaxKind.VariableDeclaration) 82 | return false; // always use the parent declaration for variable declarations! 83 | 84 | var name = syntaxKind.ToString(); 85 | return name.EndsWith("Statement", StringComparison.Ordinal) || name.EndsWith("Declaration", StringComparison.Ordinal); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Nullable.Shared/Nullable.Shared.projitems: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | true 6 | 850b0f8f-f2c0-4be3-b141-2cd93ea896d8 7 | 8 | 9 | Nullable.Shared 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Nullable.Shared/Nullable.Shared.shproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 850b0f8f-f2c0-4be3-b141-2cd93ea896d8 5 | 14.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Nullable.Shared/Nullable.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | #if NETSTANDARD || NETFRAMEWORK 6 | 7 | #define INTERNAL_NULLABLE_ATTRIBUTES 8 | 9 | namespace System.Diagnostics.CodeAnalysis 10 | { 11 | /// Specifies that null is allowed as an input even if the corresponding type disallows it. 12 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] 13 | #if INTERNAL_NULLABLE_ATTRIBUTES 14 | internal 15 | #else 16 | public 17 | #endif 18 | sealed class AllowNullAttribute : Attribute 19 | { } 20 | 21 | /// Specifies that null is disallowed as an input even if the corresponding type allows it. 22 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] 23 | #if INTERNAL_NULLABLE_ATTRIBUTES 24 | internal 25 | #else 26 | public 27 | #endif 28 | sealed class DisallowNullAttribute : Attribute 29 | { } 30 | 31 | /// Specifies that an output may be null even if the corresponding type disallows it. 32 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] 33 | #if INTERNAL_NULLABLE_ATTRIBUTES 34 | internal 35 | #else 36 | public 37 | #endif 38 | sealed class MaybeNullAttribute : Attribute 39 | { } 40 | 41 | /// Specifies that an output will not be null even if the corresponding type allows it. 42 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] 43 | #if INTERNAL_NULLABLE_ATTRIBUTES 44 | internal 45 | #else 46 | public 47 | #endif 48 | sealed class NotNullAttribute : Attribute 49 | { } 50 | 51 | /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. 52 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] 53 | #if INTERNAL_NULLABLE_ATTRIBUTES 54 | internal 55 | #else 56 | public 57 | #endif 58 | sealed class MaybeNullWhenAttribute : Attribute 59 | { 60 | /// Initializes the attribute with the specified return value condition. 61 | /// 62 | /// The return value condition. If the method returns this value, the associated parameter may be null. 63 | /// 64 | public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; 65 | 66 | /// Gets the return value condition. 67 | public bool ReturnValue { get; } 68 | } 69 | 70 | /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. 71 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] 72 | #if INTERNAL_NULLABLE_ATTRIBUTES 73 | internal 74 | #else 75 | public 76 | #endif 77 | sealed class NotNullWhenAttribute : Attribute 78 | { 79 | /// Initializes the attribute with the specified return value condition. 80 | /// 81 | /// The return value condition. If the method returns this value, the associated parameter will not be null. 82 | /// 83 | public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; 84 | 85 | /// Gets the return value condition. 86 | public bool ReturnValue { get; } 87 | } 88 | 89 | /// Specifies that the output will be non-null if the named parameter is non-null. 90 | [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] 91 | #if INTERNAL_NULLABLE_ATTRIBUTES 92 | internal 93 | #else 94 | public 95 | #endif 96 | sealed class NotNullIfNotNullAttribute : Attribute 97 | { 98 | /// Initializes the attribute with the associated parameter name. 99 | /// 100 | /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. 101 | /// 102 | public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; 103 | 104 | /// Gets the associated parameter name. 105 | public string ParameterName { get; } 106 | } 107 | 108 | /// Applied to a method that will never return under any circumstance. 109 | [AttributeUsage(AttributeTargets.Method, Inherited = false)] 110 | #if INTERNAL_NULLABLE_ATTRIBUTES 111 | internal 112 | #else 113 | public 114 | #endif 115 | sealed class DoesNotReturnAttribute : Attribute 116 | { } 117 | 118 | /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. 119 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] 120 | #if INTERNAL_NULLABLE_ATTRIBUTES 121 | internal 122 | #else 123 | public 124 | #endif 125 | sealed class DoesNotReturnIfAttribute : Attribute 126 | { 127 | /// Initializes the attribute with the specified parameter value. 128 | /// 129 | /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to 130 | /// the associated parameter matches this value. 131 | /// 132 | public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; 133 | 134 | /// Gets the condition parameter value. 135 | public bool ParameterValue { get; } 136 | } 137 | } 138 | 139 | #endif 140 | -------------------------------------------------------------------------------- /src/clean.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | FOR /R %%a in (*.*proj) DO CALL :CLEAN "%%~dpa" 4 | GOTO :EOF 5 | 6 | :CLEAN 7 | CALL :REMOVEDIR "%~1obj" 8 | CALL :REMOVEDIR "%~1bin" 9 | CALL :DELETEFILE "%~1*_wpftmp.csproj" 10 | GOTO :EOF 11 | 12 | :REMOVEDIR 13 | IF NOT EXIST %1 GOTO :EOF 14 | @ECHO REMOVE %1 15 | RD /s /q %1 16 | GOTO :EOF 17 | 18 | :DELETEFILE 19 | IF NOT EXIST %1 GOTO :EOF 20 | @ECHO DELETE %1 21 | DEL %1 22 | GOTO :EOF -------------------------------------------------------------------------------- /src/package_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tom-englert/Nullable.Extended/cef7dd65770648240c1edc09597439b35e74d35b/src/package_icon.png --------------------------------------------------------------------------------