Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions src/Compilers/CSharp/Portable/CSharpParseOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,12 @@ static void addSingleNamespaceParts(ArrayBuilder<ImmutableArray<string>> namespa
}

/// <summary>
/// Used for parsing .cs file-based programs.
/// Used for parsing .cs file-based programs, or for miscellaneous files (which aren't associated with any project and the full semantics of the code are not known).
/// </summary>
/// <remarks>
/// In this mode, ignored directives <c>#:</c> are allowed.
/// </remarks>
internal bool FileBasedProgram => Features.ContainsKey("FileBasedProgram");
internal bool AllowIgnoredDirectives => Features.ContainsKey("FileBasedProgram") || Features.ContainsKey("MiscellaneousFile");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should those feature flags documented anywhere?
Not directly related to the PR: Aren't feature flags typically used for temporary or not fully supported scenarios? Should we consider exposing a proper API on parse options for file-based apps?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Perhaps, but, I will note that explicitly including these feature names in ordinary projects and similar to result in unspecified behavior. These are generally supposed to be only included by the tooling.

It’s possible there is a more preferred way of “tagging” compilations in the desired way here than just using special feature names. Happy to have that discussion in more depth.


internal override void ValidateOptions(ArrayBuilder<Diagnostic> builder)
{
Expand Down
4 changes: 2 additions & 2 deletions src/Compilers/CSharp/Portable/Parser/DirectiveParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,7 @@ private DirectiveTriviaSyntax ParsePragmaDirective(SyntaxToken hash, SyntaxToken

private DirectiveTriviaSyntax ParseShebangDirective(SyntaxToken hash, SyntaxToken exclamation, bool isActive)
{
if (lexer.Options.Kind != SourceCodeKind.Script && !lexer.Options.FileBasedProgram)
if (lexer.Options.Kind != SourceCodeKind.Script && !lexer.Options.AllowIgnoredDirectives)
{
exclamation = this.AddError(exclamation, ErrorCode.ERR_PPShebangInProjectBasedProgram);
}
Expand All @@ -698,7 +698,7 @@ private DirectiveTriviaSyntax ParseIgnoredDirective(SyntaxToken hash, SyntaxToke
{
if (isActive)
{
if (!lexer.Options.FileBasedProgram)
if (!lexer.Options.AllowIgnoredDirectives)

@jcouv jcouv Nov 21, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry for my lack of context, but I'm surprised that the parsing of ignored directives is conditional in the first place. The spec only says we're adding directives to the language and that they are ignored: https://github.com/dotnet/csharplang/blob/main/proposals/csharp-14.0/ignored-directives.md #Closed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

AFAIK, this only controls whether an error is reported on usage.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not blocking this PR: should we update the spec or remove the condition? In other words, did we intentionally fork the language?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The spec says:

Compilers are also free to report errors if these directives are used in unsupported scenarios

It's possible we want to push some of those rules into the language, but it's not clear to me. I think the mechanism that decides whether the scenario is supported would also have to be defined in the language spec.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yes, reporting an error on #: directives in normal project-based apps is intentional. That is because those directives are ignored for project-based apps and users might be confused if they were ignored silently. This is only a tooling feature though, i.e., roslyn implements it; sdk tells it the kind of the program (file-based vs project-based) via passing the feature flag. It is not a language feature since language has no way to interpret those directives.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That is because those directives are ignored for project-based apps and users might be confused if they were ignored silently.

That behavior makes sense.
Still, the net result is that we have forked the language, but I don't recall that was discussed as part of the language design. The spec only says we add these to the grammar and they are ignored, but our implementation diverges (errors in some cases). We're using a feature flag for a long-living feature, which smells.

I'll start an email thread.

{
colon = this.AddError(colon, ErrorCode.ERR_PPIgnoredNeedsFileBasedProgram);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public void TestFieldsForEqualsAndGetHashCode()
ReflectionAssert.AssertPublicAndInternalFieldsAndProperties(
typeof(CSharpParseOptions),
"Features",
"FileBasedProgram",
"AllowIgnoredDirectives",
"Language",
"LanguageVersion",
"InterceptorsNamespaces",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,19 @@ public void Semantics()
expectedOutput: "123").VerifyDiagnostics();
}

[Fact]
public void Semantics_MiscellaneousFile()
{
var source = """
#!xyz
#:name value
System.Console.WriteLine(123);
""";
CompileAndVerify(source,
parseOptions: TestOptions.Regular.WithFeature("MiscellaneousFile"),
expectedOutput: "123").VerifyDiagnostics();
}

[Fact]
public void Api()
{
Expand Down
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">

@jcouv jcouv Nov 21, 2025

Copy link
Copy Markdown
Contributor

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
Copy Markdown
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
Copy Markdown
Contributor

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?

@RikkiGibson RikkiGibson Dec 10, 2025

Copy link
Copy Markdown
Member Author

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:" : "/";
Comment thread
RikkiGibson marked this conversation as resolved.
Outdated
Comment thread
RikkiGibson marked this conversation as resolved.
Outdated
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 @@ -54,10 +54,9 @@ languageInformation.ScriptExtension is not null &&
compilationOptions = GetCompilationOptionsWithScriptReferenceResolvers(services, compilationOptions, filePath);
}

if (parseOptions != null && languageInformation.ScriptExtension is not null && fileExtension != languageInformation.ScriptExtension)
if (parseOptions != null)
{
// Any non-script misc file should not complain about usage of '#:' ignored directives.
parseOptions = parseOptions.WithFeatures([.. parseOptions.Features, new("FileBasedProgram", "true")]);
parseOptions = parseOptions.WithFeatures([.. parseOptions.Features, new("MiscellaneousFile", "true")]);
}

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 @@ -145,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 @@ -164,13 +160,9 @@ 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')"
// #:sdk Microsoft.Net.Sdk
TestHelpers.Diagnostic(code: 9298, squiggledText: ":").WithLocation(1, 2));
Assert.Empty(canonicalSyntaxTree.GetDiagnostics(CancellationToken.None));
}

[Theory, CombinatorialData]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ private TextDocument CreatePrimordialProjectAndAddDocument_NoLock(string documen
<TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Features>$(Features);MiscellaneousFile</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