From 53ec186135cb343963f4d6b3b1419799e970446d Mon Sep 17 00:00:00 2001 From: "eddie.stanley" Date: Fri, 5 Jan 2024 11:13:18 -0800 Subject: [PATCH] [GH-87] Added new conventions to enforce that a project references/does not reference a package (by name) An example use-case is something like the coverlet collector (https://github.com/coverlet-coverage/coverlet) which should be added ONLY to (ALL) test projects --- .../AssemblyConventionSpecificationTests.cs | 40 +++++++++++++++++++ src/Core/Conventional/Convention.Assembly.cs | 10 +++++ ...ePackageAssemblyConventionSpecification.cs | 27 +++++++++++++ ...ePackageAssemblyConventionSpecification.cs | 27 +++++++++++++ ...eferenceAssemblyConventionSpecification.cs | 25 ++++++++++++ .../SdkClassLibrary1/SdkClassLibrary1.csproj | 14 +++++++ .../TestSolution.TestProject.sln | 6 +++ 7 files changed, 149 insertions(+) create mode 100644 src/Core/Conventional/Conventions/Assemblies/MustNotReferencePackageAssemblyConventionSpecification.cs create mode 100644 src/Core/Conventional/Conventions/Assemblies/MustReferencePackageAssemblyConventionSpecification.cs create mode 100644 src/Core/Conventional/Conventions/Assemblies/PackageReferenceAssemblyConventionSpecification.cs create mode 100644 src/Core/TestSolution/TestSolution.TestProject/SdkClassLibrary1/SdkClassLibrary1.csproj diff --git a/src/Core/Conventional.Tests/Conventional/Conventions/Assemblies/AssemblyConventionSpecificationTests.cs b/src/Core/Conventional.Tests/Conventional/Conventions/Assemblies/AssemblyConventionSpecificationTests.cs index 41d4232..55b5674 100644 --- a/src/Core/Conventional.Tests/Conventional/Conventions/Assemblies/AssemblyConventionSpecificationTests.cs +++ b/src/Core/Conventional.Tests/Conventional/Conventions/Assemblies/AssemblyConventionSpecificationTests.cs @@ -241,5 +241,45 @@ public void MustNotIncludeProjectReferences_Failure() result.IsSatisfied.Should().BeFalse(); result.Failures.Single().Should().StartWith("Conventional.Tests includes reference to project"); } + + [Test] + public void MustReferencePackage_Success() + { + var result = TheAssembly + .WithNameMatching("SdkClassLibrary1") + .MustConformTo(Convention.MustReferencePackage("coverlet.collector")); + + result.IsSatisfied.Should().BeTrue(); + } + + [Test] + public void MustReferencePackage_Failure() + { + var result = TheAssembly + .WithNameMatching("SdkClassLibrary1") + .MustConformTo(Convention.MustReferencePackage("koverlet.kollector")); + + result.IsSatisfied.Should().BeFalse(); + } + + [Test] + public void MustNotReferencePackage_Success() + { + var result = TheAssembly + .WithNameMatching("SdkClassLibrary1") + .MustConformTo(Convention.MustNotReferencePackage("foo.bar.baz")); + + result.IsSatisfied.Should().BeTrue(); + } + + [Test] + public void MustNotReferencePackage_Failure() + { + var result = TheAssembly + .WithNameMatching("SdkClassLibrary1") + .MustConformTo(Convention.MustNotReferencePackage("coverlet.collector")); + + result.IsSatisfied.Should().BeFalse(); + } } } \ No newline at end of file diff --git a/src/Core/Conventional/Convention.Assembly.cs b/src/Core/Conventional/Convention.Assembly.cs index 0994ce7..9ccb12c 100644 --- a/src/Core/Conventional/Convention.Assembly.cs +++ b/src/Core/Conventional/Convention.Assembly.cs @@ -84,5 +84,15 @@ public static MustBeIncludedInSetOfAssembliesConventionSpecification MustBeInclu public static MustNotIncludeProjectReferencesConventionSpecification MustNotIncludeProjectReferences => new MustNotIncludeProjectReferencesConventionSpecification(); + + public static MustReferencePackageAssemblyConventionSpecification MustReferencePackage(string packageName) + { + return new MustReferencePackageAssemblyConventionSpecification(packageName); + } + + public static MustNotReferencePackageAssemblyConventionSpecification MustNotReferencePackage(string packageName) + { + return new MustNotReferencePackageAssemblyConventionSpecification(packageName); + } } } \ No newline at end of file diff --git a/src/Core/Conventional/Conventions/Assemblies/MustNotReferencePackageAssemblyConventionSpecification.cs b/src/Core/Conventional/Conventions/Assemblies/MustNotReferencePackageAssemblyConventionSpecification.cs new file mode 100644 index 0000000..ccdf475 --- /dev/null +++ b/src/Core/Conventional/Conventions/Assemblies/MustNotReferencePackageAssemblyConventionSpecification.cs @@ -0,0 +1,27 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Conventional.Conventions.Assemblies +{ + public class MustNotReferencePackageAssemblyConventionSpecification : PackageReferenceAssemblyConventionSpecification + { + private readonly string _needlePackage; + + public MustNotReferencePackageAssemblyConventionSpecification(string needlePackage) + { + _needlePackage = needlePackage; + } + + protected override ConventionResult IsSatisfiedBy(string assemblyName, XDocument projectDocument) + { + if (GetPackageReferences(projectDocument).Contains(_needlePackage)) + { + return ConventionResult.NotSatisfied(assemblyName, string.Format(FailureMessage, assemblyName)); + } + + return ConventionResult.Satisfied(assemblyName); + } + + protected override string FailureMessage => "{0} should not reference package " + _needlePackage; + } +} \ No newline at end of file diff --git a/src/Core/Conventional/Conventions/Assemblies/MustReferencePackageAssemblyConventionSpecification.cs b/src/Core/Conventional/Conventions/Assemblies/MustReferencePackageAssemblyConventionSpecification.cs new file mode 100644 index 0000000..f4dd5ad --- /dev/null +++ b/src/Core/Conventional/Conventions/Assemblies/MustReferencePackageAssemblyConventionSpecification.cs @@ -0,0 +1,27 @@ +using System.Linq; +using System.Xml.Linq; + +namespace Conventional.Conventions.Assemblies +{ + public class MustReferencePackageAssemblyConventionSpecification : PackageReferenceAssemblyConventionSpecification + { + private readonly string _needlePackage; + + public MustReferencePackageAssemblyConventionSpecification(string needlePackage) + { + _needlePackage = needlePackage; + } + + protected override ConventionResult IsSatisfiedBy(string assemblyName, XDocument projectDocument) + { + if (GetPackageReferences(projectDocument).Contains(_needlePackage)) + { + return ConventionResult.Satisfied(assemblyName); + } + + return ConventionResult.NotSatisfied(assemblyName, string.Format(FailureMessage, assemblyName)); + } + + protected override string FailureMessage => "{0} should reference package " + _needlePackage; + } +} \ No newline at end of file diff --git a/src/Core/Conventional/Conventions/Assemblies/PackageReferenceAssemblyConventionSpecification.cs b/src/Core/Conventional/Conventions/Assemblies/PackageReferenceAssemblyConventionSpecification.cs new file mode 100644 index 0000000..88b9dc7 --- /dev/null +++ b/src/Core/Conventional/Conventions/Assemblies/PackageReferenceAssemblyConventionSpecification.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using System.Xml.XPath; + +namespace Conventional.Conventions.Assemblies +{ + public abstract class PackageReferenceAssemblyConventionSpecification : AssemblyConventionSpecification + { + protected IEnumerable GetPackageReferences(XDocument projectDocument) + { + // The Project element (and descendants) are namespaced in legacy csproj files, so our XPath ignores the + // namespace by considering the local element name only. Once we no-longer need to support legacy csproj + // files, the XPath can be simplified to /Project/ItemGroup/PackageReference + return projectDocument.XPathSelectElements("/*[local-name() = 'Project']/*[local-name() = 'ItemGroup']/*[local-name() = 'PackageReference']") + .Select(referenceElement => referenceElement.Attribute("Include")?.Value) + .Where(value => value != null); + } + + protected override ConventionResult IsSatisfiedByLegacyCsprojFormat(string assemblyName, XDocument projectDocument) + { + return IsSatisfiedBy(assemblyName, projectDocument); + } + } +} \ No newline at end of file diff --git a/src/Core/TestSolution/TestSolution.TestProject/SdkClassLibrary1/SdkClassLibrary1.csproj b/src/Core/TestSolution/TestSolution.TestProject/SdkClassLibrary1/SdkClassLibrary1.csproj new file mode 100644 index 0000000..8918493 --- /dev/null +++ b/src/Core/TestSolution/TestSolution.TestProject/SdkClassLibrary1/SdkClassLibrary1.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.1 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/src/Core/TestSolution/TestSolution.TestProject/TestSolution.TestProject.sln b/src/Core/TestSolution/TestSolution.TestProject/TestSolution.TestProject.sln index 872fdb7..15b9473 100644 --- a/src/Core/TestSolution/TestSolution.TestProject/TestSolution.TestProject.sln +++ b/src/Core/TestSolution/TestSolution.TestProject/TestSolution.TestProject.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestSolution.TestProject", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProjectTwo", "..\TestProjectTwo\TestProjectTwo.csproj", "{DA39482D-C4B4-41B8-9908-BF715AE9DD7C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SdkClassLibrary1", "SdkClassLibrary1\SdkClassLibrary1.csproj", "{C0988207-0023-4364-BFE0-04F8954501C4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {DA39482D-C4B4-41B8-9908-BF715AE9DD7C}.Debug|Any CPU.Build.0 = Debug|Any CPU {DA39482D-C4B4-41B8-9908-BF715AE9DD7C}.Release|Any CPU.ActiveCfg = Release|Any CPU {DA39482D-C4B4-41B8-9908-BF715AE9DD7C}.Release|Any CPU.Build.0 = Release|Any CPU + {C0988207-0023-4364-BFE0-04F8954501C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C0988207-0023-4364-BFE0-04F8954501C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C0988207-0023-4364-BFE0-04F8954501C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C0988207-0023-4364-BFE0-04F8954501C4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE