diff --git a/eng/ensure-sources-synced.cs b/eng/ensure-sources-synced.cs
index ed599f4a6fc76..db45b2d6da248 100755
--- a/eng/ensure-sources-synced.cs
+++ b/eng/ensure-sources-synced.cs
@@ -6,8 +6,8 @@
using System.Net.Http.Headers;
using System.Text.Json;
-// Verifies or updates the shared source files under `src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms`
-// using files from dotnet/sdk at the commit specified in `src/Features/CSharp/Portable/SyncedSource/commitid.txt`.
+// Verifies or updates the shared source files under `src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms`
+// using files from dotnet/sdk at the commit specified in `src/Workspaces/CSharp/Portable/SyncedSource/commitid.txt`.
//
// Usage:
// dotnet run --file eng/ensure-sources-synced.cs
@@ -34,13 +34,13 @@ static async Task MainAsync(string[] args)
var mode = ParseMode(args);
- var commitIdPath = Path.Combine(root, "src", "Features", "CSharp", "Portable", "SyncedSource", "commitid.txt");
+ var commitIdPath = Path.Combine(root, "src", "Workspaces", "CSharp", "Portable", "SyncedSource", "commitid.txt");
if (!File.Exists(commitIdPath)) throw new InvalidOperationException($"'{commitIdPath}' not found.");
var sdkCommit = File.ReadAllText(commitIdPath).Trim();
if (string.IsNullOrWhiteSpace(sdkCommit)) throw new InvalidOperationException($"'{commitIdPath}' is empty.");
- var localSourceDir = Path.Combine(root, "src", "Features", "CSharp", "Portable", "SyncedSource", "FileBasedPrograms");
+ var localSourceDir = Path.Combine(root, "src", "Workspaces", "CSharp", "Portable", "SyncedSource", "FileBasedPrograms");
var httpClient = CreateHttpClient();
@@ -54,6 +54,13 @@ static async Task MainAsync(string[] args)
name.EndsWith(".resx", StringComparison.OrdinalIgnoreCase),
mapRelativePath: static name => name).ConfigureAwait(false);
+ var commonFiles = await GetDirectoryFilesAsync(
+ httpClient,
+ sdkCommit,
+ githubDirectoryPath: "src/Common",
+ includeFile: static name => string.Equals(name, "MSBuildUtilities.cs", StringComparison.OrdinalIgnoreCase),
+ mapRelativePath: static name => name).ConfigureAwait(false);
+
var editorConfigFiles = await GetDirectoryFilesAsync(
httpClient,
sdkCommit,
@@ -62,6 +69,7 @@ static async Task MainAsync(string[] args)
mapRelativePath: static _ => ".editorconfig").ConfigureAwait(false);
var sourcePackageFiles = sourceFiles
+ .Concat(commonFiles)
.Concat(editorConfigFiles)
.ToList();
if (sourcePackageFiles.Count == 0) throw new InvalidOperationException("No source files found in dotnet/sdk.");
diff --git a/src/Features/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.Features.csproj b/src/Features/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.Features.csproj
index 8ffd412d2d823..8b244e44f9580 100644
--- a/src/Features/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.Features.csproj
+++ b/src/Features/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.Features.csproj
@@ -62,10 +62,6 @@
-
@@ -74,9 +70,6 @@
-
- $(DefineConstants);FILE_BASED_PROGRAMS_SOURCE_PACKAGE_GRACEFUL_EXCEPTION
-
diff --git a/src/Features/CSharp/Portable/SyncedSource/commitid.txt b/src/Features/CSharp/Portable/SyncedSource/commitid.txt
deleted file mode 100644
index dea4abdde72f1..0000000000000
--- a/src/Features/CSharp/Portable/SyncedSource/commitid.txt
+++ /dev/null
@@ -1 +0,0 @@
-b6ecfca4772c223907a0fe13b0ab944a8e197d53
\ No newline at end of file
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Microsoft.CodeAnalysis.LanguageServer.UnitTests.csproj b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Microsoft.CodeAnalysis.LanguageServer.UnitTests.csproj
index ac0f511ebb02c..b7df8e58c1e28 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Microsoft.CodeAnalysis.LanguageServer.UnitTests.csproj
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Microsoft.CodeAnalysis.LanguageServer.UnitTests.csproj
@@ -21,6 +21,7 @@
+
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/MiscellaneousFiles/FileBasedProgramsEntryPointDiscoveryTests.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/MiscellaneousFiles/FileBasedProgramsEntryPointDiscoveryTests.cs
index 5a24b0ae75534..7dfc410bbbd7c 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/MiscellaneousFiles/FileBasedProgramsEntryPointDiscoveryTests.cs
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/MiscellaneousFiles/FileBasedProgramsEntryPointDiscoveryTests.cs
@@ -3,6 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Immutable;
+using System.Reflection;
+using Microsoft.CodeAnalysis.FileBasedPrograms;
+using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms;
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
using Microsoft.CodeAnalysis.Shared.Extensions;
@@ -55,9 +58,11 @@ public void Dispose()
}
}
- private void DeferDeleteCacheDirectory(string workspacePath)
+ private void DeferDeleteCacheDirectory(TestLspServer testLspServer, string workspacePath)
{
- _additionalDirectoriesToDelete.Add(VirtualProjectXmlProvider.GetDiscoveryCacheDirectory(workspacePath));
+ var fileBasedProgramService = testLspServer.GetRequiredLspService().Workspace.Services.GetRequiredService();
+ var cacheDirectory = fileBasedProgramService.GetDiscoveryCacheDirectory(workspacePath);
+ _additionalDirectoriesToDelete.Add(cacheDirectory);
}
/// Verify that multiple invocations of 'actualFactory' result in the same 'expected' sequence.
@@ -76,7 +81,6 @@ public async Task TestDiscovery_Simple()
// Ordinary.cs
var tempDir = _tempRoot.CreateDirectory();
- DeferDeleteCacheDirectory(tempDir.Path);
var appText = """
#!/usr/bin/env dotnet
@@ -131,7 +135,6 @@ public async Task TestDiscovery_IgnoredFolders()
// App2.cs
var tempDir = _tempRoot.CreateDirectory();
- DeferDeleteCacheDirectory(tempDir.Path);
var artifactsDir = tempDir.CreateDirectory("artifacts");
var app1Text = """
@@ -161,7 +164,6 @@ public async Task TestDiscovery_DotPrefixedFolders()
// App4.cs
var tempDir = _tempRoot.CreateDirectory();
- DeferDeleteCacheDirectory(tempDir.Path);
var appText = """
#!/usr/bin/env dotnet
@@ -196,7 +198,6 @@ public async Task TestDiscovery_NestedDotPrefixedFolders()
// App2.cs
var tempDir = _tempRoot.CreateDirectory();
- DeferDeleteCacheDirectory(tempDir.Path);
var appText = """
#!/usr/bin/env dotnet
@@ -227,7 +228,6 @@ public async Task TestDiscovery_CsprojInCone()
// App.cs
var tempDir = _tempRoot.CreateDirectory();
- DeferDeleteCacheDirectory(tempDir.Path);
var projectDir = tempDir.CreateDirectory("Project");
var csprojFile = projectDir.CreateFile("Project.csproj");
@@ -256,7 +256,6 @@ public async Task TestDiscovery_Option_EnableFileBasedPrograms_True()
// Ensure discovery occurs when relevant options are enabled
// Note: the option is checked in the higher level API, so we need to verify the effects in project system.
var tempDir = _tempRoot.CreateDirectory();
- DeferDeleteCacheDirectory(tempDir.Path);
var appText = """
#!/usr/bin/env dotnet
@@ -274,6 +273,7 @@ public async Task TestDiscovery_Option_EnableFileBasedPrograms_True()
new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace1" }
]
});
+ DeferDeleteCacheDirectory(testLspServer, tempDir.Path);
var discovery = testLspServer.GetRequiredLspService();
await discovery.FindAndLoadEntryPointsAsync();
@@ -289,7 +289,6 @@ public async Task TestDiscovery_Option_EnableFileBasedPrograms_False()
// Ensure discovery doesn't occur when 'dotnet.projects.enableFileBasedPrograms: false' is set
// Note: the option is checked in the higher level API, so we need to verify the effects in project system.
var tempDir = _tempRoot.CreateDirectory();
- DeferDeleteCacheDirectory(tempDir.Path);
var appText = """
#!/usr/bin/env dotnet
@@ -307,6 +306,7 @@ public async Task TestDiscovery_Option_EnableFileBasedPrograms_False()
new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace1" }
]
});
+ DeferDeleteCacheDirectory(testLspServer, tempDir.Path);
var discovery = testLspServer.GetRequiredLspService();
await discovery.FindAndLoadEntryPointsAsync();
@@ -322,7 +322,6 @@ public async Task TestDiscovery_Option_EnableAutomaticDiscovery_False()
// Ensure discovery doesn't occur when 'dotnet.fileBasedApps.enableAutomaticDiscovery: false' is set
// Note: the option is checked in the higher level API, so we need to verify the effects in project system.
var tempDir = _tempRoot.CreateDirectory();
- DeferDeleteCacheDirectory(tempDir.Path);
var appText = """
#!/usr/bin/env dotnet
@@ -346,7 +345,6 @@ public async Task TestDiscovery_UTF8_BOM()
{
// File starting with UTF-8 BOM followed by '#!' should be discovered
var tempDir = _tempRoot.CreateDirectory();
- DeferDeleteCacheDirectory(tempDir.Path);
var appText = """
#!/usr/bin/env dotnet
@@ -364,8 +362,9 @@ public async Task TestDiscovery_UTF8_BOM()
AssertEx.SequenceEqual([appFile.Path], discovery.FindEntryPoints(tempDir.Path));
}
- private Task CreateDiscoveryTestServerAsync(string workspacePath)
- => CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace: false, new InitializationOptions
+ private async Task CreateDiscoveryTestServerAsync(string workspacePath)
+ {
+ var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace: false, new InitializationOptions
{
ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer,
// Disable background discovery so it doesn't race with direct FindEntryPoints calls.
@@ -375,6 +374,9 @@ private Task CreateDiscoveryTestServerAsync(string workspacePath)
new() { DocumentUri = CreateAbsoluteDocumentUri(workspacePath), Name = "workspace1" }
]
});
+ DeferDeleteCacheDirectory(testLspServer, workspacePath);
+ return testLspServer;
+ }
private static async Task<(Workspace? workspace, Document? document)> GetLspWorkspaceAndDocumentAsync(DocumentUri uri, TestLspServer testLspServer)
{
@@ -395,7 +397,6 @@ public async Task Swap_ReplaceFBAWithNonFBA()
{
// Swap an FBA out for non-FBA at the same path 'sub1/File1.cs'.
var tempDir = _tempRoot.CreateDirectory();
- DeferDeleteCacheDirectory(tempDir.Path);
await using var testLspServer = await CreateDiscoveryTestServerAsync(tempDir.Path);
var discovery = testLspServer.GetRequiredLspService();
@@ -416,7 +417,8 @@ public async Task Swap_ReplaceFBAWithNonFBA()
var cachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray();
// Delete cache
- var cacheDirectory = VirtualProjectXmlProvider.GetDiscoveryCacheDirectory(tempDir.Path);
+ var fileBasedProgramService = testLspServer.GetRequiredLspService().Workspace.Services.GetRequiredService();
+ var cacheDirectory = fileBasedProgramService.GetDiscoveryCacheDirectory(tempDir.Path);
Directory.Delete(cacheDirectory, recursive: true);
// Discovery without cache - should match
@@ -429,7 +431,6 @@ public async Task Swap_ReplaceNonFBAWithFBA()
{
// Swap a non-FBA out for FBA at the same path 'sub/File1.cs'.
var tempDir = _tempRoot.CreateDirectory();
- DeferDeleteCacheDirectory(tempDir.Path);
await using var testLspServer = await CreateDiscoveryTestServerAsync(tempDir.Path);
var discovery = testLspServer.GetRequiredLspService();
@@ -450,7 +451,8 @@ public async Task Swap_ReplaceNonFBAWithFBA()
var cachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray();
// Delete cache
- var cacheDirectory = VirtualProjectXmlProvider.GetDiscoveryCacheDirectory(tempDir.Path);
+ var fileBasedProgramService = testLspServer.GetRequiredLspService().Workspace.Services.GetRequiredService();
+ var cacheDirectory = fileBasedProgramService.GetDiscoveryCacheDirectory(tempDir.Path);
Directory.Delete(cacheDirectory, recursive: true);
// Discovery without cache — should match
@@ -463,7 +465,6 @@ public async Task Swap_ReplaceFBADirectoryWithNonFBADirectory()
{
// Swap a directory containing FBA out for a directory containing non-FBA at 'sub1/File1.cs'.
var tempDir = _tempRoot.CreateDirectory();
- DeferDeleteCacheDirectory(tempDir.Path);
await using var testLspServer = await CreateDiscoveryTestServerAsync(tempDir.Path);
var discovery = testLspServer.GetRequiredLspService();
@@ -485,7 +486,8 @@ public async Task Swap_ReplaceFBADirectoryWithNonFBADirectory()
var cachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray();
// Delete cache
- var cacheDirectory = VirtualProjectXmlProvider.GetDiscoveryCacheDirectory(tempDir.Path);
+ var fileBasedProgramService = testLspServer.GetRequiredLspService().Workspace.Services.GetRequiredService();
+ var cacheDirectory = fileBasedProgramService.GetDiscoveryCacheDirectory(tempDir.Path);
Directory.Delete(cacheDirectory, recursive: true);
// Discovery without cache
@@ -498,7 +500,6 @@ public async Task Swap_ReplaceNonFBADirectoryWithFBADirectory()
{
// Swap a directory containing non-FBA out for a directory containing FBA at the same path 'sub1/File1.cs'.
var tempDir = _tempRoot.CreateDirectory();
- DeferDeleteCacheDirectory(tempDir.Path);
await using var testLspServer = await CreateDiscoveryTestServerAsync(tempDir.Path);
var discovery = testLspServer.GetRequiredLspService();
@@ -520,7 +521,8 @@ public async Task Swap_ReplaceNonFBADirectoryWithFBADirectory()
var cachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray();
// Delete cache
- var cacheDirectory = VirtualProjectXmlProvider.GetDiscoveryCacheDirectory(tempDir.Path);
+ var fileBasedProgramService = testLspServer.GetRequiredLspService().Workspace.Services.GetRequiredService();
+ var cacheDirectory = fileBasedProgramService.GetDiscoveryCacheDirectory(tempDir.Path);
Directory.Delete(cacheDirectory, recursive: true);
// Discovery without cache — should match
@@ -532,7 +534,6 @@ public async Task Swap_ReplaceNonFBADirectoryWithFBADirectory()
public async Task Fuzz_1()
{
var tempDir = _tempRoot.CreateDirectory();
- DeferDeleteCacheDirectory(tempDir.Path);
await using var testLspServer = await CreateDiscoveryTestServerAsync(tempDir.Path);
var discovery = testLspServer.GetRequiredLspService();
@@ -558,7 +559,8 @@ public async Task Fuzz_1()
var cachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray();
// Delete cache
- var cacheDirectory = VirtualProjectXmlProvider.GetDiscoveryCacheDirectory(tempDir.Path);
+ var fileBasedProgramService = testLspServer.GetRequiredLspService().Workspace.Services.GetRequiredService();
+ var cacheDirectory = fileBasedProgramService.GetDiscoveryCacheDirectory(tempDir.Path);
Directory.Delete(cacheDirectory, recursive: true);
// Discovery without cache — should match
@@ -570,7 +572,6 @@ public async Task Fuzz_1()
public async Task Fuzz_2()
{
var tempDir = _tempRoot.CreateDirectory();
- DeferDeleteCacheDirectory(tempDir.Path);
await using var testLspServer = await CreateDiscoveryTestServerAsync(tempDir.Path);
var discovery = testLspServer.GetRequiredLspService();
@@ -597,7 +598,8 @@ public async Task Fuzz_2()
var cachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray();
// Delete cache
- var cacheDirectory = VirtualProjectXmlProvider.GetDiscoveryCacheDirectory(tempDir.Path);
+ var fileBasedProgramService = testLspServer.GetRequiredLspService().Workspace.Services.GetRequiredService();
+ var cacheDirectory = fileBasedProgramService.GetDiscoveryCacheDirectory(tempDir.Path);
Directory.Delete(cacheDirectory, recursive: true);
// Discovery without cache — should match
@@ -609,7 +611,6 @@ public async Task Fuzz_2()
public async Task Fuzz_3()
{
var tempDir = _tempRoot.CreateDirectory();
- DeferDeleteCacheDirectory(tempDir.Path);
await using var testLspServer = await CreateDiscoveryTestServerAsync(tempDir.Path);
var discovery = testLspServer.GetRequiredLspService();
@@ -634,7 +635,8 @@ public async Task Fuzz_3()
var cachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray();
// Delete cache
- var cacheDirectory = VirtualProjectXmlProvider.GetDiscoveryCacheDirectory(tempDir.Path);
+ var fileBasedProgramService = testLspServer.GetRequiredLspService().Workspace.Services.GetRequiredService();
+ var cacheDirectory = fileBasedProgramService.GetDiscoveryCacheDirectory(tempDir.Path);
Directory.Delete(cacheDirectory, recursive: true);
// Discovery without cache — should match
@@ -842,7 +844,6 @@ public async Task Fuzz()
_testOutputHelper.WriteLine($"Random seed: {seed}");
var tempDir = _tempRoot.CreateDirectory();
- DeferDeleteCacheDirectory(tempDir.Path);
await using var testLspServer = await CreateDiscoveryTestServerAsync(tempDir.Path);
var discovery = testLspServer.GetRequiredLspService();
@@ -865,7 +866,8 @@ public async Task Fuzz()
}
// Delete cache from any prior iteration
- var cacheDirectory = VirtualProjectXmlProvider.GetDiscoveryCacheDirectory(tempDir.Path);
+ var fileBasedProgramService = testLspServer.GetRequiredLspService().Workspace.Services.GetRequiredService();
+ var cacheDirectory = fileBasedProgramService.GetDiscoveryCacheDirectory(tempDir.Path);
if (Directory.Exists(cacheDirectory))
Directory.Delete(cacheDirectory, recursive: true);
@@ -916,6 +918,11 @@ public async Task Fuzz()
}
private void DumpFuzzReproCase(int iteration, List setupOps, List editOps)
+ {
+ _testOutputHelper.WriteLine(BuildFuzzReproCase(iteration, setupOps, editOps));
+ }
+
+ private static string BuildFuzzReproCase(int iteration, List setupOps, List editOps)
{
var sb = new System.Text.StringBuilder();
sb.AppendLine($$"""
@@ -924,20 +931,9 @@ private void DumpFuzzReproCase(int iteration, List setupOps, List options.SetGlobalOption(FileBasedAppsOptionsStorage.EnableAutomaticDiscovery, false),
- WorkspaceFolders =
- [
- new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = \"workspace1\" }
- ]
- });
+ await using var testLspServer = await CreateDiscoveryTestServerAsync(tempDir.Path);
var discovery = testLspServer.GetRequiredLspService();
- sb.AppendLine();
// Setup
""");
@@ -960,7 +956,8 @@ private void DumpFuzzReproCase(int iteration, List setupOps, List().Workspace.Services.GetRequiredService();
+ var cacheDirectory = fileBasedProgramService.GetDiscoveryCacheDirectory(tempDir.Path);
Directory.Delete(cacheDirectory, recursive: true);
// Discovery without cache — should match
@@ -969,7 +966,81 @@ private void DumpFuzzReproCase(int iteration, List setupOps, List
+ {
+ new FuzzOp.CreateDir(@"sub1\sub3"),
+ new FuzzOp.WriteCsproj("Project0.csproj"),
+ new FuzzOp.WriteFbaFile(@"sub1\sub3\Fba1.cs"),
+ new FuzzOp.WriteOrdinaryCs(@"sub1\Ordinary4.cs"),
+ };
+ var editOps = new List
+ {
+ new FuzzOp.DeleteFile("Project0.csproj"),
+ new FuzzOp.RenameFile(@"sub1\sub3\Fba1.cs", @"sub1\sub3\NewFba64.cs"),
+ };
+
+ var reproMethod = BuildFuzzReproCase(iteration: 0, setupOps, editOps);
+
+ var source = $$"""
+ using System;
+ using System.IO;
+ using System.Linq;
+ using System.Collections.Generic;
+ using System.Threading.Tasks;
+ using Microsoft.CodeAnalysis.Host;
+ using Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms;
+ using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
+ using Microsoft.CodeAnalysis.Test.Utilities;
+ using Microsoft.CodeAnalysis.FileBasedPrograms;
+ using Roslyn.Test.Utilities;
+ using static Roslyn.Test.Utilities.AbstractLanguageServerProtocolTests;
+ using Xunit;
+
+ internal sealed class Skeleton
+ {
+ private readonly TempRoot {{nameof(_tempRoot)}} = new();
+ private const string {{nameof(FbaContent)}} = "", {{nameof(OrdinaryCsContent)}} = "", {{nameof(CsprojContent)}} = "";
+ private Task {{nameof(CreateDiscoveryTestServerAsync)}}(string p) => throw null!;
+ {{reproMethod}}
+ }
+ """;
+
+ var tree = CSharp.CSharpSyntaxTree.ParseText(SourceText.From(source));
+
+ IEnumerable assemblies =
+ [
+ typeof(Assert).Assembly, // xunit.assert
+ typeof(FactAttribute).Assembly, // xunit.core
+ typeof(AssertEx).Assembly, // Microsoft.CodeAnalysis.Test.Utilities
+ typeof(AbstractLanguageServerProtocolTests).Assembly,
+ typeof(Workspace).Assembly, // Microsoft.CodeAnalysis.Workspaces
+ typeof(IHostWorkspaceProvider).Assembly, // Microsoft.CodeAnalysis.LanguageServer.Protocol.Test.Utilities
+ typeof(FileBasedProgramsEntryPointDiscovery).Assembly, // Microsoft.CodeAnalysis.LanguageServer
+ GetType().Assembly, // Microsoft.CodeAnalysis.LanguageServer.UnitTests
+ ];
+ List references =
+ [
+ .. TargetFrameworkUtil.GetReferences(TargetFramework.Net100),
+ .. assemblies.Select(a => MetadataReference.CreateFromFile(a.Location)),
+ ];
+
+ var compilation = CSharp.CSharpCompilation.Create(
+ GetType().Assembly.GetName().Name!,
+ [tree],
+ references,
+ new CSharp.CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+
+ var errors = compilation.GetDiagnostics()
+ .Where(d => d.Severity == DiagnosticSeverity.Error)
+ .Where(d => d.Id != "CS0281") // ignore IVT errors
+ .Select(d => $"{d.Id}: {d.GetMessage()}");
+ AssertEx.Empty(errors, "Generated fuzz repro case does not compile.");
}
#endregion
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/MiscellaneousFiles/FileBasedProgramsWorkspaceTests.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/MiscellaneousFiles/FileBasedProgramsWorkspaceTests.cs
index 17bc882590efd..aa79954478fac 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/MiscellaneousFiles/FileBasedProgramsWorkspaceTests.cs
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/MiscellaneousFiles/FileBasedProgramsWorkspaceTests.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Immutable;
+using System.Text;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms;
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
@@ -62,6 +63,51 @@ public async Task TestFileBasedProgram_Simple(bool mutatingLspWorkspace)
Assert.Empty(syntaxTree.GetDiagnostics(CancellationToken.None));
}
+ [Theory, CombinatorialData]
+ public async Task TestFileBasedProgram_RefDirective(bool mutatingLspWorkspace)
+ {
+ await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer });
+
+ Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer));
+ var tempDir = CreateTempDirectoryWithGlobalJson();
+ tempDir.CreateFile("Util.cs").WriteAllText("""
+ #:property TargetFramework=net10.0
+ #:property OutputType=Library
+ public static class Util
+ {
+ public static string M() => "Util";
+ }
+ """);
+ var sourceText = """
+ #:property TargetFramework=net10.0
+ #:property ExperimentalFileBasedProgramEnableRefDirective=true
+ #:ref Util.cs
+ Console.WriteLine($"Hello {Util.M()}!");
+ """;
+ var sourceFile = tempDir.CreateFile("SomeFile.cs").WriteAllText(sourceText);
+
+ // Until we can discover the `#:ref`erenced project, build it so it works as metadata reference.
+ var dotnetCliHelper = testLspServer.GetRequiredLspService();
+ using (var process = dotnetCliHelper.Run(["build", sourceFile.Path], workingDirectory: tempDir.Path, shouldLocalizeOutput: true))
+ {
+ var sb = new StringBuilder();
+ process.OutputDataReceived += (sender, args) => sb.AppendLine($"> {args.Data}");
+ process.ErrorDataReceived += (sender, args) => sb.AppendLine($"! {args.Data}");
+ process.BeginOutputReadLine();
+ process.BeginErrorReadLine();
+ await process.WaitForExitAsync();
+ Assert.True(process.ExitCode == 0, sb.ToString());
+ }
+
+ var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(sourceFile.Path);
+ await testLspServer.OpenDocumentAsync(looseFileUri, sourceText).ConfigureAwait(false);
+ await WaitForProjectLoad(looseFileUri, testLspServer);
+ var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false);
+ Assert.NotEmpty(document.Project.MetadataReferences);
+ var model = await document.GetRequiredSemanticModelAsync(CancellationToken.None);
+ model.GetDiagnostics().Verify();
+ }
+
[Theory, CombinatorialData]
public async Task TestDirectiveWithoutTopLevelStatements_IsMiscellaneousFile(bool mutatingLspWorkspace)
{
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/VirtualProjectXmlProviderTests.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/VirtualProjectXmlProviderTests.cs
deleted file mode 100644
index 95aab87f631dd..0000000000000
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/VirtualProjectXmlProviderTests.cs
+++ /dev/null
@@ -1,171 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using System.Text;
-using Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms;
-using Microsoft.Extensions.Logging;
-using Roslyn.Test.Utilities;
-using Xunit.Abstractions;
-
-namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests;
-
-///
-/// Goal of these tests:
-/// - Ensure that the various request/response forms work as expected in basic scenarios.
-/// - Ensure that various properties on the response are populated in a reasonable way.
-/// Non-goals:
-/// - Thorough behavioral testing.
-/// - Testing of more intricate behaviors which are subject to change.
-///
-public sealed class VirtualProjectXmlProviderTests(ITestOutputHelper testOutputHelper) : AbstractLanguageServerHostTests(testOutputHelper)
-{
- private async Task GetProjectXmlProviderAsync()
- {
- var exportProvider = LanguageServerTestComposition.GetSharedExportProvider(DefaultServerConfiguration, LoggerFactory);
- return exportProvider.GetExportedValue();
- }
-
- private DotnetCliHelper GetDotnetCliHelper()
- => new(LoggerFactory);
-
- [Fact(Skip = "https://github.com/dotnet/roslyn/issues/79464")]
- public async Task GetProjectXml_FileBasedProgram_SdkTooOld_01()
- {
- var projectProvider = await GetProjectXmlProviderAsync();
-
- var tempDir = TempRoot.CreateDirectory();
- var appFile = tempDir.CreateFile("app.cs");
- await appFile.WriteAllTextAsync("""
- Console.WriteLine("Hello, world!");
- """);
-
- var globalJsonFile = tempDir.CreateFile("global.json");
- globalJsonFile.WriteAllBytes(Encoding.UTF8.GetBytes("""
- {
- "sdk": {
- "version": "9.0.105"
- }
- }
- """));
-
- var contentNullable = await projectProvider.GetVirtualProjectContentAsync(appFile.Path, GetDotnetCliHelper(), LoggerFactory.CreateLogger(), localizeOutput: false, CancellationToken.None);
- Assert.Null(contentNullable);
- }
-
- [Fact]
- public async Task GetProjectXml_FileBasedProgram_01()
- {
- var projectProvider = await GetProjectXmlProviderAsync();
-
- var tempDir = TempRoot.CreateDirectory();
- var appFile = tempDir.CreateFile("app.cs");
- await appFile.WriteAllTextAsync("""
- Console.WriteLine("Hello, world!");
- """);
-
- var globalJsonFile = tempDir.CreateFile("global.json");
- await globalJsonFile.WriteAllTextAsync("""
- {
- "sdk": {
- "version": "10.0.301"
- }
- }
- """);
-
- var logger = LoggerFactory.CreateLogger();
- var contentNullable = await projectProvider.GetVirtualProjectContentAsync(appFile.Path, GetDotnetCliHelper(), logger, localizeOutput: false, CancellationToken.None);
- Assert.NotNull(contentNullable);
- var content = contentNullable.Value;
- var virtualProjectXml = content.VirtualProjectXml;
- logger.LogTrace(virtualProjectXml);
-
- Assert.Contains("net10.0", virtualProjectXml);
- Assert.Contains("", virtualProjectXml);
- Assert.Empty(content.Diagnostics);
- }
-
- [Fact]
- public async Task GetProjectXml_NonFileBasedProgram_01()
- {
- var projectProvider = await GetProjectXmlProviderAsync();
-
- var tempDir = TempRoot.CreateDirectory();
- var appFile = tempDir.CreateFile("app.cs");
- await appFile.WriteAllTextAsync("""
- public class C
- {
- }
- """);
-
- var globalJsonFile = tempDir.CreateFile("global.json");
- await globalJsonFile.WriteAllTextAsync("""
- {
- "sdk": {
- "version": "10.0.301"
- }
- }
- """);
-
- var contentNullable = await projectProvider.GetVirtualProjectContentAsync(appFile.Path, GetDotnetCliHelper(), LoggerFactory.CreateLogger(), localizeOutput: false, CancellationToken.None);
- Assert.NotNull(contentNullable);
- var content = contentNullable.Value;
- LoggerFactory.CreateLogger().LogTrace(content.VirtualProjectXml);
-
- Assert.Contains("net10.0", content.VirtualProjectXml);
- Assert.Contains("", content.VirtualProjectXml);
- Assert.Empty(content.Diagnostics);
- }
-
- [Fact]
- public async Task GetProjectXml_BadPath_01()
- {
- var projectProvider = await GetProjectXmlProviderAsync();
-
- var tempDir = TempRoot.CreateDirectory();
-
- var globalJsonFile = tempDir.CreateFile("global.json");
- await globalJsonFile.WriteAllTextAsync("""
- {
- "sdk": {
- "version": "10.0.301"
- }
- }
- """);
-
- var content = await projectProvider.GetVirtualProjectContentAsync(Path.Combine(tempDir.Path, "BAD"), GetDotnetCliHelper(), LoggerFactory.CreateLogger(), localizeOutput: false, CancellationToken.None);
- Assert.Null(content);
- }
-
- [Fact]
- public async Task GetProjectXml_BadDirective_01()
- {
- var projectProvider = await GetProjectXmlProviderAsync();
-
- var tempDir = TempRoot.CreateDirectory();
- var appFile = tempDir.CreateFile("app.cs");
- await appFile.WriteAllTextAsync("""
- #:package Newtonsoft.Json@13.0.3
- #:BAD
- Console.WriteLine("Hello, world!");
- """);
-
- var globalJsonFile = tempDir.CreateFile("global.json");
- await globalJsonFile.WriteAllTextAsync("""
- {
- "sdk": {
- "version": "10.0.301"
- }
- }
- """);
-
- var contentNullable = await projectProvider.GetVirtualProjectContentAsync(appFile.Path, GetDotnetCliHelper(), LoggerFactory.CreateLogger(), localizeOutput: false, CancellationToken.None);
- Assert.NotNull(contentNullable);
- var content = contentNullable.Value;
- var diagnostic = content.Diagnostics.Single();
- Assert.Contains("Unrecognized directive 'BAD'", diagnostic.Message);
- Assert.Equal(appFile.Path, diagnostic.Location.Path);
-
- Assert.Equal("(1,0)-(2,0)", diagnostic.Location.Span.ToLinePositionSpan().ToString());
- }
-}
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscellaneousFilesProjectProvider.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscellaneousFilesProjectProvider.cs
index f4f432ab65167..692f069367955 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscellaneousFilesProjectProvider.cs
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscellaneousFilesProjectProvider.cs
@@ -74,7 +74,7 @@ private async Task> LoadCanonicalProjectAsync(Ca
binaryLogPathProvider: null,
loggerFactory: _loggerFactory);
var buildHost = await buildHostProcessManager.GetBuildHostAsync(BuildHostProcessKind.NetCore, virtualProjectPath, dotnetPath: null, cancellationToken);
- var loadedFile = await buildHost.LoadProjectAsync(virtualProjectPath, virtualProjectXml, languageName: LanguageNames.CSharp, cancellationToken);
+ var loadedFile = await buildHost.LoadProjectAsync(virtualProjectPath, physicalFilePath: null, virtualProjectXml, languageName: LanguageNames.CSharp, globalProperties: null, cancellationToken);
return await loadedFile.GetProjectFileInfosAsync(cancellationToken);
}
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsEntryPointDiscovery.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsEntryPointDiscovery.cs
index d8f5805e842fb..b23bdc7f74739 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsEntryPointDiscovery.cs
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsEntryPointDiscovery.cs
@@ -5,14 +5,14 @@
using System.Buffers;
using System.Collections.Immutable;
using System.Composition;
-using System.Diagnostics;
using System.IO.Enumeration;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.ErrorReporting;
-using Microsoft.CodeAnalysis.Features.Workspaces;
+using Microsoft.CodeAnalysis.FileBasedPrograms;
+using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
@@ -20,7 +20,6 @@
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Shared.Utilities;
-using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.Logging;
using Roslyn.LanguageServer.Protocol;
using Roslyn.Utilities;
@@ -35,12 +34,21 @@ internal sealed class FileBasedProgramsEntryPointDiscoveryFactory(IGlobalOptionS
{
public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind)
{
- return new FileBasedProgramsEntryPointDiscovery(globalOptionService, listenerProvider.GetListener(FeatureAttribute.Workspace), lspServices.GetRequiredService(), lspServices);
+ return new FileBasedProgramsEntryPointDiscovery(
+ globalOptionService,
+ listenerProvider.GetListener(FeatureAttribute.Workspace),
+ lspServices.GetRequiredService().Workspace.Services.GetRequiredService(),
+ lspServices.GetRequiredService(),
+ lspServices);
}
}
internal sealed partial class FileBasedProgramsEntryPointDiscovery(
- IGlobalOptionService globalOptionService, IAsynchronousOperationListener listener, ILoggerFactory loggerFactory, LspServices lspServices) : ILspService, IOnInitialized
+ IGlobalOptionService globalOptionService,
+ IAsynchronousOperationListener listener,
+ IFileBasedProgramService fileBasedProgramService,
+ ILoggerFactory loggerFactory,
+ LspServices lspServices) : ILspService, IOnInitialized
{
private static readonly StringComparer s_pathComparer = StringComparer.OrdinalIgnoreCase;
@@ -114,7 +122,7 @@ internal async Task FindAndLoadEntryPointsAsync()
// Discovery pass done. Find and delete old caches.
IOUtilities.PerformIO(() =>
{
- using var enumerator = new OldCacheEnumerator();
+ using var enumerator = new OldCacheEnumerator(fileBasedProgramService);
while (enumerator.MoveNext())
{
IOUtilities.PerformIO(() => Directory.Delete(enumerator.Current, recursive: true));
@@ -122,8 +130,8 @@ internal async Task FindAndLoadEntryPointsAsync()
});
}
- private sealed class OldCacheEnumerator() : FileSystemEnumerator(
- directory: VirtualProjectXmlProvider.GetDiscoveryCacheRootDirectory(),
+ private sealed class OldCacheEnumerator(IFileBasedProgramService fileBasedProgramService) : FileSystemEnumerator(
+ directory: fileBasedProgramService.GetDiscoveryCacheRootDirectory(),
options: new() { RecurseSubdirectories = false })
{
// Yield cache directories that have not been modified in 30 days (indicates they are stale and should be deleted)
@@ -140,7 +148,7 @@ protected override bool ShouldIncludeEntry(ref FileSystemEntry entry)
internal ImmutableArray FindEntryPoints(string workspaceFolder)
{
var stopwatch = SharedStopwatch.StartNew();
- var cacheDirectory = VirtualProjectXmlProvider.GetDiscoveryCacheDirectory(workspaceFolder);
+ var cacheDirectory = fileBasedProgramService.GetDiscoveryCacheDirectory(workspaceFolder);
var cacheFilePath = Path.Join(cacheDirectory, "cache.json");
Cache? cache = null;
try
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs
index 338eec8363f11..76bdb60e84d21 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs
@@ -6,12 +6,10 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Features.Workspaces;
+using Microsoft.CodeAnalysis.FileBasedPrograms;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
-using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry;
using Microsoft.CodeAnalysis.Options;
-using Microsoft.CodeAnalysis.ProjectSystem;
-using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
@@ -28,9 +26,7 @@ internal sealed class FileBasedProgramsProjectSystem : LanguageServerProjectLoad
{
private readonly ILspServices _lspServices;
private readonly ILogger _logger;
- private readonly VirtualProjectXmlProvider _projectXmlProvider;
private readonly CanonicalMiscellaneousFilesProjectProvider _canonicalProjectProvider;
- private readonly DotnetCliHelper _dotnetCliHelper;
///
/// Virtual (in-memory) projects don't exist on disk, so MSBuild worker nodes
@@ -40,7 +36,6 @@ internal sealed class FileBasedProgramsProjectSystem : LanguageServerProjectLoad
public FileBasedProgramsProjectSystem(
ILspServices lspServices,
- VirtualProjectXmlProvider projectXmlProvider,
IGlobalOptionService globalOptionService,
ILoggerFactory loggerFactory,
IAsynchronousOperationListenerProvider listenerProvider,
@@ -58,9 +53,7 @@ public FileBasedProgramsProjectSystem(
{
_lspServices = lspServices;
_logger = loggerFactory.CreateLogger();
- _projectXmlProvider = projectXmlProvider;
_canonicalProjectProvider = new CanonicalMiscellaneousFilesProjectProvider(lspServices.GetRequiredService(), loggerFactory);
- _dotnetCliHelper = dotnetCliHelper;
globalOptionService.AddOptionChangedHandler(this, OnGlobalOptionChanged);
}
@@ -328,7 +321,8 @@ public async ValueTask CloseDocumentAsync(DocumentUri uri)
// For telemetry purposes, we will consider this file a file-based app, if we see that build artifacts exist for it in the default location.
// This implies that the user used a command like `dotnet run app.cs` with it recently.
var isFileBasedProgram = PathUtilities.IsAbsolute(documentPath)
- && Directory.Exists(VirtualProjectXmlProvider.GetArtifactsPath(documentPath));
+ && _workspaceFactory.HostWorkspace.Services.GetService() is { } fileBasedProgramService
+ && Directory.Exists(fileBasedProgramService.GetArtifactsPath(documentPath));
return new RemoteProjectLoadResult
{
@@ -349,22 +343,14 @@ public async ValueTask CloseDocumentAsync(DocumentUri uri)
// Fall through to ordinary file-based app handling.
Contract.ThrowIfFalse(documentKind is LooseDocumentKind.FileBasedApp);
- var content = await _projectXmlProvider.GetVirtualProjectContentAsync(documentPath, _dotnetCliHelper, _logger, localizeOutput: true, cancellationToken);
- if (content is not var (virtualProjectContent, virtualProjectPath, diagnostics))
- {
- _logger.LogError("Failed to obtain virtual project for '{documentPath}' using dotnet run-api.", documentPath);
- return null;
- }
-
- foreach (var diagnostic in diagnostics)
- {
- _logger.LogError($"{diagnostic.Location.Path}{diagnostic.Location.Span.Start}: {diagnostic.Message}");
- }
-
- virtualProjectPath ??= VirtualProjectXmlProvider.GetFallbackVirtualProjectPath(documentPath);
const BuildHostProcessKind buildHostKind = BuildHostProcessKind.NetCore;
- var buildHost = await buildHostProcessManager.GetBuildHostAsync(buildHostKind, virtualProjectPath, dotnetPath: null, cancellationToken);
- var loadedFile = await buildHost.LoadProjectAsync(virtualProjectPath, virtualProjectContent, languageName: LanguageNames.CSharp, cancellationToken);
+ var buildHost = await buildHostProcessManager.GetBuildHostAsync(buildHostKind, documentPath, dotnetPath: null, cancellationToken);
+ var loadedFile = await FileBasedProgramsProjectLoader.LoadFileBasedAppProjectAsync(
+ buildHost,
+ _workspaceFactory.HostWorkspace.Services.GetRequiredService(),
+ documentPath,
+ (error) => _logger.LogError(error),
+ cancellationToken);
return new RemoteProjectLoadResult
{
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsWorkspaceProviderFactory.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsWorkspaceProviderFactory.cs
index effa964fd22db..4ac18f6f04339 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsWorkspaceProviderFactory.cs
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsWorkspaceProviderFactory.cs
@@ -18,7 +18,6 @@ namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms;
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class FileBasedProgramsWorkspaceProviderFactory(
- VirtualProjectXmlProvider projectXmlProvider,
IGlobalOptionService globalOptionService,
IAsynchronousOperationListenerProvider listenerProvider,
ServerConfigurationFactory serverConfigurationFactory) : ILspServiceFactory
@@ -27,7 +26,6 @@ public ILspService CreateILspService(LspServices lspServices, WellKnownLspServer
{
return new FileBasedProgramsProjectSystem(
lspServices,
- projectXmlProvider,
globalOptionService,
lspServices.GetRequiredService(),
listenerProvider,
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/IFileBasedProgramServiceExtensions.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/IFileBasedProgramServiceExtensions.cs
new file mode 100644
index 0000000000000..487c65df92bb7
--- /dev/null
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/IFileBasedProgramServiceExtensions.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.CodeAnalysis.FileBasedPrograms;
+
+namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms;
+
+internal static class IFileBasedProgramServiceExtensions
+{
+ internal static string GetDiscoveryCacheDirectory(this IFileBasedProgramService fileBasedProgramService, string workspaceFolder)
+ => fileBasedProgramService.GetArtifactsPath(workspaceFolder, "runfile-discovery");
+
+ internal static string GetDiscoveryCacheRootDirectory(this IFileBasedProgramService fileBasedProgramService)
+ => fileBasedProgramService.GetTempSubdirectory("runfile-discovery");
+}
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/VirtualProjectXmlProvider.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/VirtualProjectXmlProvider.cs
deleted file mode 100644
index a4895cf6d9eaa..0000000000000
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/VirtualProjectXmlProvider.cs
+++ /dev/null
@@ -1,157 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using System.Collections.Immutable;
-using System.Composition;
-using System.Diagnostics.CodeAnalysis;
-using System.Runtime.InteropServices;
-using System.Security.Cryptography;
-using System.Text;
-using System.Text.Json;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.CSharp.Syntax;
-using Microsoft.CodeAnalysis.Host.Mef;
-using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
-using Microsoft.CodeAnalysis.Options;
-using Microsoft.CodeAnalysis.Text;
-using Microsoft.Extensions.Logging;
-using Roslyn.Utilities;
-
-namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms;
-
-[Export(typeof(VirtualProjectXmlProvider)), Shared]
-[method: ImportingConstructor]
-[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
-internal class VirtualProjectXmlProvider()
-{
- internal async Task<(string VirtualProjectXml, string? VirtualProjectPath, ImmutableArray Diagnostics)?> GetVirtualProjectContentAsync(
- string documentFilePath,
- DotnetCliHelper dotnetCliHelper,
- ILogger logger,
- bool localizeOutput,
- CancellationToken cancellationToken)
- {
- var workingDirectory = Path.GetDirectoryName(documentFilePath);
- var process = dotnetCliHelper.Run(["run-api"], workingDirectory, localizeOutput, keepStandardInputOpen: true);
-
- cancellationToken.Register(() =>
- {
- process?.Kill();
- });
-
- var input = new RunApiInput.GetProject() { EntryPointFileFullPath = documentFilePath };
- var inputJson = JsonSerializer.Serialize(input, RunFileApiJsonSerializerContext.Default.RunApiInput);
- await process.StandardInput.WriteAsync(inputJson);
- process.StandardInput.Close();
-
- // Debug severity is used for these because we think it will be common for the user environment to have too old of an SDK for the call to work.
- // Rather than representing a hard error condition, it represents a condition where we need to gracefully downgrade the experience.
- process.ErrorDataReceived += (sender, args) => logger.LogDebug($"[stderr] dotnet run-api: {args.Data}");
- process.BeginErrorReadLine();
-
- var responseJson = await process.StandardOutput.ReadLineAsync(cancellationToken);
- await process.WaitForExitAsync(cancellationToken);
-
- if (process.ExitCode != 0)
- {
- logger.LogDebug($"dotnet run-api exited with exit code '{process.ExitCode}'.");
- return null;
- }
-
- if (string.IsNullOrWhiteSpace(responseJson))
- {
- logger.LogError($"dotnet run-api exited with exit code 0, but did not return any response.");
- return null;
- }
-
- try
- {
- var response = JsonSerializer.Deserialize(responseJson, RunFileApiJsonSerializerContext.Default.RunApiOutput);
- if (response is RunApiOutput.Error error)
- {
- logger.LogError($"dotnet run-api version: {error.Version}. Latest known version: {RunApiOutput.LatestKnownVersion}");
- logger.LogError($"dotnet run-api returned error: '{error.Message}'");
- return null;
- }
-
- if (response is RunApiOutput.Project project)
- {
- return (project.Content, project.ProjectPath, project.Diagnostics);
- }
-
- throw ExceptionUtilities.UnexpectedValue(response);
- }
- catch (JsonException ex)
- {
- // In this case, run-api returned 0 exit code, but gave us back JSON that we don't know how to parse.
- logger.LogError(ex, "Could not deserialize run-api response.");
- logger.LogTrace($"""
- Full run-api response:
- {responseJson}
- """);
- return null;
- }
- }
-
- ///
- /// From a C# document path, get the virtual project path to pass to MSBuild. Used only for the scenario where run-api doesn't give us a VirtualProjectPath.
- /// Note: "Older" logic is used here, because, it's presumed that if run-api isn't giving us a path, it is using the older version of the logic.
- /// See also https://github.com/dotnet/sdk/pull/53182
- ///
- internal static string GetFallbackVirtualProjectPath(string documentFilePath)
- => Path.ChangeExtension(documentFilePath, ".csproj");
-
- #region Temporary copy of subset of dotnet run-api behavior
- // See https://github.com/dotnet/sdk/blob/b5dbc69cc28676ac6ea615654c8016a11b75e747/src/Cli/Microsoft.DotNet.Cli.Utils/Sha256Hasher.cs#L10
- private static class Sha256Hasher
- {
- public static string Hash(string text)
- {
- byte[] bytes = Encoding.UTF8.GetBytes(text);
- byte[] hash = SHA256.HashData(bytes);
-#if NET10_0_OR_GREATER
- return Convert.ToHexStringLower(hash);
-#else
- return Convert.ToHexString(hash).ToLowerInvariant();
-#endif
- }
-
- public static string HashWithNormalizedCasing(string text)
- {
- return Hash(text.ToUpperInvariant());
- }
- }
-
- internal static string GetDiscoveryCacheDirectory(string workspaceFolder)
- => GetTempPathCore("runfile-discovery", workspaceFolder);
-
- internal static string GetDiscoveryCacheRootDirectory()
- => GetTempDotnetSubdirectory("runfile-discovery");
-
- // See https://github.com/dotnet/sdk/blob/5a4292947487a9d34f4256c1d17fb3dc26859174/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs#L449
- internal static string GetArtifactsPath(string entryPointFileFullPath)
- => GetTempPathCore("runfile", entryPointFileFullPath);
-
- private static string GetTempDotnetSubdirectory(string dotnetSubdirectory)
- {
- // We want a location where permissions are expected to be restricted to the current user.
- string tempDirectory = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
- ? Path.GetTempPath()
- : Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
- return Path.Join(tempDirectory, "dotnet", dotnetSubdirectory);
- }
-
- private static string GetTempPathCore(string dotnetSubdirectory, string originalFilePath)
- {
- // Include original file name so the directory name is not completely opaque.
- string fileName = Path.GetFileNameWithoutExtension(originalFilePath);
- string hash = Sha256Hasher.HashWithNormalizedCasing(originalFilePath);
- string directoryName = $"{fileName}-{hash}";
-
- return Path.Join(GetTempDotnetSubdirectory(dotnetSubdirectory), directoryName);
- }
-
- #endregion
-}
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs
index 786119f5e6e44..20ce888ce483c 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs
@@ -4,11 +4,10 @@
using System.Collections.Immutable;
using System.Composition;
+using Microsoft.CodeAnalysis.FileBasedPrograms;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
-using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry;
using Microsoft.CodeAnalysis.Options;
-using Microsoft.CodeAnalysis.ProjectSystem;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
using Microsoft.CommonLanguageServerProtocol.Framework;
@@ -68,7 +67,7 @@ public LanguageServerProjectSystem(
_hostProjectFactory = lspServices.GetRequiredService().HostProjectFactory;
_clientLanguageServerManager = lspServices.GetRequiredService();
var workspace = _hostProjectFactory.Workspace;
- _projectFileExtensionRegistry = new ProjectFileExtensionRegistry(new DiagnosticReporter(workspace));
+ _projectFileExtensionRegistry = new ProjectFileExtensionRegistry(new DiagnosticReporter(workspace), workspace.Services.GetService());
}
///
diff --git a/src/Features/CSharp/Portable/Diagnostics/Analyzers/FileBasedPrograms/ExternalHelpers.cs b/src/Workspaces/CSharp/Portable/FileBasedPrograms/ExternalHelpers.cs
similarity index 100%
rename from src/Features/CSharp/Portable/Diagnostics/Analyzers/FileBasedPrograms/ExternalHelpers.cs
rename to src/Workspaces/CSharp/Portable/FileBasedPrograms/ExternalHelpers.cs
diff --git a/src/Workspaces/CSharp/Portable/FileBasedPrograms/FileBasedProgramService.cs b/src/Workspaces/CSharp/Portable/FileBasedPrograms/FileBasedProgramService.cs
new file mode 100644
index 0000000000000..ae5722cdac1db
--- /dev/null
+++ b/src/Workspaces/CSharp/Portable/FileBasedPrograms/FileBasedProgramService.cs
@@ -0,0 +1,53 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Composition;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Host.Mef;
+using Microsoft.DotNet.FileBasedPrograms;
+
+namespace Microsoft.CodeAnalysis.FileBasedPrograms;
+
+[ExportWorkspaceService(typeof(IFileBasedProgramService)), Shared]
+[method: ImportingConstructor]
+[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
+internal sealed class FileBasedProgramService() : IFileBasedProgramService
+{
+ public string GetArtifactsPath(string entryPointFileFullPath, string? dotNetSubdirectory = null)
+ => VirtualProjectBuilder.GetArtifactsPath(entryPointFileFullPath, dotNetSubdirectory);
+
+ public string GetTempSubdirectory(string? dotNetSubdirectory = null)
+ => VirtualProjectBuilder.GetTempSubdirectory(dotNetSubdirectory);
+
+ public IDictionary GetGlobalBuildProperties()
+ {
+ var result = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (var kvp in VirtualProjectBuilder.GetGlobalBuildProperties())
+ {
+ result.Add(kvp.Key, kvp.Value);
+ }
+ return result;
+ }
+
+ public bool IsValidEntryPointPath(string entryPointFilePath)
+ => VirtualProjectBuilder.IsValidEntryPointPath(entryPointFilePath);
+
+ public async ValueTask LoadFileBasedAppProjectAsync(
+ IBuildService buildService,
+ IProjectCollection projectCollection,
+ string entryPointFilePath,
+ Action reportError)
+ {
+ var entryPointFileFullPath = Path.GetFullPath(entryPointFilePath);
+ var virtualProjectBuilder = new VirtualProjectBuilder(buildService, entryPointFileFullPath, targetFramework: null);
+ var result = await virtualProjectBuilder.CreateProjectInstanceAsync(
+ projectCollection,
+ (text, path, textSpan, message, innerException) => reportError($"{new SourceFile(path, text).GetLocationString(textSpan)}: {message}"))
+ .ConfigureAwait(false);
+ return result.ProjectRootElement;
+ }
+}
diff --git a/src/Workspaces/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.Workspaces.csproj b/src/Workspaces/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.Workspaces.csproj
index a4042e0cf48bc..864f257eb703c 100644
--- a/src/Workspaces/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.Workspaces.csproj
+++ b/src/Workspaces/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.Workspaces.csproj
@@ -64,6 +64,13 @@
+
+
+
+
diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/.editorconfig b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/.editorconfig
similarity index 100%
rename from src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/.editorconfig
rename to src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/.editorconfig
diff --git a/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/Extensions.cs b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/Extensions.cs
new file mode 100644
index 0000000000000..630b4a6fc2e56
--- /dev/null
+++ b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/Extensions.cs
@@ -0,0 +1,17 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable enable
+using System.Collections.Generic;
+
+namespace Microsoft.DotNet.Utilities;
+
+internal static class Extensions
+{
+#if !NET
+ public static HashSet ToHashSet(this IEnumerable source, IEqualityComparer comparer)
+ {
+ return new HashSet(source, comparer);
+ }
+#endif
+}
diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/ExternalHelpers.cs b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/ExternalHelpers.cs
similarity index 100%
rename from src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/ExternalHelpers.cs
rename to src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/ExternalHelpers.cs
diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/FileBasedProgramsResources.resx b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/FileBasedProgramsResources.resx
similarity index 89%
rename from src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/FileBasedProgramsResources.resx
rename to src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/FileBasedProgramsResources.resx
index c686b49b6ccc6..14d31cc6d1b13 100644
--- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/FileBasedProgramsResources.resx
+++ b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/FileBasedProgramsResources.resx
@@ -165,6 +165,14 @@
The '#:project' directive is invalid: {0}{0} is the inner error message.
+
+ The '#:ref' directive is invalid: {0}
+ {Locked="#:ref"}{0} is the inner error message.
+
+
+ Could not find file '{0}'.
+ {0} is the file path.
+
Missing name of '{0}'.{0} is the directive name like 'package' or 'sdk'.
@@ -175,14 +183,25 @@
Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="}
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'='"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."}
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'.'"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid.{Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}
+
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+
+
+ File included via #:include directive (or Compile item) not found: {0}
+ {Locked="#:include"}{Locked="Compile"}. {0} is file path.
+
+
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ {Locked="MSBuild"}{Locked="true"}. {0} is MSBuild property name.
+
diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/FileLevelDirectiveHelpers.cs b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/FileLevelDirectiveHelpers.cs
similarity index 77%
rename from src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/FileLevelDirectiveHelpers.cs
rename to src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/FileLevelDirectiveHelpers.cs
index 99e7ba172924e..d5beaac92f675 100644
--- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/FileLevelDirectiveHelpers.cs
+++ b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/FileLevelDirectiveHelpers.cs
@@ -10,7 +10,6 @@
using System.IO;
using System.Linq;
using System.Text;
-using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Xml;
using Microsoft.CodeAnalysis;
@@ -36,15 +35,15 @@ public static SyntaxTokenParser CreateTokenizer(SourceText text)
/// The latter is useful for dotnet run file.cs where if there are app directives after the first token,
/// compiler reports anyway, so we speed up success scenarios by not parsing the whole file up front in the SDK CLI.
///
- public static ImmutableArray FindDirectives(SourceFile sourceFile, bool reportAllErrors, ErrorReporter errorReporter)
+ public static ImmutableArray FindDirectives(SourceFile sourceFile, bool reportAllErrors, ErrorReporter errorReporter, bool checkDuplicates = true)
{
var builder = ImmutableArray.CreateBuilder();
- var tokenizer = CreateTokenizer(sourceFile.Text);
+ using var tokenizer = CreateTokenizer(sourceFile.Text);
var result = tokenizer.ParseLeadingTrivia();
var triviaList = result.Token.LeadingTrivia;
- FindLeadingDirectives(sourceFile, triviaList, errorReporter, builder);
+ FindLeadingDirectives(sourceFile, triviaList, errorReporter, builder, checkDuplicates);
// In conversion mode, we want to report errors for any invalid directives in the rest of the file
// so users don't end up with invalid directives in the converted project.
@@ -77,7 +76,6 @@ void ReportErrorFor(SyntaxTrivia trivia)
}
}
- // The result should be ordered by source location, RemoveDirectivesFromFile depends on that.
return builder.ToImmutable();
}
@@ -87,9 +85,10 @@ public static void FindLeadingDirectives(
SourceFile sourceFile,
SyntaxTriviaList triviaList,
ErrorReporter errorReporter,
- ImmutableArray.Builder? builder)
+ ImmutableArray.Builder? builder,
+ bool checkDuplicates = true)
{
- var deduplicated = new Dictionary(NamedDirectiveComparer.Instance);
+ var deduplicator = new DirectiveDeduplicator();
TextSpan previousWhiteSpaceSpan = default;
for (var index = 0; index < triviaList.Count; index++)
@@ -112,7 +111,7 @@ public static void FindLeadingDirectives(
{
TextSpan span = GetFullSpan(previousWhiteSpaceSpan, trivia);
- var whiteSpace = GetWhiteSpaceInfo(triviaList, index);
+ var whiteSpace = GetWhiteSpaceInfo(triviaList, index, span);
var info = new CSharpDirective.ParseInfo
{
SourceFile = sourceFile,
@@ -134,7 +133,7 @@ public static void FindLeadingDirectives(
var value = parts.Length > 1 ? parts[1] : "";
Debug.Assert(!(parts.Length > 2));
- var whiteSpace = GetWhiteSpaceInfo(triviaList, index);
+ var whiteSpace = GetWhiteSpaceInfo(triviaList, index, span);
var context = new CSharpDirective.ParseContext
{
Info = new()
@@ -157,15 +156,9 @@ public static void FindLeadingDirectives(
if (CSharpDirective.Parse(context) is { } directive)
{
- // If the directive is already present, report an error.
- if (deduplicated.TryGetValue(directive, out var existingDirective))
+ if (checkDuplicates)
{
- var typeAndName = $"#:{existingDirective.GetType().Name.ToLowerInvariant()} {existingDirective.Name}";
- context.ReportError(directive.Info.Span, string.Format(FileBasedProgramsResources.DuplicateDirective, typeAndName));
- }
- else
- {
- deduplicated.Add(directive, directive);
+ deduplicator.CheckDirective(directive, errorReporter, shouldKeep: out _);
}
builder?.Add(directive);
@@ -183,35 +176,42 @@ static TextSpan GetFullSpan(TextSpan previousWhiteSpaceSpan, SyntaxTrivia trivia
return previousWhiteSpaceSpan.IsEmpty ? trivia.FullSpan : TextSpan.FromBounds(previousWhiteSpaceSpan.Start, trivia.FullSpan.End);
}
- static (WhiteSpaceInfo Leading, WhiteSpaceInfo Trailing) GetWhiteSpaceInfo(in SyntaxTriviaList triviaList, int index)
+ static (WhiteSpaceInfo Leading, WhiteSpaceInfo Trailing) GetWhiteSpaceInfo(in SyntaxTriviaList triviaList, int index, TextSpan excludeSpan)
{
(WhiteSpaceInfo Leading, WhiteSpaceInfo Trailing) result = default;
for (int i = index - 1; i >= 0; i--)
{
- if (!Fill(ref result.Leading, triviaList, i)) break;
+ if (!Fill(ref result.Leading, triviaList, i, excludeSpan)) break;
}
for (int i = index + 1; i < triviaList.Count; i++)
{
- if (!Fill(ref result.Trailing, triviaList, i)) break;
+ if (!Fill(ref result.Trailing, triviaList, i, excludeSpan)) break;
}
return result;
- static bool Fill(ref WhiteSpaceInfo info, in SyntaxTriviaList triviaList, int index)
+ static bool Fill(ref WhiteSpaceInfo info, in SyntaxTriviaList triviaList, int index, TextSpan excludeSpan)
{
var trivia = triviaList[index];
+
+ var length = trivia.FullSpan.Length - (trivia.FullSpan.Intersection(excludeSpan)?.Length ?? 0);
+
if (trivia.IsKind(SyntaxKind.EndOfLineTrivia))
{
- info.LineBreaks += 1;
- info.TotalLength += trivia.FullSpan.Length;
+ if (length != 0)
+ {
+ info.BlankLineLength += info.RestLength + length;
+ info.RestLength = 0;
+ }
+
return true;
}
if (trivia.IsKind(SyntaxKind.WhitespaceTrivia))
{
- info.TotalLength += trivia.FullSpan.Length;
+ info.RestLength += length;
return true;
}
@@ -256,8 +256,15 @@ internal static partial class Patterns
internal struct WhiteSpaceInfo
{
- public int LineBreaks;
- public int TotalLength;
+ ///
+ /// Size of whitespace that consists of only blank lines (i.e., lines that contain only whitespace).
+ ///
+ public int BlankLineLength;
+
+ ///
+ /// Size of the remaining whitespace on a not-entirely-blank line.
+ ///
+ public int RestLength;
}
///
@@ -271,11 +278,20 @@ internal abstract class CSharpDirective(in CSharpDirective.ParseInfo info)
public readonly struct ParseInfo
{
public required SourceFile SourceFile { get; init; }
+
///
/// Span of the full line including the trailing line break.
///
public required TextSpan Span { get; init; }
+
+ ///
+ /// Additional leading whitespace not included in .
+ ///
public required WhiteSpaceInfo LeadingWhiteSpace { get; init; }
+
+ ///
+ /// Additional trailing whitespace not included in .
+ ///
public required WhiteSpaceInfo TrailingWhiteSpace { get; init; }
}
@@ -301,6 +317,7 @@ public void ReportError(TextSpan span, string message)
case "property": return Property.Parse(context);
case "package": return Package.Parse(context);
case "project": return Project.Parse(context);
+ case "ref": return Ref.Parse(context);
case "include" or "exclude": return IncludeOrExclude.Parse(context);
default:
context.ReportError(string.Format(FileBasedProgramsResources.UnrecognizedDirective, context.DirectiveKind));
@@ -346,6 +363,8 @@ private static (string, string?)? ParseOptionalTwoParts(in ParseContext context,
public abstract override string ToString();
+ public virtual string KindToString() => GetType().Name.ToLowerInvariant();
+
///
/// #! directive.
///
@@ -565,6 +584,100 @@ void ReportError(string message)
public override string ToString() => $"#:project {Name}";
}
+ ///
+ /// #:ref directive. References another file-based app as a library.
+ ///
+ public sealed class Ref : Named
+ {
+ public const string ExperimentalFileBasedProgramEnableRefDirective = nameof(ExperimentalFileBasedProgramEnableRefDirective);
+
+ [SetsRequiredMembers]
+ public Ref(in ParseInfo info, string name) : base(info)
+ {
+ Name = name;
+ OriginalName = name;
+ }
+
+ ///
+ /// Preserved across calls, i.e.,
+ /// this is the original directive text as entered by the user.
+ ///
+ public string OriginalName { get; init; }
+
+ ///
+ /// This is the with MSBuild $(..) vars expanded.
+ ///
+ public string? ExpandedName { get; init; }
+
+ ///
+ /// The resolved full path to the referenced .cs file.
+ ///
+ public string? ResolvedPath { get; init; }
+
+ public static new Ref? Parse(in ParseContext context)
+ {
+ var directiveText = context.DirectiveText;
+ if (directiveText.IsWhiteSpace())
+ {
+ context.ReportError(string.Format(FileBasedProgramsResources.MissingDirectiveName, context.DirectiveKind));
+ return null;
+ }
+
+ return new Ref(context.Info, directiveText);
+ }
+
+ public enum NameKind
+ {
+ ///
+ /// Change and .
+ ///
+ Expanded = 1,
+
+ ///
+ /// Change and .
+ ///
+ Resolved = 2,
+
+ ///
+ /// Change only .
+ ///
+ Final = 3,
+ }
+
+ public Ref WithName(string name, NameKind kind)
+ {
+ return new Ref(Info, name)
+ {
+ OriginalName = OriginalName,
+ ExpandedName = kind == NameKind.Expanded ? name : ExpandedName,
+ ResolvedPath = kind == NameKind.Resolved ? name : ResolvedPath,
+ };
+ }
+
+ ///
+ /// Resolves the path relative to the source file's directory.
+ ///
+ public Ref EnsureResolvedPath(ErrorReporter errorReporter)
+ {
+ var sourcePath = Info.SourceFile.Path;
+ var sourceDirectory = Path.GetDirectoryName(sourcePath)
+ ?? throw new InvalidOperationException($"Source file path '{sourcePath}' does not have a containing directory.");
+
+ var resolvedFilePath = Path.GetFullPath(Path.Combine(sourceDirectory, Name.Replace('\\', '/')));
+
+ if (!File.Exists(resolvedFilePath))
+ {
+ errorReporter(Info.SourceFile.Text, sourcePath, Info.Span,
+ string.Format(FileBasedProgramsResources.InvalidRefDirective,
+ string.Format(FileBasedProgramsResources.CouldNotFindRefFile, resolvedFilePath)));
+ }
+
+ return WithName(resolvedFilePath, NameKind.Resolved);
+ }
+
+ public override string ToString() => $"#:ref {Name}";
+ }
+
public enum IncludeOrExcludeKind
{
Include,
@@ -576,14 +689,9 @@ public enum IncludeOrExcludeKind
///
public sealed class IncludeOrExclude(in ParseInfo info) : Named(info)
{
- public const string ExperimentalFileBasedProgramEnableIncludeDirective = nameof(ExperimentalFileBasedProgramEnableIncludeDirective);
- public const string ExperimentalFileBasedProgramEnableExcludeDirective = nameof(ExperimentalFileBasedProgramEnableExcludeDirective);
- public const string ExperimentalFileBasedProgramEnableTransitiveDirectives = nameof(ExperimentalFileBasedProgramEnableTransitiveDirectives);
- public const string ExperimentalFileBasedProgramEnableItemMapping = nameof(ExperimentalFileBasedProgramEnableItemMapping);
-
public const string MappingPropertyName = "FileBasedProgramsItemMapping";
- public static string DefaultMappingString => ".cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content";
+ public static string DefaultMappingString => ".cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content;.dll=Reference";
public static ImmutableArray<(string Extension, string ItemType)> DefaultMapping
{
@@ -597,6 +705,7 @@ public sealed class IncludeOrExclude(in ParseInfo info) : Named(info)
(".resx", "EmbeddedResource"),
(".json", "None"),
(".razor", "Content"),
+ (".dll", "Reference"),
];
}
@@ -693,7 +802,7 @@ private static IncludeOrExcludeKind KindFromString(string kind)
};
}
- public string KindToString()
+ public override string KindToString()
{
return Kind switch
{
@@ -724,7 +833,7 @@ public string KindToMSBuildString()
SourceFile sourceFile,
ErrorReporter errorReporter)
{
- var pairs = value.Split(';');
+ var pairs = value.Split([';'], StringSplitOptions.RemoveEmptyEntries);
var builder = ImmutableArray.CreateBuilder<(string Extension, string ItemType)>(pairs.Length);
@@ -764,6 +873,73 @@ void ReportError(string message)
}
}
+///
+/// Detects duplicate directives (by type and case-insensitive name)
+/// and reports errors via the provided when their values differ.
+///
+///
+/// #:project, #:ref, #:include, and #:exclude duplicates are allowed (MSBuild can handle them).
+///
+internal struct DirectiveDeduplicator
+{
+ private Dictionary? _seen;
+
+ ///
+ /// Checks for duplication and reports an error if a different unevaluated value was already seen.
+ ///
+ /// if a duplicate directive was already seen and this directive should be skipped.
+ public void CheckDirective(CSharpDirective.Named directive, ErrorReporter reportError, out bool shouldKeep)
+ {
+ if (directive is CSharpDirective.Project or CSharpDirective.Ref or CSharpDirective.IncludeOrExclude)
+ {
+ shouldKeep = true;
+ return;
+ }
+
+ _seen ??= new(NamedDirectiveComparer.Instance);
+
+ if (_seen.TryGetValue(directive, out var existingDirective))
+ {
+ if (HasSameValue(existingDirective, directive))
+ {
+ shouldKeep = false;
+ return;
+ }
+
+ var typeAndName = $"#:{existingDirective.KindToString()} {existingDirective.Name}";
+ reportError(directive.Info.SourceFile.Text, directive.Info.SourceFile.Path, directive.Info.Span,
+ string.Format(FileBasedProgramsResources.DuplicateDirective, typeAndName));
+
+ shouldKeep = false;
+ return;
+ }
+ else
+ {
+ _seen.Add(directive, directive);
+ }
+
+ shouldKeep = true;
+ }
+
+ private static bool HasSameValue(CSharpDirective.Named existingDirective, CSharpDirective.Named directive)
+ {
+ Debug.Assert(NamedDirectiveComparer.Instance.Equals(existingDirective, directive));
+ Debug.Assert(existingDirective is CSharpDirective.Sdk or CSharpDirective.Property or CSharpDirective.Package);
+ Debug.Assert(directive is CSharpDirective.Sdk or CSharpDirective.Property or CSharpDirective.Package);
+
+ return (existingDirective, directive) switch
+ {
+ (CSharpDirective.Sdk existing, CSharpDirective.Sdk current) =>
+ string.Equals(existing.Version, current.Version, StringComparison.Ordinal),
+ (CSharpDirective.Property existing, CSharpDirective.Property current) =>
+ string.Equals(existing.Value, current.Value, StringComparison.Ordinal),
+ (CSharpDirective.Package existing, CSharpDirective.Package current) =>
+ string.Equals(existing.Version, current.Version, StringComparison.Ordinal),
+ _ => false,
+ };
+ }
+}
+
///
/// Used for deduplication - compares directives by their type and name (ignoring case).
///
@@ -807,7 +983,9 @@ public readonly struct Position
{
public required string Path { get; init; }
public required LinePositionSpan Span { get; init; }
- [JsonIgnore]
+#if FILE_BASED_PROGRAMS_SYSTEM_TEXT_JSON // only run-api needs this, see remarks
+ [System.Text.Json.Serialization.JsonIgnore]
+#endif
public TextSpan TextSpan { get; init; }
}
}
diff --git a/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/IBuildService.cs b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/IBuildService.cs
new file mode 100644
index 0000000000000..65b07077d2a9a
--- /dev/null
+++ b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/IBuildService.cs
@@ -0,0 +1,86 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable enable
+
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Threading.Tasks;
+using System.Xml;
+
+namespace Microsoft.DotNet.FileBasedPrograms;
+
+///
+/// The root interface for our abstraction over MSBuild APIs.
+/// This is used by VirtualProjectBuilder so that it can be used
+/// both by dotnet CLI (which uses MSBuild APIs directly)
+/// and by roslyn IDE (which uses MSBuild through RPC).
+///
+#if FILE_BASED_PROGRAMS_PUBLIC
+public
+#else
+internal
+#endif
+interface IBuildService
+{
+ ///
+ /// An abstraction over MSBuild's ProjectInstance.FromProjectRootElement method.
+ ///
+ ///
+ /// NOTE: Unlike the MSBuild API, these properties are automatically merged with 's global properties
+ /// (the latter come first, so the former can overwrite them).
+ ///
+ ValueTask CreateProjectInstanceFromProjectRootElementAsync(
+ IProjectRootElement projectRoot,
+ IProjectCollection projectCollection,
+ IDictionary? additionalGlobalProperties);
+
+ ///
+ /// An abstraction over MSBuild's ProjectRootElement.Create method.
+ ///
+ IProjectRootElement CreateProjectRootElement(XmlReader xmlReader, IProjectCollection projectCollection, string entryPointFilePath);
+}
+
+///
+/// An abstraction of MSBuild's ProjectCollection.
+///
+///
+#if FILE_BASED_PROGRAMS_PUBLIC
+public
+#else
+internal
+#endif
+interface IProjectCollection
+{
+}
+
+///
+/// An abstraction of MSBuild's ProjectInstance.
+///
+///
+#if FILE_BASED_PROGRAMS_PUBLIC
+public
+#else
+internal
+#endif
+interface IProjectInstance
+{
+ ValueTask>> GetItemMetadataValuesAsync(string itemType, ImmutableArray metadataNames);
+ ValueTask GetPropertyValueAsync(string propertyName);
+ ValueTask ExpandStringAsync(string value);
+}
+
+///
+/// An abstraction of MSBuild's ProjectRootElement.
+///
+///
+#if FILE_BASED_PROGRAMS_PUBLIC
+public
+#else
+internal
+#endif
+interface IProjectRootElement
+{
+ string? FullPath { get; set; }
+ string GetRawXml();
+}
diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/MSBuildUtilities.cs b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/MSBuildUtilities.cs
similarity index 79%
rename from src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/MSBuildUtilities.cs
rename to src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/MSBuildUtilities.cs
index 34334d0e876b3..aa819c2b289e6 100644
--- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/MSBuildUtilities.cs
+++ b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/MSBuildUtilities.cs
@@ -1,11 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-// https://github.com/dotnet/sdk/issues/51487: avoid this extra copy of the file.
+// This file is shared between the SDK MSBuild build tasks and the Microsoft.DotNet.FileBasedPrograms source
+// package (which is also consumed by Roslyn and the dotnet CLI) to avoid a duplicate copy. See
+// https://github.com/dotnet/sdk/issues/51487. The build-tasks projects that link this file define
+// MSBUILD_BUILD_TASKS to select the Microsoft.DotNet.Cli namespace; every other consumer (the source
+// package, its external consumers, and ProjectTools) gets the Microsoft.DotNet.FileBasedPrograms form,
+// which matches the file as it originally shipped. The build tasks enable nullable and provide System via
+// ImplicitUsings, so emitting the directive and using there would be flagged as redundant (IDE0240/IDE0005).
+#if !MSBUILD_BUILD_TASKS
#nullable enable
using System;
+#endif
+#if MSBUILD_BUILD_TASKS
+namespace Microsoft.DotNet.Cli
+#else
namespace Microsoft.DotNet.FileBasedPrograms
+#endif
{
///
/// Internal utilities copied from microsoft/MSBuild repo.
diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/ProjectLocator.cs b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/ProjectLocator.cs
similarity index 100%
rename from src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/ProjectLocator.cs
rename to src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/ProjectLocator.cs
diff --git a/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/Sha256Hasher.cs b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/Sha256Hasher.cs
new file mode 100644
index 0000000000000..e42f412205d9d
--- /dev/null
+++ b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/Sha256Hasher.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable enable
+using System;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Microsoft.DotNet.Utilities;
+
+internal static class Sha256Hasher
+{
+ ///
+ /// The hashed mac address needs to be the same hashed value as produced by the other distinct sources given the same input. (e.g. VsCode)
+ ///
+ public static string Hash(string text)
+ {
+#if NET10_0_OR_GREATER
+ return Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(text)));
+#else
+ using var sha256 = SHA256.Create();
+ byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(text));
+ return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
+#endif
+ }
+
+ public static string HashWithNormalizedCasing(string text)
+ => Hash(text.ToUpperInvariant());
+}
diff --git a/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/VirtualProjectBuilder.cs b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/VirtualProjectBuilder.cs
new file mode 100644
index 0000000000000..e01aeaa06b80d
--- /dev/null
+++ b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/VirtualProjectBuilder.cs
@@ -0,0 +1,985 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Security;
+using System.Threading.Tasks;
+using System.Xml;
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.DotNet.Utilities;
+
+namespace Microsoft.DotNet.FileBasedPrograms;
+
+#if FILE_BASED_PROGRAMS_PUBLIC
+public
+#else
+internal
+#endif
+sealed class VirtualProjectBuilder
+{
+ internal readonly record struct ExplicitProjectItem(string ItemType, string Include);
+
+ internal const string FromIncludeDirectiveMetadataName = "FileBasedProgramsFromIncludeDirective";
+
+ internal const string FromRefDirectiveMetadataName = "FileBasedProgramsFromRefDirective";
+
+ private readonly IBuildService _buildService;
+
+ private readonly string? _targetFramework;
+
+ private (ImmutableArray Original, ImmutableArray Evaluated)? _evaluatedDirectives;
+
+ internal string EntryPointFileFullPath { get; }
+
+ internal SourceFile EntryPointSourceFile
+ {
+ get
+ {
+ if (field == default)
+ {
+ field = SourceFile.Load(EntryPointFileFullPath);
+ }
+
+ return field;
+ }
+ }
+
+ internal string ArtifactsPath
+ => field ??= GetArtifactsPath(EntryPointFileFullPath);
+
+ internal string[]? RequestedTargets { get; }
+
+ internal VirtualProjectBuilder(
+ IBuildService buildService,
+ string entryPointFileFullPath,
+ string? targetFramework,
+ string[]? requestedTargets = null,
+ string? artifactsPath = null,
+ SourceText? sourceText = null)
+ {
+ Debug.Assert(ExternalHelpers.IsPathFullyQualified(entryPointFileFullPath));
+
+ _buildService = buildService;
+ EntryPointFileFullPath = entryPointFileFullPath;
+ RequestedTargets = requestedTargets;
+ ArtifactsPath = artifactsPath;
+ _targetFramework = targetFramework;
+
+ if (sourceText != null)
+ {
+ EntryPointSourceFile = new SourceFile(entryPointFileFullPath, sourceText);
+ }
+ }
+
+ ///
+ /// Kept in sync with the default dotnet new console project file (enforced by DotnetProjectConvertTests.SameAsTemplate).
+ ///
+ internal static IEnumerable<(string name, string value)> GetDefaultProperties(string? targetFramework)
+ {
+ yield return ("OutputType", "Exe");
+ if (targetFramework != null) yield return ("TargetFramework", targetFramework);
+ yield return ("ImplicitUsings", "enable");
+ yield return ("Nullable", "enable");
+ yield return ("PublishAot", "true");
+ yield return ("PackAsTool", "true");
+ }
+
+ internal static IEnumerable> GetGlobalBuildProperties() =>
+ [
+ // See https://github.com/dotnet/msbuild/blob/main/documentation/specs/build-nonexistent-projects-by-default.md.
+ new KeyValuePair("_BuildNonexistentProjectsByDefault", bool.TrueString),
+ new KeyValuePair("RestoreUseSkipNonexistentTargets", bool.FalseString),
+ ];
+
+ internal static string GetArtifactsPath(string entryPointFileFullPath, string? dotNetSubdirectory = null)
+ {
+ // Include entry point file name so the directory name is not completely opaque.
+ string fileName = Path.GetFileNameWithoutExtension(entryPointFileFullPath);
+ string hash = Sha256Hasher.HashWithNormalizedCasing(entryPointFileFullPath);
+ string directoryName = $"{fileName}-{hash}";
+
+ return GetTempSubpath(name: directoryName, dotNetSubdirectory: dotNetSubdirectory);
+ }
+
+ private const string CsprojExtension = ".csproj";
+
+ public static string GetVirtualProjectPath(string entryPointFilePath)
+ => entryPointFilePath + CsprojExtension;
+
+ public static bool TryGetEntryPointFilePathFromVirtualProjectPath(string projectPath, [NotNullWhen(returnValue: true)] out string? entryPointFilePath)
+ {
+ if (projectPath.EndsWith(CsprojExtension, StringComparison.OrdinalIgnoreCase))
+ {
+ entryPointFilePath = projectPath[..^CsprojExtension.Length];
+ if (IsValidEntryPointPath(entryPointFilePath))
+ {
+ return true;
+ }
+ }
+
+ entryPointFilePath = null;
+ return false;
+ }
+
+ ///
+ /// Parses a source file to extract property value from directives.
+ ///
+ /// Array of frameworks if TargetFrameworks is specified, or empty otherwise
+ public static string? GetPropertyFromSourceFile(string sourceFilePath, string propertyName)
+ {
+ var sourceFile = SourceFile.Load(sourceFilePath);
+ var directives = FileLevelDirectiveHelpers.FindDirectives(sourceFile, reportAllErrors: false, ErrorReporters.IgnoringReporter);
+
+ // Return the first value. Conflicting duplicate directives are not supported.
+ return directives.OfType()
+ .FirstOrDefault(p => string.Equals(p.Name, propertyName, StringComparison.OrdinalIgnoreCase))?.Value;
+ }
+
+ ///
+ /// Obtains a temporary subdirectory for file-based app artifacts, e.g., /tmp/dotnet/runfile/.
+ ///
+ internal static string GetTempSubdirectory(string? dotNetSubdirectory = null)
+ {
+ // We want a location where permissions are expected to be restricted to the current user.
+ string directory = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+ ? Path.GetTempPath()
+ : Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+
+ if (string.IsNullOrEmpty(directory))
+ {
+ throw new InvalidOperationException(FileBasedProgramsResources.EmptyTempPath);
+ }
+
+ return Path.Combine(directory, "dotnet", dotNetSubdirectory ?? "runfile");
+ }
+
+ ///
+ /// Obtains a specific temporary path in a subdirectory for file-based app artifacts, e.g., /tmp/dotnet/runfile/{name}.
+ ///
+ internal static string GetTempSubpath(string name, string? dotNetSubdirectory = null)
+ {
+ return Path.Combine(GetTempSubdirectory(dotNetSubdirectory), name);
+ }
+
+ public static bool IsValidEntryPointPath(string entryPointFilePath)
+ {
+ if (!File.Exists(entryPointFilePath))
+ {
+ return false;
+ }
+
+ if (entryPointFilePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ // Check if the first two characters are #!
+ try
+ {
+ using var stream = File.OpenRead(entryPointFilePath);
+ int first = stream.ReadByte();
+ int second = stream.ReadByte();
+ return first == '#' && second == '!';
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Evaluates against a and the file system.
+ ///
+ ///
+ /// All directives that need some other evaluation (described below) are expanded as MSBuild expressions
+ /// (i.e., $() and @() are substituted with property and item values, etc.).
+ ///
+ /// #:project directives are resolved to full project file paths
+ /// (e.g., if the evaluated value is a directory, finds a project in that directory).
+ ///
+ /// #:include/#:exclude have their determined
+ /// and relative paths resolved relative to their containing file.
+ ///
+ private async ValueTask> EvaluateDirectivesAsync(
+ IProjectInstance project,
+ ImmutableArray directives,
+ ErrorReporter reportError)
+ {
+ if (!directives.Any(static d => d is CSharpDirective.Project or CSharpDirective.IncludeOrExclude or CSharpDirective.Ref))
+ {
+ return directives;
+ }
+
+ var builder = ImmutableArray.CreateBuilder(directives.Length);
+
+ ImmutableArray<(string Extension, string ItemType)> mapping = default;
+
+ foreach (var directive in directives)
+ {
+ switch (directive)
+ {
+ case CSharpDirective.Project projectDirective:
+ projectDirective = projectDirective.WithName(await project.ExpandStringAsync(projectDirective.Name).ConfigureAwait(false), CSharpDirective.Project.NameKind.Expanded);
+ projectDirective = projectDirective.EnsureProjectFilePath(reportError);
+
+ builder.Add(projectDirective);
+ break;
+
+ case CSharpDirective.Ref refDirective:
+ refDirective = refDirective.WithName(await project.ExpandStringAsync(refDirective.Name).ConfigureAwait(false), CSharpDirective.Ref.NameKind.Expanded);
+ refDirective = refDirective.EnsureResolvedPath(reportError);
+
+ builder.Add(refDirective);
+ break;
+
+ case CSharpDirective.IncludeOrExclude includeOrExcludeDirective:
+ var expandedPath = await project.ExpandStringAsync(includeOrExcludeDirective.Name).ConfigureAwait(false);
+ var fullPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(includeOrExcludeDirective.Info.SourceFile.Path)!, expandedPath));
+ includeOrExcludeDirective = includeOrExcludeDirective.WithName(fullPath);
+
+ if (mapping.IsDefault)
+ {
+ mapping = await GetItemMappingAsync(project, reportError).ConfigureAwait(false);
+ }
+
+ includeOrExcludeDirective = includeOrExcludeDirective.WithDeterminedItemType(reportError, mapping);
+
+ builder.Add(includeOrExcludeDirective);
+ break;
+
+ default:
+ builder.Add(directive);
+ break;
+ }
+ }
+
+ return builder.DrainToImmutable();
+ }
+
+ internal async ValueTask> GetItemMappingAsync(IProjectInstance project, ErrorReporter reportError)
+ {
+ return CSharpDirective.IncludeOrExclude.ParseMapping(
+ await project.GetPropertyValueAsync(CSharpDirective.IncludeOrExclude.MappingPropertyName).ConfigureAwait(false),
+ EntryPointSourceFile,
+ reportError);
+ }
+
+ public static async ValueTask CreateProjectInstanceAsync(
+ IBuildService buildService,
+ string entryPointFilePath,
+ string targetFramework,
+ IProjectCollection projectCollection,
+ Action errorReporter)
+ {
+ var builder = new VirtualProjectBuilder(buildService, entryPointFilePath, targetFramework);
+
+ var result = await builder.CreateProjectInstanceAsync(
+ projectCollection,
+ (text, path, textSpan, message, _) => errorReporter(path, text.Lines.GetLinePositionSpan(textSpan).Start.Line + 1, message)).ConfigureAwait(false);
+
+ return result.Project;
+ }
+
+ internal readonly struct Result(
+ IProjectInstance project,
+ IProjectRootElement projectRootElement,
+ ImmutableArray evaluatedDirectives)
+ {
+ public IProjectInstance Project { get; } = project;
+ public IProjectRootElement ProjectRootElement { get; } = projectRootElement;
+ public ImmutableArray EvaluatedDirectives { get; } = evaluatedDirectives;
+ }
+
+ internal async ValueTask CreateProjectInstanceAsync(
+ IProjectCollection projectCollection,
+ ErrorReporter reportError,
+ ImmutableArray directives = default,
+ IDictionary? additionalGlobalProperties = null,
+ bool validateAllDirectives = false,
+ HashSet? processedRefFiles = null)
+ {
+ IProjectInstance project;
+ IProjectRootElement projectRootElement;
+ ImmutableArray evaluatedDirectives;
+
+ var directivesOriginal = directives;
+
+ if (directives.IsDefault)
+ {
+ directives = FileLevelDirectiveHelpers.FindDirectives(EntryPointSourceFile, validateAllDirectives, reportError, checkDuplicates: false);
+ }
+
+ (string ProjectFileText, IProjectInstance ProjectInstance, IProjectRootElement ProjectRootElement)? lastProject = null;
+
+ // If we evaluated directives previously (e.g., during restore), reuse them.
+ // We don't use the additional properties from `addGlobalProperties`
+ // during directive evaluation anyway, so the directives can be reused safely.
+ if (_evaluatedDirectives is { } cached &&
+ cached.Original == directivesOriginal)
+ {
+ evaluatedDirectives = cached.Evaluated;
+ (project, projectRootElement) = await CreateProjectInstanceNoEvaluation(
+ projectCollection,
+ evaluatedDirectives,
+ additionalGlobalProperties).ConfigureAwait(false);
+ }
+ else
+ {
+ var entryPointDirectory = Path.GetDirectoryName(EntryPointFileFullPath)!;
+ var seenFiles = new HashSet(StringComparer.Ordinal) { EntryPointFileFullPath };
+ var filesToProcess = new Queue();
+ var evaluatedDirectiveBuilder = ImmutableArray.CreateBuilder();
+ var deduplicator = new DirectiveDeduplicator();
+
+ do
+ {
+ var directivesForEvaluation = DeduplicateSdkDirectives(directives);
+
+ // Create a project with properties from #:property directives so they can be expanded inside EvaluateDirectives.
+ (project, projectRootElement) = await CreateProjectInstanceNoEvaluation(
+ projectCollection,
+ [.. evaluatedDirectiveBuilder, .. directivesForEvaluation],
+ additionalGlobalProperties).ConfigureAwait(false);
+
+ // Evaluate directives, e.g., determine item types for #:include/#:exclude from their file extension.
+ var fileEvaluatedDirectives = await EvaluateDirectivesAsync(project, directivesForEvaluation, reportError).ConfigureAwait(false);
+
+ // Detect duplicate directives across all files on evaluated directives. EvaluateDirectives only expands
+ // #:project, #:ref, #:include, and #:exclude; #:property and #:package values are still unevaluated here.
+ var deduplicatedFileEvaluatedDirectiveBuilder = ImmutableArray.CreateBuilder(fileEvaluatedDirectives.Length);
+ foreach (var directive in fileEvaluatedDirectives)
+ {
+ if (directive is CSharpDirective.Sdk)
+ {
+ deduplicatedFileEvaluatedDirectiveBuilder.Add(directive);
+ continue;
+ }
+
+ if (directive is CSharpDirective.Named named)
+ {
+ deduplicator.CheckDirective(named, reportError, out bool shouldKeep);
+ if (!shouldKeep)
+ {
+ continue;
+ }
+ }
+
+ deduplicatedFileEvaluatedDirectiveBuilder.Add(directive);
+ }
+
+ fileEvaluatedDirectives = deduplicatedFileEvaluatedDirectiveBuilder.DrainToImmutable();
+
+ evaluatedDirectiveBuilder.AddRange(fileEvaluatedDirectives);
+
+ if (fileEvaluatedDirectives != directives)
+ {
+ // This project will contain items from #:include/#:exclude directives which we will traverse recursively.
+ (project, projectRootElement) = await CreateProjectInstanceNoEvaluation(
+ projectCollection,
+ evaluatedDirectiveBuilder.ToImmutable(),
+ additionalGlobalProperties).ConfigureAwait(false);
+ }
+
+ var compileItems = await project.GetItemMetadataValuesAsync("Compile", ["FullPath"]).ConfigureAwait(false);
+ foreach (var compileItem in compileItems)
+ {
+ Debug.Assert(compileItem.Length == 1);
+ var fullPath = compileItem[0];
+ var compilePath = Path.GetFullPath(Path.Combine(
+ entryPointDirectory,
+ fullPath));
+ if (seenFiles.Add(compilePath))
+ {
+ filesToProcess.Enqueue(compilePath);
+ }
+ }
+ }
+ while (TryGetNextFileToProcess());
+
+ evaluatedDirectives = evaluatedDirectiveBuilder.ToImmutable();
+ _evaluatedDirectives = (directivesOriginal, evaluatedDirectives);
+
+ bool TryGetNextFileToProcess()
+ {
+ while (filesToProcess.Count != 0)
+ {
+ var filePath = filesToProcess.Dequeue();
+ if (!File.Exists(filePath))
+ {
+ reportError(EntryPointSourceFile.Text, EntryPointSourceFile.Path, default, string.Format(FileBasedProgramsResources.IncludedFileNotFound, filePath));
+ continue;
+ }
+
+ var sourceFile = SourceFile.Load(filePath);
+ directives = FileLevelDirectiveHelpers.FindDirectives(sourceFile, validateAllDirectives, reportError, checkDuplicates: false);
+ return true;
+ }
+
+ return false;
+ }
+
+ // #:sdk directives become Sdk.props/Sdk.targets imports when creating the temporary project used for
+ // directive evaluation, so identical duplicates must be removed before that project is created.
+ ImmutableArray DeduplicateSdkDirectives(ImmutableArray directives)
+ {
+ if (!directives.Any(static directive => directive is CSharpDirective.Sdk))
+ {
+ return directives;
+ }
+
+ var builder = ImmutableArray.CreateBuilder(directives.Length);
+ var changed = false;
+
+ foreach (var directive in directives)
+ {
+ if (directive is CSharpDirective.Sdk sdk)
+ {
+ deduplicator.CheckDirective(sdk, reportError, out bool shouldKeep);
+ if (!shouldKeep)
+ {
+ changed = true;
+ continue;
+ }
+ }
+
+ builder.Add(directive);
+ }
+
+ return changed ? builder.DrainToImmutable() : directives;
+ }
+ }
+
+ await CheckDirectivesAsync(project, evaluatedDirectives, reportError).ConfigureAwait(false);
+ await CreateReferencedVirtualProjectsAsync(projectCollection, evaluatedDirectives, reportError, validateAllDirectives, processedRefFiles).ConfigureAwait(false);
+
+ return new Result(project, projectRootElement, evaluatedDirectives);
+
+ async ValueTask<(IProjectInstance, IProjectRootElement)> CreateProjectInstanceNoEvaluation(
+ IProjectCollection projectCollection,
+ ImmutableArray directives,
+ IDictionary? additionalGlobalProperties = null)
+ {
+ var projectFileWriter = new StringWriter();
+
+ WriteProjectFile(
+ projectFileWriter,
+ directives,
+ GetDefaultProperties(_targetFramework),
+ isVirtualProject: true,
+ entryPointFilePath: EntryPointFileFullPath,
+ artifactsPath: ArtifactsPath,
+ includeRuntimeConfigInformation: RequestedTargets?.Any(static t => t is "Publish" or "Pack") != true);
+
+ var projectFileText = projectFileWriter.ToString();
+
+ // If nothing changed, reuse the previous project instance to avoid unnecessary re-evaluations.
+ if (lastProject is { } cachedProject && cachedProject.ProjectFileText == projectFileText)
+ {
+ return (cachedProject.ProjectInstance, cachedProject.ProjectRootElement);
+ }
+
+ var projectRoot = CreateProjectRootElement(projectFileText, projectCollection);
+
+ var project = await _buildService.CreateProjectInstanceFromProjectRootElementAsync(projectRoot, projectCollection, additionalGlobalProperties).ConfigureAwait(false);
+
+ lastProject = (projectFileText, project, projectRoot);
+
+ return (project, projectRoot);
+
+ IProjectRootElement CreateProjectRootElement(string projectFileText, IProjectCollection projectCollection)
+ {
+ using var reader = new StringReader(projectFileText);
+ using var xmlReader = XmlReader.Create(reader);
+ var projectRoot = _buildService.CreateProjectRootElement(xmlReader, projectCollection, EntryPointFileFullPath);
+ projectRoot.FullPath = GetVirtualProjectPath(EntryPointFileFullPath);
+ return projectRoot;
+ }
+ }
+ }
+
+ ///
+ /// Recursively creates virtual s for all #:ref directives
+ /// so MSBuild can resolve <ProjectReference> items to them.
+ ///
+ private async ValueTask CreateReferencedVirtualProjectsAsync(
+ IProjectCollection projectCollection,
+ ImmutableArray directives,
+ ErrorReporter reportError,
+ bool validateAllDirectives,
+ HashSet? processedFiles)
+ {
+ if (!directives.Any(static d => d is CSharpDirective.Ref))
+ {
+ return;
+ }
+
+ processedFiles ??= new HashSet(StringComparer.OrdinalIgnoreCase);
+ processedFiles.Add(EntryPointFileFullPath);
+
+ foreach (var refDirective in directives.OfType())
+ {
+ Debug.Assert(refDirective.ResolvedPath is not null);
+
+ if (refDirective.ResolvedPath is not { } resolvedPath)
+ {
+ continue;
+ }
+
+ if (!processedFiles.Add(resolvedPath))
+ {
+ continue;
+ }
+
+ var refBuilder = new VirtualProjectBuilder(_buildService, resolvedPath, _targetFramework);
+ await refBuilder.CreateProjectInstanceAsync(
+ projectCollection,
+ reportError,
+ validateAllDirectives: validateAllDirectives,
+ processedRefFiles: processedFiles).ConfigureAwait(false);
+ }
+ }
+
+ private async ValueTask CheckDirectivesAsync(
+ IProjectInstance project,
+ ImmutableArray directives,
+ ErrorReporter reportError)
+ {
+ var refEnabled = new StrongBox();
+
+ foreach (var directive in directives)
+ {
+ if (directive is CSharpDirective.Ref)
+ {
+ await CheckFlagEnabledAsync(refEnabled, CSharpDirective.Ref.ExperimentalFileBasedProgramEnableRefDirective, directive).ConfigureAwait(false);
+ }
+ }
+
+ async ValueTask CheckFlagEnabledAsync(StrongBox flag, string flagName, CSharpDirective directive)
+ {
+ bool value = flag.Value ??= MSBuildUtilities.ConvertStringToBool(await project.GetPropertyValueAsync(flagName).ConfigureAwait(false));
+
+ if (!value)
+ {
+ reportError(
+ directive.Info.SourceFile.Text,
+ directive.Info.SourceFile.Path,
+ directive.Info.Span,
+ string.Format(FileBasedProgramsResources.ExperimentalFeatureDisabled, flagName));
+ }
+ }
+ }
+
+ internal static void WriteProjectFile(
+ TextWriter writer,
+ ImmutableArray directives,
+ IEnumerable<(string name, string value)> defaultProperties,
+ bool isVirtualProject,
+ string? entryPointFilePath = null,
+ string? artifactsPath = null,
+ bool includeRuntimeConfigInformation = true,
+ string? userSecretsId = null,
+ ImmutableArray explicitProjectItems = default)
+ {
+ Debug.Assert(userSecretsId == null || !isVirtualProject);
+
+ int processedDirectives = 0;
+
+ var sdkDirectives = directives.OfType();
+ var propertyDirectives = directives.OfType();
+ var packageDirectives = directives.OfType();
+ var projectDirectives = directives.OfType();
+ var refDirectives = directives.OfType();
+ var includeOrExcludeDirectives = directives.OfType().ToArray();
+
+ const string defaultSdkName = "Microsoft.NET.Sdk";
+ string firstSdkName;
+ string? firstSdkVersion;
+
+ if (sdkDirectives.FirstOrDefault() is { } firstSdk)
+ {
+ firstSdkName = firstSdk.Name;
+ firstSdkVersion = firstSdk.Version;
+ processedDirectives++;
+ }
+ else
+ {
+ firstSdkName = defaultSdkName;
+ firstSdkVersion = null;
+ }
+
+ if (isVirtualProject)
+ {
+ Debug.Assert(!string.IsNullOrWhiteSpace(artifactsPath));
+ Debug.Assert(entryPointFilePath is not null);
+
+ // Note that ArtifactsPath needs to be specified before Sdk.props
+ // (usually it's recommended to specify it in Directory.Build.props
+ // but importing Sdk.props manually afterwards also works).
+ writer.WriteLine($"""
+
+
+
+ false
+ {EscapeValue(artifactsPath)}
+ {EscapeValue(Path.GetFileNameWithoutExtension(entryPointFilePath))}
+ $(AssemblyName)
+ artifacts/$(AssemblyName)
+ artifacts/$(AssemblyName)
+ true
+ {EscapeValue(entryPointFilePath)}
+ {CSharpDirective.IncludeOrExclude.DefaultMappingString}
+ false
+ true
+ """);
+
+ // Only set these to false when using the default SDK with no additional SDKs
+ // to avoid including .resx and other files that are typically not expected in simple file-based apps.
+ // When other SDKs are used (e.g., Microsoft.NET.Sdk.Web), keep the default behavior.
+ bool usingOnlyDefaultSdk = firstSdkName == defaultSdkName && sdkDirectives.Count() <= 1;
+ if (usingOnlyDefaultSdk)
+ {
+ writer.WriteLine("""
+ false
+ false
+ """);
+ }
+
+ // Write default properties before importing SDKs so they can be overridden by SDKs
+ // (and implicit build files which are imported by the default .NET SDK).
+ foreach (var (name, value) in defaultProperties)
+ {
+ writer.WriteLine($"""
+ <{name}>{EscapeValue(value)}{name}>
+ """);
+ }
+
+ writer.WriteLine($"""
+
+
+
+
+
+
+ """);
+
+ if (firstSdkVersion is null)
+ {
+ writer.WriteLine($"""
+
+ """);
+ }
+ else
+ {
+ writer.WriteLine($"""
+
+ """);
+ }
+ }
+ else
+ {
+ string slashDelimited = firstSdkVersion is null
+ ? firstSdkName
+ : $"{firstSdkName}/{firstSdkVersion}";
+ writer.WriteLine($"""
+
+
+ """);
+ }
+
+ foreach (var sdk in sdkDirectives.Skip(1))
+ {
+ if (isVirtualProject)
+ {
+ WriteImport(writer, "Sdk.props", sdk);
+ }
+ else if (sdk.Version is null)
+ {
+ writer.WriteLine($"""
+
+ """);
+ }
+ else
+ {
+ writer.WriteLine($"""
+
+ """);
+ }
+
+ processedDirectives++;
+ }
+
+ if (isVirtualProject || processedDirectives > 1)
+ {
+ writer.WriteLine();
+ }
+
+ // Write default and custom properties.
+ {
+ writer.WriteLine("""
+
+ """);
+
+ // First write the default properties except those specified by the user.
+ if (!isVirtualProject)
+ {
+ var customPropertyNames = propertyDirectives
+ .Select(static d => d.Name)
+ .ToHashSet(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var (name, value) in defaultProperties)
+ {
+ if (!customPropertyNames.Contains(name))
+ {
+ writer.WriteLine($"""
+ <{name}>{EscapeValue(value)}{name}>
+ """);
+ }
+ }
+
+ if (userSecretsId != null && !customPropertyNames.Contains("UserSecretsId"))
+ {
+ writer.WriteLine($"""
+ {EscapeValue(userSecretsId)}
+ """);
+ }
+ }
+ // Some hosts (like MSBuildWorkspace) don't provide a default TargetFramework
+ // and instead want to use the TargetFramework that's default corresponding to the imported SDK props.
+ else if (!defaultProperties.Any(p => p.name == "TargetFramework"))
+ {
+ writer.WriteLine("""
+ net$(BundledNETCoreAppTargetFrameworkVersion)
+ """);
+ }
+
+ // Write custom properties.
+ foreach (var property in propertyDirectives)
+ {
+ writer.WriteLine($"""
+ <{property.Name}>{EscapeValue(property.Value)}{property.Name}>
+ """);
+
+ processedDirectives++;
+ }
+
+ // Write virtual-only properties which cannot be overridden.
+ if (isVirtualProject)
+ {
+ writer.WriteLine("""
+ false
+ $(Features);FileBasedProgram
+ """);
+ }
+
+ writer.WriteLine("""
+
+
+ """);
+ }
+
+ if (!isVirtualProject)
+ {
+ // In the real project, files are included by the conversion copying them to the output directory,
+ // hence we don't need to transfer the #:include/#:exclude directives over by default.
+ processedDirectives += includeOrExcludeDirectives.Length;
+ }
+ else if (includeOrExcludeDirectives.Length > 0)
+ {
+ writer.WriteLine("""
+
+ """);
+
+ foreach (var includeOrExclude in includeOrExcludeDirectives)
+ {
+ processedDirectives++;
+
+ var itemType = includeOrExclude.ItemType;
+
+ if (itemType == null)
+ {
+ // Before directives are evaluated, the item type is null.
+ // We still need to create the project (so that we can evaluate $() properties),
+ // but we can skip the items.
+ continue;
+ }
+
+ if (includeOrExclude.Kind == CSharpDirective.IncludeOrExcludeKind.Include)
+ {
+ writer.WriteLine($"""
+ <{itemType} Include="{EscapeValue(includeOrExclude.Name)}" {FromIncludeDirectiveMetadataName}="true" />
+ """);
+ }
+ else
+ {
+ writer.WriteLine($"""
+ <{itemType} Remove="{EscapeValue(includeOrExclude.Name)}" />
+ """);
+ }
+ }
+
+ writer.WriteLine("""
+
+
+ """);
+ }
+
+ if (!explicitProjectItems.IsDefaultOrEmpty)
+ {
+ writer.WriteLine("""
+
+ """);
+
+ foreach (var (itemType, include) in explicitProjectItems)
+ {
+ writer.WriteLine($"""
+ <{itemType} Include="{EscapeValue(include)}" />
+ """);
+ }
+
+ writer.WriteLine("""
+
+
+ """);
+ }
+
+ if (packageDirectives.Any())
+ {
+ writer.WriteLine("""
+
+ """);
+
+ foreach (var package in packageDirectives)
+ {
+ if (package.Version is null)
+ {
+ writer.WriteLine($"""
+
+ """);
+ }
+ else
+ {
+ writer.WriteLine($"""
+
+ """);
+ }
+
+ processedDirectives++;
+ }
+
+ writer.WriteLine("""
+
+
+ """);
+ }
+
+ if (projectDirectives.Any() || refDirectives.Any())
+ {
+ writer.WriteLine("""
+
+ """);
+
+ foreach (var projectReference in projectDirectives)
+ {
+ writer.WriteLine($"""
+
+ """);
+
+ processedDirectives++;
+ }
+
+ foreach (var refDirective in refDirectives)
+ {
+ if (refDirective.ResolvedPath is not null)
+ {
+ var virtualProjectPath = GetVirtualProjectPath(refDirective.ResolvedPath);
+ writer.WriteLine($"""
+
+ """);
+ }
+
+ processedDirectives++;
+ }
+
+ writer.WriteLine("""
+
+
+ """);
+ }
+
+ Debug.Assert(processedDirectives + directives.OfType().Count() == directives.Length);
+
+ if (isVirtualProject)
+ {
+ Debug.Assert(entryPointFilePath is not null);
+
+ // We Exclude existing Compile items (which could be added e.g.
+ // in Microsoft.NET.Sdk.DefaultItems.props when user sets EnableDefaultCompileItems=true,
+ // or above via #:include/#:exclude directives).
+ writer.WriteLine($"""
+
+
+
+
+ """);
+
+ if (includeRuntimeConfigInformation)
+ {
+ var entryPointDirectory = Path.GetDirectoryName(entryPointFilePath) ?? "";
+ writer.WriteLine($"""
+
+
+
+
+
+ """);
+ }
+
+ foreach (var sdk in sdkDirectives)
+ {
+ WriteImport(writer, "Sdk.targets", sdk);
+ }
+
+ if (!sdkDirectives.Any())
+ {
+ Debug.Assert(firstSdkName == defaultSdkName && firstSdkVersion == null);
+ writer.WriteLine($"""
+
+ """);
+ }
+
+ writer.WriteLine();
+ }
+
+ writer.WriteLine("""
+
+ """);
+
+ static string EscapeValue(string value) => SecurityElement.Escape(value);
+
+ static void WriteImport(TextWriter writer, string project, CSharpDirective.Sdk sdk)
+ {
+ if (sdk.Version is null)
+ {
+ writer.WriteLine($"""
+
+ """);
+ }
+ else
+ {
+ writer.WriteLine($"""
+
+ """);
+ }
+ }
+ }
+}
diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf
similarity index 77%
rename from src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf
rename to src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf
index fc0195eeec117..9487c43516c12 100644
--- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf
+++ b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf
@@ -1,4 +1,4 @@
-
+
@@ -17,6 +17,11 @@
Nenašel se projekt ani adresář {0}.
+
+ Could not find file '{0}'.
+ Soubor {0} nebyl nalezen.
+ {0} is the file path.
+ errorchyba
@@ -27,11 +32,26 @@
Duplicitní direktivy nejsou podporovány: {0}{0} is the directive type and name.
+
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+
+
+
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ {Locked="MSBuild"}{Locked="true"}. {0} is MSBuild property name.
+ Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1}Nerozpoznaná přípona souboru v direktivě {0}. V současné době jsou rozpoznávány pouze tyto přípony: {1}{0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx'
+
+ File included via #:include directive (or Compile item) not found: {0}
+ File included via #:include directive (or Compile item) not found: {0}
+ {Locked="#:include"}{Locked="Compile"}. {0} is file path.
+ The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'.Direktiva by měla obsahovat název bez speciálních znaků a volitelnou hodnotu oddělenou znakem {1}, například #:{0} Název{1}Hodnota.
@@ -39,13 +59,13 @@
Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid.
- Každá položka ve vlastnosti MSBuild FileBasedProgramsItemMapping musí mít dvě části oddělené znakem „=“. Položka {0} je neplatná.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="}
+ Každá položka ve vlastnosti MSBuild FileBasedProgramsItemMapping musí mít dvě části oddělené znakem '='. Položka {0} je neplatná.
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'='"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid.
- Každá položka ve vlastnosti MSBuild FileBasedProgramsItemMapping musí být mapována z neprázdné přípony souboru začínající na „.“. Přípona {0} v položce {1} je neplatná.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."}
+ Každá položka ve vlastnosti MSBuild FileBasedProgramsItemMapping musí být mapována z neprázdné přípony souboru začínající na '.'. Přípona {0} v položce {1} je neplatná.
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'.'"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid.
@@ -57,6 +77,11 @@
Direktiva #:project je neplatná: {0}{0} is the inner error message.
+
+ The '#:ref' directive is invalid: {0}
+ Direktiva #:ref je neplatná: {0}.
+ {Locked="#:ref"}{0} is the inner error message.
+ Missing name of '{0}'.Chybí název pro: {0}.
diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf
similarity index 77%
rename from src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf
rename to src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf
index b76d1b5f97822..ee725bd744e88 100644
--- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf
+++ b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf
@@ -1,4 +1,4 @@
-
+
@@ -17,6 +17,11 @@
Das Projekt oder Verzeichnis "{0}" wurde nicht gefunden.
+
+ Could not find file '{0}'.
+ Die Datei "{0}" konnte nicht gefunden werden.
+ {0} is the file path.
+ errorFehler
@@ -27,11 +32,26 @@
Doppelte Anweisungen werden nicht unterstützt: {0}{0} is the directive type and name.
+
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+
+
+
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ {Locked="MSBuild"}{Locked="true"}. {0} is MSBuild property name.
+ Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1}Unbekannte Dateierweiterung in der „{0}“-Anweisung. Derzeit werden nur diese Erweiterungen erkannt: {1}{0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx'
+
+ File included via #:include directive (or Compile item) not found: {0}
+ File included via #:include directive (or Compile item) not found: {0}
+ {Locked="#:include"}{Locked="Compile"}. {0} is file path.
+ The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'.Die Anweisung sollte einen Namen ohne Sonderzeichen und einen optionalen Wert enthalten, die durch „{1}“ getrennt sind, wie „#:{0} Name{1}Wert“.
@@ -39,13 +59,13 @@
Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid.
- Jeder Eintrag in der MSBuild-Eigenschaft „FileBasedProgramsItemMapping“ muss aus zwei durch „=“ getrennten Teilen bestehen. Der Eintrag „{0}“ ist ungültig.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="}
+ Jeder Eintrag in der MSBuild-Eigenschaft „FileBasedProgramsItemMapping“ muss aus zwei durch '=' getrennten Teilen bestehen. Der Eintrag „{0}“ ist ungültig.
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'='"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid.
- Jeder Eintrag in der MSBuild-Eigenschaft „FileBasedProgramsItemMapping“ muss einer nicht leeren Dateierweiterung zugeordnet sein, die mit „.“ beginnt. Die Erweiterung „{0}“ im Eintrag „{1}“ ist ungültig.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."}
+ Jeder Eintrag in der MSBuild-Eigenschaft „FileBasedProgramsItemMapping“ muss einer nicht leeren Dateierweiterung zugeordnet sein, die mit '.' beginnt. Die Erweiterung „{0}“ im Eintrag „{1}“ ist ungültig.
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'.'"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid.
@@ -57,6 +77,11 @@
Die Anweisung „#:p roject“ ist ungültig: {0}{0} is the inner error message.
+
+ The '#:ref' directive is invalid: {0}
+ Die „#:ref“-Direktive ist ungültig: {0}
+ {Locked="#:ref"}{0} is the inner error message.
+ Missing name of '{0}'.Fehlender Name der Anweisung „{0}“.
diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf
similarity index 79%
rename from src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf
rename to src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf
index 84a6d5d8fe790..6e443bbae5df4 100644
--- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf
+++ b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf
@@ -1,4 +1,4 @@
-
+
@@ -17,6 +17,11 @@
No se encuentra el proyecto o directorio "{0}".
+
+ Could not find file '{0}'.
+ No se pudo encontrar el archivo '{0}'.
+ {0} is the file path.
+ errorerror
@@ -27,11 +32,26 @@
No se admiten directivas duplicadas: {0}{0} is the directive type and name.
+
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+
+
+
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ {Locked="MSBuild"}{Locked="true"}. {0} is MSBuild property name.
+ Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1}Extensión de archivo no reconocida en la directiva ''{0}. Actualmente solo se reconocen estas extensiones: {1}{0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx'
+
+ File included via #:include directive (or Compile item) not found: {0}
+ File included via #:include directive (or Compile item) not found: {0}
+ {Locked="#:include"}{Locked="Compile"}. {0} is file path.
+ The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'.La directiva debe contener un nombre sin caracteres especiales y un valor opcional separado por "{1}" como "#:{0} Nombre{1}Valor".
@@ -40,12 +60,12 @@
Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid.Cada entrada de la propiedad MSBuild ''FileBasedProgramsItemMapping'' debe tener dos partes separadas por '='. La entrada ''{0}'' no es válida.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="}
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'='"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid.Cada entrada de la propiedad MSBuild ''FileBasedProgramsItemMapping'' debe asignarse desde una extensión de archivo que no esté vacía a partir de '.'. La extensión ''{0}'' de la entrada ''{1}'' no es válida.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."}
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'.'"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid.
@@ -57,6 +77,11 @@
La directiva "#:project" no es válida: {0}{0} is the inner error message.
+
+ The '#:ref' directive is invalid: {0}
+ La directiva "#:ref" no es válida: {0}
+ {Locked="#:ref"}{0} is the inner error message.
+ Missing name of '{0}'.Falta el nombre de "{0}".
diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf
similarity index 77%
rename from src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf
rename to src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf
index 3d647ae12c894..0ebe9ee473e98 100644
--- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf
+++ b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf
@@ -1,4 +1,4 @@
-
+
@@ -17,6 +17,11 @@
Projet ou répertoire '{0}' introuvable.
+
+ Could not find file '{0}'.
+ Impossible de trouver le fichier '{0}'.
+ {0} is the file path.
+ errorerreur
@@ -27,11 +32,26 @@
Les directives dupliquées ne sont pas prises en charge : {0}{0} is the directive type and name.
+
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+
+
+
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ {Locked="MSBuild"}{Locked="true"}. {0} is MSBuild property name.
+ Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1}Extension de fichier non reconnue dans la directive « {0} ». Seules ces extensions sont actuellement reconnues : {1}{0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx'
+
+ File included via #:include directive (or Compile item) not found: {0}
+ File included via #:include directive (or Compile item) not found: {0}
+ {Locked="#:include"}{Locked="Compile"}. {0} is file path.
+ The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'.La directive dans doit contenir un nom sans caractères spéciaux et une valeur facultative séparée par « {1} » comme « # :{0} Nom{1}Valeur ».
@@ -39,13 +59,13 @@
Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid.
- Chaque entrée de la propriété MSBuild « FileBasedProgramsItemMapping » doit comporter deux parties séparées par « = ». L’entrée « {0} » est invalide.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="}
+ Chaque entrée de la propriété MSBuild « FileBasedProgramsItemMapping » doit comporter deux parties séparées par '='. L’entrée « {0} » est invalide.
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'='"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid.
- Chaque entrée de la propriété MSBuild « FileBasedProgramsItemMapping » doit correspondre à une extension de fichier non vide commençant par « . ». L’extension « {0} » dans l’entrée « {1} » est invalide.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."}
+ Chaque entrée de la propriété MSBuild « FileBasedProgramsItemMapping » doit correspondre à une extension de fichier non vide commençant par '.'. L’extension « {0} » dans l’entrée « {1} » est invalide.
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'.'"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid.
@@ -57,6 +77,11 @@
La directive « #:project » n’est pas valide : {0}{0} is the inner error message.
+
+ The '#:ref' directive is invalid: {0}
+ La directive « #:ref » est invalide : {0}
+ {Locked="#:ref"}{0} is the inner error message.
+ Missing name of '{0}'.Nom manquant pour « {0} ».
diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf
similarity index 78%
rename from src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf
rename to src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf
index 566e9deea623e..314056aa40de4 100644
--- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf
+++ b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf
@@ -1,4 +1,4 @@
-
+
@@ -17,6 +17,11 @@
Non sono stati trovati progetti o directory `{0}`.
+
+ Could not find file '{0}'.
+ Il file '{0}' non è stato trovato.
+ {0} is the file path.
+ errorerrore
@@ -27,11 +32,26 @@
Le direttive duplicate non supportate: {0}{0} is the directive type and name.
+
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+
+
+
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ {Locked="MSBuild"}{Locked="true"}. {0} is MSBuild property name.
+ Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1}Estensione file non riconosciuta nella direttiva "{0}". Sono riconosciute solo queste estensioni: {1}{0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx'
+
+ File included via #:include directive (or Compile item) not found: {0}
+ File included via #:include directive (or Compile item) not found: {0}
+ {Locked="#:include"}{Locked="Compile"}. {0} is file path.
+ The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'.La direttiva deve contenere un nome senza caratteri speciali e un valore facoltativo delimitato da '{1}' come '#:{0}Nome {1}Valore'.
@@ -39,13 +59,13 @@
Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid.
- Ogni voce nella proprietà MSBuild "FileBasedProgramsItemMapping" deve avere due parti separate da "=". La voce "{0}" non è valida.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="}
+ Ogni voce nella proprietà MSBuild "FileBasedProgramsItemMapping" deve avere due parti separate da '='. La voce "{0}" non è valida.
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'='"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid.
- Ogni voce nella proprietà MSBuild "FileBasedProgramsItemMapping" deve essere mappata da un'estensione di file non vuota che inizia con ".". L'estensione "{0}" nella voce "{1}" non è valida.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."}
+ Ogni voce nella proprietà MSBuild "FileBasedProgramsItemMapping" deve essere mappata da un'estensione di file non vuota che inizia con '.'. L'estensione "{0}" nella voce "{1}" non è valida.
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'.'"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid.
@@ -57,6 +77,11 @@
La direttiva '#:project' non è valida: {0}{0} is the inner error message.
+
+ The '#:ref' directive is invalid: {0}
+ La direttiva "#:ref" non è valida: {0}
+ {Locked="#:ref"}{0} is the inner error message.
+ Missing name of '{0}'.Manca il nome di '{0}'.
diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf
similarity index 77%
rename from src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf
rename to src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf
index 6fbc4b90b118f..57db92af69dfb 100644
--- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf
+++ b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf
@@ -1,4 +1,4 @@
-
+
@@ -17,6 +17,11 @@
プロジェクトまたはディレクトリ `{0}` が見つかりませんでした。
+
+ Could not find file '{0}'.
+ ファイル '{0}' が見つかりませんでした。
+ {0} is the file path.
+ errorエラー
@@ -27,11 +32,26 @@
重複するディレクティブはサポートされていません: {0}{0} is the directive type and name.
+
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+
+
+
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ {Locked="MSBuild"}{Locked="true"}. {0} is MSBuild property name.
+ Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1}'{0}' ディレクティブ内の認識されないファイル拡張子。現在認識されている拡張子は次のとおりです: {1}{0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx'
+
+ File included via #:include directive (or Compile item) not found: {0}
+ File included via #:include directive (or Compile item) not found: {0}
+ {Locked="#:include"}{Locked="Compile"}. {0} is file path.
+ The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'.ディレクティブには、特殊文字を含まない名前と、'#:{0} Name{1}Value' などの '{1}' で区切られた省略可能な値を含める必要があります。
@@ -40,12 +60,12 @@
Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid.'FileBasedProgramsItemMapping' MSBuild プロパティの各エントリには、'=' で区切られた 2 つの部分が必要です。エントリ '{0}' が無効です。
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="}
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'='"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid.
- Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."}
+ 'FileBasedProgramsItemMapping' MSBuild プロパティの各エントリは、'.' で始まる空でないファイル拡張子からマップする必要があります。エントリ '{0}' の拡張子 '{1}' は無効です。
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'.'"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid.
@@ -57,6 +77,11 @@
'#:p roject' ディレクティブが無効です: {0}{0} is the inner error message.
+
+ The '#:ref' directive is invalid: {0}
+ '#:ref' ディレクティブが無効です: {0}
+ {Locked="#:ref"}{0} is the inner error message.
+ Missing name of '{0}'.'{0}' の名前がありません。
diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf
similarity index 79%
rename from src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf
rename to src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf
index 8754f08de51ca..6d3f8eb2139e9 100644
--- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf
+++ b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf
@@ -1,4 +1,4 @@
-
+
@@ -17,6 +17,11 @@
프로젝트 또는 디렉터리 {0}을(를) 찾을 수 없습니다.
+
+ Could not find file '{0}'.
+ '{0}' 파일을 찾을 수 없습니다.
+ {0} is the file path.
+ error오류
@@ -27,11 +32,26 @@
중복 지시문은 지원되지 않습니다. {0}{0} is the directive type and name.
+
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+
+
+
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ {Locked="MSBuild"}{Locked="true"}. {0} is MSBuild property name.
+ Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1}'{0}' 지시문에서 인식할 수 없는 파일 확장자입니다. 현재 인식되는 확장자는 다음과 같습니다. {1}.{0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx'
+
+ File included via #:include directive (or Compile item) not found: {0}
+ File included via #:include directive (or Compile item) not found: {0}
+ {Locked="#:include"}{Locked="Compile"}. {0} is file path.
+ The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'.지시문에는 특수 문자가 없는 이름과 '#:{0} 이름{1}값'과 같이 '{1}'(으)로 구분된 선택적 값이 포함되어야 합니다.
@@ -40,12 +60,12 @@
Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid.'FileBasedProgramsItemMapping' MSBuild 속성의 각 항목은 '='로 구분된 두 부분으로 구성되어야 합니다. '{0}' 항목이 잘못되었습니다.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="}
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'='"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid.'FileBasedProgramsItemMapping' MSBuild 속성의 각 항목은 '.'로 시작하는 비어 있지 않은 파일 확장명에 매핑되어야 합니다. '{0}' 항목의 확장명 '{1}'이(가) 잘못되었습니다.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."}
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'.'"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid.
@@ -57,6 +77,11 @@
'#:p roject' 지시문이 잘못되었습니다. {0}{0} is the inner error message.
+
+ The '#:ref' directive is invalid: {0}
+ ‘#:ref’ 지시문이 잘못되었습니다: {0}
+ {Locked="#:ref"}{0} is the inner error message.
+ Missing name of '{0}'.'{0}' 이름이 없습니다.
diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf
similarity index 77%
rename from src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf
rename to src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf
index 74c0f15935014..15a17f8c89a99 100644
--- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf
+++ b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf
@@ -1,4 +1,4 @@
-
+
@@ -17,6 +17,11 @@
Nie można odnaleźć projektu ani katalogu „{0}”.
+
+ Could not find file '{0}'.
+ Nie można odnaleźć pliku '{0}'.
+ {0} is the file path.
+ errorbłąd
@@ -27,11 +32,26 @@
Zduplikowane dyrektywy nie są obsługiwane: {0}{0} is the directive type and name.
+
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+
+
+
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ {Locked="MSBuild"}{Locked="true"}. {0} is MSBuild property name.
+ Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1}Nierozpoznane rozszerzenie pliku w dyrektywie „{0}”. Obecnie rozpoznawane są tylko te rozszerzenia: {1}{0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx'
+
+ File included via #:include directive (or Compile item) not found: {0}
+ File included via #:include directive (or Compile item) not found: {0}
+ {Locked="#:include"}{Locked="Compile"}. {0} is file path.
+ The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'.Dyrektywa powinna zawierać nazwę bez znaków specjalnych i opcjonalną wartość rozdzieloną znakiem "{1}#:{0} Name{1}Value".
@@ -39,13 +59,13 @@
Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid.
- Każdy wpis we właściwości MSBuild „FileBasedProgramsItemMapping” musi składać się z dwóch części oddzielonych znakiem „=”. Wpis „{0}” jest nieprawidłowy.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="}
+ Każdy wpis we właściwości MSBuild „FileBasedProgramsItemMapping” musi składać się z dwóch części oddzielonych znakiem '='. Wpis „{0}” jest nieprawidłowy.
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'='"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid.
- Każdy wpis we właściwości MSBuild „FileBasedProgramsItemMapping” musi mapować niepuste rozszerzenie pliku zaczynające się od „.”. Rozszerzenie „{0}” we wpisie „{1}” jest nieprawidłowe.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."}
+ Każdy wpis we właściwości MSBuild „FileBasedProgramsItemMapping” musi mapować niepuste rozszerzenie pliku zaczynające się od '.'. Rozszerzenie „{0}” we wpisie „{1}” jest nieprawidłowe.
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'.'"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid.
@@ -57,6 +77,11 @@
Dyrektywa „#:project” jest nieprawidłowa: {0}{0} is the inner error message.
+
+ The '#:ref' directive is invalid: {0}
+ Dyrektywa „#:ref” jest nieprawidłowa: {0}
+ {Locked="#:ref"}{0} is the inner error message.
+ Missing name of '{0}'.Brak nazwy „{0}”.
diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf
similarity index 79%
rename from src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf
rename to src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf
index 2c5477966a90d..6dd0253cf4232 100644
--- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf
+++ b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf
@@ -1,4 +1,4 @@
-
+
@@ -17,6 +17,11 @@
Não foi possível encontrar o projeto ou diretório ‘{0}’.
+
+ Could not find file '{0}'.
+ Não foi possível encontrar arquivo "{0}".
+ {0} is the file path.
+ errorerro
@@ -27,11 +32,26 @@
Diretivas duplicadas não são suportadas:{0}{0} is the directive type and name.
+
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+
+
+
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ {Locked="MSBuild"}{Locked="true"}. {0} is MSBuild property name.
+ Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1}Extensão de arquivo não reconhecida na diretiva '{0}'. Somente estas extensões são reconhecidas atualmente: {1}{0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx'
+
+ File included via #:include directive (or Compile item) not found: {0}
+ File included via #:include directive (or Compile item) not found: {0}
+ {Locked="#:include"}{Locked="Compile"}. {0} is file path.
+ The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'.A diretiva deve conter um nome sem caracteres especiais e um valor opcional separado por '{1}' como '#:{0} Nome{1}Valor'.
@@ -40,12 +60,12 @@
Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid.Cada entrada na propriedade MSBuild 'FileBasedProgramsItemMapping' deve ter duas partes separadas por '='. A entrada '{0}' é inválida.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="}
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'='"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid.Cada entrada na propriedade MSBuild 'FileBasedProgramsItemMapping' deve mapear a partir de uma extensão de arquivo não vazia que comece com '.'. A extensão '{0}' na entrada '{1}' é inválida.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."}
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'.'"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid.
@@ -57,6 +77,11 @@
A diretiva '#:project' é inválida:{0}{0} is the inner error message.
+
+ The '#:ref' directive is invalid: {0}
+ A diretiva ''#:ref'' é inválida: {0}
+ {Locked="#:ref"}{0} is the inner error message.
+ Missing name of '{0}'.Nome de '{0}' ausente.
diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf
similarity index 80%
rename from src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf
rename to src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf
index c383d8e60274c..4a16274475621 100644
--- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf
+++ b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf
@@ -1,4 +1,4 @@
-
+
@@ -17,6 +17,11 @@
Не удалось найти проект или каталог "{0}".
+
+ Could not find file '{0}'.
+ Не удалось найти файл "{0}".
+ {0} is the file path.
+ errorошибка
@@ -27,11 +32,26 @@
Повторяющиеся директивы не поддерживаются: {0}{0} is the directive type and name.
+
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+
+
+
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ {Locked="MSBuild"}{Locked="true"}. {0} is MSBuild property name.
+ Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1}Нераспознанное расширение файла в директиве "{0}". В настоящее время распознаются только следующие расширения: {1}{0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx'
+
+ File included via #:include directive (or Compile item) not found: {0}
+ File included via #:include directive (or Compile item) not found: {0}
+ {Locked="#:include"}{Locked="Compile"}. {0} is file path.
+ The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'.Директива должна содержать имя без специальных символов и необязательное значение, разделенные символом-разделителем "{1}", например "#:{0} Имя{1}Значение".
@@ -39,13 +59,13 @@
Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid.
- Каждая запись в свойстве MSBuild "FileBasedProgramsItemMapping" должна содержать две части, разделенные "=". Запись "{0}" недопустима.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="}
+ Каждая запись в свойстве MSBuild "FileBasedProgramsItemMapping" должна содержать две части, разделенные '='. Запись "{0}" недопустима.
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'='"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid.
- Каждая запись в свойстве MSBuild "FileBasedProgramsItemMapping" должна сопоставляться с непустым расширением файла, начинающимся с ".". Расширение "{0}" в записи "{1}" недопустимо.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."}
+ Каждая запись в свойстве MSBuild "FileBasedProgramsItemMapping" должна сопоставляться с непустым расширением файла, начинающимся с '.'. Расширение "{0}" в записи "{1}" недопустимо.
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'.'"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid.
@@ -57,6 +77,11 @@
Недопустимая директива "#:project": {0}{0} is the inner error message.
+
+ The '#:ref' directive is invalid: {0}
+ Недопустимая директива "#:ref": {0}
+ {Locked="#:ref"}{0} is the inner error message.
+ Missing name of '{0}'.Отсутствует имя "{0}".
diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf
similarity index 79%
rename from src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf
rename to src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf
index 68b291084dbc3..e37af9cce16a3 100644
--- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf
+++ b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf
@@ -1,4 +1,4 @@
-
+
@@ -17,6 +17,11 @@
`{0}` projesi veya dizini bulunamadı.
+
+ Could not find file '{0}'.
+ '{0}' dosyası bulunamadı.
+ {0} is the file path.
+ errorhata
@@ -27,11 +32,26 @@
Yinelenen yönergeler desteklenmez: {0}{0} is the directive type and name.
+
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+
+
+
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ {Locked="MSBuild"}{Locked="true"}. {0} is MSBuild property name.
+ Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1}'{0}' yönergesinde tanınmayan dosya uzantısı var. Şu anda yalnızca şu uzantılar tanınıyor: {1}{0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx'
+
+ File included via #:include directive (or Compile item) not found: {0}
+ File included via #:include directive (or Compile item) not found: {0}
+ {Locked="#:include"}{Locked="Compile"}. {0} is file path.
+ The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'.Yönerge, özel karakterler içermeyen bir ad ve ‘#:{0} Ad{1}Değer’ gibi '{1}' ile ayrılmış isteğe bağlı bir değer içermelidir.
@@ -40,12 +60,12 @@
Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid.'FileBasedProgramsItemMapping' MSBuild özelliğindeki her girdi, '=' ile ayrılmış iki bölümden oluşmalıdır. '{0}' girdisi geçersizdir.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="}
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'='"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid.'FileBasedProgramsItemMapping' MSBuild özelliğindeki her girdi, '.' ile başlayan boş olmayan bir dosya uzantısına karşılık gelmelidir. '{1}' girdisindeki '{0}' uzantısı geçersizdir.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."}
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'.'"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid.
@@ -57,6 +77,11 @@
‘#:project’ yönergesi geçersizdir: {0}{0} is the inner error message.
+
+ The '#:ref' directive is invalid: {0}
+ '#:ref' yönergesi geçersiz: {0}
+ {Locked="#:ref"}{0} is the inner error message.
+ Missing name of '{0}'.'{0}' adı eksik.
diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf
similarity index 76%
rename from src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf
rename to src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf
index 31dce0ac6e948..207210d71932d 100644
--- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf
+++ b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf
@@ -1,4 +1,4 @@
-
+
@@ -17,6 +17,11 @@
找不到项目或目录“{0}”。
+
+ Could not find file '{0}'.
+ 找不到文件“{0}”。
+ {0} is the file path.
+ error错误
@@ -27,11 +32,26 @@
不支持重复指令: {0}{0} is the directive type and name.
+
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+
+
+
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ {Locked="MSBuild"}{Locked="true"}. {0} is MSBuild property name.
+ Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1}'{0}' 指令中的文件扩展名无法识别。当前仅识别以下扩展名: {1}{0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx'
+
+ File included via #:include directive (or Compile item) not found: {0}
+ File included via #:include directive (or Compile item) not found: {0}
+ {Locked="#:include"}{Locked="Compile"}. {0} is file path.
+ The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'.该指令应包含一个不带特殊字符的名称,以及一个以 '#:{0} Name{1}Value' 等 ‘{1}’ 分隔的可选值。
@@ -40,12 +60,12 @@
Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid.'FileBasedProgramsItemMapping' MSBuild 属性中的每个条目必须包含由 '=' 分隔的两部分。条目 '{0}' 无效。
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="}
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'='"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid.
- Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."}
+ 'FileBasedProgramsItemMapping' MSBuild 属性中的每个条目必须映射自以 '.' 开头的非空文件扩展名。条目 '{1}' 中的扩展名 '{0}' 无效。
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'.'"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid.
@@ -57,6 +77,11 @@
'#:project' 指令无效: {0}{0} is the inner error message.
+
+ The '#:ref' directive is invalid: {0}
+ "#:ref" 指令无效: {0}
+ {Locked="#:ref"}{0} is the inner error message.
+ Missing name of '{0}'.缺少 '{0}' 的名称。
diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf
similarity index 76%
rename from src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf
rename to src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf
index 5c9f29928cfb7..2208f4dc906a7 100644
--- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf
+++ b/src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf
@@ -1,4 +1,4 @@
-
+
@@ -17,6 +17,11 @@
找不到專案或目錄 `{0}`。
+
+ Could not find file '{0}'.
+ 找不到檔案 '{0}'。
+ {0} is the file path.
+ error錯誤
@@ -27,11 +32,26 @@
不支援重複的指示詞: {0}{0} is the directive type and name.
+
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+ Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix.
+
+
+
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ This is an experimental feature, set MSBuild property '{0}' to 'true' to enable it.
+ {Locked="MSBuild"}{Locked="true"}. {0} is MSBuild property name.
+ Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1}'{0}' 指示詞中無法辨識的副檔名。目前僅能識別這些副檔名: {1}{0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx'
+
+ File included via #:include directive (or Compile item) not found: {0}
+ File included via #:include directive (or Compile item) not found: {0}
+ {Locked="#:include"}{Locked="Compile"}. {0} is file path.
+ The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'.指示詞應包含不含特殊字元的名稱,以及 '{1}' 分隔的選用值,例如 '#:{0} Name{1}Value'。
@@ -40,12 +60,12 @@
Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid.'FileBasedProgramsItemMapping' MSBuild 屬性中的每個項目都必須有兩個部分,以 '=' 區隔。項目 '{0}' 無效。
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="}
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'='"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid.
- Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid.
- {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."}
+ 'FileBasedProgramsItemMapping' MSBuild 屬性中的每個項目都必須從開頭為 '.' 的非空白副檔名對應。項目 '{1}' 中的副檔名 '{0}' 無效。
+ {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="'.'"}Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid.
@@ -57,6 +77,11 @@
'#:project' 指示詞無效: {0}{0} is the inner error message.
+
+ The '#:ref' directive is invalid: {0}
+ '#:ref' 指示詞無效: {0}
+ {Locked="#:ref"}{0} is the inner error message.
+ Missing name of '{0}'.缺少 '{0}' 的名稱。
diff --git a/src/Features/CSharp/Portable/SyncedSource/README.md b/src/Workspaces/CSharp/Portable/SyncedSource/README.md
similarity index 100%
rename from src/Features/CSharp/Portable/SyncedSource/README.md
rename to src/Workspaces/CSharp/Portable/SyncedSource/README.md
diff --git a/src/Workspaces/CSharp/Portable/SyncedSource/commitid.txt b/src/Workspaces/CSharp/Portable/SyncedSource/commitid.txt
new file mode 100644
index 0000000000000..fd805655cd8b2
--- /dev/null
+++ b/src/Workspaces/CSharp/Portable/SyncedSource/commitid.txt
@@ -0,0 +1 @@
+9ea4e48db1d9e9737e5fcc9adfe54ed5015a60da
\ No newline at end of file
diff --git a/src/Workspaces/Core/Portable/FileBasedPrograms/IFileBasedProgramService.cs b/src/Workspaces/Core/Portable/FileBasedPrograms/IFileBasedProgramService.cs
new file mode 100644
index 0000000000000..e615f3f503a3e
--- /dev/null
+++ b/src/Workspaces/Core/Portable/FileBasedPrograms/IFileBasedProgramService.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Host;
+using Microsoft.DotNet.FileBasedPrograms;
+
+namespace Microsoft.CodeAnalysis.FileBasedPrograms;
+
+internal interface IFileBasedProgramService : IWorkspaceService
+{
+ string GetArtifactsPath(string entryPointFileFullPath, string? dotNetSubdirectory = null);
+
+ string GetTempSubdirectory(string? dotNetSubdirectory = null);
+ IDictionary GetGlobalBuildProperties();
+
+ bool IsValidEntryPointPath(string entryPointFilePath);
+
+ ValueTask LoadFileBasedAppProjectAsync(
+ IBuildService buildService,
+ IProjectCollection projectCollection,
+ string entryPointFilePath,
+ Action reportError);
+}
diff --git a/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj b/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj
index 2473c6988659d..1e0774443c001 100644
--- a/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj
+++ b/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj
@@ -177,6 +177,9 @@
+
+
+
diff --git a/src/Workspaces/MSBuild/BuildHost/AbstractBuildHost.cs b/src/Workspaces/MSBuild/BuildHost/AbstractBuildHost.cs
index 32857f361eff8..8a1c8a64132a7 100644
--- a/src/Workspaces/MSBuild/BuildHost/AbstractBuildHost.cs
+++ b/src/Workspaces/MSBuild/BuildHost/AbstractBuildHost.cs
@@ -143,10 +143,16 @@ public Task LoadProjectFileAsync(string projectFilePath, string languageNam
///
/// Returns the target ID of the object created for this.
///
- public int LoadProject(string projectFilePath, string projectContent, string languageName)
+ public int LoadProject(string projectFilePath, string? physicalFilePath, string projectContent, string languageName, IDictionary? globalProperties)
{
EnsureMSBuildLoaded(projectFilePath);
- return LoadProjectCore(projectFilePath, projectContent, languageName);
+ return LoadProjectCore(projectFilePath, physicalFilePath, projectContent, languageName, globalProperties);
+ }
+
+ public int LoadProjectInstance(string projectFilePath, string projectContent, IDictionary? additionalGlobalProperties)
+ {
+ EnsureMSBuildLoaded(projectFilePath);
+ return LoadProjectInstanceCore(projectFilePath, projectContent, additionalGlobalProperties);
}
// When using the Mono runtime, the MSBuild types used in this method must be available
@@ -160,18 +166,18 @@ private async Task LoadProjectFileCoreAsync(string projectFilePath, string
Logger.LogInformation($"Loading {projectFilePath}");
var (project, log) = await _buildManager.LoadProjectAsync(projectFilePath, cancellationToken).ConfigureAwait(false);
- return AddProjectFileTarget(project, languageName, log);
+ return AddProjectFileTarget(project, physicalFilePath: null, languageName, log);
}
// When using the Mono runtime, the MSBuild types used in this method must be available
// to the JIT during compilation of the method, so they have to be loaded by the caller;
// therefore this method must not be inlined.
[MethodImpl(MethodImplOptions.NoInlining)]
- private int LoadProjectCore(string projectFilePath, string projectContent, string languageName)
+ private int LoadProjectCore(string projectFilePath, string? physicalFilePath, string projectContent, string languageName, IDictionary? globalProperties)
{
CreateBuildManager();
- Logger.LogInformation($"Loading an in-memory project with the path {projectFilePath}");
+ Logger.LogInformation($"Loading an in-memory project with the path {projectFilePath} ({globalProperties?.Count ?? 0} global properties)");
// We expect MSBuild to consume this stream with a utf-8 encoding.
// This is because we expect the stream we create to not include a BOM nor an an encoding declaration a la ``.
@@ -181,14 +187,29 @@ private int LoadProjectCore(string projectFilePath, string projectContent, strin
// But it seems like a very unlikely scenario to actually get into--this is not something people generally put on real project files.
var stream = new MemoryStream(Encoding.UTF8.GetBytes(projectContent));
- var (project, log) = _buildManager.LoadProject(projectFilePath, stream);
- return AddProjectFileTarget(project, languageName, log);
+ var (project, log) = _buildManager.LoadProject(projectFilePath, stream, globalProperties);
+ return AddProjectFileTarget(project, physicalFilePath, languageName, log);
+ }
+
+ // When using the Mono runtime, the MSBuild types used in this method must be available
+ // to the JIT during compilation of the method, so they have to be loaded by the caller;
+ // therefore this method must not be inlined.
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private int LoadProjectInstanceCore(string projectFilePath, string projectContent, IDictionary? additionalGlobalProperties)
+ {
+ CreateBuildManager();
+
+ Logger.LogInformation($"Loading an in-memory project instance with the path {projectFilePath}");
+
+ using var reader = new StringReader(projectContent);
+ var (projectInstance, log) = _buildManager.LoadProjectInstance(projectFilePath, reader, additionalGlobalProperties);
+ return _server.AddTarget(new ProjectInstance(_server, projectInstance, log));
}
- private int AddProjectFileTarget(Build.Evaluation.Project? project, string languageName, DiagnosticLog log)
+ private int AddProjectFileTarget(Build.Evaluation.Project? project, string? physicalFilePath, string languageName, DiagnosticLog log)
{
Contract.ThrowIfNull(_buildManager);
- return _server.AddTarget(new ProjectFile(languageName, project, _buildManager, _server, log));
+ return _server.AddTarget(new ProjectFile(languageName, project, _buildManager, _server, log) { PhysicalFilePath = physicalFilePath });
}
public Task TryGetProjectOutputPathAsync(string projectFilePath, CancellationToken cancellationToken)
diff --git a/src/Workspaces/MSBuild/BuildHost/Build/ProjectBuildManager.cs b/src/Workspaces/MSBuild/BuildHost/Build/ProjectBuildManager.cs
index f75e3d6e871ed..f65f1b2215313 100644
--- a/src/Workspaces/MSBuild/BuildHost/Build/ProjectBuildManager.cs
+++ b/src/Workspaces/MSBuild/BuildHost/Build/ProjectBuildManager.cs
@@ -141,7 +141,7 @@ public ProjectBuildManager(string[] knownCommandLineParserLanguages, Dictionary<
// is the default if we call the overload with just a stream.
await stream.CopyToAsync(readStream, bufferSize: 81920, cancellationToken).ConfigureAwait(false);
readStream.Position = 0;
- return LoadProjectCore(path, readStream, log);
+ return LoadProjectCore(path, readStream, globalProperties: null, log);
}
catch (Exception e)
{
@@ -151,7 +151,7 @@ public ProjectBuildManager(string[] knownCommandLineParserLanguages, Dictionary<
}
private (MSB.Evaluation.Project? project, DiagnosticLog log) LoadProjectCore(
- string path, Stream readStream, DiagnosticLog log)
+ string path, Stream readStream, IDictionary? globalProperties, DiagnosticLog log)
{
try
{
@@ -172,7 +172,7 @@ public ProjectBuildManager(string[] knownCommandLineParserLanguages, Dictionary<
var project = new MSB.Evaluation.Project(
xml,
- globalProperties: null,
+ globalProperties,
toolsVersion: null,
_projectCollection,
projectLoadSettings);
@@ -186,14 +186,14 @@ public ProjectBuildManager(string[] knownCommandLineParserLanguages, Dictionary<
}
}
- public (MSB.Evaluation.Project? project, DiagnosticLog log) LoadProject(string path, Stream readStream)
+ public (MSB.Evaluation.Project? project, DiagnosticLog log) LoadProject(string path, Stream readStream, IDictionary? globalProperties)
{
Contract.ThrowIfTrue(_disposed);
var log = new DiagnosticLog();
try
{
- return LoadProjectCore(path, readStream, log);
+ return LoadProjectCore(path, readStream, globalProperties, log);
}
catch (Exception e)
{
@@ -202,6 +202,42 @@ public ProjectBuildManager(string[] knownCommandLineParserLanguages, Dictionary<
}
}
+ public (MSB.Execution.ProjectInstance? projectInstance, DiagnosticLog log) LoadProjectInstance(string path, TextReader content, IDictionary? additionalGlobalProperties)
+ {
+ Contract.ThrowIfTrue(_disposed);
+
+ var log = new DiagnosticLog();
+ try
+ {
+ using var xmlReader = XmlReader.Create(content, s_xmlReaderSettings);
+ var projectRootElement = MSB.Construction.ProjectRootElement.Create(xmlReader, _projectCollection);
+ projectRootElement.FullPath = path;
+
+ var mergedGlobalProperties = new Dictionary(_projectCollection.GlobalProperties, StringComparer.OrdinalIgnoreCase);
+
+ if (additionalGlobalProperties != null)
+ {
+ foreach (var pair in additionalGlobalProperties)
+ {
+ mergedGlobalProperties[pair.Key] = pair.Value;
+ }
+ }
+
+ var projectInstance = MSB.Execution.ProjectInstance.FromProjectRootElement(projectRootElement, new MSB.Definition.ProjectOptions
+ {
+ ProjectCollection = _projectCollection,
+ GlobalProperties = mergedGlobalProperties,
+ });
+
+ return (projectInstance, log);
+ }
+ catch (Exception e)
+ {
+ log.Add(e, path);
+ return (projectInstance: null, log);
+ }
+ }
+
public async Task TryGetOutputFilePathAsync(
string path, CancellationToken cancellationToken)
{
diff --git a/src/Workspaces/MSBuild/BuildHost/MSBuild/Constants/MetadataNames.cs b/src/Workspaces/MSBuild/BuildHost/MSBuild/Constants/MetadataNames.cs
index 40e3a069a8540..51e9f3fe21d48 100644
--- a/src/Workspaces/MSBuild/BuildHost/MSBuild/Constants/MetadataNames.cs
+++ b/src/Workspaces/MSBuild/BuildHost/MSBuild/Constants/MetadataNames.cs
@@ -13,4 +13,5 @@ internal static class MetadataNames
public const string Name = nameof(Name);
public const string ReferenceOutputAssembly = nameof(ReferenceOutputAssembly);
public const string Version = nameof(Version);
+ public const string FileBasedProgramsFromRefDirective = nameof(FileBasedProgramsFromRefDirective);
}
diff --git a/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/Extensions.cs b/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/Extensions.cs
index 927548e84959d..26d106ec6eee9 100644
--- a/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/Extensions.cs
+++ b/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/Extensions.cs
@@ -67,7 +67,13 @@ public static PackageReferenceItem[] GetPackageReferences(this MSB.Execution.Pro
/// Create a from a ProjectReference node in the MSBuild file.
///
private static ProjectFileReference CreateProjectFileReference(MSB.Execution.ProjectItemInstance reference)
- => new(reference.EvaluatedInclude, reference.GetAliases(), reference.ReferenceOutputAssemblyIsTrue());
+ {
+ // If this comes from `#:ref` directive, we need to use the path of the `.cs` file, not the virtual `.csproj` file behind it.
+ var path = reference.HasMetadata(MetadataNames.FileBasedProgramsFromRefDirective)
+ ? reference.GetMetadataValue(MetadataNames.FileBasedProgramsFromRefDirective)
+ : reference.EvaluatedInclude;
+ return new(path, reference.GetAliases(), reference.ReferenceOutputAssemblyIsTrue());
+ }
public static string[] GetAliases(this MSB.Framework.ITaskItem item)
{
diff --git a/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectFile.cs b/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectFile.cs
index de50bd5255e07..cdfe14c6a003e 100644
--- a/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectFile.cs
+++ b/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectFile.cs
@@ -29,6 +29,11 @@ internal sealed class ProjectFile(
public string FilePath
=> project?.FullPath ?? string.Empty;
+ ///
+ /// The original C# file path if this corresponds to a virtual project.
+ ///
+ public string? PhysicalFilePath { get; init; }
+
public DiagnosticLogItem[] GetDiagnosticLogItems()
=> [.. log];
@@ -47,7 +52,7 @@ public async Task GetProjectFileInfosAsync(CancellationToken
var projectInstances = await buildManager.BuildProjectInstancesAsync(project, log, cancellationToken).ConfigureAwait(false);
return projectInstances.Select(
- instance => new ProjectInstanceReader(language, _commandLineProvider, instance, project).CreateProjectFileInfo()).ToArray();
+ instance => new ProjectInstanceReader(language, _commandLineProvider, instance, project, PhysicalFilePath).CreateProjectFileInfo()).ToArray();
}
public void AddDocument(string filePath, string? logicalPath = null)
diff --git a/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectInstance.cs b/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectInstance.cs
new file mode 100644
index 0000000000000..4e8570ef5265d
--- /dev/null
+++ b/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectInstance.cs
@@ -0,0 +1,58 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Linq;
+using MSB = Microsoft.Build;
+
+namespace Microsoft.CodeAnalysis.MSBuild;
+
+internal sealed class ProjectInstance(
+ RpcServer server,
+ MSB.Execution.ProjectInstance? projectInstance,
+ DiagnosticLog log) :
+#if NETFRAMEWORK
+ MarshalByRefObject, // We need this object to pass across the AppDomain boundary when on .NET Framework
+#endif
+ IProjectInstance
+{
+ public DiagnosticLogItem[] GetDiagnosticLogItems()
+ => [.. log];
+
+ public string[][] GetItemMetadataValues(string itemType, string[] metadataNames)
+ {
+ if (projectInstance is null)
+ {
+ return [];
+ }
+
+ var items = projectInstance.GetItems(itemType);
+ return items.Select(item => metadataNames.Select(metadataName => item.GetMetadataValue(metadataName)).ToArray()).ToArray();
+ }
+
+ public string GetPropertyValue(string propertyName)
+ {
+ if (projectInstance is null)
+ {
+ return string.Empty;
+ }
+
+ return projectInstance.GetPropertyValue(propertyName);
+ }
+
+ public string ExpandString(string value)
+ {
+ if (projectInstance is null)
+ {
+ return value;
+ }
+
+ return projectInstance.ExpandString(value);
+ }
+
+ public void Dispose()
+ {
+ server.RemoveTarget(this);
+ }
+}
diff --git a/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectInstanceReader.cs b/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectInstanceReader.cs
index c4be8ccd1b2ae..3344ef22c0e6a 100644
--- a/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectInstanceReader.cs
+++ b/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectInstanceReader.cs
@@ -22,11 +22,15 @@ internal readonly struct ProjectInstanceReader
public string Language { get; }
+ ///
+ /// The original C# file path if this corresponds to a virtual project.
+ ///
public ProjectInstanceReader(
string language,
ProjectCommandLineProvider? commandLineReader,
MSB.Execution.ProjectInstance projectInstance,
- MSB.Evaluation.Project? project)
+ MSB.Evaluation.Project? project,
+ string? physicalFilePath = null)
{
Language = language;
_commandLineProvider = commandLineReader;
@@ -36,7 +40,7 @@ public ProjectInstanceReader(
// The ProjectInstance we get from BuildResult.ProjectStateAfterBuild returns a limited ProjectInstance that
// only has MSBuild properties and items available; in this case the ProjectInstance.FullPath will be an empty string.
// Since in the out-of-process build case we will still have the project evaluation, we can get the full path from there.
- _projectFullPath = Project?.FullPath ?? _projectInstance.FullPath;
+ _projectFullPath = physicalFilePath ?? Project?.FullPath ?? _projectInstance.FullPath;
Contract.ThrowIfTrue(string.IsNullOrEmpty(_projectFullPath));
_projectDirectory = PathUtilities.EnsureTrailingSeparator(PathUtilities.GetDirectoryName(_projectFullPath));
}
diff --git a/src/Workspaces/MSBuild/Contracts/IBuildHost.cs b/src/Workspaces/MSBuild/Contracts/IBuildHost.cs
index 985a26bda136e..235cccf179a9d 100644
--- a/src/Workspaces/MSBuild/Contracts/IBuildHost.cs
+++ b/src/Workspaces/MSBuild/Contracts/IBuildHost.cs
@@ -43,8 +43,18 @@ internal interface IBuildHost
/// Permits loading a project file which only exists in-memory, for example, for file-based program scenarios.
///
/// A path to a project file which may or may not exist on disk. Note that an extension that is known by MSBuild, such as .csproj or .vbproj, should be used here.
+ /// The original C# file path.
/// The project file XML content.
- int LoadProject(string projectFilePath, string projectContent, string languageName);
+ /// A handle to the loaded project ().
+ int LoadProject(string projectFilePath, string? physicalFilePath, string projectContent, string languageName, IDictionary? globalProperties);
+
+ ///
+ /// Permits loading a project instance which only exists in-memory, for example, for file-based program scenarios.
+ ///
+ /// A path to a project file which may or may not exist on disk.
+ /// The project file XML content.
+ /// A handle to the loaded project instance ().
+ int LoadProjectInstance(string projectFilePath, string projectContent, IDictionary? additionalGlobalProperties);
Task TryGetProjectOutputPathAsync(string projectFilePath, CancellationToken cancellationToken);
Task ShutdownAsync();
diff --git a/src/Workspaces/MSBuild/Contracts/IProjectInstance.cs b/src/Workspaces/MSBuild/Contracts/IProjectInstance.cs
new file mode 100644
index 0000000000000..afc43dc034bf1
--- /dev/null
+++ b/src/Workspaces/MSBuild/Contracts/IProjectInstance.cs
@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+
+namespace Microsoft.CodeAnalysis.MSBuild;
+
+///
+/// RPC methods for MSBuild's ProjectInstance object.
+///
+internal interface IProjectInstance : IDisposable
+{
+ DiagnosticLogItem[] GetDiagnosticLogItems();
+ string[][] GetItemMetadataValues(string itemType, string[] metadataNames);
+ string GetPropertyValue(string propertyName);
+ string ExpandString(string value);
+}
diff --git a/src/Workspaces/MSBuild/Core/MSBuild/BuildHostProjectFileInfoProvider.cs b/src/Workspaces/MSBuild/Core/MSBuild/BuildHostProjectFileInfoProvider.cs
index 49b8ac357c341..283b5cd6a23dd 100644
--- a/src/Workspaces/MSBuild/Core/MSBuild/BuildHostProjectFileInfoProvider.cs
+++ b/src/Workspaces/MSBuild/Core/MSBuild/BuildHostProjectFileInfoProvider.cs
@@ -7,10 +7,13 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.FileBasedPrograms;
+using Microsoft.CodeAnalysis.Host;
namespace Microsoft.CodeAnalysis.MSBuild;
internal sealed class BuildHostProjectFileInfoProvider(
+ SolutionServices solutionServices,
BuildHostProcessManager buildHostProcessManager,
ProjectFileExtensionRegistry projectFileExtensionRegistry,
DiagnosticReporter diagnosticReporter,
@@ -18,19 +21,43 @@ internal sealed class BuildHostProjectFileInfoProvider(
{
public async Task> LoadProjectFileInfosAsync(string projectPath, DiagnosticReportingOptions reportingOptions, CancellationToken cancellationToken)
{
- if (!projectFileExtensionRegistry.TryGetLanguageNameFromProjectPath(projectPath, reportingOptions.OnLoaderFailure, out var languageName))
+ if (!projectFileExtensionRegistry.TryGetLanguageNameFromProjectPath(projectPath, reportingOptions.OnLoaderFailure, out var languageName, out var isFileBasedApp))
{
return []; // Failure should already be reported.
}
- var preferredBuildHostKind = BuildHostProcessManager.GetKindForProject(projectPath);
+ var preferredBuildHostKind = isFileBasedApp
+ ? BuildHostProcessKind.NetCore
+ : BuildHostProcessManager.GetKindForProject(projectPath);
var (buildHost, _) = await buildHostProcessManager.GetBuildHostWithFallbackAsync(preferredBuildHostKind, projectPath, cancellationToken).ConfigureAwait(false);
- var projectFile = await progress.DoOperationAndReportProgressAsync(
- ProjectLoadOperation.Evaluate,
- projectPath,
- targetFramework: null,
- () => buildHost.LoadProjectFileAsync(projectPath, languageName, cancellationToken)
- ).ConfigureAwait(false);
+
+ RemoteProjectFile projectFile;
+
+ if (isFileBasedApp)
+ {
+ var fileBasedProgramService = solutionServices.GetRequiredService();
+ projectFile = await progress.DoOperationAndReportProgressAsync(
+ ProjectLoadOperation.Evaluate,
+ projectPath,
+ targetFramework: null,
+ () => FileBasedProgramsProjectLoader.LoadFileBasedAppProjectAsync(
+ buildHost,
+ fileBasedProgramService,
+ projectPath,
+ (error) => diagnosticReporter.Report(new WorkspaceDiagnostic(WorkspaceDiagnosticKind.Failure, error)),
+ cancellationToken)
+ ).ConfigureAwait(false);
+ }
+ else
+ {
+ projectFile = await progress.DoOperationAndReportProgressAsync(
+ ProjectLoadOperation.Evaluate,
+ projectPath,
+ targetFramework: null,
+ () => buildHost.LoadProjectFileAsync(projectPath, languageName, cancellationToken)
+ ).ConfigureAwait(false);
+ }
+
await using var _ = projectFile.ConfigureAwait(false);
// If there were any failures during load, we won't be able to build the project. So, bail early with an empty project.
diff --git a/src/Workspaces/MSBuild/Core/MSBuild/FileBasedProgramsProjectLoader.cs b/src/Workspaces/MSBuild/Core/MSBuild/FileBasedProgramsProjectLoader.cs
new file mode 100644
index 0000000000000..8925e8ffb3740
--- /dev/null
+++ b/src/Workspaces/MSBuild/Core/MSBuild/FileBasedProgramsProjectLoader.cs
@@ -0,0 +1,118 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+using Microsoft.CodeAnalysis.FileBasedPrograms;
+using Microsoft.DotNet.FileBasedPrograms;
+
+namespace Microsoft.CodeAnalysis.MSBuild;
+
+internal static class FileBasedProgramsProjectLoader
+{
+ public static async Task LoadFileBasedAppProjectAsync(
+ RemoteBuildHost buildHost,
+ IFileBasedProgramService fileBasedProgramService,
+ string entryPointFilePath,
+ Action reportError,
+ CancellationToken cancellationToken)
+ {
+ var buildService = new FileBasedProgramsBuildService(buildHost, cancellationToken);
+ await using var _ = buildService.ConfigureAwait(false);
+ var projectRootElement = await fileBasedProgramService.LoadFileBasedAppProjectAsync(
+ buildService,
+ FileBasedProgramsBuildService.ProjectCollection,
+ entryPointFilePath,
+ reportError).ConfigureAwait(false);
+ return await buildHost.LoadProjectAsync(
+ projectRootElement.FullPath!,
+ physicalFilePath: entryPointFilePath,
+ projectRootElement.GetRawXml(),
+ LanguageNames.CSharp,
+ globalProperties: fileBasedProgramService.GetGlobalBuildProperties(),
+ cancellationToken).ConfigureAwait(false);
+ }
+}
+
+///
+/// An implementation of which uses MSBuild over RPC (via ).
+///
+file sealed class FileBasedProgramsBuildService(RemoteBuildHost buildHost, CancellationToken cancellationToken) : IBuildService, IAsyncDisposable
+{
+ public static IProjectCollection ProjectCollection => Microsoft.CodeAnalysis.MSBuild.ProjectCollection.Instance;
+
+ public ConcurrentBag Disposables { get; } = [];
+
+ public ValueTask CreateProjectInstanceFromProjectRootElementAsync(
+ IProjectRootElement projectRoot,
+ IProjectCollection projectCollection,
+ IDictionary? additionalGlobalProperties)
+ {
+ return ProjectInstance.FromProjectRootElementAsync(this, buildHost, (ProjectRootElement)projectRoot, (ProjectCollection)projectCollection, additionalGlobalProperties, cancellationToken);
+ }
+
+ public IProjectRootElement CreateProjectRootElement(XmlReader xmlReader, IProjectCollection projectCollection, string entryPointFilePath)
+ {
+ xmlReader.MoveToContent();
+ return new ProjectRootElement(xmlReader.ReadOuterXml());
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ while (Disposables.TryTake(out var disposable))
+ {
+ await disposable.DisposeAsync().ConfigureAwait(false);
+ }
+ }
+}
+
+///
+/// Our adapter for MSBuild's ProjectCollection in abstraction.
+///
+file sealed class ProjectCollection : IProjectCollection
+{
+ public static ProjectCollection Instance { get; } = new();
+
+ private ProjectCollection() { }
+}
+
+///
+/// Our adapter for MSBuild's ProjectInstance in abstraction.
+///
+file sealed class ProjectInstance(RemoteProjectInstance remoteProjectInstance, CancellationToken cancellationToken) : Microsoft.DotNet.FileBasedPrograms.IProjectInstance
+{
+ public static async ValueTask FromProjectRootElementAsync(
+ FileBasedProgramsBuildService service,
+ RemoteBuildHost buildHost,
+ ProjectRootElement projectRoot,
+ ProjectCollection projectCollection,
+ IDictionary? additionalGlobalProperties,
+ CancellationToken cancellationToken)
+ {
+ Debug.Assert(projectCollection == ProjectCollection.Instance);
+ var remoteProjectInstance = await buildHost.LoadProjectInstanceAsync(projectRoot.FullPath!, projectRoot.GetRawXml(), additionalGlobalProperties, cancellationToken).ConfigureAwait(false);
+ service.Disposables.Add(remoteProjectInstance);
+ return new ProjectInstance(remoteProjectInstance, cancellationToken);
+ }
+
+ public ValueTask>> GetItemMetadataValuesAsync(string itemType, ImmutableArray metadataNames) => new(remoteProjectInstance.GetItemMetadataValuesAsync(itemType, metadataNames.ToArray(), cancellationToken));
+ public ValueTask GetPropertyValueAsync(string propertyName) => new(remoteProjectInstance.GetPropertyValueAsync(propertyName, cancellationToken));
+ public ValueTask ExpandStringAsync(string value) => new(remoteProjectInstance.ExpandStringAsync(value, cancellationToken));
+}
+
+///
+/// Our adapter for MSBuild's ProjectRootElement in abstraction.
+///
+file sealed class ProjectRootElement(string content) : IProjectRootElement
+{
+ public string? FullPath { get; set; }
+ public string GetRawXml() => content;
+}
diff --git a/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.cs b/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.cs
index 31e3278e82e26..f92dd0dba8447 100644
--- a/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.cs
+++ b/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.cs
@@ -11,6 +11,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Build.Framework;
+using Microsoft.CodeAnalysis.FileBasedPrograms;
using Microsoft.CodeAnalysis.Host;
using Roslyn.Utilities;
@@ -43,7 +44,7 @@ internal MSBuildProjectLoader(
_diagnosticReporter = diagnosticReporter;
_loggerFactory = new Microsoft.Extensions.Logging.LoggerFactory([new DiagnosticReporterLoggerProvider(_diagnosticReporter)]);
_pathResolver = new PathResolver(_diagnosticReporter);
- _projectFileExtensionRegistry = new ProjectFileExtensionRegistry(diagnosticReporter);
+ _projectFileExtensionRegistry = new ProjectFileExtensionRegistry(diagnosticReporter, solutionServices.GetService());
Properties = ImmutableDictionary.Create(StringComparer.OrdinalIgnoreCase);
@@ -263,6 +264,7 @@ private async Task> LoadInfoAsync(
await using var _ = buildHostProcessManager.ConfigureAwait(false);
var projectFileProvider = new BuildHostProjectFileInfoProvider(
+ _solutionServices,
buildHostProcessManager,
_projectFileExtensionRegistry,
_diagnosticReporter,
diff --git a/src/Workspaces/MSBuild/Core/MSBuild/MSBuildWorkspace.cs b/src/Workspaces/MSBuild/Core/MSBuild/MSBuildWorkspace.cs
index 9b47a595208dc..6069b3c46d575 100644
--- a/src/Workspaces/MSBuild/Core/MSBuild/MSBuildWorkspace.cs
+++ b/src/Workspaces/MSBuild/Core/MSBuild/MSBuildWorkspace.cs
@@ -13,6 +13,7 @@
using System.Threading.Tasks;
using Microsoft.Build.Framework;
using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.FileBasedPrograms;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.Extensions;
@@ -220,13 +221,7 @@ public async Task OpenSolutionAsync(
return this.CurrentSolution;
}
- ///
- /// Open a project file and all referenced projects.
- ///
- /// The path to the project file to be opened. This may be an absolute path or a path relative to the
- /// current working directory.
- /// An optional that will receive updates as the project is opened.
- /// An optional to allow cancellation of this operation.
+ ///
#pragma warning disable RS0026 // Special case to avoid ILogger type getting loaded in downstream clients
public Task OpenProjectAsync(
#pragma warning restore RS0026
@@ -241,8 +236,13 @@ public Task OpenProjectAsync(
/// The path to the project file to be opened. This may be an absolute path or a path relative to the
/// current working directory.
/// An optional that will receive updates as the project is opened.
- /// An optional that will log msbuild results..
+ /// An optional that will log msbuild results.
/// An optional to allow cancellation of this operation.
+ ///
+ /// Supports file-based apps too (just pass the path to the entry point C# file as ).
+ /// is treated as a file-based app only if it does not have a recognized project file extension (see also ),
+ /// it is a file that exists, and has either the .cs extension, or has the bytes #! (shebang) as the first two bytes of its content.
+ ///
#pragma warning disable RS0026 // Special case to avoid ILogger type getting loaded in downstream clients
public async Task OpenProjectAsync(
#pragma warning restore RS0026
@@ -355,12 +355,29 @@ protected override void ApplyProjectChanges(ProjectChanges projectChanges)
return;
}
- if (_loader.ProjectFileExtensionRegistry.TryGetLanguageNameFromProjectPath(projectPath, DiagnosticReportingMode.Log, out var languageName))
+ if (_loader.ProjectFileExtensionRegistry.TryGetLanguageNameFromProjectPath(projectPath, DiagnosticReportingMode.Log, out var languageName, out var isFileBasedApp))
{
try
{
- var buildHost = _applyChangesBuildHostProcessManager.GetBuildHostWithFallbackAsync(projectPath, CancellationToken.None).Result;
- _applyChangesProjectFile = buildHost.LoadProjectFileAsync(projectPath, languageName, CancellationToken.None).Result;
+ var preferredBuildHostKind = isFileBasedApp
+ ? BuildHostProcessKind.NetCore
+ : BuildHostProcessManager.GetKindForProject(projectPath);
+ var (buildHost, _) = _applyChangesBuildHostProcessManager.GetBuildHostWithFallbackAsync(preferredBuildHostKind, projectPath, CancellationToken.None).Result;
+
+ if (isFileBasedApp)
+ {
+ var fileBasedProgramService = this.Services.GetRequiredService();
+ _applyChangesProjectFile = FileBasedProgramsProjectLoader.LoadFileBasedAppProjectAsync(
+ buildHost,
+ fileBasedProgramService,
+ projectPath,
+ (error) => Reporter.Report(new WorkspaceDiagnostic(WorkspaceDiagnosticKind.Failure, error)),
+ CancellationToken.None).Result;
+ }
+ else
+ {
+ _applyChangesProjectFile = buildHost.LoadProjectFileAsync(projectPath, languageName, CancellationToken.None).Result;
+ }
}
catch (IOException exception)
{
diff --git a/src/Workspaces/MSBuild/Core/MSBuild/ProjectFileExtensionRegistry.cs b/src/Workspaces/MSBuild/Core/MSBuild/ProjectFileExtensionRegistry.cs
index 4af610b8adf33..9d6fa1cae7ae7 100644
--- a/src/Workspaces/MSBuild/Core/MSBuild/ProjectFileExtensionRegistry.cs
+++ b/src/Workspaces/MSBuild/Core/MSBuild/ProjectFileExtensionRegistry.cs
@@ -4,10 +4,10 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
-using System.Linq;
-using Microsoft.CodeAnalysis.Host;
+using Microsoft.CodeAnalysis.FileBasedPrograms;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.MSBuild;
@@ -15,12 +15,14 @@ namespace Microsoft.CodeAnalysis.MSBuild;
internal sealed class ProjectFileExtensionRegistry
{
private readonly DiagnosticReporter _diagnosticReporter;
+ private readonly IFileBasedProgramService? _fileBasedProgramService;
private readonly Dictionary _extensionToLanguageMap;
private readonly NonReentrantLock _dataGuard;
- public ProjectFileExtensionRegistry(DiagnosticReporter diagnosticReporter)
+ public ProjectFileExtensionRegistry(DiagnosticReporter diagnosticReporter, IFileBasedProgramService? fileBasedProgramService)
{
_diagnosticReporter = diagnosticReporter;
+ _fileBasedProgramService = fileBasedProgramService;
_extensionToLanguageMap = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
@@ -45,24 +47,43 @@ public void AssociateFileExtensionWithLanguage(string fileExtension, string lang
public bool TryGetLanguageNameFromProjectPath(string? projectFilePath, DiagnosticReportingMode mode, [NotNullWhen(true)] out string? languageName)
{
- using (_dataGuard.DisposableWait())
+ return TryGetLanguageNameFromProjectPath(projectFilePath, mode, out languageName, out _);
+ }
+
+ public bool TryGetLanguageNameFromProjectPath(string? projectFilePath, DiagnosticReportingMode mode, [NotNullWhen(true)] out string? languageName, out bool isFileBasedApp)
+ {
+ var extension = Path.GetExtension(projectFilePath);
+ if (extension is null)
{
- var extension = Path.GetExtension(projectFilePath);
- if (extension is null)
- {
- languageName = null;
- _diagnosticReporter.Report(mode, $"Project file path was 'null'");
- return false;
- }
+ languageName = null;
+ isFileBasedApp = false;
+ _diagnosticReporter.Report(mode, $"Project file path was 'null'");
+ return false;
+ }
+
+ Debug.Assert(projectFilePath != null);
- if (extension is ['.', .. var rest])
- extension = rest;
+ if (extension is ['.', .. var rest])
+ extension = rest;
+ using (_dataGuard.DisposableWait())
+ {
if (_extensionToLanguageMap.TryGetValue(extension, out languageName))
+ {
+ isFileBasedApp = false;
return true;
+ }
+ }
- _diagnosticReporter.Report(mode, string.Format(WorkspacesResources.Cannot_open_project_0_because_the_file_extension_1_is_not_associated_with_a_language, projectFilePath, Path.GetExtension(projectFilePath)));
- return false;
+ if (_fileBasedProgramService?.IsValidEntryPointPath(projectFilePath) == true)
+ {
+ languageName = LanguageNames.CSharp;
+ isFileBasedApp = true;
+ return true;
}
+
+ isFileBasedApp = false;
+ _diagnosticReporter.Report(mode, string.Format(WorkspacesResources.Cannot_open_project_0_because_the_file_extension_1_is_not_associated_with_a_language, projectFilePath, Path.GetExtension(projectFilePath)));
+ return false;
}
}
diff --git a/src/Workspaces/MSBuild/Core/Rpc/RemoteBuildHost.cs b/src/Workspaces/MSBuild/Core/Rpc/RemoteBuildHost.cs
index ae548a178bc1d..1719ebd06b7b9 100644
--- a/src/Workspaces/MSBuild/Core/Rpc/RemoteBuildHost.cs
+++ b/src/Workspaces/MSBuild/Core/Rpc/RemoteBuildHost.cs
@@ -56,14 +56,22 @@ public async Task LoadProjectFileAsync(string projectFilePath
/// Permits loading a project file which only exists in-memory, for example, for file-based program scenarios.
///
/// A path to a project file which may or may not exist on disk. Note that an extension that is known by MSBuild, such as .csproj or .vbproj, should be used here.
+ /// The original C# file path.
/// The project file XML content.
- public async Task LoadProjectAsync(string projectFilePath, string projectContent, string languageName, CancellationToken cancellationToken)
+ public async Task LoadProjectAsync(string projectFilePath, string? physicalFilePath, string projectContent, string languageName, IDictionary? globalProperties, CancellationToken cancellationToken)
{
- var remoteProjectFileTargetObject = await _client.InvokeAsync(BuildHostTargetObject, nameof(IBuildHost.LoadProject), parameters: [projectFilePath, projectContent, languageName], cancellationToken).ConfigureAwait(false);
+ var remoteProjectFileTargetObject = await _client.InvokeAsync(BuildHostTargetObject, nameof(IBuildHost.LoadProject), parameters: [projectFilePath, physicalFilePath, projectContent, languageName, globalProperties], cancellationToken).ConfigureAwait(false);
return new RemoteProjectFile(_client, remoteProjectFileTargetObject);
}
+ public async Task LoadProjectInstanceAsync(string projectFilePath, string projectContent, IDictionary? additionalGlobalProperties, CancellationToken cancellationToken)
+ {
+ var remoteProjectInstanceTargetObject = await _client.InvokeAsync(BuildHostTargetObject, nameof(IBuildHost.LoadProjectInstance), parameters: [projectFilePath, projectContent, additionalGlobalProperties], cancellationToken).ConfigureAwait(false);
+
+ return new RemoteProjectInstance(_client, remoteProjectInstanceTargetObject);
+ }
+
public Task TryGetProjectOutputPathAsync(string projectFilePath, CancellationToken cancellationToken)
=> _client.InvokeNullableAsync(BuildHostTargetObject, nameof(IBuildHost.TryGetProjectOutputPathAsync), parameters: [projectFilePath], cancellationToken);
diff --git a/src/Workspaces/MSBuild/Core/Rpc/RemoteProjectInstance.cs b/src/Workspaces/MSBuild/Core/Rpc/RemoteProjectInstance.cs
new file mode 100644
index 0000000000000..59ecafa8fd885
--- /dev/null
+++ b/src/Workspaces/MSBuild/Core/Rpc/RemoteProjectInstance.cs
@@ -0,0 +1,44 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Immutable;
+using System.Threading;
+using System.Threading.Tasks;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.MSBuild;
+
+internal sealed class RemoteProjectInstance : IAsyncDisposable
+{
+ private readonly RpcClient _client;
+ private readonly int _remoteProjectInstanceTargetObject;
+
+ public RemoteProjectInstance(RpcClient client, int remoteProjectInstanceTargetObject)
+ {
+ _client = client;
+ _remoteProjectInstanceTargetObject = remoteProjectInstanceTargetObject;
+ }
+
+ public async Task> GetDiagnosticLogItemsAsync(CancellationToken cancellationToken)
+ {
+ var diagnostics = await _client.InvokeAsync(_remoteProjectInstanceTargetObject, nameof(IProjectInstance.GetDiagnosticLogItems), parameters: [], cancellationToken).ConfigureAwait(false);
+ return diagnostics.ToImmutableArray();
+ }
+
+ public async Task>> GetItemMetadataValuesAsync(string itemType, string[] metadataNames, CancellationToken cancellationToken)
+ {
+ var items = await _client.InvokeAsync(_remoteProjectInstanceTargetObject, nameof(IProjectInstance.GetItemMetadataValues), parameters: [itemType, metadataNames], cancellationToken).ConfigureAwait(false);
+ return items.SelectAsArray(values => values.ToImmutableArray());
+ }
+
+ public Task GetPropertyValueAsync(string propertyName, CancellationToken cancellationToken)
+ => _client.InvokeAsync(_remoteProjectInstanceTargetObject, nameof(IProjectInstance.GetPropertyValue), parameters: [propertyName], cancellationToken);
+
+ public Task ExpandStringAsync(string value, CancellationToken cancellationToken)
+ => _client.InvokeAsync(_remoteProjectInstanceTargetObject, nameof(IProjectInstance.ExpandString), parameters: [value], cancellationToken);
+
+ public async ValueTask DisposeAsync()
+ => await _client.InvokeAsync(_remoteProjectInstanceTargetObject, nameof(IProjectInstance.Dispose), parameters: [], CancellationToken.None).ConfigureAwait(false);
+}
diff --git a/src/Workspaces/MSBuild/Test/NetCoreTests.cs b/src/Workspaces/MSBuild/Test/NetCoreTests.cs
index 0c7f182c92af4..15547c981a035 100644
--- a/src/Workspaces/MSBuild/Test/NetCoreTests.cs
+++ b/src/Workspaces/MSBuild/Test/NetCoreTests.cs
@@ -137,7 +137,7 @@ public async Task TestOpenInMemoryProject_NetCoreApp()
await using var buildHostProcessManager = new BuildHostProcessManager([LanguageNames.CSharp], ImmutableDictionary.Empty);
var buildHost = await buildHostProcessManager.GetBuildHostAsync(BuildHostProcessKind.NetCore, CancellationToken.None);
- var projectFile = await buildHost.LoadProjectAsync(projectFilePath, content, LanguageNames.CSharp, CancellationToken.None);
+ var projectFile = await buildHost.LoadProjectAsync(projectFilePath, physicalFilePath: null, content, LanguageNames.CSharp, globalProperties: null, CancellationToken.None);
var projectFileInfo = (await projectFile.GetProjectFileInfosAsync(CancellationToken.None)).Single();
Assert.Equal(Path.Combine(projectDir, "bin", "Debug", "netcoreapp3.1", "Project.dll"), projectFileInfo.OutputFilePath);
@@ -633,6 +633,375 @@ public async Task TestOpenProject_OverrideTFM()
Assert.Contains(workspace.CurrentSolution.Projects, p => p.Name == "Library(net5)");
}
+ [ConditionalFact(typeof(DotNetSdkMSBuildInstalled))]
+ [Trait(Traits.Feature, Traits.Features.MSBuildWorkspace)]
+ [Trait(Traits.Feature, Traits.Features.NetCore)]
+ public async Task TestOpenProject_FileBasedApp()
+ {
+ var sourceText = """
+ Console.WriteLine("Hello World!");
+ """;
+
+ CreateFiles(new FileSet(("Program.cs", sourceText)));
+
+ var sourceFilePath = GetSolutionFileName("Program.cs");
+
+ using var workspace = CreateMSBuildWorkspace();
+ var project = await workspace.OpenProjectAsync(sourceFilePath);
+
+ Assert.Empty(workspace.Diagnostics);
+
+ // Assert that there is a single project loaded.
+ Assert.Single(workspace.CurrentSolution.ProjectIds);
+
+ // Assert that the project contains the source file.
+ var document = project.Documents.Single(static d => d.Name == "Program.cs");
+
+ // Assert that the document content matches.
+ var text = await document.GetTextAsync();
+ Assert.Equal(sourceText, text.ToString());
+
+ // Assert that there are references.
+ Assert.Empty(project.AllProjectReferences);
+ Assert.NotEmpty(project.AnalyzerReferences);
+ Assert.NotEmpty(project.MetadataReferences);
+
+ // Assert that there are no compilation errors.
+ var compilation = await project.GetCompilationAsync();
+ compilation.GetDiagnostics().Where(d => d.Severity > DiagnosticSeverity.Hidden).Verify();
+ Assert.Contains("DEBUG", compilation.SyntaxTrees.First().Options.PreprocessorSymbolNames);
+ }
+
+ [ConditionalFact(typeof(DotNetSdkMSBuildInstalled))]
+ [Trait(Traits.Feature, Traits.Features.MSBuildWorkspace)]
+ [Trait(Traits.Feature, Traits.Features.NetCore)]
+ public async Task TestOpenProject_FileBasedApp_NoExtension()
+ {
+ var sourceText = """
+ #!/usr/bin/env dotnet
+ Console.WriteLine("Hello World!");
+ """;
+
+ CreateFiles(new FileSet(("Program", sourceText)));
+
+ var sourceFilePath = GetSolutionFileName("Program");
+
+ using var workspace = CreateMSBuildWorkspace();
+ var project = await workspace.OpenProjectAsync(sourceFilePath);
+
+ Assert.Empty(workspace.Diagnostics);
+
+ // Assert that there is a single project loaded.
+ Assert.Single(workspace.CurrentSolution.ProjectIds);
+
+ // Assert that the project contains the source file.
+ var document = project.Documents.Single(static d => d.Name == "Program");
+
+ // Assert that the document content matches.
+ var text = await document.GetTextAsync();
+ Assert.Equal(sourceText, text.ToString());
+
+ // Assert that there are references.
+ Assert.Empty(project.AllProjectReferences);
+ Assert.NotEmpty(project.AnalyzerReferences);
+ Assert.NotEmpty(project.MetadataReferences);
+
+ // Assert that there are no compilation errors.
+ var compilation = await project.GetCompilationAsync();
+ compilation.GetDiagnostics().Where(d => d.Severity > DiagnosticSeverity.Hidden).Verify();
+ }
+
+ [ConditionalFact(typeof(DotNetSdkMSBuildInstalled))]
+ [Trait(Traits.Feature, Traits.Features.MSBuildWorkspace)]
+ [Trait(Traits.Feature, Traits.Features.NetCore)]
+ public async Task TestOpenProject_FileBasedApp_NoExtension_NoShebang()
+ {
+ var sourceText = """
+ Console.WriteLine("Hello World!");
+ """;
+
+ CreateFiles(new FileSet(("Program", sourceText)));
+
+ var sourceFilePath = GetSolutionFileName("Program");
+
+ using var workspace = CreateMSBuildWorkspace();
+
+ // System.InvalidOperationException : Cannot open project 'Program' because the file extension '' is not associated with a language.
+ await Assert.ThrowsAsync(async () => await workspace.OpenProjectAsync(sourceFilePath));
+
+ Assert.Empty(workspace.Diagnostics);
+ Assert.Empty(workspace.CurrentSolution.ProjectIds);
+ }
+
+ [ConditionalFact(typeof(DotNetSdkMSBuildInstalled))]
+ [Trait(Traits.Feature, Traits.Features.MSBuildWorkspace)]
+ [Trait(Traits.Feature, Traits.Features.NetCore)]
+ public async Task TestOpenProject_FileBasedApp_AssociateFileExtensionWithLanguage()
+ {
+ var sourceText = """
+ Console.WriteLine("Hello World!");
+ """;
+
+ CreateFiles(new FileSet(("Program.cs", sourceText)));
+
+ var sourceFilePath = GetSolutionFileName("Program.cs");
+
+ using var workspace = CreateMSBuildWorkspace();
+ workspace.AssociateFileExtensionWithLanguage("cs", LanguageNames.CSharp);
+ await workspace.OpenProjectAsync(sourceFilePath);
+
+ Assert.Collection(workspace.Diagnostics,
+ d =>
+ {
+ // [Failure] Msbuild failed when processing the file 'Program.cs' with message:
+ // The project file could not be loaded. Data at the root level is invalid. Line 1, position 1.
+ Assert.Equal(WorkspaceDiagnosticKind.Failure, d.Kind);
+ Assert.Contains("Program.cs", d.Message);
+ });
+ }
+
+ [ConditionalFact(typeof(DotNetSdkMSBuildInstalled))]
+ [Trait(Traits.Feature, Traits.Features.MSBuildWorkspace)]
+ [Trait(Traits.Feature, Traits.Features.NetCore)]
+ public async Task TestOpenProject_FileBasedApp_Diagnostics()
+ {
+ var sourceText = """
+ #:unknown-directive
+ Console.WriteLine("Hello World!");
+ """;
+
+ CreateFiles(new FileSet(("Program.cs", sourceText)));
+
+ var sourceFilePath = GetSolutionFileName("Program.cs");
+
+ using var workspace = CreateMSBuildWorkspace();
+ var project = await workspace.OpenProjectAsync(sourceFilePath);
+
+ Assert.Collection(workspace.Diagnostics,
+ d =>
+ {
+ Assert.Equal(WorkspaceDiagnosticKind.Failure, d.Kind);
+ Assert.Contains("Program.cs(1):", d.Message);
+ Assert.Contains("unknown-directive", d.Message);
+ });
+
+ // Assert that there are references.
+ Assert.Empty(project.AllProjectReferences);
+ Assert.NotEmpty(project.AnalyzerReferences);
+ Assert.NotEmpty(project.MetadataReferences);
+
+ // Assert that there are no compilation errors.
+ var compilation = await project.GetCompilationAsync();
+ compilation.GetDiagnostics().Where(d => d.Severity > DiagnosticSeverity.Hidden).Verify();
+ }
+
+ [ConditionalFact(typeof(DotNetSdkMSBuildInstalled))]
+ [Trait(Traits.Feature, Traits.Features.MSBuildWorkspace)]
+ [Trait(Traits.Feature, Traits.Features.NetCore)]
+ public async Task TestOpenProject_FileBasedApp_RefDirective()
+ {
+ CreateFiles(new FileSet(
+ ("Program.cs", """
+ #:property ExperimentalFileBasedProgramEnableRefDirective=true
+ #:ref Util.cs
+ Console.WriteLine($"Hello {Util.M()}!");
+ """),
+ ("Util.cs", """
+ #:property OutputType=Library
+ public static class Util
+ {
+ public static string M() => "Util";
+ }
+ """)));
+
+ var sourceFilePath = GetSolutionFileName("Program.cs");
+
+ using var workspace = CreateMSBuildWorkspace();
+ var project = await workspace.OpenProjectAsync(sourceFilePath);
+
+ Assert.Empty(workspace.Diagnostics);
+
+ Assert.Equal(["Program", "Util"], workspace.CurrentSolution.Projects.Select(p => p.Name).Order());
+
+ var projRef = Assert.Single(project.ProjectReferences);
+ Assert.Equal(projRef.ProjectId, workspace.CurrentSolution.Projects.Single(p => p.Name == "Util").Id);
+
+ // Assert that there are references.
+ Assert.Same(projRef, Assert.Single(project.AllProjectReferences));
+ Assert.NotEmpty(project.AnalyzerReferences);
+ Assert.NotEmpty(project.MetadataReferences);
+
+ // Assert that there are no compilation errors.
+ var compilation = await project.GetCompilationAsync();
+ compilation.GetDiagnostics().Where(d => d.Severity > DiagnosticSeverity.Hidden).Verify();
+ }
+
+ [ConditionalFact(typeof(DotNetSdkMSBuildInstalled))]
+ [Trait(Traits.Feature, Traits.Features.MSBuildWorkspace)]
+ [Trait(Traits.Feature, Traits.Features.NetCore)]
+ public async Task TestOpenProject_FileBasedApp_RefDirective_Duplicate()
+ {
+ CreateFiles(new FileSet(
+ ("Program.cs", """
+ #:property ExperimentalFileBasedProgramEnableRefDirective=true
+ #:ref Util.cs
+ #:ref Util.cs
+ Console.WriteLine($"Hello {Util.M()}!");
+ """),
+ ("Util.cs", """
+ #:property OutputType=Library
+ public static class Util
+ {
+ public static string M() => "Util";
+ }
+ """)));
+
+ var sourceFilePath = GetSolutionFileName("Program.cs");
+
+ using var workspace = CreateMSBuildWorkspace();
+ var project = await workspace.OpenProjectAsync(sourceFilePath);
+
+ Assert.Empty(workspace.Diagnostics);
+
+ Assert.Equal(["Program", "Util"], workspace.CurrentSolution.Projects.Select(p => p.Name).Order());
+
+ var projRef = Assert.Single(project.ProjectReferences);
+ Assert.Equal(projRef.ProjectId, workspace.CurrentSolution.Projects.Single(p => p.Name == "Util").Id);
+
+ // Assert that there are references.
+ Assert.Same(projRef, Assert.Single(project.AllProjectReferences));
+ Assert.NotEmpty(project.AnalyzerReferences);
+ Assert.NotEmpty(project.MetadataReferences);
+
+ // Assert that there are no compilation errors.
+ var compilation = await project.GetCompilationAsync();
+ compilation.GetDiagnostics().Where(d => d.Severity > DiagnosticSeverity.Hidden).Verify();
+ }
+
+ [ConditionalFact(typeof(DotNetSdkMSBuildInstalled))]
+ [Trait(Traits.Feature, Traits.Features.MSBuildWorkspace)]
+ [Trait(Traits.Feature, Traits.Features.NetCore)]
+ public async Task TestOpenProject_FileBasedApp_RefDirective_Self()
+ {
+ CreateFiles(new FileSet(
+ ("Program.cs", """
+ #:property ExperimentalFileBasedProgramEnableRefDirective=true
+ #:ref Program.cs
+ Console.WriteLine("Hello");
+ """)));
+
+ var sourceFilePath = GetSolutionFileName("Program.cs");
+
+ using var workspace = CreateMSBuildWorkspace();
+ var project = await workspace.OpenProjectAsync(sourceFilePath);
+
+ Assert.Empty(workspace.Diagnostics);
+
+ Assert.Equal(["Program"], workspace.CurrentSolution.Projects.Select(p => p.Name).Order());
+
+ var projRef = Assert.Single(project.ProjectReferences);
+ Assert.Equal(projRef.ProjectId, workspace.CurrentSolution.Projects.Single().Id);
+
+ // Assert that there are references.
+ Assert.Same(projRef, Assert.Single(project.AllProjectReferences));
+ Assert.NotEmpty(project.AnalyzerReferences);
+ Assert.NotEmpty(project.MetadataReferences);
+
+ // Can't assert that there are no compilation errors because self-referencing projects currently hang when calling GetCompilationAsync.
+ // See https://github.com/dotnet/roslyn/issues/84587.
+ using var cts = new CancellationTokenSource(100);
+ await Assert.ThrowsAnyAsync(() => project.GetCompilationAsync(cts.Token));
+ }
+
+ [ConditionalFact(typeof(DotNetSdkMSBuildInstalled))]
+ [Trait(Traits.Feature, Traits.Features.MSBuildWorkspace)]
+ [Trait(Traits.Feature, Traits.Features.NetCore)]
+ public async Task TestOpenProject_FileBasedApp_RefDirective_GlobalProperty()
+ {
+ CreateFiles(new FileSet(
+ ("Program.cs", """
+ #:property ExperimentalFileBasedProgramEnableRefDirective=true
+ #:ref $(MyReferencedFileName).cs
+ Console.WriteLine($"Hello {Util.M()}!");
+ """),
+ ("Util.cs", """
+ #:property OutputType=Library
+ public static class Util
+ {
+ public static string M() => "Util";
+ }
+ """)));
+
+ var sourceFilePath = GetSolutionFileName("Program.cs");
+
+ using var workspace = CreateMSBuildWorkspace(("MyReferencedFileName", "Util"));
+ var project = await workspace.OpenProjectAsync(sourceFilePath);
+
+ Assert.Empty(workspace.Diagnostics);
+
+ Assert.Equal(["Program", "Util"], workspace.CurrentSolution.Projects.Select(p => p.Name).Order());
+
+ var projRef = Assert.Single(project.ProjectReferences);
+ Assert.Equal(projRef.ProjectId, workspace.CurrentSolution.Projects.Single(p => p.Name == "Util").Id);
+
+ // Assert that there are references.
+ Assert.Same(projRef, Assert.Single(project.AllProjectReferences));
+ Assert.NotEmpty(project.AnalyzerReferences);
+ Assert.NotEmpty(project.MetadataReferences);
+
+ // Assert that there are no compilation errors.
+ var compilation = await project.GetCompilationAsync();
+ compilation.GetDiagnostics().Where(d => d.Severity > DiagnosticSeverity.Hidden).Verify();
+ }
+
+ [ConditionalFact(typeof(DotNetSdkMSBuildInstalled))]
+ [Trait(Traits.Feature, Traits.Features.MSBuildWorkspace)]
+ [Trait(Traits.Feature, Traits.Features.NetCore)]
+ public async Task TestOpenProject_FileBasedApp_AddProjectReference()
+ {
+ CreateFiles(new FileSet(
+ ("Program.cs", """
+ Util.M();
+ """),
+ ("Util.cs", """
+ #:property OutputType=Library
+ public static class Util
+ {
+ public static string M() => "Util";
+ }
+ """)));
+
+ using var workspace = CreateMSBuildWorkspace();
+ var programProject = await workspace.OpenProjectAsync(GetSolutionFileName("Program.cs"));
+
+ Assert.Empty(workspace.Diagnostics);
+ Assert.Equal(["Program"], workspace.CurrentSolution.Projects.Select(p => p.Name).Order());
+ Assert.Empty(programProject.ProjectReferences);
+
+ var diag = Assert.Single((await programProject.GetCompilationAsync()).GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error && d.GetMessage().Contains("Util")));
+ Assert.Equal("CS0103", diag.Id); // The name 'Util' does not exist in the current context
+
+ var utilProject = await workspace.OpenProjectAsync(GetSolutionFileName("Util.cs"));
+
+ Assert.Empty(workspace.Diagnostics);
+ Assert.Equal(["Program", "Util"], workspace.CurrentSolution.Projects.Select(p => p.Name).Order());
+
+ programProject = workspace.CurrentSolution.Projects.Single(p => p.Name == "Program");
+ Assert.Empty(programProject.ProjectReferences);
+
+ var solution = programProject.AddProjectReference(new ProjectReference(utilProject.Id)).Solution;
+ Assert.True(workspace.TryApplyChanges(solution));
+
+ Assert.Empty(workspace.Diagnostics);
+ Assert.Equal(["Program", "Util"], workspace.CurrentSolution.Projects.Select(p => p.Name).Order());
+
+ programProject = workspace.CurrentSolution.Projects.Single(p => p.Name == "Program");
+ var projRef = Assert.Single(programProject.ProjectReferences);
+ Assert.Equal(projRef.ProjectId, workspace.CurrentSolution.Projects.Single(p => p.Name == "Util").Id);
+
+ Assert.Empty((await programProject.GetCompilationAsync()).GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error && d.GetMessage().Contains("Util")));
+ }
+
[ConditionalFact(typeof(DotNetSdkMSBuildInstalled))]
[Trait(Traits.Feature, Traits.Features.MSBuildWorkspace)]
[Trait(Traits.Feature, Traits.Features.NetCore)]