diff --git a/.all-contributorsrc b/.all-contributorsrc index fc0d88d..229f8b1 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -53,6 +53,24 @@ "contributions": [ "doc" ] + }, + { + "login": "squid-box", + "name": "Joel Ahlgren", + "avatar_url": "https://avatars.githubusercontent.com/u/864820?v=4", + "profile": "https://www.ahlgren.io", + "contributions": [ + "code" + ] + }, + { + "login": "devlead", + "name": "Mattias Karlsson", + "avatar_url": "https://avatars.githubusercontent.com/u/1647294?v=4", + "profile": "https://www.devlead.se", + "contributions": [ + "bug" + ] } ], "contributorsPerLine": 7, @@ -60,5 +78,6 @@ "projectOwner": "cake-contrib", "repoType": "github", "repoHost": "https://github.com", - "skipCi": true + "skipCi": true, + "commitConvention": "angular" } diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..9b339cb --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,17 @@ +codecov: + notify: + require_ci_to_pass: no + +coverage: + precision: 2 + round: down + range: "75...100" + status: + project: + default: + target: 80% + threshold: 3% + patch: + default: + target: 80% + threshold: 3% \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2f4dde2..e160b18 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ ubuntu-18.04, windows-2019, macos-10.15 ] + os: [ ubuntu-22.04, windows-2022, macos-12 ] env: AZURE_PASSWORD: ${{ secrets.AZURE_PASSWORD }} @@ -44,20 +44,22 @@ jobs: steps: - name: Checkout the repository - uses: actions/checkout@v3 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - name: Fetch all tags and branches run: git fetch --prune --unshallow - - uses: actions/setup-dotnet@v3.0.3 + - uses: actions/setup-dotnet@v3.2.0 with: # codecov needs 2.1 # unittests needs 3.1 # gitversion needs 5.0 - # .NET 6 to build + # .NET 6/7 to build dotnet-version: | 2.1 3.1 5.0 6.0 + 7.0 + 8.0 - name: Cache Tools uses: actions/cache@v3 with: @@ -84,4 +86,4 @@ jobs: with: if-no-files-found: warn name: package - path: BuildArtifacts/Packages/**/* \ No newline at end of file + path: BuildArtifacts/Packages/**/* diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 34f42d1..3ac7fe7 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -17,7 +17,7 @@ on: jobs: analyze: name: Analyze - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 strategy: fail-fast: false @@ -30,10 +30,16 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 with: fetch-depth: 0 + - uses: actions/setup-dotnet@v3.2.0 + with: + dotnet-version: | + 5.0 + 8.0 + - name: Cache Tools uses: actions/cache@v3 with: @@ -46,7 +52,7 @@ jobs: with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. + # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main @@ -58,4 +64,4 @@ jobs: cake-version: 1.3.0 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 \ No newline at end of file + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/publishDocs.yml b/.github/workflows/publishDocs.yml index 20db252..9cbf842 100644 --- a/.github/workflows/publishDocs.yml +++ b/.github/workflows/publishDocs.yml @@ -15,7 +15,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@v3 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - name: Fetch all tags and branches run: git fetch --prune --unshallow diff --git a/.github/workflows/updateToc.yml b/.github/workflows/updateToc.yml index b8a088f..a1d1bbb 100644 --- a/.github/workflows/updateToc.yml +++ b/.github/workflows/updateToc.yml @@ -15,7 +15,7 @@ jobs: if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 with: fetch-depth: 0 ref: ${{ github.event.ref }} diff --git a/.vscode/settings.json b/.vscode/settings.json index 79e520a..48591ae 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "powershell.codeFormatting.addWhitespaceAroundPipe": true + "powershell.codeFormatting.addWhitespaceAroundPipe": true, + "markdown.extension.toc.updateOnSave": false } \ No newline at end of file diff --git a/README.md b/README.md index e7b6fd2..5d5ba99 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![standard-readme compliant][]][standard-readme] [![All Contributors][all-contributors-badge]](#contributors) [![Contributor Covenant][contrib-covenantimg]][contrib-covenant] -[![Appveyor build][appveyorimage]][appveyor] +[![Build][build-badge]][build] [![NuGet package][nugetimage]][nuget] Adds common guidelines to cake-contrib projects @@ -74,13 +74,17 @@ Thanks goes to these wonderful people ([emoji key][emoji-key]): - - - - - - - + + + + + + + + + + +

Kim J. Nordmo

💬 🤔

Jericho

🐛 🤔

Nils Andresen

💻 🚧

C. Augusto Proiete

🤔 👀

DiDoHH

📖
Kim J. Nordmo
Kim J. Nordmo

💬 🤔
Jericho
Jericho

🐛 🤔
Nils Andresen
Nils Andresen

💻 🚧
C. Augusto Proiete
C. Augusto Proiete

🤔 👀
DiDoHH
DiDoHH

📖
Joel Ahlgren
Joel Ahlgren

💻
Mattias Karlsson
Mattias Karlsson

🐛
@@ -94,8 +98,8 @@ Thanks goes to these wonderful people ([emoji key][emoji-key]): [all-contributors]: https://github.com/all-contributors/all-contributors [all-contributors-badge]: https://img.shields.io/github/all-contributors/cake-contrib/CakeContrib.Guidelines/develop?&style=flat-square -[appveyor]: https://ci.appveyor.com/project/cakecontrib/cakecontrib-guidelines -[appveyorimage]: https://img.shields.io/appveyor/ci/cakecontrib/cakecontrib-guidelines.svg?logo=appveyor&style=flat-square +[build]: https://github.com/cake-contrib/CakeContrib.Guidelines/actions/workflows/build.yml +[build-badge]: https://github.com/cake-contrib/CakeContrib.Guidelines/actions/workflows/build.yml/badge.svg [contrib-covenant]: https://www.contributor-covenant.org/version/2/0/code_of_conduct/ [contrib-covenantimg]: https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg [contributing]: CONTRIBUTING.md diff --git a/docs/input/guidelines/CakeInternalReferences.md b/docs/input/guidelines/CakeInternalReferences.md new file mode 100644 index 0000000..6d01219 --- /dev/null +++ b/docs/input/guidelines/CakeInternalReferences.md @@ -0,0 +1,129 @@ +--- +Title: Target Frameworks +--- + + + +## Table of Contents + +- [Goals](#goals) + - [Provided packages](#provided-packages) + - [Cake v1.0](#cake-v10) + - [Cake v2.0.0](#cake-v200) + - [Cake v3.0.0](#cake-v300) +- [Related rules](#related-rules) +- [Usage](#usage) +- [Settings](#settings) + - [Cake Version](#cake-version) + + + +## Goals + +When an addin/module references a package that is also provided by Cake at runtime, +the version the addin/module references should match the version provided by Cake. +Also, the reference of the package in the addin/module should be set as private assets. + +### Provided packages + +#### Cake v1.0 + +| Reference | Version | +| ---------------------------------------- | ------------- | +| Autofac | 6.1.0 | +| Microsoft.CodeAnalysis.CSharp.Scripting | 3.9.0-1.final | +| Microsoft.CSharp | 4.7.0 | +| Microsoft.DotNet.PlatformAbstractions | 3.1.6 | +| Microsoft.Extensions.DependencyInjection | 5.0.1 | +| Microsoft.NETCore.Platforms | 5.0.0 | +| Microsoft.Win32.Registry | 5.0.0 | +| Newtonsoft.Json | 12.0.3 | +| NuGet.Common | 5.8.0 | +| NuGet.Frameworks | 5.8.0 | +| NuGet.Packaging | 5.8.0 | +| NuGet.Protocol | 5.8.0 | +| NuGet.Resolver | 5.8.0 | +| NuGet.Versioning | 5.8.0 | +| System.Collections.Immutable | 5.0.0 | +| System.Reflection.Metadata | 5.0.0 | +| xunit | 2.4.1 | + +#### Cake v2.0.0 + +| Reference | Version | +| ---------------------------------------- | ------- | +| Autofac | 6.3.0 | +| Microsoft.CodeAnalysis.CSharp.Scripting | 4.0.1 | +| Microsoft.CSharp | 4.7.0 | +| Microsoft.DotNet.PlatformAbstractions | 3.1.6 | +| Microsoft.Extensions.DependencyInjection | 6.0.0 | +| Microsoft.NETCore.Platforms | 6.0.0 | +| Microsoft.Win32.Registry | 5.0.0 | +| Newtonsoft.Json | 13.0.1 | +| NuGet.Common | 5.11.0 | +| NuGet.Frameworks | 5.11.0 | +| NuGet.Packaging | 5.11.0 | +| NuGet.Protocol | 5.11.0 | +| NuGet.Resolver | 5.11.0 | +| NuGet.Versioning | 5.11.0 | +| System.Collections.Immutable | 6.0.0 | +| System.Reflection.Metadata | 6.0.0 | +| xunit | 2.4.1 | + +#### Cake v3.0.0 + +| Reference | Version | +| ---------------------------------------- | ------------- | +| Autofac | 6.4.0 | +| Microsoft.CodeAnalysis.CSharp.Scripting | 4.4.0-4.final | +| Microsoft.CSharp | 4.7.0 | +| Microsoft.Extensions.DependencyInjection | 7.0.0 | +| Microsoft.NETCore.Platforms | 7.0.0 | +| Microsoft.Win32.Registry | 5.0.0 | +| Newtonsoft.Json | 13.0.1 | +| NuGet.Common | 6.3.1 | +| NuGet.Frameworks | 6.3.1 | +| NuGet.Packaging | 6.3.1 | +| NuGet.Protocol | 6.3.1 | +| NuGet.Resolver | 6.3.1 | +| NuGet.Versioning | 6.3.1 | +| System.Collections.Immutable | 7.0.0 | +| System.Reflection.Metadata | 7.0.0 | +| xunit | 2.4.2 | + +#### Cake v4.0 + +| Reference | Version | +| ---------------------------------------- | ------------- | +| Autofac | 7.1.0 | +| Microsoft.CodeAnalysis.CSharp.Scripting | 4.8.0-3.final | +| Microsoft.CSharp | 4.7.0 | +| Microsoft.Extensions.DependencyInjection | 8.0.0 | +| Microsoft.NETCore.Platforms | 7.0.4 | +| Microsoft.Win32.Registry | 5.0.0 | +| Newtonsoft.Json | 13.0.3 | +| NuGet.Common | 6.7.0 | +| NuGet.Frameworks | 6.7.0 | +| NuGet.Packaging | 6.7.0 | +| NuGet.Protocol | 6.7.0 | +| NuGet.Resolver | 6.7.0 | +| NuGet.Versioning | 6.7.0 | +| System.Collections.Immutable | 8.0.0 | +| System.Reflection.Metadata | 8.0.0 | +| xunit | 2.6.1 | + +## Related rules + + * [CCG0010](../rules/ccg0010) + +These rules are only applied for [project types](../settings#projecttype) `addin` and `module`. + +## Usage + +Using this package automatically enables this guideline. + +## Settings + +### Cake Version + + \ No newline at end of file diff --git a/docs/input/guidelines/TargetFramework.md b/docs/input/guidelines/TargetFramework.md index 674a259..d222e20 100644 --- a/docs/input/guidelines/TargetFramework.md +++ b/docs/input/guidelines/TargetFramework.md @@ -11,14 +11,15 @@ Title: Target Frameworks - [Related rules](#related-rules) - [Usage](#usage) - [Settings](#settings) + - [Cake Version](#cake-version) - [Opt-Out](#opt-out) ## Goals -Each addin/module should have maximum compatibility when being used. Toward that end some Framework versions are required and some others are -suggested, depending on the Cake.Core version that is being referenced. +Each addin/module should have maximum compatibility when being used. Toward that end some Framework versions are required and some others are +suggested, depending on the Cake.Core version that is being referenced. ### Required / Suggested versions @@ -43,13 +44,23 @@ while missing a suggested target version will raise [CCG0007](../rules/ccg0007) * Cake.Core >= 3.0.0 * Required: `net6.0` * Required: `net7.0` + * Cake.Core >= 4.0.0 + * Required: `net6.0` + * Required: `net7.0` + * Required: `net8.0` * Package type: module * Cake.Core < 2.0.0 * Required: `netstandard2.0` + * No additional targets are allowed. * Cake.Core >= 2.0.0 * Required: `netcoreapp3.1` + * No additional targets are allowed. * Cake.Core >= 3.0.0 * Required: `net6.0` + * No additional targets are allowed. + * Cake.Core >= 4.0.0 + * Required: `net6.0` + * No additional targets are allowed. For package type recipe no framework reference is required or suggested. @@ -65,6 +76,10 @@ Using this package automatically enables this guideline. ## Settings +### Cake Version + + + ### Opt-Out diff --git a/docs/input/guidelines/examples/Editorconfig.md b/docs/input/guidelines/examples/Editorconfig.md index 538d7eb..ff7c5a2 100644 --- a/docs/input/guidelines/examples/Editorconfig.md +++ b/docs/input/guidelines/examples/Editorconfig.md @@ -207,19 +207,19 @@ dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case dotnet_naming_symbols.interface.applicable_kinds = interface dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = +dotnet_naming_symbols.interface.required_modifiers = dotnet_naming_symbols.method.applicable_kinds = method dotnet_naming_symbols.method.applicable_accessibilities = public -dotnet_naming_symbols.method.required_modifiers = +dotnet_naming_symbols.method.required_modifiers = dotnet_naming_symbols.public_or_protected_field.applicable_kinds = field dotnet_naming_symbols.public_or_protected_field.applicable_accessibilities = public, protected -dotnet_naming_symbols.public_or_protected_field.required_modifiers = +dotnet_naming_symbols.public_or_protected_field.required_modifiers = dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected -dotnet_naming_symbols.private_or_internal_field.required_modifiers = +dotnet_naming_symbols.private_or_internal_field.required_modifiers = dotnet_naming_symbols.private_or_internal_static_field.applicable_kinds = field dotnet_naming_symbols.private_or_internal_static_field.applicable_accessibilities = internal, private, private_protected @@ -227,27 +227,27 @@ dotnet_naming_symbols.private_or_internal_static_field.required_modifiers = stat dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = +dotnet_naming_symbols.types.required_modifiers = dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = +dotnet_naming_symbols.non_field_members.required_modifiers = # Naming styles -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case -dotnet_naming_style.camelcase.required_prefix = -dotnet_naming_style.camelcase.required_suffix = -dotnet_naming_style.camelcase.word_separator = +dotnet_naming_style.camelcase.required_prefix = +dotnet_naming_style.camelcase.required_suffix = +dotnet_naming_style.camelcase.word_separator = dotnet_naming_style.camelcase.capitalization = camel_case # Rules: diff --git a/docs/input/rules/ccg0001.md b/docs/input/rules/ccg0001.md index 192338e..4cbb750 100644 --- a/docs/input/rules/ccg0001.md +++ b/docs/input/rules/ccg0001.md @@ -3,7 +3,7 @@ Order: 1 Title: CCG0001 Description: PackageIcon is empty --- - + > PackageIcon is empty diff --git a/docs/input/rules/ccg0002.md b/docs/input/rules/ccg0002.md index 02c87a3..ccb8aac 100644 --- a/docs/input/rules/ccg0002.md +++ b/docs/input/rules/ccg0002.md @@ -28,7 +28,7 @@ that `PackageIconUrl` should be set for compatibility. ## How to fix violations -Add +Add ```xml diff --git a/docs/input/rules/ccg0003.md b/docs/input/rules/ccg0003.md index 60a2299..9b56a51 100644 --- a/docs/input/rules/ccg0003.md +++ b/docs/input/rules/ccg0003.md @@ -19,8 +19,8 @@ Description: PackageIcon can not be updated ## Cause -This warning/error is raised, when the source of `PackageIcon` was outdated -but could not be updated. +This warning/error is raised, when the source of `PackageIcon` was outdated +but could not be updated. ## Description diff --git a/docs/input/rules/ccg0004.md b/docs/input/rules/ccg0004.md index 66d81f0..6a86693 100644 --- a/docs/input/rules/ccg0004.md +++ b/docs/input/rules/ccg0004.md @@ -31,7 +31,7 @@ This error is raised, when one of the following packages is referenced without h References to `Cake.Core` or `Cake.Common` are usually only used for local compilation and should not be exposed to consuming packages. -References to `Cake.Addin.Analyzer` and `CakeContrib.Guidelines` are only used to analyze the current project and +References to `Cake.Addin.Analyzer` and `CakeContrib.Guidelines` are only used to analyze the current project and should never be exposed to consuming packages. ## How to fix violations diff --git a/docs/input/rules/ccg0010.md b/docs/input/rules/ccg0010.md new file mode 100644 index 0000000..ae18da9 --- /dev/null +++ b/docs/input/rules/ccg0010.md @@ -0,0 +1,39 @@ +--- +Order: 10 +Title: CCG0010 +Description: ProjectReference is provided by Cake +--- + + > NuGet.Common is provided by Cake. It should have `PrivateAssets="all"` set. + + > NuGet.Common is provided by Cake 3.0.0 in version 6.3.1. Do not reference a different version. + + + +## Table of Contents + +- [Cause](#cause) +- [Description](#description) +- [How to fix violations](#how-to-fix-violations) +- [Related guidelines](#related-guidelines) + + + +## Cause + +This warning is raised, when the addin/module references a package that is provided by Cake +and either does not have `PrivateAssets="all"` set, or is referenced in the wrong version. + +## Description + +If Addins/modules reference packages that are provided by Cake, +the version should match and the reference should have `PrivateAssets="all"` set. + +## How to fix violations + +Reference packages that are already provided by Cake in the correct version +and set `PrivateAssets="all"` on the `PackageReference`. + +## Related guidelines + +* [Recommended Cake Version](../guidelines/CakeInternalReferences) diff --git a/docs/input/settings/fragments/IconOmitImport.md b/docs/input/settings/fragments/IconOmitImport.md index b76664f..3deca9f 100644 --- a/docs/input/settings/fragments/IconOmitImport.md +++ b/docs/input/settings/fragments/IconOmitImport.md @@ -5,7 +5,7 @@ -The cake-contrib icon will be automatically included in the project, unless +The cake-contrib icon will be automatically included in the project, unless `CakeContribGuidelinesIconOmitImport` is set to `True`. To to use a "custom" import the following could be used: @@ -17,5 +17,5 @@ To to use a "custom" import the following could be used: - + ``` diff --git a/docs/input/settings/fragments/OmitPrivateCheck.md b/docs/input/settings/fragments/OmitPrivateCheck.md index 848c7a9..e817fc8 100644 --- a/docs/input/settings/fragments/OmitPrivateCheck.md +++ b/docs/input/settings/fragments/OmitPrivateCheck.md @@ -7,7 +7,7 @@ It it possible to opt-out of the check for `PrivateAssets` using the `CakeContribGuidelinesCakeReferenceOmitPrivateCheck` setting: -(*Keep in mind, though that it is not recommended to opt-out of this feature*) +(*Keep in mind, though, that it is not recommended to opt-out of this feature*) ```xml diff --git a/docs/input/settings/fragments/OmitRecommendedCakeVersion.md b/docs/input/settings/fragments/OmitRecommendedCakeVersion.md index a6cfd90..9604a0e 100644 --- a/docs/input/settings/fragments/OmitRecommendedCakeVersion.md +++ b/docs/input/settings/fragments/OmitRecommendedCakeVersion.md @@ -8,7 +8,7 @@ It it possible to opt-out of the check for recommended Cake version using the `CakeContribGuidelinesOmitRecommendedCakeVersion` setting and setting it's `Include` to the reference that should not be checked. -(*Keep in mind, though that it is not recommended to opt-out of this feature*) +(*Keep in mind, though, that it is not recommended to opt-out of this feature*) ```xml diff --git a/docs/input/settings/fragments/OmitRecommendedReference.md b/docs/input/settings/fragments/OmitRecommendedReference.md index b49c7c4..5c0d373 100644 --- a/docs/input/settings/fragments/OmitRecommendedReference.md +++ b/docs/input/settings/fragments/OmitRecommendedReference.md @@ -8,7 +8,7 @@ It it possible to opt-out of the check for recommended references by using the `CakeContribGuidelinesOmitRecommendedReference` setting and setting it's `Include` to the reference that should not be checked. -(*Keep in mind, though that it is not recommended to opt-out of this feature*) +(*Keep in mind, though, that it is not recommended to opt-out of this feature*) ```xml diff --git a/docs/input/settings/fragments/OmitRecommendedTag.md b/docs/input/settings/fragments/OmitRecommendedTag.md index 522a821..c7a7a7b 100644 --- a/docs/input/settings/fragments/OmitRecommendedTag.md +++ b/docs/input/settings/fragments/OmitRecommendedTag.md @@ -8,7 +8,7 @@ It it possible to opt-out of the check for recommended tags by using the `CakeContribGuidelinesOmitRecommendedTag` setting and setting it's `Include` to the tag that should not be checked. -(*Keep in mind, though that it is not recommended to opt-out of this feature*) +(*Keep in mind, though, that it is not recommended to opt-out of this feature*) ```xml diff --git a/docs/input/settings/fragments/OmitTargetFramework.md b/docs/input/settings/fragments/OmitTargetFramework.md index b03d066..b9c58b9 100644 --- a/docs/input/settings/fragments/OmitTargetFramework.md +++ b/docs/input/settings/fragments/OmitTargetFramework.md @@ -8,7 +8,7 @@ It it possible to opt-out of the check for target framework(s) by using the `CakeContribGuidelinesOmitTargetFramework` setting and setting it's `Include` to the target framework that should not be checked. -(*Keep in mind, though that it is not recommended to opt-out of this feature*) +(*Keep in mind, though, that it is not recommended to opt-out of this feature*) ```xml diff --git a/docs/input/settings/fragments/OverrideCakeVersion.md b/docs/input/settings/fragments/OverrideCakeVersion.md new file mode 100644 index 0000000..95edf59 --- /dev/null +++ b/docs/input/settings/fragments/OverrideCakeVersion.md @@ -0,0 +1,16 @@ + + + + + + + +It it possible to override the detected Cake version using the `CakeContribGuidelinesOverrideTargetFrameworkCakeVersion` setting: + +(*Keep in mind, though, that it is not recommended override the detected version.*) + +```xml + + 2.0.0 + +``` \ No newline at end of file diff --git a/docs/input/settings/index.md b/docs/input/settings/index.md index f169fe1..1ecc9dd 100644 --- a/docs/input/settings/index.md +++ b/docs/input/settings/index.md @@ -10,6 +10,7 @@ NoSidebar: true - [General](#general) - [ProjectType](#projecttype) + - [Cake version](#cake-version) - [Icons](#icons) - [IconOmitImport](#iconomitimport) - [Opt-Out](#opt-out) @@ -19,13 +20,15 @@ NoSidebar: true - [OmitRecommendedTag](#omitrecommendedtag) - [OmitPrivateCheck](#omitprivatecheck) - [OmitTargetFramework](#omittargetframework) +- [Override](#override) + - [OverrideTargetFrameworkCakeVersion](#overridetargetframeworkcakeversion) ## General ### ProjectType -A project can be one of different types: `addin`, `module`, `recipe` or `other`. +A project can be one of different types: `addin`, `module`, `recipe` or `other`. Some rules have different behavior for different project types. The project type is automatically detected but can be overridden. @@ -49,6 +52,10 @@ Though you can technically set `CakeContribGuidelinesProjectType` to anything yo different values than `addin`, `module` or `recipe` might yield unexpected results. ::: +### Cake version +Some guidelines (Currently only [Target Frameworks](../guidelines/TargetFramework)) have different characteristics, depending on the Cake version that was used. +The Cake version will be auto detected from the referenced + ## Icons ### IconOmitImport @@ -73,3 +80,8 @@ different values than `addin`, `module` or `recipe` might yield unexpected resul ### OmitTargetFramework + +## Override + +### OverrideTargetFrameworkCakeVersion + diff --git a/global.json b/global.json new file mode 100644 index 0000000..dd9e1c0 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "allowPrerelease": true, + "version": "8.0.100", + "rollForward": "latestFeature" + } +} diff --git a/recipe.cake b/recipe.cake index 7a9df5c..acec536 100644 --- a/recipe.cake +++ b/recipe.cake @@ -1,4 +1,4 @@ -#load nuget:?package=Cake.Recipe&version=3.0.1 +#load nuget:?package=Cake.Recipe&version=3.1.1 Environment.SetVariableNames(); @@ -12,7 +12,7 @@ BuildParameters.SetParameters( title: "CakeContrib.Guidelines", shouldRunDotNetCorePack: true, shouldDocumentSourceFiles: false, - testFilePattern: "/**/*.Tests.csproj", // omit integration-tests in CI-Build + testFilePattern: "/**/*.Tests.csproj", // omit integration-tests in CI-Build repositoryOwner: "cake-contrib", gitterMessage: "@/all " + standardNotificationMessage, twitterMessage: standardNotificationMessage, @@ -21,5 +21,7 @@ BuildParameters.SetParameters( BuildParameters.PrintParameters(Context); ToolSettings.SetToolSettings(context: Context); +ToolSettings.SetToolPreprocessorDirectives( + reSharperTools: "#tool nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2022.2.4"); Build.RunDotNetCore(); diff --git a/src/CakeContrib.Guidelines.sln b/src/CakeContrib.Guidelines.sln index b2292ed..6be4bd6 100644 --- a/src/CakeContrib.Guidelines.sln +++ b/src/CakeContrib.Guidelines.sln @@ -20,6 +20,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tasks.IntegrationTests", "Tasks.IntegrationTests\Tasks.IntegrationTests.csproj", "{525F8FE4-8495-4BB2-910D-C5B8DD219DBF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Generators", "Generators\Generators.csproj", "{5E05B05A-83BE-4D31-B7A5-5515C4B5CA7F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -78,6 +80,18 @@ Global {525F8FE4-8495-4BB2-910D-C5B8DD219DBF}.Release|x64.Build.0 = Release|Any CPU {525F8FE4-8495-4BB2-910D-C5B8DD219DBF}.Release|x86.ActiveCfg = Release|Any CPU {525F8FE4-8495-4BB2-910D-C5B8DD219DBF}.Release|x86.Build.0 = Release|Any CPU + {5E05B05A-83BE-4D31-B7A5-5515C4B5CA7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E05B05A-83BE-4D31-B7A5-5515C4B5CA7F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E05B05A-83BE-4D31-B7A5-5515C4B5CA7F}.Debug|x64.ActiveCfg = Debug|Any CPU + {5E05B05A-83BE-4D31-B7A5-5515C4B5CA7F}.Debug|x64.Build.0 = Debug|Any CPU + {5E05B05A-83BE-4D31-B7A5-5515C4B5CA7F}.Debug|x86.ActiveCfg = Debug|Any CPU + {5E05B05A-83BE-4D31-B7A5-5515C4B5CA7F}.Debug|x86.Build.0 = Debug|Any CPU + {5E05B05A-83BE-4D31-B7A5-5515C4B5CA7F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E05B05A-83BE-4D31-B7A5-5515C4B5CA7F}.Release|Any CPU.Build.0 = Release|Any CPU + {5E05B05A-83BE-4D31-B7A5-5515C4B5CA7F}.Release|x64.ActiveCfg = Release|Any CPU + {5E05B05A-83BE-4D31-B7A5-5515C4B5CA7F}.Release|x64.Build.0 = Release|Any CPU + {5E05B05A-83BE-4D31-B7A5-5515C4B5CA7F}.Release|x86.ActiveCfg = Release|Any CPU + {5E05B05A-83BE-4D31-B7A5-5515C4B5CA7F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Generators/Commands/PackageReferencesCommand.cs b/src/Generators/Commands/PackageReferencesCommand.cs new file mode 100644 index 0000000..91eaea4 --- /dev/null +++ b/src/Generators/Commands/PackageReferencesCommand.cs @@ -0,0 +1,242 @@ +using System.ComponentModel; + +using Microsoft.Build.Construction; + +using Spectre.Console; +using Spectre.Console.Cli; + +namespace Generators.Commands; + +public class PackageReferencesCommand : Command +{ + private readonly IAnsiConsole console; + + public PackageReferencesCommand( + IAnsiConsole console) + { + this.console = console; + } + + public class Settings : CommandSettings + { + [CommandArgument(0, "")] + public string CakeSourcePath { get; init; } + + [CommandOption("-f|--flatList")] + [Description("Print only a flat list of the references, not a code fragment")] + public bool FlatList { get; init; } + + public Predicate[] ProjectsToSkip => new Predicate[] + { + x => x.Equals("Build.csproj", StringComparison.OrdinalIgnoreCase), + x => x.Equals("Cake.Cli.csproj", StringComparison.OrdinalIgnoreCase), + x => x.Contains(".Tests.", StringComparison.OrdinalIgnoreCase), + x => x.Equals("Cake.Tool.csproj", StringComparison.OrdinalIgnoreCase), + }; + + public Predicate[] ReferencesToSkip => new Predicate[] + { + x => x.Name.StartsWith("Basic.Reference.Assemblies", StringComparison.OrdinalIgnoreCase), + }; + } + + public override int Execute(CommandContext context, Settings settings) + { + var projects = Directory.GetFiles( + settings.CakeSourcePath, + "*.csproj", + new EnumerationOptions { RecurseSubdirectories = true, MatchCasing = MatchCasing.CaseInsensitive }); + + console.WriteLine($"Found {projects.Length} projects"); + + var references = new Dictionary(); + + foreach (string proj in projects) + { + var file = Path.GetFileName(proj); + if (settings.ProjectsToSkip.Any(p => p(file))) + { + console.MarkupLineInterpolated($"[silver]Skipping project: {file}[/]"); + continue; + } + + console.MarkupLineInterpolated($"Processing {file}"); + var refs = ProcessProject(proj, settings.ReferencesToSkip); + if (refs.Length > 0) + { + references.Add(file, refs); + } + } + + console.MarkupLine("Flattening references"); + var allRefs = references + .SelectMany(x => x.Value) + .DistinctBy(x => $"{x.Name}|{x.Version ?? "0"}") + .ToLookup(x => x.Name) + .OrderBy(x => x.Key) + .ToArray(); + + foreach (var multiRef in allRefs.Where(x => x.Count() > 1)) + { + console.MarkupLineInterpolated( + $"[silver]WARN: {multiRef.Key} has multiple versions: {string.Join(", ", multiRef.Select(x => x.Version))}"); + } + + var gitInfo = GetGitInformation(settings.CakeSourcePath); + var flatRefs = allRefs.ToDictionary(x => x.Key, x => x.First().Version); + if (settings.FlatList) + { + PrintFlatList(flatRefs, gitInfo); + } + else + { + PrintCodeFragment(flatRefs, gitInfo); + } + + return 0; + } + + private string? GetGitInformation(string path) + { + console.MarkupLine("checking git commit/tag"); + var gitCloneDir = path; + string gitDir; + do + { + gitDir = Path.Combine(gitCloneDir, ".git"); + if (Directory.Exists(gitDir)) + { + break; + } + + gitCloneDir = Directory.GetParent(gitCloneDir)?.FullName; + } while (gitCloneDir != null && Directory.Exists(gitCloneDir)); + + if (!Directory.Exists(gitDir)) + { + console.MarkupLineInterpolated($"[silver]No .git dir found.[/]"); + return null; + } + + var head = Path.Combine(gitDir, "HEAD"); + if (!File.Exists(head)) + { + console.MarkupLineInterpolated($"[silver]{head} is missing.[/]"); + return null; + } + + var commit = File.ReadAllText(head); + + console.MarkupLineInterpolated($"[silver]HEAD is at {commit}[/]"); + + var tagsDir = Path.Combine(gitDir, "refs", "tags"); + if (!Directory.Exists(tagsDir)) + { + console.MarkupLineInterpolated($"[silver]no tags dir available.[/]"); + return commit; + } + + var tags = Directory.GetFiles(tagsDir) + .Select(f => + { + var content = File.ReadAllText(f); + + return new { Tag = Path.GetFileNameWithoutExtension(f), Commit = content }; + }) + .Where(x => x.Commit.Equals(commit, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + + if (tags.Length == 0) + { + console.MarkupLineInterpolated($"[silver]no tag matches the current commit.[/]"); + return commit; + } + + if (tags.Length > 1) + { + console.MarkupLineInterpolated($"[silver]multiple tags match the current commit: {string.Join(", ", tags.Select(t => t.Tag))}[/]"); + } + + return tags.Last().Tag; + } + + private void PrintCodeFragment(IReadOnlyDictionary refs, string? gitTag) + { + var varName = "references"; + console.WriteLine(); + if (gitTag != null) + { + var pascalCaseGitTag = gitTag[..1].ToUpper() + gitTag[1..]; + console.WriteLine($"// parsed from Cake: {gitTag}"); + if (gitTag.Contains('.')) + { + varName = "Cake" + pascalCaseGitTag.Replace(".", ""); + } + else + { + varName = "CakeCommit_" + gitTag[..8]; + } + } + + console.WriteLine($"private static readonly Dictionary {varName} = new Dictionary"); + console.WriteLine("{"); + foreach ((string name, string? version) in refs) + { + console.WriteLine($" {{ \"{name}\", \"{version ?? "0"}\" }},"); + } + console.WriteLine("};"); + } + + private void PrintFlatList(IReadOnlyDictionary refs, string? gitTag) + { + if (gitTag != null) + { + console.WriteLine($"#### Cake {gitTag}"); + } + var t = new Table() + .AddColumn("Reference") + .AddColumn("Version") + .Border(TableBorder.Markdown); + foreach ((string name, string? version) in refs) + { + t.AddRow(name, version ?? string.Empty); + } + + console.Write(t); + } + + private PkgReference[] ProcessProject( + string fullFile, + Predicate[] referencesToSkip) + { + var project = ProjectRootElement.Open(fullFile); + if (project == null) + { + console.MarkupLineInterpolated($"[red]Failed to parse {Path.GetFileName(fullFile)}[/]"); + return Array.Empty(); + } + + var foo = project.ItemGroups + .SelectMany(g => g.Children) + .Where(c => c.ElementName.Equals("PackageReference", StringComparison.OrdinalIgnoreCase)) + .Cast() + .ToArray(); + var references = foo + .Select(x => new PkgReference + { + Name = x.Include, + Version = x.Metadata + .FirstOrDefault(y => y.Name.Equals("Version", StringComparison.OrdinalIgnoreCase))?.Value, + }) + .Where(x => !referencesToSkip.Any(p => p(x))) + .ToArray(); + + return references; + } + + public class PkgReference + { + public string Name { get; init; } = string.Empty; + public string? Version { get; init; } + } +} diff --git a/src/Generators/Generators.csproj b/src/Generators/Generators.csproj new file mode 100644 index 0000000..d8f93c8 --- /dev/null +++ b/src/Generators/Generators.csproj @@ -0,0 +1,20 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/src/Generators/Program.cs b/src/Generators/Program.cs new file mode 100644 index 0000000..2299b43 --- /dev/null +++ b/src/Generators/Program.cs @@ -0,0 +1,13 @@ +using Generators.Commands; + +using Spectre.Console.Cli; + +var app = new CommandApp(); +app.Configure(config => +{ + config.AddCommand("package-references") + .WithAlias("packagereferences") + .WithDescription("parses package references of Cake (Core/Common) and creates a dictionary to copy into the code."); +}); + +return app.Run(args); diff --git a/src/Guidelines/build/CakeContrib.Guidelines.targets b/src/Guidelines/build/CakeContrib.Guidelines.targets index b72d121..a1f22ac 100644 --- a/src/Guidelines/build/CakeContrib.Guidelines.targets +++ b/src/Guidelines/build/CakeContrib.Guidelines.targets @@ -12,6 +12,7 @@ + + + + + + + + + + diff --git a/src/Tasks.IntegrationTests/E2eTests.cs b/src/Tasks.IntegrationTests/E2eTests.cs index 7339429..44e33b9 100644 --- a/src/Tasks.IntegrationTests/E2eTests.cs +++ b/src/Tasks.IntegrationTests/E2eTests.cs @@ -60,7 +60,7 @@ public void PackageIcon_Tag_missing_results_in_CCG0001_error() var output = result.WarningLines .First(x => x.IndexOf("!FOR-TEST!:", StringComparison.OrdinalIgnoreCase) > -1); output = output.Substring(output.IndexOf("!FOR-TEST!:", StringComparison.OrdinalIgnoreCase)+11); - output.ShouldBe("icon.png"); + output.ShouldStartWith("icon.png"); } [Fact] @@ -315,7 +315,7 @@ public void ProjectType_Default_Is_Addin() var output = result.WarningLines .First(x => x.IndexOf("!FOR-TEST!:", StringComparison.OrdinalIgnoreCase) > -1); output = output.Substring(output.IndexOf("!FOR-TEST!:", StringComparison.OrdinalIgnoreCase)+11); - output.ShouldBe("Addin", StringCompareShould.IgnoreCase); + output.ShouldStartWith("Addin"); } [Fact] @@ -343,7 +343,7 @@ public void ProjectType_Manually_Set_Is_Not_Overridden() var output = result.WarningLines .First(x => x.IndexOf("!FOR-TEST!:", StringComparison.OrdinalIgnoreCase) > -1); output = output.Substring(output.IndexOf("!FOR-TEST!:", StringComparison.OrdinalIgnoreCase)+11); - output.ShouldBe("MyCustomProjectType"); + output.ShouldStartWith("MyCustomProjectType"); } [Fact] @@ -351,6 +351,7 @@ public void ProjectType_When_Assembly_Is_Module_Is_Module() { // given fixture.WithAssemblyName("Cake.Buildsystems.Module"); + fixture.WithTargetFrameworks("netstandard2.0"); fixture.WithCustomContent(@" x.IndexOf("!FOR-TEST!:", StringComparison.OrdinalIgnoreCase) > -1); output = output.Substring(output.IndexOf("!FOR-TEST!:", StringComparison.OrdinalIgnoreCase)+11); - output.ShouldBe("Module", StringCompareShould.IgnoreCase); + output.ShouldStartWith("Module"); } [Fact] public void ProjectType_When_PackageId_Is_Module_Is_Module() { // given + fixture.WithTargetFrameworks("netstandard2.0"); fixture.WithCustomContent(@" Cake.Buildsystems.Module @@ -396,7 +398,7 @@ public void ProjectType_When_PackageId_Is_Module_Is_Module() var output = result.WarningLines .First(x => x.IndexOf("!FOR-TEST!:", StringComparison.OrdinalIgnoreCase) > -1); output = output.Substring(output.IndexOf("!FOR-TEST!:", StringComparison.OrdinalIgnoreCase)+11); - output.ShouldBe("Module", StringCompareShould.IgnoreCase); + output.ShouldStartWith("Module"); } [Fact] @@ -421,7 +423,7 @@ public void ProjectType_When_Assembly_Is_Not_Module_Is_Addin() var output = result.WarningLines .First(x => x.IndexOf("!FOR-TEST!:", StringComparison.OrdinalIgnoreCase) > -1); output = output.Substring(output.IndexOf("!FOR-TEST!:", StringComparison.OrdinalIgnoreCase)+11); - output.ShouldBe("Addin", StringCompareShould.IgnoreCase); + output.ShouldStartWith("Addin"); } [Fact] @@ -486,14 +488,18 @@ public void Packaging_Should_Add_The_Icon_As_Link_And_Pack_True() [Theory] [InlineData("Cake.7zip", "/../images/cake-contrib-addin-medium.png")] [InlineData("Cake.Recipe", "/../images/cake-contrib-recipe-medium.png")] - [InlineData("Cake.Buildsystems.Module", "/../images/cake-contrib-module-medium.png")] + [InlineData("Cake.Buildsystems.Module", "/../images/cake-contrib-module-medium.png", true)] [InlineData("Polly", "/../images/cake-contrib-community-medium.png")] - public void Packaging_Different_Types_Should_Add_The_Correct_Icon(string assemblyName, string expectedFileName) + public void Packaging_Different_Types_Should_Add_The_Correct_Icon(string assemblyName, string expectedFileName, bool isModule = false) { // given fixture.WithoutPackageIcon(); fixture.WithoutPackageIconUrl(); fixture.WithAssemblyName(assemblyName); + if (isModule) + { + fixture.WithTargetFrameworks("netstandard2.0"); + } fixture.WithCustomContent(@" true @@ -521,14 +527,18 @@ public void Packaging_Different_Types_Should_Add_The_Correct_Icon(string assembl [Theory] [InlineData("Cake.7zip", "cake-contrib/graphics/png/addin/cake-contrib-addin-medium.png")] [InlineData("Cake.Recipe", "cake-contrib/graphics/png/recipe/cake-contrib-recipe-medium.png")] - [InlineData("Cake.Buildsystems.Module", "cake-contrib/graphics/png/module/cake-contrib-module-medium.png")] + [InlineData("Cake.Buildsystems.Module", "cake-contrib/graphics/png/module/cake-contrib-module-medium.png", true)] [InlineData("Polly", "cake-contrib/graphics/png/community/cake-contrib-community-medium.png")] - public void Packaging_Different_Types_Should_Add_The_Correct_IconUrl_To_Properties(string assemblyName, string expectedUrl) + public void Packaging_Different_Types_Should_Add_The_Correct_IconUrl_To_Properties(string assemblyName, string expectedUrl, bool isModule = false) { // given fixture.WithoutPackageIcon(); fixture.WithoutPackageIconUrl(); fixture.WithAssemblyName(assemblyName); + if (isModule) + { + fixture.WithTargetFrameworks("netstandard2.0"); + } fixture.WithCustomContent(@" true @@ -579,6 +589,7 @@ public void Missing_Module_Tag_Should_Raise_CCG0008_For_Modules() Cake.Buildsystems.Module "); fixture.WithTags("cake build cake-build script"); + fixture.WithTargetFrameworks("netstandard2.0"); // when var result = fixture.Run(); diff --git a/src/Tasks.IntegrationTests/Tasks.IntegrationTests.csproj b/src/Tasks.IntegrationTests/Tasks.IntegrationTests.csproj index 20599ff..174be28 100644 --- a/src/Tasks.IntegrationTests/Tasks.IntegrationTests.csproj +++ b/src/Tasks.IntegrationTests/Tasks.IntegrationTests.csproj @@ -1,7 +1,7 @@ - netcoreapp2.1;netcoreapp3.1; + net6.0;net7.0; $(TargetFrameworks);net472 CakeContrib.Guidelines.Tasks.IntegrationTests CakeContrib.Guidelines.Tasks.IntegrationTests @@ -16,26 +16,26 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + runtime; build; native; contentfiles; analyzers all @@ -47,4 +47,4 @@ - \ No newline at end of file + diff --git a/src/Tasks.Tests/CheckCakeInternalReferencesTests.cs b/src/Tasks.Tests/CheckCakeInternalReferencesTests.cs new file mode 100644 index 0000000..8bfc5ec --- /dev/null +++ b/src/Tasks.Tests/CheckCakeInternalReferencesTests.cs @@ -0,0 +1,176 @@ +using System; +using System.Linq; + +using CakeContrib.Guidelines.Tasks.Tests.Fixtures; + +using Shouldly; + +using Xunit; + +namespace CakeContrib.Guidelines.Tasks.Tests +{ + public class CheckCakeInternalReferencesTests + { + private const string CcgRule10 = "CCG0010"; + + [Fact] + public void Should_Warn_If_NuGet_Is_Referenced_In_The_Wrong_Version() + { + // given + var fixture = new CheckCakeInternalReferencesFixture(); + fixture.WithReference("NuGet.Common", "5.11.0"); + + // when + fixture.Execute(); + + // then + fixture.BuildEngine.WarningEvents.Count.ShouldBe(1); + fixture.BuildEngine.WarningEvents.First().Code.ShouldBe(CcgRule10); + fixture.BuildEngine.WarningEvents.First().Message.ShouldContain("6.3.1"); // the required version for NuGet.Common + } + + [Fact] + public void Should_Not_Warn_If_NuGet_Is_Referenced_In_The_Correct_Version() + { + // given + var fixture = new CheckCakeInternalReferencesFixture(); + fixture.WithReference("NuGet.Common", "6.3.1"); + + // when + fixture.Execute(); + + // then + fixture.BuildEngine.WarningEvents.Count.ShouldBe(0); + } + + [Fact] + public void Should_Warn_If_NuGet_Is_Referenced_Not_Private() + { + // given + var fixture = new CheckCakeInternalReferencesFixture(); + fixture.WithReference("NuGet.Common", "6.3.1", string.Empty); + + // when + fixture.Execute(); + + // then + fixture.BuildEngine.WarningEvents.Count.ShouldBe(1); + fixture.BuildEngine.WarningEvents.First().Code.ShouldBe(CcgRule10); + fixture.BuildEngine.WarningEvents.First().Message.ShouldContain("privateAssets"); + } + + [Fact] + public void Should_Not_Warn_If_NuGet_Is_Referenced_In_The_Wrong_Version_But_Cake_Is_Not_Referenced() + { + // given + var fixture = new CheckCakeInternalReferencesFixture(); + fixture.WithReference("NuGet.Common", "5.11.0"); + fixture.WithCakeVersion(string.Empty); + + // when + fixture.Execute(); + + // then + fixture.BuildEngine.WarningEvents.Count.ShouldBe(0); + } + + [Fact] + public void Should_Not_Warn_If_NuGet_Is_Referenced_In_The_Wrong_Version_But_Project_Is_Recipe() + { + // given + var fixture = new CheckCakeInternalReferencesFixture(); + fixture.WithReference("NuGet.Common", "5.11.0"); + fixture.WithProjectType(CakeProjectType.Recipe); + + // when + fixture.Execute(); + + // then + fixture.BuildEngine.WarningEvents.Count.ShouldBe(0); + } + + [Fact] + public void Should_Not_Warn_If_NuGet_Is_Referenced_In_The_Wrong_Version_But_Cake_Version_Is_Invalid() + { + // given + var fixture = new CheckCakeInternalReferencesFixture(); + fixture.WithReference("NuGet.Common", "5.11.0"); + fixture.WithCakeVersion("a.b.c"); + + // when + fixture.Execute(); + + // then + fixture.BuildEngine.WarningEvents.Count.ShouldBe(0); + } + + [Fact] + public void Should_Not_Warn_If_NuGet_Is_Referenced_With_A_Broken_Version() + { + // given + var fixture = new CheckCakeInternalReferencesFixture(); + fixture.WithReference("NuGet.Common", "a.b.c"); + + // when + fixture.Execute(); + + // then + fixture.BuildEngine.WarningEvents.Count.ShouldBe(0); + } + + [Fact] + public void Should_Warn_If_NuGet_Is_Referenced_In_The_Wrong_Version_Even_If_Cake_Is_A_Preview_Version() + { + // given + var fixture = new CheckCakeInternalReferencesFixture(); + fixture.WithReference("NuGet.Common", "5.11.0"); + fixture.WithReference("Cake.Core", "3.0.0-preview1"); + fixture.WithCakeVersion(string.Empty); + + // when + fixture.Execute(); + + // then + fixture.BuildEngine.WarningEvents.Count.ShouldBe(1); + fixture.BuildEngine.WarningEvents.First().Code.ShouldBe(CcgRule10); + fixture.BuildEngine.WarningEvents.First().Message.ShouldContain("6.3.1"); // the required version for NuGet.Common + } + + [Fact] + public void Should_Warn_If_Cake_Version_is_Out_Of_Range() + { + // given + var fixture = new CheckCakeInternalReferencesFixture(); + fixture.WithReference("NuGet.Common", "5.11.0"); + fixture.WithReference("Cake.Core", "99.0.0"); + fixture.WithCakeVersion(string.Empty); + + // when + fixture.Execute(); + + // then + fixture.BuildEngine.WarningEvents.Count.ShouldBe(1); + fixture.BuildEngine.WarningEvents.First().Code.ShouldBe(CcgRule10); + fixture.BuildEngine.WarningEvents.First().Message.ShouldNotContain("6.3.1"); + fixture.BuildEngine.WarningEvents.First().Message.ShouldContain("no matching list of Cake provided references could be found"); + } + + [Fact] + public void Should_Warn_If_Newtonsoft_Is_Wrong_For_Cake_4() + { + // given + var fixture = new CheckCakeInternalReferencesFixture(); + fixture.WithReference("Newtonsoft.Json", "13.0.1"); + fixture.WithReference("Cake.Core", "4.0.0"); + fixture.WithCakeVersion(string.Empty); + + // when + fixture.Execute(); + + // then + fixture.BuildEngine.WarningEvents.Count.ShouldBe(1); + fixture.BuildEngine.WarningEvents.First().Code.ShouldBe(CcgRule10); + fixture.BuildEngine.WarningEvents.First().Message.ShouldContain("13.0.3"); // the required version for NuGet.Common + } + } +} diff --git a/src/Tasks.Tests/Extensions/ExtensionTests.cs b/src/Tasks.Tests/Extensions/VersionExtensionTests.cs similarity index 57% rename from src/Tasks.Tests/Extensions/ExtensionTests.cs rename to src/Tasks.Tests/Extensions/VersionExtensionTests.cs index 0b98c74..bdf93f5 100644 --- a/src/Tasks.Tests/Extensions/ExtensionTests.cs +++ b/src/Tasks.Tests/Extensions/VersionExtensionTests.cs @@ -66,5 +66,61 @@ public static IEnumerable GetLessThanData() yield return new object[] { new Version(1, 2, 3), new Version(1, 2, 2), false }; yield return new object[] { new Version(1, 2, 2), new Version(1, 2, 3), true }; } + + [Theory] + [MemberData(nameof(GetExceptionsData))] + public void ExceptionsTheory(Action action, Type expectedException) + { + Should.Throw(() => + { + action(); + }, expectedException); + } + + public static IEnumerable GetExceptionsData() + { + var noVersion = (Version)null; + var someVersion = new Version(1, 2, 3); + yield return new object[] + { + (Action)(() => noVersion.GreaterEqual(someVersion)), + typeof(ArgumentNullException), + }; + yield return new object[] + { + (Action)(() => (someVersion).GreaterEqual(noVersion)), + typeof(ArgumentNullException), + }; + yield return new object[] + { + (Action)(() => noVersion.GreaterThan(someVersion)), + typeof(ArgumentNullException), + }; + yield return new object[] + { + (Action)(() => someVersion.GreaterThan(noVersion)), + typeof(ArgumentNullException), + }; + yield return new object[] + { + (Action)(() => noVersion.LessEqual(someVersion)), + typeof(ArgumentNullException), + }; + yield return new object[] + { + (Action)(() => (someVersion).LessEqual(noVersion)), + typeof(ArgumentNullException), + }; + yield return new object[] + { + (Action)(() => noVersion.LessThan(someVersion)), + typeof(ArgumentNullException), + }; + yield return new object[] + { + (Action)(() => someVersion.LessThan(noVersion)), + typeof(ArgumentNullException), + }; + } } } diff --git a/src/Tasks.Tests/Fixtures/CheckCakeInternalReferencesFixture.cs b/src/Tasks.Tests/Fixtures/CheckCakeInternalReferencesFixture.cs new file mode 100644 index 0000000..a0e9a2e --- /dev/null +++ b/src/Tasks.Tests/Fixtures/CheckCakeInternalReferencesFixture.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; + +using Microsoft.Build.Framework; + +namespace CakeContrib.Guidelines.Tasks.Tests.Fixtures +{ + public class CheckCakeInternalReferencesFixture : BaseBuildFixture + { + private readonly List references; + + public CheckCakeInternalReferencesFixture() + { + Task.ProjectType = CakeProjectType.Addin.ToString(); + Task.CakeVersion = "3.0.0"; + Task.ProjectFile = "some.project.csproj"; + references = new List(); + } + + public void WithNoWarn(params string[] rules) + { + Task.NoWarn = rules; + } + + public void WithWarningsAsErrors(params string[] rules) + { + Task.WarningsAsErrors = rules; + } + + public void WithReference(string referenceName, string version, string privateAssets = "all") + { + var metadata = new Dictionary + { + { "version", version }, + { "privateAssets", privateAssets }, + }; + + var reference = GetMockTaskItem(referenceName, metadata); + references.Add(reference.Object); + } + + public override bool Execute() + { + Task.References = references.ToArray(); + return base.Execute(); + } + + public void WithProjectType(CakeProjectType type) + { + Task.ProjectType = type.ToString(); + } + + public void WithCakeVersion(string version) + { + Task.CakeVersion = version; + } + } +} diff --git a/src/Tasks.Tests/TargetFrameworkVersionIgnoreCaseComparerTests.cs b/src/Tasks.Tests/TargetFrameworkVersionIgnoreCaseComparerTests.cs new file mode 100644 index 0000000..eaef1c3 --- /dev/null +++ b/src/Tasks.Tests/TargetFrameworkVersionIgnoreCaseComparerTests.cs @@ -0,0 +1,33 @@ +using System.Linq; + +using CakeContrib.Guidelines.Tasks.Tests.Fixtures; + +using Shouldly; + +using Xunit; + +namespace CakeContrib.Guidelines.Tasks.Tests +{ + public class TargetFrameworkVersionIgnoreCaseComparerTests + { + private TargetFrameworkVersions.IgnoreCaseComparer sut = new TargetFrameworkVersions.IgnoreCaseComparer(); + + [Theory] + [InlineData(null, "x", false)] + [InlineData("x", null, false)] + [InlineData("someValue", "someOtherValue", false)] + [InlineData("someValue", "SoMeVaLuE", true)] + [InlineData("someValue", "someValue", true)] + [InlineData(null, null, true)] + public void NormalUsageTheory(string lhs, string rhs, bool expected) + { + // given + + // when + var actual = sut.Equals(lhs, rhs); + + // then + actual.ShouldBe(expected); + } + } +} diff --git a/src/Tasks.Tests/TargetFrameworkVersionsTests.cs b/src/Tasks.Tests/TargetFrameworkVersionsTests.cs index b2bbc31..4c8ef4e 100644 --- a/src/Tasks.Tests/TargetFrameworkVersionsTests.cs +++ b/src/Tasks.Tests/TargetFrameworkVersionsTests.cs @@ -19,6 +19,7 @@ public class TargetFrameworkVersionsTests private const string Net50 = "net5.0"; private const string Net60 = "net6.0"; private const string Net70 = "net7.0"; + private const string Net80 = "net8.0"; [Fact] public void Should_Error_If_RequiredTargetFramework_Is_Not_Targeted() @@ -335,6 +336,39 @@ public static IEnumerable Should_Error_If_RequiredTargetFramework_Is_N yield return new object[] { new[] { Net60, Net70 }, false, string.Empty }; } + [Theory] + [MemberData(nameof(Should_Error_If_RequiredTargetFramework_Is_Not_Targeted_Cake_4_Data))] + public void Should_Error_If_RequiredTargetFramework_Is_Not_Targeted_Cake_4(string[] targetFrameworks, bool expectedError, string missingTargetFramework) + { + // given + var fixture = new TargetFrameworkVersionsFixture(); + fixture.WithCakeCoreReference(4); + fixture.WithTargetFrameworks(targetFrameworks); + + // when + fixture.Execute(); + + // then + if (expectedError) + { + fixture.BuildEngine.ErrorEvents.Count.ShouldBe(1); + fixture.BuildEngine.ErrorEvents.First().Message.ShouldContain(missingTargetFramework); + } + else + { + fixture.BuildEngine.ErrorEvents.Count.ShouldBe(0); + } + } + + public static IEnumerable Should_Error_If_RequiredTargetFramework_Is_Not_Targeted_Cake_4_Data() + { + yield return new object[] { Array.Empty(), true, Net60 }; + yield return new object[] { new[] { Net60 }, true, Net70 }; + yield return new object[] { new[] { Net70 }, true, Net60 }; + yield return new object[] { new[] { Net60, Net70 }, true, Net80 }; + yield return new object[] { new[] { Net60, Net70, Net80 }, false, string.Empty }; + } + [Fact] public void Should_Error_If_RequiredTargetFramework_Is_Not_Targeted_Cake_2_Module() { @@ -367,6 +401,22 @@ public void Should_Error_If_RequiredTargetFramework_Is_Not_Targeted_Cake_3_Modul fixture.BuildEngine.ErrorEvents.First().Message.ShouldContain(Net60); } + [Fact] + public void Should_Error_If_RequiredTargetFramework_Is_Not_Targeted_Cake_4_Module() + { + // given + var fixture = new TargetFrameworkVersionsFixture(); + fixture.WithProjectType("module"); + fixture.WithCakeCoreReference("4.0.0"); + + // when + fixture.Execute(); + + // then + fixture.BuildEngine.ErrorEvents.Count.ShouldBe(1); + fixture.BuildEngine.ErrorEvents.First().Message.ShouldContain(Net60); + } + [Fact] public void Should_Error_If_RequiredTargetFramework_Is_Not_Targeted_Module_Explicit_Version() { @@ -382,5 +432,22 @@ public void Should_Error_If_RequiredTargetFramework_Is_Not_Targeted_Module_Expli fixture.BuildEngine.ErrorEvents.Count.ShouldBe(1); fixture.BuildEngine.ErrorEvents.First().Message.ShouldContain(NetStandard20); } + + [Fact] + public void Should_Error_If_Additional_TargetFrameworks_Are_Supplied_For_Module() + { + // given + var fixture = new TargetFrameworkVersionsFixture(); + fixture.WithProjectType("module"); + fixture.WithCakeCoreReference("3.0.0"); + fixture.WithTargetFrameworks("net6.0", "net7.0"); + + // when + fixture.Execute(); + + // then + fixture.BuildEngine.ErrorEvents.Count.ShouldBe(1); + fixture.BuildEngine.ErrorEvents.First().Message.ShouldContain(Net70); + } } } diff --git a/src/Tasks.Tests/Tasks.Tests.csproj b/src/Tasks.Tests/Tasks.Tests.csproj index 9678dba..611d288 100644 --- a/src/Tasks.Tests/Tasks.Tests.csproj +++ b/src/Tasks.Tests/Tasks.Tests.csproj @@ -1,7 +1,13 @@ - netcoreapp2.1;netcoreapp3.1; + + netcoreapp3.1 + net6.0;net7.0; $(TargetFrameworks);net472 CakeContrib.Guidelines.Tasks.Tests CakeContrib.Guidelines.Tasks.Tests @@ -16,26 +22,26 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + runtime; build; native; contentfiles; analyzers all diff --git a/src/Tasks/CakeVersions.cs b/src/Tasks/CakeVersions.cs new file mode 100644 index 0000000..8fe22af --- /dev/null +++ b/src/Tasks/CakeVersions.cs @@ -0,0 +1,25 @@ +using System; + +namespace CakeContrib.Guidelines.Tasks +{ + internal static class CakeVersions + { + // Cake 0.26.0 + internal static readonly Version Vo26 = new Version(0, 26, 0); + + // Cake 1.0.0 + internal static readonly Version V1 = new Version(1, 0, 0); + + // Cake 2.0.0 + internal static readonly Version V2 = new Version(2, 0, 0); + + // Cake 3.0.0 + internal static readonly Version V3 = new Version(3, 0, 0); + + // Cake 4.0.0 + public static readonly Version V4 = new Version(4, 0, 0); + + // The next, currently non-existing cake version + public static Version VNext = new Version(5, 0, 0); + } +} diff --git a/src/Tasks/CheckCakeInternalReferences.cs b/src/Tasks/CheckCakeInternalReferences.cs new file mode 100644 index 0000000..42568b6 --- /dev/null +++ b/src/Tasks/CheckCakeInternalReferences.cs @@ -0,0 +1,311 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using CakeContrib.Guidelines.Tasks.Extensions; + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace CakeContrib.Guidelines.Tasks +{ + /// + /// The Task to check for cake internal package references. + /// For the guideline . + /// + public class CheckCakeInternalReferences : Task + { + private const int CcgRule = 10; + +#if DEBUG + private const MessageImportance LogLevel = MessageImportance.High; +#else + private const MessageImportance LogLevel = MessageImportance.Low; +#endif + + // parsed from Cake: v0.38 + private static readonly Dictionary CakeV038 = new Dictionary + { + { "Autofac", "4.9.4" }, + { "Microsoft.CodeAnalysis.CSharp.Scripting", "3.6.0" }, + { "Microsoft.CSharp", "4.5.0" }, + { "Microsoft.DotNet.PlatformAbstractions", "3.1.0" }, + { "Microsoft.NETCore.Platforms", "3.1.0" }, + { "Microsoft.Win32.Registry", "4.4.0" }, + { "Newtonsoft.Json", "12.0.2" }, + { "NuGet.Common", "5.4.0" }, + { "NuGet.Frameworks", "5.4.0" }, + { "NuGet.Packaging", "5.4.0" }, + { "NuGet.Protocol", "5.4.0" }, + { "NuGet.Resolver", "5.4.0" }, + { "NuGet.Versioning", "5.4.0" }, + { "xunit", "2.4.1" }, + }; + + // parsed from Cake: v1.0.0 + private static readonly Dictionary CakeV10 = new Dictionary + { + { "Autofac", "6.1.0" }, + { "Microsoft.CodeAnalysis.CSharp.Scripting", "3.9.0-1.final" }, + { "Microsoft.CSharp", "4.7.0" }, + { "Microsoft.DotNet.PlatformAbstractions", "3.1.6" }, + { "Microsoft.Extensions.DependencyInjection", "5.0.1" }, + { "Microsoft.NETCore.Platforms", "5.0.0" }, + { "Microsoft.Win32.Registry", "5.0.0" }, + { "Newtonsoft.Json", "12.0.3" }, + { "NuGet.Common", "5.8.0" }, + { "NuGet.Frameworks", "5.8.0" }, + { "NuGet.Packaging", "5.8.0" }, + { "NuGet.Protocol", "5.8.0" }, + { "NuGet.Resolver", "5.8.0" }, + { "NuGet.Versioning", "5.8.0" }, + { "System.Collections.Immutable", "5.0.0" }, + { "System.Reflection.Metadata", "5.0.0" }, + { "xunit", "2.4.1" }, + }; + + // parsed from Cake: v2.0.0 + private static readonly Dictionary CakeV20 = new Dictionary + { + { "Autofac", "6.3.0" }, + { "Microsoft.CodeAnalysis.CSharp.Scripting", "4.0.1" }, + { "Microsoft.CSharp", "4.7.0" }, + { "Microsoft.DotNet.PlatformAbstractions", "3.1.6" }, + { "Microsoft.Extensions.DependencyInjection", "6.0.0" }, + { "Microsoft.NETCore.Platforms", "6.0.0" }, + { "Microsoft.Win32.Registry", "5.0.0" }, + { "Newtonsoft.Json", "13.0.1" }, + { "NuGet.Common", "5.11.0" }, + { "NuGet.Frameworks", "5.11.0" }, + { "NuGet.Packaging", "5.11.0" }, + { "NuGet.Protocol", "5.11.0" }, + { "NuGet.Resolver", "5.11.0" }, + { "NuGet.Versioning", "5.11.0" }, + { "System.Collections.Immutable", "6.0.0" }, + { "System.Reflection.Metadata", "6.0.0" }, + { "xunit", "2.4.1" }, + }; + + // parsed from Cake: v3.0.0 + private static readonly Dictionary CakeV30 = new Dictionary + { + { "Autofac", "6.4.0" }, + { "Microsoft.CodeAnalysis.CSharp.Scripting", "4.4.0-4.final" }, + { "Microsoft.CSharp", "4.7.0" }, + { "Microsoft.Extensions.DependencyInjection", "7.0.0" }, + { "Microsoft.NETCore.Platforms", "7.0.0" }, + { "Microsoft.Win32.Registry", "5.0.0" }, + { "Newtonsoft.Json", "13.0.1" }, + { "NuGet.Common", "6.3.1" }, + { "NuGet.Frameworks", "6.3.1" }, + { "NuGet.Packaging", "6.3.1" }, + { "NuGet.Protocol", "6.3.1" }, + { "NuGet.Resolver", "6.3.1" }, + { "NuGet.Versioning", "6.3.1" }, + { "System.Collections.Immutable", "7.0.0" }, + { "System.Reflection.Metadata", "7.0.0" }, + { "xunit", "2.4.2" }, + }; + + // parsed from Cake: v4.0 + private static readonly Dictionary CakeV40 = new Dictionary + { + { "Autofac", "7.1.0" }, + { "Microsoft.CodeAnalysis.CSharp.Scripting", "4.8.0-3.final" }, + { "Microsoft.CSharp", "4.7.0" }, + { "Microsoft.Extensions.DependencyInjection", "8.0.0" }, + { "Microsoft.NETCore.Platforms", "7.0.4" }, + { "Microsoft.Win32.Registry", "5.0.0" }, + { "Newtonsoft.Json", "13.0.3" }, + { "NuGet.Common", "6.7.0" }, + { "NuGet.Frameworks", "6.7.0" }, + { "NuGet.Packaging", "6.7.0" }, + { "NuGet.Protocol", "6.7.0" }, + { "NuGet.Resolver", "6.7.0" }, + { "NuGet.Versioning", "6.7.0" }, + { "System.Collections.Immutable", "8.0.0" }, + { "System.Reflection.Metadata", "8.0.0" }, + { "xunit", "2.6.1" }, + }; + + private readonly Dictionary, Dictionary> allInternalReferences = + new Dictionary, Dictionary> + { + { + x => x.LessThan(CakeVersions.V1), + CakeV038 + }, + { + x => x.GreaterEqual(CakeVersions.V1) && x.LessThan(CakeVersions.V2), + CakeV10 + }, + { + x => x.GreaterEqual(CakeVersions.V2) && x.LessThan(CakeVersions.V3), + CakeV20 + }, + { + x => x.GreaterEqual(CakeVersions.V3) && x.LessThan(CakeVersions.V4), + CakeV30 + }, + { + x => x.GreaterEqual(CakeVersions.V4) && x.LessThan(CakeVersions.VNext), + CakeV40 + }, + }; + + /// + /// Gets or sets the ProjectType. + /// + [Required] + public string ProjectType { get; set; } + + /// + /// Gets or sets the References. + /// + [Required] + public ITaskItem[] References { get; set; } + + /// + /// Gets or sets the warnings that are suppressed. + /// + public string[] NoWarn { get; set; } + + /// + /// Gets or sets the warnings that should be raised as errors. + /// + public string[] WarningsAsErrors { get; set; } + + /// + /// Gets or sets the project file. + /// + public string ProjectFile { get; set; } + + /// + /// Gets or sets an explicit Cake version instead of doing Cake.Core detection. + /// + public string CakeVersion { get; set; } + + /// + public override bool Execute() + { + if (!CakeProjectType.IsOneOf(ProjectType, CakeProjectType.Addin, CakeProjectType.Module)) + { + Log.LogMessage( + LogLevel, + $"Internal Cake references will not be checked for {ProjectType} projects."); + return true; + } + + if (string.IsNullOrEmpty(CakeVersion)) + { + // find cake.core version + var cakeCore = + References?.FirstOrDefault(x => x.ToString().Equals("Cake.Core", StringComparison.OrdinalIgnoreCase)); + if (cakeCore == null) + { + Log.LogMessage( + LogLevel, + "Could not find Cake.Core reference. Internal Cake references will not be checked."); + return true; + } + + CakeVersion = cakeCore.GetMetadata("version"); + var prereleaseIndex = CakeVersion.IndexOf("-", StringComparison.Ordinal); + if (prereleaseIndex > -1) + { + var prerelease = CakeVersion; + CakeVersion = CakeVersion.Substring(0, prereleaseIndex); + Log.CcgTrace($"Cake.Core has a version of {prerelease}. Assuming a prerelease and correcting version to {CakeVersion}."); + } + } + else + { + Log.CcgTrace($"Cake version explicitly set to {CakeVersion}."); + } + + if (!Version.TryParse(CakeVersion, out Version version)) + { + Log.CcgTrace($"Cake version was {CakeVersion} which is not a valid version."); + return true; + } + + var internalReferences = allInternalReferences + .Where(x => x.Key.Invoke(version)) + .Select(x => x.Value) + .FirstOrDefault(); + + if (internalReferences == null) + { + Log.CcgWarning( + CcgRule, + ProjectFile, + $"Cake version was {CakeVersion} but no matching list of Cake provided references could be found.", + NoWarn, + WarningsAsErrors); + return true; + } + + var referencesInProject = References.Select(x => new + { + Name = x.ToString(), + Version = x.GetMetadata("version"), + IsPrivate = (x.GetMetadata("PrivateAssets")?.ToLower() ?? string.Empty) == "all", + }).ToArray(); + + foreach (var reference in referencesInProject) + { + var internalRef = internalReferences + .Where(x => reference.Name.Equals(x.Key, StringComparison.OrdinalIgnoreCase)) + .Select(x => new + { + Name = x.Key, + Version = x.Value, + }) + .FirstOrDefault(); + + if (internalRef == null) + { + continue; + } + + if (!Version.TryParse(reference.Version, out var localVersion)) + { + Log.LogMessage( + LogLevel, + $"The reference {reference.Name} had version {reference.Version}. The version could not be parsed!"); + continue; + } + + if (!Version.TryParse(internalRef.Version, out var cakeRefVersion)) + { + Log.LogMessage( + LogLevel, + $"The Cake v{CakeVersion} reference {internalRef.Name} had version {internalRef.Version}. The version could not be parsed!"); + continue; + } + + if (!localVersion.Equals(cakeRefVersion)) + { + Log.CcgWarning( + CcgRule, + ProjectFile, + $"{internalRef.Name} is provided by Cake {CakeVersion} in version {cakeRefVersion}. Do not reference a different version.", + NoWarn, + WarningsAsErrors); + } + + if (!reference.IsPrivate) + { + Log.CcgWarning( + CcgRule, + ProjectFile, + $"{internalRef.Name} is provided by Cake. It should have `PrivateAssets=\"all\"` set", + NoWarn, + WarningsAsErrors); + } + } + + return true; + } + } +} diff --git a/src/Tasks/RecommendedCakeVersion.cs b/src/Tasks/RecommendedCakeVersion.cs index c359894..1fe42ab 100644 --- a/src/Tasks/RecommendedCakeVersion.cs +++ b/src/Tasks/RecommendedCakeVersion.cs @@ -10,7 +10,7 @@ namespace CakeContrib.Guidelines.Tasks { /// - /// The Task to check for References for the guideline . + /// The Task to check for the recommended Cake Version . /// public class RecommendedCakeVersion : Task { diff --git a/src/Tasks/TargetFrameworkVersions.cs b/src/Tasks/TargetFrameworkVersions.cs index a0eaca8..3012b2b 100644 --- a/src/Tasks/TargetFrameworkVersions.cs +++ b/src/Tasks/TargetFrameworkVersions.cs @@ -28,11 +28,7 @@ public class TargetFrameworkVersions : Task private const string Net50 = "net5.0"; private const string Net60 = "net6.0"; private const string Net70 = "net7.0"; - - private static readonly Version Vo26 = new Version(0, 26, 0); - private static readonly Version V1 = new Version(1, 0, 0); - private static readonly Version V2 = new Version(2, 0, 0); - private static readonly Version V3 = new Version(3, 0, 0); + private const string Net80 = "net8.0"; private static readonly TargetsDefinitions DefaultTarget = new TargetsDefinitions { @@ -49,43 +45,55 @@ public class TargetFrameworkVersions : Task new Dictionary, TargetsDefinitions> { { - d => d.IsModuleProject && d.Version.LessThan(V2), + d => d.IsModuleProject && d.Version.LessThan(CakeVersions.V2), new TargetsDefinitions { - Name = "Module", + Name = "module and CakeVersion < 2.0.0", RequiredTargets = new[] { TargetsDefinition.From(NetStandard20) }, + AllowAdditionalTargets = false, } }, { - d => d.IsModuleProject && d.Version.GreaterEqual(V2) && d.Version.LessThan(V3), + d => d.IsModuleProject && d.Version.GreaterEqual(CakeVersions.V2) && d.Version.LessThan(CakeVersions.V3), new TargetsDefinitions { - Name = "Module", + Name = "Module and 2.0.0 <= CakeVersion < 3.0.0", RequiredTargets = new[] { TargetsDefinition.From(NetCore31) }, + AllowAdditionalTargets = false, + } + }, + { + d => d.IsModuleProject && d.Version.GreaterEqual(CakeVersions.V3) && d.Version.LessThan(CakeVersions.V4), + new TargetsDefinitions + { + Name = "Module and 3.0.0 <= CakeVersion < 4.0.0", + RequiredTargets = new[] { TargetsDefinition.From(Net60) }, + AllowAdditionalTargets = false, } }, { - d => d.IsModuleProject && d.Version.GreaterEqual(V3), + d => d.IsModuleProject && d.Version.GreaterEqual(CakeVersions.V4), new TargetsDefinitions { - Name = "Module", + Name = "Module and CakeVersion >= 4.0.0", RequiredTargets = new[] { TargetsDefinition.From(Net60) }, + AllowAdditionalTargets = false, } }, { - d => !d.IsModuleProject && d.Version.GreaterEqual(Vo26) && d.Version.LessThan(V1), + d => !d.IsModuleProject && d.Version.GreaterEqual(CakeVersions.Vo26) && d.Version.LessThan(CakeVersions.V1), new TargetsDefinitions { - Name = "0.26.0 <= x < 1.0.0", + Name = "not module and 0.26.0 <= CakeVersion < 1.0.0", RequiredTargets = new[] { TargetsDefinition.From(NetStandard20) }, SuggestedTargets = new[] { TargetsDefinition.From(Net461, Net46) }, } }, { - d => !d.IsModuleProject && d.Version.GreaterEqual(V1) && d.Version.LessThan(V2), + d => !d.IsModuleProject && d.Version.GreaterEqual(CakeVersions.V1) && d.Version.LessThan(CakeVersions.V2), new TargetsDefinitions { - Name = "1.0.0 <= x < 2.0.0", + Name = "not module and 1.0.0 <= CakeVersion < 2.0.0", RequiredTargets = new[] { TargetsDefinition.From(NetStandard20) }, SuggestedTargets = new[] { @@ -95,10 +103,10 @@ public class TargetFrameworkVersions : Task } }, { - d => !d.IsModuleProject && d.Version.GreaterEqual(V2) && d.Version.LessThan(V3), + d => !d.IsModuleProject && d.Version.GreaterEqual(CakeVersions.V2) && d.Version.LessThan(CakeVersions.V3), new TargetsDefinitions { - Name = "2.0.0 <= x < 3.0.0", + Name = "not module and 2.0.0 <= CakeVersion < 3.0.0", RequiredTargets = new[] { TargetsDefinition.From(NetCore31), @@ -109,10 +117,10 @@ public class TargetFrameworkVersions : Task } }, { - d => !d.IsModuleProject && d.Version.GreaterEqual(V3), + d => !d.IsModuleProject && d.Version.GreaterEqual(CakeVersions.V3) && d.Version.LessThan(CakeVersions.V4), new TargetsDefinitions { - Name = "x >= 3.0.0", + Name = "not module and 3.0.0 <= CakeVersion < 4.0.0", RequiredTargets = new[] { TargetsDefinition.From(Net60), @@ -121,6 +129,20 @@ public class TargetFrameworkVersions : Task SuggestedTargets = Array.Empty(), } }, + { + d => !d.IsModuleProject && d.Version.GreaterEqual(CakeVersions.V4), + new TargetsDefinitions + { + Name = "not module and CakeVersion >= 4.0.0", + RequiredTargets = new[] + { + TargetsDefinition.From(Net60), + TargetsDefinition.From(Net70), + TargetsDefinition.From(Net80), + }, + SuggestedTargets = Array.Empty(), + } + }, }; /// @@ -264,12 +286,14 @@ private bool Execute(TargetsDefinitions targets) allTargets.AddRange(TargetFrameworks.Select(x => x.ToString())); } + var usedTargetsRequiredOrSuggested = new List(); allTargets = allTargets.Distinct().ToList(); // first, check required targets Log.LogMessage( LogLevel, $"Comparing TargetFramework[s] ({string.Join(";", allTargets)}) to required: {string.Join(",", targets.RequiredTargets.Select(x => x.Name))}."); + var ignoreCaseComparer = new IgnoreCaseComparer(); foreach (var requiredTarget in targets.RequiredTargets) { @@ -283,12 +307,16 @@ private bool Execute(TargetsDefinitions targets) if (allTargets.Any(x => x.Equals(requiredTarget.Name, StringComparison.OrdinalIgnoreCase))) { + usedTargetsRequiredOrSuggested.Add(requiredTarget.Name); continue; } - var found = requiredTarget.Alternatives?.Any(alternative => allTargets.Contains(alternative)); - if (found.GetValueOrDefault(false)) + var alternatives = requiredTarget.Alternatives? + .Where(alternative => allTargets.Contains(alternative, ignoreCaseComparer)) + .ToArray() ?? Array.Empty(); + if (alternatives.Length > 0) { + usedTargetsRequiredOrSuggested.AddRange(alternatives); continue; } @@ -316,12 +344,16 @@ private bool Execute(TargetsDefinitions targets) if (allTargets.Any(x => x.Equals(suggestedTarget.Name, StringComparison.OrdinalIgnoreCase))) { + usedTargetsRequiredOrSuggested.Add(suggestedTarget.Name); continue; } - var found = suggestedTarget.Alternatives?.Any(alternative => allTargets.Contains(alternative)); - if (found.GetValueOrDefault(false)) + var alternatives = suggestedTarget.Alternatives? + .Where(alternative => allTargets.Contains(alternative, ignoreCaseComparer)) + .ToArray() ?? Array.Empty(); + if (alternatives.Length > 0) { + usedTargetsRequiredOrSuggested.AddRange(alternatives); continue; } @@ -333,7 +365,30 @@ private bool Execute(TargetsDefinitions targets) WarningsAsErrors); } - return true; + // are more TFMs, than the required or suggested allowed? + if (targets.AllowAdditionalTargets) + { + return true; + } + + Log.LogMessage( + LogLevel, + "No additional TargetFrameworks are allowed."); + + var additionalTargets = allTargets + .Except(usedTargetsRequiredOrSuggested, ignoreCaseComparer) + .ToArray(); + + if (additionalTargets.Length <= 0) + { + return true; + } + + Log.CcgError( + 7, + ProjectFile, + "No additional targets are allowed. However, the following additional targets were found: " + string.Join(", ", additionalTargets)); + return false; } private class TargetsDefinitions @@ -349,6 +404,8 @@ public TargetsDefinitions() public TargetsDefinition[] RequiredTargets { get; set; } public TargetsDefinition[] SuggestedTargets { get; set; } + + public bool AllowAdditionalTargets { get; set; } = true; } private class TargetsDefinition @@ -369,5 +426,28 @@ private class Differentiator internal bool IsModuleProject { get; set; } } + + public class IgnoreCaseComparer : IEqualityComparer + { + public bool Equals(string x, string y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (x == null) + { + return false; + } + + return x.Equals(y, StringComparison.OrdinalIgnoreCase); + } + + public int GetHashCode(string obj) + { + return obj.GetHashCode(); + } + } } } diff --git a/src/Tasks/Tasks.csproj b/src/Tasks/Tasks.csproj index c251b56..31732b6 100644 --- a/src/Tasks/Tasks.csproj +++ b/src/Tasks/Tasks.csproj @@ -14,14 +14,14 @@ - + compile; build; native; contentfiles; analyzers; buildtransitive compile; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -32,11 +32,11 @@ - + - +