@@ -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