From decd38681f02fec8b7c1fcb020077e1d38ea1fe8 Mon Sep 17 00:00:00 2001 From: marcarro Date: Mon, 19 Aug 2024 13:01:46 -0700 Subject: [PATCH 01/12] Initial commit: Add handler and Params --- .../Razor/RazorComponentInfoHandler.cs | 157 ++++++++++++++++++ .../Razor/RazorComponentInfoParams.cs | 51 ++++++ 2 files changed, 208 insertions(+) create mode 100644 src/LanguageServer/Protocol/ExternalAccess/Razor/RazorComponentInfoHandler.cs create mode 100644 src/LanguageServer/Protocol/ExternalAccess/Razor/RazorComponentInfoParams.cs diff --git a/src/LanguageServer/Protocol/ExternalAccess/Razor/RazorComponentInfoHandler.cs b/src/LanguageServer/Protocol/ExternalAccess/Razor/RazorComponentInfoHandler.cs new file mode 100644 index 0000000000000..e45f198a39272 --- /dev/null +++ b/src/LanguageServer/Protocol/ExternalAccess/Razor/RazorComponentInfoHandler.cs @@ -0,0 +1,157 @@ +// 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.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ICSharpCode.Decompiler.CSharp.Syntax; +using Microsoft.CodeAnalysis.CodeCleanup; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.LanguageServerIndexFormat; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.RemoveUnnecessaryImports; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.LanguageServer.ExternalAccess.Razor; + +[ExportCSharpVisualBasicStatelessLspService(typeof(RazorComponentInfoHandler)), Shared] +[Method(RazorComponentInfoName)] +internal sealed class RazorComponentInfoHandler : ILspServiceRequestHandler +{ + public const string RazorComponentInfoName = "roslyn/razorComponentInfo"; + private readonly IGlobalOptionService _globalOptions; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public RazorComponentInfoHandler(IGlobalOptionService globalOptions) + { + _globalOptions = globalOptions; + } + + public bool MutatesSolutionState => false; + + public bool RequiresLSPSolution => true; + + public async Task HandleRequestAsync(RazorComponentInfoParams request, RequestContext context, CancellationToken cancellationToken) + { + var project = context.Solution?.GetProject(request.Project); + + if (project is null) + { + return null; + } + + // Create a document in-memory to represent the file Razor wants to add + //var newFilePath = ProtocolConversions.GetDocumentFilePathFromUri(request.NewDocument.Uri); + //var newSource = SourceText.From(request.NewContents); + //var newFileLoader = new SourceTextLoader(newSource, newFilePath); + //var newDocumentId = DocumentId.CreateNewId(project.Id); + //var newSolution = project.Solution.AddDocument( + // DocumentInfo.Create( + // newDocumentId, + // name: newFilePath, + // loader: newFileLoader, + // filePath: newFilePath)); + + //var newDocument = newSolution.GetRequiredDocument(newDocumentId); + + //var formattingService = newDocument.GetLanguageService(); + //if (formattingService is not null) + //{ + // var hintDocument = project.Documents.FirstOrDefault(); + // var cleanupOptions = await newDocument.GetCodeCleanupOptionsAsync(cancellationToken).ConfigureAwait(false); + // newDocument = await formattingService.FormatNewDocumentAsync(newDocument, hintDocument, cleanupOptions, cancellationToken).ConfigureAwait(false); + //} + + 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); + + + var (methods, fields) = GetInfoInsideRazorDocument(root, semanticModel, cancellationToken); + + var result = new RazorComponentInfo + { + Methods = methods, + Fields = fields + }; + + return result; + } + + private static (List, List) GetInfoInsideRazorDocument(CompilationUnitSyntax root, SemanticModel semanticModel, CancellationToken cancellationToken) + { + var invocationExpressions = root.DescendantNodes().OfType().ToList(); + var identifierNames = root.DescendantNodes().OfType().ToList(); + + List methods = []; + List fields = []; + + foreach (var invocation in invocationExpressions) + { + var invocationOperation = semanticModel.GetOperation(invocation, cancellationToken) as IInvocationOperation; + var invocationDataFlow = semanticModel.AnalyzeDataFlow(invocation); + if (invocationOperation is null) + { + continue; + } + + + var targetMethod = invocationOperation.TargetMethod; + if (targetMethod is null) + { + continue; + } + + var operationReturnType = invocationOperation.Type; + if (operationReturnType is null) + { + continue; + } + + var parameterTypes = targetMethod.GetParameters().Select(parameter => parameter.Type.ToDisplayString()).ToList(); + if (parameterTypes is null) + { + continue; + } + + var methodInfo = new MethodInsideRazorElementInfo + { + Name = targetMethod.Name, + ReturnType = operationReturnType.ToNameDisplayString(), + ParameterTypes = parameterTypes + }; + + methods.Add(methodInfo); + } + + foreach (var identifier in identifierNames) + { + var symbolInfo = semanticModel.GetSymbolInfo(identifier, cancellationToken); + if (symbolInfo.Symbol is IFieldSymbol or IPropertySymbol) + { + var field = new SymbolInsideRazorElementInfo + { + Name = symbolInfo.Symbol.Name, + Type = symbolInfo.Symbol.GetType().ToString() + }; + + fields.Add(field); + } + } + + return (methods, fields); + } +} diff --git a/src/LanguageServer/Protocol/ExternalAccess/Razor/RazorComponentInfoParams.cs b/src/LanguageServer/Protocol/ExternalAccess/Razor/RazorComponentInfoParams.cs new file mode 100644 index 0000000000000..83bd654e6b645 --- /dev/null +++ b/src/LanguageServer/Protocol/ExternalAccess/Razor/RazorComponentInfoParams.cs @@ -0,0 +1,51 @@ +// 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.Collections.Generic; +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using Microsoft.CodeAnalysis; +using Roslyn.LanguageServer.Protocol; + +internal sealed record RazorComponentInfoParams + +{ + [JsonPropertyName("document")] + public required TextDocumentIdentifier Document { get; init; } + + [JsonPropertyName("newDocument")] + public required TextDocumentIdentifier NewDocument { get; init; } + + [JsonPropertyName("project")] + public required TextDocumentIdentifier Project { get; init; } + + [JsonPropertyName("hostDocumentVersion")] + public required int HostDocumentVersion { get; init; } + + [JsonPropertyName("newContents")] + public required string NewContents { get; init; } +} + +// Not sure where to put these two records +internal sealed record RazorComponentInfo +{ + public required List Methods { get; set; } + public required List Fields { get; set; } +} + + +internal sealed record MethodInsideRazorElementInfo +{ + public required string Name { get; set; } + + public required string ReturnType { get; set; } + + public required List ParameterTypes { get; set; } +} + +internal sealed record SymbolInsideRazorElementInfo +{ + public required string Name { get; set; } + public required string Type { get; set; } +} From 1f6ac0f4d851010086456575560bfed3962a7ac2 Mon Sep 17 00:00:00 2001 From: marcarro Date: Mon, 19 Aug 2024 16:04:48 -0700 Subject: [PATCH 02/12] Nits --- .../Razor/RazorComponentInfoHandler.cs | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/src/LanguageServer/Protocol/ExternalAccess/Razor/RazorComponentInfoHandler.cs b/src/LanguageServer/Protocol/ExternalAccess/Razor/RazorComponentInfoHandler.cs index e45f198a39272..b11778682454c 100644 --- a/src/LanguageServer/Protocol/ExternalAccess/Razor/RazorComponentInfoHandler.cs +++ b/src/LanguageServer/Protocol/ExternalAccess/Razor/RazorComponentInfoHandler.cs @@ -52,34 +52,11 @@ public RazorComponentInfoHandler(IGlobalOptionService globalOptions) return null; } - // Create a document in-memory to represent the file Razor wants to add - //var newFilePath = ProtocolConversions.GetDocumentFilePathFromUri(request.NewDocument.Uri); - //var newSource = SourceText.From(request.NewContents); - //var newFileLoader = new SourceTextLoader(newSource, newFilePath); - //var newDocumentId = DocumentId.CreateNewId(project.Id); - //var newSolution = project.Solution.AddDocument( - // DocumentInfo.Create( - // newDocumentId, - // name: newFilePath, - // loader: newFileLoader, - // filePath: newFilePath)); - - //var newDocument = newSolution.GetRequiredDocument(newDocumentId); - - //var formattingService = newDocument.GetLanguageService(); - //if (formattingService is not null) - //{ - // var hintDocument = project.Documents.FirstOrDefault(); - // var cleanupOptions = await newDocument.GetCodeCleanupOptionsAsync(cancellationToken).ConfigureAwait(false); - // newDocument = await formattingService.FormatNewDocumentAsync(newDocument, hintDocument, cleanupOptions, cancellationToken).ConfigureAwait(false); - //} - 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); - var (methods, fields) = GetInfoInsideRazorDocument(root, semanticModel, cancellationToken); var result = new RazorComponentInfo @@ -108,7 +85,6 @@ private static (List, List Date: Sun, 1 Sep 2024 20:30:41 -0700 Subject: [PATCH 03/12] Renamed for better accuracy, changed functionality to only get symbolic info in specific range --- .../Razor/GetSymbolicInfoHandler.cs | 209 ++++++++++++++++++ ...InfoParams.cs => GetSymbolicInfoParams.cs} | 28 +-- .../Razor/RazorComponentInfoHandler.cs | 133 ----------- 3 files changed, 220 insertions(+), 150 deletions(-) create mode 100644 src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs rename src/LanguageServer/Protocol/ExternalAccess/Razor/{RazorComponentInfoParams.cs => GetSymbolicInfoParams.cs} (51%) delete mode 100644 src/LanguageServer/Protocol/ExternalAccess/Razor/RazorComponentInfoHandler.cs diff --git a/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs b/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs new file mode 100644 index 0000000000000..ae0fc5ba63b1d --- /dev/null +++ b/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs @@ -0,0 +1,209 @@ +// 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 ICSharpCode.Decompiler.CSharp.Syntax; +using ICSharpCode.Decompiler.CSharp.Transforms; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeCleanup; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.EditAndContinue; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.LanguageServerIndexFormat; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.RemoveUnnecessaryImports; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.ExternalAccess.Razor; + +[ExportCSharpVisualBasicStatelessLspService(typeof(GetSymbolicInfoHandler)), Shared] +[Method(GetSymbolicInfoMethodName)] +internal sealed class GetSymbolicInfoHandler : ILspServiceDocumentRequestHandler +{ + 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 HandleRequestAsync(GetSymbolicInfoParams request, RequestContext context, CancellationToken cancellationToken) + { + var solution = context.Solution; + if (solution is null) + { + return null; + } + + var document = solution.GetDocument(request.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)); + + var classDeclarationNode = root.DescendantNodes().OfType().FirstOrDefault(); + if (classDeclarationNode is null) + { + return null; + } + + var blockNode = classDeclarationNode.DescendantNodes().OfType().FirstOrDefault(); + if (blockNode is null) + { + return null; + } + + var dataFlowAnalysis = semanticModel.AnalyzeDataFlow(blockNode); + var writtenInsideBlock = dataFlowAnalysis.WrittenInside.Select(symbol => symbol.Name); + + var identifiersInClass = classDeclarationNode.DescendantNodes().OfType(); + var methodsInClass = classDeclarationNode.DescendantNodes().OfType(); + var fieldsInClass = classDeclarationNode.DescendantNodes().OfType(); + var propertiesInClass = classDeclarationNode.DescendantNodes().OfType(); + + var identifiersInRange = identifiersInClass.Where(i => generatedSpans.Any(s => s.Contains(i.Span))) + .Select(i => semanticModel.GetSymbolInfo(i).Symbol?.Name) + .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))); + var propertiesInRange = propertiesInClass.Where(p => identifiersInRange.Contains(p.Identifier.Text)); + + var pooledMethods = PooledHashSet.GetInstance(); + var pooledAttributes = PooledHashSet.GetInstance(); + + foreach (var method in methodsInRange) + { + + var parameterTypes = method.ParameterList.Parameters.Count > 0 + ? method.ParameterList.Parameters + .Where(p => p.Type != null) + .Select(p => p.Type!.GetFirstToken().Text) + .ToArray() + : Array.Empty(); + + pooledMethods.Add(new MethodSymbolicInfo + { + Name = method.Identifier.Text, + ReturnType = method.ReturnType.GetFirstToken().Text, + ParameterTypes = parameterTypes + }); + } + + var expressionsInClass = classDeclarationNode.DescendantNodes().OfType(); + var expressionIdentifiers = expressionsInClass.SelectMany(e => e.DescendantNodes().OfType()); + var expressionIdentifierNames = expressionIdentifiers.Select(i => semanticModel.GetSymbolInfo(i).Symbol?.Name) + .Where(n => n != null).Select(n => n!); + + foreach (var field in fieldsInRange) + { + foreach (var declaredVariable in field.Declaration.Variables) + { + ExtractAttributeInfo(declaredVariable, field.Declaration.Type, semanticModel, pooledAttributes, writtenInsideBlock, expressionIdentifierNames, cancellationToken); + } + } + + foreach (var property in propertiesInRange) + { + ExtractAttributeInfo(property, property.Type, semanticModel, pooledAttributes, writtenInsideBlock, expressionIdentifierNames, cancellationToken); + } + + var result = new MemberSymbolicInfo + { + Methods = pooledMethods.ToArray(), + Attributes = pooledAttributes.ToArray() + }; + + pooledMethods.Free(); + pooledAttributes.Free(); + + return result; + } + + private static void ExtractAttributeInfo( + SyntaxNode node, + TypeSyntax typeSyntax, + SemanticModel semanticModel, + PooledHashSet attributes, + IEnumerable writtenInsideBlock, + IEnumerable identifierSymbolNames, + CancellationToken cancellationToken) + { + 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)); + } + + var declarationInfo = semanticModel.GetDeclaredSymbol(node, cancellationToken); + var typeInfo = semanticModel.GetTypeInfo(typeSyntax, cancellationToken); + + if (declarationInfo is null || typeInfo.Type is null) + { + return; + } + + var isWrittenTo = writtenInsideBlock.Any(symbol => symbol == declarationInfo.Name); + + // 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 (typeInfo.Type.ToDisplayString() == "string" || typeInfo.Type.IsValueType) + { + isWrittenTo = identifierSymbolNames.Contains(declarationInfo.Name); + } + + attributes.Add(new AttributeSymbolicInfo + { + Name = declarationInfo.Name, + Type = typeInfo.Type.ToDisplayString(), + IsValueType = typeInfo.Type.IsValueType, + IsWrittenTo = isWrittenTo + }); + } +} diff --git a/src/LanguageServer/Protocol/ExternalAccess/Razor/RazorComponentInfoParams.cs b/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoParams.cs similarity index 51% rename from src/LanguageServer/Protocol/ExternalAccess/Razor/RazorComponentInfoParams.cs rename to src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoParams.cs index 83bd654e6b645..d3075bdabe81a 100644 --- a/src/LanguageServer/Protocol/ExternalAccess/Razor/RazorComponentInfoParams.cs +++ b/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoParams.cs @@ -2,50 +2,44 @@ // 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.Collections.Generic; -using System.Runtime.Serialization; using System.Text.Json.Serialization; -using Microsoft.CodeAnalysis; using Roslyn.LanguageServer.Protocol; -internal sealed record RazorComponentInfoParams +internal sealed record GetSymbolicInfoParams { [JsonPropertyName("document")] public required TextDocumentIdentifier Document { get; init; } - [JsonPropertyName("newDocument")] - public required TextDocumentIdentifier NewDocument { get; init; } - [JsonPropertyName("project")] public required TextDocumentIdentifier Project { get; init; } [JsonPropertyName("hostDocumentVersion")] public required int HostDocumentVersion { get; init; } - [JsonPropertyName("newContents")] - public required string NewContents { get; init; } + [JsonPropertyName("generatedDocumentRanges")] + public required Range[] GeneratedDocumentRanges { get; init; } } -// Not sure where to put these two records -internal sealed record RazorComponentInfo +internal sealed record MemberSymbolicInfo { - public required List Methods { get; set; } - public required List Fields { get; set; } + public required MethodSymbolicInfo[] Methods { get; set; } + public required AttributeSymbolicInfo[] Attributes { get; set; } } - -internal sealed record MethodInsideRazorElementInfo +internal sealed record MethodSymbolicInfo { public required string Name { get; set; } public required string ReturnType { get; set; } - public required List ParameterTypes { get; set; } + public required string[] ParameterTypes { get; set; } } -internal sealed record SymbolInsideRazorElementInfo +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; } } diff --git a/src/LanguageServer/Protocol/ExternalAccess/Razor/RazorComponentInfoHandler.cs b/src/LanguageServer/Protocol/ExternalAccess/Razor/RazorComponentInfoHandler.cs deleted file mode 100644 index b11778682454c..0000000000000 --- a/src/LanguageServer/Protocol/ExternalAccess/Razor/RazorComponentInfoHandler.cs +++ /dev/null @@ -1,133 +0,0 @@ -// 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.Composition; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ICSharpCode.Decompiler.CSharp.Syntax; -using Microsoft.CodeAnalysis.CodeCleanup; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Formatting; -using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.LanguageServer; -using Microsoft.CodeAnalysis.LanguageServer.Handler; -using Microsoft.CodeAnalysis.LanguageServerIndexFormat; -using Microsoft.CodeAnalysis.Operations; -using Microsoft.CodeAnalysis.Options; -using Microsoft.CodeAnalysis.RemoveUnnecessaryImports; -using Microsoft.CodeAnalysis.Shared.Extensions; -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.CodeAnalysis.LanguageServer.ExternalAccess.Razor; - -[ExportCSharpVisualBasicStatelessLspService(typeof(RazorComponentInfoHandler)), Shared] -[Method(RazorComponentInfoName)] -internal sealed class RazorComponentInfoHandler : ILspServiceRequestHandler -{ - public const string RazorComponentInfoName = "roslyn/razorComponentInfo"; - private readonly IGlobalOptionService _globalOptions; - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public RazorComponentInfoHandler(IGlobalOptionService globalOptions) - { - _globalOptions = globalOptions; - } - - public bool MutatesSolutionState => false; - - public bool RequiresLSPSolution => true; - - public async Task HandleRequestAsync(RazorComponentInfoParams request, RequestContext context, CancellationToken cancellationToken) - { - var project = context.Solution?.GetProject(request.Project); - - if (project is null) - { - return null; - } - - 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); - - var (methods, fields) = GetInfoInsideRazorDocument(root, semanticModel, cancellationToken); - - var result = new RazorComponentInfo - { - Methods = methods, - Fields = fields - }; - - return result; - } - - private static (List, List) GetInfoInsideRazorDocument(CompilationUnitSyntax root, SemanticModel semanticModel, CancellationToken cancellationToken) - { - var invocationExpressions = root.DescendantNodes().OfType().ToList(); - var identifierNames = root.DescendantNodes().OfType().ToList(); - - List methods = []; - List fields = []; - - foreach (var invocation in invocationExpressions) - { - var invocationOperation = semanticModel.GetOperation(invocation, cancellationToken) as IInvocationOperation; - var invocationDataFlow = semanticModel.AnalyzeDataFlow(invocation); - if (invocationOperation is null) - { - continue; - } - - var targetMethod = invocationOperation.TargetMethod; - if (targetMethod is null) - { - continue; - } - - var operationReturnType = invocationOperation.Type; - if (operationReturnType is null) - { - continue; - } - - var parameterTypes = targetMethod.GetParameters().Select(parameter => parameter.Type.ToDisplayString()).ToList(); - if (parameterTypes is null) - { - continue; - } - - var methodInfo = new MethodInsideRazorElementInfo - { - Name = targetMethod.Name, - ReturnType = operationReturnType.ToNameDisplayString(), - ParameterTypes = parameterTypes - }; - - methods.Add(methodInfo); - } - - foreach (var identifier in identifierNames) - { - var symbolInfo = semanticModel.GetSymbolInfo(identifier, cancellationToken); - if (symbolInfo.Symbol is IFieldSymbol or IPropertySymbol) - { - var field = new SymbolInsideRazorElementInfo - { - Name = symbolInfo.Symbol.Name, - Type = symbolInfo.Symbol.GetType().ToString() - }; - - fields.Add(field); - } - } - - return (methods, fields); - } -} From af22f088babeda7675d1ee9e6eae8e5763504864 Mon Sep 17 00:00:00 2001 From: marcarro Date: Tue, 3 Sep 2024 12:44:06 -0700 Subject: [PATCH 04/12] PR Feedback --- .../Razor/GetSymbolicInfoHandler.cs | 181 +++++++++++++++--- 1 file changed, 150 insertions(+), 31 deletions(-) diff --git a/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs b/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs index ae0fc5ba63b1d..9c5a04f3f43c3 100644 --- a/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs +++ b/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs @@ -60,7 +60,7 @@ public GetSymbolicInfoHandler(IGlobalOptionService globalOptions) return null; } - var document = solution.GetDocument(request.Document); + var document = context.Document; if (document is null) { return null; @@ -73,12 +73,20 @@ public GetSymbolicInfoHandler(IGlobalOptionService globalOptions) var generatedSpans = request.GeneratedDocumentRanges.Select(r => ProtocolConversions.RangeToTextSpan(r, sourceText)); - var classDeclarationNode = root.DescendantNodes().OfType().FirstOrDefault(); + // 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().FirstOrDefault(classSymbol => InheritsFromComponentBase(componentBaseSymbol, classSymbol, 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().FirstOrDefault(); if (blockNode is null) { @@ -87,57 +95,90 @@ public GetSymbolicInfoHandler(IGlobalOptionService globalOptions) var dataFlowAnalysis = semanticModel.AnalyzeDataFlow(blockNode); var writtenInsideBlock = dataFlowAnalysis.WrittenInside.Select(symbol => symbol.Name); + 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 identifiers, 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 identifiersBuilder = ArrayBuilder.GetInstance(); + var methodsBuilder = ArrayBuilder.GetInstance(); + var fieldsBuilder = ArrayBuilder.GetInstance(); + var propertiesBuilder = ArrayBuilder.GetInstance(); + foreach (var node in classDeclarationNode.DescendantNodes()) + { + switch (node.Kind()) + { + case SyntaxKind.IdentifierName: + identifiersBuilder.Add((IdentifierNameSyntax)node); + break; + case SyntaxKind.MethodDeclaration: + methodsBuilder.Add((MethodDeclarationSyntax)node); + break; + case SyntaxKind.FieldDeclaration: + fieldsBuilder.Add((FieldDeclarationSyntax)node); + break; + case SyntaxKind.PropertyDeclaration: + propertiesBuilder.Add((PropertyDeclarationSyntax)node); + break; + } + } + + var identifiersInClass = identifiersBuilder.ToImmutableAndFree(); + var methodsInClass = methodsBuilder.ToImmutableAndFree(); + var fieldsInClass = fieldsBuilder.ToImmutableAndFree(); + var propertiesInClass = propertiesBuilder.ToImmutableAndFree(); + + 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 != null); - var identifiersInClass = classDeclarationNode.DescendantNodes().OfType(); - var methodsInClass = classDeclarationNode.DescendantNodes().OfType(); - var fieldsInClass = classDeclarationNode.DescendantNodes().OfType(); - var propertiesInClass = classDeclarationNode.DescendantNodes().OfType(); + var methodsInRange = methodsInClass.Where(method => identifiersInRange + .Any(identifier => SymbolEqualityComparer.Default.Equals(identifier.Symbol, semanticModel.GetDeclaredSymbol(method)))); - var identifiersInRange = identifiersInClass.Where(i => generatedSpans.Any(s => s.Contains(i.Span))) - .Select(i => semanticModel.GetSymbolInfo(i).Symbol?.Name) - .Where(n => n != null).Select(n => n!); + var fieldsInRange = fieldsInClass.Where(field => field.Declaration.Variables + .Any(variable => identifiersInRange + .Any(identifier => SymbolEqualityComparer.Default.Equals(identifier.Symbol, semanticModel.GetDeclaredSymbol(variable))))); - 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))); - var propertiesInRange = propertiesInClass.Where(p => identifiersInRange.Contains(p.Identifier.Text)); + var propertiesInRange = propertiesInClass.Where(property => identifiersInRange + .Any(identifier => SymbolEqualityComparer.Default.Equals(identifier.Symbol, semanticModel.GetDeclaredSymbol(property)))); var pooledMethods = PooledHashSet.GetInstance(); var pooledAttributes = PooledHashSet.GetInstance(); foreach (var method in methodsInRange) { - var parameterTypes = method.ParameterList.Parameters.Count > 0 ? method.ParameterList.Parameters .Where(p => p.Type != null) - .Select(p => p.Type!.GetFirstToken().Text) + .Select(p => GetFullTypeName(p.Type!, semanticModel)) .ToArray() : Array.Empty(); pooledMethods.Add(new MethodSymbolicInfo { Name = method.Identifier.Text, - ReturnType = method.ReturnType.GetFirstToken().Text, + ReturnType = GetFullTypeName(method.ReturnType!, semanticModel), ParameterTypes = parameterTypes }); } - var expressionsInClass = classDeclarationNode.DescendantNodes().OfType(); - var expressionIdentifiers = expressionsInClass.SelectMany(e => e.DescendantNodes().OfType()); - var expressionIdentifierNames = expressionIdentifiers.Select(i => semanticModel.GetSymbolInfo(i).Symbol?.Name) - .Where(n => n != null).Select(n => n!); - + var expressionIdentifiersInRange = identifiersInRange.Where(i => i.Identifier.Ancestors().OfType().Any()); foreach (var field in fieldsInRange) { foreach (var declaredVariable in field.Declaration.Variables) { - ExtractAttributeInfo(declaredVariable, field.Declaration.Type, semanticModel, pooledAttributes, writtenInsideBlock, expressionIdentifierNames, cancellationToken); + ExtractAttributeInfo(declaredVariable, field.Declaration.Type, semanticModel, pooledAttributes, writtenInsideSymbols, expressionIdentifiersInRange, cancellationToken); } } foreach (var property in propertiesInRange) { - ExtractAttributeInfo(property, property.Type, semanticModel, pooledAttributes, writtenInsideBlock, expressionIdentifierNames, cancellationToken); + ExtractAttributeInfo(property, property.Type, semanticModel, pooledAttributes, writtenInsideSymbols, expressionIdentifiersInRange, cancellationToken); } var result = new MemberSymbolicInfo @@ -152,13 +193,50 @@ public GetSymbolicInfoHandler(IGlobalOptionService globalOptions) return result; } + private static bool InheritsFromComponentBase(ITypeSymbol componentBaseSymbol, ClassDeclarationSyntax classDeclaration, SemanticModel semanticModel) + { + if (componentBaseSymbol is null) + { + return false; + } + + var baseTypes = classDeclaration.BaseList?.Types; + if (baseTypes is null) + { + return false; + } + + foreach (var baseTypeSyntax in baseTypes) + { + var classTypeSymbol = semanticModel.GetTypeInfo(baseTypeSyntax.Type).Type; + if (classTypeSymbol is not null && InheritsFrom(classTypeSymbol, componentBaseSymbol)) + { + return true; + } + } + + return false; + } + + 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 attributes, - IEnumerable writtenInsideBlock, - IEnumerable identifierSymbolNames, + ImmutableArray writtenInsideBlockSymbols, + IEnumerable identifiersInExpressions, CancellationToken cancellationToken) { if (node is null) @@ -177,14 +255,14 @@ private static void ExtractAttributeInfo( } var declarationInfo = semanticModel.GetDeclaredSymbol(node, cancellationToken); - var typeInfo = semanticModel.GetTypeInfo(typeSyntax, cancellationToken); + var typeSymbol = semanticModel.GetTypeInfo(typeSyntax, cancellationToken).Type; - if (declarationInfo is null || typeInfo.Type is null) + if (declarationInfo is null || typeSymbol is null) { return; } - var isWrittenTo = writtenInsideBlock.Any(symbol => symbol == declarationInfo.Name); + 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. @@ -193,17 +271,58 @@ private static void ExtractAttributeInfo( // 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) + if (typeSymbol.SpecialType == SpecialType.System_String || typeSymbol.IsValueType) { - isWrittenTo = identifierSymbolNames.Contains(declarationInfo.Name); + isWrittenTo = identifiersInExpressions.Any(symbol => SymbolEqualityComparer.Default.Equals(symbol.Symbol, declarationInfo)); } attributes.Add(new AttributeSymbolicInfo { Name = declarationInfo.Name, - Type = typeInfo.Type.ToDisplayString(), - IsValueType = typeInfo.Type.IsValueType, + 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) + { + // Check if the symbol is a named type symbol (e.g., List) + if (typeSymbol is INamedTypeSymbol namedTypeSymbol) + { + // Get the base name of the type, e.g., "List" + var typeName = namedTypeSymbol.Name; + + // If there are type arguments, format them recursively + 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. + } + + // If no type arguments, just return the type name + return typeName; + } + + // Fallback for non-named types + return typeSymbol.ToDisplayString(); + } + + internal sealed record IdentifierAndSymbol + { + public required IdentifierNameSyntax Identifier { get; init; } + public ISymbol? Symbol { get; init; } + } } From b9e5bc17f2c7775776e04e39ffbeeb33d2e3292a Mon Sep 17 00:00:00 2001 From: marcarro Date: Tue, 3 Sep 2024 12:46:13 -0700 Subject: [PATCH 05/12] Some more nits --- .../Razor/GetSymbolicInfoHandler.cs | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs b/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs index 9c5a04f3f43c3..9a9432de657c5 100644 --- a/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs +++ b/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs @@ -147,9 +147,9 @@ public GetSymbolicInfoHandler(IGlobalOptionService globalOptions) var propertiesInRange = propertiesInClass.Where(property => identifiersInRange .Any(identifier => SymbolEqualityComparer.Default.Equals(identifier.Symbol, semanticModel.GetDeclaredSymbol(property)))); + // Now, we iterate through the methods, fields, and properties in the range and extract the necessary information. var pooledMethods = PooledHashSet.GetInstance(); var pooledAttributes = PooledHashSet.GetInstance(); - foreach (var method in methodsInRange) { var parameterTypes = method.ParameterList.Parameters.Count > 0 @@ -239,21 +239,6 @@ private static void ExtractAttributeInfo( IEnumerable identifiersInExpressions, CancellationToken cancellationToken) { - 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)); - } - var declarationInfo = semanticModel.GetDeclaredSymbol(node, cancellationToken); var typeSymbol = semanticModel.GetTypeInfo(typeSyntax, cancellationToken).Type; From 95b15be80b13e414818c350c8fc3e8e040a85b1f Mon Sep 17 00:00:00 2001 From: marcarro Date: Tue, 3 Sep 2024 12:57:19 -0700 Subject: [PATCH 06/12] Deleted unused variable writtenInsideBlock --- .../Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs b/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs index 9a9432de657c5..ac2eaf5c2b535 100644 --- a/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs +++ b/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs @@ -94,7 +94,6 @@ public GetSymbolicInfoHandler(IGlobalOptionService globalOptions) } var dataFlowAnalysis = semanticModel.AnalyzeDataFlow(blockNode); - var writtenInsideBlock = dataFlowAnalysis.WrittenInside.Select(symbol => symbol.Name); var writtenInsideSymbols = dataFlowAnalysis.WrittenInside; // Using the generated spans as a criterion to traverse through the tree generally returns incomplete results. From 7924c03d9b602876f2f260b39400f40967a24622 Mon Sep 17 00:00:00 2001 From: marcarro Date: Tue, 3 Sep 2024 12:59:40 -0700 Subject: [PATCH 07/12] Changed "!= null" to "is not null" --- .../Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs b/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs index ac2eaf5c2b535..88466f84d1ceb 100644 --- a/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs +++ b/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs @@ -134,7 +134,7 @@ public GetSymbolicInfoHandler(IGlobalOptionService globalOptions) Identifier = identifier, Symbol = semanticModel.GetSymbolInfo(identifier).Symbol }) - .Where(x => x.Symbol != null); + .Where(x => x.Symbol is not null); var methodsInRange = methodsInClass.Where(method => identifiersInRange .Any(identifier => SymbolEqualityComparer.Default.Equals(identifier.Symbol, semanticModel.GetDeclaredSymbol(method)))); @@ -153,7 +153,7 @@ public GetSymbolicInfoHandler(IGlobalOptionService globalOptions) { var parameterTypes = method.ParameterList.Parameters.Count > 0 ? method.ParameterList.Parameters - .Where(p => p.Type != null) + .Where(p => p.Type is not null) .Select(p => GetFullTypeName(p.Type!, semanticModel)) .ToArray() : Array.Empty(); From 59153746a41b822773bbaeadffa4e50334932d64 Mon Sep 17 00:00:00 2001 From: marcarro Date: Thu, 5 Sep 2024 15:54:54 -0700 Subject: [PATCH 08/12] PR Feedback --- .../Razor/GetSymbolicInfoHandler.cs | 100 ++++++++++++------ 1 file changed, 65 insertions(+), 35 deletions(-) diff --git a/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs b/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs index 88466f84d1ceb..02e098623fd8d 100644 --- a/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs +++ b/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs @@ -80,7 +80,7 @@ public GetSymbolicInfoHandler(IGlobalOptionService globalOptions) return null; } - var classDeclarationNode = root.DescendantNodes().OfType().FirstOrDefault(classSymbol => InheritsFromComponentBase(componentBaseSymbol, classSymbol, semanticModel)); + var classDeclarationNode = root.DescendantNodes().OfType().FirstOrDefault(classNode => InheritsFromComponentBase(componentBaseSymbol, classNode, semanticModel)); if (classDeclarationNode is null) { return null; @@ -97,54 +97,91 @@ public GetSymbolicInfoHandler(IGlobalOptionService globalOptions) 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 identifiers, methods, fields, and properties in the class. + // 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 identifiersBuilder = ArrayBuilder.GetInstance(); var methodsBuilder = ArrayBuilder.GetInstance(); var fieldsBuilder = ArrayBuilder.GetInstance(); var propertiesBuilder = ArrayBuilder.GetInstance(); + var identifiersInRangeBuilder = ArrayBuilder.GetInstance(); + var expressionIdentifiersBuilder = ArrayBuilder.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(); + foreach (var node in classDeclarationNode.DescendantNodes()) { - switch (node.Kind()) + 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 SyntaxKind.IdentifierName: - identifiersBuilder.Add((IdentifierNameSyntax)node); + case IdentifierNameSyntax identifierName: + if (generatedSpans.Any(span => span.Contains(identifierName.Span))) + { + var identifierAndSymbol = new IdentifierAndSymbol + { + Identifier = identifierName, + Symbol = semanticModel.GetSymbolInfo(identifierName, cancellationToken).Symbol + }; + + identifiersInRangeBuilder.Add(identifierAndSymbol); + + if (expressionStack.Count > 0) + { + expressionIdentifiersBuilder.Add(identifierAndSymbol); + } + } break; - case SyntaxKind.MethodDeclaration: - methodsBuilder.Add((MethodDeclarationSyntax)node); + + case MethodDeclarationSyntax methodDeclaration: + methodsBuilder.Add(methodDeclaration); break; - case SyntaxKind.FieldDeclaration: - fieldsBuilder.Add((FieldDeclarationSyntax)node); + + case FieldDeclarationSyntax fieldDeclaration: + fieldsBuilder.Add(fieldDeclaration); break; - case SyntaxKind.PropertyDeclaration: - propertiesBuilder.Add((PropertyDeclarationSyntax)node); + + case PropertyDeclarationSyntax propertyDeclaration: + propertiesBuilder.Add(propertyDeclaration); break; } } - var identifiersInClass = identifiersBuilder.ToImmutableAndFree(); + var identifiersInRange = identifiersInRangeBuilder.ToImmutableAndFree(); var methodsInClass = methodsBuilder.ToImmutableAndFree(); var fieldsInClass = fieldsBuilder.ToImmutableAndFree(); var propertiesInClass = propertiesBuilder.ToImmutableAndFree(); - 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); + 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, semanticModel.GetDeclaredSymbol(method)))); + .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, semanticModel.GetDeclaredSymbol(variable))))); + .Any(identifier => SymbolEqualityComparer.Default.Equals(identifier.Symbol, declaredFieldSymbols[variable])))); var propertiesInRange = propertiesInClass.Where(property => identifiersInRange - .Any(identifier => SymbolEqualityComparer.Default.Equals(identifier.Symbol, semanticModel.GetDeclaredSymbol(property)))); + .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.GetInstance(); @@ -283,21 +320,14 @@ private static string GetFullTypeName(TypeSyntax type, SemanticModel semanticMod private static string FormatType(ITypeSymbol typeSymbol) { - // Check if the symbol is a named type symbol (e.g., List) if (typeSymbol is INamedTypeSymbol namedTypeSymbol) { - // Get the base name of the type, e.g., "List" - var typeName = namedTypeSymbol.Name; - - // If there are type arguments, format them recursively - 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. - } + var format = new SymbolDisplayFormat( + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.UseSpecialTypes); - // If no type arguments, just return the type name - return typeName; + return namedTypeSymbol.ToDisplayString(format); } // Fallback for non-named types From 511a1e8b0159573c694aa3dcaaaaf99621f031d6 Mon Sep 17 00:00:00 2001 From: marcarro Date: Thu, 5 Sep 2024 16:02:30 -0700 Subject: [PATCH 09/12] More PR Feedback --- .../Razor/GetSymbolicInfoHandler.cs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs b/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs index 02e098623fd8d..a368b017fca46 100644 --- a/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs +++ b/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs @@ -236,22 +236,13 @@ private static bool InheritsFromComponentBase(ITypeSymbol componentBaseSymbol, C return false; } - var baseTypes = classDeclaration.BaseList?.Types; - if (baseTypes is null) + var classTypeSymbol = semanticModel.GetDeclaredSymbol(classDeclaration) as ITypeSymbol; + if (classTypeSymbol is null) { return false; } - foreach (var baseTypeSyntax in baseTypes) - { - var classTypeSymbol = semanticModel.GetTypeInfo(baseTypeSyntax.Type).Type; - if (classTypeSymbol is not null && InheritsFrom(classTypeSymbol, componentBaseSymbol)) - { - return true; - } - } - - return false; + return InheritsFrom(classTypeSymbol, componentBaseSymbol); } private static bool InheritsFrom(ITypeSymbol derivedType, ITypeSymbol baseType) From 3a2e2fd676b2c5e9de5a19e21f8b5cf5ca8e3b16 Mon Sep 17 00:00:00 2001 From: marcarro Date: Thu, 5 Sep 2024 22:16:19 -0700 Subject: [PATCH 10/12] Use named tuple in lieu of IdentifierAndSymbol --- .../Razor/GetSymbolicInfoHandler.cs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs b/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs index a368b017fca46..31a4bd08c506d 100644 --- a/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs +++ b/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; +using System.Data.Common; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -103,8 +104,8 @@ public GetSymbolicInfoHandler(IGlobalOptionService globalOptions) var methodsBuilder = ArrayBuilder.GetInstance(); var fieldsBuilder = ArrayBuilder.GetInstance(); var propertiesBuilder = ArrayBuilder.GetInstance(); - var identifiersInRangeBuilder = ArrayBuilder.GetInstance(); - var expressionIdentifiersBuilder = ArrayBuilder.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. @@ -135,11 +136,7 @@ public GetSymbolicInfoHandler(IGlobalOptionService globalOptions) case IdentifierNameSyntax identifierName: if (generatedSpans.Any(span => span.Contains(identifierName.Span))) { - var identifierAndSymbol = new IdentifierAndSymbol - { - Identifier = identifierName, - Symbol = semanticModel.GetSymbolInfo(identifierName, cancellationToken).Symbol - }; + var identifierAndSymbol = (identifierName, semanticModel.GetSymbolInfo(identifierName, cancellationToken).Symbol); identifiersInRangeBuilder.Add(identifierAndSymbol); @@ -263,7 +260,7 @@ private static void ExtractAttributeInfo( SemanticModel semanticModel, PooledHashSet attributes, ImmutableArray writtenInsideBlockSymbols, - IEnumerable identifiersInExpressions, + IEnumerable<(IdentifierNameSyntax Identifier, ISymbol? Symbol)> identifiersInExpressions, CancellationToken cancellationToken) { var declarationInfo = semanticModel.GetDeclaredSymbol(node, cancellationToken); @@ -324,10 +321,4 @@ private static string FormatType(ITypeSymbol typeSymbol) // Fallback for non-named types return typeSymbol.ToDisplayString(); } - - internal sealed record IdentifierAndSymbol - { - public required IdentifierNameSyntax Identifier { get; init; } - public ISymbol? Symbol { get; init; } - } } From de22b367031e79c75c4c1979b704490fe41cfd22 Mon Sep 17 00:00:00 2001 From: marcarro Date: Thu, 5 Sep 2024 22:45:46 -0700 Subject: [PATCH 11/12] Reuse declared symbol dictionaries in ExtractAttributeInfo --- .../Razor/GetSymbolicInfoHandler.cs | 48 ++++++++++++++++--- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs b/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs index 31a4bd08c506d..20ed19a2af128 100644 --- a/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs +++ b/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs @@ -161,10 +161,11 @@ public GetSymbolicInfoHandler(IGlobalOptionService globalOptions) } } - var identifiersInRange = identifiersInRangeBuilder.ToImmutableAndFree(); 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)); @@ -200,18 +201,35 @@ public GetSymbolicInfoHandler(IGlobalOptionService globalOptions) }); } - var expressionIdentifiersInRange = identifiersInRange.Where(i => i.Identifier.Ancestors().OfType().Any()); foreach (var field in fieldsInRange) { foreach (var declaredVariable in field.Declaration.Variables) { - ExtractAttributeInfo(declaredVariable, field.Declaration.Type, semanticModel, pooledAttributes, writtenInsideSymbols, expressionIdentifiersInRange, cancellationToken); + 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, cancellationToken); + ExtractAttributeInfo( + property, + property.Type, + semanticModel, + pooledAttributes, + writtenInsideSymbols, + expressionIdentifiersInRange, + declaredPropertySymbols, + declaredFieldSymbols, + cancellationToken); } var result = new MemberSymbolicInfo @@ -261,12 +279,28 @@ private static void ExtractAttributeInfo( PooledHashSet attributes, ImmutableArray writtenInsideBlockSymbols, IEnumerable<(IdentifierNameSyntax Identifier, ISymbol? Symbol)> identifiersInExpressions, + IReadOnlyDictionary declaredPropertySymbols, + IReadOnlyDictionary declaredFieldSymbols, CancellationToken cancellationToken) { - var declarationInfo = semanticModel.GetDeclaredSymbol(node, cancellationToken); - var typeSymbol = semanticModel.GetTypeInfo(typeSyntax, cancellationToken).Type; + ISymbol? declarationInfo = null; - if (declarationInfo is null || typeSymbol is 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; } From 1af49a589729d2864952e68b792eecab181bed53 Mon Sep 17 00:00:00 2001 From: marcarro Date: Fri, 6 Sep 2024 17:07:48 -0700 Subject: [PATCH 12/12] Remove unnecessary usings --- .../ExternalAccess/Razor/GetSymbolicInfoHandler.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs b/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs index 20ed19a2af128..4c5b99bf62bd1 100644 --- a/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs +++ b/src/LanguageServer/Protocol/ExternalAccess/Razor/GetSymbolicInfoHandler.cs @@ -6,29 +6,17 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; -using System.Data.Common; using System.Linq; using System.Threading; using System.Threading.Tasks; -using ICSharpCode.Decompiler.CSharp.Syntax; -using ICSharpCode.Decompiler.CSharp.Transforms; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CodeCleanup; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.EditAndContinue; -using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.LanguageServer; using Microsoft.CodeAnalysis.LanguageServer.Handler; -using Microsoft.CodeAnalysis.LanguageServerIndexFormat; -using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis.RemoveUnnecessaryImports; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -using Microsoft.CommonLanguageServerProtocol.Framework; using Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.ExternalAccess.Razor;