-
Notifications
You must be signed in to change notification settings - Fork 196
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
Including @using for Out-of-Scope Razor Component References #10651
Including @using for Out-of-Scope Razor Component References #10651
Conversation
From merge
21507c8
to
5bab548
Compare
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.
Overall looks fairly good. It's hard to tell what is fixed in a next review vs what I should comment here. Nothing seems blocking yet. There does seem to be a few things I would say about the design that I'm not sure are tested:
- It looks for all components (tag helpers) and gets their namespace. How do we know the
@using
will need to be added? It could be unnecessary (i.e: in_Imports
, in a parent namespace, in global usings...) - There was work for identifiers that I didn't understand why was necessary.
- There's a few hash sets that may not be necessary, and if they are should likely be pooled since they're all string hash sets
public required List<string> Dependencies { get; set; } | ||
|
||
[JsonPropertyName("usedIdentifiers")] | ||
public required HashSet<string> UsedIdentifiers { get; set; } |
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'll have to look, but do we use HashSet in our serialization objects? It feels weird but if it just serializes/deserializes the same way a list would I guess it would be okay?
} | ||
} | ||
|
||
private static void GetUsedIdentifiers(SyntaxNode divNode, SyntaxNode documentRoot, ExtractToComponentCodeActionParams actionParams) |
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 guessing this is for a future review?
actionParams.ExtractStart, | ||
actionParams.ExtractEnd, | ||
actionParams); | ||
GetUsedIdentifiers(utilityScanRoot, syntaxTree.Root, actionParams); |
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.
It's unclear to me why we need identifiers here for adding usings for components
!components.Contains(metadata.Value)) | ||
{ | ||
components.Add(metadata.Value); | ||
actionParams.Dependencies.Add($"@using {metadata.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.
Dependencies is already a hashset correct? Why do we need another one (components
)?
internal sealed class ExtractToComponentCodeActionResolver | ||
( |
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.
internal sealed class ExtractToComponentCodeActionResolver | |
( | |
internal sealed class ExtractToComponentCodeActionResolver( |
@@ -1306,4 +1409,78 @@ static IEnumerable<TagHelperDescriptor> BuildTagHelpers() | |||
} | |||
} | |||
} | |||
|
|||
private class ExtractToComponentResolverDocumentContextFactory : TestDocumentContextFactory |
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.
This file is getting fairly large. Can we make it either a partial class and create CodeActionEndToEndTest.ExtractToComponent.NetFx
or make a base class for these?
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 don't know if this PR is even worth reviewing or merging, if everything has been removed in a future PR anyway.
Have you considered working on one thing in one branch at a time? 😛
if (IsMarkupTagHelperElement(node, extractSpan)) | ||
{ | ||
var tagHelperInfo = GetTagHelperInfo(node); | ||
if (tagHelperInfo is not null) | ||
{ | ||
AddDependenciesFromTagHelperInfo(tagHelperInfo, components, actionParams); | ||
} | ||
} |
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.
Consider if these two "helper" methods are actually helping. Without them, this whole loop body could be:
if (extractSpan.Contains(node.Span) &&
node is MarkupTagHelperElementSyntax { TagHelperInfo: { } tagHelperInfo })
{
AddDependenciesFromTagHelperInfo(...)
}
if (metadata.Key == TagHelperMetadata.Common.TypeNamespace && | ||
metadata.Value is not 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.
If you search for other uses of TagHelperMetadata.Common.TypeNamespace
you'll find there are helper methods for most, if not all, of these. eg
Line 23 in 3423142
public static string GetTypeNamespace(this TagHelperDescriptor tagHelper) |
{ | ||
if (metadata.Key == TagHelperMetadata.Common.TypeNamespace && | ||
metadata.Value is not null && | ||
!components.Contains(metadata.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.
Since components
is a hashset, there is no need to check Contains before Add. Just call Add.
"""; | ||
|
||
var expectedRazorComponent = """ | ||
<div id="a"> |
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.
Shouldn't this have the using statement for Book
?
@@ -90,7 +91,15 @@ internal sealed class ExtractToNewComponentCodeActionResolver( | |||
} | |||
|
|||
var componentName = Path.GetFileNameWithoutExtension(componentPath); | |||
var newComponentContent = text.GetSubTextString(new TextSpan(actionParams.ExtractStart, actionParams.ExtractEnd - actionParams.ExtractStart)).Trim(); | |||
var newComponentContent = string.Empty; |
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.
This should use a StringBuilder, not just concatenate strings, and use a pooled one at that.
!components.Contains(metadata.Value)) | ||
{ | ||
components.Add(metadata.Value); | ||
actionParams.Dependencies.Add($"@using {metadata.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.
I don't think Dependencies
is the right name for this, though I think perhaps its been removed in a follow up anyway. I would just call it UsingDirectives
though. I'd also consider whether its simpler for the Params type to just use a string, but then again if it's been removed in a future PR this whole comment is mute.
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.
This has been renamed to ComponentDependencies
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.
Honesty, that's just leaning in to being incorrect. They were never dependencies, and they certainly aren't components. Objectively this is a list of using directives. Unless those using directives are in the witness protection program, we should call them what they are.
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.
Will do. Was basing variable names off some of the phrasing in the spec.
ProcessSelection(startElementNode, endElementNode, actionParams); | ||
|
||
var utilityScanRoot = FindNearestCommonAncestor(startElementNode, endElementNode) ?? startElementNode; | ||
|
||
// The new component usings are going to be a subset of the usings in the source razor file | ||
var usingsInFile = context.SourceText.ToString() |
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.
In general in tooling we don't want to read the sourcetext directly to get syntax. It's better to use the syntax tree. In this case it will be a CSharpStatementLiteral
where there's a keyword
of using
in it
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.
the other thing is that the text in the using may not be the full namespace. Since usings are put inside the class
they can be relative. So if you have a component MyApp.MyComponents.Header
and a component MyApp.HomePage
with @using MyComponents
then Header
is valid to use
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.
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.
perfect, I should have checked if there was an extension or helper
.OfType<CSharpStatementLiteralSyntax>() | ||
.Where(literal => literal.SerializedValue.Contains("using")) | ||
.Select(literal => literal.SerializedValue) |
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.
Close but this is still wrong because this will be true for statements where a variable has using
in the name (and worth adding a test for that case). I think this is correct but I'm coding in github so 🤷
.OfType<CSharpStatementLiteralSyntax>() | |
.Where(literal => literal.SerializedValue.Contains("using")) | |
.Select(literal => literal.SerializedValue) | |
.OfType<CSharpStatementLiteralSyntax>() | |
.Where(statement => statement.LiteralTokens.FirstOrDefault() is SyntaxToken { Kind: SyntaxKind.Keyword } token && CSharpTokenizer.GetTokenKeyword(token) == CSharpSyntaxKind.UsingKeyword)) |
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.
Ahhh, you're right
be3686f
into
dotnet:features/extract-to-component
Summary of the changes
Addition to feature wherein razor component dependencies are now identified, and their corresponding
@usings
are now also extracted into the new document.Important!
Notes: Some changes in this PR are further rectified in #10760, such as:
AddComponentDependenciesInRange
does not take actionParams anymore. In general, most (if not all) of the internal methods that takeactionParams
as a parameter were moved to the resolver, where each method returns a collection of appropiate objects.