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. diff --git a/Rascal.sln b/Rascal.sln index e74f047..c58a221 100644 --- a/Rascal.sln +++ b/Rascal.sln @@ -1,41 +1,55 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D92004B4-8D01-4BC3-B76B-A80D657FEC30}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rascal", "src\Rascal\Rascal.csproj", "{04DE173D-4E74-4104-94FA-2B1807FCD34D}" -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.Playground", "src\Rascal.Playground\Rascal.Playground.csproj", "{F5A7C08C-51B5-4043-9666-C8DCD4F9C1E4}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {04DE173D-4E74-4104-94FA-2B1807FCD34D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {04DE173D-4E74-4104-94FA-2B1807FCD34D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {04DE173D-4E74-4104-94FA-2B1807FCD34D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {04DE173D-4E74-4104-94FA-2B1807FCD34D}.Release|Any CPU.Build.0 = Release|Any CPU - {291FDB2E-8AC8-4B7A-B6E6-6DF5DCBCEB58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {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 - {F5A7C08C-51B5-4043-9666-C8DCD4F9C1E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F5A7C08C-51B5-4043-9666-C8DCD4F9C1E4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F5A7C08C-51B5-4043-9666-C8DCD4F9C1E4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F5A7C08C-51B5-4043-9666-C8DCD4F9C1E4}.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} - {F5A7C08C-51B5-4043-9666-C8DCD4F9C1E4} = {D92004B4-8D01-4BC3-B76B-A80D657FEC30} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D92004B4-8D01-4BC3-B76B-A80D657FEC30}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rascal", "src\Rascal\Rascal.csproj", "{04DE173D-4E74-4104-94FA-2B1807FCD34D}" +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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rascal.Playground", "src\Rascal.Playground\Rascal.Playground.csproj", "{F5A7C08C-51B5-4043-9666-C8DCD4F9C1E4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {04DE173D-4E74-4104-94FA-2B1807FCD34D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04DE173D-4E74-4104-94FA-2B1807FCD34D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04DE173D-4E74-4104-94FA-2B1807FCD34D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04DE173D-4E74-4104-94FA-2B1807FCD34D}.Release|Any CPU.Build.0 = Release|Any CPU + {291FDB2E-8AC8-4B7A-B6E6-6DF5DCBCEB58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 + {F5A7C08C-51B5-4043-9666-C8DCD4F9C1E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F5A7C08C-51B5-4043-9666-C8DCD4F9C1E4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5A7C08C-51B5-4043-9666-C8DCD4F9C1E4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F5A7C08C-51B5-4043-9666-C8DCD4F9C1E4}.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} + {F5A7C08C-51B5-4043-9666-C8DCD4F9C1E4} = {D92004B4-8D01-4BC3-B76B-A80D657FEC30} + EndGlobalSection +EndGlobal diff --git a/src/Rascal.Analysis.Tests/Analyzers/ToSusTypeAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/ToSusTypeAnalyzerTests.cs new file mode 100644 index 0000000..58f8902 --- /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<{|RASCAL004: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<{|RASCAL005: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<{|RASCAL005: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.Tests/Analyzers/UnnecessaryIdMapAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/UnnecessaryIdMapAnalyzerTests.cs new file mode 100644 index 0000000..b7c446f --- /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 DoesNotReport_OnMapOperationCall() => 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 Reports_OnMapIdCall() => 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.{|RASCAL003:Map(x => x)|}; + } + } + """); +} diff --git a/src/Rascal.Analysis.Tests/Analyzers/UseGetValueOrForIdMatchAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/UseGetValueOrForIdMatchAnalyzerTests.cs new file mode 100644 index 0000000..9befec8 --- /dev/null +++ b/src/Rascal.Analysis.Tests/Analyzers/UseGetValueOrForIdMatchAnalyzerTests.cs @@ -0,0 +1,38 @@ +using VerifyCS = Rascal.Analysis.Tests.Verifiers.AnalyzerVerifier; + +namespace Rascal.Analysis.Analyzers.Tests; + +public class UseGetValueOrForIdMatchAnalyzerTests +{ + [Fact] + public async Task DoesNotReport_OnOperation() => 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 Reports_OnId() => 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.{|RASCAL006:Match|}(x => x, _ => 0); + } + } + """); +} diff --git a/src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs new file mode 100644 index 0000000..e779dd0 --- /dev/null +++ b/src/Rascal.Analysis.Tests/Analyzers/UseMapAnalyzerTests.cs @@ -0,0 +1,88 @@ +using VerifyCS = Rascal.Analysis.Tests.Verifiers.AnalyzerVerifier; + +namespace Rascal.Analysis.Analyzers.Tests; + +public class UseMapAnalyzerTests +{ + [Fact] + public async Task DoesNotReport_OnOperation() => 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 Reports_OnIdOk() => 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.{|RASCAL001:Then|}(x => Ok(x)); + } + } + """); + + [Fact] + public async Task Reports_OnExplicitCtor() => 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.{|RASCAL001:Then|}(x => new Result(x)); + } + } + """); + + [Fact] + public async Task Reports_OnImplicitCtor() => 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.{|RASCAL001:Then|}(x => new(x)); + } + } + """); + + [Fact] + public async Task Reports_OnConversion() => 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.{|RASCAL001:Then|}(x => (Result)x); + } + } + """); +} diff --git a/src/Rascal.Analysis.Tests/Analyzers/UseThenAnalyzerTests.cs b/src/Rascal.Analysis.Tests/Analyzers/UseThenAnalyzerTests.cs new file mode 100644 index 0000000..4e52659 --- /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 DoesNotReport_OnMapWithoutUnnest() => 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 Report_OnMapUnnest_ExtensionForm() => 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.{|RASCAL002:Map|}(x => Ok(x)).Unnest(); + } + } + """); + + [Fact] + public async Task Reports_OnMapUnnest_MethodForm() => 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.{|RASCAL002:Map|}(x => Ok(x))); + } + } + """); +} diff --git a/src/Rascal.Analysis.Tests/CodeFixes/RemoveMapIdCallCodeFixTests.cs b/src/Rascal.Analysis.Tests/CodeFixes/RemoveMapIdCallCodeFixTests.cs new file mode 100644 index 0000000..4f853cc --- /dev/null +++ b/src/Rascal.Analysis.Tests/CodeFixes/RemoveMapIdCallCodeFixTests.cs @@ -0,0 +1,35 @@ +using VerifyCS = Rascal.Analysis.Tests.Verifiers.CodeFixVerifier; + +namespace Rascal.Analysis.CodeFixes.Tests; + +public class RemoveMapIdCallCodeFixProviderTests +{ + [Fact] + public async Task Fixes_MapIdCall() => 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.{|RASCAL003: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.Tests/CodeFixes/UseGetValueOrForIdMatchCodeFixTests.cs b/src/Rascal.Analysis.Tests/CodeFixes/UseGetValueOrForIdMatchCodeFixTests.cs new file mode 100644 index 0000000..6c34ee8 --- /dev/null +++ b/src/Rascal.Analysis.Tests/CodeFixes/UseGetValueOrForIdMatchCodeFixTests.cs @@ -0,0 +1,95 @@ +using VerifyCS = Rascal.Analysis.Tests.Verifiers.CodeFixVerifier; + +namespace Rascal.Analysis.CodeFixes.Tests; + +public class UseGetValueOrForIdMatchCodeFixTests +{ + [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.{|RASCAL006: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.{|RASCAL006: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.{|RASCAL006: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.Tests/CodeFixes/UseMapCodeFixTests.cs b/src/Rascal.Analysis.Tests/CodeFixes/UseMapCodeFixTests.cs new file mode 100644 index 0000000..a812987 --- /dev/null +++ b/src/Rascal.Analysis.Tests/CodeFixes/UseMapCodeFixTests.cs @@ -0,0 +1,122 @@ +using VerifyCS = Rascal.Analysis.Tests.Verifiers.CodeFixVerifier; + +namespace Rascal.Analysis.CodeFixes.Tests; + +public class UseMapCodeFixTests +{ + [Fact] + public async Task RemovesMap_Lambda() => 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.{|RASCAL001: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); + } + } + """); + + [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.{|RASCAL001: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.{|RASCAL001: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.{|RASCAL001: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.Tests/CodeFixes/UseThenCodeFixTests.cs b/src/Rascal.Analysis.Tests/CodeFixes/UseThenCodeFixTests.cs new file mode 100644 index 0000000..53b77dc --- /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 Fixes_ExtensionForm() => 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.{|RASCAL002: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 Fixes_CallForm() => 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.{|RASCAL002: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.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; + } +} 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 new file mode 100644 index 0000000..4a0d1a6 --- /dev/null +++ b/src/Rascal.Analysis/Analyzers/ToSusTypeAnalyzer.cs @@ -0,0 +1,72 @@ +using Microsoft.CodeAnalysis.Operations; + +namespace Rascal.Analysis.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ToSusTypeAnalyzer : BaseAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + Diagnostics.ToSameType, + Diagnostics.ToImpossibleType); + + protected override void Handle(CompilationStartAnalysisContext ctx, WellKnownSymbols symbols) => ctx.RegisterOperationAction(operationCtx => + { + var operation = (IInvocationOperation)operationCtx.Operation; + + // 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 + { + Expression: MemberAccessExpressionSyntax + { + Name: GenericNameSyntax + { + TypeArgumentList.Arguments: [var typeSyntax] + } + } + } + ? 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]; + + // 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(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); +} diff --git a/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs new file mode 100644 index 0000000..81ced6e --- /dev/null +++ b/src/Rascal.Analysis/Analyzers/UnnecessaryIdMapAnalyzer.cs @@ -0,0 +1,62 @@ +using Microsoft.CodeAnalysis.Operations; +using Microsoft.CodeAnalysis.Text; + +namespace Rascal.Analysis.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UnnecessaryIdMapAnalyzer : BaseAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + Diagnostics.UnnecessaryIdMap); + + protected override void Handle(CompilationStartAnalysisContext ctx, WellKnownSymbols symbols) => ctx.RegisterOperationAction(operationCtx => + { + var operation = (IInvocationOperation)operationCtx.Operation; + + // Check that it is Map being called. + if (!operation.TargetMethod.OriginalDefinition.Equals(symbols.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 + { + 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. + 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 new file mode 100644 index 0000000..88811a0 --- /dev/null +++ b/src/Rascal.Analysis/Analyzers/UseGetValueOrForIdMatchAnalyzer.cs @@ -0,0 +1,57 @@ +using Microsoft.CodeAnalysis.Operations; + +namespace Rascal.Analysis.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseGetValueOrForIdMatchAnalyzer : BaseAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + Diagnostics.UseGetValueOrForIdMatch); + + protected override void Handle(CompilationStartAnalysisContext ctx, WellKnownSymbols symbols) => ctx.RegisterOperationAction(operationCtx => + { + var operation = (IInvocationOperation)operationCtx.Operation; + + 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 + [ + { + 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. + 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); +} diff --git a/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs new file mode 100644 index 0000000..c94c0ad --- /dev/null +++ b/src/Rascal.Analysis/Analyzers/UseMapAnalyzer.cs @@ -0,0 +1,79 @@ +using Microsoft.CodeAnalysis.Operations; + +namespace Rascal.Analysis.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseMapAnalyzer : BaseAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + Diagnostics.UseMap); + + 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(symbols.ThenMethod, SymbolEqualityComparer.Default)) 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(); + + // Report the diagnostic. + operationCtx.ReportDiagnostic(Diagnostic.Create( + Diagnostics.UseMap, + location)); + }, OperationKind.Invocation); + + private static bool IsTargetOperation( + IInvocationOperation operation, + IMethodSymbol okMethod, + IMethodSymbol okCtor, + IMethodSymbol okConversion) + { + // 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). + 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/Analyzers/UseThenAnalyzer.cs b/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs new file mode 100644 index 0000000..b964df7 --- /dev/null +++ b/src/Rascal.Analysis/Analyzers/UseThenAnalyzer.cs @@ -0,0 +1,45 @@ +using Microsoft.CodeAnalysis.Operations; + +namespace Rascal.Analysis.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseThenAnalyzer : BaseAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + Diagnostics.UseThen); + + protected override void Handle(CompilationStartAnalysisContext ctx, WellKnownSymbols symbols) => ctx.RegisterOperationAction(operationCtx => + { + var operation = (IInvocationOperation)operationCtx.Operation; + + // Check that it is Unnest being called. + if (!operation.TargetMethod.OriginalDefinition.Equals(symbols.UnnestMethod, SymbolEqualityComparer.Default)) + return; + + // Check that the first argument is an invocation. + if (operation.Arguments is not + [ + { + Value: IInvocationOperation argumentInvocation + } + ]) return; + + // Check that the invoked method is Map. + if (!argumentInvocation.TargetMethod.OriginalDefinition + .Equals(symbols.MapMethod, SymbolEqualityComparer.Default)) + return; + + // 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/CodeFixes/RemoveMapIdCallCodeFix.cs b/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFix.cs new file mode 100644 index 0000000..1f3bd70 --- /dev/null +++ b/src/Rascal.Analysis/CodeFixes/RemoveMapIdCallCodeFix.cs @@ -0,0 +1,51 @@ +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; + +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(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 codeAction = CodeAction.Create( + "Remove Map call", + _ => Task.FromResult(ExecuteFix( + document, + root, + invocation, + invocationTarget)), + nameof(RemoveMapIdCallCodeFix)); + + ctx.RegisterCodeFix(codeAction, ctx.Diagnostics); + } + + private static Document ExecuteFix( + Document document, + SyntaxNode root, + InvocationExpressionSyntax invocation, + ExpressionSyntax invocationTarget) + { + var newRoot = root.ReplaceNode(invocation, invocationTarget); + return document.WithSyntaxRoot(newRoot); + } +} diff --git a/src/Rascal.Analysis/CodeFixes/UseGetValueOrForIdMatchCodeFix.cs b/src/Rascal.Analysis/CodeFixes/UseGetValueOrForIdMatchCodeFix.cs new file mode 100644 index 0000000..95716e4 --- /dev/null +++ b/src/Rascal.Analysis/CodeFixes/UseGetValueOrForIdMatchCodeFix.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 UseGetValueOrForIdMatchCodeFix : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create( + Diagnostics.UseGetValueOrForIdMatch.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 GetValueOr", + _ => Task.FromResult(ExecuteFix( + document, + root, + invocationSyntax, + memberAccessSyntax, + lambdaSyntax, + body, + param)), + nameof(UseGetValueOrForIdMatchCodeFix)); + + 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/CodeFixes/UseMapCodeFix.cs b/src/Rascal.Analysis/CodeFixes/UseMapCodeFix.cs new file mode 100644 index 0000000..8f800ee --- /dev/null +++ b/src/Rascal.Analysis/CodeFixes/UseMapCodeFix.cs @@ -0,0 +1,108 @@ +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); + + 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; + } + + 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/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/Diagnostics.cs b/src/Rascal.Analysis/Diagnostics.cs new file mode 100644 index 0000000..b01d84c --- /dev/null +++ b/src/Rascal.Analysis/Diagnostics.cs @@ -0,0 +1,75 @@ +namespace Rascal.Analysis; + +public static class Diagnostics +{ + public static DiagnosticDescriptor UseMap { get; } = new( + "RASCAL001", + "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( + "RASCAL002", + "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."); + + public static DiagnosticDescriptor UnnecessaryIdMap { get; } = new( + "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", + "Correctness", + DiagnosticSeverity.Warning, + 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( + "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.", + "Correctness", + DiagnosticSeverity.Warning, + true, + "Calling 'To' with the same type as that of the result will always succeed."); + + public static DiagnosticDescriptor ToImpossibleType { get; } = new( + "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.", + "Correctness", + DiagnosticSeverity.Warning, + true, + "Calling 'To' with a type which no value of the type of the result permits will always fail."); + + public static DiagnosticDescriptor UseGetValueOrForIdMatch { get; } = new( + "RASCAL006", + "Use 'GetValueOr' instead of 'Match(x => x, ...)'", + "This call matches {0} using an identity function. " + + "Use 'GetValueOr' instead to reduce allocations.", + "Correctness", + 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/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 + + + + diff --git a/src/Rascal.Analysis/SymbolExtensions.cs b/src/Rascal.Analysis/SymbolExtensions.cs new file mode 100644 index 0000000..59134dc --- /dev/null +++ b/src/Rascal.Analysis/SymbolExtensions.cs @@ -0,0 +1,17 @@ +namespace Rascal.Analysis; + +public static class SymbolExtensions +{ + public static bool Inherits(this ITypeSymbol x, ITypeSymbol type) + { + var t = x; + + while (t is not null) + { + if (t.Equals(type, SymbolEqualityComparer.Default)) return true; + t = t.BaseType; + } + + return false; + } +} diff --git a/src/Rascal.Analysis/SyntaxInator.cs b/src/Rascal.Analysis/SyntaxInator.cs new file mode 100644 index 0000000..7aff17c --- /dev/null +++ b/src/Rascal.Analysis/SyntaxInator.cs @@ -0,0 +1,73 @@ +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(); + + public static InvocationExpressionSyntax MapFrom( + ExpressionSyntax invocationTarget, + ExpressionSyntax expression) => + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + invocationTarget, + IdentifierName("Map"))) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument(expression)))) + .NormalizeWhitespace(); + + public static NameSyntax ThenName() => IdentifierName("Then"); +} 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; diff --git a/src/Rascal.Analysis/WellKnownSymbols.cs b/src/Rascal.Analysis/WellKnownSymbols.cs new file mode 100644 index 0000000..738e9c4 --- /dev/null +++ b/src/Rascal.Analysis/WellKnownSymbols.cs @@ -0,0 +1,112 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Rascal.Analysis; + +public sealed record WellKnownSymbols( + INamedTypeSymbol ObjectType, + INamedTypeSymbol Result1Type, + ITypeParameterSymbol TypeParameterT, + INamedTypeSymbol ResultExtensionsType, + INamedTypeSymbol PreludeType, + IMethodSymbol MapMethod, + IMethodSymbol ThenMethod, + IMethodSymbol ToMethod, + 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, + [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); + var preludeType = GetType(PreludeTypeName); + + if (es is not []) + { + symbols = null; + errors = es; + return false; + } + + var typeParameterT = resultType!.TypeParameters[0]; + + 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); + 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 = 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 []) + { + symbols = null; + errors = es; + return false; + } + + symbols = new( + objectType, + resultType!, + typeParameterT!, + resultExtensionsType!, + preludeType!, + mapMethod!, + thenMethod!, + toMethod!, + matchMethod!, + unnestMethod!, + okMethod!, + okCtor!, + okConversion!); + errors = null; + return true; + } + + public readonly record struct Error(string MemberName); +} 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 @@ + 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 + + + + + +