-
Notifications
You must be signed in to change notification settings - Fork 389
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Validate C# DefineConstants input #9612
base: main
Are you sure you want to change the base?
Conversation
…nvert DefineConstants input to semicolon-separated list before saving
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC VB handles these properties differently to C#, so we should make sure to test both languages. @melytc do you recall the details? I think VB allows you to have values (i.e. A=1
) for define constants, rather than just names (i.e. A
).
...Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Debug/KeyValuePairListEncoding.cs
Outdated
Show resolved
Hide resolved
...Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Debug/KeyValuePairListEncoding.cs
Outdated
Show resolved
Hide resolved
...perties/InterceptedProjectProperties/BuildPropertyPage/DefineConstantsCSharpValueProvider.cs
Outdated
Show resolved
Hide resolved
[InlineData("key1=value1;key2=value2;key3=value3", new[] { "key1", "value1", "key2", "value2", "key3", "value3" })] | ||
[InlineData("key1;key2=value2", new[] { "key1", "", "key2", "value2" })] | ||
[InlineData("key1;key2;key3=value3", new[] { "key1", "", "key2", "", "key3", "value3" })] | ||
[InlineData("key1;;;key3;;", new[] { "key1", "", "key3", "" })] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be good to cover some more inputs:
""
" "
"="
";"
If there are invalid inputs, a test that ensures that Parse
throws would be good. For example, "=="
, or null
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added test cases on those inputs except null, as the method accepts a non-nullable input string. But I wouldn't necessarily agree that there are invalid inputs possible here, since ==
should parse to an empty key, value =
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for adding more tests.
since == should parse to an empty key, value =
I'm less confident about that than you. I feel like ==
should be an invalid and ambiguous input that throws. If values are expected to contain delimiters, then they should be escaped.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like == should be an invalid and ambiguous input that throws.
I'm not entirely in agreement; the difference between = and the other delimiters is that =
has no meaning other than as part of a value after already encountering an =
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean that it's ambiguous whether the name or value is =
. Values also cannot contain commas without escaping.
What happens if the Format
method is given names/values that contain =
or ,
? My guess is that these values would not round-trip correctly.
For robustness, we should either throw on invalid inputs, or implement escaping so that these values are handled correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if you enter strings in the UI containing =
and ,
?
...jectSystem.Managed.VS.UnitTests/ProjectSystem/VS/Properties/KeyValuePairListEncodingTests.cs
Show resolved
Hide resolved
… in the property value directly
@drewnoakes I've updated this now and paired with CPS changes to bring this control to a state I feel happy with. Specifically, the additional change I made here, other than adding more tests, is to in |
that's right! VB uses this syntax: In the project properties UI, we have a different control for VB: @adamint are these changes that you are proposing only for the C# control? |
...Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/Debug/KeyValuePairListEncoding.cs
Show resolved
Hide resolved
yes the vb property has a different control and property interceptor! this is only for c# |
...perties/InterceptedProjectProperties/BuildPropertyPage/DefineConstantsCSharpValueProvider.cs
Outdated
Show resolved
Hide resolved
|
||
internal const string DefineConstantsRecursivePrefix = "$(DefineConstants)"; | ||
|
||
private HashSet<string> _removedValues = []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure it's safe to store this state here. These providers are generally safe to call concurrently. By tracking this state here, that's no longer the case. It might work most of the time, but it makes me a little nervous. I'll spend some more time here to see if I can come up with any suggestions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've removed this state and instead handle collapsing multiple values down into one inside the CPS control, as provided by a regex rule in editor metadata. Thanks, your comment prompted me to rethink how this worked.
...perties/InterceptedProjectProperties/BuildPropertyPage/DefineConstantsCSharpValueProvider.cs
Outdated
Show resolved
Hide resolved
# Conflicts: # tests/Microsoft.VisualStudio.ProjectSystem.Managed.UnitTests/Mocks/ConfiguredProjectFactory.cs
this is a blast from the past! |
if ((allowsEmptyKey && !string.IsNullOrEmpty(decodedEntryValue)) | ||
|| !string.IsNullOrEmpty(decodedEntryKey) || !string.IsNullOrEmpty(decodedEntryValue)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this right?
Logically, (A && B) || B
is the same as just B
.
Where A
is allowsEmptyKey
and B
is !string.IsNullOrEmpty(decodedEntryValue)
.
If I'm not mistaken, allowsEmptyKey
does nothing here.
I think a !
should be removed:
if ((allowsEmptyKey && !string.IsNullOrEmpty(decodedEntryValue)) | |
|| !string.IsNullOrEmpty(decodedEntryKey) || !string.IsNullOrEmpty(decodedEntryValue)) | |
if ((allowsEmptyKey && string.IsNullOrEmpty(decodedEntryValue)) | |
|| !string.IsNullOrEmpty(decodedEntryKey) || !string.IsNullOrEmpty(decodedEntryValue)) |
But the fact that this passed the unit tests makes me wonder if the tests need improving or if the check can just be removed here altogether.
// Copied from ActiveLaunchProfileEnvironmentVariableValueProvider in the .NET Project System. | ||
// In future, EnvironmentVariablesNameValueListEncoding should be exported from that code base and imported here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this comment still correct? With the changes here, these implementations have diverged.
@@ -32,4 +33,6 @@ public static ConfiguredProject ImplementUnconfiguredProject(UnconfiguredProject | |||
|
|||
return mock.Object; | |||
} | |||
|
|||
internal interface ITestConfiguredProjectImpl : ConfiguredProject, ConfiguredProject2; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than introducing a new interface, we can use Moq's built-in support for this.
https://github.com/devlooped/moq/wiki/Quickstart#advanced-features
From memory I think it's something like (those docs aren't super clear):
var mock = new Mock<ConfiguredProject>();
mock.Setup(...);
...
var mock2 = mock.As<ConfiguredProject2>();
mock2.Setup(c => c.EnsureProjectEvaluatedAsync()).Returns(Task.CompletedTask);
return mock.Object;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though creating a new interface was a pretty clever way to achieve the same thing. Use whichever you prefer. I don't think there's much material difference.
As can be seen in this bug, you can add arbitrary semicolons in the DefineConstants control and these will be saved. This PR
Microsoft Reviewers: Open in CodeFlow