diff --git a/README.md b/README.md index 2fe98e53..df7d4043 100644 --- a/README.md +++ b/README.md @@ -88,5 +88,9 @@ Analyzers checking System.Reflection REFL023 The type does not implement the interface. + + REFL024 + Prefer null over empty array. + diff --git a/ReflectionAnalyzers.Tests/REFL022UseFullyQualifiedNameTests/Diagnostics.cs b/ReflectionAnalyzers.Tests/REFL022UseFullyQualifiedNameTests/CodeFix.cs similarity index 100% rename from ReflectionAnalyzers.Tests/REFL022UseFullyQualifiedNameTests/Diagnostics.cs rename to ReflectionAnalyzers.Tests/REFL022UseFullyQualifiedNameTests/CodeFix.cs diff --git a/ReflectionAnalyzers.Tests/REFL024PreferNullOverEmptyArrayTests/CodeFix.cs b/ReflectionAnalyzers.Tests/REFL024PreferNullOverEmptyArrayTests/CodeFix.cs new file mode 100644 index 00000000..c713c843 --- /dev/null +++ b/ReflectionAnalyzers.Tests/REFL024PreferNullOverEmptyArrayTests/CodeFix.cs @@ -0,0 +1,54 @@ +namespace ReflectionAnalyzers.Tests.REFL024PreferNullOverEmptyArrayTests +{ + using Gu.Roslyn.Asserts; + using Microsoft.CodeAnalysis.CodeFixes; + using Microsoft.CodeAnalysis.Diagnostics; + using NUnit.Framework; + using ReflectionAnalyzers.Codefixes; + + public class CodeFix + { + private static readonly DiagnosticAnalyzer Analyzer = new InvokeAnalyzer(); + private static readonly CodeFixProvider Fix = new PreferNullFix(); + private static readonly ExpectedDiagnostic ExpectedDiagnostic = ExpectedDiagnostic.Create(REFL024PreferNullOverEmptyArray.Descriptor); + + [TestCase("Array.Empty()")] + [TestCase("new object[0]")] + [TestCase("new object[0] { }")] + [TestCase("new object[] { }")] + public void MemberInfoInvoke(string emptyArray) + { + var code = @" +namespace RoslynSandbox +{ + using System; + using System.Reflection; + + public class Foo + { + public Foo(MethodInfo member) + { + _ = member.Invoke(null, Array.Empty()); + } + } +}".AssertReplace("Array.Empty()", emptyArray); + + var fixedCode = @" +namespace RoslynSandbox +{ + using System; + using System.Reflection; + + public class Foo + { + public Foo(MethodInfo member) + { + _ = member.Invoke(null, null); + } + } +}"; + + AnalyzerAssert.CodeFix(Analyzer, Fix, ExpectedDiagnostic, code, fixedCode); + } + } +} diff --git a/ReflectionAnalyzers.sln b/ReflectionAnalyzers.sln index 94984efb..da77e5c8 100644 --- a/ReflectionAnalyzers.sln +++ b/ReflectionAnalyzers.sln @@ -52,6 +52,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".docs", ".docs", "{1C271AF2 documentation\REFL020.md = documentation\REFL020.md documentation\REFL022.md = documentation\REFL022.md documentation\REFL023.md = documentation\REFL023.md + documentation\REFL024.md = documentation\REFL024.md RELEASE_NOTES.md = RELEASE_NOTES.md EndProjectSection EndProject diff --git a/ReflectionAnalyzers/Codefixes/PreferNullFix.cs b/ReflectionAnalyzers/Codefixes/PreferNullFix.cs new file mode 100644 index 00000000..0fadde69 --- /dev/null +++ b/ReflectionAnalyzers/Codefixes/PreferNullFix.cs @@ -0,0 +1,38 @@ +namespace ReflectionAnalyzers.Codefixes +{ + using System.Collections.Immutable; + using System.Composition; + using System.Threading.Tasks; + using Gu.Roslyn.CodeFixExtensions; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CodeFixes; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(PreferNullFix))] + [Shared] + internal class PreferNullFix : DocumentEditorCodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create( + REFL024PreferNullOverEmptyArray.DiagnosticId); + + protected override async Task RegisterCodeFixesAsync(DocumentEditorCodeFixContext context) + { + var syntaxRoot = await context.Document.GetSyntaxRootAsync(context.CancellationToken) + .ConfigureAwait(false); + foreach (var diagnostic in context.Diagnostics) + { + if (syntaxRoot.TryFindNode(diagnostic, out ArgumentSyntax argument)) + { + context.RegisterCodeFix( + "Prefer null.", + (editor, _) => editor.ReplaceNode( + argument.Expression, + x => SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression)), + nameof(PreferNullFix), + diagnostic); + } + } + } + } +} diff --git a/ReflectionAnalyzers/NodeAnalzers/InvokeAnalyzer.cs b/ReflectionAnalyzers/NodeAnalzers/InvokeAnalyzer.cs index c5710f22..8306194a 100644 --- a/ReflectionAnalyzers/NodeAnalzers/InvokeAnalyzer.cs +++ b/ReflectionAnalyzers/NodeAnalzers/InvokeAnalyzer.cs @@ -12,7 +12,8 @@ internal class InvokeAnalyzer : DiagnosticAnalyzer { /// public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( - REFL002InvokeDiscardReturnValue.Descriptor); + REFL002InvokeDiscardReturnValue.Descriptor, + REFL024PreferNullOverEmptyArray.Descriptor); /// public override void Initialize(AnalysisContext context) @@ -28,8 +29,48 @@ private static void Handle(SyntaxNodeAnalysisContext context) context.Node is InvocationExpressionSyntax invocation && invocation.TryGetMethodName(out var name) && name == "Invoke" && - context.SemanticModel.TryGetSymbol(invocation, context.CancellationToken, out var target)) + context.SemanticModel.TryGetSymbol(invocation, context.CancellationToken, out var invoke) && + invoke.ContainingType.IsAssignableTo(KnownSymbol.MemberInfo, context.Compilation)) { + if (invoke.TryFindParameter("parameters", out var parameter) && + invocation.TryFindArgument(parameter, out var paramsArg) && + IsEmptyArray(paramsArg, context)) + { + context.ReportDiagnostic(Diagnostic.Create(REFL024PreferNullOverEmptyArray.Descriptor, paramsArg.GetLocation())); + } + } + } + + private static bool IsEmptyArray(ArgumentSyntax argument, SyntaxNodeAnalysisContext context) + { + switch (argument.Expression) + { + case InvocationExpressionSyntax invocation when context.SemanticModel.TryGetSymbol(invocation, context.CancellationToken, out var symbol) && + symbol == KnownSymbol.Array.Empty: + return true; + case ArrayCreationExpressionSyntax arrayCreation: + if (arrayCreation.Type is ArrayTypeSyntax arrayType) + { + foreach (var rankSpecifier in arrayType.RankSpecifiers) + { + foreach (var size in rankSpecifier.Sizes) + { + if (size is LiteralExpressionSyntax literal && + literal.Token.ValueText != "0") + { + return false; + } + } + } + + var initializer = arrayCreation.Initializer; + return initializer == null || + initializer.Expressions.Count == 0; + } + + return false; + default: + return false; } } } diff --git a/ReflectionAnalyzers/REFL023TypeDoesNotImplementInterface.cs b/ReflectionAnalyzers/REFL023TypeDoesNotImplementInterface.cs index 5df43609..53a5399a 100644 --- a/ReflectionAnalyzers/REFL023TypeDoesNotImplementInterface.cs +++ b/ReflectionAnalyzers/REFL023TypeDoesNotImplementInterface.cs @@ -16,4 +16,4 @@ internal static class REFL023TypeDoesNotImplementInterface description: "The type does not implement the interface.", helpLinkUri: HelpLink.ForId(DiagnosticId)); } -} \ No newline at end of file +} diff --git a/ReflectionAnalyzers/REFL024PreferNullOverEmptyArray.cs b/ReflectionAnalyzers/REFL024PreferNullOverEmptyArray.cs new file mode 100644 index 00000000..f051e815 --- /dev/null +++ b/ReflectionAnalyzers/REFL024PreferNullOverEmptyArray.cs @@ -0,0 +1,19 @@ +namespace ReflectionAnalyzers +{ + using Microsoft.CodeAnalysis; + + internal static class REFL024PreferNullOverEmptyArray + { + public const string DiagnosticId = "REFL024"; + + internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: DiagnosticId, + title: "Prefer null over empty array.", + messageFormat: "Prefer null over empty array.", + category: AnalyzerCategory.SystemReflection, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Prefer null over empty array.", + helpLinkUri: HelpLink.ForId(DiagnosticId)); + } +} \ No newline at end of file diff --git a/documentation/REFL024.md b/documentation/REFL024.md new file mode 100644 index 00000000..894c9997 --- /dev/null +++ b/documentation/REFL024.md @@ -0,0 +1,67 @@ +# REFL024 +## Prefer null over empty array. + + +
+ + + + + + + + + + + + + + + + + + + + +
CheckIdREFL024
SeverityWarning
Enabledtrue
CategoryReflectionAnalyzers.SystemReflection
CodeInvokeAnalyzer
+ + +## Description + +Prefer null over empty array. + +## Motivation + +ADD MOTIVATION HERE + +## How to fix violations + +ADD HOW TO FIX VIOLATIONS HERE + + +## Configure severity + +### Via ruleset file. + +Configure the severity per project, for more info see [MSDN](https://msdn.microsoft.com/en-us/library/dd264949.aspx). + +### Via #pragma directive. +```C# +#pragma warning disable REFL024 // Prefer null over empty array. +Code violating the rule here +#pragma warning restore REFL024 // Prefer null over empty array. +``` + +Or put this at the top of the file to disable all instances. +```C# +#pragma warning disable REFL024 // Prefer null over empty array. +``` + +### Via attribute `[SuppressMessage]`. + +```C# +[System.Diagnostics.CodeAnalysis.SuppressMessage("ReflectionAnalyzers.SystemReflection", + "REFL024:Prefer null over empty array.", + Justification = "Reason...")] +``` + \ No newline at end of file