Skip to content

Commit

Permalink
[andrewabestGH-87] Added convention to enforce a project file sets a …
Browse files Browse the repository at this point in the history
…property

Some examples:
- Convention.MustSetPropertyValue("Nullable", "enable") // Ensure nullable types are enabled for the project
- Convention.MustSetPropertyValue("IsPackable", "true") // Ensure the project can be packed for NuGet

See https://learn.microsoft.com/en-us/visualstudio/msbuild/property-element-msbuild?view=vs-2022#example
  • Loading branch information
eddie.stanley committed Jan 6, 2024
1 parent 41f311f commit be5b0b6
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -281,5 +281,60 @@ public void MustNotReferencePackage_Failure()

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")); // There's no <Vegetable>Turnip</Vegetable> 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")); // There's no <ThisPropertyShouldNeverEverExist>x</ThisPropertyShouldNeverEverExist> in the csproj

result.IsSatisfied.Should().BeFalse();
result.Failures.Single().Should().Be("SdkClassLibrary1 should have property ThisPropertyShouldNeverEverExist with value x");
}
}
}
13 changes: 13 additions & 0 deletions src/Core/Conventional/Convention.Assembly.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,18 @@ public static MustNotReferencePackageAssemblyConventionSpecification MustNotRefe
{
return new MustNotReferencePackageAssemblyConventionSpecification(packageName);
}

/// <summary>
/// Require this project to set a <see href="https://learn.microsoft.com/en-us/visualstudio/msbuild/property-element-msbuild?view=vs-2022#example">property value</see>
/// </summary>
/// <param name="propertyName">The name of the property</param>
/// <param name="value">The value the property should have</param>
/// <remarks>This convention is currently ignorant of <see href="https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-conditions">MSBuild conditions</see></remarks>
/// <remarks>This convention is unaware of shared build prop files (Directory.Build.Props + Directory.Build.Targets) - see https://github.com/andrewabest/Conventional/issues/88</remarks>
public static MustSetPropertyValueAssemblyConventionSpecification MustSetPropertyValue(
string propertyName, string value)
{
return new MustSetPropertyValueAssemblyConventionSpecification(propertyName, value);
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
{
// 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/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;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TheUniversalAnswer>42</TheUniversalAnswer>
<Vegetable>Potato</Vegetable>
<Vegetable>Carrot</Vegetable>
</PropertyGroup>

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>
Expand Down

0 comments on commit be5b0b6

Please sign in to comment.