From 5726f909074dbe662d11d3e60563bdc85c5e4f52 Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Sat, 29 May 2021 23:01:24 -0700 Subject: [PATCH 1/6] Add V2 version of GotoDefinitionService The V1 GotoDefinitionService can only return one definition for a symbol. This is particularly annoying for partial types, which have multiple locations, and the location the service returns is pretty much never the one I want. The V2 service is very similar, but it uses V2 location models and returns a list of definitions, not a single one. --- .../v2/CodeActions/ICodeActionRequest.cs | 2 +- .../GotoDefinition/GotoDefinitionRequest.cs | 11 + .../GotoDefinition/GotoDefinitionResponse.cs | 18 + .../Models/v2/Location.cs | 10 + src/OmniSharp.Abstractions/Models/v2/Point.cs | 32 +- src/OmniSharp.Abstractions/Models/v2/Range.cs | 34 +- .../OmniSharpEndpoints.cs | 1 + .../Extensions/ResponseExtensions.cs | 19 +- .../Refactoring/V2/BaseCodeActionHandler.cs | 13 +- .../Navigation/GoToDefinitionHelpers.cs | 51 ++ .../Navigation/GotoDefinitionService.cs | 87 +-- .../Navigation/GotoDefinitionServiceV2.cs | 84 +++ .../Extensions/TextExtensions.cs | 14 +- .../GoToDefinitionV2Facts.cs | 636 ++++++++++++++++++ 14 files changed, 885 insertions(+), 127 deletions(-) create mode 100644 src/OmniSharp.Abstractions/Models/v2/GotoDefinition/GotoDefinitionRequest.cs create mode 100644 src/OmniSharp.Abstractions/Models/v2/GotoDefinition/GotoDefinitionResponse.cs create mode 100644 src/OmniSharp.Abstractions/Models/v2/Location.cs create mode 100644 src/OmniSharp.Roslyn.CSharp/Services/Navigation/GoToDefinitionHelpers.cs create mode 100644 src/OmniSharp.Roslyn.CSharp/Services/Navigation/GotoDefinitionServiceV2.cs create mode 100644 tests/OmniSharp.Roslyn.CSharp.Tests/GoToDefinitionV2Facts.cs diff --git a/src/OmniSharp.Abstractions/Models/v2/CodeActions/ICodeActionRequest.cs b/src/OmniSharp.Abstractions/Models/v2/CodeActions/ICodeActionRequest.cs index 33b2aa0fc2..1ed6769ad9 100644 --- a/src/OmniSharp.Abstractions/Models/v2/CodeActions/ICodeActionRequest.cs +++ b/src/OmniSharp.Abstractions/Models/v2/CodeActions/ICodeActionRequest.cs @@ -10,6 +10,6 @@ public interface ICodeActionRequest int Column { get; } string Buffer { get; } string FileName { get; } - Range Selection { get; } + Range Selection { get; set; } } } diff --git a/src/OmniSharp.Abstractions/Models/v2/GotoDefinition/GotoDefinitionRequest.cs b/src/OmniSharp.Abstractions/Models/v2/GotoDefinition/GotoDefinitionRequest.cs new file mode 100644 index 0000000000..228b0aba68 --- /dev/null +++ b/src/OmniSharp.Abstractions/Models/v2/GotoDefinition/GotoDefinitionRequest.cs @@ -0,0 +1,11 @@ +using OmniSharp.Mef; + +namespace OmniSharp.Models.V2.GotoDefinition +{ + [OmniSharpEndpoint(OmniSharpEndpoints.V2.GotoDefinition, typeof(GotoDefinitionRequest), typeof(GotoDefinitionResponse))] + public class GotoDefinitionRequest : Request + { + public int Timeout { get; init; } = 10000; + public bool WantMetadata { get; init; } + } +} diff --git a/src/OmniSharp.Abstractions/Models/v2/GotoDefinition/GotoDefinitionResponse.cs b/src/OmniSharp.Abstractions/Models/v2/GotoDefinition/GotoDefinitionResponse.cs new file mode 100644 index 0000000000..086f16d8bc --- /dev/null +++ b/src/OmniSharp.Abstractions/Models/v2/GotoDefinition/GotoDefinitionResponse.cs @@ -0,0 +1,18 @@ +#nullable enable + +using OmniSharp.Models.Metadata; +using System.Collections.Generic; + +namespace OmniSharp.Models.V2.GotoDefinition +{ + public record GotoDefinitionResponse + { + public List? Definitions { get; init; } + } + + public record Definition + { + public Location Location { get; init; } = null!; + public MetadataSource? MetadataSource { get; init; } + } +} diff --git a/src/OmniSharp.Abstractions/Models/v2/Location.cs b/src/OmniSharp.Abstractions/Models/v2/Location.cs new file mode 100644 index 0000000000..ae72471c3d --- /dev/null +++ b/src/OmniSharp.Abstractions/Models/v2/Location.cs @@ -0,0 +1,10 @@ +#nullable enable + +namespace OmniSharp.Models.V2 +{ + public record Location + { + public string FileName { get; init; } = null!; + public Range Range { get; init; } = null!; + } +} diff --git a/src/OmniSharp.Abstractions/Models/v2/Point.cs b/src/OmniSharp.Abstractions/Models/v2/Point.cs index 7ec9c82731..8b0b74deae 100644 --- a/src/OmniSharp.Abstractions/Models/v2/Point.cs +++ b/src/OmniSharp.Abstractions/Models/v2/Point.cs @@ -1,39 +1,13 @@ using Newtonsoft.Json; using System; -using System.Collections.Generic; namespace OmniSharp.Models.V2 { - public class Point : IEquatable + public record Point : IEquatable { [JsonConverter(typeof(ZeroBasedIndexConverter))] - public int Line { get; set; } + public int Line { get; init; } [JsonConverter(typeof(ZeroBasedIndexConverter))] - public int Column { get; set; } - - public override bool Equals(object obj) - => Equals(obj as Point); - - public bool Equals(Point other) - => other != null - && Line == other.Line - && Column == other.Column; - - public override int GetHashCode() - { - var hashCode = -1456208474; - hashCode = hashCode * -1521134295 + Line.GetHashCode(); - hashCode = hashCode * -1521134295 + Column.GetHashCode(); - return hashCode; - } - - public override string ToString() - => $"Line = {Line}, Column = {Column}"; - - public static bool operator ==(Point point1, Point point2) - => EqualityComparer.Default.Equals(point1, point2); - - public static bool operator !=(Point point1, Point point2) - => !(point1 == point2); + public int Column { get; init; } } } diff --git a/src/OmniSharp.Abstractions/Models/v2/Range.cs b/src/OmniSharp.Abstractions/Models/v2/Range.cs index f183f69fcc..64a49993ef 100644 --- a/src/OmniSharp.Abstractions/Models/v2/Range.cs +++ b/src/OmniSharp.Abstractions/Models/v2/Range.cs @@ -1,12 +1,9 @@ -using System; -using System.Collections.Generic; - namespace OmniSharp.Models.V2 { - public class Range : IEquatable + public record Range { - public Point Start { get; set; } - public Point End { get; set; } + public Point Start { get; init; } + public Point End { get; init; } public bool Contains(int line, int column) { @@ -29,30 +26,5 @@ public bool Contains(int line, int column) } public bool IsValid() => Start != null && Start.Line > -1 && Start.Column > -1 && End != null && End.Line > -1 && End.Column > -1; - - public override bool Equals(object obj) - => Equals(obj as Range); - - public bool Equals(Range other) - => other != null - && EqualityComparer.Default.Equals(Start, other.Start) - && EqualityComparer.Default.Equals(End, other.End); - - public override int GetHashCode() - { - var hashCode = -1676728671; - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Start); - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(End); - return hashCode; - } - - public override string ToString() - => $"Start = {{{Start}}}, End = {{{End}}}"; - - public static bool operator ==(Range range1, Range range2) - => EqualityComparer.Default.Equals(range1, range2); - - public static bool operator !=(Range range1, Range range2) - => !(range1 == range2); } } diff --git a/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs b/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs index fd292c4c84..2bfcecd086 100644 --- a/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs +++ b/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs @@ -72,6 +72,7 @@ public static class V2 public const string Highlight = "/v2/highlight"; + public const string GotoDefinition = "/v2/gotodefinition"; } } } diff --git a/src/OmniSharp.Cake/Extensions/ResponseExtensions.cs b/src/OmniSharp.Cake/Extensions/ResponseExtensions.cs index ff30504794..67ea193833 100644 --- a/src/OmniSharp.Cake/Extensions/ResponseExtensions.cs +++ b/src/OmniSharp.Cake/Extensions/ResponseExtensions.cs @@ -272,16 +272,19 @@ private static async Task TranslateAsync(this Range range, OmniSharpWorks if (range.Start.Line == range.End.Line) { - range.Start.Line = line; - range.End.Line = line; - return range; + return range with + { + Start = range.Start with { Line = line }, + End = range.End with { Line = line } + }; } - range.Start.Line = line; - (line, _) = await LineIndexHelper.TranslateFromGenerated(request.FileName, range.End.Line, workspace, true); - range.End.Line = line; - - return range; + var (endLine, _) = await LineIndexHelper.TranslateFromGenerated(request.FileName, range.End.Line, workspace, true); + return range with + { + Start = range.Start with { Line = line }, + End = range.End with { Line = endLine } + }; } private static async Task TranslateAsync(this FileMemberElement element, OmniSharpWorkspace workspace, Request request) diff --git a/src/OmniSharp.Cake/Services/RequestHandlers/Refactoring/V2/BaseCodeActionHandler.cs b/src/OmniSharp.Cake/Services/RequestHandlers/Refactoring/V2/BaseCodeActionHandler.cs index 2ef7d5a969..8a684bd5af 100644 --- a/src/OmniSharp.Cake/Services/RequestHandlers/Refactoring/V2/BaseCodeActionHandler.cs +++ b/src/OmniSharp.Cake/Services/RequestHandlers/Refactoring/V2/BaseCodeActionHandler.cs @@ -5,7 +5,7 @@ namespace OmniSharp.Cake.Services.RequestHandlers.Refactoring.V2 { public abstract class BaseCodeActionsHandler : CakeRequestHandler - where TRequest : ICodeActionRequest + where TRequest : ICodeActionRequest, new() { protected BaseCodeActionsHandler( OmniSharpWorkspace workspace) @@ -18,10 +18,17 @@ protected override async Task TranslateRequestAsync(TRequest request) if (request.Selection != null) { var startLine = await LineIndexHelper.TranslateToGenerated(request.FileName, request.Selection.Start.Line, Workspace); - request.Selection.End.Line = request.Selection.Start.Line != request.Selection.End.Line + var endLine = request.Selection.Start.Line != request.Selection.End.Line ? await LineIndexHelper.TranslateToGenerated(request.FileName, request.Selection.End.Line, Workspace) : startLine; - request.Selection.Start.Line = startLine; + request = new TRequest() + { + Selection = request.Selection with + { + Start = request.Selection.Start with { Line = startLine }, + End = request.Selection.End with { Line = endLine } + } + }; } return await base.TranslateRequestAsync(request); diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GoToDefinitionHelpers.cs b/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GoToDefinitionHelpers.cs new file mode 100644 index 0000000000..80276202d7 --- /dev/null +++ b/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GoToDefinitionHelpers.cs @@ -0,0 +1,51 @@ +#nullable enable + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.FindSymbols; +using OmniSharp.Extensions; +using OmniSharp.Options; +using System.Threading.Tasks; + +namespace OmniSharp.Roslyn.CSharp.Services.Navigation +{ + internal static class GoToDefinitionHelpers + { + internal static async Task GetDefinitionSymbol(Document document, int line, int column) + { + var sourceText = await document.GetTextAsync(); + var position = sourceText.GetPositionFromLineAndOffset(line, column); + var symbol = await SymbolFinder.FindSymbolAtPositionAsync(document, position); + + return symbol switch + { + INamespaceSymbol => null, + // Always prefer the partial implementation over the definition + IMethodSymbol { IsPartialDefinition: true, PartialImplementationPart: var impl } => impl, + // Don't return property getters/settings/initers + IMethodSymbol { AssociatedSymbol: IPropertySymbol } => null, + _ => symbol + }; + } + + internal static async Task GetMetadataMappedSpan( + Document document, + ISymbol symbol, + ExternalSourceServiceFactory externalSourceServiceFactory, + IExternalSourceService externalSourceService, + OmniSharpOptions options, + int timeout) + { + + var cancellationToken = externalSourceServiceFactory.CreateCancellationToken(options, timeout); + var (metadataDocument, _) = await externalSourceService.GetAndAddExternalSymbolDocument(document.Project, symbol, cancellationToken); + if (metadataDocument != null) + { + cancellationToken = externalSourceServiceFactory.CreateCancellationToken(options, timeout); + var metadataLocation = await externalSourceService.GetExternalSymbolLocation(symbol, metadataDocument, cancellationToken); + return metadataLocation.GetMappedLineSpan(); + } + + return null; + } + } +} diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GotoDefinitionService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GotoDefinitionService.cs index 18d99416dc..f073a8c0da 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GotoDefinitionService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GotoDefinitionService.cs @@ -1,9 +1,9 @@ +#nullable enable + using System.Composition; using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.FindSymbols; -using Microsoft.CodeAnalysis.Text; using OmniSharp.Extensions; using OmniSharp.Mef; using OmniSharp.Models.GotoDefinition; @@ -33,67 +33,46 @@ public async Task Handle(GotoDefinitionRequest request) var document = externalSourceService.FindDocumentInCache(request.FileName) ?? _workspace.GetDocument(request.FileName); - var response = new GotoDefinitionResponse(); - - if (document != null) + var symbol = await GoToDefinitionHelpers.GetDefinitionSymbol(document, request.Line, request.Column); + if (symbol == null) { - var semanticModel = await document.GetSemanticModelAsync(); - var sourceText = await document.GetTextAsync(); - var position = sourceText.GetTextPosition(request); - var symbol = await SymbolFinder.FindSymbolAtPositionAsync(semanticModel, position, _workspace); - - // go to definition for namespaces is not supported - if (symbol != null && !(symbol is INamespaceSymbol)) - { - // for partial methods, pick the one with body - if (symbol is IMethodSymbol method) - { - // Return an empty response for property accessor symbols like get and set - if (method.AssociatedSymbol is IPropertySymbol) - return response; + return new GotoDefinitionResponse(); + } - symbol = method.PartialImplementationPart ?? symbol; - } + var location = symbol.Locations.First(); - var location = symbol.Locations.First(); + GotoDefinitionResponse? response = null; + if (location.IsInSource) + { + var lineSpan = symbol.Locations.First().GetMappedLineSpan(); + response = new GotoDefinitionResponse + { + FileName = lineSpan.Path, + Line = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character + }; + } + else if (location.IsInMetadata && request.WantMetadata) + { + var maybeSpan = await GoToDefinitionHelpers.GetMetadataMappedSpan(document, symbol, _externalSourceServiceFactory, externalSourceService, _omnisharpOptions, request.Timeout); - if (location.IsInSource) - { - var lineSpan = symbol.Locations.First().GetMappedLineSpan(); - response = new GotoDefinitionResponse - { - FileName = lineSpan.Path, - Line = lineSpan.StartLinePosition.Line, - Column = lineSpan.StartLinePosition.Character - }; - } - else if (location.IsInMetadata && request.WantMetadata) + if (maybeSpan is FileLinePositionSpan lineSpan) + { + response = new GotoDefinitionResponse { - var cancellationToken = _externalSourceServiceFactory.CreateCancellationToken(_omnisharpOptions, request.Timeout); - var (metadataDocument, _) = await externalSourceService.GetAndAddExternalSymbolDocument(document.Project, symbol, cancellationToken); - if (metadataDocument != null) + Line = lineSpan.StartLinePosition.Line, + Column = lineSpan.StartLinePosition.Character, + MetadataSource = new MetadataSource() { - cancellationToken = _externalSourceServiceFactory.CreateCancellationToken(_omnisharpOptions, request.Timeout); - var metadataLocation = await externalSourceService.GetExternalSymbolLocation(symbol, metadataDocument, cancellationToken); - var lineSpan = metadataLocation.GetMappedLineSpan(); - - response = new GotoDefinitionResponse - { - Line = lineSpan.StartLinePosition.Line, - Column = lineSpan.StartLinePosition.Character, - MetadataSource = new MetadataSource() - { - AssemblyName = symbol.ContainingAssembly.Name, - ProjectName = document.Project.Name, - TypeName = symbol.GetSymbolName() - }, - }; - } - } + AssemblyName = symbol.ContainingAssembly.Name, + ProjectName = document.Project.Name, + TypeName = symbol.GetSymbolName() + }, + }; } } - return response; + return response ?? new GotoDefinitionResponse(); } } } diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GotoDefinitionServiceV2.cs b/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GotoDefinitionServiceV2.cs new file mode 100644 index 0000000000..822afdcf74 --- /dev/null +++ b/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GotoDefinitionServiceV2.cs @@ -0,0 +1,84 @@ +#nullable enable + +using Microsoft.CodeAnalysis; +using OmniSharp.Extensions; +using OmniSharp.Mef; +using OmniSharp.Models.V2.GotoDefinition; +using OmniSharp.Options; +using System.Composition; +using System.Linq; +using System.Threading.Tasks; + +namespace OmniSharp.Roslyn.CSharp.Services.Navigation +{ + [OmniSharpHandler(OmniSharpEndpoints.V2.GotoDefinition, LanguageNames.CSharp)] + public class GotoDefinitionServiceV2 : IRequestHandler + { + private readonly OmniSharpOptions _omnisharpOptions; + private readonly OmniSharpWorkspace _workspace; + private readonly ExternalSourceServiceFactory _externalSourceServiceFactory; + + [ImportingConstructor] + public GotoDefinitionServiceV2(OmniSharpWorkspace workspace, ExternalSourceServiceFactory externalSourceServiceFactory, OmniSharpOptions omnisharpOptions) + { + _workspace = workspace; + _externalSourceServiceFactory = externalSourceServiceFactory; + _omnisharpOptions = omnisharpOptions; + } + + public async Task Handle(GotoDefinitionRequest request) + { + var cancellationToken = _externalSourceServiceFactory.CreateCancellationToken(_omnisharpOptions, request.Timeout); + var externalSourceService = _externalSourceServiceFactory.Create(_omnisharpOptions); + var document = externalSourceService.FindDocumentInCache(request.FileName) ?? + _workspace.GetDocument(request.FileName); + + if (document == null) + { + return new GotoDefinitionResponse(); + } + + var symbol = await GoToDefinitionHelpers.GetDefinitionSymbol(document, request.Line, request.Column); + if (symbol?.Locations.IsDefaultOrEmpty != false) + { + return new GotoDefinitionResponse(); + } + + if (symbol.Locations[0].IsInSource) + { + return new GotoDefinitionResponse() + { + Definitions = symbol.Locations + .Select(location => new Definition { Location = location.GetMappedLineSpan().GetLocationFromFileLinePositionSpan() }) + .ToList() + }; + } + else + { + var maybeSpan = await GoToDefinitionHelpers.GetMetadataMappedSpan(document, symbol, _externalSourceServiceFactory, externalSourceService, _omnisharpOptions, request.Timeout); + + if (maybeSpan is FileLinePositionSpan lineSpan) + { + return new GotoDefinitionResponse + { + Definitions = new() + { + new Definition + { + Location = lineSpan.GetLocationFromFileLinePositionSpan(), + MetadataSource = new OmniSharp.Models.Metadata.MetadataSource() + { + AssemblyName = symbol.ContainingAssembly.Name, + ProjectName = document.Project.Name, + TypeName = symbol.GetSymbolName() + } + } + } + }; + } + + return new GotoDefinitionResponse(); + } + } + } +} diff --git a/src/OmniSharp.Roslyn/Extensions/TextExtensions.cs b/src/OmniSharp.Roslyn/Extensions/TextExtensions.cs index 1f22f1fffe..549fa99b3d 100644 --- a/src/OmniSharp.Roslyn/Extensions/TextExtensions.cs +++ b/src/OmniSharp.Roslyn/Extensions/TextExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; using OmniSharp.Models; using OmniSharp.Models.V2; @@ -42,6 +43,17 @@ public static Range GetRangeFromSpan(this SourceText text, TextSpan span) End = text.GetPointFromPosition(span.End) }; + public static Models.V2.Location GetLocationFromFileLinePositionSpan(this FileLinePositionSpan linePositionSpan) + => new() + { + FileName = linePositionSpan.Path, + Range = new() + { + Start = new Point { Line = linePositionSpan.StartLinePosition.Line, Column = linePositionSpan.StartLinePosition.Character }, + End = new Point { Line = linePositionSpan.EndLinePosition.Line, Column = linePositionSpan.EndLinePosition.Character } + } + }; + /// /// Converts an OmniSharp to a within a . /// diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/GoToDefinitionV2Facts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/GoToDefinitionV2Facts.cs new file mode 100644 index 0000000000..10bbc5d64a --- /dev/null +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/GoToDefinitionV2Facts.cs @@ -0,0 +1,636 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using OmniSharp.Roslyn.CSharp.Services.Navigation; +using OmniSharp.Models.Metadata; +using TestUtility; +using Xunit; +using Xunit.Abstractions; +using System.Collections.Generic; +using OmniSharp.Models.V2.GotoDefinition; + +namespace OmniSharp.Roslyn.CSharp.Tests +{ + public class GoToDefinitionV2Facts : AbstractSingleRequestHandlerTestFixture + { + public GoToDefinitionV2Facts(ITestOutputHelper output, SharedOmniSharpHostFixture sharedOmniSharpHostFixture) + : base(output, sharedOmniSharpHostFixture) + { + } + + protected override string EndpointName => OmniSharpEndpoints.V2.GotoDefinition; + + [Theory] + [InlineData("foo.cs")] + [InlineData("foo.csx")] + public async Task ReturnsDefinitionInSameFile(string filename) + { + var testFile = new TestFile(filename, @" +class {|def:Foo|} { + private F$$oo foo; +}"); + + await TestGoToSourceAsync(testFile); + } + + [Theory] + [InlineData("foo.cs")] + [InlineData("foo.csx")] + public async Task ReturnsOnPropertAccessorGet(string filename) + { + var testFile = new TestFile(filename, @" +class Test { + public string Foo{ g$$et; set; } +}"); + + await TestGoToSourceAsync(testFile); + } + + [Theory] + [InlineData("foo.cs")] + [InlineData("foo.csx")] + public async Task ReturnOnPropertAccessorSet(string filename) + { + var testFile = new TestFile(filename, @" +class Test { + public int Foo{ get; s$$et; } +}"); + + await TestGoToSourceAsync(testFile); + } + + [Theory] + [InlineData("foo.cs")] + [InlineData("foo.csx")] + public async Task ReturnOnPropertyAccessorPropertyDef(string filename) + { + var testFile = new TestFile(filename, @" +class Test { + public int {|def:Fo$$o|} { get; set; } +}"); + + await TestGoToSourceAsync(testFile); + } + + [Theory] + [InlineData("foo.cs")] + [InlineData("foo.csx")] + public async Task ReturnsOnPropertyAccessorPropertySetting(string filename) + { + var testFile = new TestFile(filename, @" +class Test { + public int {|def:Foo|} { get; set; } + + public static void main() + { + F$$oo = 3; + } +}"); + + await TestGoToSourceAsync(testFile); + } + + [Theory] + [InlineData("foo.cs")] + [InlineData("foo.csx")] + public async Task ReturnsOnPropertyAccessorField1(string filename) + { + var testFile = new TestFile(filename, @" +class Test { + + public int {|def:foo|}; + + public int Foo + { + get => f$$oo; + set => foo = value; + } +}"); + + await TestGoToSourceAsync(testFile); + } + + [Theory] + [InlineData("foo.cs")] + [InlineData("foo.csx")] + public async Task ReturnsOnPropertyAccessorField2(string filename) + { + var testFile = new TestFile(filename, @" +class Test { + + public int {|def:foo|}; + + public int Foo + { + get => foo; + set => f$$oo = value; + } +}"); + + await TestGoToSourceAsync(testFile); + } + + [Theory] + [InlineData("foo.cs")] + [InlineData("foo.csx")] + public async Task ReturnsOnPropertyAccessorPropertyGetting(string filename) + { + var testFile = new TestFile(filename, @" +class Test { + public int {|def:Foo|} { get; set; } + + public static void main() + { + Foo = 3; + Console.WriteLine(F$$oo); + } +}"); + + await TestGoToSourceAsync(testFile); + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsPartialMethodDefinitionWithBody(string filename) + { + var testFile = new TestFile(filename, @" + public partial class MyClass + { + public MyClass() + { + Met$$hod(); + } + + partial void {|def:Method|}() + { + //do stuff + } + } + + public partial class MyClass + { + partial void Method(); + }"); + + await TestGoToSourceAsync(testFile); + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsMultiplePartialTypeDefinition(string filename) + { + var testFile = new TestFile(filename, @" +partial class {|def:Class|} +{ + Cla$$ss c; +} +partial class {|def:Class|} +{ +}"); + + await TestGoToSourceAsync(testFile); + } + + [Fact] + public async Task ReturnsMultiplePartialTypeDefinition_MultipleFiles() + { + var testFile1 = new TestFile("bar.cs", @" +partial class {|def:Class|} +{ + Cla$$ss c; +} +"); + + var testFile2 = new TestFile("baz.cs", @" +partial class {|def:Class|} +{ +}"); + + await TestGoToSourceAsync(testFile1, testFile2); + } + + [Fact] + public async Task ReturnsDefinitionInDifferentFile() + { + var testFile1 = new TestFile("foo.cs", @" +using System; +class {|def:Foo|} { +}"); + var testFile2 = new TestFile("bar.cs", @" +class Bar { + private F$$oo foo; +}"); + + await TestGoToSourceAsync(testFile1, testFile2); + } + + [Fact] + public async Task ReturnsEmptyResultWhenDefinitionIsNotFound() + { + var testFile1 = new TestFile("foo.cs", @" +using System; +class Foo { +}"); + var testFile2 = new TestFile("bar.cs", @" +class Bar { + private B$$az foo; +}"); + + await TestGoToSourceAsync(testFile1, testFile2); + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsDefinitionInMetadata_WhenSymbolIsStaticMethod(string filename) + { + var testFile = new TestFile(filename, @" +using System; +class Bar { + public void Baz() { + Guid.NewG$$uid(); + } +}"); + + await TestGoToMetadataAsync(testFile, + expectedAssemblyName: AssemblyHelpers.CorLibName, + expectedTypeName: "System.Guid"); + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsDecompiledDefinition_WhenSymbolIsStaticMethod(string filename) + { + var testFile = new TestFile(filename, @" +using System; +class Bar { + public void Baz() { + Guid.NewG$$uid(); + } +}"); + + await TestDecompilationAsync(testFile, + expectedAssemblyName: AssemblyHelpers.CorLibName, + expectedTypeName: "System.Guid"); + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsDefinitionInMetadata_WhenSymbolIsInstanceMethod(string filename) + { + var testFile = new TestFile(filename, @" +using System.Collections.Generic; +class Bar { + public void Baz() { + var foo = new List(); + foo.ToAr$$ray(); + } +}"); + + await TestGoToMetadataAsync(testFile, + expectedAssemblyName: AssemblyHelpers.CorLibName, + expectedTypeName: "System.Collections.Generic.List`1"); + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsDecompiledDefinition_WhenSymbolIsInstanceMethod(string filename) + { + var testFile = new TestFile(filename, @" +using System.Collections.Generic; +class Bar { + public void Baz() { + var foo = new List(); + foo.ToAr$$ray(); + } +}"); + + await TestDecompilationAsync(testFile, + expectedAssemblyName: AssemblyHelpers.CorLibName, + expectedTypeName: "System.Collections.Generic.List`1"); + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsDefinitionInMetadata_WhenSymbolIsGenericType(string filename) + { + var testFile = new TestFile(filename, @" +using System.Collections.Generic; +class Bar { + public void Baz() { + var foo = new Li$$st(); + foo.ToArray(); + } +}"); + + await TestGoToMetadataAsync(testFile, + expectedAssemblyName: AssemblyHelpers.CorLibName, + expectedTypeName: "System.Collections.Generic.List`1"); + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsDecompiledDefinition_WhenSymbolIsGenericType(string filename) + { + var testFile = new TestFile(filename, @" +using System.Collections.Generic; +class Bar { + public void Baz() { + var foo = new Li$$st(); + foo.ToArray(); + } +}"); + + await TestDecompilationAsync(testFile, + expectedAssemblyName: AssemblyHelpers.CorLibName, + expectedTypeName: "System.Collections.Generic.List`1"); + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsDefinitionInMetadata_WhenSymbolIsType(string filename) + { + var testFile = new TestFile(filename, @" +using System; +class Bar { + public void Baz() { + var str = Stri$$ng.Empty; + } +}"); + + await TestGoToMetadataAsync(testFile, + expectedAssemblyName: AssemblyHelpers.CorLibName, + expectedTypeName: "System.String"); + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsDecompiledDefinition_WhenSymbolIsType(string filename) + { + var testFile = new TestFile(filename, @" +using System; +class Bar { + public void Baz() { + var str = Stri$$ng.Empty; + } +}"); + + await TestDecompilationAsync(testFile, + expectedAssemblyName: AssemblyHelpers.CorLibName, + expectedTypeName: "System.String"); + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsDefinitionInMetadata_FromMetadata_WhenSymbolIsType(string filename) + { + var testFile = new TestFile(filename, @" +using System; +class Bar { + public void Baz() { + var number = in$$t.MaxValue; + } +}"); + + using (var host = CreateOmniSharpHost(testFile)) + { + var point = testFile.Content.GetPointFromPosition(); + + // 1. start by asking for definition of "int" + var gotoDefinitionRequest = new GotoDefinitionRequest + { + FileName = testFile.FileName, + Line = point.Line, + Column = point.Offset, + WantMetadata = true, + Timeout = 60000 + }; + var gotoDefinitionRequestHandler = GetRequestHandler(host); + var gotoDefinitionResponse = await gotoDefinitionRequestHandler.Handle(gotoDefinitionRequest); + + // 2. now, based on the response information + // go to the metadata endpoint, and ask for "int" specific metadata + var metadataRequest = new MetadataRequest + { + AssemblyName = gotoDefinitionResponse.Definitions.Single().MetadataSource.AssemblyName, + TypeName = gotoDefinitionResponse.Definitions.Single().MetadataSource.TypeName, + ProjectName = gotoDefinitionResponse.Definitions.Single().MetadataSource.ProjectName, + Language = gotoDefinitionResponse.Definitions.Single().MetadataSource.Language + }; + var metadataRequestHandler = host.GetRequestHandler(OmniSharpEndpoints.Metadata); + var metadataResponse = await metadataRequestHandler.Handle(metadataRequest); + + // 3. the metadata response contains SourceName (metadata "file") and SourceText (syntax tree) + // use the source to locate "IComparable" which is an interface implemented by Int32 struct + var metadataTree = CSharpSyntaxTree.ParseText(metadataResponse.Source); + + var iComparable = metadataTree.GetCompilationUnitRoot(). + DescendantNodesAndSelf(). + OfType().First(). + BaseList.Types.FirstOrDefault(x => x.Type.ToString() == "IComparable"); + var relevantLineSpan = iComparable.GetLocation().GetLineSpan(); + + // 4. now ask for the definition of "IComparable" + // pass in the SourceName (metadata "file") as FileName - since it's not a regular file in our workspace + var metadataNavigationRequest = new GotoDefinitionRequest + { + FileName = metadataResponse.SourceName, + Line = relevantLineSpan.StartLinePosition.Line, + Column = relevantLineSpan.StartLinePosition.Character, + WantMetadata = true + }; + var metadataNavigationResponse = await gotoDefinitionRequestHandler.Handle(metadataNavigationRequest); + + // 5. validate the response to be matching the expected IComparable meta info + Assert.NotNull(metadataNavigationResponse.Definitions.Single().MetadataSource); + Assert.Equal(AssemblyHelpers.CorLibName, metadataNavigationResponse.Definitions.Single().MetadataSource.AssemblyName); + Assert.Equal("System.IComparable", metadataNavigationResponse.Definitions.Single().MetadataSource.TypeName); + + Assert.NotEqual(0, metadataNavigationResponse.Definitions.Single().Location.Range.Start.Line); + Assert.NotEqual(0, metadataNavigationResponse.Definitions.Single().Location.Range.Start.Column); + } + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsDecompiledDefinition_FromMetadata_WhenSymbolIsType(string filename) + { + var testFile = new TestFile(filename, @" +using System; +class Bar { + public void Baz() { + var number = in$$t.MaxValue; + } +}"); + + using var host = CreateOmniSharpHost(new[] { testFile }, new Dictionary + { + ["RoslynExtensionsOptions:EnableDecompilationSupport"] = "true" + }); + + var point = testFile.Content.GetPointFromPosition(); + + // 1. start by asking for definition of "int" + var gotoDefinitionRequest = new GotoDefinitionRequest + { + FileName = testFile.FileName, + Line = point.Line, + Column = point.Offset, + WantMetadata = true, + Timeout = 60000 + }; + var gotoDefinitionRequestHandler = GetRequestHandler(host); + var gotoDefinitionResponse = await gotoDefinitionRequestHandler.Handle(gotoDefinitionRequest); + + // 2. now, based on the response information + // go to the metadata endpoint, and ask for "int" specific decompiled source + var metadataRequest = new MetadataRequest + { + AssemblyName = gotoDefinitionResponse.Definitions.Single().MetadataSource.AssemblyName, + TypeName = gotoDefinitionResponse.Definitions.Single().MetadataSource.TypeName, + ProjectName = gotoDefinitionResponse.Definitions.Single().MetadataSource.ProjectName, + Language = gotoDefinitionResponse.Definitions.Single().MetadataSource.Language, + Timeout = 60000 + }; + var metadataRequestHandler = host.GetRequestHandler(OmniSharpEndpoints.Metadata); + var metadataResponse = await metadataRequestHandler.Handle(metadataRequest); + + // 3. the response contains SourceName ("file") and SourceText (syntax tree) + // use the source to locate "IComparable" which is an interface implemented by Int32 struct + var decompiledTree = CSharpSyntaxTree.ParseText(metadataResponse.Source); + var compilationUnit = decompiledTree.GetCompilationUnitRoot(); + + // second comment should indicate we have decompiled + var comments = compilationUnit.DescendantTrivia().Where(t => t.Kind() == SyntaxKind.SingleLineCommentTrivia).ToArray(); + Assert.NotNull(comments); + Assert.Equal("// Decompiled with ICSharpCode.Decompiler 7.0.0.6488", comments[1].ToString()); + + // contrary to regular metadata, we should have methods with full bodies + // this condition would fail if decompilation wouldn't work + var methods = compilationUnit. + DescendantNodesAndSelf(). + OfType(). + Where(m => m.Body != null); + + Assert.NotEmpty(methods); + + var iComparable = compilationUnit. + DescendantNodesAndSelf(). + OfType().First(). + BaseList.Types.FirstOrDefault(x => x.Type.ToString() == "IComparable"); + var relevantLineSpan = iComparable.GetLocation().GetLineSpan(); + + // 4. now ask for the definition of "IComparable" + // pass in the SourceName (metadata "file") as FileName - since it's not a regular file in our workspace + var metadataNavigationRequest = new GotoDefinitionRequest + { + FileName = metadataResponse.SourceName, + Line = relevantLineSpan.StartLinePosition.Line, + Column = relevantLineSpan.StartLinePosition.Character, + WantMetadata = true + }; + var metadataNavigationResponse = await gotoDefinitionRequestHandler.Handle(metadataNavigationRequest); + + // 5. validate the response to be matching the expected IComparable meta info + Assert.NotNull(metadataNavigationResponse.Definitions.Single().MetadataSource); + Assert.Equal(AssemblyHelpers.CorLibName, metadataNavigationResponse.Definitions.Single().MetadataSource.AssemblyName); + Assert.Equal("System.IComparable", metadataNavigationResponse.Definitions.Single().MetadataSource.TypeName); + + Assert.NotEqual(0, metadataNavigationResponse.Definitions.Single().Location.Range.Start.Line); + Assert.NotEqual(0, metadataNavigationResponse.Definitions.Single().Location.Range.Start.Column); + } + + [Fact] + public async Task ReturnsNoResultsButDoesNotThrowForNamespaces() + { + var testFile = new TestFile("foo.cs", "namespace F$$oo {}"); + var response = await GetResponseAsync(new[] { testFile }, wantMetadata: false); + Assert.Null(response.Definitions); + } + + private async Task TestGoToSourceAsync(params TestFile[] testFiles) + { + var response = await GetResponseAsync(testFiles, wantMetadata: false); + + var targets = + from tf in testFiles + from span in tf.Content.GetSpans("def") + select (tf, span); + + if (targets.Any()) + { + foreach (var (file, definitionSpan) in targets) + { + var definitionRange = file.Content.GetRangeFromSpan(definitionSpan); + + Assert.Contains(response.Definitions, + def => file.FileName == def.Location.FileName + && definitionRange.Start.Line == def.Location.Range.Start.Line + && definitionRange.Start.Offset == def.Location.Range.Start.Column); + } + } + else + { + Assert.Null(response.Definitions); + } + } + + private async Task TestDecompilationAsync(TestFile testFile, string expectedAssemblyName, string expectedTypeName) + { + using var host = CreateOmniSharpHost(new[] { testFile }, new Dictionary + { + ["RoslynExtensionsOptions:EnableDecompilationSupport"] = "true" + }); + + var response = await GetResponseAsync(new[] { testFile }, wantMetadata: true); + + Assert.NotNull(response.Definitions.Single().MetadataSource); + Assert.Equal(expectedAssemblyName, response.Definitions.Single().MetadataSource.AssemblyName); + Assert.Equal(expectedTypeName, response.Definitions.Single().MetadataSource.TypeName); + } + + private async Task TestGoToMetadataAsync(TestFile testFile, string expectedAssemblyName, string expectedTypeName) + { + var response = await GetResponseAsync(new[] { testFile }, wantMetadata: true); + + Assert.NotNull(response.Definitions.Single().MetadataSource); + Assert.Equal(expectedAssemblyName, response.Definitions.Single().MetadataSource.AssemblyName); + Assert.Equal(expectedTypeName, response.Definitions.Single().MetadataSource.TypeName); + + // We probably shouldn't hard code metadata locations (they could change randomly) + Assert.NotEqual(0, response.Definitions.Single().Location.Range.Start.Line); + Assert.NotEqual(0, response.Definitions.Single().Location.Range.Start.Column); + } + + private async Task GetResponseAsync(TestFile[] testFiles, bool wantMetadata) + { + SharedOmniSharpTestHost.AddFilesToWorkspace(testFiles); + var source = testFiles.Single(tf => tf.Content.HasPosition); + var point = source.Content.GetPointFromPosition(); + + var request = new GotoDefinitionRequest + { + FileName = source.FileName, + Line = point.Line, + Column = point.Offset, + Timeout = 60000, + WantMetadata = wantMetadata + }; + + var requestHandler = GetRequestHandler(SharedOmniSharpTestHost); + return await requestHandler.Handle(request); + } + } +} From 2e971403a05f37546c413e3992ef648e1ab11f2b Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Sat, 29 May 2021 23:42:53 -0700 Subject: [PATCH 2/6] Use V2 definition handler for LSP as well. --- .../Handlers/OmniSharpDefinitionHandler.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpDefinitionHandler.cs b/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpDefinitionHandler.cs index ebe110b289..5395567893 100644 --- a/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpDefinitionHandler.cs +++ b/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpDefinitionHandler.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using OmniSharp.Extensions.JsonRpc; using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Document; using OmniSharp.Extensions.LanguageServer.Protocol.Models; -using OmniSharp.Extensions.LanguageServer.Protocol.Server; -using OmniSharp.Models.GotoDefinition; +using OmniSharp.Models.V2.GotoDefinition; using static OmniSharp.LanguageServerProtocol.Helpers; namespace OmniSharp.LanguageServerProtocol.Handlers @@ -41,16 +41,16 @@ public override async Task Handle(DefinitionParams requ var omnisharpResponse = await _definitionHandler.Handle(omnisharpRequest); - if (string.IsNullOrWhiteSpace(omnisharpResponse.FileName)) + if (omnisharpResponse.Definitions == null) { return new LocationOrLocationLinks(); } - return new LocationOrLocationLinks(new Location() + return new LocationOrLocationLinks(omnisharpResponse.Definitions.Select(definition => new Location() { - Uri = ToUri(omnisharpResponse.FileName), - Range = ToRange((omnisharpResponse.Column, omnisharpResponse.Line)) - }); + Uri = definition.Location.FileName, + Range = ToRange(definition.Location.Range) + })); } protected override DefinitionRegistrationOptions CreateRegistrationOptions(DefinitionCapability capability, ClientCapabilities clientCapabilities) From 0cbd0bac427c7be19a44df0c90d1b7fba3154af6 Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Sun, 30 May 2021 14:14:42 -0700 Subject: [PATCH 3/6] PR feedback, fix up a translation bug in the Cake code. --- .../v2/CodeActions/GetCodeActionsRequest.cs | 11 +++++++++++ .../Models/v2/CodeActions/ICodeActionRequest.cs | 10 ++++++---- .../Models/v2/CodeActions/RunCodeActionRequest.cs | 15 +++++++++++++++ .../Refactoring/V2/BaseCodeActionHandler.cs | 10 ++++------ .../Services/Navigation/GoToDefinitionHelpers.cs | 15 +++++---------- .../Services/Navigation/GotoDefinitionService.cs | 5 +++-- .../Navigation/GotoDefinitionServiceV2.cs | 4 ++-- src/OmniSharp.Roslyn/Extensions/TextExtensions.cs | 10 ++-------- 8 files changed, 48 insertions(+), 32 deletions(-) diff --git a/src/OmniSharp.Abstractions/Models/v2/CodeActions/GetCodeActionsRequest.cs b/src/OmniSharp.Abstractions/Models/v2/CodeActions/GetCodeActionsRequest.cs index b41dbce931..507bb8a913 100644 --- a/src/OmniSharp.Abstractions/Models/v2/CodeActions/GetCodeActionsRequest.cs +++ b/src/OmniSharp.Abstractions/Models/v2/CodeActions/GetCodeActionsRequest.cs @@ -6,5 +6,16 @@ namespace OmniSharp.Models.V2.CodeActions public class GetCodeActionsRequest : Request, ICodeActionRequest { public Range Selection { get; set; } + + public ICodeActionRequest WithSelection(Range newSelection) => new GetCodeActionsRequest + { + Line = this.Line, + Column = this.Column, + Buffer = this.Buffer, + ApplyChangesTogether = this.ApplyChangesTogether, + Changes = this.Changes, + FileName = this.FileName, + Selection = newSelection + }; } } diff --git a/src/OmniSharp.Abstractions/Models/v2/CodeActions/ICodeActionRequest.cs b/src/OmniSharp.Abstractions/Models/v2/CodeActions/ICodeActionRequest.cs index 1ed6769ad9..5757b04a23 100644 --- a/src/OmniSharp.Abstractions/Models/v2/CodeActions/ICodeActionRequest.cs +++ b/src/OmniSharp.Abstractions/Models/v2/CodeActions/ICodeActionRequest.cs @@ -5,11 +5,13 @@ namespace OmniSharp.Models.V2.CodeActions public interface ICodeActionRequest { [JsonConverter(typeof(ZeroBasedIndexConverter))] - int Line { get; } + int Line { get; set; } [JsonConverter(typeof(ZeroBasedIndexConverter))] - int Column { get; } - string Buffer { get; } - string FileName { get; } + int Column { get; set; } + string Buffer { get; set; } + string FileName { get; set; } Range Selection { get; set; } + + ICodeActionRequest WithSelection(Range newSelection); } } diff --git a/src/OmniSharp.Abstractions/Models/v2/CodeActions/RunCodeActionRequest.cs b/src/OmniSharp.Abstractions/Models/v2/CodeActions/RunCodeActionRequest.cs index fb7c4a7f61..c0ab57899c 100644 --- a/src/OmniSharp.Abstractions/Models/v2/CodeActions/RunCodeActionRequest.cs +++ b/src/OmniSharp.Abstractions/Models/v2/CodeActions/RunCodeActionRequest.cs @@ -10,5 +10,20 @@ public class RunCodeActionRequest : Request, ICodeActionRequest public bool WantsTextChanges { get; set; } public bool ApplyTextChanges { get; set; } = true; public bool WantsAllCodeActionOperations { get; set; } + + public ICodeActionRequest WithSelection(Range newSelection) => new RunCodeActionRequest + { + Line = this.Line, + Column = this.Column, + Buffer = this.Buffer, + ApplyChangesTogether = this.ApplyChangesTogether, + Changes = this.Changes, + FileName = this.FileName, + Identifier = this.Identifier, + WantsTextChanges = this.WantsTextChanges, + ApplyTextChanges = this.ApplyTextChanges, + WantsAllCodeActionOperations = this.WantsAllCodeActionOperations, + Selection = newSelection + }; } } diff --git a/src/OmniSharp.Cake/Services/RequestHandlers/Refactoring/V2/BaseCodeActionHandler.cs b/src/OmniSharp.Cake/Services/RequestHandlers/Refactoring/V2/BaseCodeActionHandler.cs index 8a684bd5af..98d59a3468 100644 --- a/src/OmniSharp.Cake/Services/RequestHandlers/Refactoring/V2/BaseCodeActionHandler.cs +++ b/src/OmniSharp.Cake/Services/RequestHandlers/Refactoring/V2/BaseCodeActionHandler.cs @@ -5,7 +5,7 @@ namespace OmniSharp.Cake.Services.RequestHandlers.Refactoring.V2 { public abstract class BaseCodeActionsHandler : CakeRequestHandler - where TRequest : ICodeActionRequest, new() + where TRequest : ICodeActionRequest { protected BaseCodeActionsHandler( OmniSharpWorkspace workspace) @@ -21,14 +21,12 @@ protected override async Task TranslateRequestAsync(TRequest request) var endLine = request.Selection.Start.Line != request.Selection.End.Line ? await LineIndexHelper.TranslateToGenerated(request.FileName, request.Selection.End.Line, Workspace) : startLine; - request = new TRequest() - { - Selection = request.Selection with + request = (TRequest)request.WithSelection( + request.Selection with { Start = request.Selection.Start with { Line = startLine }, End = request.Selection.End with { Line = endLine } - } - }; + }); } return await base.TranslateRequestAsync(request); diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GoToDefinitionHelpers.cs b/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GoToDefinitionHelpers.cs index 80276202d7..f8c005fd0c 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GoToDefinitionHelpers.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GoToDefinitionHelpers.cs @@ -3,18 +3,18 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.FindSymbols; using OmniSharp.Extensions; -using OmniSharp.Options; +using System.Threading; using System.Threading.Tasks; namespace OmniSharp.Roslyn.CSharp.Services.Navigation { internal static class GoToDefinitionHelpers { - internal static async Task GetDefinitionSymbol(Document document, int line, int column) + internal static async Task GetDefinitionSymbol(Document document, int line, int column, CancellationToken cancellationToken) { - var sourceText = await document.GetTextAsync(); + var sourceText = await document.GetTextAsync(cancellationToken); var position = sourceText.GetPositionFromLineAndOffset(line, column); - var symbol = await SymbolFinder.FindSymbolAtPositionAsync(document, position); + var symbol = await SymbolFinder.FindSymbolAtPositionAsync(document, position, cancellationToken); return symbol switch { @@ -30,17 +30,12 @@ internal static class GoToDefinitionHelpers internal static async Task GetMetadataMappedSpan( Document document, ISymbol symbol, - ExternalSourceServiceFactory externalSourceServiceFactory, IExternalSourceService externalSourceService, - OmniSharpOptions options, - int timeout) + CancellationToken cancellationToken) { - - var cancellationToken = externalSourceServiceFactory.CreateCancellationToken(options, timeout); var (metadataDocument, _) = await externalSourceService.GetAndAddExternalSymbolDocument(document.Project, symbol, cancellationToken); if (metadataDocument != null) { - cancellationToken = externalSourceServiceFactory.CreateCancellationToken(options, timeout); var metadataLocation = await externalSourceService.GetExternalSymbolLocation(symbol, metadataDocument, cancellationToken); return metadataLocation.GetMappedLineSpan(); } diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GotoDefinitionService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GotoDefinitionService.cs index f073a8c0da..d005f1acae 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GotoDefinitionService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GotoDefinitionService.cs @@ -30,10 +30,11 @@ public GotoDefinitionService(OmniSharpWorkspace workspace, ExternalSourceService public async Task Handle(GotoDefinitionRequest request) { var externalSourceService = _externalSourceServiceFactory.Create(_omnisharpOptions); + var cancellationToken = _externalSourceServiceFactory.CreateCancellationToken(_omnisharpOptions, request.Timeout); var document = externalSourceService.FindDocumentInCache(request.FileName) ?? _workspace.GetDocument(request.FileName); - var symbol = await GoToDefinitionHelpers.GetDefinitionSymbol(document, request.Line, request.Column); + var symbol = await GoToDefinitionHelpers.GetDefinitionSymbol(document, request.Line, request.Column, cancellationToken); if (symbol == null) { return new GotoDefinitionResponse(); @@ -54,7 +55,7 @@ public async Task Handle(GotoDefinitionRequest request) } else if (location.IsInMetadata && request.WantMetadata) { - var maybeSpan = await GoToDefinitionHelpers.GetMetadataMappedSpan(document, symbol, _externalSourceServiceFactory, externalSourceService, _omnisharpOptions, request.Timeout); + var maybeSpan = await GoToDefinitionHelpers.GetMetadataMappedSpan(document, symbol, externalSourceService, cancellationToken); if (maybeSpan is FileLinePositionSpan lineSpan) { diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GotoDefinitionServiceV2.cs b/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GotoDefinitionServiceV2.cs index 822afdcf74..5673900fb7 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GotoDefinitionServiceV2.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GotoDefinitionServiceV2.cs @@ -38,7 +38,7 @@ public async Task Handle(GotoDefinitionRequest request) return new GotoDefinitionResponse(); } - var symbol = await GoToDefinitionHelpers.GetDefinitionSymbol(document, request.Line, request.Column); + var symbol = await GoToDefinitionHelpers.GetDefinitionSymbol(document, request.Line, request.Column, cancellationToken); if (symbol?.Locations.IsDefaultOrEmpty != false) { return new GotoDefinitionResponse(); @@ -55,7 +55,7 @@ public async Task Handle(GotoDefinitionRequest request) } else { - var maybeSpan = await GoToDefinitionHelpers.GetMetadataMappedSpan(document, symbol, _externalSourceServiceFactory, externalSourceService, _omnisharpOptions, request.Timeout); + var maybeSpan = await GoToDefinitionHelpers.GetMetadataMappedSpan(document, symbol, externalSourceService, cancellationToken); if (maybeSpan is FileLinePositionSpan lineSpan) { diff --git a/src/OmniSharp.Roslyn/Extensions/TextExtensions.cs b/src/OmniSharp.Roslyn/Extensions/TextExtensions.cs index 549fa99b3d..68ef95277d 100644 --- a/src/OmniSharp.Roslyn/Extensions/TextExtensions.cs +++ b/src/OmniSharp.Roslyn/Extensions/TextExtensions.cs @@ -27,12 +27,6 @@ public static Point GetPointFromPosition(this SourceText text, int position) public static int GetPositionFromLineAndOffset(this SourceText text, int lineNumber, int offset) => text.Lines[lineNumber].Start + offset; - /// - /// Converts an OmniSharp to a zero-based position within a . - /// - public static int GetPositionFromPoint(this SourceText text, Point point) - => text.GetPositionFromLineAndOffset(point.Line, point.Column); - /// /// Converts a in a to an OmniSharp . /// @@ -59,8 +53,8 @@ public static Models.V2.Location GetLocationFromFileLinePositionSpan(this FileLi /// public static TextSpan GetSpanFromRange(this SourceText text, Range range) => TextSpan.FromBounds( - start: text.GetPositionFromPoint(range.Start), - end: text.GetPositionFromPoint(range.End)); + start: text.GetPositionFromLineAndOffset(range.Start.Line, range.Start.Column), + end: text.GetPositionFromLineAndOffset(range.End.Line, range.End.Column)); /// /// Converts an OmniSharp to a within a . From 560bad9a1eb72e332dce4b9d93786d0e8cb09b4c Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Sun, 30 May 2021 14:15:55 -0700 Subject: [PATCH 4/6] Remove unnecessary setters. --- .../Models/v2/CodeActions/ICodeActionRequest.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/OmniSharp.Abstractions/Models/v2/CodeActions/ICodeActionRequest.cs b/src/OmniSharp.Abstractions/Models/v2/CodeActions/ICodeActionRequest.cs index 5757b04a23..fe9ba5c90e 100644 --- a/src/OmniSharp.Abstractions/Models/v2/CodeActions/ICodeActionRequest.cs +++ b/src/OmniSharp.Abstractions/Models/v2/CodeActions/ICodeActionRequest.cs @@ -5,12 +5,12 @@ namespace OmniSharp.Models.V2.CodeActions public interface ICodeActionRequest { [JsonConverter(typeof(ZeroBasedIndexConverter))] - int Line { get; set; } + int Line { get; } [JsonConverter(typeof(ZeroBasedIndexConverter))] - int Column { get; set; } - string Buffer { get; set; } - string FileName { get; set; } - Range Selection { get; set; } + int Column { get; } + string Buffer { get; } + string FileName { get; } + Range Selection { get; } ICodeActionRequest WithSelection(Range newSelection); } From 4d2c47bda43a40a2296e72911133a720a4d03279 Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Sun, 30 May 2021 14:48:13 -0700 Subject: [PATCH 5/6] Refactor gotodefinition tests to share most via a base class. --- .../AbstractGoToDefinitionFacts.cs | 586 +++++++++++++++++ .../GoToDefinitionFacts.cs | 589 +----------------- .../GoToDefinitionV2Facts.cs | 587 +---------------- 3 files changed, 620 insertions(+), 1142 deletions(-) create mode 100644 tests/OmniSharp.Roslyn.CSharp.Tests/AbstractGoToDefinitionFacts.cs diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/AbstractGoToDefinitionFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/AbstractGoToDefinitionFacts.cs new file mode 100644 index 0000000000..92b8052f2c --- /dev/null +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/AbstractGoToDefinitionFacts.cs @@ -0,0 +1,586 @@ +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using OmniSharp.Mef; +using OmniSharp.Models.Metadata; +using OmniSharp.Roslyn.CSharp.Services.Navigation; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TestUtility; +using Xunit; +using Xunit.Abstractions; + +namespace OmniSharp.Roslyn.CSharp.Tests +{ + public abstract class AbstractGoToDefinitionFacts : AbstractSingleRequestHandlerTestFixture + where TGotoDefinitionService : IRequestHandler + { + protected AbstractGoToDefinitionFacts(ITestOutputHelper testOutput, SharedOmniSharpHostFixture sharedOmniSharpHostFixture) : base(testOutput, sharedOmniSharpHostFixture) + { + } + + + [Theory] + [InlineData("foo.cs")] + [InlineData("foo.csx")] + public async Task ReturnsDefinitionInSameFile(string filename) + { + var testFile = new TestFile(filename, @" +class {|def:Foo|} { + private F$$oo foo; +}"); + + await TestGoToSourceAsync(testFile); + } + + [Theory] + [InlineData("foo.cs")] + [InlineData("foo.csx")] + public async Task DoesNotReturnOnPropertAccessorGet(string filename) + { + var testFile = new TestFile(filename, @" +class Test { + public string Foo{ g$$et; set; } +}"); + + await TestGoToSourceAsync(testFile); + } + + [Theory] + [InlineData("foo.cs")] + [InlineData("foo.csx")] + public async Task DoesNotReturnOnPropertAccessorSet(string filename) + { + var testFile = new TestFile(filename, @" +class Test { + public int Foo{ get; s$$et; } +}"); + + await TestGoToSourceAsync(testFile); + } + + [Theory] + [InlineData("foo.cs")] + [InlineData("foo.csx")] + public async Task DoesNotReturnOnPropertyAccessorPropertyDef(string filename) + { + var testFile = new TestFile(filename, @" +class Test { + public int |def:Foo| Fo$$o{ get; set; } +}"); + + await TestGoToSourceAsync(testFile); + } + + [Theory] + [InlineData("foo.cs")] + [InlineData("foo.csx")] + public async Task ReturnsOnPropertyAccessorPropertySetting(string filename) + { + var testFile = new TestFile(filename, @" +class Test { + public int |def:Foo|{ get; set; } + + public static void main() + { + F$$oo = 3; + } +}"); + + await TestGoToSourceAsync(testFile); + } + + [Theory] + [InlineData("foo.cs")] + [InlineData("foo.csx")] + public async Task ReturnsOnPropertyAccessorField1(string filename) + { + var testFile = new TestFile(filename, @" +class Test { + + public int |def:foo|; + + public int Foo + { + get => f$$oo; + set => foo = value; + } +}"); + + await TestGoToSourceAsync(testFile); + } + + [Theory] + [InlineData("foo.cs")] + [InlineData("foo.csx")] + public async Task ReturnsOnPropertyAccessorField2(string filename) + { + var testFile = new TestFile(filename, @" +class Test { + + public int |def:foo|; + + public int Foo + { + get => foo; + set => f$$oo = value; + } +}"); + + await TestGoToSourceAsync(testFile); + } + + [Theory] + [InlineData("foo.cs")] + [InlineData("foo.csx")] + public async Task ReturnsOnPropertyAccessorPropertyGetting(string filename) + { + var testFile = new TestFile(filename, @" +class Test { + public int |def:Foo|{ get; set; } + + public static void main() + { + Foo = 3; + Console.WriteLine(F$$oo); + } +}"); + + await TestGoToSourceAsync(testFile); + } + + + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsPartialMethodDefinitionWithBody(string filename) + { + var testFile = new TestFile(filename, @" + public partial class MyClass + { + public MyClass() + { + Met$$hod(); + } + + partial void {|def:Method|}() + { + //do stuff + } + } + + public partial class MyClass + { + partial void Method(); + }"); + + await TestGoToSourceAsync(testFile); + } + + [Fact] + public async Task ReturnsDefinitionInDifferentFile() + { + var testFile1 = new TestFile("foo.cs", @" +using System; +class {|def:Foo|} { +}"); + var testFile2 = new TestFile("bar.cs", @" +class Bar { + private F$$oo foo; +}"); + + await TestGoToSourceAsync(testFile1, testFile2); + } + + [Fact] + public async Task ReturnsEmptyResultWhenDefinitionIsNotFound() + { + var testFile1 = new TestFile("foo.cs", @" +using System; +class Foo { +}"); + var testFile2 = new TestFile("bar.cs", @" +class Bar { + private B$$az foo; +}"); + + await TestGoToSourceAsync(testFile1, testFile2); + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsDefinitionInMetadata_WhenSymbolIsStaticMethod(string filename) + { + var testFile = new TestFile(filename, @" +using System; +class Bar { + public void Baz() { + Guid.NewG$$uid(); + } +}"); + + await TestGoToMetadataAsync(testFile, + expectedAssemblyName: AssemblyHelpers.CorLibName, + expectedTypeName: "System.Guid"); + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsDecompiledDefinition_WhenSymbolIsStaticMethod(string filename) + { + var testFile = new TestFile(filename, @" +using System; +class Bar { + public void Baz() { + Guid.NewG$$uid(); + } +}"); + + await TestDecompilationAsync(testFile, + expectedAssemblyName: AssemblyHelpers.CorLibName, + expectedTypeName: "System.Guid"); + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsDefinitionInMetadata_WhenSymbolIsInstanceMethod(string filename) + { + var testFile = new TestFile(filename, @" +using System.Collections.Generic; +class Bar { + public void Baz() { + var foo = new List(); + foo.ToAr$$ray(); + } +}"); + + await TestGoToMetadataAsync(testFile, + expectedAssemblyName: AssemblyHelpers.CorLibName, + expectedTypeName: "System.Collections.Generic.List`1"); + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsDecompiledDefinition_WhenSymbolIsInstanceMethod(string filename) + { + var testFile = new TestFile(filename, @" +using System.Collections.Generic; +class Bar { + public void Baz() { + var foo = new List(); + foo.ToAr$$ray(); + } +}"); + + await TestDecompilationAsync(testFile, + expectedAssemblyName: AssemblyHelpers.CorLibName, + expectedTypeName: "System.Collections.Generic.List`1"); + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsDefinitionInMetadata_WhenSymbolIsGenericType(string filename) + { + var testFile = new TestFile(filename, @" +using System.Collections.Generic; +class Bar { + public void Baz() { + var foo = new Li$$st(); + foo.ToArray(); + } +}"); + + await TestGoToMetadataAsync(testFile, + expectedAssemblyName: AssemblyHelpers.CorLibName, + expectedTypeName: "System.Collections.Generic.List`1"); + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsDecompiledDefinition_WhenSymbolIsGenericType(string filename) + { + var testFile = new TestFile(filename, @" +using System.Collections.Generic; +class Bar { + public void Baz() { + var foo = new Li$$st(); + foo.ToArray(); + } +}"); + + await TestDecompilationAsync(testFile, + expectedAssemblyName: AssemblyHelpers.CorLibName, + expectedTypeName: "System.Collections.Generic.List`1"); + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsDefinitionInMetadata_WhenSymbolIsType(string filename) + { + var testFile = new TestFile(filename, @" +using System; +class Bar { + public void Baz() { + var str = Stri$$ng.Empty; + } +}"); + + await TestGoToMetadataAsync(testFile, + expectedAssemblyName: AssemblyHelpers.CorLibName, + expectedTypeName: "System.String"); + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsDecompiledDefinition_WhenSymbolIsType(string filename) + { + var testFile = new TestFile(filename, @" +using System; +class Bar { + public void Baz() { + var str = Stri$$ng.Empty; + } +}"); + + await TestDecompilationAsync(testFile, + expectedAssemblyName: AssemblyHelpers.CorLibName, + expectedTypeName: "System.String"); + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsDefinitionInMetadata_FromMetadata_WhenSymbolIsType(string filename) + { + var testFile = new TestFile(filename, @" +using System; +class Bar { + public void Baz() { + var number = in$$t.MaxValue; + } +}"); + + using (var host = CreateOmniSharpHost(testFile)) + { + var point = testFile.Content.GetPointFromPosition(); + + // 1. start by asking for definition of "int" + var gotoDefinitionRequest = CreateRequest(testFile.FileName, point.Line, point.Offset, wantMetadata: true, timeout: 60000); + var gotoDefinitionRequestHandler = GetRequestHandler(host); + var gotoDefinitionResponse = await gotoDefinitionRequestHandler.Handle(gotoDefinitionRequest); + var gotoDefinitionResponseMetadataSource = GetMetadataSource(gotoDefinitionResponse); + + // 2. now, based on the response information + // go to the metadata endpoint, and ask for "int" specific metadata + var metadataRequest = new MetadataRequest + { + AssemblyName = gotoDefinitionResponseMetadataSource.AssemblyName, + TypeName = gotoDefinitionResponseMetadataSource.TypeName, + ProjectName = gotoDefinitionResponseMetadataSource.ProjectName, + Language = gotoDefinitionResponseMetadataSource.Language + }; + var metadataRequestHandler = host.GetRequestHandler(OmniSharpEndpoints.Metadata); + var metadataResponse = await metadataRequestHandler.Handle(metadataRequest); + + // 3. the metadata response contains SourceName (metadata "file") and SourceText (syntax tree) + // use the source to locate "IComparable" which is an interface implemented by Int32 struct + var metadataTree = CSharpSyntaxTree.ParseText(metadataResponse.Source); + + var iComparable = metadataTree.GetCompilationUnitRoot(). + DescendantNodesAndSelf(). + OfType().First(). + BaseList.Types.FirstOrDefault(x => x.Type.ToString() == "IComparable"); + var relevantLineSpan = iComparable.GetLocation().GetLineSpan(); + + // 4. now ask for the definition of "IComparable" + // pass in the SourceName (metadata "file") as FileName - since it's not a regular file in our workspace + var metadataNavigationRequest = CreateRequest(metadataResponse.SourceName, relevantLineSpan.StartLinePosition.Line, relevantLineSpan.StartLinePosition.Character, wantMetadata: true); + var metadataNavigationResponse = await gotoDefinitionRequestHandler.Handle(metadataNavigationRequest); + var metadataNavigationResponseMetadataSource = GetMetadataSource(metadataNavigationResponse); + var info = GetInfo(metadataNavigationResponse); + + // 5. validate the response to be matching the expected IComparable meta info + Assert.NotNull(metadataNavigationResponseMetadataSource); + Assert.Equal(AssemblyHelpers.CorLibName, metadataNavigationResponseMetadataSource.AssemblyName); + Assert.Equal("System.IComparable", metadataNavigationResponseMetadataSource.TypeName); + + Assert.NotEqual(0, info.Single().Line); + Assert.NotEqual(0, info.Single().Column); + } + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsDecompiledDefinition_FromMetadata_WhenSymbolIsType(string filename) + { + var testFile = new TestFile(filename, @" +using System; +class Bar { + public void Baz() { + var number = in$$t.MaxValue; + } +}"); + + using var host = CreateOmniSharpHost(new[] { testFile }, new Dictionary + { + ["RoslynExtensionsOptions:EnableDecompilationSupport"] = "true" + }); + + var point = testFile.Content.GetPointFromPosition(); + + // 1. start by asking for definition of "int" + var gotoDefinitionRequest = CreateRequest(testFile.FileName, point.Line, point.Offset, wantMetadata: true, timeout: 60000); + var gotoDefinitionRequestHandler = GetRequestHandler(host); + var gotoDefinitionResponse = await gotoDefinitionRequestHandler.Handle(gotoDefinitionRequest); + + // 2. now, based on the response information + // go to the metadata endpoint, and ask for "int" specific decompiled source + var metadataSource = GetMetadataSource(gotoDefinitionResponse); + var metadataRequest = new MetadataRequest + { + AssemblyName = metadataSource!.AssemblyName, + TypeName = metadataSource.TypeName, + ProjectName = metadataSource.ProjectName, + Language = metadataSource.Language, + Timeout = 60000 + }; + var metadataRequestHandler = host.GetRequestHandler(OmniSharpEndpoints.Metadata); + var metadataResponse = await metadataRequestHandler.Handle(metadataRequest); + + // 3. the response contains SourceName ("file") and SourceText (syntax tree) + // use the source to locate "IComparable" which is an interface implemented by Int32 struct + var decompiledTree = CSharpSyntaxTree.ParseText(metadataResponse.Source); + var compilationUnit = decompiledTree.GetCompilationUnitRoot(); + + // second comment should indicate we have decompiled + var comments = compilationUnit.DescendantTrivia().Where(t => t.Kind() == SyntaxKind.SingleLineCommentTrivia).ToArray(); + Assert.NotNull(comments); + Assert.Equal("// Decompiled with ICSharpCode.Decompiler 7.0.0.6488", comments[1].ToString()); + + // contrary to regular metadata, we should have methods with full bodies + // this condition would fail if decompilation wouldn't work + var methods = compilationUnit. + DescendantNodesAndSelf(). + OfType(). + Where(m => m.Body != null); + + Assert.NotEmpty(methods); + + var iComparable = compilationUnit. + DescendantNodesAndSelf(). + OfType().First(). + BaseList.Types.FirstOrDefault(x => x.Type.ToString() == "IComparable"); + var relevantLineSpan = iComparable.GetLocation().GetLineSpan(); + + // 4. now ask for the definition of "IComparable" + // pass in the SourceName (metadata "file") as FileName - since it's not a regular file in our workspace + var metadataNavigationRequest = CreateRequest(metadataResponse.SourceName, relevantLineSpan.StartLinePosition.Line, relevantLineSpan.StartLinePosition.Character, wantMetadata: true); + var metadataNavigationResponse = await gotoDefinitionRequestHandler.Handle(metadataNavigationRequest); + var metadataSourceResponse = GetMetadataSource(metadataNavigationResponse); + var metadataNavigationInfo = GetInfo(metadataNavigationResponse); + + // 5. validate the response to be matching the expected IComparable meta info + Assert.NotNull(metadataSource); + Assert.Equal(AssemblyHelpers.CorLibName, metadataSourceResponse.AssemblyName); + Assert.Equal("System.IComparable", metadataSourceResponse.TypeName); + + Assert.NotEqual(0, metadataNavigationInfo.Single().Line); + Assert.NotEqual(0, metadataNavigationInfo.Single().Column); + } + + [Fact] + public async Task ReturnsNoResultsButDoesNotThrowForNamespaces() + { + var testFile = new TestFile("foo.cs", "namespace F$$oo {}"); + var response = await GetResponseAsync(new[] { testFile }, wantMetadata: false); + Assert.Empty(GetInfo(response)); + } + + protected async Task TestGoToSourceAsync(params TestFile[] testFiles) + { + var response = await GetResponseAsync(testFiles, wantMetadata: false); + + var targets = + from tf in testFiles + from span in tf.Content.GetSpans("def") + select (tf, span); + + var info = GetInfo(response); + + if (targets.Any()) + { + foreach (var (file, definitionSpan) in targets) + { + var definitionRange = file.Content.GetRangeFromSpan(definitionSpan); + + Assert.Contains(info, + def => file.FileName == def.FileName + && definitionRange.Start.Line == def.Line + && definitionRange.Start.Offset == def.Column); + } + } + else + { + Assert.Empty(info); + } + } + + protected async Task TestDecompilationAsync(TestFile testFile, string expectedAssemblyName, string expectedTypeName) + { + using var host = CreateOmniSharpHost(new[] { testFile }, new Dictionary + { + ["RoslynExtensionsOptions:EnableDecompilationSupport"] = "true" + }); + + var response = await GetResponseAsync(new[] { testFile }, wantMetadata: true); + var metadataSource = GetMetadataSource(response); + + Assert.NotNull(metadataSource); + Assert.NotEmpty(GetInfo(response)); + Assert.Equal(expectedAssemblyName, metadataSource.AssemblyName); + Assert.Equal(expectedTypeName, metadataSource.TypeName); + } + + protected async Task TestGoToMetadataAsync(TestFile testFile, string expectedAssemblyName, string expectedTypeName) + { + var response = await GetResponseAsync(new[] { testFile }, wantMetadata: true); + var metadataSource = GetMetadataSource(response); + + var responseInfo = GetInfo(response); + Assert.NotNull(metadataSource); + Assert.NotEmpty(responseInfo); + Assert.Equal(expectedAssemblyName, metadataSource.AssemblyName); + Assert.Equal(expectedTypeName, metadataSource.TypeName); + + // We probably shouldn't hard code metadata locations (they could change randomly) + Assert.NotEqual(0, responseInfo.Single().Line); + Assert.NotEqual(0, responseInfo.Single().Column); + } + + protected async Task GetResponseAsync(TestFile[] testFiles, bool wantMetadata) + { + SharedOmniSharpTestHost.AddFilesToWorkspace(testFiles); + var source = testFiles.Single(tf => tf.Content.HasPosition); + var point = source.Content.GetPointFromPosition(); + + var request = CreateRequest(source.FileName, point.Line, point.Offset, timeout: 60000, wantMetadata: wantMetadata); + + var requestHandler = GetRequestHandler(SharedOmniSharpTestHost); + return await requestHandler.Handle(request); + } + + protected abstract TGotoDefinitionRequest CreateRequest(string fileName, int line, int column, bool wantMetadata, int timeout = 60000); + protected abstract MetadataSource GetMetadataSource(TGotoDefinitionResponse response); + protected abstract IEnumerable<(int Line, int Column, string FileName)> GetInfo(TGotoDefinitionResponse response); + } +} diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/GoToDefinitionFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/GoToDefinitionFacts.cs index 4dd1cc6ccf..2e8c854852 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/GoToDefinitionFacts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/GoToDefinitionFacts.cs @@ -1,19 +1,13 @@ -using System.Linq; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using OmniSharp.Models.GotoDefinition; +using OmniSharp.Models.GotoDefinition; using OmniSharp.Roslyn.CSharp.Services.Navigation; using OmniSharp.Models.Metadata; using TestUtility; -using Xunit; using Xunit.Abstractions; using System.Collections.Generic; -using Microsoft.CodeAnalysis; namespace OmniSharp.Roslyn.CSharp.Tests { - public class GoToDefinitionFacts : AbstractSingleRequestHandlerTestFixture + public class GoToDefinitionFacts : AbstractGoToDefinitionFacts { public GoToDefinitionFacts(ITestOutputHelper output, SharedOmniSharpHostFixture sharedOmniSharpHostFixture) : base(output, sharedOmniSharpHostFixture) @@ -22,580 +16,27 @@ public GoToDefinitionFacts(ITestOutputHelper output, SharedOmniSharpHostFixture protected override string EndpointName => OmniSharpEndpoints.GotoDefinition; - [Theory] - [InlineData("foo.cs")] - [InlineData("foo.csx")] - public async Task ReturnsDefinitionInSameFile(string filename) - { - var testFile = new TestFile(filename, @" -class {|def:Foo|} { - private F$$oo foo; -}"); - - await TestGoToSourceAsync(testFile); - } - - [Theory] - [InlineData("foo.cs")] - [InlineData("foo.csx")] - public async Task DoesNotReturnOnPropertAccessorGet(string filename) - { - var testFile = new TestFile(filename, @" -class Test { - public string Foo{ g$$et; set; } -}"); - - await TestGoToSourceAsync(testFile); - } - - [Theory] - [InlineData("foo.cs")] - [InlineData("foo.csx")] - public async Task DoesNotReturnOnPropertAccessorSet(string filename) - { - var testFile = new TestFile(filename, @" -class Test { - public int Foo{ get; s$$et; } -}"); - - await TestGoToSourceAsync(testFile); - } - - [Theory] - [InlineData("foo.cs")] - [InlineData("foo.csx")] - public async Task DoesNotReturnOnPropertyAccessorPropertyDef(string filename) - { - var testFile = new TestFile(filename, @" -class Test { - public int |def:Foo| Fo$$o{ get; set; } -}"); - - await TestGoToSourceAsync(testFile); - } - - [Theory] - [InlineData("foo.cs")] - [InlineData("foo.csx")] - public async Task ReturnsOnPropertyAccessorPropertySetting(string filename) - { - var testFile = new TestFile(filename, @" -class Test { - public int |def:Foo|{ get; set; } - - public static void main() - { - F$$oo = 3; - } -}"); - - await TestGoToSourceAsync(testFile); - } - - [Theory] - [InlineData("foo.cs")] - [InlineData("foo.csx")] - public async Task ReturnsOnPropertyAccessorField1(string filename) - { - var testFile = new TestFile(filename, @" -class Test { - - public int |def:foo|; - - public int Foo - { - get => f$$oo; - set => foo = value; - } -}"); - - await TestGoToSourceAsync(testFile); - } - - [Theory] - [InlineData("foo.cs")] - [InlineData("foo.csx")] - public async Task ReturnsOnPropertyAccessorField2(string filename) - { - var testFile = new TestFile(filename, @" -class Test { - - public int |def:foo|; - - public int Foo - { - get => foo; - set => f$$oo = value; - } -}"); - - await TestGoToSourceAsync(testFile); - } - - [Theory] - [InlineData("foo.cs")] - [InlineData("foo.csx")] - public async Task ReturnsOnPropertyAccessorPropertyGetting(string filename) - { - var testFile = new TestFile(filename, @" -class Test { - public int |def:Foo|{ get; set; } - - public static void main() - { - Foo = 3; - Console.WriteLine(F$$oo); - } -}"); - - await TestGoToSourceAsync(testFile); - } - - - - [Theory] - [InlineData("bar.cs")] - [InlineData("bar.csx")] - public async Task ReturnsPartialMethodDefinitionWithBody(string filename) - { - var testFile = new TestFile(filename, @" - public partial class MyClass - { - public MyClass() - { - Met$$hod(); - } - - partial void {|def:Method|}() - { - //do stuff - } - } - - public partial class MyClass - { - partial void {|def:Method|}(); - }"); - - await TestGoToSourceAsync(testFile); - } - - [Fact] - public async Task ReturnsDefinitionInDifferentFile() - { - var testFile1 = new TestFile("foo.cs", @" -using System; -class {|def:Foo|} { -}"); - var testFile2 = new TestFile("bar.cs", @" -class Bar { - private F$$oo foo; -}"); - - await TestGoToSourceAsync(testFile1, testFile2); - } - - [Fact] - public async Task ReturnsEmptyResultWhenDefinitionIsNotFound() - { - var testFile1 = new TestFile("foo.cs", @" -using System; -class Foo { -}"); - var testFile2 = new TestFile("bar.cs", @" -class Bar { - private B$$az foo; -}"); - - await TestGoToSourceAsync(testFile1, testFile2); - } - - [Theory] - [InlineData("bar.cs")] - [InlineData("bar.csx")] - public async Task ReturnsDefinitionInMetadata_WhenSymbolIsStaticMethod(string filename) - { - var testFile = new TestFile(filename, @" -using System; -class Bar { - public void Baz() { - Guid.NewG$$uid(); - } -}"); - - await TestGoToMetadataAsync(testFile, - expectedAssemblyName: AssemblyHelpers.CorLibName, - expectedTypeName: "System.Guid"); - } - - [Theory] - [InlineData("bar.cs")] - [InlineData("bar.csx")] - public async Task ReturnsDecompiledDefinition_WhenSymbolIsStaticMethod(string filename) - { - var testFile = new TestFile(filename, @" -using System; -class Bar { - public void Baz() { - Guid.NewG$$uid(); - } -}"); - - await TestDecompilationAsync(testFile, - expectedAssemblyName: AssemblyHelpers.CorLibName, - expectedTypeName: "System.Guid"); - } - - [Theory] - [InlineData("bar.cs")] - [InlineData("bar.csx")] - public async Task ReturnsDefinitionInMetadata_WhenSymbolIsInstanceMethod(string filename) - { - var testFile = new TestFile(filename, @" -using System.Collections.Generic; -class Bar { - public void Baz() { - var foo = new List(); - foo.ToAr$$ray(); - } -}"); - - await TestGoToMetadataAsync(testFile, - expectedAssemblyName: AssemblyHelpers.CorLibName, - expectedTypeName: "System.Collections.Generic.List`1"); - } - - [Theory] - [InlineData("bar.cs")] - [InlineData("bar.csx")] - public async Task ReturnsDecompiledDefinition_WhenSymbolIsInstanceMethod(string filename) - { - var testFile = new TestFile(filename, @" -using System.Collections.Generic; -class Bar { - public void Baz() { - var foo = new List(); - foo.ToAr$$ray(); - } -}"); - - await TestDecompilationAsync(testFile, - expectedAssemblyName: AssemblyHelpers.CorLibName, - expectedTypeName: "System.Collections.Generic.List`1"); - } - - [Theory] - [InlineData("bar.cs")] - [InlineData("bar.csx")] - public async Task ReturnsDefinitionInMetadata_WhenSymbolIsGenericType(string filename) - { - var testFile = new TestFile(filename, @" -using System.Collections.Generic; -class Bar { - public void Baz() { - var foo = new Li$$st(); - foo.ToArray(); - } -}"); - - await TestGoToMetadataAsync(testFile, - expectedAssemblyName: AssemblyHelpers.CorLibName, - expectedTypeName: "System.Collections.Generic.List`1"); - } - - [Theory] - [InlineData("bar.cs")] - [InlineData("bar.csx")] - public async Task ReturnsDecompiledDefinition_WhenSymbolIsGenericType(string filename) - { - var testFile = new TestFile(filename, @" -using System.Collections.Generic; -class Bar { - public void Baz() { - var foo = new Li$$st(); - foo.ToArray(); - } -}"); - - await TestDecompilationAsync(testFile, - expectedAssemblyName: AssemblyHelpers.CorLibName, - expectedTypeName: "System.Collections.Generic.List`1"); - } - - [Theory] - [InlineData("bar.cs")] - [InlineData("bar.csx")] - public async Task ReturnsDefinitionInMetadata_WhenSymbolIsType(string filename) - { - var testFile = new TestFile(filename, @" -using System; -class Bar { - public void Baz() { - var str = Stri$$ng.Empty; - } -}"); - - await TestGoToMetadataAsync(testFile, - expectedAssemblyName: AssemblyHelpers.CorLibName, - expectedTypeName: "System.String"); - } - - [Theory] - [InlineData("bar.cs")] - [InlineData("bar.csx")] - public async Task ReturnsDecompiledDefinition_WhenSymbolIsType(string filename) - { - var testFile = new TestFile(filename, @" -using System; -class Bar { - public void Baz() { - var str = Stri$$ng.Empty; - } -}"); - - await TestDecompilationAsync(testFile, - expectedAssemblyName: AssemblyHelpers.CorLibName, - expectedTypeName: "System.String"); - } - - [Theory] - [InlineData("bar.cs")] - [InlineData("bar.csx")] - public async Task ReturnsDefinitionInMetadata_FromMetadata_WhenSymbolIsType(string filename) - { - var testFile = new TestFile(filename, @" -using System; -class Bar { - public void Baz() { - var number = in$$t.MaxValue; - } -}"); - - using (var host = CreateOmniSharpHost(testFile)) - { - var point = testFile.Content.GetPointFromPosition(); - - // 1. start by asking for definition of "int" - var gotoDefinitionRequest = new GotoDefinitionRequest - { - FileName = testFile.FileName, - Line = point.Line, - Column = point.Offset, - WantMetadata = true, - Timeout = 60000 - }; - var gotoDefinitionRequestHandler = GetRequestHandler(host); - var gotoDefinitionResponse = await gotoDefinitionRequestHandler.Handle(gotoDefinitionRequest); - - // 2. now, based on the response information - // go to the metadata endpoint, and ask for "int" specific metadata - var metadataRequest = new MetadataRequest - { - AssemblyName = gotoDefinitionResponse.MetadataSource.AssemblyName, - TypeName = gotoDefinitionResponse.MetadataSource.TypeName, - ProjectName = gotoDefinitionResponse.MetadataSource.ProjectName, - Language = gotoDefinitionResponse.MetadataSource.Language - }; - var metadataRequestHandler = host.GetRequestHandler(OmniSharpEndpoints.Metadata); - var metadataResponse = await metadataRequestHandler.Handle(metadataRequest); - - // 3. the metadata response contains SourceName (metadata "file") and SourceText (syntax tree) - // use the source to locate "IComparable" which is an interface implemented by Int32 struct - var metadataTree = CSharpSyntaxTree.ParseText(metadataResponse.Source); - - var iComparable = metadataTree.GetCompilationUnitRoot(). - DescendantNodesAndSelf(). - OfType().First(). - BaseList.Types.FirstOrDefault(x => x.Type.ToString() == "IComparable"); - var relevantLineSpan = iComparable.GetLocation().GetLineSpan(); - - // 4. now ask for the definition of "IComparable" - // pass in the SourceName (metadata "file") as FileName - since it's not a regular file in our workspace - var metadataNavigationRequest = new GotoDefinitionRequest - { - FileName = metadataResponse.SourceName, - Line = relevantLineSpan.StartLinePosition.Line, - Column = relevantLineSpan.StartLinePosition.Character, - WantMetadata = true - }; - var metadataNavigationResponse = await gotoDefinitionRequestHandler.Handle(metadataNavigationRequest); - - // 5. validate the response to be matching the expected IComparable meta info - Assert.NotNull(metadataNavigationResponse.MetadataSource); - Assert.Equal(AssemblyHelpers.CorLibName, metadataNavigationResponse.MetadataSource.AssemblyName); - Assert.Equal("System.IComparable", metadataNavigationResponse.MetadataSource.TypeName); - - Assert.NotEqual(0, metadataNavigationResponse.Line); - Assert.NotEqual(0, metadataNavigationResponse.Column); - } - } - - [Theory] - [InlineData("bar.cs")] - [InlineData("bar.csx")] - public async Task ReturnsDecompiledDefinition_FromMetadata_WhenSymbolIsType(string filename) - { - var testFile = new TestFile(filename, @" -using System; -class Bar { - public void Baz() { - var number = in$$t.MaxValue; - } -}"); - - using var host = CreateOmniSharpHost(new[] { testFile }, new Dictionary - { - ["RoslynExtensionsOptions:EnableDecompilationSupport"] = "true" - }); - - var point = testFile.Content.GetPointFromPosition(); - - // 1. start by asking for definition of "int" - var gotoDefinitionRequest = new GotoDefinitionRequest + protected override GotoDefinitionRequest CreateRequest(string fileName, int line, int column, bool wantMetadata, int timeout = 60000) + => new GotoDefinitionRequest { - FileName = testFile.FileName, - Line = point.Line, - Column = point.Offset, - WantMetadata = true, - Timeout = 60000 + FileName = fileName, + Line = line, + Column = column, + WantMetadata = wantMetadata, + Timeout = timeout }; - var gotoDefinitionRequestHandler = GetRequestHandler(host); - var gotoDefinitionResponse = await gotoDefinitionRequestHandler.Handle(gotoDefinitionRequest); - - // 2. now, based on the response information - // go to the metadata endpoint, and ask for "int" specific decompiled source - var metadataRequest = new MetadataRequest - { - AssemblyName = gotoDefinitionResponse.MetadataSource.AssemblyName, - TypeName = gotoDefinitionResponse.MetadataSource.TypeName, - ProjectName = gotoDefinitionResponse.MetadataSource.ProjectName, - Language = gotoDefinitionResponse.MetadataSource.Language, - Timeout = 60000 - }; - var metadataRequestHandler = host.GetRequestHandler(OmniSharpEndpoints.Metadata); - var metadataResponse = await metadataRequestHandler.Handle(metadataRequest); - - // 3. the response contains SourceName ("file") and SourceText (syntax tree) - // use the source to locate "IComparable" which is an interface implemented by Int32 struct - var decompiledTree = CSharpSyntaxTree.ParseText(metadataResponse.Source); - var compilationUnit = decompiledTree.GetCompilationUnitRoot(); - - // second comment should indicate we have decompiled - var comments = compilationUnit.DescendantTrivia().Where(t => t.Kind() == SyntaxKind.SingleLineCommentTrivia).ToArray(); - Assert.NotNull(comments); - Assert.Equal("// Decompiled with ICSharpCode.Decompiler 7.0.0.6488", comments[1].ToString()); - - // contrary to regular metadata, we should have methods with full bodies - // this condition would fail if decompilation wouldn't work - var methods = compilationUnit. - DescendantNodesAndSelf(). - OfType(). - Where(m => m.Body != null); - - Assert.NotEmpty(methods); - - var iComparable = compilationUnit. - DescendantNodesAndSelf(). - OfType().First(). - BaseList.Types.FirstOrDefault(x => x.Type.ToString() == "IComparable"); - var relevantLineSpan = iComparable.GetLocation().GetLineSpan(); - - // 4. now ask for the definition of "IComparable" - // pass in the SourceName (metadata "file") as FileName - since it's not a regular file in our workspace - var metadataNavigationRequest = new GotoDefinitionRequest - { - FileName = metadataResponse.SourceName, - Line = relevantLineSpan.StartLinePosition.Line, - Column = relevantLineSpan.StartLinePosition.Character, - WantMetadata = true - }; - var metadataNavigationResponse = await gotoDefinitionRequestHandler.Handle(metadataNavigationRequest); - - // 5. validate the response to be matching the expected IComparable meta info - Assert.NotNull(metadataNavigationResponse.MetadataSource); - Assert.Equal(AssemblyHelpers.CorLibName, metadataNavigationResponse.MetadataSource.AssemblyName); - Assert.Equal("System.IComparable", metadataNavigationResponse.MetadataSource.TypeName); - - Assert.NotEqual(0, metadataNavigationResponse.Line); - Assert.NotEqual(0, metadataNavigationResponse.Column); - } - - [Fact] - public async Task ReturnsNoResultsButDoesNotThrowForNamespaces() - { - var testFile = new TestFile("foo.cs", "namespace F$$oo {}"); - var response = await GetResponseAsync(new[] { testFile }, wantMetadata: false); - Assert.Null(response.FileName); - } - private async Task TestGoToSourceAsync(params TestFile[] testFiles) + protected override IEnumerable<(int Line, int Column, string FileName)> GetInfo(GotoDefinitionResponse response) { - var response = await GetResponseAsync(testFiles, wantMetadata: false); - - var target = testFiles.FirstOrDefault(tf => tf.Content.GetSpans("def").Count > 0); - if (target != null) - { - var definitionSpan = target.Content.GetSpans("def").First(); - var definitionRange = target.Content.GetRangeFromSpan(definitionSpan); - - Assert.Equal(target.FileName, response.FileName); - Assert.Equal(definitionRange.Start.Line, response.Line); - Assert.Equal(definitionRange.Start.Offset, response.Column); - } - else + if (response.IsEmpty) { - Assert.Null(response.FileName); - Assert.Equal(0, response.Line); - Assert.Equal(0, response.Column); + yield break; } - } - private async Task TestDecompilationAsync(TestFile testFile, string expectedAssemblyName, string expectedTypeName) - { - using var host = CreateOmniSharpHost(new[] { testFile }, new Dictionary - { - ["RoslynExtensionsOptions:EnableDecompilationSupport"] = "true" - }); - - var response = await GetResponseAsync(new[] { testFile }, wantMetadata: true); - - Assert.NotNull(response.MetadataSource); - Assert.False(response.IsEmpty); - Assert.Equal(expectedAssemblyName, response.MetadataSource.AssemblyName); - Assert.Equal(expectedTypeName, response.MetadataSource.TypeName); + yield return (response.Line, response.Column, response.FileName); } - private async Task TestGoToMetadataAsync(TestFile testFile, string expectedAssemblyName, string expectedTypeName) - { - var response = await GetResponseAsync(new[] { testFile }, wantMetadata: true); - - Assert.NotNull(response.MetadataSource); - Assert.False(response.IsEmpty); - Assert.Equal(expectedAssemblyName, response.MetadataSource.AssemblyName); - Assert.Equal(expectedTypeName, response.MetadataSource.TypeName); - - // We probably shouldn't hard code metadata locations (they could change randomly) - Assert.NotEqual(0, response.Line); - Assert.NotEqual(0, response.Column); - } - - private async Task GetResponseAsync(TestFile[] testFiles, bool wantMetadata) - { - SharedOmniSharpTestHost.AddFilesToWorkspace(testFiles); - var source = testFiles.Single(tf => tf.Content.HasPosition); - var point = source.Content.GetPointFromPosition(); - - var request = new GotoDefinitionRequest - { - FileName = source.FileName, - Line = point.Line, - Column = point.Offset, - Timeout = 60000, - WantMetadata = wantMetadata - }; - - var requestHandler = GetRequestHandler(SharedOmniSharpTestHost); - return await requestHandler.Handle(request); - } + protected override MetadataSource GetMetadataSource(GotoDefinitionResponse response) + => response.MetadataSource; } } diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/GoToDefinitionV2Facts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/GoToDefinitionV2Facts.cs index 10bbc5d64a..c70ad4f8ce 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/GoToDefinitionV2Facts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/GoToDefinitionV2Facts.cs @@ -1,7 +1,4 @@ -using System.Linq; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Threading.Tasks; using OmniSharp.Roslyn.CSharp.Services.Navigation; using OmniSharp.Models.Metadata; using TestUtility; @@ -12,7 +9,7 @@ namespace OmniSharp.Roslyn.CSharp.Tests { - public class GoToDefinitionV2Facts : AbstractSingleRequestHandlerTestFixture + public class GoToDefinitionV2Facts : AbstractGoToDefinitionFacts { public GoToDefinitionV2Facts(ITestOutputHelper output, SharedOmniSharpHostFixture sharedOmniSharpHostFixture) : base(output, sharedOmniSharpHostFixture) @@ -21,162 +18,6 @@ public GoToDefinitionV2Facts(ITestOutputHelper output, SharedOmniSharpHostFixtur protected override string EndpointName => OmniSharpEndpoints.V2.GotoDefinition; - [Theory] - [InlineData("foo.cs")] - [InlineData("foo.csx")] - public async Task ReturnsDefinitionInSameFile(string filename) - { - var testFile = new TestFile(filename, @" -class {|def:Foo|} { - private F$$oo foo; -}"); - - await TestGoToSourceAsync(testFile); - } - - [Theory] - [InlineData("foo.cs")] - [InlineData("foo.csx")] - public async Task ReturnsOnPropertAccessorGet(string filename) - { - var testFile = new TestFile(filename, @" -class Test { - public string Foo{ g$$et; set; } -}"); - - await TestGoToSourceAsync(testFile); - } - - [Theory] - [InlineData("foo.cs")] - [InlineData("foo.csx")] - public async Task ReturnOnPropertAccessorSet(string filename) - { - var testFile = new TestFile(filename, @" -class Test { - public int Foo{ get; s$$et; } -}"); - - await TestGoToSourceAsync(testFile); - } - - [Theory] - [InlineData("foo.cs")] - [InlineData("foo.csx")] - public async Task ReturnOnPropertyAccessorPropertyDef(string filename) - { - var testFile = new TestFile(filename, @" -class Test { - public int {|def:Fo$$o|} { get; set; } -}"); - - await TestGoToSourceAsync(testFile); - } - - [Theory] - [InlineData("foo.cs")] - [InlineData("foo.csx")] - public async Task ReturnsOnPropertyAccessorPropertySetting(string filename) - { - var testFile = new TestFile(filename, @" -class Test { - public int {|def:Foo|} { get; set; } - - public static void main() - { - F$$oo = 3; - } -}"); - - await TestGoToSourceAsync(testFile); - } - - [Theory] - [InlineData("foo.cs")] - [InlineData("foo.csx")] - public async Task ReturnsOnPropertyAccessorField1(string filename) - { - var testFile = new TestFile(filename, @" -class Test { - - public int {|def:foo|}; - - public int Foo - { - get => f$$oo; - set => foo = value; - } -}"); - - await TestGoToSourceAsync(testFile); - } - - [Theory] - [InlineData("foo.cs")] - [InlineData("foo.csx")] - public async Task ReturnsOnPropertyAccessorField2(string filename) - { - var testFile = new TestFile(filename, @" -class Test { - - public int {|def:foo|}; - - public int Foo - { - get => foo; - set => f$$oo = value; - } -}"); - - await TestGoToSourceAsync(testFile); - } - - [Theory] - [InlineData("foo.cs")] - [InlineData("foo.csx")] - public async Task ReturnsOnPropertyAccessorPropertyGetting(string filename) - { - var testFile = new TestFile(filename, @" -class Test { - public int {|def:Foo|} { get; set; } - - public static void main() - { - Foo = 3; - Console.WriteLine(F$$oo); - } -}"); - - await TestGoToSourceAsync(testFile); - } - - [Theory] - [InlineData("bar.cs")] - [InlineData("bar.csx")] - public async Task ReturnsPartialMethodDefinitionWithBody(string filename) - { - var testFile = new TestFile(filename, @" - public partial class MyClass - { - public MyClass() - { - Met$$hod(); - } - - partial void {|def:Method|}() - { - //do stuff - } - } - - public partial class MyClass - { - partial void Method(); - }"); - - await TestGoToSourceAsync(testFile); - } - [Theory] [InlineData("bar.cs")] [InlineData("bar.csx")] @@ -212,425 +53,35 @@ partial class {|def:Class|} await TestGoToSourceAsync(testFile1, testFile2); } - [Fact] - public async Task ReturnsDefinitionInDifferentFile() - { - var testFile1 = new TestFile("foo.cs", @" -using System; -class {|def:Foo|} { -}"); - var testFile2 = new TestFile("bar.cs", @" -class Bar { - private F$$oo foo; -}"); - - await TestGoToSourceAsync(testFile1, testFile2); - } - - [Fact] - public async Task ReturnsEmptyResultWhenDefinitionIsNotFound() - { - var testFile1 = new TestFile("foo.cs", @" -using System; -class Foo { -}"); - var testFile2 = new TestFile("bar.cs", @" -class Bar { - private B$$az foo; -}"); - - await TestGoToSourceAsync(testFile1, testFile2); - } - - [Theory] - [InlineData("bar.cs")] - [InlineData("bar.csx")] - public async Task ReturnsDefinitionInMetadata_WhenSymbolIsStaticMethod(string filename) - { - var testFile = new TestFile(filename, @" -using System; -class Bar { - public void Baz() { - Guid.NewG$$uid(); - } -}"); - - await TestGoToMetadataAsync(testFile, - expectedAssemblyName: AssemblyHelpers.CorLibName, - expectedTypeName: "System.Guid"); - } - - [Theory] - [InlineData("bar.cs")] - [InlineData("bar.csx")] - public async Task ReturnsDecompiledDefinition_WhenSymbolIsStaticMethod(string filename) - { - var testFile = new TestFile(filename, @" -using System; -class Bar { - public void Baz() { - Guid.NewG$$uid(); - } -}"); - - await TestDecompilationAsync(testFile, - expectedAssemblyName: AssemblyHelpers.CorLibName, - expectedTypeName: "System.Guid"); - } - - [Theory] - [InlineData("bar.cs")] - [InlineData("bar.csx")] - public async Task ReturnsDefinitionInMetadata_WhenSymbolIsInstanceMethod(string filename) - { - var testFile = new TestFile(filename, @" -using System.Collections.Generic; -class Bar { - public void Baz() { - var foo = new List(); - foo.ToAr$$ray(); - } -}"); - - await TestGoToMetadataAsync(testFile, - expectedAssemblyName: AssemblyHelpers.CorLibName, - expectedTypeName: "System.Collections.Generic.List`1"); - } - - [Theory] - [InlineData("bar.cs")] - [InlineData("bar.csx")] - public async Task ReturnsDecompiledDefinition_WhenSymbolIsInstanceMethod(string filename) - { - var testFile = new TestFile(filename, @" -using System.Collections.Generic; -class Bar { - public void Baz() { - var foo = new List(); - foo.ToAr$$ray(); - } -}"); - - await TestDecompilationAsync(testFile, - expectedAssemblyName: AssemblyHelpers.CorLibName, - expectedTypeName: "System.Collections.Generic.List`1"); - } - - [Theory] - [InlineData("bar.cs")] - [InlineData("bar.csx")] - public async Task ReturnsDefinitionInMetadata_WhenSymbolIsGenericType(string filename) - { - var testFile = new TestFile(filename, @" -using System.Collections.Generic; -class Bar { - public void Baz() { - var foo = new Li$$st(); - foo.ToArray(); - } -}"); - - await TestGoToMetadataAsync(testFile, - expectedAssemblyName: AssemblyHelpers.CorLibName, - expectedTypeName: "System.Collections.Generic.List`1"); - } - - [Theory] - [InlineData("bar.cs")] - [InlineData("bar.csx")] - public async Task ReturnsDecompiledDefinition_WhenSymbolIsGenericType(string filename) - { - var testFile = new TestFile(filename, @" -using System.Collections.Generic; -class Bar { - public void Baz() { - var foo = new Li$$st(); - foo.ToArray(); - } -}"); - - await TestDecompilationAsync(testFile, - expectedAssemblyName: AssemblyHelpers.CorLibName, - expectedTypeName: "System.Collections.Generic.List`1"); - } - - [Theory] - [InlineData("bar.cs")] - [InlineData("bar.csx")] - public async Task ReturnsDefinitionInMetadata_WhenSymbolIsType(string filename) - { - var testFile = new TestFile(filename, @" -using System; -class Bar { - public void Baz() { - var str = Stri$$ng.Empty; - } -}"); - - await TestGoToMetadataAsync(testFile, - expectedAssemblyName: AssemblyHelpers.CorLibName, - expectedTypeName: "System.String"); - } - - [Theory] - [InlineData("bar.cs")] - [InlineData("bar.csx")] - public async Task ReturnsDecompiledDefinition_WhenSymbolIsType(string filename) - { - var testFile = new TestFile(filename, @" -using System; -class Bar { - public void Baz() { - var str = Stri$$ng.Empty; - } -}"); - - await TestDecompilationAsync(testFile, - expectedAssemblyName: AssemblyHelpers.CorLibName, - expectedTypeName: "System.String"); - } - - [Theory] - [InlineData("bar.cs")] - [InlineData("bar.csx")] - public async Task ReturnsDefinitionInMetadata_FromMetadata_WhenSymbolIsType(string filename) - { - var testFile = new TestFile(filename, @" -using System; -class Bar { - public void Baz() { - var number = in$$t.MaxValue; - } -}"); - - using (var host = CreateOmniSharpHost(testFile)) + protected override GotoDefinitionRequest CreateRequest(string fileName, int line, int column, bool wantMetadata, int timeout = 60000) + => new GotoDefinitionRequest { - var point = testFile.Content.GetPointFromPosition(); - - // 1. start by asking for definition of "int" - var gotoDefinitionRequest = new GotoDefinitionRequest - { - FileName = testFile.FileName, - Line = point.Line, - Column = point.Offset, - WantMetadata = true, - Timeout = 60000 - }; - var gotoDefinitionRequestHandler = GetRequestHandler(host); - var gotoDefinitionResponse = await gotoDefinitionRequestHandler.Handle(gotoDefinitionRequest); - - // 2. now, based on the response information - // go to the metadata endpoint, and ask for "int" specific metadata - var metadataRequest = new MetadataRequest - { - AssemblyName = gotoDefinitionResponse.Definitions.Single().MetadataSource.AssemblyName, - TypeName = gotoDefinitionResponse.Definitions.Single().MetadataSource.TypeName, - ProjectName = gotoDefinitionResponse.Definitions.Single().MetadataSource.ProjectName, - Language = gotoDefinitionResponse.Definitions.Single().MetadataSource.Language - }; - var metadataRequestHandler = host.GetRequestHandler(OmniSharpEndpoints.Metadata); - var metadataResponse = await metadataRequestHandler.Handle(metadataRequest); - - // 3. the metadata response contains SourceName (metadata "file") and SourceText (syntax tree) - // use the source to locate "IComparable" which is an interface implemented by Int32 struct - var metadataTree = CSharpSyntaxTree.ParseText(metadataResponse.Source); - - var iComparable = metadataTree.GetCompilationUnitRoot(). - DescendantNodesAndSelf(). - OfType().First(). - BaseList.Types.FirstOrDefault(x => x.Type.ToString() == "IComparable"); - var relevantLineSpan = iComparable.GetLocation().GetLineSpan(); - - // 4. now ask for the definition of "IComparable" - // pass in the SourceName (metadata "file") as FileName - since it's not a regular file in our workspace - var metadataNavigationRequest = new GotoDefinitionRequest - { - FileName = metadataResponse.SourceName, - Line = relevantLineSpan.StartLinePosition.Line, - Column = relevantLineSpan.StartLinePosition.Character, - WantMetadata = true - }; - var metadataNavigationResponse = await gotoDefinitionRequestHandler.Handle(metadataNavigationRequest); - - // 5. validate the response to be matching the expected IComparable meta info - Assert.NotNull(metadataNavigationResponse.Definitions.Single().MetadataSource); - Assert.Equal(AssemblyHelpers.CorLibName, metadataNavigationResponse.Definitions.Single().MetadataSource.AssemblyName); - Assert.Equal("System.IComparable", metadataNavigationResponse.Definitions.Single().MetadataSource.TypeName); - - Assert.NotEqual(0, metadataNavigationResponse.Definitions.Single().Location.Range.Start.Line); - Assert.NotEqual(0, metadataNavigationResponse.Definitions.Single().Location.Range.Start.Column); - } - } - - [Theory] - [InlineData("bar.cs")] - [InlineData("bar.csx")] - public async Task ReturnsDecompiledDefinition_FromMetadata_WhenSymbolIsType(string filename) - { - var testFile = new TestFile(filename, @" -using System; -class Bar { - public void Baz() { - var number = in$$t.MaxValue; - } -}"); - - using var host = CreateOmniSharpHost(new[] { testFile }, new Dictionary - { - ["RoslynExtensionsOptions:EnableDecompilationSupport"] = "true" - }); - - var point = testFile.Content.GetPointFromPosition(); - - // 1. start by asking for definition of "int" - var gotoDefinitionRequest = new GotoDefinitionRequest - { - FileName = testFile.FileName, - Line = point.Line, - Column = point.Offset, - WantMetadata = true, - Timeout = 60000 + FileName = fileName, + Line = line, + Column = column, + WantMetadata = wantMetadata, + Timeout = timeout }; - var gotoDefinitionRequestHandler = GetRequestHandler(host); - var gotoDefinitionResponse = await gotoDefinitionRequestHandler.Handle(gotoDefinitionRequest); - - // 2. now, based on the response information - // go to the metadata endpoint, and ask for "int" specific decompiled source - var metadataRequest = new MetadataRequest - { - AssemblyName = gotoDefinitionResponse.Definitions.Single().MetadataSource.AssemblyName, - TypeName = gotoDefinitionResponse.Definitions.Single().MetadataSource.TypeName, - ProjectName = gotoDefinitionResponse.Definitions.Single().MetadataSource.ProjectName, - Language = gotoDefinitionResponse.Definitions.Single().MetadataSource.Language, - Timeout = 60000 - }; - var metadataRequestHandler = host.GetRequestHandler(OmniSharpEndpoints.Metadata); - var metadataResponse = await metadataRequestHandler.Handle(metadataRequest); - - // 3. the response contains SourceName ("file") and SourceText (syntax tree) - // use the source to locate "IComparable" which is an interface implemented by Int32 struct - var decompiledTree = CSharpSyntaxTree.ParseText(metadataResponse.Source); - var compilationUnit = decompiledTree.GetCompilationUnitRoot(); - // second comment should indicate we have decompiled - var comments = compilationUnit.DescendantTrivia().Where(t => t.Kind() == SyntaxKind.SingleLineCommentTrivia).ToArray(); - Assert.NotNull(comments); - Assert.Equal("// Decompiled with ICSharpCode.Decompiler 7.0.0.6488", comments[1].ToString()); - - // contrary to regular metadata, we should have methods with full bodies - // this condition would fail if decompilation wouldn't work - var methods = compilationUnit. - DescendantNodesAndSelf(). - OfType(). - Where(m => m.Body != null); - - Assert.NotEmpty(methods); - - var iComparable = compilationUnit. - DescendantNodesAndSelf(). - OfType().First(). - BaseList.Types.FirstOrDefault(x => x.Type.ToString() == "IComparable"); - var relevantLineSpan = iComparable.GetLocation().GetLineSpan(); - - // 4. now ask for the definition of "IComparable" - // pass in the SourceName (metadata "file") as FileName - since it's not a regular file in our workspace - var metadataNavigationRequest = new GotoDefinitionRequest - { - FileName = metadataResponse.SourceName, - Line = relevantLineSpan.StartLinePosition.Line, - Column = relevantLineSpan.StartLinePosition.Character, - WantMetadata = true - }; - var metadataNavigationResponse = await gotoDefinitionRequestHandler.Handle(metadataNavigationRequest); - - // 5. validate the response to be matching the expected IComparable meta info - Assert.NotNull(metadataNavigationResponse.Definitions.Single().MetadataSource); - Assert.Equal(AssemblyHelpers.CorLibName, metadataNavigationResponse.Definitions.Single().MetadataSource.AssemblyName); - Assert.Equal("System.IComparable", metadataNavigationResponse.Definitions.Single().MetadataSource.TypeName); - - Assert.NotEqual(0, metadataNavigationResponse.Definitions.Single().Location.Range.Start.Line); - Assert.NotEqual(0, metadataNavigationResponse.Definitions.Single().Location.Range.Start.Column); - } - - [Fact] - public async Task ReturnsNoResultsButDoesNotThrowForNamespaces() - { - var testFile = new TestFile("foo.cs", "namespace F$$oo {}"); - var response = await GetResponseAsync(new[] { testFile }, wantMetadata: false); - Assert.Null(response.Definitions); - } - - private async Task TestGoToSourceAsync(params TestFile[] testFiles) + protected override IEnumerable<(int Line, int Column, string FileName)> GetInfo(GotoDefinitionResponse response) { - var response = await GetResponseAsync(testFiles, wantMetadata: false); - - var targets = - from tf in testFiles - from span in tf.Content.GetSpans("def") - select (tf, span); - - if (targets.Any()) - { - foreach (var (file, definitionSpan) in targets) - { - var definitionRange = file.Content.GetRangeFromSpan(definitionSpan); + if (response.Definitions is null) + yield break; - Assert.Contains(response.Definitions, - def => file.FileName == def.Location.FileName - && definitionRange.Start.Line == def.Location.Range.Start.Line - && definitionRange.Start.Offset == def.Location.Range.Start.Column); - } - } - else + foreach (var definition in response.Definitions) { - Assert.Null(response.Definitions); + yield return (definition.Location.Range.Start.Line, definition.Location.Range.Start.Column, definition.Location.FileName); } } - private async Task TestDecompilationAsync(TestFile testFile, string expectedAssemblyName, string expectedTypeName) - { - using var host = CreateOmniSharpHost(new[] { testFile }, new Dictionary - { - ["RoslynExtensionsOptions:EnableDecompilationSupport"] = "true" - }); - - var response = await GetResponseAsync(new[] { testFile }, wantMetadata: true); - - Assert.NotNull(response.Definitions.Single().MetadataSource); - Assert.Equal(expectedAssemblyName, response.Definitions.Single().MetadataSource.AssemblyName); - Assert.Equal(expectedTypeName, response.Definitions.Single().MetadataSource.TypeName); - } - - private async Task TestGoToMetadataAsync(TestFile testFile, string expectedAssemblyName, string expectedTypeName) + protected override MetadataSource GetMetadataSource(GotoDefinitionResponse response) { - var response = await GetResponseAsync(new[] { testFile }, wantMetadata: true); - - Assert.NotNull(response.Definitions.Single().MetadataSource); - Assert.Equal(expectedAssemblyName, response.Definitions.Single().MetadataSource.AssemblyName); - Assert.Equal(expectedTypeName, response.Definitions.Single().MetadataSource.TypeName); - - // We probably shouldn't hard code metadata locations (they could change randomly) - Assert.NotEqual(0, response.Definitions.Single().Location.Range.Start.Line); - Assert.NotEqual(0, response.Definitions.Single().Location.Range.Start.Column); - } - - private async Task GetResponseAsync(TestFile[] testFiles, bool wantMetadata) - { - SharedOmniSharpTestHost.AddFilesToWorkspace(testFiles); - var source = testFiles.Single(tf => tf.Content.HasPosition); - var point = source.Content.GetPointFromPosition(); - - var request = new GotoDefinitionRequest + if (response.Definitions?.Count != 1) { - FileName = source.FileName, - Line = point.Line, - Column = point.Offset, - Timeout = 60000, - WantMetadata = wantMetadata - }; + return null; + } - var requestHandler = GetRequestHandler(SharedOmniSharpTestHost); - return await requestHandler.Handle(request); + return response.Definitions[0].MetadataSource; } } } From 59cb472d665e2998ae868d9376437e21b977a9c7 Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Sun, 30 May 2021 16:33:09 -0700 Subject: [PATCH 6/6] Use correct element when translating. --- src/OmniSharp.Cake/Extensions/ResponseExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OmniSharp.Cake/Extensions/ResponseExtensions.cs b/src/OmniSharp.Cake/Extensions/ResponseExtensions.cs index 67ea193833..31402e85df 100644 --- a/src/OmniSharp.Cake/Extensions/ResponseExtensions.cs +++ b/src/OmniSharp.Cake/Extensions/ResponseExtensions.cs @@ -260,7 +260,7 @@ private static async Task TranslateAsync(this CodeElement element, continue; } - builder.AddChild(childElement); + builder.AddChild(translatedElement); } return builder.ToCodeElement();