From 426becd48037e975aecd171bca8aeae04bd2a101 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Sat, 23 Dec 2023 11:13:18 +0100 Subject: [PATCH 01/43] Add analysis project --- Rascal.sln | 14 +++++++++++++ src/Rascal.Analysis/Rascal.Analysis.csproj | 24 ++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 src/Rascal.Analysis/Rascal.Analysis.csproj diff --git a/Rascal.sln b/Rascal.sln index 444c497..8b10ae8 100644 --- a/Rascal.sln +++ b/Rascal.sln @@ -9,6 +9,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rascal", "src\Rascal\Rascal EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rascal.Tests", "src\Rascal.Tests\Rascal.Tests.csproj", "{291FDB2E-8AC8-4B7A-B6E6-6DF5DCBCEB58}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rascal.Analysis", "src\Rascal.Analysis\Rascal.Analysis.csproj", "{076764E7-54D5-41EF-99C9-96C483F15E31}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rascal.Analysis.Tests", "src\Rascal.Analysis.Tests\Rascal.Analysis.Tests.csproj", "{C2197909-F734-4435-869A-422CD571D061}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -26,9 +30,19 @@ Global {291FDB2E-8AC8-4B7A-B6E6-6DF5DCBCEB58}.Debug|Any CPU.Build.0 = Debug|Any CPU {291FDB2E-8AC8-4B7A-B6E6-6DF5DCBCEB58}.Release|Any CPU.ActiveCfg = Release|Any CPU {291FDB2E-8AC8-4B7A-B6E6-6DF5DCBCEB58}.Release|Any CPU.Build.0 = Release|Any CPU + {076764E7-54D5-41EF-99C9-96C483F15E31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {076764E7-54D5-41EF-99C9-96C483F15E31}.Debug|Any CPU.Build.0 = Debug|Any CPU + {076764E7-54D5-41EF-99C9-96C483F15E31}.Release|Any CPU.ActiveCfg = Release|Any CPU + {076764E7-54D5-41EF-99C9-96C483F15E31}.Release|Any CPU.Build.0 = Release|Any CPU + {C2197909-F734-4435-869A-422CD571D061}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2197909-F734-4435-869A-422CD571D061}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2197909-F734-4435-869A-422CD571D061}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2197909-F734-4435-869A-422CD571D061}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {04DE173D-4E74-4104-94FA-2B1807FCD34D} = {D92004B4-8D01-4BC3-B76B-A80D657FEC30} {291FDB2E-8AC8-4B7A-B6E6-6DF5DCBCEB58} = {D92004B4-8D01-4BC3-B76B-A80D657FEC30} + {076764E7-54D5-41EF-99C9-96C483F15E31} = {D92004B4-8D01-4BC3-B76B-A80D657FEC30} + {C2197909-F734-4435-869A-422CD571D061} = {D92004B4-8D01-4BC3-B76B-A80D657FEC30} EndGlobalSection EndGlobal diff --git a/src/Rascal.Analysis/Rascal.Analysis.csproj b/src/Rascal.Analysis/Rascal.Analysis.csproj new file mode 100644 index 0000000..092a881 --- /dev/null +++ b/src/Rascal.Analysis/Rascal.Analysis.csproj @@ -0,0 +1,24 @@ + + + + netstandard2.0 + latest + enable + enable + nullable + true + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + From a996e60adb3d979cf9f141c7dfdaba965db37233 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Sat, 23 Dec 2023 11:13:41 +0100 Subject: [PATCH 02/43] Add testing project --- src/Rascal.Analysis.Tests/GlobalUsings.cs | 1 + .../Rascal.Analysis.Tests.csproj | 40 +++++++++++++++++++ .../Verifiers/AnalyzerTest.cs | 19 +++++++++ .../Verifiers/AnalyzerVerifier.cs | 23 +++++++++++ .../Verifiers/CodeFixTest.cs | 21 ++++++++++ .../Verifiers/CodeFixVerifier.cs | 26 ++++++++++++ .../Verifiers/VerifierHelper.cs | 19 +++++++++ 7 files changed, 149 insertions(+) create mode 100644 src/Rascal.Analysis.Tests/GlobalUsings.cs create mode 100644 src/Rascal.Analysis.Tests/Rascal.Analysis.Tests.csproj create mode 100644 src/Rascal.Analysis.Tests/Verifiers/AnalyzerTest.cs create mode 100644 src/Rascal.Analysis.Tests/Verifiers/AnalyzerVerifier.cs create mode 100644 src/Rascal.Analysis.Tests/Verifiers/CodeFixTest.cs create mode 100644 src/Rascal.Analysis.Tests/Verifiers/CodeFixVerifier.cs create mode 100644 src/Rascal.Analysis.Tests/Verifiers/VerifierHelper.cs diff --git a/src/Rascal.Analysis.Tests/GlobalUsings.cs b/src/Rascal.Analysis.Tests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/src/Rascal.Analysis.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/src/Rascal.Analysis.Tests/Rascal.Analysis.Tests.csproj b/src/Rascal.Analysis.Tests/Rascal.Analysis.Tests.csproj new file mode 100644 index 0000000..072ef67 --- /dev/null +++ b/src/Rascal.Analysis.Tests/Rascal.Analysis.Tests.csproj @@ -0,0 +1,40 @@ + + + + net8.0 + enable + enable + nullable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + diff --git a/src/Rascal.Analysis.Tests/Verifiers/AnalyzerTest.cs b/src/Rascal.Analysis.Tests/Verifiers/AnalyzerTest.cs new file mode 100644 index 0000000..e443ba2 --- /dev/null +++ b/src/Rascal.Analysis.Tests/Verifiers/AnalyzerTest.cs @@ -0,0 +1,19 @@ +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing.Verifiers; + +namespace Rascal.Analysis.Tests.Verifiers; + +public class AnalyzerTest : CSharpAnalyzerTest + where TAnalyzer : DiagnosticAnalyzer, new() +{ + public AnalyzerTest() => SolutionTransforms.Add((solution, projectId) => + { + var compilationOptions = solution.GetProject(projectId)!.CompilationOptions; + compilationOptions = compilationOptions!.WithSpecificDiagnosticOptions( + compilationOptions.SpecificDiagnosticOptions.SetItems(VerifierHelper.NullableWarnings)); + solution = solution.WithProjectCompilationOptions(projectId, compilationOptions); + + return solution; + }); +} diff --git a/src/Rascal.Analysis.Tests/Verifiers/AnalyzerVerifier.cs b/src/Rascal.Analysis.Tests/Verifiers/AnalyzerVerifier.cs new file mode 100644 index 0000000..256282c --- /dev/null +++ b/src/Rascal.Analysis.Tests/Verifiers/AnalyzerVerifier.cs @@ -0,0 +1,23 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; + +namespace Rascal.Analysis.Tests.Verifiers; + +public static partial class AnalyzerVerifier + where TAnalyzer : DiagnosticAnalyzer, new() +{ + /// + public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) + { + var test = new AnalyzerTest + { + TestCode = source, + }; + + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile("./Rascal.dll")); + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } +} diff --git a/src/Rascal.Analysis.Tests/Verifiers/CodeFixTest.cs b/src/Rascal.Analysis.Tests/Verifiers/CodeFixTest.cs new file mode 100644 index 0000000..2e53084 --- /dev/null +++ b/src/Rascal.Analysis.Tests/Verifiers/CodeFixTest.cs @@ -0,0 +1,21 @@ +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing.Verifiers; + +namespace Rascal.Analysis.Tests.Verifiers; + +public class CodeFixTest : CSharpCodeFixTest + where TAnalyzer : DiagnosticAnalyzer, new() + where TCodeFix : CodeFixProvider, new() +{ + public CodeFixTest() => SolutionTransforms.Add((solution, projectId) => + { + var compilationOptions = solution.GetProject(projectId)!.CompilationOptions; + compilationOptions = compilationOptions!.WithSpecificDiagnosticOptions( + compilationOptions.SpecificDiagnosticOptions.SetItems(VerifierHelper.NullableWarnings)); + solution = solution.WithProjectCompilationOptions(projectId, compilationOptions); + + return solution; + }); +} diff --git a/src/Rascal.Analysis.Tests/Verifiers/CodeFixVerifier.cs b/src/Rascal.Analysis.Tests/Verifiers/CodeFixVerifier.cs new file mode 100644 index 0000000..24496fd --- /dev/null +++ b/src/Rascal.Analysis.Tests/Verifiers/CodeFixVerifier.cs @@ -0,0 +1,26 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; + +namespace Rascal.Analysis.Tests.Verifiers; + +public static partial class CodeFixVerifier + where TAnalyzer : DiagnosticAnalyzer, new() + where TCodeFix : CodeFixProvider, new() +{ + /// + public static async Task VerifyCodeFixAsync(string source, string fixedSource) + { + var test = new CodeFixTest + { + // Replace line endings because the formatter uses the environment's newline. + TestCode = source, + FixedCode = fixedSource, + }; + + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile("./Rascal.dll")); + + await test.RunAsync(CancellationToken.None); + } +} diff --git a/src/Rascal.Analysis.Tests/Verifiers/VerifierHelper.cs b/src/Rascal.Analysis.Tests/Verifiers/VerifierHelper.cs new file mode 100644 index 0000000..691d14d --- /dev/null +++ b/src/Rascal.Analysis.Tests/Verifiers/VerifierHelper.cs @@ -0,0 +1,19 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Rascal.Analysis.Tests.Verifiers; + +internal static class VerifierHelper +{ + internal static ImmutableDictionary NullableWarnings { get; } = GetNullableWarningsFromCompiler(); + + private static ImmutableDictionary GetNullableWarningsFromCompiler() + { + var args = new[] {"/warnaserror:nullable"}; + var commandLineArguments = CSharpCommandLineParser.Default.Parse(args, Environment.CurrentDirectory, Environment.CurrentDirectory); + var nullableWarnings = commandLineArguments.CompilationOptions.SpecificDiagnosticOptions; + + return nullableWarnings; + } +} From 7e75ab8c0bafa115cef6749ade0e3d5cf2eb4055 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Sat, 23 Dec 2023 11:13:49 +0100 Subject: [PATCH 03/43] Add ThrowAnalyzer --- .../Analyzers/ThrowAnalyzerTests.cs | 53 +++++++++++++++++++ .../Analyzers/ThrowAnalayzer.cs | 53 +++++++++++++++++++ src/Rascal.Analysis/Diagnostics.cs | 16 ++++++ 3 files changed, 122 insertions(+) create mode 100644 src/Rascal.Analysis.Tests/Analyzers/ThrowAnalyzerTests.cs create mode 100644 src/Rascal.Analysis/Analyzers/ThrowAnalayzer.cs create mode 100644 src/Rascal.Analysis/Diagnostics.cs diff --git a/src/Rascal.Analysis.Tests/Analyzers/ThrowAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/ThrowAnalyzerTests.cs new file mode 100644 index 0000000..30f6e42 --- /dev/null +++ b/src/Rascal.Analysis.Tests/Analyzers/ThrowAnalyzerTests.cs @@ -0,0 +1,53 @@ +using VerifyCS = Rascal.Analysis.Tests.Verifiers.AnalyzerVerifier; + +namespace Rascal.Analysis.Analyzers.Tests; + +public class ThrowAnalyzerTests +{ + [Fact] + public Task DoesNothingUsually() => VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + + public static class Foo + { + public static Result Bar() + { + return new(2); + } + } + """); + + [Fact] + public Task ThrowInsideResultMethod() => VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + + public static class Foo + { + public static Result Bar() + { + {|RASCAL0001:throw|} new Exception(); + } + } + """); + + [Fact] + public Task ThrowInsideLocalFunction() => VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + + public static class Foo + { + public static void Bar() + { + Baz(); + + static Result Baz() + { + {|RASCAL0001:throw|} new Exception(); + } + } + } + """); +} diff --git a/src/Rascal.Analysis/Analyzers/ThrowAnalayzer.cs b/src/Rascal.Analysis/Analyzers/ThrowAnalayzer.cs new file mode 100644 index 0000000..155e19a --- /dev/null +++ b/src/Rascal.Analysis/Analyzers/ThrowAnalayzer.cs @@ -0,0 +1,53 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Rascal.Analysis.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ThrowAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + Diagnostics.DoNotThrow); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(compilationCtx => + { + var resultType = compilationCtx.Compilation.GetTypeByMetadataName("Rascal.Result`1"); + if (resultType is null) return; + + compilationCtx.RegisterSymbolStartAction(symbolCtx => + { + var method = (IMethodSymbol)symbolCtx.Symbol; + + var returnType = method.ReturnType; + if (!returnType.OriginalDefinition.Equals(resultType, SymbolEqualityComparer.Default)) return; + + symbolCtx.RegisterOperationAction(operationCtx => + { + var operation = (IThrowOperation)operationCtx.Operation; + + var location = operation.Syntax switch + { + ThrowStatementSyntax x => x.ThrowKeyword.GetLocation(), + ThrowExpressionSyntax x => x.ThrowKeyword.GetLocation(), + _ => throw new InvalidOperationException( + "Syntax of throw operation is not a throw statement syntax or throw expression syntax."), + }; + + operationCtx.ReportDiagnostic(Diagnostic.Create( + Diagnostics.DoNotThrow, + location, + method.Name, + returnType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); + }, OperationKind.Throw); + }, SymbolKind.Method); + }); + } +} diff --git a/src/Rascal.Analysis/Diagnostics.cs b/src/Rascal.Analysis/Diagnostics.cs new file mode 100644 index 0000000..15995f2 --- /dev/null +++ b/src/Rascal.Analysis/Diagnostics.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis; + +namespace Rascal.Analysis; + +public static class Diagnostics +{ + public static DiagnosticDescriptor DoNotThrow { get; } = new( + "RASCAL0001", + "Do not throw inside a result method", + "Do not throw exceptions inside method '{0}' which returns '{1}'", + "Correctness", + DiagnosticSeverity.Warning, + true, + "Methods which return results should not throw exceptions unless absolutely necessary. " + + "Instead consider returning `Err()` with a description of the error."); +} From 64abaa87efa83515f9641eb32eb4f0c3c45608b0 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Sat, 23 Dec 2023 21:18:44 +0100 Subject: [PATCH 04/43] Update ThrowAnalyzer --- .../Analyzers/ThrowAnalyzerTests.cs | 42 +++++++++ .../Analyzers/ThrowAnalayzer.cs | 90 ++++++++++++++----- src/Rascal.Analysis/Diagnostics.cs | 6 +- 3 files changed, 114 insertions(+), 24 deletions(-) diff --git a/src/Rascal.Analysis.Tests/Analyzers/ThrowAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/ThrowAnalyzerTests.cs index 30f6e42..2ac7849 100644 --- a/src/Rascal.Analysis.Tests/Analyzers/ThrowAnalyzerTests.cs +++ b/src/Rascal.Analysis.Tests/Analyzers/ThrowAnalyzerTests.cs @@ -50,4 +50,46 @@ static Result Baz() } } """); + + [Fact] + public Task ThrowInsideLambda() => VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + + public static class Foo + { + public static Func> func = () => + { + {|RASCAL0001:throw|} new Exception(); + }; + } + """); + + [Fact] + public Task ThrowInsideExpressionProperty() => VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + + public static class Foo + { + public static Result X => {|RASCAL0001:throw|} new Exception(); + } + """); + + [Fact] + public Task ThrowInsideExplicitProperty() => VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + + public static class Foo + { + public static Result X + { + get + { + {|RASCAL0001:throw|} new Exception(); + } + } + } + """); } diff --git a/src/Rascal.Analysis/Analyzers/ThrowAnalayzer.cs b/src/Rascal.Analysis/Analyzers/ThrowAnalayzer.cs index 155e19a..352cdf8 100644 --- a/src/Rascal.Analysis/Analyzers/ThrowAnalayzer.cs +++ b/src/Rascal.Analysis/Analyzers/ThrowAnalayzer.cs @@ -22,32 +22,80 @@ public override void Initialize(AnalysisContext context) var resultType = compilationCtx.Compilation.GetTypeByMetadataName("Rascal.Result`1"); if (resultType is null) return; - compilationCtx.RegisterSymbolStartAction(symbolCtx => + compilationCtx.RegisterOperationAction(operationCtx => { - var method = (IMethodSymbol)symbolCtx.Symbol; + var operation = (IThrowOperation)operationCtx.Operation; + + ISymbol? symbol = GetParentMethodLikeOperation(operation) switch + { + IMethodBodyBaseOperation => operationCtx.ContainingSymbol switch + { + IMethodSymbol x => x, + IPropertySymbol x => x, + _ => null, + }, + ILocalFunctionOperation x => x.Symbol, + IAnonymousFunctionOperation x => x.Symbol, + _ => null, + }; + + if (symbol is null) return; + + var returnType = symbol switch + { + IMethodSymbol x => x.ReturnType, + IPropertySymbol x => x.Type, + _ => throw new InvalidOperationException(), + }; - var returnType = method.ReturnType; if (!returnType.OriginalDefinition.Equals(resultType, SymbolEqualityComparer.Default)) return; - symbolCtx.RegisterOperationAction(operationCtx => + var location = operation.Syntax switch { - var operation = (IThrowOperation)operationCtx.Operation; - - var location = operation.Syntax switch - { - ThrowStatementSyntax x => x.ThrowKeyword.GetLocation(), - ThrowExpressionSyntax x => x.ThrowKeyword.GetLocation(), - _ => throw new InvalidOperationException( - "Syntax of throw operation is not a throw statement syntax or throw expression syntax."), - }; - - operationCtx.ReportDiagnostic(Diagnostic.Create( - Diagnostics.DoNotThrow, - location, - method.Name, - returnType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); - }, OperationKind.Throw); - }, SymbolKind.Method); + ThrowStatementSyntax x => x.ThrowKeyword.GetLocation(), + ThrowExpressionSyntax x => x.ThrowKeyword.GetLocation(), + _ => throw new InvalidOperationException( + "Syntax of throw operation is not a throw statement syntax or throw expression syntax."), + }; + + operationCtx.ReportDiagnostic(Diagnostic.Create( + Diagnostics.DoNotThrow, + location, + symbol.Name, + returnType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); + }, OperationKind.Throw); }); } + + private static IOperation? GetParentMethodLikeOperation(IOperation operation) + { + var current = operation.Parent; + + while (current is not null) + { + if (current is IMethodBodyBaseOperation or ILocalFunctionOperation or IAnonymousFunctionOperation) + return current; + + current = current.Parent; + } + + return null; + } + + private static TOperation? GetParentOperation(IOperation op) + where TOperation : class, IOperation + { + var current = op; + + while (current is not null) + { + if (current is TOperation x) return x; + current = current.Parent; + } + + return null; + } + + private static IMethodSymbol? GetSymbolOfMethodOperation(IMethodBodyBaseOperation operation) => + operation.SemanticModel?.GetDeclaredSymbol(operation.Syntax) as IMethodSymbol; } diff --git a/src/Rascal.Analysis/Diagnostics.cs b/src/Rascal.Analysis/Diagnostics.cs index 15995f2..c8bd073 100644 --- a/src/Rascal.Analysis/Diagnostics.cs +++ b/src/Rascal.Analysis/Diagnostics.cs @@ -6,11 +6,11 @@ public static class Diagnostics { public static DiagnosticDescriptor DoNotThrow { get; } = new( "RASCAL0001", - "Do not throw inside a result method", - "Do not throw exceptions inside method '{0}' which returns '{1}'", + "Do not throw inside a result method or property", + "Do not throw exceptions inside method or property '{0}' which returns '{1}'", "Correctness", DiagnosticSeverity.Warning, true, - "Methods which return results should not throw exceptions unless absolutely necessary. " + + "Methods or properties which return results should not throw exceptions unless absolutely necessary. " + "Instead consider returning `Err()` with a description of the error."); } From b468b5512abc7d7b917c308f40bae4d0e523ba3e Mon Sep 17 00:00:00 2001 From: thinker227 Date: Fri, 29 Dec 2023 13:57:19 +0100 Subject: [PATCH 05/43] Fix async stuff --- .../Analyzers/ThrowAnalyzerTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Rascal.Analysis.Tests/Analyzers/ThrowAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/ThrowAnalyzerTests.cs index 2ac7849..766b20e 100644 --- a/src/Rascal.Analysis.Tests/Analyzers/ThrowAnalyzerTests.cs +++ b/src/Rascal.Analysis.Tests/Analyzers/ThrowAnalyzerTests.cs @@ -5,7 +5,7 @@ namespace Rascal.Analysis.Analyzers.Tests; public class ThrowAnalyzerTests { [Fact] - public Task DoesNothingUsually() => VerifyCS.VerifyAnalyzerAsync(""" + public async Task DoesNothingUsually() => await VerifyCS.VerifyAnalyzerAsync(""" using System; using Rascal; @@ -19,7 +19,7 @@ public static Result Bar() """); [Fact] - public Task ThrowInsideResultMethod() => VerifyCS.VerifyAnalyzerAsync(""" + public async Task ThrowInsideResultMethod() => await VerifyCS.VerifyAnalyzerAsync(""" using System; using Rascal; @@ -33,7 +33,7 @@ public static Result Bar() """); [Fact] - public Task ThrowInsideLocalFunction() => VerifyCS.VerifyAnalyzerAsync(""" + public async Task ThrowInsideLocalFunction() => await VerifyCS.VerifyAnalyzerAsync(""" using System; using Rascal; @@ -52,7 +52,7 @@ static Result Baz() """); [Fact] - public Task ThrowInsideLambda() => VerifyCS.VerifyAnalyzerAsync(""" + public async Task ThrowInsideLambda() => await VerifyCS.VerifyAnalyzerAsync(""" using System; using Rascal; @@ -66,7 +66,7 @@ public static class Foo """); [Fact] - public Task ThrowInsideExpressionProperty() => VerifyCS.VerifyAnalyzerAsync(""" + public async Task ThrowInsideExpressionProperty() => await VerifyCS.VerifyAnalyzerAsync(""" using System; using Rascal; @@ -77,7 +77,7 @@ public static class Foo """); [Fact] - public Task ThrowInsideExplicitProperty() => VerifyCS.VerifyAnalyzerAsync(""" + public async Task ThrowInsideExplicitProperty() => await VerifyCS.VerifyAnalyzerAsync(""" using System; using Rascal; From e8f3f24a391a3348c7230ad1352ac77f52458f30 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Fri, 29 Dec 2023 13:57:38 +0100 Subject: [PATCH 06/43] Move DoNotThrow diagnostic to RASCAL0002 --- .../Analyzers/ThrowAnalyzerTests.cs | 10 +++++----- src/Rascal.Analysis/Diagnostics.cs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Rascal.Analysis.Tests/Analyzers/ThrowAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/ThrowAnalyzerTests.cs index 766b20e..56c7b89 100644 --- a/src/Rascal.Analysis.Tests/Analyzers/ThrowAnalyzerTests.cs +++ b/src/Rascal.Analysis.Tests/Analyzers/ThrowAnalyzerTests.cs @@ -27,7 +27,7 @@ public static class Foo { public static Result Bar() { - {|RASCAL0001:throw|} new Exception(); + {|RASCAL0002:throw|} new Exception(); } } """); @@ -45,7 +45,7 @@ public static void Bar() static Result Baz() { - {|RASCAL0001:throw|} new Exception(); + {|RASCAL0002:throw|} new Exception(); } } } @@ -60,7 +60,7 @@ public static class Foo { public static Func> func = () => { - {|RASCAL0001:throw|} new Exception(); + {|RASCAL0002:throw|} new Exception(); }; } """); @@ -72,7 +72,7 @@ public async Task ThrowInsideExpressionProperty() => await VerifyCS.VerifyAnalyz public static class Foo { - public static Result X => {|RASCAL0001:throw|} new Exception(); + public static Result X => {|RASCAL0002:throw|} new Exception(); } """); @@ -87,7 +87,7 @@ public static Result X { get { - {|RASCAL0001:throw|} new Exception(); + {|RASCAL0002:throw|} new Exception(); } } } diff --git a/src/Rascal.Analysis/Diagnostics.cs b/src/Rascal.Analysis/Diagnostics.cs index c8bd073..2e42c05 100644 --- a/src/Rascal.Analysis/Diagnostics.cs +++ b/src/Rascal.Analysis/Diagnostics.cs @@ -5,7 +5,7 @@ namespace Rascal.Analysis; public static class Diagnostics { public static DiagnosticDescriptor DoNotThrow { get; } = new( - "RASCAL0001", + "RASCAL0002", "Do not throw inside a result method or property", "Do not throw exceptions inside method or property '{0}' which returns '{1}'", "Correctness", From 050c1329d265573f539331001b68ef99292a14c4 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Fri, 29 Dec 2023 13:57:48 +0100 Subject: [PATCH 07/43] Add UseMapAnalyzer --- .../Analyzers/UseMapAnalyzerTests.cs | 59 ++++++++++++++ .../Analyzers/UseMapAnalyzer.cs | 79 +++++++++++++++++++ src/Rascal.Analysis/Diagnostics.cs | 10 +++ 3 files changed, 148 insertions(+) create mode 100644 src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs create mode 100644 src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs diff --git a/src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs new file mode 100644 index 0000000..3615209 --- /dev/null +++ b/src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs @@ -0,0 +1,59 @@ +using VerifyCS = Rascal.Analysis.Tests.Verifiers.AnalyzerVerifier; + +namespace Rascal.Analysis.Analyzers.Tests; + +public class UseMapAnalyzerTests +{ + [Fact] + public async Task DoesNothingUsually() => await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = result.Then(x => F(x)); + } + + public static Result F(int x) => Ok(x + 1); + } + """); + + [Fact] + public async Task ReportsOnLambda() => await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = result.{|RASCAL0001:Then|}(x => Ok(x)); + } + } + """); + + [Fact] + public async Task ReportOnBlockBody() => await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = result.{|RASCAL0001:Then|}(x => + { + return Ok(x); + }); + } + } + """); +} diff --git a/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs new file mode 100644 index 0000000..3a78847 --- /dev/null +++ b/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs @@ -0,0 +1,79 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Rascal.Analysis.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseMapAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + Diagnostics.UseMap); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(compilationCtx => + { + var resultType = compilationCtx.Compilation.GetTypeByMetadataName("Rascal.Result`1"); + if (resultType is null) return; + + var preludeType = compilationCtx.Compilation.GetTypeByMetadataName("Rascal.Prelude"); + if (preludeType is null) return; + + var resultMembers = resultType.GetMembers(); + var preludeMembers = preludeType.GetMembers(); + + var thenMethod = (IMethodSymbol)resultMembers.First(x => x.Name == "Then"); + var okMethod = (IMethodSymbol)preludeMembers.First(x => x.Name == "Ok"); + var okCtor = resultType.InstanceConstructors + .First(x => x.Parameters is [{ Type: ITypeParameterSymbol }]); + + compilationCtx.RegisterOperationAction(operationCtx => + { + var operation = (IInvocationOperation)operationCtx.Operation; + + // Check that it is Then being called. + if (!operation.TargetMethod.OriginalDefinition.Equals(thenMethod, SymbolEqualityComparer.Default)) return; + + // Check that the first argument is a lambda. + if (operation.Arguments is not + [ + { + Value: IDelegateCreationOperation + { + Target: IAnonymousFunctionOperation + { + Body: var body + } + } + } + ]) return; + + // Check that the body is a single return operation. + if (body.Operations is not [IReturnOperation returnOperation]) return; + + // Check that the returned expression is an invocation. + if (returnOperation.ReturnedValue is not IInvocationOperation returnInvocation) return; + + // Check that the return invocation expression is calling either Ok or new(T). + if (!returnInvocation.TargetMethod.OriginalDefinition.Equals(okMethod, SymbolEqualityComparer.Default) && + !returnInvocation.TargetMethod.OriginalDefinition.Equals(okCtor, SymbolEqualityComparer.Default)) return; + + // Get the syntax of the method name. + var invocationSyntax = (InvocationExpressionSyntax)operation.Syntax; + var memberAccess = (MemberAccessExpressionSyntax)invocationSyntax.Expression; + var location = memberAccess.Name.GetLocation(); + + // Report the diagnostic. + operationCtx.ReportDiagnostic(Diagnostic.Create( + Diagnostics.UseMap, + location)); + }, OperationKind.Invocation); + }); + } +} diff --git a/src/Rascal.Analysis/Diagnostics.cs b/src/Rascal.Analysis/Diagnostics.cs index 2e42c05..366ac3a 100644 --- a/src/Rascal.Analysis/Diagnostics.cs +++ b/src/Rascal.Analysis/Diagnostics.cs @@ -4,6 +4,16 @@ namespace Rascal.Analysis; public static class Diagnostics { + public static DiagnosticDescriptor UseMap { get; } = new( + "RASCAL0001", + "Use Map instead of Then and Ok", + "Use Map instead of calling Ok directly inside Then", + "Correctness", + DiagnosticSeverity.Warning, + true, + "Calling Ok directly inside a Then call is equivalent to calling Map. " + + "Use Map instead for clarity and performance."); + public static DiagnosticDescriptor DoNotThrow { get; } = new( "RASCAL0002", "Do not throw inside a result method or property", From 0872fd4c34b432b8e65ebbddc38aa4dd08ef2a9a Mon Sep 17 00:00:00 2001 From: thinker227 Date: Fri, 29 Dec 2023 13:58:54 +0100 Subject: [PATCH 08/43] Remove ThrowAnalayzer --- .../Analyzers/ThrowAnalyzerTests.cs | 95 ---------------- .../Analyzers/ThrowAnalayzer.cs | 101 ------------------ src/Rascal.Analysis/Diagnostics.cs | 10 -- 3 files changed, 206 deletions(-) delete mode 100644 src/Rascal.Analysis.Tests/Analyzers/ThrowAnalyzerTests.cs delete mode 100644 src/Rascal.Analysis/Analyzers/ThrowAnalayzer.cs diff --git a/src/Rascal.Analysis.Tests/Analyzers/ThrowAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/ThrowAnalyzerTests.cs deleted file mode 100644 index 56c7b89..0000000 --- a/src/Rascal.Analysis.Tests/Analyzers/ThrowAnalyzerTests.cs +++ /dev/null @@ -1,95 +0,0 @@ -using VerifyCS = Rascal.Analysis.Tests.Verifiers.AnalyzerVerifier; - -namespace Rascal.Analysis.Analyzers.Tests; - -public class ThrowAnalyzerTests -{ - [Fact] - public async Task DoesNothingUsually() => await VerifyCS.VerifyAnalyzerAsync(""" - using System; - using Rascal; - - public static class Foo - { - public static Result Bar() - { - return new(2); - } - } - """); - - [Fact] - public async Task ThrowInsideResultMethod() => await VerifyCS.VerifyAnalyzerAsync(""" - using System; - using Rascal; - - public static class Foo - { - public static Result Bar() - { - {|RASCAL0002:throw|} new Exception(); - } - } - """); - - [Fact] - public async Task ThrowInsideLocalFunction() => await VerifyCS.VerifyAnalyzerAsync(""" - using System; - using Rascal; - - public static class Foo - { - public static void Bar() - { - Baz(); - - static Result Baz() - { - {|RASCAL0002:throw|} new Exception(); - } - } - } - """); - - [Fact] - public async Task ThrowInsideLambda() => await VerifyCS.VerifyAnalyzerAsync(""" - using System; - using Rascal; - - public static class Foo - { - public static Func> func = () => - { - {|RASCAL0002:throw|} new Exception(); - }; - } - """); - - [Fact] - public async Task ThrowInsideExpressionProperty() => await VerifyCS.VerifyAnalyzerAsync(""" - using System; - using Rascal; - - public static class Foo - { - public static Result X => {|RASCAL0002:throw|} new Exception(); - } - """); - - [Fact] - public async Task ThrowInsideExplicitProperty() => await VerifyCS.VerifyAnalyzerAsync(""" - using System; - using Rascal; - - public static class Foo - { - public static Result X - { - get - { - {|RASCAL0002:throw|} new Exception(); - } - } - } - """); -} diff --git a/src/Rascal.Analysis/Analyzers/ThrowAnalayzer.cs b/src/Rascal.Analysis/Analyzers/ThrowAnalayzer.cs deleted file mode 100644 index 352cdf8..0000000 --- a/src/Rascal.Analysis/Analyzers/ThrowAnalayzer.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; - -namespace Rascal.Analysis.Analyzers; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class ThrowAnalyzer : DiagnosticAnalyzer -{ - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( - Diagnostics.DoNotThrow); - - public override void Initialize(AnalysisContext context) - { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); - - context.RegisterCompilationStartAction(compilationCtx => - { - var resultType = compilationCtx.Compilation.GetTypeByMetadataName("Rascal.Result`1"); - if (resultType is null) return; - - compilationCtx.RegisterOperationAction(operationCtx => - { - var operation = (IThrowOperation)operationCtx.Operation; - - ISymbol? symbol = GetParentMethodLikeOperation(operation) switch - { - IMethodBodyBaseOperation => operationCtx.ContainingSymbol switch - { - IMethodSymbol x => x, - IPropertySymbol x => x, - _ => null, - }, - ILocalFunctionOperation x => x.Symbol, - IAnonymousFunctionOperation x => x.Symbol, - _ => null, - }; - - if (symbol is null) return; - - var returnType = symbol switch - { - IMethodSymbol x => x.ReturnType, - IPropertySymbol x => x.Type, - _ => throw new InvalidOperationException(), - }; - - if (!returnType.OriginalDefinition.Equals(resultType, SymbolEqualityComparer.Default)) return; - - var location = operation.Syntax switch - { - ThrowStatementSyntax x => x.ThrowKeyword.GetLocation(), - ThrowExpressionSyntax x => x.ThrowKeyword.GetLocation(), - _ => throw new InvalidOperationException( - "Syntax of throw operation is not a throw statement syntax or throw expression syntax."), - }; - - operationCtx.ReportDiagnostic(Diagnostic.Create( - Diagnostics.DoNotThrow, - location, - symbol.Name, - returnType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); - }, OperationKind.Throw); - }); - } - - private static IOperation? GetParentMethodLikeOperation(IOperation operation) - { - var current = operation.Parent; - - while (current is not null) - { - if (current is IMethodBodyBaseOperation or ILocalFunctionOperation or IAnonymousFunctionOperation) - return current; - - current = current.Parent; - } - - return null; - } - - private static TOperation? GetParentOperation(IOperation op) - where TOperation : class, IOperation - { - var current = op; - - while (current is not null) - { - if (current is TOperation x) return x; - current = current.Parent; - } - - return null; - } - - private static IMethodSymbol? GetSymbolOfMethodOperation(IMethodBodyBaseOperation operation) => - operation.SemanticModel?.GetDeclaredSymbol(operation.Syntax) as IMethodSymbol; -} diff --git a/src/Rascal.Analysis/Diagnostics.cs b/src/Rascal.Analysis/Diagnostics.cs index 366ac3a..fdfce9a 100644 --- a/src/Rascal.Analysis/Diagnostics.cs +++ b/src/Rascal.Analysis/Diagnostics.cs @@ -13,14 +13,4 @@ public static class Diagnostics true, "Calling Ok directly inside a Then call is equivalent to calling Map. " + "Use Map instead for clarity and performance."); - - public static DiagnosticDescriptor DoNotThrow { get; } = new( - "RASCAL0002", - "Do not throw inside a result method or property", - "Do not throw exceptions inside method or property '{0}' which returns '{1}'", - "Correctness", - DiagnosticSeverity.Warning, - true, - "Methods or properties which return results should not throw exceptions unless absolutely necessary. " + - "Instead consider returning `Err()` with a description of the error."); } From de5f10122096ea11fdf42efa321fc29cc6cd924d Mon Sep 17 00:00:00 2001 From: thinker227 Date: Fri, 29 Dec 2023 14:21:40 +0100 Subject: [PATCH 09/43] Add UseThenAnalyzer --- .../Analyzers/UseThenAnalyzerTests.cs | 54 ++++++++++++++++ .../Analyzers/UseThenAnalyzer.cs | 62 +++++++++++++++++++ src/Rascal.Analysis/Diagnostics.cs | 12 +++- 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 src/Rascal.Analysis.Tests/Analyzers/UseThenAnalyzerTests.cs create mode 100644 src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs diff --git a/src/Rascal.Analysis.Tests/Analyzers/UseThenAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/UseThenAnalyzerTests.cs new file mode 100644 index 0000000..926ca5e --- /dev/null +++ b/src/Rascal.Analysis.Tests/Analyzers/UseThenAnalyzerTests.cs @@ -0,0 +1,54 @@ +using VerifyCS = Rascal.Analysis.Tests.Verifiers.AnalyzerVerifier; + +namespace Rascal.Analysis.Analyzers.Tests; + +public class UseThenAnalyzerTests +{ + [Fact] + public async Task DoesNothingUsually() => await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = result.Map(x => Ok(2)); + } + } + """); + + [Fact] + public async Task ReportsOnMapUnnestExtensionForm() => await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = result.{|RASCAL0002:Map|}(x => Ok(x)).Unnest(); + } + } + """); + + [Fact] + public async Task ReportsOnMapUnnestCallForm() => await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = ResultExtensions.Unnest(result.{|RASCAL0002:Map|}(x => Ok(x))); + } + } + """); +} diff --git a/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs new file mode 100644 index 0000000..dd9ea42 --- /dev/null +++ b/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs @@ -0,0 +1,62 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Rascal.Analysis.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseThenAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + Diagnostics.UseThen); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(compilationCtx => + { + var resultType = compilationCtx.Compilation.GetTypeByMetadataName("Rascal.Result`1"); + if (resultType is null) return; + + var resultExtensionsType = compilationCtx.Compilation.GetTypeByMetadataName("Rascal.ResultExtensions"); + if (resultExtensionsType is null) return; + + var resultMembers = resultType.GetMembers(); + var resultExtensionsMembers = resultExtensionsType.GetMembers(); + + var mapMethod = (IMethodSymbol)resultMembers.First(x => x.Name == "Map"); + var unnestMethod = (IMethodSymbol)resultExtensionsMembers.First(x => x.Name == "Unnest"); + + compilationCtx.RegisterOperationAction(operationCtx => + { + var operation = (IInvocationOperation)operationCtx.Operation; + + if (!operation.TargetMethod.OriginalDefinition.Equals(unnestMethod, SymbolEqualityComparer.Default)) + return; + + if (operation.Arguments is not + [ + { + Value: IInvocationOperation argumentInvocation + } + ]) return; + + if (!argumentInvocation.TargetMethod.OriginalDefinition + .Equals(mapMethod, SymbolEqualityComparer.Default)) + return; + + var invocationSyntax = (InvocationExpressionSyntax)argumentInvocation.Syntax; + var memberAccessSyntax = (MemberAccessExpressionSyntax)invocationSyntax.Expression; + var location = memberAccessSyntax.Name.GetLocation(); + + operationCtx.ReportDiagnostic(Diagnostic.Create( + Diagnostics.UseThen, + location)); + }, OperationKind.Invocation); + }); + } +} diff --git a/src/Rascal.Analysis/Diagnostics.cs b/src/Rascal.Analysis/Diagnostics.cs index fdfce9a..8f05024 100644 --- a/src/Rascal.Analysis/Diagnostics.cs +++ b/src/Rascal.Analysis/Diagnostics.cs @@ -6,11 +6,21 @@ public static class Diagnostics { public static DiagnosticDescriptor UseMap { get; } = new( "RASCAL0001", - "Use Map instead of Then and Ok", + "Use Map instead of Then(x => Ok(...))", "Use Map instead of calling Ok directly inside Then", "Correctness", DiagnosticSeverity.Warning, true, "Calling Ok directly inside a Then call is equivalent to calling Map. " + "Use Map instead for clarity and performance."); + + public static DiagnosticDescriptor UseThen { get; } = new( + "RASCAL0002", + "Use Then instead of Map(...).Unnest()", + "Use Then instead of calling Unnest directly after Map", + "Correctness", + DiagnosticSeverity.Warning, + true, + "Calling Unnest directly after a Map call is equivalent to calling Then. " + + "Use Then instead for clarity and performance."); } From 995ee0d10ffd12117365857958d6120fcea32822 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Fri, 29 Dec 2023 14:57:26 +0100 Subject: [PATCH 10/43] Add UnnecessaryIdMapAnalyzer --- .../UnnecessaryIdMapAnalyzerTests.cs | 38 +++++++++ .../Analyzers/UnnecessaryIdMapAnalyzer.cs | 81 +++++++++++++++++++ src/Rascal.Analysis/Diagnostics.cs | 11 +++ 3 files changed, 130 insertions(+) create mode 100644 src/Rascal.Analysis.Tests/Analyzers/UnnecessaryIdMapAnalyzerTests.cs create mode 100644 src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs diff --git a/src/Rascal.Analysis.Tests/Analyzers/UnnecessaryIdMapAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/UnnecessaryIdMapAnalyzerTests.cs new file mode 100644 index 0000000..0ef6f3d --- /dev/null +++ b/src/Rascal.Analysis.Tests/Analyzers/UnnecessaryIdMapAnalyzerTests.cs @@ -0,0 +1,38 @@ +using VerifyCS = Rascal.Analysis.Tests.Verifiers.AnalyzerVerifier; + +namespace Rascal.Analysis.Analyzers.Tests; + +public class UnnecessaryIdMapAnalyzerTests +{ + [Fact] + public async Task DoesNothingUsually() => await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = result.Map(x => x + 1); + } + } + """); + + [Fact] + public async Task ReportsOnMapIdCall() => await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = result.{|RASCAL0003:Map(x => x)|}; + } + } + """); +} diff --git a/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs new file mode 100644 index 0000000..05667f9 --- /dev/null +++ b/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs @@ -0,0 +1,81 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.CodeAnalysis.Text; + +namespace Rascal.Analysis.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UnnecessaryIdMapAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + Diagnostics.UnnecessaryIdMap); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(compilationCtx => + { + var resultType = compilationCtx.Compilation.GetTypeByMetadataName("Rascal.Result`1"); + if (resultType is null) return; + + var resultMembers = resultType.GetMembers(); + + var mapMethod = (IMethodSymbol)resultMembers.First(x => x.Name == "Map"); + + compilationCtx.RegisterOperationAction(operationCtx => + { + var operation = (IInvocationOperation)operationCtx.Operation; + + // Check that it is Map being called. + if (!operation.TargetMethod.OriginalDefinition.Equals(mapMethod, SymbolEqualityComparer.Default)) + return; + + // Check that the first argument is a lambda with a single parameter. + if (operation.Arguments is not + [ + { + Value: IDelegateCreationOperation + { + Target: IAnonymousFunctionOperation + { + Body: var body, + Symbol.Parameters: + [ + var lambdaParameter + ] + } + } + } + ]) return; + + // Check that the body is a single return operation. + if (body.Operations is not [IReturnOperation returnOperation]) return; + + // Check that the returned expression is a parameter reference. + if (returnOperation.ReturnedValue is not IParameterReferenceOperation returnReference) return; + + // Check that the returned parameter is the same as the lambda parameter. + if (!returnReference.Parameter.Equals(lambdaParameter, SymbolEqualityComparer.Default)) return; + + // Get the syntax of the method invocation. + var invocationExpression = (InvocationExpressionSyntax)operation.Syntax; + var memberAccess = (MemberAccessExpressionSyntax)invocationExpression.Expression; + var start = memberAccess.Name.Span.Start; + var end = invocationExpression.ArgumentList.Span.End; + var span = TextSpan.FromBounds(start, end); + var location = Location.Create(invocationExpression.SyntaxTree, span); + + // Report the diagnostic. + operationCtx.ReportDiagnostic(Diagnostic.Create( + Diagnostics.UnnecessaryIdMap, + location, + lambdaParameter.Name)); + }, OperationKind.Invocation); + }); + } +} diff --git a/src/Rascal.Analysis/Diagnostics.cs b/src/Rascal.Analysis/Diagnostics.cs index 8f05024..0403d3c 100644 --- a/src/Rascal.Analysis/Diagnostics.cs +++ b/src/Rascal.Analysis/Diagnostics.cs @@ -23,4 +23,15 @@ public static class Diagnostics true, "Calling Unnest directly after a Map call is equivalent to calling Then. " + "Use Then instead for clarity and performance."); + + public static DiagnosticDescriptor UnnecessaryIdMap { get; } = new( + "RASCAL0003", + "Unnecessary Map call with identity function", + "This call maps {0} to itself. " + + "The call can be safely removed because it doesn't do anything", + "Correctness", + DiagnosticSeverity.Warning, + true, + "Calling Map with an identity function returns the same result as the input. " + + "Remove this call to Map."); } From e595b724ce703557cfafe1ad136808cd69de1fa4 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Fri, 29 Dec 2023 15:35:23 +0100 Subject: [PATCH 11/43] Add RemoveMapIdCallCodeFixer --- .../CodeFixes/RemoveMapIdCallCodeFixTests.cs | 33 ++++++++++++++ .../CodeFixes/RemoveMapIdCallCodeFixer.cs | 43 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 src/Rascal.Analysis.Tests/CodeFixes/RemoveMapIdCallCodeFixTests.cs create mode 100644 src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFixer.cs diff --git a/src/Rascal.Analysis.Tests/CodeFixes/RemoveMapIdCallCodeFixTests.cs b/src/Rascal.Analysis.Tests/CodeFixes/RemoveMapIdCallCodeFixTests.cs new file mode 100644 index 0000000..d28082c --- /dev/null +++ b/src/Rascal.Analysis.Tests/CodeFixes/RemoveMapIdCallCodeFixTests.cs @@ -0,0 +1,33 @@ +using VerifyCS = Rascal.Analysis.Tests.Verifiers.CodeFixVerifier; + +public class RemoveMapIdCallCodeFixProviderTests +{ + [Fact] + public async Task FixesMapIdCall() => await VerifyCS.VerifyCodeFixAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = result.{|RASCAL0003:Map(x => x)|}; + } + } + """, """ + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = result; + } + } + """); +} diff --git a/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFixer.cs b/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFixer.cs new file mode 100644 index 0000000..bed23f4 --- /dev/null +++ b/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFixer.cs @@ -0,0 +1,43 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Rascal.Analysis.CodeFixes; + +[ExportCodeFixProvider(LanguageNames.CSharp)] +public sealed class RemoveMapIdCallCodeFix : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create( + Diagnostics.UnnecessaryIdMap.Id); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext ctx) + { + var document = ctx.Document; + + var root = await document.GetSyntaxRootAsync(); + if (root is null) return; + + var invocation = (InvocationExpressionSyntax)root.FindNode(ctx.Span); + + var codeAction = CodeAction.Create( + "Remove Map call", + _ => Task.FromResult(ExecuteFix(document, root, invocation))); + + ctx.RegisterCodeFix(codeAction, ctx.Diagnostics); + } + + private static Document ExecuteFix( + Document document, + SyntaxNode root, + InvocationExpressionSyntax invocation) + { + var memberAccess = (MemberAccessExpressionSyntax)invocation.Expression; + var expression = memberAccess.Expression; + var newRoot = root.ReplaceNode(invocation, expression); + return document.WithSyntaxRoot(newRoot); + } +} From 2bd09aed089d2a4c3d80783a869591b6ba5211cb Mon Sep 17 00:00:00 2001 From: thinker227 Date: Fri, 29 Dec 2023 19:33:25 +0100 Subject: [PATCH 12/43] Add more comments --- src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs | 2 +- src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs | 6 +++--- src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs | 5 +++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs index 05667f9..fbdd79c 100644 --- a/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs @@ -62,7 +62,7 @@ var lambdaParameter // Check that the returned parameter is the same as the lambda parameter. if (!returnReference.Parameter.Equals(lambdaParameter, SymbolEqualityComparer.Default)) return; - // Get the syntax of the method invocation. + // Get the location of the method invocation. var invocationExpression = (InvocationExpressionSyntax)operation.Syntax; var memberAccess = (MemberAccessExpressionSyntax)invocationExpression.Expression; var start = memberAccess.Name.Span.Start; diff --git a/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs index 3a78847..f952aee 100644 --- a/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs @@ -64,10 +64,10 @@ public override void Initialize(AnalysisContext context) if (!returnInvocation.TargetMethod.OriginalDefinition.Equals(okMethod, SymbolEqualityComparer.Default) && !returnInvocation.TargetMethod.OriginalDefinition.Equals(okCtor, SymbolEqualityComparer.Default)) return; - // Get the syntax of the method name. + // Get the location of the method name. var invocationSyntax = (InvocationExpressionSyntax)operation.Syntax; - var memberAccess = (MemberAccessExpressionSyntax)invocationSyntax.Expression; - var location = memberAccess.Name.GetLocation(); + var memberAccessSyntax = (MemberAccessExpressionSyntax)invocationSyntax.Expression; + var location = memberAccessSyntax.Name.GetLocation(); // Report the diagnostic. operationCtx.ReportDiagnostic(Diagnostic.Create( diff --git a/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs index dd9ea42..67cf076 100644 --- a/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs @@ -35,9 +35,11 @@ public override void Initialize(AnalysisContext context) { var operation = (IInvocationOperation)operationCtx.Operation; + // Check that it is Unnest being called. if (!operation.TargetMethod.OriginalDefinition.Equals(unnestMethod, SymbolEqualityComparer.Default)) return; + // Check that the first argument is an invocation. if (operation.Arguments is not [ { @@ -45,14 +47,17 @@ public override void Initialize(AnalysisContext context) } ]) return; + // Check that the invoked method is Map. if (!argumentInvocation.TargetMethod.OriginalDefinition .Equals(mapMethod, SymbolEqualityComparer.Default)) return; + // Get the location of the method name. var invocationSyntax = (InvocationExpressionSyntax)argumentInvocation.Syntax; var memberAccessSyntax = (MemberAccessExpressionSyntax)invocationSyntax.Expression; var location = memberAccessSyntax.Name.GetLocation(); + // Report the diagnostic. operationCtx.ReportDiagnostic(Diagnostic.Create( Diagnostics.UseThen, location)); From 7b522edc52eef2b290d2cc7552e42ea9a25f9bce Mon Sep 17 00:00:00 2001 From: thinker227 Date: Fri, 29 Dec 2023 19:34:32 +0100 Subject: [PATCH 13/43] Rename RemoveMapIdCallCodeFixer.cs --- .../{RemoveMapIdCallCodeFixer.cs => RemoveMapIdCallCodeFix.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Rascal.Analysis/CodeFixes/{RemoveMapIdCallCodeFixer.cs => RemoveMapIdCallCodeFix.cs} (100%) diff --git a/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFixer.cs b/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFix.cs similarity index 100% rename from src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFixer.cs rename to src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFix.cs From 8d7331b099db2d24343d8dbd91cbf895acfba827 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Fri, 29 Dec 2023 19:50:02 +0100 Subject: [PATCH 14/43] Move RemoveMapIdCallCodeFixTests into namespace --- .../CodeFixes/RemoveMapIdCallCodeFixTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Rascal.Analysis.Tests/CodeFixes/RemoveMapIdCallCodeFixTests.cs b/src/Rascal.Analysis.Tests/CodeFixes/RemoveMapIdCallCodeFixTests.cs index d28082c..6d063fc 100644 --- a/src/Rascal.Analysis.Tests/CodeFixes/RemoveMapIdCallCodeFixTests.cs +++ b/src/Rascal.Analysis.Tests/CodeFixes/RemoveMapIdCallCodeFixTests.cs @@ -1,5 +1,7 @@ using VerifyCS = Rascal.Analysis.Tests.Verifiers.CodeFixVerifier; +namespace Rascal.Analysis.CodeFixes.Tests; + public class RemoveMapIdCallCodeFixProviderTests { [Fact] From f8345fe45b0026f49f2a211d606cfc14db49e1b1 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Fri, 29 Dec 2023 19:50:11 +0100 Subject: [PATCH 15/43] Inline some pattern matching --- .../Analyzers/UnnecessaryIdMapAnalyzer.cs | 19 ++++++++----------- .../Analyzers/UseMapAnalyzer.cs | 16 ++++++++-------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs index fbdd79c..bceaee7 100644 --- a/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs @@ -35,7 +35,7 @@ public override void Initialize(AnalysisContext context) if (!operation.TargetMethod.OriginalDefinition.Equals(mapMethod, SymbolEqualityComparer.Default)) return; - // Check that the first argument is a lambda with a single parameter. + // Check that the first argument is a lambda with a single parameter which immediately returns. if (operation.Arguments is not [ { @@ -43,21 +43,18 @@ public override void Initialize(AnalysisContext context) { Target: IAnonymousFunctionOperation { - Body: var body, - Symbol.Parameters: + Body.Operations: [ - var lambdaParameter - ] + IReturnOperation + { + ReturnedValue: IParameterReferenceOperation returnReference + } + ], + Symbol.Parameters: [var lambdaParameter] } } } ]) return; - - // Check that the body is a single return operation. - if (body.Operations is not [IReturnOperation returnOperation]) return; - - // Check that the returned expression is a parameter reference. - if (returnOperation.ReturnedValue is not IParameterReferenceOperation returnReference) return; // Check that the returned parameter is the same as the lambda parameter. if (!returnReference.Parameter.Equals(lambdaParameter, SymbolEqualityComparer.Default)) return; diff --git a/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs index f952aee..240ec1b 100644 --- a/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs @@ -40,7 +40,7 @@ public override void Initialize(AnalysisContext context) // Check that it is Then being called. if (!operation.TargetMethod.OriginalDefinition.Equals(thenMethod, SymbolEqualityComparer.Default)) return; - // Check that the first argument is a lambda. + // Check that the first argument is a lambda with an immediate return. if (operation.Arguments is not [ { @@ -48,18 +48,18 @@ public override void Initialize(AnalysisContext context) { Target: IAnonymousFunctionOperation { - Body: var body + Body.Operations: + [ + IReturnOperation + { + ReturnedValue: IInvocationOperation returnInvocation + } + ] } } } ]) return; - // Check that the body is a single return operation. - if (body.Operations is not [IReturnOperation returnOperation]) return; - - // Check that the returned expression is an invocation. - if (returnOperation.ReturnedValue is not IInvocationOperation returnInvocation) return; - // Check that the return invocation expression is calling either Ok or new(T). if (!returnInvocation.TargetMethod.OriginalDefinition.Equals(okMethod, SymbolEqualityComparer.Default) && !returnInvocation.TargetMethod.OriginalDefinition.Equals(okCtor, SymbolEqualityComparer.Default)) return; From 611b3b3afae395f6115cab615590a539361b233f Mon Sep 17 00:00:00 2001 From: thinker227 Date: Tue, 2 Jan 2024 10:29:16 +0100 Subject: [PATCH 16/43] Update messages --- src/Rascal.Analysis/Diagnostics.cs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Rascal.Analysis/Diagnostics.cs b/src/Rascal.Analysis/Diagnostics.cs index 0403d3c..fa0af6e 100644 --- a/src/Rascal.Analysis/Diagnostics.cs +++ b/src/Rascal.Analysis/Diagnostics.cs @@ -6,32 +6,32 @@ public static class Diagnostics { public static DiagnosticDescriptor UseMap { get; } = new( "RASCAL0001", - "Use Map instead of Then(x => Ok(...))", - "Use Map instead of calling Ok directly inside Then", + "Use 'Map' instead of 'Then(x => Ok(...))'", + "Use 'Map' instead of calling 'Ok' directly inside 'Then'", "Correctness", DiagnosticSeverity.Warning, true, - "Calling Ok directly inside a Then call is equivalent to calling Map. " + - "Use Map instead for clarity and performance."); + "Calling 'Ok' directly inside a 'Then' call is equivalent to calling 'Map'. " + + "Use 'Map' instead for clarity and performance."); public static DiagnosticDescriptor UseThen { get; } = new( "RASCAL0002", - "Use Then instead of Map(...).Unnest()", - "Use Then instead of calling Unnest directly after Map", + "Use Then instead of 'Map(...).Unnest()'", + "Use 'Then' instead of calling 'Unnest' directly after 'Map'", "Correctness", DiagnosticSeverity.Warning, true, - "Calling Unnest directly after a Map call is equivalent to calling Then. " + - "Use Then instead for clarity and performance."); + "Calling 'Unnest' directly after a 'Map' call is equivalent to calling 'Then'. " + + "Use 'Then' instead for clarity and performance."); public static DiagnosticDescriptor UnnecessaryIdMap { get; } = new( "RASCAL0003", - "Unnecessary Map call with identity function", + "Unnecessary 'Map' call with identity function", "This call maps {0} to itself. " + "The call can be safely removed because it doesn't do anything", "Correctness", DiagnosticSeverity.Warning, true, - "Calling Map with an identity function returns the same result as the input. " + - "Remove this call to Map."); + "Calling 'Map' with an identity function returns the same result as the input. " + + "Remove this call to 'Map'."); } From 511264619c9a592d2b0ec307d243a1776022c094 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Tue, 2 Jan 2024 10:57:49 +0100 Subject: [PATCH 17/43] ToSusTypeAnalyzer wip --- .../Analyzers/ToSusTypeAnalyzer.cs | 69 +++++++++++++++++++ src/Rascal.Analysis/Diagnostics.cs | 21 ++++++ src/Rascal.Analysis/SymbolExtensions.cs | 13 ++++ 3 files changed, 103 insertions(+) create mode 100644 src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs create mode 100644 src/Rascal.Analysis/SymbolExtensions.cs diff --git a/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs b/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs new file mode 100644 index 0000000..b555963 --- /dev/null +++ b/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs @@ -0,0 +1,69 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Rascal.Analysis.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ToSusTypeAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + Diagnostics.ToSameType, + Diagnostics.ToImpossibleType); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(compilationCtx => + { + var resultType = compilationCtx.Compilation.GetTypeByMetadataName("Rascal.Result`1"); + if (resultType is null) return; + + var resultMembers = resultType.GetMembers(); + + var toMethod = (IMethodSymbol)resultMembers.First(x => x.Name == "To"); + + var objectType = compilationCtx.Compilation.GetSpecialType(SpecialType.System_Object); + + compilationCtx.RegisterOperationAction(operationCtx => + { + var operation = (IInvocationOperation)operationCtx.Operation; + + var method = operation.TargetMethod; + if (!method.OriginalDefinition.Equals(toMethod, SymbolEqualityComparer.Default)) return; + + var invocationSyntax = (InvocationExpressionSyntax)operation.Syntax; + var nameSyntax = (GenericNameSyntax)invocationSyntax.Expression; + var typeSyntax = nameSyntax.TypeArgumentList.Arguments[0]; + var location = typeSyntax.GetLocation(); + + var sourceType = method.ContainingType; + var targetType = method.TypeArguments[0]; + + if (sourceType.Equals(targetType, SymbolEqualityComparer.Default)) + { + operationCtx.ReportDiagnostic(Diagnostic.Create( + Diagnostics.ToSameType, + location, + sourceType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); + return; + } + + switch (sourceType.TypeKind, targetType.TypeKind) + { + case (_, _) + when sourceType.Equals(objectType, SymbolEqualityComparer.Default) || + targetType.Equals(objectType, SymbolEqualityComparer.Default): return; + case (TypeKind.Interface, _) or (_, TypeKind.Interface): return; + // case (TypeKind.Class, TypeKind.Class) when // inherits + } + }, OperationKind.Invocation); + }); + } + + private static bool Inherits(INamedTypeSymbol type, INamedTypeSymbol inherits) +} diff --git a/src/Rascal.Analysis/Diagnostics.cs b/src/Rascal.Analysis/Diagnostics.cs index fa0af6e..ce0f7d6 100644 --- a/src/Rascal.Analysis/Diagnostics.cs +++ b/src/Rascal.Analysis/Diagnostics.cs @@ -34,4 +34,25 @@ public static class Diagnostics true, "Calling 'Map' with an identity function returns the same result as the input. " + "Remove this call to 'Map'."); + + public static DiagnosticDescriptor ToSameType { get; } = new( + "RASCAL0004", + "'To' called with same type as result", + "This call converts '{0}' to itself and will always succeed. " + + "Remove this call to 'To'.", + "Correctness", + DiagnosticSeverity.Warning, + true, + "Calling 'To' with the same type as that of the result will always succeed. " + + "Remove this call to 'To'."); + + public static DiagnosticDescriptor ToImpossibleType { get; } = new( + "RASCAL0005", + "'To' called with impossible type", + "This call tries to convert '{0}' to '{1}', but no value of type '{0}' can be of type '{1}'. " + + "The conversion will always fail", + "Correctness", + DiagnosticSeverity.Warning, + true, + "Calling 'To' with a type which no values of the type of the result permit will always fail."); } diff --git a/src/Rascal.Analysis/SymbolExtensions.cs b/src/Rascal.Analysis/SymbolExtensions.cs new file mode 100644 index 0000000..1112aff --- /dev/null +++ b/src/Rascal.Analysis/SymbolExtensions.cs @@ -0,0 +1,13 @@ +using Microsoft.CodeAnalysis; + +namespace Rascal.Analysis; + +public static class SymbolExtensions +{ + public static bool Inherits(INamedTypeSymbol x, INamedTypeSymbol type) + { + var t = x; + + throw new NotImplementedException(); + } +} From da2a8fb2144fee12b8d127381193a4a706f180be Mon Sep 17 00:00:00 2001 From: thinker227 Date: Tue, 2 Jan 2024 13:25:22 +0100 Subject: [PATCH 18/43] Finish implementing ToSusTypeAnalyzer --- .../Analyzers/ToSusTypeAnalyzerTests.cs | 164 ++++++++++++++++++ .../Analyzers/ToSusTypeAnalyzer.cs | 30 +++- src/Rascal.Analysis/Diagnostics.cs | 9 +- src/Rascal.Analysis/SymbolExtensions.cs | 10 +- 4 files changed, 198 insertions(+), 15 deletions(-) create mode 100644 src/Rascal.Analysis.Tests/Analyzers/ToSusTypeAnalyzerTests.cs diff --git a/src/Rascal.Analysis.Tests/Analyzers/ToSusTypeAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/ToSusTypeAnalyzerTests.cs new file mode 100644 index 0000000..a63504a --- /dev/null +++ b/src/Rascal.Analysis.Tests/Analyzers/ToSusTypeAnalyzerTests.cs @@ -0,0 +1,164 @@ +using VerifyCS = Rascal.Analysis.Tests.Verifiers.AnalyzerVerifier; + +namespace Rascal.Analysis.Analyzers.Tests; + +public class ToSusTypeAnalyzerTests +{ + [Fact] + public async Task Reports_SameType_OnSameType() => await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var x = Ok(2); + var r = x.To<{|RASCAL0004:int|}>(); + } + } + """); + + [Fact] + public async Task Reports_ImpossibleType_OnBothStructTypes() => await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var x = Ok(2); + var r = x.To<{|RASCAL0005:bool|}>(); + } + } + """); + + [Fact] + public async Task Reports_ImpossibleType_OnBothClassTypes_OutsideHierarchy() => await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var x = Ok(new B()); + var r = x.To<{|RASCAL0005:C|}>(); + } + } + + class A {} + class B : A {} + class C : A {} + """); + + [Fact] + public async Task DoesNotReport_OnBothClassTypes_InsideHierarchy_Up() => await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var x = Ok(new B()); + var r = x.To(); + } + } + + class A {} + class B : A {} + """); + + [Fact] + public async Task DoesNotReport_OnBothClassTypes_InsideHierarchy_Down() => await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var x = Ok(new A()); + var r = x.To(); + } + } + + class A {} + class B : A {} + """); + + [Fact] + public async Task DoesNotReport_OnFromInterfaceType() => await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var x = Err("error"); + var r = x.To(); + } + } + + interface I {} + """); + + [Fact] + public async Task DoesNotReport_OnToInterfaceType() => await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var x = Ok(2); + var r = x.To(); + } + } + + interface I {} + """); + + [Fact] + public async Task DoesNotReport_OnFromTypeParameter() => await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var x = Err("error"); + var r = x.To(); + } + } + """); + + [Fact] + public async Task DoesNotReport_OnToTypeParameter() => await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var x = Ok(2); + var r = x.To(); + } + } + """); +} diff --git a/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs b/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs index b555963..034b372 100644 --- a/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs @@ -37,11 +37,12 @@ public override void Initialize(AnalysisContext context) if (!method.OriginalDefinition.Equals(toMethod, SymbolEqualityComparer.Default)) return; var invocationSyntax = (InvocationExpressionSyntax)operation.Syntax; - var nameSyntax = (GenericNameSyntax)invocationSyntax.Expression; + var memberAccessSyntax = (MemberAccessExpressionSyntax)invocationSyntax.Expression; + var nameSyntax = (GenericNameSyntax)memberAccessSyntax.Name; var typeSyntax = nameSyntax.TypeArgumentList.Arguments[0]; var location = typeSyntax.GetLocation(); - var sourceType = method.ContainingType; + var sourceType = method.ContainingType.TypeArguments[0]; var targetType = method.TypeArguments[0]; if (sourceType.Equals(targetType, SymbolEqualityComparer.Default)) @@ -53,17 +54,30 @@ public override void Initialize(AnalysisContext context) return; } + // Return early if the types are compatible switch (sourceType.TypeKind, targetType.TypeKind) { - case (_, _) - when sourceType.Equals(objectType, SymbolEqualityComparer.Default) || - targetType.Equals(objectType, SymbolEqualityComparer.Default): return; + // One of the types is an interface case (TypeKind.Interface, _) or (_, TypeKind.Interface): return; - // case (TypeKind.Class, TypeKind.Class) when // inherits + // One of the types is a type parameter + case (TypeKind.TypeParameter, _) or (_, TypeKind.TypeParameter): return; + // One of the types is object + case (_, _) when + sourceType.Equals(objectType, SymbolEqualityComparer.Default) || + targetType.Equals(objectType, SymbolEqualityComparer.Default): return; + // Both types are classes and one inherits the other + case (TypeKind.Class, TypeKind.Class) when + sourceType.Inherits(targetType) || + targetType.Inherits(sourceType): return; } + + // The types are sus + operationCtx.ReportDiagnostic(Diagnostic.Create( + Diagnostics.ToImpossibleType, + location, + sourceType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), + targetType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); }, OperationKind.Invocation); }); } - - private static bool Inherits(INamedTypeSymbol type, INamedTypeSymbol inherits) } diff --git a/src/Rascal.Analysis/Diagnostics.cs b/src/Rascal.Analysis/Diagnostics.cs index ce0f7d6..1846fdf 100644 --- a/src/Rascal.Analysis/Diagnostics.cs +++ b/src/Rascal.Analysis/Diagnostics.cs @@ -39,20 +39,19 @@ public static class Diagnostics "RASCAL0004", "'To' called with same type as result", "This call converts '{0}' to itself and will always succeed. " + - "Remove this call to 'To'.", + "Remove this call to 'To' as it doesn't do anything.", "Correctness", DiagnosticSeverity.Warning, true, - "Calling 'To' with the same type as that of the result will always succeed. " + - "Remove this call to 'To'."); + "Calling 'To' with the same type as that of the result will always succeed."); public static DiagnosticDescriptor ToImpossibleType { get; } = new( "RASCAL0005", "'To' called with impossible type", "This call tries to convert '{0}' to '{1}', but no value of type '{0}' can be of type '{1}'. " + - "The conversion will always fail", + "The conversion will always fail.", "Correctness", DiagnosticSeverity.Warning, true, - "Calling 'To' with a type which no values of the type of the result permit will always fail."); + "Calling 'To' with a type which no value of the type of the result permits will always fail."); } diff --git a/src/Rascal.Analysis/SymbolExtensions.cs b/src/Rascal.Analysis/SymbolExtensions.cs index 1112aff..145b801 100644 --- a/src/Rascal.Analysis/SymbolExtensions.cs +++ b/src/Rascal.Analysis/SymbolExtensions.cs @@ -4,10 +4,16 @@ namespace Rascal.Analysis; public static class SymbolExtensions { - public static bool Inherits(INamedTypeSymbol x, INamedTypeSymbol type) + public static bool Inherits(this ITypeSymbol x, ITypeSymbol type) { var t = x; - throw new NotImplementedException(); + while (t is not null) + { + if (t.Equals(type, SymbolEqualityComparer.Default)) return true; + t = t.BaseType; + } + + return false; } } From 34dc7a0d6580be2a45fb85635802901cfb3fb308 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Tue, 2 Jan 2024 13:27:07 +0100 Subject: [PATCH 19/43] Specify equivalence key --- src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFix.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFix.cs b/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFix.cs index bed23f4..7e5fdf4 100644 --- a/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFix.cs +++ b/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFix.cs @@ -25,7 +25,8 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext ctx) var codeAction = CodeAction.Create( "Remove Map call", - _ => Task.FromResult(ExecuteFix(document, root, invocation))); + _ => Task.FromResult(ExecuteFix(document, root, invocation)), + nameof(RemoveMapIdCallCodeFix)); ctx.RegisterCodeFix(codeAction, ctx.Diagnostics); } From 3447d250884bfc6bc16d84cdb192f8e7778f6b74 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Tue, 2 Jan 2024 13:36:00 +0100 Subject: [PATCH 20/43] Add Usings.cs --- src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs | 4 ---- .../Analyzers/UnnecessaryIdMapAnalyzer.cs | 4 ---- src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs | 4 ---- src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs | 4 ---- .../CodeFixes/RemoveMapIdCallCodeFix.cs | 3 --- src/Rascal.Analysis/Diagnostics.cs | 12 ++++++++++-- src/Rascal.Analysis/SymbolExtensions.cs | 2 -- src/Rascal.Analysis/Usings.cs | 5 +++++ 8 files changed, 15 insertions(+), 23 deletions(-) create mode 100644 src/Rascal.Analysis/Usings.cs diff --git a/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs b/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs index 034b372..4ebff81 100644 --- a/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs @@ -1,7 +1,3 @@ -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; namespace Rascal.Analysis.Analyzers; diff --git a/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs index bceaee7..c1b7375 100644 --- a/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs @@ -1,7 +1,3 @@ -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.Text; diff --git a/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs index 240ec1b..e2a1846 100644 --- a/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs @@ -1,7 +1,3 @@ -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; namespace Rascal.Analysis.Analyzers; diff --git a/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs index 67cf076..e2e942e 100644 --- a/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs @@ -1,7 +1,3 @@ -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; namespace Rascal.Analysis.Analyzers; diff --git a/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFix.cs b/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFix.cs index 7e5fdf4..85b2e50 100644 --- a/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFix.cs +++ b/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFix.cs @@ -1,8 +1,5 @@ -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; -using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Rascal.Analysis.CodeFixes; diff --git a/src/Rascal.Analysis/Diagnostics.cs b/src/Rascal.Analysis/Diagnostics.cs index 1846fdf..7d5d488 100644 --- a/src/Rascal.Analysis/Diagnostics.cs +++ b/src/Rascal.Analysis/Diagnostics.cs @@ -1,5 +1,3 @@ -using Microsoft.CodeAnalysis; - namespace Rascal.Analysis; public static class Diagnostics @@ -54,4 +52,14 @@ public static class Diagnostics DiagnosticSeverity.Warning, true, "Calling 'To' with a type which no value of the type of the result permits will always fail."); + + public static DiagnosticDescriptor UseDefaultOrForIdMatch { get; } = new( + "RASCAL0006", + "Use 'DefaultOr' instead of 'Match(x => x, ...)'", + "This call matches {x} using an identity function. " + + "Use 'DefaultOr' instead to reduce allocations.", + "Correctness", + DiagnosticSeverity.Warning, + true, + "Calling 'Match' with an identity function for the 'ifOk' parameter is equivalent to 'DefaultOr'."); } diff --git a/src/Rascal.Analysis/SymbolExtensions.cs b/src/Rascal.Analysis/SymbolExtensions.cs index 145b801..59134dc 100644 --- a/src/Rascal.Analysis/SymbolExtensions.cs +++ b/src/Rascal.Analysis/SymbolExtensions.cs @@ -1,5 +1,3 @@ -using Microsoft.CodeAnalysis; - namespace Rascal.Analysis; public static class SymbolExtensions diff --git a/src/Rascal.Analysis/Usings.cs b/src/Rascal.Analysis/Usings.cs new file mode 100644 index 0000000..4affaf4 --- /dev/null +++ b/src/Rascal.Analysis/Usings.cs @@ -0,0 +1,5 @@ +global using System.Collections.Immutable; +global using Microsoft.CodeAnalysis; +global using Microsoft.CodeAnalysis.Diagnostics; +global using Microsoft.CodeAnalysis.CSharp; +global using Microsoft.CodeAnalysis.CSharp.Syntax; From 3bc7c01134fdf663e9d1119ab48b0c199132a13b Mon Sep 17 00:00:00 2001 From: thinker227 Date: Tue, 2 Jan 2024 17:22:52 +0100 Subject: [PATCH 21/43] Use pattern matching instead of hard-casts --- src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs | 14 ++++++++++---- .../Analyzers/UnnecessaryIdMapAnalyzer.cs | 14 +++++++++----- src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs | 8 +++++--- src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs | 8 +++++--- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs b/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs index 4ebff81..618bc39 100644 --- a/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs @@ -32,10 +32,16 @@ public override void Initialize(AnalysisContext context) var method = operation.TargetMethod; if (!method.OriginalDefinition.Equals(toMethod, SymbolEqualityComparer.Default)) return; - var invocationSyntax = (InvocationExpressionSyntax)operation.Syntax; - var memberAccessSyntax = (MemberAccessExpressionSyntax)invocationSyntax.Expression; - var nameSyntax = (GenericNameSyntax)memberAccessSyntax.Name; - var typeSyntax = nameSyntax.TypeArgumentList.Arguments[0]; + if (operation.Syntax is not InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax + { + Name: GenericNameSyntax + { + TypeArgumentList.Arguments: [var typeSyntax] + } + } + }) return; var location = typeSyntax.GetLocation(); var sourceType = method.ContainingType.TypeArguments[0]; diff --git a/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs index c1b7375..f01f0b2 100644 --- a/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs @@ -56,12 +56,16 @@ public override void Initialize(AnalysisContext context) if (!returnReference.Parameter.Equals(lambdaParameter, SymbolEqualityComparer.Default)) return; // Get the location of the method invocation. - var invocationExpression = (InvocationExpressionSyntax)operation.Syntax; - var memberAccess = (MemberAccessExpressionSyntax)invocationExpression.Expression; - var start = memberAccess.Name.Span.Start; - var end = invocationExpression.ArgumentList.Span.End; + if (operation.Syntax is not InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax + { + Name.Span.Start: var start + }, + Span.End: var end + }) return; var span = TextSpan.FromBounds(start, end); - var location = Location.Create(invocationExpression.SyntaxTree, span); + var location = Location.Create(operation.Syntax.SyntaxTree, span); // Report the diagnostic. operationCtx.ReportDiagnostic(Diagnostic.Create( diff --git a/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs index e2a1846..8c92f83 100644 --- a/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs @@ -61,9 +61,11 @@ public override void Initialize(AnalysisContext context) !returnInvocation.TargetMethod.OriginalDefinition.Equals(okCtor, SymbolEqualityComparer.Default)) return; // Get the location of the method name. - var invocationSyntax = (InvocationExpressionSyntax)operation.Syntax; - var memberAccessSyntax = (MemberAccessExpressionSyntax)invocationSyntax.Expression; - var location = memberAccessSyntax.Name.GetLocation(); + if (operation.Syntax is not MemberAccessExpressionSyntax + { + Expression: MemberAccessExpressionSyntax memberAccessExpression + }) return; + var location = memberAccessExpression.GetLocation(); // Report the diagnostic. operationCtx.ReportDiagnostic(Diagnostic.Create( diff --git a/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs index e2e942e..c8321d4 100644 --- a/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs @@ -49,9 +49,11 @@ public override void Initialize(AnalysisContext context) return; // Get the location of the method name. - var invocationSyntax = (InvocationExpressionSyntax)argumentInvocation.Syntax; - var memberAccessSyntax = (MemberAccessExpressionSyntax)invocationSyntax.Expression; - var location = memberAccessSyntax.Name.GetLocation(); + if (argumentInvocation.Syntax is not InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax memberAccessExpression + }) return; + var location = memberAccessExpression.GetLocation(); // Report the diagnostic. operationCtx.ReportDiagnostic(Diagnostic.Create( From 78913e06dd030f95dd43194b63f86820050fb702 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Tue, 2 Jan 2024 17:24:47 +0100 Subject: [PATCH 22/43] Some comments --- src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs b/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs index 618bc39..fa8e0e2 100644 --- a/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs @@ -29,9 +29,11 @@ public override void Initialize(AnalysisContext context) { var operation = (IInvocationOperation)operationCtx.Operation; + // Check that the target method is To var method = operation.TargetMethod; if (!method.OriginalDefinition.Equals(toMethod, SymbolEqualityComparer.Default)) return; + // Get the location of the type argument if (operation.Syntax is not InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax @@ -44,9 +46,11 @@ public override void Initialize(AnalysisContext context) }) return; var location = typeSyntax.GetLocation(); + // Get the source and target type for the conversion var sourceType = method.ContainingType.TypeArguments[0]; var targetType = method.TypeArguments[0]; + // Report diagnostic if both source and target types are the same if (sourceType.Equals(targetType, SymbolEqualityComparer.Default)) { operationCtx.ReportDiagnostic(Diagnostic.Create( From e3d3efb12f76d948996c1799bd707ed2a287e19d Mon Sep 17 00:00:00 2001 From: thinker227 Date: Tue, 2 Jan 2024 18:03:20 +0100 Subject: [PATCH 23/43] Use ToDisplayString --- src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs index f01f0b2..c83c981 100644 --- a/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs @@ -71,7 +71,7 @@ public override void Initialize(AnalysisContext context) operationCtx.ReportDiagnostic(Diagnostic.Create( Diagnostics.UnnecessaryIdMap, location, - lambdaParameter.Name)); + lambdaParameter.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); }, OperationKind.Invocation); }); } From e8de916b29dd917e7b23f441d319a8a17808ec8d Mon Sep 17 00:00:00 2001 From: thinker227 Date: Tue, 2 Jan 2024 18:03:28 +0100 Subject: [PATCH 24/43] Fix name thing --- src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs | 2 +- src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs index 8c92f83..9736dd0 100644 --- a/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs @@ -65,7 +65,7 @@ public override void Initialize(AnalysisContext context) { Expression: MemberAccessExpressionSyntax memberAccessExpression }) return; - var location = memberAccessExpression.GetLocation(); + var location = memberAccessExpression.Name.GetLocation(); // Report the diagnostic. operationCtx.ReportDiagnostic(Diagnostic.Create( diff --git a/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs index c8321d4..89a1003 100644 --- a/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs @@ -53,7 +53,7 @@ public override void Initialize(AnalysisContext context) { Expression: MemberAccessExpressionSyntax memberAccessExpression }) return; - var location = memberAccessExpression.GetLocation(); + var location = memberAccessExpression.Name.GetLocation(); // Report the diagnostic. operationCtx.ReportDiagnostic(Diagnostic.Create( From 60056d0b0beb4f5492fe973850e97163403bdcd1 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Tue, 2 Jan 2024 18:57:03 +0100 Subject: [PATCH 25/43] Add UseDefaultOrForIdMatchAnalyzer --- .../UseDefaultOrForIdMatchAnalyzerTests.cs | 38 ++++++++++ .../UseDefaultOrForIdMatchAnalyzer.cs | 72 +++++++++++++++++++ src/Rascal.Analysis/Diagnostics.cs | 2 +- 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 src/Rascal.Analysis.Tests/Analyzers/UseDefaultOrForIdMatchAnalyzerTests.cs create mode 100644 src/Rascal.Analysis/Analyzers/UseDefaultOrForIdMatchAnalyzer.cs diff --git a/src/Rascal.Analysis.Tests/Analyzers/UseDefaultOrForIdMatchAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/UseDefaultOrForIdMatchAnalyzerTests.cs new file mode 100644 index 0000000..74b5e29 --- /dev/null +++ b/src/Rascal.Analysis.Tests/Analyzers/UseDefaultOrForIdMatchAnalyzerTests.cs @@ -0,0 +1,38 @@ +using VerifyCS = Rascal.Analysis.Tests.Verifiers.AnalyzerVerifier; + +namespace Rascal.Analysis.Analyzers.Tests; + +public class UseDefaultOrForIdMatchAnalyzerTests +{ + [Fact] + public async Task DoesNothingUsually() => await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var r = Ok(2); + var x = r.Match(x => x + 1, _ => 0); + } + } + """); + + [Fact] + public async Task ReportsOnIdMatchCall() => await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var r = Ok(2); + var x = r.{|RASCAL0006:Match|}(x => x, _ => 0); + } + } + """); +} diff --git a/src/Rascal.Analysis/Analyzers/UseDefaultOrForIdMatchAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseDefaultOrForIdMatchAnalyzer.cs new file mode 100644 index 0000000..59fb72c --- /dev/null +++ b/src/Rascal.Analysis/Analyzers/UseDefaultOrForIdMatchAnalyzer.cs @@ -0,0 +1,72 @@ +using Microsoft.CodeAnalysis.Operations; + +namespace Rascal.Analysis.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseDefaultOrForIdMatchAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + Diagnostics.UseDefaultOrForIdMatch); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(compilationCtx => + { + var resultType = compilationCtx.Compilation.GetTypeByMetadataName("Rascal.Result`1"); + if (resultType is null) return; + + var resultMembers = resultType.GetMembers(); + + var matchMethod = (IMethodSymbol)resultMembers.First(x => x.Name == "Match"); + + compilationCtx.RegisterOperationAction(operationCtx => + { + var operation = (IInvocationOperation)operationCtx.Operation; + + if (!operation.TargetMethod.OriginalDefinition.Equals(matchMethod, SymbolEqualityComparer.Default)) + return; + + // Check that the first argument is a lambda with a single parameter which immediately returns. + if (operation.Arguments is not + [ + { + Value: IDelegateCreationOperation + { + Target: IAnonymousFunctionOperation + { + Body.Operations: + [ + IReturnOperation + { + ReturnedValue: IParameterReferenceOperation returnReference + } + ], + Symbol.Parameters: [var lambdaParameter] + } + } + }, + .. + ]) return; + + // Check that the returned parameter is the same as the lambda parameter. + if (!returnReference.Parameter.Equals(lambdaParameter, SymbolEqualityComparer.Default)) return; + + // Get the location of the method invocation. + if (operation.Syntax is not InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax memberAccessExpression + }) return; + var location = memberAccessExpression.Name.GetLocation(); + + // Report the diagnostic. + operationCtx.ReportDiagnostic(Diagnostic.Create( + Diagnostics.UseDefaultOrForIdMatch, + location, + lambdaParameter.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); + }, OperationKind.Invocation); + }); + } +} diff --git a/src/Rascal.Analysis/Diagnostics.cs b/src/Rascal.Analysis/Diagnostics.cs index 7d5d488..956d736 100644 --- a/src/Rascal.Analysis/Diagnostics.cs +++ b/src/Rascal.Analysis/Diagnostics.cs @@ -56,7 +56,7 @@ public static class Diagnostics public static DiagnosticDescriptor UseDefaultOrForIdMatch { get; } = new( "RASCAL0006", "Use 'DefaultOr' instead of 'Match(x => x, ...)'", - "This call matches {x} using an identity function. " + + "This call matches {0} using an identity function. " + "Use 'DefaultOr' instead to reduce allocations.", "Correctness", DiagnosticSeverity.Warning, From d4435509d962c909a7f824c9863b3e5703254740 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Thu, 4 Jan 2024 10:55:39 +0100 Subject: [PATCH 26/43] Fix small typo --- src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs index 9736dd0..beed5b8 100644 --- a/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs @@ -61,7 +61,7 @@ public override void Initialize(AnalysisContext context) !returnInvocation.TargetMethod.OriginalDefinition.Equals(okCtor, SymbolEqualityComparer.Default)) return; // Get the location of the method name. - if (operation.Syntax is not MemberAccessExpressionSyntax + if (operation.Syntax is not InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax memberAccessExpression }) return; From ccfe839b4ef00ae69f959bad81a2e47302664b3d Mon Sep 17 00:00:00 2001 From: thinker227 Date: Thu, 4 Jan 2024 10:56:09 +0100 Subject: [PATCH 27/43] Add UseDefaultOrForIdMatchCodeFix --- .../UseDefaultOrForIdMatchCodeFixTests.cs | 95 +++++++++++++++++++ .../UseDefaultOrForIdMatchCodeFix.cs | 86 +++++++++++++++++ src/Rascal.Analysis/SyntaxInator.cs | 57 +++++++++++ 3 files changed, 238 insertions(+) create mode 100644 src/Rascal.Analysis.Tests/CodeFixes/UseDefaultOrForIdMatchCodeFixTests.cs create mode 100644 src/Rascal.Analysis/CodeFixes/UseDefaultOrForIdMatchCodeFix.cs create mode 100644 src/Rascal.Analysis/SyntaxInator.cs diff --git a/src/Rascal.Analysis.Tests/CodeFixes/UseDefaultOrForIdMatchCodeFixTests.cs b/src/Rascal.Analysis.Tests/CodeFixes/UseDefaultOrForIdMatchCodeFixTests.cs new file mode 100644 index 0000000..037de4d --- /dev/null +++ b/src/Rascal.Analysis.Tests/CodeFixes/UseDefaultOrForIdMatchCodeFixTests.cs @@ -0,0 +1,95 @@ +using VerifyCS = Rascal.Analysis.Tests.Verifiers.CodeFixVerifier; + +namespace Rascal.Analysis.CodeFixes.Tests; + +public class UseDefaultOrForIdMatchCodeFixTests +{ + [Fact] + public async Task Fixes_MatchIdCall_WithError() => await VerifyCS.VerifyCodeFixAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var r = Ok("uwu"); + var x = r.{|RASCAL0006:Match|}(x => x, e => e.Message); + } + } + """, """ + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var r = Ok("uwu"); + var x = r.GetValueOr(e => e.Message); + } + } + """); + + [Fact] + public async Task Fixes_MatchIdCall_WithDiscard_NonConstant() => await VerifyCS.VerifyCodeFixAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var r = Ok("uwu"); + var v = "owo"; + var x = r.{|RASCAL0006:Match|}(x => x, _ => v); + } + } + """, """ + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var r = Ok("uwu"); + var v = "owo"; + var x = r.GetValueOr(() => v); + } + } + """); + + [Fact] + public async Task Fixes_MatchIdCall_WithDiscard_Constant() => await VerifyCS.VerifyCodeFixAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var r = Ok("uwu"); + var x = r.{|RASCAL0006:Match|}(x => x, _ => "owo"); + } + } + """, """ + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var r = Ok("uwu"); + var x = r.GetValueOr("owo"); + } + } + """); +} diff --git a/src/Rascal.Analysis/CodeFixes/UseDefaultOrForIdMatchCodeFix.cs b/src/Rascal.Analysis/CodeFixes/UseDefaultOrForIdMatchCodeFix.cs new file mode 100644 index 0000000..75eff48 --- /dev/null +++ b/src/Rascal.Analysis/CodeFixes/UseDefaultOrForIdMatchCodeFix.cs @@ -0,0 +1,86 @@ +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Operations; + +namespace Rascal.Analysis.CodeFixes; + +[ExportCodeFixProvider(LanguageNames.CSharp)] +public sealed class UseDefaultOrForIdMatchCodeFix : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create( + Diagnostics.UseDefaultOrForIdMatch.Id); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext ctx) + { + var document = ctx.Document; + var semanticModel = await document.GetSemanticModelAsync(ctx.CancellationToken); + if (semanticModel is null) return; + + var root = await document.GetSyntaxRootAsync(ctx.CancellationToken); + if (root is null) return; + + var invocationSyntax = root.FindNode(ctx.Span).FirstAncestorOrSelf(); + if (invocationSyntax is not { Expression: MemberAccessExpressionSyntax memberAccessSyntax }) return; + + if (semanticModel.GetOperation(invocationSyntax, ctx.CancellationToken) is not IInvocationOperation operation) return; + if (operation.Arguments is not + [ + _, + { + Value: IDelegateCreationOperation + { + Target: IAnonymousFunctionOperation + { + Body: var body, + Symbol.Parameters: [var param] + } lambda + } + } + ]) return; + + if (lambda.Syntax is not LambdaExpressionSyntax lambdaSyntax) return; + + var codeAction = CodeAction.Create( + "Use DefaultOr", + _ => Task.FromResult(ExecuteFix( + document, + root, + invocationSyntax, + memberAccessSyntax, + lambdaSyntax, + body, + param)), + nameof(UseDefaultOrForIdMatchCodeFix)); + + ctx.RegisterCodeFix(codeAction, ctx.Diagnostics); + } + + private static Document ExecuteFix( + Document document, + SyntaxNode root, + InvocationExpressionSyntax invocationExpression, + MemberAccessExpressionSyntax memberAccessSyntax, + LambdaExpressionSyntax lambdaSyntax, + IBlockOperation body, + IParameterSymbol param) + { + var discard = !body + .Descendants() + .OfType() + .Any(p => p.Parameter.Equals(param, SymbolEqualityComparer.Default)); + + var newInvocationExpression = discard + ? body.Operations is [IReturnOperation { ReturnedValue.ConstantValue.HasValue: true } ret] + ? SyntaxInator.GetValueOrConstant( + memberAccessSyntax.Expression, + (ExpressionSyntax)ret.ReturnedValue.Syntax) + : SyntaxInator.GetValueOrDiscardLambda(memberAccessSyntax.Expression, lambdaSyntax) + : SyntaxInator.GetValueOrParameterizedLambda(memberAccessSyntax.Expression, lambdaSyntax); + + var newRoot = root.ReplaceNode(invocationExpression, newInvocationExpression); + + return document.WithSyntaxRoot(newRoot); + } +} diff --git a/src/Rascal.Analysis/SyntaxInator.cs b/src/Rascal.Analysis/SyntaxInator.cs new file mode 100644 index 0000000..ede6e16 --- /dev/null +++ b/src/Rascal.Analysis/SyntaxInator.cs @@ -0,0 +1,57 @@ +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Rascal.Analysis; + +// Use https://roslynquoter.azurewebsites.net/ for generating syntax code. + +internal static class SyntaxInator +{ + public static InvocationExpressionSyntax GetValueOrConstant( + ExpressionSyntax invocationTarget, + ExpressionSyntax value) => + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + invocationTarget, + IdentifierName("GetValueOr"))) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument(value)))) + .NormalizeWhitespace(); + + public static InvocationExpressionSyntax GetValueOrDiscardLambda( + ExpressionSyntax invocationTarget, + LambdaExpressionSyntax lambda) => + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + invocationTarget, + IdentifierName("GetValueOr"))) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument( + ParenthesizedLambdaExpression( + lambda.AsyncKeyword, + ParameterList(), + lambda.ArrowToken, + lambda.Body) + .WithAttributeLists(lambda.AttributeLists) + .WithModifiers(lambda.Modifiers))))) + .NormalizeWhitespace(); + + public static InvocationExpressionSyntax GetValueOrParameterizedLambda( + ExpressionSyntax invocationTarget, + LambdaExpressionSyntax lambda) => + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + invocationTarget, + IdentifierName("GetValueOr"))) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument(lambda)))) + .NormalizeWhitespace(); +} From 8cfb8fbf6536b304a8b3d36c0d7969182a7e3196 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Thu, 4 Jan 2024 13:29:11 +0100 Subject: [PATCH 28/43] Add analyzer reference to Playground project --- src/Rascal.Playground/Rascal.Playground.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Rascal.Playground/Rascal.Playground.csproj b/src/Rascal.Playground/Rascal.Playground.csproj index 734cf03..e5b7bf3 100644 --- a/src/Rascal.Playground/Rascal.Playground.csproj +++ b/src/Rascal.Playground/Rascal.Playground.csproj @@ -10,6 +10,7 @@ + From ca35084d3a7ea787eaa710463e86bc5777d4f271 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Thu, 4 Jan 2024 13:30:27 +0100 Subject: [PATCH 29/43] Rename UseDefaultOrForIdMatch --- ...erTests.cs => UseGetValueOrForIdMatchAnalyzerTests.cs} | 4 ++-- ...FixTests.cs => UseGetValueOrForIdMatchCodeFixTests.cs} | 4 ++-- ...atchAnalyzer.cs => UseGetValueOrForIdMatchAnalyzer.cs} | 6 +++--- ...dMatchCodeFix.cs => UseGetValueOrForIdMatchCodeFix.cs} | 8 ++++---- src/Rascal.Analysis/Diagnostics.cs | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) rename src/Rascal.Analysis.Tests/Analyzers/{UseDefaultOrForIdMatchAnalyzerTests.cs => UseGetValueOrForIdMatchAnalyzerTests.cs} (88%) rename src/Rascal.Analysis.Tests/CodeFixes/{UseDefaultOrForIdMatchCodeFixTests.cs => UseGetValueOrForIdMatchCodeFixTests.cs} (92%) rename src/Rascal.Analysis/Analyzers/{UseDefaultOrForIdMatchAnalyzer.cs => UseGetValueOrForIdMatchAnalyzer.cs} (94%) rename src/Rascal.Analysis/CodeFixes/{UseDefaultOrForIdMatchCodeFix.cs => UseGetValueOrForIdMatchCodeFix.cs} (94%) diff --git a/src/Rascal.Analysis.Tests/Analyzers/UseDefaultOrForIdMatchAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/UseGetValueOrForIdMatchAnalyzerTests.cs similarity index 88% rename from src/Rascal.Analysis.Tests/Analyzers/UseDefaultOrForIdMatchAnalyzerTests.cs rename to src/Rascal.Analysis.Tests/Analyzers/UseGetValueOrForIdMatchAnalyzerTests.cs index 74b5e29..08e9929 100644 --- a/src/Rascal.Analysis.Tests/Analyzers/UseDefaultOrForIdMatchAnalyzerTests.cs +++ b/src/Rascal.Analysis.Tests/Analyzers/UseGetValueOrForIdMatchAnalyzerTests.cs @@ -1,8 +1,8 @@ -using VerifyCS = Rascal.Analysis.Tests.Verifiers.AnalyzerVerifier; +using VerifyCS = Rascal.Analysis.Tests.Verifiers.AnalyzerVerifier; namespace Rascal.Analysis.Analyzers.Tests; -public class UseDefaultOrForIdMatchAnalyzerTests +public class UseGetValueOrForIdMatchAnalyzerTests { [Fact] public async Task DoesNothingUsually() => await VerifyCS.VerifyAnalyzerAsync(""" diff --git a/src/Rascal.Analysis.Tests/CodeFixes/UseDefaultOrForIdMatchCodeFixTests.cs b/src/Rascal.Analysis.Tests/CodeFixes/UseGetValueOrForIdMatchCodeFixTests.cs similarity index 92% rename from src/Rascal.Analysis.Tests/CodeFixes/UseDefaultOrForIdMatchCodeFixTests.cs rename to src/Rascal.Analysis.Tests/CodeFixes/UseGetValueOrForIdMatchCodeFixTests.cs index 037de4d..503ad45 100644 --- a/src/Rascal.Analysis.Tests/CodeFixes/UseDefaultOrForIdMatchCodeFixTests.cs +++ b/src/Rascal.Analysis.Tests/CodeFixes/UseGetValueOrForIdMatchCodeFixTests.cs @@ -1,8 +1,8 @@ -using VerifyCS = Rascal.Analysis.Tests.Verifiers.CodeFixVerifier; +using VerifyCS = Rascal.Analysis.Tests.Verifiers.CodeFixVerifier; namespace Rascal.Analysis.CodeFixes.Tests; -public class UseDefaultOrForIdMatchCodeFixTests +public class UseGetValueOrForIdMatchCodeFixTests { [Fact] public async Task Fixes_MatchIdCall_WithError() => await VerifyCS.VerifyCodeFixAsync(""" diff --git a/src/Rascal.Analysis/Analyzers/UseDefaultOrForIdMatchAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseGetValueOrForIdMatchAnalyzer.cs similarity index 94% rename from src/Rascal.Analysis/Analyzers/UseDefaultOrForIdMatchAnalyzer.cs rename to src/Rascal.Analysis/Analyzers/UseGetValueOrForIdMatchAnalyzer.cs index 59fb72c..0c7ba17 100644 --- a/src/Rascal.Analysis/Analyzers/UseDefaultOrForIdMatchAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UseGetValueOrForIdMatchAnalyzer.cs @@ -3,10 +3,10 @@ namespace Rascal.Analysis.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class UseDefaultOrForIdMatchAnalyzer : DiagnosticAnalyzer +public sealed class UseGetValueOrForIdMatchAnalyzer : DiagnosticAnalyzer { public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( - Diagnostics.UseDefaultOrForIdMatch); + Diagnostics.UseGetValueOrForIdMatch); public override void Initialize(AnalysisContext context) { @@ -63,7 +63,7 @@ public override void Initialize(AnalysisContext context) // Report the diagnostic. operationCtx.ReportDiagnostic(Diagnostic.Create( - Diagnostics.UseDefaultOrForIdMatch, + Diagnostics.UseGetValueOrForIdMatch, location, lambdaParameter.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); }, OperationKind.Invocation); diff --git a/src/Rascal.Analysis/CodeFixes/UseDefaultOrForIdMatchCodeFix.cs b/src/Rascal.Analysis/CodeFixes/UseGetValueOrForIdMatchCodeFix.cs similarity index 94% rename from src/Rascal.Analysis/CodeFixes/UseDefaultOrForIdMatchCodeFix.cs rename to src/Rascal.Analysis/CodeFixes/UseGetValueOrForIdMatchCodeFix.cs index 75eff48..95716e4 100644 --- a/src/Rascal.Analysis/CodeFixes/UseDefaultOrForIdMatchCodeFix.cs +++ b/src/Rascal.Analysis/CodeFixes/UseGetValueOrForIdMatchCodeFix.cs @@ -5,10 +5,10 @@ namespace Rascal.Analysis.CodeFixes; [ExportCodeFixProvider(LanguageNames.CSharp)] -public sealed class UseDefaultOrForIdMatchCodeFix : CodeFixProvider +public sealed class UseGetValueOrForIdMatchCodeFix : CodeFixProvider { public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create( - Diagnostics.UseDefaultOrForIdMatch.Id); + Diagnostics.UseGetValueOrForIdMatch.Id); public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; @@ -43,7 +43,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext ctx) if (lambda.Syntax is not LambdaExpressionSyntax lambdaSyntax) return; var codeAction = CodeAction.Create( - "Use DefaultOr", + "Use GetValueOr", _ => Task.FromResult(ExecuteFix( document, root, @@ -52,7 +52,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext ctx) lambdaSyntax, body, param)), - nameof(UseDefaultOrForIdMatchCodeFix)); + nameof(UseGetValueOrForIdMatchCodeFix)); ctx.RegisterCodeFix(codeAction, ctx.Diagnostics); } diff --git a/src/Rascal.Analysis/Diagnostics.cs b/src/Rascal.Analysis/Diagnostics.cs index 956d736..dcad3f1 100644 --- a/src/Rascal.Analysis/Diagnostics.cs +++ b/src/Rascal.Analysis/Diagnostics.cs @@ -53,11 +53,11 @@ public static class Diagnostics true, "Calling 'To' with a type which no value of the type of the result permits will always fail."); - public static DiagnosticDescriptor UseDefaultOrForIdMatch { get; } = new( + public static DiagnosticDescriptor UseGetValueOrForIdMatch { get; } = new( "RASCAL0006", - "Use 'DefaultOr' instead of 'Match(x => x, ...)'", + "Use 'GetValueOr' instead of 'Match(x => x, ...)'", "This call matches {0} using an identity function. " + - "Use 'DefaultOr' instead to reduce allocations.", + "Use 'GetValueOr' instead to reduce allocations.", "Correctness", DiagnosticSeverity.Warning, true, From d2d1e21a7769fff08ad61a0add77375d8bef91ac Mon Sep 17 00:00:00 2001 From: thinker227 Date: Thu, 4 Jan 2024 13:33:32 +0100 Subject: [PATCH 30/43] Use lambda parameter names --- src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs | 2 +- .../Analyzers/UseGetValueOrForIdMatchAnalyzer.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs index c83c981..f01f0b2 100644 --- a/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs @@ -71,7 +71,7 @@ public override void Initialize(AnalysisContext context) operationCtx.ReportDiagnostic(Diagnostic.Create( Diagnostics.UnnecessaryIdMap, location, - lambdaParameter.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); + lambdaParameter.Name)); }, OperationKind.Invocation); }); } diff --git a/src/Rascal.Analysis/Analyzers/UseGetValueOrForIdMatchAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseGetValueOrForIdMatchAnalyzer.cs index 0c7ba17..fc2fb53 100644 --- a/src/Rascal.Analysis/Analyzers/UseGetValueOrForIdMatchAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UseGetValueOrForIdMatchAnalyzer.cs @@ -65,7 +65,7 @@ public override void Initialize(AnalysisContext context) operationCtx.ReportDiagnostic(Diagnostic.Create( Diagnostics.UseGetValueOrForIdMatch, location, - lambdaParameter.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); + lambdaParameter.Name)); }, OperationKind.Invocation); }); } From 38ac672a5553cc8bc1a588ca90f9e67316cac2bb Mon Sep 17 00:00:00 2001 From: thinker227 Date: Fri, 5 Jan 2024 11:31:52 +0100 Subject: [PATCH 31/43] Simplify some logic and add syntax failsafes --- .../Analyzers/ToSusTypeAnalyzer.cs | 7 +- .../Analyzers/UnnecessaryIdMapAnalyzer.cs | 8 +-- .../UseGetValueOrForIdMatchAnalyzer.cs | 7 +- .../Analyzers/UseMapAnalyzer.cs | 67 ++++++++++++------- .../Analyzers/UseThenAnalyzer.cs | 7 +- 5 files changed, 57 insertions(+), 39 deletions(-) diff --git a/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs b/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs index fa8e0e2..10a293c 100644 --- a/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs @@ -34,7 +34,7 @@ public override void Initialize(AnalysisContext context) if (!method.OriginalDefinition.Equals(toMethod, SymbolEqualityComparer.Default)) return; // Get the location of the type argument - if (operation.Syntax is not InvocationExpressionSyntax + var location = operation.Syntax is InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax { @@ -43,8 +43,9 @@ public override void Initialize(AnalysisContext context) TypeArgumentList.Arguments: [var typeSyntax] } } - }) return; - var location = typeSyntax.GetLocation(); + } + ? typeSyntax.GetLocation() + : operation.Syntax.GetLocation(); // Get the source and target type for the conversion var sourceType = method.ContainingType.TypeArguments[0]; diff --git a/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs index f01f0b2..250cc0f 100644 --- a/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs @@ -56,16 +56,16 @@ public override void Initialize(AnalysisContext context) if (!returnReference.Parameter.Equals(lambdaParameter, SymbolEqualityComparer.Default)) return; // Get the location of the method invocation. - if (operation.Syntax is not InvocationExpressionSyntax + var location = operation.Syntax is InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax { Name.Span.Start: var start }, Span.End: var end - }) return; - var span = TextSpan.FromBounds(start, end); - var location = Location.Create(operation.Syntax.SyntaxTree, span); + } + ? Location.Create(operation.Syntax.SyntaxTree, TextSpan.FromBounds(start, end)) + : operation.Syntax.GetLocation(); // Report the diagnostic. operationCtx.ReportDiagnostic(Diagnostic.Create( diff --git a/src/Rascal.Analysis/Analyzers/UseGetValueOrForIdMatchAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseGetValueOrForIdMatchAnalyzer.cs index fc2fb53..d042e15 100644 --- a/src/Rascal.Analysis/Analyzers/UseGetValueOrForIdMatchAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UseGetValueOrForIdMatchAnalyzer.cs @@ -55,11 +55,12 @@ public override void Initialize(AnalysisContext context) if (!returnReference.Parameter.Equals(lambdaParameter, SymbolEqualityComparer.Default)) return; // Get the location of the method invocation. - if (operation.Syntax is not InvocationExpressionSyntax + var location = operation.Syntax is InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax memberAccessExpression - }) return; - var location = memberAccessExpression.Name.GetLocation(); + } + ? memberAccessExpression.Name.GetLocation() + : operation.Syntax.GetLocation(); // Report the diagnostic. operationCtx.ReportDiagnostic(Diagnostic.Create( diff --git a/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs index beed5b8..89f7f65 100644 --- a/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs @@ -36,36 +36,16 @@ public override void Initialize(AnalysisContext context) // Check that it is Then being called. if (!operation.TargetMethod.OriginalDefinition.Equals(thenMethod, SymbolEqualityComparer.Default)) return; - // Check that the first argument is a lambda with an immediate return. - if (operation.Arguments is not - [ - { - Value: IDelegateCreationOperation - { - Target: IAnonymousFunctionOperation - { - Body.Operations: - [ - IReturnOperation - { - ReturnedValue: IInvocationOperation returnInvocation - } - ] - } - } - } - ]) return; - - // Check that the return invocation expression is calling either Ok or new(T). - if (!returnInvocation.TargetMethod.OriginalDefinition.Equals(okMethod, SymbolEqualityComparer.Default) && - !returnInvocation.TargetMethod.OriginalDefinition.Equals(okCtor, SymbolEqualityComparer.Default)) return; + // Check that the operation should be targeted. + if (!IsTargetOperation(operation, okMethod, okCtor)) return; // Get the location of the method name. - if (operation.Syntax is not InvocationExpressionSyntax + var location = operation.Syntax is InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax memberAccessExpression - }) return; - var location = memberAccessExpression.Name.GetLocation(); + } + ? memberAccessExpression.Name.GetLocation() + : operation.Syntax.GetLocation(); // Report the diagnostic. operationCtx.ReportDiagnostic(Diagnostic.Create( @@ -74,4 +54,39 @@ public override void Initialize(AnalysisContext context) }, OperationKind.Invocation); }); } + + private static bool IsTargetOperation( + IInvocationOperation operation, + IMethodSymbol okMethod, + IMethodSymbol okCtor) + { + // Check that the first argument is a lambda with an immediate return. + if (operation.Arguments is not + [ + { + Value: IDelegateCreationOperation + { + Target: IAnonymousFunctionOperation + { + Body.Operations: + [ + IReturnOperation + { + ReturnedValue: var returnValue + } + ] + } + } + } + ]) return false; + + // Check whether the return operation is an invocation to Ok. + if (returnValue is IInvocationOperation invocation && + invocation.TargetMethod.OriginalDefinition.Equals(okMethod, SymbolEqualityComparer.Default)) return true; + + // Check whether the return operation is an constructor invocation to new(T). + return returnValue is IObjectCreationOperation objectCreation && + (objectCreation.Constructor?.OriginalDefinition.Equals(okCtor, SymbolEqualityComparer.Default) + ?? false); + } } diff --git a/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs index 89a1003..f64fd32 100644 --- a/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs @@ -49,11 +49,12 @@ public override void Initialize(AnalysisContext context) return; // Get the location of the method name. - if (argumentInvocation.Syntax is not InvocationExpressionSyntax + var location = argumentInvocation.Syntax is InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax memberAccessExpression - }) return; - var location = memberAccessExpression.Name.GetLocation(); + } + ? memberAccessExpression.Name.GetLocation() + : argumentInvocation.Syntax.GetLocation(); // Report the diagnostic. operationCtx.ReportDiagnostic(Diagnostic.Create( From 07ee81bca1bb0835e7aa55fa708496c54156e368 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Fri, 5 Jan 2024 11:32:04 +0100 Subject: [PATCH 32/43] Update UseMapAnalyzerTests --- .../Analyzers/UseMapAnalyzerTests.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs index 3615209..67c18c6 100644 --- a/src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs +++ b/src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs @@ -23,7 +23,7 @@ public static void Bar() """); [Fact] - public async Task ReportsOnLambda() => await VerifyCS.VerifyAnalyzerAsync(""" + public async Task ReportsOnLambdaOk() => await VerifyCS.VerifyAnalyzerAsync(""" using System; using Rascal; using static Rascal.Prelude; @@ -37,9 +37,9 @@ public static void Bar() } } """); - + [Fact] - public async Task ReportOnBlockBody() => await VerifyCS.VerifyAnalyzerAsync(""" + public async Task ReportsOnLambdaCtor() => await VerifyCS.VerifyAnalyzerAsync(""" using System; using Rascal; using static Rascal.Prelude; @@ -49,10 +49,7 @@ public static class Foo public static void Bar() { var result = Ok(2); - var v = result.{|RASCAL0001:Then|}(x => - { - return Ok(x); - }); + var v = result.{|RASCAL0001:Then|}(x => new Result(x)); } } """); From 9d0f6d065884071139a3e8b31fad8d2c1d9474fc Mon Sep 17 00:00:00 2001 From: thinker227 Date: Fri, 5 Jan 2024 22:48:09 +0100 Subject: [PATCH 33/43] Update RemoveMapIdCallCodeFix --- .../CodeFixes/RemoveMapIdCallCodeFix.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFix.cs b/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFix.cs index 85b2e50..f21517c 100644 --- a/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFix.cs +++ b/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFix.cs @@ -17,12 +17,19 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext ctx) var root = await document.GetSyntaxRootAsync(); if (root is null) return; + + var invocation = root.FindNode(ctx.Span).FirstAncestorOrSelf(); + if (invocation is null) return; - var invocation = (InvocationExpressionSyntax)root.FindNode(ctx.Span); + if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) return; var codeAction = CodeAction.Create( "Remove Map call", - _ => Task.FromResult(ExecuteFix(document, root, invocation)), + _ => Task.FromResult(ExecuteFix( + document, + root, + invocation, + memberAccess)), nameof(RemoveMapIdCallCodeFix)); ctx.RegisterCodeFix(codeAction, ctx.Diagnostics); @@ -31,9 +38,9 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext ctx) private static Document ExecuteFix( Document document, SyntaxNode root, - InvocationExpressionSyntax invocation) + InvocationExpressionSyntax invocation, + MemberAccessExpressionSyntax memberAccess) { - var memberAccess = (MemberAccessExpressionSyntax)invocation.Expression; var expression = memberAccess.Expression; var newRoot = root.ReplaceNode(invocation, expression); return document.WithSyntaxRoot(newRoot); From ed85b03f74f10c5c17da5acd7a260638520aec34 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Fri, 5 Jan 2024 23:32:28 +0100 Subject: [PATCH 34/43] Add UseMapCodeFix --- .../CodeFixes/UseMapCodeFixTests.cs | 35 +++++++ .../CodeFixes/RemoveMapIdCallCodeFix.cs | 21 ++-- .../CodeFixes/UseMapCodeFix.cs | 99 +++++++++++++++++++ src/Rascal.Analysis/SyntaxInator.cs | 14 +++ 4 files changed, 160 insertions(+), 9 deletions(-) create mode 100644 src/Rascal.Analysis.Tests/CodeFixes/UseMapCodeFixTests.cs create mode 100644 src/Rascal.Analysis/CodeFixes/UseMapCodeFix.cs diff --git a/src/Rascal.Analysis.Tests/CodeFixes/UseMapCodeFixTests.cs b/src/Rascal.Analysis.Tests/CodeFixes/UseMapCodeFixTests.cs new file mode 100644 index 0000000..6d5a262 --- /dev/null +++ b/src/Rascal.Analysis.Tests/CodeFixes/UseMapCodeFixTests.cs @@ -0,0 +1,35 @@ +using VerifyCS = Rascal.Analysis.Tests.Verifiers.CodeFixVerifier; + +namespace Rascal.Analysis.CodeFixes.Tests; + +public class UseMapCodeFixTests +{ + [Fact] + public async Task RemovesMap() => await VerifyCS.VerifyCodeFixAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = result.{|RASCAL0001:Then|}(x => Ok(x)); + } + } + """, """ + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = result.Map(x => x); + } + } + """); +} diff --git a/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFix.cs b/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFix.cs index f21517c..1f3bd70 100644 --- a/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFix.cs +++ b/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFix.cs @@ -15,21 +15,25 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext ctx) { var document = ctx.Document; - var root = await document.GetSyntaxRootAsync(); + var root = await document.GetSyntaxRootAsync(ctx.CancellationToken); if (root is null) return; var invocation = root.FindNode(ctx.Span).FirstAncestorOrSelf(); - if (invocation is null) return; - - if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) return; - + if (invocation is not + { + Expression: MemberAccessExpressionSyntax + { + Expression: var invocationTarget + } + }) return; + var codeAction = CodeAction.Create( "Remove Map call", _ => Task.FromResult(ExecuteFix( document, root, invocation, - memberAccess)), + invocationTarget)), nameof(RemoveMapIdCallCodeFix)); ctx.RegisterCodeFix(codeAction, ctx.Diagnostics); @@ -39,10 +43,9 @@ private static Document ExecuteFix( Document document, SyntaxNode root, InvocationExpressionSyntax invocation, - MemberAccessExpressionSyntax memberAccess) + ExpressionSyntax invocationTarget) { - var expression = memberAccess.Expression; - var newRoot = root.ReplaceNode(invocation, expression); + var newRoot = root.ReplaceNode(invocation, invocationTarget); return document.WithSyntaxRoot(newRoot); } } diff --git a/src/Rascal.Analysis/CodeFixes/UseMapCodeFix.cs b/src/Rascal.Analysis/CodeFixes/UseMapCodeFix.cs new file mode 100644 index 0000000..223a774 --- /dev/null +++ b/src/Rascal.Analysis/CodeFixes/UseMapCodeFix.cs @@ -0,0 +1,99 @@ +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Operations; + +namespace Rascal.Analysis.CodeFixes; + +[ExportCodeFixProvider(LanguageNames.CSharp)] +public sealed class UseMapCodeFix : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create( + Diagnostics.UseMap.Id); + + public override FixAllProvider? GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext ctx) + { + var document = ctx.Document; + var semanticModel = await document.GetSemanticModelAsync(ctx.CancellationToken); + if (semanticModel is null) return; + + var root = await document.GetSyntaxRootAsync(ctx.CancellationToken); + if (root is null) return; + + var invocation = root.FindNode(ctx.Span).FirstAncestorOrSelf(); + if (invocation is not + { + Expression: MemberAccessExpressionSyntax + { + Expression: var invocationTarget + } + }) return; + + var invocationOperation = (IInvocationOperation)semanticModel.GetOperation(invocation)!; + + if (GetReturnExpression(invocationOperation) is not var (argumentExpression, returnExpression, subExpression)) return; + + var codeAction = CodeAction.Create( + "Use Map", + _ => Task.FromResult(ExecuteFix( + document, + root, + invocation, + invocationTarget, + argumentExpression, + returnExpression, + subExpression)), + nameof(UseMapCodeFix)); + + ctx.RegisterCodeFix(codeAction, ctx.Diagnostics); + } + + private static (ExpressionSyntax, ExpressionSyntax, ExpressionSyntax)? GetReturnExpression(IInvocationOperation invocation) + { + if (invocation.Arguments is not + [ + { + Value: IDelegateCreationOperation + { + Target: IAnonymousFunctionOperation + { + Body.Operations: + [ + IReturnOperation + { + ReturnedValue: var returnValue + } returnOperation + ] + } expressionOperation + } + } + ]) return null; + + var argumentExpression = (ExpressionSyntax)expressionOperation.Syntax; + var returnExpression = (ExpressionSyntax)returnOperation.Syntax; + + if (returnValue is IInvocationOperation returnInvocation) + return (argumentExpression, returnExpression, (ExpressionSyntax)returnInvocation.Arguments[0].Value.Syntax); + + if (returnValue is IObjectCreationOperation returnObjectCreation) + return (argumentExpression, returnExpression, (ExpressionSyntax)returnObjectCreation.Arguments[0].Value.Syntax); + + return null; + } + + private static Document ExecuteFix(Document document, + SyntaxNode root, + InvocationExpressionSyntax invocation, + ExpressionSyntax invocationTarget, + ExpressionSyntax argumentExpression, + ExpressionSyntax returnExpression, + ExpressionSyntax subExpression) + { + var newArgumentExpression = argumentExpression.ReplaceNode(returnExpression, subExpression); + var newInvocation = SyntaxInator.MapFrom(invocationTarget, newArgumentExpression); + + var newRoot = root.ReplaceNode(invocation, newInvocation); + return document.WithSyntaxRoot(newRoot); + } +} diff --git a/src/Rascal.Analysis/SyntaxInator.cs b/src/Rascal.Analysis/SyntaxInator.cs index ede6e16..f1d76ab 100644 --- a/src/Rascal.Analysis/SyntaxInator.cs +++ b/src/Rascal.Analysis/SyntaxInator.cs @@ -54,4 +54,18 @@ public static InvocationExpressionSyntax GetValueOrParameterizedLambda( SingletonSeparatedList( Argument(lambda)))) .NormalizeWhitespace(); + + public static InvocationExpressionSyntax MapFrom( + ExpressionSyntax invocationTarget, + ExpressionSyntax expression) => + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + invocationTarget, + IdentifierName("Map"))) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument(expression)))) + .NormalizeWhitespace(); } From afaf42ee4a647846dd4ec17108792cc8fb8896e4 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Sat, 6 Jan 2024 00:32:55 +0100 Subject: [PATCH 35/43] Take into account conversions and target-type new --- .../Analyzers/UseMapAnalyzerTests.cs | 32 +++++++ .../CodeFixes/UseMapCodeFixTests.cs | 89 ++++++++++++++++++- .../Analyzers/UseMapAnalyzer.cs | 23 +++-- .../CodeFixes/UseMapCodeFix.cs | 8 ++ 4 files changed, 146 insertions(+), 6 deletions(-) diff --git a/src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs index 67c18c6..5aee16b 100644 --- a/src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs +++ b/src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs @@ -53,4 +53,36 @@ public static void Bar() } } """); + + [Fact] + public async Task ReportsOnLambdaCtorImplicit() => await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = result.{|RASCAL0001:Then|}(x => new(x)); + } + } + """); + + [Fact] + public async Task ReportsOnLambdaConversion() => await VerifyCS.VerifyAnalyzerAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = result.{|RASCAL0001:Then|}(x => (Result)x); + } + } + """); } diff --git a/src/Rascal.Analysis.Tests/CodeFixes/UseMapCodeFixTests.cs b/src/Rascal.Analysis.Tests/CodeFixes/UseMapCodeFixTests.cs index 6d5a262..5401727 100644 --- a/src/Rascal.Analysis.Tests/CodeFixes/UseMapCodeFixTests.cs +++ b/src/Rascal.Analysis.Tests/CodeFixes/UseMapCodeFixTests.cs @@ -5,7 +5,7 @@ namespace Rascal.Analysis.CodeFixes.Tests; public class UseMapCodeFixTests { [Fact] - public async Task RemovesMap() => await VerifyCS.VerifyCodeFixAsync(""" + public async Task RemovesMap_Lambda() => await VerifyCS.VerifyCodeFixAsync(""" using System; using Rascal; using static Rascal.Prelude; @@ -32,4 +32,91 @@ public static void Bar() } } """); + + [Fact] + public async Task RemovesMap_Ctor_Explicit() => await VerifyCS.VerifyCodeFixAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = result.{|RASCAL0001:Then|}(x => new Result(x)); + } + } + """, """ + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = result.Map(x => x); + } + } + """); + + [Fact] + public async Task RemovesMap_Ctor_Implicit() => await VerifyCS.VerifyCodeFixAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = result.{|RASCAL0001:Then|}(x => new(x)); + } + } + """, """ + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = result.Map(x => x); + } + } + """); + + [Fact] + public async Task RemovesMap_Conversion() => await VerifyCS.VerifyCodeFixAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = result.{|RASCAL0001:Then|}(x => (Result)x); + } + } + """, """ + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = result.Map(x => x); + } + } + """); } diff --git a/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs index 89f7f65..b9e5430 100644 --- a/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs @@ -28,6 +28,9 @@ public override void Initialize(AnalysisContext context) var okMethod = (IMethodSymbol)preludeMembers.First(x => x.Name == "Ok"); var okCtor = resultType.InstanceConstructors .First(x => x.Parameters is [{ Type: ITypeParameterSymbol }]); + var okConversion = (IMethodSymbol)resultMembers + .OfType() + .First(x => x is { Name: "op_Implicit", Parameters: [{ Type: ITypeParameterSymbol }] }); compilationCtx.RegisterOperationAction(operationCtx => { @@ -37,7 +40,7 @@ public override void Initialize(AnalysisContext context) if (!operation.TargetMethod.OriginalDefinition.Equals(thenMethod, SymbolEqualityComparer.Default)) return; // Check that the operation should be targeted. - if (!IsTargetOperation(operation, okMethod, okCtor)) return; + if (!IsTargetOperation(operation, okMethod, okCtor, okConversion)) return; // Get the location of the method name. var location = operation.Syntax is InvocationExpressionSyntax @@ -58,7 +61,8 @@ public override void Initialize(AnalysisContext context) private static bool IsTargetOperation( IInvocationOperation operation, IMethodSymbol okMethod, - IMethodSymbol okCtor) + IMethodSymbol okCtor, + IMethodSymbol okConversion) { // Check that the first argument is a lambda with an immediate return. if (operation.Arguments is not @@ -85,8 +89,17 @@ private static bool IsTargetOperation( invocation.TargetMethod.OriginalDefinition.Equals(okMethod, SymbolEqualityComparer.Default)) return true; // Check whether the return operation is an constructor invocation to new(T). - return returnValue is IObjectCreationOperation objectCreation && - (objectCreation.Constructor?.OriginalDefinition.Equals(okCtor, SymbolEqualityComparer.Default) - ?? false); + if (returnValue is IObjectCreationOperation objectCreation && + (objectCreation.Constructor?.OriginalDefinition.Equals(okCtor, SymbolEqualityComparer.Default) + ?? false)) return true; + + // Check whether the return operation is either a target-type new or implicit/explicit conversion. + // The conversion is implicit if it's a target-type new expression. + if (returnValue is IConversionOperation conversion && + (conversion.IsImplicit || + (conversion.OperatorMethod?.OriginalDefinition.Equals(okConversion, SymbolEqualityComparer.Default) ?? false))) + return true; + + return false; } } diff --git a/src/Rascal.Analysis/CodeFixes/UseMapCodeFix.cs b/src/Rascal.Analysis/CodeFixes/UseMapCodeFix.cs index 223a774..1cc05a6 100644 --- a/src/Rascal.Analysis/CodeFixes/UseMapCodeFix.cs +++ b/src/Rascal.Analysis/CodeFixes/UseMapCodeFix.cs @@ -79,6 +79,14 @@ private static (ExpressionSyntax, ExpressionSyntax, ExpressionSyntax)? GetReturn if (returnValue is IObjectCreationOperation returnObjectCreation) return (argumentExpression, returnExpression, (ExpressionSyntax)returnObjectCreation.Arguments[0].Value.Syntax); + if (returnValue is IConversionOperation conversion) + { + if (conversion is { IsImplicit: true, Operand: IObjectCreationOperation targetTypeNew }) + return (argumentExpression, returnExpression, (ExpressionSyntax)targetTypeNew.Arguments[0].Value.Syntax); + + return (argumentExpression, returnExpression, (ExpressionSyntax)conversion.Operand.Syntax); + } + return null; } From e49b21ceee1f046fc2531cc5c22393d5a3735d11 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Sat, 6 Jan 2024 00:46:56 +0100 Subject: [PATCH 36/43] Minor whitespace --- src/Rascal.Analysis/CodeFixes/UseMapCodeFix.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Rascal.Analysis/CodeFixes/UseMapCodeFix.cs b/src/Rascal.Analysis/CodeFixes/UseMapCodeFix.cs index 1cc05a6..8f800ee 100644 --- a/src/Rascal.Analysis/CodeFixes/UseMapCodeFix.cs +++ b/src/Rascal.Analysis/CodeFixes/UseMapCodeFix.cs @@ -90,7 +90,8 @@ private static (ExpressionSyntax, ExpressionSyntax, ExpressionSyntax)? GetReturn return null; } - private static Document ExecuteFix(Document document, + private static Document ExecuteFix( + Document document, SyntaxNode root, InvocationExpressionSyntax invocation, ExpressionSyntax invocationTarget, From b80ad6c02425331b9e76183cf17d869d98d8de14 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Mon, 8 Jan 2024 13:35:32 +0100 Subject: [PATCH 37/43] Add UseThenCodeFix --- .../CodeFixes/UseThenCodeFixTests.cs | 64 +++++++++++++++++ .../CodeFixes/UseThenCodeFix.cs | 72 +++++++++++++++++++ src/Rascal.Analysis/SyntaxInator.cs | 2 + 3 files changed, 138 insertions(+) create mode 100644 src/Rascal.Analysis.Tests/CodeFixes/UseThenCodeFixTests.cs create mode 100644 src/Rascal.Analysis/CodeFixes/UseThenCodeFix.cs diff --git a/src/Rascal.Analysis.Tests/CodeFixes/UseThenCodeFixTests.cs b/src/Rascal.Analysis.Tests/CodeFixes/UseThenCodeFixTests.cs new file mode 100644 index 0000000..7cd8be5 --- /dev/null +++ b/src/Rascal.Analysis.Tests/CodeFixes/UseThenCodeFixTests.cs @@ -0,0 +1,64 @@ +using VerifyCS = Rascal.Analysis.Tests.Verifiers.CodeFixVerifier; + +namespace Rascal.Analysis.CodeFixes.Tests; + +public class UseThenCodeFixTests +{ + [Fact] + public async Task FixesExtensionForm() => await VerifyCS.VerifyCodeFixAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = result.{|RASCAL0002:Map|}(x => Ok(x)).Unnest(); + } + } + """, """ + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = result.Then(x => Ok(x)); + } + } + """); + + [Fact] + public async Task FixesCallForm() => await VerifyCS.VerifyCodeFixAsync(""" + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = ResultExtensions.Unnest(result.{|RASCAL0002:Map|}(x => Ok(x))); + } + } + """, """ + using System; + using Rascal; + using static Rascal.Prelude; + + public static class Foo + { + public static void Bar() + { + var result = Ok(2); + var v = result.Then(x => Ok(x)); + } + } + """); +} diff --git a/src/Rascal.Analysis/CodeFixes/UseThenCodeFix.cs b/src/Rascal.Analysis/CodeFixes/UseThenCodeFix.cs new file mode 100644 index 0000000..2e2e396 --- /dev/null +++ b/src/Rascal.Analysis/CodeFixes/UseThenCodeFix.cs @@ -0,0 +1,72 @@ +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Operations; + +namespace Rascal.Analysis.CodeFixes; + +[ExportCodeFixProvider(LanguageNames.CSharp)] +public sealed class UseThenCodeFix : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create( + Diagnostics.UseThen.Id); + + public override FixAllProvider? GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext ctx) + { + var document = ctx.Document; + var semanticModel = await document.GetSemanticModelAsync(ctx.CancellationToken); + if (semanticModel is null) return; + + var root = await document.GetSyntaxRootAsync(ctx.CancellationToken); + if (root is null) return; + + var invocation = root.FindNode(ctx.Span).FirstAncestorOrSelf(); + if (invocation is null) return; + + // This operation structure will be the same regardless of + // whether Unnest is called as an extension or regular method. + if (semanticModel.GetOperation(invocation) is not IInvocationOperation + { + Parent: IArgumentOperation + { + Parent: IInvocationOperation unnestOperation + } + } mapOperation) return; + + var mapSyntax = (InvocationExpressionSyntax)mapOperation.Syntax; + var unnestSyntax = (InvocationExpressionSyntax)unnestOperation.Syntax; + + if (mapSyntax is not + { + Expression: MemberAccessExpressionSyntax + { + Name: var nameSyntax + } + }) return; + + var codeAction = CodeAction.Create( + "Use Then", + _ => Task.FromResult(ExecuteFix( + document, + root, + unnestSyntax, + mapSyntax, + nameSyntax)), + nameof(UseThenCodeFix)); + + ctx.RegisterCodeFix(codeAction, ctx.Diagnostics); + } + + private static Document ExecuteFix( + Document document, + SyntaxNode root, + ExpressionSyntax containingInvocation, + InvocationExpressionSyntax invocation, + NameSyntax name) + { + var newInvocation = invocation.ReplaceNode(name, SyntaxInator.ThenName()); + var newRoot = root.ReplaceNode(containingInvocation, newInvocation); + return document.WithSyntaxRoot(newRoot); + } +} diff --git a/src/Rascal.Analysis/SyntaxInator.cs b/src/Rascal.Analysis/SyntaxInator.cs index f1d76ab..7aff17c 100644 --- a/src/Rascal.Analysis/SyntaxInator.cs +++ b/src/Rascal.Analysis/SyntaxInator.cs @@ -68,4 +68,6 @@ public static InvocationExpressionSyntax MapFrom( SingletonSeparatedList( Argument(expression)))) .NormalizeWhitespace(); + + public static NameSyntax ThenName() => IdentifierName("Then"); } From 0ef1d2869db7c07e349150c9eb2b5eb56b9e4c3a Mon Sep 17 00:00:00 2001 From: thinker227 Date: Mon, 8 Jan 2024 13:43:44 +0100 Subject: [PATCH 38/43] Use 3-digit diagnostic IDs --- .../Analyzers/ToSusTypeAnalyzerTests.cs | 6 +++--- .../Analyzers/UnnecessaryIdMapAnalyzerTests.cs | 2 +- .../UseGetValueOrForIdMatchAnalyzerTests.cs | 2 +- .../Analyzers/UseMapAnalyzerTests.cs | 8 ++++---- .../Analyzers/UseThenAnalyzerTests.cs | 4 ++-- .../CodeFixes/RemoveMapIdCallCodeFixTests.cs | 2 +- .../CodeFixes/UseGetValueOrForIdMatchCodeFixTests.cs | 6 +++--- .../CodeFixes/UseMapCodeFixTests.cs | 8 ++++---- .../CodeFixes/UseThenCodeFixTests.cs | 4 ++-- src/Rascal.Analysis/Diagnostics.cs | 12 ++++++------ 10 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Rascal.Analysis.Tests/Analyzers/ToSusTypeAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/ToSusTypeAnalyzerTests.cs index a63504a..58f8902 100644 --- a/src/Rascal.Analysis.Tests/Analyzers/ToSusTypeAnalyzerTests.cs +++ b/src/Rascal.Analysis.Tests/Analyzers/ToSusTypeAnalyzerTests.cs @@ -15,7 +15,7 @@ public static class Foo public static void Bar() { var x = Ok(2); - var r = x.To<{|RASCAL0004:int|}>(); + var r = x.To<{|RASCAL004:int|}>(); } } """); @@ -31,7 +31,7 @@ public static class Foo public static void Bar() { var x = Ok(2); - var r = x.To<{|RASCAL0005:bool|}>(); + var r = x.To<{|RASCAL005:bool|}>(); } } """); @@ -47,7 +47,7 @@ public static class Foo public static void Bar() { var x = Ok(new B()); - var r = x.To<{|RASCAL0005:C|}>(); + var r = x.To<{|RASCAL005:C|}>(); } } diff --git a/src/Rascal.Analysis.Tests/Analyzers/UnnecessaryIdMapAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/UnnecessaryIdMapAnalyzerTests.cs index 0ef6f3d..fd37672 100644 --- a/src/Rascal.Analysis.Tests/Analyzers/UnnecessaryIdMapAnalyzerTests.cs +++ b/src/Rascal.Analysis.Tests/Analyzers/UnnecessaryIdMapAnalyzerTests.cs @@ -31,7 +31,7 @@ public static class Foo public static void Bar() { var result = Ok(2); - var v = result.{|RASCAL0003:Map(x => x)|}; + var v = result.{|RASCAL003:Map(x => x)|}; } } """); diff --git a/src/Rascal.Analysis.Tests/Analyzers/UseGetValueOrForIdMatchAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/UseGetValueOrForIdMatchAnalyzerTests.cs index 08e9929..47f5ba1 100644 --- a/src/Rascal.Analysis.Tests/Analyzers/UseGetValueOrForIdMatchAnalyzerTests.cs +++ b/src/Rascal.Analysis.Tests/Analyzers/UseGetValueOrForIdMatchAnalyzerTests.cs @@ -31,7 +31,7 @@ public static class Foo public static void Bar() { var r = Ok(2); - var x = r.{|RASCAL0006:Match|}(x => x, _ => 0); + var x = r.{|RASCAL006:Match|}(x => x, _ => 0); } } """); diff --git a/src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs index 5aee16b..2b4d5f9 100644 --- a/src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs +++ b/src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs @@ -33,7 +33,7 @@ public static class Foo public static void Bar() { var result = Ok(2); - var v = result.{|RASCAL0001:Then|}(x => Ok(x)); + var v = result.{|RASCAL001:Then|}(x => Ok(x)); } } """); @@ -49,7 +49,7 @@ public static class Foo public static void Bar() { var result = Ok(2); - var v = result.{|RASCAL0001:Then|}(x => new Result(x)); + var v = result.{|RASCAL001:Then|}(x => new Result(x)); } } """); @@ -65,7 +65,7 @@ public static class Foo public static void Bar() { var result = Ok(2); - var v = result.{|RASCAL0001:Then|}(x => new(x)); + var v = result.{|RASCAL001:Then|}(x => new(x)); } } """); @@ -81,7 +81,7 @@ public static class Foo public static void Bar() { var result = Ok(2); - var v = result.{|RASCAL0001:Then|}(x => (Result)x); + var v = result.{|RASCAL001:Then|}(x => (Result)x); } } """); diff --git a/src/Rascal.Analysis.Tests/Analyzers/UseThenAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/UseThenAnalyzerTests.cs index 926ca5e..de626dc 100644 --- a/src/Rascal.Analysis.Tests/Analyzers/UseThenAnalyzerTests.cs +++ b/src/Rascal.Analysis.Tests/Analyzers/UseThenAnalyzerTests.cs @@ -31,7 +31,7 @@ public static class Foo public static void Bar() { var result = Ok(2); - var v = result.{|RASCAL0002:Map|}(x => Ok(x)).Unnest(); + var v = result.{|RASCAL002:Map|}(x => Ok(x)).Unnest(); } } """); @@ -47,7 +47,7 @@ public static class Foo public static void Bar() { var result = Ok(2); - var v = ResultExtensions.Unnest(result.{|RASCAL0002:Map|}(x => Ok(x))); + var v = ResultExtensions.Unnest(result.{|RASCAL002:Map|}(x => Ok(x))); } } """); diff --git a/src/Rascal.Analysis.Tests/CodeFixes/RemoveMapIdCallCodeFixTests.cs b/src/Rascal.Analysis.Tests/CodeFixes/RemoveMapIdCallCodeFixTests.cs index 6d063fc..84df6ef 100644 --- a/src/Rascal.Analysis.Tests/CodeFixes/RemoveMapIdCallCodeFixTests.cs +++ b/src/Rascal.Analysis.Tests/CodeFixes/RemoveMapIdCallCodeFixTests.cs @@ -15,7 +15,7 @@ public static class Foo public static void Bar() { var result = Ok(2); - var v = result.{|RASCAL0003:Map(x => x)|}; + var v = result.{|RASCAL003:Map(x => x)|}; } } """, """ diff --git a/src/Rascal.Analysis.Tests/CodeFixes/UseGetValueOrForIdMatchCodeFixTests.cs b/src/Rascal.Analysis.Tests/CodeFixes/UseGetValueOrForIdMatchCodeFixTests.cs index 503ad45..6c34ee8 100644 --- a/src/Rascal.Analysis.Tests/CodeFixes/UseGetValueOrForIdMatchCodeFixTests.cs +++ b/src/Rascal.Analysis.Tests/CodeFixes/UseGetValueOrForIdMatchCodeFixTests.cs @@ -15,7 +15,7 @@ public static class Foo public static void Bar() { var r = Ok("uwu"); - var x = r.{|RASCAL0006:Match|}(x => x, e => e.Message); + var x = r.{|RASCAL006:Match|}(x => x, e => e.Message); } } """, """ @@ -45,7 +45,7 @@ public static void Bar() { var r = Ok("uwu"); var v = "owo"; - var x = r.{|RASCAL0006:Match|}(x => x, _ => v); + var x = r.{|RASCAL006:Match|}(x => x, _ => v); } } """, """ @@ -75,7 +75,7 @@ public static class Foo public static void Bar() { var r = Ok("uwu"); - var x = r.{|RASCAL0006:Match|}(x => x, _ => "owo"); + var x = r.{|RASCAL006:Match|}(x => x, _ => "owo"); } } """, """ diff --git a/src/Rascal.Analysis.Tests/CodeFixes/UseMapCodeFixTests.cs b/src/Rascal.Analysis.Tests/CodeFixes/UseMapCodeFixTests.cs index 5401727..a812987 100644 --- a/src/Rascal.Analysis.Tests/CodeFixes/UseMapCodeFixTests.cs +++ b/src/Rascal.Analysis.Tests/CodeFixes/UseMapCodeFixTests.cs @@ -15,7 +15,7 @@ public static class Foo public static void Bar() { var result = Ok(2); - var v = result.{|RASCAL0001:Then|}(x => Ok(x)); + var v = result.{|RASCAL001:Then|}(x => Ok(x)); } } """, """ @@ -44,7 +44,7 @@ public static class Foo public static void Bar() { var result = Ok(2); - var v = result.{|RASCAL0001:Then|}(x => new Result(x)); + var v = result.{|RASCAL001:Then|}(x => new Result(x)); } } """, """ @@ -73,7 +73,7 @@ public static class Foo public static void Bar() { var result = Ok(2); - var v = result.{|RASCAL0001:Then|}(x => new(x)); + var v = result.{|RASCAL001:Then|}(x => new(x)); } } """, """ @@ -102,7 +102,7 @@ public static class Foo public static void Bar() { var result = Ok(2); - var v = result.{|RASCAL0001:Then|}(x => (Result)x); + var v = result.{|RASCAL001:Then|}(x => (Result)x); } } """, """ diff --git a/src/Rascal.Analysis.Tests/CodeFixes/UseThenCodeFixTests.cs b/src/Rascal.Analysis.Tests/CodeFixes/UseThenCodeFixTests.cs index 7cd8be5..b0afe94 100644 --- a/src/Rascal.Analysis.Tests/CodeFixes/UseThenCodeFixTests.cs +++ b/src/Rascal.Analysis.Tests/CodeFixes/UseThenCodeFixTests.cs @@ -15,7 +15,7 @@ public static class Foo public static void Bar() { var result = Ok(2); - var v = result.{|RASCAL0002:Map|}(x => Ok(x)).Unnest(); + var v = result.{|RASCAL002:Map|}(x => Ok(x)).Unnest(); } } """, """ @@ -44,7 +44,7 @@ public static class Foo public static void Bar() { var result = Ok(2); - var v = ResultExtensions.Unnest(result.{|RASCAL0002:Map|}(x => Ok(x))); + var v = ResultExtensions.Unnest(result.{|RASCAL002:Map|}(x => Ok(x))); } } """, """ diff --git a/src/Rascal.Analysis/Diagnostics.cs b/src/Rascal.Analysis/Diagnostics.cs index dcad3f1..9d625a0 100644 --- a/src/Rascal.Analysis/Diagnostics.cs +++ b/src/Rascal.Analysis/Diagnostics.cs @@ -3,7 +3,7 @@ namespace Rascal.Analysis; public static class Diagnostics { public static DiagnosticDescriptor UseMap { get; } = new( - "RASCAL0001", + "RASCAL001", "Use 'Map' instead of 'Then(x => Ok(...))'", "Use 'Map' instead of calling 'Ok' directly inside 'Then'", "Correctness", @@ -13,7 +13,7 @@ public static class Diagnostics "Use 'Map' instead for clarity and performance."); public static DiagnosticDescriptor UseThen { get; } = new( - "RASCAL0002", + "RASCAL002", "Use Then instead of 'Map(...).Unnest()'", "Use 'Then' instead of calling 'Unnest' directly after 'Map'", "Correctness", @@ -23,7 +23,7 @@ public static class Diagnostics "Use 'Then' instead for clarity and performance."); public static DiagnosticDescriptor UnnecessaryIdMap { get; } = new( - "RASCAL0003", + "RASCAL003", "Unnecessary 'Map' call with identity function", "This call maps {0} to itself. " + "The call can be safely removed because it doesn't do anything", @@ -34,7 +34,7 @@ public static class Diagnostics "Remove this call to 'Map'."); public static DiagnosticDescriptor ToSameType { get; } = new( - "RASCAL0004", + "RASCAL004", "'To' called with same type as result", "This call converts '{0}' to itself and will always succeed. " + "Remove this call to 'To' as it doesn't do anything.", @@ -44,7 +44,7 @@ public static class Diagnostics "Calling 'To' with the same type as that of the result will always succeed."); public static DiagnosticDescriptor ToImpossibleType { get; } = new( - "RASCAL0005", + "RASCAL005", "'To' called with impossible type", "This call tries to convert '{0}' to '{1}', but no value of type '{0}' can be of type '{1}'. " + "The conversion will always fail.", @@ -54,7 +54,7 @@ public static class Diagnostics "Calling 'To' with a type which no value of the type of the result permits will always fail."); public static DiagnosticDescriptor UseGetValueOrForIdMatch { get; } = new( - "RASCAL0006", + "RASCAL006", "Use 'GetValueOr' instead of 'Match(x => x, ...)'", "This call matches {0} using an identity function. " + "Use 'GetValueOr' instead to reduce allocations.", From da1d08efab606b3e81388ecfdea375f9d4780b1c Mon Sep 17 00:00:00 2001 From: thinker227 Date: Mon, 8 Jan 2024 13:59:05 +0100 Subject: [PATCH 39/43] Rename some tests --- .../Analyzers/UnnecessaryIdMapAnalyzerTests.cs | 4 ++-- .../Analyzers/UseGetValueOrForIdMatchAnalyzerTests.cs | 4 ++-- .../Analyzers/UseMapAnalyzerTests.cs | 10 +++++----- .../Analyzers/UseThenAnalyzerTests.cs | 6 +++--- .../CodeFixes/RemoveMapIdCallCodeFixTests.cs | 2 +- .../CodeFixes/UseThenCodeFixTests.cs | 4 ++-- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Rascal.Analysis.Tests/Analyzers/UnnecessaryIdMapAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/UnnecessaryIdMapAnalyzerTests.cs index fd37672..b7c446f 100644 --- a/src/Rascal.Analysis.Tests/Analyzers/UnnecessaryIdMapAnalyzerTests.cs +++ b/src/Rascal.Analysis.Tests/Analyzers/UnnecessaryIdMapAnalyzerTests.cs @@ -5,7 +5,7 @@ namespace Rascal.Analysis.Analyzers.Tests; public class UnnecessaryIdMapAnalyzerTests { [Fact] - public async Task DoesNothingUsually() => await VerifyCS.VerifyAnalyzerAsync(""" + public async Task DoesNotReport_OnMapOperationCall() => await VerifyCS.VerifyAnalyzerAsync(""" using System; using Rascal; using static Rascal.Prelude; @@ -21,7 +21,7 @@ public static void Bar() """); [Fact] - public async Task ReportsOnMapIdCall() => await VerifyCS.VerifyAnalyzerAsync(""" + public async Task Reports_OnMapIdCall() => await VerifyCS.VerifyAnalyzerAsync(""" using System; using Rascal; using static Rascal.Prelude; diff --git a/src/Rascal.Analysis.Tests/Analyzers/UseGetValueOrForIdMatchAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/UseGetValueOrForIdMatchAnalyzerTests.cs index 47f5ba1..9befec8 100644 --- a/src/Rascal.Analysis.Tests/Analyzers/UseGetValueOrForIdMatchAnalyzerTests.cs +++ b/src/Rascal.Analysis.Tests/Analyzers/UseGetValueOrForIdMatchAnalyzerTests.cs @@ -5,7 +5,7 @@ namespace Rascal.Analysis.Analyzers.Tests; public class UseGetValueOrForIdMatchAnalyzerTests { [Fact] - public async Task DoesNothingUsually() => await VerifyCS.VerifyAnalyzerAsync(""" + public async Task DoesNotReport_OnOperation() => await VerifyCS.VerifyAnalyzerAsync(""" using System; using Rascal; using static Rascal.Prelude; @@ -21,7 +21,7 @@ public static void Bar() """); [Fact] - public async Task ReportsOnIdMatchCall() => await VerifyCS.VerifyAnalyzerAsync(""" + public async Task Reports_OnId() => await VerifyCS.VerifyAnalyzerAsync(""" using System; using Rascal; using static Rascal.Prelude; diff --git a/src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs index 2b4d5f9..e779dd0 100644 --- a/src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs +++ b/src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs @@ -5,7 +5,7 @@ namespace Rascal.Analysis.Analyzers.Tests; public class UseMapAnalyzerTests { [Fact] - public async Task DoesNothingUsually() => await VerifyCS.VerifyAnalyzerAsync(""" + public async Task DoesNotReport_OnOperation() => await VerifyCS.VerifyAnalyzerAsync(""" using System; using Rascal; using static Rascal.Prelude; @@ -23,7 +23,7 @@ public static void Bar() """); [Fact] - public async Task ReportsOnLambdaOk() => await VerifyCS.VerifyAnalyzerAsync(""" + public async Task Reports_OnIdOk() => await VerifyCS.VerifyAnalyzerAsync(""" using System; using Rascal; using static Rascal.Prelude; @@ -39,7 +39,7 @@ public static void Bar() """); [Fact] - public async Task ReportsOnLambdaCtor() => await VerifyCS.VerifyAnalyzerAsync(""" + public async Task Reports_OnExplicitCtor() => await VerifyCS.VerifyAnalyzerAsync(""" using System; using Rascal; using static Rascal.Prelude; @@ -55,7 +55,7 @@ public static void Bar() """); [Fact] - public async Task ReportsOnLambdaCtorImplicit() => await VerifyCS.VerifyAnalyzerAsync(""" + public async Task Reports_OnImplicitCtor() => await VerifyCS.VerifyAnalyzerAsync(""" using System; using Rascal; using static Rascal.Prelude; @@ -71,7 +71,7 @@ public static void Bar() """); [Fact] - public async Task ReportsOnLambdaConversion() => await VerifyCS.VerifyAnalyzerAsync(""" + public async Task Reports_OnConversion() => await VerifyCS.VerifyAnalyzerAsync(""" using System; using Rascal; using static Rascal.Prelude; diff --git a/src/Rascal.Analysis.Tests/Analyzers/UseThenAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/UseThenAnalyzerTests.cs index de626dc..4e52659 100644 --- a/src/Rascal.Analysis.Tests/Analyzers/UseThenAnalyzerTests.cs +++ b/src/Rascal.Analysis.Tests/Analyzers/UseThenAnalyzerTests.cs @@ -5,7 +5,7 @@ namespace Rascal.Analysis.Analyzers.Tests; public class UseThenAnalyzerTests { [Fact] - public async Task DoesNothingUsually() => await VerifyCS.VerifyAnalyzerAsync(""" + public async Task DoesNotReport_OnMapWithoutUnnest() => await VerifyCS.VerifyAnalyzerAsync(""" using System; using Rascal; using static Rascal.Prelude; @@ -21,7 +21,7 @@ public static void Bar() """); [Fact] - public async Task ReportsOnMapUnnestExtensionForm() => await VerifyCS.VerifyAnalyzerAsync(""" + public async Task Report_OnMapUnnest_ExtensionForm() => await VerifyCS.VerifyAnalyzerAsync(""" using System; using Rascal; using static Rascal.Prelude; @@ -37,7 +37,7 @@ public static void Bar() """); [Fact] - public async Task ReportsOnMapUnnestCallForm() => await VerifyCS.VerifyAnalyzerAsync(""" + public async Task Reports_OnMapUnnest_MethodForm() => await VerifyCS.VerifyAnalyzerAsync(""" using System; using Rascal; using static Rascal.Prelude; diff --git a/src/Rascal.Analysis.Tests/CodeFixes/RemoveMapIdCallCodeFixTests.cs b/src/Rascal.Analysis.Tests/CodeFixes/RemoveMapIdCallCodeFixTests.cs index 84df6ef..4f853cc 100644 --- a/src/Rascal.Analysis.Tests/CodeFixes/RemoveMapIdCallCodeFixTests.cs +++ b/src/Rascal.Analysis.Tests/CodeFixes/RemoveMapIdCallCodeFixTests.cs @@ -5,7 +5,7 @@ namespace Rascal.Analysis.CodeFixes.Tests; public class RemoveMapIdCallCodeFixProviderTests { [Fact] - public async Task FixesMapIdCall() => await VerifyCS.VerifyCodeFixAsync(""" + public async Task Fixes_MapIdCall() => await VerifyCS.VerifyCodeFixAsync(""" using System; using Rascal; using static Rascal.Prelude; diff --git a/src/Rascal.Analysis.Tests/CodeFixes/UseThenCodeFixTests.cs b/src/Rascal.Analysis.Tests/CodeFixes/UseThenCodeFixTests.cs index b0afe94..53b77dc 100644 --- a/src/Rascal.Analysis.Tests/CodeFixes/UseThenCodeFixTests.cs +++ b/src/Rascal.Analysis.Tests/CodeFixes/UseThenCodeFixTests.cs @@ -5,7 +5,7 @@ namespace Rascal.Analysis.CodeFixes.Tests; public class UseThenCodeFixTests { [Fact] - public async Task FixesExtensionForm() => await VerifyCS.VerifyCodeFixAsync(""" + public async Task Fixes_ExtensionForm() => await VerifyCS.VerifyCodeFixAsync(""" using System; using Rascal; using static Rascal.Prelude; @@ -34,7 +34,7 @@ public static void Bar() """); [Fact] - public async Task FixesCallForm() => await VerifyCS.VerifyCodeFixAsync(""" + public async Task Fixes_CallForm() => await VerifyCS.VerifyCodeFixAsync(""" using System; using Rascal; using static Rascal.Prelude; From ae532812c784c30164ed1faba574f452f97d4bb9 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Mon, 8 Jan 2024 14:54:49 +0100 Subject: [PATCH 40/43] Add WellKnownSymbols --- src/Rascal.Analysis/WellKnownSymbols.cs | 83 +++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/Rascal.Analysis/WellKnownSymbols.cs diff --git a/src/Rascal.Analysis/WellKnownSymbols.cs b/src/Rascal.Analysis/WellKnownSymbols.cs new file mode 100644 index 0000000..a96fd6d --- /dev/null +++ b/src/Rascal.Analysis/WellKnownSymbols.cs @@ -0,0 +1,83 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Rascal.Analysis; + +public sealed record WellKnownSymbols( + INamedTypeSymbol ObjectType, + INamedTypeSymbol Result1Type, + INamedTypeSymbol ResultExtensionsType, + IMethodSymbol MapMethod, + IMethodSymbol ThenMethod, + IMethodSymbol ToMethod, + IMethodSymbol UnnestMethod) +{ + private const string Result1TypeName = "Rascal.Result`1"; + private const string ResultExtensionsTypeName = "Rascal.ResultExtensions"; + + public static bool TryCreate( + Compilation compilation, + [NotNullWhen(true)] out WellKnownSymbols? symbols, + [NotNullWhen(false)] out IReadOnlyCollection? errors) + { + var es = new List(); + + var objectType = compilation.GetSpecialType(SpecialType.System_Object); + + INamedTypeSymbol? GetType(string metadataName) + { + var type = compilation.GetTypeByMetadataName(metadataName); + if (type is null) es!.Add(new(metadataName)); + return type; + } + + var resultType = GetType(Result1TypeName); + var resultExtensionsType = GetType(ResultExtensionsTypeName); + + if (es is not []) + { + symbols = null; + errors = es; + return false; + } + + Func> GetMember(ITypeSymbol type, string typeMetadataName) => name => + { + var members = type.GetMembers(name); + if (members is []) es.Add(new($"{typeMetadataName}.{name}")); + return members; + }; + + var getResultMember = GetMember(resultType!, Result1TypeName); + + ISymbol GetSingleResultMethod(string name) => + getResultMember(name).FirstOrDefault()!; + + ImmutableArray GetExtensionMethod(string name) => + GetMember(resultExtensionsType!, ResultExtensionsTypeName)(name); + + var mapMethod = (IMethodSymbol?)GetSingleResultMethod("Map"); + var thenMethod = (IMethodSymbol?)GetSingleResultMethod("Then"); + var toMethod = (IMethodSymbol?)GetSingleResultMethod("To"); + var unnestMethod = (IMethodSymbol?)GetExtensionMethod("Unnest").FirstOrDefault(); + + if (es is not []) + { + symbols = null; + errors = es; + return false; + } + + symbols = new( + objectType, + resultType!, + resultExtensionsType!, + mapMethod!, + thenMethod!, + toMethod!, + unnestMethod!); + errors = null; + return true; + } + + public readonly record struct Error(string MemberName); +} From 77f0d0e0b387b244063ce8c651647d2c6c91307f Mon Sep 17 00:00:00 2001 From: thinker227 Date: Mon, 8 Jan 2024 15:32:50 +0100 Subject: [PATCH 41/43] Add BaseAnalyzer and MissingSymbolAnalyzer --- src/Rascal.Analysis/Analyzers/BaseAnalyzer.cs | 19 +++ .../Analyzers/MissingSymbolAnalyzer.cs | 28 ++++ .../Analyzers/ToSusTypeAnalyzer.cs | 122 ++++++++---------- .../Analyzers/UnnecessaryIdMapAnalyzer.cs | 104 +++++++-------- .../UseGetValueOrForIdMatchAnalyzer.cs | 94 ++++++-------- .../Analyzers/UseMapAnalyzer.cs | 66 +++------- .../Analyzers/UseThenAnalyzer.cs | 79 +++++------- src/Rascal.Analysis/AssemblyAttributes.cs | 3 + src/Rascal.Analysis/Diagnostics.cs | 10 ++ src/Rascal.Analysis/WellKnownSymbols.cs | 55 ++++++-- 10 files changed, 286 insertions(+), 294 deletions(-) create mode 100644 src/Rascal.Analysis/Analyzers/BaseAnalyzer.cs create mode 100644 src/Rascal.Analysis/Analyzers/MissingSymbolAnalyzer.cs create mode 100644 src/Rascal.Analysis/AssemblyAttributes.cs diff --git a/src/Rascal.Analysis/Analyzers/BaseAnalyzer.cs b/src/Rascal.Analysis/Analyzers/BaseAnalyzer.cs new file mode 100644 index 0000000..5141975 --- /dev/null +++ b/src/Rascal.Analysis/Analyzers/BaseAnalyzer.cs @@ -0,0 +1,19 @@ +namespace Rascal.Analysis.Analyzers; + +public abstract class BaseAnalyzer : DiagnosticAnalyzer +{ + public sealed override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(compilationCtx => + { + // If any errors are present, they will be reported by MissingSymbolAnalyzer. + if (WellKnownSymbols.TryCreate(compilationCtx.Compilation, out var symbols, out _)) + Handle(compilationCtx, symbols); + }); + } + + protected abstract void Handle(CompilationStartAnalysisContext ctx, WellKnownSymbols symbols); +} diff --git a/src/Rascal.Analysis/Analyzers/MissingSymbolAnalyzer.cs b/src/Rascal.Analysis/Analyzers/MissingSymbolAnalyzer.cs new file mode 100644 index 0000000..e1715a0 --- /dev/null +++ b/src/Rascal.Analysis/Analyzers/MissingSymbolAnalyzer.cs @@ -0,0 +1,28 @@ +namespace Rascal.Analysis.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class MissingSymbolAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + Diagnostics.MissingSymbol); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationAction(compilationCtx => + { + if (WellKnownSymbols.TryCreate(compilationCtx.Compilation, out _, out var errors)) + return; + + foreach (var error in errors) + { + compilationCtx.ReportDiagnostic(Diagnostic.Create( + Diagnostics.MissingSymbol, + null, + error.MemberName)); + } + }); + } +} diff --git a/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs b/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs index 10a293c..4a0d1a6 100644 --- a/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs @@ -3,88 +3,70 @@ namespace Rascal.Analysis.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class ToSusTypeAnalyzer : DiagnosticAnalyzer +public sealed class ToSusTypeAnalyzer : BaseAnalyzer { public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( Diagnostics.ToSameType, Diagnostics.ToImpossibleType); - public override void Initialize(AnalysisContext context) + protected override void Handle(CompilationStartAnalysisContext ctx, WellKnownSymbols symbols) => ctx.RegisterOperationAction(operationCtx => { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); - - context.RegisterCompilationStartAction(compilationCtx => - { - var resultType = compilationCtx.Compilation.GetTypeByMetadataName("Rascal.Result`1"); - if (resultType is null) return; - - var resultMembers = resultType.GetMembers(); - - var toMethod = (IMethodSymbol)resultMembers.First(x => x.Name == "To"); - - var objectType = compilationCtx.Compilation.GetSpecialType(SpecialType.System_Object); - - compilationCtx.RegisterOperationAction(operationCtx => - { - var operation = (IInvocationOperation)operationCtx.Operation; + var operation = (IInvocationOperation)operationCtx.Operation; - // Check that the target method is To - var method = operation.TargetMethod; - if (!method.OriginalDefinition.Equals(toMethod, SymbolEqualityComparer.Default)) return; + // Check that the target method is To + var method = operation.TargetMethod; + if (!method.OriginalDefinition.Equals(symbols.ToMethod, SymbolEqualityComparer.Default)) return; - // Get the location of the type argument - var location = operation.Syntax is InvocationExpressionSyntax + // Get the location of the type argument + var location = operation.Syntax is InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax + { + Name: GenericNameSyntax { - Expression: MemberAccessExpressionSyntax - { - Name: GenericNameSyntax - { - TypeArgumentList.Arguments: [var typeSyntax] - } - } + TypeArgumentList.Arguments: [var typeSyntax] } - ? typeSyntax.GetLocation() - : operation.Syntax.GetLocation(); + } + } + ? typeSyntax.GetLocation() + : operation.Syntax.GetLocation(); - // Get the source and target type for the conversion - var sourceType = method.ContainingType.TypeArguments[0]; - var targetType = method.TypeArguments[0]; + // Get the source and target type for the conversion + var sourceType = method.ContainingType.TypeArguments[0]; + var targetType = method.TypeArguments[0]; - // Report diagnostic if both source and target types are the same - if (sourceType.Equals(targetType, SymbolEqualityComparer.Default)) - { - operationCtx.ReportDiagnostic(Diagnostic.Create( - Diagnostics.ToSameType, - location, - sourceType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); - return; - } + // Report diagnostic if both source and target types are the same + if (sourceType.Equals(targetType, SymbolEqualityComparer.Default)) + { + operationCtx.ReportDiagnostic(Diagnostic.Create( + Diagnostics.ToSameType, + location, + sourceType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); + return; + } - // Return early if the types are compatible - switch (sourceType.TypeKind, targetType.TypeKind) - { - // One of the types is an interface - case (TypeKind.Interface, _) or (_, TypeKind.Interface): return; - // One of the types is a type parameter - case (TypeKind.TypeParameter, _) or (_, TypeKind.TypeParameter): return; - // One of the types is object - case (_, _) when - sourceType.Equals(objectType, SymbolEqualityComparer.Default) || - targetType.Equals(objectType, SymbolEqualityComparer.Default): return; - // Both types are classes and one inherits the other - case (TypeKind.Class, TypeKind.Class) when - sourceType.Inherits(targetType) || - targetType.Inherits(sourceType): return; - } + // Return early if the types are compatible + switch (sourceType.TypeKind, targetType.TypeKind) + { + // One of the types is an interface + case (TypeKind.Interface, _) or (_, TypeKind.Interface): return; + // One of the types is a type parameter + case (TypeKind.TypeParameter, _) or (_, TypeKind.TypeParameter): return; + // One of the types is object + case (_, _) when + sourceType.Equals(symbols.ObjectType, SymbolEqualityComparer.Default) || + targetType.Equals(symbols.ObjectType, SymbolEqualityComparer.Default): return; + // Both types are classes and one inherits the other + case (TypeKind.Class, TypeKind.Class) when + sourceType.Inherits(targetType) || + targetType.Inherits(sourceType): return; + } - // The types are sus - operationCtx.ReportDiagnostic(Diagnostic.Create( - Diagnostics.ToImpossibleType, - location, - sourceType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), - targetType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); - }, OperationKind.Invocation); - }); - } + // The types are sus + operationCtx.ReportDiagnostic(Diagnostic.Create( + Diagnostics.ToImpossibleType, + location, + sourceType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), + targetType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); + }, OperationKind.Invocation); } diff --git a/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs index 250cc0f..81ced6e 100644 --- a/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs @@ -4,75 +4,59 @@ namespace Rascal.Analysis.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class UnnecessaryIdMapAnalyzer : DiagnosticAnalyzer +public sealed class UnnecessaryIdMapAnalyzer : BaseAnalyzer { public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( Diagnostics.UnnecessaryIdMap); - - public override void Initialize(AnalysisContext context) + + protected override void Handle(CompilationStartAnalysisContext ctx, WellKnownSymbols symbols) => ctx.RegisterOperationAction(operationCtx => { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); + var operation = (IInvocationOperation)operationCtx.Operation; + + // Check that it is Map being called. + if (!operation.TargetMethod.OriginalDefinition.Equals(symbols.MapMethod, SymbolEqualityComparer.Default)) + return; - context.RegisterCompilationStartAction(compilationCtx => - { - var resultType = compilationCtx.Compilation.GetTypeByMetadataName("Rascal.Result`1"); - if (resultType is null) return; - - var resultMembers = resultType.GetMembers(); - - var mapMethod = (IMethodSymbol)resultMembers.First(x => x.Name == "Map"); - - compilationCtx.RegisterOperationAction(operationCtx => + // Check that the first argument is a lambda with a single parameter which immediately returns. + if (operation.Arguments is not + [ { - var operation = (IInvocationOperation)operationCtx.Operation; - - // Check that it is Map being called. - if (!operation.TargetMethod.OriginalDefinition.Equals(mapMethod, SymbolEqualityComparer.Default)) - return; - - // Check that the first argument is a lambda with a single parameter which immediately returns. - if (operation.Arguments is not - [ + Value: IDelegateCreationOperation + { + Target: IAnonymousFunctionOperation { - Value: IDelegateCreationOperation - { - Target: IAnonymousFunctionOperation + Body.Operations: + [ + IReturnOperation { - Body.Operations: - [ - IReturnOperation - { - ReturnedValue: IParameterReferenceOperation returnReference - } - ], - Symbol.Parameters: [var lambdaParameter] + ReturnedValue: IParameterReferenceOperation returnReference } - } + ], + Symbol.Parameters: [var lambdaParameter] } - ]) return; - - // Check that the returned parameter is the same as the lambda parameter. - if (!returnReference.Parameter.Equals(lambdaParameter, SymbolEqualityComparer.Default)) return; - - // Get the location of the method invocation. - var location = operation.Syntax is InvocationExpressionSyntax - { - Expression: MemberAccessExpressionSyntax - { - Name.Span.Start: var start - }, - Span.End: var end } - ? Location.Create(operation.Syntax.SyntaxTree, TextSpan.FromBounds(start, end)) - : operation.Syntax.GetLocation(); - - // Report the diagnostic. - operationCtx.ReportDiagnostic(Diagnostic.Create( - Diagnostics.UnnecessaryIdMap, - location, - lambdaParameter.Name)); - }, OperationKind.Invocation); - }); - } + } + ]) return; + + // Check that the returned parameter is the same as the lambda parameter. + if (!returnReference.Parameter.Equals(lambdaParameter, SymbolEqualityComparer.Default)) return; + + // Get the location of the method invocation. + var location = operation.Syntax is InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax + { + Name.Span.Start: var start + }, + Span.End: var end + } + ? Location.Create(operation.Syntax.SyntaxTree, TextSpan.FromBounds(start, end)) + : operation.Syntax.GetLocation(); + + // Report the diagnostic. + operationCtx.ReportDiagnostic(Diagnostic.Create( + Diagnostics.UnnecessaryIdMap, + location, + lambdaParameter.Name)); + }, OperationKind.Invocation); } diff --git a/src/Rascal.Analysis/Analyzers/UseGetValueOrForIdMatchAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseGetValueOrForIdMatchAnalyzer.cs index d042e15..88811a0 100644 --- a/src/Rascal.Analysis/Analyzers/UseGetValueOrForIdMatchAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UseGetValueOrForIdMatchAnalyzer.cs @@ -3,71 +3,55 @@ namespace Rascal.Analysis.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class UseGetValueOrForIdMatchAnalyzer : DiagnosticAnalyzer +public sealed class UseGetValueOrForIdMatchAnalyzer : BaseAnalyzer { public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( Diagnostics.UseGetValueOrForIdMatch); - public override void Initialize(AnalysisContext context) + protected override void Handle(CompilationStartAnalysisContext ctx, WellKnownSymbols symbols) => ctx.RegisterOperationAction(operationCtx => { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); - - context.RegisterCompilationStartAction(compilationCtx => - { - var resultType = compilationCtx.Compilation.GetTypeByMetadataName("Rascal.Result`1"); - if (resultType is null) return; - - var resultMembers = resultType.GetMembers(); - - var matchMethod = (IMethodSymbol)resultMembers.First(x => x.Name == "Match"); - - compilationCtx.RegisterOperationAction(operationCtx => - { - var operation = (IInvocationOperation)operationCtx.Operation; + var operation = (IInvocationOperation)operationCtx.Operation; - if (!operation.TargetMethod.OriginalDefinition.Equals(matchMethod, SymbolEqualityComparer.Default)) - return; + if (!operation.TargetMethod.OriginalDefinition.Equals(symbols.MatchMethod, SymbolEqualityComparer.Default)) + return; - // Check that the first argument is a lambda with a single parameter which immediately returns. - if (operation.Arguments is not - [ + // Check that the first argument is a lambda with a single parameter which immediately returns. + if (operation.Arguments is not + [ + { + Value: IDelegateCreationOperation + { + Target: IAnonymousFunctionOperation { - Value: IDelegateCreationOperation - { - Target: IAnonymousFunctionOperation + Body.Operations: + [ + IReturnOperation { - Body.Operations: - [ - IReturnOperation - { - ReturnedValue: IParameterReferenceOperation returnReference - } - ], - Symbol.Parameters: [var lambdaParameter] + ReturnedValue: IParameterReferenceOperation returnReference } - } - }, - .. - ]) return; - - // Check that the returned parameter is the same as the lambda parameter. - if (!returnReference.Parameter.Equals(lambdaParameter, SymbolEqualityComparer.Default)) return; - - // Get the location of the method invocation. - var location = operation.Syntax is InvocationExpressionSyntax - { - Expression: MemberAccessExpressionSyntax memberAccessExpression + ], + Symbol.Parameters: [var lambdaParameter] + } } - ? memberAccessExpression.Name.GetLocation() - : operation.Syntax.GetLocation(); + }, + .. + ]) return; + + // Check that the returned parameter is the same as the lambda parameter. + if (!returnReference.Parameter.Equals(lambdaParameter, SymbolEqualityComparer.Default)) return; + + // Get the location of the method invocation. + var location = operation.Syntax is InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax memberAccessExpression + } + ? memberAccessExpression.Name.GetLocation() + : operation.Syntax.GetLocation(); - // Report the diagnostic. - operationCtx.ReportDiagnostic(Diagnostic.Create( - Diagnostics.UseGetValueOrForIdMatch, - location, - lambdaParameter.Name)); - }, OperationKind.Invocation); - }); - } + // Report the diagnostic. + operationCtx.ReportDiagnostic(Diagnostic.Create( + Diagnostics.UseGetValueOrForIdMatch, + location, + lambdaParameter.Name)); + }, OperationKind.Invocation); } diff --git a/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs index b9e5430..c94c0ad 100644 --- a/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs @@ -3,60 +3,34 @@ namespace Rascal.Analysis.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class UseMapAnalyzer : DiagnosticAnalyzer +public sealed class UseMapAnalyzer : BaseAnalyzer { public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( Diagnostics.UseMap); - - public override void Initialize(AnalysisContext context) - { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); - - context.RegisterCompilationStartAction(compilationCtx => - { - var resultType = compilationCtx.Compilation.GetTypeByMetadataName("Rascal.Result`1"); - if (resultType is null) return; - - var preludeType = compilationCtx.Compilation.GetTypeByMetadataName("Rascal.Prelude"); - if (preludeType is null) return; - - var resultMembers = resultType.GetMembers(); - var preludeMembers = preludeType.GetMembers(); - var thenMethod = (IMethodSymbol)resultMembers.First(x => x.Name == "Then"); - var okMethod = (IMethodSymbol)preludeMembers.First(x => x.Name == "Ok"); - var okCtor = resultType.InstanceConstructors - .First(x => x.Parameters is [{ Type: ITypeParameterSymbol }]); - var okConversion = (IMethodSymbol)resultMembers - .OfType() - .First(x => x is { Name: "op_Implicit", Parameters: [{ Type: ITypeParameterSymbol }] }); - - compilationCtx.RegisterOperationAction(operationCtx => - { - var operation = (IInvocationOperation)operationCtx.Operation; + protected override void Handle(CompilationStartAnalysisContext ctx, WellKnownSymbols symbols) => ctx.RegisterOperationAction(operationCtx => + { + var operation = (IInvocationOperation)operationCtx.Operation; - // Check that it is Then being called. - if (!operation.TargetMethod.OriginalDefinition.Equals(thenMethod, SymbolEqualityComparer.Default)) return; + // Check that it is Then being called. + if (!operation.TargetMethod.OriginalDefinition.Equals(symbols.ThenMethod, SymbolEqualityComparer.Default)) return; - // Check that the operation should be targeted. - if (!IsTargetOperation(operation, okMethod, okCtor, okConversion)) return; + // Check that the operation should be targeted. + if (!IsTargetOperation(operation, symbols.OkMethod, symbols.OkCtor, symbols.OkConversion)) return; - // Get the location of the method name. - var location = operation.Syntax is InvocationExpressionSyntax - { - Expression: MemberAccessExpressionSyntax memberAccessExpression - } - ? memberAccessExpression.Name.GetLocation() - : operation.Syntax.GetLocation(); + // Get the location of the method name. + var location = operation.Syntax is InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax memberAccessExpression + } + ? memberAccessExpression.Name.GetLocation() + : operation.Syntax.GetLocation(); - // Report the diagnostic. - operationCtx.ReportDiagnostic(Diagnostic.Create( - Diagnostics.UseMap, - location)); - }, OperationKind.Invocation); - }); - } + // Report the diagnostic. + operationCtx.ReportDiagnostic(Diagnostic.Create( + Diagnostics.UseMap, + location)); + }, OperationKind.Invocation); private static bool IsTargetOperation( IInvocationOperation operation, diff --git a/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs index f64fd32..b964df7 100644 --- a/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs +++ b/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs @@ -3,64 +3,43 @@ namespace Rascal.Analysis.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class UseThenAnalyzer : DiagnosticAnalyzer +public sealed class UseThenAnalyzer : BaseAnalyzer { public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( Diagnostics.UseThen); - public override void Initialize(AnalysisContext context) + protected override void Handle(CompilationStartAnalysisContext ctx, WellKnownSymbols symbols) => ctx.RegisterOperationAction(operationCtx => { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); - - context.RegisterCompilationStartAction(compilationCtx => - { - var resultType = compilationCtx.Compilation.GetTypeByMetadataName("Rascal.Result`1"); - if (resultType is null) return; - - var resultExtensionsType = compilationCtx.Compilation.GetTypeByMetadataName("Rascal.ResultExtensions"); - if (resultExtensionsType is null) return; - - var resultMembers = resultType.GetMembers(); - var resultExtensionsMembers = resultExtensionsType.GetMembers(); - - var mapMethod = (IMethodSymbol)resultMembers.First(x => x.Name == "Map"); - var unnestMethod = (IMethodSymbol)resultExtensionsMembers.First(x => x.Name == "Unnest"); - - compilationCtx.RegisterOperationAction(operationCtx => - { - var operation = (IInvocationOperation)operationCtx.Operation; - - // Check that it is Unnest being called. - if (!operation.TargetMethod.OriginalDefinition.Equals(unnestMethod, SymbolEqualityComparer.Default)) - return; + var operation = (IInvocationOperation)operationCtx.Operation; - // Check that the first argument is an invocation. - if (operation.Arguments is not - [ - { - Value: IInvocationOperation argumentInvocation - } - ]) return; + // Check that it is Unnest being called. + if (!operation.TargetMethod.OriginalDefinition.Equals(symbols.UnnestMethod, SymbolEqualityComparer.Default)) + return; - // Check that the invoked method is Map. - if (!argumentInvocation.TargetMethod.OriginalDefinition - .Equals(mapMethod, SymbolEqualityComparer.Default)) - return; - - // Get the location of the method name. - var location = argumentInvocation.Syntax is InvocationExpressionSyntax + // Check that the first argument is an invocation. + if (operation.Arguments is not + [ { - Expression: MemberAccessExpressionSyntax memberAccessExpression + Value: IInvocationOperation argumentInvocation } - ? memberAccessExpression.Name.GetLocation() - : argumentInvocation.Syntax.GetLocation(); + ]) return; + + // Check that the invoked method is Map. + if (!argumentInvocation.TargetMethod.OriginalDefinition + .Equals(symbols.MapMethod, SymbolEqualityComparer.Default)) + return; - // Report the diagnostic. - operationCtx.ReportDiagnostic(Diagnostic.Create( - Diagnostics.UseThen, - location)); - }, OperationKind.Invocation); - }); - } + // Get the location of the method name. + var location = argumentInvocation.Syntax is InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax memberAccessExpression + } + ? memberAccessExpression.Name.GetLocation() + : argumentInvocation.Syntax.GetLocation(); + + // Report the diagnostic. + operationCtx.ReportDiagnostic(Diagnostic.Create( + Diagnostics.UseThen, + location)); + }, OperationKind.Invocation); } diff --git a/src/Rascal.Analysis/AssemblyAttributes.cs b/src/Rascal.Analysis/AssemblyAttributes.cs new file mode 100644 index 0000000..7112ee8 --- /dev/null +++ b/src/Rascal.Analysis/AssemblyAttributes.cs @@ -0,0 +1,3 @@ +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("MicrosoftCodeAnalysisPerformance", "RS1012:Start action has no registered actions")] diff --git a/src/Rascal.Analysis/Diagnostics.cs b/src/Rascal.Analysis/Diagnostics.cs index 9d625a0..b01d84c 100644 --- a/src/Rascal.Analysis/Diagnostics.cs +++ b/src/Rascal.Analysis/Diagnostics.cs @@ -62,4 +62,14 @@ public static class Diagnostics DiagnosticSeverity.Warning, true, "Calling 'Match' with an identity function for the 'ifOk' parameter is equivalent to 'DefaultOr'."); + + public static DiagnosticDescriptor MissingSymbol { get; } = new( + "RASCAL007", + "Missing symbol required for analysis", + "Cannot find type or member '{0}' which is required for analysis. " + + "No analysis will be performed. " + + "Verify that the version of the analyzer package matches that of the library, or report this as a bug.", + "Analysis", + DiagnosticSeverity.Warning, + true); } diff --git a/src/Rascal.Analysis/WellKnownSymbols.cs b/src/Rascal.Analysis/WellKnownSymbols.cs index a96fd6d..738e9c4 100644 --- a/src/Rascal.Analysis/WellKnownSymbols.cs +++ b/src/Rascal.Analysis/WellKnownSymbols.cs @@ -5,14 +5,21 @@ namespace Rascal.Analysis; public sealed record WellKnownSymbols( INamedTypeSymbol ObjectType, INamedTypeSymbol Result1Type, + ITypeParameterSymbol TypeParameterT, INamedTypeSymbol ResultExtensionsType, + INamedTypeSymbol PreludeType, IMethodSymbol MapMethod, IMethodSymbol ThenMethod, IMethodSymbol ToMethod, - IMethodSymbol UnnestMethod) + IMethodSymbol MatchMethod, + IMethodSymbol UnnestMethod, + IMethodSymbol OkMethod, + IMethodSymbol OkCtor, + IMethodSymbol OkConversion) { private const string Result1TypeName = "Rascal.Result`1"; private const string ResultExtensionsTypeName = "Rascal.ResultExtensions"; + private const string PreludeTypeName = "Rascal.Prelude"; public static bool TryCreate( Compilation compilation, @@ -26,12 +33,13 @@ public static bool TryCreate( INamedTypeSymbol? GetType(string metadataName) { var type = compilation.GetTypeByMetadataName(metadataName); - if (type is null) es!.Add(new(metadataName)); + if (type is null) es.Add(new(metadataName)); return type; } var resultType = GetType(Result1TypeName); var resultExtensionsType = GetType(ResultExtensionsTypeName); + var preludeType = GetType(PreludeTypeName); if (es is not []) { @@ -40,6 +48,8 @@ public static bool TryCreate( return false; } + var typeParameterT = resultType!.TypeParameters[0]; + Func> GetMember(ITypeSymbol type, string typeMetadataName) => name => { var members = type.GetMembers(name); @@ -48,17 +58,30 @@ Func> GetMember(ITypeSymbol type, string typeMet }; var getResultMember = GetMember(resultType!, Result1TypeName); - - ISymbol GetSingleResultMethod(string name) => - getResultMember(name).FirstOrDefault()!; - - ImmutableArray GetExtensionMethod(string name) => - GetMember(resultExtensionsType!, ResultExtensionsTypeName)(name); + var getResultExtensionsMember = GetMember(resultExtensionsType!, ResultExtensionsTypeName); + var getPreludeMember = GetMember(preludeType!, PreludeTypeName); + + IMethodSymbol? GetSingleResultMethod(string name) => + (IMethodSymbol?)getResultMember!(name).FirstOrDefault(); + + IMethodSymbol? GetSinglePreludeMethod(string name) => + (IMethodSymbol?)getPreludeMember!(name).FirstOrDefault(); - var mapMethod = (IMethodSymbol?)GetSingleResultMethod("Map"); - var thenMethod = (IMethodSymbol?)GetSingleResultMethod("Then"); - var toMethod = (IMethodSymbol?)GetSingleResultMethod("To"); - var unnestMethod = (IMethodSymbol?)GetExtensionMethod("Unnest").FirstOrDefault(); + var mapMethod = GetSingleResultMethod("Map"); + var thenMethod = GetSingleResultMethod("Then"); + var toMethod = GetSingleResultMethod("To"); + var unnestMethod = (IMethodSymbol?)getResultExtensionsMember("Unnest").FirstOrDefault(); + var matchMethod = GetSingleResultMethod("Match"); + var okMethod = GetSinglePreludeMethod("Ok"); + var okCtor = resultType!.InstanceConstructors + .FirstOrDefault(ctor => + ctor.Parameters is [{ Type: ITypeParameterSymbol t }] && + t.Equals(typeParameterT, SymbolEqualityComparer.Default)); + var okConversion = resultType.GetMembers("op_Implicit") + .OfType() + .FirstOrDefault(method => + method.Parameters is [{ Type: ITypeParameterSymbol t }] && + t.Equals(typeParameterT, SymbolEqualityComparer.Default)); if (es is not []) { @@ -70,11 +93,17 @@ ImmutableArray GetExtensionMethod(string name) => symbols = new( objectType, resultType!, + typeParameterT!, resultExtensionsType!, + preludeType!, mapMethod!, thenMethod!, toMethod!, - unnestMethod!); + matchMethod!, + unnestMethod!, + okMethod!, + okCtor!, + okConversion!); errors = null; return true; } From a70d7f48e9536b82e8cd8562668f349f9a2b0a78 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Tue, 9 Jan 2024 15:00:45 +0100 Subject: [PATCH 42/43] Include analysis project into nupkg --- src/Rascal/Rascal.csproj | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Rascal/Rascal.csproj b/src/Rascal/Rascal.csproj index 84b70f3..6293443 100644 --- a/src/Rascal/Rascal.csproj +++ b/src/Rascal/Rascal.csproj @@ -44,5 +44,11 @@ all + + + + + + From 11f5e9f3895396d3644c0a740cd568ffa35288d0 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Tue, 9 Jan 2024 15:01:07 +0100 Subject: [PATCH 43/43] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfc1670..59d7da4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,3 +27,5 @@ - Add additional overloads for `Result.Equals` which allow specifying an equality comparer to use for comparing values. - Rename `GetValueOrDefault(T @default)` and `GetValueOrDefault(Func getDefault)` to `GetValueOr`. + +- Add analyzer and code-fix suite.