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 @@ -105,7 +105,7 @@ void M()
// Should have the appropriate generated files now that we ran a design time build
Assert.Contains(canonicalDocumentOne.Project.Documents, d => d.Name == "Canonical.AssemblyInfo.cs");

// Add another loose virtual document and verify it goes into the same canonical project.
// Add another loose virtual document and verify it goes into a forked canonical project.
var looseFileUriTwo = ProtocolConversions.CreateAbsoluteDocumentUri(@"vscode-notebook-cell://dev-container/test.cs");
await testLspServer.OpenDocumentAsync(looseFileUriTwo, """
class Other
Expand All @@ -116,16 +116,111 @@ void OtherMethod()
}
""").ConfigureAwait(false);

// Add another misc file and verify it gets added to the same canonical project.
var (_, canonicalDocumentTwo) = await GetLspWorkspaceAndDocumentAsync(looseFileUriTwo, testLspServer).ConfigureAwait(false);
Assert.NotNull(canonicalDocumentTwo);
Assert.Equal(canonicalDocumentOne.Project.Id, canonicalDocumentTwo.Project.Id);
// The project should also contain the other misc document.
Assert.Contains(canonicalDocumentTwo.Project.Documents, d => d.Name == looseDocumentOne.Name);
// Should have the appropriate generated files now that we ran a design time build
Assert.NotEqual(canonicalDocumentOne.Project.Id, canonicalDocumentTwo.Project.Id);
Assert.DoesNotContain(canonicalDocumentTwo.Project.Documents, d => d.Name == looseDocumentOne.Name);
// Semantic diagnostics are not expected due to absence of top-level statements
Assert.False(canonicalDocumentTwo.Project.State.HasAllInformation);
// Should have the appropriate generated files from the base misc files project now that we ran a design time build
Assert.Contains(canonicalDocumentTwo.Project.Documents, d => d.Name == "Canonical.AssemblyInfo.cs");
}

/// <summary>Test that a document which does not have an on-disk path, is never treated as a file-based program.</summary>
[Theory, CombinatorialData]
public async Task TestNonFileDocumentsAreNotFileBasedPrograms(bool mutatingLspWorkspace)
{
await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer });
Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer));

var nonFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"vscode-notebook-cell://dev-container/test.cs");
await testLspServer.OpenDocumentAsync(nonFileUri, """
#:sdk Microsoft.Net.Sdk
Console.WriteLine("Hello World");
""").ConfigureAwait(false);

// File should be initially added as a primordial document in the canonical misc files project with no metadata references.
var (_, primordialDocument) = await GetRequiredLspWorkspaceAndDocumentAsync(nonFileUri, testLspServer).ConfigureAwait(false);
// Should have the primordial canonical document and the loose document.
Assert.Equal(2, primordialDocument.Project.Documents.Count());
Assert.Empty(primordialDocument.Project.MetadataReferences);

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));

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

// Verify the document is loaded in the canonical project.
var (miscWorkspace, canonicalDocument) = await GetRequiredLspWorkspaceAndDocumentAsync(nonFileUri, testLspServer).ConfigureAwait(false);
Assert.Equal(WorkspaceKind.MiscellaneousFiles, miscWorkspace.Kind);
Assert.NotNull(canonicalDocument);
Assert.NotEqual(primordialDocument, canonicalDocument);
// 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");

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));
}

[Theory, CombinatorialData]
public async Task TestSemanticDiagnosticsEnabledWhenTopLevelStatementsAdded(bool mutatingLspWorkspace)
{
// Create a server that supports LSP misc files and verify no misc files present.
await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer });
Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer));

var looseFileUriOne = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.cs");
await testLspServer.OpenDocumentAsync(looseFileUriOne, """
class C { }
""").ConfigureAwait(false);

// File should be initially added as a primordial document in the canonical misc files project with no metadata references.
var (miscFilesWorkspace, looseDocumentOne) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false);
Assert.Equal(WorkspaceKind.MiscellaneousFiles, miscFilesWorkspace.Kind);
// Should have the primordial canonical document and the loose document.
Assert.Equal(2, looseDocumentOne.Project.Documents.Count());
Assert.Empty(looseDocumentOne.Project.MetadataReferences);
// Semantic diagnostics are not expected because we haven't loaded references
Assert.False(looseDocumentOne.Project.State.HasAllInformation);

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

// Verify the document is loaded in the canonical project.
var (_, canonicalDocumentOne) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false);
Assert.NotEqual(looseDocumentOne, canonicalDocumentOne);
// Should have the appropriate generated files now that we ran a design time build
Assert.Contains(canonicalDocumentOne.Project.Documents, d => d.Name == "Canonical.AssemblyInfo.cs");
// There are no top-level statements, so semantic errors are still not expected.
Assert.False(canonicalDocumentOne.Project.State.HasAllInformation);

// Adding a top-level statement to a misc file causes it to report semantic errors.
var textToInsert = $"""Console.WriteLine("Hello World!");{Environment.NewLine}""";
await testLspServer.InsertTextAsync(looseFileUriOne, (Line: 0, Column: 0, Text: textToInsert));
var (workspace, canonicalDocumentTwo) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false);
Assert.Equal("""
Console.WriteLine("Hello World!");
class C { }
""",
(await canonicalDocumentTwo.GetSyntaxRootAsync())!.ToFullString());
Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind);
// When presence of top-level statements changes, the misc project is forked again in order to change attributes.
Assert.NotEqual(canonicalDocumentOne.Project.Id, canonicalDocumentTwo.Project.Id);
// Now that it has top-level statements, it should be considered to have all information.
Assert.True(canonicalDocumentTwo.Project.State.HasAllInformation);
}

[Theory, CombinatorialData]
public async Task TestFileBecomesFileBasedProgramWhenDirectiveAdded(bool mutatingLspWorkspace)
{
Expand All @@ -144,15 +239,21 @@ await testLspServer.OpenDocumentAsync(looseFileUriOne, """
// Should have the primordial canonical document and the loose document.
Assert.Equal(2, looseDocumentOne.Project.Documents.Count());
Assert.Empty(looseDocumentOne.Project.MetadataReferences);
// Semantic diagnostics are not expected because we haven't loaded references
Assert.False(looseDocumentOne.Project.State.HasAllInformation);

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

// Verify the document is loaded in the canonical project.
var (_, canonicalDocumentOne) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false);
(miscFilesWorkspace, var canonicalDocumentOne) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false);
Assert.NotEqual(looseDocumentOne, canonicalDocumentOne);
// Should have the appropriate generated files now that we ran a design time build
Assert.Contains(canonicalDocumentOne.Project.Documents, d => d.Name == "Canonical.AssemblyInfo.cs");
// This is not loaded as a file-based program (no dedicated restore done for it etc.), so it should be in the misc workspace.
Assert.Equal(WorkspaceKind.MiscellaneousFiles, miscFilesWorkspace.Kind);
// Because we have top-level statements, it should be considered to have all information (semantic diagnostics should be reported etc.)
Assert.True(canonicalDocumentOne.Project.State.HasAllInformation);

// Adding a #! directive to a misc file causes it to move to a file-based program project.
var textToInsert = $"#!/usr/bin/env dotnet{Environment.NewLine}";
Expand Down Expand Up @@ -180,5 +281,7 @@ await testLspServer.OpenDocumentAsync(looseFileUriOne, """
Assert.Equal(WorkspaceKind.Host, hostWorkspace!.Kind);
Assert.NotEqual(fileBasedProject.Id, fullFileBasedDocumentOne!.Project.Id);
Assert.Contains(fullFileBasedDocumentOne!.Project.Documents, d => d.Name == "SomeFile.AssemblyInfo.cs");
// Because it is loaded as a file-based program, it should be considered to have all information (semantic diagnostics should be reported etc.)
Assert.True(canonicalDocumentOne.Project.State.HasAllInformation);
}
}
Loading
Loading