File: Workspaces\TestWorkspace_XmlConsumption.cs
Web Access
Project: src\src\Workspaces\CoreTestUtilities\Microsoft.CodeAnalysis.Workspaces.Test.Utilities.csproj (Microsoft.CodeAnalysis.Workspaces.Test.Utilities)
// 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.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Xml.Linq;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.DecompiledSource;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.UnitTests.Extensions;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.VisualBasic;
using Microsoft.VisualStudio.Composition;
using Roslyn.Test.Utilities;
using Roslyn.Test.Utilities.TestGenerators;
using Roslyn.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.Test.Utilities
{
    public partial class TestWorkspace<TDocument, TProject, TSolution>
    {
        private const string CSharpExtension = ".cs";
        private const string CSharpScriptExtension = ".csx";
        private const string VisualBasicExtension = ".vb";
        private const string VisualBasicScriptExtension = ".vbx";
 
        private const string WorkspaceElementName = "Workspace";
        private const string ProjectElementName = "Project";
        private const string SubmissionElementName = "Submission";
        private const string MetadataReferenceElementName = "MetadataReference";
        private const string MetadataReferenceFromSourceElementName = "MetadataReferenceFromSource";
        private const string ProjectReferenceElementName = "ProjectReference";
        private const string CompilationOptionsElementName = "CompilationOptions";
        private const string RootNamespaceAttributeName = "RootNamespace";
        private const string OutputTypeAttributeName = "OutputType";
        private const string ReportDiagnosticAttributeName = "ReportDiagnostic";
        private const string CryptoKeyFileAttributeName = "CryptoKeyFile";
        private const string StrongNameProviderAttributeName = "StrongNameProvider";
        private const string DelaySignAttributeName = "DelaySign";
        private const string ParseOptionsElementName = "ParseOptions";
        private const string LanguageVersionAttributeName = "LanguageVersion";
        private const string FeaturesAttributeName = "Features";
        private const string DocumentationModeAttributeName = "DocumentationMode";
        private const string DocumentElementName = "Document";
        private const string AdditionalDocumentElementName = "AdditionalDocument";
        private const string AnalyzerConfigDocumentElementName = "AnalyzerConfigDocument";
        private const string AnalyzerElementName = "Analyzer";
        private const string AssemblyNameAttributeName = "AssemblyName";
        private const string CommonReferencesAttributeName = "CommonReferences";
        private const string CommonReferencesWithoutValueTupleAttributeName = "CommonReferencesWithoutValueTuple";
        private const string CommonReferencesWinRTAttributeName = "CommonReferencesWinRT";
        private const string CommonReferencesNet45AttributeName = "CommonReferencesNet45";
        private const string CommonReferencesPortableAttributeName = "CommonReferencesPortable";
        private const string CommonReferencesNetCoreAppName = "CommonReferencesNetCoreApp";
        private const string CommonReferencesNet6Name = "CommonReferencesNet6";
        private const string CommonReferencesNet7Name = "CommonReferencesNet7";
        private const string CommonReferencesNet8Name = "CommonReferencesNet8";
        private const string CommonReferencesNetStandard20Name = "CommonReferencesNetStandard20";
        private const string CommonReferencesMinCorlibName = "CommonReferencesMinCorlib";
        private const string ReferencesOnDiskAttributeName = "ReferencesOnDisk";
        private const string FilePathAttributeName = "FilePath";
        private const string FoldersAttributeName = "Folders";
        private const string KindAttributeName = "Kind";
        private const string LanguageAttributeName = "Language";
        private const string GlobalImportElementName = "GlobalImport";
        private const string IncludeXmlDocCommentsAttributeName = "IncludeXmlDocComments";
        private const string IsLinkFileAttributeName = "IsLinkFile";
        private const string LinkAssemblyNameAttributeName = "LinkAssemblyName";
        private const string LinkProjectNameAttributeName = "LinkProjectName";
        private const string LinkFilePathAttributeName = "LinkFilePath";
        private const string MarkupAttributeName = "Markup";
        private const string NormalizeAttributeName = "Normalize";
        private const string PreprocessorSymbolsAttributeName = "PreprocessorSymbols";
        private const string AnalyzerDisplayAttributeName = "Name";
        private const string AnalyzerFullPathAttributeName = "FullPath";
        private const string AliasAttributeName = "Alias";
        private const string ProjectNameAttribute = "Name";
        private const string CheckOverflowAttributeName = "CheckOverflow";
        private const string AllowUnsafeAttributeName = "AllowUnsafe";
        private const string OutputKindName = "OutputKind";
        private const string NullableAttributeName = "Nullable";
        private const string DocumentFromSourceGeneratorElementName = "DocumentFromSourceGenerator";
 
        /// <summary>
        /// This place-holder value is used to set a project's file path to be null.  It was explicitly chosen to be
        /// convoluted to avoid any accidental usage (e.g., what if I really wanted FilePath to be the string "null"?),
        /// obvious to anybody debugging that it is a special value, and invalid as an actual file path.
        /// </summary>
        public const string NullFilePath = "NullFilePath::{AFA13775-BB7D-4020-9E58-C68CF43D8A68}";
 
        private class TestDocumentationProvider : DocumentationProvider
        {
            protected override string GetDocumentationForSymbol(string documentationMemberID, CultureInfo preferredCulture, CancellationToken cancellationToken = default)
                => string.Format("<member name='{0}'><summary>{0}</summary></member>", documentationMemberID);
 
            public override bool Equals(object obj)
                => ReferenceEquals(this, obj);
 
            public override int GetHashCode()
                => RuntimeHelpers.GetHashCode(this);
        }
 
        private TProject CreateProject(
            XElement workspaceElement,
            XElement projectElement,
            ExportProvider exportProvider,
            IDocumentServiceProvider documentServiceProvider,
            ref int projectId,
            ref int documentId)
        {
            AssertNoChildText(projectElement);
 
            var language = GetLanguage(projectElement);
 
            var assemblyName = GetAssemblyName(projectElement, ref projectId);
 
            string projectFilePath;
 
            var projectName = projectElement.Attribute(ProjectNameAttribute)?.Value ?? assemblyName;
 
            if (projectElement.Attribute(FilePathAttributeName) != null)
            {
                projectFilePath = projectElement.Attribute(FilePathAttributeName).Value;
                if (string.Compare(projectFilePath, NullFilePath, StringComparison.Ordinal) == 0)
                {
                    // allow explicit null file path
                    projectFilePath = null;
                }
            }
            else
            {
                projectFilePath = projectName +
                    (language == LanguageNames.CSharp ? ".csproj" :
                     language == LanguageNames.VisualBasic ? ".vbproj" : ("." + language));
            }
 
            var projectOutputDir = AbstractTestHostProject.GetTestOutputDirectory(projectFilePath);
 
            var languageServices = Services.GetLanguageServices(language);
 
            var parseOptions = GetParseOptions(projectElement, language, languageServices);
            var compilationOptions = CreateCompilationOptions(projectElement, language, parseOptions);
            var rootNamespace = GetRootNamespace(compilationOptions, projectElement);
 
            var references = CreateReferenceList(projectElement);
            var analyzers = CreateAnalyzerList(projectElement);
 
            var documents = new List<TDocument>();
            var documentElements = projectElement.Elements(DocumentElementName).ToList();
            foreach (var documentElement in documentElements)
            {
                var document = CreateDocument(
                    workspaceElement,
                    documentElement,
                    exportProvider,
                    languageServices,
                    documentServiceProvider,
                    ref documentId);
 
                documents.Add(document);
            }
 
            SingleFileTestGenerator testGenerator = null;
            foreach (var sourceGeneratedDocumentElement in projectElement.Elements(DocumentFromSourceGeneratorElementName))
            {
                if (testGenerator is null)
                {
                    testGenerator = new SingleFileTestGenerator();
                    analyzers.Add(new TestGeneratorReference(testGenerator));
                }
 
                var name = GetFileName(sourceGeneratedDocumentElement, ref documentId);
 
                var markupCode = (bool?)sourceGeneratedDocumentElement.Attribute(NormalizeAttributeName) is false
                    ? sourceGeneratedDocumentElement.Value
                    : sourceGeneratedDocumentElement.NormalizedValue();
                MarkupTestFile.GetPositionAndSpans(markupCode,
                    out var code, out var cursorPosition, out IDictionary<string, ImmutableArray<TextSpan>> spans);
 
                var documentFilePath = Path.Combine(projectOutputDir, "obj", typeof(SingleFileTestGenerator).Assembly.GetName().Name, typeof(SingleFileTestGenerator).FullName, name);
                var document = CreateDocument(exportProvider, languageServices, code, name, documentFilePath, cursorPosition, spans, generator: testGenerator);
                documents.Add(document);
 
                testGenerator.AddSource(code, name);
            }
 
            var additionalDocuments = new List<TDocument>();
            var additionalDocumentElements = projectElement.Elements(AdditionalDocumentElementName).ToList();
            foreach (var additionalDocumentElement in additionalDocumentElements)
            {
                var document = CreateDocument(
                    workspaceElement,
                    additionalDocumentElement,
                    exportProvider,
                    languageServices,
                    documentServiceProvider,
                    ref documentId);
 
                additionalDocuments.Add(document);
            }
 
            var analyzerConfigDocuments = new List<TDocument>();
            var analyzerConfigElements = projectElement.Elements(AnalyzerConfigDocumentElementName).ToList();
            foreach (var analyzerConfigElement in analyzerConfigElements)
            {
                var document = CreateDocument(
                    workspaceElement,
                    analyzerConfigElement,
                    exportProvider,
                    languageServices,
                    documentServiceProvider,
                    ref documentId);
 
                analyzerConfigDocuments.Add(document);
            }
 
            return CreateProject(languageServices, compilationOptions, parseOptions, assemblyName, projectName, references, documents, additionalDocuments, analyzerConfigDocuments, filePath: projectFilePath, analyzerReferences: analyzers, defaultNamespace: rootNamespace);
        }
 
        private static ParseOptions GetParseOptions(XElement projectElement, string language, HostLanguageServices languageServices)
        {
            return language is LanguageNames.CSharp or LanguageNames.VisualBasic
                ? GetParseOptionsWorker(projectElement, language, languageServices)
                : null;
        }
 
        private static ParseOptions GetParseOptionsWorker(XElement projectElement, string language, HostLanguageServices languageServices)
        {
            ParseOptions parseOptions;
            var preprocessorSymbolsAttribute = projectElement.Attribute(PreprocessorSymbolsAttributeName);
            if (preprocessorSymbolsAttribute != null)
            {
                parseOptions = GetPreProcessorParseOptions(language, preprocessorSymbolsAttribute);
            }
            else
            {
                parseOptions = languageServices.GetService<ISyntaxTreeFactoryService>().GetDefaultParseOptions();
            }
 
            var languageVersionAttribute = projectElement.Attribute(LanguageVersionAttributeName);
            if (languageVersionAttribute != null)
            {
                parseOptions = GetParseOptionsWithLanguageVersion(language, parseOptions, languageVersionAttribute);
            }
 
            var featuresAttribute = projectElement.Attribute(FeaturesAttributeName);
            if (featuresAttribute != null)
            {
                parseOptions = GetParseOptionsWithFeatures(parseOptions, featuresAttribute);
            }
 
            var documentationMode = GetDocumentationMode(projectElement);
            if (documentationMode != null)
            {
                parseOptions = parseOptions.WithDocumentationMode(documentationMode.Value);
            }
 
            return parseOptions;
        }
 
        private static ParseOptions GetPreProcessorParseOptions(string language, XAttribute preprocessorSymbolsAttribute)
        {
            if (language == LanguageNames.CSharp)
            {
                return new CSharpParseOptions(preprocessorSymbols: preprocessorSymbolsAttribute.Value.Split(','));
            }
            else if (language == LanguageNames.VisualBasic)
            {
                return new VisualBasicParseOptions(preprocessorSymbols: preprocessorSymbolsAttribute.Value
                    .Split(',').Select(v => KeyValuePairUtil.Create(v.Split('=').ElementAt(0), (object)v.Split('=').ElementAt(1))).ToImmutableArray());
            }
            else
            {
                throw new ArgumentException("Unexpected language '{0}' for generating custom parse options.", language);
            }
        }
 
        private static ParseOptions GetParseOptionsWithFeatures(ParseOptions parseOptions, XAttribute featuresAttribute)
        {
            var entries = featuresAttribute.Value.Split(';');
            var features = entries.Select(x =>
            {
                var split = x.Split('=');
 
                var key = split[0];
                var value = split.Length == 2 ? split[1] : "true";
 
                return KeyValuePairUtil.Create(key, value);
            });
 
            return parseOptions.WithFeatures(features);
        }
 
        private static ParseOptions GetParseOptionsWithLanguageVersion(string language, ParseOptions parseOptions, XAttribute languageVersionAttribute)
        {
            if (language == LanguageNames.CSharp)
            {
                if (CodeAnalysis.CSharp.LanguageVersionFacts.TryParse(languageVersionAttribute.Value, out var languageVersion))
                {
                    return ((CSharpParseOptions)parseOptions).WithLanguageVersion(languageVersion);
                }
            }
            else if (language == LanguageNames.VisualBasic)
            {
                var languageVersion = CodeAnalysis.VisualBasic.LanguageVersion.Default;
                if (CodeAnalysis.VisualBasic.LanguageVersionFacts.TryParse(languageVersionAttribute.Value, ref languageVersion))
                {
                    return ((VisualBasicParseOptions)parseOptions).WithLanguageVersion(languageVersion);
                }
            }
 
            throw new Exception($"LanguageVersion attribute on {languageVersionAttribute.Parent} was not recognized.");
        }
 
        private static DocumentationMode? GetDocumentationMode(XElement projectElement)
        {
            var documentationModeAttribute = projectElement.Attribute(DocumentationModeAttributeName);
            if (documentationModeAttribute != null)
            {
                return (DocumentationMode)Enum.Parse(typeof(DocumentationMode), documentationModeAttribute.Value);
            }
            else
            {
                return null;
            }
        }
 
        private string GetAssemblyName(XElement projectElement, ref int projectId)
        {
            var assemblyNameAttribute = projectElement.Attribute(AssemblyNameAttributeName);
            if (assemblyNameAttribute != null)
            {
                return assemblyNameAttribute.Value;
            }
 
            var language = GetLanguage(projectElement);
 
            projectId++;
            return language == LanguageNames.CSharp ? "CSharpAssembly" + projectId :
                   language == LanguageNames.VisualBasic ? "VisualBasicAssembly" + projectId :
                                                            language + "Assembly" + projectId;
        }
 
        private string GetLanguage(XElement projectElement)
        {
            var languageAttribute = projectElement.Attribute(LanguageAttributeName);
            if (languageAttribute == null)
            {
                throw new ArgumentException($"{projectElement} is missing a {LanguageAttributeName} attribute.");
            }
 
            var languageName = languageAttribute.Value;
 
            if (!Services.SupportedLanguages.Contains(languageName))
            {
                throw new ArgumentException(string.Format("Language should be one of '{0}' and it is {1}",
                    string.Join(", ", Services.SupportedLanguages),
                    languageName));
            }
 
            return languageName;
        }
 
        private string GetRootNamespace(CompilationOptions compilationOptions, XElement projectElement)
        {
            var rootNamespaceAttribute = projectElement.Attribute(RootNamespaceAttributeName);
 
            if (GetLanguage(projectElement) == LanguageNames.VisualBasic)
            {
                // For VB tests, root namespace value must be defined in compilation options element,
                // it can't use the property in project element to avoid confusion.
                Assert.Null(rootNamespaceAttribute);
 
                var vbCompilationOptions = (VisualBasicCompilationOptions)compilationOptions;
                return vbCompilationOptions.RootNamespace;
            }
 
            // If it's not defined, default to "" (global namespace)
            return rootNamespaceAttribute?.Value ?? string.Empty;
        }
 
        private CompilationOptions CreateCompilationOptions(
            XElement projectElement,
            string language,
            ParseOptions parseOptions)
        {
            var compilationOptionsElement = projectElement.Element(CompilationOptionsElementName);
            return language is LanguageNames.CSharp or LanguageNames.VisualBasic
                ? CreateCompilationOptions(language, compilationOptionsElement, parseOptions)
                : null;
        }
 
        private CompilationOptions CreateCompilationOptions(string language, XElement compilationOptionsElement, ParseOptions parseOptions)
        {
            var rootNamespace = new VisualBasicCompilationOptions(OutputKind.ConsoleApplication).RootNamespace;
            var globalImports = new List<GlobalImport>();
            var reportDiagnostic = ReportDiagnostic.Default;
            var cryptoKeyFile = (string)null;
            var strongNameProvider = (StrongNameProvider)null;
            var delaySign = (bool?)null;
            var checkOverflow = false;
            var allowUnsafe = false;
            var outputKind = OutputKind.DynamicallyLinkedLibrary;
            var nullable = NullableContextOptions.Disable;
 
            if (compilationOptionsElement != null)
            {
                globalImports = compilationOptionsElement.Elements(GlobalImportElementName)
                                                         .Select(x => GlobalImport.Parse(x.Value)).ToList();
                var rootNamespaceAttribute = compilationOptionsElement.Attribute(RootNamespaceAttributeName);
                if (rootNamespaceAttribute != null)
                {
                    rootNamespace = rootNamespaceAttribute.Value;
                }
 
                var outputKindAttribute = compilationOptionsElement.Attribute(OutputKindName);
                if (outputKindAttribute != null)
                {
                    outputKind = (OutputKind)Enum.Parse(typeof(OutputKind), outputKindAttribute.Value);
                }
 
                var checkOverflowAttribute = compilationOptionsElement.Attribute(CheckOverflowAttributeName);
                if (checkOverflowAttribute != null)
                {
                    checkOverflow = (bool)checkOverflowAttribute;
                }
 
                var allowUnsafeAttribute = compilationOptionsElement.Attribute(AllowUnsafeAttributeName);
                if (allowUnsafeAttribute != null)
                {
                    allowUnsafe = (bool)allowUnsafeAttribute;
                }
 
                var reportDiagnosticAttribute = compilationOptionsElement.Attribute(ReportDiagnosticAttributeName);
                if (reportDiagnosticAttribute != null)
                {
                    reportDiagnostic = (ReportDiagnostic)Enum.Parse(typeof(ReportDiagnostic), (string)reportDiagnosticAttribute);
                }
 
                var cryptoKeyFileAttribute = compilationOptionsElement.Attribute(CryptoKeyFileAttributeName);
                if (cryptoKeyFileAttribute != null)
                {
                    cryptoKeyFile = (string)cryptoKeyFileAttribute;
                }
 
                var strongNameProviderAttribute = compilationOptionsElement.Attribute(StrongNameProviderAttributeName);
                if (strongNameProviderAttribute != null)
                {
                    var type = Type.GetType((string)strongNameProviderAttribute);
                    // DesktopStrongNameProvider and SigningTestHelpers.VirtualizedStrongNameProvider do
                    // not have a default constructor but constructors with optional parameters.
                    // Activator.CreateInstance does not work with this.
                    if (type == typeof(DesktopStrongNameProvider))
                    {
                        strongNameProvider = SigningTestHelpers.DefaultDesktopStrongNameProvider;
                    }
                    else
                    {
                        strongNameProvider = (StrongNameProvider)Activator.CreateInstance(type);
                    }
                }
 
                var delaySignAttribute = compilationOptionsElement.Attribute(DelaySignAttributeName);
                if (delaySignAttribute != null)
                {
                    delaySign = (bool)delaySignAttribute;
                }
 
                var nullableAttribute = compilationOptionsElement.Attribute(NullableAttributeName);
                if (nullableAttribute != null)
                {
                    nullable = (NullableContextOptions)Enum.Parse(typeof(NullableContextOptions), nullableAttribute.Value);
                }
 
                var outputTypeAttribute = compilationOptionsElement.Attribute(OutputTypeAttributeName);
                if (outputTypeAttribute != null
                    && outputTypeAttribute.Value == "WindowsRuntimeMetadata")
                {
                    if (rootNamespaceAttribute == null)
                    {
                        rootNamespace = new VisualBasicCompilationOptions(OutputKind.WindowsRuntimeMetadata).RootNamespace;
                    }
 
                    // VB needs Compilation.ParseOptions set (we do the same at the VS layer)
                    return language == LanguageNames.CSharp
                       ? new CSharpCompilationOptions(OutputKind.WindowsRuntimeMetadata, allowUnsafe: allowUnsafe)
                       : new VisualBasicCompilationOptions(OutputKind.WindowsRuntimeMetadata).WithGlobalImports(globalImports).WithRootNamespace(rootNamespace)
                            .WithParseOptions((VisualBasicParseOptions)parseOptions ?? VisualBasicParseOptions.Default);
                }
            }
            else
            {
                // Add some common global imports by default for VB
                globalImports.Add(GlobalImport.Parse("System"));
                globalImports.Add(GlobalImport.Parse("System.Collections.Generic"));
                globalImports.Add(GlobalImport.Parse("System.Linq"));
            }
 
            // TODO: Allow these to be specified.
            var languageServices = Services.GetLanguageServices(language);
            var metadataService = Services.GetService<IMetadataService>();
            var compilationOptions = languageServices.GetService<ICompilationFactoryService>().GetDefaultCompilationOptions();
            compilationOptions = compilationOptions.WithOutputKind(outputKind)
                                                   .WithGeneralDiagnosticOption(reportDiagnostic)
                                                   .WithSourceReferenceResolver(SourceFileResolver.Default)
                                                   .WithXmlReferenceResolver(XmlFileResolver.Default)
                                                   .WithMetadataReferenceResolver(new WorkspaceMetadataFileReferenceResolver(metadataService, new RelativePathResolver([], null)))
                                                   .WithAssemblyIdentityComparer(DesktopAssemblyIdentityComparer.Default)
                                                   .WithCryptoKeyFile(cryptoKeyFile)
                                                   .WithStrongNameProvider(strongNameProvider)
                                                   .WithDelaySign(delaySign)
                                                   .WithOverflowChecks(checkOverflow);
 
            if (language == LanguageNames.CSharp)
            {
                compilationOptions = ((CSharpCompilationOptions)compilationOptions).WithAllowUnsafe(allowUnsafe).WithNullableContextOptions(nullable);
            }
 
            if (language == LanguageNames.VisualBasic)
            {
                // VB needs Compilation.ParseOptions set (we do the same at the VS layer)
                compilationOptions = ((VisualBasicCompilationOptions)compilationOptions).WithRootNamespace(rootNamespace)
                                                                                        .WithGlobalImports(globalImports)
                                                                                        .WithParseOptions((VisualBasicParseOptions)parseOptions ??
                                                                                            VisualBasicParseOptions.Default);
            }
 
            return compilationOptions;
        }
 
        private TDocument CreateDocument(
            XElement workspaceElement,
            XElement documentElement,
            ExportProvider exportProvider,
            HostLanguageServices languageServiceProvider,
            IDocumentServiceProvider documentServiceProvider,
            ref int documentId)
        {
            var isLinkFileAttribute = documentElement.Attribute(IsLinkFileAttributeName);
            var isLinkFile = isLinkFileAttribute != null && ((bool?)isLinkFileAttribute).HasValue && ((bool?)isLinkFileAttribute).Value;
            if (isLinkFile)
            {
                // This is a linked file. Use the filePath and markup from the referenced document.
 
                var originalAssemblyName = documentElement.Attribute(LinkAssemblyNameAttributeName)?.Value;
                var originalProjectName = documentElement.Attribute(LinkProjectNameAttributeName)?.Value;
 
                if (originalAssemblyName == null && originalProjectName == null)
                {
                    throw new ArgumentException($"Linked files must specify either a {LinkAssemblyNameAttributeName} or {LinkProjectNameAttributeName}");
                }
 
                var originalProject = workspaceElement.Elements(ProjectElementName).FirstOrDefault(p =>
                {
                    if (originalAssemblyName != null)
                    {
                        return p.Attribute(AssemblyNameAttributeName)?.Value == originalAssemblyName;
                    }
                    else
                    {
                        return p.Attribute(ProjectNameAttribute)?.Value == originalProjectName;
                    }
                });
 
                if (originalProject == null)
                {
                    if (originalProjectName != null)
                    {
                        throw new ArgumentException($"Linked file's {LinkProjectNameAttributeName} '{originalProjectName}' project not found.");
                    }
                    else
                    {
                        throw new ArgumentException($"Linked file's {LinkAssemblyNameAttributeName} '{originalAssemblyName}' project not found.");
                    }
                }
 
                var originalDocumentPath = documentElement.Attribute(LinkFilePathAttributeName)?.Value;
 
                if (originalDocumentPath == null)
                {
                    throw new ArgumentException($"Linked files must specify a {LinkFilePathAttributeName}");
                }
 
                documentElement = originalProject.Elements(DocumentElementName).FirstOrDefault(d =>
                {
                    return d.Attribute(FilePathAttributeName)?.Value == originalDocumentPath;
                });
 
                if (documentElement == null)
                {
                    throw new ArgumentException($"Linked file's LinkFilePath '{originalDocumentPath}' file not found.");
                }
            }
 
            var markupCode = (bool?)documentElement.Attribute(NormalizeAttributeName) is false
                ? documentElement.Value
                : documentElement.NormalizedValue();
 
            var fileName = GetFileName(documentElement, ref documentId);
 
            var folders = GetFolders(documentElement);
            var optionsElement = documentElement.Element(ParseOptionsElementName);
 
            // TODO: Allow these to be specified.
            var codeKind = SourceCodeKind.Regular;
            if (optionsElement != null)
            {
                var attr = optionsElement.Attribute(KindAttributeName);
                codeKind = attr == null
                    ? SourceCodeKind.Regular
                    : (SourceCodeKind)Enum.Parse(typeof(SourceCodeKind), attr.Value);
            }
 
            var markupAttribute = documentElement.Attribute(MarkupAttributeName);
            var isMarkup = markupAttribute == null || (string)markupAttribute == "true" || (string)markupAttribute == "SpansOnly";
 
            string code;
            int? cursorPosition;
            ImmutableDictionary<string, ImmutableArray<TextSpan>> spans;
 
            if (isMarkup)
            {
                // if the caller doesn't want us caring about positions, then replace any $'s with a character unlikely
                // to ever show up in the doc naturally.  Then, after we convert things, change that character back. We
                // do this as a single character so that all the positions of the spans do not change.
                if ((string)markupAttribute == "SpansOnly")
                    markupCode = markupCode.Replace("$", "\uD7FF");
 
                TestFileMarkupParser.GetPositionAndSpans(markupCode, out code, out cursorPosition, out spans);
 
                // if we were told SpansOnly then that means that $$ isn't actually a caret (but is something like a raw
                // interpolated string delimiter.  In that case, if we did see a $$ add it back it at the location we
                // found it, and set the cursor back to null as the test will be specifying that location manually
                // itself.
                if ((string)markupAttribute == "SpansOnly")
                {
                    Contract.ThrowIfTrue(cursorPosition != null);
                    code = code.Replace("\uD7FF", "$");
                }
            }
            else
            {
                code = markupCode;
                cursorPosition = null;
                spans = ImmutableDictionary<string, ImmutableArray<TextSpan>>.Empty;
            }
 
            var testDocumentServiceProvider = GetDocumentServiceProvider(documentElement);
 
            if (documentServiceProvider == null)
            {
                documentServiceProvider = testDocumentServiceProvider;
            }
            else if (testDocumentServiceProvider != null)
            {
                AssertEx.Fail($"The document attributes on file {fileName} conflicted");
            }
 
            return CreateDocument(
                exportProvider, languageServiceProvider, code, fileName, fileName, cursorPosition, spans, codeKind, folders, isLinkFile, documentServiceProvider);
        }
#nullable enable
 
        private static TestDocumentServiceProvider? GetDocumentServiceProvider(XElement documentElement)
        {
            var canApplyChange = (bool?)documentElement.Attribute("CanApplyChange");
            var supportDiagnostics = (bool?)documentElement.Attribute("SupportDiagnostics");
 
            if (canApplyChange == null && supportDiagnostics == null)
            {
                return null;
            }
 
            return new TestDocumentServiceProvider(
                canApplyChange ?? true,
                supportDiagnostics ?? true);
        }
 
#nullable disable
 
        private string GetFileName(XElement documentElement, ref int documentId)
        {
            var filePathAttribute = documentElement.Attribute(FilePathAttributeName);
            if (filePathAttribute != null)
            {
                return filePathAttribute.Value;
            }
 
            var language = GetLanguage(documentElement.Ancestors(ProjectElementName).Single());
            documentId++;
            var name = "Test" + documentId;
            return language == LanguageNames.CSharp ? name + ".cs" : name + ".vb";
        }
 
        private static IReadOnlyList<string> GetFolders(XElement documentElement)
        {
            var folderAttribute = documentElement.Attribute(FoldersAttributeName);
            if (folderAttribute == null)
            {
                return null;
            }
 
            var folderContainers = folderAttribute.Value.Split(new[] { PathUtilities.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);
            return new ReadOnlyCollection<string>(folderContainers.ToList());
        }
 
        /// <summary>
        /// Takes completely valid code, compiles it, and emits it to a MetadataReference without using
        /// the file system
        /// </summary>
        private MetadataReference CreateMetadataReferenceFromSource(XElement projectElement, XElement referencedSource)
        {
            var compilation = CreateCompilation(referencedSource);
 
            var aliasElement = referencedSource.Attribute("Aliases")?.Value;
            var aliases = aliasElement != null ? aliasElement.Split(',').Select(s => s.Trim()).ToImmutableArray() : default;
 
            var includeXmlDocComments = false;
            var includeXmlDocCommentsAttribute = referencedSource.Attribute(IncludeXmlDocCommentsAttributeName);
            if (includeXmlDocCommentsAttribute != null &&
                ((bool?)includeXmlDocCommentsAttribute).HasValue &&
                ((bool?)includeXmlDocCommentsAttribute).Value)
            {
                includeXmlDocComments = true;
            }
 
            var referencesOnDisk = projectElement.Attribute(ReferencesOnDiskAttributeName) is { } onDiskAttribute
                && ((bool?)onDiskAttribute).GetValueOrDefault();
 
            var image = compilation.EmitToArray();
            var metadataReference = MetadataReference.CreateFromImage(image, new MetadataReferenceProperties(aliases: aliases), includeXmlDocComments ? new DeferredDocumentationProvider(compilation) : null);
            if (referencesOnDisk)
            {
                AssemblyResolver.TestAccessor.AddInMemoryImage(metadataReference, "unknown", image);
            }
 
            return metadataReference;
        }
 
        private Compilation CreateCompilation(XElement referencedSource)
        {
            AssertNoChildText(referencedSource);
 
            var languageName = GetLanguage(referencedSource);
 
            var assemblyName = "ReferencedAssembly";
            var assemblyNameAttribute = referencedSource.Attribute(AssemblyNameAttributeName);
            if (assemblyNameAttribute != null)
            {
                assemblyName = assemblyNameAttribute.Value;
            }
 
            var languageServices = Services.GetLanguageServices(languageName);
            var compilationFactory = languageServices.GetService<ICompilationFactoryService>();
            var options = compilationFactory.GetDefaultCompilationOptions().WithOutputKind(OutputKind.DynamicallyLinkedLibrary);
 
            var compilation = compilationFactory.CreateCompilation(assemblyName, options);
 
            var documentElements = referencedSource.Elements(DocumentElementName).ToList();
            var parseOptions = GetParseOptions(referencedSource, languageName, languageServices);
 
            foreach (var documentElement in documentElements)
            {
                compilation = compilation.AddSyntaxTrees(CreateSyntaxTree(parseOptions, documentElement.Value));
            }
 
            foreach (var reference in CreateReferenceList(referencedSource))
            {
                compilation = compilation.AddReferences(reference);
            }
 
            return compilation;
        }
 
        private static SyntaxTree CreateSyntaxTree(ParseOptions options, string referencedCode)
        {
            var sourceText = SourceText.From(referencedCode, encoding: null, SourceHashAlgorithms.Default);
 
            if (LanguageNames.CSharp == options.Language)
            {
                return Microsoft.CodeAnalysis.CSharp.SyntaxFactory.ParseSyntaxTree(sourceText, options);
            }
            else
            {
                return Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory.ParseSyntaxTree(sourceText, options);
            }
        }
 
        private IList<MetadataReference> CreateReferenceList(XElement element)
        {
            var references = CreateCommonReferences(element);
            foreach (var reference in element.Elements(MetadataReferenceElementName))
            {
                // Read the image to an ImmutableArray<byte>, since the GC does a better job of tracking these than
                // Marshal.AllocHGlobal and thus knowing when it's necessary to run finalizers to clean up old Metadata
                // objects that are no longer in use. There are no public APIs available to directly dispose of these
                // images, so we are relying on GC running finalizers to avoid OutOfMemoryException during tests.
                var content = File.ReadAllBytes(reference.Value);
                references.Add(MetadataReference.CreateFromImage(content, filePath: reference.Value));
            }
 
            foreach (var metadataReferenceFromSource in element.Elements(MetadataReferenceFromSourceElementName))
            {
                references.Add(CreateMetadataReferenceFromSource(element, metadataReferenceFromSource));
            }
 
            return references;
        }
 
        private static IList<AnalyzerReference> CreateAnalyzerList(XElement projectElement)
        {
            var analyzers = new List<AnalyzerReference>();
            foreach (var analyzer in projectElement.Elements(AnalyzerElementName))
            {
                analyzers.Add(
                    new AnalyzerImageReference(
                        [],
                        display: (string)analyzer.Attribute(AnalyzerDisplayAttributeName),
                        fullPath: (string)analyzer.Attribute(AnalyzerFullPathAttributeName)));
            }
 
            return analyzers;
        }
 
        private IList<MetadataReference> CreateCommonReferences(XElement element)
        {
            var references = new List<MetadataReference>();
 
            var net45 = element.Attribute(CommonReferencesNet45AttributeName);
            if (net45 != null &&
                ((bool?)net45).HasValue &&
                ((bool?)net45).Value)
            {
                references = [TestBase.MscorlibRef_v4_0_30316_17626, TestBase.SystemRef_v4_0_30319_17929, TestBase.SystemCoreRef_v4_0_30319_17929, TestBase.SystemRuntimeSerializationRef_v4_0_30319_17929];
                if (GetLanguage(element) == LanguageNames.VisualBasic)
                {
                    references.Add(TestBase.MsvbRef);
                    references.Add(TestBase.SystemXmlRef);
                    references.Add(TestBase.SystemXmlLinqRef);
                }
            }
 
            var commonReferencesAttribute = element.Attribute(CommonReferencesAttributeName);
            if (commonReferencesAttribute != null &&
                ((bool?)commonReferencesAttribute).HasValue &&
                ((bool?)commonReferencesAttribute).Value)
            {
                references = [TestBase.MscorlibRef_v46, TestBase.SystemRef_v46, TestBase.SystemCoreRef_v46, TestBase.ValueTupleRef, TestBase.SystemRuntimeFacadeRef];
                if (GetLanguage(element) == LanguageNames.VisualBasic)
                {
                    references.Add(TestBase.MsvbRef_v4_0_30319_17929);
                    references.Add(TestBase.SystemXmlRef);
                    references.Add(TestBase.SystemXmlLinqRef);
                }
            }
 
            var commonReferencesWithoutValueTupleAttribute = element.Attribute(CommonReferencesWithoutValueTupleAttributeName);
            if (commonReferencesWithoutValueTupleAttribute != null &&
                ((bool?)commonReferencesWithoutValueTupleAttribute).HasValue &&
                ((bool?)commonReferencesWithoutValueTupleAttribute).Value)
            {
                references = [TestBase.MscorlibRef_v46, TestBase.SystemRef_v46, TestBase.SystemCoreRef_v46];
            }
 
            var winRT = element.Attribute(CommonReferencesWinRTAttributeName);
            if (winRT != null &&
                ((bool?)winRT).HasValue &&
                ((bool?)winRT).Value)
            {
                references = new List<MetadataReference>(TestBase.WinRtRefs.Length);
                references.AddRange(TestBase.WinRtRefs);
                if (GetLanguage(element) == LanguageNames.VisualBasic)
                {
                    references.Add(TestBase.MsvbRef_v4_0_30319_17929);
                    references.Add(TestBase.SystemXmlRef);
                    references.Add(TestBase.SystemXmlLinqRef);
                }
            }
 
            var portable = element.Attribute(CommonReferencesPortableAttributeName);
            if (portable != null &&
                ((bool?)portable).HasValue &&
                ((bool?)portable).Value)
            {
                references = new List<MetadataReference>(TestBase.PortableRefsMinimal.Length);
                references.AddRange(TestBase.PortableRefsMinimal);
            }
 
            var netcore30 = element.Attribute(CommonReferencesNetCoreAppName);
            if (netcore30 != null &&
                ((bool?)netcore30).HasValue &&
                ((bool?)netcore30).Value)
            {
                references = [.. NetCoreApp.References];
            }
 
            var netstandard20 = element.Attribute(CommonReferencesNetStandard20Name);
            if (netstandard20 != null &&
                ((bool?)netstandard20).HasValue &&
                ((bool?)netstandard20).Value)
            {
                references = [.. TargetFrameworkUtil.NetStandard20References];
            }
 
            var net6 = element.Attribute(CommonReferencesNet6Name);
            if (net6 != null &&
                ((bool?)net6).HasValue &&
                ((bool?)net6).Value)
            {
                references = [.. TargetFrameworkUtil.GetReferences(TargetFramework.Net60)];
            }
 
            var net7 = element.Attribute(CommonReferencesNet7Name);
            if (net7 != null &&
                ((bool?)net7).HasValue &&
                ((bool?)net7).Value)
            {
                references = [.. TargetFrameworkUtil.GetReferences(TargetFramework.Net70)];
            }
 
            var net8 = element.Attribute(CommonReferencesNet8Name);
            if (net8 != null &&
                ((bool?)net8).HasValue &&
                ((bool?)net8).Value)
            {
                references = [.. TargetFrameworkUtil.GetReferences(TargetFramework.Net80)];
            }
 
            var mincorlib = element.Attribute(CommonReferencesMinCorlibName);
            if (mincorlib != null &&
                ((bool?)mincorlib).HasValue &&
                ((bool?)mincorlib).Value)
            {
                references = [TestBase.MinCorlibRef];
            }
 
            return references;
        }
 
        public static bool IsWorkspaceElement(string text)
            => text.TrimStart('\r', '\n', ' ').StartsWith("<Workspace>", StringComparison.Ordinal);
 
        private static void AssertNoChildText(XElement element)
        {
            foreach (var node in element.Nodes())
            {
                if (node is XText text && !string.IsNullOrWhiteSpace(text.Value))
                {
                    throw new Exception($"Element {element} has child text that isn't recognized. The XML syntax is invalid.");
                }
            }
        }
    }
}