diff --git a/ExhaustiveMatching.Analyzer.Enums/Analysis/SwitchOnEnumAnalyzer.cs b/ExhaustiveMatching.Analyzer.Enums/Analysis/SwitchOnEnumAnalyzer.cs index 540ee3a..d137333 100644 --- a/ExhaustiveMatching.Analyzer.Enums/Analysis/SwitchOnEnumAnalyzer.cs +++ b/ExhaustiveMatching.Analyzer.Enums/Analysis/SwitchOnEnumAnalyzer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using ExhaustiveMatching.Analyzer.Enums.Semantics; using ExhaustiveMatching.Analyzer.Enums.Utility; @@ -26,14 +27,19 @@ public static IEnumerable UnusedEnumValues( // SortedSet. Both of those use more memory and have more overhead. Hash of primitive // types is not normally well distributed. It is expected that the values used will // rarely contain duplicates. - var valuesUsed = caseExpressions.Select(e => GetEnumCaseValue(context, e, enumType)) - .WhereNotNull().ToArray(); + var valuesUsed = caseExpressions + .Select(e => GetEnumCaseValue(context, e, enumType)) + .WhereNotNull() + .ToArray(); + Array.Sort(valuesUsed); var allSymbols = enumType.GetMembers().OfType(); - // Use where instead of Except because we have a set - return allSymbols.Where(s => !SortedArrayContains(valuesUsed, s.ConstantValue)); + // Use `Where` instead of `Except` because we have a set + return allSymbols + .Where(s => s.ConstantValue != null) + .Where(s => !SortedArrayContains(valuesUsed, s.ConstantValue!)); } /// @@ -42,16 +48,16 @@ public static IEnumerable UnusedEnumValues( /// Case expressions can contain errors. They can also be various forms of literal /// zero where the type won't match the underlying type of the enum. This deals with all /// of that. - private static object GetEnumCaseValue( + private static object? GetEnumCaseValue( SyntaxNodeAnalysisContext context, ExpressionSyntax expression, INamedTypeSymbol enumType) { - var underlyingType = enumType.EnumUnderlyingType.SpecialType; + var underlyingType = enumType.EnumUnderlyingType?.SpecialType ?? SpecialType.None; return GetEnumCaseValue(context.SemanticModel, expression, underlyingType.ToTypeCode()); } - private static object GetEnumCaseValue( + private static object? GetEnumCaseValue( SemanticModel semanticModel, ExpressionSyntax expression, TypeCode typeCode) @@ -78,12 +84,12 @@ private static object GetEnumCaseValue( /// There seems to be no built in way to try a conversion. Without writing /// custom converter code for every pair of types, the only option is to catch the exception /// from - private static bool TryChangeType(object value, TypeCode typeCode, out object converted) + private static bool TryChangeType(object value, TypeCode typeCode, [NotNullWhen(true)] out object? converted) { try { converted = Convert.ChangeType(value, typeCode); - return true; + return converted != null; } catch { diff --git a/ExhaustiveMatching.Analyzer.Enums/ExhaustiveMatchEnumAnalyzer.cs b/ExhaustiveMatching.Analyzer.Enums/ExhaustiveMatchEnumAnalyzer.cs index 9750e54..a535245 100644 --- a/ExhaustiveMatching.Analyzer.Enums/ExhaustiveMatchEnumAnalyzer.cs +++ b/ExhaustiveMatching.Analyzer.Enums/ExhaustiveMatchEnumAnalyzer.cs @@ -17,8 +17,7 @@ public override ImmutableArray SupportedDiagnostics public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze - | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.RegisterSyntaxNodeAction(AnalyzeSwitchStatement, SyntaxKind.SwitchStatement); //context.RegisterSyntaxNodeAction(AnalyzeSwitchExpression, SyntaxKind.SwitchExpression); } @@ -37,7 +36,7 @@ private void AnalyzeSwitchStatement(SyntaxNodeAnalysisContext context) // Include stack trace info by ToString() the exception as part of the message. // Only the first line is included, so we have to remove newlines var exDetails = Regex.Replace(ex.ToString(), @"\r\n?|\n|\r", " "); - throw new Exception($"Uncaught exception in analyzer: {exDetails}"); + throw new Exception($"Uncaught exception in analyzer: {exDetails}", innerException: ex); } } } diff --git a/ExhaustiveMatching.Analyzer.Enums/ExhaustiveMatching.Analyzer.Enums.csproj b/ExhaustiveMatching.Analyzer.Enums/ExhaustiveMatching.Analyzer.Enums.csproj index 62a1cce..1cf699b 100644 --- a/ExhaustiveMatching.Analyzer.Enums/ExhaustiveMatching.Analyzer.Enums.csproj +++ b/ExhaustiveMatching.Analyzer.Enums/ExhaustiveMatching.Analyzer.Enums.csproj @@ -1,11 +1,13 @@ - netstandard2.0 + netstandard2.0;netstandard2.1; + 8.0 + enable - + diff --git a/ExhaustiveMatching.Analyzer.Enums/Semantics/SyntaxNodeAnalysisContextExtensions.cs b/ExhaustiveMatching.Analyzer.Enums/Semantics/SyntaxNodeAnalysisContextExtensions.cs index 291cb9c..9824394 100644 --- a/ExhaustiveMatching.Analyzer.Enums/Semantics/SyntaxNodeAnalysisContextExtensions.cs +++ b/ExhaustiveMatching.Analyzer.Enums/Semantics/SyntaxNodeAnalysisContextExtensions.cs @@ -9,7 +9,7 @@ public static class SyntaxNodeAnalysisContextExtensions /// /// Get the type of an expression. /// - public static ITypeSymbol GetExpressionType( + public static ITypeSymbol? GetExpressionType( this SyntaxNodeAnalysisContext context, ExpressionSyntax switchStatementExpression) => context.SemanticModel.GetTypeInfo(switchStatementExpression, context.CancellationToken).Type; @@ -17,12 +17,12 @@ public static ITypeSymbol GetExpressionType( /// /// Get the converted type of an expression. /// - public static ITypeSymbol GetExpressionConvertedType( + public static ITypeSymbol? GetExpressionConvertedType( this SyntaxNodeAnalysisContext context, ExpressionSyntax switchStatementExpression) => context.SemanticModel.GetTypeInfo(switchStatementExpression, context.CancellationToken).ConvertedType; - public static ISymbol GetSymbol(this SyntaxNodeAnalysisContext context, ExpressionSyntax expression) + public static ISymbol? GetSymbol(this SyntaxNodeAnalysisContext context, ExpressionSyntax expression) => context.SemanticModel.GetSymbolInfo(expression, context.CancellationToken).Symbol; } } diff --git a/ExhaustiveMatching.Analyzer.Enums/Semantics/TypeSymbolExtensions.cs b/ExhaustiveMatching.Analyzer.Enums/Semantics/TypeSymbolExtensions.cs index 13384f2..7244c1f 100644 --- a/ExhaustiveMatching.Analyzer.Enums/Semantics/TypeSymbolExtensions.cs +++ b/ExhaustiveMatching.Analyzer.Enums/Semantics/TypeSymbolExtensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.ComponentModel; using System.Linq; using Microsoft.CodeAnalysis; @@ -20,7 +21,7 @@ public static bool IsInvalidEnumArgumentException(this ITypeSymbol typeSymbol) public static bool IsEnum( this ITypeSymbol type, SyntaxNodeAnalysisContext context, - out INamedTypeSymbol enumType, + [NotNullWhen(true)] out INamedTypeSymbol? enumType, out bool nullable) { switch (type.TypeKind) @@ -31,7 +32,7 @@ public static bool IsEnum( return true; case TypeKind.Struct: var nullableType = context.Compilation.GetTypeByMetadataName(TypeNames.Nullable); - if (type.OriginalDefinition.Equals(nullableType)) + if (SymbolEqualityComparer.IncludeNullability.Equals(type.OriginalDefinition, nullableType)) { type = ((INamedTypeSymbol)type).TypeArguments.Single(); if (type.TypeKind == TypeKind.Enum) diff --git a/ExhaustiveMatching.Analyzer.Enums/Syntax/ThrowStatementSyntaxExtensions.cs b/ExhaustiveMatching.Analyzer.Enums/Syntax/ThrowStatementSyntaxExtensions.cs index 34e6ef5..cfac750 100644 --- a/ExhaustiveMatching.Analyzer.Enums/Syntax/ThrowStatementSyntaxExtensions.cs +++ b/ExhaustiveMatching.Analyzer.Enums/Syntax/ThrowStatementSyntaxExtensions.cs @@ -8,12 +8,13 @@ namespace ExhaustiveMatching.Analyzer.Enums.Syntax public static class ThrowStatementSyntaxExtensions { /// - /// The type of the expression being thrown. + /// Returns the for type being thrown by , if determinable, otherwise . /// /// The type being thrown or if it can't /// be determined (e.g. compile error). - public static ITypeSymbol ThrowsType(this ThrowStatementSyntax throwStatement, SyntaxNodeAnalysisContext context) + public static ITypeSymbol? ThrowsType(this ThrowStatementSyntax throwStatement, SyntaxNodeAnalysisContext context) { + if (throwStatement.Expression is null) return null; var exceptionType = context.GetExpressionType(throwStatement.Expression); if (exceptionType == null || exceptionType is IErrorTypeSymbol) return null; diff --git a/ExhaustiveMatching.Analyzer.Enums/Utility/EnumerableExtensions.cs b/ExhaustiveMatching.Analyzer.Enums/Utility/EnumerableExtensions.cs index 678b3b3..a57764c 100644 --- a/ExhaustiveMatching.Analyzer.Enums/Utility/EnumerableExtensions.cs +++ b/ExhaustiveMatching.Analyzer.Enums/Utility/EnumerableExtensions.cs @@ -5,14 +5,8 @@ namespace ExhaustiveMatching.Analyzer.Enums.Utility { public static class EnumerableExtensions { - public static HashSet ToHashSet(this IEnumerable values) - => new HashSet(values); + public static IReadOnlyList ToReadOnlyList(this IEnumerable values) => values.ToList(); - public static IReadOnlyList ToReadOnlyList(this IEnumerable values) - => values.ToList().AsReadOnly(); - - public static IEnumerable WhereNotNull(this IEnumerable values) - where T : class - => values.Where(v => v != null); + public static IEnumerable WhereNotNull(this IEnumerable values) where T : class => values.Where(v => v != null)!; } } diff --git a/ExhaustiveMatching.Analyzer.Enums/Utility/NullableAttributes.cs b/ExhaustiveMatching.Analyzer.Enums/Utility/NullableAttributes.cs new file mode 100644 index 0000000..cce731e --- /dev/null +++ b/ExhaustiveMatching.Analyzer.Enums/Utility/NullableAttributes.cs @@ -0,0 +1,209 @@ +// https://www.meziantou.net/how-to-use-nullable-reference-types-in-dotnet-standard-2-0-and-dotnet-.htm +// https://github.com/dotnet/runtime/blob/527f9ae88a0ee216b44d556f9bdc84037fe0ebda/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/NullableAttributes.cs + +#pragma warning disable +#define INTERNAL_NULLABLE_ATTRIBUTES + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics.CodeAnalysis +{ +#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 + /// Specifies that null is allowed as an input even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class AllowNullAttribute : Attribute + { } + + /// Specifies that null is disallowed as an input even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class DisallowNullAttribute : Attribute + { } + + /// 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)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class MaybeNullAttribute : Attribute + { } + + /// 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. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class NotNullAttribute : Attribute + { } + + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + 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; } + } + + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + 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; } + } + + /// 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)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + 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; } + } + + /// Applied to a method that will never return under any circumstance. + [AttributeUsage(AttributeTargets.Method, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class DoesNotReturnAttribute : Attribute + { } + + /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + 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; } + } +#endif + +#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 + /// 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)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + 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; } + } + + /// 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)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + 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; } + } +#endif +} diff --git a/ExhaustiveMatching.Analyzer.nuspec b/ExhaustiveMatching.Analyzer.nuspec index 7a1d633..857b7ed 100644 --- a/ExhaustiveMatching.Analyzer.nuspec +++ b/ExhaustiveMatching.Analyzer.nuspec @@ -2,8 +2,8 @@ ExhaustiveMatching.Analyzer - 0.5.0 - Jeff Walker + 0.6.0-dev + Jeff Walker + Contributors Jeff Walker BSD-3-Clause https://github.com/WalkerCodeRanger/ExhaustiveMatching @@ -20,7 +20,7 @@ ExhaustiveMatching.Analyzer goes beyond what other languages support by handling hierarchies. - Copyright 2019 Jeff Walker + Copyright 2019-2023 Jeff Walker, Contributors analyzers, switch, exhaustive, match, discriminated, union, sum-type diff --git a/ExhaustiveMatching.Analyzer/ExhaustiveMatching.Analyzer.csproj b/ExhaustiveMatching.Analyzer/ExhaustiveMatching.Analyzer.csproj index 942bb6e..fe6fa08 100644 --- a/ExhaustiveMatching.Analyzer/ExhaustiveMatching.Analyzer.csproj +++ b/ExhaustiveMatching.Analyzer/ExhaustiveMatching.Analyzer.csproj @@ -1,7 +1,9 @@ - + - netstandard2.0 + netstandard2.0;netstandard2.1; + 8.0 + enable false 0.5.0.0 0.5.0.0 diff --git a/ExhaustiveMatching.Analyzer/ExpressionAnalyzer.cs b/ExhaustiveMatching.Analyzer/ExpressionAnalyzer.cs index b7bd915..8b6d272 100644 --- a/ExhaustiveMatching.Analyzer/ExpressionAnalyzer.cs +++ b/ExhaustiveMatching.Analyzer/ExpressionAnalyzer.cs @@ -1,3 +1,5 @@ +using ExhaustiveMatching.Analyzer.Semantics; + using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -13,7 +15,7 @@ public static SwitchStatementKind SwitchStatementKindForThrown( { var exceptionType = context.SemanticModel.GetTypeInfo(thrownExpression, context.CancellationToken).Type; if (exceptionType == null || exceptionType.TypeKind == TypeKind.Error) - return new SwitchStatementKind(false, false); + return new SwitchStatementKind(isExhaustive: false, throwsInvalidEnum: false); // TODO GetTypeByMetadataName returns null if multiple types match. This isn't the way to do this var exhaustiveMatchFailedExceptionType = @@ -21,8 +23,8 @@ public static SwitchStatementKind SwitchStatementKindForThrown( var invalidEnumArgumentExceptionType = context.Compilation.GetTypeByMetadataName(TypeNames.InvalidEnumArgumentException); - var isExhaustiveMatchFailedException = exceptionType.Equals(exhaustiveMatchFailedExceptionType, SymbolEqualityComparer.IncludeNullability); - var isInvalidEnumArgumentException = exceptionType.Equals(invalidEnumArgumentExceptionType, SymbolEqualityComparer.IncludeNullability); + var isExhaustiveMatchFailedException = exceptionType.EqualsConsideringNullability(exhaustiveMatchFailedExceptionType); + var isInvalidEnumArgumentException = exceptionType.EqualsConsideringNullability(invalidEnumArgumentExceptionType); var isExhaustive = isExhaustiveMatchFailedException || isInvalidEnumArgumentException; return new SwitchStatementKind(isExhaustive, isInvalidEnumArgumentException); diff --git a/ExhaustiveMatching.Analyzer/PatternAnalyzer.cs b/ExhaustiveMatching.Analyzer/PatternAnalyzer.cs index 7655ecf..ae4c05a 100644 --- a/ExhaustiveMatching.Analyzer/PatternAnalyzer.cs +++ b/ExhaustiveMatching.Analyzer/PatternAnalyzer.cs @@ -9,7 +9,7 @@ namespace ExhaustiveMatching.Analyzer { internal static class PatternAnalyzer { - public static ITypeSymbol GetMatchedTypeSymbol( + public static ITypeSymbol? GetMatchedTypeSymbol( this SwitchLabelSyntax switchLabel, SyntaxNodeAnalysisContext context, ITypeSymbol type, @@ -27,14 +27,14 @@ public static ITypeSymbol GetMatchedTypeSymbol( } } - public static ITypeSymbol GetMatchedTypeSymbol( + public static ITypeSymbol? GetMatchedTypeSymbol( this PatternSyntax pattern, SyntaxNodeAnalysisContext context, ITypeSymbol type, HashSet allCases, bool isClosed) { - ITypeSymbol symbolUsed; + ITypeSymbol? symbolUsed; switch (pattern) { case DeclarationPatternSyntax declarationPattern: @@ -55,7 +55,7 @@ when constantPattern.IsNullPattern(): return null; } - if (isClosed && !allCases.Contains(symbolUsed)) + if (isClosed && symbolUsed != null && !allCases.Contains(symbolUsed)) { var diagnostic = Diagnostic.Create(Diagnostics.MatchMustBeOnCaseType, pattern.GetLocation(), symbolUsed.GetFullName(), type.GetFullName()); diff --git a/ExhaustiveMatching.Analyzer/Semantics/SymbolExtensions.cs b/ExhaustiveMatching.Analyzer/Semantics/SymbolExtensions.cs index 5552835..f62b218 100644 --- a/ExhaustiveMatching.Analyzer/Semantics/SymbolExtensions.cs +++ b/ExhaustiveMatching.Analyzer/Semantics/SymbolExtensions.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + using Microsoft.CodeAnalysis; namespace ExhaustiveMatching.Analyzer.Semantics @@ -9,5 +11,17 @@ public static string GetFullName(this ISymbol symbol) var ns = symbol.ContainingNamespace; return ns != null && !ns.IsGlobalNamespace ? $"{ns.GetFullName()}.{symbol.Name}" : symbol.Name; } + + /// Compares using , in accordance with RS1024. + public static bool EqualsDisregardingNullability(this ISymbol symbol, [NotNullWhen(true)] ISymbol? other) + { + return SymbolEqualityComparer.Default.Equals(symbol, other); + } + + /// Compares using , in accordance with RS1024. + public static bool EqualsConsideringNullability(this ISymbol symbol, [NotNullWhen(true)] ISymbol? other) + { + return SymbolEqualityComparer.IncludeNullability.Equals(symbol, other); + } } } diff --git a/ExhaustiveMatching.Analyzer/Semantics/SyntaxNodeAnalysisContextExtensions.cs b/ExhaustiveMatching.Analyzer/Semantics/SyntaxNodeAnalysisContextExtensions.cs index a62d4b5..134d4cc 100644 --- a/ExhaustiveMatching.Analyzer/Semantics/SyntaxNodeAnalysisContextExtensions.cs +++ b/ExhaustiveMatching.Analyzer/Semantics/SyntaxNodeAnalysisContextExtensions.cs @@ -6,7 +6,7 @@ namespace ExhaustiveMatching.Analyzer.Semantics { public static class SyntaxNodeAnalysisContextExtensions { - public static ISymbol GetSymbol(this SyntaxNodeAnalysisContext context, AttributeSyntax attribute) => + public static ISymbol? GetSymbol(this SyntaxNodeAnalysisContext context, AttributeSyntax attribute) => context.SemanticModel.GetSymbolInfo(attribute, context.CancellationToken).Symbol; } } diff --git a/ExhaustiveMatching.Analyzer/Semantics/TypeSymbolExtensions.cs b/ExhaustiveMatching.Analyzer/Semantics/TypeSymbolExtensions.cs index ac16e10..2b34f00 100644 --- a/ExhaustiveMatching.Analyzer/Semantics/TypeSymbolExtensions.cs +++ b/ExhaustiveMatching.Analyzer/Semantics/TypeSymbolExtensions.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using ExhaustiveMatching.Analyzer.Utility; using Microsoft.CodeAnalysis; @@ -11,12 +12,12 @@ internal static class TypeSymbolExtensions { public static bool IsSubtypeOf(this ITypeSymbol symbol, ITypeSymbol type) { - return symbol.Equals(type) || symbol.Implements(type) || symbol.InheritsFrom(type); + return symbol.EqualsDisregardingNullability(type) || symbol.Implements(type) || symbol.InheritsFrom(type); } public static bool IsDirectSubtypeOf(this ITypeSymbol symbol, ITypeSymbol type) { - return symbol.DirectlyImplements(type) || Equals(symbol.BaseType, type); + return symbol.DirectlyImplements(type) || type.EqualsDisregardingNullability(symbol.BaseType); } public static bool DirectlyImplements(this ITypeSymbol symbol, ITypeSymbol type) @@ -41,7 +42,7 @@ public static IEnumerable BaseClasses(this ITypeSymbol symbol) public static bool InheritsFrom(this ITypeSymbol symbol, ITypeSymbol type) { - return symbol.BaseClasses().Any(t => t.Equals(type)); + return symbol.BaseClasses().Any(t => t.EqualsDisregardingNullability(type)); } public static bool IsDirectSubtypeOfTypeWithAttribute( @@ -54,7 +55,7 @@ public static bool IsDirectSubtypeOfTypeWithAttribute( public static bool HasAttribute(this ITypeSymbol symbol, INamedTypeSymbol attributeType) { - return symbol.GetAttributes().Any(a => a.AttributeClass.Equals(attributeType)); + return symbol.GetAttributes().Any(a => attributeType.EqualsDisregardingNullability(a.AttributeClass)); } public static IEnumerable GetCaseTypeSyntaxes( @@ -62,9 +63,10 @@ public static IEnumerable GetCaseTypeSyntaxes( INamedTypeSymbol closedAttributeType) { return type.GetAttributes() - .Where(attr => attr.AttributeClass.Equals(closedAttributeType)) - .Select(attr => attr.ApplicationSyntaxReference.GetSyntax()).Cast() - .SelectMany(attr => attr.ArgumentList.Arguments) + .Where(attr => attr.AttributeClass != null && attr.ApplicationSyntaxReference != null) + .Where(attr => closedAttributeType.EqualsDisregardingNullability(attr.AttributeClass)) + .Select(attr => attr.ApplicationSyntaxReference!.GetSyntax()).Cast() + .SelectMany(attr => attr.ArgumentList?.Arguments ?? default) .Select(arg => arg.Expression) .OfType() .Select(s => s.Type); @@ -78,7 +80,7 @@ public static IEnumerable GetCaseTypes( INamedTypeSymbol closedAttributeType) { return type.GetAttributes() - .Where(a => a.AttributeClass.Equals(closedAttributeType)) + .Where(a => closedAttributeType.EqualsDisregardingNullability(a.AttributeClass)) .SelectMany(a => a.ConstructorArguments) .SelectMany(GetTypeConstants) .Select(arg => arg.Value) @@ -150,7 +152,7 @@ public static HashSet GetClosedTypeCases( || !caseType.IsSubtypeOf(rootType)) continue; - types.Add(caseType); + _ = types.Add(caseType); var caseTypes = caseType.GetValidCaseTypes(closedAttributeType); @@ -185,8 +187,13 @@ public static bool TryGetStructurallyClosedTypeCases(this ITypeSymbol rootType, if (rootType is INamedTypeSymbol namedType && rootType.TypeKind != TypeKind.Error && namedType.InstanceConstructors - .All(c => c.DeclaredAccessibility == Accessibility.Private - || rootType.IsRecord && c.DeclaredAccessibility == Accessibility.Protected && c.Parameters.Length == 1 && SymbolEqualityComparer.Default.Equals(c.Parameters[0].Type, rootType))) + .All(c => + c.DeclaredAccessibility == Accessibility.Private + || (rootType.IsRecord + && c.DeclaredAccessibility == Accessibility.Protected + && c.Parameters.Length == 1 + && rootType.EqualsDisregardingNullability(c.Parameters[0].Type)) + )) { var nestedTypes = context.SemanticModel.LookupSymbols(0, rootType) @@ -196,7 +203,7 @@ public static bool TryGetStructurallyClosedTypeCases(this ITypeSymbol rootType, if (nestedTypes.All(t => t.IsSealed || t is INamedTypeSymbol n && n.InstanceConstructors.All(c => c.DeclaredAccessibility == Accessibility.Private))) { - allCases.Add(rootType); + _ = allCases.Add(rootType); allCases.UnionWith(nestedTypes); return true; diff --git a/ExhaustiveMatching.Analyzer/SwitchExpressionAnalyzer.cs b/ExhaustiveMatching.Analyzer/SwitchExpressionAnalyzer.cs index 787a693..a2bc041 100644 --- a/ExhaustiveMatching.Analyzer/SwitchExpressionAnalyzer.cs +++ b/ExhaustiveMatching.Analyzer/SwitchExpressionAnalyzer.cs @@ -2,6 +2,7 @@ using System.Linq; using ExhaustiveMatching.Analyzer.Enums.Analysis; using ExhaustiveMatching.Analyzer.Enums.Semantics; +using ExhaustiveMatching.Analyzer.Enums.Utility; using ExhaustiveMatching.Analyzer.Semantics; using ExhaustiveMatching.Analyzer.Syntax; using Microsoft.CodeAnalysis; @@ -12,9 +13,7 @@ namespace ExhaustiveMatching.Analyzer { internal static class SwitchExpressionAnalyzer { - public static void Analyze( - SyntaxNodeAnalysisContext context, - SwitchExpressionSyntax switchExpression) + public static void Analyze(SyntaxNodeAnalysisContext context, SwitchExpressionSyntax switchExpression) { var switchKind = IsExhaustive(context, switchExpression); if (!switchKind.IsExhaustive) return; @@ -22,14 +21,19 @@ public static void Analyze( ReportWhenGuardNotSupported(context, switchExpression); var switchOnType = context.GetExpressionConvertedType(switchExpression.GoverningExpression); + if (switchOnType != null) + { + if (switchOnType.IsEnum(context, out var enumType, out var nullable)) + { + AnalyzeSwitchOnEnum(context, switchExpression, enumType, nullable); + } + else if (!switchKind.ThrowsInvalidEnum) + { + AnalyzeSwitchOnClosed(context, switchExpression, switchOnType); + } + } - if (switchOnType != null - && switchOnType.IsEnum(context, out var enumType, out var nullable)) - AnalyzeSwitchOnEnum(context, switchExpression, enumType, nullable); - else if (!switchKind.ThrowsInvalidEnum) - AnalyzeSwitchOnClosed(context, switchExpression, switchOnType); - - // TODO report warning that throws invalid enum isn't checked for exhaustiveness + // TODO report warning that `throws ` isn't checked for exhaustiveness } private static SwitchStatementKind IsExhaustive( @@ -80,6 +84,8 @@ private static void AnalyzeSwitchOnClosed( var patterns = switchExpression.Arms.Select(a => a.Pattern).ToList(); var closedAttributeType = context.GetClosedAttributeType(); + if (closedAttributeType is null) return; + var isClosed = type.HasAttribute(closedAttributeType); var allCases = type.GetClosedTypeCases(closedAttributeType); @@ -94,8 +100,8 @@ private static void AnalyzeSwitchOnClosed( } var typesUsed = patterns - .Select(pattern => pattern.GetMatchedTypeSymbol(context, type, allCases, isClosed)) - .Where(t => t != null) // returns null for invalid case clauses + .Select(pattern => pattern.GetMatchedTypeSymbol(context, type, allCases, isClosed)!) // returns null for invalid case clauses + .WhereNotNull() .ToImmutableHashSet(); // If it is an open type, we don't want to actually check for uncovered types, but diff --git a/ExhaustiveMatching.Analyzer/SwitchStatementAnalyzer.cs b/ExhaustiveMatching.Analyzer/SwitchStatementAnalyzer.cs index 7977d9b..fadc7c6 100644 --- a/ExhaustiveMatching.Analyzer/SwitchStatementAnalyzer.cs +++ b/ExhaustiveMatching.Analyzer/SwitchStatementAnalyzer.cs @@ -25,14 +25,19 @@ public static void Analyze( ReportWhenGuardNotSupported(context, switchStatement); var switchOnType = context.GetExpressionConvertedType(switchStatement.Expression); + if (switchOnType != null) + { + if (switchOnType.IsEnum(context, out var enumType, out var nullable)) + { + AnalyzeSwitchOnEnum(context, switchStatement, enumType, nullable); + } + else if (!switchKind.ThrowsInvalidEnum) + { + AnalyzeSwitchOnClosed(context, switchStatement, switchOnType); + } + } - if (switchOnType != null - && switchOnType.IsEnum(context, out var enumType, out var nullable)) - AnalyzeSwitchOnEnum(context, switchStatement, enumType, nullable); - else if (!switchKind.ThrowsInvalidEnum) - AnalyzeSwitchOnClosed(context, switchStatement, switchOnType); - - // TODO report warning that throws invalid enum isn't checked for exhaustiveness + // TODO report warning that `throws ` isn't checked for exhaustiveness } private static SwitchStatementKind IsExhaustive( @@ -42,16 +47,16 @@ private static SwitchStatementKind IsExhaustive( var defaultSection = switchStatement.Sections .FirstOrDefault(s => s.Labels.OfType().Any()); - var throwStatement = defaultSection?.Statements - .OfType().FirstOrDefault(); + var throwStatement = defaultSection?.Statements.OfType().FirstOrDefault(); // If there is no default section or it doesn't throw, we assume the // dev doesn't want an exhaustive match - if (throwStatement == null) - return new SwitchStatementKind(false, false); + if (throwStatement == null || throwStatement.Expression is null) + { + return new SwitchStatementKind(isExhaustive: false, throwsInvalidEnum: false); + } - return ExpressionAnalyzer.SwitchStatementKindForThrown(context, - throwStatement.Expression); + return ExpressionAnalyzer.SwitchStatementKindForThrown(context, throwStatement.Expression); } private static void ReportWhenGuardNotSupported( @@ -93,6 +98,8 @@ private static void AnalyzeSwitchOnClosed( CheckForNonPatternCases(context, switchLabels); var closedAttributeType = context.GetClosedAttributeType(); + if (closedAttributeType is null) return; + var isClosed = type.HasAttribute(closedAttributeType); var allCases = type.GetClosedTypeCases(closedAttributeType); @@ -107,8 +114,8 @@ private static void AnalyzeSwitchOnClosed( } var typesUsed = switchLabels - .Select(switchLabel => switchLabel.GetMatchedTypeSymbol(context, type, allCases, isClosed)) - .Where(t => t != null) // returns null for invalid case clauses + .Select(switchLabel => switchLabel.GetMatchedTypeSymbol(context, type, allCases, isClosed)) // returns null for invalid case clauses + .WhereNotNull() .ToImmutableHashSet(); // If it is an open type, we don't want to actually check for uncovered types, but diff --git a/ExhaustiveMatching.Analyzer/Syntax/ExpressionSyntaxExtensions.cs b/ExhaustiveMatching.Analyzer/Syntax/ExpressionSyntaxExtensions.cs index baad234..c4d8eb5 100644 --- a/ExhaustiveMatching.Analyzer/Syntax/ExpressionSyntaxExtensions.cs +++ b/ExhaustiveMatching.Analyzer/Syntax/ExpressionSyntaxExtensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using ExhaustiveMatching.Analyzer.Enums.Semantics; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -7,7 +8,7 @@ namespace ExhaustiveMatching.Analyzer.Syntax { public static class ExpressionSyntaxExtensions { - public static bool IsTypeIdentifier(this ExpressionSyntax expression, SyntaxNodeAnalysisContext context, out ITypeSymbol typeSymbol) + public static bool IsTypeIdentifier(this ExpressionSyntax expression, SyntaxNodeAnalysisContext context, [NotNullWhen(true)] out ITypeSymbol? typeSymbol) { if (context.GetSymbol(expression) is ITypeSymbol t) { diff --git a/ExhaustiveMatching.Analyzer/SyntaxNodeAnalysisContextExtensions.cs b/ExhaustiveMatching.Analyzer/SyntaxNodeAnalysisContextExtensions.cs index 5c6e426..6bad89d 100644 --- a/ExhaustiveMatching.Analyzer/SyntaxNodeAnalysisContextExtensions.cs +++ b/ExhaustiveMatching.Analyzer/SyntaxNodeAnalysisContextExtensions.cs @@ -25,9 +25,7 @@ public static void ReportNotExhaustiveObjectSwitch( #endregion #region Report Switch Diagnostics - public static void ReportCasePatternNotSupported( - this SyntaxNodeAnalysisContext context, - CaseSwitchLabelSyntax switchLabel) + public static void ReportCasePatternNotSupported(this SyntaxNodeAnalysisContext context, CaseSwitchLabelSyntax switchLabel) { var diagnostic = Diagnostic.Create( Diagnostics.CasePatternNotSupported, @@ -43,10 +41,7 @@ public static void ReportCasePatternNotSupported(this SyntaxNodeAnalysisContext context.ReportDiagnostic(diagnostic); } - public static void ReportOpenTypeNotSupported( - this SyntaxNodeAnalysisContext context, - ITypeSymbol type, - ExpressionSyntax switchStatementExpression) + public static void ReportOpenTypeNotSupported(this SyntaxNodeAnalysisContext context, ITypeSymbol type, ExpressionSyntax switchStatementExpression) { var diagnostic = Diagnostic.Create( Diagnostics.OpenTypeNotSupported, @@ -54,9 +49,7 @@ public static void ReportOpenTypeNotSupported( context.ReportDiagnostic(diagnostic); } - public static void ReportWhenClauseNotSupported( - this SyntaxNodeAnalysisContext context, - WhenClauseSyntax whenClause) + public static void ReportWhenClauseNotSupported(this SyntaxNodeAnalysisContext context, WhenClauseSyntax whenClause) { var diagnostic = Diagnostic.Create( Diagnostics.WhenGuardNotSupported, @@ -65,12 +58,10 @@ public static void ReportWhenClauseNotSupported( } #endregion - public static INamedTypeSymbol GetClosedAttributeType(this SyntaxNodeAnalysisContext context) + public static INamedTypeSymbol? GetClosedAttributeType(this SyntaxNodeAnalysisContext context) => context.Compilation.GetTypeByMetadataName(TypeNames.ClosedAttribute); - public static ITypeSymbol GetDeclarationType( - this SyntaxNodeAnalysisContext context, - DeclarationPatternSyntax declarationPattern) + public static ITypeSymbol? GetDeclarationType(this SyntaxNodeAnalysisContext context, DeclarationPatternSyntax declarationPattern) => context.SemanticModel.GetTypeInfo(declarationPattern.Type, context.CancellationToken).Type; } } diff --git a/ExhaustiveMatching.Analyzer/TypeDeclarationAnalyzer.cs b/ExhaustiveMatching.Analyzer/TypeDeclarationAnalyzer.cs index 51dd090..274916b 100644 --- a/ExhaustiveMatching.Analyzer/TypeDeclarationAnalyzer.cs +++ b/ExhaustiveMatching.Analyzer/TypeDeclarationAnalyzer.cs @@ -17,8 +17,10 @@ public static void Analyze( TypeDeclarationSyntax typeDeclaration) { var closedAttribute = context.Compilation.GetTypeByMetadataName(TypeNames.ClosedAttribute); + if (closedAttribute is null) return; - var typeSymbol = (ITypeSymbol)context.SemanticModel.GetDeclaredSymbol(typeDeclaration); + var typeSymbol = (ITypeSymbol?)context.SemanticModel.GetDeclaredSymbol(typeDeclaration); + if (typeSymbol is null) return; if (typeSymbol.IsDirectSubtypeOfTypeWithAttribute(closedAttribute)) MustBeCase(context, typeDeclaration, typeSymbol, closedAttribute); @@ -52,7 +54,7 @@ private static void MustBeCase( foreach (var superType in closedSuperTypes) { var isMember = superType.GetCaseTypes(closedAttribute) - .Any(t => t.Equals(typeSymbol)); + .Any(t => t.EqualsDisregardingNullability(typeSymbol)); if (isMember) continue; @@ -101,7 +103,7 @@ private static IList ClosedAttributes( { var constructorSymbol = context.GetSymbol(a); var attributeSymbol = constructorSymbol?.ContainingSymbol; - return closedAttribute.Equals(attributeSymbol); + return closedAttribute.EqualsDisregardingNullability(attributeSymbol); }).ToList(); return closedAttributes; @@ -120,14 +122,15 @@ private static void CheckClosedAttributes( } var typeSyntaxes = closedAttributes - .SelectMany(a => a.ArgumentList.Arguments) - .Select(arg => arg.Expression) - .OfType() - .Select(e => e.Type); + .Where(a => a.ArgumentList != null) + .SelectMany(a => a.ArgumentList!.Arguments) + .Select(arg => arg.Expression) + .OfType() + .Select(e => e.Type); var duplicates = typeSyntaxes - .GroupBy(t => context.GetSymbol(t)) - .SelectMany(g => g.Skip(1).Select(type => (g.Key, type))); + .GroupBy(t => context.GetSymbol(t)!) + .SelectMany(g => g.Skip(1).Select(type => (g.Key, type))); foreach (var (symbol, syntax) in duplicates) { @@ -170,12 +173,12 @@ private static void AllMemberTypesMustBeDirectSubtypes( var caseType = context.SemanticModel.GetTypeInfo(caseTypeSyntax).Type; if (caseType == null - || typeSymbol.Equals(caseType.BaseType) // BaseType is null for interfaces, avoid calling method on it - || caseType.Interfaces.Any(i => i.Equals(typeSymbol))) + || typeSymbol.EqualsDisregardingNullability(caseType.BaseType) // BaseType is null for interfaces, avoid calling method on it + || caseType.Interfaces.Any(i => i.EqualsDisregardingNullability(typeSymbol))) continue; if (caseType.InheritsFrom(typeSymbol) - || caseType.AllInterfaces.Any(i => i.Equals(typeSymbol))) + || caseType.AllInterfaces.Any(i => i.EqualsDisregardingNullability(typeSymbol))) { // It's a subtype, just not a direct one var diagnostic = Diagnostic.Create(Diagnostics.MustBeDirectSubtype, diff --git a/ExhaustiveMatching.Analyzer/Utility/NullableAttributes.cs b/ExhaustiveMatching.Analyzer/Utility/NullableAttributes.cs new file mode 100644 index 0000000..cce731e --- /dev/null +++ b/ExhaustiveMatching.Analyzer/Utility/NullableAttributes.cs @@ -0,0 +1,209 @@ +// https://www.meziantou.net/how-to-use-nullable-reference-types-in-dotnet-standard-2-0-and-dotnet-.htm +// https://github.com/dotnet/runtime/blob/527f9ae88a0ee216b44d556f9bdc84037fe0ebda/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/NullableAttributes.cs + +#pragma warning disable +#define INTERNAL_NULLABLE_ATTRIBUTES + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics.CodeAnalysis +{ +#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 + /// Specifies that null is allowed as an input even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class AllowNullAttribute : Attribute + { } + + /// Specifies that null is disallowed as an input even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class DisallowNullAttribute : Attribute + { } + + /// 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)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class MaybeNullAttribute : Attribute + { } + + /// 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. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class NotNullAttribute : Attribute + { } + + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + 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; } + } + + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + 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; } + } + + /// 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)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + 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; } + } + + /// Applied to a method that will never return under any circumstance. + [AttributeUsage(AttributeTargets.Method, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class DoesNotReturnAttribute : Attribute + { } + + /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + 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; } + } +#endif + +#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 + /// 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)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + 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; } + } + + /// 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)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + 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; } + } +#endif +} diff --git a/ExhaustiveMatching.Examples/.editorconfig b/ExhaustiveMatching.Examples/.editorconfig index 39ffda0..b299cb9 100644 --- a/ExhaustiveMatching.Examples/.editorconfig +++ b/ExhaustiveMatching.Examples/.editorconfig @@ -5,5 +5,11 @@ # EM0001: Switch on Enum Not Exhaustive dotnet_diagnostic.EM0001.severity = suggestion -# EM0002: Switch on Closed Type Not Exhaustive +# EM0002: Switch on Nullable Enum Type Not Exhaustive dotnet_diagnostic.EM0002.severity = suggestion + +# EM0003: Switch on Closed Type Not Exhaustive +dotnet_diagnostic.EM0003.severity = suggestion + +# EM0101: Case Pattern Not Supported +#dotnet_diagnostic.EM0101.severity = suggestion diff --git a/ExhaustiveMatching.Examples/ReadMe/CoinFlipExample.cs b/ExhaustiveMatching.Examples/ReadMe/CoinFlipExample.cs index 72e89f3..63d084f 100644 --- a/ExhaustiveMatching.Examples/ReadMe/CoinFlipExample.cs +++ b/ExhaustiveMatching.Examples/ReadMe/CoinFlipExample.cs @@ -17,6 +17,7 @@ public enum CoinFlip { Heads = 1, Tails } public static void Example(CoinFlip coinFlip) { #region snippet + // EM0001: Switch on Enum Not Exhaustive // ERROR Enum value not handled by switch: Tails switch (coinFlip) { @@ -27,11 +28,12 @@ public static void Example(CoinFlip coinFlip) break; } + // EM0001: Switch on Enum Not Exhaustive // ERROR Enum value not handled by switch: Tails _ = coinFlip switch { CoinFlip.Heads => "Heads!", - _ => throw ExhaustiveMatch.Failed(coinFlip), + _ => throw ExhaustiveMatch.Failed(coinFlip), }; #endregion } diff --git a/ExhaustiveMatching.Examples/ReadMe/DayOfWeekExample.cs b/ExhaustiveMatching.Examples/ReadMe/DayOfWeekExample.cs index cd31801..9933a54 100644 --- a/ExhaustiveMatching.Examples/ReadMe/DayOfWeekExample.cs +++ b/ExhaustiveMatching.Examples/ReadMe/DayOfWeekExample.cs @@ -10,6 +10,7 @@ public static class DayOfWeekExample public static void Example(DayOfWeek dayOfWeek) { #region snippet + // EM0001: Switch on Enum Not Exhaustive // ERROR Enum value not handled by switch: Sunday switch (dayOfWeek) { diff --git a/ExhaustiveMatching.Examples/ReadMe/IPAddressExample.cs b/ExhaustiveMatching.Examples/ReadMe/IPAddressExample.cs index acbd035..f266ad7 100644 --- a/ExhaustiveMatching.Examples/ReadMe/IPAddressExample.cs +++ b/ExhaustiveMatching.Examples/ReadMe/IPAddressExample.cs @@ -19,6 +19,7 @@ public class IPv6Address : IPAddress { /* … */ } public static IPv6Address Example(IPAddress ipAddress) { #region snippet + // EM0003: Switch on Closed Type Not Exhaustive // ERROR Subtype not handled by switch: IPv6Address switch (ipAddress) {