Skip to content

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
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
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();
Copy link
Member

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?

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();
}
}
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; }
}
Loading