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)} + """); + } + + 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)} + """); + } + } + + 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)} + """); + + 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. + error chyba @@ -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. + error Fehler @@ -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. + error error @@ -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. + error erreur @@ -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. + error errore @@ -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. + error błą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. + error erro @@ -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. + error hata @@ -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)]