Skip to content

Commit f750f70

Browse files
Invoke dotnet run-api to obtain virtual project (#78648)
Co-authored-by: David Barbet <dibarbet@gmail.com>
1 parent 8f8cf6e commit f750f70

File tree

9 files changed

+431
-173
lines changed

9 files changed

+431
-173
lines changed
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
// Uncomment this to test run-api locally.
6+
// Eventually when a new enough SDK is adopted in-repo we can remove this
7+
//#define RoslynTestRunApi
8+
9+
using System.Text;
10+
using System.Text.RegularExpressions;
11+
using Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms;
12+
using Microsoft.Extensions.Logging;
13+
using Roslyn.LanguageServer.Protocol;
14+
using Roslyn.Test.Utilities;
15+
using Xunit.Abstractions;
16+
17+
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests;
18+
19+
/// <summary>
20+
/// Goal of these tests:
21+
/// - Ensure that the various request/response forms work as expected in basic scenarios.
22+
/// - Ensure that various properties on the response are populated in a reasonable way.
23+
/// Non-goals:
24+
/// - Thorough behavioral testing.
25+
/// - Testing of more intricate behaviors which are subject to change.
26+
/// </summary>
27+
public sealed class VirtualProjectXmlProviderTests : AbstractLanguageServerHostTests
28+
{
29+
public VirtualProjectXmlProviderTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper)
30+
{
31+
}
32+
33+
private class EnableRunApiTests : ExecutionCondition
34+
{
35+
public override bool ShouldSkip =>
36+
#if RoslynTestRunApi
37+
false;
38+
#else
39+
true;
40+
#endif
41+
42+
public override string SkipReason => $"Compilation symbol 'RoslynTestRunApi' is not defined.";
43+
}
44+
45+
private async Task<VirtualProjectXmlProvider> GetProjectXmlProviderAsync()
46+
{
47+
var (exportProvider, _) = await LanguageServerTestComposition.CreateExportProviderAsync(
48+
LoggerFactory, includeDevKitComponents: false, MefCacheDirectory.Path, extensionPaths: null);
49+
return exportProvider.GetExportedValue<VirtualProjectXmlProvider>();
50+
}
51+
52+
[Fact]
53+
public async Task GetProjectXml_FileBasedProgram_SdkTooOld_01()
54+
{
55+
var projectProvider = await GetProjectXmlProviderAsync();
56+
57+
var tempDir = TempRoot.CreateDirectory();
58+
var appFile = tempDir.CreateFile("app.cs");
59+
await appFile.WriteAllTextAsync("""
60+
Console.WriteLine("Hello, world!");
61+
""");
62+
63+
var globalJsonFile = tempDir.CreateFile("global.json");
64+
globalJsonFile.WriteAllBytes(Encoding.UTF8.GetBytes("""
65+
{
66+
"sdk": {
67+
"version": "9.0.105"
68+
}
69+
}
70+
"""));
71+
72+
var contentNullable = await projectProvider.GetVirtualProjectContentAsync(appFile.Path, CancellationToken.None);
73+
Assert.Null(contentNullable);
74+
}
75+
76+
[ConditionalFact(typeof(EnableRunApiTests))]
77+
public async Task GetProjectXml_FileBasedProgram_01()
78+
{
79+
var projectProvider = await GetProjectXmlProviderAsync();
80+
81+
var tempDir = TempRoot.CreateDirectory();
82+
var appFile = tempDir.CreateFile("app.cs");
83+
await appFile.WriteAllTextAsync("""
84+
Console.WriteLine("Hello, world!");
85+
""");
86+
87+
var globalJsonFile = tempDir.CreateFile("global.json");
88+
await globalJsonFile.WriteAllTextAsync("""
89+
{
90+
"sdk": {
91+
"version": "10.0.100-preview.5.25265.12"
92+
}
93+
}
94+
""");
95+
96+
var contentNullable = await projectProvider.GetVirtualProjectContentAsync(appFile.Path, CancellationToken.None);
97+
var content = contentNullable.Value;
98+
var virtualProjectXml = content.VirtualProjectXml;
99+
LoggerFactory.CreateLogger<VirtualProjectXmlProviderTests>().LogTrace(virtualProjectXml);
100+
101+
Assert.Contains("<TargetFramework>net10.0</TargetFramework>", virtualProjectXml);
102+
Assert.Contains("<ArtifactsPath>", virtualProjectXml);
103+
Assert.Empty(content.Diagnostics);
104+
}
105+
106+
[ConditionalFact(typeof(EnableRunApiTests))]
107+
public async Task GetProjectXml_NonFileBasedProgram_01()
108+
{
109+
var projectProvider = await GetProjectXmlProviderAsync();
110+
111+
var tempDir = TempRoot.CreateDirectory();
112+
var appFile = tempDir.CreateFile("app.cs");
113+
await appFile.WriteAllTextAsync("""
114+
public class C
115+
{
116+
}
117+
""");
118+
119+
var globalJsonFile = tempDir.CreateFile("global.json");
120+
await globalJsonFile.WriteAllTextAsync("""
121+
{
122+
"sdk": {
123+
"version": "10.0.100-preview.5.25265.12"
124+
}
125+
}
126+
""");
127+
128+
var contentNullable = await projectProvider.GetVirtualProjectContentAsync(appFile.Path, CancellationToken.None);
129+
var content = contentNullable.Value;
130+
LoggerFactory.CreateLogger<VirtualProjectXmlProviderTests>().LogTrace(content.VirtualProjectXml);
131+
132+
Assert.Contains("<TargetFramework>net10.0</TargetFramework>", content.VirtualProjectXml);
133+
Assert.Contains("<ArtifactsPath>", content.VirtualProjectXml);
134+
Assert.Empty(content.Diagnostics);
135+
}
136+
137+
[ConditionalFact(typeof(EnableRunApiTests))]
138+
public async Task GetProjectXml_BadPath_01()
139+
{
140+
var projectProvider = await GetProjectXmlProviderAsync();
141+
142+
var tempDir = TempRoot.CreateDirectory();
143+
144+
var globalJsonFile = tempDir.CreateFile("global.json");
145+
await globalJsonFile.WriteAllTextAsync("""
146+
{
147+
"sdk": {
148+
"version": "10.0.100-preview.5.25265.12"
149+
}
150+
}
151+
""");
152+
153+
var content = await projectProvider.GetVirtualProjectContentAsync(Path.Combine(tempDir.Path, "BAD"), CancellationToken.None);
154+
Assert.Null(content);
155+
}
156+
157+
[ConditionalFact(typeof(EnableRunApiTests))]
158+
public async Task GetProjectXml_BadDirective_01()
159+
{
160+
var projectProvider = await GetProjectXmlProviderAsync();
161+
162+
var tempDir = TempRoot.CreateDirectory();
163+
var appFile = tempDir.CreateFile("app.cs");
164+
await appFile.WriteAllTextAsync("""
165+
#:package Newtonsoft.Json@13.0.3
166+
#:BAD
167+
Console.WriteLine("Hello, world!");
168+
""");
169+
170+
var globalJsonFile = tempDir.CreateFile("global.json");
171+
await globalJsonFile.WriteAllTextAsync("""
172+
{
173+
"sdk": {
174+
"version": "10.0.100-preview.5.25265.12"
175+
}
176+
}
177+
""");
178+
179+
var contentNullable = await projectProvider.GetVirtualProjectContentAsync(appFile.Path, CancellationToken.None);
180+
var content = contentNullable.Value;
181+
var diagnostic = content.Diagnostics.Single();
182+
Assert.Contains("Unrecognized directive 'BAD'", diagnostic.Message);
183+
Assert.Equal(appFile.Path, diagnostic.Location.Path);
184+
185+
// LinePositionSpan is not deserializing properly.
186+
// Address when implementing editor squiggles. https://github.com/dotnet/roslyn/issues/78688
187+
Assert.Equal("(0,0)-(0,0)", diagnostic.Location.Span.ToString());
188+
}
189+
}

src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/DotnetCliHelper.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,15 @@ private async Task<string> GetDotnetSdkFolderFromDotnetExecutableAsync(string pr
6868
return dotnetSdkFolderPath;
6969
}
7070

71-
public Process Run(string[] arguments, string? workingDirectory, bool shouldLocalizeOutput)
71+
public Process Run(string[] arguments, string? workingDirectory, bool shouldLocalizeOutput, bool redirectStandardInput = false)
7272
{
7373
_logger.LogDebug($"Running dotnet CLI command at {_dotnetExecutablePath.Value} in directory {workingDirectory} with arguments {arguments}");
7474

7575
var startInfo = new ProcessStartInfo(_dotnetExecutablePath.Value)
7676
{
7777
CreateNoWindow = true,
7878
UseShellExecute = false,
79+
RedirectStandardInput = redirectStandardInput,
7980
RedirectStandardOutput = true,
8081
RedirectStandardError = true,
8182
};
Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
using System.Collections.Immutable;
66
using System.Security;
7+
using Microsoft.CodeAnalysis;
78
using Microsoft.CodeAnalysis.Features.Workspaces;
89
using Microsoft.CodeAnalysis.Host;
10+
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
911
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry;
1012
using Microsoft.CodeAnalysis.MetadataAsSource;
1113
using Microsoft.CodeAnalysis.MSBuild;
@@ -22,18 +24,20 @@
2224
using Roslyn.Utilities;
2325
using static Microsoft.CodeAnalysis.MSBuild.BuildHostProcessManager;
2426

25-
namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
27+
namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms;
2628

2729
/// <summary>Handles loading both miscellaneous files and file-based program projects.</summary>
2830
internal sealed class FileBasedProgramsProjectSystem : LanguageServerProjectLoader, ILspMiscellaneousFilesWorkspaceProvider
2931
{
3032
private readonly ILspServices _lspServices;
3133
private readonly ILogger<FileBasedProgramsProjectSystem> _logger;
3234
private readonly IMetadataAsSourceFileService _metadataAsSourceFileService;
35+
private readonly VirtualProjectXmlProvider _projectXmlProvider;
3336

3437
public FileBasedProgramsProjectSystem(
3538
ILspServices lspServices,
3639
IMetadataAsSourceFileService metadataAsSourceFileService,
40+
VirtualProjectXmlProvider projectXmlProvider,
3741
LanguageServerWorkspaceFactory workspaceFactory,
3842
IFileChangeWatcher fileChangeWatcher,
3943
IGlobalOptionService globalOptionService,
@@ -57,6 +61,7 @@ public FileBasedProgramsProjectSystem(
5761
_lspServices = lspServices;
5862
_logger = loggerFactory.CreateLogger<FileBasedProgramsProjectSystem>();
5963
_metadataAsSourceFileService = metadataAsSourceFileService;
64+
_projectXmlProvider = projectXmlProvider;
6065
}
6166

6267
public Workspace Workspace => ProjectFactory.Workspace;
@@ -124,20 +129,33 @@ public async ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri, bool
124129
await UnloadProjectAsync(documentPath);
125130
}
126131

127-
protected override async Task<(RemoteProjectFile projectFile, bool hasAllInformation, BuildHostProcessKind preferred, BuildHostProcessKind actual)?> TryLoadProjectInMSBuildHostAsync(
132+
protected override async Task<RemoteProjectLoadResult?> TryLoadProjectInMSBuildHostAsync(
128133
BuildHostProcessManager buildHostProcessManager, string documentPath, CancellationToken cancellationToken)
129134
{
130-
const BuildHostProcessKind buildHostKind = BuildHostProcessKind.NetCore;
131-
var buildHost = await buildHostProcessManager.GetBuildHostAsync(buildHostKind, cancellationToken);
135+
var content = await _projectXmlProvider.GetVirtualProjectContentAsync(documentPath, cancellationToken);
136+
if (content is not var (virtualProjectContent, diagnostics))
137+
{
138+
// 'GetVirtualProjectContentAsync' will log errors when it fails
139+
return null;
140+
}
132141

133-
var loader = ProjectFactory.CreateFileTextLoader(documentPath);
134-
var textAndVersion = await loader.LoadTextAsync(new LoadTextOptions(SourceHashAlgorithms.Default), cancellationToken);
135-
var (virtualProjectContent, isFileBasedProgram) = VirtualCSharpFileBasedProgramProject.MakeVirtualProjectContent(documentPath, textAndVersion.Text);
142+
foreach (var diagnostic in diagnostics)
143+
{
144+
// https://github.com/dotnet/roslyn/issues/78688: Surface diagnostics in editor
145+
_logger.LogError($"{diagnostic.Location.Path}{diagnostic.Location.Span.Start}: {diagnostic.Message}");
146+
}
136147

137148
// When loading a virtual project, the path to the on-disk source file is not used. Instead the path is adjusted to end with .csproj.
138149
// This is necessary in order to get msbuild to apply the standard c# props/targets to the project.
139-
var virtualProjectPath = VirtualCSharpFileBasedProgramProject.GetVirtualProjectPath(documentPath);
150+
var virtualProjectPath = VirtualProjectXmlProvider.GetVirtualProjectPath(documentPath);
151+
152+
var loader = ProjectFactory.CreateFileTextLoader(documentPath);
153+
var textAndVersion = await loader.LoadTextAsync(new LoadTextOptions(SourceHashAlgorithms.Default), cancellationToken);
154+
var isFileBasedProgram = VirtualProjectXmlProvider.IsFileBasedProgram(documentPath, textAndVersion.Text);
155+
156+
const BuildHostProcessKind buildHostKind = BuildHostProcessKind.NetCore;
157+
var buildHost = await buildHostProcessManager.GetBuildHostAsync(buildHostKind, cancellationToken);
140158
var loadedFile = await buildHost.LoadProjectAsync(virtualProjectPath, virtualProjectContent, languageName: LanguageNames.CSharp, cancellationToken);
141-
return (loadedFile, hasAllInformation: isFileBasedProgram, preferred: buildHostKind, actual: buildHostKind);
159+
return new RemoteProjectLoadResult(loadedFile, HasAllInformation: isFileBasedProgram, Preferred: buildHostKind, Actual: buildHostKind);
142160
}
143161
}
Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Microsoft.CodeAnalysis.Host;
77
using Microsoft.CodeAnalysis.Host.Mef;
88
using Microsoft.CodeAnalysis.LanguageServer.Handler;
9+
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
910
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry;
1011
using Microsoft.CodeAnalysis.MetadataAsSource;
1112
using Microsoft.CodeAnalysis.MSBuild;
@@ -15,7 +16,7 @@
1516
using Microsoft.CommonLanguageServerProtocol.Framework;
1617
using Microsoft.Extensions.Logging;
1718

18-
namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
19+
namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms;
1920

2021
/// <summary>
2122
/// Service to create <see cref="LspMiscellaneousFilesWorkspaceProvider"/> instances.
@@ -27,6 +28,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
2728
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
2829
internal sealed class FileBasedProgramsWorkspaceProviderFactory(
2930
IMetadataAsSourceFileService metadataAsSourceFileService,
31+
VirtualProjectXmlProvider projectXmlProvider,
3032
LanguageServerWorkspaceFactory workspaceFactory,
3133
IFileChangeWatcher fileChangeWatcher,
3234
IGlobalOptionService globalOptionService,
@@ -38,6 +40,17 @@ internal sealed class FileBasedProgramsWorkspaceProviderFactory(
3840
{
3941
public ILspMiscellaneousFilesWorkspaceProvider CreateLspMiscellaneousFilesWorkspaceProvider(ILspServices lspServices, HostServices hostServices)
4042
{
41-
return new FileBasedProgramsProjectSystem(lspServices, metadataAsSourceFileService, workspaceFactory, fileChangeWatcher, globalOptionService, loggerFactory, listenerProvider, projectLoadTelemetry, serverConfigurationFactory, binLogPathProvider);
43+
return new FileBasedProgramsProjectSystem(
44+
lspServices,
45+
metadataAsSourceFileService,
46+
projectXmlProvider,
47+
workspaceFactory,
48+
fileChangeWatcher,
49+
globalOptionService,
50+
loggerFactory,
51+
listenerProvider,
52+
projectLoadTelemetry,
53+
serverConfigurationFactory,
54+
binLogPathProvider);
4255
}
4356
}

0 commit comments

Comments
 (0)