Skip to content
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

Implement WarningsNotAsErrors in the new property pages #6971

Merged
merged 1 commit into from
Feb 18, 2021

Conversation

tmeschter
Copy link
Contributor

@tmeschter tmeschter commented Feb 18, 2021

Related to #6720.

Background

The C# compiler provides a switch allowing the user to specify whether or not warnings in general should be treated as errors. In addition to that, specific warnings can be treated as errors, or blocked from being treated as errors. In the project file, these settings are stored in the TreatWarningsAsErrors, WarningsAsErrors, and WarningsNotAsErrors properties, respectively.

Currently, the project property pages only expose the first two of these. This change adds support for the third, WarningsNotAsErrors.

Explanation

The first thing to note is that all we have to do to get the new control to show up in the UI and to get the property persisted to the right place in the project file is to add a new StringProperty element to BuildPropertyPage.xaml. The Name attribute uniquely identifies the property within the Build page, and specifies the property name that is read and written in the project file.

Second, note the VisibilityCondition metadata on the new WarningsNotAsErrors and existing WarningsAsErrors property. We only want to show one or the other of these properties in the UI, not both. If warnings in general are being treated as errors it makes sense to only show WarningsNotAsErrors; but if warnings are not being treated as errors then we should only show WarningsAsErrors. The VisibilityCondition contains an expression that controls whether or not the control should be displayed.

Before I get into specifics of the expressions implemented here, it is important to note that TreatWarningsAsErrors, WarningsAsErrors, and WarningsNotAsErrors are all configuration dependent properties. That is, their values could vary from one configuration to another, and so the property effectively has multiple values--one per configuration. There are also many configuration independent properties; these have at most one value.

We want the WarningsAsErrors control to show up if TreatWarningsAsErrors is false in any configuration, and this is achieved with the following expression:

(has-evaluated-value "Build" "TreatWarningsAsErrors" "false")

This looks at all of the evaluated values of the TreatWarningsAsErrors property on the Build page and checks if any of them is "false"; if so, the overall expression evaluates to "true" and the control is displayed.

If the properties were configuration independent we could have gone with

(eq (evaluated "Build" "TreatWarningsAsErrors") "true")

to check if the evaluated value of TreatWarningsAsErrors is "true", or even just

(evaluated "Build" "TreatWarningsAsErrors")

Third, let's leave the .xaml file for the moment and look and the one piece of code, TreatWarningsAsErrorsValueProvider.cs. When the user toggles the value of TreatWarningsAsErrors we don't just want to hide or show WarningsAsErrors and/or WarningsNotAsErrors, we want to clear out the property values that no longer make sense. E.g., if we're setting TreatWarningsAsErrors to false, we can delete WarningsNotAsErrors entirely as it has no effect.

We accomplish this by implementing an IInterceptingPropertyValueProvider for TreatWarningsAsErrors. An intercepting value provider gets a chance to handle a property set before it is passed on for default handling; this allows it to manipulate the value or even other properties. It also has a chance to manipulate the values read from the default handler before they are passed on to the property pages, but we don't need this ability here.

However, if the user toggles TreatWarningsAsErrors back and forth we don't want to permanently delete whatever values they had for the other two properties. Rather, we squirrel them away in ITemporaryPropertyStorage before deleting them, and restore the value from there when switching back.

Fourth, go back to the .xaml file and take a look at the Rule.DataSource entry at the top of the page. The Persistence attribute indicates that the property values will read from and written to the "ProjectFile" handler which, of course, reads and writes to the project file itself (as opposed to the .user file or some other .props or .targets file). However, as we want to intercept the TreatWarningsAsErrors property we update its DataSource entry to instead specify the "ProjectFileWithInterception" persistence handler. This handler runs the property through an applicable interceptors before handing it off to the "ProjectFile" handler. Without this, the TreatWarningsAsErrorsValueProvider would never be called.

Fifth and finally, note that the values of WarningsAsErrors and WarningsNotAsErrors now depend on the value of TreatWarningsAsErrors. We need to tell the UI that when TreatWarningsAsErrors is updated, it should re-query the values of the other two. This is done via the DependsOn metadata added to both of those properties' entries. Without this, the UI may become out of sync with the actual values in the project as the user makes changes.

Microsoft Reviewers: Open in CodeFlow

Related to dotnet#6720.

### Background

The C# compiler provides a switch allowing the user to specify whether or not warnings _in general_ should be treated as errors. In addition to that, specific warnings can be treated as errors, or _blocked_ from being treated as errors. In the project file, these settings are stored in the `TreatWarningsAsErrors`, `WarningsAsErrors`, and `WarningsNotAsErrors` properties, respectively.

Currently, the project property pages only expose the first two of these. This change adds support for the third, `WarningsNotAsErrors`.

### Explanation

The first thing to note is that all we have to do to get the new control to show up in the UI *and* to get the property persisted to the right place in the project file is to add a new `StringProperty` element to BuildPropertyPage.xaml. The `Name` attribute uniquely identifies the property within the Build page, _and_ specifies the property name that is read and written in the project file.

Second, note the `VisibilityCondition` metadata on the new `WarningsNotAsErrors` and existing `WarningsAsErrors` property. We only want to show one or the other of these properties in the UI, not both. If warnings _in general_ are being treated as errors it makes sense to only show `WarningsNotAsErrors`; but if warnings are not being treated as errors then we should only show `WarningsAsErrors`. The `VisibilityCondition` contains an expression that controls whether or not the control should be displayed.

Before I get into specifics of the expressions implemented here, it is important to note that `TreatWarningsAsErrors`, `WarningsAsErrors`, and `WarningsNotAsErrors` are all configuration dependent properties. That is, their values could vary from one configuration to another, and so the property effectively has multiple values--one per configuration. There are also many configuration independent properties; these have at most one value.

We want the `WarningsAsErrors` control to show up if `TreatWarningsAsErrors` is false in _any_ configuration, and this is achieved with the following expression:

```
(has-evaluated-value "Build" "TreatWarningsAsErrors" "false")
```

This looks at all of the evaluated values of the `TreatWarningsAsErrors` property on the `Build` page and checks if any of them is "false"; if so, the overall expression evaluates to "true" and the control is displayed.

If the properties were configuration independent we could have gone with

```
(eq (evaluated "Build" "TreatWarningsAsErrors") "true")
```

to check if the evaluated value of TreatWarningsAsErrors is "true", or even just

```
(evaluated "Build" "TreatWarningsAsErrors")
```

Third, let's leave the .xaml file for the moment and look and the one piece of code, TreatWarningsAsErrorsValueProvider.cs. When the user toggles the value of `TreatWarningsAsErrors` we don't just want to hide or show `WarningsAsErrors` and/or `WarningsNotAsErrors`, we want to clear out the property values that no longer make sense. E.g., if we're setting `TreatWarningsAsErrors` to false, we can delete `WarningsNotAsErrors` entirely as it has no effect.

We accomplish this by implementing an `IInterceptingPropertyValueProvider` for `TreatWarningsAsErrors`. An intercepting value provider gets a chance to handle a property set before it is passed on for default handling; this allows it to manipulate the value or even other properties. It also has a chance to manipulate the values _read_ from the default handler before they are passed on to the property pages, but we don't need this ability here.

However, if the user toggles `TreatWarningsAsErrors` back and forth we don't want to permanently delete whatever values they had for the other two properties. Rather, we squirrel them away in `ITemporaryPropertyStorage` before deleting them, and restore the value from there when switching back.

Fourth, go back to the .xaml file and take a look at the `Rule.DataSource` entry at the top of the page. The `Persistence` attribute indicates that the property values will read from and written to the "ProjectFile" handler which, of course, reads and writes to the project file itself (as opposed to the .user file or some other .props or .targets file). However, as we want to intercept the `TreatWarningsAsErrors` property we update its `DataSource` entry to instead specify the "ProjectFileWithInterception" persistence handler. This handler runs the property through an applicable interceptors before handing it off to the "ProjectFile" handler. Without this, the TreatWarningsAsErrorsValueProvider would never be called.

Fifth and finally, note that the values of `WarningsAsErrors` and `WarningsNotAsErrors` now depend on the value of `TreatWarningsAsErrors`. We need to tell the UI that when `TreatWarningsAsErrors` is updated, it should re-query the values of the other two. This is done via the `DependsOn` metadata added to both of those properties' entries. Without this, the UI may become out of sync with the actual values in the project as the user makes changes.
@tmeschter tmeschter requested a review from a team as a code owner February 18, 2021 00:35
Copy link
Member

@drewnoakes drewnoakes left a comment

Choose a reason for hiding this comment

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

My only hesitation with this change is that a user who wants "All except ..." has to know to select "All" before they'll be given the option to specify exclusions. The inverse is true ("None" except).

Another way of implementing this is to change the enum property to have four values ("None", "Only specific", "All except specific", "All"). That would be a more complicated implementation, involving a hidden synthetic property. It might also be an interesting example to document.

Please add a link to this PR in https://github.com/dotnet/project-system/blob/main/docs/repo/property-pages/property-specification.md. We should add a section for good reference PRs.

@tmeschter
Copy link
Contributor Author

Yeah, once we everything implemented we're going to want to go through all the properties and figure out if we're providing the right user experience around them. For example, we should probably update the display names and descriptions for these and NoWarn to be consistent and make it clear how they all interact.

@tmeschter tmeschter merged commit 7585c15 into dotnet:main Feb 18, 2021
@ghost ghost added this to the 16.10 milestone Feb 18, 2021
tmeschter added a commit to tmeschter/roslyn-project-system that referenced this pull request Feb 18, 2021
Add a link to dotnet#6971 to the property page documentation.
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.

2 participants