diff --git a/src/Core/Conventional.Tests/AllAssembliesScenarios.cs b/src/Core/Conventional.Tests/AllAssembliesScenarios.cs index d2194a9..ef3fd82 100644 --- a/src/Core/Conventional.Tests/AllAssembliesScenarios.cs +++ b/src/Core/Conventional.Tests/AllAssembliesScenarios.cs @@ -10,7 +10,7 @@ public void GivenAPattern_LocatesAndReturnsAllAssembliesForThatPattern() { var assemblySpecimen = AllAssemblies.WithNamesMatching("*"); - assemblySpecimen.Should().HaveCount(5); + assemblySpecimen.Should().HaveCount(6); } } } \ No newline at end of file diff --git a/src/Core/Conventional.Tests/Conventional/Conventions/Assemblies/AssemblyConventionSpecificationTests.cs b/src/Core/Conventional.Tests/Conventional/Conventions/Assemblies/AssemblyConventionSpecificationTests.cs index de1592b..74bac29 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 2b7eab8..7803d38 100644 --- a/src/Core/Conventional/Convention.Assembly.cs +++ b/src/Core/Conventional/Convention.Assembly.cs @@ -88,5 +88,25 @@ public static MustBeIncludedInSetOfAssembliesConventionSpecification MustBeInclu /// This convention is unaware of shared build prop files (Directory.Build.Props + Directory.Build.Targets) - see https://github.com/andrewabest/Conventional/issues/88 public static MustNotIncludeProjectReferencesConventionSpecification MustNotIncludeProjectReferences => new MustNotIncludeProjectReferencesConventionSpecification(); + + /// + /// Requires that this assembly (project) references the specified package + /// + /// The name of the package that must be referenced + /// This convention is unaware of shared build prop files (Directory.Build.Props + Directory.Build.Targets) - see https://github.com/andrewabest/Conventional/issues/88 + public static MustReferencePackageAssemblyConventionSpecification MustReferencePackage(string packageName) + { + return new MustReferencePackageAssemblyConventionSpecification(packageName); + } + + /// + /// Requires that this assembly (project) does not reference the specified package + /// + /// The name of the package that must not be referenced + /// This convention is unaware of shared build prop files (Directory.Build.Props + Directory.Build.Targets) - see https://github.com/andrewabest/Conventional/issues/88 + 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..4eb8b5c --- /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) + { + // Note: The Project element (and descendants) are namespaced in legacy csproj files, so our XPath ignores the + // Note: namespace by considering the local element name only. Once we no-longer need to support legacy csproj + // Note: 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