.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.0s
❯ 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 : 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.