Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new parser/lexer to the StackTraceAnalyzer #57598

Merged
merged 21 commits into from
Dec 1, 2021
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c909bd2
Add new parser/lexer to the StackTraceAnalyzer
ryzngard Oct 12, 2021
e3aa08c
Add throw in loop for ResolveSymbolAsync
ryzngard Nov 5, 2021
0836018
Use GetSymbolAsync
ryzngard Nov 5, 2021
6ccfd3e
Merge branch 'main' into features/stackframe_parser_ui
ryzngard Nov 5, 2021
68dff0d
Change the extension methods to ToString() methods.
ryzngard Nov 5, 2021
1c715cd
Use type display name to check against the stackframe parameter type
ryzngard Nov 6, 2021
42bca8e
Support array parameters and add tests
ryzngard Nov 6, 2021
4728084
remove local functions. They're getting complex enough...
ryzngard Nov 6, 2021
a5f89c1
Handle special types with tests. Better handle arrays with element ty…
ryzngard Nov 6, 2021
08dc08e
Remove original text, just allow frames to use ToString for providing…
ryzngard Nov 8, 2021
2f3597c
Minor PR feedback and cleanup
ryzngard Nov 16, 2021
649db70
Move to OOP and passing definition around.
ryzngard Nov 17, 2021
c9f447f
Switch to ToString and ToFullString methods
ryzngard Nov 17, 2021
3e96c8b
ArrayExpression => ArrayRankSpecifiers
ryzngard Nov 17, 2021
df34218
Add metadata test. Fix metadata symbol finding
ryzngard Nov 18, 2021
5899745
Update src/Features/Core/Portable/StackTraceExplorer/StackTraceAnalyz…
ryzngard Nov 19, 2021
f82b9e6
Cleanup
ryzngard Nov 19, 2021
501bbc9
Add generic type test. Fix name comparison to always use `.Name`
ryzngard Nov 29, 2021
05a8f09
PR feedback, cleanup, and more tests with arity
ryzngard Nov 30, 2021
79ac4f6
Merge branch 'main' into features/stackframe_parser_ui
ryzngard Nov 30, 2021
5b6a85c
Remove extra test
ryzngard Nov 30, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eng/targets/Services.props
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<ServiceHubService Include="Microsoft.VisualStudio.LanguageServices.UnusedReferenceAnalysis" ClassName="Microsoft.CodeAnalysis.Remote.RemoteUnusedReferenceAnalysisService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.LanguageServices.ProcessTelemetry" ClassName="Microsoft.CodeAnalysis.Remote.RemoteProcessTelemetryService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.LanguageServices.CompilationAvailable" ClassName="Microsoft.CodeAnalysis.Remote.RemoteCompilationAvailableService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.LanguageServices.StackTraceExplorer" ClassName="Microsoft.CodeAnalysis.Remote.Services.StackTraceExplorer.RemoteStackTraceExplorerService+Factory" />
</ItemGroup>

<!--
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// 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.Collections.Immutable;
using System.Composition;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.EmbeddedLanguages.StackFrame;
using Microsoft.CodeAnalysis.FindUsages;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.StackTraceExplorer;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.StackTraceExplorer
{
[ExportWorkspaceService(typeof(IStackTraceExplorerService)), Shared]
internal class StackTraceExplorerService : IStackTraceExplorerService
{
[ImportingConstructor]
[System.Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public StackTraceExplorerService()
{
}

public (Document? document, int line) GetDocumentAndLine(Solution solution, ParsedFrame frame)
{
if (frame is ParsedStackFrame parsedFrame)
{
var matches = GetFileMatches(solution, parsedFrame.Root, out var line);
if (matches.IsEmpty)
{
return default;
}

return (matches[0], line);
}

return default;
}

public async Task<DefinitionItem?> TryFindDefinitionAsync(Solution solution, ParsedFrame frame, StackFrameSymbolPart symbolPart, CancellationToken cancellationToken)
{
if (frame is not ParsedStackFrame parsedFrame)
{
return null;
}

var client = await RemoteHostClient.TryGetClientAsync(solution.Workspace, cancellationToken).ConfigureAwait(false);
if (client is not null)
{
var result = await client.TryInvokeAsync<IRemoteStackTraceExplorerService, SerializableDefinitionItem?>(
solution,
(service, solutionInfo, cancellationToken) => service.TryFindDefinitionAsync(solutionInfo, parsedFrame.ToString(), symbolPart, cancellationToken),
cancellationToken).ConfigureAwait(false);

if (!result.HasValue)
{
return null;
}

var serializedDefinition = result.Value;
if (!serializedDefinition.HasValue)
{
return null;
}

return await serializedDefinition.Value.RehydrateAsync(solution, cancellationToken).ConfigureAwait(false);
}

return await StackTraceExplorerUtilities.GetDefinitionAsync(solution, parsedFrame.Root, symbolPart, cancellationToken).ConfigureAwait(false);
}

private static ImmutableArray<Document> GetFileMatches(Solution solution, StackFrameCompilationUnit root, out int lineNumber)
{
lineNumber = 0;
if (root.FileInformationExpression is null)
{
return ImmutableArray<Document>.Empty;
}

var fileName = root.FileInformationExpression.Path.ToString();
var lineString = root.FileInformationExpression.Line.ToString();
RoslynDebug.AssertNotNull(lineString);
lineNumber = int.Parse(lineString);

var documentName = Path.GetFileName(fileName);
var potentialMatches = new HashSet<Document>();

foreach (var project in solution.Projects)
{
foreach (var document in project.Documents)
{
if (document.FilePath == fileName)
{
return ImmutableArray.Create(document);
}

else if (document.Name == documentName)
{
potentialMatches.Add(document);
}
}
}

return potentialMatches.ToImmutableArray();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// 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.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.FindUsages;
using Microsoft.CodeAnalysis.EmbeddedLanguages.StackFrame;
using Microsoft.CodeAnalysis.FindUsages;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.StackTraceExplorer;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.StackTraceExplorer
{
internal static class StackTraceExplorerUtilities
{
public static async Task<DefinitionItem?> GetDefinitionAsync(Solution solution, StackFrameCompilationUnit compilationUnit, StackFrameSymbolPart symbolPart, CancellationToken cancellationToken)
{
// MemberAccessExpression is [Expression].[Identifier], and Identifier is the
// method name.
var typeExpression = compilationUnit.MethodDeclaration.MemberAccessExpression.Left;
var fullyQualifiedTypeName = typeExpression.ToString();
ryzngard marked this conversation as resolved.
Show resolved Hide resolved
var typeName = typeExpression is StackFrameQualifiedNameNode qualifiedName
? qualifiedName.Right.ToString()
: typeExpression.ToString();

RoslynDebug.AssertNotNull(fullyQualifiedTypeName);

var methodIdentifier = compilationUnit.MethodDeclaration.MemberAccessExpression.Right;
var methodTypeArguments = compilationUnit.MethodDeclaration.TypeArguments;
var methodArguments = compilationUnit.MethodDeclaration.ArgumentList;

var methodName = methodIdentifier.ToString();

//
// Do a first pass to find projects with the type name to check first
//
using var _ = PooledObjects.ArrayBuilder<Project>.GetInstance(out var candidateProjects);
foreach (var project in solution.Projects)
{
if (!project.SupportsCompilation)
{
continue;
}

var containsSymbol = await project.ContainsSymbolsWithNameAsync(
(name) => name == typeName,
ryzngard marked this conversation as resolved.
Show resolved Hide resolved
SymbolFilter.Type,
cancellationToken).ConfigureAwait(false);

if (containsSymbol)
{
var matchingMethods = await GetMatchingMembersFromCompilationAsync(project).ConfigureAwait(false);
if (matchingMethods.Any())
{
return await matchingMethods[0].ToNonClassifiedDefinitionItemAsync(solution, includeHiddenLocations: true, cancellationToken).ConfigureAwait(false);
}
}
else
{
candidateProjects.Add(project);
}
}

//
// Do a second pass to check the remaining compilations
// for the symbol, which may be a metadata symbol in the compilation
//
foreach (var project in candidateProjects)
Copy link
Member

Choose a reason for hiding this comment

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

the candidateProjects local seems unnecessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This just keeps us from getting the compilation twice and searching on a project that we already searched. Could do without... 🤷 the compilation would already be computed so theoretically the expensive part is done.

{
var matchingMethods = await GetMatchingMembersFromCompilationAsync(project).ConfigureAwait(false);
if (matchingMethods.Any())
{
return await matchingMethods[0].ToNonClassifiedDefinitionItemAsync(solution, includeHiddenLocations: true, cancellationToken).ConfigureAwait(false);
}
}

return null;

async Task<ImmutableArray<IMethodSymbol>> GetMatchingMembersFromCompilationAsync(Project project)
{
var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
Copy link
Member

Choose a reason for hiding this comment

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

we already checked source above. so really we just want to check metdata. so we should have a hashset tracking which PE references we looked at. we should then walk each project and walk the PE refs of each, seeing if it's the first time we're seeing it. if so, we should check our metadata-index for that pe-ref to see if it's a hit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

IMO we should do this in a follow up PR, it seems complex enough to warrant it's own commit and some feedback on the correct way to do it.

var type = compilation.GetTypeByMetadataName(fullyQualifiedTypeName);
if (type is null)
{
return ImmutableArray<IMethodSymbol>.Empty;
}

var members = type.GetMembers();
return members
.OfType<IMethodSymbol>()
.Where(m => m.Name == methodName)
.Where(m => MatchTypeArguments(m.TypeArguments, methodTypeArguments))
.Where(m => MatchParameters(m.Parameters, methodArguments))
.ToImmutableArrayOrEmpty();
}
}

private static bool MatchParameters(ImmutableArray<IParameterSymbol> parameters, StackFrameParameterList stackFrameParameters)
{
if (parameters.Length != stackFrameParameters.Parameters.Length)
{
return false;
}

for (var i = 0; i < stackFrameParameters.Parameters.Length; i++)
{
var stackFrameParameter = stackFrameParameters.Parameters[i];
var paramSymbol = parameters[i];

if (paramSymbol.Name != stackFrameParameter.Identifier.ToString())
{
return false;
}

if (!MatchType(paramSymbol.Type, stackFrameParameter.Type))
{
return false;
}
}

return true;
}

private static bool MatchTypeArguments(ImmutableArray<ITypeSymbol> typeArguments, StackFrameTypeArgumentList? stackFrameTypeArgumentList)
{
if (stackFrameTypeArgumentList is null)
{
return typeArguments.IsEmpty;
}

if (typeArguments.IsEmpty)
{
return false;
}

var stackFrameTypeArguments = stackFrameTypeArgumentList.TypeArguments;
return typeArguments.Length == stackFrameTypeArguments.Length;
}

private static bool MatchType(ITypeSymbol type, StackFrameTypeNode stackFrameType)
{
if (type is IArrayTypeSymbol arrayType)
{
if (stackFrameType is not StackFrameArrayTypeNode arrayTypeNode)
{
return false;
}

ITypeSymbol currentType = arrayType;

// Iterate through each array expression and make sure the dimensions
// match the element types in an array.
// Ex: string[,][]
// [,] is a 2 dimension array with element type string[]
// [] is a 1 dimension array with element type string
foreach (var arrayExpression in arrayTypeNode.ArrayRankSpecifiers)
{
if (currentType is not IArrayTypeSymbol currentArrayType)
{
return false;
}

if (currentArrayType.Rank != arrayExpression.CommaTokens.Length + 1)
{
return false;
}

currentType = currentArrayType.ElementType;
}

// All array types have been exchausted from the
// stackframe identifier and the type is still an array
if (currentType is IArrayTypeSymbol)
{
return false;
}

return MatchType(currentType, arrayTypeNode.TypeIdentifier);
}

// Special types can have different casing representations
// Ex: string and String are the same (System.String)
if (type.IsSpecialType())
{
return type.Name == stackFrameType.ToString();
}
ryzngard marked this conversation as resolved.
Show resolved Hide resolved

// Default to just comparing the display name
return type.ToDisplayString() == stackFrameType.ToString();
}
}
}
Loading