From a15262e1e7ad86c467217fe64a087b2ebc621dd0 Mon Sep 17 00:00:00 2001 From: "eddie.stanley" Date: Fri, 5 Jan 2024 10:54:59 -0800 Subject: [PATCH] [GH-87] Added new convention to enforce that a given project doesn't include project references to (any) other projects --- .../AssemblyConventionSpecificationTests.cs | 22 +++++++++++ src/Core/Conventional/Convention.Assembly.cs | 7 ++++ ...rojectReferencesConventionSpecification.cs | 39 +++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 src/Core/Conventional/Conventions/Assemblies/MustNotIncludeProjectReferencesConventionSpecification.cs diff --git a/src/Core/Conventional.Tests/Conventional/Conventions/Assemblies/AssemblyConventionSpecificationTests.cs b/src/Core/Conventional.Tests/Conventional/Conventions/Assemblies/AssemblyConventionSpecificationTests.cs index ccb5a9a..41d4232 100644 --- a/src/Core/Conventional.Tests/Conventional/Conventions/Assemblies/AssemblyConventionSpecificationTests.cs +++ b/src/Core/Conventional.Tests/Conventional/Conventions/Assemblies/AssemblyConventionSpecificationTests.cs @@ -219,5 +219,27 @@ public void MustBeIncludedInSetOfAssemblies_Failure() // TODO: Use result.Should().AllSatisfy() once we've updated to fluentassertions 6.5.0+ result.Select(x => x.IsSatisfied).Distinct().Single().Should().BeFalse(); } + + [Test] + public void MustNotIncludeProjectReferences_Success() + { + var result = TheAssembly + .WithNameMatching("TestProjectTwo") + .MustConformTo(Convention.MustNotIncludeProjectReferences); + + // TestProjectTwo doesn't import any other projects (at time of writing) + result.IsSatisfied.Should().BeTrue(); + } + + [Test] + public void MustNotIncludeProjectReferences_Failure() + { + var result = TheAssembly + .WithNameMatching("Conventional.Tests") + .MustConformTo(Convention.MustNotIncludeProjectReferences); // It of course includes a reference to Conventional + + result.IsSatisfied.Should().BeFalse(); + result.Failures.Single().Should().StartWith("Conventional.Tests includes reference to project"); + } } } \ No newline at end of file diff --git a/src/Core/Conventional/Convention.Assembly.cs b/src/Core/Conventional/Convention.Assembly.cs index 40c0a3e..2b7eab8 100644 --- a/src/Core/Conventional/Convention.Assembly.cs +++ b/src/Core/Conventional/Convention.Assembly.cs @@ -81,5 +81,12 @@ public static MustBeIncludedInSetOfAssembliesConventionSpecification MustBeInclu { return new MustBeIncludedInSetOfAssembliesConventionSpecification(assemblies, setName); } + + /// + /// Disallows this assembly (project) from referencing other projects + /// + /// 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(); } } \ No newline at end of file diff --git a/src/Core/Conventional/Conventions/Assemblies/MustNotIncludeProjectReferencesConventionSpecification.cs b/src/Core/Conventional/Conventions/Assemblies/MustNotIncludeProjectReferencesConventionSpecification.cs new file mode 100644 index 0000000..892f54e --- /dev/null +++ b/src/Core/Conventional/Conventions/Assemblies/MustNotIncludeProjectReferencesConventionSpecification.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using System.Xml.XPath; + +namespace Conventional.Conventions.Assemblies +{ + public class MustNotIncludeProjectReferencesConventionSpecification : AssemblyConventionSpecification + { + protected override ConventionResult IsSatisfiedBy(string assemblyName, XDocument projectDocument) + { + var projectReferences = GetProjectReferences(projectDocument).ToArray(); + + if (projectReferences.Any()) + { + return ConventionResult.NotSatisfied(assemblyName, string.Format(FailureMessage, assemblyName, projectReferences.First())); + } + + return ConventionResult.Satisfied(assemblyName); + } + + protected override ConventionResult IsSatisfiedByLegacyCsprojFormat(string assemblyName, XDocument projectDocument) + { + return IsSatisfiedBy(assemblyName, projectDocument); + } + + private IEnumerable GetProjectReferences(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/ProjectReference + return projectDocument.XPathSelectElements("/*[local-name() = 'Project']/*[local-name() = 'ItemGroup']/*[local-name() = 'ProjectReference']") + .Select(referenceElement => referenceElement.Attribute("Include")?.Value) + .Where(value => value != null); + } + + protected override string FailureMessage => "{0} includes reference to project {1}"; + } +} \ No newline at end of file