dotnet / msbuild

The Microsoft Build Engine (MSBuild) is the build platform for .NET and Visual Studio.

Home Page:https://docs.microsoft.com/visualstudio/msbuild/msbuild

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

.NET Framework MSBuild doesn't cast `char` to `string` in property functions

rainersigwald opened this issue · comments

Consider

<Project>
  <Target Name="Go">
    <Warning Text="$(P.EndsWith($([System.IO.Path]::DirectorySeparatorChar)))" />
  </Target>
</Project>

This works with .NET 8 MSBuild:

dotnet msbuild .\test.proj -p:P=\Some\Path\
MSBuild version 17.10.0-preview-24162-02+0326fd7c9 for .NET
  test succeeded with warnings (0.0s)
    S:\repro\dotnet\razor\pull\10220\test.proj(3,5): warning : True [S:\repro\dotnet\razor\pull\10220\test.proj]

Build succeeded with warnings in 0.0sdotnet msbuild .\test.proj -p:P=\Some\Path
MSBuild version 17.10.0-preview-24162-02+0326fd7c9 for .NET
  test succeeded with warnings (0.0s)
    S:\repro\dotnet\razor\pull\10220\test.proj(3,5): warning : False [S:\repro\dotnet\razor\pull\10220\test.proj]

Build succeeded with warnings in 0.0s

But MSBuild.exe doesn't like it:

msbuild .\test.proj -p:P=\Some\Path\
  test failed with 1 error(s) (0.0s)
    S:\repro\dotnet\razor\pull\10220\test.proj(3,14): error MSB4186: Invalid static method invocation syntax: "P.EndsWith($([System.IO.Path]::DirectorySeparatorChar))". Object of type 'System.Char' cannot be converted to type 'System.String'. Static method invocation should be of the form: $([FullTypeName]::Method()), e.g. $([System.IO.Path]::Combine(`a`, `b`)). Check that all parameters are defined, are of the correct type, and are specified in the right order.

Build failed with 1 error(s) in 0.0s

(seen while investigating dotnet/razor#10220)

Workaround

You can wrap the char-generating method:

diff --git a/test.proj b/test.proj
index 87fa27a..d08cd98 100644
--- a/test.proj
+++ b/test.proj
@@ -1,5 +1,5 @@
 <Project>
   <Target Name="Go">
-    <Warning Text="$(P.EndsWith($([System.IO.Path]::DirectorySeparatorChar)))" />
+    <Warning Text="$(P.EndsWith($([System.String]::new($([System.IO.Path]::DirectorySeparatorChar)))))" />
   </Target>
 </Project>

The logic in LateBindExecute looks wonky: it first searches for a method that has the correct number of parameters and where all parameters are strings; then if it finds one, it calls that via MethodInfo.Invoke even if the arguments are not strings. If it did not find such a method, then it would use CoerceArguments, which can convert Char to String via Convert.ChangeType.

On .NET Core 2.0 and greater, String.EndsWith(Char) already exists; I guess Type.InvokeMember calls that and LateBindExecute is not entered.

This works too:

<Project>
  <Target Name="Go">
    <Warning Text="$(P.EndsWith($([System.IO.Path]::DirectorySeparatorChar), 'StringComparison.Ordinal'))" />
  </Target>
</Project>

because there is no String.EndsWith(String, String) method and thus LateBindExecute resorts to String.EndsWith(String, StringComparison) and goes to the CoerceArguments path.

Would it be possible to change the language so that quoting the argument as in $(P.EndsWith('$([System.IO.Path]::DirectorySeparatorChar)')) always coerces it to String?

Maybe? I certainly tried that while investigating this so I like the general idea. I haven't thought through the implications though.