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 633a3c3..875c6f1 100644 --- a/src/Core/Conventional.Tests/Conventional/Conventions/Assemblies/AssemblyConventionSpecificationTests.cs +++ b/src/Core/Conventional.Tests/Conventional/Conventions/Assemblies/AssemblyConventionSpecificationTests.cs @@ -1,9 +1,9 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; -using Conventional.Extensions; using FluentAssertions; using NUnit.Framework; @@ -185,5 +185,212 @@ public void MustHaveCertainFilesBeContentCopyIfNewer_Regex() result.IsSatisfied.Should().BeFalse(); result.Failures.Single().Should().EndWith("copy-not.png"); } + + private AssemblySpecimen[] TestProjects => + AllAssemblies.WithNamesMatching("*") + .Where(specimen => specimen.ProjectFilePath.Contains("Tests")) + .ToArray(); + + // Note: In practice, this list of assemblies would be used to drive further convention tests (i.e. assembly.GetTypes()) + private static readonly List TestAssemblies = new List + { + typeof(DogFoodConventions).Assembly + }; + + [Test] + public void MustBeIncludedInSetOfAssemblies_Success() + { + var result = TestProjects + .MustConformTo(Convention.MustBeIncludedInSetOfAssemblies(TestAssemblies, "TestAssemblies")); + + // TODO: Use result.Should().AllSatisfy() once we've updated to fluentassertions 6.5.0+ + result.Select(x => x.IsSatisfied).Distinct().Single().Should().BeTrue(); + } + + [Test] + public void MustBeIncludedInSetOfAssemblies_Failure() + { + // ReSharper disable once CollectionNeverUpdated.Local + var staleTestAssemblies = new List(); + + var result = TestProjects + .MustConformTo(Convention.MustBeIncludedInSetOfAssemblies(staleTestAssemblies, "TestAssemblies")); + + // 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); + + // Note: 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); // Note: Conventional.Tests of course includes a reference to Conventional + + 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(); + } + + [Test] + public void MustSetPropertyValue_SingleValue_Success() + { + var result = TheAssembly + .WithNameMatching("SdkClassLibrary1") + .MustConformTo(Convention.MustSetPropertyValue("TheUniversalAnswer", "42")); + + result.IsSatisfied.Should().BeTrue(); + } + + [Theory] + [TestCase("Potato")] + [TestCase("Carrot")] + public void MustSetPropertyValue_MultipleValues_Success(string value) + { + var result = TheAssembly + .WithNameMatching("SdkClassLibrary1") + .MustConformTo(Convention.MustSetPropertyValue("Vegetable", value)); + + result.IsSatisfied.Should().BeTrue(); + } + + [Test] + public void MustSetPropertyValue_SingleValue_Failure() + { + var result = TheAssembly + .WithNameMatching("SdkClassLibrary1") + .MustConformTo(Convention.MustSetPropertyValue("TheUniversalAnswer", "41.999")); + + result.IsSatisfied.Should().BeFalse(); + result.Failures.Single().Should().Be("SdkClassLibrary1 should have property TheUniversalAnswer with value 41.999"); + } + + [Test] + public void MustSetPropertyValue_MultipleValues_Failure() + { + var result = TheAssembly + .WithNameMatching("SdkClassLibrary1") + .MustConformTo(Convention.MustSetPropertyValue("Vegetable", "Turnip")); // Note: Assumes no Turnip in the csproj + + result.IsSatisfied.Should().BeFalse(); + result.Failures.Single().Should().Be("SdkClassLibrary1 should have property Vegetable with value Turnip"); + } + + [Test] + public void MustSetPropertyValue_NoValues_Failure() + { + var result = TheAssembly + .WithNameMatching("SdkClassLibrary1") + .MustConformTo(Convention.MustSetPropertyValue("ThisPropertyShouldNeverEverExist", "x")); // Note: Assumes no x in the csproj + + result.IsSatisfied.Should().BeFalse(); + result.Failures.Single().Should().Be("SdkClassLibrary1 should have property ThisPropertyShouldNeverEverExist with value x"); + } + + [Theory] + [TestCase("CS0162")] + [TestCase("CS4014")] + public void MustTreatWarningAsError_MultipleWarnings_Success(string warning) + { + var result = TheAssembly + .WithNameMatching("TestProjectTwo") + .MustConformTo(Convention.MustTreatWarningAsError(warning)); + + result.IsSatisfied.Should().BeTrue(); + } + + [Test] + public void MustTreatWarningAsError_SingleWarning_Success() + { + var result = TheAssembly + .WithNameMatching("SdkClassLibrary1") + .MustConformTo(Convention.MustTreatWarningAsError("CS0162")); + + result.IsSatisfied.Should().BeTrue(); + } + + [Test] + public void MustTreatWarningAsError_MultipleWarnings_Failure() + { + var result = TheAssembly + .WithNameMatching("TestProjectTwo") + .MustConformTo(Convention.MustTreatWarningAsError("XX9999")); + + result.IsSatisfied.Should().BeFalse(); + result.Failures.Single().Should().Be("Assembly TestProjectTwo should treat warning XX9999 as an error but does not"); + } + + [Test] + public void MustTreatWarningAsError_SingleWarnings_Failure() + { + var result = TheAssembly + .WithNameMatching("SdkClassLibrary1") + .MustConformTo(Convention.MustTreatWarningAsError("XX9999")); + + result.IsSatisfied.Should().BeFalse(); + result.Failures.Single().Should().Be("Assembly SdkClassLibrary1 should treat warning XX9999 as an error but does not"); + } + + [Test] + public void MustTreatWarningAsError_NoWarnings_Failure() + { + var result = TheAssembly + .WithNameMatching("TestSolution.TestProject") + .MustConformTo(Convention.MustTreatWarningAsError("CS0162")); + + // Note: TestSolution.TestProject does not set the WarningsAsErrors property, at time of writing + result.IsSatisfied.Should().BeFalse(); + result.Failures.Single().Should().Be("Assembly TestSolution.TestProject should treat warning CS0162 as an error but does not"); + } } } \ No newline at end of file diff --git a/src/Core/Conventional.Tests/Conventional/Conventions/Cecil/CecilConventionSpecificationTests.cs b/src/Core/Conventional.Tests/Conventional/Conventions/Cecil/CecilConventionSpecificationTests.cs index c115194..9254b2f 100644 --- a/src/Core/Conventional.Tests/Conventional/Conventions/Cecil/CecilConventionSpecificationTests.cs +++ b/src/Core/Conventional.Tests/Conventional/Conventions/Cecil/CecilConventionSpecificationTests.cs @@ -120,7 +120,7 @@ public void result.Failures.Should().HaveCount(1); result.Failures.First().Should().Contain(nameof(DateTime)); } - + private class GoodDateTimeOffsetCitizen { private DateTimeOffset _current; @@ -150,7 +150,7 @@ public OffendingDateTimeOffsetCitizen() _current = DateTimeOffset.Now; } } - + private class AnotherOffendingDateTimeOffsetCitizen { private DateTimeOffset _current; @@ -178,7 +178,7 @@ public void MustNotUseDateTimeOffsetNowConventionSpecification_FailsWhenACallToD { var result = new [] { - typeof (OffendingDateTimeOffsetCitizen), + typeof (OffendingDateTimeOffsetCitizen), typeof(AnotherOffendingDateTimeOffsetCitizen), typeof(AsyncOffendingDateTimeOffsetCitizen) } @@ -221,7 +221,7 @@ public BadExceptionThrower() throw new Exception(); } } - + [Test] public void ExceptionsThrownMustBeDerivedFromConventionSpecification_FailsIfExceptionDoesNotDeriveFromCorrectBase() @@ -366,7 +366,7 @@ public void result.IsSatisfied.Should().BeFalse(); result.Failures.Should().HaveCount(1); } - + private class HasMoreThanOneParameterizedConstructor { @@ -384,7 +384,7 @@ public HasMoreThanOneParameterizedConstructor(int id, string name) public int Id { get; set; } public string Name { get; set; } } - + private class HasNoConstructors { } @@ -485,7 +485,7 @@ public void LibraryCodeShouldCallConfigureAwaitWhenAwaitingTasks_FailsWhenConfig var expectedFailureMessage = "Libraries must call Task.ConfigureAwait(false) to prevent deadlocks" + Environment.NewLine + "- HasAnAsyncMethodThatAwaitsATaskAndDoesNotCallConfigureAwaitAndAnotherThatDoes.MethodThatAwaitsATaskAndDoesNotCallConfigureAwait"; - + var result = typeof(HasAnAsyncMethodThatAwaitsATaskAndDoesNotCallConfigureAwaitAndAnotherThatDoes) .MustConformTo(Convention.LibraryCodeShouldCallConfigureAwaitWhenAwaitingTasks); @@ -515,5 +515,63 @@ public void LibraryCodeShouldCallConfigureAwaitWhenAwaitingTasks_FailsWhenConfig result.IsSatisfied.Should().BeFalse(); result.Failures.Single().Should().Be(expectedFailureMessage); } + + # region MustNotUseGuidNewGuid + + private interface IGuidProvider + { + Guid CreateIdentifier(); + } + + private class GoodGuidCreationCitizen + { + public GoodGuidCreationCitizen(IGuidProvider guidProvider) + { + guidProvider.CreateIdentifier(); + } + } + + [Test] + public void MustNotUseGuidNewGuid_Success() + { + typeof(GoodGuidCreationCitizen) + .MustConformTo(Convention.MustNotUseGuidNewGuid) + .IsSatisfied + .Should() + .BeTrue(); + } + + private class OffendingGuidCreationCitizen + { + public OffendingGuidCreationCitizen() + { + Guid.NewGuid(); + } + } + + private class AsyncOffendingGuidCreationCitizen + { + public async Task ResolveIdentifier() + { + await Task.Delay(1); + return Guid.NewGuid(); + } + } + + [Test] + public void MustNotUseGuidNewGuid_Failure() + { + var result = new [] + { + typeof(OffendingGuidCreationCitizen), + typeof(AsyncOffendingGuidCreationCitizen) + } + .MustConformTo(Convention.MustNotUseGuidNewGuid); + + result.Results.Should().OnlyContain(x => x.IsSatisfied == false); + result.Failures.Should().HaveCount(2); + } + + # endregion } } \ No newline at end of file diff --git a/src/Core/Conventional/Convention.Assembly.cs b/src/Core/Conventional/Convention.Assembly.cs index eb6b0b5..d96d4bf 100644 --- a/src/Core/Conventional/Convention.Assembly.cs +++ b/src/Core/Conventional/Convention.Assembly.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Reflection; using System.Text.RegularExpressions; using Conventional.Conventions.Assemblies; @@ -67,5 +69,67 @@ public static MustIncludeAllMatchingFilesInFolderConventionSpecification MustInc { return new MustIncludeAllMatchingFilesInFolderConventionSpecification(filePattern, subfolder); } + + /// + /// Require this assembly to be included (by name) in a list of assemblies (e.g. "TestAssemblies") + /// + /// The "haystack" set of assemblies that the "needle" assembly must appear in + /// A friendly name for the assembly set + /// Probably only useful when using i.e. / + public static MustBeIncludedInSetOfAssembliesConventionSpecification MustBeIncludedInSetOfAssemblies( + IEnumerable assemblies, string setName) + { + 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(); + + /// + /// 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); + } + + /// + /// Require this project to set a property value + /// + /// The name of the property + /// The value the property should have + /// This convention is currently ignorant of MSBuild conditions + /// 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 MustSetPropertyValueAssemblyConventionSpecification MustSetPropertyValue( + string propertyName, string value) + { + return new MustSetPropertyValueAssemblyConventionSpecification(propertyName, value); + } + + /// + /// Require this project to treat the specified compiler warning as an error + /// + /// The warning code to treat as fatal (e.g. CS0162) + /// 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 MustTreatWarningAsErrorAssemblyConventionSpecification MustTreatWarningAsError(string warning) + { + return new MustTreatWarningAsErrorAssemblyConventionSpecification(warning); + } } } \ No newline at end of file diff --git a/src/Core/Conventional/Convention.Cecil.cs b/src/Core/Conventional/Convention.Cecil.cs index e2054e9..d2c43e5 100644 --- a/src/Core/Conventional/Convention.Cecil.cs +++ b/src/Core/Conventional/Convention.Cecil.cs @@ -9,6 +9,8 @@ public static partial class Convention public static MustNotUseDateTimeOffsetNowConventionSpecification MustNotUseDateTimeOffsetNow => new MustNotUseDateTimeOffsetNowConventionSpecification(); + public static MustNotUseGuidNewGuidConventionSpecification MustNotUseGuidNewGuid => new MustNotUseGuidNewGuidConventionSpecification(); + public static ExceptionsThrownMustBeDerivedFromConventionSpecification ExceptionsThrownMustBeDerivedFrom(Type baseType) { return new ExceptionsThrownMustBeDerivedFromConventionSpecification(baseType); diff --git a/src/Core/Conventional/Conventions/Assemblies/MustBeIncludedInSetOfAssembliesConventionSpecification.cs b/src/Core/Conventional/Conventions/Assemblies/MustBeIncludedInSetOfAssembliesConventionSpecification.cs new file mode 100644 index 0000000..f6ec029 --- /dev/null +++ b/src/Core/Conventional/Conventions/Assemblies/MustBeIncludedInSetOfAssembliesConventionSpecification.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; + +namespace Conventional.Conventions.Assemblies +{ + public class MustBeIncludedInSetOfAssembliesConventionSpecification : AssemblyConventionSpecification + { + private readonly ISet _assemblyNames; + private readonly string _assemblySetName; + + public MustBeIncludedInSetOfAssembliesConventionSpecification(IEnumerable assemblyNames, string assemblySetName) + { + _assemblyNames = new HashSet(assemblyNames.Select(assembly => assembly.GetName().Name)); + _assemblySetName = assemblySetName; + } + + protected override ConventionResult IsSatisfiedByLegacyCsprojFormat(string assemblyName, XDocument projectDocument) + { + return IsSatisfiedBy(assemblyName, projectDocument); + } + + protected override ConventionResult IsSatisfiedBy(string assemblyName, XDocument projectDocument) + { + if (_assemblyNames.Contains(assemblyName)) + { + return ConventionResult.Satisfied(assemblyName); + } + + return ConventionResult.NotSatisfied(assemblyName, string.Format(FailureMessage, assemblyName)); + } + + protected override string FailureMessage => "{0} is not included in " + _assemblySetName; + } +} \ 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..d14acdb --- /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) + { + // 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/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 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/MustSetPropertyValueAssemblyConventionSpecification.cs b/src/Core/Conventional/Conventions/Assemblies/MustSetPropertyValueAssemblyConventionSpecification.cs new file mode 100644 index 0000000..175b0a2 --- /dev/null +++ b/src/Core/Conventional/Conventions/Assemblies/MustSetPropertyValueAssemblyConventionSpecification.cs @@ -0,0 +1,40 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using System.Xml.XPath; + +namespace Conventional.Conventions.Assemblies +{ + public class MustSetPropertyValueAssemblyConventionSpecification : AssemblyConventionSpecification + { + private string ExpectedPropertyName { get; } + private string ExpectedPropertyValue { get; } + + public MustSetPropertyValueAssemblyConventionSpecification(string expectedPropertyName, string expectedPropertyValue) + { + ExpectedPropertyName = expectedPropertyName; + ExpectedPropertyValue = expectedPropertyValue; + } + + protected override ConventionResult IsSatisfiedByLegacyCsprojFormat(string assemblyName, XDocument projectDocument) + { + return IsSatisfiedBy(assemblyName, projectDocument); + } + + protected override ConventionResult IsSatisfiedBy(string assemblyName, 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/PropertyGroup/{ExpectedPropertyName} + var matchingProperties = projectDocument.XPathSelectElements($"/*[local-name() = 'Project']/*[local-name() = 'PropertyGroup']/*[local-name() = '{ExpectedPropertyName}']") + .Select(propertyElement => propertyElement.Value) + .Where(propertyValue => string.Equals(ExpectedPropertyValue, propertyValue, StringComparison.InvariantCulture)); + + return matchingProperties.Count() == 1 + ? ConventionResult.Satisfied(assemblyName) + : ConventionResult.NotSatisfied(assemblyName, string.Format(FailureMessage, assemblyName)); + } + + protected override string FailureMessage => "{0} should have property " + ExpectedPropertyName + " with value " + ExpectedPropertyValue; + } +} \ No newline at end of file diff --git a/src/Core/Conventional/Conventions/Assemblies/MustTreatWarningAsErrorAssemblyConventionSpecification.cs b/src/Core/Conventional/Conventions/Assemblies/MustTreatWarningAsErrorAssemblyConventionSpecification.cs new file mode 100644 index 0000000..690b2ed --- /dev/null +++ b/src/Core/Conventional/Conventions/Assemblies/MustTreatWarningAsErrorAssemblyConventionSpecification.cs @@ -0,0 +1,42 @@ +using System; +using System.Linq; +using System.Xml.Linq; +using System.Xml.XPath; + +namespace Conventional.Conventions.Assemblies +{ + public class MustTreatWarningAsErrorAssemblyConventionSpecification : AssemblyConventionSpecification + { + private readonly string _warning; + + public MustTreatWarningAsErrorAssemblyConventionSpecification(string warning) + { + _warning = warning; + } + + protected override ConventionResult IsSatisfiedByLegacyCsprojFormat(string assemblyName, XDocument projectDocument) + { + return IsSatisfiedBy(assemblyName, projectDocument); + } + + protected override ConventionResult IsSatisfiedBy(string assemblyName, 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/ProjectReference + var warningsAsErrors = projectDocument.XPathSelectElements("/*[local-name() = 'Project']/*[local-name() = 'PropertyGroup']/*[local-name() = 'WarningsAsErrors']") + .SingleOrDefault() + ?.Value ?? ""; + + var warnings = warningsAsErrors.Split(new[] {' ', ','}, StringSplitOptions.RemoveEmptyEntries); + if (!warnings.Contains(_warning)) + { + return ConventionResult.NotSatisfied(assemblyName, string.Format(FailureMessage, assemblyName, _warning)); + } + + return ConventionResult.Satisfied(assemblyName); + } + + protected override string FailureMessage => "Assembly {0} should treat warning {1} as an error but does not"; + } +} \ 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/Conventional/Conventions/Cecil/MustNotUseGuidNewGuidConventionSpecification.cs b/src/Core/Conventional/Conventions/Cecil/MustNotUseGuidNewGuidConventionSpecification.cs new file mode 100644 index 0000000..10c09e6 --- /dev/null +++ b/src/Core/Conventional/Conventions/Cecil/MustNotUseGuidNewGuidConventionSpecification.cs @@ -0,0 +1,21 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; + +namespace Conventional.Conventions.Cecil +{ + public class MustNotUseGuidNewGuidConventionSpecification : + MustNotCallMethodConventionSpecification + { + private static MethodInfo GetMethod() + { + Expression> expr = () => Guid.NewGuid(); + return ((MethodCallExpression)expr.Body).Method; + } + + public MustNotUseGuidNewGuidConventionSpecification() + : base(new[] { GetMethod() }) + { + } + } +} \ No newline at end of file diff --git a/src/Core/TestSolution/TestProjectTwo/TestProjectTwo.csproj b/src/Core/TestSolution/TestProjectTwo/TestProjectTwo.csproj index 214cfcf..a68ce45 100644 --- a/src/Core/TestSolution/TestProjectTwo/TestProjectTwo.csproj +++ b/src/Core/TestSolution/TestProjectTwo/TestProjectTwo.csproj @@ -11,6 +11,7 @@ TestProjectTwo v4.5.1 512 + CS0162,CS4014 true diff --git a/src/Core/TestSolution/TestProjectTwo/bin/Debug/TestProjectTwo.dll b/src/Core/TestSolution/TestProjectTwo/bin/Debug/TestProjectTwo.dll index 36c4a79..eecc5f6 100644 Binary files a/src/Core/TestSolution/TestProjectTwo/bin/Debug/TestProjectTwo.dll and b/src/Core/TestSolution/TestProjectTwo/bin/Debug/TestProjectTwo.dll differ 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..6b7bc08 --- /dev/null +++ b/src/Core/TestSolution/TestSolution.TestProject/SdkClassLibrary1/SdkClassLibrary1.csproj @@ -0,0 +1,24 @@ + + + + 42 + Potato + Carrot + + + + netstandard2.1 + + + + CS0162 + + + + + 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