Skip to content

Conversation

@JaynieBai
Copy link
Member

@JaynieBai JaynieBai commented Feb 7, 2023

Fixes #8205 and #1030

Context

The referenced function GetMetadataEscaped of Item metadata function dosen't distinguish between "metadata not present" and "present but set the empty string". Both of them return the same empty string. So the following functions

AnyHaveMetdataValue
HasMetadata
WithMetadataValue

Can't tell between "metadata not present" and "present but set the empty string".

Changes Made

Add GetMetadataValueEscaped with one more parameter returnNullIfNotFound that is true that returns null when not present.
Add one new function WithOutMetadataValue

Testing

MetadataFuntionTestingWithEmtpyString

Notes

@JaynieBai JaynieBai marked this pull request as ready for review February 7, 2023 14:46
Copy link
Contributor

@Forgind Forgind left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

Copy link
Member

@ladipro ladipro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Leaving a few comments inline.

Comment on lines 906 to 907
if (value == null) { return false; }
else { return true; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
if (value == null) { return false; }
else { return true; }
return value != null;

Comment on lines 1642 to 1645
if (string.IsNullOrEmpty(name))
{
ErrorUtilities.VerifyThrowArgumentLength(name, nameof(name));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this a breaking change? It looks like the method was previously OK with taking an empty string, now it throws.

Copy link
Member Author

@JaynieBai JaynieBai Feb 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think it's necessary. HasMetadata parameter and GetMetadata parameter should have the same limitation. If name is null, Dictionary _directMetadata?.Contains(name) will throw exception.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't seem to be true. _directMetadata?.Contains(name) does not throw on null.

If there is a path that doesn't handle null, I think it would be reasonable to add a proper argument check here. I would be opposed to checking for an empty string, though, unless there is a good reason. The breaking potential is non-trivial.

Also, changes in the behavior of the public API should always come with the corresponding test coverage.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't seem to be true. _directMetadata?.Contains(name) does not throw on null.

If there is no null check. this will throw the exception as bellow. I think this exception message is not easier to undertand than ErrorUtilities.VerifyThrowArgumentLength(name, nameof(name)); And for empty string check, I think it's same with GetMetadata. but in order to avoid new potential issues, I will only check null currently.

image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have added the tests for the new public API

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, you're right, it throws on null. Thank you for making the changes in HasMetadata. The new GetMetadataValueEscaped still doesn't take empty string now. Do you think it would make sense to unify the behavior with HasMetadata and validate the argument so it throw only on null?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new GetMetadataValueEscaped still doesn't take empty string now. Do you think it would make sense to unify the behavior with HasMetadata and validate the argument so it throw only on null?

@Forgind what’s your idea?

}

return value ?? String.Empty;
return GetMetadataValueEscaped(name, false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super-nit:

Suggested change
return GetMetadataValueEscaped(name, false);
return GetMetadataValueEscaped(name, returnNullIfNotFound: false);

Comment on lines 1642 to 1645
if (string.IsNullOrEmpty(name))
{
ErrorUtilities.VerifyThrowArgumentLength(name, nameof(name));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't seem to be true. _directMetadata?.Contains(name) does not throw on null.

If there is a path that doesn't handle null, I think it would be reasonable to add a proper argument check here. I would be opposed to checking for an empty string, though, unless there is a good reason. The breaking potential is non-trivial.

Also, changes in the behavior of the public API should always come with the corresponding test coverage.

/// Test metadata item functions with empty string metadata and not present metadata
/// </summary>
[Fact]
public void MetadataFuntionTestingWithEmtpyString()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Typo Funtion.

Comment on lines 67 to 71
/// <summary>
/// Returns the metadata with the specified key.
/// Returns null if returnNullIfNotFound is true otherwise returns empty string when metadata not present
/// </summary>
string GetMetadataValueEscaped(string name, bool returnNullIfNotFound);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Can you please move this up so the two overloads of GetMetadataValueEscaped are next to each other?

Comment on lines 1642 to 1645
if (string.IsNullOrEmpty(name))
{
ErrorUtilities.VerifyThrowArgumentLength(name, nameof(name));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, you're right, it throws on null. Thank you for making the changes in HasMetadata. The new GetMetadataValueEscaped still doesn't take empty string now. Do you think it would make sense to unify the behavior with HasMetadata and validate the argument so it throw only on null?

/// Returns the metadata with the specified key.
/// Returns null if returnNullIfNotFound is true otherwise returns empty string when metadata not present
/// </summary>
public string GetMetadataValueEscaped(string name, bool returnNullIfNotFound)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it intentional to expose the method publicly? The existing overload is implemented explicitly, which keeps it internal.

Suggested change
public string GetMetadataValueEscaped(string name, bool returnNullIfNotFound)
string IItem.GetMetadataValueEscaped(string name, bool returnNullIfNotFound)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it‘s same with HasMetadata,SetMetadata and GetMetada, so make is public

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, HasMetadata, GetMetadataValue, and SetMetadataValue are public.

But the existing GetMetadataValueEscaped(string name) is not, so unless there's a need to expose the new GetMetadataValueEscaped(string name, bool returnNullIfNotFound), it should stay internal.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. I will update that.

Copy link
Member

@rainersigwald rainersigwald left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no problem adding WithoutMetadataValue, but I still don't think there should be a meaningful distinction between "metadata is set to empty" and "metadata is not set". Does the former require the latter?

@JaynieBai
Copy link
Member Author

I have no problem adding WithoutMetadataValue, but I still don't think there should be a meaningful distinction between "metadata is set to empty" and "metadata is not set". Does the former require the latter?

Yes, the former WithoutMetadataValue require the latter. In the related tests. if there is no distinction, WithoutMetadataValue('A', '') will output [One|Two] but it should be [One|Two|Four]
WithoutMetadataValue('C', '') will output [], but it should be [One|Two|Three|Four]

@rainersigwald
Copy link
Member

if there is no distinction, WithoutMetadataValue('A', '') will output [One|Two] but it should be [One|Two|Four]

I don't think I agree. I think "undefined" and "defined but set to empty string" should be equivalent. Compare how these two work with your changes:

    <Message Importance="high" Text="WithoutMetadataValueAEmpty: [@(_Item->WithoutMetadataValue('A', ''), '|')]"/>
    <Message Importance="high" Text="BatchCondition: [@(_Item, '|')]" Condition=" '%(_Item.A)' != '' "/>
$ ./.dotnet/dotnet msbuild foo.proj
MSBuild version 17.6.0-dev-23128-01+cec2cb290 for .NET

  WithoutMetadataValueAEmpty: [One|Two|Four]
  BatchCondition: [One]
  BatchCondition: [Two]

I think they should match.

@JaynieBai
Copy link
Member Author

JaynieBai commented Mar 3, 2023

if there is no distinction, WithoutMetadataValue('A', '') will output [One|Two] but it should be [One|Two|Four]

I don't think I agree. I think "undefined" and "defined but set to empty string" should be equivalent. Compare how these two work with your changes:

    <Message Importance="high" Text="WithoutMetadataValueAEmpty: [@(_Item->WithoutMetadataValue('A', ''), '|')]"/>
    <Message Importance="high" Text="BatchCondition: [@(_Item, '|')]" Condition=" '%(_Item.A)' != '' "/>
$ ./.dotnet/dotnet msbuild foo.proj
MSBuild version 17.6.0-dev-23128-01+cec2cb290 for .NET

  WithoutMetadataValueAEmpty: [One|Two|Four]
  BatchCondition: [One]
  BatchCondition: [Two]

I think they should match.

Starting from this example code.

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <ItemGroup>
     <_Item Include="One">
       <A></A>
     </_Item>
     <_Item Include="Two">
       <B></B>
     </_Item>
   </ItemGroup>
    <Target Name="MetadataValueEmpty">
      <Message Text="WithMetadataValueAEmpty: [@(_Item->WithMetadataValue('A', ''), '|')]"/>
      <Message Text="HasMetadataA: [@(_Item->HasMetadata('A'), '|')]"/>
      <!-- <Message Text="WithoutMetadataValueAEmpty: [@(_Item->WithoutMetadataValue('A', ''), '|')]"/> -->
      <Message Text="AnyHaveMetadataValueAEmpty: [@(_Item->AnyHaveMetadataValue('A', ''), '|')]"/>
      <Message Text="WithMetadataValueCEmpty: [@(_Item->WithMetadataValue('C', ''), '|')]"/>
      <Message Text="HasMetadataC: [@(_Item->HasMetadata('C'), '|')]"/>
      <!-- <Message Text="WithoutMetadataValueCEmpty: [@(_Item->WithoutMetadataValue('C', ''), '|')]"/> -->
      <Message Text="AnyHaveMetadataValueCEmpty: [@(_Item->AnyHaveMetadataValue('C', ''), '|')]"/>
      <Message Importance="high" Text="BatchConditionA: [@(_Item, '|')]" Condition=" '%(_Item.A)' == '' "/>
      <Message Importance="high" Text="BatchConditionC: [@(_Item, '|')]" Condition=" '%(_Item.C)' == '' "/>
   </Target>
 </Project>

Build as original MSBuild. The results are as following.

  1. There is no metadata "C" in the above example. But WithMetadataValueCEmpty, AnyHaveMetadataValueCEmpty and BatchConditionC, their results return metadata "C" is existed.
  2. For Metadata "A", HasMetaA returns nothing, but others conflict with HasMetadata and return that metadata "A" is existed. So, I think it's necessary to distinguish "undefined" and "defined but set to empty string".
  WithMetadataValueAEmpty: [One|Two]
  HasMetadataA: []
  AnyHaveMetadataValueAEmpty: [true]
  WithMetadataValueCEmpty: [One|Two]
  HasMetadataC: []
  AnyHaveMetadataValueCEmpty: [true]
  BatchConditionA: [One|Two]
  BatchConditionC: [One|Two]

After my changes. The outputs as following. But BatchCondition Condition=" '%(_Item.A)' == '' " should match with WithMetadataValue, that's an issue.

  WithMetadataValueAEmpty: [One]
  HasMetadataA: [One]
  AnyHaveMetadataValueAEmpty: [true]
  WithMetadataValueCEmpty: []
  HasMetadataC: []
  AnyHaveMetadataValueCEmpty: [false]
  BatchConditionA: [One|Two]
  BatchConditionC: [One|Two]

@rainersigwald rainersigwald modified the milestones: VS 17.6, VS 17.7 Mar 28, 2023
@Forgind
Copy link
Contributor

Forgind commented Apr 24, 2023

Team triage:
We decided that adding a new way to distinguish between a metadatum that has been set at some point but is currently empty and a metadatum that was never set is not how we typically deal with undefined values in MSBuild and should not be added at this point. With that in mind, we want the WithoutMetadataValue function but would prefer to skip the other part of this PR.

@JanKrivanek
Copy link
Member

JanKrivanek commented Jun 2, 2023

@JaynieBai - do you plan to update this PR based on Forgind's last comment?

@JaynieBai
Copy link
Member Author

do you plan to update this PR based on Forgind's last comment?

Thanks for your reminder. I missed that before. I will update this PR soon.

@JaynieBai
Copy link
Member Author

Team triage:
We decided that adding a new way to distinguish between a metadatum that has been set at some point but is currently empty and a metadatum that was never set is not how we typically deal with undefined values in MSBuild and should not be added at this point. With that in mind, we want the WithoutMetadataValue function but would prefer to skip the other part of this PR.

Add the WithoutMetadataValue function in another PR #8867. So close this PR.

@JaynieBai JaynieBai closed this Jun 12, 2023
@JaynieBai JaynieBai deleted the jennybai/issue8205 branch June 12, 2023 08:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consider adding a WithoutMetadataValue msbuild item function

6 participants