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

Added handler for Razor Endpoint #74815

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open

Added handler for Razor Endpoint #74815

wants to merge 13 commits into from

Conversation

marcarro
Copy link

@marcarro marcarro commented Aug 19, 2024

Summary of changes

Created handler corresponding to a new LSP request endpoint in razor, to be used in Extract To Component project.

This handler analyzes methods and fields present in the generated C# file of a Razor document and returns information about them (return type, parameter types, symbol names).

See dotnet/razor#10760 file ExtractToComponentCodeActionResolver.cs for details.

@marcarro marcarro requested a review from a team as a code owner August 19, 2024 23:02
@dotnet-issue-labeler dotnet-issue-labeler bot added Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead labels Aug 19, 2024
@dotnet-policy-service dotnet-policy-service bot added Community The pull request was submitted by a contributor who is not a Microsoft employee. VSCode labels Aug 19, 2024
@ryzngard ryzngard requested a review from a team August 19, 2024 23:04
public required string NewContents { get; init; }
}

// Not sure where to put these two records
Copy link
Author

Choose a reason for hiding this comment

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

three *

Copy link
Contributor

Choose a reason for hiding this comment

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

I think here is fine

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.

Some initial feedback. It's hard to follow how this information will be used. Do you have the razor side ready? It would be helpful to see that side to better comment on this code.

Comment on lines 48 to 55
var project = context.Solution?.GetProject(request.Project);

if (project is null)
{
return null;
}

var document = context.Solution.GetDocument(request.Document);
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're getting a document you can get the project it is in directly

Suggested change
var project = context.Solution?.GetProject(request.Project);
if (project is null)
{
return null;
}
var document = context.Solution.GetDocument(request.Document);
var document = context.Solution.GetDocument(request.Document);
var project = document.Project;

Copy link
Author

Choose a reason for hiding this comment

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

Added some more information in the initial post!

var document = context.Solution.GetDocument(request.Document);
var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var syntaxTree = semanticModel.SyntaxTree;
var root = (CompilationUnitSyntax)syntaxTree.GetRoot(cancellationToken);
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 the cast is needed here

Comment on lines 73 to 74
var invocationExpressions = root.DescendantNodes().OfType<InvocationExpressionSyntax>().ToList();
var identifierNames = root.DescendantNodes().OfType<IdentifierNameSyntax>().ToList();
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks like you're getting all the invocations and identifiers in the whole file, not just ones used by razor.

var invocationExpressions = root.DescendantNodes().OfType<InvocationExpressionSyntax>().ToList();
var identifierNames = root.DescendantNodes().OfType<IdentifierNameSyntax>().ToList();

List<MethodInsideRazorElementInfo> methods = [];
Copy link
Contributor

Choose a reason for hiding this comment

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

Likely these should be pooled types

foreach (var invocation in invocationExpressions)
{
var invocationOperation = semanticModel.GetOperation(invocation, cancellationToken) as IInvocationOperation;
var invocationDataFlow = semanticModel.AnalyzeDataFlow(invocation);
Copy link
Contributor

Choose a reason for hiding this comment

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

This appears to be unused

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'm not sure what the aim of this endpoint is, so some of my comments might be irrelevant. Feel free to ignore :)


public async Task<RazorComponentInfo?> HandleRequestAsync(RazorComponentInfoParams request, RequestContext context, CancellationToken cancellationToken)
{
var project = context.Solution?.GetProject(request.Project);
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 know what is in request.Project, but generally speaking the way things work in LSP would be to send the Uri of the document in the request, and then access everything via the context parameter. So var document = context.GetRequiredDocument();, and from there if you need the project, it would just be document.Project.

Seems like this project isn't needed though.

Comment on lines 73 to 74
var invocationExpressions = root.DescendantNodes().OfType<InvocationExpressionSyntax>().ToList();
var identifierNames = root.DescendantNodes().OfType<IdentifierNameSyntax>().ToList();
Copy link
Contributor

Choose a reason for hiding this comment

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

Rather that descending through the whole tree twice, is it better to do it once and look for both of the node types you care about?

foreach (var invocation in invocationExpressions)
{
var invocationOperation = semanticModel.GetOperation(invocation, cancellationToken) as IInvocationOperation;
var invocationDataFlow = semanticModel.AnalyzeDataFlow(invocation);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you include a comment on what this is analyzing? Is it the parameters to the method, or the method itself?

var field = new SymbolInsideRazorElementInfo
{
Name = symbolInfo.Symbol.Name,
Type = symbolInfo.Symbol.GetType().ToString()
Copy link
Contributor

Choose a reason for hiding this comment

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

Pretty sure this is going to end up being Microsoft.CodeAnalysis.SourceFieldSymbol or something. Unlikely to be what you intended.

Type = symbolInfo.Symbol.GetType().ToString()
};

fields.Add(field);
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like there should be some filtering of duplicates here, if a field is referred to more than once

public required int HostDocumentVersion { get; init; }

[JsonPropertyName("newContents")]
public required string NewContents { get; init; }
Copy link
Contributor

Choose a reason for hiding this comment

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

What goes in here?

public required TextDocumentIdentifier Document { get; init; }

[JsonPropertyName("newDocument")]
public required TextDocumentIdentifier NewDocument { get; init; }
Copy link
Contributor

Choose a reason for hiding this comment

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

What goes in here?


public required string ReturnType { get; set; }

public required List<string> ParameterTypes { 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.

Since these types are all for serialization, the List here, and the two in RazorComponentInfo should probably just be arrays.

}

// Not sure where to put these two records
internal sealed record RazorComponentInfo
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't seem to have anything to do with Razor components. Doesn't this contain C# info, not Razor info?

public required string NewContents { get; init; }
}

// Not sure where to put these two records
Copy link
Contributor

Choose a reason for hiding this comment

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

I think here is fine

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 think there are two areas to focus on here: Firstly, generally speaking this is pretty clean code, and I can see what it's doing. What is not clear is why it is doing it, and that is where code comments would be helpful.

Secondly, C# is very complicated, and it's usually a worrying sign to take shortcuts and make assumptions. eg, assuming that the first token of something will be important is unlikely to hold in all cases. Assuming that a matching on strings, names, ToDisplayString() etc. will work is similarly unlikely to hold in all cases. Roslyn is very powerful, and when things get reduced to comparing strings, it usually means that power is being thrown away.

Comment on lines 63 to 67
var document = solution.GetDocument(request.Document);
if (document is null)
{
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

There is no need to get the document from the solution, its already in the context.Document property.

return null;
}

var blockNode = classDeclarationNode.DescendantNodes().OfType<BlockSyntax>().FirstOrDefault();
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the first block expected to be? Perhaps a comment, and or some tests, or something? Would it be more appropriate to find the first block that overlaps the generatedSpans?

.Where(n => n != null).Select(n => n!);

var methodsInRange = methodsInClass.Where(m => identifiersInRange.Contains(m.Identifier.Text));
var fieldsInRange = fieldsInClass.Where(f => f.Declaration.Variables.Any(v => identifiersInRange.Contains(v.Identifier.Text)));
Copy link
Contributor

Choose a reason for hiding this comment

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

How do you know the identifier being used, that has the same name as the field, actually represents the field?

eg. this snippet has 4 identifier names that match a field name, but only two refer to the field: https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEBDAzhgHwAEAmARgFgAoIgZgAJT6Bhegb2vq/s+4AcoASwBu2DDEZkADPQB22ALYwA3Ly5rGDIinoBZABSswaegBF6AEwCU7Dd0kz5S+gF56AIneqq9+3e5EZACc+k4wVt6+AcH6GAAWgrgAdGER/lyBIWApiuGRUZIhFjlKaT7cAL7UVVTUdIwkZtQc5Rla0nK53hVAA===

Same question applies to properties in the line below.

Comment on lines 91 to 94
var identifiersInClass = classDeclarationNode.DescendantNodes().OfType<IdentifierNameSyntax>();
var methodsInClass = classDeclarationNode.DescendantNodes().OfType<MethodDeclarationSyntax>();
var fieldsInClass = classDeclarationNode.DescendantNodes().OfType<FieldDeclarationSyntax>();
var propertiesInClass = classDeclarationNode.DescendantNodes().OfType<PropertyDeclarationSyntax>();
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be much better to loop through the tree once, and grab the information as it comes, rather than multiple times like this.

var parameterTypes = method.ParameterList.Parameters.Count > 0
? method.ParameterList.Parameters
.Where(p => p.Type != null)
.Select(p => p.Type!.GetFirstToken().Text)
Copy link
Contributor

Choose a reason for hiding this comment

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

pooledMethods.Add(new MethodSymbolicInfo
{
Name = method.Identifier.Text,
ReturnType = method.ReturnType.GetFirstToken().Text,
Copy link
Contributor

Choose a reason for hiding this comment

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

As above re: the first token.

Comment on lines 126 to 128
var expressionIdentifiers = expressionsInClass.SelectMany(e => e.DescendantNodes().OfType<IdentifierNameSyntax>());
var expressionIdentifierNames = expressionIdentifiers.Select(i => semanticModel.GetSymbolInfo(i).Symbol?.Name)
.Where(n => n != null).Select(n => n!);
Copy link
Contributor

Choose a reason for hiding this comment

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

These lists will be a subset of the identifiers found above, right? Why does it need a separate list?

Comment on lines 164 to 177
if (node is null)
{
throw new ArgumentNullException(nameof(node));
}

if (typeSyntax is null)
{
throw new ArgumentNullException(nameof(typeSyntax));
}

if (semanticModel is null)
{
throw new ArgumentNullException(nameof(semanticModel));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This is annotated code, so these are all unnecessary, right?


// The 'isWrittenTo' property is not critical to functionality in current usage; it's only used in ExtractToComponent
// to determine if a code attribute that has been promoted to a parameter in a component should include a comment warning.
if (typeInfo.Type.ToDisplayString() == "string" || typeInfo.Type.IsValueType)
Copy link
Contributor

Choose a reason for hiding this comment

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

if (typeSymbol is INamedTypeSymbol namedTypeSymbol)
{
// Get the base name of the type, e.g., "List"
var typeName = namedTypeSymbol.Name;
Copy link
Author

Choose a reason for hiding this comment

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

The case with integer bothers me a bit: "int" will become "Int32" (etc.) after accessing the type symbol name. This is technically correct and causes no errors, but I wonder if there's a way to just keep the type name as the user defined it.

return null;
}

var classDeclarationNode = root.DescendantNodes().OfType<ClassDeclarationSyntax>().FirstOrDefault(classSymbol => InheritsFromComponentBase(componentBaseSymbol, classSymbol, semanticModel));
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: The parameter is not a classSymbol it is a classNode. "Symbol" has a specific meaning in Roslyn, in that it comes from the semantic model not the syntax tree.

Comment on lines 131 to 137
var identifiersInRange = identifiersInClass.Where(identifier => generatedSpans.Any(span => span.Contains(identifier.Span)))
.Select(identifier => new IdentifierAndSymbol
{
Identifier = identifier,
Symbol = semanticModel.GetSymbolInfo(identifier).Symbol
})
.Where(x => x.Symbol 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.

Consider moving this into the above loop, and simply building identifiersInRange inside that loop, rather than collecting all identifiers and filtering later.


var fieldsInRange = fieldsInClass.Where(field => field.Declaration.Variables
.Any(variable => identifiersInRange
.Any(identifier => SymbolEqualityComparer.Default.Equals(identifier.Symbol, semanticModel.GetDeclaredSymbol(variable)))));
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment applies to the methodsInRange and propertyInRange calculation too, but this happens to be the worst offender: Consider how many times GetDeclaredSymbol is called in this calculation, for each specific value of variable.

});
}

var expressionIdentifiersInRange = identifiersInRange.Where(i => i.Identifier.Ancestors().OfType<ExpressionStatementSyntax>().Any());
Copy link
Contributor

Choose a reason for hiding this comment

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

Comment what this is finding? Why isn't this needed for methods? etc.

return false;
}

foreach (var baseTypeSyntax in baseTypes)
Copy link
Contributor

Choose a reason for hiding this comment

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

This loop seems unnecessary. I think it should be possible to get the symbol info for the class declaration itself (GetDeclaredSymbol I think) and then InheritsFrom could be called directly with that type symbol.

if (namedTypeSymbol.TypeArguments.Length > 0)
{
var typeArguments = string.Join(", ", namedTypeSymbol.TypeArguments.Select(FormatType));
return $"{typeName}<{typeArguments}>"; // Returning a formatted string seems hacky so might need to be revisited.
Copy link
Contributor

Choose a reason for hiding this comment

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

Best not to do this manually, but rather use a call to ToDisplayString() that specifies an appropriate set of SymbolDisplayFormat values to create the desired result.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-IDE Community The pull request was submitted by a contributor who is not a Microsoft employee. untriaged Issues and PRs which have not yet been triaged by a lead VSCode
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants