Skip to content

#11793 Simplify tag to self-closing code action #11802

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

Open
wants to merge 7 commits into
base: release/dev17.14
Choose a base branch
from

Conversation

Peter-Juhasz
Copy link

Summary of the changes

  • Introduced a new Code Action Provider and Resolver
  • Added test cases using the cohost method as advised in comment which cover the conditions

@Peter-Juhasz Peter-Juhasz requested review from a team as code owners April 29, 2025 16:45
@Peter-Juhasz
Copy link
Author

@dotnet-policy-service agree

attribute is { TypeName: string typeName, IsEditorRequired: true } &&

// It has type of a `RenderFragment`
(typeName == RenderFragmentTypeName || typeName.StartsWith(GenericRenderFragmentTypeName, StringComparison.Ordinal)) &&
Copy link
Author

Choose a reason for hiding this comment

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

Since there is no symbol information available here, if there is a better way to check whether an Attribute is of type of a RenderFragment, let me know.

Copy link
Author

@Peter-Juhasz Peter-Juhasz Apr 29, 2025

Choose a reason for hiding this comment

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

IsChildContentProperty() and IsParameterizedChildContentProperty() on BoundAttributeDescriptor may be a better choice, although naming is not clear: they both use metadata populated by the compiler layer, calculated by the IsRenderFragment(IPropertySymbol property) method under the hood.

Shall I introduce an IsRenderFragmentProperty() alias on BoundAttributeDescriptor for IsChildContentProperty() to make naming clear at least on this layer?

Copy link
Member

Choose a reason for hiding this comment

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

It's not clear to me why we're explicitly checking for RenderFragment anyway. Why are they important? I would have thought the more important thing to check for was ChildContent.

eg, given a component with a random render fragment property "Foo", if its required and not supplied, we can check for diagnostics, if its not required and not supplied then it doesn't matter, and if its not required and supplied, then it will be non-whitespace characters in the tag, so will already be filtered out.

ChildContent though, we need to check for because something this:

<Goo>

</Goo>

That could be a component with no content, or, if there is a ChildContent property, it could be a component with ChildContent set to some whitespace, which we probably want to keep.

Copy link
Author

Choose a reason for hiding this comment

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

why we're explicitly checking for RenderFragment anyway?

Because RenderFragement typed Parameters (including named ChildContent if any) are the only ones which can be defined in the Component markup element syntax body (too). Not only named ChildContent but any other:

<LoadItemView Load="LoadAsync">
  <FoundView Context="item">...</FoundView>
  <NotFoundView>...</NotFoundView>
  <ErrorView>...</ErrorView>
</LoadItemView>

ChildContent is indeed a special case and distinguished by the language, but I believe having a generalized approach has more value.

That could be a component with no content, or, if there is a ChildContent property, it could be a component with ChildContent set to some whitespace, which we probably want to keep.

Having a Component with an optional RenderFragment? ChildContent Parameter, designed explicitly to accept some form of whitespace, which is then set by the user as a markup element body syntax (which is subject to formatting, so setting it as an attribute would make more sense) is quite an edge case I believe.

While the majority of the cases is that people have Components with optional/empty or no ChildContent Parameters, end up with their tags fully closed with an end tag, offering this refactor would bring value. (It could be a follow up task to improve Auto Insert to close applicable Component tags in a self-closing way.)

Even so, this refactoring is not offered as a Diagnostic which would be reported and then a Code Fix for it, but an optional refactor which can be executed at will, because Razor allows both ways to work (without applying this refactor).

Copy link
Author

Choose a reason for hiding this comment

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

In other words: we can only offer simplification, if all editor required RenderFragment Parameters are already set (either in body, or set as an attribute, or has a binding as an attribute).

If there are still any left (so it is not set as an attribute either), we need to keep open that it may be set in the body (which is usually the case), so we don't offer the simplification yet.

Copy link
Member

Choose a reason for hiding this comment

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

Let's agree to disagree on whether the logic should be here, because you've already written it anyway. I would ask though that this logic be re-written without the nested lambdas and multiple conditions that make it much harder to read (and probably debug). Having a separate method for this logic, that does an old fashioned foreach loop, and has a few guard conditions that just return false, would improve this a lot I think.

@Peter-Juhasz Peter-Juhasz force-pushed the simplifytagtoselfclosing branch from 148eb97 to 65fc16b Compare April 29, 2025 17:30
@Peter-Juhasz
Copy link
Author

I fixed the whitespace issues in the resource file, but there is still an error in the build pipeline, possibly the missing translations.

Shall I check in the result of msbuild /t:UpdateXlf as well? (updated .xlf files)

@ryzngard
Copy link
Contributor

I fixed the whitespace issues in the resource file, but there is still an error in the build pipeline, possibly the missing translations.

Shall I check in the result of msbuild /t:UpdateXlf as well? (updated .xlf files)

yup, that should fix. Or if you build in VS it should generate them as well

@Peter-Juhasz Peter-Juhasz force-pushed the simplifytagtoselfclosing branch from 65fc16b to 4fb2a6b Compare April 29, 2025 18:59
@davidwengier
Copy link
Member

Thanks for the PR, I'll take a look later this afternoon. Please try not to force push any more now the PR is open though, it makes it much harder for us to keep track of changes as feedback is responded to.

For other reviewers: it was my suggestion to target the release branch, as that's the easiest to test with for now. Once everything is signed off, we can change the target to main, and deal with any conflicts and merge.

Copy link
Member

@davidwengier davidwengier left a comment

Choose a reason for hiding this comment

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

This is really good! Thanks for the contribution.

Just had some thoughts about perhaps simplifying the logic and maybe doing a little less work in tooling, but rather leaning on the compiler a bit more. Curious for your thoughts on it.

attribute is { TypeName: string typeName, IsEditorRequired: true } &&

// It has type of a `RenderFragment`
(typeName == RenderFragmentTypeName || typeName.StartsWith(GenericRenderFragmentTypeName, StringComparison.Ordinal)) &&
Copy link
Member

Choose a reason for hiding this comment

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

It's not clear to me why we're explicitly checking for RenderFragment anyway. Why are they important? I would have thought the more important thing to check for was ChildContent.

eg, given a component with a random render fragment property "Foo", if its required and not supplied, we can check for diagnostics, if its not required and not supplied then it doesn't matter, and if its not required and supplied, then it will be non-whitespace characters in the tag, so will already be filtered out.

ChildContent though, we need to check for because something this:

<Goo>

</Goo>

That could be a component with no content, or, if there is a ChildContent property, it could be a component with ChildContent set to some whitespace, which we probably want to keep.

- code style
- clarify naming on params model
- better type check for RenderFragment
- recognize binding get/set syntax
@Peter-Juhasz
Copy link
Author

🌴 FYI: I will be traveling in the rest of this week, and while I can read/reply comments, I won't be close to my desktop workstation to be able to make any meaningful changes. Will be back on Monday next week.

Feel free to make any changes if needed to proceed with this PR until then.

@ryzngard
Copy link
Contributor

🌴 FYI: I will be traveling in the rest of this week, and while I can read/reply comments, I won't be close to my desktop workstation to be able to make any meaningful changes. Will be back on Monday next week.

Feel free to make any changes if needed to proceed with this PR until then.

Will do. Thanks @Peter-Juhasz ! Love to see both of your contributions. Have safe travels

Copy link
Member

@davidwengier davidwengier left a comment

Choose a reason for hiding this comment

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

Had another look and other than a couple of relatively minor (and most readability) tweaks, this is really good.

No rush on the follow up though :)

Comment on lines 135 to 136
(a.Key.StartsWith("@bind-", StringComparison.Ordinal) && a.Key.AsSpan("@bind-".Length).Equals(attribute.Name, StringComparison.Ordinal)) ||
(a.Key.StartsWith("@bind-", StringComparison.Ordinal) && a.Key.EndsWith(":get", StringComparison.Ordinal) && a.Key.AsSpan()["@bind-".Length..^":get".Length].Equals(attribute.Name, StringComparison.Ordinal))
Copy link
Member

Choose a reason for hiding this comment

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

My previous comments about this not being necessary were wrong, I think I missed that you were iterating through BoundAttributes and not the actual attribute syntax nodes.

It might still be nice though to move this attribute name parsing to a helper method so it least it only has to be written once. Perhaps a new method in RazorSyntaxFacts?

So it would be if (!markupElementSyntax.TagHelperInfo!.BindingResult.Attributes.Any(a => attribute.Name == RazorSyntaxFacts.GetAttributeNameForAttribute(a.Key)) or similar?

@DustinCampbell
Copy link
Member

FYI that @dotnet/razor-compiler also needs to take a look, since this PR adds a method in the Compiler layer.

@@ -211,4 +211,7 @@
<data name="Unsupported_razor_project_info_version_encountered" xml:space="preserve">
<value>Unsupported razor project info version encounted.</value>
</data>
<data name="Simplify_Tag_To_SelfClosing_Title" xml:space="preserve">
<value>Simplify tag to self-closing</value>
Copy link
Member

Choose a reason for hiding this comment

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

@phil-allen-msft: Does this need any word-smithing?

@Peter-Juhasz
Copy link
Author

@DustinCampbell I appreciate your feedback, but I think I need some guidance interpreting the severities.

While I totally agree and you are technically correct on each one and don't get me wrong, I'm not against "perfect" code, but suggestions which do not change meaning, like:

  • fix whitespace formatting
  • remove unnecessary parenthesis for parameterless constructor
  • seal an internal class
  • invert condition (is it subjective taste of reviewer or shall we optimize for least amount of operations?)

Are these suggestions or blockers? I'm happy to fix them all, I'm just curious about the severity when I see findings like these.

In any case, are there any analyzers I forgot to enable locally to shorten the feedback loop? Thanks.

@davidwengier
Copy link
Member

davidwengier commented May 5, 2025

Whitespace formatting would be a compile error if it violated IDE0055, and any suggested whitespace changes where there wasn't a violation are pretty rare, or would have introduced a violation. The issue with that one pattern that Dustin and I both commented on is the latter, and a Roslyn bug that I haven't got around to logging yet.

In any case, are there any analyzers I forgot to enable locally to shorten the feedback loop?

If there were any that had to be manually enabled, then that would be a bug on us not setting up our dev environment correctly.

@DustinCampbell
Copy link
Member

@DustinCampbell I appreciate your feedback, but I think I need some guidance interpreting the severities.

While I totally agree and you are technically correct on each one and don't get me wrong, I'm not against "perfect" code, but suggestions which do not change meaning, like:

  • fix whitespace formatting
  • remove unnecessary parenthesis for parameterless constructor
  • seal an internal class
  • invert condition (is it subjective taste of reviewer or shall we optimize for least amount of operations?)

Are these suggestions or blockers? I'm happy to fix them all, I'm just curious about the severity when I see findings like these.

In any case, are there any analyzers I forgot to enable locally to shorten the feedback loop? Thanks.

@Peter-Juhasz: I've left feedback that I would leave for any Razor team member submitting a pull request. My goal is to ensure consistency and readability across the code base,, which is admittedly a work-in-progress. 😄 You are free to ignore my comments that you feel are burdensome, especially if you're not planning to push any other changes. However, if you're planning to push more changes, I can't see a reason why you wouldn't make any of my suggested changes.

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.

4 participants