diff --git a/.github/workflows/dependabot-cake.yml b/.github/workflows/dependabot-cake.yml new file mode 100644 index 0000000..f34a967 --- /dev/null +++ b/.github/workflows/dependabot-cake.yml @@ -0,0 +1,13 @@ +name: Run dependabot for cake +on: + workflow_dispatch: + schedule: + # run everyday at 6 + - cron: '0 6 * * *' + +jobs: + dependabot-cake: + runs-on: ubuntu-latest # linux, because this is a docker-action + steps: + - name: check/update cake dependencies + uses: nils-org/dependabot-cake-action@v1 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0b598f7..f007d44 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,6 @@ docs/input/tasks/* # Wyam related config.wyam.* + +# JetBrains Rider +.idea/ diff --git a/docs/input/guidelines/Analysers.md b/docs/input/guidelines/RecommendedReferences.md similarity index 94% rename from docs/input/guidelines/Analysers.md rename to docs/input/guidelines/RecommendedReferences.md index c996298..e824a01 100644 --- a/docs/input/guidelines/Analysers.md +++ b/docs/input/guidelines/RecommendedReferences.md @@ -21,8 +21,8 @@ To have consistency in code-style among the different tools/plugins the use of A Example-Files can be found at: -* [`stylecop.json`](./examples/StyleCopJson.md) -* [`.editorconfig`](./examples/Editorconfig.md) +* [`stylecop.json`](./examples/StyleCopJson) +* [`.editorconfig`](./examples/Editorconfig) ## Related rules diff --git a/docs/input/guidelines/TargetFramework.md b/docs/input/guidelines/TargetFramework.md new file mode 100644 index 0000000..f8bc333 --- /dev/null +++ b/docs/input/guidelines/TargetFramework.md @@ -0,0 +1,55 @@ +--- +Order: 4 +Title: Target Frameworks +--- + + + +## Table of Contents + +- [Goals](#goals) + - [Required / Suggested versions](#required--suggested-versions) +- [Related rules](#related-rules) +- [Usage](#usage) +- [Settings](#settings) + - [Opt-Out](#opt-out) + + + +## Goals + +As .NET Framework < 4.7.2 has issues with running .NET Standard assemblies, and Cake itself can run on .NET Framework 4.6.1 it is suggested to multi-target addins to `netstandard2.0` and `net461` to have the maximum compatibility. + +### Required / Suggested versions + +Depending on the referenced `Cake.Core`-version different target versions are required and/or suggested. +Missing a required target version will raise [CCG0007](../rules/ccg0007) as an error +while missing a suggested target version will raise [CCG0007](../rules/ccg0007) as a warning. + +* Cake.Core <= 0.33.0 + * Required: `netstandard2.0` + * Suggested: `net461` + * alternative: `net46` + +## Related rules + + * [CCG0007](../rules/ccg0007) + +## Usage + +Using this package automatically enables this guideline. + +## Settings + +### Opt-Out + +It it possible to opt-out of the check for target framework using the following setting: + +(*Keep in mind, though that it is not recommended to opt-out of this feature*) + +```xml + + + + +``` diff --git a/docs/input/rules/ccg0006.md b/docs/input/rules/ccg0006.md index f3c7e0e..7918565 100644 --- a/docs/input/rules/ccg0006.md +++ b/docs/input/rules/ccg0006.md @@ -4,7 +4,7 @@ Title: CCG0006 Description: Missing recommended configuraion-file --- - > No reference to `.editorconfig` found. Usage of `.editorconfig` is strongly recommended. + > No reference to `[fileName]` found. Usage of `[fileName]` is strongly recommended. @@ -23,20 +23,19 @@ This warning is raised, when a recommended configuration-file (i.e. `stylecop.js ## Description -Code-Formatting and layout should be properly configured. This is done using -blah and blah. +Code-Formatting and layout should be properly configured. This is done using `.editorconfig` and `stylecop.json`. ## How to fix violations -Add a balh and blah to the project. +Add the required files to the project. Example-Files can be found at: -* [`stylecop.json`](../guidelines/examples/StyleCopJson.md) -* [`.editorconfig`](../guidelines/examples/Editorconfig.md) +* [`stylecop.json`](../guidelines/examples/StyleCopJson) +* [`.editorconfig`](../guidelines/examples/Editorconfig) (Or opt-out of this rule, by setting `CakeContribGuidelinesOmitRecommendedConfigFile`) ## Related guidelines -* [Usage of Analysers](../guidelines/Analysers) \ No newline at end of file +* [Recommended References](../guidelines/RecommendedReferences) \ No newline at end of file diff --git a/docs/input/rules/ccg0007.md b/docs/input/rules/ccg0007.md new file mode 100644 index 0000000..27afffc --- /dev/null +++ b/docs/input/rules/ccg0007.md @@ -0,0 +1,43 @@ +--- +Order: 7 +Title: CCG0007 +Description: Missing recommended target +--- + + > Missing required target: netstandard2.0 + + + +## 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 is not targeted to a recommended target version. +Also, This could be raised as an error, if a required target version is not set. + +## Description + +Addins should be multi-targeted to `netstandard2.0` and `net461` to have the maximum compatibility. + +## How to fix violations + +Add the recommended target(s) to the project: + +```xml + + netstandard2.0;net472 + +``` + +(Or opt-out of this rule, by setting `CakeContribGuidelinesOmitTargetFramework`) + +## Related guidelines + +* [Target Frameworks](../guidelines/TargetFramework) \ No newline at end of file diff --git a/recipe.cake b/recipe.cake index 59360d1..9089caa 100644 --- a/recipe.cake +++ b/recipe.cake @@ -1,4 +1,4 @@ -#load nuget:?package=Cake.Recipe&version=2.0.0 +#load nuget:?package=Cake.Recipe&version=2.0.1 Environment.SetVariableNames(); diff --git a/src/CakeContrib.Guidelines.sln.DotSettings b/src/CakeContrib.Guidelines.sln.DotSettings new file mode 100644 index 0000000..61f5d6a --- /dev/null +++ b/src/CakeContrib.Guidelines.sln.DotSettings @@ -0,0 +1,6 @@ + + True + True + True + True + True \ No newline at end of file diff --git a/src/Guidelines/build/CakeContrib.Guidelines.targets b/src/Guidelines/build/CakeContrib.Guidelines.targets index da22dc5..645ec72 100644 --- a/src/Guidelines/build/CakeContrib.Guidelines.targets +++ b/src/Guidelines/build/CakeContrib.Guidelines.targets @@ -9,4 +9,5 @@ + diff --git a/src/Guidelines/build/TargetFrameworkVersions.targets b/src/Guidelines/build/TargetFrameworkVersions.targets new file mode 100644 index 0000000..611d042 --- /dev/null +++ b/src/Guidelines/build/TargetFrameworkVersions.targets @@ -0,0 +1,21 @@ + + + + + + + + + + + diff --git a/src/Tasks.IntegrationTests/E2eTests.cs b/src/Tasks.IntegrationTests/E2eTests.cs index 32cafe6..07d4f74 100644 --- a/src/Tasks.IntegrationTests/E2eTests.cs +++ b/src/Tasks.IntegrationTests/E2eTests.cs @@ -1,14 +1,14 @@ using System; using System.IO; -using CakeContrib.Guidelines.Tasks.Tests.Fixtures; +using CakeContrib.Guidelines.Tasks.IntegrationTests.Fixtures; using FluentAssertions; using Xunit; using Xunit.Abstractions; -namespace CakeContrib.Guidelines.Tasks.Tests +namespace CakeContrib.Guidelines.Tasks.IntegrationTests { // TODO: Writing things to disk is not deterministic... // TODO: Running Code-Coverage on the integration-tests breaks all tests @@ -194,5 +194,35 @@ public void Missing_file_editorconfig_results_in_CCG0006_warning() result.WarningLines.Should().Contain(l => l.IndexOf("CCG0006", StringComparison.Ordinal) > -1); result.WarningLines.Should().Contain(l => l.IndexOf(".editorconfig", StringComparison.Ordinal) > -1); } + + [Fact] + public void Missing_Required_Target_results_in_CCG0007_error() + { + // given + fixture.WithTargetFrameworks("net47"); + + // when + var result = fixture.Run(); + + // then + result.IsErrorExitCode.Should().BeTrue(); + result.ErrorLines.Should().Contain(l => l.IndexOf("CCG0007", StringComparison.Ordinal) > -1); + result.ErrorLines.Should().Contain(l => l.IndexOf("netstandard2.0", StringComparison.Ordinal) > -1); + } + + [Fact] + public void Missing_Suggested_Target_results_in_CCG0007_warning() + { + // given + fixture.WithTargetFrameworks("netstandard2.0"); + + // when + var result = fixture.Run(); + + // then + result.IsErrorExitCode.Should().BeFalse(); + result.WarningLines.Should().Contain(l => l.IndexOf("CCG0007", StringComparison.Ordinal) > -1); + result.WarningLines.Should().Contain(l => l.IndexOf("net461", StringComparison.Ordinal) > -1); + } } } diff --git a/src/Tasks.IntegrationTests/Fixtures/E2eTestFixture.cs b/src/Tasks.IntegrationTests/Fixtures/E2eTestFixture.cs index 7b814c3..aa7aa83 100644 --- a/src/Tasks.IntegrationTests/Fixtures/E2eTestFixture.cs +++ b/src/Tasks.IntegrationTests/Fixtures/E2eTestFixture.cs @@ -6,7 +6,7 @@ using Xunit.Abstractions; -namespace CakeContrib.Guidelines.Tasks.Tests.Fixtures +namespace CakeContrib.Guidelines.Tasks.IntegrationTests.Fixtures { public class E2eTestFixture : IDisposable { @@ -18,7 +18,8 @@ public class E2eTestFixture : IDisposable private bool hasStylecopJson = true; private bool hasStylecopReference = true; private bool hasEditorConfig = true; - private List customContent = new List(); + private readonly List customContent = new List(); + private string targetFrameworks = "netstandard2.0;net461"; public E2eTestFixture(string tempFolder, ITestOutputHelper logger) { @@ -41,7 +42,7 @@ private string WriteProject() - netstandard2.0 + {5} {2} @@ -95,7 +96,8 @@ private string WriteProject() targets.Item2, string.Join(Environment.NewLine, properties), string.Join(Environment.NewLine, items), - string.Join(Environment.NewLine, customContent))); + string.Join(Environment.NewLine, customContent), + targetFrameworks)); return csproj; } @@ -135,6 +137,11 @@ internal void WithoutFileEditorconfig() hasEditorConfig = false; } + internal void WithTargetFrameworks(string targetFrameworks) + { + this.targetFrameworks = targetFrameworks; + } + private Tuple GetTargetsToImport() { var codeBase = typeof(E2eTestFixture).Assembly.CodeBase; diff --git a/src/Tasks.IntegrationTests/Tasks.IntegrationTests.csproj b/src/Tasks.IntegrationTests/Tasks.IntegrationTests.csproj index e97ecb1..aeadf7d 100644 --- a/src/Tasks.IntegrationTests/Tasks.IntegrationTests.csproj +++ b/src/Tasks.IntegrationTests/Tasks.IntegrationTests.csproj @@ -28,9 +28,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Tasks.Tests/CheckPrivateAssetsOnReferencesTests.cs b/src/Tasks.Tests/CheckPrivateAssetsOnReferencesTests.cs index 657193f..7597e8a 100644 --- a/src/Tasks.Tests/CheckPrivateAssetsOnReferencesTests.cs +++ b/src/Tasks.Tests/CheckPrivateAssetsOnReferencesTests.cs @@ -45,7 +45,7 @@ public void Should_Error_If_Package_Has_Not_PrivateAssets() { // given var fixture = new CheckPrivateAssetsOnReferencesFixture(); - fixture.WithReferencedPackage("Cake.Core", ""); + fixture.WithReferencedPackage("Cake.Core"); fixture.WithPackageToCheck("Cake.Core"); // when @@ -60,7 +60,7 @@ public void Should_Log_Correct_ErrorCode_On_Error() { // given var fixture = new CheckPrivateAssetsOnReferencesFixture(); - fixture.WithReferencedPackage("Cake.Core", ""); + fixture.WithReferencedPackage("Cake.Core"); fixture.WithPackageToCheck("Cake.Core"); // when @@ -78,7 +78,7 @@ public void Should_Log_Correct_ErrorSourceFile_On_Error_With_Given_ProjectFile() // given const string projectFileName = "some.project.csproj"; var fixture = new CheckPrivateAssetsOnReferencesFixture(); - fixture.WithReferencedPackage("Cake.Core", ""); + fixture.WithReferencedPackage("Cake.Core"); fixture.WithPackageToCheck("Cake.Core"); fixture.WithProjectFile(projectFileName); diff --git a/src/Tasks.Tests/Extensions/ExtensionTests.cs b/src/Tasks.Tests/Extensions/ExtensionTests.cs new file mode 100644 index 0000000..07a5693 --- /dev/null +++ b/src/Tasks.Tests/Extensions/ExtensionTests.cs @@ -0,0 +1,67 @@ +using System; + +using CakeContrib.Guidelines.Tasks.Extensions; + +using FluentAssertions; + +using Xunit; + +namespace CakeContrib.Guidelines.Tasks.Tests.Extensions +{ + public class VersionExtensionTests + { + [Fact] + public void GreaterEqual_Is_True_For_Equal_Versions() + { + var a = new Version(1, 2, 3); + var b = new Version(a.Major, a.Minor, a.Build); + + a.GreaterEqual(b).Should().BeTrue(); + } + + [Fact] + public void GreaterEqual_Is_True_For_Greater_Versions() + { + var a = new Version(1, 2, 3); + var b = new Version(a.Major, a.Minor, a.Build - 1); + + a.GreaterEqual(b).Should().BeTrue(); + } + + [Fact] + public void GreaterEqual_Is_False_For_Lesser_Versions() + { + var a = new Version(1, 2, 3); + var b = new Version(a.Major, a.Minor, a.Build + 1); + + a.GreaterEqual(b).Should().BeFalse(); + } + + [Fact] + public void LessEqual_Is_True_For_Equal_Versions() + { + var a = new Version(1, 2, 3); + var b = new Version(a.Major, a.Minor, a.Build); + + a.LessEqual(b).Should().BeTrue(); + } + + [Fact] + public void LessEqual_Is_True_For_Lesser_Versions() + { + var a = new Version(1, 2, 3); + var b = new Version(a.Major, a.Minor, a.Build + 1); + + a.LessEqual(b).Should().BeTrue(); + } + + [Fact] + public void LessEqual_Is_False_For_Greater_Versions() + { + var a = new Version(1, 2, 3); + var b = new Version(a.Major, a.Minor, a.Build - 1); + + a.LessEqual(b).Should().BeFalse(); + } + } +} diff --git a/src/Tasks.Tests/Fixtures/RequiredFileStylecopJsonFixture.cs b/src/Tasks.Tests/Fixtures/RequiredFileStylecopJsonFixture.cs index 2bb8beb..10c107a 100644 --- a/src/Tasks.Tests/Fixtures/RequiredFileStylecopJsonFixture.cs +++ b/src/Tasks.Tests/Fixtures/RequiredFileStylecopJsonFixture.cs @@ -2,8 +2,6 @@ using Microsoft.Build.Framework; -using Moq; - namespace CakeContrib.Guidelines.Tasks.Tests.Fixtures { public class RequiredFileStylecopJsonFixture : BaseBuildFixture diff --git a/src/Tasks.Tests/Fixtures/TargetFrameworkVersionsFixture.cs b/src/Tasks.Tests/Fixtures/TargetFrameworkVersionsFixture.cs new file mode 100644 index 0000000..578bf23 --- /dev/null +++ b/src/Tasks.Tests/Fixtures/TargetFrameworkVersionsFixture.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; + +using Microsoft.Build.Framework; + +using Moq; + +namespace CakeContrib.Guidelines.Tasks.Tests.Fixtures +{ + public class TargetFrameworkVersionsFixture : BaseBuildFixture + { + private readonly List references; + private readonly List targetFrameworks; + private ITaskItem targetFramework; + private readonly List omittedTargets; + + public TargetFrameworkVersionsFixture() + { + references = new List(); + omittedTargets = new List(); + targetFrameworks = new List(); + targetFramework = null; + } + + public override bool Execute() + { + Task.References = references.ToArray(); + Task.TargetFramework = targetFramework; + Task.TargetFrameworks = targetFrameworks.ToArray(); + Task.Omitted = omittedTargets.ToArray(); + return base.Execute(); + } + + public void WithTargetFramwork(string packageName) + { + targetFramework = GetMockTaskItem(packageName).Object; + } + + public void WithTargetFramworks(params string[] packageNames) + { + targetFrameworks.AddRange(packageNames.Select(n => GetMockTaskItem(n).Object)); + } + + public void WithOmittedTargetFramework(string targetFramework) + { + omittedTargets.Add(GetMockTaskItem(targetFramework).Object); + } + + public void WithCakeCoreReference(int major = 0, int minor = 0, int patch = 0) + { + var cakeRef = GetMockTaskItem("Cake.Core"); + cakeRef.Setup(x => x.GetMetadata("Version")).Returns($"{major}.{minor}.{patch}"); + references.Add(cakeRef.Object); + } + + public void WithProjectFile(string fileName) + { + Task.ProjectFile = fileName; + } + } +} diff --git a/src/Tasks.Tests/RequiredReferencesTests.cs b/src/Tasks.Tests/RequiredReferencesTests.cs index 54b98fd..5a4b30b 100644 --- a/src/Tasks.Tests/RequiredReferencesTests.cs +++ b/src/Tasks.Tests/RequiredReferencesTests.cs @@ -35,7 +35,7 @@ public void Should_Warn_If_RequiredPackage_Is_Not_Referenced() fixture.WithRequiredReferences("Some.Analyser"); // when - var actual = fixture.Execute(); + fixture.Execute(); // then fixture.BuildEngine.WarningEvents.Should().HaveCount(1); @@ -54,7 +54,7 @@ public void Should_Not_Warn_If_RequiredPackage_Is_Omitted() fixture.WithOmittedReferences(required); // when - var actual = fixture.Execute(); + fixture.Execute(); // then fixture.BuildEngine.WarningEvents.Should().HaveCount(0); diff --git a/src/Tasks.Tests/TargetFrameworkVersionsTests.cs b/src/Tasks.Tests/TargetFrameworkVersionsTests.cs new file mode 100644 index 0000000..d27c38e --- /dev/null +++ b/src/Tasks.Tests/TargetFrameworkVersionsTests.cs @@ -0,0 +1,107 @@ +using System.Linq; + +using CakeContrib.Guidelines.Tasks.Tests.Fixtures; + +using FluentAssertions; + +using Xunit; + +namespace CakeContrib.Guidelines.Tasks.Tests +{ + public class TargetFrameworkVersionsTests + { + private const string NetStandard20 = "netstandard2.0"; + private const string Net46 = "net46"; + private const string Net461 = "net461"; + + [Fact] + public void Should_Error_If_RequiredTargetFramework_Is_Not_Targeted() + { + // given + var fixture = new TargetFrameworkVersionsFixture(); + + // when + fixture.Execute(); + + // then + fixture.BuildEngine.ErrorEvents.Should().HaveCount(1); + fixture.BuildEngine.ErrorEvents.First().Message.Should().Contain(NetStandard20); + } + + [Fact] + public void Should_Not_Error_If_RequiredTargetFramework_Is_Not_Targeted_But_Omitted() + { + // given + var fixture = new TargetFrameworkVersionsFixture(); + fixture.WithOmittedTargetFramework(NetStandard20); + + // when + fixture.Execute(); + + // then + fixture.BuildEngine.ErrorEvents.Should().HaveCount(0); + } + + [Fact] + public void Should_Warn_If_SuggestedTargetFramework_Is_Not_Targeted() + { + // given + var fixture = new TargetFrameworkVersionsFixture(); + fixture.WithTargetFramwork(NetStandard20); + + // when + fixture.Execute(); + + // then + fixture.BuildEngine.ErrorEvents.Should().HaveCount(0); + fixture.BuildEngine.WarningEvents.Should().HaveCount(1); + fixture.BuildEngine.WarningEvents.First().Message.Should().Contain(Net46); + } + + [Fact] + public void Should_Not_Warn_If_SuggestedTargetFramework_Is_Not_Targeted_But_Omitted() + { + // given + var fixture = new TargetFrameworkVersionsFixture(); + fixture.WithTargetFramwork(NetStandard20); + fixture.WithOmittedTargetFramework(Net461); + + // when + fixture.Execute(); + + // then + fixture.BuildEngine.ErrorEvents.Should().HaveCount(0); + fixture.BuildEngine.WarningEvents.Should().HaveCount(0); + } + + [Fact] + public void Should_Not_Warn_If_Required_And_SuggestedTargetFramework_Is_Targeted() + { + // given + var fixture = new TargetFrameworkVersionsFixture(); + fixture.WithTargetFramworks(NetStandard20, Net461); + + // when + fixture.Execute(); + + // then + fixture.BuildEngine.ErrorEvents.Should().HaveCount(0); + fixture.BuildEngine.WarningEvents.Should().HaveCount(0); + } + + [Fact] + public void Should_Not_Warn_If_Required_And_Alternative_SuggestedTargetFramework_Is_Targeted() + { + // given + var fixture = new TargetFrameworkVersionsFixture(); + fixture.WithTargetFramworks(NetStandard20, Net46); + + // when + fixture.Execute(); + + // then + fixture.BuildEngine.ErrorEvents.Should().HaveCount(0); + fixture.BuildEngine.WarningEvents.Should().HaveCount(0); + } + } +} diff --git a/src/Tasks.Tests/Tasks.Tests.csproj b/src/Tasks.Tests/Tasks.Tests.csproj index b9cb776..fe81bbc 100644 --- a/src/Tasks.Tests/Tasks.Tests.csproj +++ b/src/Tasks.Tests/Tasks.Tests.csproj @@ -28,9 +28,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Tasks/Extensions/VersionExtensions.cs b/src/Tasks/Extensions/VersionExtensions.cs new file mode 100644 index 0000000..a84f80a --- /dev/null +++ b/src/Tasks/Extensions/VersionExtensions.cs @@ -0,0 +1,58 @@ +using System; + +namespace CakeContrib.Guidelines.Tasks.Extensions +{ + /// + /// internal extensions to . + /// + internal static class VersionExtensions + { + /// + /// compares two versions. + /// + /// the version that is extended. + /// the version to compare to. + /// + /// true, if is greater or equal to . + /// false, otherwise. + /// + public static bool GreaterEqual(this Version baseVersion, Version compareTo) + { + if (baseVersion == null) + { + throw new ArgumentNullException(nameof(baseVersion)); + } + + if (compareTo == null) + { + throw new ArgumentNullException(nameof(compareTo)); + } + + return baseVersion.CompareTo(compareTo) > -1; + } + + /// + /// compares two versions. + /// + /// the version that is extended. + /// the version to compare to. + /// + /// true, if is less or equal to . + /// false, otherwise. + /// + public static bool LessEqual(this Version baseVersion, Version compareTo) + { + if (baseVersion == null) + { + throw new ArgumentNullException(nameof(baseVersion)); + } + + if (compareTo == null) + { + throw new ArgumentNullException(nameof(compareTo)); + } + + return baseVersion.CompareTo(compareTo) < 1; + } + } +} diff --git a/src/Tasks/TargetFrameworkVersions.cs b/src/Tasks/TargetFrameworkVersions.cs new file mode 100644 index 0000000..144b13c --- /dev/null +++ b/src/Tasks/TargetFrameworkVersions.cs @@ -0,0 +1,218 @@ +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 References for the guideline . + /// + public class TargetFrameworkVersions : Task + { + private const string NetStandard20 = "netstandard2.0"; + private const string Net46 = "net46"; + private const string Net461 = "net461"; + + private static readonly Version Zero26 = new Version(0, 26, 0); + + private static readonly TargetsDefinitions DefaultTarget = new TargetsDefinitions + { + RequiredTargets = new[] { TargetsDefinition.From(NetStandard20) }, + SuggestedTargets = new[] { TargetsDefinition.From(Net461, Net46) }, + }; + + private static readonly Dictionary, TargetsDefinitions> SpecificTargets = + new Dictionary, TargetsDefinitions> + { + { + v => v.GreaterEqual(Zero26), + new TargetsDefinitions + { + RequiredTargets = new[] { TargetsDefinition.From(NetStandard20) }, + SuggestedTargets = new[] { TargetsDefinition.From(Net461, Net46) }, + } + }, + }; + + /// + /// Gets or sets the References. + /// + [Required] + public ITaskItem[] References { get; set; } + + /// + /// Gets or sets the TargetFrameworks. + /// + [Required] + public ITaskItem[] TargetFrameworks { get; set; } + + /// + /// Gets or sets the TargetFramework. + /// + [Required] + public ITaskItem TargetFramework { get; set; } + + /// + /// Gets or sets the project file. + /// + public string ProjectFile { get; set; } + + /// + /// Gets or sets Targets to omit. I.e. if those are missing, they will not be reported. + /// + public ITaskItem[] Omitted { get; set; } + + /// + public override bool Execute() + { + // find cake.core version + var cakeCore = + References?.FirstOrDefault(x => x.ToString().Equals("Cake.Core", StringComparison.OrdinalIgnoreCase)); + if (cakeCore == null) + { + Log.LogMessage( + MessageImportance.Low, + "Could not find Cake.Core reference. Using default TargetVersions."); + return Execute(DefaultTarget); + } + + if (!Version.TryParse(cakeCore.GetMetadata("version"), out Version version)) + { + Log.LogWarning( + $"Cake.Core has a version of {cakeCore.GetMetadata("version")} which is not a valid version. Using default TargetVersions."); + return Execute(DefaultTarget); + } + + foreach (var targetsDefinition in SpecificTargets) + { + var match = targetsDefinition.Key(version); + if (!match) + { + continue; + } + + return Execute(targetsDefinition.Value); + } + + Log.LogMessage( + MessageImportance.Low, + $"Could not find a specific TargetVersions-setting for Cake.Core version {version}. Using default TargetVersions."); + return Execute(DefaultTarget); + } + + private bool Execute(TargetsDefinitions targets) + { + var allTargets = new List(); + if (TargetFramework != null) + { + allTargets.Add(TargetFramework.ToString()); + } + + if (TargetFrameworks != null) + { + allTargets.AddRange(TargetFrameworks.Select(x => x.ToString())); + } + + allTargets = allTargets.Distinct().ToList(); + + // first, check required targets + Log.LogMessage( + MessageImportance.Low, + $"Comparing TargetFramework[s] ({string.Join(";", allTargets)}) to required: {string.Join(",", targets.RequiredTargets.Select(x => x.Name))}."); + + foreach (var requiredTarget in targets.RequiredTargets) + { + if (Omitted != null && Omitted.Any(x => x.ToString().Equals(requiredTarget.Name, StringComparison.OrdinalIgnoreCase))) + { + Log.LogMessage(MessageImportance.Low, $"Required TargetFramework '{requiredTarget.Name}' is set to omit."); + continue; + } + + if (allTargets.Any(x => x.Equals(requiredTarget.Name, StringComparison.OrdinalIgnoreCase))) + { + continue; + } + + var found = requiredTarget.Alternatives?.Any(alternative => allTargets.Contains(alternative)); + if (found.GetValueOrDefault(false)) + { + continue; + } + + Log.LogError( + null, + "CCG0007", + string.Empty, + ProjectFile ?? string.Empty, + 0, + 0, + 0, + 0, + "Missing required target: " + requiredTarget.Name); + return false; + } + + // now, check suggested targets + Log.LogMessage( + MessageImportance.Low, + $"Comparing TargetFramework[s] ({string.Join(";", allTargets)}) to suggested: {string.Join(",", targets.SuggestedTargets.Select(x => x.Name))}."); + + foreach (var suggestedTarget in targets.SuggestedTargets) + { + if (Omitted != null && Omitted.Any(x => x.ToString().Equals(suggestedTarget.Name, StringComparison.OrdinalIgnoreCase))) + { + Log.LogMessage(MessageImportance.Low, $"Suggested TargetFramework '{suggestedTarget.Name}' is set to omit."); + continue; + } + + if (allTargets.Any(x => x.Equals(suggestedTarget.Name, StringComparison.OrdinalIgnoreCase))) + { + continue; + } + + var found = suggestedTarget.Alternatives?.Any(alternative => allTargets.Contains(alternative)); + if (found.GetValueOrDefault(false)) + { + continue; + } + + Log.LogWarning( + null, + "CCG0007", + string.Empty, + ProjectFile ?? string.Empty, + 0, + 0, + 0, + 0, + "Missing suggested target: " + suggestedTarget.Name); + } + + return true; + } + + private class TargetsDefinitions + { + public TargetsDefinition[] RequiredTargets { get; set; } + + public TargetsDefinition[] SuggestedTargets { get; set; } + } + + private class TargetsDefinition + { + public string Name { get; private set; } + + public string[] Alternatives { get; private set; } + + public static TargetsDefinition From(string name, params string[] alternatives) + { + return new TargetsDefinition { Name = name, Alternatives = alternatives ?? Array.Empty(), }; + } + } + } +}