diff --git a/src/.idea/.idea.SaaStack/.idea/inspectionProfiles/Project_Default.xml b/src/.idea/.idea.SaaStack/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..85ed4b44 --- /dev/null +++ b/src/.idea/.idea.SaaStack/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/src/ApiHost1/ApiHost1.csproj b/src/ApiHost1/ApiHost1.csproj index 598312ef..35543e18 100644 --- a/src/ApiHost1/ApiHost1.csproj +++ b/src/ApiHost1/ApiHost1.csproj @@ -11,9 +11,7 @@ - + diff --git a/src/CarsApi/CarsApi.csproj b/src/CarsApi/CarsApi.csproj index 53db2ed1..5fe1d942 100644 --- a/src/CarsApi/CarsApi.csproj +++ b/src/CarsApi/CarsApi.csproj @@ -14,9 +14,7 @@ - + diff --git a/src/Common/Common.csproj b/src/Common/Common.csproj index 65c71b6b..e0c07d50 100644 --- a/src/Common/Common.csproj +++ b/src/Common/Common.csproj @@ -27,10 +27,4 @@ - - - - diff --git a/src/Common/Error.cs b/src/Common/Error.cs index 00714d07..3a4b316b 100644 --- a/src/Common/Error.cs +++ b/src/Common/Error.cs @@ -1,5 +1,8 @@ namespace Common; +/// +/// Defines a error, used for result return values +/// public struct Error { private Error(ErrorCode code, string? message = null) @@ -63,8 +66,13 @@ public static Error Unexpected(string? message = null) } } +/// +/// Defines the common types (codes) of errors that can happen in code at any layer, +/// that return +/// public enum ErrorCode { + // EXTEND: add other kinds of errors you want to support in Result Validation, RuleViolation, RoleViolation, diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 837edadb..c56a378a 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -64,10 +64,10 @@ - Debug - AnyCPU + Debug + AnyCPU - + true full false @@ -77,7 +77,7 @@ 4 - + pdbonly true bin\$(Configuration)\ @@ -86,7 +86,7 @@ 4 - + pdbonly true bin\$(Configuration)\ @@ -95,4 +95,9 @@ 4 + + + + + diff --git a/src/Infrastructure.WebApi.Common.IntegrationTests/Infrastructure.WebApi.Common.IntegrationTests.csproj b/src/Infrastructure.WebApi.Common.IntegrationTests/Infrastructure.WebApi.Common.IntegrationTests.csproj index 503aa153..1ab2fc6b 100644 --- a/src/Infrastructure.WebApi.Common.IntegrationTests/Infrastructure.WebApi.Common.IntegrationTests.csproj +++ b/src/Infrastructure.WebApi.Common.IntegrationTests/Infrastructure.WebApi.Common.IntegrationTests.csproj @@ -2,6 +2,7 @@ net7.0 + true diff --git a/src/SaaStack.sln b/src/SaaStack.sln index 7e680075..6a59c665 100644 --- a/src/SaaStack.sln +++ b/src/SaaStack.sln @@ -7,7 +7,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Build.props = Directory.Build.props GlobalAssemblyInfo.cs = GlobalAssemblyInfo.cs ..\CHANGELOG.md = ..\CHANGELOG.md - ..\README_PROJECT.md = ..\README_PROJECT.md + ..\README_DERIVATIVE.md = ..\README_DERIVATIVE.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{508E7DA4-4DF2-4201-955D-CCF70C41AD05}" @@ -70,7 +70,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application.Interfaces", "A EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationTesting.WebApi.Common", "IntegrationTesting.WebApi.Common\IntegrationTesting.WebApi.Common.csproj", "{A7CA7AD7-70CA-43F0-BE73-75A01342D571}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure.WebApi.Generators", "Infrastructure.WebApi.Generators\Infrastructure.WebApi.Generators.csproj", "{7AB39FD6-660F-4400-9955-B92684378492}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tools.Generators.WebApi", "Tools.Generators.WebApi\Tools.Generators.WebApi.csproj", "{7AB39FD6-660F-4400-9955-B92684378492}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{19ADDB2F-B589-49EF-9BDA-BD9908057D60}" EndProject @@ -108,6 +108,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application.Persistence.Int EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarsInfrastructure", "CarsInfrastructure\CarsInfrastructure.csproj", "{ED71C769-CDA7-4C58-B252-8218DCE3D2B5}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{BAE0D6F2-6920-4B02-9F30-D71B04B7170D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tools.Analyzers.Core", "Tools.Analyzers.Core\Tools.Analyzers.Core.csproj", "{DE31F486-AE81-49C0-BA00-3A6A325B7C42}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{A25A3BA8-5602-4825-9595-2CF96B166920}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tools.Analyzers.Core.UnitTests", "Tools.Analyzers.Core.UnitTests\Tools.Analyzers.Core.UnitTests.csproj", "{3E6AA34C-02F9-4B8B-8307-FC9CA25DB7AD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -271,6 +279,18 @@ Global {ED71C769-CDA7-4C58-B252-8218DCE3D2B5}.Release|Any CPU.Build.0 = Release|Any CPU {ED71C769-CDA7-4C58-B252-8218DCE3D2B5}.ReleaseForDeploy|Any CPU.ActiveCfg = ReleaseForDeploy|Any CPU {ED71C769-CDA7-4C58-B252-8218DCE3D2B5}.ReleaseForDeploy|Any CPU.Build.0 = ReleaseForDeploy|Any CPU + {DE31F486-AE81-49C0-BA00-3A6A325B7C42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE31F486-AE81-49C0-BA00-3A6A325B7C42}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE31F486-AE81-49C0-BA00-3A6A325B7C42}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE31F486-AE81-49C0-BA00-3A6A325B7C42}.Release|Any CPU.Build.0 = Release|Any CPU + {DE31F486-AE81-49C0-BA00-3A6A325B7C42}.ReleaseForDeploy|Any CPU.ActiveCfg = ReleaseForDeploy|Any CPU + {DE31F486-AE81-49C0-BA00-3A6A325B7C42}.ReleaseForDeploy|Any CPU.Build.0 = ReleaseForDeploy|Any CPU + {3E6AA34C-02F9-4B8B-8307-FC9CA25DB7AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3E6AA34C-02F9-4B8B-8307-FC9CA25DB7AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3E6AA34C-02F9-4B8B-8307-FC9CA25DB7AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3E6AA34C-02F9-4B8B-8307-FC9CA25DB7AD}.Release|Any CPU.Build.0 = Release|Any CPU + {3E6AA34C-02F9-4B8B-8307-FC9CA25DB7AD}.ReleaseForDeploy|Any CPU.ActiveCfg = ReleaseForDeploy|Any CPU + {3E6AA34C-02F9-4B8B-8307-FC9CA25DB7AD}.ReleaseForDeploy|Any CPU.Build.0 = ReleaseForDeploy|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {F5C77A86-38AF-40E4-82FC-617E624B2754} = {508E7DA4-4DF2-4201-955D-CCF70C41AD05} @@ -299,7 +319,6 @@ Global {B76CF102-CE56-4321-9060-F81E63B982D6} = {57FDFB31-D6B6-4369-A78C-6F3D3AEA0D79} {23FF9513-1B26-41F4-A7FE-1D8A9F0808AE} = {BA1AEAEC-68CD-4855-A8CB-0DC2070B6A8C} {A7CA7AD7-70CA-43F0-BE73-75A01342D571} = {5838EE94-374F-4A6F-A231-1BC1C87985F4} - {7AB39FD6-660F-4400-9955-B92684378492} = {B68592DF-E8E8-452A-A46F-5C8ECB178FDF} {19ADDB2F-B589-49EF-9BDA-BD9908057D60} = {B68592DF-E8E8-452A-A46F-5C8ECB178FDF} {4CF7C7E2-C95D-4440-9ECF-5D1CE2A46D7A} = {19ADDB2F-B589-49EF-9BDA-BD9908057D60} {AE57212B-9A30-4577-A795-7B411621BCDA} = {19ADDB2F-B589-49EF-9BDA-BD9908057D60} @@ -319,5 +338,11 @@ Global {23A93A15-21B8-4CA5-B128-1FF4B0C6A861} = {57FDFB31-D6B6-4369-A78C-6F3D3AEA0D79} {F6C71F8B-AFDE-471A-B1DE-6E1E8F3499C6} = {BA1AEAEC-68CD-4855-A8CB-0DC2070B6A8C} {ED71C769-CDA7-4C58-B252-8218DCE3D2B5} = {57FDFB31-D6B6-4369-A78C-6F3D3AEA0D79} + {BAE0D6F2-6920-4B02-9F30-D71B04B7170D} = {9270A12C-E16F-4932-89C4-F4ADDDA55AF3} + {DE31F486-AE81-49C0-BA00-3A6A325B7C42} = {BAE0D6F2-6920-4B02-9F30-D71B04B7170D} + {A25A3BA8-5602-4825-9595-2CF96B166920} = {BAE0D6F2-6920-4B02-9F30-D71B04B7170D} + {3E6AA34C-02F9-4B8B-8307-FC9CA25DB7AD} = {A25A3BA8-5602-4825-9595-2CF96B166920} + {33E2D4C7-525A-41CE-858C-F6A944160618} = {864DED88-9252-46EB-9D13-00269C7333F9} + {7AB39FD6-660F-4400-9955-B92684378492} = {BAE0D6F2-6920-4B02-9F30-D71B04B7170D} EndGlobalSection EndGlobal diff --git a/src/SaaStack.sln.DotSettings b/src/SaaStack.sln.DotSettings index dd0677b7..097468e4 100644 --- a/src/SaaStack.sln.DotSettings +++ b/src/SaaStack.sln.DotSettings @@ -1,4 +1,8 @@  + ExplicitlyExcluded + ExplicitlyExcluded + + WARNING WARNING WARNING diff --git a/src/Tools.Analyzers.Core.UnitTests/MissingDocsAnalyzerSpec.cs b/src/Tools.Analyzers.Core.UnitTests/MissingDocsAnalyzerSpec.cs new file mode 100644 index 00000000..4ad30fdb --- /dev/null +++ b/src/Tools.Analyzers.Core.UnitTests/MissingDocsAnalyzerSpec.cs @@ -0,0 +1,502 @@ +using JetBrains.Annotations; +using Xunit; + +namespace Tools.Analyzers.Core.UnitTests; + +[UsedImplicitly] +public class MissingDocsAnalyzerSpec +{ + [Trait("Category", "Unit")] + public class GivenAType + { + [Fact] + public async Task WhenInJetbrainsAnnotationsNamespace_ThenNoAlert() + { + const string input = @"namespace JetBrains.Annotations; +public class AClass +{ +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenInApiHost1Namespace_ThenNoAlert() + { + const string input = @"namespace ApiHost1; +public class AClass +{ +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenPublicDelegate_ThenAlerts() + { + const string input = @"public delegate void ADelegate();"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.TypeDiagnosticId, input, 1, 22, "ADelegate"); + } + + [Fact] + public async Task WhenInternalDelegate_ThenAlerts() + { + const string input = @"internal delegate void ADelegate();"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.TypeDiagnosticId, input, 1, 24, "ADelegate"); + } + + [Fact] + public async Task WhenPublicInterface_ThenAlerts() + { + const string input = @"public interface AnInterface +{ +}"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.TypeDiagnosticId, input, 1, 18, "AnInterface"); + } + + [Fact] + public async Task WhenInternalInterface_ThenAlerts() + { + const string input = @"internal interface AnInterface +{ +}"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.TypeDiagnosticId, input, 1, 20, "AnInterface"); + } + + [Fact] + public async Task WhenPublicEnum_ThenAlerts() + { + const string input = @"public enum AnEnum +{ +}"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.TypeDiagnosticId, input, 1, 13, "AnEnum"); + } + + [Fact] + public async Task WhenInternalEnum_ThenAlerts() + { + const string input = @"internal enum AnEnum +{ +}"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.TypeDiagnosticId, input, 1, 15, "AnEnum"); + } + + [Fact] + public async Task WhenPublicStruct_ThenAlerts() + { + const string input = @"public struct AStruct +{ +}"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.TypeDiagnosticId, input, 1, 15, "AStruct"); + } + + [Fact] + public async Task WhenInternalStruct_ThenAlerts() + { + const string input = @"internal struct AStruct +{ +}"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.TypeDiagnosticId, input, 1, 17, "AStruct"); + } + + [Fact] + public async Task WhenPublicReadOnlyStruct_ThenAlerts() + { + const string input = @"public readonly struct AStruct +{ +}"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.TypeDiagnosticId, input, 1, 24, "AStruct"); + } + + [Fact] + public async Task WhenInternalReadOnlyStruct_ThenAlerts() + { + const string input = @"internal readonly struct AStruct +{ +}"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.TypeDiagnosticId, input, 1, 26, "AStruct"); + } + + [Fact] + public async Task WhenPublicRecord_ThenAlerts() + { + const string input = @"public record ARecord() +{ +}"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.TypeDiagnosticId, input, 1, 15, "ARecord"); + } + + [Fact] + public async Task WhenInternalRecord_ThenAlerts() + { + const string input = @"internal record ARecord +{ +}"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.TypeDiagnosticId, input, 1, 17, "ARecord"); + } + + [Fact] + public async Task WhenPublicStaticClass_ThenNoAlert() + { + const string input = @"public static class AClass +{ +} +"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenInternalStaticClass_ThenNoAlert() + { + const string input = @"internal static class AClass +{ +} +"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenNestedPublicStaticClass_ThenNoAlert() + { + const string input = @"public static class AClass1 +{ + public static class AClass2 + { + } +} +"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenNestedPrivateStaticClass_ThenNoAlert() + { + const string input = @" +/// +/// avalue +/// +public static class AClass1 +{ + private static class AClass2 + { + } +} +"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenNestedPublicInstanceClass_ThenAlerts() + { + const string input = @" +/// +/// avalue +/// +public class AClass1 +{ + public class AClass2 + { + } +} +"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.TypeDiagnosticId, input, 7, 18, "AClass2"); + } + + [Fact] + public async Task WhenNestedPrivateInstanceClass_ThenNoAlert() + { + const string input = @" +/// +/// avalue +/// +public class AClass1 +{ + private class AClass2 + { + } +} +"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenPublicClassNoSummary_ThenAlerts() + { + const string input = @"public class AClass +{ +} +"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.TypeDiagnosticId, input, 1, 14); + } + + [Fact] + public async Task WhenInternalClassNoSummary_ThenAlerts() + { + const string input = @"internal class AClass +{ +} +"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.TypeDiagnosticId, input, 1, 16); + } + + [Fact] + public async Task WhenPublicClassHasEmptyLine_ThenAlerts() + { + const string input = @" +public class AClass +{ +} +"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.TypeDiagnosticId, input, 2, 14); + } + + [Fact] + public async Task WhenPublicClassHasBlankLine_ThenAlerts() + { + const string input = @" + +public class AClass +{ +} +"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.TypeDiagnosticId, input, 3, 14); + } + + [Fact] + public async Task WhenPublicClassHasEmptyOtherTag_ThenAlerts() + { + const string input = @" +/// +/// +/// +public class AClass +{ +} +"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.TypeDiagnosticId, input, 5, 14); + } + + [Fact] + public async Task WhenPublicClassHasSomeOtherTag_ThenAlerts() + { + const string input = @" +/// +/// avalue +/// +public class AClass +{ +} +"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.TypeDiagnosticId, input, 5, 14); + } + + [Fact] + public async Task WhenPublicClassHasEmptySummary_ThenAlerts() + { + const string input = @" +/// +/// +/// +public class AClass +{ +} +"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.TypeDiagnosticId, input, 5, 14); + } + + [Fact] + public async Task WhenPublicClassHasWhitespaceSummary_ThenAlerts() + { + const string input = @" +/// +/// +/// +public class AClass +{ +} +"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.TypeDiagnosticId, input, 5, 14); + } + + [Fact] + public async Task WhenPublicClassHasASummary_ThenNoAlert() + { + const string input = @" +/// +/// avalue +/// +public class AClass +{ +} +"; + + await Verify.NoDiagnosticExists(input); + } + } + + [Trait("Category", "Unit")] + public class GivenAMethod + { + [Fact] + public async Task WhenInJetbrainsAnnotationsNamespace_ThenNoAlert() + { + const string input = @"namespace JetBrains.Annotations; +public static class AClass +{ + public static void AMethod(){} +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenInApiHost1Namespace_ThenNoAlert() + { + const string input = @"namespace ApiHost1; +public static class AClass +{ + public static void AMethod(){} +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenInsideInternalStaticClass_ThenNoAlert() + { + const string input = @"internal static class AClass +{ + public static void AMethod1(){} + public static void AMethod2(this string value){} +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenPublicStaticMethod_ThenAlerts() + { + const string input = @"public static class AClass +{ + public static void AMethod(){} +} +"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.ExtensionMethodDiagnosticId, input, 3, 24, "AMethod"); + } + + [Fact] + public async Task WhenInternalStaticMethod_ThenAlerts() + { + const string input = @"public static class AClass +{ + internal static void AMethod(){} +} +"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.ExtensionMethodDiagnosticId, input, 3, 26, "AMethod"); + } + + [Fact] + public async Task WhenPublicStaticMethodWithParams_ThenAlerts() + { + const string input = @"public static class AClass +{ + public static void AMethod(string value){} +} +"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.ExtensionMethodDiagnosticId, input, 3, 24, "AMethod"); + } + + [Fact] + public async Task WhenInternalStaticMethodWithParams_ThenAlerts() + { + const string input = @"public static class AClass +{ + internal static void AMethod(string value){} +} +"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.ExtensionMethodDiagnosticId, input, 3, 26, "AMethod"); + } + + [Fact] + public async Task WhenInternalExtension_ThenAlerts() + { + const string input = @"public static class AClass +{ + internal static void AMethod(this string value){} +} +"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.ExtensionMethodDiagnosticId, input, 3, 26, "AMethod"); + } + + [Fact] + public async Task WhenPrivateExtension_ThenNoAlerts() + { + const string input = @"public static class AClass +{ + private static void AMethod(this string value){} +} +"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenPublicExtensionHasNoSummary_ThenAlerts() + { + const string input = @"public static class AClass +{ + public static void AMethod(this string value){} +} +"; + + await Verify.DiagnosticExists(MissingDocsAnalyzer.ExtensionMethodDiagnosticId, input, 3, 24, "AMethod"); + } + + [Fact] + public async Task WhenPublicExtensionHasASummary_ThenNoAlert() + { + const string input = @" +public static class AClass +{ + /// + /// avalue + /// + private static void AMethod(this string value){} +} +"; + + await Verify.NoDiagnosticExists(input); + } + } +} \ No newline at end of file diff --git a/src/Tools.Analyzers.Core.UnitTests/Tools.Analyzers.Core.UnitTests.csproj b/src/Tools.Analyzers.Core.UnitTests/Tools.Analyzers.Core.UnitTests.csproj new file mode 100644 index 00000000..71ed50e6 --- /dev/null +++ b/src/Tools.Analyzers.Core.UnitTests/Tools.Analyzers.Core.UnitTests.csproj @@ -0,0 +1,24 @@ + + + + net7.0 + true + + + + + + + + + + + + + + + + + + + diff --git a/src/Tools.Analyzers.Core.UnitTests/Verify.cs b/src/Tools.Analyzers.Core.UnitTests/Verify.cs new file mode 100644 index 00000000..cecf0784 --- /dev/null +++ b/src/Tools.Analyzers.Core.UnitTests/Verify.cs @@ -0,0 +1,21 @@ +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; + +namespace Tools.Analyzers.Core.UnitTests; + +public static class Verify +{ + public static async Task DiagnosticExists(string diagnosticId, string inputSnippet, int locationX, int locationY, + string argument = "AClass") + { + var expected = CSharpAnalyzerVerifier.Diagnostic(diagnosticId) + .WithLocation(locationX, locationY) + .WithArguments(argument); + await CSharpAnalyzerVerifier.VerifyAnalyzerAsync(inputSnippet, expected); + } + + public static async Task NoDiagnosticExists(string inputSnippet) + { + await CSharpAnalyzerVerifier.VerifyAnalyzerAsync(inputSnippet); + } +} \ No newline at end of file diff --git a/src/Tools.Analyzers.Core/AnalyzerReleases.Shipped.md b/src/Tools.Analyzers.Core/AnalyzerReleases.Shipped.md new file mode 100644 index 00000000..c4493a49 --- /dev/null +++ b/src/Tools.Analyzers.Core/AnalyzerReleases.Shipped.md @@ -0,0 +1,8 @@ +## Release 1.0 + +### New Rules + + Rule ID | Category | Severity | Notes +---------|---------------|----------|----------------------------------------------- + SAS001 | Documentation | Warning | The class must have documentation. + SAS002 | Documentation | Warning | The extension method must have documentation. \ No newline at end of file diff --git a/src/Tools.Analyzers.Core/AnalyzerReleases.Unshipped.md b/src/Tools.Analyzers.Core/AnalyzerReleases.Unshipped.md new file mode 100644 index 00000000..dd633832 --- /dev/null +++ b/src/Tools.Analyzers.Core/AnalyzerReleases.Unshipped.md @@ -0,0 +1,4 @@ +### New Rules + + Rule ID | Category | Severity | Notes +---------|----------|----------|------- \ No newline at end of file diff --git a/src/Tools.Analyzers.Core/MissingDocsAnalyzer.cs b/src/Tools.Analyzers.Core/MissingDocsAnalyzer.cs new file mode 100644 index 00000000..e230b167 --- /dev/null +++ b/src/Tools.Analyzers.Core/MissingDocsAnalyzer.cs @@ -0,0 +1,376 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Tools.Analyzers.Core; + +/// +/// An analyzer to find public declarations that are missing a documentation <summary> node. +/// Document declarations are only enforced for Core common/interfaces projects. +/// All public/internal classes, structs, records, interfaces, delegates and enums +/// All public/internal static methods and all public/internal extension methods (in public types) +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class MissingDocsAnalyzer : DiagnosticAnalyzer +{ + public const string ExtensionMethodDiagnosticId = "SAS002"; + private const string RoslynCategory = "Documentation"; + private const string SummaryXmlElementName = "summary"; + public const string TypeDiagnosticId = "SAS001"; + + private static readonly string[] IncludedNamespaces = + { +#if TESTINGONLY + "", +#endif + "Common", "UnitTesting.Common", "IntegrationTesting.Common", + "Infrastructure.Common", "Infrastructure.Interfaces", + "Infrastructure.Persistence.Common", "Infrastructure.Persistence.Interfaces", + "Infrastructure.WebApi.Common", "Infrastructure.WebApi.Interfaces", + "Domain.Common", "Domain.Interfaces", "Application.Common", "Application.Interfaces" + }; + + private static readonly DiagnosticDescriptor TypeRule = new(TypeDiagnosticId, + GetResource(nameof(Resources.SAS001Title)), GetResource(nameof(Resources.SAS001MessageFormat)), RoslynCategory, + DiagnosticSeverity.Warning, true, GetResource(nameof(Resources.SAS001Description))); + + private static readonly DiagnosticDescriptor ExtensionMethodRule = new(ExtensionMethodDiagnosticId, + GetResource(nameof(Resources.SAS002Title)), GetResource(nameof(Resources.SAS002MessageFormat)), RoslynCategory, + DiagnosticSeverity.Warning, true, GetResource(nameof(Resources.SAS002Description))); + + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create(TypeRule, ExtensionMethodRule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeType, SyntaxKind.StructDeclaration, SyntaxKind.ClassDeclaration, + SyntaxKind.InterfaceDeclaration, SyntaxKind.RecordDeclaration, SyntaxKind.DelegateDeclaration, + SyntaxKind.EnumDeclaration); + context.RegisterSyntaxNodeAction(AnalyzeMethod, SyntaxKind.MethodDeclaration); + } + + private static void AnalyzeType(SyntaxNodeAnalysisContext context) + { + var typeSyntax = context.Node; + if (typeSyntax is not MemberDeclarationSyntax typeDeclarationSyntax) //class, struct, interface, record + { + return; + } + + if (IsIgnoredNamespace(context)) + { + return; + } + + if (IsNotPublicNorInternalInstanceType(typeDeclarationSyntax)) + { + return; + } + + if (IsNestedAndNotPublicType(typeDeclarationSyntax)) + { + return; + } + + var docs = typeDeclarationSyntax.GetDocumentationCommentTriviaSyntax(); + if (docs is null) + { + ReportDiagnostic(context, typeDeclarationSyntax); + return; + } + + if (!IsXmlDocsForCSharp(docs)) + { + ReportDiagnostic(context, typeDeclarationSyntax); + return; + } + + var xmlContent = docs.Content; + var summary = xmlContent.GetFirstXmlElement(SummaryXmlElementName); + if (summary is null) + { + ReportDiagnostic(context, typeDeclarationSyntax); + return; + } + + if (IsEmptyNode(summary)) + { + ReportDiagnostic(context, typeDeclarationSyntax); + } + } + + private static void AnalyzeMethod(SyntaxNodeAnalysisContext context) + { + var methodSyntax = context.Node; + if (methodSyntax is not MethodDeclarationSyntax methodDeclarationSyntax) + { + return; + } + + if (IsIgnoredNamespace(context)) + { + return; + } + + if (IsParentTypeNotPublic(methodDeclarationSyntax)) + { + return; + } + + if (IsNotPublicOrInternalStaticMethod(methodDeclarationSyntax)) + { + return; + } + + var docs = methodDeclarationSyntax.GetDocumentationCommentTriviaSyntax(); + if (docs is null) + { + ReportDiagnostic(context, methodDeclarationSyntax); + return; + } + + if (!IsXmlDocsForCSharp(docs)) + { + ReportDiagnostic(context, methodDeclarationSyntax); + return; + } + + var xmlContent = docs.Content; + var summary = xmlContent.GetFirstXmlElement(SummaryXmlElementName); + if (summary is null) + { + ReportDiagnostic(context, methodDeclarationSyntax); + return; + } + + if (IsEmptyNode(summary)) + { + ReportDiagnostic(context, methodDeclarationSyntax); + } + } + + private static LocalizableResourceString GetResource(string name) + { + return new LocalizableResourceString(name, Resources.ResourceManager, typeof(Resources)); + } + + private static void ReportDiagnostic(SyntaxNodeAnalysisContext context, + MemberDeclarationSyntax memberDeclarationSyntax) + { + var location = Location.None; + var text = "Unknown"; + + if (memberDeclarationSyntax is BaseTypeDeclarationSyntax baseType) + { + location = baseType.Identifier.GetLocation(); + text = baseType.Identifier.Text; + } + + if (memberDeclarationSyntax is DelegateDeclarationSyntax delegateType) + { + location = delegateType.Identifier.GetLocation(); + text = delegateType.Identifier.Text; + } + + var diagnostic = Diagnostic.Create(TypeRule, location, text); + context.ReportDiagnostic(diagnostic); + } + + private static void ReportDiagnostic(SyntaxNodeAnalysisContext context, + MethodDeclarationSyntax methodDeclarationSyntax) + { + var identifier = methodDeclarationSyntax.Identifier; + var diagnostic = Diagnostic.Create(ExtensionMethodRule, identifier.GetLocation(), identifier.Text); + context.ReportDiagnostic(diagnostic); + } + + private static bool IsEmptyNode(XmlNodeSyntax nodeSyntax) + { + if (nodeSyntax is XmlTextSyntax textSyntax) + { + return textSyntax.TextTokens.All(token => string.IsNullOrWhiteSpace(token.ToString())); + } + + if (nodeSyntax is XmlElementSyntax xmlElementSyntax) + { + var content = xmlElementSyntax.Content; + return content.All(IsEmptyNode); + } + + return true; + } + + private static bool IsIgnoredNamespace(SyntaxNodeAnalysisContext context) + { + var parentContext = context.ContainingSymbol; + if (parentContext is null) + { + return true; + } + + var containingNamespace = parentContext.ContainingNamespace.ToDisplayString(); + var included = IncludedNamespaces.Contains(containingNamespace); + + return !included; + } + + private static bool IsNotPublicNorInternalInstanceType(MemberDeclarationSyntax memberDeclaration) + { + var accessibility = new Accessibility(memberDeclaration.Modifiers); + if (accessibility is { IsPublic: false, IsInternal: false }) + { + return true; + } + + if (accessibility.IsStatic) + { + return true; + } + + return false; + } + + private static bool IsNestedAndNotPublicType(MemberDeclarationSyntax memberDeclaration) + { + var isNested = memberDeclaration.Parent.IsKind(SyntaxKind.ClassDeclaration); + if (!isNested) + { + return false; + } + + var accessibility = new Accessibility(memberDeclaration.Modifiers); + if (accessibility.IsPublic) + { + return false; + } + + return true; + } + + private static bool IsParentTypeNotPublic(MemberDeclarationSyntax memberDeclaration) + { + var parent = memberDeclaration.Parent; + if (parent is not BaseTypeDeclarationSyntax typeDeclaration) + { + return false; + } + + var accessibility = new Accessibility(typeDeclaration.Modifiers); + if (accessibility.IsPublic) + { + return false; + } + + return true; + } + + private static bool IsNotPublicOrInternalStaticMethod(MethodDeclarationSyntax methodDeclarationSyntax) + { + var accessibility = new Accessibility(methodDeclarationSyntax.Modifiers); + if (accessibility is { IsPublic: false, IsInternal: false }) + { + return true; + } + + if (!accessibility.IsStatic) + { + return true; + } + + return false; + } + + private static bool IsPublicOrInternalExtensionMethod(MethodDeclarationSyntax methodDeclarationSyntax) + { + var isNotPublicOrInternal = IsNotPublicOrInternalStaticMethod(methodDeclarationSyntax); + if (isNotPublicOrInternal) + { + return false; + } + + var firstParameter = methodDeclarationSyntax.ParameterList.Parameters.FirstOrDefault(); + if (firstParameter is null) + { + return false; + } + + var isExtension = firstParameter.Modifiers.Any(mod => mod.IsKind(SyntaxKind.ThisKeyword)); + if (!isExtension) + { + return false; + } + + return true; + } + + private static bool IsXmlDocsForCSharp(SyntaxNode docs) + { + return docs.Language == "C#"; + } +} + +internal static class SyntaxExtensions +{ + public static DocumentationCommentTriviaSyntax? GetDocumentationCommentTriviaSyntax(this SyntaxNode node) + { + foreach (var leadingTrivia in node.GetLeadingTrivia()) + { + if (leadingTrivia.GetStructure() is DocumentationCommentTriviaSyntax structure) + { + return structure; + } + } + + return null; + } + + public static XmlNodeSyntax? GetFirstXmlElement(this SyntaxList content, string elementName) + { + return content.GetXmlElements(elementName) + .FirstOrDefault(); + } + + private static IEnumerable GetXmlElements(this SyntaxList content, string elementName) + { + foreach (var syntax in content) + { + if (syntax is XmlEmptyElementSyntax emptyElement) + { + if (string.Equals(elementName, emptyElement.Name.ToString(), StringComparison.Ordinal)) + { + yield return emptyElement; + } + + continue; + } + + if (syntax is XmlElementSyntax elementSyntax) + { + if (string.Equals(elementName, elementSyntax.StartTag?.Name?.ToString(), StringComparison.Ordinal)) + { + yield return elementSyntax; + } + } + } + } +} + +public class Accessibility +{ + public Accessibility(SyntaxTokenList modifiers) + { + IsPublic = modifiers.Any(mod => mod.IsKind(SyntaxKind.PublicKeyword)); + IsInternal = modifiers.Any(mod => mod.IsKind(SyntaxKind.InternalKeyword)); + IsStatic = modifiers.Any(mod => mod.IsKind(SyntaxKind.StaticKeyword)); + } + + public bool IsInternal { get; } + + public bool IsPublic { get; } + + public bool IsStatic { get; } +} \ No newline at end of file diff --git a/src/Tools.Analyzers.Core/Properties/launchSettings.json b/src/Tools.Analyzers.Core/Properties/launchSettings.json new file mode 100644 index 00000000..78c2ecd4 --- /dev/null +++ b/src/Tools.Analyzers.Core/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "CodingStandards-Analyzers-Development": { + "commandName": "DebugRoslynComponent", + "targetProject": "../ApiHost1/ApiHost1.csproj" + } + } +} \ No newline at end of file diff --git a/src/Tools.Analyzers.Core/Resources.Designer.cs b/src/Tools.Analyzers.Core/Resources.Designer.cs new file mode 100644 index 00000000..489d1daa --- /dev/null +++ b/src/Tools.Analyzers.Core/Resources.Designer.cs @@ -0,0 +1,116 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Tools.Analyzers.Core { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Tools.Analyzers.Core.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to This type should have a <summary> to describe what it designed to do.. + /// + internal static string SAS001Description { + get { + return ResourceManager.GetString("SAS001Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type '{0}' requires a documentation <summary/>. + /// + internal static string SAS001MessageFormat { + get { + return ResourceManager.GetString("SAS001MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Missing documentation. + /// + internal static string SAS001Title { + get { + return ResourceManager.GetString("SAS001Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This method should have a <summary> to describe what it designed to do.. + /// + internal static string SAS002Description { + get { + return ResourceManager.GetString("SAS002Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Extension method '{0}' requires a documentation <summary/>. + /// + internal static string SAS002MessageFormat { + get { + return ResourceManager.GetString("SAS002MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Missing documentation. + /// + internal static string SAS002Title { + get { + return ResourceManager.GetString("SAS002Title", resourceCulture); + } + } + } +} diff --git a/src/Tools.Analyzers.Core/Resources.resx b/src/Tools.Analyzers.Core/Resources.resx new file mode 100644 index 00000000..7b42a4fc --- /dev/null +++ b/src/Tools.Analyzers.Core/Resources.resx @@ -0,0 +1,45 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + This type should have a <summary> to describe what it designed to do. + + + Type '{0}' requires a documentation <summary/> + + + Missing documentation + + + This method should have a <summary> to describe what it designed to do. + + + Extension method '{0}' requires a documentation <summary/> + + + Missing documentation + + \ No newline at end of file diff --git a/src/Tools.Analyzers.Core/Tools.Analyzers.Core.csproj b/src/Tools.Analyzers.Core/Tools.Analyzers.Core.csproj new file mode 100644 index 00000000..9723b41f --- /dev/null +++ b/src/Tools.Analyzers.Core/Tools.Analyzers.Core.csproj @@ -0,0 +1,42 @@ + + + + .net7.0 + true + true + + + + RS2007 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + True + True + Resources.resx + + + + + + + + + diff --git a/src/Infrastructure.WebApi.Generators/Extensions/StringExtensions.cs b/src/Tools.Generators.WebApi/Extensions/StringExtensions.cs similarity index 91% rename from src/Infrastructure.WebApi.Generators/Extensions/StringExtensions.cs rename to src/Tools.Generators.WebApi/Extensions/StringExtensions.cs index e65169db..6d403682 100644 --- a/src/Infrastructure.WebApi.Generators/Extensions/StringExtensions.cs +++ b/src/Tools.Generators.WebApi/Extensions/StringExtensions.cs @@ -1,4 +1,4 @@ -namespace Infrastructure.WebApi.Generators.Extensions; +namespace Tools.Generators.WebApi.Extensions; public static class StringExtensions { diff --git a/src/Infrastructure.WebApi.Generators/MinimalApiMediatRGenerator.cs b/src/Tools.Generators.WebApi/MinimalApiMediatRGenerator.cs similarity index 97% rename from src/Infrastructure.WebApi.Generators/MinimalApiMediatRGenerator.cs rename to src/Tools.Generators.WebApi/MinimalApiMediatRGenerator.cs index ea209b26..bb5754cf 100644 --- a/src/Infrastructure.WebApi.Generators/MinimalApiMediatRGenerator.cs +++ b/src/Tools.Generators.WebApi/MinimalApiMediatRGenerator.cs @@ -1,11 +1,15 @@ using System.Text; -using Infrastructure.WebApi.Generators.Extensions; using Infrastructure.WebApi.Interfaces; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; +using Tools.Generators.WebApi.Extensions; -namespace Infrastructure.WebApi.Generators; +namespace Tools.Generators.WebApi; +/// +/// A source generators for converting to +/// Minimal API registrations and MediatR handlers +/// [Generator] public class MinimalApiMediatRGenerator : ISourceGenerator { diff --git a/src/Infrastructure.WebApi.Generators/README.md b/src/Tools.Generators.WebApi/README.md similarity index 100% rename from src/Infrastructure.WebApi.Generators/README.md rename to src/Tools.Generators.WebApi/README.md diff --git a/src/Infrastructure.WebApi.Generators/Infrastructure.WebApi.Generators.csproj b/src/Tools.Generators.WebApi/Tools.Generators.WebApi.csproj similarity index 100% rename from src/Infrastructure.WebApi.Generators/Infrastructure.WebApi.Generators.csproj rename to src/Tools.Generators.WebApi/Tools.Generators.WebApi.csproj index 2a71650d..e77bea4c 100644 --- a/src/Infrastructure.WebApi.Generators/Infrastructure.WebApi.Generators.csproj +++ b/src/Tools.Generators.WebApi/Tools.Generators.WebApi.csproj @@ -2,8 +2,8 @@ net7.0 - true true + true diff --git a/src/Infrastructure.WebApi.Generators/WebApiAssemblyVisitor.cs b/src/Tools.Generators.WebApi/WebApiAssemblyVisitor.cs similarity index 99% rename from src/Infrastructure.WebApi.Generators/WebApiAssemblyVisitor.cs rename to src/Tools.Generators.WebApi/WebApiAssemblyVisitor.cs index 7d6bc4e4..b4190b16 100644 --- a/src/Infrastructure.WebApi.Generators/WebApiAssemblyVisitor.cs +++ b/src/Tools.Generators.WebApi/WebApiAssemblyVisitor.cs @@ -1,9 +1,9 @@ -using Infrastructure.WebApi.Generators.Extensions; using Infrastructure.WebApi.Interfaces; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Tools.Generators.WebApi.Extensions; -namespace Infrastructure.WebApi.Generators; +namespace Tools.Generators.WebApi; /// /// Visits all namespaces, and types in the current assembly (only),