diff --git a/src/OmniSharp.Abstractions/Models/v2/GotoTypeDefinition/GotoTypeDefinitionRequest.cs b/src/OmniSharp.Abstractions/Models/v2/GotoTypeDefinition/GotoTypeDefinitionRequest.cs new file mode 100644 index 0000000000..bea8e6b9a8 --- /dev/null +++ b/src/OmniSharp.Abstractions/Models/v2/GotoTypeDefinition/GotoTypeDefinitionRequest.cs @@ -0,0 +1,11 @@ +using OmniSharp.Mef; + +namespace OmniSharp.Models.GotoTypeDefinition +{ + [OmniSharpEndpoint(OmniSharpEndpoints.GotoTypeDefinition, typeof(GotoTypeDefinitionRequest), typeof(GotoTypeDefinitionResponse))] + public class GotoTypeDefinitionRequest : Request + { + public int Timeout { get; init; } = 10000; + public bool WantMetadata { get; init; } + } +} diff --git a/src/OmniSharp.Abstractions/Models/v2/GotoTypeDefinition/GotoTypeDefinitionResponse.cs b/src/OmniSharp.Abstractions/Models/v2/GotoTypeDefinition/GotoTypeDefinitionResponse.cs new file mode 100644 index 0000000000..34dc8b264a --- /dev/null +++ b/src/OmniSharp.Abstractions/Models/v2/GotoTypeDefinition/GotoTypeDefinitionResponse.cs @@ -0,0 +1,20 @@ +#nullable enable + +using OmniSharp.Models.Metadata; +using OmniSharp.Models.v1.SourceGeneratedFile; +using System.Collections.Generic; + +namespace OmniSharp.Models.GotoTypeDefinition +{ + public record GotoTypeDefinitionResponse + { + public List? Definitions { get; init; } + } + + public record TypeDefinition + { + public V2.Location Location { get; init; } = null!; + public MetadataSource? MetadataSource { get; init; } + public SourceGeneratedFileInfo? SourceGeneratedFileInfo { get; init; } + } +} diff --git a/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs b/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs index c26fa48fa0..cf24b1c981 100644 --- a/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs +++ b/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs @@ -3,6 +3,7 @@ namespace OmniSharp public static class OmniSharpEndpoints { public const string GotoDefinition = "/gotodefinition"; + public const string GotoTypeDefinition = "/gototypedefinition"; public const string FindSymbols = "/findsymbols"; public const string UpdateBuffer = "/updatebuffer"; public const string ChangeBuffer = "/changebuffer"; diff --git a/src/OmniSharp.Cake/OmniSharp.Cake.csproj b/src/OmniSharp.Cake/OmniSharp.Cake.csproj index 15a6211691..779edb532c 100644 --- a/src/OmniSharp.Cake/OmniSharp.Cake.csproj +++ b/src/OmniSharp.Cake/OmniSharp.Cake.csproj @@ -7,6 +7,7 @@ + diff --git a/src/OmniSharp.Cake/Services/RequestHandlers/Navigation/GotoTypeDefinitionHandler.cs b/src/OmniSharp.Cake/Services/RequestHandlers/Navigation/GotoTypeDefinitionHandler.cs new file mode 100644 index 0000000000..a5ff2c0da9 --- /dev/null +++ b/src/OmniSharp.Cake/Services/RequestHandlers/Navigation/GotoTypeDefinitionHandler.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Composition; +using System.Linq; +using System.Threading.Tasks; +using OmniSharp.Extensions; +using OmniSharp.Mef; +using OmniSharp.Models.GotoTypeDefinition; +using OmniSharp.Models.Metadata; +using OmniSharp.Models.v1.SourceGeneratedFile; +using OmniSharp.Models.V2; +using OmniSharp.Roslyn; +using OmniSharp.Utilities; +using Location = OmniSharp.Models.V2.Location; +using Range = OmniSharp.Models.V2.Range; +using OmniSharp.Roslyn.CSharp.Services; +using OmniSharp.Options; + +namespace OmniSharp.Cake.Services.RequestHandlers.Navigation +{ + [OmniSharpHandler(OmniSharpEndpoints.GotoTypeDefinition, Constants.LanguageNames.Cake), Shared] + public class GotoTypeDefinitionHandler : CakeRequestHandler + { + private readonly IExternalSourceService _externalSourceService; + + [ImportingConstructor] + public GotoTypeDefinitionHandler( + OmniSharpWorkspace workspace, + ExternalSourceServiceFactory externalSourceServiceFactory, + OmniSharpOptions omniSharpOptions) + : base(workspace) + { + _externalSourceService = externalSourceServiceFactory?.Create(omniSharpOptions) ?? throw new ArgumentNullException(nameof(externalSourceServiceFactory)); + } + + protected override async Task TranslateResponse(GotoTypeDefinitionResponse response, GotoTypeDefinitionRequest request) + { + var definitions = new List(); + foreach (var definition in response.Definitions ?? Enumerable.Empty()) + { + var file = definition.Location.FileName; + + if (string.IsNullOrEmpty(file) || !file.Equals(Constants.Paths.Generated)) + { + if (PlatformHelper.IsWindows && !string.IsNullOrEmpty(file)) + { + file = file.Replace('/', '\\'); + } + + definitions.Add(new TypeDefinition + { + MetadataSource = definition.MetadataSource, + SourceGeneratedFileInfo = definition.SourceGeneratedFileInfo, + Location = new Location + { + FileName = file, + Range = definition.Location.Range + } + }); + + continue; + } + + if (!request.WantMetadata) + { + continue; + } + + var aliasLocations = await GotoTypeDefinitionHandlerHelper.GetAliasFromExternalSourceAsync( + Workspace, + request.FileName, + definition.Location.Range.End.Line, + request.Timeout, + _externalSourceService + ); + + definitions.AddRange( + aliasLocations.Select(loc => + new TypeDefinition + { + Location = new Location + { + FileName = loc.MetadataDocument.FilePath ?? loc.MetadataDocument.Name, + Range = new Range + { + Start = new Point + { + Column = loc.LineSpan.StartLinePosition.Character, + Line = loc.LineSpan.StartLinePosition.Line + }, + End = new Point + { + Column = loc.LineSpan.EndLinePosition.Character, + Line = loc.LineSpan.EndLinePosition.Line + }, + } + }, + MetadataSource = new MetadataSource + { + AssemblyName = loc.Symbol.ContainingAssembly.Name, + ProjectName = loc.Document.Project.Name, + TypeName = loc.Symbol.GetSymbolName() + }, + SourceGeneratedFileInfo = new SourceGeneratedFileInfo + { + DocumentGuid = loc.Document.Id.Id, + ProjectGuid = loc.Document.Id.ProjectId.Id + } + }) + .ToList()); + } + + return new GotoTypeDefinitionResponse + { + Definitions = definitions + }; + } + } +} diff --git a/src/OmniSharp.Cake/Services/RequestHandlers/Navigation/GotoTypeDefinitionHandlerHelper.cs b/src/OmniSharp.Cake/Services/RequestHandlers/Navigation/GotoTypeDefinitionHandlerHelper.cs new file mode 100644 index 0000000000..48924b7f94 --- /dev/null +++ b/src/OmniSharp.Cake/Services/RequestHandlers/Navigation/GotoTypeDefinitionHandlerHelper.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.Text; +using OmniSharp.Roslyn; +using OmniSharp.Roslyn.CSharp.Services; + +namespace OmniSharp.Cake.Services.RequestHandlers.Navigation +{ + public static class GotoTypeDefinitionHandlerHelper + { + private const int MethodLineOffset = 3; + private const int PropertyLineOffset = 7; + + internal static async Task> GetAliasFromExternalSourceAsync( + OmniSharpWorkspace workspace, + string fileName, + int line, + int timeout, + IExternalSourceService externalSourceService) + { + var document = workspace.GetDocument(fileName); + var lineIndex = line + MethodLineOffset; + int column; + + if (document == null) + { + return Enumerable.Empty(); + } + + var semanticModel = await document.GetSemanticModelAsync(); + var sourceText = await document.GetTextAsync(); + var sourceLine = sourceText.Lines[lineIndex].ToString(); + if (sourceLine.Contains("(Context")) + { + column = sourceLine.IndexOf("(Context", StringComparison.Ordinal); + } + else + { + lineIndex = line + PropertyLineOffset; + sourceLine = sourceText.Lines[lineIndex].ToString(); + if (sourceLine.Contains("(Context")) + { + column = sourceLine.IndexOf("(Context", StringComparison.Ordinal); + } + else + { + return Enumerable.Empty(); + } + } + + if (column > 0 && sourceLine[column - 1] == '>') + { + column = sourceLine.LastIndexOf("<", column, StringComparison.Ordinal); + } + + var position = sourceText.Lines.GetPosition(new LinePosition(lineIndex, column)); + var symbol = await SymbolFinder.FindSymbolAtPositionAsync(semanticModel, position, workspace); + + if (symbol == null || symbol is INamespaceSymbol) + { + return Enumerable.Empty(); + } + if (symbol is IMethodSymbol method) + { + symbol = method.PartialImplementationPart ?? symbol; + } + + var typeSymbol = symbol switch + { + ILocalSymbol localSymbol => localSymbol.Type, + IFieldSymbol fieldSymbol => fieldSymbol.Type, + IPropertySymbol propertySymbol => propertySymbol.Type, + IParameterSymbol parameterSymbol => parameterSymbol.Type, + _ => null + }; + + if (typeSymbol == null) + return Enumerable.Empty(); + + var result = new List(); + foreach (var location in typeSymbol.Locations) + { + if (!location.IsInMetadata) + { + continue; + } + + var cancellationSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeout)); + var (metadataDocument, _) = await externalSourceService.GetAndAddExternalSymbolDocument(document.Project, typeSymbol, cancellationSource.Token); + if (metadataDocument == null) + { + continue; + } + + cancellationSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeout)); + var metadataLocation = await externalSourceService.GetExternalSymbolLocation(typeSymbol, metadataDocument, cancellationSource.Token); + var lineSpan = metadataLocation.GetMappedLineSpan(); + + result.Add(new Alias + { + Document = document, + MetadataDocument = metadataDocument, + Symbol = typeSymbol, + Location = location, + LineSpan = lineSpan + }); + } + + return result; + } + + internal class Alias + { + public Document Document { get; set; } + public ISymbol Symbol { get; set; } + public Location Location { get; set; } + public FileLinePositionSpan LineSpan { get; set; } + public Document MetadataDocument { get; set; } + } + } +} diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GotoTypeDefinitionHelpers.cs b/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GotoTypeDefinitionHelpers.cs new file mode 100644 index 0000000000..bfc3d88aea --- /dev/null +++ b/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GotoTypeDefinitionHelpers.cs @@ -0,0 +1,64 @@ +#nullable enable + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.Operations; +using OmniSharp.Extensions; +using OmniSharp.Models.v1.SourceGeneratedFile; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace OmniSharp.Roslyn.CSharp.Services.Navigation +{ + internal static class GotoTypeDefinitionHelpers + { + internal static async Task GetTypeOfSymbol(Document document, int line, int column, CancellationToken cancellationToken) + { + var sourceText = await document.GetTextAsync(cancellationToken); + var position = sourceText.GetPositionFromLineAndOffset(line, column); + var symbol = await SymbolFinder.FindSymbolAtPositionAsync(document, position, cancellationToken); + + return symbol switch + { + ILocalSymbol localSymbol => localSymbol.Type, + IFieldSymbol fieldSymbol => fieldSymbol.Type, + IPropertySymbol propertySymbol => propertySymbol.Type, + IParameterSymbol parameterSymbol => parameterSymbol.Type, + _ => null + }; + } + + internal static async Task GetMetadataMappedSpan( + Document document, + ISymbol symbol, + IExternalSourceService externalSourceService, + CancellationToken cancellationToken) + { + var (metadataDocument, _) = await externalSourceService.GetAndAddExternalSymbolDocument(document.Project, symbol, cancellationToken); + if (metadataDocument != null) + { + var metadataLocation = await externalSourceService.GetExternalSymbolLocation(symbol, metadataDocument, cancellationToken); + return metadataLocation.GetMappedLineSpan(); + } + + return null; + } + + internal static SourceGeneratedFileInfo? GetSourceGeneratedFileInfo(OmniSharpWorkspace workspace, Location location) + { + Debug.Assert(location.IsInSource); + var document = workspace.CurrentSolution.GetDocument(location.SourceTree); + if (document is not SourceGeneratedDocument) + { + return null; + } + + return new SourceGeneratedFileInfo + { + ProjectGuid = document.Project.Id.Id, + DocumentGuid = document.Id.Id + }; + } + } +} diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GotoTypeDefinitionService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GotoTypeDefinitionService.cs new file mode 100644 index 0000000000..64a4de2db2 --- /dev/null +++ b/src/OmniSharp.Roslyn.CSharp/Services/Navigation/GotoTypeDefinitionService.cs @@ -0,0 +1,89 @@ +#nullable enable + +using Microsoft.CodeAnalysis; +using OmniSharp.Extensions; +using OmniSharp.Mef; +using OmniSharp.Models.GotoTypeDefinition; +using OmniSharp.Options; +using System.Composition; +using System.Linq; +using System.Threading.Tasks; +using OmniSharp.Models; + +namespace OmniSharp.Roslyn.CSharp.Services.Navigation +{ + [OmniSharpHandler(OmniSharpEndpoints.GotoTypeDefinition, LanguageNames.CSharp)] + public class GotoTypeDefinitionService : IRequestHandler + { + private readonly OmniSharpOptions _omnisharpOptions; + private readonly OmniSharpWorkspace _workspace; + private readonly ExternalSourceServiceFactory _externalSourceServiceFactory; + + [ImportingConstructor] + public GotoTypeDefinitionService(OmniSharpWorkspace workspace, ExternalSourceServiceFactory externalSourceServiceFactory, OmniSharpOptions omnisharpOptions) + { + _workspace = workspace; + _externalSourceServiceFactory = externalSourceServiceFactory; + _omnisharpOptions = omnisharpOptions; + } + + public async Task Handle(GotoTypeDefinitionRequest 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 GotoTypeDefinitionResponse(); + } + + var typeSymbol = await GotoTypeDefinitionHelpers.GetTypeOfSymbol(document, request.Line, request.Column, cancellationToken); + if (typeSymbol?.Locations.IsDefaultOrEmpty != false) + { + return new GotoTypeDefinitionResponse(); + } + + if (typeSymbol.Locations[0].IsInSource) + { + return new GotoTypeDefinitionResponse() + { + Definitions = typeSymbol.Locations + .Select(location => new TypeDefinition + { + Location = location.GetMappedLineSpan().GetLocationFromFileLinePositionSpan(), + SourceGeneratedFileInfo = GoToDefinitionHelpers.GetSourceGeneratedFileInfo(_workspace, location) + }) + .ToList() + }; + } + else + { + var maybeSpan = await GoToDefinitionHelpers.GetMetadataMappedSpan(document, typeSymbol, externalSourceService, cancellationToken); + + if (maybeSpan is FileLinePositionSpan lineSpan) + { + return new GotoTypeDefinitionResponse + { + Definitions = new() + { + new TypeDefinition + { + Location = lineSpan.GetLocationFromFileLinePositionSpan(), + MetadataSource = new OmniSharp.Models.Metadata.MetadataSource() + { + AssemblyName = typeSymbol.ContainingAssembly.Name, + ProjectName = document.Project.Name, + TypeName = typeSymbol.GetSymbolName() + } + } + } + }; + } + + return new GotoTypeDefinitionResponse(); + } + } + } +} diff --git a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorkerWithAnalyzers.cs b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorkerWithAnalyzers.cs index b04cdac505..62590c5dee 100644 --- a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorkerWithAnalyzers.cs +++ b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorkerWithAnalyzers.cs @@ -303,6 +303,11 @@ private async Task> AnalyzeDocument(Project project, var documentSemanticModel = await document.GetSemanticModelAsync(perDocumentTimeout.Token); + // Analyzers cannot be called with empty analyzer list. + var canDoFullAnalysis = allAnalyzers.Length > 0 + && (!_options.RoslynExtensionsOptions.AnalyzeOpenDocumentsOnly + || _workspace.IsDocumentOpen(document.Id)); + // Only basic syntax check is available if file is miscellanous like orphan .cs file. // Those projects are on hard coded virtual project if (project.Name == $"{Configuration.OmniSharpMiscProjectName}.csproj") @@ -310,7 +315,7 @@ private async Task> AnalyzeDocument(Project project, var syntaxTree = await document.GetSyntaxTreeAsync(); return syntaxTree.GetDiagnostics().ToImmutableArray(); } - else if (allAnalyzers.Any()) // Analyzers cannot be called with empty analyzer list. + else if (canDoFullAnalysis) { var compilationWithAnalyzers = compilation.WithAnalyzers(allAnalyzers, new CompilationWithAnalyzersOptions( workspaceAnalyzerOptions, diff --git a/src/OmniSharp.Shared/Options/RoslynExtensionsOptions.cs b/src/OmniSharp.Shared/Options/RoslynExtensionsOptions.cs index 9744c833b9..35e48b1070 100644 --- a/src/OmniSharp.Shared/Options/RoslynExtensionsOptions.cs +++ b/src/OmniSharp.Shared/Options/RoslynExtensionsOptions.cs @@ -13,6 +13,7 @@ public class RoslynExtensionsOptions : OmniSharpExtensionsOptions public bool EnableAsyncCompletion { get; set; } public int DocumentAnalysisTimeoutMs { get; set; } = 30 * 1000; public int DiagnosticWorkersThreadCount { get; set; } = Math.Max(1, (int)(Environment.ProcessorCount * 0.75)); // Use 75% of available processors by default (but at least one) + public bool AnalyzeOpenDocumentsOnly { get; set; } } public class OmniSharpExtensionsOptions diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/CustomRoslynAnalyzerFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/CustomRoslynAnalyzerFacts.cs index 1989d33136..486b7bbbbc 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/CustomRoslynAnalyzerFacts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/CustomRoslynAnalyzerFacts.cs @@ -106,7 +106,7 @@ public async Task When_custom_analyzers_are_executed_then_return_results() var testAnalyzerRef = new TestAnalyzerReference("TS1100"); - var projectIds = AddProjectWitFile(host, testFile, testAnalyzerRef); + var projectIds = AddProjectWithFile(host, testFile, testAnalyzerRef); var result = await host.RequestCodeCheckAsync("testFile_66.cs"); @@ -114,10 +114,10 @@ public async Task When_custom_analyzers_are_executed_then_return_results() } } - private OmniSharpTestHost GetHost() + private OmniSharpTestHost GetHost(bool analyzeOpenDocumentsOnly = false) { return OmniSharpTestHost.Create(testOutput: _testOutput, - configurationData: TestHelpers.GetConfigurationDataWithAnalyzerConfig(roslynAnalyzersEnabled: true)); + configurationData: TestHelpers.GetConfigurationDataWithAnalyzerConfig(roslynAnalyzersEnabled: true, analyzeOpenDocumentsOnly: analyzeOpenDocumentsOnly)); } [Fact] @@ -127,7 +127,7 @@ public async Task Always_return_results_from_net_default_analyzers() { var testFile = new TestFile("testFile_1.cs", "class SomeClass { int n = true; }"); - AddProjectWitFile(host, testFile); + AddProjectWithFile(host, testFile); var result = await host.RequestCodeCheckAsync(testFile.FileName); @@ -150,7 +150,7 @@ static void Main(string[] args) } }"); - var projectId = AddProjectWitFile(host, testFile); + var projectId = AddProjectWithFile(host, testFile); var testRules = CreateRules("CS0162", ReportDiagnostic.Hidden); host.Workspace.UpdateDiagnosticOptionsForProject(projectId, testRules.ToImmutableDictionary()); @@ -170,7 +170,7 @@ public async Task When_rules_udpate_diagnostic_severity_then_show_them_with_new_ var testAnalyzerRef = new TestAnalyzerReference("TS1100"); - var projectId = AddProjectWitFile(host, testFile, testAnalyzerRef); + var projectId = AddProjectWithFile(host, testFile, testAnalyzerRef); var testRules = CreateRules(testAnalyzerRef.Id.ToString(), ReportDiagnostic.Hidden); host.Workspace.UpdateDiagnosticOptionsForProject(projectId, testRules.ToImmutableDictionary()); @@ -200,7 +200,7 @@ public async Task When_custom_rule_is_set_to_none_dont_return_results_at_all() var testAnalyzerRef = new TestAnalyzerReference("TS1101"); - var projectId = AddProjectWitFile(host, testFile, testAnalyzerRef); + var projectId = AddProjectWithFile(host, testFile, testAnalyzerRef); var testRules = CreateRules(testAnalyzerRef.Id.ToString(), ReportDiagnostic.Suppress); @@ -220,7 +220,7 @@ public async Task When_diagnostic_is_disabled_by_default_updating_rule_will_enab var testAnalyzerRef = new TestAnalyzerReference("TS1101", isEnabledByDefault: false); - var projectId = AddProjectWitFile(host, testFile, testAnalyzerRef); + var projectId = AddProjectWithFile(host, testFile, testAnalyzerRef); var testRules = CreateRules(testAnalyzerRef.Id.ToString(), ReportDiagnostic.Error); @@ -240,7 +240,7 @@ public async Task WhenDiagnosticsRulesAreUpdated_ThenReAnalyzerFilesInProject() var testAnalyzerRef = new TestAnalyzerReference("TS1101", isEnabledByDefault: false); - var projectId = AddProjectWitFile(host, testFile, testAnalyzerRef); + var projectId = AddProjectWithFile(host, testFile, testAnalyzerRef); var testRulesOriginal = CreateRules(testAnalyzerRef.Id.ToString(), ReportDiagnostic.Error); host.Workspace.UpdateDiagnosticOptionsForProject(projectId, testRulesOriginal.ToImmutableDictionary()); await host.RequestCodeCheckAsync("testFile_4.cs"); @@ -258,7 +258,42 @@ public async Task WhenDiagnosticsRulesAreUpdated_ThenReAnalyzerFilesInProject() } } - private ProjectId AddProjectWitFile(OmniSharpTestHost host, TestFile testFile, TestAnalyzerReference testAnalyzerRef = null) + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public async Task WhenDocumentIsntOpenAndAnalyzeOpenDocumentsOnlyIsSet_DontAnalyzeFiles(bool analyzeOpenDocumentsOnly, bool isDocumentOpen) + { + using (var host = GetHost(analyzeOpenDocumentsOnly)) + { + var testFile = new TestFile("testFile.cs", "class _this_is_invalid_test_class_name { int n = true; }"); + var testAnalyzerRef = new TestAnalyzerReference("TS1100"); + + AddProjectWithFile(host, testFile, testAnalyzerRef); + + if (isDocumentOpen) + { + var doc = host.Workspace.GetDocument("testFile.cs"); + + host.Workspace.OpenDocument(doc.Id); + } + + var expectedDiagnosticCount = analyzeOpenDocumentsOnly && !isDocumentOpen ? 1 : 2; + + var result = await host.RequestCodeCheckAsync("testFile.cs"); + + if (analyzeOpenDocumentsOnly && !isDocumentOpen) + Assert.DoesNotContain(result.QuickFixes.OfType(), f => f.Id == testAnalyzerRef.Id.ToString()); + else + Assert.Contains(result.QuickFixes.OfType(), f => f.Id == testAnalyzerRef.Id.ToString()); + + Assert.Contains(result.QuickFixes.OfType(), f => f.Id == "CS0029"); + } + } + + private ProjectId AddProjectWithFile(OmniSharpTestHost host, TestFile testFile, TestAnalyzerReference testAnalyzerRef = null) { var analyzerReferences = testAnalyzerRef == null ? default : new AnalyzerReference[] { testAnalyzerRef }.ToImmutableArray(); diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/GotoTypeDefinitionFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/GotoTypeDefinitionFacts.cs new file mode 100644 index 0000000000..a1ebf30429 --- /dev/null +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/GotoTypeDefinitionFacts.cs @@ -0,0 +1,605 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using OmniSharp.Models.Metadata; +using OmniSharp.Models.v1.SourceGeneratedFile; +using OmniSharp.Models.GotoTypeDefinition; +using OmniSharp.Roslyn.CSharp.Services.Navigation; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using TestUtility; +using Xunit; +using Xunit.Abstractions; + +namespace OmniSharp.Roslyn.CSharp.Tests +{ + public class GotoTypeDefinitionFacts : AbstractSingleRequestHandlerTestFixture + { + public GotoTypeDefinitionFacts(ITestOutputHelper output, SharedOmniSharpHostFixture sharedOmniSharpHostFixture) + : base(output, sharedOmniSharpHostFixture) + { + } + protected override string EndpointName => OmniSharpEndpoints.GotoTypeDefinition; + + [Theory] + [InlineData("foo.cs")] + [InlineData("foo.csx")] + public async Task ReturnsDefinitionInSameFile(string filename) + { + var testFile = new TestFile(filename, @" +class {|def:Foo|} { + private Foo f$$oo; +}"); + + 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 ReturnsOnPropertyAccessor(string filename) + { + var testFile = new TestFile(filename, @" +class Test { + public Bar Foo{ get; set; } + public class |def:Bar| + { + public int lorem { get; set; } + } + public static void main() + { + F$$oo = 3; + } +}"); + + await TestGoToSourceAsync(testFile); + } + + [Theory] + [InlineData("foo.cs")] + [InlineData("foo.csx")] + public async Task ReturnsOnPrivateField(string filename) + { + var testFile = new TestFile(filename, @" +class Test { + + public Bar foo; + public class |def:Bar| + { + public int lorem { get; set; } + } + 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 Bar foo { get; set; }; + public class |def:Bar| + { + public int lorem { get; set; } + } + public int Foo + { + get => foo; + set => f$$oo = value; + } +}"); + + await TestGoToSourceAsync(testFile); + } + + [Theory] + [InlineData("foo.cs")] + [InlineData("foo.csx")] + public async Task ReturnsOnPropertySetterParam(string filename) + { + var testFile = new TestFile(filename, @" +class Test { + + public Bar foo { get; set; } + public class |def:Bar| + { + public int lorem { get; set; } + } + public Bar Foo + { + get => foo; + set => foo = va$$lue; + } +}"); + + await TestGoToSourceAsync(testFile); + } + + [Theory] + [InlineData("foo.cs")] + [InlineData("foo.csx")] + public async Task ReturnsOnPropertyAccessorPropertyGetting(string filename) + { + var testFile = new TestFile(filename, @" +class Test { + public Bar Foo { get; set; } + public class |def:Bar| + { + public int lorem { get; set; } + } + public static void main() + { + Foo = 3; + Console.WriteLine(F$$oo); + } +}"); + + await TestGoToSourceAsync(testFile); + } + + [Theory] + [InlineData("foo.cs")] + [InlineData("foo.csx")] + public async Task ReturnsOnImplicitLambdaParam(string filename) + { + var testFile = new TestFile(filename, @" +using System.Collections.Generic; + +class Test { + public Bar Foo { get; set; } + public class |def:Bar| + { + public int lorem { get; set; } + } + public static void Main() + { + var list = new List(); + list.Add(new Bar()); + list.ForEach(inp$$ut => _ = input.lorem); + } +}"); + + await TestGoToSourceAsync(testFile); + } + + [Theory] + [InlineData("foo.cs")] + [InlineData("foo.csx")] + public async Task ReturnsOnListFindResult(string filename) + { + var testFile = new TestFile(filename, @" +using System.Collections.Generic; + +class Test { + public Bar Foo { get; set; } + public class |def:Bar| + { + public int lorem { get; set; } + } + public static void Main() + { + var list = new List(); + list.Add(new Bar()); + var out$$put = list.Find(input => _ = input.lorem == 12); + } +}"); + + 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 Foo f$$oo; +}"); + + 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 Baz f$$oo; + }"); + + 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() { + var gu$$id = Guid.NewGuid(); + } +}"); + + 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() { + var g$$ = Guid.NewGuid(); + } +}"); + + await TestDecompilationAsync(testFile, + expectedAssemblyName: AssemblyHelpers.CorLibName, + expectedTypeName: "System.Guid"); + } + + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsDefinitionInMetadata_WhenSymbolIsParam(string filename) + { + var testFile = new TestFile(filename, @" + using System.Collections.Generic; + class Bar { + public void Baz(List par$$am1) { + var foo = new List(); + var f = param1; + } + }"); + + await TestGoToMetadataAsync(testFile, + expectedAssemblyName: AssemblyHelpers.CorLibName, + expectedTypeName: "System.Collections.Generic.List`1"); + } + + [Theory] + [InlineData("bar.cs")] + [InlineData("bar.csx")] + public async Task ReturnsDecompiledDefinition_WhenSymbolIsIndexedList(string filename) + { + var testFile = new TestFile(filename, @" +using System.Collections.Generic; +class Bar { + public void Baz() { + var foo = new List(); + var lorem = fo$$o[0]; + } +}"); + + 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 = String.Em$$pty; + } + }"); + + await TestGoToMetadataAsync(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 num$$ber = int.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: 600000000); + var gotoDefinitionRequestHandler = GetRequestHandler(host); + var gotoDefinitionResponse = await gotoDefinitionRequestHandler.Handle(gotoDefinitionRequest); + var gotoDefinitionResponseMetadataSource = GetMetadataSource(gotoDefinitionResponse); + Assert.NotNull(gotoDefinitionResponseMetadataSource); + Assert.Equal(AssemblyHelpers.CorLibName, gotoDefinitionResponseMetadataSource.AssemblyName); + Assert.Equal("System.Int32", gotoDefinitionResponseMetadataSource.TypeName); + var info = GetInfo(gotoDefinitionResponse); + 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 num$$ber = int.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: 60000000); + 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 = 6000000 + }; + 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.IsKind(SyntaxKind.SingleLineCommentTrivia)).ToArray(); + Assert.NotNull(comments); + Assert.Equal("// Decompiled with ICSharpCode.Decompiler 7.1.0.6543", 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); + } + + [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)); + } + + [Fact] + public async Task ReturnsResultsForSourceGenerators() + { + const string Source = @" +public class {|generatedClassName:Generated|} +{ + public int {|propertyName:Property|} { get; set; } +} +"; + const string FileName = "real.cs"; + TestFile generatedTestFile = new("GeneratedFile.cs", Source); + var testFile = new TestFile(FileName, @" +class C +{ + public void M(Generated gen) + { + _ = ge$$n.Property; + } +} +"); + + TestHelpers.AddProjectToWorkspace(SharedOmniSharpTestHost.Workspace, + "project.csproj", + new[] { "netcoreapp3.1" }, + new[] { testFile }, + analyzerRefs: ImmutableArray.Create(new TestGeneratorReference( + context => context.AddSource("GeneratedFile", generatedTestFile.Content.Code)))); + + var point = testFile.Content.GetPointFromPosition(); + + var gotoDefRequest = CreateRequest(FileName, point.Line, point.Offset, wantMetadata: true); + var gotoDefHandler = GetRequestHandler(SharedOmniSharpTestHost); + var response = await gotoDefHandler.Handle(gotoDefRequest); + var info = GetInfo(response).Single(); + + Assert.NotNull(info.SourceGeneratorInfo); + + var expectedSpan = generatedTestFile.Content.GetSpans("generatedClassName").Single(); + var expectedRange = generatedTestFile.Content.GetRangeFromSpan(expectedSpan); + + Assert.Equal(expectedRange.Start.Line, info.Line); + Assert.Equal(expectedRange.Start.Offset, info.Column); + + var sourceGeneratedFileHandler = SharedOmniSharpTestHost.GetRequestHandler(OmniSharpEndpoints.SourceGeneratedFile); + var sourceGeneratedRequest = new SourceGeneratedFileRequest + { + DocumentGuid = info.SourceGeneratorInfo.DocumentGuid, + ProjectGuid = info.SourceGeneratorInfo.ProjectGuid + }; + + var sourceGeneratedFileResponse = await sourceGeneratedFileHandler.Handle(sourceGeneratedRequest); + Assert.NotNull(sourceGeneratedFileResponse); + Assert.Equal(generatedTestFile.Content.Code, sourceGeneratedFileResponse.Source); + Assert.Equal(@"OmniSharp.Roslyn.CSharp.Tests\OmniSharp.Roslyn.CSharp.Tests.TestSourceGenerator\GeneratedFile.cs", sourceGeneratedFileResponse.SourceName.Replace("/", @"\")); + } + + 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 GotoTypeDefinitionRequest CreateRequest(string fileName, int line, int column, bool wantMetadata, int timeout = 60000) + => new GotoTypeDefinitionRequest + { + FileName = fileName, + Line = line, + Column = column, + WantMetadata = wantMetadata, + Timeout = timeout + }; + + protected IEnumerable<(int Line, int Column, string FileName, SourceGeneratedFileInfo SourceGeneratorInfo)> GetInfo(GotoTypeDefinitionResponse response) + { + if (response.Definitions is null) + yield break; + + foreach (var definition in response.Definitions) + { + yield return (definition.Location.Range.Start.Line, definition.Location.Range.Start.Column, definition.Location.FileName, definition.SourceGeneratedFileInfo); + } + } + + protected MetadataSource GetMetadataSource(GotoTypeDefinitionResponse response) + { + Assert.Single(response.Definitions); + return response.Definitions[0].MetadataSource; + } + } +} diff --git a/tests/TestUtility/AbstractCodeActionsTestFixture.cs b/tests/TestUtility/AbstractCodeActionsTestFixture.cs index b731208856..a7d8ff8232 100644 --- a/tests/TestUtility/AbstractCodeActionsTestFixture.cs +++ b/tests/TestUtility/AbstractCodeActionsTestFixture.cs @@ -64,9 +64,9 @@ protected async Task RunRefactoringAsync(string code, str return await RunRefactoringsAsync(code, identifier, wantsChanges); } - protected async Task> FindRefactoringNamesAsync(string code, bool isAnalyzersEnabled = true) + protected async Task> FindRefactoringNamesAsync(string code, bool isAnalyzersEnabled = true, bool analyzeOpenDocumentsOnly = false) { - var codeActions = await FindRefactoringsAsync(code, TestHelpers.GetConfigurationDataWithAnalyzerConfig(isAnalyzersEnabled)); + var codeActions = await FindRefactoringsAsync(code, TestHelpers.GetConfigurationDataWithAnalyzerConfig(isAnalyzersEnabled, analyzeOpenDocumentsOnly: analyzeOpenDocumentsOnly)); return codeActions.Select(a => a.Name); } diff --git a/tests/TestUtility/TestHelpers.cs b/tests/TestUtility/TestHelpers.cs index 3e9c7edcd4..173f628890 100644 --- a/tests/TestUtility/TestHelpers.cs +++ b/tests/TestUtility/TestHelpers.cs @@ -143,13 +143,15 @@ public static MSBuildInstance AddDotNetCoreToFakeInstance(this MSBuildInstance i public static IConfiguration GetConfigurationDataWithAnalyzerConfig( bool roslynAnalyzersEnabled = false, bool editorConfigEnabled = false, - Dictionary existingConfiguration = null) + Dictionary existingConfiguration = null, + bool analyzeOpenDocumentsOnly = false) { if (existingConfiguration == null) { return new Dictionary() { { "RoslynExtensionsOptions:EnableAnalyzersSupport", roslynAnalyzersEnabled.ToString() }, + { "RoslynExtensionsOptions:AnalyzeOpenDocumentsOnly", analyzeOpenDocumentsOnly.ToString() }, { "FormattingOptions:EnableEditorConfigSupport", editorConfigEnabled.ToString() } }.ToConfiguration(); }