From e078c856c7c1c8b150a6f571a826c6a87f59454f Mon Sep 17 00:00:00 2001 From: Sam Harwell Date: Mon, 2 Mar 2020 14:35:53 -0800 Subject: [PATCH] Add lightup support for nullable reference types --- THIRD-PARTY-NOTICES.txt | 30 ++- .../Compiler/Analyzer.Utilities.projitems | 6 + .../Compiler/Lightup/ITypeSymbolExtensions.cs | 21 ++ .../Compiler/Lightup/LightupHelpers.cs | 229 ++++++++++++++++++ .../Compiler/Lightup/NullableAnnotation.cs | 41 ++++ .../Compiler/Lightup/NullableContext.cs | 71 ++++++ .../Lightup/NullableContextExtensions.cs | 34 +++ .../Lightup/SemanticModelExtensions.cs | 16 ++ 8 files changed, 447 insertions(+), 1 deletion(-) create mode 100644 src/Utilities/Compiler/Lightup/ITypeSymbolExtensions.cs create mode 100644 src/Utilities/Compiler/Lightup/LightupHelpers.cs create mode 100644 src/Utilities/Compiler/Lightup/NullableAnnotation.cs create mode 100644 src/Utilities/Compiler/Lightup/NullableContext.cs create mode 100644 src/Utilities/Compiler/Lightup/NullableContextExtensions.cs create mode 100644 src/Utilities/Compiler/Lightup/SemanticModelExtensions.cs diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt index bf99bee0d9..e474ac1858 100644 --- a/THIRD-PARTY-NOTICES.txt +++ b/THIRD-PARTY-NOTICES.txt @@ -37,4 +37,32 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. \ No newline at end of file +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + + +License notice for StyleCop Analyzers +------------------------------------- + +The MIT License (MIT) + +Copyright (c) Tunnel Vision Laboratories, LLC + +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/Utilities/Compiler/Analyzer.Utilities.projitems b/src/Utilities/Compiler/Analyzer.Utilities.projitems index e43de3be6c..f2aed01746 100644 --- a/src/Utilities/Compiler/Analyzer.Utilities.projitems +++ b/src/Utilities/Compiler/Analyzer.Utilities.projitems @@ -47,6 +47,12 @@ + + + + + + diff --git a/src/Utilities/Compiler/Lightup/ITypeSymbolExtensions.cs b/src/Utilities/Compiler/Lightup/ITypeSymbolExtensions.cs new file mode 100644 index 0000000000..a1b64f3190 --- /dev/null +++ b/src/Utilities/Compiler/Lightup/ITypeSymbolExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.CodeAnalysis; + +namespace Analyzer.Utilities.Lightup +{ + internal static class ITypeSymbolExtensions + { + private static readonly Func s_nullableAnnotation + = LightupHelpers.CreateSymbolPropertyAccessor(typeof(ITypeSymbol), nameof(NullableAnnotation)); + private static readonly Func s_withNullableAnnotation + = LightupHelpers.CreateSymbolWithPropertyAccessor(typeof(ITypeSymbol), nameof(NullableAnnotation)); + + public static NullableAnnotation NullableAnnotation(this ITypeSymbol typeSymbol) + => s_nullableAnnotation(typeSymbol); + + public static ITypeSymbol WithNullableAnnotation(this ITypeSymbol typeSymbol, NullableAnnotation nullableAnnotation) + => s_withNullableAnnotation(typeSymbol, nullableAnnotation); + } +} diff --git a/src/Utilities/Compiler/Lightup/LightupHelpers.cs b/src/Utilities/Compiler/Lightup/LightupHelpers.cs new file mode 100644 index 0000000000..30eeb08ebe --- /dev/null +++ b/src/Utilities/Compiler/Lightup/LightupHelpers.cs @@ -0,0 +1,229 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.CodeAnalysis; + +namespace Analyzer.Utilities.Lightup +{ + internal static class LightupHelpers + { + internal static Func CreateSyntaxPropertyAccessor(Type type, string propertyName) + where TSyntax : SyntaxNode + => CreatePropertyAccessor(type, "syntax", propertyName); + + internal static Func CreateSymbolPropertyAccessor(Type type, string propertyName) + where TSymbol : ISymbol + => CreatePropertyAccessor(type, "symbol", propertyName); + + private static Func CreatePropertyAccessor(Type type, string parameterName, string propertyName) + { + static TProperty FallbackAccessor(T instance) + { + if (instance == null) + { + // Unlike an extension method which would throw ArgumentNullException here, the light-up + // behavior needs to match behavior of the underlying property. + throw new NullReferenceException(); + } + + return default!; + } + + if (type == null) + { + return FallbackAccessor; + } + + if (!typeof(T).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) + { + throw new InvalidOperationException(); + } + + var property = type.GetTypeInfo().GetDeclaredProperty(propertyName); + if (property == null) + { + return FallbackAccessor; + } + + if (!typeof(TProperty).GetTypeInfo().IsAssignableFrom(property.PropertyType.GetTypeInfo())) + { + if (property.PropertyType.GetTypeInfo().IsEnum + && typeof(TProperty).GetTypeInfo().IsEnum + && Enum.GetUnderlyingType(typeof(TProperty)).GetTypeInfo().IsAssignableFrom(Enum.GetUnderlyingType(property.PropertyType).GetTypeInfo())) + { + // Allow this + } + else + { + throw new InvalidOperationException(); + } + } + + var parameter = Expression.Parameter(typeof(T), parameterName); + Expression instance = + type.GetTypeInfo().IsAssignableFrom(typeof(T).GetTypeInfo()) + ? (Expression)parameter + : Expression.Convert(parameter, type); + + Expression result = Expression.Call(instance, property.GetMethod); + if (!typeof(TProperty).GetTypeInfo().IsAssignableFrom(property.PropertyType.GetTypeInfo())) + { + result = Expression.Convert(result, typeof(TProperty)); + } + + Expression> expression = Expression.Lambda>(result, parameter); + return expression.Compile(); + } + + internal static Func CreateSyntaxWithPropertyAccessor(Type type, string propertyName) + where TSyntax : SyntaxNode + => CreateWithPropertyAccessor(type, "syntax", propertyName); + + internal static Func CreateSymbolWithPropertyAccessor(Type type, string propertyName) + where TSymbol : ISymbol + => CreateWithPropertyAccessor(type, "symbol", propertyName); + + private static Func CreateWithPropertyAccessor(Type type, string parameterName, string propertyName) + { + static T FallbackAccessor(T instance, TProperty newValue) + { + if (instance == null) + { + // Unlike an extension method which would throw ArgumentNullException here, the light-up + // behavior needs to match behavior of the underlying property. + throw new NullReferenceException(); + } + + if (Equals(newValue, default(TProperty))) + { + return instance; + } + + throw new NotSupportedException(); + } + + if (type == null) + { + return FallbackAccessor; + } + + if (!typeof(T).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) + { + throw new InvalidOperationException(); + } + + var property = type.GetTypeInfo().GetDeclaredProperty(propertyName); + if (property == null) + { + return FallbackAccessor; + } + + if (!typeof(TProperty).GetTypeInfo().IsAssignableFrom(property.PropertyType.GetTypeInfo())) + { + if (property.PropertyType.GetTypeInfo().IsEnum + && typeof(TProperty).GetTypeInfo().IsEnum + && Enum.GetUnderlyingType(typeof(TProperty)).GetTypeInfo().IsAssignableFrom(Enum.GetUnderlyingType(property.PropertyType).GetTypeInfo())) + { + // Allow this + } + else + { + throw new InvalidOperationException(); + } + } + + var methodInfo = type.GetTypeInfo().GetDeclaredMethods("With" + propertyName) + .SingleOrDefault(m => !m.IsStatic && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.Equals(property.PropertyType)); + if (methodInfo is null) + { + return FallbackAccessor; + } + + var parameter = Expression.Parameter(typeof(T), parameterName); + var valueParameter = Expression.Parameter(typeof(TProperty), methodInfo.GetParameters()[0].Name); + Expression instance = + type.GetTypeInfo().IsAssignableFrom(typeof(T).GetTypeInfo()) + ? (Expression)parameter + : Expression.Convert(parameter, type); + Expression value = + property.PropertyType.GetTypeInfo().IsAssignableFrom(typeof(TProperty).GetTypeInfo()) + ? (Expression)valueParameter + : Expression.Convert(valueParameter, property.PropertyType); + + Expression> expression = + Expression.Lambda>( + Expression.Call(instance, methodInfo, value), + parameter, + valueParameter); + return expression.Compile(); + } + + internal static Func CreateAccessorWithArgument(Type type, string parameterName, Type argumentType, string argumentName, string methodName) + { + static TValue FallbackAccessor(T instance, TArg argument) + { + if (instance == null) + { + // Unlike an extension method which would throw ArgumentNullException here, the light-up + // behavior needs to match behavior of the underlying property. + throw new NullReferenceException(); + } + + return default!; + } + + if (type == null) + { + return FallbackAccessor; + } + + if (!typeof(T).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) + { + throw new InvalidOperationException(); + } + + var method = type.GetTypeInfo().GetDeclaredMethod(methodName); + if (method == null) + { + return FallbackAccessor; + } + + if (!typeof(TValue).GetTypeInfo().IsAssignableFrom(method.ReturnType.GetTypeInfo())) + { + if (method.ReturnType.GetTypeInfo().IsEnum + && typeof(TValue).GetTypeInfo().IsEnum + && Enum.GetUnderlyingType(typeof(TValue)).GetTypeInfo().IsAssignableFrom(Enum.GetUnderlyingType(method.ReturnType).GetTypeInfo())) + { + // Allow this + } + else + { + throw new InvalidOperationException(); + } + } + + var parameter = Expression.Parameter(typeof(T), parameterName); + var argument = Expression.Parameter(typeof(TArg), argumentName); + Expression instance = + type.GetTypeInfo().IsAssignableFrom(typeof(T).GetTypeInfo()) + ? (Expression)parameter + : Expression.Convert(parameter, type); + Expression convertedArgument = + argumentType.GetTypeInfo().IsAssignableFrom(typeof(TArg).GetTypeInfo()) + ? (Expression)argument + : Expression.Convert(argument, type); + + Expression result = Expression.Call(instance, method, convertedArgument); + if (!typeof(TValue).GetTypeInfo().IsAssignableFrom(method.ReturnType.GetTypeInfo())) + { + result = Expression.Convert(result, typeof(TValue)); + } + + Expression> expression = Expression.Lambda>(result, parameter, argument); + return expression.Compile(); + } + } +} diff --git a/src/Utilities/Compiler/Lightup/NullableAnnotation.cs b/src/Utilities/Compiler/Lightup/NullableAnnotation.cs new file mode 100644 index 0000000000..eded530ffd --- /dev/null +++ b/src/Utilities/Compiler/Lightup/NullableAnnotation.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics.CodeAnalysis; + +namespace Analyzer.Utilities.Lightup +{ + /// + /// Represents the nullability of values that can be assigned + /// to an expression used as an lvalue. + /// + [SuppressMessage("Design", "CA1028:Enum Storage should be Int32", Justification = "Underlying type must match the original underlying type.")] + internal enum NullableAnnotation : byte + { + /// + /// The expression has not been analyzed, or the syntax is + /// not an expression (such as a statement). + /// + /// + /// There are a few different reasons the expression could + /// have not been analyzed: + /// 1) The symbol producing the expression comes from + /// a method that has not been annotated, such as + /// invoking a C# 7.3 or earlier method, or a + /// method in this compilation that is in a disabled + /// context. + /// 2) Nullable is completely disabled in this + /// compilation. + /// + None = 0, + + /// + /// The expression is not annotated (does not have a ?). + /// + NotAnnotated, + + /// + /// The expression is annotated (does have a ?). + /// + Annotated, + } +} diff --git a/src/Utilities/Compiler/Lightup/NullableContext.cs b/src/Utilities/Compiler/Lightup/NullableContext.cs new file mode 100644 index 0000000000..50d35a0612 --- /dev/null +++ b/src/Utilities/Compiler/Lightup/NullableContext.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Analyzer.Utilities.Lightup +{ + /// + /// Represents the state of the nullable analysis at a specific point in a file. Bits one and + /// two correspond to whether the nullable feature is enabled. Bits three and four correspond + /// to whether the context was inherited from the global context. + /// + [Flags] + [SuppressMessage("Naming", "CA1714:Flags enums should have plural names", Justification = "Intentionally uses the name from Roslyn.")] + internal enum NullableContext + { + /// + /// Nullable warnings and annotations are explicitly turned off at this location. + /// + Disabled = 0, + + /// + /// Nullable warnings are enabled and will be reported at this file location. + /// + WarningsEnabled = 1, + + /// + /// Nullable annotations are enabled and will be shown when APIs defined at + /// this location are used in other contexts. + /// + AnnotationsEnabled = 1 << 1, + + /// + /// The nullable feature is fully enabled. + /// + Enabled = WarningsEnabled | AnnotationsEnabled, + + /// + /// The nullable warning state is inherited from the project default. + /// + /// + /// The project default can change depending on the file type. Generated + /// files have nullable off by default, regardless of of the project-level + /// default setting. + /// + WarningsContextInherited = 1 << 2, + + /// + /// The nullable annotation state is inherited from the project default. + /// + /// + /// The project default can change depending on the file type. Generated + /// files have nullable off by default, regardless of of the project-level + /// default setting. + /// + AnnotationsContextInherited = 1 << 3, + + /// + /// The current state of both warnings and annotations are inherited from + /// the project default. + /// + /// + /// This flag is set by default at the start of all files. + /// + /// The project default can change depending on the file type. Generated + /// files have nullable off by default, regardless of of the project-level + /// default setting. + /// + ContextInherited = WarningsContextInherited | AnnotationsContextInherited + } +} diff --git a/src/Utilities/Compiler/Lightup/NullableContextExtensions.cs b/src/Utilities/Compiler/Lightup/NullableContextExtensions.cs new file mode 100644 index 0000000000..7446ea35b5 --- /dev/null +++ b/src/Utilities/Compiler/Lightup/NullableContextExtensions.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Analyzer.Utilities.Lightup +{ + internal static class NullableContextExtensions + { + private static bool IsFlagSet(NullableContext context, NullableContext flag) => + (context & flag) == flag; + + /// + /// Returns whether nullable warnings are enabled for this context. + /// + public static bool WarningsEnabled(this NullableContext context) => + IsFlagSet(context, NullableContext.WarningsEnabled); + + /// + /// Returns whether nullable annotations are enabled for this context. + /// + public static bool AnnotationsEnabled(this NullableContext context) => + IsFlagSet(context, NullableContext.AnnotationsEnabled); + + /// + /// Returns whether the nullable warning state was inherited from the project default for this file type. + /// + public static bool WarningsInherited(this NullableContext context) => + IsFlagSet(context, NullableContext.WarningsContextInherited); + + /// + /// Returns whether the nullable annotation state was inherited from the project default for this file type. + /// + public static bool AnnotationsInherited(this NullableContext context) => + IsFlagSet(context, NullableContext.AnnotationsContextInherited); + } +} diff --git a/src/Utilities/Compiler/Lightup/SemanticModelExtensions.cs b/src/Utilities/Compiler/Lightup/SemanticModelExtensions.cs new file mode 100644 index 0000000000..854817688f --- /dev/null +++ b/src/Utilities/Compiler/Lightup/SemanticModelExtensions.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.CodeAnalysis; + +namespace Analyzer.Utilities.Lightup +{ + internal static class SemanticModelExtensions + { + private static readonly Func s_getNullableContext + = LightupHelpers.CreateAccessorWithArgument(typeof(SemanticModel), "semanticModel", typeof(int), "position", nameof(GetNullableContext)); + + public static NullableContext GetNullableContext(this SemanticModel semanticModel, int position) + => s_getNullableContext(semanticModel, position); + } +}