Skip to content

Commit fa41623

Browse files
Adds validation to throw MSB4259 when property references contain leading or trailing whitespace outside of conditions. (#13076)
1 parent 33d9fd7 commit fa41623

18 files changed

Lines changed: 247 additions & 74 deletions

documentation/wiki/ChangeWaves.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ A wave of features is set to "rotate out" (i.e. become standard functionality) t
2424

2525
## Current Rotation of Change Waves
2626

27+
### 18.5
28+
- [Throw MSB4281 error for property references and property function calls with leading or trailing whitespace (e.g., `$( Foo )`, `$( Foo.StartsWith('Bar') )`).](https://github.com/dotnet/msbuild/pull/13076)
29+
2730
### 18.4
2831
- [Start throwing on null or empty paths in MultiProcess and MultiThreaded Task Environment Drivers.](https://github.com/dotnet/msbuild/pull/12914)
2932

src/Build.UnitTests/Evaluation/Expander_Tests.cs

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3854,14 +3854,71 @@ public void PropertyFunctionStaticMethodIntrinsicBitOperations()
38543854
[Fact]
38553855
public void PropertySimpleSpaced()
38563856
{
3857-
PropertyDictionary<ProjectPropertyInstance> pg = new PropertyDictionary<ProjectPropertyInstance>();
3858-
pg.Set(ProjectPropertyInstance.Create("SomeStuff", "This IS SOME STUff"));
3857+
using (TestEnvironment env = TestEnvironment.Create())
3858+
{
3859+
env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_4.ToString());
38593860

3860-
Expander<ProjectPropertyInstance, ProjectItemInstance> expander = new Expander<ProjectPropertyInstance, ProjectItemInstance>(pg, FileSystems.Default);
3861+
PropertyDictionary<ProjectPropertyInstance> pg = new PropertyDictionary<ProjectPropertyInstance>();
3862+
pg.Set(ProjectPropertyInstance.Create("SomeStuff", "This IS SOME STUff"));
38613863

3862-
string result = expander.ExpandIntoStringLeaveEscaped(@"$( SomeStuff )", ExpanderOptions.ExpandProperties, MockElementLocation.Instance);
3864+
Expander<ProjectPropertyInstance, ProjectItemInstance> expander = new Expander<ProjectPropertyInstance, ProjectItemInstance>(pg, FileSystems.Default);
38633865

3864-
Assert.Equal(String.Empty, result);
3866+
string result = expander.ExpandIntoStringLeaveEscaped(@"$( SomeStuff )", ExpanderOptions.ExpandProperties, MockElementLocation.Instance);
3867+
3868+
Assert.Equal(String.Empty, result);
3869+
}
3870+
}
3871+
3872+
/// <summary>
3873+
/// Expand a property reference that has whitespace around the property name should throw error MSB4281
3874+
/// when ChangeWave 18.5 is enabled
3875+
/// </summary>
3876+
[Theory]
3877+
[InlineData("$( SomeStuff )")] // Leading and trailing space
3878+
[InlineData("$( SomeStuff)")] // Leading space only
3879+
[InlineData("$(SomeStuff )")] // Trailing space only
3880+
public void PropertyWithWhitespace_ShouldThrowError_WhenChangeWaveEnabled(string expression)
3881+
{
3882+
using (TestEnvironment env = TestEnvironment.Create())
3883+
{
3884+
env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", "");
3885+
3886+
PropertyDictionary<ProjectPropertyInstance> pg = new PropertyDictionary<ProjectPropertyInstance>();
3887+
pg.Set(ProjectPropertyInstance.Create("SomeStuff", "This IS SOME STUff"));
3888+
3889+
Expander<ProjectPropertyInstance, ProjectItemInstance> expander = new Expander<ProjectPropertyInstance, ProjectItemInstance>(pg, FileSystems.Default);
3890+
3891+
InvalidProjectFileException ex = Assert.Throws<InvalidProjectFileException>(
3892+
() => expander.ExpandIntoStringLeaveEscaped(expression, ExpanderOptions.ExpandProperties, MockElementLocation.Instance));
3893+
3894+
Assert.Equal("MSB4281", ex.ErrorCode);
3895+
}
3896+
}
3897+
3898+
/// <summary>
3899+
/// Expand a property function call that has whitespace around the property name should throw error MSB4281
3900+
/// when ChangeWave 18.5 is enabled
3901+
/// </summary>
3902+
[Theory]
3903+
[InlineData("$( SomeStuff.StartsWith('This'))")] // Leading space only
3904+
[InlineData("$(SomeStuff.StartsWith('This') )")] // Trailing space only
3905+
[InlineData("$( SomeStuff.StartsWith('This') )")] // Leading and trailing space
3906+
public void PropertyFunctionWithWhitespace_ShouldThrowError_WhenChangeWaveEnabled(string expression)
3907+
{
3908+
using (TestEnvironment env = TestEnvironment.Create())
3909+
{
3910+
env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", "");
3911+
3912+
PropertyDictionary<ProjectPropertyInstance> pg = new PropertyDictionary<ProjectPropertyInstance>();
3913+
pg.Set(ProjectPropertyInstance.Create("SomeStuff", "This IS SOME STUff"));
3914+
3915+
Expander<ProjectPropertyInstance, ProjectItemInstance> expander = new Expander<ProjectPropertyInstance, ProjectItemInstance>(pg, FileSystems.Default);
3916+
3917+
InvalidProjectFileException ex = Assert.Throws<InvalidProjectFileException>(
3918+
() => expander.ExpandIntoStringLeaveEscaped(expression, ExpanderOptions.ExpandProperties, MockElementLocation.Instance));
3919+
3920+
Assert.Equal("MSB4281", ex.ErrorCode);
3921+
}
38653922
}
38663923

38673924
[WindowsOnlyFact]

src/Build/Evaluation/Expander.cs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1385,7 +1385,30 @@ internal static object ExpandPropertiesLeaveTypedAndEscaped(
13851385
}
13861386
else // This is a regular property
13871387
{
1388-
propertyValue = LookupProperty(properties, expression, propertyStartIndex + 2, propertyEndIndex - 1, elementLocation, propertiesUseTracker);
1388+
int propertyNameStart = propertyStartIndex + 2;
1389+
int propertyNameEnd = propertyEndIndex - 1;
1390+
1391+
// Check for whitespace in property name - this is likely a typo
1392+
// Gated behind ChangeWave 18.5 as this is a breaking change
1393+
if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_5))
1394+
{
1395+
// Check if there's leading or trailing whitespace
1396+
if (Char.IsWhiteSpace(expression[propertyNameStart]) || Char.IsWhiteSpace(expression[propertyNameEnd]))
1397+
{
1398+
// Find the position of the whitespace for error message
1399+
int whitespacePosition = Char.IsWhiteSpace(expression[propertyNameStart])
1400+
? propertyNameStart
1401+
: propertyNameEnd;
1402+
1403+
ProjectErrorUtilities.ThrowInvalidProject(
1404+
elementLocation,
1405+
"IllFormedPropertySpaceInPropertyReference",
1406+
expression.Substring(propertyStartIndex, propertyEndIndex - propertyStartIndex + 1),
1407+
whitespacePosition - propertyStartIndex + 1);
1408+
}
1409+
}
1410+
1411+
propertyValue = LookupProperty(properties, expression, propertyNameStart, propertyNameEnd, elementLocation, propertiesUseTracker);
13891412
}
13901413

13911414
if (propertyValue != null)
@@ -1434,7 +1457,27 @@ internal static object ExpandPropertyBody(
14341457
Function<T> function = null;
14351458
string propertyName = propertyBody;
14361459

1437-
// Trim the body for compatibility reasons:
1460+
// Check for whitespace in property body - this is likely a typo
1461+
// Gated behind ChangeWave 18.5 as this is a breaking change
1462+
if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_5))
1463+
{
1464+
if (Char.IsWhiteSpace(propertyBody[0]) || Char.IsWhiteSpace(propertyBody[propertyBody.Length - 1]))
1465+
{
1466+
// Calculate the position of the whitespace for error message
1467+
// Position is 1-based, relative to the full property reference $({propertyBody})
1468+
int whitespacePosition = Char.IsWhiteSpace(propertyBody[0])
1469+
? 3 // Position after "$("
1470+
: propertyBody.Length + 2; // Position before ")"
1471+
1472+
ProjectErrorUtilities.ThrowInvalidProject(
1473+
elementLocation,
1474+
"IllFormedPropertySpaceInPropertyReference",
1475+
$"$({propertyBody})",
1476+
whitespacePosition);
1477+
}
1478+
}
1479+
1480+
// Trim the body for compatibility reasons (when ChangeWave is disabled):
14381481
// Spaces are not valid property name chars, but $( Foo ) is allowed, and should always expand to BLANK.
14391482
// Do a very fast check for leading and trailing whitespace, and trim them from the property body if we have any.
14401483
// But we will do a property name lookup on the propertyName that we held onto.

src/Build/Resources/Strings.resx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,10 @@
529529
<value>MSB4259: Unexpected space at position "{1}" of condition "{0}". Did you forget to remove a space?</value>
530530
<comment>{StrBegin="MSB4259: "}</comment>
531531
</data>
532+
<data name="IllFormedPropertySpaceInPropertyReference" xml:space="preserve">
533+
<value>MSB4281: Unexpected space at position "{1}" of property reference "{0}". Did you forget to remove a space?</value>
534+
<comment>{StrBegin="MSB4281: "}</comment>
535+
</data>
532536
<data name="IllFormedQuotedStringInCondition" xml:space="preserve">
533537
<value>MSB4101: Expected a closing quote after position {1} in condition "{0}".</value>
534538
<comment>{StrBegin="MSB4101: "}</comment>
@@ -2462,7 +2466,7 @@ Utilization: {0} Average Utilization: {1:###.0}</value>
24622466
<!--
24632467
The Build message bucket is: MSB4000 - MSB4999
24642468
2465-
Next message code should be MSB4281
2469+
Next message code should be MSB4282
24662470
24672471
Don't forget to update this comment after using a new code.
24682472
-->

src/Build/Resources/xlf/Strings.cs.xlf

Lines changed: 10 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Build/Resources/xlf/Strings.de.xlf

Lines changed: 10 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Build/Resources/xlf/Strings.es.xlf

Lines changed: 10 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Build/Resources/xlf/Strings.fr.xlf

Lines changed: 10 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)