From 8bbef3a0e32d0a926db7dcd4b8ef82a1f6c392cc Mon Sep 17 00:00:00 2001 From: Robin Rexstedt Date: Sat, 6 May 2023 13:24:56 +0200 Subject: [PATCH] add new analyzer for unused results --- .../Analyzers/REM0003UnusedResultAnalyzer.cs | 89 ++++++++ Remora.Results.Analyzers/Descriptors.cs | 13 ++ .../DiagnosticCategories.cs | 7 +- .../Tests/UnusedResultAnalyzerTests.cs | 214 ++++++++++++++++++ 4 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 Remora.Results.Analyzers/Analyzers/REM0003UnusedResultAnalyzer.cs create mode 100644 Tests/Remora.Results.Analyzers.Tests/Tests/UnusedResultAnalyzerTests.cs diff --git a/Remora.Results.Analyzers/Analyzers/REM0003UnusedResultAnalyzer.cs b/Remora.Results.Analyzers/Analyzers/REM0003UnusedResultAnalyzer.cs new file mode 100644 index 0000000..ded20b2 --- /dev/null +++ b/Remora.Results.Analyzers/Analyzers/REM0003UnusedResultAnalyzer.cs @@ -0,0 +1,89 @@ +// +// REM0003UnusedResultAnalyzer.cs +// +// Author: +// Jarl Gullberg +// +// Copyright (c) Jarl Gullberg +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Remora.Results.Analyzers; + +/// +/// Detects and flags unused result returning method calls. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class REM0003UnusedResultAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(Descriptors.REM0003UnusedResult); + + /// + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis + ( + GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics + ); + context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); + } + + private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) + { + if (context.Node is not InvocationExpressionSyntax invocation || + !context.Node.IsKind(SyntaxKind.InvocationExpression)) + { + return; + } + + if (context.SemanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol methodSymbol) + { + return; + } + + // Filter out everything except Result and Result return types + if (methodSymbol?.ReturnType.Name != "Result") + { + return; + } + + var parentSyntax = invocation.Parent; + + if (parentSyntax + is AssignmentExpressionSyntax + or EqualsValueClauseSyntax + or ReturnStatementSyntax + or ArrowExpressionClauseSyntax) + { + return; + } + + // Bad ! + var diagnostic = Diagnostic.Create( + descriptor: Descriptors.REM0003UnusedResult, + location: invocation.GetLocation(), + messageArgs: methodSymbol.Name); + context.ReportDiagnostic(diagnostic); + } +} diff --git a/Remora.Results.Analyzers/Descriptors.cs b/Remora.Results.Analyzers/Descriptors.cs index 5bc3f6f..6ef9ada 100644 --- a/Remora.Results.Analyzers/Descriptors.cs +++ b/Remora.Results.Analyzers/Descriptors.cs @@ -54,4 +54,17 @@ internal static class Descriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true ); + + /// + /// Holds the descriptor for unused results. + /// + internal static readonly DiagnosticDescriptor REM0003UnusedResult = new + ( + id: "REM0003", + title: "Unused result", + messageFormat: "Result from \"{0}\" is never used.", + category: DiagnosticCategories.Usage, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); } diff --git a/Remora.Results.Analyzers/DiagnosticCategories.cs b/Remora.Results.Analyzers/DiagnosticCategories.cs index 13397b7..ea997d9 100644 --- a/Remora.Results.Analyzers/DiagnosticCategories.cs +++ b/Remora.Results.Analyzers/DiagnosticCategories.cs @@ -30,5 +30,10 @@ internal static class DiagnosticCategories /// /// Gets the category string for analyzers that flag redundant code. /// - internal static string Redundancies => "Redundancies in Code"; + internal const string Redundancies = "Redundancies in Code"; + + /// + /// Gets the category string for analyzers that flag improper usage. + /// + internal const string Usage = "Usage"; } diff --git a/Tests/Remora.Results.Analyzers.Tests/Tests/UnusedResultAnalyzerTests.cs b/Tests/Remora.Results.Analyzers.Tests/Tests/UnusedResultAnalyzerTests.cs new file mode 100644 index 0000000..2227102 --- /dev/null +++ b/Tests/Remora.Results.Analyzers.Tests/Tests/UnusedResultAnalyzerTests.cs @@ -0,0 +1,214 @@ +// +// UnusedResultAnalyzerTests.cs +// +// Author: +// Jarl Gullberg +// +// Copyright (c) Jarl Gullberg +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this program. If not, see . +// + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Testing; +using Remora.Results.Analyzers.Tests.TestBases; +using Xunit; + +namespace Remora.Results.Analyzers.Tests; + +/// +/// Tests the analyzer. +/// +public class UnusedResultAnalyzerTests : ResultAnalyzerTests +{ + /// + /// Tests that the analyzer raises a warning when a Result of T is unused. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task RaisesWarningForUnusedResultOfT() + { + this.TestCode = + @" + using System; + using Remora.Results; + + public class Program + { + public Result MyMethod() => 1; + + public void Main() + { + MyMethod(); + } + } + "; + + this.ExpectedDiagnostics.Clear(); + this.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning("REM0003") + .WithSpan(11, 21, 11, 31) + .WithArguments("MyMethod")); + + await RunAsync(); + } + + /// + /// Tests that the analyzer raises a warning when a Result of T is unused. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task RaisesWarningForUnusedResult() + { + this.TestCode = + @" + using System; + using Remora.Results; + + public class Program + { + public Result MyMethod() => Result.FromSuccess(); + + public void Main() + { + MyMethod(); + } + } + "; + + this.ExpectedDiagnostics.Clear(); + this.ExpectedDiagnostics.Add(DiagnosticResult.CompilerWarning("REM0003") + .WithSpan(11, 21, 11, 31) + .WithArguments("MyMethod")); + + await RunAsync(); + } + + /// + /// Tests that the analyzer doesn't raise a warning when Result of T is used. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task IgnoresUsedResultOfTWithArrowSyntax() + { + this.TestCode = + @" + using System; + using Remora.Results; + + public class Program + { + public Result MyMethod() => 1; + + public void Main() + { + var result = MyMethod(); + } + } + "; + + this.ExpectedDiagnostics.Clear(); + + await RunAsync(); + } + + /// + /// Tests that the analyzer doesn't raise a warning when Result of T is used. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task IgnoresUsedResultOfT() + { + this.TestCode = + @" + using System; + using Remora.Results; + + public class Program + { + public Result MyMethod() + { + return 1; + } + + public void Main() + { + var result = MyMethod(); + } + } + "; + + this.ExpectedDiagnostics.Clear(); + + await RunAsync(); + } + + /// + /// Tests that the analyzer doesn't raise a warning when Result is used. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task IgnoresUsedResultWithArrowSyntax() + { + this.TestCode = + @" + using System; + using Remora.Results; + + public class Program + { + public Result MyMethod() => Result.FromSuccess(); + + public void Main() + { + var result = MyMethod(); + } + } + "; + + this.ExpectedDiagnostics.Clear(); + + await RunAsync(); + } + + /// + /// Tests that the analyzer doesn't raise a warning when Result is used. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task IgnoresUsedResult() + { + this.TestCode = + @" + using System; + using Remora.Results; + + public class Program + { + public Result MyMethod() + { + return Result.FromSuccess(); + } + + public void Main() + { + var result = MyMethod(); + } + } + "; + + this.ExpectedDiagnostics.Clear(); + + await RunAsync(); + } +}