/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs
369 строк
19 KB
Jan Jones
Allow MSBuildWorkspace to open file-based apps (#84139)
30 июл 2026, 11:03
Не верифицирован
30 июл 2026, 11:03
7e7ebef
Код
Авторство
О чём код?
// 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.CSharp; 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.Options; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; using Microsoft.CommonLanguageServerProtocol.Framework; using Microsoft.Extensions.Logging; using Roslyn.LanguageServer.Protocol; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms; /// <summary>Handles loading both miscellaneous files and file-based program projects.</summary> internal sealed class FileBasedProgramsProjectSystem : LanguageServerProjectLoader, ILspMiscellaneousFilesWorkspaceProvider { private readonly ILspServices _lspServices; private readonly ILogger<FileBasedProgramsProjectSystem> _logger; private readonly CanonicalMiscellaneousFilesProjectProvider _canonicalProjectProvider; /// <summary> /// Virtual (in-memory) projects don't exist on disk, so MSBuild worker nodes /// can't re-evaluate them. Force single-node builds to keep everything in-process. /// </summary> protected override int MaxNodeCount => 1; public FileBasedProgramsProjectSystem( ILspServices lspServices, IGlobalOptionService globalOptionService, ILoggerFactory loggerFactory, IAsynchronousOperationListenerProvider listenerProvider, ServerConfigurationFactory serverConfigurationFactory, IBinLogPathProvider binLogPathProvider, DotnetCliHelper dotnetCliHelper) : base( lspServices, globalOptionService, loggerFactory, listenerProvider, serverConfigurationFactory, binLogPathProvider, dotnetCliHelper) { _lspServices = lspServices; _logger = loggerFactory.CreateLogger<FileBasedProgramsProjectSystem>(); _canonicalProjectProvider = new CanonicalMiscellaneousFilesProjectProvider(lspServices.GetRequiredService<IHostWorkspaceProvider>(), loggerFactory); globalOptionService.AddOptionChangedHandler(this, OnGlobalOptionChanged); } public override void Dispose() { GlobalOptionService.RemoveOptionChangedHandler(this, OnGlobalOptionChanged); base.Dispose(); } private void OnGlobalOptionChanged(object sender, object target, OptionChangedEventArgs args) { foreach (var (key, value) in args.ChangedOptions) { if (key.Option.Equals(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms)) { // This event handler can't be async, so we ignore the resulting task here, // and take care that the ignored call doesn't throw an exception _ = HandleEnableFileBasedProgramsChangedAsync((bool)value!); break; } } async Task HandleEnableFileBasedProgramsChangedAsync(bool value) { using var token = Listener.BeginAsyncOperation(nameof(HandleEnableFileBasedProgramsChangedAsync)); try { _logger.LogDebug($"Detected enableFileBasedPrograms changed to '{value}'. Unloading loose file projects."); await UnloadAllProjectsAsync(); } catch (Exception ex) when (FatalError.ReportAndCatch(ex, ErrorSeverity.General)) { throw ExceptionUtilities.Unreachable(); } } } private static string GetDocumentFilePath(DocumentUri uri) => uri.GetDocumentFilePathFromUri(); private bool ClassifyAsMiscellaneousFileWithNoReferences(string filePath, LanguageInformation languageInformation) { // 2. Is `enableFileBasedPrograms` enabled? // - No → Classify as Miscellaneous File With No References // - Yes → Continue to next check var enableFileBasedPrograms = GlobalOptionService.GetOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms); if (!enableFileBasedPrograms) { return true; } // 3. Is the file a regular C# file? (i.e. not a `.csx` script, and not a file using a language besides C#) // - No → Classify as Miscellaneous File With No References // - Yes → Continue to next check if (languageInformation.LanguageName != LanguageNames.CSharp || MiscellaneousFileUtilities.IsScriptFile(languageInformation, filePath)) { return true; } return false; } private async ValueTask<LooseDocumentKind> ClassifyDocumentAsync(string filePath, string languageId, CancellationToken cancellationToken) { var languageInfoProvider = _lspServices.GetRequiredService<ILanguageInfoProvider>(); if (!languageInfoProvider.TryGetLanguageInformation(ProtocolConversions.CreateAbsoluteDocumentUri(filePath), languageId, out var languageInformation)) { Contract.Fail($"Could not find language information for '{filePath}'"); } // The design of this is described in docs/features/file-based-programs-vscode.md // Note: Step (1) is skipped, as we assume a first-chance lookup in the host workspace will handle this case. // Steps (2) and (3) if (ClassifyAsMiscellaneousFileWithNoReferences(filePath, languageInformation)) { return LooseDocumentKind.MiscellaneousFileWithNoReferences; } // 4. Does the file have an absolute path and exist on disk? (i.e. it is not a "virtual document" created for a new, not-yet-saved file, or similar.) // - Yes → Go to (5) // - No → Classify as Miscellaneous File With Standard References if (!PathUtilities.IsAbsolute(filePath)) return LooseDocumentKind.MiscellaneousFileWithStandardReferences; SourceText? sourceText = IOUtilities.PerformIO(() => { // Note: SourceText.From eagerly reads the entire file using var fileStream = File.OpenRead(filePath); return SourceText.From(fileStream); }); // File had an absolute path but we were unable to read it, due to it not existing or to some other I/O issue. if (sourceText is null) { return LooseDocumentKind.MiscellaneousFileWithStandardReferences; } var parseOptions = CSharpParseOptions.Default.WithFeatures([new("FileBasedProgram", "true")]); var tokenizer = SyntaxFactory.CreateTokenParser(sourceText, parseOptions); var result = tokenizer.ParseLeadingTrivia(); var leadingTrivia = result.Token.LeadingTrivia; // 5. Does the file have '#!' directives? // - Yes → Classify as File-Based App. Restore if needed and show semantic errors. // - No → Continue to next check if (leadingTrivia.Any(SyntaxKind.ShebangDirectiveTrivia)) { return LooseDocumentKind.FileBasedApp; } // 6. Does the file have `#:` directives? // - No → Go to (8) // - Yes → Continue to next check if (leadingTrivia.Any(SyntaxKind.IgnoredDirectiveTrivia)) { // 7. Does the file have top-level statements? // - Yes → Classify as File-Based App. Restore if needed and show semantic errors. // - No → Classify as Miscellaneous File With Standard References if (ContainsTopLevelStatements()) { return LooseDocumentKind.FileBasedApp; } return LooseDocumentKind.MiscellaneousFileWithStandardReferences; } // 8. Is `enableFileBasedProgramsWhenAmbiguous` enabled? (default: `false` in release, `true` in prerelease) // - No → Classify as Miscellaneous File With Standard References // - Yes → Continue to heuristic detection if (!GlobalOptionService.GetOption(LanguageServerProjectSystemOptionsStorage.EnableSemanticErrorsInMiscellaneousFiles)) { return LooseDocumentKind.MiscellaneousFileWithStandardReferences; } // Heuristic Detection: // 9. Are top-level statements present? // - No → Classify as Miscellaneous File With Standard References // - Yes → Continue to next check if (!ContainsTopLevelStatements()) { return LooseDocumentKind.MiscellaneousFileWithStandardReferences; } // 10. Is the file included in a `.csproj` cone? // - Yes → Classify as Miscellaneous File With Standard References (wait for project to load) // - No → Classify as Miscellaneous File With Standard References and Semantic Errors var csprojInConeChecker = _lspServices.GetRequiredService<CsprojInConeChecker>(); if (csprojInConeChecker.IsContainedInCsprojCone(filePath)) { return LooseDocumentKind.MiscellaneousFileWithStandardReferences; } return LooseDocumentKind.MiscellaneousFileWithStandardReferencesAndSemanticErrors; bool ContainsTopLevelStatements() { var syntaxTree = CSharpSyntaxTree.ParseText(sourceText, options: parseOptions, cancellationToken: cancellationToken); return syntaxTree.GetRoot(cancellationToken) is CompilationUnitSyntax compilationUnit && compilationUnit.Members.Any(SyntaxKind.GlobalStatement); } } public async ValueTask<TextDocument?> AddDocumentAsync(DocumentUri documentUri, TrackedDocumentInfo documentInfo) { var languageInfoProvider = _lspServices.GetRequiredService<ILanguageInfoProvider>(); if (!languageInfoProvider.TryGetLanguageInformation(documentUri, documentInfo.LanguageId, out var languageInformation)) { Contract.Fail($"Could not find language information for '{documentUri}'"); } var documentFilePath = GetDocumentFilePath(documentUri); var sourceTextLoader = new SourceTextLoader(documentInfo.SourceText, documentFilePath); var doDesignTimeBuild = !ClassifyAsMiscellaneousFileWithNoReferences(documentFilePath, languageInformation); return await this.GetOrLoadEntryPointDocumentAsync( documentFilePath, sourceTextLoader, languageInformation, documentInfo.SourceText.ChecksumAlgorithm, doDesignTimeBuild); } /// <summary> /// Used to begin loading a file-based app project for a file-based app on disk, if it hasn't started already, /// when the caller doesn't need to use any results of the loading process. /// </summary> public async ValueTask TryBeginLoadingFileBasedAppAsync(string documentFilePath) { Contract.ThrowIfFalse(PathUtilities.IsAbsolute(documentFilePath)); var sourceTextLoader = new WorkspaceFileTextLoader(_workspaceFactory.HostWorkspace.CurrentSolution.Services, documentFilePath, defaultEncoding: null); var languageInfoProvider = _lspServices.GetRequiredService<ILanguageInfoProvider>(); if (!languageInfoProvider.TryGetLanguageInformation(ProtocolConversions.CreateAbsoluteDocumentUri(documentFilePath), lspLanguageId: "csharp", out var languageInformation)) { Contract.Fail($"Could not find language information for '{documentFilePath}'"); } await GetOrLoadEntryPointDocumentAsync(documentFilePath, sourceTextLoader, languageInformation, SourceHashAlgorithms.Default, doDesignTimeBuild: true); } public async ValueTask<TextDocument?> GetOrLoadEntryPointDocumentAsync(string documentFilePath, TextLoader textLoader, LanguageInformation languageInformation, SourceHashAlgorithm checksumAlgorithm, bool doDesignTimeBuild) { var project = await base.GetOrLoadProjectAsync(documentFilePath, _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory, CreatePrimordialProjectInfo, doDesignTimeBuild); return project is null ? null : LookupExistingDocument(project); TextDocument? LookupExistingDocument(Project project) { var document = project.Documents.FirstOrDefault(document => document.FilePath == documentFilePath) ?? project.AdditionalDocuments.FirstOrDefault(document => document.FilePath == documentFilePath); if (document is null) { _logger.LogWarning("Could not get a document for '{documentFilePath}' because its project doesn't contain a document for it", documentFilePath); } return document; } ProjectInfo CreatePrimordialProjectInfo(ProjectSystemProjectFactory projectFactory) { var enableFileBasedPrograms = GlobalOptionService.GetOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms); return MiscellaneousFileUtilities.CreateMiscellaneousProjectInfoForDocument( projectFactory.Workspace, documentFilePath, textLoader, languageInformation, checksumAlgorithm, projectFactory.Workspace.Services.SolutionServices, [], enableFileBasedPrograms); } } public async ValueTask<bool> TryRemoveMiscellaneousDocumentAsync(DocumentUri uri) { // Note: we intentionally do not unload file-based apps in this path. // This is because we want to unload from the miscellaneous files workspace only, when a file is found in the host workspace. var documentPath = GetDocumentFilePath(uri); return await TryUnloadProjectAsync(documentPath, fromProjectFactory: _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory); } public async ValueTask CloseDocumentAsync(DocumentUri uri) { // If automatic discovery is enabled, we don't want to unload a file-based app upon closing a document. var unloadFromProjectFactory = GlobalOptionService.GetOption(FileBasedAppsOptionsStorage.EnableAutomaticDiscovery) ? _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory : null; var documentPath = GetDocumentFilePath(uri); await TryUnloadProjectAsync(documentPath, unloadFromProjectFactory); } protected override async Task<RemoteProjectLoadResult?> TryLoadProjectInMSBuildHostAsync( BuildHostProcessManager buildHostProcessManager, string documentPath, CancellationToken cancellationToken) { // Note: we assume that if we made it this far, the document is for the C# language. var documentKind = await ClassifyDocumentAsync(documentPath, languageId: "csharp", cancellationToken); _logger.LogDebug("Classified '{documentPath}' as '{documentKind}'.", documentPath, documentKind); if (documentKind == LooseDocumentKind.MiscellaneousFileWithNoReferences) { // This might happen due to a race involving changes to option values. // Just don't proceed with the reload and assume the option change handler will unload this project if needed. _logger.LogWarning("A document classified as {documentKind} should not be design-time built.", documentKind); return null; } if (documentKind is LooseDocumentKind.MiscellaneousFileWithStandardReferences or LooseDocumentKind.MiscellaneousFileWithStandardReferencesAndSemanticErrors) { var projectInfos = await _canonicalProjectProvider.GetProjectInfoAsync(documentPath, cancellationToken).ConfigureAwait(false); // Note: We might enter this path when loading a file-based app with no directives. // i.e. whether a file with no directives in it, depends on how user is using the file. // The project system doesn't determine this with 100% certainty and instead just ensures we provide semantic info which is satisfactory for the 99% case. // 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) && _workspaceFactory.HostWorkspace.Services.GetService<IFileBasedProgramService>() is { } fileBasedProgramService && Directory.Exists(fileBasedProgramService.GetArtifactsPath(documentPath)); return new RemoteProjectLoadResult { ProjectFileInfos = projectInfos, DiagnosticLogItems = [], // This points to the Canonical.csproj, which always exists on disk and can be restored regardless of SDK. ProjectRestorePath = projectInfos.FirstOrDefault()?.FilePath, ProjectFactory = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory, IsFileBasedProgram = isFileBasedProgram, HasFileBasedAppDirectives = false, IsMiscellaneousFile = true, HasAllInformation = documentKind is LooseDocumentKind.MiscellaneousFileWithStandardReferencesAndSemanticErrors, PreferredBuildHostKind = BuildHostProcessKind.NetCore, ActualBuildHostKind = BuildHostProcessKind.NetCore, }; } // Fall through to ordinary file-based app handling. Contract.ThrowIfFalse(documentKind is LooseDocumentKind.FileBasedApp); const BuildHostProcessKind buildHostKind = BuildHostProcessKind.NetCore; var buildHost = await buildHostProcessManager.GetBuildHostAsync(buildHostKind, documentPath, dotnetPath: null, cancellationToken); var loadedFile = await FileBasedProgramsProjectLoader.LoadFileBasedAppProjectAsync( buildHost, _workspaceFactory.HostWorkspace.Services.GetRequiredService<IFileBasedProgramService>(), documentPath, (error) => _logger.LogError(error), cancellationToken); return new RemoteProjectLoadResult { ProjectFileInfos = await loadedFile.GetProjectFileInfosAsync(cancellationToken), DiagnosticLogItems = await loadedFile.GetDiagnosticLogItemsAsync(cancellationToken), ProjectRestorePath = documentPath, ProjectFactory = _workspaceFactory.HostProjectFactory, IsFileBasedProgram = true, HasFileBasedAppDirectives = true, IsMiscellaneousFile = false, HasAllInformation = true, PreferredBuildHostKind = buildHostKind, ActualBuildHostKind = buildHostKind, }; } }