├── InferNull ├── Properties │ └── .gitignore ├── AppDomainExtensions.cs ├── App.config ├── InferNull.csproj ├── ExportProviderExtensions.cs ├── ProcessRunner.cs └── Program.cs ├── global.json ├── .github ├── img │ ├── FlowState.png │ ├── GenericCall.png │ ├── GenericType.png │ └── SimpleClass.png └── workflows │ └── dotnet.yml ├── .gitattributes ├── NullabilityInference ├── Statistics.cs ├── NullabilityInference.csproj ├── CallbackOnDispose.cs ├── SimpleTypeParameter.cs ├── NullType.cs ├── MaximumFlow.cs ├── NullabilityEdge.cs ├── AccessPath.cs ├── AllNullableSyntaxRewriter.cs ├── NullabilityNode.cs ├── FlowState.cs ├── TypeWithNode.cs ├── GraphVizGraph.cs ├── InferredNullabilitySyntaxRewriter.cs ├── System.Diagnostics.CodeAnalysis │ └── NullableAttributes.cs ├── GraphBuildingSyntaxVisitor.cs ├── ExtensionMethods.cs └── NullCheckingEngine.cs ├── LICENSE ├── NullabilityInference.Tests ├── Tests.csproj ├── NullabilityTestHelper.cs └── FlowTests.cs ├── azure-pipelines.yml ├── NullabilityInference.sln ├── .editorconfig ├── .gitignore └── README.md /InferNull/Properties/.gitignore: -------------------------------------------------------------------------------- 1 | /launchSettings.json 2 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "3.1" 4 | } 5 | } -------------------------------------------------------------------------------- /.github/img/FlowState.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icsharpcode/NullabilityInference/HEAD/.github/img/FlowState.png -------------------------------------------------------------------------------- /.github/img/GenericCall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icsharpcode/NullabilityInference/HEAD/.github/img/GenericCall.png -------------------------------------------------------------------------------- /.github/img/GenericType.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icsharpcode/NullabilityInference/HEAD/.github/img/GenericType.png -------------------------------------------------------------------------------- /.github/img/SimpleClass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icsharpcode/NullabilityInference/HEAD/.github/img/SimpleClass.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=crlf 2 | *.dll binary 3 | *.png binary 4 | *.jpeg binary 5 | *.jpg binary 6 | *.gif binary 7 | *.ttf binary 8 | *.exe binary 9 | *.eot binary 10 | *.woff binary 11 | *.woff2 binary 12 | 13 | CHANGELOG.md text merge=union -------------------------------------------------------------------------------- /NullabilityInference/Statistics.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace ICSharpCode.NullabilityInference 6 | { 7 | public struct Statistics 8 | { 9 | public int NullableCount; 10 | public int NonNullCount; 11 | public int NotNullWhenCount; 12 | 13 | internal void Update(in Statistics s) 14 | { 15 | this.NullableCount += s.NullableCount; 16 | this.NonNullCount += s.NonNullCount; 17 | this.NotNullWhenCount += s.NotNullWhenCount; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NullabilityInference/NullabilityInference.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | ICSharpCode.NullabilityInference 6 | ICSharpCode.NullabilityInference 7 | 8.0 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /NullabilityInference/CallbackOnDispose.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace ICSharpCode.NullabilityInference 5 | { 6 | /// 7 | /// Invokes an action when it is disposed. 8 | /// 9 | /// 10 | /// This class ensures the callback is invoked at most once, 11 | /// even when Dispose is called on multiple threads. 12 | /// 13 | public sealed class CallbackOnDispose : IDisposable 14 | { 15 | private Action? action; 16 | 17 | public CallbackOnDispose(Action action) 18 | { 19 | this.action = action ?? throw new ArgumentNullException(nameof(action)); 20 | } 21 | 22 | public void Dispose() 23 | { 24 | Action? a = Interlocked.Exchange(ref action, null); 25 | a?.Invoke(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /NullabilityInference/SimpleTypeParameter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Microsoft.CodeAnalysis; 5 | 6 | namespace ICSharpCode.NullabilityInference 7 | { 8 | /// 9 | /// Used for ExtensionMethods.FullTypeParameters() 10 | /// 11 | internal struct SimpleTypeParameter 12 | { 13 | /// 14 | /// If symbol != null, wraps a real type parameter. 15 | /// If symbol == null, this is a fake type parameter for an anonymous type. 16 | /// 17 | private ITypeParameterSymbol? symbol; 18 | 19 | public SimpleTypeParameter(ITypeParameterSymbol symbol) 20 | { 21 | this.symbol = symbol; 22 | } 23 | 24 | public VarianceKind Variance => symbol?.Variance ?? VarianceKind.None; 25 | public bool HasNotNullConstraint => symbol?.HasNotNullConstraint ?? false; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /InferNull/AppDomainExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace InferNull 5 | { 6 | internal static class AppDomainExtensions 7 | { 8 | public static void UseVersionAgnosticAssemblyResolution(this AppDomain appDomain) => appDomain.AssemblyResolve += LoadAnyVersion; 9 | 10 | private static Assembly? LoadAnyVersion(object? sender, ResolveEventArgs? args) 11 | { 12 | if (args?.Name == null) return null; 13 | var requestedAssemblyName = new AssemblyName(args.Name); 14 | if (requestedAssemblyName.Version != null && requestedAssemblyName.Name != null) { 15 | try { 16 | return Assembly.Load(new AssemblyName(requestedAssemblyName.Name) { CultureName = requestedAssemblyName.CultureName }); 17 | } catch (Exception) { 18 | return null; //Give other handlers a chance 19 | } 20 | } 21 | return null; 22 | 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 AlphaSierraPapa for the ILSpy team 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 | -------------------------------------------------------------------------------- /NullabilityInference.Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net472 4 | Library 5 | ICSharpCode.NullabilityInference.Tests 6 | ICSharpCode.NullabilityInference.Tests 7 | false 8 | 8.0 9 | $(NoWarn);1998 10 | true 11 | true 12 | true 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: windows-latest 13 | strategy: 14 | matrix: 15 | Configuration: [ Debug, Release] 16 | env: 17 | StagingDirectory: buildartifacts 18 | 19 | steps: 20 | - run: mkdir -p $env:StagingDirectory 21 | - uses: actions/checkout@v2 22 | - name: Setup .NET 23 | uses: actions/setup-dotnet@v1 24 | with: 25 | dotnet-version: 3.1.x 26 | - name: Restore dependencies 27 | run: dotnet restore 28 | - name: Build 29 | run: dotnet build --no-restore -c ${{ matrix.Configuration }} 30 | - name: Test 31 | run: dotnet test --no-build --verbosity normal -c ${{ matrix.Configuration }} 32 | - name: Zip 33 | run: 7z a -tzip $env:StagingDirectory\InferNull_${{ matrix.Configuration }}.zip .\InferNull\bin\${{ matrix.Configuration }}\* 34 | - name: Upload 35 | uses: actions/upload-artifact@v2 36 | with: 37 | name: InferNull (${{ matrix.configuration }}) ${{ github.run_number }} 38 | path: ${{ env.StagingDirectory }}\InferNull_${{ matrix.Configuration }}.zip 39 | if-no-files-found: error 40 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | 4 | pr: 5 | - master 6 | 7 | pool: 8 | vmImage: 'windows-latest' 9 | 10 | strategy: 11 | matrix: 12 | Config_Release: 13 | buildConfiguration: 'Release' 14 | Config_Debug: 15 | buildConfiguration: 'Debug' 16 | 17 | variables: 18 | solution: '**/*.sln' 19 | buildPlatform: 'Any CPU' 20 | 21 | steps: 22 | - task: NuGetToolInstaller@1 23 | 24 | - task: NuGetCommand@2 25 | inputs: 26 | restoreSolution: '$(solution)' 27 | 28 | - task: VSBuild@1 29 | inputs: 30 | solution: '$(solution)' 31 | platform: '$(buildPlatform)' 32 | configuration: '$(buildConfiguration)' 33 | 34 | - task: VSTest@2 35 | inputs: 36 | platform: '$(buildPlatform)' 37 | configuration: '$(buildConfiguration)' 38 | 39 | - task: CopyFiles@2 40 | displayName: Copy InferNull $(buildConfiguration) 41 | inputs: 42 | sourceFolder: '$(Build.SourcesDirectory)/InferNull/bin/$(buildConfiguration)' 43 | contents: '**' 44 | targetFolder: $(Build.ArtifactStagingDirectory)/$(buildConfiguration) 45 | condition: succeeded() 46 | 47 | - task: PublishPipelineArtifact@1 48 | displayName: Publish $(buildConfiguration) 49 | inputs: 50 | targetPath: $(Build.ArtifactStagingDirectory)/$(buildConfiguration) 51 | artifactName: InferNull - $(buildConfiguration) $(Build.BuildId) 52 | -------------------------------------------------------------------------------- /InferNull/App.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 | 26 | 27 | -------------------------------------------------------------------------------- /NullabilityInference/NullType.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Daniel Grunwald 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | // software and associated documentation files (the "Software"), to deal in the Software 5 | // without restriction, including without limitation the rights to use, copy, modify, merge, 6 | // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 7 | // to whom the Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 13 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 14 | // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 15 | // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | // DEALINGS IN THE SOFTWARE. 18 | 19 | namespace ICSharpCode.NullabilityInference 20 | { 21 | public enum NullType 22 | { 23 | /// 24 | /// Let the static null checker automatically infer whether this type is nullable. 25 | /// 26 | Infer = 0, 27 | /// 28 | /// Declares the type as non-nullable. 29 | /// 30 | NonNull = 1, 31 | /// 32 | /// Declares the type as nullable. 33 | /// 34 | Nullable = 2, 35 | /// 36 | /// Declares the type as oblivious. 37 | /// 38 | Oblivious = 3 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /InferNull/InferNull.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net472 6 | InferNull 7 | InferNull 8 | infernull 9 | 0.1.0.0 10 | 0.1.0.0 11 | true 12 | 8.0 13 | enable 14 | false 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /NullabilityInference.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InferNull", "InferNull\InferNull.csproj", "{60A5F778-F837-493E-A549-CC85BEA75A50}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NullabilityInference", "NullabilityInference\NullabilityInference.csproj", "{61EBF036-9D1F-451A-8A02-5B301125DF34}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BC69B7DB-0F9E-4605-87E8-AF373FA5318D}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | EndProjectSection 14 | EndProject 15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "NullabilityInference.Tests\Tests.csproj", "{9EF15FAD-17F1-4D16-B5D0-86653A3567FF}" 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Release|Any CPU = Release|Any CPU 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {60A5F778-F837-493E-A549-CC85BEA75A50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {60A5F778-F837-493E-A549-CC85BEA75A50}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {60A5F778-F837-493E-A549-CC85BEA75A50}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {60A5F778-F837-493E-A549-CC85BEA75A50}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {61EBF036-9D1F-451A-8A02-5B301125DF34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {61EBF036-9D1F-451A-8A02-5B301125DF34}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {61EBF036-9D1F-451A-8A02-5B301125DF34}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {61EBF036-9D1F-451A-8A02-5B301125DF34}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {9EF15FAD-17F1-4D16-B5D0-86653A3567FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {9EF15FAD-17F1-4D16-B5D0-86653A3567FF}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {9EF15FAD-17F1-4D16-B5D0-86653A3567FF}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {9EF15FAD-17F1-4D16-B5D0-86653A3567FF}.Release|Any CPU.Build.0 = Release|Any CPU 35 | EndGlobalSection 36 | GlobalSection(SolutionProperties) = preSolution 37 | HideSolutionNode = FALSE 38 | EndGlobalSection 39 | GlobalSection(ExtensibilityGlobals) = postSolution 40 | SolutionGuid = {4D725FEF-3650-48ED-AF74-42775ACF8934} 41 | EndGlobalSection 42 | EndGlobal 43 | -------------------------------------------------------------------------------- /NullabilityInference/MaximumFlow.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Daniel Grunwald 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | // software and associated documentation files (the "Software"), to deal in the Software 5 | // without restriction, including without limitation the rights to use, copy, modify, merge, 6 | // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 7 | // to whom the Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 13 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 14 | // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 15 | // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | // DEALINGS IN THE SOFTWARE. 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | using System.Diagnostics; 22 | using System.Threading; 23 | 24 | namespace ICSharpCode.NullabilityInference 25 | { 26 | /// 27 | /// Implements the Ford-Fulkerson algorithm for maximum flow. 28 | /// 29 | internal static class MaximumFlow 30 | { 31 | public static int Compute(IEnumerable allNodes, NullabilityNode source, NullabilityNode sink, CancellationToken cancellationToken) 32 | { 33 | Debug.Assert(source != sink); 34 | int maxFlow = 0; 35 | ResetVisited(allNodes); 36 | while (AddFlow(sink, source)) { 37 | cancellationToken.ThrowIfCancellationRequested(); 38 | maxFlow += 1; 39 | ResetVisited(allNodes); 40 | } 41 | return maxFlow; 42 | } 43 | 44 | private static bool AddFlow(NullabilityNode node, NullabilityNode source) 45 | { 46 | if (node.Visited) 47 | return false; 48 | node.Visited = true; 49 | if (node == source) 50 | return true; 51 | var predecessors = node.ResidualGraphPredecessors; 52 | for (int i = 0; i < predecessors.Count; i++) { 53 | var prevNode = predecessors[i]; 54 | if (AddFlow(prevNode, source)) { 55 | // Remove the edge from the residual graph 56 | predecessors.SwapRemoveAt(i); 57 | // and instead add the reverse edge 58 | prevNode.ResidualGraphPredecessors.Add(node); 59 | return true; 60 | } 61 | } 62 | return false; 63 | } 64 | 65 | private static void ResetVisited(IEnumerable allTypes) 66 | { 67 | foreach (NullabilityNode node in allTypes) 68 | node.Visited = false; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /NullabilityInference/NullabilityEdge.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Daniel Grunwald 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | // software and associated documentation files (the "Software"), to deal in the Software 5 | // without restriction, including without limitation the rights to use, copy, modify, merge, 6 | // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 7 | // to whom the Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 13 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 14 | // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 15 | // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | // DEALINGS IN THE SOFTWARE. 18 | 19 | using System; 20 | using System.Diagnostics; 21 | using Microsoft.CodeAnalysis; 22 | 23 | namespace ICSharpCode.NullabilityInference 24 | { 25 | /// 26 | /// An edge in the graph of nullability nodes. 27 | /// 28 | /// An edge represents the constraint "if source is nullable, then target is nullable" 29 | /// (source is assignable to target). 30 | /// 31 | [DebuggerDisplay("{Source} -> {Target}")] 32 | internal sealed class NullabilityEdge 33 | { 34 | public NullabilityNode Source { get; } 35 | public NullabilityNode Target { get; } 36 | 37 | internal readonly EdgeLabel Label; 38 | public bool IsError => Source.NullType == NullType.Nullable && Target.NullType == NullType.NonNull; 39 | 40 | /// 41 | /// Represents the subtype relation "subType <: superType". 42 | /// This means that values of type subType can be assigned to variables of type superType. 43 | /// 44 | public NullabilityEdge(NullabilityNode source, NullabilityNode target, EdgeLabel label) 45 | { 46 | this.Source = source ?? throw new ArgumentNullException(nameof(source)); 47 | this.Target = target ?? throw new ArgumentNullException(nameof(target)); 48 | this.Label = label; 49 | } 50 | } 51 | 52 | internal readonly struct EdgeLabel 53 | { 54 | private readonly string text; 55 | internal readonly Location? location; 56 | 57 | public EdgeLabel(string text) 58 | { 59 | this.text = text; 60 | this.location = null; 61 | } 62 | 63 | public EdgeLabel(string text, Location? location) 64 | { 65 | this.text = text; 66 | this.location = location; 67 | } 68 | 69 | internal EdgeLabel(string text, SyntaxNode? syntaxForLocation) 70 | : this(text, syntaxForLocation?.GetLocation()) 71 | { 72 | } 73 | 74 | internal EdgeLabel(string text, IOperation? operation) 75 | : this(text, operation?.Syntax) 76 | { 77 | } 78 | 79 | public override string ToString() 80 | { 81 | if (location == null) 82 | return text; 83 | else 84 | return text + " at " + location.StartPosToString(); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /InferNull/ExportProviderExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Composition; 8 | using System.Composition.Hosting.Core; 9 | using System.Linq; 10 | using System.Reflection; 11 | using Microsoft.VisualStudio.Composition; 12 | 13 | namespace InferNull.FromRoslynSdk 14 | { 15 | internal static class ExportProviderExtensions 16 | { 17 | public static CompositionContext AsCompositionContext(this ExportProvider exportProvider) 18 | { 19 | return new CompositionContextShim(exportProvider); 20 | } 21 | 22 | private class CompositionContextShim : CompositionContext 23 | { 24 | private readonly ExportProvider _exportProvider; 25 | 26 | public CompositionContextShim(ExportProvider exportProvider) 27 | { 28 | _exportProvider = exportProvider; 29 | } 30 | 31 | public override bool TryGetExport(CompositionContract contract, out object export) 32 | { 33 | var importMany = contract.MetadataConstraints.Contains(new KeyValuePair("IsImportMany", true)); 34 | var (contractType, metadataType) = GetContractType(contract.ContractType, importMany); 35 | 36 | if (metadataType != null) { 37 | var methodInfo = (from method in _exportProvider.GetType().GetTypeInfo().GetMethods() 38 | where method.Name == nameof(ExportProvider.GetExports) 39 | where method.IsGenericMethod && method.GetGenericArguments().Length == 2 40 | where method.GetParameters().Length == 1 && method.GetParameters()[0].ParameterType == typeof(string) 41 | select method).Single(); 42 | var parameterizedMethod = methodInfo.MakeGenericMethod(contractType, metadataType); 43 | export = parameterizedMethod.Invoke(_exportProvider, new[] { contract.ContractName }); 44 | } else { 45 | var methodInfo = (from method in _exportProvider.GetType().GetTypeInfo().GetMethods() 46 | where method.Name == nameof(ExportProvider.GetExports) 47 | where method.IsGenericMethod && method.GetGenericArguments().Length == 1 48 | where method.GetParameters().Length == 1 && method.GetParameters()[0].ParameterType == typeof(string) 49 | select method).Single(); 50 | var parameterizedMethod = methodInfo.MakeGenericMethod(contractType); 51 | export = parameterizedMethod.Invoke(_exportProvider, new[] { contract.ContractName }); 52 | } 53 | 54 | return true; 55 | } 56 | 57 | private (Type exportType, Type? metadataType) GetContractType(Type contractType, bool importMany) 58 | { 59 | if (importMany && contractType.IsConstructedGenericType) { 60 | if (contractType.GetGenericTypeDefinition() == typeof(IList<>) 61 | || contractType.GetGenericTypeDefinition() == typeof(ICollection<>) 62 | || contractType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) { 63 | contractType = contractType.GenericTypeArguments[0]; 64 | } 65 | } 66 | 67 | if (contractType.IsConstructedGenericType) { 68 | if (contractType.GetGenericTypeDefinition() == typeof(Lazy<>)) { 69 | return (contractType.GenericTypeArguments[0], null); 70 | } else if (contractType.GetGenericTypeDefinition() == typeof(Lazy<,>)) { 71 | return (contractType.GenericTypeArguments[0], contractType.GenericTypeArguments[1]); 72 | } else { 73 | throw new NotSupportedException(); 74 | } 75 | } 76 | 77 | throw new NotSupportedException(); 78 | } 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /InferNull/ProcessRunner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Runtime.InteropServices; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.Win32.SafeHandles; 9 | 10 | namespace InferNull 11 | { 12 | internal static class ProcessRunner 13 | { 14 | /// Process is started from this information 15 | /// Defaults to Console.Out 16 | /// Defaults to Console.Error 17 | internal static async Task GetExitCodeAsync(this ProcessStartInfo psi, TextWriter? stdOut = null, TextWriter? stdErr = null) 18 | { 19 | stdOut ??= Console.Out; 20 | stdErr ??= Console.Error; 21 | psi.UseShellExecute = false; 22 | psi.RedirectStandardError = true; 23 | psi.RedirectStandardOutput = true; 24 | using var process = new Process() { StartInfo = psi }; 25 | var stdOutComplete = new TaskCompletionSource(); 26 | var stdErrComplete = new TaskCompletionSource(); 27 | process.OutputDataReceived += (sender, e) => { 28 | if (e.Data != null) 29 | stdOut.WriteLine(e.Data); 30 | else 31 | stdOutComplete.SetResult(null); 32 | }; 33 | process.ErrorDataReceived += (sender, e) => { 34 | if (e.Data != null) 35 | stdErr.WriteLine(e.Data); 36 | else 37 | stdErrComplete.SetResult(null); 38 | }; 39 | try { 40 | process.Start(); 41 | } catch (Win32Exception win32Exception) { 42 | await stdErr.WriteLineAsync(win32Exception.Message).ConfigureAwait(false); 43 | return win32Exception.ErrorCode; 44 | } 45 | process.BeginOutputReadLine(); 46 | process.BeginErrorReadLine(); 47 | await Task.WhenAll(process.WaitForExitAsync(), stdOutComplete.Task, stdErrComplete.Task).ConfigureAwait(false); 48 | 49 | return process.ExitCode; 50 | } 51 | 52 | /// 53 | /// Asynchronously waits for the process to exit. 54 | /// 55 | public static Task WaitForExitAsync(this Process process) 56 | { 57 | if (process.HasExited) 58 | return Task.CompletedTask; 59 | var safeProcessHandle = process.SafeHandle; 60 | if (safeProcessHandle.IsClosed) 61 | throw new ObjectDisposedException("Process"); 62 | var tcs = new TaskCompletionSource(); 63 | var waitHandle = new ProcessWaitHandle(safeProcessHandle); 64 | RegisteredWaitHandle? registeredWaitHandle = null; 65 | lock (tcs) { 66 | registeredWaitHandle = ThreadPool.RegisterWaitForSingleObject(waitHandle, WaitForExitAsyncCallback, null, -1, true); 67 | } 68 | return tcs.Task; 69 | 70 | void WaitForExitAsyncCallback(object context, bool wasSignaled) 71 | { 72 | // The lock is used to ensure `registeredWaitHandle` is initialized here 73 | // even if the process terminates while `RegisterWaitForSingleObject` is returning. 74 | lock (tcs) { 75 | registeredWaitHandle!.Unregister(null); 76 | } 77 | waitHandle.Close(); 78 | tcs.SetResult(null); 79 | } 80 | } 81 | 82 | private sealed class ProcessWaitHandle : WaitHandle 83 | { 84 | public ProcessWaitHandle(SafeProcessHandle processHandle) 85 | { 86 | var currentProcess = new HandleRef(this, NativeMethods.GetCurrentProcess()); 87 | SafeWaitHandle safeWaitHandle; 88 | if (!NativeMethods.DuplicateHandle(currentProcess, processHandle, currentProcess, out safeWaitHandle, 0, false, NativeMethods.DUPLICATE_SAME_ACCESS)) { 89 | throw new Win32Exception(); 90 | } 91 | base.SafeWaitHandle = safeWaitHandle; 92 | } 93 | } 94 | private static class NativeMethods 95 | { 96 | [DllImport("kernel32", SetLastError = true)] 97 | [return: MarshalAs(UnmanagedType.Bool)] 98 | internal static extern bool CloseHandle(IntPtr hObject); 99 | 100 | [DllImport("kernel32.dll")] 101 | internal static extern IntPtr GetCurrentProcess(); 102 | 103 | [DllImport("kernel32.dll", BestFitMapping = false, CharSet = CharSet.Ansi)] 104 | internal static extern bool DuplicateHandle(HandleRef hSourceProcessHandle, SafeHandle hSourceHandle, HandleRef hTargetProcess, out SafeWaitHandle targetHandle, int dwDesiredAccess, bool bInheritHandle, int dwOptions); 105 | 106 | internal const int DUPLICATE_SAME_ACCESS = 2; 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /NullabilityInference/AccessPath.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Daniel Grunwald 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | // software and associated documentation files (the "Software"), to deal in the Software 5 | // without restriction, including without limitation the rights to use, copy, modify, merge, 6 | // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 7 | // to whom the Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 13 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 14 | // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 15 | // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | // DEALINGS IN THE SOFTWARE. 18 | 19 | using System; 20 | using System.Collections.Immutable; 21 | using System.Linq; 22 | using System.Text; 23 | using Microsoft.CodeAnalysis; 24 | using Microsoft.CodeAnalysis.Operations; 25 | 26 | namespace ICSharpCode.NullabilityInference 27 | { 28 | internal enum AccessPathRoot 29 | { 30 | /// 31 | /// The first element in the symbol array is a local variable or parameter. 32 | /// All following symbols are fields/properties. 33 | /// 34 | Local, 35 | /// 36 | /// The access path is rooted in 'this'. 37 | /// All symbols in the array are fields/properties. 38 | /// 39 | This, 40 | } 41 | 42 | /// 43 | /// Represents a path for which we can track the flow-state. 44 | /// 45 | internal readonly struct AccessPath : IEquatable 46 | { 47 | public readonly AccessPathRoot Root; 48 | public readonly ImmutableArray Symbols; 49 | 50 | 51 | public AccessPath(AccessPathRoot root, ImmutableArray symbols) 52 | { 53 | this.Root = root; 54 | this.Symbols = symbols; 55 | } 56 | 57 | public bool IsParameter => Root == AccessPathRoot.Local && Symbols.Length == 1 && Symbols[0].Kind == SymbolKind.Parameter; 58 | 59 | public static AccessPath? FromOperation(IOperation? operation) 60 | { 61 | var list = ImmutableArray.CreateBuilder(); 62 | list.Reverse(); 63 | while (operation is IMemberReferenceOperation mro && (mro.Kind == OperationKind.PropertyReference || mro.Kind == OperationKind.FieldReference)) { 64 | if (mro.Member is IPropertySymbol prop && !prop.Parameters.IsEmpty) { 65 | return null; // indexers are not supported 66 | } 67 | list.Add(mro.Member); 68 | operation = mro.Instance; 69 | } 70 | AccessPathRoot root; 71 | switch (operation) { 72 | case ILocalReferenceOperation localOp: 73 | list.Add(localOp.Local); 74 | root = AccessPathRoot.Local; 75 | break; 76 | case IParameterReferenceOperation paramOp: 77 | list.Add(paramOp.Parameter); 78 | root = AccessPathRoot.Local; 79 | break; 80 | case IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance }: 81 | root = AccessPathRoot.This; 82 | break; 83 | default: 84 | return null; 85 | } 86 | list.Reverse(); 87 | return new AccessPath(root, list.ToImmutable()); 88 | } 89 | 90 | public static AccessPath? FromRefArgument(IOperation? operation) 91 | { 92 | // also handle `out var decl` 93 | if (operation is IDeclarationExpressionOperation { Expression: ILocalReferenceOperation { Local: var local } }) { 94 | return new AccessPath(AccessPathRoot.Local, ImmutableArray.Create(local)); 95 | } else { 96 | return FromOperation(operation); 97 | } 98 | } 99 | 100 | public override bool Equals(object obj) => obj is AccessPath p && Equals(p); 101 | 102 | public override int GetHashCode() 103 | { 104 | int hash = (int)Root; 105 | foreach (var sym in Symbols) { 106 | hash ^= SymbolEqualityComparer.Default.GetHashCode(sym); 107 | } 108 | return hash; 109 | } 110 | 111 | public bool Equals(AccessPath other) 112 | { 113 | if (Root != other.Root) 114 | return false; 115 | if (Symbols.Length != other.Symbols.Length) 116 | return false; 117 | return Symbols.Zip(other.Symbols, SymbolEqualityComparer.Default.Equals).All(b => b); 118 | } 119 | 120 | public override string ToString() 121 | { 122 | StringBuilder b = new StringBuilder(); 123 | if (Root == AccessPathRoot.This) 124 | b.Append("this"); 125 | foreach (var sym in Symbols) { 126 | if (b.Length > 0) 127 | b.Append('.'); 128 | b.Append(sym.Name); 129 | } 130 | return b.ToString(); 131 | } 132 | } 133 | } -------------------------------------------------------------------------------- /NullabilityInference/AllNullableSyntaxRewriter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Daniel Grunwald 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | // software and associated documentation files (the "Software"), to deal in the Software 5 | // without restriction, including without limitation the rights to use, copy, modify, merge, 6 | // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 7 | // to whom the Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 13 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 14 | // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 15 | // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | // DEALINGS IN THE SOFTWARE. 18 | 19 | using System.Linq; 20 | using System.Threading; 21 | using Microsoft.CodeAnalysis; 22 | using Microsoft.CodeAnalysis.CSharp; 23 | using Microsoft.CodeAnalysis.CSharp.Syntax; 24 | 25 | namespace ICSharpCode.NullabilityInference 26 | { 27 | /// 28 | /// A C# syntax rewriter that marks all reference types as nullable. 29 | /// 30 | /// This is used initially to construct the compilation that gets analyzed by the inference logic. 31 | /// Using nullable types everywhere makes it so that we can interpret NullableFlowState.NotNull 32 | /// as expression being protected by an `if (x != null)`. 33 | /// 34 | public sealed class AllNullableSyntaxRewriter : CSharpSyntaxRewriter 35 | { 36 | public static CSharpCompilation MakeAllReferenceTypesNullable(CSharpCompilation compilation, CancellationToken cancellationToken) 37 | { 38 | var newSyntaxTrees = compilation.SyntaxTrees.AsParallel().WithCancellation(cancellationToken) 39 | .Select(syntaxTree => { 40 | var semanticModel = compilation.GetSemanticModel(syntaxTree); 41 | var oldRoot = syntaxTree.GetRoot(cancellationToken); 42 | var newRoot = new AllNullableSyntaxRewriter(semanticModel, cancellationToken).Visit(oldRoot); 43 | return syntaxTree.WithRootAndOptions(newRoot, syntaxTree.Options); 44 | }); 45 | return compilation.RemoveAllSyntaxTrees() 46 | .WithOptions(compilation.Options.WithNullableContextOptions(NullableContextOptions.Enable)) 47 | .AddSyntaxTrees(newSyntaxTrees); 48 | } 49 | 50 | private readonly SemanticModel semanticModel; 51 | private readonly CancellationToken cancellationToken; 52 | 53 | private AllNullableSyntaxRewriter(SemanticModel semanticModel, CancellationToken cancellationToken) 54 | : base(visitIntoStructuredTrivia: true) 55 | { 56 | this.semanticModel = semanticModel; 57 | this.cancellationToken = cancellationToken; 58 | } 59 | 60 | private bool isActive = true; 61 | 62 | public override SyntaxNode? VisitNullableDirectiveTrivia(NullableDirectiveTriviaSyntax node) 63 | { 64 | isActive = node.SettingToken.IsKind(SyntaxKind.RestoreKeyword); 65 | return base.VisitNullableDirectiveTrivia(node); 66 | } 67 | 68 | public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node) 69 | { 70 | return HandleTypeName(node, base.VisitIdentifierName(node)); 71 | } 72 | 73 | public override SyntaxNode? VisitGenericName(GenericNameSyntax node) 74 | { 75 | return HandleTypeName(node, base.VisitGenericName(node)); 76 | } 77 | 78 | public override SyntaxNode? VisitPredefinedType(PredefinedTypeSyntax node) 79 | { 80 | return HandleTypeName(node, base.VisitPredefinedType(node)); 81 | } 82 | 83 | public override SyntaxNode? VisitQualifiedName(QualifiedNameSyntax node) 84 | { 85 | return HandleTypeName(node, base.VisitQualifiedName(node)); 86 | } 87 | 88 | private SyntaxNode? HandleTypeName(TypeSyntax node, SyntaxNode? newNode) 89 | { 90 | if (!CanBeMadeNullableSyntax(node)) { 91 | return newNode; 92 | } 93 | var symbolInfo = semanticModel.GetSymbolInfo(node, cancellationToken); 94 | if (symbolInfo.Symbol is ITypeSymbol ty && ty.CanBeMadeNullable() && newNode is TypeSyntax newTypeSyntax) { 95 | return SyntaxFactory.NullableType( 96 | elementType: newTypeSyntax.WithoutTrailingTrivia(), 97 | questionToken: SyntaxFactory.Token(SyntaxKind.QuestionToken) 98 | ).WithTrailingTrivia(newTypeSyntax.GetTrailingTrivia()); 99 | } 100 | return newNode; 101 | } 102 | 103 | public override SyntaxNode? VisitArrayType(ArrayTypeSyntax node) 104 | { 105 | var newNode = base.VisitArrayType(node); 106 | if (CanBeMadeNullableSyntax(node) && newNode is TypeSyntax newTypeSyntax) { 107 | return SyntaxFactory.NullableType( 108 | elementType: newTypeSyntax.WithoutTrailingTrivia(), 109 | questionToken: SyntaxFactory.Token(SyntaxKind.QuestionToken) 110 | ).WithTrailingTrivia(newTypeSyntax.GetTrailingTrivia()); 111 | } else { 112 | return newNode; 113 | } 114 | } 115 | 116 | private bool CanBeMadeNullableSyntax(TypeSyntax node) 117 | { 118 | return isActive && GraphBuildingSyntaxVisitor.CanBeMadeNullableSyntax(node) && !(node.Parent is NullableTypeSyntax); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; Top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | 8 | 9 | [*.csproj] 10 | indent_style = space 11 | indent_size = 2 12 | [*.config] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.vsixmanifest] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.vsct] 21 | indent_style = space 22 | indent_size = 2 23 | 24 | [*.cs] 25 | # New line preferences 26 | csharp_new_line_before_open_brace = methods, types 27 | csharp_new_line_before_else = false 28 | csharp_new_line_before_catch = false 29 | csharp_new_line_before_finally = false 30 | csharp_new_line_before_members_in_object_initializers = false 31 | csharp_new_line_before_members_in_anonymous_types = false 32 | csharp_new_line_within_query_expression_clauses = false 33 | 34 | # Indentation preferences 35 | csharp_indent_block_contents = true 36 | csharp_indent_braces = false 37 | csharp_indent_case_contents = true 38 | csharp_indent_switch_labels = true 39 | csharp_indent_labels = one_less 40 | 41 | # Avoid 'this.' in generated code unless absolutely necessary, but allow developers to use it 42 | dotnet_style_qualification_for_field = false:silent 43 | dotnet_style_qualification_for_property = false:silent 44 | dotnet_style_qualification_for_method = false:silent 45 | dotnet_style_qualification_for_event = false:silent 46 | 47 | # Do not use 'var' when generating code, but allow developers to use it 48 | csharp_style_var_for_built_in_types = false:silent 49 | csharp_style_var_when_type_is_apparent = true:silent 50 | csharp_style_var_elsewhere = true:silent 51 | 52 | # Use language keywords instead of BCL types when generating code, but allow developers to use either 53 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 54 | dotnet_style_predefined_type_for_member_access = true:silent 55 | 56 | # Using directives 57 | dotnet_sort_system_directives_first = true 58 | 59 | # Wrapping 60 | csharp_preserve_single_line_blocks = true 61 | csharp_preserve_single_line_statements = true 62 | 63 | # Code style 64 | csharp_prefer_braces = true:silent 65 | 66 | # Expression-level preferences 67 | dotnet_style_object_initializer = true:suggestion 68 | dotnet_style_collection_initializer = true:suggestion 69 | dotnet_style_explicit_tuple_names = true:suggestion 70 | dotnet_style_coalesce_expression = true:suggestion 71 | dotnet_style_null_propagation = true:suggestion 72 | 73 | # Expression-bodied members 74 | csharp_style_expression_bodied_methods = false:silent 75 | csharp_style_expression_bodied_constructors = false:silent 76 | csharp_style_expression_bodied_operators = false:silent 77 | csharp_style_expression_bodied_properties = true:silent 78 | csharp_style_expression_bodied_indexers = true:silent 79 | csharp_style_expression_bodied_accessors = true:silent 80 | 81 | # Pattern matching 82 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 83 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 84 | csharp_style_inlined_variable_declaration = true:suggestion 85 | 86 | # Null checking preferences 87 | csharp_style_throw_expression = true:suggestion 88 | csharp_style_conditional_delegate_call = true:suggestion 89 | 90 | # Space preferences 91 | csharp_space_after_cast = false 92 | csharp_space_after_colon_in_inheritance_clause = true 93 | csharp_space_after_comma = true 94 | csharp_space_after_dot = false 95 | csharp_space_after_keywords_in_control_flow_statements = true 96 | csharp_space_after_semicolon_in_for_statement = true 97 | csharp_space_around_binary_operators = before_and_after 98 | csharp_space_around_declaration_statements = do_not_ignore 99 | csharp_space_before_colon_in_inheritance_clause = true 100 | csharp_space_before_comma = false 101 | csharp_space_before_dot = false 102 | csharp_space_before_open_square_brackets = false 103 | csharp_space_before_semicolon_in_for_statement = false 104 | csharp_space_between_empty_square_brackets = false 105 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 106 | csharp_space_between_method_call_name_and_opening_parenthesis = false 107 | csharp_space_between_method_call_parameter_list_parentheses = false 108 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 109 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 110 | csharp_space_between_method_declaration_parameter_list_parentheses = false 111 | csharp_space_between_parentheses = false 112 | csharp_space_between_square_brackets = false 113 | 114 | # RCS1141: Add 'param' element to documentation comment. 115 | dotnet_diagnostic.RCS1141.severity = none 116 | 117 | # RCS1139: Add summary element to documentation comment. 118 | dotnet_diagnostic.RCS1139.severity = none 119 | 120 | # RCS1090: Call 'ConfigureAwait(false)'. 121 | dotnet_diagnostic.RCS1090.severity = none 122 | 123 | # RCS1080: Use 'Count/Length' property instead of 'Any' method. 124 | dotnet_diagnostic.RCS1080.severity = none 125 | 126 | # RCS1124: Inline local variable. 127 | dotnet_diagnostic.RCS1124.severity = none 128 | 129 | # RCS1077: Optimize LINQ method call. 130 | dotnet_diagnostic.RCS1077.severity = none 131 | 132 | # IDE0040: Add accessibility modifiers 133 | dotnet_style_require_accessibility_modifiers = always:warning 134 | 135 | # IDE0048: Add parentheses for clarity 136 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:none 137 | 138 | # IDE0048: Add parentheses for clarity 139 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:none 140 | 141 | # IDE0048: Add parentheses for clarity 142 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none 143 | 144 | # IDE0048: Add parentheses for clarity 145 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:none 146 | 147 | # RCS1123: Add parentheses according to operator precedence. 148 | dotnet_diagnostic.RCS1123.severity = none 149 | 150 | # Default severity for analyzer diagnostics with category 'Performance' - turn this up to warn when looking for performance issues 151 | dotnet_analyzer_diagnostic.category-Performance.severity = suggestion 152 | -------------------------------------------------------------------------------- /NullabilityInference/NullabilityNode.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Daniel Grunwald 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | // software and associated documentation files (the "Software"), to deal in the Software 5 | // without restriction, including without limitation the rights to use, copy, modify, merge, 6 | // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 7 | // to whom the Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 13 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 14 | // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 15 | // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | // DEALINGS IN THE SOFTWARE. 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | using System.Diagnostics; 22 | using System.Linq; 23 | using System.Threading; 24 | using Microsoft.CodeAnalysis; 25 | using Microsoft.CodeAnalysis.CSharp.Syntax; 26 | 27 | namespace ICSharpCode.NullabilityInference 28 | { 29 | /// 30 | /// A node representing either a fixed 31 | /// 32 | public abstract class NullabilityNode 33 | { 34 | /// 35 | /// List of incoming edges. 36 | /// 37 | internal List IncomingEdges = new List(); 38 | /// 39 | /// List of outgoing edges. 40 | /// 41 | internal List OutgoingEdges = new List(); 42 | 43 | public IEnumerable Predecessors => IncomingEdges.Select(e => e.Source); 44 | public IEnumerable Successors => OutgoingEdges.Select(e => e.Target); 45 | 46 | /// 47 | /// Predecessors in the residual graph. This starts out the same as Predecessors, but gets mutated by the maximum flow algorithm: 48 | /// Any edge involved in the maximum flow will be reversed. Thus there will be no path from {null} to {nonnull} left over in the residual graph. 49 | /// 50 | /// Because we store only predecessors, this means a depth first search starting at {nonnull} using this list will visit all nodes 51 | /// on the {nonnull} half of the graph after performing the minimum cut. 52 | /// 53 | internal List ResidualGraphPredecessors = new List(); 54 | 55 | /// 56 | /// Name for the DOT graph. 57 | /// 58 | public abstract string Name { get; } 59 | 60 | /// 61 | /// Location in the source code for this node. 62 | /// 63 | public abstract Location? Location { get; } 64 | 65 | internal bool Visited; 66 | 67 | public NullType NullType = NullType.Infer; 68 | 69 | internal virtual void SetName(string name) 70 | { 71 | } 72 | 73 | private NullabilityNode? replacement; 74 | 75 | public NullabilityNode ReplacedWith { 76 | get { 77 | NullabilityNode result = this; 78 | while (result.replacement != null) 79 | result = result.replacement; 80 | return result; 81 | } 82 | } 83 | 84 | /// 85 | /// Replace with node with another node. 86 | /// All future attempts to create an edge involving this node, will instead create an edge with the other node. 87 | /// This method can only be used in the NodeBuilding phase, as otherwise there might already be edges registered; 88 | /// which will not be re-pointed. 89 | /// 90 | internal void ReplaceWith(NullabilityNode other) 91 | { 92 | if (this == other) { 93 | return; 94 | } 95 | if (this.replacement != null) { 96 | this.replacement.ReplaceWith(other); 97 | return; 98 | } 99 | while (other.replacement != null) { 100 | other = other.replacement; 101 | } 102 | Debug.Assert(this.NullType == other.NullType || this.NullType == NullType.Infer || other.NullType == NullType.Infer); 103 | // Replacements must be performed before the edges are registered. 104 | Debug.Assert(this.IncomingEdges.Count == 0); 105 | Debug.Assert(this.OutgoingEdges.Count == 0); 106 | this.replacement = other; 107 | } 108 | } 109 | 110 | [DebuggerDisplay("{Name}")] 111 | internal sealed class SpecialNullabilityNode : NullabilityNode 112 | { 113 | public SpecialNullabilityNode(NullType nullType) 114 | { 115 | this.NullType = nullType; 116 | } 117 | 118 | public override Location? Location => null; 119 | 120 | public override string Name => NullType switch { 121 | NullType.Nullable => "", 122 | NullType.NonNull => "", 123 | NullType.Oblivious => "", 124 | _ => throw new NotSupportedException(), 125 | }; 126 | } 127 | 128 | [DebuggerDisplay("{Name}")] 129 | internal sealed class SyntacticNullabilityNode : NullabilityNode 130 | { 131 | private readonly TypeSyntax typeSyntax; 132 | private readonly int id; 133 | 134 | public SyntacticNullabilityNode(TypeSyntax typeSyntax, int id) 135 | { 136 | this.typeSyntax = typeSyntax; 137 | this.id = id; 138 | } 139 | 140 | public override Location? Location => typeSyntax.GetLocation(); 141 | 142 | private string? symbolName; 143 | 144 | public override string Name => $"{symbolName}#{id}"; 145 | 146 | internal override void SetName(string name) 147 | { 148 | symbolName ??= name; 149 | } 150 | } 151 | 152 | /// 153 | /// A node that does not correspond to any syntactic construct, but is helpful when constructing the graph. 154 | /// 155 | [DebuggerDisplay("{Name}")] 156 | internal sealed class HelperNullabilityNode : NullabilityNode 157 | { 158 | private static long nextId; 159 | private readonly long id = Interlocked.Increment(ref nextId); 160 | 161 | public override Location? Location => null; 162 | 163 | private string symbolName = "helper"; 164 | 165 | public override string Name => $"<{symbolName}#{id}>"; 166 | 167 | internal override void SetName(string name) 168 | { 169 | if (symbolName == "helper") 170 | symbolName = name; 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /NullabilityInference/FlowState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Diagnostics; 4 | using System.Diagnostics.CodeAnalysis; 5 | using Microsoft.CodeAnalysis; 6 | 7 | namespace ICSharpCode.NullabilityInference 8 | { 9 | /// 10 | /// Represents the flow-state at a given point in the program. 11 | /// 12 | internal sealed class FlowState 13 | { 14 | internal readonly struct Snapshot 15 | { 16 | internal readonly bool Unreachable; 17 | internal readonly PathNode ThisPath; 18 | internal readonly ImmutableDictionary Locals; 19 | 20 | public Snapshot(PathNode thisPath, ImmutableDictionary locals, bool unreachable) 21 | { 22 | this.ThisPath = thisPath; 23 | this.Locals = locals; 24 | this.Unreachable = unreachable; 25 | } 26 | 27 | internal Snapshot? WithUnreachable() 28 | { 29 | return new Snapshot(ThisPath, Locals, unreachable: true); 30 | } 31 | } 32 | 33 | internal readonly struct PathNode 34 | { 35 | public readonly NullabilityNode Nullability; 36 | public readonly ImmutableDictionary Members; 37 | 38 | public PathNode(NullabilityNode nullability, ImmutableDictionary members) 39 | { 40 | this.Nullability = nullability; 41 | this.Members = members; 42 | } 43 | } 44 | 45 | private static readonly ImmutableDictionary emptyMembers = ImmutableDictionary.Create(SymbolEqualityComparer.Default); 46 | 47 | private readonly TypeSystem typeSystem; 48 | 49 | private bool unreachable; 50 | private PathNode thisPath; 51 | private ImmutableDictionary.Builder locals = ImmutableDictionary.CreateBuilder(SymbolEqualityComparer.Default); 52 | 53 | public FlowState(TypeSystem typeSystem) 54 | { 55 | this.typeSystem = typeSystem; 56 | Clear(); 57 | } 58 | 59 | public void Clear() 60 | { 61 | unreachable = false; 62 | thisPath = new PathNode(typeSystem.NonNullNode, emptyMembers); 63 | locals.Clear(); 64 | } 65 | 66 | public void MakeUnreachable() 67 | { 68 | unreachable = true; 69 | } 70 | 71 | public Snapshot SaveSnapshot() 72 | { 73 | return new Snapshot(thisPath, locals.ToImmutable(), unreachable); 74 | } 75 | 76 | public void RestoreSnapshot(Snapshot snapshot) 77 | { 78 | this.unreachable = snapshot.Unreachable; 79 | this.thisPath = snapshot.ThisPath; 80 | this.locals = snapshot.Locals.ToBuilder(); 81 | } 82 | 83 | public void JoinWith(Snapshot snapshot, TypeSystem.Builder tsBuilder, EdgeLabel edgeLabel) 84 | { 85 | if (unreachable) { 86 | RestoreSnapshot(snapshot); 87 | return; 88 | } else if (snapshot.Unreachable) { 89 | return; // no-op 90 | } 91 | Debug.Assert(!unreachable && !snapshot.Unreachable); 92 | thisPath = Join(thisPath, snapshot.ThisPath); 93 | locals = JoinDict(locals.ToImmutable(), snapshot.Locals); 94 | 95 | ImmutableDictionary.Builder JoinDict(ImmutableDictionary a, ImmutableDictionary b) 96 | { 97 | ImmutableDictionary.Builder newDict = emptyMembers.ToBuilder(); 98 | foreach (var (local, pathFromSnapshot) in b) { 99 | if (a.TryGetValue(local, out var pathFromThis)) { 100 | newDict[local] = Join(pathFromThis, pathFromSnapshot); 101 | } 102 | } 103 | return newDict; 104 | } 105 | 106 | PathNode Join(PathNode a, PathNode b) 107 | { 108 | var newMembers = JoinDict(a.Members, b.Members); 109 | var newNullability = tsBuilder.Join(a.Nullability, b.Nullability, edgeLabel); 110 | return new PathNode(newNullability, newMembers.ToImmutable()); 111 | } 112 | } 113 | 114 | public void SetNode(AccessPath path, NullabilityNode newNode, bool clearMembers) 115 | { 116 | switch (path.Root) { 117 | case AccessPathRoot.This: 118 | thisPath = Visit(thisPath, 0); 119 | break; 120 | case AccessPathRoot.Local: 121 | if (!locals.TryGetValue(path.Symbols[0], out var localPathNode)) { 122 | localPathNode = new PathNode(typeSystem.NonNullNode, emptyMembers); 123 | } 124 | localPathNode = Visit(localPathNode, 1); 125 | locals[path.Symbols[0]] = localPathNode; 126 | break; 127 | default: 128 | throw new NotSupportedException(); 129 | } 130 | 131 | PathNode Visit(PathNode input, int index) 132 | { 133 | if (index == path.Symbols.Length) { 134 | if (clearMembers) { 135 | return new PathNode(newNode, emptyMembers); 136 | } else { 137 | return new PathNode(newNode, input.Members); 138 | } 139 | } 140 | var member = path.Symbols[index]; 141 | if (!input.Members.TryGetValue(member, out var childNode)) { 142 | childNode = new PathNode(typeSystem.NonNullNode, emptyMembers); 143 | } 144 | childNode = Visit(childNode, index + 1); 145 | return new PathNode(typeSystem.NonNullNode, input.Members.SetItem(member, childNode)); 146 | } 147 | } 148 | 149 | public bool TryGetNode(AccessPath path, [NotNullWhen(true)] out NullabilityNode? flowNode) 150 | { 151 | flowNode = null; 152 | int index; 153 | PathNode node; 154 | switch (path.Root) { 155 | case AccessPathRoot.This: 156 | index = 0; 157 | node = thisPath; 158 | break; 159 | case AccessPathRoot.Local: 160 | if (!locals.TryGetValue(path.Symbols[0], out node)) { 161 | return false; 162 | } 163 | index = 1; 164 | break; 165 | default: 166 | throw new NotSupportedException(); 167 | } 168 | for (; index < path.Symbols.Length; index++) { 169 | if (!node.Members.TryGetValue(path.Symbols[index], out node)) { 170 | return false; 171 | } 172 | } 173 | flowNode = node.Nullability; 174 | return true; 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /NullabilityInference/TypeWithNode.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Daniel Grunwald 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | // software and associated documentation files (the "Software"), to deal in the Software 5 | // without restriction, including without limitation the rights to use, copy, modify, merge, 6 | // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 7 | // to whom the Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 13 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 14 | // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 15 | // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | // DEALINGS IN THE SOFTWARE. 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | using System.Collections.Immutable; 22 | using System.Diagnostics; 23 | using System.Linq; 24 | using Microsoft.CodeAnalysis; 25 | using Microsoft.CodeAnalysis.CSharp.Syntax; 26 | 27 | namespace ICSharpCode.NullabilityInference 28 | { 29 | /// 30 | /// Pairs a C# type with a nullability node that can be used to infer the nullability. 31 | /// 32 | [DebuggerDisplay("{Node} : {Type}")] 33 | public readonly struct TypeWithNode 34 | { 35 | public readonly ITypeSymbol? Type; 36 | public readonly NullabilityNode Node; 37 | // This should be ImmutableArray, but that runs into 38 | // https://github.com/dotnet/runtime/issues/12024 39 | public readonly IReadOnlyList TypeArguments; 40 | #if DEBUG 41 | internal readonly string? FlowLabel; 42 | #endif 43 | 44 | private static readonly TypeWithNode[] emptyTypeArguments = { }; 45 | 46 | public TypeWithNode(ITypeSymbol? type, NullabilityNode node, IReadOnlyList? typeArguments = null) 47 | { 48 | this.Type = type; 49 | this.Node = node; 50 | this.TypeArguments = typeArguments ?? emptyTypeArguments; 51 | Debug.Assert(this.TypeArguments.Count == type.FullArity()); 52 | #if DEBUG 53 | this.FlowLabel = null; 54 | #endif 55 | } 56 | 57 | private TypeWithNode(ITypeSymbol? type, NullabilityNode node, IReadOnlyList? typeArguments, string? flowLabel) 58 | : this(type, node, typeArguments) 59 | { 60 | #if DEBUG 61 | this.FlowLabel = flowLabel; 62 | #endif 63 | } 64 | 65 | /// 66 | /// Replaces the top-level nullability. 67 | /// 68 | internal TypeWithNode WithNode(NullabilityNode newNode) 69 | { 70 | return new TypeWithNode(Type, newNode, TypeArguments); 71 | } 72 | 73 | internal TypeWithNode WithFlowState(NullabilityNode flowNode, string? flowLabel) 74 | { 75 | return new TypeWithNode(Type, flowNode, TypeArguments, flowLabel); 76 | } 77 | 78 | /// 79 | /// Creates a new TypeWithNode 80 | /// 81 | /// newType must be the result of applying the substitution to this.Type. 82 | /// 83 | internal TypeWithNode WithSubstitution(ITypeSymbol newType, TypeSubstitution subst, TypeSystem.Builder? tsBuilder) 84 | { 85 | if (this.Type is ITypeParameterSymbol tp) { 86 | var substituted = subst[tp.TypeParameterKind, tp.FullOrdinal()]; 87 | Debug.Assert(SymbolEqualityComparer.Default.Equals(substituted.Type, newType)); 88 | if (tsBuilder != null) { 89 | var newNode = tsBuilder.Join(substituted.Node, this.Node, new EdgeLabel()); 90 | return substituted.WithNode(newNode); 91 | } else { 92 | return substituted; 93 | } 94 | } else if (this.Type is INamedTypeSymbol thisNamedTypeSymbol && newType is INamedTypeSymbol newNamedTypeSymbol) { 95 | Debug.Assert(SymbolEqualityComparer.Default.Equals(thisNamedTypeSymbol.OriginalDefinition, newNamedTypeSymbol.OriginalDefinition)); 96 | Debug.Assert(newNamedTypeSymbol.FullArity() == this.TypeArguments.Count); 97 | TypeWithNode[] newTypeArgs = new TypeWithNode[this.TypeArguments.Count]; 98 | var newNamedTypeSymbolTypeArguments = newNamedTypeSymbol.FullTypeArguments().ToList(); 99 | for (int i = 0; i < newTypeArgs.Length; i++) { 100 | newTypeArgs[i] = this.TypeArguments[i].WithSubstitution(newNamedTypeSymbolTypeArguments[i], subst, tsBuilder); 101 | } 102 | return new TypeWithNode(newType, this.Node, newTypeArgs); 103 | } else if (this.Type is IArrayTypeSymbol thisArrayTypeSymbol && newType is IArrayTypeSymbol newArrayTypeSymbol) { 104 | Debug.Assert(thisArrayTypeSymbol.Rank == newArrayTypeSymbol.Rank); 105 | var elementType = this.TypeArguments.Single().WithSubstitution(newArrayTypeSymbol.ElementType, subst, tsBuilder); 106 | return new TypeWithNode(newType, this.Node, new[] { elementType }); 107 | } else if (this.Type is IPointerTypeSymbol && newType is IPointerTypeSymbol newPointerTypeSymbol) { 108 | var pointedAtType = this.TypeArguments.Single().WithSubstitution(newPointerTypeSymbol.PointedAtType, subst, tsBuilder); 109 | return new TypeWithNode(newType, this.Node, new[] { pointedAtType }); 110 | } 111 | return new TypeWithNode(newType, this.Node); 112 | } 113 | 114 | [Conditional("DEBUG")] 115 | internal void SetName(string name) 116 | { 117 | Node.SetName(name); 118 | for (int i = 0; i < TypeArguments.Count; i++) { 119 | TypeArguments[i].SetName($"{name}!{i}"); 120 | } 121 | } 122 | 123 | internal IEnumerable<(NullabilityNode, VarianceKind)> NodesWithVariance() 124 | { 125 | yield return (Node, VarianceKind.Out); 126 | if (this.Type is INamedTypeSymbol namedType) { 127 | Debug.Assert(this.TypeArguments.Count == namedType.FullArity()); 128 | foreach (var (tp, ta) in namedType.FullTypeParameters().Zip(this.TypeArguments)) { 129 | foreach (var (n, v) in ta.NodesWithVariance()) { 130 | yield return (n, (v, tp.Variance).Combine()); 131 | } 132 | } 133 | } else if (this.Type is IArrayTypeSymbol || this.Type is IPointerTypeSymbol) { 134 | foreach (var pair in this.TypeArguments.Single().NodesWithVariance()) { 135 | yield return pair; 136 | } 137 | } else { 138 | Debug.Assert(this.TypeArguments.Count == 0); 139 | } 140 | } 141 | } 142 | 143 | public readonly struct TypeSubstitution 144 | { 145 | public readonly IReadOnlyList ClassTypeArguments; 146 | public readonly IReadOnlyList MethodTypeArguments; 147 | 148 | public TypeSubstitution(IReadOnlyList classTypeArguments, IReadOnlyList methodTypeArguments) 149 | { 150 | this.ClassTypeArguments = classTypeArguments; 151 | this.MethodTypeArguments = methodTypeArguments; 152 | } 153 | 154 | public TypeWithNode this[TypeParameterKind kind, int i] => kind switch 155 | { 156 | TypeParameterKind.Type => ClassTypeArguments[i], 157 | TypeParameterKind.Method => MethodTypeArguments[i], 158 | _ => throw new NotSupportedException("Unknown TypeParameterKind") 159 | }; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /NullabilityInference/GraphVizGraph.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2010-2013 AlphaSierraPapa for the SharpDevelop Team 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | // software and associated documentation files (the "Software"), to deal in the Software 5 | // without restriction, including without limitation the rights to use, copy, modify, merge, 6 | // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 7 | // to whom the Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 13 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 14 | // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 15 | // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | // DEALINGS IN THE SOFTWARE. 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | using System.Diagnostics; 22 | using System.Globalization; 23 | using System.IO; 24 | using System.Text.RegularExpressions; 25 | 26 | namespace ICSharpCode.NullabilityInference 27 | { 28 | /// 29 | /// GraphViz graph. 30 | /// 31 | public sealed class GraphVizGraph 32 | { 33 | private readonly List nodes = new List(); 34 | private readonly List edges = new List(); 35 | 36 | public string? rankdir; 37 | public string? Title; 38 | 39 | public void AddEdge(GraphVizEdge edge) 40 | { 41 | edges.Add(edge); 42 | } 43 | 44 | public void AddNode(GraphVizNode node) 45 | { 46 | nodes.Add(node); 47 | } 48 | 49 | public void Save(string fileName) 50 | { 51 | using (StreamWriter writer = new StreamWriter(fileName)) { 52 | Save(writer); 53 | } 54 | } 55 | 56 | public void Show() 57 | { 58 | Show(null); 59 | } 60 | 61 | public void Show(string? name) 62 | { 63 | if (name == null) 64 | name = Title; 65 | if (name != null) { 66 | foreach (char c in Path.GetInvalidFileNameChars()) { 67 | name = name.Replace(c, '-'); 68 | } 69 | } 70 | string fileName = name != null ? Path.Combine(Path.GetTempPath(), name) : Path.GetTempFileName(); 71 | Save(fileName + ".gv"); 72 | Process.Start("dot", "\"" + fileName + ".gv\" -Tpng -o \"" + fileName + ".png\"").WaitForExit(); 73 | Process.Start(fileName + ".png"); 74 | } 75 | 76 | private static string Escape(string text) 77 | { 78 | if (Regex.IsMatch(text, @"^[\w\d]+$")) { 79 | return text; 80 | } else { 81 | return "\"" + text.Replace("\\", "\\\\").Replace("\r", "").Replace("\n", "\\n").Replace("\"", "\\\"") + "\""; 82 | } 83 | } 84 | 85 | private static void WriteGraphAttribute(TextWriter writer, string name, string? value) 86 | { 87 | if (value != null) 88 | writer.WriteLine("{0}={1};", name, Escape(value)); 89 | } 90 | 91 | internal static void WriteAttribute(TextWriter writer, string name, double? value, ref bool isFirst) 92 | { 93 | if (value != null) { 94 | WriteAttribute(writer, name, value.Value.ToString(CultureInfo.InvariantCulture), ref isFirst); 95 | } 96 | } 97 | 98 | internal static void WriteAttribute(TextWriter writer, string name, bool? value, ref bool isFirst) 99 | { 100 | if (value != null) { 101 | WriteAttribute(writer, name, value.Value ? "true" : "false", ref isFirst); 102 | } 103 | } 104 | 105 | internal static void WriteAttribute(TextWriter writer, string name, string? value, ref bool isFirst) 106 | { 107 | if (value != null) { 108 | if (isFirst) 109 | isFirst = false; 110 | else 111 | writer.Write(','); 112 | writer.Write("{0}={1}", name, Escape(value)); 113 | } 114 | } 115 | 116 | public void Save(TextWriter writer) 117 | { 118 | if (writer == null) 119 | throw new ArgumentNullException(nameof(writer)); 120 | writer.WriteLine("digraph G {"); 121 | writer.WriteLine("node [fontsize = 16];"); 122 | WriteGraphAttribute(writer, "rankdir", rankdir); 123 | foreach (GraphVizNode node in nodes) { 124 | node.Save(writer); 125 | } 126 | foreach (GraphVizEdge edge in edges) { 127 | edge.Save(writer); 128 | } 129 | writer.WriteLine("}"); 130 | } 131 | } 132 | 133 | public sealed class GraphVizEdge 134 | { 135 | public readonly string Source, Target; 136 | 137 | /// edge stroke color 138 | public string? color; 139 | /// use edge to affect node ranking 140 | public bool? constraint; 141 | 142 | public string? label; 143 | 144 | public string? style; 145 | 146 | /// point size of label 147 | public int? fontsize; 148 | 149 | public GraphVizEdge(string source, string target) 150 | { 151 | if (source == null) 152 | throw new ArgumentNullException(nameof(source)); 153 | if (target == null) 154 | throw new ArgumentNullException(nameof(target)); 155 | this.Source = source; 156 | this.Target = target; 157 | } 158 | 159 | public GraphVizEdge(int source, int target) 160 | { 161 | this.Source = source.ToString(CultureInfo.InvariantCulture); 162 | this.Target = target.ToString(CultureInfo.InvariantCulture); 163 | } 164 | 165 | public void Save(TextWriter writer) 166 | { 167 | writer.Write("{0} -> {1} [", Source, Target); 168 | bool isFirst = true; 169 | GraphVizGraph.WriteAttribute(writer, "label", label, ref isFirst); 170 | GraphVizGraph.WriteAttribute(writer, "style", style, ref isFirst); 171 | GraphVizGraph.WriteAttribute(writer, "fontsize", fontsize, ref isFirst); 172 | GraphVizGraph.WriteAttribute(writer, "color", color, ref isFirst); 173 | GraphVizGraph.WriteAttribute(writer, "constraint", constraint, ref isFirst); 174 | writer.WriteLine("];"); 175 | } 176 | } 177 | 178 | public sealed class GraphVizNode 179 | { 180 | public readonly string ID; 181 | public string? label; 182 | 183 | public string? labelloc; 184 | 185 | /// point size of label 186 | public int? fontsize; 187 | 188 | /// minimum height in inches 189 | public double? height; 190 | 191 | /// space around label 192 | public string? margin; 193 | 194 | /// node shape 195 | public string? shape; 196 | 197 | public GraphVizNode(string id) 198 | { 199 | if (id == null) 200 | throw new ArgumentNullException(nameof(id)); 201 | this.ID = id; 202 | } 203 | 204 | public GraphVizNode(int id) 205 | { 206 | this.ID = id.ToString(CultureInfo.InvariantCulture); 207 | } 208 | 209 | public void Save(TextWriter writer) 210 | { 211 | writer.Write(ID); 212 | writer.Write(" ["); 213 | bool isFirst = true; 214 | GraphVizGraph.WriteAttribute(writer, "label", label, ref isFirst); 215 | GraphVizGraph.WriteAttribute(writer, "labelloc", labelloc, ref isFirst); 216 | GraphVizGraph.WriteAttribute(writer, "fontsize", fontsize, ref isFirst); 217 | GraphVizGraph.WriteAttribute(writer, "margin", margin, ref isFirst); 218 | GraphVizGraph.WriteAttribute(writer, "shape", shape, ref isFirst); 219 | writer.WriteLine("];"); 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /.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 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | ## Ignore Visual Studio temporary files, build results, and 30 | ## files generated by popular Visual Studio add-ons. 31 | ## 32 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 33 | 34 | # User-specific files 35 | *.rsuser 36 | *.suo 37 | *.user 38 | *.userosscache 39 | *.sln.docstates 40 | 41 | # User-specific files (MonoDevelop/Xamarin Studio) 42 | *.userprefs 43 | 44 | # Mono auto generated files 45 | mono_crash.* 46 | 47 | # Build results 48 | [Dd]ebug/ 49 | [Dd]ebugPublic/ 50 | [Rr]elease/ 51 | [Rr]eleases/ 52 | x64/ 53 | x86/ 54 | [Ww][Ii][Nn]32/ 55 | [Aa][Rr][Mm]/ 56 | [Aa][Rr][Mm]64/ 57 | bld/ 58 | [Bb]in/ 59 | [Oo]bj/ 60 | [Ll]og/ 61 | [Ll]ogs/ 62 | 63 | # Visual Studio 2015/2017 cache/options directory 64 | .vs/ 65 | # Uncomment if you have tasks that create the project's static files in wwwroot 66 | #wwwroot/ 67 | 68 | # Visual Studio 2017 auto generated files 69 | Generated\ Files/ 70 | 71 | # MSTest test Results 72 | [Tt]est[Rr]esult*/ 73 | [Bb]uild[Ll]og.* 74 | 75 | # NUnit 76 | *.VisualState.xml 77 | TestResult.xml 78 | nunit-*.xml 79 | 80 | # Build Results of an ATL Project 81 | [Dd]ebugPS/ 82 | [Rr]eleasePS/ 83 | dlldata.c 84 | 85 | # Benchmark Results 86 | BenchmarkDotNet.Artifacts/ 87 | 88 | # .NET Core 89 | project.lock.json 90 | project.fragment.lock.json 91 | artifacts/ 92 | 93 | # ASP.NET Scaffolding 94 | ScaffoldingReadMe.txt 95 | 96 | # StyleCop 97 | StyleCopReport.xml 98 | 99 | # Files built by Visual Studio 100 | *_i.c 101 | *_p.c 102 | *_h.h 103 | *.ilk 104 | *.meta 105 | *.obj 106 | *.iobj 107 | *.pch 108 | *.pdb 109 | *.ipdb 110 | *.pgc 111 | *.pgd 112 | *.rsp 113 | *.sbr 114 | *.tlb 115 | *.tli 116 | *.tlh 117 | *.tmp 118 | *.tmp_proj 119 | *_wpftmp.csproj 120 | *.log 121 | *.vspscc 122 | *.vssscc 123 | .builds 124 | *.pidb 125 | *.svclog 126 | *.scc 127 | 128 | # Chutzpah Test files 129 | _Chutzpah* 130 | 131 | # Visual C++ cache files 132 | ipch/ 133 | *.aps 134 | *.ncb 135 | *.opendb 136 | *.opensdf 137 | *.sdf 138 | *.cachefile 139 | *.VC.db 140 | *.VC.VC.opendb 141 | 142 | # Visual Studio profiler 143 | *.psess 144 | *.vsp 145 | *.vspx 146 | *.sap 147 | 148 | # Visual Studio Trace Files 149 | *.e2e 150 | 151 | # TFS 2012 Local Workspace 152 | $tf/ 153 | 154 | # Guidance Automation Toolkit 155 | *.gpState 156 | 157 | # ReSharper is a .NET coding add-in 158 | _ReSharper*/ 159 | *.[Rr]e[Ss]harper 160 | *.DotSettings.user 161 | 162 | # TeamCity is a build add-in 163 | _TeamCity* 164 | 165 | # DotCover is a Code Coverage Tool 166 | *.dotCover 167 | 168 | # AxoCover is a Code Coverage Tool 169 | .axoCover/* 170 | !.axoCover/settings.json 171 | 172 | # Coverlet is a free, cross platform Code Coverage Tool 173 | coverage*[.json, .xml, .info] 174 | 175 | # Visual Studio code coverage results 176 | *.coverage 177 | *.coveragexml 178 | 179 | # NCrunch 180 | _NCrunch_* 181 | .*crunch*.local.xml 182 | nCrunchTemp_* 183 | 184 | # MightyMoose 185 | *.mm.* 186 | AutoTest.Net/ 187 | 188 | # Web workbench (sass) 189 | .sass-cache/ 190 | 191 | # Installshield output folder 192 | [Ee]xpress/ 193 | 194 | # DocProject is a documentation generator add-in 195 | DocProject/buildhelp/ 196 | DocProject/Help/*.HxT 197 | DocProject/Help/*.HxC 198 | DocProject/Help/*.hhc 199 | DocProject/Help/*.hhk 200 | DocProject/Help/*.hhp 201 | DocProject/Help/Html2 202 | DocProject/Help/html 203 | 204 | # Click-Once directory 205 | publish/ 206 | 207 | # Publish Web Output 208 | *.[Pp]ublish.xml 209 | *.azurePubxml 210 | # Note: Comment the next line if you want to checkin your web deploy settings, 211 | # but database connection strings (with potential passwords) will be unencrypted 212 | *.pubxml 213 | *.publishproj 214 | 215 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 216 | # checkin your Azure Web App publish settings, but sensitive information contained 217 | # in these scripts will be unencrypted 218 | PublishScripts/ 219 | 220 | # NuGet Packages 221 | *.nupkg 222 | # NuGet Symbol Packages 223 | *.snupkg 224 | # The packages folder can be ignored because of Package Restore 225 | **/[Pp]ackages/* 226 | # except build/, which is used as an MSBuild target. 227 | !**/[Pp]ackages/build/ 228 | # Uncomment if necessary however generally it will be regenerated when needed 229 | #!**/[Pp]ackages/repositories.config 230 | # NuGet v3's project.json files produces more ignorable files 231 | *.nuget.props 232 | *.nuget.targets 233 | 234 | # Microsoft Azure Build Output 235 | csx/ 236 | *.build.csdef 237 | 238 | # Microsoft Azure Emulator 239 | ecf/ 240 | rcf/ 241 | 242 | # Windows Store app package directories and files 243 | AppPackages/ 244 | BundleArtifacts/ 245 | Package.StoreAssociation.xml 246 | _pkginfo.txt 247 | *.appx 248 | *.appxbundle 249 | *.appxupload 250 | 251 | # Visual Studio cache files 252 | # files ending in .cache can be ignored 253 | *.[Cc]ache 254 | # but keep track of directories ending in .cache 255 | !?*.[Cc]ache/ 256 | 257 | # Others 258 | ClientBin/ 259 | ~$* 260 | *~ 261 | *.dbmdl 262 | *.dbproj.schemaview 263 | *.jfm 264 | *.pfx 265 | *.publishsettings 266 | orleans.codegen.cs 267 | 268 | # Including strong name files can present a security risk 269 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 270 | #*.snk 271 | 272 | # Since there are multiple workflows, uncomment next line to ignore bower_components 273 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 274 | #bower_components/ 275 | 276 | # RIA/Silverlight projects 277 | Generated_Code/ 278 | 279 | # Backup & report files from converting an old project file 280 | # to a newer Visual Studio version. Backup files are not needed, 281 | # because we have git ;-) 282 | _UpgradeReport_Files/ 283 | Backup*/ 284 | UpgradeLog*.XML 285 | UpgradeLog*.htm 286 | ServiceFabricBackup/ 287 | *.rptproj.bak 288 | 289 | # SQL Server files 290 | *.mdf 291 | *.ldf 292 | *.ndf 293 | 294 | # Business Intelligence projects 295 | *.rdl.data 296 | *.bim.layout 297 | *.bim_*.settings 298 | *.rptproj.rsuser 299 | *- [Bb]ackup.rdl 300 | *- [Bb]ackup ([0-9]).rdl 301 | *- [Bb]ackup ([0-9][0-9]).rdl 302 | 303 | # Microsoft Fakes 304 | FakesAssemblies/ 305 | 306 | # GhostDoc plugin setting file 307 | *.GhostDoc.xml 308 | 309 | # Node.js Tools for Visual Studio 310 | .ntvs_analysis.dat 311 | node_modules/ 312 | 313 | # Visual Studio 6 build log 314 | *.plg 315 | 316 | # Visual Studio 6 workspace options file 317 | *.opt 318 | 319 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 320 | *.vbw 321 | 322 | # Visual Studio LightSwitch build output 323 | **/*.HTMLClient/GeneratedArtifacts 324 | **/*.DesktopClient/GeneratedArtifacts 325 | **/*.DesktopClient/ModelManifest.xml 326 | **/*.Server/GeneratedArtifacts 327 | **/*.Server/ModelManifest.xml 328 | _Pvt_Extensions 329 | 330 | # Paket dependency manager 331 | .paket/paket.exe 332 | paket-files/ 333 | 334 | # FAKE - F# Make 335 | .fake/ 336 | 337 | # CodeRush personal settings 338 | .cr/personal 339 | 340 | # Python Tools for Visual Studio (PTVS) 341 | __pycache__/ 342 | *.pyc 343 | 344 | # Cake - Uncomment if you are using it 345 | # tools/** 346 | # !tools/packages.config 347 | 348 | # Tabs Studio 349 | *.tss 350 | 351 | # Telerik's JustMock configuration file 352 | *.jmconfig 353 | 354 | # BizTalk build output 355 | *.btp.cs 356 | *.btm.cs 357 | *.odx.cs 358 | *.xsd.cs 359 | 360 | # OpenCover UI analysis results 361 | OpenCover/ 362 | 363 | # Azure Stream Analytics local run output 364 | ASALocalRun/ 365 | 366 | # MSBuild Binary and Structured Log 367 | *.binlog 368 | 369 | # NVidia Nsight GPU debugger configuration file 370 | *.nvuser 371 | 372 | # MFractors (Xamarin productivity tool) working folder 373 | .mfractor/ 374 | 375 | # Local History for Visual Studio 376 | .localhistory/ 377 | 378 | # BeatPulse healthcheck temp database 379 | healthchecksdb 380 | 381 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 382 | MigrationBackup/ 383 | 384 | # Ionide (cross platform F# VS Code tools) working folder 385 | .ionide/ 386 | 387 | # Fody - auto-generated XML schema 388 | FodyWeavers.xsd 389 | -------------------------------------------------------------------------------- /NullabilityInference/InferredNullabilitySyntaxRewriter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Daniel Grunwald 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | // software and associated documentation files (the "Software"), to deal in the Software 5 | // without restriction, including without limitation the rights to use, copy, modify, merge, 6 | // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 7 | // to whom the Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 13 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 14 | // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 15 | // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | // DEALINGS IN THE SOFTWARE. 18 | 19 | using System.Diagnostics; 20 | using System.Linq; 21 | using System.Threading; 22 | using System.Threading.Tasks; 23 | using Microsoft.CodeAnalysis; 24 | using Microsoft.CodeAnalysis.CSharp; 25 | using Microsoft.CodeAnalysis.CSharp.Syntax; 26 | 27 | namespace ICSharpCode.NullabilityInference 28 | { 29 | /// 30 | /// Rewrites a C# syntax tree by replacing nullable reference type syntax with that inferred by our analysis. 31 | /// 32 | internal sealed class InferredNullabilitySyntaxRewriter : CSharpSyntaxRewriter 33 | { 34 | private readonly SemanticModel semanticModel; 35 | private readonly TypeSystem typeSystem; 36 | private readonly SyntaxToNodeMapping mapping; 37 | private readonly CancellationToken cancellationToken; 38 | private Statistics stats = new Statistics(); 39 | 40 | public InferredNullabilitySyntaxRewriter(SemanticModel semanticModel, TypeSystem typeSystem, SyntaxToNodeMapping mapping, CancellationToken cancellationToken) 41 | : base(visitIntoStructuredTrivia: true) 42 | { 43 | this.semanticModel = semanticModel; 44 | this.typeSystem = typeSystem; 45 | this.mapping = mapping; 46 | this.cancellationToken = cancellationToken; 47 | } 48 | 49 | public ref readonly Statistics Statistics => ref stats; 50 | 51 | private bool isActive = true; 52 | 53 | public override SyntaxNode? VisitNullableDirectiveTrivia(NullableDirectiveTriviaSyntax node) 54 | { 55 | isActive = node.SettingToken.IsKind(SyntaxKind.RestoreKeyword); 56 | return base.VisitNullableDirectiveTrivia(node); 57 | } 58 | 59 | public override SyntaxNode? VisitNullableType(NullableTypeSyntax node) 60 | { 61 | var elementType = node.ElementType.Accept(this); 62 | if (elementType == null) 63 | return null; 64 | var symbolInfo = semanticModel.GetSymbolInfo(node); 65 | if (isActive && symbolInfo.Symbol is ITypeSymbol { IsReferenceType: true }) { 66 | // Remove existing nullable reference types 67 | return elementType.WithTrailingTrivia(node.GetTrailingTrivia()); 68 | } else { 69 | return node.ReplaceNode(node.ElementType, elementType); 70 | } 71 | } 72 | 73 | public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node) 74 | { 75 | return HandleTypeName(node, base.VisitIdentifierName(node)); 76 | } 77 | 78 | public override SyntaxNode? VisitGenericName(GenericNameSyntax node) 79 | { 80 | return HandleTypeName(node, base.VisitGenericName(node)); 81 | } 82 | 83 | public override SyntaxNode? VisitPredefinedType(PredefinedTypeSyntax node) 84 | { 85 | return HandleTypeName(node, base.VisitPredefinedType(node)); 86 | } 87 | 88 | public override SyntaxNode? VisitQualifiedName(QualifiedNameSyntax node) 89 | { 90 | return HandleTypeName(node, base.VisitQualifiedName(node)); 91 | } 92 | 93 | private SyntaxNode? HandleTypeName(TypeSyntax node, SyntaxNode? newNode) 94 | { 95 | if (!isActive || !GraphBuildingSyntaxVisitor.CanBeMadeNullableSyntax(node)) { 96 | return newNode; 97 | } 98 | var symbolInfo = semanticModel.GetSymbolInfo(node, cancellationToken); 99 | if (symbolInfo.Symbol is ITypeSymbol ty && ty.CanBeMadeNullable() && newNode is TypeSyntax newTypeSyntax) { 100 | var nullNode = mapping[node]; 101 | if (nullNode.NullType == NullType.Nullable) { 102 | stats.NullableCount++; 103 | return SyntaxFactory.NullableType( 104 | elementType: newTypeSyntax.WithoutTrailingTrivia(), 105 | questionToken: SyntaxFactory.Token(SyntaxKind.QuestionToken) 106 | ).WithTrailingTrivia(newTypeSyntax.GetTrailingTrivia()); 107 | } else { 108 | stats.NonNullCount++; 109 | } 110 | } 111 | return newNode; 112 | } 113 | 114 | public override SyntaxNode? VisitArrayType(ArrayTypeSyntax node) 115 | { 116 | var newNode = base.VisitArrayType(node); 117 | if (isActive && GraphBuildingSyntaxVisitor.CanBeMadeNullableSyntax(node) && newNode is TypeSyntax newTypeSyntax) { 118 | var nullNode = mapping[node]; 119 | if (nullNode.NullType == NullType.Nullable) { 120 | stats.NullableCount++; 121 | return SyntaxFactory.NullableType( 122 | elementType: newTypeSyntax.WithoutTrailingTrivia(), 123 | questionToken: SyntaxFactory.Token(SyntaxKind.QuestionToken) 124 | ).WithTrailingTrivia(newTypeSyntax.GetTrailingTrivia()); 125 | } else { 126 | stats.NonNullCount++; 127 | } 128 | } 129 | return newNode; 130 | } 131 | 132 | public override SyntaxNode? VisitParameter(ParameterSyntax node) 133 | { 134 | var param = semanticModel.GetDeclaredSymbol(node, cancellationToken); 135 | node = (ParameterSyntax)base.VisitParameter(node)!; 136 | if (isActive && param != null && typeSystem.TryGetOutParameterFlowNodes(param, out var pair)) { 137 | if (pair.whenTrue.NullType != pair.whenFalse.NullType) { 138 | Debug.Assert(pair.whenTrue.NullType == NullType.NonNull || pair.whenFalse.NullType == NullType.NonNull); 139 | // Create [NotNullWhen] attribute 140 | bool notNullWhen = (pair.whenTrue.NullType == NullType.NonNull); 141 | var attrArgument = SyntaxFactory.LiteralExpression(notNullWhen ? SyntaxKind.TrueLiteralExpression : SyntaxKind.FalseLiteralExpression); 142 | var newAttribute = SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("NotNullWhen"), 143 | SyntaxFactory.AttributeArgumentList(SyntaxFactory.SingletonSeparatedList(SyntaxFactory.AttributeArgument(attrArgument)))); 144 | var newAttributeList = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(newAttribute)); 145 | node = node.AddAttributeLists(newAttributeList.WithTrailingTrivia(SyntaxFactory.Space)); 146 | needsUsingCodeAnalysis = true; 147 | stats.NotNullWhenCount++; 148 | } 149 | } 150 | return node; 151 | } 152 | 153 | private bool needsUsingCodeAnalysis; 154 | 155 | public override SyntaxNode? VisitCompilationUnit(CompilationUnitSyntax node) 156 | { 157 | bool hasUsingCodeAnalysis = false; 158 | foreach (var u in node.Usings) { 159 | var symbolInfo = semanticModel.GetSymbolInfo(u.Name, cancellationToken); 160 | if (symbolInfo.Symbol is INamespaceSymbol ns && ns.GetFullName() == "System.Diagnostics.CodeAnalysis") { 161 | hasUsingCodeAnalysis = true; 162 | } 163 | } 164 | node = (CompilationUnitSyntax)base.VisitCompilationUnit(node)!; 165 | if (needsUsingCodeAnalysis && !hasUsingCodeAnalysis) { 166 | var qname = SyntaxFactory.QualifiedName(SyntaxFactory.QualifiedName(SyntaxFactory.IdentifierName("System"), SyntaxFactory.IdentifierName("Diagnostics")), SyntaxFactory.IdentifierName("CodeAnalysis")); 167 | node = node.AddUsings(SyntaxFactory.UsingDirective(qname.WithLeadingTrivia(SyntaxFactory.Space))); 168 | } 169 | return node; 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /NullabilityInference/System.Diagnostics.CodeAnalysis/NullableAttributes.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | #define INTERNAL_NULLABLE_ATTRIBUTES 6 | 7 | namespace System.Diagnostics.CodeAnalysis 8 | { 9 | /// Specifies that null is allowed as an input even if the corresponding type disallows it. 10 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] 11 | #if INTERNAL_NULLABLE_ATTRIBUTES 12 | internal 13 | #else 14 | public 15 | #endif 16 | sealed class AllowNullAttribute : Attribute 17 | { } 18 | 19 | /// Specifies that null is disallowed as an input even if the corresponding type allows it. 20 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] 21 | #if INTERNAL_NULLABLE_ATTRIBUTES 22 | internal 23 | #else 24 | public 25 | #endif 26 | sealed class DisallowNullAttribute : Attribute 27 | { } 28 | 29 | /// Specifies that an output may be null even if the corresponding type disallows it. 30 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] 31 | #if INTERNAL_NULLABLE_ATTRIBUTES 32 | internal 33 | #else 34 | public 35 | #endif 36 | sealed class MaybeNullAttribute : Attribute 37 | { } 38 | 39 | /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. 40 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] 41 | #if INTERNAL_NULLABLE_ATTRIBUTES 42 | internal 43 | #else 44 | public 45 | #endif 46 | sealed class NotNullAttribute : Attribute 47 | { } 48 | 49 | /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. 50 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] 51 | #if INTERNAL_NULLABLE_ATTRIBUTES 52 | internal 53 | #else 54 | public 55 | #endif 56 | sealed class MaybeNullWhenAttribute : Attribute 57 | { 58 | /// Initializes the attribute with the specified return value condition. 59 | /// 60 | /// The return value condition. If the method returns this value, the associated parameter may be null. 61 | /// 62 | public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; 63 | 64 | /// Gets the return value condition. 65 | public bool ReturnValue { get; } 66 | } 67 | 68 | /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. 69 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] 70 | #if INTERNAL_NULLABLE_ATTRIBUTES 71 | internal 72 | #else 73 | public 74 | #endif 75 | sealed class NotNullWhenAttribute : Attribute 76 | { 77 | /// Initializes the attribute with the specified return value condition. 78 | /// 79 | /// The return value condition. If the method returns this value, the associated parameter will not be null. 80 | /// 81 | public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; 82 | 83 | /// Gets the return value condition. 84 | public bool ReturnValue { get; } 85 | } 86 | 87 | /// Specifies that the output will be non-null if the named parameter is non-null. 88 | [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] 89 | #if INTERNAL_NULLABLE_ATTRIBUTES 90 | internal 91 | #else 92 | public 93 | #endif 94 | sealed class NotNullIfNotNullAttribute : Attribute 95 | { 96 | /// Initializes the attribute with the associated parameter name. 97 | /// 98 | /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. 99 | /// 100 | public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; 101 | 102 | /// Gets the associated parameter name. 103 | public string ParameterName { get; } 104 | } 105 | 106 | /// Applied to a method that will never return under any circumstance. 107 | [AttributeUsage(AttributeTargets.Method, Inherited = false)] 108 | #if INTERNAL_NULLABLE_ATTRIBUTES 109 | internal 110 | #else 111 | public 112 | #endif 113 | sealed class DoesNotReturnAttribute : Attribute 114 | { } 115 | 116 | /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. 117 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] 118 | #if INTERNAL_NULLABLE_ATTRIBUTES 119 | internal 120 | #else 121 | public 122 | #endif 123 | sealed class DoesNotReturnIfAttribute : Attribute 124 | { 125 | /// Initializes the attribute with the specified parameter value. 126 | /// 127 | /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to 128 | /// the associated parameter matches this value. 129 | /// 130 | public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; 131 | 132 | /// Gets the condition parameter value. 133 | public bool ParameterValue { get; } 134 | } 135 | 136 | /// Specifies that the method or property will ensure that the listed field and property members have not-null values. 137 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] 138 | #if INTERNAL_NULLABLE_ATTRIBUTES 139 | internal 140 | #else 141 | public 142 | #endif 143 | sealed class MemberNotNullAttribute : Attribute 144 | { 145 | /// Initializes the attribute with a field or property member. 146 | /// 147 | /// The field or property member that is promised to be not-null. 148 | /// 149 | public MemberNotNullAttribute(string member) => Members = new[] { member }; 150 | 151 | /// Initializes the attribute with the list of field and property members. 152 | /// 153 | /// The list of field and property members that are promised to be not-null. 154 | /// 155 | public MemberNotNullAttribute(params string[] members) => Members = members; 156 | 157 | /// Gets field or property member names. 158 | public string[] Members { get; } 159 | } 160 | 161 | /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. 162 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] 163 | #if INTERNAL_NULLABLE_ATTRIBUTES 164 | internal 165 | #else 166 | public 167 | #endif 168 | sealed class MemberNotNullWhenAttribute : Attribute 169 | { 170 | /// Initializes the attribute with the specified return value condition and a field or property member. 171 | /// 172 | /// The return value condition. If the method returns this value, the associated parameter will not be null. 173 | /// 174 | /// 175 | /// The field or property member that is promised to be not-null. 176 | /// 177 | public MemberNotNullWhenAttribute(bool returnValue, string member) 178 | { 179 | ReturnValue = returnValue; 180 | Members = new[] { member }; 181 | } 182 | 183 | /// Initializes the attribute with the specified return value condition and list of field and property members. 184 | /// 185 | /// The return value condition. If the method returns this value, the associated parameter will not be null. 186 | /// 187 | /// 188 | /// The list of field and property members that are promised to be not-null. 189 | /// 190 | public MemberNotNullWhenAttribute(bool returnValue, params string[] members) 191 | { 192 | ReturnValue = returnValue; 193 | Members = members; 194 | } 195 | 196 | /// Gets the return value condition. 197 | public bool ReturnValue { get; } 198 | 199 | /// Gets field or property member names. 200 | public string[] Members { get; } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /NullabilityInference.Tests/NullabilityTestHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Daniel Grunwald 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | // software and associated documentation files (the "Software"), to deal in the Software 5 | // without restriction, including without limitation the rights to use, copy, modify, merge, 6 | // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 7 | // to whom the Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 13 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 14 | // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 15 | // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | // DEALINGS IN THE SOFTWARE. 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | using System.Diagnostics; 22 | using System.IO; 23 | using System.Linq; 24 | using System.Text.RegularExpressions; 25 | using System.Threading; 26 | using System.Threading.Tasks; 27 | using Microsoft.CodeAnalysis; 28 | using Microsoft.CodeAnalysis.CSharp; 29 | using Xunit; 30 | using Xunit.Sdk; 31 | 32 | namespace ICSharpCode.NullabilityInference.Tests 33 | { 34 | public class NullabilityTestHelper 35 | { 36 | private static readonly string refAsmPath = @"c:\program files\dotnet\packs\Microsoft.NETCore.App.Ref\3.1.0\ref\netcoreapp3.1"; 37 | private static readonly Lazy> defaultReferences = new Lazy>(delegate { 38 | return new[] 39 | { 40 | MetadataReference.CreateFromFile(Path.Combine(refAsmPath, "System.dll")), 41 | MetadataReference.CreateFromFile(Path.Combine(refAsmPath, "System.Collections.dll")), 42 | MetadataReference.CreateFromFile(Path.Combine(refAsmPath, "System.Runtime.dll")), 43 | MetadataReference.CreateFromFile(Path.Combine(refAsmPath, "System.Linq.dll")), 44 | MetadataReference.CreateFromFile(Path.Combine(refAsmPath, "System.Threading.dll")), 45 | }; 46 | }); 47 | 48 | static NullabilityTestHelper() 49 | { 50 | Debug.Listeners.Insert(0, new TestTraceListener()); 51 | } 52 | 53 | private class TestTraceListener : DefaultTraceListener 54 | { 55 | public override void Fail(string message, string detailMessage) 56 | { 57 | throw new InvalidOperationException(message + " " + detailMessage); 58 | } 59 | } 60 | 61 | protected static (CSharpCompilation, NullCheckingEngine) CompileAndAnalyze(string program, CancellationToken cancellationToken = default) 62 | { 63 | var syntaxTree = SyntaxFactory.ParseSyntaxTree(program, new CSharpParseOptions(LanguageVersion.CSharp8), cancellationToken: cancellationToken); 64 | var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true); 65 | var compilation = CSharpCompilation.Create("test", new[] { syntaxTree }, defaultReferences.Value, options); 66 | compilation = AllNullableSyntaxRewriter.MakeAllReferenceTypesNullable(compilation, cancellationToken); 67 | string allNullableText = compilation.SyntaxTrees.Single().GetText().ToString(); 68 | foreach (var diag in compilation.GetDiagnostics(cancellationToken)) { 69 | Assert.False(diag.Severity == DiagnosticSeverity.Error, diag.ToString() + "\r\n\r\nSource:\r\n" + program); 70 | } 71 | var engine = new NullCheckingEngine(compilation); 72 | engine.Analyze(ConflictResolutionStrategy.MinimizeWarnings, cancellationToken); 73 | return (compilation, engine); 74 | } 75 | 76 | protected static void AssertNullabilityInference(string expectedProgram, string inputProgram = null, bool? hasWarnings = false, CancellationToken cancellationToken = default) 77 | { 78 | inputProgram ??= Regex.Replace(expectedProgram, @"(?(); 81 | engine.ConvertSyntaxTrees(cancellationToken, tree => { lock (newSyntaxes) newSyntaxes.Add(tree); }); 82 | string outputProgram = newSyntaxes.Single().GetText(cancellationToken).ToString(); 83 | // engine.ExportTypeGraph().Show(); 84 | if (hasWarnings != null) { 85 | bool actual = ReachableNodes(engine.TypeSystem.NullableNode, n => n.Successors).Contains(engine.TypeSystem.NonNullNode); 86 | if (hasWarnings != actual) { 87 | throw new AssertActualExpectedException(hasWarnings, actual, 88 | actual ? "Unexpected path from to " : "Missing path from to "); 89 | } 90 | } 91 | Assert.Equal(expectedProgram, outputProgram); 92 | } 93 | 94 | protected static bool HasPathFromParameterToReturnType(string program) 95 | { 96 | var (compilation, engine) = CompileAndAnalyze(program); 97 | var programClass = compilation.GetTypeByMetadataName("Program"); 98 | Assert.False(programClass == null, "Could not find 'Program' in test"); 99 | var testMethod = (IMethodSymbol)programClass!.GetMembers("Test").Single(); 100 | var parameterNode = engine.TypeSystem.GetSymbolType(testMethod.Parameters.Single()).Node; 101 | var returnNode = engine.TypeSystem.GetSymbolType(testMethod).Node; 102 | // engine.ExportTypeGraph().Show(); 103 | return ReachableNodes(parameterNode, n => n.Successors).Contains(returnNode); 104 | } 105 | 106 | [Flags] 107 | protected enum TestedPaths 108 | { 109 | None = 0, 110 | InputMustBeNonNull = 1, 111 | ResultMustBeNullable = 2, 112 | ResultDependsOnInput = 4, 113 | } 114 | 115 | protected static void CheckPaths(string program, bool? inputMustBeNonNull = null, bool? returnNullable = null, bool? returnDependsOnInput = null, bool? hasWarnings = false) 116 | { 117 | var (compilation, engine) = CompileAndAnalyze(program); 118 | var programClass = compilation.GetTypeByMetadataName("Program"); 119 | Assert.False(programClass == null, "Could not find 'Program' in test"); 120 | var testMethod = (IMethodSymbol)programClass!.GetMembers("Test").Single(); 121 | var returnNode = engine.TypeSystem.GetSymbolType(testMethod).Node; 122 | // engine.ExportTypeGraph().Show(); 123 | if (inputMustBeNonNull != null) { 124 | var parameterNode = engine.TypeSystem.GetSymbolType(testMethod.Parameters.Single()).Node; 125 | bool inputIsNonNull = ReachableNodes(parameterNode, n => n.Successors).Contains(engine.TypeSystem.NonNullNode); 126 | if (inputMustBeNonNull != inputIsNonNull) { 127 | throw new AssertActualExpectedException(inputMustBeNonNull, inputIsNonNull, 128 | inputIsNonNull ? "Unexpected path from input to " : "Missing path from input to "); 129 | } 130 | } 131 | if (returnNullable != null) { 132 | bool actual = ReachableNodes(engine.TypeSystem.NullableNode, n => n.Successors).Contains(returnNode); 133 | if (returnNullable != actual) { 134 | throw new AssertActualExpectedException(returnNullable, actual, 135 | actual ? "Unexpected path from to return" : "Missing path from to return"); 136 | } 137 | } 138 | if (returnDependsOnInput != null) { 139 | var parameterNode = engine.TypeSystem.GetSymbolType(testMethod.Parameters.Single()).Node; 140 | bool actual = ReachableNodes(parameterNode, n => n.Successors).Contains(returnNode); 141 | if (returnDependsOnInput != actual) { 142 | throw new AssertActualExpectedException(returnDependsOnInput, actual, 143 | actual ? "Unexpected path from parameter to return" : "Missing path from parameter to return"); 144 | } 145 | } 146 | if (hasWarnings != null) { 147 | bool actual = ReachableNodes(engine.TypeSystem.NullableNode, n => n.Successors).Contains(engine.TypeSystem.NonNullNode); 148 | if (hasWarnings != actual) { 149 | throw new AssertActualExpectedException(hasWarnings, actual, 150 | actual ? "Unexpected path from to " : "Missing path from to "); 151 | } 152 | } 153 | } 154 | 155 | private static HashSet ReachableNodes(T root, Func> successors) 156 | { 157 | var visited = new HashSet(); 158 | Visit(root); 159 | return visited; 160 | 161 | void Visit(T node) 162 | { 163 | if (visited.Add(node)) { 164 | foreach (var successor in successors(node)) { 165 | Visit(successor); 166 | } 167 | } 168 | } 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /NullabilityInference/GraphBuildingSyntaxVisitor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Daniel Grunwald 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | // software and associated documentation files (the "Software"), to deal in the Software 5 | // without restriction, including without limitation the rights to use, copy, modify, merge, 6 | // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 7 | // to whom the Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 13 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 14 | // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 15 | // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | // DEALINGS IN THE SOFTWARE. 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | using System.Diagnostics; 22 | using System.Linq; 23 | using System.Threading; 24 | using Microsoft.CodeAnalysis; 25 | using Microsoft.CodeAnalysis.CSharp; 26 | using Microsoft.CodeAnalysis.CSharp.Syntax; 27 | 28 | namespace ICSharpCode.NullabilityInference 29 | { 30 | /// 31 | /// Base class for NodeBuildingSyntaxVisitor and EdgeBuildingSyntaxVisitor. 32 | /// 33 | internal abstract class GraphBuildingSyntaxVisitor : CSharpSyntaxVisitor 34 | { 35 | internal readonly SemanticModel semanticModel; 36 | protected readonly TypeSystem.Builder typeSystem; 37 | protected readonly CancellationToken cancellationToken; 38 | 39 | protected GraphBuildingSyntaxVisitor(SemanticModel semanticModel, TypeSystem.Builder typeSystem, CancellationToken cancellationToken) 40 | { 41 | this.semanticModel = semanticModel; 42 | this.typeSystem = typeSystem; 43 | this.cancellationToken = cancellationToken; 44 | } 45 | 46 | public override TypeWithNode VisitIdentifierName(IdentifierNameSyntax node) 47 | { 48 | return HandleTypeName(node, null); 49 | } 50 | 51 | public override TypeWithNode VisitGenericName(GenericNameSyntax node) 52 | { 53 | return HandleTypeName(node, node.TypeArgumentList.Arguments); 54 | } 55 | 56 | public override TypeWithNode VisitPredefinedType(PredefinedTypeSyntax node) 57 | { 58 | return HandleTypeName(node, null); 59 | } 60 | 61 | public override TypeWithNode VisitQualifiedName(QualifiedNameSyntax node) 62 | { 63 | return HandleTypeName(node, CollectTypeArgs(node)); 64 | } 65 | 66 | protected List CollectTypeArgs(ExpressionSyntax node) 67 | { 68 | List typeArgs = new List(); 69 | Visit(node); 70 | return typeArgs; 71 | 72 | void Visit(ExpressionSyntax s) 73 | { 74 | switch (s) { 75 | case MemberAccessExpressionSyntax maes: 76 | Visit(maes.Expression); 77 | Visit(maes.Name); 78 | break; 79 | case QualifiedNameSyntax qns: 80 | Visit(qns.Left); 81 | Visit(qns.Right); 82 | break; 83 | case AliasQualifiedNameSyntax aqns: 84 | Visit(aqns.Name); 85 | break; 86 | case GenericNameSyntax gns: 87 | typeArgs.AddRange(gns.TypeArgumentList.Arguments); 88 | break; 89 | } 90 | } 91 | } 92 | 93 | public override TypeWithNode VisitAliasQualifiedName(AliasQualifiedNameSyntax node) 94 | { 95 | return HandleTypeName(node, (node.Name as GenericNameSyntax)?.TypeArgumentList.Arguments); 96 | } 97 | 98 | protected abstract TypeWithNode HandleTypeName(TypeSyntax node, IEnumerable? typeArguments); 99 | 100 | protected TypeWithNode[] InheritOuterTypeArguments(TypeWithNode[]? syntacticTypeArgs, ITypeSymbol ty) 101 | { 102 | if (ty.ContainingType.FullArity() > 0) { 103 | var typeArgs = new TypeWithNode[ty.FullArity()]; 104 | int pos = typeArgs.Length; 105 | if (syntacticTypeArgs != null) { 106 | pos -= syntacticTypeArgs.Length; 107 | Array.Copy(syntacticTypeArgs, 0, typeArgs, pos, syntacticTypeArgs.Length); 108 | } 109 | var outerTypeParameters = ty.ContainingType.FullTypeArguments().ToList(); 110 | for (pos--; pos >= 0; pos--) { 111 | typeArgs[pos] = new TypeWithNode(outerTypeParameters[pos], typeSystem.ObliviousNode); 112 | } 113 | return typeArgs; 114 | } else { 115 | return syntacticTypeArgs ?? new TypeWithNode[0]; 116 | } 117 | } 118 | 119 | public override TypeWithNode VisitNullableType(NullableTypeSyntax node) 120 | { 121 | var ty = node.ElementType.Accept(this); 122 | if (ty.Type?.IsValueType == true) { 123 | var typeInfo = semanticModel.GetTypeInfo(node, cancellationToken); 124 | if (typeInfo.Type is INamedTypeSymbol { OriginalDefinition: { SpecialType: SpecialType.System_Nullable_T } } nullableType) { 125 | return new TypeWithNode(nullableType, typeSystem.ObliviousNode, new[] { ty }); 126 | } else { 127 | throw new NotSupportedException("NullableType should resolve to System_Nullable_T"); 128 | } 129 | } else { 130 | // Ignore existing nullable reference types; we'll infer them again from scratch. 131 | return ty; 132 | } 133 | } 134 | 135 | protected abstract NullabilityNode GetMappedNode(TypeSyntax syntax); 136 | 137 | public override TypeWithNode VisitArrayType(ArrayTypeSyntax node) 138 | { 139 | var type = node.ElementType.Accept(this); 140 | // Handle nested arrays 141 | foreach (var rankSpec in node.RankSpecifiers.Reverse()) { 142 | // Trying to insert `?` for nested arrays will be tricky, 143 | // because `int[,][]` with nullable nested arrays has to turn into `int[]?[,]` 144 | // So for now, just handle nested arrays as oblivious. 145 | var arrayType = type.Type != null ? semanticModel.Compilation.CreateArrayTypeSymbol(type.Type, rankSpec.Rank) : null; 146 | type = new TypeWithNode(arrayType, typeSystem.ObliviousNode, new[] { type }); 147 | } 148 | if (CanBeMadeNullableSyntax(node)) 149 | return type.WithNode(GetMappedNode(node)); 150 | else 151 | return type; 152 | } 153 | 154 | public override TypeWithNode VisitPointerType(PointerTypeSyntax node) 155 | { 156 | var typeInfo = semanticModel.GetTypeInfo(node, cancellationToken); 157 | Debug.Assert(typeInfo.Type is IPointerTypeSymbol); 158 | var elementType = node.ElementType.Accept(this); 159 | return new TypeWithNode(typeInfo.Type, typeSystem.ObliviousNode, new[] { elementType }); 160 | } 161 | 162 | public override TypeWithNode VisitTupleType(TupleTypeSyntax node) 163 | { 164 | var elementTypes = node.Elements.Select(e => e.Type.Accept(this)).ToArray(); 165 | var symbolInfo = semanticModel.GetSymbolInfo(node, cancellationToken); 166 | return new TypeWithNode(symbolInfo.Symbol as ITypeSymbol, typeSystem.ObliviousNode, elementTypes); 167 | } 168 | 169 | public override TypeWithNode VisitRefType(RefTypeSyntax node) 170 | { 171 | return node.Type.Accept(this); 172 | } 173 | 174 | public override TypeWithNode VisitOmittedTypeArgument(OmittedTypeArgumentSyntax node) 175 | { 176 | // e.g. in `typeof(List<>)` 177 | return typeSystem.VoidType; 178 | } 179 | 180 | /// 181 | /// Gets whether it is syntactically possible to add a `NullableTypeSyntax` around the given node. 182 | /// 183 | internal static bool CanBeMadeNullableSyntax(TypeSyntax node) 184 | { 185 | if (node is IdentifierNameSyntax { IsVar: true }) 186 | return false; 187 | if (node.Parent is TypeParameterConstraintClauseSyntax constraint && constraint.Name == node) 188 | return false; 189 | switch (node.Parent?.Kind()) { 190 | case SyntaxKind.ExplicitInterfaceSpecifier: // void IDisposable?.Dispose() 191 | case SyntaxKind.TypeOfExpression: // typeof(string?) 192 | case SyntaxKind.IsExpression: // x is string? 193 | case SyntaxKind.ObjectCreationExpression: // new Class?() 194 | case SyntaxKind.ArrayCreationExpression: // new string[]? 195 | case SyntaxKind.QualifiedName: 196 | case SyntaxKind.AliasQualifiedName: 197 | case SyntaxKind.QualifiedCref: // 198 | case SyntaxKind.NameMemberCref: // 199 | case SyntaxKind.TypeCref: 200 | case SyntaxKind.XmlNameAttribute: // 201 | case SyntaxKind.SimpleMemberAccessExpression: // Console?.WriteLine 202 | case SyntaxKind.SimpleBaseType: // : IDisposable? 203 | case SyntaxKind.AsExpression: // x as string? 204 | case SyntaxKind.UsingDirective: // using System?; 205 | case SyntaxKind.DeclarationPattern: // x is string? b 206 | case SyntaxKind.RecursivePattern: // x is string? {} b 207 | case SyntaxKind.TypePattern: // x is string 208 | case SyntaxKind.ConstantPattern: // x is SomeName or SomeType 209 | case SyntaxKind.Argument: // type name can appears as argument in nameof() expression 210 | return false; 211 | } 212 | return true; 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /NullabilityInference.Tests/FlowTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Daniel Grunwald 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | // software and associated documentation files (the "Software"), to deal in the Software 5 | // without restriction, including without limitation the rights to use, copy, modify, merge, 6 | // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 7 | // to whom the Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 13 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 14 | // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 15 | // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | // DEALINGS IN THE SOFTWARE 18 | 19 | using System.Runtime.Remoting.Proxies; 20 | using Xunit; 21 | 22 | namespace ICSharpCode.NullabilityInference.Tests 23 | { 24 | public class FlowTests : NullabilityTestHelper 25 | { 26 | [Fact] 27 | public void ReturnNotNullViaNullableVar() 28 | { 29 | CheckPaths(@" 30 | class Program { 31 | public static string Test() { 32 | string? a = null; 33 | a = string.Empty; 34 | return a; 35 | } 36 | }", returnNullable: false); 37 | } 38 | 39 | [Fact] 40 | public void ReturnInputViaNullableVar() 41 | { 42 | CheckPaths(@" 43 | class Program { 44 | public static string Test(string input) { 45 | string? a = null; 46 | a = input; 47 | return a; 48 | } 49 | }", returnNullable: false, returnDependsOnInput: true); 50 | } 51 | 52 | [Fact] 53 | public void IfFlow1() 54 | { 55 | CheckPaths(@" 56 | class Program { 57 | static bool b; 58 | public static string Test(string input) { 59 | string? a = null; 60 | if (b) { 61 | a = input; 62 | } 63 | return a; 64 | } 65 | }", returnNullable: true); 66 | // Note: the path from parameter to return doesn't appear in the graph, 67 | // because the flow-state on the else-branch is , 68 | // and Join(, x) directly returns without introducing a 69 | // helper node. 70 | } 71 | 72 | [Fact] 73 | public void IfFlow2() 74 | { 75 | CheckPaths(@" 76 | class Program { 77 | static bool b; 78 | public static string Test(string input) { 79 | string? a = null; 80 | if (b) { 81 | a = input; 82 | } else { 83 | a = string.Empty; 84 | } 85 | return a; 86 | } 87 | }", returnNullable: false, returnDependsOnInput: true); 88 | } 89 | 90 | [Fact] 91 | public void IfFlow3() 92 | { 93 | CheckPaths(@" 94 | using System; 95 | class Program { 96 | static bool b; 97 | public static string Test(string input) { 98 | string? a = null; 99 | if (b) { 100 | a = input; 101 | } else { 102 | throw new Exception(); 103 | } 104 | return a; 105 | } 106 | }", returnNullable: false, returnDependsOnInput: true); 107 | } 108 | 109 | 110 | [Fact] 111 | public void GetOrCreateNode() 112 | { 113 | // 'node' local variable must be nullable, but GetNode return type can be non-nullable. 114 | AssertNullabilityInference(@" 115 | using System.Collections.Generic; 116 | class DataStructure 117 | { 118 | class Node { } 119 | 120 | Dictionary mapping = new Dictionary(); 121 | 122 | Node GetNode(string element) 123 | { 124 | Node? node; 125 | if (!mapping.TryGetValue(element, out node)) 126 | { 127 | node = new Node(); 128 | mapping.Add(element, node); 129 | } 130 | return node; 131 | } 132 | 133 | }"); 134 | } 135 | 136 | [Fact] 137 | public void GetOrCreateNodeWithOutVarDecl() 138 | { 139 | // 'node' local variable must be nullable, but GetNode return type can be non-nullable. 140 | AssertNullabilityInference(@" 141 | using System.Collections.Generic; 142 | class DataStructure 143 | { 144 | class Node { } 145 | 146 | Dictionary mapping = new Dictionary(); 147 | 148 | Node GetNode(string element) 149 | { 150 | if (!mapping.TryGetValue(element, out Node? node)) 151 | { 152 | node = new Node(); 153 | mapping.Add(element, node); 154 | } 155 | return node; 156 | } 157 | 158 | }"); 159 | } 160 | 161 | [Fact] 162 | public void ConditionalAnd() 163 | { 164 | AssertNullabilityInference(@" 165 | using System.Collections.Generic; 166 | class DataStructure 167 | { 168 | class Node { } 169 | 170 | Dictionary mapping = new Dictionary(); 171 | 172 | Node GetNode(string element) 173 | { 174 | Node? node; 175 | if (mapping.TryGetValue(element, out node) && IsValid(node)) 176 | { 177 | return node; 178 | } 179 | return new Node(); 180 | } 181 | 182 | bool IsValid(Node? n) => true; 183 | }"); 184 | } 185 | 186 | [Fact] 187 | public void CoalesceAssign() 188 | { 189 | CheckPaths(@" 190 | class Program { 191 | public static string Test(string input) { 192 | string? a = null; 193 | a ??= input; 194 | return a; 195 | } 196 | }", returnNullable: false, returnDependsOnInput: true); 197 | } 198 | 199 | [Fact] 200 | public void InferNotNullWhenTrue() 201 | { 202 | string program = @" 203 | using System.Collections.Generic; 204 | using System.Diagnostics.CodeAnalysis; 205 | class DataStructure 206 | { 207 | Dictionary dict = new Dictionary(); 208 | 209 | public bool TryGetValue(string key, [Attr] out string? val) 210 | { 211 | return dict.TryGetValue(key, out val); 212 | } 213 | }"; 214 | AssertNullabilityInference( 215 | expectedProgram: program.Replace("[Attr]", "[NotNullWhen(true)]"), 216 | inputProgram: program.Replace("[Attr] ", "")); 217 | } 218 | 219 | [Fact] 220 | public void InferNotNullWhenFalse() 221 | { 222 | string program = @" 223 | using System.Collections.Generic; 224 | using System.Diagnostics.CodeAnalysis; 225 | class DataStructure 226 | { 227 | Dictionary dict = new Dictionary(); 228 | 229 | public bool TryGetValue(string key, [Attr] out string? val) 230 | { 231 | return !dict.TryGetValue(key, out val); 232 | } 233 | }"; 234 | AssertNullabilityInference( 235 | expectedProgram: program.Replace("[Attr]", "[NotNullWhen(false)]"), 236 | inputProgram: program.Replace("[Attr] ", "")); 237 | } 238 | 239 | [Fact] 240 | public void InferNotNullWhenTrueFromControlFlow() 241 | { 242 | string program = @" 243 | using System.Collections.Generic; 244 | [using] 245 | class DataStructure 246 | { 247 | public bool TryGet(int i, [Attr] out string? name) 248 | { 249 | if (i > 0) 250 | { 251 | name = string.Empty; 252 | return true; 253 | } 254 | name = null; 255 | return false; 256 | } 257 | }"; 258 | AssertNullabilityInference( 259 | expectedProgram: program.Replace("[Attr]", "[NotNullWhen(true)]").Replace("[using]", "using System.Diagnostics.CodeAnalysis;"), 260 | inputProgram: program.Replace("[Attr] ", "").Replace("[using]", "")); 261 | } 262 | 263 | [Fact] 264 | public void InferNotNullWhenTrueFromComparisonInReturn() 265 | { 266 | string program = @" 267 | using System.Collections.Generic; 268 | using System.Diagnostics.CodeAnalysis; 269 | class DataStructure 270 | { 271 | public bool TryGet(string? input, [Attr] out string? name) 272 | { 273 | name = input; 274 | return name != null; 275 | } 276 | }"; 277 | AssertNullabilityInference( 278 | expectedProgram: program.Replace("[Attr]", "[NotNullWhen(true)]"), 279 | inputProgram: program.Replace("[Attr] ", "")); 280 | } 281 | 282 | [Fact] 283 | public void UseNotNullIfNotNull() 284 | { 285 | string program = @" 286 | using System.Diagnostics.CodeAnalysis; 287 | class Program 288 | { 289 | public void Test() 290 | { 291 | string a = Identitity(string.Empty); 292 | string? b = Identitity(null); 293 | } 294 | 295 | #nullable enable 296 | [return: NotNullIfNotNull(""input"")] 297 | public static string? Identitity(string? input) => input; 298 | }"; 299 | AssertNullabilityInference(program, program); 300 | } 301 | 302 | [Fact] 303 | public void OutIntoVar() 304 | { 305 | string program = @" 306 | using System.Collections.Generic; 307 | using System.Diagnostics.CodeAnalysis; 308 | class DataStructure 309 | { 310 | public bool TryGet(string? input, [Attr] out string? name) 311 | { 312 | name = input; 313 | return name != null; 314 | } 315 | public int Use() 316 | { 317 | var x = string.Empty; 318 | if (TryGet(null, out x)) 319 | { 320 | return x.Length; 321 | } 322 | return 0; 323 | } 324 | }"; 325 | AssertNullabilityInference( 326 | expectedProgram: program.Replace("[Attr]", "[NotNullWhen(true)]"), 327 | inputProgram: program.Replace("[Attr] ", "")); 328 | } 329 | 330 | [Fact] 331 | public void VarReassignment() 332 | { 333 | AssertNullabilityInference(@" 334 | class Program 335 | { 336 | public string? Test() 337 | { 338 | var name = GetString(); 339 | int len = name.Length; 340 | name = null; 341 | return name; 342 | } 343 | string GetString() => string.Empty; 344 | }"); 345 | } 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /NullabilityInference/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Daniel Grunwald 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | // software and associated documentation files (the "Software"), to deal in the Software 5 | // without restriction, including without limitation the rights to use, copy, modify, merge, 6 | // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 7 | // to whom the Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 13 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 14 | // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 15 | // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | // DEALINGS IN THE SOFTWARE. 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | using System.Linq; 22 | using Microsoft.CodeAnalysis; 23 | using Microsoft.CodeAnalysis.CSharp.Syntax; 24 | 25 | namespace ICSharpCode.NullabilityInference 26 | { 27 | internal static class ExtensionMethods 28 | { 29 | public static string StartPosToString(this Location location) 30 | { 31 | var lineSpan = location.GetLineSpan(); 32 | string filename = System.IO.Path.GetFileName(lineSpan.Path); 33 | return $"{filename}:{lineSpan.StartLinePosition.Line + 1}:{lineSpan.StartLinePosition.Character + 1}"; 34 | } 35 | 36 | public static string EndPosToString(this Location location) 37 | { 38 | var lineSpan = location.GetLineSpan(); 39 | string filename = System.IO.Path.GetFileName(lineSpan.Path); 40 | return $"{filename}:{lineSpan.EndLinePosition.Line + 1}:{lineSpan.EndLinePosition.Character + 1}"; 41 | } 42 | 43 | public static void Deconstruct(this KeyValuePair pair, out K key, out V value) 44 | { 45 | key = pair.Key; 46 | value = pair.Value; 47 | } 48 | 49 | public static IEnumerable<(A, B)> Zip(this IEnumerable input1, IEnumerable input2) 50 | { 51 | return input1.Zip(input2, (a, b) => (a, b)); 52 | } 53 | 54 | public static bool IsAutoProperty(this PropertyDeclarationSyntax syntax) 55 | { 56 | if (syntax.ExpressionBody != null || syntax.AccessorList == null || !syntax.AccessorList.Accessors.Any()) 57 | return false; 58 | foreach (var accessor in syntax.AccessorList.Accessors) { 59 | if (accessor.Body != null || accessor.ExpressionBody != null) 60 | return false; 61 | } 62 | return true; 63 | } 64 | 65 | public static VarianceKind Combine(this (VarianceKind, VarianceKind) variancePair) 66 | { 67 | return variancePair switch 68 | { 69 | (VarianceKind.None, _) => VarianceKind.None, 70 | (_, VarianceKind.None) => VarianceKind.None, 71 | (VarianceKind.Out, VarianceKind.Out) => VarianceKind.Out, 72 | (VarianceKind.In, VarianceKind.Out) => VarianceKind.In, 73 | (VarianceKind.Out, VarianceKind.In) => VarianceKind.In, 74 | (VarianceKind.In, VarianceKind.In) => VarianceKind.Out, 75 | _ => throw new NotSupportedException("Unknown VarianceKind") 76 | }; 77 | } 78 | 79 | public static VarianceKind ToVariance(this RefKind refKind) 80 | { 81 | return refKind switch 82 | { 83 | RefKind.None => VarianceKind.In, 84 | RefKind.In => VarianceKind.In, 85 | RefKind.Ref => VarianceKind.None, 86 | RefKind.Out => VarianceKind.Out, 87 | _ => throw new NotSupportedException($"RefKind unsupported: {refKind}") 88 | }; 89 | } 90 | 91 | public static string GetFullName(this ISymbol symbol) 92 | { 93 | if (symbol.ContainingType != null) 94 | return symbol.ContainingType.GetFullName() + "." + symbol.Name; 95 | else if (symbol.ContainingNamespace is { IsGlobalNamespace: false }) 96 | return symbol.ContainingNamespace.GetFullName() + "." + symbol.Name; 97 | else 98 | return symbol.Name; 99 | } 100 | 101 | 102 | /// 103 | /// Gets the full arity of the type symbol, including the number of type parameters inherited from outer classes. 104 | /// 105 | public static int FullArity(this ITypeSymbol? type) 106 | { 107 | if (type is INamedTypeSymbol nt) { 108 | int arity = nt.ContainingType.FullArity(); 109 | if (type.IsAnonymousType) { 110 | arity += nt.GetMembers().OfType().Count(); 111 | } else { 112 | arity += nt.Arity; 113 | } 114 | return arity; 115 | } else if (type is IArrayTypeSymbol || type is IPointerTypeSymbol) { 116 | return 1; 117 | } else { 118 | return 0; 119 | } 120 | } 121 | 122 | /// 123 | /// Gets the full arity of the method, including the number of type parameters inherited from outer methods. 124 | /// 125 | public static int FullArity(this IMethodSymbol method) 126 | { 127 | if (method.ContainingSymbol is IMethodSymbol outerMethod) 128 | return outerMethod.FullArity() + method.Arity; 129 | else 130 | return method.Arity; 131 | } 132 | 133 | /// 134 | /// Gets the full list of type arguments for the type symbol, including the number of type arguments inherited from outer classes. 135 | /// 136 | public static IEnumerable FullTypeArguments(this INamedTypeSymbol type) 137 | { 138 | if (type.ContainingType != null) { 139 | foreach (var inheritedTypeArg in type.ContainingType.FullTypeArguments()) 140 | yield return inheritedTypeArg; 141 | } 142 | if (type.IsAnonymousType) { 143 | // For anonymous types, we act as if the member types are all type arguments. 144 | // This lets us track the nullability of indiviual anonymous type members. 145 | foreach (var member in type.GetMembers()) { 146 | if (member is IPropertySymbol prop) 147 | yield return prop.Type; 148 | } 149 | } else { 150 | foreach (var typeArg in type.TypeArguments) 151 | yield return typeArg; 152 | } 153 | } 154 | 155 | /// 156 | /// Gets the full list of type arguments for the type symbol, including the number of type arguments inherited from outer methods. 157 | /// 158 | public static IEnumerable FullTypeArguments(this IMethodSymbol method) 159 | { 160 | if (method.ContainingSymbol is IMethodSymbol outerMethod) { 161 | foreach (var outerTypeArg in outerMethod.FullTypeArguments()) 162 | yield return outerTypeArg; 163 | } 164 | foreach (var typeArg in method.TypeArguments) 165 | yield return typeArg; 166 | } 167 | 168 | /// 169 | /// Gets the full list of type arguments for the type symbol, including the number of type arguments inherited from outer classes. 170 | /// 171 | public static IEnumerable FullTypeArgumentNullableAnnotations(this INamedTypeSymbol type) 172 | { 173 | if (type.ContainingType != null) { 174 | foreach (var inheritedTypeArg in type.ContainingType.FullTypeArgumentNullableAnnotations()) 175 | yield return inheritedTypeArg; 176 | } 177 | if (type.IsAnonymousType) { 178 | foreach (var member in type.GetMembers()) { 179 | if (member is IPropertySymbol) 180 | yield return NullableAnnotation.None; 181 | } 182 | } else { 183 | foreach (var annotation in type.TypeArgumentNullableAnnotations) 184 | yield return annotation; 185 | } 186 | } 187 | 188 | public static IEnumerable FullTypeParameters(this INamedTypeSymbol type) 189 | { 190 | if (type.ContainingType != null) { 191 | foreach (var inheritedTypeParam in type.ContainingType.FullTypeParameters()) 192 | yield return inheritedTypeParam; 193 | } 194 | if (type.IsAnonymousType) { 195 | foreach (var member in type.GetMembers()) { 196 | if (member is IPropertySymbol) { 197 | yield return new SimpleTypeParameter(); 198 | } 199 | } 200 | } else { 201 | foreach (var tp in type.TypeParameters) 202 | yield return new SimpleTypeParameter(tp); 203 | } 204 | } 205 | 206 | public static IEnumerable FullTypeParameters(this IMethodSymbol method) 207 | { 208 | if (method.ContainingSymbol is IMethodSymbol outerMethod) { 209 | foreach (var tp in outerMethod.FullTypeParameters()) 210 | yield return tp; 211 | } 212 | foreach (var tp in method.TypeParameters) 213 | yield return tp; 214 | } 215 | 216 | /// 217 | /// Gets the index of the type parameter in its parent type's FullTypeParameters() 218 | /// 219 | public static int FullOrdinal(this ITypeParameterSymbol tp) 220 | { 221 | if (tp.TypeParameterKind == TypeParameterKind.Type) { 222 | return tp.Ordinal + (tp.ContainingType?.ContainingType.FullArity() ?? 0); 223 | } else { 224 | if (tp.ContainingSymbol?.ContainingSymbol is IMethodSymbol outerMethod) { 225 | return tp.Ordinal + outerMethod.FullArity(); 226 | } else { 227 | return tp.Ordinal; 228 | } 229 | } 230 | } 231 | 232 | public static bool CanBeMadeNullable(this ITypeSymbol type) 233 | { 234 | if (type is ITypeParameterSymbol tp) { 235 | // Type parameters can be reference types without having the ": class" constraint, 236 | // e.g. when there's a "T : BaseClass" constraint. 237 | // However the "T?" syntax requires an actual "T:class" constraint. 238 | if (!tp.HasReferenceTypeConstraint) 239 | return false; 240 | if (tp.ReferenceTypeConstraintNullableAnnotation == NullableAnnotation.Annotated) { 241 | // we can't use "T?" if T itself might already be a nullable reference type 242 | return false; 243 | } 244 | // Moreover, this constraint must be syntactic, it is not sufficient if inherited from 245 | // an overridden method. 246 | if (tp.TypeParameterKind == TypeParameterKind.Method) { 247 | var method = (IMethodSymbol)tp.ContainingSymbol; 248 | if (method.IsOverride) 249 | return false; 250 | } 251 | return true; 252 | } 253 | return type.IsReferenceType; 254 | } 255 | 256 | public static bool IsSystemNullable(this ITypeSymbol? type) 257 | { 258 | return type?.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T; 259 | } 260 | 261 | /// 262 | /// Gets the return type used for "return" statements within the method. 263 | /// 264 | public static ITypeSymbol EffectiveReturnType(this IMethodSymbol method) 265 | { 266 | // See also: ExtractTaskReturnType() 267 | var returnType = method.ReturnType; 268 | if (method.IsAsync && returnType is INamedTypeSymbol namedType && namedType.TypeArguments.Length == 1) { 269 | returnType = namedType.TypeArguments.Single(); 270 | } 271 | return returnType; 272 | } 273 | 274 | public static bool HasDoesNotReturnAttribute(this IMethodSymbol method) 275 | { 276 | foreach (var attr in method.GetAttributes()) { 277 | if (attr.ConstructorArguments.Length == 1 && attr.AttributeClass?.GetFullName() == "System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute") { 278 | return true; 279 | } 280 | } 281 | return false; 282 | } 283 | 284 | public static bool HasDoesNotReturnIfAttribute(this IParameterSymbol param, out bool parameterValue) 285 | { 286 | foreach (var attr in param.GetAttributes()) { 287 | if (attr.ConstructorArguments.Length == 1 && attr.AttributeClass?.GetFullName() == "System.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute") { 288 | if (attr.ConstructorArguments.Single().Value is bool val) { 289 | parameterValue = val; 290 | return true; 291 | } 292 | } 293 | } 294 | parameterValue = default; 295 | return false; 296 | } 297 | 298 | /// 299 | /// Remove item at index index in O(1) by swapping it with the last element in the collection before removing it. 300 | /// Useful when the order of elements in the list is not relevant. 301 | /// 302 | public static void SwapRemoveAt(this List list, int index) 303 | { 304 | int removeIndex = list.Count - 1; 305 | list[index] = list[removeIndex]; 306 | list.RemoveAt(removeIndex); 307 | } 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /InferNull/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Daniel Grunwald 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | // software and associated documentation files (the "Software"), to deal in the Software 5 | // without restriction, including without limitation the rights to use, copy, modify, merge, 6 | // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 7 | // to whom the Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 13 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 14 | // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 15 | // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | // DEALINGS IN THE SOFTWARE. 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | using System.Collections.Immutable; 22 | using System.ComponentModel.DataAnnotations; 23 | using System.Diagnostics; 24 | using System.IO; 25 | using System.Linq; 26 | using System.Reflection; 27 | using System.Text.RegularExpressions; 28 | using System.Threading; 29 | using System.Threading.Tasks; 30 | using ICSharpCode.NullabilityInference; 31 | using InferNull.FromRoslynSdk; 32 | using McMaster.Extensions.CommandLineUtils; 33 | using Microsoft.Build.Locator; 34 | using Microsoft.CodeAnalysis; 35 | using Microsoft.CodeAnalysis.CSharp; 36 | using Microsoft.CodeAnalysis.Host; 37 | using Microsoft.CodeAnalysis.Host.Mef; 38 | using Microsoft.CodeAnalysis.MSBuild; 39 | using Microsoft.VisualStudio.Composition; 40 | 41 | namespace InferNull 42 | { 43 | [Command(Name = "infernull", Description = "Tool for inferring C# 8 nullable reference types for existing C# code bases", 44 | ExtendedHelpText = @" 45 | Remarks: 46 | Adds nullability annotations to a single project (.csproj). 47 | Please backup / commit your files to source control before use. 48 | We recommend running the conversion in-place (i.e. not specifying an output directory) for best performance. 49 | See https://github.com/icsharpcode/NullabilityInference for the source code, issues and other info. 50 | ")] 51 | [HelpOption("-h|--help")] 52 | internal class Program 53 | { 54 | public static Task Main(string[] args) => CommandLineApplication.ExecuteAsync(args); 55 | 56 | [FileExists] 57 | [Required] 58 | [Argument(0, "Project file name", "The project (.csproj file) for which to infer nullability. This argument is mandatory.")] 59 | public string ProjectName { get; } = string.Empty; 60 | 61 | [Option("-n|--dry-run", "Do not write result back to disk.", CommandOptionType.NoValue)] 62 | public bool DryRun { get; } 63 | 64 | [Option("-f|--force", "Allow overwriting uncommitted changes", CommandOptionType.NoValue)] 65 | public bool Force { get; } 66 | 67 | [Option("--all-nullable", "Don't run inference, just mark all reference types as nullable.", CommandOptionType.NoValue)] 68 | public bool AllNullable { get; } 69 | 70 | [Option("-e|--add-nullable-enable", "Add '#nullable enable' to the top of each source file.", CommandOptionType.NoValue)] 71 | public bool AddNullableEnable { get; } 72 | 73 | [Option("-s|--strategy", "Select how conflicts are resolved when a node both could be assigned null, but is also used where a non-nullable value is required. (-s:MinimizeWarnings, -s:PreferNull, -s:PreferNotNull). " 74 | + "The default is MinimizeWarnings.", CommandOptionType.SingleValue)] 75 | public ConflictResolutionStrategy Strategy { get; } = ConflictResolutionStrategy.MinimizeWarnings; 76 | 77 | #if DEBUG 78 | [Option("-g|--show-graph", "Show type graph. Requires GraphViz dot.exe in PATH.", CommandOptionType.NoValue)] 79 | public bool ShowGraph { get; } 80 | 81 | [Option("--filter-graph", "Apply filter, showing only a portion of the graph.", CommandOptionType.MultipleValue)] 82 | public List FilterGraph { get; } = new List(); 83 | 84 | [Option("--export-graph", "Save type graph to file.", CommandOptionType.SingleValue)] 85 | public string? ExportGraph { get; } = null; 86 | #endif 87 | 88 | /// Used by reflection in CommandLineApplication.ExecuteAsync 89 | private async Task OnExecuteAsync(CommandLineApplication _) 90 | { 91 | var outputDirectory = new DirectoryInfo(Path.GetDirectoryName(ProjectName)); 92 | if (await CouldOverwriteUncommittedFilesAsync(outputDirectory)) { 93 | await Console.Error.WriteLineAsync($"WARNING: There are files in {outputDirectory.FullName} which may be overwritten, and aren't committed to git"); 94 | if (Force) { 95 | await Console.Error.WriteLineAsync("Continuing with possibility of data loss due to force option."); 96 | } else { 97 | await Console.Error.WriteLineAsync("Aborting to avoid data loss (see above warning). Commit the files to git, or use the --force option to override this check."); 98 | return 1; 99 | } 100 | } 101 | 102 | var buildProps = new Dictionary(StringComparer.OrdinalIgnoreCase) { 103 | ["Configuration"] = "Debug", 104 | ["Platform"] = "AnyCPU" 105 | }; 106 | 107 | var cancellationToken = CancellationToken.None; 108 | using var workspace = await CreateWorkspaceAsync(buildProps); 109 | await Console.Error.WriteLineAsync("Loading project..."); 110 | Project project; 111 | try { 112 | project = await workspace.OpenProjectAsync(ProjectName, cancellationToken: cancellationToken); 113 | } catch (Exception ex) { 114 | await Console.Error.WriteLineAsync(ex.ToString()); 115 | return 1; 116 | } 117 | await Console.Error.WriteLineAsync("Compiling..."); 118 | var compilation = await project.GetCompilationAsync(cancellationToken) as CSharpCompilation; 119 | if (compilation == null) { 120 | await Console.Error.WriteLineAsync("project.GetCompilationAsync() did not return CSharpCompilation"); 121 | return 1; 122 | } 123 | compilation = AllNullableSyntaxRewriter.MakeAllReferenceTypesNullable(compilation, cancellationToken); 124 | if (AllNullable) { 125 | await Console.Error.WriteLineAsync("Writing modified code..."); 126 | foreach (var tree in compilation.SyntaxTrees) { 127 | WriteTree(tree, cancellationToken); 128 | } 129 | return 0; 130 | } 131 | bool hasErrors = false; 132 | foreach (var diag in compilation.GetDiagnostics(cancellationToken)) { 133 | if (diag.Severity == DiagnosticSeverity.Error) { 134 | await Console.Error.WriteLineAsync(diag.ToString()); 135 | hasErrors = true; 136 | } 137 | } 138 | if (hasErrors) { 139 | await Console.Error.WriteLineAsync("Compilation failed. Cannot infer nullability."); 140 | return 1; 141 | } 142 | await Console.Error.WriteLineAsync("Inferring nullabilities..."); 143 | var engine = new NullCheckingEngine(compilation); 144 | engine.Analyze(this.Strategy, cancellationToken); 145 | #if DEBUG 146 | GraphVizGraph? exportedGraph = null; 147 | if (ShowGraph) { 148 | await Console.Error.WriteLineAsync("Showing graph..."); 149 | exportedGraph ??= ExportTypeGraph(engine); 150 | exportedGraph.Show(); 151 | } 152 | if (ExportGraph != null) { 153 | await Console.Error.WriteLineAsync("Exporting graph..."); 154 | exportedGraph ??= ExportTypeGraph(engine); 155 | exportedGraph.Save(ExportGraph); 156 | } 157 | #endif 158 | Statistics stats; 159 | if (DryRun) { 160 | await Console.Error.WriteLineAsync("Computing statistics..."); 161 | stats = engine.ConvertSyntaxTrees(cancellationToken, tree => { }); 162 | await Console.Error.WriteLineAsync("Analysis successful. Results are discarded due to --dry-run."); 163 | await Console.Error.WriteLineAsync("Would use:"); 164 | } else { 165 | await Console.Error.WriteLineAsync("Writing modified code..."); 166 | stats = engine.ConvertSyntaxTrees(cancellationToken, tree => WriteTree(tree, cancellationToken)); 167 | await Console.Error.WriteLineAsync("Success!"); 168 | await Console.Error.WriteLineAsync("Used:"); 169 | } 170 | await Console.Error.WriteLineAsync($" {stats.NullableCount} nullable reference types."); 171 | await Console.Error.WriteLineAsync($" {stats.NonNullCount} non-nullable reference types."); 172 | await Console.Error.WriteLineAsync($" {stats.NotNullWhenCount} [NotNullWhen] attributes."); 173 | 174 | return 0; 175 | } 176 | 177 | #if DEBUG 178 | private GraphVizGraph ExportTypeGraph(NullCheckingEngine engine) 179 | { 180 | if (FilterGraph.Count == 0) { 181 | // Show complete graph 182 | return engine.ExportTypeGraph(); 183 | } else { 184 | // Show filtered graph 185 | var list = new List<(string file, int start, int end)>(); 186 | foreach (var entry in FilterGraph) { 187 | var m = Regex.Match(entry, @"^([\w_.-]+):(\d+)-(\d+)$"); 188 | if (!m.Success) { 189 | Console.WriteLine("Invalid value for --show-graph/--export-graph. Expected filename.cs:100-200"); 190 | } 191 | list.Add((m.Groups[1].Value, int.Parse(m.Groups[2].Value), int.Parse(m.Groups[3].Value))); 192 | } 193 | return engine.ExportTypeGraph(location => list.Any(e => MatchesEntry(e, location))); 194 | 195 | static bool MatchesEntry((string file, int start, int end) entry, Location loc) 196 | { 197 | var span = loc.GetLineSpan(); 198 | if (span.EndLinePosition.Line + 1 < entry.start) 199 | return false; 200 | if (entry.end < span.StartLinePosition.Line + 1) 201 | return false; 202 | return string.Equals(Path.GetFileName(span.Path), entry.file, StringComparison.OrdinalIgnoreCase); 203 | } 204 | } 205 | } 206 | #endif 207 | 208 | private void WriteTree(SyntaxTree tree, CancellationToken cancellationToken) 209 | { 210 | if (string.IsNullOrEmpty(tree.FilePath)) 211 | return; 212 | if (AddNullableEnable) { 213 | var root = tree.GetRoot(cancellationToken); 214 | if (!root.GetLeadingTrivia().Any(trivia => trivia.IsKind(SyntaxKind.NullableDirectiveTrivia))) { 215 | var newDirective = SyntaxFactory.Trivia( 216 | SyntaxFactory.NullableDirectiveTrivia( 217 | settingToken: SyntaxFactory.Token(SyntaxKind.EnableKeyword).WithLeadingTrivia(SyntaxFactory.Whitespace(" ")), 218 | isActive: true 219 | ).WithTrailingTrivia(SyntaxFactory.EndOfLine(Environment.NewLine)) 220 | ); 221 | root = root.WithLeadingTrivia(new[] { newDirective }.Concat(root.GetLeadingTrivia())); 222 | tree = tree.WithRootAndOptions(root, tree.Options); 223 | } 224 | } 225 | using var stream = new FileStream(tree.FilePath, FileMode.Create, FileAccess.Write); 226 | using var writer = new StreamWriter(stream, tree.Encoding); 227 | writer.Write(tree.GetText(cancellationToken)); 228 | } 229 | 230 | private static async Task CreateWorkspaceAsync(Dictionary buildProps) 231 | { 232 | if (MSBuildLocator.CanRegister) { 233 | var instances = MSBuildLocator.QueryVisualStudioInstances().ToArray(); 234 | var instance = instances.OrderByDescending(x => x.Version).FirstOrDefault() 235 | ?? throw new ValidationException("No Visual Studio instance available"); 236 | MSBuildLocator.RegisterInstance(instance); 237 | AppDomain.CurrentDomain.UseVersionAgnosticAssemblyResolution(); 238 | } 239 | 240 | var hostServices = await CreateHostServicesAsync(MSBuildMefHostServices.DefaultAssemblies); 241 | return MSBuildWorkspace.Create(buildProps, hostServices); 242 | } 243 | 244 | private static async Task CouldOverwriteUncommittedFilesAsync(DirectoryInfo outputDirectory) 245 | { 246 | if (!await IsInsideGitWorkTreeAsync(outputDirectory)) 247 | return true; // unversioned files might be overwritten 248 | return !await IsGitDiffEmptyAsync(outputDirectory); 249 | } 250 | 251 | private static async Task IsInsideGitWorkTreeAsync(DirectoryInfo outputDirectory) 252 | { 253 | var gitDiff = new ProcessStartInfo("git") { 254 | Arguments = ArgumentEscaper.EscapeAndConcatenate(new[] { "rev-parse", "--is-inside-work-tree" }), 255 | WorkingDirectory = outputDirectory.FullName 256 | }; 257 | 258 | using var stdout = new StringWriter(); 259 | int exitCode = await gitDiff.GetExitCodeAsync(stdout, stdErr: TextWriter.Null); 260 | return exitCode == 0; 261 | } 262 | 263 | private static async Task IsGitDiffEmptyAsync(DirectoryInfo outputDirectory) 264 | { 265 | var gitDiff = new ProcessStartInfo("git") { 266 | Arguments = ArgumentEscaper.EscapeAndConcatenate(new[] { "diff", "--exit-code", "--relative", "--summary", "--diff-filter=ACMRTUXB*" }), 267 | WorkingDirectory = outputDirectory.FullName 268 | }; 269 | 270 | using var stdout = new StringWriter(); 271 | int exitCode = await gitDiff.GetExitCodeAsync(stdout); 272 | if (exitCode == 1) Console.WriteLine(stdout.ToString()); 273 | return exitCode == 0; 274 | } 275 | 276 | 277 | /// 278 | /// Use this in all workspace creation 279 | /// 280 | private static async Task CreateHostServicesAsync(ImmutableArray assemblies) 281 | { 282 | var exportProvider = await CreateExportProviderFactoryAsync(assemblies); 283 | return MefHostServices.Create(exportProvider.CreateExportProvider().AsCompositionContext()); 284 | } 285 | 286 | private static async Task CreateExportProviderFactoryAsync(ImmutableArray assemblies) 287 | { 288 | var discovery = new AttributedPartDiscovery(Resolver.DefaultInstance, isNonPublicSupported: true); 289 | var parts = await discovery.CreatePartsAsync(assemblies); 290 | var catalog = ComposableCatalog.Create(Resolver.DefaultInstance).AddParts(parts); 291 | 292 | var configuration = CompositionConfiguration.Create(catalog); 293 | var runtimeComposition = RuntimeComposition.CreateRuntimeComposition(configuration); 294 | return runtimeComposition.CreateExportProviderFactory(); 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # C# 8 nullability inference 2 | 3 | This is a prototype for an algorithm that modifies C# code in order to minimize the number of warnings caused by enabling C# 8.0 nullable reference types. 4 | If this ever gets out of the prototype stage, this might be a useful tool when migrate existing C# code to C# 8.0. 5 | 6 | Note: this is a work in progress. Many C# constructs will trigger a NotImplementedException. 7 | 8 | ## Usage 9 | * Update your project to use C# 8.0: `8.0` 10 | * Enable nullable reference types: `enable` 11 | * If possible, update referenced libraries to newer versions that have nullability annotations. 12 | * Compile the project and notice that you get a huge bunch of nullability warnings. 13 | * Run `InferNull myproject.csproj`. This modifies your code by inserting `?` in various places. 14 | * Compile your project again. You should get a smaller (and hopefully manageable) number of nullability warnings. 15 | 16 | ## Tips+Tricks: 17 | 18 | * The inference tool will only add/remove `?` annotations on nullable reference types. It can also add the `[NotNullWhen]` attribute. It will never touch your code in any other way. 19 | * Existing `?` annotations on nullable reference types are discarded and inferred again from scratch. 20 | * Unconstrained generic types are not reference types, and thus will never be annotated by the tool. 21 | * The inference tool will not introduce any of the [advanced nullability attributes](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis). 22 | * However, if these attributes are used in the input code, the tool will in some cases use them for better inference results. 23 | * It can be useful to annotate generic code with these attributes before running the inference tool. 24 | * The inference tool acts on one project (`.csproj`) at a time. For best results, any referenced assemblies should already use nullability annotations. 25 | * If using the tool on multiple projects; apply the tool in the build order. 26 | * For the .NET base class library, use .NET Core 3 (or later), or use [ReferenceAssemblyAnnotator](https://github.com/tunnelvisionlabs/ReferenceAssemblyAnnotator). 27 | * For third-party libraries, consider upgrading to a newer version of the library if that adds nullability annotations. 28 | * You can use `#nullable enable` to mark code that you have finished manually reviewing. 29 | * The tool will never touch any code after `#nullable enable` or `#nullable disable`. It only modifies code prior to those directives and code after `#nullable restore`. 30 | * You can use this to add nullability annotations to your project file-by-file: 31 | * Don't use `enable` on the project level 32 | * Use the `InferNull --add-nullable-enable` command-line option to let the inference tool add the directive to all files 33 | * Use git to revert all changes made by the tool except those to a subset of the files. 34 | * Make code changes to that subset of files to fix the remaining warnings. 35 | * Commit, then later re-run `InferNull --add-nullable-enable` to work on the next batch of files. 36 | 37 | ## The algorithm 38 | 39 | Let's start with a simple example: 40 | 41 | ```csharp 42 | 1: class C 43 | 2: { 44 | 3: string key; // #1 45 | 4: string value; // #2 46 | 5: 47 | 6: public C(string key, string value) // key#3, value#4 48 | 7: { 49 | 8: this.key = key; 50 | 9: this.value = value; 51 | 10: } 52 | 11: 53 | 12: public override int GetHashCode() 54 | 13: { 55 | 14: return key.GetHashCode(); 56 | 15: } 57 | 16: 58 | 17: public static int Main() 59 | 18: { 60 | 19: C c = new C("abc", null); // #5 61 | 20: return c.GetHashCode(); 62 | 21: } 63 | 22: } 64 | ``` 65 | 66 | We will construct a global "nullability flow graph". 67 | For each appearance of a reference type in the source code that could be made nullable, we create a node in the graph. 68 | If there's an assignment `a = b`, we create an edge from `b`'s type to `a`'s type. 69 | If there's an assignment `b = null`, we create an edge from a special `nullable` node to `b`'s type. 70 | On a dereference `a.M();`, we create an edge from `a`'s type to a special `nonnull` node (unless the dereference is protected by `if (a != null)`). 71 | 72 | 73 | 74 | Clearly, everything reachable from the `nullable` node should be marked as nullable. 75 | Similarly, everything that can reach the `nonnull` node should be marked as non-nullable. 76 | 77 | Thus, in the example, `key` is inferred to be non-nullable, while `value` is inferred to be nullable. 78 | 79 | ## Implementation Overview 80 | 81 | Nullability inference works essentially in these steps: 82 | 83 | 1. Initially, modify the program to mark every reference type as nullable. (`AllNullableSyntaxRewriter`) 84 | 2. Create nodes for the nullability flow graph. (`NodeBuildingSyntaxVisitor`) 85 | 3. Create edges for the nullability flow graph. (`EdgeBuildingSyntaxVisitor` + `EdgeBuildingOperationVisitor`) 86 | 4. Assign nullabilities to nodes in the graph. (`NullCheckingEngine`) 87 | 5. Modify the program to mark reference types with the inferred nullabilities. (`InferredNullabilitySyntaxRewriter`) 88 | 89 | ### The nullability graph 90 | 91 | The fundamental idea is to do something similar to C#'s nullability checks. 92 | The C# compiler deals with types annotated with concrete nullabilities and emits a warning when a nullable type is used where a non-nullable type is expected. 93 | The `EdgeBuildingOperationVisitor` instead annotates types with nullability nodes, and creates an edge when node#1 is used where node#2 is expected. 94 | 95 | While in simple examples the resulting graphs can look like data flow graphs, that's not always an accurate view. 96 | An edge from node#1 to node#2 really only represents a constraint "if node#1 is nullable, then node#2 must also be nullable". 97 | 98 | To build this graph, the `EdgeBuildingOperationVisitor` assign a `TypeWithNode` to every expression in the program. 99 | For example, the field access `this.key` has the type-with-node `string#1`, where `#1` is the node that was constructed for the declaration of the `key` field. 100 | The `TypeWithNode` can also represent generic types like `IEnumerable#x`. With generics, there's a top-level node `#x` for the generic type, but there's also a 101 | separate node for each type argument. 102 | 103 | ### Minimizing the number of compiler warnings 104 | 105 | If the graph contains a path from the `nullable` node to the `nonnull` node, we will be unable to create nullability annotations that allow compiling the code without warning: 106 | no matter how we assign nullabilities to nodes along the path, there will be at least one edge where a nullable node points to a non-nullable node. 107 | This violates the constraint represented by the edge, and thus causes a compiler warning. 108 | 109 | If we cannot assign nullabilities perfectly (without causing any compiler warnings), we would like to minimize the number of warnings instead. 110 | We do this by using the Ford-Fulkerson algorithm to compute the minimum cut (=minimum set of edges to be removed from the graph) so 111 | that the `nonnull` node is no longer reachable from the `nullable` node. 112 | This separates the graph into essentially three parts: 113 | * nodes reachable from `nullable` --> must be made nullable 114 | * nodes that reach `nonnull` --> must not be made nullable 115 | * remaining nodes --> either choice would work 116 | 117 | The removed edges correspond to the constraints that will produce warnings after we insert `?` for the types inferred as nullable. 118 | Thus the minimum cut ends up finding a solution that minimizes the number of constraints violated. If the constraints represented in our graph accurately model the 119 | C# compiler, this minimizes the number of compiler warnings. 120 | 121 | For the remaining nodes where either choice would work, we mark all nodes occurring in "input positions" (e.g. parameters) as nullable. 122 | Then we propagate this nullability along the outgoing edges. 123 | Any nodes that still remain indeterminate after that, are marked as non-nullable. 124 | 125 | ## More Examples 126 | 127 | ### if (x != null) 128 | 129 | Consider this program: 130 | 131 | ```csharp 132 | 1: class Program 133 | 2: { 134 | 3: public static int Test(string input) // input#1 135 | 4: { 136 | 5: if (input == null) 137 | 6: { 138 | 7: return -1; 139 | 8: } 140 | 9: return input.Length; 141 | 10: } 142 | 11: } 143 | ``` 144 | 145 | `input` has the type-with-node `string#1`. A member access like `.Length` normally causes us to generate an edge to the special `nonnull` node, to encode that the 146 | C# compiler will emit a "Dereference of a possibly null reference." warning. 147 | However, in this example the static type-based view is not appropriate: the C# compiler performs control flow analysis, and notices that `input` cannot be 148 | null at the dereference due to the null test earlier. 149 | 150 | So for this example, we must not generate any edges, so that the `input` parameter can be made nullable. 151 | Instead of re-implementing the whole C# nullability analysis, we solve this problem by simply asking Microsoft.CodeAnalysis for the `NullableFlowState` 152 | of the expression we are analyzing. This works because prior to our analysis, we used the `AllNullableSyntaxRewriter` to mark everything as nullable -- 153 | if despite that the C# compiler still thinks something is non-nullable, it must be protected by a null check. 154 | 155 | For the use of `input` in line 9, it has `NullableFlowState.NotNull`, so we represent its type-with-node as `string#nonnull` instead of `string#1`. 156 | This way the dereference due to the `.Length` member access creates a harmless edge `nonnull`->`nonnull`. This edge is then discarded because it is not a useful constraint. 157 | Thus this method does not result in any edges being added to the graph. Without any edge constraining `input`, it will be inferred as nullable due to occurring in input position. 158 | 159 | ### Generic method invocations 160 | 161 | ```csharp 162 | 1: class Program 163 | 2: { 164 | 3: public static void Main() 165 | 4: { 166 | 5: string n = null; // n#1 167 | 6: string a = Identity(n); // a#3, type argument is #2 168 | 7: string b = Identity("abc"); // b#5, type argument is #4 169 | 8: } 170 | 9: public static T Identity(T input) => input; 171 | 10: } 172 | ``` 173 | 174 | With generic methods, we do not create nodes for the type `T`, as that cannot be marked nullable without additional constraints 175 | ("CS8627: A nullable type parameter must be known to be a value type or non-nullable reference type. Consider adding a 'class', 'struct', or type constraint."). 176 | Instead, any occurrences of `T` in the method signature are replaced with the type-with-node of the type arguments used to call the method. 177 | Thus, the example above results in the following graph: 178 | 179 | 180 | 181 | Thus, `n#1`, the type argument `#2` and `a#3` are all marked as nullable. But `b#5` and the type argument `#4` can remain non-nullable. 182 | 183 | If the type arguments are not explicitly specified but inferred by the compiler, nullability inference will create additional "helper nodes" for the graph 184 | that are not associated with any syntax. This allows us to construct the edges for the calls in the same way. 185 | 186 | ### Generic Types 187 | 188 | ```csharp 189 | 1: using System.Collections.Generic; 190 | 2: class Program 191 | 3: { 192 | 4: List list = new List(); 193 | 5: 194 | 6: public void Add(string name) => list.Add(name); 195 | 7: public string Get(int i) => list[i]; 196 | 8: } 197 | ``` 198 | 199 | 200 | 201 | In this graph, you can see how generic types are handled: 202 | The type of the `list` field generates two nodes: 203 | * `list#3` represents the nullability of the list itself. 204 | * `list!0#2` represents the nullability of the strings within the list. 205 | Similarly, `new!0#1` represents the nullability of the string type argument in the `new List` expression. 206 | Because the type parameter of `List` is invariant, the field initialization in line 4 creates a pair of edges (in both directions) 207 | between the `new!0#1` and `list!0#2` nodes. This forces both type arguments to have the same nullability. 208 | 209 | The resulting graph expresses that the nullability of the return type of `Get` (represented by `Get#5`) depends on the nullability 210 | of the `name` parameter in the `Add` method (node `name#4`). 211 | Whether these types will be inferred as nullable or non-nullable will depend on whether the remainder of the program passes a nullable type to `Add`, 212 | and on the existance of code that uses the return value of `Get` without null checks. 213 | 214 | ### Flow-analysis 215 | 216 | ```csharp 217 | 01: using System.Collections.Generic; 218 | 02: 219 | 03: class Program 220 | 04: { 221 | 05: public string someString = "hello"; 222 | 06: 223 | 07: public bool TryGet(int i, out string name) 224 | 08: { 225 | 09: if (i > 0) 226 | 10: { 227 | 11: name = someString; 228 | 12: return true; 229 | 13: } 230 | 14: name = null; 231 | 15: return false; 232 | 16: } 233 | 17: 234 | 18: public int Use(int i) 235 | 19: { 236 | 20: if (TryGet(i, out string x)) 237 | 21: { 238 | 22: return x.Length; 239 | 23: } 240 | 24: else 241 | 25: { 242 | 26: return 0; 243 | 27: } 244 | 28: } 245 | 29: } 246 | ``` 247 | 248 | The `TryGet` function involves a common C# code pattern: the nullability of an `out` parameter depends on the boolean return value. 249 | If the function returns true, callers can assume the out variable was assigned a non-null value. 250 | But if the function returns false, the value might be null. 251 | 252 | Using our [own flow-analysis](https://github.com/icsharpcode/NullabilityInference/issues/5), the InferNull tool can handle this case 253 | and automatically infer the `[NotNullWhen(true)]` attribute! 254 | 255 | 256 | 257 | For the `name` parameter (in general: for any out-parameters in functions returning `bool`), we create not only the declared type `name#2`, 258 | but also the `name_when_true` and `name_when_false` nodes. These extra helper nodes represent the nullability of `out string name` in the cases 259 | where `TryGet` returns true/false. 260 | 261 | Within the body of `TryGet`, we track the nullability of `name` based on the previous assignment as the "flow-state". 262 | After the assignment `name = someString;` in line 11, the nullability of `name` is the same as the nullability of `someString`. 263 | We represent this by saving the nullability node `someString#1` as the flow-state of `name`. 264 | On the `return true;` statement in line 12, we connect the current flow-state of the `out` parameters with the `when_true` helper nodes, 265 | resulting in the `someString#1`->`` edge. 266 | Similarly, the `return false;` statement in line 15 results in an edge from `` to ``, because the `name = null;` assignment 267 | has set the flow-state of `name` to ``. 268 | 269 | In the `Use` method, we also employ flow-state: even though `x` itself needs to be nullable, the then-branch of the `if` uses the `` node 270 | as flow-state for the `x` variable. 271 | This causes the `x.Length` dereference to create an edge starting at ``, rather than x's declared type (`x#3`). 272 | 273 | This allows inference to success (no path from `` to ``. In the inference result, `name#2` and `` are nullable, 274 | but `` is non-nullable. The difference in nullabilities between the when_false and when_true cases causes the tool to emit a `[NotNullWhen(true)]` 275 | attribute: 276 | 277 | ```csharp 278 | using System.Collections.Generic; 279 | using System.Diagnostics.CodeAnalysis; 280 | class Program 281 | { 282 | public string someString = "hello"; 283 | 284 | public bool TryGet(int i, [NotNullWhen(true)] out string? name) 285 | { 286 | if (i > 0) 287 | { 288 | name = someString; 289 | return true; 290 | } 291 | name = null; 292 | return false; 293 | } 294 | 295 | public int Use(int i) 296 | { 297 | if (TryGet(i, out string? x)) 298 | { 299 | return x.Length; 300 | } 301 | else 302 | { 303 | return 0; 304 | } 305 | } 306 | } 307 | ``` 308 | -------------------------------------------------------------------------------- /NullabilityInference/NullCheckingEngine.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Daniel Grunwald 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | // software and associated documentation files (the "Software"), to deal in the Software 5 | // without restriction, including without limitation the rights to use, copy, modify, merge, 6 | // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 7 | // to whom the Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 13 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 14 | // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 15 | // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | // DEALINGS IN THE SOFTWARE. 18 | 19 | using System; 20 | using System.Collections.Generic; 21 | using System.Diagnostics; 22 | using System.Linq; 23 | using System.Threading; 24 | using System.Threading.Tasks; 25 | using Microsoft.CodeAnalysis; 26 | using Microsoft.CodeAnalysis.CSharp; 27 | 28 | namespace ICSharpCode.NullabilityInference 29 | { 30 | /// 31 | /// Determines how to handle conflicted nodes (nodes that could be assigned null, but that also are used where a non-null type is required). 32 | /// 33 | public enum ConflictResolutionStrategy 34 | { 35 | /// 36 | /// Minimize the number of constraint violations: uses minimum cut of constraint graph. 37 | /// 38 | MinimizeWarnings, 39 | /// 40 | /// Conflicted nodes are marked null. 41 | /// 42 | PreferNull, 43 | /// 44 | /// Conflicted nodes are marked not-null. 45 | /// 46 | PreferNotNull, 47 | } 48 | 49 | public sealed class NullCheckingEngine 50 | { 51 | private readonly CSharpCompilation compilation; 52 | private readonly TypeSystem typeSystem; 53 | 54 | public TypeSystem TypeSystem => typeSystem; 55 | 56 | /// 57 | /// Creates a new NullCheckingEngine instance for the specified compilation. 58 | /// Note: for Roslyn's flow analysis to be useful to the inference, the given compilation should have as many reference types 59 | /// annotated as nullable as possible. This can be accomplished by using . 60 | /// 61 | public NullCheckingEngine(CSharpCompilation compilation) 62 | { 63 | this.compilation = compilation; 64 | this.typeSystem = new TypeSystem(compilation); 65 | } 66 | 67 | /// 68 | /// Constructs the null-type flow graph and infers nullabilities for the nodes. 69 | /// 70 | public void Analyze(ConflictResolutionStrategy strategy, CancellationToken cancellationToken) 71 | { 72 | Parallel.ForEach(compilation.SyntaxTrees, 73 | new ParallelOptions { CancellationToken = cancellationToken }, 74 | t => CreateNodes(t, cancellationToken)); 75 | 76 | Parallel.ForEach(compilation.SyntaxTrees, 77 | new ParallelOptions { CancellationToken = cancellationToken }, 78 | t => CreateEdges(t, cancellationToken)); 79 | 80 | switch (strategy) { 81 | case ConflictResolutionStrategy.MinimizeWarnings: 82 | MaximumFlow.Compute(typeSystem.AllNodes, typeSystem.NullableNode, typeSystem.NonNullNode, cancellationToken); 83 | 84 | // Infer non-null first using the residual graph. 85 | InferNonNullUsingResidualGraph(typeSystem.NonNullNode); 86 | // Then use the original graph to infer nullable types everywhere we didn't already infer non-null. 87 | // This ends up creating the minimum cut. 88 | InferNullable(typeSystem.NullableNode); 89 | // Note that for longer chains (null -> A -> B -> C -> nonnull) 90 | // this approach ends up cutting the graph as close to nonnull as possible when there's multiple 91 | // choices with the same number of warnings. This is why we use the "reverse" residual graph 92 | // (ResidualGraphPredecessors) -- using ResidualGraphSuccessors would end up cutting closer to the node. 93 | break; 94 | case ConflictResolutionStrategy.PreferNull: 95 | InferNullable(typeSystem.NullableNode); 96 | InferNonNull(typeSystem.NonNullNode); 97 | break; 98 | case ConflictResolutionStrategy.PreferNotNull: 99 | InferNonNull(typeSystem.NonNullNode); 100 | InferNullable(typeSystem.NullableNode); 101 | break; 102 | default: 103 | throw new NotSupportedException(strategy.ToString()); 104 | } 105 | 106 | // There's going to be a bunch of remaining nodes where either choice would work. 107 | // For parameters, prefer marking those as nullable: 108 | foreach (var paramNode in typeSystem.NodesInInputPositions) { 109 | if (paramNode.ReplacedWith.NullType == NullType.Infer) { 110 | InferNullable(paramNode.ReplacedWith); 111 | } 112 | } 113 | foreach (var node in typeSystem.AllNodes) { 114 | // Finally, anything left over is inferred to be non-null: 115 | if (node.NullType == NullType.Infer) { 116 | if (node.ReplacedWith.NullType != NullType.Infer) 117 | node.NullType = node.ReplacedWith.NullType; 118 | else 119 | node.NullType = NullType.NonNull; 120 | } 121 | Debug.Assert(node.NullType == node.ReplacedWith.NullType); 122 | } 123 | } 124 | 125 | private void InferNonNullUsingResidualGraph(NullabilityNode node) 126 | { 127 | Debug.Assert(node.NullType == NullType.Infer || node.NullType == NullType.NonNull); 128 | node.NullType = NullType.NonNull; 129 | foreach (var pred in node.ResidualGraphPredecessors) { 130 | if (pred.NullType == NullType.Infer) { 131 | InferNonNullUsingResidualGraph(pred); 132 | } 133 | } 134 | } 135 | 136 | /// 137 | /// Invokes the callback with the new syntax trees where the inferred nullability has been inserted. 138 | /// 139 | public Statistics ConvertSyntaxTrees(CancellationToken cancellationToken, Action action) 140 | { 141 | object statsLock = new object(); 142 | Statistics statistics = new Statistics(); 143 | Parallel.ForEach(compilation.SyntaxTrees, 144 | new ParallelOptions { CancellationToken = cancellationToken }, 145 | syntaxTree => { 146 | var semanticModel = compilation.GetSemanticModel(syntaxTree); 147 | var rewriter = new InferredNullabilitySyntaxRewriter(semanticModel, typeSystem, typeSystem.GetMapping(syntaxTree), cancellationToken); 148 | var newRoot = rewriter.Visit(syntaxTree.GetRoot()); 149 | action(syntaxTree.WithRootAndOptions(newRoot, syntaxTree.Options)); 150 | lock (statsLock) { 151 | statistics.Update(rewriter.Statistics); 152 | } 153 | }); 154 | return statistics; 155 | } 156 | 157 | private void InferNonNull(NullabilityNode node) 158 | { 159 | Debug.Assert(node.NullType == NullType.Infer || node.NullType == NullType.NonNull); 160 | node.NullType = NullType.NonNull; 161 | foreach (var edge in node.IncomingEdges) { 162 | if (edge.Source.NullType == NullType.Infer) { 163 | InferNonNull(edge.Source); 164 | } 165 | } 166 | } 167 | 168 | private void InferNullable(NullabilityNode node) 169 | { 170 | Debug.Assert(node.NullType == NullType.Infer || node.NullType == NullType.Nullable); 171 | node.NullType = NullType.Nullable; 172 | foreach (var edge in node.OutgoingEdges) { 173 | if (edge.Target.NullType == NullType.Infer) { 174 | InferNullable(edge.Target); 175 | } 176 | } 177 | } 178 | 179 | private void CreateNodes(SyntaxTree syntaxTree, CancellationToken cancellationToken) 180 | { 181 | var semanticModel = compilation.GetSemanticModel(syntaxTree); 182 | var tsBuilder = new TypeSystem.Builder(typeSystem); 183 | var visitor = new NodeBuildingSyntaxVisitor(semanticModel, tsBuilder, cancellationToken); 184 | visitor.Visit(syntaxTree.GetRoot(cancellationToken)); 185 | lock (typeSystem) { 186 | typeSystem.RegisterNodes(syntaxTree, visitor.Mapping); 187 | tsBuilder.Flush(typeSystem); 188 | } 189 | } 190 | 191 | private void CreateEdges(SyntaxTree syntaxTree, CancellationToken cancellationToken) 192 | { 193 | var semanticModel = compilation.GetSemanticModel(syntaxTree); 194 | var tsBuilder = new TypeSystem.Builder(typeSystem); 195 | var mapping = typeSystem.GetMapping(syntaxTree); 196 | var visitor = new EdgeBuildingSyntaxVisitor(semanticModel, typeSystem, tsBuilder, mapping, cancellationToken); 197 | visitor.Visit(syntaxTree.GetRoot(cancellationToken)); 198 | foreach (var cref in mapping.CrefSyntaxes) { 199 | visitor.HandleCref(cref); 200 | } 201 | lock (typeSystem) { 202 | tsBuilder.Flush(typeSystem); 203 | } 204 | } 205 | 206 | #if DEBUG 207 | /// 208 | /// Exports the type graph in a form suitable for visualization. 209 | /// 210 | public GraphVizGraph ExportTypeGraph() 211 | { 212 | return ExportTypeGraph(n => n.NullType != NullType.Oblivious && n.ReplacedWith == n); 213 | } 214 | 215 | public GraphVizGraph ExportTypeGraph(Predicate isInteresting/*, bool addAllPathsUntilNonNull = false, bool addAllPathsFromNullable = false, bool addAllPathsWithFlow = false*/) 216 | { 217 | var startNodes = new HashSet(); 218 | var startEdges = new HashSet(); 219 | foreach (var node in typeSystem.AllNodes) { 220 | if (node.Location != null && isInteresting(node.Location)) { 221 | startNodes.Add(node); 222 | } 223 | foreach (var edge in node.OutgoingEdges) { 224 | if (edge.Label.location is { } location && isInteresting(location)) { 225 | startEdges.Add(edge); 226 | } 227 | } 228 | } 229 | return ExportTypeGraph(startNodes.Contains, startEdges.Contains); 230 | /*var selectedEdges = new HashSet(selectedEdges); 231 | if (addAllPathsWithFlow) { 232 | var flowEdges = new HashSet(GetFlowEdges()) ; 233 | AddReachableNodes(n => n.OutgoingEdges.Where(flowEdges.Contains).Select(e => e.Target)); 234 | AddReachableNodes(n => n.IncomingEdges.Where(flowEdges.Contains).Select(e => e.Source)); 235 | } 236 | if (addAllPathsUntilNonNull) { 237 | AddReachableNodes(n => n.Successors); 238 | } 239 | if (addAllPathsFromNullable) { 240 | AddReachableNodes(n => n.Predecessors); 241 | } 242 | return ExportTypeGraph(selectedNodes.Contains); 243 | 244 | void AddReachableNodes(Func> successors) 245 | { 246 | var worklist = new Queue(startNodes); 247 | while (worklist.Count > 0) { 248 | foreach (var node in successors(worklist.Dequeue())) { 249 | if (selectedNodes.Add(node)) { 250 | worklist.Enqueue(node); 251 | } 252 | } 253 | } 254 | }*/ 255 | } 256 | 257 | /// 258 | /// Returns the edges that are missing from the residual graph. 259 | /// 260 | private IEnumerable GetFlowEdges() 261 | { 262 | var dict = new Dictionary(); 263 | foreach (var node in typeSystem.AllNodes) { 264 | dict.Clear(); 265 | foreach (var n in node.ResidualGraphPredecessors) { 266 | dict.TryGetValue(n, out int i); 267 | dict[n] = i + 1; 268 | } 269 | foreach (var edge in node.IncomingEdges) { 270 | if (dict.TryGetValue(edge.Source, out int i) && i > 0) { 271 | dict[edge.Source] = i - 1; 272 | } else { 273 | yield return edge; 274 | } 275 | } 276 | } 277 | } 278 | 279 | /// 280 | /// Exports a subset of the type graph in a form suitable for visualization. 281 | /// 282 | public GraphVizGraph ExportTypeGraph(Predicate nodeFilter) 283 | { 284 | return ExportTypeGraph(nodeFilter, e => false); 285 | } 286 | 287 | private GraphVizGraph ExportTypeGraph(Predicate nodeFilter, Predicate edgeFilter) 288 | { 289 | if (nodeFilter == null) 290 | throw new ArgumentNullException("includeInGraph"); 291 | GraphVizGraph graph = new GraphVizGraph { rankdir = "BT" }; 292 | List graphEdges = new List(); 293 | foreach (NullabilityNode node in typeSystem.AllNodes) { 294 | foreach (NullabilityEdge edge in node.IncomingEdges) { 295 | if (nodeFilter(edge.Source) || nodeFilter(edge.Target) || edgeFilter(edge)) { 296 | graphEdges.Add(edge); 297 | } 298 | } 299 | } 300 | // Select nodes based on include filter 301 | IEnumerable includedNodes = typeSystem.AllNodes.Where(n => nodeFilter(n)); 302 | // Add nodes necessary for selected edges 303 | includedNodes = includedNodes.Concat(graphEdges.Select(g => g.Source)).Concat(graphEdges.Select(g => g.Target)).Distinct(); 304 | var nodeIds = new Dictionary(); 305 | foreach (NullabilityNode node in includedNodes) { 306 | string nodeId = $"n{nodeIds.Count}"; 307 | nodeIds.Add(node, nodeId); 308 | GraphVizNode gvNode = new GraphVizNode(nodeId) { label = node.Name, fontsize = 32 }; 309 | if (node is SpecialNullabilityNode) { 310 | gvNode.fontsize = 24; 311 | } else { 312 | gvNode.fontsize = 12; 313 | gvNode.margin = "0.05,0.05"; 314 | gvNode.height = 0; 315 | gvNode.shape = "box"; 316 | } 317 | if (node.Location != null) { 318 | gvNode.label += $"\n{node.Location.EndPosToString()}"; 319 | } 320 | gvNode.label += $"\n{node.NullType}"; 321 | graph.AddNode(gvNode); 322 | } 323 | var flowEdges = new HashSet(GetFlowEdges()); 324 | foreach (NullabilityEdge edge in graphEdges) { 325 | var gvEdge = new GraphVizEdge(nodeIds[edge.Source], nodeIds[edge.Target]); 326 | gvEdge.label = edge.Label.ToString(); 327 | gvEdge.fontsize = 8; 328 | if (edge.IsError) 329 | gvEdge.color = "red"; 330 | else if (flowEdges.Contains(edge)) 331 | gvEdge.color = "yellow"; 332 | graph.AddEdge(gvEdge); 333 | } 334 | return graph; 335 | } 336 | #endif 337 | } 338 | } 339 | --------------------------------------------------------------------------------