├── .config └── dotnet-tools.json ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .editorconfig ├── .gitattributes ├── .github └── dependabot.yml ├── .gitignore ├── .prettierrc.yaml ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CONTRIBUTING.md ├── CSharpIsNullAnalyzer.sln ├── Directory.Build.props ├── Directory.Build.rsp ├── Directory.Build.targets ├── Directory.Packages.props ├── LICENSE ├── README.md ├── azure-pipelines.yml ├── azure-pipelines ├── Get-ArtifactsStagingDirectory.ps1 ├── Get-CodeCovTool.ps1 ├── Get-NuGetTool.ps1 ├── Get-ProcDump.ps1 ├── Get-SymbolFiles.ps1 ├── Get-TempToolsPath.ps1 ├── Merge-CodeCoverage.ps1 ├── artifacts │ ├── Variables.ps1 │ ├── _all.ps1 │ ├── _pipelines.ps1 │ ├── _stage_all.ps1 │ ├── build_logs.ps1 │ ├── coverageResults.ps1 │ ├── deployables.ps1 │ ├── projectAssetsJson.ps1 │ ├── symbols.ps1 │ ├── testResults.ps1 │ └── test_symbols.ps1 ├── build.yml ├── dotnet-test-cloud.ps1 ├── dotnet.yml ├── install-dependencies.yml ├── justnugetorg.nuget.config ├── publish-CodeCov.ps1 ├── publish-codecoverage.yml ├── publish-deployables.yml ├── publish-symbols.yml ├── release.yml ├── test.runsettings └── variables │ ├── DotNetSdkVersion.ps1 │ ├── _all.ps1 │ └── _pipelines.ps1 ├── doc └── analyzers │ ├── CSIsNull001.md │ └── CSIsNull002.md ├── global.json ├── init.cmd ├── init.ps1 ├── nuget.config ├── settings.VisualStudio.json ├── src ├── .editorconfig ├── AssemblyInfo.cs ├── AssemblyInfo.vb ├── CSharpIsNullAnalyzer.CodeFixes │ ├── AssemblyInfo.cs │ ├── CSIsNull001Fixer.cs │ ├── CSIsNull002Fixer.cs │ ├── CSharpIsNullAnalyzer.CodeFixes.csproj │ ├── IsFixer.cs │ ├── Strings.Designer.cs │ ├── Strings.resx │ └── tools │ │ ├── install.ps1 │ │ └── uninstall.ps1 ├── CSharpIsNullAnalyzer │ ├── AnalyzerReleases.Shipped.md │ ├── AnalyzerReleases.Unshipped.md │ ├── AssemblyInfo.cs │ ├── CSIsNull001.cs │ ├── CSIsNull002.cs │ ├── CSharpIsNullAnalyzer.csproj │ ├── IOperationExtensions.cs │ ├── Strings.Designer.cs │ ├── Strings.resx │ ├── Utils.cs │ └── WellKnownTypeNames.cs ├── Directory.Build.props └── Directory.Build.targets ├── strongname.snk ├── stylecop.json ├── test ├── .editorconfig ├── CSharpIsNullAnalyzer.Tests │ ├── CSIsNull001Tests.cs │ ├── CSIsNull002Tests.cs │ ├── CSharpIsNullAnalyzer.Tests.csproj │ ├── Helpers │ │ ├── CSharpCodeFixVerifier`2+Test.cs │ │ ├── CSharpCodeFixVerifier`2.cs │ │ └── ReferencesHelper.cs │ └── app.config ├── Directory.Build.props └── Directory.Build.targets ├── tools ├── Check-DotNetRuntime.ps1 ├── Check-DotNetSdk.ps1 ├── Install-DotNetSdk.ps1 ├── Install-NuGetCredProvider.ps1 └── Set-EnvVars.ps1 └── version.json /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "powershell": { 6 | "version": "7.4.3", 7 | "commands": [ 8 | "pwsh" 9 | ] 10 | }, 11 | "dotnet-coverage": { 12 | "version": "17.11.3", 13 | "commands": [ 14 | "dotnet-coverage" 15 | ] 16 | }, 17 | "nbgv": { 18 | "version": "3.6.139", 19 | "commands": [ 20 | "nbgv" 21 | ] 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Refer to https://hub.docker.com/_/microsoft-dotnet-sdk for available versions 2 | FROM mcr.microsoft.com/dotnet/sdk:8.0.300-jammy 3 | 4 | # Installing mono makes `dotnet test` work without errors even for net472. 5 | # But installing it takes a long time, so it's excluded by default. 6 | #RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF 7 | #RUN echo "deb https://download.mono-project.com/repo/ubuntu stable-bionic main" | tee /etc/apt/sources.list.d/mono-official-stable.list 8 | #RUN apt-get update 9 | #RUN DEBIAN_FRONTEND=noninteractive apt-get install -y mono-devel 10 | 11 | # Clear the NUGET_XMLDOC_MODE env var so xml api doc files get unpacked, allowing a rich experience in Intellisense. 12 | # See https://github.com/dotnet/dotnet-docker/issues/2790 for a discussion on this, where the prioritized use case 13 | # was *not* devcontainers, sadly. 14 | ENV NUGET_XMLDOC_MODE= 15 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dev space", 3 | "dockerFile": "Dockerfile", 4 | "settings": { 5 | "terminal.integrated.shell.linux": "/usr/bin/pwsh" 6 | }, 7 | "postCreateCommand": "./init.ps1 -InstallLocality machine", 8 | "extensions": [ 9 | "ms-azure-devops.azure-pipelines", 10 | "ms-dotnettools.csharp", 11 | "k--kato.docomment", 12 | "editorconfig.editorconfig", 13 | "pflannery.vscode-versionlens", 14 | "davidanson.vscode-markdownlint", 15 | "dotjoshjohnson.xml", 16 | "ms-vscode-remote.remote-containers", 17 | "ms-azuretools.vscode-docker", 18 | "ms-vscode.powershell" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome:http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Don't use tabs for indentation. 7 | [*] 8 | indent_style = space 9 | 10 | # (Please don't specify an indent_size here; that has too many unintended consequences.) 11 | 12 | [*.yml] 13 | indent_size = 2 14 | indent_style = space 15 | 16 | # Code files 17 | [*.{cs,csx,vb,vbx,h,cpp,idl}] 18 | indent_size = 4 19 | insert_final_newline = true 20 | trim_trailing_whitespace = true 21 | 22 | # MSBuild project files 23 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,msbuildproj,props,targets}] 24 | indent_size = 2 25 | 26 | # Xml config files 27 | [*.{ruleset,config,nuspec,resx,vsixmanifest,vsct,runsettings}] 28 | indent_size = 2 29 | indent_style = space 30 | 31 | # JSON files 32 | [*.json] 33 | indent_size = 2 34 | indent_style = space 35 | 36 | [*.ps1] 37 | indent_style = space 38 | indent_size = 4 39 | 40 | # Dotnet code style settings: 41 | [*.{cs,vb}] 42 | # Sort using and Import directives with System.* appearing first 43 | dotnet_sort_system_directives_first = true 44 | dotnet_separate_import_directive_groups = false 45 | dotnet_style_qualification_for_field = true:warning 46 | dotnet_style_qualification_for_property = true:warning 47 | dotnet_style_qualification_for_method = true:warning 48 | dotnet_style_qualification_for_event = true:warning 49 | 50 | # Use language keywords instead of framework type names for type references 51 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 52 | dotnet_style_predefined_type_for_member_access = true:suggestion 53 | 54 | # Suggest more modern language features when available 55 | dotnet_style_object_initializer = true:suggestion 56 | dotnet_style_collection_initializer = true:suggestion 57 | dotnet_style_coalesce_expression = true:suggestion 58 | dotnet_style_null_propagation = true:suggestion 59 | dotnet_style_explicit_tuple_names = true:suggestion 60 | 61 | # Non-private static fields are PascalCase 62 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion 63 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields 64 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style 65 | 66 | dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field 67 | dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected internal, private protected 68 | dotnet_naming_symbols.non_private_static_fields.required_modifiers = static 69 | 70 | dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case 71 | 72 | # Constants are PascalCase 73 | dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion 74 | dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants 75 | dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style 76 | 77 | dotnet_naming_symbols.constants.applicable_kinds = field, local 78 | dotnet_naming_symbols.constants.required_modifiers = const 79 | 80 | dotnet_naming_style.constant_style.capitalization = pascal_case 81 | 82 | # Static fields are camelCase 83 | dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion 84 | dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields 85 | dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style 86 | 87 | dotnet_naming_symbols.static_fields.applicable_kinds = field 88 | dotnet_naming_symbols.static_fields.required_modifiers = static 89 | 90 | dotnet_naming_style.static_field_style.capitalization = camel_case 91 | 92 | # Instance fields are camelCase 93 | dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion 94 | dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields 95 | dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style 96 | 97 | dotnet_naming_symbols.instance_fields.applicable_kinds = field 98 | 99 | dotnet_naming_style.instance_field_style.capitalization = camel_case 100 | 101 | # Locals and parameters are camelCase 102 | dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion 103 | dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters 104 | dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style 105 | 106 | dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local 107 | 108 | dotnet_naming_style.camel_case_style.capitalization = camel_case 109 | 110 | # Local functions are PascalCase 111 | dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion 112 | dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions 113 | dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style 114 | 115 | dotnet_naming_symbols.local_functions.applicable_kinds = local_function 116 | 117 | dotnet_naming_style.local_function_style.capitalization = pascal_case 118 | 119 | # By default, name items with PascalCase 120 | dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion 121 | dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members 122 | dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style 123 | 124 | dotnet_naming_symbols.all_members.applicable_kinds = * 125 | 126 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 127 | 128 | # CSharp code style settings: 129 | [*.cs] 130 | # Indentation preferences 131 | csharp_indent_block_contents = true 132 | csharp_indent_braces = false 133 | csharp_indent_case_contents = true 134 | csharp_indent_switch_labels = true 135 | csharp_indent_labels = flush_left 136 | 137 | # Prefer "var" everywhere 138 | csharp_style_var_for_built_in_types = false 139 | csharp_style_var_when_type_is_apparent = true:suggestion 140 | csharp_style_var_elsewhere = false:warning 141 | 142 | # Prefer method-like constructs to have a block body 143 | csharp_style_expression_bodied_methods = false:none 144 | csharp_style_expression_bodied_constructors = false:none 145 | csharp_style_expression_bodied_operators = false:none 146 | 147 | # Prefer property-like constructs to have an expression-body 148 | csharp_style_expression_bodied_properties = true:none 149 | csharp_style_expression_bodied_indexers = true:none 150 | csharp_style_expression_bodied_accessors = true:none 151 | 152 | # Suggest more modern language features when available 153 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 154 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 155 | csharp_style_inlined_variable_declaration = true:suggestion 156 | csharp_style_throw_expression = true:suggestion 157 | csharp_style_conditional_delegate_call = true:suggestion 158 | 159 | # Newline settings 160 | csharp_new_line_before_open_brace = all 161 | csharp_new_line_before_else = true 162 | csharp_new_line_before_catch = true 163 | csharp_new_line_before_finally = true 164 | csharp_new_line_before_members_in_object_initializers = true 165 | csharp_new_line_before_members_in_anonymous_types = true 166 | 167 | # Blocks are allowed 168 | csharp_prefer_braces = true:silent 169 | 170 | # SA1130: Use lambda syntax 171 | dotnet_diagnostic.SA1130.severity = silent 172 | 173 | # IDE1006: Naming Styles - StyleCop handles these for us 174 | dotnet_diagnostic.IDE1006.severity = none 175 | 176 | dotnet_diagnostic.DOC100.severity = silent 177 | dotnet_diagnostic.DOC104.severity = warning 178 | dotnet_diagnostic.DOC105.severity = warning 179 | dotnet_diagnostic.DOC106.severity = warning 180 | dotnet_diagnostic.DOC107.severity = warning 181 | dotnet_diagnostic.DOC108.severity = warning 182 | dotnet_diagnostic.DOC200.severity = warning 183 | dotnet_diagnostic.DOC202.severity = warning 184 | 185 | # CA1062: Validate arguments of public methods 186 | dotnet_diagnostic.CA1062.severity = warning 187 | 188 | [*.sln] 189 | indent_style = tab 190 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | # Ensure shell scripts use LF line endings (linux only accepts LF) 7 | *.sh eol=lf 8 | *.ps1 eol=lf 9 | 10 | # The macOS codesign tool is extremely picky, and requires LF line endings. 11 | *.plist eol=lf 12 | 13 | ############################################################################### 14 | # Set default behavior for command prompt diff. 15 | # 16 | # This is need for earlier builds of msysgit that does not have it on by 17 | # default for csharp files. 18 | # Note: This is only used by command line 19 | ############################################################################### 20 | #*.cs diff=csharp 21 | 22 | ############################################################################### 23 | # Set the merge driver for project and solution files 24 | # 25 | # Merging from the command prompt will add diff markers to the files if there 26 | # are conflicts (Merging from VS is not affected by the settings below, in VS 27 | # the diff markers are never inserted). Diff markers may cause the following 28 | # file extensions to fail to load in VS. An alternative would be to treat 29 | # these files as binary and thus will always conflict and require user 30 | # intervention with every merge. To do so, just uncomment the entries below 31 | ############################################################################### 32 | #*.sln merge=binary 33 | #*.csproj merge=binary 34 | #*.vbproj merge=binary 35 | #*.vcxproj merge=binary 36 | #*.vcproj merge=binary 37 | #*.dbproj merge=binary 38 | #*.fsproj merge=binary 39 | #*.lsproj merge=binary 40 | #*.wixproj merge=binary 41 | #*.modelproj merge=binary 42 | #*.sqlproj merge=binary 43 | #*.wwaproj merge=binary 44 | 45 | ############################################################################### 46 | # behavior for image files 47 | # 48 | # image files are treated as binary by default. 49 | ############################################################################### 50 | #*.jpg binary 51 | #*.png binary 52 | #*.gif binary 53 | 54 | ############################################################################### 55 | # diff behavior for common document formats 56 | # 57 | # Convert binary document formats to text before diffing them. This feature 58 | # is only available from the command line. Turn it on by uncommenting the 59 | # entries below. 60 | ############################################################################### 61 | #*.doc diff=astextplain 62 | #*.DOC diff=astextplain 63 | #*.docx diff=astextplain 64 | #*.DOCX diff=astextplain 65 | #*.dot diff=astextplain 66 | #*.DOT diff=astextplain 67 | #*.pdf diff=astextplain 68 | #*.PDF diff=astextplain 69 | #*.rtf diff=astextplain 70 | #*.RTF diff=astextplain 71 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: nuget 7 | directory: / 8 | schedule: 9 | interval: weekly 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | *.lutconfig 13 | launchSettings.json 14 | 15 | # User-specific files (MonoDevelop/Xamarin Studio) 16 | *.userprefs 17 | 18 | # Mono auto generated files 19 | mono_crash.* 20 | 21 | # Build results 22 | [Dd]ebug/ 23 | [Dd]ebugPublic/ 24 | [Rr]elease/ 25 | [Rr]eleases/ 26 | x64/ 27 | x86/ 28 | [Aa][Rr][Mm]/ 29 | [Aa][Rr][Mm]64/ 30 | bld/ 31 | [Bb]in/ 32 | [Oo]bj/ 33 | [Ll]og/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # StyleCop 61 | StyleCopReport.xml 62 | 63 | # Files built by Visual Studio 64 | *_i.c 65 | *_p.c 66 | *_h.h 67 | *.ilk 68 | *.meta 69 | *.obj 70 | *.iobj 71 | *.pch 72 | *.pdb 73 | *.ipdb 74 | *.pgc 75 | *.pgd 76 | *.rsp 77 | !Directory.Build.rsp 78 | *.sbr 79 | *.tlb 80 | *.tli 81 | *.tlh 82 | *.tmp 83 | *.tmp_proj 84 | *_wpftmp.csproj 85 | *.log 86 | *.vspscc 87 | *.vssscc 88 | .builds 89 | *.pidb 90 | *.svclog 91 | *.scc 92 | 93 | # Chutzpah Test files 94 | _Chutzpah* 95 | 96 | # Visual C++ cache files 97 | ipch/ 98 | *.aps 99 | *.ncb 100 | *.opendb 101 | *.opensdf 102 | *.sdf 103 | *.cachefile 104 | *.VC.db 105 | *.VC.VC.opendb 106 | 107 | # Visual Studio profiler 108 | *.psess 109 | *.vsp 110 | *.vspx 111 | *.sap 112 | 113 | # Visual Studio Trace Files 114 | *.e2e 115 | 116 | # TFS 2012 Local Workspace 117 | $tf/ 118 | 119 | # Guidance Automation Toolkit 120 | *.gpState 121 | 122 | # ReSharper is a .NET coding add-in 123 | _ReSharper*/ 124 | *.[Rr]e[Ss]harper 125 | *.DotSettings.user 126 | 127 | # JustCode is a .NET coding add-in 128 | .JustCode 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | /coveragereport/ 144 | 145 | # NCrunch 146 | _NCrunch_* 147 | .*crunch*.local.xml 148 | nCrunchTemp_* 149 | 150 | # MightyMoose 151 | *.mm.* 152 | AutoTest.Net/ 153 | 154 | # Web workbench (sass) 155 | .sass-cache/ 156 | 157 | # Installshield output folder 158 | [Ee]xpress/ 159 | 160 | # DocProject is a documentation generator add-in 161 | DocProject/buildhelp/ 162 | DocProject/Help/*.HxT 163 | DocProject/Help/*.HxC 164 | DocProject/Help/*.hhc 165 | DocProject/Help/*.hhk 166 | DocProject/Help/*.hhp 167 | DocProject/Help/Html2 168 | DocProject/Help/html 169 | 170 | # Click-Once directory 171 | publish/ 172 | 173 | # Publish Web Output 174 | *.[Pp]ublish.xml 175 | *.azurePubxml 176 | # Note: Comment the next line if you want to checkin your web deploy settings, 177 | # but database connection strings (with potential passwords) will be unencrypted 178 | *.pubxml 179 | *.publishproj 180 | 181 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 182 | # checkin your Azure Web App publish settings, but sensitive information contained 183 | # in these scripts will be unencrypted 184 | PublishScripts/ 185 | 186 | # NuGet Packages 187 | *.nupkg 188 | # NuGet Symbol Packages 189 | *.snupkg 190 | # The packages folder can be ignored because of Package Restore 191 | **/[Pp]ackages/* 192 | # except build/, which is used as an MSBuild target. 193 | !**/[Pp]ackages/build/ 194 | # Uncomment if necessary however generally it will be regenerated when needed 195 | #!**/[Pp]ackages/repositories.config 196 | # NuGet v3's project.json files produces more ignorable files 197 | *.nuget.props 198 | *.nuget.targets 199 | 200 | # Microsoft Azure Build Output 201 | csx/ 202 | *.build.csdef 203 | 204 | # Microsoft Azure Emulator 205 | ecf/ 206 | rcf/ 207 | 208 | # Windows Store app package directories and files 209 | AppPackages/ 210 | BundleArtifacts/ 211 | Package.StoreAssociation.xml 212 | _pkginfo.txt 213 | *.appx 214 | *.appxbundle 215 | *.appxupload 216 | 217 | # Visual Studio cache files 218 | # files ending in .cache can be ignored 219 | *.[Cc]ache 220 | # but keep track of directories ending in .cache 221 | !?*.[Cc]ache/ 222 | 223 | # Others 224 | ClientBin/ 225 | ~$* 226 | *~ 227 | *.dbmdl 228 | *.dbproj.schemaview 229 | *.jfm 230 | *.pfx 231 | *.publishsettings 232 | orleans.codegen.cs 233 | 234 | # Including strong name files can present a security risk 235 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 236 | #*.snk 237 | 238 | # Since there are multiple workflows, uncomment next line to ignore bower_components 239 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 240 | #bower_components/ 241 | 242 | # RIA/Silverlight projects 243 | Generated_Code/ 244 | 245 | # Backup & report files from converting an old project file 246 | # to a newer Visual Studio version. Backup files are not needed, 247 | # because we have git ;-) 248 | _UpgradeReport_Files/ 249 | Backup*/ 250 | UpgradeLog*.XML 251 | UpgradeLog*.htm 252 | ServiceFabricBackup/ 253 | *.rptproj.bak 254 | 255 | # SQL Server files 256 | *.mdf 257 | *.ldf 258 | *.ndf 259 | 260 | # Business Intelligence projects 261 | *.rdl.data 262 | *.bim.layout 263 | *.bim_*.settings 264 | *.rptproj.rsuser 265 | *- [Bb]ackup.rdl 266 | *- [Bb]ackup ([0-9]).rdl 267 | *- [Bb]ackup ([0-9][0-9]).rdl 268 | 269 | # Microsoft Fakes 270 | FakesAssemblies/ 271 | 272 | # GhostDoc plugin setting file 273 | *.GhostDoc.xml 274 | 275 | # Node.js Tools for Visual Studio 276 | .ntvs_analysis.dat 277 | node_modules/ 278 | 279 | # Visual Studio 6 build log 280 | *.plg 281 | 282 | # Visual Studio 6 workspace options file 283 | *.opt 284 | 285 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 286 | *.vbw 287 | 288 | # Visual Studio LightSwitch build output 289 | **/*.HTMLClient/GeneratedArtifacts 290 | **/*.DesktopClient/GeneratedArtifacts 291 | **/*.DesktopClient/ModelManifest.xml 292 | **/*.Server/GeneratedArtifacts 293 | **/*.Server/ModelManifest.xml 294 | _Pvt_Extensions 295 | 296 | # Paket dependency manager 297 | .paket/paket.exe 298 | paket-files/ 299 | 300 | # FAKE - F# Make 301 | .fake/ 302 | 303 | # CodeRush personal settings 304 | .cr/personal 305 | 306 | # Python Tools for Visual Studio (PTVS) 307 | __pycache__/ 308 | *.pyc 309 | 310 | # Cake - Uncomment if you are using it 311 | # tools/** 312 | # !tools/packages.config 313 | 314 | # Tabs Studio 315 | *.tss 316 | 317 | # Telerik's JustMock configuration file 318 | *.jmconfig 319 | 320 | # BizTalk build output 321 | *.btp.cs 322 | *.btm.cs 323 | *.odx.cs 324 | *.xsd.cs 325 | 326 | # OpenCover UI analysis results 327 | OpenCover/ 328 | 329 | # Azure Stream Analytics local run output 330 | ASALocalRun/ 331 | 332 | # MSBuild Binary and Structured Log 333 | *.binlog 334 | 335 | # NVidia Nsight GPU debugger configuration file 336 | *.nvuser 337 | 338 | # MFractors (Xamarin productivity tool) working folder 339 | .mfractor/ 340 | 341 | # Local History for Visual Studio 342 | .localhistory/ 343 | 344 | # BeatPulse healthcheck temp database 345 | healthchecksdb 346 | 347 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 348 | MigrationBackup/ 349 | 350 | # dotnet tool local install directory 351 | .store/ 352 | 353 | # mac-created file to track user view preferences for a directory 354 | .DS_Store 355 | 356 | # Analysis results 357 | *.sarif 358 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AArnott/CSharpIsNull/e29603ae4a24d6db86811135401ad5905a87d593/.prettierrc.yaml -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | // List of extensions which should be recommended for users of this workspace. 5 | "recommendations": [ 6 | "ms-azure-devops.azure-pipelines", 7 | "ms-dotnettools.csharp", 8 | "k--kato.docomment", 9 | "editorconfig.editorconfig", 10 | "esbenp.prettier-vscode", 11 | "pflannery.vscode-versionlens", 12 | "davidanson.vscode-markdownlint", 13 | "dotjoshjohnson.xml", 14 | "ms-vscode-remote.remote-containers", 15 | "ms-azuretools.vscode-docker", 16 | "tintoy.msbuild-project-tools" 17 | ], 18 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 19 | "unwantedRecommendations": [] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Attach", 9 | "type": "coreclr", 10 | "request": "attach", 11 | "processId": "${command:pickProcess}" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.trimTrailingWhitespace": true, 3 | "files.insertFinalNewline": true, 4 | "files.trimFinalNewlines": true, 5 | "omnisharp.enableEditorConfigSupport": true, 6 | "omnisharp.enableRoslynAnalyzers": true, 7 | "dotnet.completion.showCompletionItemsFromUnimportedNamespaces": true, 8 | "editor.formatOnSave": true, 9 | "[xml]": { 10 | "editor.wordWrap": "off" 11 | }, 12 | // Treat these files as Azure Pipelines files 13 | "files.associations": { 14 | "**/azure-pipelines/**/*.yml": "azure-pipelines", 15 | "azure-pipelines.yml": "azure-pipelines" 16 | }, 17 | // Use Prettier as the default formatter for Azure Pipelines files. 18 | // Needs to be explicitly configured: https://github.com/Microsoft/azure-pipelines-vscode#document-formatting 19 | "[azure-pipelines]": { 20 | "editor.defaultFormatter": "esbenp.prettier-vscode", 21 | "editor.formatOnSave": false // enable this when they conform 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project has adopted the [Microsoft Open Source Code of 4 | Conduct](https://opensource.microsoft.com/codeofconduct/). 5 | For more information see the [Code of Conduct 6 | FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 7 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) 8 | with any additional questions or comments. 9 | 10 | ## Best practices 11 | 12 | * Use Windows PowerShell or [PowerShell Core][pwsh] (including on Linux/OSX) to run .ps1 scripts. 13 | Some scripts set environment variables to help you, but they are only retained if you use PowerShell as your shell. 14 | 15 | ## Prerequisites 16 | 17 | All dependencies can be installed by running the `init.ps1` script at the root of the repository 18 | using Windows PowerShell or [PowerShell Core][pwsh] (on any OS). 19 | Some dependencies installed by `init.ps1` may only be discoverable from the same command line environment the init script was run from due to environment variables, so be sure to launch Visual Studio or build the repo from that same environment. 20 | Alternatively, run `init.ps1 -InstallLocality Machine` (which may require elevation) in order to install dependencies at machine-wide locations so Visual Studio and builds work everywhere. 21 | 22 | The only prerequisite for building, testing, and deploying from this repository 23 | is the [.NET SDK](https://get.dot.net/). 24 | You should install the version specified in `global.json` or a later version within 25 | the same major.minor.Bxx "hundreds" band. 26 | For example if 2.2.300 is specified, you may install 2.2.300, 2.2.301, or 2.2.310 27 | while the 2.2.400 version would not be considered compatible by .NET SDK. 28 | See [.NET Core Versioning](https://docs.microsoft.com/dotnet/core/versions/) for more information. 29 | 30 | ## Package restore 31 | 32 | The easiest way to restore packages may be to run `init.ps1` which automatically authenticates 33 | to the feeds that packages for this repo come from, if any. 34 | `dotnet restore` or `nuget restore` also work but may require extra steps to authenticate to any applicable feeds. 35 | 36 | ## Building 37 | 38 | This repository can be built on Windows, Linux, and OSX. 39 | 40 | Building, testing, and packing this repository can be done by using the standard dotnet CLI commands (e.g. `dotnet build`, `dotnet test`, `dotnet pack`, etc.). 41 | 42 | [pwsh]: https://docs.microsoft.com/powershell/scripting/install/installing-powershell?view=powershell-6 43 | -------------------------------------------------------------------------------- /CSharpIsNullAnalyzer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31620.382 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1CE9670B-D5FF-46A7-9D00-24E70E6ED48B}" 7 | ProjectSection(SolutionItems) = preProject 8 | .editorconfig = .editorconfig 9 | Directory.Build.props = Directory.Build.props 10 | Directory.Build.targets = Directory.Build.targets 11 | Directory.Packages.props = Directory.Packages.props 12 | global.json = global.json 13 | nuget.config = nuget.config 14 | README.md = README.md 15 | stylecop.json = stylecop.json 16 | version.json = version.json 17 | EndProjectSection 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9E154A29-1796-4B85-BD81-B6A385D8FF71}" 20 | ProjectSection(SolutionItems) = preProject 21 | src\.editorconfig = src\.editorconfig 22 | src\Directory.Build.props = src\Directory.Build.props 23 | src\Directory.Build.targets = src\Directory.Build.targets 24 | EndProjectSection 25 | EndProject 26 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{36CCE840-6FE5-4DB9-A8D5-8CF3CB6D342A}" 27 | ProjectSection(SolutionItems) = preProject 28 | test\.editorconfig = test\.editorconfig 29 | test\Directory.Build.props = test\Directory.Build.props 30 | test\Directory.Build.targets = test\Directory.Build.targets 31 | EndProjectSection 32 | EndProject 33 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DF4CAFE5-2F72-4227-A4B3-9446049DAFC8}" 34 | EndProject 35 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSharpIsNullAnalyzer", "src\CSharpIsNullAnalyzer\CSharpIsNullAnalyzer.csproj", "{25873F90-8908-4E3D-A0B4-F668217A1653}" 36 | EndProject 37 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{44275BDE-734A-4B65-A8C5-61608C892A76}" 38 | EndProject 39 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSharpIsNullAnalyzer.Tests", "test\CSharpIsNullAnalyzer.Tests\CSharpIsNullAnalyzer.Tests.csproj", "{9CA85BC1-5565-4498-928D-F2685D337B71}" 40 | EndProject 41 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSharpIsNullAnalyzer.CodeFixes", "src\CSharpIsNullAnalyzer.CodeFixes\CSharpIsNullAnalyzer.CodeFixes.csproj", "{A8C3211C-32CB-41EE-8D9A-384D0F1F6947}" 42 | EndProject 43 | Global 44 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 45 | Debug|Any CPU = Debug|Any CPU 46 | Release|Any CPU = Release|Any CPU 47 | EndGlobalSection 48 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 49 | {25873F90-8908-4E3D-A0B4-F668217A1653}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {25873F90-8908-4E3D-A0B4-F668217A1653}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {25873F90-8908-4E3D-A0B4-F668217A1653}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {25873F90-8908-4E3D-A0B4-F668217A1653}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {9CA85BC1-5565-4498-928D-F2685D337B71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {9CA85BC1-5565-4498-928D-F2685D337B71}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {9CA85BC1-5565-4498-928D-F2685D337B71}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {9CA85BC1-5565-4498-928D-F2685D337B71}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {A8C3211C-32CB-41EE-8D9A-384D0F1F6947}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {A8C3211C-32CB-41EE-8D9A-384D0F1F6947}.Debug|Any CPU.Build.0 = Debug|Any CPU 59 | {A8C3211C-32CB-41EE-8D9A-384D0F1F6947}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {A8C3211C-32CB-41EE-8D9A-384D0F1F6947}.Release|Any CPU.Build.0 = Release|Any CPU 61 | EndGlobalSection 62 | GlobalSection(SolutionProperties) = preSolution 63 | HideSolutionNode = FALSE 64 | EndGlobalSection 65 | GlobalSection(NestedProjects) = preSolution 66 | {9E154A29-1796-4B85-BD81-B6A385D8FF71} = {1CE9670B-D5FF-46A7-9D00-24E70E6ED48B} 67 | {36CCE840-6FE5-4DB9-A8D5-8CF3CB6D342A} = {1CE9670B-D5FF-46A7-9D00-24E70E6ED48B} 68 | {25873F90-8908-4E3D-A0B4-F668217A1653} = {DF4CAFE5-2F72-4227-A4B3-9446049DAFC8} 69 | {9CA85BC1-5565-4498-928D-F2685D337B71} = {44275BDE-734A-4B65-A8C5-61608C892A76} 70 | {A8C3211C-32CB-41EE-8D9A-384D0F1F6947} = {DF4CAFE5-2F72-4227-A4B3-9446049DAFC8} 71 | EndGlobalSection 72 | GlobalSection(ExtensibilityGlobals) = postSolution 73 | SolutionGuid = {E3944F6A-384B-4B0F-B93F-3BD513DC57BD} 74 | EndGlobalSection 75 | EndGlobal 76 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | $(MSBuildThisFileDirectory) 6 | $(RepoRootPath)obj\$([MSBuild]::MakeRelative($(RepoRootPath), $(MSBuildProjectDirectory)))\ 7 | $(RepoRootPath)bin\$(MSBuildProjectName)\ 8 | $(RepoRootPath)bin\Packages\$(Configuration)\ 9 | enable 10 | enable 11 | latest 12 | true 13 | true 14 | true 15 | 16 | 17 | true 18 | 19 | 20 | 21 | false 22 | 23 | 24 | $(MSBuildThisFileDirectory) 25 | 26 | 27 | embedded 28 | 29 | true 30 | $(MSBuildThisFileDirectory)strongname.snk 31 | 32 | https://github.com/AArnott/CSharpIsNull 33 | Andrew Arnott 34 | Andrew Arnott 35 | © Andrew Arnott. All rights reserved. 36 | MIT 37 | true 38 | true 39 | true 40 | snupkg 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | $(PackageProjectUrl)/releases/tag/v$(Version) 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /Directory.Build.rsp: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # This file contains command-line options that MSBuild will process as part of 3 | # every build, unless the "/noautoresponse" switch is specified. 4 | # 5 | # MSBuild processes the options in this file first, before processing the 6 | # options on the command line. As a result, options on the command line can 7 | # override the options in this file. However, depending on the options being 8 | # set, the overriding can also result in conflicts. 9 | # 10 | # NOTE: The "/noautoresponse" switch cannot be specified in this file, nor in 11 | # any response file that is referenced by this file. 12 | #------------------------------------------------------------------------------ 13 | /nr:false 14 | /m 15 | /verbosity:minimal 16 | /clp:Summary;ForceNoAlign 17 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 5 | 16.9 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | true 7 | 8 | 3.7.0 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Andrew Arnott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # C# null test syntax analyzers 2 | 3 | [![NuGet package](https://img.shields.io/nuget/v/CSharpIsNullAnalyzer.svg)](https://nuget.org/packages/CSharpIsNullAnalyzer) 4 | [![NuGet package](https://img.shields.io/badge/nuget-From%20CI-yellow)](https://dev.azure.com/andrewarnott/OSS/_packaging?_a=package&feed=PublicCI&package=CSharpIsNullAnalyzer&version=0.1.278-beta&protocolType=NuGet) 5 | [![Build Status](https://dev.azure.com/andrewarnott/OSS/_apis/build/status/CSharpIsNull?branchName=main)](https://dev.azure.com/andrewarnott/OSS/_build/latest?definitionId=54&branchName=main) 6 | 7 | ## Features 8 | 9 | * Guard against bugs from testing structs against `null`. 10 | * Bulk code fix will update all your code at once. 11 | 12 | ### Analyzers 13 | 14 | * [CSIsNull001](doc/analyzers/CSIsNull001.md) to catch uses of `== null` 15 | * [CSIsNull002](doc/analyzers/CSIsNull002.md) to catch uses of `!= null` 16 | 17 | ## Consumption 18 | 19 | Install it via NuGet through the nuget badge at the top of this file. 20 | 21 | ### Consume from CI 22 | 23 | To get the very latest analyzer [from my CI feed](https://dev.azure.com/andrewarnott/OSS/_packaging?_a=package&feed=PublicCI&package=CSharpIsNullAnalyzer&protocolType=NuGet): 24 | 25 | [Connect to the feed](https://dev.azure.com/andrewarnott/OSS/_packaging?_a=connect&feed=PublicCI): 26 | 27 | ```xml 28 | 29 | ``` 30 | 31 | Then install the package with this command in your Package Manager Console: 32 | 33 | ```ps1 34 | Install-Package CSharpIsNullAnalyzer -pre 35 | ``` 36 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | batch: true 3 | branches: 4 | include: 5 | - main 6 | - 'validate/*' 7 | paths: 8 | exclude: 9 | - doc/ 10 | - '*.md' 11 | - .vscode/ 12 | - .github/ 13 | - azure-pipelines/release.yml 14 | 15 | parameters: 16 | - name: RunTests 17 | displayName: Run tests 18 | type: boolean 19 | default: true 20 | 21 | variables: 22 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 23 | BuildConfiguration: Release 24 | codecov_token: 4cea7d85-5a0a-458e-beca-260c04190f2b 25 | ci_feed: https://pkgs.dev.azure.com/andrewarnott/OSS/_packaging/PublicCI/nuget/v3/index.json 26 | NUGET_PACKAGES: $(Agent.TempDirectory)/.nuget/packages/ 27 | 28 | jobs: 29 | - template: azure-pipelines/build.yml 30 | parameters: 31 | RunTests: ${{ parameters.RunTests }} 32 | -------------------------------------------------------------------------------- /azure-pipelines/Get-ArtifactsStagingDirectory.ps1: -------------------------------------------------------------------------------- 1 | Param( 2 | [switch]$CleanIfLocal 3 | ) 4 | if ($env:BUILD_ARTIFACTSTAGINGDIRECTORY) { 5 | $ArtifactStagingFolder = $env:BUILD_ARTIFACTSTAGINGDIRECTORY 6 | } elseif ($env:RUNNER_TEMP) { 7 | $ArtifactStagingFolder = "$env:RUNNER_TEMP\_artifacts" 8 | } else { 9 | $ArtifactStagingFolder = [System.IO.Path]::GetFullPath("$PSScriptRoot/../obj/_artifacts") 10 | if ($CleanIfLocal -and (Test-Path $ArtifactStagingFolder)) { 11 | Remove-Item $ArtifactStagingFolder -Recurse -Force 12 | } 13 | } 14 | 15 | $ArtifactStagingFolder 16 | -------------------------------------------------------------------------------- /azure-pipelines/Get-CodeCovTool.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Downloads the CodeCov.io uploader tool and returns the path to it. 4 | .PARAMETER AllowSkipVerify 5 | Allows skipping signature verification of the downloaded tool if gpg is not installed. 6 | #> 7 | [CmdletBinding()] 8 | Param( 9 | [switch]$AllowSkipVerify 10 | ) 11 | 12 | if ($IsMacOS) { 13 | $codeCovUrl = "https://uploader.codecov.io/latest/macos/codecov" 14 | $toolName = 'codecov' 15 | } 16 | elseif ($IsLinux) { 17 | $codeCovUrl = "https://uploader.codecov.io/latest/linux/codecov" 18 | $toolName = 'codecov' 19 | } 20 | else { 21 | $codeCovUrl = "https://uploader.codecov.io/latest/windows/codecov.exe" 22 | $toolName = 'codecov.exe' 23 | } 24 | 25 | $shaSuffix = ".SHA256SUM" 26 | $sigSuffix = $shaSuffix + ".sig" 27 | 28 | Function Get-FileFromWeb([Uri]$Uri, $OutDir) { 29 | $OutFile = Join-Path $OutDir $Uri.Segments[-1] 30 | if (!(Test-Path $OutFile)) { 31 | Write-Verbose "Downloading $Uri..." 32 | if (!(Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir | Out-Null } 33 | try { 34 | (New-Object System.Net.WebClient).DownloadFile($Uri, $OutFile) 35 | } finally { 36 | # This try/finally causes the script to abort 37 | } 38 | } 39 | 40 | $OutFile 41 | } 42 | 43 | $toolsPath = & "$PSScriptRoot\Get-TempToolsPath.ps1" 44 | $binaryToolsPath = Join-Path $toolsPath codecov 45 | $testingPath = Join-Path $binaryToolsPath unverified 46 | $finalToolPath = Join-Path $binaryToolsPath $toolName 47 | 48 | if (!(Test-Path $finalToolPath)) { 49 | if (Test-Path $testingPath) { 50 | Remove-Item -Recurse -Force $testingPath # ensure we download all matching files 51 | } 52 | $tool = Get-FileFromWeb $codeCovUrl $testingPath 53 | $sha = Get-FileFromWeb "$codeCovUrl$shaSuffix" $testingPath 54 | $sig = Get-FileFromWeb "$codeCovUrl$sigSuffix" $testingPath 55 | $key = Get-FileFromWeb https://keybase.io/codecovsecurity/pgp_keys.asc $testingPath 56 | 57 | if ((Get-Command gpg -ErrorAction SilentlyContinue)) { 58 | Write-Host "Importing codecov key" -ForegroundColor Yellow 59 | gpg --import $key 60 | Write-Host "Verifying signature on codecov hash" -ForegroundColor Yellow 61 | gpg --verify $sig $sha 62 | } else { 63 | if ($AllowSkipVerify) { 64 | Write-Warning "gpg not found. Unable to verify hash signature." 65 | } else { 66 | throw "gpg not found. Unable to verify hash signature. Install gpg or add -AllowSkipVerify to override." 67 | } 68 | } 69 | 70 | Write-Host "Verifying hash on downloaded tool" -ForegroundColor Yellow 71 | $actualHash = (Get-FileHash -Path $tool -Algorithm SHA256).Hash 72 | $expectedHash = (Get-Content $sha).Split()[0] 73 | if ($actualHash -ne $expectedHash) { 74 | # Validation failed. Delete the tool so we can't execute it. 75 | #Remove-Item $codeCovPath 76 | throw "codecov uploader tool failed signature validation." 77 | } 78 | 79 | Copy-Item $tool $finalToolPath 80 | 81 | if ($IsMacOS -or $IsLinux) { 82 | chmod u+x $finalToolPath 83 | } 84 | } 85 | 86 | return $finalToolPath 87 | -------------------------------------------------------------------------------- /azure-pipelines/Get-NuGetTool.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Downloads the NuGet.exe tool and returns the path to it. 4 | .PARAMETER NuGetVersion 5 | The version of the NuGet tool to acquire. 6 | #> 7 | Param( 8 | [Parameter()] 9 | [string]$NuGetVersion='6.4.0' 10 | ) 11 | 12 | $toolsPath = & "$PSScriptRoot\Get-TempToolsPath.ps1" 13 | $binaryToolsPath = Join-Path $toolsPath $NuGetVersion 14 | if (!(Test-Path $binaryToolsPath)) { $null = mkdir $binaryToolsPath } 15 | $nugetPath = Join-Path $binaryToolsPath nuget.exe 16 | 17 | if (!(Test-Path $nugetPath)) { 18 | Write-Host "Downloading nuget.exe $NuGetVersion..." -ForegroundColor Yellow 19 | (New-Object System.Net.WebClient).DownloadFile("https://dist.nuget.org/win-x86-commandline/v$NuGetVersion/NuGet.exe", $nugetPath) 20 | } 21 | 22 | return (Resolve-Path $nugetPath).Path 23 | -------------------------------------------------------------------------------- /azure-pipelines/Get-ProcDump.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Downloads 32-bit and 64-bit procdump executables and returns the path to where they were installed. 4 | #> 5 | $version = '0.0.1' 6 | $baseDir = "$PSScriptRoot\..\obj\tools" 7 | $procDumpToolPath = "$baseDir\procdump.$version\bin" 8 | if (-not (Test-Path $procDumpToolPath)) { 9 | if (-not (Test-Path $baseDir)) { New-Item -Type Directory -Path $baseDir | Out-Null } 10 | $baseDir = (Resolve-Path $baseDir).Path # Normalize it 11 | & (& $PSScriptRoot\Get-NuGetTool.ps1) install procdump -version $version -PackageSaveMode nuspec -OutputDirectory $baseDir -Source https://api.nuget.org/v3/index.json | Out-Null 12 | } 13 | 14 | (Resolve-Path $procDumpToolPath).Path 15 | -------------------------------------------------------------------------------- /azure-pipelines/Get-SymbolFiles.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Collect the list of PDBs built in this repo. 4 | .PARAMETER Path 5 | The directory to recursively search for PDBs. 6 | .PARAMETER Tests 7 | A switch indicating to find PDBs only for test binaries instead of only for shipping shipping binaries. 8 | #> 9 | [CmdletBinding()] 10 | param ( 11 | [parameter(Mandatory=$true)] 12 | [string]$Path, 13 | [switch]$Tests 14 | ) 15 | 16 | $ActivityName = "Collecting symbols from $Path" 17 | Write-Progress -Activity $ActivityName -CurrentOperation "Discovery PDB files" 18 | $PDBs = Get-ChildItem -rec "$Path/*.pdb" 19 | 20 | # Filter PDBs to product OR test related. 21 | $testregex = "unittest|tests|\.test\." 22 | 23 | Write-Progress -Activity $ActivityName -CurrentOperation "De-duplicating symbols" 24 | $PDBsByHash = @{} 25 | $i = 0 26 | $PDBs |% { 27 | Write-Progress -Activity $ActivityName -CurrentOperation "De-duplicating symbols" -PercentComplete (100 * $i / $PDBs.Length) 28 | $hash = Get-FileHash $_ 29 | $i++ 30 | Add-Member -InputObject $_ -MemberType NoteProperty -Name Hash -Value $hash.Hash 31 | Write-Output $_ 32 | } | Sort-Object CreationTime |% { 33 | # De-dupe based on hash. Prefer the first match so we take the first built copy. 34 | if (-not $PDBsByHash.ContainsKey($_.Hash)) { 35 | $PDBsByHash.Add($_.Hash, $_.FullName) 36 | Write-Output $_ 37 | } 38 | } |? { 39 | if ($Tests) { 40 | $_.FullName -match $testregex 41 | } else { 42 | $_.FullName -notmatch $testregex 43 | } 44 | } |% { 45 | # Collect the DLLs/EXEs as well. 46 | $rootName = "$($_.Directory)/$($_.BaseName)" 47 | if ($rootName.EndsWith('.ni')) { 48 | $rootName = $rootName.Substring(0, $rootName.Length - 3) 49 | } 50 | 51 | $dllPath = "$rootName.dll" 52 | $exePath = "$rootName.exe" 53 | if (Test-Path $dllPath) { 54 | $BinaryImagePath = $dllPath 55 | } elseif (Test-Path $exePath) { 56 | $BinaryImagePath = $exePath 57 | } else { 58 | Write-Warning "`"$_`" found with no matching binary file." 59 | $BinaryImagePath = $null 60 | } 61 | 62 | if ($BinaryImagePath) { 63 | Write-Output $BinaryImagePath 64 | Write-Output $_.FullName 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /azure-pipelines/Get-TempToolsPath.ps1: -------------------------------------------------------------------------------- 1 | if ($env:AGENT_TEMPDIRECTORY) { 2 | $path = "$env:AGENT_TEMPDIRECTORY\$env:BUILD_BUILDID" 3 | } elseif ($env:localappdata) { 4 | $path = "$env:localappdata\gitrepos\tools" 5 | } else { 6 | $path = "$PSScriptRoot\..\obj\tools" 7 | } 8 | 9 | if (!(Test-Path $path)) { 10 | New-Item -ItemType Directory -Path $Path | Out-Null 11 | } 12 | 13 | (Resolve-Path $path).Path 14 | -------------------------------------------------------------------------------- /azure-pipelines/Merge-CodeCoverage.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | <# 4 | .SYNOPSIS 5 | Merges code coverage reports. 6 | .PARAMETER Path 7 | The path(s) to search for Cobertura code coverage reports. 8 | .PARAMETER Format 9 | The format for the merged result. The default is Cobertura 10 | .PARAMETER OutputDir 11 | The directory the merged result will be written to. The default is `coveragereport` in the root of this repo. 12 | #> 13 | [CmdletBinding()] 14 | Param( 15 | [Parameter(Mandatory=$true)] 16 | [string[]]$Path, 17 | [ValidateSet('Badges', 'Clover', 'Cobertura', 'CsvSummary', 'Html', 'Html_Dark', 'Html_Light', 'HtmlChart', 'HtmlInline', 'HtmlInline_AzurePipelines', 'HtmlInline_AzurePipelines_Dark', 'HtmlInline_AzurePipelines_Light', 'HtmlSummary', 'JsonSummary', 'Latex', 'LatexSummary', 'lcov', 'MarkdownSummary', 'MHtml', 'PngChart', 'SonarQube', 'TeamCitySummary', 'TextSummary', 'Xml', 'XmlSummary')] 18 | [string]$Format='Cobertura', 19 | [string]$OutputFile=("$PSScriptRoot/../coveragereport/merged.cobertura.xml") 20 | ) 21 | 22 | $RepoRoot = [string](Resolve-Path $PSScriptRoot/..) 23 | Push-Location $RepoRoot 24 | try { 25 | Write-Verbose "Searching $Path for *.cobertura.xml files" 26 | $reports = Get-ChildItem -Recurse $Path -Filter *.cobertura.xml 27 | 28 | if ($reports) { 29 | $reports |% { $_.FullName } |% { 30 | # In addition to replacing {reporoot}, we also normalize on one kind of slash so that the report aggregates data for a file whether data was collected on Windows or not. 31 | $xml = [xml](Get-Content -Path $_) 32 | $xml.coverage.packages.package.classes.class |? { $_.filename} |% { 33 | $_.filename = $_.filename.Replace('{reporoot}', $RepoRoot).Replace([IO.Path]::AltDirectorySeparatorChar, [IO.Path]::DirectorySeparatorChar) 34 | } 35 | 36 | $xml.Save($_) 37 | } 38 | 39 | $Inputs = $reports |% { Resolve-Path -relative $_.FullName } 40 | 41 | if ((Split-Path $OutputFile) -and -not (Test-Path (Split-Path $OutputFile))) { 42 | New-Item -Type Directory -Path (Split-Path $OutputFile) | Out-Null 43 | } 44 | 45 | & dotnet tool run dotnet-coverage merge $Inputs -o $OutputFile -f cobertura 46 | } else { 47 | Write-Error "No reports found to merge." 48 | } 49 | } finally { 50 | Pop-Location 51 | } 52 | -------------------------------------------------------------------------------- /azure-pipelines/artifacts/Variables.ps1: -------------------------------------------------------------------------------- 1 | # This artifact captures all variables defined in the ..\variables folder. 2 | # It "snaps" the values of these variables where we can compute them during the build, 3 | # and otherwise captures the scripts to run later during an Azure Pipelines environment release. 4 | 5 | $RepoRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot/../..") 6 | $ArtifactBasePath = "$RepoRoot/obj/_artifacts" 7 | $VariablesArtifactPath = Join-Path $ArtifactBasePath variables 8 | if (-not (Test-Path $VariablesArtifactPath)) { New-Item -ItemType Directory -Path $VariablesArtifactPath | Out-Null } 9 | 10 | # Copy variables, either by value if the value is calculable now, or by script 11 | Get-ChildItem "$PSScriptRoot/../variables" |% { 12 | $value = $null 13 | if (-not $_.BaseName.StartsWith('_')) { # Skip trying to interpret special scripts 14 | # First check the environment variables in case the variable was set in a queued build 15 | # Always use all caps for env var access because Azure Pipelines converts variables to upper-case for env vars, 16 | # and on non-Windows env vars are case sensitive. 17 | $envVarName = $_.BaseName.ToUpper() 18 | if (Test-Path env:$envVarName) { 19 | $value = Get-Content "env:$envVarName" 20 | } 21 | 22 | # If that didn't give us anything, try executing the script right now from its original position 23 | if (-not $value) { 24 | $value = & $_.FullName 25 | } 26 | 27 | if ($value) { 28 | # We got something, so wrap it with quotes so it's treated like a literal value. 29 | $value = "'$value'" 30 | } 31 | } 32 | 33 | # If that didn't get us anything, just copy the script itself 34 | if (-not $value) { 35 | $value = Get-Content -Path $_.FullName 36 | } 37 | 38 | Set-Content -Path "$VariablesArtifactPath/$($_.Name)" -Value $value 39 | } 40 | 41 | @{ 42 | "$VariablesArtifactPath" = (Get-ChildItem $VariablesArtifactPath -Recurse); 43 | } 44 | -------------------------------------------------------------------------------- /azure-pipelines/artifacts/_all.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | <# 4 | .SYNOPSIS 5 | This script returns all the artifacts that should be collected after a build. 6 | Each powershell artifact is expressed as an object with these properties: 7 | Source - the full path to the source file 8 | ArtifactName - the name of the artifact to upload to 9 | ContainerFolder - the relative path within the artifact in which the file should appear 10 | Each artifact aggregating .ps1 script should return a hashtable: 11 | Key = path to the directory from which relative paths within the artifact should be calculated 12 | Value = an array of paths (absolute or relative to the BaseDirectory) to files to include in the artifact. 13 | FileInfo objects are also allowed. 14 | .PARAMETER Force 15 | Executes artifact scripts even if they have already been staged. 16 | #> 17 | 18 | [CmdletBinding(SupportsShouldProcess = $true)] 19 | param ( 20 | [string]$ArtifactNameSuffix, 21 | [switch]$Force 22 | ) 23 | 24 | Function EnsureTrailingSlash($path) { 25 | if ($path.length -gt 0 -and !$path.EndsWith('\') -and !$path.EndsWith('/')) { 26 | $path = $path + [IO.Path]::DirectorySeparatorChar 27 | } 28 | 29 | $path.Replace('\', [IO.Path]::DirectorySeparatorChar) 30 | } 31 | 32 | Function Test-ArtifactStaged($artifactName) { 33 | $varName = "ARTIFACTSTAGED_$($artifactName.ToUpper())" 34 | Test-Path "env:$varName" 35 | } 36 | 37 | Get-ChildItem "$PSScriptRoot\*.ps1" -Exclude "_*" -Recurse | % { 38 | $ArtifactName = $_.BaseName 39 | if ($Force -or !(Test-ArtifactStaged($ArtifactName + $ArtifactNameSuffix))) { 40 | $totalFileCount = 0 41 | Write-Verbose "Collecting file list for artifact $($_.BaseName)" 42 | $fileGroups = & $_ 43 | if ($fileGroups) { 44 | $fileGroups.GetEnumerator() | % { 45 | $BaseDirectory = New-Object Uri ((EnsureTrailingSlash $_.Key.ToString()), [UriKind]::Absolute) 46 | $_.Value | ? { $_ } | % { 47 | if ($_.GetType() -eq [IO.FileInfo] -or $_.GetType() -eq [IO.DirectoryInfo]) { 48 | $_ = $_.FullName 49 | } 50 | 51 | $artifact = New-Object -TypeName PSObject 52 | Add-Member -InputObject $artifact -MemberType NoteProperty -Name ArtifactName -Value $ArtifactName 53 | 54 | $SourceFullPath = New-Object Uri ($BaseDirectory, $_) 55 | Add-Member -InputObject $artifact -MemberType NoteProperty -Name Source -Value $SourceFullPath.LocalPath 56 | 57 | $RelativePath = [Uri]::UnescapeDataString($BaseDirectory.MakeRelative($SourceFullPath)) 58 | Add-Member -InputObject $artifact -MemberType NoteProperty -Name ContainerFolder -Value (Split-Path $RelativePath) 59 | 60 | Write-Output $artifact 61 | $totalFileCount += 1 62 | } 63 | } 64 | } 65 | 66 | if ($totalFileCount -eq 0) { 67 | Write-Warning "No files found for the `"$ArtifactName`" artifact." 68 | } 69 | } else { 70 | Write-Host "Skipping $ArtifactName because it has already been staged." -ForegroundColor DarkGray 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /azure-pipelines/artifacts/_pipelines.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script translates all the artifacts described by _all.ps1 4 | into commands that instruct Azure Pipelines to actually collect those artifacts. 5 | #> 6 | 7 | [CmdletBinding()] 8 | param ( 9 | [string]$ArtifactNameSuffix, 10 | [switch]$StageOnly, 11 | [switch]$AvoidSymbolicLinks 12 | ) 13 | 14 | Function Set-PipelineVariable($name, $value) { 15 | if ((Test-Path "Env:\$name") -and (Get-Item "Env:\$name").Value -eq $value) { 16 | return # already set 17 | } 18 | 19 | #New-Item -Path "Env:\$name".ToUpper() -Value $value -Force | Out-Null 20 | Write-Host "##vso[task.setvariable variable=$name]$value" 21 | } 22 | 23 | Function Test-ArtifactUploaded($artifactName) { 24 | $varName = "ARTIFACTUPLOADED_$($artifactName.ToUpper())" 25 | Test-Path "env:$varName" 26 | } 27 | 28 | & "$PSScriptRoot/_stage_all.ps1" -ArtifactNameSuffix $ArtifactNameSuffix -AvoidSymbolicLinks:$AvoidSymbolicLinks |% { 29 | # Set a variable which will out-live this script so that a subsequent attempt to collect and upload artifacts 30 | # will skip this one from a check in the _all.ps1 script. 31 | Set-PipelineVariable "ARTIFACTSTAGED_$($_.Name.ToUpper())" 'true' 32 | Write-Host "Staged artifact $($_.Name) to $($_.Path)" 33 | 34 | if (!$StageOnly) { 35 | if (Test-ArtifactUploaded $_.Name) { 36 | Write-Host "Skipping $($_.Name) because it has already been uploaded." -ForegroundColor DarkGray 37 | } else { 38 | Write-Host "##vso[artifact.upload containerfolder=$($_.Name);artifactname=$($_.Name);]$($_.Path)" 39 | 40 | # Set a variable which will out-live this script so that a subsequent attempt to collect and upload artifacts 41 | # will skip this one from a check in the _all.ps1 script. 42 | Set-PipelineVariable "ARTIFACTUPLOADED_$($_.Name.ToUpper())" 'true' 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /azure-pipelines/artifacts/_stage_all.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script links all the artifacts described by _all.ps1 4 | into a staging directory, reading for uploading to a cloud build artifact store. 5 | It returns a sequence of objects with Name and Path properties. 6 | #> 7 | 8 | [CmdletBinding()] 9 | param ( 10 | [string]$ArtifactNameSuffix, 11 | [switch]$AvoidSymbolicLinks 12 | ) 13 | 14 | $ArtifactStagingFolder = & "$PSScriptRoot/../Get-ArtifactsStagingDirectory.ps1" -CleanIfLocal 15 | 16 | function Create-SymbolicLink { 17 | param ( 18 | $Link, 19 | $Target 20 | ) 21 | 22 | if ($Link -eq $Target) { 23 | return 24 | } 25 | 26 | if (Test-Path $Link) { Remove-Item $Link } 27 | $LinkContainer = Split-Path $Link -Parent 28 | if (!(Test-Path $LinkContainer)) { mkdir $LinkContainer } 29 | if ($IsMacOS -or $IsLinux) { 30 | ln $Target $Link | Out-Null 31 | } else { 32 | cmd /c "mklink `"$Link`" `"$Target`"" | Out-Null 33 | } 34 | 35 | if ($LASTEXITCODE -ne 0) { 36 | # Windows requires admin privileges to create symbolic links 37 | # unless Developer Mode has been enabled. 38 | throw "Failed to create symbolic link at $Link that points to $Target" 39 | } 40 | } 41 | 42 | # Stage all artifacts 43 | $Artifacts = & "$PSScriptRoot\_all.ps1" -ArtifactNameSuffix $ArtifactNameSuffix 44 | $Artifacts |% { 45 | $DestinationFolder = [System.IO.Path]::GetFullPath("$ArtifactStagingFolder/$($_.ArtifactName)$ArtifactNameSuffix/$($_.ContainerFolder)").TrimEnd('\') 46 | $Name = "$(Split-Path $_.Source -Leaf)" 47 | 48 | #Write-Host "$($_.Source) -> $($_.ArtifactName)\$($_.ContainerFolder)" -ForegroundColor Yellow 49 | 50 | if (-not (Test-Path $DestinationFolder)) { New-Item -ItemType Directory -Path $DestinationFolder | Out-Null } 51 | if (Test-Path -PathType Leaf $_.Source) { # skip folders 52 | $TargetPath = Join-Path $DestinationFolder $Name 53 | if ($AvoidSymbolicLinks) { 54 | Copy-Item -Path $_.Source -Destination $TargetPath 55 | } else { 56 | Create-SymbolicLink -Link $TargetPath -Target $_.Source 57 | } 58 | } 59 | } 60 | 61 | $ArtifactNames = $Artifacts |% { "$($_.ArtifactName)$ArtifactNameSuffix" } 62 | $ArtifactNames += Get-ChildItem env:ARTIFACTSTAGED_* |% { 63 | # Return from ALLCAPS to the actual capitalization used for the artifact. 64 | $artifactNameAllCaps = "$($_.Name.Substring('ARTIFACTSTAGED_'.Length))" 65 | (Get-ChildItem $ArtifactStagingFolder\$artifactNameAllCaps* -Filter $artifactNameAllCaps).Name 66 | } 67 | $ArtifactNames | Get-Unique |% { 68 | $artifact = New-Object -TypeName PSObject 69 | Add-Member -InputObject $artifact -MemberType NoteProperty -Name Name -Value $_ 70 | Add-Member -InputObject $artifact -MemberType NoteProperty -Name Path -Value (Join-Path $ArtifactStagingFolder $_) 71 | Write-Output $artifact 72 | } 73 | -------------------------------------------------------------------------------- /azure-pipelines/artifacts/build_logs.ps1: -------------------------------------------------------------------------------- 1 | $ArtifactStagingFolder = & "$PSScriptRoot/../Get-ArtifactsStagingDirectory.ps1" 2 | 3 | if (!(Test-Path $ArtifactStagingFolder/build_logs)) { return } 4 | 5 | @{ 6 | "$ArtifactStagingFolder/build_logs" = (Get-ChildItem -Recurse "$ArtifactStagingFolder/build_logs") 7 | } 8 | -------------------------------------------------------------------------------- /azure-pipelines/artifacts/coverageResults.ps1: -------------------------------------------------------------------------------- 1 | $RepoRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..") 2 | 3 | $coverageFiles = @(Get-ChildItem "$RepoRoot/test/*.cobertura.xml" -Recurse | Where {$_.FullName -notlike "*/In/*" -and $_.FullName -notlike "*\In\*" }) 4 | 5 | # Prepare code coverage reports for merging on another machine 6 | if ($env:SYSTEM_DEFAULTWORKINGDIRECTORY) { 7 | Write-Host "Substituting $env:SYSTEM_DEFAULTWORKINGDIRECTORY with `"{reporoot}`"" 8 | $coverageFiles |% { 9 | $content = Get-Content -Path $_ |% { $_ -Replace [regex]::Escape($env:SYSTEM_DEFAULTWORKINGDIRECTORY), "{reporoot}" } 10 | Set-Content -Path $_ -Value $content -Encoding UTF8 11 | } 12 | } else { 13 | Write-Warning "coverageResults: Azure Pipelines not detected. Machine-neutral token replacement skipped." 14 | } 15 | 16 | if (!((Test-Path $RepoRoot\bin) -and (Test-Path $RepoRoot\obj))) { return } 17 | 18 | @{ 19 | $RepoRoot = ( 20 | $coverageFiles + 21 | (Get-ChildItem "$RepoRoot\obj\*.cs" -Recurse) 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /azure-pipelines/artifacts/deployables.ps1: -------------------------------------------------------------------------------- 1 | $RepoRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..") 2 | $BuildConfiguration = $env:BUILDCONFIGURATION 3 | if (!$BuildConfiguration) { 4 | $BuildConfiguration = 'Debug' 5 | } 6 | 7 | $PackagesRoot = "$RepoRoot/bin/Packages/$BuildConfiguration" 8 | 9 | if (!(Test-Path $PackagesRoot)) { return } 10 | 11 | @{ 12 | "$PackagesRoot" = (Get-ChildItem $PackagesRoot -Recurse) 13 | } 14 | -------------------------------------------------------------------------------- /azure-pipelines/artifacts/projectAssetsJson.ps1: -------------------------------------------------------------------------------- 1 | $ObjRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..\obj") 2 | 3 | if (!(Test-Path $ObjRoot)) { return } 4 | 5 | @{ 6 | "$ObjRoot" = ( 7 | (Get-ChildItem "$ObjRoot\project.assets.json" -Recurse) 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /azure-pipelines/artifacts/symbols.ps1: -------------------------------------------------------------------------------- 1 | $BinPath = [System.IO.Path]::GetFullPath("$PSScriptRoot/../../bin") 2 | if (!(Test-Path $BinPath)) { return } 3 | $symbolfiles = & "$PSScriptRoot/../Get-SymbolFiles.ps1" -Path $BinPath | Get-Unique 4 | 5 | @{ 6 | "$BinPath" = $SymbolFiles; 7 | } 8 | -------------------------------------------------------------------------------- /azure-pipelines/artifacts/testResults.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | Param( 3 | ) 4 | 5 | $result = @{} 6 | 7 | $testRoot = Resolve-Path "$PSScriptRoot\..\..\test" 8 | $result[$testRoot] = (Get-ChildItem "$testRoot\TestResults" -Recurse -Directory | Get-ChildItem -Recurse -File) 9 | 10 | $testlogsPath = "$env:BUILD_ARTIFACTSTAGINGDIRECTORY\test_logs" 11 | if (Test-Path $testlogsPath) { 12 | $result[$testlogsPath] = Get-ChildItem "$testlogsPath\*"; 13 | } 14 | 15 | $result 16 | -------------------------------------------------------------------------------- /azure-pipelines/artifacts/test_symbols.ps1: -------------------------------------------------------------------------------- 1 | $BinPath = [System.IO.Path]::GetFullPath("$PSScriptRoot/../../bin") 2 | if (!(Test-Path $BinPath)) { return } 3 | $symbolfiles = & "$PSScriptRoot/../Get-SymbolFiles.ps1" -Path $BinPath -Tests | Get-Unique 4 | 5 | @{ 6 | "$BinPath" = $SymbolFiles; 7 | } 8 | -------------------------------------------------------------------------------- /azure-pipelines/build.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: RunTests 3 | type: boolean 4 | default: true 5 | 6 | jobs: 7 | - job: Linux 8 | pool: 9 | vmImage: Ubuntu 20.04 10 | steps: 11 | - checkout: self 12 | fetchDepth: 0 # avoid shallow clone so nbgv can do its work. 13 | clean: true 14 | - template: install-dependencies.yml 15 | - script: dotnet nbgv cloud -c 16 | displayName: ⚙ Set build number 17 | - template: dotnet.yml 18 | parameters: 19 | RunTests: ${{ parameters.RunTests }} 20 | - script: dotnet format --verify-no-changes --no-restore 21 | displayName: 💅 Verify formatted code 22 | - template: publish-symbols.yml 23 | - ${{ if parameters.RunTests }}: 24 | - template: publish-codecoverage.yml 25 | - template: publish-deployables.yml 26 | -------------------------------------------------------------------------------- /azure-pipelines/dotnet-test-cloud.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | <# 4 | .SYNOPSIS 5 | Runs tests as they are run in cloud test runs. 6 | .PARAMETER Configuration 7 | The configuration within which to run tests 8 | .PARAMETER Agent 9 | The name of the agent. This is used in preparing test run titles. 10 | .PARAMETER PublishResults 11 | A switch to publish results to Azure Pipelines. 12 | .PARAMETER x86 13 | A switch to run the tests in an x86 process. 14 | .PARAMETER dotnet32 15 | The path to a 32-bit dotnet executable to use. 16 | #> 17 | [CmdletBinding()] 18 | Param( 19 | [string]$Configuration='Debug', 20 | [string]$Agent='Local', 21 | [switch]$PublishResults, 22 | [switch]$x86, 23 | [string]$dotnet32 24 | ) 25 | 26 | $RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path 27 | $ArtifactStagingFolder = & "$PSScriptRoot/Get-ArtifactsStagingDirectory.ps1" 28 | 29 | $dotnet = 'dotnet' 30 | if ($x86) { 31 | $x86RunTitleSuffix = ", x86" 32 | if ($dotnet32) { 33 | $dotnet = $dotnet32 34 | } else { 35 | $dotnet32Possibilities = "$PSScriptRoot\../obj/tools/x86/.dotnet/dotnet.exe", "$env:AGENT_TOOLSDIRECTORY/x86/dotnet/dotnet.exe", "${env:ProgramFiles(x86)}\dotnet\dotnet.exe" 36 | $dotnet32Matches = $dotnet32Possibilities |? { Test-Path $_ } 37 | if ($dotnet32Matches) { 38 | $dotnet = Resolve-Path @($dotnet32Matches)[0] 39 | Write-Host "Running tests using `"$dotnet`"" -ForegroundColor DarkGray 40 | } else { 41 | Write-Error "Unable to find 32-bit dotnet.exe" 42 | return 1 43 | } 44 | } 45 | } 46 | 47 | & $dotnet test $RepoRoot ` 48 | --no-build ` 49 | -c $Configuration ` 50 | --filter "TestCategory!=FailsInCloudTest" ` 51 | --collect "Code Coverage;Format=cobertura" ` 52 | --settings "$PSScriptRoot/test.runsettings" ` 53 | --blame-hang-timeout 60s ` 54 | --blame-crash ` 55 | -bl:"$ArtifactStagingFolder/build_logs/test.binlog" ` 56 | --diag "$ArtifactStagingFolder/test_logs/diag.log;TraceLevel=info" ` 57 | --logger trx ` 58 | 59 | $unknownCounter = 0 60 | Get-ChildItem -Recurse -Path $RepoRoot\test\*.trx |% { 61 | Copy-Item $_ -Destination $ArtifactStagingFolder/test_logs/ 62 | 63 | if ($PublishResults) { 64 | $x = [xml](Get-Content -Path $_) 65 | $runTitle = $null 66 | if ($x.TestRun.TestDefinitions -and $x.TestRun.TestDefinitions.GetElementsByTagName('UnitTest')) { 67 | $storage = $x.TestRun.TestDefinitions.GetElementsByTagName('UnitTest')[0].storage -replace '\\','/' 68 | if ($storage -match '/(?net[^/]+)/(?:(?[^/]+)/)?(?[^/]+)\.dll$') { 69 | if ($matches.rid) { 70 | $runTitle = "$($matches.lib) ($($matches.tfm), $($matches.rid), $Agent)" 71 | } else { 72 | $runTitle = "$($matches.lib) ($($matches.tfm)$x86RunTitleSuffix, $Agent)" 73 | } 74 | } 75 | } 76 | if (!$runTitle) { 77 | $unknownCounter += 1; 78 | $runTitle = "unknown$unknownCounter ($Agent$x86RunTitleSuffix)"; 79 | } 80 | 81 | Write-Host "##vso[results.publish type=VSTest;runTitle=$runTitle;publishRunAttachments=true;resultFiles=$_;failTaskOnFailedTests=true;testRunSystem=VSTS - PTR;]" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /azure-pipelines/dotnet.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | RunTests: 3 | 4 | steps: 5 | 6 | - script: dotnet build -t:build,pack --no-restore -c $(BuildConfiguration) -warnaserror /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog" 7 | displayName: 🛠 dotnet build 8 | 9 | - powershell: azure-pipelines/dotnet-test-cloud.ps1 -Configuration $(BuildConfiguration) -Agent $(Agent.JobName) -PublishResults 10 | displayName: 🧪 dotnet test 11 | condition: and(succeeded(), ${{ parameters.RunTests }}) 12 | 13 | - powershell: azure-pipelines/variables/_pipelines.ps1 14 | failOnStderr: true 15 | displayName: ⚙ Update pipeline variables based on build outputs 16 | condition: succeededOrFailed() 17 | 18 | - powershell: azure-pipelines/artifacts/_pipelines.ps1 -ArtifactNameSuffix "-$(Agent.JobName)" -Verbose 19 | failOnStderr: true 20 | displayName: 📢 Publish artifacts 21 | condition: succeededOrFailed() 22 | 23 | - ${{ if and(ne(variables['codecov_token'], ''), parameters.RunTests) }}: 24 | - powershell: | 25 | $ArtifactStagingFolder = & "azure-pipelines/Get-ArtifactsStagingDirectory.ps1" 26 | $CoverageResultsFolder = Join-Path $ArtifactStagingFolder "coverageResults-$(Agent.JobName)" 27 | azure-pipelines/publish-CodeCov.ps1 -CodeCovToken "$(codecov_token)" -PathToCodeCoverage "$CoverageResultsFolder" -Name "$(Agent.JobName) Coverage Results" -Flags "$(Agent.JobName)Host,$(BuildConfiguration)" 28 | displayName: 📢 Publish code coverage results to codecov.io 29 | timeoutInMinutes: 3 30 | continueOnError: true 31 | -------------------------------------------------------------------------------- /azure-pipelines/install-dependencies.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | initArgs: 3 | 4 | steps: 5 | 6 | - task: NuGetAuthenticate@1 7 | displayName: 🔏 Authenticate NuGet feeds 8 | inputs: 9 | forceReinstallCredentialProvider: true 10 | 11 | - powershell: | 12 | $AccessToken = '$(System.AccessToken)' # Avoid specifying the access token directly on the init.ps1 command line to avoid it showing up in errors 13 | .\init.ps1 -AccessToken $AccessToken ${{ parameters['initArgs'] }} -UpgradePrerequisites -NoNuGetCredProvider 14 | dotnet --info 15 | 16 | # Print mono version if it is present. 17 | if (Get-Command mono -ErrorAction SilentlyContinue) { 18 | mono --version 19 | } 20 | displayName: ⚙ Install prerequisites 21 | 22 | - powershell: azure-pipelines/variables/_pipelines.ps1 23 | failOnStderr: true 24 | displayName: ⚙ Set pipeline variables based on source 25 | name: SetPipelineVariables 26 | -------------------------------------------------------------------------------- /azure-pipelines/justnugetorg.nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /azure-pipelines/publish-CodeCov.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Uploads code coverage to codecov.io 4 | .PARAMETER CodeCovToken 5 | Code coverage token to use 6 | .PARAMETER PathToCodeCoverage 7 | Path to root of code coverage files 8 | .PARAMETER Name 9 | Name to upload with codecoverge 10 | .PARAMETER Flags 11 | Flags to upload with codecoverge 12 | #> 13 | [CmdletBinding()] 14 | Param ( 15 | [Parameter(Mandatory=$true)] 16 | [string]$CodeCovToken, 17 | [Parameter(Mandatory=$true)] 18 | [string]$PathToCodeCoverage, 19 | [string]$Name, 20 | [string]$Flags 21 | ) 22 | 23 | $RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path 24 | 25 | Get-ChildItem -Recurse -Path $PathToCodeCoverage -Filter "*.cobertura.xml" | % { 26 | $relativeFilePath = Resolve-Path -relative $_.FullName 27 | 28 | Write-Host "Uploading: $relativeFilePath" -ForegroundColor Yellow 29 | & (& "$PSScriptRoot/Get-CodeCovTool.ps1") -t $CodeCovToken -f $relativeFilePath -R $RepoRoot -F $Flags -n $Name 30 | } 31 | -------------------------------------------------------------------------------- /azure-pipelines/publish-codecoverage.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - powershell: azure-pipelines/Merge-CodeCoverage.ps1 -Path $env:BUILD_ARTIFACTSTAGINGDIRECTORY -OutputFile coveragereport/merged.cobertura.xml -Format Cobertura -Verbose 3 | displayName: ⚙ Merge coverage 4 | - task: PublishCodeCoverageResults@1 5 | displayName: 📢 Publish code coverage results to Azure DevOps 6 | inputs: 7 | codeCoverageTool: cobertura 8 | summaryFileLocation: coveragereport/merged.cobertura.xml 9 | failIfCoverageEmpty: true 10 | -------------------------------------------------------------------------------- /azure-pipelines/publish-deployables.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - powershell: dotnet nuget push "$(Resolve-Path '$(Build.ArtifactStagingDirectory)\deployables-Linux\')*.nupkg" -s $(ci_feed) -k azdo --skip-duplicate 3 | displayName: 📦 Push packages to CI feed 4 | condition: and(succeeded(), ne(variables['ci_feed'], ''), ne(variables['Build.Reason'], 'PullRequest')) 5 | -------------------------------------------------------------------------------- /azure-pipelines/publish-symbols.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - task: PublishSymbols@2 3 | inputs: 4 | SymbolsFolder: $(Build.ArtifactStagingDirectory)/symbols 5 | SearchPattern: '**/*.pdb' 6 | IndexSources: false 7 | SymbolServerType: TeamServices 8 | displayName: 📢 Publish symbols 9 | 10 | - task: PublishSymbols@2 11 | inputs: 12 | SymbolsFolder: $(Build.ArtifactStagingDirectory)/test_symbols 13 | SearchPattern: '**/*.pdb' 14 | IndexSources: false 15 | SymbolServerType: TeamServices 16 | displayName: 📢 Publish test symbols 17 | -------------------------------------------------------------------------------- /azure-pipelines/release.yml: -------------------------------------------------------------------------------- 1 | trigger: none # We only want to trigger manually or based on resources 2 | pr: none 3 | 4 | resources: 5 | pipelines: 6 | - pipeline: CI 7 | source: CSharpIsNull 8 | trigger: 9 | tags: 10 | - auto-release 11 | 12 | variables: 13 | - group: Publishing secrets 14 | 15 | jobs: 16 | - job: release 17 | pool: 18 | vmImage: ubuntu-latest 19 | steps: 20 | - checkout: none 21 | - powershell: | 22 | Write-Host "##vso[build.updatebuildnumber]$(resources.pipeline.CI.runName)" 23 | if ('$(resources.pipeline.CI.runName)'.Contains('-')) { 24 | Write-Host "##vso[task.setvariable variable=IsPrerelease]true" 25 | } else { 26 | Write-Host "##vso[task.setvariable variable=IsPrerelease]false" 27 | } 28 | displayName: ⚙ Set up pipeline 29 | - task: UseDotNet@2 30 | displayName: ⚙ Install .NET SDK 31 | inputs: 32 | packageType: sdk 33 | version: 6.x 34 | - download: CI 35 | artifact: deployables-Linux 36 | displayName: 🔻 Download deployables-Linux artifact 37 | patterns: 'deployables-Linux/*' 38 | - task: GitHubRelease@1 39 | displayName: 📢 GitHub release (create) 40 | inputs: 41 | gitHubConnection: github.com_AArnott_OAuth 42 | repositoryName: $(Build.Repository.Name) 43 | target: $(resources.pipeline.CI.sourceCommit) 44 | tagSource: userSpecifiedTag 45 | tag: v$(resources.pipeline.CI.runName) 46 | title: v$(resources.pipeline.CI.runName) 47 | isDraft: true # After running this step, visit the new draft release, edit, and publish. 48 | isPreRelease: $(IsPrerelease) 49 | assets: $(Pipeline.Workspace)/CI/deployables-Linux/*.nupkg 50 | changeLogCompareToRelease: lastNonDraftRelease 51 | changeLogType: issueBased 52 | changeLogLabels: | 53 | [ 54 | { "label" : "breaking change", "displayName" : "Breaking changes", "state" : "closed" }, 55 | { "label" : "bug", "displayName" : "Fixes", "state" : "closed" }, 56 | { "label" : "enhancement", "displayName": "Enhancements", "state" : "closed" } 57 | ] 58 | - script: dotnet nuget push $(Pipeline.Workspace)/CI/deployables-Linux/*.nupkg -s https://api.nuget.org/v3/index.json --api-key $(NuGetOrgApiKey) --skip-duplicate 59 | displayName: 📦 Push packages to nuget.org 60 | condition: and(succeeded(), ne(variables['NuGetOrgApiKey'], '')) 61 | -------------------------------------------------------------------------------- /azure-pipelines/test.runsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | \.dll$ 10 | \.exe$ 11 | 12 | 13 | xunit\..* 14 | 15 | 16 | 17 | 18 | ^System\.Diagnostics\.DebuggerHiddenAttribute$ 19 | ^System\.Diagnostics\.DebuggerNonUserCodeAttribute$ 20 | ^System\.CodeDom\.Compiler\.GeneratedCodeAttribute$ 21 | ^System\.Diagnostics\.CodeAnalysis\.ExcludeFromCodeCoverageAttribute$ 22 | 23 | 24 | 25 | 26 | True 27 | 28 | True 29 | 30 | True 31 | 32 | False 33 | 34 | False 35 | 36 | False 37 | 38 | True 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /azure-pipelines/variables/DotNetSdkVersion.ps1: -------------------------------------------------------------------------------- 1 | $globalJson = Get-Content -Path "$PSScriptRoot\..\..\global.json" | ConvertFrom-Json 2 | $globalJson.sdk.version 3 | -------------------------------------------------------------------------------- /azure-pipelines/variables/_all.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | <# 4 | .SYNOPSIS 5 | This script returns a hashtable of build variables that should be set 6 | at the start of a build or release definition's execution. 7 | #> 8 | 9 | [CmdletBinding(SupportsShouldProcess = $true)] 10 | param ( 11 | ) 12 | 13 | $vars = @{} 14 | 15 | Get-ChildItem "$PSScriptRoot\*.ps1" -Exclude "_*" |% { 16 | Write-Host "Computing $($_.BaseName) variable" 17 | $vars[$_.BaseName] = & $_ 18 | } 19 | 20 | $vars 21 | -------------------------------------------------------------------------------- /azure-pipelines/variables/_pipelines.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script translates the variables returned by the _all.ps1 script 4 | into commands that instruct Azure Pipelines to actually set those variables for other pipeline tasks to consume. 5 | 6 | The build or release definition may have set these variables to override 7 | what the build would do. So only set them if they have not already been set. 8 | #> 9 | 10 | [CmdletBinding()] 11 | param ( 12 | ) 13 | 14 | (& "$PSScriptRoot\_all.ps1").GetEnumerator() |% { 15 | # Always use ALL CAPS for env var names since Azure Pipelines converts variable names to all caps and on non-Windows OS, env vars are case sensitive. 16 | $keyCaps = $_.Key.ToUpper() 17 | if ((Test-Path "env:$keyCaps") -and (Get-Content "env:$keyCaps")) { 18 | Write-Host "Skipping setting $keyCaps because variable is already set to '$(Get-Content env:$keyCaps)'." -ForegroundColor Cyan 19 | } else { 20 | Write-Host "$keyCaps=$($_.Value)" -ForegroundColor Yellow 21 | if ($env:TF_BUILD) { 22 | # Create two variables: the first that can be used by its simple name and accessible only within this job. 23 | Write-Host "##vso[task.setvariable variable=$keyCaps]$($_.Value)" 24 | # and the second that works across jobs and stages but must be fully qualified when referenced. 25 | Write-Host "##vso[task.setvariable variable=$keyCaps;isOutput=true]$($_.Value)" 26 | } elseif ($env:GITHUB_ACTIONS) { 27 | Add-Content -Path $env:GITHUB_ENV -Value "$keyCaps=$($_.Value)" 28 | } 29 | Set-Item -Path "env:$keyCaps" -Value $_.Value 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /doc/analyzers/CSIsNull001.md: -------------------------------------------------------------------------------- 1 | # CSIsNull001 2 | 3 | This analyzer flags use of `== null` to test whether a value is `null`. 4 | 5 | For example this code would produce a diagnostic: 6 | 7 | ```cs 8 | if (o == null) 9 | { 10 | } 11 | ``` 12 | 13 | A code fix is offered to automate the fix, which is to use pattern syntax instead: 14 | 15 | ```cs 16 | if (o is null) 17 | { 18 | } 19 | ``` 20 | 21 | Pattern syntax is preferred because if `o` is typed as a struct, the compiler will report an error when testing it for `null`, which a struct can never be. 22 | -------------------------------------------------------------------------------- /doc/analyzers/CSIsNull002.md: -------------------------------------------------------------------------------- 1 | # CSIsNull002 2 | 3 | This analyzer flags use of `!= null` to test whether a value is not `null`. 4 | 5 | For example this code would produce a diagnostic: 6 | 7 | ```cs 8 | if (o != null) 9 | { 10 | } 11 | ``` 12 | 13 | A code fix is offered to automate the fix, which is to use pattern syntax instead: 14 | 15 | ```cs 16 | if (o is object) 17 | { 18 | } 19 | ``` 20 | 21 | Pattern syntax is preferred because if `o` is typed as a struct, the compiler will report an error when testing it for `null`, which a struct can never be. 22 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.300", 4 | "rollForward": "patch", 5 | "allowPrerelease": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /init.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | SETLOCAL 3 | set PS1UnderCmd=1 4 | 5 | :: Get the datetime in a format that can go in a filename. 6 | set _my_datetime=%date%_%time% 7 | set _my_datetime=%_my_datetime: =_% 8 | set _my_datetime=%_my_datetime::=% 9 | set _my_datetime=%_my_datetime:/=_% 10 | set _my_datetime=%_my_datetime:.=_% 11 | set CmdEnvScriptPath=%temp%\envvarscript_%_my_datetime%.cmd 12 | 13 | powershell.exe -NoProfile -NoLogo -ExecutionPolicy bypass -Command "try { & '%~dpn0.ps1' %*; exit $LASTEXITCODE } catch { write-host $_; exit 1 }" 14 | 15 | :: Set environment variables in the parent cmd.exe process. 16 | IF EXIST "%CmdEnvScriptPath%" ( 17 | ENDLOCAL 18 | CALL "%CmdEnvScriptPath%" 19 | DEL "%CmdEnvScriptPath%" 20 | ) 21 | -------------------------------------------------------------------------------- /init.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | <# 4 | .SYNOPSIS 5 | Installs dependencies required to build and test the projects in this repository. 6 | .DESCRIPTION 7 | This MAY not require elevation, as the SDK and runtimes are installed to a per-user location, 8 | unless the `-InstallLocality` switch is specified directing to a per-repo or per-machine location. 9 | See detailed help on that switch for more information. 10 | 11 | The CmdEnvScriptPath environment variable may be optionally set to a path to a cmd shell script to be created (or appended to if it already exists) that will set the environment variables in cmd.exe that are set within the PowerShell environment. 12 | This is used by init.cmd in order to reapply any new environment variables to the parent cmd.exe process that were set in the powershell child process. 13 | .PARAMETER InstallLocality 14 | A value indicating whether dependencies should be installed locally to the repo or at a per-user location. 15 | Per-user allows sharing the installed dependencies across repositories and allows use of a shared expanded package cache. 16 | Visual Studio will only notice and use these SDKs/runtimes if VS is launched from the environment that runs this script. 17 | Per-repo allows for high isolation, allowing for a more precise recreation of the environment within an Azure Pipelines build. 18 | When using 'repo', environment variables are set to cause the locally installed dotnet SDK to be used. 19 | Per-repo can lead to file locking issues when dotnet.exe is left running as a build server and can be mitigated by running `dotnet build-server shutdown`. 20 | Per-machine requires elevation and will download and install all SDKs and runtimes to machine-wide locations so all applications can find it. 21 | .PARAMETER NoPrerequisites 22 | Skips the installation of prerequisite software (e.g. SDKs, tools). 23 | .PARAMETER NoNuGetCredProvider 24 | Skips the installation of the NuGet credential provider. Useful in pipelines with the `NuGetAuthenticate` task, as a workaround for https://github.com/microsoft/artifacts-credprovider/issues/244. 25 | This switch is ignored and installation is skipped when -NoPrerequisites is specified. 26 | .PARAMETER UpgradePrerequisites 27 | Takes time to install prerequisites even if they are already present in case they need to be upgraded. 28 | No effect if -NoPrerequisites is specified. 29 | .PARAMETER NoRestore 30 | Skips the package restore step. 31 | .PARAMETER NoToolRestore 32 | Skips the dotnet tool restore step. 33 | .PARAMETER AccessToken 34 | An optional access token for authenticating to Azure Artifacts authenticated feeds. 35 | .PARAMETER Interactive 36 | Runs NuGet restore in interactive mode. This can turn authentication failures into authentication challenges. 37 | #> 38 | [CmdletBinding(SupportsShouldProcess = $true)] 39 | Param ( 40 | [ValidateSet('repo', 'user', 'machine')] 41 | [string]$InstallLocality = 'user', 42 | [Parameter()] 43 | [switch]$NoPrerequisites, 44 | [Parameter()] 45 | [switch]$NoNuGetCredProvider, 46 | [Parameter()] 47 | [switch]$UpgradePrerequisites, 48 | [Parameter()] 49 | [switch]$NoRestore, 50 | [Parameter()] 51 | [switch]$NoToolRestore, 52 | [Parameter()] 53 | [string]$AccessToken, 54 | [Parameter()] 55 | [switch]$Interactive 56 | ) 57 | 58 | $EnvVars = @{} 59 | $PrependPath = @() 60 | 61 | if (!$NoPrerequisites) { 62 | if (!$NoNuGetCredProvider) { 63 | & "$PSScriptRoot\tools\Install-NuGetCredProvider.ps1" -AccessToken $AccessToken -Force:$UpgradePrerequisites 64 | } 65 | 66 | & "$PSScriptRoot\tools\Install-DotNetSdk.ps1" -InstallLocality $InstallLocality 67 | if ($LASTEXITCODE -eq 3010) { 68 | Exit 3010 69 | } 70 | 71 | # The procdump tool and env var is required for dotnet test to collect hang/crash dumps of tests. 72 | # But it only works on Windows. 73 | if ($env:OS -eq 'Windows_NT') { 74 | $EnvVars['PROCDUMP_PATH'] = & "$PSScriptRoot\azure-pipelines\Get-ProcDump.ps1" 75 | } 76 | } 77 | 78 | # Workaround nuget credential provider bug that causes very unreliable package restores on Azure Pipelines 79 | $env:NUGET_PLUGIN_HANDSHAKE_TIMEOUT_IN_SECONDS = 20 80 | $env:NUGET_PLUGIN_REQUEST_TIMEOUT_IN_SECONDS = 20 81 | 82 | Push-Location $PSScriptRoot 83 | try { 84 | $HeaderColor = 'Green' 85 | 86 | $RestoreArguments = @() 87 | if ($Interactive) { 88 | $RestoreArguments += '--interactive' 89 | } 90 | 91 | if (!$NoRestore -and $PSCmdlet.ShouldProcess("NuGet packages", "Restore")) { 92 | Write-Host "Restoring NuGet packages" -ForegroundColor $HeaderColor 93 | dotnet restore @RestoreArguments 94 | if ($lastexitcode -ne 0) { 95 | throw "Failure while restoring packages." 96 | } 97 | } 98 | 99 | if (!$NoToolRestore -and $PSCmdlet.ShouldProcess("dotnet tool", "restore")) { 100 | dotnet tool restore @RestoreArguments 101 | if ($lastexitcode -ne 0) { 102 | throw "Failure while restoring dotnet CLI tools." 103 | } 104 | } 105 | 106 | & "$PSScriptRoot/tools/Set-EnvVars.ps1" -Variables $EnvVars -PrependPath $PrependPath | Out-Null 107 | } 108 | catch { 109 | Write-Error $error[0] 110 | exit $lastexitcode 111 | } 112 | finally { 113 | Pop-Location 114 | } 115 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /settings.VisualStudio.json: -------------------------------------------------------------------------------- 1 | { 2 | "textEditor.codeCleanup.profile": "profile1" 3 | } 4 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AArnott/CSharpIsNull/e29603ae4a24d6db86811135401ad5905a87d593/src/.editorconfig -------------------------------------------------------------------------------- /src/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Runtime.InteropServices; 5 | 6 | [assembly: DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] 7 | -------------------------------------------------------------------------------- /src/AssemblyInfo.vb: -------------------------------------------------------------------------------- 1 | ' Copyright (c) COMPANY-PLACEHOLDER. All rights reserved. 2 | ' Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | Imports System.Runtime.InteropServices 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/CSharpIsNullAnalyzer.CodeFixes/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Resources; 5 | 6 | [assembly: NeutralResourcesLanguage("en-US")] 7 | -------------------------------------------------------------------------------- /src/CSharpIsNullAnalyzer.CodeFixes/CSIsNull001Fixer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Collections.Immutable; 5 | using Microsoft.CodeAnalysis; 6 | using Microsoft.CodeAnalysis.CodeActions; 7 | using Microsoft.CodeAnalysis.CodeFixes; 8 | using Microsoft.CodeAnalysis.CSharp; 9 | using Microsoft.CodeAnalysis.CSharp.Syntax; 10 | using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; 11 | 12 | namespace CSharpIsNullAnalyzer; 13 | 14 | /// 15 | /// Provides code fixes for . 16 | /// 17 | [ExportCodeFixProvider(LanguageNames.CSharp)] 18 | public class CSIsNull001Fixer : CodeFixProvider 19 | { 20 | private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( 21 | CSIsNull001.Id); 22 | 23 | private static readonly ConstantPatternSyntax NullPattern = ConstantPattern(LiteralExpression(SyntaxKind.NullLiteralExpression)); 24 | 25 | /// 26 | public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; 27 | 28 | /// 29 | public override async Task RegisterCodeFixesAsync(CodeFixContext context) 30 | { 31 | foreach (Diagnostic diagnostic in context.Diagnostics) 32 | { 33 | if (diagnostic.Id == CSIsNull001.Id) 34 | { 35 | SyntaxNode? syntaxRoot = await context.Document.GetSyntaxRootAsync(context.CancellationToken); 36 | BinaryExpressionSyntax? expr = syntaxRoot?.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true).FirstAncestorOrSelf(); 37 | if (expr is not null) 38 | { 39 | context.RegisterCodeFix( 40 | CodeAction.Create( 41 | Strings.CSIsNull001_FixTitle, 42 | ct => expr.ReplaceBinaryExpressionWithIsPattern(context.Document, syntaxRoot!, NullPattern), 43 | equivalenceKey: "isNull"), 44 | diagnostic); 45 | } 46 | } 47 | } 48 | } 49 | 50 | /// 51 | public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; 52 | } 53 | -------------------------------------------------------------------------------- /src/CSharpIsNullAnalyzer.CodeFixes/CSIsNull002Fixer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Collections.Immutable; 5 | using Microsoft.CodeAnalysis; 6 | using Microsoft.CodeAnalysis.CodeActions; 7 | using Microsoft.CodeAnalysis.CodeFixes; 8 | using Microsoft.CodeAnalysis.CSharp; 9 | using Microsoft.CodeAnalysis.CSharp.Syntax; 10 | using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; 11 | 12 | namespace CSharpIsNullAnalyzer; 13 | 14 | /// 15 | /// Provides code fixes for . 16 | /// 17 | [ExportCodeFixProvider(LanguageNames.CSharp)] 18 | public class CSIsNull002Fixer : CodeFixProvider 19 | { 20 | /// 21 | /// The equivalence key used for the code fix that uses is object syntax. 22 | /// 23 | public const string IsObjectEquivalenceKey = "IsObject"; 24 | 25 | /// 26 | /// The equivalence key used for the code fix that uses is not null syntax. 27 | /// 28 | public const string IsNotNullEquivalenceKey = "IsNotNull"; 29 | 30 | private static readonly ImmutableArray ReusableFixableDiagnosticIds = ImmutableArray.Create( 31 | CSIsNull002.Id); 32 | 33 | private static readonly ExpressionSyntax ObjectLiteral = PredefinedType(Token(SyntaxKind.ObjectKeyword)); 34 | private static readonly PatternSyntax NotNullPattern = UnaryPattern(Token(SyntaxKind.NotKeyword), ConstantPattern(LiteralExpression(SyntaxKind.NullLiteralExpression))); 35 | 36 | /// 37 | public override ImmutableArray FixableDiagnosticIds => ReusableFixableDiagnosticIds; 38 | 39 | /// 40 | public override async Task RegisterCodeFixesAsync(CodeFixContext context) 41 | { 42 | foreach (Diagnostic diagnostic in context.Diagnostics) 43 | { 44 | if (diagnostic.Id == CSIsNull002.Id) 45 | { 46 | SyntaxNode? syntaxRoot = await context.Document.GetSyntaxRootAsync(context.CancellationToken); 47 | BinaryExpressionSyntax? expr = syntaxRoot?.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true).FirstAncestorOrSelf(); 48 | if (expr is not null) 49 | { 50 | if (context.Document.Project.ParseOptions is CSharpParseOptions { LanguageVersion: >= LanguageVersion.CSharp9 } && 51 | diagnostic.Properties.ContainsKey(CSIsNull002.OfferIsNotNullFixKey)) 52 | { 53 | context.RegisterCodeFix( 54 | CodeAction.Create( 55 | Strings.CSIsNull002_Fix2Title, 56 | ct => expr.ReplaceBinaryExpressionWithIsPattern(context.Document, syntaxRoot!, NotNullPattern), 57 | equivalenceKey: IsNotNullEquivalenceKey), 58 | diagnostic); 59 | } 60 | 61 | context.RegisterCodeFix( 62 | CodeAction.Create( 63 | Strings.CSIsNull002_Fix1Title, 64 | ct => expr.ReplaceBinaryExpressionWithIsExpression(context.Document, syntaxRoot!, ObjectLiteral), 65 | equivalenceKey: IsObjectEquivalenceKey), 66 | diagnostic); 67 | } 68 | } 69 | } 70 | } 71 | 72 | /// 73 | public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; 74 | } 75 | -------------------------------------------------------------------------------- /src/CSharpIsNullAnalyzer.CodeFixes/CSharpIsNullAnalyzer.CodeFixes.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CSharpIsNullAnalyzer 5 | CSharpIsNullAnalyzer 6 | true 7 | C# analyzer to encourage use of `is null` and `is object` syntax over `== null` or `!= null`. 8 | C#;Analyzers 9 | false 10 | $(TargetsForTfmSpecificContentInPackage);PackBuildOutputs 11 | true 12 | false 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | True 32 | True 33 | Strings.resx 34 | 35 | 36 | 37 | 38 | 39 | ResXFileCodeGenerator 40 | Strings.Designer.cs 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/CSharpIsNullAnalyzer.CodeFixes/IsFixer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Linq.Expressions; 5 | using Microsoft.CodeAnalysis; 6 | using Microsoft.CodeAnalysis.CodeFixes; 7 | using Microsoft.CodeAnalysis.CSharp; 8 | using Microsoft.CodeAnalysis.CSharp.Syntax; 9 | using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; 10 | 11 | namespace CSharpIsNullAnalyzer 12 | { 13 | /// 14 | /// Provides shared methods to replace binary expression with 'is' expression/Pattern syntax. 15 | /// 16 | internal static class IsFixer 17 | { 18 | /// 19 | /// Replaces the with an 'is' expressionOrPattern. 20 | /// 21 | /// The binary expressionOrPattern to replace. 22 | /// The document in which to do the fix. 23 | /// The root SyntaxNode to update. 24 | /// The expressionOrPattern to replace the binary expressio with. 25 | /// Document with binary expressionOrPattern replace with expressionOrPattern syntax. 26 | public static Task ReplaceBinaryExpressionWithIsPattern(this BinaryExpressionSyntax expr, Document document, SyntaxNode syntaxRoot, PatternSyntax pattern) 27 | => expr.ReplaceBinaryExpressionWithIsExpressionOrPattern(document, syntaxRoot, pattern, IsPatternExpression); 28 | 29 | /// 30 | /// Replaces the with an 'is' expressionOrPattern. 31 | /// 32 | /// The binary expressionOrPattern to replace. 33 | /// The document in which to do the fix. 34 | /// The root SyntaxNode to update. 35 | /// The expressionOrPattern to replace the binary expressio with. 36 | /// Document with binary expressionOrPattern replace with expressionOrPattern syntax. 37 | public static Task ReplaceBinaryExpressionWithIsExpression(this BinaryExpressionSyntax expr, Document document, SyntaxNode syntaxRoot, ExpressionSyntax expression) 38 | => expr.ReplaceBinaryExpressionWithIsExpressionOrPattern(document, syntaxRoot, expression, IsExpression); 39 | 40 | private static Task ReplaceBinaryExpressionWithIsExpressionOrPattern( 41 | this BinaryExpressionSyntax expr, 42 | Document document, 43 | SyntaxNode syntaxRoot, 44 | T expressionOrPattern, 45 | Func create) 46 | where T : ExpressionOrPatternSyntax 47 | { 48 | T expressionWithTrivia = expressionOrPattern.WithTriviaFrom(expr.Right); 49 | ExpressionSyntax changedExpression = expr.Right is LiteralExpressionSyntax { RawKind: (int)SyntaxKind.NullLiteralExpression or (int)SyntaxKind.DefaultLiteralExpression } or DefaultExpressionSyntax 50 | ? create(expr.Left, expressionWithTrivia) 51 | : create(expr.Right.WithoutTrailingTrivia().WithTrailingTrivia(Space), expressionWithTrivia); 52 | SyntaxNode updatedSyntaxRoot = syntaxRoot.ReplaceNode(expr, changedExpression); 53 | return Task.FromResult(document.WithSyntaxRoot(updatedSyntaxRoot)); 54 | } 55 | 56 | /// 57 | /// Creates a new IsExpressionSyntax instance. 58 | /// 59 | private static BinaryExpressionSyntax IsExpression(ExpressionSyntax left, ExpressionSyntax right) 60 | => BinaryExpression(SyntaxKind.IsExpression, left, right); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/CSharpIsNullAnalyzer.CodeFixes/Strings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace CSharpIsNullAnalyzer { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Strings { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Strings() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("CSharpIsNullAnalyzer.Strings", typeof(Strings).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to Use `is null` instead. 65 | /// 66 | internal static string CSIsNull001_FixTitle { 67 | get { 68 | return ResourceManager.GetString("CSIsNull001_FixTitle", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to Use `is object` instead. 74 | /// 75 | internal static string CSIsNull002_Fix1Title { 76 | get { 77 | return ResourceManager.GetString("CSIsNull002_Fix1Title", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to Use `is not null` instead. 83 | /// 84 | internal static string CSIsNull002_Fix2Title { 85 | get { 86 | return ResourceManager.GetString("CSIsNull002_Fix2Title", resourceCulture); 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/CSharpIsNullAnalyzer.CodeFixes/Strings.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 | Use `is null` instead 122 | 123 | 124 | Use `is object` instead 125 | 126 | 127 | Use `is not null` instead 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/CSharpIsNullAnalyzer.CodeFixes/tools/install.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | if($project.Object.SupportsPackageDependencyResolution) 4 | { 5 | if($project.Object.SupportsPackageDependencyResolution()) 6 | { 7 | # Do not install analyzers via install.ps1, instead let the project system handle it. 8 | return 9 | } 10 | } 11 | 12 | $analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve 13 | 14 | foreach($analyzersPath in $analyzersPaths) 15 | { 16 | if (Test-Path $analyzersPath) 17 | { 18 | # Install the language agnostic analyzers. 19 | foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) 20 | { 21 | if($project.Object.AnalyzerReferences) 22 | { 23 | $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) 24 | } 25 | } 26 | } 27 | } 28 | 29 | # $project.Type gives the language name like (C# or VB.NET) 30 | $languageFolder = "" 31 | if($project.Type -eq "C#") 32 | { 33 | $languageFolder = "cs" 34 | } 35 | if($project.Type -eq "VB.NET") 36 | { 37 | $languageFolder = "vb" 38 | } 39 | if($languageFolder -eq "") 40 | { 41 | return 42 | } 43 | 44 | foreach($analyzersPath in $analyzersPaths) 45 | { 46 | # Install language specific analyzers. 47 | $languageAnalyzersPath = join-path $analyzersPath $languageFolder 48 | if (Test-Path $languageAnalyzersPath) 49 | { 50 | foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) 51 | { 52 | if($project.Object.AnalyzerReferences) 53 | { 54 | $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/CSharpIsNullAnalyzer.CodeFixes/tools/uninstall.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | if($project.Object.SupportsPackageDependencyResolution) 4 | { 5 | if($project.Object.SupportsPackageDependencyResolution()) 6 | { 7 | # Do not uninstall analyzers via uninstall.ps1, instead let the project system handle it. 8 | return 9 | } 10 | } 11 | 12 | $analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve 13 | 14 | foreach($analyzersPath in $analyzersPaths) 15 | { 16 | # Uninstall the language agnostic analyzers. 17 | if (Test-Path $analyzersPath) 18 | { 19 | foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) 20 | { 21 | if($project.Object.AnalyzerReferences) 22 | { 23 | $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) 24 | } 25 | } 26 | } 27 | } 28 | 29 | # $project.Type gives the language name like (C# or VB.NET) 30 | $languageFolder = "" 31 | if($project.Type -eq "C#") 32 | { 33 | $languageFolder = "cs" 34 | } 35 | if($project.Type -eq "VB.NET") 36 | { 37 | $languageFolder = "vb" 38 | } 39 | if($languageFolder -eq "") 40 | { 41 | return 42 | } 43 | 44 | foreach($analyzersPath in $analyzersPaths) 45 | { 46 | # Uninstall language specific analyzers. 47 | $languageAnalyzersPath = join-path $analyzersPath $languageFolder 48 | if (Test-Path $languageAnalyzersPath) 49 | { 50 | foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) 51 | { 52 | if($project.Object.AnalyzerReferences) 53 | { 54 | try 55 | { 56 | $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) 57 | } 58 | catch 59 | { 60 | 61 | } 62 | } 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/CSharpIsNullAnalyzer/AnalyzerReleases.Shipped.md: -------------------------------------------------------------------------------- 1 | ; Shipped analyzer releases 2 | ; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md 3 | 4 | -------------------------------------------------------------------------------- /src/CSharpIsNullAnalyzer/AnalyzerReleases.Unshipped.md: -------------------------------------------------------------------------------- 1 | ; Unshipped analyzer release 2 | ; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md 3 | 4 | ### New Rules 5 | Rule ID | Category | Severity | Notes 6 | --------|----------|----------|------- 7 | CSIsNull001 | Usage | Info | CSIsNull001 8 | CSIsNull002 | Usage | Info | CSIsNull002 -------------------------------------------------------------------------------- /src/CSharpIsNullAnalyzer/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Resources; 5 | 6 | [assembly: NeutralResourcesLanguage("en-US")] 7 | -------------------------------------------------------------------------------- /src/CSharpIsNullAnalyzer/CSIsNull001.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Collections.Immutable; 5 | using Microsoft.CodeAnalysis; 6 | using Microsoft.CodeAnalysis.CSharp.Syntax; 7 | using Microsoft.CodeAnalysis.Diagnostics; 8 | using Microsoft.CodeAnalysis.Operations; 9 | using Microsoft.CodeAnalysis.Text; 10 | 11 | namespace CSharpIsNullAnalyzer; 12 | 13 | /// 14 | /// An analyzer that finds == null expressions. 15 | /// 16 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 17 | public class CSIsNull001 : DiagnosticAnalyzer 18 | { 19 | /// 20 | /// The ID for diagnostics reported by this analyzer. 21 | /// 22 | public const string Id = "CSIsNull001"; 23 | 24 | /// 25 | /// The descriptor used for diagnostics created by this rule. 26 | /// 27 | internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( 28 | id: Id, 29 | title: Strings.CSIsNull001_Title, 30 | messageFormat: Strings.CSIsNull001_MessageFormat, 31 | helpLinkUri: Utils.GetHelpLink(Id), 32 | category: "Usage", 33 | defaultSeverity: DiagnosticSeverity.Info, 34 | isEnabledByDefault: true); 35 | 36 | /// 37 | public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); 38 | 39 | /// 40 | public override void Initialize(AnalysisContext context) 41 | { 42 | if (context is null) 43 | { 44 | throw new ArgumentNullException(nameof(context)); 45 | } 46 | 47 | context.EnableConcurrentExecution(); 48 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.ReportDiagnostics); 49 | 50 | context.RegisterCompilationStartAction( 51 | startContext => 52 | { 53 | INamedTypeSymbol? linqExpressionType = startContext.Compilation.GetTypeByMetadataName(WellKnownTypeNames.SystemLinqExpressionsExpression1); 54 | startContext.RegisterOperationAction( 55 | ctxt => 56 | { 57 | if (ctxt.Operation.Type.SpecialType == SpecialType.System_Boolean) 58 | { 59 | if (ctxt.Operation is IBinaryOperation { OperatorKind: BinaryOperatorKind.Equals } binaryOp) 60 | { 61 | Location? location = null; 62 | if (binaryOp.RightOperand.IsNullCheck()) 63 | { 64 | location = binaryOp.RightOperand.Syntax.GetLocation(); 65 | if (binaryOp.Syntax is BinaryExpressionSyntax { OperatorToken: { } operatorLocation, Right: { } right }) 66 | { 67 | location = ctxt.Operation.Syntax.SyntaxTree.GetLocation(new TextSpan(operatorLocation.SpanStart, right.Span.End - operatorLocation.SpanStart)); 68 | } 69 | } 70 | else if (binaryOp.LeftOperand.IsNullCheck()) 71 | { 72 | location = binaryOp.LeftOperand.Syntax.GetLocation(); 73 | if (binaryOp.Syntax is BinaryExpressionSyntax { OperatorToken: { } operatorLocation, Left: { } left }) 74 | { 75 | location = ctxt.Operation.Syntax.SyntaxTree.GetLocation(new TextSpan(left.SpanStart, operatorLocation.Span.End - left.SpanStart)); 76 | } 77 | } 78 | 79 | if (location is not null && !binaryOp.IsWithinExpressionTree(linqExpressionType)) 80 | { 81 | ctxt.ReportDiagnostic(Diagnostic.Create(Descriptor, location)); 82 | } 83 | } 84 | } 85 | }, 86 | OperationKind.Binary); 87 | }); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/CSharpIsNullAnalyzer/CSIsNull002.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Collections.Immutable; 5 | using Microsoft.CodeAnalysis; 6 | using Microsoft.CodeAnalysis.CSharp.Syntax; 7 | using Microsoft.CodeAnalysis.Diagnostics; 8 | using Microsoft.CodeAnalysis.Operations; 9 | using Microsoft.CodeAnalysis.Text; 10 | 11 | namespace CSharpIsNullAnalyzer; 12 | 13 | /// 14 | /// An analyzer that finds != null expressions. 15 | /// 16 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 17 | public class CSIsNull002 : DiagnosticAnalyzer 18 | { 19 | /// 20 | /// The ID for diagnostics reported by this analyzer. 21 | /// 22 | public const string Id = "CSIsNull002"; 23 | 24 | /// 25 | /// A key to a property that will be set in a diagnostic if the is not null code fix should be offered. 26 | /// 27 | public const string OfferIsNotNullFixKey = "OfferIsNotNullFix"; 28 | 29 | /// 30 | /// The descriptor used for diagnostics created by this rule. 31 | /// 32 | internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( 33 | id: Id, 34 | title: Strings.CSIsNull002_Title, 35 | messageFormat: Strings.CSIsNull002_MessageFormat, 36 | helpLinkUri: Utils.GetHelpLink(Id), 37 | category: "Usage", 38 | defaultSeverity: DiagnosticSeverity.Info, 39 | isEnabledByDefault: true); 40 | 41 | /// 42 | public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); 43 | 44 | /// 45 | public override void Initialize(AnalysisContext context) 46 | { 47 | if (context is null) 48 | { 49 | throw new ArgumentNullException(nameof(context)); 50 | } 51 | 52 | context.EnableConcurrentExecution(); 53 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.ReportDiagnostics); 54 | 55 | context.RegisterCompilationStartAction( 56 | startContext => 57 | { 58 | INamedTypeSymbol? linqExpressionType = startContext.Compilation.GetTypeByMetadataName(WellKnownTypeNames.SystemLinqExpressionsExpression1); 59 | startContext.RegisterOperationAction( 60 | ctxt => 61 | { 62 | if (ctxt.Operation.Type.SpecialType == SpecialType.System_Boolean) 63 | { 64 | if (ctxt.Operation is IBinaryOperation { OperatorKind: BinaryOperatorKind.NotEquals } binaryOp) 65 | { 66 | Location? location = null; 67 | if (binaryOp.RightOperand.IsNullCheck()) 68 | { 69 | location = binaryOp.RightOperand.Syntax.GetLocation(); 70 | if (binaryOp.Syntax is BinaryExpressionSyntax { OperatorToken: { } operatorLocation, Right: { } right }) 71 | { 72 | location = ctxt.Operation.Syntax.SyntaxTree.GetLocation(new TextSpan(operatorLocation.SpanStart, right.Span.End - operatorLocation.SpanStart)); 73 | } 74 | } 75 | else if (binaryOp.LeftOperand.IsNullCheck()) 76 | { 77 | location = binaryOp.LeftOperand.Syntax.GetLocation(); 78 | if (binaryOp.Syntax is BinaryExpressionSyntax { OperatorToken: { } operatorLocation, Left: { } left }) 79 | { 80 | location = ctxt.Operation.Syntax.SyntaxTree.GetLocation(new TextSpan(left.SpanStart, operatorLocation.Span.End - left.SpanStart)); 81 | } 82 | } 83 | 84 | if (location is not null) 85 | { 86 | ImmutableDictionary properties = ImmutableDictionary.Empty; 87 | if (!binaryOp.IsWithinExpressionTree(linqExpressionType)) 88 | { 89 | properties = properties.Add(OfferIsNotNullFixKey, "true"); 90 | } 91 | 92 | ctxt.ReportDiagnostic(Diagnostic.Create(Descriptor, location, properties)); 93 | } 94 | } 95 | } 96 | }, 97 | OperationKind.Binary); 98 | }); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/CSharpIsNullAnalyzer/CSharpIsNullAnalyzer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | CSharpIsNullJustAnalyzer 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | True 16 | True 17 | Strings.resx 18 | 19 | 20 | 21 | 22 | 23 | ResXFileCodeGenerator 24 | Strings.Designer.cs 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/CSharpIsNullAnalyzer/IOperationExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Collections.Immutable; 5 | using System.Diagnostics.CodeAnalysis; 6 | using Microsoft.CodeAnalysis; 7 | using Microsoft.CodeAnalysis.Operations; 8 | 9 | namespace CSharpIsNullAnalyzer; 10 | 11 | /// 12 | /// Extensions methods for the interface. 13 | /// 14 | internal static class IOperationExtensions 15 | { 16 | private static readonly ImmutableArray LambdaAndLocalFunctionKinds = 17 | ImmutableArray.Create(OperationKind.AnonymousFunction, OperationKind.LocalFunction); 18 | 19 | /// 20 | /// Tests whether an operation falls within an expression tree. 21 | /// 22 | /// The operation. 23 | /// The result of a call to , passing in 24 | /// the full name of the type. 25 | /// A value indicating whether this operation falls within an expression tree. 26 | internal static bool IsWithinExpressionTree(this IOperation operation, [NotNullWhen(true)] INamedTypeSymbol? linqExpressionTreeType) 27 | => linqExpressionTreeType != null 28 | && operation.GetAncestor(LambdaAndLocalFunctionKinds)?.Parent?.Type?.OriginalDefinition is { } lambdaType 29 | && linqExpressionTreeType.Equals(lambdaType, SymbolEqualityComparer.Default); 30 | 31 | /// 32 | /// Gets the first ancestor of this operation with: 33 | /// 1. Any OperationKind from the specified . 34 | /// 2. If is non-null, it succeeds for the ancestor. 35 | /// Returns null if there is no such ancestor. 36 | /// 37 | /// The operation to start the search. 38 | /// The kinds of ancestors to look for. 39 | /// An optional test to apply before matching. 40 | /// The matching ancestor operation, if found. 41 | internal static IOperation? GetAncestor(this IOperation root, ImmutableArray ancestorKinds, Func? predicate = null) 42 | { 43 | IOperation? ancestor = root; 44 | do 45 | { 46 | ancestor = ancestor.Parent; 47 | } 48 | while (ancestor is object && !ancestorKinds.Contains(ancestor.Kind)); 49 | 50 | if (ancestor is object) 51 | { 52 | if (predicate is object && !predicate(ancestor)) 53 | { 54 | return GetAncestor(ancestor, ancestorKinds, predicate); 55 | } 56 | 57 | return ancestor; 58 | } 59 | else 60 | { 61 | return default; 62 | } 63 | } 64 | 65 | /// 66 | /// Checks if this is a null check. 67 | /// 68 | /// The operation to check. 69 | /// if this operation checks for null, otherwise. 70 | internal static bool IsNullCheck(this IOperation operation) => 71 | operation is (IConversionOperation or ILiteralOperation or IDefaultValueOperation) and { ConstantValue: { HasValue: true, Value: null } }; 72 | } 73 | -------------------------------------------------------------------------------- /src/CSharpIsNullAnalyzer/Strings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace CSharpIsNullAnalyzer { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Strings { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Strings() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("CSharpIsNullAnalyzer.Strings", typeof(Strings).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to Use `is null` instead of `== null` for null checks so the compiler can help you avoid testing struct equality to null.. 65 | /// 66 | internal static string CSIsNull001_MessageFormat { 67 | get { 68 | return ResourceManager.GetString("CSIsNull001_MessageFormat", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to Use `is null` for null checks. 74 | /// 75 | internal static string CSIsNull001_Title { 76 | get { 77 | return ResourceManager.GetString("CSIsNull001_Title", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to Use `is object` instead of `!= null` for null checks so the compiler can help you avoid testing struct equality to null.. 83 | /// 84 | internal static string CSIsNull002_MessageFormat { 85 | get { 86 | return ResourceManager.GetString("CSIsNull002_MessageFormat", resourceCulture); 87 | } 88 | } 89 | 90 | /// 91 | /// Looks up a localized string similar to Use `is object` for non-null checks. 92 | /// 93 | internal static string CSIsNull002_Title { 94 | get { 95 | return ResourceManager.GetString("CSIsNull002_Title", resourceCulture); 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/CSharpIsNullAnalyzer/Strings.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 | Use `is null` instead of `== null` for null checks so the compiler can help you avoid testing struct equality to null. 122 | 123 | 124 | Use `is null` for null checks 125 | 126 | 127 | Use `is object` instead of `!= null` for null checks so the compiler can help you avoid testing struct equality to null. 128 | 129 | 130 | Use `is object` for non-null checks 131 | 132 | -------------------------------------------------------------------------------- /src/CSharpIsNullAnalyzer/Utils.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace CSharpIsNullAnalyzer; 5 | 6 | /// 7 | /// Utility methods for analyzers. 8 | /// 9 | internal static class Utils 10 | { 11 | /// 12 | /// Gets the URL to the help topic for a particular analyzer. 13 | /// 14 | /// The ID of the analyzer. 15 | /// The URL for the analyzer's documentation. 16 | internal static string GetHelpLink(string analyzerId) 17 | { 18 | return $"https://github.com/AArnott/CSharpIsNull/blob/main/doc/analyzers/{analyzerId}.md"; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/CSharpIsNullAnalyzer/WellKnownTypeNames.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace CSharpIsNullAnalyzer; 5 | 6 | /// 7 | /// Well known type names. 8 | /// 9 | internal static class WellKnownTypeNames 10 | { 11 | /// 12 | /// The full name of the type. 13 | /// 14 | internal const string SystemLinqExpressionsExpression1 = "System.Linq.Expressions.Expression`1"; 15 | } 16 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | README.md 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | netstandard2.0 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /strongname.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AArnott/CSharpIsNull/e29603ae4a24d6db86811135401ad5905a87d593/strongname.snk -------------------------------------------------------------------------------- /stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "documentationRules": { 5 | "companyName": "Andrew Arnott", 6 | "copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the {licenseName} license. See {licenseFile} file in the project root for full license information.", 7 | "variables": { 8 | "licenseName": "MIT", 9 | "licenseFile": "LICENSE" 10 | }, 11 | "fileNamingConvention": "metadata", 12 | "xmlHeader": false 13 | }, 14 | "orderingRules": { 15 | "usingDirectivesPlacement": "outsideNamespace" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # SA1600: Elements should be documented 4 | dotnet_diagnostic.SA1600.severity = silent 5 | 6 | # SA1601: Partial elements should be documented 7 | dotnet_diagnostic.SA1601.severity = silent 8 | 9 | # SA1602: Enumeration items should be documented 10 | dotnet_diagnostic.SA1602.severity = silent 11 | 12 | # SA1615: Element return value should be documented 13 | dotnet_diagnostic.SA1615.severity = silent 14 | 15 | # VSTHRD103: Call async methods when in an async method 16 | dotnet_diagnostic.VSTHRD103.severity = silent 17 | 18 | # VSTHRD111: Use .ConfigureAwait(bool) 19 | dotnet_diagnostic.VSTHRD111.severity = none 20 | 21 | # VSTHRD200: Use Async suffix for async methods 22 | dotnet_diagnostic.VSTHRD200.severity = silent 23 | 24 | # CA1014: Mark assemblies with CLSCompliant 25 | dotnet_diagnostic.CA1014.severity = none 26 | 27 | # CA1050: Declare types in namespaces 28 | dotnet_diagnostic.CA1050.severity = none 29 | 30 | # CA1303: Do not pass literals as localized parameters 31 | dotnet_diagnostic.CA1303.severity = none 32 | 33 | # CS1591: Missing XML comment for publicly visible type or member 34 | dotnet_diagnostic.CS1591.severity = silent 35 | 36 | # CA1707: Identifiers should not contain underscores 37 | dotnet_diagnostic.CA1707.severity = silent 38 | 39 | # CA1062: Validate arguments of public methods 40 | dotnet_diagnostic.CA1062.severity = suggestion 41 | 42 | # CA1063: Implement IDisposable Correctly 43 | dotnet_diagnostic.CA1063.severity = silent 44 | 45 | # CA1816: Dispose methods should call SuppressFinalize 46 | dotnet_diagnostic.CA1816.severity = silent 47 | 48 | # CA2007: Consider calling ConfigureAwait on the awaited task 49 | dotnet_diagnostic.CA2007.severity = none 50 | 51 | # SA1401: Fields should be private 52 | dotnet_diagnostic.SA1401.severity = silent 53 | 54 | # SA1133: Do not combine attributes 55 | dotnet_diagnostic.SA1133.severity = silent 56 | -------------------------------------------------------------------------------- /test/CSharpIsNullAnalyzer.Tests/CSIsNull001Tests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using Xunit; 5 | using VerifyCS = CSharpCodeFixVerifier; 6 | 7 | public class CsIsNull001Tests 8 | { 9 | [Fact] 10 | public async Task EqualsNullInExpressionBody_ProducesDiagnostic() 11 | { 12 | string source = @" 13 | class Test 14 | { 15 | bool Method(object o) => o [|== null|]; 16 | }"; 17 | 18 | string fixedSource = @" 19 | class Test 20 | { 21 | bool Method(object o) => o is null; 22 | }"; 23 | 24 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource); 25 | } 26 | 27 | [Fact] 28 | public async Task NullEqualsInExpressionBody_ProducesDiagnostic() 29 | { 30 | string source = @" 31 | class Test 32 | { 33 | bool Method(object o) => [|null ==|] o; 34 | }"; 35 | 36 | string fixedSource = @" 37 | class Test 38 | { 39 | bool Method(object o) => o is null; 40 | }"; 41 | 42 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource); 43 | } 44 | 45 | [Fact] 46 | public async Task EqualsNullInIfExpression_ProducesDiagnostic() 47 | { 48 | string source = @" 49 | class Test 50 | { 51 | void Method(object o) 52 | { 53 | if (o [|== null|]) 54 | { 55 | } 56 | } 57 | }"; 58 | 59 | string fixedSource = @" 60 | class Test 61 | { 62 | void Method(object o) 63 | { 64 | if (o is null) 65 | { 66 | } 67 | } 68 | }"; 69 | 70 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource); 71 | } 72 | 73 | [Fact] 74 | public async Task NullEqualsInIfExpression_ProducesDiagnostic() 75 | { 76 | string source = @" 77 | class Test 78 | { 79 | void Method(object o) 80 | { 81 | if ([|null ==|] o) 82 | { 83 | } 84 | } 85 | }"; 86 | 87 | string fixedSource = @" 88 | class Test 89 | { 90 | void Method(object o) 91 | { 92 | if (o is null) 93 | { 94 | } 95 | } 96 | }"; 97 | 98 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource); 99 | } 100 | 101 | [Fact] 102 | public async Task EqualsInArgument_ProducesDiagnostic() 103 | { 104 | string source = @" 105 | class Test 106 | { 107 | void Method(object o) 108 | { 109 | Other(o [|== null|]); 110 | } 111 | 112 | void Other(bool condition) { } 113 | }"; 114 | 115 | string fixedSource = @" 116 | class Test 117 | { 118 | void Method(object o) 119 | { 120 | Other(o is null); 121 | } 122 | 123 | void Other(bool condition) { } 124 | }"; 125 | 126 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource); 127 | } 128 | 129 | [Fact] 130 | public async Task EqualsNull_NearExpressionTreeAssignment_ProducesDiagnostic() 131 | { 132 | string source = @" 133 | using System; 134 | using System.Linq.Expressions; 135 | 136 | class Test 137 | { 138 | void Method() 139 | { 140 | Expression> e = """" [|== null|] ? (s => s == null) : (Expression>)null; 141 | } 142 | }"; 143 | 144 | string fixedSource = @" 145 | using System; 146 | using System.Linq.Expressions; 147 | 148 | class Test 149 | { 150 | void Method() 151 | { 152 | Expression> e = """" is null ? (s => s == null) : (Expression>)null; 153 | } 154 | }"; 155 | 156 | await VerifyCS.VerifyAnalyzerAsync(source); 157 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource); 158 | } 159 | 160 | [Fact] 161 | public async Task EqualsNullInExpressionTree_ProducesNoDiagnostic() 162 | { 163 | string source = @" 164 | using System; 165 | using System.Linq.Expressions; 166 | 167 | class Test 168 | { 169 | void Method() 170 | { 171 | _ = (Expression>)(s => s == null); 172 | } 173 | }"; 174 | 175 | await VerifyCS.VerifyAnalyzerAsync(source); 176 | } 177 | 178 | [Fact] 179 | public async Task NullEqualsInExpressionTree_ProducesNoDiagnostic() 180 | { 181 | string source = @" 182 | using System; 183 | using System.Linq.Expressions; 184 | 185 | class Test 186 | { 187 | void Method() 188 | { 189 | _ = (Expression>)(s => null == s); 190 | } 191 | }"; 192 | 193 | await VerifyCS.VerifyAnalyzerAsync(source); 194 | } 195 | 196 | [Fact] 197 | public async Task EqualsNullInExpressionTree_TargetTyped_ProducesNoDiagnostic() 198 | { 199 | string source = @" 200 | using System; 201 | using System.Linq.Expressions; 202 | 203 | class Test 204 | { 205 | void Method() 206 | { 207 | Expression> e = s => s == null; 208 | } 209 | }"; 210 | 211 | await VerifyCS.VerifyAnalyzerAsync(source); 212 | } 213 | 214 | [Fact] 215 | public async Task EqualsDefaultInIfExpression_ProducesDiagnostic() 216 | { 217 | string source = @" 218 | class Test 219 | { 220 | void Method(object o) 221 | { 222 | if (o [|== default|]) 223 | { 224 | } 225 | } 226 | }"; 227 | 228 | string fixedSource = @" 229 | class Test 230 | { 231 | void Method(object o) 232 | { 233 | if (o is null) 234 | { 235 | } 236 | } 237 | }"; 238 | 239 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource); 240 | } 241 | 242 | [Fact] 243 | public async Task EqualsDefaultKeywordInIfExpression_ProducesDiagnostic() 244 | { 245 | string source = @" 246 | class Test 247 | { 248 | void Method(string s) 249 | { 250 | if (s [|== default(string)|]) 251 | { 252 | } 253 | } 254 | }"; 255 | 256 | string fixedSource = @" 257 | class Test 258 | { 259 | void Method(string s) 260 | { 261 | if (s is null) 262 | { 263 | } 264 | } 265 | }"; 266 | 267 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource); 268 | } 269 | 270 | [Fact] 271 | public async Task EqualsDefaultTInIfExpression_ProducesDiagnostic() 272 | { 273 | string source = @" 274 | class Test 275 | { 276 | void Method(Test t) 277 | { 278 | if (t [|== default(Test)|]) 279 | { 280 | } 281 | } 282 | }"; 283 | 284 | string fixedSource = @" 285 | class Test 286 | { 287 | void Method(Test t) 288 | { 289 | if (t is null) 290 | { 291 | } 292 | } 293 | }"; 294 | 295 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource); 296 | } 297 | 298 | [Fact] 299 | public async Task EqualsDefaultValueType_ProducesNoDiagnostic() 300 | { 301 | string source = @" 302 | class Test 303 | { 304 | void Method(int o) 305 | { 306 | if (o == default) 307 | { 308 | } 309 | } 310 | }"; 311 | 312 | await VerifyCS.VerifyAnalyzerAsync(source); 313 | } 314 | 315 | [Fact] 316 | public async Task CodeFixDoesNotAffectFormatting() 317 | { 318 | string source = @" 319 | class Test 320 | { 321 | string SafeString(string? message) 322 | { 323 | return message [|== null|] // Some Comment 324 | ? string.Empty 325 | : message; 326 | } 327 | }"; 328 | 329 | string fixedSource = @" 330 | class Test 331 | { 332 | string SafeString(string? message) 333 | { 334 | return message is null // Some Comment 335 | ? string.Empty 336 | : message; 337 | } 338 | }"; 339 | 340 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource); 341 | } 342 | 343 | [Fact] 344 | public async Task CodeFixDoesNotAffectFormattingReversed() 345 | { 346 | string source = @" 347 | class Test 348 | { 349 | string SafeString(string? message) 350 | { 351 | return [|null ==|] message 352 | ? string.Empty 353 | : message; 354 | } 355 | }"; 356 | 357 | string fixedSource = @" 358 | class Test 359 | { 360 | string SafeString(string? message) 361 | { 362 | return message is null 363 | ? string.Empty 364 | : message; 365 | } 366 | }"; 367 | 368 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource); 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /test/CSharpIsNullAnalyzer.Tests/CSIsNull002Tests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using CSharpIsNullAnalyzer; 5 | using Xunit; 6 | using VerifyCS = CSharpCodeFixVerifier; 7 | 8 | public class CSIsNull002Tests 9 | { 10 | [Fact] 11 | public async Task NotEqualsNullInExpressionBody_ProducesDiagnostic() 12 | { 13 | string source = @" 14 | class Test 15 | { 16 | bool Method(object o) => o [|!= null|]; 17 | }"; 18 | 19 | string fixedSource1 = @" 20 | class Test 21 | { 22 | bool Method(object o) => o is object; 23 | }"; 24 | 25 | string fixedSource2 = @" 26 | class Test 27 | { 28 | bool Method(object o) => o is not null; 29 | }"; 30 | 31 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource1, CSIsNull002Fixer.IsObjectEquivalenceKey); 32 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource2, CSIsNull002Fixer.IsNotNullEquivalenceKey); 33 | } 34 | 35 | [Fact] 36 | public async Task NullNotEqualsInExpressionBody_ProducesDiagnostic() 37 | { 38 | string source = @" 39 | class Test 40 | { 41 | bool Method(object o) => [|null !=|] o; 42 | }"; 43 | 44 | string fixedSource1 = @" 45 | class Test 46 | { 47 | bool Method(object o) => o is object; 48 | }"; 49 | 50 | string fixedSource2 = @" 51 | class Test 52 | { 53 | bool Method(object o) => o is not null; 54 | }"; 55 | 56 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource1, CSIsNull002Fixer.IsObjectEquivalenceKey); 57 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource2, CSIsNull002Fixer.IsNotNullEquivalenceKey); 58 | } 59 | 60 | [Fact] 61 | public async Task NotEqualsNullInIfExpression_ProducesDiagnostic() 62 | { 63 | string source = @" 64 | class Test 65 | { 66 | void Method(object o) 67 | { 68 | if (o [|!= null|]) 69 | { 70 | } 71 | } 72 | }"; 73 | 74 | string fixedSource1 = @" 75 | class Test 76 | { 77 | void Method(object o) 78 | { 79 | if (o is object) 80 | { 81 | } 82 | } 83 | }"; 84 | 85 | string fixedSource2 = @" 86 | class Test 87 | { 88 | void Method(object o) 89 | { 90 | if (o is not null) 91 | { 92 | } 93 | } 94 | }"; 95 | 96 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource1, CSIsNull002Fixer.IsObjectEquivalenceKey); 97 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource2, CSIsNull002Fixer.IsNotNullEquivalenceKey); 98 | } 99 | 100 | [Fact] 101 | public async Task NullNotEqualsInIfExpression_ProducesDiagnostic() 102 | { 103 | string source = @" 104 | class Test 105 | { 106 | void Method(object o) 107 | { 108 | if ([|null !=|] o) 109 | { 110 | } 111 | } 112 | }"; 113 | 114 | string fixedSource1 = @" 115 | class Test 116 | { 117 | void Method(object o) 118 | { 119 | if (o is object) 120 | { 121 | } 122 | } 123 | }"; 124 | 125 | string fixedSource2 = @" 126 | class Test 127 | { 128 | void Method(object o) 129 | { 130 | if (o is not null) 131 | { 132 | } 133 | } 134 | }"; 135 | 136 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource1, CSIsNull002Fixer.IsObjectEquivalenceKey); 137 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource2, CSIsNull002Fixer.IsNotNullEquivalenceKey); 138 | } 139 | 140 | [Fact] 141 | public async Task NullNotEqualsInArgument_ProducesDiagnostic() 142 | { 143 | string source = @" 144 | class Test 145 | { 146 | void Method(object o) 147 | { 148 | Other([|null !=|] o); 149 | } 150 | 151 | void Other(bool condition) { } 152 | }"; 153 | 154 | string fixedSource1 = @" 155 | class Test 156 | { 157 | void Method(object o) 158 | { 159 | Other(o is object); 160 | } 161 | 162 | void Other(bool condition) { } 163 | }"; 164 | 165 | string fixedSource2 = @" 166 | class Test 167 | { 168 | void Method(object o) 169 | { 170 | Other(o is not null); 171 | } 172 | 173 | void Other(bool condition) { } 174 | }"; 175 | 176 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource1, CSIsNull002Fixer.IsObjectEquivalenceKey); 177 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource2, CSIsNull002Fixer.IsNotNullEquivalenceKey); 178 | } 179 | 180 | [Fact] 181 | public async Task NotEqualsNullInLargerExpressionInExpressionTree_OffersOneCodeFix() 182 | { 183 | string source = @" 184 | using System; 185 | using System.Linq.Expressions; 186 | class Test 187 | { 188 | void Method(int o) 189 | { 190 | bool? b = true; 191 | Expression> e = () => (b [|!= null|] || 3 < 5); 192 | } 193 | }"; 194 | 195 | string fixedSource = @" 196 | using System; 197 | using System.Linq.Expressions; 198 | class Test 199 | { 200 | void Method(int o) 201 | { 202 | bool? b = true; 203 | Expression> e = () => (b is object || 3 < 5); 204 | } 205 | }"; 206 | 207 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource, CSIsNull002Fixer.IsObjectEquivalenceKey); 208 | await VerifyCS.VerifyCodeFixAsync(source, source, CSIsNull002Fixer.IsNotNullEquivalenceKey); // assert that this fix is not offered. 209 | } 210 | 211 | [Fact] 212 | public async Task NullNotEqualsInLargerExpressionInExpressionTree_OffersOneCodeFix() 213 | { 214 | string source = @" 215 | using System; 216 | using System.Linq.Expressions; 217 | class Test 218 | { 219 | void Method(int o) 220 | { 221 | bool? b = true; 222 | Expression> e = () => ([|null !=|] b || 3 < 5); 223 | } 224 | }"; 225 | 226 | string fixedSource = @" 227 | using System; 228 | using System.Linq.Expressions; 229 | class Test 230 | { 231 | void Method(int o) 232 | { 233 | bool? b = true; 234 | Expression> e = () => (b is object || 3 < 5); 235 | } 236 | }"; 237 | 238 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource, CSIsNull002Fixer.IsObjectEquivalenceKey); 239 | await VerifyCS.VerifyCodeFixAsync(source, source, CSIsNull002Fixer.IsNotNullEquivalenceKey); // assert that this fix is not offered. 240 | } 241 | 242 | [Fact] 243 | public async Task NotEqualsNullInExpressionTree_TargetTyped_OffersOneCodeFix() 244 | { 245 | string source = @" 246 | using System; 247 | using System.Linq.Expressions; 248 | 249 | class Test 250 | { 251 | void Method() 252 | { 253 | Expression> e = s => s [|!= null|]; 254 | } 255 | }"; 256 | 257 | string fixedSource = @" 258 | using System; 259 | using System.Linq.Expressions; 260 | 261 | class Test 262 | { 263 | void Method() 264 | { 265 | Expression> e = s => s is object; 266 | } 267 | }"; 268 | 269 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource, CSIsNull002Fixer.IsObjectEquivalenceKey); 270 | await VerifyCS.VerifyCodeFixAsync(source, source, CSIsNull002Fixer.IsNotNullEquivalenceKey); // assert that this fix is not offered. 271 | } 272 | 273 | [Fact] 274 | public async Task NotEqualsNullInExpressionTree_OffersOneCodeFix() 275 | { 276 | string source = @" 277 | using System; 278 | using System.Linq.Expressions; 279 | 280 | class Test 281 | { 282 | void Method() 283 | { 284 | _ = (Expression>)(s => s [|!= null|]); 285 | } 286 | }"; 287 | 288 | string fixedSource = @" 289 | using System; 290 | using System.Linq.Expressions; 291 | 292 | class Test 293 | { 294 | void Method() 295 | { 296 | _ = (Expression>)(s => s is object); 297 | } 298 | }"; 299 | 300 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource, CSIsNull002Fixer.IsObjectEquivalenceKey); 301 | await VerifyCS.VerifyCodeFixAsync(source, source, CSIsNull002Fixer.IsNotNullEquivalenceKey); // assert that this fix is not offered. 302 | } 303 | 304 | [Fact] 305 | public async Task NotEqualsDefaultInIfExpression_ProducesDiagnostic() 306 | { 307 | string source = @" 308 | class Test 309 | { 310 | void Method(object o) 311 | { 312 | if (o [|!= default|]) 313 | { 314 | } 315 | } 316 | }"; 317 | 318 | string fixedSource1 = @" 319 | class Test 320 | { 321 | void Method(object o) 322 | { 323 | if (o is object) 324 | { 325 | } 326 | } 327 | }"; 328 | 329 | string fixedSource2 = @" 330 | class Test 331 | { 332 | void Method(object o) 333 | { 334 | if (o is not null) 335 | { 336 | } 337 | } 338 | }"; 339 | 340 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource1, CSIsNull002Fixer.IsObjectEquivalenceKey); 341 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource2, CSIsNull002Fixer.IsNotNullEquivalenceKey); 342 | } 343 | 344 | [Fact] 345 | public async Task NotEqualsDefaultKeywordInIfExpression_ProducesDiagnostic() 346 | { 347 | string source = @" 348 | class Test 349 | { 350 | void Method(string s) 351 | { 352 | if (s [|!= default(string)|]) 353 | { 354 | } 355 | } 356 | }"; 357 | 358 | string fixedSource1 = @" 359 | class Test 360 | { 361 | void Method(string s) 362 | { 363 | if (s is object) 364 | { 365 | } 366 | } 367 | }"; 368 | 369 | string fixedSource2 = @" 370 | class Test 371 | { 372 | void Method(string s) 373 | { 374 | if (s is not null) 375 | { 376 | } 377 | } 378 | }"; 379 | 380 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource1, CSIsNull002Fixer.IsObjectEquivalenceKey); 381 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource2, CSIsNull002Fixer.IsNotNullEquivalenceKey); 382 | } 383 | 384 | [Fact] 385 | public async Task NotEqualsDefaultTInIfExpression_ProducesDiagnostic() 386 | { 387 | string source = @" 388 | class Test 389 | { 390 | void Method(Test t) 391 | { 392 | if (t [|!= default(Test)|]) 393 | { 394 | } 395 | } 396 | }"; 397 | 398 | string fixedSource1 = @" 399 | class Test 400 | { 401 | void Method(Test t) 402 | { 403 | if (t is object) 404 | { 405 | } 406 | } 407 | }"; 408 | 409 | string fixedSource2 = @" 410 | class Test 411 | { 412 | void Method(Test t) 413 | { 414 | if (t is not null) 415 | { 416 | } 417 | } 418 | }"; 419 | 420 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource1, CSIsNull002Fixer.IsObjectEquivalenceKey); 421 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource2, CSIsNull002Fixer.IsNotNullEquivalenceKey); 422 | } 423 | 424 | [Fact] 425 | public async Task NotEqualsDefaultValueType_ProducesNoDiagnostic() 426 | { 427 | string source = @" 428 | class Test 429 | { 430 | void Method(int o) 431 | { 432 | if (o != default) 433 | { 434 | } 435 | } 436 | }"; 437 | 438 | await VerifyCS.VerifyAnalyzerAsync(source); 439 | } 440 | 441 | [Fact] 442 | public async Task CodeFixDoesNotAffectFormatting() 443 | { 444 | string source = @" 445 | class Test 446 | { 447 | string SafeString(string? message) 448 | { 449 | return message [|!= null|] // Some Comment 450 | ? message 451 | : string.Empty; 452 | } 453 | }"; 454 | 455 | string fixedSource1 = @" 456 | class Test 457 | { 458 | string SafeString(string? message) 459 | { 460 | return message is object // Some Comment 461 | ? message 462 | : string.Empty; 463 | } 464 | }"; 465 | 466 | string fixedSource2 = @" 467 | class Test 468 | { 469 | string SafeString(string? message) 470 | { 471 | return message is not null // Some Comment 472 | ? message 473 | : string.Empty; 474 | } 475 | }"; 476 | 477 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource1, CSIsNull002Fixer.IsObjectEquivalenceKey); 478 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource2, CSIsNull002Fixer.IsNotNullEquivalenceKey); 479 | } 480 | 481 | [Fact] 482 | public async Task CodeFixDoesNotAffectFormattingReversed() 483 | { 484 | string source = @" 485 | class Test 486 | { 487 | string SafeString(string? message) 488 | { 489 | return [|null !=|] message // Some Comment 490 | ? message 491 | : string.Empty; 492 | } 493 | }"; 494 | 495 | string fixedSource1 = @" 496 | class Test 497 | { 498 | string SafeString(string? message) 499 | { 500 | return message is object // Some Comment 501 | ? message 502 | : string.Empty; 503 | } 504 | }"; 505 | 506 | string fixedSource2 = @" 507 | class Test 508 | { 509 | string SafeString(string? message) 510 | { 511 | return message is not null // Some Comment 512 | ? message 513 | : string.Empty; 514 | } 515 | }"; 516 | 517 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource1, CSIsNull002Fixer.IsObjectEquivalenceKey); 518 | await VerifyCS.VerifyCodeFixAsync(source, fixedSource2, CSIsNull002Fixer.IsNotNullEquivalenceKey); 519 | } 520 | } 521 | -------------------------------------------------------------------------------- /test/CSharpIsNullAnalyzer.Tests/CSharpIsNullAnalyzer.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0;net472 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/CSharpIsNullAnalyzer.Tests/Helpers/CSharpCodeFixVerifier`2+Test.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System.Reflection; 5 | using Microsoft.CodeAnalysis.CSharp; 6 | using Microsoft.CodeAnalysis.CSharp.Testing; 7 | using Microsoft.CodeAnalysis.Testing; 8 | using Microsoft.CodeAnalysis.Text; 9 | 10 | public static partial class CSharpCodeFixVerifier 11 | { 12 | public class Test : CSharpCodeFixTest 13 | { 14 | public Test() 15 | { 16 | this.ReferenceAssemblies = ReferencesHelper.DefaultReferences; 17 | this.TestBehaviors |= Microsoft.CodeAnalysis.Testing.TestBehaviors.SkipGeneratedCodeCheck; 18 | 19 | this.SolutionTransforms.Add((solution, projectId) => 20 | { 21 | var parseOptions = (CSharpParseOptions)solution.GetProject(projectId)!.ParseOptions!; 22 | solution = solution.WithProjectParseOptions(projectId, parseOptions.WithLanguageVersion(LanguageVersion.CSharp9)); 23 | 24 | return solution; 25 | }); 26 | 27 | this.TestState.AdditionalFilesFactories.Add(() => 28 | { 29 | const string additionalFilePrefix = "AdditionalFiles."; 30 | return from resourceName in Assembly.GetExecutingAssembly().GetManifestResourceNames() 31 | where resourceName.StartsWith(additionalFilePrefix, StringComparison.Ordinal) 32 | let content = ReadManifestResource(Assembly.GetExecutingAssembly(), resourceName) 33 | select (filename: resourceName.Substring(additionalFilePrefix.Length), SourceText.From(content)); 34 | }); 35 | } 36 | 37 | private static string ReadManifestResource(Assembly assembly, string resourceName) 38 | { 39 | using (var reader = new StreamReader(assembly.GetManifestResourceStream(resourceName) ?? throw new ArgumentException("No such resource stream", nameof(resourceName)))) 40 | { 41 | return reader.ReadToEnd(); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/CSharpIsNullAnalyzer.Tests/Helpers/CSharpCodeFixVerifier`2.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CodeFixes; 6 | using Microsoft.CodeAnalysis.CSharp.Testing; 7 | using Microsoft.CodeAnalysis.Diagnostics; 8 | using Microsoft.CodeAnalysis.Testing; 9 | 10 | public static partial class CSharpCodeFixVerifier 11 | where TAnalyzer : DiagnosticAnalyzer, new() 12 | where TCodeFix : CodeFixProvider, new() 13 | { 14 | public static DiagnosticResult Diagnostic() 15 | => CSharpCodeFixVerifier.Diagnostic(); 16 | 17 | public static DiagnosticResult Diagnostic(string diagnosticId) 18 | => CSharpCodeFixVerifier.Diagnostic(diagnosticId); 19 | 20 | public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) 21 | => new DiagnosticResult(descriptor); 22 | 23 | public static Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) 24 | { 25 | var test = new Test { TestCode = source }; 26 | test.ExpectedDiagnostics.AddRange(expected); 27 | return test.RunAsync(); 28 | } 29 | 30 | public static Task VerifyCodeFixAsync(string source, string fixedSource, string? codeFixEquivalenceKey = null) 31 | => VerifyCodeFixAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource, codeFixEquivalenceKey); 32 | 33 | public static Task VerifyCodeFixAsync(string source, DiagnosticResult expected, string fixedSource) 34 | => VerifyCodeFixAsync(source, new[] { expected }, fixedSource); 35 | 36 | public static Task VerifyCodeFixAsync(string source, DiagnosticResult[] expected, string fixedSource, string? codeFixEquivalenceKey = null) 37 | { 38 | var test = new Test 39 | { 40 | TestCode = source, 41 | FixedCode = fixedSource, 42 | CodeActionEquivalenceKey = codeFixEquivalenceKey, 43 | }; 44 | 45 | test.ExpectedDiagnostics.AddRange(expected); 46 | return test.RunAsync(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/CSharpIsNullAnalyzer.Tests/Helpers/ReferencesHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Arnott. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using Microsoft.CodeAnalysis.Testing; 5 | 6 | internal static class ReferencesHelper 7 | { 8 | internal static ReferenceAssemblies DefaultReferences = ReferenceAssemblies.NetFramework.Net472.Default; 9 | } 10 | -------------------------------------------------------------------------------- /test/CSharpIsNullAnalyzer.Tests/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | false 7 | true 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tools/Check-DotNetRuntime.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Checks whether a given .NET Core runtime is installed. 4 | #> 5 | [CmdletBinding()] 6 | Param ( 7 | [Parameter()] 8 | [ValidateSet('Microsoft.AspNetCore.App','Microsoft.NETCore.App')] 9 | [string]$Runtime='Microsoft.NETCore.App', 10 | [Parameter(Mandatory=$true)] 11 | [Version]$Version 12 | ) 13 | 14 | $dotnet = Get-Command dotnet -ErrorAction SilentlyContinue 15 | if (!$dotnet) { 16 | # Nothing is installed. 17 | Write-Output $false 18 | exit 1 19 | } 20 | 21 | Function IsVersionMatch { 22 | Param( 23 | [Parameter()] 24 | $actualVersion 25 | ) 26 | return $actualVersion -and 27 | $Version.Major -eq $actualVersion.Major -and 28 | $Version.Minor -eq $actualVersion.Minor -and 29 | (($Version.Build -eq -1) -or ($Version.Build -eq $actualVersion.Build)) -and 30 | (($Version.Revision -eq -1) -or ($Version.Revision -eq $actualVersion.Revision)) 31 | } 32 | 33 | $installedRuntimes = dotnet --list-runtimes |? { $_.Split()[0] -ieq $Runtime } |% { $v = $null; [Version]::tryparse($_.Split()[1], [ref] $v); $v } 34 | $matchingRuntimes = $installedRuntimes |? { IsVersionMatch -actualVersion $_ } 35 | if (!$matchingRuntimes) { 36 | Write-Output $false 37 | exit 1 38 | } 39 | 40 | Write-Output $true 41 | exit 0 42 | -------------------------------------------------------------------------------- /tools/Check-DotNetSdk.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Checks whether the .NET Core SDK required by this repo is installed. 4 | #> 5 | [CmdletBinding()] 6 | Param ( 7 | ) 8 | 9 | $dotnet = Get-Command dotnet -ErrorAction SilentlyContinue 10 | if (!$dotnet) { 11 | # Nothing is installed. 12 | Write-Output $false 13 | exit 1 14 | } 15 | 16 | # We need to set the current directory so dotnet considers the SDK required by our global.json file. 17 | Push-Location "$PSScriptRoot\.." 18 | try { 19 | dotnet -h 2>&1 | Out-Null 20 | if (($LASTEXITCODE -eq 129) -or # On Linux 21 | ($LASTEXITCODE -eq -2147450751) # On Windows 22 | ) { 23 | # These exit codes indicate no matching SDK exists. 24 | Write-Output $false 25 | exit 2 26 | } 27 | 28 | # The required SDK is already installed! 29 | Write-Output $true 30 | exit 0 31 | } catch { 32 | # I don't know why, but on some build agents (e.g. MicroBuild), an exception is thrown from the `dotnet` invocation when a match is not found. 33 | Write-Output $false 34 | exit 3 35 | } finally { 36 | Pop-Location 37 | } 38 | -------------------------------------------------------------------------------- /tools/Install-DotNetSdk.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | <# 4 | .SYNOPSIS 5 | Installs the .NET SDK specified in the global.json file at the root of this repository, 6 | along with supporting .NET runtimes used for testing. 7 | .DESCRIPTION 8 | This MAY not require elevation, as the SDK and runtimes are installed locally to this repo location, 9 | unless `-InstallLocality machine` is specified. 10 | .PARAMETER InstallLocality 11 | A value indicating whether dependencies should be installed locally to the repo or at a per-user location. 12 | Per-user allows sharing the installed dependencies across repositories and allows use of a shared expanded package cache. 13 | Visual Studio will only notice and use these SDKs/runtimes if VS is launched from the environment that runs this script. 14 | Per-repo allows for high isolation, allowing for a more precise recreation of the environment within an Azure Pipelines build. 15 | When using 'repo', environment variables are set to cause the locally installed dotnet SDK to be used. 16 | Per-repo can lead to file locking issues when dotnet.exe is left running as a build server and can be mitigated by running `dotnet build-server shutdown`. 17 | Per-machine requires elevation and will download and install all SDKs and runtimes to machine-wide locations so all applications can find it. 18 | .PARAMETER SdkOnly 19 | Skips installing the runtime. 20 | .PARAMETER IncludeX86 21 | Installs a x86 SDK and runtimes in addition to the x64 ones. Only supported on Windows. Ignored on others. 22 | .PARAMETER IncludeAspNetCore 23 | Installs the ASP.NET Core runtime along with the .NET runtime. 24 | #> 25 | [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Medium')] 26 | Param ( 27 | [ValidateSet('repo','user','machine')] 28 | [string]$InstallLocality='user', 29 | [switch]$SdkOnly, 30 | [switch]$IncludeX86, 31 | [switch]$IncludeAspNetCore 32 | ) 33 | 34 | $DotNetInstallScriptRoot = "$PSScriptRoot/../obj/tools" 35 | if (!(Test-Path $DotNetInstallScriptRoot)) { New-Item -ItemType Directory -Path $DotNetInstallScriptRoot -WhatIf:$false | Out-Null } 36 | $DotNetInstallScriptRoot = Resolve-Path $DotNetInstallScriptRoot 37 | 38 | # Look up actual required .NET SDK version from global.json 39 | $sdkVersion = & "$PSScriptRoot/../azure-pipelines/variables/DotNetSdkVersion.ps1" 40 | 41 | If ($IncludeX86 -and ($IsMacOS -or $IsLinux)) { 42 | Write-Verbose "Ignoring -IncludeX86 switch because 32-bit runtimes are only supported on Windows." 43 | $IncludeX86 = $false 44 | } 45 | 46 | $arch = [System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture 47 | if (!$arch) { # Windows Powershell leaves this blank 48 | $arch = 'x64' 49 | if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { $arch = 'ARM64' } 50 | if (${env:ProgramFiles(Arm)}) { $arch = 'ARM64' } 51 | } 52 | 53 | # Search for all .NET runtime versions referenced from MSBuild projects and arrange to install them. 54 | $runtimeVersions = @() 55 | $windowsDesktopRuntimeVersions = @() 56 | $aspnetRuntimeVersions = @() 57 | if (!$SdkOnly) { 58 | Get-ChildItem "$PSScriptRoot\..\src\*.*proj","$PSScriptRoot\..\test\*.*proj","$PSScriptRoot\..\Directory.Build.props" -Recurse |% { 59 | $projXml = [xml](Get-Content -Path $_) 60 | $pg = $projXml.Project.PropertyGroup 61 | if ($pg) { 62 | $targetFrameworks = @() 63 | $tf = $pg.TargetFramework 64 | $targetFrameworks += $tf 65 | $tfs = $pg.TargetFrameworks 66 | if ($tfs) { 67 | $targetFrameworks = $tfs -Split ';' 68 | } 69 | } 70 | $targetFrameworks |? { $_ -match 'net(?:coreapp)?(\d+\.\d+)' } |% { 71 | $v = $Matches[1] 72 | $runtimeVersions += $v 73 | $aspnetRuntimeVersions += $v 74 | if ($v -ge '3.0' -and -not ($IsMacOS -or $IsLinux)) { 75 | $windowsDesktopRuntimeVersions += $v 76 | } 77 | } 78 | 79 | # Add target frameworks of the form: netXX 80 | $targetFrameworks |? { $_ -match 'net(\d+\.\d+)' } |% { 81 | $v = $Matches[1] 82 | $runtimeVersions += $v 83 | $aspnetRuntimeVersions += $v 84 | if (-not ($IsMacOS -or $IsLinux)) { 85 | $windowsDesktopRuntimeVersions += $v 86 | } 87 | } 88 | } 89 | } 90 | 91 | if (!$IncludeAspNetCore) { 92 | $aspnetRuntimeVersions = @() 93 | } 94 | 95 | Function Get-FileFromWeb([Uri]$Uri, $OutDir) { 96 | $OutFile = Join-Path $OutDir $Uri.Segments[-1] 97 | if (!(Test-Path $OutFile)) { 98 | Write-Verbose "Downloading $Uri..." 99 | if (!(Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir | Out-Null } 100 | try { 101 | (New-Object System.Net.WebClient).DownloadFile($Uri, $OutFile) 102 | } finally { 103 | # This try/finally causes the script to abort 104 | } 105 | } 106 | 107 | $OutFile 108 | } 109 | 110 | Function Get-InstallerExe( 111 | $Version, 112 | $Architecture, 113 | [ValidateSet('Sdk','Runtime','WindowsDesktop')] 114 | [string]$sku 115 | ) { 116 | # Get the latest/actual version for the specified one 117 | $TypedVersion = $null 118 | if (![Version]::TryParse($Version, [ref] $TypedVersion)) { 119 | Write-Error "Unable to parse $Version into an a.b.c.d version. This version cannot be installed machine-wide." 120 | exit 1 121 | } 122 | 123 | if ($TypedVersion.Build -eq -1) { 124 | $versionInfo = -Split (Invoke-WebRequest -Uri "https://dotnetcli.blob.core.windows.net/dotnet/$sku/$Version/latest.version" -UseBasicParsing) 125 | $Version = $versionInfo[-1] 126 | } 127 | 128 | $majorMinor = "$($TypedVersion.Major).$($TypedVersion.Minor)" 129 | $ReleasesFile = Join-Path $DotNetInstallScriptRoot "$majorMinor\releases.json" 130 | if (!(Test-Path $ReleasesFile)) { 131 | Get-FileFromWeb -Uri "https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/$majorMinor/releases.json" -OutDir (Split-Path $ReleasesFile) | Out-Null 132 | } 133 | 134 | $releases = Get-Content $ReleasesFile | ConvertFrom-Json 135 | $url = $null 136 | foreach ($release in $releases.releases) { 137 | $filesElement = $null 138 | if ($release.$sku.version -eq $Version) { 139 | $filesElement = $release.$sku.files 140 | } 141 | if (!$filesElement -and ($sku -eq 'sdk') -and $release.sdks) { 142 | foreach ($sdk in $release.sdks) { 143 | if ($sdk.version -eq $Version) { 144 | $filesElement = $sdk.files 145 | break 146 | } 147 | } 148 | } 149 | 150 | if ($filesElement) { 151 | foreach ($file in $filesElement) { 152 | if ($file.rid -eq "win-$Architecture") { 153 | $url = $file.url 154 | Break 155 | } 156 | } 157 | 158 | if ($url) { 159 | Break 160 | } 161 | } 162 | } 163 | 164 | if ($url) { 165 | Get-FileFromWeb -Uri $url -OutDir $DotNetInstallScriptRoot 166 | } else { 167 | throw "Unable to find release of $sku v$Version" 168 | } 169 | } 170 | 171 | Function Install-DotNet($Version, $Architecture, [ValidateSet('Sdk','Runtime','WindowsDesktop','AspNetCore')][string]$sku = 'Sdk') { 172 | Write-Host "Downloading .NET $sku $Version..." 173 | $Installer = Get-InstallerExe -Version $Version -Architecture $Architecture -sku $sku 174 | Write-Host "Installing .NET $sku $Version..." 175 | cmd /c start /wait $Installer /install /passive /norestart 176 | if ($LASTEXITCODE -eq 3010) { 177 | Write-Verbose "Restart required" 178 | } elseif ($LASTEXITCODE -ne 0) { 179 | throw "Failure to install .NET SDK" 180 | } 181 | } 182 | 183 | $switches = @() 184 | $envVars = @{ 185 | # For locally installed dotnet, skip first time experience which takes a long time 186 | 'DOTNET_SKIP_FIRST_TIME_EXPERIENCE' = 'true'; 187 | } 188 | 189 | if ($InstallLocality -eq 'machine') { 190 | if ($IsMacOS -or $IsLinux) { 191 | $DotNetInstallDir = '/usr/share/dotnet' 192 | } else { 193 | $restartRequired = $false 194 | if ($PSCmdlet.ShouldProcess(".NET SDK $sdkVersion", "Install")) { 195 | Install-DotNet -Version $sdkVersion -Architecture $arch 196 | $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) 197 | 198 | if ($IncludeX86) { 199 | Install-DotNet -Version $sdkVersion -Architecture x86 200 | $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) 201 | } 202 | } 203 | 204 | $runtimeVersions | Sort-Object | Get-Unique |% { 205 | if ($PSCmdlet.ShouldProcess(".NET runtime $_", "Install")) { 206 | Install-DotNet -Version $_ -sku Runtime -Architecture $arch 207 | $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) 208 | 209 | if ($IncludeX86) { 210 | Install-DotNet -Version $_ -sku Runtime -Architecture x86 211 | $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) 212 | } 213 | } 214 | } 215 | 216 | $windowsDesktopRuntimeVersions | Sort-Object | Get-Unique |% { 217 | if ($PSCmdlet.ShouldProcess(".NET Windows Desktop $_", "Install")) { 218 | Install-DotNet -Version $_ -sku WindowsDesktop -Architecture $arch 219 | $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) 220 | 221 | if ($IncludeX86) { 222 | Install-DotNet -Version $_ -sku WindowsDesktop -Architecture x86 223 | $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) 224 | } 225 | } 226 | } 227 | 228 | $aspnetRuntimeVersions | Sort-Object | Get-Unique |% { 229 | if ($PSCmdlet.ShouldProcess("ASP.NET Core $_", "Install")) { 230 | Install-DotNet -Version $_ -sku AspNetCore -Architecture $arch 231 | $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) 232 | 233 | if ($IncludeX86) { 234 | Install-DotNet -Version $_ -sku AspNetCore -Architecture x86 235 | $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) 236 | } 237 | } 238 | } 239 | if ($restartRequired) { 240 | Write-Host -ForegroundColor Yellow "System restart required" 241 | Exit 3010 242 | } 243 | 244 | return 245 | } 246 | } elseif ($InstallLocality -eq 'repo') { 247 | $DotNetInstallDir = "$DotNetInstallScriptRoot/.dotnet" 248 | $DotNetX86InstallDir = "$DotNetInstallScriptRoot/x86/.dotnet" 249 | } elseif ($env:AGENT_TOOLSDIRECTORY) { 250 | $DotNetInstallDir = "$env:AGENT_TOOLSDIRECTORY/dotnet" 251 | $DotNetX86InstallDir = "$env:AGENT_TOOLSDIRECTORY/x86/dotnet" 252 | } else { 253 | $DotNetInstallDir = Join-Path $HOME .dotnet 254 | } 255 | 256 | if ($DotNetInstallDir) { 257 | if (!(Test-Path $DotNetInstallDir)) { New-Item -ItemType Directory -Path $DotNetInstallDir } 258 | $DotNetInstallDir = Resolve-Path $DotNetInstallDir 259 | Write-Host "Installing .NET SDK and runtimes to $DotNetInstallDir" -ForegroundColor Blue 260 | $envVars['DOTNET_MULTILEVEL_LOOKUP'] = '0' 261 | $envVars['DOTNET_ROOT'] = $DotNetInstallDir 262 | } 263 | 264 | if ($IncludeX86) { 265 | if ($DotNetX86InstallDir) { 266 | if (!(Test-Path $DotNetX86InstallDir)) { New-Item -ItemType Directory -Path $DotNetX86InstallDir } 267 | $DotNetX86InstallDir = Resolve-Path $DotNetX86InstallDir 268 | Write-Host "Installing x86 .NET SDK and runtimes to $DotNetX86InstallDir" -ForegroundColor Blue 269 | } else { 270 | # Only machine-wide or repo-wide installations can handle two unique dotnet.exe architectures. 271 | Write-Error "The installation location or OS isn't supported for x86 installation. Try a different -InstallLocality value." 272 | return 1 273 | } 274 | } 275 | 276 | if ($IsMacOS -or $IsLinux) { 277 | $DownloadUri = "https://raw.githubusercontent.com/dotnet/install-scripts/0b09de9bc136cacb5f849a6957ebd4062173c148/src/dotnet-install.sh" 278 | $DotNetInstallScriptPath = "$DotNetInstallScriptRoot/dotnet-install.sh" 279 | } else { 280 | $DownloadUri = "https://raw.githubusercontent.com/dotnet/install-scripts/0b09de9bc136cacb5f849a6957ebd4062173c148/src/dotnet-install.ps1" 281 | $DotNetInstallScriptPath = "$DotNetInstallScriptRoot/dotnet-install.ps1" 282 | } 283 | 284 | if (-not (Test-Path $DotNetInstallScriptPath)) { 285 | Invoke-WebRequest -Uri $DownloadUri -OutFile $DotNetInstallScriptPath -UseBasicParsing 286 | if ($IsMacOS -or $IsLinux) { 287 | chmod +x $DotNetInstallScriptPath 288 | } 289 | } 290 | 291 | # In case the script we invoke is in a directory with spaces, wrap it with single quotes. 292 | # In case the path includes single quotes, escape them. 293 | $DotNetInstallScriptPathExpression = $DotNetInstallScriptPath.Replace("'", "''") 294 | $DotNetInstallScriptPathExpression = "& '$DotNetInstallScriptPathExpression'" 295 | 296 | $anythingInstalled = $false 297 | $global:LASTEXITCODE = 0 298 | 299 | if ($PSCmdlet.ShouldProcess(".NET SDK $sdkVersion", "Install")) { 300 | $anythingInstalled = $true 301 | Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Version $sdkVersion -Architecture $arch -InstallDir $DotNetInstallDir $switches" 302 | 303 | if ($LASTEXITCODE -ne 0) { 304 | Write-Error ".NET SDK installation failure: $LASTEXITCODE" 305 | exit $LASTEXITCODE 306 | } 307 | } else { 308 | Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Version $sdkVersion -Architecture $arch -InstallDir $DotNetInstallDir $switches -DryRun" 309 | } 310 | 311 | if ($IncludeX86) { 312 | if ($PSCmdlet.ShouldProcess(".NET x86 SDK $sdkVersion", "Install")) { 313 | $anythingInstalled = $true 314 | Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Version $sdkVersion -Architecture x86 -InstallDir $DotNetX86InstallDir $switches" 315 | 316 | if ($LASTEXITCODE -ne 0) { 317 | Write-Error ".NET x86 SDK installation failure: $LASTEXITCODE" 318 | exit $LASTEXITCODE 319 | } 320 | } else { 321 | Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Version $sdkVersion -Architecture x86 -InstallDir $DotNetX86InstallDir $switches -DryRun" 322 | } 323 | } 324 | 325 | $dotnetRuntimeSwitches = $switches + '-Runtime','dotnet' 326 | 327 | $runtimeVersions | Sort-Object -Unique |% { 328 | if ($PSCmdlet.ShouldProcess(".NET $Arch runtime $_", "Install")) { 329 | $anythingInstalled = $true 330 | Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $dotnetRuntimeSwitches" 331 | 332 | if ($LASTEXITCODE -ne 0) { 333 | Write-Error ".NET SDK installation failure: $LASTEXITCODE" 334 | exit $LASTEXITCODE 335 | } 336 | } else { 337 | Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $dotnetRuntimeSwitches -DryRun" 338 | } 339 | 340 | if ($IncludeX86) { 341 | if ($PSCmdlet.ShouldProcess(".NET x86 runtime $_", "Install")) { 342 | $anythingInstalled = $true 343 | Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $dotnetRuntimeSwitches" 344 | 345 | if ($LASTEXITCODE -ne 0) { 346 | Write-Error ".NET SDK installation failure: $LASTEXITCODE" 347 | exit $LASTEXITCODE 348 | } 349 | } else { 350 | Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $dotnetRuntimeSwitches -DryRun" 351 | } 352 | } 353 | } 354 | 355 | $windowsDesktopRuntimeSwitches = $switches + '-Runtime','windowsdesktop' 356 | 357 | $windowsDesktopRuntimeVersions | Sort-Object -Unique |% { 358 | if ($PSCmdlet.ShouldProcess(".NET WindowsDesktop $arch runtime $_", "Install")) { 359 | $anythingInstalled = $true 360 | Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $windowsDesktopRuntimeSwitches" 361 | 362 | if ($LASTEXITCODE -ne 0) { 363 | Write-Error ".NET SDK installation failure: $LASTEXITCODE" 364 | exit $LASTEXITCODE 365 | } 366 | } else { 367 | Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $windowsDesktopRuntimeSwitches -DryRun" 368 | } 369 | 370 | if ($IncludeX86) { 371 | if ($PSCmdlet.ShouldProcess(".NET WindowsDesktop x86 runtime $_", "Install")) { 372 | $anythingInstalled = $true 373 | Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $windowsDesktopRuntimeSwitches" 374 | 375 | if ($LASTEXITCODE -ne 0) { 376 | Write-Error ".NET SDK installation failure: $LASTEXITCODE" 377 | exit $LASTEXITCODE 378 | } 379 | } else { 380 | Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $windowsDesktopRuntimeSwitches -DryRun" 381 | } 382 | } 383 | } 384 | 385 | $aspnetRuntimeSwitches = $switches + '-Runtime','aspnetcore' 386 | 387 | $aspnetRuntimeVersions | Sort-Object -Unique |% { 388 | if ($PSCmdlet.ShouldProcess(".NET ASP.NET Core $arch runtime $_", "Install")) { 389 | $anythingInstalled = $true 390 | Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $aspnetRuntimeSwitches" 391 | 392 | if ($LASTEXITCODE -ne 0) { 393 | Write-Error ".NET SDK installation failure: $LASTEXITCODE" 394 | exit $LASTEXITCODE 395 | } 396 | } else { 397 | Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $aspnetRuntimeSwitches -DryRun" 398 | } 399 | 400 | if ($IncludeX86) { 401 | if ($PSCmdlet.ShouldProcess(".NET ASP.NET Core x86 runtime $_", "Install")) { 402 | $anythingInstalled = $true 403 | Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $aspnetRuntimeSwitches" 404 | 405 | if ($LASTEXITCODE -ne 0) { 406 | Write-Error ".NET SDK installation failure: $LASTEXITCODE" 407 | exit $LASTEXITCODE 408 | } 409 | } else { 410 | Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $aspnetRuntimeSwitches -DryRun" 411 | } 412 | } 413 | } 414 | 415 | if ($PSCmdlet.ShouldProcess("Set DOTNET environment variables to discover these installed runtimes?")) { 416 | & "$PSScriptRoot/Set-EnvVars.ps1" -Variables $envVars -PrependPath $DotNetInstallDir | Out-Null 417 | } 418 | 419 | if ($anythingInstalled -and ($InstallLocality -ne 'machine') -and !$env:TF_BUILD -and !$env:GITHUB_ACTIONS) { 420 | Write-Warning ".NET runtimes or SDKs were installed to a non-machine location. Perform your builds or open Visual Studio from this same environment in order for tools to discover the location of these dependencies." 421 | } 422 | -------------------------------------------------------------------------------- /tools/Install-NuGetCredProvider.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | <# 4 | .SYNOPSIS 5 | Downloads and installs the Microsoft Artifacts Credential Provider 6 | from https://github.com/microsoft/artifacts-credprovider 7 | to assist in authenticating to Azure Artifact feeds in interactive development 8 | or unattended build agents. 9 | .PARAMETER Force 10 | Forces install of the CredProvider plugin even if one already exists. This is useful to upgrade an older version. 11 | .PARAMETER AccessToken 12 | An optional access token for authenticating to Azure Artifacts authenticated feeds. 13 | #> 14 | [CmdletBinding()] 15 | Param ( 16 | [Parameter()] 17 | [switch]$Force, 18 | [Parameter()] 19 | [string]$AccessToken 20 | ) 21 | 22 | $envVars = @{} 23 | 24 | $toolsPath = & "$PSScriptRoot\..\azure-pipelines\Get-TempToolsPath.ps1" 25 | 26 | if ($IsMacOS -or $IsLinux) { 27 | $installerScript = "installcredprovider.sh" 28 | $sourceUrl = "https://raw.githubusercontent.com/microsoft/artifacts-credprovider/master/helpers/installcredprovider.sh" 29 | } else { 30 | $installerScript = "installcredprovider.ps1" 31 | $sourceUrl = "https://raw.githubusercontent.com/microsoft/artifacts-credprovider/master/helpers/installcredprovider.ps1" 32 | } 33 | 34 | $installerScript = Join-Path $toolsPath $installerScript 35 | 36 | if (!(Test-Path $installerScript) -or $Force) { 37 | Invoke-WebRequest $sourceUrl -OutFile $installerScript 38 | } 39 | 40 | $installerScript = (Resolve-Path $installerScript).Path 41 | 42 | if ($IsMacOS -or $IsLinux) { 43 | chmod u+x $installerScript 44 | } 45 | 46 | & $installerScript -Force:$Force -AddNetfx -InstallNet6 47 | 48 | if ($AccessToken) { 49 | $endpoints = @() 50 | 51 | $endpointURIs = @() 52 | Get-ChildItem "$PSScriptRoot\..\nuget.config" -Recurse |% { 53 | $nugetConfig = [xml](Get-Content -Path $_) 54 | 55 | $nugetConfig.configuration.packageSources.add |? { ($_.value -match '^https://pkgs\.dev\.azure\.com/') -or ($_.value -match '^https://[\w\-]+\.pkgs\.visualstudio\.com/') } |% { 56 | if ($endpointURIs -notcontains $_.Value) { 57 | $endpointURIs += $_.Value 58 | $endpoint = New-Object -TypeName PSObject 59 | Add-Member -InputObject $endpoint -MemberType NoteProperty -Name endpoint -Value $_.value 60 | Add-Member -InputObject $endpoint -MemberType NoteProperty -Name username -Value ado 61 | Add-Member -InputObject $endpoint -MemberType NoteProperty -Name password -Value $AccessToken 62 | $endpoints += $endpoint 63 | } 64 | } 65 | } 66 | 67 | $auth = New-Object -TypeName PSObject 68 | Add-Member -InputObject $auth -MemberType NoteProperty -Name endpointCredentials -Value $endpoints 69 | 70 | $authJson = ConvertTo-Json -InputObject $auth 71 | $envVars += @{ 72 | 'VSS_NUGET_EXTERNAL_FEED_ENDPOINTS'=$authJson; 73 | } 74 | } 75 | 76 | & "$PSScriptRoot/Set-EnvVars.ps1" -Variables $envVars | Out-Null 77 | -------------------------------------------------------------------------------- /tools/Set-EnvVars.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Set environment variables in the environment. 4 | Azure Pipeline and CMD environments are considered. 5 | .PARAMETER Variables 6 | A hashtable of variables to be set. 7 | .PARAMETER PrependPath 8 | A set of paths to prepend to the PATH environment variable. 9 | .OUTPUTS 10 | A boolean indicating whether the environment variables can be expected to propagate to the caller's environment. 11 | .DESCRIPTION 12 | The CmdEnvScriptPath environment variable may be optionally set to a path to a cmd shell script to be created (or appended to if it already exists) that will set the environment variables in cmd.exe that are set within the PowerShell environment. 13 | This is used by init.cmd in order to reapply any new environment variables to the parent cmd.exe process that were set in the powershell child process. 14 | #> 15 | [CmdletBinding(SupportsShouldProcess=$true)] 16 | Param( 17 | [Parameter(Mandatory=$true, Position=1)] 18 | $Variables, 19 | [string[]]$PrependPath 20 | ) 21 | 22 | if ($Variables.Count -eq 0) { 23 | return $true 24 | } 25 | 26 | $cmdInstructions = !$env:TF_BUILD -and !$env:GITHUB_ACTIONS -and !$env:CmdEnvScriptPath -and ($env:PS1UnderCmd -eq '1') 27 | if ($cmdInstructions) { 28 | Write-Warning "Environment variables have been set that will be lost because you're running under cmd.exe" 29 | Write-Host "Environment variables that must be set manually:" -ForegroundColor Blue 30 | } else { 31 | Write-Host "Environment variables set:" -ForegroundColor Blue 32 | Write-Host ($Variables | Out-String) 33 | if ($PrependPath) { 34 | Write-Host "Paths prepended to PATH: $PrependPath" 35 | } 36 | } 37 | 38 | if ($env:TF_BUILD) { 39 | Write-Host "Azure Pipelines detected. Logging commands will be used to propagate environment variables and prepend path." 40 | } 41 | 42 | if ($env:GITHUB_ACTIONS) { 43 | Write-Host "GitHub Actions detected. Logging commands will be used to propagate environment variables and prepend path." 44 | } 45 | 46 | $CmdEnvScript = '' 47 | $Variables.GetEnumerator() |% { 48 | Set-Item -Path env:$($_.Key) -Value $_.Value 49 | 50 | # If we're running in a cloud CI, set these environment variables so they propagate. 51 | if ($env:TF_BUILD) { 52 | Write-Host "##vso[task.setvariable variable=$($_.Key);]$($_.Value)" 53 | } 54 | if ($env:GITHUB_ACTIONS) { 55 | Add-Content -Path $env:GITHUB_ENV -Value "$($_.Key)=$($_.Value)" 56 | } 57 | 58 | if ($cmdInstructions) { 59 | Write-Host "SET $($_.Key)=$($_.Value)" 60 | } 61 | 62 | $CmdEnvScript += "SET $($_.Key)=$($_.Value)`r`n" 63 | } 64 | 65 | $pathDelimiter = ';' 66 | if ($IsMacOS -or $IsLinux) { 67 | $pathDelimiter = ':' 68 | } 69 | 70 | if ($PrependPath) { 71 | $PrependPath |% { 72 | $newPathValue = "$_$pathDelimiter$env:PATH" 73 | Set-Item -Path env:PATH -Value $newPathValue 74 | if ($cmdInstructions) { 75 | Write-Host "SET PATH=$newPathValue" 76 | } 77 | 78 | if ($env:TF_BUILD) { 79 | Write-Host "##vso[task.prependpath]$_" 80 | } 81 | if ($env:GITHUB_ACTIONS) { 82 | Add-Content -Path $env:GITHUB_PATH -Value $_ 83 | } 84 | 85 | $CmdEnvScript += "SET PATH=$_$pathDelimiter%PATH%" 86 | } 87 | } 88 | 89 | if ($env:CmdEnvScriptPath) { 90 | if (Test-Path $env:CmdEnvScriptPath) { 91 | $CmdEnvScript = (Get-Content -Path $env:CmdEnvScriptPath) + $CmdEnvScript 92 | } 93 | 94 | Set-Content -Path $env:CmdEnvScriptPath -Value $CmdEnvScript 95 | } 96 | 97 | return !$cmdInstructions 98 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", 3 | "version": "0.1", 4 | "assemblyVersion": { 5 | "precision": "revision" 6 | }, 7 | "publicReleaseRefSpec": [ 8 | "^refs/heads/main$", 9 | "^refs/heads/v\\d+(?:\\.\\d+)?$" 10 | ], 11 | "cloudBuild": { 12 | "setVersionVariables": false 13 | } 14 | } 15 | --------------------------------------------------------------------------------