From 96a1bba1108af3fedc98b6bad497a5429955b81c Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Tue, 28 Dec 2021 18:42:30 -0800 Subject: [PATCH] Implement NullableSourceGenerator --- .globalconfig | 3 + THIRD-PARTY-NOTICES.txt | 35 ++ .../CompilationExtensions.cs | 86 ++++ .../ISymbolExtensions.cs | 57 +++ .../NullableSourceGenerator.cs | 427 ++++++++++++++++++ .../SymbolVisibility.cs | 12 + 6 files changed, 620 insertions(+) create mode 100644 THIRD-PARTY-NOTICES.txt create mode 100644 src/TunnelVisionLabs.LanguageTypes.SourceGenerator/CompilationExtensions.cs create mode 100644 src/TunnelVisionLabs.LanguageTypes.SourceGenerator/ISymbolExtensions.cs create mode 100644 src/TunnelVisionLabs.LanguageTypes.SourceGenerator/NullableSourceGenerator.cs create mode 100644 src/TunnelVisionLabs.LanguageTypes.SourceGenerator/SymbolVisibility.cs diff --git a/.globalconfig b/.globalconfig index 4650bb6..e123cd5 100644 --- a/.globalconfig +++ b/.globalconfig @@ -15,6 +15,9 @@ dotnet_diagnostic.SA1130.severity = silent # SA1600: Elements should be documented dotnet_diagnostic.SA1600.severity = silent +# SA1602: Enumeration items should be documented +dotnet_diagnostic.SA1602.severity = silent + # IDE0009: Member access should be qualified. dotnet_diagnostic.IDE0009.severity = none diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt new file mode 100644 index 0000000..b47d75f --- /dev/null +++ b/THIRD-PARTY-NOTICES.txt @@ -0,0 +1,35 @@ +Language Types uses third-party libraries or other resources that may be +distributed under licenses different than the Language Types software. + +In the event that we accidentally failed to list a required notice, please +bring it to our attention by posting an issue. + +The attached notices are provided for information only. + + +License notice for .NET Compiler Platform ("Roslyn") +---------------------------------------------------- + +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/TunnelVisionLabs.LanguageTypes.SourceGenerator/CompilationExtensions.cs b/src/TunnelVisionLabs.LanguageTypes.SourceGenerator/CompilationExtensions.cs new file mode 100644 index 0000000..5cebbc5 --- /dev/null +++ b/src/TunnelVisionLabs.LanguageTypes.SourceGenerator/CompilationExtensions.cs @@ -0,0 +1,86 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace TunnelVisionLabs.LanguageTypes.SourceGenerator +{ + using Microsoft.CodeAnalysis; + + internal static class CompilationExtensions + { + /// + /// Gets a type by its metadata name to use for code analysis within a . This method + /// attempts to find the "best" symbol to use for code analysis, which is the symbol matching the first of the + /// following rules. + /// + /// + /// + /// If only one type with the given name is found within the compilation and its referenced assemblies, that + /// type is returned regardless of accessibility. + /// + /// + /// If the current defines the symbol, that symbol is returned. + /// + /// + /// If exactly one referenced assembly defines the symbol in a manner that makes it visible to the current + /// , that symbol is returned. + /// + /// + /// Otherwise, this method returns . + /// + /// + /// + /// The to consider for analysis. + /// The fully-qualified metadata type name to find. + /// to only consider accessible types; otherwise, to consider all types. + /// The symbol to use for code analysis; otherwise, . + public static INamedTypeSymbol? GetBestTypeByMetadataName(this Compilation compilation, string fullyQualifiedMetadataName, bool requiresAccess) + { + // Try to get the unique type with this name, ignoring accessibility + var type = compilation.GetTypeByMetadataName(fullyQualifiedMetadataName); + + // Otherwise, try to get the unique type with this name originally defined in 'compilation' + type ??= compilation.Assembly.GetTypeByMetadataName(fullyQualifiedMetadataName); + + // Otherwise, try to get the unique accessible type with this name from a reference + if (type is null) + { + foreach (var module in compilation.Assembly.Modules) + { + foreach (var referencedAssembly in module.ReferencedAssemblySymbols) + { + var currentType = referencedAssembly.GetTypeByMetadataName(fullyQualifiedMetadataName); + if (currentType is null) + { + continue; + } + + switch (currentType.GetResultantVisibility()) + { + case SymbolVisibility.Public: + case SymbolVisibility.Internal when referencedAssembly.GivesAccessTo(compilation.Assembly): + break; + + default: + continue; + } + + if (type is object) + { + // Multiple visible types with the same metadata name are present + return null; + } + + type = currentType; + } + } + } + + if (requiresAccess && type is { DeclaredAccessibility: Accessibility.Internal } && !type.ContainingAssembly.GivesAccessTo(compilation.Assembly)) + { + return null; + } + + return type; + } + } +} diff --git a/src/TunnelVisionLabs.LanguageTypes.SourceGenerator/ISymbolExtensions.cs b/src/TunnelVisionLabs.LanguageTypes.SourceGenerator/ISymbolExtensions.cs new file mode 100644 index 0000000..b8c278a --- /dev/null +++ b/src/TunnelVisionLabs.LanguageTypes.SourceGenerator/ISymbolExtensions.cs @@ -0,0 +1,57 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace TunnelVisionLabs.LanguageTypes.SourceGenerator +{ + using Microsoft.CodeAnalysis; + + internal static class ISymbolExtensions + { + public static SymbolVisibility GetResultantVisibility(this ISymbol symbol) + { + // Start by assuming it's visible. + var visibility = SymbolVisibility.Public; + + switch (symbol.Kind) + { + case SymbolKind.Alias: + // Aliases are uber private. They're only visible in the same file that they + // were declared in. + return SymbolVisibility.Private; + + case SymbolKind.Parameter: + // Parameters are only as visible as their containing symbol + return GetResultantVisibility(symbol.ContainingSymbol); + + case SymbolKind.TypeParameter: + // Type Parameters are private. + return SymbolVisibility.Private; + } + + while (symbol != null && symbol.Kind != SymbolKind.Namespace) + { + switch (symbol.DeclaredAccessibility) + { + // If we see anything private, then the symbol is private. + case Accessibility.NotApplicable: + case Accessibility.Private: + return SymbolVisibility.Private; + + // If we see anything internal, then knock it down from public to + // internal. + case Accessibility.Internal: + case Accessibility.ProtectedAndInternal: + visibility = SymbolVisibility.Internal; + break; + + // For anything else (Public, Protected, ProtectedOrInternal), the + // symbol stays at the level we've gotten so far. + } + + symbol = symbol.ContainingSymbol; + } + + return visibility; + } + } +} diff --git a/src/TunnelVisionLabs.LanguageTypes.SourceGenerator/NullableSourceGenerator.cs b/src/TunnelVisionLabs.LanguageTypes.SourceGenerator/NullableSourceGenerator.cs new file mode 100644 index 0000000..16d1b1c --- /dev/null +++ b/src/TunnelVisionLabs.LanguageTypes.SourceGenerator/NullableSourceGenerator.cs @@ -0,0 +1,427 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace TunnelVisionLabs.LanguageTypes.SourceGenerator +{ + using System.Collections.Generic; + using Microsoft.CodeAnalysis; + + [Generator(LanguageNames.CSharp)] + internal class NullableSourceGenerator : IIncrementalGenerator + { + private const string AllowNullAttributeSource = @"// + +namespace System.Diagnostics.CodeAnalysis +{ + /// Specifies that null is allowed as an input even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + internal sealed class AllowNullAttribute : Attribute + { + } +} +"; + + private const string DisallowNullAttributeSource = @"// + +namespace System.Diagnostics.CodeAnalysis +{ + /// Specifies that null is disallowed as an input even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + internal sealed class DisallowNullAttribute : Attribute + { + } +} +"; + + private const string MaybeNullAttributeSource = @"// + +namespace System.Diagnostics.CodeAnalysis +{ + /// Specifies that an output may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class MaybeNullAttribute : Attribute + { + } +} +"; + + private const string NotNullAttributeSource = @"// + +namespace System.Diagnostics.CodeAnalysis +{ + /// Specifies that an output will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class NotNullAttribute : Attribute + { + } +} +"; + + private const string MaybeNullWhenAttributeSource = @"// + +namespace System.Diagnostics.CodeAnalysis +{ + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class MaybeNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } +} +"; + + private const string NotNullWhenAttributeSource = @"// + +namespace System.Diagnostics.CodeAnalysis +{ + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class NotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } +} +"; + + private const string NotNullIfNotNullAttributeSource = @"// + +namespace System.Diagnostics.CodeAnalysis +{ + /// Specifies that the output will be non-null if the named parameter is non-null. + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] + internal sealed class NotNullIfNotNullAttribute : Attribute + { + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } + } +} +"; + + private const string DoesNotReturnAttributeSource = @"// + +namespace System.Diagnostics.CodeAnalysis +{ + /// Applied to a method that will never return under any circumstance. + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + internal sealed class DoesNotReturnAttribute : Attribute + { + } +} +"; + + private const string DoesNotReturnIfAttributeSource = @"// + +namespace System.Diagnostics.CodeAnalysis +{ + /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class DoesNotReturnIfAttribute : Attribute + { + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + + /// Gets the condition parameter value. + public bool ParameterValue { get; } + } +} +"; + + private const string MemberNotNullAttributeSource = @"// + +namespace System.Diagnostics.CodeAnalysis +{ + /// Specifies that the method or property will ensure that the listed field and property members have not-null values. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullAttribute : Attribute + { + /// Initializes the attribute with a field or property member. + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullAttribute(string member) => Members = new[] { member }; + + /// Initializes the attribute with the list of field and property members. + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullAttribute(params string[] members) => Members = members; + + /// Gets field or property member names. + public string[] Members { get; } + } +} +"; + + private const string MemberNotNullWhenAttributeSource = @"// + +namespace System.Diagnostics.CodeAnalysis +{ + /// 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. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = new[] { member }; + } + + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + + /// Gets field or property member names. + public string[] Members { get; } + } +} +"; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var referencedTypesData = context.CompilationProvider.Select( + (compilation, cancellationToken) => + { + var hasAllowNullAttribute = IsCodeAnalysisAttributeAvailable(compilation, "System.Diagnostics.CodeAnalysis.AllowNullAttribute"); + var hasDisallowNullAttribute = IsCodeAnalysisAttributeAvailable(compilation, "System.Diagnostics.CodeAnalysis.DisallowNullAttribute"); + var hasMaybeNullAttribute = IsCodeAnalysisAttributeAvailable(compilation, "System.Diagnostics.CodeAnalysis.MaybeNullAttribute"); + var hasNotNullAttribute = IsCodeAnalysisAttributeAvailable(compilation, "System.Diagnostics.CodeAnalysis.NotNullAttribute"); + var hasMaybeNullWhenAttribute = IsCodeAnalysisAttributeAvailable(compilation, "System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute"); + var hasNotNullWhenAttribute = IsCodeAnalysisAttributeAvailable(compilation, "System.Diagnostics.CodeAnalysis.NotNullWhenAttribute"); + var hasNotNullIfNotNullAttribute = IsCodeAnalysisAttributeAvailable(compilation, "System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute"); + var hasDoesNotReturnAttribute = IsCodeAnalysisAttributeAvailable(compilation, "System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute"); + var hasDoesNotReturnIfAttribute = IsCodeAnalysisAttributeAvailable(compilation, "System.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute"); + var hasMemberNotNullAttribute = IsCodeAnalysisAttributeAvailable(compilation, "System.Diagnostics.CodeAnalysis.MemberNotNullAttribute"); + var hasMemberNotNullWhenAttribute = IsCodeAnalysisAttributeAvailable(compilation, "System.Diagnostics.CodeAnalysis.MemberNotNullWhenAttribute"); + + return new ReferencedTypesData( + hasAllowNullAttribute: hasAllowNullAttribute, + hasDisallowNullAttribute: hasDisallowNullAttribute, + hasMaybeNullAttribute: hasMaybeNullAttribute, + hasNotNullAttribute: hasNotNullAttribute, + hasMaybeNullWhenAttribute: hasMaybeNullWhenAttribute, + hasNotNullWhenAttribute: hasNotNullWhenAttribute, + hasNotNullIfNotNullAttribute: hasNotNullIfNotNullAttribute, + hasDoesNotReturnAttribute: hasDoesNotReturnAttribute, + hasDoesNotReturnIfAttribute: hasDoesNotReturnIfAttribute, + hasMemberNotNullAttribute: hasMemberNotNullAttribute, + hasMemberNotNullWhenAttribute: hasMemberNotNullWhenAttribute); + }); + + context.RegisterSourceOutput( + referencedTypesData, + (context, referencedTypesData) => + { + var forwarders = new List(); + + if (!referencedTypesData.HasAllowNullAttribute) + { + context.AddSource("AllowNullAttribute.g.cs", AllowNullAttributeSource); + } + else + { + forwarders.Add("AllowNullAttribute"); + } + + if (!referencedTypesData.HasDisallowNullAttribute) + { + context.AddSource("DisallowNullAttribute.g.cs", DisallowNullAttributeSource); + } + else + { + forwarders.Add("DisallowNullAttribute"); + } + + if (!referencedTypesData.HasMaybeNullAttribute) + { + context.AddSource("MaybeNullAttribute.g.cs", MaybeNullAttributeSource); + } + else + { + forwarders.Add("MaybeNullAttribute"); + } + + if (!referencedTypesData.HasNotNullAttribute) + { + context.AddSource("NotNullAttribute.g.cs", NotNullAttributeSource); + } + else + { + forwarders.Add("NotNullAttribute"); + } + + if (!referencedTypesData.HasMaybeNullWhenAttribute) + { + context.AddSource("MaybeNullWhenAttribute.g.cs", MaybeNullWhenAttributeSource); + } + else + { + forwarders.Add("MaybeNullWhenAttribute"); + } + + if (!referencedTypesData.HasNotNullWhenAttribute) + { + context.AddSource("NotNullWhenAttribute.g.cs", NotNullWhenAttributeSource); + } + else + { + forwarders.Add("NotNullWhenAttribute"); + } + + if (!referencedTypesData.HasNotNullIfNotNullAttribute) + { + context.AddSource("NotNullIfNotNullAttribute.g.cs", NotNullIfNotNullAttributeSource); + } + else + { + forwarders.Add("NotNullIfNotNullAttribute"); + } + + if (!referencedTypesData.HasDoesNotReturnAttribute) + { + context.AddSource("DoesNotReturnAttribute.g.cs", DoesNotReturnAttributeSource); + } + else + { + forwarders.Add("DoesNotReturnAttribute"); + } + + if (!referencedTypesData.HasDoesNotReturnIfAttribute) + { + context.AddSource("DoesNotReturnIfAttribute.g.cs", DoesNotReturnIfAttributeSource); + } + else + { + forwarders.Add("DoesNotReturnIfAttribute"); + } + + if (!referencedTypesData.HasMemberNotNullAttribute) + { + context.AddSource("MemberNotNullAttribute.g.cs", MemberNotNullAttributeSource); + } + else + { + forwarders.Add("MemberNotNullAttribute"); + } + + if (!referencedTypesData.HasMemberNotNullWhenAttribute) + { + context.AddSource("MemberNotNullWhenAttribute.g.cs", MemberNotNullWhenAttributeSource); + } + else + { + forwarders.Add("MemberNotNullWhenAttribute"); + } + + if (forwarders.Count > 0) + { + var nullableForwarders = $@"// + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +{string.Join("\r\n", forwarders.Select(forwarder => $"[assembly: TypeForwardedTo(typeof({forwarder}))]"))} +"; + + context.AddSource("NullableForwarders.g.cs", nullableForwarders); + } + }); + } + + private static bool IsCodeAnalysisAttributeAvailable(Compilation compilation, string fullyQualifiedMetadataName) + => compilation.GetBestTypeByMetadataName(fullyQualifiedMetadataName, requiresAccess: true) is not null; + + private sealed class ReferencedTypesData + { + public ReferencedTypesData( + bool hasAllowNullAttribute, + bool hasDisallowNullAttribute, + bool hasMaybeNullAttribute, + bool hasNotNullAttribute, + bool hasMaybeNullWhenAttribute, + bool hasNotNullWhenAttribute, + bool hasNotNullIfNotNullAttribute, + bool hasDoesNotReturnAttribute, + bool hasDoesNotReturnIfAttribute, + bool hasMemberNotNullAttribute, + bool hasMemberNotNullWhenAttribute) + { + HasAllowNullAttribute = hasAllowNullAttribute; + HasDisallowNullAttribute = hasDisallowNullAttribute; + HasMaybeNullAttribute = hasMaybeNullAttribute; + HasNotNullAttribute = hasNotNullAttribute; + HasMaybeNullWhenAttribute = hasMaybeNullWhenAttribute; + HasNotNullWhenAttribute = hasNotNullWhenAttribute; + HasNotNullIfNotNullAttribute = hasNotNullIfNotNullAttribute; + HasDoesNotReturnAttribute = hasDoesNotReturnAttribute; + HasDoesNotReturnIfAttribute = hasDoesNotReturnIfAttribute; + HasMemberNotNullAttribute = hasMemberNotNullAttribute; + HasMemberNotNullWhenAttribute = hasMemberNotNullWhenAttribute; + } + + public bool HasAllowNullAttribute { get; } + + public bool HasDisallowNullAttribute { get; } + + public bool HasMaybeNullAttribute { get; } + + public bool HasNotNullAttribute { get; } + + public bool HasMaybeNullWhenAttribute { get; } + + public bool HasNotNullWhenAttribute { get; } + + public bool HasNotNullIfNotNullAttribute { get; } + + public bool HasDoesNotReturnAttribute { get; } + + public bool HasDoesNotReturnIfAttribute { get; } + + public bool HasMemberNotNullAttribute { get; } + + public bool HasMemberNotNullWhenAttribute { get; } + } + } +} diff --git a/src/TunnelVisionLabs.LanguageTypes.SourceGenerator/SymbolVisibility.cs b/src/TunnelVisionLabs.LanguageTypes.SourceGenerator/SymbolVisibility.cs new file mode 100644 index 0000000..643282d --- /dev/null +++ b/src/TunnelVisionLabs.LanguageTypes.SourceGenerator/SymbolVisibility.cs @@ -0,0 +1,12 @@ +// Copyright (c) Tunnel Vision Laboratories, LLC. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace TunnelVisionLabs.LanguageTypes.SourceGenerator +{ + internal enum SymbolVisibility + { + Public, + Internal, + Private, + } +}