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

Including @using for Out-of-Scope Razor Component References #10651

Merged
merged 19 commits into from
Aug 30, 2024

Conversation

marcarro
Copy link
Contributor

@marcarro marcarro commented Jul 19, 2024

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 take actionParams as a parameter were moved to the resolver, where each method returns a collection of appropiate objects.

@marcarro marcarro requested a review from a team as a code owner July 19, 2024 19:57
@marcarro marcarro requested a review from a team as a code owner July 30, 2024 16:09
Copy link
Contributor

@ryzngard ryzngard left a 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:

  1. 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...)
  2. There was work for identifiers that I didn't understand why was necessary.
  3. 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; }
Copy link
Contributor

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)
Copy link
Contributor

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);
Copy link
Contributor

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}");
Copy link
Contributor

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)?

Comment on lines +30 to +31
internal sealed class ExtractToComponentCodeActionResolver
(
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
internal sealed class ExtractToComponentCodeActionResolver
(
internal sealed class ExtractToComponentCodeActionResolver(

@@ -1306,4 +1409,78 @@ static IEnumerable<TagHelperDescriptor> BuildTagHelpers()
}
}
}

private class ExtractToComponentResolverDocumentContextFactory : TestDocumentContextFactory
Copy link
Contributor

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?

Copy link
Contributor

@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.

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? 😛

Comment on lines 284 to 291
if (IsMarkupTagHelperElement(node, extractSpan))
{
var tagHelperInfo = GetTagHelperInfo(node);
if (tagHelperInfo is not null)
{
AddDependenciesFromTagHelperInfo(tagHelperInfo, components, actionParams);
}
}
Copy link
Contributor

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(...)
            }

Comment on lines 319 to 320
if (metadata.Key == TagHelperMetadata.Common.TypeNamespace &&
metadata.Value is not null &&
Copy link
Contributor

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

public static string GetTypeNamespace(this TagHelperDescriptor tagHelper)

{
if (metadata.Key == TagHelperMetadata.Common.TypeNamespace &&
metadata.Value is not null &&
!components.Contains(metadata.Value))
Copy link
Contributor

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">
Copy link
Contributor

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;
Copy link
Contributor

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}");
Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor Author

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.

@phil-allen-msft phil-allen-msft removed the request for review from a team August 26, 2024 18:26
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()
Copy link
Contributor

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

Copy link
Contributor

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

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

Comment on lines 84 to 86
.OfType<CSharpStatementLiteralSyntax>()
.Where(literal => literal.SerializedValue.Contains("using"))
.Select(literal => literal.SerializedValue)
Copy link
Contributor

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 🤷

Suggested change
.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))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ahhh, you're right

@ryzngard ryzngard merged commit be3686f into dotnet:features/extract-to-component Aug 30, 2024
12 checks passed
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.

3 participants