Skip to content

Commit b2353b3

Browse files
authored
Show semantic errors for loose files with top-level statements (#81326)
1 parent 0dfbd76 commit b2353b3

File tree

6 files changed

+236
-36
lines changed

6 files changed

+236
-36
lines changed

src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/FileBasedProgramsWorkspaceTests.cs

Lines changed: 110 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ void M()
105105
// Should have the appropriate generated files now that we ran a design time build
106106
Assert.Contains(canonicalDocumentOne.Project.Documents, d => d.Name == "Canonical.AssemblyInfo.cs");
107107

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

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

129+
/// <summary>Test that a document which does not have an on-disk path, is never treated as a file-based program.</summary>
130+
[Theory, CombinatorialData]
131+
public async Task TestNonFileDocumentsAreNotFileBasedPrograms(bool mutatingLspWorkspace)
132+
{
133+
await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer });
134+
Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer));
135+
136+
var nonFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"vscode-notebook-cell://dev-container/test.cs");
137+
await testLspServer.OpenDocumentAsync(nonFileUri, """
138+
#:sdk Microsoft.Net.Sdk
139+
Console.WriteLine("Hello World");
140+
""").ConfigureAwait(false);
141+
142+
// File should be initially added as a primordial document in the canonical misc files project with no metadata references.
143+
var (_, primordialDocument) = await GetRequiredLspWorkspaceAndDocumentAsync(nonFileUri, testLspServer).ConfigureAwait(false);
144+
// Should have the primordial canonical document and the loose document.
145+
Assert.Equal(2, primordialDocument.Project.Documents.Count());
146+
Assert.Empty(primordialDocument.Project.MetadataReferences);
147+
148+
var primordialSyntaxTree = await primordialDocument.GetRequiredSyntaxTreeAsync(CancellationToken.None);
149+
// TODO: we probably don't want to report syntax errors for '#:' in the primordial non-file document.
150+
// The logic which decides whether to add '-features:FileBasedProgram' probably needs to be adjusted.
151+
primordialSyntaxTree.GetDiagnostics(CancellationToken.None).Verify(
152+
// vscode-notebook-cell://dev-container/test.cs(1,2): error CS9298: '#:' directives can be only used in file-based programs ('-features:FileBasedProgram')"
153+
// #:sdk Microsoft.Net.Sdk
154+
TestHelpers.Diagnostic(code: 9298, squiggledText: ":").WithLocation(1, 2));
155+
156+
// Wait for the canonical project to finish loading.
157+
await testLspServer.TestWorkspace.GetService<AsynchronousOperationListenerProvider>().GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync();
158+
159+
// Verify the document is loaded in the canonical project.
160+
var (miscWorkspace, canonicalDocument) = await GetRequiredLspWorkspaceAndDocumentAsync(nonFileUri, testLspServer).ConfigureAwait(false);
161+
Assert.Equal(WorkspaceKind.MiscellaneousFiles, miscWorkspace.Kind);
162+
Assert.NotNull(canonicalDocument);
163+
Assert.NotEqual(primordialDocument, canonicalDocument);
164+
// Should have the appropriate generated files now that we ran a design time build
165+
Assert.Contains(canonicalDocument.Project.Documents, d => d.Name == "Canonical.AssemblyInfo.cs");
166+
167+
var canonicalSyntaxTree = await canonicalDocument.GetRequiredSyntaxTreeAsync(CancellationToken.None);
168+
// TODO: we probably don't want to report syntax errors for '#:' in the canonical non-file document.
169+
// The logic which decides whether to add '-features:FileBasedProgram' probably needs to be adjusted.
170+
canonicalSyntaxTree.GetDiagnostics(CancellationToken.None).Verify(
171+
// vscode-notebook-cell://dev-container/test.cs(1,2): error CS9298: '#:' directives can be only used in file-based programs ('-features:FileBasedProgram')"
172+
// #:sdk Microsoft.Net.Sdk
173+
TestHelpers.Diagnostic(code: 9298, squiggledText: ":").WithLocation(1, 2));
174+
}
175+
176+
[Theory, CombinatorialData]
177+
public async Task TestSemanticDiagnosticsEnabledWhenTopLevelStatementsAdded(bool mutatingLspWorkspace)
178+
{
179+
// Create a server that supports LSP misc files and verify no misc files present.
180+
await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer });
181+
Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer));
182+
183+
var looseFileUriOne = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.cs");
184+
await testLspServer.OpenDocumentAsync(looseFileUriOne, """
185+
class C { }
186+
""").ConfigureAwait(false);
187+
188+
// File should be initially added as a primordial document in the canonical misc files project with no metadata references.
189+
var (miscFilesWorkspace, looseDocumentOne) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false);
190+
Assert.Equal(WorkspaceKind.MiscellaneousFiles, miscFilesWorkspace.Kind);
191+
// Should have the primordial canonical document and the loose document.
192+
Assert.Equal(2, looseDocumentOne.Project.Documents.Count());
193+
Assert.Empty(looseDocumentOne.Project.MetadataReferences);
194+
// Semantic diagnostics are not expected because we haven't loaded references
195+
Assert.False(looseDocumentOne.Project.State.HasAllInformation);
196+
197+
// Wait for the canonical project to finish loading.
198+
await testLspServer.TestWorkspace.GetService<AsynchronousOperationListenerProvider>().GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync();
199+
200+
// Verify the document is loaded in the canonical project.
201+
var (_, canonicalDocumentOne) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false);
202+
Assert.NotEqual(looseDocumentOne, canonicalDocumentOne);
203+
// Should have the appropriate generated files now that we ran a design time build
204+
Assert.Contains(canonicalDocumentOne.Project.Documents, d => d.Name == "Canonical.AssemblyInfo.cs");
205+
// There are no top-level statements, so semantic errors are still not expected.
206+
Assert.False(canonicalDocumentOne.Project.State.HasAllInformation);
207+
208+
// Adding a top-level statement to a misc file causes it to report semantic errors.
209+
var textToInsert = $"""Console.WriteLine("Hello World!");{Environment.NewLine}""";
210+
await testLspServer.InsertTextAsync(looseFileUriOne, (Line: 0, Column: 0, Text: textToInsert));
211+
var (workspace, canonicalDocumentTwo) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false);
212+
Assert.Equal("""
213+
Console.WriteLine("Hello World!");
214+
class C { }
215+
""",
216+
(await canonicalDocumentTwo.GetSyntaxRootAsync())!.ToFullString());
217+
Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind);
218+
// When presence of top-level statements changes, the misc project is forked again in order to change attributes.
219+
Assert.NotEqual(canonicalDocumentOne.Project.Id, canonicalDocumentTwo.Project.Id);
220+
// Now that it has top-level statements, it should be considered to have all information.
221+
Assert.True(canonicalDocumentTwo.Project.State.HasAllInformation);
222+
}
223+
129224
[Theory, CombinatorialData]
130225
public async Task TestFileBecomesFileBasedProgramWhenDirectiveAdded(bool mutatingLspWorkspace)
131226
{
@@ -144,15 +239,21 @@ await testLspServer.OpenDocumentAsync(looseFileUriOne, """
144239
// Should have the primordial canonical document and the loose document.
145240
Assert.Equal(2, looseDocumentOne.Project.Documents.Count());
146241
Assert.Empty(looseDocumentOne.Project.MetadataReferences);
242+
// Semantic diagnostics are not expected because we haven't loaded references
243+
Assert.False(looseDocumentOne.Project.State.HasAllInformation);
147244

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

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

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

0 commit comments

Comments
 (0)