|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable disable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Formatting;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.VisualBasic;
using Microsoft.VisualStudio.Threading;
using Roslyn.Test.Utilities;
using Roslyn.Test.Utilities.TestGenerators;
using Roslyn.Utilities;
using Xunit;
using static Microsoft.CodeAnalysis.UnitTests.SolutionTestHelpers;
using CS = Microsoft.CodeAnalysis.CSharp;
using VB = Microsoft.CodeAnalysis.VisualBasic;
namespace Microsoft.CodeAnalysis.UnitTests
{
[UseExportProvider]
[Trait(Traits.Feature, Traits.Features.Workspace)]
public class SolutionTests : TestBase
{
#nullable enable
private static readonly string s_projectDir = Path.GetDirectoryName(typeof(SolutionTests).Assembly.Location)!;
private static readonly MetadataReference s_mscorlib = NetFramework.mscorlib;
private static readonly DocumentId s_unrelatedDocumentId = DocumentId.CreateNewId(ProjectId.CreateNewId());
private static Workspace CreateWorkspaceWithProjectAndDocuments(string? editorConfig = null)
{
var projectId = ProjectId.CreateNewId();
var workspace = CreateWorkspace();
Assert.True(workspace.TryApplyChanges(workspace.CurrentSolution
.AddProject(ProjectInfo.Create(projectId, VersionStamp.Default, "proj1", "proj1", LanguageNames.CSharp, Path.Combine(s_projectDir, "proj1.dll")))
.AddDocument(DocumentId.CreateNewId(projectId), "goo.cs", SourceText.From("public class Goo { }", Encoding.UTF8, SourceHashAlgorithms.Default), filePath: Path.Combine(s_projectDir, "goo.cs"))
.AddAdditionalDocument(DocumentId.CreateNewId(projectId), "add.txt", SourceText.From("text", Encoding.UTF8, SourceHashAlgorithms.Default))
.AddAnalyzerConfigDocument(DocumentId.CreateNewId(projectId), "editorcfg", SourceText.From(editorConfig ?? "#empty", Encoding.UTF8, SourceHashAlgorithms.Default), filePath: Path.Combine(s_projectDir, "editorcfg"))));
return workspace;
}
private static Workspace CreateWorkspaceWithProjectAndLinkedDocuments(
string docContents, ParseOptions? parseOptions1 = null, ParseOptions? parseOptions2 = null)
{
parseOptions1 ??= CSharpParseOptions.Default;
parseOptions2 ??= CSharpParseOptions.Default;
var projectId1 = ProjectId.CreateNewId();
var projectId2 = ProjectId.CreateNewId();
var workspace = CreateWorkspace();
// note: despite the additional-doc and analyzer-config doc being at the same path in multiple projects,
// they will still be treated as unique as the workspace only has the concept of linked docs for normal
// docs.
Assert.True(workspace.TryApplyChanges(workspace.CurrentSolution
.AddProject(projectId1, "proj1", "proj1.dll", LanguageNames.CSharp).WithProjectParseOptions(projectId1, parseOptions1)
.AddDocument(DocumentId.CreateNewId(projectId1), "goo.cs", SourceText.From(docContents, Encoding.UTF8, SourceHashAlgorithms.Default), filePath: "goo.cs")
.AddAdditionalDocument(DocumentId.CreateNewId(projectId1), "add.txt", SourceText.From("text", Encoding.UTF8, SourceHashAlgorithms.Default), filePath: "add.txt")
.AddAnalyzerConfigDocument(DocumentId.CreateNewId(projectId1), "editorcfg", SourceText.From("config", Encoding.UTF8, SourceHashAlgorithms.Default), filePath: "/a/b")
.AddProject(projectId2, "proj2", "proj2.dll", LanguageNames.CSharp).WithProjectParseOptions(projectId2, parseOptions2)
.AddDocument(DocumentId.CreateNewId(projectId2), "goo.cs", SourceText.From(docContents, Encoding.UTF8, SourceHashAlgorithms.Default), filePath: "goo.cs")
.AddAdditionalDocument(DocumentId.CreateNewId(projectId2), "add.txt", SourceText.From("text", Encoding.UTF8, SourceHashAlgorithms.Default), filePath: "add.txt")
.AddAnalyzerConfigDocument(DocumentId.CreateNewId(projectId2), "editorcfg", SourceText.From("config", Encoding.UTF8, SourceHashAlgorithms.Default), filePath: "/a/b")));
return workspace;
}
private static IEnumerable<T> EmptyEnumerable<T>()
{
yield break;
}
// Returns an enumerable that can only be enumerated once.
private static IEnumerable<T> OnceEnumerable<T>(params T[] items)
=> OnceEnumerableImpl(new StrongBox<int>(), items);
private static IEnumerable<T> OnceEnumerableImpl<T>(StrongBox<int> counter, T[] items)
{
Assert.Equal(0, counter.Value);
counter.Value++;
foreach (var item in items)
{
yield return item;
}
}
[Fact]
public void RemoveDocument_Errors()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
Assert.Throws<ArgumentNullException>(() => solution.RemoveDocument(null!));
Assert.Throws<InvalidOperationException>(() => solution.RemoveDocument(s_unrelatedDocumentId));
}
[Fact]
public void RemoveDocuments_Errors()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
Assert.Throws<ArgumentNullException>(() => solution.RemoveDocuments(default));
Assert.Throws<InvalidOperationException>(() => solution.RemoveDocuments(ImmutableArray.Create(s_unrelatedDocumentId)));
Assert.Throws<ArgumentNullException>(() => solution.RemoveDocuments(ImmutableArray.Create((DocumentId)null!)));
}
[Fact]
public void RemoveAdditionalDocument_Errors()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
Assert.Throws<ArgumentNullException>(() => solution.RemoveAdditionalDocument(null!));
Assert.Throws<InvalidOperationException>(() => solution.RemoveAdditionalDocument(s_unrelatedDocumentId));
}
[Fact]
public void RemoveAdditionalDocuments_Errors()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
Assert.Throws<ArgumentNullException>(() => solution.RemoveAdditionalDocuments(default));
Assert.Throws<InvalidOperationException>(() => solution.RemoveAdditionalDocuments(ImmutableArray.Create(s_unrelatedDocumentId)));
Assert.Throws<ArgumentNullException>(() => solution.RemoveAdditionalDocuments(ImmutableArray.Create((DocumentId)null!)));
}
[Fact]
public void RemoveAnalyzerConfigDocument_Errors()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
Assert.Throws<ArgumentNullException>(() => solution.RemoveAnalyzerConfigDocument(null!));
Assert.Throws<InvalidOperationException>(() => solution.RemoveAnalyzerConfigDocument(s_unrelatedDocumentId));
}
[Fact]
public void RemoveAnalyzerConfigDocuments_Errors()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
Assert.Throws<ArgumentNullException>(() => solution.RemoveAnalyzerConfigDocuments(default));
Assert.Throws<InvalidOperationException>(() => solution.RemoveAnalyzerConfigDocuments(ImmutableArray.Create(s_unrelatedDocumentId)));
Assert.Throws<ArgumentNullException>(() => solution.RemoveAnalyzerConfigDocuments(ImmutableArray.Create((DocumentId)null!)));
}
[Fact]
public void WithDocumentName()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var documentId = solution.Projects.Single().DocumentIds.Single();
var name = "new name";
var newSolution1 = solution.WithDocumentName(documentId, name);
Assert.Equal(name, newSolution1.GetDocument(documentId)!.Name);
var newSolution2 = newSolution1.WithDocumentName(documentId, name);
Assert.Same(newSolution1, newSolution2);
Assert.Throws<ArgumentNullException>(() => solution.WithDocumentName(documentId, name: null!));
Assert.Throws<ArgumentNullException>(() => solution.WithDocumentName(null!, name));
Assert.Throws<InvalidOperationException>(() => solution.WithDocumentName(s_unrelatedDocumentId, name));
}
[Fact]
public void WithDocumentFolders()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var documentId = solution.Projects.Single().DocumentIds.Single();
var folders = new[] { "folder1", "folder2" };
var newSolution1 = solution.WithDocumentFolders(documentId, folders);
Assert.Equal(folders, newSolution1.GetDocument(documentId)!.Folders);
var newSolution2 = newSolution1.WithDocumentFolders(documentId, folders);
Assert.Same(newSolution2, newSolution1);
// empty:
var newSolution3 = solution.WithDocumentFolders(documentId, []);
Assert.Equal([], newSolution3.GetDocument(documentId)!.Folders);
var newSolution4 = solution.WithDocumentFolders(documentId, []);
Assert.Same(newSolution3, newSolution4);
var newSolution5 = solution.WithDocumentFolders(documentId, null);
Assert.Same(newSolution3, newSolution5);
Assert.Throws<ArgumentNullException>(() => solution.WithDocumentFolders(documentId, folders: [null!]));
Assert.Throws<ArgumentNullException>(() => solution.WithDocumentFolders(null!, folders));
Assert.Throws<InvalidOperationException>(() => solution.WithDocumentFolders(s_unrelatedDocumentId, folders));
}
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/34837")]
[WorkItem("https://github.com/dotnet/roslyn/issues/37125")]
public void WithDocumentFilePath()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var documentId = solution.Projects.Single().DocumentIds.Single();
var path = "new path";
var newSolution1 = solution.WithDocumentFilePath(documentId, path);
Assert.Equal(path, newSolution1.GetRequiredDocument(documentId).FilePath);
AssertEx.Equal([documentId], newSolution1.GetDocumentIdsWithFilePath(path));
var newSolution2 = newSolution1.WithDocumentFilePath(documentId, path);
Assert.Same(newSolution1, newSolution2);
var newSolution3 = solution.WithDocumentFilePath(documentId, "");
Assert.Equal("", newSolution3.GetRequiredDocument(documentId).FilePath);
Assert.Empty(newSolution3.GetDocumentIdsWithFilePath(""));
var newSolution4 = solution.WithDocumentFilePath(documentId, null);
Assert.Null(newSolution4.GetRequiredDocument(documentId).FilePath);
Assert.Empty(newSolution4.GetDocumentIdsWithFilePath(null));
Assert.Throws<ArgumentNullException>(() => solution.WithDocumentFilePath(null!, path));
Assert.Throws<InvalidOperationException>(() => solution.WithDocumentFilePath(s_unrelatedDocumentId, path));
}
[Fact]
public void WithSourceCodeKind()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var documentId = solution.Projects.Single().DocumentIds.Single();
Assert.Same(solution, solution.WithDocumentSourceCodeKind(documentId, SourceCodeKind.Regular));
var newSolution1 = solution.WithDocumentSourceCodeKind(documentId, SourceCodeKind.Script);
Assert.Equal(SourceCodeKind.Script, newSolution1.GetRequiredDocument(documentId).SourceCodeKind);
Assert.Throws<ArgumentOutOfRangeException>(() => solution.WithDocumentSourceCodeKind(documentId, (SourceCodeKind)(-1)));
Assert.Throws<ArgumentNullException>(() => solution.WithDocumentSourceCodeKind(null!, SourceCodeKind.Script));
Assert.Throws<InvalidOperationException>(() => solution.WithDocumentSourceCodeKind(s_unrelatedDocumentId, SourceCodeKind.Script));
}
[Fact]
public void WithSourceCodeKind_ParseOptions()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var documentId = solution.Projects.Single().DocumentIds.Single();
var projectId = documentId.ProjectId;
solution = solution.WithProjectParseOptions(projectId, CSharpParseOptions.Default.WithKind(SourceCodeKind.Script));
var document1 = solution.GetRequiredDocument(documentId);
Assert.Equal(SourceCodeKind.Script, document1.DocumentState.ParseOptions?.Kind);
Assert.Equal(SourceCodeKind.Script, document1.SourceCodeKind);
var document2 = document1.WithSourceCodeKind(SourceCodeKind.Regular);
Assert.Equal(SourceCodeKind.Regular, document2.DocumentState.ParseOptions?.Kind);
Assert.Equal(SourceCodeKind.Regular, document2.SourceCodeKind);
}
[Fact, Obsolete("Testing obsolete API")]
public void WithSourceCodeKind_Obsolete()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var documentId = solution.Projects.Single().DocumentIds.Single();
var newSolution = solution.WithDocumentSourceCodeKind(documentId, SourceCodeKind.Interactive);
Assert.Equal(SourceCodeKind.Script, newSolution.GetDocument(documentId)!.SourceCodeKind);
}
[Fact]
public void WithDocumentSyntaxRoot()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var documentId = solution.Projects.Single().DocumentIds.Single();
var tree = CS.SyntaxFactory.ParseSyntaxTree("class NewClass {}");
Assert.Equal(SourceHashAlgorithm.Sha1, tree.GetText().ChecksumAlgorithm);
var root = tree.GetRoot();
var newSolution1 = solution.WithDocumentSyntaxRoot(documentId, root, PreservationMode.PreserveIdentity);
Assert.True(newSolution1.GetDocument(documentId)!.TryGetSyntaxRoot(out var actualRoot));
Assert.Equal(root.ToString(), actualRoot!.ToString());
// the actual root has a new parent SyntaxTree:
Assert.NotSame(root, actualRoot);
var newSolution2 = newSolution1.WithDocumentSyntaxRoot(documentId, actualRoot);
Assert.Same(newSolution1, newSolution2);
Assert.Throws<ArgumentOutOfRangeException>(() => solution.WithDocumentSyntaxRoot(documentId, root, (PreservationMode)(-1)));
Assert.Throws<ArgumentNullException>(() => solution.WithDocumentSyntaxRoot(null!, root));
Assert.Throws<InvalidOperationException>(() => solution.WithDocumentSyntaxRoot(s_unrelatedDocumentId, root));
}
[Fact, WorkItem(37125, "https://github.com/dotnet/roslyn/issues/41940")]
public async Task WithDocumentSyntaxRoot_AnalyzerConfigWithoutFilePath()
{
var projectId = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(projectId, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocument(DocumentId.CreateNewId(projectId), "goo.cs", "public class Goo { }")
.AddAnalyzerConfigDocument(DocumentId.CreateNewId(projectId), "editorcfg", SourceText.From("config"));
var project = solution.GetProject(projectId)!;
var compilation = (await project.GetCompilationAsync())!;
var tree = compilation.SyntaxTrees.Single();
var provider = compilation.Options.SyntaxTreeOptionsProvider!;
Assert.Throws<ArgumentException>(() => provider.TryGetDiagnosticValue(tree, "CA1234", CancellationToken.None, out _));
}
[Fact]
public void WithDocumentText_SourceText()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var documentId = solution.Projects.Single().DocumentIds.Single();
var text = SourceText.From("new text", encoding: null, SourceHashAlgorithm.Sha1);
var newSolution1 = solution.WithDocumentText(documentId, text, PreservationMode.PreserveIdentity);
var newDocument1 = newSolution1.GetRequiredDocument(documentId);
Assert.True(newDocument1.TryGetText(out var actualText));
Assert.Same(text, actualText);
var newSolution2 = newSolution1.WithDocumentText(documentId, text, PreservationMode.PreserveIdentity);
Assert.Same(newSolution1, newSolution2);
Assert.Throws<ArgumentNullException>(() => solution.WithDocumentText(documentId, (SourceText)null!, PreservationMode.PreserveIdentity));
Assert.Throws<ArgumentOutOfRangeException>(() => solution.WithDocumentText(documentId, text, (PreservationMode)(-1)));
Assert.Throws<ArgumentNullException>(() => solution.WithDocumentText((DocumentId)null!, text, PreservationMode.PreserveIdentity));
Assert.Throws<InvalidOperationException>(() => solution.WithDocumentText(s_unrelatedDocumentId, text, PreservationMode.PreserveIdentity));
}
[Fact]
public void WithDocumentText_TextAndVersion()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var documentId = solution.Projects.Single().DocumentIds.Single();
var textAndVersion = TextAndVersion.Create(SourceText.From("new text"), VersionStamp.Default);
var newSolution1 = solution.WithDocumentText(documentId, textAndVersion, PreservationMode.PreserveIdentity);
Assert.True(newSolution1.GetDocument(documentId)!.TryGetText(out var actualText));
Assert.True(newSolution1.GetDocument(documentId)!.TryGetTextVersion(out var actualVersion));
Assert.Same(textAndVersion.Text, actualText);
Assert.Equal(textAndVersion.Version, actualVersion);
var newSolution2 = newSolution1.WithDocumentText(documentId, textAndVersion, PreservationMode.PreserveIdentity);
Assert.Same(newSolution1, newSolution2);
Assert.Throws<ArgumentNullException>(() => solution.WithDocumentText(documentId, (SourceText)null!, PreservationMode.PreserveIdentity));
Assert.Throws<ArgumentOutOfRangeException>(() => solution.WithDocumentText(documentId, textAndVersion, (PreservationMode)(-1)));
Assert.Throws<ArgumentNullException>(() => solution.WithDocumentText((DocumentId)null!, textAndVersion, PreservationMode.PreserveIdentity));
Assert.Throws<InvalidOperationException>(() => solution.WithDocumentText(s_unrelatedDocumentId, textAndVersion, PreservationMode.PreserveIdentity));
}
[Fact]
public void WithDocumentText_MultipleDocuments()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var documentId = solution.Projects.Single().DocumentIds.Single();
var text = SourceText.From("new text");
var newSolution1 = solution.WithDocumentText([documentId], text, PreservationMode.PreserveIdentity);
Assert.True(newSolution1.GetDocument(documentId)!.TryGetText(out var actualText));
Assert.Same(text, actualText);
var newSolution2 = newSolution1.WithDocumentText([documentId], text, PreservationMode.PreserveIdentity);
Assert.Same(newSolution1, newSolution2);
// documents not in solution are skipped: https://github.com/dotnet/roslyn/issues/42029
Assert.Same(solution, solution.WithDocumentText([null!], text));
Assert.Same(solution, solution.WithDocumentText([s_unrelatedDocumentId], text));
Assert.Throws<ArgumentNullException>(() => solution.WithDocumentText((DocumentId[])null!, text, PreservationMode.PreserveIdentity));
Assert.Throws<ArgumentNullException>(() => solution.WithDocumentText([documentId], null!, PreservationMode.PreserveIdentity));
Assert.Throws<ArgumentOutOfRangeException>(() => solution.WithDocumentText([documentId], text, (PreservationMode)(-1)));
}
public enum TextUpdateType
{
SourceText,
TextLoader,
TextAndVersion,
}
[Theory, CombinatorialData]
public async Task WithDocumentText_LinkedFiles(
PreservationMode mode,
TextUpdateType updateType)
{
using var workspace = CreateWorkspaceWithProjectAndLinkedDocuments("public class Goo { }");
var solution = workspace.CurrentSolution;
var documentId1 = solution.Projects.First().DocumentIds.Single();
var documentId2 = solution.Projects.Last().DocumentIds.Single();
var document1 = solution.GetRequiredDocument(documentId1);
var document2 = solution.GetRequiredDocument(documentId2);
var text1 = await document1.GetTextAsync();
var text2 = await document2.GetTextAsync();
var version1 = await document1.GetTextVersionAsync();
var version2 = await document2.GetTextVersionAsync();
var root1 = await document1.GetRequiredSyntaxRootAsync(CancellationToken.None);
var root2 = await document2.GetRequiredSyntaxRootAsync(CancellationToken.None);
Assert.Equal(text1.ToString(), text2.ToString());
Assert.Equal(version1, version2);
// We get different red nodes, but they should be backed by the same green nodes.
Assert.NotEqual(root1, root2);
Assert.True(root1.IsIncrementallyIdenticalTo(root2));
var text = SourceText.From("new text", encoding: null, SourceHashAlgorithm.Sha1);
var textAndVersion = TextAndVersion.Create(text, VersionStamp.Create());
solution = UpdateSolution(mode, updateType, solution, documentId1, text, textAndVersion);
// because we only forked one doc, the text/versions should be different in this interim solution.
document1 = solution.GetRequiredDocument(documentId1);
document2 = solution.GetRequiredDocument(documentId2);
text1 = await document1.GetTextAsync();
text2 = await document2.GetTextAsync();
version1 = await document1.GetTextVersionAsync();
version2 = await document2.GetTextVersionAsync();
root1 = await document1.GetRequiredSyntaxRootAsync(CancellationToken.None);
root2 = await document2.GetRequiredSyntaxRootAsync(CancellationToken.None);
Assert.NotEqual(text1.ToString(), text2.ToString());
Assert.NotEqual(version1, version2);
// We get different red and green nodes.
Assert.NotEqual(root1, root2);
Assert.False(root1.IsIncrementallyIdenticalTo(root2));
// Now apply the change to the workspace. This should bring the linked document in sync with the one we changed.
workspace.TryApplyChanges(solution);
solution = workspace.CurrentSolution;
document1 = solution.GetRequiredDocument(documentId1);
document2 = solution.GetRequiredDocument(documentId2);
text1 = await document1.GetTextAsync();
text2 = await document2.GetTextAsync();
version1 = await document1.GetTextVersionAsync();
version2 = await document2.GetTextVersionAsync();
root1 = await document1.GetRequiredSyntaxRootAsync(CancellationToken.None);
root2 = await document2.GetRequiredSyntaxRootAsync(CancellationToken.None);
Assert.Equal(text1.ToString(), text2.ToString());
Assert.Equal(version1, version2);
// We get different red nodes, but they should be backed by the same green nodes.
Assert.NotEqual(root1, root2);
Assert.True(root1.IsIncrementallyIdenticalTo(root2));
}
private static Solution UpdateSolution(PreservationMode mode, TextUpdateType updateType, Solution solution, DocumentId documentId1, SourceText text, TextAndVersion textAndVersion)
{
solution = updateType switch
{
TextUpdateType.SourceText => solution.WithDocumentText(documentId1, text, mode),
TextUpdateType.TextAndVersion => solution.WithDocumentText(documentId1, textAndVersion, mode),
TextUpdateType.TextLoader => solution.WithDocumentTextLoader(documentId1, TextLoader.From(textAndVersion), mode),
_ => throw ExceptionUtilities.UnexpectedValue(updateType)
};
return solution;
}
[Theory, CombinatorialData]
public async Task WithDocumentText_LinkedFiles_PPConditionalDirective_SameParseOptions(
PreservationMode mode,
TextUpdateType updateType)
{
using var workspace = CreateWorkspaceWithProjectAndLinkedDocuments("""
#if NETSTANDARD
public class Goo { }
""");
var solution = workspace.CurrentSolution;
var documentId1 = solution.Projects.First().DocumentIds.Single();
var documentId2 = solution.Projects.Last().DocumentIds.Single();
var document1 = solution.GetRequiredDocument(documentId1);
var document2 = solution.GetRequiredDocument(documentId2);
var text1 = await document1.GetTextAsync();
var text2 = await document2.GetTextAsync();
var version1 = await document1.GetTextVersionAsync();
var version2 = await document2.GetTextVersionAsync();
var root1 = await document1.GetRequiredSyntaxRootAsync(CancellationToken.None);
var root2 = await document2.GetRequiredSyntaxRootAsync(CancellationToken.None);
Assert.Equal(text1.ToString(), text2.ToString());
Assert.Equal(version1, version2);
// We can reuse trees with conditional directives if the parse options are the same.
Assert.NotEqual(root1, root2);
Assert.True(root1.IsIncrementallyIdenticalTo(root2));
var text = SourceText.From("new text", encoding: null, SourceHashAlgorithm.Sha1);
var textAndVersion = TextAndVersion.Create(text, VersionStamp.Create());
solution = UpdateSolution(mode, updateType, solution, documentId1, text, textAndVersion);
// because we only forked one doc, the text/versions should be different in this interim solution.
document1 = solution.GetRequiredDocument(documentId1);
document2 = solution.GetRequiredDocument(documentId2);
text1 = await document1.GetTextAsync();
text2 = await document2.GetTextAsync();
version1 = await document1.GetTextVersionAsync();
version2 = await document2.GetTextVersionAsync();
root1 = await document1.GetRequiredSyntaxRootAsync(CancellationToken.None);
root2 = await document2.GetRequiredSyntaxRootAsync(CancellationToken.None);
Assert.NotEqual(text1.ToString(), text2.ToString());
Assert.NotEqual(version1, version2);
// We get different red and green nodes entirely
Assert.NotEqual(root1, root2);
Assert.False(root1.IsIncrementallyIdenticalTo(root2));
// Now apply the change to the workspace. This should bring the linked document in sync with the one we changed.
workspace.TryApplyChanges(solution);
solution = workspace.CurrentSolution;
document1 = solution.GetRequiredDocument(documentId1);
document2 = solution.GetRequiredDocument(documentId2);
text1 = await document1.GetTextAsync();
text2 = await document2.GetTextAsync();
version1 = await document1.GetTextVersionAsync();
version2 = await document2.GetTextVersionAsync();
root1 = await document1.GetRequiredSyntaxRootAsync(CancellationToken.None);
root2 = await document2.GetRequiredSyntaxRootAsync(CancellationToken.None);
Assert.Equal(text1.ToString(), text2.ToString());
Assert.Equal(version1, version2);
// We can reuse trees with conditional directives if the parse options are the same.
Assert.NotEqual(root1, root2);
Assert.True(root1.IsIncrementallyIdenticalTo(root2));
}
[Theory, CombinatorialData]
public async Task WithDocumentText_LinkedFiles_PPConditionalDirective_DifferentParseOptions1(
PreservationMode mode,
TextUpdateType updateType)
{
var parseOptions1 = CSharpParseOptions.Default.WithPreprocessorSymbols("UNIQUE_NAME");
var parseOptions2 = CSharpParseOptions.Default;
using var workspace = CreateWorkspaceWithProjectAndLinkedDocuments("""
#if NETSTANDARD
public class Goo { }
""", parseOptions1, parseOptions2);
var solution = workspace.CurrentSolution;
var documentId1 = solution.Projects.First().DocumentIds.Single();
var documentId2 = solution.Projects.Last().DocumentIds.Single();
var document1 = solution.GetRequiredDocument(documentId1);
var document2 = solution.GetRequiredDocument(documentId2);
var text1 = await document1.GetTextAsync();
var text2 = await document2.GetTextAsync();
var version1 = await document1.GetTextVersionAsync();
var version2 = await document2.GetTextVersionAsync();
var root1 = await document1.GetRequiredSyntaxRootAsync(CancellationToken.None);
var root2 = await document2.GetRequiredSyntaxRootAsync(CancellationToken.None);
Assert.Equal(text1.ToString(), text2.ToString());
Assert.Equal(version1, version2);
// We can never reuse trees with conditional directives.
Assert.NotEqual(root1, root2);
Assert.False(root1.IsIncrementallyIdenticalTo(root2));
Assert.Equal(parseOptions1, root1.SyntaxTree.Options);
Assert.Equal(parseOptions2, root2.SyntaxTree.Options);
// Because we removed pp directives, we'll be able to reuse after this.
var text = SourceText.From("new text without pp directives", encoding: null, SourceHashAlgorithm.Sha1);
var textAndVersion = TextAndVersion.Create(text, VersionStamp.Create());
solution = UpdateSolution(mode, updateType, solution, documentId1, text, textAndVersion);
// because we only forked one doc, the text/versions should be different in this interim solution.
document1 = solution.GetRequiredDocument(documentId1);
document2 = solution.GetRequiredDocument(documentId2);
text1 = await document1.GetTextAsync();
text2 = await document2.GetTextAsync();
version1 = await document1.GetTextVersionAsync();
version2 = await document2.GetTextVersionAsync();
root1 = await document1.GetRequiredSyntaxRootAsync(CancellationToken.None);
root2 = await document2.GetRequiredSyntaxRootAsync(CancellationToken.None);
Assert.NotEqual(text1.ToString(), text2.ToString());
Assert.NotEqual(version1, version2);
// We get different red and green nodes entirely
Assert.NotEqual(root1, root2);
Assert.False(root1.IsIncrementallyIdenticalTo(root2));
Assert.Equal(parseOptions1, root1.SyntaxTree.Options);
Assert.Equal(parseOptions2, root2.SyntaxTree.Options);
// Now apply the change to the workspace. This should bring the linked document in sync with the one we changed.
workspace.TryApplyChanges(solution);
solution = workspace.CurrentSolution;
document1 = solution.GetRequiredDocument(documentId1);
document2 = solution.GetRequiredDocument(documentId2);
text1 = await document1.GetTextAsync();
text2 = await document2.GetTextAsync();
version1 = await document1.GetTextVersionAsync();
version2 = await document2.GetTextVersionAsync();
root1 = await document1.GetRequiredSyntaxRootAsync(CancellationToken.None);
root2 = await document2.GetRequiredSyntaxRootAsync(CancellationToken.None);
Assert.Equal(text1.ToString(), text2.ToString());
Assert.Equal(version1, version2);
// We can reuse trees once they don't have conditional directives.
Assert.NotEqual(root1, root2);
Assert.True(root1.IsIncrementallyIdenticalTo(root2));
Assert.Equal(parseOptions1, root1.SyntaxTree.Options);
Assert.Equal(parseOptions2, root2.SyntaxTree.Options);
}
[Theory, CombinatorialData]
public async Task WithDocumentText_LinkedFiles_PPConditionalDirective_DifferentParseOptions2(
PreservationMode mode,
TextUpdateType updateType)
{
using var workspace = CreateWorkspaceWithProjectAndLinkedDocuments("""
#if NETSTANDARD
public class Goo { }
""", CSharpParseOptions.Default.WithPreprocessorSymbols("UNIQUE_NAME"), CSharpParseOptions.Default);
var solution = workspace.CurrentSolution;
var documentId1 = solution.Projects.First().DocumentIds.Single();
var documentId2 = solution.Projects.Last().DocumentIds.Single();
var document1 = solution.GetRequiredDocument(documentId1);
var document2 = solution.GetRequiredDocument(documentId2);
var text1 = await document1.GetTextAsync();
var text2 = await document2.GetTextAsync();
var version1 = await document1.GetTextVersionAsync();
var version2 = await document2.GetTextVersionAsync();
var root1 = await document1.GetRequiredSyntaxRootAsync(CancellationToken.None);
var root2 = await document2.GetRequiredSyntaxRootAsync(CancellationToken.None);
Assert.Equal(text1.ToString(), text2.ToString());
Assert.Equal(version1, version2);
// We can never reuse trees with conditional directives.
Assert.NotEqual(root1, root2);
Assert.False(root1.IsIncrementallyIdenticalTo(root2));
// Because we still have pp directives, we'll still not be able to reuse the file.
var text = SourceText.From("#if true", encoding: null, SourceHashAlgorithm.Sha1);
var textAndVersion = TextAndVersion.Create(text, VersionStamp.Create());
solution = UpdateSolution(mode, updateType, solution, documentId1, text, textAndVersion);
// because we only forked one doc, the text/versions should be different in this interim solution.
document1 = solution.GetRequiredDocument(documentId1);
document2 = solution.GetRequiredDocument(documentId2);
text1 = await document1.GetTextAsync();
text2 = await document2.GetTextAsync();
version1 = await document1.GetTextVersionAsync();
version2 = await document2.GetTextVersionAsync();
root1 = await document1.GetRequiredSyntaxRootAsync(CancellationToken.None);
root2 = await document2.GetRequiredSyntaxRootAsync(CancellationToken.None);
Assert.NotEqual(text1.ToString(), text2.ToString());
Assert.NotEqual(version1, version2);
// We get different red and green nodes entirely
Assert.NotEqual(root1, root2);
Assert.False(root1.IsIncrementallyIdenticalTo(root2));
// Now apply the change to the workspace. This should bring the linked document in sync with the one we changed.
workspace.TryApplyChanges(solution);
solution = workspace.CurrentSolution;
document1 = solution.GetRequiredDocument(documentId1);
document2 = solution.GetRequiredDocument(documentId2);
text1 = await document1.GetTextAsync();
text2 = await document2.GetTextAsync();
version1 = await document1.GetTextVersionAsync();
version2 = await document2.GetTextVersionAsync();
root1 = await document1.GetRequiredSyntaxRootAsync(CancellationToken.None);
root2 = await document2.GetRequiredSyntaxRootAsync(CancellationToken.None);
Assert.Equal(text1.ToString(), text2.ToString());
Assert.Equal(version1, version2);
// We can never reuse trees with conditional directives.
Assert.NotEqual(root1, root2);
Assert.False(root1.IsIncrementallyIdenticalTo(root2));
}
[Theory, CombinatorialData]
public async Task WithDocumentText_LinkedFiles_NonConditionalDirective(
PreservationMode mode,
TextUpdateType updateType)
{
using var workspace = CreateWorkspaceWithProjectAndLinkedDocuments("""
#nullable enable // should not impact being able to reuse.
public class Goo { }
""");
var solution = workspace.CurrentSolution;
var documentId1 = solution.Projects.First().DocumentIds.Single();
var documentId2 = solution.Projects.Last().DocumentIds.Single();
var document1 = solution.GetRequiredDocument(documentId1);
var document2 = solution.GetRequiredDocument(documentId2);
var text1 = await document1.GetTextAsync();
var text2 = await document2.GetTextAsync();
var version1 = await document1.GetTextVersionAsync();
var version2 = await document2.GetTextVersionAsync();
var root1 = await document1.GetRequiredSyntaxRootAsync(CancellationToken.None);
var root2 = await document2.GetRequiredSyntaxRootAsync(CancellationToken.None);
Assert.Equal(text1.ToString(), text2.ToString());
Assert.Equal(version1, version2);
// We get different red nodes, but they should be backed by the same green nodes.
Assert.NotEqual(root1, root2);
Assert.True(root1.IsIncrementallyIdenticalTo(root2));
var text = SourceText.From("new text", encoding: null, SourceHashAlgorithm.Sha1);
var textAndVersion = TextAndVersion.Create(text, VersionStamp.Create());
solution = UpdateSolution(mode, updateType, solution, documentId1, text, textAndVersion);
// because we only forked one doc, the text/versions should be different in this interim solution.
document1 = solution.GetRequiredDocument(documentId1);
document2 = solution.GetRequiredDocument(documentId2);
text1 = await document1.GetTextAsync();
text2 = await document2.GetTextAsync();
version1 = await document1.GetTextVersionAsync();
version2 = await document2.GetTextVersionAsync();
root1 = await document1.GetRequiredSyntaxRootAsync(CancellationToken.None);
root2 = await document2.GetRequiredSyntaxRootAsync(CancellationToken.None);
Assert.NotEqual(text1.ToString(), text2.ToString());
Assert.NotEqual(version1, version2);
// We get different red and green nodes.
Assert.NotEqual(root1, root2);
Assert.False(root1.IsIncrementallyIdenticalTo(root2));
// Now apply the change to the workspace. This should bring the linked document in sync with the one we changed.
workspace.TryApplyChanges(solution);
solution = workspace.CurrentSolution;
document1 = solution.GetRequiredDocument(documentId1);
document2 = solution.GetRequiredDocument(documentId2);
text1 = await document1.GetTextAsync();
text2 = await document2.GetTextAsync();
version1 = await document1.GetTextVersionAsync();
version2 = await document2.GetTextVersionAsync();
root1 = await document1.GetRequiredSyntaxRootAsync(CancellationToken.None);
root2 = await document2.GetRequiredSyntaxRootAsync(CancellationToken.None);
Assert.Equal(text1.ToString(), text2.ToString());
Assert.Equal(version1, version2);
// We get different red nodes, but they should be backed by the same green nodes.
Assert.NotEqual(root1, root2);
Assert.True(root1.IsIncrementallyIdenticalTo(root2));
}
[Theory, CombinatorialData]
public async Task WithDocumentText_LinkedFiles_DifferentLanguage(
PreservationMode mode,
TextUpdateType updateType)
{
var parseOptions1 = CSharpParseOptions.Default;
var parseOptions2 = VisualBasicParseOptions.Default;
var projectId1 = ProjectId.CreateNewId();
var projectId2 = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var docContents = "";
// Validate strange case were we have linked files to the same file, but with different languages.
Assert.True(workspace.TryApplyChanges(workspace.CurrentSolution
.AddProject(projectId1, "proj1", "proj1.dll", LanguageNames.CSharp).WithProjectParseOptions(projectId1, parseOptions1)
.AddDocument(DocumentId.CreateNewId(projectId1), "goo.cs", SourceText.From(docContents, Encoding.UTF8, SourceHashAlgorithms.Default), filePath: "goo.cs")
.AddProject(projectId2, "proj2", "proj2.dll", LanguageNames.VisualBasic).WithProjectParseOptions(projectId2, parseOptions2)
.AddDocument(DocumentId.CreateNewId(projectId2), "goo.cs", SourceText.From(docContents, Encoding.UTF8, SourceHashAlgorithms.Default), filePath: "goo.cs")));
var solution = workspace.CurrentSolution;
var documentId1 = solution.Projects.First().DocumentIds.Single();
var documentId2 = solution.Projects.Last().DocumentIds.Single();
var document1 = solution.GetRequiredDocument(documentId1);
var document2 = solution.GetRequiredDocument(documentId2);
var text1 = await document1.GetTextAsync();
var text2 = await document2.GetTextAsync();
var version1 = await document1.GetTextVersionAsync();
var version2 = await document2.GetTextVersionAsync();
var root1 = await document1.GetRequiredSyntaxRootAsync(CancellationToken.None);
var root2 = await document2.GetRequiredSyntaxRootAsync(CancellationToken.None);
Assert.Equal(text1.ToString(), text2.ToString());
Assert.Equal(version1, version2);
// These are different languages, so we should get entirely different tree structures.
Assert.NotEqual(root1.GetType(), root2.GetType());
var text = SourceText.From(" ", encoding: null, SourceHashAlgorithm.Sha1);
var textAndVersion = TextAndVersion.Create(text, VersionStamp.Create());
solution = UpdateSolution(mode, updateType, solution, documentId1, text, textAndVersion);
// because we only forked one doc, the text/versions should be different in this interim solution.
document1 = solution.GetRequiredDocument(documentId1);
document2 = solution.GetRequiredDocument(documentId2);
text1 = await document1.GetTextAsync();
text2 = await document2.GetTextAsync();
version1 = await document1.GetTextVersionAsync();
version2 = await document2.GetTextVersionAsync();
root1 = await document1.GetRequiredSyntaxRootAsync(CancellationToken.None);
root2 = await document2.GetRequiredSyntaxRootAsync(CancellationToken.None);
Assert.NotEqual(text1.ToString(), text2.ToString());
// The versions will not match as we won't share the underlying text-and-tree instances between languages.
Assert.NotEqual(version1, version2);
// These are different languages, so we should get entirely different tree structures.
Assert.NotEqual(root1.GetType(), root2.GetType());
// Now apply the change to the workspace. This should bring the linked document in sync with the one we changed.
// But not cause them to share trees.
workspace.TryApplyChanges(solution);
solution = workspace.CurrentSolution;
document1 = solution.GetRequiredDocument(documentId1);
document2 = solution.GetRequiredDocument(documentId2);
text1 = await document1.GetTextAsync();
text2 = await document2.GetTextAsync();
version1 = await document1.GetTextVersionAsync();
version2 = await document2.GetTextVersionAsync();
root1 = await document1.GetRequiredSyntaxRootAsync(CancellationToken.None);
root2 = await document2.GetRequiredSyntaxRootAsync(CancellationToken.None);
Assert.Equal(text1.ToString(), text2.ToString());
Assert.Equal(version1, version2);
// These are different languages, so we should get entirely different tree structures.
Assert.NotEqual(root1.GetType(), root2.GetType());
}
[Fact]
public void WithAdditionalDocumentText_SourceText()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var documentId = solution.Projects.Single().AdditionalDocumentIds.Single();
var text = SourceText.From("new text");
var newSolution1 = solution.WithAdditionalDocumentText(documentId, text, PreservationMode.PreserveIdentity);
Assert.True(newSolution1.GetAdditionalDocument(documentId)!.TryGetText(out var actualText));
Assert.Same(text, actualText);
var newSolution2 = newSolution1.WithAdditionalDocumentText(documentId, text, PreservationMode.PreserveIdentity);
Assert.Same(newSolution1, newSolution2);
Assert.Throws<ArgumentNullException>(() => solution.WithAdditionalDocumentText(documentId, (SourceText)null!, PreservationMode.PreserveIdentity));
Assert.Throws<ArgumentOutOfRangeException>(() => solution.WithAdditionalDocumentText(documentId, text, (PreservationMode)(-1)));
Assert.Throws<ArgumentNullException>(() => solution.WithAdditionalDocumentText((DocumentId)null!, text, PreservationMode.PreserveIdentity));
Assert.Throws<InvalidOperationException>(() => solution.WithAdditionalDocumentText(s_unrelatedDocumentId, text, PreservationMode.PreserveIdentity));
}
[Fact]
public void WithAdditionalDocumentText_TextAndVersion()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var documentId = solution.Projects.Single().AdditionalDocumentIds.Single();
var textAndVersion = TextAndVersion.Create(SourceText.From("new text"), VersionStamp.Default);
var newSolution1 = solution.WithAdditionalDocumentText(documentId, textAndVersion, PreservationMode.PreserveIdentity);
Assert.True(newSolution1.GetAdditionalDocument(documentId)!.TryGetText(out var actualText));
Assert.True(newSolution1.GetAdditionalDocument(documentId)!.TryGetTextVersion(out var actualVersion));
Assert.Same(textAndVersion.Text, actualText);
Assert.Equal(textAndVersion.Version, actualVersion);
var newSolution2 = newSolution1.WithAdditionalDocumentText(documentId, textAndVersion, PreservationMode.PreserveIdentity);
Assert.Same(newSolution1, newSolution2);
Assert.Throws<ArgumentNullException>(() => solution.WithAdditionalDocumentText(documentId, (SourceText)null!, PreservationMode.PreserveIdentity));
Assert.Throws<ArgumentOutOfRangeException>(() => solution.WithAdditionalDocumentText(documentId, textAndVersion, (PreservationMode)(-1)));
Assert.Throws<ArgumentNullException>(() => solution.WithAdditionalDocumentText((DocumentId)null!, textAndVersion, PreservationMode.PreserveIdentity));
Assert.Throws<InvalidOperationException>(() => solution.WithAdditionalDocumentText(s_unrelatedDocumentId, textAndVersion, PreservationMode.PreserveIdentity));
}
[Fact]
public void WithAnalyzerConfigDocumentText_SourceText()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var documentId = solution.Projects.Single().AnalyzerConfigDocumentIds.Single();
var text = SourceText.From("new text");
var newSolution1 = solution.WithAnalyzerConfigDocumentText(documentId, text, PreservationMode.PreserveIdentity);
Assert.True(newSolution1.GetAnalyzerConfigDocument(documentId)!.TryGetText(out var actualText));
Assert.Same(text, actualText);
var newSolution2 = newSolution1.WithAnalyzerConfigDocumentText(documentId, text, PreservationMode.PreserveIdentity);
Assert.Same(newSolution1, newSolution2);
Assert.Throws<ArgumentNullException>(() => solution.WithAnalyzerConfigDocumentText(documentId, (SourceText)null!, PreservationMode.PreserveIdentity));
Assert.Throws<ArgumentOutOfRangeException>(() => solution.WithAnalyzerConfigDocumentText(documentId, text, (PreservationMode)(-1)));
Assert.Throws<ArgumentNullException>(() => solution.WithAnalyzerConfigDocumentText((DocumentId)null!, text, PreservationMode.PreserveIdentity));
Assert.Throws<InvalidOperationException>(() => solution.WithAnalyzerConfigDocumentText(s_unrelatedDocumentId, text, PreservationMode.PreserveIdentity));
}
[Fact]
public void WithAnalyzerConfigDocumentText_TextAndVersion()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var documentId = solution.Projects.Single().AnalyzerConfigDocumentIds.Single();
var textAndVersion = TextAndVersion.Create(SourceText.From("new text"), VersionStamp.Default);
var newSolution1 = solution.WithAnalyzerConfigDocumentText(documentId, textAndVersion, PreservationMode.PreserveIdentity);
Assert.True(newSolution1.GetAnalyzerConfigDocument(documentId)!.TryGetText(out var actualText));
Assert.True(newSolution1.GetAnalyzerConfigDocument(documentId)!.TryGetTextVersion(out var actualVersion));
Assert.Same(textAndVersion.Text, actualText);
Assert.Equal(textAndVersion.Version, actualVersion);
var newSolution2 = newSolution1.WithAnalyzerConfigDocumentText(documentId, textAndVersion, PreservationMode.PreserveIdentity);
Assert.Same(newSolution1, newSolution2);
Assert.Throws<ArgumentNullException>(() => solution.WithAnalyzerConfigDocumentText(documentId, (SourceText)null!, PreservationMode.PreserveIdentity));
Assert.Throws<ArgumentOutOfRangeException>(() => solution.WithAnalyzerConfigDocumentText(documentId, textAndVersion, (PreservationMode)(-1)));
Assert.Throws<ArgumentNullException>(() => solution.WithAnalyzerConfigDocumentText((DocumentId)null!, textAndVersion, PreservationMode.PreserveIdentity));
Assert.Throws<InvalidOperationException>(() => solution.WithAnalyzerConfigDocumentText(s_unrelatedDocumentId, textAndVersion, PreservationMode.PreserveIdentity));
}
[Fact]
public void WithDocumentTextLoader()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var documentId = solution.Projects.Single().DocumentIds.Single();
var loader = new TestTextLoader("new text");
var newSolution1 = solution.WithDocumentTextLoader(documentId, loader, PreservationMode.PreserveIdentity);
Assert.Equal("new text", newSolution1.GetDocument(documentId)!.GetTextSynchronously(CancellationToken.None).ToString());
// Reusal is not currently implemented: https://github.com/dotnet/roslyn/issues/42028
var newSolution2 = solution.WithDocumentTextLoader(documentId, loader, PreservationMode.PreserveIdentity);
Assert.NotSame(newSolution1, newSolution2);
Assert.Throws<ArgumentNullException>(() => solution.WithDocumentTextLoader(documentId, null!, PreservationMode.PreserveIdentity));
Assert.Throws<ArgumentOutOfRangeException>(() => solution.WithDocumentTextLoader(documentId, loader, (PreservationMode)(-1)));
Assert.Throws<ArgumentNullException>(() => solution.WithDocumentTextLoader(null!, loader, PreservationMode.PreserveIdentity));
Assert.Throws<InvalidOperationException>(() => solution.WithDocumentTextLoader(s_unrelatedDocumentId, loader, PreservationMode.PreserveIdentity));
}
[Fact]
public void WithAdditionalDocumentTextLoader()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var documentId = solution.Projects.Single().AdditionalDocumentIds.Single();
var loader = new TestTextLoader("new text");
var newSolution1 = solution.WithAdditionalDocumentTextLoader(documentId, loader, PreservationMode.PreserveIdentity);
Assert.Equal("new text", newSolution1.GetAdditionalDocument(documentId)!.GetTextSynchronously(CancellationToken.None).ToString());
// Reusal is not currently implemented: https://github.com/dotnet/roslyn/issues/42028
var newSolution2 = solution.WithAdditionalDocumentTextLoader(documentId, loader, PreservationMode.PreserveIdentity);
Assert.NotSame(newSolution1, newSolution2);
Assert.Throws<ArgumentNullException>(() => solution.WithAdditionalDocumentTextLoader(documentId, null!, PreservationMode.PreserveIdentity));
Assert.Throws<ArgumentOutOfRangeException>(() => solution.WithAdditionalDocumentTextLoader(documentId, loader, (PreservationMode)(-1)));
Assert.Throws<ArgumentNullException>(() => solution.WithAdditionalDocumentTextLoader(null!, loader, PreservationMode.PreserveIdentity));
Assert.Throws<InvalidOperationException>(() => solution.WithAdditionalDocumentTextLoader(s_unrelatedDocumentId, loader, PreservationMode.PreserveIdentity));
}
[Fact]
public void WithAnalyzerConfigDocumentTextLoader()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var documentId = solution.Projects.Single().AnalyzerConfigDocumentIds.Single();
var loader = new TestTextLoader("new text");
var newSolution1 = solution.WithAnalyzerConfigDocumentTextLoader(documentId, loader, PreservationMode.PreserveIdentity);
Assert.Equal("new text", newSolution1.GetAnalyzerConfigDocument(documentId)!.GetTextSynchronously(CancellationToken.None).ToString());
// Reusal is not currently implemented: https://github.com/dotnet/roslyn/issues/42028
var newSolution2 = solution.WithAnalyzerConfigDocumentTextLoader(documentId, loader, PreservationMode.PreserveIdentity);
Assert.NotSame(newSolution1, newSolution2);
Assert.Throws<ArgumentNullException>(() => solution.WithAnalyzerConfigDocumentTextLoader(documentId, null!, PreservationMode.PreserveIdentity));
Assert.Throws<ArgumentOutOfRangeException>(() => solution.WithAnalyzerConfigDocumentTextLoader(documentId, loader, (PreservationMode)(-1)));
Assert.Throws<ArgumentNullException>(() => solution.WithAnalyzerConfigDocumentTextLoader(null!, loader, PreservationMode.PreserveIdentity));
Assert.Throws<InvalidOperationException>(() => solution.WithAnalyzerConfigDocumentTextLoader(s_unrelatedDocumentId, loader, PreservationMode.PreserveIdentity));
}
[Fact]
public void WithProjectInfo()
{
var projectId = ProjectId.CreateNewId();
var projectId2 = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var d1 = DocumentId.CreateNewId(projectId);
var d2 = DocumentId.CreateNewId(projectId);
var d3 = DocumentId.CreateNewId(projectId);
var a1 = DocumentId.CreateNewId(projectId);
var a2 = DocumentId.CreateNewId(projectId);
var a3 = DocumentId.CreateNewId(projectId);
var c1 = DocumentId.CreateNewId(projectId);
var c2 = DocumentId.CreateNewId(projectId);
var c3 = DocumentId.CreateNewId(projectId);
var solution = workspace.CurrentSolution
.AddProject(ProjectInfo.Create(projectId, VersionStamp.Default, "proj1", "proj1", LanguageNames.CSharp, Path.Combine(s_projectDir, "proj1.dll")))
.AddProject(ProjectInfo.Create(projectId2, VersionStamp.Default, "proj2", "proj2", LanguageNames.CSharp, Path.Combine(s_projectDir, "proj2.dll")))
.AddDocument(d1, "d1.cs", SourceText.From("class D1;", Encoding.UTF8, SourceHashAlgorithms.Default), filePath: Path.Combine(s_projectDir, "d1.cs"))
.AddDocument(d2, "d2.cs", SourceText.From("class D2;", Encoding.UTF8, SourceHashAlgorithms.Default), filePath: Path.Combine(s_projectDir, "d2.cs"))
.AddAdditionalDocument(a1, "a1.txt", SourceText.From("text1", Encoding.UTF8, SourceHashAlgorithms.Default))
.AddAdditionalDocument(a2, "a2.txt", SourceText.From("text2", Encoding.UTF8, SourceHashAlgorithms.Default))
.AddAnalyzerConfigDocument(c1, "c1", SourceText.From("#empty1", Encoding.UTF8, SourceHashAlgorithms.Default), filePath: Path.Combine(s_projectDir, "editorcfg"))
.AddAnalyzerConfigDocument(c2, "c2", SourceText.From("#empty2", Encoding.UTF8, SourceHashAlgorithms.Default), filePath: Path.Combine(s_projectDir, "editorcfg"));
var oldProject = solution.GetRequiredProject(projectId);
var documentIds = oldProject.DocumentIds;
var newDocumentInfo1 = DocumentInfo.Create(
d1,
name: "newD1",
folders: ["f", "g"],
sourceCodeKind: SourceCodeKind.Script,
loader: TextLoader.From(TextAndVersion.Create(SourceText.From("class NewD1;", Encoding.UTF32, SourceHashAlgorithm.Sha256), VersionStamp.Create(), filePath: Path.Combine(s_projectDir, "newD1.cs"))),
filePath: Path.Combine(s_projectDir, "newD1.cs"),
isGenerated: true);
var newDocumentInfo3 = DocumentInfo.Create(
d3,
name: "newD3",
folders: null,
sourceCodeKind: SourceCodeKind.Regular,
loader: TextLoader.From(TextAndVersion.Create(SourceText.From("class NewD3;", Encoding.UTF8, SourceHashAlgorithms.Default), VersionStamp.Create(), filePath: Path.Combine(s_projectDir, "newD3.cs"))),
filePath: Path.Combine(s_projectDir, "newD3.cs"),
isGenerated: false)
.WithDocumentServiceProvider(new TestDocumentServiceProvider());
var newAddDocumentInfo1 = DocumentInfo.Create(
a1,
name: "newA1",
folders: ["af", "ag"],
sourceCodeKind: SourceCodeKind.Script,
loader: TextLoader.From(TextAndVersion.Create(SourceText.From("new text1", Encoding.UTF32, SourceHashAlgorithm.Sha256), VersionStamp.Create(), filePath: Path.Combine(s_projectDir, "newD1.cs"))),
filePath: Path.Combine(s_projectDir, "newA1.txt"),
isGenerated: true);
var newAddDocumentInfo3 = DocumentInfo.Create(
a3,
name: "newA3",
folders: null,
sourceCodeKind: SourceCodeKind.Regular,
loader: TextLoader.From(TextAndVersion.Create(SourceText.From("new text3", Encoding.UTF8, SourceHashAlgorithms.Default), VersionStamp.Create(), filePath: Path.Combine(s_projectDir, "newD3.cs"))),
filePath: Path.Combine(s_projectDir, "newA3.txt"),
isGenerated: false)
.WithDocumentServiceProvider(new TestDocumentServiceProvider());
var newConfigDocumentInfo1 = DocumentInfo.Create(
c1,
name: "newC1",
folders: ["cf", "cg"],
sourceCodeKind: SourceCodeKind.Script,
loader: TextLoader.From(TextAndVersion.Create(SourceText.From("#new empty1", Encoding.UTF32, SourceHashAlgorithm.Sha256), VersionStamp.Create(), filePath: Path.Combine(s_projectDir, "newD1.cs"))),
filePath: Path.Combine(s_projectDir, "newC1"),
isGenerated: true);
var newConfigDocumentInfo3 = DocumentInfo.Create(
c3,
name: "newC3",
folders: null,
sourceCodeKind: SourceCodeKind.Regular,
loader: TextLoader.From(TextAndVersion.Create(SourceText.From("#new empty3", Encoding.UTF8, SourceHashAlgorithms.Default), VersionStamp.Create(), filePath: Path.Combine(s_projectDir, "newD3.cs"))),
filePath: Path.Combine(s_projectDir, "newC3"),
isGenerated: false)
.WithDocumentServiceProvider(new TestDocumentServiceProvider());
var metadataReference = MetadataReference.CreateFromImage([], filePath: "meta");
var projectReference = new ProjectReference(projectId2);
var analyzerReference = new TestAnalyzerReference();
var generatedOutputDir = Path.Combine(TempRoot.Root, "obj");
var assemblyPath = Path.Combine(TempRoot.Root, "bin", "assemblyName.dll");
var newInfo = ProjectInfo.Create(
projectId,
VersionStamp.Create(),
name: "newProjectName",
assemblyName: "newAssemblyName",
language: LanguageNames.CSharp,
filePath: "newFilePath.cs",
outputFilePath: "newOutputFilePath",
outputRefFilePath: "newOutputRef",
compilationOptions: new CSharpCompilationOptions(OutputKind.WindowsApplication, moduleName: "newModuleName"),
parseOptions: new CSharpParseOptions(CS.LanguageVersion.CSharp5),
documents:
[
// update existing document:
newDocumentInfo1,
// add new document:
newDocumentInfo3,
// remove existing document (d2)
],
additionalDocuments:
[
// update existing document:
newAddDocumentInfo1,
// add new document:
newAddDocumentInfo3,
// remove existing document (a2)
],
projectReferences: [projectReference],
metadataReferences: [metadataReference],
analyzerReferences: [analyzerReference],
isSubmission: false,
hostObjectType: null)
.WithAnalyzerConfigDocuments(
[
// update existing document:
newConfigDocumentInfo1,
// add new document:
newConfigDocumentInfo3,
// remove existing document (c2)
])
.WithCompilationOutputInfo(new CompilationOutputInfo(assemblyPath, generatedOutputDir));
var newSolution = solution.WithProjectInfo(newInfo);
var newProject = newSolution.GetRequiredProject(projectId);
// attributes:
Assert.True(newProject.Version.GetTestAccessor().IsNewerThan(oldProject.Version));
Assert.Equal(newInfo.Name, newProject.Name);
Assert.Equal(newInfo.AssemblyName, newProject.AssemblyName);
Assert.Equal(newInfo.Language, newProject.Language);
Assert.Equal(newInfo.FilePath, newProject.FilePath);
Assert.Equal(newInfo.OutputFilePath, newProject.OutputFilePath);
Assert.Equal(newInfo.CompilationOutputInfo, newProject.CompilationOutputInfo);
Assert.Equal(newInfo.CompilationOptions!.OutputKind, newProject.CompilationOptions!.OutputKind);
Assert.Equal(newInfo.CompilationOptions!.ModuleName, newProject.CompilationOptions!.ModuleName);
Assert.Equal(newInfo.ParseOptions!.LanguageVersion, newProject.ParseOptions!.LanguageVersion);
Assert.Equal(newInfo.OutputRefFilePath, newProject.OutputRefFilePath);
AssertEx.AreEqual([projectReference], newProject.ProjectReferences);
AssertEx.AreEqual([metadataReference], newProject.MetadataReferences);
AssertEx.AreEqual([analyzerReference], newProject.AnalyzerReferences);
// documents:
AssertEx.SetEqual([d1, d3], newProject.DocumentIds);
var newDocument1 = newProject.GetRequiredDocument(d1);
var newText1 = newDocument1.GetTextSynchronously(CancellationToken.None);
Assert.Equal(newDocumentInfo1.Name, newDocument1.Name);
Assert.Equal(newDocumentInfo1.FilePath, newDocument1.FilePath);
Assert.Same(DefaultTextDocumentServiceProvider.Instance, newDocument1.DocumentServiceProvider);
Assert.Equal("class NewD1;", newText1.ToString());
Assert.Same(Encoding.UTF32, newText1.Encoding);
Assert.Equal(SourceHashAlgorithm.Sha256, newText1.ChecksumAlgorithm);
var newDocument3 = newProject.GetRequiredDocument(d3);
var newText3 = newDocument3.GetTextSynchronously(CancellationToken.None);
Assert.Equal(newDocumentInfo3.Name, newDocument3.Name);
Assert.Equal(newDocumentInfo3.FilePath, newDocument3.FilePath);
Assert.Same(newDocumentInfo3.DocumentServiceProvider, newDocument3.DocumentServiceProvider);
Assert.Equal("class NewD3;", newDocument3.GetTextSynchronously(CancellationToken.None).ToString());
Assert.Same(Encoding.UTF8, newText3.Encoding);
Assert.Equal(SourceHashAlgorithms.Default, newText3.ChecksumAlgorithm);
// additional documents:
AssertEx.SetEqual([a1, a3], newProject.AdditionalDocumentIds);
var newAddDocument1 = newProject.GetRequiredAdditionalDocument(a1);
var newAddText1 = newAddDocument1.GetTextSynchronously(CancellationToken.None);
Assert.Equal(newAddDocumentInfo1.Name, newAddDocument1.Name);
Assert.Equal(newAddDocumentInfo1.FilePath, newAddDocument1.FilePath);
Assert.Same(DefaultTextDocumentServiceProvider.Instance, newAddDocument1.DocumentServiceProvider);
Assert.Equal("new text1", newAddText1.ToString());
Assert.Same(Encoding.UTF32, newAddText1.Encoding);
Assert.Equal(SourceHashAlgorithm.Sha256, newAddText1.ChecksumAlgorithm);
var newAddDocument3 = newProject.GetRequiredAdditionalDocument(a3);
var newAddText3 = newAddDocument3.GetTextSynchronously(CancellationToken.None);
Assert.Equal(newAddDocumentInfo3.Name, newAddDocument3.Name);
Assert.Equal(newAddDocumentInfo3.FilePath, newAddDocument3.FilePath);
Assert.Same(newAddDocumentInfo3.DocumentServiceProvider, newAddDocument3.DocumentServiceProvider);
Assert.Equal("new text3", newAddDocument3.GetTextSynchronously(CancellationToken.None).ToString());
Assert.Same(Encoding.UTF8, newAddText3.Encoding);
Assert.Equal(SourceHashAlgorithms.Default, newAddText3.ChecksumAlgorithm);
// analyzer config documents:
AssertEx.SetEqual([c1, c3], newProject.AnalyzerConfigDocumentIds);
var newConfigDocument1 = newProject.GetRequiredAnalyzerConfigDocument(c1);
var newConfigText1 = newConfigDocument1.GetTextSynchronously(CancellationToken.None);
Assert.Equal(newConfigDocumentInfo1.Name, newConfigDocument1.Name);
Assert.Equal(newConfigDocumentInfo1.FilePath, newConfigDocument1.FilePath);
Assert.Same(DefaultTextDocumentServiceProvider.Instance, newConfigDocument1.DocumentServiceProvider);
Assert.Equal("#new empty1", newConfigText1.ToString());
Assert.Same(Encoding.UTF32, newConfigText1.Encoding);
Assert.Equal(SourceHashAlgorithm.Sha256, newConfigText1.ChecksumAlgorithm);
var newConfigDocument3 = newProject.GetRequiredAnalyzerConfigDocument(c3);
var newConfigText3 = newConfigDocument3.GetTextSynchronously(CancellationToken.None);
Assert.Equal(newConfigDocumentInfo3.Name, newConfigDocument3.Name);
Assert.Equal(newConfigDocumentInfo3.FilePath, newConfigDocument3.FilePath);
Assert.Same(newConfigDocumentInfo3.DocumentServiceProvider, newConfigDocument3.DocumentServiceProvider);
Assert.Equal("#new empty3", newConfigDocument3.GetTextSynchronously(CancellationToken.None).ToString());
Assert.Same(Encoding.UTF8, newConfigText3.Encoding);
Assert.Equal(SourceHashAlgorithms.Default, newConfigText3.ChecksumAlgorithm);
}
[Fact]
public void WithProjectInfo_Unsupported_RemovingCompilationOptions()
{
var projectId = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(ProjectInfo.Create(projectId, VersionStamp.Default, "proj1", "proj1", LanguageNames.CSharp, Path.Combine(s_projectDir, "proj1.dll")));
var oldProject = solution.GetRequiredProject(projectId);
var documentIds = oldProject.DocumentIds;
var newInfo = ProjectInfo.Create(
projectId,
VersionStamp.Create(),
name: "newProjectName",
assemblyName: "newAssemblyName",
language: LanguageNames.CSharp,
filePath: "newFilePath.cs",
outputFilePath: "newOutputFilePath",
outputRefFilePath: "newOutputRef",
compilationOptions: new CSharpCompilationOptions(OutputKind.WindowsApplication, moduleName: "newModuleName"),
parseOptions: null,
documents: [],
additionalDocuments: [],
projectReferences: [],
metadataReferences: [],
analyzerReferences: [],
isSubmission: false,
hostObjectType: null);
Assert.Throws<NotSupportedException>(() => solution.WithProjectInfo(newInfo));
}
[Fact]
public void WithProjectInfo_Unsupported_RemovingParseOptions()
{
var projectId = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(ProjectInfo.Create(projectId, VersionStamp.Default, "proj1", "proj1", LanguageNames.CSharp, Path.Combine(s_projectDir, "proj1.dll")));
var oldProject = solution.GetRequiredProject(projectId);
var documentIds = oldProject.DocumentIds;
var newInfo = ProjectInfo.Create(
projectId,
VersionStamp.Create(),
name: "newProjectName",
assemblyName: "newAssemblyName",
language: LanguageNames.CSharp,
filePath: "newFilePath.cs",
outputFilePath: "newOutputFilePath",
outputRefFilePath: "newOutputRef",
compilationOptions: null,
parseOptions: new CSharpParseOptions(CS.LanguageVersion.CSharp5),
documents: [],
additionalDocuments: [],
projectReferences: [],
metadataReferences: [],
analyzerReferences: [],
isSubmission: false,
hostObjectType: null);
Assert.Throws<NotSupportedException>(() => solution.WithProjectInfo(newInfo));
}
[Fact]
public void WithProjectInfo_Unsupported_ChangingLanguage()
{
var projectId = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(ProjectInfo.Create(projectId, VersionStamp.Default, "proj1", "proj1", LanguageNames.CSharp, Path.Combine(s_projectDir, "proj1.dll")));
var oldProject = solution.GetRequiredProject(projectId);
var documentIds = oldProject.DocumentIds;
var newInfo = ProjectInfo.Create(
projectId,
VersionStamp.Create(),
name: "newProjectName",
assemblyName: "newAssemblyName",
language: LanguageNames.VisualBasic,
filePath: "newFilePath.cs",
outputFilePath: "newOutputFilePath",
outputRefFilePath: "newOutputRef",
compilationOptions: null,
parseOptions: null,
documents: [],
additionalDocuments: [],
projectReferences: [],
metadataReferences: [],
analyzerReferences: [],
isSubmission: false,
hostObjectType: null);
Assert.Throws<NotSupportedException>(() => solution.WithProjectInfo(newInfo));
}
[Fact]
public void WithProjectInfo_Unsupported_ChangingIsSubmission()
{
var projectId = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(ProjectInfo.Create(projectId, VersionStamp.Default, "proj1", "proj1", LanguageNames.CSharp, Path.Combine(s_projectDir, "proj1.dll")));
var oldProject = solution.GetRequiredProject(projectId);
var documentIds = oldProject.DocumentIds;
var newInfo = ProjectInfo.Create(
projectId,
VersionStamp.Create(),
name: "newProjectName",
assemblyName: "newAssemblyName",
language: LanguageNames.CSharp,
filePath: "newFilePath.cs",
outputFilePath: "newOutputFilePath",
outputRefFilePath: "newOutputRef",
compilationOptions: new CSharpCompilationOptions(OutputKind.WindowsApplication, moduleName: "newModuleName"),
parseOptions: CS.CSharpParseOptions.Default,
documents: [],
additionalDocuments: [],
projectReferences: [],
metadataReferences: [],
analyzerReferences: [],
isSubmission: true,
hostObjectType: null);
Assert.Throws<NotSupportedException>(() => solution.WithProjectInfo(newInfo));
}
[Fact]
public void WithProjectAssemblyName()
{
var projectId = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp);
// any character is allowed
var assemblyName = "\0<>a/b/*.dll";
var newSolution = solution.WithProjectAssemblyName(projectId, assemblyName);
Assert.Equal(assemblyName, newSolution.GetProject(projectId)!.AssemblyName);
Assert.Same(newSolution, newSolution.WithProjectAssemblyName(projectId, assemblyName));
Assert.Throws<ArgumentNullException>("assemblyName", () => solution.WithProjectAssemblyName(projectId, null!));
Assert.Throws<ArgumentNullException>("projectId", () => solution.WithProjectAssemblyName(null!, "x.dll"));
Assert.Throws<InvalidOperationException>(() => solution.WithProjectAssemblyName(ProjectId.CreateNewId(), "x.dll"));
}
[Fact]
public void WithProjectOutputFilePath()
{
var projectId = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp);
// any character is allowed
var path = "\0<>a/b/*.dll";
SolutionTestHelpers.TestProperty(
solution,
(s, value) => s.WithProjectOutputFilePath(projectId, value),
s => s.GetRequiredProject(projectId).OutputFilePath,
(string?)path,
defaultThrows: false);
Assert.Throws<ArgumentNullException>("projectId", () => solution.WithProjectOutputFilePath(null!, "x.dll"));
Assert.Throws<ArgumentNullException>("projectId", () => solution.WithProjectOutputFilePath(null!, "x.dll"));
Assert.Throws<InvalidOperationException>(() => solution.WithProjectOutputFilePath(ProjectId.CreateNewId(), "x.dll"));
}
[Fact]
public void GetEffectiveGeneratedFilesOutputDirectory()
{
var projectId = ProjectId.CreateNewId();
var objDir = Path.Combine(TempRoot.Root, "obj");
var binDir = Path.Combine(TempRoot.Root, "bin");
var otherDir = Path.Combine(TempRoot.Root, "other");
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp);
var project = solution.GetRequiredProject(projectId);
Assert.False(project.CompilationOutputInfo.HasEffectiveGeneratedFilesOutputDirectory);
project = project
.WithOutputFilePath(Path.Combine(binDir, "output.dll"))
.WithCompilationOutputInfo(new CompilationOutputInfo(
assemblyPath: Path.Combine(objDir, "output.dll"),
generatedFilesOutputDirectory: null));
Assert.True(project.CompilationOutputInfo.HasEffectiveGeneratedFilesOutputDirectory);
AssertEx.AreEqual(objDir, project.CompilationOutputInfo.GetEffectiveGeneratedFilesOutputDirectory());
project = project.WithCompilationOutputInfo(
project.CompilationOutputInfo.WithGeneratedFilesOutputDirectory(otherDir));
Assert.True(project.CompilationOutputInfo.HasEffectiveGeneratedFilesOutputDirectory);
AssertEx.AreEqual(otherDir, project.CompilationOutputInfo.GetEffectiveGeneratedFilesOutputDirectory());
}
[Fact]
public void WithProjectOutputRefFilePath()
{
var projectId = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp);
// any character is allowed
var path = "\0<>a/b/*.dll";
SolutionTestHelpers.TestProperty(
solution,
(s, value) => s.WithProjectOutputRefFilePath(projectId, value),
s => s.GetRequiredProject(projectId).OutputRefFilePath,
(string?)path,
defaultThrows: false);
Assert.Throws<ArgumentNullException>("projectId", () => solution.WithProjectOutputRefFilePath(null!, "x.dll"));
Assert.Throws<InvalidOperationException>(() => solution.WithProjectOutputRefFilePath(ProjectId.CreateNewId(), "x.dll"));
}
[Fact]
public void WithProjectCompilationOutputInfo()
{
var projectId = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp);
// any character is allowed
var info = new CompilationOutputInfo("\0<>a/b/*.dll", TempRoot.Root + "<>\0");
SolutionTestHelpers.TestProperty(
solution,
(s, value) => s.WithProjectCompilationOutputInfo(projectId, value),
s => s.GetRequiredProject(projectId).CompilationOutputInfo,
info,
defaultThrows: false);
Assert.Throws<ArgumentNullException>("projectId", () => solution.WithProjectCompilationOutputInfo(null!, info));
Assert.Throws<InvalidOperationException>(() => solution.WithProjectCompilationOutputInfo(ProjectId.CreateNewId(), info));
}
[Fact]
public void WithProjectDefaultNamespace()
{
var projectId = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution.
AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp);
// any character is allowed
var defaultNamespace = "\0<>a/b/*";
SolutionTestHelpers.TestProperty(
solution,
(s, value) => s.WithProjectDefaultNamespace(projectId, value),
s => s.GetRequiredProject(projectId).DefaultNamespace,
(string?)defaultNamespace,
defaultThrows: false);
Assert.Throws<ArgumentNullException>("projectId", () => solution.WithProjectDefaultNamespace(null!, "x"));
Assert.Throws<InvalidOperationException>(() => solution.WithProjectDefaultNamespace(ProjectId.CreateNewId(), "x"));
}
[Fact]
public void WithProjectChecksumAlgorithm()
{
var projectId = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution.
AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp);
SolutionTestHelpers.TestProperty(
solution,
(s, value) => s.WithProjectChecksumAlgorithm(projectId, value),
s => s.GetRequiredProject(projectId).State.ChecksumAlgorithm,
SourceHashAlgorithms.Default,
defaultThrows: false);
}
[Fact]
public async Task WithProjectChecksumAlgorithm_DocumentUpdates()
{
var projectId = ProjectId.CreateNewId();
var documentAId = DocumentId.CreateNewId(projectId);
var documentBId = DocumentId.CreateNewId(projectId);
var documentCId = DocumentId.CreateNewId(projectId);
var fileDocumentId = DocumentId.CreateNewId(projectId);
var fileD = Temp.CreateFile();
var bytes = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true).GetBytes("Text");
fileD.WriteAllBytes(bytes);
var sha256 = SHA256.Create();
// CodeQL [SM02196] This is not enabled by default but exists as a compat option for existing builds.
var sha1 = SHA1.Create();
var checksumSHA1 = sha1.ComputeHash(bytes);
var checksumSHA256 = sha256.ComputeHash(bytes);
sha256.Dispose();
sha1.Dispose();
using var workspace = CreateWorkspace();
var textLoaderA = new TestTextLoader("class A {}", SourceHashAlgorithm.Sha1);
var textC = SourceText.From("class C {}", encoding: null, checksumAlgorithm: SourceHashAlgorithm.Sha1);
var solution = workspace.CurrentSolution
.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp)
.WithProjectChecksumAlgorithm(projectId, SourceHashAlgorithm.Sha1);
solution = solution.AddDocument(DocumentInfo.Create(documentAId, "a.cs", loader: textLoaderA, filePath: "a.cs"));
solution = solution.AddDocument(documentBId, "b.cs", "class B {}", filePath: "b.cs");
solution = solution.AddDocument(documentCId, "c.cs", textC, filePath: "c.cs");
solution = solution.AddDocument(DocumentInfo.Create(fileDocumentId, "d.cs", loader: new FileTextLoader(fileD.Path, defaultEncoding: null), filePath: fileD.Path));
await Verify(solution.GetRequiredDocument(documentAId), SourceHashAlgorithm.Sha1);
await Verify(solution.GetRequiredDocument(documentBId), SourceHashAlgorithm.Sha1);
await Verify(solution.GetRequiredDocument(documentCId), SourceHashAlgorithm.Sha1);
await Verify(solution.GetRequiredDocument(fileDocumentId), SourceHashAlgorithm.Sha1, checksumSHA1);
// only file loader based documents support updating checksum alg:
solution = solution.WithProjectChecksumAlgorithm(projectId, SourceHashAlgorithm.Sha256);
await Verify(solution.GetRequiredDocument(documentAId), SourceHashAlgorithm.Sha1);
await Verify(solution.GetRequiredDocument(documentBId), SourceHashAlgorithm.Sha1);
await Verify(solution.GetRequiredDocument(documentCId), SourceHashAlgorithm.Sha1);
await Verify(solution.GetRequiredDocument(fileDocumentId), SourceHashAlgorithm.Sha256, checksumSHA256);
// only file loader based documents support updating checksum alg:
solution = solution.WithProjectChecksumAlgorithm(projectId, SourceHashAlgorithm.Sha1);
await Verify(solution.GetRequiredDocument(documentAId), SourceHashAlgorithm.Sha1);
await Verify(solution.GetRequiredDocument(documentBId), SourceHashAlgorithm.Sha1);
await Verify(solution.GetRequiredDocument(documentCId), SourceHashAlgorithm.Sha1);
await Verify(solution.GetRequiredDocument(fileDocumentId), SourceHashAlgorithm.Sha1, checksumSHA1);
static async Task Verify(Document document, SourceHashAlgorithm expectedAlgorithm, byte[]? expectedChecksum = null)
{
Assert.Equal(expectedAlgorithm, document.State.LoadTextOptions.ChecksumAlgorithm);
Assert.Equal(expectedAlgorithm, (await document.GetTextAsync(default)).ChecksumAlgorithm);
Assert.Equal(expectedAlgorithm, document.GetTextSynchronously(default).ChecksumAlgorithm);
Assert.Equal(expectedAlgorithm, (await document.GetRequiredSyntaxTreeAsync(default)).GetText().ChecksumAlgorithm);
Assert.Equal(expectedAlgorithm, document.GetRequiredSyntaxTreeSynchronously(default).GetText().ChecksumAlgorithm);
if (expectedChecksum != null)
{
Assert.Equal(expectedChecksum, document.GetTextSynchronously(default).GetChecksum());
}
var compilation = await document.Project.GetRequiredCompilationAsync(default);
var tree = compilation.SyntaxTrees.Single(t => t.FilePath == document.FilePath);
Assert.Equal(expectedAlgorithm, (await tree.GetTextAsync(default)).ChecksumAlgorithm);
Assert.Equal(expectedAlgorithm, tree.GetText(default).ChecksumAlgorithm);
}
}
[Fact]
public void WithProjectName()
{
var projectId = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp);
// any character is allowed
var projectName = "\0<>a/b/*";
SolutionTestHelpers.TestProperty(
solution,
(s, value) => s.WithProjectName(projectId, value),
s => s.GetRequiredProject(projectId).Name,
projectName,
defaultThrows: true);
Assert.Throws<ArgumentNullException>("projectId", () => solution.WithProjectName(null!, "x"));
Assert.Throws<InvalidOperationException>(() => solution.WithProjectName(ProjectId.CreateNewId(), "x"));
}
[Fact]
public void WithProjectFilePath()
{
var projectId = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp);
// any character is allowed
var path = "\0<>a/b/*.csproj";
SolutionTestHelpers.TestProperty(
solution,
(s, value) => s.WithProjectFilePath(projectId, value),
s => s.GetRequiredProject(projectId).FilePath,
(string?)path,
defaultThrows: false);
Assert.Throws<ArgumentNullException>("projectId", () => solution.WithProjectFilePath(null!, "x"));
Assert.Throws<InvalidOperationException>(() => solution.WithProjectFilePath(ProjectId.CreateNewId(), "x"));
}
[Fact]
public void WithProjectCompilationOptionsExceptionHandling()
{
var projectId = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp);
var options = new CSharpCompilationOptions(OutputKind.NetModule);
Assert.Throws<ArgumentNullException>("projectId", () => solution.WithProjectCompilationOptions(null!, options));
Assert.Throws<ArgumentNullException>("options", () => solution.WithProjectCompilationOptions(projectId, options: null!));
Assert.Throws<InvalidOperationException>(() => solution.WithProjectCompilationOptions(ProjectId.CreateNewId(), options));
}
[Theory, CombinatorialData]
public void WithProjectCompilationOptionsReplacesSyntaxTreeOptionProvider([CombinatorialValues(LanguageNames.CSharp, LanguageNames.VisualBasic)] string languageName)
{
var projectId = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(projectId, "proj1", "proj1.dll", languageName);
// We always have a non-null SyntaxTreeOptionsProvider for C# and VB projects
var originalSyntaxTreeOptionsProvider = solution.Projects.Single().CompilationOptions!.SyntaxTreeOptionsProvider;
Assert.NotNull(originalSyntaxTreeOptionsProvider);
var defaultOptions = solution.Projects.Single().Services.GetRequiredService<ICompilationFactoryService>().GetDefaultCompilationOptions();
Assert.Null(defaultOptions.SyntaxTreeOptionsProvider);
solution = solution.WithProjectCompilationOptions(projectId, defaultOptions);
// The CompilationOptions we replaced with didn't have a SyntaxTreeOptionsProvider, but we would have placed it
// back. The SyntaxTreeOptionsProvider should behave the same as the prior one and thus should be equal.
var newSyntaxTreeOptionsProvider = solution.Projects.Single().CompilationOptions!.SyntaxTreeOptionsProvider;
Assert.NotNull(newSyntaxTreeOptionsProvider);
Assert.Equal(originalSyntaxTreeOptionsProvider, newSyntaxTreeOptionsProvider);
}
[Fact]
public void WithProjectParseOptions()
{
var projectId = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp);
var options = new CSharpParseOptions(CS.LanguageVersion.CSharp1);
SolutionTestHelpers.TestProperty(
solution,
(s, value) => s.WithProjectParseOptions(projectId, value),
s => s.GetRequiredProject(projectId).ParseOptions!,
(ParseOptions)options,
defaultThrows: true);
Assert.Throws<ArgumentNullException>("projectId", () => solution.WithProjectParseOptions(null!, options));
Assert.Throws<ArgumentNullException>("options", () => solution.WithProjectParseOptions(projectId, options: null!));
Assert.Throws<InvalidOperationException>(() => solution.WithProjectParseOptions(ProjectId.CreateNewId(), options));
}
[Fact]
public async Task ChangingLanguageVersionReparses()
{
var projectId = ProjectId.CreateNewId();
var documentId = DocumentId.CreateNewId(projectId);
using var workspace = CreateWorkspace();
var document = workspace.CurrentSolution
.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp)
.AddDocument(documentId, "Test.cs", "// File")
.GetRequiredDocument(documentId);
var oldTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None);
Assert.Equal(document.Project.ParseOptions, oldTree.Options);
document = document.Project.WithParseOptions(new CSharpParseOptions(languageVersion: CS.LanguageVersion.CSharp1)).GetRequiredDocument(documentId);
var newTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None);
Assert.Equal(document.Project.ParseOptions, newTree.Options);
Assert.False(oldTree.GetRoot().IsIncrementallyIdenticalTo(newTree.GetRoot()));
}
[Theory]
[InlineData("#if DEBUG", false, LanguageNames.CSharp)]
[InlineData(@"#region ""goo""", true, LanguageNames.CSharp)]
[InlineData(@"#nullable enable", true, LanguageNames.CSharp)]
[InlineData(@"#elif DEBUG", true, LanguageNames.CSharp)]
[InlineData("// File", true, LanguageNames.CSharp)]
[InlineData("#if DEBUG", false, LanguageNames.VisualBasic)]
[InlineData(@"#region ""goo""", true, LanguageNames.VisualBasic)]
[InlineData(@"#ElseIf DEBUG", true, LanguageNames.VisualBasic)]
[InlineData("' File", true, LanguageNames.VisualBasic)]
public async Task ChangingPreprocessorDirectivesMayReparse(string source, bool expectReuse, string languageName)
{
var projectId = ProjectId.CreateNewId();
var documentId = DocumentId.CreateNewId(projectId);
using var workspace = CreateWorkspace();
var document = workspace.CurrentSolution
.AddProject(projectId, "proj1", "proj1.dll", languageName)
.AddDocument(documentId, "Test", source)
.GetRequiredDocument(documentId);
var oldTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None);
// Hold onto the old root, so we don't actually release the root; if the root were to fall away
// we're unable to use IsIncrementallyIdenticalTo to see if we didn't reparse, since asking for
// the old root will recover the tree and produce a new green node.
var oldRoot = oldTree.GetRoot();
Assert.Equal(document.Project.ParseOptions, oldTree.Options);
ParseOptions newOptions = languageName == LanguageNames.CSharp
? new CSharpParseOptions(preprocessorSymbols: ["DEBUG"])
: new VisualBasicParseOptions(preprocessorSymbols: [new("DEBUG", null)]);
document = document.Project.WithParseOptions(newOptions).GetRequiredDocument(documentId);
var newTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None);
Assert.Equal(document.Project.ParseOptions, newTree.Options);
Assert.Equal(expectReuse, oldRoot.IsIncrementallyIdenticalTo(newTree.GetRoot()));
}
[Theory]
[InlineData(null, "test.cs")]
[InlineData("test.cs", null)]
[InlineData("", null)]
[InlineData("test.cs", "")]
public async Task ChangingFilePathReparses(string oldPath, string newPath)
{
var projectId = ProjectId.CreateNewId();
var documentId = DocumentId.CreateNewId(projectId);
using var workspace = CreateWorkspace();
var document = workspace.CurrentSolution
.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp)
.AddDocument(documentId, name: "Test.cs", text: "// File", filePath: oldPath)
.GetRequiredDocument(documentId);
var oldTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None);
var newDocument = document.WithFilePath(newPath);
var newTree = await newDocument.GetRequiredSyntaxTreeAsync(CancellationToken.None);
Assert.False(oldTree.GetRoot().IsIncrementallyIdenticalTo(newTree.GetRoot()));
}
[Fact]
public async Task ChangingName_ReparsesWhenPathIsNull()
{
var projectId = ProjectId.CreateNewId();
var documentId = DocumentId.CreateNewId(projectId);
using var workspace = CreateWorkspace();
var document = workspace.CurrentSolution
.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp)
.AddDocument(documentId, name: "name1", text: "// File", filePath: null)
.GetRequiredDocument(documentId);
var oldTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None);
var newDocument = document.WithName("name2");
var newTree = await newDocument.GetRequiredSyntaxTreeAsync(CancellationToken.None);
Assert.False(oldTree.GetRoot().IsIncrementallyIdenticalTo(newTree.GetRoot()));
}
[Fact]
public async Task ChangingName_NoReparse()
{
var projectId = ProjectId.CreateNewId();
var documentId = DocumentId.CreateNewId(projectId);
using var workspace = CreateWorkspace();
var document = workspace.CurrentSolution
.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp)
.AddDocument(documentId, name: "name1", text: "// File", filePath: "")
.GetRequiredDocument(documentId);
var oldTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None);
var newDocument = document.WithName("name2");
var newTree = await newDocument.GetRequiredSyntaxTreeAsync(CancellationToken.None);
Assert.True(oldTree.GetRoot().IsIncrementallyIdenticalTo(newTree.GetRoot()));
}
[Fact]
public void WithProjectReferences()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var projectId = solution.Projects.Single().Id;
var projectId2 = ProjectId.CreateNewId();
solution = solution.AddProject(projectId2, "proj2", "proj2.dll", LanguageNames.CSharp);
var projectRef = new ProjectReference(projectId2);
SolutionTestHelpers.TestListProperty(solution,
(old, value) => old.WithProjectReferences(projectId, value),
opt => opt.GetProject(projectId)!.AllProjectReferences,
projectRef,
allowDuplicates: false);
var projectRefs = (IEnumerable<ProjectReference>)ImmutableArray.Create(
new ProjectReference(projectId2),
new ProjectReference(projectId2, ImmutableArray.Create("alias")),
new ProjectReference(projectId2, embedInteropTypes: true));
var solution2 = solution.WithProjectReferences(projectId, projectRefs);
Assert.Same(projectRefs, solution2.GetProject(projectId)!.AllProjectReferences);
Assert.Throws<ArgumentNullException>("projectId", () => solution.WithProjectReferences(null!, [projectRef]));
Assert.Throws<InvalidOperationException>(() => solution.WithProjectReferences(ProjectId.CreateNewId(), [projectRef]));
// cycles:
Assert.Throws<InvalidOperationException>(() => solution2.WithProjectReferences(projectId2, [new ProjectReference(projectId)]));
Assert.Throws<InvalidOperationException>(() => solution.WithProjectReferences(projectId, [new ProjectReference(projectId)]));
}
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/42406")]
public void WithProjectReferences_ProjectNotInSolution()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var projectId = solution.Projects.Single().Id;
var externalProjectRef = new ProjectReference(ProjectId.CreateNewId());
var projectRefs = (IEnumerable<ProjectReference>)ImmutableArray.Create(externalProjectRef);
var newSolution1 = solution.WithProjectReferences(projectId, projectRefs);
Assert.Same(projectRefs, newSolution1.GetProject(projectId)!.AllProjectReferences);
// project reference is not included:
Assert.Empty(newSolution1.GetProject(projectId)!.ProjectReferences);
}
[Fact]
public void AddProjectReferences()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var projectId = solution.Projects.Single().Id;
var projectId2 = ProjectId.CreateNewId();
var projectId3 = ProjectId.CreateNewId();
solution = solution
.AddProject(projectId2, "proj2", "proj2.dll", LanguageNames.CSharp)
.AddProject(projectId3, "proj3", "proj3.dll", LanguageNames.CSharp);
var projectRef2 = new ProjectReference(projectId2);
var projectRef3 = new ProjectReference(projectId3);
var externalProjectRef = new ProjectReference(ProjectId.CreateNewId());
solution = solution.AddProjectReference(projectId3, projectRef2);
var solution2 = solution.AddProjectReferences(projectId, EmptyEnumerable<ProjectReference>());
Assert.Same(solution, solution2);
var e = OnceEnumerable(projectRef2, externalProjectRef);
var solution3 = solution.AddProjectReferences(projectId, e);
AssertEx.Equal((ProjectReference[])[projectRef2], solution3.GetProject(projectId)!.ProjectReferences);
AssertEx.Equal((ProjectReference[])[projectRef2, externalProjectRef], solution3.GetProject(projectId)!.AllProjectReferences);
Assert.Throws<ArgumentNullException>("projectId", () => solution.AddProjectReferences(null!, [projectRef2]));
Assert.Throws<ArgumentNullException>("projectReferences", () => solution.AddProjectReferences(projectId, null!));
Assert.Throws<ArgumentNullException>("projectReferences[0]", () => solution.AddProjectReferences(projectId, [null!]));
Assert.Throws<ArgumentException>("projectReferences[1]", () => solution.AddProjectReferences(projectId, [projectRef2, projectRef2]));
Assert.Throws<ArgumentException>("projectReferences[1]", () => solution.AddProjectReferences(projectId, [new ProjectReference(projectId2), new ProjectReference(projectId2)]));
// dup:
Assert.Throws<InvalidOperationException>(() => solution.AddProjectReferences(projectId3, [projectRef2]));
// cycles:
Assert.Throws<InvalidOperationException>(() => solution3.AddProjectReferences(projectId2, [projectRef3]));
Assert.Throws<InvalidOperationException>(() => solution3.AddProjectReferences(projectId, [new ProjectReference(projectId)]));
}
[Fact]
public void RemoveProjectReference()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var projectId = solution.Projects.Single().Id;
var projectId2 = ProjectId.CreateNewId();
solution = solution.AddProject(projectId2, "proj2", "proj2.dll", LanguageNames.CSharp);
var projectRef2 = new ProjectReference(projectId2);
var externalProjectRef = new ProjectReference(ProjectId.CreateNewId());
solution = solution.WithProjectReferences(projectId, [projectRef2, externalProjectRef]);
// remove reference to a project that's not part of the solution:
var solution2 = solution.RemoveProjectReference(projectId, externalProjectRef);
AssertEx.Equal((ProjectReference[])[projectRef2], solution2.GetProject(projectId)!.AllProjectReferences);
// remove reference to a project that's part of the solution:
var solution3 = solution.RemoveProjectReference(projectId, projectRef2);
AssertEx.Equal((ProjectReference[])[externalProjectRef], solution3.GetProject(projectId)!.AllProjectReferences);
var solution4 = solution3.RemoveProjectReference(projectId, externalProjectRef);
Assert.Empty(solution4.GetProject(projectId)!.AllProjectReferences);
Assert.Throws<ArgumentNullException>("projectId", () => solution.RemoveProjectReference(null!, projectRef2));
Assert.Throws<ArgumentNullException>("projectReference", () => solution.RemoveProjectReference(projectId, null!));
// removing a reference that's not in the list:
Assert.Throws<ArgumentException>("projectReference", () => solution.RemoveProjectReference(projectId, new ProjectReference(ProjectId.CreateNewId())));
// project not in solution:
Assert.Throws<InvalidOperationException>(() => solution.RemoveProjectReference(ProjectId.CreateNewId(), projectRef2));
}
[Fact]
public void ProjectReferences_Submissions()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var projectId0 = ProjectId.CreateNewId();
var submissionId1 = ProjectId.CreateNewId();
var submissionId2 = ProjectId.CreateNewId();
var submissionId3 = ProjectId.CreateNewId();
solution = solution
.AddProject(projectId0, "non-submission", "non-submission.dll", LanguageNames.CSharp)
.AddProject(ProjectInfo.Create(submissionId1, VersionStamp.Default, name: "submission1", assemblyName: "submission1.dll", LanguageNames.CSharp, isSubmission: true))
.AddProject(ProjectInfo.Create(submissionId2, VersionStamp.Default, name: "submission2", assemblyName: "submission2.dll", LanguageNames.CSharp, isSubmission: true))
.AddProject(ProjectInfo.Create(submissionId3, VersionStamp.Default, name: "submission3", assemblyName: "submission3.dll", LanguageNames.CSharp, isSubmission: true))
.AddProjectReference(submissionId2, new ProjectReference(submissionId1))
.WithProjectReferences(submissionId2, [new ProjectReference(submissionId1)]);
// submission may be referenced from multiple submissions (forming a tree):
_ = solution.AddProjectReferences(submissionId3, [new ProjectReference(submissionId1)]);
_ = solution.WithProjectReferences(submissionId3, [new ProjectReference(submissionId1)]);
// submission may reference a non-submission project:
_ = solution.AddProjectReferences(submissionId3, [new ProjectReference(projectId0)]);
_ = solution.WithProjectReferences(submissionId3, [new ProjectReference(projectId0)]);
// submission can't reference multiple submissions:
Assert.Throws<InvalidOperationException>(() => solution.AddProjectReferences(submissionId2, [new ProjectReference(submissionId3)]));
Assert.Throws<InvalidOperationException>(() => solution.WithProjectReferences(submissionId1, [new ProjectReference(submissionId2), new ProjectReference(submissionId3)]));
// non-submission project can't reference a submission:
Assert.Throws<InvalidOperationException>(() => solution.AddProjectReferences(projectId0, [new ProjectReference(submissionId1)]));
Assert.Throws<InvalidOperationException>(() => solution.WithProjectReferences(projectId0, [new ProjectReference(submissionId1)]));
}
[Fact]
public void WithProjectMetadataReferences()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var projectId = solution.Projects.Single().Id;
var metadataRef = (MetadataReference)new TestMetadataReference();
SolutionTestHelpers.TestListProperty(solution,
(old, value) => old.WithProjectMetadataReferences(projectId, value),
opt => opt.GetProject(projectId)!.MetadataReferences,
metadataRef,
allowDuplicates: false);
Assert.Throws<ArgumentNullException>("projectId", () => solution.WithProjectMetadataReferences(null!, [metadataRef]));
Assert.Throws<InvalidOperationException>(() => solution.WithProjectMetadataReferences(ProjectId.CreateNewId(), [metadataRef]));
}
[Fact]
public void AddMetadataReferences()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var projectId = solution.Projects.Single().Id;
var solution2 = solution.AddMetadataReferences(projectId, EmptyEnumerable<MetadataReference>());
Assert.Same(solution, solution2);
var metadataRef1 = new TestMetadataReference();
var metadataRef2 = new TestMetadataReference();
var solution3 = solution.AddMetadataReferences(projectId, OnceEnumerable(metadataRef1, metadataRef2));
AssertEx.Equal((MetadataReference[])[metadataRef1, metadataRef2], solution3.GetProject(projectId)!.MetadataReferences);
Assert.Throws<ArgumentNullException>("projectId", () => solution.AddMetadataReferences(null!, [metadataRef1]));
Assert.Throws<ArgumentNullException>("metadataReferences", () => solution.AddMetadataReferences(projectId, null!));
Assert.Throws<ArgumentNullException>("metadataReferences[0]", () => solution.AddMetadataReferences(projectId, [null!]));
Assert.Throws<ArgumentException>("metadataReferences[1]", () => solution.AddMetadataReferences(projectId, [metadataRef1, metadataRef1]));
// dup:
Assert.Throws<InvalidOperationException>(() => solution3.AddMetadataReferences(projectId, [metadataRef1]));
}
[Fact]
public void RemoveMetadataReference()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var projectId = solution.Projects.Single().Id;
var metadataRef1 = new TestMetadataReference();
var metadataRef2 = new TestMetadataReference();
solution = solution.WithProjectMetadataReferences(projectId, [metadataRef1, metadataRef2]);
var solution2 = solution.RemoveMetadataReference(projectId, metadataRef1);
AssertEx.Equal((MetadataReference[])[metadataRef2], solution2.GetProject(projectId)!.MetadataReferences);
var solution3 = solution2.RemoveMetadataReference(projectId, metadataRef2);
Assert.Empty(solution3.GetProject(projectId)!.MetadataReferences);
Assert.Throws<ArgumentNullException>("projectId", () => solution.RemoveMetadataReference(null!, metadataRef1));
Assert.Throws<ArgumentNullException>("metadataReference", () => solution.RemoveMetadataReference(projectId, null!));
// removing a reference that's not in the list:
Assert.Throws<InvalidOperationException>(() => solution.RemoveMetadataReference(projectId, new TestMetadataReference()));
// project not in solution:
Assert.Throws<InvalidOperationException>(() => solution.RemoveMetadataReference(ProjectId.CreateNewId(), metadataRef1));
}
[Fact]
public void WithProjectAnalyzerReferences()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var projectId = solution.Projects.Single().Id;
var analyzerRef = (AnalyzerReference)new TestAnalyzerReference();
SolutionTestHelpers.TestListProperty(solution,
(old, value) => old.WithProjectAnalyzerReferences(projectId, value),
opt => opt.GetProject(projectId)!.AnalyzerReferences,
analyzerRef,
allowDuplicates: false);
Assert.Throws<ArgumentNullException>("projectId", () => solution.WithProjectAnalyzerReferences(null!, [analyzerRef]));
Assert.Throws<InvalidOperationException>(() => solution.WithProjectAnalyzerReferences(ProjectId.CreateNewId(), [analyzerRef]));
}
[Fact]
public void AddAnalyzerReferences_Project()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var projectId = solution.Projects.Single().Id;
var solution2 = solution.AddAnalyzerReferences(projectId, EmptyEnumerable<AnalyzerReference>());
Assert.Same(solution, solution2);
var analyzerRef1 = new TestAnalyzerReference();
var analyzerRef2 = new TestAnalyzerReference();
var solution3 = solution.AddAnalyzerReferences(projectId, OnceEnumerable(analyzerRef1, analyzerRef2));
AssertEx.Equal((AnalyzerReference[])[analyzerRef1, analyzerRef2], solution3.GetProject(projectId)!.AnalyzerReferences);
var solution4 = solution3.AddAnalyzerReferences(projectId, []);
Assert.Same(solution, solution2);
Assert.Throws<ArgumentNullException>("projectId", () => solution.AddAnalyzerReferences(null!, [analyzerRef1]));
Assert.Throws<ArgumentNullException>("analyzerReferences", () => solution.AddAnalyzerReferences(projectId, null!));
Assert.Throws<ArgumentNullException>("analyzerReferences[0]", () => solution.AddAnalyzerReferences(projectId, [null!]));
Assert.Throws<ArgumentException>("analyzerReferences[1]", () => solution.AddAnalyzerReferences(projectId, [analyzerRef1, analyzerRef1]));
// dup:
Assert.Throws<InvalidOperationException>(() => solution3.AddAnalyzerReferences(projectId, [analyzerRef1]));
}
[Fact]
public void RemoveAnalyzerReference_Project()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var projectId = solution.Projects.Single().Id;
var analyzerRef1 = new TestAnalyzerReference();
var analyzerRef2 = new TestAnalyzerReference();
solution = solution.WithProjectAnalyzerReferences(projectId, [analyzerRef1, analyzerRef2]);
var solution2 = solution.RemoveAnalyzerReference(projectId, analyzerRef1);
AssertEx.Equal((AnalyzerReference[])[analyzerRef2], solution2.GetProject(projectId)!.AnalyzerReferences);
var solution3 = solution2.RemoveAnalyzerReference(projectId, analyzerRef2);
Assert.Empty(solution3.GetProject(projectId)!.AnalyzerReferences);
Assert.Throws<ArgumentNullException>("projectId", () => solution.RemoveAnalyzerReference(null!, analyzerRef1));
Assert.Throws<ArgumentNullException>("analyzerReference", () => solution.RemoveAnalyzerReference(projectId, null!));
// removing a reference that's not in the list:
Assert.Throws<InvalidOperationException>(() => solution.RemoveAnalyzerReference(projectId, new TestAnalyzerReference()));
// project not in solution:
Assert.Throws<InvalidOperationException>(() => solution.RemoveAnalyzerReference(ProjectId.CreateNewId(), analyzerRef1));
}
[Fact]
public void WithAnalyzerReferences()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var analyzerRef = (AnalyzerReference)new TestAnalyzerReference();
SolutionTestHelpers.TestListProperty(solution,
(old, value) => old.WithAnalyzerReferences(value),
opt => opt.AnalyzerReferences,
analyzerRef,
allowDuplicates: false);
}
[Fact]
public void AddAnalyzerReferences()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var solution2 = solution.AddAnalyzerReferences(EmptyEnumerable<AnalyzerReference>());
Assert.Same(solution, solution2);
var analyzerRef1 = new TestAnalyzerReference();
var analyzerRef2 = new TestAnalyzerReference();
var solution3 = solution.AddAnalyzerReferences(OnceEnumerable(analyzerRef1, analyzerRef2));
AssertEx.Equal((AnalyzerReference[])[analyzerRef1, analyzerRef2], solution3.AnalyzerReferences);
var solution4 = solution3.AddAnalyzerReferences([]);
Assert.Same(solution, solution2);
Assert.Throws<ArgumentNullException>("analyzerReferences", () => solution.AddAnalyzerReferences(null!));
Assert.Throws<ArgumentNullException>("analyzerReferences[0]", () => solution.AddAnalyzerReferences([null!]));
Assert.Throws<ArgumentException>("analyzerReferences[1]", () => solution.AddAnalyzerReferences([analyzerRef1, analyzerRef1]));
// dup:
Assert.Throws<InvalidOperationException>(() => solution3.AddAnalyzerReferences([analyzerRef1]));
}
[Fact]
public void RemoveAnalyzerReference()
{
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var analyzerRef1 = new TestAnalyzerReference();
var analyzerRef2 = new TestAnalyzerReference();
solution = solution.WithAnalyzerReferences([analyzerRef1, analyzerRef2]);
var solution2 = solution.RemoveAnalyzerReference(analyzerRef1);
AssertEx.Equal((AnalyzerReference[])[analyzerRef2], solution2.AnalyzerReferences);
var solution3 = solution2.RemoveAnalyzerReference(analyzerRef2);
Assert.Empty(solution3.AnalyzerReferences);
Assert.Throws<ArgumentNullException>("analyzerReference", () => solution.RemoveAnalyzerReference(null!));
// removing a reference that's not in the list:
Assert.Throws<InvalidOperationException>(() => solution.RemoveAnalyzerReference(new TestAnalyzerReference()));
}
[Theory]
[CombinatorialData]
public void FallbackAnalyzerOptions(bool isRoot)
{
using var workspace = CreateWorkspaceWithProjectAndDocuments(editorConfig: $"""
{(isRoot ? "root = true" : "")}
[*]
optionA = A
""");
var solution = workspace.CurrentSolution;
Assert.True(solution.FallbackAnalyzerOptions.IsEmpty);
var solution2 = solution.WithFallbackAnalyzerOptions(ImmutableDictionary<string, StructuredAnalyzerConfigOptions>.Empty
.Add(LanguageNames.CSharp, StructuredAnalyzerConfigOptions.Create(new DictionaryAnalyzerConfigOptions(ImmutableDictionary<string, string>.Empty
.Add("optionA", "fallbackA")
.Add("optionB", "fallbackB")))));
TestOptionValues(solution2, expectedA: "A", hasBWithoutFallback: false, expectedB: "fallbackB", expectedBFile: "fallbackB");
var solution3 = solution2.WithFallbackAnalyzerOptions(ImmutableDictionary<string, StructuredAnalyzerConfigOptions>.Empty
.Add(LanguageNames.CSharp, StructuredAnalyzerConfigOptions.Create(new DictionaryAnalyzerConfigOptions(ImmutableDictionary<string, string>.Empty
.Add("optionA", "fallbackX")
.Add("optionB", "fallbackY")))));
TestOptionValues(solution3, expectedA: "A", hasBWithoutFallback: false, expectedB: "fallbackY", expectedBFile: "fallbackY");
Assert.True(workspace.TryApplyChanges(solution3));
TestOptionValues(workspace.CurrentSolution, expectedA: "A", hasBWithoutFallback: false, expectedB: "fallbackY", expectedBFile: "fallbackY");
var editorConfigContent = """
[*]
optionB = ec2
""";
var editorConfigId = DocumentId.CreateNewId(solution3.Projects.Single().Id);
var solution4 = solution3.AddAnalyzerConfigDocument(editorConfigId, ".editorconfig", SourceText.From(editorConfigContent), filePath: Path.Combine(s_projectDir, "subfolder", ".editorconfig"));
TestOptionValues(solution4, expectedA: "A", hasBWithoutFallback: true, expectedB: "fallbackY", expectedBFile: "ec2");
static void TestOptionValues(Solution solution, string expectedA, bool hasBWithoutFallback, string expectedB, string expectedBFile)
{
var project2 = solution.Projects.Single();
var projectOptions = project2.GetAnalyzerConfigOptions();
Assert.NotNull(projectOptions);
Assert.True(projectOptions!.Value.ConfigOptionsWithoutFallback.TryGetValue("optionA", out var value1));
Assert.Equal(expectedA, value1);
Assert.True(projectOptions!.Value.ConfigOptionsWithFallback.TryGetValue("optionA", out value1));
Assert.Equal(expectedA, value1);
Assert.False(projectOptions!.Value.ConfigOptionsWithoutFallback.TryGetValue("optionB", out var value2));
Assert.Null(value2);
Assert.True(projectOptions!.Value.ConfigOptionsWithFallback.TryGetValue("optionB", out value2));
Assert.Equal(expectedB, value2);
var sourcePathOptions = project2.State.GetAnalyzerOptionsForPath(Path.Combine(s_projectDir, "x.cs"), CancellationToken.None);
Assert.True(sourcePathOptions.ConfigOptionsWithoutFallback.TryGetValue("optionA", out var value3));
Assert.Equal(expectedA, value3);
Assert.True(sourcePathOptions.ConfigOptionsWithFallback.TryGetValue("optionA", out value3));
Assert.Equal(expectedA, value3);
Assert.False(sourcePathOptions.ConfigOptionsWithoutFallback.TryGetValue("optionB", out var value4));
Assert.Null(value4);
Assert.True(sourcePathOptions.ConfigOptionsWithFallback.TryGetValue("optionB", out value4));
Assert.Equal(expectedB, value4);
sourcePathOptions = project2.State.GetAnalyzerOptionsForPath(Path.Combine(s_projectDir, "subfolder", "x.cs"), CancellationToken.None);
Assert.True(sourcePathOptions.ConfigOptionsWithoutFallback.TryGetValue("optionA", out var value5));
Assert.Equal(expectedA, value5);
Assert.True(sourcePathOptions.ConfigOptionsWithFallback.TryGetValue("optionA", out value5));
Assert.Equal(expectedA, value5);
Assert.Equal(hasBWithoutFallback, sourcePathOptions.ConfigOptionsWithoutFallback.TryGetValue("optionB", out var value6));
Assert.Equal(hasBWithoutFallback ? expectedBFile : null, value6);
Assert.True(sourcePathOptions.ConfigOptionsWithFallback.TryGetValue("optionB", out value6));
Assert.Equal(expectedBFile, value6);
}
}
[Fact]
public void AddDocument_Loader()
{
var projectId = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp).
WithProjectChecksumAlgorithm(projectId, SourceHashAlgorithms.Default).
WithProjectParseOptions(projectId, new CSharpParseOptions(kind: SourceCodeKind.Script));
var loader = new TestTextLoader(checksumAlgorithm: SourceHashAlgorithm.Sha1);
var documentId = DocumentId.CreateNewId(projectId);
var folders = new[] { "folder1", "folder2" };
var solution2 = solution.AddDocument(documentId, "name", loader, folders);
var document = solution2.GetRequiredDocument(documentId);
AssertEx.Equal(folders, document.Folders);
Assert.Equal(SourceCodeKind.Script, document.SourceCodeKind);
Assert.Equal(SourceHashAlgorithm.Sha1, document.GetTextSynchronously(default).ChecksumAlgorithm);
Assert.Throws<ArgumentNullException>("documentId", () => solution.AddDocument(documentId: null!, "name", loader));
Assert.Throws<ArgumentNullException>("name", () => solution.AddDocument(documentId, name: null!, loader));
Assert.Throws<ArgumentNullException>("loader", () => solution.AddDocument(documentId, "name", loader: null!));
Assert.Throws<InvalidOperationException>(() => solution.AddDocument(documentId: DocumentId.CreateNewId(ProjectId.CreateNewId()), "name", loader));
}
[Fact]
public void AddDocument_Text()
{
var projectId = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp).
WithProjectChecksumAlgorithm(projectId, SourceHashAlgorithms.Default).
WithProjectParseOptions(projectId, new CSharpParseOptions(kind: SourceCodeKind.Script));
var documentId = DocumentId.CreateNewId(projectId);
var filePath = Path.Combine(TempRoot.Root, "x.cs");
var folders = new[] { "folder1", "folder2" };
var solution2 = solution.AddDocument(documentId, "name", "text", folders, filePath);
var document = solution2.GetRequiredDocument(documentId);
var sourceText = document.GetTextSynchronously(default);
Assert.Equal("text", sourceText.ToString());
Assert.Equal(SourceHashAlgorithms.Default, sourceText.ChecksumAlgorithm);
AssertEx.Equal(folders, document.Folders);
Assert.Equal(filePath, document.FilePath);
Assert.False(document.State.Attributes.IsGenerated);
Assert.Equal(SourceCodeKind.Script, document.SourceCodeKind);
Assert.Throws<ArgumentNullException>("documentId", () => solution.AddDocument(documentId: null!, "name", "text"));
Assert.Throws<ArgumentNullException>("name", () => solution.AddDocument(documentId, name: null!, "text"));
Assert.Throws<ArgumentNullException>("text", () => solution.AddDocument(documentId, "name", text: (string)null!));
Assert.Throws<InvalidOperationException>(() => solution.AddDocument(documentId: DocumentId.CreateNewId(ProjectId.CreateNewId()), "name", "text"));
}
[Fact]
public void AddDocument_SourceText()
{
var projectId = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp).
WithProjectChecksumAlgorithm(projectId, SourceHashAlgorithms.Default).
WithProjectParseOptions(projectId, new CSharpParseOptions(kind: SourceCodeKind.Script));
var documentId = DocumentId.CreateNewId(projectId);
var sourceText = SourceText.From("text", checksumAlgorithm: SourceHashAlgorithms.Default);
var filePath = Path.Combine(TempRoot.Root, "x.cs");
var folders = new[] { "folder1", "folder2" };
var solution2 = solution.AddDocument(documentId, "name", sourceText, folders, filePath, isGenerated: true);
var document = solution2.GetRequiredDocument(documentId);
AssertEx.Equal(folders, document.Folders);
Assert.Equal(filePath, document.FilePath);
Assert.True(document.State.Attributes.IsGenerated);
Assert.Equal(SourceCodeKind.Script, document.SourceCodeKind);
Assert.Throws<ArgumentNullException>("documentId", () => solution.AddDocument(documentId: null!, "name", sourceText));
Assert.Throws<ArgumentNullException>("name", () => solution.AddDocument(documentId, name: null!, sourceText));
Assert.Throws<ArgumentNullException>("text", () => solution.AddDocument(documentId, "name", text: (SourceText)null!));
Assert.Throws<InvalidOperationException>(() => solution.AddDocument(documentId: DocumentId.CreateNewId(ProjectId.CreateNewId()), "name", sourceText));
}
[Fact]
public void AddDocument_SyntaxRoot()
{
var projectId = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp).
WithProjectChecksumAlgorithm(projectId, SourceHashAlgorithms.Default).
WithProjectParseOptions(projectId, new CSharpParseOptions(kind: SourceCodeKind.Script));
var documentId = DocumentId.CreateNewId(projectId);
var filePath = Path.Combine(TempRoot.Root, "x.cs");
var folders = new[] { "folder1", "folder2" };
var root = CSharp.SyntaxFactory.ParseCompilationUnit("class C {}");
var solution2 = solution.AddDocument(documentId, "name", root, folders, filePath);
var document2 = solution2.GetRequiredDocument(documentId);
AssertEx.Equal(folders, document2.Folders);
Assert.Equal(filePath, document2.FilePath);
Assert.False(document2.State.Attributes.IsGenerated);
Assert.Equal(SourceCodeKind.Script, document2.SourceCodeKind);
Assert.Throws<ArgumentNullException>("documentId", () => solution.AddDocument(documentId: null!, "name", root));
Assert.Throws<ArgumentNullException>("name", () => solution.AddDocument(documentId, name: null!, root));
Assert.Throws<ArgumentNullException>("syntaxRoot", () => solution.AddDocument(documentId, "name", syntaxRoot: null!));
Assert.Throws<InvalidOperationException>(() => solution.AddDocument(documentId: DocumentId.CreateNewId(ProjectId.CreateNewId()), "name", syntaxRoot: root));
}
[Fact]
public void AddDocument_SyntaxRoot_ExplicitTree()
{
var projectId = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp).
WithProjectChecksumAlgorithm(projectId, SourceHashAlgorithms.Default).
WithProjectParseOptions(projectId, new CSharpParseOptions(kind: SourceCodeKind.Script));
var documentId = DocumentId.CreateNewId(projectId);
var filePath = Path.Combine(TempRoot.Root, "x.cs");
var folders = new[] { "folder1", "folder2" };
var root = CSharp.SyntaxFactory.ParseSyntaxTree(SourceText.From("class C {}", encoding: null, SourceHashAlgorithm.Sha1)).GetRoot();
Assert.Equal(SourceHashAlgorithm.Sha1, root.SyntaxTree.GetText().ChecksumAlgorithm);
var solution2 = solution.AddDocument(documentId, "name", root, folders, filePath);
var document2 = solution2.GetRequiredDocument(documentId);
var sourceText = document2.GetTextSynchronously(default);
Assert.Equal("class C {}", sourceText.ToString());
// the checksum algorithm of the tree is ignored, instead the one set on the project is used:
Assert.Equal(SourceHashAlgorithms.Default, sourceText.ChecksumAlgorithm);
AssertEx.Equal(folders, document2.Folders);
Assert.Equal(filePath, document2.FilePath);
Assert.False(document2.State.Attributes.IsGenerated);
Assert.Equal(SourceCodeKind.Script, document2.SourceCodeKind);
}
[Fact]
public void AddDocument_SyntaxRoot_SynthesizedTree()
{
var projectId = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution.AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp).
WithProjectChecksumAlgorithm(projectId, SourceHashAlgorithms.Default).
WithProjectParseOptions(projectId, new CSharpParseOptions(kind: SourceCodeKind.Script));
var documentId = DocumentId.CreateNewId(projectId);
var root = CSharp.SyntaxFactory.ParseCompilationUnit("class C {}");
Assert.Equal(SourceHashAlgorithm.Sha1, root.SyntaxTree.GetText().ChecksumAlgorithm);
var solution2 = solution.AddDocument(documentId, "name", root);
var document2 = solution2.GetRequiredDocument(documentId);
var sourceText = document2.GetTextSynchronously(default);
Assert.Equal("class C {}", sourceText.ToString());
// the checksum algorithm of the tree is ignored, instead the one set on the project is used:
Assert.Equal(SourceHashAlgorithms.Default, sourceText.ChecksumAlgorithm);
}
#nullable disable
[Fact]
public void TestAddProject()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var pid = ProjectId.CreateNewId();
solution = solution.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp);
Assert.True(solution.ProjectIds.Any(), "Solution was expected to have projects");
Assert.NotNull(pid);
var project = solution.GetProject(pid);
Assert.False(project.HasDocuments);
}
[Fact]
public void TestUpdateAssemblyName()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var project1 = ProjectId.CreateNewId();
solution = solution.AddProject(project1, "goo", "goo.dll", LanguageNames.CSharp);
solution = solution.WithProjectAssemblyName(project1, "bar");
var project = solution.GetProject(project1);
Assert.Equal("bar", project.AssemblyName);
}
[Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/543964")]
public void MultipleProjectsWithSameDisplayName()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var project1 = ProjectId.CreateNewId();
var project2 = ProjectId.CreateNewId();
solution = solution.AddProject(project1, "name", "assemblyName", LanguageNames.CSharp);
solution = solution.AddProject(project2, "name", "assemblyName", LanguageNames.CSharp);
Assert.Equal(2, solution.GetProjectsByName("name").Count());
}
[Fact]
public async Task TestAddFirstDocumentAsync()
{
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocument(did, "goo.cs", "public class Goo { }");
// verify project & document
Assert.NotNull(pid);
var project = solution.GetProject(pid);
Assert.NotNull(project);
Assert.True(solution.ContainsProject(pid), "Solution was expected to have project " + pid);
Assert.True(project.HasDocuments, "Project was expected to have documents");
Assert.Equal(project, solution.GetProject(pid));
Assert.NotNull(did);
var document = solution.GetDocument(did);
Assert.True(project.ContainsDocument(did), "Project was expected to have document " + did);
Assert.Equal(document, project.GetDocument(did));
Assert.Equal(document, solution.GetDocument(did));
var semantics = await document.GetSemanticModelAsync();
Assert.NotNull(semantics);
await ValidateSolutionAndCompilationsAsync(solution);
var pid2 = solution.Projects.Single().Id;
var did2 = DocumentId.CreateNewId(pid2);
solution = solution.AddDocument(did2, "bar.cs", "public class Bar { }");
// verify project & document
var project2 = solution.GetProject(pid2);
Assert.NotNull(project2);
Assert.NotNull(did2);
var document2 = solution.GetDocument(did2);
Assert.True(project2.ContainsDocument(did2), "Project was expected to have document " + did2);
Assert.Equal(document2, project2.GetDocument(did2));
Assert.Equal(document2, solution.GetDocument(did2));
await ValidateSolutionAndCompilationsAsync(solution);
}
[Fact]
public async Task AddTwoDocumentsForSingleProject()
{
var projectId = ProjectId.CreateNewId();
var documentInfo1 = DocumentInfo.Create(DocumentId.CreateNewId(projectId), "file1.cs");
var documentInfo2 = DocumentInfo.Create(DocumentId.CreateNewId(projectId), "file2.cs");
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(projectId, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocuments(ImmutableArray.Create(documentInfo1, documentInfo2));
var project = Assert.Single(solution.Projects);
var document1 = project.GetDocument(documentInfo1.Id);
var document2 = project.GetDocument(documentInfo2.Id);
Assert.NotSame(document1, document2);
await ValidateSolutionAndCompilationsAsync(solution);
}
[Fact]
public async Task AddTwoDocumentsForTwoProjects()
{
var projectId1 = ProjectId.CreateNewId();
var projectId2 = ProjectId.CreateNewId();
var documentInfo1 = DocumentInfo.Create(DocumentId.CreateNewId(projectId1), "file1.cs");
var documentInfo2 = DocumentInfo.Create(DocumentId.CreateNewId(projectId2), "file2.cs");
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(projectId1, "project1", "project1.dll", LanguageNames.CSharp)
.AddProject(projectId2, "project2", "project2.dll", LanguageNames.CSharp)
.AddDocuments(ImmutableArray.Create(documentInfo1, documentInfo2));
var project1 = solution.GetProject(projectId1);
var project2 = solution.GetProject(projectId2);
var document1 = project1.GetDocument(documentInfo1.Id);
var document2 = project2.GetDocument(documentInfo2.Id);
Assert.NotSame(document1, document2);
Assert.NotSame(document1.Project, document2.Project);
await ValidateSolutionAndCompilationsAsync(solution);
}
[Fact]
public void AddTwoDocumentsWithMissingProject()
{
var projectId1 = ProjectId.CreateNewId();
var projectId2 = ProjectId.CreateNewId();
var documentInfo1 = DocumentInfo.Create(DocumentId.CreateNewId(projectId1), "file1.cs");
var documentInfo2 = DocumentInfo.Create(DocumentId.CreateNewId(projectId2), "file2.cs");
// We're only adding the first project, but not the second one
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(projectId1, "project1", "project1.dll", LanguageNames.CSharp);
Assert.ThrowsAny<InvalidOperationException>(() => solution.AddDocuments(ImmutableArray.Create(documentInfo1, documentInfo2)));
}
[Fact]
public void RemoveZeroDocuments()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
Assert.Same(solution, solution.RemoveDocuments([]));
}
[Fact]
public async Task RemoveTwoDocuments()
{
var projectId = ProjectId.CreateNewId();
var documentInfo1 = DocumentInfo.Create(DocumentId.CreateNewId(projectId), "file1.cs");
var documentInfo2 = DocumentInfo.Create(DocumentId.CreateNewId(projectId), "file2.cs");
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(projectId, "project1", "project1.dll", LanguageNames.CSharp)
.AddDocuments(ImmutableArray.Create(documentInfo1, documentInfo2));
solution = solution.RemoveDocuments(ImmutableArray.Create(documentInfo1.Id, documentInfo2.Id));
var finalProject = solution.Projects.Single();
Assert.Empty(finalProject.Documents);
Assert.Empty((await finalProject.GetCompilationAsync()).SyntaxTrees);
}
[Fact]
public void RemoveTwoDocumentsFromDifferentProjects()
{
var projectId1 = ProjectId.CreateNewId();
var projectId2 = ProjectId.CreateNewId();
var documentInfo1 = DocumentInfo.Create(DocumentId.CreateNewId(projectId1), "file1.cs");
var documentInfo2 = DocumentInfo.Create(DocumentId.CreateNewId(projectId2), "file2.cs");
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(projectId1, "project1", "project1.dll", LanguageNames.CSharp)
.AddProject(projectId2, "project2", "project2.dll", LanguageNames.CSharp)
.AddDocuments(ImmutableArray.Create(documentInfo1, documentInfo2));
Assert.All(solution.Projects, p => Assert.Single(p.Documents));
solution = solution.RemoveDocuments(ImmutableArray.Create(documentInfo1.Id, documentInfo2.Id));
Assert.All(solution.Projects, p => Assert.Empty(p.Documents));
}
[Fact]
public void RemoveDocumentFromUnrelatedProject()
{
var projectId1 = ProjectId.CreateNewId();
var projectId2 = ProjectId.CreateNewId();
var documentInfo1 = DocumentInfo.Create(DocumentId.CreateNewId(projectId1), "file1.cs");
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(projectId1, "project1", "project1.dll", LanguageNames.CSharp)
.AddProject(projectId2, "project2", "project2.dll", LanguageNames.CSharp)
.AddDocument(documentInfo1);
// This should throw if we're removing one document from the wrong project. Right now we don't test the RemoveDocument
// API due to https://github.com/dotnet/roslyn/issues/41211.
Assert.Throws<ArgumentException>(() => solution.GetProject(projectId2).RemoveDocuments(ImmutableArray.Create(documentInfo1.Id)));
}
[Fact]
public void RemoveAdditionalDocumentFromUnrelatedProject()
{
var projectId1 = ProjectId.CreateNewId();
var projectId2 = ProjectId.CreateNewId();
var documentInfo1 = DocumentInfo.Create(DocumentId.CreateNewId(projectId1), "file1.txt");
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(projectId1, "project1", "project1.dll", LanguageNames.CSharp)
.AddProject(projectId2, "project2", "project2.dll", LanguageNames.CSharp)
.AddAdditionalDocument(documentInfo1);
// This should throw if we're removing one document from the wrong project. Right now we don't test the RemoveAdditionalDocument
// API due to https://github.com/dotnet/roslyn/issues/41211.
Assert.Throws<ArgumentException>(() => solution.GetProject(projectId2).RemoveAdditionalDocuments(ImmutableArray.Create(documentInfo1.Id)));
}
[Fact]
public void RemoveAnalyzerConfigDocumentFromUnrelatedProject()
{
var projectId1 = ProjectId.CreateNewId();
var projectId2 = ProjectId.CreateNewId();
var documentInfo1 = DocumentInfo.Create(DocumentId.CreateNewId(projectId1), ".editorconfig");
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(projectId1, "project1", "project1.dll", LanguageNames.CSharp)
.AddProject(projectId2, "project2", "project2.dll", LanguageNames.CSharp)
.AddAnalyzerConfigDocuments(ImmutableArray.Create(documentInfo1));
// This should throw if we're removing one document from the wrong project. Right now we don't test the RemoveAdditionalDocument
// API due to https://github.com/dotnet/roslyn/issues/41211.
Assert.Throws<ArgumentException>(() => solution.GetProject(projectId2).RemoveAnalyzerConfigDocuments(ImmutableArray.Create(documentInfo1.Id)));
}
[Fact]
public async Task TestOneCSharpProjectAsync()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject("goo", "goo.dll", LanguageNames.CSharp)
.AddMetadataReference(s_mscorlib)
.AddDocument("goo.cs", "public class Goo { }")
.Project.Solution;
await ValidateSolutionAndCompilationsAsync(solution);
}
[Fact]
public async Task TestTwoCSharpProjectsAsync()
{
using var workspace = CreateWorkspace();
var pm1 = ProjectId.CreateNewId();
var pm2 = ProjectId.CreateNewId();
var doc1 = DocumentId.CreateNewId(pm1);
var doc2 = DocumentId.CreateNewId(pm2);
var solution = workspace.CurrentSolution
.AddProject(pm1, "goo", "goo.dll", LanguageNames.CSharp)
.AddProject(pm2, "bar", "bar.dll", LanguageNames.CSharp)
.AddProjectReference(pm2, new ProjectReference(pm1))
.AddDocument(doc1, "goo.cs", "public class Goo { }")
.AddDocument(doc2, "bar.cs", "public class Bar : Goo { }");
await ValidateSolutionAndCompilationsAsync(solution);
}
[Fact]
public async Task TestCrossLanguageProjectsAsync()
{
var pm1 = ProjectId.CreateNewId();
var pm2 = ProjectId.CreateNewId();
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(pm1, "goo", "goo.dll", LanguageNames.CSharp)
.AddMetadataReference(pm1, s_mscorlib)
.AddProject(pm2, "bar", "bar.dll", LanguageNames.VisualBasic)
.AddMetadataReference(pm2, s_mscorlib)
.AddProjectReference(pm2, new ProjectReference(pm1))
.AddDocument(DocumentId.CreateNewId(pm1), "goo.cs", "public class X { }")
.AddDocument(DocumentId.CreateNewId(pm2), "bar.vb", "Public Class Y\r\nInherits X\r\nEnd Class");
await ValidateSolutionAndCompilationsAsync(solution);
}
private static async Task ValidateSolutionAndCompilationsAsync(Solution solution)
{
foreach (var project in solution.Projects)
{
Assert.True(solution.ContainsProject(project.Id), "Solution was expected to have project " + project.Id);
Assert.Equal(project, solution.GetProject(project.Id));
// these won't always be unique in real-world but should be for these tests
Assert.Equal(project, solution.GetProjectsByName(project.Name).FirstOrDefault());
var compilation = await project.GetCompilationAsync();
Assert.NotNull(compilation);
// check that the options are the same
Assert.Equal(project.CompilationOptions, compilation.Options);
// check that all known metadata references are present in the compilation
foreach (var meta in project.MetadataReferences)
{
Assert.True(compilation.References.Contains(meta), "Compilation references were expected to contain " + meta);
}
// check that all project-to-project reference metadata is present in the compilation
foreach (var referenced in project.ProjectReferences)
{
if (solution.ContainsProject(referenced.ProjectId))
{
var referencedMetadata = await solution.CompilationState.GetMetadataReferenceAsync(
referenced, solution.GetProjectState(project.Id), includeCrossLanguage: true, CancellationToken.None);
Assert.NotNull(referencedMetadata);
if (referencedMetadata is CompilationReference compilationReference)
{
compilation.References.Single(r =>
{
var cr = r as CompilationReference;
return cr != null && cr.Compilation == compilationReference.Compilation;
});
}
}
}
// check that the syntax trees are the same
var docs = project.Documents.ToList();
var trees = compilation.SyntaxTrees.ToList();
Assert.Equal(docs.Count, trees.Count);
foreach (var doc in docs)
{
Assert.True(trees.Contains(await doc.GetSyntaxTreeAsync()), "trees list was expected to contain the syntax tree of doc");
}
}
}
#if false
[Fact(Skip = "641963")]
public void TestDeepProjectReferenceTree()
{
int projectCount = 5;
var solution = CreateSolutionWithProjectDependencyChain(projectCount);
ProjectId[] projectIds = solution.ProjectIds.ToArray();
Compilation compilation;
for (int i = 0; i < projectCount; i++)
{
Assert.False(solution.GetProject(projectIds[i]).TryGetCompilation(out compilation));
}
var top = solution.GetCompilationAsync(projectIds.Last(), CancellationToken.None).Result;
var partialSolution = solution.GetPartialSolution();
for (int i = 0; i < projectCount; i++)
{
// While holding a compilation, we also hold its references, plus one further level
// of references alive. However, the references are only partial Declaration
// compilations
var isPartialAvailable = i >= projectCount - 3;
var isFinalAvailable = i == projectCount - 1;
var projectId = projectIds[i];
Assert.Equal(isFinalAvailable, solution.GetProject(projectId).TryGetCompilation(out compilation));
Assert.Equal(isPartialAvailable, partialSolution.ProjectIds.Contains(projectId) && partialSolution.GetProject(projectId).TryGetCompilation(out compilation));
}
}
#endif
[Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/636431")]
public async Task TestProjectDependencyLoadingAsync()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var projectIds = Enumerable.Range(0, 5).Select(i => ProjectId.CreateNewId()).ToArray();
for (var i = 0; i < projectIds.Length; i++)
{
solution = solution.AddProject(projectIds[i], i.ToString(), i.ToString(), LanguageNames.CSharp);
if (i >= 1)
{
solution = solution.AddProjectReference(projectIds[i], new ProjectReference(projectIds[i - 1]));
}
}
await solution.GetProject(projectIds[0]).GetCompilationAsync(CancellationToken.None);
await solution.GetProject(projectIds[2]).GetCompilationAsync(CancellationToken.None);
}
[Fact]
public async Task TestAddMetadataReferencesAsync()
{
var mefReference = NetFramework.SystemCore;
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var project1 = ProjectId.CreateNewId();
solution = solution.AddProject(project1, "goo", "goo.dll", LanguageNames.CSharp);
solution = solution.AddMetadataReference(project1, s_mscorlib);
solution = solution.AddMetadataReference(project1, mefReference);
var assemblyReference = (IAssemblySymbol)solution.GetProject(project1).GetCompilationAsync().Result.GetAssemblyOrModuleSymbol(mefReference);
var namespacesAndTypes = assemblyReference.GlobalNamespace.GetAllNamespacesAndTypes(CancellationToken.None);
var foundSymbol = from symbol in namespacesAndTypes
where symbol.Name.Equals("Enumerable")
select symbol;
Assert.Equal(1, foundSymbol.Count());
solution = solution.RemoveMetadataReference(project1, mefReference);
assemblyReference = (IAssemblySymbol)solution.GetProject(project1).GetCompilationAsync().Result.GetAssemblyOrModuleSymbol(mefReference);
Assert.Null(assemblyReference);
await ValidateSolutionAndCompilationsAsync(solution);
}
private class MockDiagnosticAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
{
get
{
throw new NotImplementedException();
}
}
public override void Initialize(AnalysisContext analysisContext)
{
}
}
[Fact]
public void TestProjectDiagnosticAnalyzers()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var project1 = ProjectId.CreateNewId();
solution = solution.AddProject(project1, "goo", "goo.dll", LanguageNames.CSharp);
Assert.Empty(solution.Projects.Single().AnalyzerReferences);
DiagnosticAnalyzer analyzer = new MockDiagnosticAnalyzer();
var analyzerReference = new AnalyzerImageReference(ImmutableArray.Create(analyzer));
// Test AddAnalyzer
var newSolution = solution.AddAnalyzerReference(project1, analyzerReference);
var actualAnalyzerReferences = newSolution.Projects.Single().AnalyzerReferences;
Assert.Equal(1, actualAnalyzerReferences.Count);
Assert.Equal(analyzerReference, actualAnalyzerReferences[0]);
var actualAnalyzers = actualAnalyzerReferences[0].GetAnalyzersForAllLanguages();
Assert.Equal(1, actualAnalyzers.Length);
Assert.Equal(analyzer, actualAnalyzers[0]);
// Test ProjectChanges
var changes = newSolution.GetChanges(solution).GetProjectChanges().Single();
var addedAnalyzerReference = changes.GetAddedAnalyzerReferences().Single();
Assert.Equal(analyzerReference, addedAnalyzerReference);
var removedAnalyzerReferences = changes.GetRemovedAnalyzerReferences();
Assert.Empty(removedAnalyzerReferences);
solution = newSolution;
// Test RemoveAnalyzer
solution = solution.RemoveAnalyzerReference(project1, analyzerReference);
actualAnalyzerReferences = solution.Projects.Single().AnalyzerReferences;
Assert.Empty(actualAnalyzerReferences);
// Test AddAnalyzers
analyzerReference = new AnalyzerImageReference(ImmutableArray.Create(analyzer));
DiagnosticAnalyzer secondAnalyzer = new MockDiagnosticAnalyzer();
var secondAnalyzerReference = new AnalyzerImageReference(ImmutableArray.Create(secondAnalyzer));
var analyzerReferences = new[] { analyzerReference, secondAnalyzerReference };
solution = solution.AddAnalyzerReferences(project1, analyzerReferences);
actualAnalyzerReferences = solution.Projects.Single().AnalyzerReferences;
Assert.Equal(2, actualAnalyzerReferences.Count);
Assert.Equal(analyzerReference, actualAnalyzerReferences[0]);
Assert.Equal(secondAnalyzerReference, actualAnalyzerReferences[1]);
solution = solution.RemoveAnalyzerReference(project1, analyzerReference);
actualAnalyzerReferences = solution.Projects.Single().AnalyzerReferences;
Assert.Equal(1, actualAnalyzerReferences.Count);
Assert.Equal(secondAnalyzerReference, actualAnalyzerReferences[0]);
// Test WithAnalyzers
solution = solution.WithProjectAnalyzerReferences(project1, analyzerReferences);
actualAnalyzerReferences = solution.Projects.Single().AnalyzerReferences;
Assert.Equal(2, actualAnalyzerReferences.Count);
Assert.Equal(analyzerReference, actualAnalyzerReferences[0]);
Assert.Equal(secondAnalyzerReference, actualAnalyzerReferences[1]);
}
[Fact]
public void TestProjectParseOptions()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var project1 = ProjectId.CreateNewId();
solution = solution.AddProject(project1, "goo", "goo.dll", LanguageNames.CSharp);
solution = solution.AddMetadataReference(project1, s_mscorlib);
// Parse Options
var oldParseOptions = solution.GetProject(project1).ParseOptions;
var newParseOptions = new CSharpParseOptions(preprocessorSymbols: ["AFTER"]);
solution = solution.WithProjectParseOptions(project1, newParseOptions);
var newUpdatedParseOptions = solution.GetProject(project1).ParseOptions;
Assert.NotEqual(oldParseOptions, newUpdatedParseOptions);
Assert.Same(newParseOptions, newUpdatedParseOptions);
}
[Fact]
public async Task TestRemoveProjectAsync()
{
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution;
var pid = ProjectId.CreateNewId();
sol = sol.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp);
Assert.True(sol.ProjectIds.Any(), "Solution was expected to have projects");
Assert.NotNull(pid);
var project = sol.GetProject(pid);
Assert.False(project.HasDocuments);
var sol2 = sol.RemoveProject(pid);
Assert.False(sol2.ProjectIds.Any());
await ValidateSolutionAndCompilationsAsync(sol);
}
[Fact]
public async Task TestRemoveProjectWithReferencesAsync()
{
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution;
var pid = ProjectId.CreateNewId();
var pid2 = ProjectId.CreateNewId();
sol = sol.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp)
.AddProject(pid2, "bar", "bar.dll", LanguageNames.CSharp)
.AddProjectReference(pid2, new ProjectReference(pid));
Assert.Equal(2, sol.Projects.Count());
// remove the project that is being referenced
// this should leave a dangling reference
var sol2 = sol.RemoveProject(pid);
Assert.False(sol2.ContainsProject(pid));
Assert.True(sol2.ContainsProject(pid2), "sol2 was expected to contain project " + pid2);
Assert.Equal(1, sol2.Projects.Count());
Assert.True(sol2.GetProject(pid2).AllProjectReferences.Any(r => r.ProjectId == pid), "sol2 project pid2 was expected to contain project reference " + pid);
await ValidateSolutionAndCompilationsAsync(sol2);
}
[Fact]
public async Task TestRemoveProjectWithReferencesAndAddItBackAsync()
{
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution;
var pid = ProjectId.CreateNewId();
var pid2 = ProjectId.CreateNewId();
sol = sol.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp)
.AddProject(pid2, "bar", "bar.dll", LanguageNames.CSharp)
.AddProjectReference(pid2, new ProjectReference(pid));
Assert.Equal(2, sol.Projects.Count());
// remove the project that is being referenced
var sol2 = sol.RemoveProject(pid);
Assert.False(sol2.ContainsProject(pid));
Assert.True(sol2.ContainsProject(pid2), "sol2 was expected to contain project " + pid2);
Assert.Equal(1, sol2.Projects.Count());
Assert.True(sol2.GetProject(pid2).AllProjectReferences.Any(r => r.ProjectId == pid), "sol2 pid2 was expected to contain " + pid);
var sol3 = sol2.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp);
Assert.True(sol3.ContainsProject(pid), "sol3 was expected to contain " + pid);
Assert.True(sol3.ContainsProject(pid2), "sol3 was expected to contain " + pid2);
Assert.Equal(2, sol3.Projects.Count());
await ValidateSolutionAndCompilationsAsync(sol3);
}
[Fact]
public async Task TestGetSyntaxRootAsync()
{
var text = "public class Goo { }";
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution
.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocument(did, "goo.cs", text);
var document = sol.GetDocument(did);
Assert.False(document.TryGetSyntaxRoot(out _));
var root = await document.GetSyntaxRootAsync();
Assert.NotNull(root);
Assert.Equal(text, root.ToString());
Assert.True(document.TryGetSyntaxRoot(out root));
Assert.NotNull(root);
}
[Fact]
public async Task TestUpdateDocumentAsync()
{
var projectId = ProjectId.CreateNewId();
var documentId = DocumentId.CreateNewId(projectId);
using var workspace = CreateWorkspace();
var solution1 = workspace.CurrentSolution
.AddProject(projectId, "ProjectName", "AssemblyName", LanguageNames.CSharp)
.AddDocument(documentId, "DocumentName", SourceText.From("class Class{}"));
var document = solution1.GetDocument(documentId);
var newRoot = await Formatter.FormatAsync(document, CSharpSyntaxFormattingOptions.Default, CancellationToken.None).Result.GetSyntaxRootAsync();
var solution2 = solution1.WithDocumentSyntaxRoot(documentId, newRoot);
Assert.NotEqual(solution1, solution2);
var newText = solution2.GetDocument(documentId).GetTextAsync().Result.ToString();
Assert.Equal("class Class { }", newText);
}
[Fact]
public void TestUpdateSyntaxTreeWithAnnotations()
{
var text = "public class Goo { }";
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution
.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocument(did, "goo.cs", text);
var document = sol.GetDocument(did);
var tree = document.GetSyntaxTreeAsync().Result;
var root = tree.GetRoot();
var annotation = new SyntaxAnnotation();
var annotatedRoot = root.WithAdditionalAnnotations(annotation);
var sol2 = sol.WithDocumentSyntaxRoot(did, annotatedRoot);
var doc2 = sol2.GetDocument(did);
var tree2 = doc2.GetSyntaxTreeAsync().Result;
var root2 = tree2.GetRoot();
// text should not be available yet (it should be defer created from the node)
// and getting the document or root should not cause it to be created.
Assert.False(tree2.TryGetText(out _));
var text2 = tree2.GetText();
Assert.NotNull(text2);
Assert.NotSame(tree, tree2);
Assert.NotSame(annotatedRoot, root2);
Assert.True(annotatedRoot.IsEquivalentTo(root2));
Assert.True(root2.HasAnnotation(annotation));
}
[Theory]
[InlineData(LanguageNames.CSharp)]
[InlineData(LanguageNames.VisualBasic)]
public async Task ParsedTreeRootOwnership(string language)
{
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
var source = (language == LanguageNames.CSharp) ? "class C {}" : "Class C : End Class";
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution
.AddProject(pid, "test", "test.dll", language)
.AddDocument(did, "test", source);
var document = sol.GetDocument(did);
// update the document syntax root:
var syntaxRoot = await document.GetSyntaxRootAsync(CancellationToken.None);
SyntaxNode newSyntaxRoot;
if (language == LanguageNames.CSharp)
{
var classNode = syntaxRoot.DescendantNodes().OfType<CS.Syntax.ClassDeclarationSyntax>().Single();
newSyntaxRoot = syntaxRoot.ReplaceNode(classNode, classNode.WithModifiers([CS.SyntaxFactory.ParseToken("public")]));
}
else
{
var classNode = syntaxRoot.DescendantNodes().OfType<VB.Syntax.ClassStatementSyntax>().Single();
newSyntaxRoot = syntaxRoot.ReplaceNode(classNode, classNode.WithModifiers(VB.SyntaxFactory.TokenList(VB.SyntaxFactory.ParseToken("Public"))));
}
var documentWithAttribute = document.WithSyntaxRoot(newSyntaxRoot);
var tree = await documentWithAttribute.GetSyntaxTreeAsync(CancellationToken.None);
var root = await documentWithAttribute.GetRequiredSyntaxRootAsync(CancellationToken.None);
Assert.Same(tree, root.SyntaxTree);
Assert.Same(tree, tree.WithRootAndOptions(root, tree.Options));
Assert.Same(tree, tree.WithFilePath(tree.FilePath));
}
[Fact]
public void TestUpdatingFilePathUpdatesSyntaxTree()
{
var projectId = ProjectId.CreateNewId();
var documentId = DocumentId.CreateNewId(projectId);
const string OldFilePath = @"Z:\OldFilePath.cs";
const string NewFilePath = @"Z:\NewFilePath.cs";
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject(projectId, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocument(documentId, "OldFilePath.cs", "public class Goo { }", filePath: OldFilePath);
// scope so later asserts don't accidentally use oldDocument
{
var oldDocument = solution.GetDocument(documentId);
Assert.Equal(OldFilePath, oldDocument.FilePath);
Assert.Equal(OldFilePath, oldDocument.GetSyntaxTreeAsync().Result.FilePath);
}
solution = solution.WithDocumentFilePath(documentId, NewFilePath);
{
var newDocument = solution.GetDocument(documentId);
Assert.Equal(NewFilePath, newDocument.FilePath);
Assert.Equal(NewFilePath, newDocument.GetSyntaxTreeAsync().Result.FilePath);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
[ConditionalFact(typeof(WindowsOnly)), WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/542736")]
public void TestDocumentChangedOnDiskIsNotObserved()
{
var text1 = "public class A {}";
var text2 = "public class B {}";
var file = Temp.CreateFile().WriteAllText(text1, Encoding.UTF8);
// create a solution that evicts from the cache immediately.
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution;
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
sol = sol.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocument(did, "x", new WorkspaceFileTextLoader(workspace.Services.SolutionServices, file.Path, Encoding.UTF8));
var observedText = GetObservedText(sol, did, text1);
// change text on disk & verify it is changed
file.WriteAllText(text2);
var textOnDisk = file.ReadAllText();
Assert.Equal(text2, textOnDisk);
// stop observing it and let GC reclaim it
observedText.AssertReleased();
// if we ask for the same text again we should get the original content
var observedText2 = sol.GetDocument(did).GetTextAsync().Result;
Assert.Equal(text1, observedText2.ToString());
}
[Fact]
public void TestGetTextAsync()
{
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
var text = "public class C {}";
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution
.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocument(did, "goo.cs", text);
var doc = sol.GetDocument(did);
var docText = doc.GetTextAsync().Result;
Assert.NotNull(docText);
Assert.Equal(text, docText.ToString());
}
[Fact]
public void TestGetLoadedTextAsync()
{
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
var text = "public class C {}";
var file = Temp.CreateFile().WriteAllText(text, Encoding.UTF8);
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution
.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocument(did, "x", new WorkspaceFileTextLoader(workspace.Services.SolutionServices, file.Path, Encoding.UTF8));
var doc = sol.GetDocument(did);
var docText = doc.GetTextAsync().Result;
Assert.NotNull(docText);
Assert.Equal(text, docText.ToString());
}
[MethodImpl(MethodImplOptions.NoInlining)]
[Fact(Skip = "https://github.com/dotnet/roslyn/issues/19427")]
public void TestGetRecoveredTextAsync()
{
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
var text = "public class C {}";
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution
.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocument(did, "goo.cs", text);
// observe the text and then wait for the references to be GC'd
var observed = GetObservedText(sol, did, text);
observed.AssertReleased();
// get it async and force it to recover from temporary storage
var doc = sol.GetDocument(did);
var docText = doc.GetTextAsync().Result;
Assert.NotNull(docText);
Assert.Equal(text, docText.ToString());
}
[Fact]
public void TestGetSyntaxTreeAsync()
{
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
var text = "public class C {}";
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution
.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocument(did, "goo.cs", text);
var doc = sol.GetDocument(did);
var docTree = doc.GetSyntaxTreeAsync().Result;
Assert.NotNull(docTree);
Assert.Equal(text, docTree.GetRoot().ToString());
}
[Fact]
public void TestGetSyntaxTreeFromLoadedTextAsync()
{
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
var text = "public class C {}";
var file = Temp.CreateFile().WriteAllText(text, Encoding.UTF8);
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution
.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocument(did, "x", new WorkspaceFileTextLoader(workspace.Services.SolutionServices, file.Path, Encoding.UTF8));
var doc = sol.GetDocument(did);
var docTree = doc.GetSyntaxTreeAsync().Result;
Assert.NotNull(docTree);
Assert.Equal(text, docTree.GetRoot().ToString());
}
[Fact]
public void TestGetSyntaxTreeFromAddedTree()
{
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
var tree = CSharp.SyntaxFactory.ParseSyntaxTree("public class C {}").GetRoot(CancellationToken.None);
tree = tree.WithAdditionalAnnotations(new SyntaxAnnotation("test"));
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution
.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocument(did, "x", tree);
var doc = sol.GetDocument(did);
var docTree = doc.GetSyntaxRootAsync().Result;
Assert.NotNull(docTree);
Assert.True(tree.IsEquivalentTo(docTree));
Assert.NotNull(docTree.GetAnnotatedNodes("test").Single());
}
[Fact]
public async Task TestGetSyntaxRootAsync2Async()
{
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
var text = "public class C {}";
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution
.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocument(did, "goo.cs", text);
var doc = sol.GetDocument(did);
var docRoot = await doc.GetSyntaxRootAsync();
Assert.NotNull(docRoot);
Assert.Equal(text, docRoot.ToString());
}
[Fact]
public void TestGetCompilationAsync()
{
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
var text = "public class C {}";
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution
.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocument(did, "goo.cs", text);
var proj = sol.GetProject(pid);
var compilation = proj.GetCompilationAsync().Result;
Assert.NotNull(compilation);
Assert.Equal(1, compilation.SyntaxTrees.Count());
}
[Fact]
public void TestGetSemanticModelAsync()
{
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
var text = "public class C {}";
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution
.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocument(did, "goo.cs", text);
var doc = sol.GetDocument(did);
var docModel = doc.GetSemanticModelAsync().Result;
Assert.NotNull(docModel);
}
[MethodImpl(MethodImplOptions.NoInlining)]
[Fact(Skip = "https://github.com/dotnet/roslyn/issues/13433")]
public void TestGetTextDoesNotKeepTextAlive()
{
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
var text = "public class C {}";
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution
.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocument(did, "goo.cs", text);
// observe the text and then wait for the references to be GC'd
var observed = GetObservedText(sol, did, text);
observed.AssertReleased();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static ObjectReference<SourceText> GetObservedText(Solution solution, DocumentId documentId, string expectedText = null)
{
var observedText = solution.GetDocument(documentId).GetTextAsync().Result;
if (expectedText != null)
{
Assert.Equal(expectedText, observedText.ToString());
}
return new ObjectReference<SourceText>(observedText);
}
[MethodImpl(MethodImplOptions.NoInlining)]
[Fact(Skip = "https://github.com/dotnet/roslyn/issues/13433")]
public void TestGetTextAsyncDoesNotKeepTextAlive()
{
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
var text = "public class C {}";
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution
.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocument(did, "goo.cs", text);
// observe the text and then wait for the references to be GC'd
var observed = GetObservedTextAsync(sol, did, text);
observed.AssertReleased();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static ObjectReference<SourceText> GetObservedTextAsync(Solution solution, DocumentId documentId, string expectedText = null)
{
var observedText = solution.GetDocument(documentId).GetTextAsync().Result;
if (expectedText != null)
{
Assert.Equal(expectedText, observedText.ToString());
}
return new ObjectReference<SourceText>(observedText);
}
[MethodImpl(MethodImplOptions.NoInlining)]
[Fact(Skip = "https://github.com/dotnet/roslyn/issues/13433")]
public void TestGetSyntaxRootDoesNotKeepRootAlive()
{
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
var text = "public class C {}";
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution
.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocument(did, "goo.cs", text);
// get it async and wait for it to get GC'd
var observed = GetObservedSyntaxTreeRoot(sol, did);
observed.AssertReleased();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static ObjectReference<SyntaxNode> GetObservedSyntaxTreeRoot(Solution solution, DocumentId documentId)
{
var observedTree = solution.GetDocument(documentId).GetSyntaxRootAsync().Result;
return new ObjectReference<SyntaxNode>(observedTree);
}
[MethodImpl(MethodImplOptions.NoInlining)]
[Fact(Skip = "https://github.com/dotnet/roslyn/issues/13433")]
public void TestGetSyntaxRootAsyncDoesNotKeepRootAlive()
{
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
var text = "public class C {}";
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution
.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocument(did, "goo.cs", text);
// get it async and wait for it to get GC'd
var observed = GetObservedSyntaxTreeRootAsync(sol, did);
observed.AssertReleased();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static ObjectReference<SyntaxNode> GetObservedSyntaxTreeRootAsync(Solution solution, DocumentId documentId)
{
var observedTree = solution.GetDocument(documentId).GetSyntaxRootAsync().Result;
return new ObjectReference<SyntaxNode>(observedTree);
}
[MethodImpl(MethodImplOptions.NoInlining)]
[Fact(Skip = "https://github.com/dotnet/roslyn/issues/13506")]
[WorkItem("https://github.com/dotnet/roslyn/issues/13506")]
public void TestRecoverableSyntaxTreeCSharp()
{
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
var text = @"public class C {
public void Method1() {}
public void Method2() {}
public void Method3() {}
public void Method4() {}
public void Method5() {}
public void Method6() {}
}";
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution
.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocument(did, "goo.cs", text);
TestRecoverableSyntaxTree(sol, did);
}
[MethodImpl(MethodImplOptions.NoInlining)]
[Fact(Skip = "https://github.com/dotnet/roslyn/issues/13433")]
public void TestRecoverableSyntaxTreeVisualBasic()
{
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
var text = @"Public Class C
Sub Method1()
End Sub
Sub Method2()
End Sub
Sub Method3()
End Sub
Sub Method4()
End Sub
Sub Method5()
End Sub
Sub Method6()
End Sub
End Class";
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution
.AddProject(pid, "goo", "goo.dll", LanguageNames.VisualBasic)
.AddDocument(did, "goo.vb", text);
TestRecoverableSyntaxTree(sol, did);
}
private static void TestRecoverableSyntaxTree(Solution sol, DocumentId did)
{
// get it async and wait for it to get GC'd
var observed = GetObservedSyntaxTreeRootAsync(sol, did);
observed.AssertReleased();
var doc = sol.GetDocument(did);
// access the tree & root again (recover it)
var tree = doc.GetSyntaxTreeAsync().Result;
// this should cause reparsing
var root = tree.GetRoot();
// prove that the new root is correctly associated with the tree
Assert.Equal(tree, root.SyntaxTree);
// reset the syntax root, to make it 'refactored' by adding an attribute
var newRoot = doc.GetSyntaxRootAsync().Result.WithAdditionalAnnotations(SyntaxAnnotation.ElasticAnnotation);
var doc2 = doc.Project.Solution.WithDocumentSyntaxRoot(doc.Id, newRoot, PreservationMode.PreserveValue).GetDocument(doc.Id);
// get it async and wait for it to get GC'd
var observed2 = GetObservedSyntaxTreeRootAsync(doc2.Project.Solution, did);
observed2.AssertReleased();
// access the tree & root again (recover it)
var tree2 = doc2.GetSyntaxTreeAsync().Result;
// this should cause deserialization
var root2 = tree2.GetRoot();
// prove that the new root is correctly associated with the tree
Assert.Equal(tree2, root2.SyntaxTree);
}
[MethodImpl(MethodImplOptions.NoInlining)]
[Fact(Skip = "https://github.com/dotnet/roslyn/issues/13433")]
public void TestGetCompilationAsyncDoesNotKeepCompilationAlive()
{
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
var text = "public class C {}";
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution
.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocument(did, "goo.cs", text);
// get it async and wait for it to get GC'd
var observed = GetObservedCompilationAsync(sol, pid);
observed.AssertReleased();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static ObjectReference<Compilation> GetObservedCompilationAsync(Solution solution, ProjectId projectId)
{
var observed = solution.GetProject(projectId).GetCompilationAsync().Result;
return new ObjectReference<Compilation>(observed);
}
[MethodImpl(MethodImplOptions.NoInlining)]
[Fact(Skip = "https://github.com/dotnet/roslyn/issues/13433")]
public void TestGetCompilationDoesNotKeepCompilationAlive()
{
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
var text = "public class C {}";
using var workspace = CreateWorkspace();
var sol = workspace.CurrentSolution
.AddProject(pid, "goo", "goo.dll", LanguageNames.CSharp)
.AddDocument(did, "goo.cs", text);
// get it async and wait for it to get GC'd
var observed = GetObservedCompilation(sol, pid);
observed.AssertReleased();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static ObjectReference<Compilation> GetObservedCompilation(Solution solution, ProjectId projectId)
{
var observed = solution.GetProject(projectId).GetCompilationAsync().Result;
return new ObjectReference<Compilation>(observed);
}
[Theory]
[InlineData(LanguageNames.CSharp)]
[InlineData(LanguageNames.VisualBasic)]
[WorkItem("https://github.com/dotnet/roslyn/issues/63834")]
public void RecoverableTree_With(string language)
{
using var workspace = CreateWorkspace();
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
var sol = workspace.CurrentSolution
.AddProject(pid, "test", "test.dll", language)
.AddDocument(did, "test", SourceText.From(language == LanguageNames.CSharp ? "class C {}" : "Class C : End Class", Encoding.UTF8, SourceHashAlgorithm.Sha256), filePath: "old path");
var document = sol.GetDocument(did);
var tree = document.GetSyntaxTreeSynchronously(default);
//var recoverableTree = Assert.IsAssignableFrom<IRecoverableSyntaxTree>(tree);
var tree2 = tree.WithFilePath("new path");
//var recoverableTree2 = Assert.IsAssignableFrom<IRecoverableSyntaxTree>(tree2);
Assert.Equal("new path", tree2.FilePath);
Assert.Same(tree2, tree2.GetRoot().SyntaxTree);
Assert.Same(tree.Options, tree2.Options);
Assert.Same(tree.Encoding, tree2.Encoding);
Assert.Equal(tree.Length, tree2.Length);
//Assert.Equal(recoverableTree.ContainsDirectives, recoverableTree2.ContainsDirectives);
// unchanged:
Assert.Same(tree, tree.WithFilePath("old path"));
var newRoot = (language == LanguageNames.CSharp) ? CS.SyntaxFactory.ParseCompilationUnit("""
#define X
#if X
class NewType {}
#endif
""") : (SyntaxNode)VB.SyntaxFactory.ParseCompilationUnit("""
#Define X
#If X
Class C
End Class
#End If
""");
Assert.True(newRoot.ContainsDirectives);
var tree3 = tree.WithRootAndOptions(newRoot, tree.Options);
//var recoverableTree3 = Assert.IsAssignableFrom<IRecoverableSyntaxTree>(tree3);
Assert.Equal("old path", tree3.FilePath);
Assert.Same(tree3, tree3.GetRoot().SyntaxTree);
Assert.Same(tree.Options, tree3.Options);
Assert.Same(tree.Encoding, tree3.Encoding);
Assert.Equal(newRoot.FullSpan.Length, tree3.Length);
//Assert.True(recoverableTree3.ContainsDirectives);
var newOptions = tree.Options.WithKind(SourceCodeKind.Script);
var tree4 = tree.WithRootAndOptions(tree.GetRoot(), newOptions);
//var recoverableTree4 = Assert.IsAssignableFrom<IRecoverableSyntaxTree>(tree4);
Assert.Equal("old path", tree4.FilePath);
Assert.Same(tree4, tree4.GetRoot().SyntaxTree);
Assert.Same(newOptions, tree4.Options);
Assert.Same(tree.Encoding, tree4.Encoding);
Assert.Equal(tree.Length, tree4.Length);
//Assert.Equal(recoverableTree.ContainsDirectives, recoverableTree4.ContainsDirectives);
// unchanged:
Assert.Same(tree, tree.WithRootAndOptions(tree.GetRoot(), tree.Options));
}
[Fact]
public void TestWorkspaceLanguageServiceOverride()
{
var hostServices = FeaturesTestCompositions.Features.AddParts(
[
typeof(TestLanguageServiceA),
typeof(TestLanguageServiceB),
]).GetHostServices();
var ws = new AdhocWorkspace(hostServices, ServiceLayer.Host);
var service = ws.Services.GetLanguageServices(LanguageNames.CSharp).GetService<ITestLanguageService>();
Assert.NotNull(service as TestLanguageServiceA);
var ws2 = new AdhocWorkspace(hostServices, "Quasimodo");
var service2 = ws2.Services.GetLanguageServices(LanguageNames.CSharp).GetService<ITestLanguageService>();
Assert.NotNull(service2 as TestLanguageServiceB);
}
#if false
[Fact]
public void TestSolutionInfo()
{
var oldSolutionId = SolutionId.CreateNewId("oldId");
var oldVersion = VersionStamp.Create();
var solutionInfo = SolutionInfo.Create(oldSolutionId, oldVersion, null, null);
var newSolutionId = SolutionId.CreateNewId("newId");
solutionInfo = solutionInfo.WithId(newSolutionId);
Assert.NotEqual(oldSolutionId, solutionInfo.Id);
Assert.Equal(newSolutionId, solutionInfo.Id);
var newVersion = oldVersion.GetNewerVersion();
solutionInfo = solutionInfo.WithVersion(newVersion);
Assert.NotEqual(oldVersion, solutionInfo.Version);
Assert.Equal(newVersion, solutionInfo.Version);
Assert.Null(solutionInfo.FilePath);
var newFilePath = @"C:\test\fake.sln";
solutionInfo = solutionInfo.WithFilePath(newFilePath);
Assert.Equal(newFilePath, solutionInfo.FilePath);
Assert.Equal(0, solutionInfo.Projects.Count());
}
#endif
private interface ITestLanguageService : ILanguageService
{
}
[ExportLanguageService(typeof(ITestLanguageService), LanguageNames.CSharp, ServiceLayer.Default), Shared, PartNotDiscoverable]
private class TestLanguageServiceA : ITestLanguageService
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public TestLanguageServiceA()
{
}
}
[ExportLanguageService(typeof(ITestLanguageService), LanguageNames.CSharp, "Quasimodo"), Shared, PartNotDiscoverable]
private class TestLanguageServiceB : ITestLanguageService
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public TestLanguageServiceB()
{
}
}
[Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/666263")]
public async Task TestDocumentFileAccessFailureMissingFile()
{
var workspace = new AdhocWorkspace();
var solution = workspace.CurrentSolution;
var pid = ProjectId.CreateNewId();
var did = DocumentId.CreateNewId(pid);
solution = solution
.AddProject(pid, "goo", "goo", LanguageNames.CSharp)
.AddDocument(did, "x", new WorkspaceFileTextLoader(solution.Services, @"C:\doesnotexist.cs", Encoding.UTF8))
.WithDocumentFilePath(did, "document path");
var doc = solution.GetDocument(did);
var text = await doc.GetTextAsync().ConfigureAwait(false);
var diagnostic = await doc.State.GetLoadDiagnosticAsync(CancellationToken.None).ConfigureAwait(false);
Assert.Equal(@"C:\doesnotexist.cs: (0,0)-(0,0)", diagnostic.Location.GetLineSpan().ToString());
Assert.Equal("", text.ToString());
// Verify invariant: The compilation is guaranteed to have a syntax tree for each document of the project (even if the contnet fails to load).
var compilation = await doc.Project.GetCompilationAsync(CancellationToken.None).ConfigureAwait(false);
var syntaxTree = compilation.SyntaxTrees.Single();
Assert.Equal("", syntaxTree.ToString());
}
[Fact]
public void TestGetProjectForAssemblySymbol()
{
var pid1 = ProjectId.CreateNewId("p1");
var pid2 = ProjectId.CreateNewId("p2");
var pid3 = ProjectId.CreateNewId("p3");
var did1 = DocumentId.CreateNewId(pid1);
var did2 = DocumentId.CreateNewId(pid2);
var did3 = DocumentId.CreateNewId(pid3);
var text1 = @"
Public Class A
End Class";
var text2 = @"
Public Class B
End Class
";
var text3 = @"
public class C : B {
}
";
var text4 = @"
public class C : A {
}
";
var solution = new AdhocWorkspace().CurrentSolution
.AddProject(pid1, "GooA", "Goo.dll", LanguageNames.VisualBasic)
.AddDocument(did1, "A.vb", text1)
.AddMetadataReference(pid1, s_mscorlib)
.AddProject(pid2, "GooB", "Goo2.dll", LanguageNames.VisualBasic)
.AddDocument(did2, "B.vb", text2)
.AddMetadataReference(pid2, s_mscorlib)
.AddProject(pid3, "Bar", "Bar.dll", LanguageNames.CSharp)
.AddDocument(did3, "C.cs", text3)
.AddMetadataReference(pid3, s_mscorlib)
.AddProjectReference(pid3, new ProjectReference(pid1))
.AddProjectReference(pid3, new ProjectReference(pid2));
var project3 = solution.GetProject(pid3);
var comp3 = project3.GetCompilationAsync().Result;
var classC = comp3.GetTypeByMetadataName("C");
var projectForBaseType = solution.GetProject(classC.BaseType.ContainingAssembly);
Assert.Equal(pid2, projectForBaseType.Id);
// switch base type to A then try again
var solution2 = solution.WithDocumentText(did3, SourceText.From(text4));
project3 = solution2.GetProject(pid3);
comp3 = project3.GetCompilationAsync().Result;
classC = comp3.GetTypeByMetadataName("C");
projectForBaseType = solution2.GetProject(classC.BaseType.ContainingAssembly);
Assert.Equal(pid1, projectForBaseType.Id);
}
[Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/1088127")]
public void TestEncodingRetainedAfterTreeChanged()
{
var ws = new AdhocWorkspace();
var proj = ws.AddProject("proj", LanguageNames.CSharp);
var doc = ws.AddDocument(proj.Id, "a.cs", SourceText.From("public class c { }", Encoding.UTF32));
Assert.Equal(Encoding.UTF32, doc.GetTextAsync().Result.Encoding);
// updating root doesn't change original encoding
var root = doc.GetSyntaxRootAsync().Result;
var newRoot = root.WithLeadingTrivia(root.GetLeadingTrivia().Add(CS.SyntaxFactory.Whitespace(" ")));
var newDoc = doc.WithSyntaxRoot(newRoot);
Assert.Equal(Encoding.UTF32, newDoc.GetTextAsync().Result.Encoding);
}
[Fact]
public async Task TestProjectWithNoMetadataReferencesHasIncompleteReferences()
{
var workspace = new AdhocWorkspace();
var project = workspace.AddProject("CSharpProject", LanguageNames.CSharp);
Assert.False(await project.HasSuccessfullyLoadedAsync(CancellationToken.None));
Assert.Empty(project.GetCompilationAsync().Result.ExternalReferences);
}
[Fact]
public async Task TestProjectWithNoBrokenReferencesHasNoIncompleteReferences()
{
var workspace = new AdhocWorkspace();
var project1 = workspace.AddProject(
ProjectInfo.Create(
ProjectId.CreateNewId(),
VersionStamp.Create(),
"CSharpProject",
"CSharpProject",
LanguageNames.CSharp,
metadataReferences: [MscorlibRef]));
var project2 = workspace.AddProject(
ProjectInfo.Create(
ProjectId.CreateNewId(),
VersionStamp.Create(),
"VisualBasicProject",
"VisualBasicProject",
LanguageNames.VisualBasic,
metadataReferences: [MscorlibRef],
projectReferences: [new ProjectReference(project1.Id)]));
// Nothing should have incomplete references, and everything should build
Assert.True(await project1.HasSuccessfullyLoadedAsync(CancellationToken.None));
Assert.True(await project2.HasSuccessfullyLoadedAsync(CancellationToken.None));
Assert.Equal(2, project2.GetCompilationAsync().Result.ExternalReferences.Length);
}
[Fact]
public async Task TestProjectWithBrokenCrossLanguageReferenceHasIncompleteReferences()
{
var workspace = new AdhocWorkspace();
var project1 = workspace.AddProject(
ProjectInfo.Create(
ProjectId.CreateNewId(),
VersionStamp.Create(),
"CSharpProject",
"CSharpProject",
LanguageNames.CSharp,
metadataReferences: [MscorlibRef]));
workspace.AddDocument(project1.Id, "Broken.cs", SourceText.From("class "));
var project2 = workspace.AddProject(
ProjectInfo.Create(
ProjectId.CreateNewId(),
VersionStamp.Create(),
"VisualBasicProject",
"VisualBasicProject",
LanguageNames.VisualBasic,
metadataReferences: [MscorlibRef],
projectReferences: [new ProjectReference(project1.Id)]));
Assert.True(await project1.HasSuccessfullyLoadedAsync(CancellationToken.None));
Assert.False(await project2.HasSuccessfullyLoadedAsync(CancellationToken.None));
Assert.Single(project2.GetCompilationAsync().Result.ExternalReferences);
}
[Fact]
public async Task TestFrozenPartialProjectHasDifferentSemanticVersions_AddedDoc()
{
using var workspace = WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics();
var project = workspace.CurrentSolution.AddProject("CSharpProject", "CSharpProject", LanguageNames.CSharp);
project = project.AddDocument("Extra.cs", SourceText.From("class Extra { }")).Project;
var documentToFreeze = project.AddDocument("DocumentToFreeze.cs", SourceText.From(""));
var frozenDocument = documentToFreeze.WithFrozenPartialSemantics(CancellationToken.None);
// Because we had no compilation produced yet, we expect that only the DocumentToFreeze is in the compilation
Assert.NotSame(frozenDocument, documentToFreeze);
var tree = Assert.Single((await frozenDocument.Project.GetCompilationAsync()).SyntaxTrees);
Assert.Equal("DocumentToFreeze.cs", tree.FilePath);
// Versions should be different
Assert.NotEqual(
await documentToFreeze.Project.GetDependentSemanticVersionAsync(),
await frozenDocument.Project.GetDependentSemanticVersionAsync());
Assert.NotEqual(
await documentToFreeze.Project.GetSemanticVersionAsync(),
await frozenDocument.Project.GetSemanticVersionAsync());
}
[Fact]
public async Task TestFreezingTwiceGivesSameDocument()
{
using var workspace = WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics();
var project = workspace.CurrentSolution.AddProject("CSharpProject", "CSharpProject", LanguageNames.CSharp);
project = project.AddDocument("Extra.cs", SourceText.From("class Extra { }")).Project;
var documentToFreeze = project.AddDocument("DocumentToFreeze.cs", SourceText.From(""));
var frozenDocument = documentToFreeze.WithFrozenPartialSemantics(CancellationToken.None);
// Because we had no compilation produced yet, we expect that only the DocumentToFreeze is in the compilation
Assert.NotSame(frozenDocument, documentToFreeze);
var tree = Assert.Single((await frozenDocument.Project.GetCompilationAsync()).SyntaxTrees);
Assert.Equal("DocumentToFreeze.cs", tree.FilePath);
// Versions should be different
Assert.NotEqual(
await documentToFreeze.Project.GetDependentSemanticVersionAsync(),
await frozenDocument.Project.GetDependentSemanticVersionAsync());
Assert.NotEqual(
await documentToFreeze.Project.GetSemanticVersionAsync(),
await frozenDocument.Project.GetSemanticVersionAsync());
var frozenDocument2 = frozenDocument.WithFrozenPartialSemantics(CancellationToken.None);
Assert.Same(frozenDocument, frozenDocument2);
}
[Fact]
public async Task TestFrozenPartialProjectHasDifferentSemanticVersions_ChangedDoc1()
{
using var workspace = WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics();
var project = workspace.CurrentSolution.AddProject("CSharpProject", "CSharpProject", LanguageNames.CSharp);
project = project.AddDocument("Extra.cs", SourceText.From("class Extra { }")).Project;
var documentToFreezeOriginal = project.AddDocument("DocumentToFreeze.cs", SourceText.From("class DocumentToFreeze { void M() { } }"));
project = documentToFreezeOriginal.Project;
var compilation = await project.GetCompilationAsync();
var solution = project.Solution.WithDocumentText(documentToFreezeOriginal.Id, SourceText.From("class DocumentToFreeze { void M() { /*no top level change*/ } }"));
var documentToFreezeChanged = solution.GetDocument(documentToFreezeOriginal.Id);
var tree = await documentToFreezeChanged.GetSyntaxTreeAsync();
var frozenDocument = documentToFreezeChanged.WithFrozenPartialSemantics(CancellationToken.None);
Assert.NotSame(frozenDocument, documentToFreezeChanged);
// Versions should the same since there wasn't a top level change different
Assert.Equal(
await documentToFreezeOriginal.GetTopLevelChangeTextVersionAsync(),
await frozenDocument.GetTopLevelChangeTextVersionAsync());
Assert.Equal(
await documentToFreezeChanged.GetTopLevelChangeTextVersionAsync(),
await frozenDocument.GetTopLevelChangeTextVersionAsync());
Assert.Equal(
await documentToFreezeOriginal.Project.GetDependentSemanticVersionAsync(),
await frozenDocument.Project.GetDependentSemanticVersionAsync());
Assert.Equal(
await documentToFreezeChanged.Project.GetDependentSemanticVersionAsync(),
await frozenDocument.Project.GetDependentSemanticVersionAsync());
Assert.Equal(
await documentToFreezeOriginal.Project.GetSemanticVersionAsync(),
await frozenDocument.Project.GetSemanticVersionAsync());
Assert.Equal(
await documentToFreezeChanged.Project.GetSemanticVersionAsync(),
await frozenDocument.Project.GetSemanticVersionAsync());
}
[Fact]
public async Task TestFrozenPartialProjectHasDifferentSemanticVersions_ChangedDoc2()
{
using var workspace = WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics();
var project = workspace.CurrentSolution.AddProject("CSharpProject", "CSharpProject", LanguageNames.CSharp);
project = project.AddDocument("Extra.cs", SourceText.From("class Extra { }")).Project;
var documentToFreezeOriginal = project.AddDocument("DocumentToFreeze.cs", SourceText.From("class DocumentToFreeze { void M() { } }"));
project = documentToFreezeOriginal.Project;
var compilation = await project.GetCompilationAsync();
var solution = project.Solution.WithDocumentText(documentToFreezeOriginal.Id, SourceText.From("class DocumentToFreeze { void M() { } public void NewMethod() { } }"));
var documentToFreezeChanged = solution.GetDocument(documentToFreezeOriginal.Id);
var tree = await documentToFreezeChanged.GetSyntaxTreeAsync();
var frozenDocument = documentToFreezeChanged.WithFrozenPartialSemantics(CancellationToken.None);
Assert.NotSame(frozenDocument, documentToFreezeChanged);
// Before/after the change must always result in a top level change.
// After the change, we should get the same version between the doc and its frozen version.
Assert.NotEqual(
await documentToFreezeOriginal.GetTopLevelChangeTextVersionAsync(),
await frozenDocument.GetTopLevelChangeTextVersionAsync());
Assert.Equal(
await documentToFreezeChanged.GetTopLevelChangeTextVersionAsync(),
await frozenDocument.GetTopLevelChangeTextVersionAsync());
Assert.NotEqual(
await documentToFreezeOriginal.Project.GetDependentSemanticVersionAsync(),
await frozenDocument.Project.GetDependentSemanticVersionAsync());
Assert.Equal(
await documentToFreezeChanged.Project.GetDependentSemanticVersionAsync(),
await frozenDocument.Project.GetDependentSemanticVersionAsync());
Assert.NotEqual(
await documentToFreezeOriginal.Project.GetSemanticVersionAsync(),
await frozenDocument.Project.GetSemanticVersionAsync());
Assert.Equal(
await documentToFreezeChanged.Project.GetSemanticVersionAsync(),
await frozenDocument.Project.GetSemanticVersionAsync());
}
[Fact]
public async Task TestFrozenPartialProjectAlwaysIsIncomplete()
{
var workspace = new AdhocWorkspace();
var project1 = workspace.AddProject(
ProjectInfo.Create(
ProjectId.CreateNewId(),
VersionStamp.Create(),
"CSharpProject",
"CSharpProject",
LanguageNames.CSharp,
metadataReferences: [MscorlibRef]));
var project2 = workspace.AddProject(
ProjectInfo.Create(
ProjectId.CreateNewId(),
VersionStamp.Create(),
"VisualBasicProject",
"VisualBasicProject",
LanguageNames.VisualBasic,
metadataReferences: [MscorlibRef],
projectReferences: [new ProjectReference(project1.Id)]));
var document = workspace.AddDocument(project2.Id, "Test.cs", SourceText.From(""));
// Nothing should have incomplete references, and everything should build
var frozenSolution = document.WithFrozenPartialSemantics(CancellationToken.None).Project.Solution;
Assert.True(await frozenSolution.GetProject(project1.Id).HasSuccessfullyLoadedAsync(CancellationToken.None));
Assert.True(await frozenSolution.GetProject(project2.Id).HasSuccessfullyLoadedAsync(CancellationToken.None));
}
[Fact]
public async Task TestFrozenPartialSemanticsProjectDoesNotHaveAdditionalDocumentsFromInProgressChange()
{
using var workspace = CreateWorkspaceWithPartialSemantics();
var project = workspace.CurrentSolution.AddProject("TestProject", "TestProject", LanguageNames.CSharp)
.AddDocument("RegularDocument.cs", "// Source File", filePath: "RegularDocument.cs").Project;
// Fetch the compilation to ensure further changes produce in progress states
var originalCompilation = await project.GetCompilationAsync();
project = project.AddAdditionalDocument("Test.txt", "").Project;
// Freeze semantics -- this should give us a compilation and state that don't include the additional file,
// since the compilation won't represent that either
var frozenDocument = project.Documents.Single().WithFrozenPartialSemantics(CancellationToken.None);
Assert.Empty(frozenDocument.Project.AdditionalDocuments);
}
[Fact]
public async Task TestFrozenPartialSemanticsNoCompilationYetBuilt()
{
using var workspace = CreateWorkspaceWithPartialSemantics();
var project = workspace.CurrentSolution.AddProject("TestProject", "TestProject", LanguageNames.CSharp)
.AddDocument("RegularDocument.cs", "// Source File", filePath: "RegularDocument.cs").Project
.AddDocument("RegularDocument2.cs", "// Source File", filePath: "RegularDocument2.cs").Project;
// Freeze semantics -- that document should be there, but nothing else will be yet.
var frozenDocument = project.Documents.First().WithFrozenPartialSemantics(CancellationToken.None);
Assert.Single(frozenDocument.Project.Documents);
var singleTree = Assert.Single((await frozenDocument.Project.GetCompilationAsync()).SyntaxTrees);
Assert.Same(await frozenDocument.GetSyntaxTreeAsync(), singleTree);
}
[Fact, WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1467404")]
public async Task TestFrozenPartialSemanticsHandlesDocumentWithSamePathBeingRemovedAndAdded()
{
using var workspace = CreateWorkspaceWithPartialSemantics();
var originalProject = workspace.CurrentSolution.AddProject("TestProject", "TestProject", LanguageNames.CSharp)
.AddDocument("RegularDocument.cs", "// Source File", filePath: "RegularDocument.cs").Project;
// Fetch the compilation to ensure further changes produce in progress states
var originalCompilation = await originalProject.GetCompilationAsync();
var forkedProject = originalProject.RemoveDocument(originalProject.DocumentIds.Single())
.AddDocument("RegularDocument.cs", "// Source File", filePath: "RegularDocument.cs").Project;
var frozenDocument = forkedProject.Documents.Single().WithFrozenPartialSemantics(CancellationToken.None);
// There will be two documents. That's because freezing the solution ends up jumping back to hte point in
// time before the remove/add happened (so the original doc is there). Then, the new doc is added as a
// sibling. That they have the same path is not relevant. They have different IDs and thus are considered
// different.
Assert.Equal(2, frozenDocument.Project.Documents.Count());
var frozenCompilation = await frozenDocument.Project.GetCompilationAsync();
Assert.True(frozenCompilation.ContainsSyntaxTree(await frozenDocument.GetSyntaxTreeAsync()));
Assert.True(frozenCompilation.ContainsSyntaxTree(await originalProject.Documents.Single().GetSyntaxTreeAsync()));
}
[Fact, WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1467404")]
public async Task TestFrozenPartialSemanticsHandlesRemoveAndAddWithNullPathAndDifferentNames()
{
using var workspace = CreateWorkspaceWithPartialSemantics();
var originalProject = workspace.CurrentSolution.AddProject("TestProject", "TestProject", LanguageNames.CSharp)
.AddDocument("RegularDocument.cs", "// Source File", filePath: null).Project;
// Fetch the compilation to ensure further changes produce in progress states
var originalCompilation = await originalProject.GetCompilationAsync();
var forkedProject = originalProject.RemoveDocument(originalProject.DocumentIds.Single())
.AddDocument("RegularDocument2.cs", "// Source File", filePath: null).Project;
var frozenDocument = forkedProject.Documents.Single().WithFrozenPartialSemantics(CancellationToken.None);
// There will be two documents. That's because freezing the solution ends up jumping back to hte point in
// time before the remove/add happened (so the original doc is there). Then, the new doc is added as a
// sibling. That they have the same path is not relevant. They have different IDs and thus are considered
// different.
// There will be two documents. That's because freezing the solution ends up jumping back to hte point in
// time before the remove/add happened (so the original doc is there). Then, the new doc is added as a
// sibling. That they have the same path is not relevant. They have different IDs and thus are considered
// different.
Assert.Equal(2, frozenDocument.Project.Documents.Count());
var frozenCompilation = await frozenDocument.Project.GetCompilationAsync();
Assert.True(frozenCompilation.ContainsSyntaxTree(await frozenDocument.GetSyntaxTreeAsync()));
Assert.True(frozenCompilation.ContainsSyntaxTree(await originalProject.Documents.Single().GetSyntaxTreeAsync()));
}
[Fact]
public async Task TestFrozenPartialSemanticsAfterSingleTextEdit()
{
using var workspace = CreateWorkspaceWithPartialSemantics();
var document = workspace.CurrentSolution.AddProject("TestProject", "TestProject", LanguageNames.CSharp)
.AddDocument("RegularDocument.cs", "// Source File", filePath: null);
// Fetch the compilation to ensure further changes produce in progress states
var originalCompilation = await document.Project.GetCompilationAsync();
document = document.WithText(SourceText.From("// Source File with Changes"));
var frozenDocument = document.WithFrozenPartialSemantics(CancellationToken.None);
Assert.Contains(await frozenDocument.GetSyntaxTreeAsync(), (await frozenDocument.Project.GetCompilationAsync()).SyntaxTrees);
}
[Theory, CombinatorialData]
public async Task TestFrozenPartialSemanticsWithMulitipleUnrelatedEdits([CombinatorialValues(1, 2, 3)] int documentToFreeze)
{
using var workspace = CreateWorkspaceWithPartialSemantics();
var solution = workspace.CurrentSolution.AddProject("TestProject", "TestProject", LanguageNames.CSharp).Solution;
var documentId1 = DocumentId.CreateNewId(solution.ProjectIds.Single());
var documentId2 = DocumentId.CreateNewId(solution.ProjectIds.Single());
var documentId3 = DocumentId.CreateNewId(solution.ProjectIds.Single());
solution = solution
.AddDocument(documentId1, nameof(documentId1), "// Document 1")
.AddDocument(documentId2, nameof(documentId2), "// Document 2")
.AddDocument(documentId3, nameof(documentId3), "// Document 3");
// Fetch the compilation to ensure further changes produce in progress states
var originalCompilation = await solution.Projects.Single().GetCompilationAsync();
solution = solution
.WithDocumentText(documentId1, SourceText.From("// Document 1 Changed"))
.WithDocumentText(documentId2, SourceText.From("// Document 2 Changed"))
.WithDocumentText(documentId3, SourceText.From("// Document 3 Changed"));
// We will freeze the appropriate document -- the reason we have three here is the code path might work accidentally if it
// was the first or last change made, so covering "first", "middle" and "last" ensures that's covered.
var documentIdToFreeze = documentToFreeze == 1 ? documentId1 : documentToFreeze == 2 ? documentId2 : documentId3;
var frozen = solution.GetRequiredDocument(documentIdToFreeze).WithFrozenPartialSemantics(CancellationToken.None);
var tree = await frozen.GetSyntaxTreeAsync();
Assert.Contains("Changed", tree.ToString());
Assert.Contains(tree, (await frozen.Project.GetCompilationAsync()).SyntaxTrees);
}
[Fact]
public async Task TestFrozenPartialSemanticsOfLinkedDocuments()
{
const int ClassDeclaration = 8855;
const int StructDeclaration = 8856;
using var workspace = CreateWorkspaceWithPartialSemantics();
var contents = """
#if X
class C
{
}
#else
struct D
{
}
#endif
""";
// Create a normal solution with a linked doc, where each project should see a different tree for the above contents.
var currentSolution = workspace.CurrentSolution;
var document1 = currentSolution.AddProject("TestProject1", "TestProject1", LanguageNames.CSharp)
.AddDocument("RegularDocument.cs", contents, filePath: "RegularDocument.cs");
currentSolution = document1.Project.Solution;
var document2 = currentSolution.AddProject("TestProject2", "TestProject2", LanguageNames.CSharp)
.AddDocument("RegularDocument.cs", contents, filePath: "RegularDocument.cs");
currentSolution = document2.Project.Solution;
var options = (CSharpParseOptions)document1.Project.ParseOptions;
currentSolution = currentSolution.WithProjectParseOptions(document1.Project.Id, options.WithPreprocessorSymbols("X"));
var relatedIds1 = currentSolution.GetRelatedDocumentIds(document1.Id);
var relatedIds2 = currentSolution.GetRelatedDocumentIds(document2.Id);
AssertEx.SetEqual(relatedIds1, ImmutableArray.Create(document1.Id, document2.Id));
AssertEx.SetEqual(relatedIds2, ImmutableArray.Create(document1.Id, document2.Id));
document1 = currentSolution.GetRequiredDocument(document1.Id);
document2 = currentSolution.GetRequiredDocument(document2.Id);
var doc1Root = await document1.GetRequiredSyntaxRootAsync(CancellationToken.None);
var doc2Root = await document2.GetRequiredSyntaxRootAsync(CancellationToken.None);
Assert.NotSame(doc1Root, doc2Root);
Assert.False(doc1Root.IsEquivalentTo(doc2Root));
{
// Now get the frozen version of each document. Freezing will update the siblings to have the same tree contents.
var frozenDoc1 = document1.WithFrozenPartialSemantics(CancellationToken.None);
var frozenDoc2 = frozenDoc1.Project.Solution.GetRequiredDocument(document2.Id);
var frozenDoc1Root = await frozenDoc1.GetRequiredSyntaxRootAsync(CancellationToken.None);
var frozenDoc2Root = await frozenDoc2.GetRequiredSyntaxRootAsync(CancellationToken.None);
Assert.NotSame(frozenDoc1Root, frozenDoc2Root);
Assert.True(frozenDoc1Root.IsEquivalentTo(frozenDoc2Root));
// We're seeing project1's view of the file. so we should only see a class decl and no struct decl.
Assert.True(frozenDoc1Root.DescendantNodes().Any(n => n.RawKind == ClassDeclaration));
Assert.False(frozenDoc1Root.DescendantNodes().Any(n => n.RawKind == StructDeclaration));
}
{
// Now get the frozen version of each document. Freezing will update the siblings to have the same tree contents.
var frozenDoc2 = document2.WithFrozenPartialSemantics(CancellationToken.None);
var frozenDoc1 = frozenDoc2.Project.Solution.GetRequiredDocument(document1.Id);
var frozenDoc1Root = await frozenDoc1.GetRequiredSyntaxRootAsync(CancellationToken.None);
var frozenDoc2Root = await frozenDoc2.GetRequiredSyntaxRootAsync(CancellationToken.None);
Assert.NotSame(frozenDoc1Root, frozenDoc2Root);
Assert.True(frozenDoc1Root.IsEquivalentTo(frozenDoc2Root));
// We're seeing project2's view of the file. so we should only see a struct decl and no class decl.
Assert.False(frozenDoc1Root.DescendantNodes().Any(n => n.RawKind == ClassDeclaration));
Assert.True(frozenDoc1Root.DescendantNodes().Any(n => n.RawKind == StructDeclaration));
}
}
[Fact]
public async Task TestProjectCompletenessWithMultipleProjects()
{
GetMultipleProjects(out var csBrokenProject, out var vbNormalProject, out var dependsOnBrokenProject, out var dependsOnVbNormalProject, out var transitivelyDependsOnBrokenProjects, out var transitivelyDependsOnNormalProjects);
// check flag for a broken project itself
Assert.False(await csBrokenProject.HasSuccessfullyLoadedAsync(CancellationToken.None));
// check flag for a normal project itself
Assert.True(await vbNormalProject.HasSuccessfullyLoadedAsync(CancellationToken.None));
// check flag for normal project that directly reference a broken project
Assert.True(await dependsOnBrokenProject.HasSuccessfullyLoadedAsync(CancellationToken.None));
// check flag for normal project that directly reference only normal project
Assert.True(await dependsOnVbNormalProject.HasSuccessfullyLoadedAsync(CancellationToken.None));
// check flag for normal project that indirectly reference a borken project
// normal project -> normal project -> broken project
Assert.True(await transitivelyDependsOnBrokenProjects.HasSuccessfullyLoadedAsync(CancellationToken.None));
// check flag for normal project that indirectly reference only normal project
// normal project -> normal project -> normal project
Assert.True(await transitivelyDependsOnNormalProjects.HasSuccessfullyLoadedAsync(CancellationToken.None));
}
private sealed class TestSmallFileTextLoader : FileTextLoader
{
public TestSmallFileTextLoader(string path, Encoding encoding)
: base(path, encoding)
{
}
// set max file length to 1 byte
internal override int MaxFileLength => 1;
}
[Fact]
public async Task TestMassiveFileSize()
{
using var root = new TempRoot();
var file = root.CreateFile(prefix: "massiveFile", extension: ".cs").WriteAllText("hello");
var loader = new TestSmallFileTextLoader(file.Path, Encoding.UTF8);
var textLength = FileUtilities.GetFileLength(file.Path);
var expected = string.Format(WorkspacesResources.File_0_size_of_1_exceeds_maximum_allowed_size_of_2, file.Path, textLength, 1);
var exceptionThrown = false;
try
{
// test async one
var unused = await loader.LoadTextAndVersionAsync(new LoadTextOptions(SourceHashAlgorithms.Default), CancellationToken.None);
}
catch (InvalidDataException ex)
{
exceptionThrown = true;
Assert.Equal(expected, ex.Message);
}
Assert.True(exceptionThrown);
exceptionThrown = false;
try
{
// test sync one
var unused = loader.LoadTextAndVersionSynchronously(new LoadTextOptions(SourceHashAlgorithms.Default), CancellationToken.None);
}
catch (InvalidDataException ex)
{
exceptionThrown = true;
Assert.Equal(expected, ex.Message);
}
Assert.True(exceptionThrown);
}
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/18697")]
public void TestWithSyntaxTree()
{
// get one to get to syntax tree factory
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var dummyProject = solution.AddProject("dummy", "dummy", LanguageNames.CSharp);
var factory = dummyProject.Services.GetService<ISyntaxTreeFactoryService>();
// create the origin tree
var text = SourceText.From("// empty", encoding: null, SourceHashAlgorithms.Default);
var strongTree = factory.ParseSyntaxTree("dummy", dummyProject.ParseOptions, text, CancellationToken.None);
var sourceText = strongTree.GetText();
}
[Fact]
public void TestUpdateDocumentsOrder()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var pid = ProjectId.CreateNewId();
VersionStamp GetVersion() => solution.GetProject(pid).Version;
ImmutableArray<DocumentId> GetDocumentIds() => [.. solution.GetProject(pid).DocumentIds];
ImmutableArray<SyntaxTree> GetSyntaxTrees()
{
return solution.GetProject(pid).GetCompilationAsync().Result.SyntaxTrees.ToImmutableArray();
}
solution = solution.AddProject(pid, "test", "test.dll", LanguageNames.CSharp);
var text1 = "public class Test1 {}";
var did1 = DocumentId.CreateNewId(pid);
solution = solution.AddDocument(did1, "test1.cs", text1);
var text2 = "public class Test2 {}";
var did2 = DocumentId.CreateNewId(pid);
solution = solution.AddDocument(did2, "test2.cs", text2);
var text3 = "public class Test3 {}";
var did3 = DocumentId.CreateNewId(pid);
solution = solution.AddDocument(did3, "test3.cs", text3);
var text4 = "public class Test4 {}";
var did4 = DocumentId.CreateNewId(pid);
solution = solution.AddDocument(did4, "test4.cs", text4);
var text5 = "public class Test5 {}";
var did5 = DocumentId.CreateNewId(pid);
solution = solution.AddDocument(did5, "test5.cs", text5);
var oldVersion = GetVersion();
solution = solution.WithProjectDocumentsOrder(pid, ImmutableList.CreateRange([did5, did4, did3, did2, did1]));
var newVersion = GetVersion();
// Make sure we have a new version because the order changed.
Assert.NotEqual(oldVersion, newVersion);
var documentIds = GetDocumentIds();
Assert.Equal(did5, documentIds[0]);
Assert.Equal(did4, documentIds[1]);
Assert.Equal(did3, documentIds[2]);
Assert.Equal(did2, documentIds[3]);
Assert.Equal(did1, documentIds[4]);
var syntaxTrees = GetSyntaxTrees();
Assert.Equal(documentIds.Count(), syntaxTrees.Count());
Assert.Equal("test5.cs", syntaxTrees[0].FilePath, StringComparer.OrdinalIgnoreCase);
Assert.Equal("test4.cs", syntaxTrees[1].FilePath, StringComparer.OrdinalIgnoreCase);
Assert.Equal("test3.cs", syntaxTrees[2].FilePath, StringComparer.OrdinalIgnoreCase);
Assert.Equal("test2.cs", syntaxTrees[3].FilePath, StringComparer.OrdinalIgnoreCase);
Assert.Equal("test1.cs", syntaxTrees[4].FilePath, StringComparer.OrdinalIgnoreCase);
solution = solution.WithProjectDocumentsOrder(pid, ImmutableList.CreateRange([did5, did4, did3, did2, did1]));
var newSameVersion = GetVersion();
// Make sure we have the same new version because the order hasn't changed.
Assert.Equal(newVersion, newSameVersion);
}
[Fact]
public void TestUpdateDocumentsOrderExceptions()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var pid = ProjectId.CreateNewId();
solution = solution.AddProject(pid, "test", "test.dll", LanguageNames.CSharp);
var text1 = "public class Test1 {}";
var did1 = DocumentId.CreateNewId(pid);
solution = solution.AddDocument(did1, "test1.cs", text1);
var text2 = "public class Test2 {}";
var did2 = DocumentId.CreateNewId(pid);
solution = solution.AddDocument(did2, "test2.cs", text2);
var text3 = "public class Test3 {}";
var did3 = DocumentId.CreateNewId(pid);
solution = solution.AddDocument(did3, "test3.cs", text3);
var text4 = "public class Test4 {}";
var did4 = DocumentId.CreateNewId(pid);
solution = solution.AddDocument(did4, "test4.cs", text4);
var text5 = "public class Test5 {}";
var did5 = DocumentId.CreateNewId(pid);
solution = solution.AddDocument(did5, "test5.cs", text5);
solution = solution.RemoveDocument(did5);
Assert.Throws<ArgumentException>(() => solution = solution.WithProjectDocumentsOrder(pid, ImmutableList.Create<DocumentId>()));
Assert.Throws<ArgumentNullException>(() => solution = solution.WithProjectDocumentsOrder(pid, null));
Assert.Throws<InvalidOperationException>(() => solution = solution.WithProjectDocumentsOrder(pid, ImmutableList.CreateRange([did5, did3, did2, did1])));
Assert.Throws<ArgumentException>(() => solution = solution.WithProjectDocumentsOrder(pid, ImmutableList.CreateRange([did3, did2, did1])));
}
[Theory, CombinatorialData]
public async Task TestAddingEditorConfigFileWithDiagnosticSeverity([CombinatorialValues(LanguageNames.CSharp, LanguageNames.VisualBasic)] string languageName)
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var extension = languageName == LanguageNames.CSharp ? ".cs" : ".vb";
var projectId = ProjectId.CreateNewId();
var sourceDocumentId = DocumentId.CreateNewId(projectId);
solution = solution.AddProject(projectId, "Test", "Test.dll", languageName);
solution = solution.AddDocument(sourceDocumentId, "Test" + extension, "", filePath: @"Z:\Test" + extension);
var originalSyntaxTree = await solution.GetDocument(sourceDocumentId).GetSyntaxTreeAsync();
var originalCompilation = await solution.GetProject(projectId).GetCompilationAsync();
var editorConfigDocumentId = DocumentId.CreateNewId(projectId);
solution = solution.AddAnalyzerConfigDocuments(ImmutableArray.Create(
DocumentInfo.Create(
editorConfigDocumentId,
".editorconfig",
filePath: @"Z:\.editorconfig",
loader: TextLoader.From(TextAndVersion.Create(SourceText.From("[*.*]\r\n\r\ndotnet_diagnostic.CA1234.severity = error"), VersionStamp.Default)))));
var newSyntaxTree = await solution.GetDocument(sourceDocumentId).GetSyntaxTreeAsync();
var project = solution.GetProject(projectId);
var newCompilation = await project.GetCompilationAsync();
Assert.Same(originalSyntaxTree, newSyntaxTree);
Assert.NotSame(originalCompilation, newCompilation);
Assert.NotEqual(originalCompilation.Options, newCompilation.Options);
var provider = project.CompilationOptions.SyntaxTreeOptionsProvider;
Assert.True(provider.TryGetDiagnosticValue(newSyntaxTree, "CA1234", CancellationToken.None, out var severity));
Assert.Equal(ReportDiagnostic.Error, severity);
}
[Theory, CombinatorialData]
public async Task TestAddingAndRemovingEditorConfigFileWithDiagnosticSeverity([CombinatorialValues(LanguageNames.CSharp, LanguageNames.VisualBasic)] string languageName)
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var extension = languageName == LanguageNames.CSharp ? ".cs" : ".vb";
var projectId = ProjectId.CreateNewId();
var sourceDocumentId = DocumentId.CreateNewId(projectId);
solution = solution.AddProject(projectId, "Test", "Test.dll", languageName);
solution = solution.AddDocument(sourceDocumentId, "Test" + extension, "", filePath: @"Z:\Test" + extension);
var editorConfigDocumentId = DocumentId.CreateNewId(projectId);
solution = solution.AddAnalyzerConfigDocuments(ImmutableArray.Create(
DocumentInfo.Create(
editorConfigDocumentId,
".editorconfig",
filePath: @"Z:\.editorconfig",
loader: TextLoader.From(TextAndVersion.Create(SourceText.From("[*.*]\r\n\r\ndotnet_diagnostic.CA1234.severity = error"), VersionStamp.Default)))));
var syntaxTreeAfterAddingEditorConfig = await solution.GetDocument(sourceDocumentId).GetSyntaxTreeAsync();
var project = solution.GetProject(projectId);
var provider = project.CompilationOptions.SyntaxTreeOptionsProvider;
Assert.True(provider.TryGetDiagnosticValue(syntaxTreeAfterAddingEditorConfig, "CA1234", CancellationToken.None, out var severity));
Assert.Equal(ReportDiagnostic.Error, severity);
solution = solution.RemoveAnalyzerConfigDocument(editorConfigDocumentId);
project = solution.GetProject(projectId);
var syntaxTreeAfterRemovingEditorConfig = await solution.GetDocument(sourceDocumentId).GetSyntaxTreeAsync();
provider = project.CompilationOptions.SyntaxTreeOptionsProvider;
Assert.False(provider.TryGetDiagnosticValue(syntaxTreeAfterAddingEditorConfig, "CA1234", CancellationToken.None, out _));
var finalCompilation = await project.GetCompilationAsync();
Assert.True(finalCompilation.ContainsSyntaxTree(syntaxTreeAfterRemovingEditorConfig));
}
[Theory, CombinatorialData]
public async Task TestChangingAnEditorConfigFile([CombinatorialValues(LanguageNames.CSharp, LanguageNames.VisualBasic)] string languageName)
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var extension = languageName == LanguageNames.CSharp ? ".cs" : ".vb";
var projectId = ProjectId.CreateNewId();
var sourceDocumentId = DocumentId.CreateNewId(projectId);
solution = solution.AddProject(projectId, "Test", "Test.dll", languageName);
solution = solution.AddDocument(sourceDocumentId, "Test" + extension, "", filePath: @"Z:\Test" + extension);
var editorConfigDocumentId = DocumentId.CreateNewId(projectId);
solution = solution.AddAnalyzerConfigDocuments(ImmutableArray.Create(
DocumentInfo.Create(
editorConfigDocumentId,
".editorconfig",
filePath: @"Z:\.editorconfig",
loader: TextLoader.From(TextAndVersion.Create(SourceText.From("[*.*]\r\n\r\ndotnet_diagnostic.CA1234.severity = error"), VersionStamp.Default)))));
var syntaxTreeBeforeEditorConfigChange = await solution.GetDocument(sourceDocumentId).GetSyntaxTreeAsync();
var project = solution.GetProject(projectId);
var provider = project.CompilationOptions.SyntaxTreeOptionsProvider;
Assert.Equal(provider, (await project.GetCompilationAsync()).Options.SyntaxTreeOptionsProvider);
Assert.True(provider.TryGetDiagnosticValue(syntaxTreeBeforeEditorConfigChange, "CA1234", CancellationToken.None, out var severity));
Assert.Equal(ReportDiagnostic.Error, severity);
solution = solution.WithAnalyzerConfigDocumentTextLoader(
editorConfigDocumentId,
TextLoader.From(TextAndVersion.Create(SourceText.From("[*.*]\r\n\r\ndotnet_diagnostic.CA6789.severity = error"), VersionStamp.Default)),
PreservationMode.PreserveValue);
var syntaxTreeAfterEditorConfigChange = await solution.GetDocument(sourceDocumentId).GetSyntaxTreeAsync();
project = solution.GetProject(projectId);
provider = project.CompilationOptions.SyntaxTreeOptionsProvider;
Assert.Equal(provider, (await project.GetCompilationAsync()).Options.SyntaxTreeOptionsProvider);
Assert.True(provider.TryGetDiagnosticValue(syntaxTreeBeforeEditorConfigChange, "CA6789", CancellationToken.None, out severity));
Assert.Equal(ReportDiagnostic.Error, severity);
var finalCompilation = await project.GetCompilationAsync();
Assert.True(finalCompilation.ContainsSyntaxTree(syntaxTreeAfterEditorConfigChange));
}
[Fact]
public void TestAddingAndRemovingGlobalEditorConfigFileWithDiagnosticSeverity()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var projectId = ProjectId.CreateNewId();
var sourceDocumentId = DocumentId.CreateNewId(projectId);
solution = solution.AddProject(projectId, "Test", "Test.dll", LanguageNames.CSharp);
solution = solution.AddDocument(sourceDocumentId, "Test.cs", "", filePath: @"Z:\Test.cs");
var originalProvider = solution.GetProject(projectId).CompilationOptions.SyntaxTreeOptionsProvider;
Assert.False(originalProvider.TryGetGlobalDiagnosticValue("CA1234", default, out _));
var editorConfigDocumentId = DocumentId.CreateNewId(projectId);
solution = solution.AddAnalyzerConfigDocuments(ImmutableArray.Create(
DocumentInfo.Create(
editorConfigDocumentId,
".globalconfig",
filePath: @"Z:\.globalconfig",
loader: TextLoader.From(TextAndVersion.Create(SourceText.From("is_global = true\r\n\r\ndotnet_diagnostic.CA1234.severity = error"), VersionStamp.Default)))));
var newProvider = solution.GetProject(projectId).CompilationOptions.SyntaxTreeOptionsProvider;
Assert.True(newProvider.TryGetGlobalDiagnosticValue("CA1234", default, out var severity));
Assert.Equal(ReportDiagnostic.Error, severity);
solution = solution.RemoveAnalyzerConfigDocument(editorConfigDocumentId);
var finalProvider = solution.GetProject(projectId).CompilationOptions.SyntaxTreeOptionsProvider;
Assert.False(finalProvider.TryGetGlobalDiagnosticValue("CA1234", default, out _));
}
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/3705")]
public async Task TestAddingEditorConfigFileWithIsGeneratedCodeOption()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var projectId = ProjectId.CreateNewId();
var sourceDocumentId = DocumentId.CreateNewId(projectId);
solution = solution.AddProject(projectId, "Test", "Test.dll", LanguageNames.CSharp)
.WithProjectMetadataReferences(projectId, [NetFramework.mscorlib])
.WithProjectCompilationOptions(projectId, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary).WithNullableContextOptions(NullableContextOptions.Enable));
var src = @"
class C
{
void M(C? c)
{
_ = c.ToString(); // warning CS8602: Dereference of a possibly null reference.
}
}";
solution = solution.AddDocument(sourceDocumentId, "Test.cs", src, filePath: @"Z:\Test.cs");
var originalSyntaxTree = await solution.GetDocument(sourceDocumentId).GetSyntaxTreeAsync();
var originalCompilation = await solution.GetProject(projectId).GetCompilationAsync();
// warning CS8602: Dereference of a possibly null reference.
var diagnostics = originalCompilation.GetDiagnostics();
var diagnostic = Assert.Single(diagnostics);
Assert.Equal("CS8602", diagnostic.Id);
var editorConfigDocumentId = DocumentId.CreateNewId(projectId);
solution = solution.AddAnalyzerConfigDocuments(ImmutableArray.Create(
DocumentInfo.Create(
editorConfigDocumentId,
".editorconfig",
filePath: @"Z:\.editorconfig",
loader: TextLoader.From(TextAndVersion.Create(SourceText.From("[*.*]\r\n\r\ngenerated_code = true"), VersionStamp.Default)))));
var newSyntaxTree = await solution.GetDocument(sourceDocumentId).GetSyntaxTreeAsync();
var newCompilation = await solution.GetProject(projectId).GetCompilationAsync();
Assert.Same(originalSyntaxTree, newSyntaxTree);
Assert.NotSame(originalCompilation, newCompilation);
Assert.NotEqual(originalCompilation.Options, newCompilation.Options);
// warning CS8669: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
// Auto-generated code requires an explicit '#nullable' directive in source.
diagnostics = newCompilation.GetDiagnostics();
diagnostic = Assert.Single(diagnostics);
Assert.Contains("CS8669", diagnostic.Id);
}
[Fact]
public void NoCompilationProjectsHaveNullSyntaxTreesAndSemanticModels()
{
using var workspace = CreateWorkspace([typeof(NoCompilationLanguageService)]);
var solution = workspace.CurrentSolution;
var projectId = ProjectId.CreateNewId();
var documentId = DocumentId.CreateNewId(projectId);
solution = solution.AddProject(projectId, "Test", "Test.dll", NoCompilationConstants.LanguageName);
solution = solution.AddDocument(documentId, "Test.cs", "", filePath: @"Z:\Test.txt");
var document = solution.GetDocument(documentId)!;
Assert.False(document.TryGetSyntaxTree(out _));
Assert.Null(document.GetSyntaxTreeAsync().Result);
Assert.Null(document.GetSyntaxTreeSynchronously(CancellationToken.None));
Assert.False(document.TryGetSemanticModel(out _));
Assert.Null(document.GetSemanticModelAsync().Result);
}
[Fact]
public void NoCompilation_SourceCodeKind()
{
using var workspace = CreateWorkspace([typeof(NoCompilationLanguageService)]);
var projectId = ProjectId.CreateNewId();
var documentId = DocumentId.CreateNewId(projectId);
var solution = workspace.CurrentSolution
.AddProject(projectId, "Test", "Test.dll", NoCompilationConstants.LanguageName)
.AddDocument(DocumentInfo.Create(documentId, "Test", sourceCodeKind: SourceCodeKind.Script));
var document1 = solution.GetRequiredDocument(documentId);
Assert.Equal(SourceCodeKind.Script, document1.SourceCodeKind);
Assert.Null(document1.DocumentState.ParseOptions);
var document2 = document1.WithSourceCodeKind(SourceCodeKind.Regular);
Assert.Equal(SourceCodeKind.Regular, document2.SourceCodeKind);
Assert.Null(document2.DocumentState.ParseOptions);
}
[Fact]
public void ChangingFilePathOfFileInNoCompilationProjectWorks()
{
using var workspace = CreateWorkspace([typeof(NoCompilationLanguageService)]);
var solution = workspace.CurrentSolution;
var projectId = ProjectId.CreateNewId();
var documentId = DocumentId.CreateNewId(projectId);
solution = solution.AddProject(projectId, "Test", "Test.dll", NoCompilationConstants.LanguageName);
solution = solution.AddDocument(documentId, "Test.cs", "", filePath: @"Z:\Test.txt");
Assert.Null(solution.GetDocument(documentId)!.GetSyntaxTreeAsync().Result);
solution = solution.WithDocumentFilePath(documentId, @"Z:\NewPath.txt");
Assert.Null(solution.GetDocument(documentId)!.GetSyntaxTreeAsync().Result);
}
[Fact]
public void AddingAndRemovingProjectsUpdatesFilePathMap()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var projectId = ProjectId.CreateNewId();
var editorConfigDocumentId = DocumentId.CreateNewId(projectId);
const string editorConfigFilePath = @"Z:\.editorconfig";
var projectInfo =
ProjectInfo.Create(projectId, VersionStamp.Default, "Test", "Test", LanguageNames.CSharp)
.WithAnalyzerConfigDocuments([DocumentInfo.Create(editorConfigDocumentId, ".editorconfig", filePath: editorConfigFilePath)]);
solution = solution.AddProject(projectInfo);
Assert.Equal(editorConfigDocumentId, Assert.Single(solution.GetDocumentIdsWithFilePath(editorConfigFilePath)));
solution = solution.RemoveProject(projectId);
Assert.Empty(solution.GetDocumentIdsWithFilePath(editorConfigFilePath));
}
[Fact]
public async Task AddMultipleProjects1()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var projectId1 = ProjectId.CreateNewId();
var projectId2 = ProjectId.CreateNewId();
using var _ = ArrayBuilder<ProjectInfo>.GetInstance(out var projects);
projects.Add(ProjectInfo.Create(projectId1, VersionStamp.Default, "Test1", "Test1", LanguageNames.CSharp));
projects.Add(ProjectInfo.Create(projectId2, VersionStamp.Default, "Test2", "Test2", LanguageNames.CSharp));
solution = solution.AddProjects(projects);
Assert.Equal(2, solution.ProjectIds.Count);
Assert.Equal(2, solution.SolutionState.ProjectCountByLanguage[LanguageNames.CSharp]);
var compilation1 = await solution.GetProject(projectId1).GetCompilationAsync();
var compilation2 = await solution.GetProject(projectId2).GetCompilationAsync();
}
[Fact]
public async Task AddMultipleProjects2()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var projectId1 = ProjectId.CreateNewId();
var projectId2 = ProjectId.CreateNewId();
using var _ = ArrayBuilder<ProjectInfo>.GetInstance(out var projects);
// Add a project that has a reference to the project that follows.
projects.Add(ProjectInfo.Create(projectId1, VersionStamp.Default, "Test1", "Test1", LanguageNames.CSharp, projectReferences: [new ProjectReference(projectId2)]));
projects.Add(ProjectInfo.Create(projectId2, VersionStamp.Default, "Test2", "Test2", LanguageNames.CSharp));
solution = solution.AddProjects(projects);
Assert.Equal(2, solution.ProjectIds.Count);
Assert.Equal(2, solution.SolutionState.ProjectCountByLanguage[LanguageNames.CSharp]);
Assert.True(solution.GetProject(projectId1).ProjectReferences.Contains(p => p.ProjectId == projectId2));
var compilation1 = await solution.GetProject(projectId1).GetCompilationAsync();
var compilation2 = await solution.GetProject(projectId2).GetCompilationAsync();
Assert.True(compilation1.References.Any(r => r is CompilationReference compilationReference && compilationReference.Compilation == compilation2));
}
[Fact]
public async Task AddMultipleProjects3()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var projectId1 = ProjectId.CreateNewId();
var projectId2 = ProjectId.CreateNewId();
using var _ = ArrayBuilder<ProjectInfo>.GetInstance(out var projects);
// Add a project that has a reference to the project that precedes it.
projects.Add(ProjectInfo.Create(projectId1, VersionStamp.Default, "Test1", "Test1", LanguageNames.CSharp));
projects.Add(ProjectInfo.Create(projectId2, VersionStamp.Default, "Test2", "Test2", LanguageNames.CSharp, projectReferences: [new ProjectReference(projectId1)]));
solution = solution.AddProjects(projects);
Assert.Equal(2, solution.ProjectIds.Count);
Assert.Equal(2, solution.SolutionState.ProjectCountByLanguage[LanguageNames.CSharp]);
Assert.True(solution.GetProject(projectId2).ProjectReferences.Contains(p => p.ProjectId == projectId1));
var compilation1 = await solution.GetProject(projectId1).GetCompilationAsync();
var compilation2 = await solution.GetProject(projectId2).GetCompilationAsync();
Assert.True(compilation2.References.Any(r => r is CompilationReference compilationReference && compilationReference.Compilation == compilation1));
}
[Fact]
public async Task AddMultipleProjects4()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var projectId1 = ProjectId.CreateNewId();
var projectId2 = ProjectId.CreateNewId();
var projectId3 = ProjectId.CreateNewId();
solution = solution.AddProject(ProjectInfo.Create(projectId1, VersionStamp.Default, "Test1", "Test1", LanguageNames.CSharp));
var compilation1 = await solution.GetProject(projectId1).GetCompilationAsync();
using var _ = ArrayBuilder<ProjectInfo>.GetInstance(out var projects);
projects.Add(ProjectInfo.Create(projectId2, VersionStamp.Default, "Test2", "Test2", LanguageNames.CSharp));
projects.Add(ProjectInfo.Create(projectId3, VersionStamp.Default, "Test3", "Test3", LanguageNames.CSharp));
solution = solution.AddProjects(projects);
Assert.Equal(3, solution.ProjectIds.Count);
Assert.Equal(3, solution.SolutionState.ProjectCountByLanguage[LanguageNames.CSharp]);
var compilation1New = await solution.GetProject(projectId1).GetCompilationAsync();
// These compilations should be the same as adding the later projects should have no impact on the first project.
Assert.Same(compilation1, compilation1New);
}
[Fact]
public async Task AddMultipleProjects5()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var projectId1 = ProjectId.CreateNewId();
var projectId2 = ProjectId.CreateNewId();
var projectId3 = ProjectId.CreateNewId();
// Reference a project that will be added later.
solution = solution.AddProject(ProjectInfo.Create(projectId1, VersionStamp.Default, "Test1", "Test1", LanguageNames.CSharp, projectReferences: [new ProjectReference(projectId2)]));
var compilation1 = await solution.GetProject(projectId1).GetCompilationAsync();
using var _ = ArrayBuilder<ProjectInfo>.GetInstance(out var projects);
// Add a project that has a reference to the project that precedes it.
projects.Add(ProjectInfo.Create(projectId2, VersionStamp.Default, "Test2", "Test2", LanguageNames.CSharp));
projects.Add(ProjectInfo.Create(projectId3, VersionStamp.Default, "Test3", "Test3", LanguageNames.CSharp));
solution = solution.AddProjects(projects);
Assert.Equal(3, solution.ProjectIds.Count);
Assert.Equal(3, solution.SolutionState.ProjectCountByLanguage[LanguageNames.CSharp]);
var compilation1New = await solution.GetProject(projectId1).GetCompilationAsync();
// These compilations should not be the same as adding the later projects should fork the first project.
Assert.NotSame(compilation1, compilation1New);
}
[Fact]
public async Task AddMultipleProjects6()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var projectId1 = ProjectId.CreateNewId();
var projectId2 = ProjectId.CreateNewId();
var projectId3 = ProjectId.CreateNewId();
// Reference both projects that will be added later.
solution = solution.AddProject(ProjectInfo.Create(projectId1, VersionStamp.Default, "Test1", "Test1", LanguageNames.CSharp,
projectReferences: [new ProjectReference(projectId2), new ProjectReference(projectId3)]));
var compilation1 = await solution.GetProject(projectId1).GetCompilationAsync();
using var _ = ArrayBuilder<ProjectInfo>.GetInstance(out var projects);
// Add a project that has a reference to the project that precedes it.
projects.Add(ProjectInfo.Create(projectId2, VersionStamp.Default, "Test2", "Test2", LanguageNames.CSharp));
projects.Add(ProjectInfo.Create(projectId3, VersionStamp.Default, "Test3", "Test3", LanguageNames.CSharp));
solution = solution.AddProjects(projects);
Assert.Equal(3, solution.ProjectIds.Count);
Assert.Equal(3, solution.SolutionState.ProjectCountByLanguage[LanguageNames.CSharp]);
var compilation1New = await solution.GetProject(projectId1).GetCompilationAsync();
// These compilations should not be the same as adding the later projects should fork the first project.
Assert.NotSame(compilation1, compilation1New);
var compilation2 = await solution.GetProject(projectId2).GetCompilationAsync();
var compilation3 = await solution.GetProject(projectId2).GetCompilationAsync();
Assert.True(compilation1New.References.Any(r => r is CompilationReference compilationReference && compilationReference.Compilation == compilation2));
Assert.True(compilation1New.References.Any(r => r is CompilationReference compilationReference && compilationReference.Compilation == compilation3));
}
[Fact]
public void AddMultipleProjects_ThrowOnDuplicateId()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var projectId1 = ProjectId.CreateNewId();
using var _ = ArrayBuilder<ProjectInfo>.GetInstance(out var projects);
// Add a project that has a reference to the project that precedes it.
projects.Add(ProjectInfo.Create(projectId1, VersionStamp.Default, "Test2", "Test2", LanguageNames.CSharp));
projects.Add(ProjectInfo.Create(projectId1, VersionStamp.Default, "Test3", "Test3", LanguageNames.CSharp));
Assert.Throws<InvalidOperationException>(() => solution.AddProjects(projects));
}
[Fact]
public async Task RemoveMultipleProjects1()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var projectId1 = ProjectId.CreateNewId();
var projectId2 = ProjectId.CreateNewId();
var projectId3 = ProjectId.CreateNewId();
using var _ = ArrayBuilder<ProjectInfo>.GetInstance(out var projects);
// Add a project that has a reference to the later projects
projects.Add(ProjectInfo.Create(projectId1, VersionStamp.Default, "Test1", "Test1", LanguageNames.CSharp,
projectReferences: [new ProjectReference(projectId2), new ProjectReference(projectId3)]));
projects.Add(ProjectInfo.Create(projectId2, VersionStamp.Default, "Test2", "Test2", LanguageNames.CSharp));
projects.Add(ProjectInfo.Create(projectId3, VersionStamp.Default, "Test3", "Test3", LanguageNames.CSharp));
solution = solution.AddProjects(projects);
Assert.Equal(3, solution.ProjectIds.Count);
Assert.Equal(3, solution.SolutionState.ProjectCountByLanguage[LanguageNames.CSharp]);
var compilation1 = await solution.GetProject(projectId1).GetCompilationAsync();
var compilation2 = await solution.GetProject(projectId2).GetCompilationAsync();
var compilation3 = await solution.GetProject(projectId2).GetCompilationAsync();
Assert.True(compilation1.References.Any(r => r is CompilationReference compilationReference && compilationReference.Compilation == compilation2));
Assert.True(compilation1.References.Any(r => r is CompilationReference compilationReference && compilationReference.Compilation == compilation3));
using var _2 = ArrayBuilder<ProjectId>.GetInstance(out var projectsToRemove);
projectsToRemove.Add(projectId2);
projectsToRemove.Add(projectId3);
solution = solution.RemoveProjects(projectsToRemove);
Assert.Equal(1, solution.ProjectIds.Count);
Assert.Equal(1, solution.SolutionState.ProjectCountByLanguage[LanguageNames.CSharp]);
Assert.Equal(projectId1, solution.ProjectIds.Single());
var compilation1New = await solution.Projects.Single().GetCompilationAsync();
Assert.NotSame(compilation1, compilation1New);
Assert.True(compilation1New.References.All(r => r is not CompilationReference));
}
[Fact]
public async Task RemoveMultipleProjects2()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var projectId1 = ProjectId.CreateNewId();
var projectId2 = ProjectId.CreateNewId();
var projectId3 = ProjectId.CreateNewId();
using var _ = ArrayBuilder<ProjectInfo>.GetInstance(out var projects);
// All projects are independent
projects.Add(ProjectInfo.Create(projectId1, VersionStamp.Default, "Test1", "Test1", LanguageNames.CSharp));
projects.Add(ProjectInfo.Create(projectId2, VersionStamp.Default, "Test2", "Test2", LanguageNames.CSharp));
projects.Add(ProjectInfo.Create(projectId3, VersionStamp.Default, "Test3", "Test3", LanguageNames.CSharp));
solution = solution.AddProjects(projects);
Assert.Equal(3, solution.ProjectIds.Count);
Assert.Equal(3, solution.SolutionState.ProjectCountByLanguage[LanguageNames.CSharp]);
var compilation1 = await solution.GetProject(projectId1).GetCompilationAsync();
var compilation2 = await solution.GetProject(projectId2).GetCompilationAsync();
var compilation3 = await solution.GetProject(projectId2).GetCompilationAsync();
Assert.False(compilation1.References.Any(r => r is CompilationReference));
using var _2 = ArrayBuilder<ProjectId>.GetInstance(out var projectsToRemove);
projectsToRemove.Add(projectId2);
projectsToRemove.Add(projectId3);
solution = solution.RemoveProjects(projectsToRemove);
Assert.Equal(1, solution.ProjectIds.Count);
Assert.Equal(1, solution.SolutionState.ProjectCountByLanguage[LanguageNames.CSharp]);
Assert.Equal(projectId1, solution.ProjectIds.Single());
var compilation1New = await solution.Projects.Single().GetCompilationAsync();
// Removing project2 and project3 should not change project1 here.
Assert.Same(compilation1, compilation1New);
}
[Fact]
public void RemoveMultipleProjects_ThrowOnDuplicateId()
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution;
var projectId1 = ProjectId.CreateNewId();
var projectId2 = ProjectId.CreateNewId();
using var _ = ArrayBuilder<ProjectInfo>.GetInstance(out var projects);
// Add a project that has a reference to the project that precedes it.
projects.Add(ProjectInfo.Create(projectId1, VersionStamp.Default, "Test2", "Test2", LanguageNames.CSharp));
projects.Add(ProjectInfo.Create(projectId2, VersionStamp.Default, "Test3", "Test3", LanguageNames.CSharp));
solution = solution.AddProjects(projects);
using var _2 = ArrayBuilder<ProjectId>.GetInstance(out var projectsToRemove);
projectsToRemove.Add(projectId1);
projectsToRemove.Add(projectId1);
Assert.Throws<InvalidOperationException>(() => solution.RemoveProjects(projectsToRemove));
}
[Fact]
public void AddAndRemoveProjectsUpdatesLanguageCount()
{
using var workspace = CreateWorkspace(additionalParts: [typeof(NoCompilationLanguageService)]);
var solution = workspace.CurrentSolution;
var csProjectId = ProjectId.CreateNewId();
var vbProjectId = ProjectId.CreateNewId();
solution = solution.AddProjects(
[
ProjectInfo.Create(csProjectId, VersionStamp.Default, "CS1", "CS1", LanguageNames.CSharp),
ProjectInfo.Create(vbProjectId, VersionStamp.Default, "VB1", "VB1", LanguageNames.VisualBasic)
]);
solution = solution
.AddProject("CS2", "CS2", LanguageNames.CSharp).Solution
.AddProject("NC1", "NC1", NoCompilationConstants.LanguageName).Solution;
AssertEx.SetEqual(
[
(LanguageNames.CSharp, 2),
(LanguageNames.VisualBasic, 1),
(NoCompilationConstants.LanguageName, 1)
], solution.SolutionState.ProjectCountByLanguage.Select(e => (e.Key, e.Value)));
solution = solution.RemoveProject(csProjectId);
AssertEx.SetEqual(
[
(LanguageNames.CSharp, 1),
(LanguageNames.VisualBasic, 1),
(NoCompilationConstants.LanguageName, 1)
], solution.SolutionState.ProjectCountByLanguage.Select(e => (e.Key, e.Value)));
solution = solution.RemoveProject(vbProjectId);
AssertEx.SetEqual(
[
(LanguageNames.CSharp, 1),
(NoCompilationConstants.LanguageName, 1)
], solution.SolutionState.ProjectCountByLanguage.Select(e => (e.Key, e.Value)));
solution = solution.RemoveProject(solution.Projects.Single(p => p.Name == "CS2").Id);
AssertEx.SetEqual(
[
(NoCompilationConstants.LanguageName, 1)
], solution.SolutionState.ProjectCountByLanguage.Select(e => (e.Key, e.Value)));
solution = solution.RemoveProject(solution.Projects.Single(p => p.Name == "NC1").Id);
Assert.Empty(solution.SolutionState.ProjectCountByLanguage);
}
private static void GetMultipleProjects(
out Project csBrokenProject,
out Project vbNormalProject,
out Project dependsOnBrokenProject,
out Project dependsOnVbNormalProject,
out Project transitivelyDependsOnBrokenProjects,
out Project transitivelyDependsOnNormalProjects)
{
var workspace = new AdhocWorkspace();
csBrokenProject = workspace.AddProject(
ProjectInfo.Create(
ProjectId.CreateNewId(),
VersionStamp.Create(),
"CSharpProject",
"CSharpProject",
LanguageNames.CSharp,
metadataReferences: [MscorlibRef]).WithHasAllInformation(hasAllInformation: false));
vbNormalProject = workspace.AddProject(
ProjectInfo.Create(
ProjectId.CreateNewId(),
VersionStamp.Create(),
"VisualBasicProject",
"VisualBasicProject",
LanguageNames.VisualBasic,
metadataReferences: [MscorlibRef]));
dependsOnBrokenProject = workspace.AddProject(
ProjectInfo.Create(
ProjectId.CreateNewId(),
VersionStamp.Create(),
"VisualBasicProject",
"VisualBasicProject",
LanguageNames.VisualBasic,
metadataReferences: [MscorlibRef],
projectReferences: [new ProjectReference(csBrokenProject.Id), new ProjectReference(vbNormalProject.Id)]));
dependsOnVbNormalProject = workspace.AddProject(
ProjectInfo.Create(
ProjectId.CreateNewId(),
VersionStamp.Create(),
"CSharpProject",
"CSharpProject",
LanguageNames.CSharp,
metadataReferences: [MscorlibRef],
projectReferences: [new ProjectReference(vbNormalProject.Id)]));
transitivelyDependsOnBrokenProjects = workspace.AddProject(
ProjectInfo.Create(
ProjectId.CreateNewId(),
VersionStamp.Create(),
"CSharpProject",
"CSharpProject",
LanguageNames.CSharp,
metadataReferences: [MscorlibRef],
projectReferences: [new ProjectReference(dependsOnBrokenProject.Id)]));
transitivelyDependsOnNormalProjects = workspace.AddProject(
ProjectInfo.Create(
ProjectId.CreateNewId(),
VersionStamp.Create(),
"VisualBasicProject",
"VisualBasicProject",
LanguageNames.VisualBasic,
metadataReferences: [MscorlibRef],
projectReferences: [new ProjectReference(dependsOnVbNormalProject.Id)]));
}
[Fact]
public void TestOptionChangesForLanguagesNotInSolution()
{
// Create an empty solution with no projects.
using var workspace = CreateWorkspace();
var s0 = workspace.CurrentSolution;
var optionService = workspace.Services.GetRequiredService<ILegacyWorkspaceOptionService>().LegacyGlobalOptions;
// Apply an option change to a C# option.
var option = FormattingOptions.UseTabs;
var defaultValue = option.DefaultValue;
var changedValue = !defaultValue;
var options = s0.Options.WithChangedOption(option, LanguageNames.CSharp, changedValue);
// Verify option change is preserved even if the solution has no project with that language.
var s1 = s0.WithOptions(options);
VerifyOptionSet(s1.Options);
// Verify option value is preserved on adding a project for a different language.
var s2 = s1.AddProject("P1", "A1", LanguageNames.VisualBasic).Solution;
VerifyOptionSet(s2.Options);
// Verify option value is preserved on removing a project.
var s4 = s2.RemoveProject(s2.Projects.Single(p => p.Name == "P1").Id);
VerifyOptionSet(s4.Options);
return;
void VerifyOptionSet(OptionSet optionSet)
{
Assert.Equal(changedValue, optionSet.GetOption(option, LanguageNames.CSharp));
Assert.Equal(defaultValue, optionSet.GetOption(option, LanguageNames.VisualBasic));
}
}
[Fact]
public async Task TestUpdatedDocumentTextIsObservablyConstantAsync()
{
using var workspace = CreateWorkspace();
var pid = ProjectId.CreateNewId();
var text = SourceText.From("public class C { }");
var version = VersionStamp.Create();
var docInfo = DocumentInfo.Create(DocumentId.CreateNewId(pid), "c.cs", loader: TextLoader.From(TextAndVersion.Create(text, version)));
var projInfo = ProjectInfo.Create(
pid,
version: VersionStamp.Default,
name: "TestProject",
assemblyName: "TestProject.dll",
language: LanguageNames.CSharp,
documents: [docInfo]);
var solution = workspace.CurrentSolution.AddProject(projInfo);
var doc = solution.GetDocument(docInfo.Id);
// change document
var root = await doc.GetSyntaxRootAsync();
var newRoot = root.WithAdditionalAnnotations(new SyntaxAnnotation());
Assert.NotSame(root, newRoot);
var newDoc = doc.Project.Solution.WithDocumentSyntaxRoot(doc.Id, newRoot).GetDocument(doc.Id);
Assert.NotSame(doc, newDoc);
var newDocText = await newDoc.GetTextAsync();
var sameText = await newDoc.GetTextAsync();
Assert.Same(newDocText, sameText);
var newDocTree = await newDoc.GetSyntaxTreeAsync();
var treeText = newDocTree.GetText();
Assert.Same(newDocText, treeText);
}
[Fact]
public async Task ReplacingTextMultipleTimesDoesNotRootIntermediateCopiesIfCompilationNotAskedFor()
{
// This test replicates the pattern of some operation changing a bunch of files, but the files aren't kept open.
// In Visual Studio we do large refactorings by opening files with an invisible editor, making changes, and closing
// again. This process means we'll queue up intermediate changes to those files, but we don't want to hold onto
// the intermediate edits when we don't really need to since the final version will be all that matters.
using var workspace = CreateWorkspaceWithProjectAndDocuments();
var solution = workspace.CurrentSolution;
var documentId = solution.Projects.Single().DocumentIds.Single();
// Fetch the compilation, so further edits are going to be incremental updates of this one
var originalCompilation = await solution.Projects.Single().GetCompilationAsync();
// Create a source text we'll release and ensure it disappears. We'll also make sure we don't accidentally root
// that solution in the middle.
var sourceTextToRelease = ObjectReference.CreateFromFactory(static () => SourceText.From(Guid.NewGuid().ToString()));
var solutionWithSourceTextToRelease = sourceTextToRelease.GetObjectReference(
static (sourceText, document) => document.Project.Solution.WithDocumentText(document.Id, sourceText, PreservationMode.PreserveIdentity),
solution.GetDocument(documentId));
// Change it again, this time by editing the text loader; this replicates us closing a file, and we don't want to pin the changes from the
// prior change.
var finalSolution = solutionWithSourceTextToRelease.GetObjectReference(
static (s, documentId) => s.WithDocumentTextLoader(documentId, new TestTextLoader(Guid.NewGuid().ToString()), PreservationMode.PreserveValue), documentId).GetReference();
// The text in the middle shouldn't be held at all, since we replaced it.
solutionWithSourceTextToRelease.ReleaseStrongReference();
sourceTextToRelease.AssertReleased();
GC.KeepAlive(finalSolution);
}
[Theory]
[InlineData("a/proj.csproj", "a/.editorconfig", "a/b/test.cs", "*.cs", true, true)]
[InlineData("a/proj.csproj", "a/b/.editorconfig", "a/b/test.cs", "*.cs", false, true)]
[InlineData("a/proj.csproj", "a/.editorconfig", null, "*.cs", true, true)]
[InlineData("a/proj.csproj", "a/b/.editorconfig", null, "*.cs", false, false)]
[InlineData("a/proj.csproj", "a/.editorconfig", "", "*.cs", true, true)]
[InlineData("a/proj.csproj", "a/b/.editorconfig", "", "*.cs", false, false)]
[InlineData(null, "a/.editorconfig", "a/b/test.cs", "*.cs", false, true)]
[InlineData(null, "a/.editorconfig", null, "*.cs", false, false)]
[InlineData("a/proj.csproj", "a/.editorconfig", null, "*test.cs", false, true)]
public async Task EditorConfigOptions(string projectPath, string configPath, string sourcePath, string pattern, bool appliedToEntireProject, bool appliedToDocument)
{
projectPath = string.IsNullOrEmpty(projectPath) ? projectPath : Path.Combine(TempRoot.Root, projectPath);
configPath = Path.Combine(TempRoot.Root, configPath);
sourcePath = string.IsNullOrEmpty(sourcePath) ? sourcePath : Path.Combine(TempRoot.Root, sourcePath);
using var workspace = CreateWorkspace();
var projectId = ProjectId.CreateNewId();
var projectInfo = ProjectInfo.Create(
projectId,
VersionStamp.Default,
name: "proj1",
assemblyName: "proj1.dll",
language: LanguageNames.CSharp,
filePath: projectPath);
var documentId = DocumentId.CreateNewId(projectId);
var solution = workspace.CurrentSolution
.AddProject(projectInfo)
.AddDocument(documentId, "test.cs", SourceText.From("public class C { }"), filePath: sourcePath)
.AddAnalyzerConfigDocument(DocumentId.CreateNewId(projectId), ".editorconfig", SourceText.From($"[{pattern}]\nindent_style = tab"), filePath: configPath);
var document = solution.GetRequiredDocument(documentId);
#pragma warning disable RS0030 // Do not used banned APIs
var documentOptions = await document.GetOptionsAsync(CancellationToken.None);
Assert.Equal(appliedToDocument, documentOptions.GetOption(FormattingOptions.UseTabs));
#pragma warning restore
var syntaxTree = await document.GetSyntaxTreeAsync();
var documentOptionsViaSyntaxTree = document.Project.State.ProjectAnalyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(syntaxTree);
Assert.Equal(appliedToDocument, documentOptionsViaSyntaxTree.TryGetValue("indent_style", out var value) == true && value == "tab");
var projectOptions = document.Project.GetAnalyzerConfigOptions();
Assert.Equal(appliedToEntireProject, projectOptions?.ConfigOptionsWithoutFallback.TryGetValue("indent_style", out value) == true && value == "tab");
Assert.Equal(appliedToEntireProject, projectOptions?.ConfigOptionsWithFallback.TryGetValue("indent_style", out value) == true && value == "tab");
}
[Fact]
public void GetRelatedDocumentsDoesNotReturnOtherTypesOfDocuments()
{
using var workspace = CreateWorkspace();
const string FilePath = "File.cs";
var solution = workspace.CurrentSolution
.AddProject("TestProject", "TestProject", LanguageNames.CSharp)
.AddDocument("File.cs", "", filePath: FilePath).Project
.AddAdditionalDocument("File.cs", text: "", filePath: FilePath).Project.Solution;
// GetDocumentIdsWithFilePath should return two, since it'll count all types of documents
Assert.Equal(2, solution.GetDocumentIdsWithFilePath(FilePath).Length);
var regularDocumentId = solution.Projects.Single().DocumentIds.Single();
Assert.Single(solution.GetRelatedDocumentIds(regularDocumentId));
}
[Theory, CombinatorialData]
public void GetRelatedDocumentsCaseInsensitive(
[CombinatorialValues("file", "File", "FILE", "FiLe")] string prefix,
[CombinatorialValues("cs", "Cs", "cS", "CS")] string extension)
{
using var workspace = CreateWorkspace();
var solution = workspace.CurrentSolution
.AddProject("TestProject1", "TestProject1", LanguageNames.CSharp)
.AddDocument("File.cs", "", filePath: "File.cs").Project.Solution
.AddProject("TestProject2", "TestProject2", LanguageNames.CSharp)
.AddDocument("file.cs", "", filePath: "file.cs").Project.Solution;
// GetDocumentIdsWithFilePath should return two, since it'll count all types of documents
Assert.Equal(2, solution.GetDocumentIdsWithFilePath($"{prefix}.{extension}").Length);
}
[Fact]
public async Task TestFrozenPartialSolution1()
{
using var workspace = WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics();
var project = workspace.CurrentSolution.AddProject("CSharpProject", "CSharpProject", LanguageNames.CSharp);
project = project.AddDocument("Extra.cs", SourceText.From("class Extra { }")).Project;
// Because we froze before ever even looking at anything semantics related, we should have no documents in
// this project.
var frozenSolution = project.Solution.WithFrozenPartialCompilations(CancellationToken.None);
var frozenProject = frozenSolution.Projects.Single();
Assert.Empty(frozenProject.Documents);
var frozenCompilation = await frozenProject.GetCompilationAsync();
Assert.Empty(frozenCompilation.SyntaxTrees);
}
[Fact]
public async Task TestFrozenPartialSolution2()
{
using var workspace = WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics();
var project = workspace.CurrentSolution.AddProject("CSharpProject", "CSharpProject", LanguageNames.CSharp);
project = project.AddDocument("Extra.cs", SourceText.From("class Extra { }")).Project;
await project.GetCompilationAsync();
// Because we froze after looking at anything semantics related, we should have the documents in this
// project.
var frozenSolution = project.Solution.WithFrozenPartialCompilations(CancellationToken.None);
var frozenProject = frozenSolution.Projects.Single();
Assert.Single(frozenProject.Documents);
var frozenCompilation = await frozenProject.GetCompilationAsync();
Assert.Single(frozenCompilation.SyntaxTrees);
Assert.True(frozenCompilation.ContainsSyntaxTree(await frozenProject.Documents.Single().GetSyntaxTreeAsync()));
}
[Fact]
public async Task TestFrozenPartialSolution3()
{
using var workspace = WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics();
var project1 = workspace.CurrentSolution.AddProject("CSharpProject1", "CSharpProject1", LanguageNames.CSharp);
var project2 = project1.Solution.AddProject("CSharpProject2", "CSharpProject2", LanguageNames.CSharp);
project1 = project2.Solution.GetProject(project1.Id).AddDocument("Doc1", SourceText.From("class Doc1 { }")).Project;
project2 = project1.Solution.GetProject(project2.Id).AddDocument("Doc2", SourceText.From("class Doc2 { }")).Project;
project1 = project2.Solution.GetProject(project1.Id);
// Getting compilation from one should not affect frozen-ness of other project.
await project1.GetCompilationAsync();
var frozenSolution = project1.Solution.WithFrozenPartialCompilations(CancellationToken.None);
var frozenProject1 = frozenSolution.GetProject(project1.Id);
Assert.Single(frozenProject1.Documents);
var frozenProject2 = frozenSolution.GetProject(project2.Id);
Assert.Empty(frozenProject2.Documents);
var frozenCompilation1 = await frozenProject1.GetCompilationAsync();
Assert.Single(frozenCompilation1.SyntaxTrees);
Assert.True(frozenCompilation1.ContainsSyntaxTree(await frozenProject1.Documents.Single().GetSyntaxTreeAsync()));
var frozenCompilation2 = await frozenProject2.GetCompilationAsync();
Assert.Empty(frozenCompilation2.SyntaxTrees);
}
[Fact]
public async Task TestFrozenPartialSolution4()
{
using var workspace = WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics();
var project1 = workspace.CurrentSolution.AddProject("CSharpProject1", "CSharpProject1", LanguageNames.CSharp);
var project2 = project1.Solution.AddProject("CSharpProject2", "CSharpProject2", LanguageNames.CSharp);
project1 = project2.Solution.GetProject(project1.Id).AddDocument("Doc1", SourceText.From("class Doc1 { }")).Project;
project2 = project1.Solution.GetProject(project2.Id).AddDocument("Doc2", SourceText.From("class Doc2 { }")).Project;
project2 = project2.AddProjectReference(new(project1.Id));
project1 = project2.Solution.GetProject(project1.Id);
// Getting compilation from project1 should not affect project 2.
await project1.GetCompilationAsync();
var frozenSolution = project1.Solution.WithFrozenPartialCompilations(CancellationToken.None);
var frozenProject1 = frozenSolution.GetProject(project1.Id);
Assert.Single(frozenProject1.Documents);
var frozenCompilation1 = await frozenProject1.GetCompilationAsync();
Assert.Single(frozenCompilation1.SyntaxTrees);
Assert.True(frozenCompilation1.ContainsSyntaxTree(await frozenProject1.Documents.Single().GetSyntaxTreeAsync()));
var frozenProject2 = frozenSolution.GetProject(project2.Id);
Assert.Empty(frozenProject2.Documents);
var frozenCompilation2 = await frozenProject2.GetCompilationAsync();
Assert.Empty(frozenCompilation2.SyntaxTrees);
}
[Fact]
public async Task TestFrozenPartialSolution5()
{
using var workspace = WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics();
var project1 = workspace.CurrentSolution.AddProject("CSharpProject1", "CSharpProject1", LanguageNames.CSharp);
var project2 = project1.Solution.AddProject("CSharpProject2", "CSharpProject2", LanguageNames.CSharp);
project1 = project2.Solution.GetProject(project1.Id).AddDocument("Doc1", SourceText.From("class Doc1 { }")).Project;
project2 = project1.Solution.GetProject(project2.Id).AddDocument("Doc2", SourceText.From("class Doc2 { }")).Project;
project2 = project2.AddProjectReference(new(project1.Id));
project1 = project2.Solution.GetProject(project1.Id);
// Getting compilation from project2 should affect project 1 as there's a ptp relationship with it.
await project2.GetCompilationAsync();
var frozenSolution = project1.Solution.WithFrozenPartialCompilations(CancellationToken.None);
var frozenProject1 = frozenSolution.GetProject(project1.Id);
Assert.Single(frozenProject1.Documents);
var frozenCompilation1 = await frozenProject1.GetCompilationAsync();
Assert.Single(frozenCompilation1.SyntaxTrees);
Assert.True(frozenCompilation1.ContainsSyntaxTree(await frozenProject1.Documents.Single().GetSyntaxTreeAsync()));
var frozenProject2 = frozenSolution.GetProject(project2.Id);
Assert.Single(frozenProject2.Documents);
var frozenCompilation2 = await frozenProject2.GetCompilationAsync();
Assert.Single(frozenCompilation2.SyntaxTrees);
Assert.True(frozenCompilation2.ContainsSyntaxTree(await frozenProject2.Documents.Single().GetSyntaxTreeAsync()));
Assert.Single(frozenCompilation2.References, r => r is CompilationReference c && c.Compilation == frozenCompilation1);
}
[Fact]
public async Task TestFrozenPartialSolution6()
{
using var workspace = WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics();
var project1 = workspace.CurrentSolution.AddProject("CSharpProject1", "CSharpProject1", LanguageNames.CSharp);
project1 = project1.AddDocument("Doc1", SourceText.From("class Doc1 { }")).Project;
// If we freeze after getting the compilation, we should see those documents then show up in frozen
// compilation as well.
var compilation1 = await project1.GetCompilationAsync();
var syntaxTree1 = await project1.Documents.Single().GetSyntaxTreeAsync();
var frozenSolution = project1.Solution.WithFrozenPartialCompilations(CancellationToken.None);
var forkedProject1 = frozenSolution.WithDocumentText(project1.Documents.Single().Id, SourceText.From("class Doc2 { }")).GetProject(project1.Id);
var forkedDocument1 = forkedProject1.Documents.Single();
var forkedSyntaxTree1 = await forkedDocument1.GetSyntaxTreeAsync();
Assert.NotEqual(syntaxTree1, forkedSyntaxTree1);
var forkedCompilation1 = await forkedProject1.GetCompilationAsync();
Assert.NotEqual(compilation1, forkedCompilation1);
Assert.True(forkedCompilation1.ContainsSyntaxTree(forkedSyntaxTree1));
}
[Theory, CombinatorialData]
public async Task TestFrozenPartialSolution7(bool freeze)
{
using var workspace = WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics();
var project1 = workspace.CurrentSolution
.AddProject("CSharpProject1", "CSharpProject1", LanguageNames.CSharp)
.WithCompilationOutputInfo(new CompilationOutputInfo(assemblyPath: Path.Combine(TempRoot.Root, "assembly.dll"), generatedFilesOutputDirectory: null));
project1 = project1.AddDocument("Doc1", SourceText.From("class Doc1 { }")).Project;
var invokeIndex = 1;
project1 = project1.AddAnalyzerReference(new TestGeneratorReference(new CallbackGenerator(() =>
{
var index = invokeIndex++;
return ("hintname" + index, "// source" + index);
})));
var compilation1 = await project1.GetCompilationAsync();
var syntaxTree1 = await project1.Documents.Single().GetSyntaxTreeAsync();
var generatedDocuments = await project1.GetSourceGeneratedDocumentsAsync();
Assert.Single(generatedDocuments);
Assert.Equal("// source1", generatedDocuments.Single().GetTextSynchronously(CancellationToken.None).ToString());
var frozenSolution = freeze
? project1.Solution.WithFrozenPartialCompilations(CancellationToken.None)
: project1.Solution;
var forkedProject1 = frozenSolution.WithDocumentText(project1.Documents.Single().Id, SourceText.From("class Doc2 { }")).GetProject(project1.Id);
var forkedDocument1 = forkedProject1.Documents.Single();
var forkedSyntaxTree1 = await forkedDocument1.GetSyntaxTreeAsync();
var forkedGeneratedDocuments = await forkedProject1.GetSourceGeneratedDocumentsAsync();
Assert.Single(forkedGeneratedDocuments);
Assert.NotEqual(syntaxTree1, forkedSyntaxTree1);
var forkedCompilation1 = await forkedProject1.GetCompilationAsync();
Assert.NotEqual(compilation1, forkedCompilation1);
Assert.True(forkedCompilation1.ContainsSyntaxTree(forkedSyntaxTree1));
Assert.NotSame(generatedDocuments.Single(), forkedGeneratedDocuments.Single());
if (freeze)
{
Assert.Equal("// source1", forkedGeneratedDocuments.Single().GetTextSynchronously(CancellationToken.None).ToString());
}
else
{
Assert.Equal("// source2", forkedGeneratedDocuments.Single().GetTextSynchronously(CancellationToken.None).ToString());
}
}
[Fact]
public async Task TestFrozenPartialSolutionOtherLanguage()
{
using var workspace = WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics();
var project = workspace.CurrentSolution.AddProject("TypeScript", "TypeScript", "TypeScript");
project = project.AddDocument("Extra.ts", SourceText.From("class Extra { }")).Project;
// Freeze should have no impact on non-c#/vb projects.
var frozenSolution = project.Solution.WithFrozenPartialCompilations(CancellationToken.None);
var frozenProject = frozenSolution.Projects.Single();
Assert.Single(frozenProject.Documents);
var frozenCompilation = await frozenProject.GetCompilationAsync();
Assert.Null(frozenCompilation);
}
[Theory]
[InlineData(1000)]
[InlineData(2000)]
[InlineData(4000)]
[InlineData(8000)]
public async Task TestLargeLinkedFileChain(int intermediatePullCount)
{
using var workspace = CreateWorkspace();
var project1 = workspace.CurrentSolution
.AddProject($"Project1", $"Project1", LanguageNames.CSharp)
.WithParseOptions(CSharpParseOptions.Default.WithPreprocessorSymbols("DEBUG"))
.AddDocument($"Document", SourceText.From("class C { }"), filePath: @"c:\test\Document.cs").Project;
var documentId1 = project1.DocumentIds.Single();
// make another project, give a separate set of pp directives, so that we do *not* try to use the sibling
// root (from project1), but instead incrementally parse using the *contents* of the file in project1 again
// our actual tree. This used to stack overflow since we'd create a long chain of incremental parsing steps
// for each edit made to the sibling file.
var project2 = project1.Solution
.AddProject($"Project2", $"Project2", LanguageNames.CSharp)
.WithParseOptions(CSharpParseOptions.Default.WithPreprocessorSymbols("RELEASE"))
.AddDocument($"Document", SourceText.From("class C { }"), filePath: @"c:\test\Document.cs").Project;
var documentId2 = project2.DocumentIds.Single();
workspace.SetCurrentSolution(
_ => project2.Solution,
(_, _) => (WorkspaceChangeKind.SolutionAdded, null, null));
for (var i = 1; i <= 8000; i++)
{
var lastContents = $"#if true //{new string('.', i)}//";
workspace.SetCurrentSolution(
old => old.WithDocumentText(documentId1, SourceText.From(lastContents)),
(_, _) => (WorkspaceChangeKind.DocumentChanged, documentId1.ProjectId, documentId1));
// Ensure that the first document is fine, and we're not stack overflowing on simply getting the tree
// from it. Do this on a disparate cadence from our pulls of the second document to ensure we are
// testing the case where we haven't necessarily immediately pulled on hte first doc before pulling on
// the second.
if (i % 33 == 0)
{
var document1 = workspace.CurrentSolution.GetRequiredDocument(documentId1);
await document1.GetSyntaxRootAsync();
}
if (i % intermediatePullCount == 0)
{
var document2 = workspace.CurrentSolution.GetRequiredDocument(documentId2);
// Getting the second document should both be fine, and have contents equivalent to what is in the first document.
var root = await document2.GetSyntaxRootAsync();
Assert.Equal(lastContents, root.ToFullString());
}
}
}
}
}
|