Skip to content

Commit

Permalink
Add lightup support for nullable reference types
Browse files Browse the repository at this point in the history
  • Loading branch information
sharwell committed Mar 4, 2020
1 parent 6ca97fd commit e078c85
Show file tree
Hide file tree
Showing 8 changed files with 447 additions and 1 deletion.
30 changes: 29 additions & 1 deletion THIRD-PARTY-NOTICES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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.
6 changes: 6 additions & 0 deletions src/Utilities/Compiler/Analyzer.Utilities.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@
<Compile Include="$(MSBuildThisFileDirectory)DisposeMethodKind.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\OperationBlockAnalysisContextExtension.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\WellKnownDiagnosticTagsExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Lightup\ITypeSymbolExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Lightup\LightupHelpers.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Lightup\NullableAnnotation.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Lightup\NullableContext.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Lightup\NullableContextExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Lightup\SemanticModelExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Options\EnumValuesPrefixTrigger.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Options\SymbolNamesWithValueOption.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Options\SymbolModifiers.cs" />
Expand Down
21 changes: 21 additions & 0 deletions src/Utilities/Compiler/Lightup/ITypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
@@ -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<ITypeSymbol, NullableAnnotation> s_nullableAnnotation
= LightupHelpers.CreateSymbolPropertyAccessor<ITypeSymbol, NullableAnnotation>(typeof(ITypeSymbol), nameof(NullableAnnotation));
private static readonly Func<ITypeSymbol, NullableAnnotation, ITypeSymbol> s_withNullableAnnotation
= LightupHelpers.CreateSymbolWithPropertyAccessor<ITypeSymbol, NullableAnnotation>(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);
}
}
229 changes: 229 additions & 0 deletions src/Utilities/Compiler/Lightup/LightupHelpers.cs
Original file line number Diff line number Diff line change
@@ -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<TSyntax, TProperty> CreateSyntaxPropertyAccessor<TSyntax, TProperty>(Type type, string propertyName)
where TSyntax : SyntaxNode
=> CreatePropertyAccessor<TSyntax, TProperty>(type, "syntax", propertyName);

internal static Func<TSymbol, TProperty> CreateSymbolPropertyAccessor<TSymbol, TProperty>(Type type, string propertyName)
where TSymbol : ISymbol
=> CreatePropertyAccessor<TSymbol, TProperty>(type, "symbol", propertyName);

private static Func<T, TProperty> CreatePropertyAccessor<T, TProperty>(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<Func<T, TProperty>> expression = Expression.Lambda<Func<T, TProperty>>(result, parameter);
return expression.Compile();
}

internal static Func<TSyntax, TProperty, TSyntax> CreateSyntaxWithPropertyAccessor<TSyntax, TProperty>(Type type, string propertyName)
where TSyntax : SyntaxNode
=> CreateWithPropertyAccessor<TSyntax, TProperty>(type, "syntax", propertyName);

internal static Func<TSymbol, TProperty, TSymbol> CreateSymbolWithPropertyAccessor<TSymbol, TProperty>(Type type, string propertyName)
where TSymbol : ISymbol
=> CreateWithPropertyAccessor<TSymbol, TProperty>(type, "symbol", propertyName);

private static Func<T, TProperty, T> CreateWithPropertyAccessor<T, TProperty>(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<Func<T, TProperty, T>> expression =
Expression.Lambda<Func<T, TProperty, T>>(
Expression.Call(instance, methodInfo, value),
parameter,
valueParameter);
return expression.Compile();
}

internal static Func<T, TArg, TValue> CreateAccessorWithArgument<T, TArg, TValue>(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<Func<T, TArg, TValue>> expression = Expression.Lambda<Func<T, TArg, TValue>>(result, parameter, argument);
return expression.Compile();
}
}
}
41 changes: 41 additions & 0 deletions src/Utilities/Compiler/Lightup/NullableAnnotation.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Represents the nullability of values that can be assigned
/// to an expression used as an lvalue.
/// </summary>
[SuppressMessage("Design", "CA1028:Enum Storage should be Int32", Justification = "Underlying type must match the original underlying type.")]
internal enum NullableAnnotation : byte
{
/// <summary>
/// The expression has not been analyzed, or the syntax is
/// not an expression (such as a statement).
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
None = 0,

/// <summary>
/// The expression is not annotated (does not have a ?).
/// </summary>
NotAnnotated,

/// <summary>
/// The expression is annotated (does have a ?).
/// </summary>
Annotated,
}
}
Loading

0 comments on commit e078c85

Please sign in to comment.