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