diff --git a/Stackworx.Analyzers.Tests/DiagnosticHelpLinksTests.cs b/Stackworx.Analyzers.Tests/DiagnosticHelpLinksTests.cs new file mode 100644 index 0000000..c9e1e1f --- /dev/null +++ b/Stackworx.Analyzers.Tests/DiagnosticHelpLinksTests.cs @@ -0,0 +1,53 @@ +namespace Stackworx.Analyzers.Tests; + +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis.Diagnostics; +using Xunit; + +public class DiagnosticHelpLinksTests +{ + [Fact] + public void AllSupportedDiagnostics_HaveExpectedDocumentationLinks() + { + var expectedLinks = new Dictionary + { + ["SW001"] = "https://stackworx-dotnet.github.io/Stackworx.Analyzers/docs/rules/sw001-avoid-implicit-datetime-to-datetimeoffset", + ["SW002"] = "https://stackworx-dotnet.github.io/Stackworx.Analyzers/docs/rules/sw002-unused-method", + ["SW101"] = "https://stackworx-dotnet.github.io/Stackworx.Analyzers/docs/rules/sw101-forbidden-namespace-reference", + ["SW102"] = "https://stackworx-dotnet.github.io/Stackworx.Analyzers/docs/rules/sw102-forbidden-namespace-using", + ["SW103"] = "https://stackworx-dotnet.github.io/Stackworx.Analyzers/docs/rules/sw103-avoid-microsoft-extensions-azure", + ["SWGQL01"] = "https://stackworx-dotnet.github.io/Stackworx.Analyzers/docs/rules/swgql01-static-extension-method-validation", + ["SWGQL02"] = "https://stackworx-dotnet.github.io/Stackworx.Analyzers/docs/rules/swgql02-unused-dataloader-interface", + ["SWGQL03"] = "https://stackworx-dotnet.github.io/Stackworx.Analyzers/docs/rules/swgql03-graphql-extension-class-static", + ["SWGQL04"] = "https://stackworx-dotnet.github.io/Stackworx.Analyzers/docs/rules/swgql04-graphql-duplicate-extension-field", + ["SWGQL05"] = "https://stackworx-dotnet.github.io/Stackworx.Analyzers/docs/rules/swgql05-06-hotchocolate-types-usedimplicitly", + ["SWGQL06"] = "https://stackworx-dotnet.github.io/Stackworx.Analyzers/docs/rules/swgql05-06-hotchocolate-types-usedimplicitly", + }; + + DiagnosticAnalyzer[] analyzers = + [ + new AvoidImplicitDateTimeToDateTimeOffsetAnalyzer(), + new AvoidMicrosoftExtensionsAzureAnalyzer(), + new GraphQLDuplicateFieldAnalyzer(), + new GraphQLHotChocolateTypeUsedImplicitlyAnalyzer(), + new GraphQLStaticExtensionClassAnalyzer(), + new GraphQLStaticModifierExtensionMethodAnalyzer(), + new GraphQLUnusedDataLoaderAnalyzer(), + new NamespaceInternalAnalyzer(), + new UnusedMethodAnalyzer(), + ]; + + var descriptors = analyzers + .SelectMany(static analyzer => analyzer.SupportedDiagnostics) + .ToArray(); + + Assert.Equal(expectedLinks.Count, descriptors.Length); + + foreach (var descriptor in descriptors) + { + Assert.True(expectedLinks.TryGetValue(descriptor.Id, out var expectedLink), $"Unexpected diagnostic id: {descriptor.Id}"); + Assert.Equal(expectedLink, descriptor.HelpLinkUri); + } + } +} diff --git a/Stackworx.Analyzers/AvoidImplicitDateTimeToDateTimeOffsetAnalyzer.cs b/Stackworx.Analyzers/AvoidImplicitDateTimeToDateTimeOffsetAnalyzer.cs index 3775af7..b6e31aa 100644 --- a/Stackworx.Analyzers/AvoidImplicitDateTimeToDateTimeOffsetAnalyzer.cs +++ b/Stackworx.Analyzers/AvoidImplicitDateTimeToDateTimeOffsetAnalyzer.cs @@ -28,7 +28,8 @@ public sealed class AvoidImplicitDateTimeToDateTimeOffsetAnalyzer : DiagnosticAn category: Category, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: Description); + description: Description, + helpLinkUri: DiagnosticHelpLinks.For(DiagnosticId)); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); diff --git a/Stackworx.Analyzers/AvoidMicrosoftExtensionsAzureAnalyzer.cs b/Stackworx.Analyzers/AvoidMicrosoftExtensionsAzureAnalyzer.cs index 3e8f3d2..c7ef61a 100644 --- a/Stackworx.Analyzers/AvoidMicrosoftExtensionsAzureAnalyzer.cs +++ b/Stackworx.Analyzers/AvoidMicrosoftExtensionsAzureAnalyzer.cs @@ -30,7 +30,8 @@ public sealed class AvoidMicrosoftExtensionsAzureAnalyzer : DiagnosticAnalyzer category: Category, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: Description); + description: Description, + helpLinkUri: DiagnosticHelpLinks.For(DiagnosticId)); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); diff --git a/Stackworx.Analyzers/DiagnosticHelpLinks.cs b/Stackworx.Analyzers/DiagnosticHelpLinks.cs new file mode 100644 index 0000000..39d5c90 --- /dev/null +++ b/Stackworx.Analyzers/DiagnosticHelpLinks.cs @@ -0,0 +1,33 @@ +namespace Stackworx.Analyzers; + +using System; + +internal static class DiagnosticHelpLinks +{ + private const string RulesBaseUrl = "https://stackworx-dotnet.github.io/Stackworx.Analyzers/docs/rules/"; + + public static string For(string diagnosticId) => RulesBaseUrl + (diagnosticId switch + { + "SW001" => + "sw001-avoid-implicit-datetime-to-datetimeoffset", + "SW002" => + "sw002-unused-method", + "SW101" => + "sw101-forbidden-namespace-reference", + "SW102" => + "sw102-forbidden-namespace-using", + "SW103" => + "sw103-avoid-microsoft-extensions-azure", + "SWGQL01" => + "swgql01-static-extension-method-validation", + "SWGQL02" => + "swgql02-unused-dataloader-interface", + "SWGQL03" => + "swgql03-graphql-extension-class-static", + "SWGQL04" => + "swgql04-graphql-duplicate-extension-field", + "SWGQL05" or "SWGQL06" => + "swgql05-06-hotchocolate-types-usedimplicitly", + _ => throw new ArgumentOutOfRangeException(nameof(diagnosticId), diagnosticId, "No documentation link configured."), + }); +} diff --git a/Stackworx.Analyzers/GraphQLDuplicateFieldAnalyzer.cs b/Stackworx.Analyzers/GraphQLDuplicateFieldAnalyzer.cs index eb7c49a..8712b2a 100644 --- a/Stackworx.Analyzers/GraphQLDuplicateFieldAnalyzer.cs +++ b/Stackworx.Analyzers/GraphQLDuplicateFieldAnalyzer.cs @@ -19,6 +19,7 @@ public sealed class GraphQLDuplicateFieldAnalyzer : DiagnosticAnalyzer defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "Detects duplicate extension fields for the same type across [ExtendObjectType] extension classes.", + helpLinkUri: DiagnosticHelpLinks.For("SWGQL04"), customTags: [WellKnownDiagnosticTags.CompilationEnd]); public override ImmutableArray SupportedDiagnostics => diff --git a/Stackworx.Analyzers/GraphQLHotChocolateTypeUsedImplicitlyAnalyzer.cs b/Stackworx.Analyzers/GraphQLHotChocolateTypeUsedImplicitlyAnalyzer.cs index f336725..81b8b03 100644 --- a/Stackworx.Analyzers/GraphQLHotChocolateTypeUsedImplicitlyAnalyzer.cs +++ b/Stackworx.Analyzers/GraphQLHotChocolateTypeUsedImplicitlyAnalyzer.cs @@ -21,7 +21,8 @@ public sealed class GraphQLHotChocolateTypeUsedImplicitlyAnalyzer : DiagnosticAn defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: - "HotChocolate GraphQL types are often discovered via reflection. Mark them with [JetBrains.Annotations.UsedImplicitly] so IDE analyzers don't flag them as unused."); + "HotChocolate GraphQL types are often discovered via reflection. Mark them with [JetBrains.Annotations.UsedImplicitly] so IDE analyzers don't flag them as unused.", + helpLinkUri: DiagnosticHelpLinks.For("SWGQL05")); public static readonly DiagnosticDescriptor UsedImplicitlyOnNonGraphQLTypeRule = new( id: "SWGQL06", @@ -32,6 +33,7 @@ public sealed class GraphQLHotChocolateTypeUsedImplicitlyAnalyzer : DiagnosticAn isEnabledByDefault: true, description: "Prefer limiting [UsedImplicitly] to types that are actually discovered implicitly (e.g., HotChocolate GraphQL types).", + helpLinkUri: DiagnosticHelpLinks.For("SWGQL06"), customTags: [WellKnownDiagnosticTags.Unnecessary]); public override ImmutableArray SupportedDiagnostics => diff --git a/Stackworx.Analyzers/GraphQLStaticExtensionClassAnalyzer.cs b/Stackworx.Analyzers/GraphQLStaticExtensionClassAnalyzer.cs index e80fb5d..a0a7951 100644 --- a/Stackworx.Analyzers/GraphQLStaticExtensionClassAnalyzer.cs +++ b/Stackworx.Analyzers/GraphQLStaticExtensionClassAnalyzer.cs @@ -21,7 +21,8 @@ public class GraphQLStaticExtensionClassAnalyzer : DiagnosticAnalyzer Category, DiagnosticSeverity.Error, isEnabledByDefault: true, - description: Description); + description: Description, + helpLinkUri: DiagnosticHelpLinks.For(DiagnosticId)); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); diff --git a/Stackworx.Analyzers/GraphQLStaticModifierExtensionMethodAnalyzer.cs b/Stackworx.Analyzers/GraphQLStaticModifierExtensionMethodAnalyzer.cs index 891ce2b..066ec7e 100644 --- a/Stackworx.Analyzers/GraphQLStaticModifierExtensionMethodAnalyzer.cs +++ b/Stackworx.Analyzers/GraphQLStaticModifierExtensionMethodAnalyzer.cs @@ -18,7 +18,8 @@ public sealed class GraphQLStaticModifierExtensionMethodAnalyzer : DiagnosticAna category: "GraphQL.Usage", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "GraphQL field extension methods must be instance methods when declared in non-static classes."); + description: "GraphQL field extension methods must be instance methods when declared in non-static classes.", + helpLinkUri: DiagnosticHelpLinks.For("SWGQL01")); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(StaticMethodInNonStaticClassRule); diff --git a/Stackworx.Analyzers/GraphQLUnusedDataLoaderAnalyzer.cs b/Stackworx.Analyzers/GraphQLUnusedDataLoaderAnalyzer.cs index e3d488c..0d092a7 100644 --- a/Stackworx.Analyzers/GraphQLUnusedDataLoaderAnalyzer.cs +++ b/Stackworx.Analyzers/GraphQLUnusedDataLoaderAnalyzer.cs @@ -21,6 +21,7 @@ public sealed class GraphQLUnusedDataLoaderAnalyzer : DiagnosticAnalyzer isEnabledByDefault: true, description: "Flags interfaces extending GreenDonut.IDataLoader that appear to be unused in the current compilation.", + helpLinkUri: DiagnosticHelpLinks.For("SWGQL02"), customTags: [WellKnownDiagnosticTags.CompilationEnd]); public override ImmutableArray SupportedDiagnostics => diff --git a/Stackworx.Analyzers/NamespaceInternalAnalyzer.cs b/Stackworx.Analyzers/NamespaceInternalAnalyzer.cs index 6811a09..1aeb5fe 100644 --- a/Stackworx.Analyzers/NamespaceInternalAnalyzer.cs +++ b/Stackworx.Analyzers/NamespaceInternalAnalyzer.cs @@ -29,10 +29,10 @@ public sealed class NamespaceInternalAnalyzer : DiagnosticAnalyzer "Using directives that import a feature’s Internal namespace are restricted to that feature."; private static readonly DiagnosticDescriptor RuleReference = new( - Id_Reference, Title1, Message1, "Architecture", DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Desc1); + Id_Reference, Title1, Message1, "Architecture", DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Desc1, helpLinkUri: DiagnosticHelpLinks.For(Id_Reference)); private static readonly DiagnosticDescriptor RuleUsing = new( - Id_Using, Title2, Message2, "Architecture", DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Desc2); + Id_Using, Title2, Message2, "Architecture", DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Desc2, helpLinkUri: DiagnosticHelpLinks.For(Id_Using)); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(RuleReference, RuleUsing); diff --git a/Stackworx.Analyzers/UnusedMethodAnalyzer.cs b/Stackworx.Analyzers/UnusedMethodAnalyzer.cs index 49137bb..2528d03 100644 --- a/Stackworx.Analyzers/UnusedMethodAnalyzer.cs +++ b/Stackworx.Analyzers/UnusedMethodAnalyzer.cs @@ -21,6 +21,7 @@ public sealed class UnusedMethodAnalyzer : DiagnosticAnalyzer isEnabledByDefault: false, description: "Flags methods that have no call sites in the current compilation. Methods annotated with JetBrains.Annotations.PublicAPI or JetBrains.Annotations.UsedImplicitly (or whose containing type is annotated) are ignored.", + helpLinkUri: DiagnosticHelpLinks.For("SW002"), customTags: [WellKnownDiagnosticTags.CompilationEnd]); public override ImmutableArray SupportedDiagnostics =>