Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.CSharp.UnitTests;
Expand Down Expand Up @@ -100,6 +101,27 @@ public async Task PathRecommendation_02()
await VerifyItemExistsAsync(markup, expectedItem: "Project.csproj");
}

[Fact]
public async Task PathRecommendation_03()
{
// Test a virtual file scenario (e.g. ctrl+N in VS Code or other cases where there is not an actual file on disk.)
var code = """
#:project $$
""";

var markup = $"""
<Workspace>
<Project Language="C#" CommonReferences="true" AssemblyName="Test1" Features="FileBasedProgram=true">
Copy link
Member

@jcouv jcouv Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Features="FileBasedProgram=true"

Did this test fail before the changes in this PR?
The reason I'm asking is because I see logic elsewhere that adds the "MiscellaneousFile" feature and therefore it's not clear whether this test should have the "FileBasedProgram" feature flag. #Closed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it passed. It’s reasonable for some of these tests to use FileBasedProgram feature flag, because we want to see how the editor features behave on file-based programs.

However I do think the actual applications of FileBasedProgram versus MiscellaneousFile feature names requires some discussion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the description of the bug being fixed, I expected a test like this one but without FileBasedProgram and with MiscellaneousFile. It used to fail, but now it would pass. Did I understand right?

Copy link
Member Author

@RikkiGibson RikkiGibson Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolving by deleting MiscellaneousFile flag from the compiler layer. (i.e. reverting all compiler changes in this PR.)

<Document FilePath="Untitled-1" ResolveFilePath="false"><![CDATA[{code}]]></Document>
</Project>
</Workspace>
""";

// In this case, only stuff like drive roots would be recommended.
var expectedRoot = PlatformInformation.IsWindows ? "C:" : "/";
await VerifyItemExistsAsync(markup, expectedRoot);
}

// Note: The editor uses a shared mechanism to filter out completion items which don't match the prefix of what the user is typing.
// Therefore we do not have "negative tests" here for file names.
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,12 @@ protected override async Task AddDirectiveContentCompletionsAsync(CompletionCont
// Suppose we have a directive '#:project path/to/pr$$'
// In this case, 'contentPrefix' is 'path/to/pr'.

var documentDirectory = PathUtilities.GetDirectoryName(context.Document.FilePath);
var fileSystemHelper = new FileSystemCompletionHelper(
Glyph.OpenFolder,
Glyph.CSharpProject,
searchPaths: [],
baseDirectory: PathUtilities.GetDirectoryName(context.Document.FilePath),
baseDirectory: PathUtilities.IsAbsolute(documentDirectory) ? documentDirectory : null,
[".csproj", ".vbproj"],
CompletionItemRules.Default);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,20 @@ internal static ProjectInfo CreateMiscellaneousProjectInfoForDocument(
// https://devdiv.visualstudio.com/DevDiv/_workitems/edit/575761
var parseOptions = languageServices.GetService<ISyntaxTreeFactoryService>()?.GetDefaultParseOptionsWithLatestLanguageVersion();

if (parseOptions != null &&
compilationOptions != null &&
languageInformation.ScriptExtension is not null &&
fileExtension == languageInformation.ScriptExtension)
if (parseOptions != null)
{
parseOptions = parseOptions.WithKind(SourceCodeKind.Script);
compilationOptions = GetCompilationOptionsWithScriptReferenceResolvers(services, compilationOptions, filePath);
}

if (parseOptions != null && languageInformation.ScriptExtension is not null && fileExtension != languageInformation.ScriptExtension)
{
// Any non-script misc file should not complain about usage of '#:' ignored directives.
parseOptions = parseOptions.WithFeatures([.. parseOptions.Features, new("FileBasedProgram", "true")]);
if (compilationOptions != null &&
languageInformation.ScriptExtension is not null &&
fileExtension == languageInformation.ScriptExtension)
{
parseOptions = parseOptions.WithKind(SourceCodeKind.Script);
compilationOptions = GetCompilationOptionsWithScriptReferenceResolvers(services, compilationOptions, filePath);
}
else
{
// Any non-script misc file should not complain about usage of '#:' ignored directives.
parseOptions = parseOptions.WithFeatures([.. parseOptions.Features, new("FileBasedProgram", "true")]);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change makes it so if there are parse options, then we are either going to treat this as a script, or we'll pass the FileBasedProgram flag (to avoid reporting possibly unwanted errors for #:). Not neither, not both.

}
}

var projectId = ProjectId.CreateNewId(debugName: $"{workspace.GetType().Name} Files Project for {filePath}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
using Microsoft.CodeAnalysis.LanguageServer.UnitTests.Miscellaneous;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Test.Utilities;
Expand Down Expand Up @@ -146,13 +145,9 @@ await testLspServer.OpenDocumentAsync(nonFileUri, """
Assert.Equal(2, primordialDocument.Project.Documents.Count());
Assert.Empty(primordialDocument.Project.MetadataReferences);

// No errors for '#:' are expected.
var primordialSyntaxTree = await primordialDocument.GetRequiredSyntaxTreeAsync(CancellationToken.None);
// TODO: we probably don't want to report syntax errors for '#:' in the primordial non-file document.
// The logic which decides whether to add '-features:FileBasedProgram' probably needs to be adjusted.
primordialSyntaxTree.GetDiagnostics(CancellationToken.None).Verify(
// vscode-notebook-cell://dev-container/test.cs(1,2): error CS9298: '#:' directives can be only used in file-based programs ('-features:FileBasedProgram')"
// #:sdk Microsoft.Net.Sdk
TestHelpers.Diagnostic(code: 9298, squiggledText: ":").WithLocation(1, 2));
Assert.Empty(primordialSyntaxTree.GetDiagnostics(CancellationToken.None));

// Wait for the canonical project to finish loading.
await testLspServer.TestWorkspace.GetService<AsynchronousOperationListenerProvider>().GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync();
Expand All @@ -165,13 +160,47 @@ await testLspServer.OpenDocumentAsync(nonFileUri, """
// Should have the appropriate generated files now that we ran a design time build
Assert.Contains(canonicalDocument.Project.Documents, d => d.Name == "Canonical.AssemblyInfo.cs");

// No errors for '#:' are expected.
var canonicalSyntaxTree = await canonicalDocument.GetRequiredSyntaxTreeAsync(CancellationToken.None);
// TODO: we probably don't want to report syntax errors for '#:' in the canonical non-file document.
// The logic which decides whether to add '-features:FileBasedProgram' probably needs to be adjusted.
canonicalSyntaxTree.GetDiagnostics(CancellationToken.None).Verify(
// vscode-notebook-cell://dev-container/test.cs(1,2): error CS9298: '#:' directives can be only used in file-based programs ('-features:FileBasedProgram')"
Assert.Empty(canonicalSyntaxTree.GetDiagnostics(CancellationToken.None));
}

[Theory, CombinatorialData]
public async Task TestScriptsWithIgnoredDirectives(bool mutatingLspWorkspace)
{
// https://github.com/dotnet/roslyn/issues/81644: A csx script with '#:' directives should not be loaded as a file-based program
await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer });
Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer));

var nonFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\script.csx");
await testLspServer.OpenDocumentAsync(nonFileUri, """
#:sdk Microsoft.Net.Sdk
Console.WriteLine("Hello World");
""").ConfigureAwait(false);

// File is added to a miscellaneous project containing only the script.
var (_, primordialDocument) = await GetRequiredLspWorkspaceAndDocumentAsync(nonFileUri, testLspServer).ConfigureAwait(false);
Assert.Equal(1, primordialDocument.Project.Documents.Count());
Assert.Empty(primordialDocument.Project.MetadataReferences);

// FileBasedProgram feature flag is not passed, so an error is expected on '#:'.
var primordialSyntaxTree = await primordialDocument.GetRequiredSyntaxTreeAsync(CancellationToken.None);
primordialSyntaxTree.GetDiagnostics(CancellationToken.None).Verify(
// script.csx(1,2): error CS9298: '#:' directives can be only used in file-based programs ('-features:FileBasedProgram')"
Copy link
Member Author

@RikkiGibson RikkiGibson Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We wouldn't want to report this error for a misc file like Untitled-1, which is likely to be ordinary .cs code. But I think it's good to report it when we know it is a .csx script.

// #:sdk Microsoft.Net.Sdk
TestHelpers.Diagnostic(code: 9298, squiggledText: ":").WithLocation(1, 2));

// Wait for project load to finish
await testLspServer.TestWorkspace.GetService<AsynchronousOperationListenerProvider>().GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync();

var (miscWorkspace, canonicalDocument) = await GetRequiredLspWorkspaceAndDocumentAsync(nonFileUri, testLspServer).ConfigureAwait(false);
Assert.Equal(WorkspaceKind.Host, miscWorkspace.Kind);
Assert.NotNull(canonicalDocument);
Assert.NotEqual(primordialDocument, canonicalDocument);
Assert.Contains(canonicalDocument.Project.Documents, d => d.Name == "script.AssemblyInfo.cs");

var canonicalSyntaxTree = await canonicalDocument.GetRequiredSyntaxTreeAsync(CancellationToken.None);
Assert.Empty(canonicalSyntaxTree.GetDiagnostics(CancellationToken.None));
}

[Theory, CombinatorialData]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,12 +244,15 @@ private TextDocument CreatePrimordialProjectAndAddDocument_NoLock(string documen
protected override async Task<RemoteProjectLoadResult?> TryLoadProjectInMSBuildHostAsync(
BuildHostProcessManager buildHostProcessManager, string documentPath, CancellationToken cancellationToken)
{
// Set the FileBasedProgram feature flag so that '#:' is permitted without errors in rich misc files.
// This allows us to avoid spurious errors for files which contain '#:' directives yet are not treated as file-based programs (due to not being saved to disk, for example.)
var virtualProjectXml = $"""
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Features>$(Features);FileBasedProgram</Features>
</PropertyGroup>
</Project>
""";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -672,7 +672,8 @@ private TDocument CreateDocument(
AssertEx.Fail($"The document attributes on file {fileName} conflicted");
}

var filePath = Path.Combine(TestWorkspace.RootDirectory, fileName);
var resolveFilePath = (bool?)documentElement.Attribute("ResolveFilePath");
var filePath = resolveFilePath is null or true ? Path.Combine(TestWorkspace.RootDirectory, fileName) : fileName;

return CreateDocument(
exportProvider, languageServiceProvider, code, fileName, filePath, cursorPosition, spans, codeKind, folders, isLinkFile, documentServiceProvider);
Expand Down
Loading