diff --git a/README.md b/README.md index 842ec7d..bb1e599 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,33 @@ -# Open Source Project Template +# GooseAnalyzers -This repository contains a template to seed a repository for an Open Source -project. +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=flat-square)](LICENSE) -## How to use this template +GooseAnalyzers is a collection of .NET Analyzers for your C# code. -1. Check out this repository -2. Delete the `.git` folder -3. Git init this repository and start working on your project! -4. Prior to submitting your request for publication, make sure to review the - [Open Source guidelines for publications](https://nventive.visualstudio.com/Internal/_wiki/wikis/Internal_wiki?wikiVersion=GBwikiMaster&pagePath=%2FOpen%20Source%2FPublishing&pageId=7120). - -## Features (to keep as-is, configure or remove) -- [Mergify](https://mergify.io/) is configured. You can edit or remove [.mergify.yml](/.mergify.yml). - -The following is the template for the final README.md file: - ---- - -# Project Title - -{Project tag line} +## Getting Started -{Small description of the purpose of the project} +1. Install the `GooseAnalyzers` NuGet package in your project. -[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) +1. Optionally, set `TreatWarningsAsErrors` to `true` when in `Release` configuration in your project files. + ```xml + true + ``` + We recommend this so that you get warnings that don't block your dev loop, but errors that block your CI/CD pipelines. -## Getting Started +## List of Analyzers -{Instructions to quickly get started using the project: pre-requisites, packages -to install, sample code, etc.} +Identifier | Name | Description +-|-|- +`GOOSE001` | XmlDocumentationRequiredSuppressor | Limits the scope of [CS1591](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/cs1591) and [SA1600](https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1600.md) to interfaces.
## Features -{More details/listing of features of the project} +### `GOOSE001` - XML Documentation on Interfaces +The `GOOSE001` analyzer is a `DiagnosticSuppressor` for the [CS1591](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/cs1591) and [SA1600](https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1600.md) rules that demand XML documentation on all public types and members. +This is a good practice, but it can unrealistic in some contexts. +We think that in those cases, having xml documentation on interfaces is a good middle ground. + +We recommend you enable the `CS1591` or `SA1600` rules in your project and use this suppressor to limit their scope to interfaces or disable this suppressor. ## Breaking Changes diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml new file mode 100644 index 0000000..cf1d957 --- /dev/null +++ b/build/azure-pipelines.yml @@ -0,0 +1,66 @@ +trigger: + branches: + include: + - main + +resources: + containers: + - container: windows + image: nventive/vs_build-tools:17.2.5 + +variables: +- name: NUGET_VERSION + value: 6.2.0 +- name: VSTEST_PLATFORM_VERSION + value: 17.2.5 +- name: ArtifactName + value: Packages +- name: SolutionFileName # Example: MyApplication.sln + value: GooseAnalyzers.sln +- name: IsReleaseBranch # Should this branch name use the release stage + value: $[or(eq(variables['Build.SourceBranch'], 'refs/heads/main'), startsWith(variables['Build.SourceBranch'], 'refs/heads/feature/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'))] +# Pool names +- name: windowsPoolName + value: 'windows 2022' + +stages: +- stage: Build + jobs: + - job: Windows + strategy: + maxParallel: 3 + matrix: + Packages: + ApplicationConfiguration: Release + ApplicationPlatform: NuGet + GeneratePackageOnBuild: true + + pool: + name: $(windowsPoolName) + + variables: + - name: PackageOutputPath # Path where nuget packages will be copied to. + value: $(Build.ArtifactStagingDirectory) + + workspace: + clean: all # Cleanup the workspaca before starting + + container: windows + + steps: + - template: stage-build.yml + +- stage: Release + # Only release when the build is not for a Pull Request and branch name fits + condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'), eq(variables['IsReleaseBranch'], 'true')) + jobs: + - job: Publish_NuGet_External + + pool: + name: $(windowsPoolName) + + workspace: + clean: all # Cleanup the workspaca before starting + + steps: + - template: stage-release.yml diff --git a/build/gitversion.yml b/build/gitversion.yml new file mode 100644 index 0000000..078ad37 --- /dev/null +++ b/build/gitversion.yml @@ -0,0 +1,27 @@ +# The version is driven by conventional commits via xxx-version-bump-message. +# Anything merged to main creates a new stable version. +# Only builds from main and feature/* are pushed to nuget.org. + +assembly-versioning-scheme: MajorMinorPatch +mode: MainLine +next-version: '' # Use git tags to set the base version. +continuous-delivery-fallback-tag: "" +commit-message-incrementing: Enabled +major-version-bump-message: "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?(!:|:.*\\n\\n((.+\\n)+\\n)?BREAKING CHANGE:\\s.+)" +minor-version-bump-message: "^(feat)(\\([\\w\\s-]*\\))?:" +patch-version-bump-message: "^(build|chore|docs|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?:" +no-bump-message: "^(ci)(\\([\\w\\s-]*\\))?:" # You can use the "ci" type to avoid bumping the version when your changes are limited to the build or .github folders. +branches: + main: + regex: ^master$|^main$ + tag: '' + dev: + regex: dev/.*?/(.*?) + tag: dev.{BranchName} + source-branches: [main] + feature: + tag: feature.{BranchName} + regex: feature/(.*?) + source-branches: [main] +ignore: + sha: [] \ No newline at end of file diff --git a/build/stage-build.yml b/build/stage-build.yml new file mode 100644 index 0000000..2b6c19f --- /dev/null +++ b/build/stage-build.yml @@ -0,0 +1,74 @@ +steps: +- task: gitversion/setup@0 + inputs: + versionSpec: '5.10.1' + displayName: 'Install GitVersion' + +- task: gitversion/execute@0 + inputs: + useConfigFile: true + configFilePath: build/gitversion.yml + displayName: 'Calculate version' + +- task: NuGetToolInstaller@1 + displayName: 'Install NuGet $(NUGET_VERSION)' + inputs: + versionSpec: $(NUGET_VERSION) + checkLatest: false + +- task: MSBuild@1 + displayName: 'Restore solution packages' + inputs: + solution: $(Build.SourcesDirectory)/src/$(SolutionFileName) + msbuildLocationMethod: version + msbuildVersion: latest + msbuildArchitecture: x86 + msbuildArguments: > + /t:restore + configuration: $(ApplicationConfiguration) + platform: $(ApplicationPlatform) + clean: false + maximumCpuCount: true + restoreNugetPackages: false + logProjectEvents: false + createLogFile: false + +- task: MSBuild@1 + displayName: 'Build solution in $(ApplicationConfiguration) | $(ApplicationPlatform)' + inputs: + solution: $(Build.SourcesDirectory)/src/$(SolutionFileName) + msbuildLocationMethod: version + msbuildVersion: latest + msbuildArchitecture: x86 + configuration: $(ApplicationConfiguration) + platform: $(ApplicationPlatform) + clean: false + maximumCpuCount: true + restoreNugetPackages: false + logProjectEvents: false + createLogFile: false + msbuildArguments: > # Set the version of the packages, will have no effect on application projects (Heads). + /p:PackageVersion=$(GitVersion.SemVer) + /p:ContinousIntegrationBuild=true + +- script: dotnet test src --no-build --configuration $(ApplicationConfiguration) --logger trx --collect "Code coverage" + displayName: 'Run tests' + condition: and(succeeded(), eq(variables['ApplicationPlatform'], 'NuGet')) + +- task: PublishTestResults@2 + displayName: 'Publish test results' + condition: succeededOrFailed() + inputs: + testRunner: VSTest + testResultsFiles: '**/*.trx' + +- task: PublishBuildArtifacts@1 + displayName: 'Publish artifact $(ApplicationConfiguration)' + inputs: + PathtoPublish: $(PackageOutputPath) + ArtifactName: $(ArtifactName) + ArtifactType: Container + +- task: PostBuildCleanup@3 + displayName: 'Post-Build cleanup : Cleanup files to keep build server clean!' + condition: always() \ No newline at end of file diff --git a/build/stage-release.yml b/build/stage-release.yml new file mode 100644 index 0000000..54e762e --- /dev/null +++ b/build/stage-release.yml @@ -0,0 +1,26 @@ +steps: +- checkout: none + +- task: DownloadBuildArtifacts@0 + inputs: + buildType: current + downloadType: single + artifactName: $(ArtifactName) + +- task: NuGetToolInstaller@1 + displayName: 'Install NuGet $(NUGET_VERSION)' + inputs: + versionSpec: $(NUGET_VERSION) + checkLatest: false + +- task: NuGetCommand@2 + displayName: 'Push to Nuget.org' + inputs: + command: 'push' + packagesToPush: '$(Build.ArtifactStagingDirectory)/$(ArtifactName)/*.nupkg' + nuGetFeedType: 'external' + publishFeedCredentials: 'NuGet.org - nventive' + +- task: PostBuildCleanup@3 + displayName: 'Post-Build cleanup : Cleanup files to keep build server clean!' + condition: always() \ No newline at end of file diff --git a/src/.editorconfig b/src/.editorconfig new file mode 100644 index 0000000..9c799fb --- /dev/null +++ b/src/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +indent_style = tab +end_of_line = crlf +charset = utf-8-bom +trim_trailing_whitespace = true + +# Code files +[*.{cs,tt,xaml,xml,md,ps1}] +indent_size = 4 +insert_final_newline = true + +# SA1633:The file header is missing or not located at the top of the file +dotnet_diagnostic.SA1633.severity = none +# SA1649: File name should match first type name +dotnet_diagnostic.SA1649.severity = suggestion diff --git a/src/GooseAnalyzers.Tests/GooseAnalyzers.Tests.csproj b/src/GooseAnalyzers.Tests/GooseAnalyzers.Tests.csproj new file mode 100644 index 0000000..40249d0 --- /dev/null +++ b/src/GooseAnalyzers.Tests/GooseAnalyzers.Tests.csproj @@ -0,0 +1,46 @@ + + + + net7.0 + enable + enable + true + $(NoWarn); + + + + false + true + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/src/GooseAnalyzers.Tests/UnitTest1.cs b/src/GooseAnalyzers.Tests/UnitTest1.cs new file mode 100644 index 0000000..a357746 --- /dev/null +++ b/src/GooseAnalyzers.Tests/UnitTest1.cs @@ -0,0 +1,30 @@ +namespace GooseAnalyzers.Tests +{ + /// + /// This is a documented interface. + /// + public interface ITestInterface + { + /// + /// Gets the name. + /// + string Name { get; } + } + + // The lack of XML doc should yield a warning. + public interface ITestInterface2 + { + // The lack of XML doc should yield a warning. + string Name { get; } + } + + // The lack of XML doc should NOT yield a warning (because this isn't an interface). + public class UnitTest1 + { + // The lack of XML doc should NOT yield a warning (because this isn't an interface member). + [Fact] + public void Test1() + { + } + } +} \ No newline at end of file diff --git a/src/GooseAnalyzers.Tests/Usings.cs b/src/GooseAnalyzers.Tests/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/src/GooseAnalyzers.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/src/GooseAnalyzers.sln b/src/GooseAnalyzers.sln new file mode 100644 index 0000000..e10ea82 --- /dev/null +++ b/src/GooseAnalyzers.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33815.320 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GooseAnalyzers", "GooseAnalyzers\GooseAnalyzers.csproj", "{82FB32E8-E3AD-438D-A378-5CA7042C45B9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GooseAnalyzers.Tests", "GooseAnalyzers.Tests\GooseAnalyzers.Tests.csproj", "{13E80283-6CF4-4D95-A428-C8E5F08693E0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {82FB32E8-E3AD-438D-A378-5CA7042C45B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {82FB32E8-E3AD-438D-A378-5CA7042C45B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {82FB32E8-E3AD-438D-A378-5CA7042C45B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {82FB32E8-E3AD-438D-A378-5CA7042C45B9}.Release|Any CPU.Build.0 = Release|Any CPU + {13E80283-6CF4-4D95-A428-C8E5F08693E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13E80283-6CF4-4D95-A428-C8E5F08693E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13E80283-6CF4-4D95-A428-C8E5F08693E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13E80283-6CF4-4D95-A428-C8E5F08693E0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CF6F53E9-E886-4209-AE5B-1C96EA5C0D5A} + EndGlobalSection +EndGlobal diff --git a/src/GooseAnalyzers/GooseAnalyzers.csproj b/src/GooseAnalyzers/GooseAnalyzers.csproj new file mode 100644 index 0000000..10799b6 --- /dev/null +++ b/src/GooseAnalyzers/GooseAnalyzers.csproj @@ -0,0 +1,51 @@ + + + + netstandard2.0 + true + 11 + enable + $(NoWarn);CS1591 + + GooseAnalyzers + nventive + nventive + GooseAnalyzers + GooseAnalyzers + A collection of .NET analyzers and DiagnosticSuppressors for C#. + true + README.md + analyzers;roslyn;diagnostics + Apache-2.0 + https://github.com/nventive/GooseAnalyzers + + + true + true + true + snupkg + + + + + + True + \ + + + + + + + + + + + + + + + + + + diff --git a/src/GooseAnalyzers/GooseAnalyzers.sln b/src/GooseAnalyzers/GooseAnalyzers.sln new file mode 100644 index 0000000..882d625 --- /dev/null +++ b/src/GooseAnalyzers/GooseAnalyzers.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33815.320 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GooseAnalyzers", "GooseAnalyzers.csproj", "{82FB32E8-E3AD-438D-A378-5CA7042C45B9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GooseAnalyzers.Tests", "..\GooseAnalyzers.Tests\GooseAnalyzers.Tests.csproj", "{13E80283-6CF4-4D95-A428-C8E5F08693E0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {82FB32E8-E3AD-438D-A378-5CA7042C45B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {82FB32E8-E3AD-438D-A378-5CA7042C45B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {82FB32E8-E3AD-438D-A378-5CA7042C45B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {82FB32E8-E3AD-438D-A378-5CA7042C45B9}.Release|Any CPU.Build.0 = Release|Any CPU + {13E80283-6CF4-4D95-A428-C8E5F08693E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13E80283-6CF4-4D95-A428-C8E5F08693E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13E80283-6CF4-4D95-A428-C8E5F08693E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13E80283-6CF4-4D95-A428-C8E5F08693E0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CF6F53E9-E886-4209-AE5B-1C96EA5C0D5A} + EndGlobalSection +EndGlobal diff --git a/src/GooseAnalyzers/XmlDocumentationRequiredSuppressor.cs b/src/GooseAnalyzers/XmlDocumentationRequiredSuppressor.cs new file mode 100644 index 0000000..d72432d --- /dev/null +++ b/src/GooseAnalyzers/XmlDocumentationRequiredSuppressor.cs @@ -0,0 +1,42 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using System; +using System.Collections.Immutable; +using System.Collections.Generic; +using System.Linq; + +namespace GooseAnalyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class XmlDocumentationRequiredSuppressor : DiagnosticSuppressor +{ + public static readonly string[] SuppressedDiagnosticIds = { "CS1591", "SA1600" }; + + public static readonly IReadOnlyDictionary SuppressionDescriptorByDiagnosticId = SuppressedDiagnosticIds.ToDictionary( + id => id, + id => new SuppressionDescriptor("GOOSE001", id, "XML documentation is most important on interface members, but not on everything else.")); + + public override ImmutableArray SupportedSuppressions { get; } = ImmutableArray.CreateRange(SuppressionDescriptorByDiagnosticId.Values); + + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + foreach (var diagnostic in context.ReportedDiagnostics) + { + if (!context.CancellationToken.IsCancellationRequested) + { + Location location = diagnostic.Location; + SyntaxNode? node = location.SourceTree?.GetRoot(context.CancellationToken).FindNode(location.SourceSpan); + if (!(node is InterfaceDeclarationSyntax || HasParentOfType(node))) + { + context.ReportSuppression(Suppression.Create(SuppressionDescriptorByDiagnosticId[diagnostic.Id], diagnostic)); + } + } + } + } + + public bool HasParentOfType(SyntaxNode? syntaxNode) where TSearched : SyntaxNode + { + return syntaxNode != null && (syntaxNode.Parent is TSearched || HasParentOfType(syntaxNode.Parent)); + } +} diff --git a/src/nuget.config b/src/nuget.config new file mode 100644 index 0000000..554c2f6 --- /dev/null +++ b/src/nuget.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/stylecop.json b/src/stylecop.json new file mode 100644 index 0000000..87207ba --- /dev/null +++ b/src/stylecop.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "nventive", + "documentationCulture": "en-US", + "documentPrivateElements": false, + "documentPrivateFields": false + }, + "indentation": { + "useTabs": true, + "indentationSize": 4 + }, + "orderingRules": { + "usingDirectivesPlacement": "outsideNamespace" + } + } +} \ No newline at end of file