├── .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 | [](https://dev.azure.com/tom-englert/Open%20Source/_build/latest?definitionId=39&branchName=master)
3 | [](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 | 
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 | 
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 | 
88 |
89 | The warning will disappear after a justification comment has been added:
90 |
91 | 
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 |
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 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
90 |
92 |
99 |
100 |
101 |
102 |
115 |
116 |
117 |
119 |
121 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
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 |
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
--------------------------------------------------------------------------------