-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Added handler for Razor Endpoint #74815
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
marcarro
wants to merge
13
commits into
dotnet:main
Choose a base branch
from
marcarro:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
decd386
Initial commit: Add handler and Params
marcarro dec4924
Merge branch 'main' of https://github.com/marcarro/roslyn into addRaz…
marcarro 1f6ac0f
Nits
marcarro 5e1cb93
Renamed for better accuracy, changed functionality to only get symbol…
marcarro af22f08
PR Feedback
marcarro b9e5bc1
Some more nits
marcarro 95b15be
Deleted unused variable writtenInsideBlock
marcarro 7924c03
Changed "!= null" to "is not null"
marcarro 5915374
PR Feedback
marcarro 511a1e8
More PR Feedback
marcarro 3a2e2fd
Use named tuple in lieu of IdentifierAndSymbol
marcarro de22b36
Reuse declared symbol dictionaries in ExtractAttributeInfo
marcarro 1af49a5
Remove unnecessary usings
marcarro File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
346 changes: 346 additions & 0 deletions
346
src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,346 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Composition; | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Microsoft.CodeAnalysis.Host.Mef; | ||
using Microsoft.CodeAnalysis.LanguageServer.Handler; | ||
using Microsoft.CodeAnalysis.Options; | ||
using Microsoft.CodeAnalysis.PooledObjects; | ||
using Microsoft.CodeAnalysis.Shared.Extensions; | ||
using Microsoft.CodeAnalysis.Text; | ||
using Roslyn.LanguageServer.Protocol; | ||
|
||
namespace Microsoft.CodeAnalysis.LanguageServer.ExternalAccess.Razor; | ||
|
||
[ExportCSharpVisualBasicStatelessLspService(typeof(GetSymbolicInfoHandler)), Shared] | ||
[Method(GetSymbolicInfoMethodName)] | ||
internal sealed class GetSymbolicInfoHandler : ILspServiceDocumentRequestHandler<GetSymbolicInfoParams, MemberSymbolicInfo?> | ||
{ | ||
public const string GetSymbolicInfoMethodName = "roslyn/getSymbolicInfo"; | ||
private readonly IGlobalOptionService _globalOptions; | ||
|
||
[ImportingConstructor] | ||
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] | ||
public GetSymbolicInfoHandler(IGlobalOptionService globalOptions) | ||
{ | ||
_globalOptions = globalOptions; | ||
} | ||
|
||
public bool MutatesSolutionState => false; | ||
|
||
public bool RequiresLSPSolution => true; | ||
|
||
public TextDocumentIdentifier GetTextDocumentIdentifier(GetSymbolicInfoParams request) => request.Document; | ||
|
||
public async Task<MemberSymbolicInfo?> HandleRequestAsync(GetSymbolicInfoParams request, RequestContext context, CancellationToken cancellationToken) | ||
{ | ||
var solution = context.Solution; | ||
if (solution is null) | ||
{ | ||
return null; | ||
} | ||
|
||
var document = context.Document; | ||
if (document is null) | ||
{ | ||
return null; | ||
} | ||
|
||
var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); | ||
var syntaxTree = semanticModel.SyntaxTree; | ||
var root = syntaxTree.GetRoot(cancellationToken); | ||
var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); | ||
|
||
var generatedSpans = request.GeneratedDocumentRanges.Select(r => ProtocolConversions.RangeToTextSpan(r, sourceText)); | ||
|
||
// First, get the class declaration for the component (implements Microsoft.AspNetCore.Components.ComponentBase). There might be a better way to get the type. | ||
var componentBaseSymbol = semanticModel.Compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Components.ComponentBase"); | ||
if (componentBaseSymbol is null) | ||
{ | ||
return null; | ||
} | ||
|
||
var classDeclarationNode = root.DescendantNodes().OfType<ClassDeclarationSyntax>().FirstOrDefault(classNode => InheritsFromComponentBase(componentBaseSymbol, classNode, semanticModel)); | ||
if (classDeclarationNode is null) | ||
{ | ||
return null; | ||
} | ||
|
||
// Get the block syntax directly inside method BuildRenderTree(RenderTreeBuilder builder). This will be the most ancestral (first) block syntax. | ||
var blockNode = classDeclarationNode.DescendantNodes().OfType<BlockSyntax>().FirstOrDefault(); | ||
if (blockNode is null) | ||
{ | ||
return null; | ||
} | ||
|
||
var dataFlowAnalysis = semanticModel.AnalyzeDataFlow(blockNode); | ||
var writtenInsideSymbols = dataFlowAnalysis.WrittenInside; | ||
|
||
// Using the generated spans as a criterion to traverse through the tree generally returns incomplete results. | ||
// Instead, we get all of the methods, fields, and properties in the class. | ||
// Then we get the identifiers that are within the generated spans. | ||
// We then find the methods, fields, and properties that correspond to the identifiers within the generated spans. | ||
var methodsBuilder = ArrayBuilder<MethodDeclarationSyntax>.GetInstance(); | ||
var fieldsBuilder = ArrayBuilder<FieldDeclarationSyntax>.GetInstance(); | ||
var propertiesBuilder = ArrayBuilder<PropertyDeclarationSyntax>.GetInstance(); | ||
var identifiersInRangeBuilder = ArrayBuilder<(IdentifierNameSyntax Identifier, ISymbol? Symbol)>.GetInstance(); | ||
var expressionIdentifiersBuilder = ArrayBuilder<(IdentifierNameSyntax Identifier, ISymbol? Symbol)>.GetInstance(); | ||
|
||
// In ExtractAttributeInfo, we need to know if an identifier is inside an expression statement | ||
// to decide whether to mark an attribute as written to. More details in the method. | ||
|
||
// Use a stack to keep track of whether we are inside an expression (this accounts for nested expressions, hence the stack). | ||
var expressionStack = new Stack<ExpressionSyntax>(); | ||
|
||
foreach (var node in classDeclarationNode.DescendantNodes()) | ||
{ | ||
if (node is ExpressionSyntax expression) | ||
{ | ||
expressionStack.Push(expression); | ||
} | ||
|
||
while (expressionStack.Count > 0 && !expressionStack.Peek().Span.Contains(node.Span)) | ||
{ | ||
// We've potentially exited one or more ExpressionSyntax nodes. | ||
// We keep popping ExpressionStatementSyntax nodes off the stack until: | ||
// 1. We find an ExpressionStatementSyntax that still contains our current node, or | ||
// 2. We've emptied the stack entirely. | ||
// This ensures we correctly handle cases where we exit multiple nested | ||
// expression statements at once. | ||
expressionStack.Pop(); | ||
} | ||
|
||
switch (node) | ||
{ | ||
case IdentifierNameSyntax identifierName: | ||
if (generatedSpans.Any(span => span.Contains(identifierName.Span))) | ||
{ | ||
var identifierAndSymbol = (identifierName, semanticModel.GetSymbolInfo(identifierName, cancellationToken).Symbol); | ||
|
||
identifiersInRangeBuilder.Add(identifierAndSymbol); | ||
|
||
if (expressionStack.Count > 0) | ||
{ | ||
expressionIdentifiersBuilder.Add(identifierAndSymbol); | ||
} | ||
} | ||
break; | ||
|
||
case MethodDeclarationSyntax methodDeclaration: | ||
methodsBuilder.Add(methodDeclaration); | ||
break; | ||
|
||
case FieldDeclarationSyntax fieldDeclaration: | ||
fieldsBuilder.Add(fieldDeclaration); | ||
break; | ||
|
||
case PropertyDeclarationSyntax propertyDeclaration: | ||
propertiesBuilder.Add(propertyDeclaration); | ||
break; | ||
} | ||
} | ||
|
||
var methodsInClass = methodsBuilder.ToImmutableAndFree(); | ||
var fieldsInClass = fieldsBuilder.ToImmutableAndFree(); | ||
var propertiesInClass = propertiesBuilder.ToImmutableAndFree(); | ||
var identifiersInRange = identifiersInRangeBuilder.ToImmutableAndFree(); | ||
var expressionIdentifiersInRange = expressionIdentifiersBuilder.ToImmutableAndFree(); | ||
|
||
var declaredMethodSymbols = methodsInClass.ToDictionary(method => method, method => semanticModel.GetDeclaredSymbol(method)); | ||
var declaredPropertySymbols = propertiesInClass.ToDictionary(property => property, property => semanticModel.GetDeclaredSymbol(property)); | ||
var declaredFieldSymbols = fieldsInClass.SelectMany(field => field.Declaration.Variables).ToDictionary(variable => variable, variable => semanticModel.GetDeclaredSymbol(variable)); | ||
|
||
var methodsInRange = methodsInClass.Where(method => identifiersInRange | ||
.Any(identifier => SymbolEqualityComparer.Default.Equals(identifier.Symbol, declaredMethodSymbols[method]))); | ||
|
||
var fieldsInRange = fieldsInClass.Where(field => field.Declaration.Variables | ||
.Any(variable => identifiersInRange | ||
.Any(identifier => SymbolEqualityComparer.Default.Equals(identifier.Symbol, declaredFieldSymbols[variable])))); | ||
|
||
var propertiesInRange = propertiesInClass.Where(property => identifiersInRange | ||
.Any(identifier => SymbolEqualityComparer.Default.Equals(identifier.Symbol, declaredPropertySymbols[property]))); | ||
|
||
// Now, we iterate through the methods, fields, and properties in the range and extract the necessary information. | ||
var pooledMethods = PooledHashSet<MethodSymbolicInfo>.GetInstance(); | ||
var pooledAttributes = PooledHashSet<AttributeSymbolicInfo>.GetInstance(); | ||
foreach (var method in methodsInRange) | ||
{ | ||
var parameterTypes = method.ParameterList.Parameters.Count > 0 | ||
? method.ParameterList.Parameters | ||
.Where(p => p.Type is not null) | ||
.Select(p => GetFullTypeName(p.Type!, semanticModel)) | ||
.ToArray() | ||
: Array.Empty<string>(); | ||
|
||
pooledMethods.Add(new MethodSymbolicInfo | ||
{ | ||
Name = method.Identifier.Text, | ||
ReturnType = GetFullTypeName(method.ReturnType!, semanticModel), | ||
ParameterTypes = parameterTypes | ||
}); | ||
} | ||
|
||
foreach (var field in fieldsInRange) | ||
{ | ||
foreach (var declaredVariable in field.Declaration.Variables) | ||
{ | ||
ExtractAttributeInfo( | ||
declaredVariable, | ||
field.Declaration.Type, | ||
semanticModel, | ||
pooledAttributes, | ||
writtenInsideSymbols, | ||
expressionIdentifiersInRange, | ||
declaredPropertySymbols, | ||
declaredFieldSymbols, | ||
cancellationToken); | ||
} | ||
} | ||
|
||
foreach (var property in propertiesInRange) | ||
{ | ||
ExtractAttributeInfo( | ||
property, | ||
property.Type, | ||
semanticModel, | ||
pooledAttributes, | ||
writtenInsideSymbols, | ||
expressionIdentifiersInRange, | ||
declaredPropertySymbols, | ||
declaredFieldSymbols, | ||
cancellationToken); | ||
} | ||
|
||
var result = new MemberSymbolicInfo | ||
{ | ||
Methods = pooledMethods.ToArray(), | ||
Attributes = pooledAttributes.ToArray() | ||
}; | ||
|
||
pooledMethods.Free(); | ||
pooledAttributes.Free(); | ||
|
||
return result; | ||
} | ||
|
||
private static bool InheritsFromComponentBase(ITypeSymbol componentBaseSymbol, ClassDeclarationSyntax classDeclaration, SemanticModel semanticModel) | ||
{ | ||
if (componentBaseSymbol is null) | ||
{ | ||
return false; | ||
} | ||
|
||
var classTypeSymbol = semanticModel.GetDeclaredSymbol(classDeclaration) as ITypeSymbol; | ||
if (classTypeSymbol is null) | ||
{ | ||
return false; | ||
} | ||
|
||
return InheritsFrom(classTypeSymbol, componentBaseSymbol); | ||
} | ||
|
||
private static bool InheritsFrom(ITypeSymbol derivedType, ITypeSymbol baseType) | ||
{ | ||
var currentType = derivedType; | ||
while (currentType is not null) | ||
{ | ||
if (SymbolEqualityComparer.Default.Equals(currentType, baseType)) | ||
return true; | ||
currentType = currentType.BaseType; | ||
} | ||
return false; | ||
} | ||
|
||
private static void ExtractAttributeInfo( | ||
SyntaxNode node, | ||
TypeSyntax typeSyntax, | ||
SemanticModel semanticModel, | ||
PooledHashSet<AttributeSymbolicInfo> attributes, | ||
ImmutableArray<ISymbol> writtenInsideBlockSymbols, | ||
IEnumerable<(IdentifierNameSyntax Identifier, ISymbol? Symbol)> identifiersInExpressions, | ||
IReadOnlyDictionary<PropertyDeclarationSyntax, ISymbol?> declaredPropertySymbols, | ||
IReadOnlyDictionary<VariableDeclaratorSyntax, ISymbol?> declaredFieldSymbols, | ||
CancellationToken cancellationToken) | ||
{ | ||
ISymbol? declarationInfo = null; | ||
|
||
if (node is PropertyDeclarationSyntax propertyDeclaration) | ||
{ | ||
declaredPropertySymbols.TryGetValue(propertyDeclaration, out declarationInfo); | ||
} | ||
else if (node is VariableDeclaratorSyntax variableDeclarator) | ||
{ | ||
declaredFieldSymbols.TryGetValue(variableDeclarator, out declarationInfo); | ||
} | ||
|
||
if (declarationInfo is null) | ||
{ | ||
return; | ||
} | ||
|
||
var typeSymbol = semanticModel.GetTypeInfo(typeSyntax, cancellationToken).Type; | ||
if (typeSymbol is null) | ||
{ | ||
return; | ||
} | ||
|
||
var isWrittenTo = writtenInsideBlockSymbols.Any(symbol => SymbolEqualityComparer.Default.Equals(symbol, declarationInfo)); | ||
|
||
// Handle special case: attribute is string type or value type. | ||
// Attributes of these types are not added to the 'WrittenInside' property of a data flow analysis when written to or mutated. | ||
|
||
// Erring on the side of caution, assume they are written to if they are involved in some type of expression. | ||
|
||
// 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 (typeSymbol.SpecialType == SpecialType.System_String || typeSymbol.IsValueType) | ||
{ | ||
isWrittenTo = identifiersInExpressions.Any(symbol => SymbolEqualityComparer.Default.Equals(symbol.Symbol, declarationInfo)); | ||
} | ||
|
||
attributes.Add(new AttributeSymbolicInfo | ||
{ | ||
Name = declarationInfo.Name, | ||
Type = GetFullTypeName(typeSyntax, semanticModel), | ||
IsValueType = typeSymbol.IsValueType, | ||
IsWrittenTo = isWrittenTo | ||
}); | ||
} | ||
|
||
private static string GetFullTypeName(TypeSyntax type, SemanticModel semanticModel) | ||
{ | ||
var symbol = semanticModel.GetSymbolInfo(type).Symbol as ITypeSymbol; | ||
if (symbol is not null) | ||
{ | ||
return FormatType(symbol); | ||
} | ||
|
||
// Fallback to string if we can't get the symbol. Ideally this should never happen. | ||
return type.ToString(); | ||
} | ||
|
||
private static string FormatType(ITypeSymbol typeSymbol) | ||
{ | ||
if (typeSymbol is INamedTypeSymbol namedTypeSymbol) | ||
{ | ||
var format = new SymbolDisplayFormat( | ||
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes, | ||
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, | ||
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.UseSpecialTypes); | ||
|
||
return namedTypeSymbol.ToDisplayString(format); | ||
} | ||
|
||
// Fallback for non-named types | ||
return typeSymbol.ToDisplayString(); | ||
} | ||
} |
45 changes: 45 additions & 0 deletions
45
src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoParams.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System.Text.Json.Serialization; | ||
using Roslyn.LanguageServer.Protocol; | ||
|
||
internal sealed record GetSymbolicInfoParams | ||
|
||
{ | ||
[JsonPropertyName("document")] | ||
public required TextDocumentIdentifier Document { get; init; } | ||
|
||
[JsonPropertyName("project")] | ||
public required TextDocumentIdentifier Project { get; init; } | ||
|
||
[JsonPropertyName("hostDocumentVersion")] | ||
public required int HostDocumentVersion { get; init; } | ||
|
||
[JsonPropertyName("generatedDocumentRanges")] | ||
public required Range[] GeneratedDocumentRanges { get; init; } | ||
} | ||
|
||
internal sealed record MemberSymbolicInfo | ||
{ | ||
public required MethodSymbolicInfo[] Methods { get; set; } | ||
public required AttributeSymbolicInfo[] Attributes { get; set; } | ||
} | ||
|
||
internal sealed record MethodSymbolicInfo | ||
{ | ||
public required string Name { get; set; } | ||
|
||
public required string ReturnType { get; set; } | ||
|
||
public required string[] ParameterTypes { get; set; } | ||
} | ||
|
||
internal sealed record AttributeSymbolicInfo | ||
{ | ||
public required string Name { get; set; } | ||
public required string Type { get; set; } | ||
public required bool IsValueType { get; set; } | ||
public required bool IsWrittenTo { get; set; } | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
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?